@jeffjassky/telemetry 0.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.
@@ -0,0 +1,687 @@
1
+ /**
2
+ * Compile-only exercise of the public declarations. Never executed — `tsc
3
+ * --noEmit` failing here means the .d.ts files drifted from the source.
4
+ *
5
+ * Hand-written types rot within a day. On featureboard this file immediately
6
+ * caught that `types/` was missing FOUR features added the same afternoon.
7
+ * Every exported symbol must appear below. See standards/traps.md #9.
8
+ *
9
+ * THE RULE THAT MATTERS: a value export must be exercised AS A VALUE — read a
10
+ * property off it, call it, assign it to a typed binding. Naming a symbol in an
11
+ * `import type` proves only that some declaration exists; it says nothing about
12
+ * whether the declaration is a const the package ships or a bare type alias.
13
+ * That gap is exactly how `TelemetryKind`, `LogLevel`, `Env`, `Origin`,
14
+ * `KeyKind` and `TenantMode` shipped as runtime objects in `src/` and type-only
15
+ * unions in `types/` — a host writing `TelemetryKind.Usage` got working code
16
+ * that failed `tsc`, and this file compiled clean the whole time. Every entry
17
+ * point's export list is walked below; if a symbol is exported and not here, it
18
+ * is drift waiting to happen.
19
+ */
20
+ import { z } from 'zod';
21
+ import type {
22
+ AttrsOf,
23
+ Checkpoint,
24
+ ClientContext,
25
+ CreateTelemetryConfig,
26
+ DimSource,
27
+ EmitBase,
28
+ EmitInput,
29
+ EmitResult,
30
+ EntityRef,
31
+ EventSpec,
32
+ ForgetResult,
33
+ Logger,
34
+ MetricsOf,
35
+ Registry,
36
+ RollupSpec,
37
+ Scoped,
38
+ SubjectInput,
39
+ Telemetry,
40
+ TelemetryCounters,
41
+ } from './index.js';
42
+ // VALUE imports — the vocabulary ships as `const` objects, so importing these
43
+ // with `import type` would have hidden the very drift this file exists to catch
44
+ import {
45
+ boundedMeta,
46
+ createTelemetry,
47
+ defineRegistry,
48
+ newId,
49
+ plain,
50
+ resolveDim,
51
+ traceKeep,
52
+ truncate,
53
+ validateRegistry,
54
+ isPlatformScope,
55
+ BODY_MAX_CHARS,
56
+ Env,
57
+ INDEX_BUDGET,
58
+ LogLevel,
59
+ Origin,
60
+ PLATFORM_SCOPE,
61
+ RETENTION_DAYS,
62
+ SAMPLE_RATE,
63
+ SCHEMA_VERSION,
64
+ TelemetryKind,
65
+ } from './index.js';
66
+
67
+ // ── the registry keeps literal shapes through defineRegistry ──
68
+ const registry = defineRegistry({
69
+ 'user.signed_up': {
70
+ kind: 'event',
71
+ origin: 'server',
72
+ subjects: ['user', 'org'],
73
+ attrs: z.object({ source: z.string().max(64), plan: z.enum(['free', 'pro']) }),
74
+ indexedAttrs: ['source'],
75
+ rollups: [
76
+ { by: ['subject'], subjects: ['user'], actors: ['user', 'system'], capture: ['attr:source'] },
77
+ { as: 'activity', by: ['subject'], subjects: ['user'], bucket: 'day', retentionDays: 730 },
78
+ ],
79
+ description: 'Account created',
80
+ },
81
+ 'llm.completion': {
82
+ kind: 'span',
83
+ origin: 'server',
84
+ subjects: ['org'],
85
+ attrs: z.object({ gen_ai_request_model: z.string(), feature: z.string() }),
86
+ metrics: z.object({ tokens_in: z.number(), tokens_out: z.number(), cost_usd: z.number() }),
87
+ data: boundedMeta(),
88
+ indexedMetrics: ['cost_usd'],
89
+ retentionDays: 400,
90
+ durable: true,
91
+ rollups: [{
92
+ as: 'llm_cost',
93
+ by: ['attr:gen_ai_request_model', 'attr:feature'],
94
+ bucket: 'day',
95
+ sum: ['cost_usd'],
96
+ dimDefault: 'none',
97
+ }],
98
+ description: 'Single model call',
99
+ },
100
+ });
101
+
102
+ declare const mongooseish: CreateTelemetryConfig['connection'];
103
+
104
+ const t = createTelemetry({
105
+ registry,
106
+ connection: mongooseish,
107
+ pepper: 'p',
108
+ platforms: ['watchos'], // EXTENDS the builtins; 'web' still validates
109
+ bodyMax: 4096,
110
+ globalSubjectRefs: true, // refs name one party in every tenant — forget() reaches '*' views
111
+ });
112
+
113
+ // ── emit is typed against the registry ──
114
+ async function writes() {
115
+ await t.emit('user.signed_up', {
116
+ tenantId: 'acc_9',
117
+ subjects: [{ type: 'user', id: 'u_1' }, { type: 'org', id: 'o_9' }],
118
+ actor: 'user:u_1',
119
+ attrs: { source: 'ads', plan: 'pro' },
120
+ });
121
+
122
+ // the durability contract is in the return type — 'written' vs 'queued'
123
+ const queued: EmitResult = await t.emit('llm.completion', {
124
+ tenantId: 'acc_9',
125
+ subjects: [{ type: 'org', id: 'o_9' }],
126
+ traceId: 'tr_1', spanId: 's_1', durationMs: 1900,
127
+ attrs: { gen_ai_request_model: 'claude-opus-5', feature: 'chat' },
128
+ metrics: { tokens_in: 1, tokens_out: 1, cost_usd: 0.04 },
129
+ });
130
+ const correlationId: string = queued.id;
131
+ if (queued.outcome === 'deduped') void correlationId;
132
+
133
+ // idempotent + awaited, per call
134
+ const { outcome } = await t.emit('user.signed_up', {
135
+ tenantId: 'acc_9',
136
+ subjects: [{ type: 'user', id: 'u_1' }, { type: 'org', id: 'o_9' }],
137
+ attrs: { source: 'ads', plan: 'pro' },
138
+ dedupeKey: 'stripe:evt_123',
139
+ durable: true,
140
+ });
141
+ const outcomes: EmitResult['outcome'][] =
142
+ ['written', 'queued', 'deduped', 'sampled', 'capped', 'rejected'];
143
+ void outcomes.includes(outcome);
144
+
145
+ // the platform union stays open — builtins autocomplete, host additions compile
146
+ const client: ClientContext = { platform: 'watchos', appVersion: '1.0.0' };
147
+ const builtin: ClientContext = { platform: 'web', appVersion: '1.0.0' };
148
+ void client, builtin;
149
+
150
+ // @ts-expect-error — unknown event name is a compile error, not a silent drop
151
+ await t.emit('user.typo', { tenantId: 'acc_9' });
152
+
153
+ await t.emit('user.signed_up', {
154
+ tenantId: 'acc_9',
155
+ // @ts-expect-error — attrs are typed per event; `plan: 'gold'` is not in the enum
156
+ attrs: { source: 'ads', plan: 'gold' },
157
+ });
158
+ }
159
+
160
+ // ── the rest of the surface ──
161
+ async function reads() {
162
+ const s: Scoped = t.scoped('acc_9');
163
+ s.find({ subjectKeys: 'user:u_1' });
164
+ s.aggregate([{ $match: { kind: 'span' } }]);
165
+ s.rollups({ as: 'llm_cost' });
166
+ s.rollupAggregate([{ $group: { _id: '$dims' } }]);
167
+
168
+ // scoped() takes a tenantId and only a tenantId — PLATFORM_SCOPE is not
169
+ // special here, it is just a string that matches no row
170
+ const literal: Scoped = t.scoped(PLATFORM_SCOPE);
171
+ literal.find();
172
+ const platform: boolean = isPlatformScope(PLATFORM_SCOPE);
173
+ const notPlatform: boolean = isPlatformScope('acc_9');
174
+ void (platform && notPlatform);
175
+
176
+ const gone: ForgetResult = await t.forget('acc_9', 'user:u_1');
177
+ void (gone.deleted + gone.redacted + gone.rollups + gone.aliases);
178
+
179
+ const cp: Checkpoint = t.checkpoint('mailery-bridge');
180
+ const mark: Date | null = await cp.get();
181
+ await cp.advance(mark ?? new Date());
182
+
183
+ await t.syncIndexes();
184
+ await t.flush();
185
+
186
+ const c: TelemetryCounters = t.counters;
187
+ void (c.rejected + c.defaulted + c.sampled + c.capped + c.rollupSkipped + c.deduped + c.truncated);
188
+
189
+ t.models.telemetry.find();
190
+ t.models.byKind.usage.countDocuments();
191
+ t.models.rollups.aggregate([]);
192
+ t.models.checkpoints.findOne();
193
+ t.collections.rejects().countDocuments();
194
+ t.collections.aliases().deleteMany({});
195
+ }
196
+
197
+ // ── helpers keep their contracts ──
198
+ const id: string = newId();
199
+ const kept: boolean = traceKeep('tr_00ff', 0.5);
200
+ const day: Date | undefined = truncate(new Date(), 'day');
201
+ const obj: unknown = plain(new Map());
202
+ /** the rollup dimension resolver, exported so burst keys and host state agree */
203
+ const resolved: unknown = resolveDim('attr:source', { attrs: new Map([['source', 'ads']]) });
204
+ const noSubjectDim: unknown = resolveDim('subject', {});
205
+ void resolved, noSubjectDim;
206
+ validateRegistry(registry);
207
+ const budget: number = INDEX_BUDGET;
208
+ const bodyCap: number = BODY_MAX_CHARS;
209
+ const spanDays: number | null = RETENTION_DAYS.span;
210
+ const rate: number = SAMPLE_RATE.usage;
211
+ const v: number = SCHEMA_VERSION;
212
+
213
+ // ── the vocabulary, in BOTH positions ──
214
+ // Each of these ships as a `const` object AND a type. A host writing
215
+ // `TelemetryKind.Usage` must compile; so must `const k: TelemetryKind`. Reading
216
+ // the member off the object is what proves the value exists — an `import type`
217
+ // of the same name proves nothing, which is how these six drifted.
218
+ const usageKind: TelemetryKind = TelemetryKind.Usage;
219
+ const everyKind: TelemetryKind[] = [
220
+ TelemetryKind.Event, TelemetryKind.Error, TelemetryKind.Span,
221
+ TelemetryKind.State, TelemetryKind.Usage,
222
+ ];
223
+ const fatal: LogLevel = LogLevel.Fatal;
224
+ const everyLevel: LogLevel[] = [LogLevel.Debug, LogLevel.Info, LogLevel.Warn, LogLevel.Error, LogLevel.Fatal];
225
+ const prod: Env = Env.Prod;
226
+ const everyEnv: Env[] = [Env.Prod, Env.Staging, Env.Dev];
227
+ const server: Origin = Origin.Server;
228
+ const everyOrigin: Origin[] = [Origin.Server, Origin.Client];
229
+ // the literal form keeps working — the const is an addition, never a narrowing
230
+ const literalKind: TelemetryKind = 'span';
231
+ const literalLevel: LogLevel = 'warn';
232
+ const literalEnv: Env = 'dev';
233
+ const literalOrigin: Origin = 'client';
234
+ // and the objects index the Records the package exports
235
+ const kindRetention: number | null = RETENTION_DAYS[TelemetryKind.Usage];
236
+ const kindRate: number = SAMPLE_RATE[TelemetryKind.Error];
237
+ void usageKind, everyKind, fatal, everyLevel, prod, everyEnv, server, everyOrigin;
238
+ void literalKind, literalLevel, literalEnv, literalOrigin, kindRetention, kindRate;
239
+
240
+ // the envelope base and its parts, standalone
241
+ const subject: SubjectInput = { type: 'user', id: 'u_1', role: 'sender' };
242
+ const base: EmitBase = {
243
+ tenantId: 'acc_9',
244
+ subjects: [subject],
245
+ actor: 'user:u_1',
246
+ onBehalfOf: 'system:cron',
247
+ occurredAt: new Date(),
248
+ severity: LogLevel.Warn,
249
+ service: 'api',
250
+ release: 'app@1.0.0',
251
+ env: Env.Staging,
252
+ origin: Origin.Server,
253
+ traceId: 'tr_1', spanId: 's_1', parentId: 'p_1', durationMs: 12,
254
+ data: { note: 'x' },
255
+ body: 'prose',
256
+ forceKeep: true,
257
+ dedupeKey: 'stripe:evt_1',
258
+ durable: true,
259
+ error: { type: 'TypeError', message: 'x', handled: false, fingerprint: 'fp', frames: [{ fn: 'f', inApp: true }] },
260
+ state: { key: 'lifecycle', from: 'trial', to: 'active', previousSinceMs: 10 },
261
+ usage: {
262
+ meter: 'tokens', quantity: 1, unit: 'token', amount: '0.04', currency: 'USD',
263
+ idempotencyKey: 'tr_1:s_1', billedTo: 'org:o_9', billable: true,
264
+ priceVersion: 'v1', reverses: 'rec_1',
265
+ },
266
+ };
267
+ void base;
268
+
269
+ // the per-event payload projections the typed emit is built from
270
+ const signupAttrs: AttrsOf<typeof registry, 'user.signed_up'> = { source: 'ads', plan: 'pro' };
271
+ const llmMetrics: MetricsOf<typeof registry, 'llm.completion'> = { tokens_in: 1, tokens_out: 1, cost_usd: 0.04 };
272
+ void signupAttrs, llmMetrics;
273
+
274
+ // ── keys + ingest surface ──
275
+ import type {
276
+ ContextAdapter,
277
+ CreateIngestOptions,
278
+ CreateKeyInput,
279
+ IngestContext,
280
+ ParsedKey,
281
+ } from './index.js';
282
+ import {
283
+ createIngest, createKey, hashSecret, parseKeyString, verifySecret, KeyKind, TenantMode,
284
+ } from './index.js';
285
+
286
+ async function keys() {
287
+ const minted = await t.createKey({
288
+ kind: 'publishable',
289
+ tenantMode: 'fixed',
290
+ tenantId: 'acc_9',
291
+ service: 'web',
292
+ env: 'prod',
293
+ origins: ['https://app.example.com'],
294
+ });
295
+ const full: string = minted.key;
296
+ const parsed: ParsedKey | null = parseKeyString(full);
297
+ const kk: KeyKind = parsed!.kind;
298
+ // both positions again — const object AND union, for the two key enums too
299
+ const publishable: KeyKind = KeyKind.Publishable;
300
+ const secret: KeyKind = KeyKind.Secret;
301
+ const tm: TenantMode = 'claimed';
302
+ const modes: TenantMode[] = [TenantMode.Fixed, TenantMode.Session, TenantMode.Claimed];
303
+ const ok: boolean = verifySecret('s', hashSecret('s'));
304
+ await createKey(t.models.keys, {
305
+ kind: KeyKind.Secret, tenantMode: TenantMode.Claimed, service: 'api', env: 'prod',
306
+ label: 'live', origins: [], allowedKinds: ['usage'], allowedNames: ['llm.completion'],
307
+ maxPerMinute: 600,
308
+ } satisfies CreateKeyInput);
309
+ void kk, publishable, secret, tm, modes, ok;
310
+ }
311
+
312
+ const adapter: ContextAdapter = {
313
+ resolveContext: () => ({ tenantId: 'acc_9', subjects: [{ type: 'user', id: 'u_1' }], actor: 'user:u_1' } satisfies IngestContext),
314
+ };
315
+ const ingestOpts: CreateIngestOptions = { telemetry: t, contextAdapter: adapter, maxRecords: 50 };
316
+ const ingestRouter = createIngest(ingestOpts);
317
+
318
+ // ── client core (types/core.d.ts) ──
319
+ import type {
320
+ ClientContextInput,
321
+ CreateClientOptions,
322
+ ClientStorage,
323
+ DimSource as CoreDimSource,
324
+ EventSpec as CoreEventSpec,
325
+ Registry as CoreRegistry,
326
+ RollupSpec as CoreRollupSpec,
327
+ Span as CoreSpan,
328
+ TelemetryClient as CoreClient,
329
+ TrackOptions,
330
+ Transport,
331
+ TransportResult,
332
+ WireRecord,
333
+ } from './core.js';
334
+ import {
335
+ createClient as createCoreClient,
336
+ newId as coreNewId,
337
+ defineRegistry as coreDefineRegistry,
338
+ boundedMeta as coreBoundedMeta,
339
+ } from './core.js';
340
+
341
+ // /core re-exports the isomorphic registry surface so a host's registry module
342
+ // imports from here and stays mongoose-free
343
+ const coreReg: CoreRegistry = coreDefineRegistry({
344
+ 'a.b': {
345
+ kind: TelemetryKind.Event,
346
+ origin: Origin.Client,
347
+ subjects: [],
348
+ data: coreBoundedMeta(),
349
+ description: 'x',
350
+ } satisfies CoreEventSpec,
351
+ });
352
+ const coreRoll: CoreRollupSpec = { by: ['attr:source' satisfies CoreDimSource], bucket: 'day' };
353
+ const coreId: string = coreNewId();
354
+ void coreReg, coreRoll, coreId;
355
+
356
+ const transport: Transport = async () => ({ ok: true } satisfies TransportResult);
357
+ const storage: ClientStorage = { get: () => null, set: () => {} };
358
+ const ctxInput: ClientContextInput = { platform: 'web', appVersion: '1.0.0', online: true };
359
+ const clientOpts: CreateClientOptions = {
360
+ key: 'pk_live_tk_000000000000000000000000',
361
+ url: 'https://app.example.com/telemetry/ingest',
362
+ release: 'app@1.0.0',
363
+ flushIntervalMs: 5_000,
364
+ maxBatchSize: 50,
365
+ maxQueueSize: 1_000,
366
+ maxRetries: 5,
367
+ transport,
368
+ storage,
369
+ clientContext: ctxInput,
370
+ consent: () => true,
371
+ errorName: 'error.unhandled',
372
+ onError: () => {},
373
+ };
374
+ const trackOpts: TrackOptions<{ source: string }, { n: number }> = {
375
+ attrs: { source: 'ads' },
376
+ metrics: { n: 1 },
377
+ data: {},
378
+ occurredAt: new Date(),
379
+ subjects: [{ type: 'user', id: 'u_1' }],
380
+ severity: LogLevel.Info,
381
+ };
382
+ void clientOpts, trackOpts;
383
+ const c: CoreClient<typeof registry> = createCoreClient<typeof registry>({
384
+ key: 'pk_live_tk_000000000000000000000000',
385
+ url: 'https://app.example.com/telemetry/ingest',
386
+ release: 'app@1.0.0',
387
+ transport,
388
+ });
389
+ c.track('user.signed_up', { attrs: { source: 'ads', plan: 'pro' } });
390
+ // @ts-expect-error — typo'd names are compile errors in clients too
391
+ c.track('user.typo');
392
+ c.identify({ user: 'u_1', org: 'o_9' });
393
+ c.captureError(new Error('x'), { handled: false });
394
+ const span: CoreSpan = c.startSpan('pdf.render');
395
+ const traced: string = span.traceId;
396
+ const spanned: string = span.spanId;
397
+ span.end({ metrics: { bytes: 100 } });
398
+ void traced, spanned;
399
+ c.state('account.lifecycle', { key: 'lifecycle', to: 'active' });
400
+ c.setActor('user:u_1');
401
+ c.setActor(undefined);
402
+ const rec: WireRecord = { _id: 'x'.repeat(16), name: 'a', occurredAt: new Date().toISOString() };
403
+ // `_internal` exists on every shipped client — the platform adapters use it —
404
+ // so the declaration says so, opaquely. Reading a field off it must be a cast,
405
+ // which is the whole point of typing it `unknown`.
406
+ const internals: unknown = c._internal;
407
+ // @ts-expect-error — opaque on purpose: no member of `_internal` is contract
408
+ void c._internal.queue;
409
+ void internals;
410
+ async function drain() {
411
+ await c.flush();
412
+ await c.shutdown();
413
+ }
414
+
415
+ // ── dashboard surface ──
416
+ import type {
417
+ CohortSubject,
418
+ CreateDashboardOptions,
419
+ FunnelCohortWindow,
420
+ FunnelExitResult,
421
+ FunnelParams,
422
+ FunnelResult,
423
+ FunnelSlice,
424
+ FunnelStageResult,
425
+ FunnelStageSpec,
426
+ Queries,
427
+ QueryLimits,
428
+ RecordFilter,
429
+ ResolvedView,
430
+ SubjectAdapter,
431
+ TimeRange,
432
+ Viewer,
433
+ ViewerAdapter,
434
+ ViewSpec,
435
+ } from './index.js';
436
+ import {
437
+ createDashboard, createQueries, defaultSpaDir, deriveViews, findFamily,
438
+ median, requireMilestoneFamily, summarizeStages, DEFAULT_LIMITS,
439
+ } from './index.js';
440
+
441
+ const viewer: Viewer = { tenantId: 'acc_9', role: 'admin', viewerRef: 'user:u_1' };
442
+ /** the platform viewer — the host authorized it, the package only expresses it */
443
+ const platformViewer: Viewer = { tenantId: PLATFORM_SCOPE, role: 'admin', viewerRef: 'user:u_ops' };
444
+ const viewerAdapter: ViewerAdapter = {
445
+ resolveViewer: () => (isPlatformScope(viewer.tenantId) ? platformViewer : viewer),
446
+ };
447
+ const subjectAdapter: SubjectAdapter = {
448
+ describe: async (refs) => Object.fromEntries(refs.map((r) => [r, { label: r }])),
449
+ };
450
+ const view: ViewSpec = {
451
+ name: 'Checkout errors',
452
+ // the sidebar renders this when present and falls back to the origin badge
453
+ icon: '⚑',
454
+ page: 'errors',
455
+ query: { range: '24h', filters: { severity: 'error' }, display: 'table' },
456
+ };
457
+ const dashOpts: CreateDashboardOptions = {
458
+ telemetry: t,
459
+ viewerAdapter,
460
+ subjectAdapter,
461
+ views: [view],
462
+ queryLimits: { records: 100 },
463
+ onSlowQuery: ({ op, ms }) => void `${op}:${ms}`,
464
+ // the threshold the callback fires above, and the query cache — all three
465
+ // forwarded to createQueries, none of them reachable before
466
+ slowMs: 250,
467
+ cacheTtlMs: 30_000,
468
+ cacheSize: 200,
469
+ mountPath: '/telemetry',
470
+ apiBase: '/telemetry',
471
+ title: 'Telemetry',
472
+ spaDir: '/srv/ui',
473
+ };
474
+ const dashRouter = createDashboard(dashOpts);
475
+ const spa: string = defaultSpaDir();
476
+ const derived: ResolvedView[] = deriveViews(registry);
477
+ const caps: QueryLimits = DEFAULT_LIMITS;
478
+
479
+ async function primitives() {
480
+ const q: Queries = createQueries({
481
+ TelemetryModel: t.models.telemetry,
482
+ RollupModel: t.models.rollups,
483
+ registry,
484
+ limits: { records: 100 },
485
+ onSlowQuery: ({ op, ms, params }) => void `${op}:${ms}:${String(params)}`,
486
+ slowMs: 250,
487
+ // the in-process cache is tunable — a ten-minute TTL is a default, not a law
488
+ cacheTtlMs: 30_000,
489
+ cacheSize: 200,
490
+ });
491
+ const range: TimeRange = { from: new Date(0), to: new Date() };
492
+ const f: RecordFilter = { kind: 'span', excludeActorTypes: ['admin'] };
493
+ const page = await q.records('acc_9', range, f, { limit: 50 });
494
+ const next: string | null = page.nextCursor;
495
+ const ser = await q.series('acc_9', range, f, { measure: 'sum:cost_usd', interval: 'day' });
496
+ void ser.buckets[0]?.value;
497
+ // the sample is complete; the computation is capped, and says so
498
+ const dist = await q.distribution('acc_9', range, f);
499
+ const scanCut: boolean = dist.truncated;
500
+ void scanCut;
501
+ void caps.distribution;
502
+ const ro = await q.rollups('acc_9', { as: 'llm_cost', sort: 'bucketAt' });
503
+ const src: 'rollups' = ro.dataSource;
504
+ const short: boolean = ro.truncated;
505
+ // the multi-subject read and the explicit cohort field, both new
506
+ await q.rollups('acc_9', { as: 'user.signed_up', dims: ['user:u_1', 'user:u_2'], on: 'firstAt', range });
507
+ await q.trace('acc_9', 'tr_1');
508
+ await q.journey('acc_9', 'user:u_1', range);
509
+ void short;
510
+
511
+ // ── the two cohort primitives ──
512
+ const dau = await q.distinctCount('acc_9', { as: 'activity', range, interval: 'day' });
513
+ const distinct: number = dau.distinct;
514
+ const grain: 'hour' | 'day' | 'week' | 'month' = dau.interval;
515
+ const perBucket: number = dau.buckets[0]?.value ?? 0;
516
+ void (distinct + perBucket), grain, dau.truncated;
517
+
518
+ const funnelParams: FunnelParams = {
519
+ stages: [{ as: 'user.signed_up' }, { as: 'activated', label: 'Activated' }],
520
+ anchor: 'user.signed_up',
521
+ // half-open by default; the closed form maxed uses is opt-in and named
522
+ cohort: { from: range.from, to: range.to, endInclusive: true } satisfies FunnelCohortWindow,
523
+ exits: [{ as: 'churned' }],
524
+ subjectType: 'user',
525
+ interval: 'week',
526
+ limit: 1_000,
527
+ };
528
+ const fun: FunnelResult = await q.funnel('acc_9', funnelParams);
529
+ const stage: FunnelStageResult = fun.stages[0]!;
530
+ const pctPrev: number | null = stage.pctOfPrevious;
531
+ const fromAnchor: number | null = stage.medianDaysFromAnchor;
532
+ const missed: number = stage.notReached;
533
+ const stalled: number | null = stage.stalledAt; // null on the terminal stage
534
+ const exit: FunnelExitResult = fun.exits[0]!;
535
+ const slice: FunnelSlice | undefined = fun.slices?.[0];
536
+ void (fun.cohortSubjects + fun.first + exit.subjects + missed);
537
+ void pctPrev, fromAnchor, stalled, slice?.at, fun.truncated, fun.cohort.anchor, fun.dataSource;
538
+
539
+ // every primitive takes the platform scope in the same position a tenantId
540
+ // goes — that IS the API: one argument, two meanings, no second entry point
541
+ await q.records(PLATFORM_SCOPE, range, f);
542
+ await q.series(PLATFORM_SCOPE, range, f);
543
+ await q.distribution(PLATFORM_SCOPE, range, f);
544
+ await q.rollups(PLATFORM_SCOPE, { as: 'llm_cost' });
545
+ await q.trace(PLATFORM_SCOPE, 'tr_1');
546
+ await q.journey(PLATFORM_SCOPE, 'user:u_1', range);
547
+ await q.distinctCount(PLATFORM_SCOPE, { as: 'activity', range });
548
+ await q.funnel(PLATFORM_SCOPE, { stages: [{ as: 'user.signed_up' }], cohort: range });
549
+ }
550
+
551
+ // ── the funnel math, usable without a database ──
552
+ const mid: number | null = median([3, 3, 4, 5]); // 3.5 — mean of the two middles
553
+ const empty: number | null = median([]); // null, never 0
554
+ const cohortIndex: CohortSubject[] = [
555
+ { ref: 'user:u_1', anchorAt: new Date(), stages: { signed_up: new Date() }, exits: {} },
556
+ { ref: 'user:u_2', anchorAt: null, stages: {}, exits: { churned: new Date() } },
557
+ ];
558
+ const stageSpec: FunnelStageSpec = { as: 'user.signed_up', key: 'signed_up', label: 'Signed up' };
559
+ const table: FunnelStageResult[] = summarizeStages(cohortIndex, [
560
+ { order: 1, key: stageSpec.key!, as: stageSpec.as, label: stageSpec.label! },
561
+ ]);
562
+ const family = findFamily(registry, 'llm_cost');
563
+ const milestoneSpec: RollupSpec = requireMilestoneFamily(registry, 'user.signed_up', 'funnel()');
564
+ void mid, empty, table[0]?.subjects, family?.spec, family?.name, milestoneSpec.by;
565
+
566
+ // forget() now reports views too
567
+ async function forgetViews() {
568
+ const gone = await t.forget('acc_9', 'user:u_1');
569
+ const v: number = gone.views;
570
+ }
571
+
572
+ // ── shapes are exported and usable standalone ──
573
+ const dim: DimSource = 'attr:source';
574
+ const roll: RollupSpec = { by: [dim], bucket: 'week', actors: ['user'], dimDefault: 'unset' };
575
+ const spec: EventSpec = { kind: 'event', origin: 'any', subjects: [], durable: true, description: 'x' };
576
+ const reg: Registry = { 'a.b': spec };
577
+ const ref: EntityRef = 'user:u_1';
578
+ const kind: TelemetryKind = 'usage';
579
+ const log: Logger = { info() {}, warn() {}, error() {} };
580
+ declare const generic: Telemetry;
581
+ const input: EmitInput<typeof registry, 'user.signed_up'> = {
582
+ tenantId: 'a',
583
+ attrs: { source: 's', plan: 'free' },
584
+ };
585
+
586
+ // ── the platform entry points ──
587
+ // Six subpaths, six declaration files, and until now this file compiled none of
588
+ // them. Each entry's FULL export list is exercised below — every re-exported
589
+ // `createClient` under its own alias, every const as a value, every factory
590
+ // called. A subpath whose exports are not walked here is a subpath that can
591
+ // drift without the gate noticing.
592
+ import type * as React from 'react';
593
+
594
+ import type { TelemetryClient as WebClient, WebTelemetryOptions } from './web.js';
595
+ import { createClient as createWebCoreClient, createWebTelemetry } from './web.js';
596
+
597
+ import type { TelemetryClient as ReactClient } from './react.js';
598
+ import {
599
+ createClient as createReactCoreClient,
600
+ TelemetryProvider,
601
+ TelemetryErrorBoundary,
602
+ useTelemetry as useReactTelemetry,
603
+ } from './react.js';
604
+
605
+ import type { TelemetryClient as VueClient } from './vue.js';
606
+ import {
607
+ createClient as createVueCoreClient,
608
+ createTelemetryPlugin,
609
+ useTelemetry as useVueTelemetry,
610
+ TELEMETRY_KEY,
611
+ } from './vue.js';
612
+
613
+ import type { MainTelemetryOptions, TelemetryClient as ElectronClient } from './electron.js';
614
+ import {
615
+ createClient as createElectronCoreClient,
616
+ createMainTelemetry,
617
+ createRendererTelemetry,
618
+ IPC_CHANNEL,
619
+ } from './electron.js';
620
+
621
+ import type { CliTelemetryOptions, TelemetryClient as CliClient } from './cli.js';
622
+ import { createClient as createCliCoreClient, createCliTelemetry } from './cli.js';
623
+
624
+ // every subpath re-exports the core factory — same shape, six import paths
625
+ const reexported: Array<typeof createCoreClient> = [
626
+ createWebCoreClient, createReactCoreClient, createVueCoreClient,
627
+ createElectronCoreClient, createCliCoreClient,
628
+ ];
629
+ void reexported;
630
+
631
+ // /web — consent is re-added on top of the Omit, DNT/GPC still win
632
+ const webOpts: WebTelemetryOptions = {
633
+ key: 'pk_live_tk_000000000000000000000000',
634
+ url: '/telemetry/ingest',
635
+ release: 'app@1.0.0',
636
+ consent: () => true,
637
+ captureGlobalErrors: true,
638
+ };
639
+ const web: WebClient<typeof registry> = createWebTelemetry<typeof registry>(webOpts);
640
+ web.track('user.signed_up', { attrs: { source: 'ads', plan: 'pro' } });
641
+
642
+ // /react — the provider, the hook, the boundary
643
+ const plainClient: ReactClient = createReactCoreClient({ key: 'pk_x', url: '/i', transport });
644
+ const providerEl: React.ReactElement = TelemetryProvider({ client: plainClient });
645
+ const boundary = new TelemetryErrorBoundary({
646
+ client: plainClient,
647
+ fallback: (e: Error) => e.message,
648
+ });
649
+ const hooked: ReactClient = useReactTelemetry();
650
+ void providerEl, boundary, hooked;
651
+
652
+ // /vue — the injection key is a value, and the plugin installs onto an app
653
+ const vueClient: VueClient = createVueCoreClient({ key: 'pk_x', url: '/i', transport });
654
+ const key: 'telemetry' = TELEMETRY_KEY;
655
+ const plugin = createTelemetryPlugin(vueClient);
656
+ plugin.install({ config: { errorHandler: () => {} }, provide: () => {} });
657
+ const injected: VueClient = useVueTelemetry(() => vueClient);
658
+ void key, injected;
659
+
660
+ // /electron — main owns the only real queue, the renderer rides IPC
661
+ const channel: 'telemetry:batch' = IPC_CHANNEL;
662
+ const mainOpts: MainTelemetryOptions = {
663
+ key: 'sk_live_tk_000000000000000000000000',
664
+ url: 'https://app.example.com/telemetry/ingest',
665
+ captureProcessErrors: true,
666
+ ipcMain: { handle: () => {} },
667
+ };
668
+ const main: ElectronClient = createMainTelemetry(mainOpts);
669
+ const renderer: ElectronClient = createRendererTelemetry(
670
+ { invoke: async () => ({ ok: true }) },
671
+ { release: 'app@1.0.0' },
672
+ );
673
+ // key/url/transport are Omitted on the renderer — they never leave main
674
+ // @ts-expect-error — the renderer must not be handed a key
675
+ createRendererTelemetry({ invoke: async () => ({}) }, { key: 'sk_leak' });
676
+ void channel, main, renderer;
677
+
678
+ // /cli — disk-backed queue, opt-out honoured hard
679
+ const cliOpts: CliTelemetryOptions = {
680
+ key: 'sk_live_tk_000000000000000000000000',
681
+ url: 'https://app.example.com/telemetry/ingest',
682
+ configDir: '~/.config/mytool',
683
+ argv: ['node', 'mytool', '--no-telemetry'],
684
+ maxQueueAgeMs: 7 * 864e5,
685
+ };
686
+ const cli: CliClient = createCliTelemetry(cliOpts);
687
+ void cli;