@interfere/react 10.0.0 → 10.0.1-canary.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.mts.map +1 -1
- package/dist/api.mjs +1 -68
- package/dist/api.mjs.map +1 -1
- package/dist/error-boundary.d.mts +1 -2
- package/dist/error-boundary.d.mts.map +1 -1
- package/dist/error-boundary.mjs +1 -42
- package/dist/error-boundary.mjs.map +1 -1
- package/dist/internal/browser-context.d.mts.map +1 -1
- package/dist/internal/browser-context.mjs +1 -59
- package/dist/internal/browser-context.mjs.map +1 -1
- package/dist/internal/capture-boundary.d.mts +1 -2
- package/dist/internal/capture-boundary.d.mts.map +1 -1
- package/dist/internal/capture-boundary.mjs +1 -48
- package/dist/internal/capture-boundary.mjs.map +1 -1
- package/dist/internal/capture.mjs +1 -27
- package/dist/internal/capture.mjs.map +1 -1
- package/dist/internal/config.d.mts +3 -1
- 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 -27
- package/dist/internal/consent.mjs.map +1 -1
- package/dist/internal/console-patch.d.mts.map +1 -1
- package/dist/internal/console-patch.mjs +1 -62
- package/dist/internal/console-patch.mjs.map +1 -1
- package/dist/internal/dom/actionable.d.mts.map +1 -1
- package/dist/internal/dom/actionable.mjs +1 -62
- package/dist/internal/dom/actionable.mjs.map +1 -1
- package/dist/internal/kernel-registry.d.mts.map +1 -1
- package/dist/internal/kernel-registry.mjs +1 -31
- package/dist/internal/kernel-registry.mjs.map +1 -1
- package/dist/internal/kernel.d.mts +1 -1
- package/dist/internal/kernel.d.mts.map +1 -1
- package/dist/internal/kernel.mjs +1 -322
- package/dist/internal/kernel.mjs.map +1 -1
- package/dist/internal/otel/exporter.d.mts +4 -12
- package/dist/internal/otel/exporter.d.mts.map +1 -1
- package/dist/internal/otel/exporter.mjs +1 -212
- package/dist/internal/otel/exporter.mjs.map +1 -1
- package/dist/internal/otel/index.mjs +1 -6
- package/dist/internal/otel/instrumentations.d.mts.map +1 -1
- package/dist/internal/otel/instrumentations.mjs +1 -150
- package/dist/internal/otel/instrumentations.mjs.map +1 -1
- package/dist/internal/otel/page-scope-context-manager.d.mts.map +1 -1
- package/dist/internal/otel/page-scope-context-manager.mjs +1 -36
- package/dist/internal/otel/page-scope-context-manager.mjs.map +1 -1
- package/dist/internal/otel/propagation.d.mts.map +1 -1
- package/dist/internal/otel/propagation.mjs +1 -40
- package/dist/internal/otel/propagation.mjs.map +1 -1
- package/dist/internal/otel/provider.d.mts +1 -2
- package/dist/internal/otel/provider.d.mts.map +1 -1
- package/dist/internal/otel/provider.mjs +1 -151
- package/dist/internal/otel/provider.mjs.map +1 -1
- package/dist/internal/otel/web-vitals.d.mts.map +1 -1
- package/dist/internal/otel/web-vitals.mjs +1 -162
- package/dist/internal/otel/web-vitals.mjs.map +1 -1
- package/dist/internal/page-lifecycle.d.mts.map +1 -1
- package/dist/internal/page-lifecycle.mjs +1 -33
- package/dist/internal/page-lifecycle.mjs.map +1 -1
- package/dist/internal/plugin-runtime.mjs +1 -101
- package/dist/internal/plugin-runtime.mjs.map +1 -1
- package/dist/internal/react-context.d.mts +1 -2
- package/dist/internal/react-context.d.mts.map +1 -1
- package/dist/internal/react-context.mjs +1 -34
- package/dist/internal/react-context.mjs.map +1 -1
- package/dist/internal/sw.d.mts.map +1 -1
- package/dist/internal/sw.mjs +1 -37
- package/dist/internal/sw.mjs.map +1 -1
- package/dist/internal/version.mjs +1 -7
- package/dist/internal/version.mjs.map +1 -1
- package/dist/internal/wrapper-singleton.d.mts.map +1 -1
- package/dist/internal/wrapper-singleton.mjs +1 -73
- package/dist/internal/wrapper-singleton.mjs.map +1 -1
- package/dist/package.mjs +1 -5
- package/dist/plugins/errors.d.mts.map +1 -1
- package/dist/plugins/errors.mjs +1 -84
- package/dist/plugins/errors.mjs.map +1 -1
- package/dist/plugins/lib/loader.mjs +1 -34
- package/dist/plugins/lib/loader.mjs.map +1 -1
- package/dist/plugins/lib/types.d.mts.map +1 -1
- package/dist/plugins/lib/types.mjs +1 -1
- package/dist/plugins/logs.d.mts.map +1 -1
- package/dist/plugins/logs.mjs +1 -53
- package/dist/plugins/logs.mjs.map +1 -1
- package/dist/plugins/rage-clicks.d.mts.map +1 -1
- package/dist/plugins/rage-clicks.mjs +1 -55
- package/dist/plugins/rage-clicks.mjs.map +1 -1
- package/dist/plugins/replay.d.mts.map +1 -1
- package/dist/plugins/replay.mjs +1 -101
- package/dist/plugins/replay.mjs.map +1 -1
- package/dist/provider.d.mts.map +1 -1
- package/dist/provider.mjs +1 -31
- package/dist/provider.mjs.map +1 -1
- package/dist/react-error-handler.d.mts.map +1 -1
- package/dist/react-error-handler.mjs +1 -62
- package/dist/react-error-handler.mjs.map +1 -1
- package/dist/tracking/api.d.mts.map +1 -1
- package/dist/tracking/api.mjs +1 -152
- package/dist/tracking/api.mjs.map +1 -1
- package/dist/tracking/device.d.mts.map +1 -1
- package/dist/tracking/device.mjs +1 -104
- package/dist/tracking/device.mjs.map +1 -1
- package/dist/tracking/geo.d.mts.map +1 -1
- package/dist/tracking/geo.mjs +2 -48
- package/dist/tracking/geo.mjs.map +1 -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.map +1 -1
- package/dist/util/bot.mjs +1 -14
- package/dist/util/bot.mjs.map +1 -1
- package/dist/util/global.d.mts.map +1 -1
- package/dist/util/global.mjs +1 -12
- package/dist/util/global.mjs.map +1 -1
- package/dist/util/log.d.mts.map +1 -1
- package/dist/util/log.mjs +1 -44
- package/dist/util/log.mjs.map +1 -1
- package/dist/util/stringify.d.mts.map +1 -1
- package/dist/util/stringify.mjs +1 -16
- package/dist/util/stringify.mjs.map +1 -1
- package/package.json +34 -33
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replay.mjs","names":[],"sources":["../../src/plugins/replay.ts"],"sourcesContent":["import { trace } from \"@opentelemetry/api\";\n\nimport { 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\";\
|
|
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.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.mts","names":[],"sources":["../src/provider.tsx"],"mappings":";;;;;;UAmBU,sBAAA,SAA+B,iBAAA;EACvC,OAAA,GAAU,YAAA;EADF
|
|
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,31 +1 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { InterfereContext, NULL_CONTEXT_VALUE } from "./internal/react-context.mjs";
|
|
3
|
-
import { CaptureBoundary } from "./internal/capture-boundary.mjs";
|
|
4
|
-
import { useContext, useEffect } from "react";
|
|
5
|
-
import { jsx } from "react/jsx-runtime";
|
|
6
|
-
//#region src/provider.tsx
|
|
7
|
-
function InterfereProvider({ kernel, children, consent, errorBoundary = true }) {
|
|
8
|
-
useEffect(() => {
|
|
9
|
-
kernel?.syncConsent(consent);
|
|
10
|
-
}, [kernel, consent]);
|
|
11
|
-
return /* @__PURE__ */ jsx(InterfereContext, {
|
|
12
|
-
value: kernel ? {
|
|
13
|
-
consent: kernel.consent,
|
|
14
|
-
device: kernel.device,
|
|
15
|
-
identity: kernel.identity,
|
|
16
|
-
kernel,
|
|
17
|
-
session: kernel.session
|
|
18
|
-
} : NULL_CONTEXT_VALUE,
|
|
19
|
-
children: errorBoundary ? /* @__PURE__ */ jsx(CaptureBoundary, { children }) : children
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
function useInterfere() {
|
|
23
|
-
const ctx = useContext(InterfereContext);
|
|
24
|
-
if (!ctx) throw new Error("useInterfere must be used within <InterfereProvider>");
|
|
25
|
-
return ctx;
|
|
26
|
-
}
|
|
27
|
-
function useSession() {
|
|
28
|
-
return useInterfere().session.getId();
|
|
29
|
-
}
|
|
30
|
-
//#endregion
|
|
31
|
-
export { InterfereContext, InterfereProvider, useInterfere, useSession };
|
|
1
|
+
"use client";import{InterfereContext,NULL_CONTEXT_VALUE}from"./internal/react-context.mjs";import{CaptureBoundary}from"./internal/capture-boundary.mjs";import{useContext,useEffect}from"react";import{jsx}from"react/jsx-runtime";function InterfereProvider({kernel,children,consent,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":[],"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":"
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react-error-handler.d.mts","names":[],"sources":["../src/react-error-handler.ts"],"mappings":";;;;;AAKmD
|
|
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,62 +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
|
-
function resolveKernel(source) {
|
|
6
|
-
if (!source) return null;
|
|
7
|
-
return typeof source === "function" ? source() : source;
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Creates the three error callbacks React 19's `createRoot()` / `hydrateRoot()`
|
|
11
|
-
* accept, wired into Interfere's capture pipeline.
|
|
12
|
-
*
|
|
13
|
-
* ```ts
|
|
14
|
-
* import { createRoot } from "react-dom/client";
|
|
15
|
-
* import { getKernelOrNull } from "@interfere/next";
|
|
16
|
-
* import { reactErrorHandler } from "@interfere/react/react-error-handler";
|
|
17
|
-
*
|
|
18
|
-
* createRoot(
|
|
19
|
-
* document.getElementById("root")!,
|
|
20
|
-
* reactErrorHandler({ kernel: getKernelOrNull })
|
|
21
|
-
* ).render(<App />);
|
|
22
|
-
* ```
|
|
23
|
-
*
|
|
24
|
-
* Pass your own callbacks to observe errors without replacing the capture
|
|
25
|
-
* behaviour — user callbacks run after Interfere has captured:
|
|
26
|
-
*
|
|
27
|
-
* ```ts
|
|
28
|
-
* reactErrorHandler({
|
|
29
|
-
* kernel: getKernelOrNull,
|
|
30
|
-
* onUncaughtError: (err) => myLogger.fatal(err),
|
|
31
|
-
* });
|
|
32
|
-
* ```
|
|
33
|
-
*/
|
|
34
|
-
function reactErrorHandler(options = {}) {
|
|
35
|
-
const { kernel: kernelSource, ...callbacks } = options;
|
|
36
|
-
return {
|
|
37
|
-
onCaughtError(error, info) {
|
|
38
|
-
if (error instanceof Error) captureReactError(resolveKernel(kernelSource), error, info.componentStack, {
|
|
39
|
-
type: MECHANISM_TYPE.react.caughtError,
|
|
40
|
-
handled: true
|
|
41
|
-
});
|
|
42
|
-
callbacks.onCaughtError?.(error, info);
|
|
43
|
-
},
|
|
44
|
-
onUncaughtError(error, info) {
|
|
45
|
-
if (error instanceof Error) captureReactError(resolveKernel(kernelSource), error, info.componentStack, {
|
|
46
|
-
type: MECHANISM_TYPE.react.uncaughtError,
|
|
47
|
-
handled: false
|
|
48
|
-
});
|
|
49
|
-
callbacks.onUncaughtError?.(error, info);
|
|
50
|
-
},
|
|
51
|
-
onRecoverableError(error, info) {
|
|
52
|
-
if (error instanceof Error) captureReactError(resolveKernel(kernelSource), error, info.componentStack, {
|
|
53
|
-
type: MECHANISM_TYPE.react.recoverableError,
|
|
54
|
-
handled: true,
|
|
55
|
-
synthetic: true
|
|
56
|
-
});
|
|
57
|
-
callbacks.onRecoverableError?.(error, info);
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
//#endregion
|
|
62
|
-
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\";\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":"
|
|
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"}
|
|
@@ -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,152 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { DeviceManager } from "./device.mjs";
|
|
3
|
-
import { GeoDetector } from "./geo.mjs";
|
|
4
|
-
import { SessionManager } from "./session.mjs";
|
|
5
|
-
//#region src/tracking/api.ts
|
|
6
|
-
const log = createLogger("tracking");
|
|
7
|
-
const SYNC_COOLDOWN_MS = 5e3;
|
|
8
|
-
var SessionTracker = class {
|
|
9
|
-
target;
|
|
10
|
-
fetcher;
|
|
11
|
-
device;
|
|
12
|
-
geo;
|
|
13
|
-
mgr = null;
|
|
14
|
-
currentIdentity = null;
|
|
15
|
-
identifiedSessionId = null;
|
|
16
|
-
syncedSessionId = null;
|
|
17
|
-
syncAttemptMs = 0;
|
|
18
|
-
generation = 0;
|
|
19
|
-
constructor(opts) {
|
|
20
|
-
this.target = opts.target;
|
|
21
|
-
this.fetcher = opts.fetcher ?? globalThis.fetch.bind(globalThis);
|
|
22
|
-
this.device = opts.device ?? new DeviceManager();
|
|
23
|
-
this.geo = opts.geo ?? new GeoDetector();
|
|
24
|
-
}
|
|
25
|
-
start() {
|
|
26
|
-
this.device.init();
|
|
27
|
-
this.geo.detect();
|
|
28
|
-
this.mgr = new SessionManager((id) => {
|
|
29
|
-
this.onRotate(id).catch(() => {});
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
sessionId() {
|
|
33
|
-
const id = this.mgr?.getSessionId() ?? null;
|
|
34
|
-
if (id) this.ensureSynced(id);
|
|
35
|
-
return id;
|
|
36
|
-
}
|
|
37
|
-
windowId() {
|
|
38
|
-
return this.mgr?.getWindowId() ?? null;
|
|
39
|
-
}
|
|
40
|
-
getDeviceId() {
|
|
41
|
-
return this.device.getDeviceId();
|
|
42
|
-
}
|
|
43
|
-
getFpHash() {
|
|
44
|
-
return this.device.getFpHash();
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Identity headers (session / window / device). Layered onto the
|
|
48
|
-
* session/identify POSTs alongside the target's static headers
|
|
49
|
-
* (content-type + auth + force-enable).
|
|
50
|
-
*/
|
|
51
|
-
headers() {
|
|
52
|
-
const h = {};
|
|
53
|
-
const sid = this.mgr?.getSessionId() ?? null;
|
|
54
|
-
if (sid) h["x-interfere-session"] = sid;
|
|
55
|
-
const wid = this.mgr?.getWindowId() ?? null;
|
|
56
|
-
if (wid) h["x-interfere-window"] = wid;
|
|
57
|
-
const did = this.device.getDeviceId();
|
|
58
|
-
if (did) h["x-interfere-device"] = did;
|
|
59
|
-
return h;
|
|
60
|
-
}
|
|
61
|
-
getIdentity() {
|
|
62
|
-
return this.currentIdentity;
|
|
63
|
-
}
|
|
64
|
-
async identify(params) {
|
|
65
|
-
if (!this.mgr) return;
|
|
66
|
-
const sessionId = this.mgr.getSessionId();
|
|
67
|
-
if (this.identifiedSessionId === sessionId) {
|
|
68
|
-
log.debug("skipped, already identified for session %s", sessionId);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
this.currentIdentity = params;
|
|
72
|
-
this.identifiedSessionId = sessionId;
|
|
73
|
-
const gen = this.generation;
|
|
74
|
-
const [fpHash, country] = await Promise.all([this.device.whenFingerprintReady(), this.geo.detect()]);
|
|
75
|
-
const deviceId = this.device.getDeviceId();
|
|
76
|
-
if (gen !== this.generation) {
|
|
77
|
-
this.identifiedSessionId = null;
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
log.info("POST session %s → user %s", sessionId, params.identifier);
|
|
81
|
-
this.fetcher(`${this.target.url}/identify`, {
|
|
82
|
-
method: "POST",
|
|
83
|
-
headers: this.requestHeaders(),
|
|
84
|
-
body: JSON.stringify({
|
|
85
|
-
sessionId,
|
|
86
|
-
deviceId,
|
|
87
|
-
fpHash,
|
|
88
|
-
...params,
|
|
89
|
-
...country && { country }
|
|
90
|
-
}),
|
|
91
|
-
keepalive: true,
|
|
92
|
-
signal: AbortSignal.timeout(1e4)
|
|
93
|
-
}).catch(() => {
|
|
94
|
-
this.identifiedSessionId = null;
|
|
95
|
-
log.warn("identify failed for session %s", sessionId);
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
clearIdentity() {
|
|
99
|
-
this.currentIdentity = null;
|
|
100
|
-
this.identifiedSessionId = null;
|
|
101
|
-
}
|
|
102
|
-
dispose() {
|
|
103
|
-
this.generation += 1;
|
|
104
|
-
this.clearIdentity();
|
|
105
|
-
this.syncedSessionId = null;
|
|
106
|
-
this.syncAttemptMs = 0;
|
|
107
|
-
this.mgr = null;
|
|
108
|
-
}
|
|
109
|
-
requestHeaders() {
|
|
110
|
-
return {
|
|
111
|
-
...Object.fromEntries(this.target.headers.entries()),
|
|
112
|
-
...this.headers()
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
ensureSynced(sessionId) {
|
|
116
|
-
if (this.syncedSessionId === sessionId) return;
|
|
117
|
-
if (Date.now() - this.syncAttemptMs < SYNC_COOLDOWN_MS) return;
|
|
118
|
-
this.syncSession(sessionId, this.device.getDeviceId(), this.device.getFpHash());
|
|
119
|
-
}
|
|
120
|
-
syncSession(sessionId, deviceId, fpHash) {
|
|
121
|
-
this.syncAttemptMs = Date.now();
|
|
122
|
-
this.syncedSessionId = sessionId;
|
|
123
|
-
this.fetcher(this.target.url, {
|
|
124
|
-
method: "POST",
|
|
125
|
-
headers: this.requestHeaders(),
|
|
126
|
-
body: JSON.stringify({
|
|
127
|
-
sessionId,
|
|
128
|
-
deviceId,
|
|
129
|
-
fpHash
|
|
130
|
-
}),
|
|
131
|
-
keepalive: true,
|
|
132
|
-
signal: AbortSignal.timeout(1e4)
|
|
133
|
-
}).then((res) => {
|
|
134
|
-
if (!res.ok) this.syncedSessionId = null;
|
|
135
|
-
}).catch(() => {
|
|
136
|
-
this.syncedSessionId = null;
|
|
137
|
-
log.warn("session sync failed, will retry");
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
async onRotate(sessionId) {
|
|
141
|
-
this.syncedSessionId = null;
|
|
142
|
-
this.syncAttemptMs = Date.now();
|
|
143
|
-
const gen = this.generation;
|
|
144
|
-
const fpHash = await this.device.whenFingerprintReady();
|
|
145
|
-
const deviceId = this.device.getDeviceId();
|
|
146
|
-
if (gen !== this.generation) return;
|
|
147
|
-
log.debug("POST session %s (device=%s fp=%s)", sessionId, deviceId ?? "pending", fpHash ?? "none");
|
|
148
|
-
this.syncSession(sessionId, deviceId, fpHash);
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
//#endregion
|
|
152
|
-
export { SessionTracker };
|
|
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};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.mjs","names":[],"sources":["../../src/tracking/api.ts"],"sourcesContent":["import type { SessionId } from \"@interfere/types/data/session\";\nimport type { IdentifyParams } from \"@interfere/types/sdk/identify\";\n\nimport type
|
|
1
|
+
{"version":3,"file":"api.mjs","names":[],"sources":["../../src/tracking/api.ts"],"sourcesContent":["import type { SessionId } from \"@interfere/types/data/session\";\nimport type { IdentifyParams } from \"@interfere/types/sdk/identify\";\n\nimport {\n appendPathBeforeQuery,\n type IngestTarget,\n} from \"../internal/config.js\";\nimport { createLogger } from \"../util/log.js\";\nimport { DeviceManager } from \"./device.js\";\nimport { GeoDetector } from \"./geo.js\";\nimport { SessionManager } from \"./session.js\";\n\nconst log = createLogger(\"tracking\");\n\nconst SYNC_COOLDOWN_MS = 5000;\n\nexport interface SessionTrackerOptions {\n device?: DeviceManager;\n fetcher?: typeof globalThis.fetch;\n geo?: GeoDetector;\n target: IngestTarget;\n}\n\nexport class SessionTracker {\n private readonly target: IngestTarget;\n private readonly fetcher: typeof globalThis.fetch;\n private readonly device: DeviceManager;\n private readonly geo: GeoDetector;\n\n private mgr: SessionManager | null = null;\n private currentIdentity: IdentifyParams | null = null;\n private identifiedSessionId: string | null = null;\n private syncedSessionId: string | null = null;\n private syncAttemptMs = 0;\n private generation = 0;\n\n constructor(opts: SessionTrackerOptions) {\n this.target = opts.target;\n this.fetcher = opts.fetcher ?? globalThis.fetch.bind(globalThis);\n this.device = opts.device ?? new DeviceManager();\n this.geo = opts.geo ?? new GeoDetector();\n }\n\n start(): void {\n this.device.init();\n // Best-effort warm-up; `GeoDetector.detect()` swallows its own errors and\n // resolves to `null`, so we don't need to attach a `.catch`. If `identify`\n // runs before the geo result lands, the country field is just omitted.\n this.geo.detect();\n this.mgr = new SessionManager((id) => {\n this.onRotate(id).catch(() => {\n /* best-effort */\n });\n });\n }\n\n sessionId(): SessionId | null {\n const id = this.mgr?.getSessionId() ?? null;\n if (id) {\n this.ensureSynced(id);\n }\n return id;\n }\n\n windowId(): string | null {\n return this.mgr?.getWindowId() ?? null;\n }\n\n getDeviceId(): string | null {\n return this.device.getDeviceId();\n }\n\n getFpHash(): string | null {\n return this.device.getFpHash();\n }\n\n /**\n * Identity headers (session / window / device). Layered onto the\n * session/identify POSTs alongside the target's static headers\n * (content-type + auth + force-enable).\n */\n headers(): Record<string, string> {\n const h: Record<string, string> = {};\n const sid = this.mgr?.getSessionId() ?? null;\n if (sid) {\n h[\"x-interfere-session\"] = sid;\n }\n const wid = this.mgr?.getWindowId() ?? null;\n if (wid) {\n h[\"x-interfere-window\"] = wid;\n }\n const did = this.device.getDeviceId();\n if (did) {\n h[\"x-interfere-device\"] = did;\n }\n return h;\n }\n\n getIdentity(): IdentifyParams | null {\n return this.currentIdentity;\n }\n\n async identify(params: IdentifyParams): Promise<void> {\n if (!this.mgr) {\n return;\n }\n const sessionId = this.mgr.getSessionId();\n if (this.identifiedSessionId === sessionId) {\n log.debug(\"skipped, already identified for session %s\", sessionId);\n return;\n }\n\n this.currentIdentity = params;\n this.identifiedSessionId = sessionId;\n\n const gen = this.generation;\n const [fpHash, country] = await Promise.all([\n this.device.whenFingerprintReady(),\n this.geo.detect(),\n ]);\n const deviceId = this.device.getDeviceId();\n if (gen !== this.generation) {\n this.identifiedSessionId = null;\n return;\n }\n // Append the path to the existing target. `new URL(\"/identify\", base)`\n // is wrong on both axes: in proxy mode `target.url` is relative\n // (`/api/interfere/v1/session`) so the URL constructor throws\n // synchronously — `currentIdentity` is set but no fetch fires; in\n // direct mode the path-absolute \"/identify\" replaces the target's\n // path, hitting `/identify` at the origin instead of the collector's\n // `/v1/session/identify` route. The helper preserves relative URLs and\n // keeps `pk` query auth at the end.\n log.info(\"POST session %s → user %s\", sessionId, params.identifier);\n this.fetcher(appendPathBeforeQuery(this.target.url, \"/identify\"), {\n method: \"POST\",\n headers: this.requestHeaders(),\n body: JSON.stringify({\n sessionId,\n deviceId,\n fpHash,\n ...params,\n ...(country && { country }),\n }),\n keepalive: true,\n signal: AbortSignal.timeout(10_000),\n }).catch(() => {\n this.identifiedSessionId = null;\n log.warn(\"identify failed for session %s\", sessionId);\n });\n }\n\n clearIdentity(): void {\n this.currentIdentity = null;\n this.identifiedSessionId = null;\n }\n\n dispose(): void {\n this.generation += 1;\n this.clearIdentity();\n this.syncedSessionId = null;\n this.syncAttemptMs = 0;\n this.mgr = null;\n }\n\n private requestHeaders(): Record<string, string> {\n return {\n ...Object.fromEntries(this.target.headers.entries()),\n ...this.headers(),\n };\n }\n\n private ensureSynced(sessionId: string): void {\n if (this.syncedSessionId === sessionId) {\n return;\n }\n if (Date.now() - this.syncAttemptMs < SYNC_COOLDOWN_MS) {\n return;\n }\n this.syncSession(\n sessionId,\n this.device.getDeviceId(),\n this.device.getFpHash()\n );\n }\n\n private syncSession(\n sessionId: string,\n deviceId: string | null,\n fpHash: string | null\n ): void {\n this.syncAttemptMs = Date.now();\n this.syncedSessionId = sessionId;\n\n this.fetcher(this.target.url, {\n method: \"POST\",\n headers: this.requestHeaders(),\n body: JSON.stringify({ sessionId, deviceId, fpHash }),\n keepalive: true,\n signal: AbortSignal.timeout(10_000),\n })\n .then((res) => {\n if (!res.ok) {\n this.syncedSessionId = null;\n }\n })\n .catch(() => {\n this.syncedSessionId = null;\n log.warn(\"session sync failed, will retry\");\n });\n }\n\n private async onRotate(sessionId: string): Promise<void> {\n this.syncedSessionId = null;\n this.syncAttemptMs = Date.now();\n const gen = this.generation;\n const fpHash = await this.device.whenFingerprintReady();\n const deviceId = this.device.getDeviceId();\n if (gen !== this.generation) {\n return;\n }\n log.debug(\n \"POST session %s (device=%s fp=%s)\",\n sessionId,\n deviceId ?? \"pending\",\n fpHash ?? \"none\"\n );\n this.syncSession(sessionId, deviceId, fpHash);\n }\n}\n"],"mappings":"yNAYA,MAAM,IAAM,aAAa,UAAU,EAWnC,IAAa,eAAb,KAA4B,CAC1B,OACA,QACA,OACA,IAEA,IAAqC,KACrC,gBAAiD,KACjD,oBAA6C,KAC7C,gBAAyC,KACzC,cAAwB,EACxB,WAAqB,EAErB,YAAY,KAA6B,CACvC,KAAK,OAAS,KAAK,OACnB,KAAK,QAAU,KAAK,SAAW,WAAW,MAAM,KAAK,UAAU,EAC/D,KAAK,OAAS,KAAK,QAAU,IAAI,cACjC,KAAK,IAAM,KAAK,KAAO,IAAI,WAC7B,CAEA,OAAc,CACZ,KAAK,OAAO,KAAK,EAIjB,KAAK,IAAI,OAAO,EAChB,KAAK,IAAM,IAAI,eAAgB,IAAO,CACpC,KAAK,SAAS,EAAE,EAAE,UAAY,CAE9B,CAAC,CACH,CAAC,CACH,CAEA,WAA8B,CAC5B,IAAM,GAAK,KAAK,KAAK,aAAa,GAAK,KAIvC,OAHI,IACF,KAAK,aAAa,EAAE,EAEf,EACT,CAEA,UAA0B,CACxB,OAAO,KAAK,KAAK,YAAY,GAAK,IACpC,CAEA,aAA6B,CAC3B,OAAO,KAAK,OAAO,YAAY,CACjC,CAEA,WAA2B,CACzB,OAAO,KAAK,OAAO,UAAU,CAC/B,CAOA,SAAkC,CAChC,IAAM,EAA4B,CAAC,EAC7B,IAAM,KAAK,KAAK,aAAa,GAAK,KACpC,MACF,EAAE,uBAAyB,KAE7B,IAAM,IAAM,KAAK,KAAK,YAAY,GAAK,KACnC,MACF,EAAE,sBAAwB,KAE5B,IAAM,IAAM,KAAK,OAAO,YAAY,EAIpC,OAHI,MACF,EAAE,sBAAwB,KAErB,CACT,CAEA,aAAqC,CACnC,OAAO,KAAK,eACd,CAEA,MAAM,SAAS,OAAuC,CACpD,GAAI,CAAC,KAAK,IACR,OAEF,IAAM,UAAY,KAAK,IAAI,aAAa,EACxC,GAAI,KAAK,sBAAwB,UAAW,CAC1C,IAAI,MAAM,6CAA8C,SAAS,EACjE,MACF,CAEA,KAAK,gBAAkB,OACvB,KAAK,oBAAsB,UAE3B,IAAM,IAAM,KAAK,WACX,CAAC,OAAQ,SAAW,MAAM,QAAQ,IAAI,CAC1C,KAAK,OAAO,qBAAqB,EACjC,KAAK,IAAI,OAAO,CAClB,CAAC,EACK,SAAW,KAAK,OAAO,YAAY,EACzC,GAAI,MAAQ,KAAK,WAAY,CAC3B,KAAK,oBAAsB,KAC3B,MACF,CASA,IAAI,KAAK,4BAA6B,UAAW,OAAO,UAAU,EAClE,KAAK,QAAQ,sBAAsB,KAAK,OAAO,IAAK,WAAW,EAAG,CAChE,OAAQ,OACR,QAAS,KAAK,eAAe,EAC7B,KAAM,KAAK,UAAU,CACnB,UACA,SACA,OACA,GAAG,OACH,GAAI,SAAW,CAAE,OAAQ,CAC3B,CAAC,EACD,UAAW,GACX,OAAQ,YAAY,QAAQ,GAAM,CACpC,CAAC,EAAE,UAAY,CACb,KAAK,oBAAsB,KAC3B,IAAI,KAAK,iCAAkC,SAAS,CACtD,CAAC,CACH,CAEA,eAAsB,CACpB,KAAK,gBAAkB,KACvB,KAAK,oBAAsB,IAC7B,CAEA,SAAgB,CACd,KAAK,YAAc,EACnB,KAAK,cAAc,EACnB,KAAK,gBAAkB,KACvB,KAAK,cAAgB,EACrB,KAAK,IAAM,IACb,CAEA,gBAAiD,CAC/C,MAAO,CACL,GAAG,OAAO,YAAY,KAAK,OAAO,QAAQ,QAAQ,CAAC,EACnD,GAAG,KAAK,QAAQ,CAClB,CACF,CAEA,aAAqB,UAAyB,CACxC,KAAK,kBAAoB,YAGzB,KAAK,IAAI,EAAI,KAAK,cAAgB,KAGtC,KAAK,YACH,UACA,KAAK,OAAO,YAAY,EACxB,KAAK,OAAO,UAAU,CACxB,EACF,CAEA,YACE,UACA,SACA,OACM,CACN,KAAK,cAAgB,KAAK,IAAI,EAC9B,KAAK,gBAAkB,UAEvB,KAAK,QAAQ,KAAK,OAAO,IAAK,CAC5B,OAAQ,OACR,QAAS,KAAK,eAAe,EAC7B,KAAM,KAAK,UAAU,CAAE,UAAW,SAAU,MAAO,CAAC,EACpD,UAAW,GACX,OAAQ,YAAY,QAAQ,GAAM,CACpC,CAAC,EACE,KAAM,KAAQ,CACR,IAAI,KACP,KAAK,gBAAkB,KAE3B,CAAC,EACA,UAAY,CACX,KAAK,gBAAkB,KACvB,IAAI,KAAK,iCAAiC,CAC5C,CAAC,CACL,CAEA,MAAc,SAAS,UAAkC,CACvD,KAAK,gBAAkB,KACvB,KAAK,cAAgB,KAAK,IAAI,EAC9B,IAAM,IAAM,KAAK,WACX,OAAS,MAAM,KAAK,OAAO,qBAAqB,EAChD,SAAW,KAAK,OAAO,YAAY,EACrC,MAAQ,KAAK,aAGjB,IAAI,MACF,oCACA,UACA,UAAY,UACZ,QAAU,MACZ,EACA,KAAK,YAAY,UAAW,SAAU,MAAM,EAC9C,CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device.d.mts","names":[],"sources":["../../src/tracking/device.ts"],"mappings":";;AAgBA
|
|
1
|
+
{"version":3,"file":"device.d.mts","names":[],"sources":["../../src/tracking/device.ts"],"mappings":";;AAgBA;;;;AAA+C;AAW/C;;KAXY,mBAAA,SAA4B,OAAO;;AAWZ;AAuEnC;;;;;iBAvEgB,QAAA,CAAA,GAAY,OAAO;AAAA,cAuEtB,aAAA;EAAA,QACH,QAAA;EAAA,QACA,MAAA;EAAA,QACA,SAAA;EAAA,iBACS,mBAAA;cAEL,mBAAA,GAAsB,mBAAA;EAKlC,IAAA,CAAA;EAAA,QAkCQ,gBAAA;EAkBR,WAAA,CAAA;EAIA,SAAA,CAAA;EAIA,oBAAA,CAAA,GAAwB,OAAO;AAAA"}
|
package/dist/tracking/device.mjs
CHANGED
|
@@ -1,104 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
//#region src/tracking/device.ts
|
|
3
|
-
const log = createLogger("device");
|
|
4
|
-
const LS_KEY = "interfere:device_id";
|
|
5
|
-
const COOKIE_NAME = "interfere_did";
|
|
6
|
-
const COOKIE_MAX_AGE_DAYS = 400;
|
|
7
|
-
const FP_IDLE_TIMEOUT_MS = 5e3;
|
|
8
|
-
/**
|
|
9
|
-
* Resolves on the next browser idle frame, capped at 5s. Falls back to
|
|
10
|
-
* `setTimeout(0)` (next macrotask) on hosts without
|
|
11
|
-
* `requestIdleCallback`. Exported only for the unit test that pins the
|
|
12
|
-
* "default provider gates fingerprint work via RIC" contract — not part
|
|
13
|
-
* of the package's public surface.
|
|
14
|
-
*/
|
|
15
|
-
function whenIdle() {
|
|
16
|
-
return new Promise((resolve) => {
|
|
17
|
-
if (typeof globalThis.requestIdleCallback === "function") {
|
|
18
|
-
globalThis.requestIdleCallback(() => resolve(), { timeout: FP_IDLE_TIMEOUT_MS });
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
setTimeout(resolve, 0);
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
const defaultFingerprintProvider = async () => {
|
|
25
|
-
await whenIdle();
|
|
26
|
-
try {
|
|
27
|
-
const result = await (await (await import("@fingerprintjs/fingerprintjs")).load()).get();
|
|
28
|
-
if (!result.visitorId) {
|
|
29
|
-
log.error("Fingerprinting returned an empty visitor id; visitor identity will be cookie-only");
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
return result.visitorId;
|
|
33
|
-
} catch (err) {
|
|
34
|
-
log.error("fingerprint init failed; visitor identity will be cookie-only", err);
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
function tryLocalStorage() {
|
|
39
|
-
try {
|
|
40
|
-
const s = globalThis.localStorage;
|
|
41
|
-
const key = "__interfere_device_probe__";
|
|
42
|
-
s.setItem(key, "1");
|
|
43
|
-
s.removeItem(key);
|
|
44
|
-
return s;
|
|
45
|
-
} catch {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
function getCookie(name) {
|
|
50
|
-
if (typeof document === "undefined") return null;
|
|
51
|
-
const match = document.cookie.split("; ").find((c) => c.startsWith(`${name}=`));
|
|
52
|
-
return match ? decodeURIComponent(match.split("=")[1] ?? "") : null;
|
|
53
|
-
}
|
|
54
|
-
function setCookie(name, value) {
|
|
55
|
-
if (typeof document === "undefined") return;
|
|
56
|
-
const maxAge = COOKIE_MAX_AGE_DAYS * 24 * 60 * 60;
|
|
57
|
-
document.cookie = `${name}=${encodeURIComponent(value)};path=/;max-age=${maxAge};SameSite=Lax`;
|
|
58
|
-
}
|
|
59
|
-
function generateId() {
|
|
60
|
-
return crypto.randomUUID();
|
|
61
|
-
}
|
|
62
|
-
var DeviceManager = class {
|
|
63
|
-
deviceId = null;
|
|
64
|
-
fpHash = null;
|
|
65
|
-
fpPending = null;
|
|
66
|
-
fingerprintProvider;
|
|
67
|
-
constructor(fingerprintProvider) {
|
|
68
|
-
this.fingerprintProvider = fingerprintProvider ?? defaultFingerprintProvider;
|
|
69
|
-
}
|
|
70
|
-
init() {
|
|
71
|
-
if (this.deviceId) return;
|
|
72
|
-
const ls = tryLocalStorage();
|
|
73
|
-
const fromLs = ls?.getItem(LS_KEY) ?? null;
|
|
74
|
-
const fromCookie = getCookie(COOKIE_NAME);
|
|
75
|
-
this.deviceId = fromLs ?? fromCookie ?? generateId();
|
|
76
|
-
if (!fromLs && ls) ls.setItem(LS_KEY, this.deviceId);
|
|
77
|
-
if (!fromCookie) setCookie(COOKIE_NAME, this.deviceId);
|
|
78
|
-
if (fromLs && !fromCookie) setCookie(COOKIE_NAME, this.deviceId);
|
|
79
|
-
if (fromCookie && !fromLs && ls) ls.setItem(LS_KEY, this.deviceId);
|
|
80
|
-
log.debug("device %s (ls=%s cookie=%s)", this.deviceId, !!fromLs, !!fromCookie);
|
|
81
|
-
this.startFingerprint();
|
|
82
|
-
}
|
|
83
|
-
startFingerprint() {
|
|
84
|
-
if (this.fpHash || this.fpPending) return;
|
|
85
|
-
this.fpPending = (async () => {
|
|
86
|
-
const hash = await this.fingerprintProvider();
|
|
87
|
-
this.fpHash = hash;
|
|
88
|
-
if (hash) log.debug("fpHash %s", hash);
|
|
89
|
-
return hash;
|
|
90
|
-
})();
|
|
91
|
-
}
|
|
92
|
-
getDeviceId() {
|
|
93
|
-
return this.deviceId;
|
|
94
|
-
}
|
|
95
|
-
getFpHash() {
|
|
96
|
-
return this.fpHash;
|
|
97
|
-
}
|
|
98
|
-
whenFingerprintReady() {
|
|
99
|
-
if (this.fpHash) return Promise.resolve(this.fpHash);
|
|
100
|
-
return this.fpPending ?? Promise.resolve(null);
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
//#endregion
|
|
104
|
-
export { DeviceManager, whenIdle };
|
|
1
|
+
import{createLogger}from"../util/log.mjs";const log=createLogger(`device`),LS_KEY=`interfere:device_id`,COOKIE_NAME=`interfere_did`;function whenIdle(){return new Promise(resolve=>{if(typeof globalThis.requestIdleCallback==`function`){globalThis.requestIdleCallback(()=>resolve(),{timeout:5e3});return}setTimeout(resolve,0)})}const defaultFingerprintProvider=async()=>{await whenIdle();try{let result=await(await(await import(`@fingerprintjs/fingerprintjs`)).load()).get();return result.visitorId?result.visitorId:(log.error(`Fingerprinting returned an empty visitor id; visitor identity will be cookie-only`),null)}catch(err){return log.error(`fingerprint init failed; visitor identity will be cookie-only`,err),null}};function tryLocalStorage(){try{let s=globalThis.localStorage,key=`__interfere_device_probe__`;return s.setItem(key,`1`),s.removeItem(key),s}catch{return null}}function getCookie(name){if(typeof document>`u`)return null;let match=document.cookie.split(`; `).find(c=>c.startsWith(`${name}=`));return match?decodeURIComponent(match.split(`=`)[1]??``):null}function setCookie(name,value){typeof document>`u`||(document.cookie=`${name}=${encodeURIComponent(value)};path=/;max-age=${400*24*60*60};SameSite=Lax`)}function generateId(){return crypto.randomUUID()}var DeviceManager=class{deviceId=null;fpHash=null;fpPending=null;fingerprintProvider;constructor(fingerprintProvider){this.fingerprintProvider=fingerprintProvider??defaultFingerprintProvider}init(){if(this.deviceId)return;let ls=tryLocalStorage(),fromLs=ls?.getItem(LS_KEY)??null,fromCookie=getCookie(COOKIE_NAME);this.deviceId=fromLs??fromCookie??generateId(),!fromLs&&ls&&ls.setItem(LS_KEY,this.deviceId),fromCookie||setCookie(COOKIE_NAME,this.deviceId),fromLs&&!fromCookie&&setCookie(COOKIE_NAME,this.deviceId),fromCookie&&!fromLs&&ls&&ls.setItem(LS_KEY,this.deviceId),log.debug(`device %s (ls=%s cookie=%s)`,this.deviceId,!!fromLs,!!fromCookie),this.startFingerprint()}startFingerprint(){this.fpHash||this.fpPending||(this.fpPending=(async()=>{let hash=await this.fingerprintProvider();return this.fpHash=hash,hash&&log.debug(`fpHash %s`,hash),hash})())}getDeviceId(){return this.deviceId}getFpHash(){return this.fpHash}whenFingerprintReady(){return this.fpHash?Promise.resolve(this.fpHash):this.fpPending??Promise.resolve(null)}};export{DeviceManager,whenIdle};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device.mjs","names":[],"sources":["../../src/tracking/device.ts"],"sourcesContent":["import { createLogger } from \"../util/log.js\";\n\nconst log = createLogger(\"device\");\n\nconst LS_KEY = \"interfere:device_id\";\nconst COOKIE_NAME = \"interfere_did\";\nconst COOKIE_MAX_AGE_DAYS = 400;\n\n/**\n * Producer of an opaque, stable per-browser fingerprint hash. The default\n * dynamic-imports the FingerprintJS OSS library and self-defers via\n * `requestIdleCallback` so the ~1.5s of synchronous font / canvas / WebGL\n * probing the library performs on a cold load doesn't land on the\n * hydration tick. Tests inject a fixed-value provider that resolves\n * immediately — they pay nothing for the production deferral.\n */\nexport type FingerprintProvider = () => Promise<string | null>;\n\nconst FP_IDLE_TIMEOUT_MS = 5000;\n\n/**\n * Resolves on the next browser idle frame, capped at 5s. Falls back to\n * `setTimeout(0)` (next macrotask) on hosts without\n * `requestIdleCallback`. Exported only for the unit test that pins the\n * \"default provider gates fingerprint work via RIC\" contract — not part\n * of the package's public surface.\n */\nexport function whenIdle(): Promise<void> {\n return new Promise((resolve) => {\n if (typeof globalThis.requestIdleCallback === \"function\") {\n globalThis.requestIdleCallback(() => resolve(), {\n timeout: FP_IDLE_TIMEOUT_MS,\n });\n return;\n }\n setTimeout(resolve, 0);\n });\n}\n\nconst defaultFingerprintProvider: FingerprintProvider = async () => {\n await whenIdle();\n try {\n const FingerprintJS = await import(\"@fingerprintjs/fingerprintjs\");\n const fp = await FingerprintJS.load();\n const result = await fp.get();\n if (!result.visitorId) {\n log.error(\n \"Fingerprinting returned an empty visitor id; visitor identity will be cookie-only\"\n );\n\n return null;\n }\n return result.visitorId;\n } catch (err) {\n log.error(\n \"fingerprint init failed; visitor identity will be cookie-only\",\n err\n );\n\n return null;\n }\n};\n\nfunction tryLocalStorage(): Storage | null {\n try {\n const s = globalThis.localStorage;\n const key = \"__interfere_device_probe__\";\n s.setItem(key, \"1\");\n s.removeItem(key);\n return s;\n } catch {\n return null;\n }\n}\n\nfunction getCookie(name: string): string | null {\n if (typeof document === \"undefined\") {\n return null;\n }\n const match = document.cookie\n .split(\"; \")\n .find((c) => c.startsWith(`${name}=`));\n return match ? decodeURIComponent(match.split(\"=\")[1] ?? \"\") : null;\n}\n\nfunction setCookie(name: string, value: string): void {\n if (typeof document === \"undefined\") {\n return;\n }\n const maxAge = COOKIE_MAX_AGE_DAYS * 24 * 60 * 60;\n // biome-ignore lint/suspicious/noDocumentCookie: Cookie Store API is async and not universally supported; synchronous access is required here\n document.cookie = `${name}=${encodeURIComponent(value)};path=/;max-age=${maxAge};SameSite=Lax`;\n}\n\nfunction generateId(): string {\n return crypto.randomUUID();\n}\n\nexport class DeviceManager {\n private deviceId: string | null = null;\n private fpHash: string | null = null;\n private fpPending: Promise<string | null> | null = null;\n private readonly fingerprintProvider: FingerprintProvider;\n\n constructor(fingerprintProvider?: FingerprintProvider) {\n this.fingerprintProvider =\n fingerprintProvider ?? defaultFingerprintProvider;\n }\n\n init(): void {\n if (this.deviceId) {\n return;\n }\n\n const ls = tryLocalStorage();\n const fromLs = ls?.getItem(LS_KEY) ?? null;\n const fromCookie = getCookie(COOKIE_NAME);\n\n this.deviceId = fromLs ?? fromCookie ?? generateId();\n\n if (!fromLs && ls) {\n ls.setItem(LS_KEY, this.deviceId);\n }\n if (!fromCookie) {\n setCookie(COOKIE_NAME, this.deviceId);\n }\n if (fromLs && !fromCookie) {\n setCookie(COOKIE_NAME, this.deviceId);\n }\n if (fromCookie && !fromLs && ls) {\n ls.setItem(LS_KEY, this.deviceId);\n }\n\n log.debug(\n \"device %s (ls=%s cookie=%s)\",\n this.deviceId,\n !!fromLs,\n !!fromCookie\n );\n\n this.startFingerprint();\n }\n\n private startFingerprint(): void {\n if (this.fpHash || this.fpPending) {\n return;\n }\n\n this.fpPending = (async () => {\n const hash = await this.fingerprintProvider();\n this.fpHash = hash;\n if (hash) {\n log.debug(\"fpHash %s\", hash);\n }\n // No log on null — the provider is responsible for surfacing its own\n // failure mode (the default provider logs at error level; custom\n // providers can choose). Logging here too would double-count.\n return hash;\n })();\n }\n\n getDeviceId(): string | null {\n return this.deviceId;\n }\n\n getFpHash(): string | null {\n return this.fpHash;\n }\n\n whenFingerprintReady(): Promise<string | null> {\n if (this.fpHash) {\n return Promise.resolve(this.fpHash);\n }\n return this.fpPending ?? Promise.resolve(null);\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"device.mjs","names":[],"sources":["../../src/tracking/device.ts"],"sourcesContent":["import { createLogger } from \"../util/log.js\";\n\nconst log = createLogger(\"device\");\n\nconst LS_KEY = \"interfere:device_id\";\nconst COOKIE_NAME = \"interfere_did\";\nconst COOKIE_MAX_AGE_DAYS = 400;\n\n/**\n * Producer of an opaque, stable per-browser fingerprint hash. The default\n * dynamic-imports the FingerprintJS OSS library and self-defers via\n * `requestIdleCallback` so the ~1.5s of synchronous font / canvas / WebGL\n * probing the library performs on a cold load doesn't land on the\n * hydration tick. Tests inject a fixed-value provider that resolves\n * immediately — they pay nothing for the production deferral.\n */\nexport type FingerprintProvider = () => Promise<string | null>;\n\nconst FP_IDLE_TIMEOUT_MS = 5000;\n\n/**\n * Resolves on the next browser idle frame, capped at 5s. Falls back to\n * `setTimeout(0)` (next macrotask) on hosts without\n * `requestIdleCallback`. Exported only for the unit test that pins the\n * \"default provider gates fingerprint work via RIC\" contract — not part\n * of the package's public surface.\n */\nexport function whenIdle(): Promise<void> {\n return new Promise((resolve) => {\n if (typeof globalThis.requestIdleCallback === \"function\") {\n globalThis.requestIdleCallback(() => resolve(), {\n timeout: FP_IDLE_TIMEOUT_MS,\n });\n return;\n }\n setTimeout(resolve, 0);\n });\n}\n\nconst defaultFingerprintProvider: FingerprintProvider = async () => {\n await whenIdle();\n try {\n const FingerprintJS = await import(\"@fingerprintjs/fingerprintjs\");\n const fp = await FingerprintJS.load();\n const result = await fp.get();\n if (!result.visitorId) {\n log.error(\n \"Fingerprinting returned an empty visitor id; visitor identity will be cookie-only\"\n );\n\n return null;\n }\n return result.visitorId;\n } catch (err) {\n log.error(\n \"fingerprint init failed; visitor identity will be cookie-only\",\n err\n );\n\n return null;\n }\n};\n\nfunction tryLocalStorage(): Storage | null {\n try {\n const s = globalThis.localStorage;\n const key = \"__interfere_device_probe__\";\n s.setItem(key, \"1\");\n s.removeItem(key);\n return s;\n } catch {\n return null;\n }\n}\n\nfunction getCookie(name: string): string | null {\n if (typeof document === \"undefined\") {\n return null;\n }\n const match = document.cookie\n .split(\"; \")\n .find((c) => c.startsWith(`${name}=`));\n return match ? decodeURIComponent(match.split(\"=\")[1] ?? \"\") : null;\n}\n\nfunction setCookie(name: string, value: string): void {\n if (typeof document === \"undefined\") {\n return;\n }\n const maxAge = COOKIE_MAX_AGE_DAYS * 24 * 60 * 60;\n // biome-ignore lint/suspicious/noDocumentCookie: Cookie Store API is async and not universally supported; synchronous access is required here\n document.cookie = `${name}=${encodeURIComponent(value)};path=/;max-age=${maxAge};SameSite=Lax`;\n}\n\nfunction generateId(): string {\n return crypto.randomUUID();\n}\n\nexport class DeviceManager {\n private deviceId: string | null = null;\n private fpHash: string | null = null;\n private fpPending: Promise<string | null> | null = null;\n private readonly fingerprintProvider: FingerprintProvider;\n\n constructor(fingerprintProvider?: FingerprintProvider) {\n this.fingerprintProvider =\n fingerprintProvider ?? defaultFingerprintProvider;\n }\n\n init(): void {\n if (this.deviceId) {\n return;\n }\n\n const ls = tryLocalStorage();\n const fromLs = ls?.getItem(LS_KEY) ?? null;\n const fromCookie = getCookie(COOKIE_NAME);\n\n this.deviceId = fromLs ?? fromCookie ?? generateId();\n\n if (!fromLs && ls) {\n ls.setItem(LS_KEY, this.deviceId);\n }\n if (!fromCookie) {\n setCookie(COOKIE_NAME, this.deviceId);\n }\n if (fromLs && !fromCookie) {\n setCookie(COOKIE_NAME, this.deviceId);\n }\n if (fromCookie && !fromLs && ls) {\n ls.setItem(LS_KEY, this.deviceId);\n }\n\n log.debug(\n \"device %s (ls=%s cookie=%s)\",\n this.deviceId,\n !!fromLs,\n !!fromCookie\n );\n\n this.startFingerprint();\n }\n\n private startFingerprint(): void {\n if (this.fpHash || this.fpPending) {\n return;\n }\n\n this.fpPending = (async () => {\n const hash = await this.fingerprintProvider();\n this.fpHash = hash;\n if (hash) {\n log.debug(\"fpHash %s\", hash);\n }\n // No log on null — the provider is responsible for surfacing its own\n // failure mode (the default provider logs at error level; custom\n // providers can choose). Logging here too would double-count.\n return hash;\n })();\n }\n\n getDeviceId(): string | null {\n return this.deviceId;\n }\n\n getFpHash(): string | null {\n return this.fpHash;\n }\n\n whenFingerprintReady(): Promise<string | null> {\n if (this.fpHash) {\n return Promise.resolve(this.fpHash);\n }\n return this.fpPending ?? Promise.resolve(null);\n }\n}\n"],"mappings":"0CAEA,MAAM,IAAM,aAAa,QAAQ,EAE3B,OAAS,sBACT,YAAc,gBAsBpB,SAAgB,UAA0B,CACxC,OAAO,IAAI,QAAS,SAAY,CAC9B,GAAI,OAAO,WAAW,qBAAwB,WAAY,CACxD,WAAW,wBAA0B,QAAQ,EAAG,CAC9C,QAAS,GACX,CAAC,EACD,MACF,CACA,WAAW,QAAS,CAAC,CACvB,CAAC,CACH,CAEA,MAAM,2BAAkD,SAAY,CAClE,MAAM,SAAS,EACf,GAAI,CAGF,IAAM,OAAS,MAAM,MADJ,MADW,OAAO,iCACJ,KAAK,GACZ,IAAI,EAQ5B,OAPK,OAAO,UAOL,OAAO,WANZ,IAAI,MACF,mFACF,EAEO,KAGX,OAAS,IAAK,CAMZ,OALA,IAAI,MACF,gEACA,GACF,EAEO,IACT,CACF,EAEA,SAAS,iBAAkC,CACzC,GAAI,CACF,IAAM,EAAI,WAAW,aACf,IAAM,6BAGZ,OAFA,EAAE,QAAQ,IAAK,GAAG,EAClB,EAAE,WAAW,GAAG,EACT,CACT,MAAQ,CACN,OAAO,IACT,CACF,CAEA,SAAS,UAAU,KAA6B,CAC9C,GAAI,OAAO,SAAa,IACtB,OAAO,KAET,IAAM,MAAQ,SAAS,OACpB,MAAM,IAAI,EACV,KAAM,GAAM,EAAE,WAAW,GAAG,KAAK,EAAE,CAAC,EACvC,OAAO,MAAQ,mBAAmB,MAAM,MAAM,GAAG,EAAE,IAAM,EAAE,EAAI,IACjE,CAEA,SAAS,UAAU,KAAc,MAAqB,CAChD,OAAO,SAAa,MAKxB,SAAS,OAAS,GAAG,KAAK,GAAG,mBAAmB,KAAK,EAAE,kBAFxC,IAAsB,GAAK,GAAK,GAEiC,eAClF,CAEA,SAAS,YAAqB,CAC5B,OAAO,OAAO,WAAW,CAC3B,CAEA,IAAa,cAAb,KAA2B,CACzB,SAAkC,KAClC,OAAgC,KAChC,UAAmD,KACnD,oBAEA,YAAY,oBAA2C,CACrD,KAAK,oBACH,qBAAuB,0BAC3B,CAEA,MAAa,CACX,GAAI,KAAK,SACP,OAGF,IAAM,GAAK,gBAAgB,EACrB,OAAS,IAAI,QAAQ,MAAM,GAAK,KAChC,WAAa,UAAU,WAAW,EAExC,KAAK,SAAW,QAAU,YAAc,WAAW,EAE/C,CAAC,QAAU,IACb,GAAG,QAAQ,OAAQ,KAAK,QAAQ,EAE7B,YACH,UAAU,YAAa,KAAK,QAAQ,EAElC,QAAU,CAAC,YACb,UAAU,YAAa,KAAK,QAAQ,EAElC,YAAc,CAAC,QAAU,IAC3B,GAAG,QAAQ,OAAQ,KAAK,QAAQ,EAGlC,IAAI,MACF,8BACA,KAAK,SACL,CAAC,CAAC,OACF,CAAC,CAAC,UACJ,EAEA,KAAK,iBAAiB,CACxB,CAEA,kBAAiC,CAC3B,KAAK,QAAU,KAAK,YAIxB,KAAK,WAAa,SAAY,CAC5B,IAAM,KAAO,MAAM,KAAK,oBAAoB,EAQ5C,MAPA,MAAK,OAAS,KACV,MACF,IAAI,MAAM,YAAa,IAAI,EAKtB,IACT,GAAG,EACL,CAEA,aAA6B,CAC3B,OAAO,KAAK,QACd,CAEA,WAA2B,CACzB,OAAO,KAAK,MACd,CAEA,sBAA+C,CAI7C,OAHI,KAAK,OACA,QAAQ,QAAQ,KAAK,MAAM,EAE7B,KAAK,WAAa,QAAQ,QAAQ,IAAI,CAC/C,CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"geo.d.mts","names":[],"sources":["../../src/tracking/geo.ts"],"mappings":";cAea,WAAA;EAAA,QACH,MAAA;EAAA,QACA,OAAA;EAAA,QACA,MAAA;EAAA,iBACS,OAAA;cAEL,OAAA,UAAiB,UAAA,CAAW,KAAA;EAIxC,MAAA,CAAA,GAAU,
|
|
1
|
+
{"version":3,"file":"geo.d.mts","names":[],"sources":["../../src/tracking/geo.ts"],"mappings":";cAea,WAAA;EAAA,QACH,MAAA;EAAA,QACA,OAAA;EAAA,QACA,MAAA;EAAA,iBACS,OAAA;cAEL,OAAA,UAAiB,UAAA,CAAW,KAAA;EAIxC,MAAA,CAAA,GAAU,OAAO;EAuBjB,UAAA,CAAA;EAAA,QAIc,gBAAA;AAAA"}
|