@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.
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Page Visits Plugin
3
+ *
4
+ * Generic page visit tracking for any SDK built on sdk-kit.
5
+ *
6
+ * Features:
7
+ * - Session-scoped counter (sessionStorage)
8
+ * - Lifetime counter with timestamps (localStorage)
9
+ * - First-visit detection
10
+ * - DNT (Do Not Track) support
11
+ * - GDPR-compliant expiration
12
+ * - Auto-loads storage plugin if missing
13
+ *
14
+ * Events emitted:
15
+ * - 'pageVisits:incremented' with PageVisitsEvent
16
+ * - 'pageVisits:reset'
17
+ * - 'pageVisits:disabled' with { reason: 'dnt' | 'config' }
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * import { SDK } from '@lytics/sdk-kit';
22
+ * import { storagePlugin, pageVisitsPlugin } from '@lytics/sdk-kit-plugins';
23
+ *
24
+ * const sdk = new SDK({
25
+ * pageVisits: {
26
+ * enabled: true,
27
+ * respectDNT: true,
28
+ * ttl: 31536000 // 1 year
29
+ * }
30
+ * });
31
+ *
32
+ * sdk.use(storagePlugin);
33
+ * sdk.use(pageVisitsPlugin);
34
+ *
35
+ * // Listen to visit events
36
+ * sdk.on('pageVisits:incremented', (event) => {
37
+ * console.log('Visit count:', event.totalVisits);
38
+ * if (event.isFirstVisit) {
39
+ * console.log('Welcome, first-time visitor!');
40
+ * }
41
+ * });
42
+ *
43
+ * // API methods
44
+ * console.log(sdk.pageVisits.getTotalCount()); // 5
45
+ * console.log(sdk.pageVisits.getSessionCount()); // 2
46
+ * console.log(sdk.pageVisits.isFirstVisit()); // false
47
+ * ```
48
+ */
49
+
50
+ import type { PluginFunction, SDK } from '@lytics/sdk-kit';
51
+ import { type StoragePlugin, storagePlugin } from '@lytics/sdk-kit-plugins';
52
+ import type { PageVisitsEvent, PageVisitsPlugin } from './types';
53
+
54
+ /**
55
+ * Storage data format for lifetime visits
56
+ */
57
+ interface TotalData {
58
+ count: number;
59
+ first: number; // Timestamp
60
+ last: number; // Timestamp
61
+ }
62
+
63
+ /**
64
+ * Pure function: Check if Do Not Track is enabled
65
+ */
66
+ export function respectsDNT(): boolean {
67
+ if (typeof navigator === 'undefined') return false;
68
+ return (
69
+ navigator.doNotTrack === '1' ||
70
+ (navigator as any).msDoNotTrack === '1' ||
71
+ (window as any).doNotTrack === '1'
72
+ );
73
+ }
74
+
75
+ /**
76
+ * Pure function: Build storage key with optional prefix
77
+ */
78
+ export function buildStorageKey(key: string, prefix?: string): string {
79
+ return prefix ? `${prefix}${key}` : key;
80
+ }
81
+
82
+ /**
83
+ * Pure function: Create PageVisitsEvent payload
84
+ */
85
+ export function createVisitsEvent(
86
+ isFirstVisit: boolean,
87
+ totalVisits: number,
88
+ sessionVisits: number,
89
+ firstVisitTime: number | undefined,
90
+ lastVisitTime: number | undefined,
91
+ timestamp: number
92
+ ): PageVisitsEvent {
93
+ return {
94
+ isFirstVisit,
95
+ totalVisits,
96
+ sessionVisits,
97
+ firstVisitTime,
98
+ lastVisitTime,
99
+ timestamp,
100
+ };
101
+ }
102
+
103
+ export const pageVisitsPlugin: PluginFunction = (plugin, instance, config) => {
104
+ plugin.ns('pageVisits');
105
+
106
+ // Set defaults
107
+ plugin.defaults({
108
+ pageVisits: {
109
+ enabled: true,
110
+ respectDNT: true,
111
+ sessionKey: 'pageVisits:session',
112
+ totalKey: 'pageVisits:total',
113
+ ttl: undefined,
114
+ autoIncrement: true,
115
+ },
116
+ });
117
+
118
+ // Auto-load storage plugin if not present
119
+ if (!(instance as SDK & { storage?: StoragePlugin }).storage) {
120
+ console.warn('[PageVisits] Storage plugin not found, auto-loading...');
121
+ instance.use(storagePlugin);
122
+ }
123
+
124
+ // Cast instance to include storage
125
+ const sdkInstance = instance as SDK & { storage: StoragePlugin };
126
+
127
+ // Internal state
128
+ let sessionCount = 0;
129
+ let totalCount = 0;
130
+ let firstVisitTime: number | undefined;
131
+ let lastVisitTime: number | undefined;
132
+ let isFirstVisitFlag = false;
133
+ let initialized = false;
134
+
135
+ /**
136
+ * Load existing visit data from storage
137
+ */
138
+ function loadData(): void {
139
+ const sessionKey = config.get('pageVisits.sessionKey') ?? 'pageVisits:session';
140
+ const totalKey = config.get('pageVisits.totalKey') ?? 'pageVisits:total';
141
+
142
+ // Load session count
143
+ const storedSession = sdkInstance.storage.get<number>(sessionKey, {
144
+ backend: 'sessionStorage',
145
+ });
146
+ sessionCount = storedSession ?? 0;
147
+
148
+ // Load total data
149
+ const storedTotal = sdkInstance.storage.get<TotalData>(totalKey, {
150
+ backend: 'localStorage',
151
+ });
152
+
153
+ if (storedTotal) {
154
+ totalCount = storedTotal.count ?? 0;
155
+ firstVisitTime = storedTotal.first;
156
+ lastVisitTime = storedTotal.last;
157
+ isFirstVisitFlag = false;
158
+ } else {
159
+ totalCount = 0;
160
+ firstVisitTime = undefined;
161
+ lastVisitTime = undefined;
162
+ isFirstVisitFlag = true;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Save visit data to storage
168
+ */
169
+ function saveData(): void {
170
+ const sessionKey = config.get('pageVisits.sessionKey') ?? 'pageVisits:session';
171
+ const totalKey = config.get('pageVisits.totalKey') ?? 'pageVisits:total';
172
+ const ttl = config.get('pageVisits.ttl');
173
+
174
+ // Save session count
175
+ sdkInstance.storage.set(sessionKey, sessionCount, {
176
+ backend: 'sessionStorage',
177
+ });
178
+
179
+ // Save total data
180
+ const totalData: TotalData = {
181
+ count: totalCount,
182
+ first: firstVisitTime ?? Date.now(),
183
+ last: lastVisitTime ?? Date.now(),
184
+ };
185
+
186
+ sdkInstance.storage.set(totalKey, totalData, {
187
+ backend: 'localStorage',
188
+ ...(ttl && { ttl }),
189
+ });
190
+ }
191
+
192
+ /**
193
+ * Increment visit counters
194
+ */
195
+ function increment(): void {
196
+ if (!initialized) {
197
+ loadData();
198
+ initialized = true;
199
+ }
200
+
201
+ // Increment counters
202
+ sessionCount += 1;
203
+ totalCount += 1;
204
+ const now = Date.now();
205
+
206
+ // Set first visit time if needed
207
+ if (isFirstVisitFlag) {
208
+ firstVisitTime = now;
209
+ }
210
+
211
+ // Update last visit time
212
+ lastVisitTime = now;
213
+
214
+ // Save to storage
215
+ saveData();
216
+
217
+ // Emit event using pure function
218
+ const event = createVisitsEvent(
219
+ isFirstVisitFlag,
220
+ totalCount,
221
+ sessionCount,
222
+ firstVisitTime,
223
+ lastVisitTime,
224
+ now
225
+ );
226
+
227
+ plugin.emit('pageVisits:incremented', event);
228
+
229
+ // After first increment, no longer first visit
230
+ if (isFirstVisitFlag) {
231
+ isFirstVisitFlag = false;
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Reset all data
237
+ */
238
+ function reset(): void {
239
+ const sessionKey = config.get('pageVisits.sessionKey') ?? 'pageVisits:session';
240
+ const totalKey = config.get('pageVisits.totalKey') ?? 'pageVisits:total';
241
+
242
+ // Clear storage
243
+ sdkInstance.storage.remove(sessionKey, { backend: 'sessionStorage' });
244
+ sdkInstance.storage.remove(totalKey, { backend: 'localStorage' });
245
+
246
+ // Reset state
247
+ sessionCount = 0;
248
+ totalCount = 0;
249
+ firstVisitTime = undefined;
250
+ lastVisitTime = undefined;
251
+ isFirstVisitFlag = false;
252
+ initialized = false;
253
+
254
+ // Emit event
255
+ plugin.emit('pageVisits:reset');
256
+ }
257
+
258
+ /**
259
+ * Get current state
260
+ */
261
+ function getState(): PageVisitsEvent {
262
+ return createVisitsEvent(
263
+ isFirstVisitFlag,
264
+ totalCount,
265
+ sessionCount,
266
+ firstVisitTime,
267
+ lastVisitTime,
268
+ Date.now()
269
+ );
270
+ }
271
+
272
+ /**
273
+ * Initialize plugin
274
+ */
275
+ function initialize(): void {
276
+ const enabled = config.get('pageVisits.enabled') ?? true;
277
+ const respectDNTConfig = config.get('pageVisits.respectDNT') ?? true;
278
+ const autoIncrement = config.get('pageVisits.autoIncrement') ?? true;
279
+
280
+ // Check DNT using pure function
281
+ if (respectDNTConfig && respectsDNT()) {
282
+ plugin.emit('pageVisits:disabled', { reason: 'dnt' });
283
+ return;
284
+ }
285
+
286
+ // Check enabled
287
+ if (!enabled) {
288
+ plugin.emit('pageVisits:disabled', { reason: 'config' });
289
+ return;
290
+ }
291
+
292
+ // Auto-increment on load
293
+ if (autoIncrement) {
294
+ increment();
295
+ }
296
+ }
297
+
298
+ // Initialize on SDK ready
299
+ instance.on('sdk:ready', initialize);
300
+
301
+ // Expose public API
302
+ plugin.expose({
303
+ pageVisits: {
304
+ getTotalCount: () => totalCount,
305
+ getSessionCount: () => sessionCount,
306
+ isFirstVisit: () => isFirstVisitFlag,
307
+ getFirstVisitTime: () => firstVisitTime,
308
+ getLastVisitTime: () => lastVisitTime,
309
+ increment,
310
+ reset,
311
+ getState,
312
+ } satisfies PageVisitsPlugin,
313
+ });
314
+ };
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Page Visits Plugin Types
3
+ *
4
+ * Generic page visit tracking for any SDK built on sdk-kit.
5
+ * Tracks session and lifetime visit counts with first-visit detection.
6
+ */
7
+
8
+ /**
9
+ * Page visits plugin configuration
10
+ */
11
+ export interface PageVisitsPluginConfig {
12
+ pageVisits?: {
13
+ /**
14
+ * Enable/disable page visit tracking
15
+ * @default true
16
+ */
17
+ enabled?: boolean;
18
+
19
+ /**
20
+ * Honor Do Not Track browser setting
21
+ * @default true
22
+ */
23
+ respectDNT?: boolean;
24
+
25
+ /**
26
+ * Storage key for session count
27
+ * @default 'pageVisits:session'
28
+ */
29
+ sessionKey?: string;
30
+
31
+ /**
32
+ * Storage key for lifetime data
33
+ * @default 'pageVisits:total'
34
+ */
35
+ totalKey?: string;
36
+
37
+ /**
38
+ * TTL for lifetime data in seconds (GDPR compliance)
39
+ * @default undefined (no expiration)
40
+ */
41
+ ttl?: number;
42
+
43
+ /**
44
+ * Automatically increment on plugin load
45
+ * @default true
46
+ */
47
+ autoIncrement?: boolean;
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Page visits event payload
53
+ */
54
+ export interface PageVisitsEvent {
55
+ /** Whether this is the user's first visit ever */
56
+ isFirstVisit: boolean;
57
+
58
+ /** Total visits across all sessions (lifetime) */
59
+ totalVisits: number;
60
+
61
+ /** Visits in current session */
62
+ sessionVisits: number;
63
+
64
+ /** Timestamp of first visit (unix ms) */
65
+ firstVisitTime?: number;
66
+
67
+ /** Timestamp of last visit (unix ms) */
68
+ lastVisitTime?: number;
69
+
70
+ /** Timestamp of current visit (unix ms) */
71
+ timestamp: number;
72
+ }
73
+
74
+ /**
75
+ * Page visits plugin API
76
+ */
77
+ export interface PageVisitsPlugin {
78
+ /**
79
+ * Get total visit count (lifetime)
80
+ */
81
+ getTotalCount(): number;
82
+
83
+ /**
84
+ * Get session visit count
85
+ */
86
+ getSessionCount(): number;
87
+
88
+ /**
89
+ * Check if this is the first visit
90
+ */
91
+ isFirstVisit(): boolean;
92
+
93
+ /**
94
+ * Get timestamp of first visit
95
+ */
96
+ getFirstVisitTime(): number | undefined;
97
+
98
+ /**
99
+ * Get timestamp of last visit
100
+ */
101
+ getLastVisitTime(): number | undefined;
102
+
103
+ /**
104
+ * Manually increment page visit
105
+ * (useful if autoIncrement is disabled)
106
+ */
107
+ increment(): void;
108
+
109
+ /**
110
+ * Reset all counters and data
111
+ * (useful for testing or user opt-out)
112
+ */
113
+ reset(): void;
114
+
115
+ /**
116
+ * Get full page visits state
117
+ */
118
+ getState(): PageVisitsEvent;
119
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Scroll Depth Plugin - Barrel Export
3
+ */
4
+
5
+ export { scrollDepthPlugin } from './scroll-depth';
6
+ export type { ScrollDepthEvent, ScrollDepthPlugin, ScrollDepthPluginConfig } from './types';