-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix(nextjs): Support custom distDir
values in default RewriteFrames
integration
#4017
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
Changes from all commits
6611e0a
f3bce8c
ef5a0bd
671bcc9
8a4c43e
ea1368b
c321ade
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -2,11 +2,11 @@ import { getSentryRelease } from '@sentry/node'; | |||||||||||
import { dropUndefinedKeys, logger } from '@sentry/utils'; | ||||||||||||
import { default as SentryWebpackPlugin } from '@sentry/webpack-plugin'; | ||||||||||||
import * as fs from 'fs'; | ||||||||||||
import * as os from 'os'; | ||||||||||||
import * as path from 'path'; | ||||||||||||
|
||||||||||||
import { | ||||||||||||
BuildContext, | ||||||||||||
EntryPointValue, | ||||||||||||
EntryPropertyObject, | ||||||||||||
NextConfigObject, | ||||||||||||
SentryWebpackPluginOptions, | ||||||||||||
|
@@ -128,14 +128,31 @@ async function addSentryToEntryProperty( | |||||||||||
const newEntryProperty = | ||||||||||||
typeof currentEntryProperty === 'function' ? await currentEntryProperty() : { ...currentEntryProperty }; | ||||||||||||
|
||||||||||||
// `sentry.server.config.js` or `sentry.client.config.js` (or their TS equivalents) | ||||||||||||
const userConfigFile = buildContext.isServer | ||||||||||||
? getUserConfigFile(buildContext.dir, 'server') | ||||||||||||
: getUserConfigFile(buildContext.dir, 'client'); | ||||||||||||
|
||||||||||||
// we need to turn the filename into a path so webpack can find it | ||||||||||||
const filesToInject = [`./${userConfigFile}`]; | ||||||||||||
|
||||||||||||
// Support non-default output directories by making the output path (easy to get here at build-time) available to the | ||||||||||||
// server SDK's default `RewriteFrames` instance (which needs it at runtime). | ||||||||||||
if (buildContext.isServer) { | ||||||||||||
const rewriteFramesHelper = path.resolve( | ||||||||||||
fs.mkdtempSync(path.resolve(os.tmpdir(), 'sentry-')), | ||||||||||||
lobsterkatie marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
'rewriteFramesHelper.js', | ||||||||||||
); | ||||||||||||
fs.writeFileSync(rewriteFramesHelper, `global.__rewriteFramesDistDir__ = '${buildContext.config.distDir}';\n`); | ||||||||||||
// stick our helper file ahead of the user's config file so the value is in the global namespace *before* | ||||||||||||
// `Sentry.init()` is called | ||||||||||||
filesToInject.unshift(rewriteFramesHelper); | ||||||||||||
} | ||||||||||||
|
||||||||||||
// inject into all entry points which might contain user's code | ||||||||||||
for (const entryPointName in newEntryProperty) { | ||||||||||||
if (shouldAddSentryToEntryPoint(entryPointName)) { | ||||||||||||
Comment on lines
+152
to
154
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not related to the PR, but if we set proper names we don't need this comment. What do you think of this suggestion? Do you have other alternatives?
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmmm - interesting. I'd probably go for |
||||||||||||
// we need to turn the filename into a path so webpack can find it | ||||||||||||
addFileToExistingEntryPoint(newEntryProperty, entryPointName, `./${userConfigFile}`); | ||||||||||||
addFilesToExistingEntryPoint(newEntryProperty, entryPointName, filesToInject); | ||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
|
@@ -163,51 +180,52 @@ export function getUserConfigFile(projectDir: string, platform: 'server' | 'clie | |||||||||||
} | ||||||||||||
|
||||||||||||
/** | ||||||||||||
* Add a file to a specific element of the given `entry` webpack config property. | ||||||||||||
* Add files to a specific element of the given `entry` webpack config property. | ||||||||||||
* | ||||||||||||
* @param entryProperty The existing `entry` config object | ||||||||||||
* @param entryPointName The key where the file should be injected | ||||||||||||
* @param filepath The path to the injected file | ||||||||||||
* @param filepaths An array of paths to the injected files | ||||||||||||
*/ | ||||||||||||
function addFileToExistingEntryPoint( | ||||||||||||
function addFilesToExistingEntryPoint( | ||||||||||||
entryProperty: EntryPropertyObject, | ||||||||||||
entryPointName: string, | ||||||||||||
filepath: string, | ||||||||||||
filepaths: string[], | ||||||||||||
): void { | ||||||||||||
// can be a string, array of strings, or object whose `import` property is one of those two | ||||||||||||
const currentEntryPoint = entryProperty[entryPointName]; | ||||||||||||
let newEntryPoint: EntryPointValue; | ||||||||||||
let newEntryPoint = currentEntryPoint; | ||||||||||||
|
||||||||||||
if (typeof currentEntryPoint === 'string') { | ||||||||||||
newEntryPoint = [filepath, currentEntryPoint]; | ||||||||||||
newEntryPoint = [...filepaths, currentEntryPoint]; | ||||||||||||
} else if (Array.isArray(currentEntryPoint)) { | ||||||||||||
newEntryPoint = [filepath, ...currentEntryPoint]; | ||||||||||||
newEntryPoint = [...filepaths, ...currentEntryPoint]; | ||||||||||||
lobsterkatie marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
} | ||||||||||||
// descriptor object (webpack 5+) | ||||||||||||
else if (typeof currentEntryPoint === 'object' && 'import' in currentEntryPoint) { | ||||||||||||
const currentImportValue = currentEntryPoint.import; | ||||||||||||
let newImportValue; | ||||||||||||
|
||||||||||||
if (typeof currentImportValue === 'string') { | ||||||||||||
newImportValue = [filepath, currentImportValue]; | ||||||||||||
newImportValue = [...filepaths, currentImportValue]; | ||||||||||||
} else { | ||||||||||||
newImportValue = [filepath, ...currentImportValue]; | ||||||||||||
newImportValue = [...filepaths, ...currentImportValue]; | ||||||||||||
} | ||||||||||||
|
||||||||||||
newEntryPoint = { | ||||||||||||
...currentEntryPoint, | ||||||||||||
import: newImportValue, | ||||||||||||
}; | ||||||||||||
} else { | ||||||||||||
// mimic the logger prefix in order to use `console.warn` (which will always be printed, regardless of SDK settings) | ||||||||||||
} | ||||||||||||
// malformed entry point (use `console.error` rather than `logger.error` because it will always be printed, regardless | ||||||||||||
// of SDK settings) | ||||||||||||
else { | ||||||||||||
// eslint-disable-next-line no-console | ||||||||||||
console.error( | ||||||||||||
'Sentry Logger [Error]:', | ||||||||||||
`Could not inject SDK initialization code into entry point ${entryPointName}, as it is not a recognized format.\n`, | ||||||||||||
`Could not inject SDK initialization code into entry point ${entryPointName}, as its current value is not in a recognized format.\n`, | ||||||||||||
`Expected: string | Array<string> | { [key:string]: any, import: string | Array<string> }\n`, | ||||||||||||
`Got: ${currentEntryPoint}`, | ||||||||||||
); | ||||||||||||
return; | ||||||||||||
} | ||||||||||||
|
||||||||||||
entryProperty[entryPointName] = newEntryPoint; | ||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
import { RewriteFrames } from '@sentry/integrations'; | ||
import { configureScope, getCurrentHub, init as nodeInit, Integrations } from '@sentry/node'; | ||
import { logger } from '@sentry/utils'; | ||
import { escapeStringForRegex, logger } from '@sentry/utils'; | ||
import * as path from 'path'; | ||
|
||
import { instrumentServer } from './utils/instrumentServer'; | ||
import { MetadataBuilder } from './utils/metadataBuilder'; | ||
|
@@ -13,6 +14,8 @@ export * from '@sentry/node'; | |
// because or SSR of next.js we can only use this. | ||
export { ErrorBoundary, withErrorBoundary } from '@sentry/react'; | ||
|
||
type GlobalWithDistDir = typeof global & { __rewriteFramesDistDir__: string }; | ||
|
||
/** Inits the Sentry NextJS SDK on node. */ | ||
export function init(options: NextjsOptions): void { | ||
if (options.debug) { | ||
|
@@ -29,7 +32,6 @@ export function init(options: NextjsOptions): void { | |
const metadataBuilder = new MetadataBuilder(options, ['nextjs', 'node']); | ||
metadataBuilder.addSdkMetadata(); | ||
options.environment = options.environment || process.env.NODE_ENV; | ||
// TODO capture project root and store in an env var for RewriteFrames? | ||
addServerIntegrations(options); | ||
// Right now we only capture frontend sessions for Next.js | ||
options.autoSessionTracking = false; | ||
|
@@ -47,25 +49,29 @@ function sdkAlreadyInitialized(): boolean { | |
return !!hub.getClient(); | ||
} | ||
|
||
const SOURCEMAP_FILENAME_REGEX = /^.*\/\.next\//; | ||
|
||
const defaultRewriteFramesIntegration = new RewriteFrames({ | ||
iteratee: frame => { | ||
frame.filename = frame.filename?.replace(SOURCEMAP_FILENAME_REGEX, 'app:///_next/'); | ||
return frame; | ||
}, | ||
}); | ||
|
||
const defaultHttpTracingIntegration = new Integrations.Http({ tracing: true }); | ||
|
||
function addServerIntegrations(options: NextjsOptions): void { | ||
// This value is injected at build time, based on the output directory specified in the build config | ||
const distDirName = (global as GlobalWithDistDir).__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); | ||
Comment on lines
+55
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
IMO this isn't precise enough, since:
From Next.js docs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You mean your build directory could be FWIW, the reason for doing it there, as Also, if it helps, you can see in the server constructor that it uses There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My comment was about the comment, not the code. IMO, if we're adding comments they should be as specific as possible, since it may create confusion over existing code. Instead of "always puts the build directory at the project root level", we can say "the build directory is a subdirectory of the project root". The difference is that the first sentence doesn't say anything about the restrictions of the value of what users set; while the second one does, and covers both the nextjs' and users' values (the only case in which this is not true is when users have Not blocking with this, so your call. |
||
const SOURCEMAP_FILENAME_REGEX = new RegExp(escapeStringForRegex(distDirAbsPath)); | ||
|
||
const defaultRewriteFramesIntegration = new RewriteFrames({ | ||
iteratee: frame => { | ||
frame.filename = frame.filename?.replace(SOURCEMAP_FILENAME_REGEX, 'app:///_next'); | ||
return frame; | ||
}, | ||
}); | ||
|
||
if (options.integrations) { | ||
options.integrations = addIntegration(defaultRewriteFramesIntegration, options.integrations); | ||
} else { | ||
options.integrations = [defaultRewriteFramesIntegration]; | ||
} | ||
|
||
if (options.tracesSampleRate !== undefined || options.tracesSampler !== undefined) { | ||
const defaultHttpTracingIntegration = new Integrations.Http({ tracing: true }); | ||
options.integrations = addIntegration(defaultHttpTracingIntegration, options.integrations, { | ||
Http: { keyPath: '_tracing', value: true }, | ||
}); | ||
|
Uh oh!
There was an error while loading. Please reload this page.