@thisbefine/analytics 0.2.1 → 0.3.1
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 +161 -3
- package/dist/core/analytics.d.ts +44 -2
- package/dist/core/analytics.d.ts.map +1 -1
- package/dist/core/analytics.js +154 -17
- package/dist/core/analytics.js.map +1 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +3 -2
- package/dist/core/errors.js.map +1 -1
- package/dist/core/logger.d.ts +2 -1
- package/dist/core/logger.d.ts.map +1 -1
- package/dist/core/logger.js +53 -1
- package/dist/core/logger.js.map +1 -1
- package/dist/core/privacy.d.ts +50 -2
- package/dist/core/privacy.d.ts.map +1 -1
- package/dist/core/privacy.js +118 -1
- package/dist/core/privacy.js.map +1 -1
- package/dist/core/queue.d.ts +76 -3
- package/dist/core/queue.d.ts.map +1 -1
- package/dist/core/queue.js +353 -40
- package/dist/core/queue.js.map +1 -1
- package/dist/core/session.d.ts +5 -3
- package/dist/core/session.d.ts.map +1 -1
- package/dist/core/session.js +20 -5
- package/dist/core/session.js.map +1 -1
- package/dist/core/types.d.ts +150 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +10 -0
- package/dist/core/types.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/next.d.ts +2 -0
- package/dist/next.d.ts.map +1 -1
- package/dist/next.js +2 -3
- package/dist/next.js.map +1 -1
- package/dist/react/context.d.ts +95 -0
- package/dist/react/context.d.ts.map +1 -0
- package/dist/react/context.js +120 -0
- package/dist/react/context.js.map +1 -0
- package/dist/react/hooks.d.ts.map +1 -1
- package/dist/react/hooks.js +38 -34
- package/dist/react/hooks.js.map +1 -1
- package/dist/react.d.ts +2 -0
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +1 -0
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -169,6 +169,38 @@ analytics.log('Cache miss for user preferences', 'debug', {
|
|
|
169
169
|
});
|
|
170
170
|
```
|
|
171
171
|
|
|
172
|
+
### Lifecycle Events
|
|
173
|
+
|
|
174
|
+
Track key SaaS milestones with built-in methods. These use reserved event names (prefixed with `$`) and typed properties for consistent analytics.
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// User lifecycle
|
|
178
|
+
analytics.signup({ method: 'google', plan: 'free' });
|
|
179
|
+
analytics.login({ method: 'passkey', isNewDevice: true });
|
|
180
|
+
analytics.logout({ reason: 'manual' });
|
|
181
|
+
analytics.accountDeleted({ reason: 'not_using', tenure: 45 });
|
|
182
|
+
|
|
183
|
+
// Subscription events
|
|
184
|
+
analytics.subscriptionStarted({ plan: 'pro', interval: 'monthly', mrr: 29 });
|
|
185
|
+
analytics.subscriptionCancelled({ plan: 'pro', reason: 'too_expensive', mrr: 29 });
|
|
186
|
+
analytics.subscriptionRenewed({ plan: 'pro', renewalCount: 12 });
|
|
187
|
+
|
|
188
|
+
// Plan changes
|
|
189
|
+
analytics.planUpgraded({ fromPlan: 'starter', toPlan: 'pro', mrrChange: 20 });
|
|
190
|
+
analytics.planDowngraded({ fromPlan: 'pro', toPlan: 'starter', reason: 'budget' });
|
|
191
|
+
|
|
192
|
+
// Trials
|
|
193
|
+
analytics.trialStarted({ plan: 'pro', trialDays: 14 });
|
|
194
|
+
analytics.trialEnded({ plan: 'pro', converted: true });
|
|
195
|
+
|
|
196
|
+
// Team/collaboration
|
|
197
|
+
analytics.inviteSent({ inviteEmail: 'teammate@company.com', role: 'editor' });
|
|
198
|
+
analytics.inviteAccepted({ invitedBy: 'user_123', role: 'editor' });
|
|
199
|
+
|
|
200
|
+
// Feature adoption
|
|
201
|
+
analytics.featureActivated({ feature: 'dark_mode', isFirstTime: true });
|
|
202
|
+
```
|
|
203
|
+
|
|
172
204
|
### Privacy Controls
|
|
173
205
|
|
|
174
206
|
Because being creepy is bad for business.
|
|
@@ -187,6 +219,36 @@ if (analytics.isOptedOut()) {
|
|
|
187
219
|
analytics.optIn();
|
|
188
220
|
```
|
|
189
221
|
|
|
222
|
+
### Consent Management
|
|
223
|
+
|
|
224
|
+
Granular control over tracking categories for GDPR/CCPA compliance.
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
// Configure consent categories on init
|
|
228
|
+
const analytics = createAnalytics({
|
|
229
|
+
apiKey: 'tbf_xxx',
|
|
230
|
+
consent: {
|
|
231
|
+
categories: ['analytics', 'marketing', 'functional'],
|
|
232
|
+
defaultConsent: false, // Don't track until explicit consent
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Check if a category is enabled
|
|
237
|
+
if (analytics.hasConsent('marketing')) {
|
|
238
|
+
// Show personalized content
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Get all consented categories
|
|
242
|
+
const categories = analytics.getConsentedCategories();
|
|
243
|
+
|
|
244
|
+
// Set consent (replaces current consent)
|
|
245
|
+
analytics.setConsent(['analytics', 'functional']);
|
|
246
|
+
|
|
247
|
+
// Grant/revoke individual categories
|
|
248
|
+
analytics.grantConsent('marketing');
|
|
249
|
+
analytics.revokeConsent('marketing');
|
|
250
|
+
```
|
|
251
|
+
|
|
190
252
|
## React Hooks
|
|
191
253
|
|
|
192
254
|
All hooks are available from both `@thisbefine/analytics/react` and `@thisbefine/analytics/next`.
|
|
@@ -289,9 +351,10 @@ const analytics = createAnalytics({
|
|
|
289
351
|
// Required
|
|
290
352
|
apiKey: 'tbf_xxx',
|
|
291
353
|
|
|
292
|
-
// Optional
|
|
354
|
+
// Optional - Basic
|
|
293
355
|
host: 'https://thisbefine.com', // API endpoint
|
|
294
356
|
debug: false, // Console logging
|
|
357
|
+
structuredLogging: false, // Output debug logs as JSON
|
|
295
358
|
flushAt: 20, // Batch size before sending
|
|
296
359
|
flushInterval: 10000, // Ms between flushes
|
|
297
360
|
sessionTimeout: 1800000, // 30 min session timeout
|
|
@@ -299,12 +362,46 @@ const analytics = createAnalytics({
|
|
|
299
362
|
respectDNT: true, // Honor Do Not Track
|
|
300
363
|
maxRetries: 3, // Retry failed requests
|
|
301
364
|
|
|
302
|
-
// Error tracking
|
|
365
|
+
// Error tracking
|
|
303
366
|
errors: {
|
|
304
367
|
enabled: true,
|
|
305
368
|
captureUnhandled: true,
|
|
306
369
|
captureConsoleErrors: true,
|
|
307
370
|
},
|
|
371
|
+
|
|
372
|
+
// Lifecycle hooks
|
|
373
|
+
onFlushError: (error, failedEvents) => {
|
|
374
|
+
console.error('Failed to send events:', error);
|
|
375
|
+
// Log to your error tracking service
|
|
376
|
+
},
|
|
377
|
+
beforeSend: (event) => {
|
|
378
|
+
// Scrub PII from event properties
|
|
379
|
+
if (event.type === 'track' && event.properties?.email) {
|
|
380
|
+
const { email, ...rest } = event.properties;
|
|
381
|
+
return { ...event, properties: rest };
|
|
382
|
+
}
|
|
383
|
+
// Return null to discard the event
|
|
384
|
+
return event;
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
// Queue persistence (crash recovery)
|
|
388
|
+
persistQueue: true, // Save queue to localStorage
|
|
389
|
+
maxPersistedEvents: 1000, // Max events to persist
|
|
390
|
+
|
|
391
|
+
// Privacy & consent
|
|
392
|
+
anonymousIdMaxAge: 2592000000, // Rotate anonymous ID every 30 days
|
|
393
|
+
consent: {
|
|
394
|
+
categories: ['analytics', 'marketing', 'functional'],
|
|
395
|
+
defaultConsent: true, // Track by default (respects DNT)
|
|
396
|
+
},
|
|
397
|
+
|
|
398
|
+
// Rate limiting & sampling
|
|
399
|
+
maxEventsPerSecond: 100, // Drop events exceeding this rate
|
|
400
|
+
sampleRate: 1, // 1 = 100%, 0.5 = 50% of events
|
|
401
|
+
|
|
402
|
+
// Circuit breaker (stops hammering failed servers)
|
|
403
|
+
circuitBreakerThreshold: 5, // Open after N consecutive failures
|
|
404
|
+
circuitBreakerResetTimeout: 30000, // Try again after 30s
|
|
308
405
|
});
|
|
309
406
|
```
|
|
310
407
|
|
|
@@ -334,12 +431,44 @@ const analytics = createAnalytics({
|
|
|
334
431
|
| `page(name?, properties?)` | Track a page view |
|
|
335
432
|
| `group(accountId, traits?)` | Associate user with account |
|
|
336
433
|
| `reset()` | Clear user data (call on logout) |
|
|
337
|
-
| `flush()` | Force send queued events |
|
|
434
|
+
| `flush()` | Force send queued events (returns `FlushResult`) |
|
|
435
|
+
| `destroy()` | Clean up resources, flush remaining events |
|
|
338
436
|
| `optOut()` | Disable tracking |
|
|
339
437
|
| `optIn()` | Re-enable tracking |
|
|
340
438
|
| `isOptedOut()` | Check opt-out status |
|
|
341
439
|
| `getUser()` | Get current user state |
|
|
342
440
|
|
|
441
|
+
### Consent Methods
|
|
442
|
+
|
|
443
|
+
| Method | Description |
|
|
444
|
+
|--------|-------------|
|
|
445
|
+
| `hasConsent(category)` | Check if a category is enabled |
|
|
446
|
+
| `getConsentedCategories()` | Get all consented categories |
|
|
447
|
+
| `setConsent(categories)` | Set consent (replaces current) |
|
|
448
|
+
| `grantConsent(category)` | Enable a category |
|
|
449
|
+
| `revokeConsent(category)` | Disable a category |
|
|
450
|
+
|
|
451
|
+
Categories: `analytics` | `marketing` | `functional`
|
|
452
|
+
|
|
453
|
+
### Lifecycle Methods
|
|
454
|
+
|
|
455
|
+
| Method | Description |
|
|
456
|
+
|--------|-------------|
|
|
457
|
+
| `signup(props?)` | Track user signup |
|
|
458
|
+
| `login(props?)` | Track user login |
|
|
459
|
+
| `logout(props?)` | Track user logout |
|
|
460
|
+
| `accountDeleted(props?)` | Track account deletion |
|
|
461
|
+
| `subscriptionStarted(props)` | Track subscription start |
|
|
462
|
+
| `subscriptionCancelled(props)` | Track subscription cancellation |
|
|
463
|
+
| `subscriptionRenewed(props)` | Track subscription renewal |
|
|
464
|
+
| `planUpgraded(props)` | Track plan upgrade |
|
|
465
|
+
| `planDowngraded(props)` | Track plan downgrade |
|
|
466
|
+
| `trialStarted(props)` | Track trial start |
|
|
467
|
+
| `trialEnded(props)` | Track trial end |
|
|
468
|
+
| `inviteSent(props?)` | Track invite sent |
|
|
469
|
+
| `inviteAccepted(props?)` | Track invite accepted |
|
|
470
|
+
| `featureActivated(props)` | Track feature activation |
|
|
471
|
+
|
|
343
472
|
### Error Tracking Methods
|
|
344
473
|
|
|
345
474
|
| Method | Description |
|
|
@@ -376,11 +505,40 @@ Full type definitions included:
|
|
|
376
505
|
|
|
377
506
|
```typescript
|
|
378
507
|
import type {
|
|
508
|
+
// Core types
|
|
379
509
|
Analytics,
|
|
380
510
|
AnalyticsConfig,
|
|
381
511
|
UserState,
|
|
382
512
|
UserTraits,
|
|
383
513
|
AccountTraits,
|
|
514
|
+
FlushResult,
|
|
515
|
+
|
|
516
|
+
// Event types
|
|
517
|
+
AnalyticsEvent,
|
|
518
|
+
TrackEvent,
|
|
519
|
+
IdentifyEvent,
|
|
520
|
+
PageEvent,
|
|
521
|
+
GroupEvent,
|
|
522
|
+
|
|
523
|
+
// Hooks
|
|
524
|
+
BeforeSend,
|
|
525
|
+
OnFlushError,
|
|
526
|
+
|
|
527
|
+
// Lifecycle event props
|
|
528
|
+
SignupProps,
|
|
529
|
+
LoginProps,
|
|
530
|
+
LogoutProps,
|
|
531
|
+
AccountDeletedProps,
|
|
532
|
+
SubscriptionStartedProps,
|
|
533
|
+
SubscriptionCancelledProps,
|
|
534
|
+
SubscriptionRenewedProps,
|
|
535
|
+
PlanUpgradedProps,
|
|
536
|
+
PlanDowngradedProps,
|
|
537
|
+
TrialStartedProps,
|
|
538
|
+
TrialEndedProps,
|
|
539
|
+
InviteSentProps,
|
|
540
|
+
InviteAcceptedProps,
|
|
541
|
+
FeatureActivatedProps,
|
|
384
542
|
} from '@thisbefine/analytics';
|
|
385
543
|
```
|
|
386
544
|
|
package/dist/core/analytics.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AccountDeletedProps, type FeatureActivatedProps, type InviteAcceptedProps, type InviteSentProps, type LoginProps, type LogoutProps, type PlanDowngradedProps, type PlanUpgradedProps, type SignupProps, type SubscriptionCancelledProps, type SubscriptionRenewedProps, type SubscriptionStartedProps, type TrialEndedProps, type TrialStartedProps } from "./lifecycle";
|
|
2
2
|
import type { LogLevel } from "./logging";
|
|
3
|
-
import type { AccountTraits, Analytics, AnalyticsConfig, UserState, UserTraits } from "./types";
|
|
3
|
+
import type { AccountTraits, Analytics, AnalyticsConfig, ConsentCategory, FlushResult, UserState, UserTraits } from "./types";
|
|
4
4
|
/**
|
|
5
5
|
* Main Analytics class - the primary entry point for the SDK
|
|
6
6
|
*
|
|
@@ -22,11 +22,26 @@ export declare class AnalyticsImpl implements Analytics {
|
|
|
22
22
|
private errorCapture;
|
|
23
23
|
private initialized;
|
|
24
24
|
private logger;
|
|
25
|
+
private eventTimestamps;
|
|
26
|
+
private rateLimitWarned;
|
|
25
27
|
constructor(config: AnalyticsConfig);
|
|
26
28
|
/**
|
|
27
29
|
* Resolve configuration with defaults
|
|
28
30
|
*/
|
|
29
31
|
private resolveConfig;
|
|
32
|
+
/**
|
|
33
|
+
* Queue an event, applying sampling, rate limiting and beforeSend hook if configured
|
|
34
|
+
*/
|
|
35
|
+
private queueEvent;
|
|
36
|
+
/**
|
|
37
|
+
* Check if event should be sampled based on sampleRate
|
|
38
|
+
*/
|
|
39
|
+
private shouldSample;
|
|
40
|
+
/**
|
|
41
|
+
* Check if event should be allowed based on rate limit
|
|
42
|
+
* Uses a sliding window algorithm
|
|
43
|
+
*/
|
|
44
|
+
private checkRateLimit;
|
|
30
45
|
/**
|
|
31
46
|
* Track a custom event
|
|
32
47
|
*/
|
|
@@ -69,8 +84,15 @@ export declare class AnalyticsImpl implements Analytics {
|
|
|
69
84
|
reset(): void;
|
|
70
85
|
/**
|
|
71
86
|
* Manually flush the event queue
|
|
87
|
+
* @returns FlushResult with success status, event count, and any errors
|
|
88
|
+
*/
|
|
89
|
+
flush(): Promise<FlushResult>;
|
|
90
|
+
/**
|
|
91
|
+
* Destroy the analytics instance, cleaning up all resources.
|
|
92
|
+
* Flushes remaining events before cleanup.
|
|
93
|
+
* After calling this, the instance should not be used.
|
|
72
94
|
*/
|
|
73
|
-
|
|
95
|
+
destroy(): Promise<void>;
|
|
74
96
|
/**
|
|
75
97
|
* Opt out of tracking
|
|
76
98
|
*/
|
|
@@ -83,6 +105,26 @@ export declare class AnalyticsImpl implements Analytics {
|
|
|
83
105
|
* Check if user has opted out
|
|
84
106
|
*/
|
|
85
107
|
isOptedOut(): boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Check if a specific consent category is enabled
|
|
110
|
+
*/
|
|
111
|
+
hasConsent(category: ConsentCategory): boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Get all currently consented categories
|
|
114
|
+
*/
|
|
115
|
+
getConsentedCategories(): ConsentCategory[];
|
|
116
|
+
/**
|
|
117
|
+
* Set consent for specific categories
|
|
118
|
+
*/
|
|
119
|
+
setConsent(categories: ConsentCategory[]): void;
|
|
120
|
+
/**
|
|
121
|
+
* Grant consent for a specific category
|
|
122
|
+
*/
|
|
123
|
+
grantConsent(category: ConsentCategory): void;
|
|
124
|
+
/**
|
|
125
|
+
* Revoke consent for a specific category
|
|
126
|
+
*/
|
|
127
|
+
revokeConsent(category: ConsentCategory): void;
|
|
86
128
|
/**
|
|
87
129
|
* Get current user state
|
|
88
130
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../../src/core/analytics.ts"],"names":[],"mappings":"AACA,OAAO,EACN,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,eAAe,EAEpB,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,0BAA0B,EAC/B,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,EAC7B,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAM1C,OAAO,KAAK,EACX,aAAa,EACb,SAAS,EACT,eAAe,
|
|
1
|
+
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../../src/core/analytics.ts"],"names":[],"mappings":"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"}
|
package/dist/core/analytics.js
CHANGED
|
@@ -5,7 +5,7 @@ import { log as sendLog } from "./logging";
|
|
|
5
5
|
import { Privacy } from "./privacy";
|
|
6
6
|
import { Queue } from "./queue";
|
|
7
7
|
import { Session } from "./session";
|
|
8
|
-
import { Storage } from "./storage";
|
|
8
|
+
import { generateId, Storage } from "./storage";
|
|
9
9
|
import { DEFAULT_CONFIG, LIBRARY_INFO } from "./types";
|
|
10
10
|
import { validateAccountId, validateEventName, validateProperties, validateUserId, } from "./validation";
|
|
11
11
|
/**
|
|
@@ -24,11 +24,13 @@ export class AnalyticsImpl {
|
|
|
24
24
|
constructor(config) {
|
|
25
25
|
this.errorCapture = null;
|
|
26
26
|
this.initialized = false;
|
|
27
|
+
this.eventTimestamps = [];
|
|
28
|
+
this.rateLimitWarned = false;
|
|
27
29
|
this.config = this.resolveConfig(config);
|
|
28
|
-
this.logger = createLogger("Analytics", this.config.debug);
|
|
30
|
+
this.logger = createLogger("Analytics", this.config.debug, this.config.structuredLogging);
|
|
29
31
|
this.storage = new Storage(this.config.cookieDomain);
|
|
30
|
-
this.session = new Session(this.storage, this.config.sessionTimeout, this.config.debug);
|
|
31
|
-
this.privacy = new Privacy(this.storage, this.config.respectDNT, this.config.debug);
|
|
32
|
+
this.session = new Session(this.storage, this.config.sessionTimeout, this.config.debug, this.config.anonymousIdMaxAge);
|
|
33
|
+
this.privacy = new Privacy(this.storage, this.config.respectDNT, this.config.debug, this.config.consent);
|
|
32
34
|
this.queue = new Queue(this.config);
|
|
33
35
|
if (this.config.errors?.enabled !== false) {
|
|
34
36
|
this.errorCapture = new ErrorCapture(this.config, this.session, this.config.errors);
|
|
@@ -57,8 +59,84 @@ export class AnalyticsImpl {
|
|
|
57
59
|
respectDNT: config.respectDNT ?? DEFAULT_CONFIG.respectDNT,
|
|
58
60
|
maxRetries: config.maxRetries ?? DEFAULT_CONFIG.maxRetries,
|
|
59
61
|
errors: config.errors,
|
|
62
|
+
onFlushError: config.onFlushError,
|
|
63
|
+
persistQueue: config.persistQueue ?? DEFAULT_CONFIG.persistQueue,
|
|
64
|
+
maxPersistedEvents: config.maxPersistedEvents ?? DEFAULT_CONFIG.maxPersistedEvents,
|
|
65
|
+
beforeSend: config.beforeSend,
|
|
66
|
+
anonymousIdMaxAge: config.anonymousIdMaxAge ?? DEFAULT_CONFIG.anonymousIdMaxAge,
|
|
67
|
+
circuitBreakerThreshold: config.circuitBreakerThreshold ??
|
|
68
|
+
DEFAULT_CONFIG.circuitBreakerThreshold,
|
|
69
|
+
circuitBreakerResetTimeout: config.circuitBreakerResetTimeout ??
|
|
70
|
+
DEFAULT_CONFIG.circuitBreakerResetTimeout,
|
|
71
|
+
consent: config.consent,
|
|
72
|
+
maxEventsPerSecond: config.maxEventsPerSecond ?? DEFAULT_CONFIG.maxEventsPerSecond,
|
|
73
|
+
sampleRate: Math.max(0, Math.min(1, config.sampleRate ?? DEFAULT_CONFIG.sampleRate)),
|
|
74
|
+
structuredLogging: config.structuredLogging ?? DEFAULT_CONFIG.structuredLogging,
|
|
60
75
|
};
|
|
61
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Queue an event, applying sampling, rate limiting and beforeSend hook if configured
|
|
79
|
+
*/
|
|
80
|
+
queueEvent(event) {
|
|
81
|
+
if (!this.shouldSample()) {
|
|
82
|
+
this.logger.log("Event dropped by sampling");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (!this.checkRateLimit()) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
let finalEvent = event;
|
|
89
|
+
if (this.config.beforeSend) {
|
|
90
|
+
try {
|
|
91
|
+
finalEvent = this.config.beforeSend(event);
|
|
92
|
+
if (finalEvent === null) {
|
|
93
|
+
this.logger.log("Event discarded by beforeSend hook:", event.type);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
this.logger.warn("beforeSend hook threw an error:", error);
|
|
99
|
+
finalEvent = event;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
this.queue.push(finalEvent);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Check if event should be sampled based on sampleRate
|
|
106
|
+
*/
|
|
107
|
+
shouldSample() {
|
|
108
|
+
const sampleRate = this.config.sampleRate;
|
|
109
|
+
if (sampleRate >= 1)
|
|
110
|
+
return true;
|
|
111
|
+
if (sampleRate <= 0)
|
|
112
|
+
return false;
|
|
113
|
+
return Math.random() < sampleRate;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Check if event should be allowed based on rate limit
|
|
117
|
+
* Uses a sliding window algorithm
|
|
118
|
+
*/
|
|
119
|
+
checkRateLimit() {
|
|
120
|
+
const maxEvents = this.config.maxEventsPerSecond;
|
|
121
|
+
if (maxEvents <= 0) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
const windowStart = now - 1000;
|
|
126
|
+
this.eventTimestamps = this.eventTimestamps.filter((t) => t > windowStart);
|
|
127
|
+
if (this.eventTimestamps.length >= maxEvents) {
|
|
128
|
+
if (!this.rateLimitWarned) {
|
|
129
|
+
this.logger.warn(`Rate limit exceeded (${maxEvents} events/second). Events are being dropped.`);
|
|
130
|
+
this.rateLimitWarned = true;
|
|
131
|
+
setTimeout(() => {
|
|
132
|
+
this.rateLimitWarned = false;
|
|
133
|
+
}, 5000);
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
this.eventTimestamps.push(now);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
62
140
|
/**
|
|
63
141
|
* Track a custom event
|
|
64
142
|
*/
|
|
@@ -77,6 +155,7 @@ export class AnalyticsImpl {
|
|
|
77
155
|
}
|
|
78
156
|
const trackEvent = {
|
|
79
157
|
type: "track",
|
|
158
|
+
messageId: generateId(),
|
|
80
159
|
event,
|
|
81
160
|
properties,
|
|
82
161
|
timestamp: new Date().toISOString(),
|
|
@@ -86,7 +165,7 @@ export class AnalyticsImpl {
|
|
|
86
165
|
accountId: this.session.getAccountId(),
|
|
87
166
|
context: this.getContext(),
|
|
88
167
|
};
|
|
89
|
-
this.
|
|
168
|
+
this.queueEvent(trackEvent);
|
|
90
169
|
this.session.updateLastActivity();
|
|
91
170
|
}
|
|
92
171
|
/**
|
|
@@ -111,6 +190,7 @@ export class AnalyticsImpl {
|
|
|
111
190
|
}
|
|
112
191
|
const identifyEvent = {
|
|
113
192
|
type: "identify",
|
|
193
|
+
messageId: generateId(),
|
|
114
194
|
userId,
|
|
115
195
|
traits,
|
|
116
196
|
timestamp: new Date().toISOString(),
|
|
@@ -119,7 +199,7 @@ export class AnalyticsImpl {
|
|
|
119
199
|
accountId: this.session.getAccountId(),
|
|
120
200
|
context: this.getContext(),
|
|
121
201
|
};
|
|
122
|
-
this.
|
|
202
|
+
this.queueEvent(identifyEvent);
|
|
123
203
|
this.session.updateLastActivity();
|
|
124
204
|
}
|
|
125
205
|
/**
|
|
@@ -137,6 +217,7 @@ export class AnalyticsImpl {
|
|
|
137
217
|
const referrer = typeof document !== "undefined" ? document.referrer : undefined;
|
|
138
218
|
const pageEvent = {
|
|
139
219
|
type: "page",
|
|
220
|
+
messageId: generateId(),
|
|
140
221
|
name,
|
|
141
222
|
properties,
|
|
142
223
|
url,
|
|
@@ -148,7 +229,7 @@ export class AnalyticsImpl {
|
|
|
148
229
|
accountId: this.session.getAccountId(),
|
|
149
230
|
context: this.getContext(),
|
|
150
231
|
};
|
|
151
|
-
this.
|
|
232
|
+
this.queueEvent(pageEvent);
|
|
152
233
|
this.session.updateLastActivity();
|
|
153
234
|
}
|
|
154
235
|
/**
|
|
@@ -173,6 +254,7 @@ export class AnalyticsImpl {
|
|
|
173
254
|
}
|
|
174
255
|
const groupEvent = {
|
|
175
256
|
type: "group",
|
|
257
|
+
messageId: generateId(),
|
|
176
258
|
accountId,
|
|
177
259
|
traits,
|
|
178
260
|
timestamp: new Date().toISOString(),
|
|
@@ -181,7 +263,7 @@ export class AnalyticsImpl {
|
|
|
181
263
|
sessionId: this.session.getSessionId(),
|
|
182
264
|
context: this.getContext(),
|
|
183
265
|
};
|
|
184
|
-
this.
|
|
266
|
+
this.queueEvent(groupEvent);
|
|
185
267
|
this.session.updateLastActivity();
|
|
186
268
|
}
|
|
187
269
|
/**
|
|
@@ -225,9 +307,26 @@ export class AnalyticsImpl {
|
|
|
225
307
|
}
|
|
226
308
|
/**
|
|
227
309
|
* Manually flush the event queue
|
|
310
|
+
* @returns FlushResult with success status, event count, and any errors
|
|
228
311
|
*/
|
|
229
312
|
async flush() {
|
|
230
|
-
|
|
313
|
+
return this.queue.flush();
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Destroy the analytics instance, cleaning up all resources.
|
|
317
|
+
* Flushes remaining events before cleanup.
|
|
318
|
+
* After calling this, the instance should not be used.
|
|
319
|
+
*/
|
|
320
|
+
async destroy() {
|
|
321
|
+
this.logger.log("Destroying analytics instance...");
|
|
322
|
+
await this.queue.destroy();
|
|
323
|
+
this.session.destroy();
|
|
324
|
+
this.errorCapture?.uninstall();
|
|
325
|
+
this.initialized = false;
|
|
326
|
+
if (globalInstance === this) {
|
|
327
|
+
globalInstance = null;
|
|
328
|
+
}
|
|
329
|
+
this.logger.log("Analytics instance destroyed");
|
|
231
330
|
}
|
|
232
331
|
/**
|
|
233
332
|
* Opt out of tracking
|
|
@@ -247,6 +346,36 @@ export class AnalyticsImpl {
|
|
|
247
346
|
isOptedOut() {
|
|
248
347
|
return this.privacy.isOptedOut();
|
|
249
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* Check if a specific consent category is enabled
|
|
351
|
+
*/
|
|
352
|
+
hasConsent(category) {
|
|
353
|
+
return this.privacy.hasConsent(category);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Get all currently consented categories
|
|
357
|
+
*/
|
|
358
|
+
getConsentedCategories() {
|
|
359
|
+
return this.privacy.getConsentedCategories();
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Set consent for specific categories
|
|
363
|
+
*/
|
|
364
|
+
setConsent(categories) {
|
|
365
|
+
this.privacy.setConsent(categories);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Grant consent for a specific category
|
|
369
|
+
*/
|
|
370
|
+
grantConsent(category) {
|
|
371
|
+
this.privacy.grantConsent(category);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Revoke consent for a specific category
|
|
375
|
+
*/
|
|
376
|
+
revokeConsent(category) {
|
|
377
|
+
this.privacy.revokeConsent(category);
|
|
378
|
+
}
|
|
250
379
|
/**
|
|
251
380
|
* Get current user state
|
|
252
381
|
*/
|
|
@@ -323,7 +452,8 @@ export class AnalyticsImpl {
|
|
|
323
452
|
*/
|
|
324
453
|
subscriptionStarted(props) {
|
|
325
454
|
if (!props?.plan) {
|
|
326
|
-
this.logger.warn("subscriptionStarted: plan is required");
|
|
455
|
+
this.logger.warn("subscriptionStarted: plan is required, event not sent");
|
|
456
|
+
return;
|
|
327
457
|
}
|
|
328
458
|
this.track(LIFECYCLE_EVENTS.SUBSCRIPTION_STARTED, props);
|
|
329
459
|
}
|
|
@@ -332,7 +462,8 @@ export class AnalyticsImpl {
|
|
|
332
462
|
*/
|
|
333
463
|
subscriptionCancelled(props) {
|
|
334
464
|
if (!props?.plan) {
|
|
335
|
-
this.logger.warn("subscriptionCancelled: plan is required");
|
|
465
|
+
this.logger.warn("subscriptionCancelled: plan is required, event not sent");
|
|
466
|
+
return;
|
|
336
467
|
}
|
|
337
468
|
this.track(LIFECYCLE_EVENTS.SUBSCRIPTION_CANCELLED, props);
|
|
338
469
|
}
|
|
@@ -341,7 +472,8 @@ export class AnalyticsImpl {
|
|
|
341
472
|
*/
|
|
342
473
|
subscriptionRenewed(props) {
|
|
343
474
|
if (!props?.plan) {
|
|
344
|
-
this.logger.warn("subscriptionRenewed: plan is required");
|
|
475
|
+
this.logger.warn("subscriptionRenewed: plan is required, event not sent");
|
|
476
|
+
return;
|
|
345
477
|
}
|
|
346
478
|
this.track(LIFECYCLE_EVENTS.SUBSCRIPTION_RENEWED, props);
|
|
347
479
|
}
|
|
@@ -350,7 +482,8 @@ export class AnalyticsImpl {
|
|
|
350
482
|
*/
|
|
351
483
|
planUpgraded(props) {
|
|
352
484
|
if (!props?.fromPlan || !props?.toPlan) {
|
|
353
|
-
this.logger.warn("planUpgraded: fromPlan and toPlan are required");
|
|
485
|
+
this.logger.warn("planUpgraded: fromPlan and toPlan are required, event not sent");
|
|
486
|
+
return;
|
|
354
487
|
}
|
|
355
488
|
this.track(LIFECYCLE_EVENTS.PLAN_UPGRADED, props);
|
|
356
489
|
}
|
|
@@ -359,7 +492,8 @@ export class AnalyticsImpl {
|
|
|
359
492
|
*/
|
|
360
493
|
planDowngraded(props) {
|
|
361
494
|
if (!props?.fromPlan || !props?.toPlan) {
|
|
362
|
-
this.logger.warn("planDowngraded: fromPlan and toPlan are required");
|
|
495
|
+
this.logger.warn("planDowngraded: fromPlan and toPlan are required, event not sent");
|
|
496
|
+
return;
|
|
363
497
|
}
|
|
364
498
|
this.track(LIFECYCLE_EVENTS.PLAN_DOWNGRADED, props);
|
|
365
499
|
}
|
|
@@ -368,7 +502,8 @@ export class AnalyticsImpl {
|
|
|
368
502
|
*/
|
|
369
503
|
trialStarted(props) {
|
|
370
504
|
if (!props?.plan) {
|
|
371
|
-
this.logger.warn("trialStarted: plan is required");
|
|
505
|
+
this.logger.warn("trialStarted: plan is required, event not sent");
|
|
506
|
+
return;
|
|
372
507
|
}
|
|
373
508
|
this.track(LIFECYCLE_EVENTS.TRIAL_STARTED, props);
|
|
374
509
|
}
|
|
@@ -377,7 +512,8 @@ export class AnalyticsImpl {
|
|
|
377
512
|
*/
|
|
378
513
|
trialEnded(props) {
|
|
379
514
|
if (!props?.plan || props?.converted === undefined) {
|
|
380
|
-
this.logger.warn("trialEnded: plan and converted are required");
|
|
515
|
+
this.logger.warn("trialEnded: plan and converted are required, event not sent");
|
|
516
|
+
return;
|
|
381
517
|
}
|
|
382
518
|
this.track(LIFECYCLE_EVENTS.TRIAL_ENDED, props);
|
|
383
519
|
}
|
|
@@ -398,7 +534,8 @@ export class AnalyticsImpl {
|
|
|
398
534
|
*/
|
|
399
535
|
featureActivated(props) {
|
|
400
536
|
if (!props?.feature) {
|
|
401
|
-
this.logger.warn("featureActivated: feature is required");
|
|
537
|
+
this.logger.warn("featureActivated: feature is required, event not sent");
|
|
538
|
+
return;
|
|
402
539
|
}
|
|
403
540
|
this.track(LIFECYCLE_EVENTS.FEATURE_ACTIVATED, props);
|
|
404
541
|
}
|