65.9K
CodeProject 正在变化。 阅读更多。
Home

AngularJS 客户端错误日志记录。

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2016年10月24日

CPOL

4分钟阅读

viewsIcon

13809

在 AngularJS 中,如何记录包含丰富且有用信息的客户端错误。

问题

在 Web 开发中,有很多用于记录错误的手段和方法,并且它们都带有相当详尽的描述。如果我们使用 ASP.NET MVC,我们可以让所有控制器继承自一个基类,其中执行通用的异常处理逻辑(实现了 OnException 方法),我们也可以在 Global.asax 中通过 Application_Error 方法来实现,等等。

但是,我们在客户端该怎么做呢?我们也想记录这些错误,并提供关于其原生描述(发生了什么)、代码位置(文件名、行号和列号)、堆栈跟踪和其他信息。本文将重点介绍在 AngularJS 中如何解决这个问题,但通过一些简单的修改,所 presented 的解决方案也可以轻松应用于其他客户端框架或任何其他自定义开发方法,其核心思想保持不变。

解决方案

正如我之前提到的,我们使用 AngularJS 框架,并希望记录发生在我们的自定义客户端代码中的错误,我指的是控制器、指令、服务等。首先,我们需要通过 $exceptionHandler 服务订阅所有异常,这个服务可以被轻松覆盖。我参考了这篇文章的材料:https://engineering.talis.com/articles/client-side-error-logging/ 作为起点,并将在后续进行一些修改。

(function () {
    angular.module('app').factory(
    "$exceptionHandler", ["$log", "$window", function ($log, $window) {
        function error(exception, cause) {

            // preserve the default behaviour which will log the error
            // to the console, and allow the application to continue running.            
            $log.error.apply($log, arguments);            

            // now try to log the error to the server side.
            try {
                var errorMessage = exception.toString();

                var stackTrace = printStackTrace({ e: exception });                

                // use AJAX (in this example jQuery) and NOT
                // an angular service such as $http
                $.ajax({
                    type: "POST",
                    url: "/logger",
                    contentType: "application/json",
                    data: angular.toJson({
                        url: $window.location.href,
                        message: errorMessage,
                        type: "exception",
                        stackTrace: stackTrace,
                        cause: cause
                    })                
                });                                             
            } catch (loggingError) {
                $log.warn("Error server-side logging failed");
                $log.log(loggingError);
            }
        }
        return (error);
    }]);
}())

在这段代码中,我们通过 StackTrace.js 库中的 printStackTrace 方法处理任何错误,以获取其堆栈跟踪:https://www.stacktracejs.com,然后我们将所有收集到的关于错误的信息简单地 post 到服务器,存储在数据库、文件或其他地方。此时你可能已经说:“就这样了,问题完全解决了!”,但还有一个重要细节。所有这些在开发环境中都会运行得很好,但在生产环境中呢?

打包和压缩时的出现的问题。

当我们应用打包和压缩(通常在生产环境中用于最小化客户端和服务器之间的数据交换)时,我们上面 presented 的解决方案会发生什么?问题会出在堆栈跟踪上——实际上我们期望得到类似这样的信息:

0: "{anonymous}()@http://www.mysite.com/Scripts/AngularStuff/myFileName.js:123,34"
1: "{anonymous}()@http://www.mysite.com/Scripts/AngularStuff/anotherFileName.js:423,24"
etc...

但我们会得到另一个输出:

0: "{anonymous}()@http://www.mysite.com/myBundleName_SomeLongHashedTail:1:2345"
1: "{anonymous}()@http://www.mysite.com/anotherBundleName_SomeLongHashedTail:1:24665"
etc...

这是唯一一个,但却是至关重要的问题——我们丢失了错误的实际位置。现在我们会得到它在 bundle 中的位置(myBundleName_SomeLongHashedTail,第 1 行,第 2345 列;),而不是我们在开发阶段工作的实际文件。这些信息几乎无用,特别是当 bundle 不仅包含 myFileName.js(如 myFileName.min.js),还包含数十个其他文件时,搜索实际错误位置会变成一项非常复杂且繁琐的任务。

为了解决这个问题,我们需要执行两个操作:

  1. 除了 bundle,我们必须创建相应的 bundle.map 映射,以产生该 bundle 与其包含的所有文件之间的关系(在本例中,我们感兴趣的是 myFileName.js)。
  2. 通过指定要查找的部分(myBundleName_SomeLongHashedTail,第 1 行,第 2345 列;)向此 bundle.map 实体发出请求,以获取实际的、期望的位置(myFileName.js,第 123 行,第 34 列;)。

要创建 bundle.map,我们可以使用这个项目:https://github.com/benmccallum/AspNetBundling,借助它,让我们在 BundleConfig.cs 中编写下面的代码:

public static class BundleConfig
{
    public static void RegisterBundles(BundleCollection bundles)
    {
         bundles.Add(new ScriptWithSourceMapBundle("~/myBundleName")
            .IncludeDirectory("~/Scripts/AngularStuff", "*.js", true));
    }
}

ScriptWithSourceMapBundle 是该项目中的类,它允许我们创建 bundle.map 项。现在我们可以为初始的 $exceptionHandler 代码添加一些额外的逻辑。

(function () {
    angular.module('app').factory(
        "$exceptionHandler", ["$log", "$window", function ($log, $window) {
        function error(exception, cause) {

            $log.error.apply($log, arguments);            

            try {
                var errorMessage = exception.toString();

                var stackTrace = printStackTrace({ e: exception });                

                var locationWithLineAndColumn = stackTrace[0].substr(stackTrace[0].indexOf('http'));
                //stackTrace[0]: 
                //'{anonymous}()@http://www.mysite.com/myBundleName_SomeLongHashedTail:1:2345'

                var array = locationWithLineAndColumn.split(':');
                var columnNumber = array[array.length - 1];
                var lineNumber = array[array.length - 2];                

                var location = locationWithLineAndColumn.substr(0, 
                      locationWithLineAndColumn.length - 2 - lineNumber.length - columnNumber.length);
                //location: 'http://www.mysite.com/myBundleName_SomeLongHashedTail'

                var stackframe = new StackFrame(undefined, [], 
                      location, parseInt(lineNumber), parseInt(columnNumber));

                new StackTraceGPS().pinpoint(stackframe).then(
                    function (answer) {                        
                        //answer == {
                        //    columnNumber:34
                        //    fileName:"http://www.mysite.com/Scripts/AngularStuff/myFileName.js"
                        //    functionName:"someName"
                        //    lineNumber:123
                        //}
                        stackTrace.unshift(answer.fileName + ':' + answer.lineNumber + 
                              ':' + answer.columnNumber);

                        $.ajax({
                            type: "POST",
                            url: "/logger",
                            contentType: "application/json",
                            data: angular.toJson({
                                url: $window.location.href,
                                message: errorMessage,
                                type: "exception",
                                stackTrace: stackTrace, //updated value with answer's stuff
                                functionName: answer.functionName, //new field
                                cause: cause
                            })
                        });   
                    },
                    function (answer) {
                        $log.warn("Error server-side logging failed");
                        console.log(answer);
                });                                             
            } catch (loggingError) {
                $log.warn("Error server-side logging failed");
                $log.log(loggingError);
            }
        }
        return (error);
    }]);
}())

stackTrace 实际上是一个数组,我们取它的第一个元素,然后解析它以获取 bundle 的 URL(location 变量)和错误的(lineNumbercolumnNumber)位置。然后,基于这些值,我们构造一个 StackFrame,它可以在库中找到:https://github.com/stacktracejs/stackframe,并将其传递给前面提到的库中可以找到的 StackTraceGPS 类的 pinpoint 方法:https://www.stacktracejs.com/。这个方法将通过传递给回调函数的参数(answer)返回带有附加信息的错误实际位置,它将包含我们期望的内容,即“myFileName.js,第 123 行,第 34 列;”。就这样,问题完全解决了。当然,这种方法在开发环境中仍然有效,尽管有些冗余。

需要安装什么

  1. AspNetBundling:https://nuget.net.cn/packages/AspNetBundling/
  2. StackFrame.js:https://github.com/stacktracejs/stackframe
  3. StackTrace.js:https://www.stacktracejs.com/
  4. StackTrace-gps.js:https://www.stacktracejs.com/

结论

错误日志记录是一项非常常见的任务,任何开发人员都会遇到。收集客户端错误非常重要且必要,幸运的是,我们有很多钩子可供使用,例如:windows.onerror 处理程序或在使用 AngularJS 框架时的 $exceptionHandler。借助 StackTrace.js 库,我们可以恢复错误的堆栈跟踪,但这还不够。在生产环境中,我们经常使用带有压缩的 bundles,这会导致丢失实际(预期)错误的位置,并成为一个真正的问题。为了解决这个问题,我们应该为我们的 bundle 创建一个 .map 映射(AspNetBundling 将完成这项工作),然后使用 StackTrace-gps.js 库调用它来查找实际文件中的相应错误位置。

您可以通过其他框架的错误处理程序来简单地使用这种方法,但堆栈跟踪的创建以及 bundle 和压缩问题的解决将保持不变。

© . All rights reserved.