Skip to content

Commit ba3728d

Browse files
feat(node): Introduce ignoreLayersType option to koa integration (#16553)
ref https://linear.app/getsentry/issue/FE-503/investigate-nested-middleware-spans-in-webfx-koa-application The Koa integration in `@sentry/node` was updated to expose the `ignoreLayersType` option from `@opentelemetry/instrumentation-koa`, aligning its configuration with the GraphQL integration. https://www.npmjs.com/package/@opentelemetry/instrumentation-koa <span><div class="markdown-heading"><h3 class="heading-element">Koa Instrumentation Options</h3><a id="user-content-koa-instrumentation-options" class="anchor" aria-label="Permalink: Koa Instrumentation Options" href="https://www.npmjs.com/package/@opentelemetry/instrumentation-koa#koa-instrumentation-options"></a></div></span><span> Options | Type | Example | Description -- | -- | -- | -- ignoreLayersType | KoaLayerType[] | ['middleware'] | Ignore layers of specified type. requestHook | KoaRequestCustomAttributeFunction | (span, info) => {} | Function for adding custom attributes to Koa middleware layers. Receives params: Span, KoaRequestInfo. <p><code>ignoreLayersType</code> accepts an array of <code>KoaLayerType</code> which can take the following string values:</p> <ul> <li> <code>router</code>,</li> <li> <code>middleware</code>.</li> </ul></span> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent eb59604 commit ba3728d

File tree

2 files changed

+134
-30
lines changed

2 files changed

+134
-30
lines changed

packages/node/src/integrations/tracing/koa.ts

Lines changed: 51 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,75 @@
1+
import type { KoaInstrumentationConfig, KoaLayerType } from '@opentelemetry/instrumentation-koa';
12
import { KoaInstrumentation } from '@opentelemetry/instrumentation-koa';
23
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
3-
import type { IntegrationFn, Span } from '@sentry/core';
4+
import type { IntegrationFn } from '@sentry/core';
45
import {
56
captureException,
67
defineIntegration,
78
getDefaultIsolationScope,
89
getIsolationScope,
910
logger,
1011
SEMANTIC_ATTRIBUTE_SENTRY_OP,
11-
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
1212
spanToJSON,
1313
} from '@sentry/core';
1414
import { DEBUG_BUILD } from '../../debug-build';
1515
import { generateInstrumentOnce } from '../../otel/instrument';
16+
import { addOriginToSpan } from '../../utils/addOriginToSpan';
1617
import { ensureIsWrapped } from '../../utils/ensureIsWrapped';
1718

19+
interface KoaOptions {
20+
/**
21+
* Ignore layers of specified types
22+
*/
23+
ignoreLayersType?: Array<'middleware' | 'router'>;
24+
}
25+
1826
const INTEGRATION_NAME = 'Koa';
1927

2028
export const instrumentKoa = generateInstrumentOnce(
2129
INTEGRATION_NAME,
22-
() =>
23-
new KoaInstrumentation({
30+
KoaInstrumentation,
31+
(options: KoaOptions = {}) => {
32+
return {
33+
ignoreLayersType: options.ignoreLayersType as KoaLayerType[],
2434
requestHook(span, info) {
25-
addKoaSpanAttributes(span);
35+
addOriginToSpan(span, 'auto.http.otel.koa');
36+
37+
const attributes = spanToJSON(span).data;
38+
39+
// this is one of: middleware, router
40+
const type = attributes['koa.type'];
41+
if (type) {
42+
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, `${type}.koa`);
43+
}
44+
45+
// Also update the name
46+
const name = attributes['koa.name'];
47+
if (typeof name === 'string') {
48+
// Somehow, name is sometimes `''` for middleware spans
49+
// See: https://github.com/open-telemetry/opentelemetry-js-contrib/issues/2220
50+
span.updateName(name || '< unknown >');
51+
}
2652

2753
if (getIsolationScope() === getDefaultIsolationScope()) {
2854
DEBUG_BUILD && logger.warn('Isolation scope is default isolation scope - skipping setting transactionName');
2955
return;
3056
}
31-
const attributes = spanToJSON(span).data;
3257
const route = attributes[ATTR_HTTP_ROUTE];
3358
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
3459
const method = info.context?.request?.method?.toUpperCase() || 'GET';
3560
if (route) {
3661
getIsolationScope().setTransactionName(`${method} ${route}`);
3762
}
3863
},
39-
}),
64+
} satisfies KoaInstrumentationConfig;
65+
},
4066
);
4167

42-
const _koaIntegration = (() => {
68+
const _koaIntegration = ((options: KoaOptions = {}) => {
4369
return {
4470
name: INTEGRATION_NAME,
4571
setupOnce() {
46-
instrumentKoa();
72+
instrumentKoa(options);
4773
},
4874
};
4975
}) satisfies IntegrationFn;
@@ -55,6 +81,8 @@ const _koaIntegration = (() => {
5581
*
5682
* For more information, see the [koa documentation](https://docs.sentry.io/platforms/javascript/guides/koa/).
5783
*
84+
* @param {KoaOptions} options Configuration options for the Koa integration.
85+
*
5886
* @example
5987
* ```javascript
6088
* const Sentry = require('@sentry/node');
@@ -63,6 +91,20 @@ const _koaIntegration = (() => {
6391
* integrations: [Sentry.koaIntegration()],
6492
* })
6593
* ```
94+
*
95+
* @example
96+
* ```javascript
97+
* // To ignore middleware spans
98+
* const Sentry = require('@sentry/node');
99+
*
100+
* Sentry.init({
101+
* integrations: [
102+
* Sentry.koaIntegration({
103+
* ignoreLayersType: ['middleware']
104+
* })
105+
* ],
106+
* })
107+
* ```
66108
*/
67109
export const koaIntegration = defineIntegration(_koaIntegration);
68110

@@ -101,24 +143,3 @@ export const setupKoaErrorHandler = (app: { use: (arg0: (ctx: any, next: any) =>
101143

102144
ensureIsWrapped(app.use, 'koa');
103145
};
104-
105-
function addKoaSpanAttributes(span: Span): void {
106-
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.http.otel.koa');
107-
108-
const attributes = spanToJSON(span).data;
109-
110-
// this is one of: middleware, router
111-
const type = attributes['koa.type'];
112-
113-
if (type) {
114-
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, `${type}.koa`);
115-
}
116-
117-
// Also update the name
118-
const name = attributes['koa.name'];
119-
if (typeof name === 'string') {
120-
// Somehow, name is sometimes `''` for middleware spans
121-
// See: https://github.com/open-telemetry/opentelemetry-js-contrib/issues/2220
122-
span.updateName(name || '< unknown >');
123-
}
124-
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { KoaInstrumentation } from '@opentelemetry/instrumentation-koa';
2+
import { type MockInstance, beforeEach, describe, expect, it, vi } from 'vitest';
3+
import { instrumentKoa, koaIntegration } from '../../../src/integrations/tracing/koa';
4+
import { INSTRUMENTED } from '../../../src/otel/instrument';
5+
6+
vi.mock('@opentelemetry/instrumentation-koa');
7+
8+
describe('Koa', () => {
9+
beforeEach(() => {
10+
vi.clearAllMocks();
11+
delete INSTRUMENTED.Koa;
12+
13+
(KoaInstrumentation as unknown as MockInstance).mockImplementation(() => {
14+
return {
15+
setTracerProvider: () => undefined,
16+
setMeterProvider: () => undefined,
17+
getConfig: () => ({}),
18+
setConfig: () => ({}),
19+
enable: () => undefined,
20+
};
21+
});
22+
});
23+
24+
it('defaults are correct for instrumentKoa', () => {
25+
instrumentKoa({});
26+
27+
expect(KoaInstrumentation).toHaveBeenCalledTimes(1);
28+
expect(KoaInstrumentation).toHaveBeenCalledWith({
29+
ignoreLayersType: undefined,
30+
requestHook: expect.any(Function),
31+
});
32+
});
33+
34+
it('passes ignoreLayersType option to instrumentation', () => {
35+
instrumentKoa({ ignoreLayersType: ['middleware'] });
36+
37+
expect(KoaInstrumentation).toHaveBeenCalledTimes(1);
38+
expect(KoaInstrumentation).toHaveBeenCalledWith({
39+
ignoreLayersType: ['middleware'],
40+
requestHook: expect.any(Function),
41+
});
42+
});
43+
44+
it('passes multiple ignoreLayersType values to instrumentation', () => {
45+
instrumentKoa({ ignoreLayersType: ['middleware', 'router'] });
46+
47+
expect(KoaInstrumentation).toHaveBeenCalledTimes(1);
48+
expect(KoaInstrumentation).toHaveBeenCalledWith({
49+
ignoreLayersType: ['middleware', 'router'],
50+
requestHook: expect.any(Function),
51+
});
52+
});
53+
54+
it('defaults are correct for koaIntegration', () => {
55+
koaIntegration().setupOnce!();
56+
57+
expect(KoaInstrumentation).toHaveBeenCalledTimes(1);
58+
expect(KoaInstrumentation).toHaveBeenCalledWith({
59+
ignoreLayersType: undefined,
60+
requestHook: expect.any(Function),
61+
});
62+
});
63+
64+
it('passes options from koaIntegration to instrumentation', () => {
65+
koaIntegration({ ignoreLayersType: ['middleware'] }).setupOnce!();
66+
67+
expect(KoaInstrumentation).toHaveBeenCalledTimes(1);
68+
expect(KoaInstrumentation).toHaveBeenCalledWith({
69+
ignoreLayersType: ['middleware'],
70+
requestHook: expect.any(Function),
71+
});
72+
});
73+
74+
it('passes multiple options from koaIntegration to instrumentation', () => {
75+
koaIntegration({ ignoreLayersType: ['router', 'middleware'] }).setupOnce!();
76+
77+
expect(KoaInstrumentation).toHaveBeenCalledTimes(1);
78+
expect(KoaInstrumentation).toHaveBeenCalledWith({
79+
ignoreLayersType: ['router', 'middleware'],
80+
requestHook: expect.any(Function),
81+
});
82+
});
83+
});

0 commit comments

Comments
 (0)