UNPKG

autotel

Version:
312 lines (253 loc) 9.63 kB
/** * Tests for semantic convention helpers */ import { describe, it, expect, beforeEach } from 'vitest'; import { traceLLM, traceDB, traceHTTP, traceMessaging, } from './semantic-helpers'; import { createTraceCollector } from './testing'; describe('Semantic Helpers', () => { let collector: ReturnType<typeof createTraceCollector>; beforeEach(() => { collector = createTraceCollector(); }); describe('traceLLM', () => { it('should add Gen AI semantic convention attributes', async () => { const generateText = traceLLM({ model: 'gpt-4', operation: 'chat', provider: 'openai', })((_ctx) => async (prompt: string) => { return `Response to: ${prompt}`; }); await generateText('Hello'); const spans = collector.getSpans(); expect(spans).toHaveLength(1); const span = spans[0]; expect(span.attributes['gen.ai.request.model']).toBe('gpt-4'); expect(span.attributes['gen.ai.operation.name']).toBe('chat'); expect(span.attributes['gen.ai.system']).toBe('openai'); }); it('should use default operation when not specified', async () => { const generateText = traceLLM({ model: 'claude-3', })((_ctx) => async () => 'result'); await generateText(); const spans = collector.getSpans(); expect(spans[0].attributes['gen.ai.operation.name']).toBe('chat'); }); it('should support embedding operation', async () => { const embed = traceLLM({ model: 'text-embedding-3-small', operation: 'embedding', provider: 'openai', })((_ctx) => async (_text: string) => [0.1, 0.2, 0.3]); await embed('test text'); const spans = collector.getSpans(); expect(spans[0].attributes['gen.ai.operation.name']).toBe('embedding'); }); it('should support additional custom attributes', async () => { const generateText = traceLLM({ model: 'gpt-4', attributes: { 'custom.attribute': 'custom-value', 'custom.number': 123, }, })((_ctx) => async () => 'result'); await generateText(); const spans = collector.getSpans(); const span = spans[0]; expect(span.attributes['gen.ai.request.model']).toBe('gpt-4'); expect(span.attributes['custom.attribute']).toBe('custom-value'); expect(span.attributes['custom.number']).toBe(123); }); }); describe('traceDB', () => { it('should add DB semantic convention attributes', async () => { const getUser = traceDB({ system: 'postgresql', operation: 'SELECT', database: 'app_db', collection: 'users', })((_ctx) => async (userId: string) => { return { id: userId, name: 'John' }; }); await getUser('123'); const spans = collector.getSpans(); expect(spans).toHaveLength(1); const span = spans[0]; expect(span.attributes['db.system']).toBe('postgresql'); expect(span.attributes['db.operation']).toBe('SELECT'); expect(span.attributes['db.name']).toBe('app_db'); expect(span.attributes['db.collection.name']).toBe('users'); }); it('should work without optional attributes', async () => { const query = traceDB({ system: 'mongodb', })((_ctx) => async () => ({ results: [] })); await query(); const spans = collector.getSpans(); const span = spans[0]; expect(span.attributes['db.system']).toBe('mongodb'); expect(span.attributes['db.operation']).toBeUndefined(); expect(span.attributes['db.name']).toBeUndefined(); }); it('should support custom attributes', async () => { const query = traceDB({ system: 'redis', operation: 'GET', attributes: { 'db.redis.ttl': 3600, }, })((_ctx) => async (key: string) => `value-${key}`); await query('test-key'); const spans = collector.getSpans(); const span = spans[0]; expect(span.attributes['db.system']).toBe('redis'); expect(span.attributes['db.redis.ttl']).toBe(3600); }); }); describe('traceHTTP', () => { it('should add HTTP semantic convention attributes', async () => { const fetchUser = traceHTTP({ method: 'GET', url: 'https://api.example.com/users/:id', })((_ctx) => async (userId: string) => { return { id: userId }; }); await fetchUser('123'); const spans = collector.getSpans(); expect(spans).toHaveLength(1); const span = spans[0]; expect(span.attributes['http.request.method']).toBe('GET'); expect(span.attributes['url.full']).toBe( 'https://api.example.com/users/:id', ); }); it('should work with only method', async () => { const request = traceHTTP({ method: 'POST', })((_ctx) => async (_data: object) => ({ success: true })); await request({ test: 'data' }); const spans = collector.getSpans(); const span = spans[0]; expect(span.attributes['http.request.method']).toBe('POST'); expect(span.attributes['url.full']).toBeUndefined(); }); it('should work with only URL', async () => { const request = traceHTTP({ url: 'https://api.example.com', })((_ctx) => async () => ({ success: true })); await request(); const spans = collector.getSpans(); const span = spans[0]; expect(span.attributes['url.full']).toBe('https://api.example.com'); expect(span.attributes['http.request.method']).toBeUndefined(); }); it('should support custom attributes', async () => { const request = traceHTTP({ method: 'POST', url: 'https://webhook.example.com', attributes: { 'http.request.retry_count': 3, 'http.request.timeout': 5000, }, })((_ctx) => async () => ({ success: true })); await request(); const spans = collector.getSpans(); const span = spans[0]; expect(span.attributes['http.request.retry_count']).toBe(3); expect(span.attributes['http.request.timeout']).toBe(5000); }); }); describe('traceMessaging', () => { it('should add Messaging semantic convention attributes', async () => { const publishEvent = traceMessaging({ system: 'kafka', operation: 'publish', destination: 'user-events', })((_ctx) => async (_event: object) => { return { messageId: '123' }; }); await publishEvent({ type: 'user.created' }); const spans = collector.getSpans(); expect(spans).toHaveLength(1); const span = spans[0]; expect(span.attributes['messaging.system']).toBe('kafka'); expect(span.attributes['messaging.operation']).toBe('publish'); expect(span.attributes['messaging.destination.name']).toBe('user-events'); }); it('should work with minimal config', async () => { const sendMessage = traceMessaging({ system: 'rabbitmq', })((_ctx) => async () => ({ sent: true })); await sendMessage(); const spans = collector.getSpans(); const span = spans[0]; expect(span.attributes['messaging.system']).toBe('rabbitmq'); expect(span.attributes['messaging.operation']).toBeUndefined(); expect(span.attributes['messaging.destination.name']).toBeUndefined(); }); it('should support receive operation', async () => { const consumeMessage = traceMessaging({ system: 'sqs', operation: 'receive', destination: 'notifications', })((_ctx) => async () => ({ messages: [] })); await consumeMessage(); const spans = collector.getSpans(); const span = spans[0]; expect(span.attributes['messaging.operation']).toBe('receive'); }); it('should support custom attributes', async () => { const publishBatch = traceMessaging({ system: 'aws_sqs', operation: 'publish', destination: 'orders', attributes: { 'messaging.batch.message_count': 10, 'messaging.kafka.partition': 0, }, })((_ctx) => async () => ({ success: true })); await publishBatch(); const spans = collector.getSpans(); const span = spans[0]; expect(span.attributes['messaging.batch.message_count']).toBe(10); expect(span.attributes['messaging.kafka.partition']).toBe(0); }); }); describe('Attribute merging', () => { it('should merge custom attributes with semantic attributes in traceLLM', async () => { const fn = traceLLM({ model: 'gpt-4', attributes: { 'gen.ai.request.temperature': 0.7, 'custom.attr': 'value', }, })((_ctx) => async () => 'result'); await fn(); const spans = collector.getSpans(); const span = spans[0]; expect(span.attributes['gen.ai.request.model']).toBe('gpt-4'); expect(span.attributes['gen.ai.request.temperature']).toBe(0.7); expect(span.attributes['custom.attr']).toBe('value'); }); it('should allow custom attributes to override semantic defaults', async () => { const fn = traceDB({ system: 'postgresql', operation: 'SELECT', attributes: { 'db.operation': 'CUSTOM_OPERATION', // Override default }, })((_ctx) => async () => ({ rows: [] })); await fn(); const spans = collector.getSpans(); const span = spans[0]; // Custom attribute should win expect(span.attributes['db.operation']).toBe('CUSTOM_OPERATION'); }); }); });