Skip to content
106 changes: 92 additions & 14 deletions packages/node/src/integrations/tracing/express.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type * as http from 'node:http';
import type { Span } from '@opentelemetry/api';
import type { ExpressRequestInfo } from '@opentelemetry/instrumentation-express';
import type {
ExpressInstrumentationConfig,
ExpressLayerType,
ExpressRequestInfo,
} from '@opentelemetry/instrumentation-express';
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
import type { IntegrationFn } from '@sentry/core';
import {
Expand All @@ -20,6 +24,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 @@ -52,30 +91,39 @@ function spanNameHook(info: ExpressRequestInfo<unknown>, defaultName: string): s
return defaultName;
}

function buildInstrumentationConfig(options: ExpressOptions): ExpressInstrumentationConfig {
const config: ExpressInstrumentationConfig = {
requestHook: (span: Span) => requestHook(span),
spanNameHook: (info: ExpressRequestInfo<unknown>, defaultName: string) => spanNameHook(info, defaultName),
};

if (options.ignoreLayers) {
config.ignoreLayers = options.ignoreLayers;
}

if (options.ignoreLayersType) {
config.ignoreLayersType = options.ignoreLayersType as ExpressLayerType[];
}

return config;
}

export const instrumentExpress = generateInstrumentOnce(
INTEGRATION_NAME,
() =>
new ExpressInstrumentation({
requestHook: span => requestHook(span),
spanNameHook: (info, defaultName) => spanNameHook(info, defaultName),
}),
(options: ExpressOptions = {}) => new ExpressInstrumentation(buildInstrumentationConfig(options)),
);

export const instrumentExpressV5 = generateInstrumentOnce(
INTEGRATION_NAME_V5,
() =>
new ExpressInstrumentationV5({
requestHook: span => requestHook(span),
spanNameHook: (info, defaultName) => spanNameHook(info, defaultName),
}),
(options: ExpressOptions = {}) => new ExpressInstrumentationV5(buildInstrumentationConfig(options)),
);

const _expressIntegration = (() => {
const _expressIntegration = ((options: ExpressOptions = {}) => {
return {
name: INTEGRATION_NAME,
setupOnce() {
instrumentExpress();
instrumentExpressV5();
instrumentExpress(options);
instrumentExpressV5(options);
},
};
}) satisfies IntegrationFn;
Expand All @@ -87,6 +135,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 @@ -95,6 +145,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