@voyant-travel/observability-sentry 0.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/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # @voyant-travel/observability-sentry
2
+
3
+ Sentry adapter for the Voyant observability `Reporter` seam (RFC [voyant#1553](https://github.com/voyant-travel/voyant/issues/1553)).
4
+
5
+ The framework owns the **mechanism** — a request id minted once and propagated
6
+ (`X-Request-Id` + `getRequestId()`), standard catch points (5xx boundary, auth
7
+ sub-app, module bootstrap, event-bus subscribers, scheduled jobs), and a
8
+ normalized `ErrorEvent` shape. This package is the **opt-in sink**: it forwards
9
+ those events to Sentry, tagging each with the same `requestId` the user sees, so
10
+ a support reference is a one-paste lookup in Sentry issue search.
11
+
12
+ ## Why an adapter and not a Sentry dependency
13
+
14
+ This package takes **no dependency on a Sentry SDK**. It binds to a structural
15
+ `SentryLike` interface, so you pass your already-initialized Sentry client —
16
+ `@sentry/cloudflare`, `@sentry/node`, `@sentry/bun`, `@sentry/browser`, any
17
+ version. Your deployment owns `init`/DSN/transport/sampling/PII scrubbing; the
18
+ adapter only maps the event and forwards it. The framework drains the buffered
19
+ event via `ctx.waitUntil` using the `flush()` the adapter returns — so you don't
20
+ hand-roll the Workers `flush`/`waitUntil` lifecycle that the RFC found broken in
21
+ three separate copies.
22
+
23
+ ## Usage
24
+
25
+ ```ts
26
+ import * as Sentry from "@sentry/cloudflare"
27
+ import { createApp } from "@voyant-travel/hono"
28
+ import { sentryReporter } from "@voyant-travel/observability-sentry"
29
+
30
+ const reporter = sentryReporter(Sentry)
31
+
32
+ const app = createApp({
33
+ reporter,
34
+ appName: "operator", // stamped on every event as the `app` tag
35
+ modules,
36
+ })
37
+ ```
38
+
39
+ That is the entire change — swap `consoleReporter()` (or the no-op default) for
40
+ `sentryReporter(Sentry)`. Every framework catch point now reaches Sentry.
41
+
42
+ ### What lands on the Sentry issue
43
+
44
+ | `ErrorEvent` field | Sentry mapping |
45
+ | --- | --- |
46
+ | `requestId` | tag `request_id` (omitted when empty, e.g. background jobs) |
47
+ | `app` | tag `app` |
48
+ | `error` | the captured exception (non-`Error` values are wrapped so they still group with a stack) |
49
+ | `context` | nested under the `voyant` context group |
50
+
51
+ ## Options
52
+
53
+ ```ts
54
+ sentryReporter(Sentry, {
55
+ requestIdTag: "request_id", // tag key for the correlation id
56
+ appTag: "app", // tag key for the logical app name
57
+ contextKey: "voyant", // context group the `ErrorEvent.context` nests under
58
+ flush: true, // default: flush when the client exposes flush() (Workers need it)
59
+ flushTimeoutMs: 2000, // flush timeout in ms
60
+ })
61
+ ```
62
+
63
+ On long-lived Node/Bun servers you can pass `flush: false` to let Sentry's own
64
+ background transport deliver events instead of flushing per capture.
65
+
66
+ ## License
67
+
68
+ Apache-2.0
@@ -0,0 +1,4 @@
1
+ export type { ErrorEvent, Reporter } from "@voyant-travel/hono/observability";
2
+ export type { SentryCaptureContext, SentryLike, SentryReporterOptions, } from "./sentry-reporter.js";
3
+ export { sentryReporter } from "./sentry-reporter.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,mCAAmC,CAAA;AAC7E,YAAY,EACV,oBAAoB,EACpB,UAAU,EACV,qBAAqB,GACtB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { sentryReporter } from "./sentry-reporter.js";
@@ -0,0 +1,71 @@
1
+ import type { Reporter } from "@voyant-travel/hono/observability";
2
+ /**
3
+ * The slice of a Sentry SDK this adapter calls. Declared structurally so the
4
+ * adapter binds to ANY Sentry flavour — `@sentry/cloudflare`, `@sentry/node`,
5
+ * `@sentry/bun`, `@sentry/browser` — and any version whose `captureException`
6
+ * accepts a capture-context hint, without this package taking a hard dependency
7
+ * on a specific Sentry SDK. The deployment owns `init`/DSN/transport/sampling;
8
+ * the adapter only maps the normalized event and forwards it. This is exactly
9
+ * the split RFC voyant#1553 asked for: the framework owns the catch points and
10
+ * event shape, the vendor SDK stays a deployment choice.
11
+ */
12
+ export interface SentryLike {
13
+ captureException(exception: unknown, hint?: SentryCaptureContext): string;
14
+ /**
15
+ * Optional. On Cloudflare Workers, Sentry buffers events and only delivers
16
+ * them on `flush`; when the client exposes it, the adapter returns
17
+ * `flush(timeout)` so the framework drains it via `ctx.waitUntil` after the
18
+ * response. Owning this lifecycle once is the point — the three hand-rolled
19
+ * `sentry.ts` copies the RFC cites each got the manual `flush`/`waitUntil`
20
+ * dance subtly wrong, and server-side Worker exceptions never delivered.
21
+ */
22
+ flush?(timeout?: number): Promise<boolean>;
23
+ }
24
+ /** The subset of Sentry's capture-context hint this adapter populates. */
25
+ export interface SentryCaptureContext {
26
+ tags?: Record<string, string>;
27
+ contexts?: Record<string, Record<string, unknown>>;
28
+ }
29
+ export interface SentryReporterOptions {
30
+ /**
31
+ * Tag key carrying the correlation id. Default `"request_id"`. This is the
32
+ * tag that makes a user-reported reference findable in Sentry — closing the
33
+ * RFC's root cause, where the id surfaced to the user never reached the event.
34
+ */
35
+ requestIdTag?: string;
36
+ /** Tag key carrying the logical app/worker name. Default `"app"`. */
37
+ appTag?: string;
38
+ /**
39
+ * Sentry context group the normalized `ErrorEvent.context` is nested under
40
+ * (shows as a labelled section on the issue). Default `"voyant"`.
41
+ */
42
+ contextKey?: string;
43
+ /**
44
+ * Flush after every capture. Workers must (events are buffered until flush);
45
+ * long-lived Node/Bun servers need not. Default: flush when the client
46
+ * exposes a `flush` method.
47
+ */
48
+ flush?: boolean;
49
+ /** Flush timeout in milliseconds. Default `2000` (matches Sentry's own). */
50
+ flushTimeoutMs?: number;
51
+ }
52
+ /**
53
+ * Build a {@link Reporter} backed by an already-initialized Sentry client.
54
+ *
55
+ * Register it once on `VoyantAppConfig.reporter` and every framework catch
56
+ * point (5xx boundary, auth sub-app, module bootstrap, event-bus subscribers,
57
+ * scheduled jobs) routes its {@link ErrorEvent} to Sentry — each tagged with
58
+ * the same `requestId` the user sees on `X-Request-Id`, so a support reference
59
+ * is a one-paste lookup in the Sentry issue search.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * import * as Sentry from "@sentry/cloudflare"
64
+ * import { sentryReporter } from "@voyant-travel/observability-sentry"
65
+ *
66
+ * const reporter = sentryReporter(Sentry)
67
+ * const app = createApp({ reporter, appName: "operator", modules })
68
+ * ```
69
+ */
70
+ export declare function sentryReporter(sentry: SentryLike, options?: SentryReporterOptions): Reporter;
71
+ //# sourceMappingURL=sentry-reporter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sentry-reporter.d.ts","sourceRoot":"","sources":["../src/sentry-reporter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAc,QAAQ,EAAE,MAAM,mCAAmC,CAAA;AAE7E;;;;;;;;;GASG;AACH,MAAM,WAAW,UAAU;IACzB,gBAAgB,CAAC,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,oBAAoB,GAAG,MAAM,CAAA;IACzE;;;;;;;OAOG;IACH,KAAK,CAAC,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CAC3C;AAED,0EAA0E;AAC1E,MAAM,WAAW,oBAAoB;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CACnD;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,GAAE,qBAA0B,GAAG,QAAQ,CA8BhG"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Build a {@link Reporter} backed by an already-initialized Sentry client.
3
+ *
4
+ * Register it once on `VoyantAppConfig.reporter` and every framework catch
5
+ * point (5xx boundary, auth sub-app, module bootstrap, event-bus subscribers,
6
+ * scheduled jobs) routes its {@link ErrorEvent} to Sentry — each tagged with
7
+ * the same `requestId` the user sees on `X-Request-Id`, so a support reference
8
+ * is a one-paste lookup in the Sentry issue search.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import * as Sentry from "@sentry/cloudflare"
13
+ * import { sentryReporter } from "@voyant-travel/observability-sentry"
14
+ *
15
+ * const reporter = sentryReporter(Sentry)
16
+ * const app = createApp({ reporter, appName: "operator", modules })
17
+ * ```
18
+ */
19
+ export function sentryReporter(sentry, options = {}) {
20
+ const requestIdTag = options.requestIdTag ?? "request_id";
21
+ const appTag = options.appTag ?? "app";
22
+ const contextKey = options.contextKey ?? "voyant";
23
+ const flushTimeoutMs = options.flushTimeoutMs ?? 2000;
24
+ const shouldFlush = options.flush ?? typeof sentry.flush === "function";
25
+ return {
26
+ captureException(event) {
27
+ const { requestId, app, error, context } = event;
28
+ const tags = { [appTag]: app };
29
+ // Only tag a non-empty id. Background/scheduled catch points emit an empty
30
+ // requestId (no request to correlate with), and an empty-string tag is
31
+ // searchable noise rather than a useful key.
32
+ if (requestId)
33
+ tags[requestIdTag] = requestId;
34
+ sentry.captureException(toError(error), {
35
+ tags,
36
+ contexts: context ? { [contextKey]: context } : undefined,
37
+ });
38
+ if (shouldFlush && sentry.flush) {
39
+ // Handed back to the framework, which drains it via `waitUntil` so the
40
+ // buffered event delivers without blocking the response. `.then`
41
+ // collapses the boolean to the `void` the Reporter contract expects.
42
+ return sentry.flush(flushTimeoutMs).then(() => { });
43
+ }
44
+ },
45
+ };
46
+ }
47
+ /**
48
+ * Sentry produces the richest issue (stack frames, grouping) from a real
49
+ * `Error`. Pass `Error` instances through untouched; wrap anything else so a
50
+ * thrown string/object still yields a grouped, stack-bearing issue instead of
51
+ * Sentry's bare "Non-Error exception captured" placeholder.
52
+ */
53
+ function toError(error) {
54
+ if (error instanceof Error)
55
+ return error;
56
+ if (typeof error === "string")
57
+ return new Error(error);
58
+ try {
59
+ return new Error(`Non-Error thrown: ${JSON.stringify(error)}`);
60
+ }
61
+ catch {
62
+ // Circular or otherwise un-serializable value.
63
+ return new Error(`Non-Error thrown: ${String(error)}`);
64
+ }
65
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@voyant-travel/observability-sentry",
3
+ "version": "0.0.0",
4
+ "license": "Apache-2.0",
5
+ "type": "module",
6
+ "description": "Sentry adapter for the Voyant observability Reporter seam (RFC voyant#1553).",
7
+ "exports": {
8
+ ".": "./src/index.ts"
9
+ },
10
+ "scripts": {
11
+ "typecheck": "tsc --noEmit",
12
+ "lint": "biome check src/",
13
+ "test": "vitest run",
14
+ "build": "tsc -p tsconfig.json",
15
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
16
+ "prepack": "pnpm run build"
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js",
27
+ "default": "./dist/index.js"
28
+ }
29
+ },
30
+ "main": "./dist/index.js",
31
+ "types": "./dist/index.d.ts"
32
+ },
33
+ "dependencies": {},
34
+ "peerDependencies": {
35
+ "@voyant-travel/hono": "workspace:^"
36
+ },
37
+ "devDependencies": {
38
+ "@voyant-travel/hono": "workspace:^",
39
+ "@voyant-travel/voyant-typescript-config": "workspace:^",
40
+ "typescript": "^6.0.2",
41
+ "vitest": "^4.1.2"
42
+ },
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "https://github.com/voyant-travel/voyant.git",
46
+ "directory": "packages/observability-sentry"
47
+ }
48
+ }