@prosdevlab/experience-sdk-plugins 0.1.4 → 0.3.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 +150 -0
- package/README.md +141 -79
- package/dist/index.d.ts +813 -35
- package/dist/index.js +1910 -66
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/banner/banner.ts +63 -62
- package/src/exit-intent/exit-intent.test.ts +423 -0
- package/src/exit-intent/exit-intent.ts +371 -0
- package/src/exit-intent/index.ts +6 -0
- package/src/exit-intent/types.ts +59 -0
- package/src/index.ts +7 -0
- package/src/inline/index.ts +3 -0
- package/src/inline/inline.test.ts +620 -0
- package/src/inline/inline.ts +269 -0
- package/src/inline/insertion.ts +66 -0
- package/src/inline/types.ts +52 -0
- package/src/integration.test.ts +421 -0
- package/src/modal/form-rendering.ts +262 -0
- package/src/modal/form-styles.ts +212 -0
- package/src/modal/form-validation.test.ts +413 -0
- package/src/modal/form-validation.ts +126 -0
- package/src/modal/index.ts +3 -0
- package/src/modal/modal-styles.ts +204 -0
- package/src/modal/modal.browser.test.ts +164 -0
- package/src/modal/modal.test.ts +1294 -0
- package/src/modal/modal.ts +685 -0
- package/src/modal/types.ts +114 -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 +580 -0
- package/src/scroll-depth/scroll-depth.ts +398 -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 +296 -0
- package/src/time-delay/types.ts +89 -0
- package/src/types.ts +20 -36
- package/src/utils/sanitize.ts +5 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,57 +1,187 @@
|
|
|
1
|
-
import { PluginFunction } from '@lytics/sdk-kit';
|
|
1
|
+
import { PluginFunction, SDK } from '@lytics/sdk-kit';
|
|
2
|
+
export { PluginFunction } from '@lytics/sdk-kit';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
* These types are re-exported by core for user convenience
|
|
5
|
+
* Inline plugin configuration
|
|
6
6
|
*/
|
|
7
|
+
interface InlinePluginConfig {
|
|
8
|
+
inline?: {
|
|
9
|
+
/** Retry selector lookup if not found (default: false) */
|
|
10
|
+
retry?: boolean;
|
|
11
|
+
/** Retry timeout in ms (default: 5000) */
|
|
12
|
+
retryTimeout?: number;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
7
15
|
/**
|
|
8
|
-
*
|
|
9
|
-
*/
|
|
10
|
-
type ExperienceContent = BannerContent | ModalContent | TooltipContent;
|
|
11
|
-
/**
|
|
12
|
-
* Banner content configuration
|
|
16
|
+
* Inline content configuration
|
|
13
17
|
*/
|
|
14
|
-
interface
|
|
15
|
-
|
|
18
|
+
interface InlineContent$1 {
|
|
19
|
+
/** CSS selector for target element */
|
|
20
|
+
selector: string;
|
|
21
|
+
/** Where to insert content (default: 'replace') */
|
|
22
|
+
position?: 'replace' | 'append' | 'prepend' | 'before' | 'after';
|
|
23
|
+
/** HTML content to insert */
|
|
16
24
|
message: string;
|
|
17
|
-
|
|
18
|
-
text: string;
|
|
19
|
-
action?: string;
|
|
20
|
-
url?: string;
|
|
21
|
-
variant?: 'primary' | 'secondary' | 'link';
|
|
22
|
-
metadata?: Record<string, any>;
|
|
23
|
-
className?: string;
|
|
24
|
-
style?: Record<string, string>;
|
|
25
|
-
}>;
|
|
25
|
+
/** Show close button (default: false) */
|
|
26
26
|
dismissable?: boolean;
|
|
27
|
-
|
|
27
|
+
/** Remember dismissal in localStorage (default: false) */
|
|
28
|
+
persist?: boolean;
|
|
29
|
+
/** Custom CSS class */
|
|
28
30
|
className?: string;
|
|
31
|
+
/** Inline styles */
|
|
29
32
|
style?: Record<string, string>;
|
|
30
33
|
}
|
|
31
34
|
/**
|
|
32
|
-
*
|
|
35
|
+
* Inline plugin API
|
|
33
36
|
*/
|
|
34
|
-
interface
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
interface InlinePlugin {
|
|
38
|
+
/** Show an inline experience */
|
|
39
|
+
show(experience: any): void;
|
|
40
|
+
/** Remove a specific inline experience */
|
|
41
|
+
remove(experienceId: string): void;
|
|
42
|
+
/** Check if an inline experience is showing */
|
|
43
|
+
isShowing(experienceId?: string): boolean;
|
|
39
44
|
}
|
|
40
45
|
/**
|
|
41
|
-
*
|
|
46
|
+
* Insertion position for inline content
|
|
42
47
|
*/
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
type InsertionPosition = 'replace' | 'append' | 'prepend' | 'before' | 'after';
|
|
49
|
+
|
|
50
|
+
interface ModalConfig {
|
|
51
|
+
modal?: {
|
|
52
|
+
/** Allow dismissal via close button (default: true) */
|
|
53
|
+
dismissable?: boolean;
|
|
54
|
+
/** Allow dismissal via backdrop click (default: true) */
|
|
55
|
+
backdropDismiss?: boolean;
|
|
56
|
+
/** Z-index for modal (default: 10001) */
|
|
57
|
+
zIndex?: number;
|
|
58
|
+
/** Modal size (default: 'md') */
|
|
59
|
+
size?: 'sm' | 'md' | 'lg' | 'fullscreen' | 'auto';
|
|
60
|
+
/** Auto-fullscreen on mobile screens <640px (default: true for 'lg', false for others) */
|
|
61
|
+
mobileFullscreen?: boolean;
|
|
62
|
+
/** Modal position (default: 'center') */
|
|
63
|
+
position?: 'center' | 'bottom';
|
|
64
|
+
/** Animation type (default: 'fade') */
|
|
65
|
+
animation?: 'fade' | 'slide-up' | 'none';
|
|
66
|
+
/** Animation duration in ms (default: 200) */
|
|
67
|
+
animationDuration?: number;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
interface ModalContent$1 {
|
|
71
|
+
/** Optional hero image at top of modal */
|
|
72
|
+
image?: {
|
|
73
|
+
/** Image source URL */
|
|
74
|
+
src: string;
|
|
75
|
+
/** Alt text for accessibility */
|
|
76
|
+
alt: string;
|
|
77
|
+
/** Max height in pixels (default: 300, 200 on mobile) */
|
|
78
|
+
maxHeight?: number;
|
|
79
|
+
};
|
|
80
|
+
/** Modal title */
|
|
81
|
+
title?: string;
|
|
82
|
+
/** Modal message (supports HTML via sanitizer) */
|
|
83
|
+
message: string;
|
|
84
|
+
/** Array of action buttons */
|
|
85
|
+
buttons?: ExperienceButton[];
|
|
86
|
+
/** Optional form configuration */
|
|
87
|
+
form?: FormConfig;
|
|
88
|
+
/** Custom CSS class */
|
|
89
|
+
className?: string;
|
|
90
|
+
/** Inline styles */
|
|
91
|
+
style?: Record<string, string>;
|
|
92
|
+
}
|
|
93
|
+
interface FormConfig {
|
|
94
|
+
/** Array of form fields */
|
|
95
|
+
fields: FormField[];
|
|
96
|
+
/** Submit button configuration */
|
|
97
|
+
submitButton: ExperienceButton;
|
|
98
|
+
/** Success state after submission */
|
|
99
|
+
successState?: FormState;
|
|
100
|
+
/** Error state on submission failure */
|
|
101
|
+
errorState?: FormState;
|
|
102
|
+
/** Custom validation function (optional) */
|
|
103
|
+
validate?: (data: Record<string, string>) => ValidationResult;
|
|
104
|
+
}
|
|
105
|
+
interface FormField {
|
|
106
|
+
/** Field name (used in form data) */
|
|
107
|
+
name: string;
|
|
108
|
+
/** Field type */
|
|
109
|
+
type: 'email' | 'text' | 'textarea' | 'tel' | 'url' | 'number';
|
|
110
|
+
/** Label text (optional) */
|
|
111
|
+
label?: string;
|
|
112
|
+
/** Placeholder text */
|
|
113
|
+
placeholder?: string;
|
|
114
|
+
/** Required field (default: false) */
|
|
115
|
+
required?: boolean;
|
|
116
|
+
/** Custom validation pattern (regex) */
|
|
117
|
+
pattern?: string;
|
|
118
|
+
/** Error message for validation failure */
|
|
119
|
+
errorMessage?: string;
|
|
120
|
+
/** Custom CSS class */
|
|
121
|
+
className?: string;
|
|
122
|
+
/** Inline styles */
|
|
123
|
+
style?: Record<string, string>;
|
|
124
|
+
}
|
|
125
|
+
interface FormState {
|
|
126
|
+
/** Title to show in success/error state */
|
|
127
|
+
title?: string;
|
|
128
|
+
/** Message to show */
|
|
45
129
|
message: string;
|
|
46
|
-
|
|
47
|
-
|
|
130
|
+
/** Optional buttons (e.g., "Close", "Try Again") */
|
|
131
|
+
buttons?: ExperienceButton[];
|
|
132
|
+
}
|
|
133
|
+
interface ValidationResult {
|
|
134
|
+
/** Whether validation passed */
|
|
135
|
+
valid: boolean;
|
|
136
|
+
/** Validation errors by field name */
|
|
137
|
+
errors?: Record<string, string>;
|
|
48
138
|
}
|
|
139
|
+
interface ModalPlugin {
|
|
140
|
+
/** Show a modal experience */
|
|
141
|
+
show(experience: any): void;
|
|
142
|
+
/** Remove a specific modal */
|
|
143
|
+
remove(experienceId: string): void;
|
|
144
|
+
/** Check if a modal is showing */
|
|
145
|
+
isShowing(experienceId?: string): boolean;
|
|
146
|
+
/** Show form success or error state */
|
|
147
|
+
showFormState(experienceId: string, state: 'success' | 'error'): void;
|
|
148
|
+
/** Reset form to initial state */
|
|
149
|
+
resetForm(experienceId: string): void;
|
|
150
|
+
/** Get current form data */
|
|
151
|
+
getFormData(experienceId: string): Record<string, string> | null;
|
|
152
|
+
}
|
|
153
|
+
|
|
49
154
|
/**
|
|
50
|
-
*
|
|
155
|
+
* Shared types for Experience SDK plugins
|
|
156
|
+
* These types are re-exported by core for user convenience
|
|
51
157
|
*/
|
|
52
|
-
|
|
158
|
+
|
|
159
|
+
type ModalContent = ModalContent$1;
|
|
160
|
+
type InlineContent = InlineContent$1;
|
|
161
|
+
/**
|
|
162
|
+
* Experience button configuration (used across all experience types)
|
|
163
|
+
*/
|
|
164
|
+
interface ExperienceButton {
|
|
165
|
+
text: string;
|
|
166
|
+
action?: string;
|
|
167
|
+
url?: string;
|
|
168
|
+
variant?: 'primary' | 'secondary' | 'link';
|
|
169
|
+
dismiss?: boolean;
|
|
170
|
+
metadata?: Record<string, any>;
|
|
171
|
+
className?: string;
|
|
172
|
+
style?: Record<string, string>;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Banner content configuration
|
|
176
|
+
*/
|
|
177
|
+
interface BannerContent {
|
|
178
|
+
title?: string;
|
|
53
179
|
message: string;
|
|
54
|
-
|
|
180
|
+
buttons?: ExperienceButton[];
|
|
181
|
+
dismissable?: boolean;
|
|
182
|
+
position?: 'top' | 'bottom';
|
|
183
|
+
className?: string;
|
|
184
|
+
style?: Record<string, string>;
|
|
55
185
|
}
|
|
56
186
|
/**
|
|
57
187
|
* Tooltip content configuration
|
|
@@ -60,6 +190,10 @@ interface TooltipContent {
|
|
|
60
190
|
message: string;
|
|
61
191
|
position?: 'top' | 'bottom' | 'left' | 'right';
|
|
62
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Experience content - varies by type
|
|
195
|
+
*/
|
|
196
|
+
type ExperienceContent = BannerContent | ModalContent | InlineContent | TooltipContent;
|
|
63
197
|
/**
|
|
64
198
|
* Experience definition
|
|
65
199
|
*/
|
|
@@ -192,6 +326,140 @@ interface DebugPlugin {
|
|
|
192
326
|
*/
|
|
193
327
|
declare const debugPlugin: PluginFunction;
|
|
194
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Exit Intent Plugin Configuration
|
|
331
|
+
*/
|
|
332
|
+
interface ExitIntentPluginConfig {
|
|
333
|
+
exitIntent?: {
|
|
334
|
+
/**
|
|
335
|
+
* Maximum Y position (px) where exit intent can trigger
|
|
336
|
+
* @default 50
|
|
337
|
+
*/
|
|
338
|
+
sensitivity?: number;
|
|
339
|
+
/**
|
|
340
|
+
* Minimum time on page (ms) before exit intent is active
|
|
341
|
+
* Prevents immediate triggers on page load
|
|
342
|
+
* @default 2000
|
|
343
|
+
*/
|
|
344
|
+
minTimeOnPage?: number;
|
|
345
|
+
/**
|
|
346
|
+
* Delay (ms) between detection and trigger
|
|
347
|
+
* @default 0
|
|
348
|
+
*/
|
|
349
|
+
delay?: number;
|
|
350
|
+
/**
|
|
351
|
+
* Number of mouse positions to track for velocity calculation
|
|
352
|
+
* @default 30
|
|
353
|
+
*/
|
|
354
|
+
positionHistorySize?: number;
|
|
355
|
+
/**
|
|
356
|
+
* Disable exit intent on mobile devices
|
|
357
|
+
* @default true
|
|
358
|
+
*/
|
|
359
|
+
disableOnMobile?: boolean;
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Exit Intent Event Payload
|
|
364
|
+
*/
|
|
365
|
+
interface ExitIntentEvent {
|
|
366
|
+
timestamp: number;
|
|
367
|
+
lastY: number;
|
|
368
|
+
previousY: number;
|
|
369
|
+
velocity: number;
|
|
370
|
+
timeOnPage: number;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Exit Intent Plugin API
|
|
374
|
+
*/
|
|
375
|
+
interface ExitIntentPlugin {
|
|
376
|
+
isTriggered(): boolean;
|
|
377
|
+
reset(): void;
|
|
378
|
+
getPositions(): Array<{
|
|
379
|
+
x: number;
|
|
380
|
+
y: number;
|
|
381
|
+
}>;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Exit Intent Plugin
|
|
386
|
+
*
|
|
387
|
+
* Detects when users are about to leave the page by tracking upward mouse movement
|
|
388
|
+
* near the top of the viewport. Inspired by Pathfora's showOnExitIntent.
|
|
389
|
+
*
|
|
390
|
+
* **Event-Driven Architecture:**
|
|
391
|
+
* This plugin emits `trigger:exitIntent` events when exit intent is detected.
|
|
392
|
+
* The core runtime listens for these events and automatically re-evaluates experiences.
|
|
393
|
+
*
|
|
394
|
+
* **Usage Pattern:**
|
|
395
|
+
* Use `targeting.custom` to check if exit intent has triggered:
|
|
396
|
+
*
|
|
397
|
+
* @example Basic usage
|
|
398
|
+
* ```typescript
|
|
399
|
+
* import { init, register } from '@prosdevlab/experience-sdk';
|
|
400
|
+
* import { exitIntentPlugin } from '@prosdevlab/experience-sdk-plugins';
|
|
401
|
+
*
|
|
402
|
+
* init({
|
|
403
|
+
* plugins: [exitIntentPlugin],
|
|
404
|
+
* exitIntent: {
|
|
405
|
+
* sensitivity: 20, // Trigger within 20px of top (default: 50)
|
|
406
|
+
* minTimeOnPage: 2000, // Wait 2s before enabling (default: 2000)
|
|
407
|
+
* delay: 0, // Delay after trigger (default: 0)
|
|
408
|
+
* disableOnMobile: true // Disable on mobile (default: true)
|
|
409
|
+
* }
|
|
410
|
+
* });
|
|
411
|
+
*
|
|
412
|
+
* // Show banner only when exit intent is detected
|
|
413
|
+
* register('exit-offer', {
|
|
414
|
+
* type: 'banner',
|
|
415
|
+
* content: {
|
|
416
|
+
* title: 'Wait! Don't leave yet!',
|
|
417
|
+
* message: 'Get 15% off your first order',
|
|
418
|
+
* buttons: [{ text: 'Claim Offer', variant: 'primary' }]
|
|
419
|
+
* },
|
|
420
|
+
* targeting: {
|
|
421
|
+
* custom: (context) => context.triggers?.exitIntent?.triggered === true
|
|
422
|
+
* },
|
|
423
|
+
* frequency: { max: 1, per: 'session' } // Only show once per session
|
|
424
|
+
* });
|
|
425
|
+
* ```
|
|
426
|
+
*
|
|
427
|
+
* @example Combining with other conditions
|
|
428
|
+
* ```typescript
|
|
429
|
+
* // Show exit offer only on shop pages with items in cart
|
|
430
|
+
* register('cart-recovery', {
|
|
431
|
+
* type: 'banner',
|
|
432
|
+
* content: { message: 'Complete your purchase and save!' },
|
|
433
|
+
* targeting: {
|
|
434
|
+
* url: { contains: '/shop' },
|
|
435
|
+
* custom: (context) => {
|
|
436
|
+
* return (
|
|
437
|
+
* context.triggers?.exitIntent?.triggered === true &&
|
|
438
|
+
* getCart().items.length > 0
|
|
439
|
+
* );
|
|
440
|
+
* }
|
|
441
|
+
* }
|
|
442
|
+
* });
|
|
443
|
+
* ```
|
|
444
|
+
*
|
|
445
|
+
* @example Combining multiple triggers (exit intent + scroll depth)
|
|
446
|
+
* ```typescript
|
|
447
|
+
* // Show offer on exit intent OR after 70% scroll
|
|
448
|
+
* register('engaged-exit', {
|
|
449
|
+
* type: 'banner',
|
|
450
|
+
* content: { message: 'You're almost there!' },
|
|
451
|
+
* targeting: {
|
|
452
|
+
* custom: (context) => {
|
|
453
|
+
* const exitIntent = context.triggers?.exitIntent?.triggered;
|
|
454
|
+
* const scrolled = (context.triggers?.scrollDepth?.percent || 0) >= 70;
|
|
455
|
+
* return exitIntent || scrolled;
|
|
456
|
+
* }
|
|
457
|
+
* }
|
|
458
|
+
* });
|
|
459
|
+
* ```
|
|
460
|
+
*/
|
|
461
|
+
declare const exitIntentPlugin: PluginFunction;
|
|
462
|
+
|
|
195
463
|
/**
|
|
196
464
|
* Frequency Capping Plugin
|
|
197
465
|
*
|
|
@@ -227,4 +495,514 @@ interface FrequencyPlugin {
|
|
|
227
495
|
*/
|
|
228
496
|
declare const frequencyPlugin: PluginFunction;
|
|
229
497
|
|
|
230
|
-
|
|
498
|
+
/**
|
|
499
|
+
* Inline Plugin
|
|
500
|
+
*
|
|
501
|
+
* Embeds experiences directly within page content using DOM selectors.
|
|
502
|
+
* Supports multiple insertion positions and dismissal with persistence.
|
|
503
|
+
*/
|
|
504
|
+
declare const inlinePlugin: (plugin: any, instance: SDK, config: any) => void;
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Insert content into a target element using specified position
|
|
508
|
+
*
|
|
509
|
+
* @param selector - CSS selector for target element
|
|
510
|
+
* @param content - HTML content to insert
|
|
511
|
+
* @param position - Where to insert the content
|
|
512
|
+
* @param experienceId - Unique identifier for the experience
|
|
513
|
+
* @returns The created wrapper element, or null if target not found
|
|
514
|
+
*/
|
|
515
|
+
declare function insertContent(selector: string, content: string, position: InsertionPosition, experienceId: string): HTMLElement | null;
|
|
516
|
+
/**
|
|
517
|
+
* Remove inline content by experience ID
|
|
518
|
+
*
|
|
519
|
+
* @param experienceId - Unique identifier for the experience
|
|
520
|
+
* @returns True if element was found and removed, false otherwise
|
|
521
|
+
*/
|
|
522
|
+
declare function removeContent(experienceId: string): boolean;
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Modal Plugin for @prosdevlab/experience-sdk
|
|
526
|
+
*
|
|
527
|
+
* Renders experiences as accessible modal dialogs with:
|
|
528
|
+
* - Focus trap and keyboard handling
|
|
529
|
+
* - ARIA attributes for screen readers
|
|
530
|
+
* - Backdrop and close button
|
|
531
|
+
* - Responsive design
|
|
532
|
+
*/
|
|
533
|
+
declare const modalPlugin: (plugin: any, instance: SDK) => void;
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Page Visits Plugin Types
|
|
537
|
+
*
|
|
538
|
+
* Generic page visit tracking for any SDK built on sdk-kit.
|
|
539
|
+
* Tracks session and lifetime visit counts with first-visit detection.
|
|
540
|
+
*/
|
|
541
|
+
/**
|
|
542
|
+
* Page visits plugin configuration
|
|
543
|
+
*/
|
|
544
|
+
interface PageVisitsPluginConfig {
|
|
545
|
+
pageVisits?: {
|
|
546
|
+
/**
|
|
547
|
+
* Enable/disable page visit tracking
|
|
548
|
+
* @default true
|
|
549
|
+
*/
|
|
550
|
+
enabled?: boolean;
|
|
551
|
+
/**
|
|
552
|
+
* Honor Do Not Track browser setting
|
|
553
|
+
* @default true
|
|
554
|
+
*/
|
|
555
|
+
respectDNT?: boolean;
|
|
556
|
+
/**
|
|
557
|
+
* Storage key for session count
|
|
558
|
+
* @default 'pageVisits:session'
|
|
559
|
+
*/
|
|
560
|
+
sessionKey?: string;
|
|
561
|
+
/**
|
|
562
|
+
* Storage key for lifetime data
|
|
563
|
+
* @default 'pageVisits:total'
|
|
564
|
+
*/
|
|
565
|
+
totalKey?: string;
|
|
566
|
+
/**
|
|
567
|
+
* TTL for lifetime data in seconds (GDPR compliance)
|
|
568
|
+
* @default undefined (no expiration)
|
|
569
|
+
*/
|
|
570
|
+
ttl?: number;
|
|
571
|
+
/**
|
|
572
|
+
* Automatically increment on plugin load
|
|
573
|
+
* @default true
|
|
574
|
+
*/
|
|
575
|
+
autoIncrement?: boolean;
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Page visits event payload
|
|
580
|
+
*/
|
|
581
|
+
interface PageVisitsEvent {
|
|
582
|
+
/** Whether this is the user's first visit ever */
|
|
583
|
+
isFirstVisit: boolean;
|
|
584
|
+
/** Total visits across all sessions (lifetime) */
|
|
585
|
+
totalVisits: number;
|
|
586
|
+
/** Visits in current session */
|
|
587
|
+
sessionVisits: number;
|
|
588
|
+
/** Timestamp of first visit (unix ms) */
|
|
589
|
+
firstVisitTime?: number;
|
|
590
|
+
/** Timestamp of last visit (unix ms) */
|
|
591
|
+
lastVisitTime?: number;
|
|
592
|
+
/** Timestamp of current visit (unix ms) */
|
|
593
|
+
timestamp: number;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Page visits plugin API
|
|
597
|
+
*/
|
|
598
|
+
interface PageVisitsPlugin {
|
|
599
|
+
/**
|
|
600
|
+
* Get total visit count (lifetime)
|
|
601
|
+
*/
|
|
602
|
+
getTotalCount(): number;
|
|
603
|
+
/**
|
|
604
|
+
* Get session visit count
|
|
605
|
+
*/
|
|
606
|
+
getSessionCount(): number;
|
|
607
|
+
/**
|
|
608
|
+
* Check if this is the first visit
|
|
609
|
+
*/
|
|
610
|
+
isFirstVisit(): boolean;
|
|
611
|
+
/**
|
|
612
|
+
* Get timestamp of first visit
|
|
613
|
+
*/
|
|
614
|
+
getFirstVisitTime(): number | undefined;
|
|
615
|
+
/**
|
|
616
|
+
* Get timestamp of last visit
|
|
617
|
+
*/
|
|
618
|
+
getLastVisitTime(): number | undefined;
|
|
619
|
+
/**
|
|
620
|
+
* Manually increment page visit
|
|
621
|
+
* (useful if autoIncrement is disabled)
|
|
622
|
+
*/
|
|
623
|
+
increment(): void;
|
|
624
|
+
/**
|
|
625
|
+
* Reset all counters and data
|
|
626
|
+
* (useful for testing or user opt-out)
|
|
627
|
+
*/
|
|
628
|
+
reset(): void;
|
|
629
|
+
/**
|
|
630
|
+
* Get full page visits state
|
|
631
|
+
*/
|
|
632
|
+
getState(): PageVisitsEvent;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Page Visits Plugin
|
|
637
|
+
*
|
|
638
|
+
* Generic page visit tracking for any SDK built on sdk-kit.
|
|
639
|
+
*
|
|
640
|
+
* Features:
|
|
641
|
+
* - Session-scoped counter (sessionStorage)
|
|
642
|
+
* - Lifetime counter with timestamps (localStorage)
|
|
643
|
+
* - First-visit detection
|
|
644
|
+
* - DNT (Do Not Track) support
|
|
645
|
+
* - GDPR-compliant expiration
|
|
646
|
+
* - Auto-loads storage plugin if missing
|
|
647
|
+
*
|
|
648
|
+
* Events emitted:
|
|
649
|
+
* - 'pageVisits:incremented' with PageVisitsEvent
|
|
650
|
+
* - 'pageVisits:reset'
|
|
651
|
+
* - 'pageVisits:disabled' with { reason: 'dnt' | 'config' }
|
|
652
|
+
*
|
|
653
|
+
* @example
|
|
654
|
+
* ```typescript
|
|
655
|
+
* import { SDK } from '@lytics/sdk-kit';
|
|
656
|
+
* import { storagePlugin, pageVisitsPlugin } from '@lytics/sdk-kit-plugins';
|
|
657
|
+
*
|
|
658
|
+
* const sdk = new SDK({
|
|
659
|
+
* pageVisits: {
|
|
660
|
+
* enabled: true,
|
|
661
|
+
* respectDNT: true,
|
|
662
|
+
* ttl: 31536000 // 1 year
|
|
663
|
+
* }
|
|
664
|
+
* });
|
|
665
|
+
*
|
|
666
|
+
* sdk.use(storagePlugin);
|
|
667
|
+
* sdk.use(pageVisitsPlugin);
|
|
668
|
+
*
|
|
669
|
+
* // Listen to visit events
|
|
670
|
+
* sdk.on('pageVisits:incremented', (event) => {
|
|
671
|
+
* console.log('Visit count:', event.totalVisits);
|
|
672
|
+
* if (event.isFirstVisit) {
|
|
673
|
+
* console.log('Welcome, first-time visitor!');
|
|
674
|
+
* }
|
|
675
|
+
* });
|
|
676
|
+
*
|
|
677
|
+
* // API methods
|
|
678
|
+
* console.log(sdk.pageVisits.getTotalCount()); // 5
|
|
679
|
+
* console.log(sdk.pageVisits.getSessionCount()); // 2
|
|
680
|
+
* console.log(sdk.pageVisits.isFirstVisit()); // false
|
|
681
|
+
* ```
|
|
682
|
+
*/
|
|
683
|
+
|
|
684
|
+
declare const pageVisitsPlugin: PluginFunction;
|
|
685
|
+
|
|
686
|
+
/** @module scrollDepthPlugin */
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Scroll Depth Plugin
|
|
690
|
+
*
|
|
691
|
+
* Tracks scroll depth and emits `trigger:scrollDepth` events when thresholds are crossed.
|
|
692
|
+
*
|
|
693
|
+
* ## How It Works
|
|
694
|
+
*
|
|
695
|
+
* 1. **Detection**: Listens to `scroll` events (throttled)
|
|
696
|
+
* 2. **Calculation**: Calculates current scroll percentage
|
|
697
|
+
* 3. **Tracking**: Tracks maximum scroll depth and threshold crossings
|
|
698
|
+
* 4. **Emission**: Emits `trigger:scrollDepth` events when thresholds are crossed
|
|
699
|
+
*
|
|
700
|
+
* ## Configuration
|
|
701
|
+
*
|
|
702
|
+
* ```typescript
|
|
703
|
+
* init({
|
|
704
|
+
* scrollDepth: {
|
|
705
|
+
* thresholds: [25, 50, 75, 100], // Percentages to track
|
|
706
|
+
* throttle: 100, // Throttle interval (ms)
|
|
707
|
+
* includeViewportHeight: true, // Calculation method
|
|
708
|
+
* recalculateOnResize: true // Recalculate on resize
|
|
709
|
+
* }
|
|
710
|
+
* });
|
|
711
|
+
* ```
|
|
712
|
+
*
|
|
713
|
+
* ## Experience Targeting
|
|
714
|
+
*
|
|
715
|
+
* ```typescript
|
|
716
|
+
* register('mid-article-cta', {
|
|
717
|
+
* type: 'banner',
|
|
718
|
+
* content: { message: 'Enjoying the article?' },
|
|
719
|
+
* targeting: {
|
|
720
|
+
* custom: (ctx) => (ctx.triggers?.scrollDepth?.percent || 0) >= 50
|
|
721
|
+
* }
|
|
722
|
+
* });
|
|
723
|
+
* ```
|
|
724
|
+
*
|
|
725
|
+
* ## API Methods
|
|
726
|
+
*
|
|
727
|
+
* ```typescript
|
|
728
|
+
* // Get maximum scroll percentage reached
|
|
729
|
+
* instance.scrollDepth.getMaxPercent(); // 73
|
|
730
|
+
*
|
|
731
|
+
* // Get current scroll percentage
|
|
732
|
+
* instance.scrollDepth.getCurrentPercent(); // 50
|
|
733
|
+
*
|
|
734
|
+
* // Get all crossed thresholds
|
|
735
|
+
* instance.scrollDepth.getThresholdsCrossed(); // [25, 50]
|
|
736
|
+
*
|
|
737
|
+
* // Reset tracking (useful for testing)
|
|
738
|
+
* instance.scrollDepth.reset();
|
|
739
|
+
* ```
|
|
740
|
+
*
|
|
741
|
+
* @param plugin Plugin interface from sdk-kit
|
|
742
|
+
* @param instance SDK instance
|
|
743
|
+
* @param config SDK configuration
|
|
744
|
+
*/
|
|
745
|
+
declare const scrollDepthPlugin: PluginFunction;
|
|
746
|
+
|
|
747
|
+
/** @module scrollDepthPlugin */
|
|
748
|
+
/**
|
|
749
|
+
* Scroll Depth Plugin API
|
|
750
|
+
*/
|
|
751
|
+
interface ScrollDepthPlugin {
|
|
752
|
+
getMaxPercent(): number;
|
|
753
|
+
getCurrentPercent(): number;
|
|
754
|
+
getThresholdsCrossed(): number[];
|
|
755
|
+
getDevice(): 'mobile' | 'tablet' | 'desktop';
|
|
756
|
+
getAdvancedMetrics(): {
|
|
757
|
+
timeOnPage: number;
|
|
758
|
+
directionChanges: number;
|
|
759
|
+
timeScrollingUp: number;
|
|
760
|
+
thresholdTimes: Record<number, number>;
|
|
761
|
+
} | null;
|
|
762
|
+
reset(): void;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Scroll Depth Plugin Configuration
|
|
766
|
+
*
|
|
767
|
+
* Tracks scroll depth and emits trigger:scrollDepth events when thresholds are crossed.
|
|
768
|
+
*/
|
|
769
|
+
interface ScrollDepthPluginConfig {
|
|
770
|
+
scrollDepth?: {
|
|
771
|
+
/**
|
|
772
|
+
* Array of scroll percentage thresholds to track (0-100).
|
|
773
|
+
* When user scrolls past a threshold, a trigger:scrollDepth event is emitted.
|
|
774
|
+
* @default [25, 50, 75, 100]
|
|
775
|
+
* @example [50, 100]
|
|
776
|
+
*/
|
|
777
|
+
thresholds?: number[];
|
|
778
|
+
/**
|
|
779
|
+
* Throttle interval in milliseconds for scroll event handler.
|
|
780
|
+
* Lower values are more responsive but impact performance.
|
|
781
|
+
* @default 100
|
|
782
|
+
* @example 200
|
|
783
|
+
*/
|
|
784
|
+
throttle?: number;
|
|
785
|
+
/**
|
|
786
|
+
* Include viewport height in scroll percentage calculation.
|
|
787
|
+
*
|
|
788
|
+
* - true: (scrollTop + viewportHeight) / totalHeight
|
|
789
|
+
* More intuitive: 100% when bottom of viewport reaches end
|
|
790
|
+
* - false: scrollTop / (totalHeight - viewportHeight)
|
|
791
|
+
* Pathfora's method: 100% when top of viewport reaches end
|
|
792
|
+
*
|
|
793
|
+
* @default true
|
|
794
|
+
*/
|
|
795
|
+
includeViewportHeight?: boolean;
|
|
796
|
+
/**
|
|
797
|
+
* Recalculate scroll on window resize.
|
|
798
|
+
* Useful for responsive layouts where content height changes.
|
|
799
|
+
* @default true
|
|
800
|
+
*/
|
|
801
|
+
recalculateOnResize?: boolean;
|
|
802
|
+
/**
|
|
803
|
+
* Track advanced metrics (velocity, direction, time-to-threshold).
|
|
804
|
+
* Enables advanced engagement quality analysis.
|
|
805
|
+
* Slight performance overhead but provides rich insights.
|
|
806
|
+
* @default false
|
|
807
|
+
*/
|
|
808
|
+
trackAdvancedMetrics?: boolean;
|
|
809
|
+
/**
|
|
810
|
+
* Velocity threshold (px/ms) to consider "fast scrolling".
|
|
811
|
+
* Fast scrolling often indicates skimming rather than reading.
|
|
812
|
+
* Only used when trackAdvancedMetrics is true.
|
|
813
|
+
* @default 3
|
|
814
|
+
*/
|
|
815
|
+
fastScrollVelocityThreshold?: number;
|
|
816
|
+
/**
|
|
817
|
+
* Disable scroll tracking on mobile devices.
|
|
818
|
+
* Useful since mobile scroll behavior differs significantly from desktop.
|
|
819
|
+
* @default false
|
|
820
|
+
*/
|
|
821
|
+
disableOnMobile?: boolean;
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Scroll Depth Event Payload
|
|
826
|
+
*
|
|
827
|
+
* Emitted as `trigger:scrollDepth` when a threshold is crossed.
|
|
828
|
+
*/
|
|
829
|
+
interface ScrollDepthEvent {
|
|
830
|
+
/** Whether the trigger has fired */
|
|
831
|
+
triggered: boolean;
|
|
832
|
+
/** Timestamp when the event was emitted */
|
|
833
|
+
timestamp: number;
|
|
834
|
+
/** Current scroll percentage (0-100) */
|
|
835
|
+
percent: number;
|
|
836
|
+
/** Maximum scroll percentage reached during session */
|
|
837
|
+
maxPercent: number;
|
|
838
|
+
/** The threshold that was just crossed */
|
|
839
|
+
threshold: number;
|
|
840
|
+
/** All thresholds that have been triggered */
|
|
841
|
+
thresholdsCrossed: number[];
|
|
842
|
+
/** Device type (mobile, tablet, desktop) */
|
|
843
|
+
device: 'mobile' | 'tablet' | 'desktop';
|
|
844
|
+
/** Advanced metrics (only present when trackAdvancedMetrics is enabled) */
|
|
845
|
+
advanced?: {
|
|
846
|
+
/** Time in milliseconds to reach this threshold from page load */
|
|
847
|
+
timeToThreshold: number;
|
|
848
|
+
/** Current scroll velocity in pixels per millisecond */
|
|
849
|
+
velocity: number;
|
|
850
|
+
/** Whether user is scrolling fast (indicates skimming) */
|
|
851
|
+
isFastScrolling: boolean;
|
|
852
|
+
/** Number of direction changes (up/down) since last threshold */
|
|
853
|
+
directionChanges: number;
|
|
854
|
+
/** Total time spent scrolling up (indicates seeking behavior) */
|
|
855
|
+
timeScrollingUp: number;
|
|
856
|
+
/** Scroll quality score (0-100, higher = more engaged) */
|
|
857
|
+
engagementScore: number;
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/** @module timeDelayPlugin */
|
|
862
|
+
/**
|
|
863
|
+
* Time Delay Plugin Configuration
|
|
864
|
+
*
|
|
865
|
+
* Tracks time elapsed since SDK initialization and emits trigger:timeDelay events.
|
|
866
|
+
*/
|
|
867
|
+
interface TimeDelayPluginConfig {
|
|
868
|
+
timeDelay?: {
|
|
869
|
+
/**
|
|
870
|
+
* Delay before emitting trigger event (milliseconds).
|
|
871
|
+
* Set to 0 to disable (immediate trigger on init).
|
|
872
|
+
* @default 0
|
|
873
|
+
* @example 5000 // 5 seconds
|
|
874
|
+
*/
|
|
875
|
+
delay?: number;
|
|
876
|
+
/**
|
|
877
|
+
* Pause timer when tab is hidden (Page Visibility API).
|
|
878
|
+
* When true, only counts "active viewing time".
|
|
879
|
+
* When false, timer runs even when tab is hidden.
|
|
880
|
+
* @default true
|
|
881
|
+
*/
|
|
882
|
+
pauseWhenHidden?: boolean;
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Time Delay Event Payload
|
|
887
|
+
*
|
|
888
|
+
* Emitted via 'trigger:timeDelay' when the configured delay is reached.
|
|
889
|
+
*/
|
|
890
|
+
interface TimeDelayEvent {
|
|
891
|
+
/** Timestamp when the trigger event was emitted */
|
|
892
|
+
timestamp: number;
|
|
893
|
+
/** Total elapsed time since init (milliseconds, includes paused time) */
|
|
894
|
+
elapsed: number;
|
|
895
|
+
/** Active elapsed time (milliseconds, excludes time when tab was hidden) */
|
|
896
|
+
activeElapsed: number;
|
|
897
|
+
/** Whether the timer was paused at any point */
|
|
898
|
+
wasPaused: boolean;
|
|
899
|
+
/** Number of times visibility changed (hidden/visible) */
|
|
900
|
+
visibilityChanges: number;
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Time Delay Plugin API
|
|
904
|
+
*/
|
|
905
|
+
interface TimeDelayPlugin {
|
|
906
|
+
/**
|
|
907
|
+
* Get total elapsed time since init (includes paused time)
|
|
908
|
+
* @returns Time in milliseconds
|
|
909
|
+
*/
|
|
910
|
+
getElapsed(): number;
|
|
911
|
+
/**
|
|
912
|
+
* Get active elapsed time (excludes paused time)
|
|
913
|
+
* @returns Time in milliseconds
|
|
914
|
+
*/
|
|
915
|
+
getActiveElapsed(): number;
|
|
916
|
+
/**
|
|
917
|
+
* Get remaining time until trigger
|
|
918
|
+
* @returns Time in milliseconds, or 0 if already triggered
|
|
919
|
+
*/
|
|
920
|
+
getRemaining(): number;
|
|
921
|
+
/**
|
|
922
|
+
* Check if timer is currently paused (tab hidden)
|
|
923
|
+
* @returns True if paused
|
|
924
|
+
*/
|
|
925
|
+
isPaused(): boolean;
|
|
926
|
+
/**
|
|
927
|
+
* Check if trigger has fired
|
|
928
|
+
* @returns True if triggered
|
|
929
|
+
*/
|
|
930
|
+
isTriggered(): boolean;
|
|
931
|
+
/**
|
|
932
|
+
* Reset timer to initial state
|
|
933
|
+
* Clears trigger flag and restarts timing
|
|
934
|
+
*/
|
|
935
|
+
reset(): void;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/** @module timeDelayPlugin */
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Time Delay Plugin
|
|
942
|
+
*
|
|
943
|
+
* Tracks time elapsed since SDK initialization and emits trigger:timeDelay events
|
|
944
|
+
* when the configured delay is reached.
|
|
945
|
+
*
|
|
946
|
+
* **Features:**
|
|
947
|
+
* - Millisecond precision timing
|
|
948
|
+
* - Pause/resume on tab visibility change (optional)
|
|
949
|
+
* - Tracks active vs total elapsed time
|
|
950
|
+
* - Full timer lifecycle management
|
|
951
|
+
*
|
|
952
|
+
* **Event-Driven Architecture:**
|
|
953
|
+
* This plugin emits `trigger:timeDelay` events when the delay threshold is reached.
|
|
954
|
+
* The core runtime listens for these events and automatically re-evaluates experiences.
|
|
955
|
+
*
|
|
956
|
+
* **Usage Pattern:**
|
|
957
|
+
* Use `targeting.custom` to check if time delay has triggered:
|
|
958
|
+
*
|
|
959
|
+
* @example Basic usage
|
|
960
|
+
* ```typescript
|
|
961
|
+
* import { init, register } from '@prosdevlab/experience-sdk';
|
|
962
|
+
*
|
|
963
|
+
* init({
|
|
964
|
+
* timeDelay: {
|
|
965
|
+
* delay: 5000, // 5 seconds
|
|
966
|
+
* pauseWhenHidden: true // Pause when tab hidden (default)
|
|
967
|
+
* }
|
|
968
|
+
* });
|
|
969
|
+
*
|
|
970
|
+
* // Show banner after 5 seconds of active viewing time
|
|
971
|
+
* register('timed-offer', {
|
|
972
|
+
* type: 'banner',
|
|
973
|
+
* content: {
|
|
974
|
+
* message: 'Limited time offer!',
|
|
975
|
+
* buttons: [{ text: 'Claim Now', variant: 'primary' }]
|
|
976
|
+
* },
|
|
977
|
+
* targeting: {
|
|
978
|
+
* custom: (context) => {
|
|
979
|
+
* const active = context.triggers?.timeDelay?.activeElapsed || 0;
|
|
980
|
+
* return active >= 5000;
|
|
981
|
+
* }
|
|
982
|
+
* }
|
|
983
|
+
* });
|
|
984
|
+
* ```
|
|
985
|
+
*
|
|
986
|
+
* @example Combining with other triggers
|
|
987
|
+
* ```typescript
|
|
988
|
+
* // Show after 10s OR on exit intent (whichever comes first)
|
|
989
|
+
* register('engaged-offer', {
|
|
990
|
+
* type: 'banner',
|
|
991
|
+
* content: { message: 'Special offer for engaged users!' },
|
|
992
|
+
* targeting: {
|
|
993
|
+
* custom: (context) => {
|
|
994
|
+
* const timeElapsed = (context.triggers?.timeDelay?.activeElapsed || 0) >= 10000;
|
|
995
|
+
* const exitIntent = context.triggers?.exitIntent?.triggered;
|
|
996
|
+
* return timeElapsed || exitIntent;
|
|
997
|
+
* }
|
|
998
|
+
* }
|
|
999
|
+
* });
|
|
1000
|
+
* ```
|
|
1001
|
+
*
|
|
1002
|
+
* @param plugin Plugin interface from sdk-kit
|
|
1003
|
+
* @param instance SDK instance
|
|
1004
|
+
* @param config SDK configuration
|
|
1005
|
+
*/
|
|
1006
|
+
declare const timeDelayPlugin: PluginFunction;
|
|
1007
|
+
|
|
1008
|
+
export { type BannerContent, type BannerPlugin, type BannerPluginConfig, type DebugPlugin, type DebugPluginConfig, type Decision, type DecisionMetadata, type ExitIntentEvent, type ExitIntentPlugin, type ExitIntentPluginConfig, type Experience, type ExperienceContent, type FormConfig, type FormField, type FormState, type FrequencyPlugin, type FrequencyPluginConfig, type InlineContent$1 as InlineContent, type InlinePlugin, type InlinePluginConfig, type InsertionPosition, type ModalConfig, type ModalContent, type ModalPlugin, type PageVisitsEvent, type PageVisitsPlugin, type PageVisitsPluginConfig, type ScrollDepthEvent, type ScrollDepthPlugin, type ScrollDepthPluginConfig, type TimeDelayEvent, type TimeDelayPlugin, type TimeDelayPluginConfig, type TooltipContent, type TraceStep, type ValidationResult, bannerPlugin, debugPlugin, exitIntentPlugin, frequencyPlugin, inlinePlugin, insertContent, modalPlugin, pageVisitsPlugin, removeContent, scrollDepthPlugin, timeDelayPlugin };
|