Skip to content

Commit dcba3fb

Browse files
authored
feat: Give access to XHR requests body in breadcrumb hint (#2904)
1 parent b063a7c commit dcba3fb

File tree

3 files changed

+55
-0
lines changed

3 files changed

+55
-0
lines changed

packages/browser/src/integrations/breadcrumbs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ export class Breadcrumbs implements Integration {
220220
},
221221
{
222222
xhr: handlerData.xhr,
223+
...(handlerData.xhr.__sentry_xhr__.body && { input: handlerData.xhr.__sentry_xhr__.body }),
223224
},
224225
);
225226

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,36 @@ describe("breadcrumbs", function() {
8080
assert.equal(summary.breadcrumbs[0].type, "http");
8181
assert.equal(summary.breadcrumbs[0].category, "xhr");
8282
assert.equal(summary.breadcrumbs[0].data.method, "GET");
83+
// To make sure that we are not providing this key for non-post requests
84+
assert.equal(summary.breadcrumbHints[0].input, undefined);
85+
});
86+
}
87+
);
88+
89+
it(
90+
optional(
91+
"should give access to request body for XMLHttpRequest POST requests",
92+
IS_LOADER
93+
),
94+
function() {
95+
return runInSandbox(sandbox, { manual: true }, function() {
96+
var xhr = new XMLHttpRequest();
97+
xhr.open("POST", "/base/subjects/example.json");
98+
xhr.send('{"foo":"bar"}');
99+
waitForXHR(xhr, function() {
100+
Sentry.captureMessage("test");
101+
window.finalizeManualTest();
102+
});
103+
}).then(function(summary) {
104+
// The async loader doesn't wrap XHR
105+
if (IS_LOADER) {
106+
return;
107+
}
108+
assert.equal(summary.breadcrumbs.length, 1);
109+
assert.equal(summary.breadcrumbs[0].type, "http");
110+
assert.equal(summary.breadcrumbs[0].category, "xhr");
111+
assert.equal(summary.breadcrumbs[0].data.method, "POST");
112+
assert.equal(summary.breadcrumbHints[0].input, '{"foo":"bar"}');
83113
});
84114
}
85115
);

packages/utils/src/instrument.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,13 +180,16 @@ function instrumentFetch(): void {
180180
});
181181
}
182182

183+
type XHRSendInput = null | Blob | BufferSource | FormData | URLSearchParams | string;
184+
183185
/** JSDoc */
184186
interface SentryWrappedXMLHttpRequest extends XMLHttpRequest {
185187
[key: string]: any;
186188
__sentry_xhr__?: {
187189
method?: string;
188190
url?: string;
189191
status_code?: number;
192+
body?: XHRSendInput;
190193
};
191194
}
192195

@@ -220,6 +223,9 @@ function instrumentXHR(): void {
220223
return;
221224
}
222225

226+
// Poor man implementation of ES6 `Map` by tracking and keeping in sync key and value separately.
227+
const requestKeys: XMLHttpRequest[] = [];
228+
const requestValues: Array<any>[] = [];
223229
const xhrproto = XMLHttpRequest.prototype;
224230

225231
fill(xhrproto, 'open', function(originalOpen: () => void): () => void {
@@ -250,6 +256,21 @@ function instrumentXHR(): void {
250256
} catch (e) {
251257
/* do nothing */
252258
}
259+
260+
try {
261+
const requestPos = requestKeys.indexOf(xhr);
262+
if (requestPos !== -1) {
263+
// Make sure to pop both, key and value to keep it in sync.
264+
requestKeys.splice(requestPos);
265+
const args = requestValues.splice(requestPos)[0];
266+
if (xhr.__sentry_xhr__ && args[0] !== undefined) {
267+
xhr.__sentry_xhr__.body = args[0] as XHRSendInput;
268+
}
269+
}
270+
} catch (e) {
271+
/* do nothing */
272+
}
273+
253274
triggerHandlers('xhr', {
254275
args,
255276
endTimestamp: Date.now(),
@@ -276,6 +297,9 @@ function instrumentXHR(): void {
276297

277298
fill(xhrproto, 'send', function(originalSend: () => void): () => void {
278299
return function(this: SentryWrappedXMLHttpRequest, ...args: any[]): void {
300+
requestKeys.push(this);
301+
requestValues.push(args);
302+
279303
triggerHandlers('xhr', {
280304
args,
281305
startTimestamp: Date.now(),

0 commit comments

Comments
 (0)