@capxul/observability 4.2.0 → 4.20.0-beta.10
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/README.md +14 -0
- package/dist/engineering.d.mts +58 -5
- package/dist/engineering.mjs +837 -55
- package/dist/index.d.mts +1011 -14
- package/dist/index.mjs +2 -2292
- package/dist/src-Dhf8f_EV.mjs +3295 -0
- package/package.json +2 -2
- package/dist/src-CWfRYsBI.mjs +0 -302
package/dist/engineering.mjs
CHANGED
|
@@ -1,7 +1,382 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Cause, Effect, Exit, FiberSet, Layer, Stream, Tracer } from "effect";
|
|
3
|
-
import { FetchHttpClient, Headers, HttpClient } from "effect/unstable/http";
|
|
4
|
-
import { OtlpExporter, OtlpLogger, OtlpSerialization, OtlpTracer } from "effect/unstable/observability";
|
|
1
|
+
import { _ as CLI_COMMAND_NAMES, cn as CAPXUL_ERROR_CODES, dn as redactSecrets, fn as redactUrlSecrets, g as CLI_CAPABILITIES, ln as FAILURE_MODES, on as isCapxulOperation, un as isCredentialField, v as CLI_INVOCATION_KINDS } from "./src-Dhf8f_EV.mjs";
|
|
2
|
+
import { Cause, Clock, Context, Deferred, Duration, Effect, Exit, FiberSet, Layer, LogLevel, Logger, Metric, Option, References, Schedule, Stream, Tracer } from "effect";
|
|
3
|
+
import { FetchHttpClient, Headers, HttpBody, HttpClient, HttpClientError, HttpClientResponse } from "effect/unstable/http";
|
|
4
|
+
import { OtlpExporter, OtlpLogger, OtlpMetrics, OtlpSerialization, OtlpTracer } from "effect/unstable/observability";
|
|
5
|
+
//#region src/engineering-delivery.ts
|
|
6
|
+
/** Fixed admission limits for one CLI invocation and its SDK client. */
|
|
7
|
+
const CLI_ENGINEERING_LIMITS = Object.freeze({
|
|
8
|
+
records: 80,
|
|
9
|
+
ordinaryRecords: 64,
|
|
10
|
+
recordBytes: 16384,
|
|
11
|
+
requests: 5,
|
|
12
|
+
ordinaryRequests: 2,
|
|
13
|
+
requestBytes: 1572864,
|
|
14
|
+
metricSeries: 16,
|
|
15
|
+
requestTimeoutMs: 1e3
|
|
16
|
+
});
|
|
17
|
+
const metricNames = /* @__PURE__ */ new Set(["capxul.cli.command.count", "capxul.cli.command.duration"]);
|
|
18
|
+
const metricValues = {
|
|
19
|
+
command: CLI_COMMAND_NAMES,
|
|
20
|
+
capability: CLI_CAPABILITIES,
|
|
21
|
+
invocation_kind: CLI_INVOCATION_KINDS,
|
|
22
|
+
outcome: [
|
|
23
|
+
"succeeded",
|
|
24
|
+
"refused",
|
|
25
|
+
"failed",
|
|
26
|
+
"cancelled"
|
|
27
|
+
],
|
|
28
|
+
capxul_env: [
|
|
29
|
+
"development",
|
|
30
|
+
"staging",
|
|
31
|
+
"production",
|
|
32
|
+
"local"
|
|
33
|
+
],
|
|
34
|
+
unit: [
|
|
35
|
+
"1",
|
|
36
|
+
"s",
|
|
37
|
+
"ms"
|
|
38
|
+
]
|
|
39
|
+
};
|
|
40
|
+
/** Only this bounded registry is supplied to the CLI's metric producers. */
|
|
41
|
+
var CliMetricRegistry = class extends Map {
|
|
42
|
+
dropped;
|
|
43
|
+
enabled;
|
|
44
|
+
constructor(dropped, enabled = true) {
|
|
45
|
+
super();
|
|
46
|
+
this.dropped = dropped;
|
|
47
|
+
this.enabled = enabled;
|
|
48
|
+
}
|
|
49
|
+
set(key, entry) {
|
|
50
|
+
if (!this.enabled || !metricNames.has(entry.id) || !this.has(key) && this.size >= CLI_ENGINEERING_LIMITS.metricSeries) {
|
|
51
|
+
this.dropped();
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
const labels = Object.entries(entry.attributes ?? {});
|
|
55
|
+
const state = entry.hooks.get(Context.empty());
|
|
56
|
+
const buckets = typeof state === "object" && state !== null && "buckets" in state ? state.buckets : void 0;
|
|
57
|
+
if (!["Counter", "Histogram"].includes(entry.type) || (entry.description?.length ?? 0) > 128 || labels.length > 6 || labels.some(([name, value]) => !metricValues[name]?.includes(String(value))) || entry.type === "Histogram" && (!Array.isArray(buckets) || buckets.length > 32)) {
|
|
58
|
+
this.dropped();
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
return super.set(key, entry);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
/** Shared, closure-owned admission state; no queue or mutable counters are exposed. */
|
|
65
|
+
function makeCliEngineeringBudget() {
|
|
66
|
+
let records = 0, peakRecords = 0, activeRequests = 0, requestBytes = 0;
|
|
67
|
+
let peakRequests = 0, peakRequestBytes = 0, dropped = 0;
|
|
68
|
+
const drop = () => {
|
|
69
|
+
dropped += 1;
|
|
70
|
+
};
|
|
71
|
+
const metricRegistry = new CliMetricRegistry(drop);
|
|
72
|
+
return Object.freeze({
|
|
73
|
+
metricRegistry,
|
|
74
|
+
emptyMetricRegistry: () => new CliMetricRegistry(drop, false),
|
|
75
|
+
drop,
|
|
76
|
+
admitRecord(terminal) {
|
|
77
|
+
if (records >= (terminal ? CLI_ENGINEERING_LIMITS.records : CLI_ENGINEERING_LIMITS.ordinaryRecords)) {
|
|
78
|
+
drop();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
records += 1;
|
|
82
|
+
peakRecords = Math.max(peakRecords, records);
|
|
83
|
+
let released = false;
|
|
84
|
+
return () => {
|
|
85
|
+
if (!released) {
|
|
86
|
+
released = true;
|
|
87
|
+
records -= 1;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
admitRequest(bytes, terminal) {
|
|
92
|
+
if (bytes > CLI_ENGINEERING_LIMITS.requestBytes || activeRequests >= (terminal ? CLI_ENGINEERING_LIMITS.requests : CLI_ENGINEERING_LIMITS.ordinaryRequests)) {
|
|
93
|
+
drop();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
activeRequests += 1;
|
|
97
|
+
requestBytes += bytes;
|
|
98
|
+
peakRequests = Math.max(peakRequests, activeRequests);
|
|
99
|
+
peakRequestBytes = Math.max(peakRequestBytes, requestBytes);
|
|
100
|
+
return () => {
|
|
101
|
+
activeRequests -= 1;
|
|
102
|
+
requestBytes -= bytes;
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
snapshot() {
|
|
106
|
+
return {
|
|
107
|
+
records,
|
|
108
|
+
reservedRecordBytes: records * CLI_ENGINEERING_LIMITS.recordBytes,
|
|
109
|
+
peakRecords,
|
|
110
|
+
peakReservedRecordBytes: peakRecords * CLI_ENGINEERING_LIMITS.recordBytes,
|
|
111
|
+
activeRequests,
|
|
112
|
+
requestBytes,
|
|
113
|
+
peakRequests,
|
|
114
|
+
peakRequestBytes,
|
|
115
|
+
dropped,
|
|
116
|
+
metricSeries: metricRegistry.size
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const text = (value) => typeof value === "string" ? value.slice(0, 128) : "capxul.engineering";
|
|
122
|
+
const attributes = (input, limit = 32) => {
|
|
123
|
+
const out = {};
|
|
124
|
+
let count = 0;
|
|
125
|
+
let bytes = 0;
|
|
126
|
+
try {
|
|
127
|
+
for (const key in input) {
|
|
128
|
+
if (count++ >= limit) break;
|
|
129
|
+
if (key.length > 64) continue;
|
|
130
|
+
const value = Object.getOwnPropertyDescriptor(input, key)?.value;
|
|
131
|
+
const scalar = typeof value === "string" ? value.slice(0, 128) : typeof value === "boolean" || typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
132
|
+
if (scalar === void 0) continue;
|
|
133
|
+
bytes += new TextEncoder().encode(JSON.stringify([key, scalar])).length;
|
|
134
|
+
if (bytes > (limit >= 24 ? 2e3 : 300)) break;
|
|
135
|
+
out[key] = scalar;
|
|
136
|
+
}
|
|
137
|
+
} catch {}
|
|
138
|
+
return out;
|
|
139
|
+
};
|
|
140
|
+
const terminalName = (name) => name === "cli.command.completed" || name === "cli.wallet.step" || CLI_COMMAND_NAMES.some((command) => name === `cli.command.${command}`);
|
|
141
|
+
/** Native exporters retain their serialization and scheduling; this owns admission only. */
|
|
142
|
+
function cliEngineeringDelivery(budget, ownsMetrics) {
|
|
143
|
+
const closed = Deferred.makeUnsafe();
|
|
144
|
+
let isClosed = false;
|
|
145
|
+
let metricsRunning = false;
|
|
146
|
+
const logs = [];
|
|
147
|
+
const spans = /* @__PURE__ */ new Map();
|
|
148
|
+
const terminalBodies = /* @__PURE__ */ new WeakSet();
|
|
149
|
+
const bodyLeases = /* @__PURE__ */ new WeakMap();
|
|
150
|
+
const pendingBodies = /* @__PURE__ */ new Set();
|
|
151
|
+
const holdBody = (body, releases) => {
|
|
152
|
+
if (releases.length === 0) return;
|
|
153
|
+
const lease = {
|
|
154
|
+
started: false,
|
|
155
|
+
release: () => {
|
|
156
|
+
if (!pendingBodies.delete(lease)) return;
|
|
157
|
+
bodyLeases.delete(body);
|
|
158
|
+
for (const release of releases) release();
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
bodyLeases.set(body, lease);
|
|
162
|
+
pendingBodies.add(lease);
|
|
163
|
+
};
|
|
164
|
+
const discardBody = (body) => bodyLeases.get(body)?.release();
|
|
165
|
+
const drainLogs = () => {
|
|
166
|
+
for (const release of logs.splice(0)) release();
|
|
167
|
+
};
|
|
168
|
+
const close = () => {
|
|
169
|
+
isClosed = true;
|
|
170
|
+
Deferred.doneUnsafe(closed, Effect.void);
|
|
171
|
+
for (const lease of pendingBodies) if (!lease.started) lease.release();
|
|
172
|
+
drainLogs();
|
|
173
|
+
for (const release of spans.values()) release();
|
|
174
|
+
spans.clear();
|
|
175
|
+
if (ownsMetrics) budget.metricRegistry.clear();
|
|
176
|
+
};
|
|
177
|
+
return {
|
|
178
|
+
close,
|
|
179
|
+
discardBody,
|
|
180
|
+
metrics(run) {
|
|
181
|
+
return Effect.suspend(() => {
|
|
182
|
+
if (isClosed || metricsRunning) return Effect.void;
|
|
183
|
+
metricsRunning = true;
|
|
184
|
+
return run.pipe(Effect.ensuring(Effect.sync(() => {
|
|
185
|
+
metricsRunning = false;
|
|
186
|
+
})));
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
logger(delegate) {
|
|
190
|
+
return Logger.make((options) => {
|
|
191
|
+
if (isClosed) return;
|
|
192
|
+
let release;
|
|
193
|
+
try {
|
|
194
|
+
const message = text(Array.isArray(options.message) ? Object.getOwnPropertyDescriptor(options.message, "0")?.value : options.message);
|
|
195
|
+
release = budget.admitRecord(terminalName(message));
|
|
196
|
+
if (release === void 0) return;
|
|
197
|
+
const context = Context.add(options.fiber.context, References.CurrentLogAnnotations, attributes(options.fiber.getRef(References.CurrentLogAnnotations)));
|
|
198
|
+
const fiber = Object.create(options.fiber);
|
|
199
|
+
Object.defineProperty(fiber, "getRef", { value: (ref) => Context.get(context, ref) });
|
|
200
|
+
logs.push(release);
|
|
201
|
+
delegate.log({
|
|
202
|
+
...options,
|
|
203
|
+
message,
|
|
204
|
+
cause: Cause.empty,
|
|
205
|
+
fiber
|
|
206
|
+
});
|
|
207
|
+
} catch {
|
|
208
|
+
if (release !== void 0) {
|
|
209
|
+
const index = logs.indexOf(release);
|
|
210
|
+
if (index !== -1) logs.splice(index, 1);
|
|
211
|
+
release();
|
|
212
|
+
}
|
|
213
|
+
budget.drop();
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
},
|
|
217
|
+
tracer(delegate) {
|
|
218
|
+
return Tracer.make({
|
|
219
|
+
span(options) {
|
|
220
|
+
const name = text(options.name);
|
|
221
|
+
const release = isClosed ? void 0 : budget.admitRecord(terminalName(name));
|
|
222
|
+
const spanOptions = {
|
|
223
|
+
...options,
|
|
224
|
+
name,
|
|
225
|
+
annotations: Context.empty(),
|
|
226
|
+
links: []
|
|
227
|
+
};
|
|
228
|
+
const span = release === void 0 ? new Tracer.NativeSpan({
|
|
229
|
+
...spanOptions,
|
|
230
|
+
sampled: false
|
|
231
|
+
}) : delegate.span(spanOptions);
|
|
232
|
+
if (release !== void 0) spans.set(span.spanId, release);
|
|
233
|
+
const wrapped = Object.create(span);
|
|
234
|
+
let fields = 0, events = 0, links = 0, attributeBytes = 0;
|
|
235
|
+
let ended = false;
|
|
236
|
+
Object.defineProperties(wrapped, {
|
|
237
|
+
attribute: { value: (key, value) => {
|
|
238
|
+
if (fields >= 32 && !span.attributes.has(key)) {
|
|
239
|
+
budget.drop();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const safe = attributes({ [key]: value }, 1);
|
|
243
|
+
if (!Object.hasOwn(safe, key)) return;
|
|
244
|
+
const previous = span.attributes.has(key) ? new TextEncoder().encode(JSON.stringify([key, span.attributes.get(key)])).length : 0;
|
|
245
|
+
const next = new TextEncoder().encode(JSON.stringify([key, safe[key]])).length;
|
|
246
|
+
if (attributeBytes - previous + next > 2e3) {
|
|
247
|
+
budget.drop();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
attributeBytes += next - previous;
|
|
251
|
+
if (!span.attributes.has(key)) fields += 1;
|
|
252
|
+
span.attribute(key, safe[key]);
|
|
253
|
+
} },
|
|
254
|
+
event: { value: (eventName, time, attrs) => {
|
|
255
|
+
if (events++ >= 2) {
|
|
256
|
+
budget.drop();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
span.event(text(eventName), time, attributes(attrs ?? {}, 4));
|
|
260
|
+
} },
|
|
261
|
+
addLinks: { value: (items) => {
|
|
262
|
+
for (const item of items.slice(0, Math.max(0, 2 - links))) {
|
|
263
|
+
if (!/^[a-f0-9]{32}$/u.test(item.span.traceId) || !/^[a-f0-9]{16}$/u.test(item.span.spanId)) {
|
|
264
|
+
budget.drop();
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
span.addLinks([{
|
|
268
|
+
span: Tracer.externalSpan({
|
|
269
|
+
traceId: item.span.traceId,
|
|
270
|
+
spanId: item.span.spanId,
|
|
271
|
+
sampled: item.span.sampled
|
|
272
|
+
}),
|
|
273
|
+
attributes: attributes(item.attributes, 4)
|
|
274
|
+
}]);
|
|
275
|
+
links += 1;
|
|
276
|
+
}
|
|
277
|
+
} },
|
|
278
|
+
end: { value: (time, exit) => {
|
|
279
|
+
if (ended) return;
|
|
280
|
+
ended = true;
|
|
281
|
+
span.end(time, Exit.isFailure(exit) ? Exit.fail("CLI operation failed") : Exit.void);
|
|
282
|
+
} }
|
|
283
|
+
});
|
|
284
|
+
wrapped.addLinks(options.links ?? []);
|
|
285
|
+
return wrapped;
|
|
286
|
+
},
|
|
287
|
+
...delegate.context === void 0 ? {} : { context: delegate.context.bind(delegate) }
|
|
288
|
+
});
|
|
289
|
+
},
|
|
290
|
+
serialization(delegate) {
|
|
291
|
+
return {
|
|
292
|
+
logs(data) {
|
|
293
|
+
const releases = logs.splice(0);
|
|
294
|
+
try {
|
|
295
|
+
const body = delegate.logs(data);
|
|
296
|
+
if (data.resourceLogs.some((r) => r.scopeLogs.some((s) => s.logRecords?.some((l) => terminalName(l.body?.stringValue ?? ""))))) terminalBodies.add(body);
|
|
297
|
+
holdBody(body, releases);
|
|
298
|
+
return body;
|
|
299
|
+
} catch (error) {
|
|
300
|
+
for (const release of releases) release();
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
traces(data) {
|
|
305
|
+
const releases = [];
|
|
306
|
+
for (const resource of data.resourceSpans) for (const scope of resource.scopeSpans) for (const span of scope.spans) {
|
|
307
|
+
const release = spans.get(span.spanId);
|
|
308
|
+
if (release !== void 0) releases.push(release);
|
|
309
|
+
spans.delete(span.spanId);
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
const body = delegate.traces(data);
|
|
313
|
+
if (data.resourceSpans.some((r) => r.scopeSpans.some((s) => s.spans.some((span) => terminalName(span.name))))) terminalBodies.add(body);
|
|
314
|
+
holdBody(body, releases);
|
|
315
|
+
return body;
|
|
316
|
+
} catch (error) {
|
|
317
|
+
for (const release of releases) release();
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
metrics(data) {
|
|
322
|
+
const body = delegate.metrics(data);
|
|
323
|
+
terminalBodies.add(body);
|
|
324
|
+
return body;
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
},
|
|
328
|
+
transport(client, enabled = () => true) {
|
|
329
|
+
const retrying = HttpClient.transform(client, (response, request) => Effect.suspend(() => {
|
|
330
|
+
if (enabled()) return response;
|
|
331
|
+
budget.drop();
|
|
332
|
+
return Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null)));
|
|
333
|
+
})).pipe(HttpClient.transformResponse((response) => Effect.tap(response, (result) => {
|
|
334
|
+
let received = 0;
|
|
335
|
+
return Stream.runForEach(result.stream, (chunk) => Effect.sync(() => {
|
|
336
|
+
received += chunk.byteLength;
|
|
337
|
+
if (received > 65536) throw new Error("Engineering response limit exceeded");
|
|
338
|
+
})).pipe(Effect.catchTag("HttpClientError", (error) => error.reason._tag === "EmptyBodyError" ? Effect.void : Effect.fail(error)));
|
|
339
|
+
}))).pipe(HttpClient.filterStatusOk, HttpClient.retryTransient({
|
|
340
|
+
times: 1,
|
|
341
|
+
schedule: Schedule.spaced("100 millis").pipe(Schedule.addDelay(({ input }) => Effect.map(Clock.currentTimeMillis, (now) => {
|
|
342
|
+
if (!HttpClientError.isHttpClientError(input) || input.reason._tag !== "StatusCodeError" || input.reason.response.status !== 429) return Duration.zero;
|
|
343
|
+
const header = input.reason.response.headers["retry-after"];
|
|
344
|
+
const numeric = Number(header);
|
|
345
|
+
const delay = header === void 0 ? 2e3 : Number.isFinite(numeric) ? numeric * 1e3 : Date.parse(header) - now;
|
|
346
|
+
return Duration.millis(Number.isFinite(delay) ? Math.min(2e3, Math.max(0, delay)) : 2e3);
|
|
347
|
+
})))
|
|
348
|
+
}));
|
|
349
|
+
return HttpClient.transform(retrying, (response, request) => Effect.suspend(() => {
|
|
350
|
+
if (isClosed) {
|
|
351
|
+
discardBody(request.body);
|
|
352
|
+
return Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null)));
|
|
353
|
+
}
|
|
354
|
+
const droppedResponse = () => HttpClientResponse.fromWeb(request, new Response(null, { status: 200 }));
|
|
355
|
+
if (request.body._tag === "Empty") {
|
|
356
|
+
discardBody(request.body);
|
|
357
|
+
return Effect.succeed(droppedResponse());
|
|
358
|
+
}
|
|
359
|
+
const bytes = request.body.contentLength;
|
|
360
|
+
const release = budget.admitRequest(bytes ?? Number.POSITIVE_INFINITY, terminalBodies.has(request.body));
|
|
361
|
+
if (release === void 0) {
|
|
362
|
+
discardBody(request.body);
|
|
363
|
+
return Effect.succeed(droppedResponse());
|
|
364
|
+
}
|
|
365
|
+
const lease = bodyLeases.get(request.body);
|
|
366
|
+
if (lease !== void 0) lease.started = true;
|
|
367
|
+
return response.pipe(Effect.asVoid, Effect.timeoutOption(CLI_ENGINEERING_LIMITS.requestTimeoutMs), Effect.raceFirst(Deferred.await(closed).pipe(Effect.andThen(Effect.interrupt))), Effect.tap((result) => Effect.sync(() => {
|
|
368
|
+
if (Option.isNone(result)) budget.drop();
|
|
369
|
+
})), Effect.catchCause(() => Effect.sync(() => {
|
|
370
|
+
budget.drop();
|
|
371
|
+
})), Effect.as(droppedResponse()), Effect.ensuring(Effect.sync(() => {
|
|
372
|
+
release();
|
|
373
|
+
discardBody(request.body);
|
|
374
|
+
})));
|
|
375
|
+
}));
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
//#endregion
|
|
5
380
|
//#region src/engineering.ts
|
|
6
381
|
const ENGINEERING_CAPXUL_ENVS = [
|
|
7
382
|
"development",
|
|
@@ -15,13 +390,18 @@ const postHogOtlpEndpoints = (host) => {
|
|
|
15
390
|
const base = host.replace(/\/+$/, "");
|
|
16
391
|
return {
|
|
17
392
|
logs: `${base}/i/v1/logs`,
|
|
18
|
-
traces: `${base}/i/v1/traces
|
|
393
|
+
traces: `${base}/i/v1/traces`,
|
|
394
|
+
metrics: `${base}/i/v1/metrics`
|
|
19
395
|
};
|
|
20
396
|
};
|
|
21
397
|
const ENGINEERING_CAPXUL_ENV_SET = new Set(ENGINEERING_CAPXUL_ENVS);
|
|
22
398
|
const ENGINEERING_PRODUCERS = /* @__PURE__ */ new Set(["browser", "server"]);
|
|
23
399
|
const PUBLIC_POSTHOG_AUTHORIZATION = /^Bearer phc_[A-Za-z0-9_-]{1,191}$/u;
|
|
24
400
|
const SAFE_RESOURCE_VALUE = /^[A-Za-z0-9][A-Za-z0-9._+@/-]{0,127}$/u;
|
|
401
|
+
function validateEngineeringPrivacy(config) {
|
|
402
|
+
if (config.privacy !== void 0 && config.privacy !== "minimal") throw new TypeError("engineering telemetry privacy must be minimal when specified");
|
|
403
|
+
if (config.privacy === "minimal" && (!/^[A-Za-z0-9][A-Za-z0-9._+@-]{0,127}$/u.test(config.sdkVersion) || config.serviceName !== void 0 && !/^[A-Za-z0-9][A-Za-z0-9._+@-]{0,127}$/u.test(config.serviceName))) throw new TypeError("minimal engineering resource values must not contain paths");
|
|
404
|
+
}
|
|
25
405
|
const validateEngineeringTelemetryConfig = (config) => {
|
|
26
406
|
let url;
|
|
27
407
|
try {
|
|
@@ -32,6 +412,8 @@ const validateEngineeringTelemetryConfig = (config) => {
|
|
|
32
412
|
if (url.protocol !== "https:" || url.username.length > 0 || url.password.length > 0 || url.pathname !== "/" || url.search.length > 0 || url.hash.length > 0 || !(url.hostname === "posthog.com" || url.hostname.endsWith(".posthog.com"))) throw new TypeError("engineering telemetry host must be a credential-free HTTPS origin");
|
|
33
413
|
if (!ENGINEERING_CAPXUL_ENV_SET.has(config.capxulEnv)) throw new TypeError("engineering telemetry capxulEnv is not canonical");
|
|
34
414
|
if (!ENGINEERING_PRODUCERS.has(config.producer)) throw new TypeError("engineering telemetry producer is not canonical");
|
|
415
|
+
if (config.enabled !== void 0 && typeof config.enabled !== "boolean" && typeof config.enabled !== "function") throw new TypeError("engineering telemetry enabled must be a boolean or callback");
|
|
416
|
+
validateEngineeringPrivacy(config);
|
|
35
417
|
if (!SAFE_RESOURCE_VALUE.test(config.sdkVersion)) throw new TypeError("engineering telemetry sdkVersion must be a bounded safe value");
|
|
36
418
|
if (config.serviceName !== void 0 && !SAFE_RESOURCE_VALUE.test(config.serviceName)) throw new TypeError("engineering telemetry serviceName must be a bounded safe value");
|
|
37
419
|
const headerEntries = Object.entries(config.headers);
|
|
@@ -82,25 +464,34 @@ const spanUpstream = (span) => {
|
|
|
82
464
|
let current = span;
|
|
83
465
|
while (current?._tag === "Span") {
|
|
84
466
|
const upstream = current.attributes.get("chain.upstream");
|
|
85
|
-
if (upstream === "alchemy" || upstream === "infura") return upstream;
|
|
467
|
+
if (upstream === "alchemy" || upstream === "infura" || upstream === "gateway") return upstream;
|
|
86
468
|
current = current.parent._tag === "Some" ? current.parent.value : void 0;
|
|
87
469
|
}
|
|
88
470
|
};
|
|
471
|
+
function redactHttpPath(path, fullUrl, source) {
|
|
472
|
+
let origin = "https://redaction.invalid";
|
|
473
|
+
try {
|
|
474
|
+
const url = new URL(String(fullUrl));
|
|
475
|
+
if (url.protocol === "https:" || url.protocol === "http:") origin = url.origin;
|
|
476
|
+
} catch {}
|
|
477
|
+
const prefix = path.startsWith("/") ? origin : "";
|
|
478
|
+
return redactUrlSecrets(`${prefix}${path}`, source).slice(prefix.length, prefix.length + 2048);
|
|
479
|
+
}
|
|
89
480
|
/** Preserve domain exits while preventing the OTLP serializer from seeing raw causes. */
|
|
90
|
-
const makeLeakSafeEngineeringTracer = (delegate) => Tracer.make({
|
|
481
|
+
const makeLeakSafeEngineeringTracer = (delegate, enabled = () => true) => Tracer.make({
|
|
91
482
|
span(options) {
|
|
92
|
-
const span = delegate.span(options);
|
|
483
|
+
const span = enabled() ? delegate.span(options) : new Tracer.NativeSpan(options);
|
|
93
484
|
const wrapped = Object.create(span);
|
|
94
485
|
Object.defineProperty(wrapped, "attribute", { value: (name, value) => {
|
|
95
486
|
const source = spanUpstream(span);
|
|
96
487
|
const header = /^http\.(?:request|response)\.header\.(.+)$/u.exec(name)?.[1];
|
|
97
488
|
if (header !== void 0 && isCredentialField(header, "header")) span.attribute(name, "[REDACTED]");
|
|
98
489
|
else if (typeof value === "string" && name === "url.query") span.attribute(name, redactUrlSecrets(`?${value}`).slice(1, 2049));
|
|
99
|
-
else if (typeof value === "string" && name === "url.path") {
|
|
100
|
-
const fullUrl = span.attributes.get("url.full");
|
|
101
|
-
const path = typeof fullUrl === "string" ? /^https?:\/\/[^/]+([^?#]*)/u.exec(fullUrl)?.[1] ?? value : value;
|
|
102
|
-
span.attribute(name,
|
|
103
|
-
} else if (typeof value === "string" && (name === "url.full" || header === "location" || header === "referer" || header === "referrer")) span.attribute(name, redactUrlSecrets(value, source).slice(0, 2048));
|
|
490
|
+
else if (typeof value === "string" && (name === "url.path" || name === "http.target")) {
|
|
491
|
+
const fullUrl = span.attributes.get("url.full") ?? span.attributes.get("http.url");
|
|
492
|
+
const path = name === "url.path" && typeof fullUrl === "string" ? /^https?:\/\/[^/]+([^?#]*)/u.exec(fullUrl)?.[1] ?? value : value;
|
|
493
|
+
span.attribute(name, redactHttpPath(path, fullUrl, source));
|
|
494
|
+
} else if (typeof value === "string" && (name === "url.full" || name === "http.url" || header === "location" || header === "referer" || header === "referrer")) span.attribute(name, redactUrlSecrets(value, source).slice(0, 2048));
|
|
104
495
|
else if (header !== void 0 && typeof value === "string") span.attribute(name, redactSecrets(value, source).slice(0, 2048));
|
|
105
496
|
else span.attribute(name, value);
|
|
106
497
|
} });
|
|
@@ -108,6 +499,15 @@ const makeLeakSafeEngineeringTracer = (delegate) => Tracer.make({
|
|
|
108
499
|
configurable: false,
|
|
109
500
|
enumerable: false,
|
|
110
501
|
value: (endTime, exit) => {
|
|
502
|
+
if (!enabled()) {
|
|
503
|
+
Object.defineProperty(wrapped, "status", { value: {
|
|
504
|
+
_tag: "Ended",
|
|
505
|
+
startTime: span.status.startTime,
|
|
506
|
+
endTime,
|
|
507
|
+
exit
|
|
508
|
+
} });
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
111
511
|
if (Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause)) {
|
|
112
512
|
const first = exit.cause.reasons.find((reason) => !Cause.isInterruptReason(reason));
|
|
113
513
|
const code = failureText(first === void 0 ? void 0 : Cause.isFailReason(first) ? first.error : first.defect, "code", 128);
|
|
@@ -121,53 +521,435 @@ const makeLeakSafeEngineeringTracer = (delegate) => Tracer.make({
|
|
|
121
521
|
},
|
|
122
522
|
...delegate.context === void 0 ? {} : { context: delegate.context.bind(delegate) }
|
|
123
523
|
});
|
|
524
|
+
const MINIMAL_LOG_NAMES = /* @__PURE__ */ new Set([
|
|
525
|
+
"capxul_sdk_operation",
|
|
526
|
+
"identity.transition",
|
|
527
|
+
"convex.connection.changed",
|
|
528
|
+
"convex.action.connection_lost",
|
|
529
|
+
"cli.command.completed",
|
|
530
|
+
"cli.sdk.operation.failed",
|
|
531
|
+
"cli.wallet.step"
|
|
532
|
+
]);
|
|
533
|
+
const MINIMAL_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
534
|
+
...CAPXUL_ERROR_CODES,
|
|
535
|
+
"CLI_USAGE",
|
|
536
|
+
"CLI_CONFIG",
|
|
537
|
+
"CLI_STORAGE",
|
|
538
|
+
"CLI_DEFECT"
|
|
539
|
+
]);
|
|
540
|
+
const MINIMAL_NUMBERS = /* @__PURE__ */ new Set([
|
|
541
|
+
"schema_version",
|
|
542
|
+
"duration_ms",
|
|
543
|
+
"wait_duration_ms",
|
|
544
|
+
"retry_count",
|
|
545
|
+
"http.status_code",
|
|
546
|
+
"http.response.status_code",
|
|
547
|
+
"http_status",
|
|
548
|
+
"rpc.code",
|
|
549
|
+
"connection_count",
|
|
550
|
+
"connection_retries",
|
|
551
|
+
"elapsed_ms",
|
|
552
|
+
"total_ms",
|
|
553
|
+
"token_calls",
|
|
554
|
+
"call",
|
|
555
|
+
"attempt",
|
|
556
|
+
"bridge_protocol_version",
|
|
557
|
+
"httpStatus"
|
|
558
|
+
]);
|
|
559
|
+
const MINIMAL_CORRELATIONS = /* @__PURE__ */ new Set([
|
|
560
|
+
"correlation_id",
|
|
561
|
+
"journey_id",
|
|
562
|
+
"invocation_id",
|
|
563
|
+
"posthogDistinctId"
|
|
564
|
+
]);
|
|
565
|
+
const MINIMAL_METRIC_LABELS = /* @__PURE__ */ new Set([
|
|
566
|
+
"command",
|
|
567
|
+
"capability",
|
|
568
|
+
"invocation_kind",
|
|
569
|
+
"outcome",
|
|
570
|
+
"capxul_env",
|
|
571
|
+
"unit"
|
|
572
|
+
]);
|
|
573
|
+
const MINIMAL_METRIC_NAMES = /* @__PURE__ */ new Set(["capxul.cli.command.count", "capxul.cli.command.duration"]);
|
|
574
|
+
const traceId = (value) => typeof value === "string" && /^(?!0+$)[0-9a-f]{32}$/u.test(value);
|
|
575
|
+
const spanId = (value) => typeof value === "string" && /^(?!0+$)[0-9a-f]{16}$/u.test(value);
|
|
576
|
+
const minimalName = (value) => typeof value === "string" && (MINIMAL_LOG_NAMES.has(value) || isCapxulOperation(value) || CLI_COMMAND_NAMES.some((command) => value === `cli.command.${command}`)) ? value : "capxul.engineering";
|
|
577
|
+
const MINIMAL_ENUM_VALUES = /* @__PURE__ */ new Map([
|
|
578
|
+
["outcome", [
|
|
579
|
+
"succeeded",
|
|
580
|
+
"refused",
|
|
581
|
+
"failed",
|
|
582
|
+
"cancelled",
|
|
583
|
+
"applied"
|
|
584
|
+
]],
|
|
585
|
+
["mode", FAILURE_MODES],
|
|
586
|
+
["failure_mode", FAILURE_MODES],
|
|
587
|
+
["capxul_env", ENGINEERING_CAPXUL_ENVS],
|
|
588
|
+
["producer", ["browser", "server"]],
|
|
589
|
+
["command", CLI_COMMAND_NAMES],
|
|
590
|
+
["capability", CLI_CAPABILITIES],
|
|
591
|
+
["invocation_kind", CLI_INVOCATION_KINDS],
|
|
592
|
+
["diagnostic_scope", [
|
|
593
|
+
"openfort.step",
|
|
594
|
+
"openfort.token",
|
|
595
|
+
"openfort.encryptionSession",
|
|
596
|
+
"openfort.embeddedState",
|
|
597
|
+
"openfort.storageCleared",
|
|
598
|
+
"openfort.configure",
|
|
599
|
+
"openfort.get",
|
|
600
|
+
"openfort.address",
|
|
601
|
+
"openfort.staleCacheHeal",
|
|
602
|
+
"openfort.signerRecovery",
|
|
603
|
+
"openfort.signerStatusListener"
|
|
604
|
+
]],
|
|
605
|
+
["step", [
|
|
606
|
+
"initialize",
|
|
607
|
+
"token",
|
|
608
|
+
"encryptionSession",
|
|
609
|
+
"embeddedState",
|
|
610
|
+
"configure",
|
|
611
|
+
"get",
|
|
612
|
+
"healStaleCache",
|
|
613
|
+
"walletReady"
|
|
614
|
+
]],
|
|
615
|
+
["provider", ["better-auth", "oidc"]],
|
|
616
|
+
["cli_identity_kind", ["anonymous", "identified"]],
|
|
617
|
+
["output_mode", ["human", "json"]],
|
|
618
|
+
["unit", [
|
|
619
|
+
"1",
|
|
620
|
+
"s",
|
|
621
|
+
"ms"
|
|
622
|
+
]]
|
|
623
|
+
]);
|
|
624
|
+
const MINIMAL_CODE_KEYS = /* @__PURE__ */ new Set([
|
|
625
|
+
"error.code",
|
|
626
|
+
"error_code",
|
|
627
|
+
"failure_code",
|
|
628
|
+
"refusal_code",
|
|
629
|
+
"capxul_error_code"
|
|
630
|
+
]);
|
|
631
|
+
const MINIMAL_VERSION_KEYS = /* @__PURE__ */ new Set([
|
|
632
|
+
"sdk_version",
|
|
633
|
+
"cli_version",
|
|
634
|
+
"release"
|
|
635
|
+
]);
|
|
636
|
+
function minimalStringValue(key, text) {
|
|
637
|
+
if (key === "operation") return isCapxulOperation(text);
|
|
638
|
+
if (MINIMAL_CODE_KEYS.has(key)) return MINIMAL_ERROR_CODES.has(text);
|
|
639
|
+
if (MINIMAL_VERSION_KEYS.has(key)) return /^[A-Za-z0-9][A-Za-z0-9._+@-]{0,127}$/u.test(text);
|
|
640
|
+
if (key === "trace_id") return traceId(text);
|
|
641
|
+
if (key === "span_id" || key === "parent_span_id") return spanId(text);
|
|
642
|
+
const values = MINIMAL_ENUM_VALUES.get(key);
|
|
643
|
+
if (values !== void 0) return values.includes(text);
|
|
644
|
+
return MINIMAL_CORRELATIONS.has(key) && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(text) && !/^0x[0-9a-f]+$/iu.test(text) && redactSecrets(text) === text;
|
|
645
|
+
}
|
|
646
|
+
/** A named privacy projection, separate from the default credential-only redaction policy. */
|
|
647
|
+
function minimalAttributes(attributes, metric = false) {
|
|
648
|
+
return attributes.flatMap(({ key, value }) => {
|
|
649
|
+
if (metric && !MINIMAL_METRIC_LABELS.has(key)) return [];
|
|
650
|
+
const text = value.stringValue;
|
|
651
|
+
const number = value.doubleValue ?? value.intValue;
|
|
652
|
+
if (MINIMAL_NUMBERS.has(key) && typeof number === "number" && Number.isFinite(number)) return [{
|
|
653
|
+
key,
|
|
654
|
+
value: { doubleValue: number }
|
|
655
|
+
}];
|
|
656
|
+
if (typeof text !== "string") return [];
|
|
657
|
+
return minimalStringValue(key, text) ? [{
|
|
658
|
+
key,
|
|
659
|
+
value: { stringValue: text }
|
|
660
|
+
}] : [];
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
const finiteProtocolNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
664
|
+
const protocolCount = (value, maximum = Number.MAX_SAFE_INTEGER) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= maximum ? value : void 0;
|
|
665
|
+
const protocolFlags = (value) => protocolCount(Object.getOwnPropertyDescriptor(value, "flags")?.value, 4294967295);
|
|
666
|
+
function protocolTime(value) {
|
|
667
|
+
if (typeof value === "string" && /^\d{1,20}$/u.test(value) && BigInt(value) <= 18446744073709551615n) return value;
|
|
668
|
+
const number = protocolCount(value);
|
|
669
|
+
if (number !== void 0) return String(number);
|
|
670
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
671
|
+
const low = protocolCount(Object.getOwnPropertyDescriptor(value, "low")?.value, 4294967295);
|
|
672
|
+
const high = protocolCount(Object.getOwnPropertyDescriptor(value, "high")?.value, 4294967295);
|
|
673
|
+
return low === void 0 || high === void 0 ? void 0 : (BigInt(high) << 32n | BigInt(low)).toString();
|
|
674
|
+
}
|
|
675
|
+
function minimalNumberPoints(points) {
|
|
676
|
+
return points.flatMap((point) => {
|
|
677
|
+
const attributes = minimalAttributes(point.attributes, true);
|
|
678
|
+
const asDouble = finiteProtocolNumber(point.asDouble);
|
|
679
|
+
const asInt = typeof point.asInt === "number" && Number.isSafeInteger(point.asInt) ? point.asInt : void 0;
|
|
680
|
+
if (attributes.length !== point.attributes.length || asDouble === void 0 && asInt === void 0) return [];
|
|
681
|
+
return [{
|
|
682
|
+
attributes,
|
|
683
|
+
timeUnixNano: protocolTime(point.timeUnixNano),
|
|
684
|
+
startTimeUnixNano: protocolTime(point.startTimeUnixNano),
|
|
685
|
+
asDouble,
|
|
686
|
+
asInt: asDouble === void 0 ? asInt : void 0,
|
|
687
|
+
flags: protocolFlags(point)
|
|
688
|
+
}];
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
function minimalHistogramPoints(points) {
|
|
692
|
+
return points.flatMap((point) => {
|
|
693
|
+
const attributes = minimalAttributes(point.attributes ?? [], true);
|
|
694
|
+
const count = protocolCount(point.count);
|
|
695
|
+
const bucketCounts = point.bucketCounts;
|
|
696
|
+
const explicitBounds = point.explicitBounds;
|
|
697
|
+
if (attributes.length !== (point.attributes?.length ?? 0) || count === void 0 || !Array.isArray(bucketCounts) || !bucketCounts.every((value) => protocolCount(value) !== void 0) || !Array.isArray(explicitBounds) || !explicitBounds.every((value) => finiteProtocolNumber(value) !== void 0) || bucketCounts.length !== explicitBounds.length + 1) return [];
|
|
698
|
+
return [{
|
|
699
|
+
attributes,
|
|
700
|
+
timeUnixNano: protocolTime(point.timeUnixNano),
|
|
701
|
+
startTimeUnixNano: protocolTime(point.startTimeUnixNano),
|
|
702
|
+
count,
|
|
703
|
+
sum: finiteProtocolNumber(point.sum),
|
|
704
|
+
min: finiteProtocolNumber(point.min),
|
|
705
|
+
max: finiteProtocolNumber(point.max),
|
|
706
|
+
bucketCounts: bucketCounts.map((value) => value),
|
|
707
|
+
explicitBounds: explicitBounds.map((value) => value),
|
|
708
|
+
flags: protocolFlags(point)
|
|
709
|
+
}];
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
function minimalSerialization(config) {
|
|
713
|
+
const service = config.serviceName ?? "capxul-sdk";
|
|
714
|
+
const resource = { attributes: Object.entries({
|
|
715
|
+
"service.name": service,
|
|
716
|
+
"service.version": config.sdkVersion,
|
|
717
|
+
sdk_version: config.sdkVersion,
|
|
718
|
+
capxul_env: config.capxulEnv,
|
|
719
|
+
producer: config.producer
|
|
720
|
+
}).map(([key, value]) => ({
|
|
721
|
+
key,
|
|
722
|
+
value: { stringValue: value }
|
|
723
|
+
})) };
|
|
724
|
+
return {
|
|
725
|
+
logs: (data) => HttpBody.jsonUnsafe({ resourceLogs: data.resourceLogs.map((entry) => ({
|
|
726
|
+
resource,
|
|
727
|
+
scopeLogs: entry.scopeLogs.map((scope) => ({
|
|
728
|
+
scope: { name: service },
|
|
729
|
+
logRecords: (scope.logRecords ?? []).map((record) => ({
|
|
730
|
+
timeUnixNano: protocolTime(record.timeUnixNano),
|
|
731
|
+
observedTimeUnixNano: protocolTime(record.observedTimeUnixNano),
|
|
732
|
+
severityNumber: protocolCount(record.severityNumber, 24),
|
|
733
|
+
severityText: [
|
|
734
|
+
"All",
|
|
735
|
+
"Trace",
|
|
736
|
+
"Debug",
|
|
737
|
+
"Info",
|
|
738
|
+
"Warn",
|
|
739
|
+
"Error",
|
|
740
|
+
"Fatal",
|
|
741
|
+
"None"
|
|
742
|
+
].includes(record.severityText ?? "") ? record.severityText : void 0,
|
|
743
|
+
body: { stringValue: minimalName(record.body?.stringValue) },
|
|
744
|
+
attributes: minimalAttributes(record.attributes),
|
|
745
|
+
droppedAttributesCount: protocolCount(record.droppedAttributesCount) ?? 0,
|
|
746
|
+
traceId: traceId(record.traceId) ? record.traceId : void 0,
|
|
747
|
+
spanId: spanId(record.spanId) ? record.spanId : void 0,
|
|
748
|
+
flags: protocolFlags(record)
|
|
749
|
+
}))
|
|
750
|
+
}))
|
|
751
|
+
})) }),
|
|
752
|
+
traces: (data) => HttpBody.jsonUnsafe({ resourceSpans: data.resourceSpans.map((entry) => ({
|
|
753
|
+
resource,
|
|
754
|
+
scopeSpans: entry.scopeSpans.map((scope) => ({
|
|
755
|
+
scope: { name: service },
|
|
756
|
+
spans: scope.spans.filter((span) => traceId(span.traceId) && spanId(span.spanId)).map((span) => {
|
|
757
|
+
const attributes = minimalAttributes(span.attributes);
|
|
758
|
+
const code = attributes.find((attribute) => attribute.key === "error.code");
|
|
759
|
+
return {
|
|
760
|
+
traceId: span.traceId,
|
|
761
|
+
spanId: span.spanId,
|
|
762
|
+
name: minimalName(span.name),
|
|
763
|
+
kind: protocolCount(span.kind, 5) ?? 0,
|
|
764
|
+
startTimeUnixNano: protocolTime(span.startTimeUnixNano),
|
|
765
|
+
endTimeUnixNano: protocolTime(span.endTimeUnixNano),
|
|
766
|
+
attributes,
|
|
767
|
+
droppedAttributesCount: protocolCount(span.droppedAttributesCount) ?? 0,
|
|
768
|
+
parentSpanId: spanId(span.parentSpanId) ? span.parentSpanId : void 0,
|
|
769
|
+
flags: protocolFlags(span),
|
|
770
|
+
status: { code: protocolCount(span.status.code, 2) ?? 0 },
|
|
771
|
+
events: span.events.slice(0, 8).map((event) => ({
|
|
772
|
+
timeUnixNano: protocolTime(event.timeUnixNano),
|
|
773
|
+
droppedAttributesCount: protocolCount(event.droppedAttributesCount) ?? 0,
|
|
774
|
+
name: event.name === "exception" ? "exception" : minimalName(event.name),
|
|
775
|
+
attributes: event.name === "exception" ? [{
|
|
776
|
+
key: "exception.type",
|
|
777
|
+
value: code?.value ?? { stringValue: "Error" }
|
|
778
|
+
}] : minimalAttributes(event.attributes)
|
|
779
|
+
})),
|
|
780
|
+
droppedEventsCount: protocolCount(span.droppedEventsCount) ?? 0,
|
|
781
|
+
links: span.links.filter((link) => traceId(link.traceId) && spanId(link.spanId)).slice(0, 8).map((link) => ({
|
|
782
|
+
traceId: link.traceId,
|
|
783
|
+
spanId: link.spanId,
|
|
784
|
+
flags: protocolFlags(link),
|
|
785
|
+
attributes: minimalAttributes(link.attributes),
|
|
786
|
+
droppedAttributesCount: protocolCount(link.droppedAttributesCount) ?? 0
|
|
787
|
+
})),
|
|
788
|
+
droppedLinksCount: protocolCount(span.droppedLinksCount) ?? 0
|
|
789
|
+
};
|
|
790
|
+
})
|
|
791
|
+
}))
|
|
792
|
+
})) }),
|
|
793
|
+
metrics: (data) => {
|
|
794
|
+
const resourceMetrics = data.resourceMetrics.map((entry) => ({
|
|
795
|
+
resource,
|
|
796
|
+
scopeMetrics: entry.scopeMetrics.map((scope) => ({
|
|
797
|
+
scope: { name: service },
|
|
798
|
+
metrics: scope.metrics.filter((metric) => MINIMAL_METRIC_NAMES.has(metric.name)).flatMap((metric) => {
|
|
799
|
+
const gauge = metric.gauge === void 0 ? void 0 : { dataPoints: minimalNumberPoints(metric.gauge.dataPoints) };
|
|
800
|
+
const sum = metric.sum === void 0 ? void 0 : {
|
|
801
|
+
aggregationTemporality: protocolCount(metric.sum.aggregationTemporality, 2) ?? 0,
|
|
802
|
+
isMonotonic: metric.sum.isMonotonic === true,
|
|
803
|
+
dataPoints: minimalNumberPoints(metric.sum.dataPoints)
|
|
804
|
+
};
|
|
805
|
+
const histogram = metric.histogram === void 0 ? void 0 : {
|
|
806
|
+
aggregationTemporality: protocolCount(metric.histogram.aggregationTemporality, 2) ?? 0,
|
|
807
|
+
dataPoints: minimalHistogramPoints(metric.histogram.dataPoints)
|
|
808
|
+
};
|
|
809
|
+
if (![
|
|
810
|
+
gauge,
|
|
811
|
+
sum,
|
|
812
|
+
histogram
|
|
813
|
+
].some((series) => (series?.dataPoints.length ?? 0) > 0)) return [];
|
|
814
|
+
return [{
|
|
815
|
+
name: metric.name,
|
|
816
|
+
unit: [
|
|
817
|
+
"1",
|
|
818
|
+
"s",
|
|
819
|
+
"ms"
|
|
820
|
+
].includes(metric.unit ?? "") ? metric.unit : "1",
|
|
821
|
+
...gauge === void 0 ? {} : { gauge },
|
|
822
|
+
...sum === void 0 ? {} : { sum },
|
|
823
|
+
...histogram === void 0 ? {} : { histogram }
|
|
824
|
+
}];
|
|
825
|
+
})
|
|
826
|
+
}))
|
|
827
|
+
}));
|
|
828
|
+
return resourceMetrics.some((entry) => entry.scopeMetrics.some((scope) => scope.metrics.length > 0)) ? HttpBody.jsonUnsafe({ resourceMetrics }) : HttpBody.empty;
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
/** Attempt to drain all engineering signals; completion does not prove ingestion. */
|
|
833
|
+
const flushEngineeringTelemetry = Effect.flatMap(OtlpExporter.Flusher, (flusher) => flusher.flush);
|
|
834
|
+
var EngineeringLog = class extends Context.Service()("capxul/EngineeringLog") {};
|
|
835
|
+
/** Deliver an owned engineering record without changing the caller's console verbosity. */
|
|
836
|
+
const logEngineeringInfo = (message, attributes) => {
|
|
837
|
+
const record = Effect.logInfo(message).pipe(Effect.annotateLogs(attributes));
|
|
838
|
+
return Effect.flatMap(LogLevel.isEnabled("Info"), (visible) => visible ? record : Effect.flatMap(Effect.serviceOption(EngineeringLog), (logger) => logger._tag === "None" ? Effect.void : record.pipe(Effect.provideService(Logger.CurrentLoggers, /* @__PURE__ */ new Set([logger.value])), Effect.provideService(References.MinimumLogLevel, "All"))));
|
|
839
|
+
};
|
|
124
840
|
/** Shared browser/server OTLP layer. OtlpLogger merges with incumbent loggers once. */
|
|
125
841
|
const makeEngineeringTelemetryLayer = (config) => {
|
|
126
842
|
const validated = validateEngineeringTelemetryConfig(config);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const headerPolicy = Layer.merge(Layer.succeed(HttpClient.TracerHeaderFilter, traceHeaderFilter), Layer.succeed(Headers.CurrentRedactedNames, [credentialHeaderPattern]));
|
|
149
|
-
const browserLifecycle = Layer.effectDiscard(Effect.gen(function* () {
|
|
150
|
-
if (validated.producer !== "browser" || !hasBrowserDocument()) return;
|
|
151
|
-
const browserWindow = window;
|
|
152
|
-
const browserDocument = document;
|
|
153
|
-
const flusher = yield* OtlpExporter.Flusher;
|
|
154
|
-
const run = yield* FiberSet.makeRuntime();
|
|
155
|
-
const flush = () => {
|
|
156
|
-
run(flusher.flush);
|
|
843
|
+
return Layer.suspend(() => {
|
|
844
|
+
const bounded = validated.deliveryBudget === void 0 ? void 0 : cliEngineeringDelivery(validated.deliveryBudget, validated.serviceName === "capxul-cli");
|
|
845
|
+
const endpoints = postHogOtlpEndpoints(validated.host);
|
|
846
|
+
let disabled = false;
|
|
847
|
+
const enabled = () => {
|
|
848
|
+
if (disabled) return false;
|
|
849
|
+
try {
|
|
850
|
+
disabled = (typeof validated.enabled === "function" ? validated.enabled() : validated.enabled ?? true) !== true;
|
|
851
|
+
} catch {
|
|
852
|
+
disabled = true;
|
|
853
|
+
}
|
|
854
|
+
return !disabled;
|
|
855
|
+
};
|
|
856
|
+
const delivery = {
|
|
857
|
+
...validated.shutdownTimeout === void 0 ? {} : { shutdownTimeout: validated.shutdownTimeout },
|
|
858
|
+
...validated.exportInterval === void 0 ? {} : { exportInterval: validated.exportInterval },
|
|
859
|
+
...bounded === void 0 ? {} : {
|
|
860
|
+
maxBatchSize: Number.MAX_SAFE_INTEGER,
|
|
861
|
+
exportInterval: 1e3,
|
|
862
|
+
shutdownTimeout: validated.serviceName === "capxul-cli" ? 0 : 500
|
|
863
|
+
}
|
|
157
864
|
};
|
|
158
|
-
const
|
|
159
|
-
|
|
865
|
+
const resource = {
|
|
866
|
+
serviceName: validated.serviceName ?? "capxul-sdk",
|
|
867
|
+
serviceVersion: validated.sdkVersion,
|
|
868
|
+
attributes: {
|
|
869
|
+
capxul_env: validated.capxulEnv,
|
|
870
|
+
producer: validated.producer,
|
|
871
|
+
sdk_version: validated.sdkVersion
|
|
872
|
+
}
|
|
160
873
|
};
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
874
|
+
const tracing = Layer.effect(Tracer.Tracer, OtlpTracer.make({
|
|
875
|
+
url: endpoints.traces,
|
|
876
|
+
headers: validated.headers,
|
|
877
|
+
resource,
|
|
878
|
+
...delivery
|
|
879
|
+
}).pipe(Effect.map((tracer) => makeLeakSafeEngineeringTracer(bounded?.tracer(tracer) ?? tracer, enabled)))).pipe(Layer.provideMerge(OtlpExporter.layerFlusher));
|
|
880
|
+
const ownedLogging = Layer.effect(EngineeringLog, OtlpLogger.make({
|
|
881
|
+
url: endpoints.logs,
|
|
882
|
+
headers: validated.headers,
|
|
883
|
+
resource,
|
|
884
|
+
...delivery,
|
|
885
|
+
...bounded === void 0 ? {} : { excludeLogSpans: true }
|
|
886
|
+
}).pipe(Effect.map((nativeLogger) => {
|
|
887
|
+
const logger = bounded?.logger(nativeLogger) ?? nativeLogger;
|
|
888
|
+
return Logger.make((options) => {
|
|
889
|
+
if (enabled()) logger.log(options);
|
|
890
|
+
});
|
|
891
|
+
}))).pipe(Layer.provideMerge(OtlpExporter.layerFlusher));
|
|
892
|
+
const logging = Logger.layer([EngineeringLog], { mergeWithExisting: true }).pipe(Layer.provideMerge(ownedLogging));
|
|
893
|
+
const metricConfig = {
|
|
894
|
+
url: endpoints.metrics,
|
|
895
|
+
headers: validated.headers,
|
|
896
|
+
resource,
|
|
897
|
+
temporality: "delta",
|
|
898
|
+
...delivery
|
|
899
|
+
};
|
|
900
|
+
const metrics = bounded === void 0 ? OtlpMetrics.layer(metricConfig) : validated.serviceName !== "capxul-cli" ? Layer.empty : Layer.effectDiscard(Effect.gen(function* () {
|
|
901
|
+
const flusher = yield* OtlpExporter.Flusher;
|
|
902
|
+
yield* OtlpMetrics.make({
|
|
903
|
+
...metricConfig,
|
|
904
|
+
exportInterval: Number.POSITIVE_INFINITY,
|
|
905
|
+
shutdownTimeout: 0
|
|
906
|
+
}).pipe(Effect.provideService(OtlpExporter.Flusher, {
|
|
907
|
+
...flusher,
|
|
908
|
+
register: (run) => flusher.register(bounded.metrics(run))
|
|
909
|
+
}));
|
|
910
|
+
})).pipe(Layer.provideMerge(OtlpExporter.layerFlusher));
|
|
911
|
+
const headerPolicy = Layer.merge(Layer.succeed(HttpClient.TracerHeaderFilter, traceHeaderFilter), Layer.succeed(Headers.CurrentRedactedNames, [credentialHeaderPattern]));
|
|
912
|
+
const browserLifecycle = Layer.effectDiscard(Effect.gen(function* () {
|
|
913
|
+
if (validated.producer !== "browser" || !hasBrowserDocument()) return;
|
|
914
|
+
const browserWindow = window;
|
|
915
|
+
const browserDocument = document;
|
|
916
|
+
const flusher = yield* OtlpExporter.Flusher;
|
|
917
|
+
const run = yield* FiberSet.makeRuntime();
|
|
918
|
+
const flush = () => {
|
|
919
|
+
run(flusher.flush);
|
|
920
|
+
};
|
|
921
|
+
const visibilityChanged = () => {
|
|
922
|
+
if (browserDocument.visibilityState === "hidden") flush();
|
|
923
|
+
};
|
|
924
|
+
yield* Effect.acquireRelease(Effect.sync(() => {
|
|
925
|
+
browserDocument.addEventListener("visibilitychange", visibilityChanged);
|
|
926
|
+
browserWindow.addEventListener("pagehide", flush);
|
|
927
|
+
}), () => Effect.sync(() => {
|
|
928
|
+
browserDocument.removeEventListener("visibilitychange", visibilityChanged);
|
|
929
|
+
browserWindow.removeEventListener("pagehide", flush);
|
|
930
|
+
}));
|
|
167
931
|
}));
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
932
|
+
const transport = Layer.effect(HttpClient.HttpClient, Effect.map(HttpClient.HttpClient, (client) => {
|
|
933
|
+
const originalTransport = validated.producer === "browser" && hasBrowserDocument() ? browserExportClient(client) : client;
|
|
934
|
+
const selectedTransport = bounded?.transport(originalTransport, enabled) ?? originalTransport;
|
|
935
|
+
return HttpClient.transform(selectedTransport, (response, request) => Effect.suspend(() => {
|
|
936
|
+
if (enabled() && request.body._tag !== "Empty") return response;
|
|
937
|
+
bounded?.discardBody(request.body);
|
|
938
|
+
return Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null, { status: 200 })));
|
|
939
|
+
}));
|
|
940
|
+
})).pipe(Layer.provide(FetchHttpClient.layer));
|
|
941
|
+
const serialization = Layer.effect(OtlpSerialization.OtlpSerialization, Effect.map(OtlpSerialization.OtlpSerialization, (json) => {
|
|
942
|
+
const projected = minimalSerialization(validated);
|
|
943
|
+
const selected = validated.privacy === "minimal" ? projected : {
|
|
944
|
+
...json,
|
|
945
|
+
metrics: projected.metrics
|
|
946
|
+
};
|
|
947
|
+
return bounded?.serialization(selected) ?? selected;
|
|
948
|
+
})).pipe(Layer.provide(OtlpSerialization.layerJson));
|
|
949
|
+
const combined = browserLifecycle.pipe(Layer.provideMerge(Layer.mergeAll(tracing, logging, metrics, headerPolicy)), Layer.provide(serialization), Layer.provide(transport));
|
|
950
|
+
const scoped = bounded === void 0 ? combined : combined.pipe(Layer.provideMerge(Layer.effectDiscard(Effect.addFinalizer(() => Effect.sync(bounded.close)))));
|
|
951
|
+
return validated.deliveryBudget === void 0 ? scoped : scoped.pipe(Layer.provideMerge(Layer.succeed(Metric.MetricRegistry, validated.serviceName === "capxul-cli" ? validated.deliveryBudget.metricRegistry : validated.deliveryBudget.emptyMetricRegistry())));
|
|
952
|
+
});
|
|
171
953
|
};
|
|
172
954
|
//#endregion
|
|
173
|
-
export { ENGINEERING_CAPXUL_ENVS, makeEngineeringTelemetryLayer, makeLeakSafeEngineeringTracer, postHogOtlpEndpoints, traceHeaderFilter };
|
|
955
|
+
export { CLI_ENGINEERING_LIMITS, ENGINEERING_CAPXUL_ENVS, flushEngineeringTelemetry, logEngineeringInfo, makeCliEngineeringBudget, makeEngineeringTelemetryLayer, makeLeakSafeEngineeringTracer, postHogOtlpEndpoints, traceHeaderFilter };
|