Skip to content

Commit 973cfde

Browse files
committed
ref: Rework XHR wrapping logic to make sure it always triggers
1 parent 24b3125 commit 973cfde

File tree

4 files changed

+44
-58
lines changed

4 files changed

+44
-58
lines changed

packages/apm/src/integrations/tracing.ts

+12-5
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ interface Activity {
8383
}
8484

8585
const global = getGlobalObject<Window>();
86+
const defaultTracingOrigins = ['localhost', /^\//];
8687

8788
/**
8889
* Tracing Integration
@@ -119,13 +120,14 @@ export class Tracing implements Integration {
119120

120121
private static _debounce: number = 0;
121122

123+
private readonly _emitOptionsWarning: boolean = false;
124+
122125
/**
123126
* Constructor for Tracing
124127
*
125128
* @param _options TracingOptions
126129
*/
127130
public constructor(private readonly _options?: Partial<TracingOptions>) {
128-
const defaultTracingOrigins = ['localhost', /^\//];
129131
const defaults = {
130132
idleTimeout: 500,
131133
maxTransactionDuration: 600,
@@ -142,11 +144,9 @@ export class Tracing implements Integration {
142144
tracesSampleRate: 1,
143145
tracingOrigins: defaultTracingOrigins,
144146
};
147+
// NOTE: Logger doesn't work in contructors, as it's initialized after integrations instances
145148
if (!_options || !Array.isArray(_options.tracingOrigins) || _options.tracingOrigins.length === 0) {
146-
logger.warn(
147-
'Sentry: You need to define `tracingOrigins` in the options. Set an array of urls or patterns to trace.',
148-
);
149-
logger.warn(`Sentry: We added a reasonable default for you: ${defaultTracingOrigins}`);
149+
this._emitOptionsWarning = true;
150150
}
151151
Tracing.options = this._options = {
152152
...defaults,
@@ -160,6 +160,13 @@ export class Tracing implements Integration {
160160
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
161161
Tracing._getCurrentHub = getCurrentHub;
162162

163+
if (this._emitOptionsWarning) {
164+
logger.warn(
165+
'Sentry: You need to define `tracingOrigins` in the options. Set an array of urls or patterns to trace.',
166+
);
167+
logger.warn(`Sentry: We added a reasonable default for you: ${defaultTracingOrigins}`);
168+
}
169+
163170
if (!Tracing._isEnabled()) {
164171
return;
165172
}

packages/browser/src/integrations/trycatch.ts

+15-30
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { fill, getFunctionName, getGlobalObject } from '@sentry/utils';
33

44
import { wrap } from '../helpers';
55

6-
type XMLHttpRequestProp = 'onload' | 'onerror' | 'onprogress';
6+
type XMLHttpRequestProp = 'onload' | 'onerror' | 'onprogress' | 'onreadystatechange';
77

88
/** Wrap timer functions and event targets to catch errors and provide better meta data */
99
export class TryCatch implements Integration {
@@ -133,12 +133,12 @@ export class TryCatch implements Integration {
133133
private _wrapXHR(originalSend: () => void): () => void {
134134
return function(this: XMLHttpRequest, ...args: any[]): void {
135135
const xhr = this; // tslint:disable-line:no-this-assignment
136-
const xmlHttpRequestProps: XMLHttpRequestProp[] = ['onload', 'onerror', 'onprogress'];
136+
const xmlHttpRequestProps: XMLHttpRequestProp[] = ['onload', 'onerror', 'onprogress', 'onreadystatechange'];
137137

138138
xmlHttpRequestProps.forEach(prop => {
139-
if (prop in this && typeof this[prop] === 'function') {
140-
fill(this, prop, original =>
141-
wrap(original, {
139+
if (prop in xhr && typeof xhr[prop] === 'function') {
140+
fill(xhr, prop, function(original: WrappedFunction): Function {
141+
const wrapOptions = {
142142
mechanism: {
143143
data: {
144144
function: prop,
@@ -147,33 +147,18 @@ export class TryCatch implements Integration {
147147
handled: true,
148148
type: 'instrument',
149149
},
150-
}),
151-
);
152-
}
153-
});
154-
155-
if ('onreadystatechange' in xhr && typeof xhr.onreadystatechange === 'function') {
156-
fill(xhr, 'onreadystatechange', function(original: WrappedFunction): Function {
157-
const wrapOptions = {
158-
mechanism: {
159-
data: {
160-
function: 'onreadystatechange',
161-
handler: getFunctionName(original),
162-
},
163-
handled: true,
164-
type: 'instrument',
165-
},
166-
};
150+
};
167151

168-
// If Instrument integration has been called before TryCatch, get the name of original function
169-
if (original.__sentry_original__) {
170-
wrapOptions.mechanism.data.handler = getFunctionName(original.__sentry_original__);
171-
}
152+
// If Instrument integration has been called before TryCatch, get the name of original function
153+
if (original.__sentry_original__) {
154+
wrapOptions.mechanism.data.handler = getFunctionName(original.__sentry_original__);
155+
}
172156

173-
// Otherwise wrap directly
174-
return wrap(original, wrapOptions);
175-
});
176-
}
157+
// Otherwise wrap directly
158+
return wrap(original, wrapOptions);
159+
});
160+
}
161+
});
177162

178163
return originalSend.apply(this, args);
179164
};

packages/browser/test/integration/suites/builtins.js

+15-5
Original file line numberDiff line numberDiff line change
@@ -118,18 +118,28 @@ describe("wrapped built-ins", function() {
118118
});
119119
});
120120

121-
it("should not call XMLHttpRequest onreadystatechange more than once", function() {
121+
it("should not call XMLHttpRequest onreadystatechange more than once per state", function() {
122122
return runInSandbox(sandbox, { manual: true }, function() {
123-
window.calls = 0;
123+
window.calls = {};
124124
var xhr = new XMLHttpRequest();
125125
xhr.open("GET", "/base/subjects/example.json");
126126
xhr.onreadystatechange = function wat() {
127-
window.finalizeManualTest();
128-
window.calls += 1;
127+
window.calls[xhr.readyState] = window.calls[xhr.readyState]
128+
? window.calls[xhr.readyState] + 1
129+
: 1;
130+
if (xhr.readyState === 4) {
131+
window.finalizeManualTest();
132+
}
129133
};
130134
xhr.send();
131135
}).then(function(summary) {
132-
assert.equal(summary.window.calls, 3);
136+
for (var state in summary.window.calls) {
137+
assert.equal(summary.window.calls[state], 1);
138+
}
139+
// IE Triggers all states (1-4), including 1 (open), despite it being assigned before
140+
// the `onreadystatechange` handler.
141+
assert.isAtLeast(Object.keys(summary.window.calls).length, 3);
142+
assert.isAtMost(Object.keys(summary.window.calls).length, 4);
133143
delete summary.window.calls;
134144
});
135145
});

packages/utils/src/instrument.ts

+2-18
Original file line numberDiff line numberDiff line change
@@ -230,10 +230,7 @@ function instrumentXHR(): void {
230230
...commonHandlerData,
231231
});
232232

233-
/**
234-
* @hidden
235-
*/
236-
function onreadystatechangeHandler(): void {
233+
xhr.addEventListener('readystatechange', function() {
237234
if (xhr.readyState === 4) {
238235
try {
239236
// touching statusCode in some platforms throws
@@ -249,20 +246,7 @@ function instrumentXHR(): void {
249246
endTimestamp: Date.now(),
250247
});
251248
}
252-
}
253-
254-
if ('onreadystatechange' in xhr && typeof xhr.onreadystatechange === 'function') {
255-
fill(xhr, 'onreadystatechange', function(original: WrappedFunction): Function {
256-
return function(...readyStateArgs: any[]): void {
257-
onreadystatechangeHandler();
258-
return original.apply(xhr, readyStateArgs);
259-
};
260-
});
261-
} else {
262-
// if onreadystatechange wasn't actually set by the page on this xhr, we
263-
// are free to set our own and capture the breadcrumb
264-
xhr.onreadystatechange = onreadystatechangeHandler;
265-
}
249+
});
266250

267251
return originalSend.apply(this, args);
268252
};

0 commit comments

Comments
 (0)