Skip to content

Commit 0f555ec

Browse files
authored
Merge branch 'master' into onur/graphql-tracing-integration
2 parents 92e0193 + 115b0f3 commit 0f555ec

34 files changed

+852
-208
lines changed

packages/browser/src/eventbuilder.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,7 @@ export function eventFromException(options: Options, exception: unknown, hint?:
2323
const event = eventFromUnknownInput(exception, syntheticException, {
2424
attachStacktrace: options.attachStacktrace,
2525
});
26-
addExceptionMechanism(event, {
27-
handled: true,
28-
type: 'generic',
29-
});
26+
addExceptionMechanism(event); // defaults to { type: 'generic', handled: true }
3027
event.level = Severity.Error;
3128
if (hint && hint.event_id) {
3229
event.event_id = hint.event_id;

packages/core/src/baseclient.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
Transport,
1414
} from '@sentry/types';
1515
import {
16+
checkOrSetAlreadyCaught,
1617
dateTimestampInSeconds,
1718
Dsn,
1819
isPlainObject,
@@ -29,6 +30,8 @@ import {
2930
import { Backend, BackendClass } from './basebackend';
3031
import { IntegrationIndex, setupIntegrations } from './integration';
3132

33+
const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been captured.";
34+
3235
/**
3336
* Base implementation for all JavaScript SDK clients.
3437
*
@@ -101,6 +104,12 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
101104
*/
102105
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
103106
public captureException(exception: any, hint?: EventHint, scope?: Scope): string | undefined {
107+
// ensure we haven't captured this very object before
108+
if (checkOrSetAlreadyCaught(exception)) {
109+
logger.log(ALREADY_SEEN_ERROR);
110+
return;
111+
}
112+
104113
let eventId: string | undefined = hint && hint.event_id;
105114

106115
this._process(
@@ -140,6 +149,12 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
140149
* @inheritDoc
141150
*/
142151
public captureEvent(event: Event, hint?: EventHint, scope?: Scope): string | undefined {
152+
// ensure we haven't captured this very object before
153+
if (hint?.originalException && checkOrSetAlreadyCaught(hint.originalException)) {
154+
logger.log(ALREADY_SEEN_ERROR);
155+
return;
156+
}
157+
143158
let eventId: string | undefined = hint && hint.event_id;
144159

145160
this._process(

packages/core/test/lib/base.test.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ const PUBLIC_DSN = 'https://username@domain/123';
1212
// eslint-disable-next-line no-var
1313
declare var global: any;
1414

15+
const backendEventFromException = jest.spyOn(TestBackend.prototype, 'eventFromException');
16+
const clientProcess = jest.spyOn(TestClient.prototype as any, '_process');
17+
1518
jest.mock('@sentry/utils', () => {
1619
const original = jest.requireActual('@sentry/utils');
1720
return {
@@ -57,7 +60,7 @@ describe('BaseClient', () => {
5760
});
5861

5962
afterEach(() => {
60-
jest.restoreAllMocks();
63+
jest.clearAllMocks();
6164
});
6265

6366
describe('constructor() / getDsn()', () => {
@@ -249,6 +252,30 @@ describe('BaseClient', () => {
249252
}),
250253
);
251254
});
255+
256+
test.each([
257+
['`Error` instance', new Error('Will I get caught twice?')],
258+
['plain object', { 'Will I': 'get caught twice?' }],
259+
['primitive wrapper', new String('Will I get caught twice?')],
260+
// Primitives aren't tested directly here because they need to be wrapped with `objectify` *before* being passed
261+
// to `captureException` (which is how we'd end up with a primitive wrapper as tested above) in order for the
262+
// already-seen check to work . Any primitive which is passed without being wrapped will be captured each time it
263+
// is encountered, so this test doesn't apply.
264+
])("doesn't capture the same exception twice - %s", (_name: string, thrown: any) => {
265+
const client = new TestClient({ dsn: PUBLIC_DSN });
266+
267+
expect(thrown.__sentry_captured__).toBeUndefined();
268+
269+
client.captureException(thrown);
270+
271+
expect(thrown.__sentry_captured__).toBe(true);
272+
expect(backendEventFromException).toHaveBeenCalledTimes(1);
273+
274+
client.captureException(thrown);
275+
276+
// `captureException` should bail right away this second time around and not get as far as calling this again
277+
expect(backendEventFromException).toHaveBeenCalledTimes(1);
278+
});
252279
});
253280

254281
describe('captureMessage', () => {
@@ -325,6 +352,35 @@ describe('BaseClient', () => {
325352
expect(TestBackend.instance!.event).toBeUndefined();
326353
});
327354

355+
test.each([
356+
['`Error` instance', new Error('Will I get caught twice?')],
357+
['plain object', { 'Will I': 'get caught twice?' }],
358+
['primitive wrapper', new String('Will I get caught twice?')],
359+
// Primitives aren't tested directly here because they need to be wrapped with `objectify` *before* being passed
360+
// to `captureEvent` (which is how we'd end up with a primitive wrapper as tested above) in order for the
361+
// already-seen check to work . Any primitive which is passed without being wrapped will be captured each time it
362+
// is encountered, so this test doesn't apply.
363+
])("doesn't capture an event wrapping the same exception twice - %s", (_name: string, thrown: any) => {
364+
// Note: this is the same test as in `describe(captureException)`, except with the exception already wrapped in a
365+
// hint and accompanying an event. Duplicated here because some methods skip `captureException` and go straight to
366+
// `captureEvent`.
367+
const client = new TestClient({ dsn: PUBLIC_DSN });
368+
const event = { exception: { values: [{ type: 'Error', message: 'Will I get caught twice?' }] } };
369+
const hint = { originalException: thrown };
370+
371+
expect(thrown.__sentry_captured__).toBeUndefined();
372+
373+
client.captureEvent(event, hint);
374+
375+
expect(thrown.__sentry_captured__).toBe(true);
376+
expect(clientProcess).toHaveBeenCalledTimes(1);
377+
378+
client.captureEvent(event, hint);
379+
380+
// `captureEvent` should bail right away this second time around and not get as far as calling this again
381+
expect(clientProcess).toHaveBeenCalledTimes(1);
382+
});
383+
328384
test('sends an event', () => {
329385
expect.assertions(2);
330386
const client = new TestClient({ dsn: PUBLIC_DSN });
@@ -798,7 +854,7 @@ describe('BaseClient', () => {
798854
expect(TestBackend.instance!.event).toBeUndefined();
799855
});
800856

801-
test('calls beforeSend gets an access to a hint as a second argument', () => {
857+
test('beforeSend gets access to a hint as a second argument', () => {
802858
expect.assertions(2);
803859
const beforeSend = jest.fn((event, hint) => ({ ...event, data: hint.data }));
804860
const client = new TestClient({ dsn: PUBLIC_DSN, beforeSend });

packages/gatsby/gatsby-browser.js

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,27 @@
1+
/* eslint-disable no-console */
12
const Sentry = require('@sentry/gatsby');
23

34
exports.onClientEntry = function(_, pluginParams) {
4-
if (pluginParams === undefined) {
5+
const isIntialized = isSentryInitialized();
6+
const areOptionsDefined = areSentryOptionsDefined(pluginParams);
7+
8+
if (isIntialized) {
9+
window.Sentry = Sentry; // For backwards compatibility
10+
if (areOptionsDefined) {
11+
console.warn(
12+
'Sentry Logger [Warn]: The SDK was initialized in the Sentry config file, but options were found in the Gatsby config. ' +
13+
'These have been ignored, merge them to the Sentry config if you want to use them.\n' +
14+
'Learn more about the Gatsby SDK on https://docs.sentry.io/platforms/javascript/guides/gatsby/',
15+
);
16+
}
17+
return;
18+
}
19+
20+
if (!areOptionsDefined) {
21+
console.error(
22+
'Sentry Logger [Error]: No config for the Gatsby SDK was found.\n' +
23+
'Learn how to configure it on https://docs.sentry.io/platforms/javascript/guides/gatsby/',
24+
);
525
return;
626
}
727

@@ -12,6 +32,21 @@ exports.onClientEntry = function(_, pluginParams) {
1232
dsn: __SENTRY_DSN__,
1333
...pluginParams,
1434
});
15-
16-
window.Sentry = Sentry;
35+
window.Sentry = Sentry; // For backwards compatibility
1736
};
37+
38+
function isSentryInitialized() {
39+
// Although `window` should exist because we're in the browser (where this script
40+
// is run), and `__SENTRY__.hub` is created when importing the Gatsby SDK, double
41+
// check that in case something weird happens.
42+
return !!(window && window.__SENTRY__ && window.__SENTRY__.hub && window.__SENTRY__.hub.getClient());
43+
}
44+
45+
function areSentryOptionsDefined(params) {
46+
if (params == undefined) return false;
47+
// Even if there aren't any options, there's a `plugins` property defined as an empty array
48+
if (Object.keys(params).length == 1 && Array.isArray(params.plugins) && params.plugins.length == 0) {
49+
return false;
50+
}
51+
return true;
52+
}

packages/gatsby/gatsby-node.js

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
const fs = require('fs');
2+
13
const sentryRelease = JSON.stringify(
24
// Always read first as Sentry takes this as precedence
35
process.env.SENTRY_RELEASE ||
@@ -15,8 +17,9 @@ const sentryRelease = JSON.stringify(
1517
);
1618

1719
const sentryDsn = JSON.stringify(process.env.SENTRY_DSN || '');
20+
const SENTRY_USER_CONFIG = ['./sentry.config.js', './sentry.config.ts'];
1821

19-
exports.onCreateWebpackConfig = ({ plugins, actions }) => {
22+
exports.onCreateWebpackConfig = ({ plugins, getConfig, actions }) => {
2023
actions.setWebpackConfig({
2124
plugins: [
2225
plugins.define({
@@ -25,4 +28,48 @@ exports.onCreateWebpackConfig = ({ plugins, actions }) => {
2528
}),
2629
],
2730
});
31+
32+
// To configure the SDK, SENTRY_USER_CONFIG is prioritized over `gatsby-config.js`,
33+
// since it isn't possible to set non-serializable parameters in the latter.
34+
// Prioritization here means what `init` is run.
35+
let configFile = null;
36+
try {
37+
configFile = SENTRY_USER_CONFIG.find(file => fs.existsSync(file));
38+
} catch (error) {
39+
// Some node versions (like v11) throw an exception on `existsSync` instead of
40+
// returning false. See https://github.com/tschaub/mock-fs/issues/256
41+
}
42+
43+
if (!configFile) {
44+
return;
45+
}
46+
// `setWebpackConfig` merges the Webpack config, ignoring some props like `entry`. See
47+
// https://www.gatsbyjs.com/docs/reference/config-files/actions/#setWebpackConfig
48+
// So it's not possible to inject the Sentry properties with that method. Instead, we
49+
// can replace the whole config with the modifications we need.
50+
const finalConfig = injectSentryConfig(getConfig(), configFile);
51+
actions.replaceWebpackConfig(finalConfig);
2852
};
53+
54+
function injectSentryConfig(config, configFile) {
55+
const injectedEntries = {};
56+
// TODO: investigate what entries need the Sentry config injected.
57+
// We may want to skip some.
58+
Object.keys(config.entry).forEach(prop => {
59+
const value = config.entry[prop];
60+
let injectedValue = value;
61+
if (typeof value === 'string') {
62+
injectedValue = [configFile, value];
63+
} else if (Array.isArray(value)) {
64+
injectedValue = [configFile, ...value];
65+
} else {
66+
// eslint-disable-next-line no-console
67+
console.error(
68+
`Sentry Logger [Error]: Could not inject SDK initialization code into ${prop}, unexpected format: `,
69+
typeof value,
70+
);
71+
}
72+
injectedEntries[prop] = injectedValue;
73+
});
74+
return { ...config, entry: injectedEntries };
75+
}

packages/gatsby/test/gatsby-browser.test.ts

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ jest.mock('@sentry/gatsby', () => {
1616
},
1717
};
1818
});
19+
global.console.warn = jest.fn();
20+
global.console.error = jest.fn();
1921

2022
let tracingAddExtensionMethods = jest.fn();
2123
jest.mock('@sentry/tracing', () => {
@@ -50,9 +52,81 @@ describe('onClientEntry', () => {
5052
}
5153
});
5254

53-
it('sets window.Sentry', () => {
54-
onClientEntry(undefined, {});
55-
expect((window as any).Sentry).not.toBeUndefined();
55+
describe('inits Sentry once', () => {
56+
afterEach(() => {
57+
delete (window as any).Sentry;
58+
delete (window as any).__SENTRY__;
59+
(global.console.warn as jest.Mock).mockClear();
60+
(global.console.error as jest.Mock).mockClear();
61+
});
62+
63+
function setMockedSentryInWindow() {
64+
(window as any).__SENTRY__ = {
65+
hub: {
66+
getClient: () => ({
67+
// Empty object mocking the client
68+
}),
69+
},
70+
};
71+
}
72+
73+
it('initialized in injected config, without pluginParams', () => {
74+
setMockedSentryInWindow();
75+
onClientEntry(undefined, { plugins: [] });
76+
// eslint-disable-next-line no-console
77+
expect(console.warn).not.toHaveBeenCalled();
78+
// eslint-disable-next-line no-console
79+
expect(console.error).not.toHaveBeenCalled();
80+
expect(sentryInit).not.toHaveBeenCalled();
81+
expect((window as any).Sentry).toBeDefined();
82+
});
83+
84+
it('initialized in injected config, with pluginParams', () => {
85+
setMockedSentryInWindow();
86+
onClientEntry(undefined, { plugins: [], dsn: 'dsn', release: 'release' });
87+
// eslint-disable-next-line no-console
88+
expect((console.warn as jest.Mock).mock.calls[0]).toMatchInlineSnapshot(`
89+
Array [
90+
"Sentry Logger [Warn]: The SDK was initialized in the Sentry config file, but options were found in the Gatsby config. These have been ignored, merge them to the Sentry config if you want to use them.
91+
Learn more about the Gatsby SDK on https://docs.sentry.io/platforms/javascript/guides/gatsby/",
92+
]
93+
`);
94+
// eslint-disable-next-line no-console
95+
expect(console.error).not.toHaveBeenCalled();
96+
expect(sentryInit).not.toHaveBeenCalled();
97+
expect((window as any).Sentry).toBeDefined();
98+
});
99+
100+
it('not initialized in injected config, without pluginParams', () => {
101+
onClientEntry(undefined, { plugins: [] });
102+
// eslint-disable-next-line no-console
103+
expect(console.warn).not.toHaveBeenCalled();
104+
// eslint-disable-next-line no-console
105+
expect((console.error as jest.Mock).mock.calls[0]).toMatchInlineSnapshot(`
106+
Array [
107+
"Sentry Logger [Error]: No config for the Gatsby SDK was found.
108+
Learn how to configure it on https://docs.sentry.io/platforms/javascript/guides/gatsby/",
109+
]
110+
`);
111+
expect((window as any).Sentry).not.toBeDefined();
112+
});
113+
114+
it('not initialized in injected config, with pluginParams', () => {
115+
onClientEntry(undefined, { plugins: [], dsn: 'dsn', release: 'release' });
116+
// eslint-disable-next-line no-console
117+
expect(console.warn).not.toHaveBeenCalled();
118+
// eslint-disable-next-line no-console
119+
expect(console.error).not.toHaveBeenCalled();
120+
expect(sentryInit).toHaveBeenCalledTimes(1);
121+
expect(sentryInit.mock.calls[0][0]).toMatchInlineSnapshot(`
122+
Object {
123+
"dsn": "dsn",
124+
"plugins": Array [],
125+
"release": "release",
126+
}
127+
`);
128+
expect((window as any).Sentry).toBeDefined();
129+
});
56130
});
57131

58132
it('sets a tracesSampleRate if defined as option', () => {

packages/integrations/src/vue.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ export class Vue implements Integration {
268268
if (transaction) {
269269
this._rootSpan = transaction.startChild({
270270
description: 'Application Render',
271-
op: 'Vue',
271+
op: 'vue',
272272
});
273273
}
274274
// Use functionality from @sentry/tracing
@@ -277,7 +277,7 @@ export class Vue implements Integration {
277277
if (activeTransaction) {
278278
this._rootSpan = activeTransaction.startChild({
279279
description: 'Application Render',
280-
op: 'Vue',
280+
op: 'vue',
281281
});
282282
}
283283
}

packages/nextjs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
},
1919
"dependencies": {
2020
"@sentry/core": "6.13.3",
21+
"@sentry/hub": "6.13.3",
2122
"@sentry/integrations": "6.13.3",
2223
"@sentry/node": "6.13.3",
2324
"@sentry/react": "6.13.3",

0 commit comments

Comments
 (0)