@interfere/react 11.0.0-canary.7 → 11.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,9 +6,10 @@ interface IngestTarget {
6
6
  interface AuthHeaders {
7
7
  headers: Headers;
8
8
  }
9
+ declare function setIngestHost(host: string): void;
9
10
  declare function withQueryParam(url: string, key: string, value: string): string;
10
11
  declare function appendPathBeforeQuery(url: string, path: string): string;
11
- declare function resolveTargets(apiHost?: string): {
12
+ declare function resolveTargets(apiHost?: string, ingestHost?: string): {
12
13
  /**
13
14
  * Base URL for OTLP exports — the trace/metric exporters append the
14
15
  * sink path themselves. Avoids the kernel having to regex-strip
@@ -25,4 +26,4 @@ declare function resolveTargets(apiHost?: string): {
25
26
  session: IngestTarget;
26
27
  };
27
28
  //#endregion
28
- export { AuthHeaders, IngestTarget, appendPathBeforeQuery, resolveTargets, withQueryParam };
29
+ export { AuthHeaders, IngestTarget, appendPathBeforeQuery, resolveTargets, setIngestHost, withQueryParam };
@@ -1 +1 @@
1
- import{getGlobal}from"../util/global.mjs";import{API_PATHS,API_URL}from"@interfere/constants/api";const TRAILING_SLASH_RE=/\/$/,RELATIVE_URL_BASE=`https://interfere.local`;function resolvePublicKey(){let injected=getGlobal(`__INTERFERE_PUBLIC_KEY__`);if(injected)return injected;if(typeof process<`u`)return process.env.INTERFERE_PUBLIC_KEY??void 0}function isForceEnableActive(){return!!getGlobal(`__INTERFERE_FORCE_ENABLE__`)}function resolveApiHost(explicit){return explicit??getGlobal(`__INTERFERE_API_URL__`)}function normalizeOrigin(host){try{return new URL(host).origin}catch{return API_URL}}function serializeUrl(input,url){return input.startsWith(`/`)?`${url.pathname}${url.search}`:url.toString()}function withQueryParam(url,key,value){let next=new URL(url,RELATIVE_URL_BASE);return next.searchParams.set(key,value),serializeUrl(url,next)}function appendPublicKey(url,publicKey){return publicKey?withQueryParam(url,`pk`,publicKey):url}function appendPathBeforeQuery(url,path){let next=new URL(url,RELATIVE_URL_BASE),suffix=path.startsWith(`/`)?path:`/${path}`;return next.pathname=`${next.pathname.replace(TRAILING_SLASH_RE,``)}${suffix}`,serializeUrl(url,next)}function resolveTargets(apiHost){let publicKey=resolvePublicKey(),headers=new Headers({"content-type":`application/json`});isForceEnableActive()&&headers.set(`x-interfere-force-enable`,`1`);let host=resolveApiHost(apiHost),baseUrl=host?normalizeOrigin(host):API_URL;return{collectorBaseUrl:appendPublicKey(baseUrl,publicKey),config:{url:appendPublicKey(`${baseUrl}${API_PATHS.CONFIG}`,publicKey),headers},ingest:{headers},session:{url:appendPublicKey(`${baseUrl}${API_PATHS.SESSION}`,publicKey),headers}}}export{appendPathBeforeQuery,resolveTargets,withQueryParam};
1
+ import{getGlobal,setGlobal}from"../util/global.mjs";import{API_PATHS,API_URL}from"@interfere/constants/api";const INGEST_HOST_GLOBAL=`__INTERFERE_INGEST_URL__`,TRAILING_SLASH_RE=/\/$/,RELATIVE_URL_BASE=`https://interfere.local`;function resolvePublicKey(){let injected=getGlobal(`__INTERFERE_PUBLIC_KEY__`);if(injected)return injected;if(typeof process<`u`)return process.env.INTERFERE_PUBLIC_KEY??void 0}function isForceEnableActive(){return!!getGlobal(`__INTERFERE_FORCE_ENABLE__`)}function resolveApiHost(explicit){return explicit??getGlobal(`__INTERFERE_API_URL__`)}function resolveIngestHost(explicit){return explicit??getGlobal(INGEST_HOST_GLOBAL)}function setIngestHost(host){setGlobal(INGEST_HOST_GLOBAL,host)}function normalizeOrigin(host){try{return new URL(host).origin}catch{return API_URL}}function serializeUrl(input,url){return input.startsWith(`/`)?`${url.pathname}${url.search}`:url.toString()}function withQueryParam(url,key,value){let next=new URL(url,RELATIVE_URL_BASE);return next.searchParams.set(key,value),serializeUrl(url,next)}function appendPublicKey(url,publicKey){return publicKey?withQueryParam(url,`pk`,publicKey):url}function appendPathBeforeQuery(url,path){let next=new URL(url,RELATIVE_URL_BASE),suffix=path.startsWith(`/`)?path:`/${path}`;return next.pathname=`${next.pathname.replace(TRAILING_SLASH_RE,``)}${suffix}`,serializeUrl(url,next)}function resolveTargets(apiHost,ingestHost){let publicKey=resolvePublicKey(),headers=new Headers({"content-type":`application/json`});isForceEnableActive()&&headers.set(`x-interfere-force-enable`,`1`);let configHost=resolveApiHost(apiHost),configBaseUrl=configHost?normalizeOrigin(configHost):API_URL,resolvedIngestHost=resolveIngestHost(ingestHost),ingestBaseUrl=resolvedIngestHost?normalizeOrigin(resolvedIngestHost):configBaseUrl;return{collectorBaseUrl:appendPublicKey(ingestBaseUrl,publicKey),config:{url:appendPublicKey(`${configBaseUrl}${API_PATHS.CONFIG}`,publicKey),headers},ingest:{headers},session:{url:appendPublicKey(`${ingestBaseUrl}${API_PATHS.SESSION}`,publicKey),headers}}}export{appendPathBeforeQuery,resolveTargets,setIngestHost,withQueryParam};
@@ -1 +1 @@
1
- import{createLogger}from"../util/log.mjs";import{onPageHidden}from"./page-lifecycle.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{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),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 await fetchRemoteConfig(targets.config,fetcher,runtime),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(),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,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,runtime){try{let headers=Object.fromEntries(configTarget.headers.entries()),res=await fetcher(configTarget.url,{method:`GET`,headers,credentials:`omit`,signal:AbortSignal.timeout(1e4)});if(!res.ok){runtime.applyRemoteConfigDefaults();return}let config=await res.json();if(config?.plugins){runtime.applyRemoteConfig(config.plugins),log.debug(`applied remote config`);return}runtime.applyRemoteConfigDefaults()}catch{runtime.applyRemoteConfigDefaults(),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{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 +1 @@
1
- import{appendPathBeforeQuery,withQueryParam}from"../config.mjs";import{PRODUCER_VERSION}from"../version.mjs";import{ExportResultCode}from"@opentelemetry/core";import{JsonLogsSerializer}from"@opentelemetry/otlp-transformer/build/esm/logs/json/logs.js";import{JsonMetricsSerializer}from"@opentelemetry/otlp-transformer/build/esm/metrics/json/metrics.js";import{JsonTraceSerializer}from"@opentelemetry/otlp-transformer/build/esm/trace/json/trace.js";import{AggregationTemporality}from"@opentelemetry/sdk-metrics";function buildSinkUrl(collectorUrl){return appendPathBeforeQuery(collectorUrl,`/v2/sink`)}function buildIngestUrl(collectorUrl){return withQueryParam(buildSinkUrl(collectorUrl),`_pv`,PRODUCER_VERSION)}function makeSender(input){let url=buildIngestUrl(input.collectorUrl);return(payload,resultCallback)=>{payload&&payload.byteLength>0&&fetch(url,{method:`POST`,keepalive:!0,credentials:`omit`,headers:{"content-type":`application/json`},body:payload}).catch(()=>void 0),resultCallback({code:ExportResultCode.SUCCESS})}}var IngestTraceExporter=class{send;constructor(send){this.send=send}export(spans,resultCallback){if(spans.length===0){resultCallback({code:ExportResultCode.SUCCESS});return}this.send(JsonTraceSerializer.serializeRequest(spans),resultCallback)}shutdown(){return Promise.resolve()}forceFlush(){return Promise.resolve()}},IngestLogExporter=class{send;constructor(send){this.send=send}export(logs,resultCallback){if(logs.length===0){resultCallback({code:ExportResultCode.SUCCESS});return}this.send(JsonLogsSerializer.serializeRequest(logs),resultCallback)}shutdown(){return Promise.resolve()}forceFlush(){return Promise.resolve()}},IngestMetricExporter=class{send;constructor(send){this.send=send}export(metrics,resultCallback){this.send(JsonMetricsSerializer.serializeRequest(metrics),resultCallback)}selectAggregationTemporality(){return AggregationTemporality.DELTA}forceFlush(){return Promise.resolve()}shutdown(){return Promise.resolve()}};function createTraceExporter(input){return new IngestTraceExporter(makeSender(input))}function createMetricExporter(input){return new IngestMetricExporter(makeSender(input))}function createLogExporter(input){return new IngestLogExporter(makeSender(input))}export{buildIngestUrl,buildSinkUrl,createLogExporter,createMetricExporter,createTraceExporter};
1
+ import{appendPathBeforeQuery,withQueryParam}from"../config.mjs";import{PRODUCER_VERSION}from"../version.mjs";import{ExportResultCode}from"@opentelemetry/core";import{JsonLogsSerializer,JsonMetricsSerializer,JsonTraceSerializer}from"@opentelemetry/otlp-transformer";import{AggregationTemporality}from"@opentelemetry/sdk-metrics";function buildSinkUrl(collectorUrl){return appendPathBeforeQuery(collectorUrl,`/v2/sink`)}function buildIngestUrl(collectorUrl){return withQueryParam(buildSinkUrl(collectorUrl),`_pv`,PRODUCER_VERSION)}function makeSender(input){let url=buildIngestUrl(input.collectorUrl);return(payload,resultCallback)=>{payload&&payload.byteLength>0&&fetch(url,{method:`POST`,keepalive:!0,credentials:`omit`,headers:{"content-type":`application/json`},body:payload}).catch(()=>void 0),resultCallback({code:ExportResultCode.SUCCESS})}}var IngestTraceExporter=class{send;constructor(send){this.send=send}export(spans,resultCallback){if(spans.length===0){resultCallback({code:ExportResultCode.SUCCESS});return}this.send(JsonTraceSerializer.serializeRequest(spans),resultCallback)}shutdown(){return Promise.resolve()}forceFlush(){return Promise.resolve()}},IngestLogExporter=class{send;constructor(send){this.send=send}export(logs,resultCallback){if(logs.length===0){resultCallback({code:ExportResultCode.SUCCESS});return}this.send(JsonLogsSerializer.serializeRequest(logs),resultCallback)}shutdown(){return Promise.resolve()}forceFlush(){return Promise.resolve()}},IngestMetricExporter=class{send;constructor(send){this.send=send}export(metrics,resultCallback){this.send(JsonMetricsSerializer.serializeRequest(metrics),resultCallback)}selectAggregationTemporality(){return AggregationTemporality.DELTA}forceFlush(){return Promise.resolve()}shutdown(){return Promise.resolve()}};function createTraceExporter(input){return new IngestTraceExporter(makeSender(input))}function createMetricExporter(input){return new IngestMetricExporter(makeSender(input))}function createLogExporter(input){return new IngestLogExporter(makeSender(input))}export{buildIngestUrl,buildSinkUrl,createLogExporter,createMetricExporter,createTraceExporter};
@@ -46,15 +46,17 @@ interface OtelProviderInput {
46
46
  /** OTLP base URL — the opaque sink path is appended by the exporters. */
47
47
  collectorUrl: string;
48
48
  /**
49
- * Stable per-device identifier from `DeviceManager`. Resolved from
50
- * localStorage / cookie at kernel boot, so it's synchronously
51
- * available before the providers are constructed. Stamped as the
52
- * OTel-semconv `device.id` resource attribute so every span / log /
53
- * metric carries it without per-record re-stamping. Optional —
54
- * tests and SSR boot paths that build the provider before the
55
- * device manager has run pass `null`.
49
+ * Returns the current `DeviceManager` device id. Wired into per-span
50
+ * and per-log processors so a mid-page device rotation
51
+ * (`identity.clear()` after logout) lands on subsequent spans / logs
52
+ * without the kernel rebuilding the providers. Returns `null` before
53
+ * the device manager has bootstrapped, or in SSR / test boot paths.
54
+ *
55
+ * Not applied to metrics: the MeterProvider takes a static resource
56
+ * and rebuilding it mid-page would destroy the periodic reader; metrics
57
+ * remain stamped with the boot-time device id.
56
58
  */
57
- deviceId?: string | null | undefined;
59
+ getDeviceId: () => string | null;
58
60
  /**
59
61
  * Returns the kernel's current session id. Wired into per-span /
60
62
  * per-log session processors so a mid-page session rotation (30-min
@@ -94,12 +96,15 @@ interface OtelProviderHandle {
94
96
  tracerProvider: WebTracerProvider;
95
97
  }
96
98
  /**
97
- * Constructs the SDK's **private** OTel providers — never registers globally
98
- * via `provider.register()` or `trace.setGlobalTracerProvider()`. Customers
99
- * with their own OTel setup (DataDog, Vercel, etc.) cohabit cleanly.
99
+ * Constructs the SDK's private OTel providers. `WebTracerProvider` is never
100
+ * installed globally — it's passed to `registerInstrumentations(...)`
101
+ * explicitly. `ContextManager` and `TextMapPropagator` are process-wide
102
+ * singletons; we install ours only when nothing else has claimed them
103
+ * (`setGlobal*` returns `false` on duplicate). If we lose that race, the
104
+ * SSR-embedded pageload parent isn't honored — everything else works.
100
105
  *
101
- * Splitting this out from `Kernel` keeps the OTel module lazy-loadable so
102
- * the error-only bundle path (no tracing) never imports the OTel SDK.
106
+ * Kept out of `Kernel` so the error-only bundle path never imports the
107
+ * OTel SDK.
103
108
  */
104
109
  declare function buildOtelProvider(input: OtelProviderInput): OtelProviderHandle;
105
110
  //#endregion
@@ -1 +1 @@
1
- import{getGlobal}from"../../util/global.mjs";import{SDK_NAME,SDK_VERSION}from"../version.mjs";import{createLogExporter,createMetricExporter,createTraceExporter}from"./exporter.mjs";import{PageScopeContextManager}from"./page-scope-context-manager.mjs";import{propagation}from"@opentelemetry/api";import{CompositePropagator,W3CBaggagePropagator,W3CTraceContextPropagator}from"@opentelemetry/core";import{MeterProvider,PeriodicExportingMetricReader}from"@opentelemetry/sdk-metrics";import{resolveEnvironment}from"@interfere/types/sdk/runtime";import{browserDetector}from"@opentelemetry/opentelemetry-browser-detector";import{detectResources,resourceFromAttributes}from"@opentelemetry/resources";import{BatchLogRecordProcessor,LoggerProvider}from"@opentelemetry/sdk-logs";import{BatchSpanProcessor}from"@opentelemetry/sdk-trace-base";import{WebTracerProvider}from"@opentelemetry/sdk-trace-web";import{createSessionLogRecordProcessor,createSessionSpanProcessor}from"@opentelemetry/web-common";const BROWSER_BATCH_OPTIONS={maxQueueSize:50,maxExportBatchSize:10,scheduledDelayMillis:2e3,exportTimeoutMillis:1e4};function buildOtelProvider(input){let environment=resolveEnvironment({explicit:getGlobal(`__INTERFERE_ENVIRONMENT__`),env:typeof process>`u`?{}:{INTERFERE_ENVIRONMENT:process.env.INTERFERE_ENVIRONMENT,VERCEL_ENV:process.env.VERCEL_ENV,NODE_ENV:process.env.NODE_ENV}}),baseResource=resourceFromAttributes({"service.name":input.serviceName??`interfere-sdk`,"service.namespace":`interfere`,"deployment.environment.name":environment,"telemetry.sdk.language":`webjs`,"interfere.sdk.name":SDK_NAME,"interfere.sdk.version":SDK_VERSION,"interfere.sdk.stack":input.sdkStack.join(`, `),...input.releaseSlug?{"release.slug":input.releaseSlug}:{},...input.deviceId?{"device.id":input.deviceId}:{}}),detected=detectResources({detectors:[browserDetector]}),resource=baseResource.merge(detected),traceExporter=createTraceExporter({collectorUrl:input.collectorUrl,authHeaders:input.authHeaders}),getSessionId=input.getSessionId,tracerProvider=new WebTracerProvider({resource,spanProcessors:[createSessionSpanProcessor({getSessionId}),new BatchSpanProcessor(traceExporter,BROWSER_BATCH_OPTIONS),...input.additionalSpanProcessors??[]]}),contextManager=new PageScopeContextManager;tracerProvider.register({contextManager}),propagation.fields().length===0&&propagation.setGlobalPropagator(new CompositePropagator({propagators:[new W3CTraceContextPropagator,new W3CBaggagePropagator]}));let metricReader=new PeriodicExportingMetricReader({exporter:createMetricExporter({collectorUrl:input.collectorUrl,authHeaders:input.authHeaders}),exportIntervalMillis:3e4}),meterProvider=new MeterProvider({resource,readers:[metricReader,...input.additionalMetricReaders??[]]}),logExporter=createLogExporter({collectorUrl:input.collectorUrl,authHeaders:input.authHeaders}),loggerProvider=new LoggerProvider({resource,processors:[createSessionLogRecordProcessor({getSessionId}),new BatchLogRecordProcessor(logExporter,BROWSER_BATCH_OPTIONS),...input.additionalLogRecordProcessors??[]]});return{contextManager,tracerProvider,meterProvider,metricReader,loggerProvider,flush:async()=>{await Promise.all([tracerProvider.forceFlush(),metricReader.forceFlush(),loggerProvider.forceFlush()])},shutdown:async()=>{await Promise.all([tracerProvider.shutdown(),meterProvider.shutdown(),loggerProvider.shutdown()])}}}export{buildOtelProvider};
1
+ import{getGlobal}from"../../util/global.mjs";import{SDK_NAME,SDK_VERSION}from"../version.mjs";import{createLogExporter,createMetricExporter,createTraceExporter}from"./exporter.mjs";import{PageScopeContextManager}from"./page-scope-context-manager.mjs";import{context,propagation}from"@opentelemetry/api";import{CompositePropagator,W3CBaggagePropagator,W3CTraceContextPropagator}from"@opentelemetry/core";import{MeterProvider,PeriodicExportingMetricReader}from"@opentelemetry/sdk-metrics";import{resolveEnvironment}from"@interfere/types/sdk/runtime";import{browserDetector}from"@opentelemetry/opentelemetry-browser-detector";import{detectResources,resourceFromAttributes}from"@opentelemetry/resources";import{BatchLogRecordProcessor,LoggerProvider}from"@opentelemetry/sdk-logs";import{BatchSpanProcessor}from"@opentelemetry/sdk-trace-base";import{WebTracerProvider}from"@opentelemetry/sdk-trace-web";import{createSessionLogRecordProcessor,createSessionSpanProcessor}from"@opentelemetry/web-common";const ATTR_DEVICE_ID=`device.id`,BROWSER_BATCH_OPTIONS={maxQueueSize:50,maxExportBatchSize:10,scheduledDelayMillis:2e3,exportTimeoutMillis:1e4};function createDeviceSpanProcessor(opts){return{onStart(span){let id=opts.getDeviceId();id&&span.setAttribute(ATTR_DEVICE_ID,id)},onEnd(){},forceFlush:()=>Promise.resolve(),shutdown:()=>Promise.resolve()}}function createDeviceLogRecordProcessor(opts){return{onEmit(logRecord){let id=opts.getDeviceId();id&&logRecord.setAttribute(ATTR_DEVICE_ID,id)},forceFlush:()=>Promise.resolve(),shutdown:()=>Promise.resolve()}}function buildOtelProvider(input){let environment=resolveEnvironment({explicit:getGlobal(`__INTERFERE_ENVIRONMENT__`),env:typeof process>`u`?{}:{INTERFERE_ENVIRONMENT:process.env.INTERFERE_ENVIRONMENT,VERCEL_ENV:process.env.VERCEL_ENV,NODE_ENV:process.env.NODE_ENV}}),baseResource=resourceFromAttributes({"service.name":input.serviceName??`interfere-sdk`,"service.namespace":`interfere`,"deployment.environment.name":environment,"telemetry.sdk.language":`webjs`,"interfere.sdk.name":SDK_NAME,"interfere.sdk.version":SDK_VERSION,"interfere.sdk.stack":input.sdkStack.join(`, `),...input.releaseSlug?{"release.slug":input.releaseSlug}:{}}),detected=detectResources({detectors:[browserDetector]}),resource=baseResource.merge(detected),traceExporter=createTraceExporter({collectorUrl:input.collectorUrl,authHeaders:input.authHeaders}),getSessionId=input.getSessionId,getDeviceId=input.getDeviceId,tracerProvider=new WebTracerProvider({resource,spanProcessors:[createSessionSpanProcessor({getSessionId}),createDeviceSpanProcessor({getDeviceId}),new BatchSpanProcessor(traceExporter,BROWSER_BATCH_OPTIONS),...input.additionalSpanProcessors??[]]}),contextManager=new PageScopeContextManager;contextManager.enable(),context.setGlobalContextManager(contextManager)||contextManager.disable(),propagation.fields().length===0&&propagation.setGlobalPropagator(new CompositePropagator({propagators:[new W3CTraceContextPropagator,new W3CBaggagePropagator]}));let metricReader=new PeriodicExportingMetricReader({exporter:createMetricExporter({collectorUrl:input.collectorUrl,authHeaders:input.authHeaders}),exportIntervalMillis:3e4}),meterProvider=new MeterProvider({resource,readers:[metricReader,...input.additionalMetricReaders??[]]}),logExporter=createLogExporter({collectorUrl:input.collectorUrl,authHeaders:input.authHeaders}),loggerProvider=new LoggerProvider({resource,processors:[createSessionLogRecordProcessor({getSessionId}),createDeviceLogRecordProcessor({getDeviceId}),new BatchLogRecordProcessor(logExporter,BROWSER_BATCH_OPTIONS),...input.additionalLogRecordProcessors??[]]});return{contextManager,tracerProvider,meterProvider,metricReader,loggerProvider,flush:async()=>{await Promise.all([tracerProvider.forceFlush(),metricReader.forceFlush(),loggerProvider.forceFlush()])},shutdown:async()=>{await Promise.all([tracerProvider.shutdown(),meterProvider.shutdown(),loggerProvider.shutdown()])}}}export{buildOtelProvider};
package/dist/package.mjs CHANGED
@@ -1 +1 @@
1
- var name=`@interfere/react`,version=`11.0.0-canary.7`;export{name,version};
1
+ var name=`@interfere/react`,version=`11.0.0`;export{name,version};
@@ -1 +1 @@
1
- import{createLogger}from"../util/log.mjs";import{ActivityMonitor}from"../internal/activity.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;activity=new ActivityMonitor;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)}),this.activity.start({markActivity:()=>this.markActivity(),reset:()=>this.rotateSession(),startedAt:()=>this.getSessionStartedAt()})}onSessionPause(handler){return this.activity.onPause(handler)}onSessionResume(handler){return this.activity.onResume(handler)}sessionId(){let id=this.mgr?.getSessionId()??null;return id&&this.ensureSynced(id),id}windowId(){return this.mgr?.getWindowId()??null}markActivity(){this.mgr?.markActivity()}getSessionStartedAt(){return this.mgr?.getSessionStartedAt()??0}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,credentials:`omit`,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()}rotateSession(){this.mgr?.reset()}dispose(){this.activity.stop(),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,credentials:`omit`,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{ActivityMonitor}from"../internal/activity.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;activity=new ActivityMonitor;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)}),this.activity.start({markActivity:()=>this.markActivity(),reset:()=>this.rotateSession(),startedAt:()=>this.getSessionStartedAt()})}onSessionPause(handler){return this.activity.onPause(handler)}onSessionResume(handler){return this.activity.onResume(handler)}sessionId(){let id=this.mgr?.getSessionId()??null;return id&&this.ensureSynced(id),id}windowId(){return this.mgr?.getWindowId()??null}markActivity(){this.mgr?.markActivity()}getSessionStartedAt(){return this.mgr?.getSessionStartedAt()??0}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,credentials:`omit`,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.device.reset(),this.mgr?.reset()}rotateSession(){this.mgr?.reset()}dispose(){this.activity.stop(),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,credentials:`omit`,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};
@@ -27,6 +27,7 @@ declare class DeviceManager {
27
27
  getDeviceId(): string | null;
28
28
  getFpHash(): string | null;
29
29
  whenFingerprintReady(): Promise<string | null>;
30
+ reset(): string;
30
31
  }
31
32
  //#endregion
32
33
  export { DeviceManager, FingerprintProvider, whenIdle };
@@ -1 +1 @@
1
- import{createLogger}from"../util/log.mjs";const log=createLogger(`device`),LS_KEY=`interfere:device_id`,COOKIE_NAME=`interfere_did`;function whenIdle(){return new Promise(resolve=>{if(typeof globalThis.requestIdleCallback==`function`){globalThis.requestIdleCallback(()=>resolve(),{timeout:5e3});return}setTimeout(resolve,0)})}const defaultFingerprintProvider=async()=>{await whenIdle();try{let result=await(await(await import(`@fingerprintjs/fingerprintjs`)).load()).get();return result.visitorId?result.visitorId:(log.error(`Fingerprinting returned an empty visitor id; visitor identity will be cookie-only`),null)}catch(err){return log.error(`fingerprint init failed; visitor identity will be cookie-only`,err),null}};function tryLocalStorage(){try{let s=globalThis.localStorage,key=`__interfere_device_probe__`;return s.setItem(key,`1`),s.removeItem(key),s}catch{return null}}function getCookie(name){if(typeof document>`u`)return null;let match=document.cookie.split(`; `).find(c=>c.startsWith(`${name}=`));return match?decodeURIComponent(match.split(`=`)[1]??``):null}function setCookie(name,value){typeof document>`u`||(document.cookie=`${name}=${encodeURIComponent(value)};path=/;max-age=${400*24*60*60};SameSite=Lax`)}function generateId(){return crypto.randomUUID()}var DeviceManager=class{deviceId=null;fpHash=null;fpPending=null;fingerprintProvider;constructor(fingerprintProvider){this.fingerprintProvider=fingerprintProvider??defaultFingerprintProvider}init(){if(this.deviceId)return;let ls=tryLocalStorage(),fromLs=ls?.getItem(LS_KEY)??null,fromCookie=getCookie(COOKIE_NAME);this.deviceId=fromLs??fromCookie??generateId(),!fromLs&&ls&&ls.setItem(LS_KEY,this.deviceId),fromCookie||setCookie(COOKIE_NAME,this.deviceId),fromLs&&!fromCookie&&setCookie(COOKIE_NAME,this.deviceId),fromCookie&&!fromLs&&ls&&ls.setItem(LS_KEY,this.deviceId),log.debug(`device %s (ls=%s cookie=%s)`,this.deviceId,!!fromLs,!!fromCookie),this.startFingerprint()}startFingerprint(){this.fpHash||this.fpPending||(this.fpPending=(async()=>{let hash=await this.fingerprintProvider();return this.fpHash=hash,hash&&log.debug(`fpHash %s`,hash),hash})())}getDeviceId(){return this.deviceId}getFpHash(){return this.fpHash}whenFingerprintReady(){return this.fpHash?Promise.resolve(this.fpHash):this.fpPending??Promise.resolve(null)}};export{DeviceManager,whenIdle};
1
+ import{createLogger}from"../util/log.mjs";const log=createLogger(`device`),LS_KEY=`interfere:device_id`,COOKIE_NAME=`interfere_did`;function whenIdle(){return new Promise(resolve=>{if(typeof globalThis.requestIdleCallback==`function`){globalThis.requestIdleCallback(()=>resolve(),{timeout:5e3});return}setTimeout(resolve,0)})}const defaultFingerprintProvider=async()=>{await whenIdle();try{let result=await(await(await import(`@fingerprintjs/fingerprintjs`)).load()).get();return result.visitorId?result.visitorId:(log.error(`Fingerprinting returned an empty visitor id; visitor identity will be cookie-only`),null)}catch(err){return log.error(`fingerprint init failed; visitor identity will be cookie-only`,err),null}};function tryLocalStorage(){try{let s=globalThis.localStorage,key=`__interfere_device_probe__`;return s.setItem(key,`1`),s.removeItem(key),s}catch{return null}}function getCookie(name){if(typeof document>`u`)return null;let match=document.cookie.split(`; `).find(c=>c.startsWith(`${name}=`));return match?decodeURIComponent(match.split(`=`)[1]??``):null}function setCookie(name,value){typeof document>`u`||(document.cookie=`${name}=${encodeURIComponent(value)};path=/;max-age=${400*24*60*60};SameSite=Lax`)}function generateId(){return crypto.randomUUID()}var DeviceManager=class{deviceId=null;fpHash=null;fpPending=null;fingerprintProvider;constructor(fingerprintProvider){this.fingerprintProvider=fingerprintProvider??defaultFingerprintProvider}init(){if(this.deviceId)return;let ls=tryLocalStorage(),fromLs=ls?.getItem(LS_KEY)??null,fromCookie=getCookie(COOKIE_NAME);this.deviceId=fromLs??fromCookie??generateId(),!fromLs&&ls&&ls.setItem(LS_KEY,this.deviceId),fromCookie||setCookie(COOKIE_NAME,this.deviceId),fromLs&&!fromCookie&&setCookie(COOKIE_NAME,this.deviceId),fromCookie&&!fromLs&&ls&&ls.setItem(LS_KEY,this.deviceId),log.debug(`device %s (ls=%s cookie=%s)`,this.deviceId,!!fromLs,!!fromCookie),this.startFingerprint()}startFingerprint(){this.fpHash||this.fpPending||(this.fpPending=(async()=>{let hash=await this.fingerprintProvider();return this.fpHash=hash,hash&&log.debug(`fpHash %s`,hash),hash})())}getDeviceId(){return this.deviceId}getFpHash(){return this.fpHash}whenFingerprintReady(){return this.fpHash?Promise.resolve(this.fpHash):this.fpPending??Promise.resolve(null)}reset(){return this.deviceId=generateId(),tryLocalStorage()?.setItem(LS_KEY,this.deviceId),setCookie(COOKIE_NAME,this.deviceId),log.debug(`device %s (reset)`,this.deviceId),this.deviceId}};export{DeviceManager,whenIdle};
@@ -6,5 +6,6 @@
6
6
  * one place.
7
7
  */
8
8
  declare function getGlobal<T>(key: string): T | undefined;
9
+ declare function setGlobal(key: string, value: unknown): void;
9
10
  //#endregion
10
- export { getGlobal };
11
+ export { getGlobal, setGlobal };
@@ -1 +1 @@
1
- function getGlobal(key){return globalThis[key]}export{getGlobal};
1
+ function getGlobal(key){return globalThis[key]}function setGlobal(key,value){globalThis[key]=value}export{getGlobal,setGlobal};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interfere/react",
3
- "version": "11.0.0-canary.7",
3
+ "version": "11.0.0",
4
4
  "license": "MIT",
5
5
  "description": "Client-side React SDK for Interfere. Error tracking, session replay, and analytics.",
6
6
  "keywords": [
@@ -127,7 +127,7 @@
127
127
  }
128
128
  },
129
129
  "devDependencies": {
130
- "@interfere/test-utils": "^9.0.1-canary.1",
130
+ "@interfere/test-utils": "^9.0.1-canary.2",
131
131
  "@interfere/typescript-config": "^9.0.1-canary.1",
132
132
  "@types/node": "^26.1.0",
133
133
  "@types/react": "19.2.17",