@littlebearapps/platform-consumer-sdk 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.
package/src/errors.ts ADDED
@@ -0,0 +1,285 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+
3
+ /**
4
+ * Platform SDK Errors
5
+ *
6
+ * Error tracking and categorisation utilities for the Platform SDK.
7
+ * Integrates with telemetry to report errors to the platform-usage consumer.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { withFeatureBudget, reportError, completeTracking } from './lib/platform-sdk';
12
+ *
13
+ * const trackedEnv = withFeatureBudget(env, 'my-app:api:users', { ctx });
14
+ * try {
15
+ * await riskyOperation(trackedEnv);
16
+ * } catch (error) {
17
+ * reportError(trackedEnv, error);
18
+ * // Handle error...
19
+ * }
20
+ * await completeTracking(trackedEnv); // Errors included in telemetry
21
+ * ```
22
+ */
23
+
24
+ import type { ErrorCategory, MetricsAccumulator } from './types';
25
+ import { CircuitBreakerError } from './types';
26
+ import { getTelemetryContext } from './telemetry';
27
+ import { createLogger, type Logger } from './logging';
28
+ import { TimeoutError } from './timeout';
29
+
30
+ // =============================================================================
31
+ // MODULE LOGGER (lazy-initialized to avoid global scope crypto calls)
32
+ // =============================================================================
33
+
34
+ let _log: Logger | null = null;
35
+ function getLog(): Logger {
36
+ if (!_log) {
37
+ _log = createLogger({
38
+ worker: 'platform-sdk',
39
+ featureId: 'platform:sdk:errors',
40
+ });
41
+ }
42
+ return _log;
43
+ }
44
+
45
+ // =============================================================================
46
+ // ERROR CATEGORISATION
47
+ // =============================================================================
48
+
49
+ /**
50
+ * Categorise an error based on its type and message.
51
+ * Uses error name, message patterns, and known error types.
52
+ */
53
+ export function categoriseError(error: unknown): ErrorCategory {
54
+ if (error instanceof CircuitBreakerError) {
55
+ return 'CIRCUIT_BREAKER';
56
+ }
57
+
58
+ if (error instanceof TimeoutError) {
59
+ return 'TIMEOUT';
60
+ }
61
+
62
+ if (error instanceof Error) {
63
+ const name = error.name.toLowerCase();
64
+ const message = error.message.toLowerCase();
65
+
66
+ // Auth errors
67
+ if (
68
+ name.includes('auth') ||
69
+ name.includes('unauthorized') ||
70
+ name.includes('forbidden') ||
71
+ message.includes('unauthorized') ||
72
+ message.includes('forbidden') ||
73
+ message.includes('401') ||
74
+ message.includes('403')
75
+ ) {
76
+ return 'AUTH';
77
+ }
78
+
79
+ // Rate limit errors
80
+ if (
81
+ name.includes('ratelimit') ||
82
+ message.includes('rate limit') ||
83
+ message.includes('too many requests') ||
84
+ message.includes('429')
85
+ ) {
86
+ return 'RATE_LIMIT';
87
+ }
88
+
89
+ // Network/timeout errors
90
+ if (
91
+ name.includes('timeout') ||
92
+ name.includes('network') ||
93
+ name.includes('fetch') ||
94
+ message.includes('timeout') ||
95
+ message.includes('network') ||
96
+ message.includes('econnrefused') ||
97
+ message.includes('enotfound') ||
98
+ message.includes('socket hang up')
99
+ ) {
100
+ return 'NETWORK';
101
+ }
102
+
103
+ // Validation errors
104
+ if (
105
+ name.includes('validation') ||
106
+ name.includes('schema') ||
107
+ name.includes('parse') ||
108
+ message.includes('invalid') ||
109
+ message.includes('required') ||
110
+ message.includes('expected')
111
+ ) {
112
+ return 'VALIDATION';
113
+ }
114
+
115
+ // D1 errors
116
+ if (name.includes('d1') || message.includes('d1_error') || message.includes('sqlite')) {
117
+ return 'D1_ERROR';
118
+ }
119
+
120
+ // KV errors
121
+ if (name.includes('kv') || message.includes('kv_error') || message.includes('namespace')) {
122
+ return 'KV_ERROR';
123
+ }
124
+
125
+ // Queue errors
126
+ if (name.includes('queue') || message.includes('queue_error')) {
127
+ return 'QUEUE_ERROR';
128
+ }
129
+
130
+ // External API errors (by common status codes)
131
+ if (
132
+ message.includes('500') ||
133
+ message.includes('502') ||
134
+ message.includes('503') ||
135
+ message.includes('504')
136
+ ) {
137
+ return 'EXTERNAL_API';
138
+ }
139
+ }
140
+
141
+ return 'INTERNAL';
142
+ }
143
+
144
+ /**
145
+ * Extract error code from an error if available.
146
+ */
147
+ export function extractErrorCode(error: unknown): string | undefined {
148
+ if (error && typeof error === 'object') {
149
+ const errorObj = error as Record<string, unknown>;
150
+ if (typeof errorObj.code === 'string') {
151
+ return errorObj.code;
152
+ }
153
+ if (typeof errorObj.errno === 'string') {
154
+ return errorObj.errno;
155
+ }
156
+ if (typeof errorObj.status === 'number') {
157
+ return `HTTP_${errorObj.status}`;
158
+ }
159
+ }
160
+ return undefined;
161
+ }
162
+
163
+ // =============================================================================
164
+ // ERROR TRACKING
165
+ // =============================================================================
166
+
167
+ /**
168
+ * Track an error in the metrics accumulator.
169
+ * Used internally by proxy wrappers and reportError.
170
+ */
171
+ export function trackError(metrics: MetricsAccumulator, error: unknown): void {
172
+ metrics.errorCount += 1;
173
+ metrics.lastErrorCategory = categoriseError(error);
174
+
175
+ const code = extractErrorCode(error);
176
+ if (code && !metrics.errorCodes.includes(code)) {
177
+ // Keep at most 10 unique error codes
178
+ if (metrics.errorCodes.length < 10) {
179
+ metrics.errorCodes.push(code);
180
+ }
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Report an error for a tracked environment.
186
+ * Call this when you catch an error that should be included in telemetry.
187
+ *
188
+ * @param env - The tracked environment from withFeatureBudget
189
+ * @param error - The error to report
190
+ *
191
+ * @example
192
+ * ```typescript
193
+ * try {
194
+ * await riskyOperation(trackedEnv);
195
+ * } catch (error) {
196
+ * reportError(trackedEnv, error);
197
+ * throw error; // Re-throw or handle
198
+ * }
199
+ * ```
200
+ */
201
+ export function reportError(env: object, error: unknown): void {
202
+ const context = getTelemetryContext(env);
203
+ if (!context) {
204
+ getLog().warn('reportError called without telemetry context');
205
+ return;
206
+ }
207
+
208
+ trackError(context.metrics, error);
209
+ }
210
+
211
+ /**
212
+ * Report an error with explicit category and code.
213
+ * Use when you know the error type better than automatic categorisation.
214
+ *
215
+ * @param env - The tracked environment from withFeatureBudget
216
+ * @param category - Error category
217
+ * @param code - Optional error code
218
+ *
219
+ * @example
220
+ * ```typescript
221
+ * if (!response.ok) {
222
+ * reportErrorExplicit(trackedEnv, 'EXTERNAL_API', `HTTP_${response.status}`);
223
+ * }
224
+ * ```
225
+ */
226
+ export function reportErrorExplicit(env: object, category: ErrorCategory, code?: string): void {
227
+ const context = getTelemetryContext(env);
228
+ if (!context) {
229
+ getLog().warn('reportErrorExplicit called without telemetry context');
230
+ return;
231
+ }
232
+
233
+ context.metrics.errorCount += 1;
234
+ context.metrics.lastErrorCategory = category;
235
+
236
+ if (code && !context.metrics.errorCodes.includes(code)) {
237
+ if (context.metrics.errorCodes.length < 10) {
238
+ context.metrics.errorCodes.push(code);
239
+ }
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Check if a tracked environment has recorded any errors.
245
+ */
246
+ export function hasErrors(env: object): boolean {
247
+ const context = getTelemetryContext(env);
248
+ return context ? context.metrics.errorCount > 0 : false;
249
+ }
250
+
251
+ /**
252
+ * Get the error count for a tracked environment.
253
+ */
254
+ export function getErrorCount(env: object): number {
255
+ const context = getTelemetryContext(env);
256
+ return context?.metrics.errorCount ?? 0;
257
+ }
258
+
259
+ // =============================================================================
260
+ // ERROR WRAPPER
261
+ // =============================================================================
262
+
263
+ /**
264
+ * Wrap an async function to automatically track errors.
265
+ * Errors are tracked and then re-thrown.
266
+ *
267
+ * @param env - The tracked environment from withFeatureBudget
268
+ * @param fn - The async function to wrap
269
+ * @returns The result of the function
270
+ *
271
+ * @example
272
+ * ```typescript
273
+ * const result = await withErrorTracking(trackedEnv, async () => {
274
+ * return await riskyOperation();
275
+ * });
276
+ * ```
277
+ */
278
+ export async function withErrorTracking<T>(env: object, fn: () => Promise<T>): Promise<T> {
279
+ try {
280
+ return await fn();
281
+ } catch (error) {
282
+ reportError(env, error);
283
+ throw error;
284
+ }
285
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Platform Feature IDs
3
+ *
4
+ * Defines all feature identifiers for Platform's own workers.
5
+ * Format: 'platform:<category>:<feature>'
6
+ *
7
+ * Categories:
8
+ * - ingest: Data ingestion workers (GitHub, Stripe, Logger)
9
+ * - connector: External service connectors (Stripe, Ads, GA4, Plausible)
10
+ * - monitor: Monitoring workers (Alert Router, Cost Spike, Observer, GitHub)
11
+ * - discovery: Discovery workers (Topology)
12
+ * - test: Test workers (Ingest, Query, Healthcheck)
13
+ */
14
+
15
+ import type { FeatureId } from './types';
16
+
17
+ // =============================================================================
18
+ // INGEST FEATURES (3)
19
+ // Workers that ingest data from external sources
20
+ // =============================================================================
21
+
22
+ /** GitHub webhook ingestion (issues, PRs, deployments) */
23
+ export const INGEST_GITHUB: FeatureId = 'platform:ingest:github';
24
+
25
+ /** Stripe webhook ingestion (payments, subscriptions) */
26
+ export const INGEST_STRIPE: FeatureId = 'platform:ingest:stripe';
27
+
28
+ /** Log ingestion from external sources */
29
+ export const INGEST_LOGGER: FeatureId = 'platform:ingest:logger';
30
+
31
+ // =============================================================================
32
+ // CONNECTOR FEATURES (4)
33
+ // Workers that connect to external analytics services
34
+ // =============================================================================
35
+
36
+ /** Stripe connector for billing data */
37
+ export const CONNECTOR_STRIPE: FeatureId = 'platform:connector:stripe';
38
+
39
+ /** Google Ads connector for ad performance data */
40
+ export const CONNECTOR_ADS: FeatureId = 'platform:connector:ads';
41
+
42
+ /** Google Analytics 4 connector */
43
+ export const CONNECTOR_GA4: FeatureId = 'platform:connector:ga4';
44
+
45
+ /** Plausible Analytics connector */
46
+ export const CONNECTOR_PLAUSIBLE: FeatureId = 'platform:connector:plausible';
47
+
48
+ // =============================================================================
49
+ // MONITOR FEATURES (4)
50
+ // Workers that monitor system health and costs
51
+ // =============================================================================
52
+
53
+ /** Alert router - routes alerts to Slack, creates GitHub issues */
54
+ export const MONITOR_ALERT_ROUTER: FeatureId = 'platform:monitor:alert-router';
55
+
56
+ /** Cost spike alerter - detects cost anomalies */
57
+ export const MONITOR_COST_SPIKE: FeatureId = 'platform:monitor:cost-spike';
58
+
59
+ /** Platform observer - watches GitHub webhooks for circuit breaker state */
60
+ export const MONITOR_OBSERVER: FeatureId = 'platform:monitor:observer';
61
+
62
+ /** GitHub monitor - monitors GitHub API events */
63
+ export const MONITOR_GITHUB: FeatureId = 'platform:monitor:github';
64
+
65
+ /** SDK integration auditor - weekly triangulation audits */
66
+ export const MONITOR_AUDITOR: FeatureId = 'platform:monitor:auditor';
67
+
68
+ /** Pattern discovery - AI-assisted transient error pattern detection */
69
+ export const MONITOR_PATTERN_DISCOVERY: FeatureId = 'platform:monitor:pattern-discovery';
70
+
71
+ // =============================================================================
72
+ // DISCOVERY FEATURES (1)
73
+ // Workers that discover infrastructure topology
74
+ // =============================================================================
75
+
76
+ /** Topology discovery - discovers services, health, deployments */
77
+ export const DISCOVERY_TOPOLOGY: FeatureId = 'platform:discovery:topology';
78
+
79
+ // =============================================================================
80
+ // TEST FEATURES (3)
81
+ // Workers used for testing Platform SDK and infrastructure
82
+ // Note: test-client is already integrated with 'test-client:validation:sdk-test'
83
+ // =============================================================================
84
+
85
+ /** Test ingest worker */
86
+ export const TEST_INGEST: FeatureId = 'platform:test:ingest';
87
+
88
+ /** Test query worker */
89
+ export const TEST_QUERY: FeatureId = 'platform:test:query';
90
+
91
+ /** Test healthcheck worker */
92
+ export const TEST_HEALTHCHECK: FeatureId = 'platform:test:healthcheck';
93
+
94
+ // =============================================================================
95
+ // HEARTBEAT FEATURE
96
+ // Used for health checks across all Platform workers
97
+ // =============================================================================
98
+
99
+ /** Generic heartbeat/health check feature */
100
+ export const HEARTBEAT_HEALTH: FeatureId = 'platform:heartbeat:health';
101
+
102
+ // =============================================================================
103
+ // EMAIL FEATURES
104
+ // Email system health monitoring
105
+ // =============================================================================
106
+
107
+ /** Email system health check - per-brand validation */
108
+ export const EMAIL_HEALTHCHECK: FeatureId = 'platform:email:healthcheck';
109
+
110
+ // =============================================================================
111
+ // ALL FEATURES (for budgets.yaml generation)
112
+ // =============================================================================
113
+
114
+ export const PLATFORM_FEATURES = {
115
+ // Ingest
116
+ INGEST_GITHUB,
117
+ INGEST_STRIPE,
118
+ INGEST_LOGGER,
119
+ // Connectors
120
+ CONNECTOR_STRIPE,
121
+ CONNECTOR_ADS,
122
+ CONNECTOR_GA4,
123
+ CONNECTOR_PLAUSIBLE,
124
+ // Monitors
125
+ MONITOR_ALERT_ROUTER,
126
+ MONITOR_COST_SPIKE,
127
+ MONITOR_OBSERVER,
128
+ MONITOR_GITHUB,
129
+ MONITOR_AUDITOR,
130
+ MONITOR_PATTERN_DISCOVERY,
131
+ // Discovery
132
+ DISCOVERY_TOPOLOGY,
133
+ // Test
134
+ TEST_INGEST,
135
+ TEST_QUERY,
136
+ TEST_HEALTHCHECK,
137
+ // Heartbeat
138
+ HEARTBEAT_HEALTH,
139
+ // Email
140
+ EMAIL_HEALTHCHECK,
141
+ } as const;
142
+
143
+ /**
144
+ * Get all Platform feature IDs as an array.
145
+ * Useful for iterating over all features.
146
+ */
147
+ export function getAllPlatformFeatures(): FeatureId[] {
148
+ return Object.values(PLATFORM_FEATURES);
149
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Gatus external endpoint heartbeat helper.
3
+ *
4
+ * All Platform cron workers call this on success/failure to push
5
+ * heartbeat status to the self-hosted Gatus instance at
6
+ * status.littlebearapps.com.
7
+ *
8
+ * Gatus external endpoints accept:
9
+ * POST {url}?success=true|false
10
+ * Authorization: Bearer {token}
11
+ *
12
+ * @see docs/quickrefs/monitoring.md
13
+ */
14
+ export function pingHeartbeat(
15
+ ctx: ExecutionContext,
16
+ url: string | undefined,
17
+ token: string | undefined,
18
+ success: boolean
19
+ ): void {
20
+ if (!url || !token) return;
21
+ ctx.waitUntil(
22
+ fetch(`${url}?success=${success}`, {
23
+ method: 'POST',
24
+ headers: { Authorization: `Bearer ${token}` },
25
+ }).catch(() => {})
26
+ );
27
+ }