A tap's span ends with its own requests' answers (#379)
traceparent keeps one trace for any request within a minute of the last, and the tap span ran to the last answer in that trace, so a live update's refresh 13 s later made a tap answered in 0.9 s read as 15 s (trace a895551d20626fb6dc071d336262b772). Now a request counts towards the tap only when one of the tap's own is in flight or it begins within two seconds of the tap or its last answer; it still joins the trace. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -243,6 +243,19 @@ test('a tap is its trace\'s root span, reported with its ids once it has an answ
|
||||
assert.deepEqual([body.ev, body.ms, body.msg, body.span.length, body.trace.length], ['tap', 250, 'keydown', 16, 32]);
|
||||
});
|
||||
|
||||
test('a request begun long after a tap\'s last answer does not stretch the tap (#379)', async () => {
|
||||
run(`globalThis.sent = []; fetch = (url, o) => { sent.push([url, o]); return Promise.resolve({ ok: true, status: 204 }); }`);
|
||||
run(`newTrace('pointerdown'); traceAt = Date.now()`);
|
||||
await run(`api('/api/a')`);
|
||||
const answered = run('answeredAt');
|
||||
assert.ok(answered >= run('rootAt'), 'its own request ends it');
|
||||
run('answeredAt -= 5000; rootAt -= 5000');
|
||||
await run(`api('/api/later')`);
|
||||
assert.equal(run('answeredAt'), answered - 5000, 'a refresh five seconds on is not the tap\'s');
|
||||
assert.equal(run('tapBusy'), 0);
|
||||
run(`rootKind = ''`);
|
||||
});
|
||||
|
||||
test('a tap ended over a minute after its last request reports once, without recursing (#378)', () => {
|
||||
run(`globalThis.sent = []; fetch = (url, o) => { sent.push([url, o]); return Promise.resolve({ ok: true, status: 204 }); }`);
|
||||
run(`newTrace('pointerdown'); answeredAt = rootAt + 100; traceAt = Date.now() - 120_000`);
|
||||
|
||||
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- A tap's time in the traces ends with the last answer to what it set off, not with a refresh made seconds later (#379).
|
||||
- Switching away from the page a minute after using it no longer stops it with "Maximum call stack size exceeded" (#378).
|
||||
- Two items from different feeds that share an id, as a blog's RSS and Atom feeds' do, are told apart: in All Subscriptions the second can be opened and marked read, where the first was marked whichever was clicked (#361).
|
||||
|
||||
|
||||
@@ -105,8 +105,11 @@ function spanText(secs: number){
|
||||
/// trace, which then lasted nine hours (#219). A play's position saves, every ten seconds, keep it.
|
||||
/// A tap or a key is the trace's root span (#376): its requests name it as their parent, and once
|
||||
/// the next one begins, or the page is hidden, the page reports it, from the tap to its last
|
||||
/// answer, for ipx to send on to Tempo with these ids.
|
||||
let traceId='', traceAt=0, rootId='', rootAt=0, rootKind='', answeredAt=0;
|
||||
/// answer, for ipx to send on to Tempo with these ids. Only requests begun while the tap is still
|
||||
/// at work count towards it: one of its own in flight, or within two seconds of the tap or its last
|
||||
/// answer, as a request an answer sets off is. A live update's refresh thirteen seconds later joined
|
||||
/// the trace and made a tap answered in 0.9 s read as 15 (#379).
|
||||
let traceId='', traceAt=0, rootId='', rootAt=0, rootKind='', answeredAt=0, tapBusy=0;
|
||||
const hex=(n: number)=>Array.from(crypto.getRandomValues(new Uint8Array(n)),b=>b.toString(16).padStart(2,'0')).join('');
|
||||
function endTap(){
|
||||
const kind=rootKind;
|
||||
@@ -118,7 +121,7 @@ function endTap(){
|
||||
}
|
||||
function newTrace(kind: string){
|
||||
endTap();
|
||||
traceId=hex(16); rootId=hex(8); rootAt=Date.now(); rootKind=kind; answeredAt=0;
|
||||
traceId=hex(16); rootId=hex(8); rootAt=Date.now(); rootKind=kind; answeredAt=0; tapBusy=0;
|
||||
}
|
||||
for(const t of ['pointerdown','keydown']) window.addEventListener(t,()=>{ newTrace(t); traceAt=Date.now(); },{capture:true,passive:true});
|
||||
window.addEventListener('pagehide',endTap);
|
||||
@@ -149,10 +152,13 @@ window.addEventListener('unhandledrejection',e=>report('rejection',{msg:String(e
|
||||
async function api(url: string, opts?: RequestInit): Promise<any>{
|
||||
// The browser's "Failed to fetch" said nothing anyone could act on (#304).
|
||||
const tp=traceparent(), mine=traceId;
|
||||
const taps=!!rootKind&&(tapBusy>0||Date.now()-Math.max(rootAt,answeredAt)<2000);
|
||||
if(taps) tapBusy++;
|
||||
const r = await fetch(url,{headers:{'Content-Type':'application/json',traceparent:tp},...opts})
|
||||
.catch(()=>{ throw new Error('Unable to reach iPX. Check your connection and try again.'); });
|
||||
// The tap lasts until its last answer.
|
||||
if(mine===traceId) answeredAt=Date.now();
|
||||
.catch(()=>{ throw new Error('Unable to reach iPX. Check your connection and try again.'); })
|
||||
.finally(()=>{ if(taps&&mine===traceId) tapBusy--; });
|
||||
// The tap lasts until the last answer to what it set off.
|
||||
if(taps&&mine===traceId) answeredAt=Date.now();
|
||||
if(r.status===401){ location.href='/login'; throw new Error('signed out'); }
|
||||
if(!r.ok){
|
||||
// A page in place of ipx's words is Cloudflare or Caddy answering for it: the message became a
|
||||
|
||||
Reference in New Issue
Block a user