The page reports its errors and playback timings, and its requests carry a trace (#190)
Nothing the page did in the browser reached the logs: a DeadAir playback failure on the phone was a toast and nothing more. POST /api/client takes page errors, unhandled rejections, hls.js and media errors, the time from play to sound and the length of a stall, and logs them as ev lines under ipx::client, inside the request's span. Only those kinds are taken, the text cut to 500 characters, and the page sends each at most once a minute. The page's API requests send traceparent, a new trace id for each tap or key, and the server's request span takes it as its parent, so one action's requests are one trace in Tempo, the report included. The stub DOM the smoke and native-bridge tests run in gains crypto and performance, which every browser the page runs in has. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,7 +27,8 @@ function setSrc(url: string): Promise<void>|null{
|
||||
});
|
||||
return ready.then(()=>{
|
||||
hls=new w.Hls();
|
||||
hls.on(w.Hls.Events.ERROR,(_,d)=>{ if(d.fatal) toast('Playback failed: '+d.details,true); });
|
||||
hls.on(w.Hls.Events.ERROR,(_,d)=>{ if(d.fatal){ toast('Playback failed: '+d.details,true);
|
||||
report('player_error',{msg:`hls.js: ${d.type} ${d.details}`,feed:player.feed,enclosure:player.enc}); } });
|
||||
hls.loadSource(url); hls.attachMedia(audio);
|
||||
});
|
||||
}
|
||||
@@ -137,6 +138,20 @@ function syncPlayButtons(){
|
||||
}
|
||||
for(const ev of ['play','pause','timeupdate']) audio.addEventListener(ev,syncListening);
|
||||
window.addEventListener('beforeunload',savePos);
|
||||
|
||||
/// For the server's log (#190): how long play takes to start, how long it stalls for, and what
|
||||
/// the browser says when it cannot play a file at all.
|
||||
let asked=0, waited=0;
|
||||
const playing=()=>({feed:player.feed,enclosure:player.enc,deadair:!!player.src?.endsWith('.m3u8')});
|
||||
audio.addEventListener('play',()=>{ asked=performance.now(); });
|
||||
audio.addEventListener('waiting',()=>{ if(!asked) waited=performance.now(); });
|
||||
audio.addEventListener('playing',()=>{
|
||||
const now=performance.now();
|
||||
if(asked) report('play_start',{ms:Math.round(now-asked),...playing()});
|
||||
else if(waited && now-waited>=500) report('stall',{ms:Math.round(now-waited),...playing()});
|
||||
asked=0; waited=0;
|
||||
});
|
||||
audio.addEventListener('error',()=>{ if(audio.error) report('player_error',{msg:`media error ${audio.error.code}: ${audio.error.message}`,...playing()}); });
|
||||
$('#pplay').onclick=()=>audio.paused?audio.play():audio.pause();
|
||||
$('#pback').onclick=()=>audio.currentTime-=15;
|
||||
$('#pfwd').onclick=()=>audio.currentTime+=30;
|
||||
|
||||
@@ -70,8 +70,30 @@ function spanText(secs){
|
||||
const h=Math.floor(secs/3600), m=Math.floor(secs%3600/60);
|
||||
return h?`${h} h ${m} min`:m?`${m} min`:`${secs} s`;
|
||||
}
|
||||
/// One trace for each thing you do (#190): the requests a tap or a key sets off share a trace id,
|
||||
/// sent as traceparent, so Tempo shows them together. getRandomValues, not randomUUID, which a
|
||||
/// LAN address over plain HTTP does not have.
|
||||
let traceId='';
|
||||
const hex=(n: number)=>Array.from(crypto.getRandomValues(new Uint8Array(n)),b=>b.toString(16).padStart(2,'0')).join('');
|
||||
for(const t of ['pointerdown','keydown']) window.addEventListener(t,()=>{ traceId=hex(16); },{capture:true,passive:true});
|
||||
function traceparent(){ return `00-${traceId||=hex(16)}-${hex(8)}-01`; }
|
||||
|
||||
/// Something that went wrong, or how long something took, in the browser, for the server's log
|
||||
/// (#190). The same thing at most once a minute: a failure in a loop would otherwise send a line
|
||||
/// a frame.
|
||||
const reported=new Map<string,number>();
|
||||
function report(ev: string, o: Record<string,any>={}){
|
||||
const key=ev+(o.msg||''), now=Date.now();
|
||||
if(o.msg==='signed out' || now-(reported.get(key)||0)<60_000) return;
|
||||
reported.set(key,now);
|
||||
fetch('/api/client',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json',traceparent:traceparent()},
|
||||
body:JSON.stringify({ev,...o})}).catch(()=>{});
|
||||
}
|
||||
window.addEventListener('error',e=>report('page_error',{msg:String(e.message||e.error||'error'),src:e.filename,line:e.lineno}));
|
||||
window.addEventListener('unhandledrejection',e=>report('rejection',{msg:String(e.reason?.message||e.reason)}));
|
||||
|
||||
async function api(url: string, opts?: RequestInit): Promise<any>{
|
||||
const r = await fetch(url,{headers:{'Content-Type':'application/json'},...opts});
|
||||
const r = await fetch(url,{headers:{'Content-Type':'application/json',traceparent:traceparent()},...opts});
|
||||
if(r.status===401){ location.href='/login'; throw new Error('signed out'); }
|
||||
if(!r.ok) throw new Error(await r.text().catch(()=>'')||String(r.status));
|
||||
return r.status===204?null:r.json().catch(()=>null);
|
||||
|
||||
Reference in New Issue
Block a user