@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.
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +232 -2
- package/package.json +1 -1
- package/templates/full/config/audit-targets.yaml +72 -0
- package/templates/full/dashboard/src/components/notifications/NotificationBell.tsx +30 -0
- package/templates/full/dashboard/src/components/notifications/NotificationList.tsx +116 -0
- package/templates/full/dashboard/src/components/notifications/index.ts +2 -0
- package/templates/full/dashboard/src/components/patterns/ActivePatterns.tsx +62 -0
- package/templates/full/dashboard/src/components/patterns/PatternStats.tsx +60 -0
- package/templates/full/dashboard/src/components/patterns/PatternTabs.tsx +116 -0
- package/templates/full/dashboard/src/components/patterns/SuggestionsQueue.tsx +115 -0
- package/templates/full/dashboard/src/components/patterns/SystemPatterns.tsx +52 -0
- package/templates/full/dashboard/src/components/patterns/index.ts +5 -0
- package/templates/full/dashboard/src/components/reports/GapDetectionReport.tsx +69 -0
- package/templates/full/dashboard/src/components/reports/SdkAuditReport.tsx +72 -0
- package/templates/full/dashboard/src/components/reports/index.ts +2 -0
- package/templates/full/dashboard/src/components/search/SearchModal.tsx +108 -0
- package/templates/full/dashboard/src/pages/api/notifications/[id]/read.ts +37 -0
- package/templates/full/dashboard/src/pages/api/notifications/index.ts +47 -0
- package/templates/full/dashboard/src/pages/api/notifications/read-all.ts +28 -0
- package/templates/full/dashboard/src/pages/api/notifications/unread-count.ts +31 -0
- package/templates/full/dashboard/src/pages/api/patterns/approve.ts +55 -0
- package/templates/full/dashboard/src/pages/api/patterns/cache-refresh.ts +38 -0
- package/templates/full/dashboard/src/pages/api/patterns/discover.ts +36 -0
- package/templates/full/dashboard/src/pages/api/patterns/index.ts +36 -0
- package/templates/full/dashboard/src/pages/api/patterns/ready-for-review.ts +39 -0
- package/templates/full/dashboard/src/pages/api/patterns/reject.ts +54 -0
- package/templates/full/dashboard/src/pages/api/patterns/stats.ts +39 -0
- package/templates/full/dashboard/src/pages/api/patterns/suggestions.ts +43 -0
- package/templates/full/dashboard/src/pages/api/reports/audit.ts +45 -0
- package/templates/full/dashboard/src/pages/api/reports/usage.ts +52 -0
- package/templates/full/dashboard/src/pages/api/search/index.ts +74 -0
- package/templates/full/dashboard/src/pages/api/search/reindex.ts +28 -0
- package/templates/full/dashboard/src/pages/api/search/stats.ts +27 -0
- package/templates/full/dashboard/src/pages/api/settings/index.ts +37 -0
- package/templates/full/dashboard/src/pages/api/settings/update.ts +41 -0
- package/templates/full/dashboard/src/pages/api/topology/index.ts +56 -0
- package/templates/full/dashboard/src/pages/notifications.astro +11 -0
- package/templates/full/migrations/008_auditor.sql +99 -0
- package/templates/full/migrations/010_pricing_versions.sql +110 -0
- package/templates/full/migrations/011_multi_account.sql +51 -0
- package/templates/full/scripts/ops/set-kv-pricing.ts +182 -0
- package/templates/full/scripts/ops/universal-backfill.ts +147 -0
- package/templates/full/workers/lib/ai-judge-schema.ts +181 -0
- package/templates/full/workers/lib/auditor/comprehensive-report.ts +407 -0
- package/templates/full/workers/lib/auditor/feature-coverage.ts +348 -0
- package/templates/full/workers/lib/auditor/index.ts +9 -0
- package/templates/full/workers/lib/auditor/types.ts +167 -0
- package/templates/full/workers/platform-auditor.ts +1071 -0
- package/templates/full/wrangler.auditor.jsonc.hbs +75 -0
- package/templates/shared/.github/workflows/contract-check.yml.hbs +42 -0
- package/templates/shared/.github/workflows/dashboard-deploy.yml.hbs +39 -0
- package/templates/shared/.github/workflows/platform-check.yml.hbs +28 -0
- package/templates/shared/.github/workflows/security.yml +33 -0
- package/templates/shared/config/observability.yaml.hbs +276 -0
- package/templates/shared/contracts/schemas/envelope.v1.schema.json +64 -0
- package/templates/shared/contracts/schemas/error_report.v1.schema.json +65 -0
- package/templates/shared/contracts/types/telemetry-envelope.types.ts +139 -0
- package/templates/shared/dashboard/astro.config.mjs +21 -0
- package/templates/shared/dashboard/package.json.hbs +29 -0
- package/templates/shared/dashboard/src/components/Header.astro +29 -0
- package/templates/shared/dashboard/src/components/Nav.astro.hbs +59 -0
- package/templates/shared/dashboard/src/components/infrastructure/AlertHistory.tsx +57 -0
- package/templates/shared/dashboard/src/components/infrastructure/InfrastructureStats.tsx +73 -0
- package/templates/shared/dashboard/src/components/infrastructure/ServiceRegistry.tsx +55 -0
- package/templates/shared/dashboard/src/components/infrastructure/UptimeStatus.tsx +56 -0
- package/templates/shared/dashboard/src/components/infrastructure/index.ts +4 -0
- package/templates/shared/dashboard/src/components/overview/ActivityFeed.tsx +134 -0
- package/templates/shared/dashboard/src/components/overview/CostQuadrant.tsx +131 -0
- package/templates/shared/dashboard/src/components/overview/ErrorsQuadrant.tsx +113 -0
- package/templates/shared/dashboard/src/components/overview/HealthQuadrant.tsx +87 -0
- package/templates/shared/dashboard/src/components/overview/MissionControl.tsx +139 -0
- package/templates/shared/dashboard/src/components/resources/AllowanceStatus.tsx +44 -0
- package/templates/shared/dashboard/src/components/resources/CostCentreOverview.tsx +42 -0
- package/templates/shared/dashboard/src/components/resources/ResourceTabs.tsx +69 -0
- package/templates/shared/dashboard/src/components/resources/index.ts +3 -0
- package/templates/shared/dashboard/src/components/settings/SettingsCard.tsx +21 -0
- package/templates/shared/dashboard/src/components/settings/index.ts +1 -0
- package/templates/shared/dashboard/src/components/ui/AlertBanner.tsx +39 -0
- package/templates/shared/dashboard/src/components/ui/Breadcrumbs.tsx +27 -0
- package/templates/shared/dashboard/src/components/ui/EmptyState.tsx +26 -0
- package/templates/shared/dashboard/src/components/ui/ErrorBoundary.tsx +42 -0
- package/templates/shared/dashboard/src/components/ui/LoadingSkeleton.tsx +18 -0
- package/templates/shared/dashboard/src/components/ui/PageShell.tsx +26 -0
- package/templates/shared/dashboard/src/components/ui/Sparkline.tsx +127 -0
- package/templates/shared/dashboard/src/components/ui/StatusDot.tsx +21 -0
- package/templates/shared/dashboard/src/components/ui/Toast.tsx +44 -0
- package/templates/shared/dashboard/src/components/ui/index.ts +9 -0
- package/templates/shared/dashboard/src/components/usage/AnomaliesWidget.tsx +68 -0
- package/templates/shared/dashboard/src/components/usage/HourlyUsageChart.tsx +55 -0
- package/templates/shared/dashboard/src/components/usage/PlanAllowanceDashboard.tsx +67 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCostBreakdown.tsx +55 -0
- package/templates/shared/dashboard/src/components/usage/index.ts +4 -0
- package/templates/shared/dashboard/src/env.d.ts.hbs +34 -0
- package/templates/shared/dashboard/src/layouts/DashboardLayout.astro +37 -0
- package/templates/shared/dashboard/src/lib/cloudflare/costs.ts +21 -0
- package/templates/shared/dashboard/src/lib/fetch.ts +29 -0
- package/templates/shared/dashboard/src/lib/types.ts +72 -0
- package/templates/shared/dashboard/src/middleware/auth.ts +100 -0
- package/templates/shared/dashboard/src/middleware/index.ts +1 -0
- package/templates/shared/dashboard/src/pages/api/costs/overview.ts +65 -0
- package/templates/shared/dashboard/src/pages/api/costs/providers.ts +47 -0
- package/templates/shared/dashboard/src/pages/api/infrastructure/services.ts +55 -0
- package/templates/shared/dashboard/src/pages/api/infrastructure/stats.ts +99 -0
- package/templates/shared/dashboard/src/pages/api/overview/summary.ts +311 -0
- package/templates/shared/dashboard/src/pages/api/usage/allowances.ts +56 -0
- package/templates/shared/dashboard/src/pages/api/usage/anomalies.ts +45 -0
- package/templates/shared/dashboard/src/pages/api/usage/billing.ts +53 -0
- package/templates/shared/dashboard/src/pages/api/usage/circuit-breakers.ts +44 -0
- package/templates/shared/dashboard/src/pages/api/usage/granular.ts +50 -0
- package/templates/shared/dashboard/src/pages/api/usage/hourly.ts +45 -0
- package/templates/shared/dashboard/src/pages/api/usage/projects.ts +51 -0
- package/templates/shared/dashboard/src/pages/api/usage/status.ts +42 -0
- package/templates/shared/dashboard/src/pages/api/user/identity.ts +11 -0
- package/templates/shared/dashboard/src/pages/dashboard.astro +11 -0
- package/templates/shared/dashboard/src/pages/index.astro +3 -0
- package/templates/shared/dashboard/src/pages/resources.astro +11 -0
- package/templates/shared/dashboard/src/pages/settings/index.astro +28 -0
- package/templates/shared/dashboard/src/pages/settings/notifications.astro +34 -0
- package/templates/shared/dashboard/src/pages/settings/thresholds.astro +39 -0
- package/templates/shared/dashboard/src/pages/settings/usage.astro +28 -0
- package/templates/shared/dashboard/src/styles/global.css +29 -0
- package/templates/shared/dashboard/tailwind.config.mjs +9 -0
- package/templates/shared/dashboard/tsconfig.json +9 -0
- package/templates/shared/dashboard/wrangler.json.hbs +47 -0
- package/templates/shared/docs/architecture.md +89 -0
- package/templates/shared/docs/post-deploy-runbook.md +126 -0
- package/templates/shared/docs/troubleshooting.md +91 -0
- package/templates/shared/package.json.hbs +17 -1
- package/templates/shared/scripts/ops/backfill-cloudflare-daily.ts +145 -0
- package/templates/shared/scripts/ops/backfill-cloudflare-hourly.ts +473 -0
- package/templates/shared/scripts/ops/backfill-monthly-rollups.ts +125 -0
- package/templates/shared/scripts/ops/discover-graphql-datasets.ts +482 -0
- package/templates/shared/scripts/ops/reset-budget-state.ts +279 -0
- package/templates/shared/scripts/ops/validate-controls.js +141 -0
- package/templates/shared/scripts/ops/validate-pipeline.ts +237 -0
- package/templates/shared/scripts/ops/verify-account-completeness.ts +236 -0
- package/templates/shared/scripts/validate-schemas.js +61 -0
- package/templates/shared/tests/contract/validate-schemas.test.ts +130 -0
- package/templates/shared/tests/fixtures/telemetry-envelope-invalid.json +9 -0
- package/templates/shared/tests/fixtures/telemetry-envelope-valid.json +27 -0
- package/templates/shared/tests/helpers/mock-d1.ts +61 -0
- package/templates/shared/tests/helpers/mock-kv.ts +37 -0
- package/templates/shared/tests/unit/workers/batch-persistence.test.ts +133 -0
- package/templates/shared/tests/unit/workers/budget-enforcement.test.ts +214 -0
- package/templates/shared/vitest.config.ts +18 -0
- package/templates/shared/workers/lib/usage/collectors/anthropic.ts +114 -0
- package/templates/shared/workers/lib/usage/collectors/apify.ts +96 -0
- package/templates/shared/workers/lib/usage/collectors/custom-http.ts +151 -0
- package/templates/shared/workers/lib/usage/collectors/deepseek.ts +92 -0
- package/templates/shared/workers/lib/usage/collectors/gemini.ts +263 -0
- package/templates/shared/workers/lib/usage/collectors/github.ts +362 -0
- package/templates/shared/workers/lib/usage/collectors/index.ts +31 -15
- package/templates/shared/workers/lib/usage/collectors/minimax.ts +106 -0
- package/templates/shared/workers/lib/usage/collectors/openai.ts +171 -0
- package/templates/shared/workers/lib/usage/collectors/resend.ts +79 -0
- package/templates/shared/workers/lib/usage/collectors/stripe.ts +192 -0
- package/templates/shared/workers/lib/usage/shared/types.ts +46 -0
- package/templates/shared/workers/platform-usage.ts +98 -8
- package/templates/standard/dashboard/src/components/errors/ErrorStats.tsx +53 -0
- package/templates/standard/dashboard/src/components/errors/ErrorsTable.tsx +133 -0
- package/templates/standard/dashboard/src/components/errors/index.ts +2 -0
- package/templates/standard/dashboard/src/components/health/CircuitBreakerEvents.tsx +69 -0
- package/templates/standard/dashboard/src/components/health/CircuitBreakerPanel.tsx +97 -0
- package/templates/standard/dashboard/src/components/health/DlqStatusCard.tsx +52 -0
- package/templates/standard/dashboard/src/components/health/HealthTabs.tsx +86 -0
- package/templates/standard/dashboard/src/components/health/index.ts +4 -0
- package/templates/standard/dashboard/src/lib/errors.ts +28 -0
- package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/mute.ts +49 -0
- package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/resolve.ts +36 -0
- package/templates/standard/dashboard/src/pages/api/errors/[fingerprint].ts +55 -0
- package/templates/standard/dashboard/src/pages/api/errors/index.ts +58 -0
- package/templates/standard/dashboard/src/pages/api/errors/stats.ts +55 -0
- package/templates/standard/dashboard/src/pages/api/health/audit-history.ts +37 -0
- package/templates/standard/dashboard/src/pages/api/health/dlq.ts +43 -0
- package/templates/standard/dashboard/src/pages/circuit-breakers.astro +13 -0
- package/templates/standard/dashboard/src/pages/errors.astro +13 -0
- package/templates/standard/dashboard/src/pages/health.astro +11 -0
- package/templates/standard/migrations/009_topology_mapper.sql +65 -0
- package/templates/standard/tests/unit/error-collector/capture.test.ts +106 -0
- package/templates/standard/tests/unit/error-collector/fingerprint.test.ts +155 -0
- package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +37 -3
- package/templates/standard/workers/lib/error-collector/gap-alerts.ts +32 -1
- package/templates/standard/workers/lib/mapper/attribution-check.ts +339 -0
- package/templates/standard/workers/lib/mapper/index.ts +7 -0
- package/templates/standard/workers/platform-mapper.ts +482 -0
- package/templates/standard/workers/platform-sdk-test-client.ts +125 -0
- package/templates/standard/wrangler.mapper.jsonc.hbs +85 -0
- package/templates/standard/wrangler.sdk-test-client.jsonc.hbs +62 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
export const GET: APIRoute = async ({ locals }) => {
|
|
4
|
+
const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
|
|
5
|
+
|
|
6
|
+
if (!db) {
|
|
7
|
+
return new Response(JSON.stringify({ byPriority: {}, byWorker: [], total: 0 }), {
|
|
8
|
+
headers: { 'Content-Type': 'application/json' },
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const byPriority = await db
|
|
14
|
+
.prepare(
|
|
15
|
+
`SELECT priority, COUNT(*) as count
|
|
16
|
+
FROM error_occurrences WHERE status = 'open'
|
|
17
|
+
GROUP BY priority
|
|
18
|
+
LIMIT 10`
|
|
19
|
+
)
|
|
20
|
+
.all<{ priority: string; count: number }>();
|
|
21
|
+
|
|
22
|
+
const byWorker = await db
|
|
23
|
+
.prepare(
|
|
24
|
+
`SELECT script_name, COUNT(*) as count
|
|
25
|
+
FROM error_occurrences WHERE status = 'open'
|
|
26
|
+
GROUP BY script_name
|
|
27
|
+
ORDER BY count DESC
|
|
28
|
+
LIMIT 10`
|
|
29
|
+
)
|
|
30
|
+
.all<{ script_name: string; count: number }>();
|
|
31
|
+
|
|
32
|
+
const total = await db
|
|
33
|
+
.prepare(`SELECT COUNT(*) as count FROM error_occurrences WHERE status = 'open' LIMIT 1`)
|
|
34
|
+
.first<{ count: number }>();
|
|
35
|
+
|
|
36
|
+
const priorityMap: Record<string, number> = {};
|
|
37
|
+
for (const row of byPriority.results ?? []) {
|
|
38
|
+
priorityMap[row.priority] = row.count;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return new Response(
|
|
42
|
+
JSON.stringify({
|
|
43
|
+
byPriority: priorityMap,
|
|
44
|
+
byWorker: byWorker.results ?? [],
|
|
45
|
+
total: total?.count ?? 0,
|
|
46
|
+
}),
|
|
47
|
+
{ headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=30' } }
|
|
48
|
+
);
|
|
49
|
+
} catch {
|
|
50
|
+
return new Response(JSON.stringify({ byPriority: {}, byWorker: [], total: 0 }), {
|
|
51
|
+
status: 500,
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
export const GET: APIRoute = async ({ locals, url }) => {
|
|
4
|
+
const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
|
|
5
|
+
|
|
6
|
+
if (!db) {
|
|
7
|
+
return new Response(JSON.stringify({ history: [] }), {
|
|
8
|
+
headers: { 'Content-Type': 'application/json' },
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const limit = Math.min(parseInt(url.searchParams.get('limit') ?? '20'), 100);
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const history = await db
|
|
16
|
+
.prepare(
|
|
17
|
+
`SELECT id, project, scan_type, ai_judge_score, sdk_score,
|
|
18
|
+
observability_score, cost_protection_score, security_score,
|
|
19
|
+
scan_date
|
|
20
|
+
FROM audit_results
|
|
21
|
+
ORDER BY scan_date DESC
|
|
22
|
+
LIMIT ?`
|
|
23
|
+
)
|
|
24
|
+
.bind(limit)
|
|
25
|
+
.all();
|
|
26
|
+
|
|
27
|
+
return new Response(
|
|
28
|
+
JSON.stringify({ history: history.results ?? [] }),
|
|
29
|
+
{ headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=120' } }
|
|
30
|
+
);
|
|
31
|
+
} catch {
|
|
32
|
+
return new Response(JSON.stringify({ history: [] }), {
|
|
33
|
+
status: 500,
|
|
34
|
+
headers: { 'Content-Type': 'application/json' },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
export const GET: APIRoute = async ({ locals }) => {
|
|
4
|
+
const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
|
|
5
|
+
|
|
6
|
+
if (!db) {
|
|
7
|
+
return new Response(JSON.stringify({ depth: 0, oldestAge: null, status: 'unknown' }), {
|
|
8
|
+
headers: { 'Content-Type': 'application/json' },
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const depth = await db
|
|
14
|
+
.prepare(`SELECT COUNT(*) as count FROM dlq_messages WHERE status = 'pending' LIMIT 1`)
|
|
15
|
+
.first<{ count: number }>();
|
|
16
|
+
|
|
17
|
+
const oldest = await db
|
|
18
|
+
.prepare(
|
|
19
|
+
`SELECT MIN(received_at) as oldest
|
|
20
|
+
FROM dlq_messages WHERE status = 'pending'
|
|
21
|
+
LIMIT 1`
|
|
22
|
+
)
|
|
23
|
+
.first<{ oldest: string | null }>();
|
|
24
|
+
|
|
25
|
+
let oldestAgeMinutes: number | null = null;
|
|
26
|
+
if (oldest?.oldest) {
|
|
27
|
+
oldestAgeMinutes = Math.round((Date.now() - new Date(oldest.oldest).getTime()) / 60000);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const dlqDepth = depth?.count ?? 0;
|
|
31
|
+
const status = dlqDepth === 0 ? 'healthy' : dlqDepth < 100 ? 'warning' : 'critical';
|
|
32
|
+
|
|
33
|
+
return new Response(
|
|
34
|
+
JSON.stringify({ depth: dlqDepth, oldestAgeMinutes, status }),
|
|
35
|
+
{ headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=30' } }
|
|
36
|
+
);
|
|
37
|
+
} catch {
|
|
38
|
+
return new Response(JSON.stringify({ depth: 0, oldestAgeMinutes: null, status: 'unknown' }), {
|
|
39
|
+
status: 500,
|
|
40
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
import DashboardLayout from '../layouts/DashboardLayout.astro';
|
|
3
|
+
import { CircuitBreakerPanel } from '../components/health/CircuitBreakerPanel';
|
|
4
|
+
import { CircuitBreakerEvents } from '../components/health/CircuitBreakerEvents';
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<DashboardLayout title="Circuit Breakers">
|
|
8
|
+
<div class="max-w-5xl mx-auto space-y-4">
|
|
9
|
+
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Circuit Breakers</h2>
|
|
10
|
+
<CircuitBreakerPanel client:load />
|
|
11
|
+
<CircuitBreakerEvents client:load />
|
|
12
|
+
</div>
|
|
13
|
+
</DashboardLayout>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
import DashboardLayout from '../../layouts/DashboardLayout.astro';
|
|
3
|
+
import { ErrorsTable } from '../../components/errors/ErrorsTable';
|
|
4
|
+
import { ErrorStats } from '../../components/errors/ErrorStats';
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<DashboardLayout title="Errors">
|
|
8
|
+
<div class="max-w-7xl mx-auto space-y-6">
|
|
9
|
+
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Error Management</h2>
|
|
10
|
+
<ErrorStats client:load />
|
|
11
|
+
<ErrorsTable client:load />
|
|
12
|
+
</div>
|
|
13
|
+
</DashboardLayout>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
import DashboardLayout from '../../layouts/DashboardLayout.astro';
|
|
3
|
+
import { HealthTabs } from '../../components/health/HealthTabs';
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<DashboardLayout title="Health">
|
|
7
|
+
<div class="max-w-7xl mx-auto">
|
|
8
|
+
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">System Health</h2>
|
|
9
|
+
<HealthTabs client:load />
|
|
10
|
+
</div>
|
|
11
|
+
</DashboardLayout>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
-- Migration 009: Topology Mapper Tables
|
|
2
|
+
--
|
|
3
|
+
-- Purpose: Infrastructure topology tracking for the platform-mapper worker.
|
|
4
|
+
-- Stores discovered Cloudflare resources, service connections, and historical snapshots.
|
|
5
|
+
--
|
|
6
|
+
-- Tables:
|
|
7
|
+
-- services — Discovered/defined infrastructure services
|
|
8
|
+
-- connections — Relationships between services
|
|
9
|
+
-- topology_snapshots — Historical topology snapshots for trend analysis
|
|
10
|
+
--
|
|
11
|
+
-- Tier: Standard+
|
|
12
|
+
-- Worker: platform-mapper (15-minute cron)
|
|
13
|
+
|
|
14
|
+
-- Services discovered or defined
|
|
15
|
+
CREATE TABLE IF NOT EXISTS services (
|
|
16
|
+
id TEXT PRIMARY KEY,
|
|
17
|
+
name TEXT NOT NULL,
|
|
18
|
+
type TEXT NOT NULL, -- 'cloudflare-worker', 'cloudflare-pages', etc.
|
|
19
|
+
tier INTEGER NOT NULL, -- 0 (critical), 1 (high), 2 (medium)
|
|
20
|
+
status TEXT NOT NULL, -- 'deployed', 'partial', 'not-deployed', 'unknown'
|
|
21
|
+
version TEXT,
|
|
22
|
+
health_endpoint TEXT,
|
|
23
|
+
health_status TEXT DEFAULT 'unknown', -- 'up', 'degraded', 'down', 'unknown'
|
|
24
|
+
last_seen TIMESTAMP,
|
|
25
|
+
metadata TEXT, -- JSON: Type-specific data
|
|
26
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
27
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_services_type ON services(type);
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_services_tier ON services(tier);
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_services_status ON services(status);
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_services_health ON services(health_status);
|
|
34
|
+
|
|
35
|
+
-- Connections between services
|
|
36
|
+
CREATE TABLE IF NOT EXISTS connections (
|
|
37
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
38
|
+
from_service TEXT NOT NULL,
|
|
39
|
+
to_service TEXT NOT NULL,
|
|
40
|
+
connection_type TEXT NOT NULL, -- 'webhook', 'api', 'data-flow', 'deployment', 'service-binding'
|
|
41
|
+
protocol TEXT, -- 'https', 'rest', 'queue', etc.
|
|
42
|
+
status TEXT NOT NULL, -- 'active', 'planned', 'not-integrated', 'broken'
|
|
43
|
+
metadata TEXT, -- JSON: Connection-specific data
|
|
44
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
45
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
46
|
+
UNIQUE (from_service, to_service, connection_type),
|
|
47
|
+
FOREIGN KEY (from_service) REFERENCES services(id),
|
|
48
|
+
FOREIGN KEY (to_service) REFERENCES services(id)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_connections_from ON connections(from_service);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_connections_to ON connections(to_service);
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_connections_status ON connections(status);
|
|
54
|
+
|
|
55
|
+
-- Historical snapshots (for topology change tracking)
|
|
56
|
+
CREATE TABLE IF NOT EXISTS topology_snapshots (
|
|
57
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
58
|
+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
59
|
+
data TEXT NOT NULL, -- JSON: Full topology snapshot
|
|
60
|
+
change_summary TEXT, -- What changed since last snapshot
|
|
61
|
+
service_count INTEGER,
|
|
62
|
+
connection_count INTEGER
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_snapshots_timestamp ON topology_snapshots(timestamp);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests for Error Capture Logic
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - shouldCapture() filtering logic
|
|
6
|
+
* - Outcome handling for all 7 non-OK outcomes
|
|
7
|
+
* - Deduplication via KV fingerprint keys
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
11
|
+
import { createMockKV } from '../../../tests/helpers/mock-kv';
|
|
12
|
+
import { MockD1Database } from '../../../tests/helpers/mock-d1';
|
|
13
|
+
|
|
14
|
+
vi.mock('@littlebearapps/platform-consumer-sdk', async (importOriginal) => {
|
|
15
|
+
const actual = await importOriginal();
|
|
16
|
+
return {
|
|
17
|
+
...actual,
|
|
18
|
+
createLoggerFromEnv: () => ({
|
|
19
|
+
info: vi.fn(),
|
|
20
|
+
warn: vi.fn(),
|
|
21
|
+
error: vi.fn(),
|
|
22
|
+
debug: vi.fn(),
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
import { shouldCapture } from '../../../workers/lib/error-collector/capture';
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Helpers
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
function createTailEvent(overrides: Record<string, unknown> = {}) {
|
|
34
|
+
return {
|
|
35
|
+
scriptName: 'test-worker',
|
|
36
|
+
outcome: 'exception',
|
|
37
|
+
exceptions: [{ name: 'Error', message: 'Test error' }],
|
|
38
|
+
logs: [],
|
|
39
|
+
eventTimestamp: Date.now(),
|
|
40
|
+
...overrides,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ===========================================================================
|
|
45
|
+
// shouldCapture
|
|
46
|
+
// ===========================================================================
|
|
47
|
+
|
|
48
|
+
describe('shouldCapture', () => {
|
|
49
|
+
it('captures exception outcomes', () => {
|
|
50
|
+
const event = createTailEvent({ outcome: 'exception' });
|
|
51
|
+
expect(shouldCapture(event as never)).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('captures exceededCpu outcomes', () => {
|
|
55
|
+
const event = createTailEvent({ outcome: 'exceededCpu' });
|
|
56
|
+
expect(shouldCapture(event as never)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('captures exceededMemory outcomes', () => {
|
|
60
|
+
const event = createTailEvent({ outcome: 'exceededMemory' });
|
|
61
|
+
expect(shouldCapture(event as never)).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('captures canceled outcomes', () => {
|
|
65
|
+
const event = createTailEvent({ outcome: 'canceled' });
|
|
66
|
+
expect(shouldCapture(event as never)).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('captures responseStreamDisconnected outcomes', () => {
|
|
70
|
+
const event = createTailEvent({ outcome: 'responseStreamDisconnected' });
|
|
71
|
+
expect(shouldCapture(event as never)).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('captures scriptNotFound outcomes', () => {
|
|
75
|
+
const event = createTailEvent({ outcome: 'scriptNotFound' });
|
|
76
|
+
expect(shouldCapture(event as never)).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('does not capture ok outcomes without error logs', () => {
|
|
80
|
+
const event = createTailEvent({ outcome: 'ok', exceptions: [], logs: [] });
|
|
81
|
+
expect(shouldCapture(event as never)).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('captures ok outcomes with error-level logs (soft errors)', () => {
|
|
85
|
+
const event = createTailEvent({
|
|
86
|
+
outcome: 'ok',
|
|
87
|
+
exceptions: [],
|
|
88
|
+
logs: [{ level: 'error', message: ['Something failed'], timestamp: Date.now() }],
|
|
89
|
+
});
|
|
90
|
+
expect(shouldCapture(event as never)).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('captures ok outcomes with warning-level logs', () => {
|
|
94
|
+
const event = createTailEvent({
|
|
95
|
+
outcome: 'ok',
|
|
96
|
+
exceptions: [],
|
|
97
|
+
logs: [{ level: 'warn', message: ['Deprecation warning'], timestamp: Date.now() }],
|
|
98
|
+
});
|
|
99
|
+
expect(shouldCapture(event as never)).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('ignores events without scriptName', () => {
|
|
103
|
+
const event = createTailEvent({ scriptName: undefined, outcome: 'exception' });
|
|
104
|
+
expect(shouldCapture(event as never)).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests for Error Fingerprinting
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Error classification by outcome type
|
|
6
|
+
* - Transient error detection (quota, rate-limit, timeout)
|
|
7
|
+
* - Message normalisation (variable stripping)
|
|
8
|
+
* - Fingerprint stability (same error = same fingerprint)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
12
|
+
|
|
13
|
+
vi.mock('@littlebearapps/platform-consumer-sdk', async (importOriginal) => {
|
|
14
|
+
const actual = await importOriginal();
|
|
15
|
+
return {
|
|
16
|
+
...actual,
|
|
17
|
+
createLoggerFromEnv: () => ({
|
|
18
|
+
info: vi.fn(),
|
|
19
|
+
warn: vi.fn(),
|
|
20
|
+
error: vi.fn(),
|
|
21
|
+
debug: vi.fn(),
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
classifyError,
|
|
28
|
+
generateFingerprint,
|
|
29
|
+
normalizeMessage,
|
|
30
|
+
isTransientError,
|
|
31
|
+
} from '../../../workers/lib/error-collector/fingerprint';
|
|
32
|
+
|
|
33
|
+
// ===========================================================================
|
|
34
|
+
// Classification
|
|
35
|
+
// ===========================================================================
|
|
36
|
+
|
|
37
|
+
describe('classifyError', () => {
|
|
38
|
+
it('classifies exception as P0-P2 based on severity', () => {
|
|
39
|
+
const result = classifyError({
|
|
40
|
+
outcome: 'exception',
|
|
41
|
+
scriptName: 'test-worker',
|
|
42
|
+
message: 'Uncaught ReferenceError: foo is not defined',
|
|
43
|
+
logs: [],
|
|
44
|
+
} as never);
|
|
45
|
+
|
|
46
|
+
expect(result).toBeDefined();
|
|
47
|
+
expect(['P0', 'P1', 'P2']).toContain(result.priority);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('classifies exceededCpu as P0', () => {
|
|
51
|
+
const result = classifyError({
|
|
52
|
+
outcome: 'exceededCpu',
|
|
53
|
+
scriptName: 'test-worker',
|
|
54
|
+
message: 'Worker exceeded CPU time limit',
|
|
55
|
+
logs: [],
|
|
56
|
+
} as never);
|
|
57
|
+
|
|
58
|
+
expect(result.priority).toBe('P0');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('classifies canceled as P2', () => {
|
|
62
|
+
const result = classifyError({
|
|
63
|
+
outcome: 'canceled',
|
|
64
|
+
scriptName: 'test-worker',
|
|
65
|
+
message: 'Request was canceled',
|
|
66
|
+
logs: [],
|
|
67
|
+
} as never);
|
|
68
|
+
|
|
69
|
+
expect(result.priority).toBe('P2');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('classifies soft_error from logs as P2-P3', () => {
|
|
73
|
+
const result = classifyError({
|
|
74
|
+
outcome: 'ok',
|
|
75
|
+
scriptName: 'test-worker',
|
|
76
|
+
message: 'Soft error captured via console.error',
|
|
77
|
+
logs: [{ level: 'error', message: ['Something failed'] }],
|
|
78
|
+
} as never);
|
|
79
|
+
|
|
80
|
+
expect(['P2', 'P3']).toContain(result.priority);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ===========================================================================
|
|
85
|
+
// Transient Detection
|
|
86
|
+
// ===========================================================================
|
|
87
|
+
|
|
88
|
+
describe('isTransientError', () => {
|
|
89
|
+
it('detects quota-exhausted as transient', () => {
|
|
90
|
+
expect(isTransientError('D1_ERROR: too many requests (quota exhausted)')).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('detects rate-limited as transient', () => {
|
|
94
|
+
expect(isTransientError('Rate limited: try again in 30 seconds')).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('detects timeout as transient', () => {
|
|
98
|
+
expect(isTransientError('Network request timed out after 30000ms')).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('does not flag regular errors as transient', () => {
|
|
102
|
+
expect(isTransientError('TypeError: Cannot read property of null')).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ===========================================================================
|
|
107
|
+
// Message Normalisation
|
|
108
|
+
// ===========================================================================
|
|
109
|
+
|
|
110
|
+
describe('normalizeMessage', () => {
|
|
111
|
+
it('strips UUIDs from messages', () => {
|
|
112
|
+
const result = normalizeMessage('Error for user a1b2c3d4-e5f6-7890-abcd-ef1234567890');
|
|
113
|
+
expect(result).not.toContain('a1b2c3d4');
|
|
114
|
+
expect(result).toContain('<UUID>');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('strips numeric IDs from messages', () => {
|
|
118
|
+
const result = normalizeMessage('Failed to process request 1234567890');
|
|
119
|
+
expect(result).not.toContain('1234567890');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('strips timestamps from messages', () => {
|
|
123
|
+
const result = normalizeMessage('Error at 2026-03-05T12:00:00Z in handler');
|
|
124
|
+
expect(result).not.toContain('2026-03-05');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('preserves error type prefixes', () => {
|
|
128
|
+
const result = normalizeMessage('TypeError: Cannot read property of null');
|
|
129
|
+
expect(result).toContain('TypeError');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ===========================================================================
|
|
134
|
+
// Fingerprint Stability
|
|
135
|
+
// ===========================================================================
|
|
136
|
+
|
|
137
|
+
describe('generateFingerprint', () => {
|
|
138
|
+
it('produces consistent fingerprint for same error', () => {
|
|
139
|
+
const fp1 = generateFingerprint('test-worker', 'TypeError: Cannot read property of null');
|
|
140
|
+
const fp2 = generateFingerprint('test-worker', 'TypeError: Cannot read property of null');
|
|
141
|
+
expect(fp1).toBe(fp2);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('produces different fingerprints for different errors', () => {
|
|
145
|
+
const fp1 = generateFingerprint('test-worker', 'TypeError: Cannot read property of null');
|
|
146
|
+
const fp2 = generateFingerprint('test-worker', 'RangeError: Maximum call stack exceeded');
|
|
147
|
+
expect(fp1).not.toBe(fp2);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('produces different fingerprints for different workers', () => {
|
|
151
|
+
const fp1 = generateFingerprint('worker-a', 'Error: timeout');
|
|
152
|
+
const fp2 = generateFingerprint('worker-b', 'Error: timeout');
|
|
153
|
+
expect(fp1).not.toBe(fp2);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -198,14 +198,48 @@ export async function processEmailHealthAlerts(
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
try {
|
|
201
|
-
|
|
201
|
+
const issueTitle = `Email Health: ${brandName} ${failure.check_type} failing`;
|
|
202
|
+
|
|
203
|
+
// Search for an existing OPEN issue before creating a new one.
|
|
204
|
+
// This prevents daily duplicate issues when a problem persists across days.
|
|
205
|
+
const openIssues = await github.searchIssues(
|
|
206
|
+
owner,
|
|
207
|
+
repo,
|
|
208
|
+
`"${issueTitle}" is:open label:cf:email-health`
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
if (openIssues.length > 0) {
|
|
212
|
+
const existing = openIssues[0];
|
|
213
|
+
await github.addComment(
|
|
214
|
+
owner,
|
|
215
|
+
repo,
|
|
216
|
+
existing.number,
|
|
217
|
+
`### Still failing (${getDateKey()})\n\n` +
|
|
218
|
+
`Check \`${failure.check_type}\` continues to fail: ${failure.error_msg}\n\n` +
|
|
219
|
+
`Run ID: \`${event.run_id}\``
|
|
220
|
+
);
|
|
221
|
+
await setDedup(env.PLATFORM_CACHE, event.brand_id, failure.check_type, existing.number);
|
|
222
|
+
|
|
223
|
+
console.log(
|
|
224
|
+
`Commented on existing email health issue #${existing.number} for ${event.brand_id}:${failure.check_type}`
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
results.skipped++;
|
|
228
|
+
results.skippedChecks.push({
|
|
229
|
+
check_type: failure.check_type,
|
|
230
|
+
reason: `Commented on existing issue #${existing.number}`,
|
|
231
|
+
});
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// No existing open issue — create a new one
|
|
202
236
|
const issue = await github.createIssue({
|
|
203
237
|
owner,
|
|
204
238
|
repo,
|
|
205
|
-
title:
|
|
239
|
+
title: issueTitle,
|
|
206
240
|
body: formatIssueBody({
|
|
207
241
|
...event,
|
|
208
|
-
failures: [failure],
|
|
242
|
+
failures: [failure],
|
|
209
243
|
}),
|
|
210
244
|
labels: EMAIL_HEALTH_LABELS,
|
|
211
245
|
});
|
|
@@ -237,7 +237,38 @@ export async function processGapAlert(
|
|
|
237
237
|
const github = new GitHubClient(env);
|
|
238
238
|
|
|
239
239
|
try {
|
|
240
|
-
//
|
|
240
|
+
// Search for an existing OPEN gap alert issue before creating a new one.
|
|
241
|
+
// This prevents daily duplicate issues when coverage stays below threshold.
|
|
242
|
+
const openIssues = await github.searchIssues(
|
|
243
|
+
owner,
|
|
244
|
+
repo,
|
|
245
|
+
`"Data Coverage Gap: ${event.project}" is:open label:cf:gap-alert`
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
if (openIssues.length > 0) {
|
|
249
|
+
const existing = openIssues[0];
|
|
250
|
+
await github.addComment(
|
|
251
|
+
owner,
|
|
252
|
+
repo,
|
|
253
|
+
existing.number,
|
|
254
|
+
`### Coverage still below threshold (${getDateKey()})\n\n` +
|
|
255
|
+
`Coverage: **${event.coveragePct}%** (threshold: 90%)\n` +
|
|
256
|
+
`Hours with data: ${event.hoursWithData}/${event.expectedHours}\n` +
|
|
257
|
+
`Missing hours: ${event.missingHours.length}`
|
|
258
|
+
);
|
|
259
|
+
await setGapAlertDedup(env.PLATFORM_CACHE, event.project, existing.number);
|
|
260
|
+
|
|
261
|
+
console.log(
|
|
262
|
+
`Commented on existing gap alert issue #${existing.number} for ${event.project}`
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
processed: false,
|
|
267
|
+
skipped: `Commented on existing issue #${existing.number}`,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// No existing open issue — create a new one
|
|
241
272
|
const issue = await github.createIssue({
|
|
242
273
|
owner,
|
|
243
274
|
repo,
|