@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.
- package/dist/cjs/index.d.ts +53 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.d.ts +53 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.ts +53 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.global.dev.js +412 -147
- 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 +462 -161
- package/dist/index.mjs +462 -161
- package/package.json +1 -1
package/dist/index.global.dev.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Grain Analytics Web SDK v1.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
userId
|
|
497
|
-
immediateKeys
|
|
498
|
-
properties
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
|
|
815
|
+
if (response) {
|
|
816
|
+
return response.configurations[key];
|
|
817
|
+
}
|
|
818
|
+
return this.config.defaultConfigurations?.[key];
|
|
563
819
|
} catch (error) {
|
|
564
|
-
this.
|
|
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
|
-
|
|
834
|
+
if (response) {
|
|
835
|
+
return { ...this.config.defaultConfigurations, ...response.configurations };
|
|
836
|
+
}
|
|
837
|
+
return { ...this.config.defaultConfigurations };
|
|
578
838
|
} catch (error) {
|
|
579
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
661
|
-
|
|
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.
|
|
927
|
+
const formattedError = this.formatError(error, "preloadConfig");
|
|
928
|
+
this.logError(formattedError);
|
|
664
929
|
}
|
|
665
930
|
}
|
|
666
931
|
/**
|