@littlebearapps/platform-admin-sdk 2.1.0 → 2.3.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 +2 -5
- package/dist/check-upgrade.d.ts +29 -0
- package/dist/check-upgrade.js +97 -0
- package/dist/index.js +59 -4
- package/dist/manifest.d.ts +2 -0
- package/dist/scaffold.js +5 -1
- package/dist/templates.d.ts +6 -1
- package/dist/templates.js +141 -3
- package/dist/upgrade.d.ts +1 -0
- package/dist/upgrade.js +21 -2
- package/package.json +1 -1
- package/templates/full/dashboard/src/components/notifications/NotificationDropdown.tsx +130 -0
- package/templates/full/dashboard/src/components/notifications/NotificationItem.tsx +264 -0
- package/templates/full/dashboard/src/components/patterns/PatternInfoButton.tsx +60 -0
- package/templates/full/dashboard/src/components/reports/FeatureUsageReport.tsx +339 -0
- package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
- package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -0
- package/templates/full/dashboard/src/pages/api/patterns/[id]/approve.ts +49 -0
- package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
- package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
- package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
- package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
- package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
- package/templates/full/dashboard/src/pages/feedback.astro +365 -0
- package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
- package/templates/full/dashboard/src/pages/map.astro +561 -0
- package/templates/full/dashboard/src/pages/revenue.astro +72 -0
- package/templates/full/dashboard/src/pages/tests.astro +431 -0
- package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
- package/templates/full/scripts/ops/verify-account-total.ts +256 -0
- package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
- package/templates/full/tests/integration/r2-archive.test.ts +108 -0
- package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
- package/templates/shared/.github/workflows/validate-controls.yml +27 -0
- package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
- package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
- package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
- package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
- package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
- package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
- package/templates/shared/dashboard/src/components/Toast.astro +170 -0
- package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
- package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
- package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
- package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
- package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
- package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
- package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
- package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
- package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
- package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
- package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
- package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
- package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
- package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
- package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
- package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
- package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
- package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
- package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
- package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
- package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
- package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
- package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
- package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
- package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
- package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
- package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
- package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
- package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
- package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
- package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
- package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
- package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
- package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
- package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
- package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
- package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
- package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
- package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
- package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
- package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
- package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
- package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
- package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
- package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
- package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
- package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -0
- package/templates/shared/dashboard/src/components/usage/scripts/ai-tab-controller.ts +419 -0
- package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
- package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
- package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
- package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
- package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
- package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
- package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
- package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
- package/templates/shared/dashboard/src/components/usage/types.ts +283 -0
- package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -0
- package/templates/shared/dashboard/src/pages/api/usage/ai-models.ts +235 -0
- package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
- package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
- package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
- package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -0
- package/templates/standard/dashboard/src/components/errors/PriorityBadge.astro +27 -0
- package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
- package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -0
- package/templates/standard/dashboard/src/pages/analytics.astro +64 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
- package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
- package/templates/standard/tests/integration/connectors.test.ts +241 -0
- package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
- package/templates/standard/tests/integration/ingestion.test.ts +211 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Audit Cost Anomaly Script
|
|
4
|
+
*
|
|
5
|
+
* Forensic analysis of cost anomalies in daily_usage_rollups.
|
|
6
|
+
* Identifies days with unexpectedly high costs and breaks down by component.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx tsx scripts/ops/audit-cost-anomaly.ts # High-cost days
|
|
10
|
+
* npx tsx scripts/ops/audit-cost-anomaly.ts --threshold 50 # Custom threshold ($50)
|
|
11
|
+
* npx tsx scripts/ops/audit-cost-anomaly.ts --detailed # Include hourly breakdown
|
|
12
|
+
* npx tsx scripts/ops/audit-cost-anomaly.ts --date 2026-01-23 # Specific date deep dive
|
|
13
|
+
*
|
|
14
|
+
* Environment Variables:
|
|
15
|
+
* CLOUDFLARE_ACCOUNT_ID - Cloudflare account ID (optional, uses default)
|
|
16
|
+
* CLOUDFLARE_API_TOKEN - API token with D1:Read (required)
|
|
17
|
+
* D1_DATABASE_ID - Platform D1 database ID (optional, uses default)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { request } from 'undici';
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// CONFIGURATION
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
const REST_API_BASE = 'https://api.cloudflare.com/client/v4';
|
|
27
|
+
const DEFAULT_ACCOUNT_ID = '55a0bf6d1396d90cbf9dcbf30fceeb14';
|
|
28
|
+
const DEFAULT_D1_DATABASE_ID = '076b2cd5-054e-4c8c-aa42-86cedc84a4ec';
|
|
29
|
+
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// TYPES
|
|
32
|
+
// =============================================================================
|
|
33
|
+
|
|
34
|
+
interface D1QueryResponse {
|
|
35
|
+
success: boolean;
|
|
36
|
+
errors?: Array<{ message: string }>;
|
|
37
|
+
result: Array<{
|
|
38
|
+
results: Record<string, unknown>[];
|
|
39
|
+
success: boolean;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface HighCostDay {
|
|
44
|
+
project: string;
|
|
45
|
+
snapshot_date: string;
|
|
46
|
+
d1_writes: number;
|
|
47
|
+
d1_reads: number;
|
|
48
|
+
kv_writes: number;
|
|
49
|
+
kv_reads: number;
|
|
50
|
+
workers_requests: number;
|
|
51
|
+
total_cost: number;
|
|
52
|
+
d1_cost: number;
|
|
53
|
+
kv_cost: number;
|
|
54
|
+
workers_cost: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface HourlySample {
|
|
58
|
+
snapshot_hour: string;
|
|
59
|
+
project: string;
|
|
60
|
+
d1_rows_written: number;
|
|
61
|
+
d1_rows_read: number;
|
|
62
|
+
kv_writes: number;
|
|
63
|
+
total_cost_usd: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// =============================================================================
|
|
67
|
+
// D1 QUERY FUNCTIONS
|
|
68
|
+
// =============================================================================
|
|
69
|
+
|
|
70
|
+
async function queryD1(
|
|
71
|
+
sql: string,
|
|
72
|
+
accountId: string,
|
|
73
|
+
apiToken: string,
|
|
74
|
+
databaseId: string
|
|
75
|
+
): Promise<Record<string, unknown>[]> {
|
|
76
|
+
const url = `${REST_API_BASE}/accounts/${accountId}/d1/database/${databaseId}/query`;
|
|
77
|
+
|
|
78
|
+
const response = await request(url, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: {
|
|
81
|
+
Authorization: `Bearer ${apiToken}`,
|
|
82
|
+
'Content-Type': 'application/json',
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify({ sql }),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const body = (await response.body.json()) as D1QueryResponse;
|
|
88
|
+
|
|
89
|
+
if (!body.success) {
|
|
90
|
+
const errorMsg = body.errors?.map((e) => e.message).join(', ') ?? 'Unknown error';
|
|
91
|
+
throw new Error(`D1 query failed: ${errorMsg}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return body.result[0]?.results ?? [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// =============================================================================
|
|
98
|
+
// ANALYSIS QUERIES
|
|
99
|
+
// =============================================================================
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Find days with costs exceeding threshold, broken down by resource type.
|
|
103
|
+
*/
|
|
104
|
+
async function findHighCostDays(
|
|
105
|
+
threshold: number,
|
|
106
|
+
accountId: string,
|
|
107
|
+
apiToken: string,
|
|
108
|
+
databaseId: string
|
|
109
|
+
): Promise<HighCostDay[]> {
|
|
110
|
+
const sql = `
|
|
111
|
+
SELECT
|
|
112
|
+
project,
|
|
113
|
+
snapshot_date,
|
|
114
|
+
SUM(d1_rows_written) as d1_writes,
|
|
115
|
+
SUM(d1_rows_read) as d1_reads,
|
|
116
|
+
SUM(kv_writes) as kv_writes,
|
|
117
|
+
SUM(kv_reads) as kv_reads,
|
|
118
|
+
SUM(workers_requests) as workers_requests,
|
|
119
|
+
SUM(total_cost_usd) as total_cost,
|
|
120
|
+
SUM(d1_cost_usd) as d1_cost,
|
|
121
|
+
SUM(kv_cost_usd) as kv_cost,
|
|
122
|
+
SUM(workers_cost_usd) as workers_cost
|
|
123
|
+
FROM daily_usage_rollups
|
|
124
|
+
WHERE total_cost_usd > ${threshold}
|
|
125
|
+
GROUP BY project, snapshot_date
|
|
126
|
+
ORDER BY total_cost DESC
|
|
127
|
+
LIMIT 50;
|
|
128
|
+
`;
|
|
129
|
+
|
|
130
|
+
const results = await queryD1(sql, accountId, apiToken, databaseId);
|
|
131
|
+
return results as unknown as HighCostDay[];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get hourly breakdown for a specific date.
|
|
136
|
+
*/
|
|
137
|
+
async function getHourlyBreakdown(
|
|
138
|
+
date: string,
|
|
139
|
+
accountId: string,
|
|
140
|
+
apiToken: string,
|
|
141
|
+
databaseId: string
|
|
142
|
+
): Promise<HourlySample[]> {
|
|
143
|
+
const sql = `
|
|
144
|
+
SELECT
|
|
145
|
+
snapshot_hour,
|
|
146
|
+
project,
|
|
147
|
+
d1_rows_written,
|
|
148
|
+
d1_rows_read,
|
|
149
|
+
kv_writes,
|
|
150
|
+
total_cost_usd
|
|
151
|
+
FROM hourly_usage_snapshots
|
|
152
|
+
WHERE snapshot_hour LIKE '${date}%'
|
|
153
|
+
ORDER BY snapshot_hour, project;
|
|
154
|
+
`;
|
|
155
|
+
|
|
156
|
+
const results = await queryD1(sql, accountId, apiToken, databaseId);
|
|
157
|
+
return results as unknown as HourlySample[];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get billing period summary.
|
|
162
|
+
*/
|
|
163
|
+
async function getBillingPeriodSummary(
|
|
164
|
+
accountId: string,
|
|
165
|
+
apiToken: string,
|
|
166
|
+
databaseId: string
|
|
167
|
+
): Promise<Record<string, unknown>[]> {
|
|
168
|
+
// First get the billing cycle day
|
|
169
|
+
const settingsSql = `
|
|
170
|
+
SELECT billing_cycle_day
|
|
171
|
+
FROM billing_settings
|
|
172
|
+
LIMIT 1;
|
|
173
|
+
`;
|
|
174
|
+
const settings = await queryD1(settingsSql, accountId, apiToken, databaseId);
|
|
175
|
+
|
|
176
|
+
// Calculate billing window dates
|
|
177
|
+
const now = new Date();
|
|
178
|
+
let billingCycleDay = 17; // Default
|
|
179
|
+
if (settings.length > 0 && settings[0].billing_cycle_day) {
|
|
180
|
+
billingCycleDay = settings[0].billing_cycle_day as number;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Calculate start date of current billing period
|
|
184
|
+
let startDate: Date;
|
|
185
|
+
if (now.getUTCDate() >= billingCycleDay) {
|
|
186
|
+
startDate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), billingCycleDay));
|
|
187
|
+
} else {
|
|
188
|
+
startDate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 1, billingCycleDay));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const startDateStr = startDate.toISOString().split('T')[0];
|
|
192
|
+
|
|
193
|
+
const sql = `
|
|
194
|
+
SELECT
|
|
195
|
+
project,
|
|
196
|
+
SUM(total_cost_usd) as period_cost,
|
|
197
|
+
SUM(d1_rows_written) as total_d1_writes,
|
|
198
|
+
SUM(d1_cost_usd) as total_d1_cost,
|
|
199
|
+
SUM(kv_writes) as total_kv_writes,
|
|
200
|
+
SUM(kv_cost_usd) as total_kv_cost,
|
|
201
|
+
COUNT(DISTINCT snapshot_date) as days_in_period
|
|
202
|
+
FROM daily_usage_rollups
|
|
203
|
+
WHERE snapshot_date >= '${startDateStr}'
|
|
204
|
+
GROUP BY project
|
|
205
|
+
ORDER BY period_cost DESC;
|
|
206
|
+
`;
|
|
207
|
+
|
|
208
|
+
const results = await queryD1(sql, accountId, apiToken, databaseId);
|
|
209
|
+
return results.map((r) => ({ ...r, billing_start: startDateStr }));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Verify cost calculation logic by checking raw metrics vs computed cost.
|
|
214
|
+
*/
|
|
215
|
+
async function verifyCostCalculation(
|
|
216
|
+
date: string,
|
|
217
|
+
accountId: string,
|
|
218
|
+
apiToken: string,
|
|
219
|
+
databaseId: string
|
|
220
|
+
): Promise<Record<string, unknown>[]> {
|
|
221
|
+
const sql = `
|
|
222
|
+
SELECT
|
|
223
|
+
project,
|
|
224
|
+
snapshot_date,
|
|
225
|
+
d1_rows_written,
|
|
226
|
+
d1_rows_read,
|
|
227
|
+
d1_cost_usd,
|
|
228
|
+
kv_writes,
|
|
229
|
+
kv_reads,
|
|
230
|
+
kv_cost_usd,
|
|
231
|
+
workers_requests,
|
|
232
|
+
workers_cost_usd,
|
|
233
|
+
total_cost_usd,
|
|
234
|
+
-- Expected D1 cost: $1/million writes + $0.001/million reads (first 25B free)
|
|
235
|
+
ROUND(d1_rows_written / 1000000.0, 4) as expected_d1_write_cost,
|
|
236
|
+
-- Expected KV cost: $1/million writes, $0.50/million reads (first 10M free)
|
|
237
|
+
ROUND(kv_writes / 1000000.0, 4) as expected_kv_write_cost
|
|
238
|
+
FROM daily_usage_rollups
|
|
239
|
+
WHERE snapshot_date = '${date}'
|
|
240
|
+
ORDER BY total_cost_usd DESC;
|
|
241
|
+
`;
|
|
242
|
+
|
|
243
|
+
return await queryD1(sql, accountId, apiToken, databaseId);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// =============================================================================
|
|
247
|
+
// FORMATTING
|
|
248
|
+
// =============================================================================
|
|
249
|
+
|
|
250
|
+
function formatNumber(n: number): string {
|
|
251
|
+
if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(2)}B`;
|
|
252
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
|
|
253
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
|
254
|
+
return n.toFixed(0);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function formatCost(n: number): string {
|
|
258
|
+
return `$${n.toFixed(2)}`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// =============================================================================
|
|
262
|
+
// CLI
|
|
263
|
+
// =============================================================================
|
|
264
|
+
|
|
265
|
+
async function main(): Promise<void> {
|
|
266
|
+
const args = process.argv.slice(2);
|
|
267
|
+
|
|
268
|
+
// Parse arguments
|
|
269
|
+
const thresholdIdx = args.indexOf('--threshold');
|
|
270
|
+
const threshold = thresholdIdx >= 0 ? parseFloat(args[thresholdIdx + 1]) : 100;
|
|
271
|
+
|
|
272
|
+
const dateIdx = args.indexOf('--date');
|
|
273
|
+
const specificDate = dateIdx >= 0 ? args[dateIdx + 1] : null;
|
|
274
|
+
|
|
275
|
+
const detailed = args.includes('--detailed');
|
|
276
|
+
|
|
277
|
+
// Environment variables
|
|
278
|
+
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID ?? DEFAULT_ACCOUNT_ID;
|
|
279
|
+
const apiToken = process.env.CLOUDFLARE_API_TOKEN;
|
|
280
|
+
const databaseId = process.env.D1_DATABASE_ID ?? DEFAULT_D1_DATABASE_ID;
|
|
281
|
+
|
|
282
|
+
if (!apiToken) {
|
|
283
|
+
console.error('Error: CLOUDFLARE_API_TOKEN environment variable is required');
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
console.log('='.repeat(80));
|
|
288
|
+
console.log('COST ANOMALY FORENSIC AUDIT');
|
|
289
|
+
console.log('='.repeat(80));
|
|
290
|
+
console.log(`Threshold: ${formatCost(threshold)}`);
|
|
291
|
+
console.log(`Database: ${databaseId}`);
|
|
292
|
+
console.log('');
|
|
293
|
+
|
|
294
|
+
// Section 1: Billing Period Summary
|
|
295
|
+
console.log('─'.repeat(80));
|
|
296
|
+
console.log('SECTION 1: CURRENT BILLING PERIOD SUMMARY');
|
|
297
|
+
console.log('─'.repeat(80));
|
|
298
|
+
|
|
299
|
+
const billingSummary = await getBillingPeriodSummary(accountId, apiToken, databaseId);
|
|
300
|
+
if (billingSummary.length > 0) {
|
|
301
|
+
console.log(`Billing period start: ${billingSummary[0].billing_start}`);
|
|
302
|
+
console.log('');
|
|
303
|
+
console.log(
|
|
304
|
+
'Project | Period Cost | D1 Writes | D1 Cost | KV Writes | KV Cost'
|
|
305
|
+
);
|
|
306
|
+
console.log(
|
|
307
|
+
'-------------------|-------------|---------------|-----------|-------------|--------'
|
|
308
|
+
);
|
|
309
|
+
for (const row of billingSummary) {
|
|
310
|
+
console.log(
|
|
311
|
+
`${String(row.project).padEnd(18)} | ` +
|
|
312
|
+
`${formatCost(row.period_cost as number).padStart(11)} | ` +
|
|
313
|
+
`${formatNumber(row.total_d1_writes as number).padStart(13)} | ` +
|
|
314
|
+
`${formatCost(row.total_d1_cost as number).padStart(9)} | ` +
|
|
315
|
+
`${formatNumber(row.total_kv_writes as number).padStart(11)} | ` +
|
|
316
|
+
`${formatCost(row.total_kv_cost as number).padStart(6)}`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
console.log('No data found for current billing period.');
|
|
321
|
+
}
|
|
322
|
+
console.log('');
|
|
323
|
+
|
|
324
|
+
// Section 2: High-Cost Days
|
|
325
|
+
console.log('─'.repeat(80));
|
|
326
|
+
console.log(`SECTION 2: HIGH-COST DAYS (>${formatCost(threshold)})`);
|
|
327
|
+
console.log('─'.repeat(80));
|
|
328
|
+
|
|
329
|
+
const highCostDays = await findHighCostDays(threshold, accountId, apiToken, databaseId);
|
|
330
|
+
if (highCostDays.length === 0) {
|
|
331
|
+
console.log(`No days found with cost > ${formatCost(threshold)}`);
|
|
332
|
+
} else {
|
|
333
|
+
console.log(
|
|
334
|
+
'Date | Project | Total Cost | D1 Writes | D1 Cost | KV Writes | KV Cost'
|
|
335
|
+
);
|
|
336
|
+
console.log(
|
|
337
|
+
'-----------|-------------|------------|---------------|-----------|-------------|--------'
|
|
338
|
+
);
|
|
339
|
+
for (const row of highCostDays) {
|
|
340
|
+
console.log(
|
|
341
|
+
`${row.snapshot_date} | ` +
|
|
342
|
+
`${String(row.project).padEnd(11)} | ` +
|
|
343
|
+
`${formatCost(row.total_cost).padStart(10)} | ` +
|
|
344
|
+
`${formatNumber(row.d1_writes).padStart(13)} | ` +
|
|
345
|
+
`${formatCost(row.d1_cost).padStart(9)} | ` +
|
|
346
|
+
`${formatNumber(row.kv_writes).padStart(11)} | ` +
|
|
347
|
+
`${formatCost(row.kv_cost).padStart(6)}`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
console.log('');
|
|
352
|
+
|
|
353
|
+
// Section 3: Cost Calculation Verification (if specific date or highest cost date)
|
|
354
|
+
const auditDate =
|
|
355
|
+
specificDate ?? (highCostDays.length > 0 ? highCostDays[0].snapshot_date : null);
|
|
356
|
+
if (auditDate) {
|
|
357
|
+
console.log('─'.repeat(80));
|
|
358
|
+
console.log(`SECTION 3: COST CALCULATION VERIFICATION FOR ${auditDate}`);
|
|
359
|
+
console.log('─'.repeat(80));
|
|
360
|
+
|
|
361
|
+
const verification = await verifyCostCalculation(auditDate, accountId, apiToken, databaseId);
|
|
362
|
+
console.log('');
|
|
363
|
+
console.log('Checking if recorded costs match expected costs based on Cloudflare pricing...');
|
|
364
|
+
console.log('D1 pricing: $1.00/million writes, $0.001/million reads (25B free)');
|
|
365
|
+
console.log('KV pricing: $1.00/million writes, $0.50/million reads (10M free)');
|
|
366
|
+
console.log('');
|
|
367
|
+
|
|
368
|
+
for (const row of verification) {
|
|
369
|
+
console.log(`Project: ${row.project}`);
|
|
370
|
+
console.log(` D1 Writes: ${formatNumber(row.d1_rows_written as number)}`);
|
|
371
|
+
console.log(` D1 Recorded Cost: ${formatCost(row.d1_cost_usd as number)}`);
|
|
372
|
+
console.log(
|
|
373
|
+
` D1 Expected Cost: ${formatCost(row.expected_d1_write_cost as number)} (writes only)`
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
const d1Diff = Math.abs((row.d1_cost_usd as number) - (row.expected_d1_write_cost as number));
|
|
377
|
+
if (d1Diff > 1) {
|
|
378
|
+
console.log(` ⚠️ D1 COST DISCREPANCY: ${formatCost(d1Diff)}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
console.log(` KV Writes: ${formatNumber(row.kv_writes as number)}`);
|
|
382
|
+
console.log(` KV Recorded Cost: ${formatCost(row.kv_cost_usd as number)}`);
|
|
383
|
+
console.log(
|
|
384
|
+
` KV Expected Cost: ${formatCost(row.expected_kv_write_cost as number)} (writes only)`
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
const kvDiff = Math.abs((row.kv_cost_usd as number) - (row.expected_kv_write_cost as number));
|
|
388
|
+
if (kvDiff > 1) {
|
|
389
|
+
console.log(` ⚠️ KV COST DISCREPANCY: ${formatCost(kvDiff)}`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
console.log(` Total Recorded Cost: ${formatCost(row.total_cost_usd as number)}`);
|
|
393
|
+
console.log('');
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Section 4: Hourly breakdown (if requested)
|
|
398
|
+
if (detailed && auditDate) {
|
|
399
|
+
console.log('─'.repeat(80));
|
|
400
|
+
console.log(`SECTION 4: HOURLY BREAKDOWN FOR ${auditDate}`);
|
|
401
|
+
console.log('─'.repeat(80));
|
|
402
|
+
|
|
403
|
+
const hourly = await getHourlyBreakdown(auditDate, accountId, apiToken, databaseId);
|
|
404
|
+
if (hourly.length === 0) {
|
|
405
|
+
console.log('No hourly data found (may have been rolled up and deleted).');
|
|
406
|
+
} else {
|
|
407
|
+
console.log('Hour | Project | D1 Writes | KV Writes | Cost');
|
|
408
|
+
console.log('---------------------|-------------|---------------|-------------|--------');
|
|
409
|
+
for (const row of hourly) {
|
|
410
|
+
console.log(
|
|
411
|
+
`${row.snapshot_hour} | ` +
|
|
412
|
+
`${String(row.project).padEnd(11)} | ` +
|
|
413
|
+
`${formatNumber(row.d1_rows_written).padStart(13)} | ` +
|
|
414
|
+
`${formatNumber(row.kv_writes).padStart(11)} | ` +
|
|
415
|
+
`${formatCost(row.total_cost_usd).padStart(6)}`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
console.log('');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
console.log('='.repeat(80));
|
|
423
|
+
console.log('AUDIT COMPLETE');
|
|
424
|
+
console.log('='.repeat(80));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
main().catch((error) => {
|
|
428
|
+
console.error('Fatal error:', error);
|
|
429
|
+
process.exit(1);
|
|
430
|
+
});
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* verify-account-total.ts
|
|
4
|
+
*
|
|
5
|
+
* Queries Cloudflare GraphQL for account-wide usage totals (not filtered by script).
|
|
6
|
+
* This provides the "ground truth" for what the account actually used, regardless
|
|
7
|
+
* of whether individual workers were deleted.
|
|
8
|
+
*
|
|
9
|
+
* Purpose: Determine if the ~$2,600 australian-history-mcp cost was REAL or PHANTOM
|
|
10
|
+
* by comparing account-level metrics to our D1 records.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* npx tsx scripts/ops/verify-account-total.ts
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const GRAPHQL_ENDPOINT = 'https://api.cloudflare.com/client/v4/graphql';
|
|
17
|
+
const ACCOUNT_ID = '55a0bf6d1396d90cbf9dcbf30fceeb14';
|
|
18
|
+
|
|
19
|
+
// Pricing constants (Workers Paid Plan)
|
|
20
|
+
const WORKERS_INCLUDED_REQUESTS = 10_000_000; // 10M included
|
|
21
|
+
const WORKERS_REQUEST_COST_PER_MILLION = 0.3; // $0.30 per million after included
|
|
22
|
+
const WORKERS_CPU_COST_PER_MILLION_MS = 0.02; // $0.02 per million CPU ms (approx)
|
|
23
|
+
|
|
24
|
+
interface GraphQLResponse {
|
|
25
|
+
data?: {
|
|
26
|
+
viewer?: {
|
|
27
|
+
accounts?: Array<{
|
|
28
|
+
workersInvocationsAdaptive?: Array<{
|
|
29
|
+
sum: {
|
|
30
|
+
requests: number;
|
|
31
|
+
errors: number;
|
|
32
|
+
subrequests: number;
|
|
33
|
+
};
|
|
34
|
+
quantiles: {
|
|
35
|
+
cpuTimeP50: number;
|
|
36
|
+
cpuTimeP99: number;
|
|
37
|
+
};
|
|
38
|
+
dimensions: {
|
|
39
|
+
date: string;
|
|
40
|
+
};
|
|
41
|
+
}>;
|
|
42
|
+
}>;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
errors?: Array<{ message: string }>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function queryAccountTotal(
|
|
49
|
+
apiToken: string,
|
|
50
|
+
startDate: string,
|
|
51
|
+
endDate: string
|
|
52
|
+
): Promise<{
|
|
53
|
+
totalRequests: number;
|
|
54
|
+
totalErrors: number;
|
|
55
|
+
totalSubrequests: number;
|
|
56
|
+
avgCpuTimeP50: number;
|
|
57
|
+
avgCpuTimeP99: number;
|
|
58
|
+
dailyBreakdown: Array<{ date: string; requests: number }>;
|
|
59
|
+
}> {
|
|
60
|
+
const query = `
|
|
61
|
+
query AccountUsage($accountTag: String!, $startDate: Time!, $endDate: Time!) {
|
|
62
|
+
viewer {
|
|
63
|
+
accounts(filter: { accountTag: $accountTag }) {
|
|
64
|
+
workersInvocationsAdaptive(
|
|
65
|
+
filter: {
|
|
66
|
+
datetime_geq: $startDate
|
|
67
|
+
datetime_lt: $endDate
|
|
68
|
+
}
|
|
69
|
+
limit: 1000
|
|
70
|
+
) {
|
|
71
|
+
sum {
|
|
72
|
+
requests
|
|
73
|
+
errors
|
|
74
|
+
subrequests
|
|
75
|
+
}
|
|
76
|
+
quantiles {
|
|
77
|
+
cpuTimeP50
|
|
78
|
+
cpuTimeP99
|
|
79
|
+
}
|
|
80
|
+
dimensions {
|
|
81
|
+
date
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
const response = await fetch(GRAPHQL_ENDPOINT, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: {
|
|
92
|
+
Authorization: `Bearer ${apiToken}`,
|
|
93
|
+
'Content-Type': 'application/json',
|
|
94
|
+
},
|
|
95
|
+
body: JSON.stringify({
|
|
96
|
+
query,
|
|
97
|
+
variables: {
|
|
98
|
+
accountTag: ACCOUNT_ID,
|
|
99
|
+
startDate: `${startDate}T00:00:00Z`,
|
|
100
|
+
endDate: `${endDate}T00:00:00Z`,
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const result = (await response.json()) as GraphQLResponse;
|
|
106
|
+
|
|
107
|
+
if (result.errors?.length) {
|
|
108
|
+
throw new Error(`GraphQL errors: ${result.errors.map((e) => e.message).join(', ')}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const data = result.data?.viewer?.accounts?.[0]?.workersInvocationsAdaptive ?? [];
|
|
112
|
+
|
|
113
|
+
let totalRequests = 0;
|
|
114
|
+
let totalErrors = 0;
|
|
115
|
+
let totalSubrequests = 0;
|
|
116
|
+
let cpuTimeP50Sum = 0;
|
|
117
|
+
let cpuTimeP99Sum = 0;
|
|
118
|
+
const dailyBreakdown: Array<{ date: string; requests: number }> = [];
|
|
119
|
+
|
|
120
|
+
for (const row of data) {
|
|
121
|
+
totalRequests += row.sum.requests;
|
|
122
|
+
totalErrors += row.sum.errors;
|
|
123
|
+
totalSubrequests += row.sum.subrequests;
|
|
124
|
+
cpuTimeP50Sum += row.quantiles.cpuTimeP50 * row.sum.requests;
|
|
125
|
+
cpuTimeP99Sum += row.quantiles.cpuTimeP99 * row.sum.requests;
|
|
126
|
+
dailyBreakdown.push({
|
|
127
|
+
date: row.dimensions.date,
|
|
128
|
+
requests: row.sum.requests,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
totalRequests,
|
|
134
|
+
totalErrors,
|
|
135
|
+
totalSubrequests,
|
|
136
|
+
avgCpuTimeP50: totalRequests > 0 ? cpuTimeP50Sum / totalRequests : 0,
|
|
137
|
+
avgCpuTimeP99: totalRequests > 0 ? cpuTimeP99Sum / totalRequests : 0,
|
|
138
|
+
dailyBreakdown: dailyBreakdown.sort((a, b) => a.date.localeCompare(b.date)),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function calculateWorkersCost(requests: number, avgCpuTimeMs: number): number {
|
|
143
|
+
// Billable requests (after 10M included)
|
|
144
|
+
const billableRequests = Math.max(0, requests - WORKERS_INCLUDED_REQUESTS);
|
|
145
|
+
const requestCost = (billableRequests / 1_000_000) * WORKERS_REQUEST_COST_PER_MILLION;
|
|
146
|
+
|
|
147
|
+
// CPU time cost (rough estimate based on total CPU ms)
|
|
148
|
+
// This is approximate - actual billing is more complex
|
|
149
|
+
const totalCpuMs = requests * avgCpuTimeMs;
|
|
150
|
+
const cpuCost = (totalCpuMs / 1_000_000) * WORKERS_CPU_COST_PER_MILLION_MS;
|
|
151
|
+
|
|
152
|
+
return requestCost + cpuCost;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function main(): Promise<void> {
|
|
156
|
+
const apiToken = process.env.CLOUDFLARE_API_TOKEN;
|
|
157
|
+
|
|
158
|
+
if (!apiToken) {
|
|
159
|
+
console.error('Error: CLOUDFLARE_API_TOKEN environment variable is required');
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log('='.repeat(70));
|
|
164
|
+
console.log('ACCOUNT-WIDE USAGE VERIFICATION');
|
|
165
|
+
console.log('='.repeat(70));
|
|
166
|
+
console.log('Purpose: Determine if the ~$2,600 was REAL or PHANTOM');
|
|
167
|
+
console.log('Method: Query account-level metrics (not filtered by script name)');
|
|
168
|
+
console.log('');
|
|
169
|
+
|
|
170
|
+
// Query January 2026
|
|
171
|
+
const startDate = '2026-01-01';
|
|
172
|
+
const endDate = '2026-02-01';
|
|
173
|
+
|
|
174
|
+
console.log(`Period: ${startDate} to ${endDate}`);
|
|
175
|
+
console.log('');
|
|
176
|
+
console.log('-'.repeat(70));
|
|
177
|
+
console.log('QUERYING CLOUDFLARE GRAPHQL...');
|
|
178
|
+
console.log('-'.repeat(70));
|
|
179
|
+
|
|
180
|
+
const metrics = await queryAccountTotal(apiToken, startDate, endDate);
|
|
181
|
+
|
|
182
|
+
console.log('');
|
|
183
|
+
console.log('ACCOUNT-WIDE TOTALS (January 2026):');
|
|
184
|
+
console.log('-'.repeat(70));
|
|
185
|
+
console.log(` Total Requests: ${metrics.totalRequests.toLocaleString()}`);
|
|
186
|
+
console.log(` Total Errors: ${metrics.totalErrors.toLocaleString()}`);
|
|
187
|
+
console.log(` Total Subrequests: ${metrics.totalSubrequests.toLocaleString()}`);
|
|
188
|
+
console.log(` Avg CPU Time (P50): ${metrics.avgCpuTimeP50.toFixed(2)} ms`);
|
|
189
|
+
console.log(` Avg CPU Time (P99): ${metrics.avgCpuTimeP99.toFixed(2)} ms`);
|
|
190
|
+
console.log('');
|
|
191
|
+
|
|
192
|
+
// Calculate estimated cost
|
|
193
|
+
const estimatedCost = calculateWorkersCost(metrics.totalRequests, metrics.avgCpuTimeP50);
|
|
194
|
+
|
|
195
|
+
console.log('ESTIMATED WORKERS COST:');
|
|
196
|
+
console.log('-'.repeat(70));
|
|
197
|
+
console.log(` Included requests: 10,000,000`);
|
|
198
|
+
console.log(
|
|
199
|
+
` Billable requests: ${Math.max(0, metrics.totalRequests - WORKERS_INCLUDED_REQUESTS).toLocaleString()}`
|
|
200
|
+
);
|
|
201
|
+
console.log(` Estimated cost: $${estimatedCost.toFixed(2)}`);
|
|
202
|
+
console.log('');
|
|
203
|
+
|
|
204
|
+
// Daily breakdown (top 10 days)
|
|
205
|
+
console.log('TOP 10 DAYS BY REQUESTS:');
|
|
206
|
+
console.log('-'.repeat(70));
|
|
207
|
+
const topDays = [...metrics.dailyBreakdown].sort((a, b) => b.requests - a.requests).slice(0, 10);
|
|
208
|
+
for (const day of topDays) {
|
|
209
|
+
console.log(` ${day.date}: ${day.requests.toLocaleString()} requests`);
|
|
210
|
+
}
|
|
211
|
+
console.log('');
|
|
212
|
+
|
|
213
|
+
// The Verdict
|
|
214
|
+
console.log('='.repeat(70));
|
|
215
|
+
console.log('THE VERDICT');
|
|
216
|
+
console.log('='.repeat(70));
|
|
217
|
+
|
|
218
|
+
if (metrics.totalRequests > 500_000) {
|
|
219
|
+
console.log('');
|
|
220
|
+
console.log('⚠️ VERDICT: POTENTIALLY REAL');
|
|
221
|
+
console.log('');
|
|
222
|
+
console.log('The account had significant traffic. However, this could be from:');
|
|
223
|
+
console.log(' 1. Known workers (scout, brand-copilot, platform-*)');
|
|
224
|
+
console.log(' 2. The deleted semantic-librarian worker');
|
|
225
|
+
console.log('');
|
|
226
|
+
console.log('Next step: Compare against D1 totals to find the gap.');
|
|
227
|
+
} else if (estimatedCost > 2000) {
|
|
228
|
+
console.log('');
|
|
229
|
+
console.log('🔴 VERDICT: REAL');
|
|
230
|
+
console.log('');
|
|
231
|
+
console.log('Account-level costs exceed $2,000.');
|
|
232
|
+
console.log('The deleted worker likely had real usage.');
|
|
233
|
+
console.log('Recommendation: Create placeholder record for billing reconciliation.');
|
|
234
|
+
} else if (estimatedCost < 500) {
|
|
235
|
+
console.log('');
|
|
236
|
+
console.log('✅ VERDICT: PHANTOM');
|
|
237
|
+
console.log('');
|
|
238
|
+
console.log('Account-level costs are under $500.');
|
|
239
|
+
console.log('The $2,600 was likely a data artifact from triple-counting.');
|
|
240
|
+
console.log('Current D1 totals (~$416) are correct.');
|
|
241
|
+
} else {
|
|
242
|
+
console.log('');
|
|
243
|
+
console.log('⚠️ VERDICT: INCONCLUSIVE');
|
|
244
|
+
console.log('');
|
|
245
|
+
console.log('Account costs are in the middle range ($500-$2000).');
|
|
246
|
+
console.log('Manual investigation recommended.');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.log('');
|
|
250
|
+
console.log('='.repeat(70));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
main().catch((error) => {
|
|
254
|
+
console.error('Fatal error:', error);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
});
|