Debug flags and toast testing
How to see an error path or a toast on a device without waiting for the real failure. There are two tools:
testToast, a field on the scene that shows any toast. It is in every build.- Debug flags, fields on
m.global.debugthat force a failure in one code path. They exist only in a debug build.
Make a debug build
Section titled “Make a debug build”- In
manifest, changebs_const=debug=falsetobs_const=debug=true. Don’t commit this change. - Build and sideload with
make build-dev install(DEVGUIDE). - In the console (
telnet <roku-ip> 8085), look for[DEBUG] DebugFlags node initialized on m.global.debugat startup. It lists the flags.
Use the console
Section titled “Use the console”The BrightScript console runs a command only while the app is paused. Press Ctrl-C in the console to pause it. The Brightscript Debugger> prompt appears, and c resumes the app.
Ctrl-C pauses whichever thread was running, so check the location the debugger prints:
- On the main thread (
pkg:/source/main.brs),m.topis invalid.Main()keeps the scene inm.scene. - On a component thread (any
pkg:/components/path), reach the scene withm.top.getScene().
m.global works from both.
Show a toast
Section titled “Show a toast”Set testToast on the scene to "type|message". The type is error, success, warning or info; with no |, the whole value is an error message.
From the console, paused on the main thread:
m.scene.testToast = "success|Item saved"Paused on a component thread:
m.top.getScene().testToast = "error|Something went wrong"Without pausing the app, from an RTA build (rta-tests.md), set it through the on-device component:
await odc.setValue({ base: 'scene', keyPath: 'testToast', value: 'success|Item saved' });If testToast shows nothing, the fault is in the toast component, not in the code you are testing.
The up-up-down-down code
Section titled “The up-up-down-down code”A debug build also shows a test toast when you press Up, Up, Down, Down with no more than 2 seconds between presses. Each use shows the next type: error, success, warning, info. It rarely works: JRScene sees the key releases only while the focus is outside the router outlet, because RowList and the outlet consume them (the comment above JRScene.onKeyEvent() explains why). Use testToast instead.
Force a failure
Section titled “Force a failure”Set a flag from the console, then do what the table says:
m.global.debug.shouldForceFavoriteFail = true' resume with c, press the favorite button, see the error toastm.global.debug.shouldForceFavoriteFail = false| Flag | What it does | How to see it |
|---|---|---|
shouldForceFiltersFail | BaseGridView.loadFilters() skips the request and reports a failure | Open a library with filters, such as Movies |
shouldForceFavoriteFail | ItemDetails.toggleFavorite() treats the server’s answer as a failure, which reverts the button and shows an error toast | Press the favorite button on an item’s details screen |
shouldForceWatchedFail | ItemDetails.toggleWatched() does the same for the watched button | Press the watched button on an item’s details screen |
extraButtonCount | Adds this many spare buttons to the button rows on the details screen and the playback OSD | Set it, then open (or reopen) a details screen or the OSD |
The flags default to off, and a flag is read where its code path starts, so a change applies the next time that path runs.
extraButtonCount: seeing the More button
Section titled “extraButtonCount: seeing the More button”A button row holds what fits before the element to its right: 8 buttons on the details screen and 10 on the OSD. Past that, the extra buttons move into a More menu. Real items never fill either row: the details screen tops out at 8 and the OSD at 7. So without this flag, nobody can see More on a device.
It is a count, not a switch, because rows start at different lengths. A person has 3 buttons on the details screen; a series with a trailer, for a user who may delete it, has 8. A row is built when its screen opens, so reopen the screen after a change.
- Use
8to see More and its menu on every item type. - To find the edge, where a row is exactly full and shows no More, lower the count one at a time until More disappears.
The spare buttons have a label and an icon, so the row and the menu look as they would with real buttons. They do nothing when pressed.
Add a flag
Section titled “Add a flag”If the failure is an API request failing or timing out, don’t add a flag. On-device specs can fail any pooled request with rtaFailRequests (rta-tests.md). A flag for each code path would repeat that once per screen.
For any other failure:
-
Add a field to
DebugFlags.xml, defaultfalse(or0for a count):<field id="shouldForceMyThingFail" type="boolean" value="false" /> -
Add it to the
[DEBUG]linessetGlobalNodes()prints inglobals.bs, so the console lists it. -
Check the flag where the code path starts, inside
#if debug, before the real work.BaseGridView.loadFilters()is the model:#if debugif isValid(m.global.debug) and m.global.debug.shouldForceMyThingFaildeliverResult({}, "[DEBUG] Forced failure")returnend if#end if -
Add a row to the table above.
-
Make a debug build, set the flag from the console and watch the failure path run.
The rules for every flag:
- Inside
#if debug. Every line that reads a flag sits inside#if debugand#end if. - Check
isValid(m.global.debug)first. The node does not exist in a release build or in test builds. [DEBUG]on injected messages, so a forced failure is easy to tell from a real one in a toast or a log.- Simulate failures only. Never put real behavior behind a flag.
extraButtonCountis the one flag that adds something, and it adds only inert buttons.
Why a release build has none of this
Section titled “Why a release build has none of this”#if debug is resolved by the Roku device, not by bsc. BrighterScript copies the directives into the .brs it emits, and the device compiles them against the bs_const line in the installed manifest. So a release build loads none of the debug code and never creates the DebugFlags node. Two consequences:
- To check whether a build has debug on, read
build/manifest. The.brsfiles always contain the#if debugblocks. manifest.bs_constin absconfigfile cannot turn debug off; it changes only BrighterScript’s own copy of the manifest. See Compile-time flags.
harden-prod-manifest.js is what guarantees it. It runs last in npm run build:prod and forces debug, ENABLE_RTA and perfTiming off in build/manifest, so no release can ship with them on. A committed debug=true has reached main twice (27d99141, dc05db8d), so a convention alone was not enough.
What else a debug build gives you
Section titled “What else a debug build gives you”rawApiData: what the server sent for an item
Section titled “rawApiData: what the server sent for an item”Every item node that JellyfinDataTransformer builds carries the server’s original BaseItemDto on its rawApiData field (JellyfinBaseItem.xml). Nothing in the app reads it. It answers one question at a breakpoint: is a tile wrong because of our transform, or because of what the server sent?
print node.rawApiDataprint node.rawApiData.UserDatacurl is often easier. The console’s [http] trace prints each request’s URL and auth token, so you can fetch the same response again and compare. rawApiData saves you working out which request produced which node.
An RTA build cannot read it: npm run test:rta turns on ENABLE_RTA only, so debug stays off and rawApiData is invalid.
The Task-thread readout
Section titled “The Task-thread readout”printTaskThreads() prints how many Task threads are live. See debug-tools.md.
Never measure performance on a debug build. Measured 2026-08-04 at n=10, a debug build painted Home 178 ms slower on a 512 MB Stick and 121 ms slower on a Stick 4K.
Troubleshooting
Section titled “Troubleshooting”m.global.debugis invalid. The build hasdebug=false. Checkbuild/manifest, then rebuild.Interface not a member of BrightScript Component(&hf3) onm.top. The console is paused on the main thread. Usem.scene(see Use the console).- A flag is set but nothing happens. Check the name’s spelling against
DebugFlags.xml. Then run the path again: a flag is read when its path starts, and a button row is built when its screen opens.