@arcote.tech/arc-otel 0.7.6

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 ADDED
@@ -0,0 +1,246 @@
1
+ # @arcote.tech/arc-otel
2
+
3
+ OpenTelemetry instrumentation primitives for the Arc framework. Wraps the
4
+ core OTel SDKs (`@opentelemetry/*`) behind an Arc-friendly API with
5
+ PII-safe defaults, dev/prod sampling modes, and W3C Trace Context
6
+ propagation.
7
+
8
+ This package is **optional**. Arc apps run identically whether you import
9
+ it or not — every span call short-circuits when no SDK is attached. Opt
10
+ in by setting `observability.enabled: true` in `deploy.arc.json` (or by
11
+ exporting `ARC_OTEL_ENABLED=true` for ad-hoc local runs).
12
+
13
+ ---
14
+
15
+ ## Architecture
16
+
17
+ ```
18
+ ┌─ browser ───────────────────────────────────────────┐
19
+ │ start-app.ts │
20
+ │ └─ if (window.__ARC_OTEL_CONFIG) │
21
+ │ await import("@arcote.tech/arc-otel/browser") │
22
+ │ initBrowserTelemetry({...}) │
23
+ └─────────────────────────────────────────────────────┘
24
+
25
+ │ OTLP/HTTP ──/otel/v1/traces──┐
26
+ ▼ ▼
27
+ ┌─ arc-prod container ─────────┐ ┌─ otel-collector ────┐
28
+ │ startPlatformServer() │ OTLP │ receivers.otlp │
29
+ │ └─ initServerTelemetry({…}) ├────────▶│ processors: │
30
+ │ traces + logs + metrics │ │ - tail_sampling │
31
+ │ createArcServer({telemetry})│ │ - attributes │
32
+ │ └─ HTTP span │ │ - batch │
33
+ │ └─ WS message span │ │ exporters: │
34
+ │ └─ command.<name> span │ │ - tempo (traces) │
35
+ │ └─ db.find/set/commit span │ │ - loki (logs) │
36
+ └──────────────────────────────┘ │ - prom (metrics) │
37
+ └────────┬────────────┘
38
+
39
+ ┌──────────────────┼────────────────┐
40
+ ▼ ▼ ▼
41
+ ┌─ tempo ────┐ ┌─ loki ─────┐ ┌─ prometheus ┐
42
+ │ 7d traces │ │ 7d logs │ │ 30d metrics │
43
+ └────────────┘ └────────────┘ └─────────────┘
44
+ \ | /
45
+ \ | /
46
+ ▼ ▼ ▼
47
+ ┌─ grafana (HTTPS via Caddy basic-auth) ─┐
48
+ │ observability.<apex-of-app-domain> │
49
+ │ admin / ARC_OBSERVABILITY_PASSWORD │
50
+ └────────────────────────────────────────┘
51
+ ```
52
+
53
+ ---
54
+
55
+ ## Quick start (production deploy)
56
+
57
+ Opt-in via `deploy.arc.json`:
58
+
59
+ ```jsonc
60
+ {
61
+ "target": {...},
62
+ "envs": {
63
+ "prod": { "domain": "app.example.com", "db": { "type": "postgres" } }
64
+ },
65
+ "caddy": { "email": "ops@example.com" },
66
+ "registry": { "domain": "registry.example.com", "passwordEnv": "ARC_REGISTRY_PASSWORD" },
67
+
68
+ "observability": {
69
+ "enabled": true
70
+ // optional:
71
+ // "subdomain": "observability",
72
+ // "adminPasswordEnv": "ARC_OBSERVABILITY_PASSWORD",
73
+ // "retention": { "traces": "168h", "logs": "168h", "metrics": "30d" }
74
+ }
75
+ }
76
+ ```
77
+
78
+ Then:
79
+
80
+ ```bash
81
+ arc platform deploy prod
82
+ ```
83
+
84
+ First deploy auto-generates `ARC_OBSERVABILITY_PASSWORD` into
85
+ `deploy.arc.env` (gitignored) and provisions five sidecar containers:
86
+ `otel-collector`, `tempo`, `loki`, `prometheus`, `grafana`. Grafana is
87
+ reachable at `https://observability.<apex>/` behind Caddy basic-auth
88
+ (`admin` / generated password).
89
+
90
+ Existing deploys that don't set `observability` are unchanged — no new
91
+ containers, no env-vars on the app, zero added latency.
92
+
93
+ ---
94
+
95
+ ## What's instrumented
96
+
97
+ ### Server (always when `ARC_OTEL_ENABLED=true`)
98
+
99
+ | Span name | Source | Notable attrs |
100
+ |---|---|---|
101
+ | `${METHOD} ${PATH}` | arc-host fetch handler | `http.request.method`, `http.route`, `http.response.status_code` |
102
+ | `ws.${type}` | arc-host websocket dispatch | `messaging.message.type`, `arc.ws.client_id` |
103
+ | `command.${name}` | ContextHandler.executeCommand | `rpc.system=arc`, `arc.command.name`, `arc.command.params_size`, payload in dev only |
104
+ | `db.${op} ${store}` | wrapDbAdapter (postgres/sqlite) | `db.system`, `db.operation.name`, `db.collection.name`, `db.response.row_count` |
105
+
106
+ Server emits **metrics** automatically:
107
+
108
+ | Metric | Type | Labels |
109
+ |---|---|---|
110
+ | `arc.commands.total` | Counter | `arc.command.name` |
111
+ | `arc.command.duration_ms` | Histogram | `arc.command.name` |
112
+ | `arc.db.find_ms` | Histogram | `db.system`, `db.collection.name` |
113
+
114
+ ### Browser (when `window.__ARC_OTEL_CONFIG` is present)
115
+
116
+ - W3C Trace Context propagator registered globally — outbound `fetch`
117
+ calls automatically attach `traceparent` / `tracestate` headers so the
118
+ server's HTTP span can pick up the client trace.
119
+ - SDK chunk is `import()`-ed on demand from `start-app.ts`, so initial
120
+ bundle size stays untouched when observability is off.
121
+ - Anonymous session id stored in `sessionStorage` (`arc:otel-session-id`)
122
+ groups spans per tab — never use as a user identifier.
123
+
124
+ > Per-hook spans (`useQuery`, `useMutation`) and WS-frame trace context
125
+ > injection are **deliberately out of scope** for v1. The server already
126
+ > wraps every command/query in a span that's parented to the HTTP route,
127
+ > which gives end-to-end visibility for traffic that originates from
128
+ > `fetch` calls. WS traceparent propagation is a follow-up — when added,
129
+ > call `injectTraceContext` on outbound messages.
130
+
131
+ ---
132
+
133
+ ## Sampling
134
+
135
+ The SDK is configured `parentbased_always_on` on the server and
136
+ `traceIdRatioBased(0.1)` on the browser by default. Final decisions are
137
+ made by the **collector's tail sampler**, evaluated 10s after the trace
138
+ finishes:
139
+
140
+ 1. **All errors** are kept (any span with status_code=ERROR).
141
+ 2. **All slow traces** are kept (duration > 500 ms).
142
+ 3. **10% of everything else** is kept (random).
143
+
144
+ This guarantees you'll never miss a failure or a latency outlier, while
145
+ typical happy-path traffic costs ~10% of total trace bandwidth.
146
+
147
+ Tune in `observability-configs.ts:generateOtelCollectorConfig` — edit
148
+ the `tail_sampling` block's policies (full spec in the OTel collector docs).
149
+
150
+ ---
151
+
152
+ ## PII safety
153
+
154
+ Span attributes go through `sanitizeAttrs()` by default, which:
155
+
156
+ - Drops any key matching `(password|token|secret|authorization|jwt|api_key|cookie|email|credit_card|ssn)` (case-insensitive, recursive).
157
+ - Truncates strings longer than 2 KB.
158
+ - Truncates serialized objects longer than 4 KB.
159
+ - Catches circular references → `"[circular]"`.
160
+
161
+ `shouldIncludePayloads()` defaults to **true in development, false in
162
+ production**. Instrumentation sites that attach raw mutation params
163
+ gate on this flag — so a prod span shows `arc.command.params_size`,
164
+ while a dev span shows the (sanitized) payload itself.
165
+
166
+ DATABASE_URL and similar credentials passing through error messages
167
+ are run through `redactConnectionString()`.
168
+
169
+ **Forbidden** (never attach as a span attribute):
170
+ - Raw JWT / rawToken
171
+ - `TokenPayload.params` in full — only `tokenType` + sanitized id-like params
172
+ - Full event payload in prod
173
+ - Raw DB error parameters
174
+
175
+ ---
176
+
177
+ ## Local development
178
+
179
+ For a fast feedback loop without the full sidecar stack:
180
+
181
+ ```bash
182
+ # Terminal 1 — Jaeger all-in-one (only traces, no logs/metrics).
183
+ docker run --rm -d --name jaeger \
184
+ -p 16686:16686 -p 4318:4318 \
185
+ cr.jaegertracing.io/jaegertracing/all-in-one:1.62
186
+
187
+ # Terminal 2 — your app
188
+ export ARC_OTEL_ENABLED=true
189
+ export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
190
+ arc platform dev
191
+
192
+ # Open http://localhost:16686 — service "arc-<env>"
193
+ ```
194
+
195
+ Browser spans need `window.__ARC_OTEL_CONFIG` injected by
196
+ `generateShellHtml`, which fires when the server has
197
+ `ARC_OTEL_ENABLED=true` — so a single env var enables both sides.
198
+
199
+ ---
200
+
201
+ ## API surface
202
+
203
+ ```ts
204
+ import {
205
+ ArcTelemetry,
206
+ sanitizeAttrs,
207
+ redactConnectionString,
208
+ injectTraceContext,
209
+ extractTraceContext,
210
+ contextFromHeaders,
211
+ wrapDbAdapter,
212
+ } from "@arcote.tech/arc-otel";
213
+
214
+ import { initServerTelemetry } from "@arcote.tech/arc-otel/server";
215
+ import { initBrowserTelemetry } from "@arcote.tech/arc-otel/browser";
216
+ ```
217
+
218
+ Key methods on `ArcTelemetry`:
219
+
220
+ ```ts
221
+ // Wrap an async function in a span; auto-records exceptions + sets status.
222
+ await telemetry.startSpan(name, async (span) => {...}, { kind, attributes });
223
+
224
+ // Counter / histogram / log API mirrors OTel's.
225
+ telemetry.incrementCounter("arc.foo.total", 1, { label: "x" });
226
+ telemetry.recordHistogram("arc.foo.duration_ms", elapsed, {...});
227
+ telemetry.log("info", "message", { attr: "value" });
228
+
229
+ // Bridge inbound trace context (HTTP headers, WS frame fields) before
230
+ // starting the active span.
231
+ telemetry.runWithExtractedContext(req.headers, () =>
232
+ telemetry.startSpan("http.request", async () => {...}),
233
+ );
234
+ ```
235
+
236
+ ---
237
+
238
+ ## Out of scope
239
+
240
+ - Alerting (Grafana alerts + notifier config).
241
+ - Long-term storage (S3/GCS) for traces and logs — needs cloud creds.
242
+ - Continuous profiling (Pyroscope/Parca).
243
+ - Synthetic uptime monitoring.
244
+ - Per-hook React span instrumentation (planned follow-up).
245
+ - WS frame traceparent injection on the client (helper exists,
246
+ integration with EventWire/CommandWire pending).
@@ -0,0 +1,29 @@
1
+ import { type Context, type Span } from "@opentelemetry/api";
2
+ /** Standard W3C headers we care about. Useful as a constant for HTTP setup. */
3
+ export declare const TRACE_CONTEXT_HEADERS: readonly ["traceparent", "tracestate"];
4
+ /**
5
+ * Inject the current active span's trace context into `carrier`.
6
+ *
7
+ * Use case: serializing a WS frame payload. The receiver passes the same
8
+ * carrier to `extractTraceContext` and runs the handler inside that context.
9
+ */
10
+ export declare function injectTraceContext(carrier?: Record<string, string>): Record<string, string>;
11
+ /**
12
+ * Extract a parent trace context from `carrier` and return it. Run handler
13
+ * code via `context.with(parentCtx, () => ...)` or `telemetry.withSpan` so
14
+ * spans created inside link back to the parent automatically.
15
+ *
16
+ * Returns the current context unchanged when no traceparent is present —
17
+ * fine for unauthenticated probes or older clients without instrumentation.
18
+ */
19
+ export declare function extractTraceContext(carrier: Record<string, unknown>): Context;
20
+ /**
21
+ * Pull the parent span out of an arbitrary headers-like object (lowercased
22
+ * keys) and return it as an OTel-ready context. Convenience wrapper used by
23
+ * the HTTP server handler.
24
+ */
25
+ export declare function contextFromHeaders(headers: Headers | Record<string, string | string[] | undefined>): Context;
26
+ /** Get the currently active span. Convenience re-export so callers don't
27
+ * need to depend on @opentelemetry/api directly. */
28
+ export declare function getActiveSpan(): Span | undefined;
29
+ //# sourceMappingURL=context-propagation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-propagation.d.ts","sourceRoot":"","sources":["../src/context-propagation.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,OAAO,EACZ,KAAK,IAAI,EACV,MAAM,oBAAoB,CAAC;AAY5B,+EAA+E;AAC/E,eAAO,MAAM,qBAAqB,wCAAyC,CAAC;AAE5E;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAG/F;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAE7E;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,GAAG,OAAO,CAe5G;AAED;qDACqD;AACrD,wBAAgB,aAAa,IAAI,IAAI,GAAG,SAAS,CAEhD"}
@@ -0,0 +1,7 @@
1
+ export { ArcTelemetry, type ObservabilityMode, type SpanOptions, type TelemetryConfig, } from "./telemetry";
2
+ export { sanitizeAttrs, redactConnectionString, DEFAULT_REDACT_KEY_PATTERN, DEFAULT_MAX_STRING_LEN, DEFAULT_MAX_JSON_LEN, type SanitizeOptions, } from "./sanitize";
3
+ export { injectTraceContext, extractTraceContext, contextFromHeaders, getActiveSpan, TRACE_CONTEXT_HEADERS, } from "./context-propagation";
4
+ export { wrapDbAdapter } from "./wrap-db-adapter";
5
+ export { SpanKind, SpanStatusCode } from "@opentelemetry/api";
6
+ export type { Attributes, Span } from "@opentelemetry/api";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,YAAY,EACZ,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,aAAa,EACb,sBAAsB,EACtB,0BAA0B,EAC1B,sBAAsB,EACtB,oBAAoB,EACpB,KAAK,eAAe,GACrB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,aAAa,EACb,qBAAqB,GACtB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAGlD,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAC9D,YAAY,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,362 @@
1
+ // src/telemetry.ts
2
+ import {
3
+ context,
4
+ propagation,
5
+ SpanStatusCode,
6
+ trace
7
+ } from "@opentelemetry/api";
8
+ import {
9
+ logs,
10
+ SeverityNumber
11
+ } from "@opentelemetry/api-logs";
12
+
13
+ // src/sanitize.ts
14
+ var DEFAULT_REDACT_KEY_PATTERN = /(password|passwd|token|secret|authorization|jwt|api[_-]?key|cookie|email|credit[_-]?card|ssn)/i;
15
+ var DEFAULT_MAX_STRING_LEN = 2048;
16
+ var DEFAULT_MAX_JSON_LEN = 4096;
17
+ function sanitizeAttrs(input, opts = {}) {
18
+ if (!input)
19
+ return {};
20
+ const redactPattern = opts.redactKeyPattern ?? DEFAULT_REDACT_KEY_PATTERN;
21
+ const maxStr = opts.maxStringLen ?? DEFAULT_MAX_STRING_LEN;
22
+ const maxJson = opts.maxJsonLen ?? DEFAULT_MAX_JSON_LEN;
23
+ const out = {};
24
+ for (const [key, raw] of Object.entries(input)) {
25
+ if (redactPattern.test(key))
26
+ continue;
27
+ const value = sanitizeValue(raw, redactPattern, maxStr, maxJson);
28
+ if (value !== undefined)
29
+ out[key] = value;
30
+ }
31
+ return out;
32
+ }
33
+ function sanitizeValue(raw, redactPattern, maxStr, maxJson) {
34
+ if (raw === null || raw === undefined)
35
+ return;
36
+ if (typeof raw === "boolean" || typeof raw === "number")
37
+ return raw;
38
+ if (typeof raw === "string") {
39
+ return raw.length > maxStr ? `${raw.slice(0, maxStr)}…(truncated:${raw.length})` : raw;
40
+ }
41
+ try {
42
+ const filtered = filterRedacted(raw, redactPattern);
43
+ const json = JSON.stringify(filtered);
44
+ if (json === undefined)
45
+ return;
46
+ return json.length > maxJson ? `${json.slice(0, maxJson)}…(truncated:${json.length})` : json;
47
+ } catch {
48
+ return "[unserializable]";
49
+ }
50
+ }
51
+ function filterRedacted(node, pattern, seen = new WeakSet) {
52
+ if (node === null || typeof node !== "object")
53
+ return node;
54
+ if (seen.has(node))
55
+ return "[circular]";
56
+ seen.add(node);
57
+ if (Array.isArray(node)) {
58
+ return node.map((v) => filterRedacted(v, pattern, seen));
59
+ }
60
+ const out = {};
61
+ for (const [k, v] of Object.entries(node)) {
62
+ if (pattern.test(k))
63
+ continue;
64
+ out[k] = filterRedacted(v, pattern, seen);
65
+ }
66
+ return out;
67
+ }
68
+ function redactConnectionString(url) {
69
+ if (!url)
70
+ return "";
71
+ try {
72
+ const u = new URL(url);
73
+ if (u.password)
74
+ u.password = "***";
75
+ return u.toString();
76
+ } catch {
77
+ return "[unparseable]";
78
+ }
79
+ }
80
+
81
+ // src/telemetry.ts
82
+ class ArcTelemetry {
83
+ config;
84
+ tracer = null;
85
+ logger = null;
86
+ meter = null;
87
+ histograms = new Map;
88
+ counters = new Map;
89
+ constructor(config) {
90
+ const mode = config.mode ?? "development";
91
+ const enabled = config.enabled ?? mode !== "disabled";
92
+ const sampleRate = config.sampleRate ?? (config.environment === "server" ? 1 : 0.1);
93
+ this.config = {
94
+ ...config,
95
+ enabled,
96
+ sampleRate,
97
+ mode,
98
+ debug: config.debug ?? false
99
+ };
100
+ }
101
+ attach(opts) {
102
+ this.tracer = opts.tracer;
103
+ this.logger = opts.logger ?? null;
104
+ this.meter = opts.meter ?? null;
105
+ }
106
+ get active() {
107
+ return this.config.enabled && this.tracer !== null;
108
+ }
109
+ shouldIncludePayloads() {
110
+ if (this.config.includePayloads !== undefined)
111
+ return this.config.includePayloads;
112
+ return this.config.mode === "development";
113
+ }
114
+ async startSpan(name, fn, options = {}) {
115
+ if (!this.active || !this.tracer) {
116
+ return fn(trace.getActiveSpan() ?? noopSpan());
117
+ }
118
+ const attributes = this.toAttributes(options.attributes, options.unsafeAttrs);
119
+ return this.tracer.startActiveSpan(name, { kind: options.kind, attributes }, async (span) => {
120
+ try {
121
+ const result = await fn(span);
122
+ span.setStatus({ code: SpanStatusCode.OK });
123
+ return result;
124
+ } catch (error) {
125
+ this.recordError(span, error);
126
+ throw error;
127
+ } finally {
128
+ span.end();
129
+ }
130
+ });
131
+ }
132
+ createSpan(name, options = {}) {
133
+ if (!this.active || !this.tracer)
134
+ return noopSpan();
135
+ const attributes = this.toAttributes(options.attributes, options.unsafeAttrs);
136
+ return this.tracer.startSpan(name, { kind: options.kind, attributes });
137
+ }
138
+ getCurrentSpan() {
139
+ return trace.getActiveSpan();
140
+ }
141
+ withSpan(parent, fn) {
142
+ return context.with(trace.setSpan(context.active(), parent), fn);
143
+ }
144
+ runWithExtractedContext(carrier, fn) {
145
+ if (!this.active)
146
+ return fn();
147
+ const flat = {};
148
+ if (typeof Headers !== "undefined" && carrier instanceof Headers) {
149
+ carrier.forEach((value, key) => {
150
+ flat[key.toLowerCase()] = value;
151
+ });
152
+ } else if (carrier) {
153
+ for (const [k, v] of Object.entries(carrier)) {
154
+ if (typeof v === "string")
155
+ flat[k.toLowerCase()] = v;
156
+ }
157
+ }
158
+ const parent = propagation.extract(context.active(), flat);
159
+ return context.with(parent, fn);
160
+ }
161
+ recordError(span, error) {
162
+ const err = error instanceof Error ? error : new Error(String(error ?? "unknown error"));
163
+ try {
164
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
165
+ span.recordException(err);
166
+ } catch {}
167
+ }
168
+ addAttributes(attrs, unsafeAttrs = false) {
169
+ const span = this.getCurrentSpan();
170
+ if (!span)
171
+ return;
172
+ span.setAttributes(this.toAttributes(attrs, unsafeAttrs));
173
+ }
174
+ log(level, body, attrs = {}, unsafeAttrs = false) {
175
+ if (!this.active)
176
+ return;
177
+ const logger = this.logger ?? logs.getLogger(this.config.serviceName);
178
+ const record = {
179
+ severityNumber: severityFor(level),
180
+ severityText: level.toUpperCase(),
181
+ body,
182
+ attributes: this.toAttributes(attrs, unsafeAttrs)
183
+ };
184
+ try {
185
+ logger.emit(record);
186
+ } catch {}
187
+ }
188
+ incrementCounter(name, value = 1, attrs = {}) {
189
+ if (!this.active || !this.meter)
190
+ return;
191
+ let counter = this.counters.get(name);
192
+ if (!counter) {
193
+ counter = this.meter.createCounter(name);
194
+ this.counters.set(name, counter);
195
+ }
196
+ try {
197
+ counter.add(value, attrs);
198
+ } catch {}
199
+ }
200
+ recordHistogram(name, value, attrs = {}) {
201
+ if (!this.active || !this.meter)
202
+ return;
203
+ let histogram = this.histograms.get(name);
204
+ if (!histogram) {
205
+ histogram = this.meter.createHistogram(name, { unit: "ms" });
206
+ this.histograms.set(name, histogram);
207
+ }
208
+ try {
209
+ histogram.record(value, attrs);
210
+ } catch {}
211
+ }
212
+ measureSince(name, start, attrs = {}) {
213
+ this.recordHistogram(name, Date.now() - start, attrs);
214
+ }
215
+ toAttributes(raw, unsafe) {
216
+ if (!raw)
217
+ return {};
218
+ if (unsafe)
219
+ return raw;
220
+ return sanitizeAttrs(raw, this.config.sanitize);
221
+ }
222
+ }
223
+ function severityFor(level) {
224
+ switch (level) {
225
+ case "debug":
226
+ return SeverityNumber.DEBUG;
227
+ case "info":
228
+ return SeverityNumber.INFO;
229
+ case "warn":
230
+ return SeverityNumber.WARN;
231
+ case "error":
232
+ return SeverityNumber.ERROR;
233
+ default:
234
+ return SeverityNumber.INFO;
235
+ }
236
+ }
237
+ function noopSpan() {
238
+ return trace.getActiveSpan() ?? trace.wrapSpanContext({
239
+ traceId: "00000000000000000000000000000000",
240
+ spanId: "0000000000000000",
241
+ traceFlags: 0
242
+ });
243
+ }
244
+ // src/context-propagation.ts
245
+ import {
246
+ context as context2,
247
+ propagation as propagation2,
248
+ trace as trace2
249
+ } from "@opentelemetry/api";
250
+ var TRACE_CONTEXT_HEADERS = ["traceparent", "tracestate"];
251
+ function injectTraceContext(carrier = {}) {
252
+ propagation2.inject(context2.active(), carrier);
253
+ return carrier;
254
+ }
255
+ function extractTraceContext(carrier) {
256
+ return propagation2.extract(context2.active(), carrier);
257
+ }
258
+ function contextFromHeaders(headers) {
259
+ const carrier = {};
260
+ if (headers instanceof Headers) {
261
+ for (const name of TRACE_CONTEXT_HEADERS) {
262
+ const value = headers.get(name);
263
+ if (value)
264
+ carrier[name] = value;
265
+ }
266
+ } else {
267
+ for (const name of TRACE_CONTEXT_HEADERS) {
268
+ const raw = headers[name];
269
+ const value = Array.isArray(raw) ? raw[0] : raw;
270
+ if (value)
271
+ carrier[name] = value;
272
+ }
273
+ }
274
+ return extractTraceContext(carrier);
275
+ }
276
+ function getActiveSpan() {
277
+ return trace2.getActiveSpan();
278
+ }
279
+ // src/wrap-db-adapter.ts
280
+ function wrapDbAdapter(adapter, telemetry, dbSystem) {
281
+ if (!telemetry || !telemetry.active)
282
+ return adapter;
283
+ const wrapRead = (tx) => ({
284
+ find: async (store, options) => telemetry.startSpan(`db.find ${store}`, async (span) => {
285
+ const start = Date.now();
286
+ try {
287
+ const rows = await tx.find(store, options);
288
+ span.setAttribute("db.response.row_count", rows.length);
289
+ return rows;
290
+ } finally {
291
+ telemetry.measureSince("arc.db.find_ms", start, {
292
+ "db.system": dbSystem,
293
+ "db.collection.name": store
294
+ });
295
+ }
296
+ }, {
297
+ kind: 3,
298
+ attributes: {
299
+ "db.system": dbSystem,
300
+ "db.operation.name": "find",
301
+ "db.collection.name": store
302
+ }
303
+ })
304
+ });
305
+ const wrapReadWrite = (tx) => ({
306
+ ...wrapRead(tx),
307
+ set: async (store, data) => telemetry.startSpan(`db.set ${store}`, () => tx.set(store, data), {
308
+ kind: 3,
309
+ attributes: {
310
+ "db.system": dbSystem,
311
+ "db.operation.name": "set",
312
+ "db.collection.name": store
313
+ }
314
+ }),
315
+ remove: async (store, id) => telemetry.startSpan(`db.remove ${store}`, () => tx.remove(store, id), {
316
+ kind: 3,
317
+ attributes: {
318
+ "db.system": dbSystem,
319
+ "db.operation.name": "remove",
320
+ "db.collection.name": store
321
+ }
322
+ }),
323
+ commit: async () => telemetry.startSpan("db.commit", () => tx.commit(), {
324
+ kind: 3,
325
+ attributes: {
326
+ "db.system": dbSystem,
327
+ "db.operation.name": "commit"
328
+ }
329
+ })
330
+ });
331
+ return new Proxy(adapter, {
332
+ get(target, prop) {
333
+ const orig = target[prop];
334
+ if (prop === "readTransaction") {
335
+ return (...args) => wrapRead(orig.apply(target, args));
336
+ }
337
+ if (prop === "readWriteTransaction") {
338
+ return (...args) => wrapReadWrite(orig.apply(target, args));
339
+ }
340
+ return typeof orig === "function" ? orig.bind(target) : orig;
341
+ }
342
+ });
343
+ }
344
+
345
+ // src/index.ts
346
+ import { SpanKind as SpanKind2, SpanStatusCode as SpanStatusCode2 } from "@opentelemetry/api";
347
+ export {
348
+ wrapDbAdapter,
349
+ sanitizeAttrs,
350
+ redactConnectionString,
351
+ injectTraceContext,
352
+ getActiveSpan,
353
+ extractTraceContext,
354
+ contextFromHeaders,
355
+ TRACE_CONTEXT_HEADERS,
356
+ SpanStatusCode2 as SpanStatusCode,
357
+ SpanKind2 as SpanKind,
358
+ DEFAULT_REDACT_KEY_PATTERN,
359
+ DEFAULT_MAX_STRING_LEN,
360
+ DEFAULT_MAX_JSON_LEN,
361
+ ArcTelemetry
362
+ };
@@ -0,0 +1,8 @@
1
+ import { ArcTelemetry, type TelemetryConfig } from "./telemetry";
2
+ export interface BrowserInitResult {
3
+ telemetry: ArcTelemetry;
4
+ /** Flush pending spans (best-effort) when the page is about to unload. */
5
+ flush: () => Promise<void>;
6
+ }
7
+ export declare function initBrowserTelemetry(config: TelemetryConfig): BrowserInitResult;
8
+ //# sourceMappingURL=init-browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init-browser.d.ts","sourceRoot":"","sources":["../src/init-browser.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,YAAY,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAUjE,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,YAAY,CAAC;IACxB,0EAA0E;IAC1E,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,eAAe,GAAG,iBAAiB,CAgE/E"}