@roku-sdk/ai 0.0.0-stage → 2.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -2
  3. package/package.json +47 -3
  4. package/skills/ads-and-raf/SKILL.md +231 -0
  5. package/skills/ads-and-raf/references/integration-flows.md +332 -0
  6. package/skills/app-packaging/SKILL.md +107 -0
  7. package/skills/app-performance/SKILL.md +133 -0
  8. package/skills/cli-dev-loop/SKILL.md +217 -0
  9. package/skills/cli-dev-loop/references/logs-and-crashes.md +114 -0
  10. package/skills/cpu-and-heap-profiling/SKILL.md +108 -0
  11. package/skills/cpu-and-heap-profiling/references/reading-profiles.md +52 -0
  12. package/skills/data-entry-keyboards/SKILL.md +160 -0
  13. package/skills/device-management/SKILL.md +158 -0
  14. package/skills/dialogs-and-modals/SKILL.md +210 -0
  15. package/skills/effects/SKILL.md +181 -0
  16. package/skills/fetch-and-data-loading/SKILL.md +164 -0
  17. package/skills/fetch-and-data-loading/references/https-and-certificates.md +79 -0
  18. package/skills/flex-grid/SKILL.md +236 -0
  19. package/skills/flex-grid/references/focus-integration.md +355 -0
  20. package/skills/flex-grid/references/plugins.md +201 -0
  21. package/skills/focus-management/SKILL.md +205 -0
  22. package/skills/focus-management/references/focus-recipes.md +282 -0
  23. package/skills/fonts-and-glyphs/SKILL.md +176 -0
  24. package/skills/fonts-and-glyphs/references/glyph-tables.md +128 -0
  25. package/skills/logging-and-errors/SKILL.md +227 -0
  26. package/skills/logging-and-errors/references/uncaught-exceptions.md +137 -0
  27. package/skills/mcp-ai-dev-loop/SKILL.md +189 -0
  28. package/skills/mcp-ai-dev-loop/references/crash-and-console.md +68 -0
  29. package/skills/mcp-ai-dev-loop/references/live-debugging.md +129 -0
  30. package/skills/mcp-setup/SKILL.md +131 -0
  31. package/skills/project-setup/SKILL.md +210 -0
  32. package/skills/project-setup/references/roku-config.md +120 -0
  33. package/skills/screen-navigation/SKILL.md +166 -0
  34. package/skills/sdk-components/SKILL.md +213 -0
  35. package/skills/sdk-components/references/dialogs.md +148 -0
  36. package/skills/sdk-components/references/scenegraph-node-spelling.md +123 -0
  37. package/skills/sdk-installation/SKILL.md +129 -0
  38. package/skills/slideshow/SKILL.md +157 -0
  39. package/skills/slideshow/references/focus-and-handoff.md +187 -0
  40. package/skills/solid-on-roku/SKILL.md +181 -0
  41. package/skills/solid-on-roku/references/hermes-and-exports.md +130 -0
  42. package/skills/testing/SKILL.md +147 -0
  43. package/skills/testing/references/async-and-timing.md +97 -0
  44. package/skills/user-accounts-and-purchases/SKILL.md +164 -0
  45. package/skills-manifest.json +316 -0
  46. package/skills-manifest.schema.json +62 -0
  47. package/src/__tests__/manifest.test.ts +267 -0
  48. package/src/__tests__/snippets.test.ts +221 -0
  49. package/src/__tests__/trigger-routing.test.ts +77 -0
  50. package/src/index.ts +36 -0
  51. package/src/manifest.ts +215 -0
  52. package/src/router.ts +147 -0
  53. package/src/snippets.ts +212 -0
@@ -0,0 +1,128 @@
1
+ # Glyph tables
2
+
3
+ 45 constants, from `@roku-sdk/components/glyphs`. They are split into three tables here for the
4
+ one reason that matters at runtime — **which font supplies the character** — because that, not the
5
+ codepoint, decides whether a symbol survives a custom app typeface.
6
+
7
+ | Group | Count | Under a system font | Under a custom app font |
8
+ |---|---|---|---|
9
+ | RokuFont01 private-use glyphs | 32 | renders | renders — firmware fallback applies |
10
+ | Unicode characters RokuFont01 backs | 3 | renders | renders — same fallback |
11
+ | RokuUI-Regular symbols | 10 | renders | **tofu**, unless the custom font supplies them |
12
+
13
+ 35 of the 45 are safe under a custom typeface. The ten that are not are the ones that look most
14
+ ordinary, which is why this split is worth carrying.
15
+
16
+ Verified on device twice, on different hardware and different firmware, by rendering the
17
+ constants in a label with a custom TrueType font.
18
+
19
+ - Roku Express 4K+, firmware 15.3.5: the private-use glyphs and the stars came through; `←` `↑`
20
+ `→` `↓` `✓` `✗` were empty boxes. `•` `…` `–` `—` rendered only because that particular font
21
+ happened to contain them — do not read that as a guarantee.
22
+ - Roku Native (2910X), firmware 16.0.4, 1080p: same split. Under the system font every constant
23
+ rendered; under a custom TrueType the button glyphs and `★` `☆` rendered and `←` `→` `✓` were
24
+ boxes. The same run also showed a `Font` with a `uri` and no `size` drawing a speck, and
25
+ `fallbackGlyph` — as `u25CF` and as a literal `●` — leaving every box exactly as it was, both
26
+ for a RokuUI-only symbol and for a codepoint (`U+0E5B`, `U+A64B`) that no font on the device
27
+ has.
28
+
29
+ The two runs agree, which is the point of having both: this split is a property of the firmware's
30
+ font chain, not of one box.
31
+
32
+ ## 1. RokuFont01 — private-use glyphs (safe under a custom font)
33
+
34
+ RokuFont01 is wired in as the system-wide fallback font in firmware, so these resolve wherever
35
+ they appear.
36
+
37
+ | Constant | Codepoint | Placeholder | Description |
38
+ |---|---|---|---|
39
+ | `OPTIONS_GLYPH` | `U+E000` | `{OPTIONS_GLYPH}` | Options / star (no border) |
40
+ | `OK_BUTTON_GLYPH` | `U+E001` | `{OK_BUTTON_GLYPH}` | OK in circle |
41
+ | `OPTIONS_BUTTON_GLYPH` | `U+E002` | `{OPTIONS_BUTTON_GLYPH}` | Options / star (with border) |
42
+ | `MICROPHONE_GLYPH` | `U+E003` | `{MICROPHONE_GLYPH}` | Microphone (no border) |
43
+ | `PLAY_PAUSE_BUTTON_GLYPH` | `U+E004` | `{PLAY_PAUSE_BUTTON_GLYPH}` | Play/pause (with border) |
44
+ | `PLAY_PAUSE_GLYPH` | `U+E005` | `{PLAY_PAUSE_GLYPH}` | Play/pause (no border) |
45
+ | `DOT_GLYPH` | `U+E006` | `{DOT_GLYPH}` | Baseline-aligned dot (icon-row form) |
46
+ | `HOME_GLYPH` | `U+E007` | `{HOME_GLYPH}` | Home (no border) |
47
+ | `SEARCH_GLYPH` | `U+E008` | `{SEARCH_GLYPH}` | Search / magnifying glass |
48
+ | `REMOTE_GLYPH` | `U+E009` | `{REMOTE_GLYPH}` | Remote control |
49
+ | `REPLAY_GLYPH` | `U+E010` | `{REPLAY_GLYPH}` | Instant replay |
50
+ | `MICROPHONE_BUTTON_GLYPH` | `U+E011` | `{MICROPHONE_BUTTON_GLYPH}` | Microphone (with border) |
51
+ | `SEARCH_BUTTON_GLYPH` | `U+E012` | `{SEARCH_BUTTON_GLYPH}` | Search (with border) |
52
+ | `FAST_FORWARD_GLYPH` | `U+E013` | `{FAST_FORWARD_GLYPH}` | Fast forward |
53
+ | `REWIND_GLYPH` | `U+E014` | `{REWIND_GLYPH}` | Rewind |
54
+ | `SHORTCUT_ONE_BUTTON_GLYPH` | `U+E015` | `{SHORTCUT_ONE_BUTTON_GLYPH}` | Shortcut 1 |
55
+ | `SHORTCUT_TWO_BUTTON_GLYPH` | `U+E016` | `{SHORTCUT_TWO_BUTTON_GLYPH}` | Shortcut 2 |
56
+ | `CLOSED_CAPTIONS_GLYPH` | `U+E017` | `{CLOSED_CAPTIONS_GLYPH}` | Closed captions |
57
+ | `LIVE_TV_GLYPH` | `U+E018` | `{LIVE_TV_GLYPH}` | Live TV |
58
+ | `SLEEP_TIMER_GLYPH` | `U+E019` | `{SLEEP_TIMER_GLYPH}` | Sleep timer |
59
+ | `GENERAL_APP_GLYPH` | `U+E020` | `{GENERAL_APP_GLYPH}` | General app icon |
60
+ | `BACK_BUTTON_GLYPH` | `U+E021` | `{BACK_BUTTON_GLYPH}` | Back |
61
+ | `HOME_BUTTON_GLYPH` | `U+E022` | `{HOME_BUTTON_GLYPH}` | Home button |
62
+ | `QUICK_LAUNCH_BUTTON_GLYPH` | `U+E023` | `{QUICK_LAUNCH_BUTTON_GLYPH}` | Quick launch / rocket |
63
+ | `DOWN_BUTTON_GLYPH` | `U+E024` | `{DOWN_BUTTON_GLYPH}` | Down |
64
+ | `LEFT_BUTTON_GLYPH` | `U+E025` | `{LEFT_BUTTON_GLYPH}` | Left |
65
+ | `RIGHT_BUTTON_GLYPH` | `U+E026` | `{RIGHT_BUTTON_GLYPH}` | Right |
66
+ | `KIDS_PROFILE_GLYPH` | `U+E027` | `{KIDS_PROFILE_GLYPH}` | Kids profile |
67
+ | `UP_BUTTON_GLYPH` | `U+E028` | `{UP_BUTTON_GLYPH}` | Up |
68
+ | `POWER_BUTTON_GLYPH` | `U+E029` | `{POWER_BUTTON_GLYPH}` | Power |
69
+ | `THUMBS_UP_GLYPH` | `U+E030` | `{THUMBS_UP_GLYPH}` | Thumbs up |
70
+ | `THUMBS_DOWN_GLYPH` | `U+E031` | `{THUMBS_DOWN_GLYPH}` | Thumbs down |
71
+
72
+ The numbering is decimal-style: the A–F slots in each decade (`U+E00A`–`U+E00F`, `U+E01A`–`U+E01F`,
73
+ `U+E02A`–`U+E02F`) are unassigned. The font defines 32 private-use codepoints and the table lists
74
+ all of them.
75
+
76
+ `DOT_GLYPH` (`U+E006`) and `BLACK_CIRCLE` (`U+25CF`) are the same outline at two vertical
77
+ positions — `DOT_GLYPH` sits on the baseline to line up with the other icon glyphs, `BLACK_CIRCLE`
78
+ is raised to text centre for use inside a run of text. Both have full outline data; neither is
79
+ reserved.
80
+
81
+ ## 2. Unicode characters RokuFont01 backs (safe under a custom font)
82
+
83
+ Real Unicode codepoints, but RokuUI-Regular does not contain them — they resolve through the same
84
+ RokuFont01 fallback as the private-use glyphs above. That makes them the *safe* shapes to use in
85
+ custom-font text.
86
+
87
+ | Constant | Codepoint | Char | Description |
88
+ |---|---|---|---|
89
+ | `BLACK_CIRCLE` | `U+25CF` | ● | Dot raised to text centre, for use inside a text run |
90
+ | `BLACK_STAR` | `U+2605` | ★ | Filled star |
91
+ | `WHITE_STAR` | `U+2606` | ☆ | Outlined star |
92
+
93
+ ## 3. RokuUI-Regular symbols (no backstop under a custom font)
94
+
95
+ Part of the default UI font, so they need no fallback under a system font. Under a custom app font
96
+ there is no RokuFont01 backstop for them either. **These are the ten constants to audit if your
97
+ app ships its own typeface.**
98
+
99
+ | Constant | Codepoint | Char | Description |
100
+ |---|---|---|---|
101
+ | `BULLET` | `U+2022` | • | Bullet |
102
+ | `ELLIPSIS` | `U+2026` | … | Horizontal ellipsis |
103
+ | `EN_DASH` | `U+2013` | – | En dash |
104
+ | `EM_DASH` | `U+2014` | — | Em dash |
105
+ | `LEFT_ARROW` | `U+2190` | ← | Leftwards arrow |
106
+ | `UP_ARROW` | `U+2191` | ↑ | Upwards arrow |
107
+ | `RIGHT_ARROW` | `U+2192` | → | Rightwards arrow |
108
+ | `DOWN_ARROW` | `U+2193` | ↓ | Downwards arrow |
109
+ | `CHECK_MARK` | `U+2713` | ✓ | Check mark |
110
+ | `BALLOT_X` | `U+2717` | ✗ | Ballot X |
111
+
112
+ ## What has no constant, on purpose
113
+
114
+ Characters you can type directly get none: `·` middle dot, `°` degree, currency signs, maths
115
+ operators, quotation marks. Type them. A constant would add a name to look up without adding
116
+ anything the compiler or the device cares about.
117
+
118
+ Geometric arrows `◀` and `▶` are in **neither** font and have no constants because they do not
119
+ render. Reach for `LEFT_ARROW` / `RIGHT_ARROW`, or the bordered `LEFT_BUTTON_GLYPH` /
120
+ `RIGHT_BUTTON_GLYPH` when you want the remote-button look.
121
+
122
+ ## Firmware placeholders
123
+
124
+ The `{CONSTANT_NAME}` column above is the firmware's own placeholder syntax, used in translated
125
+ firmware strings and resolved by the firmware's string layer. It is not a TypeScript symbol and it
126
+ is not template syntax: putting `{BACK_BUTTON_GLYPH}` in a label renders those braces literally.
127
+ Use the placeholder form only where you are explicitly producing text for something that resolves
128
+ firmware placeholders, and interpolate the imported constant everywhere else.
@@ -0,0 +1,227 @@
1
+ # Logging and Errors
2
+
3
+ ## Overview
4
+
5
+ An RSG SDK TypeScript app reports what it is doing through one exported `logger`, and survives a
6
+ throw through Solid's `ErrorBoundary`. Both have a platform edge that catches people out: log
7
+ levels are enforced in **two** places, one of which deletes your call before the app ever runs,
8
+ and the SDK already wraps every screen in a boundary whose fallback draws nothing — so the
9
+ default shape of a failure here is a blank screen rather than a crash.
10
+
11
+ ## When to Use
12
+
13
+ - a log level raised and nothing new appears
14
+ - log levels configured somewhere that turns out to have no effect
15
+ - `console` calls present while developing and gone from the build handed over
16
+ - a screen that comes up blank with no crash and no stack
17
+ - containing a throw so one bad screen does not take the app with it, or recovering afterwards
18
+ - an uncaught-exception handler that never fires, or fires and then behaves unexpectedly
19
+ - a crash stack that cannot be traced back to the TypeScript that produced it
20
+
21
+ ### When not to use
22
+
23
+ | Situation | Use instead | Because |
24
+ |---|---|---|
25
+ | Getting logs **off** a device from a shell script or a CI job | The `cli-dev-loop` skill | This skill is about a line being emitted; collecting it afterwards is a different surface |
26
+ | Getting logs off a device, or decoding a stack, inside an agent session | The `mcp-ai-dev-loop` skill | Same distinction, reached through tool calls rather than a shell |
27
+ | What the running app costs in frames, memory or startup time | The `app-performance` skill | Instrumenting for cost is measurement, not reporting |
28
+ | Which Solid primitives `@roku-sdk/solid` exports at all | The `solid-on-roku` skill | This skill owns the failure, not the export surface |
29
+
30
+ ## Read the reference first for this task
31
+
32
+ | If the request is | Read now, before answering |
33
+ |---|---|
34
+ | Writing or debugging a `setUncaughtExceptionHandler` callback | `references/uncaught-exceptions.md` |
35
+
36
+ The tables below say that the handler exists, what it never sees, and that registering one cannot
37
+ be undone. Its timeout, its inverted `done` flag and the conditions under which it silently does
38
+ nothing are only in the reference, and a handler written from the summary alone is wrong in ways
39
+ that do not show up until something throws in production.
40
+
41
+ ## Quick Reference
42
+
43
+ | Question | Answer |
44
+ |---|---|
45
+ | Get a logger | `import { logger } from "@roku-sdk/rsg-ts"` — the root one; `logger.getLogger("myscreen")` for a child |
46
+ | Levels available | `trace`, `debug`, `info`, `warn`, `error`. There is no `fatal`, and `createLogger` is not exported |
47
+ | Why a message is a function | `logger.info(() => \`...\`)` — the thunk is only invoked once the level check passes, so a filtered line costs nothing to build |
48
+ | The two level knobs | `app.logger.baseLogLevel` **deletes** call sites at build time (default INFO in production builds); `app.logger.currentLogLevel` filters what remains at runtime (default ERROR; also enforced as a runtime backstop at the base level) |
49
+ | Which one to raise | Whichever is higher wins. Raising only `currentLogLevel` recovers nothing a lower `baseLogLevel` already deleted |
50
+ | Setting levels via `buildVariables` | A silent no-op — the build overwrites `__BASE_LOG_LEVEL__` and `__CURRENT_LOG_LEVEL__` itself |
51
+ | Contain a throw | `<ErrorBoundary fallback={…}>` around the subtree, from `@roku-sdk/solid` |
52
+ | Recover from one | `catchError` for a scoped throw, `resetErrorBoundaries` to retry every boundary |
53
+ | Blank screen, no crash, no stack | The SDK's own boundary caught a root-component throw. One `error` line under source `btstp` is the entire evidence |
54
+ | `console` in a shipped build | Dropped by `rk build`, kept by `rk dev`; `dropConsole: false` keeps them, and then `installConsolePolyfill()` restores the helpers |
55
+ | Trace a stack back to TypeScript | Call `enableExceptionTagging()` before the code that can throw. Inside `main()`, before the anchor node, covers throws after `main()`; a throw at module top level needs the call in the entry file's first import |
56
+ | An uncaught-exception handler | `setUncaughtExceptionHandler` — never sees an `ErrorBoundary` catch, sees nothing thrown before it registers, and registration is permanent |
57
+ | Where these live | `logger`, `main`, `enableExceptionTagging` and `setUncaughtExceptionHandler` all come from `@roku-sdk/rsg-ts`; `installConsolePolyfill` only from `@roku-sdk/native` |
58
+
59
+ ## Instructions
60
+
61
+ 1. **Log through the exported `logger`, and give each area a child.** `logger` is the root;
62
+ `logger.getLogger("playback")` tags that area's lines. `createLogger` is deliberately module
63
+ private, so a child logger is the only sanctioned way to get a distinct source.
64
+
65
+ 2. **Pass a thunk, not a string.** Every method takes `() => string`. The level check runs first,
66
+ so an interpolation inside the thunk is never paid for on a filtered line. Building the string
67
+ at the call site throws that away.
68
+
69
+ 3. **Understand which level does what, because only one of them can be undone.**
70
+ - `baseLogLevel` is a **build-time** cut. Any statement calling a method below it is replaced
71
+ with `void 0;` in the source before the bundler sees it, so the call is not in the artifact
72
+ at all. Nothing at runtime can bring it back.
73
+ - `currentLogLevel` is a **runtime** filter over whatever survived, resolved once at module
74
+ load.
75
+
76
+ So diagnose in that order: if a line is missing, ask whether it was deleted before asking
77
+ whether it was filtered. The defaults are `INFO` for the base cut and `ERROR` for the runtime
78
+ filter, which is why a `debug` line is absent from a default build twice over.
79
+
80
+ 4. **Know what the build-time cut checks and what it does not.** It matches a whole expression
81
+ statement whose call target is a log method (e.g. `.trace`, `.debug`) on a receiver that is
82
+ recognised as an SDK logger binding in that file. Specifically, the receiver must be:
83
+ - A named import of `logger` (or an aliased import) from one of the SDK logger modules
84
+ (`@roku-sdk/rsg-ts` or `@roku-sdk/common`), or common's own relative `./logger` import.
85
+ - A child logger derived from any of the above via `const X = logger.getLogger("area")`
86
+ (and any further `.getLogger(…)` chains at module level).
87
+ - An inline `logger.getLogger("a").debug(…)` call in statement position.
88
+
89
+ Non-SDK receivers (`someLibrary.debug(…)`, `console.debug(…)`, `myEmitter.trace(…)`) are
90
+ never removed. Shadowing is respected: when a local binding reuses the name — any parameter
91
+ (constructor, accessor and destructured ones included), a `catch` binding, a loop variable, or
92
+ a local declaration — calls through it are left untouched.
93
+
94
+ One documented limitation: a logger exported from one module and re-imported under a
95
+ *different* import path (not one of the recognised specifiers) is not recognised by the
96
+ per-file pass and is not stripped at build time. The runtime backstop (see below) still
97
+ silences it. The sanctioned pattern is: import `logger` from the SDK and derive children
98
+ with `getLogger` in the same module.
99
+
100
+ A second limitation: inside a function, only a child derived directly from a recognised SDK
101
+ logger is stripped. A grandchild built from a function-local child — `const a =
102
+ logger.getLogger("a"); const b = a.getLogger("b"); b.debug(…)`, or inline
103
+ `a.getLogger("b").debug(…)` — is kept at build time. Like the case above, this only ever
104
+ leaves a call in (never removes a non-SDK one), and the runtime backstop still silences it.
105
+ Derive nested children at module level if you want them stripped.
106
+
107
+ Child loggers must be declared with `const`. A `let` or `var` child
108
+ (`let l = logger.getLogger("x")`) can be reassigned to some other object, so its calls are
109
+ kept at build time rather than risk deleting app code. The runtime backstop still silences them.
110
+
111
+ Only a bare statement is stripped. `const line = logger.debug(…)`, or a call guarded by
112
+ `&&`, survives — so a log statement written in an unusual shape will not be stripped as
113
+ expected.
114
+
115
+ 5. **Set levels in `app.logger`, never in `buildVariables`.** The two keys the logger reads are
116
+ `__BASE_LOG_LEVEL__` and `__CURRENT_LOG_LEVEL__`, and the build writes both itself —
117
+ unconditionally, after copying your `buildVariables` — then restores them only from
118
+ `app.logger`. So a value you put in `buildVariables` under either name is overwritten with an
119
+ empty string, which the reader treats exactly like an unset variable. You get the defaults,
120
+ with no warning that your setting was discarded.
121
+
122
+ 6. **Wrap a subtree that can throw in `ErrorBoundary`, and draw something in the fallback.** The
123
+ SDK already puts one boundary around the whole app, and its fallback returns nothing — so an
124
+ uncontained throw in a root component is a blank screen rather than a crash. That is the SDK
125
+ working as designed, not a missing feature. Your own boundary, closer to the risk, is what
126
+ turns it into a message the user can act on.
127
+
128
+ 7. **Recover with `catchError` or `resetErrorBoundaries`.** `catchError` handles a throw inside
129
+ one computation without unmounting a subtree; `resetErrorBoundaries` re-runs every boundary's
130
+ children, which is the "try again" button's implementation.
131
+
132
+ 8. **Expect `console` to disappear from the build you hand over.** `rk build` drops `console.*`
133
+ and `debugger`; `rk dev` keeps them. Set `dropConsole: false` when a build must keep them
134
+ on purpose — and note the helper methods (`count`, `group`, `assert` and the timing pair) come
135
+ from a polyfill that only auto-installs in a development build, so a production build that
136
+ keeps `console` also needs `installConsolePolyfill()` from `@roku-sdk/native`. Plain
137
+ `console.log`, `warn` and `error` are the runtime's own and are never wrapped. None of this
138
+ replaces `logger`: `console` has no levels, no sources and no build-time cut.
139
+
140
+ 9. **Call `enableExceptionTagging()` before the code that can throw**, when traces from this
141
+ build have to be resolvable later. Inside `main()`, before registering the anchor node, covers
142
+ throws from the `main()` callback onward. A throw while the bundle is still being evaluated
143
+ needs the call in the same module the entry file imports first (step 10). It marks each trace
144
+ with the build's source-map identity. It is idempotent, and calling it costs nothing at runtime
145
+ beyond the tagging itself. Turning a captured stack back into TypeScript afterwards is
146
+ `cli-dev-loop` from a shell and `mcp-ai-dev-loop` from a session — this step is only the app's half.
147
+
148
+ 10. **Reach for `setUncaughtExceptionHandler` only for what a boundary cannot catch**, and read
149
+ the reference before writing one. It replaces the SDK's default permanently, it never sees a
150
+ throw an `ErrorBoundary` already caught, and it is not the mechanism behind the blank screen
151
+ in step 6. It also misses anything thrown before it registers, and every module's top-level
152
+ code runs before `main()` is called. When a module can throw at its top level, register in
153
+ the entry file's first import rather than inside `main()`.
154
+
155
+ ## Render-thread transaction errors
156
+
157
+ On firmware with `roku.Transaction.setErrorCallback`, SDK `main()` installs a default
158
+ error-level logger under source `tx` before declaring app components. It reports each
159
+ render-thread transaction error with its reason, message and available operation, node ID
160
+ and property name. These asynchronous errors do not throw through `ErrorBoundary` or the
161
+ uncaught-exception handler. Apps calling `roku.Transaction.setErrorCallback` themselves
162
+ replace this default; accessors are valid only during that callback. Older firmware without
163
+ the callback API is unchanged and cannot surface these errors through this SDK handler.
164
+
165
+ ## Example
166
+
167
+ An app entry point that tags its traces, and a screen that contains its own failures instead of
168
+ letting the SDK's blank fallback have them.
169
+
170
+ ```tsx rsg-skill=complete
171
+ import { Label, LayoutGroup } from "@roku-sdk/components";
172
+ import { type AnchorNodeImplementation, defineAnchorNode, FieldType, logger } from "@roku-sdk/rsg-ts";
173
+ import { ErrorBoundary } from "@roku-sdk/solid";
174
+
175
+ const screenLog = logger.getLogger("playback");
176
+
177
+ const PlaybackScreenDescription = {
178
+ componentName: "PlaybackScreen",
179
+ extends: "Group" as const,
180
+ inputFields: {
181
+ assetId: FieldType.String("")
182
+ },
183
+ outputFields: {}
184
+ };
185
+
186
+ function NowPlaying(props: { assetId: string }) {
187
+ // The thunk is only invoked once the level check passes.
188
+ screenLog.info(() => `rendering asset ${props.assetId}`);
189
+ return <label text={props.assetId} width={800} height={48} color="0xFFFFFFFF" />;
190
+ }
191
+
192
+ const PlaybackScreen: AnchorNodeImplementation<typeof PlaybackScreenDescription> = inputs => (
193
+ <layoutGroup layoutDirection="vert">
194
+ <ErrorBoundary
195
+ fallback={error => {
196
+ // Without a fallback that draws, this subtree is simply absent.
197
+ screenLog.error(() => `playback failed: ${error.message}`);
198
+ return <label text="Playback unavailable" width={800} height={48} color="0xFF6666FF" />;
199
+ }}
200
+ >
201
+ <NowPlaying assetId={inputs.assetId ?? ""} />
202
+ </ErrorBoundary>
203
+ </layoutGroup>
204
+ );
205
+
206
+ export default defineAnchorNode(PlaybackScreenDescription, PlaybackScreen);
207
+ ```
208
+
209
+ ## Common Mistakes
210
+
211
+ | Symptom | Cause | Fix |
212
+ |---|---|---|
213
+ | A level was raised, the app rebuilt, and the lines are still missing | `baseLogLevel` deleted the call sites; raising the runtime filter cannot restore a call that is not in the artifact | Raise `app.logger.baseLogLevel` and rebuild |
214
+ | Levels set through `buildVariables` change nothing, and nothing warns | The build overwrites `__BASE_LOG_LEVEL__` and `__CURRENT_LOG_LEVEL__` with an empty string before restoring them from `app.logger` | Set them in `app.logger` |
215
+ | A screen is blank, the app is still running, and there is no stack | A root component threw and the SDK's own boundary caught it; its fallback draws nothing | Search the log for source `btstp`; add your own `ErrorBoundary` closer to the risk |
216
+ | An `ErrorBoundary` was added and the screen is still blank | The `fallback` returns nothing, or the boundary sits above the component that throws rather than around it | Return a real node from `fallback`; put the boundary around the risky subtree |
217
+ | An uncaught-exception handler never fires for a render-time throw | A boundary caught it first, and the handler only sees what nothing caught | Handle it in the boundary; keep the handler for what escapes |
218
+ | The app exits from the splash screen, and the handler never fires | A module threw at its top level while the bundle was evaluated, before the handler was registered inside `main()` | Register in a module the entry file imports first, as a bare `import "./…"` |
219
+ | A registered handler cannot be swapped back to the SDK default | Registration replaces the default permanently, by design | Register once, at startup, and branch inside your own handler |
220
+ | A handler was registered and does nothing at all, silently | `globalThis.Process` was unavailable, so the call warned and returned — and at the default runtime filter that warning is itself invisible | Read the reference; confirm the handler registers where the runtime provides it |
221
+ | Diagnostics print in development and are gone from the shipped build | `rk build` drops `console.*` by default | Use `logger`, or set `dropConsole: false` deliberately |
222
+ | `console` timing and grouping helpers are missing in a build that kept `console` | The polyfill auto-installs only in a development build | Call `installConsolePolyfill()` from `@roku-sdk/native` |
223
+ | A captured stack cannot be matched to a build | The app never tagged its traces, or tagged them inside `main()` after a module had already thrown | Call `enableExceptionTagging()` before that code. Inside `main()` covers throws after it; a module top-level throw needs the call in the entry file's first import |
224
+
225
+ `Logger`, `setUncaughtExceptionHandler` and `ErrorBoundary` are typed with TSDoc in the installed
226
+ packages. Read an option there rather than guessing its name, and prefer it over any prose,
227
+ including this file, when the two disagree.
@@ -0,0 +1,137 @@
1
+ # Uncaught exceptions
2
+
3
+ `setUncaughtExceptionHandler` is the only hook for a throw that nothing in the app caught. It is a
4
+ narrow API with four behaviours that are not guessable from its signature, each of which produces
5
+ a handler that looks correct and is not.
6
+
7
+ ## What it does and does not see
8
+
9
+ It is registered against the runtime's uncaught-exception capture callback. That means:
10
+
11
+ - **Nothing thrown before the call reaches it.** Until `setUncaughtExceptionHandler` runs, only
12
+ the SDK's default handles a throw: it logs the error and `source_map_sha`, then the app exits.
13
+ The whole bundle is evaluated before `main()` is called. Each module's top-level code runs in
14
+ import order, and a module's imports run before its own body. So a handler registered inside
15
+ `main()` is too late for a module whose top level throws, such as a constant initialized from
16
+ a configuration read that fails. To cover that, register in a module of its own, and make it
17
+ the entry file's first import: `import "./install-exception-reporting";`. Keep it a bare
18
+ side-effect import. Import sorting keeps side-effect imports first and in the order written,
19
+ but sorts named imports alphabetically, which can move the handler below the module that
20
+ throws.
21
+ - **An `ErrorBoundary` catch never reaches it.** A boundary handles the throw, so as far as the
22
+ runtime is concerned nothing escaped. Since the SDK wraps the whole app in a boundary already,
23
+ a render-time throw is caught before this handler is a possibility. If you want a monitoring
24
+ service to hear about render failures, report them from the boundary's `fallback`.
25
+ - **Unhandled promise rejections never reach it.** They go to a separate runtime callback, which
26
+ the SDK sets to log the rejection at `error` and stop there. `setUncaughtExceptionHandler` does
27
+ not forward rejections. To act on one, register your own callback with
28
+ `globalThis.Process.setUnhandledRejectionCallback`, which replaces the SDK's logging.
29
+
30
+ So the handler's actual scope is small. It covers a throw that happens after registration and
31
+ outside any boundary and the reactive render path: module-level work in modules evaluated after
32
+ it, the `main()` callback, a timer, a callback the SDK invoked directly, a listener.
33
+
34
+ ## The handler's contract
35
+
36
+ ```ts
37
+ setUncaughtExceptionHandler(
38
+ async error => {
39
+ // error: Error. The only argument; the returned Promise is awaited.
40
+ },
41
+ { timeout: 5000, resume: false }
42
+ );
43
+ ```
44
+
45
+ | Element | Behaviour |
46
+ |---|---|
47
+ | `error` | The only argument. Never null: if the runtime hands over a falsy error, the SDK synthesizes one with the message `Uncaught exception (no error object provided)`, so reading `.message` or `.stack` is always safe. With `enableExceptionTagging()` active, `error.stack` ends in `[SourceMap: <sha>]`, the build's source-map identity. Send the stack as is, or it cannot be resolved later |
48
+ | Return value | A Promise. The SDK awaits it for up to `timeout`, then tells the runtime to exit or resume |
49
+ | `timeout` | Milliseconds the returned Promise is awaited for. Default **5000**, floor **100** — a smaller value is raised to the floor rather than rejected |
50
+ | `resume` | Chooses what happens after. Default `false` |
51
+
52
+ There is no `sha` argument and no `done` argument. A handler written as `(error, sha, done)` does
53
+ not typecheck. The SDK logs the error message and `source_map_sha` itself, before your handler
54
+ runs, and registering a handler does not suppress that line. The SDK also calls the runtime's
55
+ `done` callback for you.
56
+
57
+ ## The `done` flag is inverted relative to `resume`
58
+
59
+ This is the single easiest thing to get backwards, because the two names read as synonyms and
60
+ mean opposites:
61
+
62
+ | You pass | The SDK calls | The app |
63
+ |---|---|---|
64
+ | nothing, or `resume: false` | `done(true)` | exits |
65
+ | `resume: true` | `done(false)` | resumes its event loop |
66
+
67
+ `done(true)` means "yes, we are finished — exit", not "yes, the handler succeeded". Passing
68
+ `resume: true` to keep an app alive through a background failure is legitimate, but understand
69
+ that the app then continues from a state something already failed in.
70
+
71
+ Both outcomes are unconditional: whether your handler resolves, rejects, or runs out its timeout,
72
+ `done` is invoked with the flag chosen by `resume`. There is no path in which a failing handler
73
+ changes the exit decision.
74
+
75
+ ## Registration is permanent
76
+
77
+ Calling `setUncaughtExceptionHandler` replaces the currently registered handler, including the
78
+ SDK's default. Only the last registration is active, and **there is no way to restore the SDK
79
+ default afterwards** — no unregister, no argument that means "back to normal".
80
+
81
+ The consequence for structure: register once, before anything can throw, and branch inside your own handler
82
+ rather than swapping handlers per screen. A test that registers one has no clean way to undo it,
83
+ which is why the module keeps an internal reset that is explicitly test-only and not exported for
84
+ application use.
85
+
86
+ ## It can silently do nothing
87
+
88
+ If `globalThis.Process` is unavailable when you call it, the function logs at `warn` and returns
89
+ without registering anything. Two things make that hard to notice:
90
+
91
+ - Nothing throws and nothing returns a failure — the call looks like it worked.
92
+ - The default runtime log filter is `ERROR`, so the `warn` line explaining what happened is
93
+ itself filtered out.
94
+
95
+ If a handler appears to be registered and never fires, distinguish this case from the
96
+ `ErrorBoundary` case by lowering `currentLogLevel` to `WARN` and looking for that line before
97
+ concluding the handler is wired up wrong.
98
+
99
+ ## Shape to copy
100
+
101
+ A module of its own, `src/install-exception-reporting.ts`, that registers as it is evaluated:
102
+
103
+ ```ts rsg-skill=complete
104
+ import { enableExceptionTagging, setUncaughtExceptionHandler } from "@roku-sdk/rsg-ts";
105
+
106
+ enableExceptionTagging();
107
+
108
+ setUncaughtExceptionHandler(
109
+ async error => {
110
+ await fetch("https://monitoring.example.com/exceptions", {
111
+ method: "POST",
112
+ headers: { "Content-Type": "application/json" },
113
+ body: JSON.stringify({ name: error.name, message: error.message, stack: error.stack })
114
+ });
115
+ },
116
+ { timeout: 2000 }
117
+ );
118
+ ```
119
+
120
+ The entry file imports it before anything else:
121
+
122
+ ```ts
123
+ import "./install-exception-reporting";
124
+
125
+ import { main, registerAnchorNode } from "@roku-sdk/rsg-ts";
126
+
127
+ import MyApp from "./my-app";
128
+
129
+ main(() => registerAnchorNode(MyApp));
130
+ ```
131
+
132
+ `enableExceptionTagging()` comes first, so a trace captured by the handler is already tagged.
133
+ The handler has nothing to call when it is done: the SDK finishes once the Promise settles or
134
+ 2000 ms elapse, whichever comes first. Omitting `resume` means the app exits afterwards, which
135
+ is what you want unless you have a specific reason to keep running. It is also the only sensible
136
+ choice for a throw during boot, because there is nothing to resume into: the throwing module
137
+ never finished, and `main()` never registered the app.