@prosdevlab/experience-sdk-plugins 0.1.3 → 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 +56 -0
- package/dist/index.d.ts +626 -2
- package/dist/index.js +799 -49
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/banner/banner.ts +149 -51
- 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,297 @@
|
|
|
1
|
+
/** @module timeDelayPlugin */
|
|
2
|
+
|
|
3
|
+
import type { PluginFunction } from '@lytics/sdk-kit';
|
|
4
|
+
import type { TimeDelayEvent, TimeDelayPlugin, TimeDelayPluginConfig } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Pure function: Calculate elapsed time from start
|
|
8
|
+
*/
|
|
9
|
+
export function calculateElapsed(startTime: number, pausedDuration: number): number {
|
|
10
|
+
return Date.now() - startTime - pausedDuration;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Pure function: Check if document is hidden (Page Visibility API)
|
|
15
|
+
*/
|
|
16
|
+
export function isDocumentHidden(): boolean {
|
|
17
|
+
if (typeof document === 'undefined') return false;
|
|
18
|
+
return document.hidden || false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Pure function: Create time delay event payload
|
|
23
|
+
*/
|
|
24
|
+
export function createTimeDelayEvent(
|
|
25
|
+
startTime: number,
|
|
26
|
+
pausedDuration: number,
|
|
27
|
+
wasPaused: boolean,
|
|
28
|
+
visibilityChanges: number
|
|
29
|
+
): TimeDelayEvent {
|
|
30
|
+
const timestamp = Date.now();
|
|
31
|
+
const elapsed = timestamp - startTime;
|
|
32
|
+
const activeElapsed = elapsed - pausedDuration;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
timestamp,
|
|
36
|
+
elapsed,
|
|
37
|
+
activeElapsed,
|
|
38
|
+
wasPaused,
|
|
39
|
+
visibilityChanges,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Time Delay Plugin
|
|
45
|
+
*
|
|
46
|
+
* Tracks time elapsed since SDK initialization and emits trigger:timeDelay events
|
|
47
|
+
* when the configured delay is reached.
|
|
48
|
+
*
|
|
49
|
+
* **Features:**
|
|
50
|
+
* - Millisecond precision timing
|
|
51
|
+
* - Pause/resume on tab visibility change (optional)
|
|
52
|
+
* - Tracks active vs total elapsed time
|
|
53
|
+
* - Full timer lifecycle management
|
|
54
|
+
*
|
|
55
|
+
* **Event-Driven Architecture:**
|
|
56
|
+
* This plugin emits `trigger:timeDelay` events when the delay threshold is reached.
|
|
57
|
+
* The core runtime listens for these events and automatically re-evaluates experiences.
|
|
58
|
+
*
|
|
59
|
+
* **Usage Pattern:**
|
|
60
|
+
* Use `targeting.custom` to check if time delay has triggered:
|
|
61
|
+
*
|
|
62
|
+
* @example Basic usage
|
|
63
|
+
* ```typescript
|
|
64
|
+
* import { init, register } from '@prosdevlab/experience-sdk';
|
|
65
|
+
*
|
|
66
|
+
* init({
|
|
67
|
+
* timeDelay: {
|
|
68
|
+
* delay: 5000, // 5 seconds
|
|
69
|
+
* pauseWhenHidden: true // Pause when tab hidden (default)
|
|
70
|
+
* }
|
|
71
|
+
* });
|
|
72
|
+
*
|
|
73
|
+
* // Show banner after 5 seconds of active viewing time
|
|
74
|
+
* register('timed-offer', {
|
|
75
|
+
* type: 'banner',
|
|
76
|
+
* content: {
|
|
77
|
+
* message: 'Limited time offer!',
|
|
78
|
+
* buttons: [{ text: 'Claim Now', variant: 'primary' }]
|
|
79
|
+
* },
|
|
80
|
+
* targeting: {
|
|
81
|
+
* custom: (context) => {
|
|
82
|
+
* const active = context.triggers?.timeDelay?.activeElapsed || 0;
|
|
83
|
+
* return active >= 5000;
|
|
84
|
+
* }
|
|
85
|
+
* }
|
|
86
|
+
* });
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* @example Combining with other triggers
|
|
90
|
+
* ```typescript
|
|
91
|
+
* // Show after 10s OR on exit intent (whichever comes first)
|
|
92
|
+
* register('engaged-offer', {
|
|
93
|
+
* type: 'banner',
|
|
94
|
+
* content: { message: 'Special offer for engaged users!' },
|
|
95
|
+
* targeting: {
|
|
96
|
+
* custom: (context) => {
|
|
97
|
+
* const timeElapsed = (context.triggers?.timeDelay?.activeElapsed || 0) >= 10000;
|
|
98
|
+
* const exitIntent = context.triggers?.exitIntent?.triggered;
|
|
99
|
+
* return timeElapsed || exitIntent;
|
|
100
|
+
* }
|
|
101
|
+
* }
|
|
102
|
+
* });
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* @param plugin Plugin interface from sdk-kit
|
|
106
|
+
* @param instance SDK instance
|
|
107
|
+
* @param config SDK configuration
|
|
108
|
+
*/
|
|
109
|
+
export const timeDelayPlugin: PluginFunction = (plugin, instance, config) => {
|
|
110
|
+
plugin.ns('experiences.timeDelay');
|
|
111
|
+
|
|
112
|
+
// Set defaults
|
|
113
|
+
plugin.defaults({
|
|
114
|
+
timeDelay: {
|
|
115
|
+
delay: 0,
|
|
116
|
+
pauseWhenHidden: true,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Get config
|
|
121
|
+
const timeDelayConfig = config.get('timeDelay') as TimeDelayPluginConfig['timeDelay'];
|
|
122
|
+
if (!timeDelayConfig) return;
|
|
123
|
+
|
|
124
|
+
const delay = timeDelayConfig.delay ?? 0;
|
|
125
|
+
const pauseWhenHidden = timeDelayConfig.pauseWhenHidden ?? true;
|
|
126
|
+
|
|
127
|
+
// Skip if delay is 0 (disabled)
|
|
128
|
+
if (delay <= 0) return;
|
|
129
|
+
|
|
130
|
+
// State
|
|
131
|
+
const startTime = Date.now();
|
|
132
|
+
let triggered = false;
|
|
133
|
+
let paused = false;
|
|
134
|
+
let pausedDuration = 0;
|
|
135
|
+
let lastPauseTime = 0;
|
|
136
|
+
let visibilityChanges = 0;
|
|
137
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
138
|
+
let visibilityListener: (() => void) | null = null;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Trigger the time delay event
|
|
142
|
+
*/
|
|
143
|
+
function trigger(): void {
|
|
144
|
+
if (triggered) return;
|
|
145
|
+
|
|
146
|
+
triggered = true;
|
|
147
|
+
|
|
148
|
+
// Create event payload using pure function
|
|
149
|
+
const eventPayload = createTimeDelayEvent(
|
|
150
|
+
startTime,
|
|
151
|
+
pausedDuration,
|
|
152
|
+
visibilityChanges > 0,
|
|
153
|
+
visibilityChanges
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Emit trigger event
|
|
157
|
+
instance.emit('trigger:timeDelay', eventPayload);
|
|
158
|
+
|
|
159
|
+
// Cleanup
|
|
160
|
+
cleanup();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Schedule timer with remaining delay
|
|
165
|
+
*/
|
|
166
|
+
function scheduleTimer(remainingDelay: number): void {
|
|
167
|
+
if (timer) {
|
|
168
|
+
clearTimeout(timer);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
timer = setTimeout(() => {
|
|
172
|
+
trigger();
|
|
173
|
+
}, remainingDelay);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Handle visibility change
|
|
178
|
+
*/
|
|
179
|
+
function handleVisibilityChange(): void {
|
|
180
|
+
const hidden = isDocumentHidden();
|
|
181
|
+
|
|
182
|
+
if (hidden && !paused) {
|
|
183
|
+
// Tab just became hidden - pause timer
|
|
184
|
+
paused = true;
|
|
185
|
+
lastPauseTime = Date.now();
|
|
186
|
+
visibilityChanges++;
|
|
187
|
+
|
|
188
|
+
// Clear existing timer
|
|
189
|
+
if (timer) {
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
timer = null;
|
|
192
|
+
}
|
|
193
|
+
} else if (!hidden && paused) {
|
|
194
|
+
// Tab just became visible - resume timer
|
|
195
|
+
paused = false;
|
|
196
|
+
const pauseDuration = Date.now() - lastPauseTime;
|
|
197
|
+
pausedDuration += pauseDuration;
|
|
198
|
+
visibilityChanges++;
|
|
199
|
+
|
|
200
|
+
// Calculate remaining delay
|
|
201
|
+
const elapsed = calculateElapsed(startTime, pausedDuration);
|
|
202
|
+
const remaining = delay - elapsed;
|
|
203
|
+
|
|
204
|
+
if (remaining > 0) {
|
|
205
|
+
scheduleTimer(remaining);
|
|
206
|
+
} else {
|
|
207
|
+
// Delay already elapsed during pause
|
|
208
|
+
trigger();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Cleanup listeners and timers
|
|
215
|
+
*/
|
|
216
|
+
function cleanup(): void {
|
|
217
|
+
if (timer) {
|
|
218
|
+
clearTimeout(timer);
|
|
219
|
+
timer = null;
|
|
220
|
+
}
|
|
221
|
+
if (visibilityListener && typeof document !== 'undefined') {
|
|
222
|
+
document.removeEventListener('visibilitychange', visibilityListener);
|
|
223
|
+
visibilityListener = null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Initialize timer and visibility listener
|
|
229
|
+
*/
|
|
230
|
+
function initialize(): void {
|
|
231
|
+
// Check if already hidden on init
|
|
232
|
+
if (pauseWhenHidden && isDocumentHidden()) {
|
|
233
|
+
paused = true;
|
|
234
|
+
lastPauseTime = Date.now();
|
|
235
|
+
visibilityChanges++;
|
|
236
|
+
} else {
|
|
237
|
+
// Start timer
|
|
238
|
+
scheduleTimer(delay);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Setup visibility listener if pause is enabled
|
|
242
|
+
if (pauseWhenHidden && typeof document !== 'undefined') {
|
|
243
|
+
visibilityListener = handleVisibilityChange;
|
|
244
|
+
document.addEventListener('visibilitychange', visibilityListener);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Expose API
|
|
249
|
+
plugin.expose({
|
|
250
|
+
timeDelay: {
|
|
251
|
+
getElapsed: () => {
|
|
252
|
+
return Date.now() - startTime;
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
getActiveElapsed: () => {
|
|
256
|
+
let currentPausedDuration = pausedDuration;
|
|
257
|
+
if (paused) {
|
|
258
|
+
// Add current pause duration
|
|
259
|
+
currentPausedDuration += Date.now() - lastPauseTime;
|
|
260
|
+
}
|
|
261
|
+
return calculateElapsed(startTime, currentPausedDuration);
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
getRemaining: () => {
|
|
265
|
+
if (triggered) return 0;
|
|
266
|
+
|
|
267
|
+
const elapsed = calculateElapsed(startTime, pausedDuration);
|
|
268
|
+
const remaining = delay - elapsed;
|
|
269
|
+
return Math.max(0, remaining);
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
isPaused: () => paused,
|
|
273
|
+
|
|
274
|
+
isTriggered: () => triggered,
|
|
275
|
+
|
|
276
|
+
reset: () => {
|
|
277
|
+
triggered = false;
|
|
278
|
+
paused = false;
|
|
279
|
+
pausedDuration = 0;
|
|
280
|
+
lastPauseTime = 0;
|
|
281
|
+
visibilityChanges = 0;
|
|
282
|
+
|
|
283
|
+
cleanup();
|
|
284
|
+
initialize();
|
|
285
|
+
},
|
|
286
|
+
} satisfies TimeDelayPlugin,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Initialize on plugin load
|
|
290
|
+
initialize();
|
|
291
|
+
|
|
292
|
+
// Cleanup on instance destroy
|
|
293
|
+
const destroyHandler = () => {
|
|
294
|
+
cleanup();
|
|
295
|
+
};
|
|
296
|
+
instance.on('destroy', destroyHandler);
|
|
297
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** @module timeDelayPlugin */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Time Delay Plugin Configuration
|
|
5
|
+
*
|
|
6
|
+
* Tracks time elapsed since SDK initialization and emits trigger:timeDelay events.
|
|
7
|
+
*/
|
|
8
|
+
export interface TimeDelayPluginConfig {
|
|
9
|
+
timeDelay?: {
|
|
10
|
+
/**
|
|
11
|
+
* Delay before emitting trigger event (milliseconds).
|
|
12
|
+
* Set to 0 to disable (immediate trigger on init).
|
|
13
|
+
* @default 0
|
|
14
|
+
* @example 5000 // 5 seconds
|
|
15
|
+
*/
|
|
16
|
+
delay?: number;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Pause timer when tab is hidden (Page Visibility API).
|
|
20
|
+
* When true, only counts "active viewing time".
|
|
21
|
+
* When false, timer runs even when tab is hidden.
|
|
22
|
+
* @default true
|
|
23
|
+
*/
|
|
24
|
+
pauseWhenHidden?: boolean;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Time Delay Event Payload
|
|
30
|
+
*
|
|
31
|
+
* Emitted via 'trigger:timeDelay' when the configured delay is reached.
|
|
32
|
+
*/
|
|
33
|
+
export interface TimeDelayEvent {
|
|
34
|
+
/** Timestamp when the trigger event was emitted */
|
|
35
|
+
timestamp: number;
|
|
36
|
+
|
|
37
|
+
/** Total elapsed time since init (milliseconds, includes paused time) */
|
|
38
|
+
elapsed: number;
|
|
39
|
+
|
|
40
|
+
/** Active elapsed time (milliseconds, excludes time when tab was hidden) */
|
|
41
|
+
activeElapsed: number;
|
|
42
|
+
|
|
43
|
+
/** Whether the timer was paused at any point */
|
|
44
|
+
wasPaused: boolean;
|
|
45
|
+
|
|
46
|
+
/** Number of times visibility changed (hidden/visible) */
|
|
47
|
+
visibilityChanges: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Time Delay Plugin API
|
|
52
|
+
*/
|
|
53
|
+
export interface TimeDelayPlugin {
|
|
54
|
+
/**
|
|
55
|
+
* Get total elapsed time since init (includes paused time)
|
|
56
|
+
* @returns Time in milliseconds
|
|
57
|
+
*/
|
|
58
|
+
getElapsed(): number;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get active elapsed time (excludes paused time)
|
|
62
|
+
* @returns Time in milliseconds
|
|
63
|
+
*/
|
|
64
|
+
getActiveElapsed(): number;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get remaining time until trigger
|
|
68
|
+
* @returns Time in milliseconds, or 0 if already triggered
|
|
69
|
+
*/
|
|
70
|
+
getRemaining(): number;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if timer is currently paused (tab hidden)
|
|
74
|
+
* @returns True if paused
|
|
75
|
+
*/
|
|
76
|
+
isPaused(): boolean;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if trigger has fired
|
|
80
|
+
* @returns True if triggered
|
|
81
|
+
*/
|
|
82
|
+
isTriggered(): boolean;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Reset timer to initial state
|
|
86
|
+
* Clears trigger flag and restarts timing
|
|
87
|
+
*/
|
|
88
|
+
reset(): void;
|
|
89
|
+
}
|
package/src/utils/sanitize.ts
CHANGED