cryptique-sdk 1.2.15 → 1.2.16
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 +406 -169
- package/lib/esm/index.js +406 -169
- package/lib/umd/index.js +406 -169
- package/package.json +1 -1
package/lib/cjs/index.js
CHANGED
|
@@ -145,7 +145,16 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
// Parse disabled events (comma-separated string to array)
|
|
148
|
-
//
|
|
148
|
+
// Disabling works per-event-name. All auto event names that can be individually disabled:
|
|
149
|
+
// Interaction: element_click, element_hover, rage_click, dead_click, page_scroll, text_selection
|
|
150
|
+
// Navigation: page_view
|
|
151
|
+
// Form: form_submit, form_validation_error, form_abandoned
|
|
152
|
+
// Media: media_play, media_pause, media_ended
|
|
153
|
+
// Visibility: element_view
|
|
154
|
+
// Error: js_error, network_error
|
|
155
|
+
// Performance: page_performance
|
|
156
|
+
// Session: session_start, session_end
|
|
157
|
+
// Example: auto-events-disabled-events="js_error,network_error,page_performance"
|
|
149
158
|
if (disabledEventsAttr && disabledEventsAttr.trim()) {
|
|
150
159
|
CONFIG.AUTO_EVENTS.disabledEvents = disabledEventsAttr
|
|
151
160
|
.split(',')
|
|
@@ -160,7 +169,9 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
160
169
|
isInitialized: false, // Prevents API calls during initialization
|
|
161
170
|
sessionConfirmed: false, // True after first successful POST /api/sdk/track (session exists on server)
|
|
162
171
|
eip6963Providers: [], // EIP-6963 discovered wallet providers
|
|
163
|
-
reportedWalletAddress: null
|
|
172
|
+
reportedWalletAddress: null, // Last wallet address reported to backend (prevents duplicate calls)
|
|
173
|
+
activeTimeMs: 0, // Cumulative time tab was in foreground (for active_time_seconds)
|
|
174
|
+
tabFocusTimestamp: null // When the tab most recently became visible
|
|
164
175
|
};
|
|
165
176
|
|
|
166
177
|
// Ready promise - resolves when SDK is fully initialized (allows consumers to await init)
|
|
@@ -4039,10 +4050,12 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
4039
4050
|
const interactionTypes = [
|
|
4040
4051
|
'clicks', 'mediaInteractions', 'contextMenuEvents', 'windowEvents',
|
|
4041
4052
|
'formInteractions', 'scrollEvents', 'focusEvents', 'hoverEvents',
|
|
4042
|
-
'formSubmissions', 'fieldChanges',
|
|
4053
|
+
'formSubmissions', 'fieldChanges',
|
|
4043
4054
|
'keyboardEvents', 'copyPasteEvents', 'dragDropEvents',
|
|
4044
|
-
'touchEvents', '
|
|
4045
|
-
|
|
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)
|
|
4046
4059
|
];
|
|
4047
4060
|
|
|
4048
4061
|
// Helper to normalize path for matching
|
|
@@ -4260,7 +4273,10 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
4260
4273
|
interactions: {
|
|
4261
4274
|
...sessionInteractions,
|
|
4262
4275
|
chronological: chronologicalInteractions.chronological || []
|
|
4263
|
-
}
|
|
4276
|
+
},
|
|
4277
|
+
|
|
4278
|
+
// Active tab time in ms (tab foreground time only, for active_time_seconds in DB)
|
|
4279
|
+
active_time_ms: sourceData.active_time_ms || null
|
|
4264
4280
|
};
|
|
4265
4281
|
|
|
4266
4282
|
// Remove null/undefined values for cleaner payload (optional - can be kept for explicit nulls)
|
|
@@ -4743,17 +4759,20 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
4743
4759
|
if (event.target &&
|
|
4744
4760
|
(event.target.tagName === 'INPUT' ||
|
|
4745
4761
|
event.target.tagName === 'TEXTAREA')) {
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4762
|
+
EventsManager.trackAutoEvent('form_validation_error', {
|
|
4763
|
+
field_id: event.target.id || '',
|
|
4764
|
+
field_name: event.target.name || '',
|
|
4765
|
+
field_type: event.target.type || event.target.tagName.toLowerCase(),
|
|
4766
|
+
form_id: event.target.form ? (event.target.form.id || '') : '',
|
|
4767
|
+
validation_message: event.target.validationMessage || ''
|
|
4768
|
+
}, {
|
|
4769
|
+
element_id: event.target.id || '',
|
|
4770
|
+
element_name: event.target.name || '',
|
|
4771
|
+
element_tag_name: event.target.tagName || '',
|
|
4772
|
+
element_type: event.target.type || '',
|
|
4773
|
+
element_class: event.target.className || '',
|
|
4774
|
+
element_text: (event.target.placeholder || event.target.getAttribute('aria-label') || '').trim().slice(0, 100)
|
|
4775
|
+
});
|
|
4757
4776
|
}
|
|
4758
4777
|
}, true);
|
|
4759
4778
|
} catch (error) {
|
|
@@ -5208,23 +5227,62 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5208
5227
|
*/
|
|
5209
5228
|
startPerformanceTracking() {
|
|
5210
5229
|
try {
|
|
5230
|
+
// Collect Core Web Vitals via PerformanceObserver + Navigation Timing
|
|
5231
|
+
// Emits a single queryable page_performance auto event per page load
|
|
5232
|
+
const vitals = { lcp: null, cls: 0, inp: null, fcp: null, ttfb: null };
|
|
5233
|
+
|
|
5234
|
+
// LCP
|
|
5235
|
+
try {
|
|
5236
|
+
new PerformanceObserver((list) => {
|
|
5237
|
+
const entries = list.getEntries();
|
|
5238
|
+
if (entries.length > 0) {
|
|
5239
|
+
vitals.lcp = Math.round(entries[entries.length - 1].startTime);
|
|
5240
|
+
}
|
|
5241
|
+
}).observe({ type: 'largest-contentful-paint', buffered: true });
|
|
5242
|
+
} catch (e) {}
|
|
5243
|
+
|
|
5244
|
+
// CLS
|
|
5245
|
+
try {
|
|
5246
|
+
new PerformanceObserver((list) => {
|
|
5247
|
+
for (const entry of list.getEntries()) {
|
|
5248
|
+
if (!entry.hadRecentInput) {
|
|
5249
|
+
vitals.cls = Math.round((vitals.cls + entry.value) * 10000) / 10000;
|
|
5250
|
+
}
|
|
5251
|
+
}
|
|
5252
|
+
}).observe({ type: 'layout-shift', buffered: true });
|
|
5253
|
+
} catch (e) {}
|
|
5254
|
+
|
|
5255
|
+
// INP (interaction to next paint)
|
|
5256
|
+
try {
|
|
5257
|
+
new PerformanceObserver((list) => {
|
|
5258
|
+
for (const entry of list.getEntries()) {
|
|
5259
|
+
if (entry.duration && (vitals.inp === null || entry.duration > vitals.inp)) {
|
|
5260
|
+
vitals.inp = Math.round(entry.duration);
|
|
5261
|
+
}
|
|
5262
|
+
}
|
|
5263
|
+
}).observe({ type: 'event', buffered: true, durationThreshold: 16 });
|
|
5264
|
+
} catch (e) {}
|
|
5265
|
+
|
|
5266
|
+
// FCP + TTFB from paint + navigation entries (available after load)
|
|
5211
5267
|
window.addEventListener('load', () => {
|
|
5212
|
-
// Wait a bit for all resources to load
|
|
5213
5268
|
setTimeout(() => {
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5269
|
+
try {
|
|
5270
|
+
const nav = performance.getEntriesByType('navigation')[0];
|
|
5271
|
+
const paint = performance.getEntriesByType('paint');
|
|
5272
|
+
vitals.ttfb = nav ? Math.round(nav.responseStart - nav.requestStart) : null;
|
|
5273
|
+
vitals.fcp = paint.find(p => p.name === 'first-contentful-paint')
|
|
5274
|
+
? Math.round(paint.find(p => p.name === 'first-contentful-paint').startTime)
|
|
5275
|
+
: null;
|
|
5276
|
+
} catch (e) {}
|
|
5277
|
+
|
|
5278
|
+
EventsManager.trackAutoEvent('page_performance', {
|
|
5279
|
+
lcp: vitals.lcp,
|
|
5280
|
+
cls: vitals.cls,
|
|
5281
|
+
inp: vitals.inp,
|
|
5282
|
+
fcp: vitals.fcp,
|
|
5283
|
+
ttfb: vitals.ttfb
|
|
5284
|
+
});
|
|
5285
|
+
}, 1500);
|
|
5228
5286
|
});
|
|
5229
5287
|
} catch (error) {
|
|
5230
5288
|
}
|
|
@@ -5232,40 +5290,28 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5232
5290
|
|
|
5233
5291
|
/**
|
|
5234
5292
|
* ErrorTracker - Tracks JavaScript errors
|
|
5293
|
+
* Emits as queryable auto events (js_error) instead of session-level JSONB
|
|
5235
5294
|
*/
|
|
5236
5295
|
startErrorTracking() {
|
|
5237
5296
|
try {
|
|
5238
5297
|
window.addEventListener('error', (event) => {
|
|
5239
|
-
|
|
5240
|
-
|
|
5298
|
+
EventsManager.trackAutoEvent('js_error', {
|
|
5299
|
+
error_type: 'javascript_error',
|
|
5241
5300
|
message: event.message || '',
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
tagName: event.target?.tagName || '',
|
|
5248
|
-
id: event.target?.id || '',
|
|
5249
|
-
className: event.target?.className || '',
|
|
5250
|
-
src: event.target?.src || '',
|
|
5251
|
-
href: event.target?.href || ''
|
|
5252
|
-
},
|
|
5253
|
-
path: window.location.pathname
|
|
5254
|
-
};
|
|
5255
|
-
|
|
5256
|
-
InteractionManager.add('errorEvents', errorData);
|
|
5301
|
+
source: event.filename || '',
|
|
5302
|
+
line: event.lineno || 0,
|
|
5303
|
+
column: event.colno || 0,
|
|
5304
|
+
stack: event.error?.stack || ''
|
|
5305
|
+
});
|
|
5257
5306
|
});
|
|
5258
|
-
|
|
5307
|
+
|
|
5259
5308
|
// Track unhandled promise rejections
|
|
5260
5309
|
window.addEventListener('unhandledrejection', (event) => {
|
|
5261
|
-
|
|
5262
|
-
|
|
5310
|
+
EventsManager.trackAutoEvent('js_error', {
|
|
5311
|
+
error_type: 'unhandled_promise_rejection',
|
|
5263
5312
|
message: event.reason?.message || String(event.reason || 'Unknown error'),
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
};
|
|
5267
|
-
|
|
5268
|
-
InteractionManager.add('errorEvents', errorData);
|
|
5313
|
+
stack: event.reason?.stack || String(event.reason || '')
|
|
5314
|
+
});
|
|
5269
5315
|
});
|
|
5270
5316
|
} catch (error) {
|
|
5271
5317
|
}
|
|
@@ -5303,32 +5349,24 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5303
5349
|
|
|
5304
5350
|
xhr.addEventListener('load', function() {
|
|
5305
5351
|
if (xhr.status >= 400) {
|
|
5306
|
-
|
|
5307
|
-
type: 'api_error',
|
|
5352
|
+
EventsManager.trackAutoEvent('network_error', {
|
|
5308
5353
|
url: xhr._cryptiqueUrl,
|
|
5309
5354
|
method: xhr._cryptiqueMethod || 'GET',
|
|
5310
5355
|
status: xhr.status,
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
};
|
|
5315
|
-
|
|
5316
|
-
InteractionManager.add('networkEvents', networkErrorData);
|
|
5356
|
+
status_text: `HTTP ${xhr.status} Error`,
|
|
5357
|
+
duration_ms: Date.now() - startTime
|
|
5358
|
+
});
|
|
5317
5359
|
}
|
|
5318
5360
|
});
|
|
5319
|
-
|
|
5361
|
+
|
|
5320
5362
|
xhr.addEventListener('error', function() {
|
|
5321
|
-
|
|
5322
|
-
type: 'api_error',
|
|
5363
|
+
EventsManager.trackAutoEvent('network_error', {
|
|
5323
5364
|
url: xhr._cryptiqueUrl,
|
|
5324
5365
|
method: xhr._cryptiqueMethod || 'GET',
|
|
5325
5366
|
status: 0,
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
};
|
|
5330
|
-
|
|
5331
|
-
InteractionManager.add('networkEvents', networkErrorData);
|
|
5367
|
+
status_text: 'XHR Network Error',
|
|
5368
|
+
duration_ms: Date.now() - startTime
|
|
5369
|
+
});
|
|
5332
5370
|
});
|
|
5333
5371
|
|
|
5334
5372
|
return originalXHRSend.apply(this, args);
|
|
@@ -5343,9 +5381,9 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5343
5381
|
startAdvancedFormTracking() {
|
|
5344
5382
|
try {
|
|
5345
5383
|
const formStartTimes = new Map();
|
|
5346
|
-
|
|
5384
|
+
|
|
5347
5385
|
document.addEventListener('focus', (event) => {
|
|
5348
|
-
if (event.target &&
|
|
5386
|
+
if (event.target &&
|
|
5349
5387
|
(event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA')) {
|
|
5350
5388
|
const formId = event.target.form?.id || 'unknown';
|
|
5351
5389
|
if (!formStartTimes.has(formId)) {
|
|
@@ -5353,11 +5391,11 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5353
5391
|
}
|
|
5354
5392
|
}
|
|
5355
5393
|
});
|
|
5356
|
-
|
|
5394
|
+
|
|
5357
5395
|
document.addEventListener('submit', (event) => {
|
|
5358
5396
|
const formId = event.target.id || 'unknown';
|
|
5359
5397
|
const startTime = formStartTimes.get(formId);
|
|
5360
|
-
|
|
5398
|
+
|
|
5361
5399
|
if (startTime) {
|
|
5362
5400
|
const formCompletionData = {
|
|
5363
5401
|
type: 'form_completion',
|
|
@@ -5366,7 +5404,7 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5366
5404
|
fieldCount: event.target.elements.length,
|
|
5367
5405
|
path: window.location.pathname
|
|
5368
5406
|
};
|
|
5369
|
-
|
|
5407
|
+
|
|
5370
5408
|
InteractionManager.add('formAnalytics', formCompletionData);
|
|
5371
5409
|
formStartTimes.delete(formId);
|
|
5372
5410
|
}
|
|
@@ -5375,9 +5413,172 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5375
5413
|
}
|
|
5376
5414
|
},
|
|
5377
5415
|
|
|
5416
|
+
/**
|
|
5417
|
+
* FormAbandonmentTracker - Emits form_abandoned auto event when a user
|
|
5418
|
+
* interacts with a form but navigates away without submitting.
|
|
5419
|
+
* Tracks which field they stopped at — high-value PM signal.
|
|
5420
|
+
*/
|
|
5421
|
+
startFormAbandonmentTracking() {
|
|
5422
|
+
try {
|
|
5423
|
+
// Per-form state: formId → { startTime, lastFieldId, lastFieldType, filledFields, totalFields, submitted }
|
|
5424
|
+
const formState = new Map();
|
|
5425
|
+
|
|
5426
|
+
const getOrInitForm = (formEl) => {
|
|
5427
|
+
const formId = formEl?.id || formEl?.name || 'form_' + (formEl ? [...document.forms].indexOf(formEl) : 0);
|
|
5428
|
+
if (!formState.has(formId)) {
|
|
5429
|
+
const totalFields = formEl
|
|
5430
|
+
? formEl.querySelectorAll('input:not([type=hidden]), select, textarea').length
|
|
5431
|
+
: 0;
|
|
5432
|
+
formState.set(formId, {
|
|
5433
|
+
formId,
|
|
5434
|
+
startTime: Date.now(),
|
|
5435
|
+
lastFieldId: '',
|
|
5436
|
+
lastFieldType: '',
|
|
5437
|
+
filledFields: new Set(),
|
|
5438
|
+
totalFields,
|
|
5439
|
+
submitted: false
|
|
5440
|
+
});
|
|
5441
|
+
}
|
|
5442
|
+
return formState.get(formId);
|
|
5443
|
+
};
|
|
5444
|
+
|
|
5445
|
+
// Track field focus to know where user is
|
|
5446
|
+
document.addEventListener('focus', (event) => {
|
|
5447
|
+
const target = event.target;
|
|
5448
|
+
if (!target || !['INPUT', 'SELECT', 'TEXTAREA'].includes(target.tagName)) return;
|
|
5449
|
+
if (target.type === 'hidden') return;
|
|
5450
|
+
const state = getOrInitForm(target.form);
|
|
5451
|
+
state.lastFieldId = target.id || target.name || '';
|
|
5452
|
+
state.lastFieldType = target.type || target.tagName.toLowerCase();
|
|
5453
|
+
}, true);
|
|
5454
|
+
|
|
5455
|
+
// Track field blur to record which fields have been filled
|
|
5456
|
+
document.addEventListener('blur', (event) => {
|
|
5457
|
+
const target = event.target;
|
|
5458
|
+
if (!target || !['INPUT', 'SELECT', 'TEXTAREA'].includes(target.tagName)) return;
|
|
5459
|
+
if (target.type === 'hidden') return;
|
|
5460
|
+
const state = getOrInitForm(target.form);
|
|
5461
|
+
const fieldKey = target.id || target.name || target.type;
|
|
5462
|
+
if (target.value && target.value.length > 0 && fieldKey) {
|
|
5463
|
+
state.filledFields.add(fieldKey);
|
|
5464
|
+
}
|
|
5465
|
+
}, true);
|
|
5466
|
+
|
|
5467
|
+
// Mark form as submitted so we don't fire form_abandoned
|
|
5468
|
+
document.addEventListener('submit', (event) => {
|
|
5469
|
+
const formId = event.target?.id || event.target?.name || 'form_' + [...document.forms].indexOf(event.target);
|
|
5470
|
+
if (formState.has(formId)) {
|
|
5471
|
+
formState.get(formId).submitted = true;
|
|
5472
|
+
}
|
|
5473
|
+
}, true);
|
|
5474
|
+
|
|
5475
|
+
// On page unload, fire form_abandoned for any touched, unsubmitted forms
|
|
5476
|
+
window.addEventListener('beforeunload', () => {
|
|
5477
|
+
formState.forEach((state) => {
|
|
5478
|
+
if (!state.submitted && state.filledFields.size > 0) {
|
|
5479
|
+
// Fire synchronously via sendBeacon path — trackAutoEvent is async
|
|
5480
|
+
// but beforeunload already handles this via sendBeacon in _sendEvent
|
|
5481
|
+
// Resolve the actual form DOM element (if still in DOM)
|
|
5482
|
+
const formEl = state.formId && state.formId !== 'unknown'
|
|
5483
|
+
? document.getElementById(state.formId) || document.querySelector(`form[name="${state.formId}"]`)
|
|
5484
|
+
: null;
|
|
5485
|
+
EventsManager.trackAutoEvent('form_abandoned', {
|
|
5486
|
+
form_id: state.formId,
|
|
5487
|
+
last_active_field_id: state.lastFieldId,
|
|
5488
|
+
last_active_field_type: state.lastFieldType,
|
|
5489
|
+
fields_filled_count: state.filledFields.size,
|
|
5490
|
+
total_fields_count: state.totalFields,
|
|
5491
|
+
time_on_form_ms: Date.now() - state.startTime
|
|
5492
|
+
}, {
|
|
5493
|
+
element_id: formEl?.id || state.formId || '',
|
|
5494
|
+
element_name: formEl?.getAttribute('name') || '',
|
|
5495
|
+
element_tag_name: 'FORM',
|
|
5496
|
+
element_type: '',
|
|
5497
|
+
element_class: formEl?.className || '',
|
|
5498
|
+
element_text: (formEl?.getAttribute('aria-label') || '').slice(0, 100)
|
|
5499
|
+
});
|
|
5500
|
+
}
|
|
5501
|
+
});
|
|
5502
|
+
});
|
|
5503
|
+
} catch (error) {
|
|
5504
|
+
}
|
|
5505
|
+
},
|
|
5506
|
+
|
|
5507
|
+
/**
|
|
5508
|
+
* ElementVisibilityTracker - Emits element_view auto events when elements
|
|
5509
|
+
* with IDs or data-track attributes enter the viewport for ≥ 1 second.
|
|
5510
|
+
* Tells PMs whether users actually SAW key elements (CTAs, pricing, etc.)
|
|
5511
|
+
* vs just didn't click them — completely different problems, opposite fixes.
|
|
5512
|
+
*/
|
|
5513
|
+
startElementVisibilityTracking() {
|
|
5514
|
+
try {
|
|
5515
|
+
if (typeof IntersectionObserver === 'undefined') return;
|
|
5516
|
+
|
|
5517
|
+
// Only observe elements with an id or data-cq-track attribute
|
|
5518
|
+
const getTrackableElements = () =>
|
|
5519
|
+
document.querySelectorAll('[id]:not([id=""]), [data-cq-track]');
|
|
5520
|
+
|
|
5521
|
+
const viewTimers = new Map(); // elementKey → setTimeout handle
|
|
5522
|
+
const reported = new Set(); // elementKey → already fired once per page load
|
|
5523
|
+
|
|
5524
|
+
const observer = new IntersectionObserver((entries) => {
|
|
5525
|
+
entries.forEach((entry) => {
|
|
5526
|
+
const el = entry.target;
|
|
5527
|
+
const key = el.id || el.getAttribute('data-cq-track') || el.className;
|
|
5528
|
+
if (!key || reported.has(key)) return;
|
|
5529
|
+
|
|
5530
|
+
if (entry.isIntersecting && entry.intersectionRatio >= 0.5) {
|
|
5531
|
+
// Element entered viewport — start 1s timer
|
|
5532
|
+
if (!viewTimers.has(key)) {
|
|
5533
|
+
const enterTime = Date.now();
|
|
5534
|
+
const timer = setTimeout(() => {
|
|
5535
|
+
if (reported.has(key)) return;
|
|
5536
|
+
reported.add(key);
|
|
5537
|
+
EventsManager.trackAutoEvent('element_view', {
|
|
5538
|
+
time_in_viewport_ms: Date.now() - enterTime,
|
|
5539
|
+
viewport_percent_visible: Math.round(entry.intersectionRatio * 100)
|
|
5540
|
+
}, {
|
|
5541
|
+
element_id: el.id || '',
|
|
5542
|
+
element_name: el.getAttribute('name') || el.getAttribute('data-cq-track') || '',
|
|
5543
|
+
element_tag_name: el.tagName || '',
|
|
5544
|
+
element_type: el.type || el.getAttribute('type') || '',
|
|
5545
|
+
element_class: el.className || '',
|
|
5546
|
+
element_text: (el.textContent || '').trim().slice(0, 100)
|
|
5547
|
+
});
|
|
5548
|
+
viewTimers.delete(key);
|
|
5549
|
+
}, 1000);
|
|
5550
|
+
viewTimers.set(key, timer);
|
|
5551
|
+
}
|
|
5552
|
+
} else {
|
|
5553
|
+
// Element left viewport before 1s — cancel timer
|
|
5554
|
+
if (viewTimers.has(key)) {
|
|
5555
|
+
clearTimeout(viewTimers.get(key));
|
|
5556
|
+
viewTimers.delete(key);
|
|
5557
|
+
}
|
|
5558
|
+
}
|
|
5559
|
+
});
|
|
5560
|
+
}, { threshold: 0.5 });
|
|
5561
|
+
|
|
5562
|
+
// Observe existing elements
|
|
5563
|
+
getTrackableElements().forEach(el => observer.observe(el));
|
|
5564
|
+
|
|
5565
|
+
// Observe elements added to DOM later (SPAs)
|
|
5566
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
5567
|
+
new MutationObserver(() => {
|
|
5568
|
+
getTrackableElements().forEach(el => {
|
|
5569
|
+
if (!viewTimers.has(el.id || el.getAttribute('data-cq-track') || el.className)) {
|
|
5570
|
+
observer.observe(el);
|
|
5571
|
+
}
|
|
5572
|
+
});
|
|
5573
|
+
}).observe(document.body, { childList: true, subtree: true });
|
|
5574
|
+
}
|
|
5575
|
+
} catch (error) {
|
|
5576
|
+
}
|
|
5577
|
+
},
|
|
5578
|
+
|
|
5378
5579
|
/**
|
|
5379
5580
|
* Initialize all event trackers
|
|
5380
|
-
*
|
|
5581
|
+
*
|
|
5381
5582
|
* Starts all tracking modules
|
|
5382
5583
|
*/
|
|
5383
5584
|
initialize() {
|
|
@@ -5396,6 +5597,8 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5396
5597
|
this.startErrorTracking();
|
|
5397
5598
|
this.startNetworkTracking();
|
|
5398
5599
|
this.startAdvancedFormTracking();
|
|
5600
|
+
this.startFormAbandonmentTracking();
|
|
5601
|
+
this.startElementVisibilityTracking();
|
|
5399
5602
|
}
|
|
5400
5603
|
};
|
|
5401
5604
|
|
|
@@ -5531,9 +5734,36 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5531
5734
|
}
|
|
5532
5735
|
},
|
|
5533
5736
|
|
|
5737
|
+
/**
|
|
5738
|
+
* Tab Visibility Accumulator
|
|
5739
|
+
* Tracks how much time the tab was actually in the foreground.
|
|
5740
|
+
* Writes only to runtimeState — never triggers session end or timing bugs.
|
|
5741
|
+
* active_time_ms is included in every session heartbeat for backend persistence.
|
|
5742
|
+
*/
|
|
5743
|
+
startTabVisibilityTracking() {
|
|
5744
|
+
try {
|
|
5745
|
+
// Initialise: tab starts visible
|
|
5746
|
+
runtimeState.tabFocusTimestamp = document.hidden ? null : Date.now();
|
|
5747
|
+
|
|
5748
|
+
document.addEventListener('visibilitychange', () => {
|
|
5749
|
+
if (document.hidden) {
|
|
5750
|
+
// Tab went to background — accumulate time since last focus
|
|
5751
|
+
if (runtimeState.tabFocusTimestamp !== null) {
|
|
5752
|
+
runtimeState.activeTimeMs += Date.now() - runtimeState.tabFocusTimestamp;
|
|
5753
|
+
runtimeState.tabFocusTimestamp = null;
|
|
5754
|
+
}
|
|
5755
|
+
} else {
|
|
5756
|
+
// Tab came back to foreground
|
|
5757
|
+
runtimeState.tabFocusTimestamp = Date.now();
|
|
5758
|
+
}
|
|
5759
|
+
});
|
|
5760
|
+
} catch (error) {
|
|
5761
|
+
}
|
|
5762
|
+
},
|
|
5763
|
+
|
|
5534
5764
|
/**
|
|
5535
5765
|
* Start session tracking interval
|
|
5536
|
-
*
|
|
5766
|
+
*
|
|
5537
5767
|
* Periodically sends session data and updates session metrics
|
|
5538
5768
|
*/
|
|
5539
5769
|
startSessionTracking() {
|
|
@@ -5611,6 +5841,11 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5611
5841
|
// Always sync distinctId from storage before sending to ensure consistency
|
|
5612
5842
|
sessionData.distinctId = StorageManager.getDistinctId();
|
|
5613
5843
|
|
|
5844
|
+
// Include accumulated active time (tab foreground time only)
|
|
5845
|
+
const currentActiveMs = runtimeState.activeTimeMs +
|
|
5846
|
+
(runtimeState.tabFocusTimestamp !== null ? Date.now() - runtimeState.tabFocusTimestamp : 0);
|
|
5847
|
+
sessionData.active_time_ms = currentActiveMs;
|
|
5848
|
+
|
|
5614
5849
|
// Send session data
|
|
5615
5850
|
await APIClient.sendSessionData();
|
|
5616
5851
|
} catch (error) {
|
|
@@ -5778,6 +6013,9 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5778
6013
|
// Start event trackers
|
|
5779
6014
|
EventTrackers.initialize();
|
|
5780
6015
|
|
|
6016
|
+
// Start tab visibility accumulator (active_time_ms)
|
|
6017
|
+
this.startTabVisibilityTracking();
|
|
6018
|
+
|
|
5781
6019
|
// Start session tracking interval
|
|
5782
6020
|
this.startSessionTracking();
|
|
5783
6021
|
|
|
@@ -6136,6 +6374,9 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
6136
6374
|
'Content-Type': 'application/json',
|
|
6137
6375
|
'X-Cryptique-Site-Id': getCurrentSiteId()
|
|
6138
6376
|
},
|
|
6377
|
+
// keepalive: true allows this request to outlive the page (critical for
|
|
6378
|
+
// form_abandoned events fired in beforeunload handlers)
|
|
6379
|
+
keepalive: true,
|
|
6139
6380
|
body: JSON.stringify(eventData)
|
|
6140
6381
|
});
|
|
6141
6382
|
|
|
@@ -6160,22 +6401,33 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
6160
6401
|
*/
|
|
6161
6402
|
_getAutoEventCategory(eventName) {
|
|
6162
6403
|
const categoryMap = {
|
|
6404
|
+
// Navigation
|
|
6163
6405
|
'page_view': 'navigation',
|
|
6406
|
+
// Interaction
|
|
6164
6407
|
'page_scroll': 'interaction',
|
|
6165
6408
|
'element_click': 'interaction',
|
|
6166
6409
|
'element_hover': 'interaction',
|
|
6410
|
+
'dead_click': 'interaction',
|
|
6411
|
+
'rage_click': 'interaction',
|
|
6412
|
+
'text_selection': 'interaction',
|
|
6413
|
+
// Form
|
|
6167
6414
|
'form_submit': 'form',
|
|
6168
|
-
'
|
|
6169
|
-
'
|
|
6170
|
-
|
|
6415
|
+
'form_validation_error': 'form',
|
|
6416
|
+
'form_abandoned': 'form',
|
|
6417
|
+
// Media
|
|
6171
6418
|
'media_play': 'media',
|
|
6172
6419
|
'media_pause': 'media',
|
|
6173
6420
|
'media_ended': 'media',
|
|
6174
|
-
|
|
6175
|
-
'rage_click': 'interaction',
|
|
6176
|
-
'text_selection': 'interaction',
|
|
6421
|
+
// Session
|
|
6177
6422
|
'session_start': 'session',
|
|
6178
|
-
'session_end': 'session'
|
|
6423
|
+
'session_end': 'session',
|
|
6424
|
+
// Error
|
|
6425
|
+
'js_error': 'error',
|
|
6426
|
+
'network_error': 'error',
|
|
6427
|
+
// Performance
|
|
6428
|
+
'page_performance': 'performance',
|
|
6429
|
+
// Visibility
|
|
6430
|
+
'element_view': 'visibility'
|
|
6179
6431
|
};
|
|
6180
6432
|
|
|
6181
6433
|
return categoryMap[eventName] || 'other';
|
|
@@ -6968,50 +7220,26 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
6968
7220
|
}
|
|
6969
7221
|
});
|
|
6970
7222
|
|
|
6971
|
-
//
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA' || element.tagName === 'SELECT') {
|
|
6975
|
-
const scrollX = window.scrollX != null ? window.scrollX : window.pageXOffset;
|
|
6976
|
-
const scrollY = window.scrollY != null ? window.scrollY : window.pageYOffset;
|
|
6977
|
-
const docHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
|
|
6978
|
-
const docWidth = Math.max(document.body.scrollWidth, document.documentElement.scrollWidth);
|
|
6979
|
-
const rect = element.getBoundingClientRect();
|
|
6980
|
-
const pageX = rect.left + scrollX;
|
|
6981
|
-
const pageY = rect.top + scrollY;
|
|
6982
|
-
EventsManager.trackAutoEvent('form_focus', {
|
|
6983
|
-
field_name: element.name || null,
|
|
6984
|
-
field_type: element.type || null,
|
|
6985
|
-
field_id: element.id || null,
|
|
6986
|
-
scroll_x: scrollX,
|
|
6987
|
-
scroll_y: scrollY,
|
|
6988
|
-
document_height: docHeight,
|
|
6989
|
-
document_width: docWidth,
|
|
6990
|
-
page_x: pageX,
|
|
6991
|
-
page_y: pageY,
|
|
6992
|
-
click_coordinates: { x: event.clientX != null ? event.clientX : rect.left + rect.width / 2, y: event.clientY != null ? event.clientY : rect.top + rect.height / 2 }
|
|
6993
|
-
}, {
|
|
6994
|
-
element_tag_name: element.tagName.toLowerCase(),
|
|
6995
|
-
element_id: element.id || null,
|
|
6996
|
-
element_name: element.name || element.getAttribute('name') || null, // FIX: Added element_name
|
|
6997
|
-
element_class: element.className || null, // FIX: Added element_class
|
|
6998
|
-
element_type: element.type || null,
|
|
6999
|
-
element_text: element.value ? element.value.toString().trim().substring(0, 100) : null, // FIX: Added element_text (value for form fields)
|
|
7000
|
-
element_position: { // FIX: Added element_position
|
|
7001
|
-
x: event.clientX != null ? event.clientX : rect.left,
|
|
7002
|
-
y: event.clientY != null ? event.clientY : rect.top,
|
|
7003
|
-
width: element.offsetWidth || 0,
|
|
7004
|
-
height: element.offsetHeight || 0
|
|
7005
|
-
}
|
|
7006
|
-
});
|
|
7007
|
-
}
|
|
7008
|
-
}, true);
|
|
7223
|
+
// form_focus, form_blur, form_change are intentionally NOT fired as auto events.
|
|
7224
|
+
// form_abandoned already captures the meaningful signal (which field, how many filled,
|
|
7225
|
+
// time spent) without the per-keystroke noise these would generate.
|
|
7009
7226
|
},
|
|
7010
7227
|
|
|
7011
7228
|
/**
|
|
7012
7229
|
* Setup media tracking
|
|
7230
|
+
* play/pause events are throttled per element (2s cooldown) to filter out
|
|
7231
|
+
* scrubbing noise — a user seeking through a video fires rapid play/pause
|
|
7232
|
+
* pairs that have no PM value.
|
|
7013
7233
|
*/
|
|
7014
7234
|
setupMediaTracking() {
|
|
7235
|
+
// Per-element last-event timestamps — keyed by src or generated index
|
|
7236
|
+
const lastMediaEventTime = new Map();
|
|
7237
|
+
const MEDIA_THROTTLE_MS = 2000;
|
|
7238
|
+
|
|
7239
|
+
function getMediaKey(el) {
|
|
7240
|
+
return el.src || el.id || el.currentSrc || [...document.querySelectorAll('video,audio')].indexOf(el).toString();
|
|
7241
|
+
}
|
|
7242
|
+
|
|
7015
7243
|
function getPageContext() {
|
|
7016
7244
|
const scrollX = window.scrollX != null ? window.scrollX : window.pageXOffset;
|
|
7017
7245
|
const scrollY = window.scrollY != null ? window.scrollY : window.pageYOffset;
|
|
@@ -7019,49 +7247,56 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
7019
7247
|
const docWidth = Math.max(document.body.scrollWidth, document.documentElement.scrollWidth);
|
|
7020
7248
|
return { scrollX, scrollY, docHeight, docWidth };
|
|
7021
7249
|
}
|
|
7250
|
+
|
|
7022
7251
|
['video', 'audio'].forEach(mediaType => {
|
|
7023
7252
|
document.addEventListener('play', (event) => {
|
|
7024
|
-
if (event.target.tagName.toLowerCase()
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7253
|
+
if (event.target.tagName.toLowerCase() !== mediaType) return;
|
|
7254
|
+
const el = event.target;
|
|
7255
|
+
const key = `play:${getMediaKey(el)}`;
|
|
7256
|
+
const now = Date.now();
|
|
7257
|
+
if (now - (lastMediaEventTime.get(key) || 0) < MEDIA_THROTTLE_MS) return;
|
|
7258
|
+
lastMediaEventTime.set(key, now);
|
|
7259
|
+
|
|
7260
|
+
const { scrollX, scrollY, docHeight, docWidth } = getPageContext();
|
|
7261
|
+
const rect = el.getBoundingClientRect();
|
|
7262
|
+
EventsManager.trackAutoEvent('media_play', {
|
|
7263
|
+
media_type: mediaType,
|
|
7264
|
+
media_src: el.src || null,
|
|
7265
|
+
media_duration: el.duration || null,
|
|
7266
|
+
media_current_time: el.currentTime || null,
|
|
7267
|
+
scroll_x: scrollX,
|
|
7268
|
+
scroll_y: scrollY,
|
|
7269
|
+
document_height: docHeight,
|
|
7270
|
+
document_width: docWidth,
|
|
7271
|
+
page_x: rect.left + scrollX,
|
|
7272
|
+
page_y: rect.top + scrollY,
|
|
7273
|
+
click_coordinates: { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }
|
|
7274
|
+
});
|
|
7043
7275
|
}, true);
|
|
7044
7276
|
|
|
7045
7277
|
document.addEventListener('pause', (event) => {
|
|
7046
|
-
if (event.target.tagName.toLowerCase()
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7278
|
+
if (event.target.tagName.toLowerCase() !== mediaType) return;
|
|
7279
|
+
const el = event.target;
|
|
7280
|
+
const key = `pause:${getMediaKey(el)}`;
|
|
7281
|
+
const now = Date.now();
|
|
7282
|
+
if (now - (lastMediaEventTime.get(key) || 0) < MEDIA_THROTTLE_MS) return;
|
|
7283
|
+
lastMediaEventTime.set(key, now);
|
|
7284
|
+
|
|
7285
|
+
const { scrollX, scrollY, docHeight, docWidth } = getPageContext();
|
|
7286
|
+
const rect = el.getBoundingClientRect();
|
|
7287
|
+
EventsManager.trackAutoEvent('media_pause', {
|
|
7288
|
+
media_type: mediaType,
|
|
7289
|
+
media_src: el.src || null,
|
|
7290
|
+
media_current_time: el.currentTime || null,
|
|
7291
|
+
media_duration: el.duration || null,
|
|
7292
|
+
scroll_x: scrollX,
|
|
7293
|
+
scroll_y: scrollY,
|
|
7294
|
+
document_height: docHeight,
|
|
7295
|
+
document_width: docWidth,
|
|
7296
|
+
page_x: rect.left + scrollX,
|
|
7297
|
+
page_y: rect.top + scrollY,
|
|
7298
|
+
click_coordinates: { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }
|
|
7299
|
+
});
|
|
7065
7300
|
}, true);
|
|
7066
7301
|
});
|
|
7067
7302
|
},
|
|
@@ -7088,12 +7323,14 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
7088
7323
|
|
|
7089
7324
|
const selectedText = selection.toString().trim();
|
|
7090
7325
|
|
|
7091
|
-
// Only track if
|
|
7092
|
-
|
|
7326
|
+
// Only track if selection is meaningful (≥3 chars) and different from last selection.
|
|
7327
|
+
// <3 chars filters out accidental double-clicks and single-word mis-selections.
|
|
7328
|
+
if (selectedText && selectedText.length >= 3 && selectedText !== lastSelectionText) {
|
|
7093
7329
|
const now = Date.now();
|
|
7094
|
-
|
|
7095
|
-
//
|
|
7096
|
-
|
|
7330
|
+
|
|
7331
|
+
// 1s cooldown between events — prevents rapid re-selection noise
|
|
7332
|
+
// (e.g. user adjusting selection handles fires many selectionchange events)
|
|
7333
|
+
if (now - lastSelectionTime < 1000) {
|
|
7097
7334
|
return;
|
|
7098
7335
|
}
|
|
7099
7336
|
|