luzzi-analytics 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # @luzzi/analytics
2
+
3
+ Simple, plug-and-play analytics SDK for solo builders.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @luzzi/analytics
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import luzzi from "@luzzi/analytics";
15
+
16
+ // Initialize with your API key
17
+ luzzi.init("pk_live_xxx");
18
+
19
+ // Track events
20
+ luzzi.track("button_clicked", { button: "signup" });
21
+
22
+ // Identify users
23
+ luzzi.identify("user_123", { plan: "pro" });
24
+
25
+ // Reset on logout
26
+ luzzi.reset();
27
+ ```
28
+
29
+ ## API
30
+
31
+ ### `init(apiKey, config?)`
32
+
33
+ Initialize the SDK. Must be called before any other method.
34
+
35
+ ```typescript
36
+ luzzi.init("pk_live_xxx", {
37
+ apiUrl: "https://api.luzzi.dev", // Custom API URL (optional)
38
+ batchSize: 10, // Events to batch before sending (default: 10)
39
+ flushInterval: 30000, // Flush interval in ms (default: 30s)
40
+ debug: false, // Enable debug logging (default: false)
41
+ });
42
+ ```
43
+
44
+ ### `track(eventName, properties?)`
45
+
46
+ Track an event with optional properties.
47
+
48
+ ```typescript
49
+ luzzi.track("purchase_completed", {
50
+ amount: 99.99,
51
+ currency: "USD",
52
+ product_id: "prod_123",
53
+ });
54
+ ```
55
+
56
+ ### `identify(userId, traits?)`
57
+
58
+ Identify the current user with optional traits.
59
+
60
+ ```typescript
61
+ luzzi.identify("user_123", {
62
+ email: "user@example.com",
63
+ plan: "pro",
64
+ created_at: "2024-01-01",
65
+ });
66
+ ```
67
+
68
+ ### `reset()`
69
+
70
+ Reset the current user. Call this on logout.
71
+
72
+ ```typescript
73
+ luzzi.reset();
74
+ ```
75
+
76
+ ### `flush()`
77
+
78
+ Manually flush pending events to the server.
79
+
80
+ ```typescript
81
+ await luzzi.flush();
82
+ ```
83
+
84
+ ## Automatic Features
85
+
86
+ - **Session tracking**: Automatically generates a unique session ID
87
+ - **Device info**: Collects OS, browser, screen size, language, timezone
88
+ - **Batching**: Events are batched (10 events or 30 seconds)
89
+ - **Auto-flush**: Events are flushed on page unload/visibility change
90
+ - **Retry**: Failed events are re-queued and retried
91
+
92
+ ## License
93
+
94
+ MIT
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Luzzi Analytics SDK Types
3
+ */
4
+ interface LuzziConfig {
5
+ /** API endpoint URL (defaults to https://api.luzzi.dev) */
6
+ apiUrl?: string;
7
+ /** Batch size before auto-flush (default: 10) */
8
+ batchSize?: number;
9
+ /** Flush interval in milliseconds (default: 30000 = 30s) */
10
+ flushInterval?: number;
11
+ /** Enable debug logging */
12
+ debug?: boolean;
13
+ }
14
+ interface DeviceInfo {
15
+ os?: string;
16
+ app_version?: string;
17
+ browser?: string;
18
+ screen_width?: number;
19
+ screen_height?: number;
20
+ language?: string;
21
+ timezone?: string;
22
+ }
23
+ interface EventPayload {
24
+ event: string;
25
+ properties?: Record<string, unknown>;
26
+ timestamp: string;
27
+ session_id: string;
28
+ user_id?: string;
29
+ device: DeviceInfo;
30
+ }
31
+ interface UserTraits {
32
+ [key: string]: unknown;
33
+ }
34
+
35
+ /**
36
+ * Luzzi Analytics SDK
37
+ *
38
+ * Simple, plug-and-play analytics for solo builders.
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * import luzzi from "@luzzi/analytics";
43
+ *
44
+ * // Initialize with your API key
45
+ * luzzi.init("pk_live_xxx");
46
+ *
47
+ * // Track events
48
+ * luzzi.track("button_clicked", { button: "signup" });
49
+ *
50
+ * // Identify users
51
+ * luzzi.identify("user_123", { plan: "pro" });
52
+ *
53
+ * // Reset on logout
54
+ * luzzi.reset();
55
+ * ```
56
+ */
57
+
58
+ /**
59
+ * Luzzi Analytics SDK
60
+ */
61
+ declare const luzzi: {
62
+ /**
63
+ * Initialize the SDK with your API key
64
+ * Must be called before any other method
65
+ *
66
+ * @param apiKey - Your Luzzi API key (pk_live_xxx or pk_test_xxx)
67
+ * @param config - Optional configuration
68
+ */
69
+ init: (apiKey: string, config?: LuzziConfig) => void;
70
+ /**
71
+ * Track an event
72
+ *
73
+ * @param eventName - Name of the event (e.g., "button_clicked", "page_viewed")
74
+ * @param properties - Optional event properties
75
+ *
76
+ * @example
77
+ * luzzi.track("purchase_completed", { amount: 99.99, currency: "USD" });
78
+ */
79
+ track: (eventName: string, properties?: Record<string, unknown>) => void;
80
+ /**
81
+ * Identify the current user
82
+ *
83
+ * @param userId - Unique user identifier
84
+ * @param traits - Optional user traits/properties
85
+ *
86
+ * @example
87
+ * luzzi.identify("user_123", { email: "user@example.com", plan: "pro" });
88
+ */
89
+ identify: (userId: string, traits?: UserTraits) => void;
90
+ /**
91
+ * Reset the current user (call on logout)
92
+ * Clears user ID and generates a new session
93
+ */
94
+ reset: () => void;
95
+ /**
96
+ * Manually flush pending events to the server
97
+ * Events are automatically flushed on a timer and on page unload
98
+ */
99
+ flush: () => Promise<void>;
100
+ /**
101
+ * Get the current session ID
102
+ */
103
+ getSessionId: () => string;
104
+ /**
105
+ * Get the current user ID (if identified)
106
+ */
107
+ getUserId: () => string | undefined;
108
+ };
109
+
110
+ declare const init: (apiKey: string, config?: LuzziConfig) => void;
111
+ declare const track: (eventName: string, properties?: Record<string, unknown>) => void;
112
+ declare const identify: (userId: string, traits?: UserTraits) => void;
113
+ declare const reset: () => void;
114
+ declare const flush: () => Promise<void>;
115
+ declare const getSessionId: () => string;
116
+ declare const getUserId: () => string | undefined;
117
+
118
+ export { type DeviceInfo, type EventPayload, type LuzziConfig, type UserTraits, luzzi as default, flush, getSessionId, getUserId, identify, init, reset, track };
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Luzzi Analytics SDK Types
3
+ */
4
+ interface LuzziConfig {
5
+ /** API endpoint URL (defaults to https://api.luzzi.dev) */
6
+ apiUrl?: string;
7
+ /** Batch size before auto-flush (default: 10) */
8
+ batchSize?: number;
9
+ /** Flush interval in milliseconds (default: 30000 = 30s) */
10
+ flushInterval?: number;
11
+ /** Enable debug logging */
12
+ debug?: boolean;
13
+ }
14
+ interface DeviceInfo {
15
+ os?: string;
16
+ app_version?: string;
17
+ browser?: string;
18
+ screen_width?: number;
19
+ screen_height?: number;
20
+ language?: string;
21
+ timezone?: string;
22
+ }
23
+ interface EventPayload {
24
+ event: string;
25
+ properties?: Record<string, unknown>;
26
+ timestamp: string;
27
+ session_id: string;
28
+ user_id?: string;
29
+ device: DeviceInfo;
30
+ }
31
+ interface UserTraits {
32
+ [key: string]: unknown;
33
+ }
34
+
35
+ /**
36
+ * Luzzi Analytics SDK
37
+ *
38
+ * Simple, plug-and-play analytics for solo builders.
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * import luzzi from "@luzzi/analytics";
43
+ *
44
+ * // Initialize with your API key
45
+ * luzzi.init("pk_live_xxx");
46
+ *
47
+ * // Track events
48
+ * luzzi.track("button_clicked", { button: "signup" });
49
+ *
50
+ * // Identify users
51
+ * luzzi.identify("user_123", { plan: "pro" });
52
+ *
53
+ * // Reset on logout
54
+ * luzzi.reset();
55
+ * ```
56
+ */
57
+
58
+ /**
59
+ * Luzzi Analytics SDK
60
+ */
61
+ declare const luzzi: {
62
+ /**
63
+ * Initialize the SDK with your API key
64
+ * Must be called before any other method
65
+ *
66
+ * @param apiKey - Your Luzzi API key (pk_live_xxx or pk_test_xxx)
67
+ * @param config - Optional configuration
68
+ */
69
+ init: (apiKey: string, config?: LuzziConfig) => void;
70
+ /**
71
+ * Track an event
72
+ *
73
+ * @param eventName - Name of the event (e.g., "button_clicked", "page_viewed")
74
+ * @param properties - Optional event properties
75
+ *
76
+ * @example
77
+ * luzzi.track("purchase_completed", { amount: 99.99, currency: "USD" });
78
+ */
79
+ track: (eventName: string, properties?: Record<string, unknown>) => void;
80
+ /**
81
+ * Identify the current user
82
+ *
83
+ * @param userId - Unique user identifier
84
+ * @param traits - Optional user traits/properties
85
+ *
86
+ * @example
87
+ * luzzi.identify("user_123", { email: "user@example.com", plan: "pro" });
88
+ */
89
+ identify: (userId: string, traits?: UserTraits) => void;
90
+ /**
91
+ * Reset the current user (call on logout)
92
+ * Clears user ID and generates a new session
93
+ */
94
+ reset: () => void;
95
+ /**
96
+ * Manually flush pending events to the server
97
+ * Events are automatically flushed on a timer and on page unload
98
+ */
99
+ flush: () => Promise<void>;
100
+ /**
101
+ * Get the current session ID
102
+ */
103
+ getSessionId: () => string;
104
+ /**
105
+ * Get the current user ID (if identified)
106
+ */
107
+ getUserId: () => string | undefined;
108
+ };
109
+
110
+ declare const init: (apiKey: string, config?: LuzziConfig) => void;
111
+ declare const track: (eventName: string, properties?: Record<string, unknown>) => void;
112
+ declare const identify: (userId: string, traits?: UserTraits) => void;
113
+ declare const reset: () => void;
114
+ declare const flush: () => Promise<void>;
115
+ declare const getSessionId: () => string;
116
+ declare const getUserId: () => string | undefined;
117
+
118
+ export { type DeviceInfo, type EventPayload, type LuzziConfig, type UserTraits, luzzi as default, flush, getSessionId, getUserId, identify, init, reset, track };
package/dist/index.js ADDED
@@ -0,0 +1,390 @@
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
+ default: () => index_default,
24
+ flush: () => flush,
25
+ getSessionId: () => getSessionId,
26
+ getUserId: () => getUserId,
27
+ identify: () => identify,
28
+ init: () => init,
29
+ reset: () => reset,
30
+ track: () => track
31
+ });
32
+ module.exports = __toCommonJS(index_exports);
33
+
34
+ // src/queue.ts
35
+ var EventQueue = class {
36
+ constructor() {
37
+ this.queue = [];
38
+ this.flushTimer = null;
39
+ this.apiKey = "";
40
+ this.apiUrl = "";
41
+ this.batchSize = 10;
42
+ this.flushInterval = 3e4;
43
+ this.debug = false;
44
+ }
45
+ /**
46
+ * Initialize the queue
47
+ */
48
+ init(apiKey, apiUrl, batchSize, flushInterval, debug) {
49
+ this.apiKey = apiKey;
50
+ this.apiUrl = apiUrl;
51
+ this.batchSize = batchSize;
52
+ this.flushInterval = flushInterval;
53
+ this.debug = debug;
54
+ this.startFlushTimer();
55
+ }
56
+ /**
57
+ * Add event to queue
58
+ */
59
+ push(event) {
60
+ this.queue.push(event);
61
+ this.log(`Event queued: ${event.event}`, event);
62
+ if (this.queue.length >= this.batchSize) {
63
+ this.flush();
64
+ }
65
+ }
66
+ /**
67
+ * Flush all events in queue to server
68
+ */
69
+ async flush() {
70
+ if (this.queue.length === 0) {
71
+ return;
72
+ }
73
+ const events = [...this.queue];
74
+ this.queue = [];
75
+ try {
76
+ const payload = { events };
77
+ this.log(`Flushing ${events.length} events`, payload);
78
+ const response = await fetch(`${this.apiUrl}/v1/events`, {
79
+ method: "POST",
80
+ headers: {
81
+ "Content-Type": "application/json",
82
+ "x-api-key": this.apiKey
83
+ },
84
+ body: JSON.stringify(payload)
85
+ });
86
+ if (!response.ok) {
87
+ const error = await response.json().catch(() => ({}));
88
+ console.error("[Luzzi] Failed to send events:", error);
89
+ if (this.queue.length < this.batchSize * 10) {
90
+ this.queue.unshift(...events);
91
+ }
92
+ } else {
93
+ const result = await response.json();
94
+ this.log(`Events sent successfully`, result);
95
+ }
96
+ } catch (error) {
97
+ console.error("[Luzzi] Failed to send events:", error);
98
+ if (this.queue.length < this.batchSize * 10) {
99
+ this.queue.unshift(...events);
100
+ }
101
+ }
102
+ }
103
+ /**
104
+ * Start auto-flush timer
105
+ */
106
+ startFlushTimer() {
107
+ this.stopFlushTimer();
108
+ this.flushTimer = setInterval(() => {
109
+ this.flush();
110
+ }, this.flushInterval);
111
+ }
112
+ /**
113
+ * Stop auto-flush timer
114
+ */
115
+ stopFlushTimer() {
116
+ if (this.flushTimer) {
117
+ clearInterval(this.flushTimer);
118
+ this.flushTimer = null;
119
+ }
120
+ }
121
+ /**
122
+ * Get queue length
123
+ */
124
+ get length() {
125
+ return this.queue.length;
126
+ }
127
+ /**
128
+ * Clear queue and stop timer
129
+ */
130
+ reset() {
131
+ this.queue = [];
132
+ this.stopFlushTimer();
133
+ }
134
+ /**
135
+ * Debug log
136
+ */
137
+ log(message, data) {
138
+ if (this.debug) {
139
+ console.log(`[Luzzi] ${message}`, data || "");
140
+ }
141
+ }
142
+ };
143
+
144
+ // src/core.ts
145
+ var DEFAULT_API_URL = "https://luzzi.vercel.app/api";
146
+ var DEFAULT_BATCH_SIZE = 10;
147
+ var DEFAULT_FLUSH_INTERVAL = 3e4;
148
+ var LuzziCore = class {
149
+ constructor() {
150
+ this.apiKey = "";
151
+ this.apiUrl = DEFAULT_API_URL;
152
+ this.sessionId = "";
153
+ this.userTraits = {};
154
+ this.deviceInfo = {};
155
+ this.initialized = false;
156
+ this.debug = false;
157
+ this.queue = new EventQueue();
158
+ }
159
+ /**
160
+ * Initialize the SDK
161
+ * Must be called before any other method
162
+ */
163
+ init(apiKey, config = {}) {
164
+ if (!apiKey) {
165
+ console.error("[Luzzi] API key is required");
166
+ return;
167
+ }
168
+ this.apiKey = apiKey;
169
+ this.apiUrl = config.apiUrl || DEFAULT_API_URL;
170
+ this.debug = config.debug || false;
171
+ this.sessionId = this.generateSessionId();
172
+ this.deviceInfo = this.collectDeviceInfo();
173
+ this.queue.init(
174
+ this.apiKey,
175
+ this.apiUrl,
176
+ config.batchSize || DEFAULT_BATCH_SIZE,
177
+ config.flushInterval || DEFAULT_FLUSH_INTERVAL,
178
+ this.debug
179
+ );
180
+ this.initialized = true;
181
+ this.log("Initialized", { apiKey: apiKey.slice(0, 10) + "...", sessionId: this.sessionId });
182
+ if (typeof window !== "undefined") {
183
+ window.addEventListener("beforeunload", () => {
184
+ this.flush();
185
+ });
186
+ window.addEventListener("visibilitychange", () => {
187
+ if (document.visibilityState === "hidden") {
188
+ this.flush();
189
+ }
190
+ });
191
+ }
192
+ }
193
+ /**
194
+ * Track an event
195
+ */
196
+ track(eventName, properties) {
197
+ if (!this.initialized) {
198
+ console.warn("[Luzzi] SDK not initialized. Call luzzi.init() first.");
199
+ return;
200
+ }
201
+ if (!eventName || typeof eventName !== "string") {
202
+ console.warn("[Luzzi] Event name is required and must be a string");
203
+ return;
204
+ }
205
+ this.queue.push({
206
+ event: eventName,
207
+ properties: properties || {},
208
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
209
+ session_id: this.sessionId,
210
+ user_id: this.userId,
211
+ device: this.deviceInfo
212
+ });
213
+ }
214
+ /**
215
+ * Identify the current user
216
+ */
217
+ identify(userId, traits) {
218
+ if (!this.initialized) {
219
+ console.warn("[Luzzi] SDK not initialized. Call luzzi.init() first.");
220
+ return;
221
+ }
222
+ if (!userId || typeof userId !== "string") {
223
+ console.warn("[Luzzi] User ID is required and must be a string");
224
+ return;
225
+ }
226
+ this.userId = userId;
227
+ this.userTraits = { ...this.userTraits, ...traits };
228
+ this.log("User identified", { userId, traits });
229
+ this.track("$identify", {
230
+ ...this.userTraits,
231
+ $user_id: userId
232
+ });
233
+ }
234
+ /**
235
+ * Reset the current user (logout)
236
+ */
237
+ reset() {
238
+ this.log("Resetting user");
239
+ this.flush();
240
+ this.userId = void 0;
241
+ this.userTraits = {};
242
+ this.sessionId = this.generateSessionId();
243
+ this.log("Reset complete", { newSessionId: this.sessionId });
244
+ }
245
+ /**
246
+ * Manually flush the event queue
247
+ */
248
+ async flush() {
249
+ if (!this.initialized) {
250
+ return;
251
+ }
252
+ await this.queue.flush();
253
+ }
254
+ /**
255
+ * Get the current session ID
256
+ */
257
+ getSessionId() {
258
+ return this.sessionId;
259
+ }
260
+ /**
261
+ * Get the current user ID
262
+ */
263
+ getUserId() {
264
+ return this.userId;
265
+ }
266
+ /**
267
+ * Generate a unique session ID
268
+ */
269
+ generateSessionId() {
270
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
271
+ return crypto.randomUUID();
272
+ }
273
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
274
+ }
275
+ /**
276
+ * Collect device information
277
+ */
278
+ collectDeviceInfo() {
279
+ const info = {};
280
+ if (typeof window !== "undefined" && typeof navigator !== "undefined") {
281
+ const ua = navigator.userAgent;
282
+ if (ua.includes("Windows")) info.os = "windows";
283
+ else if (ua.includes("Mac")) info.os = "macos";
284
+ else if (ua.includes("Linux")) info.os = "linux";
285
+ else if (ua.includes("Android")) info.os = "android";
286
+ else if (ua.includes("iPhone") || ua.includes("iPad")) info.os = "ios";
287
+ if (ua.includes("Chrome") && !ua.includes("Edg")) info.browser = "chrome";
288
+ else if (ua.includes("Firefox")) info.browser = "firefox";
289
+ else if (ua.includes("Safari") && !ua.includes("Chrome")) info.browser = "safari";
290
+ else if (ua.includes("Edg")) info.browser = "edge";
291
+ info.screen_width = window.screen?.width;
292
+ info.screen_height = window.screen?.height;
293
+ info.language = navigator.language;
294
+ try {
295
+ info.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
296
+ } catch {
297
+ }
298
+ }
299
+ if (typeof process !== "undefined" && process.versions?.node) {
300
+ info.os = process.platform;
301
+ info.app_version = process.versions.node;
302
+ }
303
+ return info;
304
+ }
305
+ /**
306
+ * Debug log
307
+ */
308
+ log(message, data) {
309
+ if (this.debug) {
310
+ console.log(`[Luzzi] ${message}`, data || "");
311
+ }
312
+ }
313
+ };
314
+ var luzziCore = new LuzziCore();
315
+
316
+ // src/index.ts
317
+ var luzzi = {
318
+ /**
319
+ * Initialize the SDK with your API key
320
+ * Must be called before any other method
321
+ *
322
+ * @param apiKey - Your Luzzi API key (pk_live_xxx or pk_test_xxx)
323
+ * @param config - Optional configuration
324
+ */
325
+ init: (apiKey, config) => {
326
+ luzziCore.init(apiKey, config);
327
+ },
328
+ /**
329
+ * Track an event
330
+ *
331
+ * @param eventName - Name of the event (e.g., "button_clicked", "page_viewed")
332
+ * @param properties - Optional event properties
333
+ *
334
+ * @example
335
+ * luzzi.track("purchase_completed", { amount: 99.99, currency: "USD" });
336
+ */
337
+ track: (eventName, properties) => {
338
+ luzziCore.track(eventName, properties);
339
+ },
340
+ /**
341
+ * Identify the current user
342
+ *
343
+ * @param userId - Unique user identifier
344
+ * @param traits - Optional user traits/properties
345
+ *
346
+ * @example
347
+ * luzzi.identify("user_123", { email: "user@example.com", plan: "pro" });
348
+ */
349
+ identify: (userId, traits) => {
350
+ luzziCore.identify(userId, traits);
351
+ },
352
+ /**
353
+ * Reset the current user (call on logout)
354
+ * Clears user ID and generates a new session
355
+ */
356
+ reset: () => {
357
+ luzziCore.reset();
358
+ },
359
+ /**
360
+ * Manually flush pending events to the server
361
+ * Events are automatically flushed on a timer and on page unload
362
+ */
363
+ flush: () => {
364
+ return luzziCore.flush();
365
+ },
366
+ /**
367
+ * Get the current session ID
368
+ */
369
+ getSessionId: () => {
370
+ return luzziCore.getSessionId();
371
+ },
372
+ /**
373
+ * Get the current user ID (if identified)
374
+ */
375
+ getUserId: () => {
376
+ return luzziCore.getUserId();
377
+ }
378
+ };
379
+ var index_default = luzzi;
380
+ var { init, track, identify, reset, flush, getSessionId, getUserId } = luzzi;
381
+ // Annotate the CommonJS export names for ESM import in node:
382
+ 0 && (module.exports = {
383
+ flush,
384
+ getSessionId,
385
+ getUserId,
386
+ identify,
387
+ init,
388
+ reset,
389
+ track
390
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,357 @@
1
+ // src/queue.ts
2
+ var EventQueue = class {
3
+ constructor() {
4
+ this.queue = [];
5
+ this.flushTimer = null;
6
+ this.apiKey = "";
7
+ this.apiUrl = "";
8
+ this.batchSize = 10;
9
+ this.flushInterval = 3e4;
10
+ this.debug = false;
11
+ }
12
+ /**
13
+ * Initialize the queue
14
+ */
15
+ init(apiKey, apiUrl, batchSize, flushInterval, debug) {
16
+ this.apiKey = apiKey;
17
+ this.apiUrl = apiUrl;
18
+ this.batchSize = batchSize;
19
+ this.flushInterval = flushInterval;
20
+ this.debug = debug;
21
+ this.startFlushTimer();
22
+ }
23
+ /**
24
+ * Add event to queue
25
+ */
26
+ push(event) {
27
+ this.queue.push(event);
28
+ this.log(`Event queued: ${event.event}`, event);
29
+ if (this.queue.length >= this.batchSize) {
30
+ this.flush();
31
+ }
32
+ }
33
+ /**
34
+ * Flush all events in queue to server
35
+ */
36
+ async flush() {
37
+ if (this.queue.length === 0) {
38
+ return;
39
+ }
40
+ const events = [...this.queue];
41
+ this.queue = [];
42
+ try {
43
+ const payload = { events };
44
+ this.log(`Flushing ${events.length} events`, payload);
45
+ const response = await fetch(`${this.apiUrl}/v1/events`, {
46
+ method: "POST",
47
+ headers: {
48
+ "Content-Type": "application/json",
49
+ "x-api-key": this.apiKey
50
+ },
51
+ body: JSON.stringify(payload)
52
+ });
53
+ if (!response.ok) {
54
+ const error = await response.json().catch(() => ({}));
55
+ console.error("[Luzzi] Failed to send events:", error);
56
+ if (this.queue.length < this.batchSize * 10) {
57
+ this.queue.unshift(...events);
58
+ }
59
+ } else {
60
+ const result = await response.json();
61
+ this.log(`Events sent successfully`, result);
62
+ }
63
+ } catch (error) {
64
+ console.error("[Luzzi] Failed to send events:", error);
65
+ if (this.queue.length < this.batchSize * 10) {
66
+ this.queue.unshift(...events);
67
+ }
68
+ }
69
+ }
70
+ /**
71
+ * Start auto-flush timer
72
+ */
73
+ startFlushTimer() {
74
+ this.stopFlushTimer();
75
+ this.flushTimer = setInterval(() => {
76
+ this.flush();
77
+ }, this.flushInterval);
78
+ }
79
+ /**
80
+ * Stop auto-flush timer
81
+ */
82
+ stopFlushTimer() {
83
+ if (this.flushTimer) {
84
+ clearInterval(this.flushTimer);
85
+ this.flushTimer = null;
86
+ }
87
+ }
88
+ /**
89
+ * Get queue length
90
+ */
91
+ get length() {
92
+ return this.queue.length;
93
+ }
94
+ /**
95
+ * Clear queue and stop timer
96
+ */
97
+ reset() {
98
+ this.queue = [];
99
+ this.stopFlushTimer();
100
+ }
101
+ /**
102
+ * Debug log
103
+ */
104
+ log(message, data) {
105
+ if (this.debug) {
106
+ console.log(`[Luzzi] ${message}`, data || "");
107
+ }
108
+ }
109
+ };
110
+
111
+ // src/core.ts
112
+ var DEFAULT_API_URL = "https://luzzi.vercel.app/api";
113
+ var DEFAULT_BATCH_SIZE = 10;
114
+ var DEFAULT_FLUSH_INTERVAL = 3e4;
115
+ var LuzziCore = class {
116
+ constructor() {
117
+ this.apiKey = "";
118
+ this.apiUrl = DEFAULT_API_URL;
119
+ this.sessionId = "";
120
+ this.userTraits = {};
121
+ this.deviceInfo = {};
122
+ this.initialized = false;
123
+ this.debug = false;
124
+ this.queue = new EventQueue();
125
+ }
126
+ /**
127
+ * Initialize the SDK
128
+ * Must be called before any other method
129
+ */
130
+ init(apiKey, config = {}) {
131
+ if (!apiKey) {
132
+ console.error("[Luzzi] API key is required");
133
+ return;
134
+ }
135
+ this.apiKey = apiKey;
136
+ this.apiUrl = config.apiUrl || DEFAULT_API_URL;
137
+ this.debug = config.debug || false;
138
+ this.sessionId = this.generateSessionId();
139
+ this.deviceInfo = this.collectDeviceInfo();
140
+ this.queue.init(
141
+ this.apiKey,
142
+ this.apiUrl,
143
+ config.batchSize || DEFAULT_BATCH_SIZE,
144
+ config.flushInterval || DEFAULT_FLUSH_INTERVAL,
145
+ this.debug
146
+ );
147
+ this.initialized = true;
148
+ this.log("Initialized", { apiKey: apiKey.slice(0, 10) + "...", sessionId: this.sessionId });
149
+ if (typeof window !== "undefined") {
150
+ window.addEventListener("beforeunload", () => {
151
+ this.flush();
152
+ });
153
+ window.addEventListener("visibilitychange", () => {
154
+ if (document.visibilityState === "hidden") {
155
+ this.flush();
156
+ }
157
+ });
158
+ }
159
+ }
160
+ /**
161
+ * Track an event
162
+ */
163
+ track(eventName, properties) {
164
+ if (!this.initialized) {
165
+ console.warn("[Luzzi] SDK not initialized. Call luzzi.init() first.");
166
+ return;
167
+ }
168
+ if (!eventName || typeof eventName !== "string") {
169
+ console.warn("[Luzzi] Event name is required and must be a string");
170
+ return;
171
+ }
172
+ this.queue.push({
173
+ event: eventName,
174
+ properties: properties || {},
175
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
176
+ session_id: this.sessionId,
177
+ user_id: this.userId,
178
+ device: this.deviceInfo
179
+ });
180
+ }
181
+ /**
182
+ * Identify the current user
183
+ */
184
+ identify(userId, traits) {
185
+ if (!this.initialized) {
186
+ console.warn("[Luzzi] SDK not initialized. Call luzzi.init() first.");
187
+ return;
188
+ }
189
+ if (!userId || typeof userId !== "string") {
190
+ console.warn("[Luzzi] User ID is required and must be a string");
191
+ return;
192
+ }
193
+ this.userId = userId;
194
+ this.userTraits = { ...this.userTraits, ...traits };
195
+ this.log("User identified", { userId, traits });
196
+ this.track("$identify", {
197
+ ...this.userTraits,
198
+ $user_id: userId
199
+ });
200
+ }
201
+ /**
202
+ * Reset the current user (logout)
203
+ */
204
+ reset() {
205
+ this.log("Resetting user");
206
+ this.flush();
207
+ this.userId = void 0;
208
+ this.userTraits = {};
209
+ this.sessionId = this.generateSessionId();
210
+ this.log("Reset complete", { newSessionId: this.sessionId });
211
+ }
212
+ /**
213
+ * Manually flush the event queue
214
+ */
215
+ async flush() {
216
+ if (!this.initialized) {
217
+ return;
218
+ }
219
+ await this.queue.flush();
220
+ }
221
+ /**
222
+ * Get the current session ID
223
+ */
224
+ getSessionId() {
225
+ return this.sessionId;
226
+ }
227
+ /**
228
+ * Get the current user ID
229
+ */
230
+ getUserId() {
231
+ return this.userId;
232
+ }
233
+ /**
234
+ * Generate a unique session ID
235
+ */
236
+ generateSessionId() {
237
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
238
+ return crypto.randomUUID();
239
+ }
240
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
241
+ }
242
+ /**
243
+ * Collect device information
244
+ */
245
+ collectDeviceInfo() {
246
+ const info = {};
247
+ if (typeof window !== "undefined" && typeof navigator !== "undefined") {
248
+ const ua = navigator.userAgent;
249
+ if (ua.includes("Windows")) info.os = "windows";
250
+ else if (ua.includes("Mac")) info.os = "macos";
251
+ else if (ua.includes("Linux")) info.os = "linux";
252
+ else if (ua.includes("Android")) info.os = "android";
253
+ else if (ua.includes("iPhone") || ua.includes("iPad")) info.os = "ios";
254
+ if (ua.includes("Chrome") && !ua.includes("Edg")) info.browser = "chrome";
255
+ else if (ua.includes("Firefox")) info.browser = "firefox";
256
+ else if (ua.includes("Safari") && !ua.includes("Chrome")) info.browser = "safari";
257
+ else if (ua.includes("Edg")) info.browser = "edge";
258
+ info.screen_width = window.screen?.width;
259
+ info.screen_height = window.screen?.height;
260
+ info.language = navigator.language;
261
+ try {
262
+ info.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
263
+ } catch {
264
+ }
265
+ }
266
+ if (typeof process !== "undefined" && process.versions?.node) {
267
+ info.os = process.platform;
268
+ info.app_version = process.versions.node;
269
+ }
270
+ return info;
271
+ }
272
+ /**
273
+ * Debug log
274
+ */
275
+ log(message, data) {
276
+ if (this.debug) {
277
+ console.log(`[Luzzi] ${message}`, data || "");
278
+ }
279
+ }
280
+ };
281
+ var luzziCore = new LuzziCore();
282
+
283
+ // src/index.ts
284
+ var luzzi = {
285
+ /**
286
+ * Initialize the SDK with your API key
287
+ * Must be called before any other method
288
+ *
289
+ * @param apiKey - Your Luzzi API key (pk_live_xxx or pk_test_xxx)
290
+ * @param config - Optional configuration
291
+ */
292
+ init: (apiKey, config) => {
293
+ luzziCore.init(apiKey, config);
294
+ },
295
+ /**
296
+ * Track an event
297
+ *
298
+ * @param eventName - Name of the event (e.g., "button_clicked", "page_viewed")
299
+ * @param properties - Optional event properties
300
+ *
301
+ * @example
302
+ * luzzi.track("purchase_completed", { amount: 99.99, currency: "USD" });
303
+ */
304
+ track: (eventName, properties) => {
305
+ luzziCore.track(eventName, properties);
306
+ },
307
+ /**
308
+ * Identify the current user
309
+ *
310
+ * @param userId - Unique user identifier
311
+ * @param traits - Optional user traits/properties
312
+ *
313
+ * @example
314
+ * luzzi.identify("user_123", { email: "user@example.com", plan: "pro" });
315
+ */
316
+ identify: (userId, traits) => {
317
+ luzziCore.identify(userId, traits);
318
+ },
319
+ /**
320
+ * Reset the current user (call on logout)
321
+ * Clears user ID and generates a new session
322
+ */
323
+ reset: () => {
324
+ luzziCore.reset();
325
+ },
326
+ /**
327
+ * Manually flush pending events to the server
328
+ * Events are automatically flushed on a timer and on page unload
329
+ */
330
+ flush: () => {
331
+ return luzziCore.flush();
332
+ },
333
+ /**
334
+ * Get the current session ID
335
+ */
336
+ getSessionId: () => {
337
+ return luzziCore.getSessionId();
338
+ },
339
+ /**
340
+ * Get the current user ID (if identified)
341
+ */
342
+ getUserId: () => {
343
+ return luzziCore.getUserId();
344
+ }
345
+ };
346
+ var index_default = luzzi;
347
+ var { init, track, identify, reset, flush, getSessionId, getUserId } = luzzi;
348
+ export {
349
+ index_default as default,
350
+ flush,
351
+ getSessionId,
352
+ getUserId,
353
+ identify,
354
+ init,
355
+ reset,
356
+ track
357
+ };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "luzzi-analytics",
3
+ "version": "0.1.0",
4
+ "description": "Simple, plug-and-play analytics SDK for solo builders",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "tsup src/index.ts --format cjs,esm --dts",
17
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
18
+ "clean": "rimraf dist"
19
+ },
20
+ "keywords": [
21
+ "analytics",
22
+ "tracking",
23
+ "events",
24
+ "solo-builder",
25
+ "luzzi"
26
+ ],
27
+ "author": "",
28
+ "license": "MIT",
29
+ "devDependencies": {
30
+ "tsup": "^8.0.0",
31
+ "typescript": "^5.0.0",
32
+ "rimraf": "^5.0.0"
33
+ },
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/your-username/luzzi"
40
+ }
41
+ }