@interfere/react 1.0.3 → 4.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.
@@ -1,9 +1,11 @@
1
1
  import { PluginOverrides } from "../plugins/lib/loader.mjs";
2
+ import { QueueOptions } from "../transport/queue.mjs";
2
3
  import { ConsentState } from "@interfere/types/sdk/plugins/manifest";
3
4
  import { EnvelopePayload, EventType } from "@interfere/types/sdk/envelope";
4
5
 
5
6
  //#region src/internal/client.d.ts
6
7
  interface ClientOptions {
8
+ batch?: Omit<Partial<QueueOptions>, "transport">;
7
9
  consent?: ConsentState;
8
10
  plugins?: PluginOverrides;
9
11
  }
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.mts","names":[],"sources":["../../src/internal/client.ts"],"mappings":";;;;;UAiBiB,aAAA;EACf,OAAA,GAAU,YAAA;EACV,OAAA,GAAU,eAAA;AAAA;AAAA,cAGN,MAAA;EAAA,iBACa,QAAA;EAAA,iBACA,KAAA;EAAA,iBACA,OAAA;cAEL,IAAA,EAAM,aAAA,EAAe,OAAA,UAAiB,SAAA;EAkClD,OAAA,WAAkB,SAAA,CAAA,CAAW,IAAA,EAAM,CAAA,EAAG,OAAA,EAAS,eAAA,CAAgB,CAAA;EAS/D,KAAA,CAAA;EAIA,OAAA,CAAA;EAMA,UAAA,CAAA,GAAc,YAAA;EAId,UAAA,CAAW,KAAA,GAAQ,YAAA;EAInB,YAAA,CAAA;AAAA;AAAA,iBAOc,SAAA,CAAA,GAAa,MAAA;AAAA,iBASb,IAAA,CAAK,IAAA,GAAM,aAAA;AAAA,iBAsBX,KAAA,CAAA;AAAA,cASH,OAAA;SACJ,YAAA;cAIK,YAAA;AAAA;AAAA,iBAKE,WAAA,CAAY,YAAA,EAAc,YAAA;AAAA,iBAa1B,KAAA,CAAA"}
1
+ {"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"}
@@ -26,7 +26,10 @@ var Client = class {
26
26
  releaseId
27
27
  };
28
28
  registerServiceWorker();
29
- this.queue = new BatchQueue({ transport: new HttpTransport(targets.ingest) });
29
+ this.queue = new BatchQueue({
30
+ transport: new HttpTransport(targets.ingest),
31
+ ...opts.batch
32
+ });
30
33
  this.queue.start();
31
34
  this.runtime = new PluginRuntime({
32
35
  capture: (type, payload) => this.capture(type, payload),
@@ -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 } from \"../transport/queue.js\";\nimport { createLogger } from \"../util/log.js\";\nimport { resolveTargets } from \"./config.js\";\nimport { collectContext } from \"./context.js\";\nimport { buildEnvelope, type EnvelopeMetadata } from \"./envelope.js\";\nimport { PluginRuntime } from \"./plugin-runtime.js\";\nimport { registerServiceWorker } from \"./sw.js\";\n\nconst log = createLogger(\"client\");\n\nexport interface ClientOptions {\n consent?: ConsentState;\n plugins?: PluginOverrides;\n}\n\nclass Client {\n private readonly metadata: EnvelopeMetadata;\n private readonly queue: BatchQueue;\n private readonly runtime: PluginRuntime;\n\n constructor(opts: ClientOptions, buildId: string, releaseId: string | null) {\n const targets = resolveTargets();\n bootstrap(targets.session);\n\n log.info(\"target: %s\", targets.ingest.url);\n\n this.metadata = {\n context: collectContext(),\n environment: normalizeEnv(\n typeof process === \"undefined\" ? 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 });\n this.queue.start();\n\n this.runtime = new PluginRuntime(\n {\n capture: (type, payload) => this.capture(type, payload),\n getSessionId: () => session.getId() ?? \"\",\n },\n opts.plugins,\n opts.consent\n );\n\n this.runtime.start();\n }\n\n capture<T extends EventType>(type: T, payload: EnvelopePayload<T>): void {\n const sessionId = session.getId();\n if (!(sessionId && this.runtime.canCapture(type))) {\n return;\n }\n\n this.queue.enqueue(buildEnvelope(type, payload, sessionId, this.metadata));\n }\n\n flush(): void {\n this.queue.flush();\n }\n\n dispose(): void {\n this.runtime.dispose();\n teardown();\n this.queue.dispose();\n }\n\n getConsent(): ConsentState | null {\n return this.runtime.getConsent();\n }\n\n setConsent(value?: ConsentState): void {\n this.runtime.setConsent(value);\n }\n\n resetConsent(): void {\n this.runtime.resetConsent();\n }\n}\n\nlet instance: Client | null = null;\n\nexport function getClient(): Client {\n if (!instance) {\n throw new Error(\n \"Interfere SDK not initialized. Call init() from your instrumentation-client entrypoint.\"\n );\n }\n return instance;\n}\n\nexport function init(opts: ClientOptions = {}): void {\n if (instance) {\n return;\n }\n\n const buildId = (globalThis as Record<string, unknown>)\n .__INTERFERE_BUILD_ID__ as string | undefined;\n\n const releaseId = (globalThis as Record<string, unknown>)\n .__INTERFERE_RELEASE_ID__ as string | null | undefined;\n\n if (!buildId) {\n log.error(\n \"buildId not found — ensure withInterfere() is configured in \" +\n \"next.config and instrumentation-client.ts exists in your project root.\"\n );\n return;\n }\n\n instance = new Client(opts, buildId, releaseId ?? null);\n}\n\nexport function close(): void {\n if (!instance) {\n return;\n }\n\n instance.dispose();\n instance = null;\n}\n\nexport const consent = {\n get(): ConsentState | null {\n return instance?.getConsent() ?? null;\n },\n\n set(value?: ConsentState): void {\n instance?.setConsent(value);\n },\n};\n\nexport function syncConsent(consentState: ConsentState | undefined): void {\n if (!instance) {\n return;\n }\n\n if (consentState) {\n instance.setConsent(consentState);\n return;\n }\n\n instance.resetConsent();\n}\n\nexport function flush(): void {\n instance?.flush();\n}\n"],"mappings":";;;;;;;;;;;AAeA,MAAM,MAAM,aAAa,SAAS;AAOlC,IAAM,SAAN,MAAa;CACX;CACA;CACA;CAEA,YAAY,MAAqB,SAAiB,WAA0B;EAC1E,MAAM,UAAU,gBAAgB;AAChC,YAAU,QAAQ,QAAQ;AAE1B,MAAI,KAAK,cAAc,QAAQ,OAAO,IAAI;AAE1C,OAAK,WAAW;GACd,SAAS,gBAAgB;GACzB,aAAa,aACX,OAAO,YAAY,cAAc,KAAA,IAAY,QAAQ,IAAI,SAC1D;GACD,SAAS,cAAc;GACvB;GACA;GACD;AAED,yBAAuB;AAGvB,OAAK,QAAQ,IAAI,WAAW,EAAE,WADZ,IAAI,cAAc,QAAQ,OAAO,EACV,CAAC;AAC1C,OAAK,MAAM,OAAO;AAElB,OAAK,UAAU,IAAI,cACjB;GACE,UAAU,MAAM,YAAY,KAAK,QAAQ,MAAM,QAAQ;GACvD,oBAAoB,QAAQ,OAAO,IAAI;GACxC,EACD,KAAK,SACL,KAAK,QACN;AAED,OAAK,QAAQ,OAAO;;CAGtB,QAA6B,MAAS,SAAmC;EACvE,MAAM,YAAY,QAAQ,OAAO;AACjC,MAAI,EAAE,aAAa,KAAK,QAAQ,WAAW,KAAK,EAC9C;AAGF,OAAK,MAAM,QAAQ,cAAc,MAAM,SAAS,WAAW,KAAK,SAAS,CAAC;;CAG5E,QAAc;AACZ,OAAK,MAAM,OAAO;;CAGpB,UAAgB;AACd,OAAK,QAAQ,SAAS;AACtB,YAAU;AACV,OAAK,MAAM,SAAS;;CAGtB,aAAkC;AAChC,SAAO,KAAK,QAAQ,YAAY;;CAGlC,WAAW,OAA4B;AACrC,OAAK,QAAQ,WAAW,MAAM;;CAGhC,eAAqB;AACnB,OAAK,QAAQ,cAAc;;;AAI/B,IAAI,WAA0B;AAE9B,SAAgB,YAAoB;AAClC,KAAI,CAAC,SACH,OAAM,IAAI,MACR,0FACD;AAEH,QAAO;;AAGT,SAAgB,KAAK,OAAsB,EAAE,EAAQ;AACnD,KAAI,SACF;CAGF,MAAM,UAAW,WACd;CAEH,MAAM,YAAa,WAChB;AAEH,KAAI,CAAC,SAAS;AACZ,MAAI,MACF,qIAED;AACD;;AAGF,YAAW,IAAI,OAAO,MAAM,SAAS,aAAa,KAAK;;AAGzD,SAAgB,QAAc;AAC5B,KAAI,CAAC,SACH;AAGF,UAAS,SAAS;AAClB,YAAW;;AAGb,MAAa,UAAU;CACrB,MAA2B;AACzB,SAAO,UAAU,YAAY,IAAI;;CAGnC,IAAI,OAA4B;AAC9B,YAAU,WAAW,MAAM;;CAE9B;AAED,SAAgB,YAAY,cAA8C;AACxE,KAAI,CAAC,SACH;AAGF,KAAI,cAAc;AAChB,WAAS,WAAW,aAAa;AACjC;;AAGF,UAAS,cAAc;;AAGzB,SAAgB,QAAc;AAC5B,WAAU,OAAO"}
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 +1 @@
1
- {"version":3,"file":"api.d.mts","names":[],"sources":["../../src/tracking/api.ts"],"mappings":";;;;iBAuCgB,SAAA,CAAU,aAAA,EAAe,YAAA;AAAA,cAU5B,OAAA;EAQZ,KAAA;EAAA,WAAA;AAAA;AAAA,cAEY,QAAA;SACJ,cAAA;cAIK,cAAA;;;iBA4BE,QAAA,CAAA"}
1
+ {"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"}
@@ -8,24 +8,38 @@ let mgr = null;
8
8
  let target = null;
9
9
  let currentIdentity = null;
10
10
  let identifiedSessionId = null;
11
- function fire(url, method, headers, body) {
12
- fetch(url, {
13
- method,
14
- headers: buildHeaders(headers),
15
- body: JSON.stringify(body),
16
- keepalive: true
11
+ let syncedSessionId = null;
12
+ let syncAttemptMs = 0;
13
+ const SYNC_COOLDOWN_MS = 5e3;
14
+ function syncSession(sessionId, visitorId) {
15
+ if (!target) return;
16
+ syncAttemptMs = Date.now();
17
+ fetch(target.url, {
18
+ method: "POST",
19
+ headers: buildHeaders(target.headers),
20
+ body: JSON.stringify({
21
+ sessionId,
22
+ visitorId
23
+ }),
24
+ keepalive: true,
25
+ signal: AbortSignal.timeout(1e4)
26
+ }).then((res) => {
27
+ if (res.ok) syncedSessionId = sessionId;
17
28
  }).catch(() => {
18
- log.warn("fire-and-forget %s failed", method);
29
+ log.warn("session sync failed, will retry");
19
30
  });
20
31
  }
32
+ function ensureSynced(sessionId) {
33
+ if (syncedSessionId === sessionId) return;
34
+ if (Date.now() - syncAttemptMs < SYNC_COOLDOWN_MS) return;
35
+ syncSession(sessionId, getVisitorId());
36
+ }
21
37
  async function onRotate(sessionId) {
38
+ syncedSessionId = null;
22
39
  if (!target) return;
23
40
  const visitorId = await whenVisitorReady();
24
41
  log.debug("POST session %s (visitor=%s)", sessionId, visitorId ?? "pending");
25
- fire(target.url, "POST", target.headers, {
26
- sessionId,
27
- visitorId
28
- });
42
+ syncSession(sessionId, visitorId);
29
43
  }
30
44
  function bootstrap(sessionTarget) {
31
45
  target = sessionTarget;
@@ -35,7 +49,9 @@ function bootstrap(sessionTarget) {
35
49
  }
36
50
  const session = {
37
51
  getId() {
38
- return mgr?.getSessionId() ?? null;
52
+ const id = mgr?.getSessionId() ?? null;
53
+ if (id) ensureSynced(id);
54
+ return id;
39
55
  },
40
56
  getWindowId() {
41
57
  return mgr?.getWindowId() ?? null;
@@ -56,10 +72,19 @@ const identity = {
56
72
  identifiedSessionId = sessionId;
57
73
  const visitorId = getVisitorId();
58
74
  log.info("PUT session %s → user %s", sessionId, params.identifier);
59
- fire(target.url, "PUT", target.headers, {
60
- sessionId,
61
- visitorId,
62
- ...params
75
+ fetch(target.url, {
76
+ method: "PUT",
77
+ headers: buildHeaders(target.headers),
78
+ body: JSON.stringify({
79
+ sessionId,
80
+ visitorId,
81
+ ...params
82
+ }),
83
+ keepalive: true,
84
+ signal: AbortSignal.timeout(1e4)
85
+ }).catch(() => {
86
+ identifiedSessionId = null;
87
+ log.warn("identify failed for session %s", sessionId);
63
88
  });
64
89
  },
65
90
  clear() {
@@ -69,6 +94,8 @@ const identity = {
69
94
  };
70
95
  function teardown() {
71
96
  identity.clear();
97
+ syncedSessionId = null;
98
+ syncAttemptMs = 0;
72
99
  mgr = null;
73
100
  target = null;
74
101
  }
@@ -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\nfunction fire(\n url: string,\n method: \"POST\" | \"PUT\",\n headers: Headers,\n body: unknown\n): void {\n fetch(url, {\n method,\n headers: buildHeaders(headers),\n body: JSON.stringify(body),\n keepalive: true,\n }).catch(() => {\n log.warn(\"fire-and-forget %s failed\", method);\n });\n}\n\nasync function onRotate(sessionId: string): Promise<void> {\n if (!target) {\n return;\n }\n const visitorId = await whenVisitorReady();\n log.debug(\"POST session %s (visitor=%s)\", sessionId, visitorId ?? \"pending\");\n fire(target.url, \"POST\", target.headers, { sessionId, visitorId });\n}\n\nexport function bootstrap(sessionTarget: IngestTarget): void {\n target = sessionTarget;\n\n mgr = new SessionManager((id) => {\n onRotate(id).catch(() => {\n /* best-effort */\n });\n });\n}\n\nexport const session = {\n getId(): string | null {\n return mgr?.getSessionId() ?? null;\n },\n\n getWindowId(): string | null {\n return mgr?.getWindowId() ?? null;\n },\n};\n\nexport const identity = {\n get(): IdentifyParams | null {\n return currentIdentity;\n },\n\n set(params: IdentifyParams): void {\n if (!(mgr && target)) {\n return;\n }\n const sessionId = mgr.getSessionId();\n if (identifiedSessionId === sessionId) {\n log.debug(\"skipped, already identified for session %s\", sessionId);\n return;\n }\n\n currentIdentity = params;\n identifiedSessionId = sessionId;\n\n const visitorId = getVisitorId();\n log.info(\"PUT session %s → user %s\", sessionId, params.identifier);\n fire(target.url, \"PUT\", target.headers, {\n sessionId,\n visitorId,\n ...params,\n });\n },\n\n clear(): void {\n currentIdentity = null;\n identifiedSessionId = null;\n },\n};\n\nexport function teardown(): void {\n identity.clear();\n mgr = null;\n target = null;\n}\n"],"mappings":";;;;;AAOA,MAAM,MAAM,aAAa,WAAW;AAEpC,IAAI,MAA6B;AACjC,IAAI,SAA8B;AAClC,IAAI,kBAAyC;AAC7C,IAAI,sBAAqC;AAEzC,SAAS,KACP,KACA,QACA,SACA,MACM;AACN,OAAM,KAAK;EACT;EACA,SAAS,aAAa,QAAQ;EAC9B,MAAM,KAAK,UAAU,KAAK;EAC1B,WAAW;EACZ,CAAC,CAAC,YAAY;AACb,MAAI,KAAK,6BAA6B,OAAO;GAC7C;;AAGJ,eAAe,SAAS,WAAkC;AACxD,KAAI,CAAC,OACH;CAEF,MAAM,YAAY,MAAM,kBAAkB;AAC1C,KAAI,MAAM,gCAAgC,WAAW,aAAa,UAAU;AAC5E,MAAK,OAAO,KAAK,QAAQ,OAAO,SAAS;EAAE;EAAW;EAAW,CAAC;;AAGpE,SAAgB,UAAU,eAAmC;AAC3D,UAAS;AAET,OAAM,IAAI,gBAAgB,OAAO;AAC/B,WAAS,GAAG,CAAC,YAAY,GAEvB;GACF;;AAGJ,MAAa,UAAU;CACrB,QAAuB;AACrB,SAAO,KAAK,cAAc,IAAI;;CAGhC,cAA6B;AAC3B,SAAO,KAAK,aAAa,IAAI;;CAEhC;AAED,MAAa,WAAW;CACtB,MAA6B;AAC3B,SAAO;;CAGT,IAAI,QAA8B;AAChC,MAAI,EAAE,OAAO,QACX;EAEF,MAAM,YAAY,IAAI,cAAc;AACpC,MAAI,wBAAwB,WAAW;AACrC,OAAI,MAAM,8CAA8C,UAAU;AAClE;;AAGF,oBAAkB;AAClB,wBAAsB;EAEtB,MAAM,YAAY,cAAc;AAChC,MAAI,KAAK,4BAA4B,WAAW,OAAO,WAAW;AAClE,OAAK,OAAO,KAAK,OAAO,OAAO,SAAS;GACtC;GACA;GACA,GAAG;GACJ,CAAC;;CAGJ,QAAc;AACZ,oBAAkB;AAClB,wBAAsB;;CAEzB;AAED,SAAgB,WAAiB;AAC/B,UAAS,OAAO;AAChB,OAAM;AACN,UAAS"}
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 +1 @@
1
- {"version":3,"file":"session.d.mts","names":[],"sources":["../../src/tracking/session.ts"],"mappings":";KAoBY,QAAA,IAAY,SAAA;AAAA,cAEX,cAAA;EACX,SAAA;EACA,QAAA;EAAA,iBACiB,KAAA;EAAA,iBACA,OAAA;EAAA,iBACA,QAAA;EAAA,QACT,cAAA;cAEI,QAAA,GAAW,QAAA;EAOvB,YAAA,CAAA;EAQA,WAAA,CAAA;EAAA,QAgBQ,OAAA;EAAA,QAWA,MAAA;EAAA,QAQA,KAAA;EAAA,QAKA,SAAA;AAAA"}
1
+ {"version":3,"file":"session.d.mts","names":[],"sources":["../../src/tracking/session.ts"],"mappings":";KAmBY,QAAA,IAAY,SAAA;AAAA,cAEX,cAAA;EACX,SAAA;EACA,QAAA;EAAA,iBACiB,KAAA;EAAA,iBACA,OAAA;EAAA,iBACA,QAAA;EAAA,QACT,cAAA;cAEI,QAAA,GAAW,QAAA;EAOvB,YAAA,CAAA;EAQA,WAAA,CAAA;EAAA,QAgBQ,OAAA;EAAA,QAWA,MAAA;EAAA,QAQA,KAAA;EAAA,QAKA,SAAA;AAAA"}
@@ -1,4 +1,3 @@
1
- import { nanoid } from "nanoid";
2
1
  import { v7 } from "uuid";
3
2
  //#region src/tracking/session.ts
4
3
  const SESSION_ID_KEY = "interfere:session_id";
@@ -43,7 +42,7 @@ var SessionManager = class {
43
42
  this.windowId = stored;
44
43
  return stored;
45
44
  }
46
- this.windowId = `win_${nanoid(10)}`;
45
+ this.windowId = `win_${crypto.randomUUID()}`;
47
46
  this.session?.setItem(WINDOW_ID_KEY, this.windowId);
48
47
  return this.windowId;
49
48
  }
@@ -1 +1 @@
1
- {"version":3,"file":"session.mjs","names":["uuidv7"],"sources":["../../src/tracking/session.ts"],"sourcesContent":["import { nanoid } from \"nanoid\";\nimport { v7 as uuidv7 } from \"uuid\";\n\nconst SESSION_ID_KEY = \"interfere:session_id\";\nconst LAST_ACTIVITY_KEY = \"interfere:last_activity\";\nconst WINDOW_ID_KEY = \"interfere:window_id\";\nconst SESSION_TIMEOUT_MS = 30 * 60 * 1000;\n\nfunction tryStorage(type: \"localStorage\" | \"sessionStorage\"): Storage | null {\n try {\n const s = globalThis[type];\n const key = \"__interfere_probe__\";\n s.setItem(key, \"1\");\n s.removeItem(key);\n return s;\n } catch {\n return null;\n }\n}\n\nexport type OnRotate = (sessionId: string) => void;\n\nexport class SessionManager {\n sessionId: string | null = null;\n windowId: string | null = null;\n private readonly local: Storage | null;\n private readonly session: Storage | null;\n private readonly onRotate: OnRotate | null = null;\n private lastActivityMs = 0;\n\n constructor(onRotate?: OnRotate) {\n this.local = tryStorage(\"localStorage\");\n this.session = tryStorage(\"sessionStorage\");\n this.onRotate = onRotate ?? null;\n this.restore();\n }\n\n getSessionId(): string {\n if (this.sessionId && !this.isExpired()) {\n this.touch();\n return this.sessionId;\n }\n return this.rotate();\n }\n\n getWindowId(): string {\n if (this.windowId) {\n return this.windowId;\n }\n\n const stored = this.session?.getItem(WINDOW_ID_KEY);\n if (stored) {\n this.windowId = stored;\n return stored;\n }\n\n this.windowId = `win_${nanoid(10)}`;\n this.session?.setItem(WINDOW_ID_KEY, this.windowId);\n return this.windowId;\n }\n\n private restore(): void {\n const stored = this.local?.getItem(SESSION_ID_KEY);\n\n if (stored && !this.isExpired()) {\n this.sessionId = stored;\n this.touch();\n } else {\n this.rotate();\n }\n }\n\n private rotate(): string {\n this.sessionId = uuidv7();\n this.local?.setItem(SESSION_ID_KEY, this.sessionId);\n this.touch();\n this.onRotate?.(this.sessionId);\n return this.sessionId;\n }\n\n private touch(): void {\n this.lastActivityMs = Date.now();\n this.local?.setItem(LAST_ACTIVITY_KEY, String(this.lastActivityMs));\n }\n\n private isExpired(): boolean {\n const raw = this.local?.getItem(LAST_ACTIVITY_KEY);\n const ts = raw ? Number(raw) : this.lastActivityMs;\n\n if (ts === 0) {\n return true;\n }\n return Date.now() - ts > SESSION_TIMEOUT_MS;\n }\n}\n"],"mappings":";;;AAGA,MAAM,iBAAiB;AACvB,MAAM,oBAAoB;AAC1B,MAAM,gBAAgB;AACtB,MAAM,qBAAqB,OAAU;AAErC,SAAS,WAAW,MAAyD;AAC3E,KAAI;EACF,MAAM,IAAI,WAAW;EACrB,MAAM,MAAM;AACZ,IAAE,QAAQ,KAAK,IAAI;AACnB,IAAE,WAAW,IAAI;AACjB,SAAO;SACD;AACN,SAAO;;;AAMX,IAAa,iBAAb,MAA4B;CAC1B,YAA2B;CAC3B,WAA0B;CAC1B;CACA;CACA,WAA6C;CAC7C,iBAAyB;CAEzB,YAAY,UAAqB;AAC/B,OAAK,QAAQ,WAAW,eAAe;AACvC,OAAK,UAAU,WAAW,iBAAiB;AAC3C,OAAK,WAAW,YAAY;AAC5B,OAAK,SAAS;;CAGhB,eAAuB;AACrB,MAAI,KAAK,aAAa,CAAC,KAAK,WAAW,EAAE;AACvC,QAAK,OAAO;AACZ,UAAO,KAAK;;AAEd,SAAO,KAAK,QAAQ;;CAGtB,cAAsB;AACpB,MAAI,KAAK,SACP,QAAO,KAAK;EAGd,MAAM,SAAS,KAAK,SAAS,QAAQ,cAAc;AACnD,MAAI,QAAQ;AACV,QAAK,WAAW;AAChB,UAAO;;AAGT,OAAK,WAAW,OAAO,OAAO,GAAG;AACjC,OAAK,SAAS,QAAQ,eAAe,KAAK,SAAS;AACnD,SAAO,KAAK;;CAGd,UAAwB;EACtB,MAAM,SAAS,KAAK,OAAO,QAAQ,eAAe;AAElD,MAAI,UAAU,CAAC,KAAK,WAAW,EAAE;AAC/B,QAAK,YAAY;AACjB,QAAK,OAAO;QAEZ,MAAK,QAAQ;;CAIjB,SAAyB;AACvB,OAAK,YAAYA,IAAQ;AACzB,OAAK,OAAO,QAAQ,gBAAgB,KAAK,UAAU;AACnD,OAAK,OAAO;AACZ,OAAK,WAAW,KAAK,UAAU;AAC/B,SAAO,KAAK;;CAGd,QAAsB;AACpB,OAAK,iBAAiB,KAAK,KAAK;AAChC,OAAK,OAAO,QAAQ,mBAAmB,OAAO,KAAK,eAAe,CAAC;;CAGrE,YAA6B;EAC3B,MAAM,MAAM,KAAK,OAAO,QAAQ,kBAAkB;EAClD,MAAM,KAAK,MAAM,OAAO,IAAI,GAAG,KAAK;AAEpC,MAAI,OAAO,EACT,QAAO;AAET,SAAO,KAAK,KAAK,GAAG,KAAK"}
1
+ {"version":3,"file":"session.mjs","names":["uuidv7"],"sources":["../../src/tracking/session.ts"],"sourcesContent":["import { v7 as uuidv7 } from \"uuid\";\n\nconst SESSION_ID_KEY = \"interfere:session_id\";\nconst LAST_ACTIVITY_KEY = \"interfere:last_activity\";\nconst WINDOW_ID_KEY = \"interfere:window_id\";\nconst SESSION_TIMEOUT_MS = 30 * 60 * 1000;\n\nfunction tryStorage(type: \"localStorage\" | \"sessionStorage\"): Storage | null {\n try {\n const s = globalThis[type];\n const key = \"__interfere_probe__\";\n s.setItem(key, \"1\");\n s.removeItem(key);\n return s;\n } catch {\n return null;\n }\n}\n\nexport type OnRotate = (sessionId: string) => void;\n\nexport class SessionManager {\n sessionId: string | null = null;\n windowId: string | null = null;\n private readonly local: Storage | null;\n private readonly session: Storage | null;\n private readonly onRotate: OnRotate | null = null;\n private lastActivityMs = 0;\n\n constructor(onRotate?: OnRotate) {\n this.local = tryStorage(\"localStorage\");\n this.session = tryStorage(\"sessionStorage\");\n this.onRotate = onRotate ?? null;\n this.restore();\n }\n\n getSessionId(): string {\n if (this.sessionId && !this.isExpired()) {\n this.touch();\n return this.sessionId;\n }\n return this.rotate();\n }\n\n getWindowId(): string {\n if (this.windowId) {\n return this.windowId;\n }\n\n const stored = this.session?.getItem(WINDOW_ID_KEY);\n if (stored) {\n this.windowId = stored;\n return stored;\n }\n\n this.windowId = `win_${crypto.randomUUID()}`;\n this.session?.setItem(WINDOW_ID_KEY, this.windowId);\n return this.windowId;\n }\n\n private restore(): void {\n const stored = this.local?.getItem(SESSION_ID_KEY);\n\n if (stored && !this.isExpired()) {\n this.sessionId = stored;\n this.touch();\n } else {\n this.rotate();\n }\n }\n\n private rotate(): string {\n this.sessionId = uuidv7();\n this.local?.setItem(SESSION_ID_KEY, this.sessionId);\n this.touch();\n this.onRotate?.(this.sessionId);\n return this.sessionId;\n }\n\n private touch(): void {\n this.lastActivityMs = Date.now();\n this.local?.setItem(LAST_ACTIVITY_KEY, String(this.lastActivityMs));\n }\n\n private isExpired(): boolean {\n const raw = this.local?.getItem(LAST_ACTIVITY_KEY);\n const ts = raw ? Number(raw) : this.lastActivityMs;\n\n if (ts === 0) {\n return true;\n }\n return Date.now() - ts > SESSION_TIMEOUT_MS;\n }\n}\n"],"mappings":";;AAEA,MAAM,iBAAiB;AACvB,MAAM,oBAAoB;AAC1B,MAAM,gBAAgB;AACtB,MAAM,qBAAqB,OAAU;AAErC,SAAS,WAAW,MAAyD;AAC3E,KAAI;EACF,MAAM,IAAI,WAAW;EACrB,MAAM,MAAM;AACZ,IAAE,QAAQ,KAAK,IAAI;AACnB,IAAE,WAAW,IAAI;AACjB,SAAO;SACD;AACN,SAAO;;;AAMX,IAAa,iBAAb,MAA4B;CAC1B,YAA2B;CAC3B,WAA0B;CAC1B;CACA;CACA,WAA6C;CAC7C,iBAAyB;CAEzB,YAAY,UAAqB;AAC/B,OAAK,QAAQ,WAAW,eAAe;AACvC,OAAK,UAAU,WAAW,iBAAiB;AAC3C,OAAK,WAAW,YAAY;AAC5B,OAAK,SAAS;;CAGhB,eAAuB;AACrB,MAAI,KAAK,aAAa,CAAC,KAAK,WAAW,EAAE;AACvC,QAAK,OAAO;AACZ,UAAO,KAAK;;AAEd,SAAO,KAAK,QAAQ;;CAGtB,cAAsB;AACpB,MAAI,KAAK,SACP,QAAO,KAAK;EAGd,MAAM,SAAS,KAAK,SAAS,QAAQ,cAAc;AACnD,MAAI,QAAQ;AACV,QAAK,WAAW;AAChB,UAAO;;AAGT,OAAK,WAAW,OAAO,OAAO,YAAY;AAC1C,OAAK,SAAS,QAAQ,eAAe,KAAK,SAAS;AACnD,SAAO,KAAK;;CAGd,UAAwB;EACtB,MAAM,SAAS,KAAK,OAAO,QAAQ,eAAe;AAElD,MAAI,UAAU,CAAC,KAAK,WAAW,EAAE;AAC/B,QAAK,YAAY;AACjB,QAAK,OAAO;QAEZ,MAAK,QAAQ;;CAIjB,SAAyB;AACvB,OAAK,YAAYA,IAAQ;AACzB,OAAK,OAAO,QAAQ,gBAAgB,KAAK,UAAU;AACnD,OAAK,OAAO;AACZ,OAAK,WAAW,KAAK,UAAU;AAC/B,SAAO,KAAK;;CAGd,QAAsB;AACpB,OAAK,iBAAiB,KAAK,KAAK;AAChC,OAAK,OAAO,QAAQ,mBAAmB,OAAO,KAAK,eAAe,CAAC;;CAGrE,YAA6B;EAC3B,MAAM,MAAM,KAAK,OAAO,QAAQ,kBAAkB;EAClD,MAAM,KAAK,MAAM,OAAO,IAAI,GAAG,KAAK;AAEpC,MAAI,OAAO,EACT,QAAO;AAET,SAAO,KAAK,KAAK,GAAG,KAAK"}
@@ -6,6 +6,7 @@ interface IngestTarget {
6
6
  url: string;
7
7
  }
8
8
  declare function buildHeaders(base: Headers): Record<string, string>;
9
+ declare function hasServiceWorker(): boolean;
9
10
  declare class HttpTransport {
10
11
  private readonly target;
11
12
  private pendingKeepalive;
@@ -13,4 +14,4 @@ declare class HttpTransport {
13
14
  send(envelopes: Envelope[]): Promise<void>;
14
15
  }
15
16
  //#endregion
16
- export { HttpTransport, IngestTarget, buildHeaders };
17
+ export { HttpTransport, IngestTarget, buildHeaders, hasServiceWorker };
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.mts","names":[],"sources":["../../src/transport/http.ts"],"mappings":";;;UAUiB,YAAA;EACf,OAAA,EAAS,OAAA;EACT,GAAA;AAAA;AAAA,iBAGc,YAAA,CAAa,IAAA,EAAM,OAAA,GAAU,MAAA;AAAA,cA4BhC,aAAA;EAAA,iBACM,MAAA;EAAA,QACT,gBAAA;cAEI,MAAA,EAAQ,YAAA;EAId,IAAA,CAAK,SAAA,EAAW,QAAA,KAAa,OAAA;AAAA"}
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"}
@@ -4,6 +4,7 @@ import { session } from "../tracking/api.mjs";
4
4
  import { make } from "tctx/traceparent";
5
5
  //#region src/transport/http.ts
6
6
  const log = createLogger("http");
7
+ const SEND_TIMEOUT_MS = 1e4;
7
8
  function buildHeaders(base) {
8
9
  const h = Object.fromEntries(base.entries());
9
10
  h.traceparent = String(make());
@@ -16,6 +17,9 @@ function buildHeaders(base) {
16
17
  function hasServiceWorker() {
17
18
  return typeof navigator !== "undefined" && "serviceWorker" in navigator && navigator.serviceWorker.controller !== null;
18
19
  }
20
+ function assertOk(response) {
21
+ if (!response.ok) throw new Error(`ingest responded ${response.status}`);
22
+ }
19
23
  const KEEPALIVE_BUDGET_BYTES = 61440;
20
24
  const MAX_CONCURRENT_KEEPALIVE = 15;
21
25
  var HttpTransport = class {
@@ -29,11 +33,12 @@ var HttpTransport = class {
29
33
  const headers = buildHeaders(this.target.headers);
30
34
  if (hasServiceWorker()) {
31
35
  log.debug("POST %d envelopes via SW", envelopes.length);
32
- await fetch(this.target.url, {
36
+ assertOk(await fetch(this.target.url, {
33
37
  method: "POST",
34
38
  headers,
35
- body
36
- });
39
+ body,
40
+ signal: AbortSignal.timeout(SEND_TIMEOUT_MS)
41
+ }));
37
42
  return;
38
43
  }
39
44
  const bytes = new TextEncoder().encode(body).byteLength;
@@ -41,16 +46,17 @@ var HttpTransport = class {
41
46
  if (useKeepalive) this.pendingKeepalive++;
42
47
  log.debug("POST %d envelopes direct (%d bytes, keepalive=%s)", envelopes.length, bytes, useKeepalive);
43
48
  try {
44
- await fetch(this.target.url, {
49
+ assertOk(await fetch(this.target.url, {
45
50
  method: "POST",
46
51
  headers,
47
52
  body,
48
- keepalive: useKeepalive
49
- });
53
+ keepalive: useKeepalive,
54
+ signal: AbortSignal.timeout(SEND_TIMEOUT_MS)
55
+ }));
50
56
  } finally {
51
57
  if (useKeepalive) this.pendingKeepalive--;
52
58
  }
53
59
  }
54
60
  };
55
61
  //#endregion
56
- export { HttpTransport, buildHeaders };
62
+ export { HttpTransport, buildHeaders, hasServiceWorker };
@@ -1 +1 @@
1
- {"version":3,"file":"http.mjs","names":[],"sources":["../../src/transport/http.ts"],"sourcesContent":["import type { Envelope } from \"@interfere/types/sdk/envelope\";\n\nimport { make } from \"tctx/traceparent\";\n\nimport { session } from \"../tracking/api.js\";\nimport { getVisitorId } from \"../tracking/visitor.js\";\nimport { createLogger } from \"../util/log.js\";\n\nconst log = createLogger(\"http\");\n\nexport interface IngestTarget {\n headers: Headers;\n url: string;\n}\n\nexport function buildHeaders(base: Headers): Record<string, string> {\n const h: Record<string, string> = Object.fromEntries(base.entries());\n h.traceparent = String(make());\n\n const sessionId = session.getId();\n if (sessionId) {\n h[\"x-interfere-session\"] = sessionId;\n }\n\n const visitorId = getVisitorId();\n if (visitorId) {\n h[\"x-interfere-visitor\"] = visitorId;\n }\n\n return h;\n}\n\nfunction hasServiceWorker(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n \"serviceWorker\" in navigator &&\n navigator.serviceWorker.controller !== null\n );\n}\n\nconst KEEPALIVE_BUDGET_BYTES = 61_440;\nconst MAX_CONCURRENT_KEEPALIVE = 15;\n\nexport class HttpTransport {\n private readonly target: IngestTarget;\n private pendingKeepalive = 0;\n\n constructor(target: IngestTarget) {\n this.target = target;\n }\n\n async send(envelopes: Envelope[]): Promise<void> {\n const body = JSON.stringify(envelopes);\n const headers = buildHeaders(this.target.headers);\n\n if (hasServiceWorker()) {\n log.debug(\"POST %d envelopes via SW\", envelopes.length);\n await fetch(this.target.url, {\n method: \"POST\",\n headers,\n body,\n });\n return;\n }\n\n const bytes = new TextEncoder().encode(body).byteLength;\n const useKeepalive =\n bytes < KEEPALIVE_BUDGET_BYTES &&\n this.pendingKeepalive < MAX_CONCURRENT_KEEPALIVE;\n\n if (useKeepalive) {\n this.pendingKeepalive++;\n }\n\n log.debug(\n \"POST %d envelopes direct (%d bytes, keepalive=%s)\",\n envelopes.length,\n bytes,\n useKeepalive\n );\n\n try {\n await fetch(this.target.url, {\n method: \"POST\",\n headers,\n body,\n keepalive: useKeepalive,\n });\n } finally {\n if (useKeepalive) {\n this.pendingKeepalive--;\n }\n }\n }\n}\n"],"mappings":";;;;;AAQA,MAAM,MAAM,aAAa,OAAO;AAOhC,SAAgB,aAAa,MAAuC;CAClE,MAAM,IAA4B,OAAO,YAAY,KAAK,SAAS,CAAC;AACpE,GAAE,cAAc,OAAO,MAAM,CAAC;CAE9B,MAAM,YAAY,QAAQ,OAAO;AACjC,KAAI,UACF,GAAE,yBAAyB;CAG7B,MAAM,YAAY,cAAc;AAChC,KAAI,UACF,GAAE,yBAAyB;AAG7B,QAAO;;AAGT,SAAS,mBAA4B;AACnC,QACE,OAAO,cAAc,eACrB,mBAAmB,aACnB,UAAU,cAAc,eAAe;;AAI3C,MAAM,yBAAyB;AAC/B,MAAM,2BAA2B;AAEjC,IAAa,gBAAb,MAA2B;CACzB;CACA,mBAA2B;CAE3B,YAAY,QAAsB;AAChC,OAAK,SAAS;;CAGhB,MAAM,KAAK,WAAsC;EAC/C,MAAM,OAAO,KAAK,UAAU,UAAU;EACtC,MAAM,UAAU,aAAa,KAAK,OAAO,QAAQ;AAEjD,MAAI,kBAAkB,EAAE;AACtB,OAAI,MAAM,4BAA4B,UAAU,OAAO;AACvD,SAAM,MAAM,KAAK,OAAO,KAAK;IAC3B,QAAQ;IACR;IACA;IACD,CAAC;AACF;;EAGF,MAAM,QAAQ,IAAI,aAAa,CAAC,OAAO,KAAK,CAAC;EAC7C,MAAM,eACJ,QAAQ,0BACR,KAAK,mBAAmB;AAE1B,MAAI,aACF,MAAK;AAGP,MAAI,MACF,qDACA,UAAU,QACV,OACA,aACD;AAED,MAAI;AACF,SAAM,MAAM,KAAK,OAAO,KAAK;IAC3B,QAAQ;IACR;IACA;IACA,WAAW;IACZ,CAAC;YACM;AACR,OAAI,aACF,MAAK"}
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"}
@@ -5,19 +5,25 @@ import { Envelope } from "@interfere/types/sdk/envelope";
5
5
  interface QueueOptions {
6
6
  batchMs?: number;
7
7
  batchSize?: number;
8
+ maxBufferSize?: number;
8
9
  transport: HttpTransport;
9
10
  }
10
11
  declare class BatchQueue {
11
12
  private readonly buffer;
12
13
  private timer;
14
+ private flushing;
15
+ private failures;
16
+ private pausedUntil;
13
17
  private readonly batchSize;
14
18
  private readonly batchMs;
19
+ private readonly maxBufferSize;
15
20
  private readonly transport;
16
21
  constructor(opts: QueueOptions);
17
22
  start(): void;
18
23
  enqueue(envelope: Envelope): void;
19
24
  flush(): void;
20
25
  dispose(): void;
26
+ private drain;
21
27
  private readonly onVisibilityChange;
22
28
  private readonly onBeforeUnload;
23
29
  }
@@ -1 +1 @@
1
- {"version":3,"file":"queue.d.mts","names":[],"sources":["../../src/transport/queue.ts"],"mappings":";;;;UAUiB,YAAA;EACf,OAAA;EACA,SAAA;EACA,SAAA,EAAW,aAAA;AAAA;AAAA,cAGA,UAAA;EAAA,iBACM,MAAA;EAAA,QACT,KAAA;EAAA,iBACS,SAAA;EAAA,iBACA,OAAA;EAAA,iBACA,SAAA;cAEL,IAAA,EAAM,YAAA;EAMlB,KAAA,CAAA;EAeA,OAAA,CAAQ,QAAA,EAAU,QAAA;EAOlB,KAAA,CAAA;EAWA,OAAA,CAAA;EAAA,iBAiBiB,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;EAuCA,OAAA,CAAA;EAAA,QAiBQ,KAAA;EAAA,iBASS,kBAAA;EAAA,iBAMA,cAAA;AAAA"}
@@ -1,17 +1,26 @@
1
1
  import { createLogger } from "../util/log.mjs";
2
+ import { hasServiceWorker } from "./http.mjs";
2
3
  //#region src/transport/queue.ts
3
4
  const log = createLogger("queue");
4
5
  const DEFAULT_BATCH_SIZE = 10;
5
6
  const DEFAULT_BATCH_MS = 250;
7
+ const DEFAULT_MAX_BUFFER_SIZE = 1e3;
8
+ const BREAKER_THRESHOLD = 5;
9
+ const BREAKER_COOLDOWN_MS = 3e4;
6
10
  var BatchQueue = class {
7
11
  buffer = [];
8
12
  timer = null;
13
+ flushing = false;
14
+ failures = 0;
15
+ pausedUntil = 0;
9
16
  batchSize;
10
17
  batchMs;
18
+ maxBufferSize;
11
19
  transport;
12
20
  constructor(opts) {
13
21
  this.batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;
14
22
  this.batchMs = opts.batchMs ?? DEFAULT_BATCH_MS;
23
+ this.maxBufferSize = opts.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE;
15
24
  this.transport = opts.transport;
16
25
  }
17
26
  start() {
@@ -26,17 +35,31 @@ var BatchQueue = class {
26
35
  }
27
36
  enqueue(envelope) {
28
37
  this.buffer.push(envelope);
38
+ if (this.buffer.length > this.maxBufferSize) {
39
+ const overflow = this.buffer.length - this.maxBufferSize;
40
+ this.buffer.splice(0, overflow);
41
+ log.warn("buffer full, dropped %d oldest envelopes", overflow);
42
+ }
29
43
  if (this.buffer.length >= this.batchSize) this.flush();
30
44
  }
31
45
  flush() {
32
- while (this.buffer.length > 0) {
33
- const batch = this.buffer.splice(0, this.batchSize);
34
- log.debug("flushing %d envelopes", batch.length);
35
- this.transport.send(batch).catch(() => {
36
- log.warn("send failed, re-queuing %d envelopes", batch.length);
37
- this.buffer.unshift(...batch);
38
- });
39
- }
46
+ if (this.flushing || this.buffer.length === 0) return;
47
+ if (this.failures >= BREAKER_THRESHOLD && Date.now() < this.pausedUntil) return;
48
+ this.flushing = true;
49
+ const batch = this.buffer.splice(0, this.batchSize);
50
+ this.transport.send(batch).then(() => {
51
+ if (this.failures > 0) log.info("send recovered after %d failures", this.failures);
52
+ this.failures = 0;
53
+ }).catch(() => {
54
+ if (!hasServiceWorker()) this.buffer.unshift(...batch);
55
+ this.failures++;
56
+ if (this.failures >= BREAKER_THRESHOLD) {
57
+ this.pausedUntil = Date.now() + BREAKER_COOLDOWN_MS;
58
+ log.warn("pausing sends for %dms after %d consecutive failures", BREAKER_COOLDOWN_MS, this.failures);
59
+ }
60
+ }).finally(() => {
61
+ this.flushing = false;
62
+ });
40
63
  }
41
64
  dispose() {
42
65
  if (this.timer) {
@@ -47,13 +70,19 @@ var BatchQueue = class {
47
70
  globalThis.removeEventListener("visibilitychange", this.onVisibilityChange);
48
71
  globalThis.removeEventListener("beforeunload", this.onBeforeUnload);
49
72
  }
50
- this.flush();
73
+ this.drain();
74
+ }
75
+ drain() {
76
+ while (this.buffer.length > 0) {
77
+ const batch = this.buffer.splice(0, this.batchSize);
78
+ this.transport.send(batch).catch(() => {});
79
+ }
51
80
  }
52
81
  onVisibilityChange = () => {
53
- if (document.visibilityState === "hidden") this.flush();
82
+ if (document.visibilityState === "hidden") this.drain();
54
83
  };
55
84
  onBeforeUnload = () => {
56
- this.flush();
85
+ this.drain();
57
86
  };
58
87
  };
59
88
  //#endregion
@@ -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 } from \"./http.js\";\n\nconst log = createLogger(\"queue\");\n\nconst DEFAULT_BATCH_SIZE = 10;\nconst DEFAULT_BATCH_MS = 250;\n\nexport interface QueueOptions {\n batchMs?: number;\n batchSize?: number;\n transport: HttpTransport;\n}\n\nexport class BatchQueue {\n private readonly buffer: Envelope[] = [];\n private timer: ReturnType<typeof setInterval> | null = null;\n private readonly batchSize: number;\n private readonly batchMs: 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.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 if (this.buffer.length >= this.batchSize) {\n this.flush();\n }\n }\n\n flush(): void {\n while (this.buffer.length > 0) {\n const batch = this.buffer.splice(0, this.batchSize);\n log.debug(\"flushing %d envelopes\", batch.length);\n this.transport.send(batch).catch(() => {\n log.warn(\"send failed, re-queuing %d envelopes\", batch.length);\n this.buffer.unshift(...batch);\n });\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.flush();\n }\n\n private readonly onVisibilityChange = (): void => {\n if (document.visibilityState === \"hidden\") {\n this.flush();\n }\n };\n\n private readonly onBeforeUnload = (): void => {\n this.flush();\n };\n}\n"],"mappings":";;AAKA,MAAM,MAAM,aAAa,QAAQ;AAEjC,MAAM,qBAAqB;AAC3B,MAAM,mBAAmB;AAQzB,IAAa,aAAb,MAAwB;CACtB,SAAsC,EAAE;CACxC,QAAuD;CACvD;CACA;CACA;CAEA,YAAY,MAAoB;AAC9B,OAAK,YAAY,KAAK,aAAa;AACnC,OAAK,UAAU,KAAK,WAAW;AAC/B,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;AAC1B,MAAI,KAAK,OAAO,UAAU,KAAK,UAC7B,MAAK,OAAO;;CAIhB,QAAc;AACZ,SAAO,KAAK,OAAO,SAAS,GAAG;GAC7B,MAAM,QAAQ,KAAK,OAAO,OAAO,GAAG,KAAK,UAAU;AACnD,OAAI,MAAM,yBAAyB,MAAM,OAAO;AAChD,QAAK,UAAU,KAAK,MAAM,CAAC,YAAY;AACrC,QAAI,KAAK,wCAAwC,MAAM,OAAO;AAC9D,SAAK,OAAO,QAAQ,GAAG,MAAM;KAC7B;;;CAIN,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,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\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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interfere/react",
3
- "version": "1.0.3",
3
+ "version": "4.0.0",
4
4
  "license": "MIT",
5
5
  "description": "Client-side React SDK for Interfere. Error tracking, session replay, and analytics.",
6
6
  "keywords": [
@@ -55,10 +55,9 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "@fingerprintjs/fingerprintjs-pro": "^3.12.8",
58
- "@interfere/constants": "^1.0.3",
59
- "@interfere/types": "^1.0.3",
58
+ "@interfere/constants": "^4.0.0",
59
+ "@interfere/types": "^4.0.0",
60
60
  "@ua-parser-js/pro-enterprise": "^2.0.6",
61
- "nanoid": "^5.1.6",
62
61
  "rrweb": "2.0.0-alpha.4",
63
62
  "tctx": "^0.2.5",
64
63
  "uuid": "^13.0.0"
@@ -68,8 +67,8 @@
68
67
  "react-dom": ">=19"
69
68
  },
70
69
  "devDependencies": {
71
- "@interfere/typescript-config": "^2.0.3",
72
- "@interfere/vitest-config": "^2.0.3",
70
+ "@interfere/typescript-config": "^4.0.0",
71
+ "@interfere/vitest-config": "^4.0.0",
73
72
  "@rrweb/types": "2.0.0-alpha.20",
74
73
  "@testing-library/react": "^16.3.2",
75
74
  "@types/node": "^24.12.0",
@@ -79,9 +78,9 @@
79
78
  "@vitest/browser": "4.1.0",
80
79
  "@vitest/browser-playwright": "4.1.0",
81
80
  "@vitest/coverage-v8": "^4.0.18",
82
- "jsdom": "^28.0.0",
81
+ "jsdom": "^29.0.0",
83
82
  "playwright": "^1.56.1",
84
- "tsdown": "^0.21.2",
83
+ "tsdown": "^0.21.3",
85
84
  "typescript": "5.9.3",
86
85
  "vitest": "^4.0.18"
87
86
  }