UNPKG

16.3 kBJavaScriptView Raw
1import { __assign } from "tslib";
2/* eslint-disable max-lines */
3/* eslint-disable @typescript-eslint/no-explicit-any */
4import { captureException, getCurrentHub, startTransaction, withScope } from '@sentry/core';
5import { extractTraceparentData } from '@sentry/tracing';
6import { RequestSessionStatus } from '@sentry/types';
7import { isPlainObject, isString, logger, normalize, stripUrlQueryAndFragment } from '@sentry/utils';
8import * as cookie from 'cookie';
9import * as domain from 'domain';
10import * as os from 'os';
11import * as url from 'url';
12import { flush, isAutoSessionTrackingEnabled } from './sdk';
13/**
14 * Express-compatible tracing handler.
15 * @see Exposed as `Handlers.tracingHandler`
16 */
17export function tracingHandler() {
18 return function sentryTracingMiddleware(req, res, next) {
19 // If there is a trace header set, we extract the data from it (parentSpanId, traceId, and sampling decision)
20 var traceparentData;
21 if (req.headers && isString(req.headers['sentry-trace'])) {
22 traceparentData = extractTraceparentData(req.headers['sentry-trace']);
23 }
24 var transaction = startTransaction(__assign({ name: extractExpressTransactionName(req, { path: true, method: true }), op: 'http.server' }, traceparentData),
25 // extra context passed to the tracesSampler
26 { request: extractRequestData(req) });
27 // We put the transaction on the scope so users can attach children to it
28 getCurrentHub().configureScope(function (scope) {
29 scope.setSpan(transaction);
30 });
31 // We also set __sentry_transaction on the response so people can grab the transaction there to add
32 // spans to it later.
33 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
34 res.__sentry_transaction = transaction;
35 res.once('finish', function () {
36 // Push `transaction.finish` to the next event loop so open spans have a chance to finish before the transaction
37 // closes
38 setImmediate(function () {
39 addExpressReqToTransaction(transaction, req);
40 transaction.setHttpStatus(res.statusCode);
41 transaction.finish();
42 });
43 });
44 next();
45 };
46}
47/**
48 * Set parameterized as transaction name e.g.: `GET /users/:id`
49 * Also adds more context data on the transaction from the request
50 */
51function addExpressReqToTransaction(transaction, req) {
52 if (!transaction)
53 return;
54 transaction.name = extractExpressTransactionName(req, { path: true, method: true });
55 transaction.setData('url', req.originalUrl);
56 transaction.setData('baseUrl', req.baseUrl);
57 transaction.setData('query', req.query);
58}
59/**
60 * Extracts complete generalized path from the request object and uses it to construct transaction name.
61 *
62 * eg. GET /mountpoint/user/:id
63 *
64 * @param req The ExpressRequest object
65 * @param options What to include in the transaction name (method, path, or both)
66 *
67 * @returns The fully constructed transaction name
68 */
69function extractExpressTransactionName(req, options) {
70 if (options === void 0) { options = {}; }
71 var _a;
72 var method = (_a = req.method) === null || _a === void 0 ? void 0 : _a.toUpperCase();
73 var path = '';
74 if (req.route) {
75 path = "" + (req.baseUrl || '') + req.route.path;
76 }
77 else if (req.originalUrl || req.url) {
78 path = stripUrlQueryAndFragment(req.originalUrl || req.url || '');
79 }
80 var info = '';
81 if (options.method && method) {
82 info += method;
83 }
84 if (options.method && options.path) {
85 info += " ";
86 }
87 if (options.path && path) {
88 info += path;
89 }
90 return info;
91}
92/** JSDoc */
93function extractTransaction(req, type) {
94 var _a;
95 switch (type) {
96 case 'path': {
97 return extractExpressTransactionName(req, { path: true });
98 }
99 case 'handler': {
100 return ((_a = req.route) === null || _a === void 0 ? void 0 : _a.stack[0].name) || '<anonymous>';
101 }
102 case 'methodPath':
103 default: {
104 return extractExpressTransactionName(req, { path: true, method: true });
105 }
106 }
107}
108/** Default user keys that'll be used to extract data from the request */
109var DEFAULT_USER_KEYS = ['id', 'username', 'email'];
110/** JSDoc */
111function extractUserData(user, keys) {
112 var extractedUser = {};
113 var attributes = Array.isArray(keys) ? keys : DEFAULT_USER_KEYS;
114 attributes.forEach(function (key) {
115 if (user && key in user) {
116 extractedUser[key] = user[key];
117 }
118 });
119 return extractedUser;
120}
121/** Default request keys that'll be used to extract data from the request */
122var DEFAULT_REQUEST_KEYS = ['cookies', 'data', 'headers', 'method', 'query_string', 'url'];
123/**
124 * Normalizes data from the request object, accounting for framework differences.
125 *
126 * @param req The request object from which to extract data
127 * @param keys An optional array of keys to include in the normalized data. Defaults to DEFAULT_REQUEST_KEYS if not
128 * provided.
129 * @returns An object containing normalized request data
130 */
131export function extractRequestData(req, keys) {
132 if (keys === void 0) { keys = DEFAULT_REQUEST_KEYS; }
133 var requestData = {};
134 // headers:
135 // node, express, nextjs: req.headers
136 // koa: req.header
137 var headers = (req.headers || req.header || {});
138 // method:
139 // node, express, koa, nextjs: req.method
140 var method = req.method;
141 // host:
142 // express: req.hostname in > 4 and req.host in < 4
143 // koa: req.host
144 // node, nextjs: req.headers.host
145 var host = req.hostname || req.host || headers.host || '<no host>';
146 // protocol:
147 // node, nextjs: <n/a>
148 // express, koa: req.protocol
149 var protocol = req.protocol === 'https' || req.secure || (req.socket || {}).encrypted
150 ? 'https'
151 : 'http';
152 // url (including path and query string):
153 // node, express: req.originalUrl
154 // koa, nextjs: req.url
155 var originalUrl = (req.originalUrl || req.url || '');
156 // absolute url
157 var absoluteUrl = protocol + "://" + host + originalUrl;
158 keys.forEach(function (key) {
159 switch (key) {
160 case 'headers':
161 requestData.headers = headers;
162 break;
163 case 'method':
164 requestData.method = method;
165 break;
166 case 'url':
167 requestData.url = absoluteUrl;
168 break;
169 case 'cookies':
170 // cookies:
171 // node, express, koa: req.headers.cookie
172 // vercel, sails.js, express (w/ cookie middleware), nextjs: req.cookies
173 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
174 requestData.cookies = req.cookies || cookie.parse(headers.cookie || '');
175 break;
176 case 'query_string':
177 // query string:
178 // node: req.url (raw)
179 // express, koa, nextjs: req.query
180 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
181 requestData.query_string = req.query || url.parse(originalUrl || '', false).query;
182 break;
183 case 'data':
184 if (method === 'GET' || method === 'HEAD') {
185 break;
186 }
187 // body data:
188 // express, koa, nextjs: req.body
189 //
190 // when using node by itself, you have to read the incoming stream(see
191 // https://nodejs.dev/learn/get-http-request-body-data-using-nodejs); if a user is doing that, we can't know
192 // where they're going to store the final result, so they'll have to capture this data themselves
193 if (req.body !== undefined) {
194 requestData.data = isString(req.body) ? req.body : JSON.stringify(normalize(req.body));
195 }
196 break;
197 default:
198 if ({}.hasOwnProperty.call(req, key)) {
199 requestData[key] = req[key];
200 }
201 }
202 });
203 return requestData;
204}
205/**
206 * Enriches passed event with request data.
207 *
208 * @param event Will be mutated and enriched with req data
209 * @param req Request object
210 * @param options object containing flags to enable functionality
211 * @hidden
212 */
213export function parseRequest(event, req, options) {
214 // eslint-disable-next-line no-param-reassign
215 options = __assign({ ip: false, request: true, serverName: true, transaction: true, user: true, version: true }, options);
216 if (options.version) {
217 event.contexts = __assign(__assign({}, event.contexts), { runtime: {
218 name: 'node',
219 version: global.process.version,
220 } });
221 }
222 if (options.request) {
223 // if the option value is `true`, use the default set of keys by not passing anything to `extractRequestData()`
224 var extractedRequestData = Array.isArray(options.request)
225 ? extractRequestData(req, options.request)
226 : extractRequestData(req);
227 event.request = __assign(__assign({}, event.request), extractedRequestData);
228 }
229 if (options.serverName && !event.server_name) {
230 event.server_name = global.process.env.SENTRY_NAME || os.hostname();
231 }
232 if (options.user) {
233 var extractedUser = req.user && isPlainObject(req.user) ? extractUserData(req.user, options.user) : {};
234 if (Object.keys(extractedUser)) {
235 event.user = __assign(__assign({}, event.user), extractedUser);
236 }
237 }
238 // client ip:
239 // node, nextjs: req.connection.remoteAddress
240 // express, koa: req.ip
241 if (options.ip) {
242 var ip = req.ip || (req.connection && req.connection.remoteAddress);
243 if (ip) {
244 event.user = __assign(__assign({}, event.user), { ip_address: ip });
245 }
246 }
247 if (options.transaction && !event.transaction) {
248 // TODO do we even need this anymore?
249 // TODO make this work for nextjs
250 event.transaction = extractTransaction(req, options.transaction);
251 }
252 return event;
253}
254/**
255 * Express compatible request handler.
256 * @see Exposed as `Handlers.requestHandler`
257 */
258export function requestHandler(options) {
259 var currentHub = getCurrentHub();
260 var client = currentHub.getClient();
261 // Initialise an instance of SessionFlusher on the client when `autoSessionTracking` is enabled and the
262 // `requestHandler` middleware is used indicating that we are running in SessionAggregates mode
263 if (client && isAutoSessionTrackingEnabled(client)) {
264 client.initSessionFlusher();
265 // If Scope contains a Single mode Session, it is removed in favor of using Session Aggregates mode
266 var scope = currentHub.getScope();
267 if (scope && scope.getSession()) {
268 scope.setSession();
269 }
270 }
271 return function sentryRequestMiddleware(req, res, next) {
272 if (options && options.flushTimeout && options.flushTimeout > 0) {
273 // eslint-disable-next-line @typescript-eslint/unbound-method
274 var _end_1 = res.end;
275 res.end = function (chunk, encoding, cb) {
276 var _this = this;
277 void flush(options.flushTimeout)
278 .then(function () {
279 _end_1.call(_this, chunk, encoding, cb);
280 })
281 .then(null, function (e) {
282 logger.error(e);
283 });
284 };
285 }
286 var local = domain.create();
287 local.add(req);
288 local.add(res);
289 local.on('error', next);
290 local.run(function () {
291 var currentHub = getCurrentHub();
292 currentHub.configureScope(function (scope) {
293 scope.addEventProcessor(function (event) { return parseRequest(event, req, options); });
294 var client = currentHub.getClient();
295 if (isAutoSessionTrackingEnabled(client)) {
296 var scope_1 = currentHub.getScope();
297 if (scope_1) {
298 // Set `status` of `RequestSession` to Ok, at the beginning of the request
299 scope_1.setRequestSession({ status: RequestSessionStatus.Ok });
300 }
301 }
302 });
303 res.once('finish', function () {
304 var client = currentHub.getClient();
305 if (isAutoSessionTrackingEnabled(client)) {
306 setImmediate(function () {
307 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
308 if (client && client._captureRequestSession) {
309 // Calling _captureRequestSession to capture request session at the end of the request by incrementing
310 // the correct SessionAggregates bucket i.e. crashed, errored or exited
311 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
312 client._captureRequestSession();
313 }
314 });
315 }
316 });
317 next();
318 });
319 };
320}
321/** JSDoc */
322function getStatusCodeFromResponse(error) {
323 var statusCode = error.status || error.statusCode || error.status_code || (error.output && error.output.statusCode);
324 return statusCode ? parseInt(statusCode, 10) : 500;
325}
326/** Returns true if response code is internal server error */
327function defaultShouldHandleError(error) {
328 var status = getStatusCodeFromResponse(error);
329 return status >= 500;
330}
331/**
332 * Express compatible error handler.
333 * @see Exposed as `Handlers.errorHandler`
334 */
335export function errorHandler(options) {
336 return function sentryErrorMiddleware(error, _req, res, next) {
337 // eslint-disable-next-line @typescript-eslint/unbound-method
338 var shouldHandleError = (options && options.shouldHandleError) || defaultShouldHandleError;
339 if (shouldHandleError(error)) {
340 withScope(function (_scope) {
341 // For some reason we need to set the transaction on the scope again
342 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
343 var transaction = res.__sentry_transaction;
344 if (transaction && _scope.getSpan() === undefined) {
345 _scope.setSpan(transaction);
346 }
347 var client = getCurrentHub().getClient();
348 if (client && isAutoSessionTrackingEnabled(client)) {
349 // Check if the `SessionFlusher` is instantiated on the client to go into this branch that marks the
350 // `requestSession.status` as `Crashed`, and this check is necessary because the `SessionFlusher` is only
351 // instantiated when the the`requestHandler` middleware is initialised, which indicates that we should be
352 // running in SessionAggregates mode
353 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
354 var isSessionAggregatesMode = client._sessionFlusher !== undefined;
355 if (isSessionAggregatesMode) {
356 var requestSession = _scope.getRequestSession();
357 // If an error bubbles to the `errorHandler`, then this is an unhandled error, and should be reported as a
358 // Crashed session. The `_requestSession.status` is checked to ensure that this error is happening within
359 // the bounds of a request, and if so the status is updated
360 if (requestSession && requestSession.status !== undefined)
361 requestSession.status = RequestSessionStatus.Crashed;
362 }
363 }
364 var eventId = captureException(error);
365 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
366 res.sentry = eventId;
367 next(error);
368 });
369 return;
370 }
371 next(error);
372 };
373}
374//# sourceMappingURL=handlers.js.map
\No newline at end of file