@jeffjassky/telemetry 0.4.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/dist/index.cjs +147 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +146 -4
- package/dist/index.js.map +1 -1
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.js.map +1 -1
- package/dist/ui/_assets/index-kvwB9_3A.js +41 -0
- package/dist/ui/_assets/{index-Deqhu-hT.js.map → index-kvwB9_3A.js.map} +1 -1
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
- package/types/index.d.ts +88 -0
- package/types/test-d.ts +20 -0
- package/dist/ui/_assets/index-Deqhu-hT.js +0 -41
package/dist/ui/index.html
CHANGED
|
@@ -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-
|
|
8
|
+
<script type="module" crossorigin src="./_assets/index-kvwB9_3A.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.
|
|
3
|
+
"version": "0.5.0",
|
|
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
|
@@ -632,6 +632,24 @@ export interface TelemetryCounters {
|
|
|
632
632
|
* not stripped; this groups what the quarantine lists one row at a time.
|
|
633
633
|
*/
|
|
634
634
|
undeclaredAttrs: Record<string, number>;
|
|
635
|
+
/**
|
|
636
|
+
* Write-time subject linking. All six stay at zero without a
|
|
637
|
+
* `subjectLinker`. They are split six ways because every way linking can fail
|
|
638
|
+
* ends in the same row — one written with the subjects it arrived with — and
|
|
639
|
+
* a silently unlinked record is indistinguishable from one nobody could link.
|
|
640
|
+
*/
|
|
641
|
+
/** subjects actually ADDED to records — two links on one record count twice */
|
|
642
|
+
subjectsLinked: number;
|
|
643
|
+
/** records where the linker answered `[]` — no link exists, which is an answer */
|
|
644
|
+
subjectLinkMisses: number;
|
|
645
|
+
/** the linker threw, rejected, or returned something that is not a list of refs */
|
|
646
|
+
subjectLinkErrors: number;
|
|
647
|
+
/** the linker outran `subjectLinkTimeoutMs`; the record was written unlinked */
|
|
648
|
+
subjectLinkTimeouts: number;
|
|
649
|
+
/** a linked subject whose `type` the event's `EventSpec.subjects` does not declare */
|
|
650
|
+
subjectLinkUndeclared: number;
|
|
651
|
+
/** a linked subject dropped because the record already held `SUBJECT_MAX` of them */
|
|
652
|
+
subjectLinkCapped: number;
|
|
635
653
|
}
|
|
636
654
|
|
|
637
655
|
/**
|
|
@@ -709,9 +727,71 @@ export interface CreateTelemetryConfig<R extends Registry = Registry> {
|
|
|
709
727
|
* different person in each, and one tenant's erasure would reach another's.
|
|
710
728
|
*/
|
|
711
729
|
globalSubjectRefs?: boolean;
|
|
730
|
+
/**
|
|
731
|
+
* Attach additional subjects to a record AT WRITE TIME — the desktop
|
|
732
|
+
* `machine:<installId>` the host can resolve to a `user`, joined once, onto
|
|
733
|
+
* the row and its rollups, rather than at every read that ever wants it.
|
|
734
|
+
* Must be cached: it runs once per record on the ingest path.
|
|
735
|
+
*/
|
|
736
|
+
subjectLinker?: SubjectLinker;
|
|
737
|
+
/**
|
|
738
|
+
* What `subjectLinker.link()` gets per record before the write proceeds
|
|
739
|
+
* UNLINKED and counts a timeout. Default 50.
|
|
740
|
+
*/
|
|
741
|
+
subjectLinkTimeoutMs?: number;
|
|
712
742
|
logger?: Logger;
|
|
713
743
|
}
|
|
714
744
|
|
|
745
|
+
/**
|
|
746
|
+
* WRITE-time subject linking — distinct from `SubjectAdapter`, which labels
|
|
747
|
+
* refs at read time and changes nothing about what is stored.
|
|
748
|
+
*
|
|
749
|
+
* A desktop client knows its install and nothing else, so its records carry
|
|
750
|
+
* `machine:<installId>` and no `user`. Resolving that at read time leaves a
|
|
751
|
+
* cohort funnel anchored on `user` reading zero for every desktop stage;
|
|
752
|
+
* resolving it at write time puts the party on the row AND on its rollups,
|
|
753
|
+
* which is the half a read-time join can never reach.
|
|
754
|
+
*/
|
|
755
|
+
export interface SubjectLinker {
|
|
756
|
+
/**
|
|
757
|
+
* Additional subjects for a record being written. `[]` when nothing links —
|
|
758
|
+
* that is an answer, and it is counted as one.
|
|
759
|
+
*
|
|
760
|
+
* Runs once per record on the write path, so it must answer from a cache.
|
|
761
|
+
* The package bounds it rather than trusting it: past `subjectLinkTimeoutMs`,
|
|
762
|
+
* 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.
|
|
764
|
+
*/
|
|
765
|
+
link(
|
|
766
|
+
subjects: SubjectInput[],
|
|
767
|
+
ctx: { name: string; tenantId: string },
|
|
768
|
+
): SubjectInput[] | Promise<SubjectInput[]>;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* The guarded linker the instance resolved at construction: the merged subjects
|
|
773
|
+
* to write, or `null` when nothing changed. Exposed as `t.linkSubjects` for the
|
|
774
|
+
* router factories, which reach it the way they reach the registry.
|
|
775
|
+
*/
|
|
776
|
+
export type LinkSubjects = (
|
|
777
|
+
name: string,
|
|
778
|
+
spec: { subjects: readonly string[] },
|
|
779
|
+
tenantId: string,
|
|
780
|
+
declared: unknown,
|
|
781
|
+
) => Promise<SubjectInput[] | null>;
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Total subjects one record may carry once linking has run. `subjectKeys` is a
|
|
785
|
+
* multikey index term and every subject fans a `by:['subject']` rollup out one
|
|
786
|
+
* more time, so an unbounded array is unbounded write amplification with a
|
|
787
|
+
* host's cache bug behind it. Overflow is dropped and counted in
|
|
788
|
+
* `counters.subjectLinkCapped`.
|
|
789
|
+
*/
|
|
790
|
+
export declare const SUBJECT_MAX: 8;
|
|
791
|
+
|
|
792
|
+
/** default `subjectLinkTimeoutMs` — past it the record is written unlinked */
|
|
793
|
+
export declare const SUBJECT_LINK_TIMEOUT_MS: 50;
|
|
794
|
+
|
|
715
795
|
export interface Telemetry<R extends Registry = Registry> {
|
|
716
796
|
/** write — the only write. The result says what actually happened to the row. */
|
|
717
797
|
emit<N extends keyof R & string>(name: N, doc: EmitInput<R, N>): Promise<EmitResult>;
|
|
@@ -739,6 +819,14 @@ export interface Telemetry<R extends Registry = Registry> {
|
|
|
739
819
|
counters: TelemetryCounters;
|
|
740
820
|
/** the registry this instance validates against */
|
|
741
821
|
registry: R;
|
|
822
|
+
/**
|
|
823
|
+
* Write-time subject linking, guarded and resolved once at construction;
|
|
824
|
+
* `null` without a `subjectLinker`. Exposed for the router factories: the
|
|
825
|
+
* wire path does not go through `emit()` — at-least-once delivery inverts the
|
|
826
|
+
* plane order — so `createIngest` reaches the one implementation here rather
|
|
827
|
+
* than growing a second copy of the rules.
|
|
828
|
+
*/
|
|
829
|
+
linkSubjects: LinkSubjects | null;
|
|
742
830
|
logger: Logger;
|
|
743
831
|
/** mint an ingest key against this instance's key collection */
|
|
744
832
|
createKey(input: CreateKeyInput): Promise<{ key: string; id: string }>;
|
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', {
|