@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.
- package/README.md +112 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +89 -0
- package/dist/prompts.d.ts +27 -0
- package/dist/prompts.js +80 -0
- package/dist/scaffold.d.ts +5 -0
- package/dist/scaffold.js +65 -0
- package/dist/templates.d.ts +16 -0
- package/dist/templates.js +131 -0
- package/package.json +46 -0
- package/templates/full/migrations/006_pattern_discovery.sql +199 -0
- package/templates/full/migrations/007_notifications_search.sql +127 -0
- package/templates/full/workers/lib/pattern-discovery/ai-prompt.ts +644 -0
- package/templates/full/workers/lib/pattern-discovery/clustering.ts +278 -0
- package/templates/full/workers/lib/pattern-discovery/shadow-evaluation.ts +603 -0
- package/templates/full/workers/lib/pattern-discovery/storage.ts +806 -0
- package/templates/full/workers/lib/pattern-discovery/types.ts +159 -0
- package/templates/full/workers/lib/pattern-discovery/validation.ts +278 -0
- package/templates/full/workers/pattern-discovery.ts +661 -0
- package/templates/full/workers/platform-alert-router.ts +1809 -0
- package/templates/full/workers/platform-notifications.ts +424 -0
- package/templates/full/workers/platform-search.ts +480 -0
- package/templates/full/workers/platform-settings.ts +436 -0
- package/templates/full/wrangler.alert-router.jsonc.hbs +34 -0
- package/templates/full/wrangler.notifications.jsonc.hbs +23 -0
- package/templates/full/wrangler.pattern-discovery.jsonc.hbs +33 -0
- package/templates/full/wrangler.search.jsonc.hbs +16 -0
- package/templates/full/wrangler.settings.jsonc.hbs +23 -0
- package/templates/shared/README.md.hbs +69 -0
- package/templates/shared/config/budgets.yaml.hbs +72 -0
- package/templates/shared/config/services.yaml.hbs +45 -0
- package/templates/shared/migrations/001_core_tables.sql +117 -0
- package/templates/shared/migrations/002_usage_warehouse.sql +830 -0
- package/templates/shared/migrations/003_feature_tracking.sql +250 -0
- package/templates/shared/migrations/004_settings_alerts.sql +452 -0
- package/templates/shared/migrations/seed.sql.hbs +4 -0
- package/templates/shared/package.json.hbs +21 -0
- package/templates/shared/scripts/sync-config.ts +242 -0
- package/templates/shared/tsconfig.json +12 -0
- package/templates/shared/workers/lib/analytics-engine.ts +357 -0
- package/templates/shared/workers/lib/billing.ts +293 -0
- package/templates/shared/workers/lib/circuit-breaker-middleware.ts +25 -0
- package/templates/shared/workers/lib/control.ts +292 -0
- package/templates/shared/workers/lib/economics.ts +368 -0
- package/templates/shared/workers/lib/metrics.ts +103 -0
- package/templates/shared/workers/lib/platform-settings.ts +407 -0
- package/templates/shared/workers/lib/shared/allowances.ts +333 -0
- package/templates/shared/workers/lib/shared/cloudflare.ts +1362 -0
- package/templates/shared/workers/lib/shared/types.ts +58 -0
- package/templates/shared/workers/lib/telemetry-sampling.ts +360 -0
- package/templates/shared/workers/lib/usage/collectors/example.ts +96 -0
- package/templates/shared/workers/lib/usage/collectors/index.ts +128 -0
- package/templates/shared/workers/lib/usage/handlers/audit.ts +306 -0
- package/templates/shared/workers/lib/usage/handlers/backfill.ts +845 -0
- package/templates/shared/workers/lib/usage/handlers/behavioral.ts +429 -0
- package/templates/shared/workers/lib/usage/handlers/data-queries.ts +507 -0
- package/templates/shared/workers/lib/usage/handlers/dlq-admin.ts +364 -0
- package/templates/shared/workers/lib/usage/handlers/health-trends.ts +222 -0
- package/templates/shared/workers/lib/usage/handlers/index.ts +35 -0
- package/templates/shared/workers/lib/usage/handlers/usage-admin.ts +421 -0
- package/templates/shared/workers/lib/usage/handlers/usage-features.ts +1262 -0
- package/templates/shared/workers/lib/usage/handlers/usage-metrics.ts +2420 -0
- package/templates/shared/workers/lib/usage/handlers/usage-settings.ts +610 -0
- package/templates/shared/workers/lib/usage/queue/budget-enforcement.ts +1032 -0
- package/templates/shared/workers/lib/usage/queue/cost-budget-enforcement.ts +128 -0
- package/templates/shared/workers/lib/usage/queue/cost-calculator.ts +77 -0
- package/templates/shared/workers/lib/usage/queue/dlq-handler.ts +161 -0
- package/templates/shared/workers/lib/usage/queue/index.ts +19 -0
- package/templates/shared/workers/lib/usage/queue/telemetry-processor.ts +790 -0
- package/templates/shared/workers/lib/usage/scheduled/anomaly-detection.ts +732 -0
- package/templates/shared/workers/lib/usage/scheduled/data-collection.ts +956 -0
- package/templates/shared/workers/lib/usage/scheduled/error-digest.ts +343 -0
- package/templates/shared/workers/lib/usage/scheduled/index.ts +18 -0
- package/templates/shared/workers/lib/usage/scheduled/rollups.ts +1561 -0
- package/templates/shared/workers/lib/usage/shared/constants.ts +362 -0
- package/templates/shared/workers/lib/usage/shared/index.ts +14 -0
- package/templates/shared/workers/lib/usage/shared/types.ts +1066 -0
- package/templates/shared/workers/lib/usage/shared/utils.ts +795 -0
- package/templates/shared/workers/platform-usage.ts +1915 -0
- package/templates/shared/wrangler.usage.jsonc.hbs +58 -0
- package/templates/standard/migrations/005_error_collection.sql +162 -0
- package/templates/standard/workers/error-collector.ts +2670 -0
- package/templates/standard/workers/lib/error-collector/capture.ts +213 -0
- package/templates/standard/workers/lib/error-collector/digest.ts +448 -0
- package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +262 -0
- package/templates/standard/workers/lib/error-collector/fingerprint.ts +258 -0
- package/templates/standard/workers/lib/error-collector/gap-alerts.ts +293 -0
- package/templates/standard/workers/lib/error-collector/github.ts +329 -0
- package/templates/standard/workers/lib/error-collector/types.ts +262 -0
- package/templates/standard/workers/lib/sentinel/gap-detection.ts +734 -0
- package/templates/standard/workers/lib/shared/slack-alerts.ts +585 -0
- package/templates/standard/workers/platform-sentinel.ts +1744 -0
- package/templates/standard/wrangler.error-collector.jsonc.hbs +44 -0
- package/templates/standard/wrangler.sentinel.jsonc.hbs +45 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BCU (Budget Consumption Unit) Cost Allocator
|
|
3
|
+
*
|
|
4
|
+
* Provides scarcity-weighted quota enforcement for intelligent degradation.
|
|
5
|
+
* BCU normalises different resource types into a single unit for budget comparison.
|
|
6
|
+
*
|
|
7
|
+
* Key difference from costs.ts:
|
|
8
|
+
* - costs.ts: Financial reporting (actual USD costs)
|
|
9
|
+
* - economics.ts: Scarcity-weighted quota enforcement (relative resource pressure)
|
|
10
|
+
*
|
|
11
|
+
* BCU weights reflect scarcity and impact, not just cost:
|
|
12
|
+
* - AI neurons are expensive AND scarce (weight: 100)
|
|
13
|
+
* - D1 writes have durability implications (weight: 10)
|
|
14
|
+
* - KV writes are moderately constrained (weight: 1)
|
|
15
|
+
* - Requests are abundant but need tracking (weight: 0.001)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { FeatureMetrics } from '@littlebearapps/platform-consumer-sdk';
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// TYPES
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resource type for BCU calculation.
|
|
26
|
+
*/
|
|
27
|
+
export type ResourceType =
|
|
28
|
+
| 'aiNeurons'
|
|
29
|
+
| 'aiRequests'
|
|
30
|
+
| 'd1Writes'
|
|
31
|
+
| 'd1Reads'
|
|
32
|
+
| 'd1RowsWritten'
|
|
33
|
+
| 'd1RowsRead'
|
|
34
|
+
| 'kvWrites'
|
|
35
|
+
| 'kvReads'
|
|
36
|
+
| 'kvDeletes'
|
|
37
|
+
| 'kvLists'
|
|
38
|
+
| 'r2ClassA'
|
|
39
|
+
| 'r2ClassB'
|
|
40
|
+
| 'doRequests'
|
|
41
|
+
| 'doGbSeconds'
|
|
42
|
+
| 'queueMessages'
|
|
43
|
+
| 'vectorizeQueries'
|
|
44
|
+
| 'vectorizeInserts'
|
|
45
|
+
| 'workflowInvocations'
|
|
46
|
+
| 'requests'
|
|
47
|
+
| 'cpuMs';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* BCU weights per resource type.
|
|
51
|
+
*/
|
|
52
|
+
export type BCUWeights = Record<ResourceType, number>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Result of BCU calculation.
|
|
56
|
+
*/
|
|
57
|
+
export interface BCUResult {
|
|
58
|
+
/** Total BCU value */
|
|
59
|
+
total: number;
|
|
60
|
+
/** Breakdown by resource type */
|
|
61
|
+
breakdown: Partial<Record<ResourceType, number>>;
|
|
62
|
+
/** Dominant resource (highest BCU contribution) */
|
|
63
|
+
dominantResource: ResourceType | null;
|
|
64
|
+
/** Percentage contribution of dominant resource */
|
|
65
|
+
dominantPercentage: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Budget state with BCU tracking.
|
|
70
|
+
*/
|
|
71
|
+
export interface BCUBudgetState {
|
|
72
|
+
/** Current period BCU consumption */
|
|
73
|
+
currentBCU: number;
|
|
74
|
+
/** Budget limit in BCU */
|
|
75
|
+
limitBCU: number;
|
|
76
|
+
/** Utilisation ratio (0.0-1.0+) */
|
|
77
|
+
utilisation: number;
|
|
78
|
+
/** Whether budget is exceeded */
|
|
79
|
+
exceeded: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// =============================================================================
|
|
83
|
+
// SCARCITY WEIGHTS
|
|
84
|
+
// =============================================================================
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Default BCU weights reflecting resource scarcity and impact.
|
|
88
|
+
*
|
|
89
|
+
* Philosophy:
|
|
90
|
+
* - Expensive resources that are hard to scale get high weights
|
|
91
|
+
* - Cheap, abundant resources get low weights
|
|
92
|
+
* - Writes are weighted higher than reads (durability implications)
|
|
93
|
+
*
|
|
94
|
+
* TODO: Adjust these weights based on your specific scarcity constraints.
|
|
95
|
+
*/
|
|
96
|
+
export const DEFAULT_BCU_WEIGHTS: BCUWeights = {
|
|
97
|
+
// AI Resources - Most expensive and scarce
|
|
98
|
+
aiNeurons: 100, // $0.011 per 1K neurons, compute-intensive
|
|
99
|
+
aiRequests: 50, // Each AI call is significant
|
|
100
|
+
|
|
101
|
+
// D1 Database - Writes are expensive, reads are cheap
|
|
102
|
+
d1Writes: 10, // Deprecated field (use d1RowsWritten)
|
|
103
|
+
d1Reads: 0.01, // Deprecated field (use d1RowsRead)
|
|
104
|
+
d1RowsWritten: 10, // $1.00 per million, durability critical
|
|
105
|
+
d1RowsRead: 0.01, // $0.001 per billion, cheap
|
|
106
|
+
|
|
107
|
+
// KV - Writes constrained, reads abundant
|
|
108
|
+
kvWrites: 1, // $5.00 per million
|
|
109
|
+
kvReads: 0.1, // $0.50 per million
|
|
110
|
+
kvDeletes: 1, // Same cost as writes
|
|
111
|
+
kvLists: 1, // Same cost as writes
|
|
112
|
+
|
|
113
|
+
// R2 - Operations are relatively cheap
|
|
114
|
+
r2ClassA: 0.5, // $4.50 per million (PUT, POST, LIST)
|
|
115
|
+
r2ClassB: 0.05, // $0.36 per million (GET, HEAD)
|
|
116
|
+
|
|
117
|
+
// Durable Objects - Request-based pricing
|
|
118
|
+
doRequests: 0.5, // $0.15 per million
|
|
119
|
+
doGbSeconds: 5, // $12.50 per million GB-seconds
|
|
120
|
+
|
|
121
|
+
// Queues - Moderate pricing
|
|
122
|
+
queueMessages: 0.5, // $0.40 per million
|
|
123
|
+
|
|
124
|
+
// Vectorize - Query-intensive
|
|
125
|
+
vectorizeQueries: 1, // $0.01 per million dimensions
|
|
126
|
+
vectorizeInserts: 2, // Writes more expensive
|
|
127
|
+
|
|
128
|
+
// Workflows - Still in beta
|
|
129
|
+
workflowInvocations: 1, // Placeholder
|
|
130
|
+
|
|
131
|
+
// General compute
|
|
132
|
+
requests: 0.001, // Very cheap, 10M included
|
|
133
|
+
cpuMs: 0.01, // $0.02 per million ms
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// =============================================================================
|
|
137
|
+
// BCU CALCULATION
|
|
138
|
+
// =============================================================================
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Calculate BCU (Budget Consumption Units) from metrics.
|
|
142
|
+
*
|
|
143
|
+
* BCU provides a normalised measure of resource consumption that accounts
|
|
144
|
+
* for scarcity, not just cost. This enables fair quota allocation across
|
|
145
|
+
* features with different resource profiles.
|
|
146
|
+
*
|
|
147
|
+
* @param metrics - Feature metrics from telemetry
|
|
148
|
+
* @param weights - BCU weights (defaults provided)
|
|
149
|
+
* @returns BCU result with total, breakdown, and dominant resource
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* const metrics = { aiNeurons: 1000, d1RowsWritten: 100, requests: 50 };
|
|
154
|
+
* const result = calculateBCU(metrics);
|
|
155
|
+
* // result.total = 100000 + 1000 + 0.05 = 101000.05
|
|
156
|
+
* // result.dominantResource = 'aiNeurons'
|
|
157
|
+
* // result.dominantPercentage = 99.01
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
export function calculateBCU(
|
|
161
|
+
metrics: FeatureMetrics,
|
|
162
|
+
weights: BCUWeights = DEFAULT_BCU_WEIGHTS
|
|
163
|
+
): BCUResult {
|
|
164
|
+
const breakdown: Partial<Record<ResourceType, number>> = {};
|
|
165
|
+
let total = 0;
|
|
166
|
+
let maxContribution = 0;
|
|
167
|
+
let dominantResource: ResourceType | null = null;
|
|
168
|
+
|
|
169
|
+
// Calculate BCU for each non-zero metric
|
|
170
|
+
const metricEntries: [ResourceType, number | undefined][] = [
|
|
171
|
+
['aiNeurons', metrics.aiNeurons],
|
|
172
|
+
['aiRequests', metrics.aiRequests],
|
|
173
|
+
['d1Writes', metrics.d1Writes],
|
|
174
|
+
['d1Reads', metrics.d1Reads],
|
|
175
|
+
['d1RowsWritten', metrics.d1RowsWritten],
|
|
176
|
+
['d1RowsRead', metrics.d1RowsRead],
|
|
177
|
+
['kvWrites', metrics.kvWrites],
|
|
178
|
+
['kvReads', metrics.kvReads],
|
|
179
|
+
['kvDeletes', metrics.kvDeletes],
|
|
180
|
+
['kvLists', metrics.kvLists],
|
|
181
|
+
['r2ClassA', metrics.r2ClassA],
|
|
182
|
+
['r2ClassB', metrics.r2ClassB],
|
|
183
|
+
['doRequests', metrics.doRequests],
|
|
184
|
+
// Note: doGbSeconds not in FeatureMetrics - omitted
|
|
185
|
+
['queueMessages', metrics.queueMessages],
|
|
186
|
+
['vectorizeQueries', metrics.vectorizeQueries],
|
|
187
|
+
['vectorizeInserts', metrics.vectorizeInserts],
|
|
188
|
+
['workflowInvocations', metrics.workflowInvocations],
|
|
189
|
+
['requests', metrics.requests],
|
|
190
|
+
['cpuMs', metrics.cpuMs],
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
for (const [resource, value] of metricEntries) {
|
|
194
|
+
if (value && value > 0) {
|
|
195
|
+
const weight = weights[resource];
|
|
196
|
+
const contribution = value * weight;
|
|
197
|
+
breakdown[resource] = contribution;
|
|
198
|
+
total += contribution;
|
|
199
|
+
|
|
200
|
+
if (contribution > maxContribution) {
|
|
201
|
+
maxContribution = contribution;
|
|
202
|
+
dominantResource = resource;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const dominantPercentage = total > 0 ? (maxContribution / total) * 100 : 0;
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
total,
|
|
211
|
+
breakdown,
|
|
212
|
+
dominantResource,
|
|
213
|
+
dominantPercentage,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Calculate BCU from raw metric values (not FeatureMetrics object).
|
|
219
|
+
* Useful when processing individual metric updates.
|
|
220
|
+
*/
|
|
221
|
+
export function calculateBCUForResource(
|
|
222
|
+
resource: ResourceType,
|
|
223
|
+
value: number,
|
|
224
|
+
weights: BCUWeights = DEFAULT_BCU_WEIGHTS
|
|
225
|
+
): number {
|
|
226
|
+
return value * weights[resource];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// =============================================================================
|
|
230
|
+
// BUDGET ENFORCEMENT
|
|
231
|
+
// =============================================================================
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Check BCU budget state for a feature.
|
|
235
|
+
*
|
|
236
|
+
* @param currentBCU - Current BCU consumption
|
|
237
|
+
* @param limitBCU - Budget limit in BCU
|
|
238
|
+
* @returns Budget state with utilisation info
|
|
239
|
+
*/
|
|
240
|
+
export function checkBCUBudget(currentBCU: number, limitBCU: number): BCUBudgetState {
|
|
241
|
+
const utilisation = limitBCU > 0 ? currentBCU / limitBCU : 0;
|
|
242
|
+
return {
|
|
243
|
+
currentBCU,
|
|
244
|
+
limitBCU,
|
|
245
|
+
utilisation,
|
|
246
|
+
exceeded: currentBCU > limitBCU,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Convert a USD budget to BCU budget.
|
|
252
|
+
* Useful for setting feature budgets based on dollar allocations.
|
|
253
|
+
*
|
|
254
|
+
* This is an approximation based on the most common resource mix.
|
|
255
|
+
* For precise conversion, you'd need the expected resource profile.
|
|
256
|
+
*
|
|
257
|
+
* @param usdBudget - Budget in USD
|
|
258
|
+
* @returns Approximate BCU budget
|
|
259
|
+
*/
|
|
260
|
+
export function usdToBCU(usdBudget: number): number {
|
|
261
|
+
// Approximation: assume average workload is 60% requests, 20% D1, 10% KV, 10% AI
|
|
262
|
+
// Average BCU per dollar ~= $1 buys approximately 10000 BCU in this mix
|
|
263
|
+
return usdBudget * 10000;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Convert BCU to approximate USD.
|
|
268
|
+
* Inverse of usdToBCU for reporting.
|
|
269
|
+
*/
|
|
270
|
+
export function bcuToUSD(bcu: number): number {
|
|
271
|
+
return bcu / 10000;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// =============================================================================
|
|
275
|
+
// UTILITY FUNCTIONS
|
|
276
|
+
// =============================================================================
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get human-readable description of dominant resource.
|
|
280
|
+
*/
|
|
281
|
+
export function describeDominantResource(resource: ResourceType | null): string {
|
|
282
|
+
if (!resource) return 'none';
|
|
283
|
+
|
|
284
|
+
const descriptions: Record<ResourceType, string> = {
|
|
285
|
+
aiNeurons: 'AI compute (neurons)',
|
|
286
|
+
aiRequests: 'AI API calls',
|
|
287
|
+
d1Writes: 'D1 writes (legacy)',
|
|
288
|
+
d1Reads: 'D1 reads (legacy)',
|
|
289
|
+
d1RowsWritten: 'D1 rows written',
|
|
290
|
+
d1RowsRead: 'D1 rows read',
|
|
291
|
+
kvWrites: 'KV writes',
|
|
292
|
+
kvReads: 'KV reads',
|
|
293
|
+
kvDeletes: 'KV deletes',
|
|
294
|
+
kvLists: 'KV list operations',
|
|
295
|
+
r2ClassA: 'R2 Class A ops',
|
|
296
|
+
r2ClassB: 'R2 Class B ops',
|
|
297
|
+
doRequests: 'Durable Object requests',
|
|
298
|
+
doGbSeconds: 'Durable Object compute',
|
|
299
|
+
queueMessages: 'Queue messages',
|
|
300
|
+
vectorizeQueries: 'Vectorize queries',
|
|
301
|
+
vectorizeInserts: 'Vectorize inserts',
|
|
302
|
+
workflowInvocations: 'Workflow invocations',
|
|
303
|
+
requests: 'HTTP requests',
|
|
304
|
+
cpuMs: 'CPU time',
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
return descriptions[resource] || resource;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Format BCU result for logging.
|
|
312
|
+
*/
|
|
313
|
+
export function formatBCUResult(result: BCUResult): string {
|
|
314
|
+
const dominant = result.dominantResource
|
|
315
|
+
? `${describeDominantResource(result.dominantResource)} (${result.dominantPercentage.toFixed(1)}%)`
|
|
316
|
+
: 'none';
|
|
317
|
+
return `BCU: ${result.total.toFixed(2)}, dominant: ${dominant}`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Get resource-specific BCU breakdown for detailed reporting.
|
|
322
|
+
*/
|
|
323
|
+
export function getTopContributors(
|
|
324
|
+
result: BCUResult,
|
|
325
|
+
topN: number = 3
|
|
326
|
+
): { resource: ResourceType; bcu: number; percentage: number }[] {
|
|
327
|
+
const entries = Object.entries(result.breakdown) as [ResourceType, number][];
|
|
328
|
+
return entries
|
|
329
|
+
.sort((a, b) => b[1] - a[1])
|
|
330
|
+
.slice(0, topN)
|
|
331
|
+
.map(([resource, bcu]) => ({
|
|
332
|
+
resource,
|
|
333
|
+
bcu,
|
|
334
|
+
percentage: result.total > 0 ? (bcu / result.total) * 100 : 0,
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Combine BCU results from multiple metrics.
|
|
340
|
+
*/
|
|
341
|
+
export function combineBCUResults(results: BCUResult[]): BCUResult {
|
|
342
|
+
const combined: Partial<Record<ResourceType, number>> = {};
|
|
343
|
+
let total = 0;
|
|
344
|
+
|
|
345
|
+
for (const result of results) {
|
|
346
|
+
for (const [resource, value] of Object.entries(result.breakdown) as [ResourceType, number][]) {
|
|
347
|
+
combined[resource] = (combined[resource] || 0) + value;
|
|
348
|
+
}
|
|
349
|
+
total += result.total;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Find dominant resource in combined
|
|
353
|
+
let maxContribution = 0;
|
|
354
|
+
let dominantResource: ResourceType | null = null;
|
|
355
|
+
for (const [resource, value] of Object.entries(combined) as [ResourceType, number][]) {
|
|
356
|
+
if (value > maxContribution) {
|
|
357
|
+
maxContribution = value;
|
|
358
|
+
dominantResource = resource;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
total,
|
|
364
|
+
breakdown: combined,
|
|
365
|
+
dominantResource,
|
|
366
|
+
dominantPercentage: total > 0 ? (maxContribution / total) * 100 : 0,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { D1Database } from '@cloudflare/workers-types';
|
|
2
|
+
|
|
3
|
+
export async function insertRevenueMetric(
|
|
4
|
+
db: D1Database,
|
|
5
|
+
metricType: string,
|
|
6
|
+
value: number,
|
|
7
|
+
metadata?: Record<string, unknown>
|
|
8
|
+
): Promise<void> {
|
|
9
|
+
await db
|
|
10
|
+
.prepare(
|
|
11
|
+
`INSERT INTO revenue_metrics (id, metric_type, value, timestamp, metadata)
|
|
12
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
13
|
+
)
|
|
14
|
+
.bind(
|
|
15
|
+
crypto.randomUUID(),
|
|
16
|
+
metricType,
|
|
17
|
+
value,
|
|
18
|
+
Math.floor(Date.now() / 1000),
|
|
19
|
+
metadata ? JSON.stringify(metadata) : null
|
|
20
|
+
)
|
|
21
|
+
.run();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function insertProductMetric(
|
|
25
|
+
db: D1Database,
|
|
26
|
+
source: string,
|
|
27
|
+
metricType: string,
|
|
28
|
+
value: number,
|
|
29
|
+
metadata?: Record<string, unknown>
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
await db
|
|
32
|
+
.prepare(
|
|
33
|
+
`INSERT INTO product_metrics (id, source, metric_type, value, timestamp, metadata)
|
|
34
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
35
|
+
)
|
|
36
|
+
.bind(
|
|
37
|
+
crypto.randomUUID(),
|
|
38
|
+
source,
|
|
39
|
+
metricType,
|
|
40
|
+
value,
|
|
41
|
+
Math.floor(Date.now() / 1000),
|
|
42
|
+
metadata ? JSON.stringify(metadata) : null
|
|
43
|
+
)
|
|
44
|
+
.run();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function createAlert(
|
|
48
|
+
db: D1Database,
|
|
49
|
+
category: 'revenue' | 'product',
|
|
50
|
+
severity: 'critical' | 'high' | 'medium' | 'low',
|
|
51
|
+
title: string,
|
|
52
|
+
description: string,
|
|
53
|
+
source: string
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
await db
|
|
56
|
+
.prepare(
|
|
57
|
+
`INSERT INTO alerts (id, category, severity, title, description, source, timestamp)
|
|
58
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
59
|
+
)
|
|
60
|
+
.bind(
|
|
61
|
+
crypto.randomUUID(),
|
|
62
|
+
category,
|
|
63
|
+
severity,
|
|
64
|
+
title,
|
|
65
|
+
description,
|
|
66
|
+
source,
|
|
67
|
+
Math.floor(Date.now() / 1000)
|
|
68
|
+
)
|
|
69
|
+
.run();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function getPreviousMetric(
|
|
73
|
+
db: D1Database,
|
|
74
|
+
table: 'revenue_metrics' | 'product_metrics',
|
|
75
|
+
metricType: string,
|
|
76
|
+
sinceSeconds: number,
|
|
77
|
+
source?: string
|
|
78
|
+
): Promise<number | null> {
|
|
79
|
+
const clauses = ['metric_type = ?', 'timestamp > ?'];
|
|
80
|
+
const params: Array<string | number> = [metricType, Math.floor(Date.now() / 1000) - sinceSeconds];
|
|
81
|
+
|
|
82
|
+
if (table === 'product_metrics' && source) {
|
|
83
|
+
clauses.unshift('source = ?');
|
|
84
|
+
params.unshift(source);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const result = await db
|
|
88
|
+
.prepare(
|
|
89
|
+
`SELECT value FROM ${table}
|
|
90
|
+
WHERE ${clauses.join(' AND ')}
|
|
91
|
+
ORDER BY timestamp ASC
|
|
92
|
+
LIMIT 1`
|
|
93
|
+
)
|
|
94
|
+
.bind(...params)
|
|
95
|
+
.first<{ value: number }>();
|
|
96
|
+
|
|
97
|
+
if (!result) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const value = result.value;
|
|
102
|
+
return typeof value === 'number' ? value : Number(value ?? NaN);
|
|
103
|
+
}
|