@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/cjs/index.d.ts +32 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.d.ts +32 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.ts +32 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.global.dev.js +350 -146
- 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 +393 -160
- package/dist/index.mjs +393 -160
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
376
|
-
|
|
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
|
-
|
|
379
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
userId
|
|
574
|
-
immediateKeys
|
|
575
|
-
properties
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
620
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
752
|
-
|
|
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.
|
|
987
|
+
const formattedError = this.formatError(error, 'preloadConfig');
|
|
988
|
+
this.logError(formattedError);
|
|
756
989
|
}
|
|
757
990
|
}
|
|
758
991
|
/**
|