@interfere/react 0.2.0-alpha.3 → 0.2.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/error-boundary.d.mts +2 -2
  2. package/dist/error-boundary.d.mts.map +1 -1
  3. package/dist/error-boundary.mjs +9 -7
  4. package/dist/error-boundary.mjs.map +1 -1
  5. package/dist/internal/client.d.mts +22 -3
  6. package/dist/internal/client.d.mts.map +1 -1
  7. package/dist/internal/client.mjs +77 -57
  8. package/dist/internal/client.mjs.map +1 -1
  9. package/dist/internal/consent.d.mts +15 -0
  10. package/dist/internal/consent.d.mts.map +1 -0
  11. package/dist/internal/consent.mjs +25 -0
  12. package/dist/internal/consent.mjs.map +1 -0
  13. package/dist/internal/plugin-runtime.d.mts +26 -0
  14. package/dist/internal/plugin-runtime.d.mts.map +1 -0
  15. package/dist/internal/plugin-runtime.mjs +85 -0
  16. package/dist/internal/plugin-runtime.mjs.map +1 -0
  17. package/dist/plugins/fingerprint.d.mts +6 -0
  18. package/dist/plugins/fingerprint.d.mts.map +1 -0
  19. package/dist/plugins/fingerprint.mjs +13 -0
  20. package/dist/plugins/fingerprint.mjs.map +1 -0
  21. package/dist/plugins/lib/loader.d.mts +2 -1
  22. package/dist/plugins/lib/loader.d.mts.map +1 -1
  23. package/dist/plugins/lib/loader.mjs +16 -20
  24. package/dist/plugins/lib/loader.mjs.map +1 -1
  25. package/dist/provider.d.mts +11 -4
  26. package/dist/provider.d.mts.map +1 -1
  27. package/dist/provider.mjs +7 -4
  28. package/dist/provider.mjs.map +1 -1
  29. package/dist/tracking/api.d.mts +1 -6
  30. package/dist/tracking/api.d.mts.map +1 -1
  31. package/dist/tracking/api.mjs +4 -16
  32. package/dist/tracking/api.mjs.map +1 -1
  33. package/dist/tracking/session.d.mts +0 -1
  34. package/dist/tracking/session.d.mts.map +1 -1
  35. package/dist/tracking/session.mjs +0 -19
  36. package/dist/tracking/session.mjs.map +1 -1
  37. package/dist/tracking/visitor.d.mts +2 -1
  38. package/dist/tracking/visitor.d.mts.map +1 -1
  39. package/dist/tracking/visitor.mjs +6 -1
  40. package/dist/tracking/visitor.mjs.map +1 -1
  41. package/dist/transport/http.d.mts +2 -1
  42. package/dist/transport/http.d.mts.map +1 -1
  43. package/dist/transport/http.mjs +1 -1
  44. package/dist/transport/http.mjs.map +1 -1
  45. package/package.json +16 -11
@@ -11,8 +11,8 @@ interface ErrorBoundaryState {
11
11
  }
12
12
  /**
13
13
  * Catches render-phase React errors, reports them to the SDK, and renders a
14
- * fallback. Must be placed inside `<InterfereProvider>` so the client is
15
- * initialised when `capture` is called.
14
+ * fallback. Requires the SDK to be bootstrapped via `init()` before React
15
+ * renders so `capture` has an active runtime.
16
16
  *
17
17
  * Class component required — React has no hook-based error boundary API.
18
18
  */
@@ -1 +1 @@
1
- {"version":3,"file":"error-boundary.d.mts","names":[],"sources":["../src/error-boundary.tsx"],"mappings":";;;UASiB,kBAAA;EACf,QAAA,EAAU,SAAA;EACV,QAAA,GAAW,SAAA,KAAc,KAAA,EAAO,KAAA,EAAO,KAAA,iBAAsB,SAAA;EAC7D,OAAA,IAAW,KAAA,EAAO,KAAA,EAAO,IAAA,EAAM,SAAA;AAAA;AAAA,UAGvB,kBAAA;EACR,KAAA,EAAO,KAAA;AAAA;;;;;;;;cAUI,aAAA,SAAsB,SAAA,CACjC,kBAAA,EACA,kBAAA;EAES,KAAA,EAAO,kBAAA;EAAA,OAET,wBAAA,CAAyB,KAAA,EAAO,KAAA,GAAQ,kBAAA;EAItC,iBAAA,CAAkB,KAAA,EAAO,KAAA,EAAO,IAAA,EAAM,SAAA;EAAA,iBAa9B,KAAA;EAIR,MAAA,CAAA,GAAM,SAAA;AAAA"}
1
+ {"version":3,"file":"error-boundary.d.mts","names":[],"sources":["../src/error-boundary.tsx"],"mappings":";;;UASiB,kBAAA;EACf,QAAA,EAAU,SAAA;EACV,QAAA,GAAW,SAAA,KAAc,KAAA,EAAO,KAAA,EAAO,KAAA,iBAAsB,SAAA;EAC7D,OAAA,IAAW,KAAA,EAAO,KAAA,EAAO,IAAA,EAAM,SAAA;AAAA;AAAA,UAGvB,kBAAA;EACR,KAAA,EAAO,KAAA;AAAA;;;;;;;;cAUI,aAAA,SAAsB,SAAA,CACjC,kBAAA,EACA,kBAAA;EAES,KAAA,EAAO,kBAAA;EAAA,OAET,wBAAA,CAAyB,KAAA,EAAO,KAAA,GAAQ,kBAAA;EAItC,iBAAA,CAAkB,KAAA,EAAO,KAAA,EAAO,IAAA,EAAM,SAAA;EAAA,iBAiB9B,KAAA;EAIR,MAAA,CAAA,GAAM,SAAA;AAAA"}
@@ -1,13 +1,13 @@
1
1
  "use client";
2
2
  import { seen } from "./internal/errors.mjs";
3
- import { capture } from "./internal/client.mjs";
3
+ import { getClient } from "./internal/client.mjs";
4
4
  import { toExceptions } from "@interfere/types/sdk/errors";
5
5
  import { Component } from "react";
6
6
  //#region src/error-boundary.tsx
7
7
  /**
8
8
  * Catches render-phase React errors, reports them to the SDK, and renders a
9
- * fallback. Must be placed inside `<InterfereProvider>` so the client is
10
- * initialised when `capture` is called.
9
+ * fallback. Requires the SDK to be bootstrapped via `init()` before React
10
+ * renders so `capture` has an active runtime.
11
11
  *
12
12
  * Class component required — React has no hook-based error boundary API.
13
13
  */
@@ -19,10 +19,12 @@ var ErrorBoundary = class extends Component {
19
19
  componentDidCatch(error, info) {
20
20
  if (seen.has(error)) return;
21
21
  seen.add(error);
22
- capture("error", { exceptions: toExceptions(error, {
23
- type: "react",
24
- handled: true
25
- }) });
22
+ try {
23
+ getClient().capture("error", { exceptions: toExceptions(error, {
24
+ type: "react",
25
+ handled: true
26
+ }) });
27
+ } catch {}
26
28
  this.props.onError?.(error, info);
27
29
  }
28
30
  reset = () => {
@@ -1 +1 @@
1
- {"version":3,"file":"error-boundary.mjs","names":[],"sources":["../src/error-boundary.tsx"],"sourcesContent":["\"use client\";\n\nimport { toExceptions } from \"@interfere/types/sdk/errors\";\n\nimport { Component, type ErrorInfo, type ReactNode } from \"react\";\n\nimport { capture } from \"./internal/client.js\";\nimport { seen } from \"./internal/errors.js\";\n\nexport interface ErrorBoundaryProps {\n children: ReactNode;\n fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);\n onError?: (error: Error, info: ErrorInfo) => void;\n}\n\ninterface ErrorBoundaryState {\n error: Error | null;\n}\n\n/**\n * Catches render-phase React errors, reports them to the SDK, and renders a\n * fallback. Must be placed inside `<InterfereProvider>` so the client is\n * initialised when `capture` is called.\n *\n * Class component required — React has no hook-based error boundary API.\n */\nexport class ErrorBoundary extends Component<\n ErrorBoundaryProps,\n ErrorBoundaryState\n> {\n override state: ErrorBoundaryState = { error: null };\n\n static getDerivedStateFromError(error: Error): ErrorBoundaryState {\n return { error };\n }\n\n override componentDidCatch(error: Error, info: ErrorInfo) {\n if (seen.has(error)) {\n return;\n }\n seen.add(error);\n\n capture(\"error\", {\n exceptions: toExceptions(error, { type: \"react\", handled: true }),\n });\n\n this.props.onError?.(error, info);\n }\n\n private readonly reset = () => {\n this.setState({ error: null });\n };\n\n override render() {\n const { error } = this.state;\n\n if (error) {\n const { fallback } = this.props;\n if (typeof fallback === \"function\") {\n return fallback(error, this.reset);\n }\n return fallback ?? null;\n }\n\n return this.props.children;\n }\n}\n"],"mappings":";;;;;;;;;;;;;AA0BA,IAAa,gBAAb,cAAmC,UAGjC;CACA,QAAqC,EAAE,OAAO,MAAM;CAEpD,OAAO,yBAAyB,OAAkC;AAChE,SAAO,EAAE,OAAO;;CAGlB,kBAA2B,OAAc,MAAiB;AACxD,MAAI,KAAK,IAAI,MAAM,CACjB;AAEF,OAAK,IAAI,MAAM;AAEf,UAAQ,SAAS,EACf,YAAY,aAAa,OAAO;GAAE,MAAM;GAAS,SAAS;GAAM,CAAC,EAClE,CAAC;AAEF,OAAK,MAAM,UAAU,OAAO,KAAK;;CAGnC,cAA+B;AAC7B,OAAK,SAAS,EAAE,OAAO,MAAM,CAAC;;CAGhC,SAAkB;EAChB,MAAM,EAAE,UAAU,KAAK;AAEvB,MAAI,OAAO;GACT,MAAM,EAAE,aAAa,KAAK;AAC1B,OAAI,OAAO,aAAa,WACtB,QAAO,SAAS,OAAO,KAAK,MAAM;AAEpC,UAAO,YAAY;;AAGrB,SAAO,KAAK,MAAM"}
1
+ {"version":3,"file":"error-boundary.mjs","names":[],"sources":["../src/error-boundary.tsx"],"sourcesContent":["\"use client\";\n\nimport { toExceptions } from \"@interfere/types/sdk/errors\";\n\nimport { Component, type ErrorInfo, type ReactNode } from \"react\";\n\nimport { getClient } from \"./internal/client.js\";\nimport { seen } from \"./internal/errors.js\";\n\nexport interface ErrorBoundaryProps {\n children: ReactNode;\n fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);\n onError?: (error: Error, info: ErrorInfo) => void;\n}\n\ninterface ErrorBoundaryState {\n error: Error | null;\n}\n\n/**\n * Catches render-phase React errors, reports them to the SDK, and renders a\n * fallback. Requires the SDK to be bootstrapped via `init()` before React\n * renders so `capture` has an active runtime.\n *\n * Class component required — React has no hook-based error boundary API.\n */\nexport class ErrorBoundary extends Component<\n ErrorBoundaryProps,\n ErrorBoundaryState\n> {\n override state: ErrorBoundaryState = { error: null };\n\n static getDerivedStateFromError(error: Error): ErrorBoundaryState {\n return { error };\n }\n\n override componentDidCatch(error: Error, info: ErrorInfo) {\n if (seen.has(error)) {\n return;\n }\n seen.add(error);\n\n try {\n getClient().capture(\"error\", {\n exceptions: toExceptions(error, { type: \"react\", handled: true }),\n });\n } catch {\n // SDK not initialized — error boundary still renders fallback\n }\n\n this.props.onError?.(error, info);\n }\n\n private readonly reset = () => {\n this.setState({ error: null });\n };\n\n override render() {\n const { error } = this.state;\n\n if (error) {\n const { fallback } = this.props;\n if (typeof fallback === \"function\") {\n return fallback(error, this.reset);\n }\n return fallback ?? null;\n }\n\n return this.props.children;\n }\n}\n"],"mappings":";;;;;;;;;;;;;AA0BA,IAAa,gBAAb,cAAmC,UAGjC;CACA,QAAqC,EAAE,OAAO,MAAM;CAEpD,OAAO,yBAAyB,OAAkC;AAChE,SAAO,EAAE,OAAO;;CAGlB,kBAA2B,OAAc,MAAiB;AACxD,MAAI,KAAK,IAAI,MAAM,CACjB;AAEF,OAAK,IAAI,MAAM;AAEf,MAAI;AACF,cAAW,CAAC,QAAQ,SAAS,EAC3B,YAAY,aAAa,OAAO;IAAE,MAAM;IAAS,SAAS;IAAM,CAAC,EAClE,CAAC;UACI;AAIR,OAAK,MAAM,UAAU,OAAO,KAAK;;CAGnC,cAA+B;AAC7B,OAAK,SAAS,EAAE,OAAO,MAAM,CAAC;;CAGhC,SAAkB;EAChB,MAAM,EAAE,UAAU,KAAK;AAEvB,MAAI,OAAO;GACT,MAAM,EAAE,aAAa,KAAK;AAC1B,OAAI,OAAO,aAAa,WACtB,QAAO,SAAS,OAAO,KAAK,MAAM;AAEpC,UAAO,YAAY;;AAGrB,SAAO,KAAK,MAAM"}
@@ -1,13 +1,32 @@
1
1
  import { PluginOverrides } from "../plugins/lib/loader.mjs";
2
+ import { ConsentState } from "@interfere/types/sdk/plugins/manifest";
2
3
  import { EnvelopePayload, EventType } from "@interfere/types/sdk/envelope";
3
4
 
4
5
  //#region src/internal/client.d.ts
5
6
  interface ClientOptions {
7
+ consent?: ConsentState;
6
8
  plugins?: PluginOverrides;
7
9
  }
10
+ declare class Client {
11
+ private readonly metadata;
12
+ private readonly queue;
13
+ private readonly runtime;
14
+ constructor(opts: ClientOptions, buildId: string, releaseId: string | null);
15
+ capture<T extends EventType>(type: T, payload: EnvelopePayload<T>): void;
16
+ flush(): void;
17
+ dispose(): void;
18
+ getConsent(): ConsentState | null;
19
+ setConsent(value?: ConsentState): void;
20
+ resetConsent(): void;
21
+ }
22
+ declare function getClient(): Client;
8
23
  declare function init(opts?: ClientOptions): void;
9
- declare function capture<T extends EventType>(type: T, payload: EnvelopePayload<T>): void;
10
- declare function flush(): void;
11
24
  declare function close(): void;
25
+ declare const consent: {
26
+ get(): ConsentState | null;
27
+ set(value?: ConsentState): void;
28
+ };
29
+ declare function syncConsent(consentState: ConsentState | undefined): void;
30
+ declare function flush(): void;
12
31
  //#endregion
13
- export { ClientOptions, capture, close, flush, init };
32
+ export { ClientOptions, close, consent, flush, getClient, init, syncConsent };
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.mts","names":[],"sources":["../../src/internal/client.ts"],"mappings":";;;;UAiBiB,aAAA;EACf,OAAA,GAAU,eAAA;AAAA;AAAA,iBAQI,IAAA,CAAK,IAAA,GAAM,aAAA;AAAA,iBA0DX,OAAA,WAAkB,SAAA,CAAA,CAChC,IAAA,EAAM,CAAA,EACN,OAAA,EAAS,eAAA,CAAgB,CAAA;AAAA,iBAeX,KAAA,CAAA;AAAA,iBAIA,KAAA,CAAA"}
1
+ {"version":3,"file":"client.d.mts","names":[],"sources":["../../src/internal/client.ts"],"mappings":";;;;;UAiBiB,aAAA;EACf,OAAA,GAAU,YAAA;EACV,OAAA,GAAU,eAAA;AAAA;AAAA,cAGN,MAAA;EAAA,iBACa,QAAA;EAAA,iBACA,KAAA;EAAA,iBACA,OAAA;cAEL,IAAA,EAAM,aAAA,EAAe,OAAA,UAAiB,SAAA;EAkClD,OAAA,WAAkB,SAAA,CAAA,CAAW,IAAA,EAAM,CAAA,EAAG,OAAA,EAAS,eAAA,CAAgB,CAAA;EAS/D,KAAA,CAAA;EAIA,OAAA,CAAA;EAMA,UAAA,CAAA,GAAc,YAAA;EAId,UAAA,CAAW,KAAA,GAAQ,YAAA;EAInB,YAAA,CAAA;AAAA;AAAA,iBAOc,SAAA,CAAA,GAAa,MAAA;AAAA,iBASb,IAAA,CAAK,IAAA,GAAM,aAAA;AAAA,iBAsBX,KAAA,CAAA;AAAA,cASH,OAAA;SACJ,YAAA;cAIK,YAAA;AAAA;AAAA,iBAKE,WAAA,CAAY,YAAA,EAAc,YAAA;AAAA,iBAa1B,KAAA,CAAA"}
@@ -1,80 +1,100 @@
1
- import errorsPlugin from "../plugins/errors.mjs";
2
1
  import { createLogger } from "../util/log.mjs";
3
- import { loadPlugins } from "../plugins/lib/loader.mjs";
4
- import { bootstrap, session, teardown } from "../tracking/api.mjs";
5
2
  import { HttpTransport } from "../transport/http.mjs";
3
+ import { bootstrap, session, teardown } from "../tracking/api.mjs";
6
4
  import { BatchQueue } from "../transport/queue.mjs";
7
5
  import { resolveTargets } from "./config.mjs";
8
6
  import { collectContext } from "./context.mjs";
9
7
  import { buildEnvelope } from "./envelope.mjs";
8
+ import { PluginRuntime } from "./plugin-runtime.mjs";
10
9
  import { registerServiceWorker } from "./sw.mjs";
11
10
  import { inferRuntime, normalizeEnv } from "@interfere/types/sdk/runtime";
12
11
  //#region src/internal/client.ts
13
12
  const log = createLogger("client");
14
- let state = "idle";
15
- let queue = null;
16
- let metadata = null;
17
- let cleanups = [];
13
+ var Client = class {
14
+ metadata;
15
+ queue;
16
+ runtime;
17
+ constructor(opts, buildId, releaseId) {
18
+ const targets = resolveTargets();
19
+ bootstrap(targets.session);
20
+ log.info("target: %s", targets.ingest.url);
21
+ this.metadata = {
22
+ context: collectContext(),
23
+ environment: normalizeEnv(typeof process !== "undefined" ? process.env.NODE_ENV : void 0),
24
+ runtime: inferRuntime(),
25
+ buildId,
26
+ releaseId
27
+ };
28
+ registerServiceWorker();
29
+ this.queue = new BatchQueue({ transport: new HttpTransport(targets.ingest) });
30
+ this.queue.start();
31
+ this.runtime = new PluginRuntime({
32
+ capture: (type, payload) => this.capture(type, payload),
33
+ getSessionId: () => session.getId() ?? ""
34
+ }, opts.plugins, opts.consent);
35
+ this.runtime.start();
36
+ }
37
+ capture(type, payload) {
38
+ const sessionId = session.getId();
39
+ if (!(sessionId && this.runtime.canCapture(type))) return;
40
+ this.queue.enqueue(buildEnvelope(type, payload, sessionId, this.metadata));
41
+ }
42
+ flush() {
43
+ this.queue.flush();
44
+ }
45
+ dispose() {
46
+ this.runtime.dispose();
47
+ teardown();
48
+ this.queue.dispose();
49
+ }
50
+ getConsent() {
51
+ return this.runtime.getConsent();
52
+ }
53
+ setConsent(value) {
54
+ this.runtime.setConsent(value);
55
+ }
56
+ resetConsent() {
57
+ this.runtime.resetConsent();
58
+ }
59
+ };
60
+ let instance = null;
61
+ function getClient() {
62
+ if (!instance) throw new Error("Interfere SDK not initialized. Call init() from your instrumentation-client entrypoint.");
63
+ return instance;
64
+ }
18
65
  function init(opts = {}) {
19
- if (state !== "idle") return;
66
+ if (instance) return;
20
67
  const buildId = globalThis.__INTERFERE_BUILD_ID__;
21
68
  const releaseId = globalThis.__INTERFERE_RELEASE_ID__;
22
69
  if (!buildId) {
23
70
  log.error("buildId not found — ensure withInterfere() is configured in next.config and instrumentation-client.ts exists in your project root.");
24
71
  return;
25
72
  }
26
- const targets = resolveTargets();
27
- bootstrap(targets.session, opts.plugins);
28
- log.info("target: %s", targets.ingest.url);
29
- metadata = {
30
- context: collectContext(),
31
- environment: normalizeEnv(typeof process !== "undefined" ? process.env.NODE_ENV : void 0),
32
- runtime: inferRuntime(),
33
- buildId,
34
- releaseId: releaseId ?? null
35
- };
36
- registerServiceWorker();
37
- queue = new BatchQueue({ transport: new HttpTransport(targets.ingest) });
38
- queue.start();
39
- const pluginCtx = {
40
- capture: (type, payload) => capture(type, payload),
41
- getSessionId: () => session.getId() ?? ""
42
- };
43
- const errorCleanup = errorsPlugin.setup(pluginCtx);
44
- cleanups = errorCleanup ? [errorCleanup] : [];
45
- state = "ready";
46
- loadPlugins({
47
- ...opts.plugins,
48
- errors: false
49
- }, pluginCtx).then((c) => {
50
- cleanups.push(...c);
51
- }).catch(() => {
52
- log.warn("non-critical plugin loading failed");
53
- });
73
+ instance = new Client(opts, buildId, releaseId ?? null);
54
74
  }
55
- function capture(type, payload) {
56
- try {
57
- const sessionId = session.getId();
58
- if (state !== "ready" || !sessionId || !queue || !metadata) return;
59
- queue.enqueue(buildEnvelope(type, payload, sessionId, metadata));
60
- } catch (err) {
61
- log.warn("capture failed", err);
75
+ function close() {
76
+ if (!instance) return;
77
+ instance.dispose();
78
+ instance = null;
79
+ }
80
+ const consent = {
81
+ get() {
82
+ return instance?.getConsent() ?? null;
83
+ },
84
+ set(value) {
85
+ instance?.setConsent(value);
62
86
  }
87
+ };
88
+ function syncConsent(consentState) {
89
+ if (!instance) return;
90
+ if (consentState) {
91
+ instance.setConsent(consentState);
92
+ return;
93
+ }
94
+ instance.resetConsent();
63
95
  }
64
96
  function flush() {
65
- queue?.flush();
66
- }
67
- function close() {
68
- if (state === "closed") return;
69
- state = "closed";
70
- for (const cleanup of cleanups) try {
71
- cleanup();
72
- } catch {}
73
- cleanups = [];
74
- teardown();
75
- queue?.dispose();
76
- queue = null;
77
- metadata = null;
97
+ instance?.flush();
78
98
  }
79
99
  //#endregion
80
- export { capture, close, flush, init };
100
+ export { close, consent, flush, getClient, init, syncConsent };
@@ -1 +1 @@
1
- {"version":3,"file":"client.mjs","names":[],"sources":["../../src/internal/client.ts"],"sourcesContent":["import type { EnvelopePayload, EventType } from \"@interfere/types/sdk/envelope\";\nimport { inferRuntime, normalizeEnv } from \"@interfere/types/sdk/runtime\";\n\nimport errorsPlugin from \"../plugins/errors.js\";\nimport { loadPlugins, type PluginOverrides } from \"../plugins/lib/loader.js\";\nimport type { PluginCleanup, PluginContext } from \"../plugins/lib/types.js\";\nimport { bootstrap, session, teardown } from \"../tracking/api.js\";\nimport { HttpTransport } from \"../transport/http.js\";\nimport { BatchQueue } from \"../transport/queue.js\";\nimport { createLogger } from \"../util/log.js\";\nimport { resolveTargets } from \"./config.js\";\nimport { collectContext } from \"./context.js\";\nimport { buildEnvelope, type EnvelopeMetadata } from \"./envelope.js\";\nimport { registerServiceWorker } from \"./sw.js\";\n\nconst log = createLogger(\"client\");\n\nexport interface ClientOptions {\n plugins?: PluginOverrides;\n}\n\nlet state: \"idle\" | \"ready\" | \"closed\" = \"idle\";\nlet queue: BatchQueue | null = null;\nlet metadata: EnvelopeMetadata | null = null;\nlet cleanups: PluginCleanup[] = [];\n\nexport function init(opts: ClientOptions = {}): void {\n if (state !== \"idle\") {\n return;\n }\n\n const buildId = (globalThis as Record<string, unknown>)\n .__INTERFERE_BUILD_ID__ as string | undefined;\n\n const releaseId = (globalThis as Record<string, unknown>)\n .__INTERFERE_RELEASE_ID__ as string | null | undefined;\n\n if (!buildId) {\n log.error(\n \"buildId not found — ensure withInterfere() is configured in \" +\n \"next.config and instrumentation-client.ts exists in your project root.\"\n );\n return;\n }\n\n const targets = resolveTargets();\n bootstrap(targets.session, opts.plugins);\n\n log.info(\"target: %s\", targets.ingest.url);\n\n metadata = {\n context: collectContext(),\n environment: normalizeEnv(\n typeof process !== \"undefined\" ? process.env.NODE_ENV : undefined\n ),\n runtime: inferRuntime(),\n buildId,\n releaseId: releaseId ?? null,\n };\n\n registerServiceWorker();\n\n const transport = new HttpTransport(targets.ingest);\n queue = new BatchQueue({ transport });\n queue.start();\n\n const pluginCtx: PluginContext = {\n capture: (type, payload) => capture(type, payload),\n getSessionId: () => session.getId() ?? \"\",\n };\n\n const errorCleanup = errorsPlugin.setup(pluginCtx);\n cleanups = errorCleanup ? [errorCleanup] : [];\n state = \"ready\";\n\n loadPlugins({ ...opts.plugins, errors: false }, pluginCtx)\n .then((c) => {\n cleanups.push(...c);\n })\n .catch(() => {\n log.warn(\"non-critical plugin loading failed\");\n });\n}\n\nexport function capture<T extends EventType>(\n type: T,\n payload: EnvelopePayload<T>\n): void {\n try {\n const sessionId = session.getId();\n\n if (state !== \"ready\" || !sessionId || !queue || !metadata) {\n return;\n }\n\n queue.enqueue(buildEnvelope(type, payload, sessionId, metadata));\n } catch (err) {\n log.warn(\"capture failed\", err);\n }\n}\n\nexport function flush(): void {\n queue?.flush();\n}\n\nexport function close(): void {\n if (state === \"closed\") {\n return;\n }\n state = \"closed\";\n\n for (const cleanup of cleanups) {\n try {\n cleanup();\n } catch {\n /* best-effort */\n }\n }\n cleanups = [];\n teardown();\n\n queue?.dispose();\n queue = null;\n metadata = null;\n}\n"],"mappings":";;;;;;;;;;;;AAeA,MAAM,MAAM,aAAa,SAAS;AAMlC,IAAI,QAAqC;AACzC,IAAI,QAA2B;AAC/B,IAAI,WAAoC;AACxC,IAAI,WAA4B,EAAE;AAElC,SAAgB,KAAK,OAAsB,EAAE,EAAQ;AACnD,KAAI,UAAU,OACZ;CAGF,MAAM,UAAW,WACd;CAEH,MAAM,YAAa,WAChB;AAEH,KAAI,CAAC,SAAS;AACZ,MAAI,MACF,qIAED;AACD;;CAGF,MAAM,UAAU,gBAAgB;AAChC,WAAU,QAAQ,SAAS,KAAK,QAAQ;AAExC,KAAI,KAAK,cAAc,QAAQ,OAAO,IAAI;AAE1C,YAAW;EACT,SAAS,gBAAgB;EACzB,aAAa,aACX,OAAO,YAAY,cAAc,QAAQ,IAAI,WAAW,KAAA,EACzD;EACD,SAAS,cAAc;EACvB;EACA,WAAW,aAAa;EACzB;AAED,wBAAuB;AAGvB,SAAQ,IAAI,WAAW,EAAE,WADP,IAAI,cAAc,QAAQ,OAAO,EACf,CAAC;AACrC,OAAM,OAAO;CAEb,MAAM,YAA2B;EAC/B,UAAU,MAAM,YAAY,QAAQ,MAAM,QAAQ;EAClD,oBAAoB,QAAQ,OAAO,IAAI;EACxC;CAED,MAAM,eAAe,aAAa,MAAM,UAAU;AAClD,YAAW,eAAe,CAAC,aAAa,GAAG,EAAE;AAC7C,SAAQ;AAER,aAAY;EAAE,GAAG,KAAK;EAAS,QAAQ;EAAO,EAAE,UAAU,CACvD,MAAM,MAAM;AACX,WAAS,KAAK,GAAG,EAAE;GACnB,CACD,YAAY;AACX,MAAI,KAAK,qCAAqC;GAC9C;;AAGN,SAAgB,QACd,MACA,SACM;AACN,KAAI;EACF,MAAM,YAAY,QAAQ,OAAO;AAEjC,MAAI,UAAU,WAAW,CAAC,aAAa,CAAC,SAAS,CAAC,SAChD;AAGF,QAAM,QAAQ,cAAc,MAAM,SAAS,WAAW,SAAS,CAAC;UACzD,KAAK;AACZ,MAAI,KAAK,kBAAkB,IAAI;;;AAInC,SAAgB,QAAc;AAC5B,QAAO,OAAO;;AAGhB,SAAgB,QAAc;AAC5B,KAAI,UAAU,SACZ;AAEF,SAAQ;AAER,MAAK,MAAM,WAAW,SACpB,KAAI;AACF,WAAS;SACH;AAIV,YAAW,EAAE;AACb,WAAU;AAEV,QAAO,SAAS;AAChB,SAAQ;AACR,YAAW"}
1
+ {"version":3,"file":"client.mjs","names":[],"sources":["../../src/internal/client.ts"],"sourcesContent":["import type { EnvelopePayload, EventType } from \"@interfere/types/sdk/envelope\";\nimport type { ConsentState } from \"@interfere/types/sdk/plugins/manifest\";\nimport { inferRuntime, normalizeEnv } from \"@interfere/types/sdk/runtime\";\n\nimport type { PluginOverrides } from \"../plugins/lib/loader.js\";\nimport { bootstrap, session, teardown } from \"../tracking/api.js\";\nimport { HttpTransport } from \"../transport/http.js\";\nimport { BatchQueue } from \"../transport/queue.js\";\nimport { createLogger } from \"../util/log.js\";\nimport { resolveTargets } from \"./config.js\";\nimport { collectContext } from \"./context.js\";\nimport { buildEnvelope, type EnvelopeMetadata } from \"./envelope.js\";\nimport { PluginRuntime } from \"./plugin-runtime.js\";\nimport { registerServiceWorker } from \"./sw.js\";\n\nconst log = createLogger(\"client\");\n\nexport interface ClientOptions {\n consent?: ConsentState;\n plugins?: PluginOverrides;\n}\n\nclass Client {\n private readonly metadata: EnvelopeMetadata;\n private readonly queue: BatchQueue;\n private readonly runtime: PluginRuntime;\n\n constructor(opts: ClientOptions, buildId: string, releaseId: string | null) {\n const targets = resolveTargets();\n bootstrap(targets.session);\n\n log.info(\"target: %s\", targets.ingest.url);\n\n this.metadata = {\n context: collectContext(),\n environment: normalizeEnv(\n typeof process !== \"undefined\" ? process.env.NODE_ENV : undefined\n ),\n runtime: inferRuntime(),\n buildId,\n releaseId,\n };\n\n registerServiceWorker();\n\n const transport = new HttpTransport(targets.ingest);\n this.queue = new BatchQueue({ transport });\n this.queue.start();\n\n this.runtime = new PluginRuntime(\n {\n capture: (type, payload) => this.capture(type, payload),\n getSessionId: () => session.getId() ?? \"\",\n },\n opts.plugins,\n opts.consent\n );\n\n this.runtime.start();\n }\n\n capture<T extends EventType>(type: T, payload: EnvelopePayload<T>): void {\n const sessionId = session.getId();\n if (!(sessionId && this.runtime.canCapture(type))) {\n return;\n }\n\n this.queue.enqueue(buildEnvelope(type, payload, sessionId, this.metadata));\n }\n\n flush(): void {\n this.queue.flush();\n }\n\n dispose(): void {\n this.runtime.dispose();\n teardown();\n this.queue.dispose();\n }\n\n getConsent(): ConsentState | null {\n return this.runtime.getConsent();\n }\n\n setConsent(value?: ConsentState): void {\n this.runtime.setConsent(value);\n }\n\n resetConsent(): void {\n this.runtime.resetConsent();\n }\n}\n\nlet instance: Client | null = null;\n\nexport function getClient(): Client {\n if (!instance) {\n throw new Error(\n \"Interfere SDK not initialized. Call init() from your instrumentation-client entrypoint.\"\n );\n }\n return instance;\n}\n\nexport function init(opts: ClientOptions = {}): void {\n if (instance) {\n return;\n }\n\n const buildId = (globalThis as Record<string, unknown>)\n .__INTERFERE_BUILD_ID__ as string | undefined;\n\n const releaseId = (globalThis as Record<string, unknown>)\n .__INTERFERE_RELEASE_ID__ as string | null | undefined;\n\n if (!buildId) {\n log.error(\n \"buildId not found — ensure withInterfere() is configured in \" +\n \"next.config and instrumentation-client.ts exists in your project root.\"\n );\n return;\n }\n\n instance = new Client(opts, buildId, releaseId ?? null);\n}\n\nexport function close(): void {\n if (!instance) {\n return;\n }\n\n instance.dispose();\n instance = null;\n}\n\nexport const consent = {\n get(): ConsentState | null {\n return instance?.getConsent() ?? null;\n },\n\n set(value?: ConsentState): void {\n instance?.setConsent(value);\n },\n};\n\nexport function syncConsent(consentState: ConsentState | undefined): void {\n if (!instance) {\n return;\n }\n\n if (consentState) {\n instance.setConsent(consentState);\n return;\n }\n\n instance.resetConsent();\n}\n\nexport function flush(): void {\n instance?.flush();\n}\n"],"mappings":";;;;;;;;;;;AAeA,MAAM,MAAM,aAAa,SAAS;AAOlC,IAAM,SAAN,MAAa;CACX;CACA;CACA;CAEA,YAAY,MAAqB,SAAiB,WAA0B;EAC1E,MAAM,UAAU,gBAAgB;AAChC,YAAU,QAAQ,QAAQ;AAE1B,MAAI,KAAK,cAAc,QAAQ,OAAO,IAAI;AAE1C,OAAK,WAAW;GACd,SAAS,gBAAgB;GACzB,aAAa,aACX,OAAO,YAAY,cAAc,QAAQ,IAAI,WAAW,KAAA,EACzD;GACD,SAAS,cAAc;GACvB;GACA;GACD;AAED,yBAAuB;AAGvB,OAAK,QAAQ,IAAI,WAAW,EAAE,WADZ,IAAI,cAAc,QAAQ,OAAO,EACV,CAAC;AAC1C,OAAK,MAAM,OAAO;AAElB,OAAK,UAAU,IAAI,cACjB;GACE,UAAU,MAAM,YAAY,KAAK,QAAQ,MAAM,QAAQ;GACvD,oBAAoB,QAAQ,OAAO,IAAI;GACxC,EACD,KAAK,SACL,KAAK,QACN;AAED,OAAK,QAAQ,OAAO;;CAGtB,QAA6B,MAAS,SAAmC;EACvE,MAAM,YAAY,QAAQ,OAAO;AACjC,MAAI,EAAE,aAAa,KAAK,QAAQ,WAAW,KAAK,EAC9C;AAGF,OAAK,MAAM,QAAQ,cAAc,MAAM,SAAS,WAAW,KAAK,SAAS,CAAC;;CAG5E,QAAc;AACZ,OAAK,MAAM,OAAO;;CAGpB,UAAgB;AACd,OAAK,QAAQ,SAAS;AACtB,YAAU;AACV,OAAK,MAAM,SAAS;;CAGtB,aAAkC;AAChC,SAAO,KAAK,QAAQ,YAAY;;CAGlC,WAAW,OAA4B;AACrC,OAAK,QAAQ,WAAW,MAAM;;CAGhC,eAAqB;AACnB,OAAK,QAAQ,cAAc;;;AAI/B,IAAI,WAA0B;AAE9B,SAAgB,YAAoB;AAClC,KAAI,CAAC,SACH,OAAM,IAAI,MACR,0FACD;AAEH,QAAO;;AAGT,SAAgB,KAAK,OAAsB,EAAE,EAAQ;AACnD,KAAI,SACF;CAGF,MAAM,UAAW,WACd;CAEH,MAAM,YAAa,WAChB;AAEH,KAAI,CAAC,SAAS;AACZ,MAAI,MACF,qIAED;AACD;;AAGF,YAAW,IAAI,OAAO,MAAM,SAAS,aAAa,KAAK;;AAGzD,SAAgB,QAAc;AAC5B,KAAI,CAAC,SACH;AAGF,UAAS,SAAS;AAClB,YAAW;;AAGb,MAAa,UAAU;CACrB,MAA2B;AACzB,SAAO,UAAU,YAAY,IAAI;;CAGnC,IAAI,OAA4B;AAC9B,YAAU,WAAW,MAAM;;CAE9B;AAED,SAAgB,YAAY,cAA8C;AACxE,KAAI,CAAC,SACH;AAGF,KAAI,cAAc;AAChB,WAAS,WAAW,aAAa;AACjC;;AAGF,UAAS,cAAc;;AAGzB,SAAgB,QAAc;AAC5B,WAAU,OAAO"}
@@ -0,0 +1,15 @@
1
+ import { ConsentCategory, ConsentState, PluginKey } from "@interfere/types/sdk/plugins/manifest";
2
+ import { EventType } from "@interfere/types/sdk/envelope";
3
+
4
+ //#region src/internal/consent.d.ts
5
+ declare const DEFAULT_CONSENT: {
6
+ readonly analytics: true;
7
+ readonly replay: true;
8
+ };
9
+ declare function getPluginConsentCategory(key: PluginKey): ConsentCategory;
10
+ declare function isConsentAllowed(category: ConsentCategory, consentState: ConsentState | null): boolean;
11
+ declare function shouldCaptureEvent(type: EventType, consentState: ConsentState | null): boolean;
12
+ declare function resolveGrantedConsent(consent?: ConsentState): ConsentState;
13
+ declare function hasConsentChanged(current: ConsentState | null, next: ConsentState | null): boolean;
14
+ //#endregion
15
+ export { DEFAULT_CONSENT, getPluginConsentCategory, hasConsentChanged, isConsentAllowed, resolveGrantedConsent, shouldCaptureEvent };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consent.d.mts","names":[],"sources":["../../src/internal/consent.ts"],"mappings":";;;;cAQa,eAAA;EAAA,SAGoB,SAAA;EAAA,SAAA,MAAA;AAAA;AAAA,iBAYjB,wBAAA,CAAyB,GAAA,EAAK,SAAA,GAAY,eAAA;AAAA,iBAI1C,gBAAA,CACd,QAAA,EAAU,eAAA,EACV,YAAA,EAAc,YAAA;AAAA,iBASA,kBAAA,CACd,IAAA,EAAM,SAAA,EACN,YAAA,EAAc,YAAA;AAAA,iBAKA,qBAAA,CAAsB,OAAA,GAAU,YAAA,GAAe,YAAA;AAAA,iBAI/C,iBAAA,CACd,OAAA,EAAS,YAAA,SACT,IAAA,EAAM,YAAA"}
@@ -0,0 +1,25 @@
1
+ import { PLUGIN_MANIFEST } from "@interfere/types/sdk/plugins/manifest";
2
+ //#region src/internal/consent.ts
3
+ const DEFAULT_CONSENT = {
4
+ analytics: true,
5
+ replay: true
6
+ };
7
+ const PLUGIN_CONSENT_BY_KEY = Object.fromEntries(PLUGIN_MANIFEST.map((plugin) => [plugin.name, plugin.consentCategory]));
8
+ const EVENT_CONSENT_BY_TYPE = Object.fromEntries(PLUGIN_MANIFEST.flatMap((plugin) => plugin.events.map((event) => [event.name, plugin.consentCategory])));
9
+ function getPluginConsentCategory(key) {
10
+ return PLUGIN_CONSENT_BY_KEY[key];
11
+ }
12
+ function isConsentAllowed(category, consentState) {
13
+ return category === "necessary" || consentState === null || consentState[category] === true;
14
+ }
15
+ function shouldCaptureEvent(type, consentState) {
16
+ return isConsentAllowed(EVENT_CONSENT_BY_TYPE[type], consentState);
17
+ }
18
+ function resolveGrantedConsent(consent) {
19
+ return consent ?? { ...DEFAULT_CONSENT };
20
+ }
21
+ function hasConsentChanged(current, next) {
22
+ return current?.analytics !== next?.analytics || current?.replay !== next?.replay;
23
+ }
24
+ //#endregion
25
+ export { DEFAULT_CONSENT, getPluginConsentCategory, hasConsentChanged, isConsentAllowed, resolveGrantedConsent, shouldCaptureEvent };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consent.mjs","names":[],"sources":["../../src/internal/consent.ts"],"sourcesContent":["import type { EventType } from \"@interfere/types/sdk/envelope\";\nimport {\n type ConsentCategory,\n type ConsentState,\n PLUGIN_MANIFEST,\n type PluginKey,\n} from \"@interfere/types/sdk/plugins/manifest\";\n\nexport const DEFAULT_CONSENT = {\n analytics: true,\n replay: true,\n} as const satisfies ConsentState;\n\nconst PLUGIN_CONSENT_BY_KEY = Object.fromEntries(\n PLUGIN_MANIFEST.map((plugin) => [plugin.name, plugin.consentCategory])\n) as Record<PluginKey, ConsentCategory>;\n\nconst EVENT_CONSENT_BY_TYPE = Object.fromEntries(\n PLUGIN_MANIFEST.flatMap((plugin) =>\n plugin.events.map((event) => [event.name, plugin.consentCategory] as const)\n )\n) as Record<EventType, ConsentCategory>;\n\nexport function getPluginConsentCategory(key: PluginKey): ConsentCategory {\n return PLUGIN_CONSENT_BY_KEY[key];\n}\n\nexport function isConsentAllowed(\n category: ConsentCategory,\n consentState: ConsentState | null\n): boolean {\n return (\n category === \"necessary\" ||\n consentState === null ||\n consentState[category] === true\n );\n}\n\nexport function shouldCaptureEvent(\n type: EventType,\n consentState: ConsentState | null\n): boolean {\n return isConsentAllowed(EVENT_CONSENT_BY_TYPE[type], consentState);\n}\n\nexport function resolveGrantedConsent(consent?: ConsentState): ConsentState {\n return consent ?? { ...DEFAULT_CONSENT };\n}\n\nexport function hasConsentChanged(\n current: ConsentState | null,\n next: ConsentState | null\n): boolean {\n return (\n current?.analytics !== next?.analytics || current?.replay !== next?.replay\n );\n}\n"],"mappings":";;AAQA,MAAa,kBAAkB;CAC7B,WAAW;CACX,QAAQ;CACT;AAED,MAAM,wBAAwB,OAAO,YACnC,gBAAgB,KAAK,WAAW,CAAC,OAAO,MAAM,OAAO,gBAAgB,CAAC,CACvE;AAED,MAAM,wBAAwB,OAAO,YACnC,gBAAgB,SAAS,WACvB,OAAO,OAAO,KAAK,UAAU,CAAC,MAAM,MAAM,OAAO,gBAAgB,CAAU,CAC5E,CACF;AAED,SAAgB,yBAAyB,KAAiC;AACxE,QAAO,sBAAsB;;AAG/B,SAAgB,iBACd,UACA,cACS;AACT,QACE,aAAa,eACb,iBAAiB,QACjB,aAAa,cAAc;;AAI/B,SAAgB,mBACd,MACA,cACS;AACT,QAAO,iBAAiB,sBAAsB,OAAO,aAAa;;AAGpE,SAAgB,sBAAsB,SAAsC;AAC1E,QAAO,WAAW,EAAE,GAAG,iBAAiB;;AAG1C,SAAgB,kBACd,SACA,MACS;AACT,QACE,SAAS,cAAc,MAAM,aAAa,SAAS,WAAW,MAAM"}
@@ -0,0 +1,26 @@
1
+ import { PluginContext } from "../plugins/lib/types.mjs";
2
+ import { PluginOverrides } from "../plugins/lib/loader.mjs";
3
+ import { ConsentState } from "@interfere/types/sdk/plugins/manifest";
4
+ import { EventType } from "@interfere/types/sdk/envelope";
5
+
6
+ //#region src/internal/plugin-runtime.d.ts
7
+ declare class PluginRuntime {
8
+ private readonly activeCleanups;
9
+ private readonly context;
10
+ private readonly features;
11
+ private consentState;
12
+ private syncVersion;
13
+ constructor(context: PluginContext, overrides: PluginOverrides | undefined, initialConsent: ConsentState | undefined);
14
+ getConsent(): ConsentState | null;
15
+ setConsent(nextConsent?: ConsentState): void;
16
+ resetConsent(): void;
17
+ canCapture(type: EventType): boolean;
18
+ start(): void;
19
+ dispose(): void;
20
+ private shouldEnablePlugin;
21
+ private deactivate;
22
+ private activate;
23
+ private sync;
24
+ }
25
+ //#endregion
26
+ export { PluginRuntime };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-runtime.d.mts","names":[],"sources":["../../src/internal/plugin-runtime.ts"],"mappings":";;;;;;cAyBa,aAAA;EAAA,iBACM,cAAA;EAAA,iBACA,OAAA;EAAA,iBACA,QAAA;EAAA,QACT,YAAA;EAAA,QACA,WAAA;cAGN,OAAA,EAAS,aAAA,EACT,SAAA,EAAW,eAAA,cACX,cAAA,EAAgB,YAAA;EAOlB,UAAA,CAAA,GAAc,YAAA;EAId,UAAA,CAAW,WAAA,GAAc,YAAA;EAUzB,YAAA,CAAA;EASA,UAAA,CAAW,IAAA,EAAM,SAAA;EAIjB,KAAA,CAAA;EAWA,OAAA,CAAA;EAAA,QAMQ,kBAAA;EAAA,QAOA,UAAA;EAAA,QAeM,QAAA;EAAA,QAoBN,IAAA;AAAA"}
@@ -0,0 +1,85 @@
1
+ import { createLogger } from "../util/log.mjs";
2
+ import errorsPlugin from "../plugins/errors.mjs";
3
+ import { loadPlugin, resolveFeatures } from "../plugins/lib/loader.mjs";
4
+ import { getPluginConsentCategory, hasConsentChanged, isConsentAllowed, resolveGrantedConsent, shouldCaptureEvent } from "./consent.mjs";
5
+ import { PLUGIN_MANIFEST } from "@interfere/types/sdk/plugins/manifest";
6
+ //#region src/internal/plugin-runtime.ts
7
+ const log = createLogger("plugin-runtime");
8
+ var PluginRuntime = class {
9
+ activeCleanups = /* @__PURE__ */ new Map();
10
+ context;
11
+ features;
12
+ consentState;
13
+ syncVersion = 0;
14
+ constructor(context, overrides, initialConsent) {
15
+ this.context = context;
16
+ this.features = resolveFeatures(overrides);
17
+ this.consentState = initialConsent ?? null;
18
+ }
19
+ getConsent() {
20
+ return this.consentState;
21
+ }
22
+ setConsent(nextConsent) {
23
+ const nextState = resolveGrantedConsent(nextConsent);
24
+ if (!hasConsentChanged(this.consentState, nextState)) return;
25
+ this.consentState = nextState;
26
+ this.sync();
27
+ }
28
+ resetConsent() {
29
+ if (!hasConsentChanged(this.consentState, null)) return;
30
+ this.consentState = null;
31
+ this.sync();
32
+ }
33
+ canCapture(type) {
34
+ return shouldCaptureEvent(type, this.consentState);
35
+ }
36
+ start() {
37
+ if (this.features.errors) {
38
+ const cleanup = errorsPlugin.setup(this.context);
39
+ if (cleanup) this.activeCleanups.set("errors", cleanup);
40
+ }
41
+ this.sync();
42
+ }
43
+ dispose() {
44
+ for (const key of this.activeCleanups.keys()) this.deactivate(key);
45
+ }
46
+ shouldEnablePlugin(key) {
47
+ return this.features[key] && isConsentAllowed(getPluginConsentCategory(key), this.consentState);
48
+ }
49
+ deactivate(key) {
50
+ const cleanup = this.activeCleanups.get(key);
51
+ if (!cleanup) return;
52
+ try {
53
+ cleanup();
54
+ } catch {
55
+ log.warn("cleanup failed for %s", key);
56
+ }
57
+ this.activeCleanups.delete(key);
58
+ }
59
+ async activate(key) {
60
+ if (this.activeCleanups.has(key) || !this.shouldEnablePlugin(key)) return;
61
+ const version = this.syncVersion;
62
+ const cleanup = await loadPlugin(key, this.context);
63
+ if (!cleanup) return;
64
+ if (version !== this.syncVersion || !this.shouldEnablePlugin(key)) {
65
+ cleanup();
66
+ return;
67
+ }
68
+ this.activeCleanups.set(key, cleanup);
69
+ }
70
+ sync() {
71
+ this.syncVersion += 1;
72
+ for (const plugin of PLUGIN_MANIFEST) {
73
+ if (plugin.name === "errors") continue;
74
+ if (this.shouldEnablePlugin(plugin.name)) {
75
+ this.activate(plugin.name).catch(() => {
76
+ log.warn("non-critical plugin loading failed");
77
+ });
78
+ continue;
79
+ }
80
+ this.deactivate(plugin.name);
81
+ }
82
+ }
83
+ };
84
+ //#endregion
85
+ export { PluginRuntime };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-runtime.mjs","names":[],"sources":["../../src/internal/plugin-runtime.ts"],"sourcesContent":["import type { EventType } from \"@interfere/types/sdk/envelope\";\nimport {\n type ConsentState,\n PLUGIN_MANIFEST,\n type PluginKey,\n} from \"@interfere/types/sdk/plugins/manifest\";\n\nimport errorsPlugin from \"../plugins/errors.js\";\nimport {\n loadPlugin,\n type PluginOverrides,\n resolveFeatures,\n} from \"../plugins/lib/loader.js\";\nimport type { PluginCleanup, PluginContext } from \"../plugins/lib/types.js\";\nimport { createLogger } from \"../util/log.js\";\nimport {\n getPluginConsentCategory,\n hasConsentChanged,\n isConsentAllowed,\n resolveGrantedConsent,\n shouldCaptureEvent,\n} from \"./consent.js\";\n\nconst log = createLogger(\"plugin-runtime\");\n\nexport class PluginRuntime {\n private readonly activeCleanups = new Map<PluginKey, PluginCleanup>();\n private readonly context: PluginContext;\n private readonly features: Record<PluginKey, boolean>;\n private consentState: ConsentState | null;\n private syncVersion = 0;\n\n constructor(\n context: PluginContext,\n overrides: PluginOverrides | undefined,\n initialConsent: ConsentState | undefined\n ) {\n this.context = context;\n this.features = resolveFeatures(overrides);\n this.consentState = initialConsent ?? null;\n }\n\n getConsent(): ConsentState | null {\n return this.consentState;\n }\n\n setConsent(nextConsent?: ConsentState): void {\n const nextState = resolveGrantedConsent(nextConsent);\n if (!hasConsentChanged(this.consentState, nextState)) {\n return;\n }\n\n this.consentState = nextState;\n this.sync();\n }\n\n resetConsent(): void {\n if (!hasConsentChanged(this.consentState, null)) {\n return;\n }\n\n this.consentState = null;\n this.sync();\n }\n\n canCapture(type: EventType): boolean {\n return shouldCaptureEvent(type, this.consentState);\n }\n\n start(): void {\n if (this.features.errors) {\n const cleanup = errorsPlugin.setup(this.context);\n if (cleanup) {\n this.activeCleanups.set(\"errors\", cleanup);\n }\n }\n\n this.sync();\n }\n\n dispose(): void {\n for (const key of this.activeCleanups.keys()) {\n this.deactivate(key);\n }\n }\n\n private shouldEnablePlugin(key: PluginKey): boolean {\n return (\n this.features[key] &&\n isConsentAllowed(getPluginConsentCategory(key), this.consentState)\n );\n }\n\n private deactivate(key: PluginKey): void {\n const cleanup = this.activeCleanups.get(key);\n if (!cleanup) {\n return;\n }\n\n try {\n cleanup();\n } catch {\n log.warn(\"cleanup failed for %s\", key);\n }\n\n this.activeCleanups.delete(key);\n }\n\n private async activate(key: PluginKey): Promise<void> {\n if (this.activeCleanups.has(key) || !this.shouldEnablePlugin(key)) {\n return;\n }\n\n const version = this.syncVersion;\n const cleanup = await loadPlugin(key, this.context);\n if (!cleanup) {\n return;\n }\n\n const staleSync = version !== this.syncVersion;\n if (staleSync || !this.shouldEnablePlugin(key)) {\n cleanup();\n return;\n }\n\n this.activeCleanups.set(key, cleanup);\n }\n\n private sync(): void {\n this.syncVersion += 1;\n\n for (const plugin of PLUGIN_MANIFEST) {\n if (plugin.name === \"errors\") {\n continue;\n }\n\n if (this.shouldEnablePlugin(plugin.name)) {\n this.activate(plugin.name).catch(() => {\n log.warn(\"non-critical plugin loading failed\");\n });\n continue;\n }\n\n this.deactivate(plugin.name);\n }\n }\n}\n"],"mappings":";;;;;;AAuBA,MAAM,MAAM,aAAa,iBAAiB;AAE1C,IAAa,gBAAb,MAA2B;CACzB,iCAAkC,IAAI,KAA+B;CACrE;CACA;CACA;CACA,cAAsB;CAEtB,YACE,SACA,WACA,gBACA;AACA,OAAK,UAAU;AACf,OAAK,WAAW,gBAAgB,UAAU;AAC1C,OAAK,eAAe,kBAAkB;;CAGxC,aAAkC;AAChC,SAAO,KAAK;;CAGd,WAAW,aAAkC;EAC3C,MAAM,YAAY,sBAAsB,YAAY;AACpD,MAAI,CAAC,kBAAkB,KAAK,cAAc,UAAU,CAClD;AAGF,OAAK,eAAe;AACpB,OAAK,MAAM;;CAGb,eAAqB;AACnB,MAAI,CAAC,kBAAkB,KAAK,cAAc,KAAK,CAC7C;AAGF,OAAK,eAAe;AACpB,OAAK,MAAM;;CAGb,WAAW,MAA0B;AACnC,SAAO,mBAAmB,MAAM,KAAK,aAAa;;CAGpD,QAAc;AACZ,MAAI,KAAK,SAAS,QAAQ;GACxB,MAAM,UAAU,aAAa,MAAM,KAAK,QAAQ;AAChD,OAAI,QACF,MAAK,eAAe,IAAI,UAAU,QAAQ;;AAI9C,OAAK,MAAM;;CAGb,UAAgB;AACd,OAAK,MAAM,OAAO,KAAK,eAAe,MAAM,CAC1C,MAAK,WAAW,IAAI;;CAIxB,mBAA2B,KAAyB;AAClD,SACE,KAAK,SAAS,QACd,iBAAiB,yBAAyB,IAAI,EAAE,KAAK,aAAa;;CAItE,WAAmB,KAAsB;EACvC,MAAM,UAAU,KAAK,eAAe,IAAI,IAAI;AAC5C,MAAI,CAAC,QACH;AAGF,MAAI;AACF,YAAS;UACH;AACN,OAAI,KAAK,yBAAyB,IAAI;;AAGxC,OAAK,eAAe,OAAO,IAAI;;CAGjC,MAAc,SAAS,KAA+B;AACpD,MAAI,KAAK,eAAe,IAAI,IAAI,IAAI,CAAC,KAAK,mBAAmB,IAAI,CAC/D;EAGF,MAAM,UAAU,KAAK;EACrB,MAAM,UAAU,MAAM,WAAW,KAAK,KAAK,QAAQ;AACnD,MAAI,CAAC,QACH;AAIF,MADkB,YAAY,KAAK,eAClB,CAAC,KAAK,mBAAmB,IAAI,EAAE;AAC9C,YAAS;AACT;;AAGF,OAAK,eAAe,IAAI,KAAK,QAAQ;;CAGvC,OAAqB;AACnB,OAAK,eAAe;AAEpB,OAAK,MAAM,UAAU,iBAAiB;AACpC,OAAI,OAAO,SAAS,SAClB;AAGF,OAAI,KAAK,mBAAmB,OAAO,KAAK,EAAE;AACxC,SAAK,SAAS,OAAO,KAAK,CAAC,YAAY;AACrC,SAAI,KAAK,qCAAqC;MAC9C;AACF;;AAGF,QAAK,WAAW,OAAO,KAAK"}
@@ -0,0 +1,6 @@
1
+ import { Plugin } from "./lib/types.mjs";
2
+
3
+ //#region src/plugins/fingerprint.d.ts
4
+ declare const fingerprintPlugin: Plugin;
5
+ //#endregion
6
+ export { fingerprintPlugin as default, fingerprintPlugin };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fingerprint.d.mts","names":[],"sources":["../../src/plugins/fingerprint.ts"],"mappings":";;;cAGa,iBAAA,EAAmB,MAAA"}
@@ -0,0 +1,13 @@
1
+ import { initVisitor, resetVisitor } from "../tracking/visitor.mjs";
2
+ //#region src/plugins/fingerprint.ts
3
+ const fingerprintPlugin = {
4
+ name: "fingerprint",
5
+ setup() {
6
+ initVisitor();
7
+ return () => {
8
+ resetVisitor();
9
+ };
10
+ }
11
+ };
12
+ //#endregion
13
+ export { fingerprintPlugin as default, fingerprintPlugin };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fingerprint.mjs","names":[],"sources":["../../src/plugins/fingerprint.ts"],"sourcesContent":["import { initVisitor, resetVisitor } from \"../tracking/visitor.js\";\nimport type { Plugin } from \"./lib/types.js\";\n\nexport const fingerprintPlugin: Plugin = {\n name: \"fingerprint\",\n\n setup() {\n initVisitor();\n\n return () => {\n resetVisitor();\n };\n },\n};\n\nexport default fingerprintPlugin;\n"],"mappings":";;AAGA,MAAa,oBAA4B;CACvC,MAAM;CAEN,QAAQ;AACN,eAAa;AAEb,eAAa;AACX,iBAAc;;;CAGnB"}
@@ -4,6 +4,7 @@ import { PluginKey } from "@interfere/types/sdk/plugins/manifest";
4
4
  //#region src/plugins/lib/loader.d.ts
5
5
  type PluginOverrides = Partial<Record<PluginKey, boolean>>;
6
6
  declare function resolveFeatures(overrides?: PluginOverrides): Record<PluginKey, boolean>;
7
+ declare function loadPlugin(key: PluginKey, context: PluginContext): Promise<PluginCleanup | null>;
7
8
  declare function loadPlugins(overrides: PluginOverrides | undefined, context: PluginContext): Promise<PluginCleanup[]>;
8
9
  //#endregion
9
- export { PluginOverrides, loadPlugins, resolveFeatures };
10
+ export { PluginOverrides, loadPlugin, loadPlugins, resolveFeatures };
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.mts","names":[],"sources":["../../../src/plugins/lib/loader.ts"],"mappings":";;;;KAuBY,eAAA,GAAkB,OAAA,CAAQ,MAAA,CAAO,SAAA;AAAA,iBAE7B,eAAA,CACd,SAAA,GAAY,eAAA,GACX,MAAA,CAAO,SAAA;AAAA,iBAUY,WAAA,CACpB,SAAA,EAAW,eAAA,cACX,OAAA,EAAS,aAAA,GACR,OAAA,CAAQ,aAAA"}
1
+ {"version":3,"file":"loader.d.mts","names":[],"sources":["../../../src/plugins/lib/loader.ts"],"mappings":";;;;KAwBY,eAAA,GAAkB,OAAA,CAAQ,MAAA,CAAO,SAAA;AAAA,iBAE7B,eAAA,CACd,SAAA,GAAY,eAAA,GACX,MAAA,CAAO,SAAA;AAAA,iBAUY,UAAA,CACpB,GAAA,EAAK,SAAA,EACL,OAAA,EAAS,aAAA,GACR,OAAA,CAAQ,aAAA;AAAA,iBAkBW,WAAA,CACpB,SAAA,EAAW,eAAA,cACX,OAAA,EAAS,aAAA,GACR,OAAA,CAAQ,aAAA"}
@@ -4,6 +4,7 @@ import { PLUGIN_MANIFEST } from "@interfere/types/sdk/plugins/manifest";
4
4
  const log = createLogger("plugins");
5
5
  const LOADERS = {
6
6
  errors: () => import("../errors.mjs"),
7
+ fingerprint: () => import("../fingerprint.mjs"),
7
8
  pageEvents: () => import("../pages.mjs"),
8
9
  rageClick: () => import("../rage-clicks.mjs"),
9
10
  replay: () => import("../replay.mjs")
@@ -18,30 +19,25 @@ function resolveFeatures(overrides) {
18
19
  function resolvePlugin(mod) {
19
20
  return "default" in mod && typeof mod.default.setup === "function" ? mod.default : mod;
20
21
  }
22
+ async function loadPlugin(key, context) {
23
+ const loader = LOADERS[key];
24
+ if (!loader) return null;
25
+ try {
26
+ const cleanup = resolvePlugin(await loader()).setup(context);
27
+ log.debug("loaded %s", key);
28
+ return typeof cleanup === "function" ? cleanup : null;
29
+ } catch {
30
+ log.error("failed to load plugin %s", key);
31
+ return null;
32
+ }
33
+ }
21
34
  async function loadPlugins(overrides, context) {
22
35
  const resolved = {
23
36
  ...DEFAULTS,
24
37
  ...overrides
25
38
  };
26
- const entries = Object.entries(resolved).filter(([key, enabled]) => enabled && key in LOADERS);
27
- const modules = await Promise.all(entries.map(async ([key]) => {
28
- const loader = LOADERS[key];
29
- if (!loader) return null;
30
- try {
31
- return await loader();
32
- } catch {
33
- log.error("failed to load plugin %s", key);
34
- return null;
35
- }
36
- }));
37
- const cleanups = [];
38
- for (const [i, mod] of modules.entries()) {
39
- if (!mod) continue;
40
- const cleanup = resolvePlugin(mod).setup(context);
41
- if (typeof cleanup === "function") cleanups.push(cleanup);
42
- log.debug("loaded %s", entries[i]?.[0]);
43
- }
44
- return cleanups;
39
+ const keys = Object.entries(resolved).filter(([key, enabled]) => enabled && key in LOADERS).map(([key]) => key);
40
+ return (await Promise.all(keys.map(async (key) => loadPlugin(key, context)))).filter((cleanup) => cleanup !== null);
45
41
  }
46
42
  //#endregion
47
- export { loadPlugins, resolveFeatures };
43
+ export { loadPlugin, loadPlugins, resolveFeatures };
@@ -1 +1 @@
1
- {"version":3,"file":"loader.mjs","names":[],"sources":["../../../src/plugins/lib/loader.ts"],"sourcesContent":["import {\n PLUGIN_MANIFEST,\n type PluginKey,\n} from \"@interfere/types/sdk/plugins/manifest\";\n\nimport { createLogger } from \"../../util/log.js\";\nimport type { Plugin, PluginCleanup, PluginContext } from \"./types.js\";\n\nconst log = createLogger(\"plugins\");\n\ntype PluginLoader = () => Promise<{ default: Plugin } | Plugin>;\n\nconst LOADERS: Partial<Record<PluginKey, PluginLoader>> = {\n errors: () => import(\"../errors.js\"),\n pageEvents: () => import(\"../pages.js\"),\n rageClick: () => import(\"../rage-clicks.js\"),\n replay: () => import(\"../replay.js\"),\n};\n\nconst DEFAULTS: Record<PluginKey, boolean> = Object.fromEntries(\n PLUGIN_MANIFEST.map((p) => [p.name, p.defaultEnabled])\n) as Record<PluginKey, boolean>;\n\nexport type PluginOverrides = Partial<Record<PluginKey, boolean>>;\n\nexport function resolveFeatures(\n overrides?: PluginOverrides\n): Record<PluginKey, boolean> {\n return { ...DEFAULTS, ...overrides };\n}\n\nfunction resolvePlugin(mod: { default: Plugin } | Plugin): Plugin {\n return \"default\" in mod && typeof (mod.default as Plugin).setup === \"function\"\n ? mod.default\n : (mod as Plugin);\n}\n\nexport async function loadPlugins(\n overrides: PluginOverrides | undefined,\n context: PluginContext\n): Promise<PluginCleanup[]> {\n const resolved = { ...DEFAULTS, ...overrides };\n const entries = (Object.entries(resolved) as [PluginKey, boolean][]).filter(\n ([key, enabled]) => enabled && key in LOADERS\n );\n\n const modules = await Promise.all(\n entries.map(async ([key]) => {\n const loader = LOADERS[key];\n if (!loader) {\n return null;\n }\n try {\n return await loader();\n } catch {\n log.error(\"failed to load plugin %s\", key);\n return null;\n }\n })\n );\n\n const cleanups: PluginCleanup[] = [];\n\n for (const [i, mod] of modules.entries()) {\n if (!mod) {\n continue;\n }\n const plugin = resolvePlugin(mod);\n const cleanup = plugin.setup(context);\n if (typeof cleanup === \"function\") {\n cleanups.push(cleanup);\n }\n log.debug(\"loaded %s\", entries[i]?.[0]);\n }\n\n return cleanups;\n}\n"],"mappings":";;;AAQA,MAAM,MAAM,aAAa,UAAU;AAInC,MAAM,UAAoD;CACxD,cAAc,OAAO;CACrB,kBAAkB,OAAO;CACzB,iBAAiB,OAAO;CACxB,cAAc,OAAO;CACtB;AAED,MAAM,WAAuC,OAAO,YAClD,gBAAgB,KAAK,MAAM,CAAC,EAAE,MAAM,EAAE,eAAe,CAAC,CACvD;AAID,SAAgB,gBACd,WAC4B;AAC5B,QAAO;EAAE,GAAG;EAAU,GAAG;EAAW;;AAGtC,SAAS,cAAc,KAA2C;AAChE,QAAO,aAAa,OAAO,OAAQ,IAAI,QAAmB,UAAU,aAChE,IAAI,UACH;;AAGP,eAAsB,YACpB,WACA,SAC0B;CAC1B,MAAM,WAAW;EAAE,GAAG;EAAU,GAAG;EAAW;CAC9C,MAAM,UAAW,OAAO,QAAQ,SAAS,CAA4B,QAClE,CAAC,KAAK,aAAa,WAAW,OAAO,QACvC;CAED,MAAM,UAAU,MAAM,QAAQ,IAC5B,QAAQ,IAAI,OAAO,CAAC,SAAS;EAC3B,MAAM,SAAS,QAAQ;AACvB,MAAI,CAAC,OACH,QAAO;AAET,MAAI;AACF,UAAO,MAAM,QAAQ;UACf;AACN,OAAI,MAAM,4BAA4B,IAAI;AAC1C,UAAO;;GAET,CACH;CAED,MAAM,WAA4B,EAAE;AAEpC,MAAK,MAAM,CAAC,GAAG,QAAQ,QAAQ,SAAS,EAAE;AACxC,MAAI,CAAC,IACH;EAGF,MAAM,UADS,cAAc,IAAI,CACV,MAAM,QAAQ;AACrC,MAAI,OAAO,YAAY,WACrB,UAAS,KAAK,QAAQ;AAExB,MAAI,MAAM,aAAa,QAAQ,KAAK,GAAG;;AAGzC,QAAO"}
1
+ {"version":3,"file":"loader.mjs","names":[],"sources":["../../../src/plugins/lib/loader.ts"],"sourcesContent":["import {\n PLUGIN_MANIFEST,\n type PluginKey,\n} from \"@interfere/types/sdk/plugins/manifest\";\n\nimport { createLogger } from \"../../util/log.js\";\nimport type { Plugin, PluginCleanup, PluginContext } from \"./types.js\";\n\nconst log = createLogger(\"plugins\");\n\ntype PluginLoader = () => Promise<{ default: Plugin } | Plugin>;\n\nconst LOADERS: Partial<Record<PluginKey, PluginLoader>> = {\n errors: () => import(\"../errors.js\"),\n fingerprint: () => import(\"../fingerprint.js\"),\n pageEvents: () => import(\"../pages.js\"),\n rageClick: () => import(\"../rage-clicks.js\"),\n replay: () => import(\"../replay.js\"),\n};\n\nconst DEFAULTS: Record<PluginKey, boolean> = Object.fromEntries(\n PLUGIN_MANIFEST.map((p) => [p.name, p.defaultEnabled])\n) as Record<PluginKey, boolean>;\n\nexport type PluginOverrides = Partial<Record<PluginKey, boolean>>;\n\nexport function resolveFeatures(\n overrides?: PluginOverrides\n): Record<PluginKey, boolean> {\n return { ...DEFAULTS, ...overrides };\n}\n\nfunction resolvePlugin(mod: { default: Plugin } | Plugin): Plugin {\n return \"default\" in mod && typeof (mod.default as Plugin).setup === \"function\"\n ? mod.default\n : (mod as Plugin);\n}\n\nexport async function loadPlugin(\n key: PluginKey,\n context: PluginContext\n): Promise<PluginCleanup | null> {\n const loader = LOADERS[key];\n if (!loader) {\n return null;\n }\n\n try {\n const mod = await loader();\n const plugin = resolvePlugin(mod);\n const cleanup = plugin.setup(context);\n log.debug(\"loaded %s\", key);\n return typeof cleanup === \"function\" ? cleanup : null;\n } catch {\n log.error(\"failed to load plugin %s\", key);\n return null;\n }\n}\n\nexport async function loadPlugins(\n overrides: PluginOverrides | undefined,\n context: PluginContext\n): Promise<PluginCleanup[]> {\n const resolved = { ...DEFAULTS, ...overrides };\n const keys = (Object.entries(resolved) as [PluginKey, boolean][])\n .filter(([key, enabled]) => enabled && key in LOADERS)\n .map(([key]) => key);\n\n const cleanups = await Promise.all(\n keys.map(async (key) => loadPlugin(key, context))\n );\n return cleanups.filter((cleanup) => cleanup !== null);\n}\n"],"mappings":";;;AAQA,MAAM,MAAM,aAAa,UAAU;AAInC,MAAM,UAAoD;CACxD,cAAc,OAAO;CACrB,mBAAmB,OAAO;CAC1B,kBAAkB,OAAO;CACzB,iBAAiB,OAAO;CACxB,cAAc,OAAO;CACtB;AAED,MAAM,WAAuC,OAAO,YAClD,gBAAgB,KAAK,MAAM,CAAC,EAAE,MAAM,EAAE,eAAe,CAAC,CACvD;AAID,SAAgB,gBACd,WAC4B;AAC5B,QAAO;EAAE,GAAG;EAAU,GAAG;EAAW;;AAGtC,SAAS,cAAc,KAA2C;AAChE,QAAO,aAAa,OAAO,OAAQ,IAAI,QAAmB,UAAU,aAChE,IAAI,UACH;;AAGP,eAAsB,WACpB,KACA,SAC+B;CAC/B,MAAM,SAAS,QAAQ;AACvB,KAAI,CAAC,OACH,QAAO;AAGT,KAAI;EAGF,MAAM,UADS,cADH,MAAM,QAAQ,CACO,CACV,MAAM,QAAQ;AACrC,MAAI,MAAM,aAAa,IAAI;AAC3B,SAAO,OAAO,YAAY,aAAa,UAAU;SAC3C;AACN,MAAI,MAAM,4BAA4B,IAAI;AAC1C,SAAO;;;AAIX,eAAsB,YACpB,WACA,SAC0B;CAC1B,MAAM,WAAW;EAAE,GAAG;EAAU,GAAG;EAAW;CAC9C,MAAM,OAAQ,OAAO,QAAQ,SAAS,CACnC,QAAQ,CAAC,KAAK,aAAa,WAAW,OAAO,QAAQ,CACrD,KAAK,CAAC,SAAS,IAAI;AAKtB,SAHiB,MAAM,QAAQ,IAC7B,KAAK,IAAI,OAAO,QAAQ,WAAW,KAAK,QAAQ,CAAC,CAClD,EACe,QAAQ,YAAY,YAAY,KAAK"}
@@ -1,10 +1,13 @@
1
1
  import { PropsWithChildren, ReactNode } from "react";
2
- import { EnvelopePayload, EventType } from "@interfere/types/sdk/envelope";
2
+ import { ConsentState } from "@interfere/types/sdk/plugins/manifest";
3
3
  import { IdentifyParams } from "@interfere/types/sdk/identify";
4
4
 
5
5
  //#region src/provider.d.ts
6
6
  interface InterfereContextValue {
7
- capture<T extends EventType>(type: T, payload: EnvelopePayload<T>): void;
7
+ consent: {
8
+ get(): ConsentState | null;
9
+ set(state?: ConsentState): void;
10
+ };
8
11
  identity: {
9
12
  get(): IdentifyParams | null;
10
13
  set(params: IdentifyParams): void;
@@ -14,9 +17,13 @@ interface InterfereContextValue {
14
17
  getWindowId(): string | null;
15
18
  };
16
19
  }
20
+ interface InterfereProviderProps extends PropsWithChildren {
21
+ consent?: ConsentState;
22
+ }
17
23
  declare function InterfereProvider({
18
- children
19
- }: PropsWithChildren): ReactNode;
24
+ children,
25
+ consent
26
+ }: InterfereProviderProps): ReactNode;
20
27
  declare function useInterfere(): InterfereContextValue;
21
28
  declare function useSession(): string | null;
22
29
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.mts","names":[],"sources":["../src/provider.tsx"],"mappings":";;;;;UAeU,qBAAA;EACR,OAAA,WAAkB,SAAA,EAAW,IAAA,EAAM,CAAA,EAAG,OAAA,EAAS,eAAA,CAAgB,CAAA;EAC/D,QAAA;IACE,GAAA,IAAO,cAAA;IACP,GAAA,CAAI,MAAA,EAAQ,cAAA;EAAA;EAEd,OAAA;IACE,KAAA;IACA,WAAA;EAAA;AAAA;AAAA,iBAMY,iBAAA,CAAA;EAAoB;AAAA,GAAY,iBAAA,GAAoB,SAAA;AAAA,iBAUpD,YAAA,CAAA,GAAgB,qBAAA;AAAA,iBAQhB,UAAA,CAAA"}
1
+ {"version":3,"file":"provider.d.mts","names":[],"sources":["../src/provider.tsx"],"mappings":";;;;;UAgBU,qBAAA;EACR,OAAA;IACE,GAAA,IAAO,YAAA;IACP,GAAA,CAAI,KAAA,GAAQ,YAAA;EAAA;EAEd,QAAA;IACE,GAAA,IAAO,cAAA;IACP,GAAA,CAAI,MAAA,EAAQ,cAAA;EAAA;EAEd,OAAA;IACE,KAAA;IACA,WAAA;EAAA;AAAA;AAAA,UAMM,sBAAA,SAA+B,iBAAA;EACvC,OAAA,GAAU,YAAA;AAAA;AAAA,iBAGI,iBAAA,CAAA;EACd,QAAA;EACA;AAAA,GACC,sBAAA,GAAyB,SAAA;AAAA,iBAcZ,YAAA,CAAA,GAAgB,qBAAA;AAAA,iBAQhB,UAAA,CAAA"}
package/dist/provider.mjs CHANGED
@@ -1,14 +1,17 @@
1
1
  "use client";
2
2
  import { identity, session } from "./tracking/api.mjs";
3
- import { capture } from "./internal/client.mjs";
4
- import { createContext, useContext } from "react";
3
+ import { consent, syncConsent } from "./internal/client.mjs";
4
+ import { createContext, useContext, useEffect } from "react";
5
5
  import { jsx } from "react/jsx-runtime";
6
6
  //#region src/provider.tsx
7
7
  const InterfereContext = createContext(null);
8
- function InterfereProvider({ children }) {
8
+ function InterfereProvider({ children, consent: consent$1 }) {
9
+ useEffect(() => {
10
+ syncConsent(consent$1);
11
+ }, [consent$1]);
9
12
  return /* @__PURE__ */ jsx(InterfereContext, {
10
13
  value: {
11
- capture,
14
+ consent,
12
15
  identity,
13
16
  session
14
17
  },
@@ -1 +1 @@
1
- {"version":3,"file":"provider.mjs","names":[],"sources":["../src/provider.tsx"],"sourcesContent":["\"use client\";\n\nimport type { EnvelopePayload, EventType } from \"@interfere/types/sdk/envelope\";\nimport type { IdentifyParams } from \"@interfere/types/sdk/identify\";\n\nimport {\n createContext,\n type PropsWithChildren,\n type ReactNode,\n useContext,\n} from \"react\";\n\nimport { capture } from \"./internal/client.js\";\nimport { identity, session } from \"./tracking/api.js\";\n\ninterface InterfereContextValue {\n capture<T extends EventType>(type: T, payload: EnvelopePayload<T>): void;\n identity: {\n get(): IdentifyParams | null;\n set(params: IdentifyParams): void;\n };\n session: {\n getId(): string | null;\n getWindowId(): string | null;\n };\n}\n\nconst InterfereContext = createContext<InterfereContextValue | null>(null);\n\nexport function InterfereProvider({ children }: PropsWithChildren): ReactNode {\n const value: InterfereContextValue = {\n capture,\n identity,\n session,\n };\n\n return <InterfereContext value={value}>{children}</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"],"mappings":";;;;;;AA2BA,MAAM,mBAAmB,cAA4C,KAAK;AAE1E,SAAgB,kBAAkB,EAAE,YAA0C;AAO5E,QAAO,oBAAC,kBAAD;EAAkB,OANY;GACnC;GACA;GACA;GACD;EAEuC;EAA4B,CAAA;;AAGtE,SAAgB,eAAsC;CACpD,MAAM,MAAM,WAAW,iBAAiB;AACxC,KAAI,CAAC,IACH,OAAM,IAAI,MAAM,uDAAuD;AAEzE,QAAO;;AAGT,SAAgB,aAA4B;AAC1C,QAAO,cAAc,CAAC,QAAQ,OAAO"}
1
+ {"version":3,"file":"provider.mjs","names":["consent","sdkConsent"],"sources":["../src/provider.tsx"],"sourcesContent":["\"use client\";\n\nimport type { IdentifyParams } from \"@interfere/types/sdk/identify\";\nimport type { ConsentState } from \"@interfere/types/sdk/plugins/manifest\";\n\nimport {\n createContext,\n type PropsWithChildren,\n type ReactNode,\n useContext,\n useEffect,\n} from \"react\";\n\nimport { consent as sdkConsent, syncConsent } from \"./internal/client.js\";\nimport { identity, session } from \"./tracking/api.js\";\n\ninterface InterfereContextValue {\n consent: {\n get(): ConsentState | null;\n set(state?: ConsentState): void;\n };\n identity: {\n get(): IdentifyParams | null;\n set(params: IdentifyParams): void;\n };\n session: {\n getId(): string | null;\n getWindowId(): string | null;\n };\n}\n\nconst InterfereContext = createContext<InterfereContextValue | null>(null);\n\ninterface InterfereProviderProps extends PropsWithChildren {\n consent?: ConsentState;\n}\n\nexport function InterfereProvider({\n children,\n consent,\n}: InterfereProviderProps): ReactNode {\n useEffect(() => {\n syncConsent(consent);\n }, [consent]);\n\n const value: InterfereContextValue = {\n consent: sdkConsent,\n identity,\n session,\n };\n\n return <InterfereContext value={value}>{children}</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"],"mappings":";;;;;;AA+BA,MAAM,mBAAmB,cAA4C,KAAK;AAM1E,SAAgB,kBAAkB,EAChC,UACA,SAAA,aACoC;AACpC,iBAAgB;AACd,cAAYA,UAAQ;IACnB,CAACA,UAAQ,CAAC;AAQb,QAAO,oBAAC,kBAAD;EAAkB,OANY;GAC1BC;GACT;GACA;GACD;EAEuC;EAA4B,CAAA;;AAGtE,SAAgB,eAAsC;CACpD,MAAM,MAAM,WAAW,iBAAiB;AACxC,KAAI,CAAC,IACH,OAAM,IAAI,MAAM,uDAAuD;AAEzE,QAAO;;AAGT,SAAgB,aAA4B;AAC1C,QAAO,cAAc,CAAC,QAAQ,OAAO"}
@@ -1,13 +1,8 @@
1
- import { PluginOverrides } from "../plugins/lib/loader.mjs";
2
1
  import { IngestTarget } from "../transport/http.mjs";
3
2
  import { IdentifyParams } from "@interfere/types/sdk/identify";
4
3
 
5
4
  //#region src/tracking/api.d.ts
6
- /**
7
- * Bootstraps the tracking subsystem: creates the session manager,
8
- * resolves the session target, and optionally starts Fingerprint Pro.
9
- */
10
- declare function bootstrap(sessionTarget: IngestTarget, plugins?: PluginOverrides): void;
5
+ declare function bootstrap(sessionTarget: IngestTarget): void;
11
6
  declare const session: {
12
7
  getId(): string | null;
13
8
  getWindowId(): string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.mts","names":[],"sources":["../../src/tracking/api.ts"],"mappings":";;;;;;;AA4DA;;iBAAgB,SAAA,CACd,aAAA,EAAe,YAAA,EACf,OAAA,GAAU,eAAA;AAAA,cAgBC,OAAA;EAQZ,KAAA;EAAA,WAAA;AAAA;AAAA,cAEY,QAAA;SACJ,cAAA;cAIK,cAAA;;;iBA4BE,QAAA,CAAA"}
1
+ {"version":3,"file":"api.d.mts","names":[],"sources":["../../src/tracking/api.ts"],"mappings":";;;;iBAuCgB,SAAA,CAAU,aAAA,EAAe,YAAA;AAAA,cAU5B,OAAA;EAQZ,KAAA;EAAA,WAAA;AAAA;AAAA,cAEY,QAAA;SACJ,cAAA;cAIK,cAAA;;;iBA4BE,QAAA,CAAA"}
@@ -1,8 +1,7 @@
1
1
  import { createLogger } from "../util/log.mjs";
2
- import { resolveFeatures } from "../plugins/lib/loader.mjs";
2
+ import { getVisitorId, whenVisitorReady } from "./visitor.mjs";
3
+ import { buildHeaders } from "../transport/http.mjs";
3
4
  import { SessionManager } from "./session.mjs";
4
- import { getVisitorId, initVisitor, whenVisitorReady } from "./visitor.mjs";
5
- import { make } from "tctx/traceparent";
6
5
  //#region src/tracking/api.ts
7
6
  const log = createLogger("tracking");
8
7
  let mgr = null;
@@ -10,15 +9,9 @@ let target = null;
10
9
  let currentIdentity = null;
11
10
  let identifiedSessionId = null;
12
11
  function fire(url, method, headers, body) {
13
- const h = Object.fromEntries(headers.entries());
14
- h.traceparent = String(make());
15
- const sid = mgr?.getSessionId();
16
- if (sid) h["x-interfere-session"] = sid;
17
- const vid = getVisitorId();
18
- if (vid) h["x-interfere-visitor"] = vid;
19
12
  fetch(url, {
20
13
  method,
21
- headers: h,
14
+ headers: buildHeaders(headers),
22
15
  body: JSON.stringify(body),
23
16
  keepalive: true
24
17
  }).catch(() => {
@@ -34,13 +27,8 @@ async function onRotate(sessionId) {
34
27
  visitorId
35
28
  });
36
29
  }
37
- /**
38
- * Bootstraps the tracking subsystem: creates the session manager,
39
- * resolves the session target, and optionally starts Fingerprint Pro.
40
- */
41
- function bootstrap(sessionTarget, plugins) {
30
+ function bootstrap(sessionTarget) {
42
31
  target = sessionTarget;
43
- if (resolveFeatures(plugins).fingerprint) initVisitor();
44
32
  mgr = new SessionManager((id) => {
45
33
  onRotate(id).catch(() => {});
46
34
  });
@@ -1 +1 @@
1
- {"version":3,"file":"api.mjs","names":[],"sources":["../../src/tracking/api.ts"],"sourcesContent":["import type { IdentifyParams } from \"@interfere/types/sdk/identify\";\n\nimport { make } from \"tctx/traceparent\";\n\nimport type { PluginOverrides } from \"../plugins/lib/loader.js\";\nimport { resolveFeatures } from \"../plugins/lib/loader.js\";\nimport type { IngestTarget } from \"../transport/http.js\";\nimport { createLogger } from \"../util/log.js\";\nimport { SessionManager } from \"./session.js\";\nimport { getVisitorId, initVisitor, whenVisitorReady } from \"./visitor.js\";\n\nconst log = createLogger(\"tracking\");\n\nlet mgr: SessionManager | null = null;\nlet target: IngestTarget | null = null;\nlet currentIdentity: IdentifyParams | null = null;\nlet identifiedSessionId: string | null = null;\n\nfunction fire(\n url: string,\n method: \"POST\" | \"PUT\",\n headers: Headers,\n body: unknown\n): void {\n const h: Record<string, string> = Object.fromEntries(headers.entries());\n h.traceparent = String(make());\n\n const sid = mgr?.getSessionId();\n if (sid) {\n h[\"x-interfere-session\"] = sid;\n }\n\n const vid = getVisitorId();\n if (vid) {\n h[\"x-interfere-visitor\"] = vid;\n }\n\n fetch(url, {\n method,\n headers: h,\n body: JSON.stringify(body),\n keepalive: true,\n }).catch(() => {\n log.warn(\"fire-and-forget %s failed\", method);\n });\n}\n\nasync function onRotate(sessionId: string): Promise<void> {\n if (!target) {\n return;\n }\n const visitorId = await whenVisitorReady();\n log.debug(\"POST session %s (visitor=%s)\", sessionId, visitorId ?? \"pending\");\n fire(target.url, \"POST\", target.headers, { sessionId, visitorId });\n}\n\n/**\n * Bootstraps the tracking subsystem: creates the session manager,\n * resolves the session target, and optionally starts Fingerprint Pro.\n */\nexport function bootstrap(\n sessionTarget: IngestTarget,\n plugins?: PluginOverrides\n): void {\n target = sessionTarget;\n\n const features = resolveFeatures(plugins);\n if (features.fingerprint) {\n initVisitor();\n }\n\n mgr = new SessionManager((id) => {\n onRotate(id).catch(() => {\n /* best-effort */\n });\n });\n}\n\nexport const session = {\n getId(): string | null {\n return mgr?.getSessionId() ?? null;\n },\n\n getWindowId(): string | null {\n return mgr?.getWindowId() ?? null;\n },\n};\n\nexport const identity = {\n get(): IdentifyParams | null {\n return currentIdentity;\n },\n\n set(params: IdentifyParams): void {\n if (!(mgr && target)) {\n return;\n }\n const sessionId = mgr.getSessionId();\n if (identifiedSessionId === sessionId) {\n log.debug(\"skipped, already identified for session %s\", sessionId);\n return;\n }\n\n currentIdentity = params;\n identifiedSessionId = sessionId;\n\n const visitorId = getVisitorId();\n log.info(\"PUT session %s → user %s\", sessionId, params.identifier);\n fire(target.url, \"PUT\", target.headers, {\n sessionId,\n visitorId,\n ...params,\n });\n },\n\n clear(): void {\n currentIdentity = null;\n identifiedSessionId = null;\n },\n};\n\nexport function teardown(): void {\n identity.clear();\n mgr = null;\n target = null;\n}\n"],"mappings":";;;;;;AAWA,MAAM,MAAM,aAAa,WAAW;AAEpC,IAAI,MAA6B;AACjC,IAAI,SAA8B;AAClC,IAAI,kBAAyC;AAC7C,IAAI,sBAAqC;AAEzC,SAAS,KACP,KACA,QACA,SACA,MACM;CACN,MAAM,IAA4B,OAAO,YAAY,QAAQ,SAAS,CAAC;AACvE,GAAE,cAAc,OAAO,MAAM,CAAC;CAE9B,MAAM,MAAM,KAAK,cAAc;AAC/B,KAAI,IACF,GAAE,yBAAyB;CAG7B,MAAM,MAAM,cAAc;AAC1B,KAAI,IACF,GAAE,yBAAyB;AAG7B,OAAM,KAAK;EACT;EACA,SAAS;EACT,MAAM,KAAK,UAAU,KAAK;EAC1B,WAAW;EACZ,CAAC,CAAC,YAAY;AACb,MAAI,KAAK,6BAA6B,OAAO;GAC7C;;AAGJ,eAAe,SAAS,WAAkC;AACxD,KAAI,CAAC,OACH;CAEF,MAAM,YAAY,MAAM,kBAAkB;AAC1C,KAAI,MAAM,gCAAgC,WAAW,aAAa,UAAU;AAC5E,MAAK,OAAO,KAAK,QAAQ,OAAO,SAAS;EAAE;EAAW;EAAW,CAAC;;;;;;AAOpE,SAAgB,UACd,eACA,SACM;AACN,UAAS;AAGT,KADiB,gBAAgB,QAAQ,CAC5B,YACX,cAAa;AAGf,OAAM,IAAI,gBAAgB,OAAO;AAC/B,WAAS,GAAG,CAAC,YAAY,GAEvB;GACF;;AAGJ,MAAa,UAAU;CACrB,QAAuB;AACrB,SAAO,KAAK,cAAc,IAAI;;CAGhC,cAA6B;AAC3B,SAAO,KAAK,aAAa,IAAI;;CAEhC;AAED,MAAa,WAAW;CACtB,MAA6B;AAC3B,SAAO;;CAGT,IAAI,QAA8B;AAChC,MAAI,EAAE,OAAO,QACX;EAEF,MAAM,YAAY,IAAI,cAAc;AACpC,MAAI,wBAAwB,WAAW;AACrC,OAAI,MAAM,8CAA8C,UAAU;AAClE;;AAGF,oBAAkB;AAClB,wBAAsB;EAEtB,MAAM,YAAY,cAAc;AAChC,MAAI,KAAK,4BAA4B,WAAW,OAAO,WAAW;AAClE,OAAK,OAAO,KAAK,OAAO,OAAO,SAAS;GACtC;GACA;GACA,GAAG;GACJ,CAAC;;CAGJ,QAAc;AACZ,oBAAkB;AAClB,wBAAsB;;CAEzB;AAED,SAAgB,WAAiB;AAC/B,UAAS,OAAO;AAChB,OAAM;AACN,UAAS"}
1
+ {"version":3,"file":"api.mjs","names":[],"sources":["../../src/tracking/api.ts"],"sourcesContent":["import type { IdentifyParams } from \"@interfere/types/sdk/identify\";\n\nimport { buildHeaders, type IngestTarget } from \"../transport/http.js\";\nimport { createLogger } from \"../util/log.js\";\nimport { SessionManager } from \"./session.js\";\nimport { getVisitorId, whenVisitorReady } from \"./visitor.js\";\n\nconst log = createLogger(\"tracking\");\n\nlet mgr: SessionManager | null = null;\nlet target: IngestTarget | null = null;\nlet currentIdentity: IdentifyParams | null = null;\nlet identifiedSessionId: string | null = null;\n\nfunction fire(\n url: string,\n method: \"POST\" | \"PUT\",\n headers: Headers,\n body: unknown\n): void {\n fetch(url, {\n method,\n headers: buildHeaders(headers),\n body: JSON.stringify(body),\n keepalive: true,\n }).catch(() => {\n log.warn(\"fire-and-forget %s failed\", method);\n });\n}\n\nasync function onRotate(sessionId: string): Promise<void> {\n if (!target) {\n return;\n }\n const visitorId = await whenVisitorReady();\n log.debug(\"POST session %s (visitor=%s)\", sessionId, visitorId ?? \"pending\");\n fire(target.url, \"POST\", target.headers, { sessionId, visitorId });\n}\n\nexport function bootstrap(sessionTarget: IngestTarget): void {\n target = sessionTarget;\n\n mgr = new SessionManager((id) => {\n onRotate(id).catch(() => {\n /* best-effort */\n });\n });\n}\n\nexport const session = {\n getId(): string | null {\n return mgr?.getSessionId() ?? null;\n },\n\n getWindowId(): string | null {\n return mgr?.getWindowId() ?? null;\n },\n};\n\nexport const identity = {\n get(): IdentifyParams | null {\n return currentIdentity;\n },\n\n set(params: IdentifyParams): void {\n if (!(mgr && target)) {\n return;\n }\n const sessionId = mgr.getSessionId();\n if (identifiedSessionId === sessionId) {\n log.debug(\"skipped, already identified for session %s\", sessionId);\n return;\n }\n\n currentIdentity = params;\n identifiedSessionId = sessionId;\n\n const visitorId = getVisitorId();\n log.info(\"PUT session %s → user %s\", sessionId, params.identifier);\n fire(target.url, \"PUT\", target.headers, {\n sessionId,\n visitorId,\n ...params,\n });\n },\n\n clear(): void {\n currentIdentity = null;\n identifiedSessionId = null;\n },\n};\n\nexport function teardown(): void {\n identity.clear();\n mgr = null;\n target = null;\n}\n"],"mappings":";;;;;AAOA,MAAM,MAAM,aAAa,WAAW;AAEpC,IAAI,MAA6B;AACjC,IAAI,SAA8B;AAClC,IAAI,kBAAyC;AAC7C,IAAI,sBAAqC;AAEzC,SAAS,KACP,KACA,QACA,SACA,MACM;AACN,OAAM,KAAK;EACT;EACA,SAAS,aAAa,QAAQ;EAC9B,MAAM,KAAK,UAAU,KAAK;EAC1B,WAAW;EACZ,CAAC,CAAC,YAAY;AACb,MAAI,KAAK,6BAA6B,OAAO;GAC7C;;AAGJ,eAAe,SAAS,WAAkC;AACxD,KAAI,CAAC,OACH;CAEF,MAAM,YAAY,MAAM,kBAAkB;AAC1C,KAAI,MAAM,gCAAgC,WAAW,aAAa,UAAU;AAC5E,MAAK,OAAO,KAAK,QAAQ,OAAO,SAAS;EAAE;EAAW;EAAW,CAAC;;AAGpE,SAAgB,UAAU,eAAmC;AAC3D,UAAS;AAET,OAAM,IAAI,gBAAgB,OAAO;AAC/B,WAAS,GAAG,CAAC,YAAY,GAEvB;GACF;;AAGJ,MAAa,UAAU;CACrB,QAAuB;AACrB,SAAO,KAAK,cAAc,IAAI;;CAGhC,cAA6B;AAC3B,SAAO,KAAK,aAAa,IAAI;;CAEhC;AAED,MAAa,WAAW;CACtB,MAA6B;AAC3B,SAAO;;CAGT,IAAI,QAA8B;AAChC,MAAI,EAAE,OAAO,QACX;EAEF,MAAM,YAAY,IAAI,cAAc;AACpC,MAAI,wBAAwB,WAAW;AACrC,OAAI,MAAM,8CAA8C,UAAU;AAClE;;AAGF,oBAAkB;AAClB,wBAAsB;EAEtB,MAAM,YAAY,cAAc;AAChC,MAAI,KAAK,4BAA4B,WAAW,OAAO,WAAW;AAClE,OAAK,OAAO,KAAK,OAAO,OAAO,SAAS;GACtC;GACA;GACA,GAAG;GACJ,CAAC;;CAGJ,QAAc;AACZ,oBAAkB;AAClB,wBAAsB;;CAEzB;AAED,SAAgB,WAAiB;AAC/B,UAAS,OAAO;AAChB,OAAM;AACN,UAAS"}
@@ -9,7 +9,6 @@ declare class SessionManager {
9
9
  constructor(onRotate?: OnRotate);
10
10
  getSessionId(): string;
11
11
  getWindowId(): string;
12
- whenReady(signal?: AbortSignal): Promise<string>;
13
12
  private restore;
14
13
  private rotate;
15
14
  private touch;
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.mts","names":[],"sources":["../../src/tracking/session.ts"],"mappings":";KAqBY,QAAA,IAAY,SAAA;AAAA,cAEX,cAAA;EACX,SAAA;EACA,QAAA;EAAA,iBACiB,KAAA;EAAA,iBACA,OAAA;EAAA,iBACA,QAAA;cAEL,QAAA,GAAW,QAAA;EAOvB,YAAA,CAAA;EAQA,WAAA,CAAA;EAgBA,SAAA,CAAU,MAAA,GAAS,WAAA,GAAc,OAAA;EAAA,QAsBzB,OAAA;EAAA,QAWA,MAAA;EAAA,QAQA,KAAA;EAAA,QAIA,SAAA;AAAA"}
1
+ {"version":3,"file":"session.d.mts","names":[],"sources":["../../src/tracking/session.ts"],"mappings":";KAoBY,QAAA,IAAY,SAAA;AAAA,cAEX,cAAA;EACX,SAAA;EACA,QAAA;EAAA,iBACiB,KAAA;EAAA,iBACA,OAAA;EAAA,iBACA,QAAA;cAEL,QAAA,GAAW,QAAA;EAOvB,YAAA,CAAA;EAQA,WAAA,CAAA;EAAA,QAgBQ,OAAA;EAAA,QAWA,MAAA;EAAA,QAQA,KAAA;EAAA,QAIA,SAAA;AAAA"}
@@ -5,7 +5,6 @@ const SESSION_ID_KEY = "interfere:session_id";
5
5
  const LAST_ACTIVITY_KEY = "interfere:last_activity";
6
6
  const WINDOW_ID_KEY = "interfere:window_id";
7
7
  const SESSION_TIMEOUT_MS = 1800 * 1e3;
8
- const POLL_INTERVAL_MS = 250;
9
8
  function tryStorage(type) {
10
9
  try {
11
10
  const s = globalThis[type];
@@ -47,24 +46,6 @@ var SessionManager = class {
47
46
  this.session?.setItem(WINDOW_ID_KEY, this.windowId);
48
47
  return this.windowId;
49
48
  }
50
- whenReady(signal) {
51
- const id = this.sessionId;
52
- if (id && !this.isExpired()) return Promise.resolve(id);
53
- return new Promise((resolve, reject) => {
54
- const check = setInterval(() => {
55
- if (signal?.aborted) {
56
- clearInterval(check);
57
- reject(signal.reason);
58
- return;
59
- }
60
- const current = this.getSessionId();
61
- if (current) {
62
- clearInterval(check);
63
- resolve(current);
64
- }
65
- }, POLL_INTERVAL_MS);
66
- });
67
- }
68
49
  restore() {
69
50
  const stored = this.local?.getItem(SESSION_ID_KEY);
70
51
  if (stored && !this.isExpired()) {
@@ -1 +1 @@
1
- {"version":3,"file":"session.mjs","names":["uuidv7"],"sources":["../../src/tracking/session.ts"],"sourcesContent":["import { nanoid } from \"nanoid\";\nimport { v7 as uuidv7 } from \"uuid\";\n\nconst SESSION_ID_KEY = \"interfere:session_id\";\nconst LAST_ACTIVITY_KEY = \"interfere:last_activity\";\nconst WINDOW_ID_KEY = \"interfere:window_id\";\nconst SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes\nconst POLL_INTERVAL_MS = 250;\n\nfunction tryStorage(type: \"localStorage\" | \"sessionStorage\"): Storage | null {\n try {\n const s = globalThis[type];\n const key = \"__interfere_probe__\";\n s.setItem(key, \"1\");\n s.removeItem(key);\n return s;\n } catch {\n return null;\n }\n}\n\nexport type OnRotate = (sessionId: string) => void;\n\nexport class SessionManager {\n sessionId: string | null = null;\n windowId: string | null = null;\n private readonly local: Storage | null;\n private readonly session: Storage | null;\n private readonly onRotate: OnRotate | null = null;\n\n constructor(onRotate?: OnRotate) {\n this.local = tryStorage(\"localStorage\");\n this.session = tryStorage(\"sessionStorage\");\n this.onRotate = onRotate ?? null;\n this.restore();\n }\n\n getSessionId(): string {\n if (this.sessionId && !this.isExpired()) {\n this.touch();\n return this.sessionId;\n }\n return this.rotate();\n }\n\n getWindowId(): string {\n if (this.windowId) {\n return this.windowId;\n }\n\n const stored = this.session?.getItem(WINDOW_ID_KEY);\n if (stored) {\n this.windowId = stored;\n return stored;\n }\n\n this.windowId = `win_${nanoid(10)}`;\n this.session?.setItem(WINDOW_ID_KEY, this.windowId);\n return this.windowId;\n }\n\n whenReady(signal?: AbortSignal): Promise<string> {\n const id = this.sessionId;\n if (id && !this.isExpired()) {\n return Promise.resolve(id);\n }\n\n return new Promise((resolve, reject) => {\n const check = setInterval(() => {\n if (signal?.aborted) {\n clearInterval(check);\n reject(signal.reason as Error);\n return;\n }\n const current = this.getSessionId();\n if (current) {\n clearInterval(check);\n resolve(current);\n }\n }, POLL_INTERVAL_MS);\n });\n }\n\n private restore(): void {\n const stored = this.local?.getItem(SESSION_ID_KEY);\n\n if (stored && !this.isExpired()) {\n this.sessionId = stored;\n this.touch();\n } else {\n this.rotate();\n }\n }\n\n private rotate(): string {\n this.sessionId = uuidv7();\n this.local?.setItem(SESSION_ID_KEY, this.sessionId);\n this.touch();\n this.onRotate?.(this.sessionId);\n return this.sessionId;\n }\n\n private touch(): void {\n this.local?.setItem(LAST_ACTIVITY_KEY, String(Date.now()));\n }\n\n private isExpired(): boolean {\n const raw = this.local?.getItem(LAST_ACTIVITY_KEY);\n\n if (!raw) {\n return true;\n }\n return Date.now() - Number(raw) > SESSION_TIMEOUT_MS;\n }\n}\n"],"mappings":";;;AAGA,MAAM,iBAAiB;AACvB,MAAM,oBAAoB;AAC1B,MAAM,gBAAgB;AACtB,MAAM,qBAAqB,OAAU;AACrC,MAAM,mBAAmB;AAEzB,SAAS,WAAW,MAAyD;AAC3E,KAAI;EACF,MAAM,IAAI,WAAW;EACrB,MAAM,MAAM;AACZ,IAAE,QAAQ,KAAK,IAAI;AACnB,IAAE,WAAW,IAAI;AACjB,SAAO;SACD;AACN,SAAO;;;AAMX,IAAa,iBAAb,MAA4B;CAC1B,YAA2B;CAC3B,WAA0B;CAC1B;CACA;CACA,WAA6C;CAE7C,YAAY,UAAqB;AAC/B,OAAK,QAAQ,WAAW,eAAe;AACvC,OAAK,UAAU,WAAW,iBAAiB;AAC3C,OAAK,WAAW,YAAY;AAC5B,OAAK,SAAS;;CAGhB,eAAuB;AACrB,MAAI,KAAK,aAAa,CAAC,KAAK,WAAW,EAAE;AACvC,QAAK,OAAO;AACZ,UAAO,KAAK;;AAEd,SAAO,KAAK,QAAQ;;CAGtB,cAAsB;AACpB,MAAI,KAAK,SACP,QAAO,KAAK;EAGd,MAAM,SAAS,KAAK,SAAS,QAAQ,cAAc;AACnD,MAAI,QAAQ;AACV,QAAK,WAAW;AAChB,UAAO;;AAGT,OAAK,WAAW,OAAO,OAAO,GAAG;AACjC,OAAK,SAAS,QAAQ,eAAe,KAAK,SAAS;AACnD,SAAO,KAAK;;CAGd,UAAU,QAAuC;EAC/C,MAAM,KAAK,KAAK;AAChB,MAAI,MAAM,CAAC,KAAK,WAAW,CACzB,QAAO,QAAQ,QAAQ,GAAG;AAG5B,SAAO,IAAI,SAAS,SAAS,WAAW;GACtC,MAAM,QAAQ,kBAAkB;AAC9B,QAAI,QAAQ,SAAS;AACnB,mBAAc,MAAM;AACpB,YAAO,OAAO,OAAgB;AAC9B;;IAEF,MAAM,UAAU,KAAK,cAAc;AACnC,QAAI,SAAS;AACX,mBAAc,MAAM;AACpB,aAAQ,QAAQ;;MAEjB,iBAAiB;IACpB;;CAGJ,UAAwB;EACtB,MAAM,SAAS,KAAK,OAAO,QAAQ,eAAe;AAElD,MAAI,UAAU,CAAC,KAAK,WAAW,EAAE;AAC/B,QAAK,YAAY;AACjB,QAAK,OAAO;QAEZ,MAAK,QAAQ;;CAIjB,SAAyB;AACvB,OAAK,YAAYA,IAAQ;AACzB,OAAK,OAAO,QAAQ,gBAAgB,KAAK,UAAU;AACnD,OAAK,OAAO;AACZ,OAAK,WAAW,KAAK,UAAU;AAC/B,SAAO,KAAK;;CAGd,QAAsB;AACpB,OAAK,OAAO,QAAQ,mBAAmB,OAAO,KAAK,KAAK,CAAC,CAAC;;CAG5D,YAA6B;EAC3B,MAAM,MAAM,KAAK,OAAO,QAAQ,kBAAkB;AAElD,MAAI,CAAC,IACH,QAAO;AAET,SAAO,KAAK,KAAK,GAAG,OAAO,IAAI,GAAG"}
1
+ {"version":3,"file":"session.mjs","names":["uuidv7"],"sources":["../../src/tracking/session.ts"],"sourcesContent":["import { nanoid } from \"nanoid\";\nimport { v7 as uuidv7 } from \"uuid\";\n\nconst SESSION_ID_KEY = \"interfere:session_id\";\nconst LAST_ACTIVITY_KEY = \"interfere:last_activity\";\nconst WINDOW_ID_KEY = \"interfere:window_id\";\nconst SESSION_TIMEOUT_MS = 30 * 60 * 1000;\n\nfunction tryStorage(type: \"localStorage\" | \"sessionStorage\"): Storage | null {\n try {\n const s = globalThis[type];\n const key = \"__interfere_probe__\";\n s.setItem(key, \"1\");\n s.removeItem(key);\n return s;\n } catch {\n return null;\n }\n}\n\nexport type OnRotate = (sessionId: string) => void;\n\nexport class SessionManager {\n sessionId: string | null = null;\n windowId: string | null = null;\n private readonly local: Storage | null;\n private readonly session: Storage | null;\n private readonly onRotate: OnRotate | null = null;\n\n constructor(onRotate?: OnRotate) {\n this.local = tryStorage(\"localStorage\");\n this.session = tryStorage(\"sessionStorage\");\n this.onRotate = onRotate ?? null;\n this.restore();\n }\n\n getSessionId(): string {\n if (this.sessionId && !this.isExpired()) {\n this.touch();\n return this.sessionId;\n }\n return this.rotate();\n }\n\n getWindowId(): string {\n if (this.windowId) {\n return this.windowId;\n }\n\n const stored = this.session?.getItem(WINDOW_ID_KEY);\n if (stored) {\n this.windowId = stored;\n return stored;\n }\n\n this.windowId = `win_${nanoid(10)}`;\n this.session?.setItem(WINDOW_ID_KEY, this.windowId);\n return this.windowId;\n }\n\n private restore(): void {\n const stored = this.local?.getItem(SESSION_ID_KEY);\n\n if (stored && !this.isExpired()) {\n this.sessionId = stored;\n this.touch();\n } else {\n this.rotate();\n }\n }\n\n private rotate(): string {\n this.sessionId = uuidv7();\n this.local?.setItem(SESSION_ID_KEY, this.sessionId);\n this.touch();\n this.onRotate?.(this.sessionId);\n return this.sessionId;\n }\n\n private touch(): void {\n this.local?.setItem(LAST_ACTIVITY_KEY, String(Date.now()));\n }\n\n private isExpired(): boolean {\n const raw = this.local?.getItem(LAST_ACTIVITY_KEY);\n\n if (!raw) {\n return true;\n }\n return Date.now() - Number(raw) > SESSION_TIMEOUT_MS;\n }\n}\n"],"mappings":";;;AAGA,MAAM,iBAAiB;AACvB,MAAM,oBAAoB;AAC1B,MAAM,gBAAgB;AACtB,MAAM,qBAAqB,OAAU;AAErC,SAAS,WAAW,MAAyD;AAC3E,KAAI;EACF,MAAM,IAAI,WAAW;EACrB,MAAM,MAAM;AACZ,IAAE,QAAQ,KAAK,IAAI;AACnB,IAAE,WAAW,IAAI;AACjB,SAAO;SACD;AACN,SAAO;;;AAMX,IAAa,iBAAb,MAA4B;CAC1B,YAA2B;CAC3B,WAA0B;CAC1B;CACA;CACA,WAA6C;CAE7C,YAAY,UAAqB;AAC/B,OAAK,QAAQ,WAAW,eAAe;AACvC,OAAK,UAAU,WAAW,iBAAiB;AAC3C,OAAK,WAAW,YAAY;AAC5B,OAAK,SAAS;;CAGhB,eAAuB;AACrB,MAAI,KAAK,aAAa,CAAC,KAAK,WAAW,EAAE;AACvC,QAAK,OAAO;AACZ,UAAO,KAAK;;AAEd,SAAO,KAAK,QAAQ;;CAGtB,cAAsB;AACpB,MAAI,KAAK,SACP,QAAO,KAAK;EAGd,MAAM,SAAS,KAAK,SAAS,QAAQ,cAAc;AACnD,MAAI,QAAQ;AACV,QAAK,WAAW;AAChB,UAAO;;AAGT,OAAK,WAAW,OAAO,OAAO,GAAG;AACjC,OAAK,SAAS,QAAQ,eAAe,KAAK,SAAS;AACnD,SAAO,KAAK;;CAGd,UAAwB;EACtB,MAAM,SAAS,KAAK,OAAO,QAAQ,eAAe;AAElD,MAAI,UAAU,CAAC,KAAK,WAAW,EAAE;AAC/B,QAAK,YAAY;AACjB,QAAK,OAAO;QAEZ,MAAK,QAAQ;;CAIjB,SAAyB;AACvB,OAAK,YAAYA,IAAQ;AACzB,OAAK,OAAO,QAAQ,gBAAgB,KAAK,UAAU;AACnD,OAAK,OAAO;AACZ,OAAK,WAAW,KAAK,UAAU;AAC/B,SAAO,KAAK;;CAGd,QAAsB;AACpB,OAAK,OAAO,QAAQ,mBAAmB,OAAO,KAAK,KAAK,CAAC,CAAC;;CAG5D,YAA6B;EAC3B,MAAM,MAAM,KAAK,OAAO,QAAQ,kBAAkB;AAElD,MAAI,CAAC,IACH,QAAO;AAET,SAAO,KAAK,KAAK,GAAG,OAAO,IAAI,GAAG"}
@@ -2,5 +2,6 @@
2
2
  declare function initVisitor(): void;
3
3
  declare function getVisitorId(): string | null;
4
4
  declare function whenVisitorReady(): Promise<string | null>;
5
+ declare function resetVisitor(): void;
5
6
  //#endregion
6
- export { getVisitorId, initVisitor, whenVisitorReady };
7
+ export { getVisitorId, initVisitor, resetVisitor, whenVisitorReady };
@@ -1 +1 @@
1
- {"version":3,"file":"visitor.d.mts","names":[],"sources":["../../src/tracking/visitor.ts"],"mappings":";iBAcgB,WAAA,CAAA;AAAA,iBA2BA,YAAA,CAAA;AAAA,iBAIA,gBAAA,CAAA,GAAoB,OAAA"}
1
+ {"version":3,"file":"visitor.d.mts","names":[],"sources":["../../src/tracking/visitor.ts"],"mappings":";iBAcgB,WAAA,CAAA;AAAA,iBA+BA,YAAA,CAAA;AAAA,iBAIA,gBAAA,CAAA,GAAoB,OAAA;AAAA,iBAOpB,YAAA,CAAA"}
@@ -8,6 +8,7 @@ function resolveApiKey() {
8
8
  return process.env.INTERFERE_FINGERPRINT_KEY ?? void 0;
9
9
  }
10
10
  function initVisitor() {
11
+ if (visitorId || pending) return;
11
12
  const apiKey = resolveApiKey();
12
13
  if (!apiKey) {
13
14
  log.info("no INTERFERE_FINGERPRINT_KEY, skipping");
@@ -31,5 +32,9 @@ function whenVisitorReady() {
31
32
  if (visitorId) return Promise.resolve(visitorId);
32
33
  return pending ?? Promise.resolve(null);
33
34
  }
35
+ function resetVisitor() {
36
+ visitorId = null;
37
+ pending = null;
38
+ }
34
39
  //#endregion
35
- export { getVisitorId, initVisitor, whenVisitorReady };
40
+ export { getVisitorId, initVisitor, resetVisitor, whenVisitorReady };
@@ -1 +1 @@
1
- {"version":3,"file":"visitor.mjs","names":[],"sources":["../../src/tracking/visitor.ts"],"sourcesContent":["import { createLogger } from \"../util/log.js\";\n\nconst log = createLogger(\"visitor\");\n\nlet visitorId: string | null = null;\nlet pending: Promise<string | null> | null = null;\n\nfunction resolveApiKey(): string | undefined {\n if (typeof process === \"undefined\") {\n return undefined;\n }\n return process.env.INTERFERE_FINGERPRINT_KEY ?? undefined;\n}\n\nexport function initVisitor(): void {\n const apiKey = resolveApiKey();\n if (!apiKey) {\n log.info(\"no INTERFERE_FINGERPRINT_KEY, skipping\");\n return;\n }\n\n pending = (async () => {\n try {\n const FingerprintJS = await import(\"@fingerprintjs/fingerprintjs-pro\");\n\n const fp = await FingerprintJS.load({ apiKey });\n const result = await fp.get();\n\n visitorId = result.visitorId;\n\n log.debug(\"resolved %s\", visitorId);\n\n return visitorId;\n } catch {\n log.error(\"fingerprint failed\");\n\n return null;\n }\n })();\n}\n\nexport function getVisitorId(): string | null {\n return visitorId;\n}\n\nexport function whenVisitorReady(): Promise<string | null> {\n if (visitorId) {\n return Promise.resolve(visitorId);\n }\n return pending ?? Promise.resolve(null);\n}\n"],"mappings":";;AAEA,MAAM,MAAM,aAAa,UAAU;AAEnC,IAAI,YAA2B;AAC/B,IAAI,UAAyC;AAE7C,SAAS,gBAAoC;AAC3C,KAAI,OAAO,YAAY,YACrB;AAEF,QAAO,QAAQ,IAAI,6BAA6B,KAAA;;AAGlD,SAAgB,cAAoB;CAClC,MAAM,SAAS,eAAe;AAC9B,KAAI,CAAC,QAAQ;AACX,MAAI,KAAK,yCAAyC;AAClD;;AAGF,YAAW,YAAY;AACrB,MAAI;AAMF,gBAFe,OADJ,OAFW,MAAM,OAAO,qCAEJ,KAAK,EAAE,QAAQ,CAAC,EACvB,KAAK,EAEV;AAEnB,OAAI,MAAM,eAAe,UAAU;AAEnC,UAAO;UACD;AACN,OAAI,MAAM,qBAAqB;AAE/B,UAAO;;KAEP;;AAGN,SAAgB,eAA8B;AAC5C,QAAO;;AAGT,SAAgB,mBAA2C;AACzD,KAAI,UACF,QAAO,QAAQ,QAAQ,UAAU;AAEnC,QAAO,WAAW,QAAQ,QAAQ,KAAK"}
1
+ {"version":3,"file":"visitor.mjs","names":[],"sources":["../../src/tracking/visitor.ts"],"sourcesContent":["import { createLogger } from \"../util/log.js\";\n\nconst log = createLogger(\"visitor\");\n\nlet visitorId: string | null = null;\nlet pending: Promise<string | null> | null = null;\n\nfunction resolveApiKey(): string | undefined {\n if (typeof process === \"undefined\") {\n return undefined;\n }\n return process.env.INTERFERE_FINGERPRINT_KEY ?? undefined;\n}\n\nexport function initVisitor(): void {\n if (visitorId || pending) {\n return;\n }\n\n const apiKey = resolveApiKey();\n if (!apiKey) {\n log.info(\"no INTERFERE_FINGERPRINT_KEY, skipping\");\n return;\n }\n\n pending = (async () => {\n try {\n const FingerprintJS = await import(\"@fingerprintjs/fingerprintjs-pro\");\n\n const fp = await FingerprintJS.load({ apiKey });\n const result = await fp.get();\n\n visitorId = result.visitorId;\n\n log.debug(\"resolved %s\", visitorId);\n\n return visitorId;\n } catch {\n log.error(\"fingerprint failed\");\n\n return null;\n }\n })();\n}\n\nexport function getVisitorId(): string | null {\n return visitorId;\n}\n\nexport function whenVisitorReady(): Promise<string | null> {\n if (visitorId) {\n return Promise.resolve(visitorId);\n }\n return pending ?? Promise.resolve(null);\n}\n\nexport function resetVisitor(): void {\n visitorId = null;\n pending = null;\n}\n"],"mappings":";;AAEA,MAAM,MAAM,aAAa,UAAU;AAEnC,IAAI,YAA2B;AAC/B,IAAI,UAAyC;AAE7C,SAAS,gBAAoC;AAC3C,KAAI,OAAO,YAAY,YACrB;AAEF,QAAO,QAAQ,IAAI,6BAA6B,KAAA;;AAGlD,SAAgB,cAAoB;AAClC,KAAI,aAAa,QACf;CAGF,MAAM,SAAS,eAAe;AAC9B,KAAI,CAAC,QAAQ;AACX,MAAI,KAAK,yCAAyC;AAClD;;AAGF,YAAW,YAAY;AACrB,MAAI;AAMF,gBAFe,OADJ,OAFW,MAAM,OAAO,qCAEJ,KAAK,EAAE,QAAQ,CAAC,EACvB,KAAK,EAEV;AAEnB,OAAI,MAAM,eAAe,UAAU;AAEnC,UAAO;UACD;AACN,OAAI,MAAM,qBAAqB;AAE/B,UAAO;;KAEP;;AAGN,SAAgB,eAA8B;AAC5C,QAAO;;AAGT,SAAgB,mBAA2C;AACzD,KAAI,UACF,QAAO,QAAQ,QAAQ,UAAU;AAEnC,QAAO,WAAW,QAAQ,QAAQ,KAAK;;AAGzC,SAAgB,eAAqB;AACnC,aAAY;AACZ,WAAU"}
@@ -5,6 +5,7 @@ interface IngestTarget {
5
5
  headers: Headers;
6
6
  url: string;
7
7
  }
8
+ declare function buildHeaders(base: Headers): Record<string, string>;
8
9
  declare class HttpTransport {
9
10
  private readonly target;
10
11
  private pendingKeepalive;
@@ -12,4 +13,4 @@ declare class HttpTransport {
12
13
  send(envelopes: Envelope[]): Promise<void>;
13
14
  }
14
15
  //#endregion
15
- export { HttpTransport, IngestTarget };
16
+ export { HttpTransport, IngestTarget, buildHeaders };
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.mts","names":[],"sources":["../../src/transport/http.ts"],"mappings":";;;UAUiB,YAAA;EACf,OAAA,EAAS,OAAA;EACT,GAAA;AAAA;AAAA,cA+BW,aAAA;EAAA,iBACM,MAAA;EAAA,QACT,gBAAA;cAEI,MAAA,EAAQ,YAAA;EAId,IAAA,CAAK,SAAA,EAAW,QAAA,KAAa,OAAA;AAAA"}
1
+ {"version":3,"file":"http.d.mts","names":[],"sources":["../../src/transport/http.ts"],"mappings":";;;UAUiB,YAAA;EACf,OAAA,EAAS,OAAA;EACT,GAAA;AAAA;AAAA,iBAGc,YAAA,CAAa,IAAA,EAAM,OAAA,GAAU,MAAA;AAAA,cA4BhC,aAAA;EAAA,iBACM,MAAA;EAAA,QACT,gBAAA;cAEI,MAAA,EAAQ,YAAA;EAId,IAAA,CAAK,SAAA,EAAW,QAAA,KAAa,OAAA;AAAA"}
@@ -53,4 +53,4 @@ var HttpTransport = class {
53
53
  }
54
54
  };
55
55
  //#endregion
56
- export { HttpTransport };
56
+ export { HttpTransport, buildHeaders };
@@ -1 +1 @@
1
- {"version":3,"file":"http.mjs","names":[],"sources":["../../src/transport/http.ts"],"sourcesContent":["import type { Envelope } from \"@interfere/types/sdk/envelope\";\n\nimport { make } from \"tctx/traceparent\";\n\nimport { session } from \"../tracking/api.js\";\nimport { getVisitorId } from \"../tracking/visitor.js\";\nimport { createLogger } from \"../util/log.js\";\n\nconst log = createLogger(\"http\");\n\nexport interface IngestTarget {\n headers: Headers;\n url: string;\n}\n\nfunction buildHeaders(base: Headers): Record<string, string> {\n const h: Record<string, string> = Object.fromEntries(base.entries());\n h.traceparent = String(make());\n\n const sessionId = session.getId();\n if (sessionId) {\n h[\"x-interfere-session\"] = sessionId;\n }\n\n const visitorId = getVisitorId();\n if (visitorId) {\n h[\"x-interfere-visitor\"] = visitorId;\n }\n\n return h;\n}\n\nfunction hasServiceWorker(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n \"serviceWorker\" in navigator &&\n navigator.serviceWorker.controller !== null\n );\n}\n\nconst KEEPALIVE_BUDGET_BYTES = 61_440;\nconst MAX_CONCURRENT_KEEPALIVE = 15;\n\nexport class HttpTransport {\n private readonly target: IngestTarget;\n private pendingKeepalive = 0;\n\n constructor(target: IngestTarget) {\n this.target = target;\n }\n\n async send(envelopes: Envelope[]): Promise<void> {\n const body = JSON.stringify(envelopes);\n const headers = buildHeaders(this.target.headers);\n\n if (hasServiceWorker()) {\n log.debug(\"POST %d envelopes via SW\", envelopes.length);\n await fetch(this.target.url, {\n method: \"POST\",\n headers,\n body,\n });\n return;\n }\n\n const bytes = new TextEncoder().encode(body).byteLength;\n const useKeepalive =\n bytes < KEEPALIVE_BUDGET_BYTES &&\n this.pendingKeepalive < MAX_CONCURRENT_KEEPALIVE;\n\n if (useKeepalive) {\n this.pendingKeepalive++;\n }\n\n log.debug(\n \"POST %d envelopes direct (%d bytes, keepalive=%s)\",\n envelopes.length,\n bytes,\n useKeepalive\n );\n\n try {\n await fetch(this.target.url, {\n method: \"POST\",\n headers,\n body,\n keepalive: useKeepalive,\n });\n } finally {\n if (useKeepalive) {\n this.pendingKeepalive--;\n }\n }\n }\n}\n"],"mappings":";;;;;AAQA,MAAM,MAAM,aAAa,OAAO;AAOhC,SAAS,aAAa,MAAuC;CAC3D,MAAM,IAA4B,OAAO,YAAY,KAAK,SAAS,CAAC;AACpE,GAAE,cAAc,OAAO,MAAM,CAAC;CAE9B,MAAM,YAAY,QAAQ,OAAO;AACjC,KAAI,UACF,GAAE,yBAAyB;CAG7B,MAAM,YAAY,cAAc;AAChC,KAAI,UACF,GAAE,yBAAyB;AAG7B,QAAO;;AAGT,SAAS,mBAA4B;AACnC,QACE,OAAO,cAAc,eACrB,mBAAmB,aACnB,UAAU,cAAc,eAAe;;AAI3C,MAAM,yBAAyB;AAC/B,MAAM,2BAA2B;AAEjC,IAAa,gBAAb,MAA2B;CACzB;CACA,mBAA2B;CAE3B,YAAY,QAAsB;AAChC,OAAK,SAAS;;CAGhB,MAAM,KAAK,WAAsC;EAC/C,MAAM,OAAO,KAAK,UAAU,UAAU;EACtC,MAAM,UAAU,aAAa,KAAK,OAAO,QAAQ;AAEjD,MAAI,kBAAkB,EAAE;AACtB,OAAI,MAAM,4BAA4B,UAAU,OAAO;AACvD,SAAM,MAAM,KAAK,OAAO,KAAK;IAC3B,QAAQ;IACR;IACA;IACD,CAAC;AACF;;EAGF,MAAM,QAAQ,IAAI,aAAa,CAAC,OAAO,KAAK,CAAC;EAC7C,MAAM,eACJ,QAAQ,0BACR,KAAK,mBAAmB;AAE1B,MAAI,aACF,MAAK;AAGP,MAAI,MACF,qDACA,UAAU,QACV,OACA,aACD;AAED,MAAI;AACF,SAAM,MAAM,KAAK,OAAO,KAAK;IAC3B,QAAQ;IACR;IACA;IACA,WAAW;IACZ,CAAC;YACM;AACR,OAAI,aACF,MAAK"}
1
+ {"version":3,"file":"http.mjs","names":[],"sources":["../../src/transport/http.ts"],"sourcesContent":["import type { Envelope } from \"@interfere/types/sdk/envelope\";\n\nimport { make } from \"tctx/traceparent\";\n\nimport { session } from \"../tracking/api.js\";\nimport { getVisitorId } from \"../tracking/visitor.js\";\nimport { createLogger } from \"../util/log.js\";\n\nconst log = createLogger(\"http\");\n\nexport interface IngestTarget {\n headers: Headers;\n url: string;\n}\n\nexport function buildHeaders(base: Headers): Record<string, string> {\n const h: Record<string, string> = Object.fromEntries(base.entries());\n h.traceparent = String(make());\n\n const sessionId = session.getId();\n if (sessionId) {\n h[\"x-interfere-session\"] = sessionId;\n }\n\n const visitorId = getVisitorId();\n if (visitorId) {\n h[\"x-interfere-visitor\"] = visitorId;\n }\n\n return h;\n}\n\nfunction hasServiceWorker(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n \"serviceWorker\" in navigator &&\n navigator.serviceWorker.controller !== null\n );\n}\n\nconst KEEPALIVE_BUDGET_BYTES = 61_440;\nconst MAX_CONCURRENT_KEEPALIVE = 15;\n\nexport class HttpTransport {\n private readonly target: IngestTarget;\n private pendingKeepalive = 0;\n\n constructor(target: IngestTarget) {\n this.target = target;\n }\n\n async send(envelopes: Envelope[]): Promise<void> {\n const body = JSON.stringify(envelopes);\n const headers = buildHeaders(this.target.headers);\n\n if (hasServiceWorker()) {\n log.debug(\"POST %d envelopes via SW\", envelopes.length);\n await fetch(this.target.url, {\n method: \"POST\",\n headers,\n body,\n });\n return;\n }\n\n const bytes = new TextEncoder().encode(body).byteLength;\n const useKeepalive =\n bytes < KEEPALIVE_BUDGET_BYTES &&\n this.pendingKeepalive < MAX_CONCURRENT_KEEPALIVE;\n\n if (useKeepalive) {\n this.pendingKeepalive++;\n }\n\n log.debug(\n \"POST %d envelopes direct (%d bytes, keepalive=%s)\",\n envelopes.length,\n bytes,\n useKeepalive\n );\n\n try {\n await fetch(this.target.url, {\n method: \"POST\",\n headers,\n body,\n keepalive: useKeepalive,\n });\n } finally {\n if (useKeepalive) {\n this.pendingKeepalive--;\n }\n }\n }\n}\n"],"mappings":";;;;;AAQA,MAAM,MAAM,aAAa,OAAO;AAOhC,SAAgB,aAAa,MAAuC;CAClE,MAAM,IAA4B,OAAO,YAAY,KAAK,SAAS,CAAC;AACpE,GAAE,cAAc,OAAO,MAAM,CAAC;CAE9B,MAAM,YAAY,QAAQ,OAAO;AACjC,KAAI,UACF,GAAE,yBAAyB;CAG7B,MAAM,YAAY,cAAc;AAChC,KAAI,UACF,GAAE,yBAAyB;AAG7B,QAAO;;AAGT,SAAS,mBAA4B;AACnC,QACE,OAAO,cAAc,eACrB,mBAAmB,aACnB,UAAU,cAAc,eAAe;;AAI3C,MAAM,yBAAyB;AAC/B,MAAM,2BAA2B;AAEjC,IAAa,gBAAb,MAA2B;CACzB;CACA,mBAA2B;CAE3B,YAAY,QAAsB;AAChC,OAAK,SAAS;;CAGhB,MAAM,KAAK,WAAsC;EAC/C,MAAM,OAAO,KAAK,UAAU,UAAU;EACtC,MAAM,UAAU,aAAa,KAAK,OAAO,QAAQ;AAEjD,MAAI,kBAAkB,EAAE;AACtB,OAAI,MAAM,4BAA4B,UAAU,OAAO;AACvD,SAAM,MAAM,KAAK,OAAO,KAAK;IAC3B,QAAQ;IACR;IACA;IACD,CAAC;AACF;;EAGF,MAAM,QAAQ,IAAI,aAAa,CAAC,OAAO,KAAK,CAAC;EAC7C,MAAM,eACJ,QAAQ,0BACR,KAAK,mBAAmB;AAE1B,MAAI,aACF,MAAK;AAGP,MAAI,MACF,qDACA,UAAU,QACV,OACA,aACD;AAED,MAAI;AACF,SAAM,MAAM,KAAK,OAAO,KAAK;IAC3B,QAAQ;IACR;IACA;IACA,WAAW;IACZ,CAAC;YACM;AACR,OAAI,aACF,MAAK"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interfere/react",
3
- "version": "0.2.0-alpha.3",
3
+ "version": "0.2.0-alpha.5",
4
4
  "license": "MIT",
5
5
  "description": "Client-side React SDK for Interfere. Error tracking, session replay, and analytics.",
6
6
  "keywords": [
@@ -18,22 +18,27 @@
18
18
  "repository": {
19
19
  "type": "git",
20
20
  "url": "git+https://github.com/interfere-inc/interfere.git",
21
- "directory": "src/packages/public/react-v2"
21
+ "directory": "src/packages/public/react"
22
22
  },
23
23
  "files": [
24
24
  "dist"
25
25
  ],
26
26
  "type": "module",
27
27
  "exports": {
28
- "./*": {
29
- "@source": "./src/*.ts",
30
- "types": "./dist/*.d.mts",
31
- "default": "./dist/*.mjs"
32
- },
33
28
  "./provider": {
34
29
  "@source": "./src/provider.tsx",
35
30
  "types": "./dist/provider.d.mts",
36
31
  "default": "./dist/provider.mjs"
32
+ },
33
+ "./error-boundary": {
34
+ "@source": "./src/error-boundary.tsx",
35
+ "types": "./dist/error-boundary.d.mts",
36
+ "default": "./dist/error-boundary.mjs"
37
+ },
38
+ "./internal/client": {
39
+ "@source": "./src/internal/client.ts",
40
+ "types": "./dist/internal/client.d.mts",
41
+ "default": "./dist/internal/client.mjs"
37
42
  }
38
43
  },
39
44
  "sideEffects": false,
@@ -50,8 +55,8 @@
50
55
  },
51
56
  "dependencies": {
52
57
  "@fingerprintjs/fingerprintjs-pro": "^3.12.8",
53
- "@interfere/constants": "0.2.0-alpha.2",
54
- "@interfere/types": "0.2.0-alpha.2",
58
+ "@interfere/constants": "0.2.0-alpha.4",
59
+ "@interfere/types": "0.2.0-alpha.5",
55
60
  "@ua-parser-js/pro-enterprise": "^2.0.6",
56
61
  "nanoid": "^5.1.6",
57
62
  "rrweb": "2.0.0-alpha.4",
@@ -63,8 +68,8 @@
63
68
  "react-dom": ">=19"
64
69
  },
65
70
  "devDependencies": {
66
- "@interfere/typescript-config": "1.1.0-alpha.4",
67
- "@interfere/vitest-config": "1.1.0-alpha.4",
71
+ "@interfere/typescript-config": "1.1.0-alpha.6",
72
+ "@interfere/vitest-config": "1.1.0-alpha.6",
68
73
  "@rrweb/types": "2.0.0-alpha.20",
69
74
  "@testing-library/react": "^16.3.2",
70
75
  "@types/node": "^24.12.0",