51 lines
11 KiB
JavaScript
51 lines
11 KiB
JavaScript
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'));
|
|
});
|
|
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});}
|
|
});
|