2026-10-14 00:37:32 +02:00
import test from 'node:test' ;
import assert from 'node:assert/strict' ;
import { createServer } from 'node:http' ;
import { createRequire } from 'node:module' ;
const { createDiagnostics } = createRequire ( import . meta . url )( '../apps/browser/diagnostics.cjs' );
const delay = ms => new Promise ( resolve => setTimeout ( resolve , ms ));
async function receiver ( t , handler ){ const server = createServer ( handler ). listen ( 0 , '127.0.0.1' ); await new Promise ( resolve => server . once ( 'listening' , resolve )); t . after (()=> new Promise ( resolve =>{ server . closeAllConnections (); server . close ( resolve );})); return `http://127.0.0.1: ${ server . address (). port } ` ;}
const config = endpoint =>({ enabled : true , mode : 'debug' , endpoint , debugSampleRate : 1 , sampleRate : 1 , flushIntervalMs : 10000 , exportTimeoutMs : 250 });
async function produce ( d ){ await d . span ( 'browser.request' ,{}, async ()=>{}); await d . flush ();}
test ( 'real timeout, refusal and HTTP503 keep fixed reasons without error text' ,{ timeout : 15000 }, async t =>{
const secret = 'SYNTHETIC_EXPORT_ERROR_MUST_NOT_RETAIN' ;
for ( const kind of [ 'timeout' , 'refused' , '503' ]){ let endpoint ; if ( kind === 'refused' ){ const server = createServer (). listen ( 0 , '127.0.0.1' ); await new Promise ( resolve => server . once ( 'listening' , resolve )); endpoint = `http://127.0.0.1: ${ server . address (). port } ` ; await new Promise ( resolve => server . close ( resolve ));} else endpoint = await receiver ( t ,( req , res )=>{ req . resume (); if ( kind === '503' ){ res . writeHead ( 503 , secret ,{ 'retry-after' : '100' }); res . end ( secret );}}); const d = createDiagnostics (); t . after (()=> d . shutdown ()); d . configure ( config ( endpoint )); await produce ( d ); for ( let i = 0 ; i < 100 && d . status (). lifetimeFailedExports < 2 ; i ++ ) await delay ( 10 ); const status = d . status (); assert . ok ( status . lifetimeFailedExports >= 1 , kind ); const reason = kind === 'timeout' ? 'timeout' : kind === 'refused' ? 'connection-refused' : 'http-server' ; assert . ok ( status . failureReasons [ reason ] >= 1 , kind ); assert . ok ( status . failureFacts . every ( f => f . reason === reason )); assert . ok ( ! JSON . stringify ( status ). includes ( secret )); assert . ok ( ! JSON . stringify ( status ). includes ( endpoint )); assert . ok ( status . failureFacts . every ( f =>[ 'traces' , 'logs' ]. includes ( f . signal ) && f . activeAtCompletion &&! f . retirementOverlapped )); const before = status . lifetimeFailedExports ; d . configure ({ enabled : false }); assert . equal ( d . status (). lifetimeFailedExports , before ); assert . equal ( d . status (). failedExports , 0 );}
});
test ( 'inflight retirement cancellation remains a failure with bounded facts and generation context' ,{ timeout : 15000 }, async t =>{
let received = 0 ; const endpoint = await receiver ( t ,( req , _res )=>{ req . resume (); received ++ ;}); const d = createDiagnostics (); t . after (()=> d . shutdown ()); d . configure ({... config ( endpoint ), exportTimeoutMs : 5000 },{ policyVersion : 1 }); await d . span ( 'browser.request' ,{}, async ()=>{}); const flush = d . flush (); for ( let i = 0 ; i < 100 && received < 2 ; i ++ ) await delay ( 10 ); assert . equal ( received , 2 ); d . configure ( config ( endpoint ),{ policyVersion : 2 }); await flush ; await delay ( 50 ); const status = d . status (); assert . equal ( status . failedExports , 0 ); assert . equal ( status . lifetimeFailedExports , 2 ); assert . equal ( status . failureReasons . cancelled , 2 ); assert . ok ( status . failureFacts . every ( f => f . generation === 1 &&! f . activeAtCompletion && f . retirementOverlapped && f . cancelSignalled )); assert . deepEqual ( new Set ( status . failureFacts . map ( f => f . signal )), new Set ([ 'traces' , 'logs' ])); const copy = d . status (); copy . failureFacts [ 0 ]. reason = 'forged' ; copy . failureReasons . cancelled = 0 ; assert . equal ( d . status (). failureReasons . cancelled , 2 ); assert . equal ( d . status (). failureFacts [ 0 ]. reason , 'cancelled' ); d . configure ({ enabled : false }); assert . equal ( d . status (). lifetimeFailedExports , 2 ); assert . equal ( received , 2 , 'Retirement repeated a request' );
});
test ( 'failure history is bounded while lifetime reason totals retain every failed export' ,{ timeout : 15000 }, async t =>{
const endpoint = await receiver ( t ,( req , res )=>{ req . resume (); res . writeHead ( 400 , 'Synthetic error body' ); res . end ( 'PRIVATE_BODY_NOT_RETAINED' );}); const d = createDiagnostics (); t . after (()=> d . shutdown ()); d . configure ( config ( endpoint )); for ( let i = 0 ; i < 18 ; i ++ ) await produce ( d ); for ( let i = 0 ; i < 100 && d . status (). lifetimeFailedExports < 2 ; i ++ ) await delay ( 10 ); const status = d . status (); assert . equal ( status . lifetimeFailedExports , 36 ); assert . equal ( status . failureReasons [ 'http-client' ], 36 ); assert . equal ( status . failureFacts . length , 32 ); assert . ok ( ! JSON . stringify ( status ). includes ( 'PRIVATE_BODY_NOT_RETAINED' ));
});
2026-10-14 01:18:23 +02:00
test ( 'concurrent flush shares held native exports and cancellation attribution' ,{ timeout : 15000 }, async t =>{
let received = 0 ; const endpoint = await receiver ( t , req =>{ req . resume (); received ++ ;}), d = createDiagnostics (); t . after (()=> d . shutdown ()); d . configure ({... config ( endpoint ), exportTimeoutMs : 5000 },{ policyVersion : 1 }); await d . span ( 'browser.request' ,{}, async ()=>{}); const first = d . flush (); for ( let i = 0 ; i < 100 && received < 2 ; i ++ ) await delay ( 10 ); assert . equal ( received , 2 ); await d . span ( 'browser.request' ,{}, async ()=>{}); const second = d . flush (); assert . equal ( second , first ); await delay ( 50 ); assert . equal ( d . status (). failureReasons [ 'concurrency-limit' ], undefined ); d . configure ({ enabled : false }); await Promise . all ([ first , second ]); const status = d . status (); assert . equal ( received , 2 ); assert . equal ( status . failureReasons . cancelled , 2 ); assert . equal ( status . lifetimeFailedExports , status . failureReasons . cancelled ); assert . ok ( status . failureFacts . filter ( f => f . reason === 'cancelled' ). every ( f => f . cancelSignalled && f . retirementOverlapped && f . generation === 1 ));
});
test ( 'scheduled batches queue behind held HTTP exports, expire explicitly and cancel without release' ,{ timeout : 15000 }, async t =>{
let received = 0 ; const endpoint = await receiver ( t , req =>{ req . resume (); received ++ ;}), d = createDiagnostics (); t . after (()=> d . shutdown ()); d . configure ({... config ( endpoint ), maxQueueSize : 16 , maxRecordAgeMs : 1000 , flushIntervalMs : 250 , exportTimeoutMs : 5000 });
await d . span ( 'browser.request' ,{}, async ()=>{}); const first = d . flush (); for ( let i = 0 ; i < 100 && received < 2 ; i ++ ) await delay ( 10 ); assert . equal ( received , 2 );
// Batch size triggers immediate processor export while the first real requests remain held.
for ( let i = 0 ; i < 48 ; i ++ ) await d . span ( 'browser.request' ,{}, async ()=>{});
for ( let i = 0 ; i < 200 &&! d . status (). failureReasons [ 'queue-expired' ]; i ++ ) await delay ( 10 );
const waiting = d . status (); assert . equal ( received , 2 , 'A queued batch overlapped a held native HTTP request' ); assert . ok ( waiting . failureReasons [ 'queue-expired' ] >= 1 ); assert . equal ( waiting . failureReasons [ 'concurrency-limit' ], undefined ); assert . ok ( waiting . failureFacts . filter ( f => f . reason === 'queue-expired' ). every ( f => f . queueAgeMs > 0 && f . durationMs >= f . queueAgeMs ));
// Fresh pending batch must fail immediately on admin disable, without a network attempt.
for ( let i = 0 ; i < 16 ; i ++ ) await d . span ( 'browser.request' ,{}, async ()=>{}); await delay ( 10 ); const before = d . status (). lifetimeFailedExports ; d . configure ({ enabled : false }); await first ; const final = d . status (); assert . equal ( received , 2 ); assert . ok ( final . lifetimeFailedExports >= before + 2 ); assert . ok ( final . failureReasons . cancelled >= 2 ); assert . equal ( final . failedExports , 0 );
});
test ( 'bounded export FIFO reports overflow and cancels queued callbacks using real held HTTP' ,{ timeout : 15000 }, async t =>{
const { readFileSync } = await import ( 'node:fs' ),{ runInNewContext } = await import ( 'node:vm' ); const nativeRequire = createRequire ( import . meta . url ), captured = []; const sdk = nativeRequire ( '@opentelemetry/sdk-trace' );
class CapturingProcessor extends sdk . BatchSpanProcessor { constructor ( options ){ const original = options . exporter . export ; options . exporter . export = function ( records , callback ){ captured . push ({ exporter : options . exporter , records }); return original . call ( this , records , callback );}; super ( options );}}
const sandbox = { module : { exports : {}}, require : name => name === '@opentelemetry/sdk-trace' ? {... sdk , BatchSpanProcessor : CapturingProcessor } : name . startsWith ( '../../' ) ? nativeRequire ( '../apps/browser/' + name ) : nativeRequire ( name ), setTimeout , clearTimeout , setImmediate , URL , console }; runInNewContext ( readFileSync ( new URL ( '../apps/browser/diagnostics.cjs' , import . meta . url ), 'utf8' ), sandbox );
let received = 0 ; const endpoint = await receiver ( t , req =>{ req . resume (); received ++ ;}), d = sandbox . module . exports . createDiagnostics (); t . after (()=> d . shutdown ()); d . configure ({... config ( endpoint ), maxQueueSize : 16 , exportTimeoutMs : 5000 }); await d . span ( 'browser.request' ,{}, async ()=>{}); const pending = d . flush (); for ( let i = 0 ; i < 100 && received < 2 ; i ++ ) await delay ( 10 ); assert . equal ( received , 2 );
const { exporter , records } = captured [ 0 ], results = []; exporter . export ( Array . from ({ length : 16 },()=> records [ 0 ]), r => results . push ( r )); exporter . export ([ records [ 0 ]], r => results . push ( r )); assert . equal ( d . status (). failureReasons [ 'queue-overflow' ], 1 ); assert . equal ( results . length , 1 ); assert . equal ( results [ 0 ]. code , 1 ); assert . equal ( received , 2 ); d . configure ({ enabled : false }); await pending ; assert . equal ( results . length , 2 ); assert . ok ( results . every ( r => r . code === 1 )); assert . equal ( d . status (). failureReasons . cancelled , 3 ); assert . equal ( d . status (). lifetimeFailedExports , 4 ); assert . equal ( received , 2 );
});
test ( 'held HTTP success drains scheduled FIFO with one request per signal' ,{ timeout : 15000 }, async t =>{
let received = 0 , released = false ; const held = [], active = new Map (), maximum = new Map (); const endpoint = await receiver ( t ,( req , res )=>{ const signal = req . url ; received ++ ; active . set ( signal ,( active . get ( signal ) || 0 ) + 1 ); maximum . set ( signal , Math . max ( maximum . get ( signal ) || 0 , active . get ( signal ))); res . once ( 'finish' ,()=> active . set ( signal , active . get ( signal ) - 1 )); req . resume (); if ( released ){ res . writeHead ( 200 ,{ 'content-type' : 'application/json' }); res . end ( '{}' );} else held . push ( res );});
const d = createDiagnostics (); t . after (()=> d . shutdown ()); d . configure ({... config ( endpoint ), maxQueueSize : 16 , exportTimeoutMs : 5000 }); await d . span ( 'browser.request' ,{}, async ()=>{}); const first = d . flush (); for ( let i = 0 ; i < 100 && received < 2 ; i ++ ) await delay ( 10 ); assert . equal ( received , 2 ); for ( let i = 0 ; i < 16 ; i ++ ) await d . span ( 'browser.request' ,{}, async ()=>{}); await delay ( 30 ); assert . equal ( received , 2 ); released = true ; for ( const res of held ){ res . writeHead ( 200 ,{ 'content-type' : 'application/json' }); res . end ( '{}' );} await first ; await d . flush (); for ( let i = 0 ; i < 100 && received < 4 ; i ++ ) await delay ( 10 ); assert . equal ( received , 4 , JSON . stringify ({ status : d . status (), signals : [... maximum . keys ()]})); assert . equal ( d . status (). lifetimeFailedExports , 0 ); assert . deepEqual ([... maximum . values ()],[ 1 , 1 ]);
});
test ( 'identity and exporter settings changes still immediately cancel real held requests' ,{ timeout : 15000 }, async t =>{
for ( const kind of [ 'identity' , 'settings' ]){ let received = 0 ; const endpoint = await receiver ( t , req =>{ req . resume (); received ++ ;}), d = createDiagnostics (); t . after (()=> d . shutdown ()); const settings = {... config ( endpoint ), exportTimeoutMs : 5000 }, deviceId = '00000000-0000-4000-8000-000000000001' ; d . configure ( settings ,{ deviceId , policyVersion : 1 }); await d . span ( 'browser.request' ,{}, async ()=>{}); const pending = d . flush (); for ( let i = 0 ; i < 100 && received < 2 ; i ++ ) await delay ( 10 ); assert . equal ( received , 2 ); const next = kind === 'identity' ? settings : {... settings , maxQueueSize : 16 }, context = { deviceId : kind === 'identity' ? '00000000-0000-4000-8000-000000000002' : deviceId , policyVersion : 2 }; assert . equal ( d . canContinue ( next , context ), false ); d . configure ( next , context ); await pending ; assert . equal ( received , 2 ); assert . equal ( d . status (). lifetimeFailedExports , 2 ); assert . equal ( d . status (). failedExports , 0 ); assert . equal ( d . status (). failureReasons . cancelled , 2 ); assert . ok ( d . status (). failureFacts . every ( f => f . cancelSignalled &&! f . activeAtCompletion && f . retirementOverlapped )); d . configure ({ enabled : false });}
2026-10-14 00:37:32 +02:00
});