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]);
|
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)', () => {
|
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(`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`);
|
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]
|
## [Unreleased]
|
||||||
|
|
||||||
### Fixed
|
### 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).
|
- 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).
|
- 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.
|
/// 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
|
/// 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
|
/// 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.
|
/// answer, for ipx to send on to Tempo with these ids. Only requests begun while the tap is still
|
||||||
let traceId='', traceAt=0, rootId='', rootAt=0, rootKind='', answeredAt=0;
|
/// 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('');
|
const hex=(n: number)=>Array.from(crypto.getRandomValues(new Uint8Array(n)),b=>b.toString(16).padStart(2,'0')).join('');
|
||||||
function endTap(){
|
function endTap(){
|
||||||
const kind=rootKind;
|
const kind=rootKind;
|
||||||
@@ -118,7 +121,7 @@ function endTap(){
|
|||||||
}
|
}
|
||||||
function newTrace(kind: string){
|
function newTrace(kind: string){
|
||||||
endTap();
|
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});
|
for(const t of ['pointerdown','keydown']) window.addEventListener(t,()=>{ newTrace(t); traceAt=Date.now(); },{capture:true,passive:true});
|
||||||
window.addEventListener('pagehide',endTap);
|
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>{
|
async function api(url: string, opts?: RequestInit): Promise<any>{
|
||||||
// The browser's "Failed to fetch" said nothing anyone could act on (#304).
|
// The browser's "Failed to fetch" said nothing anyone could act on (#304).
|
||||||
const tp=traceparent(), mine=traceId;
|
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})
|
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.'); });
|
.catch(()=>{ throw new Error('Unable to reach iPX. Check your connection and try again.'); })
|
||||||
// The tap lasts until its last answer.
|
.finally(()=>{ if(taps&&mine===traceId) tapBusy--; });
|
||||||
if(mine===traceId) answeredAt=Date.now();
|
// 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.status===401){ location.href='/login'; throw new Error('signed out'); }
|
||||||
if(!r.ok){
|
if(!r.ok){
|
||||||
// A page in place of ipx's words is Cloudflare or Caddy answering for it: the message became a
|
// 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