@eventra_dev/eventra-sdk 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -42,10 +42,6 @@ function uuid() {
42
42
  function sleep(ms) {
43
43
  return new Promise((r) => setTimeout(r, ms));
44
44
  }
45
- function getFetch() {
46
- if (typeof fetch !== "undefined") return fetch.bind(globalThis);
47
- throw new Error("fetch not available");
48
- }
49
45
  var Storage = class {
50
46
  constructor() {
51
47
  this.enabled = false;
@@ -75,14 +71,13 @@ var Storage = class {
75
71
  }
76
72
  };
77
73
  var Leader = class {
78
- constructor() {
74
+ constructor(enabled) {
79
75
  this.isLeader = true;
76
+ if (!enabled) return;
80
77
  if (typeof BroadcastChannel !== "undefined") {
81
78
  this.channel = new BroadcastChannel("eventra");
82
79
  this.channel.onmessage = (e) => {
83
- if (e.data === "leader") {
84
- this.isLeader = false;
85
- }
80
+ if (e.data === "leader") this.isLeader = false;
86
81
  };
87
82
  this.channel.postMessage("leader");
88
83
  }
@@ -90,45 +85,64 @@ var Leader = class {
90
85
  canSend() {
91
86
  return this.isLeader;
92
87
  }
88
+ destroy() {
89
+ this.channel?.close();
90
+ }
93
91
  };
94
92
  var Eventra = class {
95
93
  constructor(options) {
96
- this.fetch = getFetch();
97
94
  this.queue = [];
98
95
  this.inFlight = false;
99
96
  this.destroyed = false;
100
- this.maxBatch = 50;
101
- this.maxQueue = 1e4;
102
- this.flushInterval = 2e3;
103
- this.retries = 3;
104
- this.retryDelay = 300;
105
97
  this.storage = new Storage();
106
- this.leader = new Leader();
98
+ this.leader = null;
107
99
  this.failureCount = 0;
108
100
  this.circuitOpenUntil = 0;
101
+ this.exitHandlers = [];
109
102
  if (!options.apiKey) throw new Error("apiKey required");
103
+ if (!options.endpoint) throw new Error("endpoint required");
104
+ this.options = options;
110
105
  this.apiKey = options.apiKey;
111
- this.endpoint = options.endpoint ?? "";
112
- if (!this.endpoint) throw new Error("endpoint required");
106
+ this.endpoint = options.endpoint;
113
107
  this.runtime = detectRuntime();
114
- this.maxBatch = options.maxBatchSize ?? this.maxBatch;
115
- this.maxQueue = options.maxQueueSize ?? this.maxQueue;
116
- this.flushInterval = options.flushInterval ?? this.flushInterval;
108
+ this.fetch = options.fetchImpl ?? globalThis.fetch;
109
+ if (!this.fetch) {
110
+ throw new Error("fetch not available \u2014 provide fetchImpl");
111
+ }
112
+ this.maxBatch = options.maxBatchSize ?? 50;
113
+ this.maxQueue = options.maxQueueSize ?? 1e4;
114
+ this.flushInterval = options.flushInterval ?? 2e3;
115
+ this.retries = options.maxRetries ?? 3;
116
+ this.retryDelay = options.retryBaseDelayMs ?? 300;
117
117
  this.sdkInfo = {
118
118
  name: "@eventra_dev/eventra-sdk",
119
- version: "ultra",
119
+ version: "1.0.0",
120
120
  runtime: this.runtime
121
121
  };
122
122
  if (this.runtime === "browser") {
123
123
  const saved = this.storage.get("__eventra_q__");
124
- if (saved) this.queue = saved;
124
+ if (Array.isArray(saved)) this.queue = saved;
125
+ }
126
+ if (this.runtime === "browser" && options.multiTabMode === "leader") {
127
+ this.leader = new Leader(true);
128
+ }
129
+ if (!options.disableTimer) {
125
130
  this.startTimer();
131
+ }
132
+ if (this.runtime === "browser") {
126
133
  this.setupBrowserExit();
127
134
  }
135
+ if (options.autoFlushOnExit !== false && this.runtime !== "browser") {
136
+ this.setupProcessExit();
137
+ }
128
138
  }
129
- // PUBLIC
139
+ // ================= PUBLIC =================
130
140
  track(name, options) {
131
141
  if (this.destroyed) return;
142
+ if (this.queue.length >= this.maxQueue) {
143
+ this.options.onEventsDropped?.(1);
144
+ return;
145
+ }
132
146
  const event = {
133
147
  idempotencyKey: uuid(),
134
148
  name,
@@ -136,31 +150,35 @@ var Eventra = class {
136
150
  properties: options?.properties ?? {},
137
151
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
138
152
  };
139
- if (this.runtime !== "browser") {
140
- void this.send([event]);
141
- return;
142
- }
143
- if (this.queue.length >= this.maxQueue) return;
144
153
  this.queue.push(event);
145
- this.persist();
154
+ if (this.runtime === "browser") {
155
+ this.persist();
156
+ }
146
157
  if (this.queue.length >= this.maxBatch) {
147
158
  void this.flush();
148
159
  }
160
+ if (this.runtime === "serverless") {
161
+ void this.flush();
162
+ }
149
163
  }
150
164
  async flush() {
151
165
  if (this.inFlight || this.destroyed) return;
152
166
  if (!this.queue.length) return;
153
- if (!this.leader.canSend()) return;
167
+ if (this.leader && !this.leader.canSend()) return;
154
168
  if (Date.now() < this.circuitOpenUntil) return;
155
169
  this.inFlight = true;
156
170
  const batch = this.queue.splice(0, this.maxBatch);
157
- this.persist();
171
+ if (this.runtime === "browser") {
172
+ this.persist();
173
+ }
158
174
  try {
159
175
  await this.send(batch);
160
176
  this.failureCount = 0;
161
177
  } catch {
162
178
  this.queue = [...batch, ...this.queue];
163
- this.persist();
179
+ if (this.runtime === "browser") {
180
+ this.persist();
181
+ }
164
182
  this.failureCount++;
165
183
  if (this.failureCount >= 5) {
166
184
  this.circuitOpenUntil = Date.now() + 5e3;
@@ -172,8 +190,12 @@ var Eventra = class {
172
190
  destroy() {
173
191
  this.destroyed = true;
174
192
  if (this.timer) clearInterval(this.timer);
193
+ this.leader?.destroy();
194
+ for (const off of this.exitHandlers) {
195
+ off();
196
+ }
175
197
  }
176
- // SEND (CORE)
198
+ // ================= SEND =================
177
199
  async send(events) {
178
200
  const payload = JSON.stringify({
179
201
  sentAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -209,17 +231,43 @@ var Eventra = class {
209
231
  }
210
232
  }
211
233
  }
212
- // BROWSER
234
+ // ================= INTERNAL =================
213
235
  startTimer() {
214
236
  this.timer = setInterval(() => {
215
237
  void this.flush();
216
238
  }, this.flushInterval);
217
239
  }
218
240
  setupBrowserExit() {
219
- window.addEventListener("visibilitychange", () => {
241
+ const handler = () => {
220
242
  if (document.visibilityState === "hidden") {
221
243
  void this.flush();
222
244
  }
245
+ };
246
+ window.addEventListener("visibilitychange", handler);
247
+ this.exitHandlers.push(() => {
248
+ window.removeEventListener("visibilitychange", handler);
249
+ });
250
+ }
251
+ setupProcessExit() {
252
+ const g = globalThis;
253
+ const flushSafe = async () => {
254
+ try {
255
+ await this.flush();
256
+ } catch {
257
+ }
258
+ };
259
+ const beforeExit = () => {
260
+ void flushSafe();
261
+ };
262
+ const sigint = async () => {
263
+ await flushSafe();
264
+ g.exit();
265
+ };
266
+ g?.on?.("beforeExit", beforeExit);
267
+ g?.on?.("SIGINT", sigint);
268
+ this.exitHandlers.push(() => {
269
+ g?.off?.("beforeExit", beforeExit);
270
+ g?.off?.("SIGINT", sigint);
223
271
  });
224
272
  }
225
273
  persist() {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/client.ts"],"sourcesContent":["export * from \"./client\";\nexport * from \"./types\";\n","import { TrackerOptions, TrackEvent, SdkInfo } from \"./types\";\n\n// RUNTIME DETECTION\ntype Runtime = \"browser\" | \"node\" | \"edge\" | \"serverless\" | \"unknown\";\n\nfunction detectRuntime(): Runtime {\n const g = globalThis as any;\n\n if (typeof window !== \"undefined\") return \"browser\";\n if (g.EdgeRuntime) return \"edge\";\n if (g.process?.env?.AWS_LAMBDA_FUNCTION_NAME) return \"serverless\";\n if (g.process?.versions?.node) return \"node\";\n\n return \"unknown\";\n}\n\n// HELPERS\nfunction uuid(): string {\n if (typeof crypto !== \"undefined\" && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n return `${Date.now()}-${Math.random().toString(16).slice(2)}`;\n}\n\nfunction sleep(ms: number) {\n return new Promise((r) => setTimeout(r, ms));\n}\n\nfunction getFetch(): typeof fetch {\n if (typeof fetch !== \"undefined\") return fetch.bind(globalThis);\n throw new Error(\"fetch not available\");\n}\n\n// SAFE STORAGE (browser only)\nclass Storage {\n private enabled = false;\n\n constructor() {\n try {\n if (typeof localStorage !== \"undefined\") {\n localStorage.setItem(\"__t\", \"1\");\n localStorage.removeItem(\"__t\");\n this.enabled = true;\n }\n } catch {}\n }\n\n get(key: string): any {\n if (!this.enabled) return null;\n try {\n return JSON.parse(localStorage.getItem(key) || \"null\");\n } catch {\n return null;\n }\n }\n\n set(key: string, value: any) {\n if (!this.enabled) return;\n try {\n localStorage.setItem(key, JSON.stringify(value));\n } catch {}\n }\n}\n\n// LEADER ELECTION (browser)\nclass Leader {\n private channel?: BroadcastChannel;\n private isLeader = true;\n\n constructor() {\n if (typeof BroadcastChannel !== \"undefined\") {\n this.channel = new BroadcastChannel(\"eventra\");\n\n this.channel.onmessage = (e) => {\n if (e.data === \"leader\") {\n this.isLeader = false;\n }\n };\n\n this.channel.postMessage(\"leader\");\n }\n }\n\n canSend() {\n return this.isLeader;\n }\n}\n\n// SDK\nexport class Eventra {\n private apiKey: string;\n private endpoint: string;\n private runtime: Runtime;\n private fetch = getFetch();\n\n private queue: TrackEvent[] = [];\n private inFlight = false;\n private destroyed = false;\n\n private maxBatch = 50;\n private maxQueue = 10000;\n private flushInterval = 2000;\n private retries = 3;\n private retryDelay = 300;\n\n private timer?: any;\n\n private storage = new Storage();\n private leader = new Leader();\n\n private failureCount = 0;\n private circuitOpenUntil = 0;\n\n private sdkInfo: SdkInfo;\n\n constructor(options: TrackerOptions) {\n if (!options.apiKey) throw new Error(\"apiKey required\");\n\n this.apiKey = options.apiKey;\n this.endpoint = options.endpoint ?? \"\";\n\n if (!this.endpoint) throw new Error(\"endpoint required\");\n\n this.runtime = detectRuntime();\n\n this.maxBatch = options.maxBatchSize ?? this.maxBatch;\n this.maxQueue = options.maxQueueSize ?? this.maxQueue;\n this.flushInterval = options.flushInterval ?? this.flushInterval;\n\n this.sdkInfo = {\n name: \"@eventra_dev/eventra-sdk\",\n version: \"ultra\",\n runtime: this.runtime,\n };\n\n if (this.runtime === \"browser\") {\n const saved = this.storage.get(\"__eventra_q__\");\n if (saved) this.queue = saved;\n\n this.startTimer();\n this.setupBrowserExit();\n }\n }\n\n // PUBLIC\n track(name: string, options?: any) {\n if (this.destroyed) return;\n\n const event: TrackEvent = {\n idempotencyKey: uuid(),\n name,\n userId: options?.userId,\n properties: options?.properties ?? {},\n timestamp: new Date().toISOString(),\n };\n\n // server environments → instant send\n if (this.runtime !== \"browser\") {\n void this.send([event]);\n return;\n }\n\n // browser → queue\n if (this.queue.length >= this.maxQueue) return;\n\n this.queue.push(event);\n this.persist();\n\n if (this.queue.length >= this.maxBatch) {\n void this.flush();\n }\n }\n\n async flush() {\n if (this.inFlight || this.destroyed) return;\n if (!this.queue.length) return;\n\n if (!this.leader.canSend()) return;\n\n if (Date.now() < this.circuitOpenUntil) return;\n\n this.inFlight = true;\n\n const batch = this.queue.splice(0, this.maxBatch);\n this.persist();\n\n try {\n await this.send(batch);\n\n this.failureCount = 0;\n } catch {\n this.queue = [...batch, ...this.queue];\n this.persist();\n\n this.failureCount++;\n\n if (this.failureCount >= 5) {\n this.circuitOpenUntil = Date.now() + 5000;\n }\n } finally {\n this.inFlight = false;\n }\n }\n\n destroy() {\n this.destroyed = true;\n if (this.timer) clearInterval(this.timer);\n }\n\n // SEND (CORE)\n private async send(events: TrackEvent[]) {\n const payload = JSON.stringify({\n sentAt: new Date().toISOString(),\n sdk: this.sdkInfo,\n events,\n });\n\n // sendBeacon fast path\n if (\n this.runtime === \"browser\" &&\n typeof navigator !== \"undefined\" &&\n navigator.sendBeacon &&\n payload.length < 60000 &&\n document.visibilityState === \"hidden\"\n ) {\n navigator.sendBeacon(this.endpoint, payload);\n return;\n }\n\n let attempt = 0;\n\n while (attempt <= this.retries) {\n try {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 5000);\n\n const res = await this.fetch(this.endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": this.apiKey,\n },\n body: payload,\n signal: controller.signal,\n });\n\n clearTimeout(timeout);\n\n if (res.status >= 400 && res.status < 500) return;\n\n if (!res.ok) throw new Error();\n\n return;\n } catch {\n attempt++;\n\n if (attempt > this.retries) throw new Error();\n\n await sleep(this.retryDelay * 2 ** attempt);\n }\n }\n }\n\n // BROWSER\n private startTimer() {\n this.timer = setInterval(() => {\n void this.flush();\n }, this.flushInterval);\n }\n\n private setupBrowserExit() {\n window.addEventListener(\"visibilitychange\", () => {\n if (document.visibilityState === \"hidden\") {\n void this.flush();\n }\n });\n }\n\n private persist() {\n if (this.runtime !== \"browser\") return;\n this.storage.set(\"__eventra_q__\", this.queue);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKA,SAAS,gBAAyB;AAChC,QAAM,IAAI;AAEV,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI,EAAE,YAAa,QAAO;AAC1B,MAAI,EAAE,SAAS,KAAK,yBAA0B,QAAO;AACrD,MAAI,EAAE,SAAS,UAAU,KAAM,QAAO;AAEtC,SAAO;AACT;AAGA,SAAS,OAAe;AACtB,MAAI,OAAO,WAAW,eAAe,OAAO,YAAY;AACtD,WAAO,OAAO,WAAW;AAAA,EAC3B;AACA,SAAO,GAAG,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,CAAC,CAAC;AAC7D;AAEA,SAAS,MAAM,IAAY;AACzB,SAAO,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,EAAE,CAAC;AAC7C;AAEA,SAAS,WAAyB;AAChC,MAAI,OAAO,UAAU,YAAa,QAAO,MAAM,KAAK,UAAU;AAC9D,QAAM,IAAI,MAAM,qBAAqB;AACvC;AAGA,IAAM,UAAN,MAAc;AAAA,EAGZ,cAAc;AAFd,SAAQ,UAAU;AAGhB,QAAI;AACF,UAAI,OAAO,iBAAiB,aAAa;AACvC,qBAAa,QAAQ,OAAO,GAAG;AAC/B,qBAAa,WAAW,KAAK;AAC7B,aAAK,UAAU;AAAA,MACjB;AAAA,IACF,QAAQ;AAAA,IAAC;AAAA,EACX;AAAA,EAEA,IAAI,KAAkB;AACpB,QAAI,CAAC,KAAK,QAAS,QAAO;AAC1B,QAAI;AACF,aAAO,KAAK,MAAM,aAAa,QAAQ,GAAG,KAAK,MAAM;AAAA,IACvD,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,IAAI,KAAa,OAAY;AAC3B,QAAI,CAAC,KAAK,QAAS;AACnB,QAAI;AACF,mBAAa,QAAQ,KAAK,KAAK,UAAU,KAAK,CAAC;AAAA,IACjD,QAAQ;AAAA,IAAC;AAAA,EACX;AACF;AAGA,IAAM,SAAN,MAAa;AAAA,EAIX,cAAc;AAFd,SAAQ,WAAW;AAGjB,QAAI,OAAO,qBAAqB,aAAa;AAC3C,WAAK,UAAU,IAAI,iBAAiB,SAAS;AAE7C,WAAK,QAAQ,YAAY,CAAC,MAAM;AAC9B,YAAI,EAAE,SAAS,UAAU;AACvB,eAAK,WAAW;AAAA,QAClB;AAAA,MACF;AAEA,WAAK,QAAQ,YAAY,QAAQ;AAAA,IACnC;AAAA,EACF;AAAA,EAEA,UAAU;AACR,WAAO,KAAK;AAAA,EACd;AACF;AAGO,IAAM,UAAN,MAAc;AAAA,EA0BnB,YAAY,SAAyB;AAtBrC,SAAQ,QAAQ,SAAS;AAEzB,SAAQ,QAAsB,CAAC;AAC/B,SAAQ,WAAW;AACnB,SAAQ,YAAY;AAEpB,SAAQ,WAAW;AACnB,SAAQ,WAAW;AACnB,SAAQ,gBAAgB;AACxB,SAAQ,UAAU;AAClB,SAAQ,aAAa;AAIrB,SAAQ,UAAU,IAAI,QAAQ;AAC9B,SAAQ,SAAS,IAAI,OAAO;AAE5B,SAAQ,eAAe;AACvB,SAAQ,mBAAmB;AAKzB,QAAI,CAAC,QAAQ,OAAQ,OAAM,IAAI,MAAM,iBAAiB;AAEtD,SAAK,SAAS,QAAQ;AACtB,SAAK,WAAW,QAAQ,YAAY;AAEpC,QAAI,CAAC,KAAK,SAAU,OAAM,IAAI,MAAM,mBAAmB;AAEvD,SAAK,UAAU,cAAc;AAE7B,SAAK,WAAW,QAAQ,gBAAgB,KAAK;AAC7C,SAAK,WAAW,QAAQ,gBAAgB,KAAK;AAC7C,SAAK,gBAAgB,QAAQ,iBAAiB,KAAK;AAEnD,SAAK,UAAU;AAAA,MACb,MAAM;AAAA,MACN,SAAS;AAAA,MACT,SAAS,KAAK;AAAA,IAChB;AAEA,QAAI,KAAK,YAAY,WAAW;AAC9B,YAAM,QAAQ,KAAK,QAAQ,IAAI,eAAe;AAC9C,UAAI,MAAO,MAAK,QAAQ;AAExB,WAAK,WAAW;AAChB,WAAK,iBAAiB;AAAA,IACxB;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,MAAc,SAAe;AACjC,QAAI,KAAK,UAAW;AAEpB,UAAM,QAAoB;AAAA,MACxB,gBAAgB,KAAK;AAAA,MACrB;AAAA,MACA,QAAQ,SAAS;AAAA,MACjB,YAAY,SAAS,cAAc,CAAC;AAAA,MACpC,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC;AAGA,QAAI,KAAK,YAAY,WAAW;AAC9B,WAAK,KAAK,KAAK,CAAC,KAAK,CAAC;AACtB;AAAA,IACF;AAGA,QAAI,KAAK,MAAM,UAAU,KAAK,SAAU;AAExC,SAAK,MAAM,KAAK,KAAK;AACrB,SAAK,QAAQ;AAEb,QAAI,KAAK,MAAM,UAAU,KAAK,UAAU;AACtC,WAAK,KAAK,MAAM;AAAA,IAClB;AAAA,EACF;AAAA,EAEA,MAAM,QAAQ;AACZ,QAAI,KAAK,YAAY,KAAK,UAAW;AACrC,QAAI,CAAC,KAAK,MAAM,OAAQ;AAExB,QAAI,CAAC,KAAK,OAAO,QAAQ,EAAG;AAE5B,QAAI,KAAK,IAAI,IAAI,KAAK,iBAAkB;AAExC,SAAK,WAAW;AAEhB,UAAM,QAAQ,KAAK,MAAM,OAAO,GAAG,KAAK,QAAQ;AAChD,SAAK,QAAQ;AAEb,QAAI;AACF,YAAM,KAAK,KAAK,KAAK;AAErB,WAAK,eAAe;AAAA,IACtB,QAAQ;AACN,WAAK,QAAQ,CAAC,GAAG,OAAO,GAAG,KAAK,KAAK;AACrC,WAAK,QAAQ;AAEb,WAAK;AAEL,UAAI,KAAK,gBAAgB,GAAG;AAC1B,aAAK,mBAAmB,KAAK,IAAI,IAAI;AAAA,MACvC;AAAA,IACF,UAAE;AACA,WAAK,WAAW;AAAA,IAClB;AAAA,EACF;AAAA,EAEA,UAAU;AACR,SAAK,YAAY;AACjB,QAAI,KAAK,MAAO,eAAc,KAAK,KAAK;AAAA,EAC1C;AAAA;AAAA,EAGA,MAAc,KAAK,QAAsB;AACvC,UAAM,UAAU,KAAK,UAAU;AAAA,MAC7B,SAAQ,oBAAI,KAAK,GAAE,YAAY;AAAA,MAC/B,KAAK,KAAK;AAAA,MACV;AAAA,IACF,CAAC;AAGD,QACE,KAAK,YAAY,aACjB,OAAO,cAAc,eACrB,UAAU,cACV,QAAQ,SAAS,OACjB,SAAS,oBAAoB,UAC7B;AACA,gBAAU,WAAW,KAAK,UAAU,OAAO;AAC3C;AAAA,IACF;AAEA,QAAI,UAAU;AAEd,WAAO,WAAW,KAAK,SAAS;AAC9B,UAAI;AACF,cAAM,aAAa,IAAI,gBAAgB;AACvC,cAAM,UAAU,WAAW,MAAM,WAAW,MAAM,GAAG,GAAI;AAEzD,cAAM,MAAM,MAAM,KAAK,MAAM,KAAK,UAAU;AAAA,UAC1C,QAAQ;AAAA,UACR,SAAS;AAAA,YACP,gBAAgB;AAAA,YAChB,aAAa,KAAK;AAAA,UACpB;AAAA,UACA,MAAM;AAAA,UACN,QAAQ,WAAW;AAAA,QACrB,CAAC;AAED,qBAAa,OAAO;AAEpB,YAAI,IAAI,UAAU,OAAO,IAAI,SAAS,IAAK;AAE3C,YAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM;AAE7B;AAAA,MACF,QAAQ;AACN;AAEA,YAAI,UAAU,KAAK,QAAS,OAAM,IAAI,MAAM;AAE5C,cAAM,MAAM,KAAK,aAAa,KAAK,OAAO;AAAA,MAC5C;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGQ,aAAa;AACnB,SAAK,QAAQ,YAAY,MAAM;AAC7B,WAAK,KAAK,MAAM;AAAA,IAClB,GAAG,KAAK,aAAa;AAAA,EACvB;AAAA,EAEQ,mBAAmB;AACzB,WAAO,iBAAiB,oBAAoB,MAAM;AAChD,UAAI,SAAS,oBAAoB,UAAU;AACzC,aAAK,KAAK,MAAM;AAAA,MAClB;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEQ,UAAU;AAChB,QAAI,KAAK,YAAY,UAAW;AAChC,SAAK,QAAQ,IAAI,iBAAiB,KAAK,KAAK;AAAA,EAC9C;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/client.ts"],"sourcesContent":["export * from \"./client\";\nexport * from \"./types\";\n","import { TrackerOptions, TrackEvent, SdkInfo } from \"./types\";\n\n// ================= RUNTIME =================\ntype Runtime = \"browser\" | \"node\" | \"edge\" | \"serverless\" | \"unknown\";\n\nfunction detectRuntime(): Runtime {\n const g = globalThis as any;\n\n if (typeof window !== \"undefined\") return \"browser\";\n if (g.EdgeRuntime) return \"edge\";\n if (g.process?.env?.AWS_LAMBDA_FUNCTION_NAME) return \"serverless\";\n if (g.process?.versions?.node) return \"node\";\n\n return \"unknown\";\n}\n\n// ================= HELPERS =================\nfunction uuid(): string {\n if (typeof crypto !== \"undefined\" && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n return `${Date.now()}-${Math.random().toString(16).slice(2)}`;\n}\n\nfunction sleep(ms: number) {\n return new Promise((r) => setTimeout(r, ms));\n}\n\n// ================= STORAGE =================\nclass Storage {\n private enabled = false;\n\n constructor() {\n try {\n if (typeof localStorage !== \"undefined\") {\n localStorage.setItem(\"__t\", \"1\");\n localStorage.removeItem(\"__t\");\n this.enabled = true;\n }\n } catch {}\n }\n\n get(key: string): any {\n if (!this.enabled) return null;\n try {\n return JSON.parse(localStorage.getItem(key) || \"null\");\n } catch {\n return null;\n }\n }\n\n set(key: string, value: any) {\n if (!this.enabled) return;\n try {\n localStorage.setItem(key, JSON.stringify(value));\n } catch {}\n }\n}\n\n// ================= LEADER =================\nclass Leader {\n private channel?: BroadcastChannel;\n private isLeader = true;\n\n constructor(enabled: boolean) {\n if (!enabled) return;\n\n if (typeof BroadcastChannel !== \"undefined\") {\n this.channel = new BroadcastChannel(\"eventra\");\n\n this.channel.onmessage = (e) => {\n if (e.data === \"leader\") this.isLeader = false;\n };\n\n this.channel.postMessage(\"leader\");\n }\n }\n\n canSend() {\n return this.isLeader;\n }\n\n destroy() {\n this.channel?.close();\n }\n}\n\n// ================= SDK =================\nexport class Eventra {\n private apiKey: string;\n private endpoint: string;\n private runtime: Runtime;\n private fetch: typeof fetch;\n\n private queue: TrackEvent[] = [];\n private inFlight = false;\n private destroyed = false;\n\n private maxBatch: number;\n private maxQueue: number;\n private flushInterval: number;\n private retries: number;\n private retryDelay: number;\n\n private timer?: any;\n\n private storage = new Storage();\n private leader: Leader | null = null;\n\n private failureCount = 0;\n private circuitOpenUntil = 0;\n\n private sdkInfo: SdkInfo;\n private options: TrackerOptions;\n\n private exitHandlers: Array<() => void> = [];\n\n constructor(options: TrackerOptions) {\n if (!options.apiKey) throw new Error(\"apiKey required\");\n if (!options.endpoint) throw new Error(\"endpoint required\");\n\n this.options = options;\n this.apiKey = options.apiKey;\n this.endpoint = options.endpoint;\n this.runtime = detectRuntime();\n\n this.fetch = options.fetchImpl ?? globalThis.fetch;\n if (!this.fetch) {\n throw new Error(\"fetch not available — provide fetchImpl\");\n }\n\n this.maxBatch = options.maxBatchSize ?? 50;\n this.maxQueue = options.maxQueueSize ?? 10000;\n this.flushInterval = options.flushInterval ?? 2000;\n this.retries = options.maxRetries ?? 3;\n this.retryDelay = options.retryBaseDelayMs ?? 300;\n\n this.sdkInfo = {\n name: \"@eventra_dev/eventra-sdk\",\n version: \"1.0.0\",\n runtime: this.runtime,\n };\n\n // restore browser queue\n if (this.runtime === \"browser\") {\n const saved = this.storage.get(\"__eventra_q__\");\n if (Array.isArray(saved)) this.queue = saved;\n }\n\n // leader (browser only)\n if (this.runtime === \"browser\" && options.multiTabMode === \"leader\") {\n this.leader = new Leader(true);\n }\n\n // timer\n if (!options.disableTimer) {\n this.startTimer();\n }\n\n // browser exit\n if (this.runtime === \"browser\") {\n this.setupBrowserExit();\n }\n\n // node / serverless exit\n if (options.autoFlushOnExit !== false && this.runtime !== \"browser\") {\n this.setupProcessExit();\n }\n }\n\n // ================= PUBLIC =================\n track(name: string, options?: any) {\n if (this.destroyed) return;\n\n if (this.queue.length >= this.maxQueue) {\n this.options.onEventsDropped?.(1);\n return;\n }\n\n const event: TrackEvent = {\n idempotencyKey: uuid(),\n name,\n userId: options?.userId,\n properties: options?.properties ?? {},\n timestamp: new Date().toISOString(),\n };\n\n this.queue.push(event);\n\n if (this.runtime === \"browser\") {\n this.persist();\n }\n\n if (this.queue.length >= this.maxBatch) {\n void this.flush();\n }\n\n if (this.runtime === \"serverless\") {\n void this.flush();\n }\n }\n\n async flush() {\n if (this.inFlight || this.destroyed) return;\n if (!this.queue.length) return;\n\n if (this.leader && !this.leader.canSend()) return;\n if (Date.now() < this.circuitOpenUntil) return;\n\n this.inFlight = true;\n\n const batch = this.queue.splice(0, this.maxBatch);\n\n if (this.runtime === \"browser\") {\n this.persist();\n }\n\n try {\n await this.send(batch);\n this.failureCount = 0;\n } catch {\n // возвращаем батч назад (без dedupe — это задача backend)\n this.queue = [...batch, ...this.queue];\n\n if (this.runtime === \"browser\") {\n this.persist();\n }\n\n this.failureCount++;\n\n if (this.failureCount >= 5) {\n this.circuitOpenUntil = Date.now() + 5000;\n }\n } finally {\n this.inFlight = false;\n }\n }\n\n destroy() {\n this.destroyed = true;\n\n if (this.timer) clearInterval(this.timer);\n\n this.leader?.destroy();\n\n // remove process listeners\n for (const off of this.exitHandlers) {\n off();\n }\n }\n\n // ================= SEND =================\n private async send(events: TrackEvent[]) {\n const payload = JSON.stringify({\n sentAt: new Date().toISOString(),\n sdk: this.sdkInfo,\n events,\n });\n\n if (\n this.runtime === \"browser\" &&\n typeof navigator !== \"undefined\" &&\n navigator.sendBeacon &&\n payload.length < 60000 &&\n document.visibilityState === \"hidden\"\n ) {\n navigator.sendBeacon(this.endpoint, payload);\n return;\n }\n\n let attempt = 0;\n\n while (attempt <= this.retries) {\n try {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 5000);\n\n const res = await this.fetch(this.endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": this.apiKey,\n },\n body: payload,\n signal: controller.signal,\n });\n\n clearTimeout(timeout);\n\n if (res.status >= 400 && res.status < 500) return;\n if (!res.ok) throw new Error();\n\n return;\n } catch {\n attempt++;\n if (attempt > this.retries) throw new Error();\n await sleep(this.retryDelay * 2 ** attempt);\n }\n }\n }\n\n // ================= INTERNAL =================\n private startTimer() {\n this.timer = setInterval(() => {\n void this.flush();\n }, this.flushInterval);\n }\n\n private setupBrowserExit() {\n const handler = () => {\n if (document.visibilityState === \"hidden\") {\n void this.flush();\n }\n };\n\n window.addEventListener(\"visibilitychange\", handler);\n\n this.exitHandlers.push(() => {\n window.removeEventListener(\"visibilitychange\", handler);\n });\n }\n\n private setupProcessExit() {\n\n const g = globalThis as any;\n\n const flushSafe = async () => {\n try {\n await this.flush();\n } catch {}\n };\n\n const beforeExit = () => {\n void flushSafe();\n };\n\n const sigint = async () => {\n await flushSafe();\n g.exit();\n };\n\n g?.on?.(\"beforeExit\", beforeExit);\n g?.on?.(\"SIGINT\", sigint);\n\n this.exitHandlers.push(() => {\n g?.off?.(\"beforeExit\", beforeExit);\n g?.off?.(\"SIGINT\", sigint);\n });\n }\n\n private persist() {\n if (this.runtime !== \"browser\") return;\n this.storage.set(\"__eventra_q__\", this.queue);\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -46,6 +46,8 @@ declare class Eventra {
46
46
  private failureCount;
47
47
  private circuitOpenUntil;
48
48
  private sdkInfo;
49
+ private options;
50
+ private exitHandlers;
49
51
  constructor(options: TrackerOptions);
50
52
  track(name: string, options?: any): void;
51
53
  flush(): Promise<void>;
@@ -53,6 +55,7 @@ declare class Eventra {
53
55
  private send;
54
56
  private startTimer;
55
57
  private setupBrowserExit;
58
+ private setupProcessExit;
56
59
  private persist;
57
60
  }
58
61
 
package/dist/index.d.ts CHANGED
@@ -46,6 +46,8 @@ declare class Eventra {
46
46
  private failureCount;
47
47
  private circuitOpenUntil;
48
48
  private sdkInfo;
49
+ private options;
50
+ private exitHandlers;
49
51
  constructor(options: TrackerOptions);
50
52
  track(name: string, options?: any): void;
51
53
  flush(): Promise<void>;
@@ -53,6 +55,7 @@ declare class Eventra {
53
55
  private send;
54
56
  private startTimer;
55
57
  private setupBrowserExit;
58
+ private setupProcessExit;
56
59
  private persist;
57
60
  }
58
61
 
package/dist/index.mjs CHANGED
@@ -16,10 +16,6 @@ function uuid() {
16
16
  function sleep(ms) {
17
17
  return new Promise((r) => setTimeout(r, ms));
18
18
  }
19
- function getFetch() {
20
- if (typeof fetch !== "undefined") return fetch.bind(globalThis);
21
- throw new Error("fetch not available");
22
- }
23
19
  var Storage = class {
24
20
  constructor() {
25
21
  this.enabled = false;
@@ -49,14 +45,13 @@ var Storage = class {
49
45
  }
50
46
  };
51
47
  var Leader = class {
52
- constructor() {
48
+ constructor(enabled) {
53
49
  this.isLeader = true;
50
+ if (!enabled) return;
54
51
  if (typeof BroadcastChannel !== "undefined") {
55
52
  this.channel = new BroadcastChannel("eventra");
56
53
  this.channel.onmessage = (e) => {
57
- if (e.data === "leader") {
58
- this.isLeader = false;
59
- }
54
+ if (e.data === "leader") this.isLeader = false;
60
55
  };
61
56
  this.channel.postMessage("leader");
62
57
  }
@@ -64,45 +59,64 @@ var Leader = class {
64
59
  canSend() {
65
60
  return this.isLeader;
66
61
  }
62
+ destroy() {
63
+ this.channel?.close();
64
+ }
67
65
  };
68
66
  var Eventra = class {
69
67
  constructor(options) {
70
- this.fetch = getFetch();
71
68
  this.queue = [];
72
69
  this.inFlight = false;
73
70
  this.destroyed = false;
74
- this.maxBatch = 50;
75
- this.maxQueue = 1e4;
76
- this.flushInterval = 2e3;
77
- this.retries = 3;
78
- this.retryDelay = 300;
79
71
  this.storage = new Storage();
80
- this.leader = new Leader();
72
+ this.leader = null;
81
73
  this.failureCount = 0;
82
74
  this.circuitOpenUntil = 0;
75
+ this.exitHandlers = [];
83
76
  if (!options.apiKey) throw new Error("apiKey required");
77
+ if (!options.endpoint) throw new Error("endpoint required");
78
+ this.options = options;
84
79
  this.apiKey = options.apiKey;
85
- this.endpoint = options.endpoint ?? "";
86
- if (!this.endpoint) throw new Error("endpoint required");
80
+ this.endpoint = options.endpoint;
87
81
  this.runtime = detectRuntime();
88
- this.maxBatch = options.maxBatchSize ?? this.maxBatch;
89
- this.maxQueue = options.maxQueueSize ?? this.maxQueue;
90
- this.flushInterval = options.flushInterval ?? this.flushInterval;
82
+ this.fetch = options.fetchImpl ?? globalThis.fetch;
83
+ if (!this.fetch) {
84
+ throw new Error("fetch not available \u2014 provide fetchImpl");
85
+ }
86
+ this.maxBatch = options.maxBatchSize ?? 50;
87
+ this.maxQueue = options.maxQueueSize ?? 1e4;
88
+ this.flushInterval = options.flushInterval ?? 2e3;
89
+ this.retries = options.maxRetries ?? 3;
90
+ this.retryDelay = options.retryBaseDelayMs ?? 300;
91
91
  this.sdkInfo = {
92
92
  name: "@eventra_dev/eventra-sdk",
93
- version: "ultra",
93
+ version: "1.0.0",
94
94
  runtime: this.runtime
95
95
  };
96
96
  if (this.runtime === "browser") {
97
97
  const saved = this.storage.get("__eventra_q__");
98
- if (saved) this.queue = saved;
98
+ if (Array.isArray(saved)) this.queue = saved;
99
+ }
100
+ if (this.runtime === "browser" && options.multiTabMode === "leader") {
101
+ this.leader = new Leader(true);
102
+ }
103
+ if (!options.disableTimer) {
99
104
  this.startTimer();
105
+ }
106
+ if (this.runtime === "browser") {
100
107
  this.setupBrowserExit();
101
108
  }
109
+ if (options.autoFlushOnExit !== false && this.runtime !== "browser") {
110
+ this.setupProcessExit();
111
+ }
102
112
  }
103
- // PUBLIC
113
+ // ================= PUBLIC =================
104
114
  track(name, options) {
105
115
  if (this.destroyed) return;
116
+ if (this.queue.length >= this.maxQueue) {
117
+ this.options.onEventsDropped?.(1);
118
+ return;
119
+ }
106
120
  const event = {
107
121
  idempotencyKey: uuid(),
108
122
  name,
@@ -110,31 +124,35 @@ var Eventra = class {
110
124
  properties: options?.properties ?? {},
111
125
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
112
126
  };
113
- if (this.runtime !== "browser") {
114
- void this.send([event]);
115
- return;
116
- }
117
- if (this.queue.length >= this.maxQueue) return;
118
127
  this.queue.push(event);
119
- this.persist();
128
+ if (this.runtime === "browser") {
129
+ this.persist();
130
+ }
120
131
  if (this.queue.length >= this.maxBatch) {
121
132
  void this.flush();
122
133
  }
134
+ if (this.runtime === "serverless") {
135
+ void this.flush();
136
+ }
123
137
  }
124
138
  async flush() {
125
139
  if (this.inFlight || this.destroyed) return;
126
140
  if (!this.queue.length) return;
127
- if (!this.leader.canSend()) return;
141
+ if (this.leader && !this.leader.canSend()) return;
128
142
  if (Date.now() < this.circuitOpenUntil) return;
129
143
  this.inFlight = true;
130
144
  const batch = this.queue.splice(0, this.maxBatch);
131
- this.persist();
145
+ if (this.runtime === "browser") {
146
+ this.persist();
147
+ }
132
148
  try {
133
149
  await this.send(batch);
134
150
  this.failureCount = 0;
135
151
  } catch {
136
152
  this.queue = [...batch, ...this.queue];
137
- this.persist();
153
+ if (this.runtime === "browser") {
154
+ this.persist();
155
+ }
138
156
  this.failureCount++;
139
157
  if (this.failureCount >= 5) {
140
158
  this.circuitOpenUntil = Date.now() + 5e3;
@@ -146,8 +164,12 @@ var Eventra = class {
146
164
  destroy() {
147
165
  this.destroyed = true;
148
166
  if (this.timer) clearInterval(this.timer);
167
+ this.leader?.destroy();
168
+ for (const off of this.exitHandlers) {
169
+ off();
170
+ }
149
171
  }
150
- // SEND (CORE)
172
+ // ================= SEND =================
151
173
  async send(events) {
152
174
  const payload = JSON.stringify({
153
175
  sentAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -183,17 +205,43 @@ var Eventra = class {
183
205
  }
184
206
  }
185
207
  }
186
- // BROWSER
208
+ // ================= INTERNAL =================
187
209
  startTimer() {
188
210
  this.timer = setInterval(() => {
189
211
  void this.flush();
190
212
  }, this.flushInterval);
191
213
  }
192
214
  setupBrowserExit() {
193
- window.addEventListener("visibilitychange", () => {
215
+ const handler = () => {
194
216
  if (document.visibilityState === "hidden") {
195
217
  void this.flush();
196
218
  }
219
+ };
220
+ window.addEventListener("visibilitychange", handler);
221
+ this.exitHandlers.push(() => {
222
+ window.removeEventListener("visibilitychange", handler);
223
+ });
224
+ }
225
+ setupProcessExit() {
226
+ const g = globalThis;
227
+ const flushSafe = async () => {
228
+ try {
229
+ await this.flush();
230
+ } catch {
231
+ }
232
+ };
233
+ const beforeExit = () => {
234
+ void flushSafe();
235
+ };
236
+ const sigint = async () => {
237
+ await flushSafe();
238
+ g.exit();
239
+ };
240
+ g?.on?.("beforeExit", beforeExit);
241
+ g?.on?.("SIGINT", sigint);
242
+ this.exitHandlers.push(() => {
243
+ g?.off?.("beforeExit", beforeExit);
244
+ g?.off?.("SIGINT", sigint);
197
245
  });
198
246
  }
199
247
  persist() {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts"],"sourcesContent":["import { TrackerOptions, TrackEvent, SdkInfo } from \"./types\";\n\n// RUNTIME DETECTION\ntype Runtime = \"browser\" | \"node\" | \"edge\" | \"serverless\" | \"unknown\";\n\nfunction detectRuntime(): Runtime {\n const g = globalThis as any;\n\n if (typeof window !== \"undefined\") return \"browser\";\n if (g.EdgeRuntime) return \"edge\";\n if (g.process?.env?.AWS_LAMBDA_FUNCTION_NAME) return \"serverless\";\n if (g.process?.versions?.node) return \"node\";\n\n return \"unknown\";\n}\n\n// HELPERS\nfunction uuid(): string {\n if (typeof crypto !== \"undefined\" && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n return `${Date.now()}-${Math.random().toString(16).slice(2)}`;\n}\n\nfunction sleep(ms: number) {\n return new Promise((r) => setTimeout(r, ms));\n}\n\nfunction getFetch(): typeof fetch {\n if (typeof fetch !== \"undefined\") return fetch.bind(globalThis);\n throw new Error(\"fetch not available\");\n}\n\n// SAFE STORAGE (browser only)\nclass Storage {\n private enabled = false;\n\n constructor() {\n try {\n if (typeof localStorage !== \"undefined\") {\n localStorage.setItem(\"__t\", \"1\");\n localStorage.removeItem(\"__t\");\n this.enabled = true;\n }\n } catch {}\n }\n\n get(key: string): any {\n if (!this.enabled) return null;\n try {\n return JSON.parse(localStorage.getItem(key) || \"null\");\n } catch {\n return null;\n }\n }\n\n set(key: string, value: any) {\n if (!this.enabled) return;\n try {\n localStorage.setItem(key, JSON.stringify(value));\n } catch {}\n }\n}\n\n// LEADER ELECTION (browser)\nclass Leader {\n private channel?: BroadcastChannel;\n private isLeader = true;\n\n constructor() {\n if (typeof BroadcastChannel !== \"undefined\") {\n this.channel = new BroadcastChannel(\"eventra\");\n\n this.channel.onmessage = (e) => {\n if (e.data === \"leader\") {\n this.isLeader = false;\n }\n };\n\n this.channel.postMessage(\"leader\");\n }\n }\n\n canSend() {\n return this.isLeader;\n }\n}\n\n// SDK\nexport class Eventra {\n private apiKey: string;\n private endpoint: string;\n private runtime: Runtime;\n private fetch = getFetch();\n\n private queue: TrackEvent[] = [];\n private inFlight = false;\n private destroyed = false;\n\n private maxBatch = 50;\n private maxQueue = 10000;\n private flushInterval = 2000;\n private retries = 3;\n private retryDelay = 300;\n\n private timer?: any;\n\n private storage = new Storage();\n private leader = new Leader();\n\n private failureCount = 0;\n private circuitOpenUntil = 0;\n\n private sdkInfo: SdkInfo;\n\n constructor(options: TrackerOptions) {\n if (!options.apiKey) throw new Error(\"apiKey required\");\n\n this.apiKey = options.apiKey;\n this.endpoint = options.endpoint ?? \"\";\n\n if (!this.endpoint) throw new Error(\"endpoint required\");\n\n this.runtime = detectRuntime();\n\n this.maxBatch = options.maxBatchSize ?? this.maxBatch;\n this.maxQueue = options.maxQueueSize ?? this.maxQueue;\n this.flushInterval = options.flushInterval ?? this.flushInterval;\n\n this.sdkInfo = {\n name: \"@eventra_dev/eventra-sdk\",\n version: \"ultra\",\n runtime: this.runtime,\n };\n\n if (this.runtime === \"browser\") {\n const saved = this.storage.get(\"__eventra_q__\");\n if (saved) this.queue = saved;\n\n this.startTimer();\n this.setupBrowserExit();\n }\n }\n\n // PUBLIC\n track(name: string, options?: any) {\n if (this.destroyed) return;\n\n const event: TrackEvent = {\n idempotencyKey: uuid(),\n name,\n userId: options?.userId,\n properties: options?.properties ?? {},\n timestamp: new Date().toISOString(),\n };\n\n // server environments → instant send\n if (this.runtime !== \"browser\") {\n void this.send([event]);\n return;\n }\n\n // browser → queue\n if (this.queue.length >= this.maxQueue) return;\n\n this.queue.push(event);\n this.persist();\n\n if (this.queue.length >= this.maxBatch) {\n void this.flush();\n }\n }\n\n async flush() {\n if (this.inFlight || this.destroyed) return;\n if (!this.queue.length) return;\n\n if (!this.leader.canSend()) return;\n\n if (Date.now() < this.circuitOpenUntil) return;\n\n this.inFlight = true;\n\n const batch = this.queue.splice(0, this.maxBatch);\n this.persist();\n\n try {\n await this.send(batch);\n\n this.failureCount = 0;\n } catch {\n this.queue = [...batch, ...this.queue];\n this.persist();\n\n this.failureCount++;\n\n if (this.failureCount >= 5) {\n this.circuitOpenUntil = Date.now() + 5000;\n }\n } finally {\n this.inFlight = false;\n }\n }\n\n destroy() {\n this.destroyed = true;\n if (this.timer) clearInterval(this.timer);\n }\n\n // SEND (CORE)\n private async send(events: TrackEvent[]) {\n const payload = JSON.stringify({\n sentAt: new Date().toISOString(),\n sdk: this.sdkInfo,\n events,\n });\n\n // sendBeacon fast path\n if (\n this.runtime === \"browser\" &&\n typeof navigator !== \"undefined\" &&\n navigator.sendBeacon &&\n payload.length < 60000 &&\n document.visibilityState === \"hidden\"\n ) {\n navigator.sendBeacon(this.endpoint, payload);\n return;\n }\n\n let attempt = 0;\n\n while (attempt <= this.retries) {\n try {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 5000);\n\n const res = await this.fetch(this.endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": this.apiKey,\n },\n body: payload,\n signal: controller.signal,\n });\n\n clearTimeout(timeout);\n\n if (res.status >= 400 && res.status < 500) return;\n\n if (!res.ok) throw new Error();\n\n return;\n } catch {\n attempt++;\n\n if (attempt > this.retries) throw new Error();\n\n await sleep(this.retryDelay * 2 ** attempt);\n }\n }\n }\n\n // BROWSER\n private startTimer() {\n this.timer = setInterval(() => {\n void this.flush();\n }, this.flushInterval);\n }\n\n private setupBrowserExit() {\n window.addEventListener(\"visibilitychange\", () => {\n if (document.visibilityState === \"hidden\") {\n void this.flush();\n }\n });\n }\n\n private persist() {\n if (this.runtime !== \"browser\") return;\n this.storage.set(\"__eventra_q__\", this.queue);\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/client.ts"],"sourcesContent":["import { TrackerOptions, TrackEvent, SdkInfo } from \"./types\";\n\n// ================= RUNTIME =================\ntype Runtime = \"browser\" | \"node\" | \"edge\" | \"serverless\" | \"unknown\";\n\nfunction detectRuntime(): Runtime {\n const g = globalThis as any;\n\n if (typeof window !== \"undefined\") return \"browser\";\n if (g.EdgeRuntime) return \"edge\";\n if (g.process?.env?.AWS_LAMBDA_FUNCTION_NAME) return \"serverless\";\n if (g.process?.versions?.node) return \"node\";\n\n return \"unknown\";\n}\n\n// ================= HELPERS =================\nfunction uuid(): string {\n if (typeof crypto !== \"undefined\" && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n return `${Date.now()}-${Math.random().toString(16).slice(2)}`;\n}\n\nfunction sleep(ms: number) {\n return new Promise((r) => setTimeout(r, ms));\n}\n\n// ================= STORAGE =================\nclass Storage {\n private enabled = false;\n\n constructor() {\n try {\n if (typeof localStorage !== \"undefined\") {\n localStorage.setItem(\"__t\", \"1\");\n localStorage.removeItem(\"__t\");\n this.enabled = true;\n }\n } catch {}\n }\n\n get(key: string): any {\n if (!this.enabled) return null;\n try {\n return JSON.parse(localStorage.getItem(key) || \"null\");\n } catch {\n return null;\n }\n }\n\n set(key: string, value: any) {\n if (!this.enabled) return;\n try {\n localStorage.setItem(key, JSON.stringify(value));\n } catch {}\n }\n}\n\n// ================= LEADER =================\nclass Leader {\n private channel?: BroadcastChannel;\n private isLeader = true;\n\n constructor(enabled: boolean) {\n if (!enabled) return;\n\n if (typeof BroadcastChannel !== \"undefined\") {\n this.channel = new BroadcastChannel(\"eventra\");\n\n this.channel.onmessage = (e) => {\n if (e.data === \"leader\") this.isLeader = false;\n };\n\n this.channel.postMessage(\"leader\");\n }\n }\n\n canSend() {\n return this.isLeader;\n }\n\n destroy() {\n this.channel?.close();\n }\n}\n\n// ================= SDK =================\nexport class Eventra {\n private apiKey: string;\n private endpoint: string;\n private runtime: Runtime;\n private fetch: typeof fetch;\n\n private queue: TrackEvent[] = [];\n private inFlight = false;\n private destroyed = false;\n\n private maxBatch: number;\n private maxQueue: number;\n private flushInterval: number;\n private retries: number;\n private retryDelay: number;\n\n private timer?: any;\n\n private storage = new Storage();\n private leader: Leader | null = null;\n\n private failureCount = 0;\n private circuitOpenUntil = 0;\n\n private sdkInfo: SdkInfo;\n private options: TrackerOptions;\n\n private exitHandlers: Array<() => void> = [];\n\n constructor(options: TrackerOptions) {\n if (!options.apiKey) throw new Error(\"apiKey required\");\n if (!options.endpoint) throw new Error(\"endpoint required\");\n\n this.options = options;\n this.apiKey = options.apiKey;\n this.endpoint = options.endpoint;\n this.runtime = detectRuntime();\n\n this.fetch = options.fetchImpl ?? globalThis.fetch;\n if (!this.fetch) {\n throw new Error(\"fetch not available — provide fetchImpl\");\n }\n\n this.maxBatch = options.maxBatchSize ?? 50;\n this.maxQueue = options.maxQueueSize ?? 10000;\n this.flushInterval = options.flushInterval ?? 2000;\n this.retries = options.maxRetries ?? 3;\n this.retryDelay = options.retryBaseDelayMs ?? 300;\n\n this.sdkInfo = {\n name: \"@eventra_dev/eventra-sdk\",\n version: \"1.0.0\",\n runtime: this.runtime,\n };\n\n // restore browser queue\n if (this.runtime === \"browser\") {\n const saved = this.storage.get(\"__eventra_q__\");\n if (Array.isArray(saved)) this.queue = saved;\n }\n\n // leader (browser only)\n if (this.runtime === \"browser\" && options.multiTabMode === \"leader\") {\n this.leader = new Leader(true);\n }\n\n // timer\n if (!options.disableTimer) {\n this.startTimer();\n }\n\n // browser exit\n if (this.runtime === \"browser\") {\n this.setupBrowserExit();\n }\n\n // node / serverless exit\n if (options.autoFlushOnExit !== false && this.runtime !== \"browser\") {\n this.setupProcessExit();\n }\n }\n\n // ================= PUBLIC =================\n track(name: string, options?: any) {\n if (this.destroyed) return;\n\n if (this.queue.length >= this.maxQueue) {\n this.options.onEventsDropped?.(1);\n return;\n }\n\n const event: TrackEvent = {\n idempotencyKey: uuid(),\n name,\n userId: options?.userId,\n properties: options?.properties ?? {},\n timestamp: new Date().toISOString(),\n };\n\n this.queue.push(event);\n\n if (this.runtime === \"browser\") {\n this.persist();\n }\n\n if (this.queue.length >= this.maxBatch) {\n void this.flush();\n }\n\n if (this.runtime === \"serverless\") {\n void this.flush();\n }\n }\n\n async flush() {\n if (this.inFlight || this.destroyed) return;\n if (!this.queue.length) return;\n\n if (this.leader && !this.leader.canSend()) return;\n if (Date.now() < this.circuitOpenUntil) return;\n\n this.inFlight = true;\n\n const batch = this.queue.splice(0, this.maxBatch);\n\n if (this.runtime === \"browser\") {\n this.persist();\n }\n\n try {\n await this.send(batch);\n this.failureCount = 0;\n } catch {\n // возвращаем батч назад (без dedupe — это задача backend)\n this.queue = [...batch, ...this.queue];\n\n if (this.runtime === \"browser\") {\n this.persist();\n }\n\n this.failureCount++;\n\n if (this.failureCount >= 5) {\n this.circuitOpenUntil = Date.now() + 5000;\n }\n } finally {\n this.inFlight = false;\n }\n }\n\n destroy() {\n this.destroyed = true;\n\n if (this.timer) clearInterval(this.timer);\n\n this.leader?.destroy();\n\n // remove process listeners\n for (const off of this.exitHandlers) {\n off();\n }\n }\n\n // ================= SEND =================\n private async send(events: TrackEvent[]) {\n const payload = JSON.stringify({\n sentAt: new Date().toISOString(),\n sdk: this.sdkInfo,\n events,\n });\n\n if (\n this.runtime === \"browser\" &&\n typeof navigator !== \"undefined\" &&\n navigator.sendBeacon &&\n payload.length < 60000 &&\n document.visibilityState === \"hidden\"\n ) {\n navigator.sendBeacon(this.endpoint, payload);\n return;\n }\n\n let attempt = 0;\n\n while (attempt <= this.retries) {\n try {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 5000);\n\n const res = await this.fetch(this.endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": this.apiKey,\n },\n body: payload,\n signal: controller.signal,\n });\n\n clearTimeout(timeout);\n\n if (res.status >= 400 && res.status < 500) return;\n if (!res.ok) throw new Error();\n\n return;\n } catch {\n attempt++;\n if (attempt > this.retries) throw new Error();\n await sleep(this.retryDelay * 2 ** attempt);\n }\n }\n }\n\n // ================= INTERNAL =================\n private startTimer() {\n this.timer = setInterval(() => {\n void this.flush();\n }, this.flushInterval);\n }\n\n private setupBrowserExit() {\n const handler = () => {\n if (document.visibilityState === \"hidden\") {\n void this.flush();\n }\n };\n\n window.addEventListener(\"visibilitychange\", handler);\n\n this.exitHandlers.push(() => {\n window.removeEventListener(\"visibilitychange\", handler);\n });\n }\n\n private setupProcessExit() {\n\n const g = globalThis as any;\n\n const flushSafe = async () => {\n try {\n await this.flush();\n } catch {}\n };\n\n const beforeExit = () => {\n void flushSafe();\n };\n\n const sigint = async () => {\n await flushSafe();\n g.exit();\n };\n\n g?.on?.(\"beforeExit\", beforeExit);\n g?.on?.(\"SIGINT\", sigint);\n\n this.exitHandlers.push(() => {\n g?.off?.(\"beforeExit\", beforeExit);\n g?.off?.(\"SIGINT\", sigint);\n });\n }\n\n private persist() {\n if (this.runtime !== \"browser\") return;\n this.storage.set(\"__eventra_q__\", this.queue);\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventra_dev/eventra-sdk",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"