Skip to content

Commit 7c9b129

Browse files
author
Luca Forstner
committed
feat(nextjs): Add option to automatically tunnel events
1 parent 56a1391 commit 7c9b129

File tree

5 files changed

+106
-11
lines changed

5 files changed

+106
-11
lines changed

packages/nextjs/src/config/types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ export type NextConfigFunctionWithSentry = (
2727
defaults: { defaultConfig: NextConfigObject },
2828
) => NextConfigObjectWithSentry;
2929

30+
// Vendored from Next.js (this type is not complete - extend if necessary)
31+
type Rewrite = {
32+
source: string;
33+
destination: string;
34+
};
35+
3036
export type NextConfigObject = {
3137
// Custom webpack options
3238
webpack?: WebpackConfigFunction | null;
@@ -42,6 +48,15 @@ export type NextConfigObject = {
4248
publicRuntimeConfig?: { [key: string]: unknown };
4349
// File extensions that count as pages in the `pages/` directory
4450
pageExtensions?: string[];
51+
// Paths to reroute when requested
52+
rewrites?: () => Promise<
53+
| Rewrite[]
54+
| {
55+
beforeFiles: Rewrite[];
56+
afterFiles: Rewrite[];
57+
fallback: Rewrite[];
58+
}
59+
>;
4560
};
4661

4762
export type UserSentryOptions = {
@@ -75,6 +90,9 @@ export type UserSentryOptions = {
7590
// (`pages/animals/index.js` or `.\src\pages\api\animals\[animalType]\habitat.tsx`), and strings must be be a full,
7691
// exact match.
7792
excludeServerRoutes?: Array<RegExp | string>;
93+
94+
// TODO
95+
rewritesTunnel?: string;
7896
};
7997

8098
export type NextConfigFunction = (phase: string, defaults: { defaultConfig: NextConfigObject }) => NextConfigObject;

packages/nextjs/src/config/webpack.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export function constructWebpackConfigFunction(
8888
const newConfig = setUpModuleRules(rawNewConfig);
8989

9090
// Add a loader which will inject code that sets global values
91-
addValueInjectionLoader(newConfig, userNextConfig, webpackPluginOptions);
91+
addValueInjectionLoader(newConfig, userNextConfig, userSentryOptions, webpackPluginOptions);
9292

9393
if (isServer) {
9494
if (userSentryOptions.autoInstrumentServerFunctions !== false) {
@@ -654,6 +654,7 @@ function setUpModuleRules(newConfig: WebpackConfigObject): WebpackConfigObjectWi
654654
function addValueInjectionLoader(
655655
newConfig: WebpackConfigObjectWithModuleRules,
656656
userNextConfig: NextConfigObject,
657+
userSentryOptions: UserSentryOptions,
657658
webpackPluginOptions: SentryWebpackPlugin.SentryCliPluginOptions,
658659
): void {
659660
const assetPrefix = userNextConfig.assetPrefix || userNextConfig.basePath || '';
@@ -693,6 +694,9 @@ function addValueInjectionLoader(
693694
},
694695
}
695696
: undefined),
697+
698+
// `rewritesTunnel` set by the user in Next.js config
699+
__sentryRewritesTunnelPath__: userSentryOptions.rewritesTunnel,
696700
};
697701

698702
newConfig.module.rules.push(

packages/nextjs/src/config/withSentryConfig.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ function getFinalConfigObject(
4040
// to `constructWebpackConfigFunction` so that it can live in the returned function's closure.
4141
const { sentry: userSentryOptions } = incomingUserNextConfigObject;
4242
delete incomingUserNextConfigObject.sentry;
43-
// Remind TS that there's now no `sentry` property
44-
const userNextConfigObject = incomingUserNextConfigObject as NextConfigObject;
43+
44+
if (userSentryOptions?.rewritesTunnel) {
45+
setUpTunnelRewriteRules(incomingUserNextConfigObject, userSentryOptions.rewritesTunnel);
46+
}
4547

4648
// In order to prevent all of our build-time code from being bundled in people's route-handling serverless functions,
4749
// we exclude `webpack.ts` and all of its dependencies from nextjs's `@vercel/nft` filetracing. We therefore need to
@@ -50,11 +52,51 @@ function getFinalConfigObject(
5052
// eslint-disable-next-line @typescript-eslint/no-var-requires
5153
const { constructWebpackConfigFunction } = require('./webpack');
5254
return {
53-
...userNextConfigObject,
54-
webpack: constructWebpackConfigFunction(userNextConfigObject, userSentryWebpackPluginOptions, userSentryOptions),
55+
...incomingUserNextConfigObject,
56+
webpack: constructWebpackConfigFunction(
57+
incomingUserNextConfigObject,
58+
userSentryWebpackPluginOptions,
59+
userSentryOptions,
60+
),
5561
};
5662
}
5763

5864
// At runtime, we just return the user's config untouched.
59-
return userNextConfigObject;
65+
return incomingUserNextConfigObject;
66+
}
67+
68+
/**
69+
* Injects rewrite rules into the Next.js config provided by the user to tunnel
70+
* requests from the `tunnelPath` to Sentry.
71+
*/
72+
function setUpTunnelRewriteRules(userNextConfig: NextConfigObject, tunnelPath: string): void {
73+
const originalRewrites = userNextConfig.rewrites;
74+
75+
// This function doesn't take any arguments at the time of writing but we future-proof
76+
// here in case Next.js ever decides to pass some
77+
userNextConfig.rewrites = async (...args: unknown[]) => {
78+
const injectedRewrites = [
79+
{
80+
source: `${tunnelPath}/:orgid/:projectid`, // without trailing slash
81+
destination: 'https://:orgid.ingest.sentry.io/api/:projectid/envelope/',
82+
},
83+
{
84+
source: `${tunnelPath}/:orgid/:projectid/`, // with trailing slash
85+
destination: 'https://:orgid.ingest.sentry.io/api/:projectid/envelope/',
86+
},
87+
];
88+
89+
if (typeof originalRewrites === 'function') {
90+
// @ts-ignore weird args error
91+
const intermediaryValue = await originalRewrites(...args);
92+
if (Array.isArray(intermediaryValue)) {
93+
intermediaryValue.push(...injectedRewrites);
94+
} else {
95+
intermediaryValue.beforeFiles.push(...injectedRewrites);
96+
}
97+
return intermediaryValue;
98+
} else {
99+
return injectedRewrites;
100+
}
101+
};
60102
}

packages/nextjs/src/index.client.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { RewriteFrames } from '@sentry/integrations';
22
import { configureScope, init as reactInit, Integrations } from '@sentry/react';
33
import { BrowserTracing, defaultRequestInstrumentationOptions, hasTracingEnabled } from '@sentry/tracing';
44
import { EventProcessor } from '@sentry/types';
5-
import { logger } from '@sentry/utils';
5+
import { dsnFromString, logger } from '@sentry/utils';
66

77
import { nextRouterInstrumentation } from './performance/client';
88
import { buildMetadata } from './utils/metadata';
@@ -34,7 +34,6 @@ declare const __SENTRY_TRACING__: boolean;
3434
// v12.2.1-canary.3 onwards:
3535
// https://github.com/vercel/next.js/blob/166e5fb9b92f64c4b5d1f6560a05e2b9778c16fb/packages/next/build/webpack-config.ts#L206
3636
declare const EdgeRuntime: string | undefined;
37-
3837
const globalWithInjectedValues = global as typeof global & {
3938
__rewriteFramesAssetPrefixPath__: string;
4039
__sentryRewritesTunnelPath__?: string;
@@ -50,6 +49,19 @@ export function init(options: NextjsOptions): void {
5049
return;
5150
}
5251

52+
let tunnelPath: string | undefined;
53+
if (globalWithInjectedValues.__sentryRewritesTunnelPath__ && options.dsn) {
54+
const dsnComponents = dsnFromString(options.dsn);
55+
if (dsnComponents.host.match(/^o\d+\.ingest\.sentry\.io$/)) {
56+
const orgId = dsnComponents.host.split('.')[0];
57+
tunnelPath = `${globalWithInjectedValues.__sentryRewritesTunnelPath__}/${orgId}/${dsnComponents.projectId}/`;
58+
__DEBUG_BUILD__ && logger.info(`Tunneling events to "${tunnelPath}"`);
59+
options.tunnel = tunnelPath;
60+
} else {
61+
__DEBUG_BUILD__ && logger.warn('Provided DSN is not a Sentry SaaS DSN. Will not tunnel events.');
62+
}
63+
}
64+
5365
buildMetadata(options, ['nextjs', 'react']);
5466
options.environment = options.environment || process.env.NODE_ENV;
5567
addClientIntegrations(options);

packages/nextjs/src/utils/instrumentServer.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ type WrappedPageComponentFinder = PageComponentFinder;
7676
let liveServer: Server;
7777
let sdkSetupComplete = false;
7878

79+
const globalWithInjectedValues = global as typeof global & {
80+
__sentryRewritesTunnelPath__?: string;
81+
};
82+
7983
/**
8084
* Do the monkeypatching and wrapping necessary to catch errors in page routes and record transactions for both page and
8185
* API routes.
@@ -346,12 +350,27 @@ function makeWrappedMethodForGettingParameterizedPath(
346350
/**
347351
* Determine if the request should be traced, by filtering out requests for internal next files and static resources.
348352
*
349-
* @param url The URL of the request
353+
* @param path The path of the request
350354
* @param publicDirFiles A set containing relative paths to all available static resources (note that this does not
351355
* include static *pages*, but rather images and the like)
352356
* @returns false if the URL is for an internal or static resource
353357
*/
354-
function shouldTraceRequest(url: string, publicDirFiles: Set<string>): boolean {
358+
function shouldTraceRequest(path: string, publicDirFiles: Set<string>): boolean {
359+
// Don't trace tunneled sentry events
360+
const tunnelPath = globalWithInjectedValues.__sentryRewritesTunnelPath__;
361+
if (tunnelPath) {
362+
const startsWithTunnelPath = path.startsWith(tunnelPath);
363+
const transactionNameRest = path.substring(tunnelPath.length);
364+
365+
// Check if the requested path matches the path we tunnel requests to
366+
const restMatchesTunnelStructure = transactionNameRest.match(/^\/o\d+\/\d+\/?$/);
367+
368+
if (startsWithTunnelPath && restMatchesTunnelStructure) {
369+
__DEBUG_BUILD__ && logger.log(`Tunneling Sentry event to "${path}"`);
370+
return false;
371+
}
372+
}
373+
355374
// `static` is a deprecated but still-functional location for static resources
356-
return !url.startsWith('/_next/') && !url.startsWith('/static/') && !publicDirFiles.has(url);
375+
return !path.startsWith('/_next/') && !path.startsWith('/static/') && !publicDirFiles.has(path);
357376
}

0 commit comments

Comments
 (0)