@grainql/analytics-web 1.2.0 → 1.4.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/README.md +283 -7
- package/dist/cjs/index.d.ts +141 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.d.ts +141 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.ts +141 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.global.dev.js +153 -5
- 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 +161 -6
- package/dist/index.mjs +161 -6
- package/package.json +5 -5
package/dist/index.mjs
CHANGED
|
@@ -7,6 +7,7 @@ export class GrainAnalytics {
|
|
|
7
7
|
this.eventQueue = [];
|
|
8
8
|
this.flushTimer = null;
|
|
9
9
|
this.isDestroyed = false;
|
|
10
|
+
this.globalUserId = null;
|
|
10
11
|
this.config = {
|
|
11
12
|
apiUrl: 'https://api.grainql.com',
|
|
12
13
|
authStrategy: 'NONE',
|
|
@@ -19,6 +20,10 @@ export class GrainAnalytics {
|
|
|
19
20
|
...config,
|
|
20
21
|
tenantId: config.tenantId,
|
|
21
22
|
};
|
|
23
|
+
// Set global userId if provided in config
|
|
24
|
+
if (config.userId) {
|
|
25
|
+
this.globalUserId = config.userId;
|
|
26
|
+
}
|
|
22
27
|
this.validateConfig();
|
|
23
28
|
this.setupBeforeUnload();
|
|
24
29
|
this.startFlushTimer();
|
|
@@ -42,7 +47,7 @@ export class GrainAnalytics {
|
|
|
42
47
|
formatEvent(event) {
|
|
43
48
|
return {
|
|
44
49
|
eventName: event.eventName,
|
|
45
|
-
userId: event.userId || 'anonymous',
|
|
50
|
+
userId: event.userId || this.globalUserId || 'anonymous',
|
|
46
51
|
properties: event.properties || {},
|
|
47
52
|
};
|
|
48
53
|
}
|
|
@@ -95,12 +100,12 @@ export class GrainAnalytics {
|
|
|
95
100
|
for (let attempt = 0; attempt <= this.config.retryAttempts; attempt++) {
|
|
96
101
|
try {
|
|
97
102
|
const headers = await this.getAuthHeaders();
|
|
98
|
-
const url = `${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}`;
|
|
103
|
+
const url = `${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}/multi`;
|
|
99
104
|
this.log(`Sending ${events.length} events to ${url} (attempt ${attempt + 1})`);
|
|
100
105
|
const response = await fetch(url, {
|
|
101
106
|
method: 'POST',
|
|
102
107
|
headers,
|
|
103
|
-
body: JSON.stringify(events),
|
|
108
|
+
body: JSON.stringify({ events }),
|
|
104
109
|
});
|
|
105
110
|
if (!response.ok) {
|
|
106
111
|
let errorMessage = `HTTP ${response.status}`;
|
|
@@ -146,7 +151,7 @@ export class GrainAnalytics {
|
|
|
146
151
|
return;
|
|
147
152
|
try {
|
|
148
153
|
const headers = await this.getAuthHeaders();
|
|
149
|
-
const url = `${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}`;
|
|
154
|
+
const url = `${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}/multi`;
|
|
150
155
|
const body = JSON.stringify({ events });
|
|
151
156
|
// Try beacon API first (more reliable for page unload)
|
|
152
157
|
if (typeof navigator !== 'undefined' && 'sendBeacon' in navigator) {
|
|
@@ -246,9 +251,159 @@ export class GrainAnalytics {
|
|
|
246
251
|
* Identify a user (sets userId for subsequent events)
|
|
247
252
|
*/
|
|
248
253
|
identify(userId) {
|
|
249
|
-
// Store userId for future events - this would typically be handled
|
|
250
|
-
// by the application layer, but we can provide a helper
|
|
251
254
|
this.log(`Identified user: ${userId}`);
|
|
255
|
+
this.globalUserId = userId;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Set global user ID for all subsequent events
|
|
259
|
+
*/
|
|
260
|
+
setUserId(userId) {
|
|
261
|
+
this.log(`Set global user ID: ${userId}`);
|
|
262
|
+
this.globalUserId = userId;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Get current global user ID
|
|
266
|
+
*/
|
|
267
|
+
getUserId() {
|
|
268
|
+
return this.globalUserId;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Set user properties
|
|
272
|
+
*/
|
|
273
|
+
async setProperty(properties, options) {
|
|
274
|
+
if (this.isDestroyed) {
|
|
275
|
+
throw new Error('Grain Analytics: Client has been destroyed');
|
|
276
|
+
}
|
|
277
|
+
const userId = options?.userId || this.globalUserId || 'anonymous';
|
|
278
|
+
// Validate property count (max 4 properties)
|
|
279
|
+
const propertyKeys = Object.keys(properties);
|
|
280
|
+
if (propertyKeys.length > 4) {
|
|
281
|
+
throw new Error('Grain Analytics: Maximum 4 properties allowed per request');
|
|
282
|
+
}
|
|
283
|
+
if (propertyKeys.length === 0) {
|
|
284
|
+
throw new Error('Grain Analytics: At least one property is required');
|
|
285
|
+
}
|
|
286
|
+
// Serialize all values to strings
|
|
287
|
+
const serializedProperties = {};
|
|
288
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
289
|
+
if (value === null || value === undefined) {
|
|
290
|
+
serializedProperties[key] = '';
|
|
291
|
+
}
|
|
292
|
+
else if (typeof value === 'string') {
|
|
293
|
+
serializedProperties[key] = value;
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
serializedProperties[key] = JSON.stringify(value);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const payload = {
|
|
300
|
+
userId,
|
|
301
|
+
...serializedProperties,
|
|
302
|
+
};
|
|
303
|
+
await this.sendProperties(payload);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Send properties to the API
|
|
307
|
+
*/
|
|
308
|
+
async sendProperties(payload) {
|
|
309
|
+
let lastError;
|
|
310
|
+
for (let attempt = 0; attempt <= this.config.retryAttempts; attempt++) {
|
|
311
|
+
try {
|
|
312
|
+
const headers = await this.getAuthHeaders();
|
|
313
|
+
const url = `${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}/properties`;
|
|
314
|
+
this.log(`Setting properties for user ${payload.userId} (attempt ${attempt + 1})`);
|
|
315
|
+
const response = await fetch(url, {
|
|
316
|
+
method: 'POST',
|
|
317
|
+
headers,
|
|
318
|
+
body: JSON.stringify(payload),
|
|
319
|
+
});
|
|
320
|
+
if (!response.ok) {
|
|
321
|
+
let errorMessage = `HTTP ${response.status}`;
|
|
322
|
+
try {
|
|
323
|
+
const errorBody = await response.json();
|
|
324
|
+
if (errorBody?.message) {
|
|
325
|
+
errorMessage = errorBody.message;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
const errorText = await response.text();
|
|
330
|
+
if (errorText) {
|
|
331
|
+
errorMessage = errorText;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const error = new Error(`Failed to set properties: ${errorMessage}`);
|
|
335
|
+
error.status = response.status;
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
this.log(`Successfully set properties for user ${payload.userId}`);
|
|
339
|
+
return; // Success, exit retry loop
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
lastError = error;
|
|
343
|
+
if (attempt === this.config.retryAttempts) {
|
|
344
|
+
// Last attempt, don't retry
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
if (!this.isRetriableError(error)) {
|
|
348
|
+
// Non-retriable error, don't retry
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
const delayMs = this.config.retryDelay * Math.pow(2, attempt); // Exponential backoff
|
|
352
|
+
this.log(`Retrying in ${delayMs}ms after error:`, error);
|
|
353
|
+
await this.delay(delayMs);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
console.error('[Grain Analytics] Failed to set properties after all retries:', lastError);
|
|
357
|
+
throw lastError;
|
|
358
|
+
}
|
|
359
|
+
// Template event methods
|
|
360
|
+
/**
|
|
361
|
+
* Track user login event
|
|
362
|
+
*/
|
|
363
|
+
async trackLogin(properties, options) {
|
|
364
|
+
return this.track('login', properties, options);
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Track user signup event
|
|
368
|
+
*/
|
|
369
|
+
async trackSignup(properties, options) {
|
|
370
|
+
return this.track('signup', properties, options);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Track checkout event
|
|
374
|
+
*/
|
|
375
|
+
async trackCheckout(properties, options) {
|
|
376
|
+
return this.track('checkout', properties, options);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Track page view event
|
|
380
|
+
*/
|
|
381
|
+
async trackPageView(properties, options) {
|
|
382
|
+
return this.track('page_view', properties, options);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Track purchase event
|
|
386
|
+
*/
|
|
387
|
+
async trackPurchase(properties, options) {
|
|
388
|
+
return this.track('purchase', properties, options);
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Track search event
|
|
392
|
+
*/
|
|
393
|
+
async trackSearch(properties, options) {
|
|
394
|
+
return this.track('search', properties, options);
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Track add to cart event
|
|
398
|
+
*/
|
|
399
|
+
async trackAddToCart(properties, options) {
|
|
400
|
+
return this.track('add_to_cart', properties, options);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Track remove from cart event
|
|
404
|
+
*/
|
|
405
|
+
async trackRemoveFromCart(properties, options) {
|
|
406
|
+
return this.track('remove_from_cart', properties, options);
|
|
252
407
|
}
|
|
253
408
|
/**
|
|
254
409
|
* Manually flush all queued events
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grainql/analytics-web",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Lightweight TypeScript SDK for sending analytics events to Grain's REST API",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
"build:cjs": "tsc --module commonjs --target es2020 --outDir dist/cjs && mv dist/cjs/index.js dist/index.js",
|
|
23
23
|
"build:iife": "node scripts/build-iife.js",
|
|
24
24
|
"clean": "rm -rf dist",
|
|
25
|
-
"test": "
|
|
26
|
-
"test:watch": "
|
|
27
|
-
"test:coverage": "
|
|
28
|
-
"test:ci": "
|
|
25
|
+
"test": "jest",
|
|
26
|
+
"test:watch": "jest --watch",
|
|
27
|
+
"test:coverage": "jest --coverage",
|
|
28
|
+
"test:ci": "jest --ci --coverage",
|
|
29
29
|
"prepublishOnly": "npm run clean && npm run build",
|
|
30
30
|
"size": "npm run build && node scripts/bundle-analysis.js",
|
|
31
31
|
"size:limit": "size-limit"
|