@littlebearapps/platform-admin-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.
Files changed (94) hide show
  1. package/README.md +112 -0
  2. package/dist/index.d.ts +16 -0
  3. package/dist/index.js +89 -0
  4. package/dist/prompts.d.ts +27 -0
  5. package/dist/prompts.js +80 -0
  6. package/dist/scaffold.d.ts +5 -0
  7. package/dist/scaffold.js +65 -0
  8. package/dist/templates.d.ts +16 -0
  9. package/dist/templates.js +131 -0
  10. package/package.json +46 -0
  11. package/templates/full/migrations/006_pattern_discovery.sql +199 -0
  12. package/templates/full/migrations/007_notifications_search.sql +127 -0
  13. package/templates/full/workers/lib/pattern-discovery/ai-prompt.ts +644 -0
  14. package/templates/full/workers/lib/pattern-discovery/clustering.ts +278 -0
  15. package/templates/full/workers/lib/pattern-discovery/shadow-evaluation.ts +603 -0
  16. package/templates/full/workers/lib/pattern-discovery/storage.ts +806 -0
  17. package/templates/full/workers/lib/pattern-discovery/types.ts +159 -0
  18. package/templates/full/workers/lib/pattern-discovery/validation.ts +278 -0
  19. package/templates/full/workers/pattern-discovery.ts +661 -0
  20. package/templates/full/workers/platform-alert-router.ts +1809 -0
  21. package/templates/full/workers/platform-notifications.ts +424 -0
  22. package/templates/full/workers/platform-search.ts +480 -0
  23. package/templates/full/workers/platform-settings.ts +436 -0
  24. package/templates/full/wrangler.alert-router.jsonc.hbs +34 -0
  25. package/templates/full/wrangler.notifications.jsonc.hbs +23 -0
  26. package/templates/full/wrangler.pattern-discovery.jsonc.hbs +33 -0
  27. package/templates/full/wrangler.search.jsonc.hbs +16 -0
  28. package/templates/full/wrangler.settings.jsonc.hbs +23 -0
  29. package/templates/shared/README.md.hbs +69 -0
  30. package/templates/shared/config/budgets.yaml.hbs +72 -0
  31. package/templates/shared/config/services.yaml.hbs +45 -0
  32. package/templates/shared/migrations/001_core_tables.sql +117 -0
  33. package/templates/shared/migrations/002_usage_warehouse.sql +830 -0
  34. package/templates/shared/migrations/003_feature_tracking.sql +250 -0
  35. package/templates/shared/migrations/004_settings_alerts.sql +452 -0
  36. package/templates/shared/migrations/seed.sql.hbs +4 -0
  37. package/templates/shared/package.json.hbs +21 -0
  38. package/templates/shared/scripts/sync-config.ts +242 -0
  39. package/templates/shared/tsconfig.json +12 -0
  40. package/templates/shared/workers/lib/analytics-engine.ts +357 -0
  41. package/templates/shared/workers/lib/billing.ts +293 -0
  42. package/templates/shared/workers/lib/circuit-breaker-middleware.ts +25 -0
  43. package/templates/shared/workers/lib/control.ts +292 -0
  44. package/templates/shared/workers/lib/economics.ts +368 -0
  45. package/templates/shared/workers/lib/metrics.ts +103 -0
  46. package/templates/shared/workers/lib/platform-settings.ts +407 -0
  47. package/templates/shared/workers/lib/shared/allowances.ts +333 -0
  48. package/templates/shared/workers/lib/shared/cloudflare.ts +1362 -0
  49. package/templates/shared/workers/lib/shared/types.ts +58 -0
  50. package/templates/shared/workers/lib/telemetry-sampling.ts +360 -0
  51. package/templates/shared/workers/lib/usage/collectors/example.ts +96 -0
  52. package/templates/shared/workers/lib/usage/collectors/index.ts +128 -0
  53. package/templates/shared/workers/lib/usage/handlers/audit.ts +306 -0
  54. package/templates/shared/workers/lib/usage/handlers/backfill.ts +845 -0
  55. package/templates/shared/workers/lib/usage/handlers/behavioral.ts +429 -0
  56. package/templates/shared/workers/lib/usage/handlers/data-queries.ts +507 -0
  57. package/templates/shared/workers/lib/usage/handlers/dlq-admin.ts +364 -0
  58. package/templates/shared/workers/lib/usage/handlers/health-trends.ts +222 -0
  59. package/templates/shared/workers/lib/usage/handlers/index.ts +35 -0
  60. package/templates/shared/workers/lib/usage/handlers/usage-admin.ts +421 -0
  61. package/templates/shared/workers/lib/usage/handlers/usage-features.ts +1262 -0
  62. package/templates/shared/workers/lib/usage/handlers/usage-metrics.ts +2420 -0
  63. package/templates/shared/workers/lib/usage/handlers/usage-settings.ts +610 -0
  64. package/templates/shared/workers/lib/usage/queue/budget-enforcement.ts +1032 -0
  65. package/templates/shared/workers/lib/usage/queue/cost-budget-enforcement.ts +128 -0
  66. package/templates/shared/workers/lib/usage/queue/cost-calculator.ts +77 -0
  67. package/templates/shared/workers/lib/usage/queue/dlq-handler.ts +161 -0
  68. package/templates/shared/workers/lib/usage/queue/index.ts +19 -0
  69. package/templates/shared/workers/lib/usage/queue/telemetry-processor.ts +790 -0
  70. package/templates/shared/workers/lib/usage/scheduled/anomaly-detection.ts +732 -0
  71. package/templates/shared/workers/lib/usage/scheduled/data-collection.ts +956 -0
  72. package/templates/shared/workers/lib/usage/scheduled/error-digest.ts +343 -0
  73. package/templates/shared/workers/lib/usage/scheduled/index.ts +18 -0
  74. package/templates/shared/workers/lib/usage/scheduled/rollups.ts +1561 -0
  75. package/templates/shared/workers/lib/usage/shared/constants.ts +362 -0
  76. package/templates/shared/workers/lib/usage/shared/index.ts +14 -0
  77. package/templates/shared/workers/lib/usage/shared/types.ts +1066 -0
  78. package/templates/shared/workers/lib/usage/shared/utils.ts +795 -0
  79. package/templates/shared/workers/platform-usage.ts +1915 -0
  80. package/templates/shared/wrangler.usage.jsonc.hbs +58 -0
  81. package/templates/standard/migrations/005_error_collection.sql +162 -0
  82. package/templates/standard/workers/error-collector.ts +2670 -0
  83. package/templates/standard/workers/lib/error-collector/capture.ts +213 -0
  84. package/templates/standard/workers/lib/error-collector/digest.ts +448 -0
  85. package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +262 -0
  86. package/templates/standard/workers/lib/error-collector/fingerprint.ts +258 -0
  87. package/templates/standard/workers/lib/error-collector/gap-alerts.ts +293 -0
  88. package/templates/standard/workers/lib/error-collector/github.ts +329 -0
  89. package/templates/standard/workers/lib/error-collector/types.ts +262 -0
  90. package/templates/standard/workers/lib/sentinel/gap-detection.ts +734 -0
  91. package/templates/standard/workers/lib/shared/slack-alerts.ts +585 -0
  92. package/templates/standard/workers/platform-sentinel.ts +1744 -0
  93. package/templates/standard/wrangler.error-collector.jsonc.hbs +44 -0
  94. package/templates/standard/wrangler.sentinel.jsonc.hbs +45 -0
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Shared Types
3
+ *
4
+ * Type definitions for project registry and resource mapping.
5
+ * These types are used by the platform-usage worker for project identification
6
+ * and resource attribution.
7
+ */
8
+
9
+ /**
10
+ * Project record from the D1 project_registry table.
11
+ */
12
+ export interface Project {
13
+ projectId: string;
14
+ displayName: string;
15
+ description: string | null;
16
+ color: string | null;
17
+ icon: string | null;
18
+ owner: string | null;
19
+ repoPath: string | null;
20
+ status: 'active' | 'archived' | 'development';
21
+ /** Primary resource type for utilization tracking (e.g., 'd1', 'workers', 'vectorize') */
22
+ primaryResource: ResourceType | null;
23
+ /** Custom limit for the primary resource (overrides global CF_ALLOWANCES) */
24
+ customLimit: number | null;
25
+ /** Full GitHub repository URL */
26
+ repoUrl: string | null;
27
+ /** GitHub repository identifier (e.g., 'org/repo') */
28
+ githubRepoId: string | null;
29
+ }
30
+
31
+ /**
32
+ * Resource mapping record from D1.
33
+ */
34
+ export interface ResourceMapping {
35
+ resourceType: ResourceType;
36
+ resourceId: string;
37
+ resourceName: string;
38
+ projectId: string;
39
+ environment: 'production' | 'staging' | 'preview' | 'development';
40
+ notes: string | null;
41
+ }
42
+
43
+ /**
44
+ * Cloudflare resource types for project mapping.
45
+ */
46
+ export type ResourceType =
47
+ | 'worker'
48
+ | 'd1'
49
+ | 'kv'
50
+ | 'r2'
51
+ | 'vectorize'
52
+ | 'queue'
53
+ | 'workflow'
54
+ | 'ai_gateway'
55
+ | 'workers_ai'
56
+ | 'durable_object'
57
+ | 'pages'
58
+ | 'analytics_engine';
@@ -0,0 +1,360 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+
3
+ /**
4
+ * Reservoir Sampling for Latency Percentiles
5
+ *
6
+ * Implements Algorithm R for O(1) memory p99/p95 latency tracking.
7
+ * Maintains a fixed-size sample that represents the distribution of all seen values.
8
+ *
9
+ * Key properties:
10
+ * - O(1) memory: Fixed 100 samples (~800 bytes JSON)
11
+ * - O(1) per-sample: Constant time to add each sample
12
+ * - Unbiased: Each value has equal probability of being in the sample
13
+ *
14
+ * @see Vitter, J.S. (1985). "Random Sampling with a Reservoir"
15
+ */
16
+
17
+ // =============================================================================
18
+ // TYPES
19
+ // =============================================================================
20
+
21
+ /**
22
+ * Reservoir state stored in KV.
23
+ * Key: STATE:RESERVOIR:{feature_id}
24
+ */
25
+ export interface ReservoirState {
26
+ /** Fixed-size array of sampled values */
27
+ samples: number[];
28
+ /** Total number of values seen (for Algorithm R probability) */
29
+ totalSeen: number;
30
+ /** Timestamp of last update */
31
+ lastUpdate: number;
32
+ /** Pre-computed percentiles (updated on retrieval) */
33
+ percentiles?: PercentileResult;
34
+ }
35
+
36
+ /**
37
+ * Computed percentile values.
38
+ */
39
+ export interface PercentileResult {
40
+ /** 50th percentile (median) */
41
+ p50: number;
42
+ /** 75th percentile */
43
+ p75: number;
44
+ /** 90th percentile */
45
+ p90: number;
46
+ /** 95th percentile */
47
+ p95: number;
48
+ /** 99th percentile */
49
+ p99: number;
50
+ /** Maximum value in sample */
51
+ max: number;
52
+ /** Minimum value in sample */
53
+ min: number;
54
+ /** Average of samples */
55
+ avg: number;
56
+ /** Sample count used for calculation */
57
+ sampleCount: number;
58
+ /** Total values seen (for context) */
59
+ totalSeen: number;
60
+ }
61
+
62
+ /**
63
+ * Configuration for reservoir sampling.
64
+ */
65
+ export interface ReservoirConfig {
66
+ /** Maximum number of samples to keep (default: 100) */
67
+ maxSamples: number;
68
+ }
69
+
70
+ // =============================================================================
71
+ // DEFAULT CONFIGURATION
72
+ // =============================================================================
73
+
74
+ /**
75
+ * Default reservoir configuration.
76
+ * 100 samples provides good percentile estimates while keeping KV payload small.
77
+ */
78
+ export const DEFAULT_RESERVOIR_CONFIG: ReservoirConfig = {
79
+ maxSamples: 100,
80
+ };
81
+
82
+ /**
83
+ * Create a fresh reservoir state.
84
+ */
85
+ export function createReservoirState(): ReservoirState {
86
+ return {
87
+ samples: [],
88
+ totalSeen: 0,
89
+ lastUpdate: Date.now(),
90
+ };
91
+ }
92
+
93
+ // =============================================================================
94
+ // ALGORITHM R IMPLEMENTATION
95
+ // =============================================================================
96
+
97
+ /**
98
+ * Add a sample to the reservoir using Algorithm R.
99
+ *
100
+ * Algorithm R (Vitter 1985):
101
+ * - If reservoir not full: add sample directly
102
+ * - If reservoir full: with probability k/n, replace random sample
103
+ * where k = reservoir size, n = total samples seen
104
+ *
105
+ * This ensures each of the n items has equal probability k/n of being in the sample.
106
+ *
107
+ * @param state - Current reservoir state
108
+ * @param value - New value to potentially add
109
+ * @param config - Reservoir configuration
110
+ * @returns Updated reservoir state (mutates in place for efficiency)
111
+ */
112
+ export function addSample(
113
+ state: ReservoirState,
114
+ value: number,
115
+ config: ReservoirConfig = DEFAULT_RESERVOIR_CONFIG
116
+ ): ReservoirState {
117
+ state.totalSeen++;
118
+ state.lastUpdate = Date.now();
119
+
120
+ if (state.samples.length < config.maxSamples) {
121
+ // Reservoir not full - add directly
122
+ state.samples.push(value);
123
+ } else {
124
+ // Reservoir full - Algorithm R replacement
125
+ // Generate random index in range [0, totalSeen)
126
+ const j = Math.floor(Math.random() * state.totalSeen);
127
+
128
+ // If j < maxSamples, replace that element
129
+ if (j < config.maxSamples) {
130
+ state.samples[j] = value;
131
+ }
132
+ // Otherwise, the new sample is discarded
133
+ }
134
+
135
+ // Clear cached percentiles (will be recomputed on next read)
136
+ state.percentiles = undefined;
137
+
138
+ return state;
139
+ }
140
+
141
+ /**
142
+ * Add multiple samples efficiently.
143
+ * For batch processing of telemetry data.
144
+ */
145
+ export function addSamples(
146
+ state: ReservoirState,
147
+ values: number[],
148
+ config: ReservoirConfig = DEFAULT_RESERVOIR_CONFIG
149
+ ): ReservoirState {
150
+ for (const value of values) {
151
+ addSample(state, value, config);
152
+ }
153
+ return state;
154
+ }
155
+
156
+ // =============================================================================
157
+ // PERCENTILE CALCULATION
158
+ // =============================================================================
159
+
160
+ /**
161
+ * Calculate percentiles from the reservoir samples.
162
+ *
163
+ * Uses the "nearest rank" method for percentile calculation.
164
+ *
165
+ * @param state - Reservoir state with samples
166
+ * @returns Computed percentiles, or undefined if no samples
167
+ */
168
+ export function calculatePercentiles(state: ReservoirState): PercentileResult | undefined {
169
+ if (state.samples.length === 0) {
170
+ return undefined;
171
+ }
172
+
173
+ // Sort samples for percentile calculation
174
+ const sorted = [...state.samples].sort((a, b) => a - b);
175
+ const n = sorted.length;
176
+
177
+ // Helper: get percentile value using nearest rank
178
+ const getPercentile = (p: number): number => {
179
+ const rank = Math.ceil((p / 100) * n) - 1;
180
+ return sorted[Math.max(0, Math.min(n - 1, rank))];
181
+ };
182
+
183
+ // Calculate statistics
184
+ const sum = sorted.reduce((a, b) => a + b, 0);
185
+
186
+ const result: PercentileResult = {
187
+ p50: getPercentile(50),
188
+ p75: getPercentile(75),
189
+ p90: getPercentile(90),
190
+ p95: getPercentile(95),
191
+ p99: getPercentile(99),
192
+ min: sorted[0],
193
+ max: sorted[n - 1],
194
+ avg: sum / n,
195
+ sampleCount: n,
196
+ totalSeen: state.totalSeen,
197
+ };
198
+
199
+ // Cache in state
200
+ state.percentiles = result;
201
+
202
+ return result;
203
+ }
204
+
205
+ /**
206
+ * Get percentiles, computing if not cached.
207
+ */
208
+ export function getPercentiles(state: ReservoirState): PercentileResult | undefined {
209
+ if (state.percentiles) {
210
+ return state.percentiles;
211
+ }
212
+ return calculatePercentiles(state);
213
+ }
214
+
215
+ // =============================================================================
216
+ // KV PERSISTENCE HELPERS
217
+ // =============================================================================
218
+
219
+ /**
220
+ * KV key for reservoir state.
221
+ */
222
+ export function reservoirStateKey(featureId: string): string {
223
+ return `STATE:RESERVOIR:${featureId}`;
224
+ }
225
+
226
+ /**
227
+ * Get reservoir state from KV, returning fresh state if not found.
228
+ */
229
+ export async function getReservoirState(
230
+ featureId: string,
231
+ kv: KVNamespace
232
+ ): Promise<ReservoirState> {
233
+ const key = reservoirStateKey(featureId);
234
+ const data = await kv.get(key, 'json');
235
+ if (data && typeof data === 'object') {
236
+ return data as ReservoirState;
237
+ }
238
+ return createReservoirState();
239
+ }
240
+
241
+ /**
242
+ * Save reservoir state to KV with 24h TTL.
243
+ */
244
+ export async function saveReservoirState(
245
+ featureId: string,
246
+ state: ReservoirState,
247
+ kv: KVNamespace
248
+ ): Promise<void> {
249
+ const key = reservoirStateKey(featureId);
250
+
251
+ // Compute percentiles before saving for quick read access
252
+ if (!state.percentiles && state.samples.length > 0) {
253
+ calculatePercentiles(state);
254
+ }
255
+
256
+ await kv.put(key, JSON.stringify(state), { expirationTtl: 86400 });
257
+ }
258
+
259
+ // =============================================================================
260
+ // UTILITY FUNCTIONS
261
+ // =============================================================================
262
+
263
+ /**
264
+ * Merge two reservoir states.
265
+ * Useful for combining data from multiple sources.
266
+ *
267
+ * Uses weighted random selection based on total samples seen.
268
+ */
269
+ export function mergeReservoirs(
270
+ a: ReservoirState,
271
+ b: ReservoirState,
272
+ config: ReservoirConfig = DEFAULT_RESERVOIR_CONFIG
273
+ ): ReservoirState {
274
+ if (a.totalSeen === 0) return { ...b };
275
+ if (b.totalSeen === 0) return { ...a };
276
+
277
+ // Combine all samples
278
+ const combined = [...a.samples, ...b.samples];
279
+ const totalSeen = a.totalSeen + b.totalSeen;
280
+
281
+ // If combined fits in reservoir, keep all
282
+ if (combined.length <= config.maxSamples) {
283
+ return {
284
+ samples: combined,
285
+ totalSeen,
286
+ lastUpdate: Math.max(a.lastUpdate, b.lastUpdate),
287
+ };
288
+ }
289
+
290
+ // Otherwise, randomly select maxSamples
291
+ // Shuffle using Fisher-Yates
292
+ for (let i = combined.length - 1; i > 0; i--) {
293
+ const j = Math.floor(Math.random() * (i + 1));
294
+ [combined[i], combined[j]] = [combined[j], combined[i]];
295
+ }
296
+
297
+ return {
298
+ samples: combined.slice(0, config.maxSamples),
299
+ totalSeen,
300
+ lastUpdate: Math.max(a.lastUpdate, b.lastUpdate),
301
+ };
302
+ }
303
+
304
+ /**
305
+ * Reset reservoir state (clear all samples).
306
+ */
307
+ export function resetReservoir(state: ReservoirState): ReservoirState {
308
+ state.samples = [];
309
+ state.totalSeen = 0;
310
+ state.lastUpdate = Date.now();
311
+ state.percentiles = undefined;
312
+ return state;
313
+ }
314
+
315
+ /**
316
+ * Get estimated memory usage of reservoir state in bytes.
317
+ * Useful for monitoring.
318
+ */
319
+ export function estimateMemoryUsage(state: ReservoirState): number {
320
+ // Rough estimate: 8 bytes per number + JSON overhead
321
+ const samplesBytes = state.samples.length * 8;
322
+ const overheadBytes = 200; // JSON keys, metadata
323
+ return samplesBytes + overheadBytes;
324
+ }
325
+
326
+ /**
327
+ * Format percentiles for logging/display.
328
+ */
329
+ export function formatPercentiles(percentiles: PercentileResult): string {
330
+ return (
331
+ `p50=${percentiles.p50.toFixed(2)}ms, ` +
332
+ `p95=${percentiles.p95.toFixed(2)}ms, ` +
333
+ `p99=${percentiles.p99.toFixed(2)}ms, ` +
334
+ `max=${percentiles.max.toFixed(2)}ms ` +
335
+ `(n=${percentiles.sampleCount}/${percentiles.totalSeen})`
336
+ );
337
+ }
338
+
339
+ /**
340
+ * Check if percentiles indicate potential latency issues.
341
+ *
342
+ * @param percentiles - Computed percentiles
343
+ * @param thresholds - Warning thresholds in ms
344
+ * @returns Warning message if thresholds exceeded, undefined otherwise
345
+ */
346
+ export function checkLatencyThresholds(
347
+ percentiles: PercentileResult,
348
+ thresholds: { p95Warning: number; p99Warning: number } = { p95Warning: 100, p99Warning: 500 }
349
+ ): string | undefined {
350
+ const warnings: string[] = [];
351
+
352
+ if (percentiles.p95 > thresholds.p95Warning) {
353
+ warnings.push(`p95 (${percentiles.p95.toFixed(1)}ms) > ${thresholds.p95Warning}ms`);
354
+ }
355
+ if (percentiles.p99 > thresholds.p99Warning) {
356
+ warnings.push(`p99 (${percentiles.p99.toFixed(1)}ms) > ${thresholds.p99Warning}ms`);
357
+ }
358
+
359
+ return warnings.length > 0 ? warnings.join(', ') : undefined;
360
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Example External Billing Collector
3
+ *
4
+ * Template for creating a new external provider collector.
5
+ * Copy this file and modify for your provider.
6
+ *
7
+ * Usage:
8
+ * 1. Copy this file to ./your-provider.ts
9
+ * 2. Implement the collect function
10
+ * 3. Register in ./index.ts COLLECTORS array
11
+ * 4. Add required API keys to your wrangler config
12
+ */
13
+
14
+ import type { Env } from '../shared';
15
+ import type { ExternalCollector } from './index';
16
+
17
+ // =============================================================================
18
+ // TYPES
19
+ // =============================================================================
20
+
21
+ /**
22
+ * Result type for your provider's metrics.
23
+ *
24
+ * For FLOW metrics (usage that accumulates): requests, tokens, compute units
25
+ * For STOCK metrics (point-in-time): balance remaining, quota remaining
26
+ */
27
+ export interface ExampleUsageData {
28
+ /** Total requests made this billing period */
29
+ totalRequests: number;
30
+ /** Total cost in USD this billing period */
31
+ totalCostUsd: number;
32
+ /** Billing period start date (ISO) */
33
+ periodStart: string;
34
+ /** Billing period end date (ISO) */
35
+ periodEnd: string;
36
+ }
37
+
38
+ // =============================================================================
39
+ // COLLECTOR IMPLEMENTATION
40
+ // =============================================================================
41
+
42
+ /**
43
+ * Collect usage data from your external provider.
44
+ *
45
+ * @param env - Worker environment (access API keys via env.YOUR_API_KEY)
46
+ * @returns Usage data from the provider
47
+ */
48
+ export async function collectExampleUsage(env: Env): Promise<ExampleUsageData | null> {
49
+ // Check if API key is configured
50
+ const apiKey = (env as Record<string, unknown>)['EXAMPLE_API_KEY'] as string | undefined;
51
+ if (!apiKey) {
52
+ return null; // Provider not configured, skip silently
53
+ }
54
+
55
+ const response = await fetch('https://api.example.com/v1/usage', {
56
+ headers: {
57
+ Authorization: `Bearer ${apiKey}`,
58
+ 'Content-Type': 'application/json',
59
+ },
60
+ });
61
+
62
+ if (!response.ok) {
63
+ throw new Error(`Example API returned ${response.status}: ${await response.text()}`);
64
+ }
65
+
66
+ const data = (await response.json()) as {
67
+ total_requests: number;
68
+ total_cost: number;
69
+ period_start: string;
70
+ period_end: string;
71
+ };
72
+
73
+ return {
74
+ totalRequests: data.total_requests,
75
+ totalCostUsd: data.total_cost,
76
+ periodStart: data.period_start,
77
+ periodEnd: data.period_end,
78
+ };
79
+ }
80
+
81
+ // =============================================================================
82
+ // COLLECTOR REGISTRATION
83
+ // =============================================================================
84
+
85
+ /**
86
+ * Collector instance to register in ./index.ts COLLECTORS array.
87
+ *
88
+ * Example usage in index.ts:
89
+ * import { exampleCollector } from './example';
90
+ * const COLLECTORS = [exampleCollector];
91
+ */
92
+ export const exampleCollector: ExternalCollector<ExampleUsageData | null> = {
93
+ name: 'example',
94
+ collect: collectExampleUsage,
95
+ defaultValue: null,
96
+ };
@@ -0,0 +1,128 @@
1
+ /**
2
+ * External Billing Collectors
3
+ *
4
+ * Framework for collecting billing and usage data from external providers.
5
+ * Each collector handles errors gracefully - one failure doesn't stop others.
6
+ *
7
+ * TODO: Add collectors for your external providers. See example.ts for a
8
+ * collector template. Common providers include:
9
+ * - GitHub: Org billing + Enterprise consumed licenses
10
+ * - OpenAI: Organization usage (Admin API)
11
+ * - Anthropic: Organization usage (Admin API)
12
+ * - Stripe: Revenue and subscription data
13
+ *
14
+ * Metric types:
15
+ * - FLOW metrics: Usage that accumulates (requests, tokens) - safe to SUM
16
+ * - STOCK metrics: Point-in-time values (balance, quota) - do NOT SUM
17
+ */
18
+
19
+ import type { Env } from '../shared';
20
+ import { createLoggerFromEnv } from '@littlebearapps/platform-consumer-sdk';
21
+
22
+ // =============================================================================
23
+ // COLLECTOR INTERFACE
24
+ // =============================================================================
25
+
26
+ /**
27
+ * A single external metrics collector.
28
+ *
29
+ * Implement this interface for each external provider you want to track.
30
+ */
31
+ export interface ExternalCollector<T> {
32
+ /** Unique name for the collector (used in logs and error arrays) */
33
+ name: string;
34
+ /** Collect metrics from the external provider */
35
+ collect: (env: Env) => Promise<T>;
36
+ /** Default value to return on failure */
37
+ defaultValue: T;
38
+ }
39
+
40
+ /**
41
+ * Combined external metrics from all providers.
42
+ *
43
+ * TODO: Add your provider result types here.
44
+ */
45
+ export interface ExternalMetrics {
46
+ /** Results keyed by collector name */
47
+ results: Record<string, unknown>;
48
+ /** Collection timestamp */
49
+ collectedAt: string;
50
+ /** Providers that failed to collect */
51
+ errors: string[];
52
+ }
53
+
54
+ // =============================================================================
55
+ // COLLECTOR REGISTRY
56
+ // =============================================================================
57
+
58
+ /**
59
+ * Register your collectors here.
60
+ *
61
+ * TODO: Import and add your collector modules. Example:
62
+ *
63
+ * import { githubCollector } from './github';
64
+ * import { openaiCollector } from './openai';
65
+ *
66
+ * const COLLECTORS: ExternalCollector<unknown>[] = [
67
+ * githubCollector,
68
+ * openaiCollector,
69
+ * ];
70
+ */
71
+ const COLLECTORS: ExternalCollector<unknown>[] = [
72
+ // Add your collectors here
73
+ ];
74
+
75
+ // =============================================================================
76
+ // UNIFIED COLLECTION
77
+ // =============================================================================
78
+
79
+ /**
80
+ * Collect all external metrics in parallel.
81
+ *
82
+ * Each provider is collected independently - one failure doesn't affect others.
83
+ * Failed providers are logged and recorded in the errors array.
84
+ *
85
+ * @param env - Worker environment with API keys
86
+ * @returns Combined metrics from all providers
87
+ */
88
+ export async function collectExternalMetrics(env: Env): Promise<ExternalMetrics> {
89
+ const log = createLoggerFromEnv(env, 'platform-usage', 'platform:usage:external');
90
+ const errors: string[] = [];
91
+
92
+ log.info('Starting external metrics collection', {
93
+ collectorCount: COLLECTORS.length,
94
+ });
95
+ const startTime = Date.now();
96
+
97
+ // Collect all providers in parallel
98
+ const collectorResults = await Promise.all(
99
+ COLLECTORS.map(async (collector) => {
100
+ try {
101
+ const result = await collector.collect(env);
102
+ return { name: collector.name, result };
103
+ } catch (e) {
104
+ log.error(`${collector.name} collection failed`, e instanceof Error ? e : undefined);
105
+ errors.push(collector.name);
106
+ return { name: collector.name, result: collector.defaultValue };
107
+ }
108
+ })
109
+ );
110
+
111
+ const results: Record<string, unknown> = {};
112
+ for (const { name, result } of collectorResults) {
113
+ results[name] = result;
114
+ }
115
+
116
+ const duration = Date.now() - startTime;
117
+ log.info('External metrics collection complete', {
118
+ durationMs: duration,
119
+ successCount: COLLECTORS.length - errors.length,
120
+ failedProviders: errors.length > 0 ? errors.join(', ') : 'none',
121
+ });
122
+
123
+ return {
124
+ results,
125
+ collectedAt: new Date().toISOString(),
126
+ errors,
127
+ };
128
+ }