@littlebearapps/platform-admin-sdk 2.0.0 → 2.2.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 +4 -7
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +206 -4
- 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/DigestStats.tsx +151 -0
- package/templates/full/dashboard/src/components/reports/FeatureUsageReport.tsx +339 -0
- package/templates/full/dashboard/src/components/reports/HealthTrendsReport.tsx +192 -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/components/usage/AIModelBreakdown.tsx +364 -0
- package/templates/full/dashboard/src/components/usage/unified/Recommendations.tsx +149 -0
- package/templates/full/dashboard/src/lib/cloudflare/alerting.ts +486 -0
- package/templates/full/dashboard/src/lib/cloudflare/graphql.ts +4785 -0
- package/templates/full/dashboard/src/lib/cloudflare/project-registry.ts +451 -0
- package/templates/full/dashboard/src/lib/notifications/api.ts +197 -0
- package/templates/full/dashboard/src/lib/notifications/types.ts.hbs +97 -0
- package/templates/full/dashboard/src/lib/patterns/api.ts +120 -0
- package/templates/full/dashboard/src/lib/patterns/types.ts +127 -0
- package/templates/full/dashboard/src/lib/reports/types.ts +231 -0
- package/templates/full/dashboard/src/lib/search/api.ts +258 -0
- package/templates/full/dashboard/src/lib/search/types.ts.hbs +115 -0
- package/templates/full/dashboard/src/lib/settings/api.ts.hbs +201 -0
- package/templates/full/dashboard/src/lib/settings/types.ts.hbs +104 -0
- package/templates/full/dashboard/src/lib/usage/allowance-config.ts.hbs +547 -0
- package/templates/full/dashboard/src/lib/usage/providers.ts +331 -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/reports/ReportInfoButton.tsx +98 -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/react/DashboardShell.tsx +263 -0
- package/templates/shared/dashboard/src/components/usage/react/StatusBadge.tsx +77 -0
- package/templates/shared/dashboard/src/components/usage/react/UsageChart.tsx +391 -0
- package/templates/shared/dashboard/src/components/usage/react/index.ts.hbs +30 -0
- package/templates/shared/dashboard/src/components/usage/react/types.ts +137 -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/transformers.ts +478 -0
- package/templates/shared/dashboard/src/components/usage/types.ts +283 -0
- package/templates/shared/dashboard/src/components/usage/unified/AlertBanner.tsx +172 -0
- package/templates/shared/dashboard/src/components/usage/unified/HeroCardsRow.tsx +757 -0
- package/templates/shared/dashboard/src/components/usage/unified/LiveHeader.tsx +169 -0
- package/templates/shared/dashboard/src/components/usage/unified/ProjectsTable.tsx +448 -0
- package/templates/shared/dashboard/src/components/usage/unified/ResourceBreakdown.tsx +236 -0
- package/templates/shared/dashboard/src/components/usage/unified/Sparkline.tsx +127 -0
- package/templates/shared/dashboard/src/components/usage/unified/UnifiedShell.tsx +893 -0
- package/templates/shared/dashboard/src/components/usage/unified/index.ts.hbs +50 -0
- package/templates/shared/dashboard/src/components/usage/unified/types.ts +416 -0
- package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -0
- package/templates/shared/dashboard/src/lib/cloudflare/analytics.ts +310 -0
- package/templates/shared/dashboard/src/lib/cloudflare/d1.ts +55 -0
- package/templates/shared/dashboard/src/lib/cloudflare/index.ts.hbs +120 -0
- package/templates/shared/dashboard/src/lib/infrastructure/types.ts +116 -0
- package/templates/shared/dashboard/src/lib/usage/fetchWithDedup.ts +101 -0
- package/templates/shared/dashboard/src/lib/usage/index.ts.hbs +12 -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-api.test.ts +909 -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/shared/tests/helpers/mock-storage.ts +166 -0
- package/templates/shared/tests/integration/kv-cache.test.ts +252 -0
- package/templates/shared/tests/integration/platform-usage.test.ts +956 -0
- package/templates/shared/tests/unit/billing.test.ts +331 -0
- package/templates/shared/tests/unit/cloudflare/graphql.test.ts +217 -0
- package/templates/shared/tests/unit/components/usage-transformers.test.ts +473 -0
- package/templates/shared/tests/unit/control.test.ts +226 -0
- package/templates/shared/tests/unit/cost-calculator.test.ts +141 -0
- package/templates/shared/tests/unit/economics.test.ts +365 -0
- package/templates/shared/tests/unit/telemetry-sampling.test.ts +401 -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/components/reports/CircuitBreakerReport.tsx +474 -0
- package/templates/standard/dashboard/src/components/reports/CostTrendsReport.tsx +229 -0
- package/templates/standard/dashboard/src/components/reports/ErrorTrendsReport.tsx +244 -0
- package/templates/standard/dashboard/src/components/reports/ProjectHealthTable.tsx +251 -0
- package/templates/standard/dashboard/src/components/reports/WarningDigestsTable.tsx +298 -0
- package/templates/standard/dashboard/src/components/usage/react/UsageTable.tsx +385 -0
- package/templates/standard/dashboard/src/components/usage/unified/CircuitBreakerEvents.tsx +305 -0
- package/templates/standard/dashboard/src/components/usage/unified/FeatureBudgets.tsx +472 -0
- package/templates/standard/dashboard/src/lib/errors/api.ts +84 -0
- package/templates/standard/dashboard/src/lib/errors/types.ts +75 -0
- package/templates/standard/dashboard/src/lib/infrastructure/api.ts +141 -0
- package/templates/standard/dashboard/src/lib/infrastructure/gatus.ts.hbs +112 -0
- package/templates/standard/dashboard/src/lib/services/proxy/index.ts +20 -0
- package/templates/standard/dashboard/src/lib/services/proxy/proxy.ts +244 -0
- package/templates/standard/dashboard/src/lib/services/proxy/types.ts +81 -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
- package/templates/standard/tests/integration/platform-sentinel.test.ts +497 -0
- package/templates/standard/tests/unit/cloudflare/alerting.test.ts +480 -0
- package/templates/standard/tests/unit/error-collector/dedup.test.ts +350 -0
- package/templates/standard/tests/unit/error-collector/github.test.ts +187 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests for Reservoir Sampling
|
|
3
|
+
*
|
|
4
|
+
* Tests Algorithm R implementation for O(1) memory latency percentile tracking.
|
|
5
|
+
*
|
|
6
|
+
* @module tests/unit/telemetry-sampling
|
|
7
|
+
* @created 2026-01-23
|
|
8
|
+
* @task Intelligent Degradation for Platform Usage
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, it } from 'vitest';
|
|
12
|
+
import {
|
|
13
|
+
createReservoirState,
|
|
14
|
+
addSample,
|
|
15
|
+
addSamples,
|
|
16
|
+
calculatePercentiles,
|
|
17
|
+
getPercentiles,
|
|
18
|
+
mergeReservoirs,
|
|
19
|
+
resetReservoir,
|
|
20
|
+
estimateMemoryUsage,
|
|
21
|
+
formatPercentiles,
|
|
22
|
+
checkLatencyThresholds,
|
|
23
|
+
DEFAULT_RESERVOIR_CONFIG,
|
|
24
|
+
} from '../../workers/lib/telemetry-sampling';
|
|
25
|
+
|
|
26
|
+
describe('Reservoir Sampling', () => {
|
|
27
|
+
describe('createReservoirState', () => {
|
|
28
|
+
it('creates fresh state with empty samples', () => {
|
|
29
|
+
const state = createReservoirState();
|
|
30
|
+
|
|
31
|
+
expect(state.samples).toEqual([]);
|
|
32
|
+
expect(state.totalSeen).toBe(0);
|
|
33
|
+
expect(state.lastUpdate).toBeGreaterThan(0);
|
|
34
|
+
expect(state.percentiles).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('addSample', () => {
|
|
39
|
+
it('adds samples directly when reservoir is not full', () => {
|
|
40
|
+
const state = createReservoirState();
|
|
41
|
+
|
|
42
|
+
addSample(state, 10);
|
|
43
|
+
addSample(state, 20);
|
|
44
|
+
addSample(state, 30);
|
|
45
|
+
|
|
46
|
+
expect(state.samples).toEqual([10, 20, 30]);
|
|
47
|
+
expect(state.totalSeen).toBe(3);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('maintains fixed size when reservoir is full', () => {
|
|
51
|
+
const state = createReservoirState();
|
|
52
|
+
const config = { maxSamples: 5 };
|
|
53
|
+
|
|
54
|
+
// Add more samples than reservoir can hold
|
|
55
|
+
for (let i = 1; i <= 10; i++) {
|
|
56
|
+
addSample(state, i, config);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
expect(state.samples.length).toBe(5);
|
|
60
|
+
expect(state.totalSeen).toBe(10);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('clears cached percentiles on new sample', () => {
|
|
64
|
+
const state = createReservoirState();
|
|
65
|
+
addSample(state, 10);
|
|
66
|
+
calculatePercentiles(state); // Cache percentiles
|
|
67
|
+
|
|
68
|
+
expect(state.percentiles).toBeDefined();
|
|
69
|
+
|
|
70
|
+
addSample(state, 20);
|
|
71
|
+
expect(state.percentiles).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('uses Algorithm R probability for replacement', () => {
|
|
75
|
+
// Statistical test: after many samples, each value should have equal probability
|
|
76
|
+
// This is a smoke test - full statistical validation would require more samples
|
|
77
|
+
const state = createReservoirState();
|
|
78
|
+
const config = { maxSamples: 100 };
|
|
79
|
+
|
|
80
|
+
// Add 1000 samples
|
|
81
|
+
for (let i = 1; i <= 1000; i++) {
|
|
82
|
+
addSample(state, i, config);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Verify basic properties
|
|
86
|
+
expect(state.samples.length).toBe(100);
|
|
87
|
+
expect(state.totalSeen).toBe(1000);
|
|
88
|
+
|
|
89
|
+
// Check that samples span the range (not just first 100)
|
|
90
|
+
const maxSample = Math.max(...state.samples);
|
|
91
|
+
const minSample = Math.min(...state.samples);
|
|
92
|
+
expect(maxSample).toBeGreaterThan(500); // Should have late samples
|
|
93
|
+
expect(minSample).toBeLessThan(500); // Should have early samples
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('addSamples', () => {
|
|
98
|
+
it('adds multiple samples efficiently', () => {
|
|
99
|
+
const state = createReservoirState();
|
|
100
|
+
|
|
101
|
+
addSamples(state, [10, 20, 30, 40, 50]);
|
|
102
|
+
|
|
103
|
+
expect(state.totalSeen).toBe(5);
|
|
104
|
+
expect(state.samples.length).toBe(5);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('calculatePercentiles', () => {
|
|
109
|
+
it('returns undefined for empty reservoir', () => {
|
|
110
|
+
const state = createReservoirState();
|
|
111
|
+
const percentiles = calculatePercentiles(state);
|
|
112
|
+
expect(percentiles).toBeUndefined();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('calculates correct percentiles for known data', () => {
|
|
116
|
+
const state = createReservoirState();
|
|
117
|
+
// Add values 1-100
|
|
118
|
+
for (let i = 1; i <= 100; i++) {
|
|
119
|
+
addSample(state, i);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const percentiles = calculatePercentiles(state);
|
|
123
|
+
|
|
124
|
+
expect(percentiles).toBeDefined();
|
|
125
|
+
expect(percentiles!.p50).toBe(50);
|
|
126
|
+
expect(percentiles!.p90).toBe(90);
|
|
127
|
+
expect(percentiles!.p95).toBe(95);
|
|
128
|
+
expect(percentiles!.p99).toBe(99);
|
|
129
|
+
expect(percentiles!.min).toBe(1);
|
|
130
|
+
expect(percentiles!.max).toBe(100);
|
|
131
|
+
expect(percentiles!.avg).toBe(50.5);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('caches percentiles in state', () => {
|
|
135
|
+
const state = createReservoirState();
|
|
136
|
+
addSamples(state, [10, 20, 30]);
|
|
137
|
+
|
|
138
|
+
const p1 = calculatePercentiles(state);
|
|
139
|
+
const p2 = state.percentiles;
|
|
140
|
+
|
|
141
|
+
expect(p1).toBe(p2); // Same object reference
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('handles single sample', () => {
|
|
145
|
+
const state = createReservoirState();
|
|
146
|
+
addSample(state, 42);
|
|
147
|
+
|
|
148
|
+
const percentiles = calculatePercentiles(state);
|
|
149
|
+
|
|
150
|
+
expect(percentiles!.p50).toBe(42);
|
|
151
|
+
expect(percentiles!.p99).toBe(42);
|
|
152
|
+
expect(percentiles!.min).toBe(42);
|
|
153
|
+
expect(percentiles!.max).toBe(42);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('getPercentiles', () => {
|
|
158
|
+
it('returns cached percentiles if available', () => {
|
|
159
|
+
const state = createReservoirState();
|
|
160
|
+
addSamples(state, [10, 20, 30]);
|
|
161
|
+
calculatePercentiles(state);
|
|
162
|
+
|
|
163
|
+
const cached = state.percentiles;
|
|
164
|
+
const result = getPercentiles(state);
|
|
165
|
+
|
|
166
|
+
expect(result).toBe(cached);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('computes percentiles if not cached', () => {
|
|
170
|
+
const state = createReservoirState();
|
|
171
|
+
addSamples(state, [10, 20, 30]);
|
|
172
|
+
|
|
173
|
+
expect(state.percentiles).toBeUndefined();
|
|
174
|
+
|
|
175
|
+
const result = getPercentiles(state);
|
|
176
|
+
|
|
177
|
+
expect(result).toBeDefined();
|
|
178
|
+
expect(state.percentiles).toBeDefined();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('mergeReservoirs', () => {
|
|
183
|
+
it('returns copy of non-empty reservoir when other is empty', () => {
|
|
184
|
+
const a = createReservoirState();
|
|
185
|
+
addSamples(a, [10, 20, 30]);
|
|
186
|
+
|
|
187
|
+
const b = createReservoirState();
|
|
188
|
+
|
|
189
|
+
const merged = mergeReservoirs(a, b);
|
|
190
|
+
|
|
191
|
+
expect(merged.samples).toEqual([10, 20, 30]);
|
|
192
|
+
expect(merged.totalSeen).toBe(3);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('returns copy of non-empty reservoir when first is empty', () => {
|
|
196
|
+
const a = createReservoirState();
|
|
197
|
+
|
|
198
|
+
const b = createReservoirState();
|
|
199
|
+
addSamples(b, [40, 50]);
|
|
200
|
+
|
|
201
|
+
const merged = mergeReservoirs(a, b);
|
|
202
|
+
|
|
203
|
+
expect(merged.samples).toEqual([40, 50]);
|
|
204
|
+
expect(merged.totalSeen).toBe(2);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('combines samples when total fits in reservoir', () => {
|
|
208
|
+
const a = createReservoirState();
|
|
209
|
+
addSamples(a, [10, 20]);
|
|
210
|
+
|
|
211
|
+
const b = createReservoirState();
|
|
212
|
+
addSamples(b, [30, 40]);
|
|
213
|
+
|
|
214
|
+
const merged = mergeReservoirs(a, b, { maxSamples: 10 });
|
|
215
|
+
|
|
216
|
+
expect(merged.samples.length).toBe(4);
|
|
217
|
+
expect(merged.totalSeen).toBe(4);
|
|
218
|
+
expect(merged.samples).toContain(10);
|
|
219
|
+
expect(merged.samples).toContain(40);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('randomly selects when combined exceeds reservoir size', () => {
|
|
223
|
+
const a = createReservoirState();
|
|
224
|
+
addSamples(a, [1, 2, 3, 4, 5]);
|
|
225
|
+
|
|
226
|
+
const b = createReservoirState();
|
|
227
|
+
addSamples(b, [6, 7, 8, 9, 10]);
|
|
228
|
+
|
|
229
|
+
const merged = mergeReservoirs(a, b, { maxSamples: 5 });
|
|
230
|
+
|
|
231
|
+
expect(merged.samples.length).toBe(5);
|
|
232
|
+
expect(merged.totalSeen).toBe(10);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('preserves latest timestamp', () => {
|
|
236
|
+
const a = createReservoirState();
|
|
237
|
+
a.lastUpdate = 1000;
|
|
238
|
+
|
|
239
|
+
const b = createReservoirState();
|
|
240
|
+
b.lastUpdate = 2000;
|
|
241
|
+
|
|
242
|
+
const merged = mergeReservoirs(a, b);
|
|
243
|
+
|
|
244
|
+
expect(merged.lastUpdate).toBe(2000);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('resetReservoir', () => {
|
|
249
|
+
it('clears all samples and resets counters', () => {
|
|
250
|
+
const state = createReservoirState();
|
|
251
|
+
addSamples(state, [10, 20, 30]);
|
|
252
|
+
calculatePercentiles(state);
|
|
253
|
+
|
|
254
|
+
resetReservoir(state);
|
|
255
|
+
|
|
256
|
+
expect(state.samples).toEqual([]);
|
|
257
|
+
expect(state.totalSeen).toBe(0);
|
|
258
|
+
expect(state.percentiles).toBeUndefined();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('updates lastUpdate timestamp', () => {
|
|
262
|
+
const state = createReservoirState();
|
|
263
|
+
const before = state.lastUpdate;
|
|
264
|
+
|
|
265
|
+
// Small delay to ensure timestamp difference
|
|
266
|
+
resetReservoir(state);
|
|
267
|
+
|
|
268
|
+
expect(state.lastUpdate).toBeGreaterThanOrEqual(before);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe('estimateMemoryUsage', () => {
|
|
273
|
+
it('estimates memory based on sample count', () => {
|
|
274
|
+
const state = createReservoirState();
|
|
275
|
+
const emptySize = estimateMemoryUsage(state);
|
|
276
|
+
|
|
277
|
+
addSamples(state, [10, 20, 30, 40, 50]);
|
|
278
|
+
const withSamplesSize = estimateMemoryUsage(state);
|
|
279
|
+
|
|
280
|
+
expect(withSamplesSize).toBeGreaterThan(emptySize);
|
|
281
|
+
// 5 samples * 8 bytes = 40 bytes more
|
|
282
|
+
expect(withSamplesSize - emptySize).toBe(40);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('stays under 1KB for default config', () => {
|
|
286
|
+
const state = createReservoirState();
|
|
287
|
+
for (let i = 0; i < DEFAULT_RESERVOIR_CONFIG.maxSamples; i++) {
|
|
288
|
+
addSample(state, Math.random() * 1000);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const memoryUsage = estimateMemoryUsage(state);
|
|
292
|
+
expect(memoryUsage).toBeLessThan(1024);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe('formatPercentiles', () => {
|
|
297
|
+
it('formats percentiles as readable string', () => {
|
|
298
|
+
const state = createReservoirState();
|
|
299
|
+
for (let i = 1; i <= 100; i++) {
|
|
300
|
+
addSample(state, i);
|
|
301
|
+
}
|
|
302
|
+
const percentiles = calculatePercentiles(state)!;
|
|
303
|
+
|
|
304
|
+
const formatted = formatPercentiles(percentiles);
|
|
305
|
+
|
|
306
|
+
expect(formatted).toContain('p50=');
|
|
307
|
+
expect(formatted).toContain('p95=');
|
|
308
|
+
expect(formatted).toContain('p99=');
|
|
309
|
+
expect(formatted).toContain('max=');
|
|
310
|
+
expect(formatted).toContain('n=100/100');
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe('checkLatencyThresholds', () => {
|
|
315
|
+
it('returns undefined when within thresholds', () => {
|
|
316
|
+
const state = createReservoirState();
|
|
317
|
+
for (let i = 1; i <= 50; i++) {
|
|
318
|
+
addSample(state, i); // Max 50ms
|
|
319
|
+
}
|
|
320
|
+
const percentiles = calculatePercentiles(state)!;
|
|
321
|
+
|
|
322
|
+
const warning = checkLatencyThresholds(percentiles);
|
|
323
|
+
expect(warning).toBeUndefined();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('returns warning when p95 exceeds threshold', () => {
|
|
327
|
+
const state = createReservoirState();
|
|
328
|
+
for (let i = 1; i <= 100; i++) {
|
|
329
|
+
addSample(state, i * 2); // Max 200ms, p95 = 190ms
|
|
330
|
+
}
|
|
331
|
+
const percentiles = calculatePercentiles(state)!;
|
|
332
|
+
|
|
333
|
+
const warning = checkLatencyThresholds(percentiles, { p95Warning: 100, p99Warning: 500 });
|
|
334
|
+
expect(warning).toBeDefined();
|
|
335
|
+
expect(warning).toContain('p95');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('returns warning when p99 exceeds threshold', () => {
|
|
339
|
+
const state = createReservoirState();
|
|
340
|
+
for (let i = 1; i <= 100; i++) {
|
|
341
|
+
addSample(state, i * 6); // Max 600ms, p99 = 594ms
|
|
342
|
+
}
|
|
343
|
+
const percentiles = calculatePercentiles(state)!;
|
|
344
|
+
|
|
345
|
+
const warning = checkLatencyThresholds(percentiles, { p95Warning: 1000, p99Warning: 500 });
|
|
346
|
+
expect(warning).toBeDefined();
|
|
347
|
+
expect(warning).toContain('p99');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('returns multiple warnings when both thresholds exceeded', () => {
|
|
351
|
+
const state = createReservoirState();
|
|
352
|
+
for (let i = 1; i <= 100; i++) {
|
|
353
|
+
addSample(state, i * 10); // Max 1000ms
|
|
354
|
+
}
|
|
355
|
+
const percentiles = calculatePercentiles(state)!;
|
|
356
|
+
|
|
357
|
+
const warning = checkLatencyThresholds(percentiles, { p95Warning: 100, p99Warning: 500 });
|
|
358
|
+
expect(warning).toContain('p95');
|
|
359
|
+
expect(warning).toContain('p99');
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe('Statistical Properties', () => {
|
|
364
|
+
it('maintains representative sample for uniform distribution', () => {
|
|
365
|
+
// Add many samples and verify the reservoir maintains distribution characteristics
|
|
366
|
+
const state = createReservoirState();
|
|
367
|
+
|
|
368
|
+
// Add 10,000 samples from uniform distribution [0, 1000]
|
|
369
|
+
for (let i = 0; i < 10000; i++) {
|
|
370
|
+
addSample(state, Math.random() * 1000);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const percentiles = calculatePercentiles(state)!;
|
|
374
|
+
|
|
375
|
+
// For uniform distribution, p50 should be around 500, p90 around 900, etc.
|
|
376
|
+
// Allow generous tolerance since this is a statistical test
|
|
377
|
+
expect(percentiles.p50).toBeGreaterThan(350);
|
|
378
|
+
expect(percentiles.p50).toBeLessThan(650);
|
|
379
|
+
expect(percentiles.p90).toBeGreaterThan(750);
|
|
380
|
+
expect(percentiles.p90).toBeLessThan(980);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('maintains representative sample for skewed distribution', () => {
|
|
384
|
+
const state = createReservoirState();
|
|
385
|
+
|
|
386
|
+
// Add 10,000 samples with exponential-like distribution (many small, few large)
|
|
387
|
+
for (let i = 0; i < 10000; i++) {
|
|
388
|
+
// Exponential-like: -ln(U) * scale
|
|
389
|
+
const sample = -Math.log(Math.random()) * 100;
|
|
390
|
+
addSample(state, sample);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const percentiles = calculatePercentiles(state)!;
|
|
394
|
+
|
|
395
|
+
// Median should be around 69 (ln(2) * 100), p99 should be much larger
|
|
396
|
+
expect(percentiles.p50).toBeLessThan(percentiles.p90);
|
|
397
|
+
expect(percentiles.p90).toBeLessThan(percentiles.p99);
|
|
398
|
+
expect(percentiles.p99).toBeGreaterThan(percentiles.p50 * 3);
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Priority Badge Component
|
|
4
|
+
* Displays P0-P4 priority with appropriate colour coding
|
|
5
|
+
*/
|
|
6
|
+
interface Props {
|
|
7
|
+
priority: string;
|
|
8
|
+
size?: 'sm' | 'md';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { priority, size = 'md' } = Astro.props;
|
|
12
|
+
|
|
13
|
+
const colours: Record<string, string> = {
|
|
14
|
+
P0: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300 border-red-200 dark:border-red-800',
|
|
15
|
+
P1: 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300 border-orange-200 dark:border-orange-800',
|
|
16
|
+
P2: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300 border-yellow-200 dark:border-yellow-800',
|
|
17
|
+
P3: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 border-blue-200 dark:border-blue-800',
|
|
18
|
+
P4: 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300 border-gray-200 dark:border-gray-800',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const sizeClasses = size === 'sm' ? 'px-1.5 py-0.5 text-xs' : 'px-2 py-1 text-sm';
|
|
22
|
+
const colourClass = colours[priority] || colours.P4;
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<span class={`inline-flex items-center font-semibold rounded border ${colourClass} ${sizeClasses}`}>
|
|
26
|
+
{priority}
|
|
27
|
+
</span>
|