@growth-labs/monitoring 0.1.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 (117) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +115 -0
  3. package/SPEC.md +19 -0
  4. package/dist/alerting/dedup.d.ts +6 -0
  5. package/dist/alerting/dedup.d.ts.map +1 -0
  6. package/dist/alerting/dedup.js +49 -0
  7. package/dist/alerting/dedup.js.map +1 -0
  8. package/dist/alerting/escalation.d.ts +4 -0
  9. package/dist/alerting/escalation.d.ts.map +1 -0
  10. package/dist/alerting/escalation.js +26 -0
  11. package/dist/alerting/escalation.js.map +1 -0
  12. package/dist/alerting/index.d.ts +31 -0
  13. package/dist/alerting/index.d.ts.map +1 -0
  14. package/dist/alerting/index.js +50 -0
  15. package/dist/alerting/index.js.map +1 -0
  16. package/dist/alerting/thresholds.d.ts +8 -0
  17. package/dist/alerting/thresholds.d.ts.map +1 -0
  18. package/dist/alerting/thresholds.js +105 -0
  19. package/dist/alerting/thresholds.js.map +1 -0
  20. package/dist/index.d.ts +10 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +5 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/prober/index.d.ts +19 -0
  25. package/dist/prober/index.d.ts.map +1 -0
  26. package/dist/prober/index.js +48 -0
  27. package/dist/prober/index.js.map +1 -0
  28. package/dist/prober/persist.d.ts +4 -0
  29. package/dist/prober/persist.d.ts.map +1 -0
  30. package/dist/prober/persist.js +21 -0
  31. package/dist/prober/persist.js.map +1 -0
  32. package/dist/prober/runners/get-runner.d.ts +4 -0
  33. package/dist/prober/runners/get-runner.d.ts.map +1 -0
  34. package/dist/prober/runners/get-runner.js +43 -0
  35. package/dist/prober/runners/get-runner.js.map +1 -0
  36. package/dist/prober/runners/happy-path-runner.d.ts +13 -0
  37. package/dist/prober/runners/happy-path-runner.d.ts.map +1 -0
  38. package/dist/prober/runners/happy-path-runner.js +183 -0
  39. package/dist/prober/runners/happy-path-runner.js.map +1 -0
  40. package/dist/prober/runners/post-runner.d.ts +4 -0
  41. package/dist/prober/runners/post-runner.d.ts.map +1 -0
  42. package/dist/prober/runners/post-runner.js +44 -0
  43. package/dist/prober/runners/post-runner.js.map +1 -0
  44. package/dist/prober/surfaces.d.ts +46 -0
  45. package/dist/prober/surfaces.d.ts.map +1 -0
  46. package/dist/prober/surfaces.js +2 -0
  47. package/dist/prober/surfaces.js.map +1 -0
  48. package/dist/schemas/drizzle/schema.d.ts +519 -0
  49. package/dist/schemas/drizzle/schema.d.ts.map +1 -0
  50. package/dist/schemas/drizzle/schema.js +45 -0
  51. package/dist/schemas/drizzle/schema.js.map +1 -0
  52. package/dist/schemas/index.d.ts +2 -0
  53. package/dist/schemas/index.d.ts.map +1 -0
  54. package/dist/schemas/index.js +2 -0
  55. package/dist/schemas/index.js.map +1 -0
  56. package/dist/schemas/migrations/0001_uptime_checks.sql +12 -0
  57. package/dist/schemas/migrations/0002_uptime_incidents.sql +12 -0
  58. package/dist/schemas/migrations/0003_errors.sql +16 -0
  59. package/dist/schemas/migrations/README.md +15 -0
  60. package/dist/status-page/app.d.ts +10 -0
  61. package/dist/status-page/app.d.ts.map +1 -0
  62. package/dist/status-page/app.js +44 -0
  63. package/dist/status-page/app.js.map +1 -0
  64. package/dist/status-page/lib/queries.d.ts +6 -0
  65. package/dist/status-page/lib/queries.d.ts.map +1 -0
  66. package/dist/status-page/lib/queries.js +81 -0
  67. package/dist/status-page/lib/queries.js.map +1 -0
  68. package/dist/status-page/pages/api/status.json.d.ts +3 -0
  69. package/dist/status-page/pages/api/status.json.d.ts.map +1 -0
  70. package/dist/status-page/pages/api/status.json.js +19 -0
  71. package/dist/status-page/pages/api/status.json.js.map +1 -0
  72. package/dist/status-page/shell.d.ts +3 -0
  73. package/dist/status-page/shell.d.ts.map +1 -0
  74. package/dist/status-page/shell.js +18 -0
  75. package/dist/status-page/shell.js.map +1 -0
  76. package/dist/tail/categorize.d.ts +44 -0
  77. package/dist/tail/categorize.d.ts.map +1 -0
  78. package/dist/tail/categorize.js +113 -0
  79. package/dist/tail/categorize.js.map +1 -0
  80. package/dist/tail/fingerprint.d.ts +4 -0
  81. package/dist/tail/fingerprint.d.ts.map +1 -0
  82. package/dist/tail/fingerprint.js +18 -0
  83. package/dist/tail/fingerprint.js.map +1 -0
  84. package/dist/tail/index.d.ts +21 -0
  85. package/dist/tail/index.d.ts.map +1 -0
  86. package/dist/tail/index.js +50 -0
  87. package/dist/tail/index.js.map +1 -0
  88. package/dist/tail/persist.d.ts +15 -0
  89. package/dist/tail/persist.d.ts.map +1 -0
  90. package/dist/tail/persist.js +63 -0
  91. package/dist/tail/persist.js.map +1 -0
  92. package/dist/tail/redact.d.ts +5 -0
  93. package/dist/tail/redact.d.ts.map +1 -0
  94. package/dist/tail/redact.js +25 -0
  95. package/dist/tail/redact.js.map +1 -0
  96. package/dist/tail/sample.d.ts +9 -0
  97. package/dist/tail/sample.d.ts.map +1 -0
  98. package/dist/tail/sample.js +25 -0
  99. package/dist/tail/sample.js.map +1 -0
  100. package/dist/types.d.ts +87 -0
  101. package/dist/types.d.ts.map +1 -0
  102. package/dist/types.js +11 -0
  103. package/dist/types.js.map +1 -0
  104. package/package.json +85 -0
  105. package/src/schemas/migrations/0001_uptime_checks.sql +12 -0
  106. package/src/schemas/migrations/0002_uptime_incidents.sql +12 -0
  107. package/src/schemas/migrations/0003_errors.sql +16 -0
  108. package/src/schemas/migrations/README.md +15 -0
  109. package/src/status-page/README.md +14 -0
  110. package/src/status-page/app.ts +58 -0
  111. package/src/status-page/components/ErrorRollup.astro +26 -0
  112. package/src/status-page/components/IncidentList.astro +25 -0
  113. package/src/status-page/components/SurfaceRow.astro +24 -0
  114. package/src/status-page/lib/queries.ts +114 -0
  115. package/src/status-page/pages/api/status.json.ts +24 -0
  116. package/src/status-page/pages/index.astro +45 -0
  117. package/src/status-page/shell.ts +17 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # @growth-labs/monitoring
2
+
3
+ ## 0.1.0 - 2026-05-18
4
+
5
+ - Added synthetic prober factory with GET, POST, and code-signin happy-path runners.
6
+ - Added Tail Worker trace categorization, redaction, fingerprinting, sampling, D1 persistence, and WAE writes.
7
+ - Added alerting thresholds, incident dedup/open/close helpers, and escalation actions that delegate delivery to `@growth-labs/notify`.
8
+ - Added D1 SQL migrations plus matching Drizzle schema for uptime checks, incidents, and error events.
9
+ - Added Astro status-page integration, query helpers, read-only HTML view, and `/api/status.json`.
package/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # @growth-labs/monitoring
2
+
3
+ Operational observability for Growth Labs Cloudflare Workers: synthetic probes,
4
+ Tail Worker error capture, uptime/error schemas, alerting, and a small public
5
+ status page.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @growth-labs/monitoring @growth-labs/notify @growth-labs/analytics
11
+ ```
12
+
13
+ Peer: `astro ^6.0.0` for the status-page app. Runtime deps:
14
+ `@growth-labs/notify`, `@growth-labs/analytics`, `drizzle-orm`, `zod`.
15
+
16
+ ## Modules
17
+
18
+ | Subpath | Purpose |
19
+ | --- | --- |
20
+ | `@growth-labs/monitoring` | Umbrella exports for the main factories. |
21
+ | `@growth-labs/monitoring/prober` | `createProber(config)` for Cron Workers that run GET, POST, and happy-path checks. |
22
+ | `@growth-labs/monitoring/tail` | `createTailWorker(config)` for Cloudflare Tail Worker trace batches. |
23
+ | `@growth-labs/monitoring/alerting` | Threshold, dedup, incident, escalation, and notify delegation helpers. |
24
+ | `@growth-labs/monitoring/status-page` | Astro integration that injects the status page and JSON route. |
25
+ | `@growth-labs/monitoring/schemas` | Drizzle schema matching the shipped D1 migrations. |
26
+
27
+ Raw SQL migrations ship at:
28
+
29
+ ```text
30
+ node_modules/@growth-labs/monitoring/src/schemas/migrations
31
+ node_modules/@growth-labs/monitoring/dist/schemas/migrations
32
+ ```
33
+
34
+ ## Prober
35
+
36
+ ```ts
37
+ import { createProber } from '@growth-labs/monitoring/prober'
38
+
39
+ const prober = createProber({
40
+ realmId: 'fulcrum-labs',
41
+ d1Binding: 'MONITORING_DB',
42
+ notifyConfig: {
43
+ channels: ['slack', 'email'],
44
+ emailProvider: 'resend',
45
+ },
46
+ alertingConfig: { consecutiveFailuresToOpen: 2, minSeverity: 'warning' },
47
+ surfaces: [
48
+ {
49
+ name: 'fronts.co:login',
50
+ kind: 'get',
51
+ schedule: '*/5 * * * *',
52
+ url: 'https://fronts.co/login/',
53
+ assertions: { statusCode: 200, contentMarker: 'auth-eyebrow' },
54
+ timeoutMs: 10_000,
55
+ },
56
+ ],
57
+ })
58
+
59
+ export default { scheduled: prober.scheduledHandler }
60
+ ```
61
+
62
+ `auth-monitor` must use `emailProvider: 'resend'` when email alerts are enabled.
63
+ The monitor exists partly to detect Cloudflare auth delivery failures, so alert
64
+ email must not depend on Cloudflare Email.
65
+
66
+ ## Tail Worker
67
+
68
+ ```ts
69
+ import { createTailWorker } from '@growth-labs/monitoring/tail'
70
+
71
+ const tail = createTailWorker({
72
+ realmId: 'fulcrum-labs',
73
+ d1Binding: 'MONITORING_DB',
74
+ waeBinding: 'MONITORING_ERRORS',
75
+ notifyConfig: { channels: ['slack', 'email'], emailProvider: 'resend' },
76
+ alertingConfig: {
77
+ newErrorDedupWindowMs: 60 * 60 * 1000,
78
+ rateSpikeThreshold: 10,
79
+ rateSpikeWindowMs: 5 * 60 * 1000,
80
+ },
81
+ sampling: {
82
+ exceptionsPct: 1,
83
+ fivexxPct: 1,
84
+ consoleErrorPct: 1,
85
+ consoleWarnPct: 0.1,
86
+ slowRequestsPct: 0.01,
87
+ slowRequestThresholdMs: 1000,
88
+ },
89
+ })
90
+
91
+ export default { tail: tail.tailHandler }
92
+ ```
93
+
94
+ Fingerprints are computed before redaction. Every persisted error is redacted
95
+ and truncated before D1/WAE writes.
96
+
97
+ ## Status Page
98
+
99
+ ```ts
100
+ import { createStatusPageApp } from '@growth-labs/monitoring/status-page'
101
+
102
+ export default {
103
+ integrations: [
104
+ createStatusPageApp({
105
+ realm: 'fulcrum-labs',
106
+ d1Binding: 'MONITORING_DB',
107
+ surfaces: [{ name: 'fronts.co:login' }],
108
+ }),
109
+ ],
110
+ }
111
+ ```
112
+
113
+ The status page is read-only. It injects `/` and `/api/status.json` by default
114
+ and reads only the `gl_uptime_checks`, `gl_uptime_incidents`, and `gl_errors`
115
+ tables.
package/SPEC.md ADDED
@@ -0,0 +1,19 @@
1
+ # @growth-labs/monitoring v0.1.0 Spec
2
+
3
+ This package implements the operational observability contract for
4
+ `growth-labs/monitoring-platform`.
5
+
6
+ Included modules:
7
+
8
+ - Synthetic prober Cron Worker helpers for GET, POST, and code-signin happy paths.
9
+ - Tail Worker error capture for exceptions, 5xx responses, console errors/warnings, and slow requests.
10
+ - D1 schemas for `gl_uptime_checks`, `gl_uptime_incidents`, and `gl_errors`.
11
+ - Alerting thresholds, dedup, incident lifecycle, and escalation actions.
12
+ - Read-only Astro status page and JSON snapshot route.
13
+
14
+ Out of scope:
15
+
16
+ - Surface ownership. Consumers pass surface lists in config.
17
+ - Scheduling ownership. Cloudflare Cron triggers invoke the exported handler.
18
+ - Notification transport implementation. `@growth-labs/notify` sends alerts.
19
+ - Active remediation or historical replay.
@@ -0,0 +1,6 @@
1
+ import { type IdFactory, type UptimeIncident } from '../types.js';
2
+ import type { AlertAction } from './index.js';
3
+ export declare function isIncidentOpen(surface: string, db: D1Database): Promise<UptimeIncident | null>;
4
+ export declare function openIncident(action: AlertAction, db: D1Database, idFactory?: IdFactory, now?: () => number): Promise<string>;
5
+ export declare function closeIncident(incidentId: string, resolveCheckId: string, db: D1Database, now?: () => number): Promise<void>;
6
+ //# sourceMappingURL=dedup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dedup.d.ts","sourceRoot":"","sources":["../../src/alerting/dedup.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,KAAK,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAA;AAC3F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C,wBAAsB,cAAc,CACnC,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,UAAU,GACZ,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAWhC;AAED,wBAAsB,YAAY,CACjC,MAAM,EAAE,WAAW,EACnB,EAAE,EAAE,UAAU,EACd,SAAS,GAAE,SAAsB,EACjC,GAAG,eAAW,GACZ,OAAO,CAAC,MAAM,CAAC,CAyBjB;AAED,wBAAsB,aAAa,CAClC,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,MAAM,EACtB,EAAE,EAAE,UAAU,EACd,GAAG,eAAW,GACZ,OAAO,CAAC,IAAI,CAAC,CAaf"}
@@ -0,0 +1,49 @@
1
+ import { errorMessage, generateId } from '../types.js';
2
+ export async function isIncidentOpen(surface, db) {
3
+ return db
4
+ .prepare(`
5
+ SELECT id, surface, opened_at, closed_at, trigger_check_id, resolve_check_id, severity, notes
6
+ FROM gl_uptime_incidents
7
+ WHERE surface = ? AND closed_at IS NULL
8
+ ORDER BY opened_at DESC
9
+ LIMIT 1
10
+ `)
11
+ .bind(surface)
12
+ .first();
13
+ }
14
+ export async function openIncident(action, db, idFactory = generateId, now = Date.now) {
15
+ const existing = await isIncidentOpen(action.surface, db);
16
+ if (existing)
17
+ return existing.id;
18
+ const id = idFactory();
19
+ try {
20
+ await db
21
+ .prepare(`
22
+ INSERT INTO gl_uptime_incidents
23
+ (id, surface, opened_at, trigger_check_id, severity, notes)
24
+ VALUES (?, ?, ?, ?, ?, ?)
25
+ `)
26
+ .bind(id, action.surface, Math.floor(now() / 1000), typeof action.payload?.triggerCheckId === 'string' ? action.payload.triggerCheckId : null, action.severity === 'critical' ? 'critical' : 'warning', typeof action.payload?.notes === 'string' ? action.payload.notes : null)
27
+ .run();
28
+ }
29
+ catch (error) {
30
+ console.error(`[monitoring] failed to open incident ${action.surface}: ${errorMessage(error)}`);
31
+ }
32
+ return id;
33
+ }
34
+ export async function closeIncident(incidentId, resolveCheckId, db, now = Date.now) {
35
+ try {
36
+ await db
37
+ .prepare(`
38
+ UPDATE gl_uptime_incidents
39
+ SET closed_at = ?, resolve_check_id = ?
40
+ WHERE id = ? AND closed_at IS NULL
41
+ `)
42
+ .bind(Math.floor(now() / 1000), resolveCheckId, incidentId)
43
+ .run();
44
+ }
45
+ catch (error) {
46
+ console.error(`[monitoring] failed to close incident ${incidentId}: ${errorMessage(error)}`);
47
+ }
48
+ }
49
+ //# sourceMappingURL=dedup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dedup.js","sourceRoot":"","sources":["../../src/alerting/dedup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAuC,MAAM,aAAa,CAAA;AAG3F,MAAM,CAAC,KAAK,UAAU,cAAc,CACnC,OAAe,EACf,EAAc;IAEd,OAAO,EAAE;SACP,OAAO,CAAC;;;;;;GAMR,CAAC;SACD,IAAI,CAAC,OAAO,CAAC;SACb,KAAK,EAAkB,CAAA;AAC1B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CACjC,MAAmB,EACnB,EAAc,EACd,YAAuB,UAAU,EACjC,GAAG,GAAG,IAAI,CAAC,GAAG;IAEd,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC,CAAA;IACzD,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC,EAAE,CAAA;IAEhC,MAAM,EAAE,GAAG,SAAS,EAAE,CAAA;IACtB,IAAI,CAAC;QACJ,MAAM,EAAE;aACN,OAAO,CAAC;;;;IAIR,CAAC;aACD,IAAI,CACJ,EAAE,EACF,MAAM,CAAC,OAAO,EACd,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,EACxB,OAAO,MAAM,CAAC,OAAO,EAAE,cAAc,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,EACzF,MAAM,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,EACvD,OAAO,MAAM,CAAC,OAAO,EAAE,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CACvE;aACA,GAAG,EAAE,CAAA;IACR,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,OAAO,CAAC,KAAK,CAAC,wCAAwC,MAAM,CAAC,OAAO,KAAK,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;IAChG,CAAC;IACD,OAAO,EAAE,CAAA;AACV,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAClC,UAAkB,EAClB,cAAsB,EACtB,EAAc,EACd,GAAG,GAAG,IAAI,CAAC,GAAG;IAEd,IAAI,CAAC;QACJ,MAAM,EAAE;aACN,OAAO,CAAC;;;;IAIR,CAAC;aACD,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,EAAE,cAAc,EAAE,UAAU,CAAC;aAC1D,GAAG,EAAE,CAAA;IACR,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,OAAO,CAAC,KAAK,CAAC,yCAAyC,UAAU,KAAK,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;IAC7F,CAAC;AACF,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { UptimeIncident } from '../types.js';
2
+ import type { AlertAction } from './index.js';
3
+ export declare function evaluateEscalation(incident: UptimeIncident, nowSeconds?: number, alreadySent?: Set<string>): AlertAction | null;
4
+ //# sourceMappingURL=escalation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"escalation.d.ts","sourceRoot":"","sources":["../../src/alerting/escalation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C,wBAAgB,kBAAkB,CACjC,QAAQ,EAAE,cAAc,EACxB,UAAU,SAAgC,EAC1C,WAAW,GAAE,GAAG,CAAC,MAAM,CAAa,GAClC,WAAW,GAAG,IAAI,CAqBpB"}
@@ -0,0 +1,26 @@
1
+ export function evaluateEscalation(incident, nowSeconds = Math.floor(Date.now() / 1000), alreadySent = new Set()) {
2
+ if (incident.closed_at !== null)
3
+ return null;
4
+ const ageSeconds = nowSeconds - incident.opened_at;
5
+ const checkpoints = [
6
+ { key: '24h', seconds: 24 * 60 * 60, severity: 'critical' },
7
+ { key: '4h', seconds: 4 * 60 * 60, severity: 'critical' },
8
+ { key: '30m', seconds: 30 * 60, severity: 'critical' },
9
+ ];
10
+ const checkpoint = checkpoints.find((item) => ageSeconds >= item.seconds);
11
+ if (!checkpoint)
12
+ return null;
13
+ const dedupKey = `escalation:${incident.id}:${checkpoint.key}`;
14
+ if (alreadySent.has(dedupKey))
15
+ return null;
16
+ return {
17
+ kind: 'escalation',
18
+ surface: incident.surface,
19
+ severity: checkpoint.severity,
20
+ title: `${incident.surface} incident still open`,
21
+ body: `Incident ${incident.id} has been open for ${checkpoint.key}.`,
22
+ dedupKey,
23
+ payload: { incidentId: incident.id, checkpoint: checkpoint.key },
24
+ };
25
+ }
26
+ //# sourceMappingURL=escalation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"escalation.js","sourceRoot":"","sources":["../../src/alerting/escalation.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,kBAAkB,CACjC,QAAwB,EACxB,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,EAC1C,cAA2B,IAAI,GAAG,EAAE;IAEpC,IAAI,QAAQ,CAAC,SAAS,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IAC5C,MAAM,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAC,SAAS,CAAA;IAClD,MAAM,WAAW,GAAG;QACnB,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,QAAQ,EAAE,UAAmB,EAAE;QACpE,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,QAAQ,EAAE,UAAmB,EAAE;QAClE,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,EAAE,QAAQ,EAAE,UAAmB,EAAE;KAC/D,CAAA;IACD,MAAM,UAAU,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,IAAI,IAAI,CAAC,OAAO,CAAC,CAAA;IACzE,IAAI,CAAC,UAAU;QAAE,OAAO,IAAI,CAAA;IAC5B,MAAM,QAAQ,GAAG,cAAc,QAAQ,CAAC,EAAE,IAAI,UAAU,CAAC,GAAG,EAAE,CAAA;IAC9D,IAAI,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAA;IAC1C,OAAO;QACN,IAAI,EAAE,YAAY;QAClB,OAAO,EAAE,QAAQ,CAAC,OAAO;QACzB,QAAQ,EAAE,UAAU,CAAC,QAAQ;QAC7B,KAAK,EAAE,GAAG,QAAQ,CAAC,OAAO,sBAAsB;QAChD,IAAI,EAAE,YAAY,QAAQ,CAAC,EAAE,sBAAsB,UAAU,CAAC,GAAG,GAAG;QACpE,QAAQ;QACR,OAAO,EAAE,EAAE,UAAU,EAAE,QAAQ,CAAC,EAAE,EAAE,UAAU,EAAE,UAAU,CAAC,GAAG,EAAE;KAChE,CAAA;AACF,CAAC"}
@@ -0,0 +1,31 @@
1
+ import { type NotifyResult } from '@growth-labs/notify';
2
+ import type { CategorizedEvent } from '../tail/categorize.js';
3
+ import type { AlertSeverity, NotifyConfig, PersistedCheckResult, ThresholdConfig } from '../types.js';
4
+ export type AlertKind = 'open-incident' | 'close-incident' | 'rate-spike' | 'new-error' | 'escalation';
5
+ export interface AlertAction {
6
+ kind: AlertKind;
7
+ surface: string;
8
+ severity: AlertSeverity;
9
+ title: string;
10
+ body: string;
11
+ dedupKey: string;
12
+ payload?: Record<string, unknown>;
13
+ }
14
+ export interface AlertEngine {
15
+ activeIncidentSurfaces: Set<string>;
16
+ handleProbeResult(result: PersistedCheckResult, surfaceName: string, db: D1Database, env: Record<string, unknown>): Promise<AlertAction | null>;
17
+ handleErrorEvent(event: CategorizedEvent & {
18
+ fingerprint: string;
19
+ }, db: D1Database, env: Record<string, unknown>): Promise<AlertAction | null>;
20
+ send(env: Record<string, unknown>, action: AlertAction): Promise<NotifyResult>;
21
+ }
22
+ export interface AlerterConfig {
23
+ notifyConfig: NotifyConfig;
24
+ alertingConfig?: ThresholdConfig;
25
+ notifyFn?: (env: Record<string, unknown>, action: AlertAction) => Promise<NotifyResult>;
26
+ }
27
+ export declare function createAlerter(config: AlerterConfig): AlertEngine;
28
+ export { closeIncident, isIncidentOpen, openIncident } from './dedup.js';
29
+ export { evaluateEscalation } from './escalation.js';
30
+ export { evaluateError, evaluateProbe } from './thresholds.js';
31
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/alerting/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,YAAY,EAAU,MAAM,qBAAqB,CAAA;AAC/E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,KAAK,EACX,aAAa,EACb,YAAY,EACZ,oBAAoB,EACpB,eAAe,EACf,MAAM,aAAa,CAAA;AAIpB,MAAM,MAAM,SAAS,GAClB,eAAe,GACf,gBAAgB,GAChB,YAAY,GACZ,WAAW,GACX,YAAY,CAAA;AAEf,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,SAAS,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,aAAa,CAAA;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACjC;AAED,MAAM,WAAW,WAAW;IAC3B,sBAAsB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACnC,iBAAiB,CAChB,MAAM,EAAE,oBAAoB,EAC5B,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,UAAU,EACd,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC1B,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IAC9B,gBAAgB,CACf,KAAK,EAAE,gBAAgB,GAAG;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,EACjD,EAAE,EAAE,UAAU,EACd,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC1B,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IAC9B,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;CAC9E;AAED,MAAM,WAAW,aAAa;IAC7B,YAAY,EAAE,YAAY,CAAA;IAC1B,cAAc,CAAC,EAAE,eAAe,CAAA;IAChC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,CAAA;CACvF;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,WAAW,CAwChE;AAED,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA"}
@@ -0,0 +1,50 @@
1
+ import { notify } from '@growth-labs/notify';
2
+ import { closeIncident, openIncident } from './dedup.js';
3
+ import { evaluateError, evaluateProbe } from './thresholds.js';
4
+ export function createAlerter(config) {
5
+ const activeIncidentSurfaces = new Set();
6
+ const thresholds = config.alertingConfig ?? {};
7
+ async function send(env, action) {
8
+ if (config.notifyFn)
9
+ return config.notifyFn(env, action);
10
+ return notify(env, {
11
+ ...config.notifyConfig,
12
+ severity: action.severity,
13
+ title: action.title,
14
+ body: action.body,
15
+ dedupKey: action.dedupKey,
16
+ });
17
+ }
18
+ return {
19
+ activeIncidentSurfaces,
20
+ async handleProbeResult(result, surfaceName, db, env) {
21
+ const action = await evaluateProbe(result, surfaceName, db, thresholds);
22
+ if (!action)
23
+ return null;
24
+ if (action.kind === 'open-incident') {
25
+ await openIncident(action, db);
26
+ activeIncidentSurfaces.add(action.surface);
27
+ }
28
+ if (action.kind === 'close-incident' && typeof action.payload?.incidentId === 'string') {
29
+ await closeIncident(action.payload.incidentId, result.id, db);
30
+ activeIncidentSurfaces.delete(action.surface);
31
+ }
32
+ await send(env, action);
33
+ return action;
34
+ },
35
+ async handleErrorEvent(event, db, env) {
36
+ const action = await evaluateError(event, db, thresholds);
37
+ if (!action)
38
+ return null;
39
+ if (action.kind === 'rate-spike')
40
+ activeIncidentSurfaces.add(action.surface);
41
+ await send(env, action);
42
+ return action;
43
+ },
44
+ send,
45
+ };
46
+ }
47
+ export { closeIncident, isIncidentOpen, openIncident } from './dedup.js';
48
+ export { evaluateEscalation } from './escalation.js';
49
+ export { evaluateError, evaluateProbe } from './thresholds.js';
50
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/alerting/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqC,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAQ/E,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACxD,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAyC9D,MAAM,UAAU,aAAa,CAAC,MAAqB;IAClD,MAAM,sBAAsB,GAAG,IAAI,GAAG,EAAU,CAAA;IAChD,MAAM,UAAU,GAAG,MAAM,CAAC,cAAc,IAAI,EAAE,CAAA;IAE9C,KAAK,UAAU,IAAI,CAAC,GAA4B,EAAE,MAAmB;QACpE,IAAI,MAAM,CAAC,QAAQ;YAAE,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;QACxD,OAAO,MAAM,CAAC,GAAgB,EAAE;YAC/B,GAAG,MAAM,CAAC,YAAY;YACtB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,QAAQ,EAAE,MAAM,CAAC,QAAQ;SACzB,CAAC,CAAA;IACH,CAAC;IAED,OAAO;QACN,sBAAsB;QACtB,KAAK,CAAC,iBAAiB,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,EAAE,GAAG;YACnD,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,EAAE,UAAU,CAAC,CAAA;YACvE,IAAI,CAAC,MAAM;gBAAE,OAAO,IAAI,CAAA;YACxB,IAAI,MAAM,CAAC,IAAI,KAAK,eAAe,EAAE,CAAC;gBACrC,MAAM,YAAY,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;gBAC9B,sBAAsB,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;YAC3C,CAAC;YACD,IAAI,MAAM,CAAC,IAAI,KAAK,gBAAgB,IAAI,OAAO,MAAM,CAAC,OAAO,EAAE,UAAU,KAAK,QAAQ,EAAE,CAAC;gBACxF,MAAM,aAAa,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;gBAC7D,sBAAsB,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;YAC9C,CAAC;YACD,MAAM,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;YACvB,OAAO,MAAM,CAAA;QACd,CAAC;QACD,KAAK,CAAC,gBAAgB,CAAC,KAAK,EAAE,EAAE,EAAE,GAAG;YACpC,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,KAAK,EAAE,EAAE,EAAE,UAAU,CAAC,CAAA;YACzD,IAAI,CAAC,MAAM;gBAAE,OAAO,IAAI,CAAA;YACxB,IAAI,MAAM,CAAC,IAAI,KAAK,YAAY;gBAAE,sBAAsB,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;YAC5E,MAAM,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;YACvB,OAAO,MAAM,CAAA;QACd,CAAC;QACD,IAAI;KACJ,CAAA;AACF,CAAC;AAED,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA"}
@@ -0,0 +1,8 @@
1
+ import type { CategorizedEvent } from '../tail/categorize.js';
2
+ import type { CheckResult, ThresholdConfig } from '../types.js';
3
+ import type { AlertAction } from './index.js';
4
+ export declare function evaluateProbe(result: CheckResult, surfaceName: string, db: D1Database, config?: ThresholdConfig): Promise<AlertAction | null>;
5
+ export declare function evaluateError(event: CategorizedEvent & {
6
+ fingerprint: string;
7
+ }, db: D1Database, config?: ThresholdConfig): Promise<AlertAction | null>;
8
+ //# sourceMappingURL=thresholds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thresholds.d.ts","sourceRoot":"","sources":["../../src/alerting/thresholds.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAkB,MAAM,aAAa,CAAA;AAE/E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAkB7C,wBAAsB,aAAa,CAClC,MAAM,EAAE,WAAW,EACnB,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,UAAU,EACd,MAAM,GAAE,eAAoB,GAC1B,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CA8B7B;AAED,wBAAsB,aAAa,CAClC,KAAK,EAAE,gBAAgB,GAAG;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,EACjD,EAAE,EAAE,UAAU,EACd,MAAM,GAAE,eAAoB,GAC1B,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CA2C7B"}
@@ -0,0 +1,105 @@
1
+ import { isIncidentOpen } from './dedup.js';
2
+ const DEFAULT_FAILURES_TO_OPEN = 2;
3
+ const DEFAULT_SUCCESSES_TO_CLOSE = 1;
4
+ const DEFAULT_DEDUP_WINDOW_MS = 60 * 60 * 1000;
5
+ const DEFAULT_RATE_SPIKE_THRESHOLD = 10;
6
+ const DEFAULT_RATE_SPIKE_WINDOW_MS = 5 * 60 * 1000;
7
+ export async function evaluateProbe(result, surfaceName, db, config = {}) {
8
+ const open = await isIncidentOpen(surfaceName, db);
9
+ if (result.status === 'pass') {
10
+ if (!open)
11
+ return null;
12
+ const successesToClose = config.consecutiveSuccessesToClose ?? DEFAULT_SUCCESSES_TO_CLOSE;
13
+ const recent = await recentChecks(db, surfaceName, successesToClose);
14
+ if (recent.length >= successesToClose && recent.every((row) => row.status === 'pass')) {
15
+ return closeIncidentAction(open, result);
16
+ }
17
+ return null;
18
+ }
19
+ if (open)
20
+ return null;
21
+ const failuresToOpen = config.consecutiveFailuresToOpen ?? DEFAULT_FAILURES_TO_OPEN;
22
+ const recent = await recentChecks(db, surfaceName, failuresToOpen);
23
+ if (recent.length < failuresToOpen)
24
+ return null;
25
+ if (!recent.every((row) => row.status !== 'pass'))
26
+ return null;
27
+ const severity = config.minSeverity ?? 'warning';
28
+ return {
29
+ kind: 'open-incident',
30
+ surface: surfaceName,
31
+ severity,
32
+ title: `${surfaceName} synthetic check failing`,
33
+ body: `${failuresToOpen} consecutive synthetic checks failed. Latest status: ${result.status}${result.errorMessage ? ` (${result.errorMessage})` : ''}.`,
34
+ dedupKey: `incident:${surfaceName}`,
35
+ payload: { triggerCheckId: result.id },
36
+ };
37
+ }
38
+ export async function evaluateError(event, db, config = {}) {
39
+ const occurredAtSeconds = Math.floor(event.occurredAt / 1000);
40
+ const rateWindowMs = config.rateSpikeWindowMs ?? DEFAULT_RATE_SPIKE_WINDOW_MS;
41
+ const rateSpikeThreshold = config.rateSpikeThreshold ?? DEFAULT_RATE_SPIKE_THRESHOLD;
42
+ const rateCount = await countFingerprintSince(db, event.fingerprint, occurredAtSeconds - Math.floor(rateWindowMs / 1000));
43
+ if (rateCount >= rateSpikeThreshold) {
44
+ return {
45
+ kind: 'rate-spike',
46
+ surface: event.surface,
47
+ severity: 'critical',
48
+ title: `${event.surface} error rate spike`,
49
+ body: `${rateCount} events with fingerprint ${event.fingerprint} occurred in the last ${Math.round(rateWindowMs / 60_000)} minutes.`,
50
+ dedupKey: `rate-spike:${event.surface}:${event.fingerprint}`,
51
+ payload: { fingerprint: event.fingerprint, count: rateCount },
52
+ };
53
+ }
54
+ const dedupWindowMs = config.newErrorDedupWindowMs ?? DEFAULT_DEDUP_WINDOW_MS;
55
+ const dedupCount = await countFingerprintSince(db, event.fingerprint, occurredAtSeconds - Math.floor(dedupWindowMs / 1000));
56
+ if (dedupCount > 0)
57
+ return null;
58
+ return {
59
+ kind: 'new-error',
60
+ surface: event.surface,
61
+ severity: event.category === 'console-warn' || event.category === 'slow-request'
62
+ ? 'warning'
63
+ : 'critical',
64
+ title: `${event.surface} new error fingerprint`,
65
+ body: `${event.category} fingerprint ${event.fingerprint}: ${event.message}`,
66
+ dedupKey: `new-error:${event.fingerprint}`,
67
+ payload: { fingerprint: event.fingerprint, category: event.category },
68
+ };
69
+ }
70
+ async function recentChecks(db, surface, limit) {
71
+ const { results } = await db
72
+ .prepare(`
73
+ SELECT id, status, checked_at
74
+ FROM gl_uptime_checks
75
+ WHERE surface = ?
76
+ ORDER BY checked_at DESC
77
+ LIMIT ?
78
+ `)
79
+ .bind(surface, limit)
80
+ .all();
81
+ return results ?? [];
82
+ }
83
+ async function countFingerprintSince(db, fingerprint, since) {
84
+ const row = await db
85
+ .prepare(`
86
+ SELECT COUNT(*) AS count
87
+ FROM gl_errors
88
+ WHERE fingerprint = ? AND occurred_at >= ?
89
+ `)
90
+ .bind(fingerprint, since)
91
+ .first();
92
+ return Number(row?.count ?? 0);
93
+ }
94
+ function closeIncidentAction(open, result) {
95
+ return {
96
+ kind: 'close-incident',
97
+ surface: open.surface,
98
+ severity: 'info',
99
+ title: `${open.surface} recovered`,
100
+ body: `Synthetic check ${result.id ?? 'unknown'} passed and closed incident ${open.id}.`,
101
+ dedupKey: `incident-closed:${open.id}`,
102
+ payload: { incidentId: open.id, resolveCheckId: result.id },
103
+ };
104
+ }
105
+ //# sourceMappingURL=thresholds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thresholds.js","sourceRoot":"","sources":["../../src/alerting/thresholds.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export type { AlertAction, AlertEngine, AlertKind } from './alerting/index.js';
2
+ export { createAlerter } from './alerting/index.js';
3
+ export type { ProberConfig } from './prober/index.js';
4
+ export { createProber } from './prober/index.js';
5
+ export type { StatusPageAppConfig } from './status-page/app.js';
6
+ export { createStatusPageApp } from './status-page/app.js';
7
+ export type { TailWorkerConfig } from './tail/index.js';
8
+ export { createTailWorker } from './tail/index.js';
9
+ export type { AlertSeverity, CheckResult, CheckStatus, CheckType, ErrorRollup, ErrorSeverity, NotifyConfig, PersistedCheckResult, SamplingConfig, StatusPageConfig, SurfaceStatus, ThresholdConfig, UptimeIncident, } from './types.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAC9E,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnD,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,YAAY,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC1D,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAClD,YAAY,EACX,aAAa,EACb,WAAW,EACX,WAAW,EACX,SAAS,EACT,WAAW,EACX,aAAa,EACb,YAAY,EACZ,oBAAoB,EACpB,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,eAAe,EACf,cAAc,GACd,MAAM,YAAY,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { createAlerter } from './alerting/index.js';
2
+ export { createProber } from './prober/index.js';
3
+ export { createStatusPageApp } from './status-page/app.js';
4
+ export { createTailWorker } from './tail/index.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAEnD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAEhD,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAE1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA"}
@@ -0,0 +1,19 @@
1
+ import type { ExecutionContextLike, NotifyConfig, ProbeAlertingConfig, ScheduledEventLike } from '../types.js';
2
+ import type { SurfaceConfig } from './surfaces.js';
3
+ export interface ProberConfig {
4
+ realmId: string;
5
+ d1Binding: string;
6
+ notifyConfig: NotifyConfig;
7
+ alertingConfig?: ProbeAlertingConfig;
8
+ surfaces: SurfaceConfig[];
9
+ }
10
+ export declare function createProber(config: ProberConfig): {
11
+ scheduledHandler: (event: ScheduledEventLike, env: Record<string, unknown>, ctx: ExecutionContextLike) => Promise<void>;
12
+ checkSurface: (surface: SurfaceConfig, env: Record<string, unknown>) => Promise<void>;
13
+ };
14
+ export { persistCheckResult } from './persist.js';
15
+ export { runGet } from './runners/get-runner.js';
16
+ export { extractCodeFromMessage, runHappyPath } from './runners/happy-path-runner.js';
17
+ export { runPost } from './runners/post-runner.js';
18
+ export type { CodeSigninHappyPathConfig, GetSurface, GmailPollingConfig, HappyPathSurface, PostSurface, SurfaceConfig, } from './surfaces.js';
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/prober/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,oBAAoB,EACpB,YAAY,EACZ,mBAAmB,EACnB,kBAAkB,EAClB,MAAM,aAAa,CAAA;AAMpB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAElD,MAAM,WAAW,YAAY;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,YAAY,CAAA;IAC1B,cAAc,CAAC,EAAE,mBAAmB,CAAA;IACpC,QAAQ,EAAE,aAAa,EAAE,CAAA;CACzB;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY;8BAwBxC,kBAAkB,OACpB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,OACvB,oBAAoB,KACvB,OAAO,CAAC,IAAI,CAAC;4BArBqB,aAAa,OAAO,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,OAAO,CAAC,IAAI,CAAC;EA8BhG;AAaD,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAA;AAChD,OAAO,EAAE,sBAAsB,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAA;AACrF,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAA;AAClD,YAAY,EACX,yBAAyB,EACzB,UAAU,EACV,kBAAkB,EAClB,gBAAgB,EAChB,WAAW,EACX,aAAa,GACb,MAAM,eAAe,CAAA"}
@@ -0,0 +1,48 @@
1
+ import { createAlerter } from '../alerting/index.js';
2
+ import { errorMessage } from '../types.js';
3
+ import { persistCheckResult } from './persist.js';
4
+ import { runGet } from './runners/get-runner.js';
5
+ import { runHappyPath } from './runners/happy-path-runner.js';
6
+ import { runPost } from './runners/post-runner.js';
7
+ export function createProber(config) {
8
+ const alerter = createAlerter({
9
+ notifyConfig: config.notifyConfig,
10
+ alertingConfig: config.alertingConfig,
11
+ });
12
+ async function checkSurface(surface, env) {
13
+ const db = env[config.d1Binding];
14
+ try {
15
+ const result = await runSurface(surface, env);
16
+ if (!db) {
17
+ console.error(`[monitoring] missing D1 binding env.${config.d1Binding}`);
18
+ return;
19
+ }
20
+ const persisted = await persistCheckResult(db, surface, result);
21
+ await alerter.handleProbeResult(persisted, surface.name, db, env);
22
+ }
23
+ catch (error) {
24
+ console.error(`[monitoring] synthetic check failed for ${surface.name}: ${errorMessage(error)}`);
25
+ }
26
+ }
27
+ async function scheduledHandler(event, env, ctx) {
28
+ for (const surface of config.surfaces.filter((candidate) => candidate.schedule === event.cron)) {
29
+ ctx.waitUntil(checkSurface(surface, env));
30
+ }
31
+ }
32
+ return { scheduledHandler, checkSurface };
33
+ }
34
+ async function runSurface(surface, env) {
35
+ switch (surface.kind) {
36
+ case 'get':
37
+ return runGet(surface);
38
+ case 'post':
39
+ return runPost(surface);
40
+ case 'happy_path':
41
+ return runHappyPath(surface, { env });
42
+ }
43
+ }
44
+ export { persistCheckResult } from './persist.js';
45
+ export { runGet } from './runners/get-runner.js';
46
+ export { extractCodeFromMessage, runHappyPath } from './runners/happy-path-runner.js';
47
+ export { runPost } from './runners/post-runner.js';
48
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/prober/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAOpD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAA;AAChD,OAAO,EAAyB,YAAY,EAAE,MAAM,gCAAgC,CAAA;AACpF,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAA;AAWlD,MAAM,UAAU,YAAY,CAAC,MAAoB;IAChD,MAAM,OAAO,GAAG,aAAa,CAAC;QAC7B,YAAY,EAAE,MAAM,CAAC,YAAY;QACjC,cAAc,EAAE,MAAM,CAAC,cAAc;KACrC,CAAC,CAAA;IAEF,KAAK,UAAU,YAAY,CAAC,OAAsB,EAAE,GAA4B;QAC/E,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,SAAS,CAA2B,CAAA;QAC1D,IAAI,CAAC;YACJ,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;YAC7C,IAAI,CAAC,EAAE,EAAE,CAAC;gBACT,OAAO,CAAC,KAAK,CAAC,uCAAuC,MAAM,CAAC,SAAS,EAAE,CAAC,CAAA;gBACxE,OAAM;YACP,CAAC;YACD,MAAM,SAAS,GAAG,MAAM,kBAAkB,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,CAAA;YAC/D,MAAM,OAAO,CAAC,iBAAiB,CAAC,SAAS,EAAE,OAAO,CAAC,IAAI,EAAE,EAAE,EAAE,GAAG,CAAC,CAAA;QAClE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,OAAO,CAAC,KAAK,CACZ,2CAA2C,OAAO,CAAC,IAAI,KAAK,YAAY,CAAC,KAAK,CAAC,EAAE,CACjF,CAAA;QACF,CAAC;IACF,CAAC;IAED,KAAK,UAAU,gBAAgB,CAC9B,KAAyB,EACzB,GAA4B,EAC5B,GAAyB;QAEzB,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAC3C,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,QAAQ,KAAK,KAAK,CAAC,IAAI,CAChD,EAAE,CAAC;YACH,GAAG,CAAC,SAAS,CAAC,YAAY,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAA;QAC1C,CAAC;IACF,CAAC;IAED,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,CAAA;AAC1C,CAAC;AAED,KAAK,UAAU,UAAU,CAAC,OAAsB,EAAE,GAA4B;IAC7E,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;QACtB,KAAK,KAAK;YACT,OAAO,MAAM,CAAC,OAAO,CAAC,CAAA;QACvB,KAAK,MAAM;YACV,OAAO,OAAO,CAAC,OAAO,CAAC,CAAA;QACxB,KAAK,YAAY;YAChB,OAAO,YAAY,CAAC,OAAO,EAAE,EAAE,GAAG,EAA6B,CAAC,CAAA;IAClE,CAAC;AACF,CAAC;AAED,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAA;AAChD,OAAO,EAAE,sBAAsB,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAA;AACrF,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAA"}
@@ -0,0 +1,4 @@
1
+ import { type CheckResult, type IdFactory, type PersistedCheckResult } from '../types.js';
2
+ import type { SurfaceConfig } from './surfaces.js';
3
+ export declare function persistCheckResult(db: D1Database, surface: SurfaceConfig, result: CheckResult, idFactory?: IdFactory, now?: () => number): Promise<PersistedCheckResult>;
4
+ //# sourceMappingURL=persist.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persist.d.ts","sourceRoot":"","sources":["../../src/prober/persist.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,WAAW,EAGhB,KAAK,SAAS,EACd,KAAK,oBAAoB,EACzB,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAElD,wBAAsB,kBAAkB,CACvC,EAAE,EAAE,UAAU,EACd,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,WAAW,EACnB,SAAS,GAAE,SAAsB,EACjC,GAAG,eAAW,GACZ,OAAO,CAAC,oBAAoB,CAAC,CA8B/B"}
@@ -0,0 +1,21 @@
1
+ import { errorMessage, generateId, } from '../types.js';
2
+ export async function persistCheckResult(db, surface, result, idFactory = generateId, now = Date.now) {
3
+ const id = result.id ?? idFactory();
4
+ const checkedAt = result.checkedAt ?? Math.floor(now() / 1000);
5
+ const persisted = { ...result, id, checkedAt };
6
+ try {
7
+ await db
8
+ .prepare(`
9
+ INSERT INTO gl_uptime_checks
10
+ (id, surface, check_type, status, status_code, latency_ms, error_message, checked_at)
11
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
12
+ `)
13
+ .bind(id, surface.name, surface.kind, result.status, result.statusCode ?? null, result.latencyMs, result.errorMessage ?? null, checkedAt)
14
+ .run();
15
+ }
16
+ catch (error) {
17
+ console.error(`[monitoring] failed to persist uptime check ${surface.name}: ${errorMessage(error)}`);
18
+ }
19
+ return persisted;
20
+ }
21
+ //# sourceMappingURL=persist.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persist.js","sourceRoot":"","sources":["../../src/prober/persist.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,YAAY,EACZ,UAAU,GAGV,MAAM,aAAa,CAAA;AAGpB,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACvC,EAAc,EACd,OAAsB,EACtB,MAAmB,EACnB,YAAuB,UAAU,EACjC,GAAG,GAAG,IAAI,CAAC,GAAG;IAEd,MAAM,EAAE,GAAG,MAAM,CAAC,EAAE,IAAI,SAAS,EAAE,CAAA;IACnC,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;IAC9D,MAAM,SAAS,GAAyB,EAAE,GAAG,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,CAAA;IAEpE,IAAI,CAAC;QACJ,MAAM,EAAE;aACN,OAAO,CAAC;;;;IAIR,CAAC;aACD,IAAI,CACJ,EAAE,EACF,OAAO,CAAC,IAAI,EACZ,OAAO,CAAC,IAAI,EACZ,MAAM,CAAC,MAAM,EACb,MAAM,CAAC,UAAU,IAAI,IAAI,EACzB,MAAM,CAAC,SAAS,EAChB,MAAM,CAAC,YAAY,IAAI,IAAI,EAC3B,SAAS,CACT;aACA,GAAG,EAAE,CAAA;IACR,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,OAAO,CAAC,KAAK,CACZ,+CAA+C,OAAO,CAAC,IAAI,KAAK,YAAY,CAAC,KAAK,CAAC,EAAE,CACrF,CAAA;IACF,CAAC;IAED,OAAO,SAAS,CAAA;AACjB,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { type CheckResult, type RuntimeFetch } from '../../types.js';
2
+ import type { GetSurface } from '../surfaces.js';
3
+ export declare function runGet(surface: GetSurface, runtime?: RuntimeFetch): Promise<CheckResult>;
4
+ //# sourceMappingURL=get-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-runner.d.ts","sourceRoot":"","sources":["../../../src/prober/runners/get-runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAkC,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAA;AACpG,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAEhD,wBAAsB,MAAM,CAC3B,OAAO,EAAE,UAAU,EACnB,OAAO,GAAE,YAAiB,GACxB,OAAO,CAAC,WAAW,CAAC,CA0CtB"}