@jeffjassky/telemetry 0.3.0 → 0.5.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/types/test-d.ts CHANGED
@@ -34,8 +34,10 @@ import type {
34
34
  MetricsOf,
35
35
  Registry,
36
36
  RollupSpec,
37
+ LinkSubjects,
37
38
  Scoped,
38
39
  SubjectInput,
40
+ SubjectLinker,
39
41
  Telemetry,
40
42
  TelemetryCounters,
41
43
  } from './index.js';
@@ -61,6 +63,8 @@ import {
61
63
  RETENTION_DAYS,
62
64
  SAMPLE_RATE,
63
65
  SCHEMA_VERSION,
66
+ SUBJECT_LINK_TIMEOUT_MS,
67
+ SUBJECT_MAX,
64
68
  TelemetryKind,
65
69
  } from './index.js';
66
70
 
@@ -101,6 +105,15 @@ const registry = defineRegistry({
101
105
 
102
106
  declare const mongooseish: CreateTelemetryConfig['connection'];
103
107
 
108
+ /** WRITE-time: the desktop's machine ref, resolved to the account that owns it */
109
+ const subjectLinker: SubjectLinker = {
110
+ link: async (subjects, { name, tenantId }) => {
111
+ void name, tenantId;
112
+ const machine = subjects.find((s) => s.type === 'machine');
113
+ return machine ? [{ type: 'user', id: `u_${machine.id}`, role: 'owner' }] : [];
114
+ },
115
+ };
116
+
104
117
  const t = createTelemetry({
105
118
  registry,
106
119
  connection: mongooseish,
@@ -108,8 +121,15 @@ const t = createTelemetry({
108
121
  platforms: ['watchos'], // EXTENDS the builtins; 'web' still validates
109
122
  bodyMax: 4096,
110
123
  globalSubjectRefs: true, // refs name one party in every tenant — forget() reaches '*' views
124
+ subjectLinker,
125
+ subjectLinkTimeoutMs: SUBJECT_LINK_TIMEOUT_MS,
111
126
  });
112
127
 
128
+ // the guarded linker the instance resolved, shared with the router factories
129
+ const linkSubjects: LinkSubjects | null = t.linkSubjects;
130
+ const subjectCap: number = SUBJECT_MAX;
131
+ void linkSubjects, subjectCap;
132
+
113
133
  // ── emit is typed against the registry ──
114
134
  async function writes() {
115
135
  await t.emit('user.signed_up', {
@@ -185,6 +205,10 @@ async function reads() {
185
205
 
186
206
  const c: TelemetryCounters = t.counters;
187
207
  void (c.rejected + c.defaulted + c.sampled + c.capped + c.rollupSkipped + c.deduped + c.truncated);
208
+ // the same drops, attributed — `${family}|${dim}` and `${name}|${attrKey}`
209
+ const skippedBy: number | undefined = c.rollupSkippedBy['llm_cost|feature'];
210
+ const undeclared: number | undefined = c.undeclaredAttrs['llm.completion|codec'];
211
+ void skippedBy, undeclared;
188
212
 
189
213
  t.models.telemetry.find();
190
214
  t.models.byKind.usage.countDocuments();
@@ -412,6 +436,129 @@ async function drain() {
412
436
  await c.shutdown();
413
437
  }
414
438
 
439
+ // ── catalog (reports §3) ──
440
+ // Pure inference over the registry: every name a page would otherwise hardcode
441
+ // comes back typed, with its domain and whether reading it is a lookup.
442
+ import type {
443
+ Catalog, DeriveCatalogOptions, DimFacet, EventFacet, FamilyFacet, MeasureFacet,
444
+ RegistryProjection, RegistryProjectionEntry,
445
+ } from './index.js';
446
+ import { deriveCatalog, projectRegistry } from './index.js';
447
+
448
+ const catalogOpts: DeriveCatalogOptions = { platforms: ['watchos'] };
449
+ const catalog: Catalog = deriveCatalog(registry, catalogOpts);
450
+ const facet: EventFacet = catalog.events['llm.completion']!;
451
+ const familyFacet: FamilyFacet = catalog.families['llm_cost']!;
452
+ const envelopeDim: DimFacet = catalog.envelope[0]!;
453
+ const measure: MeasureFacet = facet.measures[0]!;
454
+ const exactly: string[] = measure.exactVia;
455
+ const grain: DimSource[] = familyFacet.by;
456
+ const lifetime: boolean = familyFacet.lifetime;
457
+ const domain: string[] | undefined = envelopeDim.values;
458
+ const namespaced: string[] = catalog.namespaces['llm'] ?? [];
459
+ const everySubject: string[] = catalog.subjectTypes;
460
+ const projection: RegistryProjection = projectRegistry(catalog);
461
+ const entry: RegistryProjectionEntry = projection['llm.completion']!;
462
+ const stillAttrKeys: string[] = entry.attrKeys;
463
+ void exactly, grain, lifetime, domain, namespaced, everySubject, stillAttrKeys;
464
+
465
+ // ── suggestions (reports §9) — the same inference, run backwards ──
466
+ import type { DeriveSuggestionsInput, Suggestion, TelemetryCounters as Counters } from './index.js';
467
+ import { COUNTER_OVERFLOW_KEY, MAX_SUGGESTIONS, deriveSuggestions } from './index.js';
468
+
469
+ declare const liveCounters: Counters;
470
+ const suggestInput: DeriveSuggestionsInput = {
471
+ counters: liveCounters,
472
+ catalog,
473
+ quarantine: [{ at: new Date(), name: 'video.exported', reason: 'unregistered event' }],
474
+ };
475
+ const suggestions: Suggestion[] = deriveSuggestions(suggestInput);
476
+ const suggestKind: Suggestion['kind'] = suggestions[0]?.kind ?? 'undeclared_attr';
477
+ const pasteable: string | undefined = suggestions[0]?.fix;
478
+ const capped: 50 = MAX_SUGGESTIONS;
479
+ const overflow: '(other)|(other)' = COUNTER_OVERFLOW_KEY;
480
+ void suggestKind, pasteable, capped, overflow;
481
+
482
+ // ── reports (reports §4, §6) ──
483
+ // The resolver is a value, and its return is a discriminated union: a caller
484
+ // that forgets to check `unavailable` cannot reach `primitive`.
485
+ import type {
486
+ LegacyQuery, Plan, PlanPrimitive, PlanShape, Report, ReportFilter, ReportRange,
487
+ ReportSource, ResolveOptions, Unavailable,
488
+ } from './index.js';
489
+ import type { ExecuteOptions, FoldedRollups, ReportResult, RollupDoc } from './index.js';
490
+ import {
491
+ executeReport, foldRollups, intervalForRange, normalizeQuery, parseReportQuery, rangeOf,
492
+ reportToQuery, resolveReport,
493
+ } from './index.js';
494
+
495
+ const reportSource: ReportSource = { family: 'llm_cost' };
496
+ const reportRange: ReportRange = { from: '2026-07-01T00:00:00Z', to: '2026-07-08T00:00:00Z' };
497
+ const reportFilter: ReportFilter = { dim: 'attr:gen_ai_request_model', op: 'in', value: ['opus', 'sonnet'] };
498
+ const report: Report = {
499
+ source: reportSource,
500
+ range: reportRange,
501
+ interval: 'day',
502
+ measure: 'sum:cost_usd',
503
+ groupBy: ['attr:gen_ai_request_model'],
504
+ filters: [reportFilter],
505
+ compare: 'previous',
506
+ };
507
+ const funnelReport: Report = {
508
+ source: { kind: 'event' },
509
+ range: '90d',
510
+ measure: 'funnel',
511
+ stages: ['account.signed_up', 'account.converted'],
512
+ anchor: 'account.signed_up',
513
+ exits: ['account.churned'],
514
+ subjectType: 'account',
515
+ };
516
+ const resolveOpts: ResolveOptions = { now: new Date(), limits: { breakdown: 20 } };
517
+ const planned: Plan | Unavailable = resolveReport(report, catalog, resolveOpts);
518
+ if ('unavailable' in planned) {
519
+ const refused: true = planned.unavailable;
520
+ const because: string = planned.why;
521
+ void refused, because;
522
+ } else {
523
+ const primitive: PlanPrimitive = planned.primitive;
524
+ const planArgs: unknown[] = planned.args;
525
+ const exactness: 'exact' | 'raw' | 'scan' = planned.exactness;
526
+ const shape: PlanShape | undefined = planned.shape;
527
+ const previous: unknown[] | undefined = planned.previous?.args;
528
+ void primitive, planArgs, exactness, shape, previous;
529
+ }
530
+ const legacy: LegacyQuery = { range: '7d', filters: { name: 'llm.completion' }, groupBy: 'attr:feature' };
531
+ const lifted: Report | null = normalizeQuery(legacy);
532
+ const window: TimeRange = rangeOf('7d', new Date());
533
+ const grainFor: 'hour' | 'day' | 'week' | 'month' = intervalForRange('90d');
534
+ void funnelReport, lifted, window, grainFor;
535
+
536
+ // a Report is a URL, and the two encoders are inverses. `filter` is the one key
537
+ // that can repeat, which is why the query value is a union rather than a string.
538
+ const asQuery: Record<string, string | string[]> = reportToQuery(report);
539
+ const backAgain: Report = parseReportQuery(asQuery);
540
+ void backAgain.source, backAgain.range;
541
+
542
+ // ── the executor (reports §6) ──
543
+ const execOpts: ExecuteOptions = {
544
+ now: new Date(),
545
+ limits: { rollups: 100 },
546
+ redact: (items) => items.map((r) => ({ ...r, data: '[redacted]' })),
547
+ };
548
+ async function runOne(q: Queries) {
549
+ const out: ReportResult = await executeReport(q, 'acc_9', report, catalog, execOpts);
550
+ const ranPlan: Plan = out.plan;
551
+ const store: 'raw' | 'rollups' | 'raw+rollups' = out.dataSource;
552
+ const before: unknown = out.previous;
553
+ void ranPlan, store, before, out.report, out.result;
554
+ }
555
+ // the fold is pure — same rows, same shape, no database
556
+ const foldShape: PlanShape = { groupBy: ['attr:gen_ai_request_model'], labels: ['gen_ai_request_model'], measure: 'sum:cost_usd' };
557
+ const docs: RollupDoc[] = [{ dims: ['gen_ai_request_model=opus'], count: 2, sums: { cost_usd: 4 } }];
558
+ const folded: FoldedRollups = foldRollups(docs, foldShape, false);
559
+ const foldedValue: number | undefined = folded.rows[0]?.value;
560
+ void runOne, foldedValue, folded.groups, folded.truncated, folded.dataSource;
561
+
415
562
  // ── dashboard surface ──
416
563
  import type {
417
564
  CohortSubject,
@@ -429,12 +576,15 @@ import type {
429
576
  ResolvedView,
430
577
  SubjectAdapter,
431
578
  TimeRange,
579
+ Values,
580
+ ValuesParams,
581
+ ValuesResult,
432
582
  Viewer,
433
583
  ViewerAdapter,
434
584
  ViewSpec,
435
585
  } from './index.js';
436
586
  import {
437
- createDashboard, createQueries, defaultSpaDir, deriveViews, findFamily,
587
+ createDashboard, createQueries, createValues, defaultSpaDir, deriveViews, findFamily,
438
588
  median, requireMilestoneFamily, summarizeStages, DEFAULT_LIMITS,
439
589
  } from './index.js';
440
590
 
@@ -452,7 +602,7 @@ const view: ViewSpec = {
452
602
  // the sidebar renders this when present and falls back to the origin badge
453
603
  icon: '⚑',
454
604
  page: 'errors',
455
- query: { range: '24h', filters: { severity: 'error' }, display: 'table' },
605
+ query: { source: { kind: 'error' }, range: '24h', filters: [{ dim: 'field:severity', op: 'eq', value: 'error' }] },
456
606
  };
457
607
  const dashOpts: CreateDashboardOptions = {
458
608
  telemetry: t,
@@ -523,6 +673,24 @@ async function primitives() {
523
673
  const next: string | null = page.nextCursor;
524
674
  const ser = await q.series('acc_9', range, f, { measure: 'sum:cost_usd', interval: 'day' });
525
675
  void ser.buckets[0]?.value;
676
+ // the cap is on GROUPS returned, so `truncated` means "there were more top
677
+ // groups", not "the scan stopped early"
678
+ const bd = await q.breakdown('acc_9', range, f, {
679
+ groupBy: ['attr:gen_ai_request_model', 'field:env'],
680
+ measure: 'sum:cost_usd',
681
+ interval: 'day',
682
+ limit: 10,
683
+ });
684
+ const dimValue: string | null = bd.rows[0]?.dims[0] ?? null;
685
+ const bucketAt: Date | undefined = bd.rows[0]?.at;
686
+ const groupCount: number = bd.groups;
687
+ // two flags, two axes: groups dropped vs. buckets dropped from a group shown
688
+ const missingBuckets: boolean = bd.bucketsTruncated;
689
+ void dimValue, bucketAt, groupCount, missingBuckets, bd.truncated, caps.breakdown;
690
+ // a name SET is one $in, not N reads — a namespace or a family is several names
691
+ await q.records('acc_9', range, { name: ['llm.completion', 'billing.ai_tokens'] });
692
+ // a span's duration is on the envelope, and the measure grammar knows it
693
+ await q.series('acc_9', range, f, { measure: 'avg:durationMs' });
526
694
  // the sample is complete; the computation is capped, and says so
527
695
  const dist = await q.distribution('acc_9', range, f);
528
696
  const scanCut: boolean = dist.truncated;
@@ -575,6 +743,29 @@ async function primitives() {
575
743
  await q.journey(PLATFORM_SCOPE, 'user:u_1', range);
576
744
  await q.distinctCount(PLATFORM_SCOPE, { as: 'activity', range });
577
745
  await q.funnel(PLATFORM_SCOPE, { stages: [{ as: 'user.signed_up' }], cohort: range });
746
+
747
+ // ── values: the lookup, not a primitive (reports §5) ──
748
+ const values: Values = createValues({
749
+ catalog,
750
+ TelemetryModel: t.models.telemetry,
751
+ RollupModel: t.models.rollups,
752
+ limits: { values: 50 },
753
+ });
754
+ const params: ValuesParams = {
755
+ dim: 'attr:gen_ai_request_model',
756
+ names: ['llm.completion'],
757
+ range,
758
+ limit: 20,
759
+ };
760
+ const domain: ValuesResult = await values('acc_9', params);
761
+ const observed: string[] = domain.values;
762
+ // counts are absent on the catalog answer — a declared enum has no tally
763
+ const tally: number[] | undefined = domain.counts;
764
+ const answeredBy: 'catalog' | 'rollups' | 'raw' | 'none' = domain.source;
765
+ const readFrom: string | undefined = domain.via;
766
+ void observed, tally, answeredBy, readFrom, domain.truncated, domain.dataSource, caps.values;
767
+ // no range is not an error: the raw step drops off and `none` says so
768
+ await values(PLATFORM_SCOPE, { dim: 'field:client.platform' });
578
769
  }
579
770
 
580
771
  // ── the funnel math, usable without a database ──