@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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Logg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,417 @@
1
+ # @logg/signals
2
+
3
+ Universal event tracking SDK for Logg Signals. Track events from web, React Native, and Node.js applications.
4
+
5
+ ## Features
6
+
7
+ ✅ **Universal** - Works in browsers, React Native, and Node.js
8
+ ✅ **Type-safe** - Full TypeScript support
9
+ ✅ **Automatic batching** - Efficient event batching with configurable thresholds
10
+ ✅ **Persistent storage** - Uses localStorage, AsyncStorage, or memory as fallback
11
+ ✅ **Retry logic** - Exponential backoff for failed requests
12
+ ✅ **Auto metadata** - Automatically collects browser/device information
13
+ ✅ **Small bundle** - <5KB gzipped
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @logg/signals
19
+ ```
20
+
21
+ For React Native, also install AsyncStorage:
22
+
23
+ ```bash
24
+ npm install @react-native-async-storage/async-storage
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ### Web (Browser)
30
+
31
+ ```typescript
32
+ import { Signals } from '@logg/signals';
33
+
34
+ const signals = new Signals({
35
+ apiKey: 'your-api-key',
36
+ endpoint: 'https://signals.yourdomain.com/events',
37
+ });
38
+
39
+ // Track events
40
+ signals.event({
41
+ type: 'page_view',
42
+ page: '/dashboard',
43
+ userId: '12345',
44
+ });
45
+
46
+ signals.event({
47
+ type: 'button_click',
48
+ element: 'signup_cta',
49
+ userId: '12345',
50
+ });
51
+ ```
52
+
53
+ ### React Native
54
+
55
+ ```typescript
56
+ import { Signals } from '@logg/signals';
57
+ import AsyncStorage from '@react-native-async-storage/async-storage';
58
+
59
+ const signals = new Signals({
60
+ apiKey: 'your-api-key',
61
+ endpoint: 'https://signals.yourdomain.com/events',
62
+ // AsyncStorage is auto-detected, but you can pass it explicitly
63
+ });
64
+
65
+ // Track events
66
+ signals.event({
67
+ type: 'screen_view',
68
+ screen: 'HomeScreen',
69
+ userId: user.id,
70
+ });
71
+
72
+ signals.event({
73
+ type: 'purchase',
74
+ productId: 'premium-plan',
75
+ amount: 29.99,
76
+ userId: user.id,
77
+ });
78
+ ```
79
+
80
+ ### Node.js
81
+
82
+ ```typescript
83
+ import { Signals } from '@logg/signals';
84
+
85
+ const signals = new Signals({
86
+ apiKey: 'your-api-key',
87
+ endpoint: 'https://signals.yourdomain.com/events',
88
+ });
89
+
90
+ // Track server-side events
91
+ await signals.event({
92
+ type: 'api_call',
93
+ endpoint: '/api/users',
94
+ method: 'POST',
95
+ userId: req.user.id,
96
+ });
97
+
98
+ // Make sure to flush before process exit
99
+ process.on('beforeExit', async () => {
100
+ await signals.flush();
101
+ });
102
+ ```
103
+
104
+ ## Configuration
105
+
106
+ ```typescript
107
+ const signals = new Signals({
108
+ // Required
109
+ apiKey: 'your-api-key',
110
+ endpoint: 'https://signals.yourdomain.com/events',
111
+
112
+ // Optional
113
+ batchSize: 10, // Send after 10 events (default: 10)
114
+ batchInterval: 5000, // Or every 5 seconds (default: 5000)
115
+ maxRetries: 3, // Retry failed requests 3 times (default: 3)
116
+ retryDelay: 1000, // Initial retry delay in ms (default: 1000)
117
+ debug: false, // Enable debug logging (default: false)
118
+ sessionId: 'custom-id', // Custom session ID (auto-generated by default)
119
+ storage: customAdapter, // Custom storage adapter (auto-detected by default)
120
+ });
121
+ ```
122
+
123
+ ## API Reference
124
+
125
+ ### `signals.event(eventData)`
126
+
127
+ Track an event. Events are automatically batched and sent based on `batchSize` and `batchInterval` config.
128
+
129
+ ```typescript
130
+ await signals.event({
131
+ type: 'event_type', // Required: event type
132
+ userId: 'user-123', // Optional: user ID
133
+ // ... any other properties
134
+ });
135
+ ```
136
+
137
+ **Auto-added fields:**
138
+ - `event_id` - Unique event identifier (UUID v4)
139
+ - `timestamp` - ISO 8601 timestamp
140
+ - `session_id` - Session identifier
141
+ - `client` - Client metadata (type, version, user_agent, screen, locale, timezone)
142
+
143
+ ### `signals.flush()`
144
+
145
+ Manually flush all pending events immediately.
146
+
147
+ ```typescript
148
+ await signals.flush();
149
+ ```
150
+
151
+ ### `signals.getSessionId()`
152
+
153
+ Get the current session ID.
154
+
155
+ ```typescript
156
+ const sessionId = signals.getSessionId();
157
+ ```
158
+
159
+ ### `signals.getQueueSize()`
160
+
161
+ Get the number of events in the queue.
162
+
163
+ ```typescript
164
+ const queueSize = signals.getQueueSize();
165
+ ```
166
+
167
+ ### `signals.destroy()`
168
+
169
+ Destroy the client, flush remaining events, and cleanup resources.
170
+
171
+ ```typescript
172
+ await signals.destroy();
173
+ ```
174
+
175
+ ## Event Batching
176
+
177
+ Events are automatically batched to reduce network requests:
178
+
179
+ 1. **Batch by size**: Sends when `batchSize` events are queued (default: 10)
180
+ 2. **Batch by time**: Sends every `batchInterval` milliseconds (default: 5000)
181
+ 3. **Manual flush**: Call `signals.flush()` to send immediately
182
+
183
+ **Batch format sent to backend:**
184
+
185
+ ```json
186
+ {
187
+ "api_key": "your-api-key",
188
+ "batch_id": "batch-uuid",
189
+ "timestamp": "2025-12-02T10:30:00.000Z",
190
+ "metadata": {
191
+ "type": "web",
192
+ "version": "0.1.0",
193
+ "user_agent": "Mozilla/5.0...",
194
+ "screen": { "width": 1920, "height": 1080 },
195
+ "locale": "en-US",
196
+ "timezone": "America/New_York"
197
+ },
198
+ "events": [
199
+ {
200
+ "event_id": "uuid-1",
201
+ "timestamp": "2025-12-02T10:30:00.000Z",
202
+ "session_id": "session-uuid",
203
+ "type": "page_view",
204
+ "userId": "12345",
205
+ "page": "/dashboard"
206
+ }
207
+ ]
208
+ }
209
+ ```
210
+
211
+ ## Storage Adapters
212
+
213
+ The SDK automatically detects the best storage adapter:
214
+
215
+ 1. **Web**: `LocalStorageAdapter` (uses `localStorage`)
216
+ 2. **React Native**: `AsyncStorageAdapter` (uses `@react-native-async-storage/async-storage`)
217
+ 3. **Node.js**: `MemoryStorageAdapter` (in-memory, no persistence)
218
+
219
+ ### Custom Storage Adapter
220
+
221
+ You can provide a custom storage adapter:
222
+
223
+ ```typescript
224
+ import { Signals, StorageAdapter } from '@logg/signals';
225
+
226
+ class CustomStorageAdapter implements StorageAdapter {
227
+ async getItem(key: string): Promise<string | null> {
228
+ // Your implementation
229
+ }
230
+
231
+ async setItem(key: string, value: string): Promise<void> {
232
+ // Your implementation
233
+ }
234
+
235
+ async removeItem(key: string): Promise<void> {
236
+ // Your implementation
237
+ }
238
+ }
239
+
240
+ const signals = new Signals({
241
+ apiKey: 'your-api-key',
242
+ endpoint: 'https://signals.yourdomain.com/events',
243
+ storage: new CustomStorageAdapter(),
244
+ });
245
+ ```
246
+
247
+ ## Error Handling
248
+
249
+ The SDK includes automatic retry logic with exponential backoff:
250
+
251
+ - Failed requests are retried up to `maxRetries` times (default: 3)
252
+ - Retry delay doubles after each attempt (exponential backoff)
253
+ - Events are persisted in storage and retried on next batch
254
+
255
+ ```typescript
256
+ const signals = new Signals({
257
+ apiKey: 'your-api-key',
258
+ endpoint: 'https://signals.yourdomain.com/events',
259
+ maxRetries: 5, // Retry up to 5 times
260
+ retryDelay: 2000, // Start with 2 second delay
261
+ debug: true, // Log retry attempts
262
+ });
263
+ ```
264
+
265
+ ## React Integration
266
+
267
+ ### Track Page Views
268
+
269
+ ```typescript
270
+ import { useEffect } from 'react';
271
+ import { useLocation } from 'react-router-dom';
272
+
273
+ function App() {
274
+ const location = useLocation();
275
+
276
+ useEffect(() => {
277
+ signals.event({
278
+ type: 'page_view',
279
+ page: location.pathname,
280
+ title: document.title,
281
+ });
282
+ }, [location]);
283
+
284
+ return <div>...</div>;
285
+ }
286
+ ```
287
+
288
+ ### Track User Actions
289
+
290
+ ```typescript
291
+ function SignupButton() {
292
+ const handleClick = () => {
293
+ signals.event({
294
+ type: 'button_click',
295
+ element: 'signup_cta',
296
+ page: '/landing',
297
+ });
298
+
299
+ // Navigate to signup...
300
+ };
301
+
302
+ return <button onClick={handleClick}>Sign Up</button>;
303
+ }
304
+ ```
305
+
306
+ ## React Native Integration
307
+
308
+ ```typescript
309
+ import { Signals } from '@logg/signals';
310
+ import { useEffect } from 'react';
311
+ import { useNavigation } from '@react-navigation/native';
312
+
313
+ const signals = new Signals({
314
+ apiKey: 'your-api-key',
315
+ endpoint: 'https://signals.yourdomain.com/events',
316
+ });
317
+
318
+ function HomeScreen() {
319
+ const navigation = useNavigation();
320
+
321
+ useEffect(() => {
322
+ // Track screen view
323
+ signals.event({
324
+ type: 'screen_view',
325
+ screen: 'HomeScreen',
326
+ });
327
+ }, []);
328
+
329
+ return (
330
+ <Button
331
+ title="Buy Premium"
332
+ onPress={() => {
333
+ signals.event({
334
+ type: 'button_press',
335
+ button: 'buy_premium',
336
+ screen: 'HomeScreen',
337
+ });
338
+ navigation.navigate('Checkout');
339
+ }}
340
+ />
341
+ );
342
+ }
343
+ ```
344
+
345
+ ## Best Practices
346
+
347
+ ### 1. Initialize Once
348
+
349
+ Create a single instance and reuse it:
350
+
351
+ ```typescript
352
+ // lib/signals.ts
353
+ import { Signals } from '@logg/signals';
354
+
355
+ export const signals = new Signals({
356
+ apiKey: process.env.SIGNALS_API_KEY!,
357
+ endpoint: process.env.SIGNALS_ENDPOINT!,
358
+ });
359
+
360
+ // app.tsx
361
+ import { signals } from './lib/signals';
362
+
363
+ signals.event({ type: 'app_opened' });
364
+ ```
365
+
366
+ ### 2. Flush on Exit
367
+
368
+ Always flush events before the app closes:
369
+
370
+ ```typescript
371
+ // React Native
372
+ useEffect(() => {
373
+ return () => {
374
+ signals.flush();
375
+ };
376
+ }, []);
377
+
378
+ // Node.js
379
+ process.on('beforeExit', async () => {
380
+ await signals.flush();
381
+ });
382
+ ```
383
+
384
+ ### 3. Type-safe Events
385
+
386
+ Define your event types for better DX:
387
+
388
+ ```typescript
389
+ type AppEvent =
390
+ | { type: 'page_view'; page: string; title: string }
391
+ | { type: 'button_click'; element: string }
392
+ | { type: 'purchase'; productId: string; amount: number };
393
+
394
+ const signals = new Signals({...});
395
+
396
+ function trackEvent(event: AppEvent) {
397
+ signals.event(event);
398
+ }
399
+
400
+ // Now fully type-safe!
401
+ trackEvent({ type: 'page_view', page: '/home', title: 'Home' });
402
+ ```
403
+
404
+ ### 4. User Identification
405
+
406
+ Include user ID in all events:
407
+
408
+ ```typescript
409
+ function trackUserEvent(event: Omit<EventData, 'userId'>) {
410
+ const userId = getCurrentUserId();
411
+ signals.event({ ...event, userId });
412
+ }
413
+ ```
414
+
415
+ ## License
416
+
417
+ MIT
@@ -0,0 +1,112 @@
1
+ interface EventData {
2
+ type: string;
3
+ [key: string]: unknown;
4
+ }
5
+ interface Event {
6
+ event_id: string;
7
+ timestamp: string;
8
+ session_id: string;
9
+ type: string;
10
+ [key: string]: unknown;
11
+ }
12
+ interface ClientMetadata {
13
+ type: 'web' | 'react-native' | 'node';
14
+ version: string;
15
+ user_agent?: string;
16
+ screen?: {
17
+ width: number;
18
+ height: number;
19
+ };
20
+ locale?: string;
21
+ timezone?: string;
22
+ }
23
+ interface EventBatch {
24
+ api_key: string;
25
+ batch_id: string;
26
+ timestamp: string;
27
+ metadata: ClientMetadata;
28
+ events: Event[];
29
+ }
30
+ interface SignalsConfig {
31
+ apiKey: string;
32
+ endpoint: string;
33
+ batchSize?: number;
34
+ batchInterval?: number;
35
+ maxRetries?: number;
36
+ retryDelay?: number;
37
+ debug?: boolean;
38
+ storage?: StorageAdapter;
39
+ sessionId?: string;
40
+ }
41
+ interface StorageAdapter {
42
+ getItem(key: string): Promise<string | null>;
43
+ setItem(key: string, value: string): Promise<void>;
44
+ removeItem(key: string): Promise<void>;
45
+ }
46
+ interface QueuedEvent {
47
+ event: Event;
48
+ timestamp: number;
49
+ }
50
+
51
+ declare class Signals {
52
+ private config;
53
+ private queue;
54
+ private flushTimer;
55
+ private isDestroyed;
56
+ private isFlushing;
57
+ constructor(config: SignalsConfig);
58
+ private init;
59
+ event(eventData: EventData): Promise<void>;
60
+ flush(): Promise<void>;
61
+ private sendBatch;
62
+ private startFlushTimer;
63
+ private stopFlushTimer;
64
+ getSessionId(): string;
65
+ getQueueSize(): number;
66
+ destroy(): Promise<void>;
67
+ private log;
68
+ }
69
+
70
+ declare class EventQueue {
71
+ private queue;
72
+ private storage;
73
+ private sessionId;
74
+ constructor(storage: StorageAdapter, sessionId: string);
75
+ init(): Promise<void>;
76
+ add(eventData: EventData): Promise<Event>;
77
+ getAll(): Event[];
78
+ getBatch(size: number): Event[];
79
+ remove(count: number): Promise<void>;
80
+ clear(): Promise<void>;
81
+ size(): number;
82
+ isEmpty(): boolean;
83
+ getOldestTimestamp(): number | null;
84
+ private persist;
85
+ }
86
+
87
+ declare class LocalStorageAdapter implements StorageAdapter {
88
+ getItem(key: string): Promise<string | null>;
89
+ setItem(key: string, value: string): Promise<void>;
90
+ removeItem(key: string): Promise<void>;
91
+ static isAvailable(): boolean;
92
+ }
93
+
94
+ declare class AsyncStorageAdapter implements StorageAdapter {
95
+ private asyncStorage;
96
+ constructor();
97
+ getItem(key: string): Promise<string | null>;
98
+ setItem(key: string, value: string): Promise<void>;
99
+ removeItem(key: string): Promise<void>;
100
+ static isAvailable(): boolean;
101
+ }
102
+
103
+ declare class MemoryStorageAdapter implements StorageAdapter {
104
+ private storage;
105
+ getItem(key: string): Promise<string | null>;
106
+ setItem(key: string, value: string): Promise<void>;
107
+ removeItem(key: string): Promise<void>;
108
+ }
109
+
110
+ declare function getDefaultStorageAdapter(): StorageAdapter;
111
+
112
+ export { AsyncStorageAdapter, type ClientMetadata, type Event, type EventBatch, type EventData, EventQueue, LocalStorageAdapter, MemoryStorageAdapter, type QueuedEvent, Signals, type SignalsConfig, type StorageAdapter, getDefaultStorageAdapter };
@@ -0,0 +1,112 @@
1
+ interface EventData {
2
+ type: string;
3
+ [key: string]: unknown;
4
+ }
5
+ interface Event {
6
+ event_id: string;
7
+ timestamp: string;
8
+ session_id: string;
9
+ type: string;
10
+ [key: string]: unknown;
11
+ }
12
+ interface ClientMetadata {
13
+ type: 'web' | 'react-native' | 'node';
14
+ version: string;
15
+ user_agent?: string;
16
+ screen?: {
17
+ width: number;
18
+ height: number;
19
+ };
20
+ locale?: string;
21
+ timezone?: string;
22
+ }
23
+ interface EventBatch {
24
+ api_key: string;
25
+ batch_id: string;
26
+ timestamp: string;
27
+ metadata: ClientMetadata;
28
+ events: Event[];
29
+ }
30
+ interface SignalsConfig {
31
+ apiKey: string;
32
+ endpoint: string;
33
+ batchSize?: number;
34
+ batchInterval?: number;
35
+ maxRetries?: number;
36
+ retryDelay?: number;
37
+ debug?: boolean;
38
+ storage?: StorageAdapter;
39
+ sessionId?: string;
40
+ }
41
+ interface StorageAdapter {
42
+ getItem(key: string): Promise<string | null>;
43
+ setItem(key: string, value: string): Promise<void>;
44
+ removeItem(key: string): Promise<void>;
45
+ }
46
+ interface QueuedEvent {
47
+ event: Event;
48
+ timestamp: number;
49
+ }
50
+
51
+ declare class Signals {
52
+ private config;
53
+ private queue;
54
+ private flushTimer;
55
+ private isDestroyed;
56
+ private isFlushing;
57
+ constructor(config: SignalsConfig);
58
+ private init;
59
+ event(eventData: EventData): Promise<void>;
60
+ flush(): Promise<void>;
61
+ private sendBatch;
62
+ private startFlushTimer;
63
+ private stopFlushTimer;
64
+ getSessionId(): string;
65
+ getQueueSize(): number;
66
+ destroy(): Promise<void>;
67
+ private log;
68
+ }
69
+
70
+ declare class EventQueue {
71
+ private queue;
72
+ private storage;
73
+ private sessionId;
74
+ constructor(storage: StorageAdapter, sessionId: string);
75
+ init(): Promise<void>;
76
+ add(eventData: EventData): Promise<Event>;
77
+ getAll(): Event[];
78
+ getBatch(size: number): Event[];
79
+ remove(count: number): Promise<void>;
80
+ clear(): Promise<void>;
81
+ size(): number;
82
+ isEmpty(): boolean;
83
+ getOldestTimestamp(): number | null;
84
+ private persist;
85
+ }
86
+
87
+ declare class LocalStorageAdapter implements StorageAdapter {
88
+ getItem(key: string): Promise<string | null>;
89
+ setItem(key: string, value: string): Promise<void>;
90
+ removeItem(key: string): Promise<void>;
91
+ static isAvailable(): boolean;
92
+ }
93
+
94
+ declare class AsyncStorageAdapter implements StorageAdapter {
95
+ private asyncStorage;
96
+ constructor();
97
+ getItem(key: string): Promise<string | null>;
98
+ setItem(key: string, value: string): Promise<void>;
99
+ removeItem(key: string): Promise<void>;
100
+ static isAvailable(): boolean;
101
+ }
102
+
103
+ declare class MemoryStorageAdapter implements StorageAdapter {
104
+ private storage;
105
+ getItem(key: string): Promise<string | null>;
106
+ setItem(key: string, value: string): Promise<void>;
107
+ removeItem(key: string): Promise<void>;
108
+ }
109
+
110
+ declare function getDefaultStorageAdapter(): StorageAdapter;
111
+
112
+ export { AsyncStorageAdapter, type ClientMetadata, type Event, type EventBatch, type EventData, EventQueue, LocalStorageAdapter, MemoryStorageAdapter, type QueuedEvent, Signals, type SignalsConfig, type StorageAdapter, getDefaultStorageAdapter };