Dev Investigation Notes
When it only fails in a mobile browser (Whale) and you can't access the console — an on-screen debug panel with ?debug=1
This is a translated article. Read the original in Korean ↗
I was building a video synchronization web app for a dance studio, where everyone watches their own copy of a video in sync with the leader's controls. To avoid starting a laptop server every time, I moved it to the cloud, but in Whale on Android, selecting a video still wouldn't enable the Create Room or Join buttons.
Conclusion
In this incident, page initialization stopped because the separate script fingerprint.js wasn't loading in Whale. Renaming it to mediacheck.js and updating the references on both pages restored normal operation. Which feature or filter blocked it, however, was never confirmed.
If you're fixing a mobile-only browser bug by guessing, stop and first add an on-screen log panel that appears only with ?debug=1. Four things matter:
- Make it the first script loaded. That way, it can catch errors that occur during page load
- Print the UA (browser identification information) and support for the APIs you use (features your code calls) on the first line
- Send
window.onerror(andunhandledrejection) to the screen - Add a copy button so users can paste the logs exactly as they appear
There was a five-day gap between the first attempt (August 25) and the next attempt (August 30). That doesn't mean five continuous days of debugging. Adding the panel on the second attempt exposed an initialization error that occurred before file selection, and renaming the file brought a report of success.
Below is an example implementation that reproduces the same setup (this is not the original debug.js code. The original components are listed under “Panel setup” in ## Fix below ).
<!-- The page's first <script>. Before any other script -->
<script src="/debug.js"></script>
// debug.js — an on-screen log panel enabled only with ?debug=1 (example)
(function () {
const on = new URLSearchParams(location.search).get('debug') === '1';
const lines = [];
let box;
function t() { return new Date().toISOString().slice(11, 23); }
function render() { if (box) box.textContent = lines.join('\n'); }
window.dbg = function (...args) {
if (!on) return;
const msg = args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ');
lines.push(t() + ' ' + msg);
render();
};
if (!on) return;
// 1) Register first to catch errors during page load too
window.addEventListener('error', e =>
dbg('❌ window.error:', e.message, '@', (e.filename || '') + ':' + e.lineno + ':' + e.colno));
window.addEventListener('unhandledrejection', e =>
dbg('❌ unhandledrejection:', String(e.reason)));
// 2) Environment in one line: rule out hypotheses at once
dbg('=== debug on ===');
dbg('UA:', navigator.userAgent);
dbg('secureContext:', window.isSecureContext,
'| crypto.subtle:', !!(window.crypto && crypto.subtle),
'| Blob.arrayBuffer:', typeof Blob !== 'undefined' && 'arrayBuffer' in Blob.prototype,
'| File.text:', typeof File !== 'undefined' && 'text' in File.prototype);
// 3) Panel + copy button (attach after body exists)
document.addEventListener('DOMContentLoaded', () => {
const wrap = document.createElement('div');
wrap.style.cssText = 'position:fixed;left:0;right:0;bottom:0;max-height:40vh;overflow:auto;' +
'background:#000c;color:#0f0;font:11px/1.4 monospace;z-index:99999;padding:6px';
const btn = document.createElement('button');
btn.textContent = '복사 (Copy)';
btn.onclick = () => navigator.clipboard.writeText(lines.join('\n'))
.then(() => btn.textContent = '복사됨 (Copied)', () => btn.textContent = '복사 실패 (Copy failed)');
box = document.createElement('pre');
box.style.cssText = 'margin:0;white-space:pre-wrap';
wrap.append(btn, box);
document.body.append(wrap);
render();
});
})();
Next, insert a dbg() line at each stage of the suspected pipeline. In this incident, the sequence was file information → duration reading started/completed → fingerprint calculation started/completed → final button disabled value .
Remote debugging with chrome://inspect | On-screen debug panel (?debug=1) | |
|---|---|---|
| Requirements | PC, USB cable, Developer options → USB debugging on the phone | None. Just add ?debug=1 to the URL |
| What you can see | The full DevTools suite, including console and network | Only the logs you add and global errors |
| On the phone experiencing the bug | You have to plug that phone into a PC | Just send a link. Get the logs back by having the user copy them |
| In this incident | Not attempted | The first log confirmed the cause |
If you need to inspect network requests too, chrome://inspect is better. If all you need to know is “where it died,” the panel is much faster.
Situation
The leader opens a practice room and selects a video. Participants join the same room from the room list and select the video files they received beforehand. The server relays synchronization signals rather than video, and participant playback follows the leader's controls.
The helper that calculates fingerprints to check whether the videos match was a separate script. After file selection, duration reading and fingerprint calculation determine the button state, while the viewer receives room information from the server to compare videos. So observing a disabled button alone couldn't tell us which stage had failed.
Symptom
- The web app was hosted on Cloudflare Workers. Selecting a video file on Android displayed the filename, but the "참여" (Join) and "방 만들기" (Create Room) buttons wouldn't enable
- The viewer screen stopped at "방 정보를 불러오는 중" (Loading room information). iPhone and desktop worked normally
- On the same phone, regular Chrome worked; only Whale failed
- The same symptoms persisted on the second attempt, five days after the first
In this incident, changes were made repeatedly without console logs from the affected device. The on-screen panel was added later, while looking for ways to debug on mobile.
Cause
There are two layers to consider.
FormationFingerprint is a global object containing the fingerprint function used to check whether videos match, and fingerprintVideo is the function extracted from it.
The cause of the bug itself. The panel's first log looked like this (the deployment address has been removed) .
UA: Mozilla/5.0 (Linux; Android 10; K) ... Chrome/138.0.0.0 Whale/3.9.14.9 Mobile Safari/537.36
secureContext: true | crypto.subtle: true | Blob.arrayBuffer: true | File.text: true
❌ window.error: Uncaught TypeError: Cannot destructure property 'fingerprintVideo' of 'window.FormationFingerprint' as it is undefined. @ /leader?debug=1:172:13
The error occurred as soon as the page opened, before a file was even selected. The video fingerprint helper in the separate file fingerprint.js wasn't loading in Whale alone. As a result, the inline script died at the destructuring statement at the top, and none of the file handlers, WebSocket connection, or button-enabling code below it were registered . This one error explains both the disabled buttons and the screen stuck at "방 정보를 불러오는 중" (Loading room information) .
Renaming the file to mediacheck.js made it work in Whale too . Only the filename and references changed; the global name FormationFingerprint and function names stayed the same . So it's reasonable to conclude that the filename (URL) was the basis for the block. Which Whale feature or filter list blocked it, however, wasn't checked. The explanation that “tracking prevention mistook fingerprint for a fingerprint-tracking script” was the agent's inference .
Why the cause wasn't found on the first day. With no way to see the error, fixes were based on symptoms alone. The five days were the gap between the first day's attempt (August 25) and the next attempt (August 30), not time spent working continuously on the bug. The dates in the record confirm this gap and the retry.
- On the first attempt, four hypotheses were proposed in turn—an endless wait for duration reading, a failed WebSocket connection, Whale blocking WS, and the Android photo picker—and the code was changed several times accordingly, but the symptoms remained
- On the second attempt, the user suggested “if there's a way to access the mobile browser console or debugging tools,” and that shifted the approach toward a panel
Fix
1. Why we chose the panel out of the two options
The agent offered both remote debugging with chrome://inspect and an in-app debug panel. Remote debugging required a cable and configuration. The panel was chosen first because it would work immediately in Whale without setup . The agent said remote debugging “usually works” with Whale too, but it wasn't attempted in this incident, so that remains an unverified claim here .
2. Panel setup
The usage instructions described the following setup .
- How to enable it:
.../leader.html?debug=1for the leader; append&debug=1after the existing query for the viewer - At the top: UA, secureContext, crypto.subtle, Blob.arrayBuffer, File.text support status
- Pipeline stages: file information (name/size/type) →
readDurationstart/completion/error →fingerprintstart/completion/error → finalsetupBtn.disabled/go_btn.disabledvalues indicating whether the Create Room and Join buttons are disabled - A green panel fixed to the bottom + a "복사" (Copy) button
debug.js was added as the first script on both pages . This was the most important decision in the incident. The actual error occurred during page load, not in the file selection pipeline. It was caught because the panel was running before the app script and listening for window.error .
3. What the first line did
While designing the panel, the agent defined how to interpret the logs in advance: crypto.subtle: false would mean Whale was blocking the security API; size: 0 would mean the file bytes weren't coming through, and so on . In the actual first line, all four values were true . These values only show whether the environment or APIs are present; they don't guarantee that actual calls will succeed. In this incident, the decisive evidence identifying where execution stopped was the single window.error line immediately below.
4. After receiving the logs
- The agent initially suspected a broken relative path because the URL was
/leaderwithout.html. But opening/leaderon desktop worked normally, so that hypothesis was immediately dropped fingerprint.jswas renamed tomediacheck.js, and the reason for the rename was recorded in the file's header comment- The change was committed after checking for regressions on desktop
- User: “It works!! It's working! Great job!”
- The panel was kept rather than removed, so it could be reused for future mobile issues
5. What to check in your own project, in order
The following suggested checks are drawn from the comparisons used in this incident. Verify the missing-global-object error separately from the success after renaming the file.
- Load the panel first and reopen the page. Check whether errors appear before you select a file.
- After the point where the helper script should have executed, immediately before the code that uses its object, add
dbg('helper:', typeof window.FormationFingerprint). Replace the global name with the actual one in your app. If it returnsundefined, the helper isn't ready; that alone doesn't establish that a network request was blocked. - Open the same page path in a working browser and compare. Check the script reference paths and execution order too.
- If there is still reason to suspect the filename, change only the filename and references while keeping the contents, global names, and function names unchanged, then compare. Check that the new file is being served and whether the error and button behavior have changed.
- Even if renaming makes it work, don't conclude that a particular blocking feature was responsible. In this incident, the source of the block remained unidentified.
Bonus — side lessons from the same incident
- Don't let diagnostic code go down with the app code. During the first attempt, a diagnostic indicator showing connection status was added to the viewer's "함께 보기" (Watch Together) screen . But no one reported what it displayed, and the user soon moved on to “Chrome works, Whale doesn't” . If a page script dies immediately after loading, diagnostic code inside that same script dies with it. The session didn't establish exactly which script contained that indicator, but a panel loaded first from a separate file clearly avoids this trap
- Verify deployments in a browser when possible. Checking the panel deployment with curl + grep returned zero references to
debug.js. Initially, the edge cache was suspected . Next, the suspected cause was a gzip response that hadn't been decompressed, so--compressedwas added—but this time the result was zero bytes . Opening it in a browser finally confirmed that deployment was correct . Why curl went wrong was never established - The file picker detour was a side issue. Changing
acceptthree times on the first day didn't resolve the Whale symptoms . After finding the cause, the agent concluded that the extension filter had also failed because blockingfingerprint.jswas killing the script, and restored a filter using only extensions, without MIME types such asvideo/*. The session doesn't show whether the restored filter worked in Whale - Don't use
fingerprintin script filenames. In this incident, renaming alone fixed it . That doesn't mean avoiding a particular word can prevent every blocking issue. The verified fix here was renaming this file
Reporter's Notes — get the first error before fixing the buttons
loadedmetadata isn't firing—that's the signal that basic information like video duration is ready. If it keeps waiting for the duration, the buttons won't enable either. accept attribute, which specifies selectable formats, or removing it entirely. ?debug=1 on-screen panel first this time. Load it before the app so it catches startup errors too. window.FormationFingerprint is missing. What's that? fingerprint.js file be wrong because the address is /leader without .html? /leader. What I saw in Whale wasn't a failure after file selection—it was an error saying the object was missing as soon as the page opened. mediacheck.js and updated the references on both pages. I kept the function and global object names and checked it on desktop too. Now let's see what happens in Whale.