@weatherboard/errors 0.1.0 → 0.2.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/README.md +39 -2
- package/http.d.ts +15 -0
- package/http.js +60 -0
- package/package.json +13 -3
- package/verify.d.ts +1 -0
- package/verify.js +4 -1
- package/worker-verify.d.ts +4 -0
- package/worker-verify.js +77 -0
- package/worker.d.ts +22 -0
- package/worker.js +70 -0
package/README.md
CHANGED
|
@@ -8,8 +8,10 @@ Entry points:
|
|
|
8
8
|
- `./next/client`: deferred browser initialization and `onRouterTransitionStart`. Buffers early errors and rejections, loading immediately on the first error; otherwise waits for load and idle. Breadcrumbs before SDK initialization are unavailable.
|
|
9
9
|
- `./next/global-error`: `GlobalError` reports render failures and renders its required `children`; the product supplies the fallback document and design.
|
|
10
10
|
- `./client`: `reportHandled(error, { where, extra })` for catches that show a banner or toast. Fingerprints use `where` and message. Never throws; before the deferred browser SDK loads, queues the error and its context and requests the SDK immediately. With no configured loader or a disabled SDK, it is a no-op. The `where` tag identifies the catch site.
|
|
11
|
+
- `./http`: `refuse(status, message, { kind, where, context?, body?, headers? })` returns `{ error: message, ...body }`. Expected refusals add a breadcrumb; invariant refusals capture a grouped warning. `fail(message, cause, { where, status = 500, context?, body?, headers? })` captures the original exception and returns only the supplied public message/body. Both tolerate reporting failures and skip telemetry before SDK initialization. Context redacts sensitive keys, bearer strings, known token formats and email addresses.
|
|
11
12
|
- `./noise`: shared patterns with their justifying issue ids.
|
|
12
|
-
- `./
|
|
13
|
+
- `./worker`: `withWorkerErrors`, `withDurableObjectErrors` and Worker `reportHandled`.
|
|
14
|
+
- `./verify`: `verifyErrors(appDirectory)` or `verifyWorkerWiring(workerDirectory)` registers the product wiring test.
|
|
13
15
|
|
|
14
16
|
The five configuration variables are `SENTRY_DSN` (server), `PUBLIC_SENTRY_DSN` (browser), `SENTRY_ORG`, `SENTRY_PROJECT`, and `SENTRY_AUTH_TOKEN` (secret, build only). The wrapper inlines only the public DSN. Reporting requires a DSN and `VERCEL_ENV=production` or `preview`; development and unset environments do not initialize. Release comes from `VERCEL_GIT_COMMIT_SHA`, never `NODE_ENV`. Tracing is off and default PII is disabled.
|
|
15
17
|
|
|
@@ -20,4 +22,39 @@ Wire a Next app in four files:
|
|
|
20
22
|
3. In `instrumentation-client.ts`: export `{ onRouterTransitionStart }` from `@weatherboard/errors/next/client`.
|
|
21
23
|
4. In the app's global error boundary, mark it `"use client"`, import `{ GlobalError }` from `@weatherboard/errors/next/global-error`, and render `<GlobalError error={error}><html><body>your fallback</body></html></GlobalError>`.
|
|
22
24
|
|
|
23
|
-
Add a node:test file calling `verifyErrors` with the app directory.
|
|
25
|
+
Add a node:test file calling `verifyErrors` with the app directory. React Native is outside this version. Never pass a private cause message as the public HTTP message. Extra body fields retain their existing wire shape; they are not telemetry context.
|
|
26
|
+
|
|
27
|
+
## Cloudflare Workers
|
|
28
|
+
|
|
29
|
+
Import from `@weatherboard/errors/worker`. Wrap the default handler with
|
|
30
|
+
`withWorkerErrors(handler)`. Export each Durable Object as
|
|
31
|
+
`const MyObject = withDurableObjectErrors(MyObjectImplementation)` and an instance
|
|
32
|
+
type alias when bindings reference that type. Built-in handlers, alarms, WebSocket
|
|
33
|
+
handlers and prototype RPC methods are instrumented by `@sentry/cloudflare`.
|
|
34
|
+
`verifyWorkerWiring(dir)` checks the defaults, DO classes and direct SDK imports.
|
|
35
|
+
|
|
36
|
+
Optional environment fields (extend `WorkerErrorEnv` in the Worker's typed Env):
|
|
37
|
+
|
|
38
|
+
| Field | Purpose |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| `SENTRY_DSN` | Worker secret; absent means the original invocation/construction path runs without Sentry |
|
|
41
|
+
| `SENTRY_ENVIRONMENT` | Explicit reporting environment |
|
|
42
|
+
| `ENVIRONMENT` | Existing per-environment Worker var, used when the explicit field is absent |
|
|
43
|
+
| `SENTRY_RELEASE` | Git SHA recorded by the deploy; takes precedence over version metadata |
|
|
44
|
+
| `CF_VERSION_METADATA` | Cloudflare `version_metadata` binding; when `SENTRY_RELEASE` is absent, release uses `tag`, then `id` if untagged (a platform version ID, not a git SHA) |
|
|
45
|
+
|
|
46
|
+
A Worker deployed per environment reads its own environment var. A Worker shared
|
|
47
|
+
by every environment must take the decision from its caller through a trusted
|
|
48
|
+
mapping, or treat absence as non-production; never infer production from the
|
|
49
|
+
shared Worker's deployment. Unset environment becomes `development`. Reporting is
|
|
50
|
+
enabled by a DSN; development is not promoted to production or silently discarded.
|
|
51
|
+
No DSN leaves the handler's arguments, receiver, result and thrown errors unchanged.
|
|
52
|
+
The wrapper selects the original handler at invocation time because Worker secrets
|
|
53
|
+
are unavailable at module evaluation. DO construction similarly bypasses the SDK.
|
|
54
|
+
|
|
55
|
+
Tracing is disabled (`tracesSampleRate: 0`), default PII is disabled
|
|
56
|
+
(`sendDefaultPii: false`), and the SDK flushes via `waitUntil`; responses do not wait
|
|
57
|
+
for reporting. In catches that recover, keep the existing log and call
|
|
58
|
+
`reportHandled(error, { where, extra })` inside the instrumented invocation. It
|
|
59
|
+
fingerprints by `where` and message and never throws. Only send scrubbed errors and
|
|
60
|
+
safe extras: default PII settings do not scrub application-supplied exception text.
|
package/http.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type Options = {
|
|
2
|
+
where: string;
|
|
3
|
+
context?: Record<string, unknown>;
|
|
4
|
+
body?: Record<string, unknown>;
|
|
5
|
+
headers?: HeadersInit;
|
|
6
|
+
};
|
|
7
|
+
/** A valid refusal is a breadcrumb; an impossible refusal is a grouped warning. */
|
|
8
|
+
export declare function refuse(status: number, message: string, options: Options & {
|
|
9
|
+
kind: "expected" | "invariant";
|
|
10
|
+
}): Response;
|
|
11
|
+
/** Report the original cause; only the public message and body are sent to the caller. */
|
|
12
|
+
export declare function fail(message: string, cause: unknown, options: Options & {
|
|
13
|
+
status?: number;
|
|
14
|
+
}): Response;
|
|
15
|
+
export {};
|
package/http.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as SentryModule from "@sentry/nextjs";
|
|
2
|
+
const Sentry = SentryModule.default ?? SentryModule;
|
|
3
|
+
function scrub(value, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
|
|
4
|
+
if (typeof value === "string") return value.replace(/\bBearer\s+\S+/gi, "[redacted]").replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "[redacted]").replace(/\b(?:gh[pousr]_[\w]+|github_pat_[\w]+|sk-[\w-]+|eyJ[\w-]+\.[\w-]+\.[\w-]+)\b/g, "[redacted]");
|
|
5
|
+
if (value === null || typeof value !== "object") return value;
|
|
6
|
+
if (depth >= 8 || seen.has(value)) return "[redacted]";
|
|
7
|
+
seen.add(value);
|
|
8
|
+
if (Array.isArray(value)) return value.map((item) => scrub(item, seen, depth + 1));
|
|
9
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
10
|
+
key,
|
|
11
|
+
/token|secret|password|authorization|cookie|email|api.?key|credential/i.test(key) ? "[redacted]" : scrub(item, seen, depth + 1)
|
|
12
|
+
]));
|
|
13
|
+
}
|
|
14
|
+
function template(message) {
|
|
15
|
+
return String(scrub(message)).replace(/\b[0-9a-f]{8}-[0-9a-f-]{27,}\b/gi, "<id>").replace(/\b[a-z]+_[a-z0-9-]+\b/gi, "<id>").replace(/\b[0-9a-f]{8,}\b/gi, "<id>").replace(/\d+/g, "<n>");
|
|
16
|
+
}
|
|
17
|
+
function response(message, status, options) {
|
|
18
|
+
try {
|
|
19
|
+
return Response.json({ error: message, ...options.body }, { status, headers: options.headers });
|
|
20
|
+
} catch {
|
|
21
|
+
return Response.json({ error: message }, { status: status >= 200 && status <= 599 && ![204, 205, 304].includes(status) ? status : 500 });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function refuse(status, message, options) {
|
|
25
|
+
try {
|
|
26
|
+
if (Sentry.isInitialized() && Sentry.isEnabled()) {
|
|
27
|
+
const extra = scrub(options.context);
|
|
28
|
+
const safeMessage = String(scrub(message));
|
|
29
|
+
if (options.kind === "expected") Sentry.addBreadcrumb({
|
|
30
|
+
category: "refuse",
|
|
31
|
+
level: "info",
|
|
32
|
+
message: safeMessage,
|
|
33
|
+
data: { ...extra, where: options.where, status }
|
|
34
|
+
});
|
|
35
|
+
else Sentry.captureMessage(safeMessage, {
|
|
36
|
+
level: "warning",
|
|
37
|
+
tags: { where: options.where },
|
|
38
|
+
extra,
|
|
39
|
+
fingerprint: ["refuse", options.where, template(message)]
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
return response(message, status, options);
|
|
45
|
+
}
|
|
46
|
+
function fail(message, cause, options) {
|
|
47
|
+
try {
|
|
48
|
+
if (Sentry.isInitialized() && Sentry.isEnabled()) Sentry.captureException(cause, {
|
|
49
|
+
level: "error",
|
|
50
|
+
tags: { where: options.where },
|
|
51
|
+
extra: scrub(options.context)
|
|
52
|
+
});
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
return response(message, options.status ?? 500, options);
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
fail,
|
|
59
|
+
refuse
|
|
60
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weatherboard/errors",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Shared Next.js and
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Shared Next.js, browser, HTTP and Cloudflare Worker error reporting for Weatherboard products.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@sentry/
|
|
13
|
+
"@sentry/cloudflare": "10.75.0",
|
|
14
|
+
"@sentry/nextjs": "10.75.0",
|
|
15
|
+
"typescript": "~5.7.2"
|
|
14
16
|
},
|
|
15
17
|
"peerDependencies": {
|
|
16
18
|
"next": "^16.0.0",
|
|
@@ -41,6 +43,10 @@
|
|
|
41
43
|
"types": "./client.d.ts",
|
|
42
44
|
"import": "./client.js"
|
|
43
45
|
},
|
|
46
|
+
"./http": {
|
|
47
|
+
"types": "./http.d.ts",
|
|
48
|
+
"import": "./http.js"
|
|
49
|
+
},
|
|
44
50
|
"./noise": {
|
|
45
51
|
"types": "./noise.d.ts",
|
|
46
52
|
"import": "./noise.js"
|
|
@@ -48,6 +54,10 @@
|
|
|
48
54
|
"./verify": {
|
|
49
55
|
"types": "./verify.d.ts",
|
|
50
56
|
"import": "./verify.js"
|
|
57
|
+
},
|
|
58
|
+
"./worker": {
|
|
59
|
+
"types": "./worker.d.ts",
|
|
60
|
+
"import": "./worker.js"
|
|
51
61
|
}
|
|
52
62
|
},
|
|
53
63
|
"files": [
|
package/verify.d.ts
CHANGED
|
@@ -2,3 +2,4 @@
|
|
|
2
2
|
export declare function assertErrorWiring(appDirectory: string): void;
|
|
3
3
|
/** Call from a product's node:test file with the absolute app directory. */
|
|
4
4
|
export declare function verifyErrors(appDirectory: string): void;
|
|
5
|
+
export { assertWorkerWiring, verifyWorkerWiring } from "./worker-verify.js";
|
package/verify.js
CHANGED
|
@@ -27,7 +27,10 @@ function assertErrorWiring(appDirectory) {
|
|
|
27
27
|
function verifyErrors(appDirectory) {
|
|
28
28
|
test("shared error reporting wiring", () => assertErrorWiring(appDirectory));
|
|
29
29
|
}
|
|
30
|
+
import { assertWorkerWiring, verifyWorkerWiring } from "./worker-verify.js";
|
|
30
31
|
export {
|
|
31
32
|
assertErrorWiring,
|
|
32
|
-
|
|
33
|
+
assertWorkerWiring,
|
|
34
|
+
verifyErrors,
|
|
35
|
+
verifyWorkerWiring
|
|
33
36
|
};
|
package/worker-verify.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
import { test } from "node:test";
|
|
5
|
+
import ts from "typescript";
|
|
6
|
+
function sources(dir) {
|
|
7
|
+
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
8
|
+
if (["node_modules", ".git", "dist", "test-fixtures"].includes(entry.name)) return [];
|
|
9
|
+
const path = join(dir, entry.name);
|
|
10
|
+
if (entry.isDirectory()) return sources(path);
|
|
11
|
+
return /\.[cm]?[jt]s$/.test(entry.name) && !/\.(test|spec)\./.test(entry.name) && !entry.name.endsWith(".d.ts") ? [path] : [];
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
function assertWorkerWiring(dir) {
|
|
15
|
+
let defaults = 0;
|
|
16
|
+
const classes = [];
|
|
17
|
+
const wrapped = /* @__PURE__ */ new Set();
|
|
18
|
+
for (const file of sources(dir)) {
|
|
19
|
+
const name = relative(dir, file);
|
|
20
|
+
const source = ts.createSourceFile(file, readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true);
|
|
21
|
+
const imports = /* @__PURE__ */ new Map();
|
|
22
|
+
const visit = (node) => {
|
|
23
|
+
if (ts.isStringLiteral(node) && node.text.startsWith("@sentry/")) {
|
|
24
|
+
const parent = node.parent;
|
|
25
|
+
if (ts.isImportDeclaration(parent) || ts.isExportDeclaration(parent) || ts.isCallExpression(parent)) {
|
|
26
|
+
assert.fail(`${name}: products never import @sentry/* directly`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "@weatherboard/errors/worker") {
|
|
30
|
+
const bindings = node.importClause?.namedBindings;
|
|
31
|
+
if (bindings && ts.isNamedImports(bindings)) {
|
|
32
|
+
for (const binding of bindings.elements) imports.set(binding.name.text, binding.propertyName?.text ?? binding.name.text);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
ts.forEachChild(node, visit);
|
|
36
|
+
};
|
|
37
|
+
visit(source);
|
|
38
|
+
const isWrapper = (expression, kind) => ts.isCallExpression(expression) && ts.isIdentifier(expression.expression) && imports.get(expression.expression.text) === kind;
|
|
39
|
+
for (const statement of source.statements) {
|
|
40
|
+
if (name.startsWith("src/") && ts.isExportAssignment(statement) && !statement.isExportEquals) {
|
|
41
|
+
defaults++;
|
|
42
|
+
assert.ok(isWrapper(statement.expression, "withWorkerErrors"), `${name}: default export must be wrapped with withWorkerErrors`);
|
|
43
|
+
}
|
|
44
|
+
if (ts.isClassDeclaration(statement) && statement.name) {
|
|
45
|
+
const base = statement.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword)?.types[0]?.expression;
|
|
46
|
+
if (base && ts.isIdentifier(base)) classes.push({ name: statement.name.text, base: base.text, file: name });
|
|
47
|
+
}
|
|
48
|
+
if (ts.isVariableStatement(statement) && statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
49
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
50
|
+
if (declaration.initializer && isWrapper(declaration.initializer, "withDurableObjectErrors")) {
|
|
51
|
+
const arg = declaration.initializer.arguments[0];
|
|
52
|
+
if (arg && ts.isIdentifier(arg)) wrapped.add(`${name}:${arg.text}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
assert.ok(defaults > 0, "Worker must have a wrapped default export");
|
|
59
|
+
const durable = /* @__PURE__ */ new Set(["DurableObject", "Sandbox"]);
|
|
60
|
+
for (let changed = true; changed; ) {
|
|
61
|
+
changed = false;
|
|
62
|
+
for (const item of classes) if (durable.has(item.base) && !durable.has(item.name)) {
|
|
63
|
+
durable.add(item.name);
|
|
64
|
+
changed = true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const item of classes) if (durable.has(item.base)) {
|
|
68
|
+
assert.ok(wrapped.has(`${item.file}:${item.name}`), `${item.file}: Durable Object ${item.name} must be instrumented with withDurableObjectErrors`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function verifyWorkerWiring(dir) {
|
|
72
|
+
test("shared Worker error reporting wiring", () => assertWorkerWiring(dir));
|
|
73
|
+
}
|
|
74
|
+
export {
|
|
75
|
+
assertWorkerWiring,
|
|
76
|
+
verifyWorkerWiring
|
|
77
|
+
};
|
package/worker.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type CloudflareOptions } from "@sentry/cloudflare";
|
|
2
|
+
import type { HandledContext } from "./client.js";
|
|
3
|
+
/** Shared Workers must supply caller-derived environment explicitly; unset is development. */
|
|
4
|
+
export interface WorkerErrorEnv {
|
|
5
|
+
SENTRY_DSN?: string;
|
|
6
|
+
SENTRY_ENVIRONMENT?: string;
|
|
7
|
+
ENVIRONMENT?: string;
|
|
8
|
+
SENTRY_RELEASE?: string;
|
|
9
|
+
CF_VERSION_METADATA?: {
|
|
10
|
+
id: string;
|
|
11
|
+
tag?: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
type Options<E> = (env: E) => CloudflareOptions;
|
|
15
|
+
export declare function workerErrorOptions(env: WorkerErrorEnv): CloudflareOptions;
|
|
16
|
+
/** Select instrumentation per invocation, without modifying the original handler. */
|
|
17
|
+
export declare function withWorkerErrors<H extends object>(handler: H, options?: Options<WorkerErrorEnv>): H;
|
|
18
|
+
/** Preserve the original construction path when a DSN is absent. */
|
|
19
|
+
export declare function withDurableObjectErrors<C extends new (...args: never[]) => object>(object: C, options?: Options<WorkerErrorEnv>): C;
|
|
20
|
+
/** Report inside an instrumented invocation's catch; logging stays at the call site. */
|
|
21
|
+
export declare function reportHandled(error: unknown, context: HandledContext): void;
|
|
22
|
+
export {};
|
package/worker.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
captureException,
|
|
3
|
+
getClient,
|
|
4
|
+
instrumentDurableObjectWithSentry,
|
|
5
|
+
withSentry,
|
|
6
|
+
wrapRequestHandler
|
|
7
|
+
} from "@sentry/cloudflare";
|
|
8
|
+
function workerErrorOptions(env) {
|
|
9
|
+
return {
|
|
10
|
+
dsn: env.SENTRY_DSN,
|
|
11
|
+
environment: env.SENTRY_ENVIRONMENT || env.ENVIRONMENT || "development",
|
|
12
|
+
release: env.SENTRY_RELEASE || env.CF_VERSION_METADATA?.tag || env.CF_VERSION_METADATA?.id,
|
|
13
|
+
tracesSampleRate: 0,
|
|
14
|
+
sendDefaultPii: false,
|
|
15
|
+
// On SDK 10.75, enableRpcTracePropagation alone captures only calls carrying
|
|
16
|
+
// SDK metadata. Manager-side callers do not carry it; instrument all methods.
|
|
17
|
+
instrumentPrototypeMethods: true
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function withWorkerErrors(handler, options = workerErrorOptions) {
|
|
21
|
+
let instrumented;
|
|
22
|
+
const methods = /* @__PURE__ */ new Map();
|
|
23
|
+
const wrapped = new Proxy(handler, {
|
|
24
|
+
get(target, key, receiver) {
|
|
25
|
+
const original = Reflect.get(target, key, receiver);
|
|
26
|
+
if (typeof original !== "function") return original;
|
|
27
|
+
if (!methods.has(key)) methods.set(key, new Proxy(original, {
|
|
28
|
+
apply(method, thisArg, args) {
|
|
29
|
+
const receiver2 = thisArg === wrapped ? handler : thisArg;
|
|
30
|
+
const env = args[1];
|
|
31
|
+
if (!env?.SENTRY_DSN) return Reflect.apply(method, receiver2, args);
|
|
32
|
+
if (key === "fetch" && args[0] instanceof Request && ["HEAD", "OPTIONS"].includes(args[0].method)) {
|
|
33
|
+
return wrapRequestHandler({ options: options(env), request: args[0], context: args[2] }, () => Reflect.apply(method, receiver2, args));
|
|
34
|
+
}
|
|
35
|
+
instrumented ??= withSentry(options, { ...handler });
|
|
36
|
+
const captured = Reflect.get(instrumented, key);
|
|
37
|
+
return Reflect.apply(typeof captured === "function" ? captured : method, receiver2, args);
|
|
38
|
+
}
|
|
39
|
+
}));
|
|
40
|
+
return methods.get(key);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return wrapped;
|
|
44
|
+
}
|
|
45
|
+
function withDurableObjectErrors(object, options = workerErrorOptions) {
|
|
46
|
+
let instrumented;
|
|
47
|
+
return new Proxy(object, {
|
|
48
|
+
construct(target, args, newTarget) {
|
|
49
|
+
const env = args[1];
|
|
50
|
+
if (!env?.SENTRY_DSN) return Reflect.construct(target, args, newTarget);
|
|
51
|
+
instrumented ??= instrumentDurableObjectWithSentry(options, object);
|
|
52
|
+
return Reflect.construct(instrumented, args, newTarget);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function reportHandled(error, context) {
|
|
57
|
+
try {
|
|
58
|
+
const client = getClient();
|
|
59
|
+
if (!client?.getOptions().dsn || client.getOptions().enabled === false) return;
|
|
60
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
61
|
+
captureException(error, { fingerprint: [context.where, message], extra: context.extra });
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export {
|
|
66
|
+
reportHandled,
|
|
67
|
+
withDurableObjectErrors,
|
|
68
|
+
withWorkerErrors,
|
|
69
|
+
workerErrorOptions
|
|
70
|
+
};
|