@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.
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/dist/cli.cjs +310 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +307 -0
- package/dist/cli.js.map +1 -0
- package/dist/core.cjs +276 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.js +271 -0
- package/dist/core.js.map +1 -0
- package/dist/electron.cjs +287 -0
- package/dist/electron.cjs.map +1 -0
- package/dist/electron.js +282 -0
- package/dist/electron.js.map +1 -0
- package/dist/index.cjs +2394 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +2350 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +303 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.js +278 -0
- package/dist/react.js.map +1 -0
- package/dist/ui/_assets/index-3jcToTuP.js +41 -0
- package/dist/ui/_assets/index-3jcToTuP.js.map +1 -0
- package/dist/ui/_assets/index-CXiDp_v6.css +1 -0
- package/dist/ui/index.html +14 -0
- package/dist/vue.cjs +267 -0
- package/dist/vue.cjs.map +1 -0
- package/dist/vue.js +262 -0
- package/dist/vue.js.map +1 -0
- package/dist/web.cjs +335 -0
- package/dist/web.cjs.map +1 -0
- package/dist/web.js +332 -0
- package/dist/web.js.map +1 -0
- package/package.json +126 -0
- package/types/cli.d.ts +20 -0
- package/types/core.d.ts +126 -0
- package/types/electron.d.ts +24 -0
- package/types/index.d.ts +763 -0
- package/types/react.d.ts +19 -0
- package/types/test-d.ts +687 -0
- package/types/vue.d.ts +17 -0
- package/types/web.d.ts +19 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jeff Jassky
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# @jeffjassky/telemetry
|
|
2
|
+
|
|
3
|
+
Unified telemetry — product events, errors, traces, state transitions, and
|
|
4
|
+
billable usage in **one Mongo envelope**, with typed SDKs and a mountable
|
|
5
|
+
dashboard.
|
|
6
|
+
|
|
7
|
+
Five kinds, one collection, discriminated on `kind`, driven by a registry your
|
|
8
|
+
application owns. This package ships no event names.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @jeffjassky/telemetry
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Full docs: **https://jeffjassky.github.io/telemetry/**
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import express from 'express';
|
|
20
|
+
import mongoose from 'mongoose';
|
|
21
|
+
import { z } from 'zod';
|
|
22
|
+
import {
|
|
23
|
+
createDashboard, createIngest, createTelemetry, defineRegistry,
|
|
24
|
+
} from '@jeffjassky/telemetry';
|
|
25
|
+
|
|
26
|
+
// 1. The registry is the contract: validation, types, rollups, indexes.
|
|
27
|
+
const REGISTRY = defineRegistry({
|
|
28
|
+
'account.signed_up': {
|
|
29
|
+
kind: 'event', origin: 'server', subjects: ['account'],
|
|
30
|
+
attrs: z.object({ source: z.string().max(64) }),
|
|
31
|
+
indexedAttrs: ['source'],
|
|
32
|
+
rollups: [{ by: ['subject'], subjects: ['account'], capture: ['attr:source'] }],
|
|
33
|
+
description: 'Account created',
|
|
34
|
+
},
|
|
35
|
+
'llm.completion': {
|
|
36
|
+
kind: 'span', origin: 'server', subjects: ['org'],
|
|
37
|
+
attrs: z.object({ gen_ai_request_model: z.string(), feature: z.string().max(64) }),
|
|
38
|
+
metrics: z.object({ tokens_in: z.number().int(), tokens_out: z.number().int(), cost_usd: z.number() }),
|
|
39
|
+
rollups: [{
|
|
40
|
+
as: 'llm_cost', by: ['attr:gen_ai_request_model'],
|
|
41
|
+
bucket: 'day', sum: ['cost_usd'], retentionDays: null,
|
|
42
|
+
}],
|
|
43
|
+
description: 'Single model call',
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await mongoose.connect(process.env.MONGO_URL);
|
|
48
|
+
|
|
49
|
+
// 2. One instance. Validates the registry at construction.
|
|
50
|
+
const t = createTelemetry({ registry: REGISTRY, connection: mongoose });
|
|
51
|
+
await t.syncIndexes(); // ← await before the first write. See below.
|
|
52
|
+
|
|
53
|
+
// 3. emit() is the only write, and it is typed against the registry.
|
|
54
|
+
await t.emit('account.signed_up', {
|
|
55
|
+
tenantId: 'acc_9',
|
|
56
|
+
subjects: [{ type: 'account', id: 'acct_1' }],
|
|
57
|
+
actor: 'user:u_1',
|
|
58
|
+
attrs: { source: 'ads' },
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// 4. Two routers you mount wherever you like.
|
|
62
|
+
const app = express();
|
|
63
|
+
app.use('/telemetry/ingest', createIngest({ telemetry: t }));
|
|
64
|
+
app.use('/telemetry', createDashboard({
|
|
65
|
+
telemetry: t,
|
|
66
|
+
viewerAdapter: {
|
|
67
|
+
resolveViewer: (req) =>
|
|
68
|
+
req.user ? { tenantId: req.user.orgId, role: req.user.role } : null,
|
|
69
|
+
},
|
|
70
|
+
mountPath: '/telemetry',
|
|
71
|
+
}));
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Then `open http://localhost:3000/telemetry`.
|
|
75
|
+
|
|
76
|
+
## The three things to get right
|
|
77
|
+
|
|
78
|
+
**1. `await t.syncIndexes()` before the first write.** Mongoose builds indexes
|
|
79
|
+
asynchronously; on a cold database, writes issued during the build race it. The
|
|
80
|
+
unique index on `usage.idempotencyKey` is what makes a retried billing write a
|
|
81
|
+
no-op — with the build still in flight, both copies land, and you find out when
|
|
82
|
+
a customer is billed twice.
|
|
83
|
+
|
|
84
|
+
**2. `viewerAdapter` is the entire access boundary.** `createDashboard()`
|
|
85
|
+
refuses to construct without it, because an unauthenticated telemetry dashboard
|
|
86
|
+
is a data leak with charts. The package never authenticates anyone — it asks who
|
|
87
|
+
is calling and trusts your answer. Returning `{ tenantId: '*' }` grants a
|
|
88
|
+
cross-tenant read; that is *your* authorization decision, and the package will
|
|
89
|
+
never infer it from a role or a header.
|
|
90
|
+
|
|
91
|
+
**3. Set a distinct `modelName` if two instances share one connection.** Mongoose
|
|
92
|
+
keeps compiled models per connection, so the second instance silently reuses the
|
|
93
|
+
first one's model — and therefore the first one's registry. Your events become
|
|
94
|
+
unregistered and get quarantined.
|
|
95
|
+
|
|
96
|
+
## What it is not
|
|
97
|
+
|
|
98
|
+
Not an APM, not a log aggregator, not a warehouse. No profiling, no ops time
|
|
99
|
+
series, no free-form log stream, no ad-hoc N-step funnels over raw events. It
|
|
100
|
+
answers the questions you declared in advance, exactly, from aggregates it
|
|
101
|
+
maintained on write. Sized for small SaaS — thousands of users, not billions of
|
|
102
|
+
events.
|
|
103
|
+
|
|
104
|
+
## Requirements
|
|
105
|
+
|
|
106
|
+
Node 20+. `mongoose` (7/8/9) and `express` (4.18+/5) are **optional peer
|
|
107
|
+
dependencies** — the server entry needs them, the client entries do not, so a
|
|
108
|
+
browser bundle never pulls the server stack in. All three Mongoose majors run
|
|
109
|
+
the full suite in CI. `react` and `vue` are optional peers for their client
|
|
110
|
+
entries.
|
|
111
|
+
|
|
112
|
+
Entry points: `.` (server), `/core`, `/web`, `/react`, `/vue`, `/electron`,
|
|
113
|
+
`/cli`.
|
|
114
|
+
|
|
115
|
+
## Try it
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npm run build
|
|
119
|
+
node examples/express/server.js # in-memory Mongo, seeded data, both routers
|
|
120
|
+
open http://localhost:3000/telemetry
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npm install
|
|
127
|
+
npm run check-tracked # run this FIRST — a global gitignore can eat source files
|
|
128
|
+
npm run typecheck
|
|
129
|
+
npm run build # server, then the UI bundle into dist/ui
|
|
130
|
+
npm test # after build: the UI tests assert against the bundle
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT © Jeff Jassky
|
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fs = require('fs');
|
|
4
|
+
var path = require('path');
|
|
5
|
+
var uuidv7 = require('uuidv7');
|
|
6
|
+
require('zod');
|
|
7
|
+
|
|
8
|
+
// src/client/cli.ts
|
|
9
|
+
|
|
10
|
+
// src/client/core.ts
|
|
11
|
+
var newId = uuidv7.uuidv7;
|
|
12
|
+
var SDK = { name: "@jeffjassky/telemetry", version: "0" };
|
|
13
|
+
var defaultTransport = async (url, body, headers) => {
|
|
14
|
+
const res = await fetch(url, { method: "POST", headers, body, keepalive: true });
|
|
15
|
+
return { ok: res.ok, status: res.status };
|
|
16
|
+
};
|
|
17
|
+
var memoryStorage = () => {
|
|
18
|
+
const m = /* @__PURE__ */ new Map();
|
|
19
|
+
return { get: (k) => m.get(k), set: (k, v) => void m.set(k, v) };
|
|
20
|
+
};
|
|
21
|
+
var fingerprint = (type, message, frame) => {
|
|
22
|
+
const s = `${type}|${message.replace(/\d+/g, "N").slice(0, 200)}|${frame}`;
|
|
23
|
+
let h = 5381;
|
|
24
|
+
for (let i = 0; i < s.length; i++) h = (h << 5) + h + s.charCodeAt(i) >>> 0;
|
|
25
|
+
return h.toString(16);
|
|
26
|
+
};
|
|
27
|
+
var FRAME_RE = /^\s*at\s+(?:(.+?)\s+\()?(?:(.+?):(\d+):(\d+))\)?\s*$/;
|
|
28
|
+
var parseFrames = (stack) => (stack ?? "").split("\n").slice(1, 21).map((line) => FRAME_RE.exec(line)).filter((m) => !!m).map((m) => ({
|
|
29
|
+
fn: m[1],
|
|
30
|
+
filename: m[2],
|
|
31
|
+
lineno: Number(m[3]),
|
|
32
|
+
colno: Number(m[4])
|
|
33
|
+
}));
|
|
34
|
+
function createClient(options) {
|
|
35
|
+
const {
|
|
36
|
+
key,
|
|
37
|
+
url,
|
|
38
|
+
release,
|
|
39
|
+
flushIntervalMs = 5e3,
|
|
40
|
+
maxBatchSize = 50,
|
|
41
|
+
maxQueueSize = 1e3,
|
|
42
|
+
maxRetries = 5,
|
|
43
|
+
transport = defaultTransport,
|
|
44
|
+
storage = memoryStorage(),
|
|
45
|
+
consent = () => true,
|
|
46
|
+
errorName = "error.unhandled",
|
|
47
|
+
onError = () => {
|
|
48
|
+
}
|
|
49
|
+
} = options;
|
|
50
|
+
const clientContext = {
|
|
51
|
+
platform: typeof window === "undefined" ? "server" : "web",
|
|
52
|
+
appVersion: release ?? "unknown",
|
|
53
|
+
...options.clientContext
|
|
54
|
+
};
|
|
55
|
+
let anonId = storage.get("telemetry_anon") ?? null;
|
|
56
|
+
if (!anonId) {
|
|
57
|
+
anonId = `anon_${newId()}`;
|
|
58
|
+
try {
|
|
59
|
+
storage.set("telemetry_anon", anonId);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
onError(e);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const sessionId = `ses_${newId()}`;
|
|
65
|
+
const subjects = /* @__PURE__ */ new Map([
|
|
66
|
+
["anon", anonId],
|
|
67
|
+
["session", sessionId]
|
|
68
|
+
]);
|
|
69
|
+
let actor;
|
|
70
|
+
const queue = [];
|
|
71
|
+
let dropped = 0;
|
|
72
|
+
let attempts = 0;
|
|
73
|
+
let flushing = null;
|
|
74
|
+
let timer;
|
|
75
|
+
const enqueue = (rec) => {
|
|
76
|
+
queue.push(rec);
|
|
77
|
+
while (queue.length > maxQueueSize) {
|
|
78
|
+
queue.shift();
|
|
79
|
+
dropped++;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const spanStack = [];
|
|
83
|
+
const activeTrace = () => spanStack[spanStack.length - 1];
|
|
84
|
+
const doFlush = async () => {
|
|
85
|
+
if (!queue.length) return;
|
|
86
|
+
if (!consent()) {
|
|
87
|
+
queue.length = 0;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
while (queue.length) {
|
|
91
|
+
const batch = queue.slice(0, maxBatchSize);
|
|
92
|
+
const body = JSON.stringify({
|
|
93
|
+
sdk: SDK,
|
|
94
|
+
sentAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
95
|
+
release,
|
|
96
|
+
client: clientContext,
|
|
97
|
+
context: {
|
|
98
|
+
subjects: [...subjects].map(([type, id]) => ({ type, id })),
|
|
99
|
+
...actor ? { actor } : {}
|
|
100
|
+
},
|
|
101
|
+
records: batch
|
|
102
|
+
});
|
|
103
|
+
try {
|
|
104
|
+
const res = await transport(url, body, {
|
|
105
|
+
"content-type": "application/json",
|
|
106
|
+
authorization: `Bearer ${key}`
|
|
107
|
+
});
|
|
108
|
+
if (!res.ok) throw new Error(`ingest ${res.status}`);
|
|
109
|
+
queue.splice(0, batch.length);
|
|
110
|
+
attempts = 0;
|
|
111
|
+
} catch (e) {
|
|
112
|
+
attempts++;
|
|
113
|
+
if (attempts >= maxRetries) {
|
|
114
|
+
queue.splice(0, batch.length);
|
|
115
|
+
dropped += batch.length;
|
|
116
|
+
attempts = 0;
|
|
117
|
+
}
|
|
118
|
+
onError(e);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const flush = () => flushing ??= doFlush().finally(() => flushing = null);
|
|
124
|
+
if (flushIntervalMs > 0) {
|
|
125
|
+
timer = setInterval(() => void flush(), flushIntervalMs);
|
|
126
|
+
timer.unref?.();
|
|
127
|
+
}
|
|
128
|
+
const client = {
|
|
129
|
+
track(name, opts = {}) {
|
|
130
|
+
const trace = activeTrace();
|
|
131
|
+
enqueue({
|
|
132
|
+
_id: newId(),
|
|
133
|
+
name,
|
|
134
|
+
occurredAt: (opts.occurredAt ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
135
|
+
...opts.attrs ? { attrs: opts.attrs } : {},
|
|
136
|
+
...opts.metrics ? { metrics: opts.metrics } : {},
|
|
137
|
+
...opts.data ? { data: opts.data } : {},
|
|
138
|
+
...opts.subjects ? { subjects: opts.subjects } : {},
|
|
139
|
+
...opts.severity ? { severity: opts.severity } : {},
|
|
140
|
+
...trace ? { traceId: trace.traceId, parentId: trace.spanId } : {}
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
captureError(err, ctx = {}) {
|
|
144
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
145
|
+
const frames = parseFrames(e.stack);
|
|
146
|
+
const trace = activeTrace();
|
|
147
|
+
enqueue({
|
|
148
|
+
_id: newId(),
|
|
149
|
+
name: ctx.name ?? errorName,
|
|
150
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
151
|
+
severity: "error",
|
|
152
|
+
...ctx.attrs ? { attrs: ctx.attrs } : {},
|
|
153
|
+
...trace ? { traceId: trace.traceId, parentId: trace.spanId } : {},
|
|
154
|
+
error: {
|
|
155
|
+
type: e.name || "Error",
|
|
156
|
+
message: e.message,
|
|
157
|
+
handled: ctx.handled ?? true,
|
|
158
|
+
fingerprint: fingerprint(e.name, e.message, frames[0]?.filename ?? ""),
|
|
159
|
+
frames
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
startSpan(name, opts = {}) {
|
|
164
|
+
const parent = activeTrace();
|
|
165
|
+
const traceId = parent?.traceId ?? newId();
|
|
166
|
+
const spanId = newId();
|
|
167
|
+
const startedAt = Date.now();
|
|
168
|
+
const frame = { traceId, spanId };
|
|
169
|
+
spanStack.push(frame);
|
|
170
|
+
let ended = false;
|
|
171
|
+
return {
|
|
172
|
+
traceId,
|
|
173
|
+
spanId,
|
|
174
|
+
end: (extra = {}) => {
|
|
175
|
+
if (ended) return;
|
|
176
|
+
ended = true;
|
|
177
|
+
const i = spanStack.indexOf(frame);
|
|
178
|
+
if (i >= 0) spanStack.splice(i, 1);
|
|
179
|
+
enqueue({
|
|
180
|
+
_id: newId(),
|
|
181
|
+
name,
|
|
182
|
+
occurredAt: new Date(startedAt).toISOString(),
|
|
183
|
+
traceId,
|
|
184
|
+
spanId,
|
|
185
|
+
...parent ? { parentId: parent.spanId } : {},
|
|
186
|
+
durationMs: Date.now() - startedAt,
|
|
187
|
+
...opts.attrs || extra.attrs ? { attrs: { ...opts.attrs, ...extra.attrs } } : {},
|
|
188
|
+
...extra.metrics ? { metrics: extra.metrics } : {}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
state(name, st) {
|
|
194
|
+
enqueue({ _id: newId(), name, occurredAt: (/* @__PURE__ */ new Date()).toISOString(), state: st });
|
|
195
|
+
},
|
|
196
|
+
/**
|
|
197
|
+
* Swap in the real identity. Future batches carry the new subjects; one
|
|
198
|
+
* `$identify` control record links anon → user for the stitching job.
|
|
199
|
+
* The anon subject stays on the context — stitching consumes the alias.
|
|
200
|
+
*/
|
|
201
|
+
identify(ids) {
|
|
202
|
+
const hadUser = subjects.get("user");
|
|
203
|
+
for (const [type, id] of Object.entries(ids)) {
|
|
204
|
+
if (id == null) subjects.delete(type);
|
|
205
|
+
else subjects.set(type, String(id));
|
|
206
|
+
}
|
|
207
|
+
const user = subjects.get("user");
|
|
208
|
+
if (user && user !== hadUser && anonId) {
|
|
209
|
+
enqueue({
|
|
210
|
+
_id: newId(),
|
|
211
|
+
name: "$identify",
|
|
212
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
213
|
+
anonRef: `anon:${anonId}`,
|
|
214
|
+
userRef: `user:${user}`
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (subjects.has("user")) actor = `user:${subjects.get("user")}`;
|
|
218
|
+
},
|
|
219
|
+
setActor(ref) {
|
|
220
|
+
actor = ref;
|
|
221
|
+
},
|
|
222
|
+
flush,
|
|
223
|
+
async shutdown() {
|
|
224
|
+
if (timer) clearInterval(timer);
|
|
225
|
+
await flush();
|
|
226
|
+
},
|
|
227
|
+
/** introspection for adapters and tests — not a public contract */
|
|
228
|
+
_internal: {
|
|
229
|
+
queue,
|
|
230
|
+
subjects,
|
|
231
|
+
get anonId() {
|
|
232
|
+
return anonId;
|
|
233
|
+
},
|
|
234
|
+
sessionId,
|
|
235
|
+
get dropped() {
|
|
236
|
+
return dropped;
|
|
237
|
+
},
|
|
238
|
+
enqueue
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
return client;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/client/cli.ts
|
|
245
|
+
var noopClient = (opts) => (
|
|
246
|
+
// a disabled CLI client is the same client with consent hard-off: every
|
|
247
|
+
// call works, nothing is ever sent or stored
|
|
248
|
+
createClient({ ...opts, flushIntervalMs: 0, consent: () => false })
|
|
249
|
+
);
|
|
250
|
+
function createCliTelemetry(opts) {
|
|
251
|
+
const { configDir, argv = typeof process !== "undefined" ? process.argv : [], maxQueueAgeMs = 7 * 864e5, ...rest } = opts;
|
|
252
|
+
const optedOut = typeof process !== "undefined" && (process.env.DO_NOT_TRACK === "1" || process.env.TELEMETRY_DISABLED === "1") || argv.includes("--no-telemetry");
|
|
253
|
+
if (optedOut) return noopClient(rest);
|
|
254
|
+
try {
|
|
255
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
256
|
+
} catch {
|
|
257
|
+
}
|
|
258
|
+
const file = (name) => path.join(configDir, name);
|
|
259
|
+
const storage = {
|
|
260
|
+
get: (k) => {
|
|
261
|
+
try {
|
|
262
|
+
return fs.readFileSync(file(`${k}.txt`), "utf8").trim();
|
|
263
|
+
} catch {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
set: (k, v) => {
|
|
268
|
+
try {
|
|
269
|
+
fs.writeFileSync(file(`${k}.txt`), v);
|
|
270
|
+
} catch {
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
const client = createClient({
|
|
275
|
+
...rest,
|
|
276
|
+
storage,
|
|
277
|
+
clientContext: { platform: "cli", appVersion: rest.release ?? "unknown", ...rest.clientContext }
|
|
278
|
+
});
|
|
279
|
+
const queueFile = file("telemetry-queue.json");
|
|
280
|
+
try {
|
|
281
|
+
const saved = JSON.parse(fs.readFileSync(queueFile, "utf8"));
|
|
282
|
+
fs.rmSync(queueFile, { force: true });
|
|
283
|
+
if (Array.isArray(saved)) {
|
|
284
|
+
const floor = Date.now() - maxQueueAgeMs;
|
|
285
|
+
for (const rec of saved) {
|
|
286
|
+
if (rec && typeof rec._id === "string" && Date.parse(rec.occurredAt) >= floor) {
|
|
287
|
+
client._internal.enqueue(rec);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
} catch {
|
|
292
|
+
}
|
|
293
|
+
if (typeof process !== "undefined") {
|
|
294
|
+
process.on("exit", () => {
|
|
295
|
+
const q = client._internal.queue;
|
|
296
|
+
if (q.length) {
|
|
297
|
+
try {
|
|
298
|
+
fs.writeFileSync(queueFile, JSON.stringify(q));
|
|
299
|
+
} catch {
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
return client;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
exports.createCliTelemetry = createCliTelemetry;
|
|
308
|
+
exports.createClient = createClient;
|
|
309
|
+
//# sourceMappingURL=cli.cjs.map
|
|
310
|
+
//# sourceMappingURL=cli.cjs.map
|
package/dist/cli.cjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client/core.ts","../src/client/cli.ts"],"names":["uuidv7","mkdirSync","join","readFileSync","writeFileSync","rmSync"],"mappings":"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","file":"cli.cjs","sourcesContent":["import { uuidv7 } from 'uuidv7';\n\ndeclare const window: unknown;\n\n/**\n * The isomorphic SDK core (instrumentation §6) — ~80% of every client by\n * value. Zero heavy dependencies; platform entries are thin wiring over this.\n *\n * The wire contract it upholds:\n * - every record carries a client-generated UUIDv7 `_id` (the idempotency\n * contract — transports are at-least-once)\n * - trace ids are UUIDv7 too: schema §2.6 samples on the random hex tail\n * - batch context carries subjects/actor once, not per record\n * - `identify()` swaps subjects AND posts one `$identify` control record —\n * the input to the server-side stitching job\n */\n\nexport const newId = uuidv7;\n\nexport interface ClientContextInput {\n platform?: string;\n appVersion?: string;\n userAgent?: string;\n os?: string;\n osVersion?: string;\n browser?: string;\n browserVersion?: string;\n deviceType?: string;\n locale?: string;\n timezone?: string;\n screenW?: number;\n screenH?: number;\n viewportW?: number;\n viewportH?: number;\n connection?: string;\n online?: boolean;\n}\n\nexport interface TransportResult {\n ok: boolean;\n status?: number;\n}\n\nexport type Transport = (url: string, body: string, headers: Record<string, string>) => Promise<TransportResult>;\n\nexport interface ClientStorage {\n get(key: string): string | null | undefined;\n set(key: string, value: string): void;\n}\n\nexport interface WireRecord {\n _id: string;\n name: string;\n occurredAt: string;\n attrs?: Record<string, string>;\n metrics?: Record<string, number>;\n data?: Record<string, unknown>;\n body?: string;\n severity?: string;\n subjects?: Array<{ type: string; id: string; role?: string }>;\n actor?: string;\n traceId?: string;\n spanId?: string;\n parentId?: string;\n durationMs?: number;\n error?: unknown;\n state?: unknown;\n usage?: unknown;\n anonRef?: string;\n userRef?: string;\n}\n\nexport interface CreateClientOptions {\n /** pk_ for anything shipped to users; sk_ only in trusted processes */\n key: string;\n /** the mounted ingest endpoint, e.g. https://app.example.com/telemetry/ingest */\n url: string;\n /** injected at build; the key supplies service/env */\n release?: string;\n flushIntervalMs?: number;\n /** records per POST */\n maxBatchSize?: number;\n /** ring buffer cap — drop-OLDEST beyond it; new telemetry outranks stale */\n maxQueueSize?: number;\n /** failed flushes retry with backoff; a batch is abandoned after this many */\n maxRetries?: number;\n transport?: Transport;\n /** persists the anon id (and lets platforms persist queues); memory default */\n storage?: ClientStorage;\n clientContext?: ClientContextInput;\n /** false = drop instead of send. Web adapter wires DNT/GPC/consent here. */\n consent?: () => boolean;\n /** registry name used by captureError. Convention: 'error.unhandled'. */\n errorName?: string;\n /** SDK-internal failures — never thrown at the app */\n onError?: (e: unknown) => void;\n}\n\nexport interface Span {\n readonly traceId: string;\n readonly spanId: string;\n end(extra?: { attrs?: Record<string, string>; metrics?: Record<string, number> }): void;\n}\n\nconst SDK = { name: '@jeffjassky/telemetry', version: '0' };\n\nconst defaultTransport: Transport = async (url, body, headers) => {\n const res = await fetch(url, { method: 'POST', headers, body, keepalive: true });\n return { ok: res.ok, status: res.status };\n};\n\nconst memoryStorage = (): ClientStorage => {\n const m = new Map<string, string>();\n return { get: (k) => m.get(k), set: (k, v) => void m.set(k, v) };\n};\n\n/** stable, cheap error grouping — server-side fingerprinting can refine later */\nconst fingerprint = (type: string, message: string, frame: string): string => {\n const s = `${type}|${message.replace(/\\d+/g, 'N').slice(0, 200)}|${frame}`;\n let h = 5381;\n for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) >>> 0;\n return h.toString(16);\n};\n\nconst FRAME_RE = /^\\s*at\\s+(?:(.+?)\\s+\\()?(?:(.+?):(\\d+):(\\d+))\\)?\\s*$/;\n\nconst parseFrames = (stack: string | undefined) =>\n (stack ?? '')\n .split('\\n')\n .slice(1, 21)\n .map((line) => FRAME_RE.exec(line))\n .filter((m): m is RegExpExecArray => !!m)\n .map((m) => ({\n fn: m[1],\n filename: m[2],\n lineno: Number(m[3]),\n colno: Number(m[4]),\n }));\n\nexport function createClient(options: CreateClientOptions) {\n const {\n key,\n url,\n release,\n flushIntervalMs = 5000,\n maxBatchSize = 50,\n maxQueueSize = 1000,\n maxRetries = 5,\n transport = defaultTransport,\n storage = memoryStorage(),\n consent = () => true,\n errorName = 'error.unhandled',\n onError = () => {},\n } = options;\n\n // ClientContext must always be storable — platform/appVersion are required\n // by the envelope, so the core guarantees them and adapters refine them\n const clientContext = {\n platform: typeof window === 'undefined' ? 'server' : 'web',\n appVersion: release ?? 'unknown',\n ...options.clientContext,\n };\n\n // ── identity context ──\n let anonId = storage.get('telemetry_anon') ?? null;\n if (!anonId) {\n anonId = `anon_${newId()}`;\n try {\n storage.set('telemetry_anon', anonId);\n } catch (e) {\n onError(e);\n }\n }\n const sessionId = `ses_${newId()}`;\n /** current subjects, by type. anon + session ride every batch (schema §2.3). */\n const subjects = new Map<string, string>([\n ['anon', anonId],\n ['session', sessionId],\n ]);\n let actor: string | undefined;\n\n // ── queue ──\n const queue: WireRecord[] = [];\n let dropped = 0;\n let attempts = 0;\n let flushing: Promise<void> | null = null;\n let timer: ReturnType<typeof setInterval> | undefined;\n\n const enqueue = (rec: WireRecord) => {\n queue.push(rec);\n while (queue.length > maxQueueSize) {\n queue.shift(); // drop-oldest: fresh telemetry outranks stale\n dropped++;\n }\n };\n\n // ── tracing ──\n const spanStack: Array<{ traceId: string; spanId: string }> = [];\n const activeTrace = () => spanStack[spanStack.length - 1];\n\n const doFlush = async (): Promise<void> => {\n if (!queue.length) return;\n if (!consent()) {\n queue.length = 0; // no consent = no send AND no hoard\n return;\n }\n while (queue.length) {\n const batch = queue.slice(0, maxBatchSize);\n const body = JSON.stringify({\n sdk: SDK,\n sentAt: new Date().toISOString(),\n release,\n client: clientContext,\n context: {\n subjects: [...subjects].map(([type, id]) => ({ type, id })),\n ...(actor ? { actor } : {}),\n },\n records: batch,\n });\n try {\n const res = await transport(url, body, {\n 'content-type': 'application/json',\n authorization: `Bearer ${key}`,\n });\n if (!res.ok) throw new Error(`ingest ${res.status}`);\n queue.splice(0, batch.length);\n attempts = 0;\n } catch (e) {\n // records stay queued; the SAME _ids retry, so the server dedupes\n attempts++;\n if (attempts >= maxRetries) {\n queue.splice(0, batch.length);\n dropped += batch.length;\n attempts = 0;\n }\n onError(e);\n return; // back off until the next interval\n }\n }\n };\n\n const flush = () => (flushing ??= doFlush().finally(() => (flushing = null)));\n\n if (flushIntervalMs > 0) {\n timer = setInterval(() => void flush(), flushIntervalMs);\n (timer as { unref?: () => void }).unref?.(); // never hold a node process open\n }\n\n const client = {\n track(\n name: string,\n opts: {\n attrs?: Record<string, string>;\n metrics?: Record<string, number>;\n data?: Record<string, unknown>;\n occurredAt?: Date;\n subjects?: Array<{ type: string; id: string; role?: string }>;\n severity?: string;\n } = {},\n ) {\n const trace = activeTrace();\n enqueue({\n _id: newId(),\n name,\n occurredAt: (opts.occurredAt ?? new Date()).toISOString(),\n ...(opts.attrs ? { attrs: opts.attrs } : {}),\n ...(opts.metrics ? { metrics: opts.metrics } : {}),\n ...(opts.data ? { data: opts.data } : {}),\n ...(opts.subjects ? { subjects: opts.subjects } : {}),\n ...(opts.severity ? { severity: opts.severity } : {}),\n ...(trace ? { traceId: trace.traceId, parentId: trace.spanId } : {}),\n });\n },\n\n captureError(err: unknown, ctx: { handled?: boolean; name?: string; attrs?: Record<string, string> } = {}) {\n const e = err instanceof Error ? err : new Error(String(err));\n const frames = parseFrames(e.stack);\n const trace = activeTrace();\n enqueue({\n _id: newId(),\n name: ctx.name ?? errorName,\n occurredAt: new Date().toISOString(),\n severity: 'error',\n ...(ctx.attrs ? { attrs: ctx.attrs } : {}),\n ...(trace ? { traceId: trace.traceId, parentId: trace.spanId } : {}),\n error: {\n type: e.name || 'Error',\n message: e.message,\n handled: ctx.handled ?? true,\n fingerprint: fingerprint(e.name, e.message, frames[0]?.filename ?? ''),\n frames,\n },\n });\n },\n\n startSpan(name: string, opts: { attrs?: Record<string, string> } = {}): Span {\n const parent = activeTrace();\n const traceId = parent?.traceId ?? newId();\n const spanId = newId();\n const startedAt = Date.now();\n const frame = { traceId, spanId };\n spanStack.push(frame);\n let ended = false;\n return {\n traceId,\n spanId,\n end: (extra = {}) => {\n if (ended) return;\n ended = true;\n const i = spanStack.indexOf(frame);\n if (i >= 0) spanStack.splice(i, 1);\n enqueue({\n _id: newId(),\n name,\n occurredAt: new Date(startedAt).toISOString(),\n traceId,\n spanId,\n ...(parent ? { parentId: parent.spanId } : {}),\n durationMs: Date.now() - startedAt,\n ...(opts.attrs || extra.attrs ? { attrs: { ...opts.attrs, ...extra.attrs } } : {}),\n ...(extra.metrics ? { metrics: extra.metrics } : {}),\n });\n },\n };\n },\n\n state(name: string, st: { key: string; from?: string; to: string; previousSinceMs?: number }) {\n enqueue({ _id: newId(), name, occurredAt: new Date().toISOString(), state: st });\n },\n\n /**\n * Swap in the real identity. Future batches carry the new subjects; one\n * `$identify` control record links anon → user for the stitching job.\n * The anon subject stays on the context — stitching consumes the alias.\n */\n identify(ids: Record<string, string | null | undefined>) {\n const hadUser = subjects.get('user');\n for (const [type, id] of Object.entries(ids)) {\n if (id == null) subjects.delete(type);\n else subjects.set(type, String(id));\n }\n const user = subjects.get('user');\n if (user && user !== hadUser && anonId) {\n enqueue({\n _id: newId(),\n name: '$identify',\n occurredAt: new Date().toISOString(),\n anonRef: `anon:${anonId}`,\n userRef: `user:${user}`,\n });\n }\n if (subjects.has('user')) actor = `user:${subjects.get('user')}`;\n },\n\n setActor(ref: string | undefined) {\n actor = ref;\n },\n\n flush,\n\n async shutdown() {\n if (timer) clearInterval(timer);\n await flush();\n },\n\n /** introspection for adapters and tests — not a public contract */\n _internal: {\n queue,\n subjects,\n get anonId() {\n return anonId!;\n },\n sessionId,\n get dropped() {\n return dropped;\n },\n enqueue,\n },\n };\n\n return client;\n}\n\nexport type TelemetryClient = ReturnType<typeof createClient>;\n\n// The isomorphic registry surface (instrumentation §8): the host's registry\n// file imports defineRegistry from '/core' so server AND clients can import\n// it — server at runtime, clients `import type` only, so zod never reaches a\n// bundle. This chain must stay mongoose/express-free.\nexport { defineRegistry, boundedMeta } from '../server/registry.js';\nexport type { Registry, EventSpec, RollupSpec, DimSource } from '../server/registry.js';\n","import { mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { createClient, type CreateClientOptions, type TelemetryClient } from './core.js';\n\n/**\n * CLI wiring (instrumentation §7): disk queue in the config dir — flushed on\n * exit AND on next run, because exit flushes get killed — persistent\n * machine-scoped anon id, and hard DO_NOT_TRACK / --no-telemetry respect.\n */\n\nexport interface CliTelemetryOptions extends Omit<CreateClientOptions, 'storage'> {\n /** where the anon id and offline queue live, e.g. ~/.config/mytool */\n configDir: string;\n /** argv to scan for --no-telemetry (default process.argv) */\n argv?: string[];\n /** age cap for replayed offline records — older ones drop (default 7d) */\n maxQueueAgeMs?: number;\n}\n\nconst noopClient = (opts: CreateClientOptions): TelemetryClient =>\n // a disabled CLI client is the same client with consent hard-off: every\n // call works, nothing is ever sent or stored\n createClient({ ...opts, flushIntervalMs: 0, consent: () => false });\n\nexport function createCliTelemetry(opts: CliTelemetryOptions): TelemetryClient {\n const { configDir, argv = typeof process !== 'undefined' ? process.argv : [], maxQueueAgeMs = 7 * 864e5, ...rest } = opts;\n\n const optedOut =\n (typeof process !== 'undefined' &&\n (process.env.DO_NOT_TRACK === '1' || process.env.TELEMETRY_DISABLED === '1')) ||\n argv.includes('--no-telemetry');\n if (optedOut) return noopClient(rest);\n\n try {\n mkdirSync(configDir, { recursive: true });\n } catch {}\n\n const file = (name: string) => join(configDir, name);\n const storage = {\n get: (k: string) => {\n try {\n return readFileSync(file(`${k}.txt`), 'utf8').trim();\n } catch {\n return null;\n }\n },\n set: (k: string, v: string) => {\n try {\n writeFileSync(file(`${k}.txt`), v);\n } catch {}\n },\n };\n\n const client = createClient({\n ...rest,\n storage,\n clientContext: { platform: 'cli', appVersion: rest.release ?? 'unknown', ...rest.clientContext },\n });\n\n // ── next-run replay: whatever last exit could not send ──\n const queueFile = file('telemetry-queue.json');\n try {\n const saved = JSON.parse(readFileSync(queueFile, 'utf8'));\n rmSync(queueFile, { force: true });\n if (Array.isArray(saved)) {\n const floor = Date.now() - maxQueueAgeMs;\n for (const rec of saved) {\n // the age cap keeps a machine offline for a month from replaying a\n // month of stale telemetry into today's buckets (open item §10)\n if (rec && typeof rec._id === 'string' && Date.parse(rec.occurredAt) >= floor) {\n client._internal.enqueue(rec);\n }\n }\n }\n } catch {}\n\n // ── exit save: flushes get killed, disk writes are sync ──\n if (typeof process !== 'undefined') {\n process.on('exit', () => {\n const q = client._internal.queue;\n if (q.length) {\n try {\n writeFileSync(queueFile, JSON.stringify(q));\n } catch {}\n }\n });\n }\n\n return client;\n}\n\nexport { createClient } from './core.js';\nexport type { TelemetryClient } from './core.js';\n"]}
|