* Copyright (c) 2026 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const ERROR_TYPE_RE =
/(TypeError|ReferenceError|RangeError|SyntaxError|BusinessError|ParameterError|ResourceError|SystemError|EvalError|URIError)/i;
const CRASH_SIGNAL_RE = /(jscrash|uncaught|exception|fatal|abort|crash|error)/i;
const STRONG_CRASH_SIGNAL_RE =
/(jscrash|uncaught|fatal|abort|crash|TypeError|ReferenceError|RangeError|SyntaxError|BusinessError|ParameterError|ResourceError|SystemError|EvalError|URIError)/i;
const FILE_RE = /([A-Za-z0-9_./\\-]+\.(ets|ts|js)(?::\d+:\d+)?)/i;
function trim(input) {
return input.trim();
}
function cleanLines(input) {
return input
.split(/\r?\n/)
.map((line) => line.trimEnd())
.filter((line) => line.trim().length > 0);
}
function unique(items) {
return [...new Set(items)];
}
function containsIgnoreCase(input, token) {
return input.toLowerCase().includes(token.toLowerCase());
}
function firstMatch(input, patterns) {
for (const pattern of patterns) {
const match = pattern.exec(input);
if (match?.[1]) {
return match[1].trim();
}
}
return '';
}
function errorTypeFromText(input) {
const matches = input.match(new RegExp(ERROR_TYPE_RE.source, 'ig'));
if (!matches?.length) {
return '';
}
return matches[matches.length - 1];
}
function detectErrorType(lines, anchor, focus) {
const headerText = lines.slice(0, 48).join('\n');
const fromHeader = errorTypeFromText(headerText);
if (fromHeader) {
return fromHeader;
}
for (const line of lines.slice(0, 48)) {
const match = /^(?:Reason|Error\s+name|Error\s+type)\s*[::]\s*([A-Za-z]+Error)\s*$/i.exec(line.trim());
if (match?.[1] && ERROR_TYPE_RE.test(match[1])) {
return match[1];
}
}
if (anchor >= 0) {
const aroundAnchor = errorTypeFromText(sliceWindow(lines, anchor, 12, 8).join('\n'));
if (aroundAnchor) {
return aroundAnchor;
}
}
const fromFocus = errorTypeFromText(focus);
if (fromFocus) {
return fromFocus;
}
return 'UnknownError';
}
function detectBundle(input, bundleName) {
if (bundleName) {
return bundleName;
}
return (
firstMatch(input, [
/bundleName\s*[:=]\s*([A-Za-z0-9._-]+)/i,
/bundle\s*[:=]\s*([A-Za-z0-9._-]+)/i,
/app\s*[:=]\s*([A-Za-z0-9._-]+)/i,
]) || '(unknown)'
);
}
function detectProcess(input, processHint) {
if (processHint) {
return processHint;
}
return (
firstMatch(input, [
/(?:process(?:Name)?)\s*[:=]\s*([A-Za-z0-9._-]+)/i,
/pid\s*[:=]\s*([0-9]+)/i,
]) || '(unknown)'
);
}
function stackLike(line) {
return (
/at\s+.+:\d+:\d+/i.test(line) ||
/at\s+.+\(.+:\d+:\d+\)/i.test(line) ||
/([A-Za-z0-9_./\\-]+\.(ets|ts|js)):\d+:\d+/.test(line)
);
}
function applicationFrameScore(line, bundle) {
let score = 0;
if (/entry[\\/].*\.ets/i.test(line)) {
score += 8;
}
if (/src[\\/].*\.(ets|ts|js)/i.test(line)) {
score += 6;
}
if (bundle !== '(unknown)' && containsIgnoreCase(line, bundle)) {
score += 4;
}
if (/(pages|page|feature|component|viewmodel|store|model)[\\/]/i.test(line)) {
score += 3;
}
if (/(framework|runtime|node_modules|oh_modules|libarkui|ets_runtime|foundation|system)[\\/]/i.test(line)) {
score -= 6;
}
return score;
}
function scoreCrashLine(line, bundle, processHint) {
let score = 0;
if (CRASH_SIGNAL_RE.test(line)) {
score += 5;
}
if (STRONG_CRASH_SIGNAL_RE.test(line)) {
score += 4;
}
if (ERROR_TYPE_RE.test(line)) {
score += 4;
}
if (stackLike(line)) {
score += 2;
}
if (bundle && bundle !== '(unknown)' && containsIgnoreCase(line, bundle)) {
score += 2;
}
if (processHint && processHint !== '(unknown)' && containsIgnoreCase(line, processHint)) {
score += 2;
}
return score;
}
function findCrashAnchor(lines, bundle, processHint) {
for (let index = lines.length - 1; index >= 0; index -= 1) {
const score = scoreCrashLine(lines[index], bundle, processHint);
if (score >= 7) {
return index;
}
}
for (let index = lines.length - 1; index >= 0; index -= 1) {
const score = scoreCrashLine(lines[index], bundle, processHint);
if (score >= 5) {
return index;
}
}
return -1;
}
function sliceWindow(lines, anchor, before, after) {
if (anchor < 0) {
return [];
}
const start = Math.max(0, anchor - before);
const end = Math.min(lines.length, anchor + after + 1);
return lines.slice(start, end);
}
function findCrashSignal(lines, start, end, step) {
for (let index = start; step > 0 ? index < end : index >= end; index += step) {
if (CRASH_SIGNAL_RE.test(lines[index])) {
return lines[index].trim();
}
}
return '';
}
function detectErrorMessage(lines, anchor) {
if (anchor >= 0) {
const forwardMatch = findCrashSignal(lines, anchor, Math.min(lines.length, anchor + 6), 1);
if (forwardMatch) {
return forwardMatch;
}
const backwardMatch = findCrashSignal(lines, anchor, Math.max(0, anchor - 3), -1);
if (backwardMatch) {
return backwardMatch;
}
return lines[anchor].trim();
}
for (let index = lines.length - 1; index >= 0; index -= 1) {
if (CRASH_SIGNAL_RE.test(lines[index])) {
return lines[index].trim();
}
}
return lines[lines.length - 1] || '(not found)';
}
function detectTopStack(lines, anchor) {
const around = anchor >= 0 ? sliceWindow(lines, anchor, 4, 18) : lines.slice(Math.max(0, lines.length - 24));
const frames = around.filter((line) => stackLike(line));
return unique(frames).slice(0, 8);
}
function detectSuspectedFile(topStack, bundle) {
const candidates = [];
for (let index = 0; index < topStack.length; index += 1) {
const line = topStack[index];
const match = FILE_RE.exec(line);
if (!match?.[1]) {
continue;
}
candidates.push({
line: match[1],
index,
score: applicationFrameScore(match[1], bundle) - index,
});
}
if (!candidates.length) {
return '(not found)';
}
candidates.sort((a, b) => b.score - a.score || a.index - b.index);
return candidates[0].line;
}
function detectKeywords(input) {
const candidates = [
'jscrash',
'uncaught',
'exception',
'fatal',
'typeerror',
'referenceerror',
'rangeerror',
'syntaxerror',
'businesserror',
'parametererror',
'resourceerror',
'systemerror',
'abort',
'crash',
];
return candidates.filter((item) => containsIgnoreCase(input, item));
}
function looksLikeCrash(input, anchor, errorType) {
if (errorType !== 'UnknownError') {
return true;
}
if (anchor >= 0) {
return true;
}
return /(jscrash|uncaught|fatal|abort|crash)/i.test(input);
}
function pickExcerpt(lines, anchor, bundle, processHint, limit) {
if (anchor >= 0) {
const around = sliceWindow(lines, anchor, 5, 18);
if (around.length) {
return around.slice(0, limit);
}
}
const scored = lines
.map((line, index) => ({
line,
index,
score: scoreCrashLine(line, bundle, processHint),
}))
.filter((item) => item.score > 0);
if (!scored.length) {
return lines.slice(Math.max(0, lines.length - limit));
}
const out = [];
for (const item of scored.slice(Math.max(0, scored.length - limit))) {
out.push(item.line);
}
return unique(out).slice(0, limit);
}
export function buildNextActionText(report) {
if (report.status === 'no_crash_signature') {
return 'Ask for a fuller crash log, a clearer repro, or optionally collect recent faultlogger or hilog evidence if needed.';
}
if (report.suspectedFile !== '(not found)') {
return `Inspect ${report.suspectedFile} first and make a minimal fix. Collect additional runtime evidence only if the current anchor is still too weak.`;
}
return 'Inspect the top stack frames first and make a minimal fix. If the stack is too weak, optionally collect more runtime evidence before broader code reading.';
}
export function formatCrashReportText(report) {
return [
report.status === 'detected' ? 'Crash signature detected.' : 'No clear crash signature detected.',
`source: ${report.source}`,
`device: ${report.device}`,
`bundle: ${report.bundle}`,
`process: ${report.process}`,
`error_type: ${report.errorType}`,
`error_message: ${report.errorMessage}`,
`suspected_file: ${report.suspectedFile}`,
`keywords: ${report.keywords.length ? report.keywords.join(', ') : '(none)'}`,
'',
'Top stack:',
...(report.topStack.length ? report.topStack : ['(empty)']),
'',
'Evidence excerpt:',
...(report.excerpt.length ? report.excerpt : ['(empty)']),
'',
`next_action: ${buildNextActionText(report)}`,
].join('\n');
}
export function buildCrashReport(
input,
source,
device,
bundleName,
processHint,
) {
const normalized = trim(input);
const lines = cleanLines(normalized);
const bundle = detectBundle(normalized, bundleName);
const process = detectProcess(normalized, processHint);
const anchor = findCrashAnchor(lines, bundle, process);
const focus = anchor >= 0 ? sliceWindow(lines, anchor, 5, 18).join('\n') : normalized;
const errorType = detectErrorType(lines, anchor, focus);
const topStack = detectTopStack(lines, anchor);
const excerpt = pickExcerpt(lines, anchor, bundle, process, 24);
return {
status: looksLikeCrash(normalized, anchor, errorType) ? 'detected' : 'no_crash_signature',
source,
device,
bundle,
process,
errorType,
errorMessage: detectErrorMessage(lines, anchor),
suspectedFile: detectSuspectedFile(topStack, bundle),
keywords: detectKeywords(focus || normalized),
topStack,
excerpt,
};
}