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:

  1. Make it the first script loaded. That way, it can catch errors that occur during page load
  2. Print the UA (browser identification information) and support for the APIs you use (features your code calls) on the first line
  3. Send window.onerror (and unhandledrejection) to the screen
  4. 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://inspectOn-screen debug panel (?debug=1)
RequirementsPC, USB cable, Developer options → USB debugging on the phone None. Just add ?debug=1 to the URL
What you can seeThe full DevTools suite, including console and networkOnly the logs you add and global errors
On the phone experiencing the bugYou have to plug that phone into a PCJust send a link. Get the logs back by having the user copy them
In this incidentNot attemptedThe 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

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.

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 .

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

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.

  1. Load the panel first and reopen the page. Check whether errors appear before you select a file.
  2. 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 returns undefined, the helper isn't ready; that alone doesn't establish that a network request was blocked.
  3. Open the same page path in a working browser and compare. Check the script reference paths and execution order too.
  4. 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.
  5. 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

Reporter's Notes — get the first error before fixing the buttons

Park Doeun
We're building that web app so everyone in the studio can watch the same video in sync on their phones. On my Android, the filename appears, but the Create Room and Join buttons won't enable.
Jung Haneul
Seeing that a video was selected doesn't mean everything is ready. Let's separate the stages and see where it stopped: reading the duration, calculating a fingerprint to check whether it's the same video, or enabling the buttons.
Jung Bada
If it's duration reading, maybe 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.
Jung Haneul
If that hypothesis is right, ending the wait should let it proceed. Let's make it move on after six seconds even if it can't read the duration, and change the server to accept files with an unknown duration too, then check.
Park Doeun
I tried again after the change, but Join is still disabled. iPhone and desktop work, but this phone stops at ‘방 정보를 불러오는 중’ (Loading room information).
Jung Haneul
Then we need to retract the claim that ending the duration wait fixed it. Now we've also observed that it isn't getting room information, so we can't focus only on file processing.
Park Dohyun
A timeout can be a safeguard, but it isn't evidence of the cause. If nothing changed on the affected device, we need to take back “fixed” first.
Jung Bada
If it's waiting for room information, could the problem be WebSocket—the connection used to exchange messages with the server? Let's check whether the desktop viewer does the same thing.
Park Doeun
I opened it, and desktop connects and gets the room information. I compared browsers on the phone too: regular Chrome works, only Whale fails. So the whole server hasn't stopped.
Jung Bada
Then could Whale be blocking the connection? I think we could find out by checking connection status and comparing with different blocking settings, but we don't have those results yet.
Jung Haneul
With no results, let's leave connection blocking unconfirmed. “Waiting for room information” is just what the screen shows. We need to check whether the connection code even started.
Park Doeun
We don't have that log yet, but the file picker shows a notice about cloud media. I can select the JSON file for movement paths, but not the video—could that be a clue too?
Jung Bada
That difference gives us reason to suspect the photo and video picker. Let's compare by leaving only extensions in the accept attribute, which specifies selectable formats, or removing it entirely.
Park Doeun
Changing it didn't work. Even when I tried again later after removing the filter, it still wouldn't reach a playable state. Is there a way to see execution logs on the screen?
Park Dohyun
Let's get those logs first. The picker looks suspicious, but changing the conditions didn't fix it. We should spend our time finding where execution actually stopped before making another guess.
Jung Haneul
We could get logs through remote debugging by connecting it to a PC with a cable, but let's add the ?debug=1 on-screen panel first this time. Load it before the app so it catches startup errors too.
Jung Bada
Let's also print the UA—the browser identification information—and support for the APIs, the features the code uses. With logs for each file processing stage and a copy button, we should be able to see where it stopped.
Park Doeun
The panel shows an error as soon as I open the page. I haven't selected a file yet, but it says window.FormationFingerprint is missing. What's that?
Jung Bada
It's the object holding the fingerprint function that checks whether the videos match. It's missing. Could the relative path to the separate fingerprint.js file be wrong because the address is /leader without .html?
Jung Haneul
Let's open that exact path on desktop too. If we check whether the file is ready and the object exists, we can compare whether the path alone causes the same problem.
Park Doeun
On desktop, the object exists even when I open /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.
Jung Haneul
Let's drop the hypothesis that it's just the path. Execution stopped at the beginning, so file selection handling and the server connection never started either. The same startup error explains both symptoms.
Park Dohyun
It looked like it stopped after selecting a file, but the actual failure happened earlier. This is exactly why diagnostic code needs to load before the app.
Jung Haneul
To resolve that startup error, I renamed the file to 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.
Park Doeun
It works now! I can proceed after the rename. But this success alone doesn't tell us which blocking setting in Whale caused it.
Park Dohyun
We need the first error to narrow down which stage failed. On a device where we can't access the console directly, let's get a log panel running before the app before adding more guesses.