@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/README.md +306 -0
- package/package.json +53 -0
- package/src/ai-gateway.ts +305 -0
- package/src/constants.ts +147 -0
- package/src/costs.ts +590 -0
- package/src/do-heartbeat.ts +249 -0
- package/src/dynamic-patterns.ts +273 -0
- package/src/errors.ts +285 -0
- package/src/features.ts +149 -0
- package/src/heartbeat.ts +27 -0
- package/src/index.ts +950 -0
- package/src/logging.ts +543 -0
- package/src/middleware.ts +447 -0
- package/src/patterns.ts +156 -0
- package/src/proxy.ts +732 -0
- package/src/retry.ts +19 -0
- package/src/service-client.ts +291 -0
- package/src/telemetry.ts +342 -0
- package/src/timeout.ts +212 -0
- package/src/tracing.ts +403 -0
- package/src/types.ts +465 -0
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
|
+
}
|
package/src/features.ts
ADDED
|
@@ -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
|
+
}
|
package/src/heartbeat.ts
ADDED
|
@@ -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
|
+
}
|