@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,151 @@
1
+ import type { Breadcrumb, CapturedEvent, Comment, EventContext, Issue, LogEntry, MetricPoint, MetricRollup, Project, RecordedSpan, SavedView, SavedViewFilters } from "../core/index.js";
2
+ /**
3
+ * Raw shape of a row in the `issues` table.
4
+ */
5
+ export interface IssueRow {
6
+ id: string;
7
+ project_id: string;
8
+ fingerprint: string;
9
+ title: string;
10
+ level: string;
11
+ status: string;
12
+ assignee: string | null;
13
+ event_count: number;
14
+ first_seen: Date;
15
+ last_seen: Date;
16
+ }
17
+ /**
18
+ * Raw shape of a row in the `projects` table.
19
+ */
20
+ export interface ProjectRow {
21
+ id: string;
22
+ name: string;
23
+ api_key: string | null;
24
+ created_at: Date;
25
+ }
26
+ /**
27
+ * Raw shape of a row in the `comments` table.
28
+ */
29
+ export interface CommentRow {
30
+ id: string;
31
+ issue_id: string;
32
+ author: string;
33
+ body: string;
34
+ created_at: Date;
35
+ }
36
+ /**
37
+ * Raw shape of a row in the `events` table.
38
+ */
39
+ export interface EventRow {
40
+ id: string;
41
+ fingerprint: string;
42
+ level: string;
43
+ message: string;
44
+ error_type: string | null;
45
+ stack_trace: string | null;
46
+ context: EventContext;
47
+ breadcrumbs: Breadcrumb[];
48
+ captured_at: Date;
49
+ }
50
+ /**
51
+ * Converts a raw `issues` row into the public `Issue` shape.
52
+ */
53
+ export declare function mapIssueRow(row: IssueRow): Issue;
54
+ /**
55
+ * Converts a raw `comments` row into the public `Comment` shape.
56
+ */
57
+ export declare function mapCommentRow(row: CommentRow): Comment;
58
+ /**
59
+ * Converts a raw `events` row into the public `CapturedEvent` shape.
60
+ */
61
+ export declare function mapEventRow(row: EventRow): CapturedEvent;
62
+ /**
63
+ * Raw shape of a row in the `spans` table.
64
+ */
65
+ export interface SpanRow {
66
+ id: string;
67
+ trace_id: string;
68
+ parent_id: string | null;
69
+ name: string;
70
+ status: string;
71
+ started_at: Date;
72
+ duration_ms: number;
73
+ tags: Record<string, string>;
74
+ }
75
+ /**
76
+ * Converts a raw `spans` row into the public `RecordedSpan` shape.
77
+ */
78
+ export declare function mapSpanRow(row: SpanRow): RecordedSpan;
79
+ /**
80
+ * Converts a raw `projects` row into the public `Project` shape.
81
+ */
82
+ export declare function mapProjectRow(row: ProjectRow): Project;
83
+ /**
84
+ * Raw shape of a row in the `saved_views` table.
85
+ */
86
+ export interface SavedViewRow {
87
+ id: string;
88
+ name: string;
89
+ filters: SavedViewFilters;
90
+ created_at: Date;
91
+ }
92
+ /**
93
+ * Converts a raw `saved_views` row into the public `SavedView` shape.
94
+ */
95
+ export declare function mapSavedViewRow(row: SavedViewRow): SavedView;
96
+ /**
97
+ * Raw shape of a row in the `logs` table.
98
+ */
99
+ export interface LogRow {
100
+ id: string;
101
+ level: string;
102
+ message: string;
103
+ context: Record<string, unknown>;
104
+ logged_at: Date;
105
+ }
106
+ /**
107
+ * Converts a raw `logs` row into the public `LogEntry` shape.
108
+ */
109
+ export declare function mapLogRow(row: LogRow): LogEntry;
110
+ /**
111
+ * Raw shape of a row in the `metric_points_raw` table.
112
+ */
113
+ export interface MetricPointRow {
114
+ id: string;
115
+ name: string;
116
+ kind: string;
117
+ value: number;
118
+ tags: Record<string, string>;
119
+ recorded_at: Date;
120
+ }
121
+ /**
122
+ * Converts a raw `metric_points_raw` row into the public `MetricPoint` shape.
123
+ */
124
+ export declare function mapMetricPointRow(row: MetricPointRow): MetricPoint;
125
+ /**
126
+ * Raw shape of a row in the `metric_rollups` table.
127
+ */
128
+ export interface MetricRollupRow {
129
+ id: string;
130
+ name: string;
131
+ kind: string;
132
+ bucket_start: Date;
133
+ bucket_size: string;
134
+ tags: Record<string, string>;
135
+ sum: number;
136
+ count: number;
137
+ min: number;
138
+ max: number;
139
+ }
140
+ /**
141
+ * Converts a raw `metric_rollups` row into the public `MetricRollup` shape,
142
+ * deriving `avg` (not stored) from `sum`/`count`.
143
+ */
144
+ export declare function mapMetricRollupRow(row: MetricRollupRow): MetricRollup;
145
+ /**
146
+ * Derives a short human-readable issue title from a capture input.
147
+ */
148
+ export declare function deriveTitle(input: {
149
+ errorType?: string;
150
+ message: string;
151
+ }): string;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Converts a raw `issues` row into the public `Issue` shape.
3
+ */
4
+ export function mapIssueRow(row) {
5
+ return {
6
+ id: row.id,
7
+ projectId: row.project_id,
8
+ fingerprint: row.fingerprint,
9
+ title: row.title,
10
+ level: row.level,
11
+ status: row.status,
12
+ assignee: row.assignee,
13
+ eventCount: row.event_count,
14
+ firstSeen: row.first_seen.toISOString(),
15
+ lastSeen: row.last_seen.toISOString()
16
+ };
17
+ }
18
+ /**
19
+ * Converts a raw `comments` row into the public `Comment` shape.
20
+ */
21
+ export function mapCommentRow(row) {
22
+ return {
23
+ id: row.id,
24
+ issueId: row.issue_id,
25
+ author: row.author,
26
+ body: row.body,
27
+ createdAt: row.created_at.toISOString()
28
+ };
29
+ }
30
+ /**
31
+ * Converts a raw `events` row into the public `CapturedEvent` shape.
32
+ */
33
+ export function mapEventRow(row) {
34
+ return {
35
+ id: row.id,
36
+ fingerprint: row.fingerprint,
37
+ level: row.level,
38
+ message: row.message,
39
+ errorType: row.error_type,
40
+ stackTrace: row.stack_trace,
41
+ context: row.context,
42
+ breadcrumbs: row.breadcrumbs,
43
+ capturedAt: row.captured_at.toISOString()
44
+ };
45
+ }
46
+ /**
47
+ * Converts a raw `spans` row into the public `RecordedSpan` shape.
48
+ */
49
+ export function mapSpanRow(row) {
50
+ return {
51
+ id: row.id,
52
+ traceId: row.trace_id,
53
+ parentId: row.parent_id,
54
+ name: row.name,
55
+ status: row.status,
56
+ startedAt: row.started_at.toISOString(),
57
+ durationMs: row.duration_ms,
58
+ tags: row.tags
59
+ };
60
+ }
61
+ /**
62
+ * Converts a raw `projects` row into the public `Project` shape.
63
+ */
64
+ export function mapProjectRow(row) {
65
+ return {
66
+ id: row.id,
67
+ name: row.name,
68
+ apiKey: row.api_key,
69
+ createdAt: row.created_at.toISOString()
70
+ };
71
+ }
72
+ /**
73
+ * Converts a raw `saved_views` row into the public `SavedView` shape.
74
+ */
75
+ export function mapSavedViewRow(row) {
76
+ return {
77
+ id: row.id,
78
+ name: row.name,
79
+ filters: row.filters,
80
+ createdAt: row.created_at.toISOString()
81
+ };
82
+ }
83
+ /**
84
+ * Converts a raw `logs` row into the public `LogEntry` shape.
85
+ */
86
+ export function mapLogRow(row) {
87
+ return {
88
+ id: row.id,
89
+ level: row.level,
90
+ message: row.message,
91
+ context: row.context,
92
+ loggedAt: row.logged_at.toISOString()
93
+ };
94
+ }
95
+ /**
96
+ * Converts a raw `metric_points_raw` row into the public `MetricPoint` shape.
97
+ */
98
+ export function mapMetricPointRow(row) {
99
+ return {
100
+ id: row.id,
101
+ name: row.name,
102
+ kind: row.kind,
103
+ value: row.value,
104
+ tags: row.tags,
105
+ recordedAt: row.recorded_at.toISOString()
106
+ };
107
+ }
108
+ /**
109
+ * Converts a raw `metric_rollups` row into the public `MetricRollup` shape,
110
+ * deriving `avg` (not stored) from `sum`/`count`.
111
+ */
112
+ export function mapMetricRollupRow(row) {
113
+ return {
114
+ id: row.id,
115
+ name: row.name,
116
+ kind: row.kind,
117
+ bucketStart: row.bucket_start.toISOString(),
118
+ bucketSize: row.bucket_size,
119
+ tags: row.tags,
120
+ sum: row.sum,
121
+ count: row.count,
122
+ min: row.min,
123
+ max: row.max,
124
+ avg: row.count > 0 ? row.sum / row.count : 0
125
+ };
126
+ }
127
+ const MAX_TITLE_LENGTH = 200;
128
+ /**
129
+ * Derives a short human-readable issue title from a capture input.
130
+ */
131
+ export function deriveTitle(input) {
132
+ const title = input.errorType ? `${input.errorType}: ${input.message}` : input.message;
133
+ return title.length > MAX_TITLE_LENGTH ? `${title.slice(0, MAX_TITLE_LENGTH)}…` : title;
134
+ }
@@ -0,0 +1,15 @@
1
+ import type { StorageHandle } from "./pool.js";
2
+ import type { SavedView, SavedViewFilters } from "../core/index.js";
3
+ /**
4
+ * Persists a named filter combination so it can be revisited later from the issues
5
+ * list.
6
+ */
7
+ export declare function createSavedView(handle: StorageHandle, name: string, filters: SavedViewFilters): Promise<SavedView>;
8
+ /**
9
+ * Lists every saved view, oldest first.
10
+ */
11
+ export declare function listSavedViews(handle: StorageHandle): Promise<SavedView[]>;
12
+ /**
13
+ * Deletes a saved view by id. A no-op if it doesn't exist.
14
+ */
15
+ export declare function deleteSavedView(handle: StorageHandle, id: string): Promise<void>;
@@ -0,0 +1,28 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mapSavedViewRow } from "./rows.js";
3
+ /**
4
+ * Persists a named filter combination so it can be revisited later from the issues
5
+ * list.
6
+ */
7
+ export async function createSavedView(handle, name, filters) {
8
+ await handle.ready;
9
+ const { rows } = await handle.pool.query(`INSERT INTO ${handle.quotedSchema}.saved_views (id, name, filters, created_at)
10
+ VALUES ($1, $2, $3, now())
11
+ RETURNING *`, [randomUUID(), name, filters]);
12
+ return mapSavedViewRow(rows[0]);
13
+ }
14
+ /**
15
+ * Lists every saved view, oldest first.
16
+ */
17
+ export async function listSavedViews(handle) {
18
+ await handle.ready;
19
+ const { rows } = await handle.pool.query(`SELECT * FROM ${handle.quotedSchema}.saved_views ORDER BY created_at ASC`);
20
+ return rows.map(mapSavedViewRow);
21
+ }
22
+ /**
23
+ * Deletes a saved view by id. A no-op if it doesn't exist.
24
+ */
25
+ export async function deleteSavedView(handle, id) {
26
+ await handle.ready;
27
+ await handle.pool.query(`DELETE FROM ${handle.quotedSchema}.saved_views WHERE id = $1`, [id]);
28
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Validates and double-quotes a Postgres schema name for safe interpolation into SQL.
3
+ * Schema names cannot be bind parameters in `pg`, so this is the only place a
4
+ * caller-supplied string is concatenated into a query — restricting the character set
5
+ * here rules out SQL injection via a malicious/misconfigured schema option.
6
+ */
7
+ export declare function quoteSchemaIdentifier(schema: string): string;
@@ -0,0 +1,13 @@
1
+ const VALID_IDENTIFIER = /^[a-z_][a-z0-9_]*$/;
2
+ /**
3
+ * Validates and double-quotes a Postgres schema name for safe interpolation into SQL.
4
+ * Schema names cannot be bind parameters in `pg`, so this is the only place a
5
+ * caller-supplied string is concatenated into a query — restricting the character set
6
+ * here rules out SQL injection via a malicious/misconfigured schema option.
7
+ */
8
+ export function quoteSchemaIdentifier(schema) {
9
+ if (!VALID_IDENTIFIER.test(schema)) {
10
+ throw new Error(`Invalid observability schema name "${schema}": must match ${VALID_IDENTIFIER.source}`);
11
+ }
12
+ return `"${schema}"`;
13
+ }
@@ -0,0 +1,14 @@
1
+ import type { StorageHandle } from "./pool.js";
2
+ import type { RecordedSpan } from "../core/index.js";
3
+ /**
4
+ * Persists a finished transaction or span.
5
+ */
6
+ export declare function recordSpan(handle: StorageHandle, span: RecordedSpan): Promise<void>;
7
+ /**
8
+ * Lists root transactions (spans with no parent) ordered by most recent first. Basic
9
+ * tracing only supports one level of nesting, so child spans aren't listed
10
+ * separately — they exist in storage but have no dashboard view yet.
11
+ */
12
+ export declare function listTransactions(handle: StorageHandle, options?: {
13
+ limit?: number;
14
+ }): Promise<RecordedSpan[]>;
@@ -0,0 +1,21 @@
1
+ import { mapSpanRow } from "./rows.js";
2
+ const DEFAULT_TRANSACTION_LIST_LIMIT = 100;
3
+ /**
4
+ * Persists a finished transaction or span.
5
+ */
6
+ export async function recordSpan(handle, span) {
7
+ await handle.ready;
8
+ await handle.pool.query(`INSERT INTO ${handle.quotedSchema}.spans
9
+ (id, trace_id, parent_id, name, status, started_at, duration_ms, tags)
10
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, [span.id, span.traceId, span.parentId, span.name, span.status, span.startedAt, span.durationMs, span.tags]);
11
+ }
12
+ /**
13
+ * Lists root transactions (spans with no parent) ordered by most recent first. Basic
14
+ * tracing only supports one level of nesting, so child spans aren't listed
15
+ * separately — they exist in storage but have no dashboard view yet.
16
+ */
17
+ export async function listTransactions(handle, options = {}) {
18
+ await handle.ready;
19
+ const { rows } = await handle.pool.query(`SELECT * FROM ${handle.quotedSchema}.spans WHERE parent_id IS NULL ORDER BY started_at DESC LIMIT $1`, [options.limit ?? DEFAULT_TRANSACTION_LIST_LIMIT]);
20
+ return rows.map(mapSpanRow);
21
+ }
Binary file
@@ -0,0 +1,110 @@
1
+ # Standalone example: using this package outside the ScheduleSpark monorepo
2
+
3
+ This package isn't published to npm yet (`private: true` in `package.json` — see the
4
+ [README's Status section](../README.md#status)), so the commands below describe the
5
+ intended experience once it is, plus how to try the same thing today from inside this
6
+ monorepo via a workspace link. The application code itself (everything after
7
+ `npm install`) doesn't change between the two.
8
+
9
+ ## The scenario
10
+
11
+ A separate Node/Fastify service, outside this monorepo entirely, that wants:
12
+
13
+ - Error capture with no extra service to run.
14
+ - A dashboard mounted at `/observability` inside that same service.
15
+ - Its own Postgres database (or a spare schema in an existing one) as the only new
16
+ piece of infrastructure.
17
+
18
+ ## Once published: real `npm install`
19
+
20
+ ```sh
21
+ npm install @schedulespark/observability pg fastify
22
+ ```
23
+
24
+ `pg` and `fastify` are peer dependencies — this package never bundles its own copy of
25
+ either, so it always talks to the same Postgres/Fastify version your app already runs.
26
+
27
+ `server.ts`:
28
+
29
+ ```ts
30
+ import Fastify from "fastify";
31
+ import { init, captureFastifyErrors } from "@schedulespark/observability/node";
32
+ import { createDashboard, registerDashboard } from "@schedulespark/observability/dashboard";
33
+ import { initStorage } from "@schedulespark/observability/storage";
34
+
35
+ const app = Fastify();
36
+
37
+ const client = init({ connectionString: process.env.OBSERVABILITY_DATABASE_URL! });
38
+ captureFastifyErrors(app, client);
39
+
40
+ const storage = await initStorage({ connectionString: process.env.OBSERVABILITY_DATABASE_URL! });
41
+ registerDashboard(app, createDashboard(storage), {
42
+ prefix: "/observability",
43
+ // Replace with your own auth check before exposing this beyond localhost — an
44
+ // unconfigured `authorize` allows every request through by default.
45
+ authorize: (request) => request.headers["x-admin-token"] === process.env.ADMIN_TOKEN
46
+ });
47
+
48
+ app.get("/", (_request, reply) => reply.send({ ok: true }));
49
+
50
+ await app.listen({ port: 3000 });
51
+ ```
52
+
53
+ ```sh
54
+ export OBSERVABILITY_DATABASE_URL="postgresql://user:pass@localhost:5432/myapp"
55
+ export ADMIN_TOKEN="pick-a-real-secret"
56
+ npx tsx server.ts
57
+ ```
58
+
59
+ Migrations run automatically on first `init()`/`initStorage()` call — no separate
60
+ migration step needed for local dev. Then:
61
+
62
+ - `curl localhost:3000/nonexistent-route-that-throws` to generate a test error.
63
+ - Open `http://localhost:3000/observability` (with the `x-admin-token` header, or swap
64
+ `authorize` for a browser-friendly session check) to see it show up.
65
+
66
+ For production, run migrations explicitly during deploy instead of relying on the
67
+ implicit on-`init()` migration:
68
+
69
+ ```sh
70
+ npx @schedulespark/observability migrate --db "$OBSERVABILITY_DATABASE_URL"
71
+ ```
72
+
73
+ ## Right now, from inside this monorepo: a workspace link
74
+
75
+ Until this package is published, the same `server.ts` above works unmodified from a
76
+ throwaway directory inside this monorepo by linking to the local package instead of
77
+ installing from npm:
78
+
79
+ ```sh
80
+ mkdir -p /tmp/observability-example && cd /tmp/observability-example
81
+ npm init -y
82
+ npm install fastify tsx
83
+ npm link /path/to/schedulespark/packages/observability
84
+ ```
85
+
86
+ Everything else — the `server.ts` content, the env vars, the `migrate`/`serve` CLI
87
+ commands — is identical to the published-package version above. This is exactly what
88
+ `apps/api`'s own dogfood integration does (via a `workspace:*` dependency instead of
89
+ `npm link`), just outside the monorepo's build tooling.
90
+
91
+ ## Running fully standalone (no host app at all)
92
+
93
+ If you don't want to write any server code, the CLI runs the same dashboard on its own
94
+ port:
95
+
96
+ ```sh
97
+ npx @schedulespark/observability serve --db "$OBSERVABILITY_DATABASE_URL" --port 4318 \
98
+ --token "$OBSERVABILITY_TOKEN" --ingest-key "$OBSERVABILITY_INGEST_KEY"
99
+ ```
100
+
101
+ Point the Node SDK's `client.captureException(...)` calls (or the browser SDK's
102
+ `init({ ingestUrl: "http://localhost:4318/ingest", apiKey: ... })`) at that same host
103
+ and port — no code from this walkthrough's `server.ts` is needed in that mode.
104
+
105
+ ## What this walkthrough intentionally doesn't cover
106
+
107
+ - A full example *application* (e.g. a sample Express/Next.js app committed to this
108
+ repo) — deferred; see the plan's Phase 5 notes. This doc is a copy-pasteable
109
+ quickstart, not a maintained sample app.
110
+ - Non-Node ingestion (Python/Go/etc.) — out of scope for this package entirely.
package/package.json ADDED
@@ -0,0 +1,102 @@
1
+ {
2
+ "name": "@schedulespark/observability",
3
+ "version": "0.0.2-beta-1-0-0",
4
+ "description": "Self-hosted, Sentry-style error tracking and dashboard that stores captured events in your own Postgres database.",
5
+ "license": "MIT",
6
+ "author": "Stephen M",
7
+ "type": "module",
8
+ "files": [
9
+ "CHANGELOG.md",
10
+ "dist",
11
+ "docs",
12
+ "README.md",
13
+ "SECURITY_REVIEW.md",
14
+ "LICENSE"
15
+ ],
16
+ "bin": {
17
+ "schedulespark-observability": "./dist/cli/bin.js"
18
+ },
19
+ "exports": {
20
+ "./alerts": {
21
+ "types": "./dist/alerts/index.d.ts",
22
+ "import": "./dist/alerts/index.js"
23
+ },
24
+ "./core": {
25
+ "types": "./dist/core/index.d.ts",
26
+ "import": "./dist/core/index.js"
27
+ },
28
+ "./storage": {
29
+ "types": "./dist/storage/index.d.ts",
30
+ "import": "./dist/storage/index.js"
31
+ },
32
+ "./node": {
33
+ "types": "./dist/node/index.d.ts",
34
+ "import": "./dist/node/index.js"
35
+ },
36
+ "./browser": {
37
+ "types": "./dist/browser/index.d.ts",
38
+ "import": "./dist/browser/index.js"
39
+ },
40
+ "./dashboard": {
41
+ "types": "./dist/dashboard/index.d.ts",
42
+ "import": "./dist/dashboard/index.js"
43
+ },
44
+ "./cli": {
45
+ "types": "./dist/cli/index.d.ts",
46
+ "import": "./dist/cli/index.js"
47
+ }
48
+ },
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/mstephenn/schedulespark.git",
52
+ "directory": "packages/observability"
53
+ },
54
+ "keywords": [
55
+ "observability",
56
+ "error-tracking",
57
+ "sentry",
58
+ "self-hosted",
59
+ "fastify",
60
+ "postgres"
61
+ ],
62
+ "publishConfig": {
63
+ "access": "public",
64
+ "tag": "latest"
65
+ },
66
+ "scripts": {
67
+ "build": "rm -rf dist && tsc -p tsconfig.json",
68
+ "lint": "eslint src --max-warnings=0 && tsc -p tsconfig.json --noEmit",
69
+ "test": "vitest run",
70
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json"
71
+ },
72
+ "dependencies": {
73
+ "@jridgewell/trace-mapping": "^0.3.31",
74
+ "nodemailer": "^9.0.3",
75
+ "pg": "^8.16.3",
76
+ "zod": "^4.4.3"
77
+ },
78
+ "peerDependencies": {
79
+ "@prisma/client": "^7.8.0",
80
+ "fastify": "^5.9.0"
81
+ },
82
+ "peerDependenciesMeta": {
83
+ "@prisma/client": {
84
+ "optional": true
85
+ },
86
+ "fastify": {
87
+ "optional": true
88
+ }
89
+ },
90
+ "devDependencies": {
91
+ "@jridgewell/gen-mapping": "^0.3.13",
92
+ "@types/jsdom": "^28.0.3",
93
+ "@types/node": "^26.0.1",
94
+ "@types/nodemailer": "^8.0.1",
95
+ "@types/pg": "^8.16.0",
96
+ "fastify": "^5.9.0",
97
+ "jsdom": "^29.1.1",
98
+ "pino": "^10.3.1",
99
+ "typescript": "^6.0.3",
100
+ "vitest": "^4.1.9"
101
+ }
102
+ }