Skip to content

feat(node): Introduce ignoreLayersType option to koa integration #16553

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 51 additions & 30 deletions packages/node/src/integrations/tracing/koa.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,75 @@
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,
getDefaultIsolationScope,
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';
if (route) {
getIsolationScope().setTransactionName(`${method} ${route}`);
}
},
}),
} satisfies KoaInstrumentationConfig;
},
);

const _koaIntegration = (() => {
const _koaIntegration = ((options: KoaOptions = {}) => {
return {
name: INTEGRATION_NAME,
setupOnce() {
instrumentKoa();
instrumentKoa(options);
},
};
}) satisfies IntegrationFn;
Expand All @@ -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');
Expand All @@ -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);

Expand Down Expand Up @@ -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 >');
}
}
83 changes: 83 additions & 0 deletions packages/node/test/integrations/tracing/koa.test.ts
Original file line number Diff line number Diff line change
@@ -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),
});
});
});
Loading