@littlebearapps/platform-admin-sdk 1.4.2 → 2.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.
Files changed (189) hide show
  1. package/dist/templates.d.ts +1 -1
  2. package/dist/templates.js +232 -2
  3. package/package.json +1 -1
  4. package/templates/full/config/audit-targets.yaml +72 -0
  5. package/templates/full/dashboard/src/components/notifications/NotificationBell.tsx +30 -0
  6. package/templates/full/dashboard/src/components/notifications/NotificationList.tsx +116 -0
  7. package/templates/full/dashboard/src/components/notifications/index.ts +2 -0
  8. package/templates/full/dashboard/src/components/patterns/ActivePatterns.tsx +62 -0
  9. package/templates/full/dashboard/src/components/patterns/PatternStats.tsx +60 -0
  10. package/templates/full/dashboard/src/components/patterns/PatternTabs.tsx +116 -0
  11. package/templates/full/dashboard/src/components/patterns/SuggestionsQueue.tsx +115 -0
  12. package/templates/full/dashboard/src/components/patterns/SystemPatterns.tsx +52 -0
  13. package/templates/full/dashboard/src/components/patterns/index.ts +5 -0
  14. package/templates/full/dashboard/src/components/reports/GapDetectionReport.tsx +69 -0
  15. package/templates/full/dashboard/src/components/reports/SdkAuditReport.tsx +72 -0
  16. package/templates/full/dashboard/src/components/reports/index.ts +2 -0
  17. package/templates/full/dashboard/src/components/search/SearchModal.tsx +108 -0
  18. package/templates/full/dashboard/src/pages/api/notifications/[id]/read.ts +37 -0
  19. package/templates/full/dashboard/src/pages/api/notifications/index.ts +47 -0
  20. package/templates/full/dashboard/src/pages/api/notifications/read-all.ts +28 -0
  21. package/templates/full/dashboard/src/pages/api/notifications/unread-count.ts +31 -0
  22. package/templates/full/dashboard/src/pages/api/patterns/approve.ts +55 -0
  23. package/templates/full/dashboard/src/pages/api/patterns/cache-refresh.ts +38 -0
  24. package/templates/full/dashboard/src/pages/api/patterns/discover.ts +36 -0
  25. package/templates/full/dashboard/src/pages/api/patterns/index.ts +36 -0
  26. package/templates/full/dashboard/src/pages/api/patterns/ready-for-review.ts +39 -0
  27. package/templates/full/dashboard/src/pages/api/patterns/reject.ts +54 -0
  28. package/templates/full/dashboard/src/pages/api/patterns/stats.ts +39 -0
  29. package/templates/full/dashboard/src/pages/api/patterns/suggestions.ts +43 -0
  30. package/templates/full/dashboard/src/pages/api/reports/audit.ts +45 -0
  31. package/templates/full/dashboard/src/pages/api/reports/usage.ts +52 -0
  32. package/templates/full/dashboard/src/pages/api/search/index.ts +74 -0
  33. package/templates/full/dashboard/src/pages/api/search/reindex.ts +28 -0
  34. package/templates/full/dashboard/src/pages/api/search/stats.ts +27 -0
  35. package/templates/full/dashboard/src/pages/api/settings/index.ts +37 -0
  36. package/templates/full/dashboard/src/pages/api/settings/update.ts +41 -0
  37. package/templates/full/dashboard/src/pages/api/topology/index.ts +56 -0
  38. package/templates/full/dashboard/src/pages/notifications.astro +11 -0
  39. package/templates/full/migrations/008_auditor.sql +99 -0
  40. package/templates/full/migrations/010_pricing_versions.sql +110 -0
  41. package/templates/full/migrations/011_multi_account.sql +51 -0
  42. package/templates/full/scripts/ops/set-kv-pricing.ts +182 -0
  43. package/templates/full/scripts/ops/universal-backfill.ts +147 -0
  44. package/templates/full/workers/lib/ai-judge-schema.ts +181 -0
  45. package/templates/full/workers/lib/auditor/comprehensive-report.ts +407 -0
  46. package/templates/full/workers/lib/auditor/feature-coverage.ts +348 -0
  47. package/templates/full/workers/lib/auditor/index.ts +9 -0
  48. package/templates/full/workers/lib/auditor/types.ts +167 -0
  49. package/templates/full/workers/platform-auditor.ts +1071 -0
  50. package/templates/full/wrangler.auditor.jsonc.hbs +75 -0
  51. package/templates/shared/.github/workflows/contract-check.yml.hbs +42 -0
  52. package/templates/shared/.github/workflows/dashboard-deploy.yml.hbs +39 -0
  53. package/templates/shared/.github/workflows/platform-check.yml.hbs +28 -0
  54. package/templates/shared/.github/workflows/security.yml +33 -0
  55. package/templates/shared/config/observability.yaml.hbs +276 -0
  56. package/templates/shared/contracts/schemas/envelope.v1.schema.json +64 -0
  57. package/templates/shared/contracts/schemas/error_report.v1.schema.json +65 -0
  58. package/templates/shared/contracts/types/telemetry-envelope.types.ts +139 -0
  59. package/templates/shared/dashboard/astro.config.mjs +21 -0
  60. package/templates/shared/dashboard/package.json.hbs +29 -0
  61. package/templates/shared/dashboard/src/components/Header.astro +29 -0
  62. package/templates/shared/dashboard/src/components/Nav.astro.hbs +59 -0
  63. package/templates/shared/dashboard/src/components/infrastructure/AlertHistory.tsx +57 -0
  64. package/templates/shared/dashboard/src/components/infrastructure/InfrastructureStats.tsx +73 -0
  65. package/templates/shared/dashboard/src/components/infrastructure/ServiceRegistry.tsx +55 -0
  66. package/templates/shared/dashboard/src/components/infrastructure/UptimeStatus.tsx +56 -0
  67. package/templates/shared/dashboard/src/components/infrastructure/index.ts +4 -0
  68. package/templates/shared/dashboard/src/components/overview/ActivityFeed.tsx +134 -0
  69. package/templates/shared/dashboard/src/components/overview/CostQuadrant.tsx +131 -0
  70. package/templates/shared/dashboard/src/components/overview/ErrorsQuadrant.tsx +113 -0
  71. package/templates/shared/dashboard/src/components/overview/HealthQuadrant.tsx +87 -0
  72. package/templates/shared/dashboard/src/components/overview/MissionControl.tsx +139 -0
  73. package/templates/shared/dashboard/src/components/resources/AllowanceStatus.tsx +44 -0
  74. package/templates/shared/dashboard/src/components/resources/CostCentreOverview.tsx +42 -0
  75. package/templates/shared/dashboard/src/components/resources/ResourceTabs.tsx +69 -0
  76. package/templates/shared/dashboard/src/components/resources/index.ts +3 -0
  77. package/templates/shared/dashboard/src/components/settings/SettingsCard.tsx +21 -0
  78. package/templates/shared/dashboard/src/components/settings/index.ts +1 -0
  79. package/templates/shared/dashboard/src/components/ui/AlertBanner.tsx +39 -0
  80. package/templates/shared/dashboard/src/components/ui/Breadcrumbs.tsx +27 -0
  81. package/templates/shared/dashboard/src/components/ui/EmptyState.tsx +26 -0
  82. package/templates/shared/dashboard/src/components/ui/ErrorBoundary.tsx +42 -0
  83. package/templates/shared/dashboard/src/components/ui/LoadingSkeleton.tsx +18 -0
  84. package/templates/shared/dashboard/src/components/ui/PageShell.tsx +26 -0
  85. package/templates/shared/dashboard/src/components/ui/Sparkline.tsx +127 -0
  86. package/templates/shared/dashboard/src/components/ui/StatusDot.tsx +21 -0
  87. package/templates/shared/dashboard/src/components/ui/Toast.tsx +44 -0
  88. package/templates/shared/dashboard/src/components/ui/index.ts +9 -0
  89. package/templates/shared/dashboard/src/components/usage/AnomaliesWidget.tsx +68 -0
  90. package/templates/shared/dashboard/src/components/usage/HourlyUsageChart.tsx +55 -0
  91. package/templates/shared/dashboard/src/components/usage/PlanAllowanceDashboard.tsx +67 -0
  92. package/templates/shared/dashboard/src/components/usage/ProjectCostBreakdown.tsx +55 -0
  93. package/templates/shared/dashboard/src/components/usage/index.ts +4 -0
  94. package/templates/shared/dashboard/src/env.d.ts.hbs +34 -0
  95. package/templates/shared/dashboard/src/layouts/DashboardLayout.astro +37 -0
  96. package/templates/shared/dashboard/src/lib/cloudflare/costs.ts +21 -0
  97. package/templates/shared/dashboard/src/lib/fetch.ts +29 -0
  98. package/templates/shared/dashboard/src/lib/types.ts +72 -0
  99. package/templates/shared/dashboard/src/middleware/auth.ts +100 -0
  100. package/templates/shared/dashboard/src/middleware/index.ts +1 -0
  101. package/templates/shared/dashboard/src/pages/api/costs/overview.ts +65 -0
  102. package/templates/shared/dashboard/src/pages/api/costs/providers.ts +47 -0
  103. package/templates/shared/dashboard/src/pages/api/infrastructure/services.ts +55 -0
  104. package/templates/shared/dashboard/src/pages/api/infrastructure/stats.ts +99 -0
  105. package/templates/shared/dashboard/src/pages/api/overview/summary.ts +311 -0
  106. package/templates/shared/dashboard/src/pages/api/usage/allowances.ts +56 -0
  107. package/templates/shared/dashboard/src/pages/api/usage/anomalies.ts +45 -0
  108. package/templates/shared/dashboard/src/pages/api/usage/billing.ts +53 -0
  109. package/templates/shared/dashboard/src/pages/api/usage/circuit-breakers.ts +44 -0
  110. package/templates/shared/dashboard/src/pages/api/usage/granular.ts +50 -0
  111. package/templates/shared/dashboard/src/pages/api/usage/hourly.ts +45 -0
  112. package/templates/shared/dashboard/src/pages/api/usage/projects.ts +51 -0
  113. package/templates/shared/dashboard/src/pages/api/usage/status.ts +42 -0
  114. package/templates/shared/dashboard/src/pages/api/user/identity.ts +11 -0
  115. package/templates/shared/dashboard/src/pages/dashboard.astro +11 -0
  116. package/templates/shared/dashboard/src/pages/index.astro +3 -0
  117. package/templates/shared/dashboard/src/pages/resources.astro +11 -0
  118. package/templates/shared/dashboard/src/pages/settings/index.astro +28 -0
  119. package/templates/shared/dashboard/src/pages/settings/notifications.astro +34 -0
  120. package/templates/shared/dashboard/src/pages/settings/thresholds.astro +39 -0
  121. package/templates/shared/dashboard/src/pages/settings/usage.astro +28 -0
  122. package/templates/shared/dashboard/src/styles/global.css +29 -0
  123. package/templates/shared/dashboard/tailwind.config.mjs +9 -0
  124. package/templates/shared/dashboard/tsconfig.json +9 -0
  125. package/templates/shared/dashboard/wrangler.json.hbs +47 -0
  126. package/templates/shared/docs/architecture.md +89 -0
  127. package/templates/shared/docs/post-deploy-runbook.md +126 -0
  128. package/templates/shared/docs/troubleshooting.md +91 -0
  129. package/templates/shared/package.json.hbs +17 -1
  130. package/templates/shared/scripts/ops/backfill-cloudflare-daily.ts +145 -0
  131. package/templates/shared/scripts/ops/backfill-cloudflare-hourly.ts +473 -0
  132. package/templates/shared/scripts/ops/backfill-monthly-rollups.ts +125 -0
  133. package/templates/shared/scripts/ops/discover-graphql-datasets.ts +482 -0
  134. package/templates/shared/scripts/ops/reset-budget-state.ts +279 -0
  135. package/templates/shared/scripts/ops/validate-controls.js +141 -0
  136. package/templates/shared/scripts/ops/validate-pipeline.ts +237 -0
  137. package/templates/shared/scripts/ops/verify-account-completeness.ts +236 -0
  138. package/templates/shared/scripts/validate-schemas.js +61 -0
  139. package/templates/shared/tests/contract/validate-schemas.test.ts +130 -0
  140. package/templates/shared/tests/fixtures/telemetry-envelope-invalid.json +9 -0
  141. package/templates/shared/tests/fixtures/telemetry-envelope-valid.json +27 -0
  142. package/templates/shared/tests/helpers/mock-d1.ts +61 -0
  143. package/templates/shared/tests/helpers/mock-kv.ts +37 -0
  144. package/templates/shared/tests/unit/workers/batch-persistence.test.ts +133 -0
  145. package/templates/shared/tests/unit/workers/budget-enforcement.test.ts +214 -0
  146. package/templates/shared/vitest.config.ts +18 -0
  147. package/templates/shared/workers/lib/usage/collectors/anthropic.ts +114 -0
  148. package/templates/shared/workers/lib/usage/collectors/apify.ts +96 -0
  149. package/templates/shared/workers/lib/usage/collectors/custom-http.ts +151 -0
  150. package/templates/shared/workers/lib/usage/collectors/deepseek.ts +92 -0
  151. package/templates/shared/workers/lib/usage/collectors/gemini.ts +263 -0
  152. package/templates/shared/workers/lib/usage/collectors/github.ts +362 -0
  153. package/templates/shared/workers/lib/usage/collectors/index.ts +31 -15
  154. package/templates/shared/workers/lib/usage/collectors/minimax.ts +106 -0
  155. package/templates/shared/workers/lib/usage/collectors/openai.ts +171 -0
  156. package/templates/shared/workers/lib/usage/collectors/resend.ts +79 -0
  157. package/templates/shared/workers/lib/usage/collectors/stripe.ts +192 -0
  158. package/templates/shared/workers/lib/usage/shared/types.ts +46 -0
  159. package/templates/shared/workers/platform-usage.ts +98 -8
  160. package/templates/standard/dashboard/src/components/errors/ErrorStats.tsx +53 -0
  161. package/templates/standard/dashboard/src/components/errors/ErrorsTable.tsx +133 -0
  162. package/templates/standard/dashboard/src/components/errors/index.ts +2 -0
  163. package/templates/standard/dashboard/src/components/health/CircuitBreakerEvents.tsx +69 -0
  164. package/templates/standard/dashboard/src/components/health/CircuitBreakerPanel.tsx +97 -0
  165. package/templates/standard/dashboard/src/components/health/DlqStatusCard.tsx +52 -0
  166. package/templates/standard/dashboard/src/components/health/HealthTabs.tsx +86 -0
  167. package/templates/standard/dashboard/src/components/health/index.ts +4 -0
  168. package/templates/standard/dashboard/src/lib/errors.ts +28 -0
  169. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/mute.ts +49 -0
  170. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/resolve.ts +36 -0
  171. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint].ts +55 -0
  172. package/templates/standard/dashboard/src/pages/api/errors/index.ts +58 -0
  173. package/templates/standard/dashboard/src/pages/api/errors/stats.ts +55 -0
  174. package/templates/standard/dashboard/src/pages/api/health/audit-history.ts +37 -0
  175. package/templates/standard/dashboard/src/pages/api/health/dlq.ts +43 -0
  176. package/templates/standard/dashboard/src/pages/circuit-breakers.astro +13 -0
  177. package/templates/standard/dashboard/src/pages/errors.astro +13 -0
  178. package/templates/standard/dashboard/src/pages/health.astro +11 -0
  179. package/templates/standard/migrations/009_topology_mapper.sql +65 -0
  180. package/templates/standard/tests/unit/error-collector/capture.test.ts +106 -0
  181. package/templates/standard/tests/unit/error-collector/fingerprint.test.ts +155 -0
  182. package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +37 -3
  183. package/templates/standard/workers/lib/error-collector/gap-alerts.ts +32 -1
  184. package/templates/standard/workers/lib/mapper/attribution-check.ts +339 -0
  185. package/templates/standard/workers/lib/mapper/index.ts +7 -0
  186. package/templates/standard/workers/platform-mapper.ts +482 -0
  187. package/templates/standard/workers/platform-sdk-test-client.ts +125 -0
  188. package/templates/standard/wrangler.mapper.jsonc.hbs +85 -0
  189. package/templates/standard/wrangler.sdk-test-client.jsonc.hbs +62 -0
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * Monthly Rollup Backfill Script
4
+ *
5
+ * Aggregates daily_usage_rollups into monthly_usage_rollups via the D1 REST API.
6
+ * Queries existing daily data and rolls it up to monthly granularity.
7
+ *
8
+ * Prerequisites:
9
+ * CLOUDFLARE_API_TOKEN — API token with D1:Write permissions
10
+ * CLOUDFLARE_ACCOUNT_ID — Your Cloudflare account ID
11
+ * D1_DATABASE_ID — Your platform-metrics D1 database ID
12
+ *
13
+ * Usage:
14
+ * npx tsx scripts/ops/backfill-monthly-rollups.ts
15
+ * npx tsx scripts/ops/backfill-monthly-rollups.ts --dry-run
16
+ * npx tsx scripts/ops/backfill-monthly-rollups.ts --start 2026-01 --end 2026-03
17
+ * npx tsx scripts/ops/backfill-monthly-rollups.ts --limit 12
18
+ */
19
+
20
+ const REST_API_BASE = 'https://api.cloudflare.com/client/v4';
21
+
22
+ interface Args {
23
+ start?: string;
24
+ end?: string;
25
+ dryRun: boolean;
26
+ limit: number;
27
+ }
28
+
29
+ function parseArgs(): Args {
30
+ const args = process.argv.slice(2);
31
+ const result: Args = { dryRun: false, limit: 12 };
32
+
33
+ for (let i = 0; i < args.length; i++) {
34
+ if (args[i] === '--start' && args[i + 1]) result.start = args[++i];
35
+ else if (args[i] === '--end' && args[i + 1]) result.end = args[++i];
36
+ else if (args[i] === '--limit' && args[i + 1]) result.limit = Number(args[++i]);
37
+ else if (args[i] === '--dry-run') result.dryRun = true;
38
+ }
39
+
40
+ if (!result.start) {
41
+ const d = new Date();
42
+ d.setMonth(d.getMonth() - 6);
43
+ result.start = d.toISOString().slice(0, 7);
44
+ }
45
+ if (!result.end) {
46
+ result.end = new Date().toISOString().slice(0, 7);
47
+ }
48
+
49
+ return result;
50
+ }
51
+
52
+ function getEnvOrThrow(key: string): string {
53
+ const val = process.env[key];
54
+ if (!val) throw new Error(`Missing required env var: ${key}`);
55
+ return val;
56
+ }
57
+
58
+ async function d1Query(accountId: string, dbId: string, token: string, sql: string, params: unknown[] = []) {
59
+ const res = await fetch(`${REST_API_BASE}/accounts/${accountId}/d1/database/${dbId}/query`, {
60
+ method: 'POST',
61
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
62
+ body: JSON.stringify({ sql, params }),
63
+ });
64
+ if (!res.ok) {
65
+ const text = await res.text();
66
+ throw new Error(`D1 query failed (${res.status}): ${text}`);
67
+ }
68
+ return res.json() as Promise<{ result: Array<{ results: unknown[] }> }>;
69
+ }
70
+
71
+ async function main() {
72
+ const args = parseArgs();
73
+ const token = getEnvOrThrow('CLOUDFLARE_API_TOKEN');
74
+ const accountId = getEnvOrThrow('CLOUDFLARE_ACCOUNT_ID');
75
+ const dbId = getEnvOrThrow('D1_DATABASE_ID');
76
+
77
+ console.log(`Backfilling monthly rollups: ${args.start} → ${args.end} (limit: ${args.limit}, dry-run: ${args.dryRun})`);
78
+
79
+ // Generate month list
80
+ const months: string[] = [];
81
+ const current = new Date(args.start + '-01');
82
+ const endDate = new Date(args.end + '-01');
83
+ while (current <= endDate && months.length < args.limit) {
84
+ months.push(current.toISOString().slice(0, 7));
85
+ current.setMonth(current.getMonth() + 1);
86
+ }
87
+
88
+ let inserted = 0;
89
+ for (const month of months) {
90
+ const monthStart = `${month}-01`;
91
+ const nextMonth = new Date(monthStart);
92
+ nextMonth.setMonth(nextMonth.getMonth() + 1);
93
+ const monthEnd = nextMonth.toISOString().slice(0, 10);
94
+
95
+ if (args.dryRun) {
96
+ console.log(`[DRY-RUN] Would roll up ${month}`);
97
+ continue;
98
+ }
99
+
100
+ await d1Query(accountId, dbId, token, `
101
+ INSERT OR REPLACE INTO monthly_usage_rollups (
102
+ project, snapshot_month,
103
+ d1_reads, d1_writes, kv_reads, kv_writes,
104
+ r2_reads, r2_writes, worker_requests, total_cost_usd
105
+ )
106
+ SELECT
107
+ project, ? as snapshot_month,
108
+ SUM(d1_reads), SUM(d1_writes), SUM(kv_reads), SUM(kv_writes),
109
+ SUM(r2_reads), SUM(r2_writes), SUM(worker_requests), SUM(total_cost_usd)
110
+ FROM daily_usage_rollups
111
+ WHERE project = 'all' AND snapshot_date >= ? AND snapshot_date < ?
112
+ GROUP BY project
113
+ `, [month, monthStart, monthEnd]);
114
+
115
+ inserted++;
116
+ console.log(` Rolled up ${month} (${inserted}/${months.length})`);
117
+ }
118
+
119
+ console.log(`Done. Inserted ${inserted} monthly rollup rows.`);
120
+ }
121
+
122
+ main().catch((err) => {
123
+ console.error('Fatal error:', err);
124
+ process.exit(1);
125
+ });
@@ -0,0 +1,482 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * Cloudflare GraphQL Dataset Discovery Script
4
+ *
5
+ * Discovers all available GraphQL datasets for your Cloudflare account using
6
+ * the Settings node. Helps identify datasets you're not currently querying.
7
+ *
8
+ * The Cloudflare GraphQL API exposes 70+ datasets. This script:
9
+ * 1. Queries the account Settings node to list ALL available datasets
10
+ * 2. Shows which datasets are enabled for your account
11
+ * 3. Compares against your current implementation to find gaps
12
+ *
13
+ * Prerequisites:
14
+ * - CLOUDFLARE_API_TOKEN env var with Analytics read permissions
15
+ * - CLOUDFLARE_ACCOUNT_ID env var
16
+ *
17
+ * Usage:
18
+ * npx tsx scripts/ops/discover-graphql-datasets.ts
19
+ * npx tsx scripts/ops/discover-graphql-datasets.ts --json # JSON output
20
+ * npx tsx scripts/ops/discover-graphql-datasets.ts --compare # Compare vs current
21
+ */
22
+
23
+ const GRAPHQL_ENDPOINT = 'https://api.cloudflare.com/client/v4/graphql';
24
+
25
+ // TODO: Add the datasets your platform-usage worker currently queries.
26
+ // These are the datasets collected by the default data-collection.ts handler.
27
+ // Uncomment/add entries as you enable collection for each service.
28
+ const CURRENTLY_QUERIED_DATASETS = [
29
+ 'workersInvocationsAdaptive',
30
+ // 'workersSubrequestsAdaptive',
31
+ 'd1AnalyticsAdaptiveGroups',
32
+ 'd1StorageAdaptiveGroups',
33
+ 'kvOperationsAdaptiveGroups',
34
+ 'kvStorageAdaptiveGroups',
35
+ 'r2OperationsAdaptiveGroups',
36
+ 'r2StorageAdaptiveGroups',
37
+ // 'durableObjectsInvocationsAdaptiveGroups',
38
+ // 'durableObjectsPeriodicGroups',
39
+ // 'durableObjectsStorageGroups',
40
+ // 'vectorizeV2QueriesAdaptiveGroups',
41
+ // 'vectorizeV2StorageAdaptiveGroups',
42
+ 'queueMessageOperationsAdaptiveGroups',
43
+ 'queueConsumerMetricsAdaptiveGroups',
44
+ // 'workflowsAdaptiveGroups',
45
+ // 'aiInferenceAdaptive',
46
+ // 'httpRequestsAdaptiveGroups',
47
+ ];
48
+
49
+ // Known billable datasets — these have cost implications if used
50
+ const BILLABLE_DATASETS = [
51
+ // Workers Platform
52
+ 'workersInvocationsAdaptive',
53
+ 'workersSubrequestsAdaptive',
54
+
55
+ // D1
56
+ 'd1AnalyticsAdaptiveGroups',
57
+ 'd1StorageAdaptiveGroups',
58
+
59
+ // KV
60
+ 'kvOperationsAdaptiveGroups',
61
+ 'kvStorageAdaptiveGroups',
62
+
63
+ // R2
64
+ 'r2OperationsAdaptiveGroups',
65
+ 'r2StorageAdaptiveGroups',
66
+
67
+ // Durable Objects
68
+ 'durableObjectsInvocationsAdaptiveGroups',
69
+ 'durableObjectsPeriodicGroups',
70
+ 'durableObjectsStorageGroups',
71
+
72
+ // Vectorize
73
+ 'vectorizeV2QueriesAdaptiveGroups',
74
+ 'vectorizeV2StorageAdaptiveGroups',
75
+
76
+ // Queues
77
+ 'queueMessageOperationsAdaptiveGroups',
78
+ 'queueConsumerMetricsAdaptiveGroups',
79
+
80
+ // Workflows
81
+ 'workflowsAdaptiveGroups',
82
+
83
+ // Workers AI
84
+ 'aiInferenceAdaptive',
85
+
86
+ // Stream (video)
87
+ 'streamMinutesViewedAdaptive',
88
+ 'streamMinutesViewedAdaptiveGroups',
89
+
90
+ // Images
91
+ 'imagesAdaptiveGroups',
92
+
93
+ // Pages
94
+ 'pagesProjectsAdaptiveGroups',
95
+
96
+ // AI Gateway
97
+ 'aiGatewayAdaptiveGroups',
98
+
99
+ // Hyperdrive
100
+ 'hyperdriveAdaptiveGroups',
101
+
102
+ // Browser Rendering
103
+ 'browserRenderingAdaptiveGroups',
104
+
105
+ // TURN/Calls
106
+ 'callsTurnUsageAdaptiveGroups',
107
+
108
+ // Spectrum
109
+ 'spectrumNetworkAnalyticsAdaptiveGroups',
110
+
111
+ // Email Routing
112
+ 'emailRoutingAdaptiveGroups',
113
+
114
+ // Cache/CDN
115
+ 'httpRequestsAdaptiveGroups',
116
+ ];
117
+
118
+ interface DatasetInfo {
119
+ name: string;
120
+ enabled: boolean;
121
+ maxPageSize?: number;
122
+ }
123
+
124
+ interface DiscoveryResult {
125
+ timestamp: string;
126
+ accountId: string;
127
+ totalDatasetsFound: number;
128
+ enabledDatasets: DatasetInfo[];
129
+ disabledDatasets: string[];
130
+ currentlyQueried: string[];
131
+ missingBillable: string[];
132
+ notQueried: string[];
133
+ }
134
+
135
+ // Parse command line arguments
136
+ function parseArgs(): { json: boolean; compare: boolean } {
137
+ const args = process.argv.slice(2);
138
+ return {
139
+ json: args.includes('--json'),
140
+ compare: args.includes('--compare'),
141
+ };
142
+ }
143
+
144
+ // Make GraphQL request
145
+ async function graphqlRequest<T>(
146
+ query: string,
147
+ variables: Record<string, unknown> = {}
148
+ ): Promise<T> {
149
+ const token = process.env.CLOUDFLARE_API_TOKEN;
150
+ if (!token) {
151
+ throw new Error('CLOUDFLARE_API_TOKEN environment variable is required');
152
+ }
153
+
154
+ const response = await fetch(GRAPHQL_ENDPOINT, {
155
+ method: 'POST',
156
+ headers: {
157
+ 'Content-Type': 'application/json',
158
+ Authorization: `Bearer ${token}`,
159
+ },
160
+ body: JSON.stringify({ query, variables }),
161
+ });
162
+
163
+ if (!response.ok) {
164
+ const text = await response.text();
165
+ throw new Error(`GraphQL request failed: ${response.status} ${text}`);
166
+ }
167
+
168
+ const result = (await response.json()) as { data?: T; errors?: unknown[] };
169
+
170
+ if (result.errors) {
171
+ throw new Error(`GraphQL errors: ${JSON.stringify(result.errors, null, 2)}`);
172
+ }
173
+
174
+ return result.data as T;
175
+ }
176
+
177
+ // Query via GraphQL introspection to discover available datasets
178
+ async function discoverAccountDatasets(accountId: string): Promise<DatasetInfo[]> {
179
+ const introspectionQuery = `
180
+ query IntrospectAccountFields {
181
+ __type(name: "AccountResolver") {
182
+ fields {
183
+ name
184
+ description
185
+ type {
186
+ name
187
+ kind
188
+ }
189
+ }
190
+ }
191
+ }
192
+ `;
193
+
194
+ try {
195
+ const introspectionResult = await graphqlRequest<{
196
+ __type: {
197
+ fields: Array<{
198
+ name: string;
199
+ description: string | null;
200
+ type: { name: string | null; kind: string };
201
+ }>;
202
+ } | null;
203
+ }>(introspectionQuery);
204
+
205
+ if (!introspectionResult.__type) {
206
+ console.log('Could not introspect AccountResolver, trying settings approach...');
207
+ return await discoverDatasetsViaSettings(accountId);
208
+ }
209
+
210
+ const datasets: DatasetInfo[] = [];
211
+ for (const field of introspectionResult.__type.fields) {
212
+ if (
213
+ field.name.includes('Adaptive') ||
214
+ field.name.includes('Analytics') ||
215
+ field.name.includes('Groups') ||
216
+ field.name.includes('Metrics')
217
+ ) {
218
+ datasets.push({ name: field.name, enabled: true });
219
+ }
220
+ }
221
+
222
+ return datasets;
223
+ } catch {
224
+ console.log('Introspection failed, trying Settings node approach...');
225
+ return await discoverDatasetsViaSettings(accountId);
226
+ }
227
+ }
228
+
229
+ // Alternative: Query settings node for dataset availability
230
+ async function discoverDatasetsViaSettings(accountId: string): Promise<DatasetInfo[]> {
231
+ const settingsQuery = `
232
+ query AccountDatasetSettings($accountTag: String!) {
233
+ viewer {
234
+ accounts(filter: { accountTag: $accountTag }) {
235
+ settings {
236
+ workersInvocationsAdaptive { enabled maxPageSize }
237
+ d1AnalyticsAdaptiveGroups { enabled maxPageSize }
238
+ d1StorageAdaptiveGroups { enabled maxPageSize }
239
+ kvOperationsAdaptiveGroups { enabled maxPageSize }
240
+ kvStorageAdaptiveGroups { enabled maxPageSize }
241
+ r2OperationsAdaptiveGroups { enabled maxPageSize }
242
+ r2StorageAdaptiveGroups { enabled maxPageSize }
243
+ durableObjectsInvocationsAdaptiveGroups { enabled maxPageSize }
244
+ durableObjectsPeriodicGroups { enabled maxPageSize }
245
+ durableObjectsStorageGroups { enabled maxPageSize }
246
+ vectorizeV2QueriesAdaptiveGroups { enabled maxPageSize }
247
+ vectorizeV2StorageAdaptiveGroups { enabled maxPageSize }
248
+ queueMessageOperationsAdaptiveGroups { enabled maxPageSize }
249
+ queueConsumerMetricsAdaptiveGroups { enabled maxPageSize }
250
+ workflowsAdaptiveGroups { enabled maxPageSize }
251
+ aiInferenceAdaptive { enabled maxPageSize }
252
+ httpRequestsAdaptiveGroups { enabled maxPageSize }
253
+ }
254
+ }
255
+ }
256
+ }
257
+ `;
258
+
259
+ try {
260
+ const settingsResult = await graphqlRequest<{
261
+ viewer: {
262
+ accounts: Array<{
263
+ settings: Record<string, { enabled: boolean; maxPageSize: number } | null>;
264
+ }>;
265
+ };
266
+ }>(settingsQuery, { accountTag: accountId });
267
+
268
+ const settings = settingsResult.viewer?.accounts?.[0]?.settings;
269
+ if (!settings) {
270
+ console.log('No settings returned, falling back to probe approach...');
271
+ return await probeDatasets(accountId);
272
+ }
273
+
274
+ const datasets: DatasetInfo[] = [];
275
+ for (const [name, info] of Object.entries(settings)) {
276
+ if (info) {
277
+ datasets.push({ name, enabled: info.enabled, maxPageSize: info.maxPageSize });
278
+ }
279
+ }
280
+
281
+ return datasets;
282
+ } catch (error) {
283
+ console.log('Settings query failed, falling back to probe approach...');
284
+ console.log('Error:', error);
285
+ return await probeDatasets(accountId);
286
+ }
287
+ }
288
+
289
+ // Fallback: Probe individual datasets to check availability
290
+ async function probeDatasets(accountId: string): Promise<DatasetInfo[]> {
291
+ const datasetsToProbe = BILLABLE_DATASETS;
292
+ const results: DatasetInfo[] = [];
293
+ const now = new Date();
294
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
295
+
296
+ console.log(`Probing ${datasetsToProbe.length} datasets...`);
297
+
298
+ for (const dataset of datasetsToProbe) {
299
+ const probeQuery = `
300
+ query ProbeDataset($accountTag: String!, $limit: Int!) {
301
+ viewer {
302
+ accounts(filter: { accountTag: $accountTag }) {
303
+ ${dataset}(limit: $limit, filter: { datetime_geq: "${yesterday.toISOString()}", datetime_leq: "${now.toISOString()}" }) {
304
+ dimensions {
305
+ datetime
306
+ }
307
+ }
308
+ }
309
+ }
310
+ }
311
+ `;
312
+
313
+ try {
314
+ await graphqlRequest(probeQuery, { accountTag: accountId, limit: 1 });
315
+ results.push({ name: dataset, enabled: true });
316
+ process.stdout.write('.');
317
+ } catch (error) {
318
+ const errorStr = String(error);
319
+ if (errorStr.includes('Cannot query field') || errorStr.includes('Unknown field')) {
320
+ results.push({ name: dataset, enabled: false });
321
+ process.stdout.write('x');
322
+ } else if (errorStr.includes('not enabled') || errorStr.includes('not available')) {
323
+ results.push({ name: dataset, enabled: false });
324
+ process.stdout.write('-');
325
+ } else {
326
+ results.push({ name: dataset, enabled: true });
327
+ process.stdout.write('?');
328
+ }
329
+ }
330
+
331
+ // Small delay between probes to avoid rate limiting
332
+ await new Promise((resolve) => setTimeout(resolve, 100));
333
+ }
334
+
335
+ console.log('');
336
+ return results;
337
+ }
338
+
339
+ // Main discovery function
340
+ async function discoverDatasets(): Promise<DiscoveryResult> {
341
+ const accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
342
+ if (!accountId) {
343
+ throw new Error('CLOUDFLARE_ACCOUNT_ID environment variable is required');
344
+ }
345
+
346
+ console.log('='.repeat(60));
347
+ console.log('Cloudflare GraphQL Dataset Discovery');
348
+ console.log('='.repeat(60));
349
+ console.log(`Account ID: ${accountId}`);
350
+ console.log(`Timestamp: ${new Date().toISOString()}`);
351
+ console.log('');
352
+
353
+ console.log('Discovering available datasets...');
354
+ const datasets = await discoverAccountDatasets(accountId);
355
+
356
+ const enabledDatasets = datasets.filter((d) => d.enabled);
357
+ const disabledDatasets = datasets.filter((d) => !d.enabled).map((d) => d.name);
358
+
359
+ const enabledNames = new Set(enabledDatasets.map((d) => d.name));
360
+ const queriedSet = new Set(CURRENTLY_QUERIED_DATASETS);
361
+
362
+ const missingBillable = BILLABLE_DATASETS.filter(
363
+ (d) => enabledNames.has(d) && !queriedSet.has(d)
364
+ );
365
+
366
+ const notQueried = enabledDatasets.filter((d) => !queriedSet.has(d.name)).map((d) => d.name);
367
+
368
+ return {
369
+ timestamp: new Date().toISOString(),
370
+ accountId,
371
+ totalDatasetsFound: datasets.length,
372
+ enabledDatasets,
373
+ disabledDatasets,
374
+ currentlyQueried: CURRENTLY_QUERIED_DATASETS,
375
+ missingBillable,
376
+ notQueried,
377
+ };
378
+ }
379
+
380
+ // Display results
381
+ function displayResults(result: DiscoveryResult, jsonOutput: boolean, compareMode: boolean): void {
382
+ if (jsonOutput) {
383
+ console.log(JSON.stringify(result, null, 2));
384
+ return;
385
+ }
386
+
387
+ console.log('');
388
+ console.log('='.repeat(60));
389
+ console.log('DISCOVERY RESULTS');
390
+ console.log('='.repeat(60));
391
+
392
+ console.log(`\nTotal Datasets Found: ${result.totalDatasetsFound}`);
393
+ console.log(`Enabled: ${result.enabledDatasets.length}`);
394
+ console.log(`Disabled/Unavailable: ${result.disabledDatasets.length}`);
395
+
396
+ console.log('\n--- ENABLED DATASETS ---');
397
+ for (const dataset of result.enabledDatasets) {
398
+ const queried = CURRENTLY_QUERIED_DATASETS.includes(dataset.name)
399
+ ? '[QUERIED]'
400
+ : '[NOT QUERIED]';
401
+ const billable = BILLABLE_DATASETS.includes(dataset.name) ? '(billable)' : '';
402
+ console.log(` ${queried} ${dataset.name} ${billable}`);
403
+ }
404
+
405
+ if (result.disabledDatasets.length > 0) {
406
+ console.log('\n--- DISABLED/UNAVAILABLE DATASETS ---');
407
+ for (const name of result.disabledDatasets) {
408
+ console.log(` [-] ${name}`);
409
+ }
410
+ }
411
+
412
+ if (compareMode || result.missingBillable.length > 0) {
413
+ console.log('\n' + '='.repeat(60));
414
+ console.log('GAP ANALYSIS');
415
+ console.log('='.repeat(60));
416
+
417
+ if (result.missingBillable.length > 0) {
418
+ console.log('\n*** BILLABLE DATASETS NOT BEING QUERIED ***');
419
+ for (const name of result.missingBillable) {
420
+ console.log(` [!] ${name}`);
421
+ }
422
+ } else {
423
+ console.log('\nNo billable dataset gaps found.');
424
+ }
425
+
426
+ console.log(`\n--- Currently Queried (${CURRENTLY_QUERIED_DATASETS.length}) ---`);
427
+ for (const name of CURRENTLY_QUERIED_DATASETS) {
428
+ const enabled = result.enabledDatasets.some((d) => d.name === name);
429
+ console.log(` ${enabled ? '[OK]' : '[??]'} ${name}`);
430
+ }
431
+ }
432
+
433
+ console.log('\n' + '='.repeat(60));
434
+ console.log('RECOMMENDATIONS');
435
+ console.log('='.repeat(60));
436
+
437
+ if (result.missingBillable.length > 0) {
438
+ console.log('\nAction Required: Add queries for these billable datasets:');
439
+ for (const name of result.missingBillable) {
440
+ console.log(` - ${name}`);
441
+ }
442
+ }
443
+
444
+ const unusedEnabled = result.notQueried.filter(
445
+ (name) =>
446
+ !name.includes('firewall') &&
447
+ !name.includes('Firewall') &&
448
+ !name.includes('healthCheck') &&
449
+ !name.includes('loadBalancing') &&
450
+ !name.includes('network')
451
+ );
452
+
453
+ if (unusedEnabled.length > 0) {
454
+ console.log('\nOther enabled datasets not being queried (may be relevant):');
455
+ for (const name of unusedEnabled.slice(0, 10)) {
456
+ console.log(` - ${name}`);
457
+ }
458
+ if (unusedEnabled.length > 10) {
459
+ console.log(` ... and ${unusedEnabled.length - 10} more`);
460
+ }
461
+ }
462
+ }
463
+
464
+ // Main
465
+ async function main(): Promise<void> {
466
+ const { json, compare } = parseArgs();
467
+
468
+ try {
469
+ const result = await discoverDatasets();
470
+ displayResults(result, json, compare);
471
+
472
+ // Exit with non-zero if there are missing billable datasets
473
+ if (result.missingBillable.length > 0) {
474
+ process.exit(1);
475
+ }
476
+ } catch (error) {
477
+ console.error('Error:', error);
478
+ process.exit(2);
479
+ }
480
+ }
481
+
482
+ main();