@logg/signals 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/dist/index.mjs ADDED
@@ -0,0 +1,489 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/utils/helpers.ts
9
+ function uuid() {
10
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
11
+ return crypto.randomUUID();
12
+ }
13
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
14
+ const r = Math.random() * 16 | 0;
15
+ const v = c === "x" ? r : r & 3 | 8;
16
+ return v.toString(16);
17
+ });
18
+ }
19
+ function getTimestamp() {
20
+ return (/* @__PURE__ */ new Date()).toISOString();
21
+ }
22
+ function sleep(ms) {
23
+ return new Promise((resolve) => setTimeout(resolve, ms));
24
+ }
25
+ async function retry(fn, options) {
26
+ const { maxRetries, initialDelay, onRetry } = options;
27
+ let lastError;
28
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
29
+ try {
30
+ return await fn();
31
+ } catch (error) {
32
+ lastError = error instanceof Error ? error : new Error(String(error));
33
+ if (attempt < maxRetries) {
34
+ const delay = initialDelay * Math.pow(2, attempt);
35
+ onRetry?.(attempt + 1, lastError);
36
+ await sleep(delay);
37
+ }
38
+ }
39
+ }
40
+ throw lastError;
41
+ }
42
+
43
+ // src/EventQueue.ts
44
+ var STORAGE_KEY = "session_signals_queue";
45
+ var EventQueue = class {
46
+ constructor(storage, sessionId) {
47
+ this.queue = [];
48
+ this.storage = storage;
49
+ this.sessionId = sessionId;
50
+ }
51
+ /**
52
+ * Initialize queue by loading persisted events
53
+ */
54
+ async init() {
55
+ try {
56
+ const stored = await this.storage.getItem(STORAGE_KEY);
57
+ if (stored) {
58
+ const parsed = JSON.parse(stored);
59
+ this.queue = Array.isArray(parsed) ? parsed : [];
60
+ }
61
+ } catch (error) {
62
+ console.warn("Failed to load persisted events:", error);
63
+ this.queue = [];
64
+ }
65
+ }
66
+ /**
67
+ * Add event to queue
68
+ */
69
+ async add(eventData) {
70
+ const event = {
71
+ event_id: uuid(),
72
+ timestamp: getTimestamp(),
73
+ session_id: this.sessionId,
74
+ ...eventData
75
+ };
76
+ this.queue.push({
77
+ event,
78
+ timestamp: Date.now()
79
+ });
80
+ await this.persist();
81
+ return event;
82
+ }
83
+ /**
84
+ * Get all events in queue
85
+ */
86
+ getAll() {
87
+ return this.queue.map((item) => item.event);
88
+ }
89
+ /**
90
+ * Get events up to a certain size
91
+ */
92
+ getBatch(size) {
93
+ return this.queue.slice(0, size).map((item) => item.event);
94
+ }
95
+ /**
96
+ * Remove events from queue
97
+ */
98
+ async remove(count) {
99
+ this.queue.splice(0, count);
100
+ await this.persist();
101
+ }
102
+ /**
103
+ * Clear all events from queue
104
+ */
105
+ async clear() {
106
+ this.queue = [];
107
+ await this.persist();
108
+ }
109
+ /**
110
+ * Get queue size
111
+ */
112
+ size() {
113
+ return this.queue.length;
114
+ }
115
+ /**
116
+ * Check if queue is empty
117
+ */
118
+ isEmpty() {
119
+ return this.queue.length === 0;
120
+ }
121
+ /**
122
+ * Get oldest event timestamp
123
+ */
124
+ getOldestTimestamp() {
125
+ if (this.queue.length === 0) return null;
126
+ return this.queue[0]?.timestamp ?? null;
127
+ }
128
+ /**
129
+ * Persist queue to storage
130
+ */
131
+ async persist() {
132
+ try {
133
+ await this.storage.setItem(STORAGE_KEY, JSON.stringify(this.queue));
134
+ } catch (error) {
135
+ console.warn("Failed to persist events:", error);
136
+ }
137
+ }
138
+ };
139
+
140
+ // src/storage/LocalStorageAdapter.ts
141
+ var LocalStorageAdapter = class {
142
+ async getItem(key) {
143
+ try {
144
+ return localStorage.getItem(key);
145
+ } catch (error) {
146
+ console.warn("LocalStorage not available:", error);
147
+ return null;
148
+ }
149
+ }
150
+ async setItem(key, value) {
151
+ try {
152
+ localStorage.setItem(key, value);
153
+ } catch (error) {
154
+ console.warn("LocalStorage setItem failed:", error);
155
+ }
156
+ }
157
+ async removeItem(key) {
158
+ try {
159
+ localStorage.removeItem(key);
160
+ } catch (error) {
161
+ console.warn("LocalStorage removeItem failed:", error);
162
+ }
163
+ }
164
+ /**
165
+ * Check if localStorage is available
166
+ */
167
+ static isAvailable() {
168
+ try {
169
+ const test = "__localStorage_test__";
170
+ localStorage.setItem(test, test);
171
+ localStorage.removeItem(test);
172
+ return true;
173
+ } catch {
174
+ return false;
175
+ }
176
+ }
177
+ };
178
+
179
+ // src/storage/AsyncStorageAdapter.ts
180
+ var AsyncStorageAdapter = class {
181
+ constructor() {
182
+ try {
183
+ this.asyncStorage = __require("@react-native-async-storage/async-storage").default;
184
+ } catch (error) {
185
+ throw new Error(
186
+ "AsyncStorage not found. Please install @react-native-async-storage/async-storage"
187
+ );
188
+ }
189
+ }
190
+ async getItem(key) {
191
+ try {
192
+ return await this.asyncStorage.getItem(key);
193
+ } catch (error) {
194
+ console.warn("AsyncStorage getItem failed:", error);
195
+ return null;
196
+ }
197
+ }
198
+ async setItem(key, value) {
199
+ try {
200
+ await this.asyncStorage.setItem(key, value);
201
+ } catch (error) {
202
+ console.warn("AsyncStorage setItem failed:", error);
203
+ }
204
+ }
205
+ async removeItem(key) {
206
+ try {
207
+ await this.asyncStorage.removeItem(key);
208
+ } catch (error) {
209
+ console.warn("AsyncStorage removeItem failed:", error);
210
+ }
211
+ }
212
+ /**
213
+ * Check if AsyncStorage is available
214
+ */
215
+ static isAvailable() {
216
+ try {
217
+ __require("@react-native-async-storage/async-storage");
218
+ return true;
219
+ } catch {
220
+ return false;
221
+ }
222
+ }
223
+ };
224
+
225
+ // src/storage/MemoryStorageAdapter.ts
226
+ var MemoryStorageAdapter = class {
227
+ constructor() {
228
+ this.storage = /* @__PURE__ */ new Map();
229
+ }
230
+ async getItem(key) {
231
+ return this.storage.get(key) ?? null;
232
+ }
233
+ async setItem(key, value) {
234
+ this.storage.set(key, value);
235
+ }
236
+ async removeItem(key) {
237
+ this.storage.delete(key);
238
+ }
239
+ };
240
+
241
+ // src/storage/index.ts
242
+ function getDefaultStorageAdapter() {
243
+ if (LocalStorageAdapter.isAvailable()) {
244
+ return new LocalStorageAdapter();
245
+ }
246
+ if (AsyncStorageAdapter.isAvailable()) {
247
+ return new AsyncStorageAdapter();
248
+ }
249
+ return new MemoryStorageAdapter();
250
+ }
251
+
252
+ // src/utils/metadata.ts
253
+ function detectClientType() {
254
+ if (typeof navigator !== "undefined" && navigator.product === "ReactNative") {
255
+ return "react-native";
256
+ }
257
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
258
+ return "web";
259
+ }
260
+ return "node";
261
+ }
262
+ function getUserAgent() {
263
+ if (typeof navigator !== "undefined") {
264
+ return navigator.userAgent;
265
+ }
266
+ return void 0;
267
+ }
268
+ function getScreenDimensions() {
269
+ if (typeof window !== "undefined" && window.screen) {
270
+ return {
271
+ width: window.screen.width,
272
+ height: window.screen.height
273
+ };
274
+ }
275
+ try {
276
+ const { Dimensions } = __require("react-native");
277
+ const { width, height } = Dimensions.get("window");
278
+ return { width, height };
279
+ } catch {
280
+ return void 0;
281
+ }
282
+ }
283
+ function getLocale() {
284
+ if (typeof navigator !== "undefined") {
285
+ return navigator.language;
286
+ }
287
+ return void 0;
288
+ }
289
+ function getTimezone() {
290
+ try {
291
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
292
+ } catch {
293
+ return void 0;
294
+ }
295
+ }
296
+ function collectMetadata(version) {
297
+ const clientType = detectClientType();
298
+ const metadata = {
299
+ type: clientType,
300
+ version
301
+ };
302
+ if (clientType === "web" || clientType === "react-native") {
303
+ metadata.user_agent = getUserAgent();
304
+ metadata.screen = getScreenDimensions();
305
+ metadata.locale = getLocale();
306
+ metadata.timezone = getTimezone();
307
+ }
308
+ return metadata;
309
+ }
310
+
311
+ // src/Signals.ts
312
+ var VERSION = "0.1.0";
313
+ var DEFAULT_BATCH_SIZE = 10;
314
+ var DEFAULT_BATCH_INTERVAL = 5e3;
315
+ var DEFAULT_MAX_RETRIES = 3;
316
+ var DEFAULT_RETRY_DELAY = 1e3;
317
+ var Signals = class {
318
+ constructor(config) {
319
+ this.flushTimer = null;
320
+ this.isDestroyed = false;
321
+ this.isFlushing = false;
322
+ if (!config.apiKey) {
323
+ throw new Error("Signals: apiKey is required");
324
+ }
325
+ if (!config.endpoint) {
326
+ throw new Error("Signals: endpoint is required");
327
+ }
328
+ this.config = {
329
+ apiKey: config.apiKey,
330
+ endpoint: config.endpoint,
331
+ batchSize: config.batchSize ?? DEFAULT_BATCH_SIZE,
332
+ batchInterval: config.batchInterval ?? DEFAULT_BATCH_INTERVAL,
333
+ maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
334
+ retryDelay: config.retryDelay ?? DEFAULT_RETRY_DELAY,
335
+ debug: config.debug ?? false,
336
+ storage: config.storage ?? getDefaultStorageAdapter(),
337
+ sessionId: config.sessionId ?? uuid()
338
+ };
339
+ this.queue = new EventQueue(this.config.storage, this.config.sessionId);
340
+ this.init();
341
+ }
342
+ /**
343
+ * Initialize client
344
+ */
345
+ async init() {
346
+ await this.queue.init();
347
+ this.log("Signals initialized", {
348
+ sessionId: this.config.sessionId,
349
+ queueSize: this.queue.size()
350
+ });
351
+ this.startFlushTimer();
352
+ if (!this.queue.isEmpty()) {
353
+ await this.flush();
354
+ }
355
+ }
356
+ /**
357
+ * Track an event
358
+ */
359
+ async event(eventData) {
360
+ if (this.isDestroyed) {
361
+ throw new Error("Signals instance has been destroyed");
362
+ }
363
+ const event = await this.queue.add(eventData);
364
+ this.log("Event tracked", event);
365
+ if (this.queue.size() >= this.config.batchSize) {
366
+ await this.flush();
367
+ }
368
+ }
369
+ /**
370
+ * Manually flush events
371
+ */
372
+ async flush() {
373
+ if (this.isDestroyed || this.queue.isEmpty() || this.isFlushing) {
374
+ return;
375
+ }
376
+ this.isFlushing = true;
377
+ const events = this.queue.getBatch(this.config.batchSize);
378
+ if (events.length === 0) {
379
+ this.isFlushing = false;
380
+ return;
381
+ }
382
+ this.log(`Flushing ${events.length} events`);
383
+ const batch = {
384
+ api_key: this.config.apiKey,
385
+ batch_id: uuid(),
386
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
387
+ metadata: collectMetadata(VERSION),
388
+ events
389
+ };
390
+ try {
391
+ await this.sendBatch(batch);
392
+ await this.queue.remove(events.length);
393
+ this.log("Batch sent successfully");
394
+ } catch (error) {
395
+ this.log("Failed to send batch", error);
396
+ } finally {
397
+ this.isFlushing = false;
398
+ }
399
+ }
400
+ /**
401
+ * Send batch to backend with retry logic
402
+ */
403
+ async sendBatch(batch) {
404
+ await retry(
405
+ async () => {
406
+ const response = await fetch(this.config.endpoint, {
407
+ method: "POST",
408
+ headers: {
409
+ "Content-Type": "application/json",
410
+ "Authorization": `Bearer ${this.config.apiKey}`
411
+ },
412
+ body: JSON.stringify(batch)
413
+ });
414
+ if (!response.ok) {
415
+ throw new Error(
416
+ `HTTP ${response.status}: ${response.statusText}`
417
+ );
418
+ }
419
+ },
420
+ {
421
+ maxRetries: this.config.maxRetries,
422
+ initialDelay: this.config.retryDelay,
423
+ onRetry: (attempt, error) => {
424
+ this.log(`Retry attempt ${attempt}`, error);
425
+ }
426
+ }
427
+ );
428
+ }
429
+ /**
430
+ * Start periodic flush timer
431
+ */
432
+ startFlushTimer() {
433
+ this.stopFlushTimer();
434
+ this.flushTimer = setInterval(() => {
435
+ if (!this.queue.isEmpty()) {
436
+ this.flush().catch((error) => {
437
+ this.log("Auto-flush failed", error);
438
+ });
439
+ }
440
+ }, this.config.batchInterval);
441
+ }
442
+ /**
443
+ * Stop flush timer
444
+ */
445
+ stopFlushTimer() {
446
+ if (this.flushTimer) {
447
+ clearInterval(this.flushTimer);
448
+ this.flushTimer = null;
449
+ }
450
+ }
451
+ /**
452
+ * Get current session ID
453
+ */
454
+ getSessionId() {
455
+ return this.config.sessionId;
456
+ }
457
+ /**
458
+ * Get queue size
459
+ */
460
+ getQueueSize() {
461
+ return this.queue.size();
462
+ }
463
+ /**
464
+ * Destroy client and cleanup
465
+ */
466
+ async destroy() {
467
+ if (this.isDestroyed) return;
468
+ this.log("Destroying Signals instance");
469
+ this.isDestroyed = true;
470
+ this.stopFlushTimer();
471
+ await this.flush();
472
+ }
473
+ /**
474
+ * Debug logging
475
+ */
476
+ log(message, data) {
477
+ if (this.config.debug) {
478
+ console.log(`[LoggClient] ${message}`, data ?? "");
479
+ }
480
+ }
481
+ };
482
+ export {
483
+ AsyncStorageAdapter,
484
+ EventQueue,
485
+ LocalStorageAdapter,
486
+ MemoryStorageAdapter,
487
+ Signals,
488
+ getDefaultStorageAdapter
489
+ };
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@logg/signals",
3
+ "version": "0.1.0",
4
+ "description": "Universal event tracking SDK for Logg Signals",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
20
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
21
+ "test": "vitest",
22
+ "test:ui": "vitest --ui",
23
+ "type-check": "tsc --noEmit",
24
+ "lint": "eslint src --ext ts,tsx",
25
+ "prepublishOnly": "pnpm run build"
26
+ },
27
+ "keywords": [
28
+ "analytics",
29
+ "events",
30
+ "tracking",
31
+ "logg",
32
+ "signals",
33
+ "react-native",
34
+ "web",
35
+ "nodejs"
36
+ ],
37
+ "author": "Logg",
38
+ "license": "MIT",
39
+ "homepage": "https://github.com/synctree/session-app/tree/main/apps/signals-client#readme",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/synctree/session-app.git",
43
+ "directory": "apps/signals-client"
44
+ },
45
+ "bugs": {
46
+ "url": "https://github.com/synctree/session-app/issues"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^20.10.0",
50
+ "@typescript-eslint/eslint-plugin": "^6.13.0",
51
+ "@typescript-eslint/parser": "^6.13.0",
52
+ "eslint": "^8.55.0",
53
+ "tsup": "^8.0.1",
54
+ "typescript": "^5.3.2",
55
+ "vitest": "^1.0.4"
56
+ },
57
+ "peerDependencies": {
58
+ "@react-native-async-storage/async-storage": "^1.0.0"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "@react-native-async-storage/async-storage": {
62
+ "optional": true
63
+ }
64
+ }
65
+ }