import { describe, it, expect, vi, beforeEach } from 'vitest'; import { ConfigManager } from '../src/core/ConfigManager'; import type { LightConfig, SentryEvent, ErrorEvent } from '../src/types'; // Mock the env module vi.mock('../src/utils/env', () => ({ getContexts: vi.fn(() => ({ browser: { name: 'c', version: '120' }, os: { name: 'm', version: '14' }, device: { type: 'desktop' }, })), getContextId: vi.fn(() => 'test_context_id'), getPageUrl: vi.fn(() => 'https://example.com/page'), getReferrer: vi.fn(() => 'https://google.com'), })); describe('ConfigManager', () => { let configManager: ConfigManager; beforeEach(() => { vi.clearAllMocks(); }); describe('constructor', () => { it('should create ConfigManager with valid config', () => { const config: LightConfig = { dsn: 'https://proj_abc123@log.example.com/1001', release: '1.0.0', environment: 'production', }; configManager = new ConfigManager(config); expect(configManager).toBeDefined(); }); it('should use default values for optional config', () => { const config: LightConfig = { dsn: 'https://proj_abc123@log.example.com/1001', }; configManager = new ConfigManager(config); expect(configManager.get('enabled')).toBe(true); expect(configManager.get('sampleRate')).toBe(1); expect(configManager.get('environment')).toBe('production'); }); }); describe('get/set methods', () => { beforeEach(() => { configManager = new ConfigManager({ dsn: 'https://proj_abc123@log.example.com/1001', }); }); it('should get configured value', () => { expect(configManager.get('environment')).toBe('production'); }); it('should set and get value', () => { configManager.set('environment', 'development'); expect(configManager.get('environment')).toBe('development'); }); it('should set user info', () => { configManager.setUser({ id: '123', username: 'test' }); const config = configManager.getAll(); expect(config.user?.id).toBe('123'); }); it('should set single tag', () => { configManager.setTag('page', 'home'); const config = configManager.getAll(); expect(config.tags?.page).toBe('home'); }); it('should set multiple tags', () => { configManager.setTags({ page: 'home', version: '2.0' }); const config = configManager.getAll(); expect(config.tags?.page).toBe('home'); expect(config.tags?.version).toBe('2.0'); }); it('should set extra data', () => { configManager.setExtra('order_id', '12345'); const config = configManager.getAll(); expect(config.extra?.order_id).toBe('12345'); }); }); describe('shouldSample', () => { it('should return true when sampleRate is 1', () => { configManager = new ConfigManager({ dsn: 'https://proj_abc123@log.example.com/1001', sampleRate: 1, }); expect(configManager.shouldSample()).toBe(true); }); it('should return false when sampleRate is 0', () => { configManager = new ConfigManager({ dsn: 'https://proj_abc123@log.example.com/1001', sampleRate: 0, }); expect(configManager.shouldSample()).toBe(false); }); }); describe('isIgnoredError', () => { beforeEach(() => { configManager = new ConfigManager({ dsn: 'https://proj_abc123@log.example.com/1001', ignoreErrors: [/^Script error/i, 'ResizeObserver'], }); }); it('should ignore matching pattern', () => { expect(configManager.isIgnoredError('Script error')).toBe(true); }); it('should ignore matching regex', () => { expect(configManager.isIgnoredError('Script error: something')).toBe(true); }); it('should not ignore non-matching message', () => { expect(configManager.isIgnoredError('TypeError: something')).toBe(false); }); it('should handle string pattern', () => { expect(configManager.isIgnoredError('Some ResizeObserver issue')).toBe(true); }); }); describe('applyToEvent', () => { beforeEach(() => { configManager = new ConfigManager({ dsn: 'https://proj_abc123@log.example.com/1001', release: '1.0.0', environment: 'production', user: { id: '123' }, tags: { page: 'home' }, }); }); it('should apply release to event', () => { const event: SentryEvent = { type: 'error', level: 'error', timestamp: Date.now(), } as ErrorEvent; const result = configManager.applyToEvent(event); expect(result.release).toBe('1.0.0'); }); it('should apply environment to event', () => { const event: SentryEvent = { type: 'error', level: 'error', timestamp: Date.now(), } as ErrorEvent; const result = configManager.applyToEvent(event); expect(result.environment).toBe('production'); }); it('should apply user to event', () => { const event: SentryEvent = { type: 'error', level: 'error', timestamp: Date.now(), } as ErrorEvent; const result = configManager.applyToEvent(event); expect(result.user?.id).toBe('123'); }); it('should apply and merge tags', () => { const event: SentryEvent = { type: 'error', level: 'error', timestamp: Date.now(), tags: { custom: 'value' }, } as ErrorEvent; const result = configManager.applyToEvent(event); expect(result.tags?.page).toBe('home'); expect(result.tags?.custom).toBe('value'); }); it('should add context_id to event', () => { const event: SentryEvent = { type: 'error', level: 'error', timestamp: Date.now(), } as ErrorEvent; const result = configManager.applyToEvent(event); expect(result.context_id).toBe('test_context_id'); }); it('should add contexts on first event', () => { const event: SentryEvent = { type: 'error', level: 'error', timestamp: Date.now(), } as ErrorEvent; const result = configManager.applyToEvent(event); expect(result.contexts).toBeDefined(); expect(result.contexts?.browser).toBeDefined(); }); it('should add request info to event', () => { const event: SentryEvent = { type: 'error', level: 'error', timestamp: Date.now(), } as ErrorEvent; const result = configManager.applyToEvent(event); expect(result.request?.url).toBe('https://example.com/page'); expect(result.request?.referrer).toBe('https://google.com'); }); it('should not overwrite existing request.url', () => { const event: SentryEvent = { type: 'error', level: 'error', timestamp: Date.now(), request: { url: 'https://custom.com/api' }, } as ErrorEvent; const result = configManager.applyToEvent(event); expect(result.request?.url).toBe('https://custom.com/api'); }); }); describe('markContextReported', () => { beforeEach(() => { configManager = new ConfigManager({ dsn: 'https://proj_abc123@log.example.com/1001', }); }); it('should add contexts on first event', () => { configManager.markContextReported(); const event: SentryEvent = { type: 'error', level: 'error', timestamp: Date.now(), } as ErrorEvent; const result = configManager.applyToEvent(event); expect(result.contexts).toBeUndefined(); expect(result.context_id).toBe('test_context_id'); }); }); describe('anonymousId', () => { beforeEach(() => { localStorage.clear(); }); it('should add anonymousId when user.id is not set and tracking is enabled', () => { configManager = new ConfigManager({ dsn: 'https://proj_abc123@log.example.com/1001', trackAnonymousUsers: true, }); const event: SentryEvent = { type: 'error', level: 'error', timestamp: Date.now(), } as ErrorEvent; const result = configManager.applyToEvent(event); expect((result as any).anonymousId).toBeTruthy(); expect(typeof (result as any).anonymousId).toBe('string'); }); it('should not add anonymousId when user.id is set', () => { configManager = new ConfigManager({ dsn: 'https://proj_abc123@log.example.com/1001', user: { id: 'user-123' }, trackAnonymousUsers: true, }); const event: SentryEvent = { type: 'error', level: 'error', timestamp: Date.now(), } as ErrorEvent; const result = configManager.applyToEvent(event); expect((result as any).anonymousId).toBeUndefined(); }); it('should not add anonymousId when trackAnonymousUsers is false', () => { configManager = new ConfigManager({ dsn: 'https://proj_abc123@log.example.com/1001', trackAnonymousUsers: false, }); const event: SentryEvent = { type: 'error', level: 'error', timestamp: Date.now(), } as ErrorEvent; const result = configManager.applyToEvent(event); expect((result as any).anonymousId).toBeUndefined(); }); it('should not add anonymousId when event has user.id', () => { configManager = new ConfigManager({ dsn: 'https://proj_abc123@log.example.com/1001', trackAnonymousUsers: true, }); const event: SentryEvent = { type: 'error', level: 'error', timestamp: Date.now(), user: { id: 'event-user-id' }, } as ErrorEvent; const result = configManager.applyToEvent(event); expect((result as any).anonymousId).toBeUndefined(); }); }); describe('contextLevel', () => { beforeEach(() => { configManager = new ConfigManager({ dsn: 'https://proj_abc123@log.example.com/1001', contextLevel: { error: { maxStackFrames: 5, maxBreadcrumbs: 10 }, info: { maxStackFrames: 0, maxBreadcrumbs: 0 }, warning: { maxStackFrames: 3, maxBreadcrumbs: 5 }, }, }); }); it('should truncate stack frames for error level', () => { const frames = Array.from({ length: 10 }, (_, i) => ({ filename: `file${i}.js`, function: `fn${i}`, lineno: i * 10, colno: i, })); const event: SentryEvent = { type: 'error', level: 'error', timestamp: Date.now(), exception: { type: 'Error', value: 'test', stacktrace: { frames }, }, } as ErrorEvent; const result = configManager.applyToEvent(event); expect((result as ErrorEvent).exception?.stacktrace?.frames).toHaveLength(5); }); it('should remove exception entirely for info level (maxStackFrames=0)', () => { const frames = [{ filename: 'file.js', function: 'fn', lineno: 1, colno: 1 }]; const event: SentryEvent = { type: 'error', level: 'info', timestamp: Date.now(), exception: { type: 'Error', value: 'test', stacktrace: { frames }, }, } as ErrorEvent; const result = configManager.applyToEvent(event); expect((result as ErrorEvent).exception).toBeUndefined(); }); it('should truncate breadcrumbs for error level', () => { const breadcrumbs = Array.from({ length: 20 }, (_, i) => ({ type: 'default', message: `breadcrumb ${i}`, timestamp: Date.now() + i, })); const event: SentryEvent = { type: 'error', level: 'error', timestamp: Date.now(), breadcrumbs, } as ErrorEvent; const result = configManager.applyToEvent(event); expect(result.breadcrumbs).toHaveLength(10); }); it('should remove breadcrumbs for info level (maxBreadcrumbs=0)', () => { const breadcrumbs = [{ type: 'default', message: 'test', timestamp: Date.now() }]; const event: SentryEvent = { type: 'error', level: 'info', timestamp: Date.now(), breadcrumbs, } as ErrorEvent; const result = configManager.applyToEvent(event); expect(result.breadcrumbs).toBeUndefined(); }); it('should keep last N breadcrumbs', () => { const breadcrumbs = Array.from({ length: 20 }, (_, i) => ({ type: 'default', message: `breadcrumb ${i}`, timestamp: Date.now() + i, })); const event: SentryEvent = { type: 'error', level: 'warning', timestamp: Date.now(), breadcrumbs, } as ErrorEvent; const result = configManager.applyToEvent(event); expect(result.breadcrumbs).toHaveLength(5); // Should keep last 5 expect(result.breadcrumbs?.[0].message).toBe('breadcrumb 15'); expect(result.breadcrumbs?.[4].message).toBe('breadcrumb 19'); }); it('should not modify original event', () => { const frames = Array.from({ length: 10 }, (_, i) => ({ filename: `file${i}.js`, function: `fn${i}`, lineno: i * 10, colno: i, })); const event: ErrorEvent = { type: 'error', level: 'error', message: 'test', timestamp: Date.now(), exception: { type: 'Error', value: 'test', stacktrace: { frames }, }, }; configManager.applyToEvent(event); // Original event should not be modified expect(event.exception?.stacktrace?.frames).toHaveLength(10); }); it('should handle event without exception', () => { const event: SentryEvent = { type: 'error', level: 'error', timestamp: Date.now(), } as ErrorEvent; const result = configManager.applyToEvent(event); expect(result).toBeDefined(); }); it('should handle event without breadcrumbs', () => { const event: SentryEvent = { type: 'error', level: 'error', timestamp: Date.now(), } as ErrorEvent; const result = configManager.applyToEvent(event); expect(result).toBeDefined(); }); }); describe('setUser', () => { beforeEach(() => { configManager = new ConfigManager({ dsn: 'https://proj_abc123@log.example.com/1001', user: { id: 'existing-user' }, }); }); it('should set user info', () => { configManager.setUser({ id: 'new-user', username: 'test' }); expect(configManager.get('user')).toEqual({ id: 'new-user', username: 'test' }); }); it('should clear user info when null is passed', () => { configManager.setUser(null); expect(configManager.get('user')).toBeUndefined(); }); }); describe('setTag / setTags / setExtra', () => { beforeEach(() => { configManager = new ConfigManager({ dsn: 'https://proj_abc123@log.example.com/1001', }); }); it('should set tag when no tags exist', () => { configManager.setTag('key1', 'value1'); expect(configManager.get('tags')).toEqual({ key1: 'value1' }); }); it('should merge tags', () => { configManager.setTag('key1', 'value1'); configManager.setTags({ key2: 'value2', key3: 'value3' }); expect(configManager.get('tags')).toEqual({ key1: 'value1', key2: 'value2', key3: 'value3', }); }); it('should set extra when no extra exists', () => { configManager.setExtra('key1', 'value1'); expect(configManager.get('extra')).toEqual({ key1: 'value1' }); }); }); });