From 36c3d1099e2ae1de4e5189390f48aee57fd79e7b Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Mon, 14 Jul 2025 23:48:01 +0200 Subject: [PATCH 1/4] feat(nestjs): Switch to OTel core instrumentation OTel's instrumentation supports v11 since [0.46.0](https://github.com/open-telemetry/opentelemetry-js-contrib/blob/de2260023b12c395e6f85592d29113b718a30f63/packages/instrumentation-nestjs-core/CHANGELOG.md?plain=1#L31-L36). --- packages/nestjs/src/integrations/nest.ts | 4 +- .../sentry-nest-core-instrumentation.ts | 307 ------------------ 2 files changed, 2 insertions(+), 309 deletions(-) delete mode 100644 packages/nestjs/src/integrations/sentry-nest-core-instrumentation.ts diff --git a/packages/nestjs/src/integrations/nest.ts b/packages/nestjs/src/integrations/nest.ts index 4cc68c720541..53086b7da302 100644 --- a/packages/nestjs/src/integrations/nest.ts +++ b/packages/nestjs/src/integrations/nest.ts @@ -1,13 +1,13 @@ +import { NestInstrumentation as NestInstrumentationCore } from '@opentelemetry/instrumentation-nestjs-core'; import { defineIntegration } from '@sentry/core'; import { generateInstrumentOnce } from '@sentry/node'; -import { NestInstrumentation } from './sentry-nest-core-instrumentation'; import { SentryNestEventInstrumentation } from './sentry-nest-event-instrumentation'; import { SentryNestInstrumentation } from './sentry-nest-instrumentation'; const INTEGRATION_NAME = 'Nest'; const instrumentNestCore = generateInstrumentOnce('Nest-Core', () => { - return new NestInstrumentation(); + return new NestInstrumentationCore(); }); const instrumentNestCommon = generateInstrumentOnce('Nest-Common', () => { diff --git a/packages/nestjs/src/integrations/sentry-nest-core-instrumentation.ts b/packages/nestjs/src/integrations/sentry-nest-core-instrumentation.ts deleted file mode 100644 index aec664633342..000000000000 --- a/packages/nestjs/src/integrations/sentry-nest-core-instrumentation.ts +++ /dev/null @@ -1,307 +0,0 @@ -/* - * This file is based on code from the OpenTelemetry Authors - * Source: https://github.com/open-telemetry/opentelemetry-js-contrib - * - * Modified for immediate requirements while maintaining compliance - * with the original Apache 2.0 license terms. - * - * Original License: - * 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. - */ - -import type { Controller } from '@nestjs/common/interfaces'; -import type { NestFactory } from '@nestjs/core/nest-factory.js'; -import type { RouterExecutionContext } from '@nestjs/core/router/router-execution-context.js'; -import * as api from '@opentelemetry/api'; -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import { - InstrumentationBase, - InstrumentationNodeModuleDefinition, - InstrumentationNodeModuleFile, - isWrapped, -} from '@opentelemetry/instrumentation'; -import { ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_ROUTE, SEMATTRS_HTTP_URL } from '@opentelemetry/semantic-conventions'; -import { SDK_VERSION } from '@sentry/core'; - -const supportedVersions = ['>=4.0.0 <12']; -const COMPONENT = '@nestjs/core'; - -enum AttributeNames { - VERSION = 'nestjs.version', - TYPE = 'nestjs.type', - MODULE = 'nestjs.module', - CONTROLLER = 'nestjs.controller', - CALLBACK = 'nestjs.callback', - PIPES = 'nestjs.pipes', - INTERCEPTORS = 'nestjs.interceptors', - GUARDS = 'nestjs.guards', -} - -export enum NestType { - APP_CREATION = 'app_creation', - REQUEST_CONTEXT = 'request_context', - REQUEST_HANDLER = 'handler', -} - -/** - * - */ -export class NestInstrumentation extends InstrumentationBase { - public constructor(config: InstrumentationConfig = {}) { - super('sentry-nestjs', SDK_VERSION, config); - } - - /** - * - */ - public init(): InstrumentationNodeModuleDefinition { - const module = new InstrumentationNodeModuleDefinition(COMPONENT, supportedVersions); - - module.files.push( - this._getNestFactoryFileInstrumentation(supportedVersions), - this._getRouterExecutionContextFileInstrumentation(supportedVersions), - ); - - return module; - } - - /** - * - */ - private _getNestFactoryFileInstrumentation(versions: string[]): InstrumentationNodeModuleFile { - return new InstrumentationNodeModuleFile( - '@nestjs/core/nest-factory.js', - versions, - // todo - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (NestFactoryStatic: any, moduleVersion?: string) => { - this._ensureWrapped( - // todo - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - NestFactoryStatic.NestFactoryStatic.prototype, - 'create', - createWrapNestFactoryCreate(this.tracer, moduleVersion), - ); - return NestFactoryStatic; - }, - // todo - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (NestFactoryStatic: any) => { - // todo - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - this._unwrap(NestFactoryStatic.NestFactoryStatic.prototype, 'create'); - }, - ); - } - - /** - * - */ - private _getRouterExecutionContextFileInstrumentation(versions: string[]): InstrumentationNodeModuleFile { - return new InstrumentationNodeModuleFile( - '@nestjs/core/router/router-execution-context.js', - versions, - // todo - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (RouterExecutionContext: any, moduleVersion?: string) => { - this._ensureWrapped( - // todo - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - RouterExecutionContext.RouterExecutionContext.prototype, - 'create', - createWrapCreateHandler(this.tracer, moduleVersion), - ); - return RouterExecutionContext; - }, - // todo - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (RouterExecutionContext: any) => { - // todo - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - this._unwrap(RouterExecutionContext.RouterExecutionContext.prototype, 'create'); - }, - ); - } - - /** - * - */ - // todo - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private _ensureWrapped(obj: any, methodName: string, wrapper: (original: any) => any): void { - // todo - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (isWrapped(obj[methodName])) { - this._unwrap(obj, methodName); - } - this._wrap(obj, methodName, wrapper); - } -} - -function createWrapNestFactoryCreate(tracer: api.Tracer, moduleVersion?: string) { - return function wrapCreate(original: typeof NestFactory.create) { - return function createWithTrace( - this: typeof NestFactory, - // todo - // eslint-disable-next-line @typescript-eslint/no-explicit-any - nestModule: any, - /* serverOrOptions */ - ) { - const span = tracer.startSpan('Create Nest App', { - attributes: { - component: COMPONENT, - [AttributeNames.TYPE]: NestType.APP_CREATION, - [AttributeNames.VERSION]: moduleVersion, - // todo - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - [AttributeNames.MODULE]: nestModule.name, - }, - }); - const spanContext = api.trace.setSpan(api.context.active(), span); - - return api.context.with(spanContext, async () => { - try { - // todo - // eslint-disable-next-line prefer-rest-params, @typescript-eslint/no-explicit-any - return await original.apply(this, arguments as any); - // todo - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (e: any) { - throw addError(span, e); - } finally { - span.end(); - } - }); - }; - }; -} - -function createWrapCreateHandler(tracer: api.Tracer, moduleVersion?: string) { - return function wrapCreateHandler(original: RouterExecutionContext['create']) { - return function createHandlerWithTrace( - this: RouterExecutionContext, - instance: Controller, - // todo - // eslint-disable-next-line @typescript-eslint/no-explicit-any - callback: (...args: any[]) => unknown, - ) { - // todo - // eslint-disable-next-line prefer-rest-params - arguments[1] = createWrapHandler(tracer, moduleVersion, callback); - // todo - // eslint-disable-next-line prefer-rest-params, @typescript-eslint/no-explicit-any - const handler = original.apply(this, arguments as any); - const callbackName = callback.name; - const instanceName = - // todo - // eslint-disable-next-line @typescript-eslint/prefer-optional-chain - instance.constructor && instance.constructor.name ? instance.constructor.name : 'UnnamedInstance'; - const spanName = callbackName ? `${instanceName}.${callbackName}` : instanceName; - - // todo - // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any - return function (this: any, req: any, res: any, next: (...args: any[]) => unknown) { - const span = tracer.startSpan(spanName, { - attributes: { - component: COMPONENT, - [AttributeNames.VERSION]: moduleVersion, - [AttributeNames.TYPE]: NestType.REQUEST_CONTEXT, - // todo - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - [ATTR_HTTP_REQUEST_METHOD]: req.method, - // todo - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, deprecation/deprecation - [SEMATTRS_HTTP_URL]: req.originalUrl || req.url, - // todo - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - [ATTR_HTTP_ROUTE]: req.route?.path || req.routeOptions?.url || req.routerPath, - [AttributeNames.CONTROLLER]: instanceName, - [AttributeNames.CALLBACK]: callbackName, - }, - }); - const spanContext = api.trace.setSpan(api.context.active(), span); - - return api.context.with(spanContext, async () => { - try { - // todo - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, prefer-rest-params - return await handler.apply(this, arguments as unknown); - // todo - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (e: any) { - throw addError(span, e); - } finally { - span.end(); - } - }); - }; - }; - }; -} - -function createWrapHandler( - tracer: api.Tracer, - moduleVersion: string | undefined, - // todo - // eslint-disable-next-line @typescript-eslint/ban-types - handler: Function, - // todo - // eslint-disable-next-line @typescript-eslint/no-explicit-any -): (this: RouterExecutionContext) => Promise { - const spanName = handler.name || 'anonymous nest handler'; - const options = { - attributes: { - component: COMPONENT, - [AttributeNames.VERSION]: moduleVersion, - [AttributeNames.TYPE]: NestType.REQUEST_HANDLER, - [AttributeNames.CALLBACK]: handler.name, - }, - }; - // todo - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const wrappedHandler = function (this: RouterExecutionContext): Promise { - const span = tracer.startSpan(spanName, options); - const spanContext = api.trace.setSpan(api.context.active(), span); - - return api.context.with(spanContext, async () => { - try { - // todo - // eslint-disable-next-line prefer-rest-params - return await handler.apply(this, arguments); - // todo - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (e: any) { - throw addError(span, e); - } finally { - span.end(); - } - }); - }; - - if (handler.name) { - Object.defineProperty(wrappedHandler, 'name', { value: handler.name }); - } - - // Get the current metadata and set onto the wrapper to ensure other decorators ( ie: NestJS EventPattern / RolesGuard ) - // won't be affected by the use of this instrumentation - Reflect.getMetadataKeys(handler).forEach(metadataKey => { - Reflect.defineMetadata(metadataKey, Reflect.getMetadata(metadataKey, handler), wrappedHandler); - }); - return wrappedHandler; -} - -const addError = (span: api.Span, error: Error): Error => { - span.recordException(error); - span.setStatus({ code: api.SpanStatusCode.ERROR, message: error.message }); - return error; -}; From e1c0f195d7d3b0608d715dbd9e9d588b79c37612 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Thu, 17 Jul 2025 20:24:19 +0200 Subject: [PATCH 2/4] Add `reflect-metadata` imports --- packages/nestjs/src/decorators.ts | 1 + .../nestjs/src/integrations/sentry-nest-event-instrumentation.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/nestjs/src/decorators.ts b/packages/nestjs/src/decorators.ts index 9ac7315dabd8..0575707f4340 100644 --- a/packages/nestjs/src/decorators.ts +++ b/packages/nestjs/src/decorators.ts @@ -1,3 +1,4 @@ +import 'reflect-metadata'; import type { MonitorConfig } from '@sentry/core'; import { captureException, isThenable } from '@sentry/core'; import * as Sentry from '@sentry/node'; diff --git a/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts b/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts index 85eaae360b1c..4b747b282753 100644 --- a/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts +++ b/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts @@ -1,3 +1,4 @@ +import 'reflect-metadata'; import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; import { InstrumentationBase, From 926a31c128b40a6df901c44e78f8fc991cc1ea55 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Thu, 17 Jul 2025 22:05:48 +0200 Subject: [PATCH 3/4] Add type ignores for Reflect instead --- packages/nestjs/src/decorators.ts | 9 ++++++++- .../integrations/sentry-nest-event-instrumentation.ts | 5 ++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/nestjs/src/decorators.ts b/packages/nestjs/src/decorators.ts index 0575707f4340..7ac4941be877 100644 --- a/packages/nestjs/src/decorators.ts +++ b/packages/nestjs/src/decorators.ts @@ -1,4 +1,3 @@ -import 'reflect-metadata'; import type { MonitorConfig } from '@sentry/core'; import { captureException, isThenable } from '@sentry/core'; import * as Sentry from '@sentry/node'; @@ -111,10 +110,18 @@ function copyFunctionNameAndMetadata({ }); // copy metadata + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - reflect-metadata of nestjs adds these methods to Reflect if (typeof Reflect !== 'undefined' && typeof Reflect.getMetadataKeys === 'function') { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - reflect-metadata of nestjs adds these methods to Reflect const originalMetaData = Reflect.getMetadataKeys(originalMethod); for (const key of originalMetaData) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - reflect-metadata of nestjs adds these methods to Reflect const value = Reflect.getMetadata(key, originalMethod); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - reflect-metadata of nestjs adds these methods to Reflect Reflect.defineMetadata(key, value, descriptor.value); } } diff --git a/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts b/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts index 4b747b282753..a572bb93a52f 100644 --- a/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts +++ b/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts @@ -1,4 +1,3 @@ -import 'reflect-metadata'; import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; import { InstrumentationBase, @@ -84,7 +83,11 @@ export class SentryNestEventInstrumentation extends InstrumentationBase { descriptor.value = async function (...args: unknown[]) { // When multiple @OnEvent decorators are used on a single method, we need to get all event names // from the reflector metadata as there is no information during execution which event triggered it + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - reflect-metadata of nestjs adds these methods to Reflect if (Reflect.getMetadataKeys(descriptor.value).includes('EVENT_LISTENER_METADATA')) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - reflect-metadata of nestjs adds these methods to Reflect const eventData = Reflect.getMetadata('EVENT_LISTENER_METADATA', descriptor.value); if (Array.isArray(eventData)) { eventName = eventData From b84e85fb1172daa9aec1e53de6888ee42d1e47d4 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 18 Jul 2025 11:13:20 +0200 Subject: [PATCH 4/4] Fix e2e fastify test --- .../test-applications/nestjs-fastify/tests/transactions.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts index 2b8c555d7322..ac4c8bdea83e 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts @@ -97,7 +97,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { component: '@nestjs/core', 'nestjs.version': expect.any(String), 'nestjs.type': 'request_context', - 'http.request.method': 'GET', + 'http.method': 'GET', 'http.url': '/test-transaction', 'http.route': '/test-transaction', 'nestjs.controller': 'AppController',