@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.
- package/LICENSE +21 -0
- package/README.md +88 -2
- package/package.json +47 -3
- package/skills/ads-and-raf/SKILL.md +231 -0
- package/skills/ads-and-raf/references/integration-flows.md +332 -0
- package/skills/app-packaging/SKILL.md +107 -0
- package/skills/app-performance/SKILL.md +133 -0
- package/skills/cli-dev-loop/SKILL.md +217 -0
- package/skills/cli-dev-loop/references/logs-and-crashes.md +114 -0
- package/skills/cpu-and-heap-profiling/SKILL.md +108 -0
- package/skills/cpu-and-heap-profiling/references/reading-profiles.md +52 -0
- package/skills/data-entry-keyboards/SKILL.md +160 -0
- package/skills/device-management/SKILL.md +158 -0
- package/skills/dialogs-and-modals/SKILL.md +210 -0
- package/skills/effects/SKILL.md +181 -0
- package/skills/fetch-and-data-loading/SKILL.md +164 -0
- package/skills/fetch-and-data-loading/references/https-and-certificates.md +79 -0
- package/skills/flex-grid/SKILL.md +236 -0
- package/skills/flex-grid/references/focus-integration.md +355 -0
- package/skills/flex-grid/references/plugins.md +201 -0
- package/skills/focus-management/SKILL.md +205 -0
- package/skills/focus-management/references/focus-recipes.md +282 -0
- package/skills/fonts-and-glyphs/SKILL.md +176 -0
- package/skills/fonts-and-glyphs/references/glyph-tables.md +128 -0
- package/skills/logging-and-errors/SKILL.md +227 -0
- package/skills/logging-and-errors/references/uncaught-exceptions.md +137 -0
- package/skills/mcp-ai-dev-loop/SKILL.md +189 -0
- package/skills/mcp-ai-dev-loop/references/crash-and-console.md +68 -0
- package/skills/mcp-ai-dev-loop/references/live-debugging.md +129 -0
- package/skills/mcp-setup/SKILL.md +131 -0
- package/skills/project-setup/SKILL.md +210 -0
- package/skills/project-setup/references/roku-config.md +120 -0
- package/skills/screen-navigation/SKILL.md +166 -0
- package/skills/sdk-components/SKILL.md +213 -0
- package/skills/sdk-components/references/dialogs.md +148 -0
- package/skills/sdk-components/references/scenegraph-node-spelling.md +123 -0
- package/skills/sdk-installation/SKILL.md +129 -0
- package/skills/slideshow/SKILL.md +157 -0
- package/skills/slideshow/references/focus-and-handoff.md +187 -0
- package/skills/solid-on-roku/SKILL.md +181 -0
- package/skills/solid-on-roku/references/hermes-and-exports.md +130 -0
- package/skills/testing/SKILL.md +147 -0
- package/skills/testing/references/async-and-timing.md +97 -0
- package/skills/user-accounts-and-purchases/SKILL.md +164 -0
- package/skills-manifest.json +316 -0
- package/skills-manifest.schema.json +62 -0
- package/src/__tests__/manifest.test.ts +267 -0
- package/src/__tests__/snippets.test.ts +221 -0
- package/src/__tests__/trigger-routing.test.ts +77 -0
- package/src/index.ts +36 -0
- package/src/manifest.ts +215 -0
- package/src/router.ts +147 -0
- 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.
|