From 0c1226d2db0294b7245b00d11e70e820338794a5 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 29 Jul 2026 11:19:47 +0200 Subject: [PATCH 1/7] ref(nestjs): Remove import-in-the-middle instrumentation Migrate `@sentry/nestjs` fully to channel-based instrumentation and drop the OpenTelemetry `InstrumentationBase` module patchers, so the package no longer relies on the import-in-the-middle ESM loader hook. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 2 + packages/nestjs/package.json | 2 - .../enums/AttributeNames.ts => enums.ts} | 6 + packages/nestjs/src/integrations/helpers.ts | 9 +- packages/nestjs/src/integrations/nest.ts | 50 +----- .../sentry-nest-bullmq-instrumentation.ts | 77 -------- .../sentry-nest-event-instrumentation.ts | 72 -------- .../sentry-nest-instrumentation.ts | 115 ------------ .../sentry-nest-schedule-instrumentation.ts | 93 ---------- .../integrations/vendored/enums/NestType.ts | 14 -- .../src/integrations/vendored/enums/index.ts | 11 -- .../integrations/vendored/instrumentation.ts | 126 ------------- .../src/integrations/wrap-components.ts | 3 +- .../nestjs/src/integrations/wrap-handlers.ts | 5 +- .../nestjs/src/integrations/wrap-route.ts | 7 +- .../nestjs/test/integrations/bullmq.test.ts | 155 ---------------- .../test/integrations/nest-event.test.ts | 166 ------------------ .../nestjs/test/integrations/schedule.test.ts | 113 ------------ .../test/integrations/span-origin.test.ts | 41 +---- .../nestjs/test/orchestrion/nestjs.test.ts | 99 +++++++++++ 20 files changed, 128 insertions(+), 1038 deletions(-) rename packages/nestjs/src/integrations/{vendored/enums/AttributeNames.ts => enums.ts} (80%) delete mode 100644 packages/nestjs/src/integrations/sentry-nest-bullmq-instrumentation.ts delete mode 100644 packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts delete mode 100644 packages/nestjs/src/integrations/sentry-nest-instrumentation.ts delete mode 100644 packages/nestjs/src/integrations/sentry-nest-schedule-instrumentation.ts delete mode 100644 packages/nestjs/src/integrations/vendored/enums/NestType.ts delete mode 100644 packages/nestjs/src/integrations/vendored/enums/index.ts delete mode 100644 packages/nestjs/src/integrations/vendored/instrumentation.ts delete mode 100644 packages/nestjs/test/integrations/bullmq.test.ts delete mode 100644 packages/nestjs/test/integrations/nest-event.test.ts delete mode 100644 packages/nestjs/test/integrations/schedule.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 86436b9158be..54a8fdb457b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ Work in this release was contributed by @psh4607, @trinitiwowka, @nehaprasad-dev, and @JealousGx. Thank you for your contributions! +- ref(nestjs): Remove `import-in-the-middle`-based instrumentation in favor of channel-based instrumentation + - feat(deno)!: Rename several default integrations to match the other SDKs ([#22404](https://github.com/getsentry/sentry-javascript/pull/22404)). The `deno*Integration` exports are kept as deprecated aliases. If you were relying on the names (for example, to disable them), then note that these have changed: - `DenoAmqplib` => `Amqplib` - `DenoKoa` => `Koa` diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index 2cfe49b1a28d..69c89a327ed6 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -44,8 +44,6 @@ "access": "public" }, "dependencies": { - "@opentelemetry/api": "^1.9.1", - "@opentelemetry/instrumentation": "^0.220.0", "@sentry/conventions": "^0.16.0", "@sentry/core": "10.67.0", "@sentry/node": "10.67.0", diff --git a/packages/nestjs/src/integrations/vendored/enums/AttributeNames.ts b/packages/nestjs/src/integrations/enums.ts similarity index 80% rename from packages/nestjs/src/integrations/vendored/enums/AttributeNames.ts rename to packages/nestjs/src/integrations/enums.ts index 29398847a24d..b253542fe7e8 100644 --- a/packages/nestjs/src/integrations/vendored/enums/AttributeNames.ts +++ b/packages/nestjs/src/integrations/enums.ts @@ -14,3 +14,9 @@ export enum AttributeNames { CONTROLLER = 'nestjs.controller', CALLBACK = 'nestjs.callback', } + +export enum NestType { + APP_CREATION = 'app_creation', + REQUEST_CONTEXT = 'request_context', + REQUEST_HANDLER = 'handler', +} diff --git a/packages/nestjs/src/integrations/helpers.ts b/packages/nestjs/src/integrations/helpers.ts index f9ae4373c806..154ba32548f2 100644 --- a/packages/nestjs/src/integrations/helpers.ts +++ b/packages/nestjs/src/integrations/helpers.ts @@ -5,7 +5,6 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, withActiveSpan, } from '@sentry/core'; -import { isOrchestrionInjected } from '@sentry/server-utils/orchestrion'; import type { CatchTarget, InjectableTarget, NextFunction, Observable, Subscription } from './types'; /** A function of unknown signature, matching the methods/handlers we wrap. */ @@ -71,7 +70,7 @@ export function isTargetPatched(target: object, flag: 'sentryPatchedInjectable' /** Origin for middleware/guard/pipe/interceptor/exception_filter spans. */ function middlewareOrigin(componentType?: string): string { - const base = isOrchestrionInjected() ? 'auto.middleware.orchestrion.nestjs' : 'auto.middleware.nestjs'; + const base = 'auto.middleware.orchestrion.nestjs'; return componentType ? `${base}.${componentType}` : base; } @@ -79,17 +78,17 @@ function middlewareOrigin(componentType?: string): string { * Origin for the app-creation / request-context / request-handler HTTP spans. */ export function httpOrigin(): string { - return isOrchestrionInjected() ? 'auto.http.orchestrion.nestjs' : 'auto.http.otel.nestjs'; + return 'auto.http.orchestrion.nestjs'; } /** Origin for `@OnEvent` spans. */ function eventOrigin(): string { - return isOrchestrionInjected() ? 'auto.event.orchestrion.nestjs' : 'auto.event.nestjs'; + return 'auto.event.orchestrion.nestjs'; } /** Origin for BullMQ `@Processor` `process` spans. */ function bullmqOrigin(): string { - return isOrchestrionInjected() ? 'auto.queue.orchestrion.nestjs.bullmq' : 'auto.queue.nestjs.bullmq'; + return 'auto.queue.orchestrion.nestjs.bullmq'; } /** diff --git a/packages/nestjs/src/integrations/nest.ts b/packages/nestjs/src/integrations/nest.ts index 9abae800e244..6ba36880827e 100644 --- a/packages/nestjs/src/integrations/nest.ts +++ b/packages/nestjs/src/integrations/nest.ts @@ -1,60 +1,20 @@ -import { NestInstrumentation as NestInstrumentationCore } from './vendored/instrumentation'; import { defineIntegration } from '@sentry/core'; -import { generateInstrumentOnce } from '@sentry/node'; -import { isOrchestrionInjected } from '@sentry/server-utils/orchestrion'; import { subscribeToNestChannels } from './orchestrion-subscriber'; -import { SentryNestBullMQInstrumentation } from './sentry-nest-bullmq-instrumentation'; -import { SentryNestEventInstrumentation } from './sentry-nest-event-instrumentation'; -import { SentryNestInstrumentation } from './sentry-nest-instrumentation'; -import { SentryNestScheduleInstrumentation } from './sentry-nest-schedule-instrumentation'; const INTEGRATION_NAME = 'Nest' as const; -const instrumentNestCore = generateInstrumentOnce(`${INTEGRATION_NAME}.Core`, () => { - return new NestInstrumentationCore(); -}); - -const instrumentNestCommon = generateInstrumentOnce(`${INTEGRATION_NAME}.Common`, () => { - return new SentryNestInstrumentation(); -}); - -const instrumentNestEvent = generateInstrumentOnce(`${INTEGRATION_NAME}.Event`, () => { - return new SentryNestEventInstrumentation(); -}); - -const instrumentNestSchedule = generateInstrumentOnce(`${INTEGRATION_NAME}.Schedule`, () => { - return new SentryNestScheduleInstrumentation(); -}); - -const instrumentNestBullMQ = generateInstrumentOnce(`${INTEGRATION_NAME}.BullMQ`, () => { - return new SentryNestBullMQInstrumentation(); -}); - -export const instrumentNest = Object.assign( - (): void => { - instrumentNestCore(); - instrumentNestCommon(); - instrumentNestEvent(); - instrumentNestSchedule(); - instrumentNestBullMQ(); - }, - { id: INTEGRATION_NAME }, -); - /** * Integration capturing tracing data for NestJS. - * Only the span origin differs between otel and orchestrion implementations. - * See the shared `./wrap-*` helpers + * + * Instrumentation is channel-based: it subscribes to the diagnostics channels + * that orchestrion injects into `@nestjs/*` (via the runtime hook installed by + * `Sentry.init()` or a bundler plugin). See the shared `./wrap-*` helpers. */ export const nestIntegration = defineIntegration(() => { return { name: INTEGRATION_NAME, setupOnce() { - if (isOrchestrionInjected()) { - subscribeToNestChannels(); - } else { - instrumentNest(); - } + subscribeToNestChannels(); }, }; }); diff --git a/packages/nestjs/src/integrations/sentry-nest-bullmq-instrumentation.ts b/packages/nestjs/src/integrations/sentry-nest-bullmq-instrumentation.ts deleted file mode 100644 index bf40bd70db8c..000000000000 --- a/packages/nestjs/src/integrations/sentry-nest-bullmq-instrumentation.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import { - InstrumentationBase, - InstrumentationNodeModuleDefinition, - InstrumentationNodeModuleFile, - isWrapped, -} from '@opentelemetry/instrumentation'; -import { SDK_VERSION } from '@sentry/core'; -import type { ProcessorDecoratorTarget } from './types'; -import { extractQueueName, patchProcessorTarget } from './wrap-handlers'; - -const supportedVersions = ['>=10.0.0']; -const COMPONENT = '@nestjs/bullmq'; - -/** - * Custom instrumentation for nestjs bullmq module. - * - * This hooks into the `@Processor` class decorator, which is applied on queue - * processor classes. - */ -export class SentryNestBullMQInstrumentation extends InstrumentationBase { - public constructor(config: InstrumentationConfig = {}) { - super('sentry-nestjs-bullmq', SDK_VERSION, config); - } - - /** - * Initializes the instrumentation by defining the modules to be patched. - */ - public init(): InstrumentationNodeModuleDefinition { - const moduleDef = new InstrumentationNodeModuleDefinition(COMPONENT, supportedVersions); - - moduleDef.files.push(this._getProcessorFileInstrumentation(supportedVersions)); - return moduleDef; - } - - /** - * Wraps the @Processor decorator. - */ - private _getProcessorFileInstrumentation(versions: string[]): InstrumentationNodeModuleFile { - return new InstrumentationNodeModuleFile( - '@nestjs/bullmq/dist/decorators/processor.decorator.js', - versions, - (moduleExports: { Processor: ProcessorDecoratorTarget }) => { - if (isWrapped(moduleExports.Processor)) { - this._unwrap(moduleExports, 'Processor'); - } - this._wrap(moduleExports, 'Processor', this._createWrapProcessor()); - return moduleExports; - }, - (moduleExports: { Processor: ProcessorDecoratorTarget }) => { - this._unwrap(moduleExports, 'Processor'); - }, - ); - } - - /** - * Creates a wrapper function for the @Processor class decorator. - */ - private _createWrapProcessor() { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function wrapProcessor(original: any) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function wrappedProcessor(...decoratorArgs: any[]) { - const queueName = extractQueueName(decoratorArgs[0]); - - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const classDecorator = original(...decoratorArgs); - - return function (target: ProcessorDecoratorTarget) { - patchProcessorTarget(target, queueName); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return classDecorator(target); - }; - }; - }; - } -} diff --git a/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts b/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts deleted file mode 100644 index 7bb2967cc453..000000000000 --- a/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import { - InstrumentationBase, - InstrumentationNodeModuleDefinition, - InstrumentationNodeModuleFile, - isWrapped, -} from '@opentelemetry/instrumentation'; -import { SDK_VERSION } from '@sentry/core'; -import type { AnyFn } from './helpers'; -import type { OnEventTarget } from './types'; -import { patchMethodDescriptor, wrapEventHandler } from './wrap-handlers'; - -const supportedVersions = ['>=2.0.0']; -const COMPONENT = '@nestjs/event-emitter'; - -/** - * Custom instrumentation for nestjs event-emitter - * - * This hooks into the `OnEvent` decorator, which is applied on event handlers. - */ -export class SentryNestEventInstrumentation extends InstrumentationBase { - public constructor(config: InstrumentationConfig = {}) { - super('sentry-nestjs-event', SDK_VERSION, config); - } - - /** - * Initializes the instrumentation by defining the modules to be patched. - */ - public init(): InstrumentationNodeModuleDefinition { - const moduleDef = new InstrumentationNodeModuleDefinition(COMPONENT, supportedVersions); - - moduleDef.files.push(this._getOnEventFileInstrumentation(supportedVersions)); - return moduleDef; - } - - /** - * Wraps the @OnEvent decorator. - */ - private _getOnEventFileInstrumentation(versions: string[]): InstrumentationNodeModuleFile { - return new InstrumentationNodeModuleFile( - '@nestjs/event-emitter/dist/decorators/on-event.decorator.js', - versions, - (moduleExports: { OnEvent: OnEventTarget }) => { - if (isWrapped(moduleExports.OnEvent)) { - this._unwrap(moduleExports, 'OnEvent'); - } - this._wrap(moduleExports, 'OnEvent', this._createWrapOnEvent()); - return moduleExports; - }, - (moduleExports: { OnEvent: OnEventTarget }) => { - this._unwrap(moduleExports, 'OnEvent'); - }, - ); - } - - /** - * Creates a wrapper function for the @OnEvent decorator. - */ - private _createWrapOnEvent() { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function wrapOnEvent(original: any) { - return function wrappedOnEvent(event: unknown, options?: unknown) { - const decoratorResult = original(event, options); - - return (target: OnEventTarget, propertyKey: string | symbol, descriptor: PropertyDescriptor) => { - patchMethodDescriptor(target, propertyKey, descriptor, (handler: AnyFn) => wrapEventHandler(handler, event)); - return decoratorResult(target, propertyKey, descriptor); - }; - }; - }; - } -} diff --git a/packages/nestjs/src/integrations/sentry-nest-instrumentation.ts b/packages/nestjs/src/integrations/sentry-nest-instrumentation.ts deleted file mode 100644 index 810907f9640e..000000000000 --- a/packages/nestjs/src/integrations/sentry-nest-instrumentation.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import { - InstrumentationBase, - InstrumentationNodeModuleDefinition, - InstrumentationNodeModuleFile, - isWrapped, -} from '@opentelemetry/instrumentation'; -import { SDK_VERSION } from '@sentry/core'; -import { patchCatchTarget, patchInjectableTarget } from './wrap-components'; -import type { CatchTarget, InjectableTarget, MinimalNestJsExecutionContext } from './types'; - -const supportedVersions = ['>=8.0.0 <12']; -const COMPONENT = '@nestjs/common'; - -/** - * Custom instrumentation for nestjs. - * - * This hooks into - * 1. @Injectable decorator, which is applied on class middleware, - * interceptors and guards. - * 2. @Catch decorator, which is applied on exception filters. - */ -export class SentryNestInstrumentation extends InstrumentationBase { - public constructor(config: InstrumentationConfig = {}) { - super('sentry-nestjs', SDK_VERSION, config); - } - - /** - * Initializes the instrumentation by defining the modules to be patched. - */ - public init(): InstrumentationNodeModuleDefinition { - const moduleDef = new InstrumentationNodeModuleDefinition(COMPONENT, supportedVersions); - - moduleDef.files.push( - this._getInjectableFileInstrumentation(supportedVersions), - this._getCatchFileInstrumentation(supportedVersions), - ); - return moduleDef; - } - - /** - * Wraps the @Injectable decorator. - */ - private _getInjectableFileInstrumentation(versions: string[]): InstrumentationNodeModuleFile { - return new InstrumentationNodeModuleFile( - '@nestjs/common/decorators/core/injectable.decorator.js', - versions, - (moduleExports: { Injectable: InjectableTarget }) => { - if (isWrapped(moduleExports.Injectable)) { - this._unwrap(moduleExports, 'Injectable'); - } - this._wrap(moduleExports, 'Injectable', this._createWrapInjectable()); - return moduleExports; - }, - (moduleExports: { Injectable: InjectableTarget }) => { - this._unwrap(moduleExports, 'Injectable'); - }, - ); - } - - /** - * Wraps the @Catch decorator. - */ - private _getCatchFileInstrumentation(versions: string[]): InstrumentationNodeModuleFile { - return new InstrumentationNodeModuleFile( - '@nestjs/common/decorators/core/catch.decorator.js', - versions, - (moduleExports: { Catch: CatchTarget }) => { - if (isWrapped(moduleExports.Catch)) { - this._unwrap(moduleExports, 'Catch'); - } - this._wrap(moduleExports, 'Catch', this._createWrapCatch()); - return moduleExports; - }, - (moduleExports: { Catch: CatchTarget }) => { - this._unwrap(moduleExports, 'Catch'); - }, - ); - } - - /** - * Creates a wrapper function for the @Injectable decorator. It patches the - * decorated class (via `patchInjectableTarget`) and delegates to the - * original decorator. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private _createWrapInjectable(): (original: any) => (options?: unknown) => (target: InjectableTarget) => any { - const seenContexts = new WeakSet(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function wrapInjectable(original: any) { - return function wrappedInjectable(options?: unknown) { - return function (target: InjectableTarget) { - patchInjectableTarget(target, seenContexts); - return original(options)(target); - }; - }; - }; - } - - /** - * Creates a wrapper function for the @Catch decorator. Used to instrument - * exception filters. - */ - private _createWrapCatch() { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function wrapCatch(original: any) { - return function wrappedCatch(...exceptions: unknown[]) { - return function (target: CatchTarget) { - patchCatchTarget(target); - return original(...exceptions)(target); - }; - }; - }; - } -} diff --git a/packages/nestjs/src/integrations/sentry-nest-schedule-instrumentation.ts b/packages/nestjs/src/integrations/sentry-nest-schedule-instrumentation.ts deleted file mode 100644 index c846659175bc..000000000000 --- a/packages/nestjs/src/integrations/sentry-nest-schedule-instrumentation.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import { - InstrumentationBase, - InstrumentationNodeModuleDefinition, - InstrumentationNodeModuleFile, - isWrapped, -} from '@opentelemetry/instrumentation'; -import { SDK_VERSION } from '@sentry/core'; -import type { AnyFn } from './helpers'; -import type { ScheduleDecoratorTarget } from './types'; -import { - MECHANISM_CRON, - MECHANISM_INTERVAL, - MECHANISM_TIMEOUT, - patchMethodDescriptor, - wrapScheduleHandler, -} from './wrap-handlers'; - -const supportedVersions = ['>=2.0.0']; -const COMPONENT = '@nestjs/schedule'; - -/** - * Custom instrumentation for nestjs schedule module. - * - * This hooks into the `@Cron`, `@Interval`, and `@Timeout` decorators, which - * are applied on scheduled task handlers. - */ -export class SentryNestScheduleInstrumentation extends InstrumentationBase { - public constructor(config: InstrumentationConfig = {}) { - super('sentry-nestjs-schedule', SDK_VERSION, config); - } - - /** - * Initializes the instrumentation by defining the modules to be patched. - */ - public init(): InstrumentationNodeModuleDefinition { - const moduleDef = new InstrumentationNodeModuleDefinition(COMPONENT, supportedVersions); - - moduleDef.files.push(this._getDecoratorFileInstrumentation('Cron', 'cron', MECHANISM_CRON, supportedVersions)); - moduleDef.files.push( - this._getDecoratorFileInstrumentation('Interval', 'interval', MECHANISM_INTERVAL, supportedVersions), - ); - moduleDef.files.push( - this._getDecoratorFileInstrumentation('Timeout', 'timeout', MECHANISM_TIMEOUT, supportedVersions), - ); - return moduleDef; - } - - /** - * Wraps a schedule decorator (`@Cron`/`@Interval`/`@Timeout`). - */ - private _getDecoratorFileInstrumentation( - exportName: 'Cron' | 'Interval' | 'Timeout', - fileName: string, - mechanismType: string, - versions: string[], - ): InstrumentationNodeModuleFile { - return new InstrumentationNodeModuleFile( - `@nestjs/schedule/dist/decorators/${fileName}.decorator.js`, - versions, - (moduleExports: Record) => { - if (isWrapped(moduleExports[exportName])) { - this._unwrap(moduleExports, exportName); - } - this._wrap(moduleExports, exportName, this._createWrapDecorator(mechanismType)); - return moduleExports; - }, - (moduleExports: Record) => { - this._unwrap(moduleExports, exportName); - }, - ); - } - - /** - * Creates a wrapper function for a schedule decorator. - */ - private _createWrapDecorator(mechanismType: string) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function wrapDecorator(original: any) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function wrappedDecorator(...decoratorArgs: any[]) { - const decoratorResult = original(...decoratorArgs); - - return (target: ScheduleDecoratorTarget, propertyKey: string | symbol, descriptor: PropertyDescriptor) => { - patchMethodDescriptor(target, propertyKey, descriptor, (handler: AnyFn) => - wrapScheduleHandler(handler, mechanismType), - ); - return decoratorResult(target, propertyKey, descriptor); - }; - }; - }; - } -} diff --git a/packages/nestjs/src/integrations/vendored/enums/NestType.ts b/packages/nestjs/src/integrations/vendored/enums/NestType.ts deleted file mode 100644 index 14b39a01c6db..000000000000 --- a/packages/nestjs/src/integrations/vendored/enums/NestType.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-nestjs-core - * - Upstream version: @opentelemetry/instrumentation-nestjs-core@0.64.0 - */ - -export enum NestType { - APP_CREATION = 'app_creation', - REQUEST_CONTEXT = 'request_context', - REQUEST_HANDLER = 'handler', -} diff --git a/packages/nestjs/src/integrations/vendored/enums/index.ts b/packages/nestjs/src/integrations/vendored/enums/index.ts deleted file mode 100644 index 328a4080108d..000000000000 --- a/packages/nestjs/src/integrations/vendored/enums/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-nestjs-core - * - Upstream version: @opentelemetry/instrumentation-nestjs-core@0.64.0 - */ - -export { AttributeNames } from './AttributeNames'; -export { NestType } from './NestType'; diff --git a/packages/nestjs/src/integrations/vendored/instrumentation.ts b/packages/nestjs/src/integrations/vendored/instrumentation.ts deleted file mode 100644 index b40aa98f768c..000000000000 --- a/packages/nestjs/src/integrations/vendored/instrumentation.ts +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-nestjs-core - * - Upstream version: @opentelemetry/instrumentation-nestjs-core@0.64.0 - * - Some types vendored from @nestjs/core and @nestjs/common with simplifications - * - The span-emitting logic (app-creation / request-context / request-handler - * spans) has been extracted to `../wrap-route` and is shared with the - * orchestrion (diagnostics-channel) path; this file only wraps the - * `NestFactory.create` / `RouterExecutionContext.create` methods to feed into it. - */ -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import { - InstrumentationBase, - InstrumentationNodeModuleDefinition, - InstrumentationNodeModuleFile, - isWrapped, -} from '@opentelemetry/instrumentation'; -import { SDK_VERSION, startSpan } from '@sentry/core'; -import type { AnyFn } from '../helpers'; -import { getAppCreationSpanOptions, wrapRequestContextHandler, wrapRouteHandler } from '../wrap-route'; - -const PACKAGE_NAME = '@sentry/instrumentation-nestjs-core'; - -type Controller = object; - -declare const NestFactory: { - create(...args: unknown[]): Promise; -}; - -interface RouterExecutionContext { - create(instance: Controller, callback: (...args: unknown[]) => unknown, ...args: unknown[]): unknown; -} - -const supportedVersions = ['>=4.0.0 <12']; - -export class NestInstrumentation extends InstrumentationBase { - static readonly COMPONENT = '@nestjs/core'; - - constructor(config: InstrumentationConfig = {}) { - super(PACKAGE_NAME, SDK_VERSION, config); - } - - init() { - const module = new InstrumentationNodeModuleDefinition(NestInstrumentation.COMPONENT, supportedVersions); - - module.files.push( - this.getNestFactoryFileInstrumentation(supportedVersions), - this.getRouterExecutionContextFileInstrumentation(supportedVersions), - ); - - return module; - } - - getNestFactoryFileInstrumentation(versions: string[]) { - return new InstrumentationNodeModuleFile( - '@nestjs/core/nest-factory.js', - versions, - (moduleExports: { NestFactoryStatic: { prototype: typeof NestFactory } }, moduleVersion?: string) => { - this.ensureWrapped( - moduleExports.NestFactoryStatic.prototype, - 'create', - createWrapNestFactoryCreate(moduleVersion), - ); - return moduleExports; - }, - (moduleExports: { NestFactoryStatic: { prototype: typeof NestFactory } }) => { - this._unwrap(moduleExports.NestFactoryStatic.prototype, 'create'); - }, - ); - } - - getRouterExecutionContextFileInstrumentation(versions: string[]) { - return new InstrumentationNodeModuleFile( - '@nestjs/core/router/router-execution-context.js', - versions, - (moduleExports: { RouterExecutionContext: { prototype: RouterExecutionContext } }, moduleVersion?: string) => { - this.ensureWrapped( - moduleExports.RouterExecutionContext.prototype, - 'create', - createWrapCreateHandler(moduleVersion), - ); - return moduleExports; - }, - (moduleExports: { RouterExecutionContext: { prototype: RouterExecutionContext } }) => { - this._unwrap(moduleExports.RouterExecutionContext.prototype, 'create'); - }, - ); - } - - private ensureWrapped( - obj: T, - methodName: K, - wrapper: (original: T[K]) => T[K], - ): void { - if (isWrapped(obj[methodName])) { - this._unwrap(obj, methodName); - } - this._wrap(obj, methodName, wrapper); - } -} - -function createWrapNestFactoryCreate(moduleVersion?: string) { - return function wrapCreate(original: typeof NestFactory.create): typeof NestFactory.create { - return function createWithTrace(this: typeof NestFactory, ...args: unknown[]) { - const nestModule = args[0] as { name?: string }; - return startSpan(getAppCreationSpanOptions(moduleVersion, nestModule?.name), () => original.apply(this, args)); - }; - }; -} - -function createWrapCreateHandler(moduleVersion: string | undefined) { - return function wrapCreateHandler(original: RouterExecutionContext['create']): RouterExecutionContext['create'] { - return function createHandlerWithTrace(this: RouterExecutionContext, ...args: unknown[]) { - const instance = args[0] as { constructor?: { name?: string } }; - const callback = args[1] as AnyFn; - const instanceName = instance?.constructor?.name || 'UnnamedInstance'; - const callbackName = typeof callback === 'function' ? callback.name : ''; - args[1] = wrapRouteHandler(callback, moduleVersion); - const handler = original.apply(this, args) as AnyFn; - return wrapRequestContextHandler(handler, instanceName, callbackName, moduleVersion); - }; - }; -} diff --git a/packages/nestjs/src/integrations/wrap-components.ts b/packages/nestjs/src/integrations/wrap-components.ts index 8664d72c49e7..5a0dc965da98 100644 --- a/packages/nestjs/src/integrations/wrap-components.ts +++ b/packages/nestjs/src/integrations/wrap-components.ts @@ -7,8 +7,7 @@ import type { CallHandler, CatchTarget, InjectableTarget, MinimalNestJsExecution /** * Shared span-emitting logic for `@Injectable` * (middleware/guard/pipe/interceptor) and `@Catch` (exception filter) classes. - * Used by both the OTel decorator wraps (`SentryNestInstrumentation`) and the - * orchestrion channel subscriber. + * Used by the orchestrion channel subscriber. */ function patchInterceptor( diff --git a/packages/nestjs/src/integrations/wrap-handlers.ts b/packages/nestjs/src/integrations/wrap-handlers.ts index 58e080a5cde6..8888428832f5 100644 --- a/packages/nestjs/src/integrations/wrap-handlers.ts +++ b/packages/nestjs/src/integrations/wrap-handlers.ts @@ -133,9 +133,8 @@ export function wrapBullMQProcess(process: AnyFn, queueName: string): AnyFn { /** * Replace a method decorator's `descriptor.value` with a wrapped handler (via - * `wrapHandler`), preserving the handler name and marking it wrapped. Shared - * by the OTel schedule/event decorator wraps and the orchestrion factory - * subscriber. + * `wrapHandler`), preserving the handler name and marking it wrapped. Used by + * the orchestrion factory subscriber. */ export function patchMethodDescriptor( target: { __SENTRY_INTERNAL__?: boolean } | undefined, diff --git a/packages/nestjs/src/integrations/wrap-route.ts b/packages/nestjs/src/integrations/wrap-route.ts index 7d31f539dd3a..371ee3ecadcc 100644 --- a/packages/nestjs/src/integrations/wrap-route.ts +++ b/packages/nestjs/src/integrations/wrap-route.ts @@ -3,13 +3,12 @@ import type { SpanAttributes } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core'; import type { AnyFn } from './helpers'; import { copyReflectMetadata, httpOrigin, isWrapped, markWrapped } from './helpers'; -import { AttributeNames, NestType } from './vendored/enums'; +import { AttributeNames, NestType } from './enums'; /** * Shared span-emitting logic for the NestJS route/controller spans - * (app-creation / request-context / request-handler). Used by both the OTel - * `NestFactory.create` / `RouterExecutionContext.create` wraps (`./vendored`) - * and the orchestrion channel subscriber. + * (app-creation / request-context / request-handler). Used by the orchestrion + * channel subscriber. */ const NESTJS_COMPONENT = '@nestjs/core'; diff --git a/packages/nestjs/test/integrations/bullmq.test.ts b/packages/nestjs/test/integrations/bullmq.test.ts deleted file mode 100644 index 349a0c1b8e43..000000000000 --- a/packages/nestjs/test/integrations/bullmq.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -import 'reflect-metadata'; -import * as core from '@sentry/core'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { SentryNestBullMQInstrumentation } from '../../src/integrations/sentry-nest-bullmq-instrumentation'; - -describe('BullMQInstrumentation', () => { - let instrumentation: SentryNestBullMQInstrumentation; - - beforeEach(() => { - instrumentation = new SentryNestBullMQInstrumentation(); - vi.spyOn(core, 'captureException'); - vi.spyOn(core, 'withIsolationScope').mockImplementation(callback => { - return (callback as () => unknown)(); - }); - vi.spyOn(core, 'startSpan').mockImplementation((_, callback) => { - return (callback as () => unknown)(); - }); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe('Processor decorator wrapping', () => { - let wrappedDecorator: any; - let mockClassDecorator: vi.Mock; - let mockProcessor: any; - - beforeEach(() => { - mockClassDecorator = vi.fn().mockImplementation(() => { - return (target: any) => target; - }); - - const moduleDef = instrumentation.init(); - const file = moduleDef.files[0]; - const moduleExports = { Processor: mockClassDecorator }; - file?.patch(moduleExports); - wrappedDecorator = moduleExports.Processor; - }); - - it('should call withIsolationScope and startSpan on process execution', async () => { - const originalProcess = vi.fn().mockResolvedValue('result'); - - mockProcessor = class TestProcessor { - process = originalProcess; - }; - mockProcessor.prototype.process = originalProcess; - - const classDecoratorFn = wrappedDecorator('test-queue'); - classDecoratorFn(mockProcessor); - - await mockProcessor.prototype.process(); - - expect(core.withIsolationScope).toHaveBeenCalled(); - expect(core.startSpan).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'test-queue process', - forceTransaction: true, - attributes: expect.objectContaining({ - 'sentry.op': 'queue.process', - 'sentry.origin': 'auto.queue.nestjs.bullmq', - 'messaging.system': 'bullmq', - 'messaging.destination.name': 'test-queue', - }), - }), - expect.any(Function), - ); - expect(originalProcess).toHaveBeenCalled(); - }); - - it('should capture async exceptions and rethrow', async () => { - const error = new Error('Test error'); - const originalProcess = vi.fn().mockRejectedValue(error); - - mockProcessor = class TestProcessor {}; - mockProcessor.prototype.process = originalProcess; - - const classDecoratorFn = wrappedDecorator('test-queue'); - classDecoratorFn(mockProcessor); - - await expect(mockProcessor.prototype.process()).rejects.toThrow(error); - expect(core.captureException).toHaveBeenCalledWith(error, { - mechanism: { - handled: false, - type: 'auto.queue.nestjs.bullmq', - }, - }); - }); - - it('should skip wrapping when __SENTRY_INTERNAL__ is set', async () => { - const originalProcess = vi.fn().mockResolvedValue('result'); - - mockProcessor = class TestProcessor {}; - mockProcessor.prototype.process = originalProcess; - mockProcessor.__SENTRY_INTERNAL__ = true; - - const classDecoratorFn = wrappedDecorator('test-queue'); - classDecoratorFn(mockProcessor); - - // process should not be wrapped - expect(mockProcessor.prototype.process).toBe(originalProcess); - }); - - it('should not double-wrap process method', async () => { - const originalProcess = vi.fn().mockResolvedValue('result'); - - mockProcessor = class TestProcessor {}; - mockProcessor.prototype.process = originalProcess; - - const classDecoratorFn = wrappedDecorator('test-queue'); - classDecoratorFn(mockProcessor); - - const wrappedProcess = mockProcessor.prototype.process; - expect(wrappedProcess).not.toBe(originalProcess); - - // Apply decorator again - const classDecoratorFn2 = wrappedDecorator('test-queue'); - classDecoratorFn2(mockProcessor); - - // Should still be the same wrapped function (not double-wrapped) - expect(mockProcessor.prototype.process).toBe(wrappedProcess); - }); - - it('should extract queue name from ProcessorOptions object', async () => { - const originalProcess = vi.fn().mockResolvedValue('result'); - - mockProcessor = class TestProcessor {}; - mockProcessor.prototype.process = originalProcess; - - const classDecoratorFn = wrappedDecorator({ name: 'my-queue' }); - classDecoratorFn(mockProcessor); - - await mockProcessor.prototype.process(); - - expect(core.startSpan).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'my-queue process', - }), - expect.any(Function), - ); - }); - - it('should apply the original class decorator', () => { - const originalProcess = vi.fn().mockResolvedValue('result'); - - mockProcessor = class TestProcessor {}; - mockProcessor.prototype.process = originalProcess; - - const classDecoratorFn = wrappedDecorator('test-queue'); - classDecoratorFn(mockProcessor); - - expect(mockClassDecorator).toHaveBeenCalledWith('test-queue'); - }); - }); -}); diff --git a/packages/nestjs/test/integrations/nest-event.test.ts b/packages/nestjs/test/integrations/nest-event.test.ts deleted file mode 100644 index debf5bc8e34a..000000000000 --- a/packages/nestjs/test/integrations/nest-event.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -import 'reflect-metadata'; -import * as core from '@sentry/core'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { SentryNestEventInstrumentation } from '../../src/integrations/sentry-nest-event-instrumentation'; -import type { OnEventTarget } from '../../src/integrations/types'; - -describe('EventInstrumentation', () => { - let instrumentation: SentryNestEventInstrumentation; - let mockOnEvent: vi.Mock; - let mockTarget: OnEventTarget; - - beforeEach(() => { - instrumentation = new SentryNestEventInstrumentation(); - // Mock OnEvent to return a function that applies the descriptor - mockOnEvent = vi.fn().mockImplementation(() => { - return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { - return descriptor; - }; - }); - mockTarget = { - name: 'TestClass', - prototype: {}, - } as OnEventTarget; - vi.spyOn(core, 'startSpan'); - vi.spyOn(core, 'captureException'); - vi.spyOn(core, 'withIsolationScope'); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe('init()', () => { - it('should return module definition with correct component name', () => { - const moduleDef = instrumentation.init(); - expect(moduleDef.name).toBe('@nestjs/event-emitter'); - }); - }); - - describe('OnEvent decorator wrapping', () => { - let wrappedOnEvent: any; - let descriptor: PropertyDescriptor; - let originalHandler: vi.Mock; - - beforeEach(() => { - originalHandler = vi.fn().mockResolvedValue('result'); - descriptor = { - value: originalHandler, - }; - - const moduleDef = instrumentation.init(); - const onEventFile = moduleDef.files[0]; - const moduleExports = { OnEvent: mockOnEvent }; - onEventFile?.patch(moduleExports); - wrappedOnEvent = moduleExports.OnEvent; - }); - - it('should wrap string event handlers', async () => { - const decorated = wrappedOnEvent('test.event'); - decorated(mockTarget, 'testMethod', descriptor); - - await descriptor.value(); - - expect(core.withIsolationScope).toHaveBeenCalled(); - expect(core.startSpan).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'event test.event', - }), - expect.any(Function), - ); - expect(originalHandler).toHaveBeenCalled(); - }); - - it('should wrap symbol event handlers', async () => { - const decorated = wrappedOnEvent(Symbol('test.event')); - decorated(mockTarget, 'testMethod', descriptor); - - await descriptor.value(); - - expect(core.withIsolationScope).toHaveBeenCalled(); - expect(core.startSpan).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'event Symbol(test.event)', - }), - expect.any(Function), - ); - expect(originalHandler).toHaveBeenCalled(); - }); - - it('should wrap string array event handlers', async () => { - const decorated = wrappedOnEvent(['test.event1', 'test.event2']); - decorated(mockTarget, 'testMethod', descriptor); - - await descriptor.value(); - - expect(core.withIsolationScope).toHaveBeenCalled(); - expect(core.startSpan).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'event test.event1,test.event2', - }), - expect.any(Function), - ); - expect(originalHandler).toHaveBeenCalled(); - }); - - it('should wrap symbol array event handlers', async () => { - const decorated = wrappedOnEvent([Symbol('test.event1'), Symbol('test.event2')]); - decorated(mockTarget, 'testMethod', descriptor); - - await descriptor.value(); - - expect(core.withIsolationScope).toHaveBeenCalled(); - expect(core.startSpan).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'event Symbol(test.event1),Symbol(test.event2)', - }), - expect.any(Function), - ); - expect(originalHandler).toHaveBeenCalled(); - }); - - it('should wrap mixed type array event handlers', async () => { - const decorated = wrappedOnEvent([Symbol('test.event1'), 'test.event2', Symbol('test.event3')]); - decorated(mockTarget, 'testMethod', descriptor); - - await descriptor.value(); - - expect(core.withIsolationScope).toHaveBeenCalled(); - expect(core.startSpan).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'event Symbol(test.event1),test.event2,Symbol(test.event3)', - }), - expect.any(Function), - ); - expect(originalHandler).toHaveBeenCalled(); - }); - - it('should capture exceptions and rethrow', async () => { - const error = new Error('Test error'); - originalHandler.mockRejectedValue(error); - - const decorated = wrappedOnEvent('test.event'); - decorated(mockTarget, 'testMethod', descriptor); - - await expect(descriptor.value()).rejects.toThrow(error); - expect(core.captureException).toHaveBeenCalledWith(error, { - mechanism: { - handled: false, - type: 'auto.event.nestjs', - }, - }); - }); - - it('should skip wrapping for internal Sentry handlers', () => { - const internalTarget = { - ...mockTarget, - __SENTRY_INTERNAL__: true, - }; - - const decorated = wrappedOnEvent('test.event'); - decorated(internalTarget, 'testMethod', descriptor); - - expect(descriptor.value).toBe(originalHandler); - }); - }); -}); diff --git a/packages/nestjs/test/integrations/schedule.test.ts b/packages/nestjs/test/integrations/schedule.test.ts deleted file mode 100644 index 3694499c1919..000000000000 --- a/packages/nestjs/test/integrations/schedule.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -import 'reflect-metadata'; -import * as core from '@sentry/core'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { SentryNestScheduleInstrumentation } from '../../src/integrations/sentry-nest-schedule-instrumentation'; -import type { ScheduleDecoratorTarget } from '../../src/integrations/types'; - -describe('ScheduleInstrumentation', () => { - let instrumentation: SentryNestScheduleInstrumentation; - let mockTarget: ScheduleDecoratorTarget; - - beforeEach(() => { - instrumentation = new SentryNestScheduleInstrumentation(); - mockTarget = { - name: 'TestClass', - } as ScheduleDecoratorTarget; - vi.spyOn(core, 'captureException'); - vi.spyOn(core, 'withIsolationScope').mockImplementation(callback => { - return (callback as () => unknown)(); - }); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe.each([ - { decoratorName: 'Cron', fileIndex: 0, mechanismType: 'auto.function.nestjs.cron' }, - { decoratorName: 'Interval', fileIndex: 1, mechanismType: 'auto.function.nestjs.interval' }, - { decoratorName: 'Timeout', fileIndex: 2, mechanismType: 'auto.function.nestjs.timeout' }, - ])('$decoratorName decorator wrapping', ({ decoratorName, fileIndex, mechanismType }) => { - let wrappedDecorator: any; - let descriptor: PropertyDescriptor; - let originalHandler: vi.Mock; - let mockDecorator: vi.Mock; - - beforeEach(() => { - originalHandler = vi.fn(function testHandler() { - return 'result'; - }); - descriptor = { - value: originalHandler, - }; - - mockDecorator = vi.fn().mockImplementation(() => { - return (_target: any, _propertyKey: string, descriptor: PropertyDescriptor) => { - return descriptor; - }; - }); - - const moduleDef = instrumentation.init(); - const file = moduleDef.files[fileIndex]; - const moduleExports = { [decoratorName]: mockDecorator }; - file?.patch(moduleExports); - wrappedDecorator = moduleExports[decoratorName]; - }); - - it('should call withIsolationScope on handler execution', () => { - const decorated = wrappedDecorator('test-arg'); - decorated(mockTarget, 'testMethod', descriptor); - - descriptor.value(); - - expect(core.withIsolationScope).toHaveBeenCalled(); - expect(originalHandler).toHaveBeenCalled(); - }); - - it('should capture sync exceptions and rethrow', () => { - const error = new Error('Test error'); - originalHandler.mockImplementation(() => { - throw error; - }); - - const decorated = wrappedDecorator('test-arg'); - decorated(mockTarget, 'testMethod', descriptor); - - expect(() => descriptor.value()).toThrow(error); - expect(core.captureException).toHaveBeenCalledWith(error, { - mechanism: { - handled: false, - type: mechanismType, - }, - }); - }); - - it('should capture async exceptions and rethrow', async () => { - const error = new Error('Test error'); - originalHandler.mockReturnValue(Promise.reject(error)); - - const decorated = wrappedDecorator('test-arg'); - decorated(mockTarget, 'testMethod', descriptor); - - await expect(descriptor.value()).rejects.toThrow(error); - expect(core.captureException).toHaveBeenCalledWith(error, { - mechanism: { - handled: false, - type: mechanismType, - }, - }); - }); - - it('should skip wrapping for internal Sentry handlers', () => { - const internalTarget = { - ...mockTarget, - __SENTRY_INTERNAL__: true, - }; - - const decorated = wrappedDecorator('test-arg'); - decorated(internalTarget, 'testMethod', descriptor); - - expect(descriptor.value).toBe(originalHandler); - }); - }); -}); diff --git a/packages/nestjs/test/integrations/span-origin.test.ts b/packages/nestjs/test/integrations/span-origin.test.ts index 5e7ab09ecf92..ae31b40bcd82 100644 --- a/packages/nestjs/test/integrations/span-origin.test.ts +++ b/packages/nestjs/test/integrations/span-origin.test.ts @@ -1,5 +1,5 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; -import { afterEach, describe, expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { getBullMQProcessSpanOptions, getEventSpanOptions, @@ -7,12 +7,6 @@ import { httpOrigin, } from '../../src/integrations/helpers'; -type Marker = { runtime?: boolean; bundler?: boolean } | undefined; - -function setMarker(marker: Marker): void { - (globalThis as { __SENTRY_ORCHESTRION__?: Marker }).__SENTRY_ORCHESTRION__ = marker; -} - function middlewareOrigin(componentType?: string): unknown { return getMiddlewareSpanOptions({ name: 'X' }, undefined, componentType).attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]; } @@ -25,39 +19,16 @@ function bullmqOrigin(): unknown { return getBullMQProcessSpanOptions('q').attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]; } -// `nestIntegration` takes the channel path exactly when `isOrchestrionInjected()` -// is true (the `@nestjs/*` transform is always in the static config, so the -// global flag is sufficient), and the OTel path otherwise. The span origin -// follows the same flag. +// NestJS instrumentation is channel-based, so spans always use the orchestrion +// origins. describe('NestJS span origin selection', () => { - afterEach(() => { - delete (globalThis as { __SENTRY_ORCHESTRION__?: Marker }).__SENTRY_ORCHESTRION__; - }); - - it('emits orchestrion origins when injected via the runtime hook', () => { - setMarker({ runtime: true }); - + it('emits orchestrion origins', () => { expect(httpOrigin()).toBe('auto.http.orchestrion.nestjs'); expect(middlewareOrigin()).toBe('auto.middleware.orchestrion.nestjs'); expect(middlewareOrigin('guard')).toBe('auto.middleware.orchestrion.nestjs.guard'); + expect(middlewareOrigin('interceptor')).toBe('auto.middleware.orchestrion.nestjs.interceptor'); + expect(middlewareOrigin('pipe')).toBe('auto.middleware.orchestrion.nestjs.pipe'); expect(eventOrigin()).toBe('auto.event.orchestrion.nestjs'); expect(bullmqOrigin()).toBe('auto.queue.orchestrion.nestjs.bullmq'); }); - - it('emits orchestrion origins when injected via a bundler plugin', () => { - setMarker({ bundler: true }); - - expect(httpOrigin()).toBe('auto.http.orchestrion.nestjs'); - expect(middlewareOrigin('interceptor')).toBe('auto.middleware.orchestrion.nestjs.interceptor'); - }); - - it('emits OTel origins when orchestrion is not injected', () => { - setMarker(undefined); - - expect(httpOrigin()).toBe('auto.http.otel.nestjs'); - expect(middlewareOrigin()).toBe('auto.middleware.nestjs'); - expect(middlewareOrigin('pipe')).toBe('auto.middleware.nestjs.pipe'); - expect(eventOrigin()).toBe('auto.event.nestjs'); - expect(bullmqOrigin()).toBe('auto.queue.nestjs.bullmq'); - }); }); diff --git a/packages/nestjs/test/orchestrion/nestjs.test.ts b/packages/nestjs/test/orchestrion/nestjs.test.ts index 1e80c7f9854c..8bd859e3c2a3 100644 --- a/packages/nestjs/test/orchestrion/nestjs.test.ts +++ b/packages/nestjs/test/orchestrion/nestjs.test.ts @@ -938,4 +938,103 @@ describe('NestJS orchestrion subscriber: schedule / event / bullmq', () => { expect(spanToJSON(spanInside!).description).toBe('reports process'); }); }); + + it('schedule @Timeout: captures sync errors with the timeout mechanism', () => { + installTestAsyncContextStrategy(); + initTestClient(); + subscribeToNestChannels(); + const captureSpy = vi.spyOn(SentryCore, 'captureException').mockReturnValue('event-id'); + + const wrappedDecorator = driveFactory(CHANNELS.NESTJS_SCHEDULE_TIMEOUT, [1000], (_t, _k, d) => d); + const descriptor: PropertyDescriptor = { + value: function doTimeout(): void { + throw new Error('timeout boom'); + }, + configurable: true, + }; + wrappedDecorator({}, 'doTimeout', descriptor); + + expect(() => (descriptor.value as AnyFn)()).toThrow('timeout boom'); + expect(captureSpy).toHaveBeenCalledWith(expect.any(Error), { + mechanism: { handled: false, type: 'auto.function.nestjs.timeout' }, + }); + }); + + it('schedule @Cron: skips wrapping handlers flagged __SENTRY_INTERNAL__', () => { + installTestAsyncContextStrategy(); + initTestClient(); + subscribeToNestChannels(); + + const wrappedDecorator = driveFactory(CHANNELS.NESTJS_SCHEDULE_CRON, ['*/5 * * * *'], (_t, _k, d) => d); + const handler = function doCron(): void {}; + const descriptor: PropertyDescriptor = { value: handler, configurable: true }; + wrappedDecorator({ __SENTRY_INTERNAL__: true }, 'doCron', descriptor); + + expect(descriptor.value).toBe(handler); + }); + + it.each([ + { label: 'symbol', event: Symbol('user.created'), expected: 'event Symbol(user.created)' }, + { label: 'string array', event: ['user.created', 'user.updated'], expected: 'event user.created,user.updated' }, + { + label: 'mixed array', + event: [Symbol('user.created'), 'user.updated'], + expected: 'event Symbol(user.created),user.updated', + }, + ])('event @OnEvent: names the transaction from a $label event', async ({ event, expected }) => { + installTestAsyncContextStrategy(); + initTestClient(); + subscribeToNestChannels(); + + const wrappedDecorator = driveFactory(CHANNELS.NESTJS_ONEVENT, [event], (_t, _k, d) => d); + + let spanInside: Span | undefined; + const descriptor: PropertyDescriptor = { + value: async function onEvent(): Promise { + spanInside = getActiveSpan(); + return 'ok'; + }, + configurable: true, + }; + wrappedDecorator({}, 'onEvent', descriptor); + + await (descriptor.value as AnyFn)(); + + expect(spanToJSON(spanInside!).description).toBe(expected); + }); + + it('bullmq @Processor: skips wrapping when the class is flagged __SENTRY_INTERNAL__', () => { + installTestAsyncContextStrategy(); + initTestClient(); + subscribeToNestChannels(); + + const wrappedDecorator = driveFactory(CHANNELS.NESTJS_PROCESSOR, ['emails'], () => undefined); + + class InternalProcessor { + public async process(): Promise {} + } + (InternalProcessor as { __SENTRY_INTERNAL__?: boolean }).__SENTRY_INTERNAL__ = true; + const originalProcess = InternalProcessor.prototype.process; + wrappedDecorator(InternalProcessor); + + expect(InternalProcessor.prototype.process).toBe(originalProcess); + }); + + it('bullmq @Processor: does not double-wrap the process method', () => { + installTestAsyncContextStrategy(); + initTestClient(); + subscribeToNestChannels(); + + class EmailProcessor { + public async process(): Promise {} + } + const originalProcess = EmailProcessor.prototype.process; + + driveFactory(CHANNELS.NESTJS_PROCESSOR, ['emails'], () => undefined)(EmailProcessor); + const wrappedProcess = EmailProcessor.prototype.process; + expect(wrappedProcess).not.toBe(originalProcess); + + driveFactory(CHANNELS.NESTJS_PROCESSOR, ['emails'], () => undefined)(EmailProcessor); + expect(EmailProcessor.prototype.process).toBe(wrappedProcess); + }); }); From 5c2d0b21ab85a35d6709fe711a0f0807373e669c Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 29 Jul 2026 12:04:28 +0200 Subject: [PATCH 2/7] Trim redundant comment on nestIntegration --- packages/nestjs/src/integrations/nest.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/nestjs/src/integrations/nest.ts b/packages/nestjs/src/integrations/nest.ts index 6ba36880827e..01ef7d722e9c 100644 --- a/packages/nestjs/src/integrations/nest.ts +++ b/packages/nestjs/src/integrations/nest.ts @@ -5,10 +5,6 @@ const INTEGRATION_NAME = 'Nest' as const; /** * Integration capturing tracing data for NestJS. - * - * Instrumentation is channel-based: it subscribes to the diagnostics channels - * that orchestrion injects into `@nestjs/*` (via the runtime hook installed by - * `Sentry.init()` or a bundler plugin). See the shared `./wrap-*` helpers. */ export const nestIntegration = defineIntegration(() => { return { From a86f39d3eec6048b88a8bda32f7a11ab28275157 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 29 Jul 2026 12:19:15 +0200 Subject: [PATCH 3/7] Trim redundant comments in span-origin test --- packages/nestjs/test/integrations/span-origin.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/nestjs/test/integrations/span-origin.test.ts b/packages/nestjs/test/integrations/span-origin.test.ts index ae31b40bcd82..42143a664434 100644 --- a/packages/nestjs/test/integrations/span-origin.test.ts +++ b/packages/nestjs/test/integrations/span-origin.test.ts @@ -19,10 +19,8 @@ function bullmqOrigin(): unknown { return getBullMQProcessSpanOptions('q').attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]; } -// NestJS instrumentation is channel-based, so spans always use the orchestrion -// origins. describe('NestJS span origin selection', () => { - it('emits orchestrion origins', () => { + it('emits origins', () => { expect(httpOrigin()).toBe('auto.http.orchestrion.nestjs'); expect(middlewareOrigin()).toBe('auto.middleware.orchestrion.nestjs'); expect(middlewareOrigin('guard')).toBe('auto.middleware.orchestrion.nestjs.guard'); From 05f3582be526d2f612e350f99aeafd19ab03d2ab Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 29 Jul 2026 12:19:32 +0200 Subject: [PATCH 4/7] Rename span-origin describe block --- packages/nestjs/test/integrations/span-origin.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nestjs/test/integrations/span-origin.test.ts b/packages/nestjs/test/integrations/span-origin.test.ts index 42143a664434..e4090b4b916c 100644 --- a/packages/nestjs/test/integrations/span-origin.test.ts +++ b/packages/nestjs/test/integrations/span-origin.test.ts @@ -19,7 +19,7 @@ function bullmqOrigin(): unknown { return getBullMQProcessSpanOptions('q').attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]; } -describe('NestJS span origin selection', () => { +describe('NestJS span origins', () => { it('emits origins', () => { expect(httpOrigin()).toBe('auto.http.orchestrion.nestjs'); expect(middlewareOrigin()).toBe('auto.middleware.orchestrion.nestjs'); From c88c430ed6ce66f02767b2a2e2f1c82f6a185a1f Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 29 Jul 2026 12:20:46 +0200 Subject: [PATCH 5/7] Drop changelog entry --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54a8fdb457b4..86436b9158be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,6 @@ Work in this release was contributed by @psh4607, @trinitiwowka, @nehaprasad-dev, and @JealousGx. Thank you for your contributions! -- ref(nestjs): Remove `import-in-the-middle`-based instrumentation in favor of channel-based instrumentation - - feat(deno)!: Rename several default integrations to match the other SDKs ([#22404](https://github.com/getsentry/sentry-javascript/pull/22404)). The `deno*Integration` exports are kept as deprecated aliases. If you were relying on the names (for example, to disable them), then note that these have changed: - `DenoAmqplib` => `Amqplib` - `DenoKoa` => `Koa` From 49b1eea8871c0d15e208a3c235aba97075d390a6 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 29 Jul 2026 12:32:07 +0200 Subject: [PATCH 6/7] Remove dead orchestrion marker from test --- packages/nestjs/test/orchestrion/nestjs.test.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/nestjs/test/orchestrion/nestjs.test.ts b/packages/nestjs/test/orchestrion/nestjs.test.ts index 8bd859e3c2a3..90a7937449d1 100644 --- a/packages/nestjs/test/orchestrion/nestjs.test.ts +++ b/packages/nestjs/test/orchestrion/nestjs.test.ts @@ -17,20 +17,10 @@ import { setAsyncContextStrategy, spanToJSON, } from '@sentry/core'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { nestjsChannels as CHANNELS } from '@sentry/server-utils/orchestrion'; import { subscribeToNestChannels } from '../../src/integrations/orchestrion-subscriber'; -// The subscriber only ever runs when orchestrion has instrumented `@nestjs/*`. -// `isOrchestrionInjected()` selects the `orchestrion` span origins the -// assertions below expect, so the marker must be set. -beforeEach(() => { - (globalThis as { __SENTRY_ORCHESTRION__?: unknown }).__SENTRY_ORCHESTRION__ = { runtime: true }; -}); -afterEach(() => { - delete (globalThis as { __SENTRY_ORCHESTRION__?: unknown }).__SENTRY_ORCHESTRION__; -}); - // Mirrors harness in `tracing-channel.test.ts`: `bindTracingChannelToSpan` // only creates/ends spans when an async-context binding is available, so the // strategy below must be installed for the subscriber to do anything. From cad53a71522f22f1e6a7ff85ea73e3286b541286 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 29 Jul 2026 14:12:52 +0200 Subject: [PATCH 7/7] Move orchestrion subscriber test to integrations dir --- .../orchestrion-subscriber.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/nestjs/test/{orchestrion/nestjs.test.ts => integrations/orchestrion-subscriber.test.ts} (100%) diff --git a/packages/nestjs/test/orchestrion/nestjs.test.ts b/packages/nestjs/test/integrations/orchestrion-subscriber.test.ts similarity index 100% rename from packages/nestjs/test/orchestrion/nestjs.test.ts rename to packages/nestjs/test/integrations/orchestrion-subscriber.test.ts