@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,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage Admin Handlers
|
|
3
|
+
*
|
|
4
|
+
* Administrative handlers for circuit breaker management and data backfill.
|
|
5
|
+
* Extracted from platform-usage.ts as part of handler modularisation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Env, SamplingMode, DailyUsageMetrics } from '../shared';
|
|
9
|
+
import { SamplingMode as SamplingModeEnum, CB_KEYS, jsonResponse } from '../shared';
|
|
10
|
+
import { createLoggerFromEnv } from '@littlebearapps/platform-consumer-sdk';
|
|
11
|
+
import { CloudflareGraphQL, calculateDailyCosts } from '../../shared/cloudflare';
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// RESET CIRCUIT BREAKER HANDLER
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Handle POST /usage/reset-circuit-breaker
|
|
19
|
+
*
|
|
20
|
+
* Manually resets circuit breaker state for any or all projects.
|
|
21
|
+
* This allows immediate recovery after fixing issues, without waiting
|
|
22
|
+
* for the 24-hour KV expiration.
|
|
23
|
+
*
|
|
24
|
+
* Request body:
|
|
25
|
+
* - service: string - project ID to reset, or 'all' (default: 'all')
|
|
26
|
+
* - resetSampling: boolean (default: true) - also reset platform-usage sampling to FULL
|
|
27
|
+
*/
|
|
28
|
+
export async function handleResetCircuitBreaker(request: Request, env: Env): Promise<Response> {
|
|
29
|
+
const startTime = Date.now();
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const body = (await request.json().catch(() => ({}))) as {
|
|
33
|
+
service?: string;
|
|
34
|
+
resetSampling?: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const service = body.service ?? 'all';
|
|
38
|
+
const resetSampling = body.resetSampling !== false; // Default true
|
|
39
|
+
|
|
40
|
+
const resetActions: string[] = [];
|
|
41
|
+
|
|
42
|
+
// Get registered projects from D1 to reset their circuit breakers
|
|
43
|
+
// TODO: Populate project_registry with your projects, or hardcode project IDs here.
|
|
44
|
+
const projectRows = await env.PLATFORM_DB.prepare(
|
|
45
|
+
`SELECT project_id FROM project_registry WHERE project_id != 'all'`
|
|
46
|
+
).all<{ project_id: string }>();
|
|
47
|
+
|
|
48
|
+
const registeredProjects = projectRows.results?.map((r) => r.project_id) ?? ['platform'];
|
|
49
|
+
|
|
50
|
+
for (const projectId of registeredProjects) {
|
|
51
|
+
if (service === projectId || service === 'all') {
|
|
52
|
+
const cbKey = `PROJECT:${projectId.toUpperCase().replace(/-/g, '-')}:STATUS`;
|
|
53
|
+
await env.PLATFORM_CACHE.delete(cbKey);
|
|
54
|
+
resetActions.push(projectId);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Optionally reset platform-usage sampling mode to FULL
|
|
59
|
+
if (resetSampling) {
|
|
60
|
+
await env.PLATFORM_CACHE.put(CB_KEYS.USAGE_SAMPLING_MODE, SamplingModeEnum.FULL.toString());
|
|
61
|
+
resetActions.push('sampling-mode');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Log the reset event to D1 for audit trail
|
|
65
|
+
try {
|
|
66
|
+
const resetId = `cb-reset-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
67
|
+
await env.PLATFORM_DB.prepare(
|
|
68
|
+
`
|
|
69
|
+
INSERT INTO circuit_breaker_logs (id, event_type, service, reason, sampling_mode, created_at)
|
|
70
|
+
VALUES (?, 'reset', ?, ?, ?, unixepoch())
|
|
71
|
+
`
|
|
72
|
+
)
|
|
73
|
+
.bind(
|
|
74
|
+
resetId,
|
|
75
|
+
service,
|
|
76
|
+
`Manual reset via API (services: ${resetActions.join(', ')})`,
|
|
77
|
+
resetSampling ? 'FULL' : null
|
|
78
|
+
)
|
|
79
|
+
.run();
|
|
80
|
+
} catch {
|
|
81
|
+
// Don't fail the reset if logging fails
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Circuit breaker reset completed
|
|
85
|
+
|
|
86
|
+
return jsonResponse({
|
|
87
|
+
success: true,
|
|
88
|
+
message: 'Circuit breaker(s) reset successfully',
|
|
89
|
+
reset: resetActions,
|
|
90
|
+
timestamp: new Date().toISOString(),
|
|
91
|
+
responseTimeMs: Date.now() - startTime,
|
|
92
|
+
});
|
|
93
|
+
} catch (error) {
|
|
94
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
95
|
+
// Error resetting circuit breaker
|
|
96
|
+
|
|
97
|
+
return jsonResponse(
|
|
98
|
+
{
|
|
99
|
+
success: false,
|
|
100
|
+
error: 'Failed to reset circuit breaker',
|
|
101
|
+
message: errorMessage,
|
|
102
|
+
},
|
|
103
|
+
500
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// =============================================================================
|
|
109
|
+
// BACKFILL HANDLER
|
|
110
|
+
// =============================================================================
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Handle POST /usage/backfill
|
|
114
|
+
*
|
|
115
|
+
* Backfills daily_usage_rollups from Cloudflare GraphQL for historical data.
|
|
116
|
+
* Required because the worker was just deployed and D1 tables are empty.
|
|
117
|
+
*
|
|
118
|
+
* Query params:
|
|
119
|
+
* - startDate: YYYY-MM-DD (required)
|
|
120
|
+
* - endDate: YYYY-MM-DD (required)
|
|
121
|
+
*
|
|
122
|
+
* Note: Cloudflare GraphQL supports up to 90 days lookback.
|
|
123
|
+
*/
|
|
124
|
+
export async function handleBackfill(request: Request, env: Env): Promise<Response> {
|
|
125
|
+
const url = new URL(request.url);
|
|
126
|
+
const startDateStr = url.searchParams.get('startDate');
|
|
127
|
+
const endDateStr = url.searchParams.get('endDate');
|
|
128
|
+
|
|
129
|
+
// Validate required params
|
|
130
|
+
if (!startDateStr || !endDateStr) {
|
|
131
|
+
return jsonResponse(
|
|
132
|
+
{ error: 'Missing required params: startDate and endDate (YYYY-MM-DD)' },
|
|
133
|
+
400
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Validate date format
|
|
138
|
+
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
|
139
|
+
if (!dateRegex.test(startDateStr) || !dateRegex.test(endDateStr)) {
|
|
140
|
+
return jsonResponse({ error: 'Invalid date format. Use YYYY-MM-DD.' }, 400);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const startDate = new Date(startDateStr);
|
|
144
|
+
const endDate = new Date(endDateStr);
|
|
145
|
+
|
|
146
|
+
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
|
|
147
|
+
return jsonResponse({ error: 'Invalid date values' }, 400);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (startDate > endDate) {
|
|
151
|
+
return jsonResponse({ error: 'startDate must be before endDate' }, 400);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Check max range (90 days - Cloudflare API limit)
|
|
155
|
+
const daysDiff = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
|
|
156
|
+
if (daysDiff > 90) {
|
|
157
|
+
return jsonResponse(
|
|
158
|
+
{ error: `Date range too large: ${daysDiff} days. Maximum is 90 days.` },
|
|
159
|
+
400
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const log = createLoggerFromEnv(env, 'platform-usage', 'platform:usage:backfill');
|
|
164
|
+
log.info('Starting backfill', {
|
|
165
|
+
startDate: startDateStr,
|
|
166
|
+
endDate: endDateStr,
|
|
167
|
+
days: daysDiff + 1,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Create GraphQL client
|
|
171
|
+
const client = new CloudflareGraphQL({
|
|
172
|
+
CLOUDFLARE_ACCOUNT_ID: env.CLOUDFLARE_ACCOUNT_ID,
|
|
173
|
+
CLOUDFLARE_API_TOKEN: env.CLOUDFLARE_API_TOKEN,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const results: Array<{ date: string; status: string; error?: string }> = [];
|
|
177
|
+
let successCount = 0;
|
|
178
|
+
let errorCount = 0;
|
|
179
|
+
|
|
180
|
+
// Process each day
|
|
181
|
+
const currentDate = new Date(startDate);
|
|
182
|
+
while (currentDate <= endDate) {
|
|
183
|
+
const dateStr = currentDate.toISOString().split('T')[0];
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
// For single-day queries, Cloudflare GraphQL requires endDate > startDate
|
|
187
|
+
// (date_leq appears to be exclusive). Use next day as endDate.
|
|
188
|
+
const nextDay = new Date(currentDate);
|
|
189
|
+
nextDay.setDate(nextDay.getDate() + 1);
|
|
190
|
+
const nextDayStr = nextDay.toISOString().split('T')[0];
|
|
191
|
+
|
|
192
|
+
log.info('Processing', { date: dateStr, queryRange: `${dateStr} to ${nextDayStr}` });
|
|
193
|
+
|
|
194
|
+
// Fetch metrics from Cloudflare GraphQL for this single day
|
|
195
|
+
const metrics = await client.getMetricsForDateRange({
|
|
196
|
+
startDate: dateStr,
|
|
197
|
+
endDate: nextDayStr,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// DEBUG: Log raw metrics structure to diagnose empty results
|
|
201
|
+
log.info('Raw metrics', {
|
|
202
|
+
date: dateStr,
|
|
203
|
+
workersCount: metrics.workers?.length ?? 'undefined',
|
|
204
|
+
d1Count: metrics.d1?.length ?? 'undefined',
|
|
205
|
+
kvCount: metrics.kv?.length ?? 'undefined',
|
|
206
|
+
r2Count: metrics.r2?.length ?? 'undefined',
|
|
207
|
+
doExists: metrics.durableObjects ? 'yes' : 'no',
|
|
208
|
+
vectorizeCount: metrics.vectorize?.length ?? 'undefined',
|
|
209
|
+
aiGatewayCount: metrics.aiGateway?.length ?? 'undefined',
|
|
210
|
+
pagesCount: metrics.pages?.length ?? 'undefined',
|
|
211
|
+
firstWorker: metrics.workers?.[0]
|
|
212
|
+
? { name: metrics.workers[0].scriptName, requests: metrics.workers[0].requests }
|
|
213
|
+
: 'none',
|
|
214
|
+
doData: metrics.durableObjects
|
|
215
|
+
? {
|
|
216
|
+
requests: metrics.durableObjects.requests,
|
|
217
|
+
gbSeconds: metrics.durableObjects.gbSeconds,
|
|
218
|
+
}
|
|
219
|
+
: 'none',
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Aggregate Workers metrics
|
|
223
|
+
const workersRequests = metrics.workers.reduce((sum, w) => sum + w.requests, 0);
|
|
224
|
+
const workersErrors = metrics.workers.reduce((sum, w) => sum + w.errors, 0);
|
|
225
|
+
const workersCpuTimeMs = metrics.workers.reduce((sum, w) => sum + w.cpuTimeMs, 0);
|
|
226
|
+
const workersDurationP50 =
|
|
227
|
+
metrics.workers.length > 0
|
|
228
|
+
? metrics.workers.reduce((sum, w) => sum + w.duration50thMs, 0) / metrics.workers.length
|
|
229
|
+
: 0;
|
|
230
|
+
const workersDurationP99 =
|
|
231
|
+
metrics.workers.length > 0 ? Math.max(...metrics.workers.map((w) => w.duration99thMs)) : 0;
|
|
232
|
+
|
|
233
|
+
// Aggregate D1 metrics
|
|
234
|
+
const d1RowsRead = metrics.d1.reduce((sum, d) => sum + d.rowsRead, 0);
|
|
235
|
+
const d1RowsWritten = metrics.d1.reduce((sum, d) => sum + d.rowsWritten, 0);
|
|
236
|
+
|
|
237
|
+
// Aggregate KV metrics
|
|
238
|
+
const kvReads = metrics.kv.reduce((sum, k) => sum + k.reads, 0);
|
|
239
|
+
const kvWrites = metrics.kv.reduce((sum, k) => sum + k.writes, 0);
|
|
240
|
+
const kvDeletes = metrics.kv.reduce((sum, k) => sum + k.deletes, 0);
|
|
241
|
+
const kvLists = metrics.kv.reduce((sum, k) => sum + k.lists, 0);
|
|
242
|
+
|
|
243
|
+
// Aggregate R2 metrics
|
|
244
|
+
const r2ClassA = metrics.r2.reduce((sum, r) => sum + r.classAOperations, 0);
|
|
245
|
+
const r2ClassB = metrics.r2.reduce((sum, r) => sum + r.classBOperations, 0);
|
|
246
|
+
const r2StorageBytes = metrics.r2.reduce((sum, r) => sum + r.storageBytes, 0);
|
|
247
|
+
const r2EgressBytes = metrics.r2.reduce((sum, r) => sum + r.egressBytes, 0);
|
|
248
|
+
|
|
249
|
+
// Durable Objects metrics (single object, not array)
|
|
250
|
+
const doRequests = metrics.durableObjects.requests ?? 0;
|
|
251
|
+
const doGbSeconds = metrics.durableObjects.gbSeconds ?? 0;
|
|
252
|
+
|
|
253
|
+
// Vectorize metrics
|
|
254
|
+
// Note: VectorizeInfo from REST API doesn't include query counts
|
|
255
|
+
// Query counts are collected via GraphQL (vectorizeV2QueriesAdaptiveGroups) in the scheduled cron
|
|
256
|
+
const vectorizeQueries = 0; // Query data comes from hourly collection, not available in daily endpoint
|
|
257
|
+
|
|
258
|
+
// AI Gateway metrics (array - aggregate all gateways)
|
|
259
|
+
const aiGatewayRequests = metrics.aiGateway.reduce(
|
|
260
|
+
(sum, g) => sum + (g.totalRequests ?? 0),
|
|
261
|
+
0
|
|
262
|
+
);
|
|
263
|
+
const aiGatewayTokensIn = metrics.aiGateway.reduce((sum, g) => sum + (g.totalTokens ?? 0), 0);
|
|
264
|
+
const aiGatewayTokensOut = 0; // Not available in AIGatewayMetrics interface
|
|
265
|
+
const aiGatewayCachedRequests = metrics.aiGateway.reduce(
|
|
266
|
+
(sum, g) => sum + (g.cachedRequests ?? 0),
|
|
267
|
+
0
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// Pages metrics (array - aggregate all projects)
|
|
271
|
+
const pagesDeployments = metrics.pages.reduce((sum, p) => sum + (p.totalBuilds ?? 0), 0);
|
|
272
|
+
const pagesBandwidth = 0; // Bandwidth not in PagesMetrics interface
|
|
273
|
+
|
|
274
|
+
// Calculate costs using the shared cost calculation function
|
|
275
|
+
const usage: DailyUsageMetrics = {
|
|
276
|
+
workersRequests,
|
|
277
|
+
workersCpuMs: workersCpuTimeMs,
|
|
278
|
+
d1Reads: d1RowsRead,
|
|
279
|
+
d1Writes: d1RowsWritten,
|
|
280
|
+
kvReads,
|
|
281
|
+
kvWrites,
|
|
282
|
+
kvDeletes,
|
|
283
|
+
kvLists,
|
|
284
|
+
r2ClassA,
|
|
285
|
+
r2ClassB,
|
|
286
|
+
vectorizeQueries,
|
|
287
|
+
aiGatewayRequests,
|
|
288
|
+
durableObjectsRequests: doRequests,
|
|
289
|
+
durableObjectsGbSeconds: doGbSeconds,
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const costs = calculateDailyCosts(usage);
|
|
293
|
+
|
|
294
|
+
// DEBUG: Log aggregated values before D1 insert
|
|
295
|
+
log.info('Aggregated values', {
|
|
296
|
+
date: dateStr,
|
|
297
|
+
workersRequests,
|
|
298
|
+
workersErrors,
|
|
299
|
+
d1RowsRead,
|
|
300
|
+
d1RowsWritten,
|
|
301
|
+
kvReads,
|
|
302
|
+
doRequests,
|
|
303
|
+
doGbSeconds,
|
|
304
|
+
totalCost: costs.total,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Insert into daily_usage_rollups
|
|
308
|
+
await env.PLATFORM_DB.prepare(
|
|
309
|
+
`
|
|
310
|
+
INSERT OR REPLACE INTO daily_usage_rollups (
|
|
311
|
+
snapshot_date, project,
|
|
312
|
+
workers_requests, workers_errors, workers_cpu_time_ms,
|
|
313
|
+
workers_duration_p50_ms_avg, workers_duration_p99_ms_max, workers_cost_usd,
|
|
314
|
+
d1_rows_read, d1_rows_written, d1_storage_bytes_max, d1_cost_usd,
|
|
315
|
+
kv_reads, kv_writes, kv_deletes, kv_list_ops, kv_storage_bytes_max, kv_cost_usd,
|
|
316
|
+
r2_class_a_ops, r2_class_b_ops, r2_storage_bytes_max, r2_egress_bytes, r2_cost_usd,
|
|
317
|
+
do_requests, do_gb_seconds, do_websocket_connections, do_storage_reads,
|
|
318
|
+
do_storage_writes, do_storage_deletes, do_cost_usd,
|
|
319
|
+
vectorize_queries, vectorize_vectors_stored_max, vectorize_cost_usd,
|
|
320
|
+
aigateway_requests, aigateway_tokens_in, aigateway_tokens_out,
|
|
321
|
+
aigateway_cached_requests, aigateway_cost_usd,
|
|
322
|
+
pages_deployments, pages_bandwidth_bytes, pages_cost_usd,
|
|
323
|
+
queues_messages_produced, queues_messages_consumed, queues_cost_usd,
|
|
324
|
+
workersai_requests, workersai_neurons, workersai_cost_usd,
|
|
325
|
+
workflows_executions, workflows_successes, workflows_failures,
|
|
326
|
+
workflows_wall_time_ms, workflows_cpu_time_ms, workflows_cost_usd,
|
|
327
|
+
total_cost_usd, samples_count, rollup_version
|
|
328
|
+
) VALUES (
|
|
329
|
+
?, 'all',
|
|
330
|
+
?, ?, ?,
|
|
331
|
+
?, ?, ?,
|
|
332
|
+
?, ?, 0, ?,
|
|
333
|
+
?, ?, ?, ?, 0, ?,
|
|
334
|
+
?, ?, ?, ?, ?,
|
|
335
|
+
?, ?, 0, 0, 0, 0, ?,
|
|
336
|
+
?, 0, ?,
|
|
337
|
+
?, ?, ?, ?, ?,
|
|
338
|
+
?, ?, ?,
|
|
339
|
+
0, 0, ?,
|
|
340
|
+
0, 0, ?,
|
|
341
|
+
0, 0, 0, 0, 0, 0,
|
|
342
|
+
?, 24, 2
|
|
343
|
+
)
|
|
344
|
+
`
|
|
345
|
+
)
|
|
346
|
+
.bind(
|
|
347
|
+
dateStr,
|
|
348
|
+
// Workers
|
|
349
|
+
workersRequests,
|
|
350
|
+
workersErrors,
|
|
351
|
+
workersCpuTimeMs,
|
|
352
|
+
workersDurationP50,
|
|
353
|
+
workersDurationP99,
|
|
354
|
+
costs.workers,
|
|
355
|
+
// D1
|
|
356
|
+
d1RowsRead,
|
|
357
|
+
d1RowsWritten,
|
|
358
|
+
costs.d1,
|
|
359
|
+
// KV
|
|
360
|
+
kvReads,
|
|
361
|
+
kvWrites,
|
|
362
|
+
kvDeletes,
|
|
363
|
+
kvLists,
|
|
364
|
+
costs.kv,
|
|
365
|
+
// R2
|
|
366
|
+
r2ClassA,
|
|
367
|
+
r2ClassB,
|
|
368
|
+
r2StorageBytes,
|
|
369
|
+
r2EgressBytes,
|
|
370
|
+
costs.r2,
|
|
371
|
+
// DO
|
|
372
|
+
doRequests,
|
|
373
|
+
doGbSeconds,
|
|
374
|
+
costs.durableObjects,
|
|
375
|
+
// Vectorize
|
|
376
|
+
vectorizeQueries,
|
|
377
|
+
costs.vectorize,
|
|
378
|
+
// AI Gateway
|
|
379
|
+
aiGatewayRequests,
|
|
380
|
+
aiGatewayTokensIn,
|
|
381
|
+
aiGatewayTokensOut,
|
|
382
|
+
aiGatewayCachedRequests,
|
|
383
|
+
costs.aiGateway,
|
|
384
|
+
// Pages
|
|
385
|
+
pagesDeployments,
|
|
386
|
+
pagesBandwidth,
|
|
387
|
+
0, // Pages cost is always 0 on paid plan
|
|
388
|
+
// Queues
|
|
389
|
+
costs.queues,
|
|
390
|
+
// Workers AI
|
|
391
|
+
costs.workersAI,
|
|
392
|
+
// Total
|
|
393
|
+
costs.total
|
|
394
|
+
)
|
|
395
|
+
.run();
|
|
396
|
+
|
|
397
|
+
results.push({ date: dateStr, status: 'ok' });
|
|
398
|
+
successCount++;
|
|
399
|
+
log.info('Backfill success', { date: dateStr, totalCost: costs.total });
|
|
400
|
+
} catch (error) {
|
|
401
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
402
|
+
results.push({ date: dateStr, status: 'error', error: errorMsg });
|
|
403
|
+
errorCount++;
|
|
404
|
+
log.error(`Backfill error for ${dateStr}: ${errorMsg}`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Move to next day
|
|
408
|
+
currentDate.setDate(currentDate.getDate() + 1);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
log.info('Backfill complete', { success: successCount, errors: errorCount });
|
|
412
|
+
|
|
413
|
+
return jsonResponse({
|
|
414
|
+
success: errorCount === 0,
|
|
415
|
+
message: `Backfilled ${successCount} days (${errorCount} errors)`,
|
|
416
|
+
backfilled: successCount,
|
|
417
|
+
errors: errorCount,
|
|
418
|
+
dateRange: { start: startDateStr, end: endDateStr },
|
|
419
|
+
results,
|
|
420
|
+
});
|
|
421
|
+
}
|