Skip to content

ref(nextjs): Use generic loader to inject global values #6484

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

Merged
merged 6 commits into from
Dec 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions packages/nextjs/rollup.npm.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ export default [
...makeNPMConfigVariants(
makeBaseNPMConfig({
entrypoints: [
'src/config/templates/serverRewriteFramesPrefixLoaderTemplate.ts',
'src/config/templates/clientRewriteFramesPrefixLoaderTemplate.ts',
'src/config/templates/releasePrefixLoaderTemplate.ts',
'src/config/templates/pageProxyLoaderTemplate.ts',
'src/config/templates/apiProxyLoaderTemplate.ts',
],
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/src/config/loaders/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as valueInjectionLoader } from './valueInjectionLoader';
export { default as prefixLoader } from './prefixLoader';
export { default as proxyLoader } from './proxyLoader';
26 changes: 26 additions & 0 deletions packages/nextjs/src/config/loaders/valueInjectionLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { LoaderThis } from './types';

type LoaderOptions = {
values: Record<string, unknown>;
};

/**
* Set values on the global/window object at the start of a module.
*
* Options:
* - `values`: An object where the keys correspond to the keys of the global values to set and the values
* correspond to the values of the values on the global object. Values must be JSON serializable.
*/
export default function valueInjectionLoader(this: LoaderThis<LoaderOptions>, userCode: string): string {
// We know one or the other will be defined, depending on the version of webpack being used
const { values } = 'getOptions' in this ? this.getOptions() : this.query;

// Define some global proxy that works on server and on the browser.
let injectedCode = 'var _sentryCollisionFreeGlobalObject = typeof window === "undefined" ? global : window;\n';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

M: Do we need to worry about web workers/self here, do you think? Is there a reason not to just use GLOBAL_OBJ from @sentry/utils?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we don't need to worry about web workers here. The loader is only applied to sentry.server.config.js and sentry.client.config.js and I think we are safe to assume that sentry.client.config.js will not be injected into any web worker contexts. Unless I am missing something.

Considering the above I would just keep it as-is to keep the logic simple.


Object.entries(values).forEach(([key, value]) => {
injectedCode += `_sentryCollisionFreeGlobalObject["${key}"] = ${JSON.stringify(value)};\n`;
});

return `${injectedCode}\n${userCode}`;
}

This file was deleted.

This file was deleted.

This file was deleted.

123 changes: 66 additions & 57 deletions packages/nextjs/src/config/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,26 +87,8 @@ export function constructWebpackConfigFunction(
// `newConfig.module.rules` is required, so we don't have to keep asserting its existence
const newConfig = setUpModuleRules(rawNewConfig);

// Add a loader which will inject code that sets global values for use by `RewriteFrames`
addRewriteFramesLoader(newConfig, isServer ? 'server' : 'client', userNextConfig);

newConfig.module.rules.push({
test: /sentry\.(server|client)\.config\.(jsx?|tsx?)/,
use: [
{
// Inject the release value the same way the webpack plugin does.
loader: path.resolve(__dirname, 'loaders/prefixLoader.js'),
options: {
templatePrefix: 'release',
replacements: [
['__RELEASE__', webpackPluginOptions.release || process.env.SENTRY_RELEASE],
['__ORG__', webpackPluginOptions.org || process.env.SENTRY_ORG],
['__PROJECT__', webpackPluginOptions.project || process.env.SENTRY_PROJECT || ''],
],
},
},
],
});
// Add a loader which will inject code that sets global values
addValueInjectionLoader(newConfig, userNextConfig, webpackPluginOptions);

if (isServer) {
if (userSentryOptions.autoInstrumentServerFunctions !== false) {
Expand Down Expand Up @@ -667,49 +649,76 @@ function setUpModuleRules(newConfig: WebpackConfigObject): WebpackConfigObjectWi
}

/**
* Support the `distDir` and `assetPrefix` options by making their values (easy to get here at build-time) available at
* runtime (for use by `RewriteFrames`), by injecting code to attach their values to `global` or `window`.
*
* @param newConfig The webpack config object being constructed
* @param target Either 'server' or 'client'
* @param userNextConfig The user's nextjs config options
* Adds loaders to inject values on the global object based on user configuration.
*/
function addRewriteFramesLoader(
function addValueInjectionLoader(
newConfig: WebpackConfigObjectWithModuleRules,
target: 'server' | 'client',
userNextConfig: NextConfigObject,
webpackPluginOptions: SentryWebpackPlugin.SentryCliPluginOptions,
): void {
// Nextjs will use `basePath` in place of `assetPrefix` if it's defined but `assetPrefix` is not
const assetPrefix = userNextConfig.assetPrefix || userNextConfig.basePath || '';
const replacements = {
server: [
[
'__DIST_DIR__',
// Make sure that if we have a windows path, the backslashes are interpreted as such (rather than as escape
// characters)
userNextConfig.distDir?.replace(/\\/g, '\\\\') || '.next',
],
],
client: [
[
'__ASSET_PREFIX_PATH__',
// Get the path part of `assetPrefix`, minus any trailing slash. (We use a placeholder for the origin if
// `assetPreix` doesn't include one. Since we only care about the path, it doesn't matter what it is.)
assetPrefix ? new URL(assetPrefix, 'http://dogs.are.great').pathname.replace(/\/$/, '') : '',
],
],
const releaseValue = webpackPluginOptions.release || process.env.SENTRY_RELEASE;
const orgValue = webpackPluginOptions.org || process.env.SENTRY_ORG;
const projectValue = webpackPluginOptions.project || process.env.SENTRY_PROJECT;

const isomorphicValues = {
// Inject release into SDK
...(releaseValue
? {
SENTRY_RELEASE: {
id: releaseValue,
},
}
: undefined),

// Enable module federation support (see https://github.com/getsentry/sentry-webpack-plugin/pull/307)
...(projectValue && releaseValue
? {
SENTRY_RELEASES: {
[orgValue ? `${projectValue}@${orgValue}` : projectValue]: { id: releaseValue },
},
}
: undefined),
};

newConfig.module.rules.push({
test: new RegExp(`sentry\\.${target}\\.config\\.(jsx?|tsx?)`),
use: [
{
loader: path.resolve(__dirname, 'loaders/prefixLoader.js'),
options: {
templatePrefix: `${target}RewriteFrames`,
replacements: replacements[target],
const serverValues = {
...isomorphicValues,
// Make sure that if we have a windows path, the backslashes are interpreted as such (rather than as escape
// characters)
__rewriteFramesDistDir__: userNextConfig.distDir?.replace(/\\/g, '\\\\') || '.next',
};

const clientValues = {
...isomorphicValues,
// Get the path part of `assetPrefix`, minus any trailing slash. (We use a placeholder for the origin if
// `assetPreix` doesn't include one. Since we only care about the path, it doesn't matter what it is.)
__rewriteFramesAssetPrefixPath__: assetPrefix
? new URL(assetPrefix, 'http://dogs.are.great').pathname.replace(/\/$/, '')
: '',
};

newConfig.module.rules.push(
{
test: /sentry\.server\.config\.(jsx?|tsx?)/,
use: [
{
loader: path.resolve(__dirname, 'loaders/valueInjectionLoader.js'),
options: {
values: serverValues,
},
},
},
],
});
],
},
{
test: /sentry\.client\.config\.(jsx?|tsx?)/,
use: [
{
loader: path.resolve(__dirname, 'loaders/valueInjectionLoader.js'),
options: {
values: clientValues,
},
},
],
},
);
}
7 changes: 5 additions & 2 deletions packages/nextjs/src/index.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ declare const __SENTRY_TRACING__: boolean;
// https://github.com/vercel/next.js/blob/166e5fb9b92f64c4b5d1f6560a05e2b9778c16fb/packages/next/build/webpack-config.ts#L206
declare const EdgeRuntime: string | undefined;

type GlobalWithAssetPrefixPath = typeof global & { __rewriteFramesAssetPrefixPath__: string };
const globalWithInjectedValues = global as typeof global & {
__rewriteFramesAssetPrefixPath__: string;
__sentryRewritesTunnelPath__?: string;
};

/** Inits the Sentry NextJS SDK on the browser with the React SDK. */
export function init(options: NextjsOptions): void {
Expand Down Expand Up @@ -67,7 +70,7 @@ function addClientIntegrations(options: NextjsOptions): void {

// This value is injected at build time, based on the output directory specified in the build config. Though a default
// is set there, we set it here as well, just in case something has gone wrong with the injection.
const assetPrefixPath = (global as GlobalWithAssetPrefixPath).__rewriteFramesAssetPrefixPath__ || '';
const assetPrefixPath = globalWithInjectedValues.__rewriteFramesAssetPrefixPath__ || '';

const defaultRewriteFramesIntegration = new RewriteFrames({
// Turn `<origin>/<path>/_next/static/...` into `app:///_next/static/...`
Expand Down
7 changes: 5 additions & 2 deletions packages/nextjs/src/index.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ export { captureUnderscoreErrorException } from './utils/_error';
// because or SSR of next.js we can only use this.
export { ErrorBoundary, showReportDialog, withErrorBoundary } from '@sentry/react';

type GlobalWithDistDir = typeof global & { __rewriteFramesDistDir__: string };
const globalWithInjectedValues = global as typeof global & {
__rewriteFramesDistDir__: string;
};

const domain = domainModule as typeof domainModule & { active: (domainModule.Domain & Carrier) | null };

// This is a variable that Next.js will string replace during build with a string if run in an edge runtime from Next.js
Expand Down Expand Up @@ -114,7 +117,7 @@ function addServerIntegrations(options: NextjsOptions): void {

// This value is injected at build time, based on the output directory specified in the build config. Though a default
// is set there, we set it here as well, just in case something has gone wrong with the injection.
const distDirName = (global as GlobalWithDistDir).__rewriteFramesDistDir__ || '.next';
const distDirName = globalWithInjectedValues.__rewriteFramesDistDir__ || '.next';
// nextjs always puts the build directory at the project root level, which is also where you run `next start` from, so
// we can read in the project directory from the currently running process
const distDirAbsPath = path.resolve(process.cwd(), distDirName);
Expand Down
71 changes: 12 additions & 59 deletions packages/nextjs/test/config/loaders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
exportedNextConfig,
serverBuildContext,
serverWebpackConfig,
userSentryWebpackPluginConfig,
} from './fixtures';
import { materializeFinalWebpackConfig } from './testUtils';

Expand Down Expand Up @@ -41,7 +40,7 @@ declare global {

describe('webpack loaders', () => {
describe('server loaders', () => {
it('adds server `RewriteFrames` loader to server config', async () => {
it('adds server `valueInjection` loader to server config', async () => {
const finalWebpackConfig = await materializeFinalWebpackConfig({
exportedNextConfig,
incomingWebpackConfig: serverWebpackConfig,
Expand All @@ -52,42 +51,19 @@ describe('webpack loaders', () => {
test: /sentry\.server\.config\.(jsx?|tsx?)/,
use: [
{
loader: expect.stringEndingWith('prefixLoader.js'),
options: expect.objectContaining({ templatePrefix: 'serverRewriteFrames' }),
},
],
});
});

it('adds release prefix loader to server config', async () => {
const finalWebpackConfig = await materializeFinalWebpackConfig({
exportedNextConfig,
incomingWebpackConfig: serverWebpackConfig,
incomingWebpackBuildContext: serverBuildContext,
userSentryWebpackPluginConfig: userSentryWebpackPluginConfig,
});

expect(finalWebpackConfig.module.rules).toContainEqual({
test: /sentry\.(server|client)\.config\.(jsx?|tsx?)/,
use: [
{
loader: expect.stringEndingWith('prefixLoader.js'),
options: {
templatePrefix: 'release',
replacements: [
['__RELEASE__', 'doGsaREgReaT'],
['__ORG__', 'squirrelChasers'],
['__PROJECT__', 'simulator'],
],
},
loader: expect.stringEndingWith('valueInjectionLoader.js'),
// We use `expect.objectContaining({})` rather than `expect.any(Object)` to match any plain object because
// the latter will also match arrays, regexes, dates, sets, etc. - anything whose `typeof` value is
// `'object'`.
options: expect.objectContaining({ values: expect.objectContaining({}) }),
},
],
});
});
});

describe('client loaders', () => {
it('adds `RewriteFrames` loader to client config', async () => {
it('adds `valueInjection` loader to client config', async () => {
const finalWebpackConfig = await materializeFinalWebpackConfig({
exportedNextConfig,
incomingWebpackConfig: clientWebpackConfig,
Expand All @@ -98,34 +74,11 @@ describe('webpack loaders', () => {
test: /sentry\.client\.config\.(jsx?|tsx?)/,
use: [
{
loader: expect.stringEndingWith('prefixLoader.js'),
options: expect.objectContaining({ templatePrefix: 'clientRewriteFrames' }),
},
],
});
});

it('adds release prefix loader to client config', async () => {
const finalWebpackConfig = await materializeFinalWebpackConfig({
exportedNextConfig,
incomingWebpackConfig: clientWebpackConfig,
incomingWebpackBuildContext: clientBuildContext,
userSentryWebpackPluginConfig: userSentryWebpackPluginConfig,
});

expect(finalWebpackConfig.module.rules).toContainEqual({
test: /sentry\.(server|client)\.config\.(jsx?|tsx?)/,
use: [
{
loader: expect.stringEndingWith('prefixLoader.js'),
options: {
templatePrefix: 'release',
replacements: [
['__RELEASE__', 'doGsaREgReaT'],
['__ORG__', 'squirrelChasers'],
['__PROJECT__', 'simulator'],
],
},
loader: expect.stringEndingWith('valueInjectionLoader.js'),
// We use `expect.objectContaining({})` rather than `expect.any(Object)` to match any plain object because
// the latter will also match arrays, regexes, dates, sets, etc. - anything whose `typeof` value is
// `'object'`.
options: expect.objectContaining({ values: expect.objectContaining({}) }),
},
],
});
Expand Down