@littlebearapps/platform-admin-sdk 1.2.0 → 1.4.1
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 +140 -45
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +4 -1
- package/package.json +13 -3
- package/templates/full/workers/lib/pattern-discovery/index.ts +13 -0
- package/templates/full/workers/lib/pattern-discovery/storage.ts +1 -0
- package/templates/shared/docs/kv-key-patterns.md +101 -0
- package/templates/shared/migrations/002_usage_warehouse.sql +1 -0
- package/templates/shared/migrations/seed.sql.hbs +2 -2
- package/templates/shared/scripts/sync-config.ts +59 -5
- package/templates/shared/workers/lib/platform-settings.ts +16 -1
- package/templates/shared/workers/lib/usage/queue/budget-enforcement.ts +11 -5
- package/templates/shared/workers/lib/usage/scheduled/anomaly-detection.ts +23 -8
- package/templates/shared/workers/lib/usage/scheduled/rollups.ts +8 -5
- package/templates/standard/workers/error-collector.ts +188 -365
- package/templates/standard/workers/lib/sentinel/gap-detection.ts +48 -8
- package/templates/standard/workers/platform-sentinel.ts +4 -4
|
@@ -357,7 +357,8 @@ export async function checkAndTripCircuitBreakers(env: Env): Promise<boolean> {
|
|
|
357
357
|
|
|
358
358
|
// Fetch settings and D1 writes in parallel
|
|
359
359
|
const [settings, writes24h] = await Promise.all([getPlatformSettings(env), getD1WriteCount(env)]);
|
|
360
|
-
|
|
360
|
+
// Defense-in-depth: floor critical limits to prevent stale/poisoned cache values
|
|
361
|
+
const d1WriteLimit = Math.max(settings.d1WriteLimit, 1000);
|
|
361
362
|
|
|
362
363
|
// Check D1 write limit (global)
|
|
363
364
|
const d1Status = determineCircuitBreakerStatus(writes24h, d1WriteLimit);
|
|
@@ -869,8 +870,9 @@ const MONTHLY_METRIC_TO_COLUMN: Record<string, string> = {
|
|
|
869
870
|
/** Allowlist for safe column interpolation in SQL. */
|
|
870
871
|
const ALLOWED_MONTHLY_COLUMNS = new Set(Object.values(MONTHLY_METRIC_TO_COLUMN));
|
|
871
872
|
|
|
872
|
-
// TODO: Add your project
|
|
873
|
-
|
|
873
|
+
// TODO: Add your project slugs from project_registry (e.g., 'my-app', 'my-api')
|
|
874
|
+
// 'all' is the account-level aggregate and should always be included.
|
|
875
|
+
const MONTHLY_PROJECTS = ['all'] as const;
|
|
874
876
|
|
|
875
877
|
/**
|
|
876
878
|
* Check monthly budget usage against limits.
|
|
@@ -910,6 +912,10 @@ export async function checkMonthlyBudgets(env: Env): Promise<number> {
|
|
|
910
912
|
return 0;
|
|
911
913
|
}
|
|
912
914
|
|
|
915
|
+
// Pre-calculate month start to enable index usage on (project, snapshot_date)
|
|
916
|
+
const now = new Date();
|
|
917
|
+
const monthStart = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, '0')}-01`;
|
|
918
|
+
|
|
913
919
|
// For each project, get monthly totals from daily_usage_rollups
|
|
914
920
|
for (const project of MONTHLY_PROJECTS) {
|
|
915
921
|
// Get the monthly sum for this project
|
|
@@ -929,9 +935,9 @@ export async function checkMonthlyBudgets(env: Env): Promise<number> {
|
|
|
929
935
|
SUM(vectorize_queries) as vectorize_queries,
|
|
930
936
|
SUM(vectorize_inserts) as vectorize_inserts
|
|
931
937
|
FROM daily_usage_rollups
|
|
932
|
-
WHERE project = ? AND snapshot_date >=
|
|
938
|
+
WHERE project = ? AND snapshot_date >= ?
|
|
933
939
|
LIMIT 1
|
|
934
|
-
`).bind(project).first<Record<string, number | null>>();
|
|
940
|
+
`).bind(project, monthStart).first<Record<string, number | null>>();
|
|
935
941
|
|
|
936
942
|
if (!monthlyTotals) continue;
|
|
937
943
|
|
|
@@ -388,8 +388,9 @@ const MONITORED_METRICS = [
|
|
|
388
388
|
* Projects monitored for per-project anomaly detection.
|
|
389
389
|
* Includes 'all' (aggregate) plus individual projects.
|
|
390
390
|
*/
|
|
391
|
-
// TODO: Add your project
|
|
392
|
-
|
|
391
|
+
// TODO: Add your project slugs from project_registry (e.g., 'my-app', 'my-api')
|
|
392
|
+
// 'all' is the account-level aggregate and should always be included.
|
|
393
|
+
const MONITORED_PROJECTS = ['all'] as const;
|
|
393
394
|
|
|
394
395
|
/**
|
|
395
396
|
* Run anomaly detection for key metrics across all monitored projects.
|
|
@@ -467,6 +468,13 @@ export async function calculateHourlyRollingStats(
|
|
|
467
468
|
}
|
|
468
469
|
|
|
469
470
|
try {
|
|
471
|
+
// Pre-calculate time bounds to enable index usage on snapshot_hour
|
|
472
|
+
const now = new Date();
|
|
473
|
+
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
474
|
+
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
|
475
|
+
const sevenDaysAgoStr = sevenDaysAgo.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
476
|
+
const oneHourAgoStr = oneHourAgo.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
477
|
+
|
|
470
478
|
const result = await env.PLATFORM_DB.prepare(
|
|
471
479
|
`
|
|
472
480
|
SELECT
|
|
@@ -476,11 +484,11 @@ export async function calculateHourlyRollingStats(
|
|
|
476
484
|
AVG(${metric}) as avg_value
|
|
477
485
|
FROM hourly_usage_snapshots
|
|
478
486
|
WHERE project = ?
|
|
479
|
-
AND snapshot_hour >=
|
|
480
|
-
AND snapshot_hour <
|
|
487
|
+
AND snapshot_hour >= ?
|
|
488
|
+
AND snapshot_hour < ?
|
|
481
489
|
`
|
|
482
490
|
)
|
|
483
|
-
.bind(project)
|
|
491
|
+
.bind(project, sevenDaysAgoStr, oneHourAgoStr)
|
|
484
492
|
.first<{
|
|
485
493
|
sample_count: number;
|
|
486
494
|
sum_value: number;
|
|
@@ -522,19 +530,26 @@ export async function detectHourlyD1WriteAnomalies(env: Env): Promise<number> {
|
|
|
522
530
|
const log = createLoggerFromEnv(env, 'platform-usage', 'platform:usage:anomaly');
|
|
523
531
|
|
|
524
532
|
try {
|
|
533
|
+
// Pre-calculate time bounds to enable index usage on snapshot_hour
|
|
534
|
+
const now = new Date();
|
|
535
|
+
const twoHoursAgo = new Date(now.getTime() - 2 * 60 * 60 * 1000);
|
|
536
|
+
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
|
537
|
+
const twoHoursAgoStr = twoHoursAgo.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
538
|
+
const oneHourAgoStr = oneHourAgo.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
539
|
+
|
|
525
540
|
// Get the last completed hour's value
|
|
526
541
|
const lastHourResult = await env.PLATFORM_DB.prepare(
|
|
527
542
|
`
|
|
528
543
|
SELECT ${metric} as value
|
|
529
544
|
FROM hourly_usage_snapshots
|
|
530
545
|
WHERE project = ?
|
|
531
|
-
AND snapshot_hour >=
|
|
532
|
-
AND snapshot_hour <
|
|
546
|
+
AND snapshot_hour >= ?
|
|
547
|
+
AND snapshot_hour < ?
|
|
533
548
|
ORDER BY snapshot_hour DESC
|
|
534
549
|
LIMIT 1
|
|
535
550
|
`
|
|
536
551
|
)
|
|
537
|
-
.bind(project)
|
|
552
|
+
.bind(project, twoHoursAgoStr, oneHourAgoStr)
|
|
538
553
|
.first<{ value: number }>();
|
|
539
554
|
|
|
540
555
|
if (!lastHourResult || lastHourResult.value === 0) {
|
|
@@ -127,11 +127,14 @@ export async function runDailyRollup(env: Env, date: string): Promise<number> {
|
|
|
127
127
|
const log = createLoggerFromEnv(env, 'platform-usage', 'platform:usage:scheduled');
|
|
128
128
|
log.info(`Running daily rollup for ${date}`, { tag: 'SCHEDULED' });
|
|
129
129
|
|
|
130
|
-
// Calculate
|
|
130
|
+
// Calculate previous and next day dates for range queries (index-friendly)
|
|
131
131
|
const targetDate = new Date(date + 'T00:00:00Z');
|
|
132
132
|
const prevDate = new Date(targetDate);
|
|
133
133
|
prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
134
134
|
const prevDateStr = prevDate.toISOString().split('T')[0];
|
|
135
|
+
const nextDate = new Date(targetDate);
|
|
136
|
+
nextDate.setUTCDate(nextDate.getUTCDate() + 1);
|
|
137
|
+
const nextDateStr = nextDate.toISOString().split('T')[0];
|
|
135
138
|
|
|
136
139
|
// Check if this is the first day of the month
|
|
137
140
|
const isFirstDayOfMonth = targetDate.getUTCDate() === 1;
|
|
@@ -269,11 +272,11 @@ export async function runDailyRollup(env: Env, date: string): Promise<number> {
|
|
|
269
272
|
MAX(total_cost_usd) as total_cost_usd,
|
|
270
273
|
COUNT(*) as samples_count
|
|
271
274
|
FROM hourly_usage_snapshots
|
|
272
|
-
WHERE
|
|
275
|
+
WHERE snapshot_hour >= ? AND snapshot_hour < ?
|
|
273
276
|
GROUP BY project
|
|
274
277
|
`
|
|
275
278
|
)
|
|
276
|
-
.bind(date)
|
|
279
|
+
.bind(date + 'T00:00:00Z', nextDateStr + 'T00:00:00Z')
|
|
277
280
|
.all<HourlyMaxRow>();
|
|
278
281
|
|
|
279
282
|
if (!todayResult.results || todayResult.results.length === 0) {
|
|
@@ -324,11 +327,11 @@ export async function runDailyRollup(env: Env, date: string): Promise<number> {
|
|
|
324
327
|
MAX(COALESCE(workflows_cost_usd, 0)) as workflows_cost_usd,
|
|
325
328
|
MAX(total_cost_usd) as total_cost_usd
|
|
326
329
|
FROM hourly_usage_snapshots
|
|
327
|
-
WHERE
|
|
330
|
+
WHERE snapshot_hour >= ? AND snapshot_hour < ?
|
|
328
331
|
GROUP BY project
|
|
329
332
|
`
|
|
330
333
|
)
|
|
331
|
-
.bind(prevDateStr)
|
|
334
|
+
.bind(prevDateStr + 'T00:00:00Z', date + 'T00:00:00Z')
|
|
332
335
|
.all<PrevDayMaxRow>();
|
|
333
336
|
|
|
334
337
|
if (prevResult.results) {
|