@jeffjassky/telemetry 0.2.0 → 0.4.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
@@ -185,6 +185,10 @@ async function reads() {
185
185
 
186
186
  const c: TelemetryCounters = t.counters;
187
187
  void (c.rejected + c.defaulted + c.sampled + c.capped + c.rollupSkipped + c.deduped + c.truncated);
188
+ // the same drops, attributed — `${family}|${dim}` and `${name}|${attrKey}`
189
+ const skippedBy: number | undefined = c.rollupSkippedBy['llm_cost|feature'];
190
+ const undeclared: number | undefined = c.undeclaredAttrs['llm.completion|codec'];
191
+ void skippedBy, undeclared;
188
192
 
189
193
  t.models.telemetry.find();
190
194
  t.models.byKind.usage.countDocuments();
@@ -412,6 +416,129 @@ async function drain() {
412
416
  await c.shutdown();
413
417
  }
414
418
 
419
+ // ── catalog (reports §3) ──
420
+ // Pure inference over the registry: every name a page would otherwise hardcode
421
+ // comes back typed, with its domain and whether reading it is a lookup.
422
+ import type {
423
+ Catalog, DeriveCatalogOptions, DimFacet, EventFacet, FamilyFacet, MeasureFacet,
424
+ RegistryProjection, RegistryProjectionEntry,
425
+ } from './index.js';
426
+ import { deriveCatalog, projectRegistry } from './index.js';
427
+
428
+ const catalogOpts: DeriveCatalogOptions = { platforms: ['watchos'] };
429
+ const catalog: Catalog = deriveCatalog(registry, catalogOpts);
430
+ const facet: EventFacet = catalog.events['llm.completion']!;
431
+ const familyFacet: FamilyFacet = catalog.families['llm_cost']!;
432
+ const envelopeDim: DimFacet = catalog.envelope[0]!;
433
+ const measure: MeasureFacet = facet.measures[0]!;
434
+ const exactly: string[] = measure.exactVia;
435
+ const grain: DimSource[] = familyFacet.by;
436
+ const lifetime: boolean = familyFacet.lifetime;
437
+ const domain: string[] | undefined = envelopeDim.values;
438
+ const namespaced: string[] = catalog.namespaces['llm'] ?? [];
439
+ const everySubject: string[] = catalog.subjectTypes;
440
+ const projection: RegistryProjection = projectRegistry(catalog);
441
+ const entry: RegistryProjectionEntry = projection['llm.completion']!;
442
+ const stillAttrKeys: string[] = entry.attrKeys;
443
+ void exactly, grain, lifetime, domain, namespaced, everySubject, stillAttrKeys;
444
+
445
+ // ── suggestions (reports §9) — the same inference, run backwards ──
446
+ import type { DeriveSuggestionsInput, Suggestion, TelemetryCounters as Counters } from './index.js';
447
+ import { COUNTER_OVERFLOW_KEY, MAX_SUGGESTIONS, deriveSuggestions } from './index.js';
448
+
449
+ declare const liveCounters: Counters;
450
+ const suggestInput: DeriveSuggestionsInput = {
451
+ counters: liveCounters,
452
+ catalog,
453
+ quarantine: [{ at: new Date(), name: 'video.exported', reason: 'unregistered event' }],
454
+ };
455
+ const suggestions: Suggestion[] = deriveSuggestions(suggestInput);
456
+ const suggestKind: Suggestion['kind'] = suggestions[0]?.kind ?? 'undeclared_attr';
457
+ const pasteable: string | undefined = suggestions[0]?.fix;
458
+ const capped: 50 = MAX_SUGGESTIONS;
459
+ const overflow: '(other)|(other)' = COUNTER_OVERFLOW_KEY;
460
+ void suggestKind, pasteable, capped, overflow;
461
+
462
+ // ── reports (reports §4, §6) ──
463
+ // The resolver is a value, and its return is a discriminated union: a caller
464
+ // that forgets to check `unavailable` cannot reach `primitive`.
465
+ import type {
466
+ LegacyQuery, Plan, PlanPrimitive, PlanShape, Report, ReportFilter, ReportRange,
467
+ ReportSource, ResolveOptions, Unavailable,
468
+ } from './index.js';
469
+ import type { ExecuteOptions, FoldedRollups, ReportResult, RollupDoc } from './index.js';
470
+ import {
471
+ executeReport, foldRollups, intervalForRange, normalizeQuery, parseReportQuery, rangeOf,
472
+ reportToQuery, resolveReport,
473
+ } from './index.js';
474
+
475
+ const reportSource: ReportSource = { family: 'llm_cost' };
476
+ const reportRange: ReportRange = { from: '2026-07-01T00:00:00Z', to: '2026-07-08T00:00:00Z' };
477
+ const reportFilter: ReportFilter = { dim: 'attr:gen_ai_request_model', op: 'in', value: ['opus', 'sonnet'] };
478
+ const report: Report = {
479
+ source: reportSource,
480
+ range: reportRange,
481
+ interval: 'day',
482
+ measure: 'sum:cost_usd',
483
+ groupBy: ['attr:gen_ai_request_model'],
484
+ filters: [reportFilter],
485
+ compare: 'previous',
486
+ };
487
+ const funnelReport: Report = {
488
+ source: { kind: 'event' },
489
+ range: '90d',
490
+ measure: 'funnel',
491
+ stages: ['account.signed_up', 'account.converted'],
492
+ anchor: 'account.signed_up',
493
+ exits: ['account.churned'],
494
+ subjectType: 'account',
495
+ };
496
+ const resolveOpts: ResolveOptions = { now: new Date(), limits: { breakdown: 20 } };
497
+ const planned: Plan | Unavailable = resolveReport(report, catalog, resolveOpts);
498
+ if ('unavailable' in planned) {
499
+ const refused: true = planned.unavailable;
500
+ const because: string = planned.why;
501
+ void refused, because;
502
+ } else {
503
+ const primitive: PlanPrimitive = planned.primitive;
504
+ const planArgs: unknown[] = planned.args;
505
+ const exactness: 'exact' | 'raw' | 'scan' = planned.exactness;
506
+ const shape: PlanShape | undefined = planned.shape;
507
+ const previous: unknown[] | undefined = planned.previous?.args;
508
+ void primitive, planArgs, exactness, shape, previous;
509
+ }
510
+ const legacy: LegacyQuery = { range: '7d', filters: { name: 'llm.completion' }, groupBy: 'attr:feature' };
511
+ const lifted: Report | null = normalizeQuery(legacy);
512
+ const window: TimeRange = rangeOf('7d', new Date());
513
+ const grainFor: 'hour' | 'day' | 'week' | 'month' = intervalForRange('90d');
514
+ void funnelReport, lifted, window, grainFor;
515
+
516
+ // a Report is a URL, and the two encoders are inverses. `filter` is the one key
517
+ // that can repeat, which is why the query value is a union rather than a string.
518
+ const asQuery: Record<string, string | string[]> = reportToQuery(report);
519
+ const backAgain: Report = parseReportQuery(asQuery);
520
+ void backAgain.source, backAgain.range;
521
+
522
+ // ── the executor (reports §6) ──
523
+ const execOpts: ExecuteOptions = {
524
+ now: new Date(),
525
+ limits: { rollups: 100 },
526
+ redact: (items) => items.map((r) => ({ ...r, data: '[redacted]' })),
527
+ };
528
+ async function runOne(q: Queries) {
529
+ const out: ReportResult = await executeReport(q, 'acc_9', report, catalog, execOpts);
530
+ const ranPlan: Plan = out.plan;
531
+ const store: 'raw' | 'rollups' | 'raw+rollups' = out.dataSource;
532
+ const before: unknown = out.previous;
533
+ void ranPlan, store, before, out.report, out.result;
534
+ }
535
+ // the fold is pure — same rows, same shape, no database
536
+ const foldShape: PlanShape = { groupBy: ['attr:gen_ai_request_model'], labels: ['gen_ai_request_model'], measure: 'sum:cost_usd' };
537
+ const docs: RollupDoc[] = [{ dims: ['gen_ai_request_model=opus'], count: 2, sums: { cost_usd: 4 } }];
538
+ const folded: FoldedRollups = foldRollups(docs, foldShape, false);
539
+ const foldedValue: number | undefined = folded.rows[0]?.value;
540
+ void runOne, foldedValue, folded.groups, folded.truncated, folded.dataSource;
541
+
415
542
  // ── dashboard surface ──
416
543
  import type {
417
544
  CohortSubject,
@@ -429,12 +556,15 @@ import type {
429
556
  ResolvedView,
430
557
  SubjectAdapter,
431
558
  TimeRange,
559
+ Values,
560
+ ValuesParams,
561
+ ValuesResult,
432
562
  Viewer,
433
563
  ViewerAdapter,
434
564
  ViewSpec,
435
565
  } from './index.js';
436
566
  import {
437
- createDashboard, createQueries, defaultSpaDir, deriveViews, findFamily,
567
+ createDashboard, createQueries, createValues, defaultSpaDir, deriveViews, findFamily,
438
568
  median, requireMilestoneFamily, summarizeStages, DEFAULT_LIMITS,
439
569
  } from './index.js';
440
570
 
@@ -452,7 +582,7 @@ const view: ViewSpec = {
452
582
  // the sidebar renders this when present and falls back to the origin badge
453
583
  icon: '⚑',
454
584
  page: 'errors',
455
- query: { range: '24h', filters: { severity: 'error' }, display: 'table' },
585
+ query: { source: { kind: 'error' }, range: '24h', filters: [{ dim: 'field:severity', op: 'eq', value: 'error' }] },
456
586
  };
457
587
  const dashOpts: CreateDashboardOptions = {
458
588
  telemetry: t,
@@ -523,6 +653,24 @@ async function primitives() {
523
653
  const next: string | null = page.nextCursor;
524
654
  const ser = await q.series('acc_9', range, f, { measure: 'sum:cost_usd', interval: 'day' });
525
655
  void ser.buckets[0]?.value;
656
+ // the cap is on GROUPS returned, so `truncated` means "there were more top
657
+ // groups", not "the scan stopped early"
658
+ const bd = await q.breakdown('acc_9', range, f, {
659
+ groupBy: ['attr:gen_ai_request_model', 'field:env'],
660
+ measure: 'sum:cost_usd',
661
+ interval: 'day',
662
+ limit: 10,
663
+ });
664
+ const dimValue: string | null = bd.rows[0]?.dims[0] ?? null;
665
+ const bucketAt: Date | undefined = bd.rows[0]?.at;
666
+ const groupCount: number = bd.groups;
667
+ // two flags, two axes: groups dropped vs. buckets dropped from a group shown
668
+ const missingBuckets: boolean = bd.bucketsTruncated;
669
+ void dimValue, bucketAt, groupCount, missingBuckets, bd.truncated, caps.breakdown;
670
+ // a name SET is one $in, not N reads — a namespace or a family is several names
671
+ await q.records('acc_9', range, { name: ['llm.completion', 'billing.ai_tokens'] });
672
+ // a span's duration is on the envelope, and the measure grammar knows it
673
+ await q.series('acc_9', range, f, { measure: 'avg:durationMs' });
526
674
  // the sample is complete; the computation is capped, and says so
527
675
  const dist = await q.distribution('acc_9', range, f);
528
676
  const scanCut: boolean = dist.truncated;
@@ -575,6 +723,29 @@ async function primitives() {
575
723
  await q.journey(PLATFORM_SCOPE, 'user:u_1', range);
576
724
  await q.distinctCount(PLATFORM_SCOPE, { as: 'activity', range });
577
725
  await q.funnel(PLATFORM_SCOPE, { stages: [{ as: 'user.signed_up' }], cohort: range });
726
+
727
+ // ── values: the lookup, not a primitive (reports §5) ──
728
+ const values: Values = createValues({
729
+ catalog,
730
+ TelemetryModel: t.models.telemetry,
731
+ RollupModel: t.models.rollups,
732
+ limits: { values: 50 },
733
+ });
734
+ const params: ValuesParams = {
735
+ dim: 'attr:gen_ai_request_model',
736
+ names: ['llm.completion'],
737
+ range,
738
+ limit: 20,
739
+ };
740
+ const domain: ValuesResult = await values('acc_9', params);
741
+ const observed: string[] = domain.values;
742
+ // counts are absent on the catalog answer — a declared enum has no tally
743
+ const tally: number[] | undefined = domain.counts;
744
+ const answeredBy: 'catalog' | 'rollups' | 'raw' | 'none' = domain.source;
745
+ const readFrom: string | undefined = domain.via;
746
+ void observed, tally, answeredBy, readFrom, domain.truncated, domain.dataSource, caps.values;
747
+ // no range is not an error: the raw step drops off and `none` says so
748
+ await values(PLATFORM_SCOPE, { dim: 'field:client.platform' });
578
749
  }
579
750
 
580
751
  // ── the funnel math, usable without a database ──
package/types/web.d.ts CHANGED
@@ -3,11 +3,19 @@ import type { CreateClientOptions, Registry, TelemetryClient } from './core.js';
3
3
  export { createClient } from './core.js';
4
4
  export type { TelemetryClient } from './core.js';
5
5
 
6
+ /** browser-raised non-errors, dropped by default (see `captureBenignErrors`) */
7
+ export declare const BENIGN_BROWSER_ERRORS: readonly RegExp[];
8
+
6
9
  export interface WebTelemetryOptions extends Omit<CreateClientOptions, 'storage' | 'consent'> {
7
10
  /** host consent (cookie banner etc). ANDed with DNT/GPC — those always win. */
8
11
  consent?: () => boolean;
9
12
  /** auto-capture window.onerror / unhandledrejection (default true) */
10
13
  captureGlobalErrors?: boolean;
14
+ /** drop error records by message — strings match by substring, RegExp by test.
15
+ * ADDED to `BENIGN_BROWSER_ERRORS`, not replacing it. */
16
+ ignoreErrors?: Array<string | RegExp>;
17
+ /** keep the `BENIGN_BROWSER_ERRORS` records instead of dropping them */
18
+ captureBenignErrors?: boolean;
11
19
  }
12
20
 
13
21
  /**