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