351 lines
11 KiB
TypeScript
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;
|
|
}
|