@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/dist/index.d.ts CHANGED
@@ -117,6 +117,7 @@ interface BannerPluginConfig {
117
117
  position?: 'top' | 'bottom';
118
118
  dismissable?: boolean;
119
119
  zIndex?: number;
120
+ pushDown?: string;
120
121
  };
121
122
  }
122
123
  interface BannerPlugin {
@@ -134,7 +135,23 @@ interface BannerPlugin {
134
135
  * import { createInstance } from '@prosdevlab/experience-sdk';
135
136
  * import { bannerPlugin } from '@prosdevlab/experience-sdk-plugins';
136
137
  *
137
- * const sdk = createInstance({ banner: { position: 'top', dismissable: true } });
138
+ * // Basic usage (banner overlays at top)
139
+ * const sdk = createInstance({
140
+ * banner: {
141
+ * position: 'top',
142
+ * dismissable: true
143
+ * }
144
+ * });
145
+ * sdk.use(bannerPlugin);
146
+ *
147
+ * // With pushDown (pushes navigation down instead of overlaying)
148
+ * const sdk = createInstance({
149
+ * banner: {
150
+ * position: 'top',
151
+ * dismissable: true,
152
+ * pushDown: 'header' // CSS selector of element to push down
153
+ * }
154
+ * });
138
155
  * sdk.use(bannerPlugin);
139
156
  * ```
140
157
  */
@@ -175,6 +192,140 @@ interface DebugPlugin {
175
192
  */
176
193
  declare const debugPlugin: PluginFunction;
177
194
 
195
+ /**
196
+ * Exit Intent Plugin Configuration
197
+ */
198
+ interface ExitIntentPluginConfig {
199
+ exitIntent?: {
200
+ /**
201
+ * Maximum Y position (px) where exit intent can trigger
202
+ * @default 50
203
+ */
204
+ sensitivity?: number;
205
+ /**
206
+ * Minimum time on page (ms) before exit intent is active
207
+ * Prevents immediate triggers on page load
208
+ * @default 2000
209
+ */
210
+ minTimeOnPage?: number;
211
+ /**
212
+ * Delay (ms) between detection and trigger
213
+ * @default 0
214
+ */
215
+ delay?: number;
216
+ /**
217
+ * Number of mouse positions to track for velocity calculation
218
+ * @default 30
219
+ */
220
+ positionHistorySize?: number;
221
+ /**
222
+ * Disable exit intent on mobile devices
223
+ * @default true
224
+ */
225
+ disableOnMobile?: boolean;
226
+ };
227
+ }
228
+ /**
229
+ * Exit Intent Event Payload
230
+ */
231
+ interface ExitIntentEvent {
232
+ timestamp: number;
233
+ lastY: number;
234
+ previousY: number;
235
+ velocity: number;
236
+ timeOnPage: number;
237
+ }
238
+ /**
239
+ * Exit Intent Plugin API
240
+ */
241
+ interface ExitIntentPlugin {
242
+ isTriggered(): boolean;
243
+ reset(): void;
244
+ getPositions(): Array<{
245
+ x: number;
246
+ y: number;
247
+ }>;
248
+ }
249
+
250
+ /**
251
+ * Exit Intent Plugin
252
+ *
253
+ * Detects when users are about to leave the page by tracking upward mouse movement
254
+ * near the top of the viewport. Inspired by Pathfora's showOnExitIntent.
255
+ *
256
+ * **Event-Driven Architecture:**
257
+ * This plugin emits `trigger:exitIntent` events when exit intent is detected.
258
+ * The core runtime listens for these events and automatically re-evaluates experiences.
259
+ *
260
+ * **Usage Pattern:**
261
+ * Use `targeting.custom` to check if exit intent has triggered:
262
+ *
263
+ * @example Basic usage
264
+ * ```typescript
265
+ * import { init, register } from '@prosdevlab/experience-sdk';
266
+ * import { exitIntentPlugin } from '@prosdevlab/experience-sdk-plugins';
267
+ *
268
+ * init({
269
+ * plugins: [exitIntentPlugin],
270
+ * exitIntent: {
271
+ * sensitivity: 20, // Trigger within 20px of top (default: 50)
272
+ * minTimeOnPage: 2000, // Wait 2s before enabling (default: 2000)
273
+ * delay: 0, // Delay after trigger (default: 0)
274
+ * disableOnMobile: true // Disable on mobile (default: true)
275
+ * }
276
+ * });
277
+ *
278
+ * // Show banner only when exit intent is detected
279
+ * register('exit-offer', {
280
+ * type: 'banner',
281
+ * content: {
282
+ * title: 'Wait! Don't leave yet!',
283
+ * message: 'Get 15% off your first order',
284
+ * buttons: [{ text: 'Claim Offer', variant: 'primary' }]
285
+ * },
286
+ * targeting: {
287
+ * custom: (context) => context.triggers?.exitIntent?.triggered === true
288
+ * },
289
+ * frequency: { max: 1, per: 'session' } // Only show once per session
290
+ * });
291
+ * ```
292
+ *
293
+ * @example Combining with other conditions
294
+ * ```typescript
295
+ * // Show exit offer only on shop pages with items in cart
296
+ * register('cart-recovery', {
297
+ * type: 'banner',
298
+ * content: { message: 'Complete your purchase and save!' },
299
+ * targeting: {
300
+ * url: { contains: '/shop' },
301
+ * custom: (context) => {
302
+ * return (
303
+ * context.triggers?.exitIntent?.triggered === true &&
304
+ * getCart().items.length > 0
305
+ * );
306
+ * }
307
+ * }
308
+ * });
309
+ * ```
310
+ *
311
+ * @example Combining multiple triggers (exit intent + scroll depth)
312
+ * ```typescript
313
+ * // Show offer on exit intent OR after 70% scroll
314
+ * register('engaged-exit', {
315
+ * type: 'banner',
316
+ * content: { message: 'You're almost there!' },
317
+ * targeting: {
318
+ * custom: (context) => {
319
+ * const exitIntent = context.triggers?.exitIntent?.triggered;
320
+ * const scrolled = (context.triggers?.scrollDepth?.percent || 0) >= 70;
321
+ * return exitIntent || scrolled;
322
+ * }
323
+ * }
324
+ * });
325
+ * ```
326
+ */
327
+ declare const exitIntentPlugin: PluginFunction;
328
+
178
329
  /**
179
330
  * Frequency Capping Plugin
180
331
  *
@@ -210,4 +361,477 @@ interface FrequencyPlugin {
210
361
  */
211
362
  declare const frequencyPlugin: PluginFunction;
212
363
 
213
- export { type BannerContent, type BannerPlugin, type BannerPluginConfig, type DebugPlugin, type DebugPluginConfig, type Decision, type DecisionMetadata, type Experience, type ExperienceContent, type FrequencyPlugin, type FrequencyPluginConfig, type ModalContent, type TooltipContent, type TraceStep, bannerPlugin, debugPlugin, frequencyPlugin };
364
+ /**
365
+ * Page Visits Plugin Types
366
+ *
367
+ * Generic page visit tracking for any SDK built on sdk-kit.
368
+ * Tracks session and lifetime visit counts with first-visit detection.
369
+ */
370
+ /**
371
+ * Page visits plugin configuration
372
+ */
373
+ interface PageVisitsPluginConfig {
374
+ pageVisits?: {
375
+ /**
376
+ * Enable/disable page visit tracking
377
+ * @default true
378
+ */
379
+ enabled?: boolean;
380
+ /**
381
+ * Honor Do Not Track browser setting
382
+ * @default true
383
+ */
384
+ respectDNT?: boolean;
385
+ /**
386
+ * Storage key for session count
387
+ * @default 'pageVisits:session'
388
+ */
389
+ sessionKey?: string;
390
+ /**
391
+ * Storage key for lifetime data
392
+ * @default 'pageVisits:total'
393
+ */
394
+ totalKey?: string;
395
+ /**
396
+ * TTL for lifetime data in seconds (GDPR compliance)
397
+ * @default undefined (no expiration)
398
+ */
399
+ ttl?: number;
400
+ /**
401
+ * Automatically increment on plugin load
402
+ * @default true
403
+ */
404
+ autoIncrement?: boolean;
405
+ };
406
+ }
407
+ /**
408
+ * Page visits event payload
409
+ */
410
+ interface PageVisitsEvent {
411
+ /** Whether this is the user's first visit ever */
412
+ isFirstVisit: boolean;
413
+ /** Total visits across all sessions (lifetime) */
414
+ totalVisits: number;
415
+ /** Visits in current session */
416
+ sessionVisits: number;
417
+ /** Timestamp of first visit (unix ms) */
418
+ firstVisitTime?: number;
419
+ /** Timestamp of last visit (unix ms) */
420
+ lastVisitTime?: number;
421
+ /** Timestamp of current visit (unix ms) */
422
+ timestamp: number;
423
+ }
424
+ /**
425
+ * Page visits plugin API
426
+ */
427
+ interface PageVisitsPlugin {
428
+ /**
429
+ * Get total visit count (lifetime)
430
+ */
431
+ getTotalCount(): number;
432
+ /**
433
+ * Get session visit count
434
+ */
435
+ getSessionCount(): number;
436
+ /**
437
+ * Check if this is the first visit
438
+ */
439
+ isFirstVisit(): boolean;
440
+ /**
441
+ * Get timestamp of first visit
442
+ */
443
+ getFirstVisitTime(): number | undefined;
444
+ /**
445
+ * Get timestamp of last visit
446
+ */
447
+ getLastVisitTime(): number | undefined;
448
+ /**
449
+ * Manually increment page visit
450
+ * (useful if autoIncrement is disabled)
451
+ */
452
+ increment(): void;
453
+ /**
454
+ * Reset all counters and data
455
+ * (useful for testing or user opt-out)
456
+ */
457
+ reset(): void;
458
+ /**
459
+ * Get full page visits state
460
+ */
461
+ getState(): PageVisitsEvent;
462
+ }
463
+
464
+ /**
465
+ * Page Visits Plugin
466
+ *
467
+ * Generic page visit tracking for any SDK built on sdk-kit.
468
+ *
469
+ * Features:
470
+ * - Session-scoped counter (sessionStorage)
471
+ * - Lifetime counter with timestamps (localStorage)
472
+ * - First-visit detection
473
+ * - DNT (Do Not Track) support
474
+ * - GDPR-compliant expiration
475
+ * - Auto-loads storage plugin if missing
476
+ *
477
+ * Events emitted:
478
+ * - 'pageVisits:incremented' with PageVisitsEvent
479
+ * - 'pageVisits:reset'
480
+ * - 'pageVisits:disabled' with { reason: 'dnt' | 'config' }
481
+ *
482
+ * @example
483
+ * ```typescript
484
+ * import { SDK } from '@lytics/sdk-kit';
485
+ * import { storagePlugin, pageVisitsPlugin } from '@lytics/sdk-kit-plugins';
486
+ *
487
+ * const sdk = new SDK({
488
+ * pageVisits: {
489
+ * enabled: true,
490
+ * respectDNT: true,
491
+ * ttl: 31536000 // 1 year
492
+ * }
493
+ * });
494
+ *
495
+ * sdk.use(storagePlugin);
496
+ * sdk.use(pageVisitsPlugin);
497
+ *
498
+ * // Listen to visit events
499
+ * sdk.on('pageVisits:incremented', (event) => {
500
+ * console.log('Visit count:', event.totalVisits);
501
+ * if (event.isFirstVisit) {
502
+ * console.log('Welcome, first-time visitor!');
503
+ * }
504
+ * });
505
+ *
506
+ * // API methods
507
+ * console.log(sdk.pageVisits.getTotalCount()); // 5
508
+ * console.log(sdk.pageVisits.getSessionCount()); // 2
509
+ * console.log(sdk.pageVisits.isFirstVisit()); // false
510
+ * ```
511
+ */
512
+
513
+ declare const pageVisitsPlugin: PluginFunction;
514
+
515
+ /** @module scrollDepthPlugin */
516
+
517
+ /**
518
+ * Scroll Depth Plugin
519
+ *
520
+ * Tracks scroll depth and emits `trigger:scrollDepth` events when thresholds are crossed.
521
+ *
522
+ * ## How It Works
523
+ *
524
+ * 1. **Detection**: Listens to `scroll` events (throttled)
525
+ * 2. **Calculation**: Calculates current scroll percentage
526
+ * 3. **Tracking**: Tracks maximum scroll depth and threshold crossings
527
+ * 4. **Emission**: Emits `trigger:scrollDepth` events when thresholds are crossed
528
+ *
529
+ * ## Configuration
530
+ *
531
+ * ```typescript
532
+ * init({
533
+ * scrollDepth: {
534
+ * thresholds: [25, 50, 75, 100], // Percentages to track
535
+ * throttle: 100, // Throttle interval (ms)
536
+ * includeViewportHeight: true, // Calculation method
537
+ * recalculateOnResize: true // Recalculate on resize
538
+ * }
539
+ * });
540
+ * ```
541
+ *
542
+ * ## Experience Targeting
543
+ *
544
+ * ```typescript
545
+ * register('mid-article-cta', {
546
+ * type: 'banner',
547
+ * content: { message: 'Enjoying the article?' },
548
+ * targeting: {
549
+ * custom: (ctx) => (ctx.triggers?.scrollDepth?.percent || 0) >= 50
550
+ * }
551
+ * });
552
+ * ```
553
+ *
554
+ * ## API Methods
555
+ *
556
+ * ```typescript
557
+ * // Get maximum scroll percentage reached
558
+ * instance.scrollDepth.getMaxPercent(); // 73
559
+ *
560
+ * // Get current scroll percentage
561
+ * instance.scrollDepth.getCurrentPercent(); // 50
562
+ *
563
+ * // Get all crossed thresholds
564
+ * instance.scrollDepth.getThresholdsCrossed(); // [25, 50]
565
+ *
566
+ * // Reset tracking (useful for testing)
567
+ * instance.scrollDepth.reset();
568
+ * ```
569
+ *
570
+ * @param plugin Plugin interface from sdk-kit
571
+ * @param instance SDK instance
572
+ * @param config SDK configuration
573
+ */
574
+ declare const scrollDepthPlugin: PluginFunction;
575
+
576
+ /** @module scrollDepthPlugin */
577
+ /**
578
+ * Scroll Depth Plugin API
579
+ */
580
+ interface ScrollDepthPlugin {
581
+ getMaxPercent(): number;
582
+ getCurrentPercent(): number;
583
+ getThresholdsCrossed(): number[];
584
+ getDevice(): 'mobile' | 'tablet' | 'desktop';
585
+ getAdvancedMetrics(): {
586
+ timeOnPage: number;
587
+ directionChanges: number;
588
+ timeScrollingUp: number;
589
+ thresholdTimes: Record<number, number>;
590
+ } | null;
591
+ reset(): void;
592
+ }
593
+ /**
594
+ * Scroll Depth Plugin Configuration
595
+ *
596
+ * Tracks scroll depth and emits trigger:scrollDepth events when thresholds are crossed.
597
+ */
598
+ interface ScrollDepthPluginConfig {
599
+ scrollDepth?: {
600
+ /**
601
+ * Array of scroll percentage thresholds to track (0-100).
602
+ * When user scrolls past a threshold, a trigger:scrollDepth event is emitted.
603
+ * @default [25, 50, 75, 100]
604
+ * @example [50, 100]
605
+ */
606
+ thresholds?: number[];
607
+ /**
608
+ * Throttle interval in milliseconds for scroll event handler.
609
+ * Lower values are more responsive but impact performance.
610
+ * @default 100
611
+ * @example 200
612
+ */
613
+ throttle?: number;
614
+ /**
615
+ * Include viewport height in scroll percentage calculation.
616
+ *
617
+ * - true: (scrollTop + viewportHeight) / totalHeight
618
+ * More intuitive: 100% when bottom of viewport reaches end
619
+ * - false: scrollTop / (totalHeight - viewportHeight)
620
+ * Pathfora's method: 100% when top of viewport reaches end
621
+ *
622
+ * @default true
623
+ */
624
+ includeViewportHeight?: boolean;
625
+ /**
626
+ * Recalculate scroll on window resize.
627
+ * Useful for responsive layouts where content height changes.
628
+ * @default true
629
+ */
630
+ recalculateOnResize?: boolean;
631
+ /**
632
+ * Track advanced metrics (velocity, direction, time-to-threshold).
633
+ * Enables advanced engagement quality analysis.
634
+ * Slight performance overhead but provides rich insights.
635
+ * @default false
636
+ */
637
+ trackAdvancedMetrics?: boolean;
638
+ /**
639
+ * Velocity threshold (px/ms) to consider "fast scrolling".
640
+ * Fast scrolling often indicates skimming rather than reading.
641
+ * Only used when trackAdvancedMetrics is true.
642
+ * @default 3
643
+ */
644
+ fastScrollVelocityThreshold?: number;
645
+ /**
646
+ * Disable scroll tracking on mobile devices.
647
+ * Useful since mobile scroll behavior differs significantly from desktop.
648
+ * @default false
649
+ */
650
+ disableOnMobile?: boolean;
651
+ };
652
+ }
653
+ /**
654
+ * Scroll Depth Event Payload
655
+ *
656
+ * Emitted as `trigger:scrollDepth` when a threshold is crossed.
657
+ */
658
+ interface ScrollDepthEvent {
659
+ /** Whether the trigger has fired */
660
+ triggered: boolean;
661
+ /** Timestamp when the event was emitted */
662
+ timestamp: number;
663
+ /** Current scroll percentage (0-100) */
664
+ percent: number;
665
+ /** Maximum scroll percentage reached during session */
666
+ maxPercent: number;
667
+ /** The threshold that was just crossed */
668
+ threshold: number;
669
+ /** All thresholds that have been triggered */
670
+ thresholdsCrossed: number[];
671
+ /** Device type (mobile, tablet, desktop) */
672
+ device: 'mobile' | 'tablet' | 'desktop';
673
+ /** Advanced metrics (only present when trackAdvancedMetrics is enabled) */
674
+ advanced?: {
675
+ /** Time in milliseconds to reach this threshold from page load */
676
+ timeToThreshold: number;
677
+ /** Current scroll velocity in pixels per millisecond */
678
+ velocity: number;
679
+ /** Whether user is scrolling fast (indicates skimming) */
680
+ isFastScrolling: boolean;
681
+ /** Number of direction changes (up/down) since last threshold */
682
+ directionChanges: number;
683
+ /** Total time spent scrolling up (indicates seeking behavior) */
684
+ timeScrollingUp: number;
685
+ /** Scroll quality score (0-100, higher = more engaged) */
686
+ engagementScore: number;
687
+ };
688
+ }
689
+
690
+ /** @module timeDelayPlugin */
691
+ /**
692
+ * Time Delay Plugin Configuration
693
+ *
694
+ * Tracks time elapsed since SDK initialization and emits trigger:timeDelay events.
695
+ */
696
+ interface TimeDelayPluginConfig {
697
+ timeDelay?: {
698
+ /**
699
+ * Delay before emitting trigger event (milliseconds).
700
+ * Set to 0 to disable (immediate trigger on init).
701
+ * @default 0
702
+ * @example 5000 // 5 seconds
703
+ */
704
+ delay?: number;
705
+ /**
706
+ * Pause timer when tab is hidden (Page Visibility API).
707
+ * When true, only counts "active viewing time".
708
+ * When false, timer runs even when tab is hidden.
709
+ * @default true
710
+ */
711
+ pauseWhenHidden?: boolean;
712
+ };
713
+ }
714
+ /**
715
+ * Time Delay Event Payload
716
+ *
717
+ * Emitted via 'trigger:timeDelay' when the configured delay is reached.
718
+ */
719
+ interface TimeDelayEvent {
720
+ /** Timestamp when the trigger event was emitted */
721
+ timestamp: number;
722
+ /** Total elapsed time since init (milliseconds, includes paused time) */
723
+ elapsed: number;
724
+ /** Active elapsed time (milliseconds, excludes time when tab was hidden) */
725
+ activeElapsed: number;
726
+ /** Whether the timer was paused at any point */
727
+ wasPaused: boolean;
728
+ /** Number of times visibility changed (hidden/visible) */
729
+ visibilityChanges: number;
730
+ }
731
+ /**
732
+ * Time Delay Plugin API
733
+ */
734
+ interface TimeDelayPlugin {
735
+ /**
736
+ * Get total elapsed time since init (includes paused time)
737
+ * @returns Time in milliseconds
738
+ */
739
+ getElapsed(): number;
740
+ /**
741
+ * Get active elapsed time (excludes paused time)
742
+ * @returns Time in milliseconds
743
+ */
744
+ getActiveElapsed(): number;
745
+ /**
746
+ * Get remaining time until trigger
747
+ * @returns Time in milliseconds, or 0 if already triggered
748
+ */
749
+ getRemaining(): number;
750
+ /**
751
+ * Check if timer is currently paused (tab hidden)
752
+ * @returns True if paused
753
+ */
754
+ isPaused(): boolean;
755
+ /**
756
+ * Check if trigger has fired
757
+ * @returns True if triggered
758
+ */
759
+ isTriggered(): boolean;
760
+ /**
761
+ * Reset timer to initial state
762
+ * Clears trigger flag and restarts timing
763
+ */
764
+ reset(): void;
765
+ }
766
+
767
+ /** @module timeDelayPlugin */
768
+
769
+ /**
770
+ * Time Delay Plugin
771
+ *
772
+ * Tracks time elapsed since SDK initialization and emits trigger:timeDelay events
773
+ * when the configured delay is reached.
774
+ *
775
+ * **Features:**
776
+ * - Millisecond precision timing
777
+ * - Pause/resume on tab visibility change (optional)
778
+ * - Tracks active vs total elapsed time
779
+ * - Full timer lifecycle management
780
+ *
781
+ * **Event-Driven Architecture:**
782
+ * This plugin emits `trigger:timeDelay` events when the delay threshold is reached.
783
+ * The core runtime listens for these events and automatically re-evaluates experiences.
784
+ *
785
+ * **Usage Pattern:**
786
+ * Use `targeting.custom` to check if time delay has triggered:
787
+ *
788
+ * @example Basic usage
789
+ * ```typescript
790
+ * import { init, register } from '@prosdevlab/experience-sdk';
791
+ *
792
+ * init({
793
+ * timeDelay: {
794
+ * delay: 5000, // 5 seconds
795
+ * pauseWhenHidden: true // Pause when tab hidden (default)
796
+ * }
797
+ * });
798
+ *
799
+ * // Show banner after 5 seconds of active viewing time
800
+ * register('timed-offer', {
801
+ * type: 'banner',
802
+ * content: {
803
+ * message: 'Limited time offer!',
804
+ * buttons: [{ text: 'Claim Now', variant: 'primary' }]
805
+ * },
806
+ * targeting: {
807
+ * custom: (context) => {
808
+ * const active = context.triggers?.timeDelay?.activeElapsed || 0;
809
+ * return active >= 5000;
810
+ * }
811
+ * }
812
+ * });
813
+ * ```
814
+ *
815
+ * @example Combining with other triggers
816
+ * ```typescript
817
+ * // Show after 10s OR on exit intent (whichever comes first)
818
+ * register('engaged-offer', {
819
+ * type: 'banner',
820
+ * content: { message: 'Special offer for engaged users!' },
821
+ * targeting: {
822
+ * custom: (context) => {
823
+ * const timeElapsed = (context.triggers?.timeDelay?.activeElapsed || 0) >= 10000;
824
+ * const exitIntent = context.triggers?.exitIntent?.triggered;
825
+ * return timeElapsed || exitIntent;
826
+ * }
827
+ * }
828
+ * });
829
+ * ```
830
+ *
831
+ * @param plugin Plugin interface from sdk-kit
832
+ * @param instance SDK instance
833
+ * @param config SDK configuration
834
+ */
835
+ declare const timeDelayPlugin: PluginFunction;
836
+
837
+ 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 FrequencyPlugin, type FrequencyPluginConfig, type ModalContent, type PageVisitsEvent, type PageVisitsPlugin, type PageVisitsPluginConfig, type ScrollDepthEvent, type ScrollDepthPlugin, type ScrollDepthPluginConfig, type TimeDelayEvent, type TimeDelayPlugin, type TimeDelayPluginConfig, type TooltipContent, type TraceStep, bannerPlugin, debugPlugin, exitIntentPlugin, frequencyPlugin, pageVisitsPlugin, scrollDepthPlugin, timeDelayPlugin };