@schedulespark/observability 0.0.2-beta-1-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.
Files changed (115) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +21 -0
  3. package/README.md +384 -0
  4. package/SECURITY_REVIEW.md +90 -0
  5. package/dist/alerts/dispatch.d.ts +6 -0
  6. package/dist/alerts/dispatch.js +11 -0
  7. package/dist/alerts/email.d.ts +37 -0
  8. package/dist/alerts/email.js +29 -0
  9. package/dist/alerts/format.d.ts +12 -0
  10. package/dist/alerts/format.js +30 -0
  11. package/dist/alerts/index.d.ts +8 -0
  12. package/dist/alerts/index.js +4 -0
  13. package/dist/alerts/notifier.d.ts +15 -0
  14. package/dist/alerts/notifier.js +31 -0
  15. package/dist/alerts/spike.d.ts +25 -0
  16. package/dist/alerts/spike.js +41 -0
  17. package/dist/alerts/types.d.ts +38 -0
  18. package/dist/alerts/types.js +1 -0
  19. package/dist/alerts/webhook.d.ts +14 -0
  20. package/dist/alerts/webhook.js +59 -0
  21. package/dist/browser/client.d.ts +26 -0
  22. package/dist/browser/client.js +131 -0
  23. package/dist/browser/index.d.ts +3 -0
  24. package/dist/browser/index.js +1 -0
  25. package/dist/cli/bin.d.ts +2 -0
  26. package/dist/cli/bin.js +6 -0
  27. package/dist/cli/index.d.ts +10 -0
  28. package/dist/cli/index.js +45 -0
  29. package/dist/cli/migrate.d.ts +6 -0
  30. package/dist/cli/migrate.js +14 -0
  31. package/dist/cli/options.d.ts +17 -0
  32. package/dist/cli/options.js +28 -0
  33. package/dist/cli/projects.d.ts +6 -0
  34. package/dist/cli/projects.js +52 -0
  35. package/dist/cli/prune.d.ts +8 -0
  36. package/dist/cli/prune.js +32 -0
  37. package/dist/cli/rollup.d.ts +6 -0
  38. package/dist/cli/rollup.js +24 -0
  39. package/dist/cli/serve.d.ts +5 -0
  40. package/dist/cli/serve.js +72 -0
  41. package/dist/cli/sourcemap.d.ts +8 -0
  42. package/dist/cli/sourcemap.js +113 -0
  43. package/dist/core/breadcrumbs.d.ts +21 -0
  44. package/dist/core/breadcrumbs.js +20 -0
  45. package/dist/core/fingerprint.d.ts +10 -0
  46. package/dist/core/fingerprint.js +35 -0
  47. package/dist/core/index.d.ts +4 -0
  48. package/dist/core/index.js +4 -0
  49. package/dist/core/types.d.ts +219 -0
  50. package/dist/core/types.js +35 -0
  51. package/dist/dashboard/core.d.ts +67 -0
  52. package/dist/dashboard/core.js +40 -0
  53. package/dist/dashboard/fastify.d.ts +17 -0
  54. package/dist/dashboard/fastify.js +247 -0
  55. package/dist/dashboard/html-secondary.d.ts +15 -0
  56. package/dist/dashboard/html-secondary.js +85 -0
  57. package/dist/dashboard/html-shared.d.ts +14 -0
  58. package/dist/dashboard/html-shared.js +39 -0
  59. package/dist/dashboard/html.d.ts +18 -0
  60. package/dist/dashboard/html.js +161 -0
  61. package/dist/dashboard/index.d.ts +5 -0
  62. package/dist/dashboard/index.js +3 -0
  63. package/dist/node/client.d.ts +38 -0
  64. package/dist/node/client.js +191 -0
  65. package/dist/node/exception.d.ts +15 -0
  66. package/dist/node/exception.js +33 -0
  67. package/dist/node/fastify.d.ts +7 -0
  68. package/dist/node/fastify.js +20 -0
  69. package/dist/node/index.d.ts +11 -0
  70. package/dist/node/index.js +5 -0
  71. package/dist/node/logs.d.ts +23 -0
  72. package/dist/node/logs.js +90 -0
  73. package/dist/node/metrics.d.ts +27 -0
  74. package/dist/node/metrics.js +18 -0
  75. package/dist/node/prisma.d.ts +63 -0
  76. package/dist/node/prisma.js +53 -0
  77. package/dist/node/queue.d.ts +25 -0
  78. package/dist/node/queue.js +42 -0
  79. package/dist/node/redact.d.ts +7 -0
  80. package/dist/node/redact.js +26 -0
  81. package/dist/node/tracing.d.ts +28 -0
  82. package/dist/node/tracing.js +49 -0
  83. package/dist/storage/comments.d.ts +13 -0
  84. package/dist/storage/comments.js +20 -0
  85. package/dist/storage/index.d.ts +31 -0
  86. package/dist/storage/index.js +33 -0
  87. package/dist/storage/logs.d.ts +19 -0
  88. package/dist/storage/logs.js +35 -0
  89. package/dist/storage/metrics-ingest.d.ts +14 -0
  90. package/dist/storage/metrics-ingest.js +16 -0
  91. package/dist/storage/metrics-rollup.d.ts +38 -0
  92. package/dist/storage/metrics-rollup.js +80 -0
  93. package/dist/storage/metrics.d.ts +7 -0
  94. package/dist/storage/metrics.js +10 -0
  95. package/dist/storage/migrations.d.ts +17 -0
  96. package/dist/storage/migrations.js +238 -0
  97. package/dist/storage/pool.d.ts +30 -0
  98. package/dist/storage/pool.js +28 -0
  99. package/dist/storage/projects.d.ts +17 -0
  100. package/dist/storage/projects.js +31 -0
  101. package/dist/storage/queries.d.ts +48 -0
  102. package/dist/storage/queries.js +146 -0
  103. package/dist/storage/retention.d.ts +28 -0
  104. package/dist/storage/retention.js +29 -0
  105. package/dist/storage/rows.d.ts +151 -0
  106. package/dist/storage/rows.js +134 -0
  107. package/dist/storage/saved-views.d.ts +15 -0
  108. package/dist/storage/saved-views.js +28 -0
  109. package/dist/storage/schema-ident.d.ts +7 -0
  110. package/dist/storage/schema-ident.js +13 -0
  111. package/dist/storage/spans.d.ts +14 -0
  112. package/dist/storage/spans.js +21 -0
  113. package/docs/screenshots/dashboard.jpg +0 -0
  114. package/docs/standalone-example.md +110 -0
  115. package/package.json +102 -0
@@ -0,0 +1,24 @@
1
+ import { parseArgs } from "node:util";
2
+ import { closeStorage, initStorage, rollupMetrics } from "../storage/index.js";
3
+ import { parseCommonOptions, valueAsString } from "./options.js";
4
+ const DEFAULT_RAW_RETENTION_DAYS = 3;
5
+ /**
6
+ * Runs the `rollup` CLI subcommand: aggregates raw metric points into hourly/daily
7
+ * buckets, then prunes raw points older than the retention window. Intended to be
8
+ * invoked on a schedule (e.g. a cron job), the same way `prune` is.
9
+ */
10
+ export async function runRollup(argv) {
11
+ const common = parseCommonOptions(argv);
12
+ const { values } = parseArgs({
13
+ args: argv,
14
+ options: { "raw-older-than-days": { type: "string" } },
15
+ allowPositionals: true,
16
+ strict: false
17
+ });
18
+ const rawRetentionDays = Number(valueAsString(values["raw-older-than-days"]) ?? DEFAULT_RAW_RETENTION_DAYS);
19
+ const handle = await initStorage(common);
20
+ const result = await rollupMetrics(handle, { rawRetentionDays });
21
+ console.error(`[observability] rolled up ${String(result.rolledUpBuckets)} metric buckets`);
22
+ console.error(`[observability] pruned ${String(result.prunedRawPoints)} raw metric points older than ${String(rawRetentionDays)} days`);
23
+ await closeStorage(handle);
24
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * `serve` subcommand: runs the dashboard as its own standalone HTTP server, for teams
3
+ * who'd rather view it on a separate page than mount it inside a host application.
4
+ */
5
+ export declare function runServe(argv: string[]): Promise<void>;
@@ -0,0 +1,72 @@
1
+ import { parseArgs } from "node:util";
2
+ import Fastify from "fastify";
3
+ import { slackWebhookChannel, webhookChannel } from "../alerts/index.js";
4
+ import { createDashboard, registerDashboard } from "../dashboard/index.js";
5
+ import { initStorage } from "../storage/index.js";
6
+ import { parseCommonOptions, valueAsString } from "./options.js";
7
+ const DEFAULT_PORT = 4318;
8
+ /**
9
+ * `serve` subcommand: runs the dashboard as its own standalone HTTP server, for teams
10
+ * who'd rather view it on a separate page than mount it inside a host application.
11
+ */
12
+ export async function runServe(argv) {
13
+ const common = parseCommonOptions(argv);
14
+ const { values } = parseArgs({
15
+ args: argv,
16
+ options: {
17
+ port: { type: "string" },
18
+ token: { type: "string" },
19
+ "ingest-key": { type: "string" },
20
+ webhook: { type: "string" },
21
+ "slack-webhook": { type: "string" }
22
+ },
23
+ allowPositionals: true,
24
+ strict: false
25
+ });
26
+ const port = parsePort(valueAsString(values.port));
27
+ const token = valueAsString(values.token) ?? process.env.OBSERVABILITY_TOKEN;
28
+ const ingestKey = valueAsString(values["ingest-key"]) ?? process.env.OBSERVABILITY_INGEST_KEY;
29
+ const channels = buildChannels(values);
30
+ const handle = await initStorage(common);
31
+ const app = Fastify({ logger: true });
32
+ registerDashboard(app, createDashboard(handle, { channels }), {
33
+ authorize: (request) => authorizeStandalone(request, token),
34
+ ingestKey
35
+ });
36
+ await app.listen({ port, host: "0.0.0.0" });
37
+ console.error(`[observability] dashboard listening on http://localhost:${String(port)}`);
38
+ }
39
+ /**
40
+ * Builds the alert channel list from `--webhook`/`--slack-webhook` flags or their
41
+ * `OBSERVABILITY_*` environment variable equivalents.
42
+ */
43
+ function buildChannels(values) {
44
+ const channels = [];
45
+ const webhookUrl = valueAsString(values.webhook) ?? process.env.OBSERVABILITY_WEBHOOK_URL;
46
+ const slackUrl = valueAsString(values["slack-webhook"]) ?? process.env.OBSERVABILITY_SLACK_WEBHOOK_URL;
47
+ if (webhookUrl) {
48
+ channels.push(webhookChannel(webhookUrl));
49
+ }
50
+ if (slackUrl) {
51
+ channels.push(slackWebhookChannel(slackUrl));
52
+ }
53
+ return channels;
54
+ }
55
+ /**
56
+ * Parses and validates the `--port` flag, falling back to the default port.
57
+ */
58
+ function parsePort(raw) {
59
+ const port = Number(raw ?? DEFAULT_PORT);
60
+ if (!Number.isInteger(port) || port <= 0) {
61
+ throw new Error(`Invalid --port value: "${raw ?? ""}"`);
62
+ }
63
+ return port;
64
+ }
65
+ /**
66
+ * Simple bearer-token check for standalone mode. When no token is configured, every
67
+ * request is allowed — hosts embedding the dashboard via `registerDashboard` directly
68
+ * are expected to supply their own `authorize` hook instead.
69
+ */
70
+ function authorizeStandalone(request, token) {
71
+ return !token || request.headers.authorization === `Bearer ${token}`;
72
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `sourcemap` subcommand: resolves a minified browser stack trace's frames back to
3
+ * original source locations, using `.map` files already produced by a build with
4
+ * `sourcemap: true` (e.g. `apps/web`'s Vite config). CLI-only for this pass — there's
5
+ * no dashboard-side automatic resolution, since that would need a release-to-map
6
+ * mapping this package doesn't have yet.
7
+ */
8
+ export declare function runSourcemap(argv: string[]): Promise<void>;
@@ -0,0 +1,113 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { parseArgs } from "node:util";
4
+ import { originalPositionFor, TraceMap } from "@jridgewell/trace-mapping";
5
+ import { closeStorage, getIssueWithEvents, initStorage } from "../storage/index.js";
6
+ import { valueAsString } from "./options.js";
7
+ // The trailing `)`/whitespace is a lookahead, not part of the match itself, so
8
+ // replacing `match[0]` below doesn't eat the frame's closing paren.
9
+ const FRAME_LOCATION_PATTERN = /([^\s()]+\.js):(\d+):(\d+)(?=\)?\s*$)/;
10
+ /**
11
+ * `sourcemap` subcommand: resolves a minified browser stack trace's frames back to
12
+ * original source locations, using `.map` files already produced by a build with
13
+ * `sourcemap: true` (e.g. `apps/web`'s Vite config). CLI-only for this pass — there's
14
+ * no dashboard-side automatic resolution, since that would need a release-to-map
15
+ * mapping this package doesn't have yet.
16
+ */
17
+ export async function runSourcemap(argv) {
18
+ const { values } = parseArgs({
19
+ args: argv,
20
+ options: {
21
+ maps: { type: "string" },
22
+ "stack-file": { type: "string" },
23
+ issue: { type: "string" },
24
+ db: { type: "string" },
25
+ schema: { type: "string" }
26
+ },
27
+ allowPositionals: true,
28
+ strict: false
29
+ });
30
+ const mapsDir = valueAsString(values.maps);
31
+ if (!mapsDir) {
32
+ throw new Error("A source maps directory is required: pass --maps <dir>.");
33
+ }
34
+ const stackTrace = await readStackTrace(values);
35
+ console.error(resolveStackTrace(stackTrace, mapsDir));
36
+ }
37
+ /**
38
+ * Reads the stack trace to resolve, either from a local file (`--stack-file`) or by
39
+ * fetching an issue's most recent event from storage (`--issue` plus `--db`).
40
+ */
41
+ async function readStackTrace(values) {
42
+ const stackFile = valueAsString(values["stack-file"]);
43
+ if (stackFile) {
44
+ return readFileSync(stackFile, "utf8");
45
+ }
46
+ const issueId = valueAsString(values.issue);
47
+ const connectionString = valueAsString(values.db) ?? process.env.OBSERVABILITY_DATABASE_URL;
48
+ if (!issueId || !connectionString) {
49
+ throw new Error("Provide either --stack-file <path>, or --issue <id> and --db <connectionString>.");
50
+ }
51
+ const handle = await initStorage({ connectionString, schema: valueAsString(values.schema) });
52
+ const detail = await getIssueWithEvents(handle, issueId);
53
+ await closeStorage(handle);
54
+ const stackTrace = detail?.events[0]?.stackTrace;
55
+ if (!stackTrace) {
56
+ throw new Error(`No stack trace found for issue "${issueId}".`);
57
+ }
58
+ return stackTrace;
59
+ }
60
+ /**
61
+ * Resolves every frame in a stack trace, leaving lines with no recognizable
62
+ * `file.js:line:col` frame (or no matching `.map` file) unchanged.
63
+ */
64
+ function resolveStackTrace(stackTrace, mapsDir) {
65
+ const cache = new Map();
66
+ return stackTrace
67
+ .split("\n")
68
+ .map((line) => resolveStackLine(line, mapsDir, cache))
69
+ .join("\n");
70
+ }
71
+ /**
72
+ * Resolves a single stack trace line's `file.js:line:col` frame, if present, to its
73
+ * original source location.
74
+ */
75
+ function resolveStackLine(line, mapsDir, cache) {
76
+ const match = FRAME_LOCATION_PATTERN.exec(line);
77
+ if (!match) {
78
+ return line;
79
+ }
80
+ const [frame, filePath, lineNumber, columnNumber] = match;
81
+ const traceMap = loadTraceMap(filePath, mapsDir, cache);
82
+ if (!traceMap) {
83
+ return line;
84
+ }
85
+ const original = originalPositionFor(traceMap, {
86
+ line: Number(lineNumber),
87
+ column: Number(columnNumber)
88
+ });
89
+ if (!original.source) {
90
+ return line;
91
+ }
92
+ return line.replace(frame, `${original.source}:${String(original.line)}:${String(original.column)}`);
93
+ }
94
+ /**
95
+ * Loads (and caches) the `.map` file matching a minified file's basename from the
96
+ * maps directory, or `undefined` if none exists.
97
+ */
98
+ function loadTraceMap(filePath, mapsDir, cache) {
99
+ const basename = path.basename(filePath);
100
+ if (cache.has(basename)) {
101
+ return cache.get(basename);
102
+ }
103
+ try {
104
+ const raw = readFileSync(path.join(mapsDir, `${basename}.map`), "utf8");
105
+ const traceMap = new TraceMap(JSON.parse(raw));
106
+ cache.set(basename, traceMap);
107
+ return traceMap;
108
+ }
109
+ catch {
110
+ cache.set(basename, undefined);
111
+ return undefined;
112
+ }
113
+ }
@@ -0,0 +1,21 @@
1
+ import type { Breadcrumb } from "./types.js";
2
+ /**
3
+ * The fields a caller supplies to `addBreadcrumb()` — `timestamp` is stamped
4
+ * automatically by the buffer, never provided by the caller.
5
+ */
6
+ export type BreadcrumbInput = Omit<Breadcrumb, "timestamp">;
7
+ /**
8
+ * A bounded, drop-oldest ring buffer of recent breadcrumbs, snapshotted onto the next
9
+ * captured error/message rather than cleared afterward (a burst of related errors
10
+ * benefits from overlapping context, matching Sentry's own default).
11
+ */
12
+ export interface BreadcrumbBuffer {
13
+ add: (breadcrumb: BreadcrumbInput) => void;
14
+ snapshot: () => Breadcrumb[];
15
+ }
16
+ /**
17
+ * Creates a breadcrumb ring buffer. Pure array operations only, no Node- or
18
+ * browser-only APIs, so unlike `fingerprint.ts` this is safe to use from — and export
19
+ * directly out of — the shared `core` barrel.
20
+ */
21
+ export declare function createBreadcrumbBuffer(maxSize?: number): BreadcrumbBuffer;
@@ -0,0 +1,20 @@
1
+ const DEFAULT_MAX_BREADCRUMBS = 25;
2
+ /**
3
+ * Creates a breadcrumb ring buffer. Pure array operations only, no Node- or
4
+ * browser-only APIs, so unlike `fingerprint.ts` this is safe to use from — and export
5
+ * directly out of — the shared `core` barrel.
6
+ */
7
+ export function createBreadcrumbBuffer(maxSize = DEFAULT_MAX_BREADCRUMBS) {
8
+ const buffer = [];
9
+ return {
10
+ add(breadcrumb) {
11
+ buffer.push({ ...breadcrumb, timestamp: new Date().toISOString() });
12
+ if (buffer.length > maxSize) {
13
+ buffer.shift();
14
+ }
15
+ },
16
+ snapshot() {
17
+ return [...buffer];
18
+ }
19
+ };
20
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Computes a stable fingerprint used to group events into the same issue. Prefers the
3
+ * error type plus the top stack frames when available, falling back to the raw message
4
+ * for events without a stack trace (e.g. `captureMessage`).
5
+ */
6
+ export declare function computeFingerprint(input: {
7
+ errorType?: string;
8
+ message: string;
9
+ stackTrace?: string;
10
+ }): string;
@@ -0,0 +1,35 @@
1
+ import { createHash } from "node:crypto";
2
+ const STACK_FRAMES_FOR_FINGERPRINT = 5;
3
+ /**
4
+ * Strips file paths, line numbers, and column numbers from a stack trace line so that
5
+ * fingerprints stay stable across deploys where absolute paths or line numbers shift.
6
+ */
7
+ function normalizeStackLine(line) {
8
+ return line
9
+ .trim()
10
+ .replace(/\(.*\)$/, "")
11
+ .replace(/:\d+:\d+$/, "")
12
+ .replace(/\s+/g, " ");
13
+ }
14
+ /**
15
+ * Extracts the top N normalized frames from a raw stack trace string, skipping the
16
+ * first line (the error message itself, not a stack frame).
17
+ */
18
+ function topFrames(stackTrace) {
19
+ return stackTrace
20
+ .split("\n")
21
+ .slice(1, STACK_FRAMES_FOR_FINGERPRINT + 1)
22
+ .map(normalizeStackLine)
23
+ .filter((line) => line.length > 0);
24
+ }
25
+ /**
26
+ * Computes a stable fingerprint used to group events into the same issue. Prefers the
27
+ * error type plus the top stack frames when available, falling back to the raw message
28
+ * for events without a stack trace (e.g. `captureMessage`).
29
+ */
30
+ export function computeFingerprint(input) {
31
+ const parts = input.stackTrace
32
+ ? [input.errorType ?? "Error", ...topFrames(input.stackTrace)]
33
+ : [input.errorType ?? "Message", input.message];
34
+ return createHash("sha1").update(parts.join("|")).digest("hex");
35
+ }
@@ -0,0 +1,4 @@
1
+ export { createBreadcrumbBuffer } from "./breadcrumbs.js";
2
+ export { captureInputSchema, DEFAULT_PROJECT_ID } from "./types.js";
3
+ export type { BreadcrumbBuffer, BreadcrumbInput } from "./breadcrumbs.js";
4
+ export type { Breadcrumb, CaptureInput, CapturedEvent, Comment, EventContext, EventLevel, Issue, IssueStatus, LogEntry, LogLevel, MetricBucketSize, MetricKind, MetricPoint, MetricRollup, Project, RecordedSpan, SavedView, SavedViewFilters, SpanStatus } from "./types.js";
@@ -0,0 +1,4 @@
1
+ // `./fingerprint` is deliberately not re-exported here: it uses `node:crypto`, and
2
+ // this barrel is imported by the browser SDK too. Storage imports it directly instead.
3
+ export { createBreadcrumbBuffer } from "./breadcrumbs.js";
4
+ export { captureInputSchema, DEFAULT_PROJECT_ID } from "./types.js";
@@ -0,0 +1,219 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Severity of a captured event.
4
+ */
5
+ export type EventLevel = "error" | "warning" | "info";
6
+ declare const eventContextSchema: z.ZodObject<{
7
+ method: z.ZodOptional<z.ZodString>;
8
+ route: z.ZodOptional<z.ZodString>;
9
+ statusCode: z.ZodOptional<z.ZodNumber>;
10
+ environment: z.ZodOptional<z.ZodString>;
11
+ release: z.ZodOptional<z.ZodString>;
12
+ userId: z.ZodOptional<z.ZodString>;
13
+ tags: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
14
+ extra: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
15
+ }, z.core.$strip>;
16
+ /**
17
+ * Contextual metadata attached to a captured event.
18
+ */
19
+ export type EventContext = z.infer<typeof eventContextSchema>;
20
+ declare const breadcrumbSchema: z.ZodObject<{
21
+ timestamp: z.ZodString;
22
+ category: z.ZodString;
23
+ message: z.ZodString;
24
+ level: z.ZodOptional<z.ZodEnum<{
25
+ error: "error";
26
+ warning: "warning";
27
+ info: "info";
28
+ }>>;
29
+ data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
30
+ }, z.core.$strip>;
31
+ /**
32
+ * A single recorded action ("clicked X", "GET /api/foo → 200") leading up to a
33
+ * captured error.
34
+ */
35
+ export type Breadcrumb = z.infer<typeof breadcrumbSchema>;
36
+ export declare const captureInputSchema: z.ZodObject<{
37
+ level: z.ZodDefault<z.ZodEnum<{
38
+ error: "error";
39
+ warning: "warning";
40
+ info: "info";
41
+ }>>;
42
+ message: z.ZodString;
43
+ errorType: z.ZodOptional<z.ZodString>;
44
+ stackTrace: z.ZodOptional<z.ZodString>;
45
+ context: z.ZodOptional<z.ZodObject<{
46
+ method: z.ZodOptional<z.ZodString>;
47
+ route: z.ZodOptional<z.ZodString>;
48
+ statusCode: z.ZodOptional<z.ZodNumber>;
49
+ environment: z.ZodOptional<z.ZodString>;
50
+ release: z.ZodOptional<z.ZodString>;
51
+ userId: z.ZodOptional<z.ZodString>;
52
+ tags: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
53
+ extra: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
54
+ }, z.core.$strip>>;
55
+ breadcrumbs: z.ZodOptional<z.ZodArray<z.ZodObject<{
56
+ timestamp: z.ZodString;
57
+ category: z.ZodString;
58
+ message: z.ZodString;
59
+ level: z.ZodOptional<z.ZodEnum<{
60
+ error: "error";
61
+ warning: "warning";
62
+ info: "info";
63
+ }>>;
64
+ data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
65
+ }, z.core.$strip>>>;
66
+ }, z.core.$strip>;
67
+ /**
68
+ * Shape of a validated payload accepted from an SDK or the HTTP ingestion endpoint.
69
+ */
70
+ export type CaptureInput = z.infer<typeof captureInputSchema>;
71
+ /**
72
+ * A single event as persisted in storage, after fingerprinting.
73
+ */
74
+ export interface CapturedEvent {
75
+ id: string;
76
+ fingerprint: string;
77
+ level: EventLevel;
78
+ message: string;
79
+ errorType: string | null;
80
+ stackTrace: string | null;
81
+ context: EventContext;
82
+ breadcrumbs: Breadcrumb[];
83
+ capturedAt: string;
84
+ }
85
+ /**
86
+ * Workflow status of a grouped issue.
87
+ */
88
+ export type IssueStatus = "unresolved" | "resolved" | "ignored";
89
+ /**
90
+ * A group of one or more events sharing the same fingerprint.
91
+ */
92
+ export interface Issue {
93
+ id: string;
94
+ projectId: string;
95
+ fingerprint: string;
96
+ title: string;
97
+ level: EventLevel;
98
+ status: IssueStatus;
99
+ assignee: string | null;
100
+ eventCount: number;
101
+ firstSeen: string;
102
+ lastSeen: string;
103
+ }
104
+ /**
105
+ * A free-text note left on an issue.
106
+ */
107
+ export interface Comment {
108
+ id: string;
109
+ issueId: string;
110
+ author: string;
111
+ body: string;
112
+ createdAt: string;
113
+ }
114
+ export declare const DEFAULT_PROJECT_ID = "default";
115
+ /**
116
+ * A named grouping of issues/events, identified to ingestion clients by its own API
117
+ * key. Every install has at least the auto-created `"default"` project.
118
+ */
119
+ export interface Project {
120
+ id: string;
121
+ name: string;
122
+ apiKey: string | null;
123
+ createdAt: string;
124
+ }
125
+ /**
126
+ * Outcome of a finished transaction or span.
127
+ */
128
+ export type SpanStatus = "ok" | "error";
129
+ /**
130
+ * A single timed operation: either a root transaction (`parentId: null`) or a span
131
+ * nested directly under one. Basic tracing only supports one level of nesting.
132
+ */
133
+ export interface RecordedSpan {
134
+ id: string;
135
+ traceId: string;
136
+ parentId: string | null;
137
+ name: string;
138
+ status: SpanStatus;
139
+ startedAt: string;
140
+ durationMs: number;
141
+ tags: Record<string, string>;
142
+ }
143
+ /**
144
+ * The filter combination a saved view persists — the same shape as the issues list's
145
+ * own filters, so "apply a view" is just a link built from these fields.
146
+ */
147
+ export interface SavedViewFilters {
148
+ status?: IssueStatus;
149
+ q?: string;
150
+ projectId?: string;
151
+ }
152
+ /**
153
+ * A named, persisted filter combination on the issues list (e.g. "Unresolved mobile
154
+ * errors"), shared across everyone with dashboard access — there's no per-user
155
+ * scoping, matching the rest of the dashboard's no-user-directory design.
156
+ */
157
+ export interface SavedView {
158
+ id: string;
159
+ name: string;
160
+ filters: SavedViewFilters;
161
+ createdAt: string;
162
+ }
163
+ /**
164
+ * A structured log line's severity — pino's own level names, since that's what this
165
+ * feature forwards from.
166
+ */
167
+ export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
168
+ /**
169
+ * A single structured log line, stored separately from `events`/`issues` — log lines
170
+ * don't share the "same bug, many occurrences" fingerprint-grouping semantics errors
171
+ * do, so they get their own table rather than a bolted-on second grouping scheme.
172
+ */
173
+ export interface LogEntry {
174
+ id: string;
175
+ level: LogLevel;
176
+ message: string;
177
+ context: Record<string, unknown>;
178
+ loggedAt: string;
179
+ }
180
+ /**
181
+ * The kind of a single metric point — StatsD-shaped, matching the Node SDK's
182
+ * `client.metrics.increment/gauge/histogram()` API.
183
+ */
184
+ export type MetricKind = "counter" | "gauge" | "histogram";
185
+ /**
186
+ * A single raw metric data point. Tags directly multiply row count (one row per
187
+ * unique tag combination per point) — keep them to bounded-cardinality dimensions
188
+ * (e.g. `route`, `statusCode`), not free-form values like a user ID or request ID.
189
+ */
190
+ export interface MetricPoint {
191
+ id: string;
192
+ name: string;
193
+ kind: MetricKind;
194
+ value: number;
195
+ tags: Record<string, string>;
196
+ recordedAt: string;
197
+ }
198
+ /**
199
+ * The rollup granularity a `MetricRollup` bucket was aggregated at.
200
+ */
201
+ export type MetricBucketSize = "hour" | "day";
202
+ /**
203
+ * A single aggregated bucket of raw metric points, sharing a name/tags combination
204
+ * and time window. `avg` is derived (`sum / count`), not stored.
205
+ */
206
+ export interface MetricRollup {
207
+ id: string;
208
+ name: string;
209
+ kind: MetricKind;
210
+ bucketStart: string;
211
+ bucketSize: MetricBucketSize;
212
+ tags: Record<string, string>;
213
+ sum: number;
214
+ count: number;
215
+ min: number;
216
+ max: number;
217
+ avg: number;
218
+ }
219
+ export {};
@@ -0,0 +1,35 @@
1
+ import { z } from "zod";
2
+ const eventLevelSchema = z.enum(["error", "warning", "info"]);
3
+ const eventContextSchema = z.object({
4
+ method: z.string().max(16).optional(),
5
+ route: z.string().max(500).optional(),
6
+ statusCode: z.number().int().optional(),
7
+ environment: z.string().max(100).optional(),
8
+ release: z.string().max(200).optional(),
9
+ userId: z.string().max(200).optional(),
10
+ tags: z.record(z.string(), z.string()).optional(),
11
+ extra: z.record(z.string(), z.unknown()).optional()
12
+ });
13
+ const MAX_BREADCRUMBS_PER_EVENT = 100;
14
+ const breadcrumbSchema = z.object({
15
+ timestamp: z.string(),
16
+ category: z.string().max(50),
17
+ message: z.string().max(500),
18
+ level: eventLevelSchema.optional(),
19
+ data: z.record(z.string(), z.unknown()).optional()
20
+ });
21
+ export /**
22
+ * Validated payload accepted from an SDK or the HTTP ingestion endpoint.
23
+ */ const captureInputSchema = z.object({
24
+ level: eventLevelSchema.default("error"),
25
+ message: z.string().min(1).max(2000),
26
+ errorType: z.string().max(200).optional(),
27
+ stackTrace: z.string().max(20000).optional(),
28
+ context: eventContextSchema.optional(),
29
+ breadcrumbs: z.array(breadcrumbSchema).max(MAX_BREADCRUMBS_PER_EVENT).optional()
30
+ });
31
+ export /**
32
+ * The `projectId` every issue/event belongs to when no multi-project setup
33
+ * has been configured — a fresh single-project install behaves identically to
34
+ * before multi-project support existed.
35
+ */ const DEFAULT_PROJECT_ID = "default";
@@ -0,0 +1,67 @@
1
+ import type { NotificationChannel } from "../alerts/index.js";
2
+ import type { CapturedEvent, Comment, Issue, IssueStatus, LogEntry, LogLevel, MetricBucketSize, MetricRollup, Project, RecordedSpan, SavedView, SavedViewFilters } from "../core/index.js";
3
+ import type { StorageHandle } from "../storage/index.js";
4
+ /**
5
+ * Result of attempting to ingest a single capture payload.
6
+ */
7
+ export type IngestResult = {
8
+ ok: true;
9
+ } | {
10
+ ok: false;
11
+ error: string;
12
+ };
13
+ /**
14
+ * Filters accepted by `Dashboard.listIssues`.
15
+ */
16
+ export interface ListIssuesOptions {
17
+ status?: IssueStatus;
18
+ q?: string;
19
+ projectId?: string;
20
+ }
21
+ /**
22
+ * Framework-agnostic dashboard operations: ingestion plus the read/update queries
23
+ * the UI needs. Kept independent of any HTTP framework so a new server adapter
24
+ * (Fastify, Express, a future Next.js route) only has to translate requests/responses,
25
+ * never reimplement this logic.
26
+ */
27
+ export interface Dashboard {
28
+ ingest: (payload: unknown, projectId?: string) => Promise<IngestResult>;
29
+ listIssues: (options?: ListIssuesOptions) => Promise<Issue[]>;
30
+ getIssue: (id: string) => Promise<{
31
+ issue: Issue;
32
+ events: CapturedEvent[];
33
+ comments: Comment[];
34
+ } | null>;
35
+ updateIssueStatus: (id: string, status: IssueStatus) => Promise<Issue | null>;
36
+ assignIssue: (id: string, assignee: string | null) => Promise<Issue | null>;
37
+ addComment: (id: string, input: {
38
+ author: string;
39
+ body: string;
40
+ }) => Promise<Comment>;
41
+ listTransactions: () => Promise<RecordedSpan[]>;
42
+ listProjects: () => Promise<Project[]>;
43
+ resolveProjectId: (apiKey: string | undefined) => Promise<string>;
44
+ listSavedViews: () => Promise<SavedView[]>;
45
+ createSavedView: (name: string, filters: SavedViewFilters) => Promise<SavedView>;
46
+ deleteSavedView: (id: string) => Promise<void>;
47
+ listLogs: (options?: {
48
+ level?: LogLevel;
49
+ projectId?: string;
50
+ }) => Promise<LogEntry[]>;
51
+ listMetricRollups: (options?: {
52
+ bucketSize?: MetricBucketSize;
53
+ projectId?: string;
54
+ }) => Promise<MetricRollup[]>;
55
+ }
56
+ /**
57
+ * Options for building the dashboard operations.
58
+ */
59
+ export interface DashboardOptions {
60
+ channels?: NotificationChannel[];
61
+ }
62
+ /**
63
+ * Builds the dashboard operations bound to a storage handle. Ingested events notify
64
+ * any configured alert channels the same way the Node SDK does, so alerts fire
65
+ * regardless of whether an event arrived in-process or over HTTP.
66
+ */
67
+ export declare function createDashboard(storage: StorageHandle, options?: DashboardOptions): Dashboard;