写点什么

源码分析 Sentry 用户行为记录实现过程

作者:南城FE
  • 2022 年 7 月 24 日
  • 本文字数:3146 字

    阅读完需:约 10 分钟

今日介绍前端异常监控利器 Sentry 平台中用户行为记录的源码实现过程,为什么使用 Sentry,可以看以前的文章https://xie.infoq.cn/article/40fd56e8c8fd296b9bd0d91aa

前言

在日常排查问题过程中,用户的行为操作记录能给到我们很大的参考及排查方向。目前市面上有的平台还不能记录用户的行为记录,Sentry 提供了很好的行为记录查询,如下图所示。从用户跳转页面,到点击元素请求接口,发生异常,如图所示,极大可能是因为接口相应的数据没有达到预期导致页面发送错误,本文将探讨 Sentry JS SDK 中是如何记录用户的操作行为记录。


源码实现

SDK 初始化时会调用init方法,最终init方法的 initAndBind 指向 getCurrentHub().bindClient()


Hub.prototype.bindClient = function (client) {    // 获取最后一个    var top = this.getStackTop();    // 把 new BrowerClient() 实例 绑定到top上    top.client = client;    if (client && client.setupIntegrations) {        client.setupIntegrations();    }};
复制代码


这里调用了默认的集成 client.setupIntegrations(),最终指向 @sentry/core/integration.js,这里的 options 即 sdk 初始化时传入并处理后的 options。


export function setupIntegrations(options) {    var integrations = {};    getIntegrationsToSetup(options).forEach(function (integration) {        integrations[integration.name] = integration;        setupIntegration(integration);    });    return integrations;}
复制代码


setupIntegration 调用 setupOnce 初始化拦截,这里的 setupOnce 是每个集成功能都有的能力。


export function setupIntegration(integration) {    if (installedIntegrations.indexOf(integration.name) !== -1) {        return;    }    integration.setupOnce(addGlobalEventProcessor, getCurrentHub);    installedIntegrations.push(integration.name);    logger.log("Integration installed: " + integration.name);}
复制代码


breadcrumbs 面包屑的 setupOnce 函数


@sentry/broswer/integrations/breadcrumbs.jssetupOnce 函数,可以看到对 console,dom 事件,网络请求,页面变化都做了拦截处理。


Breadcrumbs.prototype.setupOnce = function () {       var _this = this;       if (this._options.console) {           addInstrumentationHandler({               callback: function () {                   var args = [];                   for (var _i = 0; _i < arguments.length; _i++) {                       args[_i] = arguments[_i];                   }                   _this._consoleBreadcrumb.apply(_this, __spread(args));               },               type: 'console',           });       }       if (this._options.dom) {           addInstrumentationHandler({               ...,               type: 'dom',           });       }       if (this._options.xhr) {           addInstrumentationHandler({               ..,               type: 'xhr',           });       }       if (this._options.fetch) {           addInstrumentationHandler({               ...,               type: 'fetch',           });       }       if (this._options.history) {           addInstrumentationHandler({               ...,               type: 'history',           });       }   };
复制代码


addInstrumentationHandler 出自 @sentry/utils/instrument.js,最终调用了instrument方法,此方法实现了每种拦截类型的具体捕获逻辑。



function instrument(type) { if (instrumented[type]) { return; } instrumented[type] = true; switch (type) { case 'console': instrumentConsole(); break; case 'dom': instrumentDOM(); break; case 'xhr': instrumentXHR(); break; case 'fetch': instrumentFetch(); break; case 'history': instrumentHistory(); break; case 'error': instrumentError(); break; case 'unhandledrejection': instrumentUnhandledRejection(); break; default: logger.warn('unknown instrumentation type:', type); }}
复制代码


再看针对日志打印的具体拦截捕获回调函数,所有捕获的回调函数最终都会调用 getCurrentHub().addBreadcrumb 添加行为记录,只是不同类型的行为记录会传入不同的处理参数。


Breadcrumbs.prototype._consoleBreadcrumb = function (handlerData) {    var breadcrumb = {        category: 'console',        data: {            arguments: handlerData.args,            logger: 'console',        },        level: Severity.fromString(handlerData.level),        message: safeJoin(handlerData.args, ' '),    };    if (handlerData.level === 'assert') {        if (handlerData.args[0] === false) {            breadcrumb.message = "Assertion failed: " + (safeJoin(handlerData.args.slice(1), ' ') || 'console.assert');            breadcrumb.data.arguments = handlerData.args.slice(1);        }        else {            // Don't capture a breadcrumb for passed assertions            return;        }    }    getCurrentHub().addBreadcrumb(breadcrumb, {        input: handlerData.args,        level: handlerData.level,    });}
复制代码


getCurrentHub().addBreadcrumb 最终调用于 @sentry/hub/scope.js,在这里增加了时间的记录,以及最大记录条数的处理,最终将行为记录保存在 this._breadcrumbs(Scope)数组中。


Scope.prototype.addBreadcrumb = function (breadcrumb, maxBreadcrumbs) {    var mergedBreadcrumb = __assign({ timestamp: dateTimestampInSeconds() }, breadcrumb);    this._breadcrumbs =        maxBreadcrumbs !== undefined && maxBreadcrumbs >= 0            ? __spread(this._breadcrumbs, [mergedBreadcrumb]).slice(-maxBreadcrumbs)            : __spread(this._breadcrumbs, [mergedBreadcrumb]);    this._notifyScopeListeners();    return this;};
复制代码


在控制台打印全局注入的__SENTRY__对象可以在 hub 对象中的 _breadcrumbs 看到以下格式的数据。



最终在发起异常上报时,通过调用this.getStackTop()获取到当前scope中的数据一起上报,这里的 this.getStackTop().scope 即包括上述的 _breadcrumbs 行为数据,除此之前还有 _user 用户信息, _tags 标签数据, _extra 额外数据等。


Hub.prototype._invokeClient = function (method) {    var _a;    var args = [];    for (var _i = 1; _i < arguments.length; _i++) {        args[_i - 1] = arguments[_i];    }    var top = this.getStackTop();    if (top && top.client && top.client[method]) {        (_a = top.client)[method].apply(_a, __spread(args, [top.scope]));    }};
复制代码


最终异常上报的数据格式,如下图所示。


整体流程图

最后

整体源码分析就到这里了,Sentry 是一款功能强大的前端异常监控平台,涉及的功能及数据分析很多,有兴趣的同学可以尝试看看,看我觉得有用记得点个赞再走吧,收藏起来说不定哪天就需要了。

发布于: 2 小时前阅读数: 7
用户头像

南城FE

关注

还未添加个人签名 2019.02.12 加入

专注前端开发,分享前端知识

评论

发布
暂无评论
源码分析Sentry用户行为记录实现过程_前端_南城FE_InfoQ写作社区