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

Improve XHR so that it cooperates better with other instrumentations #34

Merged
merged 3 commits into from
May 7, 2019
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion lib/__mocks__/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const doc = win.document;
export const nav = global.navigator;

export const xhrRequests = [];
export const OriginalXMLHttpRequest = function() {
export const XMLHttpRequest = function() {
const requestFunctions = {};
// Using the prototype chain to avoid adding mock functions to the jest snapshots.
const request = Object.create(requestFunctions);
Expand Down
3 changes: 2 additions & 1 deletion lib/__mocks__/vars.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export default {
reportingUrl: 'http://eum.example.com',
xhrTransmissionTimeout: 5432,
beaconBatchingTime: 0
beaconBatchingTime: 0,
secretPropertyKey: '__secret__'
};
2 changes: 1 addition & 1 deletion lib/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const win: any = window;
export const doc: any = win.document;
export const nav: any = navigator;
export const encodeURIComponent: string => string = win.encodeURIComponent;
export const OriginalXMLHttpRequest = win.XMLHttpRequest;
export const XMLHttpRequest = win.XMLHttpRequest;
export const originalFetch = win.fetch;

/**
Expand Down
215 changes: 103 additions & 112 deletions lib/hooks/XMLHttpRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import {createExcessiveUsageIdentifier} from '../excessiveUsageIdentification';
import {addCommonBeaconProperties} from '../commonBeaconProperties';
import {isWhitelistedOrigin} from '../whitelistedOrigins';
import {OriginalXMLHttpRequest, win} from '../browser';
import {sendBeacon} from '../transmission/index';
import {now, generateUniqueId} from '../util';
import {normalizeUrl} from './normalizeUrl';
import {isUrlIgnored} from '../ignoreRules';
import {win, XMLHttpRequest} from '../browser';
import type {XhrBeacon} from '../types';
import {debug, info} from '../debug';
import {isSameOrigin} from '../sop';
Expand Down Expand Up @@ -42,56 +42,97 @@ const additionalStatuses = {

const traceIdHeaderRegEx = /^X-INSTANA-T$/i;

export function disableMonitoringForXMLHttpRequest(xhr: any): void {
const state = xhr[vars.secretPropertyKey] = xhr[vars.secretPropertyKey] || {};
state.ignored = true;
}

export function instrumentXMLHttpRequest() {
if (!OriginalXMLHttpRequest ||
!(new OriginalXMLHttpRequest()).addEventListener) {
if (!XMLHttpRequest ||
!(new XMLHttpRequest()).addEventListener) {
if (DEBUG) {
info('Browser does not support the features required for XHR instrumentation.');
}
return;
}

function InstrumentedXMLHttpRequest() {
const xhr = new OriginalXMLHttpRequest();
const originalOpen = XMLHttpRequest.prototype.open;
const originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
const originalSend = XMLHttpRequest.prototype.send;

XMLHttpRequest.prototype.open = function open(method, url, async) {
const xhr = this;

if (isExcessiveUsage()) {
if (DEBUG) {
info('Reached the maximum number of XMLHttpRequests to monitor.');
}
return xhr;

return originalOpen.apply(xhr, arguments);
}

const state = xhr[vars.secretPropertyKey] = xhr[vars.secretPropertyKey] || {};
// probably ignored due to disableMonitoringForXMLHttpRequest calls
if (state.ignored) {
return originalOpen.apply(xhr, arguments);
}

state.ignored = isUrlIgnored(url);
if (state.ignored) {
if (DEBUG) {
debug(
'Not generating XHR beacon because it should be ignored according to user configuration. URL: ' + url
);
}
return originalOpen.apply(xhr, arguments);
}

const originalOpen = xhr.open;
const originalSetRequestHeader = xhr.setRequestHeader;
const originalSend = xhr.send;
state.spanAndTraceId = generateUniqueId();
state.setBackendCorrelationHeaders = isSameOrigin(url) || isWhitelistedOrigin(url);

// $FlowFixMe: Some properties deliberately left our for js file size reasons.
const beacon: XhrBeacon = {
'ty': 'xhr',

// general beacon data
// 't': '',
't': state.spanAndTraceId,
's': state.spanAndTraceId,
'ts': 0,
'd': 0,

// xhr beacon specific data
// 's': '',
'l': win.location.href,
'm': '',
'u': '',
'a': 1,
'm': method,
'u': normalizeUrl(url),
'a': async === undefined || async ? 1 : 0,
'st': 0,
'e': undefined
'e': undefined,
'bc': state.setBackendCorrelationHeaders ? 1 : 0
};

// Whether or not we should ignore this beacon, e.g. because the URL is ignored.
let ignored = false;

let spanAndTraceId;
let setBackendCorrelationHeaders = false;
state.beacon = beacon;

try {
const result = originalOpen.apply(xhr, arguments);
xhr.addEventListener('timeout', onTimeout);
xhr.addEventListener('error', onError);
xhr.addEventListener('abort', onAbort);
xhr.addEventListener('readystatechange', onReadystatechange);
return result;
} catch (e) {
beacon['ts'] = now() - vars.referenceTimestamp;
beacon['st'] = additionalStatuses.openError;
beacon['e'] = e.message;
addCommonBeaconProperties(beacon);
sendBeacon(beacon);
xhr[vars.secretPropertyKey] = null;
throw e;
}

function onFinish(status) {
if (ignored) return;
if (state.ignored) {
return;
}

if (beacon['st'] !== 0) {
// Multiple finish events. Should only happen when we setup the event handlers
Expand All @@ -112,31 +153,27 @@ export function instrumentXMLHttpRequest() {
sendBeacon(beacon);
}

xhr.addEventListener('timeout', function onTimeout() {
if (ignored) return;

function onTimeout() {
onFinish(additionalStatuses.timeout);
});
}

xhr.addEventListener('error', function onError(e) {
if (ignored) return;
function onError(e) {
if (state.ignored) {
return;
}

let message = e && ((e.error && e.error.message) || e.message);
bripkens marked this conversation as resolved.
Show resolved Hide resolved
if (typeof message === 'string') {
beacon['e'] = message.substring(0, 300);
}
onFinish(additionalStatuses.error);
});

xhr.addEventListener('abort', function onAbort() {
if (ignored) return;
}

function onAbort() {
onFinish(additionalStatuses.abort);
});

xhr.addEventListener('readystatechange', function onReadystatechange() {
if (ignored) return;
}

function onReadystatechange() {
if (xhr.readyState === 4) {
let status;

Expand All @@ -155,88 +192,42 @@ export function instrumentXMLHttpRequest() {
onFinish(status);
}
}
});
}
};

XMLHttpRequest.prototype.setRequestHeader = function setRequestHeader(header) {
const state = this[vars.secretPropertyKey];

xhr.open = function open(method, url, async) {
const urlIgnored = isUrlIgnored(url);
ignored = ignored || urlIgnored;
if (DEBUG && urlIgnored) {
// If this request was initiated by a fetch polyfill, the Instana headers
// will be set before xhr.send is called (by the fetch polyfill,
// translating the headers from the request definition object into
// XHR.setRequestHeader calls). We need to keep track of this so we can
// set this XHR to ignored in xhr.send.
if (state && traceIdHeaderRegEx.test(header)) {
if (DEBUG) {
debug(
'Not generating XHR beacon because it should be ignored according to user configuration. URL: ' + url
'Not generating XHR beacon because correlation header is already set (possibly fetch polyfill applied).'
);
}
if (ignored) {
return originalOpen.apply(xhr, arguments);
}

if (async === undefined) {
async = true;
}

spanAndTraceId = generateUniqueId();

setBackendCorrelationHeaders = isSameOrigin(url) || isWhitelistedOrigin(url);

beacon['t'] = spanAndTraceId;
beacon['s'] = spanAndTraceId;
beacon['m'] = method;
beacon['u'] = normalizeUrl(url);
beacon['a'] = async ? 1 : 0;
beacon['bc'] = setBackendCorrelationHeaders ? 1 : 0;

try {
return originalOpen.apply(xhr, arguments);
} catch (e) {
beacon['ts'] = now() - vars.referenceTimestamp;
beacon['st'] = additionalStatuses.openError;
beacon['e'] = e.message;
addCommonBeaconProperties(beacon);
sendBeacon(beacon);
throw e;
}
};

xhr.setRequestHeader = function setRequestHeader(header) {
// If this request was initiated by a fetch polyfill, the Instana headers
// will be set before xhr.send is called (by the fetch polyfill,
// translating the headers from the request definition object into
// XHR.setRequestHeader calls). We need to keep track of this so we can
// set this XHR to ignored in xhr.send.
if (traceIdHeaderRegEx.test(header)) {
if (DEBUG) {
debug(
'Not generating XHR beacon because correlation header is already set (possibly fetch polyfill applied).'
);
}
ignored = true;
}
return originalSetRequestHeader.apply(xhr, arguments);
};

xhr.send = function send() {
if (ignored) {
return originalSend.apply(xhr, arguments);
}

if (setBackendCorrelationHeaders) {
originalSetRequestHeader.call(xhr, 'X-INSTANA-T', spanAndTraceId);
originalSetRequestHeader.call(xhr, 'X-INSTANA-S', spanAndTraceId);
originalSetRequestHeader.call(xhr, 'X-INSTANA-L', '1');
}
state.ignored = true;
}
return originalSetRequestHeader.apply(this, arguments);
};

beacon['ts'] = now() - vars.referenceTimestamp;
addCommonBeaconProperties(beacon);
return originalSend.apply(xhr, arguments);
};
XMLHttpRequest.prototype.send = function send() {
const state = this[vars.secretPropertyKey];
if (!state || state.ignored) {
return originalSend.apply(this, arguments);
}

return xhr;
}
if (state.setBackendCorrelationHeaders) {
originalSetRequestHeader.call(this, 'X-INSTANA-T', state.spanAndTraceId);
originalSetRequestHeader.call(this, 'X-INSTANA-S', state.spanAndTraceId);
originalSetRequestHeader.call(this, 'X-INSTANA-L', '1');
}

InstrumentedXMLHttpRequest.prototype = OriginalXMLHttpRequest.prototype;
InstrumentedXMLHttpRequest.DONE = OriginalXMLHttpRequest.DONE;
InstrumentedXMLHttpRequest.HEADERS_RECEIVED = OriginalXMLHttpRequest.HEADERS_RECEIVED;
InstrumentedXMLHttpRequest.LOADING = OriginalXMLHttpRequest.LOADING;
InstrumentedXMLHttpRequest.OPENED = OriginalXMLHttpRequest.OPENED;
InstrumentedXMLHttpRequest.UNSENT = OriginalXMLHttpRequest.UNSENT;
win.XMLHttpRequest = InstrumentedXMLHttpRequest;
state.beacon['ts'] = now() - vars.referenceTimestamp;
addCommonBeaconProperties(state.beacon);
return originalSend.apply(this, arguments);
};
}
8 changes: 5 additions & 3 deletions lib/transmission/batched.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @flow

import { doc, nav, win, OriginalXMLHttpRequest, sendBeacon as sendBeaconInternal } from '../browser';
import { doc, nav, win, XMLHttpRequest, sendBeacon as sendBeaconInternal } from '../browser';
import { disableMonitoringForXMLHttpRequest } from '../hooks/XMLHttpRequest';
import { addEventListener } from '../util';
import { encode } from './lineEncoding';
import type { Beacon } from '../types';
Expand All @@ -10,7 +11,7 @@ const pendingBeacons: Array<Beacon> = [];
let pendingBeaconTransmittingTimeout;

const isVisibilityApiSupported = typeof doc.visibilityState === 'string';
const isSupported = !!OriginalXMLHttpRequest && isVisibilityApiSupported && isSendBeaconApiSupported();
const isSupported = !!XMLHttpRequest && isVisibilityApiSupported && isSendBeaconApiSupported();

export function isEnabled() {
return isSupported && vars.beaconBatchingTime > 0;
Expand Down Expand Up @@ -83,7 +84,8 @@ function transmit() {
// If it doesn't work via the sendBeacon, try it via plain old AJAX APIs
// as a last resort.
if (sendBeaconState === false) {
const xhr = new OriginalXMLHttpRequest();
const xhr = new XMLHttpRequest();
disableMonitoringForXMLHttpRequest(xhr);
xhr.open('POST', String(vars.reportingUrl), true);
xhr.setRequestHeader('Content-type', 'text/plain;charset=UTF-8');
// Ensure that browsers do not try to automatically parse the response.
Expand Down
8 changes: 5 additions & 3 deletions lib/transmission/formEncoded.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @flow

import {encodeURIComponent, OriginalXMLHttpRequest, executeImageRequest} from '../browser';
import {XMLHttpRequest, encodeURIComponent, executeImageRequest} from '../browser';
import { disableMonitoringForXMLHttpRequest } from '../hooks/XMLHttpRequest';
import {hasOwnProperty} from '../util';
import type {Beacon} from '../types';
import vars from '../vars';
Expand All @@ -13,8 +14,9 @@ export function sendBeacon(data: Beacon) {
return;
}

if (OriginalXMLHttpRequest && str.length > maxLengthForImgRequest) {
const xhr = new OriginalXMLHttpRequest();
if (XMLHttpRequest && str.length > maxLengthForImgRequest) {
const xhr = new XMLHttpRequest();
disableMonitoringForXMLHttpRequest(xhr);
bripkens marked this conversation as resolved.
Show resolved Hide resolved
xhr.open('POST', String(vars.reportingUrl), true);
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
// Ensure that browsers do not try to automatically parse the response.
Expand Down
5 changes: 5 additions & 0 deletions lib/vars.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,10 @@ const defaultVars: {
// eum('wrapTimers', true)
wrapTimers: boolean,

// This key will be used by Weasel to privately store data on objects.
// Make sure to change this key when deploying Weasel in production.
secretPropertyKey: string,

// Weasel will not attempt automatic user tracking via cookies,
// fingerprinting or any other means. Instead, we give users the
// ability manually define user identifying properties. This means
Expand Down Expand Up @@ -225,6 +229,7 @@ const defaultVars: {
wrapEventHandlers: false,
wrappedEventHandlersOriginalFunctionStorageKey: '__weaselOriginalFunctions__',
wrapTimers: false,
secretPropertyKey: '__weaselSecretData__',
userId: undefined,
userName: undefined,
userEmail: undefined
Expand Down
Loading