Mapping runtime errors to TypeScript source code

The rk decode-stack command maps a raw Hermes or JavaScript stack trace to file, line, and column positions in your TypeScript source code. When a runtime error occurs on a Roku device, the address that the device reports (for example, index.hbc:1:46343 or index.js:3948:28) refers to a position inside compiled or minified output, not your original TypeScript code.

To decode a trace, you pass it to the command as a file, as piped input, as an argument, or by pasting it. The command finds the matching source map in your build output and prints the TypeScript position of each frame.

Generating source maps

Every build generates source maps automatically, and they do not require any additional configuration. The following table lists the source maps that each build command creates.

Build commandSource maps generated
npx rk dev --compile-tsdist-dev/bundle/source/compiled/index.js.map (ESBuild) +
dist-dev/bundle/source/compiled/index.hbc.map (Hermes) +
dist-dev/bundle/source/compiled/<sha>.source.map (composed universal map); SHA embedded in manifest as hbc_source_map_sha
npx rk devdist-dev/bundle/source/compiled/index.js.map only (no Hermes bytecode); SHA of the JavaScript source map embedded in manifest as hbc_source_map_sha
npx rk build --compile-tsdist-build/bundle/source/compiled/index.js.map +
dist-build/bundle/source/compiled/index.hbc.map +
dist-build/bundle/source/compiled/<sha>.source.map; SHA embedded in manifest as hbc_source_map_sha

The <sha>.source.map file is a universal source map. It is a single file that chains the Hermes bytecode and ESBuild TypeScript maps together. As a result, a debugger or crash-reporting tool can jump directly from a bytecode offset to a TypeScript line without any extra tooling steps. The SHA-256 name uniquely identifies the build that produced the file; therefore, you can archive it safely alongside release builds.

Identifying the source map from device logs

The device log can carry the SHA of the source map that matches the running build. This section describes how to tag stack traces with the SHA and how to find the matching map in your bundle directory.

Enabling exception tagging

You can call the enableExceptionTagging() function from within your main() callback to tag every Error.stack string with the source map SHA:

import { main, enableExceptionTagging } from "@roku-sdk/rsg-ts";

main(() => {
    enableExceptionTagging();
    // Rest of app initialization
});

The function installs either Error.prepareStackTrace (when the runtime provides full CallSite column information) or a fallback Error.prototype.stack getter override. When you use setUncaughtExceptionHandler(), the handler ensures that the tag is present on error.stack before it passes the error to your callback, even on runtimes where neither hook takes effect (for example, Hermes bytecode builds where CallSite column information or prototype getter overrides are unavailable).

Every logged stack trace ends with the following tag:

Error: Cannot read property 'x' of null
    at handleFocus (index.hbc:1:46343)
    at onFocusChange (index.hbc:1:51200)
    [SourceMap: 4a7f3c8e...]

This tag appears in any log line that captures a full stack trace, not only uncaught exceptions. As a result, crash-reporting pipelines and developers have a direct link from a raw log entry to the correct universal source map. Calling the function more than once has the same effect as calling it once.

For uncaught exceptions, the SDK additionally logs the SHA on a dedicated line immediately before the process exits. This happens automatically, independent of enableExceptionTagging(). The line has the following format:

source_map_sha=4a7f3c8e... (64 hex chars)

The SHA identifies the source map stored locally:

  • For bytecode builds (--compile-ts), the SHA identifies the file dist-dev/bundle/source/compiled/<sha>.source.map or dist-build/bundle/source/compiled/<sha>.source.map.
  • For JavaScript builds (plain rk dev, rk dev -d, or rk build --debug), the SHA is the SHA of index.js.map in the corresponding bundle/source/compiled/ directory. A default rk build package does not have a SHA yet; the store's compiler writes it when it produces the bytecode.

You can then locate the matching map in your bundle directory. You can also pipe the stack trace directly into rk decode-stack, which resolves the correct map automatically. See Decoding a stack trace for more information.

Getting an error address from the device

The format of the error address depends on whether the build is a bytecode build or a JavaScript build.

Bytecode build (npx rk dev --compile-ts / npx rk build --compile-ts)

The Roku telemetry log reports Hermes bytecode (HBC) positions. Frames use the address at prefix inside parentheses, as in the following example:

TypeError: Cannot read property 'property' of null
    at function-name-stripped (address at /source/compiled/index.hbc:1:46998)
    [SourceMap: aee878f1...]

In older firmware, the device reports the error address as a bare crash line in <file>:<line>:<column> format:

JS crash: index.hbc:1:46343

Hermes bytecode frames use line 1 for the entire bundle; the column is a byte offset. The rk decode-stack command recognizes both the address at and bare formats and extracts the function name when present.

JavaScript build (npx rk dev / npx rk dev -d)

The plain rk dev command runs unminified JavaScript directly. The -d option adds the debug preset (debugger probe stubs, config overrides) and does not change the build from bytecode to JavaScript. Error addresses use an absolute device path and include the function name when available:

TypeError: Cannot read property 'host' of null
    at deepPropertyAccess (/source/compiled/index.js:4027:38)
    at /source/compiled/index.js:4087:35
    [SourceMap: 7832f4ad...]

The prefix /source/compiled/ is the on-device location of the deployed JavaScript bundle. The rk decode-stack command strips the path prefix and matches the frame against the local index.js.map automatically.

Decoding a stack trace

The rk decode-stack command translates a raw Hermes or JavaScript stack trace to TypeScript source positions in one step. You must run it from your app root directory (the folder containing roku-config.ts). The command automatically locates the most recent dist-dev/bundle or dist-build/bundle directory and picks the correct maps.

CLI reference

See rk decode-stack for more information about the command arguments and options.

Choosing an input method

The rk decode-stack command accepts stack trace input from multiple sources. The command checks the sources in the following order:

  1. The --file option reads the entire file at the given path.
  2. When the command detects piped or redirected input (for example, when process.stdin.isTTY is false), it reads stdin automatically.
  3. When you pass positional arguments, the command joins them with newlines and treats the result as inline stack trace text.
  4. When none of the above apply (a terminal with no arguments), the command uses interactive paste. The command prompts you to paste a stack trace and waits for the end-of-file signal (Ctrl+D on macOS/Linux, Ctrl+Z then Enter on Windows).

Using interactive paste

To decode a stack trace interactively, run the rk decode-stack command, paste the trace into the terminal, and press Ctrl+D (Ctrl+Z, Enter on Windows):

npx rk decode-stack
# Paste your stack trace, then press Ctrl+D to decode:
#   at handleFocus (index.hbc:1:46343)
#   at onFocusChange (index.hbc:1:51200)
# ^D

Interactive mode times out after 5 minutes by default. You can use the --timeout option to override this value in milliseconds.

Reading from a file

You can pass the path of a log file to the --file (or -f) option:

npx rk decode-stack --file error.log
npx rk decode-stack -f crash-report.txt

Piping from clipboard or log file

You can pipe a stack trace from the clipboard or from a log file into the command:

# macOS: copy a stack trace, then decode from clipboard
pbpaste | npx rk decode-stack

# Pipe from a log file
cat production.log | npx rk decode-stack

Decoding a live device stream

You can use the --stream option to process a live telnet log line by line. The command passes non-frame lines through unchanged, and it decodes stack frames and annotates them inline as they arrive:

telnet <ROKU_IP> 8085 | npx rk decode-stack --stream
📘

Important

You cannot use the --stream and --file options together. Stream mode reads from stdin only.

Without the --stream option, the decode-stack command buffers all stdin before processing. If data does not arrive within the timeout (the default is 30 seconds, and you can change it with the --timeout option), the command times out and prints a message. For a long-running telnet session, you can either use the --stream option or capture the output to a file first:

# 1. Record device output (Ctrl+C when you have the crash)
telnet <ROKU_IP> 8085 > device.log

# 2. Decode the captured log
npx rk decode-stack --file device.log

Redirecting a file to stdin

You can redirect a file to the command's stdin:

npx rk decode-stack < error.log

Using heredoc for multi-line traces

You can pass a multi-line trace as a heredoc (a block of text that the shell sends to the command as stdin):

npx rk decode-stack << 'EOF'
  at handleFocus (index.hbc:1:46343)
  at onFocusChange (index.hbc:1:51200)
EOF

Passing an inline argument

You can pass the trace as a quoted argument:

npx rk decode-stack "  at render (index.hbc:1:46343)"

Resolving the bundle

By default, the decode-stack command scans dist-build/bundle and dist-dev/bundle under the current working directory and picks whichever bundle has the newer source maps. You can use the following options to override this default behavior:

  • The --build-dir <dir> option points to an explicit bundle directory (relative or absolute). This option is useful when maps live outside the standard locations.

    npx rk decode-stack --build-dir /archive/builds/v2.1.0/bundle -f crash.log
  • The --sha <sha> option looks for maps in dist-build/<sha>/bundle/. The value must be a hexadecimal string of at least 8 characters. If the directory does not exist, the command silently falls back to auto-detection (whichever of dist-build/bundle or dist-dev/bundle is newer) without printing a warning. This means a mistyped or incorrect SHA can cause the tool to decode against the wrong source maps; therefore, you must always verify that the SHA matches the build you intend to decode.

    npx rk decode-stack --sha 4a7f3c8e... -f crash.log
  • When you use the --build-dir and --sha options together, the command looks in <build-dir>/<sha>/bundle/ first. If that directory does not exist, it falls back to <build-dir> directly and prints a warning to stderr. This is useful for archived builds organized by SHA inside a common root.

    npx rk decode-stack --build-dir /archive/builds --sha 4a7f3c8e -f crash.log
    # resolves: /archive/builds/4a7f3c8e/bundle/ → fallback (with warning) → /archive/builds/

Detecting the SHA automatically

In non-stream mode, the decode-stack command reads the entire input before resolving source maps. If you do not provide the --sha option, the command scans the input for source_map_sha=<hex> or [SourceMap: <hex>] patterns and uses the extracted SHA to locate the correct build artifacts automatically. As a result, you can pipe a raw Roku log that contains the SHA tag, and the command resolves the correct source map without extra options:

# SHA is embedded in the stack trace; the command detects it automatically
pbpaste | npx rk decode-stack

An explicit --sha value always takes precedence over auto-detection. Per-trace SHA auto-detection is not available in --stream mode because the command processes input line by line and must resolve the bundle before a trace's [SourceMap: ...] tag (which trails its frames) arrives.

However, --stream mode does track the local build on disk. Whenever the source maps under the resolved bundle directory change (for example, after you rebuild and redeploy with npx rk dev or npx rk dev -d), the command reloads the maps automatically and prints an Info: local build changed — reloaded source maps for stream decoding. line to stderr. You do not need to restart the stream after a redeploy. Passing an explicit --sha option locks decoding to that build and disables this automatic reloading; therefore, the stream always decodes against that one build.

Detecting frame types automatically

The decode-stack command detects .hbc and .js frames automatically. The command matches only frames that reference index.hbc or index.js (with or without a path prefix like /source/compiled/) and ignores other .js filenames in log output to avoid false positives.

  • The command decodes Hermes bytecode (HBC) frames (builds with --compile-ts enabled) with the universal source map when available; otherwise, it falls back to the two-step lookup from Hermes bytecode to JavaScript to TypeScript.
  • The command decodes JavaScript frames directly with index.js.map. JavaScript is the default output for both development and release builds; --compile-ts is off by default.

Choosing an output format

The decode-stack command supports three output formats that you can select with the --format <type> option. The following table lists the formats.

FormatOptionDescription
pretty--format pretty(Default) Colored, human-readable output with ANSI escape codes
plain--format plainSame layout as pretty, but without any ANSI colors
json--format json
--json
Machine-readable JSON for scripting and piping

Using the pretty format (default)

The default pretty format prints each frame with the original trace line dimmed, followed by the resolved TypeScript location and surrounding source context. ANSI colors highlight function names, source paths, and the crash line:

  at handleFocus (index.hbc:1:46343)
  → handleFocus  src/components/my-panel.tsx:47:4

       44 | const handleFocus = () => {
       45 |     const node = inputs.focusedItem;
       46 |     if (!node) return;
  >    47 |     node.setFocus(true);
       48 |     outputs.onFocusChanged(node.id);

You can use the --context option to control how many lines of source the command shows around the crash site. You can set it to 0 to hide the context completely:

npx rk decode-stack --context 0 -f error.log
npx rk decode-stack -c 5 -f error.log

Using the plain format

The --format plain option produces the same layout as pretty (it includes the original frame, resolved location arrow, and source context) but strips all ANSI color codes. This format is useful for writing output to a file, embedding in a ticket, or viewing in a tool that does not render ANSI escapes:

npx rk decode-stack --format plain --file error.log
npx rk decode-stack --format plain < error.log > decoded.txt

The output looks identical to pretty but without color:

  at handleFocus (index.hbc:1:46343)
  → handleFocus  src/components/my-panel.tsx:47:4

       44 | const handleFocus = () => {
       45 |     const node = inputs.focusedItem;
       46 |     if (!node) return;
  >    47 |     node.setFocus(true);
       48 |     outputs.onFocusChanged(node.id);

Plain mode also works with the --stream option for live log decoding without color:

telnet <ROKU_IP> 8085 | npx rk decode-stack --stream --format plain

Using the JSON format

You can use the --json (or --format json) option for machine-readable output suitable for piping into other tools:

npx rk decode-stack --json < error.log
npx rk decode-stack --json -f crash.log | jq '.[] | .source'

In batch mode (without the --stream option), the JSON output is an array of objects, with one object per frame:

[
  {
    "original": "  at handleFocus (index.hbc:1:46343)",
    "function": "handleFocus",
    "source": "src/components/my-panel.tsx",
    "line": 47,
    "column": 4,
    "name": "handleFocus",
    "error": null
  }
]

In streaming mode (--stream --json), the command emits each decoded frame as a single-line JSON object (newline-delimited JSON, or NDJSON) for simple real-time processing:

telnet <ROKU_IP> 8085 | npx rk decode-stack --stream --json | jq '.source'

When a frame cannot be resolved, the source, line, and column fields are all null and the error field contains a diagnostic message.

Disabling colors

You can use the --format plain option to get uncolored output:

npx rk decode-stack --format plain -f error.log

Writing output to a file

You can use the --output (or -o) option to write the decoded result to a file instead of stdout. This is useful for archiving decoded crash reports or embedding them in tickets:

npx rk decode-stack --file crash.log --output decoded.txt
npx rk decode-stack --file crash.log --output report.json --json

When you use the --output option, the format automatically defaults to plain (no ANSI colors) unless you explicitly override it with the --format or --json options.

The --output option also works in --stream mode. The command writes the decoded output to the file and captures non-frame lines and decoded frames as they arrive:

telnet <ROKU_IP> 8085 | npx rk decode-stack --stream --output session.log

Adjusting the stdin timeout

By default, the decode-stack command waits up to 30 seconds for stdin data before timing out. For slow pipes or very large log files, you should increase the timeout with the --timeout (or -t) option (value in milliseconds). This option does not have any effect when you use --file or --stream.

npx rk decode-stack --timeout 60000 < very-large-log.txt

Understanding source map accuracy

The toolchain produces source maps with per-statement precision. The reported TypeScript line corresponds to the exact statement that crashed, not an adjacent one.

The SDK intentionally disables ESBuild's minifySyntax optimization. This pass performs constant folding (evaluating constant expressions at build time) and statement merging, which collapses multiple consecutive statements into a single JavaScript expression. The pass then attributes the merged span to the position of the first statement, which causes the source map to be off by one or more lines. The following example demonstrates what occurs without this safeguard:

// TypeScript: two statements (lines 54 and 55)
const obj: any = null;
console.log(obj.property);  // ← actual crash

// After minifySyntax: merged into one, mapped back to line 54 only
console.log(null.property)

Whitespace compression and identifier shortening (minifyWhitespace, minifyIdentifiers) remain enabled because they do not alter statement boundaries and account for the bulk of the bundle size reduction.

📘

Note

Builds produced before this fix may point one or two lines earlier than the actual crash. You should rebuild the app to get accurate mappings.

Understanding source map storage

The following tree shows where the build writes source maps in your app directory.

my-app/
├── dist-dev/
│   └── bundle/
│       └── source/compiled/
│           ├── index.js       ESBuild output (unminified JS, deployed to device)
│           └── index.js.map   ESBuild map: JS → TypeScript
└── dist-build/
    └── bundle/
        └── ...                Same JS/map structure; JavaScript is minified by default

With --compile-ts, bundle/source/compiled/ also contains index.hbc, index.hbc.map and <sha>.source.map (the composed bytecode → TypeScript map). The build copies only index.js (or index.hbc with --compile-ts) to the device (pkg-dir/source/compiled/). With rk dev, all map files stay local in the bundle/ directory and rk dev never sends them to the device. A JavaScript rk build package also carries source/compiled/index.js.map. See Keeping source maps off the device for more information.

Multi-bundle decoding (DCL)

When a host app loads a Dynamic Component Library (DCL), stack traces can contain frames from both the host bundle (index.hbc) and the DCL bundle (for example, tsDCL.hbc). The exception tag reflects this, as the following example shows:

Error: null reference
    at renderItem (index.hbc:1:46343)
    at loadContent (tsDCL.hbc:1:8120)
    [SourceMap: index:a1b2c3d4... tsDCL:f9e8d7c6...]

You can use the --dcl-dir option to point at the DCL's build output:

npx rk decode-stack --dcl-dir tsDCL=/path/to/dcl-app/dist-dev

The following example decodes a trace that contains a frame from a DCL bundle:

cd apps/rsg-ts/dcl-test-host-app

echo 'Error: DCL test crash
    at function-name-stripped (address at /tmp/plugin/LIBAAAfZbkO4/pkg:/source/compiled/tsDCL.hbc:1:2161)
    [SourceMap: index:416de74aae8e4612f4ffe6d1db81328ff14d919a9e7e42e9b2369474e188e4ad tsDCL:a807c57850741bff79ec19be20ad3a9404923f20aab670fdcccb7b7f32591676]' | npx rk decode-stack --dcl-dir tsDCL=/path/to/ts-dcl/dist-dev/bundle

Comparing DCL SHA to host SHA

The following table describes the differences between the SHA of a host app and the SHA of a DCL bundle:

PropertyHost appDCL bundle
SHA sourceContent hash of the universal source map (SHA256(universalMap))Content hash of the JavaScript source map (SHA256(jsMap))
Stored inmanifest → hbc_source_map_shavar DCL_BUNDLE_SHA prepended to the JavaScript bundle before Hermes compilation
Runtime readAppInfo.getManifestValue("hbc_source_map_sha")globalThis.__DCL_SOURCE_MAP_SHAS[libraryName] (set by DCL register())
Source map file{universalSha}.source.map (filename = content hash){jsMapSha}.source.map (filename ≠ content hash; file contains the universal map)

For the host app, the SHA is a true content hash, and the SHA256(fileContents) value matches the filename. For DCLs, the SHA is derived from the JavaScript source map (computed before Hermes) and used as a lookup key. The DCL build renames the universal source map file to match this key so the decoder can locate it.

This difference exists because the firmware provides AppInfo.getManifestValue() for host apps, but does not include an API that lets DCLs read their own manifest at runtime. The SHA must therefore be embedded inside the compiled bundle. The universal map SHA depends on the Hermes output, but the Hermes input must already contain the SHA. To break this cycle, the build uses the JavaScript source map hash, which is available before Hermes compilation.

Troubleshooting

The following table provides solutions for common decoding errors:

ErrorCauseFix
Timed out waiting for stdin input (30s)The command detected stdin (non-TTY), but no data arrived within the timeout (default 30 seconds).Pipe data into the command. You can use --file, use --stream for live log sessions, or increase the timeout with --timeout <ms>.
Timed out waiting for interactive input (300s)Interactive paste mode timed out after 5 minutes without an EOF signal.Press Ctrl+D (macOS/Linux) or Ctrl+Z then Enter (Windows) after pasting. You can use --timeout to change the limit, or use --file instead.
--stream and --file cannot be used togetherYou passed both --stream and --file.You must use only one of the two options. The --stream option reads from stdin, and the --file option reads from a file.
File not found: <path>The path passed to --file does not exist or is not readable.Check the path and file permissions.
Input was emptyThe input (file, stdin, or inline arguments) contained only whitespace or was completely empty.Verify that the file has content, or check that data is being piped. In interactive paste mode, paste the trace before pressing Ctrl+D.
No stack frames found in inputThe text passed to decode-stack does not contain any recognizable index.hbc or index.js frame patterns.Copy the raw stack trace exactly as it appears in the device log. Frames must reference index.hbc or index.js; the command does not match other .js filenames. In interactive paste mode, press Ctrl+D (not Ctrl+C) after pasting.
No HBC source map foundThe app has not been built yet, or you deleted dist-dev/.For bytecode frames, rebuild with npx rk dev --compile-ts (or npx rk build --compile-ts) and reproduce the crash. The plain rk dev command produces JavaScript maps only. Use --build-dir to point to the bundle that produced the trace.
No mapping for <line>:<col> in universal mapYou copied the offset incorrectly, or the build that produced the map differs from the running app.Verify the column number (without commas) and ensure the deployed app matches the current build.
No JS source map foundOnly a Hermes bytecode map exists; you deleted the JavaScript map or interrupted the build.Rebuild the app.
--sha must be a hexadecimal stringThe value passed to --sha contains non-hex characters (letters g–z, slashes, spaces, and so on).Use only the hex SHA from the source map filename or device log (for example, 4a7f3c8e...).
--sha must be at least 8 hex charactersThe value passed to --sha is shorter than the required minimum of 8 characters.Provide the full (or longer prefix of the) SHA from the source map filename or device log.
Unknown option: --<name>You passed an unrecognized flag.Use the --help option (for example, rk decode-stack --help) to see the supported options.
Mapped line is one or two lines before the crashThe build was produced with an older SDK version that had minifySyntax enabled.Rebuild with the current SDK, which disables minifySyntax.
SHA changes every buildExpected. The SHA is the hash of the composed map content.You should archive the <sha>.source.map file with each release build for later decoding. You can use the --sha option to decode against a specific archived build.

Setting up monitoring integrations

The @roku-sdk/stack-decoder npm package provides a programmatic TypeScript API for decoding stack traces inside server-side pipelines. You can use it to translate Hermes bytecode frames to TypeScript source positions before they reach your error-tracking dashboard.

On-device forwarding: You can use the setUncaughtExceptionHandler() function from @roku-sdk/rsg-ts to send raw exception data from the Roku device to your monitoring server before the app exits. See Forwarding exceptions to monitoring services for more information about setup and examples. You can then use the server-side integrations listed in this section to decode the forwarded traces to TypeScript source positions.

Sentry

You can hook into Sentry's beforeSend callback to decode frames before Sentry sends an event:

import { decodeStack } from '@roku-sdk/stack-decoder';
import * as Sentry from '@sentry/node';

Sentry.init({
  async beforeSend(event) {
    if (event.exception?.values?.[0]?.stacktrace) {
      const rawStack = event.exception.values[0].stacktrace.frames
        .map(f => `  at ${f.function} (${f.filename}:${f.lineno}:${f.colno})`)
        .join('\n');

      try {
        const decoded = await decodeStack(rawStack, {
          sourceMap: await fetchSourceMap(event.release)
        });
        event.exception.values[0].stacktrace.frames = decoded.frames.map(f => ({
          filename: f.original?.source ?? f.bytecode.file,
          lineno: f.original?.line ?? f.bytecode.line,
          colno: f.original?.column ?? f.bytecode.column,
          function: f.functionName,
          context_line: f.sourceLine
        }));
      } catch {
        // Fall back to original stack if decoding fails
      }
    }
    return event;
  }
});

Bugsnag

You can use Bugsnag's onError callback:

import { decodeStack } from '@roku-sdk/stack-decoder';
import Bugsnag from '@bugsnag/node';

Bugsnag.start({
  async onError(event) {
    const rawStack = event.errors[0].stacktrace
      .map(f => `  at ${f.method} (${f.file}:${f.lineNumber}:${f.columnNumber})`)
      .join('\n');

    try {
      const decoded = await decodeStack(rawStack, {
        sourceMap: await fetchSourceMap(event.app.version)
      });
      event.errors[0].stacktrace = decoded.frames.map(f => ({
        file: f.original?.source ?? f.bytecode.file,
        lineNumber: f.original?.line ?? f.bytecode.line,
        columnNumber: f.original?.column ?? f.bytecode.column,
        method: f.functionName
      }));
    } catch {
      // Fall back to original stack
    }
  }
});

Decoding in high-throughput pipelines

If you are processing many stack traces against the same build, you can load the source map once with loadSourceMap and pass the cached bundle to every decodeStack call:

import { decodeStack, loadSourceMap } from '@roku-sdk/stack-decoder';

const bundle = await loadSourceMap('./dist-build/bundle', 'abc123');

for (const trace of incomingTraces) {
  const decoded = await decodeStack(trace, { bundle });
  forwardToMonitoring(decoded);
}

Using CLI JSON output in pipelines

The CLI --json option produces machine-readable output that you can feed into external tools:

npx rk decode-stack --json -f crash.log | curl -X POST \
  -H 'Content-Type: application/json' \
  -d @- https://errors.example.com/api/ingest

See Stack decoder examples for more information about the runnable examples of each integration.

Applying best practices for error handling

This section describes guidelines for error handling that you should incorporate in your app.

Preserving the SourceMap tag in log pipelines

When enableExceptionTagging() is active, the SDK appends a [SourceMap: <sha>] tag to every Error.stack string. If your log aggregation pipeline truncates or filters stack traces, you must preserve this tag because it is the only link between a runtime error and the correct source map. You must configure log field length limits to accommodate at least 80 additional characters beyond the raw stack.

Archiving source maps with every release

Each production build produces a unique <sha>.source.map file. You should archive this file alongside the release artifacts so that you can decode stack traces from any deployed version at a later time.

You can maintain a mapping from application version (or release tag) to the source map SHA so that you can route incoming crash reports to the correct map automatically. The following commands build the app and copy the source map to an artifact store:

npx rk build
cp dist-build/bundle/source/compiled/*.source.map /your/artifact-store/v$(node -p "require('./package.json').version")/

Using structured error logging

When you catch and re-throw errors, you should preserve the original stack trace. The Error.stack property already includes the [SourceMap: ...] tag; therefore, you can log it verbatim to ensure that the SHA is available for later decoding. The following example logs the stack and re-throws the error:

try {
  riskyOperation();
} catch (err) {
  logger.error('Operation failed', {
    stack: (err as Error).stack,
    context: { userId, sessionId }
  });
  throw err;
}

Testing decoding during CI

You can add a continuous integration (CI) step that runs rk decode-stack against a known stack trace after every build to verify that the build generates source maps correctly and that they produce accurate mappings. If the output contains "error": null for all frames, the maps are valid. The following commands show such a step:

npx rk build
echo "  at handleFocus (index.hbc:1:46343)" | npx rk decode-stack --json --build-dir dist-build/bundle

Protecting privacy and security

This section describes privacy and security guidelines for source maps.

Protecting original source code in source maps

Source maps embed the full sourcesContent array, including the original TypeScript source of every file in the bundle. You must treat source maps with the same confidentiality as your application source code.

Keeping source maps off the device

The build copies only index.js (or index.hbc bytecode with --compile-ts) to the Roku device (into pkg:/source/compiled/). With rk dev, every .map file stays in the local bundle/ directory and rk dev never sideloads it. A JavaScript rk build package is different: it also carries source/compiled/index.js.map because the store's compiler needs it to produce the bytecode; therefore, you must treat that package with the same confidentiality as your source code. The on-device manifest contains only the SHA, not the map contents.

Storing production maps securely

For production releases, you should store source maps in access-controlled storage:

  • You can use a private S3 bucket (or equivalent) with IAM policies restricting access to the crash-reporting pipeline and authorized developers.
  • You must disable public access to any bucket or server that hosts source maps.
  • You can rotate credentials used by CI to upload maps, and audit access logs periodically.

Verifying the SHA

The SHA-256 filename of the universal source map serves as an integrity check. When loading a source map by SHA, you can verify that the hash of the file contents matches the filename to guard against corruption or tampering:

import crypto from 'crypto';
import fs from 'fs/promises';

async function loadVerifiedMap(buildDir: string, sha: string) {
  const content = await fs.readFile(`${buildDir}/${sha}.source.map`, 'utf-8');
  const actual = crypto.createHash('sha256').update(content).digest('hex');

  if (actual !== sha) {
    throw new Error(`Source map integrity check failed: expected ${sha}, got ${actual}`);
  }

  return JSON.parse(content);
}

Minimizing exposure in error-tracking services

When forwarding decoded stack frames to third-party services (Sentry, Bugsnag, and so on), the sourceLine and sourceContext fields contain verbatim source code. If your security policy prohibits sharing source with third parties, you can omit these fields or redact them before sending:

const sanitised = decoded.frames.map(f => ({
  file: f.original?.source,
  line: f.original?.line,
  column: f.original?.column,
  function: f.functionName
  // sourceLine and sourceContext intentionally omitted
}));

Related documentation


Did this page help you?