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

使用 JSNLog 记录 Angular 应用程序中的错误

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (6投票s)

2015年1月17日

CPOL

4分钟阅读

viewsIcon

33263

downloadIcon

375

介绍如何配置你的 Angular 应用程序使用 JSNLog,一个流行的 JavaScript 日志记录库

引言

当你不了解用户正在做什么以及看到什么时,调试客户端错误并不容易。此外,当错误发生时,客户甚至都看不到它们。他们只看到应用程序显示一些奇怪的东西,或者根本什么都不显示。AngularJS 很好地处理了这些错误;它会捕获客户端错误并在浏览器控制台中显示它们,让你的应用程序继续运行。问题是,即使是普通用户,除非他们是懂浏览器控制台以及如何打开它的高级用户,否则也不会知道错误发生了,也不知道错误的详细信息。

解决方案是什么?将错误、用户操作和上下文信息(如页面名称)记录在服务器端,这样你就可以轻松地查看和审查它们。本文的目标是向你展示如何在 AngularJS 应用程序中使用一个客户端日志库——JSNLog(由 Matt Perdeck 编写)来实现这一点。你可以找到 Matt 写的几篇关于 JSNLog 是什么以及如何使用的文章。我建议你先阅读他的文章;它们非常翔实,并且有大量关于如何使用 JSNLog 以及如何将其与 NLog、Log4Net 或 Elmah 等服务器端日志框架集成的信息。

JSNLog 的文档非常详尽。你可以在 http://js.jsnlog.com/Documentation/JSNLogJs 找到它。

本文还将帮助解决另一个问题,那就是当你拥有自己的自定义服务器端日志解决方案,并且不想切换到上述任何第三方库时。

为了演示目的,我编写了一个小程序,它将使用提供的 API 从 CodeProject.com 下载文章。

开始吧

首先,访问 http://js.jsnlog.com,下载独立的 jsnlog.min.js 版本并将其添加到我们的项目中。

其次,导航到 http://js.jsnlog.com/Documentation/GetStartedLogging/AngularJsErrorHandling,并将该页面上的所有代码复制到一个单独的文件中。

代码看起来会是这样

(function () {
    'use strict'

    // Create new module logToServer with new $log service
    angular.module('logToServer', [])
        // Make AngularJS do JavaScript logging through JSNLog so we can log to the server 
        // by replacing the $log service
        .service('$log', function () {
            this.log = function (msg) {
                JL('Angular').trace(msg);
            }
            this.debug = function (msg) {
                JL('Angular').debug(msg);
            }
            this.info = function (msg) {
                JL('Angular').info(msg);
            }
            this.warn = function (msg) {
                JL('Angular').warn(msg);
            }
            this.error = function (msg) {
                JL('Angular').error(msg);
            }
        })
        // Replace the factory that creates the standard $exceptionHandler service
        .factory('$exceptionHandler', function () {
            return function (exception, cause) {
                JL('Angular').fatalException(cause, exception);
                throw exception;
            };
        })
        // Add a factory to create the interceptor to the logToServer module
        .factory('logToServerInterceptor', ['$q', function ($q) {
            var myInterceptor = {
                'request': function (config) {
                    config.msBeforeAjaxCall = new Date().getTime();
                    return config;
                },
                'response': function (response) {
                    if (response.config.warningAfter) {
                        var msAfterAjaxCall = new Date().getTime();
                        var timeTakenInMs = msAfterAjaxCall - response.config.msBeforeAjaxCall;
                        if (timeTakenInMs > response.config.warningAfter) {
                            JL('Angular.Ajax').warn({
                                timeTakenInMs: timeTakenInMs,
                                config: response.config,
                                data: response.data
                            });
                        }
                    }
                    return response;
                },
                'responseError': function (rejection) {
                    var errorMessage = "timeout";
                    if (rejection.status != 0) {
                        errorMessage = rejection.data.ExceptionMessage;
                    }
                    JL('Angular.Ajax').fatalException({
                        errorMessage: errorMessage,
                        status: rejection.status,
                        config: rejection.config
                    }, rejection.data);
                    return $q.reject(rejection);
                }
            };
            return myInterceptor;
        }]);
})();

然后,将 logToServer 模块添加到你的主模块中(导入),并将新的拦截器添加到主模块的拦截器管道中

// Create module
angular.module('ArticlesModule', ['logToServer'])
    // Register the controller
    .controller('ArticlesController', ['$scope', '$http', ArticlesController])
    // Add the new interceptor to the interceptor pipeline of the main module
    .config(['$httpProvider', function ($httpProvider) {
        $httpProvider.interceptors.push('logToServerInterceptor');
    }]);

最后一步,我们需要告诉 JSNLog 将消息发送到哪个默认 URL。实际上,使用 RESTful Web 服务可能更好,但在演示应用程序中,一个普通的 .aspx 页面就足够了。我在 logToServer 模块本身中添加了以下代码,这样所有应用程序页面将使用相同的 URL

JL.setOptions({
    'defaultAjaxUrl': 'LogDetails.aspx'
})

在服务器端代码(无论是 .aspx 页面还是 Web 服务)中,你将使用自己的日志代码来保存错误。为了简单起见,我只将数据保存到文本文件中。

好了!你已经可以记录客户端错误了。

让我们测试一下我们的代码。我添加了 'use strict',所以所有变量都必须定义,否则会抛出异常。我在应用程序模块中添加了 myVariable ,日志记录结果如下

{"r":"","lg":[{"l":6000,
"m":"{\"stack\":\"ReferenceError: myVariable is not defined\\n    at new ArticlesController
(https://:4283/app.js:32:9)\\n    at Object.e [as invoke]
(https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.3.1/angular.min.js:36:365)\\n    at F.instance
(https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.3.1/angular.min.js:75:91)\\n    at
https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.3.1/angular.min.js:58:287\\n    at s
(https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.3.1/angular.min.js:7:408)\\n    at G
(https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.3.1/angular.min.js:58:270)\\n    at g
(https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.3.1/angular.min.js:51:172)\\n    at g
(https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.3.1/angular.min.js:51:189)\\n    at
https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.3.1/angular.min.js:50:280\\n    at
https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.3.1/angular.min.js:18:8\",
\"message\":\"myVariable is not
defined\",\"name\":\"ReferenceError\"}","n":"Angular","t":1421533414109}]}

这很有用,但让我们尝试改进一下。一个缺失的是我们不知道哪个页面记录了错误。我们可以使用所谓的请求 ID——参见上面的 "r":""。请求 ID 用于唯一标识每个请求,并且在 JSNLog 的 .NET 版本中被设置为一个随机数。恕我直言,用一个随机数来标识哪些日志消息属于哪个用户可能对某些网站来说不是一个解决方案,因为它并没有告诉你关于用户的任何信息。有时,了解用户的类型——普通用户或管理员/超级用户等——很重要。我们可以将任何任意信息放入请求 ID 字段,但我将存储页面名称。

JL.setOptions({
    'requestId': window.location.pathname.split("/").pop()
});

现在请求 ID 包含了页面名称:"r":"index.htm"。其余的都一样。

如果我们不想记录整个异常怎么办?那么,我们可以更改下面的行

JL('Angular').fatalException(cause, exception);

to

JL().log(4000, { 'stack': exception.stack, 'error': exception.message });

然后将记录以下内容

{"r":"Index.html","lg":[{"l":4000,
"m":"{\"stack\":\"ReferenceError: myVariable is not defined\\n    at new
ArticlesController (https://:4283/app.js:32:9)\\n    at Object.e [as invoke]
(https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.3.1/angular.min.js:36:365)\\n    at F.instance
(https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.3.1/angular.min.js:75:91)\\n    at
https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.3.1/angular.min.js:58:287\\n    at s
(https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.3.1/angular.min.js:7:408)\\n    at G
(https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.3.1/angular.min.js:58:270)\\n    at g
(https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.3.1/angular.min.js:51:172)\\n    at g
(https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.3.1/angular.min.js:51:189)\\n    at
https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.3.1/angular.min.js:50:280\\n    at
https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.3.1/angular.min.js:18:8\",
\"error\":\"myVariable is not
defined\"}","n":"","t":1421539948122}]}

我们可以改进的另一个方面是 AJAX 调用。在现代 JavaScript 应用程序中,AJAX 调用使用非常广泛,记录 AJAX 错误及其持续时间很重要。让我们将拦截器更改为如下所示

.factory('logToServerInterceptor', ['$q', function ($q) {
    var myInterceptor = {
        // The request function is called before the AJAX request is sent
        'request': function (config) {
            config.msBeforeAjaxCall = new Date().getTime();
            return config;
        },
        // The response function is called after receiving a good response from the server
        'response': function (response) {
            var msAfterAjaxCall = new Date().getTime();
            var timeTakenInMs = msAfterAjaxCall - response.config.msBeforeAjaxCall;
            JL('Angular.Ajax').info({
                url: response.config.url,
                timeTakenInMs: timeTakenInMs
            });
            return response;
        },
        // The responseError function is called when an error response was received, 
        // or when a timeout happened.
        'responseError': function (rejection) {
            var errorMessage = "unknown";
            JL('Angular.Ajax').fatalException({
                status: rejection.status,
                url: rejection.config.url,
                errorMessage: rejection.data.error
            });
            return $q.reject(rejection);
        }
    };
    return myInterceptor;
}]);

现在,如果 AJAX 调用成功,将记录以下内容

{"r":"index.htm","lg":[{"l":3000,"m":"{\"url\":\"https://api.codeproject.com/v1/Articles?page=1
\",\"timeTakenInMs\":606}","n":"Angular.Ajax","t":1421543626142}]}

如果 AJAX 调用失败

{"r":"index.htm","lg":
[{"l":6000,"m":"{\"status\":400,\"url\":\"https://api.codeproject.com/token\",
\"errorMessage\":\"invalid_client\"}","n":"Angular.Ajax","t":1421543588139}]}

你可能想知道如何记录用户操作、添加一些跟踪信息等。这很简单。你可以在任何地方添加日志代码。页面加载时间可以这样记录

if (!window.performance) {
    // IE 8 and below is not supported
    JL().warn('Performance object is not supported');
} else {
    var now = new Date().getTime();
    var pageLoadTime = now - window.performance.timing.navigationStart;

    // Log the page load time
    JL().info(window.location.pathname.split("/").pop() + ' load time-' + pageLoadTime + ' ms');
}

你将在日志文件中看到以下内容

{"r":"index.htm","lg":[{"l":3000,"m":"index.htm load time-384 ms","n":"","t":1421544517551}]}

结论

正如你所见,使用 JSNLog 库进行 JavaScript 日志记录非常容易,希望本文能帮助任何人立即开始。祝你日志愉快!

© . All rights reserved.