@capxul/observability 4.2.0 → 4.20.0-beta.2

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 CHANGED
@@ -94,3 +94,17 @@ vp run --filter @capxul/observability build
94
94
 
95
95
  Read [`CONTEXT.md`](./CONTEXT.md) before changing event names, schemas,
96
96
  redaction, journeys, or adapter behavior.
97
+
98
+ ## Engineering composition
99
+
100
+ Import `makeEngineeringTelemetryLayer` and `flushEngineeringTelemetry` from
101
+ `@capxul/observability/engineering`. The layer shares OTLP logs, traces, and delta
102
+ metrics. The flush Effect attempts delivery without claiming ingestion. Optional
103
+ `enabled` stops collection for the acquired layer when false or when its callback
104
+ throws. `shutdownTimeout` applies per exporter; a host can set it to zero after
105
+ it runs an explicit flush under its own total deadline.
106
+
107
+ Set `privacy: "minimal"` for hosts that must export only closed operation/error
108
+ codes, timings, environment, and validated correlation. This opt-in removes
109
+ free-form logs, provider messages, URLs, bodies, and source paths, including
110
+ span exception events. Default consumers retain the existing redaction policy.
@@ -1,4 +1,4 @@
1
- import { Layer, Tracer } from "effect";
1
+ import { Duration, Effect, Layer, Tracer } from "effect";
2
2
  import { OtlpExporter } from "effect/unstable/observability";
3
3
  //#region src/engineering.d.ts
4
4
  type EngineeringProducer = "browser" | "server";
@@ -11,15 +11,24 @@ interface EngineeringTelemetryConfig {
11
11
  readonly producer: EngineeringProducer;
12
12
  readonly sdkVersion: string;
13
13
  readonly serviceName?: string;
14
+ /** False or a thrown callback disables this acquired layer for its remaining lifetime. */
15
+ readonly enabled?: boolean | (() => boolean);
16
+ /** Codes, timings, and correlation only; no free text or private source paths. */
17
+ readonly privacy?: "minimal";
18
+ readonly shutdownTimeout?: Duration.Input;
19
+ readonly exportInterval?: Duration.Input;
14
20
  }
15
21
  declare const traceHeaderFilter: (_name: string) => boolean;
16
22
  declare const postHogOtlpEndpoints: (host: string) => {
17
23
  readonly logs: `${string}/i/v1/logs`;
18
24
  readonly traces: `${string}/i/v1/traces`;
25
+ readonly metrics: `${string}/i/v1/metrics`;
19
26
  };
20
27
  /** Preserve domain exits while preventing the OTLP serializer from seeing raw causes. */
21
- declare const makeLeakSafeEngineeringTracer: (delegate: Tracer.Tracer) => Tracer.Tracer;
28
+ declare const makeLeakSafeEngineeringTracer: (delegate: Tracer.Tracer, enabled?: () => boolean) => Tracer.Tracer;
29
+ /** Attempt to drain all engineering signals; completion does not prove ingestion. */
30
+ declare const flushEngineeringTelemetry: Effect.Effect<void, never, OtlpExporter.Flusher>;
22
31
  /** Shared browser/server OTLP layer. OtlpLogger merges with incumbent loggers once. */
23
32
  declare const makeEngineeringTelemetryLayer: (config: EngineeringTelemetryConfig) => Layer.Layer<OtlpExporter.Flusher, never, never>;
24
33
  //#endregion
25
- export { ENGINEERING_CAPXUL_ENVS, EngineeringCapxulEnv, EngineeringProducer, EngineeringTelemetryConfig, makeEngineeringTelemetryLayer, makeLeakSafeEngineeringTracer, postHogOtlpEndpoints, traceHeaderFilter };
34
+ export { ENGINEERING_CAPXUL_ENVS, EngineeringCapxulEnv, EngineeringProducer, EngineeringTelemetryConfig, flushEngineeringTelemetry, makeEngineeringTelemetryLayer, makeLeakSafeEngineeringTracer, postHogOtlpEndpoints, traceHeaderFilter };
@@ -1,7 +1,7 @@
1
- import { i as redactUrlSecrets, n as isCredentialField, r as redactSecrets } from "./src-CWfRYsBI.mjs";
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 { c as redactSecrets, i as CAPXUL_ERROR_CODES, l as redactUrlSecrets, n as isCapxulOperation, o as FAILURE_MODES, s as isCredentialField } from "./operations-sJPrehYY.mjs";
2
+ import { Cause, Effect, Exit, FiberSet, Layer, Logger, Stream, Tracer } from "effect";
3
+ import { FetchHttpClient, Headers, HttpBody, HttpClient, HttpClientResponse } from "effect/unstable/http";
4
+ import { OtlpExporter, OtlpLogger, OtlpMetrics, OtlpSerialization, OtlpTracer } from "effect/unstable/observability";
5
5
  //#region src/engineering.ts
6
6
  const ENGINEERING_CAPXUL_ENVS = [
7
7
  "development",
@@ -15,13 +15,18 @@ const postHogOtlpEndpoints = (host) => {
15
15
  const base = host.replace(/\/+$/, "");
16
16
  return {
17
17
  logs: `${base}/i/v1/logs`,
18
- traces: `${base}/i/v1/traces`
18
+ traces: `${base}/i/v1/traces`,
19
+ metrics: `${base}/i/v1/metrics`
19
20
  };
20
21
  };
21
22
  const ENGINEERING_CAPXUL_ENV_SET = new Set(ENGINEERING_CAPXUL_ENVS);
22
23
  const ENGINEERING_PRODUCERS = /* @__PURE__ */ new Set(["browser", "server"]);
23
24
  const PUBLIC_POSTHOG_AUTHORIZATION = /^Bearer phc_[A-Za-z0-9_-]{1,191}$/u;
24
25
  const SAFE_RESOURCE_VALUE = /^[A-Za-z0-9][A-Za-z0-9._+@/-]{0,127}$/u;
26
+ function validateEngineeringPrivacy(config) {
27
+ if (config.privacy !== void 0 && config.privacy !== "minimal") throw new TypeError("engineering telemetry privacy must be minimal when specified");
28
+ 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");
29
+ }
25
30
  const validateEngineeringTelemetryConfig = (config) => {
26
31
  let url;
27
32
  try {
@@ -32,6 +37,8 @@ const validateEngineeringTelemetryConfig = (config) => {
32
37
  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
38
  if (!ENGINEERING_CAPXUL_ENV_SET.has(config.capxulEnv)) throw new TypeError("engineering telemetry capxulEnv is not canonical");
34
39
  if (!ENGINEERING_PRODUCERS.has(config.producer)) throw new TypeError("engineering telemetry producer is not canonical");
40
+ 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");
41
+ validateEngineeringPrivacy(config);
35
42
  if (!SAFE_RESOURCE_VALUE.test(config.sdkVersion)) throw new TypeError("engineering telemetry sdkVersion must be a bounded safe value");
36
43
  if (config.serviceName !== void 0 && !SAFE_RESOURCE_VALUE.test(config.serviceName)) throw new TypeError("engineering telemetry serviceName must be a bounded safe value");
37
44
  const headerEntries = Object.entries(config.headers);
@@ -82,25 +89,34 @@ const spanUpstream = (span) => {
82
89
  let current = span;
83
90
  while (current?._tag === "Span") {
84
91
  const upstream = current.attributes.get("chain.upstream");
85
- if (upstream === "alchemy" || upstream === "infura") return upstream;
92
+ if (upstream === "alchemy" || upstream === "infura" || upstream === "gateway") return upstream;
86
93
  current = current.parent._tag === "Some" ? current.parent.value : void 0;
87
94
  }
88
95
  };
96
+ function redactHttpPath(path, fullUrl, source) {
97
+ let origin = "https://redaction.invalid";
98
+ try {
99
+ const url = new URL(String(fullUrl));
100
+ if (url.protocol === "https:" || url.protocol === "http:") origin = url.origin;
101
+ } catch {}
102
+ const prefix = path.startsWith("/") ? origin : "";
103
+ return redactUrlSecrets(`${prefix}${path}`, source).slice(prefix.length, prefix.length + 2048);
104
+ }
89
105
  /** Preserve domain exits while preventing the OTLP serializer from seeing raw causes. */
90
- const makeLeakSafeEngineeringTracer = (delegate) => Tracer.make({
106
+ const makeLeakSafeEngineeringTracer = (delegate, enabled = () => true) => Tracer.make({
91
107
  span(options) {
92
- const span = delegate.span(options);
108
+ const span = enabled() ? delegate.span(options) : new Tracer.NativeSpan(options);
93
109
  const wrapped = Object.create(span);
94
110
  Object.defineProperty(wrapped, "attribute", { value: (name, value) => {
95
111
  const source = spanUpstream(span);
96
112
  const header = /^http\.(?:request|response)\.header\.(.+)$/u.exec(name)?.[1];
97
113
  if (header !== void 0 && isCredentialField(header, "header")) span.attribute(name, "[REDACTED]");
98
114
  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, redactUrlSecrets(path, source).slice(0, 2048));
103
- } else if (typeof value === "string" && (name === "url.full" || header === "location" || header === "referer" || header === "referrer")) span.attribute(name, redactUrlSecrets(value, source).slice(0, 2048));
115
+ else if (typeof value === "string" && (name === "url.path" || name === "http.target")) {
116
+ const fullUrl = span.attributes.get("url.full") ?? span.attributes.get("http.url");
117
+ const path = name === "url.path" && typeof fullUrl === "string" ? /^https?:\/\/[^/]+([^?#]*)/u.exec(fullUrl)?.[1] ?? value : value;
118
+ span.attribute(name, redactHttpPath(path, fullUrl, source));
119
+ } 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
120
  else if (header !== void 0 && typeof value === "string") span.attribute(name, redactSecrets(value, source).slice(0, 2048));
105
121
  else span.attribute(name, value);
106
122
  } });
@@ -108,6 +124,15 @@ const makeLeakSafeEngineeringTracer = (delegate) => Tracer.make({
108
124
  configurable: false,
109
125
  enumerable: false,
110
126
  value: (endTime, exit) => {
127
+ if (!enabled()) {
128
+ Object.defineProperty(wrapped, "status", { value: {
129
+ _tag: "Ended",
130
+ startTime: span.status.startTime,
131
+ endTime,
132
+ exit
133
+ } });
134
+ return;
135
+ }
111
136
  if (Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause)) {
112
137
  const first = exit.cause.reasons.find((reason) => !Cause.isInterruptReason(reason));
113
138
  const code = failureText(first === void 0 ? void 0 : Cause.isFailReason(first) ? first.error : first.defect, "code", 128);
@@ -121,53 +146,361 @@ const makeLeakSafeEngineeringTracer = (delegate) => Tracer.make({
121
146
  },
122
147
  ...delegate.context === void 0 ? {} : { context: delegate.context.bind(delegate) }
123
148
  });
149
+ const MINIMAL_LOG_NAMES = /* @__PURE__ */ new Set([
150
+ "capxul_sdk_operation",
151
+ "identity.transition",
152
+ "convex.connection.changed",
153
+ "convex.action.connection_lost",
154
+ "cli.command.completed",
155
+ "cli.sdk.operation.failed"
156
+ ]);
157
+ const MINIMAL_ERROR_CODES = /* @__PURE__ */ new Set([
158
+ ...CAPXUL_ERROR_CODES,
159
+ "CLI_USAGE",
160
+ "CLI_CONFIG",
161
+ "CLI_STORAGE",
162
+ "CLI_DEFECT"
163
+ ]);
164
+ const MINIMAL_NUMBERS = /* @__PURE__ */ new Set([
165
+ "schema_version",
166
+ "duration_ms",
167
+ "wait_duration_ms",
168
+ "retry_count",
169
+ "http.status_code",
170
+ "http.response.status_code",
171
+ "http_status",
172
+ "rpc.code",
173
+ "connection_count",
174
+ "connection_retries"
175
+ ]);
176
+ const MINIMAL_CORRELATIONS = /* @__PURE__ */ new Set([
177
+ "correlation_id",
178
+ "journey_id",
179
+ "invocation_id"
180
+ ]);
181
+ const MINIMAL_METRIC_LABELS = /* @__PURE__ */ new Set([
182
+ "command",
183
+ "outcome",
184
+ "capxul_env",
185
+ "unit"
186
+ ]);
187
+ const MINIMAL_METRIC_NAMES = /* @__PURE__ */ new Set(["capxul.cli.command.count", "capxul.cli.command.duration"]);
188
+ const traceId = (value) => typeof value === "string" && /^(?!0+$)[0-9a-f]{32}$/u.test(value);
189
+ const spanId = (value) => typeof value === "string" && /^(?!0+$)[0-9a-f]{16}$/u.test(value);
190
+ const minimalName = (value) => typeof value === "string" && (MINIMAL_LOG_NAMES.has(value) || isCapxulOperation(value)) ? value : "capxul.engineering";
191
+ const MINIMAL_ENUM_VALUES = /* @__PURE__ */ new Map([
192
+ ["outcome", [
193
+ "succeeded",
194
+ "refused",
195
+ "failed",
196
+ "cancelled",
197
+ "applied"
198
+ ]],
199
+ ["mode", FAILURE_MODES],
200
+ ["failure_mode", FAILURE_MODES],
201
+ ["capxul_env", ENGINEERING_CAPXUL_ENVS],
202
+ ["producer", ["browser", "server"]],
203
+ ["command", ["doctor"]],
204
+ ["output_mode", ["human", "json"]],
205
+ ["unit", [
206
+ "1",
207
+ "s",
208
+ "ms"
209
+ ]]
210
+ ]);
211
+ const MINIMAL_CODE_KEYS = /* @__PURE__ */ new Set([
212
+ "error.code",
213
+ "error_code",
214
+ "failure_code",
215
+ "refusal_code",
216
+ "capxul_error_code"
217
+ ]);
218
+ const MINIMAL_VERSION_KEYS = /* @__PURE__ */ new Set([
219
+ "sdk_version",
220
+ "cli_version",
221
+ "release"
222
+ ]);
223
+ function minimalStringValue(key, text) {
224
+ if (key === "operation") return isCapxulOperation(text);
225
+ if (MINIMAL_CODE_KEYS.has(key)) return MINIMAL_ERROR_CODES.has(text);
226
+ if (MINIMAL_VERSION_KEYS.has(key)) return /^[A-Za-z0-9][A-Za-z0-9._+@-]{0,127}$/u.test(text);
227
+ if (key === "trace_id") return traceId(text);
228
+ if (key === "span_id" || key === "parent_span_id") return spanId(text);
229
+ const values = MINIMAL_ENUM_VALUES.get(key);
230
+ if (values !== void 0) return values.includes(text);
231
+ 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;
232
+ }
233
+ /** A named privacy projection, separate from the default credential-only redaction policy. */
234
+ function minimalAttributes(attributes, metric = false) {
235
+ return attributes.flatMap(({ key, value }) => {
236
+ if (metric && !MINIMAL_METRIC_LABELS.has(key)) return [];
237
+ const text = value.stringValue;
238
+ const number = value.doubleValue ?? value.intValue;
239
+ if (MINIMAL_NUMBERS.has(key) && typeof number === "number" && Number.isFinite(number)) return [{
240
+ key,
241
+ value: { doubleValue: number }
242
+ }];
243
+ if (typeof text !== "string") return [];
244
+ return minimalStringValue(key, text) ? [{
245
+ key,
246
+ value: { stringValue: text }
247
+ }] : [];
248
+ });
249
+ }
250
+ const finiteProtocolNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
251
+ const protocolCount = (value, maximum = Number.MAX_SAFE_INTEGER) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= maximum ? value : void 0;
252
+ const protocolFlags = (value) => protocolCount(Object.getOwnPropertyDescriptor(value, "flags")?.value, 4294967295);
253
+ function protocolTime(value) {
254
+ if (typeof value === "string" && /^\d{1,20}$/u.test(value) && BigInt(value) <= 18446744073709551615n) return value;
255
+ const number = protocolCount(value);
256
+ if (number !== void 0) return String(number);
257
+ if (value === null || typeof value !== "object") return void 0;
258
+ const low = protocolCount(Object.getOwnPropertyDescriptor(value, "low")?.value, 4294967295);
259
+ const high = protocolCount(Object.getOwnPropertyDescriptor(value, "high")?.value, 4294967295);
260
+ return low === void 0 || high === void 0 ? void 0 : (BigInt(high) << 32n | BigInt(low)).toString();
261
+ }
262
+ function minimalNumberPoints(points) {
263
+ return points.flatMap((point) => {
264
+ const attributes = minimalAttributes(point.attributes, true);
265
+ const asDouble = finiteProtocolNumber(point.asDouble);
266
+ const asInt = typeof point.asInt === "number" && Number.isSafeInteger(point.asInt) ? point.asInt : void 0;
267
+ if (attributes.length !== point.attributes.length || asDouble === void 0 && asInt === void 0) return [];
268
+ return [{
269
+ attributes,
270
+ timeUnixNano: protocolTime(point.timeUnixNano),
271
+ startTimeUnixNano: protocolTime(point.startTimeUnixNano),
272
+ asDouble,
273
+ asInt: asDouble === void 0 ? asInt : void 0,
274
+ flags: protocolFlags(point)
275
+ }];
276
+ });
277
+ }
278
+ function minimalHistogramPoints(points) {
279
+ return points.flatMap((point) => {
280
+ const attributes = minimalAttributes(point.attributes ?? [], true);
281
+ const count = protocolCount(point.count);
282
+ const bucketCounts = point.bucketCounts;
283
+ const explicitBounds = point.explicitBounds;
284
+ 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 [];
285
+ return [{
286
+ attributes,
287
+ timeUnixNano: protocolTime(point.timeUnixNano),
288
+ startTimeUnixNano: protocolTime(point.startTimeUnixNano),
289
+ count,
290
+ sum: finiteProtocolNumber(point.sum),
291
+ min: finiteProtocolNumber(point.min),
292
+ max: finiteProtocolNumber(point.max),
293
+ bucketCounts: bucketCounts.map((value) => value),
294
+ explicitBounds: explicitBounds.map((value) => value),
295
+ flags: protocolFlags(point)
296
+ }];
297
+ });
298
+ }
299
+ function minimalSerialization(config) {
300
+ const service = config.serviceName ?? "capxul-sdk";
301
+ const resource = { attributes: Object.entries({
302
+ "service.name": service,
303
+ "service.version": config.sdkVersion,
304
+ sdk_version: config.sdkVersion,
305
+ capxul_env: config.capxulEnv,
306
+ producer: config.producer
307
+ }).map(([key, value]) => ({
308
+ key,
309
+ value: { stringValue: value }
310
+ })) };
311
+ return {
312
+ logs: (data) => HttpBody.jsonUnsafe({ resourceLogs: data.resourceLogs.map((entry) => ({
313
+ resource,
314
+ scopeLogs: entry.scopeLogs.map((scope) => ({
315
+ scope: { name: service },
316
+ logRecords: (scope.logRecords ?? []).map((record) => ({
317
+ timeUnixNano: protocolTime(record.timeUnixNano),
318
+ observedTimeUnixNano: protocolTime(record.observedTimeUnixNano),
319
+ severityNumber: protocolCount(record.severityNumber, 24),
320
+ severityText: [
321
+ "All",
322
+ "Trace",
323
+ "Debug",
324
+ "Info",
325
+ "Warn",
326
+ "Error",
327
+ "Fatal",
328
+ "None"
329
+ ].includes(record.severityText ?? "") ? record.severityText : void 0,
330
+ body: { stringValue: minimalName(record.body?.stringValue) },
331
+ attributes: minimalAttributes(record.attributes),
332
+ droppedAttributesCount: protocolCount(record.droppedAttributesCount) ?? 0,
333
+ traceId: traceId(record.traceId) ? record.traceId : void 0,
334
+ spanId: spanId(record.spanId) ? record.spanId : void 0,
335
+ flags: protocolFlags(record)
336
+ }))
337
+ }))
338
+ })) }),
339
+ traces: (data) => HttpBody.jsonUnsafe({ resourceSpans: data.resourceSpans.map((entry) => ({
340
+ resource,
341
+ scopeSpans: entry.scopeSpans.map((scope) => ({
342
+ scope: { name: service },
343
+ spans: scope.spans.filter((span) => traceId(span.traceId) && spanId(span.spanId)).map((span) => {
344
+ const attributes = minimalAttributes(span.attributes);
345
+ const code = attributes.find((attribute) => attribute.key === "error.code");
346
+ return {
347
+ traceId: span.traceId,
348
+ spanId: span.spanId,
349
+ name: minimalName(span.name),
350
+ kind: protocolCount(span.kind, 5) ?? 0,
351
+ startTimeUnixNano: protocolTime(span.startTimeUnixNano),
352
+ endTimeUnixNano: protocolTime(span.endTimeUnixNano),
353
+ attributes,
354
+ droppedAttributesCount: protocolCount(span.droppedAttributesCount) ?? 0,
355
+ parentSpanId: spanId(span.parentSpanId) ? span.parentSpanId : void 0,
356
+ flags: protocolFlags(span),
357
+ status: { code: protocolCount(span.status.code, 2) ?? 0 },
358
+ events: span.events.slice(0, 8).map((event) => ({
359
+ timeUnixNano: protocolTime(event.timeUnixNano),
360
+ droppedAttributesCount: protocolCount(event.droppedAttributesCount) ?? 0,
361
+ name: event.name === "exception" ? "exception" : minimalName(event.name),
362
+ attributes: event.name === "exception" ? [{
363
+ key: "exception.type",
364
+ value: code?.value ?? { stringValue: "Error" }
365
+ }] : minimalAttributes(event.attributes)
366
+ })),
367
+ droppedEventsCount: protocolCount(span.droppedEventsCount) ?? 0,
368
+ links: span.links.filter((link) => traceId(link.traceId) && spanId(link.spanId)).slice(0, 8).map((link) => ({
369
+ traceId: link.traceId,
370
+ spanId: link.spanId,
371
+ flags: protocolFlags(link),
372
+ attributes: minimalAttributes(link.attributes),
373
+ droppedAttributesCount: protocolCount(link.droppedAttributesCount) ?? 0
374
+ })),
375
+ droppedLinksCount: protocolCount(span.droppedLinksCount) ?? 0
376
+ };
377
+ })
378
+ }))
379
+ })) }),
380
+ metrics: (data) => {
381
+ const resourceMetrics = data.resourceMetrics.map((entry) => ({
382
+ resource,
383
+ scopeMetrics: entry.scopeMetrics.map((scope) => ({
384
+ scope: { name: service },
385
+ metrics: scope.metrics.filter((metric) => MINIMAL_METRIC_NAMES.has(metric.name)).flatMap((metric) => {
386
+ const gauge = metric.gauge === void 0 ? void 0 : { dataPoints: minimalNumberPoints(metric.gauge.dataPoints) };
387
+ const sum = metric.sum === void 0 ? void 0 : {
388
+ aggregationTemporality: protocolCount(metric.sum.aggregationTemporality, 2) ?? 0,
389
+ isMonotonic: metric.sum.isMonotonic === true,
390
+ dataPoints: minimalNumberPoints(metric.sum.dataPoints)
391
+ };
392
+ const histogram = metric.histogram === void 0 ? void 0 : {
393
+ aggregationTemporality: protocolCount(metric.histogram.aggregationTemporality, 2) ?? 0,
394
+ dataPoints: minimalHistogramPoints(metric.histogram.dataPoints)
395
+ };
396
+ if (![
397
+ gauge,
398
+ sum,
399
+ histogram
400
+ ].some((series) => (series?.dataPoints.length ?? 0) > 0)) return [];
401
+ return [{
402
+ name: metric.name,
403
+ unit: [
404
+ "1",
405
+ "s",
406
+ "ms"
407
+ ].includes(metric.unit ?? "") ? metric.unit : "1",
408
+ ...gauge === void 0 ? {} : { gauge },
409
+ ...sum === void 0 ? {} : { sum },
410
+ ...histogram === void 0 ? {} : { histogram }
411
+ }];
412
+ })
413
+ }))
414
+ }));
415
+ return resourceMetrics.some((entry) => entry.scopeMetrics.some((scope) => scope.metrics.length > 0)) ? HttpBody.jsonUnsafe({ resourceMetrics }) : HttpBody.empty;
416
+ }
417
+ };
418
+ }
419
+ /** Attempt to drain all engineering signals; completion does not prove ingestion. */
420
+ const flushEngineeringTelemetry = Effect.flatMap(OtlpExporter.Flusher, (flusher) => flusher.flush);
124
421
  /** Shared browser/server OTLP layer. OtlpLogger merges with incumbent loggers once. */
125
422
  const makeEngineeringTelemetryLayer = (config) => {
126
423
  const validated = validateEngineeringTelemetryConfig(config);
127
- const endpoints = postHogOtlpEndpoints(validated.host);
128
- const resource = {
129
- serviceName: validated.serviceName ?? "capxul-sdk",
130
- serviceVersion: validated.sdkVersion,
131
- attributes: {
132
- capxul_env: validated.capxulEnv,
133
- producer: validated.producer,
134
- sdk_version: validated.sdkVersion
135
- }
136
- };
137
- const tracing = Layer.effect(Tracer.Tracer, OtlpTracer.make({
138
- url: endpoints.traces,
139
- headers: validated.headers,
140
- resource
141
- }).pipe(Effect.map(makeLeakSafeEngineeringTracer))).pipe(Layer.provideMerge(OtlpExporter.layerFlusher));
142
- const logging = OtlpLogger.layer({
143
- url: endpoints.logs,
144
- headers: validated.headers,
145
- resource,
146
- mergeWithExisting: true
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);
424
+ return Layer.suspend(() => {
425
+ const endpoints = postHogOtlpEndpoints(validated.host);
426
+ let disabled = false;
427
+ const enabled = () => {
428
+ if (disabled) return false;
429
+ try {
430
+ disabled = (typeof validated.enabled === "function" ? validated.enabled() : validated.enabled ?? true) !== true;
431
+ } catch {
432
+ disabled = true;
433
+ }
434
+ return !disabled;
157
435
  };
158
- const visibilityChanged = () => {
159
- if (browserDocument.visibilityState === "hidden") flush();
436
+ const delivery = {
437
+ ...validated.shutdownTimeout === void 0 ? {} : { shutdownTimeout: validated.shutdownTimeout },
438
+ ...validated.exportInterval === void 0 ? {} : { exportInterval: validated.exportInterval }
160
439
  };
161
- yield* Effect.acquireRelease(Effect.sync(() => {
162
- browserDocument.addEventListener("visibilitychange", visibilityChanged);
163
- browserWindow.addEventListener("pagehide", flush);
164
- }), () => Effect.sync(() => {
165
- browserDocument.removeEventListener("visibilitychange", visibilityChanged);
166
- browserWindow.removeEventListener("pagehide", flush);
440
+ const resource = {
441
+ serviceName: validated.serviceName ?? "capxul-sdk",
442
+ serviceVersion: validated.sdkVersion,
443
+ attributes: {
444
+ capxul_env: validated.capxulEnv,
445
+ producer: validated.producer,
446
+ sdk_version: validated.sdkVersion
447
+ }
448
+ };
449
+ const tracing = Layer.effect(Tracer.Tracer, OtlpTracer.make({
450
+ url: endpoints.traces,
451
+ headers: validated.headers,
452
+ resource,
453
+ ...delivery
454
+ }).pipe(Effect.map((tracer) => makeLeakSafeEngineeringTracer(tracer, enabled)))).pipe(Layer.provideMerge(OtlpExporter.layerFlusher));
455
+ const logging = Logger.layer([OtlpLogger.make({
456
+ url: endpoints.logs,
457
+ headers: validated.headers,
458
+ resource,
459
+ ...delivery
460
+ }).pipe(Effect.map((logger) => Logger.make((options) => {
461
+ if (enabled()) logger.log(options);
462
+ })))], { mergeWithExisting: true }).pipe(Layer.provideMerge(OtlpExporter.layerFlusher));
463
+ const metrics = OtlpMetrics.layer({
464
+ url: endpoints.metrics,
465
+ headers: validated.headers,
466
+ resource,
467
+ temporality: "delta",
468
+ ...delivery
469
+ });
470
+ const headerPolicy = Layer.merge(Layer.succeed(HttpClient.TracerHeaderFilter, traceHeaderFilter), Layer.succeed(Headers.CurrentRedactedNames, [credentialHeaderPattern]));
471
+ const browserLifecycle = Layer.effectDiscard(Effect.gen(function* () {
472
+ if (validated.producer !== "browser" || !hasBrowserDocument()) return;
473
+ const browserWindow = window;
474
+ const browserDocument = document;
475
+ const flusher = yield* OtlpExporter.Flusher;
476
+ const run = yield* FiberSet.makeRuntime();
477
+ const flush = () => {
478
+ run(flusher.flush);
479
+ };
480
+ const visibilityChanged = () => {
481
+ if (browserDocument.visibilityState === "hidden") flush();
482
+ };
483
+ yield* Effect.acquireRelease(Effect.sync(() => {
484
+ browserDocument.addEventListener("visibilitychange", visibilityChanged);
485
+ browserWindow.addEventListener("pagehide", flush);
486
+ }), () => Effect.sync(() => {
487
+ browserDocument.removeEventListener("visibilitychange", visibilityChanged);
488
+ browserWindow.removeEventListener("pagehide", flush);
489
+ }));
167
490
  }));
168
- }));
169
- const transport = Layer.effect(HttpClient.HttpClient, Effect.map(HttpClient.HttpClient, (client) => validated.producer === "browser" && hasBrowserDocument() ? browserExportClient(client) : client)).pipe(Layer.provide(FetchHttpClient.layer));
170
- return browserLifecycle.pipe(Layer.provideMerge(Layer.mergeAll(tracing, logging, headerPolicy)), Layer.provide(OtlpSerialization.layerJson), Layer.provide(transport));
491
+ const transport = Layer.effect(HttpClient.HttpClient, Effect.map(HttpClient.HttpClient, (client) => {
492
+ const selectedTransport = validated.producer === "browser" && hasBrowserDocument() ? browserExportClient(client) : client;
493
+ return HttpClient.transform(selectedTransport, (response, request) => Effect.suspend(() => enabled() && request.body._tag !== "Empty" ? response : Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null, { status: 200 })))));
494
+ })).pipe(Layer.provide(FetchHttpClient.layer));
495
+ const serialization = Layer.effect(OtlpSerialization.OtlpSerialization, Effect.map(OtlpSerialization.OtlpSerialization, (json) => {
496
+ const projected = minimalSerialization(validated);
497
+ return validated.privacy === "minimal" ? projected : {
498
+ ...json,
499
+ metrics: projected.metrics
500
+ };
501
+ })).pipe(Layer.provide(OtlpSerialization.layerJson));
502
+ return browserLifecycle.pipe(Layer.provideMerge(Layer.mergeAll(tracing, logging, metrics, headerPolicy)), Layer.provide(serialization), Layer.provide(transport));
503
+ });
171
504
  };
172
505
  //#endregion
173
- export { ENGINEERING_CAPXUL_ENVS, makeEngineeringTelemetryLayer, makeLeakSafeEngineeringTracer, postHogOtlpEndpoints, traceHeaderFilter };
506
+ export { ENGINEERING_CAPXUL_ENVS, flushEngineeringTelemetry, makeEngineeringTelemetryLayer, makeLeakSafeEngineeringTracer, postHogOtlpEndpoints, traceHeaderFilter };