Android平台Typescript类文件 async函数错误捕获如何精确定位行数?

  • Creator 版本:Cocos Creator2.4.11 目前应该后续的 2.4.x 版本都存在同样的问题

  • 目标平台: Android

  • 重现方式:必现

Android平台async函数捕获错误精确定位行数?

Android平台 Promise 捕获错误精确定位行数?

环境

  • Mac
  • Android Studio
  • Chrome
  • Cocos Creator 2.4.11
  • Typescript

问题

如何精确的在原生Android平台像Web平台一样精确定位线上错误的行数?

代码

Helloworld.ts

import { GlobalErrorHandler } from “./GlobalErrorHandler”;
const {ccclass, property} = cc._decorator;

@ccclass
export default class Helloworld extends cc.Component {

@property(cc.Label)
label: cc.Label = null;

@property
text: string = ‘hello’;

protected onLoad() {
// 初始化错误监听器
GlobalErrorHandler.init();
}

start () {
// init logic
this.label.string = this.text;
this.testHandler();

}

private async testHandler(){
console.log(“testHandler”);
const data = {};
data.a.b = 1;
console.log("-------------");
console.log(“testHandler end”);
}

private async testHandler2() {
console.log(“testHandler2 end”);
}
private async testHandler3(){
console.log(“testHandler3”);
// await SplitFrameUtil.checkNextFrame();
// await this.testHandler4();
console.log(“testHandler3 end”);
}
}

注意: data.a.b = 1;其中这一行会报错 (data下没有a属性所以给data.a.b赋值的时候报错) ,其中这个函数为async 函数,本次的问题也是关于async函数的。

GlobalErrorHandler.ts

export class GlobalErrorHandler {
static init() {
// 捕获同步错误
window.onerror = (message, source, lineno, colno, error) => {
console.error(“同步错误:”, {
message,
source,
lineno,
colno,
error,
});
GlobalErrorHandler.outputCompleteError({
type: ‘同步错误’,
message,
source,
lineno,
colno,
error
});
};
console.log(“GlobalErrorHandler init success”)
if (‘onunhandledrejection’ in window) {
console.log(“当前环境支持 onunhandledrejection 事件”);
// 处理未捕获的Promise错误
window.addEventListener(‘unhandledrejection’, function(event) {
console.log(“未处理的 Promise 错误:”, event.reason);
const message = event.reason.message;
// const stack = event.reason.stack.replace(/\n/g, ‘
’);
const stack = event.reason.stack;
const jsErrorStack = JSON.stringify(event.reason);
console.log(“smile::rocket::GlobalErrorHandler m: l:54->”, jsErrorStack);
GlobalErrorHandler.outputCompleteError({
type: ‘未处理的 Promise 错误’,
reason: { type: event.type, message, stack }
});
event.preventDefault(); // 阻止默认处理
});
} else {
console.warn(“当前环境不支持 onunhandledrejection 事件”);
}
if (typeof process !== ‘undefined’) {
console.log(“当前环境支持 process.unhandledRejection 事件”);
process.on(‘unhandledRejection’, (reason, promise) => {
console.log(“捕获未处理的 Promise 错误:”, reason);
});
console.log(“当前环境支持 unhandledRejection success”)
}else {
console.warn(“当前环境不支持 process.unhandledRejection 事件”);
}

    // 捕获已处理的 Promise 错误
    window.onrejectionhandled = (event: PromiseRejectionEvent) => {
        console.log("Promise 错误已被处理:", event.reason);
        const message = event.reason.message
        const stack = event.reason.stack
        GlobalErrorHandler.outputCompleteError({
            type: '已处理的 Promise 错误',
            reason: {type:event.type,message,stack}
        });
    };
    console.log("GlobalErrorHandler init success")
    if(typeof jsb !== 'undefined'){
        jsb && jsb.onError(function (location, message, stack) {
            console.log("jsb 错误:", location);
            console.log("jsb message:", message);
            console.log("jsb stack:", stack);
        });
    }
}

// 输出完整的错误信息(可以进一步扩展为上传至错误监控系统等操作)
static outputCompleteError(errorDetails: { type: string; [key: string]: any }) {
    // 这里可以扩展为将错误发送到远程日志或监控系统
    console.log("\n完整的错误信息:", JSON.stringify(errorDetails, null, 2));
}

}

构建web平台和 Android平台

image-20250207114828282

其中 Web平台报错

Android平台报错

问题分析

对比构建后的文件

我们看到 构建后的文件 内容都是完全一致的,但是web 可以精确报错到某一行,但是在Android平台 就无法精确了,我们可以看到在Android平台报错内容 只能看到 275行 而无法像 Web平台那样 看到 411行报错,而且最近的报错是 406行 ,这样就带来一个问题,线上游戏报错凡是async函数的报错都无法精确到报错到具体行数,如果函数内方法简单,相对来说还容易找到问题,但是对于一些行数很多的方案这样无法精确到哪一行。

自己做的一些探究

原生代码

我找到了代码 Creator/2.4.11/CocosCreator.app/Contents/Resources/cocos2d-x/cocos/scripting/js-bindings/manual/jsb_cocos2dx_manual.cpp

这个路径下:

se::ScriptEngine::getInstance()->setJSExceptionCallback([objFunc](const char *location, const char *message, const char *stack) {
se::ValueArray jsArgs;
jsArgs.resize(3);
jsArgs[0] = se::Value(location);
jsArgs[1] = se::Value(message);
jsArgs[2] = se::Value(stack);
objFunc->call(jsArgs, nullptr);
});

在这里添加打印发现:

se::ScriptEngine::getInstance()->setJSExceptionCallback([objFunc](const char *location, const char *message, const char *stack) {
// 输出异常的堆栈信息
CCLOG(“JS Exception caught!”);
CCLOG(“Location@@@: %s”, location);
CCLOG(“Message@@@: %s”, message);
CCLOG(“Stack@@@: %s”, stack);
se::ValueArray jsArgs;
jsArgs.resize(3);
jsArgs[0] = se::Value(location);
jsArgs[1] = se::Value(message);
jsArgs[2] = se::Value(stack);
objFunc->call(jsArgs, nullptr);
});

重新构建引擎,出包发现,是传进来的时候 就缺少了 完整的栈信息

然后找到了 这里:

polyfill/typescript.js

在这个后面添加catch,构建引擎,发现构建后的引擎文件被成功修改:

但是构建后项目内的bundle内的index.js 里面的promise被转化的代码 还是没有带catch,于是有了下面的猜想

解决方案求教

我这里想到两个解决思路:

方案一:完整传给jsb报错信息

既然在jsb 那边得到的就是不完整的,那么这个栈信息是如何丢失的?如何才能把完整的传过去?

方案二:如果在构建的时候把catch信息补充完整

这里指的补充完整是在转换的时候,由于补充后,会改变原来js代码的行数,会影响原来生成的sourcemap 所以,请教如何不使用插件在完成后更改,而是通过引擎模板或者构建设置更改?

最后:请教各位对引擎熟悉的,能否给出一个解决方案,可以精准定位线上报错信息位置的行数?

下面是demo 的完整代码 有愿意帮忙的 请大佬帮忙看看
DemoProject.zip (285.8 KB)

接个bugly也行

目前还没有接入bugly 感觉 这个应该官方没有做完善导致的 想着把问题抛出来 大家看看 问题解决后,受益者是全体开发者

经过许久自己找到解决方案了 使用的方案一 结贴!!

这里感谢 android平台有办法捕获到全局未处理的promise异常吗?
他的两张图 给出了很多线索

修改文件 cocos2d-x/cocos/scripting/js-bindings/jswrapper/v8/ScriptEngine.cpp
的两个方法:
onPromiseRejectCallback

修改后 :

void ScriptEngine::onPromiseRejectCallback(v8::PromiseRejectMessage msg)
{
v8::Isolate *isolate = getInstance()->_isolate;
v8::HandleScope scope(isolate);
std::stringstream ss;
auto event = msg.GetEvent();
auto value = msg.GetValue();
const char *eventName = “[invalidatePromiseEvent]”;

    if(event == v8::kPromiseRejectWithNoHandler) {
        eventName = "unhandledRejectedPromise";
    }else if(event == v8::kPromiseHandlerAddedAfterReject) {
        eventName = "handlerAddedAfterPromiseRejected";
    }else if(event == v8::kPromiseRejectAfterResolved) {
        eventName = "rejectAfterPromiseResolved";
    }else if( event == v8::kPromiseResolveAfterResolved) {
        eventName = "resolveAfterPromiseResolved";
    }
    
    if(!value.IsEmpty()) {
        // prepend error object to stack message
        v8::Local<v8::Object> errorObj = value->ToObject(isolate->GetCurrentContext()).ToLocalChecked();
        v8::Local<v8::Value> stackValue;
        SE_LOGE("-------走到这里------00--:\n");
        if (errorObj->Get(isolate->GetCurrentContext(), v8::String::NewFromUtf8(isolate, "stack", v8::NewStringType::kNormal).ToLocalChecked()).ToLocal(&stackValue) && !stackValue.IsEmpty()) {
            v8::String::Utf8Value stackUtf8(isolate, stackValue);
            ss << "stacktrace: " << std::endl;
            ss << *stackUtf8 << std::endl;
            SE_LOGE("-------走到这里----01----:\n");
        } else {
            // 如果错误对象没有 stack 属性,则使用 getCurrentStackTrace 方法
            auto stackStr = ScriptEngine::getInstance()->getCurrentStackTrace();
            ss << "stacktrace: " << std::endl;
            ss << stackStr << std::endl;
                SE_LOGE("-------走到这里----02----:\n");
        }
        SE_LOGE("-------走到这里----03----:\n");
        // 获取错误对象的其他信息
        v8::Local<v8::String> str = value->ToString(isolate->GetCurrentContext()).ToLocalChecked();
        v8::String::Utf8Value valueUtf8(isolate, str);
        ss << *valueUtf8 << std::endl;
    } else {
        // 如果没有错误值,则使用 getCurrentStackTrace 方法
        auto stackStr = ScriptEngine::getInstance()->getCurrentStackTrace();
        ss << "stacktrace: " << std::endl;
        ss << stackStr << std::endl;
            SE_LOGE("-------走到这里----04----:\n");
    }
    SE_LOGE("-------走到这里----05----:\n");
    std::string errorDetails = ss.str();
    ScriptEngine::getInstance()->callExceptionCallback("", eventName, errorDetails.c_str());
}

方法 getCurrentStackTrace

内容为:

std::string ScriptEngine::getCurrentStackTrace()
{
    if (!_isValid)
        return std::string();

    v8::HandleScope hs(_isolate);
    v8::Local<v8::StackTrace> stack = v8::StackTrace::CurrentStackTrace(_isolate, __jsbStackFrameLimit, v8::StackTrace::kDetailed);
    return stackTraceToString(stack);
}

下面是对比截图:

4赞

mark住,应该会用到


在2.4.15版本上,会导致v8闪退……
最后会走到onFatalErrorCallback方法中
v8的ToLocalChecked方法挂了