@prosdevlab/experience-sdk-plugins 0.1.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,6 @@
1
+ /**
2
+ * Debug Plugin - Barrel Export
3
+ */
4
+
5
+ export type { DebugPlugin, DebugPluginConfig } from './debug';
6
+ export { debugPlugin } from './debug';
@@ -0,0 +1,361 @@
1
+ import { SDK } from '@lytics/sdk-kit';
2
+ import { type StoragePlugin, storagePlugin } from '@lytics/sdk-kit-plugins';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import type { Decision } from '../types';
5
+ import { type FrequencyPlugin, frequencyPlugin } from './frequency';
6
+
7
+ type SDKWithFrequency = SDK & { frequency: FrequencyPlugin; storage: StoragePlugin };
8
+
9
+ describe('Frequency Plugin', () => {
10
+ let sdk: SDKWithFrequency;
11
+
12
+ beforeEach(() => {
13
+ // Clear any existing storage to avoid pollution between tests
14
+ if (typeof sessionStorage !== 'undefined') {
15
+ sessionStorage.clear();
16
+ }
17
+ if (typeof localStorage !== 'undefined') {
18
+ localStorage.clear();
19
+ }
20
+
21
+ // Use memory storage for tests
22
+ sdk = new SDK({
23
+ frequency: { enabled: true },
24
+ storage: { backend: 'memory' },
25
+ }) as SDKWithFrequency;
26
+
27
+ // Install plugins
28
+ sdk.use(storagePlugin);
29
+ sdk.use(frequencyPlugin);
30
+ });
31
+
32
+ describe('Plugin Registration', () => {
33
+ it('should register frequency plugin', () => {
34
+ expect(sdk.frequency).toBeDefined();
35
+ });
36
+
37
+ it('should expose frequency API methods', () => {
38
+ expect(sdk.frequency.getImpressionCount).toBeTypeOf('function');
39
+ expect(sdk.frequency.hasReachedCap).toBeTypeOf('function');
40
+ expect(sdk.frequency.recordImpression).toBeTypeOf('function');
41
+ });
42
+
43
+ it('should auto-load storage plugin if not present', () => {
44
+ const newSdk = new SDK({ frequency: { enabled: true } }) as SDKWithFrequency;
45
+ newSdk.use(frequencyPlugin);
46
+ expect(newSdk.storage).toBeDefined();
47
+ });
48
+ });
49
+
50
+ describe('Configuration', () => {
51
+ it('should use default config', () => {
52
+ const enabled = sdk.get('frequency.enabled');
53
+ const namespace = sdk.get('frequency.namespace');
54
+
55
+ expect(enabled).toBe(true);
56
+ expect(namespace).toBe('experiences:frequency');
57
+ });
58
+
59
+ it('should allow custom config', () => {
60
+ const customSdk = new SDK({
61
+ frequency: { enabled: false, namespace: 'custom:freq' },
62
+ storage: { backend: 'memory' },
63
+ }) as SDKWithFrequency;
64
+
65
+ customSdk.use(storagePlugin);
66
+ customSdk.use(frequencyPlugin);
67
+
68
+ expect(customSdk.get('frequency.enabled')).toBe(false);
69
+ expect(customSdk.get('frequency.namespace')).toBe('custom:freq');
70
+ });
71
+ });
72
+
73
+ describe('Impression Tracking', () => {
74
+ it('should initialize impression count at 0', () => {
75
+ const count = sdk.frequency.getImpressionCount('welcome-banner');
76
+ expect(count).toBe(0);
77
+ });
78
+
79
+ it('should record impressions', () => {
80
+ sdk.frequency.recordImpression('welcome-banner');
81
+ expect(sdk.frequency.getImpressionCount('welcome-banner')).toBe(1);
82
+
83
+ sdk.frequency.recordImpression('welcome-banner');
84
+ expect(sdk.frequency.getImpressionCount('welcome-banner')).toBe(2);
85
+ });
86
+
87
+ it('should track impressions per experience independently', () => {
88
+ sdk.frequency.recordImpression('banner-1');
89
+ sdk.frequency.recordImpression('banner-2');
90
+ sdk.frequency.recordImpression('banner-1');
91
+
92
+ expect(sdk.frequency.getImpressionCount('banner-1')).toBe(2);
93
+ expect(sdk.frequency.getImpressionCount('banner-2')).toBe(1);
94
+ });
95
+
96
+ it('should emit impression-recorded event', () => {
97
+ const handler = vi.fn();
98
+ sdk.on('experiences:impression-recorded', handler);
99
+
100
+ sdk.frequency.recordImpression('welcome-banner');
101
+
102
+ expect(handler).toHaveBeenCalledWith({
103
+ experienceId: 'welcome-banner',
104
+ count: 1,
105
+ timestamp: expect.any(Number),
106
+ });
107
+ });
108
+
109
+ it('should not record impressions when disabled', () => {
110
+ sdk.set('frequency.enabled', false);
111
+ sdk.frequency.recordImpression('welcome-banner');
112
+ expect(sdk.frequency.getImpressionCount('welcome-banner')).toBe(0);
113
+ });
114
+ });
115
+
116
+ describe('Session Frequency Caps', () => {
117
+ it('should not reach cap below limit', () => {
118
+ sdk.frequency.recordImpression('welcome-banner');
119
+ expect(sdk.frequency.hasReachedCap('welcome-banner', 2, 'session')).toBe(false);
120
+ });
121
+
122
+ it('should reach cap at limit', () => {
123
+ sdk.frequency.recordImpression('welcome-banner');
124
+ sdk.frequency.recordImpression('welcome-banner');
125
+ expect(sdk.frequency.hasReachedCap('welcome-banner', 2, 'session')).toBe(true);
126
+ });
127
+
128
+ it('should reach cap above limit', () => {
129
+ sdk.frequency.recordImpression('welcome-banner');
130
+ sdk.frequency.recordImpression('welcome-banner');
131
+ sdk.frequency.recordImpression('welcome-banner');
132
+ expect(sdk.frequency.hasReachedCap('welcome-banner', 2, 'session')).toBe(true);
133
+ });
134
+ });
135
+
136
+ describe('Time-Based Frequency Caps', () => {
137
+ it('should count impressions within day window', () => {
138
+ const now = Date.now();
139
+
140
+ // Mock Date.now() for first impression (25 hours ago - outside window)
141
+ vi.spyOn(Date, 'now').mockReturnValue(now - 25 * 60 * 60 * 1000);
142
+ sdk.frequency.recordImpression('welcome-banner', 'day');
143
+
144
+ // Mock Date.now() for second impression (now - inside window)
145
+ vi.spyOn(Date, 'now').mockReturnValue(now);
146
+ sdk.frequency.recordImpression('welcome-banner', 'day');
147
+
148
+ // Only 1 impression within last 24 hours
149
+ expect(sdk.frequency.hasReachedCap('welcome-banner', 2, 'day')).toBe(false);
150
+ expect(sdk.frequency.hasReachedCap('welcome-banner', 1, 'day')).toBe(true);
151
+
152
+ vi.restoreAllMocks();
153
+ });
154
+
155
+ it('should count impressions within week window', () => {
156
+ const now = Date.now();
157
+
158
+ // Record 2 impressions 8 days ago (outside week window)
159
+ vi.spyOn(Date, 'now').mockReturnValue(now - 8 * 24 * 60 * 60 * 1000);
160
+ sdk.frequency.recordImpression('welcome-banner', 'week');
161
+ sdk.frequency.recordImpression('welcome-banner', 'week');
162
+
163
+ // Record 1 impression 3 days ago (inside week window)
164
+ vi.spyOn(Date, 'now').mockReturnValue(now - 3 * 24 * 60 * 60 * 1000);
165
+ sdk.frequency.recordImpression('welcome-banner', 'week');
166
+
167
+ // Current time
168
+ vi.spyOn(Date, 'now').mockReturnValue(now);
169
+
170
+ // Only 1 impression within last 7 days
171
+ expect(sdk.frequency.hasReachedCap('welcome-banner', 2, 'week')).toBe(false);
172
+ expect(sdk.frequency.hasReachedCap('welcome-banner', 1, 'week')).toBe(true);
173
+
174
+ vi.restoreAllMocks();
175
+ });
176
+
177
+ it('should handle multiple impressions within time window', () => {
178
+ const now = Date.now();
179
+
180
+ // Record 3 impressions within last day
181
+ for (let i = 0; i < 3; i++) {
182
+ vi.spyOn(Date, 'now').mockReturnValue(now - i * 60 * 60 * 1000); // Each hour
183
+ sdk.frequency.recordImpression('welcome-banner', 'day');
184
+ }
185
+
186
+ vi.spyOn(Date, 'now').mockReturnValue(now);
187
+
188
+ expect(sdk.frequency.hasReachedCap('welcome-banner', 3, 'day')).toBe(true);
189
+ expect(sdk.frequency.hasReachedCap('welcome-banner', 4, 'day')).toBe(false);
190
+
191
+ vi.restoreAllMocks();
192
+ });
193
+ });
194
+
195
+ describe('Event Integration', () => {
196
+ it('should auto-record impression on experiences:evaluated event when show=true', () => {
197
+ const decision: Decision = {
198
+ show: true,
199
+ experienceId: 'welcome-banner',
200
+ reasons: ['URL matches'],
201
+ trace: [],
202
+ context: {
203
+ url: 'https://example.com',
204
+ timestamp: Date.now(),
205
+ },
206
+ metadata: {
207
+ evaluatedAt: Date.now(),
208
+ totalDuration: 10,
209
+ experiencesEvaluated: 1,
210
+ },
211
+ };
212
+
213
+ sdk.emit('experiences:evaluated', { decision });
214
+
215
+ expect(sdk.frequency.getImpressionCount('welcome-banner')).toBe(1);
216
+ });
217
+
218
+ it('should not record impression when show=false', () => {
219
+ const decision: Decision = {
220
+ show: false,
221
+ reasons: ['Frequency cap reached'],
222
+ trace: [],
223
+ context: {
224
+ url: 'https://example.com',
225
+ timestamp: Date.now(),
226
+ },
227
+ metadata: {
228
+ evaluatedAt: Date.now(),
229
+ totalDuration: 10,
230
+ experiencesEvaluated: 1,
231
+ },
232
+ };
233
+
234
+ sdk.emit('experiences:evaluated', { decision });
235
+
236
+ expect(sdk.frequency.getImpressionCount('welcome-banner')).toBe(0);
237
+ });
238
+
239
+ it('should not record impression when experienceId is missing', () => {
240
+ const decision: Decision = {
241
+ show: true,
242
+ reasons: ['No matching experience'],
243
+ trace: [],
244
+ context: {
245
+ url: 'https://example.com',
246
+ timestamp: Date.now(),
247
+ },
248
+ metadata: {
249
+ evaluatedAt: Date.now(),
250
+ totalDuration: 10,
251
+ experiencesEvaluated: 0,
252
+ },
253
+ };
254
+
255
+ sdk.emit('experiences:evaluated', { decision });
256
+
257
+ // Should not throw or record
258
+ expect(sdk.frequency.getImpressionCount('any-experience')).toBe(0);
259
+ });
260
+
261
+ it('should not auto-record when frequency plugin is disabled', () => {
262
+ sdk.set('frequency.enabled', false);
263
+
264
+ const decision: Decision = {
265
+ show: true,
266
+ experienceId: 'welcome-banner',
267
+ reasons: ['URL matches'],
268
+ trace: [],
269
+ context: {
270
+ url: 'https://example.com',
271
+ timestamp: Date.now(),
272
+ },
273
+ metadata: {
274
+ evaluatedAt: Date.now(),
275
+ totalDuration: 10,
276
+ experiencesEvaluated: 1,
277
+ },
278
+ };
279
+
280
+ sdk.emit('experiences:evaluated', { decision });
281
+
282
+ expect(sdk.frequency.getImpressionCount('welcome-banner')).toBe(0);
283
+ });
284
+ });
285
+
286
+ describe('Storage Integration', () => {
287
+ it('should persist impressions across SDK instances (session storage)', () => {
288
+ // Record impression in first instance (uses sessionStorage)
289
+ sdk.frequency.recordImpression('welcome-banner');
290
+ expect(sdk.frequency.getImpressionCount('welcome-banner')).toBe(1);
291
+
292
+ // Create second instance with same storage backend
293
+ const sdk2 = new SDK({
294
+ frequency: { enabled: true },
295
+ storage: { backend: 'memory' },
296
+ }) as SDKWithFrequency;
297
+ sdk2.use(storagePlugin);
298
+ sdk2.use(frequencyPlugin);
299
+
300
+ // Impressions SHOULD persist (sessionStorage is shared)
301
+ expect(sdk2.frequency.getImpressionCount('welcome-banner')).toBe(1);
302
+ });
303
+
304
+ it('should use namespaced storage keys', () => {
305
+ sdk.frequency.recordImpression('welcome-banner');
306
+
307
+ // Check sessionStorage directly (frequency plugin uses sessionStorage for 'session' per)
308
+ const storageKey = 'experiences:frequency:welcome-banner';
309
+ const storageData = JSON.parse(sessionStorage.getItem(storageKey) || '{}');
310
+ expect(storageData).toBeDefined();
311
+ expect(storageData.count).toBe(1);
312
+ });
313
+ });
314
+
315
+ describe('Edge Cases', () => {
316
+ it('should handle empty experience ID gracefully', () => {
317
+ expect(() => sdk.frequency.recordImpression('')).not.toThrow();
318
+ expect(sdk.frequency.getImpressionCount('')).toBe(1);
319
+ });
320
+
321
+ it('should handle very large impression counts', () => {
322
+ for (let i = 0; i < 1000; i++) {
323
+ sdk.frequency.recordImpression('welcome-banner');
324
+ }
325
+ expect(sdk.frequency.getImpressionCount('welcome-banner')).toBe(1000);
326
+ });
327
+
328
+ it('should clean up old impressions (beyond 7 days)', () => {
329
+ const now = Date.now();
330
+
331
+ // Record 5 impressions: 3 old (>7 days), 2 recent
332
+ vi.spyOn(Date, 'now').mockReturnValue(now - 10 * 24 * 60 * 60 * 1000);
333
+ sdk.frequency.recordImpression('welcome-banner');
334
+
335
+ vi.spyOn(Date, 'now').mockReturnValue(now - 8 * 24 * 60 * 60 * 1000);
336
+ sdk.frequency.recordImpression('welcome-banner');
337
+
338
+ vi.spyOn(Date, 'now').mockReturnValue(now - 9 * 24 * 60 * 60 * 1000);
339
+ sdk.frequency.recordImpression('welcome-banner');
340
+
341
+ vi.spyOn(Date, 'now').mockReturnValue(now - 2 * 24 * 60 * 60 * 1000);
342
+ sdk.frequency.recordImpression('welcome-banner');
343
+
344
+ vi.spyOn(Date, 'now').mockReturnValue(now);
345
+ sdk.frequency.recordImpression('welcome-banner');
346
+
347
+ // Total count should be 5, but old impressions cleaned up
348
+ expect(sdk.frequency.getImpressionCount('welcome-banner')).toBe(5);
349
+
350
+ // Check storage for cleaned impressions array
351
+ const storageKey = 'experiences:frequency:welcome-banner';
352
+ const storageData = JSON.parse(sessionStorage.getItem(storageKey) || '{}') as {
353
+ impressions: number[];
354
+ };
355
+ // Should only keep impressions from last 7 days (2 recent ones)
356
+ expect(storageData.impressions.length).toBe(2);
357
+
358
+ vi.restoreAllMocks();
359
+ });
360
+ });
361
+ });
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Frequency Capping Plugin
3
+ *
4
+ * Tracks experience impressions and enforces frequency caps.
5
+ * Uses sdk-kit's storage plugin for persistence.
6
+ */
7
+
8
+ import type { PluginFunction, SDK } from '@lytics/sdk-kit';
9
+ import { type StoragePlugin, storagePlugin } from '@lytics/sdk-kit-plugins';
10
+ import type { Decision, TraceStep } from '../types';
11
+
12
+ export interface FrequencyPluginConfig {
13
+ frequency?: {
14
+ enabled?: boolean;
15
+ namespace?: string;
16
+ };
17
+ }
18
+
19
+ export interface FrequencyPlugin {
20
+ getImpressionCount(experienceId: string): number;
21
+ hasReachedCap(experienceId: string, max: number, per: 'session' | 'day' | 'week'): boolean;
22
+ recordImpression(experienceId: string): void;
23
+ }
24
+
25
+ interface ImpressionData {
26
+ count: number;
27
+ lastImpression: number;
28
+ impressions: number[];
29
+ per?: 'session' | 'day' | 'week'; // Track which storage type this uses
30
+ }
31
+
32
+ /**
33
+ * Frequency Capping Plugin
34
+ *
35
+ * Automatically tracks impressions and enforces frequency caps.
36
+ * Requires storage plugin for persistence.
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * import { createInstance } from '@prosdevlab/experience-sdk';
41
+ * import { frequencyPlugin } from '@prosdevlab/experience-sdk-plugins';
42
+ *
43
+ * const sdk = createInstance({ frequency: { enabled: true } });
44
+ * sdk.use(frequencyPlugin);
45
+ * ```
46
+ */
47
+ export const frequencyPlugin: PluginFunction = (plugin, instance, config) => {
48
+ plugin.ns('frequency');
49
+
50
+ // Set defaults
51
+ plugin.defaults({
52
+ frequency: {
53
+ enabled: true,
54
+ namespace: 'experiences:frequency',
55
+ },
56
+ });
57
+
58
+ // Track experience frequency configs
59
+ const experienceFrequencyMap = new Map<string, 'session' | 'day' | 'week'>();
60
+
61
+ // Auto-load storage plugin if not already loaded
62
+ if (!(instance as SDK & { storage?: StoragePlugin }).storage) {
63
+ instance.use(storagePlugin);
64
+ }
65
+
66
+ const isEnabled = (): boolean => config.get('frequency.enabled') ?? true;
67
+ const getNamespace = (): string => config.get('frequency.namespace') ?? 'experiences:frequency';
68
+
69
+ // Helper to get the right storage backend based on frequency type
70
+ const getStorageBackend = (per: 'session' | 'day' | 'week'): Storage => {
71
+ return per === 'session' ? sessionStorage : localStorage;
72
+ };
73
+
74
+ // Helper to get storage key
75
+ const getStorageKey = (experienceId: string): string => {
76
+ return `${getNamespace()}:${experienceId}`;
77
+ };
78
+
79
+ // Helper to get impression data
80
+ const getImpressionData = (
81
+ experienceId: string,
82
+ per: 'session' | 'day' | 'week'
83
+ ): ImpressionData => {
84
+ const storage = getStorageBackend(per);
85
+ const key = getStorageKey(experienceId);
86
+ const raw = storage.getItem(key);
87
+
88
+ if (!raw) {
89
+ return {
90
+ count: 0,
91
+ lastImpression: 0,
92
+ impressions: [],
93
+ per,
94
+ };
95
+ }
96
+
97
+ try {
98
+ return JSON.parse(raw) as ImpressionData;
99
+ } catch {
100
+ return {
101
+ count: 0,
102
+ lastImpression: 0,
103
+ impressions: [],
104
+ per,
105
+ };
106
+ }
107
+ };
108
+
109
+ // Helper to save impression data
110
+ const saveImpressionData = (experienceId: string, data: ImpressionData): void => {
111
+ const per = data.per || 'session'; // Default to session if not specified
112
+ const storage = getStorageBackend(per);
113
+ const key = getStorageKey(experienceId);
114
+ storage.setItem(key, JSON.stringify(data));
115
+ };
116
+
117
+ // Get time window in milliseconds
118
+ const getTimeWindow = (per: 'session' | 'day' | 'week'): number => {
119
+ switch (per) {
120
+ case 'session':
121
+ return Number.POSITIVE_INFINITY; // Session storage handles this
122
+ case 'day':
123
+ return 24 * 60 * 60 * 1000; // 24 hours
124
+ case 'week':
125
+ return 7 * 24 * 60 * 60 * 1000; // 7 days
126
+ }
127
+ };
128
+
129
+ /**
130
+ * Get impression count for an experience
131
+ */
132
+ const getImpressionCount = (
133
+ experienceId: string,
134
+ per: 'session' | 'day' | 'week' = 'session'
135
+ ): number => {
136
+ if (!isEnabled()) return 0;
137
+ const data = getImpressionData(experienceId, per);
138
+ return data.count;
139
+ };
140
+
141
+ /**
142
+ * Check if an experience has reached its frequency cap
143
+ */
144
+ const hasReachedCap = (
145
+ experienceId: string,
146
+ max: number,
147
+ per: 'session' | 'day' | 'week'
148
+ ): boolean => {
149
+ if (!isEnabled()) return false;
150
+
151
+ const data = getImpressionData(experienceId, per);
152
+ const timeWindow = getTimeWindow(per);
153
+ const now = Date.now();
154
+
155
+ // For session caps, just check total count
156
+ if (per === 'session') {
157
+ return data.count >= max;
158
+ }
159
+
160
+ // For time-based caps, count impressions within the window
161
+ const recentImpressions = data.impressions.filter((timestamp) => now - timestamp < timeWindow);
162
+
163
+ return recentImpressions.length >= max;
164
+ };
165
+
166
+ /**
167
+ * Record an impression for an experience
168
+ */
169
+ const recordImpression = (
170
+ experienceId: string,
171
+ per: 'session' | 'day' | 'week' = 'session'
172
+ ): void => {
173
+ if (!isEnabled()) return;
174
+
175
+ const data = getImpressionData(experienceId, per);
176
+ const now = Date.now();
177
+
178
+ // Update count and add timestamp
179
+ data.count += 1;
180
+ data.lastImpression = now;
181
+ data.impressions.push(now);
182
+ data.per = per; // Store the frequency type
183
+
184
+ // Keep only recent impressions (last 7 days)
185
+ const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000;
186
+ data.impressions = data.impressions.filter((ts) => ts > sevenDaysAgo);
187
+
188
+ // Save updated data
189
+ saveImpressionData(experienceId, data);
190
+
191
+ // Emit event
192
+ instance.emit('experiences:impression-recorded', {
193
+ experienceId,
194
+ count: data.count,
195
+ timestamp: now,
196
+ });
197
+ };
198
+
199
+ // Expose frequency API
200
+ plugin.expose({
201
+ frequency: {
202
+ getImpressionCount,
203
+ hasReachedCap,
204
+ recordImpression,
205
+ // Internal method to register experience frequency config
206
+ _registerExperience: (experienceId: string, per: 'session' | 'day' | 'week') => {
207
+ experienceFrequencyMap.set(experienceId, per);
208
+ },
209
+ },
210
+ });
211
+
212
+ // Listen to evaluation events and record impressions
213
+ if (isEnabled()) {
214
+ instance.on('experiences:evaluated', (payload: unknown) => {
215
+ // Handle both single decision and array of decisions
216
+ // evaluate() emits: { decision, experience }
217
+ // evaluateAll() emits: [{ decision, experience }, ...]
218
+ const items = Array.isArray(payload) ? payload : [payload];
219
+
220
+ for (const item of items) {
221
+ // Item is { decision, experience }
222
+ const decision = (item as { decision?: Decision }).decision;
223
+
224
+ // Only record if experience was shown
225
+ if (decision?.show && decision.experienceId) {
226
+ // Try to get the 'per' value from our map, fall back to checking the input in trace
227
+ let per: 'session' | 'day' | 'week' =
228
+ experienceFrequencyMap.get(decision.experienceId) || 'session';
229
+
230
+ // If not in map, try to infer from the decision trace
231
+ if (!experienceFrequencyMap.has(decision.experienceId)) {
232
+ const freqStep = decision.trace.find(
233
+ (t: TraceStep) => t.step === 'check-frequency-cap'
234
+ );
235
+ if (freqStep?.input && typeof freqStep.input === 'object' && 'per' in freqStep.input) {
236
+ per = (freqStep.input as { per: 'session' | 'day' | 'week' }).per;
237
+ // Cache it for next time
238
+ experienceFrequencyMap.set(decision.experienceId, per);
239
+ }
240
+ }
241
+
242
+ recordImpression(decision.experienceId, per);
243
+ }
244
+ }
245
+ });
246
+ }
247
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Frequency Capping Plugin - Barrel Export
3
+ */
4
+
5
+ export type { FrequencyPlugin, FrequencyPluginConfig } from './frequency';
6
+ export { frequencyPlugin } from './frequency';
package/src/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Experience SDK Plugins
3
+ *
4
+ * Official plugins for Experience SDK
5
+ */
6
+
7
+ export * from './banner';
8
+
9
+ // Export plugins
10
+ export * from './debug';
11
+ export * from './frequency';
12
+ // Export shared types
13
+ export type {
14
+ BannerContent,
15
+ Decision,
16
+ DecisionMetadata,
17
+ Experience,
18
+ ExperienceContent,
19
+ ModalContent,
20
+ TooltipContent,
21
+ TraceStep,
22
+ } from './types';