7.5 KiB
Mintegral Checker — TODO
Task Overview
Build a local VS Code webview equivalent of the Mintegral PlayTurbo checker
(https://www.playturbo.com/review). Validates a Mintegral playable ad ZIP against
PlayTurbo requirements. Two check categories:
- Static — parse
index.html+ ZIP structure without running the ad. - Runtime — load the ad in a sandboxed
srcdociframe, inject a monitoring stub, and detect whether lifecycle callbacks (gameReady,gameEnd,gameStart,gameClose,install) actually fire.
Finished
- Pure-Node ZIP parser (EOCD → Central Directory → Local File Headers, zlib DEFLATE)
- All 9 static checks:
- HTML Requirements (charset, viewport, single-file)
- CTA Method (
window.installreference in scripts) - Game End / Game Ready / Game Start / Game Close (regex on scripts)
- File Handling (no external URLs, base64 assets)
- File Spec (ZIP ≤ 5 MB, single
index.html) - Storage (no
localStorage.setItem/sessionStorage.setItem)
- Device-frame simulator (phone mockup around
srcdociframe) - Event log panel showing runtime events as they fire
- Runtime timer with 60s timeout and per-check waiting badges
__ping__handshake → "Simulator connected" in event log (confirms stub runs)- Dual notification channels: polling
__hplMtgEvents+parent.__hplMtgNotify MW_INITinterceptor (wrapsemitCheckData, forces_hasReview/_isPreview)window.postMessageoverride (catchespreviewer:gameReadyself-posts)mkAccess—Object.definePropertyaccessor with undefined getter so AD'stypeof window.gameReady === 'undefined'guard passes, setter wraps assignment- Capture-phase
loadsafety net — wraps any SDK function declarations that overwrote accessors, fires beforebody.onload - Mute button UI (shows on ZIP load, hidden on reset, toggles Mute/Unmute label)
- Diagnostic lifecycle logging (
post-parse,DOMContentLoaded,load,load+500ms, callback assignment/call traces) - AudioContext construction proxy for mute/unmute of closure-owned engine audio
Bugs
1. Callbacks never fire (critical, diagnostics added)
Symptom: Event log shows "Simulator connected" (__ping__ fires) but no lifecycle
events appear. The same ZIP passes all checks on the real PlayTurbo site.
Root cause candidates (narrowed down but not confirmed):
A. body.onload vs capture load ordering on window — When the event target is
window itself, capture and bubble listeners both run at the "at target" phase in
registration order. Our stub is in <head> and registers first, so it should fire
before the body's onload. But this assumes no edge-case behaviour in VS Code's
Chromium sandbox / srcdoc context.
B. SDK function declaration closes over a local reference — If the ad does:
var gameReady = function() { ... };
window.gameReady = gameReady;
and body.onload="gameReady()" resolves to the local var (via closure), then
reassigning window.gameReady in our load-time wrapper has no effect.
C. CSP in VS Code webview blocks stub execution silently — The webview panel has
an implicit CSP. The stub is injected into srcdoc; it might run in a restricted
context that prevents property interception or parent access for later events (even
though __ping__ works because it runs synchronously at parse time).
D. Ad calls gameReady() before load — If the ad boots via DOMContentLoaded and
calls gameReady() there (rather than body.onload), our load-event safety net fires
too late. The mkAccess accessor should still catch it, but only if the ad assigns
window.gameReady first.
What's been added:
- Injected diagnostic logging for lifecycle function
typeofat post-parse, DOMContentLoaded, load capture before/after wrapping, and 500 ms after load. - Logs callback assignment/wrapping and wrapper call paths to the event log.
- Logs dynamic
document.createElement('script')calls to help trace script injection.
What's still needed if callbacks still do not fire:
- Check if the ad bundles
preview-util.jsinsideindex.htmlor references it externally (would fail file-handling check anyway). - Compare the diagnostic timeline against the real PlayTurbo execution path.
2. Mute button does nothing (fixed in monitor stub)
Symptom: Clicking Mute/Unmute changes the label but has no audible effect.
Root cause: The current approach searches Object.keys(cw) for an AudioContext
instance. AudioContexts are almost always created in closures (inside Phaser or the ad
IIFE) and are never assigned as window properties — Object.keys will never find them.
cw.Phaser.game.sound also likely fails because Phaser stores the game instance
internally, not at window.Phaser.game.
Fix applied:
Intercepts AudioContext/webkitAudioContext construction in the monitoring stub,
stores captured contexts/gain nodes on window.__hplAudioContexts and
window.__hplMasterGains, and routes ctx.destination through a master gain when
possible. The mute button now sets captured master gain values first, then falls back
to suspending/resuming captured contexts and exposed Phaser/global game sound objects.
Improvements / Nice-to-Have
-
Inject SDK alongside ad — The real PlayTurbo checker loads
preview-util.jsnext to the ad. Bundling (or downloading once) the 265 KB SDK and injecting it into the srcdoc would letMW_INIT/emitCheckDatafire natively, which is the most reliable callback path and matches what the real checker does. -
Diagnostic mode — A "Debug" toggle that logs
typeofof each lifecycle function at key moments (post-parse, DCL, load, 500ms after load) to the event log, making root-cause analysis faster. -
Replay button — Reload only the iframe (not the full ZIP re-parse) to retest without re-picking the file.
-
Portrait / landscape toggle — Flip the device frame aspect ratio.
-
Multiple HTML entry points — Some ZIPs have
playable.htmlinstead ofindex.html; add a fallback search. -
Orientation lock detection — Check for
screen.orientation.lock()calls (Mintegral requires the ad to handle both orientations). -
Version bump and changelog entry — Once callbacks are working, bump to 0.1.8 and add a changelog entry for the Mintegral Checker tool.
Known Issues / Constraints
-
VS Code
WebviewPanelruns in a sandboxed Chromium process.srcdociframes are same-origin with the webview, soparentaccess works — but the webview's implicit CSP may still block certain APIs inside nested iframes. -
The
Object.definePropertyaccessor approach (mkAccess) is the only way to let the AD'stypeof window.X === 'undefined'guard pass while still intercepting the assignment. Any early concrete assignment (evenundefined) breaks the guard in some runtimes. -
SDK
functiondeclarations at brace-depth 0 are[[DefineOwnProperty]]calls at parse time; they overwrite our configurable accessors unconditionally. The load-time safety net is the only recovery path for that scenario. -
The
applyStaticResultsfunction setsruntimeStatus = nullfor lifecycle checks that already failed statically.handleRuntimeEventonly transitionsruntimeStatus === 'waiting'→'pass'; it silently ignoresnull. This means a static-fail check can't be runtime-rescued via the direct lifecycle path (though thehandleSdkCheckpath does handlenull). Intentional, but worth documenting.