@interfere/react 11.0.0 → 11.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- import{PLUGIN_MANIFEST}from"@interfere/types/sdk/plugins/manifest";const DEFAULT_CONSENT={analytics:!0,replay:!0},PLUGIN_CONSENT_BY_KEY=Object.fromEntries(PLUGIN_MANIFEST.map(plugin=>[plugin.name,plugin.consentCategory])),EVENT_CONSENT_BY_TYPE=Object.fromEntries(PLUGIN_MANIFEST.flatMap(plugin=>plugin.events.map(event=>[event.name,plugin.consentCategory]))),GATEABLE_CATEGORIES=Object.keys(DEFAULT_CONSENT);function getPluginConsentCategory(key){return PLUGIN_CONSENT_BY_KEY[key]}function isConsentAllowed(category,consentState){return category===`necessary`||consentState===null||consentState[category]===!0}function shouldCaptureEvent(type,consentState){return isConsentAllowed(EVENT_CONSENT_BY_TYPE[type],consentState)}function resolveGrantedConsent(consent){return consent??{...DEFAULT_CONSENT}}function hasConsentChanged(current,next){for(let category of GATEABLE_CATEGORIES)if(current?.[category]!==next?.[category])return!0;return!1}export{DEFAULT_CONSENT,getPluginConsentCategory,hasConsentChanged,isConsentAllowed,resolveGrantedConsent,shouldCaptureEvent};
1
+ import{PLUGIN_MANIFEST}from"@interfere/types/sdk/plugins/manifest";const DEFAULT_CONSENT={analytics:!0,replay:!0},PLUGIN_CONSENT_BY_KEY=Object.fromEntries(PLUGIN_MANIFEST.map(plugin=>[plugin.name,plugin.consentCategory])),EVENT_CONSENT_BY_TYPE=Object.fromEntries(PLUGIN_MANIFEST.flatMap(plugin=>plugin.events.map(event=>[event.name,plugin.consentCategory]))),GATEABLE_CATEGORIES=Object.keys(DEFAULT_CONSENT);function getPluginConsentCategory(key){return PLUGIN_CONSENT_BY_KEY[key]}function isConsentAllowed(category,consentState){return category===`necessary`||consentState===null||consentState[category]===!0}function shouldCaptureEvent(type,consentState){return isConsentAllowed(EVENT_CONSENT_BY_TYPE[type],consentState)}function resolveGrantedConsent(consent){return{...DEFAULT_CONSENT,...consent}}function hasConsentChanged(current,next){for(let category of GATEABLE_CATEGORIES)if(current?.[category]!==next?.[category])return!0;return!1}export{DEFAULT_CONSENT,getPluginConsentCategory,hasConsentChanged,isConsentAllowed,resolveGrantedConsent,shouldCaptureEvent};
@@ -1,3 +1,4 @@
1
+ import { ReplayConfig } from "../plugins/replay/config.mjs";
1
2
  import { SessionResumeReason } from "./activity.mjs";
2
3
  import { PluginConfigState, PluginContext } from "../plugins/lib/types.mjs";
3
4
  import { PluginOverrides } from "../plugins/lib/loader.mjs";
@@ -26,7 +27,6 @@ interface KernelOptions {
26
27
  * survive ad-blockers, or to a regional endpoint. Must be a full URL.
27
28
  */
28
29
  apiHost?: string;
29
- consent?: ConsentState;
30
30
  /**
31
31
  * Override the automatic dev-mode guard. When `undefined`, the SDK
32
32
  * auto-detects: it disables itself if `process.env["NODE_ENV"]` is not
@@ -37,7 +37,6 @@ interface KernelOptions {
37
37
  enabled?: boolean;
38
38
  /** Override `globalThis.fetch` for tests / non-browser hosts. */
39
39
  fetch?: typeof globalThis.fetch;
40
- plugins?: PluginOverrides;
41
40
  /**
42
41
  * Override the OTel `service.name` resource attribute. Defaults to
43
42
  * `"interfere-sdk"`. Customers running multiple frontends in one
@@ -221,6 +220,7 @@ declare class Kernel implements PluginContext {
221
220
  onSessionPause(handler: () => void): () => void;
222
221
  onSessionResume(handler: (reason: SessionResumeReason) => void): () => void;
223
222
  getPluginConfigState(key: PluginKey): PluginConfigState;
223
+ getReplayConfig(): ReplayConfig;
224
224
  subscribePluginConfigState(key: PluginKey, listener: (state: PluginConfigState) => void): () => void;
225
225
  /**
226
226
  * Records an exception event on the active OTel span. No envelope
@@ -273,11 +273,10 @@ declare class Kernel implements PluginContext {
273
273
  * `span()` / `capture()` helpers in `api.ts`.
274
274
  */
275
275
  attachOtel(handle: OtelProviderHandle, dispose: () => void): void;
276
- /**
277
- * Provider-driven consent sync. Pass `undefined` to clear consent (treats
278
- * the prop as "unmanaged"); pass a state to apply it.
279
- */
280
- syncConsent(value: ConsentState | undefined): void;
276
+ syncProviderConfig(config: {
277
+ consent?: ConsentState | undefined;
278
+ plugins?: PluginOverrides | undefined;
279
+ }): void;
281
280
  dispose(): Promise<void>;
282
281
  }
283
282
  interface CreateKernelInput {
@@ -1 +1 @@
1
- import{createLogger}from"../util/log.mjs";import{onPageHidden}from"./page-lifecycle.mjs";import{getGlobal}from"../util/global.mjs";import{resolveTargets,setIngestHost}from"./config.mjs";import{SessionTracker}from"../tracking/api.mjs";import{safeStringify}from"../util/stringify.mjs";import{recordInitialNavigation}from"./otel/initial-navigation.mjs";import{PluginRuntime}from"./plugin-runtime.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=/\/$/,REPLAY_UPLOAD_RE=/\/v2\/replay\/upload\//;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};if(opts.errorDigest&&(attrs[`interfere.error.digest`]=opts.errorDigest),opts.subtype&&(attrs[`interfere.exception.subtype`]=opts.subtype),isNonErrorException(value))return 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));let debugIds=globalThis._debugIds;return debugIds&&typeof debugIds==`object`&&Object.keys(debugIds).length>0&&(attrs[`interfere.exception.debug_ids`]=safeStringify(debugIds)),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()??``}onSessionPause(handler){return this.tracker.onSessionPause(handler)}onSessionResume(handler){return this.tracker.onSessionResume(handler)}getPluginConfigState(key){return this.requireRuntime().getPluginConfigState(key)}subscribePluginConfigState(key,listener){return this.requireRuntime().subscribePluginConfigState(key,listener)}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),remoteConfig=await fetchRemoteConfig(resolveTargets(opts.apiHost).config,fetcher);remoteConfig?.ingestUrl&&setIngestHost(remoteConfig.ingestUrl);let targets=resolveTargets(opts.apiHost);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 remoteConfig?.plugins?runtime.applyRemoteConfig(remoteConfig.plugins):runtime.applyRemoteConfigDefaults(),kernel}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(),getDeviceId:()=>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,REPLAY_UPLOAD_RE],...tracing.apiHosts?{apiHosts:tracing.apiHosts}:{},...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){try{let headers=Object.fromEntries(configTarget.headers.entries()),res=await fetcher(configTarget.url,{method:`GET`,headers,credentials:`omit`,signal:AbortSignal.timeout(1e4)});return res.ok?await res.json():null}catch{return log.warn(`remote config fetch failed, using local defaults`),null}}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{onPageHidden}from"./page-lifecycle.mjs";import{getGlobal}from"../util/global.mjs";import{resolveTargets,setIngestHost}from"./config.mjs";import{SessionTracker}from"../tracking/api.mjs";import{safeStringify}from"../util/stringify.mjs";import{recordInitialNavigation}from"./otel/initial-navigation.mjs";import{PluginRuntime}from"./plugin-runtime.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=/\/$/,REPLAY_UPLOAD_RE=/\/v2\/replay\/upload\//;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};if(opts.errorDigest&&(attrs[`interfere.error.digest`]=opts.errorDigest),opts.subtype&&(attrs[`interfere.exception.subtype`]=opts.subtype),isNonErrorException(value))return 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));let debugIds=globalThis._debugIds;return debugIds&&typeof debugIds==`object`&&Object.keys(debugIds).length>0&&(attrs[`interfere.exception.debug_ids`]=safeStringify(debugIds)),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()??``}onSessionPause(handler){return this.tracker.onSessionPause(handler)}onSessionResume(handler){return this.tracker.onSessionResume(handler)}getPluginConfigState(key){return this.requireRuntime().getPluginConfigState(key)}getReplayConfig(){return this.requireRuntime().getReplayConfig()}subscribePluginConfigState(key,listener){return this.requireRuntime().subscribePluginConfigState(key,listener)}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}syncProviderConfig(config){this.requireRuntime().applyProviderConfig(config.plugins,config.consent)}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),remoteConfig=await fetchRemoteConfig(resolveTargets(opts.apiHost).config,fetcher);remoteConfig?.ingestUrl&&setIngestHost(remoteConfig.ingestUrl);let targets=resolveTargets(opts.apiHost);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);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 remoteConfig?.plugins?runtime.applyRemoteConfig(remoteConfig.plugins):runtime.applyRemoteConfigDefaults(),kernel}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(),getDeviceId:()=>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,REPLAY_UPLOAD_RE],...tracing.apiHosts?{apiHosts:tracing.apiHosts}:{},...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){try{let headers=Object.fromEntries(configTarget.headers.entries()),res=await fetcher(configTarget.url,{method:`GET`,headers,credentials:`omit`,signal:AbortSignal.timeout(1e4)});return res.ok?await res.json():null}catch{return log.warn(`remote config fetch failed, using local defaults`),null}}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,3 +1,4 @@
1
+ import { ReplayConfig } from "../plugins/replay/config.mjs";
1
2
  import { PluginConfigState, PluginContext } from "../plugins/lib/types.mjs";
2
3
  import { PluginOverrides } from "../plugins/lib/loader.mjs";
3
4
  import { ConsentState, PluginKey } from "@interfere/types/sdk/plugins/manifest";
@@ -8,15 +9,19 @@ declare class PluginRuntime {
8
9
  private readonly activeCleanups;
9
10
  private readonly pending;
10
11
  private readonly context;
11
- private readonly features;
12
+ private features;
12
13
  private readonly pluginConfigListeners;
13
14
  private remoteConfig;
14
15
  private remoteConfigSettled;
15
16
  private consentState;
17
+ private configSettled;
18
+ private replayConfig;
16
19
  private syncVersion;
17
20
  private disposed;
18
- constructor(context: PluginContext, overrides: PluginOverrides | undefined, initialConsent: ConsentState | undefined);
21
+ constructor(context: PluginContext);
19
22
  getConsent(): ConsentState | null;
23
+ getReplayConfig(): ReplayConfig;
24
+ applyProviderConfig(overrides: PluginOverrides | undefined, consent: ConsentState | undefined): void;
20
25
  setConsent(nextConsent?: ConsentState): void;
21
26
  resetConsent(): void;
22
27
  applyRemoteConfig(config: RemotePluginConfig): void;
@@ -1 +1 @@
1
- import{createLogger}from"../util/log.mjs";import{getPluginConsentCategory,hasConsentChanged,isConsentAllowed,resolveGrantedConsent}from"./consent.mjs";import errorsPlugin from"../plugins/errors.mjs";import{loadPlugin,resolveFeatures}from"../plugins/lib/loader.mjs";import{PLUGIN_MANIFEST}from"@interfere/types/sdk/plugins/manifest";const log=createLogger(`plugin-runtime`);var PluginRuntime=class{activeCleanups=new Map;pending=new Set;context;features;pluginConfigListeners=new Map;remoteConfig={};remoteConfigSettled=!1;consentState;syncVersion=0;disposed=!1;constructor(context,overrides,initialConsent){this.context=context,this.features=resolveFeatures(overrides),this.consentState=initialConsent??null}getConsent(){return this.consentState}setConsent(nextConsent){let nextState=resolveGrantedConsent(nextConsent);hasConsentChanged(this.consentState,nextState)&&(this.consentState=nextState,this.sync())}resetConsent(){hasConsentChanged(this.consentState,null)&&(this.consentState=null,this.sync())}applyRemoteConfig(config){this.remoteConfig=config,this.remoteConfigSettled=!0,this.emitPluginConfigState(),this.sync()}applyRemoteConfigDefaults(){this.remoteConfigSettled=!0,this.emitPluginConfigState(),this.sync()}start(){if(this.shouldEnablePlugin(`errors`)){let cleanup=errorsPlugin.setup(this.context);cleanup&&this.activeCleanups.set(`errors`,cleanup)}this.sync()}async dispose(){this.disposed=!0,this.syncVersion+=1;for(let key of this.activeCleanups.keys())this.deactivate(key);await Promise.allSettled(this.pending)}shouldEnablePlugin(key){return this.features[key]&&this.remoteConfig[key]!==!1&&isConsentAllowed(getPluginConsentCategory(key),this.consentState)}getPluginConfigState(key){return this.remoteConfigSettled?this.remoteConfig[key]===!1?`disabled`:`enabled`:`pending`}subscribePluginConfigState(key,listener){let listeners=this.pluginConfigListeners.get(key)??new Set;return listeners.add(listener),this.pluginConfigListeners.set(key,listeners),()=>{listeners.delete(listener),listeners.size===0&&this.pluginConfigListeners.delete(key)}}emitPluginConfigState(){for(let[key,listeners]of this.pluginConfigListeners){let state=this.getPluginConfigState(key);for(let listener of listeners)listener(state)}}deactivate(key){let cleanup=this.activeCleanups.get(key);if(cleanup){try{cleanup()}catch{log.warn(`cleanup failed for %s`,key)}this.activeCleanups.delete(key)}}async activate(key){if(this.activeCleanups.has(key)||!this.shouldEnablePlugin(key))return;let version=this.syncVersion,cleanup=await loadPlugin(key,this.context);if(cleanup){if(version!==this.syncVersion||!this.shouldEnablePlugin(key)){cleanup();return}this.activeCleanups.set(key,cleanup)}}sync(){if(!this.disposed){this.syncVersion+=1;for(let plugin of PLUGIN_MANIFEST){if(this.shouldEnablePlugin(plugin.name)){if(plugin.name===`errors`){if(!this.activeCleanups.has(`errors`)){let cleanup=errorsPlugin.setup(this.context);cleanup&&this.activeCleanups.set(`errors`,cleanup)}continue}let p=this.activate(plugin.name).catch(()=>{log.warn(`non-critical plugin loading failed`)});this.pending.add(p),p.finally(()=>this.pending.delete(p));continue}this.deactivate(plugin.name)}}}};export{PluginRuntime};
1
+ import{createLogger}from"../util/log.mjs";import{getPluginConsentCategory,hasConsentChanged,isConsentAllowed,resolveGrantedConsent}from"./consent.mjs";import errorsPlugin from"../plugins/errors.mjs";import{loadPlugin,resolveFeatures}from"../plugins/lib/loader.mjs";import{PLUGIN_MANIFEST}from"@interfere/types/sdk/plugins/manifest";const log=createLogger(`plugin-runtime`);var PluginRuntime=class{activeCleanups=new Map;pending=new Set;context;features=resolveFeatures(void 0);pluginConfigListeners=new Map;remoteConfig={};remoteConfigSettled=!1;consentState=null;configSettled=!1;replayConfig={};syncVersion=0;disposed=!1;constructor(context){this.context=context}getConsent(){return this.consentState}getReplayConfig(){return this.replayConfig}applyProviderConfig(overrides,consent){let replay=overrides?.replay;this.features=resolveFeatures(overrides),this.replayConfig=typeof replay==`object`?replay:{},this.consentState=consent===void 0?null:resolveGrantedConsent(consent),this.configSettled=!0,this.sync()}setConsent(nextConsent){let nextState=resolveGrantedConsent(nextConsent);this.configSettled&&!hasConsentChanged(this.consentState,nextState)||(this.consentState=nextState,this.configSettled=!0,this.sync())}resetConsent(){this.configSettled&&!hasConsentChanged(this.consentState,null)||(this.consentState=null,this.configSettled=!0,this.sync())}applyRemoteConfig(config){this.remoteConfig=config,this.remoteConfigSettled=!0,this.emitPluginConfigState(),this.sync()}applyRemoteConfigDefaults(){this.remoteConfigSettled=!0,this.emitPluginConfigState(),this.sync()}start(){if(this.shouldEnablePlugin(`errors`)){let cleanup=errorsPlugin.setup(this.context);cleanup&&this.activeCleanups.set(`errors`,cleanup)}this.sync()}async dispose(){this.disposed=!0,this.syncVersion+=1;for(let key of this.activeCleanups.keys())this.deactivate(key);await Promise.allSettled(this.pending)}shouldEnablePlugin(key){let category=getPluginConsentCategory(key);return category!==`necessary`&&!this.configSettled?!1:this.features[key]&&this.remoteConfig[key]!==!1&&isConsentAllowed(category,this.consentState)}getPluginConfigState(key){return this.remoteConfigSettled?this.remoteConfig[key]===!1?`disabled`:`enabled`:`pending`}subscribePluginConfigState(key,listener){let listeners=this.pluginConfigListeners.get(key)??new Set;return listeners.add(listener),this.pluginConfigListeners.set(key,listeners),()=>{listeners.delete(listener),listeners.size===0&&this.pluginConfigListeners.delete(key)}}emitPluginConfigState(){for(let[key,listeners]of this.pluginConfigListeners){let state=this.getPluginConfigState(key);for(let listener of listeners)listener(state)}}deactivate(key){let cleanup=this.activeCleanups.get(key);if(cleanup){try{cleanup()}catch{log.warn(`cleanup failed for %s`,key)}this.activeCleanups.delete(key)}}async activate(key){if(this.activeCleanups.has(key)||!this.shouldEnablePlugin(key))return;let version=this.syncVersion,cleanup=await loadPlugin(key,this.context);if(cleanup){if(version!==this.syncVersion||!this.shouldEnablePlugin(key)){cleanup();return}this.activeCleanups.set(key,cleanup)}}sync(){if(!this.disposed){this.syncVersion+=1;for(let plugin of PLUGIN_MANIFEST){if(this.shouldEnablePlugin(plugin.name)){if(plugin.name===`errors`){if(!this.activeCleanups.has(`errors`)){let cleanup=errorsPlugin.setup(this.context);cleanup&&this.activeCleanups.set(`errors`,cleanup)}continue}let p=this.activate(plugin.name).catch(()=>{log.warn(`non-critical plugin loading failed`)});this.pending.add(p),p.finally(()=>this.pending.delete(p));continue}this.deactivate(plugin.name)}}}};export{PluginRuntime};
package/dist/package.mjs CHANGED
@@ -1 +1 @@
1
- var name=`@interfere/react`,version=`11.0.0`;export{name,version};
1
+ var name=`@interfere/react`,version=`11.0.1`;export{name,version};
@@ -1,8 +1,11 @@
1
+ import { ReplayOption } from "../replay/config.mjs";
1
2
  import { PluginCleanup, PluginContext } from "./types.mjs";
2
3
  import { PluginKey } from "@interfere/types/sdk/plugins/manifest";
3
4
 
4
5
  //#region src/plugins/lib/loader.d.ts
5
- type PluginOverrides = Partial<Record<PluginKey, boolean>>;
6
+ type PluginOverrides = Partial<Record<Exclude<PluginKey, "replay">, boolean>> & {
7
+ replay?: ReplayOption;
8
+ };
6
9
  declare function resolveFeatures(overrides?: PluginOverrides): Record<PluginKey, boolean>;
7
10
  declare function loadPlugin(key: PluginKey, context: PluginContext): Promise<PluginCleanup | null>;
8
11
  //#endregion
@@ -1 +1 @@
1
- import{createLogger}from"../../util/log.mjs";import{PLUGIN_MANIFEST}from"@interfere/types/sdk/plugins/manifest";const log=createLogger(`plugins`),LOADERS={errors:()=>import(`../errors.mjs`),logs:()=>import(`../logs.mjs`),rageClick:()=>import(`../rage-clicks.mjs`),replay:()=>import(`../replay/index.mjs`)},DEFAULTS=Object.fromEntries(PLUGIN_MANIFEST.map(p=>[p.name,p.defaultEnabled]));function resolveFeatures(overrides){return{...DEFAULTS,...overrides}}function resolvePlugin(mod){return`default`in mod&&typeof mod.default.setup==`function`?mod.default:mod}async function loadPlugin(key,context){let loader=LOADERS[key];if(!loader)return null;try{let cleanup=resolvePlugin(await loader()).setup(context);return log.debug(`loaded %s`,key),typeof cleanup==`function`?cleanup:null}catch{return log.error(`failed to load plugin %s`,key),null}}export{loadPlugin,resolveFeatures};
1
+ import{createLogger}from"../../util/log.mjs";import{PLUGIN_MANIFEST}from"@interfere/types/sdk/plugins/manifest";const log=createLogger(`plugins`),LOADERS={errors:()=>import(`../errors.mjs`),logs:()=>import(`../logs.mjs`),rageClick:()=>import(`../rage-clicks.mjs`),replay:()=>import(`../replay/index.mjs`)},DEFAULTS=Object.fromEntries(PLUGIN_MANIFEST.map(p=>[p.name,p.defaultEnabled]));function resolveFeatures(overrides){if(!overrides)return{...DEFAULTS};let{replay,...rest}=overrides;return{...DEFAULTS,...rest,replay:replay===void 0?DEFAULTS.replay:replay!==!1}}function resolvePlugin(mod){return`default`in mod&&typeof mod.default.setup==`function`?mod.default:mod}async function loadPlugin(key,context){let loader=LOADERS[key];if(!loader)return null;try{let cleanup=resolvePlugin(await loader()).setup(context);return log.debug(`loaded %s`,key),typeof cleanup==`function`?cleanup:null}catch{return log.error(`failed to load plugin %s`,key),null}}export{loadPlugin,resolveFeatures};
@@ -1,3 +1,4 @@
1
+ import { ReplayConfig } from "../replay/config.mjs";
1
2
  import { SessionResumeReason } from "../../internal/activity.mjs";
2
3
  import { RecordExceptionOpts } from "../../internal/kernel.mjs";
3
4
  import { NonErrorException } from "@interfere/types/sdk/errors";
@@ -8,6 +9,7 @@ type PluginCleanup = () => void;
8
9
  type PluginConfigState = "disabled" | "enabled" | "pending";
9
10
  interface PluginContext {
10
11
  getPluginConfigState(key: PluginKey): PluginConfigState;
12
+ getReplayConfig(): ReplayConfig;
11
13
  getSessionId(): string;
12
14
  onSessionPause(handler: () => void): () => void;
13
15
  onSessionResume(handler: (reason: SessionResumeReason) => void): () => void;
@@ -0,0 +1,13 @@
1
+ import { SetRequired } from "@interfere/helpers/set-required";
2
+ import { recordOptions } from "rrweb";
3
+
4
+ //#region src/plugins/replay/config.d.ts
5
+ declare const INTERFERE_MASK_TEXT_CLASS = "interfere-mask";
6
+ declare const INTERFERE_BLOCK_CLASS = "interfere-block";
7
+ declare const INTERFERE_IGNORE_CLASS = "interfere-ignore";
8
+ type ReplayConfig = Pick<recordOptions<unknown>, "blockClass" | "blockSelector" | "ignoreClass" | "ignoreSelector" | "maskAllInputs" | "maskInputFn" | "maskInputOptions" | "maskTextClass" | "maskTextFn" | "maskTextSelector" | "recordCanvas">;
9
+ type ReplayOption = boolean | ReplayConfig;
10
+ type ReplayRecordOptions = SetRequired<ReplayConfig, "maskTextClass" | "blockClass" | "ignoreClass">;
11
+ declare function toRecordOptions(config: ReplayConfig): ReplayRecordOptions;
12
+ //#endregion
13
+ export { INTERFERE_BLOCK_CLASS, INTERFERE_IGNORE_CLASS, INTERFERE_MASK_TEXT_CLASS, ReplayConfig, ReplayOption, ReplayRecordOptions, toRecordOptions };
@@ -0,0 +1 @@
1
+ const INTERFERE_MASK_TEXT_CLASS=`interfere-mask`,INTERFERE_BLOCK_CLASS=`interfere-block`,INTERFERE_IGNORE_CLASS=`interfere-ignore`;function toRecordOptions(config){return{...config,maskTextClass:INTERFERE_MASK_TEXT_CLASS,blockClass:INTERFERE_BLOCK_CLASS,ignoreClass:INTERFERE_IGNORE_CLASS}}export{INTERFERE_BLOCK_CLASS,INTERFERE_IGNORE_CLASS,INTERFERE_MASK_TEXT_CLASS,toRecordOptions};
@@ -1 +1 @@
1
- import{createLogger}from"../../util/log.mjs";import{onPageHidden,onPageShown}from"../../internal/page-lifecycle.mjs";import{resolveTargets}from"../../internal/config.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,unsubscribeConfig=null,unsubscribeHidden=null,unsubscribeShown=null,unsubscribePause=null,unsubscribeResume=null,isPaused=!1,compressor=createCompressor();function flush(opts){if(ctx.getPluginConfigState(`replay`)!==`enabled`)return;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}),pauseSending=unload=>{isPaused||(isPaused=!0,flush(unload?{skipCompression:!0}:void 0))},resumeSending=()=>{isPaused&&(isPaused=!1,recorder?.takeFullSnapshot())};return(async()=>{recorder=await startReplayRecorder(event=>{isPaused||buffer.push(event)}),recorder&&(unsubscribeConfig=ctx.subscribePluginConfigState(`replay`,state=>{state===`enabled`&&flush()}),flushTimer=setInterval(flush,1e4),unsubscribeHidden=onPageHidden(()=>pauseSending(!0)),unsubscribeShown=onPageShown(resumeSending),unsubscribePause=ctx.onSessionPause(()=>pauseSending(!1)),unsubscribeResume=ctx.onSessionResume(resumeSending),log.debug(`recording started`))})(),()=>{flush(),recorder?.stop(),flushTimer&&clearInterval(flushTimer),unsubscribeConfig?.(),unsubscribeHidden?.(),unsubscribeShown?.(),unsubscribePause?.(),unsubscribeResume?.(),compressor.dispose()}}};export{replayPlugin as default};
1
+ import{createLogger}from"../../util/log.mjs";import{onPageHidden,onPageShown}from"../../internal/page-lifecycle.mjs";import{resolveTargets}from"../../internal/config.mjs";import{splitReplayEvents,toReplayChunkPayload}from"./chunking.mjs";import{createReplayBuffer}from"./buffer.mjs";import{createCompressor}from"./compressor-host.mjs";import{toRecordOptions}from"./config.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,unsubscribeConfig=null,unsubscribeHidden=null,unsubscribeShown=null,unsubscribePause=null,unsubscribeResume=null,isPaused=!1,compressor=createCompressor();function flush(opts){if(ctx.getPluginConfigState(`replay`)!==`enabled`)return;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}),pauseSending=unload=>{isPaused||(isPaused=!0,flush(unload?{skipCompression:!0}:void 0))},resumeSending=()=>{isPaused&&(isPaused=!1,recorder?.takeFullSnapshot())},recordOptions=toRecordOptions(ctx.getReplayConfig());return(async()=>{recorder=await startReplayRecorder(event=>{isPaused||buffer.push(event)},recordOptions),recorder&&(unsubscribeConfig=ctx.subscribePluginConfigState(`replay`,state=>{state===`enabled`&&flush()}),flushTimer=setInterval(flush,1e4),unsubscribeHidden=onPageHidden(()=>pauseSending(!0)),unsubscribeShown=onPageShown(resumeSending),unsubscribePause=ctx.onSessionPause(()=>pauseSending(!1)),unsubscribeResume=ctx.onSessionResume(resumeSending),log.debug(`recording started`))})(),()=>{flush(),recorder?.stop(),flushTimer&&clearInterval(flushTimer),unsubscribeConfig?.(),unsubscribeHidden?.(),unsubscribeShown?.(),unsubscribePause?.(),unsubscribeResume?.(),compressor.dispose()}}};export{replayPlugin as default};
@@ -1,8 +1,10 @@
1
+ import { ReplayRecordOptions } from "./config.mjs";
2
+
1
3
  //#region src/plugins/replay/recorder.d.ts
2
4
  interface ReplayRecorder {
3
5
  stop(): void;
4
6
  takeFullSnapshot(): void;
5
7
  }
6
- declare function startReplayRecorder(onEvent: (event: unknown) => void): Promise<ReplayRecorder | null>;
8
+ declare function startReplayRecorder(onEvent: (event: unknown) => void, maskingOptions: ReplayRecordOptions): Promise<ReplayRecorder | null>;
7
9
  //#endregion
8
10
  export { ReplayRecorder, startReplayRecorder };
@@ -1 +1 @@
1
- import{createLogger}from"../../util/log.mjs";const log=createLogger(`replay`);async function startReplayRecorder(onEvent){try{let rrweb=await import(`rrweb`),stop=rrweb.record({emit:onEvent,recordAfter:`load`,inlineStylesheet:!0,slimDOMOptions:`all`,sampling:{mousemove:50,scroll:150,media:800}})??null;return{stop:()=>{stop?.()},takeFullSnapshot:()=>{try{rrweb.record.takeFullSnapshot?.(!0)}catch(error){log.warn(`takeFullSnapshot failed: %o`,error)}}}}catch{return log.error(`rrweb failed to load, replay disabled`),null}}export{startReplayRecorder};
1
+ import{createLogger}from"../../util/log.mjs";const log=createLogger(`replay`);async function startReplayRecorder(onEvent,maskingOptions){try{let rrweb=await import(`rrweb`),stop=rrweb.record({emit:onEvent,recordAfter:`load`,inlineStylesheet:!0,slimDOMOptions:`all`,sampling:{mousemove:50,scroll:150,media:800},...maskingOptions})??null;return{stop:()=>{stop?.()},takeFullSnapshot:()=>{try{rrweb.record.takeFullSnapshot?.(!0)}catch(error){log.warn(`takeFullSnapshot failed: %o`,error)}}}}catch{return log.error(`rrweb failed to load, replay disabled`),null}}export{startReplayRecorder};
@@ -1,3 +1,5 @@
1
+ import { ReplayConfig, ReplayOption } from "./plugins/replay/config.mjs";
2
+ import { PluginOverrides } from "./plugins/lib/loader.mjs";
1
3
  import { Kernel } from "./internal/kernel.mjs";
2
4
  import { InterfereContext, InterfereContextValue } from "./internal/react-context.mjs";
3
5
  import { PropsWithChildren, ReactNode } from "react";
@@ -5,6 +7,12 @@ import { ConsentState } from "@interfere/types/sdk/plugins/manifest";
5
7
 
6
8
  //#region src/provider.d.ts
7
9
  interface InterfereProviderProps extends PropsWithChildren {
10
+ /**
11
+ * Consent state gating which plugin categories may run. Omit (or pass
12
+ * `undefined`) to leave consent unmanaged — every category is allowed.
13
+ * This is the only place consent is configured; `init()` no longer
14
+ * accepts it.
15
+ */
8
16
  consent?: ConsentState | undefined;
9
17
  /**
10
18
  * Auto-install an internal capture boundary around `children` so
@@ -30,14 +38,29 @@ interface InterfereProviderProps extends PropsWithChildren {
30
38
  * and switches to the real kernel once it's available.
31
39
  */
32
40
  kernel: Kernel | null;
41
+ /**
42
+ * Per-plugin overrides, merged over the manifest defaults. Generic plugins
43
+ * are booleans (e.g. `{ pageEvents: false }`). `replay` additionally accepts
44
+ * a config object — `{ replay: { maskAllInputs: true } }` enables replay
45
+ * with that masking config; `{ replay: false }` disables it. Privacy classes
46
+ * are always applied, regardless of config: `interfere-block` elements are
47
+ * dropped and replay as a same-size placeholder, `interfere-mask` masks the
48
+ * text of an element and all its children, and `interfere-ignore` stops an
49
+ * element's input events from being recorded. `input[type="password"]` is
50
+ * masked by default; use `maskInputOptions`/`maskAllInputs` to mask other
51
+ * input values. `init()` no longer accepts plugin config; this is the only
52
+ * place it lives.
53
+ */
54
+ plugins?: PluginOverrides | undefined;
33
55
  }
34
56
  declare function InterfereProvider({
35
57
  kernel,
36
58
  children,
37
59
  consent,
60
+ plugins,
38
61
  errorBoundary
39
62
  }: InterfereProviderProps): ReactNode;
40
63
  declare function useInterfere(): InterfereContextValue;
41
64
  declare function useSession(): string | null;
42
65
  //#endregion
43
- export { InterfereContext, type InterfereContextValue, InterfereProvider, useInterfere, useSession };
66
+ export { InterfereContext, type InterfereContextValue, InterfereProvider, type PluginOverrides, type ReplayConfig, type ReplayOption, useInterfere, useSession };
package/dist/provider.mjs CHANGED
@@ -1 +1 @@
1
- "use client";import{InterfereContext,NULL_CONTEXT_VALUE}from"./internal/react-context.mjs";import{CaptureBoundary}from"./internal/capture-boundary.mjs";import{useContext,useEffect}from"react";import{jsx}from"react/jsx-runtime";function InterfereProvider({kernel,children,consent,errorBoundary=!0}){return useEffect(()=>{kernel?.syncConsent(consent)},[kernel,consent]),jsx(InterfereContext,{value:kernel?{consent:kernel.consent,device:kernel.device,identity:kernel.identity,kernel,session:kernel.session}:NULL_CONTEXT_VALUE,children:errorBoundary?jsx(CaptureBoundary,{children}):children})}function useInterfere(){let ctx=useContext(InterfereContext);if(!ctx)throw Error(`useInterfere must be used within <InterfereProvider>`);return ctx}function useSession(){return useInterfere().session.getId()}export{InterfereContext,InterfereProvider,useInterfere,useSession};
1
+ "use client";import{InterfereContext,NULL_CONTEXT_VALUE}from"./internal/react-context.mjs";import{CaptureBoundary}from"./internal/capture-boundary.mjs";import{useContext,useEffect}from"react";import{jsx}from"react/jsx-runtime";function InterfereProvider({kernel,children,consent,plugins,errorBoundary=!0}){return useEffect(()=>{kernel?.syncProviderConfig({consent,plugins})},[kernel,consent,plugins]),jsx(InterfereContext,{value:kernel?{consent:kernel.consent,device:kernel.device,identity:kernel.identity,kernel,session:kernel.session}:NULL_CONTEXT_VALUE,children:errorBoundary?jsx(CaptureBoundary,{children}):children})}function useInterfere(){let ctx=useContext(InterfereContext);if(!ctx)throw Error(`useInterfere must be used within <InterfereProvider>`);return ctx}function useSession(){return useInterfere().session.getId()}export{InterfereContext,InterfereProvider,useInterfere,useSession};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interfere/react",
3
- "version": "11.0.0",
3
+ "version": "11.0.1",
4
4
  "license": "MIT",
5
5
  "description": "Client-side React SDK for Interfere. Error tracking, session replay, and analytics.",
6
6
  "keywords": [
@@ -87,8 +87,9 @@
87
87
  },
88
88
  "dependencies": {
89
89
  "@fingerprintjs/fingerprintjs": "^5.2.0",
90
- "@interfere/constants": "^9.0.2-canary.3",
91
- "@interfere/types": "^10.0.0-canary.6",
90
+ "@interfere/constants": "^9.0.2",
91
+ "@interfere/helpers": "^9.0.2",
92
+ "@interfere/types": "^10.0.0",
92
93
  "@opentelemetry/api": "^1.9.1",
93
94
  "@opentelemetry/api-logs": "^0.219.0",
94
95
  "@opentelemetry/context-zone": "^2.7.1",
@@ -127,8 +128,8 @@
127
128
  }
128
129
  },
129
130
  "devDependencies": {
130
- "@interfere/test-utils": "^9.0.1-canary.2",
131
- "@interfere/typescript-config": "^9.0.1-canary.1",
131
+ "@interfere/test-utils": "^9.0.2",
132
+ "@interfere/typescript-config": "^9.0.1",
132
133
  "@types/node": "^26.1.0",
133
134
  "@types/react": "19.2.17",
134
135
  "@types/react-dom": "19.2.3",