Skip to content

Commit 066e637

Browse files
committed
WIP express example
1 parent 5885d1e commit 066e637

3 files changed

Lines changed: 47 additions & 46 deletions

File tree

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ export type {
137137
ExpressHandlerOptions,
138138
ExpressMiddleware,
139139
ExpressErrorMiddleware,
140+
ExpressModuleExport,
140141
} from './integrations/express/types';
141142
export { dedupeIntegration } from './integrations/dedupe';
142143
export { extraErrorDataIntegration } from './integrations/extraerrordata';

packages/node-core/src/module-wrapper/index.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ export interface ModuleWrapperFileOptions<TOptions = unknown> {
2929
}
3030

3131
/** Options for registering a module wrapper */
32-
export interface ModuleWrapperOptions<TOptions = unknown> {
32+
export interface ModuleWrapperOptions<TModuleExports = unknown, TOptions = unknown> {
3333
/** Module name to wrap (e.g., 'express', 'pg', '@prisma/client') */
3434
moduleName: string;
3535
/** Semver ranges for supported versions (e.g., ['>=4.0.0 <5.0.0']) */
3636
supportedVersions: string[];
3737
/** Function to patch the module's exports. Use getOptions() to access current options at runtime. */
38-
patch: (moduleExports: unknown, getOptions: () => TOptions | undefined, version?: string) => unknown;
38+
patch: (moduleExports: TModuleExports, getOptions: () => TOptions | undefined, version?: string) => unknown;
3939
/** Optional array of specific files within the module to patch */
4040
files?: ModuleWrapperFileOptions<TOptions>[];
4141
/** Optional configuration options that can be updated on subsequent calls */
@@ -69,7 +69,9 @@ export interface ModuleWrapperOptions<TOptions = unknown> {
6969
* });
7070
* ```
7171
*/
72-
export function registerModuleWrapper<TOptions = unknown>(wrapperOptions: ModuleWrapperOptions<TOptions>): void {
72+
export function registerModuleWrapper<TModuleExports = unknown, TOptions = unknown>(
73+
wrapperOptions: ModuleWrapperOptions<TModuleExports, TOptions>,
74+
): void {
7375
const { moduleName, supportedVersions, patch, files, options } = wrapperOptions;
7476

7577
// Always update the stored options (even if already registered)
@@ -102,7 +104,7 @@ export function registerModuleWrapper<TOptions = unknown>(wrapperOptions: Module
102104
`file hooks: ${files?.map(f => f.name).join(', ')}`,
103105
);
104106

105-
return patch(exports, getOptions, version);
107+
return patch(exports as TModuleExports, getOptions, version);
106108
}
107109
} else if (files) {
108110
// Check if this is one of the specified files
@@ -157,7 +159,7 @@ export function registerModuleWrapper<TOptions = unknown>(wrapperOptions: Module
157159
`file hooks: ${files?.map(f => f.name).join(', ')}`,
158160
);
159161

160-
return patch(exports, getOptions, version);
162+
return patch(exports as TModuleExports, getOptions, version);
161163
}
162164
} else if (files) {
163165
for (const file of files) {

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

Lines changed: 39 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
1-
// Automatic istrumentation for Express using OTel
2-
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
3-
import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
41
import { context } from '@opentelemetry/api';
52
import { getRPCMetadata, RPCType } from '@opentelemetry/core';
63

7-
import { ensureIsWrapped, generateInstrumentOnce } from '@sentry/node-core';
4+
import { ensureIsWrapped, registerModuleWrapper } from '@sentry/node-core';
85
import {
96
type ExpressIntegrationOptions,
7+
type ExpressModuleExport,
108
type IntegrationFn,
119
debug,
1210
patchExpressModule,
13-
SDK_VERSION,
1411
defineIntegration,
1512
setupExpressErrorHandler as coreSetupExpressErrorHandler,
1613
type ExpressHandlerOptions,
@@ -19,6 +16,7 @@ export { expressErrorHandler } from '@sentry/core';
1916
import { DEBUG_BUILD } from '../../debug-build';
2017

2118
const INTEGRATION_NAME = 'Express';
19+
const MODULE_NAME = 'express';
2220
const SUPPORTED_VERSIONS = ['>=4.0.0 <6'];
2321

2422
export function setupExpressErrorHandler(
@@ -30,44 +28,44 @@ export function setupExpressErrorHandler(
3028
ensureIsWrapped(app.use, 'express');
3129
}
3230

33-
export type ExpressInstrumentationConfig = InstrumentationConfig &
34-
Omit<ExpressIntegrationOptions, 'express' | 'onRouteResolved'>;
31+
export type ExpressInstrumentationConfig = Omit<ExpressIntegrationOptions, 'onRouteResolved'>;
3532

36-
export const instrumentExpress = generateInstrumentOnce(
37-
INTEGRATION_NAME,
38-
(options?: ExpressInstrumentationConfig) => new ExpressInstrumentation(options),
39-
);
40-
41-
export class ExpressInstrumentation extends InstrumentationBase<ExpressInstrumentationConfig> {
42-
public constructor(config: ExpressInstrumentationConfig = {}) {
43-
super('sentry-express', SDK_VERSION, config);
44-
}
45-
public init(): InstrumentationNodeModuleDefinition {
46-
const module = new InstrumentationNodeModuleDefinition(
47-
'express',
48-
SUPPORTED_VERSIONS,
49-
express => {
50-
try {
51-
patchExpressModule(express, () => ({
52-
...this.getConfig(),
53-
onRouteResolved(route) {
54-
const rpcMetadata = getRPCMetadata(context.active());
55-
if (route && rpcMetadata?.type === RPCType.HTTP) {
56-
rpcMetadata.route = route;
57-
}
58-
},
59-
}));
60-
} catch (e) {
61-
DEBUG_BUILD && debug.error('Failed to patch express module:', e);
62-
}
63-
return express;
64-
},
65-
// we do not ever actually unpatch in our SDKs
66-
express => express,
67-
);
68-
return module;
69-
}
33+
/**
34+
* Instrument Express using registerModuleWrapper.
35+
* This registers hooks for both CJS and ESM module loading.
36+
*
37+
* Calling this multiple times is safe:
38+
* - Hooks are only registered once (first call)
39+
* - Options are updated on each call
40+
* - Use getOptions() in the patch to access current options at runtime
41+
*/
42+
export function instrumentExpress(options: ExpressInstrumentationConfig = {}): void {
43+
registerModuleWrapper<ExpressModuleExport, ExpressInstrumentationConfig>({
44+
moduleName: MODULE_NAME,
45+
supportedVersions: SUPPORTED_VERSIONS,
46+
options,
47+
patch: (moduleExports, getOptions) => {
48+
try {
49+
patchExpressModule(moduleExports, () => ({
50+
...getOptions(),
51+
onRouteResolved(route) {
52+
const rpcMetadata = getRPCMetadata(context.active());
53+
if (route && rpcMetadata?.type === RPCType.HTTP) {
54+
rpcMetadata.route = route;
55+
}
56+
},
57+
}));
58+
} catch (e) {
59+
DEBUG_BUILD && debug.error('Failed to patch express module:', e);
60+
}
61+
return moduleExports;
62+
},
63+
});
7064
}
65+
66+
// Add id property for compatibility with preloadOpenTelemetry logging
67+
instrumentExpress.id = INTEGRATION_NAME;
68+
7169
const _expressIntegration = ((options?: ExpressInstrumentationConfig) => {
7270
return {
7371
name: INTEGRATION_NAME,

0 commit comments

Comments
 (0)