|
| 1 | +import type { Carrier, Scope } from '@sentry/hub'; |
| 2 | +import { getHubFromCarrier, getMainCarrier } from '@sentry/hub'; |
| 3 | +import { RewriteFrames } from '@sentry/integrations'; |
| 4 | +import { addRequestDataToEvent, configureScope, getCurrentHub, init as nodeInit, Integrations } from '@sentry/node'; |
| 5 | +import { hasTracingEnabled } from '@sentry/tracing'; |
| 6 | +import type { EventProcessor } from '@sentry/types'; |
| 7 | +import type { CrossPlatformRequest } from '@sentry/utils'; |
| 8 | +import { escapeStringForRegex, logger } from '@sentry/utils'; |
| 9 | +import * as domainModule from 'domain'; |
| 10 | +import * as path from 'path'; |
| 11 | + |
| 12 | +import { isBuild } from './utils/isBuild'; |
| 13 | +import { buildMetadata } from './utils/metadata'; |
| 14 | +import type { NextjsOptions } from './utils/nextjsOptions'; |
| 15 | +import { addOrUpdateIntegration } from './utils/userIntegrations'; |
| 16 | + |
| 17 | +export * from '@sentry/node'; |
| 18 | +export { captureUnderscoreErrorException } from './utils/_error'; |
| 19 | + |
| 20 | +// Here we want to make sure to only include what doesn't have browser specifics |
| 21 | +// because or SSR of next.js we can only use this. |
| 22 | +export { ErrorBoundary, showReportDialog, withErrorBoundary } from '@sentry/react'; |
| 23 | + |
| 24 | +type GlobalWithDistDir = typeof global & { __rewriteFramesDistDir__: string }; |
| 25 | +const domain = domainModule as typeof domainModule & { active: (domainModule.Domain & Carrier) | null }; |
| 26 | + |
| 27 | +const isVercel = !!process.env.VERCEL; |
| 28 | + |
| 29 | +/** Inits the Sentry NextJS SDK on node. */ |
| 30 | +export function init(options: NextjsOptions): void { |
| 31 | + let eventProcessors: EventProcessor[] = []; |
| 32 | + |
| 33 | + if (__DEBUG_BUILD__ && options.debug) { |
| 34 | + logger.enable(); |
| 35 | + } |
| 36 | + |
| 37 | + __DEBUG_BUILD__ && logger.log('Initializing SDK...'); |
| 38 | + |
| 39 | + if (sdkAlreadyInitialized()) { |
| 40 | + __DEBUG_BUILD__ && logger.log('SDK already initialized'); |
| 41 | + return; |
| 42 | + } |
| 43 | + |
| 44 | + buildMetadata(options, ['nextjs', 'node']); |
| 45 | + options.environment = options.environment || process.env.NODE_ENV; |
| 46 | + addServerIntegrations(options); |
| 47 | + // Right now we only capture frontend sessions for Next.js |
| 48 | + options.autoSessionTracking = false; |
| 49 | + |
| 50 | + // In an ideal world, this init function would be called before any requests are handled. That way, every domain we |
| 51 | + // use to wrap a request would inherit its scope and client from the global hub. In practice, however, handling the |
| 52 | + // first request is what causes us to initialize the SDK, as the init code is injected into `_app` and all API route |
| 53 | + // handlers, and those are only accessed in the course of handling a request. As a result, we're already in a domain |
| 54 | + // when `init` is called. In order to compensate for this and mimic the ideal world scenario, we stash the active |
| 55 | + // domain, run `init` as normal, and then restore the domain afterwards, copying over data from the main hub as if we |
| 56 | + // really were inheriting. |
| 57 | + const activeDomain = domain.active; |
| 58 | + domain.active = null; |
| 59 | + |
| 60 | + nodeInit(options); |
| 61 | + |
| 62 | + configureScope(scope => { |
| 63 | + scope.setTag('runtime', 'node'); |
| 64 | + if (isVercel) { |
| 65 | + scope.setTag('vercel', true); |
| 66 | + } |
| 67 | + |
| 68 | + eventProcessors = addEventProcessors(scope); |
| 69 | + }); |
| 70 | + |
| 71 | + if (activeDomain) { |
| 72 | + const globalHub = getHubFromCarrier(getMainCarrier()); |
| 73 | + const domainHub = getHubFromCarrier(activeDomain); |
| 74 | + |
| 75 | + // apply the changes made by `nodeInit` to the domain's hub also |
| 76 | + domainHub.bindClient(globalHub.getClient()); |
| 77 | + domainHub.getScope()?.update(globalHub.getScope()); |
| 78 | + // `scope.update()` doesn't copy over event processors, so we have to add them manually |
| 79 | + eventProcessors.forEach(processor => { |
| 80 | + domainHub.getScope()?.addEventProcessor(processor); |
| 81 | + }); |
| 82 | + |
| 83 | + // restore the domain hub as the current one |
| 84 | + domain.active = activeDomain; |
| 85 | + } |
| 86 | + |
| 87 | + __DEBUG_BUILD__ && logger.log('SDK successfully initialized'); |
| 88 | +} |
| 89 | + |
| 90 | +function sdkAlreadyInitialized(): boolean { |
| 91 | + const hub = getCurrentHub(); |
| 92 | + return !!hub.getClient(); |
| 93 | +} |
| 94 | + |
| 95 | +function addServerIntegrations(options: NextjsOptions): void { |
| 96 | + // This value is injected at build time, based on the output directory specified in the build config. Though a default |
| 97 | + // is set there, we set it here as well, just in case something has gone wrong with the injection. |
| 98 | + const distDirName = (global as GlobalWithDistDir).__rewriteFramesDistDir__ || '.next'; |
| 99 | + // nextjs always puts the build directory at the project root level, which is also where you run `next start` from, so |
| 100 | + // we can read in the project directory from the currently running process |
| 101 | + const distDirAbsPath = path.resolve(process.cwd(), distDirName); |
| 102 | + const SOURCEMAP_FILENAME_REGEX = new RegExp(escapeStringForRegex(distDirAbsPath)); |
| 103 | + |
| 104 | + const defaultRewriteFramesIntegration = new RewriteFrames({ |
| 105 | + iteratee: frame => { |
| 106 | + frame.filename = frame.filename?.replace(SOURCEMAP_FILENAME_REGEX, 'app:///_next'); |
| 107 | + return frame; |
| 108 | + }, |
| 109 | + }); |
| 110 | + |
| 111 | + if (options.integrations) { |
| 112 | + options.integrations = addOrUpdateIntegration(defaultRewriteFramesIntegration, options.integrations); |
| 113 | + } else { |
| 114 | + options.integrations = [defaultRewriteFramesIntegration]; |
| 115 | + } |
| 116 | + |
| 117 | + if (hasTracingEnabled(options)) { |
| 118 | + const defaultHttpTracingIntegration = new Integrations.Http({ tracing: true }); |
| 119 | + options.integrations = addOrUpdateIntegration(defaultHttpTracingIntegration, options.integrations, { |
| 120 | + Http: { keyPath: '_tracing', value: true }, |
| 121 | + }); |
| 122 | + } |
| 123 | +} |
| 124 | + |
| 125 | +function addEventProcessors(scope: Scope): EventProcessor[] { |
| 126 | + // Note: If you add an event processor here, you have to add it to the array that's returned also |
| 127 | + const filterTransactions: EventProcessor = event => { |
| 128 | + return event.type === 'transaction' && event.transaction === '/404' ? null : event; |
| 129 | + }; |
| 130 | + |
| 131 | + const addRequestData: EventProcessor = event => { |
| 132 | + const req = event.sdkProcessingMetadata?.req as CrossPlatformRequest; |
| 133 | + |
| 134 | + addRequestDataToEvent(event, req, { include: 'get options from user' }); |
| 135 | + |
| 136 | + return event; |
| 137 | + }; |
| 138 | + |
| 139 | + // Assign an `id` property to each event processor so that our logger and error messages can refer to it by name |
| 140 | + filterTransactions.id = 'FilterTransactions'; |
| 141 | + addRequestData.id = 'AddRequestData'; |
| 142 | + |
| 143 | + scope.addEventProcessor(filterTransactions); |
| 144 | + scope.addEventProcessor(addRequestData); |
| 145 | + |
| 146 | + return [filterTransactions, addRequestData]; |
| 147 | +} |
| 148 | + |
| 149 | +export type { SentryWebpackPluginOptions } from './config/types'; |
| 150 | +export { withSentryConfig } from './config'; |
| 151 | +export { isBuild } from './utils/isBuild'; |
| 152 | +export { |
| 153 | + withSentryGetServerSideProps, |
| 154 | + withSentryGetStaticProps, |
| 155 | + withSentryServerSideGetInitialProps, |
| 156 | + withSentryServerSideAppGetInitialProps, |
| 157 | + withSentryServerSideDocumentGetInitialProps, |
| 158 | + withSentryServerSideErrorGetInitialProps, |
| 159 | +} from './config/wrappers'; |
| 160 | +export { withSentry } from './utils/withSentry'; |
| 161 | + |
| 162 | +// Wrap various server methods to enable error monitoring and tracing. (Note: This only happens for non-Vercel |
| 163 | +// deployments, because the current method of doing the wrapping a) crashes Next 12 apps deployed to Vercel and |
| 164 | +// b) doesn't work on those apps anyway. We also don't do it during build, because there's no server running in that |
| 165 | +// phase.) |
| 166 | +if (!isVercel && !isBuild()) { |
| 167 | + // Dynamically require the file because even importing from it causes Next 12 to crash on Vercel. |
| 168 | + // In environments where the JS file doesn't exist, such as testing, import the TS file. |
| 169 | + try { |
| 170 | + // eslint-disable-next-line @typescript-eslint/no-var-requires |
| 171 | + const { instrumentServer } = require('./utils/instrumentServer.js'); |
| 172 | + instrumentServer(); |
| 173 | + } catch (err) { |
| 174 | + __DEBUG_BUILD__ && logger.warn(`Error: Unable to instrument server for tracing. Got ${err}.`); |
| 175 | + } |
| 176 | +} |
0 commit comments