UNPKG

autotel-cloudflare

Version:

The #1 OpenTelemetry package for Cloudflare Workers - complete bindings coverage, native CF OTel integration, advanced sampling

225 lines (185 loc) 8.32 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { instrumentQueueProducer } from './queue-producer'; import { trace, SpanStatusCode, SpanKind } from '@opentelemetry/api'; describe('Queue Producer Binding Instrumentation', () => { let mockTracer: any; let mockSpan: any; let getTracerSpy: any; beforeEach(() => { mockSpan = { spanContext: () => ({ traceId: 'test-trace-id', spanId: 'test-span-id', traceFlags: 1, }), setAttribute: vi.fn(), setAttributes: vi.fn(), setStatus: vi.fn(), recordException: vi.fn(), end: vi.fn(), isRecording: () => true, updateName: vi.fn(), addEvent: vi.fn(), }; mockTracer = { startActiveSpan: vi.fn((name, options, fn) => { return fn(mockSpan); }), }; getTracerSpy = vi.spyOn(trace, 'getTracer').mockReturnValue(mockTracer as any); }); afterEach(() => { getTracerSpy.mockRestore(); }); function createMockQueue(overrides: Partial<Queue> = {}): Queue { return { send: vi.fn(async () => ({ messageId: 'msg-123', outcome: 'ok' })), sendBatch: vi.fn(async () => ({})), ...overrides, } as unknown as Queue; } describe('send()', () => { it('should create span with PRODUCER kind and correct attributes', async () => { const mockQueue = createMockQueue(); const instrumented = instrumentQueueProducer(mockQueue, 'my-queue'); await instrumented.send({ data: 'test-payload' }); expect(mockTracer.startActiveSpan).toHaveBeenCalledTimes(1); const [spanName, options] = mockTracer.startActiveSpan.mock.calls[0]; expect(spanName).toBe('Queue my-queue: send'); expect(options.kind).toBe(SpanKind.PRODUCER); expect(options.attributes['messaging.system']).toBe('cloudflare-queues'); expect(options.attributes['messaging.operation.type']).toBe('publish'); expect(options.attributes['messaging.operation']).toBe('send'); expect(options.attributes['messaging.destination.name']).toBe('my-queue'); }); it('should record messageId from result when available', async () => { const mockQueue = createMockQueue({ send: vi.fn(async () => ({ messageId: 'msg-456', outcome: 'ok' })) as any, }); const instrumented = instrumentQueueProducer(mockQueue, 'my-queue'); await instrumented.send({ data: 'test-payload' }); expect(mockSpan.setAttribute).toHaveBeenCalledWith('messaging.message.id', 'msg-456'); expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.OK }); expect(mockSpan.end).toHaveBeenCalled(); }); it('should not set messageId attribute when result has no messageId', async () => { const mockQueue = createMockQueue({ send: vi.fn(async () => ({})) as any, }); const instrumented = instrumentQueueProducer(mockQueue, 'my-queue'); await instrumented.send({ data: 'test-payload' }); // setAttr uses the common helper which skips undefined/null values const messageIdCalls = mockSpan.setAttribute.mock.calls.filter( (call: any) => call[0] === 'messaging.message.id' ); expect(messageIdCalls.length).toBe(0); }); it('should handle errors in send()', async () => { const sendError = new Error('Queue full'); const mockQueue = createMockQueue({ send: vi.fn(async () => { throw sendError; }) as any, }); const instrumented = instrumentQueueProducer(mockQueue, 'my-queue'); await expect(instrumented.send({ data: 'test-payload' })).rejects.toThrow('Queue full'); expect(mockSpan.recordException).toHaveBeenCalledWith(sendError); expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.ERROR, message: 'Queue full', }); expect(mockSpan.end).toHaveBeenCalled(); }); it('should use default queue name when none provided', async () => { const mockQueue = createMockQueue(); const instrumented = instrumentQueueProducer(mockQueue); await instrumented.send({ data: 'test-payload' }); const [spanName, options] = mockTracer.startActiveSpan.mock.calls[0]; expect(spanName).toBe('Queue queue: send'); expect(options.attributes['messaging.destination.name']).toBe('queue'); }); }); describe('sendBatch()', () => { it('should create span with batch_message_count', async () => { const mockQueue = createMockQueue(); const instrumented = instrumentQueueProducer(mockQueue, 'my-queue'); const messages = [ { body: { data: 'msg-1' } }, { body: { data: 'msg-2' } }, { body: { data: 'msg-3' } }, ]; await instrumented.sendBatch(messages); expect(mockTracer.startActiveSpan).toHaveBeenCalledTimes(1); const [spanName, options] = mockTracer.startActiveSpan.mock.calls[0]; expect(spanName).toBe('Queue my-queue: sendBatch'); expect(options.kind).toBe(SpanKind.PRODUCER); expect(options.attributes['messaging.system']).toBe('cloudflare-queues'); expect(options.attributes['messaging.operation.type']).toBe('publish'); expect(options.attributes['messaging.operation']).toBe('sendBatch'); expect(options.attributes['messaging.destination.name']).toBe('my-queue'); expect(options.attributes['messaging.batch.message_count']).toBe(3); }); it('should set OK status and end span on success', async () => { const mockQueue = createMockQueue(); const instrumented = instrumentQueueProducer(mockQueue, 'my-queue'); await instrumented.sendBatch([{ body: { data: 'msg-1' } }]); expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.OK }); expect(mockSpan.end).toHaveBeenCalled(); }); it('should handle errors in sendBatch()', async () => { const batchError = new Error('Batch limit exceeded'); const mockQueue = createMockQueue({ sendBatch: vi.fn(async () => { throw batchError; }) as any, }); const instrumented = instrumentQueueProducer(mockQueue, 'my-queue'); const messages = [{ body: { data: 'msg-1' } }, { body: { data: 'msg-2' } }]; await expect(instrumented.sendBatch(messages)).rejects.toThrow('Batch limit exceeded'); expect(mockSpan.recordException).toHaveBeenCalledWith(batchError); expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.ERROR, message: 'Batch limit exceeded', }); expect(mockSpan.end).toHaveBeenCalled(); }); }); describe('this-binding', () => { it('should invoke send() with original object as this, not the proxy', async () => { let receivedThis: any; const mockQueue = { send: vi.fn(async function(this: any) { // eslint-disable-next-line unicorn/no-this-assignment, @typescript-eslint/no-this-alias receivedThis = this; return { messageId: 'msg-123' }; }), sendBatch: vi.fn(async () => ({})), } as unknown as Queue; const instrumented = instrumentQueueProducer(mockQueue, 'test'); await instrumented.send({ data: 'test' }); expect(receivedThis).toBe(mockQueue); }); it('should invoke sendBatch() with original object as this, not the proxy', async () => { let receivedThis: any; const mockQueue = { send: vi.fn(async () => ({ messageId: 'msg-123' })), sendBatch: vi.fn(async function(this: any) { // eslint-disable-next-line unicorn/no-this-assignment, @typescript-eslint/no-this-alias receivedThis = this; return {}; }), } as unknown as Queue; const instrumented = instrumentQueueProducer(mockQueue, 'test'); await instrumented.sendBatch([{ body: { data: 'test' } }]); expect(receivedThis).toBe(mockQueue); }); }); describe('non-instrumented methods', () => { it('should pass through non-instrumented properties', () => { const mockQueue = createMockQueue(); (mockQueue as any).customProp = 'test-value'; const instrumented = instrumentQueueProducer(mockQueue, 'my-queue'); expect((instrumented as any).customProp).toBe('test-value'); }); }); });