zebra-report: deploy telemetry fingerprint + camera-error breadcrumbs

Pulls in zebra-report 1ed5263: every entry now emits a one-shot
'session: ua=... ctx={...} devs={...} picked={...}' line so the
signal-server log carries browser + audioCtx + device-count + saved-
deviceId-shortprefix context for diagnosing the open tickets
(0001 fedora chrome silence, 0002 fedora firefox cannot share camera).
Camera-open errors now include e.name + the constraints we tried
plus auto-recovery from OverconstrainedError + saved deviceId.
This commit is contained in:
russell@unturf.com 2026-06-07 11:27:39 -04:00
parent 0f36e02e42
commit 8e762fb100
No known key found for this signature in database

View file

@ -4560,7 +4560,36 @@ async function sfuPublishCamera(){
* speaker can choose camera-on while still using a different audio
* input (monitor source, music mode, etc) */
stream = await navigator.mediaDevices.getUserMedia({ video: videoConstraints, audio: false });
} catch(e){ logLine('err','camera open cancelled: '+e.message); fsm.send('FAILED', { error: e.message }); fsm.send('DONE'); return; }
} catch(e){
/* surface e.name + the constraints we tried so signal-server telemetry
* can disambiguate NotFoundError (device gone) vs OverconstrainedError
* (saved deviceId or 720p/16:9 not supported) vs NotAllowedError (user
* declined permission) vs NotReadableError (camera held by another app).
* Defect #0002 fedora firefox: e.message alone wasn't enough to pick
* the right branch. */
const exact = videoConstraints.deviceId && videoConstraints.deviceId.exact;
logLine('err',
'camera open cancelled: '+(e.name||'Error')+' — '+e.message+
' (constraints: 1280x720@30 16:9'+(exact?' deviceId='+String(exact).slice(0,8):'')+')'
);
/* OverconstrainedError + a saved deviceId is the canonical "saved
* device gone or renamed" case — retry once without deviceId so the
* user gets default camera rather than a permanent fail. */
if (e.name === 'OverconstrainedError' && exact){
logLine('', 'camera open retry without saved deviceId');
try {
const fallbackConstraints = Object.assign({}, videoConstraints);
delete fallbackConstraints.deviceId;
stream = await navigator.mediaDevices.getUserMedia({ video: fallbackConstraints, audio: false });
logLine('', 'camera open recovered with default device');
} catch(e2){
logLine('err','camera open retry failed: '+(e2.name||'Error')+' — '+e2.message);
fsm.send('FAILED', { error: e2.message }); fsm.send('DONE'); return;
}
} else {
fsm.send('FAILED', { error: e.message }); fsm.send('DONE'); return;
}
}
fsm.send('ACQUIRED', { stream });
sfuCameraStream = stream;
const pc = new RTCPeerConnection(rtcConfig);
@ -6029,6 +6058,52 @@ async function handleSignal(raw){
broadcastSpotlight();
reorderTiles();
logLine('', 'joined as '+myRole+' — uuid '+myUUID);
/* one-shot session fingerprint for telemetry (see docs/tickets/
* 0001 + 0002): captures the browser + audio/video device state
* at entry so a reporter's signal-server log carries enough
* context to disambiguate browser-specific defects. Mobile + UA
* fields land first because the bug shape often correlates with
* a single UA family (fxhp-phone-firefox, fedora-chrome, etc).
* Best-effort — wrapped + caught so a hostile UA doesn't break
* entry. */
(async () => {
try {
const ua = navigator.userAgent || '';
const isMobile = /Mobi|Android|iPhone|iPad|iPod/.test(ua);
const browser =
/Firefox\//.test(ua) ? 'firefox' :
/Edg\//.test(ua) ? 'edge' :
/Chrome\//.test(ua) ? 'chrome' :
/Safari\//.test(ua) ? 'safari' : 'other';
let sr = 'n/a', bl = 'n/a', sinkSupp = false, ctxSink = 'n/a';
try {
if (audioCtx){
sr = audioCtx.sampleRate;
bl = (audioCtx.baseLatency != null) ? audioCtx.baseLatency.toFixed(4) : 'n/a';
sinkSupp = typeof audioCtx.setSinkId === 'function';
ctxSink = (audioCtx.sinkId === undefined) ? 'n/a' : (audioCtx.sinkId || 'default');
}
} catch(_){}
let outCount = 0, inCount = 0, camCount = 0;
try {
const devs = await navigator.mediaDevices.enumerateDevices();
for (const d of devs){
if (d.kind === 'audiooutput') outCount++;
else if (d.kind === 'audioinput') inCount++;
else if (d.kind === 'videoinput') camCount++;
}
} catch(_){}
const pickShort = id => id ? String(id).slice(0,8) : '';
logLine('',
'session: ua='+browser+(isMobile?'/mobile':'/desktop')+
' ctx={sr='+sr+' baseLat='+bl+' sinkSupp='+(sinkSupp?1:0)+' sink='+ctxSink+'}'+
' devs={out='+outCount+' in='+inCount+' cam='+camCount+'}'+
' picked={mic='+pickShort(micDeviceId)+
' cam='+pickShort(cameraDeviceId)+
' spk='+pickShort(speakerDeviceId)+'}'
);
} catch(_){}
})();
/* dot, leave/mute button visibility, sec-room reveal, entry-row
* hide and status text are all driven by applyCallStateUI()
* from the 'joined' state. Welcome already dispatched WELCOME
@ -8008,9 +8083,9 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');
</script>
<footer style="margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-06-06</span><br>
md5 <span class="stamp-md5">43f585be4e478d110d4250eff68b9d68</span><br>
sha256 <span class="stamp-sha">6677c5b69c23976a7c9d35104fdc324fe69fa80d24120e1f32aaab23ebdd0606</span><br>
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-06-07</span><br>
md5 <span class="stamp-md5">14af06bd5893feb70229dded7e6c5497</span><br>
sha256 <span class="stamp-sha">e603642628f264fa919f55be33e6578d77d2d42858566c2d708beefce091aed5</span><br>
<span style="color:#bbb">hashes are of this page with these two fields zeroed — to verify, blank them and re-hash</span><br>
<span style="color:#bbb">one self-contained file — <strong>save a copy</strong> and verify against these hashes; point at your own servers with ?signal= and ?turncred=, or <a href="host-your-own.html" style="color:#999">host your own community</a></span>
</footer>