@belvador/core 0.3.0 → 0.4.0

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.
@@ -1,4 +1,4 @@
1
- export declare const DIAGNOSTIC_CODES: readonly ["BLV_DSN_MISSING", "BLV_DSN_INVALID", "BLV_INIT_CONFLICT", "BLV_NOT_INITIALIZED", "BLV_SEND_UNAUTHORIZED", "BLV_SEND_RATE_LIMITED", "BLV_SEND_REJECTED", "BLV_SEND_NETWORK", "BLV_SEND_SERVER", "BLV_QUEUE_FULL", "BLV_EVENT_TOO_LARGE", "BLV_HOOK_MISSING", "BLV_DROPPED_BY_APP", "BLV_MONITOR_INVALID_SLUG", "BLV_MONITOR_INVALID_REASON", "BLV_MONITOR_CONFIG_INVALID", "BLV_CHECK_IN_DROPPED", "BLV_TRACE_TARGET_INVALID"];
1
+ export declare const DIAGNOSTIC_CODES: readonly ["BLV_DSN_MISSING", "BLV_DSN_INVALID", "BLV_INIT_CONFLICT", "BLV_NOT_INITIALIZED", "BLV_SEND_UNAUTHORIZED", "BLV_SEND_RATE_LIMITED", "BLV_SEND_REJECTED", "BLV_SEND_NETWORK", "BLV_SEND_SERVER", "BLV_QUEUE_FULL", "BLV_EVENT_TOO_LARGE", "BLV_HOOK_MISSING", "BLV_DROPPED_BY_APP", "BLV_MONITOR_INVALID_SLUG", "BLV_MONITOR_INVALID_REASON", "BLV_MONITOR_CONFIG_INVALID", "BLV_CHECK_IN_DROPPED", "BLV_TRACE_TARGET_INVALID", "BLV_OTEL_INVALID", "BLV_TRACE_OWNER_OTEL"];
2
2
  export type DiagnosticCode = (typeof DIAGNOSTIC_CODES)[number];
3
3
  /** Public page explaining every code and the verification (one anchor per code). */
4
4
  export declare const VERIFY_DOCS_URL = "https://belvador.dev/en/docs/sdk/verify";
@@ -21,6 +21,8 @@ export const DIAGNOSTIC_CODES = [
21
21
  "BLV_MONITOR_CONFIG_INVALID",
22
22
  "BLV_CHECK_IN_DROPPED",
23
23
  "BLV_TRACE_TARGET_INVALID",
24
+ "BLV_OTEL_INVALID",
25
+ "BLV_TRACE_OWNER_OTEL",
24
26
  ];
25
27
  /** Public page explaining every code and the verification (one anchor per code). */
26
28
  export const VERIFY_DOCS_URL = "https://belvador.dev/en/docs/sdk/verify";
@@ -45,6 +47,8 @@ const MESSAGES = {
45
47
  BLV_MONITOR_CONFIG_INVALID: "a monitor schedule or setting is not valid — the check-in was sent without it",
46
48
  BLV_CHECK_IN_DROPPED: "a check-in was dropped (too many waiting, rate limited or not sent)",
47
49
  BLV_TRACE_TARGET_INVALID: "a tracePropagation target is not a string or RegExp — it was ignored",
50
+ BLV_OTEL_INVALID: "otel.api is not @opentelemetry/api — ignored",
51
+ BLV_TRACE_OWNER_OTEL: "otel is set, so tracePropagation is ignored",
48
52
  };
49
53
  const config = {
50
54
  onDiagnostic: undefined,
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export { SDK_VERSION, ENGINE_VERSION } from "./version.js";
2
2
  export { checkDsn, type BelvadorDsn } from "./dsn.js";
3
3
  export { isAbortError, isClientAbort, isViewTransitionSkip, noteClientAbort } from "./aborts.js";
4
4
  export { INTERVAL_UNITS, MONITOR_CONFIG_LIMITS, MONITOR_CONTEXT, MONITOR_SLUG_RE, RUN_CONTEXT, RUN_REASON_RE, buildCheckIn, captureCheckIn, heartbeat, monitorConfigOf, runIdOf, withMonitor, type CheckInInput, type IntervalUnit, type MonitorConfigInput, type MonitorSchedule, type RunHandle, type WithMonitorOptions, } from "./monitor.js";
5
+ export { OTEL_OPTION, otelApiOf, otelBridgeIntegration, otelTraceContext, type OtelApiLike, type OtelOptions, } from "./otel.js";
5
6
  export { TRACE_OPTION, installTracePropagation, sentryTraceFromTraceparent, traceConfigOf, urlMatches, withIncomingTraceparent, type TraceConfig, type TracePropagationOptions, } from "./trace.js";
6
7
  export { FILTERED, SENSITIVE_KEY_PARTS, isSensitiveKey, scrubBreadcrumb, scrubEvent, scrubParams, scrubText, scrubValue, } from "./privacy.js";
7
8
  export { MAX_QUEUED_CHECK_INS, SEND_LIMITS, SENT_ITEM_TYPES, filterEnvelope, makeBelvadorTransport, sendWithRetry, type FetchLike, type SendPolicy, type SendState, } from "./transport.js";
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ export { SDK_VERSION, ENGINE_VERSION } from "./version.js";
5
5
  export { checkDsn } from "./dsn.js";
6
6
  export { isAbortError, isClientAbort, isViewTransitionSkip, noteClientAbort } from "./aborts.js";
7
7
  export { INTERVAL_UNITS, MONITOR_CONFIG_LIMITS, MONITOR_CONTEXT, MONITOR_SLUG_RE, RUN_CONTEXT, RUN_REASON_RE, buildCheckIn, captureCheckIn, heartbeat, monitorConfigOf, runIdOf, withMonitor, } from "./monitor.js";
8
+ export { OTEL_OPTION, otelApiOf, otelBridgeIntegration, otelTraceContext, } from "./otel.js";
8
9
  export { TRACE_OPTION, installTracePropagation, sentryTraceFromTraceparent, traceConfigOf, urlMatches, withIncomingTraceparent, } from "./trace.js";
9
10
  export { FILTERED, SENSITIVE_KEY_PARTS, isSensitiveKey, scrubBreadcrumb, scrubEvent, scrubParams, scrubText, scrubValue, } from "./privacy.js";
10
11
  export { MAX_QUEUED_CHECK_INS, SEND_LIMITS, SENT_ITEM_TYPES, filterEnvelope, makeBelvadorTransport, sendWithRetry, } from "./transport.js";
package/dist/options.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Breadcrumb, BreadcrumbHint, ErrorEvent, EventHint } from "@sentry/core";
2
2
  import { type Diagnostic } from "./diagnostics.js";
3
+ import { type OtelOptions } from "./otel.js";
3
4
  import { type TracePropagationOptions } from "./trace.js";
4
5
  import { type SendPolicy } from "./transport.js";
5
6
  export type BelvadorEvent = ErrorEvent;
@@ -39,6 +40,12 @@ export type BelvadorOptions = {
39
40
  * (the receiving API must allow them in CORS). No spans or transactions are sent.
40
41
  */
41
42
  tracePropagation?: TracePropagationOptions;
43
+ /**
44
+ * The app already runs OpenTelemetry: pass `{ api }` from `@opentelemetry/api` and errors
45
+ * captured inside an active span carry that span's trace and span ids. OpenTelemetry then
46
+ * owns propagation, so `tracePropagation` is ignored. Belvador creates no spans.
47
+ */
48
+ otel?: OtelOptions;
42
49
  /** Send attachments added with scope.addAttachment (each ≤ 5 MB). Default false. */
43
50
  attachments?: boolean;
44
51
  /** Runs before Belvador's own scrubbing; return null to drop the event. */
@@ -86,6 +93,7 @@ export declare function buildEngineOptions(opts: BelvadorOptions, runtime: Runti
86
93
  transport: (options: import("@sentry/core").BaseTransportOptions) => import("@sentry/core").Transport;
87
94
  _belvador: boolean;
88
95
  _belvadorTrace: import("./trace.js").TraceConfig | null;
96
+ _belvadorOtel: import("./otel.js").OtelApiLike | null;
89
97
  _metadata: {
90
98
  sdk: {
91
99
  name: string;
package/dist/options.js CHANGED
@@ -3,6 +3,7 @@ import { configureDiagnostics, reportDiagnostic } from "./diagnostics.js";
3
3
  import { checkDsn } from "./dsn.js";
4
4
  import { scrubBreadcrumb, scrubEvent } from "./privacy.js";
5
5
  import { VERIFICATION_CONTEXT, VERIFICATION_TAG, noteVerificationOutcome, verificationOf, } from "./verification.js";
6
+ import { OTEL_OPTION, otelApiOf } from "./otel.js";
6
7
  import { TRACE_OPTION, traceConfigOf } from "./trace.js";
7
8
  import { makeBelvadorTransport } from "./transport.js";
8
9
  import { ENGINE_VERSION, SDK_VERSION } from "./version.js";
@@ -116,6 +117,10 @@ export function buildEngineOptions(opts, runtime, send) {
116
117
  return null;
117
118
  }
118
119
  const attachments = opts.attachments === true;
120
+ const otel = otelApiOf(opts.otel);
121
+ // One owner of propagation: with OpenTelemetry present, Belvador adds no headers of its own.
122
+ if (otel && opts.tracePropagation)
123
+ reportDiagnostic("BLV_TRACE_OWNER_OTEL");
119
124
  return {
120
125
  dsn: opts.dsn,
121
126
  release: typeof opts.release === "string" && opts.release ? opts.release : undefined,
@@ -140,7 +145,9 @@ export function buildEngineOptions(opts, runtime, send) {
140
145
  transport: makeBelvadorTransport({ ...send, attachments, debug: opts.debug === true }),
141
146
  [BELVADOR_MARKER]: true,
142
147
  // Checked trace propagation of this client (null = off); the engine itself never adds headers.
143
- [TRACE_OPTION]: traceConfigOf(opts.tracePropagation),
148
+ [TRACE_OPTION]: otel ? null : traceConfigOf(opts.tracePropagation),
149
+ // The checked `@opentelemetry/api` of this client (null = no bridge); runtimes add the integration.
150
+ [OTEL_OPTION]: otel,
144
151
  _metadata: {
145
152
  sdk: {
146
153
  name: sdkName,
package/dist/otel.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import type { Integration } from "@sentry/core";
2
+ /** The part of `@opentelemetry/api` the bridge reads (structural, any 1.x version fits). */
3
+ export type OtelApiLike = {
4
+ trace: {
5
+ getActiveSpan(): {
6
+ spanContext(): {
7
+ traceId: string;
8
+ spanId: string;
9
+ };
10
+ } | undefined;
11
+ };
12
+ };
13
+ export type OtelOptions = {
14
+ /** `import * as api from "@opentelemetry/api"` — the same copy your OpenTelemetry SDK uses. */
15
+ api: OtelApiLike;
16
+ };
17
+ /** Engine-options key carrying the checked API of one client (null = bridge off). */
18
+ export declare const OTEL_OPTION = "_belvadorOtel";
19
+ /** The API when it looks like `@opentelemetry/api`, else null (a wrong value is reported). */
20
+ export declare function otelApiOf(opts: OtelOptions | undefined): OtelApiLike | null;
21
+ /** Ids of the active OpenTelemetry span, or null (none, invalid, all-zero, or the API threw). */
22
+ export declare function otelTraceContext(api: OtelApiLike): {
23
+ trace_id: string;
24
+ span_id: string;
25
+ } | null;
26
+ /**
27
+ * Stamps `contexts.trace` from the active span when the error is captured. `preprocessEvent`
28
+ * runs synchronously inside the capture call, while the app's span is still current; the
29
+ * engine keeps an event's own trace context over the scope's.
30
+ */
31
+ export declare function otelBridgeIntegration(api: OtelApiLike): Integration;
package/dist/otel.js ADDED
@@ -0,0 +1,44 @@
1
+ import { reportDiagnostic } from "./diagnostics.js";
2
+ /** Engine-options key carrying the checked API of one client (null = bridge off). */
3
+ export const OTEL_OPTION = "_belvadorOtel";
4
+ /** The API when it looks like `@opentelemetry/api`, else null (a wrong value is reported). */
5
+ export function otelApiOf(opts) {
6
+ if (!opts)
7
+ return null;
8
+ const api = opts.api;
9
+ if (typeof api?.trace?.getActiveSpan === "function")
10
+ return api;
11
+ reportDiagnostic("BLV_OTEL_INVALID");
12
+ return null;
13
+ }
14
+ // 32 + 16 lowercase hex, neither all zeros (W3C Trace Context invalid ids).
15
+ const IDS = /^(?!0{32})[0-9a-f]{32}-(?!0{16})[0-9a-f]{16}$/;
16
+ /** Ids of the active OpenTelemetry span, or null (none, invalid, all-zero, or the API threw). */
17
+ export function otelTraceContext(api) {
18
+ try {
19
+ const c = api.trace.getActiveSpan()?.spanContext();
20
+ const ids = c ? `${c.traceId}-${c.spanId}`.toLowerCase() : "";
21
+ return IDS.test(ids) ? { trace_id: ids.slice(0, 32), span_id: ids.slice(33) } : null;
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ /**
28
+ * Stamps `contexts.trace` from the active span when the error is captured. `preprocessEvent`
29
+ * runs synchronously inside the capture call, while the app's span is still current; the
30
+ * engine keeps an event's own trace context over the scope's.
31
+ */
32
+ export function otelBridgeIntegration(api) {
33
+ return {
34
+ name: "BelvadorOtelBridge",
35
+ preprocessEvent(event) {
36
+ const ids = !event.type && otelTraceContext(api); // errors only
37
+ if (!ids)
38
+ return;
39
+ const trace = { ...event.contexts?.trace, ...ids };
40
+ delete trace.parent_span_id; // the scope's parent is not this span's parent
41
+ event.contexts = { ...event.contexts, trace };
42
+ },
43
+ };
44
+ }
package/dist/privacy.js CHANGED
@@ -368,6 +368,25 @@ export function scrubBreadcrumb(crumb) {
368
368
  return null;
369
369
  }
370
370
  }
371
+ // W3C trace/span ids are random hex, not personal data; an all-digit span id can look like a
372
+ // card number, so exact-format ids under `contexts.trace` survive (parity with the node).
373
+ const HEX = /^[0-9a-f]+$/;
374
+ function scrubContexts(contexts) {
375
+ const out = scrubValue(contexts, 0);
376
+ try {
377
+ const t = contexts.trace;
378
+ if (t && out?.trace && typeof out.trace === "object")
379
+ for (const k of ["trace_id", "span_id", "parent_span_id"]) {
380
+ const id = t[k];
381
+ if (typeof id === "string" && id.length === (k === "trace_id" ? 32 : 16) && HEX.test(id))
382
+ out.trace[k] = id;
383
+ }
384
+ }
385
+ catch {
386
+ /* a throwing getter: keep the scrubbed copy */
387
+ }
388
+ return out;
389
+ }
371
390
  /**
372
391
  * Whole-event scrub, run after the app's own `beforeSend` and before any byte
373
392
  * leaves the app. Returns a new event, or null (drop) when scrubbing fails —
@@ -390,6 +409,8 @@ export function scrubEvent(event) {
390
409
  next = scrubUser(v);
391
410
  else if (key === "exception")
392
411
  next = scrubException(v);
412
+ else if (key === "contexts")
413
+ next = scrubContexts(v);
393
414
  else if (key === "breadcrumbs")
394
415
  next = Array.isArray(v)
395
416
  ? v.flatMap((b) => {
package/dist/version.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export declare const SDK_VERSION = "0.3.0";
1
+ export declare const SDK_VERSION = "0.4.0";
2
2
  /** The audited engine version every package pins exactly (research/05-decisions.md D3). */
3
3
  export declare const ENGINE_VERSION = "10.76.2";
package/dist/version.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // One version for every @belvador/* package (lockstep). Changed only by
2
2
  // `bun scripts/set-version.ts`; tests compare it with every package.json and
3
3
  // with src/content/sdk-matrix.ts in the web app.
4
- export const SDK_VERSION = "0.3.0";
4
+ export const SDK_VERSION = "0.4.0";
5
5
  /** The audited engine version every package pins exactly (research/05-decisions.md D3). */
6
6
  export const ENGINE_VERSION = "10.76.2";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@belvador/core",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Internal building blocks of the Belvador SDK (privacy, sending policy). Install @belvador/browser, @belvador/react, @belvador/cloudflare or @belvador/tanstack-start instead.",
5
5
  "license": "MIT",
6
6
  "author": "Belvador (https://belvador.dev)",