Skip to content

Commit

Permalink
feat(nextjs): Add experimental flag to not strip origin information f…
Browse files Browse the repository at this point in the history
…rom different origin stack frames (#15418)
  • Loading branch information
lforst authored Feb 24, 2025
1 parent 08569e6 commit bac7387
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 20 deletions.
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ module.exports = [
import: createImport('init'),
ignore: ['next/router', 'next/constants'],
gzip: true,
limit: '40 KB',
limit: '41 KB',
},
// SvelteKit SDK (ESM)
{
Expand Down
86 changes: 70 additions & 16 deletions packages/nextjs/src/client/clientNormalizationIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,84 @@ import { rewriteFramesIntegration } from '@sentry/browser';
import { defineIntegration } from '@sentry/core';

export const nextjsClientStackFrameNormalizationIntegration = defineIntegration(
({ assetPrefixPath }: { assetPrefixPath: string }) => {
({
assetPrefix,
basePath,
rewriteFramesAssetPrefixPath,
experimentalThirdPartyOriginStackFrames,
}: {
assetPrefix?: string;
basePath?: string;
rewriteFramesAssetPrefixPath: string;
experimentalThirdPartyOriginStackFrames: boolean;
}) => {
const rewriteFramesInstance = rewriteFramesIntegration({
// 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
if (experimentalThirdPartyOriginStackFrames) {
// Not sure why but access to global WINDOW from @sentry/Browser causes hideous ci errors
// eslint-disable-next-line no-restricted-globals
const windowOrigin = typeof window !== 'undefined' && window.location ? window.location.origin : '';
// A filename starting with the local origin and not ending with JS is most likely JS in HTML which we do not want to rewrite
if (frame.filename?.startsWith(windowOrigin) && !frame.filename.endsWith('.js')) {
return frame;
}

if (assetPrefix) {
// If the user defined an asset prefix, we need to strip it so that we can match it with uploaded sourcemaps.
// assetPrefix always takes priority over basePath.
if (frame.filename?.startsWith(assetPrefix)) {
frame.filename = frame.filename.replace(assetPrefix, 'app://');
}
} else if (basePath) {
// If the user defined a base path, we need to strip it to match with uploaded sourcemaps.
// We should only do this for same-origin filenames though, so that third party assets are not rewritten.
try {
const { origin: frameOrigin } = new URL(frame.filename as string);
if (frameOrigin === windowOrigin) {
frame.filename = frame.filename?.replace(frameOrigin, 'app://').replace(basePath, '');
}
} catch (err) {
// Filename wasn't a properly formed URL, so there's nothing we can do
}
}
} else {
try {
const { origin } = new URL(frame.filename as string);
frame.filename = frame.filename?.replace(origin, 'app://').replace(rewriteFramesAssetPrefixPath, '');
} catch (err) {
// Filename wasn't a properly formed URL, so there's nothing we can do
}
}

// We need to URI-decode the filename because Next.js has wildcard routes like "/users/[id].js" which show up as "/users/%5id%5.js" in Error stacktraces.
// The corresponding sources that Next.js generates have proper brackets so we also need proper brackets in the frame so that source map resolving works.
if (frame.filename?.startsWith('app:///_next')) {
frame.filename = decodeURI(frame.filename);
}
if (experimentalThirdPartyOriginStackFrames) {
if (frame.filename?.includes('/_next')) {
frame.filename = decodeURI(frame.filename);
}

if (
frame.filename?.match(
/\/_next\/static\/chunks\/(main-|main-app-|polyfills-|webpack-|framework-|framework\.)[0-9a-f]+\.js$/,
)
) {
// We don't care about these frames. It's Next.js internal code.
frame.in_app = false;
}
} else {
if (frame.filename?.startsWith('app:///_next')) {
frame.filename = decodeURI(frame.filename);
}

if (
frame.filename?.match(
/^app:\/\/\/_next\/static\/chunks\/(main-|main-app-|polyfills-|webpack-|framework-|framework\.)[0-9a-f]+\.js$/,
)
) {
// We don't care about these frames. It's Next.js internal code.
frame.in_app = false;
if (
frame.filename?.match(
/^app:\/\/\/_next\/static\/chunks\/(main-|main-app-|polyfills-|webpack-|framework-|framework\.)[0-9a-f]+\.js$/,
)
) {
// We don't care about these frames. It's Next.js internal code.
frame.in_app = false;
}
}

return frame;
Expand Down
21 changes: 18 additions & 3 deletions packages/nextjs/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export { browserTracingIntegration } from './browserTracingIntegration';

const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
_sentryRewriteFramesAssetPrefixPath: string;
_sentryAssetPrefix?: string;
_sentryBasePath?: string;
_experimentalThirdPartyOriginStackFrames?: string;
};

// Treeshakable guard to remove all code related to tracing
Expand Down Expand Up @@ -67,13 +70,25 @@ function getDefaultIntegrations(options: BrowserOptions): Integration[] {
customDefaultIntegrations.push(browserTracingIntegration());
}

// This value is injected at build time, based on the output directory specified in the build config. Though a default
// These values are 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 =
const rewriteFramesAssetPrefixPath =
process.env._sentryRewriteFramesAssetPrefixPath ||
globalWithInjectedValues._sentryRewriteFramesAssetPrefixPath ||
'';
customDefaultIntegrations.push(nextjsClientStackFrameNormalizationIntegration({ assetPrefixPath }));
const assetPrefix = process.env._sentryAssetPrefix || globalWithInjectedValues._sentryAssetPrefix;
const basePath = process.env._sentryBasePath || globalWithInjectedValues._sentryBasePath;
const experimentalThirdPartyOriginStackFrames =
process.env._experimentalThirdPartyOriginStackFrames === 'true' ||
globalWithInjectedValues._experimentalThirdPartyOriginStackFrames === 'true';
customDefaultIntegrations.push(
nextjsClientStackFrameNormalizationIntegration({
assetPrefix,
basePath,
rewriteFramesAssetPrefixPath,
experimentalThirdPartyOriginStackFrames,
}),
);

return customDefaultIntegrations;
}
Expand Down
9 changes: 9 additions & 0 deletions packages/nextjs/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,15 @@ export type SentryBuildOptions = {
* Defaults to `false`.
*/
automaticVercelMonitors?: boolean;

/**
* Contains a set of experimental flags that might change in future releases. These flags enable
* features that are still in development and may be modified, renamed, or removed without notice.
* Use with caution in production environments.
*/
_experimental?: Partial<{
thirdPartyOriginStackFrames: boolean;
}>;
};

export type NextConfigFunction = (
Expand Down
4 changes: 4 additions & 0 deletions packages/nextjs/src/config/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,10 @@ function addValueInjectionLoader(
_sentryRewriteFramesAssetPrefixPath: assetPrefix
? new URL(assetPrefix, 'http://dogs.are.great').pathname.replace(/\/$/, '')
: '',
_sentryAssetPrefix: userNextConfig.assetPrefix,
_sentryExperimentalThirdPartyOriginStackFrames: userSentryOptions._experimental?.thirdPartyOriginStackFrames
? 'true'
: undefined,
};

if (buildContext.isServer) {
Expand Down
16 changes: 16 additions & 0 deletions packages/nextjs/src/config/withSentryConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,14 @@ function setUpBuildTimeVariables(userNextConfig: NextConfigObject, userSentryOpt
: '',
};

if (userNextConfig.assetPrefix) {
buildTimeVariables._assetsPrefix = userNextConfig.assetPrefix;
}

if (userSentryOptions._experimental?.thirdPartyOriginStackFrames) {
buildTimeVariables._experimentalThirdPartyOriginStackFrames = 'true';
}

if (rewritesTunnelPath) {
buildTimeVariables._sentryRewritesTunnelPath = rewritesTunnelPath;
}
Expand All @@ -276,6 +284,14 @@ function setUpBuildTimeVariables(userNextConfig: NextConfigObject, userSentryOpt
buildTimeVariables._sentryBasePath = basePath;
}

if (userNextConfig.assetPrefix) {
buildTimeVariables._sentryAssetPrefix = userNextConfig.assetPrefix;
}

if (userSentryOptions._experimental?.thirdPartyOriginStackFrames) {
buildTimeVariables._experimentalThirdPartyOriginStackFrames = 'true';
}

if (typeof userNextConfig.env === 'object') {
userNextConfig.env = { ...buildTimeVariables, ...userNextConfig.env };
} else if (userNextConfig.env === undefined) {
Expand Down

0 comments on commit bac7387

Please sign in to comment.