cryptique-sdk 1.2.15 → 1.2.17
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 +594 -176
- package/lib/esm/index.js +594 -176
- package/lib/umd/index.js +594 -176
- package/package.json +1 -1
package/lib/cjs/index.js
CHANGED
|
@@ -92,8 +92,8 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
92
92
|
|
|
93
93
|
// Geolocation API
|
|
94
94
|
GEOLOCATION: {
|
|
95
|
-
PRIMARY_URL: "https://ipinfo.io/json?token=
|
|
96
|
-
BACKUP_URL: "https://ipinfo.io/json?token=
|
|
95
|
+
PRIMARY_URL: "https://ipinfo.io/json?token=8fc6409059aa39",
|
|
96
|
+
BACKUP_URL: "https://ipinfo.io/json?token=8fc6409059aa39&http=1.1",
|
|
97
97
|
TIMEOUT_MS: 5000 // 5 second timeout
|
|
98
98
|
},
|
|
99
99
|
|
|
@@ -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) {
|
|
@@ -4980,19 +4999,26 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
4980
4999
|
startCopyPasteTracking() {
|
|
4981
5000
|
try {
|
|
4982
5001
|
document.addEventListener('copy', (event) => {
|
|
5002
|
+
const sel = window.getSelection();
|
|
4983
5003
|
const copyData = {
|
|
4984
5004
|
type: 'copy_action',
|
|
4985
|
-
selectedText:
|
|
5005
|
+
selectedText: sel ? sel.toString().substring(0, 100) : '',
|
|
4986
5006
|
target: {
|
|
4987
5007
|
tagName: event.target?.tagName || '',
|
|
4988
5008
|
id: event.target?.id || ''
|
|
4989
5009
|
},
|
|
4990
5010
|
path: window.location.pathname
|
|
4991
5011
|
};
|
|
4992
|
-
|
|
4993
5012
|
InteractionManager.add('copyPasteEvents', copyData);
|
|
5013
|
+
// Fire structured auto-event (low volume — only when user explicitly copies)
|
|
5014
|
+
EventsManager.trackAutoEvent('copy_action', {
|
|
5015
|
+
text_length: sel ? sel.toString().length : 0,
|
|
5016
|
+
element_id: event.target?.id || '',
|
|
5017
|
+
element_tag: event.target?.tagName?.toLowerCase() || '',
|
|
5018
|
+
element_class: (event.target?.className || '').split(' ').filter(Boolean).slice(0, 3).join(' '),
|
|
5019
|
+
});
|
|
4994
5020
|
});
|
|
4995
|
-
|
|
5021
|
+
|
|
4996
5022
|
document.addEventListener('paste', (event) => {
|
|
4997
5023
|
const pasteData = {
|
|
4998
5024
|
type: 'paste_action',
|
|
@@ -5002,7 +5028,6 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5002
5028
|
},
|
|
5003
5029
|
path: window.location.pathname
|
|
5004
5030
|
};
|
|
5005
|
-
|
|
5006
5031
|
InteractionManager.add('copyPasteEvents', pasteData);
|
|
5007
5032
|
});
|
|
5008
5033
|
} catch (error) {
|
|
@@ -5028,8 +5053,15 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5028
5053
|
},
|
|
5029
5054
|
path: window.location.pathname
|
|
5030
5055
|
};
|
|
5031
|
-
|
|
5032
5056
|
InteractionManager.add('contextMenuEvents', contextData);
|
|
5057
|
+
// Fire structured auto-event (low volume — only when user right-clicks)
|
|
5058
|
+
EventsManager.trackAutoEvent('context_menu', {
|
|
5059
|
+
element_tag: event.target?.tagName?.toLowerCase() || '',
|
|
5060
|
+
element_id: event.target?.id || '',
|
|
5061
|
+
element_class: (event.target?.className || '').split(' ').filter(Boolean).slice(0, 3).join(' '),
|
|
5062
|
+
page_x: event.pageX,
|
|
5063
|
+
page_y: event.pageY,
|
|
5064
|
+
});
|
|
5033
5065
|
});
|
|
5034
5066
|
} catch (error) {
|
|
5035
5067
|
}
|
|
@@ -5208,23 +5240,62 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5208
5240
|
*/
|
|
5209
5241
|
startPerformanceTracking() {
|
|
5210
5242
|
try {
|
|
5243
|
+
// Collect Core Web Vitals via PerformanceObserver + Navigation Timing
|
|
5244
|
+
// Emits a single queryable page_performance auto event per page load
|
|
5245
|
+
const vitals = { lcp: null, cls: 0, inp: null, fcp: null, ttfb: null };
|
|
5246
|
+
|
|
5247
|
+
// LCP
|
|
5248
|
+
try {
|
|
5249
|
+
new PerformanceObserver((list) => {
|
|
5250
|
+
const entries = list.getEntries();
|
|
5251
|
+
if (entries.length > 0) {
|
|
5252
|
+
vitals.lcp = Math.round(entries[entries.length - 1].startTime);
|
|
5253
|
+
}
|
|
5254
|
+
}).observe({ type: 'largest-contentful-paint', buffered: true });
|
|
5255
|
+
} catch (e) {}
|
|
5256
|
+
|
|
5257
|
+
// CLS
|
|
5258
|
+
try {
|
|
5259
|
+
new PerformanceObserver((list) => {
|
|
5260
|
+
for (const entry of list.getEntries()) {
|
|
5261
|
+
if (!entry.hadRecentInput) {
|
|
5262
|
+
vitals.cls = Math.round((vitals.cls + entry.value) * 10000) / 10000;
|
|
5263
|
+
}
|
|
5264
|
+
}
|
|
5265
|
+
}).observe({ type: 'layout-shift', buffered: true });
|
|
5266
|
+
} catch (e) {}
|
|
5267
|
+
|
|
5268
|
+
// INP (interaction to next paint)
|
|
5269
|
+
try {
|
|
5270
|
+
new PerformanceObserver((list) => {
|
|
5271
|
+
for (const entry of list.getEntries()) {
|
|
5272
|
+
if (entry.duration && (vitals.inp === null || entry.duration > vitals.inp)) {
|
|
5273
|
+
vitals.inp = Math.round(entry.duration);
|
|
5274
|
+
}
|
|
5275
|
+
}
|
|
5276
|
+
}).observe({ type: 'event', buffered: true, durationThreshold: 16 });
|
|
5277
|
+
} catch (e) {}
|
|
5278
|
+
|
|
5279
|
+
// FCP + TTFB from paint + navigation entries (available after load)
|
|
5211
5280
|
window.addEventListener('load', () => {
|
|
5212
|
-
// Wait a bit for all resources to load
|
|
5213
5281
|
setTimeout(() => {
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5282
|
+
try {
|
|
5283
|
+
const nav = performance.getEntriesByType('navigation')[0];
|
|
5284
|
+
const paint = performance.getEntriesByType('paint');
|
|
5285
|
+
vitals.ttfb = nav ? Math.round(nav.responseStart - nav.requestStart) : null;
|
|
5286
|
+
vitals.fcp = paint.find(p => p.name === 'first-contentful-paint')
|
|
5287
|
+
? Math.round(paint.find(p => p.name === 'first-contentful-paint').startTime)
|
|
5288
|
+
: null;
|
|
5289
|
+
} catch (e) {}
|
|
5290
|
+
|
|
5291
|
+
EventsManager.trackAutoEvent('page_performance', {
|
|
5292
|
+
lcp: vitals.lcp,
|
|
5293
|
+
cls: vitals.cls,
|
|
5294
|
+
inp: vitals.inp,
|
|
5295
|
+
fcp: vitals.fcp,
|
|
5296
|
+
ttfb: vitals.ttfb
|
|
5297
|
+
});
|
|
5298
|
+
}, 1500);
|
|
5228
5299
|
});
|
|
5229
5300
|
} catch (error) {
|
|
5230
5301
|
}
|
|
@@ -5232,40 +5303,28 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5232
5303
|
|
|
5233
5304
|
/**
|
|
5234
5305
|
* ErrorTracker - Tracks JavaScript errors
|
|
5306
|
+
* Emits as queryable auto events (js_error) instead of session-level JSONB
|
|
5235
5307
|
*/
|
|
5236
5308
|
startErrorTracking() {
|
|
5237
5309
|
try {
|
|
5238
5310
|
window.addEventListener('error', (event) => {
|
|
5239
|
-
|
|
5240
|
-
|
|
5311
|
+
EventsManager.trackAutoEvent('js_error', {
|
|
5312
|
+
error_type: 'javascript_error',
|
|
5241
5313
|
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);
|
|
5314
|
+
source: event.filename || '',
|
|
5315
|
+
line: event.lineno || 0,
|
|
5316
|
+
column: event.colno || 0,
|
|
5317
|
+
stack: event.error?.stack || ''
|
|
5318
|
+
});
|
|
5257
5319
|
});
|
|
5258
|
-
|
|
5320
|
+
|
|
5259
5321
|
// Track unhandled promise rejections
|
|
5260
5322
|
window.addEventListener('unhandledrejection', (event) => {
|
|
5261
|
-
|
|
5262
|
-
|
|
5323
|
+
EventsManager.trackAutoEvent('js_error', {
|
|
5324
|
+
error_type: 'unhandled_promise_rejection',
|
|
5263
5325
|
message: event.reason?.message || String(event.reason || 'Unknown error'),
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
};
|
|
5267
|
-
|
|
5268
|
-
InteractionManager.add('errorEvents', errorData);
|
|
5326
|
+
stack: event.reason?.stack || String(event.reason || '')
|
|
5327
|
+
});
|
|
5269
5328
|
});
|
|
5270
5329
|
} catch (error) {
|
|
5271
5330
|
}
|
|
@@ -5303,32 +5362,24 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5303
5362
|
|
|
5304
5363
|
xhr.addEventListener('load', function() {
|
|
5305
5364
|
if (xhr.status >= 400) {
|
|
5306
|
-
|
|
5307
|
-
type: 'api_error',
|
|
5365
|
+
EventsManager.trackAutoEvent('network_error', {
|
|
5308
5366
|
url: xhr._cryptiqueUrl,
|
|
5309
5367
|
method: xhr._cryptiqueMethod || 'GET',
|
|
5310
5368
|
status: xhr.status,
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
};
|
|
5315
|
-
|
|
5316
|
-
InteractionManager.add('networkEvents', networkErrorData);
|
|
5369
|
+
status_text: `HTTP ${xhr.status} Error`,
|
|
5370
|
+
duration_ms: Date.now() - startTime
|
|
5371
|
+
});
|
|
5317
5372
|
}
|
|
5318
5373
|
});
|
|
5319
|
-
|
|
5374
|
+
|
|
5320
5375
|
xhr.addEventListener('error', function() {
|
|
5321
|
-
|
|
5322
|
-
type: 'api_error',
|
|
5376
|
+
EventsManager.trackAutoEvent('network_error', {
|
|
5323
5377
|
url: xhr._cryptiqueUrl,
|
|
5324
5378
|
method: xhr._cryptiqueMethod || 'GET',
|
|
5325
5379
|
status: 0,
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
};
|
|
5330
|
-
|
|
5331
|
-
InteractionManager.add('networkEvents', networkErrorData);
|
|
5380
|
+
status_text: 'XHR Network Error',
|
|
5381
|
+
duration_ms: Date.now() - startTime
|
|
5382
|
+
});
|
|
5332
5383
|
});
|
|
5333
5384
|
|
|
5334
5385
|
return originalXHRSend.apply(this, args);
|
|
@@ -5343,9 +5394,9 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5343
5394
|
startAdvancedFormTracking() {
|
|
5344
5395
|
try {
|
|
5345
5396
|
const formStartTimes = new Map();
|
|
5346
|
-
|
|
5397
|
+
|
|
5347
5398
|
document.addEventListener('focus', (event) => {
|
|
5348
|
-
if (event.target &&
|
|
5399
|
+
if (event.target &&
|
|
5349
5400
|
(event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA')) {
|
|
5350
5401
|
const formId = event.target.form?.id || 'unknown';
|
|
5351
5402
|
if (!formStartTimes.has(formId)) {
|
|
@@ -5353,11 +5404,11 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5353
5404
|
}
|
|
5354
5405
|
}
|
|
5355
5406
|
});
|
|
5356
|
-
|
|
5407
|
+
|
|
5357
5408
|
document.addEventListener('submit', (event) => {
|
|
5358
5409
|
const formId = event.target.id || 'unknown';
|
|
5359
5410
|
const startTime = formStartTimes.get(formId);
|
|
5360
|
-
|
|
5411
|
+
|
|
5361
5412
|
if (startTime) {
|
|
5362
5413
|
const formCompletionData = {
|
|
5363
5414
|
type: 'form_completion',
|
|
@@ -5366,7 +5417,7 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5366
5417
|
fieldCount: event.target.elements.length,
|
|
5367
5418
|
path: window.location.pathname
|
|
5368
5419
|
};
|
|
5369
|
-
|
|
5420
|
+
|
|
5370
5421
|
InteractionManager.add('formAnalytics', formCompletionData);
|
|
5371
5422
|
formStartTimes.delete(formId);
|
|
5372
5423
|
}
|
|
@@ -5375,9 +5426,333 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5375
5426
|
}
|
|
5376
5427
|
},
|
|
5377
5428
|
|
|
5429
|
+
/**
|
|
5430
|
+
* FormAbandonmentTracker - Emits form_abandoned auto event when a user
|
|
5431
|
+
* interacts with a form but navigates away without submitting.
|
|
5432
|
+
* Tracks which field they stopped at — high-value PM signal.
|
|
5433
|
+
*/
|
|
5434
|
+
startFormAbandonmentTracking() {
|
|
5435
|
+
try {
|
|
5436
|
+
// Per-form state: formId → { startTime, lastFieldId, lastFieldType, filledFields, totalFields, submitted }
|
|
5437
|
+
const formState = new Map();
|
|
5438
|
+
|
|
5439
|
+
const getOrInitForm = (formEl) => {
|
|
5440
|
+
const formId = formEl?.id || formEl?.name || 'form_' + (formEl ? [...document.forms].indexOf(formEl) : 0);
|
|
5441
|
+
if (!formState.has(formId)) {
|
|
5442
|
+
const totalFields = formEl
|
|
5443
|
+
? formEl.querySelectorAll('input:not([type=hidden]), select, textarea').length
|
|
5444
|
+
: 0;
|
|
5445
|
+
formState.set(formId, {
|
|
5446
|
+
formId,
|
|
5447
|
+
startTime: Date.now(),
|
|
5448
|
+
lastFieldId: '',
|
|
5449
|
+
lastFieldType: '',
|
|
5450
|
+
filledFields: new Set(),
|
|
5451
|
+
totalFields,
|
|
5452
|
+
submitted: false
|
|
5453
|
+
});
|
|
5454
|
+
}
|
|
5455
|
+
return formState.get(formId);
|
|
5456
|
+
};
|
|
5457
|
+
|
|
5458
|
+
// Track field focus to know where user is
|
|
5459
|
+
document.addEventListener('focus', (event) => {
|
|
5460
|
+
const target = event.target;
|
|
5461
|
+
if (!target || !['INPUT', 'SELECT', 'TEXTAREA'].includes(target.tagName)) return;
|
|
5462
|
+
if (target.type === 'hidden') return;
|
|
5463
|
+
const state = getOrInitForm(target.form);
|
|
5464
|
+
state.lastFieldId = target.id || target.name || '';
|
|
5465
|
+
state.lastFieldType = target.type || target.tagName.toLowerCase();
|
|
5466
|
+
}, true);
|
|
5467
|
+
|
|
5468
|
+
// Track field blur to record which fields have been filled
|
|
5469
|
+
document.addEventListener('blur', (event) => {
|
|
5470
|
+
const target = event.target;
|
|
5471
|
+
if (!target || !['INPUT', 'SELECT', 'TEXTAREA'].includes(target.tagName)) return;
|
|
5472
|
+
if (target.type === 'hidden') return;
|
|
5473
|
+
const state = getOrInitForm(target.form);
|
|
5474
|
+
const fieldKey = target.id || target.name || target.type;
|
|
5475
|
+
if (target.value && target.value.length > 0 && fieldKey) {
|
|
5476
|
+
state.filledFields.add(fieldKey);
|
|
5477
|
+
}
|
|
5478
|
+
}, true);
|
|
5479
|
+
|
|
5480
|
+
// Mark form as submitted so we don't fire form_abandoned
|
|
5481
|
+
document.addEventListener('submit', (event) => {
|
|
5482
|
+
const formId = event.target?.id || event.target?.name || 'form_' + [...document.forms].indexOf(event.target);
|
|
5483
|
+
if (formState.has(formId)) {
|
|
5484
|
+
formState.get(formId).submitted = true;
|
|
5485
|
+
}
|
|
5486
|
+
}, true);
|
|
5487
|
+
|
|
5488
|
+
// On page unload, fire form_abandoned for any touched, unsubmitted forms
|
|
5489
|
+
window.addEventListener('beforeunload', () => {
|
|
5490
|
+
formState.forEach((state) => {
|
|
5491
|
+
if (!state.submitted && state.filledFields.size > 0) {
|
|
5492
|
+
// Fire synchronously via sendBeacon path — trackAutoEvent is async
|
|
5493
|
+
// but beforeunload already handles this via sendBeacon in _sendEvent
|
|
5494
|
+
// Resolve the actual form DOM element (if still in DOM)
|
|
5495
|
+
const formEl = state.formId && state.formId !== 'unknown'
|
|
5496
|
+
? document.getElementById(state.formId) || document.querySelector(`form[name="${state.formId}"]`)
|
|
5497
|
+
: null;
|
|
5498
|
+
EventsManager.trackAutoEvent('form_abandoned', {
|
|
5499
|
+
form_id: state.formId,
|
|
5500
|
+
last_active_field_id: state.lastFieldId,
|
|
5501
|
+
last_active_field_type: state.lastFieldType,
|
|
5502
|
+
fields_filled_count: state.filledFields.size,
|
|
5503
|
+
total_fields_count: state.totalFields,
|
|
5504
|
+
time_on_form_ms: Date.now() - state.startTime
|
|
5505
|
+
}, {
|
|
5506
|
+
element_id: formEl?.id || state.formId || '',
|
|
5507
|
+
element_name: formEl?.getAttribute('name') || '',
|
|
5508
|
+
element_tag_name: 'FORM',
|
|
5509
|
+
element_type: '',
|
|
5510
|
+
element_class: formEl?.className || '',
|
|
5511
|
+
element_text: (formEl?.getAttribute('aria-label') || '').slice(0, 100)
|
|
5512
|
+
});
|
|
5513
|
+
}
|
|
5514
|
+
});
|
|
5515
|
+
});
|
|
5516
|
+
} catch (error) {
|
|
5517
|
+
}
|
|
5518
|
+
},
|
|
5519
|
+
|
|
5520
|
+
/**
|
|
5521
|
+
* ElementVisibilityTracker - Emits element_view auto events when elements
|
|
5522
|
+
* with IDs or data-track attributes enter the viewport for ≥ 1 second.
|
|
5523
|
+
* Tells PMs whether users actually SAW key elements (CTAs, pricing, etc.)
|
|
5524
|
+
* vs just didn't click them — completely different problems, opposite fixes.
|
|
5525
|
+
*/
|
|
5526
|
+
startElementVisibilityTracking() {
|
|
5527
|
+
try {
|
|
5528
|
+
if (typeof IntersectionObserver === 'undefined') return;
|
|
5529
|
+
|
|
5530
|
+
// Only observe elements with an id or data-cq-track attribute
|
|
5531
|
+
const getTrackableElements = () =>
|
|
5532
|
+
document.querySelectorAll('[id]:not([id=""]), [data-cq-track]');
|
|
5533
|
+
|
|
5534
|
+
const viewTimers = new Map(); // elementKey → setTimeout handle
|
|
5535
|
+
const reported = new Set(); // elementKey → already fired once per page load
|
|
5536
|
+
|
|
5537
|
+
const observer = new IntersectionObserver((entries) => {
|
|
5538
|
+
entries.forEach((entry) => {
|
|
5539
|
+
const el = entry.target;
|
|
5540
|
+
const key = el.id || el.getAttribute('data-cq-track') || el.className;
|
|
5541
|
+
if (!key || reported.has(key)) return;
|
|
5542
|
+
|
|
5543
|
+
if (entry.isIntersecting && entry.intersectionRatio >= 0.5) {
|
|
5544
|
+
// Element entered viewport — start 1s timer
|
|
5545
|
+
if (!viewTimers.has(key)) {
|
|
5546
|
+
const enterTime = Date.now();
|
|
5547
|
+
const timer = setTimeout(() => {
|
|
5548
|
+
if (reported.has(key)) return;
|
|
5549
|
+
reported.add(key);
|
|
5550
|
+
EventsManager.trackAutoEvent('element_view', {
|
|
5551
|
+
time_in_viewport_ms: Date.now() - enterTime,
|
|
5552
|
+
viewport_percent_visible: Math.round(entry.intersectionRatio * 100)
|
|
5553
|
+
}, {
|
|
5554
|
+
element_id: el.id || '',
|
|
5555
|
+
element_name: el.getAttribute('name') || el.getAttribute('data-cq-track') || '',
|
|
5556
|
+
element_tag_name: el.tagName || '',
|
|
5557
|
+
element_type: el.type || el.getAttribute('type') || '',
|
|
5558
|
+
element_class: el.className || '',
|
|
5559
|
+
element_text: (el.textContent || '').trim().slice(0, 100)
|
|
5560
|
+
});
|
|
5561
|
+
viewTimers.delete(key);
|
|
5562
|
+
}, 1000);
|
|
5563
|
+
viewTimers.set(key, timer);
|
|
5564
|
+
}
|
|
5565
|
+
} else {
|
|
5566
|
+
// Element left viewport before 1s — cancel timer
|
|
5567
|
+
if (viewTimers.has(key)) {
|
|
5568
|
+
clearTimeout(viewTimers.get(key));
|
|
5569
|
+
viewTimers.delete(key);
|
|
5570
|
+
}
|
|
5571
|
+
}
|
|
5572
|
+
});
|
|
5573
|
+
}, { threshold: 0.5 });
|
|
5574
|
+
|
|
5575
|
+
// Observe existing elements
|
|
5576
|
+
getTrackableElements().forEach(el => observer.observe(el));
|
|
5577
|
+
|
|
5578
|
+
// Observe elements added to DOM later (SPAs)
|
|
5579
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
5580
|
+
new MutationObserver(() => {
|
|
5581
|
+
getTrackableElements().forEach(el => {
|
|
5582
|
+
if (!viewTimers.has(el.id || el.getAttribute('data-cq-track') || el.className)) {
|
|
5583
|
+
observer.observe(el);
|
|
5584
|
+
}
|
|
5585
|
+
});
|
|
5586
|
+
}).observe(document.body, { childList: true, subtree: true });
|
|
5587
|
+
}
|
|
5588
|
+
} catch (error) {
|
|
5589
|
+
}
|
|
5590
|
+
},
|
|
5591
|
+
|
|
5592
|
+
/**
|
|
5593
|
+
* PageSummaryTracker — collects high-frequency signals in-memory and flushes
|
|
5594
|
+
* them as a single 'page_summary' auto-event on page unload.
|
|
5595
|
+
*
|
|
5596
|
+
* This keeps event counts low (1 per page visit) while capturing:
|
|
5597
|
+
* - Mouse movement grid (move_grid)
|
|
5598
|
+
* - Hover dwell grid (hover_grid)
|
|
5599
|
+
* - Tab-switch count and hidden time
|
|
5600
|
+
* - Scroll reversal count
|
|
5601
|
+
* - Per-field focus dwell times
|
|
5602
|
+
*/
|
|
5603
|
+
startPageSummaryTracking() {
|
|
5604
|
+
try {
|
|
5605
|
+
const GRID_SIZE = 40;
|
|
5606
|
+
// In-memory accumulators — never sent to the server individually
|
|
5607
|
+
const moveGrid = new Map(); // cellKey → move count
|
|
5608
|
+
const hoverGrid = new Map(); // cellKey → total dwell_ms
|
|
5609
|
+
const fieldDwells = []; // [{field_id, field_label, field_type, dwell_ms, was_filled}]
|
|
5610
|
+
|
|
5611
|
+
let tabSwitches = 0;
|
|
5612
|
+
let totalTabHiddenMs = 0;
|
|
5613
|
+
let tabHiddenTime = null;
|
|
5614
|
+
let scrollReversals = 0;
|
|
5615
|
+
|
|
5616
|
+
// Track last significant scroll position for reversal detection
|
|
5617
|
+
let lastSigScrollY = window.scrollY;
|
|
5618
|
+
let lastSigDir = 0; // 1=down, -1=up
|
|
5619
|
+
|
|
5620
|
+
function getGridCell(pageX, pageY) {
|
|
5621
|
+
const docW = Math.max(1, document.documentElement.scrollWidth || document.body.scrollWidth || window.innerWidth);
|
|
5622
|
+
const docH = Math.max(1, document.documentElement.scrollHeight || document.body.scrollHeight || window.innerHeight);
|
|
5623
|
+
const col = Math.min(GRID_SIZE - 1, Math.max(0, Math.floor((pageX / docW) * GRID_SIZE)));
|
|
5624
|
+
const row = Math.min(GRID_SIZE - 1, Math.max(0, Math.floor((pageY / docH) * GRID_SIZE)));
|
|
5625
|
+
return col * GRID_SIZE + row;
|
|
5626
|
+
}
|
|
5627
|
+
|
|
5628
|
+
// ── Mouse movement → move_grid (throttled to max 1 sample / 100 ms)
|
|
5629
|
+
let moveThrottle = false;
|
|
5630
|
+
document.addEventListener('mousemove', (e) => {
|
|
5631
|
+
if (moveThrottle) return;
|
|
5632
|
+
moveThrottle = true;
|
|
5633
|
+
setTimeout(() => { moveThrottle = false; }, 100);
|
|
5634
|
+
const key = getGridCell(e.pageX, e.pageY);
|
|
5635
|
+
moveGrid.set(key, (moveGrid.get(key) || 0) + 1);
|
|
5636
|
+
}, { passive: true });
|
|
5637
|
+
|
|
5638
|
+
// ── Hover dwell → hover_grid (track all elements, filter < 100 ms)
|
|
5639
|
+
const hoverStarts = new Map(); // element → enterTime
|
|
5640
|
+
document.addEventListener('mouseover', (e) => {
|
|
5641
|
+
if (e.target && e.target !== document.documentElement) {
|
|
5642
|
+
hoverStarts.set(e.target, Date.now());
|
|
5643
|
+
}
|
|
5644
|
+
}, { passive: true });
|
|
5645
|
+
document.addEventListener('mouseout', (e) => {
|
|
5646
|
+
const target = e.target;
|
|
5647
|
+
if (!target || !hoverStarts.has(target)) return;
|
|
5648
|
+
const dwell = Date.now() - hoverStarts.get(target);
|
|
5649
|
+
hoverStarts.delete(target);
|
|
5650
|
+
if (dwell < 100) return; // skip accidental hovers
|
|
5651
|
+
try {
|
|
5652
|
+
const rect = target.getBoundingClientRect();
|
|
5653
|
+
const cx = rect.left + rect.width / 2 + window.scrollX;
|
|
5654
|
+
const cy = rect.top + rect.height / 2 + window.scrollY;
|
|
5655
|
+
const key = getGridCell(cx, cy);
|
|
5656
|
+
hoverGrid.set(key, (hoverGrid.get(key) || 0) + dwell);
|
|
5657
|
+
} catch (_) {}
|
|
5658
|
+
}, { passive: true });
|
|
5659
|
+
|
|
5660
|
+
// ── Tab visibility → tab_switches + total_tab_hidden_ms
|
|
5661
|
+
document.addEventListener('visibilitychange', () => {
|
|
5662
|
+
if (document.hidden) {
|
|
5663
|
+
tabHiddenTime = Date.now();
|
|
5664
|
+
tabSwitches++;
|
|
5665
|
+
} else if (tabHiddenTime !== null) {
|
|
5666
|
+
totalTabHiddenMs += Date.now() - tabHiddenTime;
|
|
5667
|
+
tabHiddenTime = null;
|
|
5668
|
+
}
|
|
5669
|
+
});
|
|
5670
|
+
|
|
5671
|
+
// ── Scroll reversal detection (direction change > 200 px)
|
|
5672
|
+
window.addEventListener('scroll', () => {
|
|
5673
|
+
const curr = window.scrollY;
|
|
5674
|
+
const diff = curr - lastSigScrollY;
|
|
5675
|
+
if (Math.abs(diff) < 50) return; // ignore tiny movements
|
|
5676
|
+
const dir = diff > 0 ? 1 : -1;
|
|
5677
|
+
if (lastSigDir !== 0 && dir !== lastSigDir && Math.abs(curr - lastSigScrollY) >= 200) {
|
|
5678
|
+
scrollReversals++;
|
|
5679
|
+
}
|
|
5680
|
+
if (Math.abs(diff) >= 50) { lastSigDir = dir; lastSigScrollY = curr; }
|
|
5681
|
+
}, { passive: true });
|
|
5682
|
+
|
|
5683
|
+
// ── Field dwell → per-field focus duration (no content captured)
|
|
5684
|
+
const fieldFocusTimes = new Map();
|
|
5685
|
+
document.addEventListener('focus', (e) => {
|
|
5686
|
+
const t = e.target;
|
|
5687
|
+
if (!t || !['INPUT', 'SELECT', 'TEXTAREA'].includes(t.tagName)) return;
|
|
5688
|
+
if (t.type === 'hidden') return;
|
|
5689
|
+
fieldFocusTimes.set(t, Date.now());
|
|
5690
|
+
}, true);
|
|
5691
|
+
document.addEventListener('blur', (e) => {
|
|
5692
|
+
const t = e.target;
|
|
5693
|
+
if (!t || !fieldFocusTimes.has(t)) return;
|
|
5694
|
+
const dwell = Date.now() - fieldFocusTimes.get(t);
|
|
5695
|
+
fieldFocusTimes.delete(t);
|
|
5696
|
+
if (dwell < 100) return; // skip accidental focus
|
|
5697
|
+
const label = t.id
|
|
5698
|
+
? (document.querySelector(`label[for="${CSS.escape(t.id)}"]`)?.textContent?.trim() || t.placeholder || t.name || t.id)
|
|
5699
|
+
: (t.placeholder || t.name || t.type || '');
|
|
5700
|
+
fieldDwells.push({
|
|
5701
|
+
field_id: (t.id || t.name || t.type || '').substring(0, 100),
|
|
5702
|
+
field_label: (label || '').substring(0, 100),
|
|
5703
|
+
field_type: t.type || t.tagName.toLowerCase(),
|
|
5704
|
+
dwell_ms: dwell,
|
|
5705
|
+
was_filled: !!(t.value && t.value.length > 0),
|
|
5706
|
+
});
|
|
5707
|
+
}, true);
|
|
5708
|
+
|
|
5709
|
+
// ── Serialise grid Map to [[col, row, value], ...] sparse array
|
|
5710
|
+
function serializeGrid(grid) {
|
|
5711
|
+
const out = [];
|
|
5712
|
+
grid.forEach((val, key) => {
|
|
5713
|
+
if (val > 0) {
|
|
5714
|
+
const col = Math.floor(key / GRID_SIZE);
|
|
5715
|
+
const row = key % GRID_SIZE;
|
|
5716
|
+
out.push([col, row, Math.round(val)]);
|
|
5717
|
+
}
|
|
5718
|
+
});
|
|
5719
|
+
return out;
|
|
5720
|
+
}
|
|
5721
|
+
|
|
5722
|
+
let summarySent = false;
|
|
5723
|
+
function firePageSummary() {
|
|
5724
|
+
if (summarySent) return; // only fire once per page lifecycle
|
|
5725
|
+
if (moveGrid.size === 0 && hoverGrid.size === 0 && fieldDwells.length === 0
|
|
5726
|
+
&& tabSwitches === 0 && scrollReversals === 0) return;
|
|
5727
|
+
summarySent = true;
|
|
5728
|
+
try {
|
|
5729
|
+
EventsManager.trackAutoEvent('page_summary', {
|
|
5730
|
+
scroll_reversals: scrollReversals,
|
|
5731
|
+
tab_switches: tabSwitches,
|
|
5732
|
+
total_tab_hidden_ms: totalTabHiddenMs,
|
|
5733
|
+
move_grid: serializeGrid(moveGrid),
|
|
5734
|
+
hover_grid: serializeGrid(hoverGrid),
|
|
5735
|
+
field_dwells: fieldDwells,
|
|
5736
|
+
document_height: document.documentElement.scrollHeight || document.body.scrollHeight || 0,
|
|
5737
|
+
document_width: document.documentElement.scrollWidth || document.body.scrollWidth || 0,
|
|
5738
|
+
viewport_width: window.innerWidth,
|
|
5739
|
+
viewport_height: window.innerHeight,
|
|
5740
|
+
});
|
|
5741
|
+
} catch (_) {}
|
|
5742
|
+
}
|
|
5743
|
+
|
|
5744
|
+
// Fire on hard navigation / tab close (pagehide is more reliable than beforeunload)
|
|
5745
|
+
window.addEventListener('pagehide', firePageSummary);
|
|
5746
|
+
// Also fire when tab becomes hidden (covers SPA navigation that doesn't fire pagehide)
|
|
5747
|
+
document.addEventListener('visibilitychange', () => {
|
|
5748
|
+
if (document.hidden) firePageSummary();
|
|
5749
|
+
});
|
|
5750
|
+
} catch (error) {}
|
|
5751
|
+
},
|
|
5752
|
+
|
|
5378
5753
|
/**
|
|
5379
5754
|
* Initialize all event trackers
|
|
5380
|
-
*
|
|
5755
|
+
*
|
|
5381
5756
|
* Starts all tracking modules
|
|
5382
5757
|
*/
|
|
5383
5758
|
initialize() {
|
|
@@ -5396,6 +5771,9 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5396
5771
|
this.startErrorTracking();
|
|
5397
5772
|
this.startNetworkTracking();
|
|
5398
5773
|
this.startAdvancedFormTracking();
|
|
5774
|
+
this.startFormAbandonmentTracking();
|
|
5775
|
+
this.startElementVisibilityTracking();
|
|
5776
|
+
this.startPageSummaryTracking();
|
|
5399
5777
|
}
|
|
5400
5778
|
};
|
|
5401
5779
|
|
|
@@ -5531,9 +5909,36 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5531
5909
|
}
|
|
5532
5910
|
},
|
|
5533
5911
|
|
|
5912
|
+
/**
|
|
5913
|
+
* Tab Visibility Accumulator
|
|
5914
|
+
* Tracks how much time the tab was actually in the foreground.
|
|
5915
|
+
* Writes only to runtimeState — never triggers session end or timing bugs.
|
|
5916
|
+
* active_time_ms is included in every session heartbeat for backend persistence.
|
|
5917
|
+
*/
|
|
5918
|
+
startTabVisibilityTracking() {
|
|
5919
|
+
try {
|
|
5920
|
+
// Initialise: tab starts visible
|
|
5921
|
+
runtimeState.tabFocusTimestamp = document.hidden ? null : Date.now();
|
|
5922
|
+
|
|
5923
|
+
document.addEventListener('visibilitychange', () => {
|
|
5924
|
+
if (document.hidden) {
|
|
5925
|
+
// Tab went to background — accumulate time since last focus
|
|
5926
|
+
if (runtimeState.tabFocusTimestamp !== null) {
|
|
5927
|
+
runtimeState.activeTimeMs += Date.now() - runtimeState.tabFocusTimestamp;
|
|
5928
|
+
runtimeState.tabFocusTimestamp = null;
|
|
5929
|
+
}
|
|
5930
|
+
} else {
|
|
5931
|
+
// Tab came back to foreground
|
|
5932
|
+
runtimeState.tabFocusTimestamp = Date.now();
|
|
5933
|
+
}
|
|
5934
|
+
});
|
|
5935
|
+
} catch (error) {
|
|
5936
|
+
}
|
|
5937
|
+
},
|
|
5938
|
+
|
|
5534
5939
|
/**
|
|
5535
5940
|
* Start session tracking interval
|
|
5536
|
-
*
|
|
5941
|
+
*
|
|
5537
5942
|
* Periodically sends session data and updates session metrics
|
|
5538
5943
|
*/
|
|
5539
5944
|
startSessionTracking() {
|
|
@@ -5611,6 +6016,11 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5611
6016
|
// Always sync distinctId from storage before sending to ensure consistency
|
|
5612
6017
|
sessionData.distinctId = StorageManager.getDistinctId();
|
|
5613
6018
|
|
|
6019
|
+
// Include accumulated active time (tab foreground time only)
|
|
6020
|
+
const currentActiveMs = runtimeState.activeTimeMs +
|
|
6021
|
+
(runtimeState.tabFocusTimestamp !== null ? Date.now() - runtimeState.tabFocusTimestamp : 0);
|
|
6022
|
+
sessionData.active_time_ms = currentActiveMs;
|
|
6023
|
+
|
|
5614
6024
|
// Send session data
|
|
5615
6025
|
await APIClient.sendSessionData();
|
|
5616
6026
|
} catch (error) {
|
|
@@ -5778,6 +6188,9 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
5778
6188
|
// Start event trackers
|
|
5779
6189
|
EventTrackers.initialize();
|
|
5780
6190
|
|
|
6191
|
+
// Start tab visibility accumulator (active_time_ms)
|
|
6192
|
+
this.startTabVisibilityTracking();
|
|
6193
|
+
|
|
5781
6194
|
// Start session tracking interval
|
|
5782
6195
|
this.startSessionTracking();
|
|
5783
6196
|
|
|
@@ -6136,6 +6549,9 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
6136
6549
|
'Content-Type': 'application/json',
|
|
6137
6550
|
'X-Cryptique-Site-Id': getCurrentSiteId()
|
|
6138
6551
|
},
|
|
6552
|
+
// keepalive: true allows this request to outlive the page (critical for
|
|
6553
|
+
// form_abandoned events fired in beforeunload handlers)
|
|
6554
|
+
keepalive: true,
|
|
6139
6555
|
body: JSON.stringify(eventData)
|
|
6140
6556
|
});
|
|
6141
6557
|
|
|
@@ -6160,22 +6576,38 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
6160
6576
|
*/
|
|
6161
6577
|
_getAutoEventCategory(eventName) {
|
|
6162
6578
|
const categoryMap = {
|
|
6579
|
+
// Navigation
|
|
6163
6580
|
'page_view': 'navigation',
|
|
6581
|
+
// Interaction
|
|
6164
6582
|
'page_scroll': 'interaction',
|
|
6165
6583
|
'element_click': 'interaction',
|
|
6166
6584
|
'element_hover': 'interaction',
|
|
6585
|
+
'dead_click': 'interaction',
|
|
6586
|
+
'rage_click': 'interaction',
|
|
6587
|
+
'text_selection': 'interaction',
|
|
6588
|
+
// Form
|
|
6167
6589
|
'form_submit': 'form',
|
|
6168
|
-
'
|
|
6169
|
-
'
|
|
6170
|
-
|
|
6590
|
+
'form_validation_error': 'form',
|
|
6591
|
+
'form_abandoned': 'form',
|
|
6592
|
+
// Media
|
|
6171
6593
|
'media_play': 'media',
|
|
6172
6594
|
'media_pause': 'media',
|
|
6173
6595
|
'media_ended': 'media',
|
|
6174
|
-
|
|
6175
|
-
'rage_click': 'interaction',
|
|
6176
|
-
'text_selection': 'interaction',
|
|
6596
|
+
// Session
|
|
6177
6597
|
'session_start': 'session',
|
|
6178
|
-
'session_end': 'session'
|
|
6598
|
+
'session_end': 'session',
|
|
6599
|
+
// Error
|
|
6600
|
+
'js_error': 'error',
|
|
6601
|
+
'network_error': 'error',
|
|
6602
|
+
// Performance
|
|
6603
|
+
'page_performance': 'performance',
|
|
6604
|
+
// Visibility
|
|
6605
|
+
'element_view': 'visibility',
|
|
6606
|
+
// Page summary (aggregate)
|
|
6607
|
+
'page_summary': 'interaction',
|
|
6608
|
+
// Clipboard & context
|
|
6609
|
+
'copy_action': 'interaction',
|
|
6610
|
+
'context_menu': 'interaction',
|
|
6179
6611
|
};
|
|
6180
6612
|
|
|
6181
6613
|
return categoryMap[eventName] || 'other';
|
|
@@ -6968,50 +7400,26 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
6968
7400
|
}
|
|
6969
7401
|
});
|
|
6970
7402
|
|
|
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);
|
|
7403
|
+
// form_focus, form_blur, form_change are intentionally NOT fired as auto events.
|
|
7404
|
+
// form_abandoned already captures the meaningful signal (which field, how many filled,
|
|
7405
|
+
// time spent) without the per-keystroke noise these would generate.
|
|
7009
7406
|
},
|
|
7010
7407
|
|
|
7011
7408
|
/**
|
|
7012
7409
|
* Setup media tracking
|
|
7410
|
+
* play/pause events are throttled per element (2s cooldown) to filter out
|
|
7411
|
+
* scrubbing noise — a user seeking through a video fires rapid play/pause
|
|
7412
|
+
* pairs that have no PM value.
|
|
7013
7413
|
*/
|
|
7014
7414
|
setupMediaTracking() {
|
|
7415
|
+
// Per-element last-event timestamps — keyed by src or generated index
|
|
7416
|
+
const lastMediaEventTime = new Map();
|
|
7417
|
+
const MEDIA_THROTTLE_MS = 2000;
|
|
7418
|
+
|
|
7419
|
+
function getMediaKey(el) {
|
|
7420
|
+
return el.src || el.id || el.currentSrc || [...document.querySelectorAll('video,audio')].indexOf(el).toString();
|
|
7421
|
+
}
|
|
7422
|
+
|
|
7015
7423
|
function getPageContext() {
|
|
7016
7424
|
const scrollX = window.scrollX != null ? window.scrollX : window.pageXOffset;
|
|
7017
7425
|
const scrollY = window.scrollY != null ? window.scrollY : window.pageYOffset;
|
|
@@ -7019,49 +7427,56 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
7019
7427
|
const docWidth = Math.max(document.body.scrollWidth, document.documentElement.scrollWidth);
|
|
7020
7428
|
return { scrollX, scrollY, docHeight, docWidth };
|
|
7021
7429
|
}
|
|
7430
|
+
|
|
7022
7431
|
['video', 'audio'].forEach(mediaType => {
|
|
7023
7432
|
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
|
-
|
|
7433
|
+
if (event.target.tagName.toLowerCase() !== mediaType) return;
|
|
7434
|
+
const el = event.target;
|
|
7435
|
+
const key = `play:${getMediaKey(el)}`;
|
|
7436
|
+
const now = Date.now();
|
|
7437
|
+
if (now - (lastMediaEventTime.get(key) || 0) < MEDIA_THROTTLE_MS) return;
|
|
7438
|
+
lastMediaEventTime.set(key, now);
|
|
7439
|
+
|
|
7440
|
+
const { scrollX, scrollY, docHeight, docWidth } = getPageContext();
|
|
7441
|
+
const rect = el.getBoundingClientRect();
|
|
7442
|
+
EventsManager.trackAutoEvent('media_play', {
|
|
7443
|
+
media_type: mediaType,
|
|
7444
|
+
media_src: el.src || null,
|
|
7445
|
+
media_duration: el.duration || null,
|
|
7446
|
+
media_current_time: el.currentTime || null,
|
|
7447
|
+
scroll_x: scrollX,
|
|
7448
|
+
scroll_y: scrollY,
|
|
7449
|
+
document_height: docHeight,
|
|
7450
|
+
document_width: docWidth,
|
|
7451
|
+
page_x: rect.left + scrollX,
|
|
7452
|
+
page_y: rect.top + scrollY,
|
|
7453
|
+
click_coordinates: { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }
|
|
7454
|
+
});
|
|
7043
7455
|
}, true);
|
|
7044
7456
|
|
|
7045
7457
|
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
|
-
|
|
7458
|
+
if (event.target.tagName.toLowerCase() !== mediaType) return;
|
|
7459
|
+
const el = event.target;
|
|
7460
|
+
const key = `pause:${getMediaKey(el)}`;
|
|
7461
|
+
const now = Date.now();
|
|
7462
|
+
if (now - (lastMediaEventTime.get(key) || 0) < MEDIA_THROTTLE_MS) return;
|
|
7463
|
+
lastMediaEventTime.set(key, now);
|
|
7464
|
+
|
|
7465
|
+
const { scrollX, scrollY, docHeight, docWidth } = getPageContext();
|
|
7466
|
+
const rect = el.getBoundingClientRect();
|
|
7467
|
+
EventsManager.trackAutoEvent('media_pause', {
|
|
7468
|
+
media_type: mediaType,
|
|
7469
|
+
media_src: el.src || null,
|
|
7470
|
+
media_current_time: el.currentTime || null,
|
|
7471
|
+
media_duration: el.duration || null,
|
|
7472
|
+
scroll_x: scrollX,
|
|
7473
|
+
scroll_y: scrollY,
|
|
7474
|
+
document_height: docHeight,
|
|
7475
|
+
document_width: docWidth,
|
|
7476
|
+
page_x: rect.left + scrollX,
|
|
7477
|
+
page_y: rect.top + scrollY,
|
|
7478
|
+
click_coordinates: { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }
|
|
7479
|
+
});
|
|
7065
7480
|
}, true);
|
|
7066
7481
|
});
|
|
7067
7482
|
},
|
|
@@ -7088,12 +7503,14 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
7088
7503
|
|
|
7089
7504
|
const selectedText = selection.toString().trim();
|
|
7090
7505
|
|
|
7091
|
-
// Only track if
|
|
7092
|
-
|
|
7506
|
+
// Only track if selection is meaningful (≥3 chars) and different from last selection.
|
|
7507
|
+
// <3 chars filters out accidental double-clicks and single-word mis-selections.
|
|
7508
|
+
if (selectedText && selectedText.length >= 3 && selectedText !== lastSelectionText) {
|
|
7093
7509
|
const now = Date.now();
|
|
7094
|
-
|
|
7095
|
-
//
|
|
7096
|
-
|
|
7510
|
+
|
|
7511
|
+
// 1s cooldown between events — prevents rapid re-selection noise
|
|
7512
|
+
// (e.g. user adjusting selection handles fires many selectionchange events)
|
|
7513
|
+
if (now - lastSelectionTime < 1000) {
|
|
7097
7514
|
return;
|
|
7098
7515
|
}
|
|
7099
7516
|
|
|
@@ -7987,6 +8404,7 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
|
|
|
7987
8404
|
* The script.js file is inlined during the build process by Rollup.
|
|
7988
8405
|
*/
|
|
7989
8406
|
|
|
8407
|
+
|
|
7990
8408
|
// Create a wrapper that provides programmatic initialization
|
|
7991
8409
|
const CryptiqueSDK = {
|
|
7992
8410
|
/**
|