diff --git a/packages/plugins/src/debug/debug.test.ts b/packages/plugins/src/debug/debug.test.ts new file mode 100644 index 0000000..cb7adbf --- /dev/null +++ b/packages/plugins/src/debug/debug.test.ts @@ -0,0 +1,235 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { SDK } from '@lytics/sdk-kit'; +import { debugPlugin } from './debug'; + +describe('Debug Plugin', () => { + let sdk: SDK; + + beforeEach(() => { + sdk = new SDK({ debug: { enabled: true, console: true, window: true } }); + }); + + describe('Plugin Registration', () => { + it('should register without errors', () => { + expect(() => sdk.use(debugPlugin)).not.toThrow(); + }); + + it('should expose debug API', () => { + sdk.use(debugPlugin); + + expect(sdk.debug).toBeDefined(); + expect(sdk.debug.log).toBeTypeOf('function'); + expect(sdk.debug.isEnabled).toBeTypeOf('function'); + }); + }); + + describe('Configuration', () => { + it('should respect debug.enabled config', () => { + const disabledSdk = new SDK({ debug: { enabled: false } }); + disabledSdk.use(debugPlugin); + + expect(disabledSdk.debug.isEnabled()).toBe(false); + }); + + it('should default to disabled', () => { + const defaultSdk = new SDK(); + defaultSdk.use(debugPlugin); + + expect(defaultSdk.debug.isEnabled()).toBe(false); + }); + + it('should respect debug.console config', () => { + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + const consoleEnabledSdk = new SDK({ debug: { enabled: true, console: true } }); + consoleEnabledSdk.use(debugPlugin); + consoleEnabledSdk.debug.log('test message'); + + expect(consoleSpy).toHaveBeenCalledWith('[experiences] test message', ''); + consoleSpy.mockRestore(); + }); + + it('should not log to console when disabled', () => { + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + const consoleDisabledSdk = new SDK({ debug: { enabled: true, console: false } }); + consoleDisabledSdk.use(debugPlugin); + consoleDisabledSdk.debug.log('test message'); + + expect(consoleSpy).not.toHaveBeenCalled(); + consoleSpy.mockRestore(); + }); + }); + + describe('Window Events', () => { + it('should emit window events when enabled', () => { + if (typeof window === 'undefined') { + // Skip in non-browser environment + return; + } + + const eventHandler = vi.fn(); + window.addEventListener('experience-sdk:debug', eventHandler); + + sdk.use(debugPlugin); + sdk.debug.log('test message', { foo: 'bar' }); + + expect(eventHandler).toHaveBeenCalled(); + const event = eventHandler.mock.calls[0][0] as CustomEvent; + expect(event.detail).toMatchObject({ + message: 'test message', + data: { foo: 'bar' }, + }); + expect(event.detail.timestamp).toBeDefined(); + + window.removeEventListener('experience-sdk:debug', eventHandler); + }); + + it('should not emit window events when debug is disabled', () => { + if (typeof window === 'undefined') { + return; + } + + const eventHandler = vi.fn(); + window.addEventListener('experience-sdk:debug', eventHandler); + + const disabledSdk = new SDK({ debug: { enabled: false } }); + disabledSdk.use(debugPlugin); + disabledSdk.debug.log('test message'); + + expect(eventHandler).not.toHaveBeenCalled(); + + window.removeEventListener('experience-sdk:debug', eventHandler); + }); + + it('should not emit window events when window is disabled', () => { + if (typeof window === 'undefined') { + return; + } + + const eventHandler = vi.fn(); + window.addEventListener('experience-sdk:debug', eventHandler); + + const windowDisabledSdk = new SDK({ debug: { enabled: true, window: false } }); + windowDisabledSdk.use(debugPlugin); + windowDisabledSdk.debug.log('test message'); + + expect(eventHandler).not.toHaveBeenCalled(); + + window.removeEventListener('experience-sdk:debug', eventHandler); + }); + }); + + describe('Event Listening', () => { + it('should listen to experiences:ready event', () => { + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + sdk.use(debugPlugin); + sdk.emit('experiences:ready'); + + expect(consoleSpy).toHaveBeenCalledWith( + '[experiences] SDK initialized and ready', + '' + ); + + consoleSpy.mockRestore(); + }); + + it('should listen to experiences:registered event', () => { + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + sdk.use(debugPlugin); + const payload = { id: 'test', experience: { type: 'banner' } }; + sdk.emit('experiences:registered', payload); + + expect(consoleSpy).toHaveBeenCalledWith( + '[experiences] Experience registered', + payload + ); + + consoleSpy.mockRestore(); + }); + + it('should listen to experiences:evaluated event', () => { + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + sdk.use(debugPlugin); + const decision = { show: true, experienceId: 'test' }; + sdk.emit('experiences:evaluated', decision); + + expect(consoleSpy).toHaveBeenCalledWith('[experiences] Experience evaluated', decision); + + consoleSpy.mockRestore(); + }); + + it('should not log when debug is disabled', () => { + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + const disabledSdk = new SDK({ debug: { enabled: false } }); + disabledSdk.use(debugPlugin); + disabledSdk.emit('experiences:ready'); + + expect(consoleSpy).not.toHaveBeenCalled(); + + consoleSpy.mockRestore(); + }); + }); + + describe('debug.log() method', () => { + it('should log message without data', () => { + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + sdk.use(debugPlugin); + sdk.debug.log('test message'); + + expect(consoleSpy).toHaveBeenCalledWith('[experiences] test message', ''); + + consoleSpy.mockRestore(); + }); + + it('should log message with data', () => { + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + sdk.use(debugPlugin); + const data = { foo: 'bar', count: 42 }; + sdk.debug.log('test message', data); + + expect(consoleSpy).toHaveBeenCalledWith('[experiences] test message', data); + + consoleSpy.mockRestore(); + }); + + it('should include timestamp in window event', () => { + if (typeof window === 'undefined') { + return; + } + + const eventHandler = vi.fn(); + window.addEventListener('experience-sdk:debug', eventHandler); + + sdk.use(debugPlugin); + sdk.debug.log('test message'); + + const event = eventHandler.mock.calls[0][0] as CustomEvent; + expect(event.detail.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); + + window.removeEventListener('experience-sdk:debug', eventHandler); + }); + }); + + describe('debug.isEnabled() method', () => { + it('should return true when enabled', () => { + sdk.use(debugPlugin); + + expect(sdk.debug.isEnabled()).toBe(true); + }); + + it('should return false when disabled', () => { + const disabledSdk = new SDK({ debug: { enabled: false } }); + disabledSdk.use(debugPlugin); + + expect(disabledSdk.debug.isEnabled()).toBe(false); + }); + }); +}); + diff --git a/packages/plugins/src/debug/debug.ts b/packages/plugins/src/debug/debug.ts new file mode 100644 index 0000000..ca1a19f --- /dev/null +++ b/packages/plugins/src/debug/debug.ts @@ -0,0 +1,107 @@ +/** + * Debug Plugin + * + * Emits structured debug events to window and optionally logs to console. + * Useful for debugging and Chrome extension integration. + */ + +import type { PluginFunction } from '@lytics/sdk-kit'; + +export interface DebugPluginConfig { + debug?: { + enabled?: boolean; + console?: boolean; + window?: boolean; + }; +} + +export interface DebugPlugin { + log(message: string, data?: unknown): void; + isEnabled(): boolean; +} + +/** + * Debug Plugin + * + * Listens to all SDK events and emits them as window events for debugging. + * Also optionally logs to console. + * + * @example + * ```typescript + * import { createInstance } from '@prosdevlab/experience-sdk'; + * import { debugPlugin } from '@prosdevlab/experience-sdk-plugins'; + * + * const sdk = createInstance({ debug: { enabled: true, console: true } }); + * sdk.use(debugPlugin); + * ``` + */ +export const debugPlugin: PluginFunction = (plugin, instance, config) => { + plugin.ns('debug'); + + // Set defaults + plugin.defaults({ + debug: { + enabled: false, + console: false, + window: true, + }, + }); + + // Helper to check if debug is enabled + const isEnabled = (): boolean => config.get('debug.enabled') ?? false; + const shouldLogConsole = (): boolean => config.get('debug.console') ?? false; + const shouldEmitWindow = (): boolean => config.get('debug.window') ?? true; + + // Log function + const log = (message: string, data?: unknown): void => { + if (!isEnabled()) return; + + const timestamp = new Date().toISOString(); + const logData = { + timestamp, + message, + data, + }; + + // Console logging + if (shouldLogConsole()) { + console.log(`[experiences] ${message}`, data || ''); + } + + // Window event emission + if (shouldEmitWindow() && typeof window !== 'undefined') { + const event = new CustomEvent('experience-sdk:debug', { + detail: logData, + }); + window.dispatchEvent(event); + } + }; + + // Expose debug API + plugin.expose({ + debug: { + log, + isEnabled, + }, + }); + + // If debug is enabled, listen to all events + if (isEnabled()) { + // Listen to experiences:* events + instance.on('experiences:ready', () => { + if (!isEnabled()) return; + log('SDK initialized and ready'); + }); + + instance.on('experiences:registered', (payload) => { + if (!isEnabled()) return; + log('Experience registered', payload); + }); + + instance.on('experiences:evaluated', (payload) => { + if (!isEnabled()) return; + log('Experience evaluated', payload); + }); + } +}; + diff --git a/packages/plugins/src/debug/index.ts b/packages/plugins/src/debug/index.ts new file mode 100644 index 0000000..0dbf4dc --- /dev/null +++ b/packages/plugins/src/debug/index.ts @@ -0,0 +1,7 @@ +/** + * Debug Plugin - Barrel Export + */ + +export type { DebugPlugin, DebugPluginConfig } from './debug'; +export { debugPlugin } from './debug'; + diff --git a/packages/plugins/src/index.ts b/packages/plugins/src/index.ts index 0125bac..3fc11ae 100644 --- a/packages/plugins/src/index.ts +++ b/packages/plugins/src/index.ts @@ -1,4 +1,7 @@ -// Placeholder for plugins package -// Will be implemented in Phase 0 +/** + * Experience SDK Plugins + * + * Official plugins for Experience SDK + */ -export {}; +export * from './debug';