459 lines
15 KiB
TypeScript
459 lines
15 KiB
TypeScript
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');
|
|
});
|
|
});
|