From decae48f1a98341ec3272a790186b08031f71490 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 28 Jul 2026 12:16:55 +0200 Subject: [PATCH] ref(node): Remove vendored Koa instrumentation Removes the dead OpenTelemetry `KoaInstrumentation` from `@sentry/node`. The Koa integration is provided by the channel-based implementation in `@sentry/server-utils` (default since #22501); only the Sentry-specific `setupKoaErrorHandler` is kept here. Ref: #22346 Co-Authored-By: Claude Opus 4.8 --- .../src/integrations/tracing/koa/index.ts | 22 --- .../koa/vendored/enums/AttributeNames.ts | 13 -- .../tracing/koa/vendored/instrumentation.ts | 186 ------------------ .../tracing/koa/vendored/internal-types.ts | 53 ----- .../tracing/koa/vendored/types.ts | 23 --- .../tracing/koa/vendored/utils.ts | 54 ----- .../test/integrations/tracing/koa.test.ts | 76 ------- 7 files changed, 427 deletions(-) delete mode 100644 packages/node/src/integrations/tracing/koa/vendored/enums/AttributeNames.ts delete mode 100644 packages/node/src/integrations/tracing/koa/vendored/instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/koa/vendored/internal-types.ts delete mode 100644 packages/node/src/integrations/tracing/koa/vendored/types.ts delete mode 100644 packages/node/src/integrations/tracing/koa/vendored/utils.ts delete mode 100644 packages/node/test/integrations/tracing/koa.test.ts diff --git a/packages/node/src/integrations/tracing/koa/index.ts b/packages/node/src/integrations/tracing/koa/index.ts index 0cb1671bd8c2..7c467df9b6f8 100644 --- a/packages/node/src/integrations/tracing/koa/index.ts +++ b/packages/node/src/integrations/tracing/koa/index.ts @@ -1,28 +1,6 @@ -import type { KoaInstrumentationConfig, KoaLayerType } from './vendored/types'; -import { KoaInstrumentation } from './vendored/instrumentation'; import { captureException } from '@sentry/core'; -import { generateInstrumentOnce } from '../../../otel/instrument'; import { ensureIsWrapped } from '../../../utils/ensureIsWrapped'; -interface KoaOptions { - /** - * Ignore layers of specified types - */ - ignoreLayersType?: Array<'middleware' | 'router'>; -} - -const INTEGRATION_NAME = 'Koa' as const; - -export const instrumentKoa = generateInstrumentOnce( - INTEGRATION_NAME, - KoaInstrumentation, - (options: KoaOptions = {}) => { - return { - ignoreLayersType: options.ignoreLayersType as KoaLayerType[], - } satisfies KoaInstrumentationConfig; - }, -); - /** * Add an Koa error handler to capture errors to Sentry. * diff --git a/packages/node/src/integrations/tracing/koa/vendored/enums/AttributeNames.ts b/packages/node/src/integrations/tracing/koa/vendored/enums/AttributeNames.ts deleted file mode 100644 index eb986fa5aa03..000000000000 --- a/packages/node/src/integrations/tracing/koa/vendored/enums/AttributeNames.ts +++ /dev/null @@ -1,13 +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-koa - * - Upstream version: @opentelemetry/instrumentation-koa@0.66.0 - */ - -export enum AttributeNames { - KOA_TYPE = 'koa.type', - KOA_NAME = 'koa.name', -} diff --git a/packages/node/src/integrations/tracing/koa/vendored/instrumentation.ts b/packages/node/src/integrations/tracing/koa/vendored/instrumentation.ts deleted file mode 100644 index 012d3ad70599..000000000000 --- a/packages/node/src/integrations/tracing/koa/vendored/instrumentation.ts +++ /dev/null @@ -1,186 +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-koa - * - Upstream version: @opentelemetry/instrumentation-koa@0.66.0 - * - Minor TypeScript strictness adjustments for this repository's compiler settings - * - Span creation migrated to the @sentry/core API; op/origin/name and transaction name folded into - * span creation (previously set via a Sentry requestHook) - */ - -import * as api from '@opentelemetry/api'; -import { isWrapped, InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'; - -import { KoaLayerType, type KoaInstrumentationConfig } from './types'; -import { - debug, - getDefaultIsolationScope, - getIsolationScope, - SDK_VERSION, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - startSpan, -} from '@sentry/core'; -import { HTTP_ROUTE } from '@sentry/conventions/attributes'; -import { getMiddlewareMetadata, isLayerIgnored } from './utils'; -import { setHttpServerSpanRouteAttribute } from '../../../../utils/setHttpServerSpanRouteAttribute'; -import { DEBUG_BUILD } from '../../../../debug-build'; -import { AttributeNames } from './enums/AttributeNames'; -import { - kLayerPatched, - type Next, - type KoaContext, - type KoaMiddleware, - type KoaPatchedMiddleware, -} from './internal-types'; - -const PACKAGE_NAME = '@sentry/instrumentation-koa'; - -interface KoaModuleExports { - prototype: { use: KoaMiddleware }; -} - -type KoaModule = KoaModuleExports & { [Symbol.toStringTag]?: string; default?: KoaModuleExports }; - -/** Koa instrumentation for OpenTelemetry */ -export class KoaInstrumentation extends InstrumentationBase { - constructor(config: KoaInstrumentationConfig = {}) { - super(PACKAGE_NAME, SDK_VERSION, config); - } - - protected init() { - return new InstrumentationNodeModuleDefinition( - 'koa', - ['>=2.0.0 <4'], - (module: KoaModule) => { - const moduleExports = - module[Symbol.toStringTag] === 'Module' - ? module.default // ESM - : module; // CommonJS - if (moduleExports == null) { - return moduleExports; - } - if (isWrapped(moduleExports.prototype.use)) { - this._unwrap(moduleExports.prototype, 'use'); - } - this._wrap(moduleExports.prototype, 'use', this._getKoaUsePatch.bind(this)); - return module; - }, - (module: KoaModule) => { - const moduleExports = - module[Symbol.toStringTag] === 'Module' - ? module.default // ESM - : module; // CommonJS - if (moduleExports && isWrapped(moduleExports.prototype.use)) { - this._unwrap(moduleExports.prototype, 'use'); - } - }, - ); - } - - /** - * Patches the Koa.use function in order to instrument each original - * middleware layer which is introduced - * @param {KoaMiddleware} middleware - the original middleware function - */ - private _getKoaUsePatch(original: (middleware: KoaMiddleware) => unknown) { - const patchRouterDispatch = this._patchRouterDispatch.bind(this); - const patchLayer = this._patchLayer.bind(this); - return function use(this: unknown, middlewareFunction: KoaMiddleware) { - const patchedFunction = middlewareFunction.router - ? patchRouterDispatch(middlewareFunction) - : patchLayer(middlewareFunction, false); - return original.apply(this, [patchedFunction]); - }; - } - - /** - * Patches the dispatch function used by @koa/router. This function - * goes through each routed middleware and adds instrumentation via a call - * to the @function _patchLayer function. - * @param {KoaMiddleware} dispatchLayer - the original dispatch function which dispatches - * routed middleware - */ - private _patchRouterDispatch(dispatchLayer: KoaMiddleware): KoaMiddleware { - const router = dispatchLayer.router; - - const routesStack = router?.stack ?? []; - for (const pathLayer of routesStack) { - const path = pathLayer.path; - const pathStack = pathLayer.stack; - for (let j = 0; j < pathStack.length; j++) { - const routedMiddleware: KoaMiddleware = pathStack[j]!; - pathStack[j] = this._patchLayer(routedMiddleware, true, path); - } - } - - return dispatchLayer; - } - - /** - * Patches each individual @param middlewareLayer function in order to create the - * span and propagate context. It does not create spans when there is no parent span. - * @param {KoaMiddleware} middlewareLayer - the original middleware function. - * @param {boolean} isRouter - tracks whether the original middleware function - * was dispatched by the router originally - * @param {string?} layerPath - if present, provides additional data from the - * router about the routed path which the middleware is attached to - */ - private _patchLayer( - middlewareLayer: KoaPatchedMiddleware, - isRouter: boolean, - layerPath?: string | RegExp, - ): KoaMiddleware { - const layerType = isRouter ? KoaLayerType.ROUTER : KoaLayerType.MIDDLEWARE; - // Skip patching layer if its ignored in the config - if (middlewareLayer[kLayerPatched] === true || isLayerIgnored(layerType, this.getConfig())) return middlewareLayer; - - if ( - middlewareLayer.constructor.name === 'GeneratorFunction' || - middlewareLayer.constructor.name === 'AsyncGeneratorFunction' - ) { - return middlewareLayer; - } - - middlewareLayer[kLayerPatched] = true; - - return (context: KoaContext, next: Next) => { - const parent = api.trace.getSpan(api.context.active()); - if (parent === undefined) { - return middlewareLayer(context, next); - } - const metadata = getMiddlewareMetadata(context, middlewareLayer, isRouter, layerPath); - - if (context._matchedRoute) { - setHttpServerSpanRouteAttribute(context._matchedRoute.toString()); - } - - const koaName = metadata.attributes[AttributeNames.KOA_NAME]; - // Somehow, name is sometimes `''` for middleware spans - // See: https://github.com/open-telemetry/opentelemetry-js-contrib/issues/2220 - const name = typeof koaName === 'string' ? koaName || '< unknown >' : metadata.name; - - return startSpan( - { - name, - op: `${layerType}.koa`, - attributes: { - ...metadata.attributes, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.koa', - }, - }, - () => { - const route = metadata.attributes[HTTP_ROUTE]; - if (getIsolationScope() === getDefaultIsolationScope()) { - DEBUG_BUILD && debug.warn('Isolation scope is default isolation scope - skipping setting transactionName'); - } else if (route) { - const method = (context.request as { method?: string } | undefined)?.method?.toUpperCase() || 'GET'; - getIsolationScope().setTransactionName(`${method} ${route}`); - } - return middlewareLayer(context, next); - }, - ); - }; - } -} diff --git a/packages/node/src/integrations/tracing/koa/vendored/internal-types.ts b/packages/node/src/integrations/tracing/koa/vendored/internal-types.ts deleted file mode 100644 index 583f5efb8c0f..000000000000 --- a/packages/node/src/integrations/tracing/koa/vendored/internal-types.ts +++ /dev/null @@ -1,53 +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-koa - * - Upstream version: @opentelemetry/instrumentation-koa@0.66.0 - * - Some types vendored from @types/koa, @types/koa-compose, and @types/koa__router with simplifications - */ - -interface DefaultState {} - -export type Next = () => Promise; - -type ParameterizedContext<_StateT = DefaultState, ContextT = {}, _ResponseBodyT = unknown> = { - [key: string]: unknown; -} & ContextT; - -type Middleware = ( - context: ParameterizedContext, - next: Next, -) => unknown; - -interface RouterParamContext { - params: Record; - router: Router; - _matchedRoute: string | RegExp | undefined; - _matchedRouteName: string | undefined; -} - -interface Layer { - path: string | RegExp; - stack: KoaMiddleware[]; -} - -export interface Router<_StateT = DefaultState, _ContextT = {}> { - stack: Layer[]; -} - -export type KoaContext = ParameterizedContext; -export type KoaMiddleware = Middleware & { - router?: Router; -}; - -/** - * This symbol is used to mark a Koa layer as being already instrumented - * since its possible to use a given layer multiple times (ex: middlewares) - */ -export const kLayerPatched: unique symbol = Symbol('koa-layer-patched'); - -export type KoaPatchedMiddleware = KoaMiddleware & { - [kLayerPatched]?: boolean; -}; diff --git a/packages/node/src/integrations/tracing/koa/vendored/types.ts b/packages/node/src/integrations/tracing/koa/vendored/types.ts deleted file mode 100644 index 0565c1d4ce2c..000000000000 --- a/packages/node/src/integrations/tracing/koa/vendored/types.ts +++ /dev/null @@ -1,23 +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-koa - * - Upstream version: @opentelemetry/instrumentation-koa@0.66.0 - */ - -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; - -export enum KoaLayerType { - ROUTER = 'router', - MIDDLEWARE = 'middleware', -} - -/** - * Options available for the Koa Instrumentation (see [documentation](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-Instrumentation-koa#koa-Instrumentation-options)) - */ -export interface KoaInstrumentationConfig extends InstrumentationConfig { - /** Ignore specific layers based on their type */ - ignoreLayersType?: KoaLayerType[]; -} diff --git a/packages/node/src/integrations/tracing/koa/vendored/utils.ts b/packages/node/src/integrations/tracing/koa/vendored/utils.ts deleted file mode 100644 index 070aa7eb965d..000000000000 --- a/packages/node/src/integrations/tracing/koa/vendored/utils.ts +++ /dev/null @@ -1,54 +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-koa - * - Upstream version: @opentelemetry/instrumentation-koa@0.66.0 - */ - -import { KoaLayerType, type KoaInstrumentationConfig } from './types'; -import type { KoaContext, KoaMiddleware } from './internal-types'; -import { AttributeNames } from './enums/AttributeNames'; -import type { Attributes } from '@opentelemetry/api'; -import { CODE_FUNCTION_NAME, HTTP_ROUTE } from '@sentry/conventions/attributes'; - -export const getMiddlewareMetadata = ( - context: KoaContext, - layer: KoaMiddleware, - isRouter: boolean, - layerPath?: string | RegExp, -): { - attributes: Attributes; - name: string; -} => { - if (isRouter) { - return { - attributes: { - [AttributeNames.KOA_NAME]: layerPath?.toString(), // TODO(v11): remove, replaced by http.route - [AttributeNames.KOA_TYPE]: KoaLayerType.ROUTER, - [HTTP_ROUTE]: layerPath?.toString(), - }, - name: context._matchedRouteName || `router - ${layerPath}`, - }; - } else { - return { - attributes: { - [AttributeNames.KOA_NAME]: layer.name ?? 'middleware', // TODO(v11): remove, replaced by code.function.name - [AttributeNames.KOA_TYPE]: KoaLayerType.MIDDLEWARE, - [CODE_FUNCTION_NAME]: layer.name ?? 'middleware', - }, - name: `middleware - ${layer.name}`, - }; - } -}; - -/** - * Check whether the given request is ignored by configuration - * @param [list] List of ignore patterns - * @param [onException] callback for doing something when an exception has - * occurred - */ -export const isLayerIgnored = (type: KoaLayerType, config?: KoaInstrumentationConfig): boolean => { - return !!(Array.isArray(config?.ignoreLayersType) && config?.ignoreLayersType?.includes(type)); -}; diff --git a/packages/node/test/integrations/tracing/koa.test.ts b/packages/node/test/integrations/tracing/koa.test.ts deleted file mode 100644 index 067aa9322383..000000000000 --- a/packages/node/test/integrations/tracing/koa.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { beforeEach, describe, expect, it, type MockInstance, vi } from 'vitest'; -import { KoaInstrumentation } from '../../../src/integrations/tracing/koa/vendored/instrumentation'; -import { INSTRUMENTED } from '../../../src/otel/instrument'; -import { koaIntegration } from '@sentry/server-utils/orchestrion'; -import { instrumentKoa } from '../../../src/integrations/tracing/koa'; -import { isLayerIgnored } from '../../../src/integrations/tracing/koa/vendored/utils'; -import { KoaLayerType, type KoaInstrumentationConfig } from '../../../src/integrations/tracing/koa/vendored/types'; - -vi.mock('../../../src/integrations/tracing/koa/vendored/instrumentation'); - -describe('Koa', () => { - beforeEach(() => { - vi.clearAllMocks(); - delete INSTRUMENTED.Koa; - - (KoaInstrumentation as unknown as MockInstance).mockImplementation(() => { - return { - setTracerProvider: () => undefined, - setMeterProvider: () => undefined, - getConfig: () => ({}), - setConfig: () => ({}), - enable: () => undefined, - }; - }); - }); - - it('defaults are correct for instrumentKoa', () => { - instrumentKoa({}); - - expect(KoaInstrumentation).toHaveBeenCalledTimes(1); - expect(KoaInstrumentation).toHaveBeenCalledWith({ - ignoreLayersType: undefined, - }); - }); - - it('passes ignoreLayersType option to instrumentation', () => { - instrumentKoa({ ignoreLayersType: ['middleware'] }); - - expect(KoaInstrumentation).toHaveBeenCalledTimes(1); - expect(KoaInstrumentation).toHaveBeenCalledWith({ - ignoreLayersType: ['middleware'], - }); - }); - - it('passes multiple ignoreLayersType values to instrumentation', () => { - instrumentKoa({ ignoreLayersType: ['middleware', 'router'] }); - - expect(KoaInstrumentation).toHaveBeenCalledTimes(1); - expect(KoaInstrumentation).toHaveBeenCalledWith({ - ignoreLayersType: ['middleware', 'router'], - }); - }); - - // `koaIntegration()` is now the channel-based (orchestrion) integration by default; it no longer - // sets up the vendored OTel `KoaInstrumentation`. The channel subscriber's span behavior is covered - // in `@sentry/server-utils` and the node-integration koa suite. Here we only assert the public - // factory keeps the `Koa` name so the default-integration set and user overrides stay aligned. - it('koaIntegration is the channel integration with the Koa name', () => { - expect(koaIntegration().name).toBe('Koa'); - expect(koaIntegration({ ignoreLayersType: ['middleware'] }).name).toBe('Koa'); - }); -}); - -describe('isLayerIgnored', () => { - it('does not fail with invalid config', () => { - expect(isLayerIgnored(KoaLayerType.MIDDLEWARE)).toBe(false); - expect(isLayerIgnored(KoaLayerType.MIDDLEWARE, {} as KoaInstrumentationConfig)).toBe(false); - expect(isLayerIgnored(KoaLayerType.MIDDLEWARE, { ignoreLayersType: {} } as KoaInstrumentationConfig)).toBe(false); - expect(isLayerIgnored(KoaLayerType.ROUTER, { ignoreLayersType: {} } as KoaInstrumentationConfig)).toBe(false); - }); - - it('ignores based on type', () => { - expect(isLayerIgnored(KoaLayerType.MIDDLEWARE, { ignoreLayersType: [KoaLayerType.MIDDLEWARE] })).toBe(true); - expect(isLayerIgnored(KoaLayerType.ROUTER, { ignoreLayersType: [KoaLayerType.MIDDLEWARE] })).toBe(false); - }); -});