@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.
- package/dist/internal/activity.d.mts +38 -0
- package/dist/internal/activity.mjs +1 -0
- package/dist/internal/capture-boundary.mjs +1 -1
- package/dist/internal/capture.mjs +1 -1
- package/dist/internal/hydration.d.mts +10 -0
- package/dist/internal/hydration.mjs +1 -0
- package/dist/internal/kernel.d.mts +31 -11
- package/dist/internal/kernel.mjs +1 -1
- package/dist/internal/otel/initial-navigation.d.mts +13 -0
- package/dist/internal/otel/initial-navigation.mjs +1 -0
- package/dist/internal/otel/instrumentations.d.mts +25 -5
- package/dist/internal/otel/instrumentations.mjs +1 -1
- package/dist/internal/otel/propagation.d.mts +2 -2
- package/dist/internal/otel/propagation.mjs +1 -1
- package/dist/internal/plugin-runtime.d.mts +8 -2
- package/dist/internal/plugin-runtime.mjs +1 -1
- package/dist/internal/react-context.d.mts +1 -0
- package/dist/internal/react-context.mjs +1 -1
- package/dist/internal/server-digest.d.mts +9 -0
- package/dist/internal/server-digest.mjs +1 -0
- package/dist/internal/sw-queue.d.mts +26 -0
- package/dist/internal/sw-queue.mjs +1 -0
- package/dist/internal/third-party-hosts.d.mts +14 -0
- package/dist/internal/third-party-hosts.mjs +1 -0
- package/dist/internal/wrapper-singleton.d.mts +1 -0
- package/dist/internal/wrapper-singleton.mjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/errors.d.mts +1 -1
- package/dist/plugins/errors.mjs +1 -1
- package/dist/plugins/lib/types.d.mts +8 -1
- package/dist/plugins/logs.d.mts +1 -1
- package/dist/plugins/logs.mjs +1 -1
- package/dist/plugins/rage-clicks.d.mts +1 -1
- package/dist/plugins/rage-clicks.mjs +1 -1
- package/dist/plugins/replay/index.d.mts +1 -1
- package/dist/plugins/replay/index.mjs +1 -1
- package/dist/react-error-handler.d.mts +1 -1
- package/dist/sw.mjs +1 -1
- package/dist/tracking/api.d.mts +8 -0
- package/dist/tracking/api.mjs +1 -1
- package/dist/tracking/session.d.mts +4 -0
- package/dist/tracking/session.mjs +1 -1
- package/package.json +32 -33
- package/dist/api.d.mts.map +0 -1
- package/dist/api.mjs.map +0 -1
- package/dist/error-boundary.d.mts.map +0 -1
- package/dist/error-boundary.mjs.map +0 -1
- package/dist/internal/browser-context.d.mts.map +0 -1
- package/dist/internal/browser-context.mjs.map +0 -1
- package/dist/internal/capture-boundary.d.mts.map +0 -1
- package/dist/internal/capture-boundary.mjs.map +0 -1
- package/dist/internal/capture.d.mts.map +0 -1
- package/dist/internal/capture.mjs.map +0 -1
- package/dist/internal/config.d.mts.map +0 -1
- package/dist/internal/config.mjs.map +0 -1
- package/dist/internal/consent.d.mts.map +0 -1
- package/dist/internal/consent.mjs.map +0 -1
- package/dist/internal/console-patch.d.mts.map +0 -1
- package/dist/internal/console-patch.mjs.map +0 -1
- package/dist/internal/dom/actionable.d.mts.map +0 -1
- package/dist/internal/dom/actionable.mjs.map +0 -1
- package/dist/internal/kernel-registry.d.mts.map +0 -1
- package/dist/internal/kernel-registry.mjs.map +0 -1
- package/dist/internal/kernel.d.mts.map +0 -1
- package/dist/internal/kernel.mjs.map +0 -1
- package/dist/internal/otel/exporter.d.mts.map +0 -1
- package/dist/internal/otel/exporter.mjs.map +0 -1
- package/dist/internal/otel/instrumentations.d.mts.map +0 -1
- package/dist/internal/otel/instrumentations.mjs.map +0 -1
- package/dist/internal/otel/page-scope-context-manager.d.mts.map +0 -1
- package/dist/internal/otel/page-scope-context-manager.mjs.map +0 -1
- package/dist/internal/otel/propagation.d.mts.map +0 -1
- package/dist/internal/otel/propagation.mjs.map +0 -1
- package/dist/internal/otel/provider.d.mts.map +0 -1
- package/dist/internal/otel/provider.mjs.map +0 -1
- package/dist/internal/otel/web-vitals.d.mts.map +0 -1
- package/dist/internal/otel/web-vitals.mjs.map +0 -1
- package/dist/internal/page-lifecycle.d.mts.map +0 -1
- package/dist/internal/page-lifecycle.mjs.map +0 -1
- package/dist/internal/plugin-runtime.d.mts.map +0 -1
- package/dist/internal/plugin-runtime.mjs.map +0 -1
- package/dist/internal/react-context.d.mts.map +0 -1
- package/dist/internal/react-context.mjs.map +0 -1
- package/dist/internal/sw.d.mts.map +0 -1
- package/dist/internal/sw.mjs.map +0 -1
- package/dist/internal/version.d.mts.map +0 -1
- package/dist/internal/version.mjs.map +0 -1
- package/dist/internal/wrapper-singleton.d.mts.map +0 -1
- package/dist/internal/wrapper-singleton.mjs.map +0 -1
- package/dist/package.mjs.map +0 -1
- package/dist/plugins/errors.d.mts.map +0 -1
- package/dist/plugins/errors.mjs.map +0 -1
- package/dist/plugins/lib/loader.d.mts.map +0 -1
- package/dist/plugins/lib/loader.mjs.map +0 -1
- package/dist/plugins/lib/types.d.mts.map +0 -1
- package/dist/plugins/logs.d.mts.map +0 -1
- package/dist/plugins/logs.mjs.map +0 -1
- package/dist/plugins/rage-clicks.d.mts.map +0 -1
- package/dist/plugins/rage-clicks.mjs.map +0 -1
- package/dist/plugins/replay/buffer.d.mts.map +0 -1
- package/dist/plugins/replay/buffer.mjs.map +0 -1
- package/dist/plugins/replay/chunking.d.mts.map +0 -1
- package/dist/plugins/replay/chunking.mjs.map +0 -1
- package/dist/plugins/replay/compressor-host.d.mts.map +0 -1
- package/dist/plugins/replay/compressor-host.mjs.map +0 -1
- package/dist/plugins/replay/delivery.d.mts.map +0 -1
- package/dist/plugins/replay/delivery.mjs.map +0 -1
- package/dist/plugins/replay/index.d.mts.map +0 -1
- package/dist/plugins/replay/index.mjs.map +0 -1
- package/dist/plugins/replay/recorder.d.mts.map +0 -1
- package/dist/plugins/replay/recorder.mjs.map +0 -1
- package/dist/provider.d.mts.map +0 -1
- package/dist/provider.mjs.map +0 -1
- package/dist/react-error-handler.d.mts.map +0 -1
- package/dist/react-error-handler.mjs.map +0 -1
- package/dist/tracking/api.d.mts.map +0 -1
- package/dist/tracking/api.mjs.map +0 -1
- package/dist/tracking/device.d.mts.map +0 -1
- package/dist/tracking/device.mjs.map +0 -1
- package/dist/tracking/geo.d.mts.map +0 -1
- package/dist/tracking/geo.mjs.map +0 -1
- package/dist/tracking/session.d.mts.map +0 -1
- package/dist/tracking/session.mjs.map +0 -1
- package/dist/util/bot.d.mts.map +0 -1
- package/dist/util/bot.mjs.map +0 -1
- package/dist/util/global.d.mts.map +0 -1
- package/dist/util/global.mjs.map +0 -1
- package/dist/util/log.d.mts.map +0 -1
- package/dist/util/log.mjs.map +0 -1
- package/dist/util/stringify.d.mts.map +0 -1
- package/dist/util/stringify.mjs.map +0 -1
|
@@ -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
|
|
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 {
|
|
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.
|
|
60
|
-
*
|
|
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
|
/**
|
package/dist/internal/kernel.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createLogger}from"../util/log.mjs";import{getGlobal}from"../util/global.mjs";import{resolveTargets}from"./config.mjs";import{SessionTracker}from"../tracking/api.mjs";import{safeStringify}from"../util/stringify.mjs";import{
|
|
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&®isterServiceWorker({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 `[[
|
|
17
|
+
/** Defaults to `[[400, 599]]` (4xx + 5xx) when not specified. */
|
|
18
18
|
statusCodes?: readonly HttpStatusCodeRange[];
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Promote tracked failed fetch/XHR responses to captured exception events.
|
|
22
|
+
* On by default (same-origin 4xx + 5xx, deduped + rate-limited); `false`
|
|
23
|
+
* disables, `{ statusCodes }` tunes the matched ranges.
|
|
24
|
+
*/
|
|
21
25
|
type CaptureFailedRequestsOption = boolean | CaptureFailedRequestsOptions;
|
|
22
|
-
declare function
|
|
26
|
+
declare function resolveFailureRanges(opt: CaptureFailedRequestsOption | undefined): readonly HttpStatusCodeRange[] | null;
|
|
27
|
+
/**
|
|
28
|
+
* Per-instrumentation gate that keeps failed-request capture low-noise:
|
|
29
|
+
* dedupes by `method+status+path` (so a flapping endpoint is captured once)
|
|
30
|
+
* and rate-limits to {@link FAILURE_RATE_MAX} captures per
|
|
31
|
+
* {@link FAILURE_RATE_WINDOW_MS} as a flood backstop. State lives in the
|
|
32
|
+
* closure — one gate per kernel instance, no module-level mutable state.
|
|
33
|
+
*/
|
|
34
|
+
declare function createFailureGate(): (key: string) => boolean;
|
|
35
|
+
type NetworkErrorKind = "abort" | "network" | "timeout";
|
|
36
|
+
declare function classifyNetworkError(message: string): NetworkErrorKind;
|
|
37
|
+
declare function applyFetchSpanAttrs(span: Span, request: FetchSpanRequest, result: Response | FetchErrorShape, failureRanges: readonly HttpStatusCodeRange[] | null, isTrackedUrl: (url: string) => boolean, resolveRoute: ((pathname: string) => string | undefined) | undefined, gate?: (key: string) => boolean): void;
|
|
38
|
+
declare function applyXhrSpanAttrs(span: Span, xhr: Pick<XMLHttpRequest, "responseURL" | "status">, failureRanges: readonly HttpStatusCodeRange[] | null, isTrackedUrl: (url: string) => boolean, gate?: (key: string) => boolean): void;
|
|
23
39
|
declare const _testOnlyApplyFetchSpanAttrs: typeof applyFetchSpanAttrs;
|
|
40
|
+
declare const _testOnlyApplyXhrSpanAttrs: typeof applyXhrSpanAttrs;
|
|
41
|
+
declare const _testOnlyResolveFailureRanges: typeof resolveFailureRanges;
|
|
42
|
+
declare const _testOnlyCreateFailureGate: typeof createFailureGate;
|
|
43
|
+
declare const _testOnlyClassifyNetworkError: typeof classifyNetworkError;
|
|
24
44
|
interface InstrumentationsInput {
|
|
25
|
-
/** See {@link CaptureFailedRequestsOption}.
|
|
45
|
+
/** See {@link CaptureFailedRequestsOption}. On by default. */
|
|
26
46
|
captureFailedRequests?: CaptureFailedRequestsOption;
|
|
27
47
|
/**
|
|
28
48
|
* URLs the SDK exempts from `fetch` and `XHR` instrumentation —
|
|
@@ -62,4 +82,4 @@ interface InstrumentationsInput {
|
|
|
62
82
|
*/
|
|
63
83
|
declare function registerBundledInstrumentations(input: InstrumentationsInput): () => void;
|
|
64
84
|
//#endregion
|
|
65
|
-
export { CaptureFailedRequestsOption, CaptureFailedRequestsOptions, HttpStatusCodeRange, InstrumentationsInput, _testOnlyApplyFetchSpanAttrs, registerBundledInstrumentations };
|
|
85
|
+
export { CaptureFailedRequestsOption, CaptureFailedRequestsOptions, HttpStatusCodeRange, InstrumentationsInput, _testOnlyApplyFetchSpanAttrs, _testOnlyApplyXhrSpanAttrs, _testOnlyClassifyNetworkError, _testOnlyCreateFailureGate, _testOnlyResolveFailureRanges, registerBundledInstrumentations };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{describeActionable,isActionable}from"../dom/actionable.mjs";import{MECHANISM_TYPE}from"@interfere/types/sdk/errors";import{registerInstrumentations}from"@opentelemetry/instrumentation";import{BrowserNavigationInstrumentation,defaultSanitizeUrl}from"@opentelemetry/instrumentation-browser-navigation";import{DocumentLoadInstrumentation}from"@opentelemetry/instrumentation-document-load";import{FetchInstrumentation}from"@opentelemetry/instrumentation-fetch";import{LongTaskInstrumentation}from"@opentelemetry/instrumentation-long-task";import{UserInteractionInstrumentation}from"@opentelemetry/instrumentation-user-interaction";import{XMLHttpRequestInstrumentation}from"@opentelemetry/instrumentation-xml-http-request";const ATTR_HTTP_ROUTE=`http.route`,ATTR_URL_FULL=`url.full`,ATTR_URL_PATH=`url.path`,DEFAULT_FAILURE_RANGES=[[
|
|
1
|
+
import{THIRD_PARTY_IGNORE}from"../third-party-hosts.mjs";import{describeActionable,isActionable}from"../dom/actionable.mjs";import{MECHANISM_TYPE}from"@interfere/types/sdk/errors";import{registerInstrumentations}from"@opentelemetry/instrumentation";import{BrowserNavigationInstrumentation,defaultSanitizeUrl}from"@opentelemetry/instrumentation-browser-navigation";import{DocumentLoadInstrumentation}from"@opentelemetry/instrumentation-document-load";import{FetchInstrumentation}from"@opentelemetry/instrumentation-fetch";import{LongTaskInstrumentation}from"@opentelemetry/instrumentation-long-task";import{UserInteractionInstrumentation}from"@opentelemetry/instrumentation-user-interaction";import{XMLHttpRequestInstrumentation}from"@opentelemetry/instrumentation-xml-http-request";const ATTR_HTTP_ROUTE=`http.route`,ATTR_URL_FULL=`url.full`,ATTR_URL_PATH=`url.path`,DEFAULT_FAILURE_RANGES=[[400,599]];function resolveFailureRanges(opt){if(opt===!1)return null;if(opt===void 0||opt===!0)return DEFAULT_FAILURE_RANGES;let codes=opt.statusCodes;return codes&&codes.length>0?codes:DEFAULT_FAILURE_RANGES}function createFailureGate(){let seen=new Set,windowStart=0,windowCount=0;return key=>{if(seen.has(key))return!1;let now=Date.now();return now-windowStart>6e4&&(windowStart=now,windowCount=0),windowCount>=20?!1:(seen.size>=100&&seen.clear(),seen.add(key),windowCount+=1,!0)}}function failureKey(method,discriminator,url){let path;try{path=new URL(url).pathname}catch{path=url}return`${method} ${discriminator} ${path}`}function statusMatches(status,ranges){for(let range of ranges)if(typeof range==`number`){if(status===range)return!0}else if(status>=range[0]&&status<=range[1])return!0;return!1}function recordHttpFailure(span,status,method,url,mechanism){span.addEvent(`exception`,{"exception.type":`HttpResponseError`,"exception.message":`HTTP ${status} ${method} ${url}`,"interfere.exception.mechanism":mechanism,"interfere.exception.handled":!1,"interfere.exception.kind":`error`})}function maybeCaptureFetchFailure(span,result,failureRanges,method,url,gate){if(!failureRanges)return;let status=result?.status;typeof status!=`number`||status<=0||!statusMatches(status,failureRanges)||gate&&!gate(failureKey(method,status,url))||recordHttpFailure(span,status,method,url,MECHANISM_TYPE.browser.fetchFailedResponse)}const NETWORK_TIMEOUT_RE=/timeout|timed out/i,NETWORK_ABORT_RE=/abort/i;function classifyNetworkError(message){return NETWORK_TIMEOUT_RE.test(message)?`timeout`:NETWORK_ABORT_RE.test(message)?`abort`:`network`}function recordNetworkFailure(span,kind,method,url,mechanism){span.addEvent(`exception`,{"exception.type":`NetworkError`,"exception.message":`Network ${kind} ${method} ${url}`,"interfere.exception.mechanism":mechanism,"interfere.exception.handled":!1,"interfere.exception.kind":`error`,"interfere.exception.subtype":kind})}function maybeCaptureNetworkFailure(span,result,failureRanges,method,url,gate){if(!failureRanges)return;let message=result.message;if(typeof message!=`string`||message.length===0)return;let kind=classifyNetworkError(message);gate&&!gate(failureKey(method,kind,url))||recordNetworkFailure(span,kind,method,url,MECHANISM_TYPE.browser.fetchNetworkError)}function applyFetchSpanAttrs(span,request,result,failureRanges,isTrackedUrl,resolveRoute,gate){let url=resolveFetchUrl(span,request,result);if(!url)return;let parsed;try{parsed=new URL(url)}catch{return}span.setAttribute(ATTR_URL_PATH,parsed.pathname);let tracked=isTrackedUrl(url),method=resolveFetchMethod(request);if(tracked){let route=resolveRoute?.(parsed.pathname);route&&span.setAttribute(ATTR_HTTP_ROUTE,route),span.updateName(`${method} ${route??parsed.pathname}`),result instanceof Response?maybeCaptureFetchFailure(span,result,failureRanges,method,url,gate):maybeCaptureNetworkFailure(span,result,failureRanges,method,url,gate)}}function applyXhrSpanAttrs(span,xhr,failureRanges,isTrackedUrl,gate){if(!failureRanges)return;let url=xhr.responseURL;if(!(url&&isTrackedUrl(url)))return;let status=xhr.status;if(typeof status==`number`){if(status===0){(!gate||gate(failureKey(`XHR`,`network`,url)))&&recordNetworkFailure(span,`network`,`XHR`,url,MECHANISM_TYPE.browser.xhrNetworkError);return}statusMatches(status,failureRanges)&&(gate&&!gate(failureKey(`XHR`,status,url))||recordHttpFailure(span,status,`XHR`,url,MECHANISM_TYPE.browser.xhrFailedResponse))}}function resolveFetchUrl(span,request,result){return responseUrl(result)??requestUrl(request)??spanUrlFull(span)}function isReadableSpan(span){return`attributes`in span}function spanUrlFull(span){if(!isReadableSpan(span))return null;let url=span.attributes[ATTR_URL_FULL]??span.attributes[`http.url`];return typeof url==`string`?url:null}function responseUrl(result){return result instanceof Response&&result.url?result.url:null}function requestUrl(request){return typeof request==`string`?request:readStringProp(request,`url`)??readStringProp(request,`href`)}function resolveFetchMethod(request){return readStringProp(request,`method`)??`GET`}function readStringProp(value,key){if(!(value&&typeof value==`object`&&key in value))return null;let prop=value[key];return typeof prop==`string`?prop:null}const _testOnlyApplyFetchSpanAttrs=applyFetchSpanAttrs,_testOnlyApplyXhrSpanAttrs=applyXhrSpanAttrs,_testOnlyResolveFailureRanges=resolveFailureRanges,_testOnlyCreateFailureGate=createFailureGate,_testOnlyClassifyNetworkError=classifyNetworkError,SEMCONV_STABILITY_OPT_IN=`http,database,messaging,gen_ai_latest_experimental`,NEVER_MATCH=/^$/,ESCAPE_RE=/[.*+?^${}()|[\]\\]/g;function sameOriginPattern(){if(typeof window>`u`)return NEVER_MATCH;let{protocol,host}=window.location;return RegExp(`^${protocol}//${host.replace(ESCAPE_RE,`\\$&`)}`)}function stampInteractionAttrs(eventType,element,span){let desc=describeActionable(element);span.setAttribute(`ui.event_type`,eventType),span.setAttribute(`ui.target.tag`,desc.tag),desc.id&&span.setAttribute(`ui.target.id`,desc.id),desc.name&&span.setAttribute(`ui.target.name`,desc.name),desc.role&&span.setAttribute(`ui.target.role`,desc.role),desc.ariaLabel&&span.setAttribute(`ui.target.aria_label`,desc.ariaLabel),desc.text&&span.setAttribute(`ui.target.text`,desc.text)}function stampUrlAttrs(span,resolveRoute){if(typeof window>`u`)return;let pathname=window.location.pathname,route=resolveRoute?.(pathname);span.setAttribute(ATTR_URL_PATH,route??pathname),span.setAttribute(ATTR_URL_FULL,window.location.href),route&&span.setAttribute(ATTR_HTTP_ROUTE,route)}function registerBundledInstrumentations(input){let{tracerProvider,loggerProvider,ignoreUrls,propagateContextUrls=[]}=input,resolveRoute=input.resolveRoute,origin=sameOriginPattern(),fetchIgnore=[...ignoreUrls,...THIRD_PARTY_IGNORE],failureRanges=resolveFailureRanges(input.captureFailedRequests),failureGate=createFailureGate();function isTrackedUrl(url){return origin.test(url)||propagateContextUrls.some(re=>re instanceof RegExp?re.test(url):url.includes(re))}return registerInstrumentations({tracerProvider,...loggerProvider?{loggerProvider}:{},instrumentations:[new FetchInstrumentation({propagateTraceHeaderCorsUrls:[origin,...propagateContextUrls],ignoreUrls:fetchIgnore,semconvStabilityOptIn:SEMCONV_STABILITY_OPT_IN,measureRequestSize:!0,applyCustomAttributesOnSpan:(span,request,result)=>{applyFetchSpanAttrs(span,request,result,failureRanges,isTrackedUrl,resolveRoute,failureGate)}}),new XMLHttpRequestInstrumentation({propagateTraceHeaderCorsUrls:[origin,...propagateContextUrls],ignoreUrls:fetchIgnore,semconvStabilityOptIn:SEMCONV_STABILITY_OPT_IN,applyCustomAttributesOnSpan:(span,xhr)=>{applyXhrSpanAttrs(span,xhr,failureRanges,isTrackedUrl,failureGate)}}),new DocumentLoadInstrumentation({semconvStabilityOptIn:SEMCONV_STABILITY_OPT_IN,applyCustomAttributesOnSpan:{resourceFetch:(span,entry)=>{let name=entry?.name;if(typeof name==`string`)try{let url=new URL(name),isCrossOrigin=typeof window<`u`&&url.origin!==window.location.origin;span.updateName(isCrossOrigin?`resource: ${url.host}${url.pathname}`:`resource: ${url.pathname}`),span.setAttribute(ATTR_URL_FULL,name),span.setAttribute(ATTR_URL_PATH,url.pathname),span.setAttribute(`server.address`,url.host)}catch{return}}}}),new BrowserNavigationInstrumentation({sanitizeUrl:defaultSanitizeUrl}),new UserInteractionInstrumentation({eventNames:[`click`,`submit`],shouldPreventSpanCreation:(eventType,element,span)=>eventType===`click`&&!isActionable(element)?!0:(stampInteractionAttrs(eventType,element,span),stampUrlAttrs(span,resolveRoute),!1)}),new LongTaskInstrumentation({observerCallback:(span,info)=>{stampUrlAttrs(span,resolveRoute);let containerType=info.longtaskEntry.attribution[0]?.containerType;containerType&&span.setAttribute(`longtask.container_type`,containerType)}})]})}export{_testOnlyApplyFetchSpanAttrs,_testOnlyApplyXhrSpanAttrs,_testOnlyClassifyNetworkError,_testOnlyCreateFailureGate,_testOnlyResolveFailureRanges,registerBundledInstrumentations};
|
|
@@ -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
|
|
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
|
|
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};
|
|
@@ -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};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{registerKernel,unregisterKernel}from"./kernel-registry.mjs";import{createKernel,isEnabledByEnvironment}from"./kernel.mjs";import{isBotUserAgent}from"../util/bot.mjs";function createWrapperSingleton(input){let kernel=null,pending=null,listeners=new Set;function emit(){for(let listener of listeners)listener()}return{init(opts={}){return kernel?Promise.resolve(kernel):pending||(!(opts.enabled??isEnabledByEnvironment())||isBotUserAgent()?Promise.resolve(null):(pending=createKernel({opts:{...opts,_wrapperVersions:[input.producerVersion]}}).then(k=>(kernel=k,k&®isterKernel(k),emit(),k)).finally(()=>{pending=null}),pending))},getKernel(){if(!kernel)throw Error(`Interfere SDK not initialized. Call init() from your ${input.initEntryName} entrypoint.`);return kernel},getKernelOrNull(){return kernel},async close(){if(!kernel)return;let previous=kernel;await previous.dispose(),kernel=null,unregisterKernel(previous),emit()},subscribeToKernel(listener){return listeners.add(listener),()=>{listeners.delete(listener)}},consent:{get:()=>kernel?.consent.get()??null,set:value=>kernel?.consent.set(value)},identity:{get:()=>kernel?.identity.get()??null,set:params=>kernel?.identity.set(params)??Promise.resolve()}}}export{createWrapperSingleton};
|
|
1
|
+
import{registerKernel,unregisterKernel}from"./kernel-registry.mjs";import{createKernel,isEnabledByEnvironment}from"./kernel.mjs";import{isBotUserAgent}from"../util/bot.mjs";function createWrapperSingleton(input){let kernel=null,pending=null,listeners=new Set;function emit(){for(let listener of listeners)listener()}return{init(opts={}){return kernel?Promise.resolve(kernel):pending||(!(opts.enabled??isEnabledByEnvironment())||isBotUserAgent()?Promise.resolve(null):(pending=createKernel({opts:{...opts,_wrapperVersions:[input.producerVersion]}}).then(k=>(kernel=k,k&®isterKernel(k),emit(),k)).finally(()=>{pending=null}),pending))},getKernel(){if(!kernel)throw Error(`Interfere SDK not initialized. Call init() from your ${input.initEntryName} entrypoint.`);return kernel},getKernelOrNull(){return kernel},async close(){if(!kernel)return;let previous=kernel;await previous.dispose(),kernel=null,unregisterKernel(previous),emit()},subscribeToKernel(listener){return listeners.add(listener),()=>{listeners.delete(listener)}},consent:{get:()=>kernel?.consent.get()??null,set:value=>kernel?.consent.set(value)},identity:{clear:()=>kernel?.identity.clear(),get:()=>kernel?.identity.get()??null,set:params=>kernel?.identity.set(params)??Promise.resolve()}}}export{createWrapperSingleton};
|
package/dist/package.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var name=`@interfere/react`,version=`11.0.0-canary.
|
|
1
|
+
var name=`@interfere/react`,version=`11.0.0-canary.4`;export{name,version};
|
package/dist/plugins/errors.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{onConsoleCall}from"../internal/console-patch.mjs";import{MECHANISM_TYPE,isNonErrorException,toException}from"@interfere/types/sdk/errors";function capture(ctx,opts){ctx.recordException(opts.error,{mechanism:opts.mechanism,...opts.fallbackFrame&&!isNonErrorException(opts.error)?{fallbackFrames:[opts.fallbackFrame]}:{}})}function findErrorArg(args){
|
|
1
|
+
import{hasServerDigest}from"../internal/server-digest.mjs";import{onConsoleCall}from"../internal/console-patch.mjs";import{THIRD_PARTY_IGNORE}from"../internal/third-party-hosts.mjs";import{MECHANISM_TYPE,isNonErrorException,toException}from"@interfere/types/sdk/errors";function capture(ctx,opts){ctx.recordException(opts.error,{mechanism:opts.mechanism,...opts.fallbackFrame&&!isNonErrorException(opts.error)?{fallbackFrames:[opts.fallbackFrame]}:{}})}function findErrorArg(args){return args.find(arg=>arg instanceof Error)??null}function resourceError(target){return target instanceof HTMLImageElement||target instanceof HTMLScriptElement?target.src?{tag:target.localName,url:target.src}:null:target instanceof HTMLLinkElement&&target.href?{tag:target.localName,url:target.href}:null}function isIgnoredResourceUrl(url){return THIRD_PARTY_IGNORE.some(pattern=>pattern.test(url))}function createResourceGate(){let seen=new Set,windowStart=0,windowCount=0;return url=>{if(seen.has(url))return!1;let now=Date.now();return now-windowStart>6e4&&(windowStart=now,windowCount=0),windowCount>=20?!1:(seen.size>=100&&seen.clear(),seen.add(url),windowCount+=1,!0)}}const errorsPlugin={name:`errors`,setup(ctx){let originalOnError=globalThis.onerror,originalStackTraceLimit=Error.stackTraceLimit;Error.stackTraceLimit<50&&(Error.stackTraceLimit=50),globalThis.onerror=(msg,source,line,col,error)=>{if(error instanceof Error&&!hasServerDigest(error)){let fallbackFrame=typeof source==`string`?{id:`fallback-0`,file:source,...typeof line==`number`?{line}:{},...typeof col==`number`?{column:col}:{}}:null;capture(ctx,{error,mechanism:{type:MECHANISM_TYPE.browser.onerror,handled:!1},...fallbackFrame?{fallbackFrame}:{}})}return typeof originalOnError==`function`?originalOnError.call(globalThis,msg,source,line,col,error):!1};let onUnhandledRejection=event=>{hasServerDigest(event.reason)||capture(ctx,{error:toException(event.reason),mechanism:{type:MECHANISM_TYPE.browser.onunhandledrejection,handled:!1,...event.reason instanceof Error?{}:{synthetic:!0}}})},seenCspReports=new Set,onSecurityPolicyViolation=event=>{let key=`${event.violatedDirective}|${event.blockedURI}`;seenCspReports.has(key)||seenCspReports.size>=5||(seenCspReports.add(key),capture(ctx,{error:{kind:`non-error`,type:`SecurityPolicyViolation`,value:`CSP ${event.violatedDirective} blocked ${event.blockedURI||`inline`}`,serialized:{violatedDirective:event.violatedDirective,blockedURI:event.blockedURI,sourceFile:event.sourceFile,lineNumber:event.lineNumber,columnNumber:event.columnNumber,disposition:event.disposition}},mechanism:{type:MECHANISM_TYPE.browser.securityPolicyViolation,handled:!1}}))},resourceGate=createResourceGate(),onResourceError=event=>{let resource=resourceError(event.target);!resource||isIgnoredResourceUrl(resource.url)||!resourceGate(resource.url)||capture(ctx,{error:{kind:`non-error`,type:`ResourceLoadError`,value:`Failed to load ${resource.tag}: ${resource.url}`,serialized:{url:resource.url}},mechanism:{type:MECHANISM_TYPE.browser.resourceError,handled:!1}})},supportsEventTarget=typeof globalThis.addEventListener==`function`&&typeof globalThis.removeEventListener==`function`;supportsEventTarget&&(globalThis.addEventListener(`unhandledrejection`,onUnhandledRejection),globalThis.addEventListener(`securitypolicyviolation`,onSecurityPolicyViolation),globalThis.addEventListener(`error`,onResourceError,!0));let unsubscribeConsole=onConsoleCall((level,args)=>{if(level!==`error`)return;let error=findErrorArg(args);error&&!hasServerDigest(error)&&capture(ctx,{error,mechanism:{type:MECHANISM_TYPE.browser.consoleError,handled:!0}})});return()=>{Error.stackTraceLimit=originalStackTraceLimit,globalThis.onerror=originalOnError,supportsEventTarget&&(globalThis.removeEventListener(`unhandledrejection`,onUnhandledRejection),globalThis.removeEventListener(`securitypolicyviolation`,onSecurityPolicyViolation),globalThis.removeEventListener(`error`,onResourceError,!0)),unsubscribeConsole()}}};export{errorsPlugin as default};
|
|
@@ -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 };
|
package/dist/plugins/logs.d.mts
CHANGED
package/dist/plugins/logs.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{activeKernel}from"../internal/kernel-registry.mjs";import{onConsoleCall}from"../internal/console-patch.mjs";import{safeStringify}from"../util/stringify.mjs";import{SeverityNumber}from"@opentelemetry/api-logs";const LEVEL_TO_SEVERITY={debug:{number:SeverityNumber.DEBUG,text:`DEBUG`},info:{number:SeverityNumber.INFO,text:`INFO`},log:{number:SeverityNumber.INFO,text:`INFO`},warn:{number:SeverityNumber.WARN,text:`WARN`},error:{number:SeverityNumber.ERROR,text:`ERROR`}},logsPlugin={name:`logs`,setup(){return onConsoleCall((level,args)=>{if(args.some(a=>a instanceof Error))return;let kernel=activeKernel();if(!kernel)return;let sev=LEVEL_TO_SEVERITY[level];kernel.recordLog({severityText:sev.text,severityNumber:sev.number,body:args.length===1?safeStringify(args[0]):args.map(safeStringify).join(` `),attributes:{"console.level":level}})})}};export{logsPlugin as default
|
|
1
|
+
import{activeKernel}from"../internal/kernel-registry.mjs";import{onConsoleCall}from"../internal/console-patch.mjs";import{safeStringify}from"../util/stringify.mjs";import{SeverityNumber}from"@opentelemetry/api-logs";const LEVEL_TO_SEVERITY={debug:{number:SeverityNumber.DEBUG,text:`DEBUG`},info:{number:SeverityNumber.INFO,text:`INFO`},log:{number:SeverityNumber.INFO,text:`INFO`},warn:{number:SeverityNumber.WARN,text:`WARN`},error:{number:SeverityNumber.ERROR,text:`ERROR`}},logsPlugin={name:`logs`,setup(){return onConsoleCall((level,args)=>{if(args.some(a=>a instanceof Error))return;let kernel=activeKernel();if(!kernel)return;let sev=LEVEL_TO_SEVERITY[level];kernel.recordLog({severityText:sev.text,severityNumber:sev.number,body:args.length===1?safeStringify(args[0]):args.map(safeStringify).join(` `),attributes:{"console.level":level}})})}};export{logsPlugin as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{trace}from"@opentelemetry/api";function distance(a,b){return Math.sqrt((a.x-b.x)**2+(a.y-b.y)**2)}function selectorFor(el){if(!el)return`unknown`;if(el.id)return`#${el.id}`;let classes=[...el.classList].slice(0,3).join(`.`),tag=el.tagName.toLowerCase();return classes?`${tag}.${classes}`:tag}const rageClicksPlugin={name:`rage-clicks`,setup(){let clicks=[],onClick=event=>{let now=Date.now();for(clicks.push({x:event.clientX,y:event.clientY,ts:now,target:event.target instanceof Element?event.target:null});clicks.length>0&&now-(clicks[0]?.ts??0)>800;)clicks.shift();if(clicks.length<3)return;let anchor=clicks[0];if(!anchor)return;let clustered=clicks.filter(c=>distance(anchor,c)<=30);if(clustered.length<3)return;let last=clustered.at(-1);last&&(trace.getTracer(`@interfere/react/rage-clicks`).startSpan(`rage_click`,{attributes:{"ui.event_type":`rage_click`,"ui.rage_click.count":clustered.length,"ui.rage_click.time_window_ms":last.ts-anchor.ts,"ui.rage_click.selector":selectorFor(last.target),"ui.rage_click.text":last.target?.textContent?.trim().slice(0,120)??``,"ui.rage_click.x":last.x,"ui.rage_click.y":last.y}}).end(),clicks.length=0)};return document.addEventListener(`click`,onClick,{capture:!0}),()=>{document.removeEventListener(`click`,onClick,{capture:!0})}}};export{rageClicksPlugin as default
|
|
1
|
+
import{trace}from"@opentelemetry/api";function distance(a,b){return Math.sqrt((a.x-b.x)**2+(a.y-b.y)**2)}function selectorFor(el){if(!el)return`unknown`;if(el.id)return`#${el.id}`;let classes=[...el.classList].slice(0,3).join(`.`),tag=el.tagName.toLowerCase();return classes?`${tag}.${classes}`:tag}const rageClicksPlugin={name:`rage-clicks`,setup(){let clicks=[],onClick=event=>{let now=Date.now();for(clicks.push({x:event.clientX,y:event.clientY,ts:now,target:event.target instanceof Element?event.target:null});clicks.length>0&&now-(clicks[0]?.ts??0)>800;)clicks.shift();if(clicks.length<3)return;let anchor=clicks[0];if(!anchor)return;let clustered=clicks.filter(c=>distance(anchor,c)<=30);if(clustered.length<3)return;let last=clustered.at(-1);last&&(trace.getTracer(`@interfere/react/rage-clicks`).startSpan(`rage_click`,{attributes:{"ui.event_type":`rage_click`,"ui.rage_click.count":clustered.length,"ui.rage_click.time_window_ms":last.ts-anchor.ts,"ui.rage_click.selector":selectorFor(last.target),"ui.rage_click.text":last.target?.textContent?.trim().slice(0,120)??``,"ui.rage_click.x":last.x,"ui.rage_click.y":last.y}}).end(),clicks.length=0)};return document.addEventListener(`click`,onClick,{capture:!0}),()=>{document.removeEventListener(`click`,onClick,{capture:!0})}}};export{rageClicksPlugin as default};
|