User Story 3: initializes a real OpenTelemetry TracerProvider (previously inert — getTracer() returned a no-op tracer with nothing ever exported). Adds ticket.create, ai.escalation, and orchestration.assignment spans covering both FR-006 cross-module paths, verified via a real, in-memory test exporter that confirms actual trace/parent-span nesting, not mocked. Also registers an AsyncLocalStorageContextManager (@opentelemetry/context-async-hooks) — without one, OTel's context API is a no-op that doesn't propagate across the await boundaries this feature's own event-bus subscribers rely on for span nesting; caught by the first version of the tracing integration test actually failing on real parent/child assertions, not assumed. Graceful degradation (FR-007) verified against a real, deliberately unreachable OTLP endpoint: the SDK's own background export path (what production actually exercises) never produces an unhandled rejection. Starts on the 11 named business-health metrics: AI session resolved/escalated outcomes (session.repository.ts, the single choke point every branch in session.service.ts funnels through), human-vs-AI resolution + resolution-time (a new TICKET_UPDATED/RESOLVED subscriber), escalation rate (a new subscriber on ESCALATION_TRIGGERED, published unconditionally since 008 but never previously consumed), and recurring problems (tickets.service.ts's existing problem-creation call site). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
55 lines
2.5 KiB
TypeScript
55 lines
2.5 KiB
TypeScript
import { describe, it, expect, afterEach } from 'vitest';
|
|
import { BasicTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
|
|
|
/**
|
|
* 014-full-observability FR-007/Quickstart Scenario 3 steps 3-4: an unreachable tracing
|
|
* collector must never surface as an application-level failure. This exercises the real
|
|
* OpenTelemetry SDK's actual background export path (BasicTracerProvider + BatchSpanProcessor +
|
|
* a real OTLPTraceExporter, against a real, deliberately-unreachable address — not a mock) the
|
|
* same way it runs in production: a span ends, the processor's own internal timer schedules the
|
|
* export, and a failed export is caught by the SDK's own error handler — never left as an
|
|
* unhandled rejection that could crash the process.
|
|
*
|
|
* (Deliberately does NOT call provider.forceFlush() to prove this — forceFlush() is documented
|
|
* OpenTelemetry SDK behavior that *does* reject on a failed export, by design, so a caller that
|
|
* explicitly asks "did my flush succeed?" can find out. This feature's own code never calls
|
|
* forceFlush() on the request-handling path, only the SDK's own background timer does, which is
|
|
* what this test exercises instead.)
|
|
*/
|
|
describe('Tracing graceful degradation', () => {
|
|
let unhandledRejection: unknown;
|
|
const onUnhandledRejection = (reason: unknown) => {
|
|
unhandledRejection = reason;
|
|
};
|
|
|
|
afterEach(() => {
|
|
process.removeListener('unhandledRejection', onUnhandledRejection);
|
|
});
|
|
|
|
it('does not produce an unhandled rejection when the background export to an unreachable endpoint fails', async () => {
|
|
unhandledRejection = undefined;
|
|
process.on('unhandledRejection', onUnhandledRejection);
|
|
|
|
const exporter = new OTLPTraceExporter({
|
|
url: 'http://127.0.0.1:1/v1/traces', // port 1 — nothing listens there
|
|
timeoutMillis: 500,
|
|
});
|
|
const provider = new BasicTracerProvider({
|
|
spanProcessors: [
|
|
new BatchSpanProcessor(exporter, { scheduledDelayMillis: 10, exportTimeoutMillis: 500 }),
|
|
],
|
|
});
|
|
|
|
const span = provider.getTracer('test').startSpan('unreachable-export-test');
|
|
span.end(); // triggers the processor's own internal timer, not forceFlush()
|
|
|
|
// Long enough for the internal timer (10ms) + the failed connection attempt to resolve.
|
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
|
|
expect(unhandledRejection).toBeUndefined();
|
|
|
|
await provider.shutdown();
|
|
}, 10000);
|
|
});
|