@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.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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
372
|
-
|
|
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
|
-
|
|
375
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
userId
|
|
570
|
-
immediateKeys
|
|
571
|
-
properties
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
616
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
748
|
-
|
|
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.
|
|
983
|
+
const formattedError = this.formatError(error, 'preloadConfig');
|
|
984
|
+
this.logError(formattedError);
|
|
752
985
|
}
|
|
753
986
|
}
|
|
754
987
|
/**
|