import { describe, expect, test, beforeEach, afterEach } from 'bun:test'; import { NOOP_HOOK, withTracer, type OtelTracerLike } from '../src/index.ts'; interface RecordedSpan { name: string; attrs: Record; ended: boolean; } function makeFakeTracer(): { tracer: OtelTracerLike; spans: RecordedSpan[] } { const spans: RecordedSpan[] = []; const tracer: OtelTracerLike = { startSpan(name, options) { const rec: RecordedSpan = { name, attrs: { ...(options?.attributes ?? {}) }, ended: false, }; spans.push(rec); return { setAttribute(k, v) { rec.attrs[k] = v; return undefined; }, end() { rec.ended = true; return undefined; }, }; }, }; return { tracer, spans }; } describe('withTracer (off-by-default)', () => { beforeEach(() => { delete (globalThis as unknown as { process?: { env?: Record } }).process?.env?.SHADE_OTEL_ENABLED; }); test('returns NOOP_HOOK when tracer is undefined', () => { const hook = withTracer(undefined); expect(hook).toBe(NOOP_HOOK); }); test('returns NOOP_HOOK when env-var is not set (default)', () => { const { tracer, spans } = makeFakeTracer(); const hook = withTracer(tracer); const span = hook.startSpan('shade.test'); span.setAttribute('foo', 'bar'); span.end(); expect(spans.length).toBe(0); // never reached the OTel tracer }); test('force=true bypasses the env gate', () => { const { tracer, spans } = makeFakeTracer(); const hook = withTracer(tracer, { force: true }); hook.startSpan('shade.test', { foo: 'bar' }).end(); expect(spans.length).toBe(1); expect(spans[0]?.name).toBe('shade.test'); expect(spans[0]?.attrs.foo).toBe('bar'); expect(spans[0]?.ended).toBe(true); }); }); describe('withTracer (env-enabled)', () => { beforeEach(() => { process.env.SHADE_OTEL_ENABLED = '1'; }); afterEach(() => { delete process.env.SHADE_OTEL_ENABLED; }); test('emits spans through the tracer when env is set', () => { const { tracer, spans } = makeFakeTracer(); const hook = withTracer(tracer); const span = hook.startSpan('shade.upload', { 'shade.bytes.bin': '1–10MB' }); span.setAttribute('shade.result', 'ok'); span.setStatus('ok'); span.end(); expect(spans.length).toBe(1); expect(spans[0]?.name).toBe('shade.upload'); expect(spans[0]?.attrs['shade.bytes.bin']).toBe('1–10MB'); expect(spans[0]?.attrs['shade.result']).toBe('ok'); expect(spans[0]?.ended).toBe(true); }); test('respects per-span sampling', () => { const { tracer, spans } = makeFakeTracer(); let n = 0; const random = () => { // Alternates: 0.1 (sampled in), 0.9 (sampled out) const v = n % 2 === 0 ? 0.1 : 0.9; n++; return v; }; const hook = withTracer(tracer, { sample: 0.5, random }); for (let i = 0; i < 10; i++) hook.startSpan(`s${i}`).end(); // Half (5 of 10) should reach the OTel tracer. expect(spans.length).toBe(5); }); test('sample=0 means no spans even when env is on', () => { const { tracer, spans } = makeFakeTracer(); const hook = withTracer(tracer, { sample: 0 }); hook.startSpan('shade.test').end(); expect(spans.length).toBe(0); }); test('end() is idempotent', () => { const { tracer, spans } = makeFakeTracer(); const hook = withTracer(tracer); const span = hook.startSpan('shade.test'); span.end(); span.end(); expect(spans.length).toBe(1); expect(spans[0]?.ended).toBe(true); }); test('attribute mutations after end() are no-op', () => { const { tracer, spans } = makeFakeTracer(); const hook = withTracer(tracer); const span = hook.startSpan('shade.test', { a: 1 }); span.end(); span.setAttribute('after_end', 'oops'); expect(spans[0]?.attrs.after_end).toBeUndefined(); }); });