@capxul/observability 4.20.0-beta.31 → 4.20.0-beta.32

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
@@ -34,6 +34,7 @@ naming events rather than producing them.
34
34
 
35
35
  ```text
36
36
  src/
37
+ ├── devtools.ts
37
38
  ├── engineering-delivery.ts
38
39
  ├── engineering.ts
39
40
  ├── index.ts
@@ -41,6 +42,7 @@ src/
41
42
  ├── telemetry-port.ts
42
43
  ├── __tests__/
43
44
  │ ├── engineering-delivery.test.ts
45
+ │ ├── devtools.test.ts
44
46
  │ ├── engineering.test.ts
45
47
  │ ├── operations.test.ts
46
48
  │ ├── posthog-browser-privacy.test.ts
@@ -92,6 +94,18 @@ src/
92
94
 
93
95
  ## The required envelope (ADR-0020 A1 amendment)
94
96
 
97
+ The `@capxul/observability/devtools` subpath exports `withDevTools(layer, url)` and
98
+ `validateDevToolsUrl(url)` for local Effect inspection. An omitted URL keeps the
99
+ original layer. An explicit local WebSocket URL adds the native Effect 4
100
+ DevTools connection after the host's services and metric registry are built.
101
+ The connection closes with the host scope. Local spans retain their original
102
+ detail; existing remote exporters keep their own privacy policy. The local
103
+ tracer does not mutate the remote exporter's span methods. Its optional protocol
104
+ loads only when enabled. The separate entry stays out of Convex's static runtime graph.
105
+
106
+ See [the development workflow](../../docs/canon/development-and-verification.md#local-effect-devtools)
107
+ for the editor and agent collector commands.
108
+
95
109
  One PostHog project holds every environment, so `capxul_env` — not project
96
110
  isolation — is what keeps a production surface from reading dev traffic. It is
97
111
  REQUIRED on every catalog event, together with `producer`. Call sites do not
@@ -0,0 +1,7 @@
1
+ import { Layer } from "effect";
2
+ //#region src/devtools.d.ts
3
+ /** Local inspection is an explicit host choice, independent of remote telemetry. */
4
+ export declare function validateDevToolsUrl(value: string): string;
5
+ /** Acquire after the host layer so traces and metrics use the same context. */
6
+ export declare function withDevTools<R, E, I>(layer: Layer.Layer<R, E, I>, url: string | undefined): Layer.Layer<R, E, I>;
7
+ //#endregion
@@ -0,0 +1,72 @@
1
+ import { Effect, Layer, Tracer } from "effect";
2
+ //#region src/devtools.ts
3
+ /** Local inspection is an explicit host choice, independent of remote telemetry. */
4
+ function validateDevToolsUrl(value) {
5
+ const url = new URL(value);
6
+ if (!["ws:", "wss:"].includes(url.protocol) || ![
7
+ "localhost",
8
+ "127.0.0.1",
9
+ "[::1]",
10
+ "host.docker.internal"
11
+ ].includes(url.hostname) || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "") throw new TypeError("devToolsUrl must be a local WebSocket URL without credentials");
12
+ return url.href;
13
+ }
14
+ function localSpan(delegate) {
15
+ const attributes = new Map(delegate.attributes);
16
+ let status = delegate.status;
17
+ return {
18
+ _tag: "Span",
19
+ name: delegate.name,
20
+ spanId: delegate.spanId,
21
+ traceId: delegate.traceId,
22
+ parent: delegate.parent,
23
+ annotations: delegate.annotations,
24
+ sampled: delegate.sampled,
25
+ kind: delegate.kind,
26
+ attributes,
27
+ get status() {
28
+ return status;
29
+ },
30
+ get links() {
31
+ return delegate.links;
32
+ },
33
+ attribute(key, value) {
34
+ attributes.set(key, value);
35
+ delegate.attribute(key, value);
36
+ },
37
+ event(name, time, values) {
38
+ delegate.event(name, time, values);
39
+ },
40
+ addLinks(links) {
41
+ delegate.addLinks(links);
42
+ },
43
+ end(endTime, exit) {
44
+ delegate.end(endTime, exit);
45
+ status = {
46
+ _tag: "Ended",
47
+ startTime: status.startTime,
48
+ endTime,
49
+ exit
50
+ };
51
+ }
52
+ };
53
+ }
54
+ /** Acquire after the host layer so traces and metrics use the same context. */
55
+ function withDevTools(layer, url) {
56
+ if (url === void 0) return layer;
57
+ const endpoint = validateDevToolsUrl(url);
58
+ return Layer.unwrap(Effect.promise(async () => {
59
+ const [{ DevToolsClient }, { Socket }] = await Promise.all([import("effect/devtools"), import("effect/socket")]);
60
+ const client = DevToolsClient.layer.pipe(Layer.provide(Socket.layerWebSocket(endpoint)), Layer.provide(Socket.layerWebSocketConstructorGlobal));
61
+ return Layer.effect(Tracer.Tracer, Effect.gen(function* () {
62
+ const delegate = yield* Effect.tracer;
63
+ const mirror = Tracer.make({
64
+ span: (options) => localSpan(delegate.span(options)),
65
+ ...delegate.context === void 0 ? {} : { context: delegate.context.bind(delegate) }
66
+ });
67
+ return yield* DevToolsClient.makeTracer.pipe(Effect.provideService(Tracer.Tracer, mirror));
68
+ })).pipe(Layer.provide(client), Layer.provideMerge(layer));
69
+ }));
70
+ }
71
+ //#endregion
72
+ export { validateDevToolsUrl, withDevTools };
@@ -1,4 +1,4 @@
1
- import { I as CLI_CAPABILITIES, L as CLI_COMMAND_NAMES, R as CLI_INVOCATION_KINDS, an as isCapxulOperation, cn as ORGANIZATION_SETUP_STATUSES, dn as isCredentialField, fn as CAPXUL_ERROR_CODES, ln as redactSecrets, pn as FAILURE_MODES, sn as ORGANIZATION_SETUP_REASONS, un as redactUrlSecrets } from "./src-L9SjUzWV.mjs";
1
+ import { G as CLI_COMMAND_NAMES, K as CLI_INVOCATION_KINDS, W as CLI_CAPABILITIES, _n as redactSecrets, bn as CAPXUL_ERROR_CODES, gn as ORGANIZATION_SETUP_STATUSES, hn as ORGANIZATION_SETUP_REASONS, pn as isCapxulOperation, vn as redactUrlSecrets, xn as FAILURE_MODES, yn as isCredentialField } from "./src-BPQVVRCG.mjs";
2
2
  import { Cause, Clock, Context, Deferred, Duration, Effect, Exit, FiberSet, Layer, LogLevel, Logger, Metric, Option, References, Schedule, Stream, Tracer } from "effect";
3
3
  import { FetchHttpClient, Headers, HttpBody, HttpClient, HttpClientError, HttpClientResponse } from "effect/http";
4
4
  import { OtlpExporter, OtlpLogger, OtlpMetrics, OtlpSerialization, OtlpTracer } from "effect/observability";
@@ -75,6 +75,123 @@ const attributes = (input, limit = 32) => {
75
75
  };
76
76
  const terminalName = (name) => name === "cli.command.completed" || name === "cli.wallet.step" || CLI_COMMAND_NAMES.some((command) => name === `cli.command.${command}`);
77
77
  //#endregion
78
+ //#region src/engineering/tracer.ts
79
+ const failureText = (failure, key, limit, source) => {
80
+ if (failure === null || typeof failure !== "object") return void 0;
81
+ try {
82
+ let owner = failure;
83
+ let descriptor;
84
+ for (let depth = 0; owner !== null && depth < 4; depth += 1) {
85
+ descriptor = Object.getOwnPropertyDescriptor(owner, key);
86
+ if (descriptor !== void 0 || key !== "name") break;
87
+ owner = Object.getPrototypeOf(owner);
88
+ }
89
+ const value = descriptor?.value;
90
+ return typeof value === "string" || key === "code" && typeof value === "number" ? redactSecrets(String(value), source).slice(0, limit) : void 0;
91
+ } catch {
92
+ return;
93
+ }
94
+ };
95
+ const exportFailure = (failure, source) => {
96
+ const message = failureText(failure, "message", 2048, source) ?? (typeof failure === "string" ? redactSecrets(failure, source).slice(0, 2048) : "Engineering operation failed");
97
+ const code = failureText(failure, "code", 128);
98
+ const error = new Error((code === void 0 ? message : `${message} [code=${code}]`).slice(0, 2048));
99
+ error.name = failureText(failure, "_tag", 128) ?? failureText(failure, "name", 128) ?? "Error";
100
+ error.stack = failureText(failure, "stack", 8192, source) ?? `${error.name}: ${error.message}`;
101
+ return error;
102
+ };
103
+ const exportFailureExit = (cause, source) => Exit.failCause(Cause.fromReasons(cause.reasons.filter((reason) => !Cause.isInterruptReason(reason)).slice(0, 8).map((reason) => Cause.isFailReason(reason) ? Cause.makeFailReason(exportFailure(reason.error, source)) : Cause.makeDieReason(exportFailure(reason.defect, source)))));
104
+ const BROWSER_KEEPALIVE_BYTES = 65536;
105
+ let unfinishedBrowserExportBytes = 0;
106
+ const hasBrowserDocument = () => typeof window !== "undefined" && typeof document !== "undefined";
107
+ const browserExportClient = (client) => HttpClient.transform(client, (response, request) => Effect.acquireUseRelease(Effect.sync(() => {
108
+ const bytes = "contentLength" in request.body ? request.body.contentLength : void 0;
109
+ if (request.body._tag === "Stream" || request.body._tag === "FormData" || bytes === void 0 || !Number.isSafeInteger(bytes) || bytes < 0 || unfinishedBrowserExportBytes + bytes > BROWSER_KEEPALIVE_BYTES) return;
110
+ unfinishedBrowserExportBytes += bytes;
111
+ return bytes;
112
+ }), (bytes) => response.pipe(Effect.provideService(FetchHttpClient.RequestInit, { keepalive: bytes !== void 0 }), Effect.tap((result) => bytes === void 0 ? Effect.void : Stream.runDrain(result.stream).pipe(Effect.catchIf((error) => error.reason._tag === "EmptyBodyError", () => Effect.void)))), (bytes) => Effect.sync(() => {
113
+ if (bytes !== void 0) unfinishedBrowserExportBytes -= bytes;
114
+ })));
115
+ const spanUpstream = (span) => {
116
+ let current = span;
117
+ while (current?._tag === "Span") {
118
+ const upstream = current.attributes.get("chain.upstream");
119
+ if (upstream === "alchemy" || upstream === "infura" || upstream === "gateway") return upstream;
120
+ current = current.parent._tag === "Some" ? current.parent.value : void 0;
121
+ }
122
+ };
123
+ function redactHttpPath(path, fullUrl, source) {
124
+ let origin = "https://redaction.invalid";
125
+ try {
126
+ const url = new URL(String(fullUrl));
127
+ if (url.protocol === "https:" || url.protocol === "http:") origin = url.origin;
128
+ } catch {}
129
+ const prefix = path.startsWith("/") ? origin : "";
130
+ return redactUrlSecrets(`${prefix}${path}`, source).slice(prefix.length, prefix.length + 2048);
131
+ }
132
+ /** A span whose prototype is `span`: it answers every Span member until an own property overrides it. */
133
+ function inheritSpan(span) {
134
+ const wrapped = Object.create(span);
135
+ Object.defineProperties(wrapped, {
136
+ traceId: { get: () => span.traceId },
137
+ spanId: { get: () => span.spanId },
138
+ attributes: { get: () => span.attributes },
139
+ event: {
140
+ value: span.event.bind(span),
141
+ configurable: true
142
+ },
143
+ addLinks: {
144
+ value: span.addLinks.bind(span),
145
+ configurable: true
146
+ }
147
+ });
148
+ return wrapped;
149
+ }
150
+ /** Preserve domain exits while preventing the OTLP serializer from seeing raw causes. */
151
+ const makeLeakSafeEngineeringTracer = (delegate, enabled = () => true) => Tracer.make({
152
+ span(options) {
153
+ const span = enabled() ? delegate.span(options) : new Tracer.NativeSpan(options);
154
+ const wrapped = inheritSpan(span);
155
+ Object.defineProperty(wrapped, "attribute", { value: (name, value) => {
156
+ const source = spanUpstream(span);
157
+ const header = /^http\.(?:request|response)\.header\.(.+)$/u.exec(name)?.[1];
158
+ if (header !== void 0 && isCredentialField(header, "header")) span.attribute(name, "[REDACTED]");
159
+ else if (typeof value === "string" && name === "url.query") span.attribute(name, redactUrlSecrets(`?${value}`).slice(1, 2049));
160
+ else if (typeof value === "string" && (name === "url.path" || name === "http.target")) {
161
+ const fullUrl = span.attributes.get("url.full") ?? span.attributes.get("http.url");
162
+ const path = name === "url.path" && typeof fullUrl === "string" ? /^https?:\/\/[^/]+([^?#]*)/u.exec(fullUrl)?.[1] ?? value : value;
163
+ span.attribute(name, redactHttpPath(path, fullUrl, source));
164
+ } 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));
165
+ else if (header !== void 0 && typeof value === "string") span.attribute(name, redactSecrets(value, source).slice(0, 2048));
166
+ else span.attribute(name, value);
167
+ } });
168
+ Object.defineProperty(wrapped, "end", {
169
+ configurable: false,
170
+ enumerable: false,
171
+ value: (endTime, exit) => {
172
+ if (!enabled()) {
173
+ Object.defineProperty(wrapped, "status", { value: {
174
+ _tag: "Ended",
175
+ startTime: span.status.startTime,
176
+ endTime,
177
+ exit
178
+ } });
179
+ return;
180
+ }
181
+ if (Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause)) {
182
+ const first = exit.cause.reasons.find((reason) => !Cause.isInterruptReason(reason));
183
+ const code = failureText(first === void 0 ? void 0 : Cause.isFailReason(first) ? first.error : first.defect, "code", 128);
184
+ if (code !== void 0) span.attribute("error.code", code);
185
+ span.end(endTime, exportFailureExit(exit.cause, spanUpstream(span)));
186
+ } else span.end(endTime, exit);
187
+ },
188
+ writable: false
189
+ });
190
+ return wrapped;
191
+ },
192
+ ...delegate.context === void 0 ? {} : { context: delegate.context.bind(delegate) }
193
+ });
194
+ //#endregion
78
195
  //#region src/delivery/records.ts
79
196
  function admittedLogger(state, delegate) {
80
197
  return Logger.make((options) => {
@@ -130,7 +247,7 @@ function admittedTracer(state, delegate) {
130
247
  /** The span as a producer sees it: at most 32 fields and 2000 attribute bytes, 2 events, 2 links. */
131
248
  function boundedSpan(state, span) {
132
249
  const { budget } = state;
133
- const wrapped = Object.create(span);
250
+ const wrapped = inheritSpan(span);
134
251
  let fields = 0, events = 0, links = 0, attributeBytes = 0;
135
252
  let ended = false;
136
253
  Object.defineProperties(wrapped, {
@@ -466,109 +583,6 @@ const validateEngineeringTelemetryConfig = (config) => {
466
583
  };
467
584
  };
468
585
  //#endregion
469
- //#region src/engineering/tracer.ts
470
- const failureText = (failure, key, limit, source) => {
471
- if (failure === null || typeof failure !== "object") return void 0;
472
- try {
473
- let owner = failure;
474
- let descriptor;
475
- for (let depth = 0; owner !== null && depth < 4; depth += 1) {
476
- descriptor = Object.getOwnPropertyDescriptor(owner, key);
477
- if (descriptor !== void 0 || key !== "name") break;
478
- owner = Object.getPrototypeOf(owner);
479
- }
480
- const value = descriptor?.value;
481
- return typeof value === "string" || key === "code" && typeof value === "number" ? redactSecrets(String(value), source).slice(0, limit) : void 0;
482
- } catch {
483
- return;
484
- }
485
- };
486
- const exportFailure = (failure, source) => {
487
- const message = failureText(failure, "message", 2048, source) ?? (typeof failure === "string" ? redactSecrets(failure, source).slice(0, 2048) : "Engineering operation failed");
488
- const code = failureText(failure, "code", 128);
489
- const error = new Error((code === void 0 ? message : `${message} [code=${code}]`).slice(0, 2048));
490
- error.name = failureText(failure, "_tag", 128) ?? failureText(failure, "name", 128) ?? "Error";
491
- error.stack = failureText(failure, "stack", 8192, source) ?? `${error.name}: ${error.message}`;
492
- return error;
493
- };
494
- const exportFailureExit = (cause, source) => Exit.failCause(Cause.fromReasons(cause.reasons.filter((reason) => !Cause.isInterruptReason(reason)).slice(0, 8).map((reason) => Cause.isFailReason(reason) ? Cause.makeFailReason(exportFailure(reason.error, source)) : Cause.makeDieReason(exportFailure(reason.defect, source)))));
495
- const BROWSER_KEEPALIVE_BYTES = 65536;
496
- let unfinishedBrowserExportBytes = 0;
497
- const hasBrowserDocument = () => typeof window !== "undefined" && typeof document !== "undefined";
498
- const browserExportClient = (client) => HttpClient.transform(client, (response, request) => Effect.acquireUseRelease(Effect.sync(() => {
499
- const bytes = "contentLength" in request.body ? request.body.contentLength : void 0;
500
- if (request.body._tag === "Stream" || request.body._tag === "FormData" || bytes === void 0 || !Number.isSafeInteger(bytes) || bytes < 0 || unfinishedBrowserExportBytes + bytes > BROWSER_KEEPALIVE_BYTES) return;
501
- unfinishedBrowserExportBytes += bytes;
502
- return bytes;
503
- }), (bytes) => response.pipe(Effect.provideService(FetchHttpClient.RequestInit, { keepalive: bytes !== void 0 }), Effect.tap((result) => bytes === void 0 ? Effect.void : Stream.runDrain(result.stream).pipe(Effect.catchIf((error) => error.reason._tag === "EmptyBodyError", () => Effect.void)))), (bytes) => Effect.sync(() => {
504
- if (bytes !== void 0) unfinishedBrowserExportBytes -= bytes;
505
- })));
506
- const spanUpstream = (span) => {
507
- let current = span;
508
- while (current?._tag === "Span") {
509
- const upstream = current.attributes.get("chain.upstream");
510
- if (upstream === "alchemy" || upstream === "infura" || upstream === "gateway") return upstream;
511
- current = current.parent._tag === "Some" ? current.parent.value : void 0;
512
- }
513
- };
514
- function redactHttpPath(path, fullUrl, source) {
515
- let origin = "https://redaction.invalid";
516
- try {
517
- const url = new URL(String(fullUrl));
518
- if (url.protocol === "https:" || url.protocol === "http:") origin = url.origin;
519
- } catch {}
520
- const prefix = path.startsWith("/") ? origin : "";
521
- return redactUrlSecrets(`${prefix}${path}`, source).slice(prefix.length, prefix.length + 2048);
522
- }
523
- /** A span whose prototype is `span`: it answers every Span member until an own property overrides it. */
524
- function inheritSpan(span) {
525
- return Object.create(span);
526
- }
527
- /** Preserve domain exits while preventing the OTLP serializer from seeing raw causes. */
528
- const makeLeakSafeEngineeringTracer = (delegate, enabled = () => true) => Tracer.make({
529
- span(options) {
530
- const span = enabled() ? delegate.span(options) : new Tracer.NativeSpan(options);
531
- const wrapped = inheritSpan(span);
532
- Object.defineProperty(wrapped, "attribute", { value: (name, value) => {
533
- const source = spanUpstream(span);
534
- const header = /^http\.(?:request|response)\.header\.(.+)$/u.exec(name)?.[1];
535
- if (header !== void 0 && isCredentialField(header, "header")) span.attribute(name, "[REDACTED]");
536
- else if (typeof value === "string" && name === "url.query") span.attribute(name, redactUrlSecrets(`?${value}`).slice(1, 2049));
537
- else if (typeof value === "string" && (name === "url.path" || name === "http.target")) {
538
- const fullUrl = span.attributes.get("url.full") ?? span.attributes.get("http.url");
539
- const path = name === "url.path" && typeof fullUrl === "string" ? /^https?:\/\/[^/]+([^?#]*)/u.exec(fullUrl)?.[1] ?? value : value;
540
- span.attribute(name, redactHttpPath(path, fullUrl, source));
541
- } 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));
542
- else if (header !== void 0 && typeof value === "string") span.attribute(name, redactSecrets(value, source).slice(0, 2048));
543
- else span.attribute(name, value);
544
- } });
545
- Object.defineProperty(wrapped, "end", {
546
- configurable: false,
547
- enumerable: false,
548
- value: (endTime, exit) => {
549
- if (!enabled()) {
550
- Object.defineProperty(wrapped, "status", { value: {
551
- _tag: "Ended",
552
- startTime: span.status.startTime,
553
- endTime,
554
- exit
555
- } });
556
- return;
557
- }
558
- if (Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause)) {
559
- const first = exit.cause.reasons.find((reason) => !Cause.isInterruptReason(reason));
560
- const code = failureText(first === void 0 ? void 0 : Cause.isFailReason(first) ? first.error : first.defect, "code", 128);
561
- if (code !== void 0) span.attribute("error.code", code);
562
- span.end(endTime, exportFailureExit(exit.cause, spanUpstream(span)));
563
- } else span.end(endTime, exit);
564
- },
565
- writable: false
566
- });
567
- return wrapped;
568
- },
569
- ...delegate.context === void 0 ? {} : { context: delegate.context.bind(delegate) }
570
- });
571
- //#endregion
572
586
  //#region src/engineering/minimal.ts
573
587
  const MINIMAL_LOG_NAMES = /* @__PURE__ */ new Set([
574
588
  "capxul_sdk_operation",