@prosdevlab/experience-sdk-plugins 0.1.4 → 0.2.0
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/.turbo/turbo-build.log +6 -6
- package/CHANGELOG.md +30 -0
- package/dist/index.d.ts +608 -1
- package/dist/index.js +692 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/exit-intent/exit-intent.test.ts +423 -0
- package/src/exit-intent/exit-intent.ts +372 -0
- package/src/exit-intent/index.ts +6 -0
- package/src/exit-intent/types.ts +59 -0
- package/src/index.ts +5 -0
- package/src/integration.test.ts +362 -0
- package/src/page-visits/index.ts +6 -0
- package/src/page-visits/page-visits.test.ts +562 -0
- package/src/page-visits/page-visits.ts +314 -0
- package/src/page-visits/types.ts +119 -0
- package/src/scroll-depth/index.ts +6 -0
- package/src/scroll-depth/scroll-depth.test.ts +545 -0
- package/src/scroll-depth/scroll-depth.ts +400 -0
- package/src/scroll-depth/types.ts +122 -0
- package/src/time-delay/index.ts +6 -0
- package/src/time-delay/time-delay.test.ts +477 -0
- package/src/time-delay/time-delay.ts +297 -0
- package/src/time-delay/types.ts +89 -0
- package/src/utils/sanitize.ts +1 -1
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/** @module scrollDepthPlugin */
|
|
2
|
+
|
|
3
|
+
import type { PluginFunction } from '@lytics/sdk-kit';
|
|
4
|
+
import type { ScrollDepthEvent, ScrollDepthPluginConfig } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Pure function: Detect device type based on user agent and screen size
|
|
8
|
+
*/
|
|
9
|
+
export function detectDevice(): 'mobile' | 'tablet' | 'desktop' {
|
|
10
|
+
if (typeof window === 'undefined') return 'desktop';
|
|
11
|
+
|
|
12
|
+
const ua = navigator.userAgent;
|
|
13
|
+
const isMobile = /Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
|
|
14
|
+
const isTablet = /iPad|Android(?!.*Mobile)/i.test(ua);
|
|
15
|
+
|
|
16
|
+
// Also check screen size as fallback
|
|
17
|
+
const width = window.innerWidth;
|
|
18
|
+
if (width < 768) return 'mobile';
|
|
19
|
+
if (width < 1024) return 'tablet';
|
|
20
|
+
if (isMobile) return 'mobile';
|
|
21
|
+
if (isTablet) return 'tablet';
|
|
22
|
+
|
|
23
|
+
return 'desktop';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Pure function: Throttle helper
|
|
28
|
+
* @param func Function to throttle
|
|
29
|
+
* @param wait Wait time in milliseconds
|
|
30
|
+
* @returns Throttled function
|
|
31
|
+
*/
|
|
32
|
+
export function throttle<T extends (...args: any[]) => void>(
|
|
33
|
+
func: T,
|
|
34
|
+
wait: number
|
|
35
|
+
): (...args: Parameters<T>) => void {
|
|
36
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
37
|
+
let previous = 0;
|
|
38
|
+
|
|
39
|
+
return function throttled(...args: Parameters<T>) {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
const remaining = wait - (now - previous);
|
|
42
|
+
|
|
43
|
+
if (remaining <= 0 || remaining > wait) {
|
|
44
|
+
if (timeout) {
|
|
45
|
+
clearTimeout(timeout);
|
|
46
|
+
timeout = null;
|
|
47
|
+
}
|
|
48
|
+
previous = now;
|
|
49
|
+
func(...args);
|
|
50
|
+
} else if (!timeout) {
|
|
51
|
+
timeout = setTimeout(() => {
|
|
52
|
+
previous = Date.now();
|
|
53
|
+
timeout = null;
|
|
54
|
+
func(...args);
|
|
55
|
+
}, remaining);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Pure function: Calculate scroll percentage
|
|
62
|
+
*/
|
|
63
|
+
export function calculateScrollPercent(includeViewportHeight: boolean): number {
|
|
64
|
+
if (typeof document === 'undefined') return 0;
|
|
65
|
+
|
|
66
|
+
// Browser compatibility: Use scrollingElement or fallback
|
|
67
|
+
const scrollingElement = document.scrollingElement || document.documentElement;
|
|
68
|
+
|
|
69
|
+
const scrollTop = scrollingElement.scrollTop;
|
|
70
|
+
const scrollHeight = scrollingElement.scrollHeight;
|
|
71
|
+
const clientHeight = scrollingElement.clientHeight;
|
|
72
|
+
|
|
73
|
+
// Handle edge case: content shorter than viewport
|
|
74
|
+
if (scrollHeight <= clientHeight) {
|
|
75
|
+
return 100; // Treat as fully scrolled
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (includeViewportHeight) {
|
|
79
|
+
// Include viewport: more intuitive for users
|
|
80
|
+
// 100% when bottom of viewport reaches end of content
|
|
81
|
+
return Math.min(((scrollTop + clientHeight) / scrollHeight) * 100, 100);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Exclude viewport: traditional method
|
|
85
|
+
// 100% when top of viewport reaches scrollable end
|
|
86
|
+
return Math.min((scrollTop / (scrollHeight - clientHeight)) * 100, 100);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Pure function: Calculate engagement score from metrics
|
|
91
|
+
*/
|
|
92
|
+
export function calculateEngagementScore(
|
|
93
|
+
velocity: number,
|
|
94
|
+
fastScrollThreshold: number,
|
|
95
|
+
directionChanges: number,
|
|
96
|
+
timeScrollingUp: number,
|
|
97
|
+
totalTime: number
|
|
98
|
+
): number {
|
|
99
|
+
// Lower score = more engaged (slower scrolling, fewer direction changes)
|
|
100
|
+
// Higher score = skimming (fast scrolling, lots of seeking)
|
|
101
|
+
const velocityScore = Math.min((velocity / fastScrollThreshold) * 50, 50);
|
|
102
|
+
const directionScore = Math.min((directionChanges / 5) * 30, 30);
|
|
103
|
+
const seekingScore = Math.min((timeScrollingUp / totalTime) * 20, 20);
|
|
104
|
+
|
|
105
|
+
return Math.max(0, 100 - (velocityScore + directionScore + seekingScore));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Scroll Depth Plugin
|
|
110
|
+
*
|
|
111
|
+
* Tracks scroll depth and emits `trigger:scrollDepth` events when thresholds are crossed.
|
|
112
|
+
*
|
|
113
|
+
* ## How It Works
|
|
114
|
+
*
|
|
115
|
+
* 1. **Detection**: Listens to `scroll` events (throttled)
|
|
116
|
+
* 2. **Calculation**: Calculates current scroll percentage
|
|
117
|
+
* 3. **Tracking**: Tracks maximum scroll depth and threshold crossings
|
|
118
|
+
* 4. **Emission**: Emits `trigger:scrollDepth` events when thresholds are crossed
|
|
119
|
+
*
|
|
120
|
+
* ## Configuration
|
|
121
|
+
*
|
|
122
|
+
* ```typescript
|
|
123
|
+
* init({
|
|
124
|
+
* scrollDepth: {
|
|
125
|
+
* thresholds: [25, 50, 75, 100], // Percentages to track
|
|
126
|
+
* throttle: 100, // Throttle interval (ms)
|
|
127
|
+
* includeViewportHeight: true, // Calculation method
|
|
128
|
+
* recalculateOnResize: true // Recalculate on resize
|
|
129
|
+
* }
|
|
130
|
+
* });
|
|
131
|
+
* ```
|
|
132
|
+
*
|
|
133
|
+
* ## Experience Targeting
|
|
134
|
+
*
|
|
135
|
+
* ```typescript
|
|
136
|
+
* register('mid-article-cta', {
|
|
137
|
+
* type: 'banner',
|
|
138
|
+
* content: { message: 'Enjoying the article?' },
|
|
139
|
+
* targeting: {
|
|
140
|
+
* custom: (ctx) => (ctx.triggers?.scrollDepth?.percent || 0) >= 50
|
|
141
|
+
* }
|
|
142
|
+
* });
|
|
143
|
+
* ```
|
|
144
|
+
*
|
|
145
|
+
* ## API Methods
|
|
146
|
+
*
|
|
147
|
+
* ```typescript
|
|
148
|
+
* // Get maximum scroll percentage reached
|
|
149
|
+
* instance.scrollDepth.getMaxPercent(); // 73
|
|
150
|
+
*
|
|
151
|
+
* // Get current scroll percentage
|
|
152
|
+
* instance.scrollDepth.getCurrentPercent(); // 50
|
|
153
|
+
*
|
|
154
|
+
* // Get all crossed thresholds
|
|
155
|
+
* instance.scrollDepth.getThresholdsCrossed(); // [25, 50]
|
|
156
|
+
*
|
|
157
|
+
* // Reset tracking (useful for testing)
|
|
158
|
+
* instance.scrollDepth.reset();
|
|
159
|
+
* ```
|
|
160
|
+
*
|
|
161
|
+
* @param plugin Plugin interface from sdk-kit
|
|
162
|
+
* @param instance SDK instance
|
|
163
|
+
* @param config SDK configuration
|
|
164
|
+
*/
|
|
165
|
+
export const scrollDepthPlugin: PluginFunction = (plugin, instance, config) => {
|
|
166
|
+
plugin.ns('experiences.scrollDepth');
|
|
167
|
+
|
|
168
|
+
// Set defaults
|
|
169
|
+
plugin.defaults({
|
|
170
|
+
scrollDepth: {
|
|
171
|
+
thresholds: [25, 50, 75, 100],
|
|
172
|
+
throttle: 100,
|
|
173
|
+
includeViewportHeight: true,
|
|
174
|
+
recalculateOnResize: true,
|
|
175
|
+
trackAdvancedMetrics: false,
|
|
176
|
+
fastScrollVelocityThreshold: 3,
|
|
177
|
+
disableOnMobile: false,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Get config
|
|
182
|
+
const scrollConfig = config.get('scrollDepth') as ScrollDepthPluginConfig['scrollDepth'];
|
|
183
|
+
if (!scrollConfig) return;
|
|
184
|
+
|
|
185
|
+
// TypeScript guard: scrollConfig is now guaranteed to be defined
|
|
186
|
+
const cfg = scrollConfig;
|
|
187
|
+
|
|
188
|
+
// Check device and disable if needed (using pure function)
|
|
189
|
+
const device = detectDevice();
|
|
190
|
+
if (cfg.disableOnMobile && device === 'mobile') {
|
|
191
|
+
return; // Skip initialization on mobile
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// State
|
|
195
|
+
let maxScrollPercent = 0;
|
|
196
|
+
const triggeredThresholds = new Set<number>();
|
|
197
|
+
|
|
198
|
+
// Advanced metrics state
|
|
199
|
+
const pageLoadTime = Date.now();
|
|
200
|
+
let lastScrollPosition = 0;
|
|
201
|
+
let lastScrollTime = Date.now();
|
|
202
|
+
let lastScrollDirection: 'up' | 'down' | null = null;
|
|
203
|
+
let directionChangesSinceLastThreshold = 0;
|
|
204
|
+
let timeScrollingUp = 0;
|
|
205
|
+
const thresholdTimes = new Map<number, number>(); // threshold -> time reached
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Handle scroll event
|
|
209
|
+
*/
|
|
210
|
+
function handleScroll() {
|
|
211
|
+
// Use pure function for calculation
|
|
212
|
+
const currentPercent = calculateScrollPercent(cfg.includeViewportHeight ?? true);
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
const scrollingElement = document.scrollingElement || document.documentElement;
|
|
215
|
+
const currentPosition = scrollingElement.scrollTop;
|
|
216
|
+
|
|
217
|
+
// Track advanced metrics if enabled
|
|
218
|
+
let velocity = 0;
|
|
219
|
+
let _directionChange = false;
|
|
220
|
+
|
|
221
|
+
if (cfg.trackAdvancedMetrics) {
|
|
222
|
+
// Calculate velocity (pixels per millisecond)
|
|
223
|
+
const timeDelta = now - lastScrollTime;
|
|
224
|
+
const positionDelta = currentPosition - lastScrollPosition;
|
|
225
|
+
velocity = timeDelta > 0 ? Math.abs(positionDelta) / timeDelta : 0;
|
|
226
|
+
|
|
227
|
+
// Detect direction changes
|
|
228
|
+
const currentDirection =
|
|
229
|
+
positionDelta > 0 ? 'down' : positionDelta < 0 ? 'up' : lastScrollDirection;
|
|
230
|
+
if (currentDirection && lastScrollDirection && currentDirection !== lastScrollDirection) {
|
|
231
|
+
directionChangesSinceLastThreshold++;
|
|
232
|
+
_directionChange = true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Track time spent scrolling up (seeking behavior)
|
|
236
|
+
if (currentDirection === 'up' && timeDelta > 0) {
|
|
237
|
+
timeScrollingUp += timeDelta;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
lastScrollDirection = currentDirection;
|
|
241
|
+
lastScrollPosition = currentPosition;
|
|
242
|
+
lastScrollTime = now;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Update max scroll
|
|
246
|
+
maxScrollPercent = Math.max(maxScrollPercent, currentPercent);
|
|
247
|
+
|
|
248
|
+
// Check thresholds
|
|
249
|
+
for (const threshold of cfg.thresholds || []) {
|
|
250
|
+
if (currentPercent >= threshold && !triggeredThresholds.has(threshold)) {
|
|
251
|
+
triggeredThresholds.add(threshold);
|
|
252
|
+
|
|
253
|
+
// Record time to threshold
|
|
254
|
+
if (cfg.trackAdvancedMetrics) {
|
|
255
|
+
thresholdTimes.set(threshold, now - pageLoadTime);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Build event payload
|
|
259
|
+
const eventPayload: ScrollDepthEvent = {
|
|
260
|
+
triggered: true,
|
|
261
|
+
timestamp: now,
|
|
262
|
+
percent: Math.round(currentPercent * 100) / 100,
|
|
263
|
+
maxPercent: Math.round(maxScrollPercent * 100) / 100,
|
|
264
|
+
threshold,
|
|
265
|
+
thresholdsCrossed: Array.from(triggeredThresholds).sort((a, b) => a - b),
|
|
266
|
+
device,
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// Add advanced metrics if enabled
|
|
270
|
+
if (cfg.trackAdvancedMetrics) {
|
|
271
|
+
const fastScrollThreshold = cfg.fastScrollVelocityThreshold || 3;
|
|
272
|
+
const isFastScrolling = velocity > fastScrollThreshold;
|
|
273
|
+
|
|
274
|
+
// Calculate engagement score using pure function
|
|
275
|
+
const engagementScore = calculateEngagementScore(
|
|
276
|
+
velocity,
|
|
277
|
+
fastScrollThreshold,
|
|
278
|
+
directionChangesSinceLastThreshold,
|
|
279
|
+
timeScrollingUp,
|
|
280
|
+
now - pageLoadTime
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
eventPayload.advanced = {
|
|
284
|
+
timeToThreshold: now - pageLoadTime,
|
|
285
|
+
velocity: Math.round(velocity * 1000) / 1000, // Round to 3 decimals
|
|
286
|
+
isFastScrolling,
|
|
287
|
+
directionChanges: directionChangesSinceLastThreshold,
|
|
288
|
+
timeScrollingUp,
|
|
289
|
+
engagementScore: Math.round(engagementScore),
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// Reset direction changes counter after threshold
|
|
293
|
+
directionChangesSinceLastThreshold = 0;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
instance.emit('trigger:scrollDepth', eventPayload);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Throttle scroll handler (using pure function)
|
|
302
|
+
const throttledScrollHandler = throttle(handleScroll, cfg.throttle || 100);
|
|
303
|
+
|
|
304
|
+
// Throttle resize handler (using pure function)
|
|
305
|
+
const throttledResizeHandler = throttle(handleScroll, cfg.throttle || 100);
|
|
306
|
+
|
|
307
|
+
// Initialize
|
|
308
|
+
function initialize() {
|
|
309
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
310
|
+
return; // Not in browser environment
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Add scroll listener
|
|
314
|
+
window.addEventListener('scroll', throttledScrollHandler, { passive: true });
|
|
315
|
+
|
|
316
|
+
// Add resize listener (optional)
|
|
317
|
+
if (cfg.recalculateOnResize) {
|
|
318
|
+
window.addEventListener('resize', throttledResizeHandler, { passive: true });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Don't check initial scroll position - wait for first user interaction
|
|
322
|
+
// This avoids triggering all thresholds immediately on pages that start scrolled
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Cleanup function
|
|
326
|
+
function cleanup() {
|
|
327
|
+
window.removeEventListener('scroll', throttledScrollHandler);
|
|
328
|
+
window.removeEventListener('resize', throttledResizeHandler);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Setup destroy handler
|
|
332
|
+
const destroyHandler = () => {
|
|
333
|
+
cleanup();
|
|
334
|
+
};
|
|
335
|
+
instance.on('destroy', destroyHandler);
|
|
336
|
+
|
|
337
|
+
// Expose API
|
|
338
|
+
plugin.expose({
|
|
339
|
+
scrollDepth: {
|
|
340
|
+
/**
|
|
341
|
+
* Get the maximum scroll percentage reached during the session
|
|
342
|
+
*/
|
|
343
|
+
getMaxPercent: () => maxScrollPercent,
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Get the current scroll percentage
|
|
347
|
+
*/
|
|
348
|
+
getCurrentPercent: () => calculateScrollPercent(cfg.includeViewportHeight ?? true),
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Get all thresholds that have been crossed
|
|
352
|
+
*/
|
|
353
|
+
getThresholdsCrossed: () => Array.from(triggeredThresholds).sort((a, b) => a - b),
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Get the detected device type
|
|
357
|
+
*/
|
|
358
|
+
getDevice: () => device,
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Get advanced metrics (only available when trackAdvancedMetrics is enabled)
|
|
362
|
+
*/
|
|
363
|
+
getAdvancedMetrics: () => {
|
|
364
|
+
if (!cfg.trackAdvancedMetrics) return null;
|
|
365
|
+
|
|
366
|
+
const now = Date.now();
|
|
367
|
+
return {
|
|
368
|
+
timeOnPage: now - pageLoadTime,
|
|
369
|
+
directionChanges: directionChangesSinceLastThreshold,
|
|
370
|
+
timeScrollingUp,
|
|
371
|
+
thresholdTimes: Object.fromEntries(thresholdTimes),
|
|
372
|
+
};
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Reset scroll depth tracking
|
|
377
|
+
* Clears all triggered thresholds, max scroll, and advanced metrics
|
|
378
|
+
*/
|
|
379
|
+
reset: () => {
|
|
380
|
+
maxScrollPercent = 0;
|
|
381
|
+
triggeredThresholds.clear();
|
|
382
|
+
directionChangesSinceLastThreshold = 0;
|
|
383
|
+
timeScrollingUp = 0;
|
|
384
|
+
thresholdTimes.clear();
|
|
385
|
+
lastScrollDirection = null;
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Initialize on next tick to ensure DOM is ready
|
|
391
|
+
if (typeof window !== 'undefined') {
|
|
392
|
+
setTimeout(initialize, 0);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Return cleanup function
|
|
396
|
+
return () => {
|
|
397
|
+
cleanup();
|
|
398
|
+
instance.off('destroy', destroyHandler);
|
|
399
|
+
};
|
|
400
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/** @module scrollDepthPlugin */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Scroll Depth Plugin API
|
|
5
|
+
*/
|
|
6
|
+
export interface ScrollDepthPlugin {
|
|
7
|
+
getMaxPercent(): number;
|
|
8
|
+
getCurrentPercent(): number;
|
|
9
|
+
getThresholdsCrossed(): number[];
|
|
10
|
+
getDevice(): 'mobile' | 'tablet' | 'desktop';
|
|
11
|
+
getAdvancedMetrics(): {
|
|
12
|
+
timeOnPage: number;
|
|
13
|
+
directionChanges: number;
|
|
14
|
+
timeScrollingUp: number;
|
|
15
|
+
thresholdTimes: Record<number, number>;
|
|
16
|
+
} | null;
|
|
17
|
+
reset(): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Scroll Depth Plugin Configuration
|
|
22
|
+
*
|
|
23
|
+
* Tracks scroll depth and emits trigger:scrollDepth events when thresholds are crossed.
|
|
24
|
+
*/
|
|
25
|
+
export interface ScrollDepthPluginConfig {
|
|
26
|
+
scrollDepth?: {
|
|
27
|
+
/**
|
|
28
|
+
* Array of scroll percentage thresholds to track (0-100).
|
|
29
|
+
* When user scrolls past a threshold, a trigger:scrollDepth event is emitted.
|
|
30
|
+
* @default [25, 50, 75, 100]
|
|
31
|
+
* @example [50, 100]
|
|
32
|
+
*/
|
|
33
|
+
thresholds?: number[];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Throttle interval in milliseconds for scroll event handler.
|
|
37
|
+
* Lower values are more responsive but impact performance.
|
|
38
|
+
* @default 100
|
|
39
|
+
* @example 200
|
|
40
|
+
*/
|
|
41
|
+
throttle?: number;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Include viewport height in scroll percentage calculation.
|
|
45
|
+
*
|
|
46
|
+
* - true: (scrollTop + viewportHeight) / totalHeight
|
|
47
|
+
* More intuitive: 100% when bottom of viewport reaches end
|
|
48
|
+
* - false: scrollTop / (totalHeight - viewportHeight)
|
|
49
|
+
* Pathfora's method: 100% when top of viewport reaches end
|
|
50
|
+
*
|
|
51
|
+
* @default true
|
|
52
|
+
*/
|
|
53
|
+
includeViewportHeight?: boolean;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Recalculate scroll on window resize.
|
|
57
|
+
* Useful for responsive layouts where content height changes.
|
|
58
|
+
* @default true
|
|
59
|
+
*/
|
|
60
|
+
recalculateOnResize?: boolean;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Track advanced metrics (velocity, direction, time-to-threshold).
|
|
64
|
+
* Enables advanced engagement quality analysis.
|
|
65
|
+
* Slight performance overhead but provides rich insights.
|
|
66
|
+
* @default false
|
|
67
|
+
*/
|
|
68
|
+
trackAdvancedMetrics?: boolean;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Velocity threshold (px/ms) to consider "fast scrolling".
|
|
72
|
+
* Fast scrolling often indicates skimming rather than reading.
|
|
73
|
+
* Only used when trackAdvancedMetrics is true.
|
|
74
|
+
* @default 3
|
|
75
|
+
*/
|
|
76
|
+
fastScrollVelocityThreshold?: number;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Disable scroll tracking on mobile devices.
|
|
80
|
+
* Useful since mobile scroll behavior differs significantly from desktop.
|
|
81
|
+
* @default false
|
|
82
|
+
*/
|
|
83
|
+
disableOnMobile?: boolean;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Scroll Depth Event Payload
|
|
89
|
+
*
|
|
90
|
+
* Emitted as `trigger:scrollDepth` when a threshold is crossed.
|
|
91
|
+
*/
|
|
92
|
+
export interface ScrollDepthEvent {
|
|
93
|
+
/** Whether the trigger has fired */
|
|
94
|
+
triggered: boolean;
|
|
95
|
+
/** Timestamp when the event was emitted */
|
|
96
|
+
timestamp: number;
|
|
97
|
+
/** Current scroll percentage (0-100) */
|
|
98
|
+
percent: number;
|
|
99
|
+
/** Maximum scroll percentage reached during session */
|
|
100
|
+
maxPercent: number;
|
|
101
|
+
/** The threshold that was just crossed */
|
|
102
|
+
threshold: number;
|
|
103
|
+
/** All thresholds that have been triggered */
|
|
104
|
+
thresholdsCrossed: number[];
|
|
105
|
+
/** Device type (mobile, tablet, desktop) */
|
|
106
|
+
device: 'mobile' | 'tablet' | 'desktop';
|
|
107
|
+
/** Advanced metrics (only present when trackAdvancedMetrics is enabled) */
|
|
108
|
+
advanced?: {
|
|
109
|
+
/** Time in milliseconds to reach this threshold from page load */
|
|
110
|
+
timeToThreshold: number;
|
|
111
|
+
/** Current scroll velocity in pixels per millisecond */
|
|
112
|
+
velocity: number;
|
|
113
|
+
/** Whether user is scrolling fast (indicates skimming) */
|
|
114
|
+
isFastScrolling: boolean;
|
|
115
|
+
/** Number of direction changes (up/down) since last threshold */
|
|
116
|
+
directionChanges: number;
|
|
117
|
+
/** Total time spent scrolling up (indicates seeking behavior) */
|
|
118
|
+
timeScrollingUp: number;
|
|
119
|
+
/** Scroll quality score (0-100, higher = more engaged) */
|
|
120
|
+
engagementScore: number;
|
|
121
|
+
};
|
|
122
|
+
}
|