@interfere/react 11.0.0-canary.2 → 11.0.0-canary.4

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.
Files changed (131) hide show
  1. package/dist/internal/activity.d.mts +38 -0
  2. package/dist/internal/activity.mjs +1 -0
  3. package/dist/internal/capture-boundary.mjs +1 -1
  4. package/dist/internal/capture.mjs +1 -1
  5. package/dist/internal/hydration.d.mts +10 -0
  6. package/dist/internal/hydration.mjs +1 -0
  7. package/dist/internal/kernel.d.mts +31 -11
  8. package/dist/internal/kernel.mjs +1 -1
  9. package/dist/internal/otel/initial-navigation.d.mts +13 -0
  10. package/dist/internal/otel/initial-navigation.mjs +1 -0
  11. package/dist/internal/otel/instrumentations.d.mts +25 -5
  12. package/dist/internal/otel/instrumentations.mjs +1 -1
  13. package/dist/internal/otel/propagation.d.mts +2 -2
  14. package/dist/internal/otel/propagation.mjs +1 -1
  15. package/dist/internal/plugin-runtime.d.mts +8 -2
  16. package/dist/internal/plugin-runtime.mjs +1 -1
  17. package/dist/internal/react-context.d.mts +1 -0
  18. package/dist/internal/react-context.mjs +1 -1
  19. package/dist/internal/server-digest.d.mts +9 -0
  20. package/dist/internal/server-digest.mjs +1 -0
  21. package/dist/internal/sw-queue.d.mts +26 -0
  22. package/dist/internal/sw-queue.mjs +1 -0
  23. package/dist/internal/third-party-hosts.d.mts +14 -0
  24. package/dist/internal/third-party-hosts.mjs +1 -0
  25. package/dist/internal/wrapper-singleton.d.mts +1 -0
  26. package/dist/internal/wrapper-singleton.mjs +1 -1
  27. package/dist/package.mjs +1 -1
  28. package/dist/plugins/errors.d.mts +1 -1
  29. package/dist/plugins/errors.mjs +1 -1
  30. package/dist/plugins/lib/types.d.mts +8 -1
  31. package/dist/plugins/logs.d.mts +1 -1
  32. package/dist/plugins/logs.mjs +1 -1
  33. package/dist/plugins/rage-clicks.d.mts +1 -1
  34. package/dist/plugins/rage-clicks.mjs +1 -1
  35. package/dist/plugins/replay/index.d.mts +1 -1
  36. package/dist/plugins/replay/index.mjs +1 -1
  37. package/dist/react-error-handler.d.mts +1 -1
  38. package/dist/sw.mjs +1 -1
  39. package/dist/tracking/api.d.mts +8 -0
  40. package/dist/tracking/api.mjs +1 -1
  41. package/dist/tracking/session.d.mts +4 -0
  42. package/dist/tracking/session.mjs +1 -1
  43. package/package.json +32 -33
  44. package/dist/api.d.mts.map +0 -1
  45. package/dist/api.mjs.map +0 -1
  46. package/dist/error-boundary.d.mts.map +0 -1
  47. package/dist/error-boundary.mjs.map +0 -1
  48. package/dist/internal/browser-context.d.mts.map +0 -1
  49. package/dist/internal/browser-context.mjs.map +0 -1
  50. package/dist/internal/capture-boundary.d.mts.map +0 -1
  51. package/dist/internal/capture-boundary.mjs.map +0 -1
  52. package/dist/internal/capture.d.mts.map +0 -1
  53. package/dist/internal/capture.mjs.map +0 -1
  54. package/dist/internal/config.d.mts.map +0 -1
  55. package/dist/internal/config.mjs.map +0 -1
  56. package/dist/internal/consent.d.mts.map +0 -1
  57. package/dist/internal/consent.mjs.map +0 -1
  58. package/dist/internal/console-patch.d.mts.map +0 -1
  59. package/dist/internal/console-patch.mjs.map +0 -1
  60. package/dist/internal/dom/actionable.d.mts.map +0 -1
  61. package/dist/internal/dom/actionable.mjs.map +0 -1
  62. package/dist/internal/kernel-registry.d.mts.map +0 -1
  63. package/dist/internal/kernel-registry.mjs.map +0 -1
  64. package/dist/internal/kernel.d.mts.map +0 -1
  65. package/dist/internal/kernel.mjs.map +0 -1
  66. package/dist/internal/otel/exporter.d.mts.map +0 -1
  67. package/dist/internal/otel/exporter.mjs.map +0 -1
  68. package/dist/internal/otel/instrumentations.d.mts.map +0 -1
  69. package/dist/internal/otel/instrumentations.mjs.map +0 -1
  70. package/dist/internal/otel/page-scope-context-manager.d.mts.map +0 -1
  71. package/dist/internal/otel/page-scope-context-manager.mjs.map +0 -1
  72. package/dist/internal/otel/propagation.d.mts.map +0 -1
  73. package/dist/internal/otel/propagation.mjs.map +0 -1
  74. package/dist/internal/otel/provider.d.mts.map +0 -1
  75. package/dist/internal/otel/provider.mjs.map +0 -1
  76. package/dist/internal/otel/web-vitals.d.mts.map +0 -1
  77. package/dist/internal/otel/web-vitals.mjs.map +0 -1
  78. package/dist/internal/page-lifecycle.d.mts.map +0 -1
  79. package/dist/internal/page-lifecycle.mjs.map +0 -1
  80. package/dist/internal/plugin-runtime.d.mts.map +0 -1
  81. package/dist/internal/plugin-runtime.mjs.map +0 -1
  82. package/dist/internal/react-context.d.mts.map +0 -1
  83. package/dist/internal/react-context.mjs.map +0 -1
  84. package/dist/internal/sw.d.mts.map +0 -1
  85. package/dist/internal/sw.mjs.map +0 -1
  86. package/dist/internal/version.d.mts.map +0 -1
  87. package/dist/internal/version.mjs.map +0 -1
  88. package/dist/internal/wrapper-singleton.d.mts.map +0 -1
  89. package/dist/internal/wrapper-singleton.mjs.map +0 -1
  90. package/dist/package.mjs.map +0 -1
  91. package/dist/plugins/errors.d.mts.map +0 -1
  92. package/dist/plugins/errors.mjs.map +0 -1
  93. package/dist/plugins/lib/loader.d.mts.map +0 -1
  94. package/dist/plugins/lib/loader.mjs.map +0 -1
  95. package/dist/plugins/lib/types.d.mts.map +0 -1
  96. package/dist/plugins/logs.d.mts.map +0 -1
  97. package/dist/plugins/logs.mjs.map +0 -1
  98. package/dist/plugins/rage-clicks.d.mts.map +0 -1
  99. package/dist/plugins/rage-clicks.mjs.map +0 -1
  100. package/dist/plugins/replay/buffer.d.mts.map +0 -1
  101. package/dist/plugins/replay/buffer.mjs.map +0 -1
  102. package/dist/plugins/replay/chunking.d.mts.map +0 -1
  103. package/dist/plugins/replay/chunking.mjs.map +0 -1
  104. package/dist/plugins/replay/compressor-host.d.mts.map +0 -1
  105. package/dist/plugins/replay/compressor-host.mjs.map +0 -1
  106. package/dist/plugins/replay/delivery.d.mts.map +0 -1
  107. package/dist/plugins/replay/delivery.mjs.map +0 -1
  108. package/dist/plugins/replay/index.d.mts.map +0 -1
  109. package/dist/plugins/replay/index.mjs.map +0 -1
  110. package/dist/plugins/replay/recorder.d.mts.map +0 -1
  111. package/dist/plugins/replay/recorder.mjs.map +0 -1
  112. package/dist/provider.d.mts.map +0 -1
  113. package/dist/provider.mjs.map +0 -1
  114. package/dist/react-error-handler.d.mts.map +0 -1
  115. package/dist/react-error-handler.mjs.map +0 -1
  116. package/dist/tracking/api.d.mts.map +0 -1
  117. package/dist/tracking/api.mjs.map +0 -1
  118. package/dist/tracking/device.d.mts.map +0 -1
  119. package/dist/tracking/device.mjs.map +0 -1
  120. package/dist/tracking/geo.d.mts.map +0 -1
  121. package/dist/tracking/geo.mjs.map +0 -1
  122. package/dist/tracking/session.d.mts.map +0 -1
  123. package/dist/tracking/session.mjs.map +0 -1
  124. package/dist/util/bot.d.mts.map +0 -1
  125. package/dist/util/bot.mjs.map +0 -1
  126. package/dist/util/global.d.mts.map +0 -1
  127. package/dist/util/global.mjs.map +0 -1
  128. package/dist/util/log.d.mts.map +0 -1
  129. package/dist/util/log.mjs.map +0 -1
  130. package/dist/util/stringify.d.mts.map +0 -1
  131. package/dist/util/stringify.mjs.map +0 -1
@@ -0,0 +1,38 @@
1
+ //#region src/internal/activity.d.ts
2
+ declare const IDLE_AFTER_MS: number;
3
+ declare const EXPIRE_AFTER_MS: number;
4
+ declare const MAX_AGE_MS: number;
5
+ declare const CHECK_INTERVAL_MS: number;
6
+ declare const KEEPALIVE_THROTTLE_MS = 1000;
7
+ interface ActivitySession {
8
+ markActivity(): void;
9
+ reset(): void;
10
+ /** Wall-clock epoch ms the session began; `0` when unknown. */
11
+ startedAt(): number;
12
+ }
13
+ type SessionResumeReason = "idle" | "rotated";
14
+ type Handler<A> = (arg: A) => void;
15
+ declare class ActivityMonitor {
16
+ private session;
17
+ private paused;
18
+ private lastActivityMs;
19
+ private keepaliveUntilMs;
20
+ private checkTimer;
21
+ private unsubscribeShown;
22
+ private readonly pauseHandlers;
23
+ private readonly resumeHandlers;
24
+ onPause(handler: Handler<void>): () => void;
25
+ onResume(handler: Handler<SessionResumeReason>): () => void;
26
+ start(session: ActivitySession): void;
27
+ stop(): void;
28
+ private idleMs;
29
+ private isOverMaxAge;
30
+ private readonly poll;
31
+ private readonly onInput;
32
+ private resume;
33
+ private touch;
34
+ private subscribe;
35
+ private fire;
36
+ }
37
+ //#endregion
38
+ export { ActivityMonitor, ActivitySession, CHECK_INTERVAL_MS, EXPIRE_AFTER_MS, IDLE_AFTER_MS, KEEPALIVE_THROTTLE_MS, MAX_AGE_MS, SessionResumeReason };
@@ -0,0 +1 @@
1
+ import{createLogger}from"../util/log.mjs";import{onPageShown}from"./page-lifecycle.mjs";const log=createLogger(`activity`),IDLE_AFTER_MS=300*1e3,EXPIRE_AFTER_MS=900*1e3,MAX_AGE_MS=1440*60*1e3,CHECK_INTERVAL_MS=30*1e3,KEEPALIVE_THROTTLE_MS=1e3,ACTIVITY_EVENTS=[`pointerdown`,`keydown`,`scroll`,`touchstart`,`mousemove`];function monotonicNow(){return typeof performance>`u`?Date.now():performance.now()}var ActivityMonitor=class{session=null;paused=!1;lastActivityMs=0;keepaliveUntilMs=0;checkTimer=null;unsubscribeShown=null;pauseHandlers=new Set;resumeHandlers=new Set;onPause(handler){return this.subscribe(this.pauseHandlers,handler)}onResume(handler){return this.subscribe(this.resumeHandlers,handler)}start(session){if(!(this.checkTimer!==null||typeof globalThis.addEventListener!=`function`)){this.session=session,this.paused=!1,this.lastActivityMs=monotonicNow(),this.keepaliveUntilMs=0;for(let type of ACTIVITY_EVENTS)globalThis.addEventListener(type,this.onInput,{capture:!0,passive:!0});this.unsubscribeShown=onPageShown(this.onInput),this.checkTimer=setInterval(this.poll,CHECK_INTERVAL_MS)}}stop(){if(this.checkTimer!==null){clearInterval(this.checkTimer),this.checkTimer=null;for(let type of ACTIVITY_EVENTS)globalThis.removeEventListener(type,this.onInput,!0);this.unsubscribeShown?.(),this.unsubscribeShown=null,this.session=null,this.paused=!1,this.lastActivityMs=0,this.keepaliveUntilMs=0}}idleMs(){return monotonicNow()-this.lastActivityMs}isOverMaxAge(){let startedAt=this.session?.startedAt()??0;return startedAt>0&&Date.now()-startedAt>=864e5}poll=()=>{this.paused||(this.isOverMaxAge()||this.idleMs()>=3e5)&&(this.paused=!0,log.debug(`session paused`),this.fire(this.pauseHandlers))};onInput=()=>{let now=monotonicNow();if(this.isOverMaxAge()||now-this.lastActivityMs>=9e5){this.resume(now,`rotated`);return}if(this.paused){this.resume(now,`idle`);return}now<this.keepaliveUntilMs||(this.touch(now),this.session?.markActivity())};resume(now,reason){reason===`rotated`&&(log.debug(`resume after expiry → new session`),this.session?.reset()),this.paused=!1,this.touch(now),this.session?.markActivity(),this.fire(this.resumeHandlers,reason)}touch(now){this.lastActivityMs=now,this.keepaliveUntilMs=now+KEEPALIVE_THROTTLE_MS}subscribe(set,handler){return set.add(handler),()=>{set.delete(handler)}}fire(handlers,arg){for(let handler of handlers)try{handler(arg)}catch(err){log.warn(`activity handler threw`,err)}}};export{ActivityMonitor,CHECK_INTERVAL_MS,EXPIRE_AFTER_MS,IDLE_AFTER_MS,KEEPALIVE_THROTTLE_MS,MAX_AGE_MS};
@@ -1 +1 @@
1
- "use client";import{captureReactError}from"./capture.mjs";import{InterfereContext}from"./react-context.mjs";import{MECHANISM_TYPE}from"@interfere/types/sdk/errors";import{Component}from"react";var CaptureBoundary=class extends Component{static contextType=InterfereContext;state={error:null};static getDerivedStateFromError(error){return{error}}render(){if(this.state.error)throw captureReactError(this.context?.kernel??null,this.state.error,null,{type:MECHANISM_TYPE.react.captureBoundary,handled:!1}),this.state.error;return this.props.children}};export{CaptureBoundary};
1
+ "use client";import{captureReactError}from"./capture.mjs";import{InterfereContext}from"./react-context.mjs";import{hasServerDigest}from"./server-digest.mjs";import{MECHANISM_TYPE}from"@interfere/types/sdk/errors";import{Component}from"react";var CaptureBoundary=class extends Component{static contextType=InterfereContext;state={error:null};static getDerivedStateFromError(error){return{error}}render(){if(this.state.error)throw hasServerDigest(this.state.error)||captureReactError(this.context?.kernel??null,this.state.error,null,{type:MECHANISM_TYPE.react.captureBoundary,handled:!1}),this.state.error;return this.props.children}};export{CaptureBoundary};
@@ -1 +1 @@
1
- import{parseReactComponentStack}from"@interfere/types/sdk/errors";function captureReactError(kernel,error,componentStack,mechanism){kernel&&kernel.recordException(error,{mechanism,...componentStack?{appendFrames:parseReactComponentStack(componentStack)}:{},...error.digest?{errorDigest:error.digest}:{}})}export{captureReactError};
1
+ import{isHydrationError}from"./hydration.mjs";import{parseReactComponentStack}from"@interfere/types/sdk/errors";function captureReactError(kernel,error,componentStack,mechanism){if(!kernel)return;let hydration=isHydrationError(error);kernel.recordException(error,{mechanism:hydration?{...mechanism,synthetic:!1}:mechanism,...hydration?{subtype:`hydration`}:{},...componentStack?{appendFrames:parseReactComponentStack(componentStack)}:{},...error.digest?{errorDigest:error.digest}:{}})}export{captureReactError};
@@ -0,0 +1,10 @@
1
+ //#region src/internal/hydration.d.ts
2
+ /**
3
+ * True when an error is a React hydration mismatch — detected by the stable
4
+ * `react.dev/errors/{418,423,425}` codes (production) or React's hydration
5
+ * message wording (development). String-matches React internals, so it tracks
6
+ * React's phrasing; the numeric error codes are the durable anchor.
7
+ */
8
+ declare function isHydrationError(error: unknown): boolean;
9
+ //#endregion
10
+ export { isHydrationError };
@@ -0,0 +1 @@
1
+ const HYDRATION_CODE=/(?:Minified React error #|react\.dev\/errors\/)(?:418|423|425)/,HYDRATION_TEXT=/hydration failed|error while hydrating|hydrated but|text content does not match|server[- ]rendered html|did(?:n't| not) match the client/i;function isHydrationError(error){if(!(error instanceof Error))return!1;let message=error.message;return HYDRATION_CODE.test(message)||HYDRATION_TEXT.test(message)}export{isHydrationError};
@@ -1,14 +1,15 @@
1
- import { PluginContext } from "../plugins/lib/types.mjs";
1
+ import { SessionResumeReason } from "./activity.mjs";
2
+ import { PluginConfigState, PluginContext } from "../plugins/lib/types.mjs";
2
3
  import { PluginOverrides } from "../plugins/lib/loader.mjs";
3
4
  import { SessionTracker } from "../tracking/api.mjs";
4
5
  import { CaptureFailedRequestsOption, CaptureFailedRequestsOptions, HttpStatusCodeRange } from "./otel/instrumentations.mjs";
5
6
  import { OtelProviderHandle } from "./otel/provider.mjs";
6
7
  import { PluginRuntime } from "./plugin-runtime.mjs";
7
8
  import { NonErrorException } from "@interfere/types/sdk/errors";
8
- import { ConsentState } from "@interfere/types/sdk/plugins/manifest";
9
+ import { ConsentState, PluginKey } from "@interfere/types/sdk/plugins/manifest";
9
10
  import { ReleaseSlug } from "@interfere/types/releases/slug";
10
11
  import { MetricReader } from "@opentelemetry/sdk-metrics";
11
- import { LogRecordProcessor } from "@opentelemetry/sdk-logs";
12
+ import { LogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs";
12
13
  import { SpanProcessor } from "@opentelemetry/sdk-trace-base";
13
14
  import { IngestedFrame } from "@interfere/types/data/frame";
14
15
  import { SessionId } from "@interfere/types/data/session";
@@ -38,6 +39,16 @@ interface KernelOptions {
38
39
  * cleanly by surface.
39
40
  */
40
41
  serviceName?: string;
42
+ /**
43
+ * `false` skips registering the SDK's standalone ingest service worker.
44
+ * Set this when the host already runs a service worker at scope `/`
45
+ * (e.g. a Workbox precache worker) and wires {@link installIngestQueue}
46
+ * into it instead — two workers can't both control `/`, and a worker
47
+ * scoped to `/api/interfere/` never sees page requests.
48
+ *
49
+ * @default true
50
+ */
51
+ serviceWorker?: boolean;
41
52
  /**
42
53
  * `false` skips the OTel module entirely — the bundler code-splits
43
54
  * `internal/otel/*` out of this path so the error-only bundle never
@@ -56,8 +67,10 @@ interface KernelOptions {
56
67
  interface TracingOptions {
57
68
  /**
58
69
  * Promote failed `fetch` / `XHR` responses to exception events on the
59
- * active span. Off by default — when on, defaults to capturing 5xx.
60
- * Only fires for same-origin and `propagateContextUrls` matches.
70
+ * active span. **On by default** — captures same-origin (and
71
+ * `propagateContextUrls`) 4xx + 5xx, deduped by `method+status+path` and
72
+ * rate-limited so a flapping endpoint can't flood. Pass `false` to
73
+ * disable, or `{ statusCodes }` to tune the ranges.
61
74
  * See {@link CaptureFailedRequestsOption}.
62
75
  */
63
76
  captureFailedRequests?: CaptureFailedRequestsOption;
@@ -127,6 +140,7 @@ interface KernelConsent {
127
140
  set(value?: ConsentState): void;
128
141
  }
129
142
  interface KernelIdentity {
143
+ clear(): void;
130
144
  get(): IdentifyParams | null;
131
145
  set(params: IdentifyParams): Promise<void>;
132
146
  }
@@ -162,6 +176,12 @@ interface RecordExceptionOpts {
162
176
  */
163
177
  readonly fallbackFrames?: readonly IngestedFrame[];
164
178
  readonly mechanism: ErrorMechanism;
179
+ /**
180
+ * A distinct error sub-classification layered on top of the mechanism —
181
+ * currently `"hydration"` for React hydration mismatches. Lets Interfere
182
+ * triage by kind without re-parsing the message.
183
+ */
184
+ readonly subtype?: string;
165
185
  }
166
186
  /**
167
187
  * The replacement for the old `Client` god-object. Pure construction graph
@@ -181,6 +201,7 @@ declare class Kernel implements PluginContext {
181
201
  readonly session: KernelSession;
182
202
  private readonly tracker;
183
203
  private readonly seen;
204
+ private readonly initialNavigationSeen;
184
205
  private runtime;
185
206
  private otel;
186
207
  private otelDispose;
@@ -191,6 +212,10 @@ declare class Kernel implements PluginContext {
191
212
  private requireRuntime;
192
213
  /** PluginContext shim: plugins expect a non-null string. */
193
214
  getSessionId(): string;
215
+ onSessionPause(handler: () => void): () => void;
216
+ onSessionResume(handler: (reason: SessionResumeReason) => void): () => void;
217
+ getPluginConfigState(key: PluginKey): PluginConfigState;
218
+ subscribePluginConfigState(key: PluginKey, listener: (state: PluginConfigState) => void): () => void;
194
219
  /**
195
220
  * Records an exception event on the active OTel span. No envelope
196
221
  * path — the SDK is OTel-only. Single dedup boundary: drops repeats
@@ -225,6 +250,7 @@ declare class Kernel implements PluginContext {
225
250
  body: string;
226
251
  attributes?: Record<string, string>;
227
252
  }): void;
253
+ recordInitialNavigation(loggerProvider: LoggerProvider): void;
228
254
  /**
229
255
  * Force-flushes the OTel exporters. Returns once they've settled
230
256
  * (success or failure) so callers driving unload — page-lifecycle
@@ -251,12 +277,6 @@ declare class Kernel implements PluginContext {
251
277
  interface CreateKernelInput {
252
278
  opts?: KernelInternalOptions;
253
279
  }
254
- /**
255
- * Pure construction graph. Awaits the remote-config fetch before resolving,
256
- * so the runtime sees a fully-applied config before any capture is attempted
257
- * by the caller. Removes the capture-before-config race the old `Client`
258
- * had.
259
- */
260
280
  declare function createKernel(input?: CreateKernelInput): Promise<Kernel>;
261
281
  declare function isEnabledByEnvironment(): boolean;
262
282
  /**
@@ -1 +1 @@
1
- import{createLogger}from"../util/log.mjs";import{getGlobal}from"../util/global.mjs";import{resolveTargets}from"./config.mjs";import{SessionTracker}from"../tracking/api.mjs";import{safeStringify}from"../util/stringify.mjs";import{onPageHidden}from"./page-lifecycle.mjs";import{PluginRuntime}from"./plugin-runtime.mjs";import{registerServiceWorker}from"./sw.mjs";import{PRODUCER_VERSION}from"./version.mjs";import{isNonErrorException,shouldDropBrowserExtensionNoise,shouldDropUnresolvableStack,toExceptions}from"@interfere/types/sdk/errors";import{trace}from"@opentelemetry/api";import{releaseSlugSchema}from"@interfere/types/releases/slug";const log=createLogger(`kernel`),TRAILING_SLASH_RE=/\/$/;function buildSdkStack(wrapperVersions){return[...wrapperVersions??[],PRODUCER_VERSION]}function buildExceptionEventAttrs(value,opts){let attrs={"interfere.exception.mechanism":opts.mechanism.type,"interfere.exception.handled":opts.mechanism.handled};return opts.errorDigest&&(attrs[`interfere.error.digest`]=opts.errorDigest),isNonErrorException(value)?(attrs[`exception.type`]=value.type,attrs[`exception.message`]=value.value,attrs[`interfere.exception.kind`]=`non-error`,attrs[`interfere.exception.serialized`]=safeStringify(value.serialized),attrs):(attrs[`exception.type`]=value.name,attrs[`exception.message`]=value.message,attrs[`interfere.exception.kind`]=`error`,value.stack&&(attrs[`exception.stacktrace`]=value.stack),attrs[`interfere.exception.chain`]=safeStringify(toExceptions(value,opts.mechanism)),opts.appendFrames&&opts.appendFrames.length>0&&(attrs[`interfere.exception.appended_frames`]=safeStringify(opts.appendFrames)),opts.fallbackFrames&&opts.fallbackFrames.length>0&&(attrs[`interfere.exception.fallback_frames`]=safeStringify(opts.fallbackFrames)),attrs)}var Kernel=class{consent;identity;device;session;tracker;seen=new WeakSet;runtime=null;otel=null;otelDispose=null;unsubscribePageHidden=null;constructor(tracker){this.tracker=tracker,this.consent={get:()=>this.requireRuntime().getConsent(),set:value=>{let runtime=this.requireRuntime();value?runtime.setConsent(value):runtime.resetConsent()}},this.identity={get:()=>this.tracker.getIdentity(),set:params=>this.tracker.identify(params)},this.device={getDeviceId:()=>this.tracker.getDeviceId(),getFpHash:()=>this.tracker.getFpHash()},this.session={getId:()=>this.tracker.sessionId(),getWindowId:()=>this.tracker.windowId()},this.unsubscribePageHidden=onPageHidden(()=>{this.flush().catch(()=>void 0)})}attachRuntime(runtime){this.runtime=runtime}requireRuntime(){if(!this.runtime)throw Error(`Kernel runtime not attached. createKernel must call attachRuntime before any consent/dispose path runs.`);return this.runtime}getSessionId(){return this.tracker.sessionId()??``}recordException(value,opts){if(!isNonErrorException(value)){if(this.seen.has(value))return;this.seen.add(value)}if(!isNonErrorException(value)){let stacks=[],current=value;for(let depth=0;current&&depth<5;depth+=1)current.stack&&stacks.push(current.stack),current=current.cause instanceof Error?current.cause:void 0;if(shouldDropBrowserExtensionNoise(stacks)||shouldDropUnresolvableStack(stacks))return}let attrs=buildExceptionEventAttrs(value,opts),active=trace.getActiveSpan();if(active?.isRecording()){active.addEvent(`exception`,attrs);return}let span=this.otel?.tracerProvider.getTracer(`@interfere/react/exception`).startSpan(`interfere.exception.capture`);span&&(span.addEvent(`exception`,attrs),span.end())}recordLog(input){this.otel&&this.otel.loggerProvider.getLogger(`@interfere/react`).emit({severityText:input.severityText,severityNumber:input.severityNumber,body:input.body,...input.attributes?{attributes:input.attributes}:{}})}async flush(){this.otel&&await this.otel.flush()}attachOtel(handle,dispose){this.otel=handle,this.otelDispose=dispose}syncConsent(value){let runtime=this.requireRuntime();if(value){runtime.setConsent(value);return}runtime.resetConsent()}async dispose(){this.unsubscribePageHidden?.(),this.unsubscribePageHidden=null,this.runtime&&await this.runtime.dispose(),this.tracker.dispose(),this.otelDispose?.(),this.otelDispose=null,this.otel&&=(await this.otel.shutdown(),null)}};async function createKernel(input={}){let{opts={}}=input,fetcher=opts.fetch??globalThis.fetch.bind(globalThis),targets=resolveTargets();log.info(`collector: %s`,targets.collectorBaseUrl),getGlobal(`__INTERFERE_FORCE_ENABLE__`)&&log.warn(`FORCE_ENABLE is active. Events will be dropped by production collectors. This should only be set during local development.`);let tracker=new SessionTracker({target:targets.session,fetcher});opts._wrapperVersions?.length&&(globalThis.__INTERFERE_SDK_STACK__=opts._wrapperVersions);let kernel=new Kernel(tracker),runtime=new PluginRuntime(kernel,opts.plugins,opts.consent);if(kernel.attachRuntime(runtime),tracker.start(),runtime.start(),opts.tracing!==!1){let tracingOpts=typeof opts.tracing==`object`?opts.tracing:void 0;await wireOtel(kernel,targets,opts._wrapperVersions,{...tracingOpts??{},...opts.serviceName?{serviceName:opts.serviceName}:{},...opts._internalAdditionalSpanProcessors?{additionalSpanProcessors:opts._internalAdditionalSpanProcessors}:{},...opts._internalAdditionalLogRecordProcessors?{additionalLogRecordProcessors:opts._internalAdditionalLogRecordProcessors}:{},...opts._internalAdditionalMetricReaders?{additionalMetricReaders:opts._internalAdditionalMetricReaders}:{}})}return await fetchRemoteConfig(targets.config,fetcher,runtime),registerServiceWorker({onMessage:msg=>recordSwMessage(kernel,msg)}).catch(()=>void 0),kernel}function recordSwMessage(kernel,msg){kernel.recordLog({severityText:`info`,severityNumber:9,body:msg.type,attributes:{"url.full":msg.url,...msg.reason?{"interfere.sw.reason":msg.reason}:{}}})}async function wireOtel(kernel,targets,wrapperVersions,tracing){let{buildOtelProvider,captureWebVitals,readPropagationFromDocument,registerBundledInstrumentations}=await import(`./otel/index.mjs`),collectorUrl=targets.collectorBaseUrl.replace(TRAILING_SLASH_RE,``),releaseSlug=readReleaseSlugFromGlobals(),handle=buildOtelProvider({collectorUrl,authHeaders:targets.ingest.headers,sdkStack:buildSdkStack(wrapperVersions),getSessionId:()=>kernel.session.getId(),deviceId:kernel.device.getDeviceId(),releaseSlug,...tracing.serviceName?{serviceName:tracing.serviceName}:{},...tracing.additionalSpanProcessors?{additionalSpanProcessors:tracing.additionalSpanProcessors}:{},...tracing.additionalLogRecordProcessors?{additionalLogRecordProcessors:tracing.additionalLogRecordProcessors}:{},...tracing.additionalMetricReaders?{additionalMetricReaders:tracing.additionalMetricReaders}:{}}),extracted=readPropagationFromDocument();extracted&&handle.contextManager.setPageScope(extracted);let dispose=registerBundledInstrumentations({tracerProvider:handle.tracerProvider,loggerProvider:handle.loggerProvider,ignoreUrls:[`${collectorUrl}/v2/sink`,targets.config.url,targets.session.url],...tracing.propagateContextUrls?{propagateContextUrls:tracing.propagateContextUrls}:{},...tracing.resolveRoute?{resolveRoute:tracing.resolveRoute}:{},...tracing.captureFailedRequests===void 0?{}:{captureFailedRequests:tracing.captureFailedRequests}});tracing.webVitals!==!1&&captureWebVitals({meter:handle.meterProvider.getMeter(`@interfere/react/web-vitals`),flush:()=>handle.flush(),...tracing.resolveRoute?{resolveRoute:tracing.resolveRoute}:{}}),kernel.attachOtel(handle,dispose)}async function fetchRemoteConfig(configTarget,fetcher,runtime){try{let headers=Object.fromEntries(configTarget.headers.entries()),res=await fetcher(configTarget.url,{method:`GET`,headers,signal:AbortSignal.timeout(1e4)});if(!res.ok)return;let config=await res.json();config?.plugins&&(runtime.applyRemoteConfig(config.plugins),log.debug(`applied remote config`))}catch{log.warn(`remote config fetch failed, using local defaults`)}}function isEnabledByEnvironment(){if(typeof process>`u`||!process.env)return!0;let nodeEnv=process.env.NODE_ENV;return nodeEnv===`production`||nodeEnv===void 0?!0:!!(getGlobal(`__INTERFERE_FORCE_ENABLE__`)||process.env.INTERFERE_FORCE_ENABLE)}function readReleaseSlugFromGlobals(){let value=getGlobal(`__INTERFERE_RELEASE_SLUG__`);return typeof value==`string`?releaseSlugSchema.parse(value):null}export{Kernel,buildSdkStack,createKernel,isEnabledByEnvironment,readReleaseSlugFromGlobals};
1
+ import{createLogger}from"../util/log.mjs";import{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{registerServiceWorker}from"./sw.mjs";import{PRODUCER_VERSION}from"./version.mjs";import{isNonErrorException,shouldDropBrowserExtensionNoise,shouldDropUnresolvableStack,toExceptions}from"@interfere/types/sdk/errors";import{trace}from"@opentelemetry/api";import{releaseSlugSchema}from"@interfere/types/releases/slug";const log=createLogger(`kernel`),TRAILING_SLASH_RE=/\/$/;function buildSdkStack(wrapperVersions){return[...wrapperVersions??[],PRODUCER_VERSION]}function buildExceptionEventAttrs(value,opts){let attrs={"interfere.exception.mechanism":opts.mechanism.type,"interfere.exception.handled":opts.mechanism.handled};return opts.errorDigest&&(attrs[`interfere.error.digest`]=opts.errorDigest),opts.subtype&&(attrs[`interfere.exception.subtype`]=opts.subtype),isNonErrorException(value)?(attrs[`exception.type`]=value.type,attrs[`exception.message`]=value.value,attrs[`interfere.exception.kind`]=`non-error`,attrs[`interfere.exception.serialized`]=safeStringify(value.serialized),attrs):(attrs[`exception.type`]=value.name,attrs[`exception.message`]=value.message,attrs[`interfere.exception.kind`]=`error`,value.stack&&(attrs[`exception.stacktrace`]=value.stack),attrs[`interfere.exception.chain`]=safeStringify(toExceptions(value,opts.mechanism)),opts.appendFrames&&opts.appendFrames.length>0&&(attrs[`interfere.exception.appended_frames`]=safeStringify(opts.appendFrames)),opts.fallbackFrames&&opts.fallbackFrames.length>0&&(attrs[`interfere.exception.fallback_frames`]=safeStringify(opts.fallbackFrames)),attrs)}var Kernel=class{consent;identity;device;session;tracker;seen=new WeakSet;initialNavigationSeen=new Set;runtime=null;otel=null;otelDispose=null;unsubscribePageHidden=null;constructor(tracker){this.tracker=tracker,this.consent={get:()=>this.requireRuntime().getConsent(),set:value=>{let runtime=this.requireRuntime();value?runtime.setConsent(value):runtime.resetConsent()}},this.identity={clear:()=>this.tracker.reset(),get:()=>this.tracker.getIdentity(),set:params=>this.tracker.identify(params)},this.device={getDeviceId:()=>this.tracker.getDeviceId(),getFpHash:()=>this.tracker.getFpHash()},this.session={getId:()=>this.tracker.sessionId(),getWindowId:()=>this.tracker.windowId()},this.unsubscribePageHidden=onPageHidden(()=>{this.flush().catch(()=>void 0)})}attachRuntime(runtime){this.runtime=runtime}requireRuntime(){if(!this.runtime)throw Error(`Kernel runtime not attached. createKernel must call attachRuntime before any consent/dispose path runs.`);return this.runtime}getSessionId(){return this.tracker.sessionId()??``}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();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),opts.serviceWorker!==!1&&registerServiceWorker({onMessage:msg=>recordSwMessage(kernel,msg)}).catch(()=>void 0),kernel}function recordSwMessage(kernel,msg){kernel.recordLog({severityText:`info`,severityNumber:9,body:msg.type,attributes:{"url.full":msg.url,...msg.reason?{"interfere.sw.reason":msg.reason}:{}}})}async function wireOtel(kernel,targets,wrapperVersions,tracing){let{buildOtelProvider,captureWebVitals,readPropagationFromDocument,registerBundledInstrumentations}=await import(`./otel/index.mjs`),collectorUrl=targets.collectorBaseUrl.replace(TRAILING_SLASH_RE,``),releaseSlug=readReleaseSlugFromGlobals(),handle=buildOtelProvider({collectorUrl,authHeaders:targets.ingest.headers,sdkStack:buildSdkStack(wrapperVersions),getSessionId:()=>kernel.session.getId(),deviceId:kernel.device.getDeviceId(),releaseSlug,...tracing.serviceName?{serviceName:tracing.serviceName}:{},...tracing.additionalSpanProcessors?{additionalSpanProcessors:tracing.additionalSpanProcessors}:{},...tracing.additionalLogRecordProcessors?{additionalLogRecordProcessors:tracing.additionalLogRecordProcessors}:{},...tracing.additionalMetricReaders?{additionalMetricReaders:tracing.additionalMetricReaders}:{}}),extracted=readPropagationFromDocument();extracted&&handle.contextManager.setPageScope(extracted);let dispose=registerBundledInstrumentations({tracerProvider:handle.tracerProvider,loggerProvider:handle.loggerProvider,ignoreUrls:[`${collectorUrl}/v2/sink`,targets.config.url,targets.session.url],...tracing.propagateContextUrls?{propagateContextUrls:tracing.propagateContextUrls}:{},...tracing.resolveRoute?{resolveRoute:tracing.resolveRoute}:{},...tracing.captureFailedRequests===void 0?{}:{captureFailedRequests:tracing.captureFailedRequests}});tracing.webVitals!==!1&&captureWebVitals({meter:handle.meterProvider.getMeter(`@interfere/react/web-vitals`),flush:()=>handle.flush(),...tracing.resolveRoute?{resolveRoute:tracing.resolveRoute}:{}}),kernel.attachOtel(handle,dispose),kernel.recordInitialNavigation(handle.loggerProvider)}async function fetchRemoteConfig(configTarget,fetcher,runtime){try{let headers=Object.fromEntries(configTarget.headers.entries()),res=await fetcher(configTarget.url,{method:`GET`,headers,signal:AbortSignal.timeout(1e4)});if(!res.ok){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};
@@ -0,0 +1,13 @@
1
+ import { LoggerProvider } from "@opentelemetry/sdk-logs";
2
+
3
+ //#region src/internal/otel/initial-navigation.d.ts
4
+ interface InitialNavigationInput {
5
+ getLocationHref?(): string | undefined;
6
+ getSessionId(): string | null;
7
+ getWindowId(): string | null;
8
+ loggerProvider: LoggerProvider;
9
+ seen: Set<string>;
10
+ }
11
+ declare function recordInitialNavigation(input: InitialNavigationInput): void;
12
+ //#endregion
13
+ export { InitialNavigationInput, recordInitialNavigation };
@@ -0,0 +1 @@
1
+ function recordInitialNavigation(input){let url=input.getLocationHref?input.getLocationHref():globalThis.location?.href;if(!url)return;let sessionId=input.getSessionId();if(!sessionId)return;let key=`${sessionId}:${input.getWindowId()??``}:${url}`;input.seen.has(key)||(input.seen.add(key),input.loggerProvider.getLogger(`@opentelemetry/instrumentation-browser-navigation`).emit({severityNumber:0,attributes:{"browser.navigation.type":`pageload`,"url.full":url}}))}export{recordInitialNavigation};
@@ -14,15 +14,35 @@ type FetchSpanRequest = Request | RequestInfo | RequestInit | URL;
14
14
  */
15
15
  type HttpStatusCodeRange = number | readonly [number, number];
16
16
  interface CaptureFailedRequestsOptions {
17
- /** Defaults to `[[500, 599]]` when `captureFailedRequests` is `true`. */
17
+ /** Defaults to `[[400, 599]]` (4xx + 5xx) when not specified. */
18
18
  statusCodes?: readonly HttpStatusCodeRange[];
19
19
  }
20
- /** Promote tracked failed fetch/XHR responses to captured exception events. */
20
+ /**
21
+ * Promote tracked failed fetch/XHR responses to captured exception events.
22
+ * On by default (same-origin 4xx + 5xx, deduped + rate-limited); `false`
23
+ * disables, `{ statusCodes }` tunes the matched ranges.
24
+ */
21
25
  type CaptureFailedRequestsOption = boolean | CaptureFailedRequestsOptions;
22
- declare function applyFetchSpanAttrs(span: Span, request: FetchSpanRequest, result: Response | FetchErrorShape, failureRanges: readonly HttpStatusCodeRange[] | null, isTrackedUrl: (url: string) => boolean, resolveRoute: ((pathname: string) => string | undefined) | undefined): void;
26
+ declare function resolveFailureRanges(opt: CaptureFailedRequestsOption | undefined): readonly HttpStatusCodeRange[] | null;
27
+ /**
28
+ * Per-instrumentation gate that keeps failed-request capture low-noise:
29
+ * dedupes by `method+status+path` (so a flapping endpoint is captured once)
30
+ * and rate-limits to {@link FAILURE_RATE_MAX} captures per
31
+ * {@link FAILURE_RATE_WINDOW_MS} as a flood backstop. State lives in the
32
+ * closure — one gate per kernel instance, no module-level mutable state.
33
+ */
34
+ declare function createFailureGate(): (key: string) => boolean;
35
+ type NetworkErrorKind = "abort" | "network" | "timeout";
36
+ declare function classifyNetworkError(message: string): NetworkErrorKind;
37
+ declare function applyFetchSpanAttrs(span: Span, request: FetchSpanRequest, result: Response | FetchErrorShape, failureRanges: readonly HttpStatusCodeRange[] | null, isTrackedUrl: (url: string) => boolean, resolveRoute: ((pathname: string) => string | undefined) | undefined, gate?: (key: string) => boolean): void;
38
+ declare function applyXhrSpanAttrs(span: Span, xhr: Pick<XMLHttpRequest, "responseURL" | "status">, failureRanges: readonly HttpStatusCodeRange[] | null, isTrackedUrl: (url: string) => boolean, gate?: (key: string) => boolean): void;
23
39
  declare const _testOnlyApplyFetchSpanAttrs: typeof applyFetchSpanAttrs;
40
+ declare const _testOnlyApplyXhrSpanAttrs: typeof applyXhrSpanAttrs;
41
+ declare const _testOnlyResolveFailureRanges: typeof resolveFailureRanges;
42
+ declare const _testOnlyCreateFailureGate: typeof createFailureGate;
43
+ declare const _testOnlyClassifyNetworkError: typeof classifyNetworkError;
24
44
  interface InstrumentationsInput {
25
- /** See {@link CaptureFailedRequestsOption}. Off by default. */
45
+ /** See {@link CaptureFailedRequestsOption}. On by default. */
26
46
  captureFailedRequests?: CaptureFailedRequestsOption;
27
47
  /**
28
48
  * URLs the SDK exempts from `fetch` and `XHR` instrumentation —
@@ -62,4 +82,4 @@ interface InstrumentationsInput {
62
82
  */
63
83
  declare function registerBundledInstrumentations(input: InstrumentationsInput): () => void;
64
84
  //#endregion
65
- export { CaptureFailedRequestsOption, CaptureFailedRequestsOptions, HttpStatusCodeRange, InstrumentationsInput, _testOnlyApplyFetchSpanAttrs, registerBundledInstrumentations };
85
+ export { CaptureFailedRequestsOption, CaptureFailedRequestsOptions, HttpStatusCodeRange, InstrumentationsInput, _testOnlyApplyFetchSpanAttrs, _testOnlyApplyXhrSpanAttrs, _testOnlyClassifyNetworkError, _testOnlyCreateFailureGate, _testOnlyResolveFailureRanges, registerBundledInstrumentations };
@@ -1 +1 @@
1
- import{describeActionable,isActionable}from"../dom/actionable.mjs";import{MECHANISM_TYPE}from"@interfere/types/sdk/errors";import{registerInstrumentations}from"@opentelemetry/instrumentation";import{BrowserNavigationInstrumentation,defaultSanitizeUrl}from"@opentelemetry/instrumentation-browser-navigation";import{DocumentLoadInstrumentation}from"@opentelemetry/instrumentation-document-load";import{FetchInstrumentation}from"@opentelemetry/instrumentation-fetch";import{LongTaskInstrumentation}from"@opentelemetry/instrumentation-long-task";import{UserInteractionInstrumentation}from"@opentelemetry/instrumentation-user-interaction";import{XMLHttpRequestInstrumentation}from"@opentelemetry/instrumentation-xml-http-request";const ATTR_HTTP_ROUTE=`http.route`,ATTR_URL_FULL=`url.full`,ATTR_URL_PATH=`url.path`,DEFAULT_FAILURE_RANGES=[[500,599]];function resolveFailureRanges(opt){if(!opt)return null;if(opt===!0)return DEFAULT_FAILURE_RANGES;let codes=opt.statusCodes;return codes&&codes.length>0?codes:DEFAULT_FAILURE_RANGES}function statusMatches(status,ranges){for(let range of ranges)if(typeof range==`number`){if(status===range)return!0}else if(status>=range[0]&&status<=range[1])return!0;return!1}function recordHttpFailure(span,status,method,url,mechanism){span.addEvent(`exception`,{"exception.type":`HttpResponseError`,"exception.message":`HTTP ${status} ${method} ${url}`,"interfere.exception.mechanism":mechanism,"interfere.exception.handled":!1,"interfere.exception.kind":`error`})}function maybeCaptureFetchFailure(span,result,failureRanges,method,url){if(!failureRanges)return;let status=result?.status;typeof status!=`number`||status<=0||!statusMatches(status,failureRanges)||recordHttpFailure(span,status,method,url,MECHANISM_TYPE.browser.fetchFailedResponse)}function applyFetchSpanAttrs(span,request,result,failureRanges,isTrackedUrl,resolveRoute){let url=resolveFetchUrl(request,result);if(!url)return;let parsed;try{parsed=new URL(url)}catch{return}span.setAttribute(ATTR_URL_PATH,parsed.pathname);let tracked=isTrackedUrl(url),method=resolveFetchMethod(request);if(tracked){let route=resolveRoute?.(parsed.pathname);route&&span.setAttribute(ATTR_HTTP_ROUTE,route),span.updateName(`${method} ${route??parsed.pathname}`),maybeCaptureFetchFailure(span,result,failureRanges,method,url)}}function resolveFetchUrl(request,result){return responseUrl(result)??requestUrl(request)}function responseUrl(result){return result instanceof Response&&result.url?result.url:null}function requestUrl(request){return typeof request==`string`?request:readStringProp(request,`url`)??readStringProp(request,`href`)}function resolveFetchMethod(request){return readStringProp(request,`method`)??`GET`}function readStringProp(value,key){if(!(value&&typeof value==`object`&&key in value))return null;let prop=value[key];return typeof prop==`string`?prop:null}const _testOnlyApplyFetchSpanAttrs=applyFetchSpanAttrs,SEMCONV_STABILITY_OPT_IN=`http,database,messaging,gen_ai_latest_experimental`,THIRD_PARTY_IGNORE=[/(?:^|\/\/)clerk\./,/\.clerk\./,/\.sentry\./,/\.intercom\./,/\.posthog\./,/\.googletagmanager\.com/,/\.google-analytics\.com/,/\.googleapis\.com/,/\.doubleclick\.net/,/\.facebook\.com/,/\.fbcdn\.net/,/\.analytics\.google\.com/,/px\.ads\.linkedin\.com/,/[?&]_rsc=/],NEVER_MATCH=/^$/,ESCAPE_RE=/[.*+?^${}()|[\]\\]/g;function sameOriginPattern(){if(typeof window>`u`)return NEVER_MATCH;let{protocol,host}=window.location;return RegExp(`^${protocol}//${host.replace(ESCAPE_RE,`\\$&`)}`)}function stampInteractionAttrs(eventType,element,span){let desc=describeActionable(element);span.setAttribute(`ui.event_type`,eventType),span.setAttribute(`ui.target.tag`,desc.tag),desc.id&&span.setAttribute(`ui.target.id`,desc.id),desc.name&&span.setAttribute(`ui.target.name`,desc.name),desc.role&&span.setAttribute(`ui.target.role`,desc.role),desc.ariaLabel&&span.setAttribute(`ui.target.aria_label`,desc.ariaLabel),desc.text&&span.setAttribute(`ui.target.text`,desc.text)}function stampUrlAttrs(span,resolveRoute){if(typeof window>`u`)return;let pathname=window.location.pathname,route=resolveRoute?.(pathname);span.setAttribute(ATTR_URL_PATH,route??pathname),span.setAttribute(ATTR_URL_FULL,window.location.href),route&&span.setAttribute(ATTR_HTTP_ROUTE,route)}function registerBundledInstrumentations(input){let{tracerProvider,loggerProvider,ignoreUrls,propagateContextUrls=[]}=input,resolveRoute=input.resolveRoute,origin=sameOriginPattern(),fetchIgnore=[...ignoreUrls,...THIRD_PARTY_IGNORE],failureRanges=resolveFailureRanges(input.captureFailedRequests);function isTrackedUrl(url){return origin.test(url)||propagateContextUrls.some(re=>re instanceof RegExp?re.test(url):url.includes(re))}return registerInstrumentations({tracerProvider,...loggerProvider?{loggerProvider}:{},instrumentations:[new FetchInstrumentation({propagateTraceHeaderCorsUrls:[origin,...propagateContextUrls],ignoreUrls:fetchIgnore,semconvStabilityOptIn:SEMCONV_STABILITY_OPT_IN,measureRequestSize:!0,applyCustomAttributesOnSpan:(span,request,result)=>{applyFetchSpanAttrs(span,request,result,failureRanges,isTrackedUrl,resolveRoute)}}),new XMLHttpRequestInstrumentation({propagateTraceHeaderCorsUrls:[origin,...propagateContextUrls],ignoreUrls:fetchIgnore,semconvStabilityOptIn:SEMCONV_STABILITY_OPT_IN,applyCustomAttributesOnSpan:(span,xhr)=>{if(!failureRanges)return;let url=xhr.responseURL;if(!(url&&isTrackedUrl(url)))return;let status=xhr.status;typeof status!=`number`||status===0||statusMatches(status,failureRanges)&&recordHttpFailure(span,status,`XHR`,url,MECHANISM_TYPE.browser.xhrFailedResponse)}}),new DocumentLoadInstrumentation({semconvStabilityOptIn:SEMCONV_STABILITY_OPT_IN,applyCustomAttributesOnSpan:{resourceFetch:(span,entry)=>{let name=entry?.name;if(typeof name==`string`)try{let url=new URL(name),isCrossOrigin=typeof window<`u`&&url.origin!==window.location.origin;span.updateName(isCrossOrigin?`resource: ${url.host}${url.pathname}`:`resource: ${url.pathname}`),span.setAttribute(ATTR_URL_FULL,name),span.setAttribute(ATTR_URL_PATH,url.pathname),span.setAttribute(`server.address`,url.host)}catch{return}}}}),new BrowserNavigationInstrumentation({sanitizeUrl:defaultSanitizeUrl}),new UserInteractionInstrumentation({eventNames:[`click`,`submit`],shouldPreventSpanCreation:(eventType,element,span)=>eventType===`click`&&!isActionable(element)?!0:(stampInteractionAttrs(eventType,element,span),stampUrlAttrs(span,resolveRoute),!1)}),new LongTaskInstrumentation({observerCallback:(span,info)=>{stampUrlAttrs(span,resolveRoute);let containerType=info.longtaskEntry.attribution[0]?.containerType;containerType&&span.setAttribute(`longtask.container_type`,containerType)}})]})}export{_testOnlyApplyFetchSpanAttrs,registerBundledInstrumentations};
1
+ import{THIRD_PARTY_IGNORE}from"../third-party-hosts.mjs";import{describeActionable,isActionable}from"../dom/actionable.mjs";import{MECHANISM_TYPE}from"@interfere/types/sdk/errors";import{registerInstrumentations}from"@opentelemetry/instrumentation";import{BrowserNavigationInstrumentation,defaultSanitizeUrl}from"@opentelemetry/instrumentation-browser-navigation";import{DocumentLoadInstrumentation}from"@opentelemetry/instrumentation-document-load";import{FetchInstrumentation}from"@opentelemetry/instrumentation-fetch";import{LongTaskInstrumentation}from"@opentelemetry/instrumentation-long-task";import{UserInteractionInstrumentation}from"@opentelemetry/instrumentation-user-interaction";import{XMLHttpRequestInstrumentation}from"@opentelemetry/instrumentation-xml-http-request";const ATTR_HTTP_ROUTE=`http.route`,ATTR_URL_FULL=`url.full`,ATTR_URL_PATH=`url.path`,DEFAULT_FAILURE_RANGES=[[400,599]];function resolveFailureRanges(opt){if(opt===!1)return null;if(opt===void 0||opt===!0)return DEFAULT_FAILURE_RANGES;let codes=opt.statusCodes;return codes&&codes.length>0?codes:DEFAULT_FAILURE_RANGES}function createFailureGate(){let seen=new Set,windowStart=0,windowCount=0;return key=>{if(seen.has(key))return!1;let now=Date.now();return now-windowStart>6e4&&(windowStart=now,windowCount=0),windowCount>=20?!1:(seen.size>=100&&seen.clear(),seen.add(key),windowCount+=1,!0)}}function failureKey(method,discriminator,url){let path;try{path=new URL(url).pathname}catch{path=url}return`${method} ${discriminator} ${path}`}function statusMatches(status,ranges){for(let range of ranges)if(typeof range==`number`){if(status===range)return!0}else if(status>=range[0]&&status<=range[1])return!0;return!1}function recordHttpFailure(span,status,method,url,mechanism){span.addEvent(`exception`,{"exception.type":`HttpResponseError`,"exception.message":`HTTP ${status} ${method} ${url}`,"interfere.exception.mechanism":mechanism,"interfere.exception.handled":!1,"interfere.exception.kind":`error`})}function maybeCaptureFetchFailure(span,result,failureRanges,method,url,gate){if(!failureRanges)return;let status=result?.status;typeof status!=`number`||status<=0||!statusMatches(status,failureRanges)||gate&&!gate(failureKey(method,status,url))||recordHttpFailure(span,status,method,url,MECHANISM_TYPE.browser.fetchFailedResponse)}const NETWORK_TIMEOUT_RE=/timeout|timed out/i,NETWORK_ABORT_RE=/abort/i;function classifyNetworkError(message){return NETWORK_TIMEOUT_RE.test(message)?`timeout`:NETWORK_ABORT_RE.test(message)?`abort`:`network`}function recordNetworkFailure(span,kind,method,url,mechanism){span.addEvent(`exception`,{"exception.type":`NetworkError`,"exception.message":`Network ${kind} ${method} ${url}`,"interfere.exception.mechanism":mechanism,"interfere.exception.handled":!1,"interfere.exception.kind":`error`,"interfere.exception.subtype":kind})}function maybeCaptureNetworkFailure(span,result,failureRanges,method,url,gate){if(!failureRanges)return;let message=result.message;if(typeof message!=`string`||message.length===0)return;let kind=classifyNetworkError(message);gate&&!gate(failureKey(method,kind,url))||recordNetworkFailure(span,kind,method,url,MECHANISM_TYPE.browser.fetchNetworkError)}function applyFetchSpanAttrs(span,request,result,failureRanges,isTrackedUrl,resolveRoute,gate){let url=resolveFetchUrl(span,request,result);if(!url)return;let parsed;try{parsed=new URL(url)}catch{return}span.setAttribute(ATTR_URL_PATH,parsed.pathname);let tracked=isTrackedUrl(url),method=resolveFetchMethod(request);if(tracked){let route=resolveRoute?.(parsed.pathname);route&&span.setAttribute(ATTR_HTTP_ROUTE,route),span.updateName(`${method} ${route??parsed.pathname}`),result instanceof Response?maybeCaptureFetchFailure(span,result,failureRanges,method,url,gate):maybeCaptureNetworkFailure(span,result,failureRanges,method,url,gate)}}function applyXhrSpanAttrs(span,xhr,failureRanges,isTrackedUrl,gate){if(!failureRanges)return;let url=xhr.responseURL;if(!(url&&isTrackedUrl(url)))return;let status=xhr.status;if(typeof status==`number`){if(status===0){(!gate||gate(failureKey(`XHR`,`network`,url)))&&recordNetworkFailure(span,`network`,`XHR`,url,MECHANISM_TYPE.browser.xhrNetworkError);return}statusMatches(status,failureRanges)&&(gate&&!gate(failureKey(`XHR`,status,url))||recordHttpFailure(span,status,`XHR`,url,MECHANISM_TYPE.browser.xhrFailedResponse))}}function resolveFetchUrl(span,request,result){return responseUrl(result)??requestUrl(request)??spanUrlFull(span)}function isReadableSpan(span){return`attributes`in span}function spanUrlFull(span){if(!isReadableSpan(span))return null;let url=span.attributes[ATTR_URL_FULL]??span.attributes[`http.url`];return typeof url==`string`?url:null}function responseUrl(result){return result instanceof Response&&result.url?result.url:null}function requestUrl(request){return typeof request==`string`?request:readStringProp(request,`url`)??readStringProp(request,`href`)}function resolveFetchMethod(request){return readStringProp(request,`method`)??`GET`}function readStringProp(value,key){if(!(value&&typeof value==`object`&&key in value))return null;let prop=value[key];return typeof prop==`string`?prop:null}const _testOnlyApplyFetchSpanAttrs=applyFetchSpanAttrs,_testOnlyApplyXhrSpanAttrs=applyXhrSpanAttrs,_testOnlyResolveFailureRanges=resolveFailureRanges,_testOnlyCreateFailureGate=createFailureGate,_testOnlyClassifyNetworkError=classifyNetworkError,SEMCONV_STABILITY_OPT_IN=`http,database,messaging,gen_ai_latest_experimental`,NEVER_MATCH=/^$/,ESCAPE_RE=/[.*+?^${}()|[\]\\]/g;function sameOriginPattern(){if(typeof window>`u`)return NEVER_MATCH;let{protocol,host}=window.location;return RegExp(`^${protocol}//${host.replace(ESCAPE_RE,`\\$&`)}`)}function stampInteractionAttrs(eventType,element,span){let desc=describeActionable(element);span.setAttribute(`ui.event_type`,eventType),span.setAttribute(`ui.target.tag`,desc.tag),desc.id&&span.setAttribute(`ui.target.id`,desc.id),desc.name&&span.setAttribute(`ui.target.name`,desc.name),desc.role&&span.setAttribute(`ui.target.role`,desc.role),desc.ariaLabel&&span.setAttribute(`ui.target.aria_label`,desc.ariaLabel),desc.text&&span.setAttribute(`ui.target.text`,desc.text)}function stampUrlAttrs(span,resolveRoute){if(typeof window>`u`)return;let pathname=window.location.pathname,route=resolveRoute?.(pathname);span.setAttribute(ATTR_URL_PATH,route??pathname),span.setAttribute(ATTR_URL_FULL,window.location.href),route&&span.setAttribute(ATTR_HTTP_ROUTE,route)}function registerBundledInstrumentations(input){let{tracerProvider,loggerProvider,ignoreUrls,propagateContextUrls=[]}=input,resolveRoute=input.resolveRoute,origin=sameOriginPattern(),fetchIgnore=[...ignoreUrls,...THIRD_PARTY_IGNORE],failureRanges=resolveFailureRanges(input.captureFailedRequests),failureGate=createFailureGate();function isTrackedUrl(url){return origin.test(url)||propagateContextUrls.some(re=>re instanceof RegExp?re.test(url):url.includes(re))}return registerInstrumentations({tracerProvider,...loggerProvider?{loggerProvider}:{},instrumentations:[new FetchInstrumentation({propagateTraceHeaderCorsUrls:[origin,...propagateContextUrls],ignoreUrls:fetchIgnore,semconvStabilityOptIn:SEMCONV_STABILITY_OPT_IN,measureRequestSize:!0,applyCustomAttributesOnSpan:(span,request,result)=>{applyFetchSpanAttrs(span,request,result,failureRanges,isTrackedUrl,resolveRoute,failureGate)}}),new XMLHttpRequestInstrumentation({propagateTraceHeaderCorsUrls:[origin,...propagateContextUrls],ignoreUrls:fetchIgnore,semconvStabilityOptIn:SEMCONV_STABILITY_OPT_IN,applyCustomAttributesOnSpan:(span,xhr)=>{applyXhrSpanAttrs(span,xhr,failureRanges,isTrackedUrl,failureGate)}}),new DocumentLoadInstrumentation({semconvStabilityOptIn:SEMCONV_STABILITY_OPT_IN,applyCustomAttributesOnSpan:{resourceFetch:(span,entry)=>{let name=entry?.name;if(typeof name==`string`)try{let url=new URL(name),isCrossOrigin=typeof window<`u`&&url.origin!==window.location.origin;span.updateName(isCrossOrigin?`resource: ${url.host}${url.pathname}`:`resource: ${url.pathname}`),span.setAttribute(ATTR_URL_FULL,name),span.setAttribute(ATTR_URL_PATH,url.pathname),span.setAttribute(`server.address`,url.host)}catch{return}}}}),new BrowserNavigationInstrumentation({sanitizeUrl:defaultSanitizeUrl}),new UserInteractionInstrumentation({eventNames:[`click`,`submit`],shouldPreventSpanCreation:(eventType,element,span)=>eventType===`click`&&!isActionable(element)?!0:(stampInteractionAttrs(eventType,element,span),stampUrlAttrs(span,resolveRoute),!1)}),new LongTaskInstrumentation({observerCallback:(span,info)=>{stampUrlAttrs(span,resolveRoute);let containerType=info.longtaskEntry.attribution[0]?.containerType;containerType&&span.setAttribute(`longtask.container_type`,containerType)}})]})}export{_testOnlyApplyFetchSpanAttrs,_testOnlyApplyXhrSpanAttrs,_testOnlyClassifyNetworkError,_testOnlyCreateFailureGate,_testOnlyResolveFailureRanges,registerBundledInstrumentations};
@@ -2,8 +2,8 @@ import { Context } from "@opentelemetry/api";
2
2
 
3
3
  //#region src/internal/otel/propagation.d.ts
4
4
  /**
5
- * Reads the W3C `traceparent` (and optional `tracestate`) meta tag from the
6
- * document head and extracts an OTel `Context`. SSR renderers (Next, Vite,
5
+ * Reads the W3C `traceparent` (and optional `tracestate` / `baggage`) meta
6
+ * tags from the document head and extracts an OTel `Context`. SSR renderers (Next, Vite,
7
7
  * …) inject the server-side request span's context so the client SDK can
8
8
  * stitch its spans onto the same trace.
9
9
  *
@@ -1 +1 @@
1
- import{ROOT_CONTEXT,defaultTextMapGetter,trace}from"@opentelemetry/api";import{W3CTraceContextPropagator}from"@opentelemetry/core";const TRACESTATE_META=`tracestate`,W3C_PROPAGATOR=new W3CTraceContextPropagator;function readMeta(name){return typeof document>`u`?null:document.querySelector(`meta[name="${name}"]`)?.getAttribute(`content`)??null}function readPropagationFromDocument(){let traceparent=readMeta(`traceparent`);if(!traceparent)return null;let carrier={traceparent},tracestate=readMeta(TRACESTATE_META);tracestate&&(carrier[TRACESTATE_META]=tracestate);let extracted=W3C_PROPAGATOR.extract(ROOT_CONTEXT,carrier,defaultTextMapGetter),spanCtx=trace.getSpanContext(extracted);return!spanCtx||spanCtx.traceId===`00000000000000000000000000000000`||!(spanCtx.traceFlags&1)?null:extracted}export{readPropagationFromDocument};
1
+ import{ROOT_CONTEXT,defaultTextMapGetter,trace}from"@opentelemetry/api";import{W3CBaggagePropagator,W3CTraceContextPropagator}from"@opentelemetry/core";const TRACESTATE_META=`tracestate`,BAGGAGE_META=`baggage`,W3C_PROPAGATOR=new W3CTraceContextPropagator,W3C_BAGGAGE_PROPAGATOR=new W3CBaggagePropagator;function readMeta(name){return typeof document>`u`?null:document.querySelector(`meta[name="${name}"]`)?.getAttribute(`content`)??null}function readPropagationFromDocument(){let traceparent=readMeta(`traceparent`);if(!traceparent)return null;let carrier={traceparent},tracestate=readMeta(TRACESTATE_META);tracestate&&(carrier[TRACESTATE_META]=tracestate);let baggage=readMeta(BAGGAGE_META);baggage&&(carrier[BAGGAGE_META]=baggage);let withTrace=W3C_PROPAGATOR.extract(ROOT_CONTEXT,carrier,defaultTextMapGetter),extracted=W3C_BAGGAGE_PROPAGATOR.extract(withTrace,carrier,defaultTextMapGetter),spanCtx=trace.getSpanContext(extracted);return!spanCtx||spanCtx.traceId===`00000000000000000000000000000000`||!(spanCtx.traceFlags&1)?null:extracted}export{readPropagationFromDocument};
@@ -1,6 +1,6 @@
1
- import { PluginContext } from "../plugins/lib/types.mjs";
1
+ import { PluginConfigState, PluginContext } from "../plugins/lib/types.mjs";
2
2
  import { PluginOverrides } from "../plugins/lib/loader.mjs";
3
- import { ConsentState } from "@interfere/types/sdk/plugins/manifest";
3
+ import { ConsentState, PluginKey } from "@interfere/types/sdk/plugins/manifest";
4
4
  import { RemotePluginConfig } from "@interfere/types/sdk/remote-config";
5
5
 
6
6
  //#region src/internal/plugin-runtime.d.ts
@@ -9,7 +9,9 @@ declare class PluginRuntime {
9
9
  private readonly pending;
10
10
  private readonly context;
11
11
  private readonly features;
12
+ private readonly pluginConfigListeners;
12
13
  private remoteConfig;
14
+ private remoteConfigSettled;
13
15
  private consentState;
14
16
  private syncVersion;
15
17
  private disposed;
@@ -18,9 +20,13 @@ declare class PluginRuntime {
18
20
  setConsent(nextConsent?: ConsentState): void;
19
21
  resetConsent(): void;
20
22
  applyRemoteConfig(config: RemotePluginConfig): void;
23
+ applyRemoteConfigDefaults(): void;
21
24
  start(): void;
22
25
  dispose(): Promise<void>;
23
26
  private shouldEnablePlugin;
27
+ getPluginConfigState(key: PluginKey): PluginConfigState;
28
+ subscribePluginConfigState(key: PluginKey, listener: (state: PluginConfigState) => void): () => void;
29
+ private emitPluginConfigState;
24
30
  private deactivate;
25
31
  private activate;
26
32
  private sync;
@@ -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;remoteConfig={};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.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)}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;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};
@@ -13,6 +13,7 @@ interface InterfereContextValue {
13
13
  getFpHash(): string | null;
14
14
  };
15
15
  identity: {
16
+ clear(): void;
16
17
  get(): IdentifyParams | null;
17
18
  set(params: IdentifyParams): Promise<void>;
18
19
  };
@@ -1 +1 @@
1
- "use client";import{createContext}from"react";const NULL_CONTEXT_VALUE={consent:{get:()=>null,set:()=>void 0},device:{getDeviceId:()=>null,getFpHash:()=>null},identity:{get:()=>null,set:()=>Promise.resolve()},kernel:null,session:{getId:()=>null,getWindowId:()=>null}},InterfereContext=createContext(null);export{InterfereContext,NULL_CONTEXT_VALUE};
1
+ "use client";import{createContext}from"react";const NULL_CONTEXT_VALUE={consent:{get:()=>null,set:()=>void 0},device:{getDeviceId:()=>null,getFpHash:()=>null},identity:{clear:()=>void 0,get:()=>null,set:()=>Promise.resolve()},kernel:null,session:{getId:()=>null,getWindowId:()=>null}},InterfereContext=createContext(null);export{InterfereContext,NULL_CONTEXT_VALUE};
@@ -0,0 +1,9 @@
1
+ //#region src/internal/server-digest.d.ts
2
+ /**
3
+ * True when a thrown value carries a Next.js server `digest` — it originated on
4
+ * the server and was already captured there (`onRequestError`). Client capture
5
+ * sites skip these so one server error isn't recorded again on the client.
6
+ */
7
+ declare function hasServerDigest(value: unknown): boolean;
8
+ //#endregion
9
+ export { hasServerDigest };
@@ -0,0 +1 @@
1
+ function hasServerDigest(value){return typeof value==`object`&&!!value&&typeof value.digest==`string`}export{hasServerDigest};
@@ -0,0 +1,26 @@
1
+ //#region src/internal/sw-queue.d.ts
2
+ interface IngestQueueMessage {
3
+ reason?: string;
4
+ type: "interfere.sw.queued" | "interfere.sw.replayed" | "interfere.sw.dropped";
5
+ url: string;
6
+ }
7
+ interface InstallIngestQueueOptions {
8
+ /**
9
+ * Predicate selecting which POSTs to durably queue on failure. Proxy-mode
10
+ * customers leave this unset (same-origin `/api/interfere/*`); a host that
11
+ * posts ingest elsewhere (e.g. a direct-mode SPA hitting the collector
12
+ * cross-origin) passes its own matcher.
13
+ */
14
+ match?: (url: URL, request: Request) => boolean;
15
+ queueName?: string;
16
+ }
17
+ /**
18
+ * Wire the ingest durability queue into the current service worker. Adds
19
+ * `fetch` (intercept + queue matching POSTs) and `activate` (replay)
20
+ * listeners only — it does NOT touch `skipWaiting` / `clients.claim`, so
21
+ * a host SW (e.g. the app's Workbox precache worker) keeps full ownership
22
+ * of its update lifecycle. Additive listeners coexist with the host's.
23
+ */
24
+ declare function installIngestQueue(options?: InstallIngestQueueOptions): void;
25
+ //#endregion
26
+ export { IngestQueueMessage, InstallIngestQueueOptions, installIngestQueue };
@@ -0,0 +1 @@
1
+ import{Queue}from"workbox-background-sync";const SYNTHETIC_QUEUED_RESPONSE={status:202,statusText:`Queued`};async function postToClients(message){let clients=await self.clients.matchAll({includeUncontrolled:!1});for(let client of clients)client.postMessage(message)}async function totalQueueBytes(queue){let entries=await queue.getAll(),total=0;for(let entry of entries){let meta=entry.metadata;typeof meta?.bytes==`number`&&(total+=meta.bytes)}return total}async function pruneOldest(queue,neededBytes){let total=await totalQueueBytes(queue);for(;total+neededBytes>10485760;){let dropped=await queue.shiftRequest();if(!dropped)break;let meta=dropped.metadata;total-=meta?.bytes??0,await postToClients({type:`interfere.sw.dropped`,url:dropped.request.url,reason:`queue_size_cap`})}}async function enqueue(queue,request){let cloned=request.clone(),bytes=(await cloned.clone().arrayBuffer()).byteLength;await pruneOldest(queue,bytes),await queue.pushRequest({request:cloned,metadata:{bytes}}),await postToClients({type:`interfere.sw.queued`,url:request.url})}function installIngestQueue(options={}){let matches=options.match??(url=>url.pathname.startsWith(`/api/interfere/`)),queue=new Queue(options.queueName??`interfere-ingest`,{maxRetentionTime:1440,onSync:async({queue:q})=>{let entry=await q.shiftRequest();for(;entry;){try{let res=await self.fetch(entry.request.clone());if(!res.ok&&(res.status>=500||res.status===429))throw Error(`retry status ${res.status}`);await postToClients({type:`interfere.sw.replayed`,url:entry.request.url})}catch(err){throw await q.unshiftRequest(entry),err}entry=await q.shiftRequest()}}});self.addEventListener(`activate`,event=>{event.waitUntil(queue.replayRequests().catch(()=>void 0))}),self.addEventListener(`fetch`,event=>{let{request}=event;if(request.method!==`POST`)return;let url;try{url=new URL(request.url)}catch{return}matches(url,request)&&event.respondWith((async()=>{try{let res=await self.fetch(request.clone());return res.ok?res:res.status>=500||res.status===429?(await enqueue(queue,request),new Response(null,SYNTHETIC_QUEUED_RESPONSE)):res}catch{return await enqueue(queue,request),new Response(null,SYNTHETIC_QUEUED_RESPONSE)}})())})}export{installIngestQueue};
@@ -0,0 +1,14 @@
1
+ //#region src/internal/third-party-hosts.d.ts
2
+ /**
3
+ * Hosts/paths whose client-side fetch / XHR / resource loads we never
4
+ * surface: common analytics / ads / auth / replay vendors that otherwise
5
+ * turn into orphan-span and resource-error noise. Conservative and
6
+ * customer-generic — app-specific noise belongs in the customer's own
7
+ * `ignoreUrls` config, not hardcoded into a public SDK.
8
+ *
9
+ * Dependency-free on purpose so the error-only bundle can reuse it without
10
+ * pulling in the OTel SDK that `internal/otel/*` carries.
11
+ */
12
+ declare const THIRD_PARTY_IGNORE: readonly RegExp[];
13
+ //#endregion
14
+ export { THIRD_PARTY_IGNORE };
@@ -0,0 +1 @@
1
+ const THIRD_PARTY_IGNORE=[/(?:^|\/\/)clerk\./,/\.clerk\./,/\.sentry\./,/\.intercom\./,/\.posthog\./,/\.googletagmanager\.com/,/\.google-analytics\.com/,/\.googleapis\.com/,/\.doubleclick\.net/,/\.facebook\.com/,/\.fbcdn\.net/,/\.analytics\.google\.com/,/px\.ads\.linkedin\.com/,/[?&]_rsc=/];export{THIRD_PARTY_IGNORE};
@@ -12,6 +12,7 @@ interface WrapperSingleton {
12
12
  getKernel(): Kernel;
13
13
  getKernelOrNull(): Kernel | null;
14
14
  identity: {
15
+ clear(): void;
15
16
  get(): IdentifyParams | null;
16
17
  set(params: IdentifyParams): Promise<void>;
17
18
  };
@@ -1 +1 @@
1
- import{registerKernel,unregisterKernel}from"./kernel-registry.mjs";import{createKernel,isEnabledByEnvironment}from"./kernel.mjs";import{isBotUserAgent}from"../util/bot.mjs";function createWrapperSingleton(input){let kernel=null,pending=null,listeners=new Set;function emit(){for(let listener of listeners)listener()}return{init(opts={}){return kernel?Promise.resolve(kernel):pending||(!(opts.enabled??isEnabledByEnvironment())||isBotUserAgent()?Promise.resolve(null):(pending=createKernel({opts:{...opts,_wrapperVersions:[input.producerVersion]}}).then(k=>(kernel=k,k&&registerKernel(k),emit(),k)).finally(()=>{pending=null}),pending))},getKernel(){if(!kernel)throw Error(`Interfere SDK not initialized. Call init() from your ${input.initEntryName} entrypoint.`);return kernel},getKernelOrNull(){return kernel},async close(){if(!kernel)return;let previous=kernel;await previous.dispose(),kernel=null,unregisterKernel(previous),emit()},subscribeToKernel(listener){return listeners.add(listener),()=>{listeners.delete(listener)}},consent:{get:()=>kernel?.consent.get()??null,set:value=>kernel?.consent.set(value)},identity:{get:()=>kernel?.identity.get()??null,set:params=>kernel?.identity.set(params)??Promise.resolve()}}}export{createWrapperSingleton};
1
+ import{registerKernel,unregisterKernel}from"./kernel-registry.mjs";import{createKernel,isEnabledByEnvironment}from"./kernel.mjs";import{isBotUserAgent}from"../util/bot.mjs";function createWrapperSingleton(input){let kernel=null,pending=null,listeners=new Set;function emit(){for(let listener of listeners)listener()}return{init(opts={}){return kernel?Promise.resolve(kernel):pending||(!(opts.enabled??isEnabledByEnvironment())||isBotUserAgent()?Promise.resolve(null):(pending=createKernel({opts:{...opts,_wrapperVersions:[input.producerVersion]}}).then(k=>(kernel=k,k&&registerKernel(k),emit(),k)).finally(()=>{pending=null}),pending))},getKernel(){if(!kernel)throw Error(`Interfere SDK not initialized. Call init() from your ${input.initEntryName} entrypoint.`);return kernel},getKernelOrNull(){return kernel},async close(){if(!kernel)return;let previous=kernel;await previous.dispose(),kernel=null,unregisterKernel(previous),emit()},subscribeToKernel(listener){return listeners.add(listener),()=>{listeners.delete(listener)}},consent:{get:()=>kernel?.consent.get()??null,set:value=>kernel?.consent.set(value)},identity:{clear:()=>kernel?.identity.clear(),get:()=>kernel?.identity.get()??null,set:params=>kernel?.identity.set(params)??Promise.resolve()}}}export{createWrapperSingleton};
package/dist/package.mjs CHANGED
@@ -1 +1 @@
1
- var name=`@interfere/react`,version=`11.0.0-canary.2`;export{name,version};
1
+ var name=`@interfere/react`,version=`11.0.0-canary.4`;export{name,version};
@@ -3,4 +3,4 @@ import { Plugin } from "./lib/types.mjs";
3
3
  //#region src/plugins/errors.d.ts
4
4
  declare const errorsPlugin: Plugin;
5
5
  //#endregion
6
- export { errorsPlugin as default, errorsPlugin };
6
+ export { errorsPlugin as default };
@@ -1 +1 @@
1
- import{onConsoleCall}from"../internal/console-patch.mjs";import{MECHANISM_TYPE,isNonErrorException,toException}from"@interfere/types/sdk/errors";function capture(ctx,opts){ctx.recordException(opts.error,{mechanism:opts.mechanism,...opts.fallbackFrame&&!isNonErrorException(opts.error)?{fallbackFrames:[opts.fallbackFrame]}:{}})}function findErrorArg(args){for(let arg of args)if(arg instanceof Error)return arg;return null}const errorsPlugin={name:`errors`,setup(ctx){let originalOnError=globalThis.onerror,originalStackTraceLimit=Error.stackTraceLimit;Error.stackTraceLimit<50&&(Error.stackTraceLimit=50),globalThis.onerror=(msg,source,line,col,error)=>{if(error instanceof Error){let fallbackFrame=typeof source==`string`?{id:`fallback-0`,file:source,...typeof line==`number`?{line}:{},...typeof col==`number`?{column:col}:{}}:null;capture(ctx,{error,mechanism:{type:MECHANISM_TYPE.browser.onerror,handled:!1},...fallbackFrame?{fallbackFrame}:{}})}return typeof originalOnError==`function`?originalOnError.call(globalThis,msg,source,line,col,error):!1};let onUnhandledRejection=event=>{capture(ctx,{error:toException(event.reason),mechanism:{type:MECHANISM_TYPE.browser.onunhandledrejection,handled:!1,...event.reason instanceof Error?{}:{synthetic:!0}}})},supportsEventTarget=typeof globalThis.addEventListener==`function`&&typeof globalThis.removeEventListener==`function`;supportsEventTarget&&globalThis.addEventListener(`unhandledrejection`,onUnhandledRejection);let unsubscribeConsole=onConsoleCall((level,args)=>{if(level!==`error`)return;let error=findErrorArg(args);error&&capture(ctx,{error,mechanism:{type:MECHANISM_TYPE.browser.consoleError,handled:!0}})});return()=>{Error.stackTraceLimit=originalStackTraceLimit,globalThis.onerror=originalOnError,supportsEventTarget&&globalThis.removeEventListener(`unhandledrejection`,onUnhandledRejection),unsubscribeConsole()}}};export{errorsPlugin as default,errorsPlugin};
1
+ import{hasServerDigest}from"../internal/server-digest.mjs";import{onConsoleCall}from"../internal/console-patch.mjs";import{THIRD_PARTY_IGNORE}from"../internal/third-party-hosts.mjs";import{MECHANISM_TYPE,isNonErrorException,toException}from"@interfere/types/sdk/errors";function capture(ctx,opts){ctx.recordException(opts.error,{mechanism:opts.mechanism,...opts.fallbackFrame&&!isNonErrorException(opts.error)?{fallbackFrames:[opts.fallbackFrame]}:{}})}function findErrorArg(args){return args.find(arg=>arg instanceof Error)??null}function resourceError(target){return target instanceof HTMLImageElement||target instanceof HTMLScriptElement?target.src?{tag:target.localName,url:target.src}:null:target instanceof HTMLLinkElement&&target.href?{tag:target.localName,url:target.href}:null}function isIgnoredResourceUrl(url){return THIRD_PARTY_IGNORE.some(pattern=>pattern.test(url))}function createResourceGate(){let seen=new Set,windowStart=0,windowCount=0;return url=>{if(seen.has(url))return!1;let now=Date.now();return now-windowStart>6e4&&(windowStart=now,windowCount=0),windowCount>=20?!1:(seen.size>=100&&seen.clear(),seen.add(url),windowCount+=1,!0)}}const errorsPlugin={name:`errors`,setup(ctx){let originalOnError=globalThis.onerror,originalStackTraceLimit=Error.stackTraceLimit;Error.stackTraceLimit<50&&(Error.stackTraceLimit=50),globalThis.onerror=(msg,source,line,col,error)=>{if(error instanceof Error&&!hasServerDigest(error)){let fallbackFrame=typeof source==`string`?{id:`fallback-0`,file:source,...typeof line==`number`?{line}:{},...typeof col==`number`?{column:col}:{}}:null;capture(ctx,{error,mechanism:{type:MECHANISM_TYPE.browser.onerror,handled:!1},...fallbackFrame?{fallbackFrame}:{}})}return typeof originalOnError==`function`?originalOnError.call(globalThis,msg,source,line,col,error):!1};let onUnhandledRejection=event=>{hasServerDigest(event.reason)||capture(ctx,{error:toException(event.reason),mechanism:{type:MECHANISM_TYPE.browser.onunhandledrejection,handled:!1,...event.reason instanceof Error?{}:{synthetic:!0}}})},seenCspReports=new Set,onSecurityPolicyViolation=event=>{let key=`${event.violatedDirective}|${event.blockedURI}`;seenCspReports.has(key)||seenCspReports.size>=5||(seenCspReports.add(key),capture(ctx,{error:{kind:`non-error`,type:`SecurityPolicyViolation`,value:`CSP ${event.violatedDirective} blocked ${event.blockedURI||`inline`}`,serialized:{violatedDirective:event.violatedDirective,blockedURI:event.blockedURI,sourceFile:event.sourceFile,lineNumber:event.lineNumber,columnNumber:event.columnNumber,disposition:event.disposition}},mechanism:{type:MECHANISM_TYPE.browser.securityPolicyViolation,handled:!1}}))},resourceGate=createResourceGate(),onResourceError=event=>{let resource=resourceError(event.target);!resource||isIgnoredResourceUrl(resource.url)||!resourceGate(resource.url)||capture(ctx,{error:{kind:`non-error`,type:`ResourceLoadError`,value:`Failed to load ${resource.tag}: ${resource.url}`,serialized:{url:resource.url}},mechanism:{type:MECHANISM_TYPE.browser.resourceError,handled:!1}})},supportsEventTarget=typeof globalThis.addEventListener==`function`&&typeof globalThis.removeEventListener==`function`;supportsEventTarget&&(globalThis.addEventListener(`unhandledrejection`,onUnhandledRejection),globalThis.addEventListener(`securitypolicyviolation`,onSecurityPolicyViolation),globalThis.addEventListener(`error`,onResourceError,!0));let unsubscribeConsole=onConsoleCall((level,args)=>{if(level!==`error`)return;let error=findErrorArg(args);error&&!hasServerDigest(error)&&capture(ctx,{error,mechanism:{type:MECHANISM_TYPE.browser.consoleError,handled:!0}})});return()=>{Error.stackTraceLimit=originalStackTraceLimit,globalThis.onerror=originalOnError,supportsEventTarget&&(globalThis.removeEventListener(`unhandledrejection`,onUnhandledRejection),globalThis.removeEventListener(`securitypolicyviolation`,onSecurityPolicyViolation),globalThis.removeEventListener(`error`,onResourceError,!0)),unsubscribeConsole()}}};export{errorsPlugin as default};
@@ -1,15 +1,22 @@
1
+ import { SessionResumeReason } from "../../internal/activity.mjs";
1
2
  import { RecordExceptionOpts } from "../../internal/kernel.mjs";
2
3
  import { NonErrorException } from "@interfere/types/sdk/errors";
4
+ import { PluginKey } from "@interfere/types/sdk/plugins/manifest";
3
5
 
4
6
  //#region src/plugins/lib/types.d.ts
5
7
  type PluginCleanup = () => void;
8
+ type PluginConfigState = "disabled" | "enabled" | "pending";
6
9
  interface PluginContext {
10
+ getPluginConfigState(key: PluginKey): PluginConfigState;
7
11
  getSessionId(): string;
12
+ onSessionPause(handler: () => void): () => void;
13
+ onSessionResume(handler: (reason: SessionResumeReason) => void): () => void;
8
14
  recordException(value: Error | NonErrorException, opts: RecordExceptionOpts): void;
15
+ subscribePluginConfigState(key: PluginKey, listener: (state: PluginConfigState) => void): () => void;
9
16
  }
10
17
  interface Plugin {
11
18
  readonly name: string;
12
19
  setup(ctx: PluginContext): PluginCleanup | undefined;
13
20
  }
14
21
  //#endregion
15
- export { Plugin, PluginCleanup, PluginContext };
22
+ export { Plugin, PluginCleanup, PluginConfigState, PluginContext };
@@ -10,4 +10,4 @@ import { Plugin } from "./lib/types.mjs";
10
10
  */
11
11
  declare const logsPlugin: Plugin;
12
12
  //#endregion
13
- export { logsPlugin as default, logsPlugin };
13
+ export { logsPlugin as default };
@@ -1 +1 @@
1
- import{activeKernel}from"../internal/kernel-registry.mjs";import{onConsoleCall}from"../internal/console-patch.mjs";import{safeStringify}from"../util/stringify.mjs";import{SeverityNumber}from"@opentelemetry/api-logs";const LEVEL_TO_SEVERITY={debug:{number:SeverityNumber.DEBUG,text:`DEBUG`},info:{number:SeverityNumber.INFO,text:`INFO`},log:{number:SeverityNumber.INFO,text:`INFO`},warn:{number:SeverityNumber.WARN,text:`WARN`},error:{number:SeverityNumber.ERROR,text:`ERROR`}},logsPlugin={name:`logs`,setup(){return onConsoleCall((level,args)=>{if(args.some(a=>a instanceof Error))return;let kernel=activeKernel();if(!kernel)return;let sev=LEVEL_TO_SEVERITY[level];kernel.recordLog({severityText:sev.text,severityNumber:sev.number,body:args.length===1?safeStringify(args[0]):args.map(safeStringify).join(` `),attributes:{"console.level":level}})})}};export{logsPlugin as default,logsPlugin};
1
+ import{activeKernel}from"../internal/kernel-registry.mjs";import{onConsoleCall}from"../internal/console-patch.mjs";import{safeStringify}from"../util/stringify.mjs";import{SeverityNumber}from"@opentelemetry/api-logs";const LEVEL_TO_SEVERITY={debug:{number:SeverityNumber.DEBUG,text:`DEBUG`},info:{number:SeverityNumber.INFO,text:`INFO`},log:{number:SeverityNumber.INFO,text:`INFO`},warn:{number:SeverityNumber.WARN,text:`WARN`},error:{number:SeverityNumber.ERROR,text:`ERROR`}},logsPlugin={name:`logs`,setup(){return onConsoleCall((level,args)=>{if(args.some(a=>a instanceof Error))return;let kernel=activeKernel();if(!kernel)return;let sev=LEVEL_TO_SEVERITY[level];kernel.recordLog({severityText:sev.text,severityNumber:sev.number,body:args.length===1?safeStringify(args[0]):args.map(safeStringify).join(` `),attributes:{"console.level":level}})})}};export{logsPlugin as default};
@@ -3,4 +3,4 @@ import { Plugin } from "./lib/types.mjs";
3
3
  //#region src/plugins/rage-clicks.d.ts
4
4
  declare const rageClicksPlugin: Plugin;
5
5
  //#endregion
6
- export { rageClicksPlugin as default, rageClicksPlugin };
6
+ export { rageClicksPlugin as default };
@@ -1 +1 @@
1
- import{trace}from"@opentelemetry/api";function distance(a,b){return Math.sqrt((a.x-b.x)**2+(a.y-b.y)**2)}function selectorFor(el){if(!el)return`unknown`;if(el.id)return`#${el.id}`;let classes=[...el.classList].slice(0,3).join(`.`),tag=el.tagName.toLowerCase();return classes?`${tag}.${classes}`:tag}const rageClicksPlugin={name:`rage-clicks`,setup(){let clicks=[],onClick=event=>{let now=Date.now();for(clicks.push({x:event.clientX,y:event.clientY,ts:now,target:event.target instanceof Element?event.target:null});clicks.length>0&&now-(clicks[0]?.ts??0)>800;)clicks.shift();if(clicks.length<3)return;let anchor=clicks[0];if(!anchor)return;let clustered=clicks.filter(c=>distance(anchor,c)<=30);if(clustered.length<3)return;let last=clustered.at(-1);last&&(trace.getTracer(`@interfere/react/rage-clicks`).startSpan(`rage_click`,{attributes:{"ui.event_type":`rage_click`,"ui.rage_click.count":clustered.length,"ui.rage_click.time_window_ms":last.ts-anchor.ts,"ui.rage_click.selector":selectorFor(last.target),"ui.rage_click.text":last.target?.textContent?.trim().slice(0,120)??``,"ui.rage_click.x":last.x,"ui.rage_click.y":last.y}}).end(),clicks.length=0)};return document.addEventListener(`click`,onClick,{capture:!0}),()=>{document.removeEventListener(`click`,onClick,{capture:!0})}}};export{rageClicksPlugin as default,rageClicksPlugin};
1
+ import{trace}from"@opentelemetry/api";function distance(a,b){return Math.sqrt((a.x-b.x)**2+(a.y-b.y)**2)}function selectorFor(el){if(!el)return`unknown`;if(el.id)return`#${el.id}`;let classes=[...el.classList].slice(0,3).join(`.`),tag=el.tagName.toLowerCase();return classes?`${tag}.${classes}`:tag}const rageClicksPlugin={name:`rage-clicks`,setup(){let clicks=[],onClick=event=>{let now=Date.now();for(clicks.push({x:event.clientX,y:event.clientY,ts:now,target:event.target instanceof Element?event.target:null});clicks.length>0&&now-(clicks[0]?.ts??0)>800;)clicks.shift();if(clicks.length<3)return;let anchor=clicks[0];if(!anchor)return;let clustered=clicks.filter(c=>distance(anchor,c)<=30);if(clustered.length<3)return;let last=clustered.at(-1);last&&(trace.getTracer(`@interfere/react/rage-clicks`).startSpan(`rage_click`,{attributes:{"ui.event_type":`rage_click`,"ui.rage_click.count":clustered.length,"ui.rage_click.time_window_ms":last.ts-anchor.ts,"ui.rage_click.selector":selectorFor(last.target),"ui.rage_click.text":last.target?.textContent?.trim().slice(0,120)??``,"ui.rage_click.x":last.x,"ui.rage_click.y":last.y}}).end(),clicks.length=0)};return document.addEventListener(`click`,onClick,{capture:!0}),()=>{document.removeEventListener(`click`,onClick,{capture:!0})}}};export{rageClicksPlugin as default};
@@ -3,4 +3,4 @@ import { Plugin } from "../lib/types.mjs";
3
3
  //#region src/plugins/replay/index.d.ts
4
4
  declare const replayPlugin: Plugin;
5
5
  //#endregion
6
- export { replayPlugin as default, replayPlugin };
6
+ export { replayPlugin as default };