@grainql/analytics-web 1.6.1 → 1.7.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.js CHANGED
@@ -115,6 +115,125 @@ class GrainAnalytics {
115
115
  console.log('[Grain Analytics]', ...args);
116
116
  }
117
117
  }
118
+ /**
119
+ * Create error digest from events
120
+ */
121
+ createErrorDigest(events) {
122
+ const eventNames = [...new Set(events.map(e => e.eventName))];
123
+ const userIds = [...new Set(events.map(e => e.userId))];
124
+ let totalProperties = 0;
125
+ let totalSize = 0;
126
+ events.forEach(event => {
127
+ const properties = event.properties || {};
128
+ totalProperties += Object.keys(properties).length;
129
+ totalSize += JSON.stringify(event).length;
130
+ });
131
+ return {
132
+ eventCount: events.length,
133
+ totalProperties,
134
+ totalSize,
135
+ eventNames,
136
+ userIds,
137
+ };
138
+ }
139
+ /**
140
+ * Format error with beautiful structure
141
+ */
142
+ formatError(error, context, events) {
143
+ const digest = events ? this.createErrorDigest(events) : {
144
+ eventCount: 0,
145
+ totalProperties: 0,
146
+ totalSize: 0,
147
+ eventNames: [],
148
+ userIds: [],
149
+ };
150
+ let code = 'UNKNOWN_ERROR';
151
+ let message = 'An unknown error occurred';
152
+ if (error instanceof Error) {
153
+ message = error.message;
154
+ // Determine error code based on error type and message
155
+ if (message.includes('fetch failed') || message.includes('network error')) {
156
+ code = 'NETWORK_ERROR';
157
+ }
158
+ else if (message.includes('timeout')) {
159
+ code = 'TIMEOUT_ERROR';
160
+ }
161
+ else if (message.includes('HTTP 4')) {
162
+ code = 'CLIENT_ERROR';
163
+ }
164
+ else if (message.includes('HTTP 5')) {
165
+ code = 'SERVER_ERROR';
166
+ }
167
+ else if (message.includes('JSON')) {
168
+ code = 'PARSE_ERROR';
169
+ }
170
+ else if (message.includes('auth') || message.includes('unauthorized')) {
171
+ code = 'AUTH_ERROR';
172
+ }
173
+ else if (message.includes('rate limit') || message.includes('429')) {
174
+ code = 'RATE_LIMIT_ERROR';
175
+ }
176
+ else {
177
+ code = 'GENERAL_ERROR';
178
+ }
179
+ }
180
+ else if (typeof error === 'string') {
181
+ message = error;
182
+ code = 'STRING_ERROR';
183
+ }
184
+ else if (error && typeof error === 'object' && 'status' in error) {
185
+ const status = error.status;
186
+ code = `HTTP_${status}`;
187
+ message = `HTTP ${status} error`;
188
+ }
189
+ return {
190
+ code,
191
+ message,
192
+ digest,
193
+ timestamp: new Date().toISOString(),
194
+ context,
195
+ originalError: error,
196
+ };
197
+ }
198
+ /**
199
+ * Log formatted error gracefully
200
+ */
201
+ logError(formattedError) {
202
+ const { code, message, digest, timestamp, context } = formattedError;
203
+ const errorOutput = {
204
+ '🚨 Grain Analytics Error': {
205
+ 'Error Code': code,
206
+ 'Message': message,
207
+ 'Context': context,
208
+ 'Timestamp': timestamp,
209
+ 'Event Digest': {
210
+ 'Events': digest.eventCount,
211
+ 'Properties': digest.totalProperties,
212
+ 'Size (bytes)': digest.totalSize,
213
+ 'Event Names': digest.eventNames.length > 0 ? digest.eventNames.join(', ') : 'None',
214
+ 'User IDs': digest.userIds.length > 0 ? digest.userIds.slice(0, 3).join(', ') + (digest.userIds.length > 3 ? '...' : '') : 'None',
215
+ }
216
+ }
217
+ };
218
+ console.error('🚨 Grain Analytics Error:', errorOutput);
219
+ // Also log in a more compact format for debugging
220
+ if (this.config.debug) {
221
+ console.error(`[Grain Analytics] ${code}: ${message} (${context}) - Events: ${digest.eventCount}, Props: ${digest.totalProperties}, Size: ${digest.totalSize}B`);
222
+ }
223
+ }
224
+ /**
225
+ * Safely execute a function with error handling
226
+ */
227
+ async safeExecute(fn, context, events) {
228
+ try {
229
+ return await fn();
230
+ }
231
+ catch (error) {
232
+ const formattedError = this.formatError(error, context, events);
233
+ this.logError(formattedError);
234
+ return null;
235
+ }
236
+ }
118
237
  formatEvent(event) {
119
238
  return {
120
239
  eventName: event.eventName,
@@ -202,20 +321,22 @@ class GrainAnalytics {
202
321
  catch (error) {
203
322
  lastError = error;
204
323
  if (attempt === this.config.retryAttempts) {
205
- // Last attempt, don't retry
206
- break;
324
+ // Last attempt, don't retry - log error gracefully
325
+ const formattedError = this.formatError(error, `sendEvents (attempt ${attempt + 1}/${this.config.retryAttempts + 1})`, events);
326
+ this.logError(formattedError);
327
+ return; // Don't throw, just return gracefully
207
328
  }
208
329
  if (!this.isRetriableError(error)) {
209
- // Non-retriable error, don't retry
210
- break;
330
+ // Non-retriable error, don't retry - log error gracefully
331
+ const formattedError = this.formatError(error, `sendEvents (non-retriable error)`, events);
332
+ this.logError(formattedError);
333
+ return; // Don't throw, just return gracefully
211
334
  }
212
335
  const delayMs = this.config.retryDelay * Math.pow(2, attempt); // Exponential backoff
213
336
  this.log(`Retrying in ${delayMs}ms after error:`, error);
214
337
  await this.delay(delayMs);
215
338
  }
216
339
  }
217
- console.error('[Grain Analytics] Failed to send events after all retries:', lastError);
218
- throw lastError;
219
340
  }
220
341
  async sendEventsWithBeacon(events) {
221
342
  if (events.length === 0)
@@ -243,7 +364,9 @@ class GrainAnalytics {
243
364
  this.log(`Successfully sent ${events.length} events via fetch (keepalive)`);
244
365
  }
245
366
  catch (error) {
246
- console.error('[Grain Analytics] Failed to send events via beacon:', error);
367
+ // Log error gracefully for beacon failures (page unload scenarios)
368
+ const formattedError = this.formatError(error, 'sendEventsWithBeacon', events);
369
+ this.logError(formattedError);
247
370
  }
248
371
  }
249
372
  startFlushTimer() {
@@ -253,7 +376,8 @@ class GrainAnalytics {
253
376
  this.flushTimer = window.setInterval(() => {
254
377
  if (this.eventQueue.length > 0) {
255
378
  this.flush().catch((error) => {
256
- console.error('[Grain Analytics] Auto-flush failed:', error);
379
+ const formattedError = this.formatError(error, 'auto-flush');
380
+ this.logError(formattedError);
257
381
  });
258
382
  }
259
383
  }, this.config.flushInterval);
@@ -294,28 +418,37 @@ class GrainAnalytics {
294
418
  });
295
419
  }
296
420
  async track(eventOrName, propertiesOrOptions, options) {
297
- if (this.isDestroyed) {
298
- throw new Error('Grain Analytics: Client has been destroyed');
299
- }
300
- let event;
301
- let opts = {};
302
- if (typeof eventOrName === 'string') {
303
- event = {
304
- eventName: eventOrName,
305
- properties: propertiesOrOptions,
306
- };
307
- opts = options || {};
308
- }
309
- else {
310
- event = eventOrName;
311
- opts = propertiesOrOptions || {};
421
+ try {
422
+ if (this.isDestroyed) {
423
+ const error = new Error('Grain Analytics: Client has been destroyed');
424
+ const formattedError = this.formatError(error, 'track (client destroyed)');
425
+ this.logError(formattedError);
426
+ return;
427
+ }
428
+ let event;
429
+ let opts = {};
430
+ if (typeof eventOrName === 'string') {
431
+ event = {
432
+ eventName: eventOrName,
433
+ properties: propertiesOrOptions,
434
+ };
435
+ opts = options || {};
436
+ }
437
+ else {
438
+ event = eventOrName;
439
+ opts = propertiesOrOptions || {};
440
+ }
441
+ const formattedEvent = this.formatEvent(event);
442
+ this.eventQueue.push(formattedEvent);
443
+ this.log(`Queued event: ${event.eventName}`, event.properties);
444
+ // Check if we should flush immediately
445
+ if (opts.flush || this.eventQueue.length >= this.config.batchSize) {
446
+ await this.flush();
447
+ }
312
448
  }
313
- const formattedEvent = this.formatEvent(event);
314
- this.eventQueue.push(formattedEvent);
315
- this.log(`Queued event: ${event.eventName}`, event.properties);
316
- // Check if we should flush immediately
317
- if (opts.flush || this.eventQueue.length >= this.config.batchSize) {
318
- await this.flush();
449
+ catch (error) {
450
+ const formattedError = this.formatError(error, 'track');
451
+ this.logError(formattedError);
319
452
  }
320
453
  }
321
454
  /**
@@ -354,36 +487,51 @@ class GrainAnalytics {
354
487
  * Set user properties
355
488
  */
356
489
  async setProperty(properties, options) {
357
- if (this.isDestroyed) {
358
- throw new Error('Grain Analytics: Client has been destroyed');
359
- }
360
- const userId = options?.userId || this.getEffectiveUserId();
361
- // Validate property count (max 4 properties)
362
- const propertyKeys = Object.keys(properties);
363
- if (propertyKeys.length > 4) {
364
- throw new Error('Grain Analytics: Maximum 4 properties allowed per request');
365
- }
366
- if (propertyKeys.length === 0) {
367
- throw new Error('Grain Analytics: At least one property is required');
368
- }
369
- // Serialize all values to strings
370
- const serializedProperties = {};
371
- for (const [key, value] of Object.entries(properties)) {
372
- if (value === null || value === undefined) {
373
- serializedProperties[key] = '';
490
+ try {
491
+ if (this.isDestroyed) {
492
+ const error = new Error('Grain Analytics: Client has been destroyed');
493
+ const formattedError = this.formatError(error, 'setProperty (client destroyed)');
494
+ this.logError(formattedError);
495
+ return;
374
496
  }
375
- else if (typeof value === 'string') {
376
- serializedProperties[key] = value;
497
+ const userId = options?.userId || this.getEffectiveUserId();
498
+ // Validate property count (max 4 properties)
499
+ const propertyKeys = Object.keys(properties);
500
+ if (propertyKeys.length > 4) {
501
+ const error = new Error('Grain Analytics: Maximum 4 properties allowed per request');
502
+ const formattedError = this.formatError(error, 'setProperty (validation)');
503
+ this.logError(formattedError);
504
+ return;
377
505
  }
378
- else {
379
- serializedProperties[key] = JSON.stringify(value);
506
+ if (propertyKeys.length === 0) {
507
+ const error = new Error('Grain Analytics: At least one property is required');
508
+ const formattedError = this.formatError(error, 'setProperty (validation)');
509
+ this.logError(formattedError);
510
+ return;
511
+ }
512
+ // Serialize all values to strings
513
+ const serializedProperties = {};
514
+ for (const [key, value] of Object.entries(properties)) {
515
+ if (value === null || value === undefined) {
516
+ serializedProperties[key] = '';
517
+ }
518
+ else if (typeof value === 'string') {
519
+ serializedProperties[key] = value;
520
+ }
521
+ else {
522
+ serializedProperties[key] = JSON.stringify(value);
523
+ }
380
524
  }
525
+ const payload = {
526
+ userId,
527
+ ...serializedProperties,
528
+ };
529
+ await this.sendProperties(payload);
530
+ }
531
+ catch (error) {
532
+ const formattedError = this.formatError(error, 'setProperty');
533
+ this.logError(formattedError);
381
534
  }
382
- const payload = {
383
- userId,
384
- ...serializedProperties,
385
- };
386
- await this.sendProperties(payload);
387
535
  }
388
536
  /**
389
537
  * Send properties to the API
@@ -424,83 +572,139 @@ class GrainAnalytics {
424
572
  catch (error) {
425
573
  lastError = error;
426
574
  if (attempt === this.config.retryAttempts) {
427
- // Last attempt, don't retry
428
- break;
575
+ // Last attempt, don't retry - log error gracefully
576
+ const formattedError = this.formatError(error, `sendProperties (attempt ${attempt + 1}/${this.config.retryAttempts + 1})`);
577
+ this.logError(formattedError);
578
+ return; // Don't throw, just return gracefully
429
579
  }
430
580
  if (!this.isRetriableError(error)) {
431
- // Non-retriable error, don't retry
432
- break;
581
+ // Non-retriable error, don't retry - log error gracefully
582
+ const formattedError = this.formatError(error, 'sendProperties (non-retriable error)');
583
+ this.logError(formattedError);
584
+ return; // Don't throw, just return gracefully
433
585
  }
434
586
  const delayMs = this.config.retryDelay * Math.pow(2, attempt); // Exponential backoff
435
587
  this.log(`Retrying in ${delayMs}ms after error:`, error);
436
588
  await this.delay(delayMs);
437
589
  }
438
590
  }
439
- console.error('[Grain Analytics] Failed to set properties after all retries:', lastError);
440
- throw lastError;
441
591
  }
442
592
  // Template event methods
443
593
  /**
444
594
  * Track user login event
445
595
  */
446
596
  async trackLogin(properties, options) {
447
- return this.track('login', properties, options);
597
+ try {
598
+ return await this.track('login', properties, options);
599
+ }
600
+ catch (error) {
601
+ const formattedError = this.formatError(error, 'trackLogin');
602
+ this.logError(formattedError);
603
+ }
448
604
  }
449
605
  /**
450
606
  * Track user signup event
451
607
  */
452
608
  async trackSignup(properties, options) {
453
- return this.track('signup', properties, options);
609
+ try {
610
+ return await this.track('signup', properties, options);
611
+ }
612
+ catch (error) {
613
+ const formattedError = this.formatError(error, 'trackSignup');
614
+ this.logError(formattedError);
615
+ }
454
616
  }
455
617
  /**
456
618
  * Track checkout event
457
619
  */
458
620
  async trackCheckout(properties, options) {
459
- return this.track('checkout', properties, options);
621
+ try {
622
+ return await this.track('checkout', properties, options);
623
+ }
624
+ catch (error) {
625
+ const formattedError = this.formatError(error, 'trackCheckout');
626
+ this.logError(formattedError);
627
+ }
460
628
  }
461
629
  /**
462
630
  * Track page view event
463
631
  */
464
632
  async trackPageView(properties, options) {
465
- return this.track('page_view', properties, options);
633
+ try {
634
+ return await this.track('page_view', properties, options);
635
+ }
636
+ catch (error) {
637
+ const formattedError = this.formatError(error, 'trackPageView');
638
+ this.logError(formattedError);
639
+ }
466
640
  }
467
641
  /**
468
642
  * Track purchase event
469
643
  */
470
644
  async trackPurchase(properties, options) {
471
- return this.track('purchase', properties, options);
645
+ try {
646
+ return await this.track('purchase', properties, options);
647
+ }
648
+ catch (error) {
649
+ const formattedError = this.formatError(error, 'trackPurchase');
650
+ this.logError(formattedError);
651
+ }
472
652
  }
473
653
  /**
474
654
  * Track search event
475
655
  */
476
656
  async trackSearch(properties, options) {
477
- return this.track('search', properties, options);
657
+ try {
658
+ return await this.track('search', properties, options);
659
+ }
660
+ catch (error) {
661
+ const formattedError = this.formatError(error, 'trackSearch');
662
+ this.logError(formattedError);
663
+ }
478
664
  }
479
665
  /**
480
666
  * Track add to cart event
481
667
  */
482
668
  async trackAddToCart(properties, options) {
483
- return this.track('add_to_cart', properties, options);
669
+ try {
670
+ return await this.track('add_to_cart', properties, options);
671
+ }
672
+ catch (error) {
673
+ const formattedError = this.formatError(error, 'trackAddToCart');
674
+ this.logError(formattedError);
675
+ }
484
676
  }
485
677
  /**
486
678
  * Track remove from cart event
487
679
  */
488
680
  async trackRemoveFromCart(properties, options) {
489
- return this.track('remove_from_cart', properties, options);
681
+ try {
682
+ return await this.track('remove_from_cart', properties, options);
683
+ }
684
+ catch (error) {
685
+ const formattedError = this.formatError(error, 'trackRemoveFromCart');
686
+ this.logError(formattedError);
687
+ }
490
688
  }
491
689
  /**
492
690
  * Manually flush all queued events
493
691
  */
494
692
  async flush() {
495
- if (this.eventQueue.length === 0)
496
- return;
497
- const eventsToSend = [...this.eventQueue];
498
- this.eventQueue = [];
499
- // Split events into chunks to respect maxEventsPerRequest limit
500
- const chunks = this.chunkEvents(eventsToSend, this.config.maxEventsPerRequest);
501
- // Send all chunks sequentially to maintain order
502
- for (const chunk of chunks) {
503
- await this.sendEvents(chunk);
693
+ try {
694
+ if (this.eventQueue.length === 0)
695
+ return;
696
+ const eventsToSend = [...this.eventQueue];
697
+ this.eventQueue = [];
698
+ // Split events into chunks to respect maxEventsPerRequest limit
699
+ const chunks = this.chunkEvents(eventsToSend, this.config.maxEventsPerRequest);
700
+ // Send all chunks sequentially to maintain order
701
+ for (const chunk of chunks) {
702
+ await this.sendEvents(chunk);
703
+ }
704
+ }
705
+ catch (error) {
706
+ const formattedError = this.formatError(error, 'flush');
707
+ this.logError(formattedError);
504
708
  }
505
709
  }
506
710
  // Remote Config Methods
@@ -563,89 +767,109 @@ class GrainAnalytics {
563
767
  * Fetch configurations from API
564
768
  */
565
769
  async fetchConfig(options = {}) {
566
- if (this.isDestroyed) {
567
- throw new Error('Grain Analytics: Client has been destroyed');
568
- }
569
- const userId = options.userId || this.getEffectiveUserId();
570
- const immediateKeys = options.immediateKeys || [];
571
- const properties = options.properties || {};
572
- const request = {
573
- userId,
574
- immediateKeys,
575
- properties,
576
- };
577
- let lastError;
578
- for (let attempt = 0; attempt <= this.config.retryAttempts; attempt++) {
579
- try {
580
- const headers = await this.getAuthHeaders();
581
- const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
582
- this.log(`Fetching configurations for user ${userId} (attempt ${attempt + 1})`);
583
- const response = await fetch(url, {
584
- method: 'POST',
585
- headers,
586
- body: JSON.stringify(request),
587
- });
588
- if (!response.ok) {
589
- let errorMessage = `HTTP ${response.status}`;
590
- try {
591
- const errorBody = await response.json();
592
- if (errorBody?.message) {
593
- errorMessage = errorBody.message;
770
+ try {
771
+ if (this.isDestroyed) {
772
+ const error = new Error('Grain Analytics: Client has been destroyed');
773
+ const formattedError = this.formatError(error, 'fetchConfig (client destroyed)');
774
+ this.logError(formattedError);
775
+ return null;
776
+ }
777
+ const userId = options.userId || this.getEffectiveUserId();
778
+ const immediateKeys = options.immediateKeys || [];
779
+ const properties = options.properties || {};
780
+ const request = {
781
+ userId,
782
+ immediateKeys,
783
+ properties,
784
+ };
785
+ let lastError;
786
+ for (let attempt = 0; attempt <= this.config.retryAttempts; attempt++) {
787
+ try {
788
+ const headers = await this.getAuthHeaders();
789
+ const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
790
+ this.log(`Fetching configurations for user ${userId} (attempt ${attempt + 1})`);
791
+ const response = await fetch(url, {
792
+ method: 'POST',
793
+ headers,
794
+ body: JSON.stringify(request),
795
+ });
796
+ if (!response.ok) {
797
+ let errorMessage = `HTTP ${response.status}`;
798
+ try {
799
+ const errorBody = await response.json();
800
+ if (errorBody?.message) {
801
+ errorMessage = errorBody.message;
802
+ }
594
803
  }
595
- }
596
- catch {
597
- const errorText = await response.text();
598
- if (errorText) {
599
- errorMessage = errorText;
804
+ catch {
805
+ const errorText = await response.text();
806
+ if (errorText) {
807
+ errorMessage = errorText;
808
+ }
600
809
  }
810
+ const error = new Error(`Failed to fetch configurations: ${errorMessage}`);
811
+ error.status = response.status;
812
+ throw error;
601
813
  }
602
- const error = new Error(`Failed to fetch configurations: ${errorMessage}`);
603
- error.status = response.status;
604
- throw error;
605
- }
606
- const configResponse = await response.json();
607
- // Update cache if successful
608
- if (configResponse.configurations) {
609
- this.updateConfigCache(configResponse, userId);
610
- }
611
- this.log(`Successfully fetched configurations for user ${userId}:`, configResponse);
612
- return configResponse;
613
- }
614
- catch (error) {
615
- lastError = error;
616
- if (attempt === this.config.retryAttempts) {
617
- break;
814
+ const configResponse = await response.json();
815
+ // Update cache if successful
816
+ if (configResponse.configurations) {
817
+ this.updateConfigCache(configResponse, userId);
818
+ }
819
+ this.log(`Successfully fetched configurations for user ${userId}:`, configResponse);
820
+ return configResponse;
618
821
  }
619
- if (!this.isRetriableError(error)) {
620
- break;
822
+ catch (error) {
823
+ lastError = error;
824
+ if (attempt === this.config.retryAttempts) {
825
+ // Last attempt, don't retry - log error gracefully
826
+ const formattedError = this.formatError(error, `fetchConfig (attempt ${attempt + 1}/${this.config.retryAttempts + 1})`);
827
+ this.logError(formattedError);
828
+ return null;
829
+ }
830
+ if (!this.isRetriableError(error)) {
831
+ // Non-retriable error, don't retry - log error gracefully
832
+ const formattedError = this.formatError(error, 'fetchConfig (non-retriable error)');
833
+ this.logError(formattedError);
834
+ return null;
835
+ }
836
+ const delayMs = this.config.retryDelay * Math.pow(2, attempt);
837
+ this.log(`Retrying config fetch in ${delayMs}ms after error:`, error);
838
+ await this.delay(delayMs);
621
839
  }
622
- const delayMs = this.config.retryDelay * Math.pow(2, attempt);
623
- this.log(`Retrying config fetch in ${delayMs}ms after error:`, error);
624
- await this.delay(delayMs);
625
840
  }
841
+ return null;
842
+ }
843
+ catch (error) {
844
+ const formattedError = this.formatError(error, 'fetchConfig');
845
+ this.logError(formattedError);
846
+ return null;
626
847
  }
627
- console.error('[Grain Analytics] Failed to fetch configurations after all retries:', lastError);
628
- throw lastError;
629
848
  }
630
849
  /**
631
850
  * Get configuration asynchronously (cache-first with fallback to API)
632
851
  */
633
852
  async getConfigAsync(key, options = {}) {
634
- // Return immediately if we have it in cache and not forcing refresh
635
- if (!options.forceRefresh && this.configCache?.configurations?.[key]) {
636
- return this.configCache.configurations[key];
637
- }
638
- // Return default if available and not forcing refresh
639
- if (!options.forceRefresh && this.config.defaultConfigurations?.[key]) {
640
- return this.config.defaultConfigurations[key];
641
- }
642
- // Fetch from API
643
853
  try {
854
+ // Return immediately if we have it in cache and not forcing refresh
855
+ if (!options.forceRefresh && this.configCache?.configurations?.[key]) {
856
+ return this.configCache.configurations[key];
857
+ }
858
+ // Return default if available and not forcing refresh
859
+ if (!options.forceRefresh && this.config.defaultConfigurations?.[key]) {
860
+ return this.config.defaultConfigurations[key];
861
+ }
862
+ // Fetch from API
644
863
  const response = await this.fetchConfig(options);
645
- return response.configurations[key];
864
+ if (response) {
865
+ return response.configurations[key];
866
+ }
867
+ // Return default as fallback
868
+ return this.config.defaultConfigurations?.[key];
646
869
  }
647
870
  catch (error) {
648
- this.log(`Failed to fetch config for key "${key}":`, error);
871
+ const formattedError = this.formatError(error, 'getConfigAsync');
872
+ this.logError(formattedError);
649
873
  // Return default as fallback
650
874
  return this.config.defaultConfigurations?.[key];
651
875
  }
@@ -654,17 +878,22 @@ class GrainAnalytics {
654
878
  * Get all configurations asynchronously (cache-first with fallback to API)
655
879
  */
656
880
  async getAllConfigsAsync(options = {}) {
657
- // Return cache if available and not forcing refresh
658
- if (!options.forceRefresh && this.configCache?.configurations) {
659
- return { ...this.config.defaultConfigurations, ...this.configCache.configurations };
660
- }
661
- // Fetch from API
662
881
  try {
882
+ // Return cache if available and not forcing refresh
883
+ if (!options.forceRefresh && this.configCache?.configurations) {
884
+ return { ...this.config.defaultConfigurations, ...this.configCache.configurations };
885
+ }
886
+ // Fetch from API
663
887
  const response = await this.fetchConfig(options);
664
- return { ...this.config.defaultConfigurations, ...response.configurations };
888
+ if (response) {
889
+ return { ...this.config.defaultConfigurations, ...response.configurations };
890
+ }
891
+ // Return defaults as fallback
892
+ return { ...this.config.defaultConfigurations };
665
893
  }
666
894
  catch (error) {
667
- this.log('Failed to fetch all configs:', error);
895
+ const formattedError = this.formatError(error, 'getAllConfigsAsync');
896
+ this.logError(formattedError);
668
897
  // Return defaults as fallback
669
898
  return { ...this.config.defaultConfigurations };
670
899
  }
@@ -725,7 +954,8 @@ class GrainAnalytics {
725
954
  this.configRefreshTimer = window.setInterval(() => {
726
955
  if (!this.isDestroyed && this.globalUserId) {
727
956
  this.fetchConfig().catch((error) => {
728
- console.error('[Grain Analytics] Auto-config refresh failed:', error);
957
+ const formattedError = this.formatError(error, 'auto-config refresh');
958
+ this.logError(formattedError);
729
959
  });
730
960
  }
731
961
  }, this.config.configRefreshInterval);
@@ -743,16 +973,19 @@ class GrainAnalytics {
743
973
  * Preload configurations for immediate access
744
974
  */
745
975
  async preloadConfig(immediateKeys = [], properties) {
746
- if (!this.globalUserId) {
747
- this.log('Cannot preload config: no user ID set');
748
- return;
749
- }
750
976
  try {
751
- await this.fetchConfig({ immediateKeys, properties });
752
- this.startConfigRefreshTimer();
977
+ if (!this.globalUserId) {
978
+ this.log('Cannot preload config: no user ID set');
979
+ return;
980
+ }
981
+ const response = await this.fetchConfig({ immediateKeys, properties });
982
+ if (response) {
983
+ this.startConfigRefreshTimer();
984
+ }
753
985
  }
754
986
  catch (error) {
755
- this.log('Failed to preload config:', error);
987
+ const formattedError = this.formatError(error, 'preloadConfig');
988
+ this.logError(formattedError);
756
989
  }
757
990
  }
758
991
  /**