Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
235 changes: 235 additions & 0 deletions packages/plugins/src/debug/debug.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});

107 changes: 107 additions & 0 deletions packages/plugins/src/debug/debug.ts
Original file line number Diff line number Diff line change
@@ -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);
});
}
};

7 changes: 7 additions & 0 deletions packages/plugins/src/debug/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Debug Plugin - Barrel Export
*/

export type { DebugPlugin, DebugPluginConfig } from './debug';
export { debugPlugin } from './debug';

9 changes: 6 additions & 3 deletions packages/plugins/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';