Skip to content

Commit caea4f8

Browse files
committed
feat: Store requestes to sentry as breadcrumbs
1 parent f25fb26 commit caea4f8

File tree

4 files changed

+110
-27
lines changed

4 files changed

+110
-27
lines changed

packages/browser/src/integrations/breadcrumbs.ts

Lines changed: 99 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { API } from '@sentry/core';
1+
import { API, logger } from '@sentry/core';
22
import { getCurrentHub } from '@sentry/hub';
33
import { Integration, Severity } from '@sentry/types';
44
import { isFunction, isString } from '@sentry/utils/is';
55
import { getGlobalObject, parseUrl } from '@sentry/utils/misc';
6-
import { fill } from '@sentry/utils/object';
6+
import { deserialize, fill } from '@sentry/utils/object';
77
import { safeJoin } from '@sentry/utils/string';
8-
import { supportsFetch, supportsHistory } from '@sentry/utils/supports';
8+
import { supportsBeacon, supportsFetch, supportsHistory } from '@sentry/utils/supports';
99
import { BrowserOptions } from '../backend';
1010
import { breadcrumbEventHandler, keypressEventHandler, wrap } from './helpers';
1111

@@ -27,6 +27,24 @@ export interface SentryWrappedXMLHttpRequest extends XMLHttpRequest {
2727
};
2828
}
2929

30+
/** JSDoc */
31+
function addSentryBreadcrumb(serializedData: string): void {
32+
// There's always something that can go wrong with deserialization...
33+
try {
34+
const data: { [key: string]: any } = deserialize(serializedData);
35+
const exception = data.exception && data.exception.values && data.exception.values[0];
36+
37+
getCurrentHub().addBreadcrumb({
38+
category: 'sentry',
39+
event_id: data.event_id,
40+
level: data.level || Severity.fromString('error'),
41+
message: exception ? `${exception.type ? `${exception.type}: ` : ''}${exception.value}` : data.message,
42+
});
43+
} catch (_oO) {
44+
logger.error('Error while adding sentry type breadcrumb');
45+
}
46+
}
47+
3048
/** Default Breadcrumbs instrumentations */
3149
export class Breadcrumbs implements Integration {
3250
/**
@@ -39,20 +57,66 @@ export class Breadcrumbs implements Integration {
3957
*/
4058
public constructor(
4159
private readonly config: {
60+
beacon?: boolean;
4261
console?: boolean;
4362
dom?: boolean;
4463
fetch?: boolean;
4564
history?: boolean;
65+
sentry?: boolean;
4666
xhr?: boolean;
4767
} = {
68+
beacon: true,
4869
console: true,
4970
dom: true,
5071
fetch: true,
5172
history: true,
73+
sentry: true,
5274
xhr: true,
5375
},
5476
) {}
5577

78+
/** JSDoc */
79+
private instrumentBeacon(options: { filterUrl?: string }): void {
80+
if (!supportsBeacon()) {
81+
return;
82+
}
83+
84+
/** JSDoc */
85+
function beaconReplacementFunction(originalBeaconFunction: () => void): () => void {
86+
return function(this: History, ...args: any[]): void {
87+
const url = args[0];
88+
const data = args[1];
89+
// If the browser successfully queues the request for delivery, the method returns "true" and returns "false" otherwise.
90+
// https://developer.mozilla.org/en-US/docs/Web/API/Beacon_API/Using_the_Beacon_API
91+
const result = originalBeaconFunction.apply(this, args);
92+
93+
// if Sentry key appears in URL, don't capture it as a request
94+
// but rather as our own 'sentry' type breadcrumb
95+
if (options.filterUrl && url.includes(options.filterUrl)) {
96+
addSentryBreadcrumb(data);
97+
return result;
98+
}
99+
100+
// What is wrong with you TypeScript...
101+
const crumb = ({
102+
category: 'beacon',
103+
data,
104+
type: 'http',
105+
} as any) as { [key: string]: any };
106+
107+
if (!result) {
108+
crumb.level = Severity.Error;
109+
}
110+
111+
getCurrentHub().addBreadcrumb(crumb);
112+
113+
return result;
114+
};
115+
}
116+
117+
fill(global.navigator, 'sendBeacon', beaconReplacementFunction);
118+
}
119+
56120
/** JSDoc */
57121
private instrumentConsole(): void {
58122
if (!('console' in global)) {
@@ -133,15 +197,19 @@ export class Breadcrumbs implements Integration {
133197
url = String(fetchInput);
134198
}
135199

136-
// if Sentry key appears in URL, don't capture, as it's our own request
137-
if (options.filterUrl && url.includes(options.filterUrl)) {
138-
return originalFetch.apply(global, args);
139-
}
140-
141200
if (args[1] && args[1].method) {
142201
method = args[1].method;
143202
}
144203

204+
// if Sentry key appears in URL, don't capture it as a request
205+
// but rather as our own 'sentry' type breadcrumb
206+
if (options.filterUrl && url.includes(options.filterUrl)) {
207+
if (method === 'POST' && args[1] && args[1].body) {
208+
addSentryBreadcrumb(args[1].body);
209+
}
210+
return originalFetch.apply(global, args);
211+
}
212+
145213
const fetchData: {
146214
method: string;
147215
url: string;
@@ -245,6 +313,7 @@ export class Breadcrumbs implements Integration {
245313
fill(global.history, 'pushState', historyReplacementFunction);
246314
fill(global.history, 'replaceState', historyReplacementFunction);
247315
}
316+
248317
/** JSDoc */
249318
private instrumentXHR(options: { filterUrl?: string }): void {
250319
if (!('XMLHttpRequest' in global)) {
@@ -277,12 +346,14 @@ export class Breadcrumbs implements Integration {
277346
originalOpen =>
278347
function(this: SentryWrappedXMLHttpRequest, ...args: any[]): void {
279348
const url = args[1];
280-
// if Sentry key appears in URL, don't capture, as it's our own request
281-
if (isString(url) && (options.filterUrl && !url.includes(options.filterUrl))) {
282-
this.__sentry_xhr__ = {
283-
method: args[0],
284-
url: args[1],
285-
};
349+
this.__sentry_xhr__ = {
350+
method: args[0],
351+
url: args[1],
352+
};
353+
// if Sentry key appears in URL, don't capture it as a request
354+
// but rather as our own 'sentry' type breadcrumb
355+
if (isString(url) && (options.filterUrl && url.includes(options.filterUrl))) {
356+
this.__sentry_own_request__ = true;
286357
}
287358
return originalOpen.apply(this, args);
288359
},
@@ -295,13 +366,22 @@ export class Breadcrumbs implements Integration {
295366
function(this: SentryWrappedXMLHttpRequest, ...args: any[]): void {
296367
const xhr = this; // tslint:disable-line:no-this-assignment
297368

369+
if (xhr.__sentry_own_request__) {
370+
addSentryBreadcrumb(args[0]);
371+
}
372+
298373
/** JSDoc */
299374
function onreadystatechangeHandler(): void {
300-
if (xhr.__sentry_xhr__ && xhr.readyState === 4) {
375+
if (xhr.readyState === 4) {
376+
if (xhr.__sentry_own_request__) {
377+
return;
378+
}
301379
try {
302380
// touching statusCode in some platforms throws
303381
// an exception
304-
xhr.__sentry_xhr__.status_code = xhr.status;
382+
if (xhr.__sentry_xhr__) {
383+
xhr.__sentry_xhr__.status_code = xhr.status;
384+
}
305385
} catch (e) {
306386
/* do nothing */
307387
}
@@ -368,6 +448,9 @@ export class Breadcrumbs implements Integration {
368448
if (this.config.fetch) {
369449
this.instrumentFetch({ filterUrl });
370450
}
451+
if (this.config.beacon) {
452+
this.instrumentBeacon({ filterUrl });
453+
}
371454
if (this.config.history) {
372455
this.instrumentHistory();
373456
}

packages/browser/src/sdk.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export const defaultIntegrations = [
3232
* import { init } from '@sentry/browser';
3333
*
3434
* init({
35-
* dsn: '__Dsn__',
35+
* dsn: '__DSN__',
3636
* // ...
3737
* });
3838
*

packages/browser/test/integration/test.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -759,24 +759,24 @@ describe('integration', function() {
759759
);
760760
});
761761

762-
it('should NOT denote XMLHttpRequests to the Sentry store endpoint as requiring breadcrumb capture', function(done) {
762+
it('should transform XMLHttpRequests to the Sentry store endpoint as sentry type breadcrumb', function(done) {
763763
var iframe = this.iframe;
764764
iframeExecute(
765765
iframe,
766766
done,
767767
function() {
768768
var xhr = new XMLHttpRequest();
769-
xhr.open('GET', 'http://example.com/api/1/store/?sentry_key=public');
770-
771-
// can't actually transmit an XHR (breadcrumb isnt recorded until
772-
// onreadystatechange fires), so enough to just verify that
773-
// __sentry_xhr wasn't set on xhr object
774-
775-
window.sentryData = xhr.hasOwnProperty('__sentry_xhr__');
769+
xhr.open('GET', 'https://[email protected]/1/store');
770+
xhr.send('{"message":"someMessage","level":"warning"}');
776771
setTimeout(done);
777772
},
778773
function() {
779-
assert.isFalse(iframe.contentWindow.sentryData);
774+
var Sentry = iframe.contentWindow.Sentry;
775+
var breadcrumbs = Sentry.getCurrentHub().getScope().breadcrumbs;
776+
assert.equal(breadcrumbs.length, 1);
777+
assert.equal(breadcrumbs[0].category, 'sentry');
778+
assert.equal(breadcrumbs[0].level, 'warning');
779+
assert.equal(breadcrumbs[0].message, 'someMessage');
780780
},
781781
);
782782
});

packages/node/src/sdk.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const defaultIntegrations = [
3030
* const { init } = require('@sentry/node');
3131
*
3232
* init({
33-
* dsn: '__Dsn__',
33+
* dsn: '__DSN__',
3434
* // ...
3535
* });
3636
*

0 commit comments

Comments
 (0)