@roku-sdk/stack-decoder
v2.19.1
Published
TypeScript API for decoding Roku Hermes/JS stack traces to original source positions
Keywords
Readme
@roku-sdk/stack-decoder
TypeScript API for decoding Roku Hermes/JS stack traces to original TypeScript source positions. Designed for programmatic use in custom tooling and monitoring integrations (Sentry, Bugsnag, etc.).
Installation
pnpm add @roku-sdk/stack-decoderQuick Start
Auto-detect source map from build directory
import { decodeStack } from '@roku-sdk/stack-decoder';
const rawStack = `
at renderComponent (index.hbc:1234:56)
at updateView (index.hbc:890:12)
`;
const decoded = await decodeStack(rawStack, {
contextLines: 5
});
console.log(decoded.frames[0].original?.source);
// => "/Users/dev/project/src/components/Grid.tsx"
console.log(decoded.frames[0].sourceLine);
// => " const items = data.map(item => ("Provide source map directly
Bypass filesystem loading by passing a source map object. Useful for remote source maps, CI pipelines, or serverless environments.
import { decodeStack } from '@roku-sdk/stack-decoder';
import fs from 'fs/promises';
const sourceMapContent = await fs.readFile('path/to/sourcemap.json', 'utf-8');
const sourceMap = JSON.parse(sourceMapContent);
const decoded = await decodeStack(rawStack, {
sourceMap,
contextLines: 3
});Fetch source map from remote server
import { decodeStack } from '@roku-sdk/stack-decoder';
async function decodeProductionStack(stackTrace: string, sha: string) {
const response = await fetch(`https://source-maps.example.com/${sha}.json`);
const sourceMap = await response.json();
return await decodeStack(stackTrace, {
sourceMap,
sha // Still provide SHA for output metadata
});
}API Reference
decodeStack(stackTrace, options?)
Decode a Roku Hermes/JS stack trace to original TypeScript source positions.
Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| stackTrace | string | Raw stack trace text containing index.hbc or index.js frames |
| options | DecodeOptions | Optional decoding options |
Returns: Promise<DecodedStack>
loadSourceMap(buildDir, sha?, cwd?)
Load source maps from a build directory. Useful for caching across multiple decodeStack calls in high-throughput scenarios.
Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| buildDir | string | Path to the build directory containing source maps |
| sha | string | Optional source map SHA to locate specific build artifacts |
| cwd | string | Optional working directory (defaults to process.cwd()) |
Returns: Promise<LoadedBundle> — a pre-parsed bundle that can be passed to decodeStack via the bundle option.
Types
interface DecodeOptions {
sha?: string; // Source map SHA (auto-detect if omitted)
buildDir?: string; // Build directory (auto-detect if omitted)
sourceMap?: RawSourceMap; // Provide source map directly (bypasses filesystem)
bundle?: LoadedBundle; // Pre-loaded bundle from loadSourceMap (highest priority)
contextLines?: number; // Source context lines (default: 3)
cwd?: string; // Working directory for resolving build artifacts (default: process.cwd())
}
interface RawSourceMap {
version: number;
sources: string[];
names: string[];
mappings: string;
file?: string;
sourceRoot?: string;
sourcesContent?: string[];
}
interface DecodedStack {
sourceMapSha: string;
buildDir: string;
frames: DecodedFrame[];
}
interface DecodedFrame {
functionName: string;
bytecode: {
file: string;
line: number;
column: number;
};
original: {
source: string;
line: number;
column: number;
name: string | null;
} | null;
sourceLine?: string; // The source line at the mapped position
sourceContext?: string[]; // Surrounding source lines (raw text)
error?: string; // Set when frame could not be decoded
}Error Handling
The StackDecoderError class is thrown for input validation failures:
import { decodeStack, StackDecoderError } from '@roku-sdk/stack-decoder';
try {
const decoded = await decodeStack(rawStack, { sourceMap: invalidMap });
} catch (err) {
if (err instanceof StackDecoderError) {
console.error(`${err.code}: ${err.message}`);
// "INVALID_SOURCE_MAP: sourceMap.mappings must be a string"
}
}Error codes:
INVALID_OPTIONS— Invalid option values (e.g.,contextLines: NaN)INVALID_SOURCE_MAP— Malformed source map object passed tosourceMapoptionSOURCE_MAP_NOT_FOUND— No source maps found in the build directory (filesystem path only)
Frames that fail to decode do not throw. Instead, the frame's original is null and error contains the failure reason.
Integration Examples
Sentry
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 = formatSentryStack(event.exception.values[0].stacktrace);
try {
const decoded = await decodeStack(rawStack);
event.exception.values[0].stacktrace = formatDecodedForSentry(decoded);
} catch {
// Fall back to original stack if decoding fails
}
}
return event;
}
});Bugsnag
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
}
}
});High-Throughput: Cache Source Maps
For scenarios processing many stack traces against the same build, load the source maps once:
import { decodeStack, loadSourceMap } from '@roku-sdk/stack-decoder';
// Load once
const bundle = await loadSourceMap('./dist-build/bundle', 'abc123');
// Decode many — pass the pre-loaded bundle directly
for (let i = 0; i < incomingTraces.length; i++) {
const decoded = await decodeStack(incomingTraces[i], { bundle });
process.stdout.write(JSON.stringify(decoded) + '\n');
}Requirements
- Node.js >= 20.0.0
@roku-sdk/tooling(installed automatically as a dependency)
