@grainql/analytics-web 1.6.0 → 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.
@@ -1,4 +1,4 @@
1
- /* Grain Analytics Web SDK v1.6.0 | MIT License | Development Build */
1
+ /* Grain Analytics Web SDK v1.7.0 | MIT License | Development Build */
2
2
  "use strict";
3
3
  var Grain = (() => {
4
4
  var __defProp = Object.defineProperty;
@@ -32,6 +32,7 @@ var Grain = (() => {
32
32
  this.flushTimer = null;
33
33
  this.isDestroyed = false;
34
34
  this.globalUserId = null;
35
+ this.persistentAnonymousUserId = null;
35
36
  // Remote Config properties
36
37
  this.configCache = null;
37
38
  this.configRefreshTimer = null;
@@ -62,6 +63,7 @@ var Grain = (() => {
62
63
  this.globalUserId = config.userId;
63
64
  }
64
65
  this.validateConfig();
66
+ this.initializePersistentAnonymousUserId();
65
67
  this.setupBeforeUnload();
66
68
  this.startFlushTimer();
67
69
  this.initializeConfigCache();
@@ -77,15 +79,171 @@ var Grain = (() => {
77
79
  throw new Error("Grain Analytics: authProvider is required for JWT auth strategy");
78
80
  }
79
81
  }
82
+ /**
83
+ * Generate a UUID v4 string
84
+ */
85
+ generateUUID() {
86
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
87
+ return crypto.randomUUID();
88
+ }
89
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
90
+ const r = Math.random() * 16 | 0;
91
+ const v = c === "x" ? r : r & 3 | 8;
92
+ return v.toString(16);
93
+ });
94
+ }
95
+ /**
96
+ * Format UUID for anonymous user ID (remove dashes and prefix with 'temp:')
97
+ */
98
+ formatAnonymousUserId(uuid) {
99
+ return `temp:${uuid.replace(/-/g, "")}`;
100
+ }
101
+ /**
102
+ * Initialize persistent anonymous user ID from localStorage or create new one
103
+ */
104
+ initializePersistentAnonymousUserId() {
105
+ if (typeof window === "undefined")
106
+ return;
107
+ const storageKey = `grain_anonymous_user_id_${this.config.tenantId}`;
108
+ try {
109
+ const stored = localStorage.getItem(storageKey);
110
+ if (stored) {
111
+ this.persistentAnonymousUserId = stored;
112
+ this.log("Loaded persistent anonymous user ID:", this.persistentAnonymousUserId);
113
+ } else {
114
+ const uuid = this.generateUUID();
115
+ this.persistentAnonymousUserId = this.formatAnonymousUserId(uuid);
116
+ localStorage.setItem(storageKey, this.persistentAnonymousUserId);
117
+ this.log("Generated new persistent anonymous user ID:", this.persistentAnonymousUserId);
118
+ }
119
+ } catch (error) {
120
+ this.log("Failed to initialize persistent anonymous user ID:", error);
121
+ const uuid = this.generateUUID();
122
+ this.persistentAnonymousUserId = this.formatAnonymousUserId(uuid);
123
+ }
124
+ }
125
+ /**
126
+ * Get the effective user ID (global userId or persistent anonymous ID)
127
+ */
128
+ getEffectiveUserId() {
129
+ return this.globalUserId || this.persistentAnonymousUserId || "anonymous";
130
+ }
80
131
  log(...args) {
81
132
  if (this.config.debug) {
82
133
  console.log("[Grain Analytics]", ...args);
83
134
  }
84
135
  }
136
+ /**
137
+ * Create error digest from events
138
+ */
139
+ createErrorDigest(events) {
140
+ const eventNames = [...new Set(events.map((e) => e.eventName))];
141
+ const userIds = [...new Set(events.map((e) => e.userId))];
142
+ let totalProperties = 0;
143
+ let totalSize = 0;
144
+ events.forEach((event) => {
145
+ const properties = event.properties || {};
146
+ totalProperties += Object.keys(properties).length;
147
+ totalSize += JSON.stringify(event).length;
148
+ });
149
+ return {
150
+ eventCount: events.length,
151
+ totalProperties,
152
+ totalSize,
153
+ eventNames,
154
+ userIds
155
+ };
156
+ }
157
+ /**
158
+ * Format error with beautiful structure
159
+ */
160
+ formatError(error, context, events) {
161
+ const digest = events ? this.createErrorDigest(events) : {
162
+ eventCount: 0,
163
+ totalProperties: 0,
164
+ totalSize: 0,
165
+ eventNames: [],
166
+ userIds: []
167
+ };
168
+ let code = "UNKNOWN_ERROR";
169
+ let message = "An unknown error occurred";
170
+ if (error instanceof Error) {
171
+ message = error.message;
172
+ if (message.includes("fetch failed") || message.includes("network error")) {
173
+ code = "NETWORK_ERROR";
174
+ } else if (message.includes("timeout")) {
175
+ code = "TIMEOUT_ERROR";
176
+ } else if (message.includes("HTTP 4")) {
177
+ code = "CLIENT_ERROR";
178
+ } else if (message.includes("HTTP 5")) {
179
+ code = "SERVER_ERROR";
180
+ } else if (message.includes("JSON")) {
181
+ code = "PARSE_ERROR";
182
+ } else if (message.includes("auth") || message.includes("unauthorized")) {
183
+ code = "AUTH_ERROR";
184
+ } else if (message.includes("rate limit") || message.includes("429")) {
185
+ code = "RATE_LIMIT_ERROR";
186
+ } else {
187
+ code = "GENERAL_ERROR";
188
+ }
189
+ } else if (typeof error === "string") {
190
+ message = error;
191
+ code = "STRING_ERROR";
192
+ } else if (error && typeof error === "object" && "status" in error) {
193
+ const status = error.status;
194
+ code = `HTTP_${status}`;
195
+ message = `HTTP ${status} error`;
196
+ }
197
+ return {
198
+ code,
199
+ message,
200
+ digest,
201
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
202
+ context,
203
+ originalError: error
204
+ };
205
+ }
206
+ /**
207
+ * Log formatted error gracefully
208
+ */
209
+ logError(formattedError) {
210
+ const { code, message, digest, timestamp, context } = formattedError;
211
+ const errorOutput = {
212
+ "\u{1F6A8} Grain Analytics Error": {
213
+ "Error Code": code,
214
+ "Message": message,
215
+ "Context": context,
216
+ "Timestamp": timestamp,
217
+ "Event Digest": {
218
+ "Events": digest.eventCount,
219
+ "Properties": digest.totalProperties,
220
+ "Size (bytes)": digest.totalSize,
221
+ "Event Names": digest.eventNames.length > 0 ? digest.eventNames.join(", ") : "None",
222
+ "User IDs": digest.userIds.length > 0 ? digest.userIds.slice(0, 3).join(", ") + (digest.userIds.length > 3 ? "..." : "") : "None"
223
+ }
224
+ }
225
+ };
226
+ console.error("\u{1F6A8} Grain Analytics Error:", errorOutput);
227
+ if (this.config.debug) {
228
+ console.error(`[Grain Analytics] ${code}: ${message} (${context}) - Events: ${digest.eventCount}, Props: ${digest.totalProperties}, Size: ${digest.totalSize}B`);
229
+ }
230
+ }
231
+ /**
232
+ * Safely execute a function with error handling
233
+ */
234
+ async safeExecute(fn, context, events) {
235
+ try {
236
+ return await fn();
237
+ } catch (error) {
238
+ const formattedError = this.formatError(error, context, events);
239
+ this.logError(formattedError);
240
+ return null;
241
+ }
242
+ }
85
243
  formatEvent(event) {
86
244
  return {
87
245
  eventName: event.eventName,
88
- userId: event.userId || this.globalUserId || "anonymous",
246
+ userId: event.userId || this.getEffectiveUserId(),
89
247
  properties: event.properties || {}
90
248
  };
91
249
  }
@@ -165,18 +323,20 @@ var Grain = (() => {
165
323
  } catch (error) {
166
324
  lastError = error;
167
325
  if (attempt === this.config.retryAttempts) {
168
- break;
326
+ const formattedError = this.formatError(error, `sendEvents (attempt ${attempt + 1}/${this.config.retryAttempts + 1})`, events);
327
+ this.logError(formattedError);
328
+ return;
169
329
  }
170
330
  if (!this.isRetriableError(error)) {
171
- break;
331
+ const formattedError = this.formatError(error, `sendEvents (non-retriable error)`, events);
332
+ this.logError(formattedError);
333
+ return;
172
334
  }
173
335
  const delayMs = this.config.retryDelay * Math.pow(2, attempt);
174
336
  this.log(`Retrying in ${delayMs}ms after error:`, error);
175
337
  await this.delay(delayMs);
176
338
  }
177
339
  }
178
- console.error("[Grain Analytics] Failed to send events after all retries:", lastError);
179
- throw lastError;
180
340
  }
181
341
  async sendEventsWithBeacon(events) {
182
342
  if (events.length === 0)
@@ -201,7 +361,8 @@ var Grain = (() => {
201
361
  });
202
362
  this.log(`Successfully sent ${events.length} events via fetch (keepalive)`);
203
363
  } catch (error) {
204
- console.error("[Grain Analytics] Failed to send events via beacon:", error);
364
+ const formattedError = this.formatError(error, "sendEventsWithBeacon", events);
365
+ this.logError(formattedError);
205
366
  }
206
367
  }
207
368
  startFlushTimer() {
@@ -211,7 +372,8 @@ var Grain = (() => {
211
372
  this.flushTimer = window.setInterval(() => {
212
373
  if (this.eventQueue.length > 0) {
213
374
  this.flush().catch((error) => {
214
- console.error("[Grain Analytics] Auto-flush failed:", error);
375
+ const formattedError = this.formatError(error, "auto-flush");
376
+ this.logError(formattedError);
215
377
  });
216
378
  }
217
379
  }, this.config.flushInterval);
@@ -245,26 +407,34 @@ var Grain = (() => {
245
407
  });
246
408
  }
247
409
  async track(eventOrName, propertiesOrOptions, options) {
248
- if (this.isDestroyed) {
249
- throw new Error("Grain Analytics: Client has been destroyed");
250
- }
251
- let event;
252
- let opts = {};
253
- if (typeof eventOrName === "string") {
254
- event = {
255
- eventName: eventOrName,
256
- properties: propertiesOrOptions
257
- };
258
- opts = options || {};
259
- } else {
260
- event = eventOrName;
261
- opts = propertiesOrOptions || {};
262
- }
263
- const formattedEvent = this.formatEvent(event);
264
- this.eventQueue.push(formattedEvent);
265
- this.log(`Queued event: ${event.eventName}`, event.properties);
266
- if (opts.flush || this.eventQueue.length >= this.config.batchSize) {
267
- await this.flush();
410
+ try {
411
+ if (this.isDestroyed) {
412
+ const error = new Error("Grain Analytics: Client has been destroyed");
413
+ const formattedError = this.formatError(error, "track (client destroyed)");
414
+ this.logError(formattedError);
415
+ return;
416
+ }
417
+ let event;
418
+ let opts = {};
419
+ if (typeof eventOrName === "string") {
420
+ event = {
421
+ eventName: eventOrName,
422
+ properties: propertiesOrOptions
423
+ };
424
+ opts = options || {};
425
+ } else {
426
+ event = eventOrName;
427
+ opts = propertiesOrOptions || {};
428
+ }
429
+ const formattedEvent = this.formatEvent(event);
430
+ this.eventQueue.push(formattedEvent);
431
+ this.log(`Queued event: ${event.eventName}`, event.properties);
432
+ if (opts.flush || this.eventQueue.length >= this.config.batchSize) {
433
+ await this.flush();
434
+ }
435
+ } catch (error) {
436
+ const formattedError = this.formatError(error, "track");
437
+ this.logError(formattedError);
268
438
  }
269
439
  }
270
440
  /**
@@ -273,6 +443,7 @@ var Grain = (() => {
273
443
  identify(userId) {
274
444
  this.log(`Identified user: ${userId}`);
275
445
  this.globalUserId = userId;
446
+ this.persistentAnonymousUserId = null;
276
447
  }
277
448
  /**
278
449
  * Set global user ID for all subsequent events
@@ -280,6 +451,9 @@ var Grain = (() => {
280
451
  setUserId(userId) {
281
452
  this.log(`Set global user ID: ${userId}`);
282
453
  this.globalUserId = userId;
454
+ if (userId) {
455
+ this.persistentAnonymousUserId = null;
456
+ }
283
457
  }
284
458
  /**
285
459
  * Get current global user ID
@@ -287,36 +461,56 @@ var Grain = (() => {
287
461
  getUserId() {
288
462
  return this.globalUserId;
289
463
  }
464
+ /**
465
+ * Get current effective user ID (global userId or persistent anonymous ID)
466
+ */
467
+ getEffectiveUserIdPublic() {
468
+ return this.getEffectiveUserId();
469
+ }
290
470
  /**
291
471
  * Set user properties
292
472
  */
293
473
  async setProperty(properties, options) {
294
- if (this.isDestroyed) {
295
- throw new Error("Grain Analytics: Client has been destroyed");
296
- }
297
- const userId = options?.userId || this.globalUserId || "anonymous";
298
- const propertyKeys = Object.keys(properties);
299
- if (propertyKeys.length > 4) {
300
- throw new Error("Grain Analytics: Maximum 4 properties allowed per request");
301
- }
302
- if (propertyKeys.length === 0) {
303
- throw new Error("Grain Analytics: At least one property is required");
304
- }
305
- const serializedProperties = {};
306
- for (const [key, value] of Object.entries(properties)) {
307
- if (value === null || value === void 0) {
308
- serializedProperties[key] = "";
309
- } else if (typeof value === "string") {
310
- serializedProperties[key] = value;
311
- } else {
312
- serializedProperties[key] = JSON.stringify(value);
474
+ try {
475
+ if (this.isDestroyed) {
476
+ const error = new Error("Grain Analytics: Client has been destroyed");
477
+ const formattedError = this.formatError(error, "setProperty (client destroyed)");
478
+ this.logError(formattedError);
479
+ return;
480
+ }
481
+ const userId = options?.userId || this.getEffectiveUserId();
482
+ const propertyKeys = Object.keys(properties);
483
+ if (propertyKeys.length > 4) {
484
+ const error = new Error("Grain Analytics: Maximum 4 properties allowed per request");
485
+ const formattedError = this.formatError(error, "setProperty (validation)");
486
+ this.logError(formattedError);
487
+ return;
488
+ }
489
+ if (propertyKeys.length === 0) {
490
+ const error = new Error("Grain Analytics: At least one property is required");
491
+ const formattedError = this.formatError(error, "setProperty (validation)");
492
+ this.logError(formattedError);
493
+ return;
494
+ }
495
+ const serializedProperties = {};
496
+ for (const [key, value] of Object.entries(properties)) {
497
+ if (value === null || value === void 0) {
498
+ serializedProperties[key] = "";
499
+ } else if (typeof value === "string") {
500
+ serializedProperties[key] = value;
501
+ } else {
502
+ serializedProperties[key] = JSON.stringify(value);
503
+ }
313
504
  }
505
+ const payload = {
506
+ userId,
507
+ ...serializedProperties
508
+ };
509
+ await this.sendProperties(payload);
510
+ } catch (error) {
511
+ const formattedError = this.formatError(error, "setProperty");
512
+ this.logError(formattedError);
314
513
  }
315
- const payload = {
316
- userId,
317
- ...serializedProperties
318
- };
319
- await this.sendProperties(payload);
320
514
  }
321
515
  /**
322
516
  * Send properties to the API
@@ -355,79 +549,126 @@ var Grain = (() => {
355
549
  } catch (error) {
356
550
  lastError = error;
357
551
  if (attempt === this.config.retryAttempts) {
358
- break;
552
+ const formattedError = this.formatError(error, `sendProperties (attempt ${attempt + 1}/${this.config.retryAttempts + 1})`);
553
+ this.logError(formattedError);
554
+ return;
359
555
  }
360
556
  if (!this.isRetriableError(error)) {
361
- break;
557
+ const formattedError = this.formatError(error, "sendProperties (non-retriable error)");
558
+ this.logError(formattedError);
559
+ return;
362
560
  }
363
561
  const delayMs = this.config.retryDelay * Math.pow(2, attempt);
364
562
  this.log(`Retrying in ${delayMs}ms after error:`, error);
365
563
  await this.delay(delayMs);
366
564
  }
367
565
  }
368
- console.error("[Grain Analytics] Failed to set properties after all retries:", lastError);
369
- throw lastError;
370
566
  }
371
567
  // Template event methods
372
568
  /**
373
569
  * Track user login event
374
570
  */
375
571
  async trackLogin(properties, options) {
376
- return this.track("login", properties, options);
572
+ try {
573
+ return await this.track("login", properties, options);
574
+ } catch (error) {
575
+ const formattedError = this.formatError(error, "trackLogin");
576
+ this.logError(formattedError);
577
+ }
377
578
  }
378
579
  /**
379
580
  * Track user signup event
380
581
  */
381
582
  async trackSignup(properties, options) {
382
- return this.track("signup", properties, options);
583
+ try {
584
+ return await this.track("signup", properties, options);
585
+ } catch (error) {
586
+ const formattedError = this.formatError(error, "trackSignup");
587
+ this.logError(formattedError);
588
+ }
383
589
  }
384
590
  /**
385
591
  * Track checkout event
386
592
  */
387
593
  async trackCheckout(properties, options) {
388
- return this.track("checkout", properties, options);
594
+ try {
595
+ return await this.track("checkout", properties, options);
596
+ } catch (error) {
597
+ const formattedError = this.formatError(error, "trackCheckout");
598
+ this.logError(formattedError);
599
+ }
389
600
  }
390
601
  /**
391
602
  * Track page view event
392
603
  */
393
604
  async trackPageView(properties, options) {
394
- return this.track("page_view", properties, options);
605
+ try {
606
+ return await this.track("page_view", properties, options);
607
+ } catch (error) {
608
+ const formattedError = this.formatError(error, "trackPageView");
609
+ this.logError(formattedError);
610
+ }
395
611
  }
396
612
  /**
397
613
  * Track purchase event
398
614
  */
399
615
  async trackPurchase(properties, options) {
400
- return this.track("purchase", properties, options);
616
+ try {
617
+ return await this.track("purchase", properties, options);
618
+ } catch (error) {
619
+ const formattedError = this.formatError(error, "trackPurchase");
620
+ this.logError(formattedError);
621
+ }
401
622
  }
402
623
  /**
403
624
  * Track search event
404
625
  */
405
626
  async trackSearch(properties, options) {
406
- return this.track("search", properties, options);
627
+ try {
628
+ return await this.track("search", properties, options);
629
+ } catch (error) {
630
+ const formattedError = this.formatError(error, "trackSearch");
631
+ this.logError(formattedError);
632
+ }
407
633
  }
408
634
  /**
409
635
  * Track add to cart event
410
636
  */
411
637
  async trackAddToCart(properties, options) {
412
- return this.track("add_to_cart", properties, options);
638
+ try {
639
+ return await this.track("add_to_cart", properties, options);
640
+ } catch (error) {
641
+ const formattedError = this.formatError(error, "trackAddToCart");
642
+ this.logError(formattedError);
643
+ }
413
644
  }
414
645
  /**
415
646
  * Track remove from cart event
416
647
  */
417
648
  async trackRemoveFromCart(properties, options) {
418
- return this.track("remove_from_cart", properties, options);
649
+ try {
650
+ return await this.track("remove_from_cart", properties, options);
651
+ } catch (error) {
652
+ const formattedError = this.formatError(error, "trackRemoveFromCart");
653
+ this.logError(formattedError);
654
+ }
419
655
  }
420
656
  /**
421
657
  * Manually flush all queued events
422
658
  */
423
659
  async flush() {
424
- if (this.eventQueue.length === 0)
425
- return;
426
- const eventsToSend = [...this.eventQueue];
427
- this.eventQueue = [];
428
- const chunks = this.chunkEvents(eventsToSend, this.config.maxEventsPerRequest);
429
- for (const chunk of chunks) {
430
- await this.sendEvents(chunk);
660
+ try {
661
+ if (this.eventQueue.length === 0)
662
+ return;
663
+ const eventsToSend = [...this.eventQueue];
664
+ this.eventQueue = [];
665
+ const chunks = this.chunkEvents(eventsToSend, this.config.maxEventsPerRequest);
666
+ for (const chunk of chunks) {
667
+ await this.sendEvents(chunk);
668
+ }
669
+ } catch (error) {
670
+ const formattedError = this.formatError(error, "flush");
671
+ this.logError(formattedError);
431
672
  }
432
673
  }
433
674
  // Remote Config Methods
@@ -486,82 +727,98 @@ var Grain = (() => {
486
727
  * Fetch configurations from API
487
728
  */
488
729
  async fetchConfig(options = {}) {
489
- if (this.isDestroyed) {
490
- throw new Error("Grain Analytics: Client has been destroyed");
491
- }
492
- const userId = options.userId || this.globalUserId || "anonymous";
493
- const immediateKeys = options.immediateKeys || [];
494
- const properties = options.properties || {};
495
- const request = {
496
- userId,
497
- immediateKeys,
498
- properties
499
- };
500
- let lastError;
501
- for (let attempt = 0; attempt <= this.config.retryAttempts; attempt++) {
502
- try {
503
- const headers = await this.getAuthHeaders();
504
- const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
505
- this.log(`Fetching configurations for user ${userId} (attempt ${attempt + 1})`);
506
- const response = await fetch(url, {
507
- method: "POST",
508
- headers,
509
- body: JSON.stringify(request)
510
- });
511
- if (!response.ok) {
512
- let errorMessage = `HTTP ${response.status}`;
513
- try {
514
- const errorBody = await response.json();
515
- if (errorBody?.message) {
516
- errorMessage = errorBody.message;
517
- }
518
- } catch {
519
- const errorText = await response.text();
520
- if (errorText) {
521
- errorMessage = errorText;
730
+ try {
731
+ if (this.isDestroyed) {
732
+ const error = new Error("Grain Analytics: Client has been destroyed");
733
+ const formattedError = this.formatError(error, "fetchConfig (client destroyed)");
734
+ this.logError(formattedError);
735
+ return null;
736
+ }
737
+ const userId = options.userId || this.getEffectiveUserId();
738
+ const immediateKeys = options.immediateKeys || [];
739
+ const properties = options.properties || {};
740
+ const request = {
741
+ userId,
742
+ immediateKeys,
743
+ properties
744
+ };
745
+ let lastError;
746
+ for (let attempt = 0; attempt <= this.config.retryAttempts; attempt++) {
747
+ try {
748
+ const headers = await this.getAuthHeaders();
749
+ const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
750
+ this.log(`Fetching configurations for user ${userId} (attempt ${attempt + 1})`);
751
+ const response = await fetch(url, {
752
+ method: "POST",
753
+ headers,
754
+ body: JSON.stringify(request)
755
+ });
756
+ if (!response.ok) {
757
+ let errorMessage = `HTTP ${response.status}`;
758
+ try {
759
+ const errorBody = await response.json();
760
+ if (errorBody?.message) {
761
+ errorMessage = errorBody.message;
762
+ }
763
+ } catch {
764
+ const errorText = await response.text();
765
+ if (errorText) {
766
+ errorMessage = errorText;
767
+ }
522
768
  }
769
+ const error = new Error(`Failed to fetch configurations: ${errorMessage}`);
770
+ error.status = response.status;
771
+ throw error;
523
772
  }
524
- const error = new Error(`Failed to fetch configurations: ${errorMessage}`);
525
- error.status = response.status;
526
- throw error;
527
- }
528
- const configResponse = await response.json();
529
- if (configResponse.configurations) {
530
- this.updateConfigCache(configResponse, userId);
531
- }
532
- this.log(`Successfully fetched configurations for user ${userId}:`, configResponse);
533
- return configResponse;
534
- } catch (error) {
535
- lastError = error;
536
- if (attempt === this.config.retryAttempts) {
537
- break;
538
- }
539
- if (!this.isRetriableError(error)) {
540
- break;
773
+ const configResponse = await response.json();
774
+ if (configResponse.configurations) {
775
+ this.updateConfigCache(configResponse, userId);
776
+ }
777
+ this.log(`Successfully fetched configurations for user ${userId}:`, configResponse);
778
+ return configResponse;
779
+ } catch (error) {
780
+ lastError = error;
781
+ if (attempt === this.config.retryAttempts) {
782
+ const formattedError = this.formatError(error, `fetchConfig (attempt ${attempt + 1}/${this.config.retryAttempts + 1})`);
783
+ this.logError(formattedError);
784
+ return null;
785
+ }
786
+ if (!this.isRetriableError(error)) {
787
+ const formattedError = this.formatError(error, "fetchConfig (non-retriable error)");
788
+ this.logError(formattedError);
789
+ return null;
790
+ }
791
+ const delayMs = this.config.retryDelay * Math.pow(2, attempt);
792
+ this.log(`Retrying config fetch in ${delayMs}ms after error:`, error);
793
+ await this.delay(delayMs);
541
794
  }
542
- const delayMs = this.config.retryDelay * Math.pow(2, attempt);
543
- this.log(`Retrying config fetch in ${delayMs}ms after error:`, error);
544
- await this.delay(delayMs);
545
795
  }
796
+ return null;
797
+ } catch (error) {
798
+ const formattedError = this.formatError(error, "fetchConfig");
799
+ this.logError(formattedError);
800
+ return null;
546
801
  }
547
- console.error("[Grain Analytics] Failed to fetch configurations after all retries:", lastError);
548
- throw lastError;
549
802
  }
550
803
  /**
551
804
  * Get configuration asynchronously (cache-first with fallback to API)
552
805
  */
553
806
  async getConfigAsync(key, options = {}) {
554
- if (!options.forceRefresh && this.configCache?.configurations?.[key]) {
555
- return this.configCache.configurations[key];
556
- }
557
- if (!options.forceRefresh && this.config.defaultConfigurations?.[key]) {
558
- return this.config.defaultConfigurations[key];
559
- }
560
807
  try {
808
+ if (!options.forceRefresh && this.configCache?.configurations?.[key]) {
809
+ return this.configCache.configurations[key];
810
+ }
811
+ if (!options.forceRefresh && this.config.defaultConfigurations?.[key]) {
812
+ return this.config.defaultConfigurations[key];
813
+ }
561
814
  const response = await this.fetchConfig(options);
562
- return response.configurations[key];
815
+ if (response) {
816
+ return response.configurations[key];
817
+ }
818
+ return this.config.defaultConfigurations?.[key];
563
819
  } catch (error) {
564
- this.log(`Failed to fetch config for key "${key}":`, error);
820
+ const formattedError = this.formatError(error, "getConfigAsync");
821
+ this.logError(formattedError);
565
822
  return this.config.defaultConfigurations?.[key];
566
823
  }
567
824
  }
@@ -569,14 +826,18 @@ var Grain = (() => {
569
826
  * Get all configurations asynchronously (cache-first with fallback to API)
570
827
  */
571
828
  async getAllConfigsAsync(options = {}) {
572
- if (!options.forceRefresh && this.configCache?.configurations) {
573
- return { ...this.config.defaultConfigurations, ...this.configCache.configurations };
574
- }
575
829
  try {
830
+ if (!options.forceRefresh && this.configCache?.configurations) {
831
+ return { ...this.config.defaultConfigurations, ...this.configCache.configurations };
832
+ }
576
833
  const response = await this.fetchConfig(options);
577
- return { ...this.config.defaultConfigurations, ...response.configurations };
834
+ if (response) {
835
+ return { ...this.config.defaultConfigurations, ...response.configurations };
836
+ }
837
+ return { ...this.config.defaultConfigurations };
578
838
  } catch (error) {
579
- this.log("Failed to fetch all configs:", error);
839
+ const formattedError = this.formatError(error, "getAllConfigsAsync");
840
+ this.logError(formattedError);
580
841
  return { ...this.config.defaultConfigurations };
581
842
  }
582
843
  }
@@ -634,7 +895,8 @@ var Grain = (() => {
634
895
  this.configRefreshTimer = window.setInterval(() => {
635
896
  if (!this.isDestroyed && this.globalUserId) {
636
897
  this.fetchConfig().catch((error) => {
637
- console.error("[Grain Analytics] Auto-config refresh failed:", error);
898
+ const formattedError = this.formatError(error, "auto-config refresh");
899
+ this.logError(formattedError);
638
900
  });
639
901
  }
640
902
  }, this.config.configRefreshInterval);
@@ -652,15 +914,18 @@ var Grain = (() => {
652
914
  * Preload configurations for immediate access
653
915
  */
654
916
  async preloadConfig(immediateKeys = [], properties) {
655
- if (!this.globalUserId) {
656
- this.log("Cannot preload config: no user ID set");
657
- return;
658
- }
659
917
  try {
660
- await this.fetchConfig({ immediateKeys, properties });
661
- this.startConfigRefreshTimer();
918
+ if (!this.globalUserId) {
919
+ this.log("Cannot preload config: no user ID set");
920
+ return;
921
+ }
922
+ const response = await this.fetchConfig({ immediateKeys, properties });
923
+ if (response) {
924
+ this.startConfigRefreshTimer();
925
+ }
662
926
  } catch (error) {
663
- this.log("Failed to preload config:", error);
927
+ const formattedError = this.formatError(error, "preloadConfig");
928
+ this.logError(formattedError);
664
929
  }
665
930
  }
666
931
  /**