Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gatsby): Support non-serializable SDK options #4064

Merged
merged 11 commits into from
Oct 22, 2021
41 changes: 38 additions & 3 deletions packages/gatsby/gatsby-browser.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,27 @@
/* eslint-disable no-console */
const Sentry = require('@sentry/gatsby');

exports.onClientEntry = function(_, pluginParams) {
if (pluginParams === undefined) {
const isIntialized = isSentryInitialized();
const areOptionsDefined = areSentryOptionsDefined(pluginParams);
iker-barriocanal marked this conversation as resolved.
Show resolved Hide resolved

if (isIntialized) {
window.Sentry = Sentry; // For backwards compatibility
if (areOptionsDefined) {
console.warn(
'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.\n' +
'Learn more about the Gatsby SDK on https://docs.sentry.io/platforms/javascript/guides/gatsby/',
iker-barriocanal marked this conversation as resolved.
Show resolved Hide resolved
);
}
return;
}

if (!areOptionsDefined) {
console.error(
'Sentry Logger [Error]: No config for the Gatsby SDK was found.\n' +
rhcarvalho marked this conversation as resolved.
Show resolved Hide resolved
'Learn how to configure it on https://docs.sentry.io/platforms/javascript/guides/gatsby/',
);
return;
}

Expand All @@ -12,6 +32,21 @@ exports.onClientEntry = function(_, pluginParams) {
dsn: __SENTRY_DSN__,
...pluginParams,
});

window.Sentry = Sentry;
window.Sentry = Sentry; // For backwards compatibility
};

function isSentryInitialized() {
// Although `window` should exist because we're in the browser (where this script
// is run), and `__SENTRY__.hub` is created when importing the Gatsby SDK, double
// check that in case something weird happens.
return !!(window && window.__SENTRY__ && window.__SENTRY__.hub && window.__SENTRY__.hub.getClient());
}

function areSentryOptionsDefined(params) {
if (params == undefined) return false;
// Even if there aren't any options, there's a `plugins` property defined as an empty array
if (Object.keys(params).length == 1 && Array.isArray(params.plugins) && params.plugins.length == 0) {
return false;
}
return true;
}
49 changes: 48 additions & 1 deletion packages/gatsby/gatsby-node.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const fs = require('fs');

const sentryRelease = JSON.stringify(
// Always read first as Sentry takes this as precedence
process.env.SENTRY_RELEASE ||
Expand All @@ -15,8 +17,9 @@ const sentryRelease = JSON.stringify(
);

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

exports.onCreateWebpackConfig = ({ plugins, actions }) => {
exports.onCreateWebpackConfig = ({ plugins, getConfig, actions }) => {
actions.setWebpackConfig({
plugins: [
plugins.define({
Expand All @@ -25,4 +28,48 @@ exports.onCreateWebpackConfig = ({ plugins, actions }) => {
}),
],
});

// To configure the SDK, SENTRY_USER_CONFIG is prioritized over `gatsby-config.js`,
// since it isn't possible to set non-serializable parameters in the latter.
// Prioritization here means what `init` is run.
let configFile = null;
try {
configFile = SENTRY_USER_CONFIG.find(file => fs.existsSync(file));
} catch (error) {
// Some node versions (like v11) throw an exception on `existsSync` instead of
// returning false. See https://github.com/tschaub/mock-fs/issues/256
}

if (!configFile) {
return;
}
// `setWebpackConfig` merges the Webpack config, ignoring some props like `entry`. See
// https://www.gatsbyjs.com/docs/reference/config-files/actions/#setWebpackConfig
// So it's not possible to inject the Sentry properties with that method. Instead, we
// can replace the whole config with the modifications we need.
const finalConfig = injectSentryConfig(getConfig(), configFile);
actions.replaceWebpackConfig(finalConfig);
};

function injectSentryConfig(config, configFile) {
const injectedEntries = {};
// TODO: investigate what entries need the Sentry config injected.
// We may want to skip some.
Object.keys(config.entry).map(prop => {
const value = config.entry[prop];
let injectedValue = value;
if (typeof value === 'string') {
injectedValue = [configFile, value];
} else if (Array.isArray(value)) {
injectedValue = [configFile, ...value];
} else {
// eslint-disable-next-line no-console
console.error(
`Sentry Logger [Error]: Could not inject SDK initialization code into ${prop}, unexpected format: `,
typeof value,
);
}
injectedEntries[prop] = injectedValue;
});
return { ...config, entry: injectedEntries };
iker-barriocanal marked this conversation as resolved.
Show resolved Hide resolved
}
80 changes: 77 additions & 3 deletions packages/gatsby/test/gatsby-browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ jest.mock('@sentry/gatsby', () => {
},
};
});
global.console.warn = jest.fn();
global.console.error = jest.fn();

let tracingAddExtensionMethods = jest.fn();
jest.mock('@sentry/tracing', () => {
Expand Down Expand Up @@ -50,9 +52,81 @@ describe('onClientEntry', () => {
}
});

it('sets window.Sentry', () => {
onClientEntry(undefined, {});
expect((window as any).Sentry).not.toBeUndefined();
describe('inits Sentry once', () => {
afterEach(() => {
delete (window as any).Sentry;
delete (window as any).__SENTRY__;
(global.console.warn as jest.Mock).mockClear();
(global.console.error as jest.Mock).mockClear();
});

function setMockedSentryInWindow() {
(window as any).__SENTRY__ = {
hub: {
getClient: () => ({
// Empty object mocking the client
}),
},
};
}

it('initialized in injected config, without pluginParams', () => {
setMockedSentryInWindow();
onClientEntry(undefined, { plugins: [] });
// eslint-disable-next-line no-console
expect(console.warn).not.toHaveBeenCalled();
// eslint-disable-next-line no-console
expect(console.error).not.toHaveBeenCalled();
expect(sentryInit).not.toHaveBeenCalled();
expect((window as any).Sentry).toBeDefined();
});

it('initialized in injected config, with pluginParams', () => {
setMockedSentryInWindow();
onClientEntry(undefined, { plugins: [], dsn: 'dsn', release: 'release' });
// eslint-disable-next-line no-console
expect((console.warn as jest.Mock).mock.calls[0]).toMatchInlineSnapshot(`
Array [
"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.
Learn more about the Gatsby SDK on https://docs.sentry.io/platforms/javascript/guides/gatsby/",
]
`);
// eslint-disable-next-line no-console
expect(console.error).not.toHaveBeenCalled();
expect(sentryInit).not.toHaveBeenCalled();
expect((window as any).Sentry).toBeDefined();
});

it('not initialized in injected config, without pluginParams', () => {
onClientEntry(undefined, { plugins: [] });
// eslint-disable-next-line no-console
expect(console.warn).not.toHaveBeenCalled();
// eslint-disable-next-line no-console
expect((console.error as jest.Mock).mock.calls[0]).toMatchInlineSnapshot(`
Array [
"Sentry Logger [Error]: No config for the Gatsby SDK was found.
Learn how to configure it on https://docs.sentry.io/platforms/javascript/guides/gatsby/",
]
`);
expect((window as any).Sentry).not.toBeDefined();
});

it('not initialized in injected config, with pluginParams', () => {
onClientEntry(undefined, { plugins: [], dsn: 'dsn', release: 'release' });
// eslint-disable-next-line no-console
expect(console.warn).not.toHaveBeenCalled();
// eslint-disable-next-line no-console
expect(console.error).not.toHaveBeenCalled();
expect(sentryInit).toHaveBeenCalledTimes(1);
expect(sentryInit.mock.calls[0][0]).toMatchInlineSnapshot(`
Object {
"dsn": "dsn",
"plugins": Array [],
"release": "release",
}
`);
expect((window as any).Sentry).toBeDefined();
});
});

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