@interfere/react 9.0.2 → 10.0.1-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/api.d.mts +25 -0
- package/dist/api.d.mts.map +1 -0
- package/dist/api.mjs +1 -0
- package/dist/api.mjs.map +1 -0
- package/dist/error-boundary.d.mts +10 -4
- package/dist/error-boundary.d.mts.map +1 -1
- package/dist/error-boundary.mjs +1 -39
- package/dist/error-boundary.mjs.map +1 -1
- package/dist/internal/browser-context.d.mts +6 -0
- package/dist/internal/browser-context.d.mts.map +1 -0
- package/dist/internal/browser-context.mjs +1 -0
- package/dist/internal/browser-context.mjs.map +1 -0
- package/dist/internal/capture-boundary.d.mts +4 -1
- package/dist/internal/capture-boundary.d.mts.map +1 -1
- package/dist/internal/capture-boundary.mjs +1 -44
- package/dist/internal/capture-boundary.mjs.map +1 -1
- package/dist/internal/capture.d.mts +16 -5
- package/dist/internal/capture.d.mts.map +1 -1
- package/dist/internal/capture.mjs +1 -23
- package/dist/internal/capture.mjs.map +1 -1
- package/dist/internal/config.d.mts +22 -4
- package/dist/internal/config.d.mts.map +1 -1
- package/dist/internal/config.mjs +1 -33
- package/dist/internal/config.mjs.map +1 -1
- package/dist/internal/consent.d.mts.map +1 -1
- package/dist/internal/consent.mjs +1 -25
- package/dist/internal/consent.mjs.map +1 -1
- package/dist/internal/console-patch.d.mts +19 -0
- package/dist/internal/console-patch.d.mts.map +1 -0
- package/dist/internal/console-patch.mjs +1 -0
- package/dist/internal/console-patch.mjs.map +1 -0
- package/dist/internal/dom/actionable.d.mts +27 -0
- package/dist/internal/dom/actionable.d.mts.map +1 -0
- package/dist/internal/dom/actionable.mjs +1 -0
- package/dist/internal/dom/actionable.mjs.map +1 -0
- package/dist/internal/kernel-registry.d.mts +8 -0
- package/dist/internal/kernel-registry.d.mts.map +1 -0
- package/dist/internal/kernel-registry.mjs +1 -0
- package/dist/internal/kernel-registry.mjs.map +1 -0
- package/dist/internal/kernel.d.mts +267 -0
- package/dist/internal/kernel.d.mts.map +1 -0
- package/dist/internal/kernel.mjs +1 -0
- package/dist/internal/kernel.mjs.map +1 -0
- package/dist/internal/otel/exporter.d.mts +85 -0
- package/dist/internal/otel/exporter.d.mts.map +1 -0
- package/dist/internal/otel/exporter.mjs +1 -0
- package/dist/internal/otel/exporter.mjs.map +1 -0
- package/dist/internal/otel/index.d.mts +6 -0
- package/dist/internal/otel/index.mjs +1 -0
- package/dist/internal/otel/instrumentations.d.mts +42 -0
- package/dist/internal/otel/instrumentations.d.mts.map +1 -0
- package/dist/internal/otel/instrumentations.mjs +1 -0
- package/dist/internal/otel/instrumentations.mjs.map +1 -0
- package/dist/internal/otel/page-scope-context-manager.d.mts +32 -0
- package/dist/internal/otel/page-scope-context-manager.d.mts.map +1 -0
- package/dist/internal/otel/page-scope-context-manager.mjs +1 -0
- package/dist/internal/otel/page-scope-context-manager.mjs.map +1 -0
- package/dist/internal/otel/propagation.d.mts +21 -0
- package/dist/internal/otel/propagation.d.mts.map +1 -0
- package/dist/internal/otel/propagation.mjs +1 -0
- package/dist/internal/otel/propagation.mjs.map +1 -0
- package/dist/internal/otel/provider.d.mts +106 -0
- package/dist/internal/otel/provider.d.mts.map +1 -0
- package/dist/internal/otel/provider.mjs +1 -0
- package/dist/internal/otel/provider.mjs.map +1 -0
- package/dist/internal/otel/web-vitals.d.mts +35 -0
- package/dist/internal/otel/web-vitals.d.mts.map +1 -0
- package/dist/internal/otel/web-vitals.mjs +1 -0
- package/dist/internal/otel/web-vitals.mjs.map +1 -0
- package/dist/internal/page-lifecycle.d.mts +21 -0
- package/dist/internal/page-lifecycle.d.mts.map +1 -0
- package/dist/internal/page-lifecycle.mjs +1 -0
- package/dist/internal/page-lifecycle.mjs.map +1 -0
- package/dist/internal/plugin-runtime.d.mts +0 -2
- package/dist/internal/plugin-runtime.d.mts.map +1 -1
- package/dist/internal/plugin-runtime.mjs +1 -107
- package/dist/internal/plugin-runtime.mjs.map +1 -1
- package/dist/internal/react-context.d.mts +44 -0
- package/dist/internal/react-context.d.mts.map +1 -0
- package/dist/internal/react-context.mjs +1 -0
- package/dist/internal/react-context.mjs.map +1 -0
- package/dist/internal/sw.d.mts +22 -2
- package/dist/internal/sw.d.mts.map +1 -1
- package/dist/internal/sw.mjs +1 -10
- package/dist/internal/sw.mjs.map +1 -1
- package/dist/internal/version.d.mts +3 -1
- package/dist/internal/version.d.mts.map +1 -1
- package/dist/internal/version.mjs +1 -5
- package/dist/internal/version.mjs.map +1 -1
- package/dist/internal/wrapper-singleton.d.mts +47 -0
- package/dist/internal/wrapper-singleton.d.mts.map +1 -0
- package/dist/internal/wrapper-singleton.mjs +1 -0
- package/dist/internal/wrapper-singleton.mjs.map +1 -0
- package/dist/package.mjs +1 -5
- package/dist/plugins/errors.d.mts.map +1 -1
- package/dist/plugins/errors.mjs +1 -91
- package/dist/plugins/errors.mjs.map +1 -1
- package/dist/plugins/lib/loader.d.mts +1 -2
- package/dist/plugins/lib/loader.d.mts.map +1 -1
- package/dist/plugins/lib/loader.mjs +1 -43
- package/dist/plugins/lib/loader.mjs.map +1 -1
- package/dist/plugins/lib/types.d.mts +3 -2
- package/dist/plugins/lib/types.d.mts.map +1 -1
- package/dist/plugins/lib/types.mjs +1 -1
- package/dist/plugins/logs.d.mts +13 -0
- package/dist/plugins/logs.d.mts.map +1 -0
- package/dist/plugins/logs.mjs +1 -0
- package/dist/plugins/logs.mjs.map +1 -0
- package/dist/plugins/rage-clicks.d.mts.map +1 -1
- package/dist/plugins/rage-clicks.mjs +1 -53
- package/dist/plugins/rage-clicks.mjs.map +1 -1
- package/dist/plugins/replay.d.mts.map +1 -1
- package/dist/plugins/replay.mjs +1 -62
- package/dist/plugins/replay.mjs.map +1 -1
- package/dist/provider.d.mts +11 -20
- package/dist/provider.d.mts.map +1 -1
- package/dist/provider.mjs +1 -32
- package/dist/provider.mjs.map +1 -1
- package/dist/react-error-handler.d.mts +21 -5
- package/dist/react-error-handler.d.mts.map +1 -1
- package/dist/react-error-handler.mjs +1 -54
- package/dist/react-error-handler.mjs.map +1 -1
- package/dist/sw.d.mts +2 -0
- package/dist/sw.mjs +2 -0
- package/dist/tracking/api.d.mts +41 -15
- package/dist/tracking/api.d.mts.map +1 -1
- package/dist/tracking/api.mjs +1 -134
- package/dist/tracking/api.mjs.map +1 -1
- package/dist/tracking/device.d.mts +30 -7
- package/dist/tracking/device.d.mts.map +1 -1
- package/dist/tracking/device.mjs +1 -80
- package/dist/tracking/device.mjs.map +1 -1
- package/dist/tracking/geo.d.mts +11 -3
- package/dist/tracking/geo.d.mts.map +1 -1
- package/dist/tracking/geo.mjs +2 -44
- package/dist/tracking/geo.mjs.map +1 -1
- package/dist/tracking/session.d.mts +3 -1
- package/dist/tracking/session.d.mts.map +1 -1
- package/dist/tracking/session.mjs +1 -75
- package/dist/tracking/session.mjs.map +1 -1
- package/dist/util/bot.d.mts +10 -0
- package/dist/util/bot.d.mts.map +1 -0
- package/dist/util/bot.mjs +1 -0
- package/dist/util/bot.mjs.map +1 -0
- package/dist/util/global.d.mts +10 -0
- package/dist/util/global.d.mts.map +1 -0
- package/dist/util/global.mjs +1 -0
- package/dist/util/global.mjs.map +1 -0
- package/dist/util/log.d.mts.map +1 -1
- package/dist/util/log.mjs +1 -37
- package/dist/util/log.mjs.map +1 -1
- package/dist/util/stringify.d.mts +9 -0
- package/dist/util/stringify.d.mts.map +1 -0
- package/dist/util/stringify.mjs +1 -0
- package/dist/util/stringify.mjs.map +1 -0
- package/package.json +79 -25
- package/dist/internal/client.d.mts +0 -48
- package/dist/internal/client.d.mts.map +0 -1
- package/dist/internal/client.mjs +0 -146
- package/dist/internal/client.mjs.map +0 -1
- package/dist/internal/context.d.mts +0 -6
- package/dist/internal/context.d.mts.map +0 -1
- package/dist/internal/context.mjs +0 -32
- package/dist/internal/context.mjs.map +0 -1
- package/dist/internal/envelope.d.mts +0 -15
- package/dist/internal/envelope.d.mts.map +0 -1
- package/dist/internal/envelope.mjs +0 -24
- package/dist/internal/envelope.mjs.map +0 -1
- package/dist/internal/errors.d.mts +0 -4
- package/dist/internal/errors.d.mts.map +0 -1
- package/dist/internal/errors.mjs +0 -4
- package/dist/internal/errors.mjs.map +0 -1
- package/dist/plugins/device.d.mts +0 -6
- package/dist/plugins/device.d.mts.map +0 -1
- package/dist/plugins/device.mjs +0 -13
- package/dist/plugins/device.mjs.map +0 -1
- package/dist/plugins/pages.d.mts +0 -6
- package/dist/plugins/pages.d.mts.map +0 -1
- package/dist/plugins/pages.mjs +0 -102
- package/dist/plugins/pages.mjs.map +0 -1
- package/dist/transport/http.d.mts +0 -25
- package/dist/transport/http.d.mts.map +0 -1
- package/dist/transport/http.mjs +0 -80
- package/dist/transport/http.mjs.map +0 -1
- package/dist/transport/queue.d.mts +0 -34
- package/dist/transport/queue.d.mts.map +0 -1
- package/dist/transport/queue.mjs +0 -100
- package/dist/transport/queue.mjs.map +0 -1
package/dist/plugins/replay.mjs
CHANGED
|
@@ -1,62 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
//#region src/plugins/replay.ts
|
|
3
|
-
const log = createLogger("replay");
|
|
4
|
-
const FLUSH_INTERVAL_MS = 1e4;
|
|
5
|
-
const replayPlugin = {
|
|
6
|
-
name: "replay",
|
|
7
|
-
setup(ctx) {
|
|
8
|
-
let stopFn = null;
|
|
9
|
-
let events = [];
|
|
10
|
-
let flushTimer = null;
|
|
11
|
-
let firstTs = null;
|
|
12
|
-
let lastTs = null;
|
|
13
|
-
const flush = () => {
|
|
14
|
-
if (events.length === 0) return;
|
|
15
|
-
const chunk = events;
|
|
16
|
-
events = [];
|
|
17
|
-
const fts = firstTs;
|
|
18
|
-
const lts = lastTs;
|
|
19
|
-
firstTs = null;
|
|
20
|
-
lastTs = null;
|
|
21
|
-
ctx.capture("replay_chunk", {
|
|
22
|
-
ts: Date.now(),
|
|
23
|
-
count: chunk.length,
|
|
24
|
-
events: chunk,
|
|
25
|
-
...fts !== null && { first_event_ts: fts },
|
|
26
|
-
...lts !== null && { last_event_ts: lts }
|
|
27
|
-
});
|
|
28
|
-
};
|
|
29
|
-
const onVisibilityChange = () => {
|
|
30
|
-
if (document.visibilityState === "hidden") flush();
|
|
31
|
-
};
|
|
32
|
-
const onBeforeUnload = () => {
|
|
33
|
-
flush();
|
|
34
|
-
};
|
|
35
|
-
const init = async () => {
|
|
36
|
-
try {
|
|
37
|
-
stopFn = (await import("rrweb")).record({ emit(event) {
|
|
38
|
-
const ts = Date.now();
|
|
39
|
-
if (firstTs === null) firstTs = ts;
|
|
40
|
-
lastTs = ts;
|
|
41
|
-
events.push(JSON.stringify(event));
|
|
42
|
-
} }) ?? null;
|
|
43
|
-
flushTimer = setInterval(flush, FLUSH_INTERVAL_MS);
|
|
44
|
-
globalThis.addEventListener("visibilitychange", onVisibilityChange);
|
|
45
|
-
globalThis.addEventListener("beforeunload", onBeforeUnload);
|
|
46
|
-
log.debug("recording started");
|
|
47
|
-
} catch {
|
|
48
|
-
log.error("rrweb failed to load, replay disabled");
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
init().catch(() => {});
|
|
52
|
-
return () => {
|
|
53
|
-
flush();
|
|
54
|
-
stopFn?.();
|
|
55
|
-
if (flushTimer) clearInterval(flushTimer);
|
|
56
|
-
globalThis.removeEventListener("visibilitychange", onVisibilityChange);
|
|
57
|
-
globalThis.removeEventListener("beforeunload", onBeforeUnload);
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
//#endregion
|
|
62
|
-
export { replayPlugin as default, replayPlugin };
|
|
1
|
+
import{createLogger}from"../util/log.mjs";import{appendPathBeforeQuery,resolveTargets}from"../internal/config.mjs";import{onPageHidden}from"../internal/page-lifecycle.mjs";import{trace}from"@opentelemetry/api";const log=createLogger(`replay`);async function deliverChunk({ctx,uploadBaseUrl,authHeaders,payload}){let sessionId=ctx.getSessionId();if(!sessionId)return;let body=JSON.stringify(payload.events),sizeBytes=body.length,res=await fetch(appendPathBeforeQuery(uploadBaseUrl,`/v2/replay/upload/${encodeURIComponent(sessionId)}`),{method:`POST`,headers:{...authHeaders,"content-type":`application/json`},body});if(!res.ok){log.warn(`replay chunk upload rejected (%d)`,res.status);return}let{objectKey}=await res.json();trace.getTracer(`@interfere/react/replay`).startSpan(`replay.chunk`,{attributes:{"replay.chunk.uri":objectKey,"replay.chunk.size_bytes":sizeBytes,"replay.chunk.event_count":payload.events.length,...payload.firstTs===null?{}:{"replay.chunk.first_event_ts":payload.firstTs},...payload.lastTs===null?{}:{"replay.chunk.last_event_ts":payload.lastTs}}}).end()}const replayPlugin={name:`replay`,setup(ctx){let targets=resolveTargets(),uploadBaseUrl=targets.collectorBaseUrl,authHeaders=Object.fromEntries(targets.ingest.headers.entries()),stopFn=null,events=[],flushTimer=null,firstTs=null,lastTs=null,flush=()=>{if(events.length===0)return;let chunk=events;events=[];let payload={events:chunk,firstTs,lastTs};firstTs=null,lastTs=null,deliverChunk({ctx,uploadBaseUrl,authHeaders,payload}).catch(error=>{log.warn(`replay chunk dropped: %o`,error)})},unsubscribeHidden=null;return(async()=>{try{stopFn=(await import(`rrweb`)).record({emit(event){let ts=Date.now();firstTs===null&&(firstTs=ts),lastTs=ts,events.push(JSON.stringify(event))}})??null,flushTimer=setInterval(flush,1e4),unsubscribeHidden=onPageHidden(flush),log.debug(`recording started`)}catch{log.error(`rrweb failed to load, replay disabled`)}})(),()=>{flush(),stopFn?.(),flushTimer&&clearInterval(flushTimer),unsubscribeHidden?.()}}};export{replayPlugin as default,replayPlugin};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replay.mjs","names":[],"sources":["../../src/plugins/replay.ts"],"sourcesContent":["import { createLogger } from \"../util/log.js\";\nimport type { Plugin } from \"./lib/types.js\";\n\nconst log = createLogger(\"replay\");\n\nconst FLUSH_INTERVAL_MS = 10_000;\n\nexport const replayPlugin: Plugin = {\n name: \"replay\",\n\n setup(ctx) {\n let stopFn: (() => void) | null = null;\n let events: string[] = [];\n let flushTimer: ReturnType<typeof setInterval> | null = null;\n let firstTs: number | null = null;\n let lastTs: number | null = null;\n\n const flush = () => {\n if (events.length === 0) {\n return;\n }\n const chunk = events;\n events = [];\n const
|
|
1
|
+
{"version":3,"file":"replay.mjs","names":[],"sources":["../../src/plugins/replay.ts"],"sourcesContent":["import { trace } from \"@opentelemetry/api\";\n\nimport { appendPathBeforeQuery, resolveTargets } from \"../internal/config.js\";\nimport { onPageHidden } from \"../internal/page-lifecycle.js\";\nimport { createLogger } from \"../util/log.js\";\nimport type { Plugin, PluginContext } from \"./lib/types.js\";\n\nconst log = createLogger(\"replay\");\n\nconst FLUSH_INTERVAL_MS = 10_000;\nconst UPLOAD_PATH_PREFIX = \"/v2/replay/upload/\";\nconst TRACER_NAME = \"@interfere/react/replay\";\n\ninterface UploadResponse {\n objectKey: string;\n}\n\ninterface ChunkPayload {\n events: string[];\n firstTs: number | null;\n lastTs: number | null;\n}\n\ninterface DeliverInput {\n authHeaders: Record<string, string>;\n ctx: PluginContext;\n payload: ChunkPayload;\n uploadBaseUrl: string;\n}\n\n/**\n * POSTs the chunk's events array to the collector. The collector writes\n * to R2 server-side and returns the object key we stamp onto the\n * `replay.chunk` span event. Single round trip; if the request fails,\n * the chunk is dropped — replay is best-effort and the customer's next\n * session will still record cleanly.\n */\nasync function deliverChunk({\n ctx,\n uploadBaseUrl,\n authHeaders,\n payload,\n}: DeliverInput): Promise<void> {\n const sessionId = ctx.getSessionId();\n if (!sessionId) {\n return;\n }\n\n const body = JSON.stringify(payload.events);\n const sizeBytes = body.length;\n\n const res = await fetch(\n appendPathBeforeQuery(\n uploadBaseUrl,\n `${UPLOAD_PATH_PREFIX}${encodeURIComponent(sessionId)}`\n ),\n {\n method: \"POST\",\n headers: { ...authHeaders, \"content-type\": \"application/json\" },\n body,\n }\n );\n if (!res.ok) {\n log.warn(\"replay chunk upload rejected (%d)\", res.status);\n return;\n }\n\n const { objectKey } = (await res.json()) as UploadResponse;\n\n // Span-per-chunk is the cleanest OTel mapping: replay chunks are\n // events-in-time, not \"things that happened during a span\", so the\n // span itself carries the attrs. Mapper picks up by name later\n // (PRD 3 replay UI work).\n const span = trace.getTracer(TRACER_NAME).startSpan(\"replay.chunk\", {\n attributes: {\n \"replay.chunk.uri\": objectKey,\n \"replay.chunk.size_bytes\": sizeBytes,\n \"replay.chunk.event_count\": payload.events.length,\n ...(payload.firstTs === null\n ? {}\n : { \"replay.chunk.first_event_ts\": payload.firstTs }),\n ...(payload.lastTs === null\n ? {}\n : { \"replay.chunk.last_event_ts\": payload.lastTs }),\n },\n });\n span.end();\n}\n\nexport const replayPlugin: Plugin = {\n name: \"replay\",\n\n setup(ctx) {\n const targets = resolveTargets();\n const uploadBaseUrl = targets.collectorBaseUrl;\n const authHeaders = Object.fromEntries(targets.ingest.headers.entries());\n\n let stopFn: (() => void) | null = null;\n let events: string[] = [];\n let flushTimer: ReturnType<typeof setInterval> | null = null;\n let firstTs: number | null = null;\n let lastTs: number | null = null;\n\n const flush = () => {\n if (events.length === 0) {\n return;\n }\n const chunk = events;\n events = [];\n const payload: ChunkPayload = {\n events: chunk,\n firstTs,\n lastTs,\n };\n firstTs = null;\n lastTs = null;\n\n // Fire-and-forget; replay capture must never block the rrweb event\n // loop. Failed chunks are dropped — replay is best-effort and the\n // next session will still record cleanly.\n deliverChunk({ ctx, uploadBaseUrl, authHeaders, payload }).catch(\n (error: unknown) => {\n log.warn(\"replay chunk dropped: %o\", error);\n }\n );\n };\n\n let unsubscribeHidden: (() => void) | null = null;\n\n const init = async () => {\n try {\n const rrweb = await import(\"rrweb\");\n stopFn =\n rrweb.record({\n emit(event) {\n const ts = Date.now();\n if (firstTs === null) {\n firstTs = ts;\n }\n lastTs = ts;\n events.push(JSON.stringify(event));\n },\n }) ?? null;\n\n flushTimer = setInterval(flush, FLUSH_INTERVAL_MS);\n unsubscribeHidden = onPageHidden(flush);\n log.debug(\"recording started\");\n } catch {\n log.error(\"rrweb failed to load, replay disabled\");\n }\n };\n\n // `init()` swallows rrweb load failures internally — no outer `.catch`\n // needed (it can never reject).\n init();\n\n return () => {\n flush();\n stopFn?.();\n if (flushTimer) {\n clearInterval(flushTimer);\n }\n unsubscribeHidden?.();\n };\n },\n};\n\nexport default replayPlugin;\n"],"mappings":"kNAOA,MAAM,IAAM,aAAa,QAAQ,EA8BjC,eAAe,aAAa,CAC1B,IACA,cACA,YACA,SAC8B,CAC9B,IAAM,UAAY,IAAI,aAAa,EACnC,GAAI,CAAC,UACH,OAGF,IAAM,KAAO,KAAK,UAAU,QAAQ,MAAM,EACpC,UAAY,KAAK,OAEjB,IAAM,MAAM,MAChB,sBACE,cACA,qBAAwB,mBAAmB,SAAS,GACtD,EACA,CACE,OAAQ,OACR,QAAS,CAAE,GAAG,YAAa,eAAgB,kBAAmB,EAC9D,IACF,CACF,EACA,GAAI,CAAC,IAAI,GAAI,CACX,IAAI,KAAK,oCAAqC,IAAI,MAAM,EACxD,MACF,CAEA,GAAM,CAAE,WAAe,MAAM,IAAI,KAAK,EAmBtC,MAbmB,UAAU,yBAAW,EAAE,UAAU,eAAgB,CAClE,WAAY,CACV,mBAAoB,UACpB,0BAA2B,UAC3B,2BAA4B,QAAQ,OAAO,OAC3C,GAAI,QAAQ,UAAY,KACpB,CAAC,EACD,CAAE,8BAA+B,QAAQ,OAAQ,EACrD,GAAI,QAAQ,SAAW,KACnB,CAAC,EACD,CAAE,6BAA8B,QAAQ,MAAO,CACrD,CACF,CACG,EAAE,IAAI,CACX,CAEA,MAAa,aAAuB,CAClC,KAAM,SAEN,MAAM,IAAK,CACT,IAAM,QAAU,eAAe,EACzB,cAAgB,QAAQ,iBACxB,YAAc,OAAO,YAAY,QAAQ,OAAO,QAAQ,QAAQ,CAAC,EAEnE,OAA8B,KAC9B,OAAmB,CAAC,EACpB,WAAoD,KACpD,QAAyB,KACzB,OAAwB,KAEtB,UAAc,CAClB,GAAI,OAAO,SAAW,EACpB,OAEF,IAAM,MAAQ,OACd,OAAS,CAAC,EACV,IAAM,QAAwB,CAC5B,OAAQ,MACR,QACA,MACF,EACA,QAAU,KACV,OAAS,KAKT,aAAa,CAAE,IAAK,cAAe,YAAa,OAAQ,CAAC,EAAE,MACxD,OAAmB,CAClB,IAAI,KAAK,2BAA4B,KAAK,CAC5C,CACF,CACF,EAEI,kBAAyC,KA6B7C,OAFA,SAzByB,CACvB,GAAI,CAEF,QACE,MAFkB,OAAO,UAEnB,OAAO,CACX,KAAK,MAAO,CACV,IAAM,GAAK,KAAK,IAAI,EAChB,UAAY,OACd,QAAU,IAEZ,OAAS,GACT,OAAO,KAAK,KAAK,UAAU,KAAK,CAAC,CACnC,CACF,CAAC,GAAK,KAER,WAAa,YAAY,MAAO,GAAiB,EACjD,kBAAoB,aAAa,KAAK,EACtC,IAAI,MAAM,mBAAmB,CAC/B,MAAQ,CACN,IAAI,MAAM,uCAAuC,CACnD,CACF,GAIK,MAEQ,CACX,MAAM,EACN,SAAS,EACL,YACF,cAAc,UAAU,EAE1B,oBAAoB,CACtB,CACF,CACF"}
|
package/dist/provider.d.mts
CHANGED
|
@@ -1,26 +1,9 @@
|
|
|
1
|
+
import { Kernel } from "./internal/kernel.mjs";
|
|
2
|
+
import { InterfereContext, InterfereContextValue } from "./internal/react-context.mjs";
|
|
1
3
|
import { PropsWithChildren, ReactNode } from "react";
|
|
2
4
|
import { ConsentState } from "@interfere/types/sdk/plugins/manifest";
|
|
3
|
-
import { IdentifyParams } from "@interfere/types/sdk/identify";
|
|
4
5
|
|
|
5
6
|
//#region src/provider.d.ts
|
|
6
|
-
interface InterfereContextValue {
|
|
7
|
-
consent: {
|
|
8
|
-
get(): ConsentState | null;
|
|
9
|
-
set(state?: ConsentState): void;
|
|
10
|
-
};
|
|
11
|
-
device: {
|
|
12
|
-
getDeviceId(): string | null;
|
|
13
|
-
getFpHash(): string | null;
|
|
14
|
-
};
|
|
15
|
-
identity: {
|
|
16
|
-
get(): IdentifyParams | null;
|
|
17
|
-
set(params: IdentifyParams): Promise<void>;
|
|
18
|
-
};
|
|
19
|
-
session: {
|
|
20
|
-
getId(): string | null;
|
|
21
|
-
getWindowId(): string | null;
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
7
|
interface InterfereProviderProps extends PropsWithChildren {
|
|
25
8
|
consent?: ConsentState | undefined;
|
|
26
9
|
/**
|
|
@@ -40,8 +23,16 @@ interface InterfereProviderProps extends PropsWithChildren {
|
|
|
40
23
|
* @default true
|
|
41
24
|
*/
|
|
42
25
|
errorBoundary?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* The active kernel from `init()`. Pass `null` (or omit on the server)
|
|
28
|
+
* when the SDK isn't initialized — useful during SSR/SSG where `init()`
|
|
29
|
+
* runs client-side only. The provider mounts with null-safe accessors
|
|
30
|
+
* and switches to the real kernel once it's available.
|
|
31
|
+
*/
|
|
32
|
+
kernel: Kernel | null;
|
|
43
33
|
}
|
|
44
34
|
declare function InterfereProvider({
|
|
35
|
+
kernel,
|
|
45
36
|
children,
|
|
46
37
|
consent,
|
|
47
38
|
errorBoundary
|
|
@@ -49,4 +40,4 @@ declare function InterfereProvider({
|
|
|
49
40
|
declare function useInterfere(): InterfereContextValue;
|
|
50
41
|
declare function useSession(): string | null;
|
|
51
42
|
//#endregion
|
|
52
|
-
export { InterfereProvider, useInterfere, useSession };
|
|
43
|
+
export { InterfereContext, type InterfereContextValue, InterfereProvider, useInterfere, useSession };
|
package/dist/provider.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.mts","names":[],"sources":["../src/provider.tsx"],"mappings":"
|
|
1
|
+
{"version":3,"file":"provider.d.mts","names":[],"sources":["../src/provider.tsx"],"mappings":";;;;;;UAmBU,sBAAA,SAA+B,iBAAA;EACvC,OAAA,GAAU,YAAA;EADF;;;;;;;;;;;;;;;AAyBM;EAPd,aAAA;EAU+B;;;;;;EAH/B,MAAA,EAAQ,MAAA;AAAA;AAAA,iBAGM,iBAAA,CAAA;EACd,MAAA;EACA,QAAA;EACA,OAAA;EACA;AAAA,GACC,sBAAA,GAAyB,SAAA;AAAA,iBAwBZ,YAAA,CAAA,GAAgB,qBAAqB;AAAA,iBAQrC,UAAA,CAAA"}
|
package/dist/provider.mjs
CHANGED
|
@@ -1,32 +1 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { device, identity, session } from "./tracking/api.mjs";
|
|
3
|
-
import { consent, syncConsent } from "./internal/client.mjs";
|
|
4
|
-
import { CaptureBoundary } from "./internal/capture-boundary.mjs";
|
|
5
|
-
import { createContext, useContext, useEffect } from "react";
|
|
6
|
-
import { jsx } from "react/jsx-runtime";
|
|
7
|
-
//#region src/provider.tsx
|
|
8
|
-
const InterfereContext = createContext(null);
|
|
9
|
-
function InterfereProvider({ children, consent: consent$1, errorBoundary = true }) {
|
|
10
|
-
useEffect(() => {
|
|
11
|
-
syncConsent(consent$1);
|
|
12
|
-
}, [consent$1]);
|
|
13
|
-
return /* @__PURE__ */ jsx(InterfereContext, {
|
|
14
|
-
value: {
|
|
15
|
-
consent,
|
|
16
|
-
device,
|
|
17
|
-
identity,
|
|
18
|
-
session
|
|
19
|
-
},
|
|
20
|
-
children: errorBoundary ? /* @__PURE__ */ jsx(CaptureBoundary, { children }) : children
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
function useInterfere() {
|
|
24
|
-
const ctx = useContext(InterfereContext);
|
|
25
|
-
if (!ctx) throw new Error("useInterfere must be used within <InterfereProvider>");
|
|
26
|
-
return ctx;
|
|
27
|
-
}
|
|
28
|
-
function useSession() {
|
|
29
|
-
return useInterfere().session.getId();
|
|
30
|
-
}
|
|
31
|
-
//#endregion
|
|
32
|
-
export { InterfereProvider, useInterfere, useSession };
|
|
1
|
+
"use client";import{InterfereContext,NULL_CONTEXT_VALUE}from"./internal/react-context.mjs";import{CaptureBoundary}from"./internal/capture-boundary.mjs";import{useContext,useEffect}from"react";import{jsx}from"react/jsx-runtime";function InterfereProvider({kernel,children,consent,errorBoundary=!0}){return useEffect(()=>{kernel?.syncConsent(consent)},[kernel,consent]),jsx(InterfereContext,{value:kernel?{consent:kernel.consent,device:kernel.device,identity:kernel.identity,kernel,session:kernel.session}:NULL_CONTEXT_VALUE,children:errorBoundary?jsx(CaptureBoundary,{children}):children})}function useInterfere(){let ctx=useContext(InterfereContext);if(!ctx)throw Error(`useInterfere must be used within <InterfereProvider>`);return ctx}function useSession(){return useInterfere().session.getId()}export{InterfereContext,InterfereProvider,useInterfere,useSession};
|
package/dist/provider.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.mjs","names":[
|
|
1
|
+
{"version":3,"file":"provider.mjs","names":[],"sources":["../src/provider.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ConsentState } from \"@interfere/types/sdk/plugins/manifest\";\n\nimport {\n type PropsWithChildren,\n type ReactNode,\n useContext,\n useEffect,\n} from \"react\";\n\nimport { CaptureBoundary } from \"./internal/capture-boundary.js\";\nimport type { Kernel } from \"./internal/kernel.js\";\nimport type { InterfereContextValue } from \"./internal/react-context.js\";\nimport {\n InterfereContext,\n NULL_CONTEXT_VALUE,\n} from \"./internal/react-context.js\";\n\ninterface InterfereProviderProps extends PropsWithChildren {\n consent?: ConsentState | undefined;\n /**\n * Auto-install an internal capture boundary around `children` so\n * render-phase React errors are reported even when the host app has no\n * `ErrorBoundary` / `error.tsx` of its own.\n *\n * The internal boundary is transparent: it captures the error and then\n * re-throws so upstream boundaries (the customer's own, Next.js's\n * `error.tsx` / `global-error.tsx`, or React's default unmount) keep full\n * control of what the user sees. Safe to leave enabled.\n *\n * Pass `false` only if you explicitly don't want automatic capture of\n * render-phase errors — for example, if you've already wired\n * {@link reactErrorHandler} into `createRoot()`.\n *\n * @default true\n */\n errorBoundary?: boolean;\n /**\n * The active kernel from `init()`. Pass `null` (or omit on the server)\n * when the SDK isn't initialized — useful during SSR/SSG where `init()`\n * runs client-side only. The provider mounts with null-safe accessors\n * and switches to the real kernel once it's available.\n */\n kernel: Kernel | null;\n}\n\nexport function InterfereProvider({\n kernel,\n children,\n consent,\n errorBoundary = true,\n}: InterfereProviderProps): ReactNode {\n useEffect(() => {\n kernel?.syncConsent(consent);\n }, [kernel, consent]);\n\n const value: InterfereContextValue = kernel\n ? {\n consent: kernel.consent,\n device: kernel.device,\n identity: kernel.identity,\n kernel,\n session: kernel.session,\n }\n : NULL_CONTEXT_VALUE;\n\n const body = errorBoundary ? (\n <CaptureBoundary>{children}</CaptureBoundary>\n ) : (\n children\n );\n\n return <InterfereContext value={value}>{body}</InterfereContext>;\n}\n\nexport function useInterfere(): InterfereContextValue {\n const ctx = useContext(InterfereContext);\n if (!ctx) {\n throw new Error(\"useInterfere must be used within <InterfereProvider>\");\n }\n return ctx;\n}\n\nexport function useSession(): string | null {\n return useInterfere().session.getId();\n}\n\nexport type { InterfereContextValue } from \"./internal/react-context.js\";\n// biome-ignore lint/performance/noBarrelFile: Re-export the context handle alongside the provider so consumers (custom error boundaries, the `@interfere/next` wrapper) can import everything from one entry.\nexport { InterfereContext } from \"./internal/react-context.js\";\n"],"mappings":"mOA+CA,SAAgB,kBAAkB,CAChC,OACA,SACA,QACA,cAAgB,IACoB,CAqBpC,OApBA,cAAgB,CACd,QAAQ,YAAY,OAAO,CAC7B,EAAG,CAAC,OAAQ,OAAO,CAAC,EAkBb,IAAC,iBAAD,CAAkB,MAhBY,OACjC,CACE,QAAS,OAAO,QAChB,OAAQ,OAAO,OACf,SAAU,OAAO,SACjB,OACA,QAAS,OAAO,OAClB,EACA,4BAES,cACX,IAAC,gBAAD,CAAkB,QAA0B,CAAA,EAE5C,QAG6D,CAAA,CACjE,CAEA,SAAgB,cAAsC,CACpD,IAAM,IAAM,WAAW,gBAAgB,EACvC,GAAI,CAAC,IACH,MAAU,MAAM,sDAAsD,EAExE,OAAO,GACT,CAEA,SAAgB,YAA4B,CAC1C,OAAO,aAAa,EAAE,QAAQ,MAAM,CACtC"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Kernel } from "./internal/kernel.mjs";
|
|
2
|
+
|
|
1
3
|
//#region src/react-error-handler.d.ts
|
|
2
4
|
/**
|
|
3
5
|
* Matches the shape React 19 passes to `onCaughtError`, `onUncaughtError`,
|
|
@@ -30,7 +32,18 @@ interface ReactErrorHandlerCallbacks {
|
|
|
30
32
|
*/
|
|
31
33
|
onUncaughtError?: RootErrorCallback;
|
|
32
34
|
}
|
|
33
|
-
interface ReactErrorHandlerOptions extends ReactErrorHandlerCallbacks {
|
|
35
|
+
interface ReactErrorHandlerOptions extends ReactErrorHandlerCallbacks {
|
|
36
|
+
/**
|
|
37
|
+
* The kernel to capture errors against. Pass either the kernel directly
|
|
38
|
+
* or a getter — framework wrappers expose `getKernelOrNull()` for the
|
|
39
|
+
* latter pattern, which lets you wire the handler before `init()`
|
|
40
|
+
* resolves. Pass `getKernelOrNull` (not `getKernel`) — the throwing
|
|
41
|
+
* variant breaks this contract since the handler runs in error-recovery
|
|
42
|
+
* paths where rethrowing isn't useful. Capture is silently skipped when
|
|
43
|
+
* no kernel is available.
|
|
44
|
+
*/
|
|
45
|
+
kernel?: Kernel | (() => Kernel | null) | null;
|
|
46
|
+
}
|
|
34
47
|
interface ReactRootErrorHandlers {
|
|
35
48
|
onCaughtError: RootErrorCallback;
|
|
36
49
|
onRecoverableError: RootErrorCallback;
|
|
@@ -42,11 +55,13 @@ interface ReactRootErrorHandlers {
|
|
|
42
55
|
*
|
|
43
56
|
* ```ts
|
|
44
57
|
* import { createRoot } from "react-dom/client";
|
|
58
|
+
* import { getKernelOrNull } from "@interfere/next";
|
|
45
59
|
* import { reactErrorHandler } from "@interfere/react/react-error-handler";
|
|
46
60
|
*
|
|
47
|
-
* createRoot(
|
|
48
|
-
*
|
|
49
|
-
* )
|
|
61
|
+
* createRoot(
|
|
62
|
+
* document.getElementById("root")!,
|
|
63
|
+
* reactErrorHandler({ kernel: getKernelOrNull })
|
|
64
|
+
* ).render(<App />);
|
|
50
65
|
* ```
|
|
51
66
|
*
|
|
52
67
|
* Pass your own callbacks to observe errors without replacing the capture
|
|
@@ -54,10 +69,11 @@ interface ReactRootErrorHandlers {
|
|
|
54
69
|
*
|
|
55
70
|
* ```ts
|
|
56
71
|
* reactErrorHandler({
|
|
72
|
+
* kernel: getKernelOrNull,
|
|
57
73
|
* onUncaughtError: (err) => myLogger.fatal(err),
|
|
58
74
|
* });
|
|
59
75
|
* ```
|
|
60
76
|
*/
|
|
61
|
-
declare function reactErrorHandler(
|
|
77
|
+
declare function reactErrorHandler(options?: ReactErrorHandlerOptions): ReactRootErrorHandlers;
|
|
62
78
|
//#endregion
|
|
63
79
|
export { ReactErrorHandlerCallbacks, ReactErrorHandlerOptions, reactErrorHandler };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react-error-handler.d.mts","names":[],"sources":["../src/react-error-handler.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"react-error-handler.d.mts","names":[],"sources":["../src/react-error-handler.ts"],"mappings":";;;;;AAKmD;;;;AAUnC;UADN,kBAAA;EACR,cAAc;AAAA;AAAA,KAGX,iBAAA,IAAqB,KAAA,WAAgB,IAAA,EAAM,kBAAkB;AAAA,UAEjD,0BAAA;EAF+B;;;AAAkB;AAElE;EAME,aAAA,GAAgB,iBAAA;;;;;;EAMhB,kBAAA,GAAqB,iBAAA;EANrB;;;;;EAYA,eAAA,GAAkB,iBAAA;AAAA;AAAA,UAGH,wBAAA,SAAiC,0BAAA;EAAjC;;;;;;;;;EAUf,MAAA,GAAS,MAAA,UAAgB,MAAA;AAAA;AAAA,UAGjB,sBAAA;EACR,aAAA,EAAe,iBAAA;EACf,kBAAA,EAAoB,iBAAA;EACpB,eAAA,EAAiB,iBAAA;AAAA;;;;;;;;;;;;;;AAAiB;AAqCpC;;;;;;;;AAEyB;;;iBAFT,iBAAA,CACd,OAAA,GAAS,wBAAA,GACR,sBAAsB"}
|
|
@@ -1,54 +1 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { captureReactError } from "./internal/capture.mjs";
|
|
3
|
-
import { MECHANISM_TYPE } from "@interfere/types/sdk/errors";
|
|
4
|
-
//#region src/react-error-handler.ts
|
|
5
|
-
/**
|
|
6
|
-
* Creates the three error callbacks React 19's `createRoot()` / `hydrateRoot()`
|
|
7
|
-
* accept, wired into Interfere's capture pipeline.
|
|
8
|
-
*
|
|
9
|
-
* ```ts
|
|
10
|
-
* import { createRoot } from "react-dom/client";
|
|
11
|
-
* import { reactErrorHandler } from "@interfere/react/react-error-handler";
|
|
12
|
-
*
|
|
13
|
-
* createRoot(document.getElementById("root")!, reactErrorHandler()).render(
|
|
14
|
-
* <App />
|
|
15
|
-
* );
|
|
16
|
-
* ```
|
|
17
|
-
*
|
|
18
|
-
* Pass your own callbacks to observe errors without replacing the capture
|
|
19
|
-
* behaviour — user callbacks run after Interfere has captured:
|
|
20
|
-
*
|
|
21
|
-
* ```ts
|
|
22
|
-
* reactErrorHandler({
|
|
23
|
-
* onUncaughtError: (err) => myLogger.fatal(err),
|
|
24
|
-
* });
|
|
25
|
-
* ```
|
|
26
|
-
*/
|
|
27
|
-
function reactErrorHandler(callbacks = {}) {
|
|
28
|
-
return {
|
|
29
|
-
onCaughtError(error, info) {
|
|
30
|
-
if (error instanceof Error) captureReactError(error, info.componentStack, {
|
|
31
|
-
type: MECHANISM_TYPE.react.caughtError,
|
|
32
|
-
handled: true
|
|
33
|
-
});
|
|
34
|
-
callbacks.onCaughtError?.(error, info);
|
|
35
|
-
},
|
|
36
|
-
onUncaughtError(error, info) {
|
|
37
|
-
if (error instanceof Error) captureReactError(error, info.componentStack, {
|
|
38
|
-
type: MECHANISM_TYPE.react.uncaughtError,
|
|
39
|
-
handled: false
|
|
40
|
-
});
|
|
41
|
-
callbacks.onUncaughtError?.(error, info);
|
|
42
|
-
},
|
|
43
|
-
onRecoverableError(error, info) {
|
|
44
|
-
if (error instanceof Error) captureReactError(error, info.componentStack, {
|
|
45
|
-
type: MECHANISM_TYPE.react.recoverableError,
|
|
46
|
-
handled: true,
|
|
47
|
-
synthetic: true
|
|
48
|
-
});
|
|
49
|
-
callbacks.onRecoverableError?.(error, info);
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
//#endregion
|
|
54
|
-
export { reactErrorHandler };
|
|
1
|
+
"use client";import{captureReactError}from"./internal/capture.mjs";import{MECHANISM_TYPE}from"@interfere/types/sdk/errors";function resolveKernel(source){return source?typeof source==`function`?source():source:null}function reactErrorHandler(options={}){let{kernel:kernelSource,...callbacks}=options;return{onCaughtError(error,info){error instanceof Error&&captureReactError(resolveKernel(kernelSource),error,info.componentStack,{type:MECHANISM_TYPE.react.caughtError,handled:!0}),callbacks.onCaughtError?.(error,info)},onUncaughtError(error,info){error instanceof Error&&captureReactError(resolveKernel(kernelSource),error,info.componentStack,{type:MECHANISM_TYPE.react.uncaughtError,handled:!1}),callbacks.onUncaughtError?.(error,info)},onRecoverableError(error,info){error instanceof Error&&captureReactError(resolveKernel(kernelSource),error,info.componentStack,{type:MECHANISM_TYPE.react.recoverableError,handled:!0,synthetic:!0}),callbacks.onRecoverableError?.(error,info)}}}export{reactErrorHandler};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react-error-handler.mjs","names":[],"sources":["../src/react-error-handler.ts"],"sourcesContent":["\"use client\";\n\nimport { MECHANISM_TYPE } from \"@interfere/types/sdk/errors\";\n\nimport { captureReactError } from \"./internal/capture.js\";\n\n/**\n * Matches the shape React 19 passes to `onCaughtError`, `onUncaughtError`,\n * and `onRecoverableError` on `createRoot()` / `hydrateRoot()`.\n *\n * We intentionally keep this permissive — React has iterated on the exact\n * fields of `ErrorInfo` across 19.x minors and we only need `componentStack`.\n */\ninterface ReactRootErrorInfo {\n componentStack?: string | null | undefined;\n}\n\ntype RootErrorCallback = (error: unknown, info: ReactRootErrorInfo) => void;\n\nexport interface ReactErrorHandlerCallbacks {\n /**\n * Called when a React error boundary caught and handled an error. Interfere\n * reports it with `mechanism.handled: true`. Your callback runs after the\n * capture.\n */\n onCaughtError?: RootErrorCallback;\n /**\n * Called when concurrent React auto-recovered from an error by retrying\n * the render. Reported with `mechanism.synthetic: true` so the agent can\n * deprioritize — these often aren't user-facing bugs.\n */\n onRecoverableError?: RootErrorCallback;\n /**\n * Called when a render-phase error propagated past every boundary and\n * React unmounted (or fell back to its default UI). Reported with\n * `mechanism.handled: false` — this is the \"real\" uncaught case.\n */\n onUncaughtError?: RootErrorCallback;\n}\n\nexport interface ReactErrorHandlerOptions extends ReactErrorHandlerCallbacks {}\n\ninterface ReactRootErrorHandlers {\n onCaughtError: RootErrorCallback;\n onRecoverableError: RootErrorCallback;\n onUncaughtError: RootErrorCallback;\n}\n\n/**\n * Creates the three error callbacks React 19's `createRoot()` / `hydrateRoot()`\n * accept, wired into Interfere's capture pipeline.\n *\n * ```ts\n * import { createRoot } from \"react-dom/client\";\n * import { reactErrorHandler } from \"@interfere/react/react-error-handler\";\n *\n * createRoot(document.getElementById(\"root\")
|
|
1
|
+
{"version":3,"file":"react-error-handler.mjs","names":[],"sources":["../src/react-error-handler.ts"],"sourcesContent":["\"use client\";\n\nimport { MECHANISM_TYPE } from \"@interfere/types/sdk/errors\";\n\nimport { captureReactError } from \"./internal/capture.js\";\nimport type { Kernel } from \"./internal/kernel.js\";\n\n/**\n * Matches the shape React 19 passes to `onCaughtError`, `onUncaughtError`,\n * and `onRecoverableError` on `createRoot()` / `hydrateRoot()`.\n *\n * We intentionally keep this permissive — React has iterated on the exact\n * fields of `ErrorInfo` across 19.x minors and we only need `componentStack`.\n */\ninterface ReactRootErrorInfo {\n componentStack?: string | null | undefined;\n}\n\ntype RootErrorCallback = (error: unknown, info: ReactRootErrorInfo) => void;\n\nexport interface ReactErrorHandlerCallbacks {\n /**\n * Called when a React error boundary caught and handled an error. Interfere\n * reports it with `mechanism.handled: true`. Your callback runs after the\n * capture.\n */\n onCaughtError?: RootErrorCallback;\n /**\n * Called when concurrent React auto-recovered from an error by retrying\n * the render. Reported with `mechanism.synthetic: true` so the agent can\n * deprioritize — these often aren't user-facing bugs.\n */\n onRecoverableError?: RootErrorCallback;\n /**\n * Called when a render-phase error propagated past every boundary and\n * React unmounted (or fell back to its default UI). Reported with\n * `mechanism.handled: false` — this is the \"real\" uncaught case.\n */\n onUncaughtError?: RootErrorCallback;\n}\n\nexport interface ReactErrorHandlerOptions extends ReactErrorHandlerCallbacks {\n /**\n * The kernel to capture errors against. Pass either the kernel directly\n * or a getter — framework wrappers expose `getKernelOrNull()` for the\n * latter pattern, which lets you wire the handler before `init()`\n * resolves. Pass `getKernelOrNull` (not `getKernel`) — the throwing\n * variant breaks this contract since the handler runs in error-recovery\n * paths where rethrowing isn't useful. Capture is silently skipped when\n * no kernel is available.\n */\n kernel?: Kernel | (() => Kernel | null) | null;\n}\n\ninterface ReactRootErrorHandlers {\n onCaughtError: RootErrorCallback;\n onRecoverableError: RootErrorCallback;\n onUncaughtError: RootErrorCallback;\n}\n\nfunction resolveKernel(\n source: ReactErrorHandlerOptions[\"kernel\"]\n): Kernel | null {\n if (!source) {\n return null;\n }\n return typeof source === \"function\" ? source() : source;\n}\n\n/**\n * Creates the three error callbacks React 19's `createRoot()` / `hydrateRoot()`\n * accept, wired into Interfere's capture pipeline.\n *\n * ```ts\n * import { createRoot } from \"react-dom/client\";\n * import { getKernelOrNull } from \"@interfere/next\";\n * import { reactErrorHandler } from \"@interfere/react/react-error-handler\";\n *\n * createRoot(\n * document.getElementById(\"root\")!,\n * reactErrorHandler({ kernel: getKernelOrNull })\n * ).render(<App />);\n * ```\n *\n * Pass your own callbacks to observe errors without replacing the capture\n * behaviour — user callbacks run after Interfere has captured:\n *\n * ```ts\n * reactErrorHandler({\n * kernel: getKernelOrNull,\n * onUncaughtError: (err) => myLogger.fatal(err),\n * });\n * ```\n */\nexport function reactErrorHandler(\n options: ReactErrorHandlerOptions = {}\n): ReactRootErrorHandlers {\n const { kernel: kernelSource, ...callbacks } = options;\n return {\n onCaughtError(error, info) {\n if (error instanceof Error) {\n captureReactError(\n resolveKernel(kernelSource),\n error,\n info.componentStack,\n {\n type: MECHANISM_TYPE.react.caughtError,\n handled: true,\n }\n );\n }\n callbacks.onCaughtError?.(error, info);\n },\n onUncaughtError(error, info) {\n if (error instanceof Error) {\n captureReactError(\n resolveKernel(kernelSource),\n error,\n info.componentStack,\n {\n type: MECHANISM_TYPE.react.uncaughtError,\n handled: false,\n }\n );\n }\n callbacks.onUncaughtError?.(error, info);\n },\n onRecoverableError(error, info) {\n if (error instanceof Error) {\n captureReactError(\n resolveKernel(kernelSource),\n error,\n info.componentStack,\n {\n type: MECHANISM_TYPE.react.recoverableError,\n handled: true,\n synthetic: true,\n }\n );\n }\n callbacks.onRecoverableError?.(error, info);\n },\n };\n}\n"],"mappings":"2HA4DA,SAAS,cACP,OACe,CAIf,OAHK,OAGE,OAAO,QAAW,WAAa,OAAO,EAAI,OAFxC,IAGX,CA2BA,SAAgB,kBACd,QAAoC,CAAC,EACb,CACxB,GAAM,CAAE,OAAQ,aAAc,GAAG,WAAc,QAC/C,MAAO,CACL,cAAc,MAAO,KAAM,CACrB,iBAAiB,OACnB,kBACE,cAAc,YAAY,EAC1B,MACA,KAAK,eACL,CACE,KAAM,eAAe,MAAM,YAC3B,QAAS,EACX,CACF,EAEF,UAAU,gBAAgB,MAAO,IAAI,CACvC,EACA,gBAAgB,MAAO,KAAM,CACvB,iBAAiB,OACnB,kBACE,cAAc,YAAY,EAC1B,MACA,KAAK,eACL,CACE,KAAM,eAAe,MAAM,cAC3B,QAAS,EACX,CACF,EAEF,UAAU,kBAAkB,MAAO,IAAI,CACzC,EACA,mBAAmB,MAAO,KAAM,CAC1B,iBAAiB,OACnB,kBACE,cAAc,YAAY,EAC1B,MACA,KAAK,eACL,CACE,KAAM,eAAe,MAAM,iBAC3B,QAAS,GACT,UAAW,EACb,CACF,EAEF,UAAU,qBAAqB,MAAO,IAAI,CAC5C,CACF,CACF"}
|
package/dist/sw.d.mts
ADDED
package/dist/sw.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// AUTOGENERATED by scripts/build-sw.ts — do not edit.
|
|
2
|
+
export const SW_SCRIPT = "(function(){try{self[`workbox:core:7.4.0`]&&_()}catch{}let e=(e,...t)=>{let n=e;return t.length>0&&(n+=` :: ${JSON.stringify(t)}`),n};var t=class extends Error{constructor(t,n){let r=e(t,n);super(r),this.name=t,this.details=n}};let n=(e,t)=>t.some(t=>e instanceof t),r,i;function a(){return r||=[IDBDatabase,IDBObjectStore,IDBIndex,IDBCursor,IDBTransaction]}function o(){return i||=[IDBCursor.prototype.advance,IDBCursor.prototype.continue,IDBCursor.prototype.continuePrimaryKey]}let s=new WeakMap,c=new WeakMap,l=new WeakMap,u=new WeakMap,d=new WeakMap;function f(e){let t=new Promise((t,n)=>{let r=()=>{e.removeEventListener(`success`,i),e.removeEventListener(`error`,a)},i=()=>{t(y(e.result)),r()},a=()=>{n(e.error),r()};e.addEventListener(`success`,i),e.addEventListener(`error`,a)});return t.then(t=>{t instanceof IDBCursor&&s.set(t,e)}).catch(()=>{}),d.set(t,e),t}function p(e){if(c.has(e))return;let t=new Promise((t,n)=>{let r=()=>{e.removeEventListener(`complete`,i),e.removeEventListener(`error`,a),e.removeEventListener(`abort`,a)},i=()=>{t(),r()},a=()=>{n(e.error||new DOMException(`AbortError`,`AbortError`)),r()};e.addEventListener(`complete`,i),e.addEventListener(`error`,a),e.addEventListener(`abort`,a)});c.set(e,t)}let m={get(e,t,n){if(e instanceof IDBTransaction){if(t===`done`)return c.get(e);if(t===`objectStoreNames`)return e.objectStoreNames||l.get(e);if(t===`store`)return n.objectStoreNames[1]?void 0:n.objectStore(n.objectStoreNames[0])}return y(e[t])},set(e,t,n){return e[t]=n,!0},has(e,t){return e instanceof IDBTransaction&&(t===`done`||t===`store`)?!0:t in e}};function h(e){m=e(m)}function g(e){return e===IDBDatabase.prototype.transaction&&!(`objectStoreNames`in IDBTransaction.prototype)?function(t,...n){let r=e.call(b(this),t,...n);return l.set(r,t.sort?t.sort():[t]),y(r)}:o().includes(e)?function(...t){return e.apply(b(this),t),y(s.get(this))}:function(...t){return y(e.apply(b(this),t))}}function v(e){return typeof e==`function`?g(e):(e instanceof IDBTransaction&&p(e),n(e,a())?new Proxy(e,m):e)}function y(e){if(e instanceof IDBRequest)return f(e);if(u.has(e))return u.get(e);let t=v(e);return t!==e&&(u.set(e,t),d.set(t,e)),t}let b=e=>d.get(e);function x(e,t,{blocked:n,upgrade:r,blocking:i,terminated:a}={}){let o=indexedDB.open(e,t),s=y(o);return r&&o.addEventListener(`upgradeneeded`,e=>{r(y(o.result),e.oldVersion,e.newVersion,y(o.transaction),e)}),n&&o.addEventListener(`blocked`,e=>n(e.oldVersion,e.newVersion,e)),s.then(e=>{a&&e.addEventListener(`close`,()=>a()),i&&e.addEventListener(`versionchange`,e=>i(e.oldVersion,e.newVersion,e))}).catch(()=>{}),s}let S=[`get`,`getKey`,`getAll`,`getAllKeys`,`count`],C=[`put`,`add`,`delete`,`clear`],w=new Map;function T(e,t){if(!(e instanceof IDBDatabase&&!(t in e)&&typeof t==`string`))return;if(w.get(t))return w.get(t);let n=t.replace(/FromIndex$/,``),r=t!==n,i=C.includes(n);if(!(n in(r?IDBIndex:IDBObjectStore).prototype)||!(i||S.includes(n)))return;let a=async function(e,...t){let a=this.transaction(e,i?`readwrite`:`readonly`),o=a.store;return r&&(o=o.index(t.shift())),(await Promise.all([o[n](...t),i&&a.done]))[0]};return w.set(t,a),a}h(e=>({...e,get:(t,n,r)=>T(t,n)||e.get(t,n,r),has:(t,n)=>!!T(t,n)||e.has(t,n)}));try{self[`workbox:background-sync:7.4.0`]&&_()}catch{}let E=`requests`,D=`queueName`;var O=class{constructor(){this._db=null}async addEntry(e){let t=(await this.getDb()).transaction(E,`readwrite`,{durability:`relaxed`});await t.store.add(e),await t.done}async getFirstEntryId(){return(await(await this.getDb()).transaction(E).store.openCursor())?.value.id}async getAllEntriesByQueueName(e){return await(await this.getDb()).getAllFromIndex(E,D,IDBKeyRange.only(e))||[]}async getEntryCountByQueueName(e){return(await this.getDb()).countFromIndex(E,D,IDBKeyRange.only(e))}async deleteEntry(e){await(await this.getDb()).delete(E,e)}async getFirstEntryByQueueName(e){return await this.getEndEntryFromIndex(IDBKeyRange.only(e),`next`)}async getLastEntryByQueueName(e){return await this.getEndEntryFromIndex(IDBKeyRange.only(e),`prev`)}async getEndEntryFromIndex(e,t){return(await(await this.getDb()).transaction(E).store.index(D).openCursor(e,t))?.value}async getDb(){return this._db||=await x(`workbox-background-sync`,3,{upgrade:this._upgradeDb}),this._db}_upgradeDb(e,t){t>0&&t<3&&e.objectStoreNames.contains(E)&&e.deleteObjectStore(E),e.createObjectStore(E,{autoIncrement:!0,keyPath:`id`}).createIndex(D,D,{unique:!1})}},k=class{constructor(e){this._queueName=e,this._queueDb=new O}async pushEntry(e){delete e.id,e.queueName=this._queueName,await this._queueDb.addEntry(e)}async unshiftEntry(e){let t=await this._queueDb.getFirstEntryId();t?e.id=t-1:delete e.id,e.queueName=this._queueName,await this._queueDb.addEntry(e)}async popEntry(){return this._removeEntry(await this._queueDb.getLastEntryByQueueName(this._queueName))}async shiftEntry(){return this._removeEntry(await this._queueDb.getFirstEntryByQueueName(this._queueName))}async getAll(){return await this._queueDb.getAllEntriesByQueueName(this._queueName)}async size(){return await this._queueDb.getEntryCountByQueueName(this._queueName)}async deleteEntry(e){await this._queueDb.deleteEntry(e)}async _removeEntry(e){return e&&await this.deleteEntry(e.id),e}};let A=[`method`,`referrer`,`referrerPolicy`,`mode`,`credentials`,`cache`,`redirect`,`integrity`,`keepalive`];var j=class e{static async fromRequest(t){let n={url:t.url,headers:{}};t.method!==`GET`&&(n.body=await t.clone().arrayBuffer());for(let[e,r]of t.headers.entries())n.headers[e]=r;for(let e of A)t[e]!==void 0&&(n[e]=t[e]);return new e(n)}constructor(e){e.mode===`navigate`&&(e.mode=`same-origin`),this._requestData=e}toObject(){let e=Object.assign({},this._requestData);return e.headers=Object.assign({},this._requestData.headers),e.body&&=e.body.slice(0),e}toRequest(){return new Request(this._requestData.url,this._requestData)}clone(){return new e(this.toObject())}};let M=`workbox-background-sync`,N=new Set,P=e=>{let t={request:new j(e.requestData).toRequest(),timestamp:e.timestamp};return e.metadata&&(t.metadata=e.metadata),t};var F=class{constructor(e,{forceSyncFallback:n,onSync:r,maxRetentionTime:i}={}){if(this._syncInProgress=!1,this._requestsAddedDuringSync=!1,N.has(e))throw new t(`duplicate-queue-name`,{name:e});N.add(e),this._name=e,this._onSync=r||this.replayRequests,this._maxRetentionTime=i||10080,this._forceSyncFallback=!!n,this._queueStore=new k(this._name),this._addSyncListener()}get name(){return this._name}async pushRequest(e){await this._addRequest(e,`push`)}async unshiftRequest(e){await this._addRequest(e,`unshift`)}async popRequest(){return this._removeRequest(`pop`)}async shiftRequest(){return this._removeRequest(`shift`)}async getAll(){let e=await this._queueStore.getAll(),t=Date.now(),n=[];for(let r of e){let e=this._maxRetentionTime*60*1e3;t-r.timestamp>e?await this._queueStore.deleteEntry(r.id):n.push(P(r))}return n}async size(){return await this._queueStore.size()}async _addRequest({request:e,metadata:t,timestamp:n=Date.now()},r){let i={requestData:(await j.fromRequest(e.clone())).toObject(),timestamp:n};switch(t&&(i.metadata=t),r){case`push`:await this._queueStore.pushEntry(i);break;case`unshift`:await this._queueStore.unshiftEntry(i);break}this._syncInProgress?this._requestsAddedDuringSync=!0:await this.registerSync()}async _removeRequest(e){let t=Date.now(),n;switch(e){case`pop`:n=await this._queueStore.popEntry();break;case`shift`:n=await this._queueStore.shiftEntry();break}if(n){let r=this._maxRetentionTime*60*1e3;return t-n.timestamp>r?this._removeRequest(e):P(n)}else return}async replayRequests(){let e;for(;e=await this.shiftRequest();)try{await fetch(e.request.clone())}catch{throw await this.unshiftRequest(e),new t(`queue-replay-failed`,{name:this._name})}}async registerSync(){if(`sync`in self.registration&&!this._forceSyncFallback)try{await self.registration.sync.register(`${M}:${this._name}`)}catch{}}_addSyncListener(){`sync`in self.registration&&!this._forceSyncFallback?self.addEventListener(`sync`,e=>{e.tag===`${M}:${this._name}`&&e.waitUntil((async()=>{this._syncInProgress=!0;let t;try{await this._onSync({queue:this})}catch(e){if(e instanceof Error)throw t=e,t}finally{this._requestsAddedDuringSync&&!(t&&!e.lastChance)&&await this.registerSync(),this._syncInProgress=!1,this._requestsAddedDuringSync=!1}})())}):this._onSync({queue:this})}static get _queueNames(){return N}};let I={status:202,statusText:`Queued`};async function L(e){let t=await self.clients.matchAll({includeUncontrolled:!1});for(let n of t)n.postMessage(e)}async function R(e){let t=await e.getAll(),n=0;for(let e of t){let t=e.metadata;typeof t?.bytes==`number`&&(n+=t.bytes)}return n}async function z(e,t){let n=await R(e);for(;n+t>10485760;){let t=await e.shiftRequest();if(!t)break;let r=t.metadata;n-=r?.bytes??0,await L({type:`interfere.sw.dropped`,url:t.request.url,reason:`queue_size_cap`})}}async function B(e,t){let n=t.clone(),r=(await n.clone().arrayBuffer()).byteLength;await z(e,r),await e.pushRequest({request:n,metadata:{bytes:r}}),await L({type:`interfere.sw.queued`,url:t.url})}let V=new F(`interfere-ingest`,{maxRetentionTime:1440,onSync:async({queue:e})=>{let t=await e.shiftRequest();for(;t;){try{let e=await fetch(t.request.clone());if(!e.ok&&(e.status>=500||e.status===429))throw Error(`retry status ${e.status}`);await L({type:`interfere.sw.replayed`,url:t.request.url})}catch(n){throw await e.unshiftRequest(t),n}t=await e.shiftRequest()}}});self.addEventListener(`install`,()=>{self.skipWaiting()}),self.addEventListener(`activate`,e=>{e.waitUntil((async()=>{await self.clients.claim(),V.replayRequests().catch(()=>{})})())}),self.addEventListener(`fetch`,e=>{let{request:t}=e;if(t.method!==`POST`)return;let n;try{n=new URL(t.url)}catch{return}n.pathname.startsWith(`/api/interfere/`)&&e.respondWith((async()=>{try{let e=await fetch(t.clone());return e.ok?e:e.status>=500||e.status===429?(await B(V,t),new Response(null,I)):e}catch{return await B(V,t),new Response(null,I)}})())})})();";
|
package/dist/tracking/api.d.mts
CHANGED
|
@@ -1,21 +1,47 @@
|
|
|
1
|
-
import { IngestTarget } from "../
|
|
1
|
+
import { IngestTarget } from "../internal/config.mjs";
|
|
2
|
+
import { DeviceManager } from "./device.mjs";
|
|
3
|
+
import { GeoDetector } from "./geo.mjs";
|
|
4
|
+
import { SessionId } from "@interfere/types/data/session";
|
|
2
5
|
import { IdentifyParams } from "@interfere/types/sdk/identify";
|
|
3
6
|
|
|
4
7
|
//#region src/tracking/api.d.ts
|
|
5
|
-
|
|
6
|
-
|
|
8
|
+
interface SessionTrackerOptions {
|
|
9
|
+
device?: DeviceManager;
|
|
10
|
+
fetcher?: typeof globalThis.fetch;
|
|
11
|
+
geo?: GeoDetector;
|
|
12
|
+
target: IngestTarget;
|
|
13
|
+
}
|
|
14
|
+
declare class SessionTracker {
|
|
15
|
+
private readonly target;
|
|
16
|
+
private readonly fetcher;
|
|
17
|
+
private readonly device;
|
|
18
|
+
private readonly geo;
|
|
19
|
+
private mgr;
|
|
20
|
+
private currentIdentity;
|
|
21
|
+
private identifiedSessionId;
|
|
22
|
+
private syncedSessionId;
|
|
23
|
+
private syncAttemptMs;
|
|
24
|
+
private generation;
|
|
25
|
+
constructor(opts: SessionTrackerOptions);
|
|
26
|
+
start(): void;
|
|
27
|
+
sessionId(): SessionId | null;
|
|
28
|
+
windowId(): string | null;
|
|
7
29
|
getDeviceId(): string | null;
|
|
8
30
|
getFpHash(): string | null;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Identity headers (session / window / device). Layered onto the
|
|
33
|
+
* session/identify POSTs alongside the target's static headers
|
|
34
|
+
* (content-type + auth + force-enable).
|
|
35
|
+
*/
|
|
36
|
+
headers(): Record<string, string>;
|
|
37
|
+
getIdentity(): IdentifyParams | null;
|
|
38
|
+
identify(params: IdentifyParams): Promise<void>;
|
|
39
|
+
clearIdentity(): void;
|
|
40
|
+
dispose(): void;
|
|
41
|
+
private requestHeaders;
|
|
42
|
+
private ensureSynced;
|
|
43
|
+
private syncSession;
|
|
44
|
+
private onRotate;
|
|
45
|
+
}
|
|
20
46
|
//#endregion
|
|
21
|
-
export {
|
|
47
|
+
export { SessionTracker, SessionTrackerOptions };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.mts","names":[],"sources":["../../src/tracking/api.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"api.d.mts","names":[],"sources":["../../src/tracking/api.ts"],"mappings":";;;;;;;UAgBiB,qBAAA;EACf,MAAA,GAAS,aAAA;EACT,OAAA,UAAiB,UAAA,CAAW,KAAA;EAC5B,GAAA,GAAM,WAAA;EACN,MAAA,EAAQ,YAAA;AAAA;AAAA,cAGG,cAAA;EAAA,iBACM,MAAA;EAAA,iBACA,OAAA;EAAA,iBACA,MAAA;EAAA,iBACA,GAAA;EAAA,QAET,GAAA;EAAA,QACA,eAAA;EAAA,QACA,mBAAA;EAAA,QACA,eAAA;EAAA,QACA,aAAA;EAAA,QACA,UAAA;cAEI,IAAA,EAAM,qBAAA;EAOlB,KAAA,CAAA;EAaA,SAAA,CAAA,GAAa,SAAA;EAQb,QAAA,CAAA;EAIA,WAAA,CAAA;EAIA,SAAA,CAAA;EAjDyB;;;;;EA0DzB,OAAA,CAAA,GAAW,MAAA;EAiBX,WAAA,CAAA,GAAe,cAAA;EAIT,QAAA,CAAS,MAAA,EAAQ,cAAA,GAAiB,OAAA;EAkDxC,aAAA,CAAA;EAKA,OAAA,CAAA;EAAA,QAQQ,cAAA;EAAA,QAOA,YAAA;EAAA,QAcA,WAAA;EAAA,QA0BM,QAAA;AAAA"}
|
package/dist/tracking/api.mjs
CHANGED
|
@@ -1,134 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { getDeviceId, getFpHash, initDevice, whenDeviceReady, whenFingerprintReady } from "./device.mjs";
|
|
3
|
-
import { buildHeaders } from "../transport/http.mjs";
|
|
4
|
-
import { detectCountryCode, resetGeo } from "./geo.mjs";
|
|
5
|
-
import { SessionManager } from "./session.mjs";
|
|
6
|
-
//#region src/tracking/api.ts
|
|
7
|
-
const log = createLogger("tracking");
|
|
8
|
-
let mgr = null;
|
|
9
|
-
let target = null;
|
|
10
|
-
let currentIdentity = null;
|
|
11
|
-
let identifiedSessionId = null;
|
|
12
|
-
let syncedSessionId = null;
|
|
13
|
-
let syncAttemptMs = 0;
|
|
14
|
-
let generation = 0;
|
|
15
|
-
const SYNC_COOLDOWN_MS = 5e3;
|
|
16
|
-
function syncSession(sessionId, deviceId, fpHash) {
|
|
17
|
-
if (!target) return;
|
|
18
|
-
syncAttemptMs = Date.now();
|
|
19
|
-
syncedSessionId = sessionId;
|
|
20
|
-
fetch(target.url, {
|
|
21
|
-
method: "POST",
|
|
22
|
-
headers: buildHeaders(target.headers),
|
|
23
|
-
body: JSON.stringify({
|
|
24
|
-
sessionId,
|
|
25
|
-
deviceId,
|
|
26
|
-
fpHash
|
|
27
|
-
}),
|
|
28
|
-
keepalive: true,
|
|
29
|
-
signal: AbortSignal.timeout(1e4)
|
|
30
|
-
}).then((res) => {
|
|
31
|
-
if (!res.ok) syncedSessionId = null;
|
|
32
|
-
}).catch(() => {
|
|
33
|
-
syncedSessionId = null;
|
|
34
|
-
log.warn("session sync failed, will retry");
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
function ensureSynced(sessionId) {
|
|
38
|
-
if (syncedSessionId === sessionId) return;
|
|
39
|
-
if (Date.now() - syncAttemptMs < SYNC_COOLDOWN_MS) return;
|
|
40
|
-
syncSession(sessionId, getDeviceId(), getFpHash());
|
|
41
|
-
}
|
|
42
|
-
async function onRotate(sessionId) {
|
|
43
|
-
syncedSessionId = null;
|
|
44
|
-
syncAttemptMs = Date.now();
|
|
45
|
-
if (!target) return;
|
|
46
|
-
const gen = generation;
|
|
47
|
-
const [deviceId, fpHash] = await Promise.all([whenDeviceReady(), whenFingerprintReady()]);
|
|
48
|
-
if (gen !== generation) return;
|
|
49
|
-
log.debug("POST session %s (device=%s fp=%s)", sessionId, deviceId ?? "pending", fpHash ?? "none");
|
|
50
|
-
syncSession(sessionId, deviceId, fpHash);
|
|
51
|
-
}
|
|
52
|
-
function bootstrap(sessionTarget) {
|
|
53
|
-
target = sessionTarget;
|
|
54
|
-
initDevice();
|
|
55
|
-
detectCountryCode();
|
|
56
|
-
mgr = new SessionManager((id) => {
|
|
57
|
-
onRotate(id).catch(() => {});
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
const device = {
|
|
61
|
-
getDeviceId() {
|
|
62
|
-
return getDeviceId();
|
|
63
|
-
},
|
|
64
|
-
getFpHash() {
|
|
65
|
-
return getFpHash();
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
const session = {
|
|
69
|
-
getId() {
|
|
70
|
-
const id = mgr?.getSessionId() ?? null;
|
|
71
|
-
if (id) ensureSynced(id);
|
|
72
|
-
return id;
|
|
73
|
-
},
|
|
74
|
-
getWindowId() {
|
|
75
|
-
return mgr?.getWindowId() ?? null;
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
const identity = {
|
|
79
|
-
get() {
|
|
80
|
-
return currentIdentity;
|
|
81
|
-
},
|
|
82
|
-
async set(params) {
|
|
83
|
-
if (!(mgr && target)) return;
|
|
84
|
-
const sessionId = mgr.getSessionId();
|
|
85
|
-
if (identifiedSessionId === sessionId) {
|
|
86
|
-
log.debug("skipped, already identified for session %s", sessionId);
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
currentIdentity = params;
|
|
90
|
-
identifiedSessionId = sessionId;
|
|
91
|
-
const gen = generation;
|
|
92
|
-
const [deviceId, fpHash, country] = await Promise.all([
|
|
93
|
-
whenDeviceReady(),
|
|
94
|
-
whenFingerprintReady(),
|
|
95
|
-
detectCountryCode()
|
|
96
|
-
]);
|
|
97
|
-
if (gen !== generation || !target) {
|
|
98
|
-
identifiedSessionId = null;
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
log.info("PUT session %s → user %s", sessionId, params.identifier);
|
|
102
|
-
fetch(target.url, {
|
|
103
|
-
method: "PUT",
|
|
104
|
-
headers: buildHeaders(target.headers),
|
|
105
|
-
body: JSON.stringify({
|
|
106
|
-
sessionId,
|
|
107
|
-
deviceId,
|
|
108
|
-
fpHash,
|
|
109
|
-
...params,
|
|
110
|
-
...country && { country }
|
|
111
|
-
}),
|
|
112
|
-
keepalive: true,
|
|
113
|
-
signal: AbortSignal.timeout(1e4)
|
|
114
|
-
}).catch(() => {
|
|
115
|
-
identifiedSessionId = null;
|
|
116
|
-
log.warn("identify failed for session %s", sessionId);
|
|
117
|
-
});
|
|
118
|
-
},
|
|
119
|
-
clear() {
|
|
120
|
-
currentIdentity = null;
|
|
121
|
-
identifiedSessionId = null;
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
function teardown() {
|
|
125
|
-
generation += 1;
|
|
126
|
-
identity.clear();
|
|
127
|
-
resetGeo();
|
|
128
|
-
syncedSessionId = null;
|
|
129
|
-
syncAttemptMs = 0;
|
|
130
|
-
mgr = null;
|
|
131
|
-
target = null;
|
|
132
|
-
}
|
|
133
|
-
//#endregion
|
|
134
|
-
export { bootstrap, device, identity, session, teardown };
|
|
1
|
+
import{createLogger}from"../util/log.mjs";import{appendPathBeforeQuery}from"../internal/config.mjs";import{DeviceManager}from"./device.mjs";import{GeoDetector}from"./geo.mjs";import{SessionManager}from"./session.mjs";const log=createLogger(`tracking`);var SessionTracker=class{target;fetcher;device;geo;mgr=null;currentIdentity=null;identifiedSessionId=null;syncedSessionId=null;syncAttemptMs=0;generation=0;constructor(opts){this.target=opts.target,this.fetcher=opts.fetcher??globalThis.fetch.bind(globalThis),this.device=opts.device??new DeviceManager,this.geo=opts.geo??new GeoDetector}start(){this.device.init(),this.geo.detect(),this.mgr=new SessionManager(id=>{this.onRotate(id).catch(()=>{})})}sessionId(){let id=this.mgr?.getSessionId()??null;return id&&this.ensureSynced(id),id}windowId(){return this.mgr?.getWindowId()??null}getDeviceId(){return this.device.getDeviceId()}getFpHash(){return this.device.getFpHash()}headers(){let h={},sid=this.mgr?.getSessionId()??null;sid&&(h[`x-interfere-session`]=sid);let wid=this.mgr?.getWindowId()??null;wid&&(h[`x-interfere-window`]=wid);let did=this.device.getDeviceId();return did&&(h[`x-interfere-device`]=did),h}getIdentity(){return this.currentIdentity}async identify(params){if(!this.mgr)return;let sessionId=this.mgr.getSessionId();if(this.identifiedSessionId===sessionId){log.debug(`skipped, already identified for session %s`,sessionId);return}this.currentIdentity=params,this.identifiedSessionId=sessionId;let gen=this.generation,[fpHash,country]=await Promise.all([this.device.whenFingerprintReady(),this.geo.detect()]),deviceId=this.device.getDeviceId();if(gen!==this.generation){this.identifiedSessionId=null;return}log.info(`POST session %s → user %s`,sessionId,params.identifier),this.fetcher(appendPathBeforeQuery(this.target.url,`/identify`),{method:`POST`,headers:this.requestHeaders(),body:JSON.stringify({sessionId,deviceId,fpHash,...params,...country&&{country}}),keepalive:!0,signal:AbortSignal.timeout(1e4)}).catch(()=>{this.identifiedSessionId=null,log.warn(`identify failed for session %s`,sessionId)})}clearIdentity(){this.currentIdentity=null,this.identifiedSessionId=null}dispose(){this.generation+=1,this.clearIdentity(),this.syncedSessionId=null,this.syncAttemptMs=0,this.mgr=null}requestHeaders(){return{...Object.fromEntries(this.target.headers.entries()),...this.headers()}}ensureSynced(sessionId){this.syncedSessionId!==sessionId&&(Date.now()-this.syncAttemptMs<5e3||this.syncSession(sessionId,this.device.getDeviceId(),this.device.getFpHash()))}syncSession(sessionId,deviceId,fpHash){this.syncAttemptMs=Date.now(),this.syncedSessionId=sessionId,this.fetcher(this.target.url,{method:`POST`,headers:this.requestHeaders(),body:JSON.stringify({sessionId,deviceId,fpHash}),keepalive:!0,signal:AbortSignal.timeout(1e4)}).then(res=>{res.ok||(this.syncedSessionId=null)}).catch(()=>{this.syncedSessionId=null,log.warn(`session sync failed, will retry`)})}async onRotate(sessionId){this.syncedSessionId=null,this.syncAttemptMs=Date.now();let gen=this.generation,fpHash=await this.device.whenFingerprintReady(),deviceId=this.device.getDeviceId();gen===this.generation&&(log.debug(`POST session %s (device=%s fp=%s)`,sessionId,deviceId??`pending`,fpHash??`none`),this.syncSession(sessionId,deviceId,fpHash))}};export{SessionTracker};
|