-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
Copy pathexpress.ts
211 lines (188 loc) · 6.64 KB
/
express.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
import type * as http from 'node:http';
import type { Span } from '@opentelemetry/api';
import type { ExpressRequestInfo } from '@opentelemetry/instrumentation-express';
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
import type { IntegrationFn } from '@sentry/core';
import {
httpRequestToRequestData,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
captureException,
defineIntegration,
getDefaultIsolationScope,
getIsolationScope,
logger,
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';
import { ExpressInstrumentationV5 } from './express-v5/instrumentation';
const INTEGRATION_NAME = 'Express';
const INTEGRATION_NAME_V5 = 'Express-V5';
function requestHook(span: Span): void {
addOriginToSpan(span, 'auto.http.otel.express');
const attributes = spanToJSON(span).data;
// this is one of: middleware, request_handler, router
const type = attributes['express.type'];
if (type) {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, `${type}.express`);
}
// Also update the name, we don't need to "middleware - " prefix
const name = attributes['express.name'];
if (typeof name === 'string') {
span.updateName(name);
}
}
function spanNameHook(info: ExpressRequestInfo<unknown>, defaultName: string): string {
if (getIsolationScope() === getDefaultIsolationScope()) {
DEBUG_BUILD && logger.warn('Isolation scope is still default isolation scope - skipping setting transactionName');
return defaultName;
}
if (info.layerType === 'request_handler') {
// type cast b/c Otel unfortunately types info.request as any :(
const req = info.request as { method?: string };
const method = req.method ? req.method.toUpperCase() : 'GET';
getIsolationScope().setTransactionName(`${method} ${info.route}`);
}
return defaultName;
}
export const instrumentExpress = generateInstrumentOnce(
INTEGRATION_NAME,
() =>
new ExpressInstrumentation({
requestHook: span => requestHook(span),
spanNameHook: (info, defaultName) => spanNameHook(info, defaultName),
}),
);
export const instrumentExpressV5 = generateInstrumentOnce(
INTEGRATION_NAME_V5,
() =>
new ExpressInstrumentationV5({
requestHook: span => requestHook(span),
spanNameHook: (info, defaultName) => spanNameHook(info, defaultName),
}),
);
const _expressIntegration = (() => {
return {
name: INTEGRATION_NAME,
setupOnce() {
instrumentExpress();
instrumentExpressV5();
},
};
}) satisfies IntegrationFn;
/**
* Adds Sentry tracing instrumentation for [Express](https://expressjs.com/).
*
* If you also want to capture errors, you need to call `setupExpressErrorHandler(app)` after you set up your Express server.
*
* For more information, see the [express documentation](https://docs.sentry.io/platforms/javascript/guides/express/).
*
* @example
* ```javascript
* const Sentry = require('@sentry/node');
*
* Sentry.init({
* integrations: [Sentry.expressIntegration()],
* })
* ```
*/
export const expressIntegration = defineIntegration(_expressIntegration);
interface MiddlewareError extends Error {
status?: number | string;
statusCode?: number | string;
status_code?: number | string;
output?: {
statusCode?: number | string;
};
}
type ExpressMiddleware = (req: http.IncomingMessage, res: http.ServerResponse, next: () => void) => void;
type ExpressErrorMiddleware = (
error: MiddlewareError,
req: http.IncomingMessage,
res: http.ServerResponse,
next: (error: MiddlewareError) => void,
) => void;
interface ExpressHandlerOptions {
/**
* Callback method deciding whether error should be captured and sent to Sentry
* @param error Captured middleware error
*/
shouldHandleError?(this: void, error: MiddlewareError): boolean;
}
/**
* An Express-compatible error handler.
*/
export function expressErrorHandler(options?: ExpressHandlerOptions): ExpressErrorMiddleware {
return function sentryErrorMiddleware(
error: MiddlewareError,
request: http.IncomingMessage,
res: http.ServerResponse,
next: (error: MiddlewareError) => void,
): void {
const normalizedRequest = httpRequestToRequestData(request);
// Ensure we use the express-enhanced request here, instead of the plain HTTP one
// When an error happens, the `expressRequestHandler` middleware does not run, so we set it here too
getIsolationScope().setSDKProcessingMetadata({ normalizedRequest });
const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError;
if (shouldHandleError(error)) {
const eventId = captureException(error, { mechanism: { type: 'middleware', handled: false } });
(res as { sentry?: string }).sentry = eventId;
}
next(error);
};
}
function expressRequestHandler(): ExpressMiddleware {
return function sentryRequestMiddleware(
request: http.IncomingMessage,
_res: http.ServerResponse,
next: () => void,
): void {
const normalizedRequest = httpRequestToRequestData(request);
// Ensure we use the express-enhanced request here, instead of the plain HTTP one
getIsolationScope().setSDKProcessingMetadata({ normalizedRequest });
next();
};
}
/**
* Add an Express error handler to capture errors to Sentry.
*
* The error handler must be before any other middleware and after all controllers.
*
* @param app The Express instances
* @param options {ExpressHandlerOptions} Configuration options for the handler
*
* @example
* ```javascript
* const Sentry = require('@sentry/node');
* const express = require("express");
*
* const app = express();
*
* // Add your routes, etc.
*
* // Add this after all routes,
* // but before any and other error-handling middlewares are defined
* Sentry.setupExpressErrorHandler(app);
*
* app.listen(3000);
* ```
*/
export function setupExpressErrorHandler(
app: { use: (middleware: ExpressMiddleware | ExpressErrorMiddleware) => unknown },
options?: ExpressHandlerOptions,
): void {
app.use(expressRequestHandler());
app.use(expressErrorHandler(options));
ensureIsWrapped(app.use, 'express');
}
function getStatusCodeFromResponse(error: MiddlewareError): number {
const statusCode = error.status || error.statusCode || error.status_code || error.output?.statusCode;
return statusCode ? parseInt(statusCode as string, 10) : 500;
}
/** Returns true if response code is internal server error */
function defaultShouldHandleError(error: MiddlewareError): boolean {
const status = getStatusCodeFromResponse(error);
return status >= 500;
}