@prairielearn/sentry 3.0.7 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +13 -1
- package/dist/express.d.ts +50 -0
- package/dist/express.js +78 -0
- package/dist/express.js.map +1 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/express.ts +132 -0
- package/src/index.ts +5 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @prairielearn/sentry
|
|
2
2
|
|
|
3
|
+
## 4.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- b55261c: Upgrade to TypeScript 5.9
|
|
8
|
+
|
|
9
|
+
## 4.0.0
|
|
10
|
+
|
|
11
|
+
### Major Changes
|
|
12
|
+
|
|
13
|
+
- a7d1ad9: Switch to using `@sentry/node-core`. The consumer must now ensure that OpenTelemetry is configured correctly.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 23adb05: Upgrade all JavaScript dependencies
|
|
18
|
+
|
|
3
19
|
## 3.0.7
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@prairielearn/sentry`
|
|
2
2
|
|
|
3
|
-
Opinionated wrapper around `@sentry/node`.
|
|
3
|
+
Opinionated wrapper around `@sentry/core` and `@sentry/node-core`. The main modification is an async `init` function that automatically sets the release to the current Git revision, if available.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
6
|
import { init } from '@prairielearn/sentry';
|
|
@@ -10,3 +10,15 @@ await init({
|
|
|
10
10
|
environment: 'ENVIRONMENT HERE',
|
|
11
11
|
});
|
|
12
12
|
```
|
|
13
|
+
|
|
14
|
+
## Why `@sentry/node-core` instead of `@sentry/node`?
|
|
15
|
+
|
|
16
|
+
`@sentry/node` ships with automatic OpenTelemetry integration. This has two main downsides for us:
|
|
17
|
+
|
|
18
|
+
- PrairieLearn applications have their own OpenTelemetry setup, which conflicts with Sentry's desire to control OpenTelemetry. In isolation, this wouldn't be a problem, as they offer configuration options to disable automatic OpenTelemetry setup. However...
|
|
19
|
+
- It pins OpenTelemetry instrumentation packages to specific versions. This makes it hard for us to upgrade OpenTelemetry instrumentation packages independently.
|
|
20
|
+
- It includes a lot of unnecessary OpenTelemetry instrumentation packages that are unused in our codebase.
|
|
21
|
+
|
|
22
|
+
By using `@sentry/node-core` instead, we retain full control over the OpenTelemetry setup and the versions of OpenTelemetry instrumentation packages we use.
|
|
23
|
+
|
|
24
|
+
See <https://github.com/getsentry/sentry-javascript/issues/15213> for slightly more historical context.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type * as http from 'node:http';
|
|
2
|
+
interface MiddlewareError extends Error {
|
|
3
|
+
status?: number | string;
|
|
4
|
+
statusCode?: number | string;
|
|
5
|
+
status_code?: number | string;
|
|
6
|
+
output?: {
|
|
7
|
+
statusCode?: number | string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
type ExpressMiddleware = (req: http.IncomingMessage, res: http.ServerResponse, next: () => void) => void;
|
|
11
|
+
type ExpressErrorMiddleware = (error: MiddlewareError, req: http.IncomingMessage, res: http.ServerResponse, next: (error: MiddlewareError) => void) => void;
|
|
12
|
+
interface ExpressHandlerOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Callback method deciding whether error should be captured and sent to Sentry
|
|
15
|
+
* @param error Captured middleware error
|
|
16
|
+
*/
|
|
17
|
+
shouldHandleError?(error: MiddlewareError): boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* An Express-compatible error handler.
|
|
21
|
+
*/
|
|
22
|
+
export declare function expressErrorHandler(options?: ExpressHandlerOptions): ExpressErrorMiddleware;
|
|
23
|
+
/**
|
|
24
|
+
* Add an Express error handler to capture errors to Sentry.
|
|
25
|
+
*
|
|
26
|
+
* The error handler must be before any other middleware and after all controllers.
|
|
27
|
+
*
|
|
28
|
+
* @param app The Express instances
|
|
29
|
+
* @param options {ExpressHandlerOptions} Configuration options for the handler
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```javascript
|
|
33
|
+
* const Sentry = require('@sentry/node');
|
|
34
|
+
* const express = require("express");
|
|
35
|
+
*
|
|
36
|
+
* const app = express();
|
|
37
|
+
*
|
|
38
|
+
* // Add your routes, etc.
|
|
39
|
+
*
|
|
40
|
+
* // Add this after all routes,
|
|
41
|
+
* // but before any and other error-handling middlewares are defined
|
|
42
|
+
* Sentry.setupExpressErrorHandler(app);
|
|
43
|
+
*
|
|
44
|
+
* app.listen(3000);
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function setupExpressErrorHandler(app: {
|
|
48
|
+
use: (middleware: ExpressMiddleware | ExpressErrorMiddleware) => unknown;
|
|
49
|
+
}, options?: ExpressHandlerOptions): void;
|
|
50
|
+
export {};
|
package/dist/express.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/* eslint-disable jsdoc/check-param-names */
|
|
2
|
+
// This is a fork of Sentry's Express integration from `@sentry/node`that's not
|
|
3
|
+
// available in the `@sentry/node-core` package`. It has been lightly modified
|
|
4
|
+
// to remove unused code and conform to PrairieLearn's coding style.
|
|
5
|
+
//
|
|
6
|
+
// See this package's `README.md` for more information about why we aren't
|
|
7
|
+
// using `@sentry/node` directly.
|
|
8
|
+
//
|
|
9
|
+
// This was forked from the following file on 2025-07-30:
|
|
10
|
+
// https://github.com/getsentry/sentry-javascript/blob/12ac49a9956fd1b64b3f8ad4b2b8f1da426a1efd/packages/node/src/integrations/tracing/express.ts
|
|
11
|
+
import { captureException, getIsolationScope, httpRequestToRequestData } from '@sentry/core';
|
|
12
|
+
import { ensureIsWrapped } from '@sentry/node-core';
|
|
13
|
+
/**
|
|
14
|
+
* An Express-compatible error handler.
|
|
15
|
+
*/
|
|
16
|
+
export function expressErrorHandler(options) {
|
|
17
|
+
return function sentryErrorMiddleware(error, request, res, next) {
|
|
18
|
+
const normalizedRequest = httpRequestToRequestData(request);
|
|
19
|
+
// Ensure we use the express-enhanced request here, instead of the plain HTTP one
|
|
20
|
+
// When an error happens, the `expressRequestHandler` middleware does not run, so we set it here too
|
|
21
|
+
getIsolationScope().setSDKProcessingMetadata({ normalizedRequest });
|
|
22
|
+
const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError;
|
|
23
|
+
if (shouldHandleError(error)) {
|
|
24
|
+
const eventId = captureException(error, {
|
|
25
|
+
mechanism: { type: 'middleware', handled: false },
|
|
26
|
+
});
|
|
27
|
+
res.sentry = eventId;
|
|
28
|
+
}
|
|
29
|
+
next(error);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function expressRequestHandler() {
|
|
33
|
+
return function sentryRequestMiddleware(request, _res, next) {
|
|
34
|
+
const normalizedRequest = httpRequestToRequestData(request);
|
|
35
|
+
// Ensure we use the express-enhanced request here, instead of the plain HTTP one
|
|
36
|
+
getIsolationScope().setSDKProcessingMetadata({ normalizedRequest });
|
|
37
|
+
next();
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Add an Express error handler to capture errors to Sentry.
|
|
42
|
+
*
|
|
43
|
+
* The error handler must be before any other middleware and after all controllers.
|
|
44
|
+
*
|
|
45
|
+
* @param app The Express instances
|
|
46
|
+
* @param options {ExpressHandlerOptions} Configuration options for the handler
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```javascript
|
|
50
|
+
* const Sentry = require('@sentry/node');
|
|
51
|
+
* const express = require("express");
|
|
52
|
+
*
|
|
53
|
+
* const app = express();
|
|
54
|
+
*
|
|
55
|
+
* // Add your routes, etc.
|
|
56
|
+
*
|
|
57
|
+
* // Add this after all routes,
|
|
58
|
+
* // but before any and other error-handling middlewares are defined
|
|
59
|
+
* Sentry.setupExpressErrorHandler(app);
|
|
60
|
+
*
|
|
61
|
+
* app.listen(3000);
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export function setupExpressErrorHandler(app, options) {
|
|
65
|
+
app.use(expressRequestHandler());
|
|
66
|
+
app.use(expressErrorHandler(options));
|
|
67
|
+
ensureIsWrapped(app.use, 'express');
|
|
68
|
+
}
|
|
69
|
+
function getStatusCodeFromResponse(error) {
|
|
70
|
+
const statusCode = error.status || error.statusCode || error.status_code || error.output?.statusCode;
|
|
71
|
+
return statusCode ? parseInt(statusCode, 10) : 500;
|
|
72
|
+
}
|
|
73
|
+
/** Returns true if response code is internal server error */
|
|
74
|
+
function defaultShouldHandleError(error) {
|
|
75
|
+
const status = getStatusCodeFromResponse(error);
|
|
76
|
+
return status >= 500;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=express.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"express.js","sourceRoot":"","sources":["../src/express.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,+EAA+E;AAC/E,8EAA8E;AAC9E,oEAAoE;AACpE,EAAE;AACF,0EAA0E;AAC1E,iCAAiC;AACjC,EAAE;AACF,yDAAyD;AACzD,iJAAiJ;AAIjJ,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAC7F,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAgCpD;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAA+B;IACjE,OAAO,SAAS,qBAAqB,CACnC,KAAsB,EACtB,OAA6B,EAC7B,GAAwB,EACxB,IAAsC;QAEtC,MAAM,iBAAiB,GAAG,wBAAwB,CAAC,OAAO,CAAC,CAAC;QAC5D,iFAAiF;QACjF,oGAAoG;QACpG,iBAAiB,EAAE,CAAC,wBAAwB,CAAC,EAAE,iBAAiB,EAAE,CAAC,CAAC;QAEpE,MAAM,iBAAiB,GAAG,OAAO,EAAE,iBAAiB,IAAI,wBAAwB,CAAC;QAEjF,IAAI,iBAAiB,CAAC,KAAK,CAAC,EAAE,CAAC;YAC7B,MAAM,OAAO,GAAG,gBAAgB,CAAC,KAAK,EAAE;gBACtC,SAAS,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE;aAClD,CAAC,CAAC;YACF,GAA2B,CAAC,MAAM,GAAG,OAAO,CAAC;QAChD,CAAC;QAED,IAAI,CAAC,KAAK,CAAC,CAAC;IACd,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,qBAAqB;IAC5B,OAAO,SAAS,uBAAuB,CACrC,OAA6B,EAC7B,IAAyB,EACzB,IAAgB;QAEhB,MAAM,iBAAiB,GAAG,wBAAwB,CAAC,OAAO,CAAC,CAAC;QAC5D,iFAAiF;QACjF,iBAAiB,EAAE,CAAC,wBAAwB,CAAC,EAAE,iBAAiB,EAAE,CAAC,CAAC;QAEpE,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,wBAAwB,CACtC,GAAiF,EACjF,OAA+B;IAE/B,GAAG,CAAC,GAAG,CAAC,qBAAqB,EAAE,CAAC,CAAC;IACjC,GAAG,CAAC,GAAG,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC;IACtC,eAAe,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;AACtC,CAAC;AAED,SAAS,yBAAyB,CAAC,KAAsB;IACvD,MAAM,UAAU,GACd,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,UAAU,IAAI,KAAK,CAAC,WAAW,IAAI,KAAK,CAAC,MAAM,EAAE,UAAU,CAAC;IACpF,OAAO,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,UAAoB,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;AAC/D,CAAC;AAED,6DAA6D;AAC7D,SAAS,wBAAwB,CAAC,KAAsB;IACtD,MAAM,MAAM,GAAG,yBAAyB,CAAC,KAAK,CAAC,CAAC;IAChD,OAAO,MAAM,IAAI,GAAG,CAAC;AACvB,CAAC","sourcesContent":["/* eslint-disable jsdoc/check-param-names */\n// This is a fork of Sentry's Express integration from `@sentry/node`that's not\n// available in the `@sentry/node-core` package`. It has been lightly modified\n// to remove unused code and conform to PrairieLearn's coding style.\n//\n// See this package's `README.md` for more information about why we aren't\n// using `@sentry/node` directly.\n//\n// This was forked from the following file on 2025-07-30:\n// https://github.com/getsentry/sentry-javascript/blob/12ac49a9956fd1b64b3f8ad4b2b8f1da426a1efd/packages/node/src/integrations/tracing/express.ts\n\nimport type * as http from 'node:http';\n\nimport { captureException, getIsolationScope, httpRequestToRequestData } from '@sentry/core';\nimport { ensureIsWrapped } from '@sentry/node-core';\n\ninterface MiddlewareError extends Error {\n status?: number | string;\n statusCode?: number | string;\n status_code?: number | string;\n output?: {\n statusCode?: number | string;\n };\n}\n\ntype ExpressMiddleware = (\n req: http.IncomingMessage,\n res: http.ServerResponse,\n next: () => void,\n) => void;\n\ntype ExpressErrorMiddleware = (\n error: MiddlewareError,\n req: http.IncomingMessage,\n res: http.ServerResponse,\n next: (error: MiddlewareError) => void,\n) => void;\n\ninterface ExpressHandlerOptions {\n /**\n * Callback method deciding whether error should be captured and sent to Sentry\n * @param error Captured middleware error\n */\n shouldHandleError?(error: MiddlewareError): boolean;\n}\n\n/**\n * An Express-compatible error handler.\n */\nexport function expressErrorHandler(options?: ExpressHandlerOptions): ExpressErrorMiddleware {\n return function sentryErrorMiddleware(\n error: MiddlewareError,\n request: http.IncomingMessage,\n res: http.ServerResponse,\n next: (error: MiddlewareError) => void,\n ): void {\n const normalizedRequest = httpRequestToRequestData(request);\n // Ensure we use the express-enhanced request here, instead of the plain HTTP one\n // When an error happens, the `expressRequestHandler` middleware does not run, so we set it here too\n getIsolationScope().setSDKProcessingMetadata({ normalizedRequest });\n\n const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError;\n\n if (shouldHandleError(error)) {\n const eventId = captureException(error, {\n mechanism: { type: 'middleware', handled: false },\n });\n (res as { sentry?: string }).sentry = eventId;\n }\n\n next(error);\n };\n}\n\nfunction expressRequestHandler(): ExpressMiddleware {\n return function sentryRequestMiddleware(\n request: http.IncomingMessage,\n _res: http.ServerResponse,\n next: () => void,\n ): void {\n const normalizedRequest = httpRequestToRequestData(request);\n // Ensure we use the express-enhanced request here, instead of the plain HTTP one\n getIsolationScope().setSDKProcessingMetadata({ normalizedRequest });\n\n next();\n };\n}\n\n/**\n * Add an Express error handler to capture errors to Sentry.\n *\n * The error handler must be before any other middleware and after all controllers.\n *\n * @param app The Express instances\n * @param options {ExpressHandlerOptions} Configuration options for the handler\n *\n * @example\n * ```javascript\n * const Sentry = require('@sentry/node');\n * const express = require(\"express\");\n *\n * const app = express();\n *\n * // Add your routes, etc.\n *\n * // Add this after all routes,\n * // but before any and other error-handling middlewares are defined\n * Sentry.setupExpressErrorHandler(app);\n *\n * app.listen(3000);\n * ```\n */\nexport function setupExpressErrorHandler(\n app: { use: (middleware: ExpressMiddleware | ExpressErrorMiddleware) => unknown },\n options?: ExpressHandlerOptions,\n): void {\n app.use(expressRequestHandler());\n app.use(expressErrorHandler(options));\n ensureIsWrapped(app.use, 'express');\n}\n\nfunction getStatusCodeFromResponse(error: MiddlewareError): number {\n const statusCode =\n error.status || error.statusCode || error.status_code || error.output?.statusCode;\n return statusCode ? parseInt(statusCode as string, 10) : 500;\n}\n\n/** Returns true if response code is internal server error */\nfunction defaultShouldHandleError(error: MiddlewareError): boolean {\n const status = getStatusCodeFromResponse(error);\n return status >= 500;\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as Sentry from '@sentry/node';
|
|
1
|
+
import * as Sentry from '@sentry/node-core';
|
|
2
2
|
/**
|
|
3
3
|
* A thin wrapper around {@link Sentry.init} that automatically sets `release`
|
|
4
4
|
* based on the current Git revision.
|
|
@@ -18,5 +18,6 @@ export declare function init(options: Sentry.NodeOptions): Promise<void>;
|
|
|
18
18
|
* isolate requests and set request data for Sentry.
|
|
19
19
|
*/
|
|
20
20
|
export declare function requestHandler(): (req: any, _res: any, next: any) => void;
|
|
21
|
-
export type { Breadcrumb, BreadcrumbHint, Event, EventHint, Exception, NodeOptions, PolymorphicRequest, SdkInfo, Session, SeverityLevel, Span, StackFrame, Stacktrace, Thread, User, } from '@sentry/node';
|
|
22
|
-
export { addBreadcrumb, addEventProcessor, captureEvent, captureException, captureMessage, close, createTransport, defaultStackParser,
|
|
21
|
+
export type { Breadcrumb, BreadcrumbHint, Event, EventHint, Exception, NodeOptions, PolymorphicRequest, SdkInfo, Session, SeverityLevel, Span, StackFrame, Stacktrace, Thread, User, } from '@sentry/node-core';
|
|
22
|
+
export { addBreadcrumb, addEventProcessor, captureEvent, captureException, captureMessage, close, createTransport, defaultStackParser, flush, getCurrentScope, getSentryRelease, makeNodeTransport, NodeClient, Scope, SDK_VERSION, SentryContextManager, setContext, setExtra, setExtras, setTag, setTags, setUser, startInactiveSpan, startSpan, startSpanManual, withIsolationScope, withScope, } from '@sentry/node-core';
|
|
23
|
+
export { expressErrorHandler, setupExpressErrorHandler } from './express.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { httpRequestToRequestData, stripUrlQueryAndFragment } from '@sentry/core';
|
|
2
|
-
import * as Sentry from '@sentry/node';
|
|
2
|
+
import * as Sentry from '@sentry/node-core';
|
|
3
3
|
import { execa } from 'execa';
|
|
4
4
|
/**
|
|
5
5
|
* A thin wrapper around {@link Sentry.init} that automatically sets `release`
|
|
@@ -74,5 +74,6 @@ export function requestHandler() {
|
|
|
74
74
|
});
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
|
-
export { addBreadcrumb, addEventProcessor, captureEvent, captureException, captureMessage, close, createTransport, defaultStackParser,
|
|
77
|
+
export { addBreadcrumb, addEventProcessor, captureEvent, captureException, captureMessage, close, createTransport, defaultStackParser, flush, getCurrentScope, getSentryRelease, makeNodeTransport, NodeClient, Scope, SDK_VERSION, SentryContextManager, setContext, setExtra, setExtras, setTag, setTags, setUser, startInactiveSpan, startSpan, startSpanManual, withIsolationScope, withScope, } from '@sentry/node-core';
|
|
78
|
+
export { expressErrorHandler, setupExpressErrorHandler } from './express.js';
|
|
78
79
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAClF,OAAO,KAAK,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAClF,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAE9B;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,OAA2B;IACpD,IAAI,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAE9B,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,IAAI,CAAC;YACH,OAAO,GAAG,CAAC,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACtE,CAAC;QAAC,MAAM,CAAC;YACP,mEAAmE;YACnE,oCAAoC;QACtC,CAAC;IACH,CAAC;IAED,MAAM,CAAC,IAAI,CAAC;QACV,OAAO;QACP,GAAG,OAAO;KACX,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,SAAS,kBAAkB,CAAC,GAAQ;IAClC,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;IAC/C,MAAM,IAAI,GAAG,wBAAwB,CAAC,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC;IAExE,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,MAAM,EAAE,CAAC;QACX,IAAI,IAAI,MAAM,CAAC;IACjB,CAAC;IACD,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;QACnB,IAAI,IAAI,GAAG,CAAC;IACd,CAAC;IACD,IAAI,IAAI,EAAE,CAAC;QACT,IAAI,IAAI,IAAI,CAAC;IACf,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO,CAAC,GAAQ,EAAE,IAAS,EAAE,IAAS,EAAE,EAAE;QACxC,MAAM,CAAC,kBAAkB,CAAC,CAAC,KAAK,EAAE,EAAE;YAClC,KAAK,CAAC,iBAAiB,CAAC,CAAC,KAAK,EAAE,EAAE;gBAChC,kEAAkE;gBAClE,kEAAkE;gBAClE,oEAAoE;gBACpE,sBAAsB;gBACtB,IAAI,CAAC;oBACH,KAAK,CAAC,WAAW,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;oBAC5C,KAAK,CAAC,OAAO,GAAG,wBAAwB,CAAC,GAAG,CAAC,CAAC;oBAC9C,OAAO,KAAK,CAAC;gBACf,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,KAAK,CAAC;gBACf,CAAC;YACH,CAAC,CAAC,CAAC;YAEH,IAAI,EAAE,CAAC;QACT,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC;AAuBD,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,KAAK,EACL,eAAe,EACf,kBAAkB,EAClB,KAAK,EACL,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,UAAU,EACV,KAAK,EACL,WAAW,EACX,oBAAoB,EACpB,UAAU,EACV,QAAQ,EACR,SAAS,EACT,MAAM,EACN,OAAO,EACP,OAAO,EACP,iBAAiB,EACjB,SAAS,EACT,eAAe,EACf,kBAAkB,EAClB,SAAS,GACV,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,mBAAmB,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC","sourcesContent":["import { httpRequestToRequestData, stripUrlQueryAndFragment } from '@sentry/core';\nimport * as Sentry from '@sentry/node-core';\nimport { execa } from 'execa';\n\n/**\n * A thin wrapper around {@link Sentry.init} that automatically sets `release`\n * based on the current Git revision.\n */\nexport async function init(options: Sentry.NodeOptions) {\n let release = options.release;\n\n if (!release) {\n try {\n release = (await execa('git', ['rev-parse', 'HEAD'])).stdout.trim();\n } catch {\n // This most likely isn't running in an initialized git repository.\n // Default to not setting a release.\n }\n }\n\n Sentry.init({\n release,\n ...options,\n });\n}\n\n/**\n * Based on Sentry code that is not exported:\n * https://github.com/getsentry/sentry-javascript/blob/602703652959b581304a7849cb97117f296493bc/packages/utils/src/requestdata.ts#L102\n */\nfunction extractTransaction(req: any) {\n const method = req.method?.toUpperCase() || '';\n const path = stripUrlQueryAndFragment(req.originalUrl || req.url || '');\n\n let name = '';\n if (method) {\n name += method;\n }\n if (method && path) {\n name += ' ';\n }\n if (path) {\n name += path;\n }\n\n return name;\n}\n\n/**\n * Sentry v8 switched from simple, manual instrumentation to \"automatic\"\n * instrumentation based on OpenTelemetry. However, this interferes with\n * the way that our applications asynchronously load their configuration,\n * specifically the Sentry DSN. Sentry's automatic request isolation and\n * request data extraction requires that `Sentry.init` be called before\n * any other code is loaded, but our application startup structure is such\n * that we import most of our own code before we can load the Sentry DSN.\n *\n * Rather than jumping through hoops to restructure our application to\n * support this, this small function can be added as Express middleware to\n * isolate requests and set request data for Sentry.\n */\nexport function requestHandler() {\n return (req: any, _res: any, next: any) => {\n Sentry.withIsolationScope((scope) => {\n scope.addEventProcessor((event) => {\n // If an event processor throws an error, Sentry will catch it and\n // retrigger the event processor, which infinitely recurses. We'll\n // treat our event processor as a best-effort operation and silently\n // swallow any errors.\n try {\n event.transaction = extractTransaction(req);\n event.request = httpRequestToRequestData(req);\n return event;\n } catch {\n return event;\n }\n });\n\n next();\n });\n };\n}\n\n// We export every type and function from `@sentry/node` *except* for init,\n// which we replace with our own version up above.\n\nexport type {\n Breadcrumb,\n BreadcrumbHint,\n Event,\n EventHint,\n Exception,\n NodeOptions,\n PolymorphicRequest,\n SdkInfo,\n Session,\n SeverityLevel,\n Span,\n StackFrame,\n Stacktrace,\n Thread,\n User,\n} from '@sentry/node-core';\n\nexport {\n addBreadcrumb,\n addEventProcessor,\n captureEvent,\n captureException,\n captureMessage,\n close,\n createTransport,\n defaultStackParser,\n flush,\n getCurrentScope,\n getSentryRelease,\n makeNodeTransport,\n NodeClient,\n Scope,\n SDK_VERSION,\n SentryContextManager,\n setContext,\n setExtra,\n setExtras,\n setTag,\n setTags,\n setUser,\n startInactiveSpan,\n startSpan,\n startSpanManual,\n withIsolationScope,\n withScope,\n} from '@sentry/node-core';\n\nexport { expressErrorHandler, setupExpressErrorHandler } from './express.js';\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prairielearn/sentry",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
"dev": "tsc --watch --preserveWatchOutput"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@sentry/core": "^
|
|
17
|
-
"@sentry/node": "^
|
|
16
|
+
"@sentry/core": "^10.0.0",
|
|
17
|
+
"@sentry/node-core": "^10.0.0",
|
|
18
18
|
"execa": "^9.6.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@prairielearn/tsconfig": "^0.0.0",
|
|
22
|
-
"@types/node": "^22.
|
|
22
|
+
"@types/node": "^22.17.0",
|
|
23
23
|
"tsx": "^4.20.3",
|
|
24
|
-
"typescript": "^5.
|
|
24
|
+
"typescript": "^5.9.2"
|
|
25
25
|
}
|
|
26
26
|
}
|
package/src/express.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/* eslint-disable jsdoc/check-param-names */
|
|
2
|
+
// This is a fork of Sentry's Express integration from `@sentry/node`that's not
|
|
3
|
+
// available in the `@sentry/node-core` package`. It has been lightly modified
|
|
4
|
+
// to remove unused code and conform to PrairieLearn's coding style.
|
|
5
|
+
//
|
|
6
|
+
// See this package's `README.md` for more information about why we aren't
|
|
7
|
+
// using `@sentry/node` directly.
|
|
8
|
+
//
|
|
9
|
+
// This was forked from the following file on 2025-07-30:
|
|
10
|
+
// https://github.com/getsentry/sentry-javascript/blob/12ac49a9956fd1b64b3f8ad4b2b8f1da426a1efd/packages/node/src/integrations/tracing/express.ts
|
|
11
|
+
|
|
12
|
+
import type * as http from 'node:http';
|
|
13
|
+
|
|
14
|
+
import { captureException, getIsolationScope, httpRequestToRequestData } from '@sentry/core';
|
|
15
|
+
import { ensureIsWrapped } from '@sentry/node-core';
|
|
16
|
+
|
|
17
|
+
interface MiddlewareError extends Error {
|
|
18
|
+
status?: number | string;
|
|
19
|
+
statusCode?: number | string;
|
|
20
|
+
status_code?: number | string;
|
|
21
|
+
output?: {
|
|
22
|
+
statusCode?: number | string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type ExpressMiddleware = (
|
|
27
|
+
req: http.IncomingMessage,
|
|
28
|
+
res: http.ServerResponse,
|
|
29
|
+
next: () => void,
|
|
30
|
+
) => void;
|
|
31
|
+
|
|
32
|
+
type ExpressErrorMiddleware = (
|
|
33
|
+
error: MiddlewareError,
|
|
34
|
+
req: http.IncomingMessage,
|
|
35
|
+
res: http.ServerResponse,
|
|
36
|
+
next: (error: MiddlewareError) => void,
|
|
37
|
+
) => void;
|
|
38
|
+
|
|
39
|
+
interface ExpressHandlerOptions {
|
|
40
|
+
/**
|
|
41
|
+
* Callback method deciding whether error should be captured and sent to Sentry
|
|
42
|
+
* @param error Captured middleware error
|
|
43
|
+
*/
|
|
44
|
+
shouldHandleError?(error: MiddlewareError): boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* An Express-compatible error handler.
|
|
49
|
+
*/
|
|
50
|
+
export function expressErrorHandler(options?: ExpressHandlerOptions): ExpressErrorMiddleware {
|
|
51
|
+
return function sentryErrorMiddleware(
|
|
52
|
+
error: MiddlewareError,
|
|
53
|
+
request: http.IncomingMessage,
|
|
54
|
+
res: http.ServerResponse,
|
|
55
|
+
next: (error: MiddlewareError) => void,
|
|
56
|
+
): void {
|
|
57
|
+
const normalizedRequest = httpRequestToRequestData(request);
|
|
58
|
+
// Ensure we use the express-enhanced request here, instead of the plain HTTP one
|
|
59
|
+
// When an error happens, the `expressRequestHandler` middleware does not run, so we set it here too
|
|
60
|
+
getIsolationScope().setSDKProcessingMetadata({ normalizedRequest });
|
|
61
|
+
|
|
62
|
+
const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError;
|
|
63
|
+
|
|
64
|
+
if (shouldHandleError(error)) {
|
|
65
|
+
const eventId = captureException(error, {
|
|
66
|
+
mechanism: { type: 'middleware', handled: false },
|
|
67
|
+
});
|
|
68
|
+
(res as { sentry?: string }).sentry = eventId;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
next(error);
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function expressRequestHandler(): ExpressMiddleware {
|
|
76
|
+
return function sentryRequestMiddleware(
|
|
77
|
+
request: http.IncomingMessage,
|
|
78
|
+
_res: http.ServerResponse,
|
|
79
|
+
next: () => void,
|
|
80
|
+
): void {
|
|
81
|
+
const normalizedRequest = httpRequestToRequestData(request);
|
|
82
|
+
// Ensure we use the express-enhanced request here, instead of the plain HTTP one
|
|
83
|
+
getIsolationScope().setSDKProcessingMetadata({ normalizedRequest });
|
|
84
|
+
|
|
85
|
+
next();
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Add an Express error handler to capture errors to Sentry.
|
|
91
|
+
*
|
|
92
|
+
* The error handler must be before any other middleware and after all controllers.
|
|
93
|
+
*
|
|
94
|
+
* @param app The Express instances
|
|
95
|
+
* @param options {ExpressHandlerOptions} Configuration options for the handler
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```javascript
|
|
99
|
+
* const Sentry = require('@sentry/node');
|
|
100
|
+
* const express = require("express");
|
|
101
|
+
*
|
|
102
|
+
* const app = express();
|
|
103
|
+
*
|
|
104
|
+
* // Add your routes, etc.
|
|
105
|
+
*
|
|
106
|
+
* // Add this after all routes,
|
|
107
|
+
* // but before any and other error-handling middlewares are defined
|
|
108
|
+
* Sentry.setupExpressErrorHandler(app);
|
|
109
|
+
*
|
|
110
|
+
* app.listen(3000);
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export function setupExpressErrorHandler(
|
|
114
|
+
app: { use: (middleware: ExpressMiddleware | ExpressErrorMiddleware) => unknown },
|
|
115
|
+
options?: ExpressHandlerOptions,
|
|
116
|
+
): void {
|
|
117
|
+
app.use(expressRequestHandler());
|
|
118
|
+
app.use(expressErrorHandler(options));
|
|
119
|
+
ensureIsWrapped(app.use, 'express');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getStatusCodeFromResponse(error: MiddlewareError): number {
|
|
123
|
+
const statusCode =
|
|
124
|
+
error.status || error.statusCode || error.status_code || error.output?.statusCode;
|
|
125
|
+
return statusCode ? parseInt(statusCode as string, 10) : 500;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Returns true if response code is internal server error */
|
|
129
|
+
function defaultShouldHandleError(error: MiddlewareError): boolean {
|
|
130
|
+
const status = getStatusCodeFromResponse(error);
|
|
131
|
+
return status >= 500;
|
|
132
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { httpRequestToRequestData, stripUrlQueryAndFragment } from '@sentry/core';
|
|
2
|
-
import * as Sentry from '@sentry/node';
|
|
2
|
+
import * as Sentry from '@sentry/node-core';
|
|
3
3
|
import { execa } from 'execa';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -100,7 +100,7 @@ export type {
|
|
|
100
100
|
Stacktrace,
|
|
101
101
|
Thread,
|
|
102
102
|
User,
|
|
103
|
-
} from '@sentry/node';
|
|
103
|
+
} from '@sentry/node-core';
|
|
104
104
|
|
|
105
105
|
export {
|
|
106
106
|
addBreadcrumb,
|
|
@@ -111,8 +111,6 @@ export {
|
|
|
111
111
|
close,
|
|
112
112
|
createTransport,
|
|
113
113
|
defaultStackParser,
|
|
114
|
-
expressErrorHandler,
|
|
115
|
-
expressIntegration,
|
|
116
114
|
flush,
|
|
117
115
|
getCurrentScope,
|
|
118
116
|
getSentryRelease,
|
|
@@ -126,11 +124,12 @@ export {
|
|
|
126
124
|
setExtras,
|
|
127
125
|
setTag,
|
|
128
126
|
setTags,
|
|
129
|
-
setupExpressErrorHandler,
|
|
130
127
|
setUser,
|
|
131
128
|
startInactiveSpan,
|
|
132
129
|
startSpan,
|
|
133
130
|
startSpanManual,
|
|
134
131
|
withIsolationScope,
|
|
135
132
|
withScope,
|
|
136
|
-
} from '@sentry/node';
|
|
133
|
+
} from '@sentry/node-core';
|
|
134
|
+
|
|
135
|
+
export { expressErrorHandler, setupExpressErrorHandler } from './express.js';
|