@interfere/react 4.0.0 → 5.0.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.
Files changed (50) hide show
  1. package/dist/internal/client.d.mts +1 -0
  2. package/dist/internal/client.d.mts.map +1 -1
  3. package/dist/internal/client.mjs +19 -1
  4. package/dist/internal/client.mjs.map +1 -1
  5. package/dist/internal/config.d.mts +1 -0
  6. package/dist/internal/config.d.mts.map +1 -1
  7. package/dist/internal/config.mjs +5 -0
  8. package/dist/internal/config.mjs.map +1 -1
  9. package/dist/internal/envelope.d.mts.map +1 -1
  10. package/dist/internal/envelope.mjs +2 -0
  11. package/dist/internal/envelope.mjs.map +1 -1
  12. package/dist/internal/plugin-runtime.d.mts +3 -0
  13. package/dist/internal/plugin-runtime.d.mts.map +1 -1
  14. package/dist/internal/plugin-runtime.mjs +14 -3
  15. package/dist/internal/plugin-runtime.mjs.map +1 -1
  16. package/dist/internal/version.d.mts +4 -0
  17. package/dist/internal/version.d.mts.map +1 -0
  18. package/dist/internal/version.mjs +5 -0
  19. package/dist/internal/version.mjs.map +1 -0
  20. package/dist/package.mjs +5 -0
  21. package/dist/package.mjs.map +1 -0
  22. package/dist/plugins/device.d.mts +6 -0
  23. package/dist/plugins/device.d.mts.map +1 -0
  24. package/dist/plugins/device.mjs +13 -0
  25. package/dist/plugins/device.mjs.map +1 -0
  26. package/dist/plugins/lib/loader.mjs +1 -1
  27. package/dist/plugins/lib/loader.mjs.map +1 -1
  28. package/dist/tracking/api.d.mts.map +1 -1
  29. package/dist/tracking/api.mjs +12 -9
  30. package/dist/tracking/api.mjs.map +1 -1
  31. package/dist/tracking/device.d.mts +8 -0
  32. package/dist/tracking/device.d.mts.map +1 -0
  33. package/dist/tracking/device.mjs +76 -0
  34. package/dist/tracking/device.mjs.map +1 -0
  35. package/dist/transport/http.d.mts +7 -3
  36. package/dist/transport/http.d.mts.map +1 -1
  37. package/dist/transport/http.mjs +14 -8
  38. package/dist/transport/http.mjs.map +1 -1
  39. package/dist/transport/queue.d.mts.map +1 -1
  40. package/dist/transport/queue.mjs +5 -1
  41. package/dist/transport/queue.mjs.map +1 -1
  42. package/package.json +6 -7
  43. package/dist/plugins/fingerprint.d.mts +0 -6
  44. package/dist/plugins/fingerprint.d.mts.map +0 -1
  45. package/dist/plugins/fingerprint.mjs +0 -13
  46. package/dist/plugins/fingerprint.mjs.map +0 -1
  47. package/dist/tracking/visitor.d.mts +0 -7
  48. package/dist/tracking/visitor.d.mts.map +0 -1
  49. package/dist/tracking/visitor.mjs +0 -40
  50. package/dist/tracking/visitor.mjs.map +0 -1
@@ -14,6 +14,7 @@ declare class Client {
14
14
  private readonly queue;
15
15
  private readonly runtime;
16
16
  constructor(opts: ClientOptions, buildId: string, releaseId: string | null);
17
+ private fetchRemoteConfig;
17
18
  capture<T extends EventType>(type: T, payload: EnvelopePayload<T>): void;
18
19
  flush(): void;
19
20
  dispose(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.mts","names":[],"sources":["../../src/internal/client.ts"],"mappings":";;;;;;UAiBiB,aAAA;EACf,KAAA,GAAQ,IAAA,CAAK,OAAA,CAAQ,YAAA;EACrB,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
+ {"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;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;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,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,5 +1,5 @@
1
1
  import { createLogger } from "../util/log.mjs";
2
- import { HttpTransport } from "../transport/http.mjs";
2
+ import { HttpTransport, buildHeaders } from "../transport/http.mjs";
3
3
  import { bootstrap, session, teardown } from "../tracking/api.mjs";
4
4
  import { BatchQueue } from "../transport/queue.mjs";
5
5
  import { resolveTargets } from "./config.mjs";
@@ -36,6 +36,24 @@ var Client = class {
36
36
  getSessionId: () => session.getId() ?? ""
37
37
  }, opts.plugins, opts.consent);
38
38
  this.runtime.start();
39
+ this.fetchRemoteConfig(targets.config);
40
+ }
41
+ fetchRemoteConfig(configTarget) {
42
+ fetch(configTarget.url, {
43
+ method: "GET",
44
+ headers: buildHeaders(configTarget.headers),
45
+ signal: AbortSignal.timeout(1e4)
46
+ }).then((res) => {
47
+ if (!res.ok) return;
48
+ return res.json();
49
+ }).then((config) => {
50
+ if (config?.plugins) {
51
+ this.runtime.applyRemoteConfig(config.plugins);
52
+ log.debug("applied remote config");
53
+ }
54
+ }).catch(() => {
55
+ log.warn("remote config fetch failed, using local defaults");
56
+ });
39
57
  }
40
58
  capture(type, payload) {
41
59
  const sessionId = session.getId();
@@ -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 { 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, 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 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\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;AAQlC,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,KAAA,IAAY,QAAQ,IAAI,SAC1D;GACD,SAAS,cAAc;GACvB;GACA;GACD;AAED,yBAAuB;AAGvB,OAAK,QAAQ,IAAI,WAAW;GAAE,WADZ,IAAI,cAAc,QAAQ,OAAO;GACV,GAAG,KAAK;GAAO,CAAC;AACzD,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"}
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 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\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":";;;;;;;;;;;AAgBA,MAAM,MAAM,aAAa,SAAS;AAQlC,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,KAAA,IAAY,QAAQ,IAAI,SAC1D;GACD,SAAS,cAAc;GACvB;GACA;GACD;AAED,yBAAuB;AAGvB,OAAK,QAAQ,IAAI,WAAW;GAAE,WADZ,IAAI,cAAc,QAAQ,OAAO;GACV,GAAG,KAAK;GAAO,CAAC;AACzD,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;AAEpB,OAAK,kBAAkB,QAAQ,OAAO;;CAGxC,kBAA0B,cAGjB;AACP,QAAM,aAAa,KAAK;GACtB,QAAQ;GACR,SAAS,aAAa,aAAa,QAAQ;GAC3C,QAAQ,YAAY,QAAQ,IAAO;GACpC,CAAC,CACC,MAAM,QAAQ;AACb,OAAI,CAAC,IAAI,GACP;AAEF,UAAO,IAAI,MAAM;IACjB,CACD,MAAM,WAAW;AAChB,OAAI,QAAQ,SAAS;AACnB,SAAK,QAAQ,kBAAkB,OAAO,QAAQ;AAC9C,QAAI,MAAM,wBAAwB;;IAEpC,CACD,YAAY;AACX,OAAI,KAAK,mDAAmD;IAC5D;;CAGN,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"}
@@ -2,6 +2,7 @@ import { IngestTarget } from "../transport/http.mjs";
2
2
 
3
3
  //#region src/internal/config.d.ts
4
4
  declare function resolveTargets(): {
5
+ config: IngestTarget;
5
6
  ingest: IngestTarget;
6
7
  session: IngestTarget;
7
8
  };
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.mts","names":[],"sources":["../../src/internal/config.ts"],"mappings":";;;iBAcgB,cAAA,CAAA;EACd,MAAA,EAAQ,YAAA;EACR,OAAA,EAAS,YAAA;AAAA"}
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"}
@@ -2,6 +2,7 @@ import { API_PATHS, API_URL } from "@interfere/constants/api";
2
2
  //#region src/internal/config.ts
3
3
  const DEFAULT_PROXY_URL = "/api/interfere";
4
4
  const DEFAULT_SESSION_PATH = "/v1/session";
5
+ const DEFAULT_CONFIG_PATH = "/v1/config";
5
6
  function resolvePublicKey() {
6
7
  if (typeof process === "undefined") return;
7
8
  return process.env.INTERFERE_PUBLIC_KEY ?? void 0;
@@ -13,6 +14,10 @@ function resolveTargets() {
13
14
  const baseUrl = publicKey ? API_URL : DEFAULT_PROXY_URL;
14
15
  const sessionPath = API_PATHS.SESSION ?? DEFAULT_SESSION_PATH;
15
16
  return {
17
+ config: {
18
+ url: `${baseUrl}${API_PATHS.CONFIG ?? DEFAULT_CONFIG_PATH}`,
19
+ headers
20
+ },
16
21
  ingest: {
17
22
  url: `${baseUrl}${API_PATHS.INGEST}`,
18
23
  headers
@@ -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\";\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 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\n return {\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;AAE7B,SAAS,mBAAuC;AAC9C,KAAI,OAAO,YAAY,YACrB;AAEF,QAAO,QAAQ,IAAI,wBAAwB,KAAA;;AAG7C,SAAgB,iBAGd;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;AAEjD,QAAO;EACL,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 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,wBAAwB,KAAA;;AAG7C,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 +1 @@
1
- {"version":3,"file":"envelope.d.mts","names":[],"sources":["../../src/internal/envelope.ts"],"mappings":";;;;UAUiB,gBAAA;EACf,OAAA;EACA,OAAA,EAAS,eAAA;EACT,WAAA,EAAa,GAAA;EACb,SAAA;EACA,OAAA,EAAS,OAAA;AAAA;AAAA,iBAGK,aAAA,WAAwB,SAAA,CAAA,CACtC,IAAA,EAAM,CAAA,EACN,OAAA,EAAS,eAAA,CAAgB,CAAA,GACzB,SAAA,UACA,QAAA,EAAU,gBAAA,GACT,QAAA,CAAS,CAAA"}
1
+ {"version":3,"file":"envelope.d.mts","names":[],"sources":["../../src/internal/envelope.ts"],"mappings":";;;;UAYiB,gBAAA;EACf,OAAA;EACA,OAAA,EAAS,eAAA;EACT,WAAA,EAAa,GAAA;EACb,SAAA;EACA,OAAA,EAAS,OAAA;AAAA;AAAA,iBAGK,aAAA,WAAwB,SAAA,CAAA,CACtC,IAAA,EAAM,CAAA,EACN,OAAA,EAAS,eAAA,CAAgB,CAAA,GACzB,SAAA,UACA,QAAA,EAAU,gBAAA,GACT,QAAA,CAAS,CAAA"}
@@ -1,3 +1,4 @@
1
+ import { PRODUCER_VERSION } from "./version.mjs";
1
2
  import { v7 } from "uuid";
2
3
  //#region src/internal/envelope.ts
3
4
  function buildEnvelope(type, payload, sessionId, metadata) {
@@ -11,6 +12,7 @@ function buildEnvelope(type, payload, sessionId, metadata) {
11
12
  environment: metadata.environment,
12
13
  buildId: metadata.buildId,
13
14
  releaseId: metadata.releaseId,
15
+ producerVersion: PRODUCER_VERSION,
14
16
  clientTs: Date.now(),
15
17
  sessionId,
16
18
  sessionSource: "client"
@@ -1 +1 @@
1
- {"version":3,"file":"envelope.mjs","names":["uuidv7"],"sources":["../../src/internal/envelope.ts"],"sourcesContent":["import type {\n Envelope,\n EnvelopeContext,\n EnvelopePayload,\n EventType,\n} from \"@interfere/types/sdk/envelope\";\nimport type { Env, Runtime } from \"@interfere/types/sdk/runtime\";\n\nimport { v7 as uuidv7 } from \"uuid\";\n\nexport interface EnvelopeMetadata {\n buildId: string;\n context: EnvelopeContext;\n environment: Env;\n releaseId: string | null;\n runtime: Runtime;\n}\n\nexport function buildEnvelope<T extends EventType>(\n type: T,\n payload: EnvelopePayload<T>,\n sessionId: string,\n metadata: EnvelopeMetadata\n): Envelope<T> {\n return {\n uuid: uuidv7(),\n v: 0 as const,\n type,\n payload,\n context: metadata.context,\n runtime: metadata.runtime,\n environment: metadata.environment,\n buildId: metadata.buildId,\n releaseId: metadata.releaseId,\n clientTs: Date.now(),\n sessionId,\n sessionSource: \"client\" as const,\n } as Envelope<T>;\n}\n"],"mappings":";;AAkBA,SAAgB,cACd,MACA,SACA,WACA,UACa;AACb,QAAO;EACL,MAAMA,IAAQ;EACd,GAAG;EACH;EACA;EACA,SAAS,SAAS;EAClB,SAAS,SAAS;EAClB,aAAa,SAAS;EACtB,SAAS,SAAS;EAClB,WAAW,SAAS;EACpB,UAAU,KAAK,KAAK;EACpB;EACA,eAAe;EAChB"}
1
+ {"version":3,"file":"envelope.mjs","names":["uuidv7"],"sources":["../../src/internal/envelope.ts"],"sourcesContent":["import type {\n Envelope,\n EnvelopeContext,\n EnvelopePayload,\n EventType,\n} from \"@interfere/types/sdk/envelope\";\nimport type { Env, Runtime } from \"@interfere/types/sdk/runtime\";\n\nimport { v7 as uuidv7 } from \"uuid\";\n\nimport { PRODUCER_VERSION } from \"./version.js\";\n\nexport interface EnvelopeMetadata {\n buildId: string;\n context: EnvelopeContext;\n environment: Env;\n releaseId: string | null;\n runtime: Runtime;\n}\n\nexport function buildEnvelope<T extends EventType>(\n type: T,\n payload: EnvelopePayload<T>,\n sessionId: string,\n metadata: EnvelopeMetadata\n): Envelope<T> {\n return {\n uuid: uuidv7(),\n v: 0 as const,\n type,\n payload,\n context: metadata.context,\n runtime: metadata.runtime,\n environment: metadata.environment,\n buildId: metadata.buildId,\n releaseId: metadata.releaseId,\n producerVersion: PRODUCER_VERSION,\n clientTs: Date.now(),\n sessionId,\n sessionSource: \"client\" as const,\n } as Envelope<T>;\n}\n"],"mappings":";;;AAoBA,SAAgB,cACd,MACA,SACA,WACA,UACa;AACb,QAAO;EACL,MAAMA,IAAQ;EACd,GAAG;EACH;EACA;EACA,SAAS,SAAS;EAClB,SAAS,SAAS;EAClB,aAAa,SAAS;EACtB,SAAS,SAAS;EAClB,WAAW,SAAS;EACpB,iBAAiB;EACjB,UAAU,KAAK,KAAK;EACpB;EACA,eAAe;EAChB"}
@@ -2,18 +2,21 @@ 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
4
  import { EventType } from "@interfere/types/sdk/envelope";
5
+ import { RemotePluginConfig } from "@interfere/types/sdk/remote-config";
5
6
 
6
7
  //#region src/internal/plugin-runtime.d.ts
7
8
  declare class PluginRuntime {
8
9
  private readonly activeCleanups;
9
10
  private readonly context;
10
11
  private readonly features;
12
+ private remoteConfig;
11
13
  private consentState;
12
14
  private syncVersion;
13
15
  constructor(context: PluginContext, overrides: PluginOverrides | undefined, initialConsent: ConsentState | undefined);
14
16
  getConsent(): ConsentState | null;
15
17
  setConsent(nextConsent?: ConsentState): void;
16
18
  resetConsent(): void;
19
+ applyRemoteConfig(config: RemotePluginConfig): void;
17
20
  canCapture(type: EventType): boolean;
18
21
  start(): void;
19
22
  dispose(): void;
@@ -1 +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"}
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"}
@@ -9,6 +9,7 @@ var PluginRuntime = class {
9
9
  activeCleanups = /* @__PURE__ */ new Map();
10
10
  context;
11
11
  features;
12
+ remoteConfig = {};
12
13
  consentState;
13
14
  syncVersion = 0;
14
15
  constructor(context, overrides, initialConsent) {
@@ -30,11 +31,15 @@ var PluginRuntime = class {
30
31
  this.consentState = null;
31
32
  this.sync();
32
33
  }
34
+ applyRemoteConfig(config) {
35
+ this.remoteConfig = config;
36
+ this.sync();
37
+ }
33
38
  canCapture(type) {
34
39
  return shouldCaptureEvent(type, this.consentState);
35
40
  }
36
41
  start() {
37
- if (this.features.errors) {
42
+ if (this.shouldEnablePlugin("errors")) {
38
43
  const cleanup = errorsPlugin.setup(this.context);
39
44
  if (cleanup) this.activeCleanups.set("errors", cleanup);
40
45
  }
@@ -44,7 +49,7 @@ var PluginRuntime = class {
44
49
  for (const key of this.activeCleanups.keys()) this.deactivate(key);
45
50
  }
46
51
  shouldEnablePlugin(key) {
47
- return this.features[key] && isConsentAllowed(getPluginConsentCategory(key), this.consentState);
52
+ return this.features[key] && this.remoteConfig[key] !== false && isConsentAllowed(getPluginConsentCategory(key), this.consentState);
48
53
  }
49
54
  deactivate(key) {
50
55
  const cleanup = this.activeCleanups.get(key);
@@ -70,8 +75,14 @@ var PluginRuntime = class {
70
75
  sync() {
71
76
  this.syncVersion += 1;
72
77
  for (const plugin of PLUGIN_MANIFEST) {
73
- if (plugin.name === "errors") continue;
74
78
  if (this.shouldEnablePlugin(plugin.name)) {
79
+ if (plugin.name === "errors") {
80
+ if (!this.activeCleanups.has("errors")) {
81
+ const cleanup = errorsPlugin.setup(this.context);
82
+ if (cleanup) this.activeCleanups.set("errors", cleanup);
83
+ }
84
+ continue;
85
+ }
75
86
  this.activate(plugin.name).catch(() => {
76
87
  log.warn("non-critical plugin loading failed");
77
88
  });
@@ -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\";\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"}
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"}
@@ -0,0 +1,4 @@
1
+ //#region src/internal/version.d.ts
2
+ declare const PRODUCER_VERSION: string;
3
+ //#endregion
4
+ export { PRODUCER_VERSION };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.mts","names":[],"sources":["../../src/internal/version.ts"],"mappings":";cAEa,gBAAA"}
@@ -0,0 +1,5 @@
1
+ import { name, version } from "../package.mjs";
2
+ //#region src/internal/version.ts
3
+ const PRODUCER_VERSION = `${name}@${version}`;
4
+ //#endregion
5
+ export { PRODUCER_VERSION };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.mjs","names":["pkg.name","pkg.version"],"sources":["../../src/internal/version.ts"],"sourcesContent":["import pkg from \"../../package.json\" with { type: \"json\" };\n\nexport const PRODUCER_VERSION = `${pkg.name}@${pkg.version}`;\n"],"mappings":";;AAEA,MAAa,mBAAmB,GAAGA,KAAS,GAAGC"}
@@ -0,0 +1,5 @@
1
+ //#region package.json
2
+ var name = "@interfere/react";
3
+ var version = "5.0.0";
4
+ //#endregion
5
+ export { name, version };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package.mjs","names":[],"sources":["../package.json"],"sourcesContent":[""],"mappings":""}
@@ -0,0 +1,6 @@
1
+ import { Plugin } from "./lib/types.mjs";
2
+
3
+ //#region src/plugins/device.d.ts
4
+ declare const devicePlugin: Plugin;
5
+ //#endregion
6
+ export { devicePlugin as default, devicePlugin };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device.d.mts","names":[],"sources":["../../src/plugins/device.ts"],"mappings":";;;cAGa,YAAA,EAAc,MAAA"}
@@ -0,0 +1,13 @@
1
+ import { initDevice, resetDevice } from "../tracking/device.mjs";
2
+ //#region src/plugins/device.ts
3
+ const devicePlugin = {
4
+ name: "device",
5
+ setup() {
6
+ initDevice();
7
+ return () => {
8
+ resetDevice();
9
+ };
10
+ }
11
+ };
12
+ //#endregion
13
+ export { devicePlugin as default, devicePlugin };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device.mjs","names":[],"sources":["../../src/plugins/device.ts"],"sourcesContent":["import { initDevice, resetDevice } from \"../tracking/device.js\";\nimport type { Plugin } from \"./lib/types.js\";\n\nexport const devicePlugin: Plugin = {\n name: \"device\",\n\n setup() {\n initDevice();\n\n return () => {\n resetDevice();\n };\n },\n};\n\nexport default devicePlugin;\n"],"mappings":";;AAGA,MAAa,eAAuB;CAClC,MAAM;CAEN,QAAQ;AACN,cAAY;AAEZ,eAAa;AACX,gBAAa;;;CAGlB"}
@@ -4,7 +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
+ device: () => import("../device.mjs"),
8
8
  pageEvents: () => import("../pages.mjs"),
9
9
  rageClick: () => import("../rage-clicks.mjs"),
10
10
  replay: () => import("../replay.mjs")
@@ -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 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
+ {"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 device: () => import(\"../device.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,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,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 +1 @@
1
- {"version":3,"file":"api.d.mts","names":[],"sources":["../../src/tracking/api.ts"],"mappings":";;;;iBA8DgB,SAAA,CAAU,aAAA,EAAe,YAAA;AAAA,cAU5B,OAAA;EAYZ,KAAA;EAAA,WAAA;AAAA;AAAA,cAEY,QAAA;SACJ,cAAA;cAIK,cAAA;;;iBAiCE,QAAA,CAAA"}
1
+ {"version":3,"file":"api.d.mts","names":[],"sources":["../../src/tracking/api.ts"],"mappings":";;;;iBAkEgB,SAAA,CAAU,aAAA,EAAe,YAAA;AAAA,cAU5B,OAAA;EAYZ,KAAA;EAAA,WAAA;AAAA;AAAA,cAEY,QAAA;SACJ,cAAA;cAIK,cAAA;;;iBAkCE,QAAA,CAAA"}
@@ -1,5 +1,5 @@
1
1
  import { createLogger } from "../util/log.mjs";
2
- import { getVisitorId, whenVisitorReady } from "./visitor.mjs";
2
+ import { getDeviceId, getFpHash, whenDeviceReady } from "./device.mjs";
3
3
  import { buildHeaders } from "../transport/http.mjs";
4
4
  import { SessionManager } from "./session.mjs";
5
5
  //#region src/tracking/api.ts
@@ -11,7 +11,7 @@ let identifiedSessionId = null;
11
11
  let syncedSessionId = null;
12
12
  let syncAttemptMs = 0;
13
13
  const SYNC_COOLDOWN_MS = 5e3;
14
- function syncSession(sessionId, visitorId) {
14
+ function syncSession(sessionId, deviceId, fpHash) {
15
15
  if (!target) return;
16
16
  syncAttemptMs = Date.now();
17
17
  fetch(target.url, {
@@ -19,7 +19,8 @@ function syncSession(sessionId, visitorId) {
19
19
  headers: buildHeaders(target.headers),
20
20
  body: JSON.stringify({
21
21
  sessionId,
22
- visitorId
22
+ deviceId,
23
+ fpHash
23
24
  }),
24
25
  keepalive: true,
25
26
  signal: AbortSignal.timeout(1e4)
@@ -32,14 +33,14 @@ function syncSession(sessionId, visitorId) {
32
33
  function ensureSynced(sessionId) {
33
34
  if (syncedSessionId === sessionId) return;
34
35
  if (Date.now() - syncAttemptMs < SYNC_COOLDOWN_MS) return;
35
- syncSession(sessionId, getVisitorId());
36
+ syncSession(sessionId, getDeviceId(), getFpHash());
36
37
  }
37
38
  async function onRotate(sessionId) {
38
39
  syncedSessionId = null;
39
40
  if (!target) return;
40
- const visitorId = await whenVisitorReady();
41
- log.debug("POST session %s (visitor=%s)", sessionId, visitorId ?? "pending");
42
- syncSession(sessionId, visitorId);
41
+ const deviceId = await whenDeviceReady();
42
+ log.debug("POST session %s (device=%s)", sessionId, deviceId ?? "pending");
43
+ syncSession(sessionId, deviceId, getFpHash());
43
44
  }
44
45
  function bootstrap(sessionTarget) {
45
46
  target = sessionTarget;
@@ -70,14 +71,16 @@ const identity = {
70
71
  }
71
72
  currentIdentity = params;
72
73
  identifiedSessionId = sessionId;
73
- const visitorId = getVisitorId();
74
+ const deviceId = getDeviceId();
75
+ const fpHash = getFpHash();
74
76
  log.info("PUT session %s → user %s", sessionId, params.identifier);
75
77
  fetch(target.url, {
76
78
  method: "PUT",
77
79
  headers: buildHeaders(target.headers),
78
80
  body: JSON.stringify({
79
81
  sessionId,
80
- visitorId,
82
+ deviceId,
83
+ fpHash,
81
84
  ...params
82
85
  }),
83
86
  keepalive: true,
@@ -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 { 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\nlet syncedSessionId: string | null = null;\nlet syncAttemptMs = 0;\nconst SYNC_COOLDOWN_MS = 5000;\n\nfunction syncSession(sessionId: string, visitorId: string | null): 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, visitorId }),\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, getVisitorId());\n}\n\nasync function onRotate(sessionId: string): Promise<void> {\n syncedSessionId = null;\n if (!target) {\n return;\n }\n const visitorId = await whenVisitorReady();\n log.debug(\"POST session %s (visitor=%s)\", sessionId, visitorId ?? \"pending\");\n syncSession(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 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 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 fetch(target.url, {\n method: \"PUT\",\n headers: buildHeaders(target.headers),\n body: JSON.stringify({ sessionId, visitorId, ...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":";;;;;AAOA,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,YAAY,WAAmB,WAAgC;AACtE,KAAI,CAAC,OACH;AAGF,iBAAgB,KAAK,KAAK;AAE1B,OAAM,OAAO,KAAK;EAChB,QAAQ;EACR,SAAS,aAAa,OAAO,QAAQ;EACrC,MAAM,KAAK,UAAU;GAAE;GAAW;GAAW,CAAC;EAC9C,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,cAAc,CAAC;;AAGxC,eAAe,SAAS,WAAkC;AACxD,mBAAkB;AAClB,KAAI,CAAC,OACH;CAEF,MAAM,YAAY,MAAM,kBAAkB;AAC1C,KAAI,MAAM,gCAAgC,WAAW,aAAa,UAAU;AAC5E,aAAY,WAAW,UAAU;;AAGnC,SAAgB,UAAU,eAAmC;AAC3D,UAAS;AAET,OAAM,IAAI,gBAAgB,OAAO;AAC/B,WAAS,GAAG,CAAC,YAAY,GAEvB;GACF;;AAGJ,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,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,QAAM,OAAO,KAAK;GAChB,QAAQ;GACR,SAAS,aAAa,OAAO,QAAQ;GACrC,MAAM,KAAK,UAAU;IAAE;IAAW;IAAW,GAAG;IAAQ,CAAC;GACzD,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 { getDeviceId, getFpHash, whenDeviceReady } 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 if (!target) {\n return;\n }\n const deviceId = await whenDeviceReady();\n log.debug(\"POST session %s (device=%s)\", sessionId, deviceId ?? \"pending\");\n syncSession(sessionId, deviceId, getFpHash());\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 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 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 deviceId = getDeviceId();\n const fpHash = getFpHash();\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":";;;;;AAOA,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,KAAI,CAAC,OACH;CAEF,MAAM,WAAW,MAAM,iBAAiB;AACxC,KAAI,MAAM,+BAA+B,WAAW,YAAY,UAAU;AAC1E,aAAY,WAAW,UAAU,WAAW,CAAC;;AAG/C,SAAgB,UAAU,eAAmC;AAC3D,UAAS;AAET,OAAM,IAAI,gBAAgB,OAAO;AAC/B,WAAS,GAAG,CAAC,YAAY,GAEvB;GACF;;AAGJ,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,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,WAAW,aAAa;EAC9B,MAAM,SAAS,WAAW;AAC1B,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"}
@@ -0,0 +1,8 @@
1
+ //#region src/tracking/device.d.ts
2
+ declare function initDevice(): void;
3
+ declare function getDeviceId(): string | null;
4
+ declare function getFpHash(): string | null;
5
+ declare function whenDeviceReady(): Promise<string | null>;
6
+ declare function resetDevice(): void;
7
+ //#endregion
8
+ export { getDeviceId, getFpHash, initDevice, resetDevice, whenDeviceReady };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device.d.mts","names":[],"sources":["../../src/tracking/device.ts"],"mappings":";iBA+CgB,UAAA,CAAA;AAAA,iBAiDA,WAAA,CAAA;AAAA,iBAIA,SAAA,CAAA;AAAA,iBAIA,eAAA,CAAA,GAAmB,OAAA;AAAA,iBAOnB,WAAA,CAAA"}
@@ -0,0 +1,76 @@
1
+ import { createLogger } from "../util/log.mjs";
2
+ //#region src/tracking/device.ts
3
+ const log = createLogger("device");
4
+ const LS_KEY = "interfere:device_id";
5
+ const COOKIE_NAME = "interfere_did";
6
+ const COOKIE_MAX_AGE_DAYS = 400;
7
+ let deviceId = null;
8
+ let fpHash = null;
9
+ let fpPending = null;
10
+ function tryLocalStorage() {
11
+ try {
12
+ const s = globalThis.localStorage;
13
+ const key = "__interfere_device_probe__";
14
+ s.setItem(key, "1");
15
+ s.removeItem(key);
16
+ return s;
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+ function getCookie(name) {
22
+ if (typeof document === "undefined") return null;
23
+ const match = document.cookie.split("; ").find((c) => c.startsWith(`${name}=`));
24
+ return match ? decodeURIComponent(match.split("=")[1] ?? "") : null;
25
+ }
26
+ function setCookie(name, value) {
27
+ if (typeof document === "undefined") return;
28
+ const maxAge = COOKIE_MAX_AGE_DAYS * 24 * 60 * 60;
29
+ document.cookie = `${name}=${encodeURIComponent(value)};path=/;max-age=${maxAge};SameSite=Lax`;
30
+ }
31
+ function generateId() {
32
+ return crypto.randomUUID();
33
+ }
34
+ function initDevice() {
35
+ if (deviceId) return;
36
+ const ls = tryLocalStorage();
37
+ const fromLs = ls?.getItem(LS_KEY) ?? null;
38
+ const fromCookie = getCookie(COOKIE_NAME);
39
+ deviceId = fromLs ?? fromCookie ?? generateId();
40
+ if (!fromLs && ls) ls.setItem(LS_KEY, deviceId);
41
+ if (!fromCookie) setCookie(COOKIE_NAME, deviceId);
42
+ if (fromLs && !fromCookie) setCookie(COOKIE_NAME, deviceId);
43
+ if (fromCookie && !fromLs && ls) ls.setItem(LS_KEY, deviceId);
44
+ log.debug("device %s (ls=%s cookie=%s)", deviceId, !!fromLs, !!fromCookie);
45
+ initFpHash();
46
+ }
47
+ function initFpHash() {
48
+ if (fpHash || fpPending) return;
49
+ fpPending = (async () => {
50
+ try {
51
+ fpHash = (await (await (await import("@fingerprintjs/fingerprintjs")).load()).get()).visitorId;
52
+ log.debug("fpHash %s", fpHash);
53
+ return fpHash;
54
+ } catch {
55
+ log.warn("fp hash failed");
56
+ return null;
57
+ }
58
+ })();
59
+ }
60
+ function getDeviceId() {
61
+ return deviceId;
62
+ }
63
+ function getFpHash() {
64
+ return fpHash;
65
+ }
66
+ function whenDeviceReady() {
67
+ if (deviceId) return Promise.resolve(deviceId);
68
+ return Promise.resolve(null);
69
+ }
70
+ function resetDevice() {
71
+ deviceId = null;
72
+ fpHash = null;
73
+ fpPending = null;
74
+ }
75
+ //#endregion
76
+ export { getDeviceId, getFpHash, initDevice, resetDevice, whenDeviceReady };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device.mjs","names":[],"sources":["../../src/tracking/device.ts"],"sourcesContent":["import { createLogger } from \"../util/log.js\";\n\nconst log = createLogger(\"device\");\n\nconst LS_KEY = \"interfere:device_id\";\nconst COOKIE_NAME = \"interfere_did\";\nconst COOKIE_MAX_AGE_DAYS = 400;\n\nlet deviceId: string | null = null;\nlet fpHash: string | null = null;\nlet fpPending: Promise<string | null> | null = null;\n\nfunction tryLocalStorage(): Storage | null {\n try {\n const s = globalThis.localStorage;\n const key = \"__interfere_device_probe__\";\n s.setItem(key, \"1\");\n s.removeItem(key);\n return s;\n } catch {\n return null;\n }\n}\n\nfunction getCookie(name: string): string | null {\n if (typeof document === \"undefined\") {\n return null;\n }\n const match = document.cookie\n .split(\"; \")\n .find((c) => c.startsWith(`${name}=`));\n return match ? decodeURIComponent(match.split(\"=\")[1] ?? \"\") : null;\n}\n\nfunction setCookie(name: string, value: string): void {\n if (typeof document === \"undefined\") {\n return;\n }\n const maxAge = COOKIE_MAX_AGE_DAYS * 24 * 60 * 60;\n // biome-ignore lint/suspicious/noDocumentCookie: Cookie Store API is async and not universally supported; synchronous access is required here\n document.cookie = `${name}=${encodeURIComponent(value)};path=/;max-age=${maxAge};SameSite=Lax`;\n}\n\nfunction generateId(): string {\n return crypto.randomUUID();\n}\n\nexport function initDevice(): void {\n if (deviceId) {\n return;\n }\n\n const ls = tryLocalStorage();\n const fromLs = ls?.getItem(LS_KEY) ?? null;\n const fromCookie = getCookie(COOKIE_NAME);\n\n deviceId = fromLs ?? fromCookie ?? generateId();\n\n if (!fromLs && ls) {\n ls.setItem(LS_KEY, deviceId);\n }\n if (!fromCookie) {\n setCookie(COOKIE_NAME, deviceId);\n }\n if (fromLs && !fromCookie) {\n setCookie(COOKIE_NAME, deviceId);\n }\n if (fromCookie && !fromLs && ls) {\n ls.setItem(LS_KEY, deviceId);\n }\n\n log.debug(\"device %s (ls=%s cookie=%s)\", deviceId, !!fromLs, !!fromCookie);\n\n initFpHash();\n}\n\nfunction initFpHash(): void {\n if (fpHash || fpPending) {\n return;\n }\n\n fpPending = (async () => {\n try {\n const FingerprintJS = await import(\"@fingerprintjs/fingerprintjs\");\n const fp = await FingerprintJS.load();\n const result = await fp.get();\n fpHash = result.visitorId;\n log.debug(\"fpHash %s\", fpHash);\n return fpHash;\n } catch {\n log.warn(\"fp hash failed\");\n return null;\n }\n })();\n}\n\nexport function getDeviceId(): string | null {\n return deviceId;\n}\n\nexport function getFpHash(): string | null {\n return fpHash;\n}\n\nexport function whenDeviceReady(): Promise<string | null> {\n if (deviceId) {\n return Promise.resolve(deviceId);\n }\n return Promise.resolve(null);\n}\n\nexport function resetDevice(): void {\n deviceId = null;\n fpHash = null;\n fpPending = null;\n}\n"],"mappings":";;AAEA,MAAM,MAAM,aAAa,SAAS;AAElC,MAAM,SAAS;AACf,MAAM,cAAc;AACpB,MAAM,sBAAsB;AAE5B,IAAI,WAA0B;AAC9B,IAAI,SAAwB;AAC5B,IAAI,YAA2C;AAE/C,SAAS,kBAAkC;AACzC,KAAI;EACF,MAAM,IAAI,WAAW;EACrB,MAAM,MAAM;AACZ,IAAE,QAAQ,KAAK,IAAI;AACnB,IAAE,WAAW,IAAI;AACjB,SAAO;SACD;AACN,SAAO;;;AAIX,SAAS,UAAU,MAA6B;AAC9C,KAAI,OAAO,aAAa,YACtB,QAAO;CAET,MAAM,QAAQ,SAAS,OACpB,MAAM,KAAK,CACX,MAAM,MAAM,EAAE,WAAW,GAAG,KAAK,GAAG,CAAC;AACxC,QAAO,QAAQ,mBAAmB,MAAM,MAAM,IAAI,CAAC,MAAM,GAAG,GAAG;;AAGjE,SAAS,UAAU,MAAc,OAAqB;AACpD,KAAI,OAAO,aAAa,YACtB;CAEF,MAAM,SAAS,sBAAsB,KAAK,KAAK;AAE/C,UAAS,SAAS,GAAG,KAAK,GAAG,mBAAmB,MAAM,CAAC,kBAAkB,OAAO;;AAGlF,SAAS,aAAqB;AAC5B,QAAO,OAAO,YAAY;;AAG5B,SAAgB,aAAmB;AACjC,KAAI,SACF;CAGF,MAAM,KAAK,iBAAiB;CAC5B,MAAM,SAAS,IAAI,QAAQ,OAAO,IAAI;CACtC,MAAM,aAAa,UAAU,YAAY;AAEzC,YAAW,UAAU,cAAc,YAAY;AAE/C,KAAI,CAAC,UAAU,GACb,IAAG,QAAQ,QAAQ,SAAS;AAE9B,KAAI,CAAC,WACH,WAAU,aAAa,SAAS;AAElC,KAAI,UAAU,CAAC,WACb,WAAU,aAAa,SAAS;AAElC,KAAI,cAAc,CAAC,UAAU,GAC3B,IAAG,QAAQ,QAAQ,SAAS;AAG9B,KAAI,MAAM,+BAA+B,UAAU,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;AAE1E,aAAY;;AAGd,SAAS,aAAmB;AAC1B,KAAI,UAAU,UACZ;AAGF,cAAa,YAAY;AACvB,MAAI;AAIF,aADe,OADJ,OADW,MAAM,OAAO,iCACJ,MAAM,EACb,KAAK,EACb;AAChB,OAAI,MAAM,aAAa,OAAO;AAC9B,UAAO;UACD;AACN,OAAI,KAAK,iBAAiB;AAC1B,UAAO;;KAEP;;AAGN,SAAgB,cAA6B;AAC3C,QAAO;;AAGT,SAAgB,YAA2B;AACzC,QAAO;;AAGT,SAAgB,kBAA0C;AACxD,KAAI,SACF,QAAO,QAAQ,QAAQ,SAAS;AAElC,QAAO,QAAQ,QAAQ,KAAK;;AAG9B,SAAgB,cAAoB;AAClC,YAAW;AACX,UAAS;AACT,aAAY"}
@@ -5,13 +5,17 @@ interface IngestTarget {
5
5
  headers: Headers;
6
6
  url: string;
7
7
  }
8
- declare function buildHeaders(base: Headers): Record<string, string>;
8
+ interface DeliveryMeta {
9
+ queueDepth: number;
10
+ retryCount: number;
11
+ }
12
+ declare function buildHeaders(base: Headers, meta?: DeliveryMeta): Record<string, string>;
9
13
  declare function hasServiceWorker(): boolean;
10
14
  declare class HttpTransport {
11
15
  private readonly target;
12
16
  private pendingKeepalive;
13
17
  constructor(target: IngestTarget);
14
- send(envelopes: Envelope[]): Promise<void>;
18
+ send(envelopes: Envelope[], meta?: DeliveryMeta): Promise<void>;
15
19
  }
16
20
  //#endregion
17
- export { HttpTransport, IngestTarget, buildHeaders, hasServiceWorker };
21
+ export { DeliveryMeta, HttpTransport, IngestTarget, buildHeaders, hasServiceWorker };
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.mts","names":[],"sources":["../../src/transport/http.ts"],"mappings":";;;UAYiB,YAAA;EACf,OAAA,EAAS,OAAA;EACT,GAAA;AAAA;AAAA,iBAGc,YAAA,CAAa,IAAA,EAAM,OAAA,GAAU,MAAA;AAAA,iBAiB7B,gBAAA,CAAA;AAAA,cAiBH,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":";;;UAWiB,YAAA;EACf,OAAA,EAAS,OAAA;EACT,GAAA;AAAA;AAAA,UAGe,YAAA;EACf,UAAA;EACA,UAAA;AAAA;AAAA,iBAGc,YAAA,CACd,IAAA,EAAM,OAAA,EACN,IAAA,GAAO,YAAA,GACN,MAAA;AAAA,iBA4Ba,gBAAA,CAAA;AAAA,cAiBH,aAAA;EAAA,iBACM,MAAA;EAAA,QACT,gBAAA;cAEI,MAAA,EAAQ,YAAA;EAId,IAAA,CAAK,SAAA,EAAW,QAAA,IAAY,IAAA,GAAO,YAAA,GAAe,OAAA;AAAA"}
@@ -1,17 +1,23 @@
1
+ import { PRODUCER_VERSION } from "../internal/version.mjs";
1
2
  import { createLogger } from "../util/log.mjs";
2
- import { getVisitorId } from "../tracking/visitor.mjs";
3
+ import { getDeviceId } from "../tracking/device.mjs";
3
4
  import { session } from "../tracking/api.mjs";
4
- import { make } from "tctx/traceparent";
5
5
  //#region src/transport/http.ts
6
6
  const log = createLogger("http");
7
7
  const SEND_TIMEOUT_MS = 1e4;
8
- function buildHeaders(base) {
8
+ function buildHeaders(base, meta) {
9
9
  const h = Object.fromEntries(base.entries());
10
- h.traceparent = String(make());
10
+ h["x-interfere-sdk-version"] = PRODUCER_VERSION;
11
11
  const sessionId = session.getId();
12
12
  if (sessionId) h["x-interfere-session"] = sessionId;
13
- const visitorId = getVisitorId();
14
- if (visitorId) h["x-interfere-visitor"] = visitorId;
13
+ const windowId = session.getWindowId();
14
+ if (windowId) h["x-interfere-window"] = windowId;
15
+ const deviceId = getDeviceId();
16
+ if (deviceId) h["x-interfere-device"] = deviceId;
17
+ if (meta) {
18
+ h["x-interfere-retry-count"] = String(meta.retryCount);
19
+ h["x-interfere-queue-depth"] = String(meta.queueDepth);
20
+ }
15
21
  return h;
16
22
  }
17
23
  function hasServiceWorker() {
@@ -28,9 +34,9 @@ var HttpTransport = class {
28
34
  constructor(target) {
29
35
  this.target = target;
30
36
  }
31
- async send(envelopes) {
37
+ async send(envelopes, meta) {
32
38
  const body = JSON.stringify(envelopes);
33
- const headers = buildHeaders(this.target.headers);
39
+ const headers = buildHeaders(this.target.headers, meta);
34
40
  if (hasServiceWorker()) {
35
41
  log.debug("POST %d envelopes via SW", envelopes.length);
36
42
  assertOk(await fetch(this.target.url, {
@@ -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\nconst SEND_TIMEOUT_MS = 10_000;\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\nexport function hasServiceWorker(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n \"serviceWorker\" in navigator &&\n navigator.serviceWorker.controller !== null\n );\n}\n\nfunction assertOk(response: Response): void {\n if (!response.ok) {\n throw new Error(`ingest responded ${response.status}`);\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 const res = await fetch(this.target.url, {\n method: \"POST\",\n headers,\n body,\n signal: AbortSignal.timeout(SEND_TIMEOUT_MS),\n });\n assertOk(res);\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 const res = await fetch(this.target.url, {\n method: \"POST\",\n headers,\n body,\n keepalive: useKeepalive,\n signal: AbortSignal.timeout(SEND_TIMEOUT_MS),\n });\n assertOk(res);\n } finally {\n if (useKeepalive) {\n this.pendingKeepalive--;\n }\n }\n }\n}\n"],"mappings":";;;;;AAQA,MAAM,MAAM,aAAa,OAAO;AAEhC,MAAM,kBAAkB;AAOxB,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,SAAgB,mBAA4B;AAC1C,QACE,OAAO,cAAc,eACrB,mBAAmB,aACnB,UAAU,cAAc,eAAe;;AAI3C,SAAS,SAAS,UAA0B;AAC1C,KAAI,CAAC,SAAS,GACZ,OAAM,IAAI,MAAM,oBAAoB,SAAS,SAAS;;AAI1D,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;AAOvD,YANY,MAAM,MAAM,KAAK,OAAO,KAAK;IACvC,QAAQ;IACR;IACA;IACA,QAAQ,YAAY,QAAQ,gBAAgB;IAC7C,CAAC,CACW;AACb;;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;AAQF,YAPY,MAAM,MAAM,KAAK,OAAO,KAAK;IACvC,QAAQ;IACR;IACA;IACA,WAAW;IACX,QAAQ,YAAY,QAAQ,gBAAgB;IAC7C,CAAC,CACW;YACL;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 { PRODUCER_VERSION } from \"../internal/version.js\";\nimport { session } from \"../tracking/api.js\";\nimport { getDeviceId } from \"../tracking/device.js\";\nimport { createLogger } from \"../util/log.js\";\n\nconst log = createLogger(\"http\");\n\nconst SEND_TIMEOUT_MS = 10_000;\n\nexport interface IngestTarget {\n headers: Headers;\n url: string;\n}\n\nexport interface DeliveryMeta {\n queueDepth: number;\n retryCount: number;\n}\n\nexport function buildHeaders(\n base: Headers,\n meta?: DeliveryMeta\n): Record<string, string> {\n const h: Record<string, string> = Object.fromEntries(base.entries());\n\n h[\"x-interfere-sdk-version\"] = PRODUCER_VERSION;\n\n const sessionId = session.getId();\n if (sessionId) {\n h[\"x-interfere-session\"] = sessionId;\n }\n\n const windowId = session.getWindowId();\n if (windowId) {\n h[\"x-interfere-window\"] = windowId;\n }\n\n const deviceId = getDeviceId();\n if (deviceId) {\n h[\"x-interfere-device\"] = deviceId;\n }\n\n if (meta) {\n h[\"x-interfere-retry-count\"] = String(meta.retryCount);\n h[\"x-interfere-queue-depth\"] = String(meta.queueDepth);\n }\n\n return h;\n}\n\nexport function hasServiceWorker(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n \"serviceWorker\" in navigator &&\n navigator.serviceWorker.controller !== null\n );\n}\n\nfunction assertOk(response: Response): void {\n if (!response.ok) {\n throw new Error(`ingest responded ${response.status}`);\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[], meta?: DeliveryMeta): Promise<void> {\n const body = JSON.stringify(envelopes);\n const headers = buildHeaders(this.target.headers, meta);\n\n if (hasServiceWorker()) {\n log.debug(\"POST %d envelopes via SW\", envelopes.length);\n const res = await fetch(this.target.url, {\n method: \"POST\",\n headers,\n body,\n signal: AbortSignal.timeout(SEND_TIMEOUT_MS),\n });\n assertOk(res);\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 const res = await fetch(this.target.url, {\n method: \"POST\",\n headers,\n body,\n keepalive: useKeepalive,\n signal: AbortSignal.timeout(SEND_TIMEOUT_MS),\n });\n assertOk(res);\n } finally {\n if (useKeepalive) {\n this.pendingKeepalive--;\n }\n }\n }\n}\n"],"mappings":";;;;;AAOA,MAAM,MAAM,aAAa,OAAO;AAEhC,MAAM,kBAAkB;AAYxB,SAAgB,aACd,MACA,MACwB;CACxB,MAAM,IAA4B,OAAO,YAAY,KAAK,SAAS,CAAC;AAEpE,GAAE,6BAA6B;CAE/B,MAAM,YAAY,QAAQ,OAAO;AACjC,KAAI,UACF,GAAE,yBAAyB;CAG7B,MAAM,WAAW,QAAQ,aAAa;AACtC,KAAI,SACF,GAAE,wBAAwB;CAG5B,MAAM,WAAW,aAAa;AAC9B,KAAI,SACF,GAAE,wBAAwB;AAG5B,KAAI,MAAM;AACR,IAAE,6BAA6B,OAAO,KAAK,WAAW;AACtD,IAAE,6BAA6B,OAAO,KAAK,WAAW;;AAGxD,QAAO;;AAGT,SAAgB,mBAA4B;AAC1C,QACE,OAAO,cAAc,eACrB,mBAAmB,aACnB,UAAU,cAAc,eAAe;;AAI3C,SAAS,SAAS,UAA0B;AAC1C,KAAI,CAAC,SAAS,GACZ,OAAM,IAAI,MAAM,oBAAoB,SAAS,SAAS;;AAI1D,MAAM,yBAAyB;AAC/B,MAAM,2BAA2B;AAEjC,IAAa,gBAAb,MAA2B;CACzB;CACA,mBAA2B;CAE3B,YAAY,QAAsB;AAChC,OAAK,SAAS;;CAGhB,MAAM,KAAK,WAAuB,MAAoC;EACpE,MAAM,OAAO,KAAK,UAAU,UAAU;EACtC,MAAM,UAAU,aAAa,KAAK,OAAO,SAAS,KAAK;AAEvD,MAAI,kBAAkB,EAAE;AACtB,OAAI,MAAM,4BAA4B,UAAU,OAAO;AAOvD,YANY,MAAM,MAAM,KAAK,OAAO,KAAK;IACvC,QAAQ;IACR;IACA;IACA,QAAQ,YAAY,QAAQ,gBAAgB;IAC7C,CAAC,CACW;AACb;;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;AAQF,YAPY,MAAM,MAAM,KAAK,OAAO,KAAK;IACvC,QAAQ;IACR;IACA;IACA,WAAW;IACX,QAAQ,YAAY,QAAQ,gBAAgB;IAC7C,CAAC,CACW;YACL;AACR,OAAI,aACF,MAAK"}
@@ -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;EAuCA,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;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"}
@@ -47,7 +47,11 @@ var BatchQueue = class {
47
47
  if (this.failures >= BREAKER_THRESHOLD && Date.now() < this.pausedUntil) return;
48
48
  this.flushing = true;
49
49
  const batch = this.buffer.splice(0, this.batchSize);
50
- this.transport.send(batch).then(() => {
50
+ const meta = {
51
+ retryCount: this.failures,
52
+ queueDepth: this.buffer.length
53
+ };
54
+ this.transport.send(batch, meta).then(() => {
51
55
  if (this.failures > 0) log.info("send recovered after %d failures", this.failures);
52
56
  this.failures = 0;
53
57
  }).catch(() => {
@@ -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\n this.transport\n .send(batch)\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;AAEnD,OAAK,UACF,KAAK,MAAM,CACX,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 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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interfere/react",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "license": "MIT",
5
5
  "description": "Client-side React SDK for Interfere. Error tracking, session replay, and analytics.",
6
6
  "keywords": [
@@ -54,12 +54,11 @@
54
54
  "test": "bun run test:unit && bun run test:browser"
55
55
  },
56
56
  "dependencies": {
57
- "@fingerprintjs/fingerprintjs-pro": "^3.12.8",
58
- "@interfere/constants": "^4.0.0",
59
- "@interfere/types": "^4.0.0",
57
+ "@fingerprintjs/fingerprintjs": "^4.5.1",
58
+ "@interfere/constants": "^5.0.0",
59
+ "@interfere/types": "^5.0.0",
60
60
  "@ua-parser-js/pro-enterprise": "^2.0.6",
61
61
  "rrweb": "2.0.0-alpha.4",
62
- "tctx": "^0.2.5",
63
62
  "uuid": "^13.0.0"
64
63
  },
65
64
  "peerDependencies": {
@@ -67,8 +66,8 @@
67
66
  "react-dom": ">=19"
68
67
  },
69
68
  "devDependencies": {
70
- "@interfere/typescript-config": "^4.0.0",
71
- "@interfere/vitest-config": "^4.0.0",
69
+ "@interfere/typescript-config": "^5.0.0",
70
+ "@interfere/vitest-config": "^5.0.0",
72
71
  "@rrweb/types": "2.0.0-alpha.20",
73
72
  "@testing-library/react": "^16.3.2",
74
73
  "@types/node": "^24.12.0",
@@ -1,6 +0,0 @@
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 };
@@ -1 +0,0 @@
1
- {"version":3,"file":"fingerprint.d.mts","names":[],"sources":["../../src/plugins/fingerprint.ts"],"mappings":";;;cAGa,iBAAA,EAAmB,MAAA"}
@@ -1,13 +0,0 @@
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 };
@@ -1 +0,0 @@
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"}
@@ -1,7 +0,0 @@
1
- //#region src/tracking/visitor.d.ts
2
- declare function initVisitor(): void;
3
- declare function getVisitorId(): string | null;
4
- declare function whenVisitorReady(): Promise<string | null>;
5
- declare function resetVisitor(): void;
6
- //#endregion
7
- export { getVisitorId, initVisitor, resetVisitor, whenVisitorReady };
@@ -1 +0,0 @@
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"}
@@ -1,40 +0,0 @@
1
- import { createLogger } from "../util/log.mjs";
2
- //#region src/tracking/visitor.ts
3
- const log = createLogger("visitor");
4
- let visitorId = null;
5
- let pending = null;
6
- function resolveApiKey() {
7
- if (typeof process === "undefined") return;
8
- return process.env.INTERFERE_FINGERPRINT_KEY ?? void 0;
9
- }
10
- function initVisitor() {
11
- if (visitorId || pending) return;
12
- const apiKey = resolveApiKey();
13
- if (!apiKey) {
14
- log.info("no INTERFERE_FINGERPRINT_KEY, skipping");
15
- return;
16
- }
17
- pending = (async () => {
18
- try {
19
- visitorId = (await (await (await import("@fingerprintjs/fingerprintjs-pro")).load({ apiKey })).get()).visitorId;
20
- log.debug("resolved %s", visitorId);
21
- return visitorId;
22
- } catch {
23
- log.error("fingerprint failed");
24
- return null;
25
- }
26
- })();
27
- }
28
- function getVisitorId() {
29
- return visitorId;
30
- }
31
- function whenVisitorReady() {
32
- if (visitorId) return Promise.resolve(visitorId);
33
- return pending ?? Promise.resolve(null);
34
- }
35
- function resetVisitor() {
36
- visitorId = null;
37
- pending = null;
38
- }
39
- //#endregion
40
- export { getVisitorId, initVisitor, resetVisitor, whenVisitorReady };
@@ -1 +0,0 @@
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"}