@littlebearapps/platform-admin-sdk 2.0.0 → 2.1.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 (74) hide show
  1. package/README.md +2 -2
  2. package/dist/templates.d.ts +1 -1
  3. package/dist/templates.js +86 -2
  4. package/package.json +1 -1
  5. package/templates/full/dashboard/src/components/reports/DigestStats.tsx +151 -0
  6. package/templates/full/dashboard/src/components/reports/HealthTrendsReport.tsx +192 -0
  7. package/templates/full/dashboard/src/components/usage/AIModelBreakdown.tsx +364 -0
  8. package/templates/full/dashboard/src/components/usage/unified/Recommendations.tsx +149 -0
  9. package/templates/full/dashboard/src/lib/cloudflare/alerting.ts +486 -0
  10. package/templates/full/dashboard/src/lib/cloudflare/graphql.ts +4785 -0
  11. package/templates/full/dashboard/src/lib/cloudflare/project-registry.ts +451 -0
  12. package/templates/full/dashboard/src/lib/notifications/api.ts +197 -0
  13. package/templates/full/dashboard/src/lib/notifications/types.ts.hbs +97 -0
  14. package/templates/full/dashboard/src/lib/patterns/api.ts +120 -0
  15. package/templates/full/dashboard/src/lib/patterns/types.ts +127 -0
  16. package/templates/full/dashboard/src/lib/reports/types.ts +231 -0
  17. package/templates/full/dashboard/src/lib/search/api.ts +258 -0
  18. package/templates/full/dashboard/src/lib/search/types.ts.hbs +115 -0
  19. package/templates/full/dashboard/src/lib/settings/api.ts.hbs +201 -0
  20. package/templates/full/dashboard/src/lib/settings/types.ts.hbs +104 -0
  21. package/templates/full/dashboard/src/lib/usage/allowance-config.ts.hbs +547 -0
  22. package/templates/full/dashboard/src/lib/usage/providers.ts +331 -0
  23. package/templates/shared/dashboard/src/components/reports/ReportInfoButton.tsx +98 -0
  24. package/templates/shared/dashboard/src/components/usage/react/DashboardShell.tsx +263 -0
  25. package/templates/shared/dashboard/src/components/usage/react/StatusBadge.tsx +77 -0
  26. package/templates/shared/dashboard/src/components/usage/react/UsageChart.tsx +391 -0
  27. package/templates/shared/dashboard/src/components/usage/react/index.ts.hbs +30 -0
  28. package/templates/shared/dashboard/src/components/usage/react/types.ts +137 -0
  29. package/templates/shared/dashboard/src/components/usage/transformers.ts +478 -0
  30. package/templates/shared/dashboard/src/components/usage/unified/AlertBanner.tsx +172 -0
  31. package/templates/shared/dashboard/src/components/usage/unified/HeroCardsRow.tsx +757 -0
  32. package/templates/shared/dashboard/src/components/usage/unified/LiveHeader.tsx +169 -0
  33. package/templates/shared/dashboard/src/components/usage/unified/ProjectsTable.tsx +448 -0
  34. package/templates/shared/dashboard/src/components/usage/unified/ResourceBreakdown.tsx +236 -0
  35. package/templates/shared/dashboard/src/components/usage/unified/Sparkline.tsx +127 -0
  36. package/templates/shared/dashboard/src/components/usage/unified/UnifiedShell.tsx +893 -0
  37. package/templates/shared/dashboard/src/components/usage/unified/index.ts.hbs +50 -0
  38. package/templates/shared/dashboard/src/components/usage/unified/types.ts +416 -0
  39. package/templates/shared/dashboard/src/lib/cloudflare/analytics.ts +310 -0
  40. package/templates/shared/dashboard/src/lib/cloudflare/d1.ts +55 -0
  41. package/templates/shared/dashboard/src/lib/cloudflare/index.ts.hbs +120 -0
  42. package/templates/shared/dashboard/src/lib/infrastructure/types.ts +116 -0
  43. package/templates/shared/dashboard/src/lib/usage/fetchWithDedup.ts +101 -0
  44. package/templates/shared/dashboard/src/lib/usage/index.ts.hbs +12 -0
  45. package/templates/shared/tests/e2e/usage-api.test.ts +909 -0
  46. package/templates/shared/tests/helpers/mock-storage.ts +166 -0
  47. package/templates/shared/tests/integration/kv-cache.test.ts +252 -0
  48. package/templates/shared/tests/integration/platform-usage.test.ts +956 -0
  49. package/templates/shared/tests/unit/billing.test.ts +331 -0
  50. package/templates/shared/tests/unit/cloudflare/graphql.test.ts +217 -0
  51. package/templates/shared/tests/unit/components/usage-transformers.test.ts +473 -0
  52. package/templates/shared/tests/unit/control.test.ts +226 -0
  53. package/templates/shared/tests/unit/cost-calculator.test.ts +141 -0
  54. package/templates/shared/tests/unit/economics.test.ts +365 -0
  55. package/templates/shared/tests/unit/telemetry-sampling.test.ts +401 -0
  56. package/templates/standard/dashboard/src/components/reports/CircuitBreakerReport.tsx +474 -0
  57. package/templates/standard/dashboard/src/components/reports/CostTrendsReport.tsx +229 -0
  58. package/templates/standard/dashboard/src/components/reports/ErrorTrendsReport.tsx +244 -0
  59. package/templates/standard/dashboard/src/components/reports/ProjectHealthTable.tsx +251 -0
  60. package/templates/standard/dashboard/src/components/reports/WarningDigestsTable.tsx +298 -0
  61. package/templates/standard/dashboard/src/components/usage/react/UsageTable.tsx +385 -0
  62. package/templates/standard/dashboard/src/components/usage/unified/CircuitBreakerEvents.tsx +305 -0
  63. package/templates/standard/dashboard/src/components/usage/unified/FeatureBudgets.tsx +472 -0
  64. package/templates/standard/dashboard/src/lib/errors/api.ts +84 -0
  65. package/templates/standard/dashboard/src/lib/errors/types.ts +75 -0
  66. package/templates/standard/dashboard/src/lib/infrastructure/api.ts +141 -0
  67. package/templates/standard/dashboard/src/lib/infrastructure/gatus.ts.hbs +112 -0
  68. package/templates/standard/dashboard/src/lib/services/proxy/index.ts +20 -0
  69. package/templates/standard/dashboard/src/lib/services/proxy/proxy.ts +244 -0
  70. package/templates/standard/dashboard/src/lib/services/proxy/types.ts +81 -0
  71. package/templates/standard/tests/integration/platform-sentinel.test.ts +497 -0
  72. package/templates/standard/tests/unit/cloudflare/alerting.test.ts +480 -0
  73. package/templates/standard/tests/unit/error-collector/dedup.test.ts +350 -0
  74. 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
+ });