Skip to content

feat(nextjs): Support assetPrefix option #6388

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 8 commits into from
Dec 2, 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
1 change: 1 addition & 0 deletions packages/nextjs/rollup.npm.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default [
makeBaseNPMConfig({
entrypoints: [
'src/config/templates/serverRewriteFramesPrefixLoaderTemplate.ts',
'src/config/templates/clientRewriteFramesPrefixLoaderTemplate.ts',
'src/config/templates/pageProxyLoaderTemplate.ts',
'src/config/templates/apiProxyLoaderTemplate.ts',
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* eslint-disable no-restricted-globals */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */

(window as any).__rewriteFramesAssetPrefixPath__ = '__ASSET_PREFIX_PATH__';

// We need this to make this file an ESM module, which TS requires when using `isolatedModules`, but it doesn't affect
// the end result - Rollup recognizes that it's a no-op and doesn't include it when building our code.
export {};
2 changes: 2 additions & 0 deletions packages/nextjs/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export type NextConfigObject = {
target?: 'server' | 'experimental-serverless-trace';
// The output directory for the built app (defaults to ".next")
distDir?: string;
// URL location of `_next/static` directory when hosted on a CDN
assetPrefix?: string;
// The root at which the nextjs app will be served (defaults to "/")
basePath?: string;
// Config which will be available at runtime
Expand Down
26 changes: 18 additions & 8 deletions packages/nextjs/src/config/webpack.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable complexity */
/* eslint-disable max-lines */
import { getSentryRelease } from '@sentry/node';
import {
Expand Down Expand Up @@ -80,10 +81,10 @@ export function constructWebpackConfigFunction(
// `newConfig.module.rules` is required, so we don't have to keep asserting its existence
const newConfig = setUpModuleRules(rawNewConfig);

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

if (isServer) {
if (userSentryOptions.autoInstrumentServerFunctions !== false) {
const pagesDir = newConfig.resolve?.alias?.['private-next-pages'] as string;

Expand Down Expand Up @@ -498,7 +499,7 @@ export function getWebpackPluginOptions(
const isWebpack5 = webpack.version.startsWith('5');
const isServerless = userNextConfig.target === 'experimental-serverless-trace';
const hasSentryProperties = fs.existsSync(path.resolve(projectDir, 'sentry.properties'));
const urlPrefix = userNextConfig.basePath ? `~${userNextConfig.basePath}/_next` : '~/_next';
const urlPrefix = '~/_next';

const serverInclude = isServerless
? [{ paths: [`${distDirAbsPath}/serverless/`], urlPrefix: `${urlPrefix}/serverless` }]
Expand Down Expand Up @@ -645,8 +646,8 @@ function setUpModuleRules(newConfig: WebpackConfigObject): WebpackConfigObjectWi
}

/**
* Support the `distDir` option by making its value (easy to get here at build-time) available to the server SDK's
* default `RewriteFrames` instance (which needs it at runtime), by injecting code to attach it to `global`.
* 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'
Expand All @@ -666,6 +667,16 @@ function addRewriteFramesLoader(
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.)
userNextConfig.assetPrefix
? new URL(userNextConfig.assetPrefix, 'http://dogs.are.great').pathname.replace(/\/$/, '')
: '',
],
],
};

newConfig.module.rules.push({
Expand All @@ -675,8 +686,7 @@ function addRewriteFramesLoader(
loader: path.resolve(__dirname, 'loaders/prefixLoader.js'),
options: {
templatePrefix: `${target}RewriteFrames`,
// This weird cast will go away as soon as we add the client half of this function in
replacements: replacements[target as 'server'],
replacements: replacements[target],
},
},
],
Expand Down
22 changes: 22 additions & 0 deletions packages/nextjs/src/index.client.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { RewriteFrames } from '@sentry/integrations';
import { configureScope, init as reactInit, Integrations } from '@sentry/react';
import { BrowserTracing, defaultRequestInstrumentationOptions, hasTracingEnabled } from '@sentry/tracing';
import { EventProcessor } from '@sentry/types';
Expand Down Expand Up @@ -28,6 +29,8 @@ export { BrowserTracing };
// Treeshakable guard to remove all code related to tracing
declare const __SENTRY_TRACING__: boolean;

type GlobalWithAssetPrefixPath = typeof global & { __rewriteFramesAssetPrefixPath__: string };

/** Inits the Sentry NextJS SDK on the browser with the React SDK. */
export function init(options: NextjsOptions): void {
buildMetadata(options, ['nextjs', 'react']);
Expand All @@ -48,6 +51,25 @@ export function init(options: NextjsOptions): void {
function addClientIntegrations(options: NextjsOptions): void {
let integrations = options.integrations || [];

// 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 defaultRewriteFramesIntegration = new RewriteFrames({
// Turn `<origin>/<path>/_next/static/...` into `app:///_next/static/...`
iteratee: frame => {
try {
const { origin } = new URL(frame.filename as string);
frame.filename = frame.filename?.replace(origin, 'app://').replace(assetPrefixPath, '');
} catch (err) {
// Filename wasn't a properly formed URL, so there's nothing we can do
}

return frame;
},
});
integrations = addOrUpdateIntegration(defaultRewriteFramesIntegration, integrations);

// This evaluates to true unless __SENTRY_TRACING__ is text-replaced with "false", in which case everything inside
// will get treeshaken away
if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) {
Expand Down
21 changes: 10 additions & 11 deletions packages/nextjs/test/config/loaders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,23 +60,22 @@ describe('webpack loaders', () => {
});

describe('client loaders', () => {
it("doesn't add `RewriteFrames` loader to client config", async () => {
it('adds `RewriteFrames` loader to client config', async () => {
const finalWebpackConfig = await materializeFinalWebpackConfig({
exportedNextConfig,
incomingWebpackConfig: clientWebpackConfig,
incomingWebpackBuildContext: clientBuildContext,
});

expect(finalWebpackConfig.module.rules).not.toContainEqual(
expect.objectContaining({
use: [
{
loader: expect.stringEndingWith('prefixLoader.js'),
options: expect.objectContaining({ templatePrefix: expect.stringContaining('RewriteFrames') }),
},
],
}),
);
expect(finalWebpackConfig.module.rules).toContainEqual({
test: /sentry\.client\.config\.(jsx?|tsx?)/,
use: [
{
loader: expect.stringEndingWith('prefixLoader.js'),
options: expect.objectContaining({ templatePrefix: 'clientRewriteFrames' }),
},
],
});
});
});
});
Expand Down
93 changes: 0 additions & 93 deletions packages/nextjs/test/config/webpack/sentryWebpackPlugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,99 +202,6 @@ describe('Sentry webpack plugin config', () => {
});
});

describe("Sentry webpack plugin `include` option with basePath filled on next's config", () => {
const exportedNextConfigWithBasePath = {
...exportedNextConfig,
basePath: '/city-park',
};

it('has the correct value when building client bundles', async () => {
const buildContext = getBuildContext('client', exportedNextConfigWithBasePath);
const finalWebpackConfig = await materializeFinalWebpackConfig({
exportedNextConfig: exportedNextConfigWithBasePath,
incomingWebpackConfig: clientWebpackConfig,
incomingWebpackBuildContext: buildContext,
});

const sentryWebpackPluginInstance = findWebpackPlugin(
finalWebpackConfig,
'SentryCliPlugin',
) as SentryWebpackPlugin;

expect(sentryWebpackPluginInstance.options.include).toEqual([
{
paths: [`${buildContext.dir}/.next/static/chunks/pages`],
urlPrefix: '~/city-park/_next/static/chunks/pages',
},
]);
});

it('has the correct value when building serverless server bundles', async () => {
const exportedNextConfigServerless = {
...exportedNextConfigWithBasePath,
target: 'experimental-serverless-trace' as const,
};
const buildContext = getBuildContext('server', exportedNextConfigServerless);

const finalWebpackConfig = await materializeFinalWebpackConfig({
exportedNextConfig: exportedNextConfigServerless,
incomingWebpackConfig: serverWebpackConfig,
incomingWebpackBuildContext: buildContext,
});

const sentryWebpackPluginInstance = findWebpackPlugin(
finalWebpackConfig,
'SentryCliPlugin',
) as SentryWebpackPlugin;

expect(sentryWebpackPluginInstance.options.include).toEqual([
{ paths: [`${buildContext.dir}/.next/serverless/`], urlPrefix: '~/city-park/_next/serverless' },
]);
});

it('has the correct value when building serverful server bundles using webpack 4', async () => {
const serverBuildContextWebpack4 = getBuildContext('server', exportedNextConfigWithBasePath);
serverBuildContextWebpack4.webpack.version = '4.15.13';

const finalWebpackConfig = await materializeFinalWebpackConfig({
exportedNextConfig: exportedNextConfigWithBasePath,
incomingWebpackConfig: serverWebpackConfig,
incomingWebpackBuildContext: serverBuildContextWebpack4,
});

const sentryWebpackPluginInstance = findWebpackPlugin(
finalWebpackConfig,
'SentryCliPlugin',
) as SentryWebpackPlugin;

expect(sentryWebpackPluginInstance.options.include).toEqual([
{
paths: [`${serverBuildContextWebpack4.dir}/.next/server/pages/`],
urlPrefix: '~/city-park/_next/server/pages',
},
]);
});

it('has the correct value when building serverful server bundles using webpack 5', async () => {
const buildContext = getBuildContext('server', exportedNextConfigWithBasePath);
const finalWebpackConfig = await materializeFinalWebpackConfig({
exportedNextConfig: exportedNextConfigWithBasePath,
incomingWebpackConfig: serverWebpackConfig,
incomingWebpackBuildContext: buildContext,
});

const sentryWebpackPluginInstance = findWebpackPlugin(
finalWebpackConfig,
'SentryCliPlugin',
) as SentryWebpackPlugin;

expect(sentryWebpackPluginInstance.options.include).toEqual([
{ paths: [`${buildContext.dir}/.next/server/pages/`], urlPrefix: '~/city-park/_next/server/pages' },
{ paths: [`${buildContext.dir}/.next/server/chunks/`], urlPrefix: '~/city-park/_next/server/chunks' },
]);
});
});

describe('SentryWebpackPlugin enablement', () => {
let processEnvBackup: typeof process.env;

Expand Down
6 changes: 5 additions & 1 deletion packages/nextjs/test/index.client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ describe('Client init()', () => {
},
},
environment: 'test',
integrations: [],
integrations: expect.arrayContaining([
expect.objectContaining({
name: 'RewriteFrames',
}),
]),
}),
);
});
Expand Down