import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { TracingManager } from '../src/core/TracingManager'; import type { LightConfig, Transaction, Span } from '../src/types'; describe('TracingManager', () => { let tracingManager: TracingManager; let mockClient: ReturnType; beforeEach(() => { mockClient = createMockClient(); const config: LightConfig = { dsn: 'https://abc@log.example.com/1001', tracesSampleRate: 1, }; tracingManager = new TracingManager(config, mockClient, 'abc'); }); afterEach(() => { tracingManager.destroy(); vi.clearAllMocks(); }); describe('startTransaction', () => { it('should create transaction with correct properties', () => { const transaction = tracingManager.startTransaction({ name: 'test-transaction', op: 'custom', }); expect(transaction).toBeDefined(); expect(transaction?.name).toBe('test-transaction'); expect(transaction?.op).toBe('custom'); expect(transaction?.traceId).toBeDefined(); expect(transaction?.spanId).toBeDefined(); expect(transaction?.sampled).toBe(true); // tracesSampleRate = 1 }); it('should return transaction even when SDK would be disabled (TracingManager does not check enabled)', () => { // Note: Client.startTransaction checks enabled, but TracingManager does not const disabledConfig: LightConfig = { dsn: 'https://abc@log.example.com/1001', enabled: false, tracesSampleRate: 1, }; const disabledManager = new TracingManager(disabledConfig, mockClient, 'abc'); const transaction = disabledManager.startTransaction({ name: 'test-transaction', }); // TracingManager doesn't know about enabled flag, it still creates transaction expect(transaction).toBeDefined(); disabledManager.destroy(); }); it('should sample transaction based on tracesSampleRate', () => { const sampledConfig: LightConfig = { dsn: 'https://abc@log.example.com/1001', tracesSampleRate: 0, // No sampling }; const sampledManager = new TracingManager(sampledConfig, mockClient, 'abc'); const transaction = sampledManager.startTransaction({ name: 'test-transaction', }); expect(transaction?.sampled).toBe(false); sampledManager.destroy(); }); it('should use tracesSampler for dynamic sampling', () => { const customSamplerConfig: LightConfig = { dsn: 'https://abc@log.example.com/1001', tracesSampler: () => false, }; const customSamplerManager = new TracingManager(customSamplerConfig, mockClient, 'abc'); const transaction = customSamplerManager.startTransaction({ name: 'test-transaction', }); expect(transaction?.sampled).toBe(false); customSamplerManager.destroy(); }); it('should set current transaction after start', () => { const transaction = tracingManager.startTransaction({ name: 'test-transaction', }); expect(tracingManager.getCurrentTransaction()).toBe(transaction); }); it('should use tracesSampler for dynamic sampling with parentSampled', () => { // With tracesSampler, parentSampled is passed to the sampler const customSamplerConfig: LightConfig = { dsn: 'https://abc@log.example.com/1001', tracesSampler: (ctx) => { // If parent was sampled=false, child should also be not sampled if (ctx.parentSampled === false) { return false; } return true; }, }; const parentSamplerManager = new TracingManager(customSamplerConfig, mockClient, 'abc'); const parentTransaction = parentSamplerManager.startTransaction({ name: 'parent', sampled: false, }); const childTransaction = parentSamplerManager.startTransaction({ name: 'child', sampled: undefined, }); // Child should inherit parent's sampled=false via tracesSampler expect(childTransaction?.sampled).toBe(false); parentSamplerManager.destroy(); }); }); describe('getCurrentTransaction', () => { it('should return null when no transaction exists', () => { expect(tracingManager.getCurrentTransaction()).toBeNull(); }); it('should return current transaction', () => { const transaction = tracingManager.startTransaction({ name: 'test-transaction', }); expect(tracingManager.getCurrentTransaction()).toBe(transaction); }); it('should return current transaction after finish (via TracingManager.onTransactionFinished)', () => { // Note: Transaction.finish() calls client.tracingManager.onTransactionFinished // if client is properly set up. With mock client, this doesn't happen automatically. const transaction = tracingManager.startTransaction({ name: 'test-transaction', }); expect(tracingManager.getCurrentTransaction()).toBe(transaction); // Manually call onTransactionFinished to simulate proper cleanup tracingManager.onTransactionFinished(transaction!); expect(tracingManager.getCurrentTransaction()).toBeNull(); }); }); describe('getCurrentSpan', () => { it('should return transaction as span when no child spans', () => { const transaction = tracingManager.startTransaction({ name: 'test-transaction', }); const currentSpan = tracingManager.getCurrentSpan(); expect(currentSpan?.spanId).toBe(transaction?.spanId); }); it('should return most recent child span', () => { const transaction = tracingManager.startTransaction({ name: 'test-transaction', }); const childSpan = tracingManager.startSpan({ op: 'child.span', description: 'child operation', }); const currentSpan = tracingManager.getCurrentSpan(); expect(currentSpan?.spanId).toBe(childSpan?.spanId); }); }); describe('startSpan', () => { it('should create child span under current transaction', () => { tracingManager.startTransaction({ name: 'test-transaction', }); const childSpan = tracingManager.startSpan({ op: 'http.client', description: 'GET /api/users', }); expect(childSpan).toBeDefined(); expect(childSpan?.op).toBe('http.client'); expect(childSpan?.parentSpanId).toBeDefined(); }); it('should return null when no active span and no traceId', () => { const noSpanManager = new TracingManager( { dsn: 'https://abc@log.example.com/1001' }, mockClient, 'abc' ); const span = noSpanManager.startSpan({ op: 'test.span', }); expect(span).toBeNull(); noSpanManager.destroy(); }); it('should create span with traceId when no parent span exists', () => { const noSpanManager = new TracingManager( { dsn: 'https://abc@log.example.com/1001' }, mockClient, 'abc' ); const span = noSpanManager.startSpan({ op: 'test.span', traceId: 'abcd1234abcd1234abcd1234abcd1234', }); expect(span).toBeDefined(); expect(span?.traceId).toBe('abcd1234abcd1234abcd1234abcd1234'); noSpanManager.destroy(); }); it('should not add span to unsampled transaction', () => { const unsampledManager = new TracingManager( { dsn: 'https://abc@log.example.com/1001', tracesSampleRate: 0 }, mockClient, 'abc' ); const transaction = unsampledManager.startTransaction({ name: 'unsampled' }); const span = unsampledManager.startSpan({ op: 'child' }); expect(span).toBeDefined(); // Span should not be in transaction.spans for unsampled transaction expect(transaction?.spans.length).toBe(0); unsampledManager.destroy(); }); }); describe('setCurrentSpan', () => { it('should set current span', () => { tracingManager.startTransaction({ name: 'test' }); const customSpan = { spanId: 'custom-span' } as Span; tracingManager.setCurrentSpan(customSpan); expect(tracingManager.getCurrentSpan()?.spanId).toBe('custom-span'); }); it('should pop span from stack when set to null', () => { tracingManager.startTransaction({ name: 'test' }); const childSpan = tracingManager.startSpan({ op: 'child' }); expect(tracingManager.getCurrentSpan()?.spanId).toBe(childSpan?.spanId); tracingManager.setCurrentSpan(null); // Should return to transaction expect(tracingManager.getCurrentSpan()?.spanId).toBeDefined(); }); }); describe('onSpanFinished', () => { it('should remove finished span from stack', () => { tracingManager.startTransaction({ name: 'test' }); const childSpan = tracingManager.startSpan({ op: 'child' }); expect(tracingManager.getCurrentSpan()?.spanId).toBe(childSpan?.spanId); tracingManager.onSpanFinished(childSpan!); // Should return to transaction after child is removed expect(tracingManager.getCurrentSpan()?.spanId).toBeDefined(); }); }); describe('onTransactionFinished', () => { it('should clear current transaction', () => { const transaction = tracingManager.startTransaction({ name: 'test' }); expect(tracingManager.getCurrentTransaction()).toBe(transaction); tracingManager.onTransactionFinished(transaction!); expect(tracingManager.getCurrentTransaction()).toBeNull(); }); }); describe('updateConfig', () => { it('should update baggage when user changes', () => { const transaction = tracingManager.startTransaction({ name: 'test' }); expect(transaction).toBeDefined(); tracingManager.updateConfig({ dsn: 'https://abc@log.example.com/1001', user: { id: 'user123', segment: 'premium' }, }); // Transaction baggage should have user info // This is an internal detail, just verify no error expect(transaction).toBeDefined(); }); }); describe('destroy', () => { it('should finish current transaction on destroy', () => { const transaction = tracingManager.startTransaction({ name: 'test' }); tracingManager.destroy(); // Transaction should be finished (sampled=false since no client capture) expect(tracingManager.getCurrentTransaction()).toBeNull(); }); it('should clear span stack on destroy', () => { tracingManager.startTransaction({ name: 'test' }); tracingManager.startSpan({ op: 'child' }); tracingManager.destroy(); expect(tracingManager.getCurrentSpan()).toBeNull(); }); }); }); function createMockClient() { return { config: {}, dsn: { protocol: 'https', publicKey: 'abc', host: 'log.example.com', projectId: '1001' }, captureException: vi.fn(), captureMessage: vi.fn(), captureEvent: vi.fn(), setUser: vi.fn(), setAnonymousId: vi.fn(), setTag: vi.fn(), setTags: vi.fn(), setExtra: vi.fn(), addBreadcrumb: vi.fn(), startTransaction: vi.fn(), getCurrentTransaction: vi.fn(), getCurrentSpan: vi.fn(), setSpan: vi.fn(), startSpan: vi.fn(), flush: vi.fn(), disable: vi.fn(), enable: vi.fn(), use: vi.fn(), init: vi.fn(), destroy: vi.fn(), on: vi.fn(), off: vi.fn(), emit: vi.fn(), } as any; }