Skip to content

Commit f350027

Browse files
author
Luca Forstner
committed
Don't start transactions for tunneled events
1 parent 9e27723 commit f350027

13 files changed

+117
-144
lines changed

packages/nextjs/rollup.npm.config.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ export default [
2020
...makeNPMConfigVariants(
2121
makeBaseNPMConfig({
2222
entrypoints: [
23-
'src/config/templates/serverRewriteFramesPrefixLoaderTemplate.ts',
24-
'src/config/templates/clientRewriteFramesPrefixLoaderTemplate.ts',
2523
'src/config/templates/pageProxyLoaderTemplate.ts',
2624
'src/config/templates/apiProxyLoaderTemplate.ts',
2725
],
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export { default as prefixLoader } from './prefixLoader';
1+
export { default as valueInjectionLoader } from './valueInjectionLoader';
22
export { default as proxyLoader } from './proxyLoader';

packages/nextjs/src/config/loaders/prefixLoader.ts

Lines changed: 0 additions & 38 deletions
This file was deleted.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { LoaderThis } from './types';
2+
3+
type LoaderOptions = {
4+
values: Record<string, string>;
5+
};
6+
7+
/**
8+
* Set values on the global/window object at the start of a module.
9+
*
10+
* Options:
11+
* - `values`: A record where the keys correspond to the keys of the global values to set and the values
12+
* correspond to the values of the values on the global object.
13+
*/
14+
export default function valueInjectionLoader(this: LoaderThis<LoaderOptions>, userCode: string): string {
15+
// We know one or the other will be defined, depending on the version of webpack being used
16+
const { values } = 'getOptions' in this ? this.getOptions() : this.query;
17+
18+
// Define some global proxy that works on server and on the browser.
19+
let injectedCode = 'var _sentryCollisionFreeGlobalObject = typeof window === "undefined" ? global : window;\n';
20+
21+
Object.entries(values).forEach(([key, value]) => {
22+
injectedCode += `_sentryCollisionFreeGlobalObject.${key} = '${value}';\n`;
23+
});
24+
25+
return `${injectedCode}\n${userCode}`;
26+
}

packages/nextjs/src/config/templates/clientRewriteFramesPrefixLoaderTemplate.ts

Lines changed: 0 additions & 9 deletions
This file was deleted.

packages/nextjs/src/config/templates/serverRewriteFramesPrefixLoaderTemplate.ts

Lines changed: 0 additions & 6 deletions
This file was deleted.

packages/nextjs/src/config/types.ts

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,26 +26,10 @@ export type NextConfigFunctionWithSentry = (
2626
defaults: { defaultConfig: NextConfigObject },
2727
) => NextConfigObjectWithSentry;
2828

29-
// Vendored from Next.js
30-
type RouteHas =
31-
| {
32-
type: 'header' | 'query' | 'cookie';
33-
key: string;
34-
value?: string;
35-
}
36-
| {
37-
type: 'host';
38-
key?: undefined;
39-
value: string;
40-
};
41-
42-
// Vendored from Next.js
29+
// Vendored from Next.js (this type is not complete - extend if necessary)
4330
type Rewrite = {
4431
source: string;
4532
destination: string;
46-
basePath?: false;
47-
locale?: false;
48-
has?: RouteHas[];
4933
};
5034

5135
export type NextConfigObject = {
@@ -63,7 +47,7 @@ export type NextConfigObject = {
6347
publicRuntimeConfig?: { [key: string]: unknown };
6448
// File extensions that count as pages in the `pages/` directory
6549
pageExtensions?: string[];
66-
// TODO
50+
// Paths to reroute when requested
6751
rewrites?: () => Promise<
6852
| Rewrite[]
6953
| {

packages/nextjs/src/config/webpack.ts

Lines changed: 36 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { default as SentryWebpackPlugin } from '@sentry/webpack-plugin';
1313
import * as chalk from 'chalk';
1414
import * as fs from 'fs';
1515
import * as path from 'path';
16-
import * as webpack from 'webpack';
1716

1817
// Note: If you need to import a type from Webpack, do it in `types.ts` and export it from there. Otherwise, our
1918
// circular dependency check thinks this file is importing from itself. See https://github.com/pahen/madge/issues/306.
@@ -82,8 +81,8 @@ export function constructWebpackConfigFunction(
8281
// `newConfig.module.rules` is required, so we don't have to keep asserting its existence
8382
const newConfig = setUpModuleRules(rawNewConfig);
8483

85-
// Add a loader which will inject code that sets global values for use by `RewriteFrames`
86-
addRewriteFramesLoader(newConfig, isServer ? 'server' : 'client', userNextConfig);
84+
// Add a loader which will inject code that sets global values
85+
addValueInjectionLoader(newConfig, userNextConfig, userSentryOptions);
8786

8887
if (isServer) {
8988
if (userSentryOptions.autoInstrumentServerFunctions !== false) {
@@ -202,15 +201,6 @@ export function constructWebpackConfigFunction(
202201
);
203202
}
204203

205-
if (userSentryOptions.rewritesTunnel) {
206-
newConfig.plugins = newConfig.plugins || [];
207-
newConfig.plugins.push(
208-
new webpack.DefinePlugin({
209-
__SENTRY_REWRITES_TUNNEL_PATH__: JSON.stringify(userSentryOptions.rewritesTunnel),
210-
}),
211-
);
212-
}
213-
214204
return newConfig;
215205
};
216206
}
@@ -656,47 +646,50 @@ function setUpModuleRules(newConfig: WebpackConfigObject): WebpackConfigObjectWi
656646
}
657647

658648
/**
659-
* Support the `distDir` and `assetPrefix` options by making their values (easy to get here at build-time) available at
660-
* runtime (for use by `RewriteFrames`), by injecting code to attach their values to `global` or `window`.
661-
*
662-
* @param newConfig The webpack config object being constructed
663-
* @param target Either 'server' or 'client'
664-
* @param userNextConfig The user's nextjs config options
649+
* TODO
665650
*/
666-
function addRewriteFramesLoader(
651+
function addValueInjectionLoader(
667652
newConfig: WebpackConfigObjectWithModuleRules,
668-
target: 'server' | 'client',
669653
userNextConfig: NextConfigObject,
654+
userSentryOptions: UserSentryOptions,
670655
): void {
671-
const replacements = {
672-
server: [
673-
[
674-
'__DIST_DIR__',
675-
// Make sure that if we have a windows path, the backslashes are interpreted as such (rather than as escape
676-
// characters)
677-
userNextConfig.distDir?.replace(/\\/g, '\\\\') || '.next',
678-
],
679-
],
680-
client: [
681-
[
682-
'__ASSET_PREFIX_PATH__',
683-
// Get the path part of `assetPrefix`, minus any trailing slash. (We use a placeholder for the origin if
684-
// `assetPreix` doesn't include one. Since we only care about the path, it doesn't matter what it is.)
685-
userNextConfig.assetPrefix
686-
? new URL(userNextConfig.assetPrefix, 'http://dogs.are.great').pathname.replace(/\/$/, '')
687-
: '',
688-
],
689-
],
656+
const serverValues = {
657+
// Make sure that if we have a windows path, the backslashes are interpreted as such (rather than as escape
658+
// characters)
659+
__rewriteFramesDistDir__: userNextConfig.distDir?.replace(/\\/g, '\\\\') || '.next',
660+
// TODO
661+
__sentryRewritesTunnelPath__: userSentryOptions.rewritesTunnel,
662+
};
663+
664+
const clientValues = {
665+
// Get the path part of `assetPrefix`, minus any trailing slash. (We use a placeholder for the origin if
666+
// `assetPreix` doesn't include one. Since we only care about the path, it doesn't matter what it is.)
667+
__rewriteFramesAssetPrefixPath__: userNextConfig.assetPrefix
668+
? new URL(userNextConfig.assetPrefix, 'http://dogs.are.great').pathname.replace(/\/$/, '')
669+
: '',
670+
// TODO
671+
__sentryRewritesTunnelPath__: userSentryOptions.rewritesTunnel,
690672
};
691673

692674
newConfig.module.rules.push({
693-
test: new RegExp(`sentry\\.${target}\\.config\\.(jsx?|tsx?)`),
675+
test: /sentry\.server\.config\.(jsx?|tsx?)/,
676+
use: [
677+
{
678+
loader: path.resolve(__dirname, 'loaders/valueInjectionLoader.js'),
679+
options: {
680+
values: serverValues,
681+
},
682+
},
683+
],
684+
});
685+
686+
newConfig.module.rules.push({
687+
test: /sentry\.client\.config\.(jsx?|tsx?)/,
694688
use: [
695689
{
696-
loader: path.resolve(__dirname, 'loaders/prefixLoader.js'),
690+
loader: path.resolve(__dirname, 'loaders/valueInjectionLoader.js'),
697691
options: {
698-
templatePrefix: `${target}RewriteFrames`,
699-
replacements: replacements[target],
692+
values: clientValues,
700693
},
701694
},
702695
],

packages/nextjs/src/config/withSentryConfig.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ function getFinalConfigObject(
4242
delete incomingUserNextConfigObject.sentry;
4343

4444
if (userSentryOptions?.rewritesTunnel) {
45-
setUpRewriteRules(incomingUserNextConfigObject, userSentryOptions.rewritesTunnel);
45+
setUpTunnelRewriteRules(incomingUserNextConfigObject, userSentryOptions.rewritesTunnel);
4646
}
4747

4848
// In order to prevent all of our build-time code from being bundled in people's route-handling serverless functions,
@@ -66,23 +66,22 @@ function getFinalConfigObject(
6666
}
6767

6868
/**
69-
* TODO
69+
* Injects rewrite rules into the Next.js config provided by the user to tunnel
70+
* requests from the `tunnelPath` to Sentry.
7071
*/
71-
function setUpRewriteRules(userNextConfig: NextConfigObject, tunnelPath: string): void {
72+
function setUpTunnelRewriteRules(userNextConfig: NextConfigObject, tunnelPath: string): void {
7273
const originalRewrites = userNextConfig.rewrites;
7374

7475
// This function doesn't take any arguments at the time of writing but we future-proof
7576
// here in case Next.js ever decides to pass some
7677
userNextConfig.rewrites = async (...args: unknown[]) => {
77-
// TODO: Consider trailing slash option
78-
7978
const injectedRewrites = [
8079
{
81-
source: `${tunnelPath}/:orgid/:projectid`, // without slash
80+
source: `${tunnelPath}/:orgid/:projectid`, // without trailing slash
8281
destination: 'https://:orgid.ingest.sentry.io/api/:projectid/envelope/',
8382
},
8483
{
85-
source: `${tunnelPath}/:orgid/:projectid/`, // with slash
84+
source: `${tunnelPath}/:orgid/:projectid/`, // with trailing slash
8685
destination: 'https://:orgid.ingest.sentry.io/api/:projectid/envelope/',
8786
},
8887
];

packages/nextjs/src/index.client.ts

Lines changed: 14 additions & 9 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 { dsnFromString } from '@sentry/utils';
5+
import { dsnFromString, logger } from '@sentry/utils';
66

77
import { nextRouterInstrumentation } from './performance/client';
88
import { buildMetadata } from './utils/metadata';
@@ -30,18 +30,23 @@ export { BrowserTracing };
3030
// Treeshakable guard to remove all code related to tracing
3131
declare const __SENTRY_TRACING__: boolean;
3232

33-
// Variable that is injected at build-time to have an automatic tunneling setup that uses Next.js' rewrites option
34-
declare const __SENTRY_REWRITES_TUNNEL_PATH__: string | undefined;
35-
36-
type GlobalWithAssetPrefixPath = typeof global & { __rewriteFramesAssetPrefixPath__: string };
33+
const globalWithInjectedValues = global as typeof global & {
34+
__rewriteFramesAssetPrefixPath__: string;
35+
__sentryRewritesTunnelPath__?: string;
36+
};
3737

3838
/** Inits the Sentry NextJS SDK on the browser with the React SDK. */
3939
export function init(options: NextjsOptions): void {
40-
if (typeof __SENTRY_REWRITES_TUNNEL_PATH__ === 'string' && options.dsn) {
40+
let tunnelPath: string | undefined;
41+
if (globalWithInjectedValues.__sentryRewritesTunnelPath__ && options.dsn) {
4142
const dsnComponents = dsnFromString(options.dsn);
42-
if (dsnComponents.host.match(/^o\d+\.ingest.sentry.io$/)) {
43+
if (dsnComponents.host.match(/^o\d+\.ingest\.sentry\.io$/)) {
4344
const orgId = dsnComponents.host.split('.')[0];
44-
options.tunnel = `${__SENTRY_REWRITES_TUNNEL_PATH__}/${orgId}/${dsnComponents.projectId}/`;
45+
tunnelPath = `${globalWithInjectedValues.__sentryRewritesTunnelPath__}/${orgId}/${dsnComponents.projectId}/`;
46+
__DEBUG_BUILD__ && logger.info(`Tunneling events to "${tunnelPath}"`);
47+
options.tunnel = tunnelPath;
48+
} else {
49+
__DEBUG_BUILD__ && logger.warn('Provided DSN is not a Sentry SaaS DSN. Will not tunnel events.');
4550
}
4651
}
4752

@@ -65,7 +70,7 @@ function addClientIntegrations(options: NextjsOptions): void {
6570

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

7075
const defaultRewriteFramesIntegration = new RewriteFrames({
7176
// Turn `<origin>/<path>/_next/static/...` into `app:///_next/static/...`

packages/nextjs/src/index.server.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ export { captureUnderscoreErrorException } from './utils/_error';
1919
// because or SSR of next.js we can only use this.
2020
export { ErrorBoundary, showReportDialog, withErrorBoundary } from '@sentry/react';
2121

22-
type GlobalWithDistDir = typeof global & { __rewriteFramesDistDir__: string };
22+
const globalWithInjectedValues = global as typeof global & {
23+
__rewriteFramesDistDir__: string;
24+
};
25+
2326
const domain = domainModule as typeof domainModule & { active: (domainModule.Domain & Carrier) | null };
2427

2528
// Exporting this constant means we can compute it without the linter complaining, even if we stop directly using it in
@@ -104,7 +107,7 @@ function addServerIntegrations(options: NextjsOptions): void {
104107

105108
// This value is injected at build time, based on the output directory specified in the build config. Though a default
106109
// is set there, we set it here as well, just in case something has gone wrong with the injection.
107-
const distDirName = (global as GlobalWithDistDir).__rewriteFramesDistDir__ || '.next';
110+
const distDirName = globalWithInjectedValues.__rewriteFramesDistDir__ || '.next';
108111
// nextjs always puts the build directory at the project root level, which is also where you run `next start` from, so
109112
// we can read in the project directory from the currently running process
110113
const distDirAbsPath = path.resolve(process.cwd(), distDirName);

packages/nextjs/src/utils/instrumentServer.ts

Lines changed: 21 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,26 @@ 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+
const tunnelPath = globalWithInjectedValues.__sentryRewritesTunnelPath__;
360+
if (tunnelPath) {
361+
const startsWithTunnelPath = path.startsWith(tunnelPath);
362+
const transactionNameRest = path.substring(tunnelPath.length);
363+
364+
// Check if the requested path matches the path we tunnel requests to
365+
const restMatchesTunnelStructure = transactionNameRest.match(/^\/o\d+\/\d+\/?$/);
366+
367+
if (startsWithTunnelPath && restMatchesTunnelStructure) {
368+
__DEBUG_BUILD__ && logger.log(`Tunneling Sentry event to "${path}"`);
369+
return false;
370+
}
371+
}
372+
355373
// `static` is a deprecated but still-functional location for static resources
356-
return !url.startsWith('/_next/') && !url.startsWith('/static/') && !publicDirFiles.has(url);
374+
return !path.startsWith('/_next/') && !path.startsWith('/static/') && !publicDirFiles.has(path);
357375
}

0 commit comments

Comments
 (0)