@prairielearn/sentry 3.0.7 → 4.0.0

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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # @prairielearn/sentry
2
2
 
3
+ ## 4.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - a7d1ad9: Switch to using `@sentry/node-core`. The consumer must now ensure that OpenTelemetry is configured correctly.
8
+
9
+ ### Patch Changes
10
+
11
+ - 23adb05: Upgrade all JavaScript dependencies
12
+
3
13
  ## 3.0.7
4
14
 
5
15
  ### Patch Changes
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@prairielearn/sentry`
2
2
 
3
- Opinionated wrapper around `@sentry/node`. Currently, the only difference is that it exports an async `init` function that automatically sets the release to the current Git revision, if available.
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 {};
@@ -0,0 +1,77 @@
1
+ // This is a fork of Sentry's Express integration from `@sentry/node`that's not
2
+ // available in the `@sentry/node-core` package`. It has been lightly modified
3
+ // to remove unused code and conform to PrairieLearn's coding style.
4
+ //
5
+ // See this package's `README.md` for more information about why we aren't
6
+ // using `@sentry/node` directly.
7
+ //
8
+ // This was forked from the following file on 2025-07-30:
9
+ // https://github.com/getsentry/sentry-javascript/blob/12ac49a9956fd1b64b3f8ad4b2b8f1da426a1efd/packages/node/src/integrations/tracing/express.ts
10
+ import { captureException, getIsolationScope, httpRequestToRequestData } from '@sentry/core';
11
+ import { ensureIsWrapped } from '@sentry/node-core';
12
+ /**
13
+ * An Express-compatible error handler.
14
+ */
15
+ export function expressErrorHandler(options) {
16
+ return function sentryErrorMiddleware(error, request, res, next) {
17
+ const normalizedRequest = httpRequestToRequestData(request);
18
+ // Ensure we use the express-enhanced request here, instead of the plain HTTP one
19
+ // When an error happens, the `expressRequestHandler` middleware does not run, so we set it here too
20
+ getIsolationScope().setSDKProcessingMetadata({ normalizedRequest });
21
+ const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError;
22
+ if (shouldHandleError(error)) {
23
+ const eventId = captureException(error, {
24
+ mechanism: { type: 'middleware', handled: false },
25
+ });
26
+ res.sentry = eventId;
27
+ }
28
+ next(error);
29
+ };
30
+ }
31
+ function expressRequestHandler() {
32
+ return function sentryRequestMiddleware(request, _res, next) {
33
+ const normalizedRequest = httpRequestToRequestData(request);
34
+ // Ensure we use the express-enhanced request here, instead of the plain HTTP one
35
+ getIsolationScope().setSDKProcessingMetadata({ normalizedRequest });
36
+ next();
37
+ };
38
+ }
39
+ /**
40
+ * Add an Express error handler to capture errors to Sentry.
41
+ *
42
+ * The error handler must be before any other middleware and after all controllers.
43
+ *
44
+ * @param app The Express instances
45
+ * @param options {ExpressHandlerOptions} Configuration options for the handler
46
+ *
47
+ * @example
48
+ * ```javascript
49
+ * const Sentry = require('@sentry/node');
50
+ * const express = require("express");
51
+ *
52
+ * const app = express();
53
+ *
54
+ * // Add your routes, etc.
55
+ *
56
+ * // Add this after all routes,
57
+ * // but before any and other error-handling middlewares are defined
58
+ * Sentry.setupExpressErrorHandler(app);
59
+ *
60
+ * app.listen(3000);
61
+ * ```
62
+ */
63
+ export function setupExpressErrorHandler(app, options) {
64
+ app.use(expressRequestHandler());
65
+ app.use(expressErrorHandler(options));
66
+ ensureIsWrapped(app.use, 'express');
67
+ }
68
+ function getStatusCodeFromResponse(error) {
69
+ const statusCode = error.status || error.statusCode || error.status_code || error.output?.statusCode;
70
+ return statusCode ? parseInt(statusCode, 10) : 500;
71
+ }
72
+ /** Returns true if response code is internal server error */
73
+ function defaultShouldHandleError(error) {
74
+ const status = getStatusCodeFromResponse(error);
75
+ return status >= 500;
76
+ }
77
+ //# sourceMappingURL=express.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.js","sourceRoot":"","sources":["../src/express.ts"],"names":[],"mappings":"AAAA,+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":["// 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, expressErrorHandler, expressIntegration, flush, getCurrentScope, getSentryRelease, makeNodeTransport, NodeClient, Scope, SDK_VERSION, SentryContextManager, setContext, setExtra, setExtras, setTag, setTags, setupExpressErrorHandler, setUser, startInactiveSpan, startSpan, startSpanManual, withIsolationScope, withScope, } from '@sentry/node';
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, expressErrorHandler, expressIntegration, flush, getCurrentScope, getSentryRelease, makeNodeTransport, NodeClient, Scope, SDK_VERSION, SentryContextManager, setContext, setExtra, setExtras, setTag, setTags, setupExpressErrorHandler, setUser, startInactiveSpan, startSpan, startSpanManual, withIsolationScope, withScope, } from '@sentry/node';
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,cAAc,CAAC;AACvC,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,mBAAmB,EACnB,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,wBAAwB,EACxB,OAAO,EACP,iBAAiB,EACjB,SAAS,EACT,eAAe,EACf,kBAAkB,EAClB,SAAS,GACV,MAAM,cAAc,CAAC","sourcesContent":["import { httpRequestToRequestData, stripUrlQueryAndFragment } from '@sentry/core';\nimport * as Sentry from '@sentry/node';\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';\n\nexport {\n addBreadcrumb,\n addEventProcessor,\n captureEvent,\n captureException,\n captureMessage,\n close,\n createTransport,\n defaultStackParser,\n expressErrorHandler,\n expressIntegration,\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 setupExpressErrorHandler,\n setUser,\n startInactiveSpan,\n startSpan,\n startSpanManual,\n withIsolationScope,\n withScope,\n} from '@sentry/node';\n"]}
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.0.7",
3
+ "version": "4.0.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,13 +13,13 @@
13
13
  "dev": "tsc --watch --preserveWatchOutput"
14
14
  },
15
15
  "dependencies": {
16
- "@sentry/core": "^9.34.0",
17
- "@sentry/node": "^9.34.0",
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.15.34",
22
+ "@types/node": "^22.17.0",
23
23
  "tsx": "^4.20.3",
24
24
  "typescript": "^5.8.3"
25
25
  }
package/src/express.ts ADDED
@@ -0,0 +1,131 @@
1
+ // This is a fork of Sentry's Express integration from `@sentry/node`that's not
2
+ // available in the `@sentry/node-core` package`. It has been lightly modified
3
+ // to remove unused code and conform to PrairieLearn's coding style.
4
+ //
5
+ // See this package's `README.md` for more information about why we aren't
6
+ // using `@sentry/node` directly.
7
+ //
8
+ // This was forked from the following file on 2025-07-30:
9
+ // https://github.com/getsentry/sentry-javascript/blob/12ac49a9956fd1b64b3f8ad4b2b8f1da426a1efd/packages/node/src/integrations/tracing/express.ts
10
+
11
+ import type * as http from 'node:http';
12
+
13
+ import { captureException, getIsolationScope, httpRequestToRequestData } from '@sentry/core';
14
+ import { ensureIsWrapped } from '@sentry/node-core';
15
+
16
+ interface MiddlewareError extends Error {
17
+ status?: number | string;
18
+ statusCode?: number | string;
19
+ status_code?: number | string;
20
+ output?: {
21
+ statusCode?: number | string;
22
+ };
23
+ }
24
+
25
+ type ExpressMiddleware = (
26
+ req: http.IncomingMessage,
27
+ res: http.ServerResponse,
28
+ next: () => void,
29
+ ) => void;
30
+
31
+ type ExpressErrorMiddleware = (
32
+ error: MiddlewareError,
33
+ req: http.IncomingMessage,
34
+ res: http.ServerResponse,
35
+ next: (error: MiddlewareError) => void,
36
+ ) => void;
37
+
38
+ interface ExpressHandlerOptions {
39
+ /**
40
+ * Callback method deciding whether error should be captured and sent to Sentry
41
+ * @param error Captured middleware error
42
+ */
43
+ shouldHandleError?(error: MiddlewareError): boolean;
44
+ }
45
+
46
+ /**
47
+ * An Express-compatible error handler.
48
+ */
49
+ export function expressErrorHandler(options?: ExpressHandlerOptions): ExpressErrorMiddleware {
50
+ return function sentryErrorMiddleware(
51
+ error: MiddlewareError,
52
+ request: http.IncomingMessage,
53
+ res: http.ServerResponse,
54
+ next: (error: MiddlewareError) => void,
55
+ ): void {
56
+ const normalizedRequest = httpRequestToRequestData(request);
57
+ // Ensure we use the express-enhanced request here, instead of the plain HTTP one
58
+ // When an error happens, the `expressRequestHandler` middleware does not run, so we set it here too
59
+ getIsolationScope().setSDKProcessingMetadata({ normalizedRequest });
60
+
61
+ const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError;
62
+
63
+ if (shouldHandleError(error)) {
64
+ const eventId = captureException(error, {
65
+ mechanism: { type: 'middleware', handled: false },
66
+ });
67
+ (res as { sentry?: string }).sentry = eventId;
68
+ }
69
+
70
+ next(error);
71
+ };
72
+ }
73
+
74
+ function expressRequestHandler(): ExpressMiddleware {
75
+ return function sentryRequestMiddleware(
76
+ request: http.IncomingMessage,
77
+ _res: http.ServerResponse,
78
+ next: () => void,
79
+ ): void {
80
+ const normalizedRequest = httpRequestToRequestData(request);
81
+ // Ensure we use the express-enhanced request here, instead of the plain HTTP one
82
+ getIsolationScope().setSDKProcessingMetadata({ normalizedRequest });
83
+
84
+ next();
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Add an Express error handler to capture errors to Sentry.
90
+ *
91
+ * The error handler must be before any other middleware and after all controllers.
92
+ *
93
+ * @param app The Express instances
94
+ * @param options {ExpressHandlerOptions} Configuration options for the handler
95
+ *
96
+ * @example
97
+ * ```javascript
98
+ * const Sentry = require('@sentry/node');
99
+ * const express = require("express");
100
+ *
101
+ * const app = express();
102
+ *
103
+ * // Add your routes, etc.
104
+ *
105
+ * // Add this after all routes,
106
+ * // but before any and other error-handling middlewares are defined
107
+ * Sentry.setupExpressErrorHandler(app);
108
+ *
109
+ * app.listen(3000);
110
+ * ```
111
+ */
112
+ export function setupExpressErrorHandler(
113
+ app: { use: (middleware: ExpressMiddleware | ExpressErrorMiddleware) => unknown },
114
+ options?: ExpressHandlerOptions,
115
+ ): void {
116
+ app.use(expressRequestHandler());
117
+ app.use(expressErrorHandler(options));
118
+ ensureIsWrapped(app.use, 'express');
119
+ }
120
+
121
+ function getStatusCodeFromResponse(error: MiddlewareError): number {
122
+ const statusCode =
123
+ error.status || error.statusCode || error.status_code || error.output?.statusCode;
124
+ return statusCode ? parseInt(statusCode as string, 10) : 500;
125
+ }
126
+
127
+ /** Returns true if response code is internal server error */
128
+ function defaultShouldHandleError(error: MiddlewareError): boolean {
129
+ const status = getStatusCodeFromResponse(error);
130
+ return status >= 500;
131
+ }
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';