@antseed/telemetry 0.0.0-stage → 0.1.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.
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Bounded PostHog transport. Captures queue in memory and leave in batches
3
+ * (`/batch/`), with a short timeout and no retries. Every failure is
4
+ * swallowed: telemetry never affects a request, startup or shutdown.
5
+ */
6
+ export const POSTHOG_CAPTURE_TIMEOUT_MS = 4_000;
7
+ export function createPostHogSender(options) {
8
+ const batchUrl = `${options.host.replace(/\/+$/, '')}/batch/`;
9
+ const timeoutMs = options.timeoutMs ?? POSTHOG_CAPTURE_TIMEOUT_MS;
10
+ return async (events) => {
11
+ if (events.length === 0)
12
+ return;
13
+ try {
14
+ await fetch(batchUrl, {
15
+ method: 'POST',
16
+ headers: { 'content-type': 'application/json' },
17
+ body: JSON.stringify({ api_key: options.projectApiKey, batch: events }),
18
+ signal: AbortSignal.timeout(timeoutMs),
19
+ });
20
+ }
21
+ catch {
22
+ // PostHog outages, offline machines, DNS failures: all ignored.
23
+ }
24
+ };
25
+ }
26
+ export class BatchQueue {
27
+ options;
28
+ queue = [];
29
+ timer = null;
30
+ inFlight = null;
31
+ maxQueue;
32
+ maxBatch;
33
+ flushIntervalMs;
34
+ constructor(options) {
35
+ this.options = options;
36
+ this.maxQueue = options.maxQueue ?? 1_000;
37
+ this.maxBatch = options.maxBatch ?? 100;
38
+ this.flushIntervalMs = options.flushIntervalMs ?? 2_000;
39
+ }
40
+ get size() {
41
+ return this.queue.length;
42
+ }
43
+ push(event) {
44
+ this.queue.push(event);
45
+ if (this.queue.length > this.maxQueue)
46
+ this.queue.splice(0, this.queue.length - this.maxQueue);
47
+ if (!this.timer) {
48
+ this.timer = setTimeout(() => {
49
+ this.timer = null;
50
+ void this.flush();
51
+ }, this.flushIntervalMs);
52
+ this.timer.unref?.();
53
+ }
54
+ }
55
+ /** Sends everything queued. Safe to call concurrently; never rejects. */
56
+ async flush() {
57
+ if (this.inFlight)
58
+ await this.inFlight;
59
+ while (this.queue.length > 0) {
60
+ // Before every batch: an opt-out while an earlier batch was sending stops the rest.
61
+ if (this.options.isEnabled && !this.options.isEnabled()) {
62
+ this.queue.length = 0;
63
+ return;
64
+ }
65
+ const batch = this.queue.splice(0, this.maxBatch);
66
+ // Through a promise, so a sender that throws before returning one is contained too.
67
+ this.inFlight = Promise.resolve().then(() => this.options.send(batch)).catch(() => undefined);
68
+ await this.inFlight;
69
+ this.inFlight = null;
70
+ }
71
+ }
72
+ /** Flushes, waiting at most `timeoutMs`. */
73
+ async close(timeoutMs = 750) {
74
+ if (this.timer) {
75
+ clearTimeout(this.timer);
76
+ this.timer = null;
77
+ }
78
+ let timeout = null;
79
+ await Promise.race([
80
+ this.flush(),
81
+ new Promise((resolve) => {
82
+ timeout = setTimeout(resolve, timeoutMs);
83
+ timeout.unref?.();
84
+ }),
85
+ ]);
86
+ if (timeout)
87
+ clearTimeout(timeout);
88
+ }
89
+ }
90
+ //# sourceMappingURL=posthog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"posthog.js","sourceRoot":"","sources":["../src/posthog.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The privacy boundary. Every property of every event passes through here:
3
+ * - keys not on the event's allowlist are dropped;
4
+ * - a key with allowed values or a pattern takes only strings, and a key
5
+ * with a range only numbers: a value of another type is dropped;
6
+ * - string values with a validator must be one of its values;
7
+ * - strings without one are truncated to 64 characters (and may be pattern-checked);
8
+ * - numbers are rounded and bounded; booleans pass; anything else is dropped.
9
+ */
10
+ export declare const MAX_STRING_LENGTH = 64;
11
+ /** What an event may carry: allowed keys, value sets and patterns. */
12
+ export type EventSchema = {
13
+ /** Allowed property keys. */
14
+ keys: ReadonlySet<string>;
15
+ /** Allowed values per string property. */
16
+ values?: Readonly<Record<string, ReadonlySet<string>>>;
17
+ /** Required shape per free-form string property. */
18
+ patterns?: Readonly<Record<string, RegExp>>;
19
+ /** Numeric properties that must be integers in [min, max]. */
20
+ ranges?: Readonly<Record<string, readonly [number, number]>>;
21
+ };
22
+ export type EventCatalog = Readonly<Record<string, EventSchema>>;
23
+ export type SanitizedProperties = Record<string, string | number | boolean>;
24
+ /** Public model ids: a conservative slug, or the fixed placeholder. */
25
+ export declare const PUBLIC_MODEL_ID_PATTERN: RegExp;
26
+ /** Random v4 UUIDs, for correlating two events of one request. */
27
+ export declare const RANDOM_REQUEST_ID_PATTERN: RegExp;
28
+ export declare const HTTP_STATUS_RANGE: readonly [number, number];
29
+ /** Returns null when the event is not in the catalog: unknown events are never sent. */
30
+ export declare function sanitizeEventProperties(catalog: EventCatalog, event: string, properties: Readonly<Record<string, unknown>> | null | undefined): SanitizedProperties | null;
31
+ //# sourceMappingURL=sanitize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../src/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC,sEAAsE;AACtE,MAAM,MAAM,WAAW,GAAG;IACxB,6BAA6B;IAC7B,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1B,0CAA0C;IAC1C,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACvD,oDAAoD;IACpD,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5C,8DAA8D;IAC9D,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;CAC9D,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;AAEjE,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;AAE5E,uEAAuE;AACvE,eAAO,MAAM,uBAAuB,QAAwD,CAAC;AAC7F,kEAAkE;AAClE,eAAO,MAAM,yBAAyB,QAA2E,CAAC;AAClH,eAAO,MAAM,iBAAiB,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAc,CAAC;AAEvE,wFAAwF;AACxF,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,YAAY,EACrB,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,GAC/D,mBAAmB,GAAG,IAAI,CA+B5B"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The privacy boundary. Every property of every event passes through here:
3
+ * - keys not on the event's allowlist are dropped;
4
+ * - a key with allowed values or a pattern takes only strings, and a key
5
+ * with a range only numbers: a value of another type is dropped;
6
+ * - string values with a validator must be one of its values;
7
+ * - strings without one are truncated to 64 characters (and may be pattern-checked);
8
+ * - numbers are rounded and bounded; booleans pass; anything else is dropped.
9
+ */
10
+ export const MAX_STRING_LENGTH = 64;
11
+ /** Public model ids: a conservative slug, or the fixed placeholder. */
12
+ export const PUBLIC_MODEL_ID_PATTERN = /^(?:custom_or_unknown|[a-z0-9][a-z0-9._:/-]{0,63})$/;
13
+ /** Random v4 UUIDs, for correlating two events of one request. */
14
+ export const RANDOM_REQUEST_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
15
+ export const HTTP_STATUS_RANGE = [100, 599];
16
+ /** Returns null when the event is not in the catalog: unknown events are never sent. */
17
+ export function sanitizeEventProperties(catalog, event, properties) {
18
+ const schema = catalog[event];
19
+ if (!schema)
20
+ return null;
21
+ const out = {};
22
+ for (const [key, value] of Object.entries(properties ?? {})) {
23
+ if (!schema.keys.has(key))
24
+ continue;
25
+ const stringOnly = schema.values?.[key] !== undefined || schema.patterns?.[key] !== undefined;
26
+ const numberOnly = schema.ranges?.[key] !== undefined;
27
+ if (stringOnly && typeof value !== 'string')
28
+ continue;
29
+ if (numberOnly && typeof value !== 'number')
30
+ continue;
31
+ if (typeof value === 'boolean') {
32
+ out[key] = value;
33
+ }
34
+ else if (typeof value === 'number') {
35
+ if (!Number.isFinite(value))
36
+ continue;
37
+ const range = schema.ranges?.[key];
38
+ if (range) {
39
+ if (!Number.isInteger(value) || value < range[0] || value > range[1])
40
+ continue;
41
+ out[key] = value;
42
+ continue;
43
+ }
44
+ out[key] = Math.round(Math.max(-Number.MAX_SAFE_INTEGER, Math.min(Number.MAX_SAFE_INTEGER, value)));
45
+ }
46
+ else if (typeof value === 'string') {
47
+ const allowed = schema.values?.[key];
48
+ if (allowed && !allowed.has(value))
49
+ continue;
50
+ const pattern = schema.patterns?.[key];
51
+ if (pattern && !pattern.test(value))
52
+ continue;
53
+ out[key] = value.length > MAX_STRING_LENGTH ? value.slice(0, MAX_STRING_LENGTH) : value;
54
+ }
55
+ // Objects, arrays, null, undefined and functions are dropped.
56
+ }
57
+ return out;
58
+ }
59
+ //# sourceMappingURL=sanitize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.js","sourceRoot":"","sources":["../src/sanitize.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,27 @@
1
1
  {
2
2
  "name": "@antseed/telemetry",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "description": "Privacy-safe product telemetry shared by the Antseed desktop app, gateway and buyer: buckets, failure classifiers, a strict sanitizer, consent resolution and a bounded PostHog transport",
5
+ "type": "module",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "main": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "types": "./dist/index.d.ts"
15
+ }
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^20.11.0",
19
+ "typescript": "^5.5.0"
20
+ },
21
+ "scripts": {
22
+ "prebuild": "node ../../scripts/remove-paths.mjs dist",
23
+ "build": "tsc",
24
+ "test": "node ../../scripts/remove-paths.mjs test-dist && tsc -p tsconfig.test.json && node --test test-dist/*.test.js",
25
+ "typecheck": "tsc --noEmit"
26
+ }
6
27
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.