@yraylabs/boring-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,253 @@
1
+ # boring-analytics
2
+
3
+ Official SDK for [Boring Analytics](https://boringanalytics.io) — lightweight product analytics and error monitoring.
4
+
5
+ - **Zero dependencies** — uses native `fetch` and `crypto`
6
+ - **Works everywhere** — browser, Node.js, Edge runtimes
7
+ - **TypeScript-first** — full type definitions included
8
+ - **Tiny footprint** — tree-shakeable ESM + CJS builds
9
+ - **Auto-batching** — queues events and flushes efficiently
10
+ - **Error monitoring** — captures unhandled errors automatically
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install boring-analytics
16
+ ```
17
+
18
+ ## Quick Start
19
+
20
+ ```typescript
21
+ import { BoringAnalytics } from 'boring-analytics';
22
+
23
+ const analytics = new BoringAnalytics({
24
+ apiKey: 'your-api-key',
25
+ });
26
+
27
+ // Track events
28
+ analytics.track('button_clicked', {
29
+ properties: { buttonId: 'signup', page: '/home' },
30
+ });
31
+
32
+ // Identify users
33
+ analytics.identify('user-123', {
34
+ traits: { email: 'user@example.com', name: 'Jane' },
35
+ });
36
+
37
+ // Track page views
38
+ analytics.page('Home');
39
+ ```
40
+
41
+ ## Configuration
42
+
43
+ ```typescript
44
+ const analytics = new BoringAnalytics({
45
+ // Required
46
+ apiKey: 'your-api-key',
47
+
48
+ // Optional
49
+ endpoint: 'https://api.boringanalytics.io', // API endpoint
50
+ flushAt: 20, // Flush after N queued items (default: 20)
51
+ flushInterval: 5000, // Flush every N ms (default: 5000)
52
+ maxRetries: 3, // Retry failed requests (default: 3)
53
+ debug: false, // Log debug info to console
54
+
55
+ // Auto-capture
56
+ autoCapture: {
57
+ errors: true, // Capture unhandled errors (default: true)
58
+ pageViews: false, // Auto-track SPA navigation (default: false)
59
+ },
60
+
61
+ // Merged into every event
62
+ defaultProperties: {
63
+ appVersion: '1.2.0',
64
+ environment: 'production',
65
+ },
66
+
67
+ // Called on transport errors
68
+ onError: (err) => console.error('Analytics error:', err),
69
+ });
70
+ ```
71
+
72
+ ## API
73
+
74
+ ### `track(name, options?)`
75
+
76
+ Track a named event.
77
+
78
+ ```typescript
79
+ analytics.track('purchase_completed', {
80
+ properties: {
81
+ orderId: 'order-456',
82
+ amount: 99.99,
83
+ currency: 'USD',
84
+ },
85
+ });
86
+ ```
87
+
88
+ ### `identify(userId, options?)`
89
+
90
+ Identify the current user. Sets the userId for all subsequent calls.
91
+
92
+ ```typescript
93
+ analytics.identify('user-123', {
94
+ traits: {
95
+ email: 'user@example.com',
96
+ name: 'Jane Doe',
97
+ plan: 'pro',
98
+ },
99
+ });
100
+ ```
101
+
102
+ ### `page(name?, options?)`
103
+
104
+ Track a page view.
105
+
106
+ ```typescript
107
+ analytics.page('Pricing');
108
+ analytics.page('Product', {
109
+ properties: { productId: 'abc-123' },
110
+ });
111
+ ```
112
+
113
+ ### `screen(name, options?)`
114
+
115
+ Track a screen view (mobile apps).
116
+
117
+ ```typescript
118
+ analytics.screen('Settings');
119
+ ```
120
+
121
+ ### `group(groupId, options?)`
122
+
123
+ Associate the user with a group/organization.
124
+
125
+ ```typescript
126
+ analytics.group('org-456', {
127
+ traits: { name: 'Acme Inc', plan: 'enterprise' },
128
+ });
129
+ ```
130
+
131
+ ### `alias(userId, previousId)`
132
+
133
+ Link two user identities.
134
+
135
+ ```typescript
136
+ analytics.alias('user-123', 'anon-789');
137
+ ```
138
+
139
+ ### `captureError(error, options?)`
140
+
141
+ Manually capture an error.
142
+
143
+ ```typescript
144
+ try {
145
+ await riskyOperation();
146
+ } catch (err) {
147
+ analytics.captureError(err, {
148
+ level: 'ERROR', // DEBUG | INFO | WARNING | ERROR | FATAL
149
+ tags: { component: 'checkout' },
150
+ metadata: { orderId: '123' },
151
+ release: 'v1.2.3',
152
+ handled: true,
153
+ });
154
+ }
155
+ ```
156
+
157
+ ### `flush()`
158
+
159
+ Immediately send all queued events.
160
+
161
+ ```typescript
162
+ await analytics.flush();
163
+ ```
164
+
165
+ ### `reset()`
166
+
167
+ Clear the current user identity. Call on logout.
168
+
169
+ ```typescript
170
+ analytics.reset();
171
+ ```
172
+
173
+ ### `shutdown()`
174
+
175
+ Gracefully shut down — flushes remaining events and removes global error handlers.
176
+
177
+ ```typescript
178
+ await analytics.shutdown();
179
+ ```
180
+
181
+ ## Auto Page View Tracking
182
+
183
+ Enable automatic page view tracking for SPAs:
184
+
185
+ ```typescript
186
+ const analytics = new BoringAnalytics({
187
+ apiKey: 'your-api-key',
188
+ autoCapture: {
189
+ pageViews: true, // Tracks pushState, replaceState, and popstate
190
+ },
191
+ });
192
+ ```
193
+
194
+ ## Error Monitoring
195
+
196
+ Unhandled errors are captured automatically by default. You can also capture errors manually:
197
+
198
+ ```typescript
199
+ // Automatic (enabled by default)
200
+ // window.onerror and unhandledrejection are captured
201
+
202
+ // Manual capture
203
+ analytics.captureError(new Error('Something went wrong'), {
204
+ level: 'WARNING',
205
+ tags: { feature: 'checkout' },
206
+ });
207
+ ```
208
+
209
+ ## Context Enrichment
210
+
211
+ The SDK automatically captures context in the browser:
212
+ - **Browser**: name, version
213
+ - **OS**: name, version
214
+ - **Device**: type (desktop/mobile/tablet)
215
+ - **Page**: URL, referrer, title, path
216
+ - **User Agent** and **Locale**
217
+
218
+ You can override or extend context per-call:
219
+
220
+ ```typescript
221
+ analytics.track('event', {
222
+ context: {
223
+ location: { country: 'US', city: 'San Francisco' },
224
+ },
225
+ });
226
+ ```
227
+
228
+ ## Server-Side Usage (Node.js)
229
+
230
+ ```typescript
231
+ import { BoringAnalytics } from 'boring-analytics';
232
+
233
+ const analytics = new BoringAnalytics({
234
+ apiKey: 'your-server-api-key',
235
+ autoCapture: { errors: false },
236
+ });
237
+
238
+ // Track server-side events
239
+ analytics.identify('user-123');
240
+ analytics.track('api_request', {
241
+ properties: { endpoint: '/users', method: 'GET', duration: 42 },
242
+ });
243
+
244
+ // Flush before process exits
245
+ process.on('SIGTERM', async () => {
246
+ await analytics.shutdown();
247
+ process.exit(0);
248
+ });
249
+ ```
250
+
251
+ ## License
252
+
253
+ MIT
@@ -0,0 +1,190 @@
1
+ interface BoringAnalyticsConfig {
2
+ apiKey: string;
3
+ endpoint?: string;
4
+ /** Max events to batch before auto-flushing (default: 20) */
5
+ flushAt?: number;
6
+ /** Interval in ms between auto-flushes (default: 5000) */
7
+ flushInterval?: number;
8
+ /** Max retry attempts for failed requests (default: 3) */
9
+ maxRetries?: number;
10
+ /** Auto-capture settings */
11
+ autoCapture?: {
12
+ /** Capture unhandled errors and promise rejections (default: true) */
13
+ errors?: boolean;
14
+ /** Auto-track page views on navigation — browser only (default: false) */
15
+ pageViews?: boolean;
16
+ };
17
+ /** Default properties merged into every event */
18
+ defaultProperties?: Record<string, unknown>;
19
+ /** Called on transport errors */
20
+ onError?: (error: Error) => void;
21
+ /** Enable debug logging to console (default: false) */
22
+ debug?: boolean;
23
+ }
24
+ type EventType = 'TRACK' | 'IDENTIFY' | 'PAGE' | 'SCREEN' | 'GROUP' | 'ALIAS';
25
+ type ErrorLevel = 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'FATAL';
26
+ interface EventContext {
27
+ ip?: string;
28
+ userAgent?: string;
29
+ locale?: string;
30
+ page?: {
31
+ url?: string;
32
+ referrer?: string;
33
+ title?: string;
34
+ path?: string;
35
+ };
36
+ device?: {
37
+ type?: string;
38
+ };
39
+ os?: {
40
+ name?: string;
41
+ version?: string;
42
+ };
43
+ browser?: {
44
+ name?: string;
45
+ version?: string;
46
+ };
47
+ location?: {
48
+ country?: string;
49
+ city?: string;
50
+ region?: string;
51
+ };
52
+ [key: string]: unknown;
53
+ }
54
+ interface IngestEventPayload {
55
+ type?: EventType;
56
+ name: string;
57
+ properties?: Record<string, unknown>;
58
+ timestamp?: string;
59
+ userId?: string;
60
+ sessionId?: string;
61
+ anonymousId?: string;
62
+ context?: EventContext;
63
+ }
64
+ interface IngestErrorPayload {
65
+ type: string;
66
+ message: string;
67
+ stackTrace?: string;
68
+ level?: ErrorLevel;
69
+ fingerprint?: string;
70
+ handled?: boolean;
71
+ metadata?: Record<string, unknown>;
72
+ tags?: Record<string, string>;
73
+ timestamp?: string;
74
+ userId?: string;
75
+ sessionId?: string;
76
+ context?: {
77
+ ip?: string;
78
+ userAgent?: string;
79
+ browser?: string;
80
+ os?: string;
81
+ device?: string;
82
+ url?: string;
83
+ release?: string;
84
+ [key: string]: unknown;
85
+ };
86
+ }
87
+ interface TrackOptions {
88
+ properties?: Record<string, unknown>;
89
+ timestamp?: Date;
90
+ context?: Partial<EventContext>;
91
+ }
92
+ interface IdentifyOptions {
93
+ traits?: Record<string, unknown>;
94
+ timestamp?: Date;
95
+ context?: Partial<EventContext>;
96
+ }
97
+ interface PageOptions {
98
+ properties?: Record<string, unknown>;
99
+ timestamp?: Date;
100
+ context?: Partial<EventContext>;
101
+ }
102
+ interface ScreenOptions {
103
+ properties?: Record<string, unknown>;
104
+ timestamp?: Date;
105
+ context?: Partial<EventContext>;
106
+ }
107
+ interface GroupOptions {
108
+ traits?: Record<string, unknown>;
109
+ timestamp?: Date;
110
+ context?: Partial<EventContext>;
111
+ }
112
+ interface CaptureErrorOptions {
113
+ level?: ErrorLevel;
114
+ metadata?: Record<string, unknown>;
115
+ tags?: Record<string, string>;
116
+ fingerprint?: string;
117
+ handled?: boolean;
118
+ timestamp?: Date;
119
+ release?: string;
120
+ }
121
+
122
+ declare class BoringAnalytics {
123
+ private queue;
124
+ private session;
125
+ private errorHandler;
126
+ private debug;
127
+ private config;
128
+ private pageViewUnsubscribe;
129
+ constructor(config: BoringAnalyticsConfig);
130
+ /**
131
+ * Track a named event.
132
+ *
133
+ * @example
134
+ * analytics.track('button_clicked', { properties: { buttonId: 'signup' } });
135
+ */
136
+ track(name: string, options?: TrackOptions): void;
137
+ /**
138
+ * Identify a user with traits.
139
+ * Sets the userId for all subsequent calls.
140
+ *
141
+ * @example
142
+ * analytics.identify('user-123', { traits: { email: 'user@example.com' } });
143
+ */
144
+ identify(userId: string, options?: IdentifyOptions): void;
145
+ /**
146
+ * Track a page view (typically browser).
147
+ *
148
+ * @example
149
+ * analytics.page('Home');
150
+ * analytics.page('Product', { properties: { productId: '123' } });
151
+ */
152
+ page(name?: string, options?: PageOptions): void;
153
+ /**
154
+ * Track a screen view (typically mobile).
155
+ */
156
+ screen(name: string, options?: ScreenOptions): void;
157
+ /**
158
+ * Associate a user with a group.
159
+ */
160
+ group(groupId: string, options?: GroupOptions): void;
161
+ /**
162
+ * Create an alias between two user identities.
163
+ */
164
+ alias(userId: string, previousId: string): void;
165
+ /**
166
+ * Capture an error for error monitoring.
167
+ *
168
+ * @example
169
+ * try { riskyOp(); }
170
+ * catch (err) { analytics.captureError(err, { tags: { env: 'prod' } }); }
171
+ */
172
+ captureError(error: Error | string, options?: CaptureErrorOptions): void;
173
+ /**
174
+ * Flush all queued events and errors immediately.
175
+ */
176
+ flush(): Promise<void>;
177
+ /**
178
+ * Reset the current user. Clears userId, generates new anonymousId and sessionId.
179
+ * Call this on logout.
180
+ */
181
+ reset(): void;
182
+ /**
183
+ * Gracefully shut down: flush remaining events, remove global handlers.
184
+ */
185
+ shutdown(): Promise<void>;
186
+ private enqueueEvent;
187
+ private installPageViewTracking;
188
+ }
189
+
190
+ export { BoringAnalytics, type BoringAnalyticsConfig, type CaptureErrorOptions, type ErrorLevel, type EventContext, type EventType, type GroupOptions, type IdentifyOptions, type IngestErrorPayload, type IngestEventPayload, type PageOptions, type ScreenOptions, type TrackOptions };
@@ -0,0 +1,190 @@
1
+ interface BoringAnalyticsConfig {
2
+ apiKey: string;
3
+ endpoint?: string;
4
+ /** Max events to batch before auto-flushing (default: 20) */
5
+ flushAt?: number;
6
+ /** Interval in ms between auto-flushes (default: 5000) */
7
+ flushInterval?: number;
8
+ /** Max retry attempts for failed requests (default: 3) */
9
+ maxRetries?: number;
10
+ /** Auto-capture settings */
11
+ autoCapture?: {
12
+ /** Capture unhandled errors and promise rejections (default: true) */
13
+ errors?: boolean;
14
+ /** Auto-track page views on navigation — browser only (default: false) */
15
+ pageViews?: boolean;
16
+ };
17
+ /** Default properties merged into every event */
18
+ defaultProperties?: Record<string, unknown>;
19
+ /** Called on transport errors */
20
+ onError?: (error: Error) => void;
21
+ /** Enable debug logging to console (default: false) */
22
+ debug?: boolean;
23
+ }
24
+ type EventType = 'TRACK' | 'IDENTIFY' | 'PAGE' | 'SCREEN' | 'GROUP' | 'ALIAS';
25
+ type ErrorLevel = 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'FATAL';
26
+ interface EventContext {
27
+ ip?: string;
28
+ userAgent?: string;
29
+ locale?: string;
30
+ page?: {
31
+ url?: string;
32
+ referrer?: string;
33
+ title?: string;
34
+ path?: string;
35
+ };
36
+ device?: {
37
+ type?: string;
38
+ };
39
+ os?: {
40
+ name?: string;
41
+ version?: string;
42
+ };
43
+ browser?: {
44
+ name?: string;
45
+ version?: string;
46
+ };
47
+ location?: {
48
+ country?: string;
49
+ city?: string;
50
+ region?: string;
51
+ };
52
+ [key: string]: unknown;
53
+ }
54
+ interface IngestEventPayload {
55
+ type?: EventType;
56
+ name: string;
57
+ properties?: Record<string, unknown>;
58
+ timestamp?: string;
59
+ userId?: string;
60
+ sessionId?: string;
61
+ anonymousId?: string;
62
+ context?: EventContext;
63
+ }
64
+ interface IngestErrorPayload {
65
+ type: string;
66
+ message: string;
67
+ stackTrace?: string;
68
+ level?: ErrorLevel;
69
+ fingerprint?: string;
70
+ handled?: boolean;
71
+ metadata?: Record<string, unknown>;
72
+ tags?: Record<string, string>;
73
+ timestamp?: string;
74
+ userId?: string;
75
+ sessionId?: string;
76
+ context?: {
77
+ ip?: string;
78
+ userAgent?: string;
79
+ browser?: string;
80
+ os?: string;
81
+ device?: string;
82
+ url?: string;
83
+ release?: string;
84
+ [key: string]: unknown;
85
+ };
86
+ }
87
+ interface TrackOptions {
88
+ properties?: Record<string, unknown>;
89
+ timestamp?: Date;
90
+ context?: Partial<EventContext>;
91
+ }
92
+ interface IdentifyOptions {
93
+ traits?: Record<string, unknown>;
94
+ timestamp?: Date;
95
+ context?: Partial<EventContext>;
96
+ }
97
+ interface PageOptions {
98
+ properties?: Record<string, unknown>;
99
+ timestamp?: Date;
100
+ context?: Partial<EventContext>;
101
+ }
102
+ interface ScreenOptions {
103
+ properties?: Record<string, unknown>;
104
+ timestamp?: Date;
105
+ context?: Partial<EventContext>;
106
+ }
107
+ interface GroupOptions {
108
+ traits?: Record<string, unknown>;
109
+ timestamp?: Date;
110
+ context?: Partial<EventContext>;
111
+ }
112
+ interface CaptureErrorOptions {
113
+ level?: ErrorLevel;
114
+ metadata?: Record<string, unknown>;
115
+ tags?: Record<string, string>;
116
+ fingerprint?: string;
117
+ handled?: boolean;
118
+ timestamp?: Date;
119
+ release?: string;
120
+ }
121
+
122
+ declare class BoringAnalytics {
123
+ private queue;
124
+ private session;
125
+ private errorHandler;
126
+ private debug;
127
+ private config;
128
+ private pageViewUnsubscribe;
129
+ constructor(config: BoringAnalyticsConfig);
130
+ /**
131
+ * Track a named event.
132
+ *
133
+ * @example
134
+ * analytics.track('button_clicked', { properties: { buttonId: 'signup' } });
135
+ */
136
+ track(name: string, options?: TrackOptions): void;
137
+ /**
138
+ * Identify a user with traits.
139
+ * Sets the userId for all subsequent calls.
140
+ *
141
+ * @example
142
+ * analytics.identify('user-123', { traits: { email: 'user@example.com' } });
143
+ */
144
+ identify(userId: string, options?: IdentifyOptions): void;
145
+ /**
146
+ * Track a page view (typically browser).
147
+ *
148
+ * @example
149
+ * analytics.page('Home');
150
+ * analytics.page('Product', { properties: { productId: '123' } });
151
+ */
152
+ page(name?: string, options?: PageOptions): void;
153
+ /**
154
+ * Track a screen view (typically mobile).
155
+ */
156
+ screen(name: string, options?: ScreenOptions): void;
157
+ /**
158
+ * Associate a user with a group.
159
+ */
160
+ group(groupId: string, options?: GroupOptions): void;
161
+ /**
162
+ * Create an alias between two user identities.
163
+ */
164
+ alias(userId: string, previousId: string): void;
165
+ /**
166
+ * Capture an error for error monitoring.
167
+ *
168
+ * @example
169
+ * try { riskyOp(); }
170
+ * catch (err) { analytics.captureError(err, { tags: { env: 'prod' } }); }
171
+ */
172
+ captureError(error: Error | string, options?: CaptureErrorOptions): void;
173
+ /**
174
+ * Flush all queued events and errors immediately.
175
+ */
176
+ flush(): Promise<void>;
177
+ /**
178
+ * Reset the current user. Clears userId, generates new anonymousId and sessionId.
179
+ * Call this on logout.
180
+ */
181
+ reset(): void;
182
+ /**
183
+ * Gracefully shut down: flush remaining events, remove global handlers.
184
+ */
185
+ shutdown(): Promise<void>;
186
+ private enqueueEvent;
187
+ private installPageViewTracking;
188
+ }
189
+
190
+ export { BoringAnalytics, type BoringAnalyticsConfig, type CaptureErrorOptions, type ErrorLevel, type EventContext, type EventType, type GroupOptions, type IdentifyOptions, type IngestErrorPayload, type IngestEventPayload, type PageOptions, type ScreenOptions, type TrackOptions };