diff --git a/packages/node/src/integrations/tracing/koa.ts b/packages/node/src/integrations/tracing/koa.ts index e2a2a52264ae..43b901afebee 100644 --- a/packages/node/src/integrations/tracing/koa.ts +++ b/packages/node/src/integrations/tracing/koa.ts @@ -1,6 +1,7 @@ +import type { KoaInstrumentationConfig, KoaLayerType } from '@opentelemetry/instrumentation-koa'; import { KoaInstrumentation } from '@opentelemetry/instrumentation-koa'; import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions'; -import type { IntegrationFn, Span } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/core'; import { captureException, defineIntegration, @@ -8,27 +9,51 @@ import { getIsolationScope, logger, SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, } from '@sentry/core'; import { DEBUG_BUILD } from '../../debug-build'; import { generateInstrumentOnce } from '../../otel/instrument'; +import { addOriginToSpan } from '../../utils/addOriginToSpan'; import { ensureIsWrapped } from '../../utils/ensureIsWrapped'; +interface KoaOptions { + /** + * Ignore layers of specified types + */ + ignoreLayersType?: Array<'middleware' | 'router'>; +} + const INTEGRATION_NAME = 'Koa'; export const instrumentKoa = generateInstrumentOnce( INTEGRATION_NAME, - () => - new KoaInstrumentation({ + KoaInstrumentation, + (options: KoaOptions = {}) => { + return { + ignoreLayersType: options.ignoreLayersType as KoaLayerType[], requestHook(span, info) { - addKoaSpanAttributes(span); + addOriginToSpan(span, 'auto.http.otel.koa'); + + const attributes = spanToJSON(span).data; + + // this is one of: middleware, router + const type = attributes['koa.type']; + if (type) { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, `${type}.koa`); + } + + // Also update the name + const name = attributes['koa.name']; + if (typeof name === 'string') { + // Somehow, name is sometimes `''` for middleware spans + // See: https://github.com/open-telemetry/opentelemetry-js-contrib/issues/2220 + span.updateName(name || '< unknown >'); + } if (getIsolationScope() === getDefaultIsolationScope()) { DEBUG_BUILD && logger.warn('Isolation scope is default isolation scope - skipping setting transactionName'); return; } - const attributes = spanToJSON(span).data; const route = attributes[ATTR_HTTP_ROUTE]; // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const method = info.context?.request?.method?.toUpperCase() || 'GET'; @@ -36,14 +61,15 @@ export const instrumentKoa = generateInstrumentOnce( getIsolationScope().setTransactionName(`${method} ${route}`); } }, - }), + } satisfies KoaInstrumentationConfig; + }, ); -const _koaIntegration = (() => { +const _koaIntegration = ((options: KoaOptions = {}) => { return { name: INTEGRATION_NAME, setupOnce() { - instrumentKoa(); + instrumentKoa(options); }, }; }) satisfies IntegrationFn; @@ -55,6 +81,8 @@ const _koaIntegration = (() => { * * For more information, see the [koa documentation](https://docs.sentry.io/platforms/javascript/guides/koa/). * + * @param {KoaOptions} options Configuration options for the Koa integration. + * * @example * ```javascript * const Sentry = require('@sentry/node'); @@ -63,6 +91,20 @@ const _koaIntegration = (() => { * integrations: [Sentry.koaIntegration()], * }) * ``` + * + * @example + * ```javascript + * // To ignore middleware spans + * const Sentry = require('@sentry/node'); + * + * Sentry.init({ + * integrations: [ + * Sentry.koaIntegration({ + * ignoreLayersType: ['middleware'] + * }) + * ], + * }) + * ``` */ export const koaIntegration = defineIntegration(_koaIntegration); @@ -101,24 +143,3 @@ export const setupKoaErrorHandler = (app: { use: (arg0: (ctx: any, next: any) => ensureIsWrapped(app.use, 'koa'); }; - -function addKoaSpanAttributes(span: Span): void { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.http.otel.koa'); - - const attributes = spanToJSON(span).data; - - // this is one of: middleware, router - const type = attributes['koa.type']; - - if (type) { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, `${type}.koa`); - } - - // Also update the name - const name = attributes['koa.name']; - if (typeof name === 'string') { - // Somehow, name is sometimes `''` for middleware spans - // See: https://github.com/open-telemetry/opentelemetry-js-contrib/issues/2220 - span.updateName(name || '< unknown >'); - } -} diff --git a/packages/node/test/integrations/tracing/koa.test.ts b/packages/node/test/integrations/tracing/koa.test.ts new file mode 100644 index 000000000000..9ca221dfba03 --- /dev/null +++ b/packages/node/test/integrations/tracing/koa.test.ts @@ -0,0 +1,83 @@ +import { KoaInstrumentation } from '@opentelemetry/instrumentation-koa'; +import { type MockInstance, beforeEach, describe, expect, it, vi } from 'vitest'; +import { instrumentKoa, koaIntegration } from '../../../src/integrations/tracing/koa'; +import { INSTRUMENTED } from '../../../src/otel/instrument'; + +vi.mock('@opentelemetry/instrumentation-koa'); + +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, + requestHook: expect.any(Function), + }); + }); + + it('passes ignoreLayersType option to instrumentation', () => { + instrumentKoa({ ignoreLayersType: ['middleware'] }); + + expect(KoaInstrumentation).toHaveBeenCalledTimes(1); + expect(KoaInstrumentation).toHaveBeenCalledWith({ + ignoreLayersType: ['middleware'], + requestHook: expect.any(Function), + }); + }); + + it('passes multiple ignoreLayersType values to instrumentation', () => { + instrumentKoa({ ignoreLayersType: ['middleware', 'router'] }); + + expect(KoaInstrumentation).toHaveBeenCalledTimes(1); + expect(KoaInstrumentation).toHaveBeenCalledWith({ + ignoreLayersType: ['middleware', 'router'], + requestHook: expect.any(Function), + }); + }); + + it('defaults are correct for koaIntegration', () => { + koaIntegration().setupOnce!(); + + expect(KoaInstrumentation).toHaveBeenCalledTimes(1); + expect(KoaInstrumentation).toHaveBeenCalledWith({ + ignoreLayersType: undefined, + requestHook: expect.any(Function), + }); + }); + + it('passes options from koaIntegration to instrumentation', () => { + koaIntegration({ ignoreLayersType: ['middleware'] }).setupOnce!(); + + expect(KoaInstrumentation).toHaveBeenCalledTimes(1); + expect(KoaInstrumentation).toHaveBeenCalledWith({ + ignoreLayersType: ['middleware'], + requestHook: expect.any(Function), + }); + }); + + it('passes multiple options from koaIntegration to instrumentation', () => { + koaIntegration({ ignoreLayersType: ['router', 'middleware'] }).setupOnce!(); + + expect(KoaInstrumentation).toHaveBeenCalledTimes(1); + expect(KoaInstrumentation).toHaveBeenCalledWith({ + ignoreLayersType: ['router', 'middleware'], + requestHook: expect.any(Function), + }); + }); +});