@eventra_dev/eventra-sdk 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,197 @@
1
+ # Eventra SDK
2
+
3
+ ![npm](https://img.shields.io/npm/v/@eventra/sdk)
4
+ ![bundle size](https://img.shields.io/bundlephobia/minzip/@eventra/sdk)
5
+ ![license](https://img.shields.io/npm/l/@eventra/sdk)
6
+ ![types](https://img.shields.io/npm/types/@eventra/sdk)
7
+
8
+ Production-grade TypeScript SDK for tracking product events across **browser, server, and edge runtimes**.
9
+
10
+ Eventra is designed for **deterministic ingestion**, batching efficiency, and near-zero runtime overhead.
11
+
12
+ ---
13
+
14
+ ## Features
15
+
16
+ - UUID v4 idempotency keys
17
+ - Safe retry policy
18
+ - Exponential backoff with jitter
19
+ - sendBeacon support for browsers
20
+ - Multi-runtime support (Browser / Node / Bun / Deno / Edge)
21
+ - Optional multi-tab leader mode
22
+ - Queue overflow protection
23
+ - Zero dependencies
24
+
25
+ ---
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pnpm add @eventra/sdk
31
+ ```
32
+
33
+ or
34
+
35
+ ```bash
36
+ npm install @eventra/sdk
37
+ ```
38
+
39
+ ---
40
+
41
+ # Quick Start
42
+
43
+ ## Browser
44
+
45
+ ```ts
46
+ import { Eventra } from "@eventra/sdk";
47
+
48
+ const tracker = new Eventra({
49
+ apiKey: "YOUR_API_KEY"
50
+ });
51
+
52
+ tracker.track("checkout.completed", {
53
+ userId: "user_123"
54
+ });
55
+ ```
56
+
57
+ ---
58
+
59
+ ## Node / NestJS
60
+
61
+ ```ts
62
+ import { Eventra } from "@eventra/sdk";
63
+
64
+ const tracker = new Eventra({
65
+ apiKey: process.env.EVENTRA_API_KEY!
66
+ });
67
+
68
+ tracker.track("invoice.created", {
69
+ userId: "user_123"
70
+ });
71
+ ```
72
+
73
+ ---
74
+
75
+ # Configuration
76
+
77
+ | Option | Default |
78
+ |------|------|
79
+ | flushInterval | 2000 ms |
80
+ | maxBatchSize | 50 |
81
+ | maxRetries | 3 |
82
+ | maxQueueSize | 10000 |
83
+ | retryBaseDelayMs | 300 |
84
+
85
+ Example:
86
+
87
+ ```ts
88
+ const tracker = new Eventra({
89
+ apiKey: "API_KEY",
90
+ flushInterval: 2000,
91
+ maxBatchSize: 50
92
+ });
93
+ ```
94
+
95
+ ---
96
+
97
+ # Reliability Model
98
+
99
+ ### Idempotency
100
+
101
+ Each event receives a **UUID v4 idempotency key**, ensuring:
102
+
103
+ - safe retries
104
+ - duplicate suppression
105
+ - multi-tab safety
106
+
107
+ ---
108
+
109
+ ### Retry Policy
110
+
111
+ The SDK retries only when safe:
112
+
113
+ ✔ network failures
114
+ ✔ HTTP 5xx
115
+
116
+ It does **not retry**:
117
+
118
+ ✖ HTTP 4xx
119
+ ✖ validation errors
120
+ ✖ authentication errors
121
+
122
+ ---
123
+
124
+ ### Backoff Strategy
125
+
126
+ ```
127
+ delay = base * 2^attempt * jitter(0.5–1.5)
128
+ ```
129
+
130
+ ---
131
+
132
+ # Multi-Tab Mode
133
+
134
+ Default:
135
+
136
+ ```ts
137
+ multiTabMode: "independent"
138
+ ```
139
+
140
+ Leader mode:
141
+
142
+ ```ts
143
+ const tracker = new Eventra({
144
+ apiKey: "API_KEY",
145
+ multiTabMode: "leader"
146
+ });
147
+ ```
148
+
149
+ One tab becomes the sender and reduces duplicate traffic.
150
+
151
+ ---
152
+
153
+ # Architecture
154
+
155
+ ```
156
+ Application
157
+
158
+
159
+ Eventra SDK
160
+
161
+ batching + retry
162
+
163
+
164
+ Eventra Ingest API
165
+
166
+ idempotent pipeline
167
+
168
+
169
+ Eventra Event Store
170
+ ```
171
+
172
+ ---
173
+
174
+ # Best Practices
175
+
176
+ Use semantic event names.
177
+
178
+ Good:
179
+
180
+ ```
181
+ invoice.created
182
+ checkout.completed
183
+ team.invited
184
+ ```
185
+
186
+ Bad:
187
+
188
+ ```
189
+ button_clicked
190
+ modal_opened
191
+ ```
192
+
193
+ ---
194
+
195
+ # License
196
+
197
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,262 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ Eventra: () => Eventra
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/client.ts
28
+ var SDK_VERSION = "0.1.2";
29
+ var DEFAULTS = {
30
+ endpoint: "http://api.eventra.dev/api/v1/ingest/batch",
31
+ flushInterval: 2e3,
32
+ maxBatchSize: 50,
33
+ maxQueueSize: 1e4,
34
+ maxRetries: 3,
35
+ retryBaseDelay: 300
36
+ };
37
+ function getGlobalFetch() {
38
+ if (typeof fetch !== "undefined") {
39
+ return fetch.bind(globalThis);
40
+ }
41
+ return void 0;
42
+ }
43
+ function generateUUIDv4() {
44
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
45
+ return crypto.randomUUID();
46
+ }
47
+ const rnd = Math.random().toString(16).slice(2);
48
+ const ts = Date.now().toString(16);
49
+ return `${ts}-${rnd}`;
50
+ }
51
+ function isBrowser() {
52
+ return typeof window !== "undefined";
53
+ }
54
+ function sleep(ms) {
55
+ return new Promise((r) => setTimeout(r, ms));
56
+ }
57
+ var LEADER_KEY = "__eventra_sdk_leader__";
58
+ var LEADER_TTL = 4e3;
59
+ function tryBecomeLeader() {
60
+ if (!isBrowser()) return true;
61
+ try {
62
+ const now = Date.now();
63
+ const raw = localStorage.getItem(LEADER_KEY);
64
+ if (!raw) {
65
+ localStorage.setItem(
66
+ LEADER_KEY,
67
+ JSON.stringify({ ts: now })
68
+ );
69
+ return true;
70
+ }
71
+ const parsed = JSON.parse(raw);
72
+ if (now - parsed.ts > LEADER_TTL) {
73
+ localStorage.setItem(
74
+ LEADER_KEY,
75
+ JSON.stringify({ ts: now })
76
+ );
77
+ return true;
78
+ }
79
+ return false;
80
+ } catch {
81
+ return true;
82
+ }
83
+ }
84
+ var Eventra = class {
85
+ constructor(options) {
86
+ this.queue = [];
87
+ this.inFlight = false;
88
+ this.destroyed = false;
89
+ this.droppedEvents = 0;
90
+ if (!options.apiKey) {
91
+ throw new Error("Eventra: apiKey is required");
92
+ }
93
+ this.apiKey = options.apiKey;
94
+ this.endpoint = options.endpoint ?? DEFAULTS.endpoint ?? "";
95
+ if (!this.endpoint) {
96
+ throw new Error("Eventra: endpoint is not configured");
97
+ }
98
+ this.flushInterval = options.flushInterval ?? DEFAULTS.flushInterval;
99
+ this.maxBatchSize = options.maxBatchSize ?? DEFAULTS.maxBatchSize;
100
+ this.maxQueueSize = options.maxQueueSize ?? DEFAULTS.maxQueueSize;
101
+ this.maxRetries = options.maxRetries ?? DEFAULTS.maxRetries;
102
+ this.retryBaseDelay = options.retryBaseDelayMs ?? DEFAULTS.retryBaseDelay;
103
+ this.multiTabMode = options.multiTabMode ?? "independent";
104
+ this.fetchImpl = options.fetchImpl ?? getGlobalFetch();
105
+ if (!this.fetchImpl) {
106
+ throw new Error(
107
+ "Eventra: fetch is not available. Provide fetchImpl."
108
+ );
109
+ }
110
+ this.sdkInfo = {
111
+ name: "@eventra/sdk",
112
+ version: SDK_VERSION,
113
+ runtime: this.detectRuntime()
114
+ };
115
+ if (!options.disableTimer) {
116
+ this.startTimer();
117
+ }
118
+ if (options.autoFlushOnExit !== false) {
119
+ this.setupExitHandlers();
120
+ }
121
+ this.onEventsDropped = options.onEventsDropped;
122
+ }
123
+ /* ---------------- Public API ---------------- */
124
+ track(name, options) {
125
+ if (this.destroyed) return;
126
+ if (this.queue.length >= this.maxQueueSize) {
127
+ this.droppedEvents++;
128
+ this.onEventsDropped?.(this.droppedEvents);
129
+ return;
130
+ }
131
+ this.queue.push({
132
+ idempotencyKey: generateUUIDv4(),
133
+ name,
134
+ userId: options?.userId,
135
+ properties: options?.properties ?? {},
136
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
137
+ });
138
+ if (this.queue.length >= this.maxBatchSize) {
139
+ void this.flush();
140
+ }
141
+ }
142
+ async flush() {
143
+ if (this.destroyed) return;
144
+ if (this.inFlight) return;
145
+ if (this.queue.length === 0) return;
146
+ if (this.multiTabMode === "leader" && !tryBecomeLeader()) {
147
+ return;
148
+ }
149
+ this.inFlight = true;
150
+ const batch = this.queue.splice(0, this.queue.length);
151
+ try {
152
+ await this.send(batch);
153
+ } catch {
154
+ this.queue.unshift(...batch);
155
+ } finally {
156
+ this.inFlight = false;
157
+ }
158
+ }
159
+ destroy() {
160
+ this.destroyed = true;
161
+ if (this.timer) clearInterval(this.timer);
162
+ }
163
+ /* ---------------- Transport ---------------- */
164
+ trySendBeacon(payload) {
165
+ if (!isBrowser()) return false;
166
+ try {
167
+ if (navigator.sendBeacon) {
168
+ const blob = new Blob([payload], {
169
+ type: "application/json"
170
+ });
171
+ return navigator.sendBeacon(this.endpoint, blob);
172
+ }
173
+ } catch {
174
+ }
175
+ return false;
176
+ }
177
+ async send(events) {
178
+ const payload = JSON.stringify({
179
+ sentAt: (/* @__PURE__ */ new Date()).toISOString(),
180
+ sdk: this.sdkInfo,
181
+ events
182
+ });
183
+ if (isBrowser() && payload.length < 6e4 && document.visibilityState === "hidden") {
184
+ if (this.trySendBeacon(payload)) return;
185
+ }
186
+ let attempt = 0;
187
+ while (attempt <= this.maxRetries) {
188
+ try {
189
+ const res = await this.fetchImpl(this.endpoint, {
190
+ method: "POST",
191
+ headers: {
192
+ "Content-Type": "application/json",
193
+ "x-api-key": this.apiKey
194
+ },
195
+ body: payload
196
+ });
197
+ if (res.status >= 400 && res.status < 500) {
198
+ if (res.status === 401 || res.status === 403) {
199
+ console.error("Eventra: Invalid API key");
200
+ }
201
+ return;
202
+ }
203
+ if (!res.ok) {
204
+ throw new Error(`HTTP ${res.status}`);
205
+ }
206
+ return;
207
+ } catch (err) {
208
+ attempt++;
209
+ if (attempt > this.maxRetries) {
210
+ throw err;
211
+ }
212
+ const jitter = 0.5 + Math.random();
213
+ const delay = this.retryBaseDelay * 2 ** (attempt - 1) * jitter;
214
+ await sleep(delay);
215
+ }
216
+ }
217
+ }
218
+ /* ---------------- Runtime ---------------- */
219
+ detectRuntime() {
220
+ if (typeof window !== "undefined") return "browser";
221
+ const g = globalThis;
222
+ if (g["Deno"]) return "deno";
223
+ if (g["Bun"]) return "bun";
224
+ const maybeProcess = g["process"];
225
+ if (maybeProcess?.versions?.node) return "node";
226
+ return "unknown";
227
+ }
228
+ /* ---------------- Timer ---------------- */
229
+ startTimer() {
230
+ this.timer = setInterval(() => {
231
+ void this.flush();
232
+ }, this.flushInterval);
233
+ }
234
+ /* ---------------- Exit handling ---------------- */
235
+ setupExitHandlers() {
236
+ if (typeof window !== "undefined") {
237
+ window.addEventListener("visibilitychange", () => {
238
+ if (document.visibilityState === "hidden") {
239
+ void this.flush();
240
+ }
241
+ });
242
+ }
243
+ const g = globalThis;
244
+ const maybeProcess = g["process"];
245
+ if (typeof maybeProcess?.once === "function") {
246
+ const shutdown = async () => {
247
+ try {
248
+ await this.flush();
249
+ } catch {
250
+ }
251
+ };
252
+ maybeProcess.once("SIGINT", shutdown);
253
+ maybeProcess.once("SIGTERM", shutdown);
254
+ maybeProcess.once("beforeExit", shutdown);
255
+ }
256
+ }
257
+ };
258
+ // Annotate the CommonJS export names for ESM import in node:
259
+ 0 && (module.exports = {
260
+ Eventra
261
+ });
262
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/client.ts"],"sourcesContent":["export * from \"./client\";\nexport * from \"./types\";\n","import {\n TrackerOptions,\n TrackEvent,\n SdkInfo,\n} from \"./types\";\n\n/* ---------------- Constants ---------------- */\n\n\ndeclare const __SDK_VERSION__: string;\ndeclare const __EVENTRA_ENDPOINT__: string | undefined;\n\nconst SDK_VERSION = __SDK_VERSION__;\n\nconst DEFAULTS = {\n endpoint: __EVENTRA_ENDPOINT__ ?? \"\",\n flushInterval: 2000,\n maxBatchSize: 50,\n maxQueueSize: 10_000,\n maxRetries: 3,\n retryBaseDelay: 300,\n};\n\n/* ---------------- Helpers ---------------- */\n\nfunction getGlobalFetch(): typeof fetch | undefined {\n if (typeof fetch !== \"undefined\") {\n return fetch.bind(globalThis);\n }\n return undefined;\n}\n\nfunction generateUUIDv4(): string {\n if (\n typeof crypto !== \"undefined\" &&\n typeof crypto.randomUUID === \"function\"\n ) {\n return crypto.randomUUID();\n }\n\n // ultra-rare fallback (не должен происходить в нормальной среде)\n const rnd = Math.random().toString(16).slice(2);\n const ts = Date.now().toString(16);\n return `${ts}-${rnd}`;\n}\n\nfunction isBrowser(): boolean {\n return typeof window !== \"undefined\";\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n\n/* ---------------- Leader Election (minimal) ---------------- */\n\nconst LEADER_KEY = \"__eventra_sdk_leader__\";\nconst LEADER_TTL = 4000;\n\nfunction tryBecomeLeader(): boolean {\n if (!isBrowser()) return true;\n\n try {\n const now = Date.now();\n const raw = localStorage.getItem(LEADER_KEY);\n\n if (!raw) {\n localStorage.setItem(\n LEADER_KEY,\n JSON.stringify({ ts: now })\n );\n return true;\n }\n\n const parsed = JSON.parse(raw) as { ts: number };\n\n if (now - parsed.ts > LEADER_TTL) {\n localStorage.setItem(\n LEADER_KEY,\n JSON.stringify({ ts: now })\n );\n return true;\n }\n\n return false;\n } catch {\n return true; // fail-open\n }\n}\n\n/* ============================================================ */\n\nexport class Eventra {\n private apiKey: string;\n private endpoint: string;\n private flushInterval: number;\n private maxBatchSize: number;\n private maxQueueSize: number;\n private maxRetries: number;\n private retryBaseDelay: number;\n private multiTabMode: \"independent\" | \"leader\";\n\n private fetchImpl?: typeof fetch;\n private queue: TrackEvent[] = [];\n private timer?: ReturnType<typeof setInterval>;\n private inFlight = false;\n private destroyed = false;\n private droppedEvents = 0;\n\n private sdkInfo: SdkInfo;\n\n constructor(options: TrackerOptions) {\n if (!options.apiKey) {\n throw new Error(\"Eventra: apiKey is required\");\n }\n\n this.apiKey = options.apiKey;\n\n this.endpoint = options.endpoint ?? DEFAULTS.endpoint ?? \"\";\n if (!this.endpoint) {\n throw new Error(\"Eventra: endpoint is not configured\");\n }\n\n this.flushInterval =\n options.flushInterval ?? DEFAULTS.flushInterval;\n\n this.maxBatchSize =\n options.maxBatchSize ?? DEFAULTS.maxBatchSize;\n\n this.maxQueueSize =\n options.maxQueueSize ?? DEFAULTS.maxQueueSize;\n\n this.maxRetries =\n options.maxRetries ?? DEFAULTS.maxRetries;\n\n this.retryBaseDelay =\n options.retryBaseDelayMs ?? DEFAULTS.retryBaseDelay;\n\n this.multiTabMode =\n options.multiTabMode ?? \"independent\";\n\n this.fetchImpl =\n options.fetchImpl ?? getGlobalFetch();\n\n if (!this.fetchImpl) {\n throw new Error(\n \"Eventra: fetch is not available. Provide fetchImpl.\"\n );\n }\n\n this.sdkInfo = {\n name: \"@eventra/sdk\",\n version: SDK_VERSION,\n runtime: this.detectRuntime(),\n };\n\n if (!options.disableTimer) {\n this.startTimer();\n }\n\n if (options.autoFlushOnExit !== false) {\n this.setupExitHandlers();\n }\n\n this.onEventsDropped = options.onEventsDropped;\n }\n\n private onEventsDropped?: (count: number) => void;\n\n /* ---------------- Public API ---------------- */\n\n track(\n name: string,\n options?: {\n userId?: string;\n properties?: Record<string, unknown>;\n }\n ) {\n if (this.destroyed) return;\n\n if (this.queue.length >= this.maxQueueSize) {\n this.droppedEvents++;\n this.onEventsDropped?.(this.droppedEvents);\n return;\n }\n\n this.queue.push({\n idempotencyKey: generateUUIDv4(),\n name,\n userId: options?.userId,\n properties: options?.properties ?? {},\n timestamp: new Date().toISOString(),\n });\n\n if (this.queue.length >= this.maxBatchSize) {\n void this.flush();\n }\n }\n\n async flush(): Promise<void> {\n if (this.destroyed) return;\n if (this.inFlight) return;\n if (this.queue.length === 0) return;\n\n if (\n this.multiTabMode === \"leader\" &&\n !tryBecomeLeader()\n ) {\n return;\n }\n\n this.inFlight = true;\n\n const batch = this.queue.splice(0, this.queue.length);\n\n try {\n await this.send(batch);\n } catch {\n this.queue.unshift(...batch);\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 /* ---------------- Transport ---------------- */\n\n private trySendBeacon(payload: string): boolean {\n if (!isBrowser()) return false;\n\n try {\n if (navigator.sendBeacon) {\n const blob = new Blob([payload], {\n type: \"application/json\",\n });\n return navigator.sendBeacon(this.endpoint, blob);\n }\n } catch {\n // ignore\n }\n\n return false;\n }\n\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 // beacon fast-path (browser only)\n if (\n isBrowser() &&\n payload.length < 60_000 &&\n document.visibilityState === \"hidden\"\n ) {\n if (this.trySendBeacon(payload)) return;\n }\n\n let attempt = 0;\n\n while (attempt <= this.maxRetries) {\n try {\n const res = await this.fetchImpl!(this.endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": this.apiKey,\n },\n body: payload,\n });\n\n // ❗ DO NOT retry client errors\n if (res.status >= 400 && res.status < 500) {\n if (res.status === 401 || res.status === 403) {\n console.error(\"Eventra: Invalid API key\");\n }\n return;\n }\n\n if (!res.ok) {\n throw new Error(`HTTP ${res.status}`);\n }\n\n return;\n } catch (err) {\n attempt++;\n\n if (attempt > this.maxRetries) {\n throw err;\n }\n\n const jitter = 0.5 + Math.random(); // 0.5–1.5\n const delay =\n this.retryBaseDelay *\n 2 ** (attempt - 1) *\n jitter;\n\n await sleep(delay);\n }\n }\n }\n\n /* ---------------- Runtime ---------------- */\n\n private detectRuntime(): string {\n if (typeof window !== \"undefined\") return \"browser\";\n\n const g = globalThis as Record<string, unknown>;\n\n if (g[\"Deno\"]) return \"deno\";\n if (g[\"Bun\"]) return \"bun\";\n\n const maybeProcess = g[\"process\"] as\n | { versions?: { node?: string } }\n | undefined;\n\n if (maybeProcess?.versions?.node) return \"node\";\n\n return \"unknown\";\n }\n\n /* ---------------- Timer ---------------- */\n\n private startTimer() {\n this.timer = setInterval(() => {\n void this.flush();\n }, this.flushInterval);\n }\n\n /* ---------------- Exit handling ---------------- */\n\n private setupExitHandlers() {\n // browser\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"visibilitychange\", () => {\n if (document.visibilityState === \"hidden\") {\n void this.flush();\n }\n });\n }\n\n // node\n const g = globalThis as Record<string, unknown>;\n\n const maybeProcess = g[\"process\"] as\n | { once?: (event: string, cb: () => void) => void }\n | undefined;\n\n if (typeof maybeProcess?.once === \"function\") {\n const shutdown = async () => {\n try {\n await this.flush();\n } catch {}\n };\n\n maybeProcess.once(\"SIGINT\", shutdown);\n maybeProcess.once(\"SIGTERM\", shutdown);\n maybeProcess.once(\"beforeExit\", shutdown);\n }\n }\n}\n\nexport * from \"./types\";\n"],"mappings":"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","names":[]}
@@ -0,0 +1,61 @@
1
+ interface TrackerOptions {
2
+ apiKey: string;
3
+ endpoint?: string;
4
+ flushInterval?: number;
5
+ maxBatchSize?: number;
6
+ maxQueueSize?: number;
7
+ maxRetries?: number;
8
+ retryBaseDelayMs?: number;
9
+ fetchImpl?: typeof fetch;
10
+ autoFlushOnExit?: boolean;
11
+ disableTimer?: boolean;
12
+ /** Called when events are dropped due to queue overflow */
13
+ onEventsDropped?: (count: number) => void;
14
+ /** Multi-tab mode (browser only) */
15
+ multiTabMode?: "independent" | "leader";
16
+ }
17
+ interface TrackEvent {
18
+ idempotencyKey: string;
19
+ name: string;
20
+ userId?: string;
21
+ properties?: Record<string, unknown>;
22
+ timestamp: string;
23
+ }
24
+ interface SdkInfo {
25
+ name: string;
26
+ version: string;
27
+ runtime: string;
28
+ }
29
+
30
+ declare class Eventra {
31
+ private apiKey;
32
+ private endpoint;
33
+ private flushInterval;
34
+ private maxBatchSize;
35
+ private maxQueueSize;
36
+ private maxRetries;
37
+ private retryBaseDelay;
38
+ private multiTabMode;
39
+ private fetchImpl?;
40
+ private queue;
41
+ private timer?;
42
+ private inFlight;
43
+ private destroyed;
44
+ private droppedEvents;
45
+ private sdkInfo;
46
+ constructor(options: TrackerOptions);
47
+ private onEventsDropped?;
48
+ track(name: string, options?: {
49
+ userId?: string;
50
+ properties?: Record<string, unknown>;
51
+ }): void;
52
+ flush(): Promise<void>;
53
+ destroy(): void;
54
+ private trySendBeacon;
55
+ private send;
56
+ private detectRuntime;
57
+ private startTimer;
58
+ private setupExitHandlers;
59
+ }
60
+
61
+ export { Eventra, type SdkInfo, type TrackEvent, type TrackerOptions };
@@ -0,0 +1,61 @@
1
+ interface TrackerOptions {
2
+ apiKey: string;
3
+ endpoint?: string;
4
+ flushInterval?: number;
5
+ maxBatchSize?: number;
6
+ maxQueueSize?: number;
7
+ maxRetries?: number;
8
+ retryBaseDelayMs?: number;
9
+ fetchImpl?: typeof fetch;
10
+ autoFlushOnExit?: boolean;
11
+ disableTimer?: boolean;
12
+ /** Called when events are dropped due to queue overflow */
13
+ onEventsDropped?: (count: number) => void;
14
+ /** Multi-tab mode (browser only) */
15
+ multiTabMode?: "independent" | "leader";
16
+ }
17
+ interface TrackEvent {
18
+ idempotencyKey: string;
19
+ name: string;
20
+ userId?: string;
21
+ properties?: Record<string, unknown>;
22
+ timestamp: string;
23
+ }
24
+ interface SdkInfo {
25
+ name: string;
26
+ version: string;
27
+ runtime: string;
28
+ }
29
+
30
+ declare class Eventra {
31
+ private apiKey;
32
+ private endpoint;
33
+ private flushInterval;
34
+ private maxBatchSize;
35
+ private maxQueueSize;
36
+ private maxRetries;
37
+ private retryBaseDelay;
38
+ private multiTabMode;
39
+ private fetchImpl?;
40
+ private queue;
41
+ private timer?;
42
+ private inFlight;
43
+ private destroyed;
44
+ private droppedEvents;
45
+ private sdkInfo;
46
+ constructor(options: TrackerOptions);
47
+ private onEventsDropped?;
48
+ track(name: string, options?: {
49
+ userId?: string;
50
+ properties?: Record<string, unknown>;
51
+ }): void;
52
+ flush(): Promise<void>;
53
+ destroy(): void;
54
+ private trySendBeacon;
55
+ private send;
56
+ private detectRuntime;
57
+ private startTimer;
58
+ private setupExitHandlers;
59
+ }
60
+
61
+ export { Eventra, type SdkInfo, type TrackEvent, type TrackerOptions };
package/dist/index.js ADDED
@@ -0,0 +1,235 @@
1
+ // src/client.ts
2
+ var SDK_VERSION = "0.1.2";
3
+ var DEFAULTS = {
4
+ endpoint: "http://api.eventra.dev/api/v1/ingest/batch",
5
+ flushInterval: 2e3,
6
+ maxBatchSize: 50,
7
+ maxQueueSize: 1e4,
8
+ maxRetries: 3,
9
+ retryBaseDelay: 300
10
+ };
11
+ function getGlobalFetch() {
12
+ if (typeof fetch !== "undefined") {
13
+ return fetch.bind(globalThis);
14
+ }
15
+ return void 0;
16
+ }
17
+ function generateUUIDv4() {
18
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
19
+ return crypto.randomUUID();
20
+ }
21
+ const rnd = Math.random().toString(16).slice(2);
22
+ const ts = Date.now().toString(16);
23
+ return `${ts}-${rnd}`;
24
+ }
25
+ function isBrowser() {
26
+ return typeof window !== "undefined";
27
+ }
28
+ function sleep(ms) {
29
+ return new Promise((r) => setTimeout(r, ms));
30
+ }
31
+ var LEADER_KEY = "__eventra_sdk_leader__";
32
+ var LEADER_TTL = 4e3;
33
+ function tryBecomeLeader() {
34
+ if (!isBrowser()) return true;
35
+ try {
36
+ const now = Date.now();
37
+ const raw = localStorage.getItem(LEADER_KEY);
38
+ if (!raw) {
39
+ localStorage.setItem(
40
+ LEADER_KEY,
41
+ JSON.stringify({ ts: now })
42
+ );
43
+ return true;
44
+ }
45
+ const parsed = JSON.parse(raw);
46
+ if (now - parsed.ts > LEADER_TTL) {
47
+ localStorage.setItem(
48
+ LEADER_KEY,
49
+ JSON.stringify({ ts: now })
50
+ );
51
+ return true;
52
+ }
53
+ return false;
54
+ } catch {
55
+ return true;
56
+ }
57
+ }
58
+ var Eventra = class {
59
+ constructor(options) {
60
+ this.queue = [];
61
+ this.inFlight = false;
62
+ this.destroyed = false;
63
+ this.droppedEvents = 0;
64
+ if (!options.apiKey) {
65
+ throw new Error("Eventra: apiKey is required");
66
+ }
67
+ this.apiKey = options.apiKey;
68
+ this.endpoint = options.endpoint ?? DEFAULTS.endpoint ?? "";
69
+ if (!this.endpoint) {
70
+ throw new Error("Eventra: endpoint is not configured");
71
+ }
72
+ this.flushInterval = options.flushInterval ?? DEFAULTS.flushInterval;
73
+ this.maxBatchSize = options.maxBatchSize ?? DEFAULTS.maxBatchSize;
74
+ this.maxQueueSize = options.maxQueueSize ?? DEFAULTS.maxQueueSize;
75
+ this.maxRetries = options.maxRetries ?? DEFAULTS.maxRetries;
76
+ this.retryBaseDelay = options.retryBaseDelayMs ?? DEFAULTS.retryBaseDelay;
77
+ this.multiTabMode = options.multiTabMode ?? "independent";
78
+ this.fetchImpl = options.fetchImpl ?? getGlobalFetch();
79
+ if (!this.fetchImpl) {
80
+ throw new Error(
81
+ "Eventra: fetch is not available. Provide fetchImpl."
82
+ );
83
+ }
84
+ this.sdkInfo = {
85
+ name: "@eventra/sdk",
86
+ version: SDK_VERSION,
87
+ runtime: this.detectRuntime()
88
+ };
89
+ if (!options.disableTimer) {
90
+ this.startTimer();
91
+ }
92
+ if (options.autoFlushOnExit !== false) {
93
+ this.setupExitHandlers();
94
+ }
95
+ this.onEventsDropped = options.onEventsDropped;
96
+ }
97
+ /* ---------------- Public API ---------------- */
98
+ track(name, options) {
99
+ if (this.destroyed) return;
100
+ if (this.queue.length >= this.maxQueueSize) {
101
+ this.droppedEvents++;
102
+ this.onEventsDropped?.(this.droppedEvents);
103
+ return;
104
+ }
105
+ this.queue.push({
106
+ idempotencyKey: generateUUIDv4(),
107
+ name,
108
+ userId: options?.userId,
109
+ properties: options?.properties ?? {},
110
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
111
+ });
112
+ if (this.queue.length >= this.maxBatchSize) {
113
+ void this.flush();
114
+ }
115
+ }
116
+ async flush() {
117
+ if (this.destroyed) return;
118
+ if (this.inFlight) return;
119
+ if (this.queue.length === 0) return;
120
+ if (this.multiTabMode === "leader" && !tryBecomeLeader()) {
121
+ return;
122
+ }
123
+ this.inFlight = true;
124
+ const batch = this.queue.splice(0, this.queue.length);
125
+ try {
126
+ await this.send(batch);
127
+ } catch {
128
+ this.queue.unshift(...batch);
129
+ } finally {
130
+ this.inFlight = false;
131
+ }
132
+ }
133
+ destroy() {
134
+ this.destroyed = true;
135
+ if (this.timer) clearInterval(this.timer);
136
+ }
137
+ /* ---------------- Transport ---------------- */
138
+ trySendBeacon(payload) {
139
+ if (!isBrowser()) return false;
140
+ try {
141
+ if (navigator.sendBeacon) {
142
+ const blob = new Blob([payload], {
143
+ type: "application/json"
144
+ });
145
+ return navigator.sendBeacon(this.endpoint, blob);
146
+ }
147
+ } catch {
148
+ }
149
+ return false;
150
+ }
151
+ async send(events) {
152
+ const payload = JSON.stringify({
153
+ sentAt: (/* @__PURE__ */ new Date()).toISOString(),
154
+ sdk: this.sdkInfo,
155
+ events
156
+ });
157
+ if (isBrowser() && payload.length < 6e4 && document.visibilityState === "hidden") {
158
+ if (this.trySendBeacon(payload)) return;
159
+ }
160
+ let attempt = 0;
161
+ while (attempt <= this.maxRetries) {
162
+ try {
163
+ const res = await this.fetchImpl(this.endpoint, {
164
+ method: "POST",
165
+ headers: {
166
+ "Content-Type": "application/json",
167
+ "x-api-key": this.apiKey
168
+ },
169
+ body: payload
170
+ });
171
+ if (res.status >= 400 && res.status < 500) {
172
+ if (res.status === 401 || res.status === 403) {
173
+ console.error("Eventra: Invalid API key");
174
+ }
175
+ return;
176
+ }
177
+ if (!res.ok) {
178
+ throw new Error(`HTTP ${res.status}`);
179
+ }
180
+ return;
181
+ } catch (err) {
182
+ attempt++;
183
+ if (attempt > this.maxRetries) {
184
+ throw err;
185
+ }
186
+ const jitter = 0.5 + Math.random();
187
+ const delay = this.retryBaseDelay * 2 ** (attempt - 1) * jitter;
188
+ await sleep(delay);
189
+ }
190
+ }
191
+ }
192
+ /* ---------------- Runtime ---------------- */
193
+ detectRuntime() {
194
+ if (typeof window !== "undefined") return "browser";
195
+ const g = globalThis;
196
+ if (g["Deno"]) return "deno";
197
+ if (g["Bun"]) return "bun";
198
+ const maybeProcess = g["process"];
199
+ if (maybeProcess?.versions?.node) return "node";
200
+ return "unknown";
201
+ }
202
+ /* ---------------- Timer ---------------- */
203
+ startTimer() {
204
+ this.timer = setInterval(() => {
205
+ void this.flush();
206
+ }, this.flushInterval);
207
+ }
208
+ /* ---------------- Exit handling ---------------- */
209
+ setupExitHandlers() {
210
+ if (typeof window !== "undefined") {
211
+ window.addEventListener("visibilitychange", () => {
212
+ if (document.visibilityState === "hidden") {
213
+ void this.flush();
214
+ }
215
+ });
216
+ }
217
+ const g = globalThis;
218
+ const maybeProcess = g["process"];
219
+ if (typeof maybeProcess?.once === "function") {
220
+ const shutdown = async () => {
221
+ try {
222
+ await this.flush();
223
+ } catch {
224
+ }
225
+ };
226
+ maybeProcess.once("SIGINT", shutdown);
227
+ maybeProcess.once("SIGTERM", shutdown);
228
+ maybeProcess.once("beforeExit", shutdown);
229
+ }
230
+ }
231
+ };
232
+ export {
233
+ Eventra
234
+ };
235
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts"],"sourcesContent":["import {\n TrackerOptions,\n TrackEvent,\n SdkInfo,\n} from \"./types\";\n\n/* ---------------- Constants ---------------- */\n\n\ndeclare const __SDK_VERSION__: string;\ndeclare const __EVENTRA_ENDPOINT__: string | undefined;\n\nconst SDK_VERSION = __SDK_VERSION__;\n\nconst DEFAULTS = {\n endpoint: __EVENTRA_ENDPOINT__ ?? \"\",\n flushInterval: 2000,\n maxBatchSize: 50,\n maxQueueSize: 10_000,\n maxRetries: 3,\n retryBaseDelay: 300,\n};\n\n/* ---------------- Helpers ---------------- */\n\nfunction getGlobalFetch(): typeof fetch | undefined {\n if (typeof fetch !== \"undefined\") {\n return fetch.bind(globalThis);\n }\n return undefined;\n}\n\nfunction generateUUIDv4(): string {\n if (\n typeof crypto !== \"undefined\" &&\n typeof crypto.randomUUID === \"function\"\n ) {\n return crypto.randomUUID();\n }\n\n // ultra-rare fallback (не должен происходить в нормальной среде)\n const rnd = Math.random().toString(16).slice(2);\n const ts = Date.now().toString(16);\n return `${ts}-${rnd}`;\n}\n\nfunction isBrowser(): boolean {\n return typeof window !== \"undefined\";\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n\n/* ---------------- Leader Election (minimal) ---------------- */\n\nconst LEADER_KEY = \"__eventra_sdk_leader__\";\nconst LEADER_TTL = 4000;\n\nfunction tryBecomeLeader(): boolean {\n if (!isBrowser()) return true;\n\n try {\n const now = Date.now();\n const raw = localStorage.getItem(LEADER_KEY);\n\n if (!raw) {\n localStorage.setItem(\n LEADER_KEY,\n JSON.stringify({ ts: now })\n );\n return true;\n }\n\n const parsed = JSON.parse(raw) as { ts: number };\n\n if (now - parsed.ts > LEADER_TTL) {\n localStorage.setItem(\n LEADER_KEY,\n JSON.stringify({ ts: now })\n );\n return true;\n }\n\n return false;\n } catch {\n return true; // fail-open\n }\n}\n\n/* ============================================================ */\n\nexport class Eventra {\n private apiKey: string;\n private endpoint: string;\n private flushInterval: number;\n private maxBatchSize: number;\n private maxQueueSize: number;\n private maxRetries: number;\n private retryBaseDelay: number;\n private multiTabMode: \"independent\" | \"leader\";\n\n private fetchImpl?: typeof fetch;\n private queue: TrackEvent[] = [];\n private timer?: ReturnType<typeof setInterval>;\n private inFlight = false;\n private destroyed = false;\n private droppedEvents = 0;\n\n private sdkInfo: SdkInfo;\n\n constructor(options: TrackerOptions) {\n if (!options.apiKey) {\n throw new Error(\"Eventra: apiKey is required\");\n }\n\n this.apiKey = options.apiKey;\n\n this.endpoint = options.endpoint ?? DEFAULTS.endpoint ?? \"\";\n if (!this.endpoint) {\n throw new Error(\"Eventra: endpoint is not configured\");\n }\n\n this.flushInterval =\n options.flushInterval ?? DEFAULTS.flushInterval;\n\n this.maxBatchSize =\n options.maxBatchSize ?? DEFAULTS.maxBatchSize;\n\n this.maxQueueSize =\n options.maxQueueSize ?? DEFAULTS.maxQueueSize;\n\n this.maxRetries =\n options.maxRetries ?? DEFAULTS.maxRetries;\n\n this.retryBaseDelay =\n options.retryBaseDelayMs ?? DEFAULTS.retryBaseDelay;\n\n this.multiTabMode =\n options.multiTabMode ?? \"independent\";\n\n this.fetchImpl =\n options.fetchImpl ?? getGlobalFetch();\n\n if (!this.fetchImpl) {\n throw new Error(\n \"Eventra: fetch is not available. Provide fetchImpl.\"\n );\n }\n\n this.sdkInfo = {\n name: \"@eventra/sdk\",\n version: SDK_VERSION,\n runtime: this.detectRuntime(),\n };\n\n if (!options.disableTimer) {\n this.startTimer();\n }\n\n if (options.autoFlushOnExit !== false) {\n this.setupExitHandlers();\n }\n\n this.onEventsDropped = options.onEventsDropped;\n }\n\n private onEventsDropped?: (count: number) => void;\n\n /* ---------------- Public API ---------------- */\n\n track(\n name: string,\n options?: {\n userId?: string;\n properties?: Record<string, unknown>;\n }\n ) {\n if (this.destroyed) return;\n\n if (this.queue.length >= this.maxQueueSize) {\n this.droppedEvents++;\n this.onEventsDropped?.(this.droppedEvents);\n return;\n }\n\n this.queue.push({\n idempotencyKey: generateUUIDv4(),\n name,\n userId: options?.userId,\n properties: options?.properties ?? {},\n timestamp: new Date().toISOString(),\n });\n\n if (this.queue.length >= this.maxBatchSize) {\n void this.flush();\n }\n }\n\n async flush(): Promise<void> {\n if (this.destroyed) return;\n if (this.inFlight) return;\n if (this.queue.length === 0) return;\n\n if (\n this.multiTabMode === \"leader\" &&\n !tryBecomeLeader()\n ) {\n return;\n }\n\n this.inFlight = true;\n\n const batch = this.queue.splice(0, this.queue.length);\n\n try {\n await this.send(batch);\n } catch {\n this.queue.unshift(...batch);\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 /* ---------------- Transport ---------------- */\n\n private trySendBeacon(payload: string): boolean {\n if (!isBrowser()) return false;\n\n try {\n if (navigator.sendBeacon) {\n const blob = new Blob([payload], {\n type: \"application/json\",\n });\n return navigator.sendBeacon(this.endpoint, blob);\n }\n } catch {\n // ignore\n }\n\n return false;\n }\n\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 // beacon fast-path (browser only)\n if (\n isBrowser() &&\n payload.length < 60_000 &&\n document.visibilityState === \"hidden\"\n ) {\n if (this.trySendBeacon(payload)) return;\n }\n\n let attempt = 0;\n\n while (attempt <= this.maxRetries) {\n try {\n const res = await this.fetchImpl!(this.endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": this.apiKey,\n },\n body: payload,\n });\n\n // ❗ DO NOT retry client errors\n if (res.status >= 400 && res.status < 500) {\n if (res.status === 401 || res.status === 403) {\n console.error(\"Eventra: Invalid API key\");\n }\n return;\n }\n\n if (!res.ok) {\n throw new Error(`HTTP ${res.status}`);\n }\n\n return;\n } catch (err) {\n attempt++;\n\n if (attempt > this.maxRetries) {\n throw err;\n }\n\n const jitter = 0.5 + Math.random(); // 0.5–1.5\n const delay =\n this.retryBaseDelay *\n 2 ** (attempt - 1) *\n jitter;\n\n await sleep(delay);\n }\n }\n }\n\n /* ---------------- Runtime ---------------- */\n\n private detectRuntime(): string {\n if (typeof window !== \"undefined\") return \"browser\";\n\n const g = globalThis as Record<string, unknown>;\n\n if (g[\"Deno\"]) return \"deno\";\n if (g[\"Bun\"]) return \"bun\";\n\n const maybeProcess = g[\"process\"] as\n | { versions?: { node?: string } }\n | undefined;\n\n if (maybeProcess?.versions?.node) return \"node\";\n\n return \"unknown\";\n }\n\n /* ---------------- Timer ---------------- */\n\n private startTimer() {\n this.timer = setInterval(() => {\n void this.flush();\n }, this.flushInterval);\n }\n\n /* ---------------- Exit handling ---------------- */\n\n private setupExitHandlers() {\n // browser\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"visibilitychange\", () => {\n if (document.visibilityState === \"hidden\") {\n void this.flush();\n }\n });\n }\n\n // node\n const g = globalThis as Record<string, unknown>;\n\n const maybeProcess = g[\"process\"] as\n | { once?: (event: string, cb: () => void) => void }\n | undefined;\n\n if (typeof maybeProcess?.once === \"function\") {\n const shutdown = async () => {\n try {\n await this.flush();\n } catch {}\n };\n\n maybeProcess.once(\"SIGINT\", shutdown);\n maybeProcess.once(\"SIGTERM\", shutdown);\n maybeProcess.once(\"beforeExit\", shutdown);\n }\n }\n}\n\nexport * from \"./types\";\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@eventra_dev/eventra-sdk",
3
+ "version": "0.1.2",
4
+ "private": false,
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "type": "module",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.js"
14
+ }
15
+ },
16
+ "main": "./dist/index.js",
17
+ "module": "./dist/index.mjs",
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "sideEffects": false,
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "clean": "rm -rf dist"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/and-1991/eventra-sdk"
29
+ },
30
+ "homepage": "https://eventra.dev",
31
+ "devDependencies": {
32
+ "tsup": "^8.0.0",
33
+ "typescript": "^5.6.0"
34
+ },
35
+ "engines": {
36
+ "node": ">=18"
37
+ }
38
+ }