@interfere/react 8.1.2 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/error-boundary.d.mts.map +1 -1
- package/dist/error-boundary.mjs +6 -12
- package/dist/error-boundary.mjs.map +1 -1
- package/dist/internal/capture-boundary.d.mts +39 -0
- package/dist/internal/capture-boundary.d.mts.map +1 -0
- package/dist/internal/capture-boundary.mjs +44 -0
- package/dist/internal/capture-boundary.mjs.map +1 -0
- package/dist/internal/capture.d.mts +13 -0
- package/dist/internal/capture.d.mts.map +1 -0
- package/dist/internal/capture.mjs +23 -0
- package/dist/internal/capture.mjs.map +1 -0
- package/dist/internal/client.mjs +2 -1
- package/dist/internal/client.mjs.map +1 -1
- package/dist/internal/config.mjs.map +1 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/errors.d.mts.map +1 -1
- package/dist/plugins/errors.mjs +46 -16
- package/dist/plugins/errors.mjs.map +1 -1
- package/dist/plugins/lib/loader.mjs.map +1 -1
- package/dist/plugins/replay.mjs.map +1 -1
- package/dist/provider.d.mts +19 -1
- package/dist/provider.d.mts.map +1 -1
- package/dist/provider.mjs +3 -2
- package/dist/provider.mjs.map +1 -1
- package/dist/react-error-handler.d.mts +63 -0
- package/dist/react-error-handler.d.mts.map +1 -0
- package/dist/react-error-handler.mjs +54 -0
- package/dist/react-error-handler.mjs.map +1 -0
- package/dist/tracking/device.mjs.map +1 -1
- package/dist/transport/http.mjs.map +1 -1
- package/dist/util/log.d.mts.map +1 -1
- package/dist/util/log.mjs +1 -1
- package/dist/util/log.mjs.map +1 -1
- package/package.json +25 -14
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-boundary.d.mts","names":[],"sources":["../src/error-boundary.tsx"],"mappings":";;;
|
|
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"}
|
package/dist/error-boundary.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 {
|
|
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"}
|
package/dist/internal/client.mjs
CHANGED
|
@@ -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
|
|
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":";;;;;;;;;;;;AAiBA,MAAM,MAAM,aAAa,SAAS;AAElC,SAAgB,cAAc,iBAAsC;AAClE,QAAO,CAAC,GAAI,mBAAmB,EAAE,EAAG,iBAAiB;;AAmBvD,IAAM,SAAN,MAAa;CACX;CACA;CACA;CAEA,YAAY,MAAqB,SAAiB,WAA0B;EAC1E,MAAM,UAAU,gBAAgB;AAChC,YAAU,QAAQ,QAAQ;AAE1B,MAAI,KAAK,cAAc,QAAQ,OAAO,IAAI;AAE1C,OAAK,WAAW;GACd,SAAS,gBAAgB;GACzB,aAAa,aACX,OAAO,YAAY,cAAc,KAAA,IAAY,QAAQ,IAAI,YAC1D;GACD,SAAS,cAAc;GACvB;GACA;GACA,GAAI,KAAK,mBACL,EAAE,iBAAiB,KAAK,kBAAkB,GAC1C,EAAE;GACP;AAED,yBAAuB;
|
|
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":";;;;;;;;;;;;AAiBA,MAAM,MAAM,aAAa,SAAS;AAElC,SAAgB,cAAc,iBAAsC;AAClE,QAAO,CAAC,GAAI,mBAAmB,EAAE,EAAG,iBAAiB;;AAmBvD,IAAM,SAAN,MAAa;CACX;CACA;CACA;CAEA,YAAY,MAAqB,SAAiB,WAA0B;EAC1E,MAAM,UAAU,gBAAgB;AAChC,YAAU,QAAQ,QAAQ;AAE1B,MAAI,KAAK,cAAc,QAAQ,OAAO,IAAI;AAE1C,OAAK,WAAW;GACd,SAAS,gBAAgB;GACzB,aAAa,aACX,OAAO,YAAY,cAAc,KAAA,IAAY,QAAQ,IAAI,YAC1D;GACD,SAAS,cAAc;GACvB;GACA;GACA,GAAI,KAAK,mBACL,EAAE,iBAAiB,KAAK,kBAAkB,GAC1C,EAAE;GACP;AAED,yBAAuB;EAEvB,MAAM,YAAY,IAAI,cAAc,QAAQ,OAAO;AACnD,OAAK,QAAQ,IAAI,WAAW;GAAE;GAAW,GAAG,KAAK;GAAO,CAAC;AACzD,OAAK,MAAM,OAAO;AAElB,OAAK,UAAU,IAAI,cACjB;GACE,UAAU,MAAM,YAAY,KAAK,QAAQ,MAAM,QAAQ;GACvD,oBAAoB,QAAQ,OAAO,IAAI;GACxC,EACD,KAAK,SACL,KAAK,QACN;AAED,OAAK,QAAQ,OAAO;AAEpB,OAAK,kBAAkB,QAAQ,OAAO;;CAGxC,kBAA0B,cAGjB;AACP,QAAM,aAAa,KAAK;GACtB,QAAQ;GACR,SAAS,aAAa,aAAa,QAAQ;GAC3C,QAAQ,YAAY,QAAQ,IAAO;GACpC,CAAC,CACC,MAAM,QAAQ;AACb,OAAI,CAAC,IAAI,GACP;AAEF,UAAO,IAAI,MAAM;IACjB,CACD,MAAM,WAAW;AAChB,OAAI,QAAQ,SAAS;AACnB,SAAK,QAAQ,kBAAkB,OAAO,QAAQ;AAC9C,QAAI,MAAM,wBAAwB;;IAEpC,CACD,YAAY;AACX,OAAI,KAAK,mDAAmD;IAC5D;;CAGN,QAA6B,MAAS,SAAmC;EACvE,MAAM,YAAY,QAAQ,OAAO;AACjC,MAAI,EAAE,aAAa,KAAK,QAAQ,WAAW,KAAK,EAC9C;AAGF,OAAK,MAAM,QAAQ,cAAc,MAAM,SAAS,WAAW,KAAK,SAAS,CAAC;;CAG5E,QAAc;AACZ,OAAK,MAAM,OAAO;;CAGpB,MAAM,UAAyB;AAC7B,QAAM,KAAK,QAAQ,SAAS;AAC5B,YAAU;AACV,OAAK,MAAM,SAAS;;CAGtB,aAAkC;AAChC,SAAO,KAAK,QAAQ,YAAY;;CAGlC,WAAW,OAA4B;AACrC,OAAK,QAAQ,WAAW,MAAM;;CAGhC,eAAqB;AACnB,OAAK,QAAQ,cAAc;;;AAI/B,IAAI,WAA0B;AAE9B,SAAgB,YAAoB;AAClC,KAAI,CAAC,SACH,OAAM,IAAI,MACR,0FACD;AAEH,QAAO;;AAGT,SAAS,yBAAkC;AACzC,KAAI;AACF,MAAI,OAAO,YAAY,eAAe,CAAC,QAAQ,IAC7C,QAAO;AAET,MAAI,QAAQ,IAAI,gBAAgB,aAC9B,QAAO;AAET,MAAI,QAAQ,IAAI,gBAAgB,KAAA,EAC9B,QAAO;AAET,SAAO,CAAC,EACL,WAAuC,iCACxC,QAAQ,IAAI;SAER;AACN,SAAO;;;AAIX,SAAgB,KAAK,OAAsB,EAAE,EAAQ;AACnD,KAAI,SACF;AAGF,KAAI,EAAE,KAAK,WAAW,wBAAwB,GAAG;AAC/C,MAAI,KACF,4FACD;AACD;;CAGF,MAAM,UAAW,WACf;CAGF,MAAM,YAAa,WACjB;AAGF,KAAI,CAAC,SAAS;AACZ,MAAI,MACF,qIAED;AACD;;AAGF,YAAW,IAAI,OAAO,MAAM,SAAS,aAAa,KAAK;AAEvD,KAAI,OAAO,WAAW,YACnB,QAA8C,6BAC7C,cAAc,KAAK,iBAAiB;;AAI1C,eAAsB,QAAuB;AAC3C,KAAI,CAAC,SACH;AAGF,OAAM,SAAS,SAAS;AACxB,YAAW;;AAGb,MAAa,UAAU;CACrB,MAA2B;AACzB,SAAO,UAAU,YAAY,IAAI;;CAGnC,IAAI,OAA4B;AAC9B,YAAU,WAAW,MAAM;;CAE9B;AAED,SAAgB,YAAY,cAA8C;AACxE,KAAI,CAAC,SACH;AAGF,KAAI,cAAc;AAChB,WAAS,WAAW,aAAa;AACjC;;AAGF,UAAS,cAAc;;AAGzB,SAAgB,QAAc;AAC5B,WAAU,OAAO;;;AAInB,SAAgB,SAAe;AAC7B,YAAW"}
|
|
@@ -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
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.mts","names":[],"sources":["../../src/plugins/errors.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"errors.d.mts","names":[],"sources":["../../src/plugins/errors.ts"],"mappings":";;;cA2Ea,YAAA,EAAc,MAAA"}
|
package/dist/plugins/errors.mjs
CHANGED
|
@@ -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 (
|
|
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)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
35
|
-
error: event.reason,
|
|
61
|
+
capture(ctx, {
|
|
62
|
+
error: toError(event.reason),
|
|
36
63
|
mechanism: {
|
|
37
|
-
type:
|
|
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
|
-
|
|
46
|
-
|
|
73
|
+
const error = findErrorArg(args);
|
|
74
|
+
if (error) capture(ctx, {
|
|
75
|
+
error,
|
|
47
76
|
mechanism: {
|
|
48
|
-
type:
|
|
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\
|
|
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,
|
|
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,
|
|
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"}
|
package/dist/provider.d.mts
CHANGED
|
@@ -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;
|
package/dist/provider.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.mts","names":[],"sources":["../src/provider.tsx"],"mappings":";;;;;
|
|
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() {
|
package/dist/provider.mjs.map
CHANGED
|
@@ -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}>{
|
|
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,
|
|
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
|
|
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"}
|
package/dist/util/log.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log.d.mts","names":[],"sources":["../../src/util/log.ts"],"mappings":";KAAY,QAAA;AAAA,
|
|
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
|
}
|
package/dist/util/log.mjs.map
CHANGED
|
@@ -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;
|
|
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": "
|
|
3
|
+
"version": "9.0.0",
|
|
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,37 +57,43 @@
|
|
|
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
|
-
"@interfere/constants": "^
|
|
62
|
-
"@interfere/types": "^
|
|
65
|
+
"@interfere/constants": "^9.0.0",
|
|
66
|
+
"@interfere/types": "^9.0.0",
|
|
63
67
|
"@ua-parser-js/pro-enterprise": "^2.0.6",
|
|
64
68
|
"rrweb": "2.0.0-alpha.4",
|
|
65
|
-
"uuid": "^
|
|
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
|
-
"@interfere/test-utils": "^
|
|
73
|
-
"@interfere/typescript-config": "^
|
|
84
|
+
"@interfere/test-utils": "^9.0.0",
|
|
85
|
+
"@interfere/typescript-config": "^9.0.0",
|
|
74
86
|
"@rrweb/types": "2.0.0-alpha.20",
|
|
75
87
|
"@types/node": "^24.12.0",
|
|
76
88
|
"@types/react": "19.2.14",
|
|
77
89
|
"@types/react-dom": "19.2.3",
|
|
78
|
-
"@
|
|
79
|
-
"@vitest/browser": "4.1.
|
|
80
|
-
"@vitest/
|
|
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.
|
|
84
|
-
"typescript": "6.0.
|
|
85
|
-
"vitest": "^4.1.
|
|
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
|
}
|