import { describe, it, expect, vi, beforeEach } from 'vitest'; import { Span, Transaction, httpStatusToSpanStatus, dbStatusToSpanStatus, statusToSpanStatus } from '../src/utils/tracing'; import type { SpanContext, TransactionContext } from '../src/types'; describe('Span', () => { let span: Span; beforeEach(() => { span = new Span({ op: 'test.span' }); }); describe('constructor', () => { it('should create span with generated IDs when not provided', () => { const newSpan = new Span({ op: 'test' }); expect(newSpan.traceId).toMatch(/^[a-f0-9]{32}$/i); expect(newSpan.spanId).toMatch(/^[a-f0-9]{16}$/i); }); it('should use provided traceId and spanId', () => { const newSpan = new Span({ op: 'test', traceId: 'abc123abc123abc123abc123abc123ab', spanId: 'abc123abc123abcd', }); expect(newSpan.traceId).toBe('abc123abc123abc123abc123abc123ab'); expect(newSpan.spanId).toBe('abc123abc123abcd'); }); it('should set parentSpanId from context', () => { const childSpan = new Span({ op: 'child', parentSpanId: 'parent-span-id', }); expect(childSpan.parentSpanId).toBe('parent-span-id'); }); it('should set op and description', () => { const newSpan = new Span({ op: 'http.client', description: 'GET /api/users', }); expect(newSpan.op).toBe('http.client'); expect(newSpan.description).toBe('GET /api/users'); }); it('should initialize with current timestamp', () => { const before = Date.now(); const newSpan = new Span({ op: 'test' }); const after = Date.now(); expect(newSpan.startTimestamp).toBeGreaterThanOrEqual(before); expect(newSpan.startTimestamp).toBeLessThanOrEqual(after); }); it('should use provided startTimestamp', () => { const newSpan = new Span({ op: 'test', startTimestamp: 1234567890000, }); expect(newSpan.startTimestamp).toBe(1234567890000); }); }); describe('setTag', () => { it('should add tag to span', () => { const result = span.setTag('http.method', 'GET'); expect(span.tags).toEqual({ 'http.method': 'GET' }); expect(result).toBe(span); // should return this for chaining }); it('should overwrite existing tag', () => { span.setTag('http.method', 'GET'); span.setTag('http.method', 'POST'); expect(span.tags['http.method']).toBe('POST'); }); it('should support chaining', () => { const result = span.setTag('key1', 'value1').setTag('key2', 'value2'); expect(result).toBe(span); expect(span.tags).toEqual({ key1: 'value1', key2: 'value2' }); }); }); describe('setData', () => { it('should add data to span', () => { span.setData('http.url', 'https://api.example.com/users'); expect(span.data).toEqual({ 'http.url': 'https://api.example.com/users' }); }); it('should support chaining', () => { const result = span.setData('key1', 'value1').setData('key2', 'value2'); expect(result).toBe(span); }); }); describe('setStatus', () => { it('should set status', () => { span.setStatus('ok'); expect(span.status).toBe('ok'); }); it('should support chaining', () => { const result = span.setStatus('ok'); expect(result).toBe(span); }); }); describe('setHttpStatus', () => { it('should set status for 2xx', () => { span.setHttpStatus(200); expect(span.status).toBe('ok'); expect(span.tags['http.status_code']).toBe('200'); }); it('should set status for 4xx', () => { span.setHttpStatus(404); expect(span.status).toBe('not_found'); expect(span.tags['http.status_code']).toBe('404'); }); it('should set status for 5xx', () => { span.setHttpStatus(500); expect(span.status).toBe('internal_error'); expect(span.tags['http.status_code']).toBe('500'); }); it('should support chaining', () => { const result = span.setHttpStatus(200); expect(result).toBe(span); }); }); describe('setStatusFromResponse', () => { it('should handle string status', () => { span.setStatusFromResponse('ok'); expect(span.status).toBe('ok'); }); it('should handle number status', () => { span.setStatusFromResponse(404); expect(span.status).toBe('not_found'); }); it('should handle undefined status', () => { span.setStatusFromResponse(undefined); expect(span.status).toBe('unknown_error'); }); it('should set tag when tagKey provided', () => { span.setStatusFromResponse(200, 'custom.status'); expect(span.tags['custom.status']).toBe('200'); }); }); describe('startChild', () => { it('should create child span', () => { const child = span.startChild({ op: 'http.client' }); expect(child).toBeDefined(); expect(child.op).toBe('http.client'); expect(child.parentSpanId).toBe(span.spanId); expect(child.traceId).toBe(span.traceId); }); it('should set transaction reference', () => { const tx = new Transaction({ name: 'test-tx' }); const child = tx.startChild({ op: 'child' }); expect(child.transaction).toBe(tx); }); it('should not add span to unsampled transaction', () => { const tx = new Transaction({ name: 'test-tx', sampled: false }); const child = tx.startChild({ op: 'child' }); expect(tx.spans.length).toBe(0); }); }); describe('finish', () => { it('should set endTimestamp', () => { span.finish(); expect(span.endTimestamp).toBeDefined(); }); it('should use provided endTimestamp', () => { span.finish(1234567890000); expect(span.endTimestamp).toBe(1234567890000); }); it('should only finish once', () => { span.finish(); const firstEndTime = span.endTimestamp; span.finish(); // should not change expect(span.endTimestamp).toBe(firstEndTime); }); }); describe('toTraceparent', () => { it('should return traceparent without sampled flag when not sampled', () => { const newSpan = new Span({ op: 'test' }); const traceparent = newSpan.toTraceparent(); expect(traceparent).toMatch(/^[a-f0-9]{32}-[a-f0-9]{16}$/); }); it('should return traceparent with sampled=1 when sampled', () => { const tx = new Transaction({ name: 'test', sampled: true }); const traceparent = tx.toTraceparent(); expect(traceparent).toMatch(/^[a-f0-9]{32}-[a-f0-9]{16}-1$/); }); it('should return traceparent with sampled=0 when not sampled', () => { const tx = new Transaction({ name: 'test', sampled: false }); const traceparent = tx.toTraceparent(); expect(traceparent).toMatch(/^[a-f0-9]{32}-[a-f0-9]{16}-0$/); }); }); describe('toJSON', () => { it('should return correct JSON structure', () => { span.setTag('http.method', 'GET'); span.setData('http.url', 'https://api.example.com'); span.setStatus('ok'); span.finish(); const json = span.toJSON(); expect(json.trace_id).toBe(span.traceId); expect(json.span_id).toBe(span.spanId); expect(json.op).toBe('test.span'); expect(json.status).toBe('ok'); expect(json.tags).toEqual({ 'http.method': 'GET' }); expect(json.data).toEqual({ 'http.url': 'https://api.example.com' }); expect(json.start_timestamp).toBe(span.startTimestamp); expect(json.timestamp).toBe(span.endTimestamp); }); it('should omit empty tags and data', () => { const newSpan = new Span({ op: 'test' }); const json = newSpan.toJSON(); expect(json.tags).toBeUndefined(); expect(json.data).toBeUndefined(); }); }); }); describe('Transaction', () => { let transaction: Transaction; beforeEach(() => { transaction = new Transaction({ name: 'test-transaction', op: 'transaction' }); }); describe('constructor', () => { it('should create transaction with name and op', () => { expect(transaction.name).toBe('test-transaction'); expect(transaction.op).toBe('transaction'); }); it('should set transaction reference to itself', () => { expect(transaction.transaction).toBe(transaction); }); it('should initialize empty spans array', () => { expect(transaction.spans).toEqual([]); }); it('should set maxSpans default', () => { expect(transaction.maxSpans).toBe(1000); }); it('should accept custom maxSpans', () => { const tx = new Transaction({ name: 'test' }, undefined, undefined, undefined, undefined, 500); expect(tx.maxSpans).toBe(500); }); }); describe('_addSpan', () => { it('should add span to transaction', () => { const child = transaction.startChild({ op: 'child' }); expect(transaction.spans.length).toBe(1); expect(transaction.spans[0]).toBe(child); }); it('should not exceed maxSpans', () => { const tx = new Transaction({ name: 'test' }, undefined, undefined, undefined, undefined, 2); tx.startChild({ op: 'child1' }); tx.startChild({ op: 'child2' }); tx.startChild({ op: 'child3' }); // should be dropped expect(tx.spans.length).toBe(2); }); }); describe('setBaggage', () => { it('should set baggage field', () => { const result = transaction.setBaggage('custom.key', 'value'); expect(transaction.toBaggage()).toContain('custom.key=value'); expect(result).toBe(transaction); }); }); describe('setUserBaggage', () => { it('should set user_id in baggage', () => { transaction.setUserBaggage('user123'); expect(transaction.toBaggage()).toContain('sentry-user_id=user123'); }); it('should set user_segment in baggage', () => { transaction.setUserBaggage('user123', 'premium'); expect(transaction.toBaggage()).toContain('sentry-user_id=user123'); expect(transaction.toBaggage()).toContain('sentry-user_segment=premium'); }); it('should delete user_id when undefined', () => { transaction.setUserBaggage('user123'); transaction.setUserBaggage(undefined); expect(transaction.toBaggage()).not.toContain('user_id'); }); }); describe('toBaggage', () => { it('should return serialized baggage with trace_id', () => { const baggage = transaction.toBaggage(); expect(baggage).toContain('sentry-trace_id='); }); it('should include sampled when explicitly set', () => { const txWithSample = new Transaction({ name: 'test', sampled: true }); const baggage = txWithSample.toBaggage(); expect(baggage).toContain('sentry-sampled=true'); }); }); describe('toJSON', () => { it('should include spans in JSON', () => { transaction.startChild({ op: 'child1' }); transaction.startChild({ op: 'child2' }); const json = transaction.toJSON(); expect(json.spans).toHaveLength(2); expect(json.transaction).toBe('test-transaction'); expect(json.sampled).toBeUndefined(); // not sampled by default }); }); describe('toEvent', () => { it('should return transaction event', () => { transaction.startChild({ op: 'child' }); const event = transaction.toEvent(); expect(event.type).toBe('transaction'); expect(event.transaction).toBe('test-transaction'); expect(event.spans).toHaveLength(1); expect(event.contexts?.trace).toBeDefined(); expect(event.platform).toBe('javascript'); }); }); }); describe('httpStatusToSpanStatus', () => { it('should map 2xx to ok', () => { expect(httpStatusToSpanStatus(200)).toBe('ok'); expect(httpStatusToSpanStatus(201)).toBe('ok'); expect(httpStatusToSpanStatus(299)).toBe('ok'); }); it('should map 400 to invalid_argument', () => { expect(httpStatusToSpanStatus(400)).toBe('invalid_argument'); }); it('should map 401 to unauthenticated', () => { expect(httpStatusToSpanStatus(401)).toBe('unauthenticated'); }); it('should map 403 to permission_denied', () => { expect(httpStatusToSpanStatus(403)).toBe('permission_denied'); }); it('should map 404 to not_found', () => { expect(httpStatusToSpanStatus(404)).toBe('not_found'); }); it('should map 409 to already_exists', () => { expect(httpStatusToSpanStatus(409)).toBe('already_exists'); }); it('should map 429 to resource_exhausted', () => { expect(httpStatusToSpanStatus(429)).toBe('resource_exhausted'); }); it('should map 4xx to invalid_argument', () => { expect(httpStatusToSpanStatus(499)).toBe('invalid_argument'); }); it('should map 5xx to internal_error', () => { expect(httpStatusToSpanStatus(500)).toBe('internal_error'); expect(httpStatusToSpanStatus(503)).toBe('internal_error'); expect(httpStatusToSpanStatus(599)).toBe('internal_error'); }); }); describe('dbStatusToSpanStatus', () => { it('should map 0 and 1 to ok', () => { expect(dbStatusToSpanStatus(0)).toBe('ok'); expect(dbStatusToSpanStatus(1)).toBe('ok'); expect(dbStatusToSpanStatus('0')).toBe('ok'); expect(dbStatusToSpanStatus('1')).toBe('ok'); }); it('should map 10-19 to invalid_argument', () => { expect(dbStatusToSpanStatus(10)).toBe('invalid_argument'); expect(dbStatusToSpanStatus(15)).toBe('invalid_argument'); }); it('should map 20-29 to not_found', () => { expect(dbStatusToSpanStatus(20)).toBe('not_found'); expect(dbStatusToSpanStatus(25)).toBe('not_found'); }); it('should map 30-39 to already_exists', () => { expect(dbStatusToSpanStatus(30)).toBe('already_exists'); expect(dbStatusToSpanStatus(35)).toBe('already_exists'); }); it('should map 50+ to internal_error', () => { expect(dbStatusToSpanStatus(50)).toBe('internal_error'); expect(dbStatusToSpanStatus(100)).toBe('internal_error'); }); it('should return unknown_error for undefined/null/empty', () => { expect(dbStatusToSpanStatus(undefined)).toBe('unknown_error'); expect(dbStatusToSpanStatus(null)).toBe('unknown_error'); expect(dbStatusToSpanStatus('')).toBe('unknown_error'); }); }); describe('statusToSpanStatus', () => { it('should handle string status ok', () => { expect(statusToSpanStatus('ok', undefined)).toBe('ok'); expect(statusToSpanStatus('success', undefined)).toBe('ok'); }); it('should handle string status cancelled', () => { expect(statusToSpanStatus('cancelled', undefined)).toBe('cancelled'); expect(statusToSpanStatus('canceled', undefined)).toBe('cancelled'); }); it('should handle HTTP status codes', () => { expect(statusToSpanStatus(404, undefined)).toBe('not_found'); expect(statusToSpanStatus(500, undefined)).toBe('internal_error'); }); it('should use db rules for db ops', () => { expect(statusToSpanStatus(0, 'db.query')).toBe('ok'); expect(statusToSpanStatus(10, 'db.query')).toBe('invalid_argument'); }); it('should return unknown_error for unknown strings', () => { expect(statusToSpanStatus('unknown_status', undefined)).toBe('unknown_error'); }); });