Skip to content

Commit 83b5cc6

Browse files
feat(gatsby): Support non-serializable SDK options (#4064)
The current Gatsby SDK requires setting the SDK options in `gatsby-config.js`, which doesn't support non-serializable options. This PR introduces `sentry.config.js`, a config file where you can define your `Sentry.init` with non-serializable options. Both approaches are supported, but only one can be used (the config file approach is prioritized).
1 parent 3a1f7a1 commit 83b5cc6

File tree

3 files changed

+163
-7
lines changed

3 files changed

+163
-7
lines changed

packages/gatsby/gatsby-browser.js

+38-3
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

+48-1
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

+77-3
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', () => {

0 commit comments

Comments
 (0)