From 20d85e78ec311139130127d11d31fb1d49a0c177 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Jun 2025 14:41:42 +0000 Subject: [PATCH 1/3] Add Koa integration options to ignore specific layer types --- packages/node/src/integrations/tracing/koa.ts | 49 +++++++++-- .../test/integrations/tracing/koa.test.ts | 83 +++++++++++++++++++ 2 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 packages/node/test/integrations/tracing/koa.test.ts diff --git a/packages/node/src/integrations/tracing/koa.ts b/packages/node/src/integrations/tracing/koa.ts index e2a2a52264ae..423b71578d6b 100644 --- a/packages/node/src/integrations/tracing/koa.ts +++ b/packages/node/src/integrations/tracing/koa.ts @@ -14,15 +14,35 @@ import { import { DEBUG_BUILD } from '../../debug-build'; import { generateInstrumentOnce } from '../../otel/instrument'; import { ensureIsWrapped } from '../../utils/ensureIsWrapped'; +import { addOriginToSpan } from '../../utils/addOriginToSpan'; + +/** + * Koa layer types for ignoreLayersType option + */ +export type KoaLayerType = 'router' | 'middleware'; + +interface KoaOptions { + /** + * Ignore layers of specified types. + * + * @example ['middleware'] - Ignore all middleware layers + * @example ['router'] - Ignore all router layers + * @example ['middleware', 'router'] - Ignore both middleware and router layers + */ + ignoreLayersType?: KoaLayerType[]; +} const INTEGRATION_NAME = 'Koa'; export const instrumentKoa = generateInstrumentOnce( INTEGRATION_NAME, - () => - new KoaInstrumentation({ + KoaInstrumentation, + (options: KoaOptions = {}) => { + return { + ignoreLayersType: options.ignoreLayersType as any, requestHook(span, info) { addKoaSpanAttributes(span); + addOriginToSpan(span, 'auto.http.otel.koa'); if (getIsolationScope() === getDefaultIsolationScope()) { DEBUG_BUILD && logger.warn('Isolation scope is default isolation scope - skipping setting transactionName'); @@ -36,14 +56,15 @@ export const instrumentKoa = generateInstrumentOnce( getIsolationScope().setTransactionName(`${method} ${route}`); } }, - }), + }; + }, ); -const _koaIntegration = (() => { +const _koaIntegration = ((options: KoaOptions = {}) => { return { name: INTEGRATION_NAME, setupOnce() { - instrumentKoa(); + instrumentKoa(options); }, }; }) satisfies IntegrationFn; @@ -55,6 +76,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 +86,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); @@ -103,8 +140,6 @@ export const setupKoaErrorHandler = (app: { use: (arg0: (ctx: any, next: any) => }; 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 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..f7b0924dc216 --- /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 { koaIntegration, instrumentKoa } 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), + }); + }); +}); From 0e904d6108a7d94fead915d07cd0ed8b8cfa4e0c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Jun 2025 14:47:33 +0000 Subject: [PATCH 2/3] Refactor Koa instrumentation with improved type safety and exports --- packages/node/src/integrations/tracing/koa.ts | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/node/src/integrations/tracing/koa.ts b/packages/node/src/integrations/tracing/koa.ts index 423b71578d6b..d8c65b2cd3a7 100644 --- a/packages/node/src/integrations/tracing/koa.ts +++ b/packages/node/src/integrations/tracing/koa.ts @@ -1,4 +1,5 @@ import { KoaInstrumentation } from '@opentelemetry/instrumentation-koa'; +import type { KoaLayerType, KoaInstrumentationConfig } from '@opentelemetry/instrumentation-koa'; import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions'; import type { IntegrationFn, Span } from '@sentry/core'; import { @@ -16,18 +17,9 @@ import { generateInstrumentOnce } from '../../otel/instrument'; import { ensureIsWrapped } from '../../utils/ensureIsWrapped'; import { addOriginToSpan } from '../../utils/addOriginToSpan'; -/** - * Koa layer types for ignoreLayersType option - */ -export type KoaLayerType = 'router' | 'middleware'; - interface KoaOptions { /** - * Ignore layers of specified types. - * - * @example ['middleware'] - Ignore all middleware layers - * @example ['router'] - Ignore all router layers - * @example ['middleware', 'router'] - Ignore both middleware and router layers + * Ignore layers of specified types */ ignoreLayersType?: KoaLayerType[]; } @@ -39,7 +31,7 @@ export const instrumentKoa = generateInstrumentOnce( KoaInstrumentation, (options: KoaOptions = {}) => { return { - ignoreLayersType: options.ignoreLayersType as any, + ignoreLayersType: options.ignoreLayersType, requestHook(span, info) { addKoaSpanAttributes(span); addOriginToSpan(span, 'auto.http.otel.koa'); @@ -51,12 +43,13 @@ export const instrumentKoa = generateInstrumentOnce( 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'; - if (route) { + const method = info.context?.request?.method?.toUpperCase?.(); + + if (typeof route === 'string' && typeof method === 'string') { getIsolationScope().setTransactionName(`${method} ${route}`); } }, - }; + } satisfies KoaInstrumentationConfig; }, ); @@ -157,3 +150,6 @@ function addKoaSpanAttributes(span: Span): void { span.updateName(name || '< unknown >'); } } + +// Re-export the KoaLayerType for users +export type { KoaLayerType }; From 9ba5f84adc166fbf3a7ba94648994534e636dcd5 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 11 Jun 2025 10:52:55 -0400 Subject: [PATCH 3/3] cursor cleanup --- packages/node/src/integrations/tracing/koa.ts | 56 ++++++++----------- .../test/integrations/tracing/koa.test.ts | 2 +- 2 files changed, 24 insertions(+), 34 deletions(-) diff --git a/packages/node/src/integrations/tracing/koa.ts b/packages/node/src/integrations/tracing/koa.ts index d8c65b2cd3a7..43b901afebee 100644 --- a/packages/node/src/integrations/tracing/koa.ts +++ b/packages/node/src/integrations/tracing/koa.ts @@ -1,7 +1,7 @@ +import type { KoaInstrumentationConfig, KoaLayerType } from '@opentelemetry/instrumentation-koa'; import { KoaInstrumentation } from '@opentelemetry/instrumentation-koa'; -import type { KoaLayerType, KoaInstrumentationConfig } 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, @@ -9,19 +9,18 @@ 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 { ensureIsWrapped } from '../../utils/ensureIsWrapped'; import { addOriginToSpan } from '../../utils/addOriginToSpan'; +import { ensureIsWrapped } from '../../utils/ensureIsWrapped'; interface KoaOptions { /** * Ignore layers of specified types */ - ignoreLayersType?: KoaLayerType[]; + ignoreLayersType?: Array<'middleware' | 'router'>; } const INTEGRATION_NAME = 'Koa'; @@ -31,21 +30,34 @@ export const instrumentKoa = generateInstrumentOnce( KoaInstrumentation, (options: KoaOptions = {}) => { return { - ignoreLayersType: options.ignoreLayersType, + 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?.(); - - if (typeof route === 'string' && typeof method === 'string') { + const method = info.context?.request?.method?.toUpperCase() || 'GET'; + if (route) { getIsolationScope().setTransactionName(`${method} ${route}`); } }, @@ -131,25 +143,3 @@ export const setupKoaErrorHandler = (app: { use: (arg0: (ctx: any, next: any) => ensureIsWrapped(app.use, 'koa'); }; - -function addKoaSpanAttributes(span: Span): void { - 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 >'); - } -} - -// Re-export the KoaLayerType for users -export type { KoaLayerType }; diff --git a/packages/node/test/integrations/tracing/koa.test.ts b/packages/node/test/integrations/tracing/koa.test.ts index f7b0924dc216..9ca221dfba03 100644 --- a/packages/node/test/integrations/tracing/koa.test.ts +++ b/packages/node/test/integrations/tracing/koa.test.ts @@ -1,6 +1,6 @@ import { KoaInstrumentation } from '@opentelemetry/instrumentation-koa'; import { type MockInstance, beforeEach, describe, expect, it, vi } from 'vitest'; -import { koaIntegration, instrumentKoa } from '../../../src/integrations/tracing/koa'; +import { instrumentKoa, koaIntegration } from '../../../src/integrations/tracing/koa'; import { INSTRUMENTED } from '../../../src/otel/instrument'; vi.mock('@opentelemetry/instrumentation-koa');