cryptique-sdk 1.2.19 → 1.2.21
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/lib/cjs/index.js +341 -582
- package/lib/esm/index.js +341 -582
- package/lib/umd/index.js +341 -582
- package/package.json +1 -1
package/lib/cjs/index.js
CHANGED
|
@@ -92,8 +92,8 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
92
92
|
|
|
93
93
|
// Geolocation API
|
|
94
94
|
GEOLOCATION: {
|
|
95
|
-
PRIMARY_URL: "https://ipinfo.io/json?token=
|
|
96
|
-
BACKUP_URL: "https://ipinfo.io/json?token=
|
|
95
|
+
PRIMARY_URL: "https://ipinfo.io/json?token=3a0c034aefbef8",
|
|
96
|
+
BACKUP_URL: "https://ipinfo.io/json?token=3a0c034aefbef8&http=1.1",
|
|
97
97
|
TIMEOUT_MS: 5000 // 5 second timeout
|
|
98
98
|
},
|
|
99
99
|
|
|
@@ -1139,7 +1139,6 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
1139
1139
|
isFirstPage: true,
|
|
1140
1140
|
|
|
1141
1141
|
// JSONB fields
|
|
1142
|
-
interactions: {},
|
|
1143
1142
|
visited_pages: [],
|
|
1144
1143
|
|
|
1145
1144
|
// Also stored in locationData object for structured access (backward compatibility)
|
|
@@ -2084,7 +2083,6 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
2084
2083
|
if (sessionData.utm_id === undefined) sessionData.utm_id = null;
|
|
2085
2084
|
|
|
2086
2085
|
// JSONB fields
|
|
2087
|
-
if (!sessionData.interactions) sessionData.interactions = {};
|
|
2088
2086
|
if (!sessionData.visited_pages) sessionData.visited_pages = [];
|
|
2089
2087
|
|
|
2090
2088
|
// Internal tracking
|
|
@@ -2149,8 +2147,7 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
2149
2147
|
// UTM fields (already snake_case, no mapping needed)
|
|
2150
2148
|
|
|
2151
2149
|
// JSONB fields
|
|
2152
|
-
'visited_pages': 'visited_pages'
|
|
2153
|
-
'interactions': 'interactions' // Already snake_case
|
|
2150
|
+
'visited_pages': 'visited_pages' // Already snake_case
|
|
2154
2151
|
};
|
|
2155
2152
|
|
|
2156
2153
|
return fieldMap[internalName] || this.toSnakeCase(internalName);
|
|
@@ -3194,333 +3191,24 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
3194
3191
|
}
|
|
3195
3192
|
};
|
|
3196
3193
|
|
|
3197
|
-
//
|
|
3198
|
-
//
|
|
3199
|
-
//
|
|
3200
|
-
//
|
|
3201
|
-
// Handles all user interactions (clicks, forms, scroll, hover, etc.)
|
|
3202
|
-
//
|
|
3203
|
-
// KEY PRINCIPLES:
|
|
3204
|
-
// 1. All interactions stored in categorized structure
|
|
3205
|
-
// 2. Automatic timestamp addition
|
|
3206
|
-
// 3. Total interactions count maintained
|
|
3207
|
-
// 4. Chronological sorting for backend
|
|
3208
|
-
// 5. Immediate session storage updates
|
|
3209
|
-
// 6. Prevents duplicate interactions
|
|
3210
|
-
//
|
|
3211
|
-
// BENEFITS:
|
|
3212
|
-
// - Centralized interaction tracking
|
|
3213
|
-
// - Consistent data structure
|
|
3214
|
-
// - Single place to fix interaction bugs
|
|
3215
|
-
// - Prevents repeating interactions
|
|
3216
|
-
// ============================================================================
|
|
3194
|
+
// SECTION 10: INTERACTION MANAGEMENT — REMOVED
|
|
3195
|
+
// Interactions JSONB tracking has been removed from sessions.
|
|
3196
|
+
// Important signals (clicks, errors, etc.) are captured as individual
|
|
3197
|
+
// queryable auto events in the events table instead.
|
|
3217
3198
|
|
|
3218
|
-
|
|
3219
|
-
* InteractionManager - Interaction tracking and management
|
|
3220
|
-
*
|
|
3221
|
-
* This object handles all user interaction tracking, storage, and management
|
|
3222
|
-
*/
|
|
3199
|
+
// No-op stub — all methods removed with interactions feature.
|
|
3223
3200
|
const InteractionManager = {
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
'hoverEvents',
|
|
3236
|
-
'formSubmissions',
|
|
3237
|
-
'fieldChanges',
|
|
3238
|
-
'validationErrors',
|
|
3239
|
-
'keyboardEvents',
|
|
3240
|
-
'copyPasteEvents',
|
|
3241
|
-
'contextMenuEvents',
|
|
3242
|
-
'dragDropEvents',
|
|
3243
|
-
'touchEvents',
|
|
3244
|
-
'windowEvents',
|
|
3245
|
-
'performanceEvents',
|
|
3246
|
-
'errorEvents',
|
|
3247
|
-
'networkEvents',
|
|
3248
|
-
'formAnalytics'
|
|
3249
|
-
],
|
|
3250
|
-
|
|
3251
|
-
/**
|
|
3252
|
-
* Initialize interactions structure
|
|
3253
|
-
*
|
|
3254
|
-
* Creates empty interactions object with all categories
|
|
3255
|
-
*/
|
|
3256
|
-
initialize() {
|
|
3257
|
-
if (!sessionData.interactions || Object.keys(sessionData.interactions).length === 0) {
|
|
3258
|
-
sessionData.interactions = {
|
|
3259
|
-
totalInteractions: 0,
|
|
3260
|
-
clicks: [],
|
|
3261
|
-
formInteractions: [],
|
|
3262
|
-
mediaInteractions: [],
|
|
3263
|
-
scrollEvents: [],
|
|
3264
|
-
focusEvents: [],
|
|
3265
|
-
hoverEvents: [],
|
|
3266
|
-
formSubmissions: [],
|
|
3267
|
-
fieldChanges: [],
|
|
3268
|
-
validationErrors: [],
|
|
3269
|
-
keyboardEvents: [],
|
|
3270
|
-
copyPasteEvents: [],
|
|
3271
|
-
contextMenuEvents: [],
|
|
3272
|
-
dragDropEvents: [],
|
|
3273
|
-
touchEvents: [],
|
|
3274
|
-
windowEvents: [],
|
|
3275
|
-
performanceEvents: [],
|
|
3276
|
-
errorEvents: [],
|
|
3277
|
-
networkEvents: [],
|
|
3278
|
-
formAnalytics: []
|
|
3279
|
-
};
|
|
3280
|
-
}
|
|
3281
|
-
},
|
|
3282
|
-
|
|
3283
|
-
/**
|
|
3284
|
-
* Add interaction to appropriate category
|
|
3285
|
-
*
|
|
3286
|
-
* Logic:
|
|
3287
|
-
* 1. Ensures interactions are initialized
|
|
3288
|
-
* 2. Adds timestamp if not provided
|
|
3289
|
-
* 3. Adds to specific category array
|
|
3290
|
-
* 4. Updates total interactions count
|
|
3291
|
-
* 5. Updates session activity
|
|
3292
|
-
* 6. Immediately updates session storage
|
|
3293
|
-
*
|
|
3294
|
-
* Parameters:
|
|
3295
|
-
* - category: Interaction category (e.g., 'clicks', 'formInteractions')
|
|
3296
|
-
* - interactionData: Interaction data object
|
|
3297
|
-
*/
|
|
3298
|
-
add(category, interactionData) {
|
|
3299
|
-
// Ensure interactions are initialized
|
|
3300
|
-
this.initialize();
|
|
3301
|
-
|
|
3302
|
-
// Validate category
|
|
3303
|
-
if (!this.CATEGORIES.includes(category)) {
|
|
3304
|
-
return;
|
|
3305
|
-
}
|
|
3306
|
-
|
|
3307
|
-
// Add timestamp if not provided
|
|
3308
|
-
if (!interactionData.timestamp) {
|
|
3309
|
-
interactionData.timestamp = nowIso();
|
|
3310
|
-
}
|
|
3311
|
-
|
|
3312
|
-
// Add to specific category
|
|
3313
|
-
if (sessionData.interactions[category]) {
|
|
3314
|
-
sessionData.interactions[category].push(interactionData);
|
|
3315
|
-
}
|
|
3316
|
-
|
|
3317
|
-
// Update total interactions count
|
|
3318
|
-
this.updateTotalCount();
|
|
3319
|
-
|
|
3320
|
-
// Update last activity
|
|
3321
|
-
sessionData.lastActivity = Date.now();
|
|
3322
|
-
|
|
3323
|
-
// Immediately update session storage
|
|
3324
|
-
this.updateSessionStorage();
|
|
3325
|
-
},
|
|
3326
|
-
|
|
3327
|
-
/**
|
|
3328
|
-
* Update total interactions count
|
|
3329
|
-
*
|
|
3330
|
-
* Calculates total from all category arrays
|
|
3331
|
-
*/
|
|
3332
|
-
updateTotalCount() {
|
|
3333
|
-
if (!sessionData.interactions) return;
|
|
3334
|
-
|
|
3335
|
-
let total = 0;
|
|
3336
|
-
for (const category of this.CATEGORIES) {
|
|
3337
|
-
if (sessionData.interactions[category] && Array.isArray(sessionData.interactions[category])) {
|
|
3338
|
-
total += sessionData.interactions[category].length;
|
|
3339
|
-
}
|
|
3340
|
-
}
|
|
3341
|
-
|
|
3342
|
-
sessionData.interactions.totalInteractions = total;
|
|
3343
|
-
},
|
|
3344
|
-
|
|
3345
|
-
/**
|
|
3346
|
-
* Update session storage with interactions
|
|
3347
|
-
*
|
|
3348
|
-
* Immediately syncs interactions to session storage
|
|
3349
|
-
*/
|
|
3350
|
-
updateSessionStorage() {
|
|
3351
|
-
try {
|
|
3352
|
-
const storedSession = StorageManager.loadSession();
|
|
3353
|
-
if (storedSession && storedSession.sessionData) {
|
|
3354
|
-
storedSession.sessionData.interactions = sessionData.interactions;
|
|
3355
|
-
StorageManager.saveSession(storedSession);
|
|
3356
|
-
}
|
|
3357
|
-
} catch (error) {
|
|
3358
|
-
}
|
|
3359
|
-
},
|
|
3360
|
-
|
|
3361
|
-
/**
|
|
3362
|
-
* Get all interactions sorted chronologically
|
|
3363
|
-
*
|
|
3364
|
-
* Collects all interactions from all categories, adds category identifiers,
|
|
3365
|
-
* and sorts by timestamp
|
|
3366
|
-
*
|
|
3367
|
-
* Returns: Array of interactions sorted by timestamp
|
|
3368
|
-
*/
|
|
3369
|
-
getChronological() {
|
|
3370
|
-
if (!sessionData.interactions) {
|
|
3371
|
-
return [];
|
|
3372
|
-
}
|
|
3373
|
-
|
|
3374
|
-
const allInteractions = [];
|
|
3375
|
-
|
|
3376
|
-
// Map of category to category identifier
|
|
3377
|
-
const categoryMap = {
|
|
3378
|
-
'clicks': { category: 'click', originalCategory: 'clicks' },
|
|
3379
|
-
'formInteractions': { category: 'formInteraction', originalCategory: 'formInteractions' },
|
|
3380
|
-
'mediaInteractions': { category: 'mediaInteraction', originalCategory: 'mediaInteractions' },
|
|
3381
|
-
'scrollEvents': { category: 'scrollEvent', originalCategory: 'scrollEvents' },
|
|
3382
|
-
'focusEvents': { category: 'focusEvent', originalCategory: 'focusEvents' },
|
|
3383
|
-
'hoverEvents': { category: 'hoverEvent', originalCategory: 'hoverEvents' },
|
|
3384
|
-
'formSubmissions': { category: 'formSubmission', originalCategory: 'formSubmissions' },
|
|
3385
|
-
'fieldChanges': { category: 'fieldChange', originalCategory: 'fieldChanges' },
|
|
3386
|
-
'validationErrors': { category: 'validationError', originalCategory: 'validationErrors' },
|
|
3387
|
-
'keyboardEvents': { category: 'keyboardEvent', originalCategory: 'keyboardEvents' },
|
|
3388
|
-
'copyPasteEvents': { category: 'copyPasteEvent', originalCategory: 'copyPasteEvents' },
|
|
3389
|
-
'contextMenuEvents': { category: 'contextMenuEvent', originalCategory: 'contextMenuEvents' },
|
|
3390
|
-
'dragDropEvents': { category: 'dragDropEvent', originalCategory: 'dragDropEvents' },
|
|
3391
|
-
'touchEvents': { category: 'touchEvent', originalCategory: 'touchEvents' },
|
|
3392
|
-
'windowEvents': { category: 'windowEvent', originalCategory: 'windowEvents' },
|
|
3393
|
-
'performanceEvents': { category: 'performanceEvent', originalCategory: 'performanceEvents' },
|
|
3394
|
-
'errorEvents': { category: 'errorEvent', originalCategory: 'errorEvents' },
|
|
3395
|
-
'networkEvents': { category: 'networkEvent', originalCategory: 'networkEvents' },
|
|
3396
|
-
'formAnalytics': { category: 'formAnalytic', originalCategory: 'formAnalytics' }
|
|
3397
|
-
};
|
|
3398
|
-
|
|
3399
|
-
// Collect all interactions from all categories
|
|
3400
|
-
for (const category of this.CATEGORIES) {
|
|
3401
|
-
if (sessionData.interactions[category] && Array.isArray(sessionData.interactions[category])) {
|
|
3402
|
-
const categoryInfo = categoryMap[category];
|
|
3403
|
-
sessionData.interactions[category].forEach(interaction => {
|
|
3404
|
-
allInteractions.push({
|
|
3405
|
-
...interaction,
|
|
3406
|
-
category: categoryInfo.category,
|
|
3407
|
-
originalCategory: categoryInfo.originalCategory
|
|
3408
|
-
});
|
|
3409
|
-
});
|
|
3410
|
-
}
|
|
3411
|
-
}
|
|
3412
|
-
|
|
3413
|
-
// Sort by timestamp (chronological order)
|
|
3414
|
-
allInteractions.sort((a, b) => {
|
|
3415
|
-
const timestampA = new Date(a.timestamp || 0);
|
|
3416
|
-
const timestampB = new Date(b.timestamp || 0);
|
|
3417
|
-
return timestampA - timestampB;
|
|
3418
|
-
});
|
|
3419
|
-
|
|
3420
|
-
return allInteractions;
|
|
3421
|
-
},
|
|
3422
|
-
|
|
3423
|
-
/**
|
|
3424
|
-
* Get chronological interactions for backend
|
|
3425
|
-
*
|
|
3426
|
-
* Returns both chronological array and categorized structure
|
|
3427
|
-
*
|
|
3428
|
-
* Returns: Object with chronological and categorized interactions
|
|
3429
|
-
*/
|
|
3430
|
-
getChronologicalForBackend() {
|
|
3431
|
-
const chronological = this.getChronological();
|
|
3432
|
-
|
|
3433
|
-
return {
|
|
3434
|
-
chronological: chronological,
|
|
3435
|
-
categorized: sessionData.interactions
|
|
3436
|
-
};
|
|
3437
|
-
},
|
|
3438
|
-
|
|
3439
|
-
/**
|
|
3440
|
-
* Deduplicate interactions
|
|
3441
|
-
*
|
|
3442
|
-
* Removes duplicate interactions based on timestamp, category, and key fields
|
|
3443
|
-
*
|
|
3444
|
-
* Logic:
|
|
3445
|
-
* - Compares interactions by timestamp, category, and element identifier
|
|
3446
|
-
* - Keeps first occurrence of duplicates
|
|
3447
|
-
* - Updates total count after deduplication
|
|
3448
|
-
*/
|
|
3449
|
-
deduplicate() {
|
|
3450
|
-
if (!sessionData.interactions) return;
|
|
3451
|
-
|
|
3452
|
-
const seen = new Set();
|
|
3453
|
-
|
|
3454
|
-
for (const category of this.CATEGORIES) {
|
|
3455
|
-
if (sessionData.interactions[category] && Array.isArray(sessionData.interactions[category])) {
|
|
3456
|
-
sessionData.interactions[category] = sessionData.interactions[category].filter(interaction => {
|
|
3457
|
-
// Create unique key from timestamp, category, and element identifier
|
|
3458
|
-
const elementId = interaction.elementId || interaction.target || interaction.selector || '';
|
|
3459
|
-
const key = `${interaction.timestamp}_${category}_${elementId}`;
|
|
3460
|
-
|
|
3461
|
-
if (seen.has(key)) {
|
|
3462
|
-
return false; // Duplicate, remove
|
|
3463
|
-
}
|
|
3464
|
-
|
|
3465
|
-
seen.add(key);
|
|
3466
|
-
return true; // Keep
|
|
3467
|
-
});
|
|
3468
|
-
}
|
|
3469
|
-
}
|
|
3470
|
-
|
|
3471
|
-
// Update total count after deduplication
|
|
3472
|
-
this.updateTotalCount();
|
|
3473
|
-
|
|
3474
|
-
// Update session storage
|
|
3475
|
-
this.updateSessionStorage();
|
|
3476
|
-
},
|
|
3477
|
-
|
|
3478
|
-
/**
|
|
3479
|
-
* Get interactions by category
|
|
3480
|
-
*
|
|
3481
|
-
* Returns all interactions for a specific category
|
|
3482
|
-
*
|
|
3483
|
-
* Parameters: category - Interaction category name
|
|
3484
|
-
* Returns: Array of interactions for that category
|
|
3485
|
-
*/
|
|
3486
|
-
getByCategory(category) {
|
|
3487
|
-
if (!sessionData.interactions) {
|
|
3488
|
-
return [];
|
|
3489
|
-
}
|
|
3490
|
-
|
|
3491
|
-
if (!this.CATEGORIES.includes(category)) {
|
|
3492
|
-
return [];
|
|
3493
|
-
}
|
|
3494
|
-
|
|
3495
|
-
return sessionData.interactions[category] || [];
|
|
3496
|
-
},
|
|
3497
|
-
|
|
3498
|
-
/**
|
|
3499
|
-
* Get total interactions count
|
|
3500
|
-
*
|
|
3501
|
-
* Returns total number of interactions across all categories
|
|
3502
|
-
*/
|
|
3503
|
-
getTotalCount() {
|
|
3504
|
-
if (!sessionData.interactions) {
|
|
3505
|
-
return 0;
|
|
3506
|
-
}
|
|
3507
|
-
|
|
3508
|
-
return sessionData.interactions.totalInteractions || 0;
|
|
3509
|
-
},
|
|
3510
|
-
|
|
3511
|
-
/**
|
|
3512
|
-
* Clear all interactions
|
|
3513
|
-
*
|
|
3514
|
-
* Resets interactions object to empty state
|
|
3515
|
-
*/
|
|
3516
|
-
clear() {
|
|
3517
|
-
this.initialize();
|
|
3518
|
-
for (const category of this.CATEGORIES) {
|
|
3519
|
-
sessionData.interactions[category] = [];
|
|
3520
|
-
}
|
|
3521
|
-
sessionData.interactions.totalInteractions = 0;
|
|
3522
|
-
this.updateSessionStorage();
|
|
3523
|
-
}
|
|
3201
|
+
CATEGORIES: [],
|
|
3202
|
+
initialize() {},
|
|
3203
|
+
add() {},
|
|
3204
|
+
updateTotalCount() {},
|
|
3205
|
+
updateSessionStorage() {},
|
|
3206
|
+
getChronological() { return []; },
|
|
3207
|
+
getChronologicalForBackend() { return { chronological: [], categorized: {} }; },
|
|
3208
|
+
deduplicate() {},
|
|
3209
|
+
getByCategory() { return []; },
|
|
3210
|
+
getTotalCount() { return 0; },
|
|
3211
|
+
clear() {}
|
|
3524
3212
|
};
|
|
3525
3213
|
|
|
3526
3214
|
// ============================================================================
|
|
@@ -3914,6 +3602,8 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
3914
3602
|
|
|
3915
3603
|
history.pushState = function(...args) {
|
|
3916
3604
|
originalPushState.apply(history, args);
|
|
3605
|
+
// Notify page_summary exit-type tracker that an SPA navigation occurred
|
|
3606
|
+
try { window.dispatchEvent(new CustomEvent('cq:spa-navigation')); } catch (_) {}
|
|
3917
3607
|
// Use requestAnimationFrame for better timing with React Router
|
|
3918
3608
|
requestAnimationFrame(() => {
|
|
3919
3609
|
self.track();
|
|
@@ -3922,6 +3612,8 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
3922
3612
|
|
|
3923
3613
|
history.replaceState = function(...args) {
|
|
3924
3614
|
originalReplaceState.apply(history, args);
|
|
3615
|
+
// Notify page_summary exit-type tracker (replaceState = SPA redirect)
|
|
3616
|
+
try { window.dispatchEvent(new CustomEvent('cq:spa-navigation')); } catch (_) {}
|
|
3925
3617
|
// Use requestAnimationFrame for better timing with React Router
|
|
3926
3618
|
requestAnimationFrame(() => {
|
|
3927
3619
|
self.track();
|
|
@@ -3984,7 +3676,6 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
3984
3676
|
// Ensure arrays exist
|
|
3985
3677
|
s.pageVisits = s.pageVisits || [];
|
|
3986
3678
|
s.visited_pages = s.visited_pages || [];
|
|
3987
|
-
s.interactions = s.interactions || { totalInteractions: 0 };
|
|
3988
3679
|
|
|
3989
3680
|
// Normalize timestamps to ISO strings
|
|
3990
3681
|
if (s.startTime && !(typeof s.startTime === 'string')) {
|
|
@@ -4039,171 +3730,8 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
4039
3730
|
// First normalize the shape
|
|
4040
3731
|
sourceData = this.normalizeShape(sourceData);
|
|
4041
3732
|
|
|
4042
|
-
// Get
|
|
4043
|
-
const chronologicalInteractions = InteractionManager.getChronologicalForBackend();
|
|
4044
|
-
|
|
4045
|
-
// Get page visits and session-level interactions
|
|
3733
|
+
// Get page visits
|
|
4046
3734
|
const pageVisits = sourceData.visited_pages || sourceData.pageVisits || sourceData.visitedPages || [];
|
|
4047
|
-
const sessionInteractions = sourceData.interactions || {};
|
|
4048
|
-
|
|
4049
|
-
// CRITICAL FIX: Distribute session-level interactions to appropriate pages based on path
|
|
4050
|
-
const interactionTypes = [
|
|
4051
|
-
'clicks', 'mediaInteractions', 'contextMenuEvents', 'windowEvents',
|
|
4052
|
-
'formInteractions', 'scrollEvents', 'focusEvents', 'hoverEvents',
|
|
4053
|
-
'formSubmissions', 'fieldChanges',
|
|
4054
|
-
'keyboardEvents', 'copyPasteEvents', 'dragDropEvents',
|
|
4055
|
-
'touchEvents', 'formAnalytics'
|
|
4056
|
-
// Note: validationErrors, performanceEvents, errorEvents, networkEvents are no longer
|
|
4057
|
-
// stored in session interactions — they are emitted as individual queryable auto events
|
|
4058
|
-
// (form_validation_error, page_performance, js_error, network_error)
|
|
4059
|
-
];
|
|
4060
|
-
|
|
4061
|
-
// Helper to normalize path for matching
|
|
4062
|
-
const normalizePathForMatching = (path) => {
|
|
4063
|
-
if (!path) return '/';
|
|
4064
|
-
// Remove query params and hash
|
|
4065
|
-
const cleanPath = path.split('?')[0].split('#')[0];
|
|
4066
|
-
return cleanPath || '/';
|
|
4067
|
-
};
|
|
4068
|
-
|
|
4069
|
-
// Get session start time in milliseconds for alignment
|
|
4070
|
-
const sessionStartTime = sourceData.start_time || sourceData.startTime;
|
|
4071
|
-
const sessionStartMs = sessionStartTime
|
|
4072
|
-
? (typeof sessionStartTime === 'string' ? new Date(sessionStartTime).getTime() : sessionStartTime)
|
|
4073
|
-
: Date.now();
|
|
4074
|
-
|
|
4075
|
-
// Distribute interactions to pages and fix mount/unmount times
|
|
4076
|
-
// CRITICAL: Create copies to avoid mutating original objects
|
|
4077
|
-
const pagesWithInteractions = pageVisits.map((pageVisit, index) => {
|
|
4078
|
-
// Create a copy of the page visit to avoid mutating the original
|
|
4079
|
-
const pageVisitCopy = { ...pageVisit };
|
|
4080
|
-
const pagePath = normalizePathForMatching(pageVisitCopy.path || pageVisitCopy.url);
|
|
4081
|
-
const isFirstPage = index === 0 || pageVisitCopy.isEntry === true;
|
|
4082
|
-
|
|
4083
|
-
// CRITICAL FIX: If unmountTime is null, set it to next page's mountTime
|
|
4084
|
-
if (!pageVisitCopy.unmountTime && index < pageVisits.length - 1) {
|
|
4085
|
-
const nextPage = pageVisits[index + 1];
|
|
4086
|
-
if (nextPage && nextPage.mountTime) {
|
|
4087
|
-
pageVisitCopy.unmountTime = typeof nextPage.mountTime === 'number'
|
|
4088
|
-
? nextPage.mountTime
|
|
4089
|
-
: new Date(nextPage.mountTime).getTime();
|
|
4090
|
-
// Recalculate duration
|
|
4091
|
-
if (pageVisitCopy.mountTime) {
|
|
4092
|
-
const mountMs = typeof pageVisitCopy.mountTime === 'number'
|
|
4093
|
-
? pageVisitCopy.mountTime
|
|
4094
|
-
: new Date(pageVisitCopy.mountTime).getTime();
|
|
4095
|
-
pageVisitCopy.duration = Math.floor((pageVisitCopy.unmountTime - mountMs) / 1000);
|
|
4096
|
-
}
|
|
4097
|
-
}
|
|
4098
|
-
}
|
|
4099
|
-
|
|
4100
|
-
// If still no unmountTime and this is the last page, set to session endTime
|
|
4101
|
-
if (!pageVisitCopy.unmountTime && index === pageVisits.length - 1) {
|
|
4102
|
-
const endTime = sourceData.end_time || sourceData.endTime;
|
|
4103
|
-
if (endTime) {
|
|
4104
|
-
pageVisitCopy.unmountTime = typeof endTime === 'number'
|
|
4105
|
-
? endTime
|
|
4106
|
-
: new Date(endTime).getTime();
|
|
4107
|
-
// Recalculate duration
|
|
4108
|
-
if (pageVisitCopy.mountTime) {
|
|
4109
|
-
const mountMs = typeof pageVisitCopy.mountTime === 'number'
|
|
4110
|
-
? pageVisitCopy.mountTime
|
|
4111
|
-
: new Date(pageVisitCopy.mountTime).getTime();
|
|
4112
|
-
pageVisitCopy.duration = Math.floor((pageVisitCopy.unmountTime - mountMs) / 1000);
|
|
4113
|
-
}
|
|
4114
|
-
}
|
|
4115
|
-
}
|
|
4116
|
-
|
|
4117
|
-
// FIX: Align first page mountTime with session start_time (only in copy, not original)
|
|
4118
|
-
if (isFirstPage && pageVisitCopy.mountTime) {
|
|
4119
|
-
const mountTimeMs = typeof pageVisitCopy.mountTime === 'number'
|
|
4120
|
-
? pageVisitCopy.mountTime
|
|
4121
|
-
: new Date(pageVisitCopy.mountTime).getTime();
|
|
4122
|
-
|
|
4123
|
-
// If mountTime is more than 5 minutes off from session start, align it
|
|
4124
|
-
const timeDiff = Math.abs(mountTimeMs - sessionStartMs);
|
|
4125
|
-
if (timeDiff > 5 * 60 * 1000) { // More than 5 minutes difference
|
|
4126
|
-
pageVisitCopy.mountTime = sessionStartMs;
|
|
4127
|
-
// Recalculate duration if unmountTime exists
|
|
4128
|
-
if (pageVisitCopy.unmountTime) {
|
|
4129
|
-
const unmountTimeMs = typeof pageVisitCopy.unmountTime === 'number'
|
|
4130
|
-
? pageVisitCopy.unmountTime
|
|
4131
|
-
: new Date(pageVisitCopy.unmountTime).getTime();
|
|
4132
|
-
pageVisitCopy.duration = Math.floor((unmountTimeMs - sessionStartMs) / 1000);
|
|
4133
|
-
}
|
|
4134
|
-
}
|
|
4135
|
-
}
|
|
4136
|
-
|
|
4137
|
-
// Ensure mountTime and unmountTime are numbers (milliseconds) for consistency (in copy only)
|
|
4138
|
-
if (pageVisitCopy.mountTime && typeof pageVisitCopy.mountTime !== 'number') {
|
|
4139
|
-
pageVisitCopy.mountTime = new Date(pageVisitCopy.mountTime).getTime();
|
|
4140
|
-
}
|
|
4141
|
-
if (pageVisitCopy.unmountTime && typeof pageVisitCopy.unmountTime !== 'number') {
|
|
4142
|
-
pageVisitCopy.unmountTime = new Date(pageVisitCopy.unmountTime).getTime();
|
|
4143
|
-
}
|
|
4144
|
-
|
|
4145
|
-
// Initialize interactions object for this page if it doesn't exist (in copy)
|
|
4146
|
-
if (!pageVisitCopy.interactions) {
|
|
4147
|
-
pageVisitCopy.interactions = {
|
|
4148
|
-
totalInteractions: 0,
|
|
4149
|
-
clicks: [],
|
|
4150
|
-
mediaInteractions: [],
|
|
4151
|
-
contextMenuEvents: [],
|
|
4152
|
-
windowEvents: [],
|
|
4153
|
-
formInteractions: [],
|
|
4154
|
-
scrollEvents: [],
|
|
4155
|
-
focusEvents: [],
|
|
4156
|
-
hoverEvents: [],
|
|
4157
|
-
formSubmissions: [],
|
|
4158
|
-
fieldChanges: [],
|
|
4159
|
-
validationErrors: [],
|
|
4160
|
-
keyboardEvents: [],
|
|
4161
|
-
copyPasteEvents: [],
|
|
4162
|
-
dragDropEvents: [],
|
|
4163
|
-
touchEvents: [],
|
|
4164
|
-
performanceEvents: [],
|
|
4165
|
-
errorEvents: [],
|
|
4166
|
-
networkEvents: [],
|
|
4167
|
-
formAnalytics: []
|
|
4168
|
-
};
|
|
4169
|
-
}
|
|
4170
|
-
|
|
4171
|
-
// Distribute interactions from session-level to this page based on path matching (in copy)
|
|
4172
|
-
interactionTypes.forEach(type => {
|
|
4173
|
-
if (Array.isArray(sessionInteractions[type])) {
|
|
4174
|
-
sessionInteractions[type].forEach(interaction => {
|
|
4175
|
-
const interactionPath = normalizePathForMatching(interaction.path);
|
|
4176
|
-
if (interactionPath === pagePath) {
|
|
4177
|
-
// Add interaction to this page (avoid duplicates)
|
|
4178
|
-
const exists = pageVisitCopy.interactions[type].some(existing => {
|
|
4179
|
-
// Check for duplicates based on timestamp and key properties
|
|
4180
|
-
if (existing.timestamp && interaction.timestamp) {
|
|
4181
|
-
return existing.timestamp === interaction.timestamp;
|
|
4182
|
-
}
|
|
4183
|
-
// For clicks, check element properties
|
|
4184
|
-
if (type === 'clicks' && existing.element && interaction.element) {
|
|
4185
|
-
return existing.element.id === interaction.element.id &&
|
|
4186
|
-
existing.element.tagName === interaction.element.tagName;
|
|
4187
|
-
}
|
|
4188
|
-
return false;
|
|
4189
|
-
});
|
|
4190
|
-
|
|
4191
|
-
if (!exists) {
|
|
4192
|
-
pageVisitCopy.interactions[type].push(interaction);
|
|
4193
|
-
}
|
|
4194
|
-
}
|
|
4195
|
-
});
|
|
4196
|
-
}
|
|
4197
|
-
});
|
|
4198
|
-
|
|
4199
|
-
// Calculate totalInteractions for this page (in copy)
|
|
4200
|
-
const total = interactionTypes.reduce((sum, type) => {
|
|
4201
|
-
return sum + (pageVisitCopy.interactions[type]?.length || 0);
|
|
4202
|
-
}, 0);
|
|
4203
|
-
pageVisitCopy.interactions.totalInteractions = total;
|
|
4204
|
-
|
|
4205
|
-
return pageVisitCopy;
|
|
4206
|
-
});
|
|
4207
3735
|
|
|
4208
3736
|
// Create transformed object - PostgreSQL snake_case format only
|
|
4209
3737
|
const transformed = {
|
|
@@ -4268,12 +3796,8 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
4268
3796
|
utm_content: sourceData.utm_content || sourceData.utmData?.content || null,
|
|
4269
3797
|
utm_id: sourceData.utm_id || sourceData.utmData?.utm_id || null,
|
|
4270
3798
|
|
|
4271
|
-
// JSONB fields
|
|
4272
|
-
visited_pages:
|
|
4273
|
-
interactions: {
|
|
4274
|
-
...sessionInteractions,
|
|
4275
|
-
chronological: chronologicalInteractions.chronological || []
|
|
4276
|
-
},
|
|
3799
|
+
// JSONB fields
|
|
3800
|
+
visited_pages: pageVisits,
|
|
4277
3801
|
|
|
4278
3802
|
// Active tab time in ms (tab foreground time only, for active_time_seconds in DB)
|
|
4279
3803
|
active_time_ms: sourceData.active_time_ms || null
|
|
@@ -4941,12 +4465,6 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
4941
4465
|
if (hoveredElements.has(elementId)) {
|
|
4942
4466
|
hoveredElements.delete(elementId);
|
|
4943
4467
|
|
|
4944
|
-
// Update hover duration in the interaction
|
|
4945
|
-
const interactions = InteractionManager.getByCategory('hoverEvents');
|
|
4946
|
-
const hoverEvent = interactions.find(h => h.elementId === elementId);
|
|
4947
|
-
if (hoverEvent) {
|
|
4948
|
-
hoverEvent.hoverDuration = Date.now() - new Date(hoverEvent.timestamp).getTime();
|
|
4949
|
-
}
|
|
4950
4468
|
}
|
|
4951
4469
|
}
|
|
4952
4470
|
});
|
|
@@ -5288,12 +4806,33 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5288
4806
|
: null;
|
|
5289
4807
|
} catch (e) {}
|
|
5290
4808
|
|
|
4809
|
+
// Collect top-3 slowest resources to pinpoint what is causing slow LCP/FCP.
|
|
4810
|
+
// Each entry: { name (URL), initiator_type, duration_ms, transfer_size_kb }
|
|
4811
|
+
// Only includes same-origin resources or entries where the timing is exposed
|
|
4812
|
+
// (cross-origin resources without Timing-Allow-Origin return duration = 0
|
|
4813
|
+
// and are excluded as they carry no actionable signal).
|
|
4814
|
+
let slowestResources = [];
|
|
4815
|
+
try {
|
|
4816
|
+
const resources = performance.getEntriesByType('resource');
|
|
4817
|
+
slowestResources = resources
|
|
4818
|
+
.filter(r => r.duration > 0) // exclude zero-duration cross-origin entries
|
|
4819
|
+
.sort((a, b) => b.duration - a.duration)
|
|
4820
|
+
.slice(0, 3)
|
|
4821
|
+
.map(r => ({
|
|
4822
|
+
name: r.name.split('?')[0].slice(-120), // trim query string + cap length
|
|
4823
|
+
initiator_type: r.initiatorType || 'other', // script, css, img, fetch, xmlhttprequest…
|
|
4824
|
+
duration_ms: Math.round(r.duration),
|
|
4825
|
+
transfer_size_kb: r.transferSize ? Math.round(r.transferSize / 1024) : null
|
|
4826
|
+
}));
|
|
4827
|
+
} catch (_) {}
|
|
4828
|
+
|
|
5291
4829
|
EventsManager.trackAutoEvent('page_performance', {
|
|
5292
|
-
lcp:
|
|
5293
|
-
cls:
|
|
5294
|
-
inp:
|
|
5295
|
-
fcp:
|
|
5296
|
-
ttfb:
|
|
4830
|
+
lcp: vitals.lcp,
|
|
4831
|
+
cls: vitals.cls,
|
|
4832
|
+
inp: vitals.inp,
|
|
4833
|
+
fcp: vitals.fcp,
|
|
4834
|
+
ttfb: vitals.ttfb,
|
|
4835
|
+
slowest_resources: slowestResources // [] when none available
|
|
5297
4836
|
});
|
|
5298
4837
|
}, 1500);
|
|
5299
4838
|
});
|
|
@@ -5303,54 +4842,108 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5303
4842
|
|
|
5304
4843
|
/**
|
|
5305
4844
|
* ErrorTracker - Tracks JavaScript errors
|
|
5306
|
-
* Emits as queryable auto events (js_error) instead of session-level JSONB
|
|
4845
|
+
* Emits as queryable auto events (js_error) instead of session-level JSONB.
|
|
4846
|
+
*
|
|
4847
|
+
* Improvements over original:
|
|
4848
|
+
* - error_class: extracts the constructor name (TypeError, ReferenceError, etc.)
|
|
4849
|
+
* so the AI can categorise errors without parsing message strings.
|
|
4850
|
+
* - Session-level deduplication: same error (same message + source + line)
|
|
4851
|
+
* fires at most once per 10 s to prevent event floods from looping errors.
|
|
4852
|
+
* - last_event_name: name of the most-recently tracked auto event, giving a
|
|
4853
|
+
* lightweight "what was the user doing just before the crash" signal.
|
|
5307
4854
|
*/
|
|
5308
4855
|
startErrorTracking() {
|
|
5309
4856
|
try {
|
|
4857
|
+
// Deduplication: hash → last-fired timestamp (ms)
|
|
4858
|
+
const recentErrors = new Map();
|
|
4859
|
+
const ERROR_DEDUPE_MS = 10000; // suppress repeats within 10 s
|
|
4860
|
+
|
|
4861
|
+
// Lightweight "preceding action" tracker — updated by trackAutoEvent callers
|
|
4862
|
+
// via a module-level variable so it's always current.
|
|
4863
|
+
if (!EventsManager._lastAutoEventName) {
|
|
4864
|
+
EventsManager._lastAutoEventName = '';
|
|
4865
|
+
}
|
|
4866
|
+
|
|
4867
|
+
const dedupeKey = (msg, src, line) =>
|
|
4868
|
+
`${msg}|${src}|${line}`.slice(0, 200);
|
|
4869
|
+
|
|
4870
|
+
const shouldSuppress = (key) => {
|
|
4871
|
+
const last = recentErrors.get(key);
|
|
4872
|
+
if (!last) return false;
|
|
4873
|
+
return (Date.now() - last) < ERROR_DEDUPE_MS;
|
|
4874
|
+
};
|
|
4875
|
+
|
|
4876
|
+
const markSeen = (key) => recentErrors.set(key, Date.now());
|
|
4877
|
+
|
|
4878
|
+
// Extract class name from the error object's constructor, e.g. "TypeError"
|
|
4879
|
+
const getErrorClass = (errObj) => {
|
|
4880
|
+
if (!errObj) return 'Error';
|
|
4881
|
+
try { return errObj.constructor?.name || 'Error'; } catch (_) { return 'Error'; }
|
|
4882
|
+
};
|
|
4883
|
+
|
|
5310
4884
|
window.addEventListener('error', (event) => {
|
|
4885
|
+
const msg = event.message || '';
|
|
4886
|
+
const src = event.filename || '';
|
|
4887
|
+
const line = event.lineno || 0;
|
|
4888
|
+
const key = dedupeKey(msg, src, line);
|
|
4889
|
+
if (shouldSuppress(key)) return;
|
|
4890
|
+
markSeen(key);
|
|
4891
|
+
|
|
5311
4892
|
EventsManager.trackAutoEvent('js_error', {
|
|
5312
|
-
error_type:
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
4893
|
+
error_type: 'javascript_error',
|
|
4894
|
+
error_class: getErrorClass(event.error),
|
|
4895
|
+
message: msg,
|
|
4896
|
+
source: src,
|
|
4897
|
+
line: line,
|
|
4898
|
+
column: event.colno || 0,
|
|
4899
|
+
stack: event.error?.stack || '',
|
|
4900
|
+
last_event_name: EventsManager._lastAutoEventName || ''
|
|
5318
4901
|
});
|
|
5319
4902
|
});
|
|
5320
4903
|
|
|
5321
|
-
//
|
|
4904
|
+
// Unhandled promise rejections (fetch failures that aren't caught, async throws, etc.)
|
|
5322
4905
|
window.addEventListener('unhandledrejection', (event) => {
|
|
4906
|
+
const msg = event.reason?.message || String(event.reason || 'Unknown error');
|
|
4907
|
+
const key = dedupeKey(msg, '', 0);
|
|
4908
|
+
if (shouldSuppress(key)) return;
|
|
4909
|
+
markSeen(key);
|
|
4910
|
+
|
|
5323
4911
|
EventsManager.trackAutoEvent('js_error', {
|
|
5324
|
-
error_type:
|
|
5325
|
-
|
|
5326
|
-
|
|
4912
|
+
error_type: 'unhandled_promise_rejection',
|
|
4913
|
+
error_class: getErrorClass(event.reason),
|
|
4914
|
+
message: msg,
|
|
4915
|
+
stack: event.reason?.stack || String(event.reason || ''),
|
|
4916
|
+
last_event_name: EventsManager._lastAutoEventName || ''
|
|
5327
4917
|
});
|
|
5328
4918
|
});
|
|
5329
4919
|
} catch (error) {
|
|
5330
|
-
|
|
4920
|
+
}
|
|
5331
4921
|
},
|
|
5332
4922
|
|
|
5333
4923
|
/**
|
|
5334
|
-
* NetworkTracker - Tracks network errors (XHR
|
|
4924
|
+
* NetworkTracker - Tracks network errors (XHR + fetch)
|
|
5335
4925
|
*
|
|
5336
|
-
*
|
|
5337
|
-
*
|
|
5338
|
-
*
|
|
5339
|
-
*
|
|
5340
|
-
*
|
|
4926
|
+
* XHR: only wraps SDK calls to sdkapi.cryptique.io (unchanged behaviour).
|
|
4927
|
+
*
|
|
4928
|
+
* fetch: wraps window.fetch to track HTTP ≥ 400 responses and network
|
|
4929
|
+
* failures for same-origin requests made by the host application.
|
|
4930
|
+
* Third-party (cross-origin) fetches are passed through untouched to
|
|
4931
|
+
* minimise DevTools attribution noise. The SDK's own fetch calls via
|
|
4932
|
+
* APIClient.send() are already error-handled and are excluded by the
|
|
4933
|
+
* same-origin guard (they go to sdkapi.cryptique.io, not the app origin).
|
|
5341
4934
|
*/
|
|
5342
4935
|
startNetworkTracking() {
|
|
5343
4936
|
try {
|
|
5344
|
-
//
|
|
4937
|
+
// ── XHR: unchanged — only track SDK backend calls ──────────────────
|
|
5345
4938
|
const originalXHROpen = XMLHttpRequest.prototype.open;
|
|
5346
4939
|
const originalXHRSend = XMLHttpRequest.prototype.send;
|
|
5347
|
-
|
|
4940
|
+
|
|
5348
4941
|
XMLHttpRequest.prototype.open = function(method, url, ...args) {
|
|
5349
4942
|
this._cryptiqueMethod = method;
|
|
5350
4943
|
this._cryptiqueUrl = url;
|
|
5351
4944
|
return originalXHROpen.apply(this, [method, url, ...args]);
|
|
5352
4945
|
};
|
|
5353
|
-
|
|
4946
|
+
|
|
5354
4947
|
XMLHttpRequest.prototype.send = function(...args) {
|
|
5355
4948
|
const startTime = Date.now();
|
|
5356
4949
|
const xhr = this;
|
|
@@ -5359,7 +4952,7 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5359
4952
|
if (!xhr._cryptiqueUrl || !xhr._cryptiqueUrl.includes('sdkapi.cryptique.io')) {
|
|
5360
4953
|
return originalXHRSend.apply(this, args);
|
|
5361
4954
|
}
|
|
5362
|
-
|
|
4955
|
+
|
|
5363
4956
|
xhr.addEventListener('load', function() {
|
|
5364
4957
|
if (xhr.status >= 400) {
|
|
5365
4958
|
EventsManager.trackAutoEvent('network_error', {
|
|
@@ -5381,11 +4974,77 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5381
4974
|
duration_ms: Date.now() - startTime
|
|
5382
4975
|
});
|
|
5383
4976
|
});
|
|
5384
|
-
|
|
4977
|
+
|
|
5385
4978
|
return originalXHRSend.apply(this, args);
|
|
5386
4979
|
};
|
|
5387
|
-
|
|
4980
|
+
|
|
4981
|
+
// ── fetch: track same-origin HTTP errors from the host application ──
|
|
4982
|
+
// We intercept only same-origin (or explicitly relative) URLs so that
|
|
4983
|
+
// third-party CDN / analytics / auth calls are never touched.
|
|
4984
|
+
const originalFetch = window.fetch;
|
|
4985
|
+
if (typeof originalFetch === 'function') {
|
|
4986
|
+
window.fetch = function(input, init) {
|
|
4987
|
+
const startTime = Date.now();
|
|
4988
|
+
|
|
4989
|
+
// Resolve the URL string regardless of whether input is a string,
|
|
4990
|
+
// URL object, or Request object.
|
|
4991
|
+
let urlStr = '';
|
|
4992
|
+
try {
|
|
4993
|
+
urlStr = (input instanceof Request ? input.url : String(input)) || '';
|
|
4994
|
+
} catch (_) {}
|
|
4995
|
+
|
|
4996
|
+
// Determine if same-origin (relative URLs are always same-origin)
|
|
4997
|
+
let isSameOrigin = false;
|
|
4998
|
+
try {
|
|
4999
|
+
if (urlStr.startsWith('/') || urlStr.startsWith('./') || urlStr.startsWith('../')) {
|
|
5000
|
+
isSameOrigin = true;
|
|
5001
|
+
} else {
|
|
5002
|
+
const parsed = new URL(urlStr, window.location.href);
|
|
5003
|
+
isSameOrigin = parsed.origin === window.location.origin;
|
|
5004
|
+
}
|
|
5005
|
+
} catch (_) {}
|
|
5006
|
+
|
|
5007
|
+
// Skip SDK's own backend calls — already handled by APIClient.send()
|
|
5008
|
+
if (urlStr.includes('sdkapi.cryptique.io')) {
|
|
5009
|
+
return originalFetch.apply(this, arguments);
|
|
5010
|
+
}
|
|
5011
|
+
|
|
5012
|
+
// Pass cross-origin calls through untouched
|
|
5013
|
+
if (!isSameOrigin) {
|
|
5014
|
+
return originalFetch.apply(this, arguments);
|
|
5015
|
+
}
|
|
5016
|
+
|
|
5017
|
+
const method = (init?.method || (input instanceof Request ? input.method : 'GET') || 'GET').toUpperCase();
|
|
5018
|
+
|
|
5019
|
+
return originalFetch.apply(this, arguments).then(
|
|
5020
|
+
(response) => {
|
|
5021
|
+
if (response.status >= 400) {
|
|
5022
|
+
EventsManager.trackAutoEvent('network_error', {
|
|
5023
|
+
url: urlStr,
|
|
5024
|
+
method: method,
|
|
5025
|
+
status: response.status,
|
|
5026
|
+
status_text: `HTTP ${response.status} Error`,
|
|
5027
|
+
duration_ms: Date.now() - startTime
|
|
5028
|
+
});
|
|
5029
|
+
}
|
|
5030
|
+
return response;
|
|
5031
|
+
},
|
|
5032
|
+
(err) => {
|
|
5033
|
+
// Network-level failure (offline, DNS, CORS preflight, etc.)
|
|
5034
|
+
EventsManager.trackAutoEvent('network_error', {
|
|
5035
|
+
url: urlStr,
|
|
5036
|
+
method: method,
|
|
5037
|
+
status: 0,
|
|
5038
|
+
status_text: 'Fetch Network Error',
|
|
5039
|
+
duration_ms: Date.now() - startTime
|
|
5040
|
+
});
|
|
5041
|
+
throw err; // re-throw so the calling code still gets the error
|
|
5042
|
+
}
|
|
5043
|
+
);
|
|
5044
|
+
};
|
|
5388
5045
|
}
|
|
5046
|
+
} catch (error) {
|
|
5047
|
+
}
|
|
5389
5048
|
},
|
|
5390
5049
|
|
|
5391
5050
|
/**
|
|
@@ -5658,8 +5317,54 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5658
5317
|
let clsScore = 0;
|
|
5659
5318
|
let longTasksCount = 0;
|
|
5660
5319
|
let maxLongTaskMs = 0;
|
|
5320
|
+
let longTaskTimestamps = []; // [{start_ms, duration_ms}] capped at 20
|
|
5661
5321
|
let inpMs = 0;
|
|
5662
5322
|
|
|
5323
|
+
// ── Exit type detection ─────────────────────────────────────────────
|
|
5324
|
+
// Classifies HOW the user left this page so the AI can distinguish
|
|
5325
|
+
// explicit rejection (back-button) from fatigue (tab-close) from
|
|
5326
|
+
// internal navigation (SPA route change or page refresh).
|
|
5327
|
+
//
|
|
5328
|
+
// Values:
|
|
5329
|
+
// 'back_forward' – browser back/forward button (popstate)
|
|
5330
|
+
// 'spa_navigation' – SPA pushState / replaceState route change
|
|
5331
|
+
// 'reload' – page refresh (F5 / Ctrl+R / location.reload)
|
|
5332
|
+
// 'tab_close' – tab or window closed (beforeunload, no subsequent nav)
|
|
5333
|
+
// 'unknown' – couldn't be determined (visibilitychange, iOS Safari, etc.)
|
|
5334
|
+
let pageExitType = 'unknown';
|
|
5335
|
+
|
|
5336
|
+
window.addEventListener('popstate', () => {
|
|
5337
|
+
pageExitType = 'back_forward';
|
|
5338
|
+
}, { once: true });
|
|
5339
|
+
|
|
5340
|
+
// Intercept SPA navigation — hook into the already-overridden pushState /
|
|
5341
|
+
// replaceState (the overrides were applied in Section 11: SessionTracker).
|
|
5342
|
+
// We listen to the 'cq:spa-navigation' custom event that we dispatch below,
|
|
5343
|
+
// rather than overriding pushState a second time.
|
|
5344
|
+
// Detect by watching for our own navigation event (fired by SessionTracker).
|
|
5345
|
+
window.addEventListener('cq:spa-navigation', () => {
|
|
5346
|
+
if (pageExitType === 'unknown') pageExitType = 'spa_navigation';
|
|
5347
|
+
}, { once: true });
|
|
5348
|
+
|
|
5349
|
+
// Detect reload via PerformanceNavigationTiming.type === 'reload'
|
|
5350
|
+
try {
|
|
5351
|
+
const navEntry = performance.getEntriesByType('navigation')[0];
|
|
5352
|
+
if (navEntry && navEntry.type === 'reload') {
|
|
5353
|
+
// The CURRENT page was reached by reload; if user reloads again
|
|
5354
|
+
// we'll see another 'reload' on the next load — set a provisional flag.
|
|
5355
|
+
pageExitType = 'reload_arrived'; // overridden by popstate/spa if applicable
|
|
5356
|
+
}
|
|
5357
|
+
} catch (_) {}
|
|
5358
|
+
|
|
5359
|
+
window.addEventListener('beforeunload', () => {
|
|
5360
|
+
// beforeunload fires for tab-close AND for hard navigations (link clicks,
|
|
5361
|
+
// address-bar entry). SPA navigations do NOT fire it. popstate fires
|
|
5362
|
+
// before pagehide for back/forward so the flag is already set.
|
|
5363
|
+
if (pageExitType === 'unknown' || pageExitType === 'reload_arrived') {
|
|
5364
|
+
pageExitType = 'tab_close';
|
|
5365
|
+
}
|
|
5366
|
+
});
|
|
5367
|
+
|
|
5663
5368
|
const pageLoadTime = Date.now();
|
|
5664
5369
|
let lastSigScrollY = window.scrollY;
|
|
5665
5370
|
let lastSigDir = 0; // 1=down, -1=up
|
|
@@ -5691,6 +5396,16 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5691
5396
|
for (const e of list.getEntries()) {
|
|
5692
5397
|
longTasksCount++;
|
|
5693
5398
|
if (e.duration > maxLongTaskMs) maxLongTaskMs = Math.round(e.duration);
|
|
5399
|
+
// Record when each long task started (ms since page load, same reference
|
|
5400
|
+
// frame as first_interaction_ms) so the AI can correlate main-thread
|
|
5401
|
+
// blocking with the moment a user first tried to interact.
|
|
5402
|
+
// Cap at 20 — pathological pages sample the earliest 20 tasks.
|
|
5403
|
+
if (longTaskTimestamps.length < 20) {
|
|
5404
|
+
longTaskTimestamps.push({
|
|
5405
|
+
start_ms: Math.round(e.startTime),
|
|
5406
|
+
duration_ms: Math.round(e.duration)
|
|
5407
|
+
});
|
|
5408
|
+
}
|
|
5694
5409
|
}
|
|
5695
5410
|
}).observe({ type: 'longtask', buffered: true });
|
|
5696
5411
|
} catch (_) {}
|
|
@@ -5931,6 +5646,11 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5931
5646
|
.sort((a, b) => b.dwell_ms - a.dwell_ms)
|
|
5932
5647
|
.slice(0, 20);
|
|
5933
5648
|
|
|
5649
|
+
// Finalise exit_type: if it's still the provisional reload_arrived value
|
|
5650
|
+
// (user reloaded this page and didn't navigate away via back/spa), keep it
|
|
5651
|
+
// as 'reload' since the beforeunload didn't fire (e.g. visibilitychange path).
|
|
5652
|
+
const finalExitType = pageExitType === 'reload_arrived' ? 'reload' : pageExitType;
|
|
5653
|
+
|
|
5934
5654
|
EventsManager.trackAutoEvent('page_summary', {
|
|
5935
5655
|
scroll_reversals: scrollReversals,
|
|
5936
5656
|
scroll_reversal_depths: scrollReversalDepths,
|
|
@@ -5947,7 +5667,9 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5947
5667
|
cls_score: clsScore,
|
|
5948
5668
|
long_tasks_count: longTasksCount,
|
|
5949
5669
|
max_long_task_ms: maxLongTaskMs,
|
|
5670
|
+
long_task_timestamps: longTaskTimestamps,
|
|
5950
5671
|
inp_ms: inpMs,
|
|
5672
|
+
exit_type: finalExitType,
|
|
5951
5673
|
document_height: document.documentElement.scrollHeight || document.body.scrollHeight || 0,
|
|
5952
5674
|
document_width: document.documentElement.scrollWidth || document.body.scrollWidth || 0,
|
|
5953
5675
|
viewport_width: window.innerWidth,
|
|
@@ -6061,9 +5783,6 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
6061
5783
|
// Initialize session data structure
|
|
6062
5784
|
SessionDataManager.initialize(sessionId, userId);
|
|
6063
5785
|
|
|
6064
|
-
// Initialize interactions
|
|
6065
|
-
InteractionManager.initialize();
|
|
6066
|
-
|
|
6067
5786
|
// Initialize page visits
|
|
6068
5787
|
PageVisitManager.initialize();
|
|
6069
5788
|
} catch (error) {
|
|
@@ -6206,15 +5925,6 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
6206
5925
|
sessionData.pagesViewed = sessionData.visited_pages.length;
|
|
6207
5926
|
}
|
|
6208
5927
|
|
|
6209
|
-
// Sync interactions
|
|
6210
|
-
if (storedSession.sessionData.interactions) {
|
|
6211
|
-
const storedInteractions = storedSession.sessionData.interactions;
|
|
6212
|
-
if (storedInteractions.totalInteractions >
|
|
6213
|
-
(sessionData.interactions?.totalInteractions || 0)) {
|
|
6214
|
-
// Update interactions from storage
|
|
6215
|
-
sessionData.interactions = storedInteractions;
|
|
6216
|
-
}
|
|
6217
|
-
}
|
|
6218
5928
|
}
|
|
6219
5929
|
|
|
6220
5930
|
// Update endTime
|
|
@@ -6277,9 +5987,6 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
6277
5987
|
}));
|
|
6278
5988
|
sessionData.pagesViewed = sessionData.visited_pages.length;
|
|
6279
5989
|
}
|
|
6280
|
-
if (storedSession.sessionData.interactions) {
|
|
6281
|
-
sessionData.interactions = storedSession.sessionData.interactions;
|
|
6282
|
-
}
|
|
6283
5990
|
}
|
|
6284
5991
|
|
|
6285
5992
|
// Track browser close (last page unmount)
|
|
@@ -6654,6 +6361,13 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
6654
6361
|
*/
|
|
6655
6362
|
async trackAutoEvent(eventName, autoEventData = {}, elementData = {}) {
|
|
6656
6363
|
try {
|
|
6364
|
+
// Keep a running record of the last auto-event name so that js_error
|
|
6365
|
+
// events can include a "last_event_name" field for context.
|
|
6366
|
+
// Skip error-category events themselves to avoid circular noise.
|
|
6367
|
+
if (eventName && eventName !== 'js_error' && eventName !== 'network_error') {
|
|
6368
|
+
EventsManager._lastAutoEventName = eventName;
|
|
6369
|
+
}
|
|
6370
|
+
|
|
6657
6371
|
// Check if auto events should be tracked (silently skip if disabled)
|
|
6658
6372
|
if (!shouldTrackAutoEvents()) {
|
|
6659
6373
|
return; // Silently skip - don't log to avoid console spam
|
|
@@ -7332,8 +7046,23 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
7332
7046
|
let clickCount = 0;
|
|
7333
7047
|
let lastClickTime = 0;
|
|
7334
7048
|
let lastClickElement = null;
|
|
7049
|
+
let rageStartTime = 0; // Timestamp of the first click in the current rage sequence
|
|
7050
|
+
let rageTimer = null; // Debounce timer — fires ONE rage_click after sequence ends
|
|
7335
7051
|
const pendingDeadClicks = new Map(); // Track clicks that might be dead clicks
|
|
7336
7052
|
|
|
7053
|
+
// MutationObserver counter — incremented on ANY DOM change.
|
|
7054
|
+
// Dead-click detection compares the count at click-time vs 800 ms later:
|
|
7055
|
+
// if nothing changed (URL stable + zero new mutations) it's a true dead click.
|
|
7056
|
+
// This prevents false positives in SPAs where interactions update the DOM
|
|
7057
|
+
// without changing the URL (modals, dropdowns, cart updates, tab panels, etc.).
|
|
7058
|
+
let domMutationCount = 0;
|
|
7059
|
+
try {
|
|
7060
|
+
new MutationObserver(() => { domMutationCount++; })
|
|
7061
|
+
.observe(document.documentElement, {
|
|
7062
|
+
childList: true, subtree: true, attributes: true, characterData: false
|
|
7063
|
+
});
|
|
7064
|
+
} catch (_) {}
|
|
7065
|
+
|
|
7337
7066
|
// Helper function to check if element is interactive
|
|
7338
7067
|
const isInteractiveElement = (el) => {
|
|
7339
7068
|
if (!el) return false;
|
|
@@ -7423,31 +7152,60 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
7423
7152
|
elementData.image_height = element.naturalHeight || null;
|
|
7424
7153
|
}
|
|
7425
7154
|
|
|
7426
|
-
// Detect rage clicks
|
|
7155
|
+
// Detect rage clicks — fire exactly ONE event per sequence when the burst ends.
|
|
7156
|
+
// A "sequence" is N rapid clicks on the same element within 1 s of each other.
|
|
7157
|
+
// We debounce 800 ms after the last click so the event carries the FINAL count
|
|
7158
|
+
// and the total time-span from first to last click (not just the last interval).
|
|
7427
7159
|
if (element === lastClickElement && now - lastClickTime < 1000) {
|
|
7428
7160
|
clickCount++;
|
|
7161
|
+
|
|
7429
7162
|
if (clickCount >= 3) {
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7163
|
+
// Cancel any previously scheduled fire — sequence is still ongoing
|
|
7164
|
+
if (rageTimer) { clearTimeout(rageTimer); rageTimer = null; }
|
|
7165
|
+
|
|
7166
|
+
// Snapshot mutable values at the time of THIS click
|
|
7167
|
+
const snapScrollX = window.scrollX != null ? window.scrollX : window.pageXOffset;
|
|
7168
|
+
const snapScrollY = window.scrollY != null ? window.scrollY : window.pageYOffset;
|
|
7169
|
+
const snapDocHeight = Math.min(Math.max(document.body.scrollHeight, document.documentElement.scrollHeight), 25000);
|
|
7170
|
+
const snapDocWidth = Math.min(Math.max(document.body.scrollWidth, document.documentElement.scrollWidth), 25000);
|
|
7171
|
+
const snapClientX = event.clientX;
|
|
7172
|
+
const snapClientY = event.clientY;
|
|
7173
|
+
const snapPageX = event.pageX;
|
|
7174
|
+
const snapPageY = event.pageY;
|
|
7175
|
+
const snapElemData = Object.assign({}, elementData);
|
|
7176
|
+
const snapCategory = elementCategory;
|
|
7177
|
+
const snapArea = element.offsetWidth * element.offsetHeight;
|
|
7178
|
+
const snapAriaLabel = element.getAttribute('aria-label') || null;
|
|
7179
|
+
const snapStartTime = rageStartTime;
|
|
7180
|
+
const snapNow = now;
|
|
7181
|
+
|
|
7182
|
+
// Fire after 800 ms of inactivity — reads the FINAL clickCount from closure
|
|
7183
|
+
rageTimer = setTimeout(() => {
|
|
7184
|
+
rageTimer = null;
|
|
7185
|
+
EventsManager.trackAutoEvent('rage_click', {
|
|
7186
|
+
click_coordinates: { x: snapClientX, y: snapClientY },
|
|
7187
|
+
page_x: snapPageX,
|
|
7188
|
+
page_y: snapPageY,
|
|
7189
|
+
scroll_x: snapScrollX,
|
|
7190
|
+
scroll_y: snapScrollY,
|
|
7191
|
+
document_height: snapDocHeight,
|
|
7192
|
+
document_width: snapDocWidth,
|
|
7193
|
+
click_count: clickCount, // final count for the whole sequence
|
|
7194
|
+
time_span: snapNow - snapStartTime, // first→last click duration
|
|
7195
|
+
element_area: snapArea,
|
|
7196
|
+
element_category: snapCategory,
|
|
7197
|
+
aria_label: snapAriaLabel
|
|
7198
|
+
}, snapElemData).catch(() => {});
|
|
7199
|
+
// Reset sequence state after the event is dispatched
|
|
7200
|
+
clickCount = 0;
|
|
7201
|
+
lastClickElement = null;
|
|
7202
|
+
}, 800);
|
|
7448
7203
|
}
|
|
7449
7204
|
} else {
|
|
7450
|
-
|
|
7205
|
+
// New element or gap > 1 s — start a fresh sequence
|
|
7206
|
+
if (rageTimer) { clearTimeout(rageTimer); rageTimer = null; }
|
|
7207
|
+
clickCount = 1;
|
|
7208
|
+
rageStartTime = now;
|
|
7451
7209
|
}
|
|
7452
7210
|
|
|
7453
7211
|
// Check if this might be a dead click (non-interactive element)
|
|
@@ -7471,6 +7229,7 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
7471
7229
|
elementCategory,
|
|
7472
7230
|
timestamp: now,
|
|
7473
7231
|
url: window.location.href,
|
|
7232
|
+
snapshotMutationCount: domMutationCount, // DOM state at click time
|
|
7474
7233
|
clickX,
|
|
7475
7234
|
clickY,
|
|
7476
7235
|
page_x: event.pageX,
|
|
@@ -7481,13 +7240,17 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
7481
7240
|
document_width: docWidth
|
|
7482
7241
|
});
|
|
7483
7242
|
|
|
7484
|
-
// Check after
|
|
7243
|
+
// Check after 800 ms: if URL unchanged AND DOM unchanged → true dead click.
|
|
7244
|
+
// Using 800 ms (down from 1 s) tightens the window while still allowing
|
|
7245
|
+
// async renders (React setState, animations) to complete before we decide.
|
|
7485
7246
|
setTimeout(() => {
|
|
7486
7247
|
checkNavigation();
|
|
7487
|
-
|
|
7248
|
+
|
|
7488
7249
|
const pendingClick = pendingDeadClicks.get(clickId);
|
|
7489
|
-
if (pendingClick &&
|
|
7490
|
-
|
|
7250
|
+
if (pendingClick &&
|
|
7251
|
+
window.location.href === pendingClick.url &&
|
|
7252
|
+
domMutationCount === pendingClick.snapshotMutationCount) {
|
|
7253
|
+
// No URL change AND no DOM mutations → confirmed dead click
|
|
7491
7254
|
pendingDeadClicks.delete(clickId);
|
|
7492
7255
|
|
|
7493
7256
|
EventsManager.trackAutoEvent('dead_click', {
|
|
@@ -7507,10 +7270,10 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
7507
7270
|
console.error('❌ [AutoEvents] Failed to track dead_click:', err);
|
|
7508
7271
|
});
|
|
7509
7272
|
} else if (pendingClick) {
|
|
7510
|
-
//
|
|
7273
|
+
// URL changed or DOM mutated — interaction was real, not a dead click
|
|
7511
7274
|
pendingDeadClicks.delete(clickId);
|
|
7512
7275
|
}
|
|
7513
|
-
},
|
|
7276
|
+
}, 800);
|
|
7514
7277
|
}
|
|
7515
7278
|
|
|
7516
7279
|
// Track regular click with enhanced data (viewport + page-relative for heatmaps)
|
|
@@ -8406,10 +8169,6 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
8406
8169
|
return sessionData;
|
|
8407
8170
|
},
|
|
8408
8171
|
|
|
8409
|
-
// Interaction Functions
|
|
8410
|
-
getChronologicalInteractions: InteractionManager.getChronological.bind(InteractionManager),
|
|
8411
|
-
sortInteractionsChronologically: InteractionManager.getChronological.bind(InteractionManager),
|
|
8412
|
-
|
|
8413
8172
|
// Auto Events Configuration
|
|
8414
8173
|
enableAutoEvents: function() {
|
|
8415
8174
|
CONFIG.AUTO_EVENTS.enabled = true;
|