@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 +14 -0
- package/dist/engineering.d.mts +12 -3
- package/dist/engineering.mjs +388 -55
- package/dist/index.d.mts +579 -5
- package/dist/index.mjs +404 -460
- package/dist/operations-sJPrehYY.mjs +748 -0
- package/package.json +2 -2
- package/dist/src-CWfRYsBI.mjs +0 -302
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.
|
package/dist/engineering.d.mts
CHANGED
|
@@ -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 };
|
package/dist/engineering.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { i as redactUrlSecrets, n as
|
|
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,
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
159
|
-
|
|
436
|
+
const delivery = {
|
|
437
|
+
...validated.shutdownTimeout === void 0 ? {} : { shutdownTimeout: validated.shutdownTimeout },
|
|
438
|
+
...validated.exportInterval === void 0 ? {} : { exportInterval: validated.exportInterval }
|
|
160
439
|
};
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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 };
|