From b7786acbbb960e165e936cdd91b2f06f7aae2b93 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 20 Sep 2024 13:34:48 +0000 Subject: [PATCH 1/8] This is very scary --- packages/vercel-edge/package.json | 6 + .../async-local-storage-context-manager.ts | 324 ++++++++++++++++++ packages/vercel-edge/src/async.ts | 87 ----- packages/vercel-edge/src/client.ts | 20 ++ packages/vercel-edge/src/sdk.ts | 138 +++++++- packages/vercel-edge/src/types.ts | 21 ++ yarn.lock | 72 +++- 7 files changed, 561 insertions(+), 107 deletions(-) create mode 100644 packages/vercel-edge/src/async-local-storage-context-manager.ts delete mode 100644 packages/vercel-edge/src/async.ts diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json index 0e211e5de086..0a13c67c155d 100644 --- a/packages/vercel-edge/package.json +++ b/packages/vercel-edge/package.json @@ -39,7 +39,13 @@ "access": "public" }, "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^1.25.1", + "@opentelemetry/resources": "^1.26.0", + "@opentelemetry/sdk-trace-base": "^1.26.0", + "@opentelemetry/semantic-conventions": "^1.27.0", "@sentry/core": "8.30.0", + "@sentry/opentelemetry": "8.30.0", "@sentry/types": "8.30.0", "@sentry/utils": "8.30.0" }, diff --git a/packages/vercel-edge/src/async-local-storage-context-manager.ts b/packages/vercel-edge/src/async-local-storage-context-manager.ts new file mode 100644 index 000000000000..1c9a772df138 --- /dev/null +++ b/packages/vercel-edge/src/async-local-storage-context-manager.ts @@ -0,0 +1,324 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Taken from: +// - https://github.com/open-telemetry/opentelemetry-js/blob/6515ed8098333646a63a74a8c0150cc2daf520db/packages/opentelemetry-context-async-hooks/src/AbstractAsyncHooksContextManager.ts +// - https://github.com/open-telemetry/opentelemetry-js/blob/6515ed8098333646a63a74a8c0150cc2daf520db/packages/opentelemetry-context-async-hooks/src/AsyncLocalStorageContextManager.ts + +import { EventEmitter } from 'events'; +import { ROOT_CONTEXT } from '@opentelemetry/api'; +import type { Context, ContextManager } from '@opentelemetry/api'; +import { logger } from '@sentry/utils'; +import { DEBUG_BUILD } from './debug-build'; + +interface AsyncLocalStorage { + getStore(): T | undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + run(store: T, callback: (...args: TArgs) => R, ...args: TArgs): R; + disable(): void; +} + +type Func = (...args: unknown[]) => T; + +/** + * Store a map for each event of all original listeners and their "patched" + * version. So when a listener is removed by the user, the corresponding + * patched function will be also removed. + */ +interface PatchMap { + [name: string]: WeakMap, Func>; +} + +const ADD_LISTENER_METHODS = [ + 'addListener' as const, + 'on' as const, + 'once' as const, + 'prependListener' as const, + 'prependOnceListener' as const, +]; + +abstract class AbstractAsyncHooksContextManager implements ContextManager { + private readonly _kOtListeners = Symbol('OtListeners'); + private _wrapped = false; + + /** + * Binds a the certain context or the active one to the target function and then returns the target + * @param context A context (span) to be bind to target + * @param target a function or event emitter. When target or one of its callbacks is called, + * the provided context will be used as the active context for the duration of the call. + */ + public bind(context: Context, target: T): T { + if (target instanceof EventEmitter) { + return this._bindEventEmitter(context, target); + } + + if (typeof target === 'function') { + // @ts-expect-error This is vendored + return this._bindFunction(context, target); + } + return target; + } + + /** + * By default, EventEmitter call their callback with their context, which we do + * not want, instead we will bind a specific context to all callbacks that + * go through it. + * @param context the context we want to bind + * @param ee EventEmitter an instance of EventEmitter to patch + */ + private _bindEventEmitter(context: Context, ee: T): T { + const map = this._getPatchMap(ee); + if (map !== undefined) return ee; + this._createPatchMap(ee); + + // patch methods that add a listener to propagate context + ADD_LISTENER_METHODS.forEach(methodName => { + if (ee[methodName] === undefined) return; + ee[methodName] = this._patchAddListener(ee, ee[methodName], context); + }); + // patch methods that remove a listener + if (typeof ee.removeListener === 'function') { + // eslint-disable-next-line @typescript-eslint/unbound-method + ee.removeListener = this._patchRemoveListener(ee, ee.removeListener); + } + if (typeof ee.off === 'function') { + // eslint-disable-next-line @typescript-eslint/unbound-method + ee.off = this._patchRemoveListener(ee, ee.off); + } + // patch method that remove all listeners + if (typeof ee.removeAllListeners === 'function') { + // eslint-disable-next-line @typescript-eslint/unbound-method + ee.removeAllListeners = this._patchRemoveAllListeners(ee, ee.removeAllListeners); + } + return ee; + } + + /** + * Patch methods that remove a given listener so that we match the "patched" + * version of that listener (the one that propagate context). + * @param ee EventEmitter instance + * @param original reference to the patched method + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _patchRemoveListener(ee: EventEmitter, original: (...args: any[]) => any): any { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const contextManager = this; + return function (this: never, event: string, listener: Func) { + const events = contextManager._getPatchMap(ee)?.[event]; + if (events === undefined) { + return original.call(this, event, listener); + } + const patchedListener = events.get(listener); + return original.call(this, event, patchedListener || listener); + }; + } + + /** + * Patch methods that remove all listeners so we remove our + * internal references for a given event. + * @param ee EventEmitter instance + * @param original reference to the patched method + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _patchRemoveAllListeners(ee: EventEmitter, original: (...args: any[]) => any): any { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const contextManager = this; + return function (this: never, event: string) { + const map = contextManager._getPatchMap(ee); + if (map !== undefined) { + if (arguments.length === 0) { + contextManager._createPatchMap(ee); + } else if (map[event] !== undefined) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete map[event]; + } + } + // eslint-disable-next-line prefer-rest-params + return original.apply(this, arguments); + }; + } + + /** + * Patch methods on an event emitter instance that can add listeners so we + * can force them to propagate a given context. + * @param ee EventEmitter instance + * @param original reference to the patched method + * @param [context] context to propagate when calling listeners + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _patchAddListener(ee: EventEmitter, original: (...args: any[]) => any, context: Context): any { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const contextManager = this; + return function (this: never, event: string, listener: Func) { + /** + * This check is required to prevent double-wrapping the listener. + * The implementation for ee.once wraps the listener and calls ee.on. + * Without this check, we would wrap that wrapped listener. + * This causes an issue because ee.removeListener depends on the onceWrapper + * to properly remove the listener. If we wrap their wrapper, we break + * that detection. + */ + if (contextManager._wrapped) { + return original.call(this, event, listener); + } + let map = contextManager._getPatchMap(ee); + if (map === undefined) { + map = contextManager._createPatchMap(ee); + } + let listeners = map[event]; + if (listeners === undefined) { + listeners = new WeakMap(); + map[event] = listeners; + } + const patchedListener = contextManager.bind(context, listener); + // store a weak reference of the user listener to ours + listeners.set(listener, patchedListener); + + /** + * See comment at the start of this function for the explanation of this property. + */ + contextManager._wrapped = true; + try { + return original.call(this, event, patchedListener); + } finally { + contextManager._wrapped = false; + } + }; + } + + /** + * + */ + private _createPatchMap(ee: EventEmitter): PatchMap { + const map = Object.create(null); + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + (ee as any)[this._kOtListeners] = map; + return map; + } + /** + * + */ + private _getPatchMap(ee: EventEmitter): PatchMap | undefined { + return (ee as never)[this._kOtListeners]; + } + + /** + * + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _bindFunction any>(context: Context, target: T): T { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const manager = this; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const contextWrapper = function (this: never, ...args: unknown[]): any { + return manager.with(context, () => target.apply(this, args)); + }; + Object.defineProperty(contextWrapper, 'length', { + enumerable: false, + configurable: true, + writable: false, + value: target.length, + }); + /** + * It isn't possible to tell Typescript that contextWrapper is the same as T + * so we forced to cast as any here. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return contextWrapper as any; + } + + public abstract active(): Context; + + public abstract with ReturnType>( + context: Context, + fn: F, + thisArg?: ThisParameterType, + ...args: A + ): ReturnType; + + public abstract enable(): this; + + public abstract disable(): this; +} + +/** + * + */ +export class AsyncLocalStorageContextManager extends AbstractAsyncHooksContextManager { + private _asyncLocalStorage: AsyncLocalStorage; + + public constructor() { + super(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any + const MaybeGlobalAsyncLocalStorage = (globalThis as any).AsyncLocalStorage; + + if (!MaybeGlobalAsyncLocalStorage) { + DEBUG_BUILD && + logger.warn( + "Tried to register AsyncLocalStorage async context strategy in a runtime that doesn't support AsyncLocalStorage.", + ); + this._asyncLocalStorage = { + getStore() { + return undefined; + }, + run(_store, callback, ...args) { + return callback.apply(this, args); + }, + disable() { + // noop + }, + }; + } else { + this._asyncLocalStorage = new MaybeGlobalAsyncLocalStorage(); + } + } + + /** + * + */ + public active(): Context { + return this._asyncLocalStorage.getStore() ?? ROOT_CONTEXT; + } + + /** + * + */ + public with ReturnType>( + context: Context, + fn: F, + thisArg?: ThisParameterType, + ...args: A + ): ReturnType { + const cb = thisArg == null ? fn : fn.bind(thisArg); + return this._asyncLocalStorage.run(context, cb as never, ...args); + } + + /** + * + */ + public enable(): this { + return this; + } + + /** + * + */ + public disable(): this { + this._asyncLocalStorage.disable(); + return this; + } +} diff --git a/packages/vercel-edge/src/async.ts b/packages/vercel-edge/src/async.ts deleted file mode 100644 index dd7432c8e959..000000000000 --- a/packages/vercel-edge/src/async.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { getDefaultCurrentScope, getDefaultIsolationScope, setAsyncContextStrategy } from '@sentry/core'; -import type { Scope } from '@sentry/types'; -import { GLOBAL_OBJ, logger } from '@sentry/utils'; - -import { DEBUG_BUILD } from './debug-build'; - -interface AsyncLocalStorage { - getStore(): T | undefined; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - run(store: T, callback: (...args: TArgs) => R, ...args: TArgs): R; -} - -let asyncStorage: AsyncLocalStorage<{ scope: Scope; isolationScope: Scope }>; - -/** - * Sets the async context strategy to use AsyncLocalStorage which should be available in the edge runtime. - */ -export function setAsyncLocalStorageAsyncContextStrategy(): void { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any - const MaybeGlobalAsyncLocalStorage = (GLOBAL_OBJ as any).AsyncLocalStorage; - - if (!MaybeGlobalAsyncLocalStorage) { - DEBUG_BUILD && - logger.warn( - "Tried to register AsyncLocalStorage async context strategy in a runtime that doesn't support AsyncLocalStorage.", - ); - return; - } - - if (!asyncStorage) { - asyncStorage = new MaybeGlobalAsyncLocalStorage(); - } - - function getScopes(): { scope: Scope; isolationScope: Scope } { - const scopes = asyncStorage.getStore(); - - if (scopes) { - return scopes; - } - - // fallback behavior: - // if, for whatever reason, we can't find scopes on the context here, we have to fix this somehow - return { - scope: getDefaultCurrentScope(), - isolationScope: getDefaultIsolationScope(), - }; - } - - function withScope(callback: (scope: Scope) => T): T { - const scope = getScopes().scope.clone(); - const isolationScope = getScopes().isolationScope; - return asyncStorage.run({ scope, isolationScope }, () => { - return callback(scope); - }); - } - - function withSetScope(scope: Scope, callback: (scope: Scope) => T): T { - const isolationScope = getScopes().isolationScope.clone(); - return asyncStorage.run({ scope, isolationScope }, () => { - return callback(scope); - }); - } - - function withIsolationScope(callback: (isolationScope: Scope) => T): T { - const scope = getScopes().scope; - const isolationScope = getScopes().isolationScope.clone(); - return asyncStorage.run({ scope, isolationScope }, () => { - return callback(isolationScope); - }); - } - - function withSetIsolationScope(isolationScope: Scope, callback: (isolationScope: Scope) => T): T { - const scope = getScopes().scope; - return asyncStorage.run({ scope, isolationScope }, () => { - return callback(isolationScope); - }); - } - - setAsyncContextStrategy({ - withScope, - withSetScope, - withIsolationScope, - withSetIsolationScope, - getCurrentScope: () => getScopes().scope, - getIsolationScope: () => getScopes().isolationScope, - }); -} diff --git a/packages/vercel-edge/src/client.ts b/packages/vercel-edge/src/client.ts index b2c7416130bc..09987eacd030 100644 --- a/packages/vercel-edge/src/client.ts +++ b/packages/vercel-edge/src/client.ts @@ -2,6 +2,7 @@ import type { ServerRuntimeClientOptions } from '@sentry/core'; import { applySdkMetadata } from '@sentry/core'; import { ServerRuntimeClient } from '@sentry/core'; +import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import type { VercelEdgeClientOptions } from './types'; declare const process: { @@ -15,6 +16,8 @@ declare const process: { * @see ServerRuntimeClient for usage documentation. */ export class VercelEdgeClient extends ServerRuntimeClient { + public traceProvider: BasicTracerProvider | undefined; + /** * Creates a new Vercel Edge Runtime SDK instance. * @param options Configuration options for this SDK. @@ -33,4 +36,21 @@ export class VercelEdgeClient extends ServerRuntimeClient { + const provider = this.traceProvider; + const spanProcessor = provider?.activeSpanProcessor; + + if (spanProcessor) { + await spanProcessor.forceFlush(); + } + + if (this.getOptions().sendClientReports) { + this._flushOutcomes(); + } + + return super.flush(timeout); + } } diff --git a/packages/vercel-edge/src/sdk.ts b/packages/vercel-edge/src/sdk.ts index 4e1bed208c34..2a8a5c698c07 100644 --- a/packages/vercel-edge/src/sdk.ts +++ b/packages/vercel-edge/src/sdk.ts @@ -1,20 +1,47 @@ import { dedupeIntegration, functionToStringIntegration, + getCurrentScope, getIntegrationsToSetup, + hasTracingEnabled, inboundFiltersIntegration, - initAndBind, linkedErrorsIntegration, requestDataIntegration, } from '@sentry/core'; import type { Client, Integration, Options } from '@sentry/types'; -import { GLOBAL_OBJ, createStackParser, nodeStackLineParser, stackParserFromStackParserOptions } from '@sentry/utils'; +import { + GLOBAL_OBJ, + SDK_VERSION, + createStackParser, + logger, + nodeStackLineParser, + stackParserFromStackParserOptions, +} from '@sentry/utils'; -import { setAsyncLocalStorageAsyncContextStrategy } from './async'; +import { DiagLogLevel, diag } from '@opentelemetry/api'; +import { Resource } from '@opentelemetry/resources'; +import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; +import { + ATTR_SERVICE_NAME, + ATTR_SERVICE_VERSION, + SEMRESATTRS_SERVICE_NAMESPACE, +} from '@opentelemetry/semantic-conventions/*'; +import { + SentryPropagator, + SentrySampler, + SentrySpanProcessor, + enhanceDscWithOpenTelemetryRootSpanName, + openTelemetrySetupCheck, + setOpenTelemetryContextAsyncContextStrategy, + setupEventContextTrace, + wrapContextManagerClass, +} from '@sentry/opentelemetry'; +import { AsyncLocalStorageContextManager } from './async-local-storage-context-manager'; import { VercelEdgeClient } from './client'; +import { DEBUG_BUILD } from './debug-build'; import { winterCGFetchIntegration } from './integrations/wintercg-fetch'; import { makeEdgeTransport } from './transports'; -import type { VercelEdgeClientOptions, VercelEdgeOptions } from './types'; +import type { VercelEdgeOptions } from './types'; import { getVercelEnv } from './utils/vercel'; declare const process: { @@ -37,7 +64,10 @@ export function getDefaultIntegrations(options: Options): Integration[] { /** Inits the Sentry NextJS SDK on the Edge Runtime. */ export function init(options: VercelEdgeOptions = {}): Client | undefined { - setAsyncLocalStorageAsyncContextStrategy(); + setOpenTelemetryContextAsyncContextStrategy(); + + const scope = getCurrentScope(); + scope.update(options.initialScope); if (options.defaultIntegrations === undefined) { options.defaultIntegrations = getDefaultIntegrations(options); @@ -71,14 +101,106 @@ export function init(options: VercelEdgeOptions = {}): Client | undefined { options.autoSessionTracking = true; } - const clientOptions: VercelEdgeClientOptions = { + const client = new VercelEdgeClient({ ...options, stackParser: stackParserFromStackParserOptions(options.stackParser || nodeStackParser), integrations: getIntegrationsToSetup(options), transport: options.transport || makeEdgeTransport, - }; + }); + // The client is on the current scope, from where it generally is inherited + getCurrentScope().setClient(client); + + client.init(); + + // If users opt-out of this, they _have_ to set up OpenTelemetry themselves + // There is no way to use this SDK without OpenTelemetry! + if (!options.skipOpenTelemetrySetup) { + initOpenTelemetry(client); + validateOpenTelemetrySetup(); + } + + enhanceDscWithOpenTelemetryRootSpanName(client); + setupEventContextTrace(client); + + return client; +} + +function validateOpenTelemetrySetup(): void { + if (!DEBUG_BUILD) { + return; + } + + const setup = openTelemetrySetupCheck(); + + const required: ReturnType = ['SentryContextManager', 'SentryPropagator']; + + if (hasTracingEnabled()) { + required.push('SentrySpanProcessor'); + } + + for (const k of required) { + if (!setup.includes(k)) { + logger.error( + `You have to set up the ${k}. Without this, the OpenTelemetry & Sentry integration will not work properly.`, + ); + } + } + + if (!setup.includes('SentrySampler')) { + logger.warn( + 'You have to set up the SentrySampler. Without this, the OpenTelemetry & Sentry integration may still work, but sample rates set for the Sentry SDK will not be respected. If you use a custom sampler, make sure to use `wrapSamplingDecision`.', + ); + } +} + +function initOpenTelemetry(client: VercelEdgeClient): void { + if (client.getOptions().debug) { + setupOpenTelemetryLogger(); + } + + // Create and configure NodeTracerProvider + const provider = new BasicTracerProvider({ + sampler: new SentrySampler(client), + resource: new Resource({ + [ATTR_SERVICE_NAME]: 'edge', + // eslint-disable-next-line deprecation/deprecation + [SEMRESATTRS_SERVICE_NAMESPACE]: 'sentry', + [ATTR_SERVICE_VERSION]: SDK_VERSION, + }), + forceFlushTimeoutMillis: 500, + }); + + provider.addSpanProcessor( + new SentrySpanProcessor({ + timeout: client.getOptions().maxSpanWaitDuration, + }), + ); + + const SentryContextManager = wrapContextManagerClass(AsyncLocalStorageContextManager); + + // Initialize the provider + provider.register({ + propagator: new SentryPropagator(), + contextManager: new SentryContextManager(), + }); + + client.traceProvider = provider; +} + +/** + * Setup the OTEL logger to use our own logger. + */ +function setupOpenTelemetryLogger(): void { + const otelLogger = new Proxy(logger as typeof logger & { verbose: (typeof logger)['debug'] }, { + get(target, prop, receiver) { + const actualProp = prop === 'verbose' ? 'debug' : prop; + return Reflect.get(target, actualProp, receiver); + }, + }); - return initAndBind(VercelEdgeClient, clientOptions); + // Disable diag, to ensure this works even if called multiple times + diag.disable(); + diag.setLogger(otelLogger, DiagLogLevel.DEBUG); } /** diff --git a/packages/vercel-edge/src/types.ts b/packages/vercel-edge/src/types.ts index 7544820c75a3..26bc1b911875 100644 --- a/packages/vercel-edge/src/types.ts +++ b/packages/vercel-edge/src/types.ts @@ -33,6 +33,27 @@ export interface BaseVercelEdgeOptions { * */ clientClass?: typeof VercelEdgeClient; + /** + * If this is set to true, the SDK will not set up OpenTelemetry automatically. + * In this case, you _have_ to ensure to set it up correctly yourself, including: + * * The `SentrySpanProcessor` + * * The `SentryPropagator` + * * The `SentryContextManager` + * * The `SentrySampler` + */ + skipOpenTelemetrySetup?: boolean; + + /** + * The max. duration in seconds that the SDK will wait for parent spans to be finished before discarding a span. + * The SDK will automatically clean up spans that have no finished parent after this duration. + * This is necessary to prevent memory leaks in case of parent spans that are never finished or otherwise dropped/missing. + * However, if you have very long-running spans in your application, a shorter duration might cause spans to be discarded too early. + * In this case, you can increase this duration to a value that fits your expected data. + * + * Defaults to 300 seconds (5 minutes). + */ + maxSpanWaitDuration?: number; + /** Callback that is executed when a fatal global error occurs. */ onFatalError?(this: void, error: Error): void; } diff --git a/yarn.lock b/yarn.lock index c94576d2e979..d35a585a6eb1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9700,8 +9700,17 @@ dependencies: "@types/unist" "*" -"@types/history-4@npm:@types/history@4.7.8", "@types/history-5@npm:@types/history@4.7.8", "@types/history@*": - name "@types/history-4" +"@types/history-4@npm:@types/history@4.7.8": + version "4.7.8" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" + integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== + +"@types/history-5@npm:@types/history@4.7.8": + version "4.7.8" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" + integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== + +"@types/history@*": version "4.7.8" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== @@ -10029,7 +10038,15 @@ "@types/history" "^3" "@types/react" "*" -"@types/react-router-4@npm:@types/react-router@5.1.14", "@types/react-router-5@npm:@types/react-router@5.1.14": +"@types/react-router-4@npm:@types/react-router@5.1.14": + version "5.1.14" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" + integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== + dependencies: + "@types/history" "*" + "@types/react" "*" + +"@types/react-router-5@npm:@types/react-router@5.1.14": version "5.1.14" resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== @@ -28437,8 +28454,7 @@ react-is@^18.0.0: dependencies: "@remix-run/router" "1.0.2" -"react-router-6@npm:react-router@6.3.0", react-router@6.3.0: - name react-router-6 +"react-router-6@npm:react-router@6.3.0": version "6.3.0" resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== @@ -28453,6 +28469,13 @@ react-router-dom@^6.2.2: history "^5.2.0" react-router "6.3.0" +react-router@6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" + integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== + dependencies: + history "^5.2.0" + react@^18.0.0: version "18.0.0" resolved "https://registry.yarnpkg.com/react/-/react-18.0.0.tgz#b468736d1f4a5891f38585ba8e8fb29f91c3cb96" @@ -30920,7 +30943,16 @@ string-template@~0.2.1: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0= -"string-width-cjs@npm:string-width@^4.2.0", string-width@4.2.3, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@4.2.3, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -31032,7 +31064,14 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -33184,10 +33223,10 @@ vite-plugin-vue-inspector@^5.1.0: kolorist "^1.8.0" magic-string "^0.30.4" -vite@4.5.3: - version "4.5.3" - resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.3.tgz#d88a4529ea58bae97294c7e2e6f0eab39a50fb1a" - integrity sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg== +vite@4.5.5: + version "4.5.5" + resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.5.tgz#639b9feca5c0a3bfe3c60cb630ef28bf219d742e" + integrity sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ== dependencies: esbuild "^0.18.10" postcss "^8.4.27" @@ -34001,7 +34040,16 @@ wrangler@^3.67.1: optionalDependencies: fsevents "~2.3.2" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@7.0.0, wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@7.0.0, wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From 7bf2a71e3fb6aeedbd8a84c49b4aa63d60d1c6ed Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 23 Sep 2024 08:05:12 +0000 Subject: [PATCH 2/8] Fix import --- packages/vercel-edge/src/sdk.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vercel-edge/src/sdk.ts b/packages/vercel-edge/src/sdk.ts index 2a8a5c698c07..33bb38ac2e02 100644 --- a/packages/vercel-edge/src/sdk.ts +++ b/packages/vercel-edge/src/sdk.ts @@ -25,7 +25,7 @@ import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, SEMRESATTRS_SERVICE_NAMESPACE, -} from '@opentelemetry/semantic-conventions/*'; +} from '@opentelemetry/semantic-conventions'; import { SentryPropagator, SentrySampler, From 54e9caea59ba5b57eb803de2cb6b977c372c0e26 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 30 Sep 2024 13:37:35 +0000 Subject: [PATCH 3/8] Don't rely on event emitter which is nodejs api --- .../vercel-edge/src/async-local-storage-context-manager.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/vercel-edge/src/async-local-storage-context-manager.ts b/packages/vercel-edge/src/async-local-storage-context-manager.ts index 1c9a772df138..abdfaeffa48e 100644 --- a/packages/vercel-edge/src/async-local-storage-context-manager.ts +++ b/packages/vercel-edge/src/async-local-storage-context-manager.ts @@ -18,7 +18,7 @@ // - https://github.com/open-telemetry/opentelemetry-js/blob/6515ed8098333646a63a74a8c0150cc2daf520db/packages/opentelemetry-context-async-hooks/src/AbstractAsyncHooksContextManager.ts // - https://github.com/open-telemetry/opentelemetry-js/blob/6515ed8098333646a63a74a8c0150cc2daf520db/packages/opentelemetry-context-async-hooks/src/AsyncLocalStorageContextManager.ts -import { EventEmitter } from 'events'; +import type { EventEmitter } from 'events'; import { ROOT_CONTEXT } from '@opentelemetry/api'; import type { Context, ContextManager } from '@opentelemetry/api'; import { logger } from '@sentry/utils'; @@ -61,8 +61,8 @@ abstract class AbstractAsyncHooksContextManager implements ContextManager { * the provided context will be used as the active context for the duration of the call. */ public bind(context: Context, target: T): T { - if (target instanceof EventEmitter) { - return this._bindEventEmitter(context, target); + if (typeof target === 'object' && target !== null && 'on' in target) { + return this._bindEventEmitter(context, target as unknown as EventEmitter) as T; } if (typeof target === 'function') { From 8ac57ab2abe3848d86ec37f6648bbe115bf1be58 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 1 Oct 2024 12:57:57 +0000 Subject: [PATCH 4/8] Bundle OTEL deps into `@sentry/vercel-edge` package --- dev-packages/rollup-utils/npmHelpers.mjs | 3 +- packages/nextjs/src/edge/index.ts | 2 + packages/vercel-edge/package.json | 10 ++-- packages/vercel-edge/rollup.npm.config.mjs | 61 +++++++++++++++++++++- 4 files changed, 68 insertions(+), 8 deletions(-) diff --git a/dev-packages/rollup-utils/npmHelpers.mjs b/dev-packages/rollup-utils/npmHelpers.mjs index 1a855e5674b7..4e6483364ee4 100644 --- a/dev-packages/rollup-utils/npmHelpers.mjs +++ b/dev-packages/rollup-utils/npmHelpers.mjs @@ -36,6 +36,7 @@ export function makeBaseNPMConfig(options = {}) { packageSpecificConfig = {}, addPolyfills = true, sucrase = {}, + bundledBuiltins = [], } = options; const nodeResolvePlugin = makeNodeResolvePlugin(); @@ -113,7 +114,7 @@ export function makeBaseNPMConfig(options = {}) { // don't include imported modules from outside the package in the final output external: [ - ...builtinModules, + ...builtinModules.filter(m => !bundledBuiltins.includes(m)), ...Object.keys(packageDotJSON.dependencies || {}), ...Object.keys(packageDotJSON.peerDependencies || {}), ...Object.keys(packageDotJSON.optionalDependencies || {}), diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index 7034873f665e..fcd0ec0e5932 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -6,6 +6,8 @@ import { getDefaultIntegrations, init as vercelEdgeInit } from '@sentry/vercel-e import { isBuild } from '../common/utils/isBuild'; import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration'; +export { captureUnderscoreErrorException } from '../common/_error'; + export type EdgeOptions = VercelEdgeOptions; const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json index 0a13c67c155d..fda8b808d529 100644 --- a/packages/vercel-edge/package.json +++ b/packages/vercel-edge/package.json @@ -40,16 +40,16 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/core": "^1.25.1", - "@opentelemetry/resources": "^1.26.0", - "@opentelemetry/sdk-trace-base": "^1.26.0", - "@opentelemetry/semantic-conventions": "^1.27.0", "@sentry/core": "8.30.0", - "@sentry/opentelemetry": "8.30.0", "@sentry/types": "8.30.0", "@sentry/utils": "8.30.0" }, "devDependencies": { + "@opentelemetry/semantic-conventions": "^1.27.0", + "@opentelemetry/core": "^1.25.1", + "@opentelemetry/resources": "^1.26.0", + "@opentelemetry/sdk-trace-base": "^1.26.0", + "@sentry/opentelemetry": "8.30.0", "@edge-runtime/types": "3.0.1" }, "scripts": { diff --git a/packages/vercel-edge/rollup.npm.config.mjs b/packages/vercel-edge/rollup.npm.config.mjs index 84a06f2fb64a..3cfd779d57f6 100644 --- a/packages/vercel-edge/rollup.npm.config.mjs +++ b/packages/vercel-edge/rollup.npm.config.mjs @@ -1,3 +1,60 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; +import replace from '@rollup/plugin-replace'; +import { makeBaseNPMConfig, makeNPMConfigVariants, plugins } from '@sentry-internal/rollup-utils'; -export default makeNPMConfigVariants(makeBaseNPMConfig()); +export default makeNPMConfigVariants( + makeBaseNPMConfig({ + entrypoints: ['src/index.ts'], + bundledBuiltins: ['perf_hooks'], + packageSpecificConfig: { + context: 'globalThis', + output: { + preserveModules: false, + }, + plugins: [ + plugins.makeCommonJSPlugin({ transformMixedEsModules: true }), // Needed because various modules in the OTEL toolchain use CJS (require-in-the-middle, shimmer, etc..) + plugins.makeJsonPlugin(), // Needed because `require-in-the-middle` imports json via require + replace({ + preventAssignment: true, + values: { + 'process.argv0': JSON.stringify(''), // needed because otel relies on process.argv0 for the default service name, but that api is not available in the edge runtime. + }, + }), + { + // This plugin is needed because otel imports `performance` from `perf_hooks` and also uses it via the `performance` global. + // Both of these APIs are not available in the edge runtime so we need to define a polyfill. + // Vercel does something similar in the `@vercel/otel` package: https://github.com/vercel/otel/blob/087601ae585cb116bb2b46c211d014520de76c71/packages/otel/build.ts#L62 + name: 'perf-hooks-performance-polyfill', + banner: ` + { + if (globalThis.performance === undefined) { + globalThis.performance = { + timeOrigin: 0, + now: () => Date.now() + }; + } + } + `, + resolveId: source => { + if (source === 'perf_hooks') { + return '\0perf_hooks_sentry_shim'; + } else { + return null; + } + }, + load: id => { + if (id === '\0perf_hooks_sentry_shim') { + return ` + export const performance = { + timeOrigin: 0, + now: () => Date.now() + } + `; + } else { + return null; + } + }, + }, + ], + }, + }), +); From 0ab56c112781f3426846cefd828fae794424e613 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 1 Oct 2024 14:37:54 +0000 Subject: [PATCH 5/8] Remove Next.js tracing escapages for datafetchers --- .../src/common/utils/edgeWrapperUtils.ts | 108 +++++++++--------- .../nextjs/src/common/utils/wrapperUtils.ts | 106 +++-------------- packages/nextjs/src/server/index.ts | 23 ++-- 3 files changed, 79 insertions(+), 158 deletions(-) diff --git a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts index 65bdabc93dda..df896a95298b 100644 --- a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts +++ b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts @@ -13,7 +13,7 @@ import { winterCGRequestToRequestData } from '@sentry/utils'; import type { EdgeRouteHandler } from '../../edge/types'; import { flushSafelyWithTimeout } from './responseEnd'; -import { commonObjectToIsolationScope, escapeNextjsTracing } from './tracingUtils'; +import { commonObjectToIsolationScope } from './tracingUtils'; import { vercelWaitUntil } from './vercelWaitUntil'; /** @@ -24,68 +24,66 @@ export function withEdgeWrapping( options: { spanDescription: string; spanOp: string; mechanismFunctionName: string }, ): (...params: Parameters) => Promise> { return async function (this: unknown, ...args) { - return escapeNextjsTracing(() => { - const req: unknown = args[0]; - return withIsolationScope(commonObjectToIsolationScope(req), isolationScope => { - let sentryTrace; - let baggage; + const req: unknown = args[0]; + return withIsolationScope(commonObjectToIsolationScope(req), isolationScope => { + let sentryTrace; + let baggage; - if (req instanceof Request) { - sentryTrace = req.headers.get('sentry-trace') || ''; - baggage = req.headers.get('baggage'); + if (req instanceof Request) { + sentryTrace = req.headers.get('sentry-trace') || ''; + baggage = req.headers.get('baggage'); - isolationScope.setSDKProcessingMetadata({ - request: winterCGRequestToRequestData(req), - }); - } + isolationScope.setSDKProcessingMetadata({ + request: winterCGRequestToRequestData(req), + }); + } - isolationScope.setTransactionName(options.spanDescription); + isolationScope.setTransactionName(options.spanDescription); - return continueTrace( - { - sentryTrace, - baggage, - }, - () => { - return startSpan( - { - name: options.spanDescription, - op: options.spanOp, - forceTransaction: true, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.withEdgeWrapping', - }, + return continueTrace( + { + sentryTrace, + baggage, + }, + () => { + return startSpan( + { + name: options.spanDescription, + op: options.spanOp, + forceTransaction: true, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.withEdgeWrapping', }, - async span => { - const handlerResult = await handleCallbackErrors( - () => handler.apply(this, args), - error => { - captureException(error, { - mechanism: { - type: 'instrument', - handled: false, - data: { - function: options.mechanismFunctionName, - }, + }, + async span => { + const handlerResult = await handleCallbackErrors( + () => handler.apply(this, args), + error => { + captureException(error, { + mechanism: { + type: 'instrument', + handled: false, + data: { + function: options.mechanismFunctionName, }, - }); - }, - ); + }, + }); + }, + ); - if (handlerResult instanceof Response) { - setHttpStatus(span, handlerResult.status); - } else { - span.setStatus({ code: SPAN_STATUS_OK }); - } + if (handlerResult instanceof Response) { + setHttpStatus(span, handlerResult.status); + } else { + span.setStatus({ code: SPAN_STATUS_OK }); + } - return handlerResult; - }, - ); - }, - ).finally(() => { - vercelWaitUntil(flushSafelyWithTimeout()); - }); + return handlerResult; + }, + ); + }, + ).finally(() => { + vercelWaitUntil(flushSafelyWithTimeout()); }); }); }; diff --git a/packages/nextjs/src/common/utils/wrapperUtils.ts b/packages/nextjs/src/common/utils/wrapperUtils.ts index f07970e4db3b..ae07825151bd 100644 --- a/packages/nextjs/src/common/utils/wrapperUtils.ts +++ b/packages/nextjs/src/common/utils/wrapperUtils.ts @@ -5,19 +5,14 @@ import { SPAN_STATUS_ERROR, SPAN_STATUS_OK, captureException, - continueTrace, getTraceData, - startInactiveSpan, startSpan, - startSpanManual, - withActiveSpan, withIsolationScope, } from '@sentry/core'; import type { Span } from '@sentry/types'; -import { isString } from '@sentry/utils'; -import { autoEndSpanOnResponseEnd, flushSafelyWithTimeout } from './responseEnd'; -import { commonObjectToIsolationScope, escapeNextjsTracing } from './tracingUtils'; +import { flushSafelyWithTimeout } from './responseEnd'; +import { commonObjectToIsolationScope } from './tracingUtils'; import { vercelWaitUntil } from './vercelWaitUntil'; declare module 'http' { @@ -26,21 +21,6 @@ declare module 'http' { } } -/** - * Grabs a span off a Next.js datafetcher request object, if it was previously put there via - * `setSpanOnRequest`. - * - * @param req The Next.js datafetcher request object - * @returns the span on the request object if there is one, or `undefined` if the request object didn't have one. - */ -export function getSpanFromRequest(req: IncomingMessage): Span | undefined { - return req._sentrySpan; -} - -function setSpanOnRequest(span: Span, req: IncomingMessage): void { - req._sentrySpan = span; -} - /** * Wraps a function that potentially throws. If it does, the error is passed to `captureException` and rethrown. * @@ -80,7 +60,10 @@ export function withErrorInstrumentation any>( export function withTracedServerSideDataFetcher Promise | any>( origDataFetcher: F, req: IncomingMessage, + // TODO(v9): Remove these unused arguments + // eslint-disable-next-line @typescript-eslint/no-unused-vars res: ServerResponse, + // eslint-disable-next-line @typescript-eslint/no-unused-vars options: { /** Parameterized route of the request - will be used for naming the transaction. */ requestedRouteName: string; @@ -94,80 +77,25 @@ export function withTracedServerSideDataFetcher Pr this: unknown, ...args: Parameters ): Promise<{ data: ReturnType; sentryTrace?: string; baggage?: string }> { - return escapeNextjsTracing(() => { - const isolationScope = commonObjectToIsolationScope(req); - return withIsolationScope(isolationScope, () => { - isolationScope.setTransactionName(`${options.dataFetchingMethodName} (${options.dataFetcherRouteName})`); - isolationScope.setSDKProcessingMetadata({ - request: req, - }); - - const sentryTrace = - req.headers && isString(req.headers['sentry-trace']) ? req.headers['sentry-trace'] : undefined; - const baggage = req.headers?.baggage; - - return continueTrace({ sentryTrace, baggage }, () => { - const requestSpan = getOrStartRequestSpan(req, res, options.requestedRouteName); - return withActiveSpan(requestSpan, () => { - return startSpanManual( - { - op: 'function.nextjs', - name: `${options.dataFetchingMethodName} (${options.dataFetcherRouteName})`, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - }, - }, - async dataFetcherSpan => { - dataFetcherSpan.setStatus({ code: SPAN_STATUS_OK }); - const { 'sentry-trace': sentryTrace, baggage } = getTraceData(); - try { - return { - sentryTrace: sentryTrace, - baggage: baggage, - data: await origDataFetcher.apply(this, args), - }; - } catch (e) { - dataFetcherSpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); - requestSpan?.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); - throw e; - } finally { - dataFetcherSpan.end(); - } - }, - ); - }); - }); + const isolationScope = commonObjectToIsolationScope(req); + // TODO(lforst): Get rid of this with + return withIsolationScope(isolationScope, async () => { + isolationScope.setSDKProcessingMetadata({ + request: req, }); + + const { 'sentry-trace': sentryTrace, baggage } = getTraceData(); + return { + sentryTrace: sentryTrace, + baggage: baggage, + data: await origDataFetcher.apply(this, args), + }; }).finally(() => { vercelWaitUntil(flushSafelyWithTimeout()); }); }; } -function getOrStartRequestSpan(req: IncomingMessage, res: ServerResponse, name: string): Span { - const existingSpan = getSpanFromRequest(req); - if (existingSpan) { - return existingSpan; - } - - const requestSpan = startInactiveSpan({ - name, - forceTransaction: true, - op: 'http.server', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - }, - }); - - requestSpan.setStatus({ code: SPAN_STATUS_OK }); - setSpanOnRequest(requestSpan, req); - autoEndSpanOnResponseEnd(requestSpan, res); - - return requestSpan; -} - /** * Call a data fetcher and trace it. Only traces the function if there is an active transaction on the scope. * diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index e787f978cf22..20780d3638fc 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -1,6 +1,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, applySdkMetadata, getClient, getGlobalScope, @@ -211,15 +212,6 @@ export function init(options: NodeOptions): NodeClient | undefined { return null; } - // We only want to use our HTTP integration/instrumentation for app router requests, which are marked with the `sentry.rsc` attribute. - if ( - (event.contexts?.trace?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.http.otel.http' || - event.contexts?.trace?.data?.['next.span_type'] === 'BaseServer.handleRequest') && - event.contexts?.trace?.data?.['sentry.rsc'] !== true - ) { - return null; - } - // Filter out transactions for requests to the tunnel route if ( globalWithInjectedValues.__sentryRewritesTunnelPath__ && @@ -238,8 +230,7 @@ export function init(options: NodeOptions): NodeClient | undefined { // Pages router event.transaction === '/404' || // App router (could be "GET /404", "POST /404", ...) - event.transaction?.match(/^(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH) \/404$/) || - event.transaction === 'GET /_not-found' + event.transaction?.match(/^(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH) \/(404|_not-found)$/) ) { return null; } @@ -295,13 +286,17 @@ export function init(options: NodeOptions): NodeClient | undefined { // Next.js that are actually more or less correct server HTTP spans, so we are backfilling the op here. if ( event.type === 'transaction' && - event.transaction?.match(/^(RSC )?GET /) && - event.contexts?.trace?.data?.['sentry.rsc'] === true && - !event.contexts.trace.op + !!event.contexts?.trace?.data?.['http.method'] && + !!event.contexts?.trace?.data?.['next.span_type'] && + event.transaction?.match(/^(RSC )?(GET|HEAD|POST|PUT|DELETE|PATCH|OPTIONS|CONNECT|TRACE)/) ) { event.contexts.trace.data = event.contexts.trace.data || {}; + event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_OP] = 'http.server'; event.contexts.trace.op = 'http.server'; + + event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = 'auto.http.server.nextjs'; + event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = 'route'; } return event; From 0919e154a5c7a03b8dbf5be2d6bf7a1e32235bf5 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 1 Oct 2024 15:05:32 +0000 Subject: [PATCH 6/8] Don't record spans for things in the public folder --- packages/nextjs/src/server/index.ts | 58 ++++++++++++++++------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 20780d3638fc..4339bf2f9722 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -212,6 +212,12 @@ export function init(options: NodeOptions): NodeClient | undefined { return null; } + // Filter out transactions for requests requesting public assets. + // These transactions usually don't contain any spans. + if (event.transaction?.match(/^GET \/.+/) && event.spans?.length === 0) { + return null; + } + // Filter out transactions for requests to the tunnel route if ( globalWithInjectedValues.__sentryRewritesTunnelPath__ && @@ -244,6 +250,32 @@ export function init(options: NodeOptions): NodeClient | undefined { ), ); + getGlobalScope().addEventProcessor( + Object.assign( + (event => { + // Sometimes, the HTTP integration will not work, causing us not to properly set an op for spans generated by + // Next.js that are actually more or less correct server HTTP spans, so we are backfilling the op here. + if ( + event.type === 'transaction' && + !!event.contexts?.trace?.data?.['http.method'] && + !!event.contexts?.trace?.data?.['next.span_type'] && + event.transaction?.match(/^(RSC )?(GET|HEAD|POST|PUT|DELETE|PATCH|OPTIONS|CONNECT|TRACE)/) + ) { + event.contexts.trace.data = event.contexts.trace.data || {}; + + event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_OP] = 'http.server'; + event.contexts.trace.op = 'http.server'; + + event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = 'auto.http.server.nextjs'; + event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = 'route'; + } + + return event; + }) satisfies EventProcessor, + { id: 'NextjsTransactionEnhancer' }, + ), + ); + getGlobalScope().addEventProcessor( Object.assign( ((event, hint) => { @@ -279,32 +311,6 @@ export function init(options: NodeOptions): NodeClient | undefined { ), ); - getGlobalScope().addEventProcessor( - Object.assign( - (event => { - // Sometimes, the HTTP integration will not work, causing us not to properly set an op for spans generated by - // Next.js that are actually more or less correct server HTTP spans, so we are backfilling the op here. - if ( - event.type === 'transaction' && - !!event.contexts?.trace?.data?.['http.method'] && - !!event.contexts?.trace?.data?.['next.span_type'] && - event.transaction?.match(/^(RSC )?(GET|HEAD|POST|PUT|DELETE|PATCH|OPTIONS|CONNECT|TRACE)/) - ) { - event.contexts.trace.data = event.contexts.trace.data || {}; - - event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_OP] = 'http.server'; - event.contexts.trace.op = 'http.server'; - - event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = 'auto.http.server.nextjs'; - event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = 'route'; - } - - return event; - }) satisfies EventProcessor, - { id: 'NextjsTransactionEnhancer' }, - ), - ); - if (process.env.NODE_ENV === 'development') { getGlobalScope().addEventProcessor(devErrorSymbolicationEventProcessor); } From 4d8162ba25c11836457a1054da5217dd3acb5732 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 1 Oct 2024 15:10:37 +0000 Subject: [PATCH 7/8] remove custom tracing logic from pages router component error instrumentation --- .../src/common/wrapPageComponentWithSentry.ts | 80 +++++-------------- 1 file changed, 20 insertions(+), 60 deletions(-) diff --git a/packages/nextjs/src/common/wrapPageComponentWithSentry.ts b/packages/nextjs/src/common/wrapPageComponentWithSentry.ts index 8cd4a250ac14..e42722392ed4 100644 --- a/packages/nextjs/src/common/wrapPageComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapPageComponentWithSentry.ts @@ -1,6 +1,4 @@ -import { captureException, getCurrentScope, withIsolationScope } from '@sentry/core'; -import { extractTraceparentData } from '@sentry/utils'; -import { escapeNextjsTracing } from './utils/tracingUtils'; +import { captureException } from '@sentry/core'; interface FunctionComponent { (...args: unknown[]): unknown; @@ -25,69 +23,31 @@ export function wrapPageComponentWithSentry(pageComponent: FunctionComponent | C if (isReactClassComponent(pageComponent)) { return class SentryWrappedPageComponent extends pageComponent { public render(...args: unknown[]): unknown { - return escapeNextjsTracing(() => { - return withIsolationScope(() => { - const scope = getCurrentScope(); - // We extract the sentry trace data that is put in the component props by datafetcher wrappers - const sentryTraceData = - typeof this.props === 'object' && - this.props !== null && - '_sentryTraceData' in this.props && - typeof this.props._sentryTraceData === 'string' - ? this.props._sentryTraceData - : undefined; - - if (sentryTraceData) { - const traceparentData = extractTraceparentData(sentryTraceData); - scope.setContext('trace', { - span_id: traceparentData?.parentSpanId, - trace_id: traceparentData?.traceId, - }); - } - - try { - return super.render(...args); - } catch (e) { - captureException(e, { - mechanism: { - handled: false, - }, - }); - throw e; - } + try { + return super.render(...args); + } catch (e) { + captureException(e, { + mechanism: { + handled: false, + }, }); - }); + throw e; + } } }; } else if (typeof pageComponent === 'function') { return new Proxy(pageComponent, { - apply(target, thisArg, argArray: [{ _sentryTraceData?: string } | undefined]) { - return escapeNextjsTracing(() => { - return withIsolationScope(() => { - const scope = getCurrentScope(); - // We extract the sentry trace data that is put in the component props by datafetcher wrappers - const sentryTraceData = argArray?.[0]?._sentryTraceData; - - if (sentryTraceData) { - const traceparentData = extractTraceparentData(sentryTraceData); - scope.setContext('trace', { - span_id: traceparentData?.parentSpanId, - trace_id: traceparentData?.traceId, - }); - } - - try { - return target.apply(thisArg, argArray); - } catch (e) { - captureException(e, { - mechanism: { - handled: false, - }, - }); - throw e; - } + apply(target, thisArg, argArray) { + try { + return target.apply(thisArg, argArray); + } catch (e) { + captureException(e, { + mechanism: { + handled: false, + }, }); - }); + throw e; + } }, }); } else { From 1312085a25a7d8f5e5e3895cb8787bbd95abf912 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 2 Oct 2024 08:33:59 +0000 Subject: [PATCH 8/8] Create isolation scope --- packages/nextjs/src/server/index.ts | 35 ++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 4339bf2f9722..40442e4fc5f9 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -3,22 +3,28 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, applySdkMetadata, + getCapturedScopesOnSpan, getClient, + getCurrentScope, getGlobalScope, + getIsolationScope, getRootSpan, + setCapturedScopesOnSpan, spanToJSON, } from '@sentry/core'; import { getDefaultIntegrations, init as nodeInit } from '@sentry/node'; import type { NodeClient, NodeOptions } from '@sentry/node'; import { GLOBAL_OBJ, logger } from '@sentry/utils'; +import { context } from '@opentelemetry/api'; import { ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_ROUTE, SEMATTRS_HTTP_METHOD, SEMATTRS_HTTP_TARGET, } from '@opentelemetry/semantic-conventions'; -import type { EventProcessor } from '@sentry/types'; +import { getScopesFromContext } from '@sentry/opentelemetry'; +import type { EventProcessor, Scope } from '@sentry/types'; import { DEBUG_BUILD } from '../common/debug-build'; import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor'; import { getVercelEnv } from '../common/getVercelEnv'; @@ -197,6 +203,22 @@ export function init(options: NodeOptions): NodeClient | undefined { // We want to rename these spans because they look like "GET /path/to/route" and we already emit spans that look // like this with our own http instrumentation. if (spanAttributes?.['next.span_type'] === 'BaseServer.handleRequest') { + const isRootSpan = span === getRootSpan(span); + if (isRootSpan) { + // TODO(lforst): Verify if this is actually working and add tests + const scopes = getCapturedScopesOnSpan(span); + + const isolationScope = (scopes.isolationScope || getIsolationScope()).clone(); + const scope = scopes.scope || getCurrentScope(); + + const client = getClient(); + if (client && client.getOptions().autoSessionTracking) { + isolationScope.setRequestSession({ status: 'ok' }); + } + setIsolationScope(isolationScope); + setCapturedScopesOnSpan(span, scope, isolationScope); + } + } else { span.updateName('next server handler'); // This is all lowercase because the spans that Next.js emits by itself generally look like this. } }); @@ -327,3 +349,14 @@ function sdkAlreadyInitialized(): boolean { export * from '../common'; export { wrapApiHandlerWithSentry } from '../common/wrapApiHandlerWithSentry'; + +/* + * Update the active isolation scope. + * Should be used with caution! + */ +function setIsolationScope(isolationScope: Scope): void { + const scopes = getScopesFromContext(context.active()); + if (scopes) { + scopes.isolationScope = isolationScope; + } +}