@interfere/react 8.1.2 → 8.1.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.
@@ -1 +1 @@
1
- {"version":3,"file":"error-boundary.d.mts","names":[],"sources":["../src/error-boundary.tsx"],"mappings":";;;UAYiB,kBAAA;EACf,QAAA,EAAU,SAAA;EACV,QAAA,GAAW,SAAA,KAAc,KAAA,EAAO,KAAA,EAAO,KAAA,iBAAsB,SAAA;EAC7D,OAAA,IAAW,KAAA,EAAO,KAAA,EAAO,IAAA,EAAM,SAAA;AAAA;AAAA,UAGvB,kBAAA;EACR,KAAA,EAAO,KAAA;AAAA;;;;;;;;cAUI,aAAA,SAAsB,SAAA,CACjC,kBAAA,EACA,kBAAA;EAES,KAAA,EAAO,kBAAA;EAAA,OAET,wBAAA,CAAyB,KAAA,EAAO,KAAA,GAAQ,kBAAA;EAItC,iBAAA,CAAkB,KAAA,EAAO,KAAA,EAAO,IAAA,EAAM,SAAA;EAAA,iBAqB9B,KAAA;EAIR,MAAA,CAAA,GAAM,SAAA;AAAA"}
1
+ {"version":3,"file":"error-boundary.d.mts","names":[],"sources":["../src/error-boundary.tsx"],"mappings":";;;UAQiB,kBAAA;EACf,QAAA,EAAU,SAAA;EACV,QAAA,GAAW,SAAA,KAAc,KAAA,EAAO,KAAA,EAAO,KAAA,iBAAsB,SAAA;EAC7D,OAAA,IAAW,KAAA,EAAO,KAAA,EAAO,IAAA,EAAM,SAAA;AAAA;AAAA,UAGvB,kBAAA;EACR,KAAA,EAAO,KAAA;AAAA;;;;;;;;cAUI,aAAA,SAAsB,SAAA,CACjC,kBAAA,EACA,kBAAA;EAES,KAAA,EAAO,kBAAA;EAAA,OAET,wBAAA,CAAyB,KAAA,EAAO,KAAA,GAAQ,kBAAA;EAItC,iBAAA,CAAkB,KAAA,EAAO,KAAA,EAAO,IAAA,EAAM,SAAA;EAAA,iBAS9B,KAAA;EAIR,MAAA,CAAA,GAAM,SAAA;AAAA"}
@@ -1,7 +1,6 @@
1
1
  "use client";
2
- import { seen } from "./internal/errors.mjs";
3
- import { getClient } from "./internal/client.mjs";
4
- import { shouldDropBrowserExtensionNoise, toExceptions } from "@interfere/types/sdk/errors";
2
+ import { captureReactError } from "./internal/capture.mjs";
3
+ import { MECHANISM_TYPE } from "@interfere/types/sdk/errors";
5
4
  import { Component } from "react";
6
5
  //#region src/error-boundary.tsx
7
6
  /**
@@ -17,15 +16,10 @@ var ErrorBoundary = class extends Component {
17
16
  return { error };
18
17
  }
19
18
  componentDidCatch(error, info) {
20
- if (seen.has(error)) return;
21
- seen.add(error);
22
- try {
23
- const exceptions = toExceptions(error, {
24
- type: "react",
25
- handled: true
26
- });
27
- if (!shouldDropBrowserExtensionNoise(exceptions)) getClient().capture("error", { exceptions });
28
- } catch {}
19
+ captureReactError(error, info.componentStack, {
20
+ type: MECHANISM_TYPE.react.errorBoundary,
21
+ handled: true
22
+ });
29
23
  this.props.onError?.(error, info);
30
24
  }
31
25
  reset = () => {
@@ -1 +1 @@
1
- {"version":3,"file":"error-boundary.mjs","names":[],"sources":["../src/error-boundary.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n shouldDropBrowserExtensionNoise,\n toExceptions,\n} from \"@interfere/types/sdk/errors\";\n\nimport { Component, type ErrorInfo, type ReactNode } from \"react\";\n\nimport { getClient } from \"./internal/client.js\";\nimport { seen } from \"./internal/errors.js\";\n\nexport interface ErrorBoundaryProps {\n children: ReactNode;\n fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);\n onError?: (error: Error, info: ErrorInfo) => void;\n}\n\ninterface ErrorBoundaryState {\n error: Error | null;\n}\n\n/**\n * Catches render-phase React errors, reports them to the SDK, and renders a\n * fallback. Requires the SDK to be bootstrapped via `init()` before React\n * renders so `capture` has an active runtime.\n *\n * Class component required — React has no hook-based error boundary API.\n */\nexport class ErrorBoundary extends Component<\n ErrorBoundaryProps,\n ErrorBoundaryState\n> {\n override state: ErrorBoundaryState = { error: null };\n\n static getDerivedStateFromError(error: Error): ErrorBoundaryState {\n return { error };\n }\n\n override componentDidCatch(error: Error, info: ErrorInfo) {\n if (seen.has(error)) {\n return;\n }\n seen.add(error);\n\n try {\n const exceptions = toExceptions(error, {\n type: \"react\",\n handled: true,\n });\n if (!shouldDropBrowserExtensionNoise(exceptions)) {\n getClient().capture(\"error\", { exceptions });\n }\n } catch {\n // SDK not initialized — error boundary still renders fallback\n }\n\n this.props.onError?.(error, info);\n }\n\n private readonly reset = () => {\n this.setState({ error: null });\n };\n\n override render() {\n const { error } = this.state;\n\n if (error) {\n const { fallback } = this.props;\n if (typeof fallback === \"function\") {\n return fallback(error, this.reset);\n }\n return fallback ?? null;\n }\n\n return this.props.children;\n }\n}\n"],"mappings":";;;;;;;;;;;;;AA6BA,IAAa,gBAAb,cAAmC,UAGjC;CACA,QAAqC,EAAE,OAAO,MAAM;CAEpD,OAAO,yBAAyB,OAAkC;AAChE,SAAO,EAAE,OAAO;;CAGlB,kBAA2B,OAAc,MAAiB;AACxD,MAAI,KAAK,IAAI,MAAM,CACjB;AAEF,OAAK,IAAI,MAAM;AAEf,MAAI;GACF,MAAM,aAAa,aAAa,OAAO;IACrC,MAAM;IACN,SAAS;IACV,CAAC;AACF,OAAI,CAAC,gCAAgC,WAAW,CAC9C,YAAW,CAAC,QAAQ,SAAS,EAAE,YAAY,CAAC;UAExC;AAIR,OAAK,MAAM,UAAU,OAAO,KAAK;;CAGnC,cAA+B;AAC7B,OAAK,SAAS,EAAE,OAAO,MAAM,CAAC;;CAGhC,SAAkB;EAChB,MAAM,EAAE,UAAU,KAAK;AAEvB,MAAI,OAAO;GACT,MAAM,EAAE,aAAa,KAAK;AAC1B,OAAI,OAAO,aAAa,WACtB,QAAO,SAAS,OAAO,KAAK,MAAM;AAEpC,UAAO,YAAY;;AAGrB,SAAO,KAAK,MAAM"}
1
+ {"version":3,"file":"error-boundary.mjs","names":[],"sources":["../src/error-boundary.tsx"],"sourcesContent":["\"use client\";\n\nimport { MECHANISM_TYPE } from \"@interfere/types/sdk/errors\";\n\nimport { Component, type ErrorInfo, type ReactNode } from \"react\";\n\nimport { captureReactError } from \"./internal/capture.js\";\n\nexport interface ErrorBoundaryProps {\n children: ReactNode;\n fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);\n onError?: (error: Error, info: ErrorInfo) => void;\n}\n\ninterface ErrorBoundaryState {\n error: Error | null;\n}\n\n/**\n * Catches render-phase React errors, reports them to the SDK, and renders a\n * fallback. Requires the SDK to be bootstrapped via `init()` before React\n * renders so `capture` has an active runtime.\n *\n * Class component required — React has no hook-based error boundary API.\n */\nexport class ErrorBoundary extends Component<\n ErrorBoundaryProps,\n ErrorBoundaryState\n> {\n override state: ErrorBoundaryState = { error: null };\n\n static getDerivedStateFromError(error: Error): ErrorBoundaryState {\n return { error };\n }\n\n override componentDidCatch(error: Error, info: ErrorInfo) {\n captureReactError(error, info.componentStack, {\n type: MECHANISM_TYPE.react.errorBoundary,\n handled: true,\n });\n\n this.props.onError?.(error, info);\n }\n\n private readonly reset = () => {\n this.setState({ error: null });\n };\n\n override render() {\n const { error } = this.state;\n\n if (error) {\n const { fallback } = this.props;\n if (typeof fallback === \"function\") {\n return fallback(error, this.reset);\n }\n return fallback ?? null;\n }\n\n return this.props.children;\n }\n}\n"],"mappings":";;;;;;;;;;;;AAyBA,IAAa,gBAAb,cAAmC,UAGjC;CACA,QAAqC,EAAE,OAAO,MAAM;CAEpD,OAAO,yBAAyB,OAAkC;AAChE,SAAO,EAAE,OAAO;;CAGlB,kBAA2B,OAAc,MAAiB;AACxD,oBAAkB,OAAO,KAAK,gBAAgB;GAC5C,MAAM,eAAe,MAAM;GAC3B,SAAS;GACV,CAAC;AAEF,OAAK,MAAM,UAAU,OAAO,KAAK;;CAGnC,cAA+B;AAC7B,OAAK,SAAS,EAAE,OAAO,MAAM,CAAC;;CAGhC,SAAkB;EAChB,MAAM,EAAE,UAAU,KAAK;AAEvB,MAAI,OAAO;GACT,MAAM,EAAE,aAAa,KAAK;AAC1B,OAAI,OAAO,aAAa,WACtB,QAAO,SAAS,OAAO,KAAK,MAAM;AAEpC,UAAO,YAAY;;AAGrB,SAAO,KAAK,MAAM"}
@@ -0,0 +1,39 @@
1
+ import { Component, ReactNode } from "react";
2
+
3
+ //#region src/internal/capture-boundary.d.ts
4
+ interface CaptureBoundaryProps {
5
+ children: ReactNode;
6
+ }
7
+ interface CaptureBoundaryState {
8
+ error: Error | null;
9
+ }
10
+ /**
11
+ * Internal boundary used by `<InterfereProvider>` to capture render-phase
12
+ * React errors without changing the app's UX.
13
+ *
14
+ * Unlike the public `ErrorBoundary`, this boundary always re-throws the
15
+ * captured error in its `render()` so upstream boundaries — the customer's
16
+ * own `ErrorBoundary`, Next.js's `error.tsx` / `global-error.tsx`, or React's
17
+ * default unmount — keep control of what the user sees. The net effect:
18
+ * zero visual change, full capture coverage for any render-phase error in
19
+ * the subtree.
20
+ *
21
+ * Capture happens inside `getDerivedStateFromError` rather than
22
+ * `componentDidCatch` because `componentDidCatch` does not fire on a boundary
23
+ * that re-throws in render — React considers such a boundary to have failed
24
+ * and skips its commit-phase lifecycle. The trade-off: we don't get
25
+ * `errorInfo.componentStack` in this capture. Callers who want the component
26
+ * tree should use the public `ErrorBoundary` (which renders a fallback and
27
+ * therefore receives `componentDidCatch`), or pass
28
+ * {@link reactErrorHandler} to `createRoot()`.
29
+ *
30
+ * `mechanism.handled` is `false` because from Interfere's perspective we
31
+ * captured the error but did not render a fallback — propagation continues.
32
+ */
33
+ declare class CaptureBoundary extends Component<CaptureBoundaryProps, CaptureBoundaryState> {
34
+ state: CaptureBoundaryState;
35
+ static getDerivedStateFromError(error: Error): CaptureBoundaryState;
36
+ render(): ReactNode;
37
+ }
38
+ //#endregion
39
+ export { CaptureBoundary };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture-boundary.d.mts","names":[],"sources":["../../src/internal/capture-boundary.tsx"],"mappings":";;;UAQU,oBAAA;EACR,QAAA,EAAU,SAAA;AAAA;AAAA,UAGF,oBAAA;EACR,KAAA,EAAO,KAAA;AAAA;;AAJY;;;;;AA8BrB;;;;;;;;;;;;;;;;;cAAa,eAAA,SAAwB,SAAA,CACnC,oBAAA,EACA,oBAAA;EAES,KAAA,EAAO,oBAAA;EAAA,OAET,wBAAA,CAAyB,KAAA,EAAO,KAAA,GAAQ,oBAAA;EAQtC,MAAA,CAAA,GAAU,SAAA;AAAA"}
@@ -0,0 +1,44 @@
1
+ "use client";
2
+ import { captureReactError } from "./capture.mjs";
3
+ import { MECHANISM_TYPE } from "@interfere/types/sdk/errors";
4
+ import { Component } from "react";
5
+ //#region src/internal/capture-boundary.tsx
6
+ /**
7
+ * Internal boundary used by `<InterfereProvider>` to capture render-phase
8
+ * React errors without changing the app's UX.
9
+ *
10
+ * Unlike the public `ErrorBoundary`, this boundary always re-throws the
11
+ * captured error in its `render()` so upstream boundaries — the customer's
12
+ * own `ErrorBoundary`, Next.js's `error.tsx` / `global-error.tsx`, or React's
13
+ * default unmount — keep control of what the user sees. The net effect:
14
+ * zero visual change, full capture coverage for any render-phase error in
15
+ * the subtree.
16
+ *
17
+ * Capture happens inside `getDerivedStateFromError` rather than
18
+ * `componentDidCatch` because `componentDidCatch` does not fire on a boundary
19
+ * that re-throws in render — React considers such a boundary to have failed
20
+ * and skips its commit-phase lifecycle. The trade-off: we don't get
21
+ * `errorInfo.componentStack` in this capture. Callers who want the component
22
+ * tree should use the public `ErrorBoundary` (which renders a fallback and
23
+ * therefore receives `componentDidCatch`), or pass
24
+ * {@link reactErrorHandler} to `createRoot()`.
25
+ *
26
+ * `mechanism.handled` is `false` because from Interfere's perspective we
27
+ * captured the error but did not render a fallback — propagation continues.
28
+ */
29
+ var CaptureBoundary = class extends Component {
30
+ state = { error: null };
31
+ static getDerivedStateFromError(error) {
32
+ captureReactError(error, null, {
33
+ type: MECHANISM_TYPE.react.captureBoundary,
34
+ handled: false
35
+ });
36
+ return { error };
37
+ }
38
+ render() {
39
+ if (this.state.error) throw this.state.error;
40
+ return this.props.children;
41
+ }
42
+ };
43
+ //#endregion
44
+ export { CaptureBoundary };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture-boundary.mjs","names":[],"sources":["../../src/internal/capture-boundary.tsx"],"sourcesContent":["\"use client\";\n\nimport { MECHANISM_TYPE } from \"@interfere/types/sdk/errors\";\n\nimport { Component, type ReactNode } from \"react\";\n\nimport { captureReactError } from \"./capture.js\";\n\ninterface CaptureBoundaryProps {\n children: ReactNode;\n}\n\ninterface CaptureBoundaryState {\n error: Error | null;\n}\n\n/**\n * Internal boundary used by `<InterfereProvider>` to capture render-phase\n * React errors without changing the app's UX.\n *\n * Unlike the public `ErrorBoundary`, this boundary always re-throws the\n * captured error in its `render()` so upstream boundaries — the customer's\n * own `ErrorBoundary`, Next.js's `error.tsx` / `global-error.tsx`, or React's\n * default unmount — keep control of what the user sees. The net effect:\n * zero visual change, full capture coverage for any render-phase error in\n * the subtree.\n *\n * Capture happens inside `getDerivedStateFromError` rather than\n * `componentDidCatch` because `componentDidCatch` does not fire on a boundary\n * that re-throws in render — React considers such a boundary to have failed\n * and skips its commit-phase lifecycle. The trade-off: we don't get\n * `errorInfo.componentStack` in this capture. Callers who want the component\n * tree should use the public `ErrorBoundary` (which renders a fallback and\n * therefore receives `componentDidCatch`), or pass\n * {@link reactErrorHandler} to `createRoot()`.\n *\n * `mechanism.handled` is `false` because from Interfere's perspective we\n * captured the error but did not render a fallback — propagation continues.\n */\nexport class CaptureBoundary extends Component<\n CaptureBoundaryProps,\n CaptureBoundaryState\n> {\n override state: CaptureBoundaryState = { error: null };\n\n static getDerivedStateFromError(error: Error): CaptureBoundaryState {\n captureReactError(error, null, {\n type: MECHANISM_TYPE.react.captureBoundary,\n handled: false,\n });\n return { error };\n }\n\n override render(): ReactNode {\n if (this.state.error) {\n throw this.state.error;\n }\n return this.props.children;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,IAAa,kBAAb,cAAqC,UAGnC;CACA,QAAuC,EAAE,OAAO,MAAM;CAEtD,OAAO,yBAAyB,OAAoC;AAClE,oBAAkB,OAAO,MAAM;GAC7B,MAAM,eAAe,MAAM;GAC3B,SAAS;GACV,CAAC;AACF,SAAO,EAAE,OAAO;;CAGlB,SAA6B;AAC3B,MAAI,KAAK,MAAM,MACb,OAAM,KAAK,MAAM;AAEnB,SAAO,KAAK,MAAM"}
@@ -0,0 +1,13 @@
1
+ import { ErrorMechanism } from "@interfere/types/sdk/plugins/payload/errors";
2
+
3
+ //#region src/internal/capture.d.ts
4
+ /**
5
+ * Captures a React error through the SDK, attaching the component stack
6
+ * reported by React as additional frames. Dedupes on the Error instance.
7
+ *
8
+ * Swallows errors from `getClient()` so callers can run before the SDK is
9
+ * initialized (e.g. an error boundary catching a pre-init crash).
10
+ */
11
+ declare function captureReactError(error: Error, componentStack: string | null | undefined, mechanism: ErrorMechanism): void;
12
+ //#endregion
13
+ export { captureReactError };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture.d.mts","names":[],"sources":["../../src/internal/capture.ts"],"mappings":";;;;;AAiBA;;;;;iBAAgB,iBAAA,CACd,KAAA,EAAO,KAAA,EACP,cAAA,6BACA,SAAA,EAAW,cAAA"}
@@ -0,0 +1,23 @@
1
+ import { seen } from "./errors.mjs";
2
+ import { getClient } from "./client.mjs";
3
+ import { parseReactComponentStack, shouldDropBrowserExtensionNoise, toExceptions } from "@interfere/types/sdk/errors";
4
+ //#region src/internal/capture.ts
5
+ /**
6
+ * Captures a React error through the SDK, attaching the component stack
7
+ * reported by React as additional frames. Dedupes on the Error instance.
8
+ *
9
+ * Swallows errors from `getClient()` so callers can run before the SDK is
10
+ * initialized (e.g. an error boundary catching a pre-init crash).
11
+ */
12
+ function captureReactError(error, componentStack, mechanism) {
13
+ if (seen.has(error)) return;
14
+ seen.add(error);
15
+ try {
16
+ const exceptions = toExceptions(error, mechanism);
17
+ if (componentStack && exceptions[0]) exceptions[0].frames.push(...parseReactComponentStack(componentStack));
18
+ if (shouldDropBrowserExtensionNoise(exceptions)) return;
19
+ getClient().capture("error", { exceptions });
20
+ } catch {}
21
+ }
22
+ //#endregion
23
+ export { captureReactError };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture.mjs","names":[],"sources":["../../src/internal/capture.ts"],"sourcesContent":["import {\n parseReactComponentStack,\n shouldDropBrowserExtensionNoise,\n toExceptions,\n} from \"@interfere/types/sdk/errors\";\nimport type { ErrorMechanism } from \"@interfere/types/sdk/plugins/payload/errors\";\n\nimport { getClient } from \"./client.js\";\nimport { seen } from \"./errors.js\";\n\n/**\n * Captures a React error through the SDK, attaching the component stack\n * reported by React as additional frames. Dedupes on the Error instance.\n *\n * Swallows errors from `getClient()` so callers can run before the SDK is\n * initialized (e.g. an error boundary catching a pre-init crash).\n */\nexport function captureReactError(\n error: Error,\n componentStack: string | null | undefined,\n mechanism: ErrorMechanism\n): void {\n if (seen.has(error)) {\n return;\n }\n seen.add(error);\n\n try {\n const exceptions = toExceptions(error, mechanism);\n\n if (componentStack && exceptions[0]) {\n exceptions[0].frames.push(...parseReactComponentStack(componentStack));\n }\n\n if (shouldDropBrowserExtensionNoise(exceptions)) {\n return;\n }\n\n getClient().capture(\"error\", { exceptions });\n } catch {\n // SDK not initialized — the caller still gets their fallback / user cb.\n }\n}\n"],"mappings":";;;;;;;;;;;AAiBA,SAAgB,kBACd,OACA,gBACA,WACM;AACN,KAAI,KAAK,IAAI,MAAM,CACjB;AAEF,MAAK,IAAI,MAAM;AAEf,KAAI;EACF,MAAM,aAAa,aAAa,OAAO,UAAU;AAEjD,MAAI,kBAAkB,WAAW,GAC/B,YAAW,GAAG,OAAO,KAAK,GAAG,yBAAyB,eAAe,CAAC;AAGxE,MAAI,gCAAgC,WAAW,CAC7C;AAGF,aAAW,CAAC,QAAQ,SAAS,EAAE,YAAY,CAAC;SACtC"}
@@ -31,8 +31,9 @@ var Client = class {
31
31
  ...opts._wrapperVersions ? { wrapperVersions: opts._wrapperVersions } : {}
32
32
  };
33
33
  registerServiceWorker();
34
+ const transport = new HttpTransport(targets.ingest);
34
35
  this.queue = new BatchQueue({
35
- transport: new HttpTransport(targets.ingest),
36
+ transport,
36
37
  ...opts.batch
37
38
  });
38
39
  this.queue.start();
@@ -1 +1 @@
1
- {"version":3,"file":"client.mjs","names":[],"sources":["../../src/internal/client.ts"],"sourcesContent":["import type { EnvelopePayload, EventType } from \"@interfere/types/sdk/envelope\";\nimport type { ConsentState } from \"@interfere/types/sdk/plugins/manifest\";\nimport type { RemoteConfig } from \"@interfere/types/sdk/remote-config\";\nimport { inferRuntime, normalizeEnv } from \"@interfere/types/sdk/runtime\";\n\nimport type { PluginOverrides } from \"../plugins/lib/loader.js\";\nimport { bootstrap, session, teardown } from \"../tracking/api.js\";\nimport { buildHeaders, HttpTransport } from \"../transport/http.js\";\nimport { BatchQueue, type QueueOptions } from \"../transport/queue.js\";\nimport { createLogger } from \"../util/log.js\";\nimport { resolveTargets } from \"./config.js\";\nimport { collectContext } from \"./context.js\";\nimport { buildEnvelope, type EnvelopeMetadata } from \"./envelope.js\";\nimport { PluginRuntime } from \"./plugin-runtime.js\";\nimport { registerServiceWorker } from \"./sw.js\";\nimport { PRODUCER_VERSION } from \"./version.js\";\n\nconst log = createLogger(\"client\");\n\nexport function buildSdkStack(wrapperVersions?: string[]): string[] {\n return [...(wrapperVersions ?? []), PRODUCER_VERSION];\n}\n\nexport interface ClientOptions {\n /** @internal Wrapper SDK versions (e.g. `@interfere/next@8.1.0`). */\n _wrapperVersions?: string[];\n batch?: Omit<Partial<QueueOptions>, \"transport\">;\n consent?: ConsentState;\n /**\n * Override the automatic dev-mode guard. When `undefined`, the SDK\n * auto-detects: it disables itself if `process.env[\"NODE_ENV\"]` is not\n * `\"production\"` (Node / webpack / Next.js). In environments where\n * `process` does not exist (Vite, CRA, plain browser) the SDK\n * defaults to **enabled** — pass `false` to disable explicitly.\n */\n enabled?: boolean;\n plugins?: PluginOverrides;\n}\n\nclass Client {\n private readonly metadata: EnvelopeMetadata;\n private readonly queue: BatchQueue;\n private readonly runtime: PluginRuntime;\n\n constructor(opts: ClientOptions, buildId: string, releaseId: string | null) {\n const targets = resolveTargets();\n bootstrap(targets.session);\n\n log.info(\"target: %s\", targets.ingest.url);\n\n this.metadata = {\n context: collectContext(),\n environment: normalizeEnv(\n typeof process === \"undefined\" ? undefined : process.env[\"NODE_ENV\"]\n ),\n runtime: inferRuntime(),\n buildId,\n releaseId,\n ...(opts._wrapperVersions\n ? { wrapperVersions: opts._wrapperVersions }\n : {}),\n };\n\n registerServiceWorker();\n\n const transport = new HttpTransport(targets.ingest);\n this.queue = new BatchQueue({ transport, ...opts.batch });\n this.queue.start();\n\n this.runtime = new PluginRuntime(\n {\n capture: (type, payload) => this.capture(type, payload),\n getSessionId: () => session.getId() ?? \"\",\n },\n opts.plugins,\n opts.consent\n );\n\n this.runtime.start();\n\n this.fetchRemoteConfig(targets.config);\n }\n\n private fetchRemoteConfig(configTarget: {\n url: string;\n headers: Headers;\n }): void {\n fetch(configTarget.url, {\n method: \"GET\",\n headers: buildHeaders(configTarget.headers),\n signal: AbortSignal.timeout(10_000),\n })\n .then((res) => {\n if (!res.ok) {\n return;\n }\n return res.json() as Promise<RemoteConfig>;\n })\n .then((config) => {\n if (config?.plugins) {\n this.runtime.applyRemoteConfig(config.plugins);\n log.debug(\"applied remote config\");\n }\n })\n .catch(() => {\n log.warn(\"remote config fetch failed, using local defaults\");\n });\n }\n\n capture<T extends EventType>(type: T, payload: EnvelopePayload<T>): void {\n const sessionId = session.getId();\n if (!(sessionId && this.runtime.canCapture(type))) {\n return;\n }\n\n this.queue.enqueue(buildEnvelope(type, payload, sessionId, this.metadata));\n }\n\n flush(): void {\n this.queue.flush();\n }\n\n async dispose(): Promise<void> {\n await this.runtime.dispose();\n teardown();\n this.queue.dispose();\n }\n\n getConsent(): ConsentState | null {\n return this.runtime.getConsent();\n }\n\n setConsent(value?: ConsentState): void {\n this.runtime.setConsent(value);\n }\n\n resetConsent(): void {\n this.runtime.resetConsent();\n }\n}\n\nlet instance: Client | null = null;\n\nexport function getClient(): Client {\n if (!instance) {\n throw new Error(\n \"Interfere SDK not initialized. Call init() from your instrumentation-client entrypoint.\"\n );\n }\n return instance;\n}\n\nfunction isEnabledByEnvironment(): boolean {\n try {\n if (typeof process === \"undefined\" || !process.env) {\n return true;\n }\n if (process.env[\"NODE_ENV\"] === \"production\") {\n return true;\n }\n if (process.env[\"NODE_ENV\"] === undefined) {\n return true;\n }\n return !!(\n (globalThis as Record<string, unknown>)[\"__INTERFERE_FORCE_ENABLE__\"] ||\n process.env[\"INTERFERE_FORCE_ENABLE\"]\n );\n } catch {\n return true;\n }\n}\n\nexport function init(opts: ClientOptions = {}): void {\n if (instance) {\n return;\n }\n\n if (!(opts.enabled ?? isEnabledByEnvironment())) {\n log.info(\n \"Disabled in non-production. Pass enabled: true to init() or set INTERFERE_FORCE_ENABLE=1.\"\n );\n return;\n }\n\n const buildId = (globalThis as Record<string, unknown>)[\n \"__INTERFERE_BUILD_ID__\"\n ] as string | undefined;\n\n const releaseId = (globalThis as Record<string, unknown>)[\n \"__INTERFERE_RELEASE_ID__\"\n ] as string | null | undefined;\n\n if (!buildId) {\n log.error(\n \"buildId not found — ensure withInterfere() is configured in \" +\n \"next.config and instrumentation-client.ts exists in your project root.\"\n );\n return;\n }\n\n instance = new Client(opts, buildId, releaseId ?? null);\n\n if (typeof window !== \"undefined\") {\n (window as unknown as Record<string, unknown>)[\"__INTERFERE_SDK_STACK__\"] =\n buildSdkStack(opts._wrapperVersions);\n }\n}\n\nexport async function close(): Promise<void> {\n if (!instance) {\n return;\n }\n\n await instance.dispose();\n instance = null;\n}\n\nexport const consent = {\n get(): ConsentState | null {\n return instance?.getConsent() ?? null;\n },\n\n set(value?: ConsentState): void {\n instance?.setConsent(value);\n },\n};\n\nexport function syncConsent(consentState: ConsentState | undefined): void {\n if (!instance) {\n return;\n }\n\n if (consentState) {\n instance.setConsent(consentState);\n return;\n }\n\n instance.resetConsent();\n}\n\nexport function flush(): void {\n instance?.flush();\n}\n\n/** @internal Test-only. Resets the module state so init() can be called again. */\nexport function _reset(): void {\n instance = null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.mjs","names":[],"sources":["../../src/internal/client.ts"],"sourcesContent":["import type { EnvelopePayload, EventType } from \"@interfere/types/sdk/envelope\";\nimport type { ConsentState } from \"@interfere/types/sdk/plugins/manifest\";\nimport type { RemoteConfig } from \"@interfere/types/sdk/remote-config\";\nimport { inferRuntime, normalizeEnv } from \"@interfere/types/sdk/runtime\";\n\nimport type { PluginOverrides } from \"../plugins/lib/loader.js\";\nimport { bootstrap, session, teardown } from \"../tracking/api.js\";\nimport { buildHeaders, HttpTransport } from \"../transport/http.js\";\nimport { BatchQueue, type QueueOptions } from \"../transport/queue.js\";\nimport { createLogger } from \"../util/log.js\";\nimport { resolveTargets } from \"./config.js\";\nimport { collectContext } from \"./context.js\";\nimport { buildEnvelope, type EnvelopeMetadata } from \"./envelope.js\";\nimport { PluginRuntime } from \"./plugin-runtime.js\";\nimport { registerServiceWorker } from \"./sw.js\";\nimport { PRODUCER_VERSION } from \"./version.js\";\n\nconst log = createLogger(\"client\");\n\nexport function buildSdkStack(wrapperVersions?: string[]): string[] {\n return [...(wrapperVersions ?? []), PRODUCER_VERSION];\n}\n\nexport interface ClientOptions {\n /** @internal Wrapper SDK versions (e.g. `@interfere/next@8.1.0`). */\n _wrapperVersions?: string[];\n batch?: Omit<Partial<QueueOptions>, \"transport\">;\n consent?: ConsentState;\n /**\n * Override the automatic dev-mode guard. When `undefined`, the SDK\n * auto-detects: it disables itself if `process.env[\"NODE_ENV\"]` is not\n * `\"production\"` (Node / webpack / Next.js). In environments where\n * `process` does not exist (Vite, CRA, plain browser) the SDK\n * defaults to **enabled** — pass `false` to disable explicitly.\n */\n enabled?: boolean;\n plugins?: PluginOverrides;\n}\n\nclass Client {\n private readonly metadata: EnvelopeMetadata;\n private readonly queue: BatchQueue;\n private readonly runtime: PluginRuntime;\n\n constructor(opts: ClientOptions, buildId: string, releaseId: string | null) {\n const targets = resolveTargets();\n bootstrap(targets.session);\n\n log.info(\"target: %s\", targets.ingest.url);\n\n this.metadata = {\n context: collectContext(),\n environment: normalizeEnv(\n typeof process === \"undefined\" ? undefined : process.env[\"NODE_ENV\"]\n ),\n runtime: inferRuntime(),\n buildId,\n releaseId,\n ...(opts._wrapperVersions\n ? { wrapperVersions: opts._wrapperVersions }\n : {}),\n };\n\n registerServiceWorker();\n\n const transport = new HttpTransport(targets.ingest);\n this.queue = new BatchQueue({ transport, ...opts.batch });\n this.queue.start();\n\n this.runtime = new PluginRuntime(\n {\n capture: (type, payload) => this.capture(type, payload),\n getSessionId: () => session.getId() ?? \"\",\n },\n opts.plugins,\n opts.consent\n );\n\n this.runtime.start();\n\n this.fetchRemoteConfig(targets.config);\n }\n\n private fetchRemoteConfig(configTarget: {\n url: string;\n headers: Headers;\n }): void {\n fetch(configTarget.url, {\n method: \"GET\",\n headers: buildHeaders(configTarget.headers),\n signal: AbortSignal.timeout(10_000),\n })\n .then((res) => {\n if (!res.ok) {\n return;\n }\n return res.json() as Promise<RemoteConfig>;\n })\n .then((config) => {\n if (config?.plugins) {\n this.runtime.applyRemoteConfig(config.plugins);\n log.debug(\"applied remote config\");\n }\n })\n .catch(() => {\n log.warn(\"remote config fetch failed, using local defaults\");\n });\n }\n\n capture<T extends EventType>(type: T, payload: EnvelopePayload<T>): void {\n const sessionId = session.getId();\n if (!(sessionId && this.runtime.canCapture(type))) {\n return;\n }\n\n this.queue.enqueue(buildEnvelope(type, payload, sessionId, this.metadata));\n }\n\n flush(): void {\n this.queue.flush();\n }\n\n async dispose(): Promise<void> {\n await this.runtime.dispose();\n teardown();\n this.queue.dispose();\n }\n\n getConsent(): ConsentState | null {\n return this.runtime.getConsent();\n }\n\n setConsent(value?: ConsentState): void {\n this.runtime.setConsent(value);\n }\n\n resetConsent(): void {\n this.runtime.resetConsent();\n }\n}\n\nlet instance: Client | null = null;\n\nexport function getClient(): Client {\n if (!instance) {\n throw new Error(\n \"Interfere SDK not initialized. Call init() from your instrumentation-client entrypoint.\"\n );\n }\n return instance;\n}\n\nfunction isEnabledByEnvironment(): boolean {\n try {\n if (typeof process === \"undefined\" || !process.env) {\n return true;\n }\n if (process.env[\"NODE_ENV\"] === \"production\") {\n return true;\n }\n if (process.env[\"NODE_ENV\"] === undefined) {\n return true;\n }\n return !!(\n (globalThis as Record<string, unknown>)[\"__INTERFERE_FORCE_ENABLE__\"] ||\n process.env[\"INTERFERE_FORCE_ENABLE\"]\n );\n } catch {\n return true;\n }\n}\n\nexport function init(opts: ClientOptions = {}): void {\n if (instance) {\n return;\n }\n\n if (!(opts.enabled ?? isEnabledByEnvironment())) {\n log.info(\n \"Disabled in non-production. Pass enabled: true to init() or set INTERFERE_FORCE_ENABLE=1.\"\n );\n return;\n }\n\n const buildId = (globalThis as Record<string, unknown>)[\n \"__INTERFERE_BUILD_ID__\"\n ] as string | undefined;\n\n const releaseId = (globalThis as Record<string, unknown>)[\n \"__INTERFERE_RELEASE_ID__\"\n ] as string | null | undefined;\n\n if (!buildId) {\n log.error(\n \"buildId not found — ensure withInterfere() is configured in \" +\n \"next.config and instrumentation-client.ts exists in your project root.\"\n );\n return;\n }\n\n instance = new Client(opts, buildId, releaseId ?? null);\n\n if (typeof window !== \"undefined\") {\n (window as unknown as Record<string, unknown>)[\"__INTERFERE_SDK_STACK__\"] =\n buildSdkStack(opts._wrapperVersions);\n }\n}\n\nexport async function close(): Promise<void> {\n if (!instance) {\n return;\n }\n\n await instance.dispose();\n instance = null;\n}\n\nexport const consent = {\n get(): ConsentState | null {\n return instance?.getConsent() ?? null;\n },\n\n set(value?: ConsentState): void {\n instance?.setConsent(value);\n },\n};\n\nexport function syncConsent(consentState: ConsentState | undefined): void {\n if (!instance) {\n return;\n }\n\n if (consentState) {\n instance.setConsent(consentState);\n return;\n }\n\n instance.resetConsent();\n}\n\nexport function flush(): void {\n instance?.flush();\n}\n\n/** @internal Test-only. Resets the module state so init() can be called again. */\nexport function _reset(): void {\n instance = null;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"config.mjs","names":[],"sources":["../../src/internal/config.ts"],"sourcesContent":["import { API_PATHS, API_URL } from \"@interfere/constants/api\";\n\nimport type { IngestTarget } from \"../transport/http.js\";\n\nconst DEFAULT_PROXY_URL = \"/api/interfere\";\nconst DEFAULT_SESSION_PATH = \"/v1/session\";\nconst DEFAULT_CONFIG_PATH = \"/v1/config\";\n\nfunction resolvePublicKey(): string | undefined {\n // Vite plugin injects the key onto globalThis at build time\n const injected = (globalThis as Record<string, unknown>)[\n \"__INTERFERE_PUBLIC_KEY__\"\n ] as string | undefined;\n if (injected) {\n return injected;\n }\n\n // Node / webpack / Next.js: read from process.env\n if (typeof process !== \"undefined\") {\n return process.env[\"INTERFERE_PUBLIC_KEY\"] ?? undefined;\n }\n\n return undefined;\n}\n\nexport function resolveTargets(): {\n config: IngestTarget;\n ingest: IngestTarget;\n session: IngestTarget;\n} {\n const publicKey = resolvePublicKey();\n const headers = new Headers({ \"content-type\": \"application/json\" });\n if (publicKey) {\n headers.set(\"x-interfere-pub-token\", publicKey);\n }\n\n const baseUrl = publicKey ? API_URL : DEFAULT_PROXY_URL;\n const sessionPath =\n (API_PATHS as { SESSION?: string }).SESSION ?? DEFAULT_SESSION_PATH;\n const configPath =\n (API_PATHS as { CONFIG?: string }).CONFIG ?? DEFAULT_CONFIG_PATH;\n\n return {\n config: {\n url: `${baseUrl}${configPath}`,\n headers,\n },\n ingest: {\n url: `${baseUrl}${API_PATHS.INGEST}`,\n headers,\n },\n session: {\n url: `${baseUrl}${sessionPath}`,\n headers,\n },\n };\n}\n"],"mappings":";;AAIA,MAAM,oBAAoB;AAC1B,MAAM,uBAAuB;AAC7B,MAAM,sBAAsB;AAE5B,SAAS,mBAAuC;CAE9C,MAAM,WAAY,WAChB;AAEF,KAAI,SACF,QAAO;AAIT,KAAI,OAAO,YAAY,YACrB,QAAO,QAAQ,IAAI,2BAA2B,KAAA;;AAMlD,SAAgB,iBAId;CACA,MAAM,YAAY,kBAAkB;CACpC,MAAM,UAAU,IAAI,QAAQ,EAAE,gBAAgB,oBAAoB,CAAC;AACnE,KAAI,UACF,SAAQ,IAAI,yBAAyB,UAAU;CAGjD,MAAM,UAAU,YAAY,UAAU;CACtC,MAAM,cACH,UAAmC,WAAW;AAIjD,QAAO;EACL,QAAQ;GACN,KAAK,GAAG,UAJT,UAAkC,UAAU;GAK3C;GACD;EACD,QAAQ;GACN,KAAK,GAAG,UAAU,UAAU;GAC5B;GACD;EACD,SAAS;GACP,KAAK,GAAG,UAAU;GAClB;GACD;EACF"}
1
+ {"version":3,"file":"config.mjs","names":[],"sources":["../../src/internal/config.ts"],"sourcesContent":["import { API_PATHS, API_URL } from \"@interfere/constants/api\";\n\nimport type { IngestTarget } from \"../transport/http.js\";\n\nconst DEFAULT_PROXY_URL = \"/api/interfere\";\nconst DEFAULT_SESSION_PATH = \"/v1/session\";\nconst DEFAULT_CONFIG_PATH = \"/v1/config\";\n\nfunction resolvePublicKey(): string | undefined {\n // Vite plugin injects the key onto globalThis at build time\n const injected = (globalThis as Record<string, unknown>)[\n \"__INTERFERE_PUBLIC_KEY__\"\n ] as string | undefined;\n if (injected) {\n return injected;\n }\n\n // Node / webpack / Next.js: read from process.env\n if (typeof process !== \"undefined\") {\n return process.env[\"INTERFERE_PUBLIC_KEY\"] ?? undefined;\n }\n\n return;\n}\n\nexport function resolveTargets(): {\n config: IngestTarget;\n ingest: IngestTarget;\n session: IngestTarget;\n} {\n const publicKey = resolvePublicKey();\n const headers = new Headers({ \"content-type\": \"application/json\" });\n if (publicKey) {\n headers.set(\"x-interfere-pub-token\", publicKey);\n }\n\n const baseUrl = publicKey ? API_URL : DEFAULT_PROXY_URL;\n const sessionPath =\n (API_PATHS as { SESSION?: string }).SESSION ?? DEFAULT_SESSION_PATH;\n const configPath =\n (API_PATHS as { CONFIG?: string }).CONFIG ?? DEFAULT_CONFIG_PATH;\n\n return {\n config: {\n url: `${baseUrl}${configPath}`,\n headers,\n },\n ingest: {\n url: `${baseUrl}${API_PATHS.INGEST}`,\n headers,\n },\n session: {\n url: `${baseUrl}${sessionPath}`,\n headers,\n },\n };\n}\n"],"mappings":";;AAIA,MAAM,oBAAoB;AAC1B,MAAM,uBAAuB;AAC7B,MAAM,sBAAsB;AAE5B,SAAS,mBAAuC;CAE9C,MAAM,WAAY,WAChB;AAEF,KAAI,SACF,QAAO;AAIT,KAAI,OAAO,YAAY,YACrB,QAAO,QAAQ,IAAI,2BAA2B,KAAA;;AAMlD,SAAgB,iBAId;CACA,MAAM,YAAY,kBAAkB;CACpC,MAAM,UAAU,IAAI,QAAQ,EAAE,gBAAgB,oBAAoB,CAAC;AACnE,KAAI,UACF,SAAQ,IAAI,yBAAyB,UAAU;CAGjD,MAAM,UAAU,YAAY,UAAU;CACtC,MAAM,cACH,UAAmC,WAAW;AAIjD,QAAO;EACL,QAAQ;GACN,KAAK,GAAG,UAJT,UAAkC,UAAU;GAK3C;GACD;EACD,QAAQ;GACN,KAAK,GAAG,UAAU,UAAU;GAC5B;GACD;EACD,SAAS;GACP,KAAK,GAAG,UAAU;GAClB;GACD;EACF"}
package/dist/package.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
2
  var name = "@interfere/react";
3
- var version = "8.1.2";
3
+ var version = "8.1.6";
4
4
  //#endregion
5
5
  export { name, version };
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.mts","names":[],"sources":["../../src/plugins/errors.ts"],"mappings":";;;cAgCa,YAAA,EAAc,MAAA"}
1
+ {"version":3,"file":"errors.d.mts","names":[],"sources":["../../src/plugins/errors.ts"],"mappings":";;;cA2Ea,YAAA,EAAc,MAAA"}
@@ -1,6 +1,12 @@
1
1
  import { seen } from "../internal/errors.mjs";
2
- import { shouldDropBrowserExtensionNoise, toExceptions } from "@interfere/types/sdk/errors";
2
+ import { MECHANISM_TYPE, shouldDropBrowserExtensionNoise, shouldDropUnresolvableStack, toError, toExceptions } from "@interfere/types/sdk/errors";
3
3
  //#region src/plugins/errors.ts
4
+ /**
5
+ * V8's default stack limit is 10. Deep React trees routinely exceed that
6
+ * before reaching the actual application frames, leaving only react-dom
7
+ * internals in the captured stack. Matches Sentry's browser SDK.
8
+ */
9
+ const STACK_TRACE_LIMIT = 50;
4
10
  let capturing = false;
5
11
  function capture(ctx, opts) {
6
12
  if (capturing || seen.has(opts.error)) return;
@@ -8,49 +14,73 @@ function capture(ctx, opts) {
8
14
  capturing = true;
9
15
  try {
10
16
  const exceptions = toExceptions(opts.error, opts.mechanism);
11
- if (shouldDropBrowserExtensionNoise(exceptions)) return;
17
+ if (opts.fallbackFrame && exceptions[0]?.frames.length === 0) exceptions[0].frames.push(opts.fallbackFrame);
18
+ if (shouldDropBrowserExtensionNoise(exceptions) || shouldDropUnresolvableStack(exceptions)) return;
12
19
  ctx.capture("error", { exceptions });
13
20
  } finally {
14
21
  capturing = false;
15
22
  }
16
23
  }
24
+ /**
25
+ * Finds the first `Error` instance in a list of `console.error` arguments.
26
+ * React 18+ in production logs errors as
27
+ * `console.error("The above error occurred in ...", error, componentStack)`,
28
+ * so scanning only `args[0]` misses React's own uncaught-error reports.
29
+ */
30
+ function findErrorArg(args) {
31
+ for (const arg of args) if (arg instanceof Error) return arg;
32
+ return null;
33
+ }
17
34
  const errorsPlugin = {
18
35
  name: "errors",
19
36
  setup(ctx) {
20
37
  const originalOnError = globalThis.onerror;
21
38
  const originalConsoleError = globalThis.console.error;
39
+ const originalStackTraceLimit = Error.stackTraceLimit;
40
+ if (Error.stackTraceLimit < STACK_TRACE_LIMIT) Error.stackTraceLimit = STACK_TRACE_LIMIT;
22
41
  globalThis.onerror = (msg, source, line, col, error) => {
23
- if (error instanceof Error) capture(ctx, {
24
- error,
25
- mechanism: {
26
- type: "onerror",
27
- handled: false
28
- }
29
- });
42
+ if (error instanceof Error) {
43
+ const fallbackFrame = typeof source === "string" ? {
44
+ fileName: source,
45
+ ...typeof line === "number" ? { lineNumber: line } : {},
46
+ ...typeof col === "number" ? { columnNumber: col } : {}
47
+ } : null;
48
+ capture(ctx, {
49
+ error,
50
+ mechanism: {
51
+ type: MECHANISM_TYPE.browser.onerror,
52
+ handled: false
53
+ },
54
+ ...fallbackFrame ? { fallbackFrame } : {}
55
+ });
56
+ }
30
57
  if (typeof originalOnError === "function") return originalOnError.call(globalThis, msg, source, line, col, error);
31
58
  return false;
32
59
  };
33
60
  const onUnhandledRejection = (event) => {
34
- if (event.reason instanceof Error) capture(ctx, {
35
- error: event.reason,
61
+ capture(ctx, {
62
+ error: toError(event.reason),
36
63
  mechanism: {
37
- type: "unhandledrejection",
38
- handled: false
64
+ type: MECHANISM_TYPE.browser.onunhandledrejection,
65
+ handled: false,
66
+ ...event.reason instanceof Error ? {} : { synthetic: true }
39
67
  }
40
68
  });
41
69
  };
42
70
  globalThis.addEventListener("unhandledrejection", onUnhandledRejection);
43
71
  globalThis.console.error = (...args) => {
44
72
  originalConsoleError.apply(globalThis.console, args);
45
- if (args[0] instanceof Error) capture(ctx, {
46
- error: args[0],
73
+ const error = findErrorArg(args);
74
+ if (error) capture(ctx, {
75
+ error,
47
76
  mechanism: {
48
- type: "console.error",
77
+ type: MECHANISM_TYPE.browser.consoleError,
49
78
  handled: true
50
79
  }
51
80
  });
52
81
  };
53
82
  return () => {
83
+ Error.stackTraceLimit = originalStackTraceLimit;
54
84
  globalThis.onerror = originalOnError;
55
85
  globalThis.removeEventListener("unhandledrejection", onUnhandledRejection);
56
86
  globalThis.console.error = originalConsoleError;
@@ -1 +1 @@
1
- {"version":3,"file":"errors.mjs","names":[],"sources":["../../src/plugins/errors.ts"],"sourcesContent":["import {\n shouldDropBrowserExtensionNoise,\n toExceptions,\n} from \"@interfere/types/sdk/errors\";\nimport type { ErrorMechanism } from \"@interfere/types/sdk/plugins/payload/errors\";\n\nimport { seen } from \"../internal/errors.js\";\nimport type { Plugin, PluginContext } from \"./lib/types.js\";\n\nlet capturing = false;\n\nfunction capture(\n ctx: PluginContext,\n opts: { error: Error; mechanism: ErrorMechanism }\n) {\n if (capturing || seen.has(opts.error)) {\n return;\n }\n\n seen.add(opts.error);\n capturing = true;\n try {\n const exceptions = toExceptions(opts.error, opts.mechanism);\n if (shouldDropBrowserExtensionNoise(exceptions)) {\n return;\n }\n ctx.capture(\"error\", { exceptions });\n } finally {\n capturing = false;\n }\n}\n\nexport const errorsPlugin: Plugin = {\n name: \"errors\",\n\n setup(ctx) {\n const originalOnError = globalThis.onerror;\n const originalConsoleError = globalThis.console.error;\n\n globalThis.onerror = (msg, source, line, col, error) => {\n if (error instanceof Error) {\n capture(ctx, {\n error,\n mechanism: { type: \"onerror\", handled: false },\n });\n }\n if (typeof originalOnError === \"function\") {\n return originalOnError.call(globalThis, msg, source, line, col, error);\n }\n return false;\n };\n\n const onUnhandledRejection = (event: PromiseRejectionEvent) => {\n if (event.reason instanceof Error) {\n capture(ctx, {\n error: event.reason,\n mechanism: { type: \"unhandledrejection\", handled: false },\n });\n }\n };\n globalThis.addEventListener(\"unhandledrejection\", onUnhandledRejection);\n\n globalThis.console.error = (...args: unknown[]) => {\n originalConsoleError.apply(globalThis.console, args);\n if (args[0] instanceof Error) {\n capture(ctx, {\n error: args[0],\n mechanism: { type: \"console.error\", handled: true },\n });\n }\n };\n\n return () => {\n globalThis.onerror = originalOnError;\n globalThis.removeEventListener(\n \"unhandledrejection\",\n onUnhandledRejection\n );\n globalThis.console.error = originalConsoleError;\n };\n },\n};\n\nexport default errorsPlugin;\n"],"mappings":";;;AASA,IAAI,YAAY;AAEhB,SAAS,QACP,KACA,MACA;AACA,KAAI,aAAa,KAAK,IAAI,KAAK,MAAM,CACnC;AAGF,MAAK,IAAI,KAAK,MAAM;AACpB,aAAY;AACZ,KAAI;EACF,MAAM,aAAa,aAAa,KAAK,OAAO,KAAK,UAAU;AAC3D,MAAI,gCAAgC,WAAW,CAC7C;AAEF,MAAI,QAAQ,SAAS,EAAE,YAAY,CAAC;WAC5B;AACR,cAAY;;;AAIhB,MAAa,eAAuB;CAClC,MAAM;CAEN,MAAM,KAAK;EACT,MAAM,kBAAkB,WAAW;EACnC,MAAM,uBAAuB,WAAW,QAAQ;AAEhD,aAAW,WAAW,KAAK,QAAQ,MAAM,KAAK,UAAU;AACtD,OAAI,iBAAiB,MACnB,SAAQ,KAAK;IACX;IACA,WAAW;KAAE,MAAM;KAAW,SAAS;KAAO;IAC/C,CAAC;AAEJ,OAAI,OAAO,oBAAoB,WAC7B,QAAO,gBAAgB,KAAK,YAAY,KAAK,QAAQ,MAAM,KAAK,MAAM;AAExE,UAAO;;EAGT,MAAM,wBAAwB,UAAiC;AAC7D,OAAI,MAAM,kBAAkB,MAC1B,SAAQ,KAAK;IACX,OAAO,MAAM;IACb,WAAW;KAAE,MAAM;KAAsB,SAAS;KAAO;IAC1D,CAAC;;AAGN,aAAW,iBAAiB,sBAAsB,qBAAqB;AAEvE,aAAW,QAAQ,SAAS,GAAG,SAAoB;AACjD,wBAAqB,MAAM,WAAW,SAAS,KAAK;AACpD,OAAI,KAAK,cAAc,MACrB,SAAQ,KAAK;IACX,OAAO,KAAK;IACZ,WAAW;KAAE,MAAM;KAAiB,SAAS;KAAM;IACpD,CAAC;;AAIN,eAAa;AACX,cAAW,UAAU;AACrB,cAAW,oBACT,sBACA,qBACD;AACD,cAAW,QAAQ,QAAQ;;;CAGhC"}
1
+ {"version":3,"file":"errors.mjs","names":[],"sources":["../../src/plugins/errors.ts"],"sourcesContent":["import type { IngestedFrame } from \"@interfere/types/data/frame\";\nimport {\n MECHANISM_TYPE,\n shouldDropBrowserExtensionNoise,\n shouldDropUnresolvableStack,\n toError,\n toExceptions,\n} from \"@interfere/types/sdk/errors\";\nimport type { ErrorMechanism } from \"@interfere/types/sdk/plugins/payload/errors\";\n\nimport { seen } from \"../internal/errors.js\";\nimport type { Plugin, PluginContext } from \"./lib/types.js\";\n\n/**\n * V8's default stack limit is 10. Deep React trees routinely exceed that\n * before reaching the actual application frames, leaving only react-dom\n * internals in the captured stack. Matches Sentry's browser SDK.\n */\nconst STACK_TRACE_LIMIT = 50;\n\nlet capturing = false;\n\ninterface CaptureOpts {\n readonly error: Error;\n /**\n * Fallback frame to inject when the parsed stack of the root exception is\n * empty. Used for `window.onerror` calls where the browser provides\n * `source`/`line`/`col` even though the Error object itself has a\n * degenerate stack.\n */\n readonly fallbackFrame?: IngestedFrame;\n readonly mechanism: ErrorMechanism;\n}\n\nfunction capture(ctx: PluginContext, opts: CaptureOpts) {\n if (capturing || seen.has(opts.error)) {\n return;\n }\n\n seen.add(opts.error);\n capturing = true;\n try {\n const exceptions = toExceptions(opts.error, opts.mechanism);\n\n if (opts.fallbackFrame && exceptions[0]?.frames.length === 0) {\n exceptions[0].frames.push(opts.fallbackFrame);\n }\n\n if (\n shouldDropBrowserExtensionNoise(exceptions) ||\n shouldDropUnresolvableStack(exceptions)\n ) {\n return;\n }\n ctx.capture(\"error\", { exceptions });\n } finally {\n capturing = false;\n }\n}\n\n/**\n * Finds the first `Error` instance in a list of `console.error` arguments.\n * React 18+ in production logs errors as\n * `console.error(\"The above error occurred in ...\", error, componentStack)`,\n * so scanning only `args[0]` misses React's own uncaught-error reports.\n */\nfunction findErrorArg(args: readonly unknown[]): Error | null {\n for (const arg of args) {\n if (arg instanceof Error) {\n return arg;\n }\n }\n return null;\n}\n\nexport const errorsPlugin: Plugin = {\n name: \"errors\",\n\n setup(ctx) {\n const originalOnError = globalThis.onerror;\n const originalConsoleError = globalThis.console.error;\n const originalStackTraceLimit = Error.stackTraceLimit;\n if (Error.stackTraceLimit < STACK_TRACE_LIMIT) {\n Error.stackTraceLimit = STACK_TRACE_LIMIT;\n }\n\n globalThis.onerror = (msg, source, line, col, error) => {\n if (error instanceof Error) {\n const fallbackFrame =\n typeof source === \"string\"\n ? {\n fileName: source,\n ...(typeof line === \"number\" ? { lineNumber: line } : {}),\n ...(typeof col === \"number\" ? { columnNumber: col } : {}),\n }\n : null;\n\n capture(ctx, {\n error,\n mechanism: { type: MECHANISM_TYPE.browser.onerror, handled: false },\n ...(fallbackFrame ? { fallbackFrame } : {}),\n });\n }\n if (typeof originalOnError === \"function\") {\n return originalOnError.call(globalThis, msg, source, line, col, error);\n }\n return false;\n };\n\n const onUnhandledRejection = (event: PromiseRejectionEvent) => {\n capture(ctx, {\n error: toError(event.reason),\n mechanism: {\n type: MECHANISM_TYPE.browser.onunhandledrejection,\n handled: false,\n ...(event.reason instanceof Error ? {} : { synthetic: true }),\n },\n });\n };\n globalThis.addEventListener(\"unhandledrejection\", onUnhandledRejection);\n\n globalThis.console.error = (...args: unknown[]) => {\n originalConsoleError.apply(globalThis.console, args);\n const error = findErrorArg(args);\n if (error) {\n capture(ctx, {\n error,\n mechanism: {\n type: MECHANISM_TYPE.browser.consoleError,\n handled: true,\n },\n });\n }\n };\n\n return () => {\n Error.stackTraceLimit = originalStackTraceLimit;\n globalThis.onerror = originalOnError;\n globalThis.removeEventListener(\n \"unhandledrejection\",\n onUnhandledRejection\n );\n globalThis.console.error = originalConsoleError;\n };\n },\n};\n\nexport default errorsPlugin;\n"],"mappings":";;;;;;;;AAkBA,MAAM,oBAAoB;AAE1B,IAAI,YAAY;AAchB,SAAS,QAAQ,KAAoB,MAAmB;AACtD,KAAI,aAAa,KAAK,IAAI,KAAK,MAAM,CACnC;AAGF,MAAK,IAAI,KAAK,MAAM;AACpB,aAAY;AACZ,KAAI;EACF,MAAM,aAAa,aAAa,KAAK,OAAO,KAAK,UAAU;AAE3D,MAAI,KAAK,iBAAiB,WAAW,IAAI,OAAO,WAAW,EACzD,YAAW,GAAG,OAAO,KAAK,KAAK,cAAc;AAG/C,MACE,gCAAgC,WAAW,IAC3C,4BAA4B,WAAW,CAEvC;AAEF,MAAI,QAAQ,SAAS,EAAE,YAAY,CAAC;WAC5B;AACR,cAAY;;;;;;;;;AAUhB,SAAS,aAAa,MAAwC;AAC5D,MAAK,MAAM,OAAO,KAChB,KAAI,eAAe,MACjB,QAAO;AAGX,QAAO;;AAGT,MAAa,eAAuB;CAClC,MAAM;CAEN,MAAM,KAAK;EACT,MAAM,kBAAkB,WAAW;EACnC,MAAM,uBAAuB,WAAW,QAAQ;EAChD,MAAM,0BAA0B,MAAM;AACtC,MAAI,MAAM,kBAAkB,kBAC1B,OAAM,kBAAkB;AAG1B,aAAW,WAAW,KAAK,QAAQ,MAAM,KAAK,UAAU;AACtD,OAAI,iBAAiB,OAAO;IAC1B,MAAM,gBACJ,OAAO,WAAW,WACd;KACE,UAAU;KACV,GAAI,OAAO,SAAS,WAAW,EAAE,YAAY,MAAM,GAAG,EAAE;KACxD,GAAI,OAAO,QAAQ,WAAW,EAAE,cAAc,KAAK,GAAG,EAAE;KACzD,GACD;AAEN,YAAQ,KAAK;KACX;KACA,WAAW;MAAE,MAAM,eAAe,QAAQ;MAAS,SAAS;MAAO;KACnE,GAAI,gBAAgB,EAAE,eAAe,GAAG,EAAE;KAC3C,CAAC;;AAEJ,OAAI,OAAO,oBAAoB,WAC7B,QAAO,gBAAgB,KAAK,YAAY,KAAK,QAAQ,MAAM,KAAK,MAAM;AAExE,UAAO;;EAGT,MAAM,wBAAwB,UAAiC;AAC7D,WAAQ,KAAK;IACX,OAAO,QAAQ,MAAM,OAAO;IAC5B,WAAW;KACT,MAAM,eAAe,QAAQ;KAC7B,SAAS;KACT,GAAI,MAAM,kBAAkB,QAAQ,EAAE,GAAG,EAAE,WAAW,MAAM;KAC7D;IACF,CAAC;;AAEJ,aAAW,iBAAiB,sBAAsB,qBAAqB;AAEvE,aAAW,QAAQ,SAAS,GAAG,SAAoB;AACjD,wBAAqB,MAAM,WAAW,SAAS,KAAK;GACpD,MAAM,QAAQ,aAAa,KAAK;AAChC,OAAI,MACF,SAAQ,KAAK;IACX;IACA,WAAW;KACT,MAAM,eAAe,QAAQ;KAC7B,SAAS;KACV;IACF,CAAC;;AAIN,eAAa;AACX,SAAM,kBAAkB;AACxB,cAAW,UAAU;AACrB,cAAW,oBACT,sBACA,qBACD;AACD,cAAW,QAAQ,QAAQ;;;CAGhC"}
@@ -1 +1 @@
1
- {"version":3,"file":"loader.mjs","names":[],"sources":["../../../src/plugins/lib/loader.ts"],"sourcesContent":["import {\n PLUGIN_MANIFEST,\n type PluginKey,\n} from \"@interfere/types/sdk/plugins/manifest\";\n\nimport { createLogger } from \"../../util/log.js\";\nimport type { Plugin, PluginCleanup, PluginContext } from \"./types.js\";\n\nconst log = createLogger(\"plugins\");\n\ntype PluginLoader = () => Promise<{ default: Plugin } | Plugin>;\n\nconst LOADERS: Partial<Record<PluginKey, PluginLoader>> = {\n errors: () => import(\"../errors.js\"),\n device: () => import(\"../device.js\"),\n pageEvents: () => import(\"../pages.js\"),\n rageClick: () => import(\"../rage-clicks.js\"),\n replay: () => import(\"../replay.js\"),\n};\n\nconst DEFAULTS: Record<PluginKey, boolean> = Object.fromEntries(\n PLUGIN_MANIFEST.map((p) => [p.name, p.defaultEnabled])\n) as Record<PluginKey, boolean>;\n\nexport type PluginOverrides = Partial<Record<PluginKey, boolean>>;\n\nexport function resolveFeatures(\n overrides?: PluginOverrides\n): Record<PluginKey, boolean> {\n return { ...DEFAULTS, ...overrides };\n}\n\nfunction resolvePlugin(mod: { default: Plugin } | Plugin): Plugin {\n return \"default\" in mod && typeof (mod.default as Plugin).setup === \"function\"\n ? mod.default\n : (mod as Plugin);\n}\n\nexport async function loadPlugin(\n key: PluginKey,\n context: PluginContext\n): Promise<PluginCleanup | null> {\n const loader = LOADERS[key];\n if (!loader) {\n return null;\n }\n\n try {\n const mod = await loader();\n const plugin = resolvePlugin(mod);\n const cleanup = plugin.setup(context);\n log.debug(\"loaded %s\", key);\n return typeof cleanup === \"function\" ? cleanup : null;\n } catch {\n log.error(\"failed to load plugin %s\", key);\n return null;\n }\n}\n\nexport async function loadPlugins(\n overrides: PluginOverrides | undefined,\n context: PluginContext\n): Promise<PluginCleanup[]> {\n const resolved = { ...DEFAULTS, ...overrides };\n const keys = (Object.entries(resolved) as [PluginKey, boolean][])\n .filter(([key, enabled]) => enabled && key in LOADERS)\n .map(([key]) => key);\n\n const cleanups = await Promise.all(\n keys.map(async (key) => loadPlugin(key, context))\n );\n return cleanups.filter((cleanup) => cleanup !== null);\n}\n"],"mappings":";;;AAQA,MAAM,MAAM,aAAa,UAAU;AAInC,MAAM,UAAoD;CACxD,cAAc,OAAO;CACrB,cAAc,OAAO;CACrB,kBAAkB,OAAO;CACzB,iBAAiB,OAAO;CACxB,cAAc,OAAO;CACtB;AAED,MAAM,WAAuC,OAAO,YAClD,gBAAgB,KAAK,MAAM,CAAC,EAAE,MAAM,EAAE,eAAe,CAAC,CACvD;AAID,SAAgB,gBACd,WAC4B;AAC5B,QAAO;EAAE,GAAG;EAAU,GAAG;EAAW;;AAGtC,SAAS,cAAc,KAA2C;AAChE,QAAO,aAAa,OAAO,OAAQ,IAAI,QAAmB,UAAU,aAChE,IAAI,UACH;;AAGP,eAAsB,WACpB,KACA,SAC+B;CAC/B,MAAM,SAAS,QAAQ;AACvB,KAAI,CAAC,OACH,QAAO;AAGT,KAAI;EAGF,MAAM,UADS,cADH,MAAM,QAAQ,CACO,CACV,MAAM,QAAQ;AACrC,MAAI,MAAM,aAAa,IAAI;AAC3B,SAAO,OAAO,YAAY,aAAa,UAAU;SAC3C;AACN,MAAI,MAAM,4BAA4B,IAAI;AAC1C,SAAO;;;AAIX,eAAsB,YACpB,WACA,SAC0B;CAC1B,MAAM,WAAW;EAAE,GAAG;EAAU,GAAG;EAAW;CAC9C,MAAM,OAAQ,OAAO,QAAQ,SAAS,CACnC,QAAQ,CAAC,KAAK,aAAa,WAAW,OAAO,QAAQ,CACrD,KAAK,CAAC,SAAS,IAAI;AAKtB,SAHiB,MAAM,QAAQ,IAC7B,KAAK,IAAI,OAAO,QAAQ,WAAW,KAAK,QAAQ,CAAC,CAClD,EACe,QAAQ,YAAY,YAAY,KAAK"}
1
+ {"version":3,"file":"loader.mjs","names":[],"sources":["../../../src/plugins/lib/loader.ts"],"sourcesContent":["import {\n PLUGIN_MANIFEST,\n type PluginKey,\n} from \"@interfere/types/sdk/plugins/manifest\";\n\nimport { createLogger } from \"../../util/log.js\";\nimport type { Plugin, PluginCleanup, PluginContext } from \"./types.js\";\n\nconst log = createLogger(\"plugins\");\n\ntype PluginLoader = () => Promise<{ default: Plugin } | Plugin>;\n\nconst LOADERS: Partial<Record<PluginKey, PluginLoader>> = {\n errors: () => import(\"../errors.js\"),\n device: () => import(\"../device.js\"),\n pageEvents: () => import(\"../pages.js\"),\n rageClick: () => import(\"../rage-clicks.js\"),\n replay: () => import(\"../replay.js\"),\n};\n\nconst DEFAULTS: Record<PluginKey, boolean> = Object.fromEntries(\n PLUGIN_MANIFEST.map((p) => [p.name, p.defaultEnabled])\n) as Record<PluginKey, boolean>;\n\nexport type PluginOverrides = Partial<Record<PluginKey, boolean>>;\n\nexport function resolveFeatures(\n overrides?: PluginOverrides\n): Record<PluginKey, boolean> {\n return { ...DEFAULTS, ...overrides };\n}\n\nfunction resolvePlugin(mod: { default: Plugin } | Plugin): Plugin {\n return \"default\" in mod && typeof (mod.default as Plugin).setup === \"function\"\n ? mod.default\n : (mod as Plugin);\n}\n\nexport async function loadPlugin(\n key: PluginKey,\n context: PluginContext\n): Promise<PluginCleanup | null> {\n const loader = LOADERS[key];\n if (!loader) {\n return null;\n }\n\n try {\n const mod = await loader();\n const plugin = resolvePlugin(mod);\n const cleanup = plugin.setup(context);\n log.debug(\"loaded %s\", key);\n return typeof cleanup === \"function\" ? cleanup : null;\n } catch {\n log.error(\"failed to load plugin %s\", key);\n return null;\n }\n}\n\nexport async function loadPlugins(\n overrides: PluginOverrides | undefined,\n context: PluginContext\n): Promise<PluginCleanup[]> {\n const resolved = { ...DEFAULTS, ...overrides };\n const keys = (Object.entries(resolved) as [PluginKey, boolean][])\n .filter(([key, enabled]) => enabled && key in LOADERS)\n .map(([key]) => key);\n\n const cleanups = await Promise.all(\n keys.map(async (key) => loadPlugin(key, context))\n );\n return cleanups.filter((cleanup) => cleanup !== null);\n}\n"],"mappings":";;;AAQA,MAAM,MAAM,aAAa,UAAU;AAInC,MAAM,UAAoD;CACxD,cAAc,OAAO;CACrB,cAAc,OAAO;CACrB,kBAAkB,OAAO;CACzB,iBAAiB,OAAO;CACxB,cAAc,OAAO;CACtB;AAED,MAAM,WAAuC,OAAO,YAClD,gBAAgB,KAAK,MAAM,CAAC,EAAE,MAAM,EAAE,eAAe,CAAC,CACvD;AAID,SAAgB,gBACd,WAC4B;AAC5B,QAAO;EAAE,GAAG;EAAU,GAAG;EAAW;;AAGtC,SAAS,cAAc,KAA2C;AAChE,QAAO,aAAa,OAAO,OAAQ,IAAI,QAAmB,UAAU,aAChE,IAAI,UACH;;AAGP,eAAsB,WACpB,KACA,SAC+B;CAC/B,MAAM,SAAS,QAAQ;AACvB,KAAI,CAAC,OACH,QAAO;AAGT,KAAI;EAGF,MAAM,UADS,cAAc,MADX,QAAQ,CAEJ,CAAC,MAAM,QAAQ;AACrC,MAAI,MAAM,aAAa,IAAI;AAC3B,SAAO,OAAO,YAAY,aAAa,UAAU;SAC3C;AACN,MAAI,MAAM,4BAA4B,IAAI;AAC1C,SAAO;;;AAIX,eAAsB,YACpB,WACA,SAC0B;CAC1B,MAAM,WAAW;EAAE,GAAG;EAAU,GAAG;EAAW;CAC9C,MAAM,OAAQ,OAAO,QAAQ,SAAS,CACnC,QAAQ,CAAC,KAAK,aAAa,WAAW,OAAO,QAAQ,CACrD,KAAK,CAAC,SAAS,IAAI;AAKtB,SAAO,MAHgB,QAAQ,IAC7B,KAAK,IAAI,OAAO,QAAQ,WAAW,KAAK,QAAQ,CAAC,CAClD,EACe,QAAQ,YAAY,YAAY,KAAK"}
@@ -1 +1 @@
1
- {"version":3,"file":"replay.mjs","names":[],"sources":["../../src/plugins/replay.ts"],"sourcesContent":["import { createLogger } from \"../util/log.js\";\nimport type { Plugin } from \"./lib/types.js\";\n\nconst log = createLogger(\"replay\");\n\nconst FLUSH_INTERVAL_MS = 10_000;\n\nexport const replayPlugin: Plugin = {\n name: \"replay\",\n\n setup(ctx) {\n let stopFn: (() => void) | null = null;\n let events: string[] = [];\n let flushTimer: ReturnType<typeof setInterval> | null = null;\n let firstTs: number | null = null;\n let lastTs: number | null = null;\n\n const flush = () => {\n if (events.length === 0) {\n return;\n }\n const chunk = events;\n events = [];\n const fts = firstTs;\n const lts = lastTs;\n firstTs = null;\n lastTs = null;\n\n ctx.capture(\"replay_chunk\", {\n ts: Date.now(),\n count: chunk.length,\n events: chunk,\n ...(fts !== null && { first_event_ts: fts }),\n ...(lts !== null && { last_event_ts: lts }),\n });\n };\n\n const onVisibilityChange = () => {\n if (document.visibilityState === \"hidden\") {\n flush();\n }\n };\n const onBeforeUnload = () => {\n flush();\n };\n\n const init = async () => {\n try {\n const rrweb = await import(\"rrweb\");\n stopFn =\n rrweb.record({\n emit(event) {\n const ts = Date.now();\n if (firstTs === null) {\n firstTs = ts;\n }\n lastTs = ts;\n events.push(JSON.stringify(event));\n },\n }) ?? null;\n\n flushTimer = setInterval(flush, FLUSH_INTERVAL_MS);\n globalThis.addEventListener(\"visibilitychange\", onVisibilityChange);\n globalThis.addEventListener(\"beforeunload\", onBeforeUnload);\n log.debug(\"recording started\");\n } catch {\n log.error(\"rrweb failed to load, replay disabled\");\n }\n };\n\n init().catch(() => {\n // rrweb load failure is non-fatal\n });\n\n return () => {\n flush();\n stopFn?.();\n if (flushTimer) {\n clearInterval(flushTimer);\n }\n globalThis.removeEventListener(\"visibilitychange\", onVisibilityChange);\n globalThis.removeEventListener(\"beforeunload\", onBeforeUnload);\n };\n },\n};\n\nexport default replayPlugin;\n"],"mappings":";;AAGA,MAAM,MAAM,aAAa,SAAS;AAElC,MAAM,oBAAoB;AAE1B,MAAa,eAAuB;CAClC,MAAM;CAEN,MAAM,KAAK;EACT,IAAI,SAA8B;EAClC,IAAI,SAAmB,EAAE;EACzB,IAAI,aAAoD;EACxD,IAAI,UAAyB;EAC7B,IAAI,SAAwB;EAE5B,MAAM,cAAc;AAClB,OAAI,OAAO,WAAW,EACpB;GAEF,MAAM,QAAQ;AACd,YAAS,EAAE;GACX,MAAM,MAAM;GACZ,MAAM,MAAM;AACZ,aAAU;AACV,YAAS;AAET,OAAI,QAAQ,gBAAgB;IAC1B,IAAI,KAAK,KAAK;IACd,OAAO,MAAM;IACb,QAAQ;IACR,GAAI,QAAQ,QAAQ,EAAE,gBAAgB,KAAK;IAC3C,GAAI,QAAQ,QAAQ,EAAE,eAAe,KAAK;IAC3C,CAAC;;EAGJ,MAAM,2BAA2B;AAC/B,OAAI,SAAS,oBAAoB,SAC/B,QAAO;;EAGX,MAAM,uBAAuB;AAC3B,UAAO;;EAGT,MAAM,OAAO,YAAY;AACvB,OAAI;AAEF,cADc,MAAM,OAAO,UAEnB,OAAO,EACX,KAAK,OAAO;KACV,MAAM,KAAK,KAAK,KAAK;AACrB,SAAI,YAAY,KACd,WAAU;AAEZ,cAAS;AACT,YAAO,KAAK,KAAK,UAAU,MAAM,CAAC;OAErC,CAAC,IAAI;AAER,iBAAa,YAAY,OAAO,kBAAkB;AAClD,eAAW,iBAAiB,oBAAoB,mBAAmB;AACnE,eAAW,iBAAiB,gBAAgB,eAAe;AAC3D,QAAI,MAAM,oBAAoB;WACxB;AACN,QAAI,MAAM,wCAAwC;;;AAItD,QAAM,CAAC,YAAY,GAEjB;AAEF,eAAa;AACX,UAAO;AACP,aAAU;AACV,OAAI,WACF,eAAc,WAAW;AAE3B,cAAW,oBAAoB,oBAAoB,mBAAmB;AACtE,cAAW,oBAAoB,gBAAgB,eAAe;;;CAGnE"}
1
+ {"version":3,"file":"replay.mjs","names":[],"sources":["../../src/plugins/replay.ts"],"sourcesContent":["import { createLogger } from \"../util/log.js\";\nimport type { Plugin } from \"./lib/types.js\";\n\nconst log = createLogger(\"replay\");\n\nconst FLUSH_INTERVAL_MS = 10_000;\n\nexport const replayPlugin: Plugin = {\n name: \"replay\",\n\n setup(ctx) {\n let stopFn: (() => void) | null = null;\n let events: string[] = [];\n let flushTimer: ReturnType<typeof setInterval> | null = null;\n let firstTs: number | null = null;\n let lastTs: number | null = null;\n\n const flush = () => {\n if (events.length === 0) {\n return;\n }\n const chunk = events;\n events = [];\n const fts = firstTs;\n const lts = lastTs;\n firstTs = null;\n lastTs = null;\n\n ctx.capture(\"replay_chunk\", {\n ts: Date.now(),\n count: chunk.length,\n events: chunk,\n ...(fts !== null && { first_event_ts: fts }),\n ...(lts !== null && { last_event_ts: lts }),\n });\n };\n\n const onVisibilityChange = () => {\n if (document.visibilityState === \"hidden\") {\n flush();\n }\n };\n const onBeforeUnload = () => {\n flush();\n };\n\n const init = async () => {\n try {\n const rrweb = await import(\"rrweb\");\n stopFn =\n rrweb.record({\n emit(event) {\n const ts = Date.now();\n if (firstTs === null) {\n firstTs = ts;\n }\n lastTs = ts;\n events.push(JSON.stringify(event));\n },\n }) ?? null;\n\n flushTimer = setInterval(flush, FLUSH_INTERVAL_MS);\n globalThis.addEventListener(\"visibilitychange\", onVisibilityChange);\n globalThis.addEventListener(\"beforeunload\", onBeforeUnload);\n log.debug(\"recording started\");\n } catch {\n log.error(\"rrweb failed to load, replay disabled\");\n }\n };\n\n init().catch(() => {\n // rrweb load failure is non-fatal\n });\n\n return () => {\n flush();\n stopFn?.();\n if (flushTimer) {\n clearInterval(flushTimer);\n }\n globalThis.removeEventListener(\"visibilitychange\", onVisibilityChange);\n globalThis.removeEventListener(\"beforeunload\", onBeforeUnload);\n };\n },\n};\n\nexport default replayPlugin;\n"],"mappings":";;AAGA,MAAM,MAAM,aAAa,SAAS;AAElC,MAAM,oBAAoB;AAE1B,MAAa,eAAuB;CAClC,MAAM;CAEN,MAAM,KAAK;EACT,IAAI,SAA8B;EAClC,IAAI,SAAmB,EAAE;EACzB,IAAI,aAAoD;EACxD,IAAI,UAAyB;EAC7B,IAAI,SAAwB;EAE5B,MAAM,cAAc;AAClB,OAAI,OAAO,WAAW,EACpB;GAEF,MAAM,QAAQ;AACd,YAAS,EAAE;GACX,MAAM,MAAM;GACZ,MAAM,MAAM;AACZ,aAAU;AACV,YAAS;AAET,OAAI,QAAQ,gBAAgB;IAC1B,IAAI,KAAK,KAAK;IACd,OAAO,MAAM;IACb,QAAQ;IACR,GAAI,QAAQ,QAAQ,EAAE,gBAAgB,KAAK;IAC3C,GAAI,QAAQ,QAAQ,EAAE,eAAe,KAAK;IAC3C,CAAC;;EAGJ,MAAM,2BAA2B;AAC/B,OAAI,SAAS,oBAAoB,SAC/B,QAAO;;EAGX,MAAM,uBAAuB;AAC3B,UAAO;;EAGT,MAAM,OAAO,YAAY;AACvB,OAAI;AAEF,cACE,MAFkB,OAAO,UAEnB,OAAO,EACX,KAAK,OAAO;KACV,MAAM,KAAK,KAAK,KAAK;AACrB,SAAI,YAAY,KACd,WAAU;AAEZ,cAAS;AACT,YAAO,KAAK,KAAK,UAAU,MAAM,CAAC;OAErC,CAAC,IAAI;AAER,iBAAa,YAAY,OAAO,kBAAkB;AAClD,eAAW,iBAAiB,oBAAoB,mBAAmB;AACnE,eAAW,iBAAiB,gBAAgB,eAAe;AAC3D,QAAI,MAAM,oBAAoB;WACxB;AACN,QAAI,MAAM,wCAAwC;;;AAItD,QAAM,CAAC,YAAY,GAEjB;AAEF,eAAa;AACX,UAAO;AACP,aAAU;AACV,OAAI,WACF,eAAc,WAAW;AAE3B,cAAW,oBAAoB,oBAAoB,mBAAmB;AACtE,cAAW,oBAAoB,gBAAgB,eAAe;;;CAGnE"}
@@ -23,10 +23,28 @@ interface InterfereContextValue {
23
23
  }
24
24
  interface InterfereProviderProps extends PropsWithChildren {
25
25
  consent?: ConsentState | undefined;
26
+ /**
27
+ * Auto-install an internal capture boundary around `children` so
28
+ * render-phase React errors are reported even when the host app has no
29
+ * `ErrorBoundary` / `error.tsx` of its own.
30
+ *
31
+ * The internal boundary is transparent: it captures the error and then
32
+ * re-throws so upstream boundaries (the customer's own, Next.js's
33
+ * `error.tsx` / `global-error.tsx`, or React's default unmount) keep full
34
+ * control of what the user sees. Safe to leave enabled.
35
+ *
36
+ * Pass `false` only if you explicitly don't want automatic capture of
37
+ * render-phase errors — for example, if you've already wired
38
+ * {@link reactErrorHandler} into `createRoot()`.
39
+ *
40
+ * @default true
41
+ */
42
+ errorBoundary?: boolean;
26
43
  }
27
44
  declare function InterfereProvider({
28
45
  children,
29
- consent
46
+ consent,
47
+ errorBoundary
30
48
  }: InterfereProviderProps): ReactNode;
31
49
  declare function useInterfere(): InterfereContextValue;
32
50
  declare function useSession(): string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.mts","names":[],"sources":["../src/provider.tsx"],"mappings":";;;;;UAgBU,qBAAA;EACR,OAAA;IACE,GAAA,IAAO,YAAA;IACP,GAAA,CAAI,KAAA,GAAQ,YAAA;EAAA;EAEd,MAAA;IACE,WAAA;IACA,SAAA;EAAA;EAEF,QAAA;IACE,GAAA,IAAO,cAAA;IACP,GAAA,CAAI,MAAA,EAAQ,cAAA,GAAiB,OAAA;EAAA;EAE/B,OAAA;IACE,KAAA;IACA,WAAA;EAAA;AAAA;AAAA,UAMM,sBAAA,SAA+B,iBAAA;EACvC,OAAA,GAAU,YAAA;AAAA;AAAA,iBAGI,iBAAA,CAAA;EACd,QAAA;EACA;AAAA,GACC,sBAAA,GAAyB,SAAA;AAAA,iBAeZ,YAAA,CAAA,GAAgB,qBAAA;AAAA,iBAQhB,UAAA,CAAA"}
1
+ {"version":3,"file":"provider.d.mts","names":[],"sources":["../src/provider.tsx"],"mappings":";;;;;UAiBU,qBAAA;EACR,OAAA;IACE,GAAA,IAAO,YAAA;IACP,GAAA,CAAI,KAAA,GAAQ,YAAA;EAAA;EAEd,MAAA;IACE,WAAA;IACA,SAAA;EAAA;EAEF,QAAA;IACE,GAAA,IAAO,cAAA;IACP,GAAA,CAAI,MAAA,EAAQ,cAAA,GAAiB,OAAA;EAAA;EAE/B,OAAA;IACE,KAAA;IACA,WAAA;EAAA;AAAA;AAAA,UAMM,sBAAA,SAA+B,iBAAA;EACvC,OAAA,GAAU,YAAA;EAfR;;;;;;;;;;;;;AAQW;;;EAwBb,aAAA;AAAA;AAAA,iBAGc,iBAAA,CAAA;EACd,QAAA;EACA,OAAA;EACA;AAAA,GACC,sBAAA,GAAyB,SAAA;AAAA,iBAqBZ,YAAA,CAAA,GAAgB,qBAAA;AAAA,iBAQhB,UAAA,CAAA"}
package/dist/provider.mjs CHANGED
@@ -1,11 +1,12 @@
1
1
  "use client";
2
2
  import { device, identity, session } from "./tracking/api.mjs";
3
3
  import { consent, syncConsent } from "./internal/client.mjs";
4
+ import { CaptureBoundary } from "./internal/capture-boundary.mjs";
4
5
  import { createContext, useContext, useEffect } from "react";
5
6
  import { jsx } from "react/jsx-runtime";
6
7
  //#region src/provider.tsx
7
8
  const InterfereContext = createContext(null);
8
- function InterfereProvider({ children, consent: consent$1 }) {
9
+ function InterfereProvider({ children, consent: consent$1, errorBoundary = true }) {
9
10
  useEffect(() => {
10
11
  syncConsent(consent$1);
11
12
  }, [consent$1]);
@@ -16,7 +17,7 @@ function InterfereProvider({ children, consent: consent$1 }) {
16
17
  identity,
17
18
  session
18
19
  },
19
- children
20
+ children: errorBoundary ? /* @__PURE__ */ jsx(CaptureBoundary, { children }) : children
20
21
  });
21
22
  }
22
23
  function useInterfere() {
@@ -1 +1 @@
1
- {"version":3,"file":"provider.mjs","names":["consent","sdkConsent"],"sources":["../src/provider.tsx"],"sourcesContent":["\"use client\";\n\nimport type { IdentifyParams } from \"@interfere/types/sdk/identify\";\nimport type { ConsentState } from \"@interfere/types/sdk/plugins/manifest\";\n\nimport {\n createContext,\n type PropsWithChildren,\n type ReactNode,\n useContext,\n useEffect,\n} from \"react\";\n\nimport { consent as sdkConsent, syncConsent } from \"./internal/client.js\";\nimport { device, identity, session } from \"./tracking/api.js\";\n\ninterface InterfereContextValue {\n consent: {\n get(): ConsentState | null;\n set(state?: ConsentState): void;\n };\n device: {\n getDeviceId(): string | null;\n getFpHash(): string | null;\n };\n identity: {\n get(): IdentifyParams | null;\n set(params: IdentifyParams): Promise<void>;\n };\n session: {\n getId(): string | null;\n getWindowId(): string | null;\n };\n}\n\nconst InterfereContext = createContext<InterfereContextValue | null>(null);\n\ninterface InterfereProviderProps extends PropsWithChildren {\n consent?: ConsentState | undefined;\n}\n\nexport function InterfereProvider({\n children,\n consent,\n}: InterfereProviderProps): ReactNode {\n useEffect(() => {\n syncConsent(consent);\n }, [consent]);\n\n const value: InterfereContextValue = {\n consent: sdkConsent,\n device,\n identity,\n session,\n };\n\n return <InterfereContext value={value}>{children}</InterfereContext>;\n}\n\nexport function useInterfere(): InterfereContextValue {\n const ctx = useContext(InterfereContext);\n if (!ctx) {\n throw new Error(\"useInterfere must be used within <InterfereProvider>\");\n }\n return ctx;\n}\n\nexport function useSession(): string | null {\n return useInterfere().session.getId();\n}\n"],"mappings":";;;;;;AAmCA,MAAM,mBAAmB,cAA4C,KAAK;AAM1E,SAAgB,kBAAkB,EAChC,UACA,SAAA,aACoC;AACpC,iBAAgB;AACd,cAAYA,UAAQ;IACnB,CAACA,UAAQ,CAAC;AASb,QAAO,oBAAC,kBAAD;EAAkB,OAPY;GAC1BC;GACT;GACA;GACA;GACD;EAEuC;EAA4B,CAAA;;AAGtE,SAAgB,eAAsC;CACpD,MAAM,MAAM,WAAW,iBAAiB;AACxC,KAAI,CAAC,IACH,OAAM,IAAI,MAAM,uDAAuD;AAEzE,QAAO;;AAGT,SAAgB,aAA4B;AAC1C,QAAO,cAAc,CAAC,QAAQ,OAAO"}
1
+ {"version":3,"file":"provider.mjs","names":["consent","sdkConsent"],"sources":["../src/provider.tsx"],"sourcesContent":["\"use client\";\n\nimport type { IdentifyParams } from \"@interfere/types/sdk/identify\";\nimport type { ConsentState } from \"@interfere/types/sdk/plugins/manifest\";\n\nimport {\n createContext,\n type PropsWithChildren,\n type ReactNode,\n useContext,\n useEffect,\n} from \"react\";\n\nimport { CaptureBoundary } from \"./internal/capture-boundary.js\";\nimport { consent as sdkConsent, syncConsent } from \"./internal/client.js\";\nimport { device, identity, session } from \"./tracking/api.js\";\n\ninterface InterfereContextValue {\n consent: {\n get(): ConsentState | null;\n set(state?: ConsentState): void;\n };\n device: {\n getDeviceId(): string | null;\n getFpHash(): string | null;\n };\n identity: {\n get(): IdentifyParams | null;\n set(params: IdentifyParams): Promise<void>;\n };\n session: {\n getId(): string | null;\n getWindowId(): string | null;\n };\n}\n\nconst InterfereContext = createContext<InterfereContextValue | null>(null);\n\ninterface InterfereProviderProps extends PropsWithChildren {\n consent?: ConsentState | undefined;\n /**\n * Auto-install an internal capture boundary around `children` so\n * render-phase React errors are reported even when the host app has no\n * `ErrorBoundary` / `error.tsx` of its own.\n *\n * The internal boundary is transparent: it captures the error and then\n * re-throws so upstream boundaries (the customer's own, Next.js's\n * `error.tsx` / `global-error.tsx`, or React's default unmount) keep full\n * control of what the user sees. Safe to leave enabled.\n *\n * Pass `false` only if you explicitly don't want automatic capture of\n * render-phase errors — for example, if you've already wired\n * {@link reactErrorHandler} into `createRoot()`.\n *\n * @default true\n */\n errorBoundary?: boolean;\n}\n\nexport function InterfereProvider({\n children,\n consent,\n errorBoundary = true,\n}: InterfereProviderProps): ReactNode {\n useEffect(() => {\n syncConsent(consent);\n }, [consent]);\n\n const value: InterfereContextValue = {\n consent: sdkConsent,\n device,\n identity,\n session,\n };\n\n const body = errorBoundary ? (\n <CaptureBoundary>{children}</CaptureBoundary>\n ) : (\n children\n );\n\n return <InterfereContext value={value}>{body}</InterfereContext>;\n}\n\nexport function useInterfere(): InterfereContextValue {\n const ctx = useContext(InterfereContext);\n if (!ctx) {\n throw new Error(\"useInterfere must be used within <InterfereProvider>\");\n }\n return ctx;\n}\n\nexport function useSession(): string | null {\n return useInterfere().session.getId();\n}\n"],"mappings":";;;;;;;AAoCA,MAAM,mBAAmB,cAA4C,KAAK;AAuB1E,SAAgB,kBAAkB,EAChC,UACA,SAAA,WACA,gBAAgB,QACoB;AACpC,iBAAgB;AACd,cAAYA,UAAQ;IACnB,CAACA,UAAQ,CAAC;AAeb,QAAO,oBAAC,kBAAD;EAAkB,OAAO;GAZrBC;GACT;GACA;GACA;GASmC;YANxB,gBACX,oBAAC,iBAAD,EAAkB,UAA2B,CAAA,GAE7C;EAG8D,CAAA;;AAGlE,SAAgB,eAAsC;CACpD,MAAM,MAAM,WAAW,iBAAiB;AACxC,KAAI,CAAC,IACH,OAAM,IAAI,MAAM,uDAAuD;AAEzE,QAAO;;AAGT,SAAgB,aAA4B;AAC1C,QAAO,cAAc,CAAC,QAAQ,OAAO"}
@@ -0,0 +1,63 @@
1
+ //#region src/react-error-handler.d.ts
2
+ /**
3
+ * Matches the shape React 19 passes to `onCaughtError`, `onUncaughtError`,
4
+ * and `onRecoverableError` on `createRoot()` / `hydrateRoot()`.
5
+ *
6
+ * We intentionally keep this permissive — React has iterated on the exact
7
+ * fields of `ErrorInfo` across 19.x minors and we only need `componentStack`.
8
+ */
9
+ interface ReactRootErrorInfo {
10
+ componentStack?: string | null | undefined;
11
+ }
12
+ type RootErrorCallback = (error: unknown, info: ReactRootErrorInfo) => void;
13
+ interface ReactErrorHandlerCallbacks {
14
+ /**
15
+ * Called when a React error boundary caught and handled an error. Interfere
16
+ * reports it with `mechanism.handled: true`. Your callback runs after the
17
+ * capture.
18
+ */
19
+ onCaughtError?: RootErrorCallback;
20
+ /**
21
+ * Called when concurrent React auto-recovered from an error by retrying
22
+ * the render. Reported with `mechanism.synthetic: true` so the agent can
23
+ * deprioritize — these often aren't user-facing bugs.
24
+ */
25
+ onRecoverableError?: RootErrorCallback;
26
+ /**
27
+ * Called when a render-phase error propagated past every boundary and
28
+ * React unmounted (or fell back to its default UI). Reported with
29
+ * `mechanism.handled: false` — this is the "real" uncaught case.
30
+ */
31
+ onUncaughtError?: RootErrorCallback;
32
+ }
33
+ interface ReactErrorHandlerOptions extends ReactErrorHandlerCallbacks {}
34
+ interface ReactRootErrorHandlers {
35
+ onCaughtError: RootErrorCallback;
36
+ onRecoverableError: RootErrorCallback;
37
+ onUncaughtError: RootErrorCallback;
38
+ }
39
+ /**
40
+ * Creates the three error callbacks React 19's `createRoot()` / `hydrateRoot()`
41
+ * accept, wired into Interfere's capture pipeline.
42
+ *
43
+ * ```ts
44
+ * import { createRoot } from "react-dom/client";
45
+ * import { reactErrorHandler } from "@interfere/react/react-error-handler";
46
+ *
47
+ * createRoot(document.getElementById("root")!, reactErrorHandler()).render(
48
+ * <App />
49
+ * );
50
+ * ```
51
+ *
52
+ * Pass your own callbacks to observe errors without replacing the capture
53
+ * behaviour — user callbacks run after Interfere has captured:
54
+ *
55
+ * ```ts
56
+ * reactErrorHandler({
57
+ * onUncaughtError: (err) => myLogger.fatal(err),
58
+ * });
59
+ * ```
60
+ */
61
+ declare function reactErrorHandler(callbacks?: ReactErrorHandlerOptions): ReactRootErrorHandlers;
62
+ //#endregion
63
+ export { ReactErrorHandlerCallbacks, ReactErrorHandlerOptions, reactErrorHandler };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react-error-handler.d.mts","names":[],"sources":["../src/react-error-handler.ts"],"mappings":";;;;;;;AAcgB;UADN,kBAAA;EACR,cAAA;AAAA;AAAA,KAGG,iBAAA,IAAqB,KAAA,WAAgB,IAAA,EAAM,kBAAA;AAAA,UAE/B,0BAAA;EAFyB;;;AAE1C;;EAME,aAAA,GAAgB,iBAAA;EAAA;;;;;EAMhB,kBAAA,GAAqB,iBAAA;EANL;;;;;EAYhB,eAAA,GAAkB,iBAAA;AAAA;AAAA,UAGH,wBAAA,SAAiC,0BAAA;AAAA,UAExC,sBAAA;EACR,aAAA,EAAe,iBAAA;EACf,kBAAA,EAAoB,iBAAA;EACpB,eAAA,EAAiB,iBAAA;AAAA;;;;;;;;;;;;;;;AAyBnB;;;;;;;;iBAAgB,iBAAA,CACd,SAAA,GAAW,wBAAA,GACV,sBAAA"}
@@ -0,0 +1,54 @@
1
+ "use client";
2
+ import { captureReactError } from "./internal/capture.mjs";
3
+ import { MECHANISM_TYPE } from "@interfere/types/sdk/errors";
4
+ //#region src/react-error-handler.ts
5
+ /**
6
+ * Creates the three error callbacks React 19's `createRoot()` / `hydrateRoot()`
7
+ * accept, wired into Interfere's capture pipeline.
8
+ *
9
+ * ```ts
10
+ * import { createRoot } from "react-dom/client";
11
+ * import { reactErrorHandler } from "@interfere/react/react-error-handler";
12
+ *
13
+ * createRoot(document.getElementById("root")!, reactErrorHandler()).render(
14
+ * <App />
15
+ * );
16
+ * ```
17
+ *
18
+ * Pass your own callbacks to observe errors without replacing the capture
19
+ * behaviour — user callbacks run after Interfere has captured:
20
+ *
21
+ * ```ts
22
+ * reactErrorHandler({
23
+ * onUncaughtError: (err) => myLogger.fatal(err),
24
+ * });
25
+ * ```
26
+ */
27
+ function reactErrorHandler(callbacks = {}) {
28
+ return {
29
+ onCaughtError(error, info) {
30
+ if (error instanceof Error) captureReactError(error, info.componentStack, {
31
+ type: MECHANISM_TYPE.react.caughtError,
32
+ handled: true
33
+ });
34
+ callbacks.onCaughtError?.(error, info);
35
+ },
36
+ onUncaughtError(error, info) {
37
+ if (error instanceof Error) captureReactError(error, info.componentStack, {
38
+ type: MECHANISM_TYPE.react.uncaughtError,
39
+ handled: false
40
+ });
41
+ callbacks.onUncaughtError?.(error, info);
42
+ },
43
+ onRecoverableError(error, info) {
44
+ if (error instanceof Error) captureReactError(error, info.componentStack, {
45
+ type: MECHANISM_TYPE.react.recoverableError,
46
+ handled: true,
47
+ synthetic: true
48
+ });
49
+ callbacks.onRecoverableError?.(error, info);
50
+ }
51
+ };
52
+ }
53
+ //#endregion
54
+ export { reactErrorHandler };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react-error-handler.mjs","names":[],"sources":["../src/react-error-handler.ts"],"sourcesContent":["\"use client\";\n\nimport { MECHANISM_TYPE } from \"@interfere/types/sdk/errors\";\n\nimport { captureReactError } from \"./internal/capture.js\";\n\n/**\n * Matches the shape React 19 passes to `onCaughtError`, `onUncaughtError`,\n * and `onRecoverableError` on `createRoot()` / `hydrateRoot()`.\n *\n * We intentionally keep this permissive — React has iterated on the exact\n * fields of `ErrorInfo` across 19.x minors and we only need `componentStack`.\n */\ninterface ReactRootErrorInfo {\n componentStack?: string | null | undefined;\n}\n\ntype RootErrorCallback = (error: unknown, info: ReactRootErrorInfo) => void;\n\nexport interface ReactErrorHandlerCallbacks {\n /**\n * Called when a React error boundary caught and handled an error. Interfere\n * reports it with `mechanism.handled: true`. Your callback runs after the\n * capture.\n */\n onCaughtError?: RootErrorCallback;\n /**\n * Called when concurrent React auto-recovered from an error by retrying\n * the render. Reported with `mechanism.synthetic: true` so the agent can\n * deprioritize — these often aren't user-facing bugs.\n */\n onRecoverableError?: RootErrorCallback;\n /**\n * Called when a render-phase error propagated past every boundary and\n * React unmounted (or fell back to its default UI). Reported with\n * `mechanism.handled: false` — this is the \"real\" uncaught case.\n */\n onUncaughtError?: RootErrorCallback;\n}\n\nexport interface ReactErrorHandlerOptions extends ReactErrorHandlerCallbacks {}\n\ninterface ReactRootErrorHandlers {\n onCaughtError: RootErrorCallback;\n onRecoverableError: RootErrorCallback;\n onUncaughtError: RootErrorCallback;\n}\n\n/**\n * Creates the three error callbacks React 19's `createRoot()` / `hydrateRoot()`\n * accept, wired into Interfere's capture pipeline.\n *\n * ```ts\n * import { createRoot } from \"react-dom/client\";\n * import { reactErrorHandler } from \"@interfere/react/react-error-handler\";\n *\n * createRoot(document.getElementById(\"root\")!, reactErrorHandler()).render(\n * <App />\n * );\n * ```\n *\n * Pass your own callbacks to observe errors without replacing the capture\n * behaviour — user callbacks run after Interfere has captured:\n *\n * ```ts\n * reactErrorHandler({\n * onUncaughtError: (err) => myLogger.fatal(err),\n * });\n * ```\n */\nexport function reactErrorHandler(\n callbacks: ReactErrorHandlerOptions = {}\n): ReactRootErrorHandlers {\n return {\n onCaughtError(error, info) {\n if (error instanceof Error) {\n captureReactError(error, info.componentStack, {\n type: MECHANISM_TYPE.react.caughtError,\n handled: true,\n });\n }\n callbacks.onCaughtError?.(error, info);\n },\n onUncaughtError(error, info) {\n if (error instanceof Error) {\n captureReactError(error, info.componentStack, {\n type: MECHANISM_TYPE.react.uncaughtError,\n handled: false,\n });\n }\n callbacks.onUncaughtError?.(error, info);\n },\n onRecoverableError(error, info) {\n if (error instanceof Error) {\n captureReactError(error, info.componentStack, {\n type: MECHANISM_TYPE.react.recoverableError,\n handled: true,\n synthetic: true,\n });\n }\n callbacks.onRecoverableError?.(error, info);\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAsEA,SAAgB,kBACd,YAAsC,EAAE,EAChB;AACxB,QAAO;EACL,cAAc,OAAO,MAAM;AACzB,OAAI,iBAAiB,MACnB,mBAAkB,OAAO,KAAK,gBAAgB;IAC5C,MAAM,eAAe,MAAM;IAC3B,SAAS;IACV,CAAC;AAEJ,aAAU,gBAAgB,OAAO,KAAK;;EAExC,gBAAgB,OAAO,MAAM;AAC3B,OAAI,iBAAiB,MACnB,mBAAkB,OAAO,KAAK,gBAAgB;IAC5C,MAAM,eAAe,MAAM;IAC3B,SAAS;IACV,CAAC;AAEJ,aAAU,kBAAkB,OAAO,KAAK;;EAE1C,mBAAmB,OAAO,MAAM;AAC9B,OAAI,iBAAiB,MACnB,mBAAkB,OAAO,KAAK,gBAAgB;IAC5C,MAAM,eAAe,MAAM;IAC3B,SAAS;IACT,WAAW;IACZ,CAAC;AAEJ,aAAU,qBAAqB,OAAO,KAAK;;EAE9C"}
@@ -1 +1 @@
1
- {"version":3,"file":"device.mjs","names":[],"sources":["../../src/tracking/device.ts"],"sourcesContent":["import { createLogger } from \"../util/log.js\";\n\nconst log = createLogger(\"device\");\n\nconst LS_KEY = \"interfere:device_id\";\nconst COOKIE_NAME = \"interfere_did\";\nconst COOKIE_MAX_AGE_DAYS = 400;\n\nlet deviceId: string | null = null;\nlet fpHash: string | null = null;\nlet fpPending: Promise<string | null> | null = null;\n\nfunction tryLocalStorage(): Storage | null {\n try {\n const s = globalThis.localStorage;\n const key = \"__interfere_device_probe__\";\n s.setItem(key, \"1\");\n s.removeItem(key);\n return s;\n } catch {\n return null;\n }\n}\n\nfunction getCookie(name: string): string | null {\n if (typeof document === \"undefined\") {\n return null;\n }\n const match = document.cookie\n .split(\"; \")\n .find((c) => c.startsWith(`${name}=`));\n return match ? decodeURIComponent(match.split(\"=\")[1] ?? \"\") : null;\n}\n\nfunction setCookie(name: string, value: string): void {\n if (typeof document === \"undefined\") {\n return;\n }\n const maxAge = COOKIE_MAX_AGE_DAYS * 24 * 60 * 60;\n // biome-ignore lint/suspicious/noDocumentCookie: Cookie Store API is async and not universally supported; synchronous access is required here\n document.cookie = `${name}=${encodeURIComponent(value)};path=/;max-age=${maxAge};SameSite=Lax`;\n}\n\nfunction generateId(): string {\n return crypto.randomUUID();\n}\n\nexport function initDevice(): void {\n if (deviceId) {\n return;\n }\n\n const ls = tryLocalStorage();\n const fromLs = ls?.getItem(LS_KEY) ?? null;\n const fromCookie = getCookie(COOKIE_NAME);\n\n deviceId = fromLs ?? fromCookie ?? generateId();\n\n if (!fromLs && ls) {\n ls.setItem(LS_KEY, deviceId);\n }\n if (!fromCookie) {\n setCookie(COOKIE_NAME, deviceId);\n }\n if (fromLs && !fromCookie) {\n setCookie(COOKIE_NAME, deviceId);\n }\n if (fromCookie && !fromLs && ls) {\n ls.setItem(LS_KEY, deviceId);\n }\n\n log.debug(\"device %s (ls=%s cookie=%s)\", deviceId, !!fromLs, !!fromCookie);\n\n initFpHash();\n}\n\nfunction initFpHash(): void {\n if (fpHash || fpPending) {\n return;\n }\n\n fpPending = (async () => {\n try {\n const FingerprintJS = await import(\"@fingerprintjs/fingerprintjs\");\n const fp = await FingerprintJS.load();\n const result = await fp.get();\n fpHash = result.visitorId;\n log.debug(\"fpHash %s\", fpHash);\n return fpHash;\n } catch {\n log.warn(\"fp hash failed\");\n return null;\n }\n })();\n}\n\nexport function getDeviceId(): string | null {\n return deviceId;\n}\n\nexport function getFpHash(): string | null {\n return fpHash;\n}\n\nexport function whenDeviceReady(): Promise<string | null> {\n if (deviceId) {\n return Promise.resolve(deviceId);\n }\n return Promise.resolve(null);\n}\n\nexport function whenFingerprintReady(): Promise<string | null> {\n if (fpHash) {\n return Promise.resolve(fpHash);\n }\n return fpPending ?? Promise.resolve(null);\n}\n\nexport function resetDevice(): void {\n deviceId = null;\n fpHash = null;\n fpPending = null;\n}\n"],"mappings":";;AAEA,MAAM,MAAM,aAAa,SAAS;AAElC,MAAM,SAAS;AACf,MAAM,cAAc;AACpB,MAAM,sBAAsB;AAE5B,IAAI,WAA0B;AAC9B,IAAI,SAAwB;AAC5B,IAAI,YAA2C;AAE/C,SAAS,kBAAkC;AACzC,KAAI;EACF,MAAM,IAAI,WAAW;EACrB,MAAM,MAAM;AACZ,IAAE,QAAQ,KAAK,IAAI;AACnB,IAAE,WAAW,IAAI;AACjB,SAAO;SACD;AACN,SAAO;;;AAIX,SAAS,UAAU,MAA6B;AAC9C,KAAI,OAAO,aAAa,YACtB,QAAO;CAET,MAAM,QAAQ,SAAS,OACpB,MAAM,KAAK,CACX,MAAM,MAAM,EAAE,WAAW,GAAG,KAAK,GAAG,CAAC;AACxC,QAAO,QAAQ,mBAAmB,MAAM,MAAM,IAAI,CAAC,MAAM,GAAG,GAAG;;AAGjE,SAAS,UAAU,MAAc,OAAqB;AACpD,KAAI,OAAO,aAAa,YACtB;CAEF,MAAM,SAAS,sBAAsB,KAAK,KAAK;AAE/C,UAAS,SAAS,GAAG,KAAK,GAAG,mBAAmB,MAAM,CAAC,kBAAkB,OAAO;;AAGlF,SAAS,aAAqB;AAC5B,QAAO,OAAO,YAAY;;AAG5B,SAAgB,aAAmB;AACjC,KAAI,SACF;CAGF,MAAM,KAAK,iBAAiB;CAC5B,MAAM,SAAS,IAAI,QAAQ,OAAO,IAAI;CACtC,MAAM,aAAa,UAAU,YAAY;AAEzC,YAAW,UAAU,cAAc,YAAY;AAE/C,KAAI,CAAC,UAAU,GACb,IAAG,QAAQ,QAAQ,SAAS;AAE9B,KAAI,CAAC,WACH,WAAU,aAAa,SAAS;AAElC,KAAI,UAAU,CAAC,WACb,WAAU,aAAa,SAAS;AAElC,KAAI,cAAc,CAAC,UAAU,GAC3B,IAAG,QAAQ,QAAQ,SAAS;AAG9B,KAAI,MAAM,+BAA+B,UAAU,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;AAE1E,aAAY;;AAGd,SAAS,aAAmB;AAC1B,KAAI,UAAU,UACZ;AAGF,cAAa,YAAY;AACvB,MAAI;AAIF,aADe,OADJ,OADW,MAAM,OAAO,iCACJ,MAAM,EACb,KAAK,EACb;AAChB,OAAI,MAAM,aAAa,OAAO;AAC9B,UAAO;UACD;AACN,OAAI,KAAK,iBAAiB;AAC1B,UAAO;;KAEP;;AAGN,SAAgB,cAA6B;AAC3C,QAAO;;AAGT,SAAgB,YAA2B;AACzC,QAAO;;AAGT,SAAgB,kBAA0C;AACxD,KAAI,SACF,QAAO,QAAQ,QAAQ,SAAS;AAElC,QAAO,QAAQ,QAAQ,KAAK;;AAG9B,SAAgB,uBAA+C;AAC7D,KAAI,OACF,QAAO,QAAQ,QAAQ,OAAO;AAEhC,QAAO,aAAa,QAAQ,QAAQ,KAAK;;AAG3C,SAAgB,cAAoB;AAClC,YAAW;AACX,UAAS;AACT,aAAY"}
1
+ {"version":3,"file":"device.mjs","names":[],"sources":["../../src/tracking/device.ts"],"sourcesContent":["import { createLogger } from \"../util/log.js\";\n\nconst log = createLogger(\"device\");\n\nconst LS_KEY = \"interfere:device_id\";\nconst COOKIE_NAME = \"interfere_did\";\nconst COOKIE_MAX_AGE_DAYS = 400;\n\nlet deviceId: string | null = null;\nlet fpHash: string | null = null;\nlet fpPending: Promise<string | null> | null = null;\n\nfunction tryLocalStorage(): Storage | null {\n try {\n const s = globalThis.localStorage;\n const key = \"__interfere_device_probe__\";\n s.setItem(key, \"1\");\n s.removeItem(key);\n return s;\n } catch {\n return null;\n }\n}\n\nfunction getCookie(name: string): string | null {\n if (typeof document === \"undefined\") {\n return null;\n }\n const match = document.cookie\n .split(\"; \")\n .find((c) => c.startsWith(`${name}=`));\n return match ? decodeURIComponent(match.split(\"=\")[1] ?? \"\") : null;\n}\n\nfunction setCookie(name: string, value: string): void {\n if (typeof document === \"undefined\") {\n return;\n }\n const maxAge = COOKIE_MAX_AGE_DAYS * 24 * 60 * 60;\n // biome-ignore lint/suspicious/noDocumentCookie: Cookie Store API is async and not universally supported; synchronous access is required here\n document.cookie = `${name}=${encodeURIComponent(value)};path=/;max-age=${maxAge};SameSite=Lax`;\n}\n\nfunction generateId(): string {\n return crypto.randomUUID();\n}\n\nexport function initDevice(): void {\n if (deviceId) {\n return;\n }\n\n const ls = tryLocalStorage();\n const fromLs = ls?.getItem(LS_KEY) ?? null;\n const fromCookie = getCookie(COOKIE_NAME);\n\n deviceId = fromLs ?? fromCookie ?? generateId();\n\n if (!fromLs && ls) {\n ls.setItem(LS_KEY, deviceId);\n }\n if (!fromCookie) {\n setCookie(COOKIE_NAME, deviceId);\n }\n if (fromLs && !fromCookie) {\n setCookie(COOKIE_NAME, deviceId);\n }\n if (fromCookie && !fromLs && ls) {\n ls.setItem(LS_KEY, deviceId);\n }\n\n log.debug(\"device %s (ls=%s cookie=%s)\", deviceId, !!fromLs, !!fromCookie);\n\n initFpHash();\n}\n\nfunction initFpHash(): void {\n if (fpHash || fpPending) {\n return;\n }\n\n fpPending = (async () => {\n try {\n const FingerprintJS = await import(\"@fingerprintjs/fingerprintjs\");\n const fp = await FingerprintJS.load();\n const result = await fp.get();\n fpHash = result.visitorId;\n log.debug(\"fpHash %s\", fpHash);\n return fpHash;\n } catch {\n log.warn(\"fp hash failed\");\n return null;\n }\n })();\n}\n\nexport function getDeviceId(): string | null {\n return deviceId;\n}\n\nexport function getFpHash(): string | null {\n return fpHash;\n}\n\nexport function whenDeviceReady(): Promise<string | null> {\n if (deviceId) {\n return Promise.resolve(deviceId);\n }\n return Promise.resolve(null);\n}\n\nexport function whenFingerprintReady(): Promise<string | null> {\n if (fpHash) {\n return Promise.resolve(fpHash);\n }\n return fpPending ?? Promise.resolve(null);\n}\n\nexport function resetDevice(): void {\n deviceId = null;\n fpHash = null;\n fpPending = null;\n}\n"],"mappings":";;AAEA,MAAM,MAAM,aAAa,SAAS;AAElC,MAAM,SAAS;AACf,MAAM,cAAc;AACpB,MAAM,sBAAsB;AAE5B,IAAI,WAA0B;AAC9B,IAAI,SAAwB;AAC5B,IAAI,YAA2C;AAE/C,SAAS,kBAAkC;AACzC,KAAI;EACF,MAAM,IAAI,WAAW;EACrB,MAAM,MAAM;AACZ,IAAE,QAAQ,KAAK,IAAI;AACnB,IAAE,WAAW,IAAI;AACjB,SAAO;SACD;AACN,SAAO;;;AAIX,SAAS,UAAU,MAA6B;AAC9C,KAAI,OAAO,aAAa,YACtB,QAAO;CAET,MAAM,QAAQ,SAAS,OACpB,MAAM,KAAK,CACX,MAAM,MAAM,EAAE,WAAW,GAAG,KAAK,GAAG,CAAC;AACxC,QAAO,QAAQ,mBAAmB,MAAM,MAAM,IAAI,CAAC,MAAM,GAAG,GAAG;;AAGjE,SAAS,UAAU,MAAc,OAAqB;AACpD,KAAI,OAAO,aAAa,YACtB;CAEF,MAAM,SAAS,sBAAsB,KAAK,KAAK;AAE/C,UAAS,SAAS,GAAG,KAAK,GAAG,mBAAmB,MAAM,CAAC,kBAAkB,OAAO;;AAGlF,SAAS,aAAqB;AAC5B,QAAO,OAAO,YAAY;;AAG5B,SAAgB,aAAmB;AACjC,KAAI,SACF;CAGF,MAAM,KAAK,iBAAiB;CAC5B,MAAM,SAAS,IAAI,QAAQ,OAAO,IAAI;CACtC,MAAM,aAAa,UAAU,YAAY;AAEzC,YAAW,UAAU,cAAc,YAAY;AAE/C,KAAI,CAAC,UAAU,GACb,IAAG,QAAQ,QAAQ,SAAS;AAE9B,KAAI,CAAC,WACH,WAAU,aAAa,SAAS;AAElC,KAAI,UAAU,CAAC,WACb,WAAU,aAAa,SAAS;AAElC,KAAI,cAAc,CAAC,UAAU,GAC3B,IAAG,QAAQ,QAAQ,SAAS;AAG9B,KAAI,MAAM,+BAA+B,UAAU,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;AAE1E,aAAY;;AAGd,SAAS,aAAmB;AAC1B,KAAI,UAAU,UACZ;AAGF,cAAa,YAAY;AACvB,MAAI;AAIF,aAAS,OADY,OADJ,MADW,OAAO,iCACJ,MAAM,EACb,KAAK,EACb;AAChB,OAAI,MAAM,aAAa,OAAO;AAC9B,UAAO;UACD;AACN,OAAI,KAAK,iBAAiB;AAC1B,UAAO;;KAEP;;AAGN,SAAgB,cAA6B;AAC3C,QAAO;;AAGT,SAAgB,YAA2B;AACzC,QAAO;;AAGT,SAAgB,kBAA0C;AACxD,KAAI,SACF,QAAO,QAAQ,QAAQ,SAAS;AAElC,QAAO,QAAQ,QAAQ,KAAK;;AAG9B,SAAgB,uBAA+C;AAC7D,KAAI,OACF,QAAO,QAAQ,QAAQ,OAAO;AAEhC,QAAO,aAAa,QAAQ,QAAQ,KAAK;;AAG3C,SAAgB,cAAoB;AAClC,YAAW;AACX,UAAS;AACT,aAAY"}
@@ -1 +1 @@
1
- {"version":3,"file":"http.mjs","names":[],"sources":["../../src/transport/http.ts"],"sourcesContent":["import type { Envelope } from \"@interfere/types/sdk/envelope\";\n\nimport { session } from \"../tracking/api.js\";\nimport { getDeviceId } from \"../tracking/device.js\";\nimport { createLogger } from \"../util/log.js\";\n\nconst log = createLogger(\"http\");\n\nconst SEND_TIMEOUT_MS = 10_000;\n\nexport interface IngestTarget {\n headers: Headers;\n url: string;\n}\n\nexport interface DeliveryMeta {\n queueDepth: number;\n retryCount: number;\n}\n\nfunction getSdkStack(): string[] | undefined {\n if (typeof window !== \"undefined\") {\n return (window as unknown as Record<string, unknown>)[\n \"__INTERFERE_SDK_STACK__\"\n ] as string[] | undefined;\n }\n return undefined;\n}\n\nexport function buildHeaders(\n base: Headers,\n meta?: DeliveryMeta\n): Record<string, string> {\n const h: Record<string, string> = Object.fromEntries(base.entries());\n\n const stack = getSdkStack();\n h[\"x-interfere-sdk-version\"] = stack?.[0] ?? \"unknown\";\n if (stack && stack.length > 1) {\n h[\"x-interfere-sdk-stack\"] = stack.join(\", \");\n }\n\n const sessionId = session.getId();\n if (sessionId) {\n h[\"x-interfere-session\"] = sessionId;\n }\n\n const windowId = session.getWindowId();\n if (windowId) {\n h[\"x-interfere-window\"] = windowId;\n }\n\n const deviceId = getDeviceId();\n if (deviceId) {\n h[\"x-interfere-device\"] = deviceId;\n }\n\n if (meta) {\n h[\"x-interfere-retry-count\"] = String(meta.retryCount);\n h[\"x-interfere-queue-depth\"] = String(meta.queueDepth);\n }\n\n return h;\n}\n\nexport function hasServiceWorker(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n \"serviceWorker\" in navigator &&\n navigator.serviceWorker.controller !== null\n );\n}\n\nfunction assertOk(response: Response): void {\n if (!response.ok) {\n throw new Error(`ingest responded ${response.status}`);\n }\n}\n\nconst KEEPALIVE_BUDGET_BYTES = 61_440;\nconst MAX_CONCURRENT_KEEPALIVE = 15;\n\nexport class HttpTransport {\n private readonly target: IngestTarget;\n private pendingKeepalive = 0;\n\n constructor(target: IngestTarget) {\n this.target = target;\n }\n\n async send(envelopes: Envelope[], meta?: DeliveryMeta): Promise<void> {\n const body = JSON.stringify(envelopes);\n const headers = buildHeaders(this.target.headers, meta);\n\n if (hasServiceWorker()) {\n log.debug(\"POST %d envelopes via SW\", envelopes.length);\n const res = await fetch(this.target.url, {\n method: \"POST\",\n headers,\n body,\n signal: AbortSignal.timeout(SEND_TIMEOUT_MS),\n });\n assertOk(res);\n return;\n }\n\n const bytes = new TextEncoder().encode(body).byteLength;\n const useKeepalive =\n bytes < KEEPALIVE_BUDGET_BYTES &&\n this.pendingKeepalive < MAX_CONCURRENT_KEEPALIVE;\n\n if (useKeepalive) {\n this.pendingKeepalive++;\n }\n\n log.debug(\n \"POST %d envelopes direct (%d bytes, keepalive=%s)\",\n envelopes.length,\n bytes,\n useKeepalive\n );\n\n try {\n const res = await fetch(this.target.url, {\n method: \"POST\",\n headers,\n body,\n keepalive: useKeepalive,\n signal: AbortSignal.timeout(SEND_TIMEOUT_MS),\n });\n assertOk(res);\n } finally {\n if (useKeepalive) {\n this.pendingKeepalive--;\n }\n }\n }\n}\n"],"mappings":";;;;AAMA,MAAM,MAAM,aAAa,OAAO;AAEhC,MAAM,kBAAkB;AAYxB,SAAS,cAAoC;AAC3C,KAAI,OAAO,WAAW,YACpB,QAAQ,OACN;;AAMN,SAAgB,aACd,MACA,MACwB;CACxB,MAAM,IAA4B,OAAO,YAAY,KAAK,SAAS,CAAC;CAEpE,MAAM,QAAQ,aAAa;AAC3B,GAAE,6BAA6B,QAAQ,MAAM;AAC7C,KAAI,SAAS,MAAM,SAAS,EAC1B,GAAE,2BAA2B,MAAM,KAAK,KAAK;CAG/C,MAAM,YAAY,QAAQ,OAAO;AACjC,KAAI,UACF,GAAE,yBAAyB;CAG7B,MAAM,WAAW,QAAQ,aAAa;AACtC,KAAI,SACF,GAAE,wBAAwB;CAG5B,MAAM,WAAW,aAAa;AAC9B,KAAI,SACF,GAAE,wBAAwB;AAG5B,KAAI,MAAM;AACR,IAAE,6BAA6B,OAAO,KAAK,WAAW;AACtD,IAAE,6BAA6B,OAAO,KAAK,WAAW;;AAGxD,QAAO;;AAGT,SAAgB,mBAA4B;AAC1C,QACE,OAAO,cAAc,eACrB,mBAAmB,aACnB,UAAU,cAAc,eAAe;;AAI3C,SAAS,SAAS,UAA0B;AAC1C,KAAI,CAAC,SAAS,GACZ,OAAM,IAAI,MAAM,oBAAoB,SAAS,SAAS;;AAI1D,MAAM,yBAAyB;AAC/B,MAAM,2BAA2B;AAEjC,IAAa,gBAAb,MAA2B;CACzB;CACA,mBAA2B;CAE3B,YAAY,QAAsB;AAChC,OAAK,SAAS;;CAGhB,MAAM,KAAK,WAAuB,MAAoC;EACpE,MAAM,OAAO,KAAK,UAAU,UAAU;EACtC,MAAM,UAAU,aAAa,KAAK,OAAO,SAAS,KAAK;AAEvD,MAAI,kBAAkB,EAAE;AACtB,OAAI,MAAM,4BAA4B,UAAU,OAAO;AAOvD,YANY,MAAM,MAAM,KAAK,OAAO,KAAK;IACvC,QAAQ;IACR;IACA;IACA,QAAQ,YAAY,QAAQ,gBAAgB;IAC7C,CAAC,CACW;AACb;;EAGF,MAAM,QAAQ,IAAI,aAAa,CAAC,OAAO,KAAK,CAAC;EAC7C,MAAM,eACJ,QAAQ,0BACR,KAAK,mBAAmB;AAE1B,MAAI,aACF,MAAK;AAGP,MAAI,MACF,qDACA,UAAU,QACV,OACA,aACD;AAED,MAAI;AAQF,YAPY,MAAM,MAAM,KAAK,OAAO,KAAK;IACvC,QAAQ;IACR;IACA;IACA,WAAW;IACX,QAAQ,YAAY,QAAQ,gBAAgB;IAC7C,CAAC,CACW;YACL;AACR,OAAI,aACF,MAAK"}
1
+ {"version":3,"file":"http.mjs","names":[],"sources":["../../src/transport/http.ts"],"sourcesContent":["import type { Envelope } from \"@interfere/types/sdk/envelope\";\n\nimport { session } from \"../tracking/api.js\";\nimport { getDeviceId } from \"../tracking/device.js\";\nimport { createLogger } from \"../util/log.js\";\n\nconst log = createLogger(\"http\");\n\nconst SEND_TIMEOUT_MS = 10_000;\n\nexport interface IngestTarget {\n headers: Headers;\n url: string;\n}\n\nexport interface DeliveryMeta {\n queueDepth: number;\n retryCount: number;\n}\n\nfunction getSdkStack(): string[] | undefined {\n if (typeof window !== \"undefined\") {\n return (window as unknown as Record<string, unknown>)[\n \"__INTERFERE_SDK_STACK__\"\n ] as string[] | undefined;\n }\n return;\n}\n\nexport function buildHeaders(\n base: Headers,\n meta?: DeliveryMeta\n): Record<string, string> {\n const h: Record<string, string> = Object.fromEntries(base.entries());\n\n const stack = getSdkStack();\n h[\"x-interfere-sdk-version\"] = stack?.[0] ?? \"unknown\";\n if (stack && stack.length > 1) {\n h[\"x-interfere-sdk-stack\"] = stack.join(\", \");\n }\n\n const sessionId = session.getId();\n if (sessionId) {\n h[\"x-interfere-session\"] = sessionId;\n }\n\n const windowId = session.getWindowId();\n if (windowId) {\n h[\"x-interfere-window\"] = windowId;\n }\n\n const deviceId = getDeviceId();\n if (deviceId) {\n h[\"x-interfere-device\"] = deviceId;\n }\n\n if (meta) {\n h[\"x-interfere-retry-count\"] = String(meta.retryCount);\n h[\"x-interfere-queue-depth\"] = String(meta.queueDepth);\n }\n\n return h;\n}\n\nexport function hasServiceWorker(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n \"serviceWorker\" in navigator &&\n navigator.serviceWorker.controller !== null\n );\n}\n\nfunction assertOk(response: Response): void {\n if (!response.ok) {\n throw new Error(`ingest responded ${response.status}`);\n }\n}\n\nconst KEEPALIVE_BUDGET_BYTES = 61_440;\nconst MAX_CONCURRENT_KEEPALIVE = 15;\n\nexport class HttpTransport {\n private readonly target: IngestTarget;\n private pendingKeepalive = 0;\n\n constructor(target: IngestTarget) {\n this.target = target;\n }\n\n async send(envelopes: Envelope[], meta?: DeliveryMeta): Promise<void> {\n const body = JSON.stringify(envelopes);\n const headers = buildHeaders(this.target.headers, meta);\n\n if (hasServiceWorker()) {\n log.debug(\"POST %d envelopes via SW\", envelopes.length);\n const res = await fetch(this.target.url, {\n method: \"POST\",\n headers,\n body,\n signal: AbortSignal.timeout(SEND_TIMEOUT_MS),\n });\n assertOk(res);\n return;\n }\n\n const bytes = new TextEncoder().encode(body).byteLength;\n const useKeepalive =\n bytes < KEEPALIVE_BUDGET_BYTES &&\n this.pendingKeepalive < MAX_CONCURRENT_KEEPALIVE;\n\n if (useKeepalive) {\n this.pendingKeepalive++;\n }\n\n log.debug(\n \"POST %d envelopes direct (%d bytes, keepalive=%s)\",\n envelopes.length,\n bytes,\n useKeepalive\n );\n\n try {\n const res = await fetch(this.target.url, {\n method: \"POST\",\n headers,\n body,\n keepalive: useKeepalive,\n signal: AbortSignal.timeout(SEND_TIMEOUT_MS),\n });\n assertOk(res);\n } finally {\n if (useKeepalive) {\n this.pendingKeepalive--;\n }\n }\n }\n}\n"],"mappings":";;;;AAMA,MAAM,MAAM,aAAa,OAAO;AAEhC,MAAM,kBAAkB;AAYxB,SAAS,cAAoC;AAC3C,KAAI,OAAO,WAAW,YACpB,QAAQ,OACN;;AAMN,SAAgB,aACd,MACA,MACwB;CACxB,MAAM,IAA4B,OAAO,YAAY,KAAK,SAAS,CAAC;CAEpE,MAAM,QAAQ,aAAa;AAC3B,GAAE,6BAA6B,QAAQ,MAAM;AAC7C,KAAI,SAAS,MAAM,SAAS,EAC1B,GAAE,2BAA2B,MAAM,KAAK,KAAK;CAG/C,MAAM,YAAY,QAAQ,OAAO;AACjC,KAAI,UACF,GAAE,yBAAyB;CAG7B,MAAM,WAAW,QAAQ,aAAa;AACtC,KAAI,SACF,GAAE,wBAAwB;CAG5B,MAAM,WAAW,aAAa;AAC9B,KAAI,SACF,GAAE,wBAAwB;AAG5B,KAAI,MAAM;AACR,IAAE,6BAA6B,OAAO,KAAK,WAAW;AACtD,IAAE,6BAA6B,OAAO,KAAK,WAAW;;AAGxD,QAAO;;AAGT,SAAgB,mBAA4B;AAC1C,QACE,OAAO,cAAc,eACrB,mBAAmB,aACnB,UAAU,cAAc,eAAe;;AAI3C,SAAS,SAAS,UAA0B;AAC1C,KAAI,CAAC,SAAS,GACZ,OAAM,IAAI,MAAM,oBAAoB,SAAS,SAAS;;AAI1D,MAAM,yBAAyB;AAC/B,MAAM,2BAA2B;AAEjC,IAAa,gBAAb,MAA2B;CACzB;CACA,mBAA2B;CAE3B,YAAY,QAAsB;AAChC,OAAK,SAAS;;CAGhB,MAAM,KAAK,WAAuB,MAAoC;EACpE,MAAM,OAAO,KAAK,UAAU,UAAU;EACtC,MAAM,UAAU,aAAa,KAAK,OAAO,SAAS,KAAK;AAEvD,MAAI,kBAAkB,EAAE;AACtB,OAAI,MAAM,4BAA4B,UAAU,OAAO;AAOvD,YAAS,MANS,MAAM,KAAK,OAAO,KAAK;IACvC,QAAQ;IACR;IACA;IACA,QAAQ,YAAY,QAAQ,gBAAgB;IAC7C,CAAC,CACW;AACb;;EAGF,MAAM,QAAQ,IAAI,aAAa,CAAC,OAAO,KAAK,CAAC;EAC7C,MAAM,eACJ,QAAQ,0BACR,KAAK,mBAAmB;AAE1B,MAAI,aACF,MAAK;AAGP,MAAI,MACF,qDACA,UAAU,QACV,OACA,aACD;AAED,MAAI;AAQF,YAAS,MAPS,MAAM,KAAK,OAAO,KAAK;IACvC,QAAQ;IACR;IACA;IACA,WAAW;IACX,QAAQ,YAAY,QAAQ,gBAAgB;IAC7C,CAAC,CACW;YACL;AACR,OAAI,aACF,MAAK"}
@@ -1 +1 @@
1
- {"version":3,"file":"log.d.mts","names":[],"sources":["../../src/util/log.ts"],"mappings":";KAAY,QAAA;AAAA,iBAsBI,WAAA,CAAY,KAAA,EAAO,QAAA;AAAA,iBAInB,WAAA,CAAA,GAAe,QAAA;AAAA,UAKd,MAAA;EACf,KAAA,IAAS,IAAA;EACT,KAAA,IAAS,IAAA;EACT,IAAA,IAAQ,IAAA;EACR,IAAA,IAAQ,IAAA;AAAA;AAAA,iBAeM,YAAA,CAAa,KAAA,WAAgB,MAAA"}
1
+ {"version":3,"file":"log.d.mts","names":[],"sources":["../../src/util/log.ts"],"mappings":";KAAY,QAAA;AAAA,iBA0BI,WAAA,CAAY,KAAA,EAAO,QAAA;AAAA,iBAInB,WAAA,CAAA,GAAe,QAAA;AAAA,UAKd,MAAA;EACf,KAAA,IAAS,IAAA;EACT,KAAA,IAAS,IAAA;EACT,IAAA,IAAQ,IAAA;EACR,IAAA,IAAQ,IAAA;AAAA;AAAA,iBAeM,YAAA,CAAa,KAAA,WAAgB,MAAA"}
package/dist/util/log.mjs CHANGED
@@ -12,7 +12,7 @@ const CONSOLE_FN = {
12
12
  warn: "warn",
13
13
  error: "error"
14
14
  };
15
- let threshold = PRIORITY.warn;
15
+ let threshold = typeof import.meta !== "undefined" && Boolean(import.meta.env?.VITEST) ? PRIORITY.none : PRIORITY.warn;
16
16
  function setLogLevel(level) {
17
17
  threshold = PRIORITY[level];
18
18
  }
@@ -1 +1 @@
1
- {"version":3,"file":"log.mjs","names":[],"sources":["../../src/util/log.ts"],"sourcesContent":["export type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\" | \"none\";\n\nconst PRIORITY: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3,\n none: 4,\n};\n\nconst CONSOLE_FN: Record<\n Exclude<LogLevel, \"none\">,\n \"debug\" | \"info\" | \"warn\" | \"error\"\n> = {\n debug: \"debug\",\n info: \"info\",\n warn: \"warn\",\n error: \"error\",\n};\n\nlet threshold = PRIORITY.warn;\n\nexport function setLogLevel(level: LogLevel): void {\n threshold = PRIORITY[level];\n}\n\nexport function getLogLevel(): LogLevel {\n const entry = Object.entries(PRIORITY).find(([, v]) => v === threshold);\n return (entry?.[0] ?? \"warn\") as LogLevel;\n}\n\nexport interface Logger {\n debug(...args: unknown[]): void;\n error(...args: unknown[]): void;\n info(...args: unknown[]): void;\n warn(...args: unknown[]): void;\n}\n\nfunction emit(\n level: Exclude<LogLevel, \"none\">,\n prefix: string,\n args: unknown[]\n): void {\n if (PRIORITY[level] < threshold) {\n return;\n }\n const fn = CONSOLE_FN[level];\n globalThis.console[fn](prefix, ...args);\n}\n\nexport function createLogger(scope: string): Logger {\n const prefix = `[Interfere:${scope}]`;\n return {\n debug: (...args) => emit(\"debug\", prefix, args),\n info: (...args) => emit(\"info\", prefix, args),\n warn: (...args) => emit(\"warn\", prefix, args),\n error: (...args) => emit(\"error\", prefix, args),\n };\n}\n"],"mappings":";AAEA,MAAM,WAAqC;CACzC,OAAO;CACP,MAAM;CACN,MAAM;CACN,OAAO;CACP,MAAM;CACP;AAED,MAAM,aAGF;CACF,OAAO;CACP,MAAM;CACN,MAAM;CACN,OAAO;CACR;AAED,IAAI,YAAY,SAAS;AAEzB,SAAgB,YAAY,OAAuB;AACjD,aAAY,SAAS;;AAGvB,SAAgB,cAAwB;AAEtC,QADc,OAAO,QAAQ,SAAS,CAAC,MAAM,GAAG,OAAO,MAAM,UAAU,GACvD,MAAM;;AAUxB,SAAS,KACP,OACA,QACA,MACM;AACN,KAAI,SAAS,SAAS,UACpB;CAEF,MAAM,KAAK,WAAW;AACtB,YAAW,QAAQ,IAAI,QAAQ,GAAG,KAAK;;AAGzC,SAAgB,aAAa,OAAuB;CAClD,MAAM,SAAS,cAAc,MAAM;AACnC,QAAO;EACL,QAAQ,GAAG,SAAS,KAAK,SAAS,QAAQ,KAAK;EAC/C,OAAO,GAAG,SAAS,KAAK,QAAQ,QAAQ,KAAK;EAC7C,OAAO,GAAG,SAAS,KAAK,QAAQ,QAAQ,KAAK;EAC7C,QAAQ,GAAG,SAAS,KAAK,SAAS,QAAQ,KAAK;EAChD"}
1
+ {"version":3,"file":"log.mjs","names":[],"sources":["../../src/util/log.ts"],"sourcesContent":["export type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\" | \"none\";\n\nconst PRIORITY: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3,\n none: 4,\n};\n\nconst CONSOLE_FN: Record<\n Exclude<LogLevel, \"none\">,\n \"debug\" | \"info\" | \"warn\" | \"error\"\n> = {\n debug: \"debug\",\n info: \"info\",\n warn: \"warn\",\n error: \"error\",\n};\n\nconst isTestEnv =\n typeof import.meta !== \"undefined\" &&\n Boolean((import.meta as { env?: { VITEST?: unknown } }).env?.VITEST);\n\nlet threshold = isTestEnv ? PRIORITY.none : PRIORITY.warn;\n\nexport function setLogLevel(level: LogLevel): void {\n threshold = PRIORITY[level];\n}\n\nexport function getLogLevel(): LogLevel {\n const entry = Object.entries(PRIORITY).find(([, v]) => v === threshold);\n return (entry?.[0] ?? \"warn\") as LogLevel;\n}\n\nexport interface Logger {\n debug(...args: unknown[]): void;\n error(...args: unknown[]): void;\n info(...args: unknown[]): void;\n warn(...args: unknown[]): void;\n}\n\nfunction emit(\n level: Exclude<LogLevel, \"none\">,\n prefix: string,\n args: unknown[]\n): void {\n if (PRIORITY[level] < threshold) {\n return;\n }\n const fn = CONSOLE_FN[level];\n globalThis.console[fn](prefix, ...args);\n}\n\nexport function createLogger(scope: string): Logger {\n const prefix = `[Interfere:${scope}]`;\n return {\n debug: (...args) => emit(\"debug\", prefix, args),\n info: (...args) => emit(\"info\", prefix, args),\n warn: (...args) => emit(\"warn\", prefix, args),\n error: (...args) => emit(\"error\", prefix, args),\n };\n}\n"],"mappings":";AAEA,MAAM,WAAqC;CACzC,OAAO;CACP,MAAM;CACN,MAAM;CACN,OAAO;CACP,MAAM;CACP;AAED,MAAM,aAGF;CACF,OAAO;CACP,MAAM;CACN,MAAM;CACN,OAAO;CACR;AAMD,IAAI,YAHF,OAAO,OAAO,SAAS,eACvB,QAAS,OAAO,KAAwC,KAAK,OAAO,GAE1C,SAAS,OAAO,SAAS;AAErD,SAAgB,YAAY,OAAuB;AACjD,aAAY,SAAS;;AAGvB,SAAgB,cAAwB;AAEtC,QADc,OAAO,QAAQ,SAAS,CAAC,MAAM,GAAG,OAAO,MAAM,UAChD,GAAG,MAAM;;AAUxB,SAAS,KACP,OACA,QACA,MACM;AACN,KAAI,SAAS,SAAS,UACpB;CAEF,MAAM,KAAK,WAAW;AACtB,YAAW,QAAQ,IAAI,QAAQ,GAAG,KAAK;;AAGzC,SAAgB,aAAa,OAAuB;CAClD,MAAM,SAAS,cAAc,MAAM;AACnC,QAAO;EACL,QAAQ,GAAG,SAAS,KAAK,SAAS,QAAQ,KAAK;EAC/C,OAAO,GAAG,SAAS,KAAK,QAAQ,QAAQ,KAAK;EAC7C,OAAO,GAAG,SAAS,KAAK,QAAQ,QAAQ,KAAK;EAC7C,QAAQ,GAAG,SAAS,KAAK,SAAS,QAAQ,KAAK;EAChD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interfere/react",
3
- "version": "8.1.2",
3
+ "version": "8.1.6",
4
4
  "license": "MIT",
5
5
  "description": "Client-side React SDK for Interfere. Error tracking, session replay, and analytics.",
6
6
  "keywords": [
@@ -35,6 +35,11 @@
35
35
  "types": "./dist/error-boundary.d.mts",
36
36
  "default": "./dist/error-boundary.mjs"
37
37
  },
38
+ "./react-error-handler": {
39
+ "@source": "./src/react-error-handler.ts",
40
+ "types": "./dist/react-error-handler.d.mts",
41
+ "default": "./dist/react-error-handler.mjs"
42
+ },
38
43
  "./internal/client": {
39
44
  "@source": "./src/internal/client.ts",
40
45
  "types": "./dist/internal/client.d.mts",
@@ -52,22 +57,29 @@
52
57
  },
53
58
  "scripts": {
54
59
  "build": "tsdown",
55
- "dev": "tsdown --watch",
56
60
  "test": "vitest run --coverage",
57
61
  "typecheck": "tsc --noEmit --incremental"
58
62
  },
59
63
  "dependencies": {
60
64
  "@fingerprintjs/fingerprintjs": "^5.2.0",
61
65
  "@interfere/constants": "^8.1.2",
62
- "@interfere/types": "^8.1.2",
66
+ "@interfere/types": "^8.1.6",
63
67
  "@ua-parser-js/pro-enterprise": "^2.0.6",
64
68
  "rrweb": "2.0.0-alpha.4",
65
- "uuid": "^13.0.0"
69
+ "uuid": "^14.0.0"
66
70
  },
67
71
  "peerDependencies": {
68
72
  "react": ">=19.2.5",
69
73
  "react-dom": ">=19.2.5"
70
74
  },
75
+ "peerDependenciesMeta": {
76
+ "react": {
77
+ "optional": true
78
+ },
79
+ "react-dom": {
80
+ "optional": true
81
+ }
82
+ },
71
83
  "devDependencies": {
72
84
  "@interfere/test-utils": "^1.0.1",
73
85
  "@interfere/typescript-config": "^8.1.1",
@@ -75,14 +87,13 @@
75
87
  "@types/node": "^24.12.0",
76
88
  "@types/react": "19.2.14",
77
89
  "@types/react-dom": "19.2.3",
78
- "@vitejs/plugin-react": "^6.0.1",
79
- "@vitest/browser": "4.1.4",
80
- "@vitest/browser-playwright": "4.1.4",
81
- "@vitest/coverage-v8": "^4.1.4",
90
+ "@vitest/browser": "4.1.5",
91
+ "@vitest/browser-playwright": "4.1.5",
92
+ "@vitest/coverage-v8": "^4.1.5",
82
93
  "playwright": "^1.59.0",
83
- "tsdown": "^0.21.7",
84
- "typescript": "6.0.2",
85
- "vitest": "^4.1.4",
94
+ "tsdown": "^0.21.10",
95
+ "typescript": "6.0.3",
96
+ "vitest": "^4.1.5",
86
97
  "vitest-browser-react": "2.2.0"
87
98
  }
88
99
  }