Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Karlie/web snippet #891

Merged
merged 19 commits into from
Mar 30, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions AutoCollection/WebSnippet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import * as path from "path";
import http = require("http");
import fs = require('fs');

import Logging = require("../Library/Logging");
import TelemetryClient = require("../Library/TelemetryClient");

class WebSnippet {

public static INSTANCE: WebSnippet;

private static _snippet: string;
private static _aiUrl: string;
private _isEnabled: boolean;
private _isInitialized: boolean;

constructor(client: TelemetryClient) {
if (!!WebSnippet.INSTANCE) {
throw new Error("Web snippet injection should be configured from the applicationInsights object");
Karlie-777 marked this conversation as resolved.
Show resolved Hide resolved
}

WebSnippet.INSTANCE = this;
// AI URL used to validate if snippet already included
//WebSnippet._aiUrl = "https://az416426.vo.msecnd.net/scripts/b/ai.2";
WebSnippet._aiUrl = " https://js.monitor.azure.com/scripts/b/ai.2";
Karlie-777 marked this conversation as resolved.
Show resolved Hide resolved

let snippetPath = path.resolve(__dirname, "../../AutoCollection/WebSnippet/snippet.min.js");
if (client.config.isDebugWebSnippet) {
snippetPath = path.resolve(__dirname, "../../AutoCollection/WebSnippet/snippet.js");
}
fs.readFile(snippetPath, function (err, snippet) {
Karlie-777 marked this conversation as resolved.
Show resolved Hide resolved
if (err) {
Logging.warn("Failed to load AI Web snippet. Ex:" + err);
}
WebSnippet._snippet = snippet.toString().replace("INSTRUMENTATION_KEY", client.config.instrumentationKey);
});
}

public enable(isEnabled: boolean) {
Karlie-777 marked this conversation as resolved.
Show resolved Hide resolved
this._isEnabled = isEnabled;

if (this._isEnabled && !this._isInitialized) {
this._initialize();
}
}

public isInitialized() {
return this._isInitialized;
}

private _initialize() {
this._isInitialized = true;
ramthi marked this conversation as resolved.
Show resolved Hide resolved
const originalHttpServer = http.createServer;
http.createServer = (requestListener?: (request: http.IncomingMessage, response: http.ServerResponse) => void) => {
const originalRequestListener = requestListener;
if (originalRequestListener) {
requestListener = (request: http.IncomingMessage, response: http.ServerResponse) => {
// Patch response write method
let originalResponseWrite = response.write;
let isEnabled = this._isEnabled;
response.write = function wrap(a: Buffer | string, b?: Function | string, c?: Function | string) {
if (isEnabled) {
if (WebSnippet.ValidateInjection(response, a)) {
arguments[0] = WebSnippet.InjectWebSnippet(response, a);
}
}
return originalResponseWrite.apply(response, arguments);
}
// Patch response end method for cases when HTML is added there
let originalResponseEnd = response.end;

response.end = function wrap(a?: Buffer | string | any, b?: Function | string, c?: Function) {
if (isEnabled) {
if (WebSnippet.ValidateInjection(response, a)) {
arguments[0] = WebSnippet.InjectWebSnippet(response, a);
}
}
originalResponseEnd.apply(response, arguments);
}

originalRequestListener(request, response);
}
}
return originalHttpServer(requestListener);
}
}

/**
* Validate response and try to inject Web snippet
*/
public static ValidateInjection(response: http.ServerResponse, input: string | Buffer): boolean {
Karlie-777 marked this conversation as resolved.
Show resolved Hide resolved
if (response && input) {
// insert snippet if only response returns 200
if (response.statusCode != 200) return false;
let contentType = response.getHeader('Content-Type');
Karlie-777 marked this conversation as resolved.
Show resolved Hide resolved
let contentEncoding = response.getHeader('Content-Encoding'); // Compressed content not supported for injection
if (!contentEncoding && contentType && typeof contentType == "string" && contentType.toLowerCase().indexOf("text/html") >= 0) {
let html = input.toString();
if (html.indexOf("<head>") >= 0 && html.indexOf("</head>") >= 0) {
// Check if snippet not already present looking for AI Web SDK URL
if (html.indexOf(WebSnippet._aiUrl) < 0) {
return true;
}
}
}
}
return false;
}

/**
* Inject Web snippet
*/
public static InjectWebSnippet(response: http.ServerResponse, input: string | Buffer): string | Buffer {
try {
let hasContentHeader = !!response.getHeader('Content-Length');
Karlie-777 marked this conversation as resolved.
Show resolved Hide resolved
// Clean content-length header
if (hasContentHeader) {
response.removeHeader('Content-Length');
}
// Read response stream
let html = input.toString();
// Try to add script before HTML head closure
let index = html.indexOf("</head>");
if (index >= 0) {
let subStart = html.substring(0, index);
let subEnd = html.substring(index);
input = subStart + '<script type="text/javascript">' + WebSnippet._snippet + '</script>' + subEnd;
html = subStart + '<script type="text/javascript">' + WebSnippet._snippet + '</script>' + subEnd;
Karlie-777 marked this conversation as resolved.
Show resolved Hide resolved
// Set headers
if (hasContentHeader) {
response.setHeader("Content-Length", input.length.toString());
Karlie-777 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
catch (ex) {
Logging.info("Failed to change content-lenght headers for JS injection. Exception:" + ex);
Karlie-777 marked this conversation as resolved.
Show resolved Hide resolved
}
return input;
}

public dispose() {
WebSnippet.INSTANCE = null;
this.enable(false);
this._isInitialized = false;
}
}

export = WebSnippet;
26 changes: 26 additions & 0 deletions AutoCollection/WebSnippet/fetchSnippet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const path = require('path');
Karlie-777 marked this conversation as resolved.
Show resolved Hide resolved
const fs = require('fs');
const https = require('https');

let snippetUrl = 'https://raw.githubusercontent.com/microsoft/ApplicationInsights-JS/master/AISKU/snippet/snippet.js';
Karlie-777 marked this conversation as resolved.
Show resolved Hide resolved
let snippetMinUrl = 'https://raw.githubusercontent.com/microsoft/ApplicationInsights-JS/master/AISKU/snippet/snippet.min.js';

let snippetPath = path.resolve(__dirname, "snippet.js");
var snippet = fs.createWriteStream(snippetPath);
https.get(snippetUrl, function (res) {
res.on('data', function (data) {
snippet.write(data);
}).on('end', function () {
snippet.end();
});
});

let minSnippetPath = path.resolve(__dirname, "snippet.min.js");
var minSnippet = fs.createWriteStream(minSnippetPath);
https.get(snippetMinUrl, function (res) {
res.on('data', function (data) {
minSnippet.write(data);
}).on('end', function () {
minSnippet.end();
});
});
Loading