@interfere/react 8.0.0 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,16 +19,23 @@
19
19
  </p>
20
20
 
21
21
  <p align="center">
22
- <a href="https://interfere.com/docs">Documentation</a>
22
+ <a href="https://support.interfere.com/docs">Documentation</a>
23
23
  ·
24
- <a href="https://github.com/interfere-inc/interfere/issues/new">Report a Bug</a>
24
+ <a href="https://support.interfere.com/roadmap">Feature Requests</a>
25
+ ·
26
+ <a href="https://support.interfere.com/requests">Report a Bug</a>
25
27
  ·
26
28
  <a href="mailto:support@interfere.com">Get Help</a>
27
29
  </p>
28
30
 
29
31
  ---
30
32
 
31
- > **Using Next.js?** You probably want [`@interfere/next`](https://www.npmjs.com/package/@interfere/next) instead — This is currently a work-in-progress that will add support for Vite and other React frameworks.
33
+ ## Framework Guides
34
+
35
+ | Framework | Package | Guide |
36
+ | --- | --- | --- |
37
+ | Next.js | [`@interfere/next`](https://www.npmjs.com/package/@interfere/next) | [docs/next.md](docs/next.md) |
38
+ | Vite | [`@interfere/vite`](https://www.npmjs.com/package/@interfere/vite) + `@interfere/react` | [docs/vite.md](docs/vite.md) |
32
39
 
33
40
  ## Installation
34
41
 
@@ -75,6 +82,7 @@ function MyComponent() {
75
82
  identifier: "usr_123",
76
83
  email: "jane@example.com",
77
84
  name: "Jane",
85
+ source: { type: "clerk", name: "Clerk" },
78
86
  });
79
87
 
80
88
  const sessionId = session.getId();
@@ -25,19 +25,21 @@ declare class Client {
25
25
  private fetchRemoteConfig;
26
26
  capture<T extends EventType>(type: T, payload: EnvelopePayload<T>): void;
27
27
  flush(): void;
28
- dispose(): void;
28
+ dispose(): Promise<void>;
29
29
  getConsent(): ConsentState | null;
30
30
  setConsent(value?: ConsentState): void;
31
31
  resetConsent(): void;
32
32
  }
33
33
  declare function getClient(): Client;
34
34
  declare function init(opts?: ClientOptions): void;
35
- declare function close(): void;
35
+ declare function close(): Promise<void>;
36
36
  declare const consent: {
37
37
  get(): ConsentState | null;
38
38
  set(value?: ConsentState): void;
39
39
  };
40
40
  declare function syncConsent(consentState: ConsentState | undefined): void;
41
41
  declare function flush(): void;
42
+ /** @internal Test-only. Resets the module state so init() can be called again. */
43
+ declare function _reset(): void;
42
44
  //#endregion
43
- export { ClientOptions, close, consent, flush, getClient, init, syncConsent };
45
+ export { ClientOptions, _reset, close, consent, flush, getClient, init, syncConsent };
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.mts","names":[],"sources":["../../src/internal/client.ts"],"mappings":";;;;;;UAkBiB,aAAA;EACf,KAAA,GAAQ,IAAA,CAAK,OAAA,CAAQ,YAAA;EACrB,OAAA,GAAU,YAAA;EAFkB;;;;;;;EAU5B,OAAA;EACA,OAAA,GAAU,eAAA;AAAA;AAAA,cAGN,MAAA;EAAA,iBACa,QAAA;EAAA,iBACA,KAAA;EAAA,iBACA,OAAA;cAEL,IAAA,EAAM,aAAA,EAAe,OAAA,UAAiB,SAAA;EAAA,QAoC1C,iBAAA;EA0BR,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,iBA0Bb,IAAA,CAAK,IAAA,GAAM,aAAA;AAAA,iBA+BX,KAAA,CAAA;AAAA,cASH,OAAA;SACJ,YAAA;cAIK,YAAA;AAAA;AAAA,iBAKE,WAAA,CAAY,YAAA,EAAc,YAAA;AAAA,iBAa1B,KAAA,CAAA"}
1
+ {"version":3,"file":"client.d.mts","names":[],"sources":["../../src/internal/client.ts"],"mappings":";;;;;;UAkBiB,aAAA;EACf,KAAA,GAAQ,IAAA,CAAK,OAAA,CAAQ,YAAA;EACrB,OAAA,GAAU,YAAA;EAFkB;;;;;;;EAU5B,OAAA;EACA,OAAA,GAAU,eAAA;AAAA;AAAA,cAGN,MAAA;EAAA,iBACa,QAAA;EAAA,iBACA,KAAA;EAAA,iBACA,OAAA;cAEL,IAAA,EAAM,aAAA,EAAe,OAAA,UAAiB,SAAA;EAAA,QAoC1C,iBAAA;EA0BR,OAAA,WAAkB,SAAA,CAAA,CAAW,IAAA,EAAM,CAAA,EAAG,OAAA,EAAS,eAAA,CAAgB,CAAA;EAS/D,KAAA,CAAA;EAIM,OAAA,CAAA,GAAW,OAAA;EAMjB,UAAA,CAAA,GAAc,YAAA;EAId,UAAA,CAAW,KAAA,GAAQ,YAAA;EAInB,YAAA,CAAA;AAAA;AAAA,iBAOc,SAAA,CAAA,GAAa,MAAA;AAAA,iBA6Bb,IAAA,CAAK,IAAA,GAAM,aAAA;AAAA,iBA+BL,KAAA,CAAA,GAAS,OAAA;AAAA,cASlB,OAAA;SACJ,YAAA;cAIK,YAAA;AAAA;AAAA,iBAKE,WAAA,CAAY,YAAA,EAAc,YAAA;AAAA,iBAa1B,KAAA,CAAA;;iBAKA,MAAA,CAAA"}
@@ -63,8 +63,8 @@ var Client = class {
63
63
  flush() {
64
64
  this.queue.flush();
65
65
  }
66
- dispose() {
67
- this.runtime.dispose();
66
+ async dispose() {
67
+ await this.runtime.dispose();
68
68
  teardown();
69
69
  this.queue.dispose();
70
70
  }
@@ -88,7 +88,7 @@ function isEnabledByEnvironment() {
88
88
  if (typeof process === "undefined" || !process.env) return true;
89
89
  if (process.env["NODE_ENV"] === "production") return true;
90
90
  if (process.env["NODE_ENV"] === void 0) return true;
91
- return !!process.env["NEXT_PUBLIC_INTERFERE_FORCE_ENABLE"];
91
+ return !!(globalThis["__INTERFERE_FORCE_ENABLE__"] || process.env["INTERFERE_FORCE_ENABLE"]);
92
92
  } catch {
93
93
  return true;
94
94
  }
@@ -96,7 +96,7 @@ function isEnabledByEnvironment() {
96
96
  function init(opts = {}) {
97
97
  if (instance) return;
98
98
  if (!(opts.enabled ?? isEnabledByEnvironment())) {
99
- log.info("Disabled in non-production. Pass enabled: true to init() or set NEXT_PUBLIC_INTERFERE_FORCE_ENABLE=1.");
99
+ log.info("Disabled in non-production. Pass enabled: true to init() or set INTERFERE_FORCE_ENABLE=1.");
100
100
  return;
101
101
  }
102
102
  const buildId = globalThis["__INTERFERE_BUILD_ID__"];
@@ -107,9 +107,9 @@ function init(opts = {}) {
107
107
  }
108
108
  instance = new Client(opts, buildId, releaseId ?? null);
109
109
  }
110
- function close() {
110
+ async function close() {
111
111
  if (!instance) return;
112
- instance.dispose();
112
+ await instance.dispose();
113
113
  instance = null;
114
114
  }
115
115
  const consent = {
@@ -131,5 +131,9 @@ function syncConsent(consentState) {
131
131
  function flush() {
132
132
  instance?.flush();
133
133
  }
134
+ /** @internal Test-only. Resets the module state so init() can be called again. */
135
+ function _reset() {
136
+ instance = null;
137
+ }
134
138
  //#endregion
135
- export { close, consent, flush, getClient, init, syncConsent };
139
+ export { _reset, 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 type { ConsentState } from \"@interfere/types/sdk/plugins/manifest\";\nimport type { RemoteConfig } from \"@interfere/types/sdk/remote-config\";\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 { buildHeaders, HttpTransport } from \"../transport/http.js\";\nimport { BatchQueue, type QueueOptions } 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 batch?: Omit<Partial<QueueOptions>, \"transport\">;\n consent?: ConsentState;\n /**\n * Override the automatic dev-mode guard. When `undefined`, the SDK\n * auto-detects: it disables itself if `process.env[\"NODE_ENV\"]` is not\n * `\"production\"` (Node / webpack / Next.js). In environments where\n * `process` does not exist (Vite, CRA, plain browser) the SDK\n * defaults to **enabled** — pass `false` to disable explicitly.\n */\n enabled?: boolean;\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\" ? undefined : process.env[\"NODE_ENV\"]\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, ...opts.batch });\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 this.fetchRemoteConfig(targets.config);\n }\n\n private fetchRemoteConfig(configTarget: {\n url: string;\n headers: Headers;\n }): void {\n fetch(configTarget.url, {\n method: \"GET\",\n headers: buildHeaders(configTarget.headers),\n signal: AbortSignal.timeout(10_000),\n })\n .then((res) => {\n if (!res.ok) {\n return;\n }\n return res.json() as Promise<RemoteConfig>;\n })\n .then((config) => {\n if (config?.plugins) {\n this.runtime.applyRemoteConfig(config.plugins);\n log.debug(\"applied remote config\");\n }\n })\n .catch(() => {\n log.warn(\"remote config fetch failed, using local defaults\");\n });\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\nfunction isEnabledByEnvironment(): boolean {\n try {\n if (typeof process === \"undefined\" || !process.env) {\n return true;\n }\n if (process.env[\"NODE_ENV\"] === \"production\") {\n return true;\n }\n if (process.env[\"NODE_ENV\"] === undefined) {\n return true;\n }\n return !!process.env[\"NEXT_PUBLIC_INTERFERE_FORCE_ENABLE\"];\n } catch {\n return true;\n }\n}\n\nexport function init(opts: ClientOptions = {}): void {\n if (instance) {\n return;\n }\n\n if (!(opts.enabled ?? isEnabledByEnvironment())) {\n log.info(\n \"Disabled in non-production. Pass enabled: true to init() or set NEXT_PUBLIC_INTERFERE_FORCE_ENABLE=1.\"\n );\n return;\n }\n\n const buildId = (globalThis as Record<string, unknown>)[\n \"__INTERFERE_BUILD_ID__\"\n ] as string | undefined;\n\n const releaseId = (globalThis as Record<string, unknown>)[\n \"__INTERFERE_RELEASE_ID__\"\n ] 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":"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"}
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 type { RemoteConfig } from \"@interfere/types/sdk/remote-config\";\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 { buildHeaders, HttpTransport } from \"../transport/http.js\";\nimport { BatchQueue, type QueueOptions } 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 batch?: Omit<Partial<QueueOptions>, \"transport\">;\n consent?: ConsentState;\n /**\n * Override the automatic dev-mode guard. When `undefined`, the SDK\n * auto-detects: it disables itself if `process.env[\"NODE_ENV\"]` is not\n * `\"production\"` (Node / webpack / Next.js). In environments where\n * `process` does not exist (Vite, CRA, plain browser) the SDK\n * defaults to **enabled** — pass `false` to disable explicitly.\n */\n enabled?: boolean;\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\" ? undefined : process.env[\"NODE_ENV\"]\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, ...opts.batch });\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 this.fetchRemoteConfig(targets.config);\n }\n\n private fetchRemoteConfig(configTarget: {\n url: string;\n headers: Headers;\n }): void {\n fetch(configTarget.url, {\n method: \"GET\",\n headers: buildHeaders(configTarget.headers),\n signal: AbortSignal.timeout(10_000),\n })\n .then((res) => {\n if (!res.ok) {\n return;\n }\n return res.json() as Promise<RemoteConfig>;\n })\n .then((config) => {\n if (config?.plugins) {\n this.runtime.applyRemoteConfig(config.plugins);\n log.debug(\"applied remote config\");\n }\n })\n .catch(() => {\n log.warn(\"remote config fetch failed, using local defaults\");\n });\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 async dispose(): Promise<void> {\n await 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\nfunction isEnabledByEnvironment(): boolean {\n try {\n if (typeof process === \"undefined\" || !process.env) {\n return true;\n }\n if (process.env[\"NODE_ENV\"] === \"production\") {\n return true;\n }\n if (process.env[\"NODE_ENV\"] === undefined) {\n return true;\n }\n return !!(\n (globalThis as Record<string, unknown>)[\"__INTERFERE_FORCE_ENABLE__\"] ||\n process.env[\"INTERFERE_FORCE_ENABLE\"]\n );\n } catch {\n return true;\n }\n}\n\nexport function init(opts: ClientOptions = {}): void {\n if (instance) {\n return;\n }\n\n if (!(opts.enabled ?? isEnabledByEnvironment())) {\n log.info(\n \"Disabled in non-production. Pass enabled: true to init() or set INTERFERE_FORCE_ENABLE=1.\"\n );\n return;\n }\n\n const buildId = (globalThis as Record<string, unknown>)[\n \"__INTERFERE_BUILD_ID__\"\n ] as string | undefined;\n\n const releaseId = (globalThis as Record<string, unknown>)[\n \"__INTERFERE_RELEASE_ID__\"\n ] 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 async function close(): Promise<void> {\n if (!instance) {\n return;\n }\n\n await 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\n/** @internal Test-only. Resets the module state so init() can be called again. */\nexport function _reset(): void {\n instance = null;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.mts","names":[],"sources":["../../src/internal/config.ts"],"mappings":";;;iBAegB,cAAA,CAAA;EACd,MAAA,EAAQ,YAAA;EACR,MAAA,EAAQ,YAAA;EACR,OAAA,EAAS,YAAA;AAAA"}
1
+ {"version":3,"file":"config.d.mts","names":[],"sources":["../../src/internal/config.ts"],"mappings":";;;iBAyBgB,cAAA,CAAA;EACd,MAAA,EAAQ,YAAA;EACR,MAAA,EAAQ,YAAA;EACR,OAAA,EAAS,YAAA;AAAA"}
@@ -4,8 +4,9 @@ const DEFAULT_PROXY_URL = "/api/interfere";
4
4
  const DEFAULT_SESSION_PATH = "/v1/session";
5
5
  const DEFAULT_CONFIG_PATH = "/v1/config";
6
6
  function resolvePublicKey() {
7
- if (typeof process === "undefined") return;
8
- return process.env["INTERFERE_PUBLIC_KEY"] ?? void 0;
7
+ const injected = globalThis["__INTERFERE_PUBLIC_KEY__"];
8
+ if (injected) return injected;
9
+ if (typeof process !== "undefined") return process.env["INTERFERE_PUBLIC_KEY"] ?? void 0;
9
10
  }
10
11
  function resolveTargets() {
11
12
  const publicKey = resolvePublicKey();
@@ -1 +1 @@
1
- {"version":3,"file":"config.mjs","names":[],"sources":["../../src/internal/config.ts"],"sourcesContent":["import { API_PATHS, API_URL } from \"@interfere/constants/api\";\n\nimport type { IngestTarget } from \"../transport/http.js\";\n\nconst DEFAULT_PROXY_URL = \"/api/interfere\";\nconst DEFAULT_SESSION_PATH = \"/v1/session\";\nconst DEFAULT_CONFIG_PATH = \"/v1/config\";\n\nfunction resolvePublicKey(): string | undefined {\n if (typeof process === \"undefined\") {\n return undefined;\n }\n return process.env[\"INTERFERE_PUBLIC_KEY\"] ?? undefined;\n}\n\nexport function resolveTargets(): {\n config: IngestTarget;\n ingest: IngestTarget;\n session: IngestTarget;\n} {\n const publicKey = resolvePublicKey();\n const headers = new Headers({ \"content-type\": \"application/json\" });\n if (publicKey) {\n headers.set(\"x-interfere-pub-token\", publicKey);\n }\n\n const baseUrl = publicKey ? API_URL : DEFAULT_PROXY_URL;\n const sessionPath =\n (API_PATHS as { SESSION?: string }).SESSION ?? DEFAULT_SESSION_PATH;\n const configPath =\n (API_PATHS as { CONFIG?: string }).CONFIG ?? DEFAULT_CONFIG_PATH;\n\n return {\n config: {\n url: `${baseUrl}${configPath}`,\n headers,\n },\n ingest: {\n url: `${baseUrl}${API_PATHS.INGEST}`,\n headers,\n },\n session: {\n url: `${baseUrl}${sessionPath}`,\n headers,\n },\n };\n}\n"],"mappings":";;AAIA,MAAM,oBAAoB;AAC1B,MAAM,uBAAuB;AAC7B,MAAM,sBAAsB;AAE5B,SAAS,mBAAuC;AAC9C,KAAI,OAAO,YAAY,YACrB;AAEF,QAAO,QAAQ,IAAI,2BAA2B,KAAA;;AAGhD,SAAgB,iBAId;CACA,MAAM,YAAY,kBAAkB;CACpC,MAAM,UAAU,IAAI,QAAQ,EAAE,gBAAgB,oBAAoB,CAAC;AACnE,KAAI,UACF,SAAQ,IAAI,yBAAyB,UAAU;CAGjD,MAAM,UAAU,YAAY,UAAU;CACtC,MAAM,cACH,UAAmC,WAAW;AAIjD,QAAO;EACL,QAAQ;GACN,KAAK,GAAG,UAJT,UAAkC,UAAU;GAK3C;GACD;EACD,QAAQ;GACN,KAAK,GAAG,UAAU,UAAU;GAC5B;GACD;EACD,SAAS;GACP,KAAK,GAAG,UAAU;GAClB;GACD;EACF"}
1
+ {"version":3,"file":"config.mjs","names":[],"sources":["../../src/internal/config.ts"],"sourcesContent":["import { API_PATHS, API_URL } from \"@interfere/constants/api\";\n\nimport type { IngestTarget } from \"../transport/http.js\";\n\nconst DEFAULT_PROXY_URL = \"/api/interfere\";\nconst DEFAULT_SESSION_PATH = \"/v1/session\";\nconst DEFAULT_CONFIG_PATH = \"/v1/config\";\n\nfunction resolvePublicKey(): string | undefined {\n // Vite plugin injects the key onto globalThis at build time\n const injected = (globalThis as Record<string, unknown>)[\n \"__INTERFERE_PUBLIC_KEY__\"\n ] as string | undefined;\n if (injected) {\n return injected;\n }\n\n // Node / webpack / Next.js: read from process.env\n if (typeof process !== \"undefined\") {\n return process.env[\"INTERFERE_PUBLIC_KEY\"] ?? undefined;\n }\n\n return undefined;\n}\n\nexport function resolveTargets(): {\n config: IngestTarget;\n ingest: IngestTarget;\n session: IngestTarget;\n} {\n const publicKey = resolvePublicKey();\n const headers = new Headers({ \"content-type\": \"application/json\" });\n if (publicKey) {\n headers.set(\"x-interfere-pub-token\", publicKey);\n }\n\n const baseUrl = publicKey ? API_URL : DEFAULT_PROXY_URL;\n const sessionPath =\n (API_PATHS as { SESSION?: string }).SESSION ?? DEFAULT_SESSION_PATH;\n const configPath =\n (API_PATHS as { CONFIG?: string }).CONFIG ?? DEFAULT_CONFIG_PATH;\n\n return {\n config: {\n url: `${baseUrl}${configPath}`,\n headers,\n },\n ingest: {\n url: `${baseUrl}${API_PATHS.INGEST}`,\n headers,\n },\n session: {\n url: `${baseUrl}${sessionPath}`,\n headers,\n },\n };\n}\n"],"mappings":";;AAIA,MAAM,oBAAoB;AAC1B,MAAM,uBAAuB;AAC7B,MAAM,sBAAsB;AAE5B,SAAS,mBAAuC;CAE9C,MAAM,WAAY,WAChB;AAEF,KAAI,SACF,QAAO;AAIT,KAAI,OAAO,YAAY,YACrB,QAAO,QAAQ,IAAI,2BAA2B,KAAA;;AAMlD,SAAgB,iBAId;CACA,MAAM,YAAY,kBAAkB;CACpC,MAAM,UAAU,IAAI,QAAQ,EAAE,gBAAgB,oBAAoB,CAAC;AACnE,KAAI,UACF,SAAQ,IAAI,yBAAyB,UAAU;CAGjD,MAAM,UAAU,YAAY,UAAU;CACtC,MAAM,cACH,UAAmC,WAAW;AAIjD,QAAO;EACL,QAAQ;GACN,KAAK,GAAG,UAJT,UAAkC,UAAU;GAK3C;GACD;EACD,QAAQ;GACN,KAAK,GAAG,UAAU,UAAU;GAC5B;GACD;EACD,SAAS;GACP,KAAK,GAAG,UAAU;GAClB;GACD;EACF"}
@@ -1,17 +1,19 @@
1
1
  import { PluginContext } from "../plugins/lib/types.mjs";
2
2
  import { PluginOverrides } from "../plugins/lib/loader.mjs";
3
3
  import { ConsentState } from "@interfere/types/sdk/plugins/manifest";
4
- import { EventType } from "@interfere/types/sdk/envelope";
5
4
  import { RemotePluginConfig } from "@interfere/types/sdk/remote-config";
5
+ import { EventType } from "@interfere/types/sdk/envelope";
6
6
 
7
7
  //#region src/internal/plugin-runtime.d.ts
8
8
  declare class PluginRuntime {
9
9
  private readonly activeCleanups;
10
+ private readonly pending;
10
11
  private readonly context;
11
12
  private readonly features;
12
13
  private remoteConfig;
13
14
  private consentState;
14
15
  private syncVersion;
16
+ private disposed;
15
17
  constructor(context: PluginContext, overrides: PluginOverrides | undefined, initialConsent: ConsentState | undefined);
16
18
  getConsent(): ConsentState | null;
17
19
  setConsent(nextConsent?: ConsentState): void;
@@ -19,7 +21,7 @@ declare class PluginRuntime {
19
21
  applyRemoteConfig(config: RemotePluginConfig): void;
20
22
  canCapture(type: EventType): boolean;
21
23
  start(): void;
22
- dispose(): void;
24
+ dispose(): Promise<void>;
23
25
  private shouldEnablePlugin;
24
26
  private deactivate;
25
27
  private activate;
@@ -1 +1 @@
1
- {"version":3,"file":"plugin-runtime.d.mts","names":[],"sources":["../../src/internal/plugin-runtime.ts"],"mappings":";;;;;;;cA0Ba,aAAA;EAAA,iBACM,cAAA;EAAA,iBACA,OAAA;EAAA,iBACA,QAAA;EAAA,QACT,YAAA;EAAA,QACA,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,iBAAA,CAAkB,MAAA,EAAQ,kBAAA;EAK1B,UAAA,CAAW,IAAA,EAAM,SAAA;EAIjB,KAAA,CAAA;EAWA,OAAA,CAAA;EAAA,QAMQ,kBAAA;EAAA,QAQA,UAAA;EAAA,QAeM,QAAA;EAAA,QAoBN,IAAA;AAAA"}
1
+ {"version":3,"file":"plugin-runtime.d.mts","names":[],"sources":["../../src/internal/plugin-runtime.ts"],"mappings":";;;;;;;cA6Ba,aAAA;EAAA,iBACM,cAAA;EAAA,iBACA,OAAA;EAAA,iBACA,OAAA;EAAA,iBACA,QAAA;EAAA,QACT,YAAA;EAAA,QACA,YAAA;EAAA,QACA,WAAA;EAAA,QACA,QAAA;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,iBAAA,CAAkB,MAAA,EAAQ,kBAAA;EAK1B,UAAA,CAAW,IAAA,EAAM,SAAA;EAQjB,KAAA,CAAA;EAWM,OAAA,CAAA,GAAW,OAAA;EAAA,QAWT,kBAAA;EAAA,QAQA,UAAA;EAAA,QAeM,QAAA;EAAA,QAoBN,IAAA;AAAA"}
@@ -3,15 +3,18 @@ import errorsPlugin from "../plugins/errors.mjs";
3
3
  import { loadPlugin, resolveFeatures } from "../plugins/lib/loader.mjs";
4
4
  import { getPluginConsentCategory, hasConsentChanged, isConsentAllowed, resolveGrantedConsent, shouldCaptureEvent } from "./consent.mjs";
5
5
  import { PLUGIN_MANIFEST } from "@interfere/types/sdk/plugins/manifest";
6
+ import { EVENT_TYPE_TO_PLUGIN } from "@interfere/types/sdk/remote-config";
6
7
  //#region src/internal/plugin-runtime.ts
7
8
  const log = createLogger("plugin-runtime");
8
9
  var PluginRuntime = class {
9
10
  activeCleanups = /* @__PURE__ */ new Map();
11
+ pending = /* @__PURE__ */ new Set();
10
12
  context;
11
13
  features;
12
14
  remoteConfig = {};
13
15
  consentState;
14
16
  syncVersion = 0;
17
+ disposed = false;
15
18
  constructor(context, overrides, initialConsent) {
16
19
  this.context = context;
17
20
  this.features = resolveFeatures(overrides);
@@ -36,6 +39,8 @@ var PluginRuntime = class {
36
39
  this.sync();
37
40
  }
38
41
  canCapture(type) {
42
+ const plugin = EVENT_TYPE_TO_PLUGIN[type];
43
+ if (plugin && this.remoteConfig[plugin] === false) return false;
39
44
  return shouldCaptureEvent(type, this.consentState);
40
45
  }
41
46
  start() {
@@ -45,8 +50,11 @@ var PluginRuntime = class {
45
50
  }
46
51
  this.sync();
47
52
  }
48
- dispose() {
53
+ async dispose() {
54
+ this.disposed = true;
55
+ this.syncVersion += 1;
49
56
  for (const key of this.activeCleanups.keys()) this.deactivate(key);
57
+ await Promise.allSettled(this.pending);
50
58
  }
51
59
  shouldEnablePlugin(key) {
52
60
  return this.features[key] && this.remoteConfig[key] !== false && isConsentAllowed(getPluginConsentCategory(key), this.consentState);
@@ -73,6 +81,7 @@ var PluginRuntime = class {
73
81
  this.activeCleanups.set(key, cleanup);
74
82
  }
75
83
  sync() {
84
+ if (this.disposed) return;
76
85
  this.syncVersion += 1;
77
86
  for (const plugin of PLUGIN_MANIFEST) {
78
87
  if (this.shouldEnablePlugin(plugin.name)) {
@@ -83,9 +92,11 @@ var PluginRuntime = class {
83
92
  }
84
93
  continue;
85
94
  }
86
- this.activate(plugin.name).catch(() => {
95
+ const p = this.activate(plugin.name).catch(() => {
87
96
  log.warn("non-critical plugin loading failed");
88
97
  });
98
+ this.pending.add(p);
99
+ p.finally(() => this.pending.delete(p));
89
100
  continue;
90
101
  }
91
102
  this.deactivate(plugin.name);
@@ -1 +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\";\nimport type { RemotePluginConfig } from \"@interfere/types/sdk/remote-config\";\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 remoteConfig: RemotePluginConfig = {};\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 applyRemoteConfig(config: RemotePluginConfig): void {\n this.remoteConfig = config;\n this.sync();\n }\n\n canCapture(type: EventType): boolean {\n return shouldCaptureEvent(type, this.consentState);\n }\n\n start(): void {\n if (this.shouldEnablePlugin(\"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 this.remoteConfig[key] !== false &&\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 (this.shouldEnablePlugin(plugin.name)) {\n if (plugin.name === \"errors\") {\n if (!this.activeCleanups.has(\"errors\")) {\n const cleanup = errorsPlugin.setup(this.context);\n if (cleanup) {\n this.activeCleanups.set(\"errors\", cleanup);\n }\n }\n continue;\n }\n\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":";;;;;;AAwBA,MAAM,MAAM,aAAa,iBAAiB;AAE1C,IAAa,gBAAb,MAA2B;CACzB,iCAAkC,IAAI,KAA+B;CACrE;CACA;CACA,eAA2C,EAAE;CAC7C;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,kBAAkB,QAAkC;AAClD,OAAK,eAAe;AACpB,OAAK,MAAM;;CAGb,WAAW,MAA0B;AACnC,SAAO,mBAAmB,MAAM,KAAK,aAAa;;CAGpD,QAAc;AACZ,MAAI,KAAK,mBAAmB,SAAS,EAAE;GACrC,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,KAAK,aAAa,SAAS,SAC3B,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,KAAK,mBAAmB,OAAO,KAAK,EAAE;AACxC,QAAI,OAAO,SAAS,UAAU;AAC5B,SAAI,CAAC,KAAK,eAAe,IAAI,SAAS,EAAE;MACtC,MAAM,UAAU,aAAa,MAAM,KAAK,QAAQ;AAChD,UAAI,QACF,MAAK,eAAe,IAAI,UAAU,QAAQ;;AAG9C;;AAGF,SAAK,SAAS,OAAO,KAAK,CAAC,YAAY;AACrC,SAAI,KAAK,qCAAqC;MAC9C;AACF;;AAGF,QAAK,WAAW,OAAO,KAAK"}
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\";\nimport {\n EVENT_TYPE_TO_PLUGIN,\n type RemotePluginConfig,\n} from \"@interfere/types/sdk/remote-config\";\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 pending = new Set<Promise<void>>();\n private readonly context: PluginContext;\n private readonly features: Record<PluginKey, boolean>;\n private remoteConfig: RemotePluginConfig = {};\n private consentState: ConsentState | null;\n private syncVersion = 0;\n private disposed = false;\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 applyRemoteConfig(config: RemotePluginConfig): void {\n this.remoteConfig = config;\n this.sync();\n }\n\n canCapture(type: EventType): boolean {\n const plugin = EVENT_TYPE_TO_PLUGIN[type];\n if (plugin && this.remoteConfig[plugin] === false) {\n return false;\n }\n return shouldCaptureEvent(type, this.consentState);\n }\n\n start(): void {\n if (this.shouldEnablePlugin(\"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 async dispose(): Promise<void> {\n this.disposed = true;\n this.syncVersion += 1;\n\n for (const key of this.activeCleanups.keys()) {\n this.deactivate(key);\n }\n\n await Promise.allSettled(this.pending);\n }\n\n private shouldEnablePlugin(key: PluginKey): boolean {\n return (\n this.features[key] &&\n this.remoteConfig[key] !== false &&\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 if (this.disposed) {\n return;\n }\n\n this.syncVersion += 1;\n\n for (const plugin of PLUGIN_MANIFEST) {\n if (this.shouldEnablePlugin(plugin.name)) {\n if (plugin.name === \"errors\") {\n if (!this.activeCleanups.has(\"errors\")) {\n const cleanup = errorsPlugin.setup(this.context);\n if (cleanup) {\n this.activeCleanups.set(\"errors\", cleanup);\n }\n }\n continue;\n }\n\n const p = this.activate(plugin.name).catch(() => {\n log.warn(\"non-critical plugin loading failed\");\n });\n this.pending.add(p);\n p.finally(() => this.pending.delete(p));\n continue;\n }\n\n this.deactivate(plugin.name);\n }\n }\n}\n"],"mappings":";;;;;;;AA2BA,MAAM,MAAM,aAAa,iBAAiB;AAE1C,IAAa,gBAAb,MAA2B;CACzB,iCAAkC,IAAI,KAA+B;CACrE,0BAA2B,IAAI,KAAoB;CACnD;CACA;CACA,eAA2C,EAAE;CAC7C;CACA,cAAsB;CACtB,WAAmB;CAEnB,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,kBAAkB,QAAkC;AAClD,OAAK,eAAe;AACpB,OAAK,MAAM;;CAGb,WAAW,MAA0B;EACnC,MAAM,SAAS,qBAAqB;AACpC,MAAI,UAAU,KAAK,aAAa,YAAY,MAC1C,QAAO;AAET,SAAO,mBAAmB,MAAM,KAAK,aAAa;;CAGpD,QAAc;AACZ,MAAI,KAAK,mBAAmB,SAAS,EAAE;GACrC,MAAM,UAAU,aAAa,MAAM,KAAK,QAAQ;AAChD,OAAI,QACF,MAAK,eAAe,IAAI,UAAU,QAAQ;;AAI9C,OAAK,MAAM;;CAGb,MAAM,UAAyB;AAC7B,OAAK,WAAW;AAChB,OAAK,eAAe;AAEpB,OAAK,MAAM,OAAO,KAAK,eAAe,MAAM,CAC1C,MAAK,WAAW,IAAI;AAGtB,QAAM,QAAQ,WAAW,KAAK,QAAQ;;CAGxC,mBAA2B,KAAyB;AAClD,SACE,KAAK,SAAS,QACd,KAAK,aAAa,SAAS,SAC3B,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,MAAI,KAAK,SACP;AAGF,OAAK,eAAe;AAEpB,OAAK,MAAM,UAAU,iBAAiB;AACpC,OAAI,KAAK,mBAAmB,OAAO,KAAK,EAAE;AACxC,QAAI,OAAO,SAAS,UAAU;AAC5B,SAAI,CAAC,KAAK,eAAe,IAAI,SAAS,EAAE;MACtC,MAAM,UAAU,aAAa,MAAM,KAAK,QAAQ;AAChD,UAAI,QACF,MAAK,eAAe,IAAI,UAAU,QAAQ;;AAG9C;;IAGF,MAAM,IAAI,KAAK,SAAS,OAAO,KAAK,CAAC,YAAY;AAC/C,SAAI,KAAK,qCAAqC;MAC9C;AACF,SAAK,QAAQ,IAAI,EAAE;AACnB,MAAE,cAAc,KAAK,QAAQ,OAAO,EAAE,CAAC;AACvC;;AAGF,QAAK,WAAW,OAAO,KAAK"}
package/dist/package.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
2
  var name = "@interfere/react";
3
- var version = "8.0.0";
3
+ var version = "8.1.0";
4
4
  //#endregion
5
5
  export { name, version };
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.mts","names":[],"sources":["../../src/tracking/api.ts"],"mappings":";;;;iBAiFgB,SAAA,CAAU,aAAA,EAAe,YAAA;AAAA,cAW5B,MAAA;EAQZ,WAAA;EAAA,SAAA;AAAA;AAAA,cAEY,OAAA;EAYZ,KAAA;EAAA,WAAA;AAAA;AAAA,cAEY,QAAA;SACJ,cAAA;cAIW,cAAA,GAAiB,OAAA;;;iBAwCrB,QAAA,CAAA"}
1
+ {"version":3,"file":"api.d.mts","names":[],"sources":["../../src/tracking/api.ts"],"mappings":";;;;iBAyFgB,SAAA,CAAU,aAAA,EAAe,YAAA;AAAA,cAY5B,MAAA;EAQZ,WAAA;EAAA,SAAA;AAAA;AAAA,cAEY,OAAA;EAYZ,KAAA;EAAA,WAAA;AAAA;AAAA,cAEY,QAAA;SACJ,cAAA;cAIW,cAAA,GAAiB,OAAA;;;iBAgDrB,QAAA,CAAA"}
@@ -1,6 +1,7 @@
1
1
  import { createLogger } from "../util/log.mjs";
2
2
  import { getDeviceId, getFpHash, initDevice, whenDeviceReady, whenFingerprintReady } from "./device.mjs";
3
3
  import { buildHeaders } from "../transport/http.mjs";
4
+ import { detectCountryCode, resetGeo } from "./geo.mjs";
4
5
  import { SessionManager } from "./session.mjs";
5
6
  //#region src/tracking/api.ts
6
7
  const log = createLogger("tracking");
@@ -10,10 +11,12 @@ let currentIdentity = null;
10
11
  let identifiedSessionId = null;
11
12
  let syncedSessionId = null;
12
13
  let syncAttemptMs = 0;
14
+ let generation = 0;
13
15
  const SYNC_COOLDOWN_MS = 5e3;
14
16
  function syncSession(sessionId, deviceId, fpHash) {
15
17
  if (!target) return;
16
18
  syncAttemptMs = Date.now();
19
+ syncedSessionId = sessionId;
17
20
  fetch(target.url, {
18
21
  method: "POST",
19
22
  headers: buildHeaders(target.headers),
@@ -25,8 +28,9 @@ function syncSession(sessionId, deviceId, fpHash) {
25
28
  keepalive: true,
26
29
  signal: AbortSignal.timeout(1e4)
27
30
  }).then((res) => {
28
- if (res.ok) syncedSessionId = sessionId;
31
+ if (!res.ok) syncedSessionId = null;
29
32
  }).catch(() => {
33
+ syncedSessionId = null;
30
34
  log.warn("session sync failed, will retry");
31
35
  });
32
36
  }
@@ -39,13 +43,16 @@ async function onRotate(sessionId) {
39
43
  syncedSessionId = null;
40
44
  syncAttemptMs = Date.now();
41
45
  if (!target) return;
46
+ const gen = generation;
42
47
  const [deviceId, fpHash] = await Promise.all([whenDeviceReady(), whenFingerprintReady()]);
48
+ if (gen !== generation) return;
43
49
  log.debug("POST session %s (device=%s fp=%s)", sessionId, deviceId ?? "pending", fpHash ?? "none");
44
50
  syncSession(sessionId, deviceId, fpHash);
45
51
  }
46
52
  function bootstrap(sessionTarget) {
47
53
  target = sessionTarget;
48
54
  initDevice();
55
+ detectCountryCode();
49
56
  mgr = new SessionManager((id) => {
50
57
  onRotate(id).catch(() => {});
51
58
  });
@@ -81,8 +88,13 @@ const identity = {
81
88
  }
82
89
  currentIdentity = params;
83
90
  identifiedSessionId = sessionId;
84
- const [deviceId, fpHash] = await Promise.all([whenDeviceReady(), whenFingerprintReady()]);
85
- if (!target) {
91
+ const gen = generation;
92
+ const [deviceId, fpHash, country] = await Promise.all([
93
+ whenDeviceReady(),
94
+ whenFingerprintReady(),
95
+ detectCountryCode()
96
+ ]);
97
+ if (gen !== generation || !target) {
86
98
  identifiedSessionId = null;
87
99
  return;
88
100
  }
@@ -94,7 +106,8 @@ const identity = {
94
106
  sessionId,
95
107
  deviceId,
96
108
  fpHash,
97
- ...params
109
+ ...params,
110
+ ...country && { country }
98
111
  }),
99
112
  keepalive: true,
100
113
  signal: AbortSignal.timeout(1e4)
@@ -109,7 +122,9 @@ const identity = {
109
122
  }
110
123
  };
111
124
  function teardown() {
125
+ generation += 1;
112
126
  identity.clear();
127
+ resetGeo();
113
128
  syncedSessionId = null;
114
129
  syncAttemptMs = 0;
115
130
  mgr = null;
@@ -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 { buildHeaders, type IngestTarget } from \"../transport/http.js\";\nimport { createLogger } from \"../util/log.js\";\nimport {\n getDeviceId,\n getFpHash,\n initDevice,\n whenDeviceReady,\n whenFingerprintReady,\n} from \"./device.js\";\nimport { SessionManager } from \"./session.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\nlet syncedSessionId: string | null = null;\nlet syncAttemptMs = 0;\nconst SYNC_COOLDOWN_MS = 5000;\n\nfunction syncSession(\n sessionId: string,\n deviceId: string | null,\n fpHash: string | null\n): void {\n if (!target) {\n return;\n }\n\n syncAttemptMs = Date.now();\n\n fetch(target.url, {\n method: \"POST\",\n headers: buildHeaders(target.headers),\n body: JSON.stringify({ sessionId, deviceId, fpHash }),\n keepalive: true,\n signal: AbortSignal.timeout(10_000),\n })\n .then((res) => {\n if (res.ok) {\n syncedSessionId = sessionId;\n }\n })\n .catch(() => {\n log.warn(\"session sync failed, will retry\");\n });\n}\n\nfunction ensureSynced(sessionId: string): void {\n if (syncedSessionId === sessionId) {\n return;\n }\n if (Date.now() - syncAttemptMs < SYNC_COOLDOWN_MS) {\n return;\n }\n syncSession(sessionId, getDeviceId(), getFpHash());\n}\n\nasync function onRotate(sessionId: string): Promise<void> {\n syncedSessionId = null;\n syncAttemptMs = Date.now();\n if (!target) {\n return;\n }\n const [deviceId, fpHash] = await Promise.all([\n whenDeviceReady(),\n whenFingerprintReady(),\n ]);\n log.debug(\n \"POST session %s (device=%s fp=%s)\",\n sessionId,\n deviceId ?? \"pending\",\n fpHash ?? \"none\"\n );\n syncSession(sessionId, deviceId, fpHash);\n}\n\nexport function bootstrap(sessionTarget: IngestTarget): void {\n target = sessionTarget;\n initDevice();\n\n mgr = new SessionManager((id) => {\n onRotate(id).catch(() => {\n /* best-effort */\n });\n });\n}\n\nexport const device = {\n getDeviceId(): string | null {\n return getDeviceId();\n },\n\n getFpHash(): string | null {\n return getFpHash();\n },\n};\n\nexport const session = {\n getId(): string | null {\n const id = mgr?.getSessionId() ?? null;\n if (id) {\n ensureSynced(id);\n }\n return id;\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 async set(params: IdentifyParams): Promise<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 [deviceId, fpHash] = await Promise.all([\n whenDeviceReady(),\n whenFingerprintReady(),\n ]);\n if (!target) {\n identifiedSessionId = null;\n return;\n }\n log.info(\"PUT session %s → user %s\", sessionId, params.identifier);\n fetch(target.url, {\n method: \"PUT\",\n headers: buildHeaders(target.headers),\n body: JSON.stringify({ sessionId, deviceId, fpHash, ...params }),\n keepalive: true,\n signal: AbortSignal.timeout(10_000),\n }).catch(() => {\n identifiedSessionId = null;\n log.warn(\"identify failed for session %s\", sessionId);\n });\n },\n\n clear(): void {\n currentIdentity = null;\n identifiedSessionId = null;\n },\n};\n\nexport function teardown(): void {\n identity.clear();\n syncedSessionId = null;\n syncAttemptMs = 0;\n mgr = null;\n target = null;\n}\n"],"mappings":";;;;;AAaA,MAAM,MAAM,aAAa,WAAW;AAEpC,IAAI,MAA6B;AACjC,IAAI,SAA8B;AAClC,IAAI,kBAAyC;AAC7C,IAAI,sBAAqC;AAEzC,IAAI,kBAAiC;AACrC,IAAI,gBAAgB;AACpB,MAAM,mBAAmB;AAEzB,SAAS,YACP,WACA,UACA,QACM;AACN,KAAI,CAAC,OACH;AAGF,iBAAgB,KAAK,KAAK;AAE1B,OAAM,OAAO,KAAK;EAChB,QAAQ;EACR,SAAS,aAAa,OAAO,QAAQ;EACrC,MAAM,KAAK,UAAU;GAAE;GAAW;GAAU;GAAQ,CAAC;EACrD,WAAW;EACX,QAAQ,YAAY,QAAQ,IAAO;EACpC,CAAC,CACC,MAAM,QAAQ;AACb,MAAI,IAAI,GACN,mBAAkB;GAEpB,CACD,YAAY;AACX,MAAI,KAAK,kCAAkC;GAC3C;;AAGN,SAAS,aAAa,WAAyB;AAC7C,KAAI,oBAAoB,UACtB;AAEF,KAAI,KAAK,KAAK,GAAG,gBAAgB,iBAC/B;AAEF,aAAY,WAAW,aAAa,EAAE,WAAW,CAAC;;AAGpD,eAAe,SAAS,WAAkC;AACxD,mBAAkB;AAClB,iBAAgB,KAAK,KAAK;AAC1B,KAAI,CAAC,OACH;CAEF,MAAM,CAAC,UAAU,UAAU,MAAM,QAAQ,IAAI,CAC3C,iBAAiB,EACjB,sBAAsB,CACvB,CAAC;AACF,KAAI,MACF,qCACA,WACA,YAAY,WACZ,UAAU,OACX;AACD,aAAY,WAAW,UAAU,OAAO;;AAG1C,SAAgB,UAAU,eAAmC;AAC3D,UAAS;AACT,aAAY;AAEZ,OAAM,IAAI,gBAAgB,OAAO;AAC/B,WAAS,GAAG,CAAC,YAAY,GAEvB;GACF;;AAGJ,MAAa,SAAS;CACpB,cAA6B;AAC3B,SAAO,aAAa;;CAGtB,YAA2B;AACzB,SAAO,WAAW;;CAErB;AAED,MAAa,UAAU;CACrB,QAAuB;EACrB,MAAM,KAAK,KAAK,cAAc,IAAI;AAClC,MAAI,GACF,cAAa,GAAG;AAElB,SAAO;;CAGT,cAA6B;AAC3B,SAAO,KAAK,aAAa,IAAI;;CAEhC;AAED,MAAa,WAAW;CACtB,MAA6B;AAC3B,SAAO;;CAGT,MAAM,IAAI,QAAuC;AAC/C,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,CAAC,UAAU,UAAU,MAAM,QAAQ,IAAI,CAC3C,iBAAiB,EACjB,sBAAsB,CACvB,CAAC;AACF,MAAI,CAAC,QAAQ;AACX,yBAAsB;AACtB;;AAEF,MAAI,KAAK,4BAA4B,WAAW,OAAO,WAAW;AAClE,QAAM,OAAO,KAAK;GAChB,QAAQ;GACR,SAAS,aAAa,OAAO,QAAQ;GACrC,MAAM,KAAK,UAAU;IAAE;IAAW;IAAU;IAAQ,GAAG;IAAQ,CAAC;GAChE,WAAW;GACX,QAAQ,YAAY,QAAQ,IAAO;GACpC,CAAC,CAAC,YAAY;AACb,yBAAsB;AACtB,OAAI,KAAK,kCAAkC,UAAU;IACrD;;CAGJ,QAAc;AACZ,oBAAkB;AAClB,wBAAsB;;CAEzB;AAED,SAAgB,WAAiB;AAC/B,UAAS,OAAO;AAChB,mBAAkB;AAClB,iBAAgB;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 {\n getDeviceId,\n getFpHash,\n initDevice,\n whenDeviceReady,\n whenFingerprintReady,\n} from \"./device.js\";\nimport { detectCountryCode, resetGeo } from \"./geo.js\";\nimport { SessionManager } from \"./session.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\nlet syncedSessionId: string | null = null;\nlet syncAttemptMs = 0;\nlet generation = 0;\nconst SYNC_COOLDOWN_MS = 5000;\n\nfunction syncSession(\n sessionId: string,\n deviceId: string | null,\n fpHash: string | null\n): void {\n if (!target) {\n return;\n }\n\n syncAttemptMs = Date.now();\n syncedSessionId = sessionId;\n\n fetch(target.url, {\n method: \"POST\",\n headers: buildHeaders(target.headers),\n body: JSON.stringify({ sessionId, deviceId, fpHash }),\n keepalive: true,\n signal: AbortSignal.timeout(10_000),\n })\n .then((res) => {\n if (!res.ok) {\n syncedSessionId = null;\n }\n })\n .catch(() => {\n syncedSessionId = null;\n log.warn(\"session sync failed, will retry\");\n });\n}\n\nfunction ensureSynced(sessionId: string): void {\n if (syncedSessionId === sessionId) {\n return;\n }\n if (Date.now() - syncAttemptMs < SYNC_COOLDOWN_MS) {\n return;\n }\n syncSession(sessionId, getDeviceId(), getFpHash());\n}\n\nasync function onRotate(sessionId: string): Promise<void> {\n syncedSessionId = null;\n syncAttemptMs = Date.now();\n if (!target) {\n return;\n }\n const gen = generation;\n const [deviceId, fpHash] = await Promise.all([\n whenDeviceReady(),\n whenFingerprintReady(),\n ]);\n if (gen !== generation) {\n return;\n }\n log.debug(\n \"POST session %s (device=%s fp=%s)\",\n sessionId,\n deviceId ?? \"pending\",\n fpHash ?? \"none\"\n );\n syncSession(sessionId, deviceId, fpHash);\n}\n\nexport function bootstrap(sessionTarget: IngestTarget): void {\n target = sessionTarget;\n initDevice();\n detectCountryCode();\n\n mgr = new SessionManager((id) => {\n onRotate(id).catch(() => {\n /* best-effort */\n });\n });\n}\n\nexport const device = {\n getDeviceId(): string | null {\n return getDeviceId();\n },\n\n getFpHash(): string | null {\n return getFpHash();\n },\n};\n\nexport const session = {\n getId(): string | null {\n const id = mgr?.getSessionId() ?? null;\n if (id) {\n ensureSynced(id);\n }\n return id;\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 async set(params: IdentifyParams): Promise<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 gen = generation;\n const [deviceId, fpHash, country] = await Promise.all([\n whenDeviceReady(),\n whenFingerprintReady(),\n detectCountryCode(),\n ]);\n if (gen !== generation || !target) {\n identifiedSessionId = null;\n return;\n }\n log.info(\"PUT session %s → user %s\", sessionId, params.identifier);\n fetch(target.url, {\n method: \"PUT\",\n headers: buildHeaders(target.headers),\n body: JSON.stringify({\n sessionId,\n deviceId,\n fpHash,\n ...params,\n ...(country && { country }),\n }),\n keepalive: true,\n signal: AbortSignal.timeout(10_000),\n }).catch(() => {\n identifiedSessionId = null;\n log.warn(\"identify failed for session %s\", sessionId);\n });\n },\n\n clear(): void {\n currentIdentity = null;\n identifiedSessionId = null;\n },\n};\n\nexport function teardown(): void {\n generation += 1;\n identity.clear();\n resetGeo();\n syncedSessionId = null;\n syncAttemptMs = 0;\n mgr = null;\n target = null;\n}\n"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ //#region src/tracking/geo.d.ts
2
+ declare function detectCountryCode(): Promise<string | null>;
3
+ declare function resetGeo(): void;
4
+ //#endregion
5
+ export { detectCountryCode, resetGeo };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geo.d.mts","names":[],"sources":["../../src/tracking/geo.ts"],"mappings":";iBAwCgB,iBAAA,CAAA,GAAqB,OAAA;AAAA,iBAuBrB,QAAA,CAAA"}
@@ -0,0 +1,44 @@
1
+ import { cloudflareTraceSchema } from "@interfere/types/sdk/geo";
2
+ //#region src/tracking/geo.ts
3
+ const CF_TRACE_URL = "https://cloudflare.com/cdn-cgi/trace";
4
+ let cached = null;
5
+ let pending = null;
6
+ let failed = false;
7
+ function parseTrace(text) {
8
+ const result = {};
9
+ for (const line of text.split("\n")) {
10
+ const idx = line.indexOf("=");
11
+ if (idx > 0) result[line.slice(0, idx)] = line.slice(idx + 1);
12
+ }
13
+ return result;
14
+ }
15
+ async function fetchCountryCode() {
16
+ try {
17
+ const res = await fetch(CF_TRACE_URL, { signal: AbortSignal.timeout(3e3) });
18
+ if (!res.ok) return null;
19
+ const raw = parseTrace(await res.text());
20
+ const trace = cloudflareTraceSchema.safeParse(raw);
21
+ if (!trace.success) return null;
22
+ return trace.data.loc.toUpperCase();
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+ function detectCountryCode() {
28
+ if (cached) return Promise.resolve(cached);
29
+ if (failed) return Promise.resolve(null);
30
+ if (!pending) pending = fetchCountryCode().then((code) => {
31
+ cached = code;
32
+ if (!code) failed = true;
33
+ pending = null;
34
+ return code;
35
+ });
36
+ return pending;
37
+ }
38
+ function resetGeo() {
39
+ cached = null;
40
+ pending = null;
41
+ failed = false;
42
+ }
43
+ //#endregion
44
+ export { detectCountryCode, resetGeo };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geo.mjs","names":[],"sources":["../../src/tracking/geo.ts"],"sourcesContent":["import { cloudflareTraceSchema } from \"@interfere/types/sdk/geo\";\n\nconst CF_TRACE_URL = \"https://cloudflare.com/cdn-cgi/trace\";\n\nlet cached: string | null = null;\nlet pending: Promise<string | null> | null = null;\nlet failed = false;\n\nfunction parseTrace(text: string): Record<string, string> {\n const result: Record<string, string> = {};\n for (const line of text.split(\"\\n\")) {\n const idx = line.indexOf(\"=\");\n if (idx > 0) {\n result[line.slice(0, idx)] = line.slice(idx + 1);\n }\n }\n return result;\n}\n\nasync function fetchCountryCode(): Promise<string | null> {\n try {\n const res = await fetch(CF_TRACE_URL, {\n signal: AbortSignal.timeout(3000),\n });\n if (!res.ok) {\n return null;\n }\n\n const raw = parseTrace(await res.text());\n const trace = cloudflareTraceSchema.safeParse(raw);\n if (!trace.success) {\n return null;\n }\n\n return trace.data.loc.toUpperCase();\n } catch {\n return null;\n }\n}\n\nexport function detectCountryCode(): Promise<string | null> {\n if (cached) {\n return Promise.resolve(cached);\n }\n\n if (failed) {\n return Promise.resolve(null);\n }\n\n if (!pending) {\n pending = fetchCountryCode().then((code) => {\n cached = code;\n if (!code) {\n failed = true;\n }\n pending = null;\n return code;\n });\n }\n\n return pending;\n}\n\nexport function resetGeo(): void {\n cached = null;\n pending = null;\n failed = false;\n}\n"],"mappings":";;AAEA,MAAM,eAAe;AAErB,IAAI,SAAwB;AAC5B,IAAI,UAAyC;AAC7C,IAAI,SAAS;AAEb,SAAS,WAAW,MAAsC;CACxD,MAAM,SAAiC,EAAE;AACzC,MAAK,MAAM,QAAQ,KAAK,MAAM,KAAK,EAAE;EACnC,MAAM,MAAM,KAAK,QAAQ,IAAI;AAC7B,MAAI,MAAM,EACR,QAAO,KAAK,MAAM,GAAG,IAAI,IAAI,KAAK,MAAM,MAAM,EAAE;;AAGpD,QAAO;;AAGT,eAAe,mBAA2C;AACxD,KAAI;EACF,MAAM,MAAM,MAAM,MAAM,cAAc,EACpC,QAAQ,YAAY,QAAQ,IAAK,EAClC,CAAC;AACF,MAAI,CAAC,IAAI,GACP,QAAO;EAGT,MAAM,MAAM,WAAW,MAAM,IAAI,MAAM,CAAC;EACxC,MAAM,QAAQ,sBAAsB,UAAU,IAAI;AAClD,MAAI,CAAC,MAAM,QACT,QAAO;AAGT,SAAO,MAAM,KAAK,IAAI,aAAa;SAC7B;AACN,SAAO;;;AAIX,SAAgB,oBAA4C;AAC1D,KAAI,OACF,QAAO,QAAQ,QAAQ,OAAO;AAGhC,KAAI,OACF,QAAO,QAAQ,QAAQ,KAAK;AAG9B,KAAI,CAAC,QACH,WAAU,kBAAkB,CAAC,MAAM,SAAS;AAC1C,WAAS;AACT,MAAI,CAAC,KACH,UAAS;AAEX,YAAU;AACV,SAAO;GACP;AAGJ,QAAO;;AAGT,SAAgB,WAAiB;AAC/B,UAAS;AACT,WAAU;AACV,UAAS"}
@@ -5,6 +5,8 @@ import { Envelope } from "@interfere/types/sdk/envelope";
5
5
  interface QueueOptions {
6
6
  batchMs?: number;
7
7
  batchSize?: number;
8
+ /** @internal Test-only. Override the service worker check. */
9
+ isServiceWorkerActive?: () => boolean;
8
10
  maxBufferSize?: number;
9
11
  transport: HttpTransport;
10
12
  }
@@ -18,6 +20,7 @@ declare class BatchQueue {
18
20
  private readonly batchMs;
19
21
  private readonly maxBufferSize;
20
22
  private readonly transport;
23
+ private readonly isServiceWorkerActive;
21
24
  constructor(opts: QueueOptions);
22
25
  start(): void;
23
26
  enqueue(envelope: Envelope): void;
@@ -1 +1 @@
1
- {"version":3,"file":"queue.d.mts","names":[],"sources":["../../src/transport/queue.ts"],"mappings":";;;;UAciB,YAAA;EACf,OAAA;EACA,SAAA;EACA,aAAA;EACA,SAAA,EAAW,aAAA;AAAA;AAAA,cAGA,UAAA;EAAA,iBACM,MAAA;EAAA,QACT,KAAA;EAAA,QACA,QAAA;EAAA,QACA,QAAA;EAAA,QACA,WAAA;EAAA,iBACS,SAAA;EAAA,iBACA,OAAA;EAAA,iBACA,aAAA;EAAA,iBACA,SAAA;cAEL,IAAA,EAAM,YAAA;EAOlB,KAAA,CAAA;EAeA,OAAA,CAAQ,QAAA,EAAU,QAAA;EAclB,KAAA,CAAA;EA2CA,OAAA,CAAA;EAAA,QAiBQ,KAAA;EAAA,iBASS,kBAAA;EAAA,iBAMA,cAAA;AAAA"}
1
+ {"version":3,"file":"queue.d.mts","names":[],"sources":["../../src/transport/queue.ts"],"mappings":";;;;UAciB,YAAA;EACf,OAAA;EACA,SAAA;EAF2B;EAI3B,qBAAA;EACA,aAAA;EACA,SAAA,EAAW,aAAA;AAAA;AAAA,cAGA,UAAA;EAAA,iBACM,MAAA;EAAA,QACT,KAAA;EAAA,QACA,QAAA;EAAA,QACA,QAAA;EAAA,QACA,WAAA;EAAA,iBACS,SAAA;EAAA,iBACA,OAAA;EAAA,iBACA,aAAA;EAAA,iBACA,SAAA;EAAA,iBACA,qBAAA;cAEL,IAAA,EAAM,YAAA;EAQlB,KAAA,CAAA;EAeA,OAAA,CAAQ,QAAA,EAAU,QAAA;EAclB,KAAA,CAAA;EA2CA,OAAA,CAAA;EAAA,QAiBQ,KAAA;EAAA,iBASS,kBAAA;EAAA,iBAMA,cAAA;AAAA"}
@@ -17,11 +17,13 @@ var BatchQueue = class {
17
17
  batchMs;
18
18
  maxBufferSize;
19
19
  transport;
20
+ isServiceWorkerActive;
20
21
  constructor(opts) {
21
22
  this.batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;
22
23
  this.batchMs = opts.batchMs ?? DEFAULT_BATCH_MS;
23
24
  this.maxBufferSize = opts.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE;
24
25
  this.transport = opts.transport;
26
+ this.isServiceWorkerActive = opts.isServiceWorkerActive ?? hasServiceWorker;
25
27
  }
26
28
  start() {
27
29
  if (this.timer) return;
@@ -55,7 +57,7 @@ var BatchQueue = class {
55
57
  if (this.failures > 0) log.info("send recovered after %d failures", this.failures);
56
58
  this.failures = 0;
57
59
  }).catch(() => {
58
- if (!hasServiceWorker()) this.buffer.unshift(...batch);
60
+ if (!this.isServiceWorkerActive()) this.buffer.unshift(...batch);
59
61
  this.failures++;
60
62
  if (this.failures >= BREAKER_THRESHOLD) {
61
63
  this.pausedUntil = Date.now() + BREAKER_COOLDOWN_MS;
@@ -1 +1 @@
1
- {"version":3,"file":"queue.mjs","names":[],"sources":["../../src/transport/queue.ts"],"sourcesContent":["import type { Envelope } from \"@interfere/types/sdk/envelope\";\n\nimport { createLogger } from \"../util/log.js\";\nimport { type HttpTransport, hasServiceWorker } from \"./http.js\";\n\nconst log = createLogger(\"queue\");\n\nconst DEFAULT_BATCH_SIZE = 10;\nconst DEFAULT_BATCH_MS = 250;\nconst DEFAULT_MAX_BUFFER_SIZE = 1000;\n\nconst BREAKER_THRESHOLD = 5;\nconst BREAKER_COOLDOWN_MS = 30_000;\n\nexport interface QueueOptions {\n batchMs?: number;\n batchSize?: number;\n maxBufferSize?: number;\n transport: HttpTransport;\n}\n\nexport class BatchQueue {\n private readonly buffer: Envelope[] = [];\n private timer: ReturnType<typeof setInterval> | null = null;\n private flushing = false;\n private failures = 0;\n private pausedUntil = 0;\n private readonly batchSize: number;\n private readonly batchMs: number;\n private readonly maxBufferSize: number;\n private readonly transport: HttpTransport;\n\n constructor(opts: QueueOptions) {\n this.batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;\n this.batchMs = opts.batchMs ?? DEFAULT_BATCH_MS;\n this.maxBufferSize = opts.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE;\n this.transport = opts.transport;\n }\n\n start(): void {\n if (this.timer) {\n return;\n }\n\n this.timer = setInterval(() => {\n this.flush();\n }, this.batchMs);\n\n if (typeof globalThis.addEventListener === \"function\") {\n globalThis.addEventListener(\"visibilitychange\", this.onVisibilityChange);\n globalThis.addEventListener(\"beforeunload\", this.onBeforeUnload);\n }\n }\n\n enqueue(envelope: Envelope): void {\n this.buffer.push(envelope);\n\n if (this.buffer.length > this.maxBufferSize) {\n const overflow = this.buffer.length - this.maxBufferSize;\n this.buffer.splice(0, overflow);\n log.warn(\"buffer full, dropped %d oldest envelopes\", overflow);\n }\n\n if (this.buffer.length >= this.batchSize) {\n this.flush();\n }\n }\n\n flush(): void {\n if (this.flushing || this.buffer.length === 0) {\n return;\n }\n\n if (this.failures >= BREAKER_THRESHOLD && Date.now() < this.pausedUntil) {\n return;\n }\n\n this.flushing = true;\n const batch = this.buffer.splice(0, this.batchSize);\n const meta = {\n retryCount: this.failures,\n queueDepth: this.buffer.length,\n };\n\n this.transport\n .send(batch, meta)\n .then(() => {\n if (this.failures > 0) {\n log.info(\"send recovered after %d failures\", this.failures);\n }\n this.failures = 0;\n })\n .catch(() => {\n if (!hasServiceWorker()) {\n this.buffer.unshift(...batch);\n }\n this.failures++;\n if (this.failures >= BREAKER_THRESHOLD) {\n this.pausedUntil = Date.now() + BREAKER_COOLDOWN_MS;\n log.warn(\n \"pausing sends for %dms after %d consecutive failures\",\n BREAKER_COOLDOWN_MS,\n this.failures\n );\n }\n })\n .finally(() => {\n this.flushing = false;\n });\n }\n\n dispose(): void {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n\n if (typeof globalThis.removeEventListener === \"function\") {\n globalThis.removeEventListener(\n \"visibilitychange\",\n this.onVisibilityChange\n );\n globalThis.removeEventListener(\"beforeunload\", this.onBeforeUnload);\n }\n\n this.drain();\n }\n\n private drain(): void {\n while (this.buffer.length > 0) {\n const batch = this.buffer.splice(0, this.batchSize);\n this.transport.send(batch).catch(() => {\n /* best-effort — SW BackgroundSync handles persistence */\n });\n }\n }\n\n private readonly onVisibilityChange = (): void => {\n if (document.visibilityState === \"hidden\") {\n this.drain();\n }\n };\n\n private readonly onBeforeUnload = (): void => {\n this.drain();\n };\n}\n"],"mappings":";;;AAKA,MAAM,MAAM,aAAa,QAAQ;AAEjC,MAAM,qBAAqB;AAC3B,MAAM,mBAAmB;AACzB,MAAM,0BAA0B;AAEhC,MAAM,oBAAoB;AAC1B,MAAM,sBAAsB;AAS5B,IAAa,aAAb,MAAwB;CACtB,SAAsC,EAAE;CACxC,QAAuD;CACvD,WAAmB;CACnB,WAAmB;CACnB,cAAsB;CACtB;CACA;CACA;CACA;CAEA,YAAY,MAAoB;AAC9B,OAAK,YAAY,KAAK,aAAa;AACnC,OAAK,UAAU,KAAK,WAAW;AAC/B,OAAK,gBAAgB,KAAK,iBAAiB;AAC3C,OAAK,YAAY,KAAK;;CAGxB,QAAc;AACZ,MAAI,KAAK,MACP;AAGF,OAAK,QAAQ,kBAAkB;AAC7B,QAAK,OAAO;KACX,KAAK,QAAQ;AAEhB,MAAI,OAAO,WAAW,qBAAqB,YAAY;AACrD,cAAW,iBAAiB,oBAAoB,KAAK,mBAAmB;AACxE,cAAW,iBAAiB,gBAAgB,KAAK,eAAe;;;CAIpE,QAAQ,UAA0B;AAChC,OAAK,OAAO,KAAK,SAAS;AAE1B,MAAI,KAAK,OAAO,SAAS,KAAK,eAAe;GAC3C,MAAM,WAAW,KAAK,OAAO,SAAS,KAAK;AAC3C,QAAK,OAAO,OAAO,GAAG,SAAS;AAC/B,OAAI,KAAK,4CAA4C,SAAS;;AAGhE,MAAI,KAAK,OAAO,UAAU,KAAK,UAC7B,MAAK,OAAO;;CAIhB,QAAc;AACZ,MAAI,KAAK,YAAY,KAAK,OAAO,WAAW,EAC1C;AAGF,MAAI,KAAK,YAAY,qBAAqB,KAAK,KAAK,GAAG,KAAK,YAC1D;AAGF,OAAK,WAAW;EAChB,MAAM,QAAQ,KAAK,OAAO,OAAO,GAAG,KAAK,UAAU;EACnD,MAAM,OAAO;GACX,YAAY,KAAK;GACjB,YAAY,KAAK,OAAO;GACzB;AAED,OAAK,UACF,KAAK,OAAO,KAAK,CACjB,WAAW;AACV,OAAI,KAAK,WAAW,EAClB,KAAI,KAAK,oCAAoC,KAAK,SAAS;AAE7D,QAAK,WAAW;IAChB,CACD,YAAY;AACX,OAAI,CAAC,kBAAkB,CACrB,MAAK,OAAO,QAAQ,GAAG,MAAM;AAE/B,QAAK;AACL,OAAI,KAAK,YAAY,mBAAmB;AACtC,SAAK,cAAc,KAAK,KAAK,GAAG;AAChC,QAAI,KACF,wDACA,qBACA,KAAK,SACN;;IAEH,CACD,cAAc;AACb,QAAK,WAAW;IAChB;;CAGN,UAAgB;AACd,MAAI,KAAK,OAAO;AACd,iBAAc,KAAK,MAAM;AACzB,QAAK,QAAQ;;AAGf,MAAI,OAAO,WAAW,wBAAwB,YAAY;AACxD,cAAW,oBACT,oBACA,KAAK,mBACN;AACD,cAAW,oBAAoB,gBAAgB,KAAK,eAAe;;AAGrE,OAAK,OAAO;;CAGd,QAAsB;AACpB,SAAO,KAAK,OAAO,SAAS,GAAG;GAC7B,MAAM,QAAQ,KAAK,OAAO,OAAO,GAAG,KAAK,UAAU;AACnD,QAAK,UAAU,KAAK,MAAM,CAAC,YAAY,GAErC;;;CAIN,2BAAkD;AAChD,MAAI,SAAS,oBAAoB,SAC/B,MAAK,OAAO;;CAIhB,uBAA8C;AAC5C,OAAK,OAAO"}
1
+ {"version":3,"file":"queue.mjs","names":[],"sources":["../../src/transport/queue.ts"],"sourcesContent":["import type { Envelope } from \"@interfere/types/sdk/envelope\";\n\nimport { createLogger } from \"../util/log.js\";\nimport { type HttpTransport, hasServiceWorker } from \"./http.js\";\n\nconst log = createLogger(\"queue\");\n\nconst DEFAULT_BATCH_SIZE = 10;\nconst DEFAULT_BATCH_MS = 250;\nconst DEFAULT_MAX_BUFFER_SIZE = 1000;\n\nconst BREAKER_THRESHOLD = 5;\nconst BREAKER_COOLDOWN_MS = 30_000;\n\nexport interface QueueOptions {\n batchMs?: number;\n batchSize?: number;\n /** @internal Test-only. Override the service worker check. */\n isServiceWorkerActive?: () => boolean;\n maxBufferSize?: number;\n transport: HttpTransport;\n}\n\nexport class BatchQueue {\n private readonly buffer: Envelope[] = [];\n private timer: ReturnType<typeof setInterval> | null = null;\n private flushing = false;\n private failures = 0;\n private pausedUntil = 0;\n private readonly batchSize: number;\n private readonly batchMs: number;\n private readonly maxBufferSize: number;\n private readonly transport: HttpTransport;\n private readonly isServiceWorkerActive: () => boolean;\n\n constructor(opts: QueueOptions) {\n this.batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;\n this.batchMs = opts.batchMs ?? DEFAULT_BATCH_MS;\n this.maxBufferSize = opts.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE;\n this.transport = opts.transport;\n this.isServiceWorkerActive = opts.isServiceWorkerActive ?? hasServiceWorker;\n }\n\n start(): void {\n if (this.timer) {\n return;\n }\n\n this.timer = setInterval(() => {\n this.flush();\n }, this.batchMs);\n\n if (typeof globalThis.addEventListener === \"function\") {\n globalThis.addEventListener(\"visibilitychange\", this.onVisibilityChange);\n globalThis.addEventListener(\"beforeunload\", this.onBeforeUnload);\n }\n }\n\n enqueue(envelope: Envelope): void {\n this.buffer.push(envelope);\n\n if (this.buffer.length > this.maxBufferSize) {\n const overflow = this.buffer.length - this.maxBufferSize;\n this.buffer.splice(0, overflow);\n log.warn(\"buffer full, dropped %d oldest envelopes\", overflow);\n }\n\n if (this.buffer.length >= this.batchSize) {\n this.flush();\n }\n }\n\n flush(): void {\n if (this.flushing || this.buffer.length === 0) {\n return;\n }\n\n if (this.failures >= BREAKER_THRESHOLD && Date.now() < this.pausedUntil) {\n return;\n }\n\n this.flushing = true;\n const batch = this.buffer.splice(0, this.batchSize);\n const meta = {\n retryCount: this.failures,\n queueDepth: this.buffer.length,\n };\n\n this.transport\n .send(batch, meta)\n .then(() => {\n if (this.failures > 0) {\n log.info(\"send recovered after %d failures\", this.failures);\n }\n this.failures = 0;\n })\n .catch(() => {\n if (!this.isServiceWorkerActive()) {\n this.buffer.unshift(...batch);\n }\n this.failures++;\n if (this.failures >= BREAKER_THRESHOLD) {\n this.pausedUntil = Date.now() + BREAKER_COOLDOWN_MS;\n log.warn(\n \"pausing sends for %dms after %d consecutive failures\",\n BREAKER_COOLDOWN_MS,\n this.failures\n );\n }\n })\n .finally(() => {\n this.flushing = false;\n });\n }\n\n dispose(): void {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n\n if (typeof globalThis.removeEventListener === \"function\") {\n globalThis.removeEventListener(\n \"visibilitychange\",\n this.onVisibilityChange\n );\n globalThis.removeEventListener(\"beforeunload\", this.onBeforeUnload);\n }\n\n this.drain();\n }\n\n private drain(): void {\n while (this.buffer.length > 0) {\n const batch = this.buffer.splice(0, this.batchSize);\n this.transport.send(batch).catch(() => {\n /* best-effort — SW BackgroundSync handles persistence */\n });\n }\n }\n\n private readonly onVisibilityChange = (): void => {\n if (document.visibilityState === \"hidden\") {\n this.drain();\n }\n };\n\n private readonly onBeforeUnload = (): void => {\n this.drain();\n };\n}\n"],"mappings":";;;AAKA,MAAM,MAAM,aAAa,QAAQ;AAEjC,MAAM,qBAAqB;AAC3B,MAAM,mBAAmB;AACzB,MAAM,0BAA0B;AAEhC,MAAM,oBAAoB;AAC1B,MAAM,sBAAsB;AAW5B,IAAa,aAAb,MAAwB;CACtB,SAAsC,EAAE;CACxC,QAAuD;CACvD,WAAmB;CACnB,WAAmB;CACnB,cAAsB;CACtB;CACA;CACA;CACA;CACA;CAEA,YAAY,MAAoB;AAC9B,OAAK,YAAY,KAAK,aAAa;AACnC,OAAK,UAAU,KAAK,WAAW;AAC/B,OAAK,gBAAgB,KAAK,iBAAiB;AAC3C,OAAK,YAAY,KAAK;AACtB,OAAK,wBAAwB,KAAK,yBAAyB;;CAG7D,QAAc;AACZ,MAAI,KAAK,MACP;AAGF,OAAK,QAAQ,kBAAkB;AAC7B,QAAK,OAAO;KACX,KAAK,QAAQ;AAEhB,MAAI,OAAO,WAAW,qBAAqB,YAAY;AACrD,cAAW,iBAAiB,oBAAoB,KAAK,mBAAmB;AACxE,cAAW,iBAAiB,gBAAgB,KAAK,eAAe;;;CAIpE,QAAQ,UAA0B;AAChC,OAAK,OAAO,KAAK,SAAS;AAE1B,MAAI,KAAK,OAAO,SAAS,KAAK,eAAe;GAC3C,MAAM,WAAW,KAAK,OAAO,SAAS,KAAK;AAC3C,QAAK,OAAO,OAAO,GAAG,SAAS;AAC/B,OAAI,KAAK,4CAA4C,SAAS;;AAGhE,MAAI,KAAK,OAAO,UAAU,KAAK,UAC7B,MAAK,OAAO;;CAIhB,QAAc;AACZ,MAAI,KAAK,YAAY,KAAK,OAAO,WAAW,EAC1C;AAGF,MAAI,KAAK,YAAY,qBAAqB,KAAK,KAAK,GAAG,KAAK,YAC1D;AAGF,OAAK,WAAW;EAChB,MAAM,QAAQ,KAAK,OAAO,OAAO,GAAG,KAAK,UAAU;EACnD,MAAM,OAAO;GACX,YAAY,KAAK;GACjB,YAAY,KAAK,OAAO;GACzB;AAED,OAAK,UACF,KAAK,OAAO,KAAK,CACjB,WAAW;AACV,OAAI,KAAK,WAAW,EAClB,KAAI,KAAK,oCAAoC,KAAK,SAAS;AAE7D,QAAK,WAAW;IAChB,CACD,YAAY;AACX,OAAI,CAAC,KAAK,uBAAuB,CAC/B,MAAK,OAAO,QAAQ,GAAG,MAAM;AAE/B,QAAK;AACL,OAAI,KAAK,YAAY,mBAAmB;AACtC,SAAK,cAAc,KAAK,KAAK,GAAG;AAChC,QAAI,KACF,wDACA,qBACA,KAAK,SACN;;IAEH,CACD,cAAc;AACb,QAAK,WAAW;IAChB;;CAGN,UAAgB;AACd,MAAI,KAAK,OAAO;AACd,iBAAc,KAAK,MAAM;AACzB,QAAK,QAAQ;;AAGf,MAAI,OAAO,WAAW,wBAAwB,YAAY;AACxD,cAAW,oBACT,oBACA,KAAK,mBACN;AACD,cAAW,oBAAoB,gBAAgB,KAAK,eAAe;;AAGrE,OAAK,OAAO;;CAGd,QAAsB;AACpB,SAAO,KAAK,OAAO,SAAS,GAAG;GAC7B,MAAM,QAAQ,KAAK,OAAO,OAAO,GAAG,KAAK,UAAU;AACnD,QAAK,UAAU,KAAK,MAAM,CAAC,YAAY,GAErC;;;CAIN,2BAAkD;AAChD,MAAI,SAAS,oBAAoB,SAC/B,MAAK,OAAO;;CAIhB,uBAA8C;AAC5C,OAAK,OAAO"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interfere/react",
3
- "version": "8.0.0",
3
+ "version": "8.1.0",
4
4
  "license": "MIT",
5
5
  "description": "Client-side React SDK for Interfere. Error tracking, session replay, and analytics.",
6
6
  "keywords": [
@@ -48,15 +48,13 @@
48
48
  "scripts": {
49
49
  "build": "tsdown",
50
50
  "dev": "tsdown --watch",
51
- "test:e2e": "vitest run --project browser",
52
- "test:unit": "vitest run --project unit --coverage",
53
- "typecheck": "tsc --noEmit --incremental",
54
- "test": "bun run test:unit"
51
+ "test": "vitest run --coverage",
52
+ "typecheck": "tsc --noEmit --incremental"
55
53
  },
56
54
  "dependencies": {
57
55
  "@fingerprintjs/fingerprintjs": "^5.1.0",
58
- "@interfere/constants": "^8.0.0",
59
- "@interfere/types": "^8.0.0",
56
+ "@interfere/constants": "^8.1.0",
57
+ "@interfere/types": "^8.1.0",
60
58
  "@ua-parser-js/pro-enterprise": "^2.0.6",
61
59
  "rrweb": "2.0.0-alpha.4",
62
60
  "uuid": "^13.0.0"
@@ -66,21 +64,20 @@
66
64
  "react-dom": ">=19"
67
65
  },
68
66
  "devDependencies": {
69
- "@interfere/typescript-config": "^8.0.0",
70
- "@interfere/vitest-config": "^8.0.0",
67
+ "@interfere/typescript-config": "^8.1.0",
68
+ "@interfere/test-utils": "^1.0.0",
71
69
  "@rrweb/types": "2.0.0-alpha.20",
72
- "@testing-library/react": "^16.3.2",
73
70
  "@types/node": "^24.12.0",
74
71
  "@types/react": "19.2.14",
75
72
  "@types/react-dom": "19.2.3",
76
73
  "@vitejs/plugin-react": "^6.0.1",
77
- "@vitest/browser": "4.1.0",
78
- "@vitest/browser-playwright": "4.1.0",
79
- "@vitest/coverage-v8": "^4.0.18",
80
- "jsdom": "^29.0.1",
81
- "playwright": "^1.56.1",
82
- "tsdown": "^0.21.4",
83
- "typescript": "5.9.3",
84
- "vitest": "^4.0.18"
74
+ "@vitest/browser": "4.1.2",
75
+ "@vitest/browser-playwright": "4.1.2",
76
+ "@vitest/coverage-v8": "^4.1.2",
77
+ "playwright": "^1.59.0",
78
+ "tsdown": "^0.21.6",
79
+ "typescript": "6.0.2",
80
+ "vitest": "^4.1.2",
81
+ "vitest-browser-react": "2.1.0"
85
82
  }
86
83
  }