【技术分享之四】JS错误上报及错误定位的方法

前言

经常看论坛有人在问Bugly的JS错误上传怎么搞,按官方的文档会有各种错误等等。这个问题的原因是Creator的JS引擎升级了,导致Bugly调用的API在新引擎中不存在,所以不能成功接入。

最近刚好把异常上报机制整完,就把这部分内容拿出来分享一下;对于Bugly怎么接入Android和IOS,还是自己去看官方文档吧,照着做一般也是能成功的。

安卓中间会有一点小错误,主要是搜索路径的问题,如果按官方的做法,最后你还需要在frameworks\runtime-src\proj.android-studio\app\jni\Android.mk中加上搜索路径:

# bugly:
LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../../Classes \
$(LOCAL_PATH)/../../../../cocos2d-x/external

我们不希望Bugly污染到cocos2d-x引擎,所以没按照Bugly官方的方式接入,而是直接把它加到Classes中。

这里要分享的是怎么上报JS错误的,以及上报的JS错误如何轻松定位到。

JS异常上报

Bugly上报JS错误的方法是调用BuglyJSAgent::registerJSExceptionHandler注册异常处理,里面调用JS_SetErrorReporter(cx, BuglyJSAgent::reportJSError);

但是creator1.6之后JS引擎提升到SM52,这个函数不见了,所以原来的Bugly方法不可用。

我改造了另一种方法,利用ScriptingCore暴露回调函数给Bugly使用,这样就不用依赖JS引擎的函数:

  • ScriptingCore.h加一个回调函数声明,以及设置回调函数的方法:
// 错误报告回调
typedef void (*ExceptionReportCallback)(const char *message, const char *traceback);
//
static void setExceptionReportCallback(ExceptionReportCallback cb);
  • ScriptingCore.cpp声明静态变量,以及实现设置方法:
static ExceptionReportCallback _exceptReportCallback = nullptr;
//
void ScriptingCore::setExceptionReportCallback(ExceptionReportCallback cb) {
    _exceptReportCallback = cb;
}
  • 接着加上回调函数的调用:这两天panda提了一个PR,把ScriptingCore::reportError整合到异常调用中,但是在v1.6.1中还没有,需要从Github上合过来,如果没有合的话,也可以在handlePendingException里面调:
void handlePendingException(JSContext *cx)
{
	// ...
	if (_exceptReportCallback) {
		_exceptReportCallback(message.c_str(), stackStr.c_str());
	}
}

上面的message是脚本文件和行号,stackStr是调用堆线信息

如果整合了PR,回调可以写在reportError中,完整代码如下:

void ScriptingCore::reportError(JSContext *cx, JSErrorReport *report, JS::HandleValue err)
{
    if (cx && report)
    {
        std::string fileName = report->filename ? report->filename : "<no filename>";
        int32_t lineno = report->lineno;
        std::string msg = report->message().c_str();
        CCLOGERROR("JSException: %s, file: %s, lineno: %u\n", msg.c_str(), fileName.c_str(), lineno);
        
        std::string stackStr = "";
        if (err.isObject())
        {
            JS::RootedObject errObj(cx, err.toObjectOrNull());
            JS::RootedValue stack(cx);
            if (JS_GetProperty(cx, errObj, "stack", &stack) && stack.isString())
            {
                JS::RootedString jsstackStr(cx, stack.toString());
                stackStr = JS_EncodeStringToUTF8(cx, jsstackStr);
                CCLOGERROR("Stack: %s\n", stackStr.c_str());
            }
        }
        
        JS::AutoValueVector valArr(cx);
        JS::RootedValue argv(cx);
        std_string_to_jsval(cx, fileName, &argv);
        valArr.append(argv);
        valArr.append(JS::Int32Value(lineno));
        std_string_to_jsval(cx, msg, &argv);
        valArr.append(argv);
        std_string_to_jsval(cx, stackStr, &argv);
        valArr.append(argv);
        JS::HandleValueArray args(valArr);
        
        JS::RootedValue rval(cx);
        JS::RootedValue global(cx, JS::ObjectOrNullValue(getInstance()->getGlobalObject()));
        getInstance()->executeFunctionWithOwner(global, "__errorHandler", args, &rval);
		
        // 就是这里
        std::stringstream sstm;
        sstm << fileName << ":" << lineno << "  " << msg;
        std::string message = sstm.str();
        if (_exceptReportCallback) {
            _exceptReportCallback(message.c_str(), stackStr.c_str());
        }
    }
};
  • 加完之后,当有JS错误,回调函数即会调用,接下来是修改Bugly

打开AppDelegate.cpp,加上如下代码:

// 在applicationDidFinishLaunching中
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID || CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
    // bugly: js exception handler
    ScriptingCore::setExceptionReportCallback(CrashReport::reportJSException);
#endif

打开CrashReport.h加上

static void reportJSException(const char *message, const char *traceback);

打开CrashReport.mm加上

void CrashReport::reportJSException(const char *message, const char *traceback) {
    reportException(CATEGORY_JS_EXCEPTION,  "JSException", message, traceback);
}
  • 到此改造完成,现在有JS错误,就会上报到Bulgy上面。

工程代码错误的定位

上报完JS错误会发现另一个难题,Creator在编译release工程时,先把工程合成一个project.js,然后再编译成字节码project.jsc。问题出在合成project.js时,还把代码给压缩了,空格和换行都删除了,project.js变成只有一行的文件。此时Bugly上报的错误信息是第1行的第xxxx列。

看到这个,一来没有project.js,二来只有一行的文件相当难看懂。这样子即使上报到Bugly,顶多只知道有脚本错,却很难知道错在哪里。

要解决这个问题,需要有这样的支持:

  1. 有一个命令可以单独生成project.js,且可以设定是否压缩脚本。
  2. 有另一个命令可以将project.js编译成jsc。

不幸问过引擎组人之后,确定第1个命令没有,我思来想去,终于找到一个很绕但可以实现的方法,最后也整合到打包的流程当中。这整个过程大概是这样的:

  1. 用命令行编译工程,这次用debug=true的方式:
cmd = '%s --path %s --build "platform=android;debug=true"' % (COCOS_CREATOR, PROGRAM_PATH)
os.system(cmd)

上面编完之后,可以得到project.dev.js文件,这是一个没有压缩过的工程脚本。

  1. 将project.dev.js拷贝到一个备份目录中,如果构建过程有版本的,最好给文件名再加一个版本号,我们是这样的:
# 备份project.dev.js
src_proj_file = os.path.join(PROJECT_ROOT, "src", "project.dev.js")
bak_proj_file = os.path.join(PROJECT_ROOT, "backup-src", "project.%s.js" % self.build_version)
shutil.copy(src_proj_file, bak_proj_file)

拷贝完之后,会得到如project.1242.js的文件,以后报错,根据版本号,就可以知道是哪个脚本。

  1. 再用命令行编译工程,这次用debug=false的方式:
cmd = '%s --path %s --build "platform=android;debug=false"' % (COCOS_CREATOR, PROGRAM_PATH)
os.system(cmd)

编完之后,你就得到一堆jsc文件,接下来是重点。

  1. 将刚才备份的project.js编译成jsc替换掉release版的,这样就得到正常格式的jsc文件。
proj_file = os.path.join(PROJECT_ROOT, "src", "project.jsc")
ret = os.system('%s "%s" "%s"' % (JSTOOL, bak_proj_file, proj_file))

JSTOOL在:$(COCOS_ROOT)\\resources\\cocos2d-x\\tools\\cocos2d-console\\plugins\\plugin_jscompile\\bin\\jsbcc.exe"

经过上面一大翻周折之后,你得到一个正常格式project.jsc。其他文件如jsb_polyfill.jsc是引擎脚本,我并没有这样处理,因为我相信引擎还是相对稳定一些的。

打包成原生包之后,遇到脚本错,可以在Bugly网站上看到信息,如有下面的错误:

D:\mydev\ahzh\release\client\v16\jsb-default\backup-src\project.5692.js:7729 addType is not defined
1 updateGemRes@D:\mydev\ahzh\release\client\v16\jsb-default\backup-src\project.5692.js:7729:17
2 SM_upgradeGemRes@D:\mydev\ahzh\release\client\v16\jsb-default\backup-src\project.5692.js:10542:17
3 onMessage@D:\mydev\ahzh\release\client\v16\jsb-default\backup-src\project.5692.js:18385:21
4 require<.SocketManager</connect/this.socket.onmessage@D:\mydev\ahzh\release\client\v16\jsb-default\backup-src\project.5692.js:18351:21

看上面的错误,直接打开相应的脚本,定位到7729行,马上就知道问题所在。

我不知道其他人是怎么解决这个问题,可能你们也有更好的办法,如果没有,倒是可以试试我这套比较绕的方案:)

等1.6.2出来,可能会更简单一些。

13赞

学习了

太赞了,谢谢分享

支持大佬 的干货~~

大赞啊!!1.6.2 出来后,project.jsc 等所有 jsc 文件中都会带上行号,同时构建后会保留所有分行后的 js 文件便于查阅源码。

2赞

jare,你这是在装逼,161都在beta呢,你这样在厦门会被抓去做沙茶面的

赞,这就是我想要的

期待@panda 大大早日搞完

如果能在jsb上使用source maps就好更棒了!!

虽然看到怎么修复这个问题,但是这也太折腾了,引擎升级应该把这块考虑进去吧,毕竟是辅助收集错误的sdk,真的是蛋疼

我觉得不是引擎应该怎么改,而是应该通知Bugly说引擎的API改了,让他们尽快调整过来。

1.6.2 还没发 最新的错误回调还没合并

看的一脸懵逼,:joy:

建议引擎组把楼主挖入cocos开发团队.

做H5不知道有什么好的bug上报方法

我们用的window.__errorHandler, 之前在这里问过:
http://forum.cocos.com/t/js/45593/2
不知两者有没有什么区别

我们这样的好处是在js里处理错误上报,和h5统一起来.

1赞

找了一下,真有web的报错方法
window.onerror =function (sMsg,sUrl,sLine){
//web平台
}

赞啊,终于出这个功能了,以后手机端看错误行号可以直接定位了

1.6.2 已经出了 是不是更新一下? 难得的精品文章

试试看