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