Skip to content

feat(node): Add ignoreLayers and ignoreLayersType options to express instrumentation #16556

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

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
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: 75 additions & 6 deletions packages/node/src/integrations/tracing/express.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type * as http from 'node:http';
import type { Span } from '@opentelemetry/api';
import type { ExpressRequestInfo } from '@opentelemetry/instrumentation-express';
import type { ExpressLayerType, ExpressRequestInfo } from '@opentelemetry/instrumentation-express';
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
import type { IntegrationFn } from '@sentry/core';
import {
Expand All @@ -22,6 +22,41 @@ import { ExpressInstrumentationV5 } from './express-v5/instrumentation';
const INTEGRATION_NAME = 'Express';
const INTEGRATION_NAME_V5 = 'Express-V5';

type IgnoreMatcher = string | RegExp | ((path: string) => boolean);

interface ExpressOptions {
/**
* Ignore specific layers based on their path.
*
* Accepts an array of matchers that can be:
* - String: exact path match
* - RegExp: pattern matching
* - Function: custom logic that receives the path and returns boolean
*/
ignoreLayers?: IgnoreMatcher[];
/**
* Ignore specific layers based on their type.
*
* Available layer types:
* - 'router': Express router layers
* - 'middleware': Express middleware layers
* - 'request_handler': Express request handler layers
*
* @example
* ```javascript
* // Ignore only middleware layers
* ignoreLayersType: ['middleware']
*
* // Ignore multiple layer types
* ignoreLayersType: ['middleware', 'router']
*
* // Ignore all layer types (effectively disables tracing)
* ignoreLayersType: ['middleware', 'router', 'request_handler']
* ```
*/
ignoreLayersType?: ('router' | 'middleware' | 'request_handler')[];
}

function requestHook(span: Span): void {
addOriginToSpan(span, 'auto.http.otel.express');

Expand Down Expand Up @@ -56,28 +91,32 @@ function spanNameHook(info: ExpressRequestInfo<unknown>, defaultName: string): s

export const instrumentExpress = generateInstrumentOnce(
INTEGRATION_NAME,
() =>
(options: ExpressOptions = {}) =>
new ExpressInstrumentation({
requestHook: span => requestHook(span),
spanNameHook: (info, defaultName) => spanNameHook(info, defaultName),
ignoreLayers: options.ignoreLayers,
ignoreLayersType: options.ignoreLayersType as ExpressLayerType[],
}),
);

export const instrumentExpressV5 = generateInstrumentOnce(
INTEGRATION_NAME_V5,
() =>
(options: ExpressOptions = {}) =>
new ExpressInstrumentationV5({
requestHook: span => requestHook(span),
spanNameHook: (info, defaultName) => spanNameHook(info, defaultName),
ignoreLayers: options.ignoreLayers,
ignoreLayersType: options.ignoreLayersType as ExpressLayerType[],
}),
);

const _expressIntegration = (() => {
const _expressIntegration = ((options: ExpressOptions = {}) => {
return {
name: INTEGRATION_NAME,
setupOnce() {
instrumentExpress();
instrumentExpressV5();
instrumentExpress(options);
instrumentExpressV5(options);
},
};
}) satisfies IntegrationFn;
Expand All @@ -89,6 +128,8 @@ const _expressIntegration = (() => {
*
* For more information, see the [express documentation](https://docs.sentry.io/platforms/javascript/guides/express/).
*
* @param {ExpressOptions} options Configuration options for the Express integration.
*
* @example
* ```javascript
* const Sentry = require('@sentry/node');
Expand All @@ -97,6 +138,34 @@ const _expressIntegration = (() => {
* integrations: [Sentry.expressIntegration()],
* })
* ```
*
* @example
* ```javascript
* // To ignore specific middleware layers by path
* const Sentry = require('@sentry/node');
*
* Sentry.init({
* integrations: [
* Sentry.expressIntegration({
* ignoreLayers: ['/health', /^\/internal/]
* })
* ],
* })
* ```
*
* @example
* ```javascript
* // To ignore specific middleware layers by type
* const Sentry = require('@sentry/node');
*
* Sentry.init({
* integrations: [
* Sentry.expressIntegration({
* ignoreLayersType: ['middleware']
* })
* ],
* })
* ```
*/
export const expressIntegration = defineIntegration(_expressIntegration);

Expand Down
197 changes: 197 additions & 0 deletions packages/node/test/integrations/tracing/express.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
import { type MockInstance, beforeEach, describe, expect, it, vi } from 'vitest';
import { expressIntegration, instrumentExpress, instrumentExpressV5 } from '../../../src/integrations/tracing/express';
import { ExpressInstrumentationV5 } from '../../../src/integrations/tracing/express-v5/instrumentation';
import { INSTRUMENTED } from '../../../src/otel/instrument';

vi.mock('@opentelemetry/instrumentation-express');
vi.mock('../../../src/integrations/tracing/express-v5/instrumentation');

describe('Express', () => {
beforeEach(() => {
vi.clearAllMocks();
delete INSTRUMENTED.Express;
delete INSTRUMENTED['Express-V5'];

(ExpressInstrumentation as unknown as MockInstance).mockImplementation(() => {
return {
setTracerProvider: () => undefined,
setMeterProvider: () => undefined,
getConfig: () => ({}),
setConfig: () => ({}),
enable: () => undefined,
};
});

(ExpressInstrumentationV5 as unknown as MockInstance).mockImplementation(() => {
return {
setTracerProvider: () => undefined,
setMeterProvider: () => undefined,
getConfig: () => ({}),
setConfig: () => ({}),
enable: () => undefined,
};
});
});

describe('instrumentExpress', () => {
it('defaults are correct for instrumentExpress', () => {
instrumentExpress({});

expect(ExpressInstrumentation).toHaveBeenCalledTimes(1);
expect(ExpressInstrumentation).toHaveBeenCalledWith({
ignoreLayers: undefined,
ignoreLayersType: undefined,
requestHook: expect.any(Function),
spanNameHook: expect.any(Function),
});
});

it('passes ignoreLayers option to instrumentation', () => {
instrumentExpress({ ignoreLayers: ['/health', /^\/internal/] });

expect(ExpressInstrumentation).toHaveBeenCalledTimes(1);
expect(ExpressInstrumentation).toHaveBeenCalledWith({
ignoreLayers: ['/health', /^\/internal/],
ignoreLayersType: undefined,
requestHook: expect.any(Function),
spanNameHook: expect.any(Function),
});
});

it('passes ignoreLayersType option to instrumentation', () => {
instrumentExpress({ ignoreLayersType: ['middleware'] });

expect(ExpressInstrumentation).toHaveBeenCalledTimes(1);
expect(ExpressInstrumentation).toHaveBeenCalledWith({
ignoreLayers: undefined,
ignoreLayersType: ['middleware'],
requestHook: expect.any(Function),
spanNameHook: expect.any(Function),
});
});

it('passes multiple ignoreLayersType values to instrumentation', () => {
instrumentExpress({ ignoreLayersType: ['middleware', 'router'] });

expect(ExpressInstrumentation).toHaveBeenCalledTimes(1);
expect(ExpressInstrumentation).toHaveBeenCalledWith({
ignoreLayers: undefined,
ignoreLayersType: ['middleware', 'router'],
requestHook: expect.any(Function),
spanNameHook: expect.any(Function),
});
});

it('passes both options to instrumentation', () => {
instrumentExpress({
ignoreLayers: ['/health'],
ignoreLayersType: ['request_handler'],
});

expect(ExpressInstrumentation).toHaveBeenCalledTimes(1);
expect(ExpressInstrumentation).toHaveBeenCalledWith({
ignoreLayers: ['/health'],
ignoreLayersType: ['request_handler'],
requestHook: expect.any(Function),
spanNameHook: expect.any(Function),
});
});
});

describe('instrumentExpressV5', () => {
it('defaults are correct for instrumentExpressV5', () => {
instrumentExpressV5({});

expect(ExpressInstrumentationV5).toHaveBeenCalledTimes(1);
expect(ExpressInstrumentationV5).toHaveBeenCalledWith({
ignoreLayers: undefined,
ignoreLayersType: undefined,
requestHook: expect.any(Function),
spanNameHook: expect.any(Function),
});
});

it('passes options to instrumentExpressV5', () => {
instrumentExpressV5({
ignoreLayers: [(path: string) => path.startsWith('/admin')],
ignoreLayersType: ['middleware', 'router'],
});

expect(ExpressInstrumentationV5).toHaveBeenCalledTimes(1);
expect(ExpressInstrumentationV5).toHaveBeenCalledWith({
ignoreLayers: [expect.any(Function)],
ignoreLayersType: ['middleware', 'router'],
requestHook: expect.any(Function),
spanNameHook: expect.any(Function),
});
});
});

describe('expressIntegration', () => {
it('defaults are correct for expressIntegration', () => {
expressIntegration().setupOnce!();

expect(ExpressInstrumentation).toHaveBeenCalledTimes(1);
expect(ExpressInstrumentation).toHaveBeenCalledWith({
ignoreLayers: undefined,
ignoreLayersType: undefined,
requestHook: expect.any(Function),
spanNameHook: expect.any(Function),
});

expect(ExpressInstrumentationV5).toHaveBeenCalledTimes(1);
expect(ExpressInstrumentationV5).toHaveBeenCalledWith({
ignoreLayers: undefined,
ignoreLayersType: undefined,
requestHook: expect.any(Function),
spanNameHook: expect.any(Function),
});
});

it('passes options from expressIntegration to both instrumentations', () => {
expressIntegration({
ignoreLayers: [/^\/api\/v1/],
ignoreLayersType: ['middleware'],
}).setupOnce!();

expect(ExpressInstrumentation).toHaveBeenCalledTimes(1);
expect(ExpressInstrumentation).toHaveBeenCalledWith({
ignoreLayers: [/^\/api\/v1/],
ignoreLayersType: ['middleware'],
requestHook: expect.any(Function),
spanNameHook: expect.any(Function),
});

expect(ExpressInstrumentationV5).toHaveBeenCalledTimes(1);
expect(ExpressInstrumentationV5).toHaveBeenCalledWith({
ignoreLayers: [/^\/api\/v1/],
ignoreLayersType: ['middleware'],
requestHook: expect.any(Function),
spanNameHook: expect.any(Function),
});
});

it('passes all layer types from expressIntegration to instrumentation', () => {
expressIntegration({
ignoreLayersType: ['router', 'middleware', 'request_handler'],
}).setupOnce!();

expect(ExpressInstrumentation).toHaveBeenCalledTimes(1);
expect(ExpressInstrumentation).toHaveBeenCalledWith({
ignoreLayers: undefined,
ignoreLayersType: ['router', 'middleware', 'request_handler'],
requestHook: expect.any(Function),
spanNameHook: expect.any(Function),
});

expect(ExpressInstrumentationV5).toHaveBeenCalledTimes(1);
expect(ExpressInstrumentationV5).toHaveBeenCalledWith({
ignoreLayers: undefined,
ignoreLayersType: ['router', 'middleware', 'request_handler'],
requestHook: expect.any(Function),
spanNameHook: expect.any(Function),
});
});
});
});
Loading