@heystack/otel 0.11.1 → 0.12.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.
package/README.md CHANGED
@@ -400,6 +400,8 @@ await instrumentWeb({
400
400
 
401
401
  Otherwise it's **cost-aware and safe by design**: off unless you opt in; head-sampled (an unsampled request still propagates `traceparent` with the sampled flag cleared, so the backend makes the same keep/drop decision — no orphaned server spans); and the exporter posts through the *original* `fetch`, never tracing its own upload (no self-export loop). Spans post to `/v1/traces` cross-origin with no CORS setup on your side.
402
402
 
403
+ **Remote kill-switch (safety valve).** Browser tracing can also be disabled **from the server**, without an app redeploy — a way to stop it fleet-wide if it ever misbehaves in the wild. It is **fail-open**: if the server config is unreachable or omits the flag, tracing stays on (absent flag = enabled), so the switch can only ever *disable*, never accidentally turn a working app off.
404
+
403
405
  ### Browser error & console collection
404
406
 
405
407
  `instrumentWeb` captures **uncaught browser errors** out of the box — no extra setup. Any `window.onerror` / unhandled promise rejection becomes an OTLP **log** (`event.name=browser.error`, `ERROR` severity) carrying the OTel `exception.type` / `exception.message` / `exception.stacktrace` semconv attributes, the page `url.full`, the `session_id` (so it correlates to the session replay), and — when `tracing` is on — the active `trace_id` / `span_id`. `console.error` is captured too by default.
@@ -417,6 +419,8 @@ await instrumentWeb({
417
419
 
418
420
  It's the **same cost-safe design** as tracing/replay: logs POST to `/v1/logs` through the *original* `fetch` (captured before any patching) so the exporter never traces its own upload — no self-export loop. Console capture is **recursion-guarded** (anything logged on the export path is never re-captured) and **rate-capped** (max ~60 records/minute; the overflow is dropped and counted, with one summary log emitted when the cap lifts) so a runaway `console.error` in a render loop can't flood ingest. Errors show up in the console **Logs** tab and correlate to their session replay and trace.
419
421
 
422
+ Like tracing, error / console capture has a **remote kill-switch**: it can be disabled **from the server** without an app redeploy, and is **fail-open** (if the server config is unreachable or omits the flag, capture stays on — absent flag = enabled).
423
+
420
424
  ### In-app bug reports (`reportBug`)
421
425
 
422
426
  Let users report a bug from inside your app. `reportBug` is a headless API — **no widget, you own the UX** — that files a structured report and auto-attaches the context a triager needs: the current URL, user agent, replay `session_id`, active `trace_id`, `release` / `build`, and the **last few captured browser errors** (so you see what was going wrong on the page right before the report). The report also appears on the Logs timeline (`event.name=user.bug_report`).
package/dist/web.d.ts CHANGED
@@ -17,6 +17,19 @@ export interface RecorderConfig {
17
17
  enabled: boolean;
18
18
  sample_rate: number;
19
19
  masking_mode: "strict" | "relaxed";
20
+ /**
21
+ * Server-side kill-switch for browser distributed tracing. Absent/`true` = enabled
22
+ * (**fail-open**); set `false` server-side to remotely disable browser tracing
23
+ * WITHOUT an app redeploy — the safety valve for the 2026-07-03 cross-origin
24
+ * `traceparent` outage. See `tracingActive`.
25
+ */
26
+ tracing_enabled?: boolean;
27
+ /**
28
+ * Server-side kill-switch for browser error / console capture. Absent/`true` =
29
+ * enabled (**fail-open**); set `false` server-side to remotely disable it without
30
+ * an app redeploy. See `errorCaptureActive`.
31
+ */
32
+ errors_enabled?: boolean;
20
33
  }
21
34
  /** Pure: decide once per session whether to record. rng defaults to Math.random. */
22
35
  export declare function shouldRecord(cfg: RecorderConfig, rng?: () => number): boolean;
@@ -116,6 +129,21 @@ export interface InstrumentWebOptions {
116
129
  */
117
130
  captureConsole?: "error" | "warn" | false;
118
131
  }
132
+ /**
133
+ * Pure: is browser distributed tracing active for this run? Requires BOTH the app
134
+ * to have opted in (`opts.tracing`) AND the server-side kill-switch to not be off
135
+ * (`cfg.tracing_enabled`). **Fail-open**: an absent/`true` flag counts as enabled, so
136
+ * an old or unreachable `/v1/replay/config` never silently disables tracing. Split out
137
+ * as a tiny exported predicate so the gating decision is unit-testable without booting
138
+ * rrweb (which `instrumentWeb` imports). See `RecorderConfig.tracing_enabled`.
139
+ */
140
+ export declare function tracingActive(opts: Pick<InstrumentWebOptions, "tracing">, cfg: Pick<RecorderConfig, "tracing_enabled">): boolean;
141
+ /**
142
+ * Pure: is browser error / console capture active for this run? On by default
143
+ * (`opts.errors !== false`) AND gated by the server-side kill-switch
144
+ * (`cfg.errors_enabled`). **Fail-open**: an absent/`true` flag counts as enabled.
145
+ */
146
+ export declare function errorCaptureActive(opts: Pick<InstrumentWebOptions, "errors">, cfg: Pick<RecorderConfig, "errors_enabled">): boolean;
119
147
  /** Entry point: fetch config, decide sampling, start rrweb, stream chunks.
120
148
  * Returns a stop() function. Safe to call in any browser; no-ops on the server. */
121
149
  export declare function instrumentWeb(opts: InstrumentWebOptions): Promise<() => void>;
package/dist/web.js CHANGED
@@ -81,6 +81,25 @@ export class ReplayTransport {
81
81
  }
82
82
  const DEFAULT_FLUSH_MS = 5000;
83
83
  const DEFAULT_FLUSH_EVENTS = 200;
84
+ /**
85
+ * Pure: is browser distributed tracing active for this run? Requires BOTH the app
86
+ * to have opted in (`opts.tracing`) AND the server-side kill-switch to not be off
87
+ * (`cfg.tracing_enabled`). **Fail-open**: an absent/`true` flag counts as enabled, so
88
+ * an old or unreachable `/v1/replay/config` never silently disables tracing. Split out
89
+ * as a tiny exported predicate so the gating decision is unit-testable without booting
90
+ * rrweb (which `instrumentWeb` imports). See `RecorderConfig.tracing_enabled`.
91
+ */
92
+ export function tracingActive(opts, cfg) {
93
+ return !!opts.tracing && cfg.tracing_enabled !== false;
94
+ }
95
+ /**
96
+ * Pure: is browser error / console capture active for this run? On by default
97
+ * (`opts.errors !== false`) AND gated by the server-side kill-switch
98
+ * (`cfg.errors_enabled`). **Fail-open**: an absent/`true` flag counts as enabled.
99
+ */
100
+ export function errorCaptureActive(opts, cfg) {
101
+ return opts.errors !== false && cfg.errors_enabled !== false;
102
+ }
84
103
  /** Entry point: fetch config, decide sampling, start rrweb, stream chunks.
85
104
  * Returns a stop() function. Safe to call in any browser; no-ops on the server. */
86
105
  export async function instrumentWeb(opts) {
@@ -113,7 +132,9 @@ export async function instrumentWeb(opts) {
113
132
  // a real CLIENT span per outbound fetch + propagates W3C context to the backend.
114
133
  let stopTracing = () => { };
115
134
  let fetchPatched = false;
116
- if (opts.tracing) {
135
+ // Gated by the server kill-switch (cfg.tracing_enabled) as well as the app opt-in —
136
+ // fail-open, so an unreachable/old config can't disable tracing. See tracingActive.
137
+ if (tracingActive(opts, cfg)) {
117
138
  const exporter = new BrowserTraceExporter({
118
139
  endpoint, apiKey: opts.apiKey, service: opts.service, fetchImpl: originalFetch,
119
140
  resourceAttributes: releaseAttrs,
@@ -157,7 +178,9 @@ export async function instrumentWeb(opts) {
157
178
  // 2b. Browser error / console log collection — ON by default, INDEPENDENT of both
158
179
  // tracing and replay sampling. Exports OTLP logs to /v1/logs via the original fetch.
159
180
  let stopErrors = () => { };
160
- if (opts.errors !== false) {
181
+ // Gated by the server kill-switch (cfg.errors_enabled) as well as the opts.errors
182
+ // default — fail-open. See errorCaptureActive.
183
+ if (errorCaptureActive(opts, cfg)) {
161
184
  const unpatchErrors = startErrorCapture({
162
185
  exporter: logsExporter,
163
186
  sessionId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heystack/otel",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "Runtime-aware OpenTelemetry tracing that exports to Heystack (Node, Next.js, Workers).",
5
5
  "license": "MIT",
6
6
  "type": "module",