@jeffjassky/telemetry 0.5.0 → 0.6.1

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.
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>Telemetry</title>
7
7
  <!--telemetry-config-->
8
- <script type="module" crossorigin src="./_assets/index-kvwB9_3A.js"></script>
8
+ <script type="module" crossorigin src="./_assets/index-BVZYi-43.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="./_assets/index-COswHpSX.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffjassky/telemetry",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Unified telemetry — product events, errors, traces, state transitions, and billable usage in one Mongo envelope, with typed SDKs and a mountable dashboard.",
5
5
  "license": "MIT",
6
6
  "author": "Jeff Jassky <jeff@jeffjassky.com>",
package/types/index.d.ts CHANGED
@@ -696,6 +696,59 @@ export interface ForgetResult {
696
696
  views: number;
697
697
  }
698
698
 
699
+ export interface RelinkOptions {
700
+ /**
701
+ * Restrict to these event names. Default: every stored record, whatever its
702
+ * name. A name the registry does not declare THROWS, before any I/O — a typo
703
+ * that silently relinks nothing looks exactly like a clean run.
704
+ */
705
+ names?: string[];
706
+ /** only records at/after this instant (`occurredAt`) */
707
+ since?: Date;
708
+ /**
709
+ * Stop after this many records are EXAMINED — not linked. A budget for the
710
+ * scan, so a huge collection can be probed cheaply. Default unbounded.
711
+ */
712
+ limit?: number;
713
+ /**
714
+ * Report what would change and write NOTHING. **Defaults to `true`**, because
715
+ * this rewrites historical aggregates and the short call has to be the safe
716
+ * one. A dry run still asks the host's linker, so the linking counters move;
717
+ * nothing on disk does.
718
+ */
719
+ dryRun?: boolean;
720
+ /** records fetched per batch, and the `onProgress` cadence. Default 500. */
721
+ batchSize?: number;
722
+ /** cumulative counts after each batch, for progress output. Guarded: a
723
+ * printer that throws does not kill the backfill. */
724
+ onProgress?: (r: RelinkResult) => void;
725
+ }
726
+
727
+ export interface RelinkResult {
728
+ /** rows read */
729
+ examined: number;
730
+ /** rows that gained at least one subject */
731
+ linked: number;
732
+ /** subjects added in total — two links on one row count twice */
733
+ subjects: number;
734
+ /** rollup documents written, or under `dryRun` that would have been */
735
+ rollups: number;
736
+ /** rows where the linker answered `[]`. On a backfill this is the expected
737
+ * answer for most rows, and it is not a failure. */
738
+ misses: number;
739
+ /** rows where the linker threw, rejected, timed out or answered garbage —
740
+ * once per ROW however many ways it went wrong. `counters.subjectLinkErrors`
741
+ * and `subjectLinkTimeouts` keep the finer split. */
742
+ errors: number;
743
+ /** rows the run declined to offer the linker (a stored name the registry no
744
+ * longer declares, so its rollup families are unknowable), plus one standing
745
+ * for a call with no `subjectLinker` configured, which reads nothing. */
746
+ skipped: number;
747
+ }
748
+
749
+ /** records fetched per batch by `relink()`, and how often `onProgress` fires */
750
+ export declare const RELINK_BATCH_SIZE: 500;
751
+
699
752
  export interface Scoped {
700
753
  find(q?: Record<string, unknown>): Query<any[], any>;
701
754
  aggregate(stages: Record<string, unknown>[]): Aggregate<any[]>;
@@ -760,7 +813,9 @@ export interface SubjectLinker {
760
813
  * Runs once per record on the write path, so it must answer from a cache.
761
814
  * The package bounds it rather than trusting it: past `subjectLinkTimeoutMs`,
762
815
  * or on a throw, the record is written unlinked and counted. A linked subject
763
- * whose `type` the event does not declare is refused, not written.
816
+ * whose `type` the event does not declare is written ANYWAY and counted in
817
+ * `counters.subjectLinkUndeclared` — refusing it could only ever be obeyed by
818
+ * losing rows, because `EventSpec.subjects` is a required list.
764
819
  */
765
820
  link(
766
821
  subjects: SubjectInput[],
@@ -802,6 +857,25 @@ export interface Telemetry<R extends Registry = Registry> {
802
857
  * platform-scoped saved views only when `globalSubjectRefs` is set.
803
858
  */
804
859
  forget(tenantId: string, ref: EntityRef): Promise<ForgetResult>;
860
+ /**
861
+ * Backfill for `subjectLinker`: re-ask it about records ALREADY on disk, and
862
+ * replay the rollups the new subjects reach.
863
+ *
864
+ * Linking happens at write time, so configuring the hook fixes the future and
865
+ * nothing else — a lifetime `by:['subject']` family is keyed on the subject
866
+ * the record was written with, permanently, and no read-time join can reach
867
+ * back into it. A host that adopts linking on a Tuesday therefore has a
868
+ * backlog whose rows carry only `machine:<installId>` and whose user-keyed
869
+ * families have no member for any of them. This is how it catches up.
870
+ *
871
+ * **Dry run by default.** It rewrites historical aggregates, so `t.relink()`
872
+ * reports and `t.relink({ dryRun: false })` writes. Idempotent by
873
+ * construction: a row that already carries the linked subject yields nothing
874
+ * new, so a second run writes nothing and replays nothing. Returns
875
+ * `{ skipped: 1 }` rather than throwing when no `subjectLinker` is
876
+ * configured.
877
+ */
878
+ relink(opts?: RelinkOptions): Promise<RelinkResult>;
805
879
  /**
806
880
  * Tenant scope is not optional — every read goes through here. Unconditional
807
881
  * on purpose: it does not understand PLATFORM_SCOPE, so `scoped('*')` scopes
package/types/test-d.ts CHANGED
@@ -33,6 +33,8 @@ import type {
33
33
  Logger,
34
34
  MetricsOf,
35
35
  Registry,
36
+ RelinkOptions,
37
+ RelinkResult,
36
38
  RollupSpec,
37
39
  LinkSubjects,
38
40
  Scoped,
@@ -60,6 +62,7 @@ import {
60
62
  LogLevel,
61
63
  Origin,
62
64
  PLATFORM_SCOPE,
65
+ RELINK_BATCH_SIZE,
63
66
  RETENTION_DAYS,
64
67
  SAMPLE_RATE,
65
68
  SCHEMA_VERSION,
@@ -196,6 +199,21 @@ async function reads() {
196
199
  const gone: ForgetResult = await t.forget('acc_9', 'user:u_1');
197
200
  void (gone.deleted + gone.redacted + gone.rollups + gone.aliases);
198
201
 
202
+ // relink() — the backfill for subjectLinker. Dry run by DEFAULT, so the
203
+ // no-argument call is the safe one and writing is opted into.
204
+ const preview: RelinkResult = await t.relink();
205
+ const opts: RelinkOptions = {
206
+ names: ['user.signed_up'],
207
+ since: new Date('2026-01-01T00:00:00Z'),
208
+ limit: 10_000,
209
+ dryRun: false,
210
+ batchSize: RELINK_BATCH_SIZE,
211
+ onProgress: (p) => void p.examined,
212
+ };
213
+ const done: RelinkResult = await t.relink(opts);
214
+ void (preview.examined + done.linked + done.subjects + done.rollups
215
+ + done.misses + done.errors + done.skipped);
216
+
199
217
  const cp: Checkpoint = t.checkpoint('mailery-bridge');
200
218
  const mark: Date | null = await cp.get();
201
219
  await cp.advance(mark ?? new Date());