@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,15 @@
1
+ import type { NotificationChannel } from "./types.js";
2
+ import type { CaptureInput } from "../core/index.js";
3
+ import type { RecordEventResult, StorageHandle } from "../storage/index.js";
4
+ /**
5
+ * Wraps storage's `recordEvent` so newly created issues and reopened
6
+ * ("regression") issues are announced to every configured notification channel.
7
+ */
8
+ export interface Notifier {
9
+ recordEvent: (handle: StorageHandle, input: CaptureInput, projectId?: string) => Promise<RecordEventResult>;
10
+ }
11
+ /**
12
+ * Creates a notifier bound to a set of channels. With no channels configured, this
13
+ * behaves exactly like calling storage's `recordEvent` directly.
14
+ */
15
+ export declare function createNotifier(channels: NotificationChannel[]): Notifier;
@@ -0,0 +1,31 @@
1
+ import { recordEvent } from "../storage/index.js";
2
+ import { dispatchAlert } from "./dispatch.js";
3
+ /**
4
+ * Creates a notifier bound to a set of channels. With no channels configured, this
5
+ * behaves exactly like calling storage's `recordEvent` directly.
6
+ */
7
+ export function createNotifier(channels) {
8
+ return {
9
+ async recordEvent(handle, input, projectId) {
10
+ const result = await recordEvent(handle, input, projectId);
11
+ const alert = toAlert(result);
12
+ if (alert) {
13
+ dispatchAlert(channels, alert);
14
+ }
15
+ return result;
16
+ }
17
+ };
18
+ }
19
+ /**
20
+ * Maps a record-event result to the alert it should raise, if any. Repeat
21
+ * occurrences of an already-unresolved issue raise nothing.
22
+ */
23
+ function toAlert(result) {
24
+ if (result.isNewIssue) {
25
+ return { kind: "new_issue", issue: result.issue, event: result.event };
26
+ }
27
+ if (result.isRegression) {
28
+ return { kind: "regression", issue: result.issue, event: result.event };
29
+ }
30
+ return undefined;
31
+ }
@@ -0,0 +1,25 @@
1
+ import type { NotificationChannel } from "./types.js";
2
+ import type { StorageHandle } from "../storage/index.js";
3
+ /**
4
+ * Options for the spike monitor.
5
+ */
6
+ export interface SpikeMonitorOptions {
7
+ channels: NotificationChannel[];
8
+ thresholdCount?: number;
9
+ windowMinutes?: number;
10
+ checkIntervalMs?: number;
11
+ }
12
+ /**
13
+ * A running spike monitor.
14
+ */
15
+ export interface SpikeMonitor {
16
+ stop: () => void;
17
+ }
18
+ /**
19
+ * Periodically checks the error rate over a rolling window and notifies channels
20
+ * once it crosses a threshold, with a cooldown (one window's length) between
21
+ * repeated alerts so a sustained spike doesn't re-notify on every check. State is
22
+ * kept in memory only, so each process/instance monitors independently — fine for a
23
+ * single-instance self-hosted setup, an undercount risk with multiple instances.
24
+ */
25
+ export declare function startSpikeMonitor(handle: StorageHandle, options: SpikeMonitorOptions): SpikeMonitor;
@@ -0,0 +1,41 @@
1
+ import { countEventsSince } from "../storage/index.js";
2
+ import { dispatchAlert } from "./dispatch.js";
3
+ const DEFAULT_THRESHOLD_COUNT = 20;
4
+ const DEFAULT_WINDOW_MINUTES = 5;
5
+ const DEFAULT_CHECK_INTERVAL_MS = 60_000;
6
+ /**
7
+ * Periodically checks the error rate over a rolling window and notifies channels
8
+ * once it crosses a threshold, with a cooldown (one window's length) between
9
+ * repeated alerts so a sustained spike doesn't re-notify on every check. State is
10
+ * kept in memory only, so each process/instance monitors independently — fine for a
11
+ * single-instance self-hosted setup, an undercount risk with multiple instances.
12
+ */
13
+ export function startSpikeMonitor(handle, options) {
14
+ const thresholdCount = options.thresholdCount ?? DEFAULT_THRESHOLD_COUNT;
15
+ const windowMinutes = options.windowMinutes ?? DEFAULT_WINDOW_MINUTES;
16
+ let lastAlertedAt = 0;
17
+ const timer = setInterval(() => {
18
+ void checkForSpike({ handle, options, thresholdCount, windowMinutes, lastAlertedAt }).then((alertedAt) => {
19
+ lastAlertedAt = alertedAt;
20
+ });
21
+ }, options.checkIntervalMs ?? DEFAULT_CHECK_INTERVAL_MS);
22
+ timer.unref();
23
+ return { stop: () => { clearInterval(timer); } };
24
+ }
25
+ /**
26
+ * Runs one spike check, returning the (possibly updated) `lastAlertedAt` timestamp.
27
+ */
28
+ async function checkForSpike(ctx) {
29
+ try {
30
+ const count = await countEventsSince(ctx.handle, "error", ctx.windowMinutes);
31
+ const cooledDown = Date.now() - ctx.lastAlertedAt > ctx.windowMinutes * 60_000;
32
+ if (count >= ctx.thresholdCount && cooledDown) {
33
+ dispatchAlert(ctx.options.channels, { kind: "spike", count, windowMinutes: ctx.windowMinutes });
34
+ return Date.now();
35
+ }
36
+ }
37
+ catch (error) {
38
+ console.error("[observability] spike monitor check failed", error);
39
+ }
40
+ return ctx.lastAlertedAt;
41
+ }
@@ -0,0 +1,38 @@
1
+ import type { CapturedEvent, Issue } from "../core/index.js";
2
+ /**
3
+ * Fired the first time a fingerprint is ever seen.
4
+ */
5
+ export interface NewIssueAlert {
6
+ kind: "new_issue";
7
+ issue: Issue;
8
+ event: CapturedEvent;
9
+ }
10
+ /**
11
+ * Fired when a previously-resolved issue receives a new event (auto-reopened).
12
+ */
13
+ export interface RegressionAlert {
14
+ kind: "regression";
15
+ issue: Issue;
16
+ event: CapturedEvent;
17
+ }
18
+ /**
19
+ * Fired when the error rate over a rolling window crosses a configured threshold.
20
+ */
21
+ export interface SpikeAlert {
22
+ kind: "spike";
23
+ count: number;
24
+ windowMinutes: number;
25
+ }
26
+ /**
27
+ * Payload delivered to a notification channel.
28
+ */
29
+ export type Alert = NewIssueAlert | RegressionAlert | SpikeAlert;
30
+ /**
31
+ * A destination for alert notifications (webhook, Slack, email, ...). Implementations
32
+ * must never throw past the notifier — failures are caught and logged so a broken
33
+ * channel can't block event capture.
34
+ */
35
+ export interface NotificationChannel {
36
+ name: string;
37
+ notify: (alert: Alert) => Promise<void>;
38
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import type { NotificationChannel } from "./types.js";
2
+ /**
3
+ * Creates a channel that POSTs a generic JSON payload describing the alert to any
4
+ * webhook URL.
5
+ */
6
+ export declare function webhookChannel(url: string, options?: {
7
+ name?: string;
8
+ }): NotificationChannel;
9
+ /**
10
+ * Creates a channel formatted for Slack (or Slack-compatible) incoming webhooks.
11
+ */
12
+ export declare function slackWebhookChannel(url: string, options?: {
13
+ dashboardUrl?: string;
14
+ }): NotificationChannel;
@@ -0,0 +1,59 @@
1
+ import { alertIssueLink, summarizeAlert } from "./format.js";
2
+ /**
3
+ * Creates a channel that POSTs a generic JSON payload describing the alert to any
4
+ * webhook URL.
5
+ */
6
+ export function webhookChannel(url, options = {}) {
7
+ return {
8
+ name: options.name ?? "webhook",
9
+ notify: (alert) => postJson(url, toWebhookPayload(alert))
10
+ };
11
+ }
12
+ /**
13
+ * Creates a channel formatted for Slack (or Slack-compatible) incoming webhooks.
14
+ */
15
+ export function slackWebhookChannel(url, options = {}) {
16
+ return {
17
+ name: "slack",
18
+ notify: (alert) => postJson(url, { text: toSlackText(alert, options.dashboardUrl) })
19
+ };
20
+ }
21
+ /**
22
+ * Builds the generic JSON body sent to a webhook channel.
23
+ */
24
+ function toWebhookPayload(alert) {
25
+ if (alert.kind === "spike") {
26
+ return { kind: alert.kind, count: alert.count, windowMinutes: alert.windowMinutes };
27
+ }
28
+ return {
29
+ kind: alert.kind,
30
+ issueId: alert.issue.id,
31
+ title: alert.issue.title,
32
+ level: alert.issue.level,
33
+ message: alert.event.message,
34
+ firstSeen: alert.issue.firstSeen
35
+ };
36
+ }
37
+ /**
38
+ * Formats a Slack message for an alert, linking to the dashboard when a base URL is
39
+ * configured (issue-scoped alerts only — a spike has no single issue to link to).
40
+ */
41
+ function toSlackText(alert, dashboardUrl) {
42
+ const emoji = alert.kind === "spike" ? ":chart_with_upwards_trend:" : ":rotating_light:";
43
+ const link = alertIssueLink(alert, dashboardUrl);
44
+ return `${emoji} ${summarizeAlert(alert)}${link ? ` <${link}|View>` : ""}`;
45
+ }
46
+ /**
47
+ * POSTs a JSON body to a webhook URL, throwing on a non-2xx response so the caller's
48
+ * error handling (never propagated back to event capture) can log it.
49
+ */
50
+ async function postJson(url, body) {
51
+ const response = await fetch(url, {
52
+ method: "POST",
53
+ headers: { "content-type": "application/json" },
54
+ body: JSON.stringify(body)
55
+ });
56
+ if (!response.ok) {
57
+ throw new Error(`webhook responded with status ${String(response.status)}`);
58
+ }
59
+ }
@@ -0,0 +1,26 @@
1
+ import type { BreadcrumbInput, EventContext, EventLevel } from "../core/index.js";
2
+ /**
3
+ * Options for initializing the browser observability client.
4
+ */
5
+ export interface BrowserClientOptions {
6
+ ingestUrl: string;
7
+ apiKey?: string;
8
+ environment?: string;
9
+ release?: string;
10
+ captureGlobalErrors?: boolean;
11
+ autoBreadcrumbs?: boolean;
12
+ }
13
+ /**
14
+ * The browser SDK's public capture API.
15
+ */
16
+ export interface BrowserClient {
17
+ captureException: (error: unknown, context?: EventContext) => void;
18
+ captureMessage: (message: string, level?: EventLevel, context?: EventContext) => void;
19
+ addBreadcrumb: (breadcrumb: BreadcrumbInput) => void;
20
+ }
21
+ /**
22
+ * Initializes the browser client. By default also installs `window.onerror` and
23
+ * `unhandledrejection` listeners so uncaught client-side errors are reported without
24
+ * any extra wiring, and wraps `window.fetch` to auto-record a breadcrumb per request.
25
+ */
26
+ export declare function init(options: BrowserClientOptions): BrowserClient;
@@ -0,0 +1,131 @@
1
+ import { captureInputSchema, createBreadcrumbBuffer } from "../core/index.js";
2
+ const DEFAULT_CAPTURE_GLOBAL_ERRORS = true;
3
+ const DEFAULT_AUTO_BREADCRUMBS = true;
4
+ /**
5
+ * Initializes the browser client. By default also installs `window.onerror` and
6
+ * `unhandledrejection` listeners so uncaught client-side errors are reported without
7
+ * any extra wiring, and wraps `window.fetch` to auto-record a breadcrumb per request.
8
+ */
9
+ export function init(options) {
10
+ const breadcrumbs = createBreadcrumbBuffer();
11
+ const client = {
12
+ captureException(error, context) {
13
+ send(options, { ...buildExceptionInput(error, mergeContext(context, options)), breadcrumbs: breadcrumbs.snapshot() });
14
+ },
15
+ captureMessage(message, level = "info", context) {
16
+ send(options, { level, message, context: mergeContext(context, options), breadcrumbs: breadcrumbs.snapshot() });
17
+ },
18
+ addBreadcrumb(breadcrumb) {
19
+ breadcrumbs.add({ ...breadcrumb, data: breadcrumb.data ? redactBreadcrumbData(breadcrumb.data) : undefined });
20
+ }
21
+ };
22
+ if (options.captureGlobalErrors ?? DEFAULT_CAPTURE_GLOBAL_ERRORS) {
23
+ registerGlobalHandlers(client);
24
+ }
25
+ if (options.autoBreadcrumbs ?? DEFAULT_AUTO_BREADCRUMBS) {
26
+ registerFetchBreadcrumbs(breadcrumbs, options.ingestUrl);
27
+ }
28
+ return client;
29
+ }
30
+ const BREADCRUMB_SECRET_KEY_PATTERN = /authorization|cookie|password|secret|token|api[-_]?key/i;
31
+ /**
32
+ * Redacts secret-looking keys from breadcrumb `data` before it's buffered — the same
33
+ * risk `context.extra` has, and the same key-pattern the Node SDK's `redact.ts` uses.
34
+ * Duplicated here (rather than shared) since this is the only place the browser SDK
35
+ * needs it.
36
+ */
37
+ function redactBreadcrumbData(data) {
38
+ const result = {};
39
+ for (const [key, value] of Object.entries(data)) {
40
+ result[key] = BREADCRUMB_SECRET_KEY_PATTERN.test(key) ? "[redacted]" : value;
41
+ }
42
+ return result;
43
+ }
44
+ /**
45
+ * Wraps `window.fetch` once so every request auto-records a `"fetch"` breadcrumb —
46
+ * the single highest-value default breadcrumb source, mirroring Sentry's own. Skips
47
+ * the SDK's own ingestion POSTs, which would otherwise bury every capture's real
48
+ * breadcrumb trail under "POST <ingestUrl> → 202" from reporting itself.
49
+ */
50
+ function registerFetchBreadcrumbs(breadcrumbs, ingestUrl) {
51
+ const originalFetch = window.fetch.bind(window);
52
+ window.fetch = async (...args) => {
53
+ const url = describeFetchUrl(args[0]);
54
+ if (url === ingestUrl) {
55
+ return originalFetch(...args);
56
+ }
57
+ const method = (args[1]?.method ?? "GET").toUpperCase();
58
+ try {
59
+ const response = await originalFetch(...args);
60
+ breadcrumbs.add({ category: "fetch", message: `${method} ${url}`, data: { status: response.status } });
61
+ return response;
62
+ }
63
+ catch (error) {
64
+ breadcrumbs.add({ category: "fetch", message: `${method} ${url}`, level: "error", data: { failed: true } });
65
+ throw error;
66
+ }
67
+ };
68
+ }
69
+ /**
70
+ * Renders a `fetch` call's first argument (URL string, `URL`, or `Request`) as a
71
+ * plain string for the breadcrumb message.
72
+ */
73
+ function describeFetchUrl(input) {
74
+ if (typeof input === "string") {
75
+ return input;
76
+ }
77
+ return input instanceof URL ? input.toString() : input.url;
78
+ }
79
+ /**
80
+ * Fills in `environment`/`release` on a context object from client defaults.
81
+ */
82
+ function mergeContext(context, options) {
83
+ return {
84
+ ...context,
85
+ environment: context?.environment ?? options.environment,
86
+ release: context?.release ?? options.release
87
+ };
88
+ }
89
+ /**
90
+ * Builds a capture payload from a thrown value.
91
+ */
92
+ function buildExceptionInput(error, context) {
93
+ const normalized = error instanceof Error ? error : new Error(String(error));
94
+ return {
95
+ level: "error",
96
+ message: normalized.message || "Unknown error",
97
+ errorType: normalized.name,
98
+ stackTrace: normalized.stack,
99
+ context
100
+ };
101
+ }
102
+ /**
103
+ * Validates and fire-and-forget POSTs a capture payload to the ingestion endpoint.
104
+ */
105
+ function send(options, input) {
106
+ const parsed = captureInputSchema.safeParse(input);
107
+ if (!parsed.success) {
108
+ return;
109
+ }
110
+ const headers = { "content-type": "application/json" };
111
+ if (options.apiKey) {
112
+ headers.authorization = `Bearer ${options.apiKey}`;
113
+ }
114
+ fetch(options.ingestUrl, {
115
+ method: "POST",
116
+ headers,
117
+ body: JSON.stringify(parsed.data),
118
+ keepalive: true
119
+ }).catch(() => undefined);
120
+ }
121
+ /**
122
+ * Installs `window.onerror`/`unhandledrejection` listeners that report to the client.
123
+ */
124
+ function registerGlobalHandlers(client) {
125
+ window.addEventListener("error", (event) => {
126
+ client.captureException(event.error ?? new Error(event.message));
127
+ });
128
+ window.addEventListener("unhandledrejection", (event) => {
129
+ client.captureException(event.reason);
130
+ });
131
+ }
@@ -0,0 +1,3 @@
1
+ export { init } from "./client.js";
2
+ export type { BrowserClient, BrowserClientOptions } from "./client.js";
3
+ export type { Breadcrumb, BreadcrumbInput } from "../core/index.js";
@@ -0,0 +1 @@
1
+ export { init } from "./client.js";
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "./index.js";
3
+ runCli(process.argv.slice(2)).catch((error) => {
4
+ console.error(error);
5
+ process.exitCode = 1;
6
+ });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Dispatches a CLI invocation to the matching subcommand.
3
+ */
4
+ export declare function runCli(argv: string[]): Promise<void>;
5
+ export { runMigrate } from "./migrate.js";
6
+ export { runProjects } from "./projects.js";
7
+ export { runPrune } from "./prune.js";
8
+ export { runRollup } from "./rollup.js";
9
+ export { runServe } from "./serve.js";
10
+ export { runSourcemap } from "./sourcemap.js";
@@ -0,0 +1,45 @@
1
+ import { runMigrate } from "./migrate.js";
2
+ import { runProjects } from "./projects.js";
3
+ import { runPrune } from "./prune.js";
4
+ import { runRollup } from "./rollup.js";
5
+ import { runServe } from "./serve.js";
6
+ import { runSourcemap } from "./sourcemap.js";
7
+ const USAGE = `Usage: schedulespark-observability <serve|migrate|prune|projects|sourcemap|rollup> [--db <connectionString>] [--schema <schema>]`;
8
+ /**
9
+ * Dispatches a CLI invocation to the matching subcommand.
10
+ */
11
+ export async function runCli(argv) {
12
+ const [command, ...rest] = argv;
13
+ if (command === "serve") {
14
+ await runServe(rest);
15
+ return;
16
+ }
17
+ if (command === "migrate") {
18
+ await runMigrate(rest);
19
+ return;
20
+ }
21
+ if (command === "prune") {
22
+ await runPrune(rest);
23
+ return;
24
+ }
25
+ if (command === "projects") {
26
+ await runProjects(rest);
27
+ return;
28
+ }
29
+ if (command === "sourcemap") {
30
+ await runSourcemap(rest);
31
+ return;
32
+ }
33
+ if (command === "rollup") {
34
+ await runRollup(rest);
35
+ return;
36
+ }
37
+ console.error(USAGE);
38
+ process.exitCode = 1;
39
+ }
40
+ export { runMigrate } from "./migrate.js";
41
+ export { runProjects } from "./projects.js";
42
+ export { runPrune } from "./prune.js";
43
+ export { runRollup } from "./rollup.js";
44
+ export { runServe } from "./serve.js";
45
+ export { runSourcemap } from "./sourcemap.js";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * `migrate` subcommand: applies any pending schema migrations and exits. Intended for
3
+ * production deploys, where migrating explicitly ahead of time is safer than the
4
+ * automatic on-`init()` migration the Node SDK does in development.
5
+ */
6
+ export declare function runMigrate(argv: string[]): Promise<void>;
@@ -0,0 +1,14 @@
1
+ import { closeStorage, initStorage } from "../storage/index.js";
2
+ import { parseCommonOptions } from "./options.js";
3
+ const MIGRATE_LOG_PREFIX = "[observability]";
4
+ /**
5
+ * `migrate` subcommand: applies any pending schema migrations and exits. Intended for
6
+ * production deploys, where migrating explicitly ahead of time is safer than the
7
+ * automatic on-`init()` migration the Node SDK does in development.
8
+ */
9
+ export async function runMigrate(argv) {
10
+ const options = parseCommonOptions(argv);
11
+ const handle = await initStorage(options);
12
+ console.error(`${MIGRATE_LOG_PREFIX} schema "${handle.schema}" is up to date`);
13
+ await closeStorage(handle);
14
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Common `--db`/`--schema` flags shared by every CLI subcommand.
3
+ */
4
+ export interface CommonOptions {
5
+ connectionString: string;
6
+ schema?: string;
7
+ }
8
+ /**
9
+ * Resolves the database connection string from a `--db` flag or the
10
+ * `OBSERVABILITY_DATABASE_URL` environment variable, throwing a clear error if
11
+ * neither is set.
12
+ */
13
+ export declare function parseCommonOptions(argv: string[]): CommonOptions;
14
+ /**
15
+ * Narrows a parsed argument value to a string, ignoring booleans/arrays.
16
+ */
17
+ export declare function valueAsString(value: unknown): string | undefined;
@@ -0,0 +1,28 @@
1
+ import { parseArgs } from "node:util";
2
+ /**
3
+ * Resolves the database connection string from a `--db` flag or the
4
+ * `OBSERVABILITY_DATABASE_URL` environment variable, throwing a clear error if
5
+ * neither is set.
6
+ */
7
+ export function parseCommonOptions(argv) {
8
+ const { values } = parseArgs({
9
+ args: argv,
10
+ options: {
11
+ db: { type: "string" },
12
+ schema: { type: "string" }
13
+ },
14
+ allowPositionals: true,
15
+ strict: false
16
+ });
17
+ const connectionString = valueAsString(values.db) ?? process.env.OBSERVABILITY_DATABASE_URL;
18
+ if (!connectionString) {
19
+ throw new Error("A database connection string is required: pass --db or set OBSERVABILITY_DATABASE_URL.");
20
+ }
21
+ return { connectionString, schema: valueAsString(values.schema) };
22
+ }
23
+ /**
24
+ * Narrows a parsed argument value to a string, ignoring booleans/arrays.
25
+ */
26
+ export function valueAsString(value) {
27
+ return typeof value === "string" ? value : undefined;
28
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * `projects` subcommand: manages multi-project setups (`create --name X`, `list`).
3
+ * A fresh single-project install never needs this — every schema already has an
4
+ * auto-created `default` project.
5
+ */
6
+ export declare function runProjects(argv: string[]): Promise<void>;
@@ -0,0 +1,52 @@
1
+ import { parseArgs } from "node:util";
2
+ import { closeStorage, createProject, initStorage, listProjects } from "../storage/index.js";
3
+ import { parseCommonOptions, valueAsString } from "./options.js";
4
+ const USAGE = "Usage: schedulespark-observability projects <create|list> [--db <connectionString>] [--schema <schema>]";
5
+ /**
6
+ * `projects` subcommand: manages multi-project setups (`create --name X`, `list`).
7
+ * A fresh single-project install never needs this — every schema already has an
8
+ * auto-created `default` project.
9
+ */
10
+ export async function runProjects(argv) {
11
+ const [subcommand, ...rest] = argv;
12
+ if (subcommand === "create") {
13
+ await runProjectsCreate(rest);
14
+ return;
15
+ }
16
+ if (subcommand === "list") {
17
+ await runProjectsList(rest);
18
+ return;
19
+ }
20
+ console.error(USAGE);
21
+ process.exitCode = 1;
22
+ }
23
+ /**
24
+ * `projects create --name X`: creates a project and prints its generated API key.
25
+ */
26
+ async function runProjectsCreate(argv) {
27
+ const common = parseCommonOptions(argv);
28
+ const { values } = parseArgs({
29
+ args: argv,
30
+ options: { name: { type: "string" } },
31
+ allowPositionals: true,
32
+ strict: false
33
+ });
34
+ const name = valueAsString(values.name);
35
+ if (!name) {
36
+ throw new Error("A project name is required: pass --name.");
37
+ }
38
+ const handle = await initStorage(common);
39
+ const project = await createProject(handle, name);
40
+ console.error(JSON.stringify(project, null, 2));
41
+ await closeStorage(handle);
42
+ }
43
+ /**
44
+ * `projects list`: prints every configured project.
45
+ */
46
+ async function runProjectsList(argv) {
47
+ const common = parseCommonOptions(argv);
48
+ const handle = await initStorage(common);
49
+ const projects = await listProjects(handle);
50
+ console.error(JSON.stringify(projects, null, 2));
51
+ await closeStorage(handle);
52
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `prune` subcommand: deletes events older than the retention window (default 90
3
+ * days), keeping their issues intact, and separately prunes log lines (default 14
4
+ * days — logs are typically much higher-volume than events). A stopgap against
5
+ * unbounded table growth — intended to be run on a schedule (cron, Render cron job,
6
+ * etc.) since nothing prunes automatically.
7
+ */
8
+ export declare function runPrune(argv: string[]): Promise<void>;
@@ -0,0 +1,32 @@
1
+ import { parseArgs } from "node:util";
2
+ import { closeStorage, initStorage, pruneEvents, pruneLogs } from "../storage/index.js";
3
+ import { parseCommonOptions, valueAsString } from "./options.js";
4
+ const DEFAULT_RETENTION_DAYS = 90;
5
+ const DEFAULT_LOG_RETENTION_DAYS = 14;
6
+ /**
7
+ * `prune` subcommand: deletes events older than the retention window (default 90
8
+ * days), keeping their issues intact, and separately prunes log lines (default 14
9
+ * days — logs are typically much higher-volume than events). A stopgap against
10
+ * unbounded table growth — intended to be run on a schedule (cron, Render cron job,
11
+ * etc.) since nothing prunes automatically.
12
+ */
13
+ export async function runPrune(argv) {
14
+ const common = parseCommonOptions(argv);
15
+ const { values } = parseArgs({
16
+ args: argv,
17
+ options: {
18
+ "older-than-days": { type: "string" },
19
+ "logs-older-than-days": { type: "string" }
20
+ },
21
+ allowPositionals: true,
22
+ strict: false
23
+ });
24
+ const olderThanDays = Number(valueAsString(values["older-than-days"]) ?? DEFAULT_RETENTION_DAYS);
25
+ const logsOlderThanDays = Number(valueAsString(values["logs-older-than-days"]) ?? DEFAULT_LOG_RETENTION_DAYS);
26
+ const handle = await initStorage(common);
27
+ const eventsResult = await pruneEvents(handle, olderThanDays);
28
+ console.error(`[observability] deleted ${String(eventsResult.deletedEvents)} events older than ${String(olderThanDays)} days`);
29
+ const logsResult = await pruneLogs(handle, logsOlderThanDays);
30
+ console.error(`[observability] deleted ${String(logsResult.deletedLogs)} logs older than ${String(logsOlderThanDays)} days`);
31
+ await closeStorage(handle);
32
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Runs the `rollup` CLI subcommand: aggregates raw metric points into hourly/daily
3
+ * buckets, then prunes raw points older than the retention window. Intended to be
4
+ * invoked on a schedule (e.g. a cron job), the same way `prune` is.
5
+ */
6
+ export declare function runRollup(argv: string[]): Promise<void>;