@littlebearapps/create-platform 1.0.0 → 1.1.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 +98 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +36 -6
- package/dist/prompts.d.ts +14 -2
- package/dist/prompts.js +29 -7
- package/dist/templates.js +78 -0
- package/package.json +3 -2
- 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/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/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
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Engine SQL API Helper
|
|
3
|
+
*
|
|
4
|
+
* Provides helpers for querying Analytics Engine via the SQL API.
|
|
5
|
+
* Used by the daily rollup to aggregate SDK telemetry from PLATFORM_ANALYTICS.
|
|
6
|
+
*
|
|
7
|
+
* @module workers/lib/analytics-engine
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { withExponentialBackoff } from '@littlebearapps/platform-sdk';
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// TYPES
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Analytics Engine SQL API response structure.
|
|
18
|
+
*
|
|
19
|
+
* The SQL API returns data in one of two formats:
|
|
20
|
+
* 1. Direct format (success): { meta: [...], data: [...], rows: N }
|
|
21
|
+
* 2. Wrapped format (via REST API): { success: true, result: { meta, data, rows } }
|
|
22
|
+
* 3. Error format: { errors: [...] }
|
|
23
|
+
*/
|
|
24
|
+
interface AnalyticsEngineResponse {
|
|
25
|
+
// Direct format (SQL API)
|
|
26
|
+
meta?: Array<{ name: string; type: string }>;
|
|
27
|
+
data?: unknown[];
|
|
28
|
+
rows?: number;
|
|
29
|
+
rows_before_limit_at_least?: number;
|
|
30
|
+
|
|
31
|
+
// Wrapped format (REST API)
|
|
32
|
+
success?: boolean;
|
|
33
|
+
errors?: Array<{ code: number; message: string }>;
|
|
34
|
+
result?: {
|
|
35
|
+
data: unknown[];
|
|
36
|
+
meta: Array<{ name: string; type: string }>;
|
|
37
|
+
rows: number;
|
|
38
|
+
rows_before_limit_at_least: number;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Daily usage aggregation from Analytics Engine
|
|
44
|
+
*/
|
|
45
|
+
export interface DailyUsageAggregation {
|
|
46
|
+
project_id: string;
|
|
47
|
+
feature_id: string;
|
|
48
|
+
d1_reads: number;
|
|
49
|
+
d1_writes: number;
|
|
50
|
+
d1_rows_read: number;
|
|
51
|
+
d1_rows_written: number;
|
|
52
|
+
kv_reads: number;
|
|
53
|
+
kv_writes: number;
|
|
54
|
+
kv_deletes: number;
|
|
55
|
+
kv_lists: number;
|
|
56
|
+
ai_requests: number;
|
|
57
|
+
ai_neurons: number;
|
|
58
|
+
vectorize_queries: number;
|
|
59
|
+
vectorize_inserts: number;
|
|
60
|
+
vectorize_deletes: number;
|
|
61
|
+
interaction_count: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// =============================================================================
|
|
65
|
+
// ANALYTICS ENGINE SQL API CLIENT
|
|
66
|
+
// =============================================================================
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Query Analytics Engine via the SQL API.
|
|
70
|
+
*
|
|
71
|
+
* @param accountId Cloudflare account ID
|
|
72
|
+
* @param apiToken Cloudflare API token with Analytics Engine read access
|
|
73
|
+
* @param sql SQL query to execute
|
|
74
|
+
* @returns Query results
|
|
75
|
+
*/
|
|
76
|
+
export async function queryAnalyticsEngine<T>(
|
|
77
|
+
accountId: string,
|
|
78
|
+
apiToken: string,
|
|
79
|
+
sql: string
|
|
80
|
+
): Promise<T[]> {
|
|
81
|
+
const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/analytics_engine/sql`;
|
|
82
|
+
|
|
83
|
+
const response = await fetch(url, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: {
|
|
86
|
+
Authorization: `Bearer ${apiToken}`,
|
|
87
|
+
'Content-Type': 'text/plain',
|
|
88
|
+
},
|
|
89
|
+
body: sql,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
const text = await response.text();
|
|
94
|
+
throw new Error(`Analytics Engine API error: ${response.status} - ${text}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const rawText = await response.text();
|
|
98
|
+
let data: AnalyticsEngineResponse;
|
|
99
|
+
try {
|
|
100
|
+
data = JSON.parse(rawText) as AnalyticsEngineResponse;
|
|
101
|
+
} catch {
|
|
102
|
+
throw new Error(`Analytics Engine returned invalid JSON: ${rawText.slice(0, 500)}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check for error response
|
|
106
|
+
if (data.errors && data.errors.length > 0) {
|
|
107
|
+
const errorMessages = data.errors.map((e) => e.message).join(', ');
|
|
108
|
+
throw new Error(`Analytics Engine query failed: ${errorMessages}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Handle both response formats:
|
|
112
|
+
// 1. Direct format: { meta, data, rows }
|
|
113
|
+
// 2. Wrapped format: { success, result: { meta, data, rows } }
|
|
114
|
+
const meta = data.meta ?? data.result?.meta;
|
|
115
|
+
const resultData = data.data ?? data.result?.data;
|
|
116
|
+
|
|
117
|
+
// Validate response structure
|
|
118
|
+
if (!meta || !resultData) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Analytics Engine response missing expected fields. ` +
|
|
121
|
+
`Got keys: ${JSON.stringify(Object.keys(data))}`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Map the result data to typed objects using column metadata
|
|
126
|
+
// Analytics Engine can return data in two formats:
|
|
127
|
+
// 1. Array of arrays: [[val1, val2], [val1, val2]] - needs column mapping
|
|
128
|
+
// 2. Array of objects: [{col1: val1, col2: val2}, ...] - already in object format
|
|
129
|
+
const columns = meta.map((m) => m.name);
|
|
130
|
+
|
|
131
|
+
return resultData.map((row) => {
|
|
132
|
+
// If row is already an object (not an array), return it directly
|
|
133
|
+
if (row !== null && typeof row === 'object' && !Array.isArray(row)) {
|
|
134
|
+
return row as T;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Row is an array - map using column metadata
|
|
138
|
+
const rowArray = row as unknown[];
|
|
139
|
+
const obj: Record<string, unknown> = {};
|
|
140
|
+
columns.forEach((col, i) => {
|
|
141
|
+
obj[col] = rowArray[i];
|
|
142
|
+
});
|
|
143
|
+
return obj as T;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get daily usage aggregation from Analytics Engine.
|
|
149
|
+
* Queries the PLATFORM_ANALYTICS dataset for yesterday's telemetry data.
|
|
150
|
+
*
|
|
151
|
+
* @param accountId Cloudflare account ID
|
|
152
|
+
* @param apiToken Cloudflare API token
|
|
153
|
+
* @param datasetName Analytics Engine dataset name (default: platform-analytics)
|
|
154
|
+
* @returns Aggregated usage by project and feature
|
|
155
|
+
*/
|
|
156
|
+
export async function getDailyUsageFromAnalyticsEngine(
|
|
157
|
+
accountId: string,
|
|
158
|
+
apiToken: string,
|
|
159
|
+
datasetName = 'platform-analytics'
|
|
160
|
+
): Promise<DailyUsageAggregation[]> {
|
|
161
|
+
// Query for yesterday's data (00:00:00 to 23:59:59 UTC)
|
|
162
|
+
// Analytics Engine uses blob1-20 and double1-20 naming convention
|
|
163
|
+
//
|
|
164
|
+
// Data schema from queue handler (platform-usage.ts):
|
|
165
|
+
// blobs: [project, category, feature] (feature_key is in indexes)
|
|
166
|
+
// doubles: [d1Writes, d1Reads, kvReads, kvWrites, doRequests, doGbSeconds,
|
|
167
|
+
// r2ClassA, r2ClassB, aiNeurons, queueMessages, requests, cpuMs,
|
|
168
|
+
// d1RowsRead, d1RowsWritten, kvDeletes, kvLists, aiRequests,
|
|
169
|
+
// vectorizeQueries, vectorizeInserts, workflowInvocations]
|
|
170
|
+
// indexes: [feature_key]
|
|
171
|
+
//
|
|
172
|
+
// NOTE: Table name must be quoted because it contains a hyphen
|
|
173
|
+
const sql = `
|
|
174
|
+
SELECT
|
|
175
|
+
blob1 as project_id,
|
|
176
|
+
index1 as feature_id,
|
|
177
|
+
SUM(double2) as d1_reads,
|
|
178
|
+
SUM(double1) as d1_writes,
|
|
179
|
+
SUM(double13) as d1_rows_read,
|
|
180
|
+
SUM(double14) as d1_rows_written,
|
|
181
|
+
SUM(double3) as kv_reads,
|
|
182
|
+
SUM(double4) as kv_writes,
|
|
183
|
+
SUM(double15) as kv_deletes,
|
|
184
|
+
SUM(double16) as kv_lists,
|
|
185
|
+
SUM(double17) as ai_requests,
|
|
186
|
+
SUM(double9) as ai_neurons,
|
|
187
|
+
SUM(double18) as vectorize_queries,
|
|
188
|
+
SUM(double19) as vectorize_inserts,
|
|
189
|
+
0 as vectorize_deletes,
|
|
190
|
+
count() as interaction_count
|
|
191
|
+
FROM "${datasetName}"
|
|
192
|
+
WHERE timestamp >= NOW() - INTERVAL '1' DAY
|
|
193
|
+
GROUP BY project_id, feature_id
|
|
194
|
+
ORDER BY project_id, feature_id
|
|
195
|
+
`;
|
|
196
|
+
|
|
197
|
+
return queryAnalyticsEngine<DailyUsageAggregation>(accountId, apiToken, sql);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get aggregated project-level usage from Analytics Engine.
|
|
202
|
+
* Groups all features by project for higher-level reporting.
|
|
203
|
+
*
|
|
204
|
+
* @param accountId Cloudflare account ID
|
|
205
|
+
* @param apiToken Cloudflare API token
|
|
206
|
+
* @param datasetName Analytics Engine dataset name
|
|
207
|
+
* @returns Aggregated usage by project
|
|
208
|
+
*/
|
|
209
|
+
export async function getProjectUsageFromAnalyticsEngine(
|
|
210
|
+
accountId: string,
|
|
211
|
+
apiToken: string,
|
|
212
|
+
datasetName = 'platform-analytics'
|
|
213
|
+
): Promise<Omit<DailyUsageAggregation, 'feature_id'>[]> {
|
|
214
|
+
// NOTE: Table name must be quoted because it contains a hyphen
|
|
215
|
+
// Schema matches METRIC_FIELDS order from platform-sdk/constants.ts
|
|
216
|
+
const sql = `
|
|
217
|
+
SELECT
|
|
218
|
+
blob1 as project_id,
|
|
219
|
+
SUM(double2) as d1_reads,
|
|
220
|
+
SUM(double1) as d1_writes,
|
|
221
|
+
SUM(double13) as d1_rows_read,
|
|
222
|
+
SUM(double14) as d1_rows_written,
|
|
223
|
+
SUM(double3) as kv_reads,
|
|
224
|
+
SUM(double4) as kv_writes,
|
|
225
|
+
SUM(double15) as kv_deletes,
|
|
226
|
+
SUM(double16) as kv_lists,
|
|
227
|
+
SUM(double17) as ai_requests,
|
|
228
|
+
SUM(double9) as ai_neurons,
|
|
229
|
+
SUM(double18) as vectorize_queries,
|
|
230
|
+
SUM(double19) as vectorize_inserts,
|
|
231
|
+
0 as vectorize_deletes,
|
|
232
|
+
count() as interaction_count
|
|
233
|
+
FROM "${datasetName}"
|
|
234
|
+
WHERE timestamp >= NOW() - INTERVAL '1' DAY
|
|
235
|
+
GROUP BY project_id
|
|
236
|
+
ORDER BY project_id
|
|
237
|
+
`;
|
|
238
|
+
|
|
239
|
+
return queryAnalyticsEngine<Omit<DailyUsageAggregation, 'feature_id'>>(accountId, apiToken, sql);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// =============================================================================
|
|
243
|
+
// TIME-BUCKETED QUERIES
|
|
244
|
+
// =============================================================================
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Time-bucketed usage data from Analytics Engine.
|
|
248
|
+
* Aggregates metrics by time bucket (hour/day) and project.
|
|
249
|
+
*/
|
|
250
|
+
export interface TimeBucketedUsage {
|
|
251
|
+
time_bucket: string;
|
|
252
|
+
project_id: string;
|
|
253
|
+
d1_writes: number;
|
|
254
|
+
d1_reads: number;
|
|
255
|
+
d1_rows_read: number;
|
|
256
|
+
d1_rows_written: number;
|
|
257
|
+
kv_reads: number;
|
|
258
|
+
kv_writes: number;
|
|
259
|
+
kv_deletes: number;
|
|
260
|
+
kv_lists: number;
|
|
261
|
+
do_requests: number;
|
|
262
|
+
do_gb_seconds: number;
|
|
263
|
+
r2_class_a: number;
|
|
264
|
+
r2_class_b: number;
|
|
265
|
+
ai_neurons: number;
|
|
266
|
+
ai_requests: number;
|
|
267
|
+
queue_messages: number;
|
|
268
|
+
requests: number;
|
|
269
|
+
cpu_ms: number;
|
|
270
|
+
vectorize_queries: number;
|
|
271
|
+
vectorize_inserts: number;
|
|
272
|
+
workflow_invocations: number;
|
|
273
|
+
interaction_count: number;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Query parameters for time-bucketed usage.
|
|
278
|
+
*/
|
|
279
|
+
export interface TimeBucketQueryParams {
|
|
280
|
+
period: '24h' | '7d' | '30d';
|
|
281
|
+
groupBy: 'hour' | 'day';
|
|
282
|
+
project?: string;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Query usage by time bucket from Analytics Engine.
|
|
287
|
+
* Returns aggregated metrics grouped by time interval (hour/day) and project.
|
|
288
|
+
*
|
|
289
|
+
* @param accountId Cloudflare account ID
|
|
290
|
+
* @param apiToken Cloudflare API token
|
|
291
|
+
* @param params Query parameters (period, groupBy, optional project filter)
|
|
292
|
+
* @param datasetName Analytics Engine dataset name
|
|
293
|
+
* @returns Time-bucketed usage data
|
|
294
|
+
*/
|
|
295
|
+
export async function queryUsageByTimeBucket(
|
|
296
|
+
accountId: string,
|
|
297
|
+
apiToken: string,
|
|
298
|
+
params: TimeBucketQueryParams,
|
|
299
|
+
datasetName = 'platform-analytics'
|
|
300
|
+
): Promise<TimeBucketedUsage[]> {
|
|
301
|
+
// Determine interval based on groupBy
|
|
302
|
+
const interval = params.groupBy === 'hour' ? 'HOUR' : 'DAY';
|
|
303
|
+
|
|
304
|
+
// Map period to interval parts (number and unit must be separate for Analytics Engine)
|
|
305
|
+
const periodMap: Record<string, { num: string; unit: string }> = {
|
|
306
|
+
'24h': { num: '1', unit: 'DAY' },
|
|
307
|
+
'7d': { num: '7', unit: 'DAY' },
|
|
308
|
+
'30d': { num: '30', unit: 'DAY' },
|
|
309
|
+
};
|
|
310
|
+
const periodParts = periodMap[params.period] ?? { num: '1', unit: 'DAY' };
|
|
311
|
+
|
|
312
|
+
// Build project filter clause
|
|
313
|
+
const projectFilter = params.project ? `AND blob1 = '${params.project}'` : '';
|
|
314
|
+
|
|
315
|
+
// NOTE: Table name must be quoted because it contains a hyphen
|
|
316
|
+
// Analytics Engine columns map (from platform-sdk/constants.ts METRIC_FIELDS):
|
|
317
|
+
// double1=d1Writes, double2=d1Reads, double3=kvReads, double4=kvWrites,
|
|
318
|
+
// double5=doRequests, double6=doGbSeconds, double7=r2ClassA, double8=r2ClassB,
|
|
319
|
+
// double9=aiNeurons, double10=queueMessages, double11=requests, double12=cpuMs,
|
|
320
|
+
// double13=d1RowsRead, double14=d1RowsWritten, double15=kvDeletes, double16=kvLists,
|
|
321
|
+
// double17=aiRequests, double18=vectorizeQueries, double19=vectorizeInserts,
|
|
322
|
+
// double20=workflowInvocations
|
|
323
|
+
// blobs: blob1=project, blob2=category, blob3=feature
|
|
324
|
+
const sql = `
|
|
325
|
+
SELECT
|
|
326
|
+
toStartOfInterval(timestamp, INTERVAL '1' ${interval}) as time_bucket,
|
|
327
|
+
blob1 as project_id,
|
|
328
|
+
SUM(double1) as d1_writes,
|
|
329
|
+
SUM(double2) as d1_reads,
|
|
330
|
+
SUM(double13) as d1_rows_read,
|
|
331
|
+
SUM(double14) as d1_rows_written,
|
|
332
|
+
SUM(double3) as kv_reads,
|
|
333
|
+
SUM(double4) as kv_writes,
|
|
334
|
+
SUM(double15) as kv_deletes,
|
|
335
|
+
SUM(double16) as kv_lists,
|
|
336
|
+
SUM(double5) as do_requests,
|
|
337
|
+
SUM(double6) as do_gb_seconds,
|
|
338
|
+
SUM(double7) as r2_class_a,
|
|
339
|
+
SUM(double8) as r2_class_b,
|
|
340
|
+
SUM(double9) as ai_neurons,
|
|
341
|
+
SUM(double17) as ai_requests,
|
|
342
|
+
SUM(double10) as queue_messages,
|
|
343
|
+
SUM(double11) as requests,
|
|
344
|
+
SUM(double12) as cpu_ms,
|
|
345
|
+
SUM(double18) as vectorize_queries,
|
|
346
|
+
SUM(double19) as vectorize_inserts,
|
|
347
|
+
SUM(double20) as workflow_invocations,
|
|
348
|
+
count() as interaction_count
|
|
349
|
+
FROM "${datasetName}"
|
|
350
|
+
WHERE timestamp >= NOW() - INTERVAL '${periodParts.num}' ${periodParts.unit}
|
|
351
|
+
${projectFilter}
|
|
352
|
+
GROUP BY time_bucket, project_id
|
|
353
|
+
ORDER BY time_bucket ASC, project_id ASC
|
|
354
|
+
`;
|
|
355
|
+
|
|
356
|
+
return queryAnalyticsEngine<TimeBucketedUsage>(accountId, apiToken, sql);
|
|
357
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing Period Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides billing-cycle-aware calculations for accurate allowance proration.
|
|
5
|
+
* Supports both calendar-month and mid-month billing cycles.
|
|
6
|
+
*
|
|
7
|
+
* @see https://developers.cloudflare.com/workers/platform/pricing/
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Billing period information
|
|
12
|
+
*/
|
|
13
|
+
export interface BillingPeriod {
|
|
14
|
+
/** Start date of the current billing period */
|
|
15
|
+
startDate: Date;
|
|
16
|
+
/** End date of the current billing period */
|
|
17
|
+
endDate: Date;
|
|
18
|
+
/** Total days in this billing period */
|
|
19
|
+
daysInPeriod: number;
|
|
20
|
+
/** Days elapsed since billing period started */
|
|
21
|
+
daysElapsed: number;
|
|
22
|
+
/** Days remaining until billing period ends */
|
|
23
|
+
daysRemaining: number;
|
|
24
|
+
/** Progress through billing period (0-1) */
|
|
25
|
+
progress: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Plan types supported by Cloudflare
|
|
30
|
+
*/
|
|
31
|
+
export type PlanType = 'free' | 'paid' | 'enterprise';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Billing settings from D1
|
|
35
|
+
*/
|
|
36
|
+
export interface BillingSettings {
|
|
37
|
+
accountId: string;
|
|
38
|
+
planType: PlanType;
|
|
39
|
+
billingCycleDay: number; // 1-28 or 0 for calendar month
|
|
40
|
+
billingCurrency: string;
|
|
41
|
+
baseCostMonthly: number;
|
|
42
|
+
notes?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Calculate the billing period boundaries for a given reference date.
|
|
47
|
+
*
|
|
48
|
+
* @param billingCycleDay - Day of month billing starts (1-28) or 0 for calendar month
|
|
49
|
+
* @param refDate - Reference date (defaults to now)
|
|
50
|
+
* @returns Billing period information
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* // Calendar month billing (billing_cycle_day = 0 or 1)
|
|
54
|
+
* calculateBillingPeriod(1, new Date('2026-01-15'))
|
|
55
|
+
* // Returns: startDate: Jan 1, endDate: Jan 31, daysInPeriod: 31
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* // Mid-month billing (billing_cycle_day = 15)
|
|
59
|
+
* calculateBillingPeriod(15, new Date('2026-01-20'))
|
|
60
|
+
* // Returns: startDate: Jan 15, endDate: Feb 14, daysInPeriod: 31
|
|
61
|
+
*/
|
|
62
|
+
export function calculateBillingPeriod(
|
|
63
|
+
billingCycleDay: number,
|
|
64
|
+
refDate = new Date()
|
|
65
|
+
): BillingPeriod {
|
|
66
|
+
// Normalise to calendar month if 0 or 1
|
|
67
|
+
const cycleDay = billingCycleDay <= 1 ? 1 : Math.min(billingCycleDay, 28);
|
|
68
|
+
|
|
69
|
+
const year = refDate.getFullYear();
|
|
70
|
+
const month = refDate.getMonth();
|
|
71
|
+
const day = refDate.getDate();
|
|
72
|
+
|
|
73
|
+
let startDate: Date;
|
|
74
|
+
let endDate: Date;
|
|
75
|
+
|
|
76
|
+
if (cycleDay === 1) {
|
|
77
|
+
// Calendar month billing
|
|
78
|
+
startDate = new Date(year, month, 1);
|
|
79
|
+
endDate = new Date(year, month + 1, 0); // Last day of current month
|
|
80
|
+
} else {
|
|
81
|
+
// Mid-month billing
|
|
82
|
+
if (day >= cycleDay) {
|
|
83
|
+
// We're in the period that started this month
|
|
84
|
+
startDate = new Date(year, month, cycleDay);
|
|
85
|
+
endDate = new Date(year, month + 1, cycleDay - 1);
|
|
86
|
+
} else {
|
|
87
|
+
// We're in the period that started last month
|
|
88
|
+
startDate = new Date(year, month - 1, cycleDay);
|
|
89
|
+
endDate = new Date(year, month, cycleDay - 1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Calculate days
|
|
94
|
+
const daysInPeriod =
|
|
95
|
+
Math.round((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)) + 1;
|
|
96
|
+
const daysElapsed =
|
|
97
|
+
Math.round((refDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)) + 1;
|
|
98
|
+
const daysRemaining = Math.max(0, daysInPeriod - daysElapsed);
|
|
99
|
+
const progress = Math.min(1, daysElapsed / daysInPeriod);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
startDate,
|
|
103
|
+
endDate,
|
|
104
|
+
daysInPeriod,
|
|
105
|
+
daysElapsed,
|
|
106
|
+
daysRemaining,
|
|
107
|
+
progress,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Prorate a monthly allowance based on query period vs billing period.
|
|
113
|
+
*
|
|
114
|
+
* @param monthlyAllowance - Full monthly allowance (e.g., 10M Workers requests)
|
|
115
|
+
* @param periodDays - Number of days in the query period (e.g., 1 for 24h, 7 for 7d)
|
|
116
|
+
* @param billingDays - Total days in the billing period (default 30)
|
|
117
|
+
* @returns Prorated allowance for the query period
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* // 24h query against 10M monthly allowance
|
|
121
|
+
* prorateAllowance(10_000_000, 1, 30)
|
|
122
|
+
* // Returns: 333,333 (1/30th of monthly)
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* // 7d query against 50M monthly allowance
|
|
126
|
+
* prorateAllowance(50_000_000, 7, 31)
|
|
127
|
+
* // Returns: 11,290,323 (7/31ths of monthly)
|
|
128
|
+
*/
|
|
129
|
+
export function prorateAllowance(
|
|
130
|
+
monthlyAllowance: number,
|
|
131
|
+
periodDays: number,
|
|
132
|
+
billingDays = 30
|
|
133
|
+
): number {
|
|
134
|
+
if (billingDays <= 0) return monthlyAllowance;
|
|
135
|
+
if (periodDays >= billingDays) return monthlyAllowance;
|
|
136
|
+
return Math.round(monthlyAllowance * (periodDays / billingDays));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Calculate billable usage after subtracting prorated allowance.
|
|
141
|
+
*
|
|
142
|
+
* @param usage - Raw usage for the period
|
|
143
|
+
* @param monthlyAllowance - Full monthly allowance
|
|
144
|
+
* @param periodDays - Number of days in the query period
|
|
145
|
+
* @param billingDays - Total days in the billing period (default 30)
|
|
146
|
+
* @returns Object with raw, prorated allowance, billable usage, and percentage
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* // 500K requests in 24h against 10M monthly allowance
|
|
150
|
+
* calculateBillableUsage(500_000, 10_000_000, 1, 30)
|
|
151
|
+
* // Returns: { raw: 500000, proratedAllowance: 333333, billable: 166667, pctOfAllowance: 150 }
|
|
152
|
+
*/
|
|
153
|
+
export function calculateBillableUsage(
|
|
154
|
+
usage: number,
|
|
155
|
+
monthlyAllowance: number,
|
|
156
|
+
periodDays: number,
|
|
157
|
+
billingDays = 30
|
|
158
|
+
): {
|
|
159
|
+
raw: number;
|
|
160
|
+
proratedAllowance: number;
|
|
161
|
+
billable: number;
|
|
162
|
+
pctOfAllowance: number;
|
|
163
|
+
} {
|
|
164
|
+
const proratedAllowance = prorateAllowance(monthlyAllowance, periodDays, billingDays);
|
|
165
|
+
const billable = Math.max(0, usage - proratedAllowance);
|
|
166
|
+
const pctOfAllowance = proratedAllowance > 0 ? (usage / proratedAllowance) * 100 : 0;
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
raw: usage,
|
|
170
|
+
proratedAllowance,
|
|
171
|
+
billable,
|
|
172
|
+
pctOfAllowance,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get the default billing settings.
|
|
178
|
+
* Used as fallback when D1 data is unavailable.
|
|
179
|
+
*/
|
|
180
|
+
export function getDefaultBillingSettings(): BillingSettings {
|
|
181
|
+
return {
|
|
182
|
+
accountId: 'default',
|
|
183
|
+
planType: 'paid',
|
|
184
|
+
billingCycleDay: 1, // Calendar month
|
|
185
|
+
billingCurrency: 'USD',
|
|
186
|
+
baseCostMonthly: 5.0, // Workers Paid Plan
|
|
187
|
+
notes: 'Default billing settings',
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Calculate fair share allowance allocation for a project.
|
|
193
|
+
*
|
|
194
|
+
* Uses proportional fair share: each project gets a share of the total
|
|
195
|
+
* allowance proportional to their share of total usage.
|
|
196
|
+
*
|
|
197
|
+
* @param projectUsage - Usage for this project
|
|
198
|
+
* @param totalAccountUsage - Total usage across all projects
|
|
199
|
+
* @param monthlyAllowance - Total monthly allowance for the account
|
|
200
|
+
* @returns Object with allowance share and billable usage
|
|
201
|
+
*/
|
|
202
|
+
export function calculateProjectAllowanceShare(
|
|
203
|
+
projectUsage: number,
|
|
204
|
+
totalAccountUsage: number,
|
|
205
|
+
monthlyAllowance: number
|
|
206
|
+
): {
|
|
207
|
+
share: number;
|
|
208
|
+
billable: number;
|
|
209
|
+
proportion: number;
|
|
210
|
+
} {
|
|
211
|
+
if (totalAccountUsage <= 0) {
|
|
212
|
+
return { share: 0, billable: 0, proportion: 0 };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const proportion = projectUsage / totalAccountUsage;
|
|
216
|
+
const share = monthlyAllowance * proportion;
|
|
217
|
+
const billable = Math.max(0, projectUsage - share);
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
share: Math.round(share),
|
|
221
|
+
billable: Math.round(billable),
|
|
222
|
+
proportion,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Billing window with ISO date strings for SQL queries.
|
|
228
|
+
*/
|
|
229
|
+
export interface BillingWindow {
|
|
230
|
+
/** Start date as YYYY-MM-DD string */
|
|
231
|
+
startDate: string;
|
|
232
|
+
/** End date as YYYY-MM-DD string */
|
|
233
|
+
endDate: string;
|
|
234
|
+
/** Days elapsed in current period */
|
|
235
|
+
daysElapsed: number;
|
|
236
|
+
/** Total days in billing period */
|
|
237
|
+
daysInPeriod: number;
|
|
238
|
+
/** Progress through period (0-1) */
|
|
239
|
+
progress: number;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get billing window dates for SQL queries.
|
|
244
|
+
*
|
|
245
|
+
* Convenience wrapper around calculateBillingPeriod that returns
|
|
246
|
+
* ISO date strings ready for D1 queries.
|
|
247
|
+
*
|
|
248
|
+
* @param anchorDay - Day of month billing resets (1-28) or 0/1 for calendar month
|
|
249
|
+
* @param refDate - Reference date (defaults to now)
|
|
250
|
+
* @returns Billing window with date strings
|
|
251
|
+
*/
|
|
252
|
+
export function getBillingWindow(anchorDay: number, refDate = new Date()): BillingWindow {
|
|
253
|
+
const period = calculateBillingPeriod(anchorDay, refDate);
|
|
254
|
+
|
|
255
|
+
// Format as YYYY-MM-DD in local time (not UTC) to match D1 date storage
|
|
256
|
+
const formatLocalDate = (date: Date): string => {
|
|
257
|
+
const y = date.getFullYear();
|
|
258
|
+
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
259
|
+
const d = String(date.getDate()).padStart(2, '0');
|
|
260
|
+
return `${y}-${m}-${d}`;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
startDate: formatLocalDate(period.startDate),
|
|
265
|
+
endDate: formatLocalDate(period.endDate),
|
|
266
|
+
daysElapsed: period.daysElapsed,
|
|
267
|
+
daysInPeriod: period.daysInPeriod,
|
|
268
|
+
progress: period.progress,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Format billing period for display.
|
|
274
|
+
*
|
|
275
|
+
* @param period - Billing period from calculateBillingPeriod
|
|
276
|
+
* @returns Formatted string like "Jan 1 - Jan 31"
|
|
277
|
+
*/
|
|
278
|
+
export function formatBillingPeriod(period: BillingPeriod): string {
|
|
279
|
+
const formatter = new Intl.DateTimeFormat('en-AU', { month: 'short', day: 'numeric' });
|
|
280
|
+
return `${formatter.format(period.startDate)} - ${formatter.format(period.endDate)}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Get billing countdown text.
|
|
285
|
+
*
|
|
286
|
+
* @param daysRemaining - Days remaining in billing period
|
|
287
|
+
* @returns Human-readable countdown string
|
|
288
|
+
*/
|
|
289
|
+
export function getBillingCountdownText(daysRemaining: number): string {
|
|
290
|
+
if (daysRemaining <= 0) return 'Billing reset today';
|
|
291
|
+
if (daysRemaining === 1) return '1 day until billing reset';
|
|
292
|
+
return `${daysRemaining} days until billing reset`;
|
|
293
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Circuit Breaker Middleware -- Thin Re-export from Platform SDK
|
|
5
|
+
*
|
|
6
|
+
* All logic lives in @littlebearapps/platform-sdk/middleware.
|
|
7
|
+
* This file re-exports with the original names for backward compatibility.
|
|
8
|
+
*
|
|
9
|
+
* @see packages/platform-sdk/src/middleware.ts
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Re-export with original names (SDK uses prefixed names)
|
|
13
|
+
export {
|
|
14
|
+
PROJECT_CB_STATUS as CB_STATUS,
|
|
15
|
+
CB_PROJECT_KEYS,
|
|
16
|
+
CB_ERROR_CODES,
|
|
17
|
+
BUDGET_STATUS_HEADER,
|
|
18
|
+
checkProjectCircuitBreaker as checkCircuitBreaker,
|
|
19
|
+
checkProjectCircuitBreakerDetailed as checkCircuitBreakerDetailed,
|
|
20
|
+
createCircuitBreakerMiddleware,
|
|
21
|
+
getCircuitBreakerStates,
|
|
22
|
+
type CircuitBreakerStatusValue,
|
|
23
|
+
type CircuitBreakerCheckResult,
|
|
24
|
+
type CircuitBreakerErrorResponse,
|
|
25
|
+
} from '@littlebearapps/platform-sdk/middleware';
|