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