@grainql/analytics-web 1.4.0 → 1.6.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 +338 -3
- package/dist/cjs/index.d.ts +92 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.d.ts +92 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.ts +92 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.global.dev.js +248 -1
- package/dist/index.global.dev.js.map +2 -2
- package/dist/index.global.js +2 -2
- package/dist/index.global.js.map +3 -3
- package/dist/index.js +267 -0
- package/dist/index.mjs +267 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -12,6 +12,11 @@ class GrainAnalytics {
|
|
|
12
12
|
this.flushTimer = null;
|
|
13
13
|
this.isDestroyed = false;
|
|
14
14
|
this.globalUserId = null;
|
|
15
|
+
// Remote Config properties
|
|
16
|
+
this.configCache = null;
|
|
17
|
+
this.configRefreshTimer = null;
|
|
18
|
+
this.configChangeListeners = [];
|
|
19
|
+
this.configFetchPromise = null;
|
|
15
20
|
this.config = {
|
|
16
21
|
apiUrl: 'https://api.grainql.com',
|
|
17
22
|
authStrategy: 'NONE',
|
|
@@ -21,6 +26,11 @@ class GrainAnalytics {
|
|
|
21
26
|
retryDelay: 1000, // 1 second
|
|
22
27
|
maxEventsPerRequest: 160, // Maximum events per API request
|
|
23
28
|
debug: false,
|
|
29
|
+
// Remote Config defaults
|
|
30
|
+
defaultConfigurations: {},
|
|
31
|
+
configCacheKey: 'grain_config',
|
|
32
|
+
configRefreshInterval: 300000, // 5 minutes
|
|
33
|
+
enableConfigCache: true,
|
|
24
34
|
...config,
|
|
25
35
|
tenantId: config.tenantId,
|
|
26
36
|
};
|
|
@@ -31,6 +41,7 @@ class GrainAnalytics {
|
|
|
31
41
|
this.validateConfig();
|
|
32
42
|
this.setupBeforeUnload();
|
|
33
43
|
this.startFlushTimer();
|
|
44
|
+
this.initializeConfigCache();
|
|
34
45
|
}
|
|
35
46
|
validateConfig() {
|
|
36
47
|
if (!this.config.tenantId) {
|
|
@@ -424,6 +435,258 @@ class GrainAnalytics {
|
|
|
424
435
|
await this.sendEvents(chunk);
|
|
425
436
|
}
|
|
426
437
|
}
|
|
438
|
+
// Remote Config Methods
|
|
439
|
+
/**
|
|
440
|
+
* Initialize configuration cache from localStorage
|
|
441
|
+
*/
|
|
442
|
+
initializeConfigCache() {
|
|
443
|
+
if (!this.config.enableConfigCache || typeof window === 'undefined')
|
|
444
|
+
return;
|
|
445
|
+
try {
|
|
446
|
+
const cached = localStorage.getItem(this.config.configCacheKey);
|
|
447
|
+
if (cached) {
|
|
448
|
+
this.configCache = JSON.parse(cached);
|
|
449
|
+
this.log('Loaded configuration from cache:', this.configCache);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
this.log('Failed to load configuration cache:', error);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Save configuration cache to localStorage
|
|
458
|
+
*/
|
|
459
|
+
saveConfigCache(cache) {
|
|
460
|
+
if (!this.config.enableConfigCache || typeof window === 'undefined')
|
|
461
|
+
return;
|
|
462
|
+
try {
|
|
463
|
+
localStorage.setItem(this.config.configCacheKey, JSON.stringify(cache));
|
|
464
|
+
this.log('Saved configuration to cache:', cache);
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
this.log('Failed to save configuration cache:', error);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Get configuration value with fallback to defaults
|
|
472
|
+
*/
|
|
473
|
+
getConfig(key) {
|
|
474
|
+
// First check cache
|
|
475
|
+
if (this.configCache?.configurations?.[key]) {
|
|
476
|
+
return this.configCache.configurations[key];
|
|
477
|
+
}
|
|
478
|
+
// Then check defaults
|
|
479
|
+
if (this.config.defaultConfigurations?.[key]) {
|
|
480
|
+
return this.config.defaultConfigurations[key];
|
|
481
|
+
}
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Get all configurations with fallback to defaults
|
|
486
|
+
*/
|
|
487
|
+
getAllConfigs() {
|
|
488
|
+
const configs = { ...this.config.defaultConfigurations };
|
|
489
|
+
if (this.configCache?.configurations) {
|
|
490
|
+
Object.assign(configs, this.configCache.configurations);
|
|
491
|
+
}
|
|
492
|
+
return configs;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Fetch configurations from API
|
|
496
|
+
*/
|
|
497
|
+
async fetchConfig(options = {}) {
|
|
498
|
+
if (this.isDestroyed) {
|
|
499
|
+
throw new Error('Grain Analytics: Client has been destroyed');
|
|
500
|
+
}
|
|
501
|
+
const userId = options.userId || this.globalUserId || 'anonymous';
|
|
502
|
+
const immediateKeys = options.immediateKeys || [];
|
|
503
|
+
const properties = options.properties || {};
|
|
504
|
+
const request = {
|
|
505
|
+
userId,
|
|
506
|
+
immediateKeys,
|
|
507
|
+
properties,
|
|
508
|
+
};
|
|
509
|
+
let lastError;
|
|
510
|
+
for (let attempt = 0; attempt <= this.config.retryAttempts; attempt++) {
|
|
511
|
+
try {
|
|
512
|
+
const headers = await this.getAuthHeaders();
|
|
513
|
+
const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
|
|
514
|
+
this.log(`Fetching configurations for user ${userId} (attempt ${attempt + 1})`);
|
|
515
|
+
const response = await fetch(url, {
|
|
516
|
+
method: 'POST',
|
|
517
|
+
headers,
|
|
518
|
+
body: JSON.stringify(request),
|
|
519
|
+
});
|
|
520
|
+
if (!response.ok) {
|
|
521
|
+
let errorMessage = `HTTP ${response.status}`;
|
|
522
|
+
try {
|
|
523
|
+
const errorBody = await response.json();
|
|
524
|
+
if (errorBody?.message) {
|
|
525
|
+
errorMessage = errorBody.message;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
const errorText = await response.text();
|
|
530
|
+
if (errorText) {
|
|
531
|
+
errorMessage = errorText;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const error = new Error(`Failed to fetch configurations: ${errorMessage}`);
|
|
535
|
+
error.status = response.status;
|
|
536
|
+
throw error;
|
|
537
|
+
}
|
|
538
|
+
const configResponse = await response.json();
|
|
539
|
+
// Update cache if successful
|
|
540
|
+
if (configResponse.configurations) {
|
|
541
|
+
this.updateConfigCache(configResponse, userId);
|
|
542
|
+
}
|
|
543
|
+
this.log(`Successfully fetched configurations for user ${userId}:`, configResponse);
|
|
544
|
+
return configResponse;
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
lastError = error;
|
|
548
|
+
if (attempt === this.config.retryAttempts) {
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
if (!this.isRetriableError(error)) {
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
const delayMs = this.config.retryDelay * Math.pow(2, attempt);
|
|
555
|
+
this.log(`Retrying config fetch in ${delayMs}ms after error:`, error);
|
|
556
|
+
await this.delay(delayMs);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
console.error('[Grain Analytics] Failed to fetch configurations after all retries:', lastError);
|
|
560
|
+
throw lastError;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Get configuration asynchronously (cache-first with fallback to API)
|
|
564
|
+
*/
|
|
565
|
+
async getConfigAsync(key, options = {}) {
|
|
566
|
+
// Return immediately if we have it in cache and not forcing refresh
|
|
567
|
+
if (!options.forceRefresh && this.configCache?.configurations?.[key]) {
|
|
568
|
+
return this.configCache.configurations[key];
|
|
569
|
+
}
|
|
570
|
+
// Return default if available and not forcing refresh
|
|
571
|
+
if (!options.forceRefresh && this.config.defaultConfigurations?.[key]) {
|
|
572
|
+
return this.config.defaultConfigurations[key];
|
|
573
|
+
}
|
|
574
|
+
// Fetch from API
|
|
575
|
+
try {
|
|
576
|
+
const response = await this.fetchConfig(options);
|
|
577
|
+
return response.configurations[key];
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
this.log(`Failed to fetch config for key "${key}":`, error);
|
|
581
|
+
// Return default as fallback
|
|
582
|
+
return this.config.defaultConfigurations?.[key];
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Get all configurations asynchronously (cache-first with fallback to API)
|
|
587
|
+
*/
|
|
588
|
+
async getAllConfigsAsync(options = {}) {
|
|
589
|
+
// Return cache if available and not forcing refresh
|
|
590
|
+
if (!options.forceRefresh && this.configCache?.configurations) {
|
|
591
|
+
return { ...this.config.defaultConfigurations, ...this.configCache.configurations };
|
|
592
|
+
}
|
|
593
|
+
// Fetch from API
|
|
594
|
+
try {
|
|
595
|
+
const response = await this.fetchConfig(options);
|
|
596
|
+
return { ...this.config.defaultConfigurations, ...response.configurations };
|
|
597
|
+
}
|
|
598
|
+
catch (error) {
|
|
599
|
+
this.log('Failed to fetch all configs:', error);
|
|
600
|
+
// Return defaults as fallback
|
|
601
|
+
return { ...this.config.defaultConfigurations };
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Update configuration cache and notify listeners
|
|
606
|
+
*/
|
|
607
|
+
updateConfigCache(response, userId) {
|
|
608
|
+
const newCache = {
|
|
609
|
+
configurations: response.configurations,
|
|
610
|
+
snapshotId: response.snapshotId,
|
|
611
|
+
timestamp: response.timestamp,
|
|
612
|
+
userId,
|
|
613
|
+
};
|
|
614
|
+
const oldConfigs = this.configCache?.configurations || {};
|
|
615
|
+
this.configCache = newCache;
|
|
616
|
+
this.saveConfigCache(newCache);
|
|
617
|
+
// Notify listeners if configurations changed
|
|
618
|
+
if (JSON.stringify(oldConfigs) !== JSON.stringify(response.configurations)) {
|
|
619
|
+
this.notifyConfigChangeListeners(response.configurations);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Add configuration change listener
|
|
624
|
+
*/
|
|
625
|
+
addConfigChangeListener(listener) {
|
|
626
|
+
this.configChangeListeners.push(listener);
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Remove configuration change listener
|
|
630
|
+
*/
|
|
631
|
+
removeConfigChangeListener(listener) {
|
|
632
|
+
const index = this.configChangeListeners.indexOf(listener);
|
|
633
|
+
if (index > -1) {
|
|
634
|
+
this.configChangeListeners.splice(index, 1);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Notify all configuration change listeners
|
|
639
|
+
*/
|
|
640
|
+
notifyConfigChangeListeners(configurations) {
|
|
641
|
+
this.configChangeListeners.forEach(listener => {
|
|
642
|
+
try {
|
|
643
|
+
listener(configurations);
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
console.error('[Grain Analytics] Config change listener error:', error);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Start automatic configuration refresh timer
|
|
652
|
+
*/
|
|
653
|
+
startConfigRefreshTimer() {
|
|
654
|
+
if (this.configRefreshTimer) {
|
|
655
|
+
clearInterval(this.configRefreshTimer);
|
|
656
|
+
}
|
|
657
|
+
this.configRefreshTimer = window.setInterval(() => {
|
|
658
|
+
if (!this.isDestroyed && this.globalUserId) {
|
|
659
|
+
this.fetchConfig().catch((error) => {
|
|
660
|
+
console.error('[Grain Analytics] Auto-config refresh failed:', error);
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
}, this.config.configRefreshInterval);
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Stop automatic configuration refresh timer
|
|
667
|
+
*/
|
|
668
|
+
stopConfigRefreshTimer() {
|
|
669
|
+
if (this.configRefreshTimer) {
|
|
670
|
+
clearInterval(this.configRefreshTimer);
|
|
671
|
+
this.configRefreshTimer = null;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Preload configurations for immediate access
|
|
676
|
+
*/
|
|
677
|
+
async preloadConfig(immediateKeys = [], properties) {
|
|
678
|
+
if (!this.globalUserId) {
|
|
679
|
+
this.log('Cannot preload config: no user ID set');
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
try {
|
|
683
|
+
await this.fetchConfig({ immediateKeys, properties });
|
|
684
|
+
this.startConfigRefreshTimer();
|
|
685
|
+
}
|
|
686
|
+
catch (error) {
|
|
687
|
+
this.log('Failed to preload config:', error);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
427
690
|
/**
|
|
428
691
|
* Split events array into chunks of specified size
|
|
429
692
|
*/
|
|
@@ -443,6 +706,10 @@ class GrainAnalytics {
|
|
|
443
706
|
clearInterval(this.flushTimer);
|
|
444
707
|
this.flushTimer = null;
|
|
445
708
|
}
|
|
709
|
+
// Stop config refresh timer
|
|
710
|
+
this.stopConfigRefreshTimer();
|
|
711
|
+
// Clear config change listeners
|
|
712
|
+
this.configChangeListeners = [];
|
|
446
713
|
// Send any remaining events (in chunks if necessary)
|
|
447
714
|
if (this.eventQueue.length > 0) {
|
|
448
715
|
const eventsToSend = [...this.eventQueue];
|
package/dist/index.mjs
CHANGED
|
@@ -8,6 +8,11 @@ export class GrainAnalytics {
|
|
|
8
8
|
this.flushTimer = null;
|
|
9
9
|
this.isDestroyed = false;
|
|
10
10
|
this.globalUserId = null;
|
|
11
|
+
// Remote Config properties
|
|
12
|
+
this.configCache = null;
|
|
13
|
+
this.configRefreshTimer = null;
|
|
14
|
+
this.configChangeListeners = [];
|
|
15
|
+
this.configFetchPromise = null;
|
|
11
16
|
this.config = {
|
|
12
17
|
apiUrl: 'https://api.grainql.com',
|
|
13
18
|
authStrategy: 'NONE',
|
|
@@ -17,6 +22,11 @@ export class GrainAnalytics {
|
|
|
17
22
|
retryDelay: 1000, // 1 second
|
|
18
23
|
maxEventsPerRequest: 160, // Maximum events per API request
|
|
19
24
|
debug: false,
|
|
25
|
+
// Remote Config defaults
|
|
26
|
+
defaultConfigurations: {},
|
|
27
|
+
configCacheKey: 'grain_config',
|
|
28
|
+
configRefreshInterval: 300000, // 5 minutes
|
|
29
|
+
enableConfigCache: true,
|
|
20
30
|
...config,
|
|
21
31
|
tenantId: config.tenantId,
|
|
22
32
|
};
|
|
@@ -27,6 +37,7 @@ export class GrainAnalytics {
|
|
|
27
37
|
this.validateConfig();
|
|
28
38
|
this.setupBeforeUnload();
|
|
29
39
|
this.startFlushTimer();
|
|
40
|
+
this.initializeConfigCache();
|
|
30
41
|
}
|
|
31
42
|
validateConfig() {
|
|
32
43
|
if (!this.config.tenantId) {
|
|
@@ -420,6 +431,258 @@ export class GrainAnalytics {
|
|
|
420
431
|
await this.sendEvents(chunk);
|
|
421
432
|
}
|
|
422
433
|
}
|
|
434
|
+
// Remote Config Methods
|
|
435
|
+
/**
|
|
436
|
+
* Initialize configuration cache from localStorage
|
|
437
|
+
*/
|
|
438
|
+
initializeConfigCache() {
|
|
439
|
+
if (!this.config.enableConfigCache || typeof window === 'undefined')
|
|
440
|
+
return;
|
|
441
|
+
try {
|
|
442
|
+
const cached = localStorage.getItem(this.config.configCacheKey);
|
|
443
|
+
if (cached) {
|
|
444
|
+
this.configCache = JSON.parse(cached);
|
|
445
|
+
this.log('Loaded configuration from cache:', this.configCache);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
this.log('Failed to load configuration cache:', error);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Save configuration cache to localStorage
|
|
454
|
+
*/
|
|
455
|
+
saveConfigCache(cache) {
|
|
456
|
+
if (!this.config.enableConfigCache || typeof window === 'undefined')
|
|
457
|
+
return;
|
|
458
|
+
try {
|
|
459
|
+
localStorage.setItem(this.config.configCacheKey, JSON.stringify(cache));
|
|
460
|
+
this.log('Saved configuration to cache:', cache);
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
this.log('Failed to save configuration cache:', error);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Get configuration value with fallback to defaults
|
|
468
|
+
*/
|
|
469
|
+
getConfig(key) {
|
|
470
|
+
// First check cache
|
|
471
|
+
if (this.configCache?.configurations?.[key]) {
|
|
472
|
+
return this.configCache.configurations[key];
|
|
473
|
+
}
|
|
474
|
+
// Then check defaults
|
|
475
|
+
if (this.config.defaultConfigurations?.[key]) {
|
|
476
|
+
return this.config.defaultConfigurations[key];
|
|
477
|
+
}
|
|
478
|
+
return undefined;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Get all configurations with fallback to defaults
|
|
482
|
+
*/
|
|
483
|
+
getAllConfigs() {
|
|
484
|
+
const configs = { ...this.config.defaultConfigurations };
|
|
485
|
+
if (this.configCache?.configurations) {
|
|
486
|
+
Object.assign(configs, this.configCache.configurations);
|
|
487
|
+
}
|
|
488
|
+
return configs;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Fetch configurations from API
|
|
492
|
+
*/
|
|
493
|
+
async fetchConfig(options = {}) {
|
|
494
|
+
if (this.isDestroyed) {
|
|
495
|
+
throw new Error('Grain Analytics: Client has been destroyed');
|
|
496
|
+
}
|
|
497
|
+
const userId = options.userId || this.globalUserId || 'anonymous';
|
|
498
|
+
const immediateKeys = options.immediateKeys || [];
|
|
499
|
+
const properties = options.properties || {};
|
|
500
|
+
const request = {
|
|
501
|
+
userId,
|
|
502
|
+
immediateKeys,
|
|
503
|
+
properties,
|
|
504
|
+
};
|
|
505
|
+
let lastError;
|
|
506
|
+
for (let attempt = 0; attempt <= this.config.retryAttempts; attempt++) {
|
|
507
|
+
try {
|
|
508
|
+
const headers = await this.getAuthHeaders();
|
|
509
|
+
const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
|
|
510
|
+
this.log(`Fetching configurations for user ${userId} (attempt ${attempt + 1})`);
|
|
511
|
+
const response = await fetch(url, {
|
|
512
|
+
method: 'POST',
|
|
513
|
+
headers,
|
|
514
|
+
body: JSON.stringify(request),
|
|
515
|
+
});
|
|
516
|
+
if (!response.ok) {
|
|
517
|
+
let errorMessage = `HTTP ${response.status}`;
|
|
518
|
+
try {
|
|
519
|
+
const errorBody = await response.json();
|
|
520
|
+
if (errorBody?.message) {
|
|
521
|
+
errorMessage = errorBody.message;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
const errorText = await response.text();
|
|
526
|
+
if (errorText) {
|
|
527
|
+
errorMessage = errorText;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const error = new Error(`Failed to fetch configurations: ${errorMessage}`);
|
|
531
|
+
error.status = response.status;
|
|
532
|
+
throw error;
|
|
533
|
+
}
|
|
534
|
+
const configResponse = await response.json();
|
|
535
|
+
// Update cache if successful
|
|
536
|
+
if (configResponse.configurations) {
|
|
537
|
+
this.updateConfigCache(configResponse, userId);
|
|
538
|
+
}
|
|
539
|
+
this.log(`Successfully fetched configurations for user ${userId}:`, configResponse);
|
|
540
|
+
return configResponse;
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
lastError = error;
|
|
544
|
+
if (attempt === this.config.retryAttempts) {
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
if (!this.isRetriableError(error)) {
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
const delayMs = this.config.retryDelay * Math.pow(2, attempt);
|
|
551
|
+
this.log(`Retrying config fetch in ${delayMs}ms after error:`, error);
|
|
552
|
+
await this.delay(delayMs);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
console.error('[Grain Analytics] Failed to fetch configurations after all retries:', lastError);
|
|
556
|
+
throw lastError;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Get configuration asynchronously (cache-first with fallback to API)
|
|
560
|
+
*/
|
|
561
|
+
async getConfigAsync(key, options = {}) {
|
|
562
|
+
// Return immediately if we have it in cache and not forcing refresh
|
|
563
|
+
if (!options.forceRefresh && this.configCache?.configurations?.[key]) {
|
|
564
|
+
return this.configCache.configurations[key];
|
|
565
|
+
}
|
|
566
|
+
// Return default if available and not forcing refresh
|
|
567
|
+
if (!options.forceRefresh && this.config.defaultConfigurations?.[key]) {
|
|
568
|
+
return this.config.defaultConfigurations[key];
|
|
569
|
+
}
|
|
570
|
+
// Fetch from API
|
|
571
|
+
try {
|
|
572
|
+
const response = await this.fetchConfig(options);
|
|
573
|
+
return response.configurations[key];
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
this.log(`Failed to fetch config for key "${key}":`, error);
|
|
577
|
+
// Return default as fallback
|
|
578
|
+
return this.config.defaultConfigurations?.[key];
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Get all configurations asynchronously (cache-first with fallback to API)
|
|
583
|
+
*/
|
|
584
|
+
async getAllConfigsAsync(options = {}) {
|
|
585
|
+
// Return cache if available and not forcing refresh
|
|
586
|
+
if (!options.forceRefresh && this.configCache?.configurations) {
|
|
587
|
+
return { ...this.config.defaultConfigurations, ...this.configCache.configurations };
|
|
588
|
+
}
|
|
589
|
+
// Fetch from API
|
|
590
|
+
try {
|
|
591
|
+
const response = await this.fetchConfig(options);
|
|
592
|
+
return { ...this.config.defaultConfigurations, ...response.configurations };
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
this.log('Failed to fetch all configs:', error);
|
|
596
|
+
// Return defaults as fallback
|
|
597
|
+
return { ...this.config.defaultConfigurations };
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Update configuration cache and notify listeners
|
|
602
|
+
*/
|
|
603
|
+
updateConfigCache(response, userId) {
|
|
604
|
+
const newCache = {
|
|
605
|
+
configurations: response.configurations,
|
|
606
|
+
snapshotId: response.snapshotId,
|
|
607
|
+
timestamp: response.timestamp,
|
|
608
|
+
userId,
|
|
609
|
+
};
|
|
610
|
+
const oldConfigs = this.configCache?.configurations || {};
|
|
611
|
+
this.configCache = newCache;
|
|
612
|
+
this.saveConfigCache(newCache);
|
|
613
|
+
// Notify listeners if configurations changed
|
|
614
|
+
if (JSON.stringify(oldConfigs) !== JSON.stringify(response.configurations)) {
|
|
615
|
+
this.notifyConfigChangeListeners(response.configurations);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Add configuration change listener
|
|
620
|
+
*/
|
|
621
|
+
addConfigChangeListener(listener) {
|
|
622
|
+
this.configChangeListeners.push(listener);
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Remove configuration change listener
|
|
626
|
+
*/
|
|
627
|
+
removeConfigChangeListener(listener) {
|
|
628
|
+
const index = this.configChangeListeners.indexOf(listener);
|
|
629
|
+
if (index > -1) {
|
|
630
|
+
this.configChangeListeners.splice(index, 1);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Notify all configuration change listeners
|
|
635
|
+
*/
|
|
636
|
+
notifyConfigChangeListeners(configurations) {
|
|
637
|
+
this.configChangeListeners.forEach(listener => {
|
|
638
|
+
try {
|
|
639
|
+
listener(configurations);
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
console.error('[Grain Analytics] Config change listener error:', error);
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Start automatic configuration refresh timer
|
|
648
|
+
*/
|
|
649
|
+
startConfigRefreshTimer() {
|
|
650
|
+
if (this.configRefreshTimer) {
|
|
651
|
+
clearInterval(this.configRefreshTimer);
|
|
652
|
+
}
|
|
653
|
+
this.configRefreshTimer = window.setInterval(() => {
|
|
654
|
+
if (!this.isDestroyed && this.globalUserId) {
|
|
655
|
+
this.fetchConfig().catch((error) => {
|
|
656
|
+
console.error('[Grain Analytics] Auto-config refresh failed:', error);
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
}, this.config.configRefreshInterval);
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Stop automatic configuration refresh timer
|
|
663
|
+
*/
|
|
664
|
+
stopConfigRefreshTimer() {
|
|
665
|
+
if (this.configRefreshTimer) {
|
|
666
|
+
clearInterval(this.configRefreshTimer);
|
|
667
|
+
this.configRefreshTimer = null;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Preload configurations for immediate access
|
|
672
|
+
*/
|
|
673
|
+
async preloadConfig(immediateKeys = [], properties) {
|
|
674
|
+
if (!this.globalUserId) {
|
|
675
|
+
this.log('Cannot preload config: no user ID set');
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
try {
|
|
679
|
+
await this.fetchConfig({ immediateKeys, properties });
|
|
680
|
+
this.startConfigRefreshTimer();
|
|
681
|
+
}
|
|
682
|
+
catch (error) {
|
|
683
|
+
this.log('Failed to preload config:', error);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
423
686
|
/**
|
|
424
687
|
* Split events array into chunks of specified size
|
|
425
688
|
*/
|
|
@@ -439,6 +702,10 @@ export class GrainAnalytics {
|
|
|
439
702
|
clearInterval(this.flushTimer);
|
|
440
703
|
this.flushTimer = null;
|
|
441
704
|
}
|
|
705
|
+
// Stop config refresh timer
|
|
706
|
+
this.stopConfigRefreshTimer();
|
|
707
|
+
// Clear config change listeners
|
|
708
|
+
this.configChangeListeners = [];
|
|
442
709
|
// Send any remaining events (in chunks if necessary)
|
|
443
710
|
if (this.eventQueue.length > 0) {
|
|
444
711
|
const eventsToSend = [...this.eventQueue];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grainql/analytics-web",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Lightweight TypeScript SDK for sending analytics events
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "Lightweight TypeScript SDK for sending analytics events and managing remote configurations via Grain's REST API",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.ts",
|