@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,369 @@
1
+ /**
2
+ * Banner Plugin
3
+ *
4
+ * Renders banner experiences at the top or bottom of the page.
5
+ * Auto-shows banners when experiences are evaluated.
6
+ */
7
+
8
+ import type { PluginFunction } from '@lytics/sdk-kit';
9
+ import type { BannerContent, Decision, Experience } from '../types';
10
+
11
+ export interface BannerPluginConfig {
12
+ banner?: {
13
+ position?: 'top' | 'bottom';
14
+ dismissable?: boolean;
15
+ zIndex?: number;
16
+ };
17
+ }
18
+
19
+ export interface BannerPlugin {
20
+ show(experience: Experience): void;
21
+ remove(): void;
22
+ isShowing(): boolean;
23
+ }
24
+
25
+ /**
26
+ * Banner Plugin
27
+ *
28
+ * Automatically renders banner experiences when they are evaluated.
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * import { createInstance } from '@prosdevlab/experience-sdk';
33
+ * import { bannerPlugin } from '@prosdevlab/experience-sdk-plugins';
34
+ *
35
+ * const sdk = createInstance({ banner: { position: 'top', dismissable: true } });
36
+ * sdk.use(bannerPlugin);
37
+ * ```
38
+ */
39
+ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
40
+ plugin.ns('banner');
41
+
42
+ // Set defaults
43
+ plugin.defaults({
44
+ banner: {
45
+ position: 'top',
46
+ dismissable: true,
47
+ zIndex: 10000,
48
+ },
49
+ });
50
+
51
+ // Track multiple active banners by experience ID
52
+ const activeBanners = new Map<string, HTMLElement>();
53
+
54
+ /**
55
+ * Create banner DOM element
56
+ */
57
+ function createBannerElement(experience: Experience): HTMLElement {
58
+ const content = experience.content as BannerContent;
59
+ // Allow per-experience position override, fall back to global config
60
+ const position = content.position ?? config.get('banner.position') ?? 'top';
61
+ const dismissable = content.dismissable ?? config.get('banner.dismissable') ?? true;
62
+ const zIndex = config.get('banner.zIndex') ?? 10000;
63
+
64
+ // Detect dark mode
65
+ const isDarkMode = document.documentElement.classList.contains('dark');
66
+
67
+ // Theme-aware colors - professional subtle style
68
+ const bgColor = isDarkMode ? '#1f2937' : '#f9fafb';
69
+ const textColor = isDarkMode ? '#f3f4f6' : '#111827';
70
+ const borderColor = isDarkMode ? '#374151' : '#e5e7eb';
71
+ const shadowColor = isDarkMode ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.05)';
72
+
73
+ // Create banner container
74
+ const banner = document.createElement('div');
75
+ banner.setAttribute('data-experience-id', experience.id);
76
+
77
+ // Add responsive media query styles
78
+ const styleId = `banner-responsive-${experience.id}`;
79
+ if (!document.getElementById(styleId)) {
80
+ const style = document.createElement('style');
81
+ style.id = styleId;
82
+ style.textContent = `
83
+ @media (max-width: 640px) {
84
+ [data-experience-id="${experience.id}"] {
85
+ flex-direction: column !important;
86
+ align-items: flex-start !important;
87
+ }
88
+ [data-experience-id="${experience.id}"] > div:last-child {
89
+ width: 100%;
90
+ flex-direction: column !important;
91
+ }
92
+ [data-experience-id="${experience.id}"] button {
93
+ width: 100%;
94
+ }
95
+ }
96
+ `;
97
+ document.head.appendChild(style);
98
+ }
99
+
100
+ banner.style.cssText = `
101
+ position: fixed;
102
+ ${position}: 0;
103
+ left: 0;
104
+ right: 0;
105
+ background: ${bgColor};
106
+ color: ${textColor};
107
+ padding: 16px 20px;
108
+ border-${position === 'top' ? 'bottom' : 'top'}: 1px solid ${borderColor};
109
+ box-shadow: 0 ${position === 'top' ? '1' : '-1'}px 3px 0 ${shadowColor};
110
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
111
+ font-size: 14px;
112
+ line-height: 1.5;
113
+ z-index: ${zIndex};
114
+ display: flex;
115
+ align-items: center;
116
+ justify-content: space-between;
117
+ box-sizing: border-box;
118
+ `;
119
+
120
+ // Create content container
121
+ const contentDiv = document.createElement('div');
122
+ contentDiv.style.cssText = 'flex: 1; margin-right: 20px;';
123
+
124
+ // Add title if present
125
+ if (content.title) {
126
+ const title = document.createElement('div');
127
+ title.textContent = content.title;
128
+ title.style.cssText = 'font-weight: 600; margin-bottom: 4px;';
129
+ contentDiv.appendChild(title);
130
+ }
131
+
132
+ // Add message
133
+ const message = document.createElement('div');
134
+ message.textContent = content.message;
135
+ contentDiv.appendChild(message);
136
+
137
+ banner.appendChild(contentDiv);
138
+
139
+ // Create button container for actions and/or dismiss
140
+ const buttonContainer = document.createElement('div');
141
+ buttonContainer.style.cssText = `
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 12px;
145
+ flex-wrap: wrap;
146
+ `;
147
+
148
+ // Helper function to create button with variant styling
149
+ function createButton(buttonConfig: {
150
+ text: string;
151
+ action?: string;
152
+ url?: string;
153
+ variant?: 'primary' | 'secondary' | 'link';
154
+ metadata?: Record<string, unknown>;
155
+ }): HTMLButtonElement {
156
+ const button = document.createElement('button');
157
+ button.textContent = buttonConfig.text;
158
+
159
+ const variant = buttonConfig.variant || 'primary';
160
+
161
+ // Variant-based styling
162
+ let bg: string, hoverBg: string, textColor: string, border: string;
163
+
164
+ if (variant === 'primary') {
165
+ bg = isDarkMode ? '#3b82f6' : '#2563eb';
166
+ hoverBg = isDarkMode ? '#2563eb' : '#1d4ed8';
167
+ textColor = '#ffffff';
168
+ border = 'none';
169
+ } else if (variant === 'secondary') {
170
+ bg = isDarkMode ? '#374151' : '#ffffff';
171
+ hoverBg = isDarkMode ? '#4b5563' : '#f9fafb';
172
+ textColor = isDarkMode ? '#f3f4f6' : '#374151';
173
+ border = isDarkMode ? '1px solid #4b5563' : '1px solid #d1d5db';
174
+ } else {
175
+ // 'link'
176
+ bg = 'transparent';
177
+ hoverBg = isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.05)';
178
+ textColor = isDarkMode ? '#93c5fd' : '#2563eb';
179
+ border = 'none';
180
+ }
181
+
182
+ button.style.cssText = `
183
+ background: ${bg};
184
+ border: ${border};
185
+ color: ${textColor};
186
+ padding: ${variant === 'link' ? '4px 8px' : '8px 16px'};
187
+ font-size: 14px;
188
+ font-weight: ${variant === 'link' ? '400' : '500'};
189
+ border-radius: 6px;
190
+ cursor: pointer;
191
+ transition: all 0.2s;
192
+ text-decoration: ${variant === 'link' ? 'underline' : 'none'};
193
+ `;
194
+
195
+ button.addEventListener('mouseenter', () => {
196
+ button.style.background = hoverBg;
197
+ });
198
+
199
+ button.addEventListener('mouseleave', () => {
200
+ button.style.background = bg;
201
+ });
202
+
203
+ button.addEventListener('click', () => {
204
+ // Emit action event
205
+ instance.emit('experiences:action', {
206
+ experienceId: experience.id,
207
+ type: 'banner',
208
+ action: buttonConfig.action,
209
+ url: buttonConfig.url,
210
+ metadata: buttonConfig.metadata,
211
+ variant: variant,
212
+ timestamp: Date.now(),
213
+ });
214
+
215
+ // Navigate if URL provided
216
+ if (buttonConfig.url) {
217
+ window.location.href = buttonConfig.url;
218
+ }
219
+ });
220
+
221
+ return button;
222
+ }
223
+
224
+ // Add buttons from buttons array
225
+ if (content.buttons && content.buttons.length > 0) {
226
+ content.buttons.forEach((buttonConfig) => {
227
+ const button = createButton(buttonConfig);
228
+ buttonContainer.appendChild(button);
229
+ });
230
+ }
231
+
232
+ // Add dismiss button if dismissable
233
+ if (dismissable) {
234
+ const closeButton = document.createElement('button');
235
+ closeButton.innerHTML = '&times;';
236
+ closeButton.setAttribute('aria-label', 'Close banner');
237
+
238
+ const closeColor = isDarkMode ? '#9ca3af' : '#6b7280';
239
+
240
+ closeButton.style.cssText = `
241
+ background: transparent;
242
+ border: none;
243
+ color: ${closeColor};
244
+ font-size: 24px;
245
+ line-height: 1;
246
+ cursor: pointer;
247
+ padding: 0;
248
+ margin: 0;
249
+ opacity: 0.7;
250
+ transition: opacity 0.2s;
251
+ `;
252
+
253
+ closeButton.addEventListener('mouseenter', () => {
254
+ closeButton.style.opacity = '1';
255
+ });
256
+
257
+ closeButton.addEventListener('mouseleave', () => {
258
+ closeButton.style.opacity = '0.7';
259
+ });
260
+
261
+ closeButton.addEventListener('click', () => {
262
+ remove(experience.id);
263
+ instance.emit('experiences:dismissed', {
264
+ experienceId: experience.id,
265
+ type: 'banner',
266
+ });
267
+ });
268
+
269
+ buttonContainer.appendChild(closeButton);
270
+ }
271
+
272
+ banner.appendChild(buttonContainer);
273
+
274
+ return banner;
275
+ }
276
+
277
+ /**
278
+ * Show a banner experience
279
+ */
280
+ function show(experience: Experience): void {
281
+ // If banner already showing for this experience, skip
282
+ if (activeBanners.has(experience.id)) {
283
+ return;
284
+ }
285
+
286
+ // Only show if we're in a browser environment
287
+ if (typeof document === 'undefined') {
288
+ return;
289
+ }
290
+
291
+ const banner = createBannerElement(experience);
292
+ document.body.appendChild(banner);
293
+ activeBanners.set(experience.id, banner);
294
+
295
+ instance.emit('experiences:shown', {
296
+ experienceId: experience.id,
297
+ type: 'banner',
298
+ timestamp: Date.now(),
299
+ });
300
+ }
301
+
302
+ /**
303
+ * Remove a banner by experience ID (or all if no ID provided)
304
+ */
305
+ function remove(experienceId?: string): void {
306
+ if (experienceId) {
307
+ // Remove specific banner
308
+ const banner = activeBanners.get(experienceId);
309
+ if (banner?.parentNode) {
310
+ banner.parentNode.removeChild(banner);
311
+ }
312
+ activeBanners.delete(experienceId);
313
+ } else {
314
+ // Remove all banners
315
+ for (const [id, banner] of activeBanners.entries()) {
316
+ if (banner?.parentNode) {
317
+ banner.parentNode.removeChild(banner);
318
+ }
319
+ activeBanners.delete(id);
320
+ }
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Check if any banner is currently showing
326
+ */
327
+ function isShowing(): boolean {
328
+ return activeBanners.size > 0;
329
+ }
330
+
331
+ // Expose banner API
332
+ plugin.expose({
333
+ banner: {
334
+ show,
335
+ remove,
336
+ isShowing,
337
+ },
338
+ });
339
+
340
+ // Auto-show banner on experiences:evaluated event
341
+ instance.on('experiences:evaluated', (payload: unknown) => {
342
+ // Handle both single decision and array of decisions
343
+ // evaluate() emits: { decision, experience }
344
+ // evaluateAll() emits: [{ decision, experience }, ...]
345
+ const items = Array.isArray(payload) ? payload : [payload];
346
+
347
+ for (const item of items) {
348
+ // Item is { decision, experience }
349
+ const typedItem = item as { decision?: Decision; experience?: Experience };
350
+ const decision = typedItem.decision;
351
+ const experience = typedItem.experience;
352
+
353
+ // Only handle banner-type experiences
354
+ if (experience?.type === 'banner') {
355
+ if (decision?.show) {
356
+ show(experience);
357
+ } else if (experience.id && activeBanners.has(experience.id)) {
358
+ // Hide specific banner if decision says don't show
359
+ remove(experience.id);
360
+ }
361
+ }
362
+ }
363
+ });
364
+
365
+ // Cleanup on destroy
366
+ instance.on('sdk:destroy', () => {
367
+ remove();
368
+ });
369
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Banner Plugin - Barrel Export
3
+ */
4
+
5
+ export type { BannerPlugin, BannerPluginConfig } from './banner';
6
+ export { bannerPlugin } from './banner';
@@ -0,0 +1,230 @@
1
+ import { SDK } from '@lytics/sdk-kit';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { type DebugPlugin, debugPlugin } from './debug';
4
+
5
+ describe('Debug Plugin', () => {
6
+ let sdk: SDK & { debug: DebugPlugin };
7
+
8
+ beforeEach(() => {
9
+ sdk = new SDK({ debug: { enabled: true, console: true, window: true } }) as SDK & {
10
+ debug: DebugPlugin;
11
+ };
12
+ });
13
+
14
+ describe('Plugin Registration', () => {
15
+ it('should register without errors', () => {
16
+ expect(() => sdk.use(debugPlugin)).not.toThrow();
17
+ });
18
+
19
+ it('should expose debug API', () => {
20
+ sdk.use(debugPlugin);
21
+
22
+ expect(sdk.debug).toBeDefined();
23
+ expect(sdk.debug.log).toBeTypeOf('function');
24
+ expect(sdk.debug.isEnabled).toBeTypeOf('function');
25
+ });
26
+ });
27
+
28
+ describe('Configuration', () => {
29
+ it('should respect debug.enabled config', () => {
30
+ const disabledSdk = new SDK({ debug: { enabled: false } });
31
+ disabledSdk.use(debugPlugin);
32
+
33
+ expect(disabledSdk.debug.isEnabled()).toBe(false);
34
+ });
35
+
36
+ it('should default to disabled', () => {
37
+ const defaultSdk = new SDK();
38
+ defaultSdk.use(debugPlugin);
39
+
40
+ expect(defaultSdk.debug.isEnabled()).toBe(false);
41
+ });
42
+
43
+ it('should respect debug.console config', () => {
44
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
45
+
46
+ const consoleEnabledSdk = new SDK({ debug: { enabled: true, console: true } });
47
+ consoleEnabledSdk.use(debugPlugin);
48
+ consoleEnabledSdk.debug.log('test message');
49
+
50
+ expect(consoleSpy).toHaveBeenCalledWith('[experiences] test message', '');
51
+ consoleSpy.mockRestore();
52
+ });
53
+
54
+ it('should not log to console when disabled', () => {
55
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
56
+
57
+ const consoleDisabledSdk = new SDK({ debug: { enabled: true, console: false } });
58
+ consoleDisabledSdk.use(debugPlugin);
59
+ consoleDisabledSdk.debug.log('test message');
60
+
61
+ expect(consoleSpy).not.toHaveBeenCalled();
62
+ consoleSpy.mockRestore();
63
+ });
64
+ });
65
+
66
+ describe('Window Events', () => {
67
+ it('should emit window events when enabled', () => {
68
+ if (typeof window === 'undefined') {
69
+ // Skip in non-browser environment
70
+ return;
71
+ }
72
+
73
+ const eventHandler = vi.fn();
74
+ window.addEventListener('experience-sdk:debug', eventHandler);
75
+
76
+ sdk.use(debugPlugin);
77
+ sdk.debug.log('test message', { foo: 'bar' });
78
+
79
+ expect(eventHandler).toHaveBeenCalled();
80
+ const event = eventHandler.mock.calls[0][0] as CustomEvent;
81
+ expect(event.detail).toMatchObject({
82
+ message: 'test message',
83
+ data: { foo: 'bar' },
84
+ });
85
+ expect(event.detail.timestamp).toBeDefined();
86
+
87
+ window.removeEventListener('experience-sdk:debug', eventHandler);
88
+ });
89
+
90
+ it('should not emit window events when debug is disabled', () => {
91
+ if (typeof window === 'undefined') {
92
+ return;
93
+ }
94
+
95
+ const eventHandler = vi.fn();
96
+ window.addEventListener('experience-sdk:debug', eventHandler);
97
+
98
+ const disabledSdk = new SDK({ debug: { enabled: false } });
99
+ disabledSdk.use(debugPlugin);
100
+ disabledSdk.debug.log('test message');
101
+
102
+ expect(eventHandler).not.toHaveBeenCalled();
103
+
104
+ window.removeEventListener('experience-sdk:debug', eventHandler);
105
+ });
106
+
107
+ it('should not emit window events when window is disabled', () => {
108
+ if (typeof window === 'undefined') {
109
+ return;
110
+ }
111
+
112
+ const eventHandler = vi.fn();
113
+ window.addEventListener('experience-sdk:debug', eventHandler);
114
+
115
+ const windowDisabledSdk = new SDK({ debug: { enabled: true, window: false } });
116
+ windowDisabledSdk.use(debugPlugin);
117
+ windowDisabledSdk.debug.log('test message');
118
+
119
+ expect(eventHandler).not.toHaveBeenCalled();
120
+
121
+ window.removeEventListener('experience-sdk:debug', eventHandler);
122
+ });
123
+ });
124
+
125
+ describe('Event Listening', () => {
126
+ it('should listen to experiences:ready event', () => {
127
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
128
+
129
+ sdk.use(debugPlugin);
130
+ sdk.emit('experiences:ready');
131
+
132
+ expect(consoleSpy).toHaveBeenCalledWith('[experiences] SDK initialized and ready', '');
133
+
134
+ consoleSpy.mockRestore();
135
+ });
136
+
137
+ it('should listen to experiences:registered event', () => {
138
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
139
+
140
+ sdk.use(debugPlugin);
141
+ const payload = { id: 'test', experience: { type: 'banner' } };
142
+ sdk.emit('experiences:registered', payload);
143
+
144
+ expect(consoleSpy).toHaveBeenCalledWith('[experiences] Experience registered', payload);
145
+
146
+ consoleSpy.mockRestore();
147
+ });
148
+
149
+ it('should listen to experiences:evaluated event', () => {
150
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
151
+
152
+ sdk.use(debugPlugin);
153
+ const decision = { show: true, experienceId: 'test' };
154
+ sdk.emit('experiences:evaluated', decision);
155
+
156
+ expect(consoleSpy).toHaveBeenCalledWith('[experiences] Experience evaluated', decision);
157
+
158
+ consoleSpy.mockRestore();
159
+ });
160
+
161
+ it('should not log when debug is disabled', () => {
162
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
163
+
164
+ const disabledSdk = new SDK({ debug: { enabled: false } });
165
+ disabledSdk.use(debugPlugin);
166
+ disabledSdk.emit('experiences:ready');
167
+
168
+ expect(consoleSpy).not.toHaveBeenCalled();
169
+
170
+ consoleSpy.mockRestore();
171
+ });
172
+ });
173
+
174
+ describe('debug.log() method', () => {
175
+ it('should log message without data', () => {
176
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
177
+
178
+ sdk.use(debugPlugin);
179
+ sdk.debug.log('test message');
180
+
181
+ expect(consoleSpy).toHaveBeenCalledWith('[experiences] test message', '');
182
+
183
+ consoleSpy.mockRestore();
184
+ });
185
+
186
+ it('should log message with data', () => {
187
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
188
+
189
+ sdk.use(debugPlugin);
190
+ const data = { foo: 'bar', count: 42 };
191
+ sdk.debug.log('test message', data);
192
+
193
+ expect(consoleSpy).toHaveBeenCalledWith('[experiences] test message', data);
194
+
195
+ consoleSpy.mockRestore();
196
+ });
197
+
198
+ it('should include timestamp in window event', () => {
199
+ if (typeof window === 'undefined') {
200
+ return;
201
+ }
202
+
203
+ const eventHandler = vi.fn();
204
+ window.addEventListener('experience-sdk:debug', eventHandler);
205
+
206
+ sdk.use(debugPlugin);
207
+ sdk.debug.log('test message');
208
+
209
+ const event = eventHandler.mock.calls[0][0] as CustomEvent;
210
+ expect(event.detail.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
211
+
212
+ window.removeEventListener('experience-sdk:debug', eventHandler);
213
+ });
214
+ });
215
+
216
+ describe('debug.isEnabled() method', () => {
217
+ it('should return true when enabled', () => {
218
+ sdk.use(debugPlugin);
219
+
220
+ expect(sdk.debug.isEnabled()).toBe(true);
221
+ });
222
+
223
+ it('should return false when disabled', () => {
224
+ const disabledSdk = new SDK({ debug: { enabled: false } });
225
+ disabledSdk.use(debugPlugin);
226
+
227
+ expect(disabledSdk.debug.isEnabled()).toBe(false);
228
+ });
229
+ });
230
+ });
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Debug Plugin
3
+ *
4
+ * Emits structured debug events to window and optionally logs to console.
5
+ * Useful for debugging and Chrome extension integration.
6
+ */
7
+
8
+ import type { PluginFunction } from '@lytics/sdk-kit';
9
+
10
+ export interface DebugPluginConfig {
11
+ debug?: {
12
+ enabled?: boolean;
13
+ console?: boolean;
14
+ window?: boolean;
15
+ };
16
+ }
17
+
18
+ export interface DebugPlugin {
19
+ log(message: string, data?: unknown): void;
20
+ isEnabled(): boolean;
21
+ }
22
+
23
+ /**
24
+ * Debug Plugin
25
+ *
26
+ * Listens to all SDK events and emits them as window events for debugging.
27
+ * Also optionally logs to console.
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * import { createInstance } from '@prosdevlab/experience-sdk';
32
+ * import { debugPlugin } from '@prosdevlab/experience-sdk-plugins';
33
+ *
34
+ * const sdk = createInstance({ debug: { enabled: true, console: true } });
35
+ * sdk.use(debugPlugin);
36
+ * ```
37
+ */
38
+ export const debugPlugin: PluginFunction = (plugin, instance, config) => {
39
+ plugin.ns('debug');
40
+
41
+ // Set defaults
42
+ plugin.defaults({
43
+ debug: {
44
+ enabled: false,
45
+ console: false,
46
+ window: true,
47
+ },
48
+ });
49
+
50
+ // Helper to check if debug is enabled
51
+ const isEnabled = (): boolean => config.get('debug.enabled') ?? false;
52
+ const shouldLogConsole = (): boolean => config.get('debug.console') ?? false;
53
+ const shouldEmitWindow = (): boolean => config.get('debug.window') ?? true;
54
+
55
+ // Log function
56
+ const log = (message: string, data?: unknown): void => {
57
+ if (!isEnabled()) return;
58
+
59
+ const timestamp = new Date().toISOString();
60
+ const logData = {
61
+ timestamp,
62
+ message,
63
+ data,
64
+ };
65
+
66
+ // Console logging
67
+ if (shouldLogConsole()) {
68
+ console.log(`[experiences] ${message}`, data || '');
69
+ }
70
+
71
+ // Window event emission
72
+ if (shouldEmitWindow() && typeof window !== 'undefined') {
73
+ const event = new CustomEvent('experience-sdk:debug', {
74
+ detail: logData,
75
+ });
76
+ window.dispatchEvent(event);
77
+ }
78
+ };
79
+
80
+ // Expose debug API
81
+ plugin.expose({
82
+ debug: {
83
+ log,
84
+ isEnabled,
85
+ },
86
+ });
87
+
88
+ // If debug is enabled, listen to all events
89
+ if (isEnabled()) {
90
+ // Listen to experiences:* events
91
+ instance.on('experiences:ready', () => {
92
+ if (!isEnabled()) return;
93
+ log('SDK initialized and ready');
94
+ });
95
+
96
+ instance.on('experiences:registered', (payload) => {
97
+ if (!isEnabled()) return;
98
+ log('Experience registered', payload);
99
+ });
100
+
101
+ instance.on('experiences:evaluated', (payload) => {
102
+ if (!isEnabled()) return;
103
+ log('Experience evaluated', payload);
104
+ });
105
+ }
106
+ };