@interfere/react 11.0.0-canary.2 → 11.0.0-canary.3
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/internal/capture-boundary.mjs +1 -1
- package/dist/internal/capture.mjs +1 -1
- package/dist/internal/hydration.d.mts +10 -0
- package/dist/internal/hydration.mjs +1 -0
- package/dist/internal/kernel.d.mts +14 -3
- package/dist/internal/kernel.mjs +1 -1
- package/dist/internal/otel/initial-navigation.d.mts +13 -0
- package/dist/internal/otel/initial-navigation.mjs +1 -0
- package/dist/internal/otel/instrumentations.d.mts +25 -5
- package/dist/internal/otel/instrumentations.mjs +1 -1
- package/dist/internal/react-context.d.mts +1 -0
- package/dist/internal/react-context.mjs +1 -1
- package/dist/internal/server-digest.d.mts +9 -0
- package/dist/internal/server-digest.mjs +1 -0
- package/dist/internal/third-party-hosts.d.mts +14 -0
- package/dist/internal/third-party-hosts.mjs +1 -0
- package/dist/internal/wrapper-singleton.d.mts +1 -0
- package/dist/internal/wrapper-singleton.mjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/errors.d.mts +1 -1
- package/dist/plugins/errors.mjs +1 -1
- package/dist/plugins/logs.d.mts +1 -1
- package/dist/plugins/logs.mjs +1 -1
- package/dist/plugins/rage-clicks.d.mts +1 -1
- package/dist/plugins/rage-clicks.mjs +1 -1
- package/dist/plugins/replay/index.d.mts +1 -1
- package/dist/plugins/replay/index.mjs +1 -1
- package/dist/tracking/api.d.mts +1 -0
- package/dist/tracking/api.mjs +1 -1
- package/dist/tracking/session.d.mts +1 -0
- package/dist/tracking/session.mjs +1 -1
- package/package.json +25 -31
- package/dist/api.d.mts.map +0 -1
- package/dist/api.mjs.map +0 -1
- package/dist/error-boundary.d.mts.map +0 -1
- package/dist/error-boundary.mjs.map +0 -1
- package/dist/internal/browser-context.d.mts.map +0 -1
- package/dist/internal/browser-context.mjs.map +0 -1
- package/dist/internal/capture-boundary.d.mts.map +0 -1
- package/dist/internal/capture-boundary.mjs.map +0 -1
- package/dist/internal/capture.d.mts.map +0 -1
- package/dist/internal/capture.mjs.map +0 -1
- package/dist/internal/config.d.mts.map +0 -1
- package/dist/internal/config.mjs.map +0 -1
- package/dist/internal/consent.d.mts.map +0 -1
- package/dist/internal/consent.mjs.map +0 -1
- package/dist/internal/console-patch.d.mts.map +0 -1
- package/dist/internal/console-patch.mjs.map +0 -1
- package/dist/internal/dom/actionable.d.mts.map +0 -1
- package/dist/internal/dom/actionable.mjs.map +0 -1
- package/dist/internal/kernel-registry.d.mts.map +0 -1
- package/dist/internal/kernel-registry.mjs.map +0 -1
- package/dist/internal/kernel.d.mts.map +0 -1
- package/dist/internal/kernel.mjs.map +0 -1
- package/dist/internal/otel/exporter.d.mts.map +0 -1
- package/dist/internal/otel/exporter.mjs.map +0 -1
- package/dist/internal/otel/instrumentations.d.mts.map +0 -1
- package/dist/internal/otel/instrumentations.mjs.map +0 -1
- package/dist/internal/otel/page-scope-context-manager.d.mts.map +0 -1
- package/dist/internal/otel/page-scope-context-manager.mjs.map +0 -1
- package/dist/internal/otel/propagation.d.mts.map +0 -1
- package/dist/internal/otel/propagation.mjs.map +0 -1
- package/dist/internal/otel/provider.d.mts.map +0 -1
- package/dist/internal/otel/provider.mjs.map +0 -1
- package/dist/internal/otel/web-vitals.d.mts.map +0 -1
- package/dist/internal/otel/web-vitals.mjs.map +0 -1
- package/dist/internal/page-lifecycle.d.mts.map +0 -1
- package/dist/internal/page-lifecycle.mjs.map +0 -1
- package/dist/internal/plugin-runtime.d.mts.map +0 -1
- package/dist/internal/plugin-runtime.mjs.map +0 -1
- package/dist/internal/react-context.d.mts.map +0 -1
- package/dist/internal/react-context.mjs.map +0 -1
- package/dist/internal/sw.d.mts.map +0 -1
- package/dist/internal/sw.mjs.map +0 -1
- package/dist/internal/version.d.mts.map +0 -1
- package/dist/internal/version.mjs.map +0 -1
- package/dist/internal/wrapper-singleton.d.mts.map +0 -1
- package/dist/internal/wrapper-singleton.mjs.map +0 -1
- package/dist/package.mjs.map +0 -1
- package/dist/plugins/errors.d.mts.map +0 -1
- package/dist/plugins/errors.mjs.map +0 -1
- package/dist/plugins/lib/loader.d.mts.map +0 -1
- package/dist/plugins/lib/loader.mjs.map +0 -1
- package/dist/plugins/lib/types.d.mts.map +0 -1
- package/dist/plugins/logs.d.mts.map +0 -1
- package/dist/plugins/logs.mjs.map +0 -1
- package/dist/plugins/rage-clicks.d.mts.map +0 -1
- package/dist/plugins/rage-clicks.mjs.map +0 -1
- package/dist/plugins/replay/buffer.d.mts.map +0 -1
- package/dist/plugins/replay/buffer.mjs.map +0 -1
- package/dist/plugins/replay/chunking.d.mts.map +0 -1
- package/dist/plugins/replay/chunking.mjs.map +0 -1
- package/dist/plugins/replay/compressor-host.d.mts.map +0 -1
- package/dist/plugins/replay/compressor-host.mjs.map +0 -1
- package/dist/plugins/replay/delivery.d.mts.map +0 -1
- package/dist/plugins/replay/delivery.mjs.map +0 -1
- package/dist/plugins/replay/index.d.mts.map +0 -1
- package/dist/plugins/replay/index.mjs.map +0 -1
- package/dist/plugins/replay/recorder.d.mts.map +0 -1
- package/dist/plugins/replay/recorder.mjs.map +0 -1
- package/dist/provider.d.mts.map +0 -1
- package/dist/provider.mjs.map +0 -1
- package/dist/react-error-handler.d.mts.map +0 -1
- package/dist/react-error-handler.mjs.map +0 -1
- package/dist/tracking/api.d.mts.map +0 -1
- package/dist/tracking/api.mjs.map +0 -1
- package/dist/tracking/device.d.mts.map +0 -1
- package/dist/tracking/device.mjs.map +0 -1
- package/dist/tracking/geo.d.mts.map +0 -1
- package/dist/tracking/geo.mjs.map +0 -1
- package/dist/tracking/session.d.mts.map +0 -1
- package/dist/tracking/session.mjs.map +0 -1
- package/dist/util/bot.d.mts.map +0 -1
- package/dist/util/bot.mjs.map +0 -1
- package/dist/util/global.d.mts.map +0 -1
- package/dist/util/global.mjs.map +0 -1
- package/dist/util/log.d.mts.map +0 -1
- package/dist/util/log.mjs.map +0 -1
- package/dist/util/stringify.d.mts.map +0 -1
- package/dist/util/stringify.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{captureReactError}from"./capture.mjs";import{InterfereContext}from"./react-context.mjs";import{MECHANISM_TYPE}from"@interfere/types/sdk/errors";import{Component}from"react";var CaptureBoundary=class extends Component{static contextType=InterfereContext;state={error:null};static getDerivedStateFromError(error){return{error}}render(){if(this.state.error)throw captureReactError(this.context?.kernel??null,this.state.error,null,{type:MECHANISM_TYPE.react.captureBoundary,handled:!1}),this.state.error;return this.props.children}};export{CaptureBoundary};
|
|
1
|
+
"use client";import{captureReactError}from"./capture.mjs";import{InterfereContext}from"./react-context.mjs";import{hasServerDigest}from"./server-digest.mjs";import{MECHANISM_TYPE}from"@interfere/types/sdk/errors";import{Component}from"react";var CaptureBoundary=class extends Component{static contextType=InterfereContext;state={error:null};static getDerivedStateFromError(error){return{error}}render(){if(this.state.error)throw hasServerDigest(this.state.error)||captureReactError(this.context?.kernel??null,this.state.error,null,{type:MECHANISM_TYPE.react.captureBoundary,handled:!1}),this.state.error;return this.props.children}};export{CaptureBoundary};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{parseReactComponentStack}from"@interfere/types/sdk/errors";function captureReactError(kernel,error,componentStack,mechanism){kernel
|
|
1
|
+
import{isHydrationError}from"./hydration.mjs";import{parseReactComponentStack}from"@interfere/types/sdk/errors";function captureReactError(kernel,error,componentStack,mechanism){if(!kernel)return;let hydration=isHydrationError(error);kernel.recordException(error,{mechanism:hydration?{...mechanism,synthetic:!1}:mechanism,...hydration?{subtype:`hydration`}:{},...componentStack?{appendFrames:parseReactComponentStack(componentStack)}:{},...error.digest?{errorDigest:error.digest}:{}})}export{captureReactError};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/internal/hydration.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* True when an error is a React hydration mismatch — detected by the stable
|
|
4
|
+
* `react.dev/errors/{418,423,425}` codes (production) or React's hydration
|
|
5
|
+
* message wording (development). String-matches React internals, so it tracks
|
|
6
|
+
* React's phrasing; the numeric error codes are the durable anchor.
|
|
7
|
+
*/
|
|
8
|
+
declare function isHydrationError(error: unknown): boolean;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { isHydrationError };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const HYDRATION_CODE=/(?:Minified React error #|react\.dev\/errors\/)(?:418|423|425)/,HYDRATION_TEXT=/hydration failed|error while hydrating|hydrated but|text content does not match|server[- ]rendered html|did(?:n't| not) match the client/i;function isHydrationError(error){if(!(error instanceof Error))return!1;let message=error.message;return HYDRATION_CODE.test(message)||HYDRATION_TEXT.test(message)}export{isHydrationError};
|
|
@@ -8,7 +8,7 @@ import { NonErrorException } from "@interfere/types/sdk/errors";
|
|
|
8
8
|
import { ConsentState } from "@interfere/types/sdk/plugins/manifest";
|
|
9
9
|
import { ReleaseSlug } from "@interfere/types/releases/slug";
|
|
10
10
|
import { MetricReader } from "@opentelemetry/sdk-metrics";
|
|
11
|
-
import { LogRecordProcessor } from "@opentelemetry/sdk-logs";
|
|
11
|
+
import { LogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs";
|
|
12
12
|
import { SpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
13
13
|
import { IngestedFrame } from "@interfere/types/data/frame";
|
|
14
14
|
import { SessionId } from "@interfere/types/data/session";
|
|
@@ -56,8 +56,10 @@ interface KernelOptions {
|
|
|
56
56
|
interface TracingOptions {
|
|
57
57
|
/**
|
|
58
58
|
* Promote failed `fetch` / `XHR` responses to exception events on the
|
|
59
|
-
* active span.
|
|
60
|
-
*
|
|
59
|
+
* active span. **On by default** — captures same-origin (and
|
|
60
|
+
* `propagateContextUrls`) 4xx + 5xx, deduped by `method+status+path` and
|
|
61
|
+
* rate-limited so a flapping endpoint can't flood. Pass `false` to
|
|
62
|
+
* disable, or `{ statusCodes }` to tune the ranges.
|
|
61
63
|
* See {@link CaptureFailedRequestsOption}.
|
|
62
64
|
*/
|
|
63
65
|
captureFailedRequests?: CaptureFailedRequestsOption;
|
|
@@ -127,6 +129,7 @@ interface KernelConsent {
|
|
|
127
129
|
set(value?: ConsentState): void;
|
|
128
130
|
}
|
|
129
131
|
interface KernelIdentity {
|
|
132
|
+
clear(): void;
|
|
130
133
|
get(): IdentifyParams | null;
|
|
131
134
|
set(params: IdentifyParams): Promise<void>;
|
|
132
135
|
}
|
|
@@ -162,6 +165,12 @@ interface RecordExceptionOpts {
|
|
|
162
165
|
*/
|
|
163
166
|
readonly fallbackFrames?: readonly IngestedFrame[];
|
|
164
167
|
readonly mechanism: ErrorMechanism;
|
|
168
|
+
/**
|
|
169
|
+
* A distinct error sub-classification layered on top of the mechanism —
|
|
170
|
+
* currently `"hydration"` for React hydration mismatches. Lets the agent
|
|
171
|
+
* triage by kind without re-parsing the message.
|
|
172
|
+
*/
|
|
173
|
+
readonly subtype?: string;
|
|
165
174
|
}
|
|
166
175
|
/**
|
|
167
176
|
* The replacement for the old `Client` god-object. Pure construction graph
|
|
@@ -181,6 +190,7 @@ declare class Kernel implements PluginContext {
|
|
|
181
190
|
readonly session: KernelSession;
|
|
182
191
|
private readonly tracker;
|
|
183
192
|
private readonly seen;
|
|
193
|
+
private readonly initialNavigationSeen;
|
|
184
194
|
private runtime;
|
|
185
195
|
private otel;
|
|
186
196
|
private otelDispose;
|
|
@@ -225,6 +235,7 @@ declare class Kernel implements PluginContext {
|
|
|
225
235
|
body: string;
|
|
226
236
|
attributes?: Record<string, string>;
|
|
227
237
|
}): void;
|
|
238
|
+
recordInitialNavigation(loggerProvider: LoggerProvider): void;
|
|
228
239
|
/**
|
|
229
240
|
* Force-flushes the OTel exporters. Returns once they've settled
|
|
230
241
|
* (success or failure) so callers driving unload — page-lifecycle
|
package/dist/internal/kernel.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createLogger}from"../util/log.mjs";import{getGlobal}from"../util/global.mjs";import{resolveTargets}from"./config.mjs";import{SessionTracker}from"../tracking/api.mjs";import{safeStringify}from"../util/stringify.mjs";import{onPageHidden}from"./page-lifecycle.mjs";import{PluginRuntime}from"./plugin-runtime.mjs";import{registerServiceWorker}from"./sw.mjs";import{PRODUCER_VERSION}from"./version.mjs";import{isNonErrorException,shouldDropBrowserExtensionNoise,shouldDropUnresolvableStack,toExceptions}from"@interfere/types/sdk/errors";import{trace}from"@opentelemetry/api";import{releaseSlugSchema}from"@interfere/types/releases/slug";const log=createLogger(`kernel`),TRAILING_SLASH_RE=/\/$/;function buildSdkStack(wrapperVersions){return[...wrapperVersions??[],PRODUCER_VERSION]}function buildExceptionEventAttrs(value,opts){let attrs={"interfere.exception.mechanism":opts.mechanism.type,"interfere.exception.handled":opts.mechanism.handled};return opts.errorDigest&&(attrs[`interfere.error.digest`]=opts.errorDigest),isNonErrorException(value)?(attrs[`exception.type`]=value.type,attrs[`exception.message`]=value.value,attrs[`interfere.exception.kind`]=`non-error`,attrs[`interfere.exception.serialized`]=safeStringify(value.serialized),attrs):(attrs[`exception.type`]=value.name,attrs[`exception.message`]=value.message,attrs[`interfere.exception.kind`]=`error`,value.stack&&(attrs[`exception.stacktrace`]=value.stack),attrs[`interfere.exception.chain`]=safeStringify(toExceptions(value,opts.mechanism)),opts.appendFrames&&opts.appendFrames.length>0&&(attrs[`interfere.exception.appended_frames`]=safeStringify(opts.appendFrames)),opts.fallbackFrames&&opts.fallbackFrames.length>0&&(attrs[`interfere.exception.fallback_frames`]=safeStringify(opts.fallbackFrames)),attrs)}var Kernel=class{consent;identity;device;session;tracker;seen=new WeakSet;runtime=null;otel=null;otelDispose=null;unsubscribePageHidden=null;constructor(tracker){this.tracker=tracker,this.consent={get:()=>this.requireRuntime().getConsent(),set:value=>{let runtime=this.requireRuntime();value?runtime.setConsent(value):runtime.resetConsent()}},this.identity={get:()=>this.tracker.getIdentity(),set:params=>this.tracker.identify(params)},this.device={getDeviceId:()=>this.tracker.getDeviceId(),getFpHash:()=>this.tracker.getFpHash()},this.session={getId:()=>this.tracker.sessionId(),getWindowId:()=>this.tracker.windowId()},this.unsubscribePageHidden=onPageHidden(()=>{this.flush().catch(()=>void 0)})}attachRuntime(runtime){this.runtime=runtime}requireRuntime(){if(!this.runtime)throw Error(`Kernel runtime not attached. createKernel must call attachRuntime before any consent/dispose path runs.`);return this.runtime}getSessionId(){return this.tracker.sessionId()??``}recordException(value,opts){if(!isNonErrorException(value)){if(this.seen.has(value))return;this.seen.add(value)}if(!isNonErrorException(value)){let stacks=[],current=value;for(let depth=0;current&&depth<5;depth+=1)current.stack&&stacks.push(current.stack),current=current.cause instanceof Error?current.cause:void 0;if(shouldDropBrowserExtensionNoise(stacks)||shouldDropUnresolvableStack(stacks))return}let attrs=buildExceptionEventAttrs(value,opts),active=trace.getActiveSpan();if(active?.isRecording()){active.addEvent(`exception`,attrs);return}let span=this.otel?.tracerProvider.getTracer(`@interfere/react/exception`).startSpan(`interfere.exception.capture`);span&&(span.addEvent(`exception`,attrs),span.end())}recordLog(input){this.otel&&this.otel.loggerProvider.getLogger(`@interfere/react`).emit({severityText:input.severityText,severityNumber:input.severityNumber,body:input.body,...input.attributes?{attributes:input.attributes}:{}})}async flush(){this.otel&&await this.otel.flush()}attachOtel(handle,dispose){this.otel=handle,this.otelDispose=dispose}syncConsent(value){let runtime=this.requireRuntime();if(value){runtime.setConsent(value);return}runtime.resetConsent()}async dispose(){this.unsubscribePageHidden?.(),this.unsubscribePageHidden=null,this.runtime&&await this.runtime.dispose(),this.tracker.dispose(),this.otelDispose?.(),this.otelDispose=null,this.otel&&=(await this.otel.shutdown(),null)}};async function createKernel(input={}){let{opts={}}=input,fetcher=opts.fetch??globalThis.fetch.bind(globalThis),targets=resolveTargets();log.info(`collector: %s`,targets.collectorBaseUrl),getGlobal(`__INTERFERE_FORCE_ENABLE__`)&&log.warn(`FORCE_ENABLE is active. Events will be dropped by production collectors. This should only be set during local development.`);let tracker=new SessionTracker({target:targets.session,fetcher});opts._wrapperVersions?.length&&(globalThis.__INTERFERE_SDK_STACK__=opts._wrapperVersions);let kernel=new Kernel(tracker),runtime=new PluginRuntime(kernel,opts.plugins,opts.consent);if(kernel.attachRuntime(runtime),tracker.start(),runtime.start(),opts.tracing!==!1){let tracingOpts=typeof opts.tracing==`object`?opts.tracing:void 0;await wireOtel(kernel,targets,opts._wrapperVersions,{...tracingOpts??{},...opts.serviceName?{serviceName:opts.serviceName}:{},...opts._internalAdditionalSpanProcessors?{additionalSpanProcessors:opts._internalAdditionalSpanProcessors}:{},...opts._internalAdditionalLogRecordProcessors?{additionalLogRecordProcessors:opts._internalAdditionalLogRecordProcessors}:{},...opts._internalAdditionalMetricReaders?{additionalMetricReaders:opts._internalAdditionalMetricReaders}:{}})}return await fetchRemoteConfig(targets.config,fetcher,runtime),registerServiceWorker({onMessage:msg=>recordSwMessage(kernel,msg)}).catch(()=>void 0),kernel}function recordSwMessage(kernel,msg){kernel.recordLog({severityText:`info`,severityNumber:9,body:msg.type,attributes:{"url.full":msg.url,...msg.reason?{"interfere.sw.reason":msg.reason}:{}}})}async function wireOtel(kernel,targets,wrapperVersions,tracing){let{buildOtelProvider,captureWebVitals,readPropagationFromDocument,registerBundledInstrumentations}=await import(`./otel/index.mjs`),collectorUrl=targets.collectorBaseUrl.replace(TRAILING_SLASH_RE,``),releaseSlug=readReleaseSlugFromGlobals(),handle=buildOtelProvider({collectorUrl,authHeaders:targets.ingest.headers,sdkStack:buildSdkStack(wrapperVersions),getSessionId:()=>kernel.session.getId(),deviceId:kernel.device.getDeviceId(),releaseSlug,...tracing.serviceName?{serviceName:tracing.serviceName}:{},...tracing.additionalSpanProcessors?{additionalSpanProcessors:tracing.additionalSpanProcessors}:{},...tracing.additionalLogRecordProcessors?{additionalLogRecordProcessors:tracing.additionalLogRecordProcessors}:{},...tracing.additionalMetricReaders?{additionalMetricReaders:tracing.additionalMetricReaders}:{}}),extracted=readPropagationFromDocument();extracted&&handle.contextManager.setPageScope(extracted);let dispose=registerBundledInstrumentations({tracerProvider:handle.tracerProvider,loggerProvider:handle.loggerProvider,ignoreUrls:[`${collectorUrl}/v2/sink`,targets.config.url,targets.session.url],...tracing.propagateContextUrls?{propagateContextUrls:tracing.propagateContextUrls}:{},...tracing.resolveRoute?{resolveRoute:tracing.resolveRoute}:{},...tracing.captureFailedRequests===void 0?{}:{captureFailedRequests:tracing.captureFailedRequests}});tracing.webVitals!==!1&&captureWebVitals({meter:handle.meterProvider.getMeter(`@interfere/react/web-vitals`),flush:()=>handle.flush(),...tracing.resolveRoute?{resolveRoute:tracing.resolveRoute}:{}}),kernel.attachOtel(handle,dispose)}async function fetchRemoteConfig(configTarget,fetcher,runtime){try{let headers=Object.fromEntries(configTarget.headers.entries()),res=await fetcher(configTarget.url,{method:`GET`,headers,signal:AbortSignal.timeout(1e4)});if(!res.ok)return;let config=await res.json();config?.plugins&&(runtime.applyRemoteConfig(config.plugins),log.debug(`applied remote config`))}catch{log.warn(`remote config fetch failed, using local defaults`)}}function isEnabledByEnvironment(){if(typeof process>`u`||!process.env)return!0;let nodeEnv=process.env.NODE_ENV;return nodeEnv===`production`||nodeEnv===void 0?!0:!!(getGlobal(`__INTERFERE_FORCE_ENABLE__`)||process.env.INTERFERE_FORCE_ENABLE)}function readReleaseSlugFromGlobals(){let value=getGlobal(`__INTERFERE_RELEASE_SLUG__`);return typeof value==`string`?releaseSlugSchema.parse(value):null}export{Kernel,buildSdkStack,createKernel,isEnabledByEnvironment,readReleaseSlugFromGlobals};
|
|
1
|
+
import{createLogger}from"../util/log.mjs";import{getGlobal}from"../util/global.mjs";import{resolveTargets}from"./config.mjs";import{SessionTracker}from"../tracking/api.mjs";import{safeStringify}from"../util/stringify.mjs";import{recordInitialNavigation}from"./otel/initial-navigation.mjs";import{onPageHidden}from"./page-lifecycle.mjs";import{PluginRuntime}from"./plugin-runtime.mjs";import{registerServiceWorker}from"./sw.mjs";import{PRODUCER_VERSION}from"./version.mjs";import{isNonErrorException,shouldDropBrowserExtensionNoise,shouldDropUnresolvableStack,toExceptions}from"@interfere/types/sdk/errors";import{trace}from"@opentelemetry/api";import{releaseSlugSchema}from"@interfere/types/releases/slug";const log=createLogger(`kernel`),TRAILING_SLASH_RE=/\/$/;function buildSdkStack(wrapperVersions){return[...wrapperVersions??[],PRODUCER_VERSION]}function buildExceptionEventAttrs(value,opts){let attrs={"interfere.exception.mechanism":opts.mechanism.type,"interfere.exception.handled":opts.mechanism.handled};return opts.errorDigest&&(attrs[`interfere.error.digest`]=opts.errorDigest),opts.subtype&&(attrs[`interfere.exception.subtype`]=opts.subtype),isNonErrorException(value)?(attrs[`exception.type`]=value.type,attrs[`exception.message`]=value.value,attrs[`interfere.exception.kind`]=`non-error`,attrs[`interfere.exception.serialized`]=safeStringify(value.serialized),attrs):(attrs[`exception.type`]=value.name,attrs[`exception.message`]=value.message,attrs[`interfere.exception.kind`]=`error`,value.stack&&(attrs[`exception.stacktrace`]=value.stack),attrs[`interfere.exception.chain`]=safeStringify(toExceptions(value,opts.mechanism)),opts.appendFrames&&opts.appendFrames.length>0&&(attrs[`interfere.exception.appended_frames`]=safeStringify(opts.appendFrames)),opts.fallbackFrames&&opts.fallbackFrames.length>0&&(attrs[`interfere.exception.fallback_frames`]=safeStringify(opts.fallbackFrames)),attrs)}var Kernel=class{consent;identity;device;session;tracker;seen=new WeakSet;initialNavigationSeen=new Set;runtime=null;otel=null;otelDispose=null;unsubscribePageHidden=null;constructor(tracker){this.tracker=tracker,this.consent={get:()=>this.requireRuntime().getConsent(),set:value=>{let runtime=this.requireRuntime();value?runtime.setConsent(value):runtime.resetConsent()}},this.identity={clear:()=>this.tracker.reset(),get:()=>this.tracker.getIdentity(),set:params=>this.tracker.identify(params)},this.device={getDeviceId:()=>this.tracker.getDeviceId(),getFpHash:()=>this.tracker.getFpHash()},this.session={getId:()=>this.tracker.sessionId(),getWindowId:()=>this.tracker.windowId()},this.unsubscribePageHidden=onPageHidden(()=>{this.flush().catch(()=>void 0)})}attachRuntime(runtime){this.runtime=runtime}requireRuntime(){if(!this.runtime)throw Error(`Kernel runtime not attached. createKernel must call attachRuntime before any consent/dispose path runs.`);return this.runtime}getSessionId(){return this.tracker.sessionId()??``}recordException(value,opts){if(!isNonErrorException(value)){if(this.seen.has(value))return;this.seen.add(value)}if(!isNonErrorException(value)){let stacks=[],current=value;for(let depth=0;current&&depth<5;depth+=1)current.stack&&stacks.push(current.stack),current=current.cause instanceof Error?current.cause:void 0;if(shouldDropBrowserExtensionNoise(stacks)||shouldDropUnresolvableStack(stacks))return}let attrs=buildExceptionEventAttrs(value,opts),active=trace.getActiveSpan();if(active?.isRecording()){active.addEvent(`exception`,attrs);return}let span=this.otel?.tracerProvider.getTracer(`@interfere/react/exception`).startSpan(`interfere.exception.capture`);span&&(span.addEvent(`exception`,attrs),span.end())}recordLog(input){this.otel&&this.otel.loggerProvider.getLogger(`@interfere/react`).emit({severityText:input.severityText,severityNumber:input.severityNumber,body:input.body,...input.attributes?{attributes:input.attributes}:{}})}recordInitialNavigation(loggerProvider){recordInitialNavigation({getSessionId:()=>this.session.getId(),getWindowId:()=>this.session.getWindowId(),loggerProvider,seen:this.initialNavigationSeen})}async flush(){this.otel&&await this.otel.flush()}attachOtel(handle,dispose){this.otel=handle,this.otelDispose=dispose}syncConsent(value){let runtime=this.requireRuntime();if(value){runtime.setConsent(value);return}runtime.resetConsent()}async dispose(){this.unsubscribePageHidden?.(),this.unsubscribePageHidden=null,this.runtime&&await this.runtime.dispose(),this.tracker.dispose(),this.otelDispose?.(),this.otelDispose=null,this.otel&&=(await this.otel.shutdown(),null)}};async function createKernel(input={}){let{opts={}}=input,fetcher=opts.fetch??globalThis.fetch.bind(globalThis),targets=resolveTargets();log.info(`collector: %s`,targets.collectorBaseUrl),getGlobal(`__INTERFERE_FORCE_ENABLE__`)&&log.warn(`FORCE_ENABLE is active. Events will be dropped by production collectors. This should only be set during local development.`);let tracker=new SessionTracker({target:targets.session,fetcher});opts._wrapperVersions?.length&&(globalThis.__INTERFERE_SDK_STACK__=opts._wrapperVersions);let kernel=new Kernel(tracker),runtime=new PluginRuntime(kernel,opts.plugins,opts.consent);if(kernel.attachRuntime(runtime),tracker.start(),runtime.start(),opts.tracing!==!1){let tracingOpts=typeof opts.tracing==`object`?opts.tracing:void 0;await wireOtel(kernel,targets,opts._wrapperVersions,{...tracingOpts??{},...opts.serviceName?{serviceName:opts.serviceName}:{},...opts._internalAdditionalSpanProcessors?{additionalSpanProcessors:opts._internalAdditionalSpanProcessors}:{},...opts._internalAdditionalLogRecordProcessors?{additionalLogRecordProcessors:opts._internalAdditionalLogRecordProcessors}:{},...opts._internalAdditionalMetricReaders?{additionalMetricReaders:opts._internalAdditionalMetricReaders}:{}})}return await fetchRemoteConfig(targets.config,fetcher,runtime),registerServiceWorker({onMessage:msg=>recordSwMessage(kernel,msg)}).catch(()=>void 0),kernel}function recordSwMessage(kernel,msg){kernel.recordLog({severityText:`info`,severityNumber:9,body:msg.type,attributes:{"url.full":msg.url,...msg.reason?{"interfere.sw.reason":msg.reason}:{}}})}async function wireOtel(kernel,targets,wrapperVersions,tracing){let{buildOtelProvider,captureWebVitals,readPropagationFromDocument,registerBundledInstrumentations}=await import(`./otel/index.mjs`),collectorUrl=targets.collectorBaseUrl.replace(TRAILING_SLASH_RE,``),releaseSlug=readReleaseSlugFromGlobals(),handle=buildOtelProvider({collectorUrl,authHeaders:targets.ingest.headers,sdkStack:buildSdkStack(wrapperVersions),getSessionId:()=>kernel.session.getId(),deviceId:kernel.device.getDeviceId(),releaseSlug,...tracing.serviceName?{serviceName:tracing.serviceName}:{},...tracing.additionalSpanProcessors?{additionalSpanProcessors:tracing.additionalSpanProcessors}:{},...tracing.additionalLogRecordProcessors?{additionalLogRecordProcessors:tracing.additionalLogRecordProcessors}:{},...tracing.additionalMetricReaders?{additionalMetricReaders:tracing.additionalMetricReaders}:{}}),extracted=readPropagationFromDocument();extracted&&handle.contextManager.setPageScope(extracted);let dispose=registerBundledInstrumentations({tracerProvider:handle.tracerProvider,loggerProvider:handle.loggerProvider,ignoreUrls:[`${collectorUrl}/v2/sink`,targets.config.url,targets.session.url],...tracing.propagateContextUrls?{propagateContextUrls:tracing.propagateContextUrls}:{},...tracing.resolveRoute?{resolveRoute:tracing.resolveRoute}:{},...tracing.captureFailedRequests===void 0?{}:{captureFailedRequests:tracing.captureFailedRequests}});tracing.webVitals!==!1&&captureWebVitals({meter:handle.meterProvider.getMeter(`@interfere/react/web-vitals`),flush:()=>handle.flush(),...tracing.resolveRoute?{resolveRoute:tracing.resolveRoute}:{}}),kernel.attachOtel(handle,dispose),kernel.recordInitialNavigation(handle.loggerProvider)}async function fetchRemoteConfig(configTarget,fetcher,runtime){try{let headers=Object.fromEntries(configTarget.headers.entries()),res=await fetcher(configTarget.url,{method:`GET`,headers,signal:AbortSignal.timeout(1e4)});if(!res.ok)return;let config=await res.json();config?.plugins&&(runtime.applyRemoteConfig(config.plugins),log.debug(`applied remote config`))}catch{log.warn(`remote config fetch failed, using local defaults`)}}function isEnabledByEnvironment(){if(typeof process>`u`||!process.env)return!0;let nodeEnv=process.env.NODE_ENV;return nodeEnv===`production`||nodeEnv===void 0?!0:!!(getGlobal(`__INTERFERE_FORCE_ENABLE__`)||process.env.INTERFERE_FORCE_ENABLE)}function readReleaseSlugFromGlobals(){let value=getGlobal(`__INTERFERE_RELEASE_SLUG__`);return typeof value==`string`?releaseSlugSchema.parse(value):null}export{Kernel,buildSdkStack,createKernel,isEnabledByEnvironment,readReleaseSlugFromGlobals};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { LoggerProvider } from "@opentelemetry/sdk-logs";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/otel/initial-navigation.d.ts
|
|
4
|
+
interface InitialNavigationInput {
|
|
5
|
+
getLocationHref?(): string | undefined;
|
|
6
|
+
getSessionId(): string | null;
|
|
7
|
+
getWindowId(): string | null;
|
|
8
|
+
loggerProvider: LoggerProvider;
|
|
9
|
+
seen: Set<string>;
|
|
10
|
+
}
|
|
11
|
+
declare function recordInitialNavigation(input: InitialNavigationInput): void;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { InitialNavigationInput, recordInitialNavigation };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function recordInitialNavigation(input){let url=input.getLocationHref?input.getLocationHref():globalThis.location?.href;if(!url)return;let sessionId=input.getSessionId();if(!sessionId)return;let key=`${sessionId}:${input.getWindowId()??``}:${url}`;input.seen.has(key)||(input.seen.add(key),input.loggerProvider.getLogger(`@opentelemetry/instrumentation-browser-navigation`).emit({severityNumber:0,attributes:{"browser.navigation.type":`pageload`,"url.full":url}}))}export{recordInitialNavigation};
|
|
@@ -14,15 +14,35 @@ type FetchSpanRequest = Request | RequestInfo | RequestInit | URL;
|
|
|
14
14
|
*/
|
|
15
15
|
type HttpStatusCodeRange = number | readonly [number, number];
|
|
16
16
|
interface CaptureFailedRequestsOptions {
|
|
17
|
-
/** Defaults to `[[
|
|
17
|
+
/** Defaults to `[[400, 599]]` (4xx + 5xx) when not specified. */
|
|
18
18
|
statusCodes?: readonly HttpStatusCodeRange[];
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Promote tracked failed fetch/XHR responses to captured exception events.
|
|
22
|
+
* On by default (same-origin 4xx + 5xx, deduped + rate-limited); `false`
|
|
23
|
+
* disables, `{ statusCodes }` tunes the matched ranges.
|
|
24
|
+
*/
|
|
21
25
|
type CaptureFailedRequestsOption = boolean | CaptureFailedRequestsOptions;
|
|
22
|
-
declare function
|
|
26
|
+
declare function resolveFailureRanges(opt: CaptureFailedRequestsOption | undefined): readonly HttpStatusCodeRange[] | null;
|
|
27
|
+
/**
|
|
28
|
+
* Per-instrumentation gate that keeps failed-request capture low-noise:
|
|
29
|
+
* dedupes by `method+status+path` (so a flapping endpoint is captured once)
|
|
30
|
+
* and rate-limits to {@link FAILURE_RATE_MAX} captures per
|
|
31
|
+
* {@link FAILURE_RATE_WINDOW_MS} as a flood backstop. State lives in the
|
|
32
|
+
* closure — one gate per kernel instance, no module-level mutable state.
|
|
33
|
+
*/
|
|
34
|
+
declare function createFailureGate(): (key: string) => boolean;
|
|
35
|
+
type NetworkErrorKind = "abort" | "network" | "timeout";
|
|
36
|
+
declare function classifyNetworkError(message: string): NetworkErrorKind;
|
|
37
|
+
declare function applyFetchSpanAttrs(span: Span, request: FetchSpanRequest, result: Response | FetchErrorShape, failureRanges: readonly HttpStatusCodeRange[] | null, isTrackedUrl: (url: string) => boolean, resolveRoute: ((pathname: string) => string | undefined) | undefined, gate?: (key: string) => boolean): void;
|
|
38
|
+
declare function applyXhrSpanAttrs(span: Span, xhr: Pick<XMLHttpRequest, "responseURL" | "status">, failureRanges: readonly HttpStatusCodeRange[] | null, isTrackedUrl: (url: string) => boolean, gate?: (key: string) => boolean): void;
|
|
23
39
|
declare const _testOnlyApplyFetchSpanAttrs: typeof applyFetchSpanAttrs;
|
|
40
|
+
declare const _testOnlyApplyXhrSpanAttrs: typeof applyXhrSpanAttrs;
|
|
41
|
+
declare const _testOnlyResolveFailureRanges: typeof resolveFailureRanges;
|
|
42
|
+
declare const _testOnlyCreateFailureGate: typeof createFailureGate;
|
|
43
|
+
declare const _testOnlyClassifyNetworkError: typeof classifyNetworkError;
|
|
24
44
|
interface InstrumentationsInput {
|
|
25
|
-
/** See {@link CaptureFailedRequestsOption}.
|
|
45
|
+
/** See {@link CaptureFailedRequestsOption}. On by default. */
|
|
26
46
|
captureFailedRequests?: CaptureFailedRequestsOption;
|
|
27
47
|
/**
|
|
28
48
|
* URLs the SDK exempts from `fetch` and `XHR` instrumentation —
|
|
@@ -62,4 +82,4 @@ interface InstrumentationsInput {
|
|
|
62
82
|
*/
|
|
63
83
|
declare function registerBundledInstrumentations(input: InstrumentationsInput): () => void;
|
|
64
84
|
//#endregion
|
|
65
|
-
export { CaptureFailedRequestsOption, CaptureFailedRequestsOptions, HttpStatusCodeRange, InstrumentationsInput, _testOnlyApplyFetchSpanAttrs, registerBundledInstrumentations };
|
|
85
|
+
export { CaptureFailedRequestsOption, CaptureFailedRequestsOptions, HttpStatusCodeRange, InstrumentationsInput, _testOnlyApplyFetchSpanAttrs, _testOnlyApplyXhrSpanAttrs, _testOnlyClassifyNetworkError, _testOnlyCreateFailureGate, _testOnlyResolveFailureRanges, registerBundledInstrumentations };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{describeActionable,isActionable}from"../dom/actionable.mjs";import{MECHANISM_TYPE}from"@interfere/types/sdk/errors";import{registerInstrumentations}from"@opentelemetry/instrumentation";import{BrowserNavigationInstrumentation,defaultSanitizeUrl}from"@opentelemetry/instrumentation-browser-navigation";import{DocumentLoadInstrumentation}from"@opentelemetry/instrumentation-document-load";import{FetchInstrumentation}from"@opentelemetry/instrumentation-fetch";import{LongTaskInstrumentation}from"@opentelemetry/instrumentation-long-task";import{UserInteractionInstrumentation}from"@opentelemetry/instrumentation-user-interaction";import{XMLHttpRequestInstrumentation}from"@opentelemetry/instrumentation-xml-http-request";const ATTR_HTTP_ROUTE=`http.route`,ATTR_URL_FULL=`url.full`,ATTR_URL_PATH=`url.path`,DEFAULT_FAILURE_RANGES=[[
|
|
1
|
+
import{THIRD_PARTY_IGNORE}from"../third-party-hosts.mjs";import{describeActionable,isActionable}from"../dom/actionable.mjs";import{MECHANISM_TYPE}from"@interfere/types/sdk/errors";import{registerInstrumentations}from"@opentelemetry/instrumentation";import{BrowserNavigationInstrumentation,defaultSanitizeUrl}from"@opentelemetry/instrumentation-browser-navigation";import{DocumentLoadInstrumentation}from"@opentelemetry/instrumentation-document-load";import{FetchInstrumentation}from"@opentelemetry/instrumentation-fetch";import{LongTaskInstrumentation}from"@opentelemetry/instrumentation-long-task";import{UserInteractionInstrumentation}from"@opentelemetry/instrumentation-user-interaction";import{XMLHttpRequestInstrumentation}from"@opentelemetry/instrumentation-xml-http-request";const ATTR_HTTP_ROUTE=`http.route`,ATTR_URL_FULL=`url.full`,ATTR_URL_PATH=`url.path`,DEFAULT_FAILURE_RANGES=[[400,599]];function resolveFailureRanges(opt){if(opt===!1)return null;if(opt===void 0||opt===!0)return DEFAULT_FAILURE_RANGES;let codes=opt.statusCodes;return codes&&codes.length>0?codes:DEFAULT_FAILURE_RANGES}function createFailureGate(){let seen=new Set,windowStart=0,windowCount=0;return key=>{if(seen.has(key))return!1;let now=Date.now();return now-windowStart>6e4&&(windowStart=now,windowCount=0),windowCount>=20?!1:(seen.size>=100&&seen.clear(),seen.add(key),windowCount+=1,!0)}}function failureKey(method,discriminator,url){let path;try{path=new URL(url).pathname}catch{path=url}return`${method} ${discriminator} ${path}`}function statusMatches(status,ranges){for(let range of ranges)if(typeof range==`number`){if(status===range)return!0}else if(status>=range[0]&&status<=range[1])return!0;return!1}function recordHttpFailure(span,status,method,url,mechanism){span.addEvent(`exception`,{"exception.type":`HttpResponseError`,"exception.message":`HTTP ${status} ${method} ${url}`,"interfere.exception.mechanism":mechanism,"interfere.exception.handled":!1,"interfere.exception.kind":`error`})}function maybeCaptureFetchFailure(span,result,failureRanges,method,url,gate){if(!failureRanges)return;let status=result?.status;typeof status!=`number`||status<=0||!statusMatches(status,failureRanges)||gate&&!gate(failureKey(method,status,url))||recordHttpFailure(span,status,method,url,MECHANISM_TYPE.browser.fetchFailedResponse)}const NETWORK_TIMEOUT_RE=/timeout|timed out/i,NETWORK_ABORT_RE=/abort/i;function classifyNetworkError(message){return NETWORK_TIMEOUT_RE.test(message)?`timeout`:NETWORK_ABORT_RE.test(message)?`abort`:`network`}function recordNetworkFailure(span,kind,method,url,mechanism){span.addEvent(`exception`,{"exception.type":`NetworkError`,"exception.message":`Network ${kind} ${method} ${url}`,"interfere.exception.mechanism":mechanism,"interfere.exception.handled":!1,"interfere.exception.kind":`error`,"interfere.exception.subtype":kind})}function maybeCaptureNetworkFailure(span,result,failureRanges,method,url,gate){if(!failureRanges)return;let message=result.message;if(typeof message!=`string`||message.length===0)return;let kind=classifyNetworkError(message);gate&&!gate(failureKey(method,kind,url))||recordNetworkFailure(span,kind,method,url,MECHANISM_TYPE.browser.fetchNetworkError)}function applyFetchSpanAttrs(span,request,result,failureRanges,isTrackedUrl,resolveRoute,gate){let url=resolveFetchUrl(span,request,result);if(!url)return;let parsed;try{parsed=new URL(url)}catch{return}span.setAttribute(ATTR_URL_PATH,parsed.pathname);let tracked=isTrackedUrl(url),method=resolveFetchMethod(request);if(tracked){let route=resolveRoute?.(parsed.pathname);route&&span.setAttribute(ATTR_HTTP_ROUTE,route),span.updateName(`${method} ${route??parsed.pathname}`),result instanceof Response?maybeCaptureFetchFailure(span,result,failureRanges,method,url,gate):maybeCaptureNetworkFailure(span,result,failureRanges,method,url,gate)}}function applyXhrSpanAttrs(span,xhr,failureRanges,isTrackedUrl,gate){if(!failureRanges)return;let url=xhr.responseURL;if(!(url&&isTrackedUrl(url)))return;let status=xhr.status;if(typeof status==`number`){if(status===0){(!gate||gate(failureKey(`XHR`,`network`,url)))&&recordNetworkFailure(span,`network`,`XHR`,url,MECHANISM_TYPE.browser.xhrNetworkError);return}statusMatches(status,failureRanges)&&(gate&&!gate(failureKey(`XHR`,status,url))||recordHttpFailure(span,status,`XHR`,url,MECHANISM_TYPE.browser.xhrFailedResponse))}}function resolveFetchUrl(span,request,result){return responseUrl(result)??requestUrl(request)??spanUrlFull(span)}function isReadableSpan(span){return`attributes`in span}function spanUrlFull(span){if(!isReadableSpan(span))return null;let url=span.attributes[ATTR_URL_FULL]??span.attributes[`http.url`];return typeof url==`string`?url:null}function responseUrl(result){return result instanceof Response&&result.url?result.url:null}function requestUrl(request){return typeof request==`string`?request:readStringProp(request,`url`)??readStringProp(request,`href`)}function resolveFetchMethod(request){return readStringProp(request,`method`)??`GET`}function readStringProp(value,key){if(!(value&&typeof value==`object`&&key in value))return null;let prop=value[key];return typeof prop==`string`?prop:null}const _testOnlyApplyFetchSpanAttrs=applyFetchSpanAttrs,_testOnlyApplyXhrSpanAttrs=applyXhrSpanAttrs,_testOnlyResolveFailureRanges=resolveFailureRanges,_testOnlyCreateFailureGate=createFailureGate,_testOnlyClassifyNetworkError=classifyNetworkError,SEMCONV_STABILITY_OPT_IN=`http,database,messaging,gen_ai_latest_experimental`,NEVER_MATCH=/^$/,ESCAPE_RE=/[.*+?^${}()|[\]\\]/g;function sameOriginPattern(){if(typeof window>`u`)return NEVER_MATCH;let{protocol,host}=window.location;return RegExp(`^${protocol}//${host.replace(ESCAPE_RE,`\\$&`)}`)}function stampInteractionAttrs(eventType,element,span){let desc=describeActionable(element);span.setAttribute(`ui.event_type`,eventType),span.setAttribute(`ui.target.tag`,desc.tag),desc.id&&span.setAttribute(`ui.target.id`,desc.id),desc.name&&span.setAttribute(`ui.target.name`,desc.name),desc.role&&span.setAttribute(`ui.target.role`,desc.role),desc.ariaLabel&&span.setAttribute(`ui.target.aria_label`,desc.ariaLabel),desc.text&&span.setAttribute(`ui.target.text`,desc.text)}function stampUrlAttrs(span,resolveRoute){if(typeof window>`u`)return;let pathname=window.location.pathname,route=resolveRoute?.(pathname);span.setAttribute(ATTR_URL_PATH,route??pathname),span.setAttribute(ATTR_URL_FULL,window.location.href),route&&span.setAttribute(ATTR_HTTP_ROUTE,route)}function registerBundledInstrumentations(input){let{tracerProvider,loggerProvider,ignoreUrls,propagateContextUrls=[]}=input,resolveRoute=input.resolveRoute,origin=sameOriginPattern(),fetchIgnore=[...ignoreUrls,...THIRD_PARTY_IGNORE],failureRanges=resolveFailureRanges(input.captureFailedRequests),failureGate=createFailureGate();function isTrackedUrl(url){return origin.test(url)||propagateContextUrls.some(re=>re instanceof RegExp?re.test(url):url.includes(re))}return registerInstrumentations({tracerProvider,...loggerProvider?{loggerProvider}:{},instrumentations:[new FetchInstrumentation({propagateTraceHeaderCorsUrls:[origin,...propagateContextUrls],ignoreUrls:fetchIgnore,semconvStabilityOptIn:SEMCONV_STABILITY_OPT_IN,measureRequestSize:!0,applyCustomAttributesOnSpan:(span,request,result)=>{applyFetchSpanAttrs(span,request,result,failureRanges,isTrackedUrl,resolveRoute,failureGate)}}),new XMLHttpRequestInstrumentation({propagateTraceHeaderCorsUrls:[origin,...propagateContextUrls],ignoreUrls:fetchIgnore,semconvStabilityOptIn:SEMCONV_STABILITY_OPT_IN,applyCustomAttributesOnSpan:(span,xhr)=>{applyXhrSpanAttrs(span,xhr,failureRanges,isTrackedUrl,failureGate)}}),new DocumentLoadInstrumentation({semconvStabilityOptIn:SEMCONV_STABILITY_OPT_IN,applyCustomAttributesOnSpan:{resourceFetch:(span,entry)=>{let name=entry?.name;if(typeof name==`string`)try{let url=new URL(name),isCrossOrigin=typeof window<`u`&&url.origin!==window.location.origin;span.updateName(isCrossOrigin?`resource: ${url.host}${url.pathname}`:`resource: ${url.pathname}`),span.setAttribute(ATTR_URL_FULL,name),span.setAttribute(ATTR_URL_PATH,url.pathname),span.setAttribute(`server.address`,url.host)}catch{return}}}}),new BrowserNavigationInstrumentation({sanitizeUrl:defaultSanitizeUrl}),new UserInteractionInstrumentation({eventNames:[`click`,`submit`],shouldPreventSpanCreation:(eventType,element,span)=>eventType===`click`&&!isActionable(element)?!0:(stampInteractionAttrs(eventType,element,span),stampUrlAttrs(span,resolveRoute),!1)}),new LongTaskInstrumentation({observerCallback:(span,info)=>{stampUrlAttrs(span,resolveRoute);let containerType=info.longtaskEntry.attribution[0]?.containerType;containerType&&span.setAttribute(`longtask.container_type`,containerType)}})]})}export{_testOnlyApplyFetchSpanAttrs,_testOnlyApplyXhrSpanAttrs,_testOnlyClassifyNetworkError,_testOnlyCreateFailureGate,_testOnlyResolveFailureRanges,registerBundledInstrumentations};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{createContext}from"react";const NULL_CONTEXT_VALUE={consent:{get:()=>null,set:()=>void 0},device:{getDeviceId:()=>null,getFpHash:()=>null},identity:{get:()=>null,set:()=>Promise.resolve()},kernel:null,session:{getId:()=>null,getWindowId:()=>null}},InterfereContext=createContext(null);export{InterfereContext,NULL_CONTEXT_VALUE};
|
|
1
|
+
"use client";import{createContext}from"react";const NULL_CONTEXT_VALUE={consent:{get:()=>null,set:()=>void 0},device:{getDeviceId:()=>null,getFpHash:()=>null},identity:{clear:()=>void 0,get:()=>null,set:()=>Promise.resolve()},kernel:null,session:{getId:()=>null,getWindowId:()=>null}},InterfereContext=createContext(null);export{InterfereContext,NULL_CONTEXT_VALUE};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/internal/server-digest.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* True when a thrown value carries a Next.js server `digest` — it originated on
|
|
4
|
+
* the server and was already captured there (`onRequestError`). Client capture
|
|
5
|
+
* sites skip these so one server error isn't recorded again on the client.
|
|
6
|
+
*/
|
|
7
|
+
declare function hasServerDigest(value: unknown): boolean;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { hasServerDigest };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function hasServerDigest(value){return typeof value==`object`&&!!value&&typeof value.digest==`string`}export{hasServerDigest};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/internal/third-party-hosts.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Hosts/paths whose client-side fetch / XHR / resource loads we never
|
|
4
|
+
* surface: common analytics / ads / auth / replay vendors that otherwise
|
|
5
|
+
* turn into orphan-span and resource-error noise. Conservative and
|
|
6
|
+
* customer-generic — app-specific noise belongs in the customer's own
|
|
7
|
+
* `ignoreUrls` config, not hardcoded into a public SDK.
|
|
8
|
+
*
|
|
9
|
+
* Dependency-free on purpose so the error-only bundle can reuse it without
|
|
10
|
+
* pulling in the OTel SDK that `internal/otel/*` carries.
|
|
11
|
+
*/
|
|
12
|
+
declare const THIRD_PARTY_IGNORE: readonly RegExp[];
|
|
13
|
+
//#endregion
|
|
14
|
+
export { THIRD_PARTY_IGNORE };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const THIRD_PARTY_IGNORE=[/(?:^|\/\/)clerk\./,/\.clerk\./,/\.sentry\./,/\.intercom\./,/\.posthog\./,/\.googletagmanager\.com/,/\.google-analytics\.com/,/\.googleapis\.com/,/\.doubleclick\.net/,/\.facebook\.com/,/\.fbcdn\.net/,/\.analytics\.google\.com/,/px\.ads\.linkedin\.com/,/[?&]_rsc=/];export{THIRD_PARTY_IGNORE};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{registerKernel,unregisterKernel}from"./kernel-registry.mjs";import{createKernel,isEnabledByEnvironment}from"./kernel.mjs";import{isBotUserAgent}from"../util/bot.mjs";function createWrapperSingleton(input){let kernel=null,pending=null,listeners=new Set;function emit(){for(let listener of listeners)listener()}return{init(opts={}){return kernel?Promise.resolve(kernel):pending||(!(opts.enabled??isEnabledByEnvironment())||isBotUserAgent()?Promise.resolve(null):(pending=createKernel({opts:{...opts,_wrapperVersions:[input.producerVersion]}}).then(k=>(kernel=k,k&®isterKernel(k),emit(),k)).finally(()=>{pending=null}),pending))},getKernel(){if(!kernel)throw Error(`Interfere SDK not initialized. Call init() from your ${input.initEntryName} entrypoint.`);return kernel},getKernelOrNull(){return kernel},async close(){if(!kernel)return;let previous=kernel;await previous.dispose(),kernel=null,unregisterKernel(previous),emit()},subscribeToKernel(listener){return listeners.add(listener),()=>{listeners.delete(listener)}},consent:{get:()=>kernel?.consent.get()??null,set:value=>kernel?.consent.set(value)},identity:{get:()=>kernel?.identity.get()??null,set:params=>kernel?.identity.set(params)??Promise.resolve()}}}export{createWrapperSingleton};
|
|
1
|
+
import{registerKernel,unregisterKernel}from"./kernel-registry.mjs";import{createKernel,isEnabledByEnvironment}from"./kernel.mjs";import{isBotUserAgent}from"../util/bot.mjs";function createWrapperSingleton(input){let kernel=null,pending=null,listeners=new Set;function emit(){for(let listener of listeners)listener()}return{init(opts={}){return kernel?Promise.resolve(kernel):pending||(!(opts.enabled??isEnabledByEnvironment())||isBotUserAgent()?Promise.resolve(null):(pending=createKernel({opts:{...opts,_wrapperVersions:[input.producerVersion]}}).then(k=>(kernel=k,k&®isterKernel(k),emit(),k)).finally(()=>{pending=null}),pending))},getKernel(){if(!kernel)throw Error(`Interfere SDK not initialized. Call init() from your ${input.initEntryName} entrypoint.`);return kernel},getKernelOrNull(){return kernel},async close(){if(!kernel)return;let previous=kernel;await previous.dispose(),kernel=null,unregisterKernel(previous),emit()},subscribeToKernel(listener){return listeners.add(listener),()=>{listeners.delete(listener)}},consent:{get:()=>kernel?.consent.get()??null,set:value=>kernel?.consent.set(value)},identity:{clear:()=>kernel?.identity.clear(),get:()=>kernel?.identity.get()??null,set:params=>kernel?.identity.set(params)??Promise.resolve()}}}export{createWrapperSingleton};
|
package/dist/package.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var name=`@interfere/react`,version=`11.0.0-canary.
|
|
1
|
+
var name=`@interfere/react`,version=`11.0.0-canary.3`;export{name,version};
|
package/dist/plugins/errors.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{onConsoleCall}from"../internal/console-patch.mjs";import{MECHANISM_TYPE,isNonErrorException,toException}from"@interfere/types/sdk/errors";function capture(ctx,opts){ctx.recordException(opts.error,{mechanism:opts.mechanism,...opts.fallbackFrame&&!isNonErrorException(opts.error)?{fallbackFrames:[opts.fallbackFrame]}:{}})}function findErrorArg(args){
|
|
1
|
+
import{hasServerDigest}from"../internal/server-digest.mjs";import{onConsoleCall}from"../internal/console-patch.mjs";import{THIRD_PARTY_IGNORE}from"../internal/third-party-hosts.mjs";import{MECHANISM_TYPE,isNonErrorException,toException}from"@interfere/types/sdk/errors";function capture(ctx,opts){ctx.recordException(opts.error,{mechanism:opts.mechanism,...opts.fallbackFrame&&!isNonErrorException(opts.error)?{fallbackFrames:[opts.fallbackFrame]}:{}})}function findErrorArg(args){return args.find(arg=>arg instanceof Error)??null}function resourceError(target){return target instanceof HTMLImageElement||target instanceof HTMLScriptElement?target.src?{tag:target.localName,url:target.src}:null:target instanceof HTMLLinkElement&&target.href?{tag:target.localName,url:target.href}:null}function isIgnoredResourceUrl(url){return THIRD_PARTY_IGNORE.some(pattern=>pattern.test(url))}function createResourceGate(){let seen=new Set,windowStart=0,windowCount=0;return url=>{if(seen.has(url))return!1;let now=Date.now();return now-windowStart>6e4&&(windowStart=now,windowCount=0),windowCount>=20?!1:(seen.size>=100&&seen.clear(),seen.add(url),windowCount+=1,!0)}}const errorsPlugin={name:`errors`,setup(ctx){let originalOnError=globalThis.onerror,originalStackTraceLimit=Error.stackTraceLimit;Error.stackTraceLimit<50&&(Error.stackTraceLimit=50),globalThis.onerror=(msg,source,line,col,error)=>{if(error instanceof Error&&!hasServerDigest(error)){let fallbackFrame=typeof source==`string`?{id:`fallback-0`,file:source,...typeof line==`number`?{line}:{},...typeof col==`number`?{column:col}:{}}:null;capture(ctx,{error,mechanism:{type:MECHANISM_TYPE.browser.onerror,handled:!1},...fallbackFrame?{fallbackFrame}:{}})}return typeof originalOnError==`function`?originalOnError.call(globalThis,msg,source,line,col,error):!1};let onUnhandledRejection=event=>{hasServerDigest(event.reason)||capture(ctx,{error:toException(event.reason),mechanism:{type:MECHANISM_TYPE.browser.onunhandledrejection,handled:!1,...event.reason instanceof Error?{}:{synthetic:!0}}})},seenCspReports=new Set,onSecurityPolicyViolation=event=>{let key=`${event.violatedDirective}|${event.blockedURI}`;seenCspReports.has(key)||seenCspReports.size>=5||(seenCspReports.add(key),capture(ctx,{error:{kind:`non-error`,type:`SecurityPolicyViolation`,value:`CSP ${event.violatedDirective} blocked ${event.blockedURI||`inline`}`,serialized:{violatedDirective:event.violatedDirective,blockedURI:event.blockedURI,sourceFile:event.sourceFile,lineNumber:event.lineNumber,columnNumber:event.columnNumber,disposition:event.disposition}},mechanism:{type:MECHANISM_TYPE.browser.securityPolicyViolation,handled:!1}}))},resourceGate=createResourceGate(),onResourceError=event=>{let resource=resourceError(event.target);!resource||isIgnoredResourceUrl(resource.url)||!resourceGate(resource.url)||capture(ctx,{error:{kind:`non-error`,type:`ResourceLoadError`,value:`Failed to load ${resource.tag}: ${resource.url}`,serialized:{url:resource.url}},mechanism:{type:MECHANISM_TYPE.browser.resourceError,handled:!1}})},supportsEventTarget=typeof globalThis.addEventListener==`function`&&typeof globalThis.removeEventListener==`function`;supportsEventTarget&&(globalThis.addEventListener(`unhandledrejection`,onUnhandledRejection),globalThis.addEventListener(`securitypolicyviolation`,onSecurityPolicyViolation),globalThis.addEventListener(`error`,onResourceError,!0));let unsubscribeConsole=onConsoleCall((level,args)=>{if(level!==`error`)return;let error=findErrorArg(args);error&&!hasServerDigest(error)&&capture(ctx,{error,mechanism:{type:MECHANISM_TYPE.browser.consoleError,handled:!0}})});return()=>{Error.stackTraceLimit=originalStackTraceLimit,globalThis.onerror=originalOnError,supportsEventTarget&&(globalThis.removeEventListener(`unhandledrejection`,onUnhandledRejection),globalThis.removeEventListener(`securitypolicyviolation`,onSecurityPolicyViolation),globalThis.removeEventListener(`error`,onResourceError,!0)),unsubscribeConsole()}}};export{errorsPlugin as default};
|
package/dist/plugins/logs.d.mts
CHANGED
package/dist/plugins/logs.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{activeKernel}from"../internal/kernel-registry.mjs";import{onConsoleCall}from"../internal/console-patch.mjs";import{safeStringify}from"../util/stringify.mjs";import{SeverityNumber}from"@opentelemetry/api-logs";const LEVEL_TO_SEVERITY={debug:{number:SeverityNumber.DEBUG,text:`DEBUG`},info:{number:SeverityNumber.INFO,text:`INFO`},log:{number:SeverityNumber.INFO,text:`INFO`},warn:{number:SeverityNumber.WARN,text:`WARN`},error:{number:SeverityNumber.ERROR,text:`ERROR`}},logsPlugin={name:`logs`,setup(){return onConsoleCall((level,args)=>{if(args.some(a=>a instanceof Error))return;let kernel=activeKernel();if(!kernel)return;let sev=LEVEL_TO_SEVERITY[level];kernel.recordLog({severityText:sev.text,severityNumber:sev.number,body:args.length===1?safeStringify(args[0]):args.map(safeStringify).join(` `),attributes:{"console.level":level}})})}};export{logsPlugin as default
|
|
1
|
+
import{activeKernel}from"../internal/kernel-registry.mjs";import{onConsoleCall}from"../internal/console-patch.mjs";import{safeStringify}from"../util/stringify.mjs";import{SeverityNumber}from"@opentelemetry/api-logs";const LEVEL_TO_SEVERITY={debug:{number:SeverityNumber.DEBUG,text:`DEBUG`},info:{number:SeverityNumber.INFO,text:`INFO`},log:{number:SeverityNumber.INFO,text:`INFO`},warn:{number:SeverityNumber.WARN,text:`WARN`},error:{number:SeverityNumber.ERROR,text:`ERROR`}},logsPlugin={name:`logs`,setup(){return onConsoleCall((level,args)=>{if(args.some(a=>a instanceof Error))return;let kernel=activeKernel();if(!kernel)return;let sev=LEVEL_TO_SEVERITY[level];kernel.recordLog({severityText:sev.text,severityNumber:sev.number,body:args.length===1?safeStringify(args[0]):args.map(safeStringify).join(` `),attributes:{"console.level":level}})})}};export{logsPlugin as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{trace}from"@opentelemetry/api";function distance(a,b){return Math.sqrt((a.x-b.x)**2+(a.y-b.y)**2)}function selectorFor(el){if(!el)return`unknown`;if(el.id)return`#${el.id}`;let classes=[...el.classList].slice(0,3).join(`.`),tag=el.tagName.toLowerCase();return classes?`${tag}.${classes}`:tag}const rageClicksPlugin={name:`rage-clicks`,setup(){let clicks=[],onClick=event=>{let now=Date.now();for(clicks.push({x:event.clientX,y:event.clientY,ts:now,target:event.target instanceof Element?event.target:null});clicks.length>0&&now-(clicks[0]?.ts??0)>800;)clicks.shift();if(clicks.length<3)return;let anchor=clicks[0];if(!anchor)return;let clustered=clicks.filter(c=>distance(anchor,c)<=30);if(clustered.length<3)return;let last=clustered.at(-1);last&&(trace.getTracer(`@interfere/react/rage-clicks`).startSpan(`rage_click`,{attributes:{"ui.event_type":`rage_click`,"ui.rage_click.count":clustered.length,"ui.rage_click.time_window_ms":last.ts-anchor.ts,"ui.rage_click.selector":selectorFor(last.target),"ui.rage_click.text":last.target?.textContent?.trim().slice(0,120)??``,"ui.rage_click.x":last.x,"ui.rage_click.y":last.y}}).end(),clicks.length=0)};return document.addEventListener(`click`,onClick,{capture:!0}),()=>{document.removeEventListener(`click`,onClick,{capture:!0})}}};export{rageClicksPlugin as default
|
|
1
|
+
import{trace}from"@opentelemetry/api";function distance(a,b){return Math.sqrt((a.x-b.x)**2+(a.y-b.y)**2)}function selectorFor(el){if(!el)return`unknown`;if(el.id)return`#${el.id}`;let classes=[...el.classList].slice(0,3).join(`.`),tag=el.tagName.toLowerCase();return classes?`${tag}.${classes}`:tag}const rageClicksPlugin={name:`rage-clicks`,setup(){let clicks=[],onClick=event=>{let now=Date.now();for(clicks.push({x:event.clientX,y:event.clientY,ts:now,target:event.target instanceof Element?event.target:null});clicks.length>0&&now-(clicks[0]?.ts??0)>800;)clicks.shift();if(clicks.length<3)return;let anchor=clicks[0];if(!anchor)return;let clustered=clicks.filter(c=>distance(anchor,c)<=30);if(clustered.length<3)return;let last=clustered.at(-1);last&&(trace.getTracer(`@interfere/react/rage-clicks`).startSpan(`rage_click`,{attributes:{"ui.event_type":`rage_click`,"ui.rage_click.count":clustered.length,"ui.rage_click.time_window_ms":last.ts-anchor.ts,"ui.rage_click.selector":selectorFor(last.target),"ui.rage_click.text":last.target?.textContent?.trim().slice(0,120)??``,"ui.rage_click.x":last.x,"ui.rage_click.y":last.y}}).end(),clicks.length=0)};return document.addEventListener(`click`,onClick,{capture:!0}),()=>{document.removeEventListener(`click`,onClick,{capture:!0})}}};export{rageClicksPlugin as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createLogger}from"../../util/log.mjs";import{resolveTargets}from"../../internal/config.mjs";import{onPageHidden,onPageShown}from"../../internal/page-lifecycle.mjs";import{splitReplayEvents,toReplayChunkPayload}from"./chunking.mjs";import{createReplayBuffer}from"./buffer.mjs";import{createCompressor}from"./compressor-host.mjs";import{deliverChunk}from"./delivery.mjs";import{startReplayRecorder}from"./recorder.mjs";const log=createLogger(`replay`),replayPlugin={name:`replay`,setup(ctx){let targets=resolveTargets(),uploadBaseUrl=targets.collectorBaseUrl,authHeaders=Object.fromEntries(targets.ingest.headers.entries()),recorder=null,flushTimer=null,unsubscribeHidden=null,unsubscribeShown=null,isHidden=!1,compressor=createCompressor();function flush(opts){let drained=buffer.drain();if(drained.length===0)return;let{chunks,droppedEventBytes}=splitReplayEvents(drained);for(let bytes of droppedEventBytes)log.warn(`dropping replay event of ~%d bytes: too large to upload`,bytes);for(let group of chunks)deliverChunk({ctx,uploadBaseUrl,authHeaders,compressor,payload:toReplayChunkPayload(group),skipCompression:opts?.skipCompression??!1}).catch(error=>log.warn(`replay chunk dropped: %o`,error))}let buffer=createReplayBuffer({softLimitBytes:4194304,onSoftLimit:flush}),onHide=()=>{isHidden=!0,flush({skipCompression:!0})},onShow=()=>{isHidden&&(isHidden=!1,recorder?.takeFullSnapshot())};return(async()=>{recorder=await startReplayRecorder(event=>{isHidden||buffer.push(event)}),recorder&&(flushTimer=setInterval(flush,1e4),unsubscribeHidden=onPageHidden(onHide),unsubscribeShown=onPageShown(onShow),log.debug(`recording started`))})(),()=>{flush(),recorder?.stop(),flushTimer&&clearInterval(flushTimer),unsubscribeHidden?.(),unsubscribeShown?.(),compressor.dispose()}}};export{replayPlugin as default
|
|
1
|
+
import{createLogger}from"../../util/log.mjs";import{resolveTargets}from"../../internal/config.mjs";import{onPageHidden,onPageShown}from"../../internal/page-lifecycle.mjs";import{splitReplayEvents,toReplayChunkPayload}from"./chunking.mjs";import{createReplayBuffer}from"./buffer.mjs";import{createCompressor}from"./compressor-host.mjs";import{deliverChunk}from"./delivery.mjs";import{startReplayRecorder}from"./recorder.mjs";const log=createLogger(`replay`),replayPlugin={name:`replay`,setup(ctx){let targets=resolveTargets(),uploadBaseUrl=targets.collectorBaseUrl,authHeaders=Object.fromEntries(targets.ingest.headers.entries()),recorder=null,flushTimer=null,unsubscribeHidden=null,unsubscribeShown=null,isHidden=!1,compressor=createCompressor();function flush(opts){let drained=buffer.drain();if(drained.length===0)return;let{chunks,droppedEventBytes}=splitReplayEvents(drained);for(let bytes of droppedEventBytes)log.warn(`dropping replay event of ~%d bytes: too large to upload`,bytes);for(let group of chunks)deliverChunk({ctx,uploadBaseUrl,authHeaders,compressor,payload:toReplayChunkPayload(group),skipCompression:opts?.skipCompression??!1}).catch(error=>log.warn(`replay chunk dropped: %o`,error))}let buffer=createReplayBuffer({softLimitBytes:4194304,onSoftLimit:flush}),onHide=()=>{isHidden=!0,flush({skipCompression:!0})},onShow=()=>{isHidden&&(isHidden=!1,recorder?.takeFullSnapshot())};return(async()=>{recorder=await startReplayRecorder(event=>{isHidden||buffer.push(event)}),recorder&&(flushTimer=setInterval(flush,1e4),unsubscribeHidden=onPageHidden(onHide),unsubscribeShown=onPageShown(onShow),log.debug(`recording started`))})(),()=>{flush(),recorder?.stop(),flushTimer&&clearInterval(flushTimer),unsubscribeHidden?.(),unsubscribeShown?.(),compressor.dispose()}}};export{replayPlugin as default};
|
package/dist/tracking/api.d.mts
CHANGED
package/dist/tracking/api.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createLogger}from"../util/log.mjs";import{appendPathBeforeQuery}from"../internal/config.mjs";import{DeviceManager}from"./device.mjs";import{GeoDetector}from"./geo.mjs";import{SessionManager}from"./session.mjs";const log=createLogger(`tracking`);var SessionTracker=class{target;fetcher;device;geo;mgr=null;currentIdentity=null;identifiedSessionId=null;syncedSessionId=null;syncAttemptMs=0;generation=0;constructor(opts){this.target=opts.target,this.fetcher=opts.fetcher??globalThis.fetch.bind(globalThis),this.device=opts.device??new DeviceManager,this.geo=opts.geo??new GeoDetector}start(){this.device.init(),this.geo.detect(),this.mgr=new SessionManager(id=>{this.onRotate(id).catch(()=>void 0)})}sessionId(){let id=this.mgr?.getSessionId()??null;return id&&this.ensureSynced(id),id}windowId(){return this.mgr?.getWindowId()??null}getDeviceId(){return this.device.getDeviceId()}getFpHash(){return this.device.getFpHash()}headers(){let h={},sid=this.mgr?.getSessionId()??null;sid&&(h[`x-interfere-session`]=sid);let wid=this.mgr?.getWindowId()??null;wid&&(h[`x-interfere-window`]=wid);let did=this.device.getDeviceId();return did&&(h[`x-interfere-device`]=did),h}getIdentity(){return this.currentIdentity}async identify(params){if(!this.mgr)return;let sessionId=this.mgr.getSessionId();if(this.identifiedSessionId===sessionId){log.debug(`skipped, already identified for session %s`,sessionId);return}this.currentIdentity=params,this.identifiedSessionId=sessionId;let gen=this.generation,[fpHash,country]=await Promise.all([this.device.whenFingerprintReady(),this.geo.detect()]),deviceId=this.device.getDeviceId();if(gen!==this.generation){this.identifiedSessionId=null;return}log.info(`POST session %s → identify`,sessionId),this.fetcher(appendPathBeforeQuery(this.target.url,`/identify`),{method:`POST`,headers:this.requestHeaders(),body:JSON.stringify({sessionId,deviceId,fpHash,...params,...country&&{country}}),keepalive:!0,signal:AbortSignal.timeout(1e4)}).catch(()=>{this.identifiedSessionId=null,log.warn(`identify failed for session %s`,sessionId)})}clearIdentity(){this.currentIdentity=null,this.identifiedSessionId=null}dispose(){this.generation+=1,this.clearIdentity(),this.syncedSessionId=null,this.syncAttemptMs=0,this.mgr=null}requestHeaders(){return{...Object.fromEntries(this.target.headers.entries()),...this.headers()}}ensureSynced(sessionId){this.syncedSessionId!==sessionId&&(Date.now()-this.syncAttemptMs<5e3||this.syncSession(sessionId,this.device.getDeviceId(),this.device.getFpHash()))}syncSession(sessionId,deviceId,fpHash){this.syncAttemptMs=Date.now(),this.syncedSessionId=sessionId,this.fetcher(this.target.url,{method:`POST`,headers:this.requestHeaders(),body:JSON.stringify({sessionId,deviceId,fpHash}),keepalive:!0,signal:AbortSignal.timeout(1e4)}).then(res=>{res.ok||(this.syncedSessionId=null)}).catch(()=>{this.syncedSessionId=null,log.warn(`session sync failed, will retry`)})}async onRotate(sessionId){this.syncedSessionId=null,this.syncAttemptMs=Date.now();let gen=this.generation,fpHash=await this.device.whenFingerprintReady(),deviceId=this.device.getDeviceId();gen===this.generation&&(log.debug(`POST session %s (device=%s fp=%s)`,sessionId,deviceId??`pending`,fpHash??`none`),this.syncSession(sessionId,deviceId,fpHash))}};export{SessionTracker};
|
|
1
|
+
import{createLogger}from"../util/log.mjs";import{appendPathBeforeQuery}from"../internal/config.mjs";import{DeviceManager}from"./device.mjs";import{GeoDetector}from"./geo.mjs";import{SessionManager}from"./session.mjs";const log=createLogger(`tracking`);var SessionTracker=class{target;fetcher;device;geo;mgr=null;currentIdentity=null;identifiedSessionId=null;syncedSessionId=null;syncAttemptMs=0;generation=0;constructor(opts){this.target=opts.target,this.fetcher=opts.fetcher??globalThis.fetch.bind(globalThis),this.device=opts.device??new DeviceManager,this.geo=opts.geo??new GeoDetector}start(){this.device.init(),this.geo.detect(),this.mgr=new SessionManager(id=>{this.onRotate(id).catch(()=>void 0)})}sessionId(){let id=this.mgr?.getSessionId()??null;return id&&this.ensureSynced(id),id}windowId(){return this.mgr?.getWindowId()??null}getDeviceId(){return this.device.getDeviceId()}getFpHash(){return this.device.getFpHash()}headers(){let h={},sid=this.mgr?.getSessionId()??null;sid&&(h[`x-interfere-session`]=sid);let wid=this.mgr?.getWindowId()??null;wid&&(h[`x-interfere-window`]=wid);let did=this.device.getDeviceId();return did&&(h[`x-interfere-device`]=did),h}getIdentity(){return this.currentIdentity}async identify(params){if(!this.mgr)return;let sessionId=this.mgr.getSessionId();if(this.identifiedSessionId===sessionId){log.debug(`skipped, already identified for session %s`,sessionId);return}this.currentIdentity=params,this.identifiedSessionId=sessionId;let gen=this.generation,[fpHash,country]=await Promise.all([this.device.whenFingerprintReady(),this.geo.detect()]),deviceId=this.device.getDeviceId();if(gen!==this.generation){this.identifiedSessionId=null;return}log.info(`POST session %s → identify`,sessionId),this.fetcher(appendPathBeforeQuery(this.target.url,`/identify`),{method:`POST`,headers:this.requestHeaders(),body:JSON.stringify({sessionId,deviceId,fpHash,...params,...country&&{country}}),keepalive:!0,signal:AbortSignal.timeout(1e4)}).catch(()=>{this.identifiedSessionId=null,log.warn(`identify failed for session %s`,sessionId)})}clearIdentity(){this.currentIdentity=null,this.identifiedSessionId=null}reset(){this.clearIdentity(),this.mgr?.reset()}dispose(){this.generation+=1,this.clearIdentity(),this.syncedSessionId=null,this.syncAttemptMs=0,this.mgr=null}requestHeaders(){return{...Object.fromEntries(this.target.headers.entries()),...this.headers()}}ensureSynced(sessionId){this.syncedSessionId!==sessionId&&(Date.now()-this.syncAttemptMs<5e3||this.syncSession(sessionId,this.device.getDeviceId(),this.device.getFpHash()))}syncSession(sessionId,deviceId,fpHash){this.syncAttemptMs=Date.now(),this.syncedSessionId=sessionId,this.fetcher(this.target.url,{method:`POST`,headers:this.requestHeaders(),body:JSON.stringify({sessionId,deviceId,fpHash}),keepalive:!0,signal:AbortSignal.timeout(1e4)}).then(res=>{res.ok||(this.syncedSessionId=null)}).catch(()=>{this.syncedSessionId=null,log.warn(`session sync failed, will retry`)})}async onRotate(sessionId){this.syncedSessionId=null,this.syncAttemptMs=Date.now();let gen=this.generation,fpHash=await this.device.whenFingerprintReady(),deviceId=this.device.getDeviceId();gen===this.generation&&(log.debug(`POST session %s (device=%s fp=%s)`,sessionId,deviceId??`pending`,fpHash??`none`),this.syncSession(sessionId,deviceId,fpHash))}};export{SessionTracker};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{v7}from"uuid";const SESSION_ID_KEY=`interfere:session_id`,LAST_ACTIVITY_KEY=`interfere:last_activity`,WINDOW_ID_KEY=`interfere:window_id`;function tryStorage(type){try{let s=globalThis[type],key=`__interfere_probe__`;return s.setItem(key,`1`),s.removeItem(key),s}catch{return null}}var SessionManager=class{sessionId=null;windowId=null;local;session;onRotate=null;lastActivityMs=0;constructor(onRotate){this.local=tryStorage(`localStorage`),this.session=tryStorage(`sessionStorage`),this.onRotate=onRotate??null,this.restore()}getSessionId(){return this.sessionId&&!this.isExpired()?(this.touch(),this.sessionId):this.rotate()}getWindowId(){if(this.windowId)return this.windowId;let stored=this.session?.getItem(WINDOW_ID_KEY);return stored?(this.windowId=stored,stored):(this.windowId=`win_${crypto.randomUUID()}`,this.session?.setItem(WINDOW_ID_KEY,this.windowId),this.windowId)}restore(){let stored=this.local?.getItem(SESSION_ID_KEY);stored&&!this.isExpired()?(this.sessionId=stored,this.touch()):this.rotate()}rotate(){return this.sessionId=v7(),this.local?.setItem(SESSION_ID_KEY,this.sessionId),this.touch(),this.onRotate?.(this.sessionId),this.sessionId}touch(){this.lastActivityMs=Date.now(),this.local?.setItem(LAST_ACTIVITY_KEY,String(this.lastActivityMs))}isExpired(){let raw=this.local?.getItem(LAST_ACTIVITY_KEY),ts=raw?Number(raw):this.lastActivityMs;return ts===0?!0:Date.now()-ts>18e5}};export{SessionManager};
|
|
1
|
+
import{v7}from"uuid";const SESSION_ID_KEY=`interfere:session_id`,LAST_ACTIVITY_KEY=`interfere:last_activity`,WINDOW_ID_KEY=`interfere:window_id`;function tryStorage(type){try{let s=globalThis[type],key=`__interfere_probe__`;return s.setItem(key,`1`),s.removeItem(key),s}catch{return null}}var SessionManager=class{sessionId=null;windowId=null;local;session;onRotate=null;lastActivityMs=0;constructor(onRotate){this.local=tryStorage(`localStorage`),this.session=tryStorage(`sessionStorage`),this.onRotate=onRotate??null,this.restore()}getSessionId(){return this.sessionId&&!this.isExpired()?(this.touch(),this.sessionId):this.rotate()}getWindowId(){if(this.windowId)return this.windowId;let stored=this.session?.getItem(WINDOW_ID_KEY);return stored?(this.windowId=stored,stored):(this.windowId=`win_${crypto.randomUUID()}`,this.session?.setItem(WINDOW_ID_KEY,this.windowId),this.windowId)}reset(){return this.rotate()}restore(){let stored=this.local?.getItem(SESSION_ID_KEY);stored&&!this.isExpired()?(this.sessionId=stored,this.touch()):this.rotate()}rotate(){return this.sessionId=v7(),this.local?.setItem(SESSION_ID_KEY,this.sessionId),this.touch(),this.onRotate?.(this.sessionId),this.sessionId}touch(){this.lastActivityMs=Date.now(),this.local?.setItem(LAST_ACTIVITY_KEY,String(this.lastActivityMs))}isExpired(){let raw=this.local?.getItem(LAST_ACTIVITY_KEY),ts=raw?Number(raw):this.lastActivityMs;return ts===0?!0:Date.now()-ts>18e5}};export{SessionManager};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@interfere/react",
|
|
3
|
-
"version": "11.0.0-canary.
|
|
3
|
+
"version": "11.0.0-canary.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Client-side React SDK for Interfere. Error tracking, session replay, and analytics.",
|
|
6
6
|
"keywords": [
|
|
@@ -88,36 +88,31 @@
|
|
|
88
88
|
"dependencies": {
|
|
89
89
|
"@fingerprintjs/fingerprintjs": "^5.2.0",
|
|
90
90
|
"@interfere/constants": "^9.0.2-canary.0",
|
|
91
|
-
"@interfere/types": "^9.0.3-canary.
|
|
91
|
+
"@interfere/types": "^9.0.3-canary.2",
|
|
92
92
|
"@opentelemetry/api": "^1.9.1",
|
|
93
|
-
"@opentelemetry/api-logs": "^0.
|
|
93
|
+
"@opentelemetry/api-logs": "^0.219.0",
|
|
94
94
|
"@opentelemetry/context-zone": "^2.7.1",
|
|
95
95
|
"@opentelemetry/core": "^2.7.0",
|
|
96
|
-
"@opentelemetry/
|
|
97
|
-
"@opentelemetry/
|
|
98
|
-
"@opentelemetry/
|
|
99
|
-
"@opentelemetry/instrumentation": "^0.
|
|
100
|
-
"@opentelemetry/instrumentation-
|
|
101
|
-
"@opentelemetry/instrumentation-
|
|
102
|
-
"@opentelemetry/instrumentation-
|
|
103
|
-
"@opentelemetry/
|
|
104
|
-
"@opentelemetry/
|
|
105
|
-
"@opentelemetry/instrumentation-xml-http-request": "^0.218.0",
|
|
106
|
-
"@opentelemetry/opentelemetry-browser-detector": "^0.218.0",
|
|
107
|
-
"@opentelemetry/otlp-transformer": "^0.218.0",
|
|
96
|
+
"@opentelemetry/instrumentation": "^0.219.0",
|
|
97
|
+
"@opentelemetry/instrumentation-browser-navigation": "^0.12.0",
|
|
98
|
+
"@opentelemetry/instrumentation-document-load": "^0.64.0",
|
|
99
|
+
"@opentelemetry/instrumentation-fetch": "^0.219.0",
|
|
100
|
+
"@opentelemetry/instrumentation-long-task": "^0.63.0",
|
|
101
|
+
"@opentelemetry/instrumentation-user-interaction": "^0.63.0",
|
|
102
|
+
"@opentelemetry/instrumentation-xml-http-request": "^0.219.0",
|
|
103
|
+
"@opentelemetry/opentelemetry-browser-detector": "^0.219.0",
|
|
104
|
+
"@opentelemetry/otlp-transformer": "^0.219.0",
|
|
108
105
|
"@opentelemetry/resources": "^2.7.0",
|
|
109
|
-
"@opentelemetry/sdk-logs": "^0.
|
|
106
|
+
"@opentelemetry/sdk-logs": "^0.219.0",
|
|
110
107
|
"@opentelemetry/sdk-metrics": "^2.7.0",
|
|
111
108
|
"@opentelemetry/sdk-trace-base": "^2.7.0",
|
|
112
109
|
"@opentelemetry/sdk-trace-web": "^2.7.1",
|
|
113
|
-
"@opentelemetry/web-common": "^0.
|
|
110
|
+
"@opentelemetry/web-common": "^0.219.0",
|
|
114
111
|
"@ua-parser-js/pro-enterprise": "^2.0.6",
|
|
115
|
-
"rrweb": "2.0.
|
|
112
|
+
"rrweb": "2.0.1",
|
|
116
113
|
"uuid": "^14.0.0",
|
|
117
114
|
"web-vitals": "^5.3.0",
|
|
118
|
-
"workbox-background-sync": "^7.4.0"
|
|
119
|
-
"workbox-routing": "^7.4.0",
|
|
120
|
-
"workbox-strategies": "^7.4.0"
|
|
115
|
+
"workbox-background-sync": "^7.4.0"
|
|
121
116
|
},
|
|
122
117
|
"peerDependencies": {
|
|
123
118
|
"react": "^19.2.6",
|
|
@@ -134,21 +129,20 @@
|
|
|
134
129
|
"devDependencies": {
|
|
135
130
|
"@interfere/test-utils": "^9.0.0",
|
|
136
131
|
"@interfere/typescript-config": "^9.0.0",
|
|
137
|
-
"@
|
|
138
|
-
"@types/
|
|
139
|
-
"@types/react": "19.2.15",
|
|
132
|
+
"@types/node": "^25.9.3",
|
|
133
|
+
"@types/react": "19.2.17",
|
|
140
134
|
"@types/react-dom": "19.2.3",
|
|
141
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
142
|
-
"@vitest/browser": "4.1.
|
|
143
|
-
"@vitest/browser-playwright": "4.1.
|
|
144
|
-
"@vitest/coverage-v8": "4.1.
|
|
135
|
+
"@typescript/native-preview": "7.0.0-dev.20260617.2",
|
|
136
|
+
"@vitest/browser": "4.1.9",
|
|
137
|
+
"@vitest/browser-playwright": "4.1.9",
|
|
138
|
+
"@vitest/coverage-v8": "4.1.9",
|
|
145
139
|
"fflate": "^0.8.3",
|
|
146
140
|
"playwright": "^1.60.0",
|
|
147
141
|
"react": "^19.2.6",
|
|
148
142
|
"react-dom": "^19.2.6",
|
|
149
|
-
"rolldown": "1.
|
|
150
|
-
"tsdown": "0.22.
|
|
151
|
-
"vitest": "4.1.
|
|
143
|
+
"rolldown": "^1.1.1",
|
|
144
|
+
"tsdown": "^0.22.2",
|
|
145
|
+
"vitest": "4.1.9",
|
|
152
146
|
"vitest-browser-react": "2.2.0"
|
|
153
147
|
}
|
|
154
148
|
}
|
package/dist/api.d.mts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.mts","names":[],"sources":["../src/api.ts"],"mappings":";;;;;AAuCA;;;;;;;;iBAAgB,IAAA,IACd,IAAA,UACA,EAAA,GAAK,IAAA,EAAM,IAAA,KAAS,CAAA,EACpB,OAAA,GAAU,WAAA,GACT,CAAA;;;;;;;;;iBAuCa,OAAA,CAAQ,KAAc"}
|
package/dist/api.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"api.mjs","names":[],"sources":["../src/api.ts"],"sourcesContent":["import {\n MECHANISM_TYPE,\n toError,\n toException,\n} from \"@interfere/types/sdk/errors\";\n\nimport {\n type Span,\n type SpanOptions,\n SpanStatusCode,\n trace,\n} from \"@opentelemetry/api\";\n\nimport { activeKernel } from \"./internal/kernel-registry.js\";\n\nconst TRACER_NAME = \"@interfere/react\";\n\nfunction tracer() {\n return trace.getTracer(TRACER_NAME);\n}\n\nfunction isPromise<T>(value: unknown): value is Promise<T> {\n return (\n !!value &&\n (typeof value === \"object\" || typeof value === \"function\") &&\n typeof (value as { then?: unknown }).then === \"function\"\n );\n}\n\n/**\n * Wraps `fn` in an OTel span. The span becomes the active context for the\n * duration of the call (sync) or the awaited promise (async). Errors thrown\n * by `fn` are recorded on the span before re-throwing — caller still owns\n * the error.\n *\n * Works pre-init: `trace.getTracer()` returns a no-op tracer that still\n * runs `fn` and propagates its return value, so call sites don't need\n * defensive checks.\n */\nexport function span<T>(\n name: string,\n fn: (span: Span) => T,\n options?: SpanOptions\n): T {\n return tracer().startActiveSpan(name, options ?? {}, (s): T => {\n try {\n const result = fn(s);\n if (isPromise<T>(result)) {\n return result.then(\n (value) => {\n s.end();\n return value;\n },\n (err: unknown) => {\n const e = toError(err);\n s.recordException(e);\n s.setStatus({ code: SpanStatusCode.ERROR, message: e.message });\n s.end();\n throw err;\n }\n ) as T;\n }\n s.end();\n return result;\n } catch (err) {\n const e = toError(err);\n s.recordException(e);\n s.setStatus({ code: SpanStatusCode.ERROR, message: e.message });\n s.end();\n throw err;\n }\n });\n}\n\n/**\n * Captures an error. Records on the active OTel span (when present) AND on\n * the legacy envelope path. Accepts any thrown value; non-Error inputs that\n * carry structure (objects without a recoverable nested Error) are preserved\n * via `toException` so the original shape survives to the wire.\n *\n * Safe to call before init — drops silently if no kernel is registered.\n */\nexport function capture(error: unknown): void {\n activeKernel()?.recordException(toException(error), {\n mechanism: { type: MECHANISM_TYPE.manual.capture, handled: true },\n });\n}\n"],"mappings":"0LAiBA,SAAS,QAAS,CAChB,OAAO,MAAM,UAAU,kBAAW,CACpC,CAEA,SAAS,UAAa,MAAqC,CACzD,MACE,CAAC,CAAC,QACD,OAAO,OAAU,UAAY,OAAO,OAAU,aAC/C,OAAQ,MAA6B,MAAS,UAElD,CAYA,SAAgB,KACd,KACA,GACA,QACG,CACH,OAAO,OAAO,EAAE,gBAAgB,KAAM,SAAW,CAAC,EAAI,GAAS,CAC7D,GAAI,CACF,IAAM,OAAS,GAAG,CAAC,EAiBnB,OAhBI,UAAa,MAAM,EACd,OAAO,KACX,QACC,EAAE,IAAI,EACC,OAER,KAAiB,CAChB,IAAM,EAAI,QAAQ,GAAG,EAIrB,MAHA,EAAE,gBAAgB,CAAC,EACnB,EAAE,UAAU,CAAE,KAAM,eAAe,MAAO,QAAS,EAAE,OAAQ,CAAC,EAC9D,EAAE,IAAI,EACA,GACR,CACF,GAEF,EAAE,IAAI,EACC,OACT,OAAS,IAAK,CACZ,IAAM,EAAI,QAAQ,GAAG,EAIrB,MAHA,EAAE,gBAAgB,CAAC,EACnB,EAAE,UAAU,CAAE,KAAM,eAAe,MAAO,QAAS,EAAE,OAAQ,CAAC,EAC9D,EAAE,IAAI,EACA,GACR,CACF,CAAC,CACH,CAUA,SAAgB,QAAQ,MAAsB,CAC5C,aAAa,GAAG,gBAAgB,YAAY,KAAK,EAAG,CAClD,UAAW,CAAE,KAAM,eAAe,OAAO,QAAS,QAAS,EAAK,CAClE,CAAC,CACH"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"error-boundary.d.mts","names":[],"sources":["../src/error-boundary.tsx"],"mappings":";;;;UAciB,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,KAAK;AAAA;;;;;;;;;cAWD,aAAA,SAAsB,SAAA,CACjC,kBAAA,EACA,kBAAA;EAAA,OAEgB,WAAA,kBAAW,OAAA,CAFT,qBAAA;EAGV,OAAA,EAAS,WAAA,QAAmB,gBAAA;EAE3B,KAAA,EAAO,kBAAA;EAAA,OAET,wBAAA,CAAyB,KAAA,EAAO,KAAA,GAAQ,kBAAA;EAItC,iBAAA,CACP,KAAA,EAAO,KAAA;IAAU,MAAA;EAAA,GACjB,IAAA,EAAM,SAAA;EAAA,iBAeS,KAAA;EAIR,MAAA,IAAM,SAAA;AAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
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 {\n Component,\n type ContextType,\n type ErrorInfo,\n type ReactNode,\n} from \"react\";\n\nimport { captureReactError } from \"./internal/capture.js\";\nimport { InterfereContext } from \"./internal/react-context.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. Reads the kernel from `InterfereContext`; mounting outside the\n * provider is supported — capture is silently skipped, fallback + `onError`\n * still fire.\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 static override contextType = InterfereContext;\n declare context: ContextType<typeof InterfereContext>;\n\n override state: ErrorBoundaryState = { error: null };\n\n static getDerivedStateFromError(error: Error): ErrorBoundaryState {\n return { error };\n }\n\n override componentDidCatch(\n error: Error & { digest?: string },\n info: ErrorInfo\n ) {\n captureReactError(\n this.context?.kernel ?? null,\n error,\n info.componentStack,\n {\n type: MECHANISM_TYPE.react.errorBoundary,\n handled: true,\n }\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":"mNAgCA,IAAa,cAAb,cAAmC,SAGjC,CACA,OAAgB,YAAc,iBAG9B,MAAqC,CAAE,MAAO,IAAK,EAEnD,OAAO,yBAAyB,MAAkC,CAChE,MAAO,CAAE,KAAM,CACjB,CAEA,kBACE,MACA,KACA,CACA,kBACE,KAAK,SAAS,QAAU,KACxB,MACA,KAAK,eACL,CACE,KAAM,eAAe,MAAM,cAC3B,QAAS,EACX,CACF,EAEA,KAAK,MAAM,UAAU,MAAO,IAAI,CAClC,CAEA,UAA+B,CAC7B,KAAK,SAAS,CAAE,MAAO,IAAK,CAAC,CAC/B,EAEA,QAAkB,CAChB,GAAM,CAAE,OAAU,KAAK,MAEvB,GAAI,MAAO,CACT,GAAM,CAAE,UAAa,KAAK,MAI1B,OAHI,OAAO,UAAa,WACf,SAAS,MAAO,KAAK,KAAK,EAE5B,UAAY,IACrB,CAEA,OAAO,KAAK,MAAM,QACpB,CACF"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"browser-context.d.mts","names":[],"sources":["../../src/internal/browser-context.ts"],"mappings":";;;iBAgGsB,cAAA,IAAkB,OAAO,CAAC,cAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"browser-context.mjs","names":[],"sources":["../../src/internal/browser-context.ts"],"sourcesContent":["import type {\n BrowserContext,\n BrowserMetadata,\n DeviceMetadata,\n} from \"@interfere/types/sdk/plugins/context/browser\";\n\nimport { UAParser } from \"@ua-parser-js/pro-enterprise\";\nimport { InApps } from \"@ua-parser-js/pro-enterprise/extensions\";\nimport { isFrozenUA } from \"@ua-parser-js/pro-enterprise/helpers\";\n\ninterface NavigatorUABrand {\n brand: string;\n version: string;\n}\n\ninterface NavigatorUAData {\n brands?: NavigatorUABrand[];\n getHighEntropyValues?: (hints: string[]) => Promise<HighEntropyValues>;\n}\n\ninterface HighEntropyValues {\n bitness?: string;\n fullVersionList?: NavigatorUABrand[];\n platformVersion?: string;\n}\n\nconst NOT_A_BRAND_REGEX = /not.a.brand/i;\nconst HIGH_ENTROPY_HINTS = [\"bitness\", \"platformVersion\", \"fullVersionList\"];\n\nasync function getDeviceMetadata(): Promise<DeviceMetadata | null> {\n if (typeof navigator === \"undefined\") {\n return null;\n }\n\n const uaData = (navigator as Navigator & { userAgentData?: NavigatorUAData })\n .userAgentData;\n\n const [parsed, hints] = await Promise.all([\n UAParser(navigator.userAgent, InApps).withClientHints(),\n uaData?.getHighEntropyValues\n ? uaData\n .getHighEntropyValues(HIGH_ENTROPY_HINTS)\n .catch((): HighEntropyValues => ({}))\n : Promise.resolve<HighEntropyValues>({}),\n ]);\n\n if (!parsed) {\n return null;\n }\n\n const fullVersion = hints.fullVersionList?.find(\n (b) => !NOT_A_BRAND_REGEX.test(b.brand)\n )?.version;\n\n return {\n ...parsed,\n browser: {\n ...parsed.browser,\n ...(fullVersion ? { fullVersion } : {}),\n },\n cpu: {\n ...parsed.cpu,\n ...(hints.bitness ? { bitness: hints.bitness } : {}),\n },\n os: {\n ...parsed.os,\n ...(hints.platformVersion ? { versionFull: hints.platformVersion } : {}),\n },\n frozenUa: isFrozenUA(navigator.userAgent),\n };\n}\n\nfunction getBrowserMetadata(): BrowserMetadata | null {\n if (\n [typeof navigator, typeof screen, typeof globalThis].some(\n (x) => x === \"undefined\"\n )\n ) {\n return null;\n }\n\n const { language } = navigator;\n\n return {\n language,\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n display: {\n screen: {\n height: screen.availHeight,\n width: screen.availWidth,\n orientation: screen.orientation?.type,\n },\n },\n };\n}\n\nexport async function collectContext(): Promise<BrowserContext> {\n return {\n runtime: \"browser\",\n browser: getBrowserMetadata(),\n device: await getDeviceMetadata(),\n };\n}\n"],"mappings":"4KA0BA,MAAM,kBAAoB,eACpB,mBAAqB,CAAC,UAAW,kBAAmB,iBAAiB,EAE3E,eAAe,mBAAoD,CACjE,GAAI,OAAO,UAAc,IACvB,OAAO,KAGT,IAAM,OAAU,UACb,cAEG,CAAC,OAAQ,OAAS,MAAM,QAAQ,IAAI,CACxC,SAAS,UAAU,UAAW,MAAM,EAAE,gBAAgB,EACtD,QAAQ,qBACJ,OACG,qBAAqB,kBAAkB,EACvC,WAAgC,CAAC,EAAE,EACtC,QAAQ,QAA2B,CAAC,CAAC,CAC3C,CAAC,EAED,GAAI,CAAC,OACH,OAAO,KAGT,IAAM,YAAc,MAAM,iBAAiB,KACxC,GAAM,CAAC,kBAAkB,KAAK,EAAE,KAAK,CACxC,GAAG,QAEH,MAAO,CACL,GAAG,OACH,QAAS,CACP,GAAG,OAAO,QACV,GAAI,YAAc,CAAE,WAAY,EAAI,CAAC,CACvC,EACA,IAAK,CACH,GAAG,OAAO,IACV,GAAI,MAAM,QAAU,CAAE,QAAS,MAAM,OAAQ,EAAI,CAAC,CACpD,EACA,GAAI,CACF,GAAG,OAAO,GACV,GAAI,MAAM,gBAAkB,CAAE,YAAa,MAAM,eAAgB,EAAI,CAAC,CACxE,EACA,SAAU,WAAW,UAAU,SAAS,CAC1C,CACF,CAEA,SAAS,oBAA6C,CACpD,GACE,CAAC,OAAO,UAAW,OAAO,OAAQ,OAAO,UAAU,EAAE,KAClD,GAAM,IAAM,WACf,EAEA,OAAO,KAGT,GAAM,CAAE,UAAa,UAErB,MAAO,CACL,SACA,SAAU,KAAK,eAAe,EAAE,gBAAgB,EAAE,SAClD,QAAS,CACP,OAAQ,CACN,OAAQ,OAAO,YACf,MAAO,OAAO,WACd,YAAa,OAAO,aAAa,IACnC,CACF,CACF,CACF,CAEA,eAAsB,gBAA0C,CAC9D,MAAO,CACL,QAAS,UACT,QAAS,mBAAmB,EAC5B,OAAQ,MAAM,kBAAkB,CAClC,CACF"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"capture-boundary.d.mts","names":[],"sources":["../../src/internal/capture-boundary.tsx"],"mappings":";;;;UASU,oBAAA;EACR,QAAA,EAAU,SAAS;AAAA;AAAA,UAGX,oBAAA;EACR,KAAA,EAAO,KAAK;AAAA;AAJO;AAAA;;;;AAIP;AA0Bd;;;;;;;;;;;;;;;;;AA9BqB,cA8BR,eAAA,SAAwB,SAAA,CACnC,oBAAA,EACA,oBAAA;EAAA,OAEgB,WAAA,kBAAW,OAAA,CAFP,qBAAA;EAGZ,OAAA,EAAS,WAAA,QAAmB,gBAAA;EAE3B,KAAA,EAAO,oBAAA;EAAA,OAET,wBAAA,CAAyB,KAAA,EAAO,KAAA,GAAQ,oBAAA;EAItC,MAAA,IAAU,SAAA;AAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
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 ContextType, type ReactNode } from \"react\";\n\nimport { captureReactError } from \"./capture.js\";\nimport { InterfereContext } from \"./react-context.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 static override contextType = InterfereContext;\n declare context: ContextType<typeof InterfereContext>;\n\n override state: CaptureBoundaryState = { error: null };\n\n static getDerivedStateFromError(error: Error): CaptureBoundaryState {\n return { error };\n }\n\n override render(): ReactNode {\n if (this.state.error) {\n captureReactError(this.context?.kernel ?? null, this.state.error, null, {\n type: MECHANISM_TYPE.react.captureBoundary,\n handled: false,\n });\n throw this.state.error;\n }\n return this.props.children;\n }\n}\n"],"mappings":"iMAwCA,IAAa,gBAAb,cAAqC,SAGnC,CACA,OAAgB,YAAc,iBAG9B,MAAuC,CAAE,MAAO,IAAK,EAErD,OAAO,yBAAyB,MAAoC,CAClE,MAAO,CAAE,KAAM,CACjB,CAEA,QAA6B,CAC3B,GAAI,KAAK,MAAM,MAKb,MAJA,kBAAkB,KAAK,SAAS,QAAU,KAAM,KAAK,MAAM,MAAO,KAAM,CACtE,KAAM,eAAe,MAAM,gBAC3B,QAAS,EACX,CAAC,EACK,KAAK,MAAM,MAEnB,OAAO,KAAK,MAAM,QACpB,CACF"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"capture.d.mts","names":[],"sources":["../../src/internal/capture.ts"],"mappings":";;;;;;AAoBA;;;;;;;;;;;;;iBAAgB,iBAAA,CACd,MAAA,EAAQ,MAAA,SACR,KAAA,EAAO,KAAA;EAAU,MAAA;AAAA,GACjB,cAAA,6BACA,SAAA,EAAW,cAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"capture.mjs","names":[],"sources":["../../src/internal/capture.ts"],"sourcesContent":["import { parseReactComponentStack } from \"@interfere/types/sdk/errors\";\nimport type { ErrorMechanism } from \"@interfere/types/sdk/plugins/payload/errors\";\n\nimport type { Kernel } from \"./kernel.js\";\n\n/**\n * Routes a React error through `kernel.recordException()`, attaching the\n * component stack reported by React as additional frames. Dedup of the\n * same `Error` instance caught by both the boundary and `window.onerror`\n * happens inside `recordException`.\n *\n * `error.digest` is React's identifier for an RSC server-side throw that\n * was sanitised and rebuilt on the client — promote it so enrichment can\n * fingerprint the redacted client capture into the same problem as the\n * unredacted server capture.\n *\n * Accepts a nullable kernel — error boundaries can mount outside the\n * `<InterfereProvider>` tree, in which case we silently skip capture and\n * let the host render its fallback / call its `onError` callback.\n */\nexport function captureReactError(\n kernel: Kernel | null,\n error: Error & { digest?: string },\n componentStack: string | null | undefined,\n mechanism: ErrorMechanism\n): void {\n if (!kernel) {\n return;\n }\n kernel.recordException(error, {\n mechanism,\n ...(componentStack\n ? { appendFrames: parseReactComponentStack(componentStack) }\n : {}),\n ...(error.digest ? { errorDigest: error.digest } : {}),\n });\n}\n"],"mappings":"kEAoBA,SAAgB,kBACd,OACA,MACA,eACA,UACM,CACD,QAGL,OAAO,gBAAgB,MAAO,CAC5B,UACA,GAAI,eACA,CAAE,aAAc,yBAAyB,cAAc,CAAE,EACzD,CAAC,EACL,GAAI,MAAM,OAAS,CAAE,YAAa,MAAM,MAAO,EAAI,CAAC,CACtD,CAAC,CACH"}
|