light-sentry-sdk/tests/TracingManager.test.ts

351 lines
11 KiB
TypeScript

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<typeof createMockClient>;
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;
}