@timo972/cc-router 0.12.1 → 0.12.2-rc.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/Dockerfile +1 -0
  3. package/README.md +1 -1
  4. package/dist/cli/cmd-accounts.js +116 -65
  5. package/dist/cli/cmd-setup.js +100 -30
  6. package/dist/cli/cmd-status.js +14 -2
  7. package/dist/cli/cmd-telemetry.js +43 -32
  8. package/dist/cli/index.js +15 -1
  9. package/dist/config/directory.js +14 -0
  10. package/dist/config/telemetry.js +192 -41
  11. package/dist/providers/anthropic/usage-refresher.js +35 -1
  12. package/dist/providers/model-discovery.js +17 -11
  13. package/dist/providers/openai/device-oauth.js +88 -31
  14. package/dist/providers/openai/token-refresher.js +12 -2
  15. package/dist/providers/openai/usage-fetch.js +19 -2
  16. package/dist/proxy/anthropic-messages-route.js +144 -5
  17. package/dist/proxy/anthropic-proxy.js +10 -0
  18. package/dist/proxy/anthropic-response-capture.js +6 -19
  19. package/dist/proxy/openai-ingress.js +98 -1
  20. package/dist/proxy/server.js +35 -22
  21. package/dist/proxy/token-refresher.js +12 -2
  22. package/dist/proxy/usage-capture.js +41 -4
  23. package/dist/telemetry/contracts.js +129 -0
  24. package/dist/telemetry/facade.js +654 -0
  25. package/dist/telemetry/otel-exporters.js +289 -0
  26. package/dist/telemetry/posthog-client.js +398 -0
  27. package/dist/telemetry/privacy.js +567 -0
  28. package/dist/telemetry/runtime.js +306 -0
  29. package/dist/telemetry/setup-diagnostics.js +239 -0
  30. package/dist/utils/token-extractor.js +79 -11
  31. package/dist/utils/token-validator.js +25 -9
  32. package/docs/README.md +34 -0
  33. package/docs/telemetry.md +167 -0
  34. package/package.json +12 -2
  35. package/dist/utils/telemetry.js +0 -88
@@ -0,0 +1,289 @@
1
+ import { SpanKind, SpanStatusCode, TraceFlags } from "@opentelemetry/api";
2
+ import { SeverityNumber } from "@opentelemetry/api-logs";
3
+ import { ExporterMetrics, OTLPExporterBase, createOtlpNetworkExportDelegate, } from "@opentelemetry/otlp-exporter-base";
4
+ import { JsonLogsSerializer, LogsExporterMetricsHelper, ProtobufTraceSerializer, TraceExporterMetricsHelper, } from "@opentelemetry/otlp-transformer";
5
+ import { resourceFromAttributes } from "@opentelemetry/resources";
6
+ import { POSTHOG_HOST, POSTHOG_PROJECT_TOKEN, POSTHOG_REQUEST_TIMEOUT_MS, } from "./contracts.js";
7
+ import { fromOtelAttributes, LOG_ATTRIBUTE_KEYS, RESOURCE_ATTRIBUTE_KEYS, reconstructLog, reconstructResource, reconstructSpan, SPAN_ATTRIBUTE_KEYS, toOtelAttributes, } from "./privacy.js";
8
+ const SUCCESS = { code: 0 };
9
+ const FAILED = { code: 1 };
10
+ const EXPORT_TIMEOUT_MS = POSTHOG_REQUEST_TIMEOUT_MS;
11
+ const LIFECYCLE_TIMEOUT_MS = 2_000;
12
+ const MAX_BATCH_SIZE = 100;
13
+ const POSTHOG_TRACE_URL = `${POSTHOG_HOST}/i/v1/traces`;
14
+ const POSTHOG_LOG_URL = `${POSTHOG_HOST}/i/v1/logs`;
15
+ function own(input, key) {
16
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
17
+ return descriptor && "value" in descriptor ? descriptor.value : undefined;
18
+ }
19
+ async function settleWithin(operation, timeoutMs) {
20
+ let timer;
21
+ try {
22
+ await Promise.race([
23
+ Promise.resolve().then(operation).catch(() => undefined),
24
+ new Promise(resolve => {
25
+ timer = setTimeout(resolve, timeoutMs);
26
+ timer.unref?.();
27
+ }),
28
+ ]);
29
+ }
30
+ catch {
31
+ // Telemetry lifecycle failures are isolated from application behavior.
32
+ }
33
+ finally {
34
+ clearTimeout(timer);
35
+ }
36
+ }
37
+ function hrTimeToMilliseconds(time) {
38
+ return time[0] * 1_000 + time[1] / 1_000_000;
39
+ }
40
+ function millisecondsToHrTime(milliseconds) {
41
+ const seconds = Math.floor(milliseconds / 1_000);
42
+ return [seconds, Math.round((milliseconds - seconds * 1_000) * 1_000_000)];
43
+ }
44
+ function spanKindName(kind) {
45
+ switch (kind) {
46
+ case SpanKind.INTERNAL: return "internal";
47
+ case SpanKind.SERVER: return "server";
48
+ case SpanKind.CLIENT: return "client";
49
+ default: return undefined;
50
+ }
51
+ }
52
+ function otelSpanKind(kind) {
53
+ switch (kind) {
54
+ case "internal": return SpanKind.INTERNAL;
55
+ case "server": return SpanKind.SERVER;
56
+ case "client": return SpanKind.CLIENT;
57
+ }
58
+ }
59
+ function spanStatusName(code) {
60
+ switch (code) {
61
+ case SpanStatusCode.UNSET: return "unset";
62
+ case SpanStatusCode.OK: return "ok";
63
+ case SpanStatusCode.ERROR: return "error";
64
+ default: return undefined;
65
+ }
66
+ }
67
+ function otelSpanStatus(code) {
68
+ switch (code) {
69
+ case "unset": return SpanStatusCode.UNSET;
70
+ case "ok": return SpanStatusCode.OK;
71
+ case "error": return SpanStatusCode.ERROR;
72
+ }
73
+ }
74
+ function severityName(value) {
75
+ switch (value) {
76
+ case SeverityNumber.INFO: return "info";
77
+ case SeverityNumber.WARN: return "warn";
78
+ case SeverityNumber.ERROR: return "error";
79
+ case SeverityNumber.FATAL: return "fatal";
80
+ default: return undefined;
81
+ }
82
+ }
83
+ function otelSeverity(value) {
84
+ switch (value) {
85
+ case "info": return { text: "INFO", number: SeverityNumber.INFO };
86
+ case "warn": return { text: "WARN", number: SeverityNumber.WARN };
87
+ case "error": return { text: "ERROR", number: SeverityNumber.ERROR };
88
+ case "fatal": return { text: "FATAL", number: SeverityNumber.FATAL };
89
+ }
90
+ }
91
+ function safeResource(candidate, installationId) {
92
+ return reconstructResource(fromOtelAttributes(RESOURCE_ATTRIBUTE_KEYS, candidate.attributes), { installationId });
93
+ }
94
+ function rebuildSpan(candidate, snapshot) {
95
+ try {
96
+ const context = candidate.spanContext();
97
+ const resource = safeResource(candidate.resource, snapshot.state.installId);
98
+ const safe = reconstructSpan({
99
+ scope: candidate.instrumentationScope.name,
100
+ operation: own(candidate.attributes, "cc_router.operation"),
101
+ traceId: context.traceId,
102
+ spanId: context.spanId,
103
+ parentSpanId: candidate.parentSpanContext?.spanId,
104
+ kind: spanKindName(candidate.kind),
105
+ startTimeMs: hrTimeToMilliseconds(candidate.startTime),
106
+ durationMs: hrTimeToMilliseconds(candidate.duration),
107
+ statusCode: spanStatusName(candidate.status.code),
108
+ attributes: fromOtelAttributes(SPAN_ATTRIBUTE_KEYS, candidate.attributes),
109
+ });
110
+ if (!resource || !safe)
111
+ return undefined;
112
+ return newReadableSpan(safe, resource);
113
+ }
114
+ catch {
115
+ return undefined;
116
+ }
117
+ }
118
+ function newReadableSpan(safe, resource) {
119
+ const spanContext = { traceId: safe.traceId, spanId: safe.spanId, traceFlags: TraceFlags.SAMPLED };
120
+ return {
121
+ name: safe.name,
122
+ kind: otelSpanKind(safe.kind),
123
+ spanContext: () => ({ ...spanContext }),
124
+ ...(safe.parentSpanId === undefined ? {} : {
125
+ parentSpanContext: {
126
+ traceId: safe.traceId,
127
+ spanId: safe.parentSpanId,
128
+ traceFlags: TraceFlags.SAMPLED,
129
+ },
130
+ }),
131
+ startTime: millisecondsToHrTime(safe.startTimeMs),
132
+ endTime: millisecondsToHrTime(safe.startTimeMs + safe.durationMs),
133
+ duration: millisecondsToHrTime(safe.durationMs),
134
+ status: { code: otelSpanStatus(safe.statusCode) },
135
+ attributes: toOtelAttributes(SPAN_ATTRIBUTE_KEYS, safe.attributes),
136
+ links: [],
137
+ events: [],
138
+ ended: true,
139
+ resource: resourceFromAttributes({ ...resource }),
140
+ instrumentationScope: { name: safe.scope },
141
+ droppedAttributesCount: 0,
142
+ droppedEventsCount: 0,
143
+ droppedLinksCount: 0,
144
+ };
145
+ }
146
+ function rebuildLog(candidate, snapshot) {
147
+ try {
148
+ const resource = safeResource(candidate.resource, snapshot.state.installId);
149
+ // The diagnostic id is a per-attempt random UUID; reconstructLog still
150
+ // validates it and rejects anything equal to the install identity.
151
+ const diagnosticId = own(candidate.attributes, LOG_ATTRIBUTE_KEYS.diagnosticId);
152
+ const safe = reconstructLog({
153
+ scope: candidate.instrumentationScope.name,
154
+ body: candidate.body,
155
+ severity: severityName(candidate.severityNumber),
156
+ timestampMs: hrTimeToMilliseconds(candidate.hrTime),
157
+ traceId: candidate.spanContext?.traceId,
158
+ spanId: candidate.spanContext?.spanId,
159
+ attributes: fromOtelAttributes(LOG_ATTRIBUTE_KEYS, candidate.attributes),
160
+ }, {
161
+ installationId: snapshot.state.installId,
162
+ ...(typeof diagnosticId === "string" ? { diagnosticId } : {}),
163
+ });
164
+ if (!resource || !safe)
165
+ return undefined;
166
+ return newReadableLog(safe, resource);
167
+ }
168
+ catch {
169
+ return undefined;
170
+ }
171
+ }
172
+ function newReadableLog(safe, resource) {
173
+ const severity = otelSeverity(safe.severity);
174
+ const hrTime = millisecondsToHrTime(safe.timestampMs);
175
+ return {
176
+ hrTime,
177
+ hrTimeObserved: [...hrTime],
178
+ ...(safe.traceId && safe.spanId
179
+ ? { spanContext: { traceId: safe.traceId, spanId: safe.spanId, traceFlags: TraceFlags.SAMPLED } }
180
+ : {}),
181
+ severityText: severity.text,
182
+ severityNumber: severity.number,
183
+ body: safe.body,
184
+ resource: resourceFromAttributes({ ...resource }),
185
+ instrumentationScope: { name: safe.scope },
186
+ attributes: toOtelAttributes(LOG_ATTRIBUTE_KEYS, safe.attributes),
187
+ droppedAttributesCount: 0,
188
+ };
189
+ }
190
+ /**
191
+ * Wrap an OTLP delegate so nothing reaches the network unless consent still
192
+ * holds, and only records rebuilt from the closed allowlist are handed over.
193
+ */
194
+ function gatedExporter(getSnapshot, rebuild, delegate) {
195
+ return {
196
+ export(records, callback) {
197
+ let settled = false;
198
+ let timer;
199
+ const settle = (result) => {
200
+ if (settled)
201
+ return;
202
+ settled = true;
203
+ clearTimeout(timer);
204
+ callback(result);
205
+ };
206
+ try {
207
+ const snapshot = getSnapshot();
208
+ if (!snapshot) {
209
+ settle(SUCCESS);
210
+ return;
211
+ }
212
+ const safe = records.slice(0, MAX_BATCH_SIZE).flatMap(record => {
213
+ const rebuilt = rebuild(record, snapshot);
214
+ return rebuilt ? [rebuilt] : [];
215
+ });
216
+ // Re-check: an opt-out during reconstruction must still drop the batch.
217
+ if (safe.length === 0 || !getSnapshot()) {
218
+ settle(SUCCESS);
219
+ return;
220
+ }
221
+ timer = setTimeout(() => settle(FAILED), EXPORT_TIMEOUT_MS);
222
+ timer.unref?.();
223
+ delegate.export(safe, result => settle(result.code === 0 ? SUCCESS : FAILED));
224
+ }
225
+ catch {
226
+ settle(FAILED);
227
+ }
228
+ },
229
+ async forceFlush() {
230
+ if (!getSnapshot())
231
+ return;
232
+ await settleWithin(() => delegate.forceFlush?.(), LIFECYCLE_TIMEOUT_MS);
233
+ },
234
+ async shutdown() {
235
+ await settleWithin(() => delegate.shutdown(), LIFECYCLE_TIMEOUT_MS);
236
+ },
237
+ };
238
+ }
239
+ /**
240
+ * One HTTP POST per export, consent re-read immediately before it. The SDK's
241
+ * stock exporters wrap their transport in a retrying layer that would resend a
242
+ * batch after a 503 without consulting consent again; this transport never
243
+ * reports a retryable result, so nothing is transmitted twice.
244
+ */
245
+ function consentGatedTransport(url, contentType, getSnapshot) {
246
+ return {
247
+ async send(data, timeoutMillis) {
248
+ if (!getSnapshot()?.enabled)
249
+ return { status: "success" };
250
+ try {
251
+ const response = await fetch(url, {
252
+ method: "POST",
253
+ headers: {
254
+ "content-type": contentType,
255
+ authorization: `Bearer ${POSTHOG_PROJECT_TOKEN}`,
256
+ },
257
+ body: new Uint8Array(data),
258
+ signal: AbortSignal.timeout(Math.max(1, timeoutMillis)),
259
+ });
260
+ if (response.ok)
261
+ return { status: "success" };
262
+ return { status: "failure", error: new Error(`OTLP export rejected with HTTP ${response.status}`) };
263
+ }
264
+ catch {
265
+ // The raw transport error may name hosts or addresses; it stays local.
266
+ return { status: "failure", error: new Error("OTLP export failed") };
267
+ }
268
+ },
269
+ shutdown() { },
270
+ };
271
+ }
272
+ function otlpExporter(url, contentType, serializer, metricsHelper, getSnapshot) {
273
+ const options = { timeoutMillis: POSTHOG_REQUEST_TIMEOUT_MS, concurrencyLimit: 1, compression: "none" };
274
+ return new OTLPExporterBase(createOtlpNetworkExportDelegate(options, serializer, new ExporterMetrics({
275
+ componentType: "cc_router_otlp_exporter",
276
+ metricsHelper,
277
+ url,
278
+ meterProvider: undefined,
279
+ responseAttributesFromError: () => ({}),
280
+ }), consentGatedTransport(url, contentType, getSnapshot)));
281
+ }
282
+ export function createPostHogOtlpExporters(options) {
283
+ const traceUrl = options.traceUrl ?? POSTHOG_TRACE_URL;
284
+ const logUrl = options.logUrl ?? POSTHOG_LOG_URL;
285
+ return {
286
+ spanExporter: gatedExporter(options.getSnapshot, rebuildSpan, otlpExporter(traceUrl, "application/x-protobuf", ProtobufTraceSerializer, TraceExporterMetricsHelper, options.getSnapshot)),
287
+ logExporter: gatedExporter(options.getSnapshot, rebuildLog, otlpExporter(logUrl, "application/json", JsonLogsSerializer, LogsExporterMetricsHelper, options.getSnapshot)),
288
+ };
289
+ }
@@ -0,0 +1,398 @@
1
+ import { PostHog } from "posthog-node";
2
+ import { ERROR_KINDS, MAX_STACK_FRAMES, MAX_STACK_FRAME_PATH_LENGTH, OPERATIONS, POSTHOG_FLUSH_AT, POSTHOG_FLUSH_INTERVAL_MS, POSTHOG_HOST, POSTHOG_MAX_QUEUE_SIZE, POSTHOG_PROJECT_TOKEN, POSTHOG_REQUEST_TIMEOUT_MS, PROVIDERS, RUNTIME_MODES, SETUP_REASONS, SETUP_STAGES, SYSTEM_ERROR_CODES, } from "./contracts.js";
3
+ import { reconstructAnalyticsEvent } from "./privacy.js";
4
+ /** Stamped on every capture and re-checked in before_send against live consent. */
5
+ const CONSENT_GENERATION_PROPERTY = "cc_router.consent_generation";
6
+ const QUEUE_KEYS = ["queue", "ai_queue", "logs_queue"];
7
+ function isRecord(value) {
8
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9
+ }
10
+ function own(input, key) {
11
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
12
+ return descriptor && "value" in descriptor ? descriptor.value : undefined;
13
+ }
14
+ function member(values, value) {
15
+ return typeof value === "string" && values.includes(value)
16
+ ? value
17
+ : undefined;
18
+ }
19
+ function uuid(value) {
20
+ if (typeof value !== "string")
21
+ return undefined;
22
+ const normalized = value.toLowerCase();
23
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(normalized)
24
+ ? normalized
25
+ : undefined;
26
+ }
27
+ function positiveInteger(value) {
28
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 1 ? value : undefined;
29
+ }
30
+ function httpStatusCode(value) {
31
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 100 && value <= 599
32
+ ? value
33
+ : undefined;
34
+ }
35
+ function optionalMember(values, value) {
36
+ if (value === undefined)
37
+ return { valid: true };
38
+ const normalized = member(values, value);
39
+ return normalized === undefined ? { valid: false } : { valid: true, value: normalized };
40
+ }
41
+ function safeFramePath(value) {
42
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_STACK_FRAME_PATH_LENGTH) {
43
+ return undefined;
44
+ }
45
+ if (!value.startsWith("dist/") && !value.startsWith("node_modules/"))
46
+ return undefined;
47
+ const segments = value.split("/");
48
+ if (segments.some(segment => segment.length === 0
49
+ || segment === "."
50
+ || segment === ".."
51
+ || /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment)
52
+ || !/^[0-9A-Za-z@._+~-]+$/.test(segment))) {
53
+ return undefined;
54
+ }
55
+ if (segments[0] === "node_modules") {
56
+ const packageSegments = segments[1]?.startsWith("@") ? 2 : 1;
57
+ if (segments.length < packageSegments + 2)
58
+ return undefined;
59
+ }
60
+ return value;
61
+ }
62
+ function reconstructExceptionFrames(input) {
63
+ if (!Array.isArray(input) || input.length > MAX_STACK_FRAMES)
64
+ return undefined;
65
+ const frames = [];
66
+ for (const candidate of input) {
67
+ if (!isRecord(candidate))
68
+ return undefined;
69
+ const filename = safeFramePath(own(candidate, "filename"));
70
+ const line = positiveInteger(own(candidate, "lineno"));
71
+ const column = positiveInteger(own(candidate, "colno"));
72
+ if (!filename || !line || !column)
73
+ return undefined;
74
+ frames.push({ platform: "node:javascript", filename, lineno: line, colno: column });
75
+ }
76
+ return frames;
77
+ }
78
+ function reconstructExceptionList(input, reason) {
79
+ if (!Array.isArray(input) || input.length !== 1 || !isRecord(input[0]))
80
+ return undefined;
81
+ const exception = input[0];
82
+ const mechanism = own(exception, "mechanism");
83
+ if (own(exception, "type") !== "Error"
84
+ || own(exception, "value") !== reason
85
+ || !isRecord(mechanism)
86
+ || own(mechanism, "type") !== "generic"
87
+ || own(mechanism, "handled") !== true
88
+ || own(mechanism, "synthetic") !== false) {
89
+ return undefined;
90
+ }
91
+ const stacktrace = own(exception, "stacktrace");
92
+ if (stacktrace === undefined) {
93
+ return [{ type: "Error", value: reason, mechanism: { type: "generic", handled: true, synthetic: false } }];
94
+ }
95
+ if (!isRecord(stacktrace) || own(stacktrace, "type") !== "raw")
96
+ return undefined;
97
+ const frames = reconstructExceptionFrames(own(stacktrace, "frames"));
98
+ if (!frames)
99
+ return undefined;
100
+ return [{
101
+ type: "Error",
102
+ value: reason,
103
+ mechanism: { type: "generic", handled: true, synthetic: false },
104
+ stacktrace: { type: "raw", frames },
105
+ }];
106
+ }
107
+ /** Rebuild the SDK's own $exception payload from the closed allowlist. */
108
+ function reconstructExceptionEvent(event, installationId) {
109
+ if (event.event !== "$exception" || !isRecord(event.properties))
110
+ return null;
111
+ const properties = event.properties;
112
+ const category = own(properties, "category");
113
+ const reason = member(SETUP_REASONS, own(properties, "reason"));
114
+ const errorKind = member(ERROR_KINDS, own(properties, "errorKind"));
115
+ const fingerprint = own(properties, "$exception_fingerprint");
116
+ const diagnosticId = uuid(own(properties, "diagnosticId"));
117
+ if ((category !== "setup" && category !== "runtime")
118
+ || !reason
119
+ || !errorKind
120
+ || typeof fingerprint !== "string"
121
+ || !/^[0-9a-f]{64}$/.test(fingerprint)
122
+ || !diagnosticId
123
+ || diagnosticId === installationId
124
+ || own(properties, "$exception_level") !== "error") {
125
+ return null;
126
+ }
127
+ const exceptionList = reconstructExceptionList(own(properties, "$exception_list"), reason);
128
+ if (!exceptionList)
129
+ return null;
130
+ const systemErrorCode = optionalMember(SYSTEM_ERROR_CODES, own(properties, "systemErrorCode"));
131
+ const operation = optionalMember(OPERATIONS, own(properties, "operation"));
132
+ const provider = optionalMember(PROVIDERS, own(properties, "provider"));
133
+ const setupStage = optionalMember(SETUP_STAGES, own(properties, "setupStage"));
134
+ const runtimeMode = optionalMember(RUNTIME_MODES, own(properties, "runtimeMode"));
135
+ const statusCandidate = own(properties, "httpStatusCode");
136
+ const status = statusCandidate === undefined ? undefined : httpStatusCode(statusCandidate);
137
+ if (!systemErrorCode.valid || !operation.valid || !provider.valid || !setupStage.valid || !runtimeMode.valid
138
+ || (statusCandidate !== undefined && status === undefined)) {
139
+ return null;
140
+ }
141
+ const safeProperties = {
142
+ $exception_list: exceptionList,
143
+ $exception_level: "error",
144
+ $exception_fingerprint: fingerprint,
145
+ category,
146
+ reason,
147
+ errorKind,
148
+ };
149
+ if (systemErrorCode.value !== undefined)
150
+ safeProperties.systemErrorCode = systemErrorCode.value;
151
+ if (status !== undefined)
152
+ safeProperties.httpStatusCode = status;
153
+ if (operation.value !== undefined)
154
+ safeProperties.operation = operation.value;
155
+ if (provider.value !== undefined)
156
+ safeProperties.provider = provider.value;
157
+ if (setupStage.value !== undefined)
158
+ safeProperties.setupStage = setupStage.value;
159
+ if (runtimeMode.value !== undefined)
160
+ safeProperties.runtimeMode = runtimeMode.value;
161
+ safeProperties.diagnosticId = diagnosticId;
162
+ safeProperties.$process_person_profile = false;
163
+ safeProperties.$geoip_disable = true;
164
+ return { event: "$exception", distinctId: installationId, disableGeoip: true, properties: safeProperties };
165
+ }
166
+ function noOpResponse() {
167
+ return { status: 204, text: async () => "", json: async () => ({}), headers: { get: () => null } };
168
+ }
169
+ function boundedDeadline(value) {
170
+ return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
171
+ }
172
+ async function settleWithin(operation, deadlineMs) {
173
+ let timeout;
174
+ try {
175
+ await Promise.race([
176
+ Promise.resolve().then(operation).catch(() => undefined),
177
+ new Promise(resolve => { timeout = setTimeout(resolve, boundedDeadline(deadlineMs)); }),
178
+ ]);
179
+ }
180
+ catch {
181
+ // Telemetry lifecycle failures must never affect application behavior.
182
+ }
183
+ finally {
184
+ clearTimeout(timeout);
185
+ }
186
+ }
187
+ function exceptionProperties(exception, consentGeneration) {
188
+ const properties = {
189
+ $exception_fingerprint: exception.fingerprint,
190
+ category: exception.category,
191
+ reason: exception.reason,
192
+ errorKind: exception.errorKind,
193
+ diagnosticId: exception.diagnosticId,
194
+ $process_person_profile: false,
195
+ $geoip_disable: true,
196
+ [CONSENT_GENERATION_PROPERTY]: consentGeneration,
197
+ };
198
+ if (exception.systemErrorCode !== undefined)
199
+ properties.systemErrorCode = exception.systemErrorCode;
200
+ if (exception.httpStatusCode !== undefined)
201
+ properties.httpStatusCode = exception.httpStatusCode;
202
+ if (exception.operation !== undefined)
203
+ properties.operation = exception.operation;
204
+ if (exception.provider !== undefined)
205
+ properties.provider = exception.provider;
206
+ if (exception.setupStage !== undefined)
207
+ properties.setupStage = exception.setupStage;
208
+ if (exception.runtimeMode !== undefined)
209
+ properties.runtimeMode = exception.runtimeMode;
210
+ return properties;
211
+ }
212
+ /**
213
+ * posthog-node wrapper pinned to the EU host with no person profiles and no
214
+ * GeoIP. Consent is enforced three times: when a capture is prepared, in
215
+ * before_send (which also rebuilds every event from the closed allowlist), and
216
+ * in the transport, which answers 204 without a request once consent is gone.
217
+ */
218
+ export function createPostHogTelemetryClient(options) {
219
+ const getSnapshot = options.getSnapshot;
220
+ const transport = options.fetch ?? ((url, init) => globalThis.fetch(url, init));
221
+ const createSdkClient = options.createSdkClient
222
+ ?? ((token, sdkOptions) => new PostHog(token, sdkOptions));
223
+ let sdkClient;
224
+ let initializationFailed = false;
225
+ let shutdownStarted = false;
226
+ const activeTransports = new Set();
227
+ const abortActiveTransports = () => {
228
+ for (const controller of activeTransports)
229
+ controller.abort();
230
+ activeTransports.clear();
231
+ };
232
+ const discardPending = () => {
233
+ abortActiveTransports();
234
+ for (const key of QUEUE_KEYS) {
235
+ try {
236
+ sdkClient?.setPersistedProperty(key, []);
237
+ }
238
+ catch {
239
+ // Queue cleanup failures are still caught by the transport gate.
240
+ }
241
+ }
242
+ };
243
+ const beforeSend = (event) => {
244
+ try {
245
+ if (!event || !isRecord(event.properties))
246
+ return null;
247
+ const snapshot = getSnapshot();
248
+ const installationId = snapshot && uuid(snapshot.state.installId);
249
+ if (!snapshot || !installationId)
250
+ return null;
251
+ if (own(event.properties, CONSENT_GENERATION_PROPERTY) !== snapshot.state.consentGeneration) {
252
+ return null;
253
+ }
254
+ if (event.event === "$exception")
255
+ return reconstructExceptionEvent(event, installationId);
256
+ const diagnosticId = own(event.properties, "diagnosticId");
257
+ const safe = reconstructAnalyticsEvent({
258
+ event: event.event,
259
+ properties: event.properties,
260
+ }, {
261
+ installationId,
262
+ ...(typeof diagnosticId === "string" ? { diagnosticId } : {}),
263
+ });
264
+ if (!safe)
265
+ return null;
266
+ return {
267
+ event: safe.event,
268
+ distinctId: safe.installationId,
269
+ disableGeoip: true,
270
+ properties: { ...safe.properties, $process_person_profile: false, $geoip_disable: true },
271
+ };
272
+ }
273
+ catch {
274
+ return null;
275
+ }
276
+ };
277
+ const gatedFetch = async (url, fetchOptions) => {
278
+ try {
279
+ // Consent is re-read immediately before every transmission so a batch
280
+ // the SDK queued before an opt-out never leaves the process.
281
+ if (!getSnapshot()?.enabled)
282
+ return noOpResponse();
283
+ const controller = new AbortController();
284
+ const inherited = fetchOptions.signal;
285
+ const forwardAbort = () => { controller.abort(); };
286
+ if (inherited?.aborted)
287
+ controller.abort();
288
+ else
289
+ inherited?.addEventListener("abort", forwardAbort, { once: true });
290
+ activeTransports.add(controller);
291
+ try {
292
+ return await transport(url, { ...fetchOptions, signal: controller.signal });
293
+ }
294
+ finally {
295
+ inherited?.removeEventListener("abort", forwardAbort);
296
+ activeTransports.delete(controller);
297
+ }
298
+ }
299
+ catch {
300
+ throw new Error("PostHog transport failed");
301
+ }
302
+ };
303
+ const activeClient = (capturedConsentGeneration) => {
304
+ if (shutdownStarted || initializationFailed)
305
+ return undefined;
306
+ const snapshot = getSnapshot();
307
+ const installationId = snapshot && uuid(snapshot.state.installId);
308
+ if (!snapshot || !installationId)
309
+ return undefined;
310
+ if (snapshot.state.consentGeneration !== capturedConsentGeneration)
311
+ return undefined;
312
+ if (!sdkClient) {
313
+ try {
314
+ sdkClient = createSdkClient(POSTHOG_PROJECT_TOKEN, {
315
+ host: POSTHOG_HOST,
316
+ disableGeoip: true,
317
+ enableExceptionAutocapture: false,
318
+ disableRemoteConfig: true,
319
+ disableRemoteFeatureFlags: true,
320
+ flushAt: POSTHOG_FLUSH_AT,
321
+ maxQueueSize: POSTHOG_MAX_QUEUE_SIZE,
322
+ flushInterval: POSTHOG_FLUSH_INTERVAL_MS,
323
+ requestTimeout: POSTHOG_REQUEST_TIMEOUT_MS,
324
+ fetchRetryCount: 0,
325
+ disableCompression: true,
326
+ before_send: beforeSend,
327
+ fetch: gatedFetch,
328
+ });
329
+ }
330
+ catch {
331
+ initializationFailed = true;
332
+ return undefined;
333
+ }
334
+ }
335
+ return { client: sdkClient, installationId };
336
+ };
337
+ return {
338
+ captureAnalytics(event, consentGeneration) {
339
+ try {
340
+ const active = activeClient(consentGeneration);
341
+ active?.client.capture({
342
+ event: event.event,
343
+ distinctId: active.installationId,
344
+ disableGeoip: true,
345
+ properties: {
346
+ ...event.properties,
347
+ $process_person_profile: false,
348
+ $geoip_disable: true,
349
+ [CONSENT_GENERATION_PROPERTY]: consentGeneration,
350
+ },
351
+ });
352
+ }
353
+ catch {
354
+ // Capture is best effort only.
355
+ }
356
+ },
357
+ captureException(exception, consentGeneration) {
358
+ try {
359
+ const active = activeClient(consentGeneration);
360
+ active?.client.captureException(exception.error, active.installationId, exceptionProperties(exception, consentGeneration));
361
+ }
362
+ catch {
363
+ // Capture is best effort only.
364
+ }
365
+ },
366
+ async captureExceptionImmediate(exception, consentGeneration) {
367
+ try {
368
+ const active = activeClient(consentGeneration);
369
+ if (!active)
370
+ return;
371
+ await active.client.captureExceptionImmediate(exception.error, active.installationId, exceptionProperties(exception, consentGeneration));
372
+ }
373
+ catch {
374
+ // A crash-path capture must not change process behavior.
375
+ }
376
+ },
377
+ async flushWithin(deadlineMs) {
378
+ const client = sdkClient;
379
+ if (!client)
380
+ return;
381
+ if (!getSnapshot()) {
382
+ discardPending();
383
+ return;
384
+ }
385
+ await settleWithin(() => client.flush(), deadlineMs);
386
+ },
387
+ async shutdownWithin(deadlineMs) {
388
+ const client = sdkClient;
389
+ shutdownStarted = true;
390
+ if (!client)
391
+ return;
392
+ await settleWithin(() => client.shutdown(boundedDeadline(deadlineMs)), deadlineMs);
393
+ abortActiveTransports();
394
+ sdkClient = undefined;
395
+ },
396
+ discardPending,
397
+ };
398
+ }