关于埋点:当-sendBeacon-遇上-Blob

2014 年,W3C 公布了信标(Beacon)的规范草案最终征求意见稿(目前曾经是候选举荐草案)。该标准定义了一个异步非阻塞的数据上报接口,能够最大限度地缩小对其余要害操作的资源占用,同时保障申请能失常收回。同年,该接口就被引入了 Firefox 和 Chrome,即 navigator.sendBeacon(下文简称为 sendBeacon)。 在理论开发工作中,该接口最常见的应用场景就是数据埋点。与其余埋点技术计划相比,sendBeacon 的劣势在于: 不会跟业务代码抢占资源,而是在浏览器闲暇的时候再去发送;在页面卸载(敞开、刷新、跳转)时也能保障申请发送,同时不阻塞页面卸载。第一个配角——sendBeaconsendBeacon 的办法原型非常简单: navigator.sendBeacon(url, data);其中: data 是将要发送的数据,能够是 ArrayBuffer、ArrayBufferView、Blob、FormData、URLSearchParams 或字符串。URL 是 data 将要被发送到的网络地址。当数据胜利退出到传输队列时,返回值为 true,否则为 false。 一个简略的调用示例如下: const data = new FormData();data.append('id', '1');data.append('type', 'test');const result = navigator.sendBeacon(url, data);console.log(result);思考到 sendBeacon 可能会存在退出队列失败的状况,以及浏览器兼容性问题,一般来说还须要加上降级反对。 let result;if (typeof navigator.sendBeacon === 'function') { result = navigator.sendBeacon(url, data);}if (!result) { const xhr = new XMLHttpRequest(); xhr.open('post', url); xhr.send(data);}除此以外,sendBeacon 还有以下留神要点: 该办法的次要应用场景是将大量剖析数据发送给服务器,以确保申请可能疾速及时地实现,因而它会限度最大的负载体积。该办法总是以 HTTP POST 去发送申请,且无奈设置自定义申请头或其余与申请、响应相干的属性。该办法没有提供获取响应后果的形式。调用该办法时必须以 navigator 作为上下文对象,否则会抛出 Illegal invocation 的异样。第二个配角——Blob如果须要通过 sendBeacon 发送 JSON 数据,能够这样调用: navigator.sendBeacon( url, new Blob([JSON.stringify(data)], { type: 'application/json' }));发送这个申请时,浏览器会把 content-type 申请头设为 application/json。 ...

April 27, 2023 · 1 min · jiezi

关于埋点:Vue-项目声明式主动埋点

公司零碎需要加上埋点性能,用来统计各页面性能的应用状况。于是,联合网上材料以及之前应用埋点零碎的经验,认真钻研钻研。 调研埋点分类常见的埋点类型有三种 代码埋点 通过 JavaScript 代码被动将所须要的信息上报给服务器。长处:能够准确的上报所需的数据,对于大量埋点需要较为适合。毛病:代码遍布我的项目各处,不好保护治理。且埋点只能通过开发人员手动实现。可视化埋点 须要另外一个可视化埋点圈选零碎来圈选须要埋点的 DOM 元素。而后通过在零碎中集成 SDK 来被动上报这些区域的埋点信息。其实算是另一种意义上的代码埋点。长处:有圈选零碎能够让产品、运维同学自行决定埋点区域。毛病:适用范围无限,如内网零碎、挪动端 Hybrid 页面这些就很难用内部的可视化埋点来做。无埋点 其实也叫全量埋点,即全局监听系统事件,把用户所有行为都进行上报。长处:行为数据记录全面,无需减少或保护埋点代码。毛病:上报数据量大,对服务器有肯定压力。且无奈准确上报某一性能的特停数据。埋点指标数据监控:通过埋点让产品运维同学晓得我的项目以后的具体情况,从而有针对性的去优化我的项目。异样监控:从开发角度去收集我的项目中产生的 JS 报错、接口报错等异常情况。发现问题、解决问题、优化我的项目。性能监控:收集我的项目运行中的各种性能指标,如白屏工夫、首屏加载工夫、接口申请工夫等等。埋点 SDK 实现猜测以我之前工作中用到过的埋点零碎 GrowingIO 为例。咱们能够通过它的 SDK 文档 来验证下面的实践。 它通过全局引入 JS 代码的形式来进行集成,它会在 window 全局对象下加上一个 gio 函数解决各种埋点行为。因为埋点零碎会为很多我的项目服务,所以须要初始化的时候加上 gio('init', 'your projectId', {})。它要求在须要圈选的 DOM 元素上 data-growing-container 属性,这其实是 HTML 元素的 dataset 属性,能够用来对元素进行自定义数据属性的读写操作。有了圈选标记,埋点事件拦挡的时候就能够指哪打哪了。它通过 gio('track', eventId, eventLevelVariables); 函数实现了被动埋点行为,这个天然是必不可少的。总有埋点需要是主动埋点做不了的。它的无埋点记录的是所有元素的点击量和浏览量,应该是全局监听了元素的点击和可视事件。它的可视化圈选是通过 XPath 来惟一定位一个元素的,那么可视化圈选其实就是将指标 DOM 的 xPath 保存起来,在埋点的时候去获取指定 DOM 元素的点击量和访问量。(对于 xpath 的应用能够看 Introduction to using XPath in JavaScript - XPath | MDN)我的埋点计划抉择因为我的项目的埋点只须要记录一些指定的行为,所以全埋点计划被我 PASS 了。同时也没有必要另外写一个页面去做埋点的圈选,最终,抉择了最简略粗犷地被动埋点。 被动埋点 1.0一开始埋点其实很简略,通过在 JavaScript 代码中写埋点代码来进行实现。 ...

March 1, 2023 · 2 min · jiezi

关于埋点:极光笔记-埋点体系建设与实施方法论

PART 01 前 言随着网络技术的倒退,从粗暴型到精细化经营型,再到当初的数字化经营,数据变得越来越细分和重要,不仅能够进行策略调整,还能够实现自动化的精细化经营。而数据价值的终点就是埋点,只有正当地埋点,标准地上报,数据才会产生价值。 PART 02 数据埋点的必要性正当、无效的数据埋点以及主观的数据闭环反馈,能够帮忙企业从不同维度剖析用户,构建用户数据体系,为企业提供决策、营销、和精细化经营撑持。 决策:实时把握外围指标,定时定期报表推送,撑持业务决策,应答市场变动。营销:找寻优质渠道资源,调整营销策略,晋升线索转化率,从而进步市场整体ROI。经营:精准定位不同用户群,个性化营销疏导,让用户经营对症下药。产品:追踪用户行为,剖析外围步骤转化,疾速验证改版计划,晋升新老用户转化。 PART 03 什么是数据埋点指针对特定场景的用户行为或事件进行捕捉、解决和上报的过程。在整个过程捕捉的所需信息,用以跟踪用户的应用状况,最初剖析这一系列数据作为领导决策、产品迭代、营销经营的无效撑持。埋点形式次要分为三类:代码埋点、可视化埋点和全埋点。代码埋点: 指开发工程师将埋点联合到代码逻辑中,在APP或者界面初始化的时候,初始化第三方数据分析服务商的SDK,而后在某个事件产生时就调用SDK外面相应的数据发送接口发送数据,此种形式是从代码逻辑上捕捉用户行为并且上报数据。 可视化埋点: 是一种不须要额定去写代码的埋点形式,而是由业务/经营人员通过拜访剖析平台的埋点圈选性能,“圈”出须要对用户行为进行捕获的控件,并给出相应的事件命名。当圈选结束后,这些配置会从平台侧同步到所有用户终端,当终端有触发已圈选的事件,SDK就会依照圈选的配置主动进行用户行为数据的采集和发送。 全埋点: 指事后收集用户的所有行为数据,在集成采集SDK后,SDK便间接开始捕获用户在终端利用上的所有行为数据并全副上报,在后续应用数据的时候就能够从数据库中间接查问。 埋点数据起源:客户端数据:页面/弹窗曝光、点击数据。服务端数据:装置数据,领取数据,业务数据等。 埋点形式和数据起源的关系如下表所示: PART 04 用户行为数据埋点设计&方法论用户行为数据埋点设计&方法论埋点设计流程: 1、事件埋点设计之前,须要先理解业务场景,梳理和确认业务流程、用户操作门路和各种不同的细分场景。依据用户在产品上具象的操作形式和流程,定义用户行为门路。场景拆解策略:分明业务,制订规范,布局指标,确定策略,创立打算 分明业务: 设计埋点的前提须要清晰理解客户端的业务场景,不仅须要相熟用户的操作,不同用户门路下有什么页面,经营位、按钮、弹窗,而且还须要理解产品所有性能,局部或者极少用户应用的业务也要做到成竹在胸。 制订规范: 分明业务后,须要针对每个业务模块/分类制订指标,依据指标拆解为一个个确定的数据指标,而后把指标细分成一个或者多个埋点事件,即多个埋点事件都能间接影响指标。 举例:如下图所示,如果需姚须要晋升新用户的注册率,须要拆解的埋点事件有:注册页面曝光、注册页_手机号是否输出、注册页_是否右滑验证、注册页_点击获取验证码、注册页_是否发送验证码、注册页_是否输出验证码、注册页_是否输出明码、注册页_是否勾选协定、注册页_提交按钮点击 2、在充沛理解终端的业务状况和场景拆解后,输入要晋升的指标和特定事件,指标和事件的拆解,可能是一对一或者一对多的关系,即一个指标可能由一个或者多个埋点事件组成;其次,须要充分考虑到这些事件会用于哪些指标的剖析,须要上报哪些字段或属性,具体分析时的外围利用的是哪些。指标体系优先级如下图所示: 3、设计埋点事件,针对拆解的业务场景,梳理指标和事件,抽取指标与事件之间的关联,再联合相应的属性,确定要上报的事件、属性以及上报机会等5因素设计埋点事件。 4、埋点施行,开发共事依据埋点设计施行埋点并上报,须要思考事件埋点形式和事件上报触发逻辑;埋点形式分为:全埋点、代码埋点、可视化埋点三种。事件上报的触发逻辑可做如下分类: 依据业务场景做最优的触发上报逻辑。 5、依据上报的埋点数据,验证埋点的准确性,从流量埋点事件(如点击、曝光)上报到业务、财务指标逐步递升,汇聚成多流量、内容、业务、财务的数据指标,构建用户全方位的数据指标体系;埋点测试验收应该保障埋点数据正确性、程序性、完整性: 正确性: 最根底的是确认是否有数据上报,其次,检查数据内容与字段是否与埋点设计文档统一;程序性: 数据上报正确,还须要查看上报的程序是否正确;完整性: 测试时,针对多场景要全副测试,如申请验证码的各个场景都应该上报。 6、数据荡涤、存储、聚合转换、剖析;埋点上线,并不象征就完结,重点要察看对应的指标是否精确上报,对业务是否指导作用,与优化前的版本相比拟是否有所改善。很多时候可能不能一步到位就把问题解决掉,须要迭代优化,一直通过数据跟踪来修改优化策略,达到最终设计指标。 PART 05 如何做好埋点设计1、基于业务场景,埋点5因素:WHO:即参加这个事件的用户是谁,如:用户ID,设施ID WHEN:即这个事件产生的工夫,如:工夫戳WHAT: 形容了一个事件具体是什么,如:事件名称/页面题目名HOW:即用户从事这个事件的形式,如:上报机会,页面属性WHERE:IP、国家、省、市区等用户属性,如:IP地址 每个事件上报都必须蕴含上述5个因素。 举例:某APP须要上报【商城_XX经营位】被点击的埋点事件,如下图所示: 点击事件须要上报的5因素:①是哪个用户ID/设施ID:用户ID:001,苹果终端②什么时候点做的:2022年12月16日10时46分05秒或工夫戳③是什么事件:XX经营位点击④怎么点击的:经营位ID:0A_001,经营位名称:商城_首页轮播⑤在哪里点击的:IP地址:101.XX.XX.XX 上述5因素合成一条数据上报到数据系统。 2、要上报的埋点,归纳起来总共分为以下三类:①曝光事件:页面曝光、弹窗曝光、按钮/文案曝光、经营位/banner位曝光等;举例:某iOS利用,【举荐】栏目,A经营位的曝光数据;②点击事件:经营位/banner位点击、按钮/文案点击;举例:某iOS利用,【举荐】栏目,A经营位的点击数据, 并上报经营位的内容id和内容;③非凡事件/属性:服务端上报;非凡场景,多个不确定选择项上报。举例:某iOS利用,【我的】栏目-问题反馈页面-问题勾选,具体勾选项数据;属性值通过枚举上报。上述举例的埋点设计: 3、埋点的整体准则&标准:①事件名称尽可能简略、清晰,升高应用门槛;②同个终端/平台,如多个场景都用到一样的事件,通常倡议用属性作为辨别;举例:弹窗,是否确定/勾销。该弹窗在三个页面都呈现。埋点设计:如上例子所示,一个弹窗按钮点击事件,用页面类型(自定义属性)属性作为辨别,可用一个埋点上报3个页面的弹窗按钮点击数据。 4、数据埋点流程: PART 06 总结基于埋点的重要性,在于埋点采集自身,应该被当成独立的研发业务来做,而不只是一个产品研发过程中的附属品,属于可有可无、顺带做一下的工作项。埋点是为了更好地应用数据,而应用数据是为了更好的服务于业务。正当的数据埋点和剖析能够帮忙企业从不同维度剖析用户,构建用户数据体系,为企业提供业务决策、营销转化、产品迭代和精细化经营撑持。 对于极光极光(Aurora Mobile,纳斯达克股票代码:JG)成立于2011年,是中国当先的客户互动和营销科技服务商。成立之初,极光专一于为企业提供稳固高效的音讯推送服务,凭借先发劣势,曾经成长为市场份额遥遥领先的挪动音讯推送服务商。随着企业对客户触达和营销增长需要的不断加强,极光前瞻性地推出了音讯云和营销云等解决方案,帮忙企业实现多渠道的客户触达和互动需要,以及人工智能和大数据驱动的营销科技利用,助力企业数字化转型。

February 24, 2023 · 1 min · jiezi

关于埋点:一文读懂字节跳动埋点验证平台

序言埋点数据作为举荐、搜寻、产品优化的基石,其数据品质的重要性显而易见,而要保障埋点数据的品质,埋点验证则首当其冲。工欲善其事必先利其器,要做好埋点验证会面临很多技术挑战:易用性、准确性、实时性、稳定性、扩展性,如何攻克这些挑战呢,其实还是技术,这也是本文的宗旨所在。目前埋点验证已在字节外部失去宽泛应用,通过一键扫码开启验证、实时上报验证、主动生成验证报告,解决了埋点数据验证难、埋点品质保障难的问题。埋点验证流程埋点生命周期:4+6 4个角色:PM、DA、RD、QA6个节点:提出需要、设计埋点、开发埋点、测试埋点、上报埋点、剖析埋点埋点验证流程:3+3+33 个角色:DA、RD、QA3 个节点:设计埋点、测试埋点、验收埋点3 个物料:埋点验证计划、埋点验证工具、埋点验证报告 技术架构产品流程先简略介绍一下产品,以便大家能对平台有整体意识,不便大家更加轻松地了解技术,平台次要包含三局部:埋点验证计划、埋点验证工具、埋点验证报告,三者相辅相成,极大的升高了用户的埋点验证老本。附埋点验证工具图 技术架构图埋点验证的链路很长,能够简略概括为三个环节:埋点上报、埋点接管、埋点验证,每个环节都有肯定的复杂性,此处先介绍整体流程,让大家能够疾速对全流程有所意识。其次将次要聚焦于“埋点验证”环节,此环节的重中之重是埋点验证引擎,它包含 4 个局部:规定生成器、规定选择器、埋点验证器和埋点推送器,通过对埋点验证引擎的详解让大家对“埋点如何验证”有更深的了解。 埋点上报环节重点是丰盛的 SDK(客户端、服务端、JS、Chrome 插件),要做到简略易用并且保障埋点实时上报。埋点接管环节重点是数据接管服务(客户端-applog、Web 端-mcs、服务端-databus)、数据保留服务(音讯队列),要保障服务稳固并且保障埋点不失落。埋点验证环节重点是埋点验证引擎,要确保服务高性能并且保障埋点验证后果的准确性。 规定生成器规定生成器将“埋点验证计划”转换为“验证规定”。埋点验证计划是验证规定的逻辑视图,不便用户操作,升高验证规定的编写和保护老本。通过逻辑视图和物理视图两层逻辑,确保了埋点验证引擎底层不受业务变动的影响。 埋点计划埋点验证计划反对 2 种: 按需要验证:即新建需要打算,针对某次需要验证按元数据验证:即按元数据验证,元数据是指所有需要的并集按元数据验证:埋点名称:video_play参数信息 (名称、类型、是否必填、值校验、是否是场景条件)enter_from,string,必传,固定值(login),是duration,integer,必传,值无限度,否type,integer,必传,枚举(1,2,3),否埋点数据:{ "app_id":100, "event":"click", "params":{ "enter_from":"login", "duration":1, "type":3 }}埋点规定: { "app_id":100, "event_name":"video_play", "logical_filter":{ "enter_from":"login" }, "meta":{ "required_field":[ "duration", "enter_from", "type" ], "scene":{ "condition":"enter_from=login", "name":"登录页" }, "validate_field":[ "duration", "enter_from", "type" ] }, "physical_validation":"{\"$schema\":\"https://json-schema.org/draft/2019-09/schema\",\"type\":\"object\",\"properties\":{\"params\":{\"type\":\"object\",\"properties\":{\"duration\":{\"type\":\"integer\"},\"enter_from\":{\"type\":\"string\",\"enum\":[\"login\"]},\"type\":{\"type\":\"integer\",\"enum\":[1,2,3]}},\"required\":[\"duration\",\"enter_from\",\"type\"]}},\"required\":[\"params\"]}", "source":"schema_scene"}埋点规定字段阐明 app_id:利用idevent_name:埋点名称logical_filter:用于“规定选择器”physical_validation:用于“埋点验证器”source:辨别规定起源:按需要验证、按元数据验证规定选择器规定选择器将根据“埋点”中的要害信息,从“验证规定池”中抉择出对应的“埋点验证规定”。 抉择逻辑:具体数据参考“规定生成器” 依据“埋点数据”中app_id和event从“验证规定池”中筛选出“匹配的规定”将“埋点数据”的parms字段和“匹配的规定”的login_filter规字段进行匹配,抉择出最终的“埋点验证规定” 埋点验证器埋点验证器将根据“根底验证规定”以及“规定选择器”产出的“埋点验证规定”,对“埋点数据”进行验证并产出“验证后果”。 根底验证规定:埋点是否注销;埋点是否禁用;是否是debug埋点;埋点验证规定:参数是否失落;参数类型是否正确;参数取值是否合乎预期:枚举、范畴、正则;埋点验证后果:验证后果提供双语格局,用户可自行抉择中文或者英文; 埋点推送器埋点推送器将“埋点验证后果”推送到前端,推送的过程存在数据交互频繁、数据体积大、数据传输稳定性的要求,这里咱们自建 Push 服务进行数据传输,保证数据实时可达。 技术挑战易用性:疾速接入埋点验证,疾速开始埋点验证准确性:埋点验证后果精确、用户可信实时性:埋点数据实时可见稳定性:埋点数据牢靠不失落扩展性:疾速接入新的埋点数据格式 易用性:疾速接入埋点验证,疾速开始埋点验证SDK疾速接入埋点验证SDK提供“埋点验证开关”,客户端集成SDK的时候,可依据不同环境来配置是否开启“埋点验证开关”SDK层判断如果开启“埋点验证开关”,埋点数据会双发,此过程对业务是通明的 双发的起因或者为什么不从“线上埋点通道”取数?这里次要思考两个起因: “线上埋点通道”数据量太大SDK层线上上报逻辑是采纳微批的模式,默认1分钟从客户端上报一次,而埋点验证要求实时性,所以采纳独自的通道 扫码连贯疾速开始埋点验证连贯流程 建设WS连贯:服务端和验证平台建设长连贯,用于通信ws_id:验证平台依据ws_id生成二维码扫码:客户端扫描二维码获取并关上验证开关:客户端获取设施信息并且关上埋点验证开关上报device_id:客户端将长连贯信息和设施信息上报至服务端下发device_id:服务端将设施信息推送到验证平台开始验证:埋点验证平台进入验证阶段上报埋点:客户端开始上报埋点推送埋点:服务端将埋点推送到验证平台下发原理 客户端上报的埋点数据中含有设施信息用户通过扫码在验证平台回填设施信息服务端接管到埋点数据后,将埋点数据中的设施信息和验证平台的设施信息进行匹配,如果匹配则将埋点数据进行下发准确性埋点验证后果精确、用户可信埋点验证引擎必须保障埋点验证后果的准确性,能力升高验证老本。针对埋点数据自身的格局验证,咱们采纳了JsonSchema作为验证伎俩,以反对欠缺的验证规定、可信的验证后果。上文中的“规定生成器”、“规定选择器”、“埋点验证器”也都在肯定水平上保障了埋点验证后果的准确性。埋点计划 event:video_play埋点名称:video_play参数信息 (名称、类型、是否必填、值校验、是否是场景条件)enter_from,string,必传,固定值(login),是duration,integer,必传,值无限度,否type,integer,必传,枚举(1,2,3),否埋点规定jsonSchema{ "$schema":"https://json-schema.org/draft/2019-09/schema", "type":"object", "properties":{ "params":{ "type":"object", "properties":{ "duration":{ "type":"integer" }, "enter_from":{ "type":"string", "enum":[ "login" ] }, "type":{ "type":"integer", "enum":[ 1, 2, 3 ] } }, "required":[ "duration", "enter_from", "type" ] } }, "required":[ "params" ]}埋点数据event:video_play{ "app_id":100, "event":"click", "params":{ "enter_from":"login", "duration":1, "type":3 }}验证后果event:video_play ...

August 8, 2022 · 1 min · jiezi

关于埋点:字节跳动埋点数据流建设与治理实践

更多技术交换、求职机会、试用福利,欢送关注字节跳动数据平台微信公众号,回复【1】进入官网交换群本文将介绍字节跳动在埋点数据流业务场景遇到的需要和挑战以及具体实际,蕴含埋点数据流简介、埋点数据流建设实际、埋点数据流治理实际以及将来布局。关注字节跳动数据平台微信公众号,回复【0627】取得本次分享资料。 埋点数据流埋点数据流在字节跳动埋点数据流次要解决的数据是埋点,埋点也叫Event Tracking,是数据和业务之间的桥梁,也是数据分析、举荐、经营的基石。 用户在应用 App 、小程序、 Web 等各种线上利用时产生的用户行为数据次要通过埋点的模式进行采集上报,按不同的起源能够分为: 客户端埋点Web端埋点服务端埋点埋点通过埋点收集服务接管到MQ,通过一系列的Flink实时ETL对埋点进行数据标准化、数据荡涤、数据字段裁减、实时风控反作弊等解决,最终散发到不同的上游。上游次要包含举荐、广告、ABTest、行为剖析零碎、实时数仓、离线数仓等。因为埋点数据流处在整个数据处理链路的最上游,所以决定了“稳定性”是埋点数据流最为关注的一点。字节跳动的埋点数据流规模字节跳动埋点数据流的规模比拟大,体现在以下几个方面: 接入的业务数量很多,包含抖音、今日头条、西瓜视频、番茄小说在内的多个App和服务,都接入了埋点数据流。 流量很大,以后字节跳动埋点数据流峰值流量超过1亿每秒,每天解决超过万亿量级埋点,PB级数据存储增量。 ETL工作规模体量较大,在多个机房部署了超过1000个Flink工作和超过1000个MQ Topic,应用了超过50万Core CPU资源,单个工作最大超过12万Core CPU,单个MQ Topic最大达到10000个partition。 那么在这么微小的流量和工作规模下,埋点数据流次要解决的是哪些问题呢?咱们来看几个具体的业务场景。 业务场景UserAction ETL在举荐场景中,因为埋点品种多、流量微小,而举荐只关注其中局部埋点,因而须要通过UserAction ETL对埋点流进行解决,对这个场景来说有两个需要点: 数据流的时效性ETL规定动静更新为了晋升上流举荐零碎的解决效率,咱们在数据流配置ETL规定对举荐关注的埋点进行过滤,并对字段进行删减、映射、标准化等荡涤解决,将埋点打上不同的动作类型标识,解决之后的埋点外部个别称为UserAction。UserAction与服务端展示、Feature等数据会在举荐Joiner工作的分钟级窗口中进行拼接解决,产出instance训练样本。举个例子:一个客户端的文章点赞埋点,形容了一个用户在某一个工夫点对某一篇文章进行了点赞操作,这个埋点通过埋点收集服务进入ETL链路,通过UserAction ETL解决后,实时地进入举荐Joiner工作中拼接生成样本,更新举荐模型,从而晋升用户的应用体验。 如果产出UserAction数据的ETL链路呈现比拟大的提早,就不能在拼接窗口内及时地实现训练样本的拼接,可能会导致用户体验的降落,因而对于举荐来说,数据流的时效性是比拟强的需要。而举荐模型的迭代和产品埋点的变动都可能导致UserAction ETL规定的变动,如果咱们把这个ETL规定硬编码在代码中,每次批改都须要降级代码并重启相干的Flink ETL工作,这样会影响数据流的稳定性和数据的时效性,因而这个场景的另一个需要是ETL规定的动静更新。 数据分流抖音的埋点Topic晚顶峰超过一亿每秒,而上游电商、直播、短视频等不同业务关注的埋点都只是其中一部分。如果每个业务都别离应用一个Flink工作去生产抖音的全量埋点去过滤出本人关注的埋点,会耗费大量的计算资源,同时也会造成MQ集群带宽扇出十分重大,影响MQ集群的稳定性。 因而咱们提供了数据分流服务,实现上是咱们应用一个Flink工作去生产上游埋点Topic,通过在工作中配置分流规定的形式,将各个业务关注的埋点分流到上游的小Topic中提供给各业务生产,缩小不必要的资源开销,同时也升高了MQ集群出带宽。 分流需要大多对SLA有肯定要求,断流和数据提早可能会影响上流的举荐成果、广告支出以及数据报表更新等。另外随着业务的倒退,实时数据需要日益减少,分流规定新增和批改变得十分频繁,如果每次规定变动都须要批改代码和重启工作会对上游造成较大影响,因而在数据分流这个场景,规定的动静更新也是比拟强的需要。 容灾降级另一个场景是容灾降级。数据流容灾首先思考的是避免单个机房级别的故障导致埋点数据流齐全不可用,因而埋点数据流须要反对多机房的容灾部署。其次当呈现机房级别的故障时,须要将故障机房的流量疾速调度到可用机房实现服务的容灾复原,因而须要埋点数据流具备机房间疾速切流的能力。 而数据流降级次要思考的是埋点数据流容量不足以承载全副流量的场景,比方春晚流动、电商大促这类有较大突发流量的场景。为了保障链路的稳定性和可用性,须要服务具备被动或者被动的降级能力。 埋点数据流遇到挑战挑战次要是流量大和业务多导致的。流量大服务规模就大,不仅会导致老本治理的问题,还会带来单机故障多、性能瓶颈等因素引发的稳定性问题。而上游业务多、需要变动频繁,举荐、广告、实时数仓等上游业务对稳定性和实时性都有比拟高的要求。在流量大、业务多这样的背景下,如何保障埋点数据流稳定性的同时降低成本、提高效率,是埋点数据流稳定性治理和老本治理面对的挑战。 埋点数据流建设实际上文咱们理解了埋点数据流的业务场景和面对的挑战,接下来会介绍埋点数据流在ETL链路建设和容灾与降级能力上的一些实际。 ETL链路建设倒退历程埋点数据流ETL链路倒退到当初次要经验了三个阶段。第一个阶段是2018年以前,业务需要疾速迭代的晚期阶段。那时咱们次要应用PyJStorm与基于Python的规定引擎构建次要的流式解决链路。特点是比拟灵便,能够疾速反对业务的各种需要,随同着埋点量的疾速上涨,PyJStorm暴露出很多稳定性和运维上的问题,性能也不足以撑持业务增长。2018年外部开始大力推广Flink,并且针对大量旧工作应用PyJStorm的状况提供了PyJStorm到PyFlink的兼容适配,流式工作托管平台的建设肯定水平上也解决了流式工作运维治理问题,数据流ETL链路也在2018年全面迁徙到了PyFlink,进入到Flink流式计算的新时代。 第二个阶段是2018年到2020年,随着流量的进一步上涨,PyFlink和kafka的性能瓶颈以及过后应用的JSON数据格式带来的性能和数据品质问题纷纷显现出来。与此同时,上流业务对数据提早、数据品质的敏感水平一劳永逸。咱们不仅对一些痛点进行了针对性优化,还破费一年多的工夫将整个ETL链路从PyFlink切换到Java Flink,应用基于Groovy的规定引擎替换了基于Python的规定引擎,应用Protobuf代替了JSON,新链路相比旧链路性能晋升了数倍。同时大数据开发平台和流量平台的建设晋升了埋点数据流在工作开发、ETL规定治理、埋点治理、多机房容灾降级等多方面的能力。 第三个阶段是从2021年开始至今,进一步晋升数据流ETL链路的性能和稳定性,在满足流量增长和需要增长的同时,升高资源老本和运维老本是这一阶段的次要指标。咱们次要从三个方面进行了优化。 优化了引擎性能,随着流量和ETL规定的一直减少,咱们基于Groovy的规定引擎应用的资源也在一直减少,所以基于Janino对规定引擎进行了重构,引擎的性能失去了十倍的晋升。 基于流量平台建设了一套比较完善的埋点治理体系,通过埋点下线、埋点管控、埋点采样等伎俩升高埋点老本。 将链路进行了分级,不同的等级的链路保障不同的SLA,在资源有余的状况下,优先保障高优链路。 接下来是咱们2018至2020年之间埋点数据流ETL链路建设的一些具体实际。 基于规定引擎的Flink ETL在介绍业务场景时,提到咱们一个次要的需要是ETL规定的动静更新,那么咱们来看一下埋点数据流Flink ETL 工作是如何基于规定引擎反对动静更新的,如何在不重启工作的状况下,实时的更新上下游的Schema信息、规定的解决逻辑以及批改路由拓扑。首先,咱们在流量平台上配置了上下游数据集的拓扑关系、Schema和ETL规定,而后通过ConfigCenter将这些元数据发送给Flink ETL Job,每个Flink ETL Job的TaskManager都有一个Meta Updater更新线程,更新线程每分钟通过RPC申请从流量平台拉取并更新相干的元数据,Source operator从MQ Topic中生产到的数据传入ProcessFunction,依据MQ Topic对应的Schema信息反序列化为InputMessage,而后进入到规定引擎中,通过规定索引算法匹配出须要运行的规定,每条规定咱们形象为一个Filter模块和一个Action模块,Fliter和Action都反对UDF,Filter筛选命中后,会通过Action模块对数据进行字段的映射和荡涤,而后输入到OutputMessage中,每条规定也指定了对应的上游数据集,路由信息也会一并写出。 当OutputMessage输入到Slink后,Slink依据其中的路由信息将数据发送到SlinkManager治理的不同的Client中,而后由对应的Client发送到上游的MQ中。 规定引擎规定引擎为埋点数据流ETL链路提供了动静更新规定的能力,而埋点数据流Flink ETL Job应用的规定引擎也经验了从Python到Groovy再到Janino的迭代。因为Python脚本语言自身的灵活性,基于Python实现动静加载规定比较简单。通过Compile函数能够将一段代码片段编译成字节代码,再通过eval函数进行调用就能够实现。但Python规定引擎存在性能较弱、规定不足治理等问题。 迁徙到Java Flink后,在流量平台上对立治理运维ETL规定以及schema、数据集等元数据,用户在流量平台编辑相应的ETL规定,从前端发送到后端,通过一系列的校验最终保留为逻辑规定。引擎会将这个逻辑规定编译为理论执行的物理规定,基于Groovy的引擎通过GroovyClassLoader动静加载规定和对应的UDF。尽管Groovy引擎性能比Python引擎晋升了多倍,但Groovy自身也存在额定的性能开销,因而咱们又借助Janino能够动静高效地编译Java代码间接执行的能力,将Groovy替换成了Janino,同时也将解决Protobuf数据时应用的DynamicMessage替换成了GeneratedMessage,整体性能晋升了10倍。 除了规定引擎的迭代,咱们在平台侧的测试公布和监控方面也做了很多建设。测试公布环节反对了规定的线下测试,线上调试,以及灰度公布的性能。监控环节反对了字段、规定、工作等不同粒度的异样监控,如规定的流量稳定报警、工作的资源报警等。 Flink拆分工作规定引擎的利用解决了埋点数据流ETL链路如何疾速响应业务需要的问题,实现了ETL规定的动静更新,从而批改ETL规定不须要批改代码和重启工作。但规定引擎自身的迭代、流量增长导致的资源扩容等场景,还是须要降级重启Flink工作,导致上游断流。 除了重启断流外,大工作还可能在重启时遇到启动慢、队列资源有余或者资源碎片导致起不来等状况。 针对这些痛点咱们上线了Flink拆分工作,实质上是将一个大工作拆分为一组子工作,每个子工作按比例去生产上游Topic的局部Partition,按雷同的逻辑解决后再别离写出到上游Topic。 举个例子:上游Topic有200个Partition,咱们在一站式开发平台下来配置Flink拆分工作时只须要指定每个子工作的流量比例,每个子工作就能主动计算出它须要生产的topic partition区间,其余参数也反对按流量比例主动调整。 拆分工作的利用使得数据流除了规定粒度的灰度公布能力之外,还具备了Job粒度的灰度公布能力,降级扩容的时候不会产生断流,上线的危险更可控。同时因为拆分工作的各子工作是独立的,因而单个子工作呈现反压、Failover对上游的影响更小。另一个长处是,单个子工作的资源使用量更小,资源能够同时在多个队列进行灵便的部署。 容灾与降级能力建设说到ETL链路建设,埋点数据流在容灾与降级能力建设方面也进行了一些实际。首先是容灾能力的建设,埋点数据流在Flink、MQ、Yarn、HDFS等组件反对多机房容灾的根底上实现了多机房容灾部署,并筹备了多种流量调度的预案。 失常状况下流量会平均打到多个机房,MQ在多个机房间同步,Flink ETL Job默认从本地MQ进行生产,如果某个机房呈现故障,咱们依据状况能够抉择通过配置下发的形式从客户端将流量调度到其余非受灾机房,也能够在CDN侧将流量调度到其余非受灾机房。埋点数据流ETL链路能够分钟级地进入容灾模式,迅速将故障机房的Flink Job切换到可用的机房。 ...

June 27, 2022 · 1 min · jiezi

关于埋点:得物技术埋点自动化验证的探索和最佳实践

背景埋点对电商类app的业务倒退始终有着重要的指导作用,然而其简单的数据组成使得它的稳定性难以失去保障,往往业务逻辑的一些重构就会导致一些埋点属性甚至是整个埋点的失落。 也正是因为埋点具备多个数据源,惯例的自动化验证只能验证埋点是否存在,无奈跟业务场景匹配。而对于人工排查来说,尽管能解决和场景匹配的问题,然而像不同埋点的属性之间的关联或者属性和接口字段的关联这类简单的校验做起来也十分的艰难。 本文将介绍咱们是如何通过teslaLab+埋点验证平台实现了埋点的主动回归以及多维度的埋点校验,并在最近三个版本中累积发现了数十个埋点问题。 安卓 IOS 痛点一个埋点中的数据次要由三个局部组成: 接口下发数据用户行为本地运行的代码要想验证一个埋点是否合乎预期的设计,要害的难点就在于如何固定住这三个数据源,使其每次生成的后果都保持一致或者合乎咱们预约的规定。因而咱们别离采纳了以下计划来实现埋点生成时数据源的固定 接口数据:对接口mock实现了对接口数据的录制&回放用户行为数据:通过谷歌的uiautomator来实现用户行为的录制&回放本地代码:和ab平台买通,通过动静调整ab配置下发接口来最大限度上固定客户端ab试验相干的代码。概览名词解释teslaLab: 是一款无线测试工具(MAC/WIN利用),提供开箱即用的无线性能 /体验 /UI自动化 等专项测试工具。ubt-verification:安卓侧实现埋点数据录制和接口Mock性能的SDK。测试场景:即校验规定组,对应着一个测试用例,蕴含了这个case的所有埋点校验规定mock记录:蕴含了一个case在运行过程中申请的所有接口的数据,用于自动化回归时进行接口mock。测试记录:自动化回归的产物,即录制到的所有埋点数据。验证报告:即测试记录和测试场景的联合产物,蕴含了所有异样埋点的具体异样信息。工作组:来自teslaLab的概念,即测试记录的汇合,个别用于聚合各业务线的测试记录。通过率:指一个测试记录中没有任何埋点异样的埋点(去重)占埋点总数(去重)的百分比。工作组通过率:指工作组中所有测试记录中没有异样的埋点总数之和(去重)占所有埋点总数之和(去重)的百分比。零碎架构图整个埋点自动化验证平台次要由三局部组成,别离是: 自动化工具:teslaLab挪动端SDK:安卓的ubt-verification和ios的kylin验证平台:无线研发平台的埋点验证模块 流程图下图为单个测试用例的首次验证流程: 次要可分为三个阶段: 筹备阶段<!----> 用teslaLab录制或者编写自动化脚本在脚本编辑页手动执行脚本并抉择录制Mock性能,录制接口数据并创立Mock记录新建运行工作并选中后面新建的脚本和Mock记录,执行工作之后失去一组埋点数据(即测试记录)。依据须要验证的埋点的数量自行抉择手动配置(不举荐,太麻烦)或者从上一步失去的测试记录中主动生成(举荐)相应根底规定(即测试场景)。至此,埋点自动化验证的三要素(自动化脚本,Mock记录,测试场景)已集齐。<!----> 运行阶段<!----> 能够手动执行或者通过配置Cron表达式实现定时执行在筹备阶段新建的自动化工作,产出一份测试记录和一份验证报告。<!----> 验收阶段<!----> 依据预约策略和测试记录主动生成的验证规定可能无奈满足预期,因而产出的验证报告中的问题须要人工核查是否无效。 如果有效则能够通过报告详情页中的修复按钮疾速勘误规定,或者返回测试场景详情页手动勘误规定。如果无效则能够选中无效的埋点并提交报障至埋点治理平台,提交胜利后会飞书告诉至最初一位负责该埋点的研发和测试,当问题修复并被测试验收之后即可验收报障。至此,一个测试用例的残缺验证流程完结。 详情接下来将依照验证流程的程序逐个介绍这三个模块的具体流程 自动化模块埋点自动化依赖 「Tesla-lab 」 的自动化模块,其中数据Mock 记录的录制和UI自动化 脚本的录制依赖 「Tesla-lab」 本地编辑器,工作和工作组的定时执行依赖 「Tesla-lab」 任务调度模块,工作的理论执行依赖 「Tesla-lab」 工作执行器。 「Tesla-lab 」自动化流程图 「Tesla-lab 」自动化整体架构实现 实现上分三个模块: 端<!----> 编辑器端,负责脚本编辑和数据录制TeslaLab 客户端负责本地脚本/ 工作的调试/执行,定时工作的部署<!----> Core-local java agent<!----> 本地外围服务,封装了基于Quartz 的工作 / 工作组本地设施治理<!----> iOS 基于 tidevice / wdaAndroid基于 uiautomator2<!----> 自动化工作理论执行者<!----> 基于pytest 的脚本执行器<!----> TeslaLab 服务<!----> 提供脚本治理工作/工作组治理设施远程管理和同步「Tesla-lab 」自动化录制编辑器编辑器/录制器(客户端内嵌Web) 录制脚本<!----> 反对手动编写或者通过点击页面生成对应代码,反对运行/暂停来调试脚本 数据录制,通过和挪动端SDK进行本地的socket通信,触发SDK的接口/埋点的录制/回放流程<!----> ...

June 8, 2022 · 1 min · jiezi

关于埋点:云音乐曙光埋点还原数据理想国

图片起源:https://unsplash.com 本文作者:渔夫 背景进入挪动互联网的下半场,以用户行为数据分析驱动的算法个性化举荐和人工精细化经营已成为各个产品必不可缺的配置,数据成为各产品的外围竞争力之一,各厂均开始致力于建设本人的数据仓库和数据中台。其中埋点作为互联网产品的次要数据起源起着至关重要的作用,从下图可见是整个数据链路的起源,决定了整个数据体系的品质和能力。然而埋点因其生产生产链路太长,体现不对用户间接可见,过程中信息传递和积淀艰难等起因,使得埋点往往存在问题多发现晚保障老本低等特点;且随着业务倒退到不同阶段,对于数据埋点的能力诉求亦会变动,但往往后续治理也是十分艰难。 云音乐作为一个典型简单内容产品,蕴含了多种内容介质(歌曲/播客/视频/动静/评论/直播/歌房 等),多种组织模式(歌单/播单/专辑/话题/云圈 等),多种用户身份(音乐人/达人/普通用户/主播 等),以及十分多内容散发场景(举荐流/搜寻/榜单/专区 等),具备了一个典型内容产品对散发效率敏感的特点,因而在整体流量/内容/用户等多个域内都具备非常复杂的用户生产数据和行为剖析诉求。这对端上标准化和全面埋点提出了极高的挑战,产品决策/BI剖析/数仓开发/算法举荐等诸多方面均须要有一套整体高效稳固标准化的埋点设计来撑持。然而过往历史中,因计划建设不佳历史包袱重,埋点始终是业务突出痛点,存在如下图几个方面问题。 基于此背景,笔者联结网易杭州研究院及云音乐内诸多部门成立曙光埋点我的项目,设计方案推动共建优化落地,目前已根本实现外围建设并在搜寻、播客、首页举荐等外围业务上实现落地和数据革新,剩平台的数据回归稽查能力、搭建买通以及若干易用性优化,以及更多的业务落地治理在22年收尾。 业界调研要实现埋点提效提质,须要在埋点定位规范、埋点机会口径、参数设置收集等方面进行良好设计晋升信息精确表白升高出错可能,并且须要在各个环节开发各种工具和平台去赋能,综合察看了业界各家的解决,整体在如下方面各有偏重。 在坑位定位方面,鉴于大前端的DOM树布局渲染模型,很容易联想到用坑位节点本身在树上的地位去进行形容。基于此,Mixpanel、GrowingIO、神策、以及网易的HubbleData 等抉择了x-path形容法,因x-path全自动埋点量大及无更多业务参数,均联合端主动截屏,IDE和平台联合,进行更多的坑位圈选参数设置;以及采纳一些形容优化伎俩升高层级、类型和地位变动对ID的敏感水平。但始终无奈从根本上解决定位形容的稳固标准化和不同端的一致性形容问题,计划难以利用到外围的业务报表,以及线上算法应用上。 腾讯、美团、字节、快手等建设联合坑位示意图、参数治理进行面向坑位的埋点准确治理,定位形容由平台随机生成或策动按默认标准输出,埋点参数均由开发面向坑位手动设置无层级上下文汇总收集,并联合IDE插件买通开发和测试赋能进行提效(可参考:字节跳动大规模埋点数据治理最佳实际)。其中美团在外部的动静布局上与x-path找到了不错的结合点,买通实现坑位圈选定位(x-path)和模型对应的参数配置,建设MTFlexbox上的可视化主动埋点,实现了埋点的齐全策动/BI自助化。 阿里通过四段式SPM(站点.页面.页面区块.区块内点位)和SCM(投放零碎ID.投放算法ID.投放算法版本ID.投放人群ID) 进行地位和内容的标准化形容,开发亦是面向坑位进行坑位进行标识和参数设置,无层级上下文汇总收集,在埋点上各端均做了AOP实现自动化,并通过截图图像编码买通平台进行更多业务参数的可视化配置。网易严选亦采纳了与阿里类似的计划。 能够看到各个大厂在支流大产品中均未采纳类x-path的定位形容自生成以及上下文参数根据汇总收集方面的建设。而云音乐新近亦采纳类似的治理计划,坑位的定位形容采纳过平台随机生成以及四段式SPM。然而因为云音乐自身简单内容社区型产品存在的大量资源散发组织模式,以及外部工程化建设背景,如下图所示,一个歌单和动静均会在诸多场景和模块分区中呈现,而采纳业界现有的各种治理方法,即便端上歌单卡片组件是对立复用的,然而其埋点亦需随着场景组合状况的减少和出现极大水平的扩张,使得埋点开发在工作量和品质以及标准化方面均始终很痛。 基于内容型产品自身业务和架构特点(后续会输入文章进行专门解说和剖析,敬请关注),要彻底解决问题满足业务需要,仍然须要思考类x-path型的页面层级上下文自定位自汇总的计划,只是咱们须要通过肯定伎俩去解决业界始终未能无效解决的精确稳固一致性难题。 其中在上下文参数的自汇总收集方面:西瓜视频基于责任链模式,在业务层进行了相干父子层级参数的汇总收集,不过会对业务存在较强的侵入性,且不彻底。腾讯PCG数据中台的大同埋点平台亦联合DOM树的UI层级进行各级参数的主动收集和汇总格式化,然而大同治理平台自身做得较差,且采集SDK的计划不够彻底泛化,能力无限保护艰难,未彻底走向虚构对象树建设这条路。 曙光计划在曙光埋点中,思考到x-path的地位形容其实存在较多无数据意义的层级且这些层级又往往随着端上同学的款式重构批改而变动(往往会有容器层级的新增或类型批改),而在理论埋点中往往仅须要关注的是蕴含若干层级的资源卡片、模块频道容器、互动组件等,如果能够仅将这些层级标注进去并进行自汇总,那么定位的形容如果产品设计层面未产生需要变动的状况下,就是稳固易懂且可多端统一的(毕竟不同端上产品设计是统一的)。基于此,引入了如下所示的埋点对象概念,在整个DOM树中,通过oid去标识所须要的对象层级(图示意用,理论层级比画出的更多,仅红色-页面和蓝色-元素的层级进行oid标记),借助DOM树的构造,天然造成了一个稠密的埋点对象树。 整个曙光埋点的根本思维就是要在端上(客户端&跨端&前端)构建页面的埋点对象树并放弃同步更新,实现页面和元素曝光的主动埋点,以及通过用户行为API的Hook实现点击的主动埋点。 如上图示意,曙光建设了业务无感的主动埋点SDK,主动生成和更新页面的对象树,实现曝光和点击事件的自动化埋点,在坑位埋点生成时从对应节点往上查找到根对象节点,收集树支上所有节点业务设置的参数,即可主动生成坑位的标准化地位SPM和内容SCM形容;在此基础上SDK内有序记录了相干埋点事件,辨认出途中用户操作行为,间接在埋点生成时按约定格局生成和记录refer参数,以进行高效精确的行为归因。同时亦能够看出,该计划对于组件化的内容型架构特地敌对,对于卡片在不同场景再复用再组织的状况,卡片自身是无需再反复埋点的。计划中波及的相干参数规范和埋点构造约定如下,列出了次要要害参数及含意。 在此基础上,建设相应埋点平台来进行埋点需要治理、参数等元数据管理、以及埋点测试和稽查校验等,整体造成了如下流程: 回到上文的痛点剖析,配合SDK和平台,以及基于曙光埋点的后续UI自动化等的能力建设,咱们针对于各个痛点的解决思路能够总结如下: 曙光SDK曙光SDK是整个计划实现的外围,其整体蕴含内容如下,以后已实现Android,IOS,WEB,RN四端的反对: 为了更好标准化,咱们将规范的事件限定成曝光和点击两个大事件,再通过对象的spm和scm去辨别具体的事件业务含意。比方,老的埋点计划中(个别业界也多采纳这类定义),端上的关注事件会定义EventCode为"focus",在曙光中会间接定义成能够产生关注行为的元素点击操作(个别会采纳雷同oid==btn_focus来对立定义),故整体上点击事件的表现力是比拟充沛的。而曝光上,咱们辨别了页面和元素,页面的曝光和反曝光会触发对应节点往下的元素曝光检测,且页面节点根据其所处层级不同有根页面和子页面辨别,但业务开发无需关怀,只需API设置elementOID和pageOID。基于此,SDK的根本外围性能就是在各端通过SDK去实现页面/元素曝光和点击事件的主动埋点,并自动记录和关联用户行为。目前在Android、IOS、WEB三端的SDK解决流程如下图所示。 图中要害流程:通过零碎AOP触发树更新,在主线程根据页面理论DOM树的遍历过滤出OID节点生成虚构树,将树更新事件推送给曙光工作线程去进行视图的可见遮挡穿插判断以进行曝光埋点的主动生成,进而在工作线程根据树结构进行埋点节点的树枝回溯收集相干节点的参数进行架构化;同时在工作线程中保护了用户行为链路的refer参数(由页面曝光、元素点击、用户自定义和插入refer组成),这些refer参数会在埋点中被间接带上,供数据侧生产归因。 图中绿色圆圈为凋谢给业务开发应用的API,其蕴含了节点的参数设置、被动触发刷新(局部极非凡状况页面更新未被AOP的业务可自行触发)、自定义埋点事件、未AOP的Refer插入等四局部,整体操作很轻量,最次要的节点参数设置API与各端给View设置显示数据的理论可保持一致,且各层只需关注本人的参数,所处的父级模块、页面等信息由SDK在埋点回溯时主动收集。 当然这是现实状态下的主流程,在整体计划中在前端和客户端均会存在非凡状况须要去抹平,如图中粉色背景的方块中,在生成vTree的过程中,为了尽可能达到多端统一,以及满足业务数据需要的状况,咱们反对了逻辑挂载来将一个节点脱离自身View父子关系建设新的父子关系(如上图左,点击歌曲更多关上的浮层可能是一个独立Window,与歌单页和外部的元素无父子关系;然而在数据分析上,会心愿看到浮层内的各个点击能够看到歌单页以及对应歌曲卡片的信息,这时候能够抉择将浮层对象逻辑挂载到歌单卡片或者歌单页对象下),以及虚构层级以反对对本来不存在的View父子层级减少一个可剖析的数据层级(如上图右,对于模块内的各个歌单和歌曲卡片以及头部的更多、播放按钮的埋点事件,会须要晓得所属的模块信息,但端上可能并不存在整个模块的层级--图中红色虚线,只是头部和下方横向滚动列表在整体列表上的拼接假象,对于端开发同学为了缩小UI层级这种状况是比拟常见的,此时能够通过减少一层与虚线红框对应的虚构模块节点来疾速达到目标)。 SDK内对于View的曝光可见性也做了较为齐备的扩大反对和判断,业务能够通过API去批改View的理论Frame(显示Rect,以便反对半透理论背地可见等状况)。同时,在曝光检测中,也以节点的可见区域和树结构,做了严格的遮挡比照判断,整体准则和流程如上图所示。 如上为Android、IOS、WEB三端的整体状况,其中WEB端的计划基于DOM而非VDOM操作,能够适配各种不同前端技术栈。对于RN类Native渲染的跨端平台,其在JS端放弃与WEB端雷同的Component配置API,而后客户端通过ViewManager扩大属性,将相干节点配置对应到Native侧的理论View节点即可,其余流程与原生是统一。另外,对于Flutter,Android端的Compose这类渲染框架,亦可在其理论布局节点树上找到切入点,构建出对应的埋点对象树结构,更多端后续会持续反对,同时SDK打算往年开源,更多细节届时再披露。 Android端BaseViewManager中扩大:@ReactProp(name = "eventTracing")public void setEventTracing(@Nonnull T view, @Nullable ReadableMap eventTracing) { // 调用客户端侧曙光埋点SDK的节点设置API}IOS端RCTViewManager中扩大:RCT_CUSTOM_VIEW_PROPERTY(eventTracing, NSDictionary *, RCTView) { // 调用客户端侧曙光埋点SDK的节点设置API}除了SDK的外围性能之外,曙光我的项目还开发了一个工具,以反对各端间接进行对象和层级查看,辅助开发测试,并且随着计划的落地和规范数仓的开发,后续亦可在端上间接做数据可视化,整体成果如下,具体细节不再赘述。 EasyInsightEasyInsight是曙光建设的埋点治理平台,在数据埋点的事先中后均起着十分重要的作用,平台的整体蕴含的性能简介如下图所示,其中曾经实现的为需要治理和参数治理,以及测试稽查中的需要测试局部。无关回归稽查、动静取参、以及后续更多扩大开发写此文时尚在进行中。 事先其承载埋点元数据管理,埋点需要提出、埋点设计、评审和任务分配。 事中开发同学根据对象详情页里的参数需要,根据层级(血统)关系判断适合的层级和组件进行埋点参数设置,平台在对象层面保护了无关对象和SPM所需的各种公参私参,对相干参数的取值状况进行配置,并通过父对象的保护构建起一个个复用下页面的对象树,同时还提供了血统树的残缺视角,更显直观。 ...

March 8, 2022 · 1 min · jiezi

关于埋点:如何优雅引入神策Web-JS-SDK

背景公司以前采纳GrowingIo埋点,都是在每个我的项目中引入其提供的Web JS SDK,然而最近因为应用期限到了,加上一些其余层面的思考,筹备采纳神策做数据埋点和数据分析,因而须要移除每个我的项目中引入的GrowingIo Web JS SDK,而后引入神策提供的Web JS SDK。 计划如果还是采纳在每个我的项目中独立引入Web JS SDK,首先须要梳理出有多个我的项目有埋点需要,而后安顿告诉每个业务组的前端同学在迭代开发内引入。有没有感觉特地麻烦,明明是同一个Web JS SDK却须要屡次引入,还波及到跨组合作、任务分配、资源安顿等问题。更重要的是,如果下次又更换采纳另一个埋点工具,那反复的事件又要做一遍,吃力不讨好。 因而,咱们心愿可能在某个中央引入后,我的项目就不须要独立引入了,这个计划有没有想到跟网关服务、中间件、拦截器的思维特地像呢?咱们是否能采纳这类思维解决呢?答案是必定的,通过在前端路由服务申请到的动态html脚本中注入神策Web JS SDK 对于一些前后端拆散我的项目能够在前端路由服务中操作动态html脚本注入神策Web JS SDK实现,但对于一些前后端未分离的我的项目(例如freemarker)该怎么解决呢?能在webagent网关做对立解决吗?其实也是可行的,咱们能够判断响应头Content-Type: text/html,如果是,则操作html注入神策Web JS SDK;如果不是,则不做解决。但在理论状况中,鉴于前后端未分离我的项目较少,咱们还是采纳第一种计划,独立在我的项目中引入。 封装神策Web JS SDK神策有提供全埋点性能,我的项目中心愿应用启用该性能,因而须要再封装,初始化SDK,能够参考如下封装: ;(function () { /** 判断环境 */ var hostname = window.location.hostname; var serverUrl = 'https://cj.casstime.com/sa?project=default'; if (hostname === 'www.cassmall.com' || hostname === 'h.cassmall.com') { serverUrl = 'https://cj.casstime.com/sa?project=production'; } // 开启全埋点,Web JS SDK 全埋点包含三种事件:Web 页面浏览、Web 元素点击、Web 视区停留 function addShenCeScript() { window.cassSensors = window['sensorsDataAnalytic201505']; // 初始化 SDK window.cassSensors.init({ server_url: serverUrl, is_track_single_page: true, // 单页面配置,默认开启,若页面中有锚点设计,须要将该配置删除,否则触发锚点会多触发 $pageview 事件 heatmap: { /** * Web 元素点击($WebClick) * 是否开启点击图,default 示意开启,主动采集 $WebClick 事件,能够设置 'not_collect' 示意敞开。 * 默认只有点击 a input button textarea 四种元素时,才会触发 $WebClick 元素点击事件 */ clickmap: 'default', /** * 视区停留事件($WebStay) * 是否开启触达图,default 示意开启,主动采集 $WebStay 事件,能够设置 'not_collect' 示意敞开。 * 须要 Web JS SDK 版本号大于 1.9.1 */ scroll_notice_map: 'default', // 通过 collect_tags 配置是否开启其余任意元素的全埋点采集(默认不采集),其中 div 通过配置最多能够采集 3 层嵌套状况。 collect_tags: { div: { max_level: 3, // 默认是 1,即只反对叶子 div。可配置范畴是 [1, 2, 3],非该范畴配置值,会被当作 1 解决。 }, li: true, span: true, i: true, img: true }, }, }) // 注册公共属性 window.cassSensors.registerPage({ platform_type: 'web', path_name: window.location.pathname, }) /** * Web 页面浏览($pageview) * 设置之后,SDK 就会主动收集页面浏览事件,以及设置初始起源。 */ window.cassSensors.quick('autoTrack'); /** 获取用户登录ID,用户登录后,开发人员调用login,将用户登录ID传给SDK,后续该设施上所有事件的distinct_id就会变成用户所对应的登录ID。*/ ajax({ url: '/webim/user/jwt_token', type: 'GET', success: function (res) { try { var data = JSON.parse(res); window.cassSensors.login(data.username); } catch (e) { } }, error: function (error) { } }); } var script = document.createElement('script'); script.setAttribute('type', 'text/javascript'); var explorer = window.navigator.userAgent; if (explorer.indexOf('MSIE') >= 0) { // ie script.onreadystatechange = function () { if (this.readyState === 'loaded' || this.readyState === 'complete') { addShenCeScript(); } } } else { // chrome script.onload = function () { addShenCeScript(); } } script.setAttribute( 'src', 'https://mstatic.cassmall.com/assets/sensors/sensorsdata1.19.4.min.js' ); /** 封装ajax申请 */ function ajax(params) { params = params || {}; params.data = params.data || {}; // 判断是ajax申请还是jsonp申请 var json = params.jsonp ? jsonp(params) : json(params); // ajax申请 function json(params) { // 申请形式,默认是GET params.type = (params.type || 'GET').toUpperCase(); // 防止有特殊字符,必须格式化传输数据 params.data = formatParams(params.data); var xhr = null; // 实例化XMLHttpRequest对象 if (window.XMLHttpRequest) { xhr = new XMLHttpRequest(); } else { // IE6及其以下版本 xhr = new ActiveXObjcet('Microsoft.XMLHTTP'); }; // 监听事件,只有 readyState 的值变动,就会调用 readystatechange 事件 xhr.onreadystatechange = function () { // readyState属性示意申请/响应过程的以后流动阶段,4为实现,曾经接管到全副响应数据 if (xhr.readyState == 4) { var status = xhr.status; // status:响应的HTTP状态码,以2结尾的都是胜利 if (status >= 200 && status < 300) { var response = ''; // 判断承受数据的内容类型 var type = xhr.getResponseHeader('Content-type'); if (type.indexOf('xml') !== -1 && xhr.responseXML) { response = xhr.responseXML; //Document对象响应 } else if (type === 'application/json') { response = JSON.parse(xhr.responseText); //JSON响应 } else { response = xhr.responseText; //字符串响应 }; // 胜利回调函数 params.success && params.success(response); } else { params.error && params.error(status); } }; }; // 连贯和传输数据 if (params.type == 'GET') { // 三个参数:申请形式、申请地址(get形式时,传输数据是加在地址后的)、是否异步申请(同步申请的状况极少); xhr.open(params.type, params.url + '?' + params.data, true); xhr.send(null); } else { xhr.open(params.type, params.url, true); //必须,设置提交时的内容类型 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); // 传输数据 xhr.send(params.data); } } //格式化参数 function formatParams(data) { var arr = []; for (var name in data) { // encodeURIComponent() :用于对 URI 中的某一部分进行编码 arr.push(encodeURIComponent(name) + '=' + encodeURIComponent(data[name])); }; // 增加一个随机数参数,避免缓存 arr.push('v=' + random()); return arr.join('&'); } // 获取随机数 function random() { return Math.floor(Math.random() * 10000 + 500); } } if (!window.cassSensors) { document.getElementsByTagName('head')[0].appendChild(script); }})()

January 20, 2022 · 3 min · jiezi

关于埋点:前端组件化埋点的实践

本文作者 杨运心头图来自Carlos Muza on Unsplash1、前言开始注释前先介绍一下相干概念,相熟的读者能够略过。前端埋点:一种收集产品数据的形式,它的目标是上报相干行为数据,相干人员以数据为根据来剖析产品在用户端的应用状况,依据剖析进去的后果辅助产品优化、迭代。BI:商业智能,公司外部做数据分析相干的部门。 2、背景在流量红利逐步隐没的当初,数据的采集、剖析和精细化的经营显得更加重要,所以埋点在互联网产品中是很常见的,它能够更好的辅助咱们去迭代、欠缺产品性能。平时咱们在实现根底的业务需要之后,还须要开发实现埋点需要。所以咱们谋求的是简略快捷的做好埋点工作,且不会占用咱们太多的精力。然而事实却不那么美妙,目前咱们团队在前端埋点方面存在一些痛点:在结构埋点字段的时候须要依据 BI 的规定,把若干个字段拼接成一个,这样费时费力还有谬误的危险;一些曝光场景下的点不好打比方:分页列表、虚构列表;他们的的曝光埋点实现较为繁琐;逻辑复用问题:特地是曝光相干的点须要在业务代码外面做额定的解决,所以逻辑复用很艰难,对现有代码的侵入也很重大;所以咱们须要一种适宜咱们的埋点计划解决咱们目前的问题,晋升咱们的开发效率,不再为埋点而困扰。 3、常见前端埋点计划咱们对目前市场上几种埋点计划进行了一些调研,惯例有 3 种计划:手动代码埋点:用户触发某个动作后手动上报数据 长处:是最精确的,能够满足很多定制化的需要。毛病:埋点逻辑与业务代码耦合到一起,不利于代码保护和复用。可视化埋点:通过可视化工具配置采集节点,指定本人想要监测的元素和属性。外围是查找 dom 而后绑定事件,业界比拟有名的是 Mixpanel 长处:能够做到按需配置,又不会像全埋点那样产生大量的无用数据。毛病:比拟难加载一些运行时参数;页面构造发生变化的时候,可能就须要进行局部重新配置。无埋点:也叫“全埋点”,前端主动采集全副事件并上报埋点数据,在后端数据计算时过滤出有用数据 长处:收集用户的所有端上行为,很全面。毛病:有效的数据很多、上报数据量大。4、埋点计划在调研完这些计划后,我认为上述计划并不齐全适宜咱们,咱们须要的计划是精确、疾速埋点,同时把埋点的代码与业务逻辑解耦,并且咱们的音街挪动站能够绝对平滑的迁徙到咱们新的埋点库下面来。联合咱们目前的技术栈 React,以及现状和经营、产品侧的需要咱们决定采纳申明式的组件化埋点 + 缓冲队列计划,这里论述一下咱们的大抵思路。 为了解决埋点代码与业务逻辑耦合的问题,咱们认为能够在视图层解决,埋点能够演绎为两大类,点击与曝光埋点。咱们能够形象出两个组件别离解决这两种场景。在一些场景下疾速滑动、频繁点击会在短时间打出大量的点,造成频繁的接口调用,这在挪动端是要防止的,针对这种场景咱们引入了缓冲队列,产生的点位信息先进入队列,通过定时工作分批次上报数据,针对不同类型的点也能够利用不同的上报频率。目前对于一些字段采纳的是人工拼接,比方 BI 定义的 _mspm2 等相干通用字段,相似这种咱们齐全能够在库对立解决,既不容易出错,也不便前期拓展。对于页面级曝光,咱们能够在埋点库初始化后主动注册对于页面曝光的相干事件,不须要使用者关怀。以页面为维度治理埋点配置 咱们的站点是同构利用,跟咱们的架构比拟符合更加清晰,便于保护目前也是采纳这种计划治理,迁徙老本会更小5、要害节点5.1 流程梳理这里存在一个问题,可能库还没初始化结束,一些点曾经产生了,比方曝光类的,如果这时候生成对应的点进入缓冲队列,就是属于有效的点因为没有加载到坑位信息、配置参数等,所以针对这种场景下产生的点位信息,咱们新开一个队列存储,等到初始化实现再去解决;流程图: 5.2 点击埋点点击埋点咱们开始的思考是提供一个组件,包裹须要进行点击埋点的 dom 元素,也有可能是组件,而后给子元素绑定点击事件,当用户触发事件时进行埋点相干解决。 依照上述思路咱们就必须绑定点击事件到 dom 上,然而咱们又不想引入额定的 dom 元素,因为这会减少 dom 构造层级,给使用者带来麻烦,这样留给咱们的操作空间就剩下 props.children ,所以咱们去递归 TrackerClick 组件的 children,找到最外层的dom元素,同时要求 TrackerClick 上面必须有一个 container 元素,依照这个思路咱们进行了解决。 export default function TrackerClick({ name, extra, immediate, children,}) { handleClick = () => { // todo append queue }; function AddClickEvent(ele) { return React.cloneElement(ele, { onClick: (e) => { const originClick = ele.props.onClick || noop; originClick.call(ele, e); handleClick(); } }); } function findHtmlElement(ele) { if (typeof ele.type === 'function') { if (ele.type.prototype instanceof React.Component) { ele = new ele.type(ele.props).render(); } else { ele = ele.type(ele.props); } } if (typeof ele.type === 'string') { return AddClickEvent(ele); } return React.cloneElement(ele, { children: findHtmlElement(ele.props.children) }); } return findHtmlElement(React.Children.only(children));}// case1<TrackerClick name='namespace.click'> <button>点击</button></TrackerClick>// case2<TrackerClick name='namespace.click'> <CustomerComp> <button>点击</button> </CustomerComp></TrackerClick>从应用上来说很简便,达到了咱们的目标。然而通过咱们的实际也发现了一些问题,比方使用者并不分明外面的实现细节,有可能外面没有一个 container 包裹,也可能应用了 React.Fragment 造成一些不可预估的行为、同时也有形的减少了dom构造层级(尽管咱们没有引入,然而咱们在通知用户,你最好有个 container )。咱们又在反思这种计划的合理性,尽管应用上带来了便捷,然而带来了不确定性。通过探讨咱们决定把绑定的工作交给组件使用者,咱们只须要明确通知他能够应用哪些办法,这是确定性的工作。应用方只须要把触发的回调绑定到对应的事件上即可。革新后如下: ...

October 30, 2020 · 3 min · jiezi

关于埋点:业务线用户路径问题

背景每个2c公司的公司都会关注用户在本人app沉闷的用户的行为轨迹,每个公司的日志格局不统一,对应的做法也都不统一,这里记录一种解决的形式,日志数据来源于与无线日志,次要的存储计算介质为hive。ps:只解决主流程的门路。 前提日志格局简略假如业务线的每行日志中都有uid标识一个惟一的用户,同时有对应的日志的工夫戳,同时每行日志还会有一个log_id的字段去代表本行日志,用ref_log_id字段去标识本行日志的起源页面的下级日志的log_id。所以日志能够简略的记录为一下的格局:2020-09-15 12:00:13.343 uid=xxx,log_id=log_id_xxx,ref_lof_id=ref_log_id_xxx,page=detail2020-09-15 13:00:14.343 uid=yyy,log_id=log_id_yyy,ref_lof_id=ref_log_id_yyy,page=list 日志格式化对于上述比拟工整的格局的日志,咱们须要先将其中的每个字段都解析格式化解析进去。假如上述的日志曾经在hdfs的某个门路下了,在门路上建设表面去解析日志即可,建表用的是hive提供的正则去匹配字段,语句如下: drop table tmp.log_test_20200915;CREATE EXTERNAL TABLE tmp.log_test_20200915( time string COMMENT '工夫', uid string COMMENT 'uid', log_id string COMMENT 'log_id', ref_log_id string COMMENT 'ref_log_id', page string COMMENT '用户拜访的页面')COMMENT '测试表'ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.RegexSerDe' WITH SERDEPROPERTIES ( 'input.regex'='^(.*) uid=(.*),log_id=(.*),ref_lof_id=(.*),page=(.*)$', 'output.format.string'='%1$s %2$s %3$s %4$s %5$s') LOCATION 'viewfs://cluster/user/test/logtest';执行上述语句后的后果如下: 到此为止曾经将日志做了格式化,将每个字段都匹配了进去。 用户轨迹解决用户数据预处理在数据格式化当前须要对用户的数据进行预处理,将每个用户的数据整合到一起。对于很多关怀用户数据的人来说,他们不关系具体的页面只关怀具体的页面类型,例如detail是详情页,list是列表页,同时可能会有多个详情页和列表页,首页订单页也类型。在这里咱们做一个小小的转换,详情页用D示意,list页用来标识,首页用H来示意...人为的在表外面加上一个page_type的字段去示意页面的类型。 with process_table as ( select uid, log_id, ref_log_id, page, case when page = 'detail' then 'D' when page = 'list' then 'L' end as page_type from tmp.log_test_20200915),aggregate_table as ( select uid, coalesce(concat('{', concat_ws(',', ws_log_id, ws_ref_log_id, ws_page, ws_page_type), '}'), '') json_str from ( select uid, log_id, ref_log_id, page, page_type, concat(concat_ws('\":\"', '\"log_id', coalesce(log_id, '')), '\"') ws_log_id, concat(concat_ws('\":\"', '\"ref_log_id', coalesce(ref_log_id, '')), '\"') ws_ref_log_id, concat(concat_ws('\":\"', '\"page', coalesce(page, '')), '\"') ws_page, concat(concat_ws('\":\"', '\"page_type', coalesce(page_type, '')), '\"') ws_page_type from process_table ) a),json_table as ( select uid, concat_ws(',', collect_set (json_str)) json_str from aggregate_table group by uid) select uid, concat('[', json_str, ']') json_str from json_table通过下面的sql,咱们能够失去一个 uid和用户在当天的所有数据的汇合。执行上述的sql失去的后果如下: ...

September 15, 2020 · 2 min · jiezi

从零开始搭建前端监控系统二实现圈选无埋点

前言本系列文章旨在讲解如何从零开始搭建前端监控系统。 项目已经开源 项目地址: https://github.com/bombayjs/b... (web sdk)https://github.com/bombayjs/b... (服务端,用于提供api)(未完)https://github.com/bombayjs/b... (后台管理系统,可视化数据等)(未完)您的支持是我们不断前进的动力。 喜欢请start!!! 喜欢请start!!! 喜欢请start!!! 本文是该系列第二篇,重点讲解如何实现圈选功能。 如果你还不了解怎么捕获click事件,请先看第一篇 系列文章: 从零开始搭建前端监控系统(一)——web探针sdk示例https://bombayjs.github.io/bo... 演示 源码https://github.com/bombayjs/b... 原理通过postMessage实现iframe的通信通过监听mouseover事件来圈选通过监听click事件获取点击目标的路径通过stopPropagation阻止原来的点击事件实现parent<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title></head><body> <div> <iframe id='iframe' src='./a.html'></iframe></div> <script> window.addEventListener('message', function(event) { console.log(event.data.path) }, false) </script></body></html>iframe// a.html<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title></head><body> <div> <a href='#a'>click me</a></div> <script> window.addEventListener('message', function(event) { console.log(event.data.path) }, false) window.addEventListener('click', function(event) { event.stopPropagation() window.parent.postMessage({ path: '此处需要自己解析出元素路径' }, '*') return }, false) window.addEventListener('mouseover', function(event) { event.target.style = 'border: #ff0000 solid 1px' }, false) </script></body></html>更多资源https://github.com/abc-club/f... ...

September 30, 2019 · 1 min · jiezi

看完就懂的无痕埋点

无痕埋点的设计与实现在移动互联网时代,对于每个公司、企业来说,用户的数据非常重要。重要到什么程度,用户在这个页面停留多久、点击了什么按钮、浏览了什么内容、什么手机、什么网络环境、App什么版本等都需要清清楚楚。甚至一些大厂的蛮多业务成果都是靠基于用户操作行为和记录的推荐转换二次。那么有了上述的诉求,那么技术人员如何满足这些需求?引出来了一个技术点-“埋点”埋点手段业界中对于代码埋点主要有3种主流的方案:代码手动埋点、可视化埋点、无痕埋点。简单说说这几种埋点方案。代码手动埋点:根据业务需求(运营、产品、开发多个角度出发)在需要埋点地方手动调用埋点接口,上传埋点数据。可视化埋点:通过可视化配置工具完成采集节点,在前端自动解析配置并上报埋点数据,从而实现可视化“无痕埋点”无痕埋点:通过技术手段,完成对用户行为数据无差别的统计上传的工作。后期数据分析处理的时候通过技术手段筛选出合适的数据进行统计分析。技术选型代码手动埋点该方案情况下,如果需要埋点,则需要在工程代码中,写埋点相关代码。因为侵入了业务代码,对业务代码产生了污染,显而易见的缺点是埋点的成本较高、且违背了单一原则。例1:假如你需要知道用户在点击“购买按钮”时的相关信息(手机型号、App版本、页面路径、停留时间、动作等等),那么就需要在按钮的点击事件里面去写埋点统计的代码。这样明显的弊端就是在之前业务逻辑的代码上面又多出了埋点的代码。由于埋点代码分散、埋点的工作量很大、代码维护成本较高、后期重构很头痛。例2:假如 App 采用了 Hybrid 架构,当 App 的第一版本发布的时候 H5 的关键业务逻辑统计是由 Native 定义好关键逻辑(比如H5调起了Native的分享功能,那么存在一个分享的埋点事件)的桥接。假如某天增加了一个扫一扫功能,未定义扫一扫的埋点桥接,那么 H5 页面变动的时候,Native 埋点代码不去更新的话,变动的 H5 的业务就未被精确统计。优点:产品、运营工作量少,对照业务映射表就可以还原出相关业务场景、数据精细无须大量的加工和处理缺点:开发工作量大、前期需要和运营、产品指定的好业务标识,以便产品和运营进行数据统计分析可视化埋点可视化埋点的出现,是为解决代码埋点流程复杂、成本高、新开发的页面(H5、或者服务端下发的 json 去生成相应页面)不能及时拥有埋点能力前端在「埋点编辑模式」下,以“可视化”的方式去配置、绑定关键业务模块的路径到前端可以唯一确定到view的xpath过程。用户每次操作的控件,都生成一个 xpath 字符串,然后通过接口将 xpath 字符串(view在前端系统中的唯一定位。以 iOS 为例,App名称、控制器名称、一层层view、同类型view的序号:“GoodCell.21.RetailTableView.GoodsViewController.*baoApp”)到真正的业务模块(“宝App-商城控制器-分销商品列表-第21个商品被点击了”)的映射关系上传到服务端。xpath 具体是什么在下文会有介绍。之后操作 App 就生成对应的 xpath 和埋点数据(开发者通过技术手段将从服务端获取的关键数据塞到前端的 UI 控件上。 iOS 端为例, UIView 的 accessibilityIdentifier 属性可以设置我们从服务端获取的埋点数据)上传到服务端。优点:数据量相对准确、后期数据分析成本低缺点:前期控件的唯一识别、定位都需要额外开发;可视化平台的开发成本较高;对于额外需求的分析可能会比较困难无痕埋点通过技术手段无差别地记录用户在前端页面上的行为。可以正确的获取 PV、UV、IP、Action、Time 等信息。缺点:前期开发统计基础信息的技术产品成本较高、后期数据分析数据量很大、分析成本较高(大量数据传统的关系型数据库压力大)优点:开发人员工作量小、数据全面、无遗漏、产品和运营按需分析、支持动态页面的统计分析如何选择结合上述优缺点,我们选择了无痕埋点+可视化埋点结合的技术方案。怎么说呢?对于关键的业务开发结束上线后、通过可视化方案(类似于一个界面,想想看 Dreamwaver,你在界面上拖拖控件,简单编辑下就可以生成对应的 HTML 代码)点击一下绑定对应关系到服务端。那么这个对应关系是什么?我们需要唯一定位一个前端元素,那么想到的办法就是不管 Native 和 Web 前端,控件或者元素来说就是一个树形层级,DOM tree 或者 UI tree,所以我们通过技术手段定位到这个元素,以 Native iOS 为例子假如我点击商品详情页的加入购物车按钮会根据 UI 层级结构生成一个唯一标识 “addCartButton.GoodsViewController.GoodsView.*BaoApp” 。但是用户在使用 App 的时候,上传的是这串东西的 MD5到服务端。这么做有2个原因:服务端数据库存储这串很长的东西不是很好;埋点数据被劫持的话直接看到明文不太好。所以 MD5 再上传。操刀就干数据的收集实现方案由以下几个关键指标:现有代码改动少、尽量不要侵入业务代码去实现拦截系统事件全量收集如何唯一标识一个控件元素不侵入业务代码拦截系统事件以 iOS 为例。我们会想到 AOP(Aspect Oriented Programming)面向切面编程思想。动态地在函数调用前后插入相应的代码,在 Objective-C 中我们可以利用 Runtime 特性,用 Method Swizzling 来 hook 相应的函数为了给所有类方便地 hook,我们可以给 NSObject 添加个 Category,名字叫做 NSObject+MethodSwizzling+ (void)swizzleMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector{ Class class = [self class]; //原有方法 Method originalMethod = class_getInstanceMethod(class, originalSelector); //替换原有方法的新方法 Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); //先尝试給源SEL添加IMP,这里是为了避免源SEL没有实现IMP的情况 BOOL didAddMethod = class_addMethod(class,originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) {//添加成功:表明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMP class_replaceMethod(class,swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else {//添加失败:表明源SEL已经有IMP,直接将两个SEL的IMP交换即可 method_exchangeImplementations(originalMethod, swizzledMethod); }}+ (void)swizzleClassMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector{ Class class = [self class]; //原有方法 Method originalMethod = class_getClassMethod(class, originalSelector); //替换原有方法的新方法 Method swizzledMethod = class_getClassMethod(class, swizzledSelector); //先尝试給源SEL添加IMP,这里是为了避免源SEL没有实现IMP的情况 BOOL didAddMethod = class_addMethod(class,originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) {//添加成功:表明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMP method_exchangeImplementations(originalMethod, swizzledMethod); } else {//添加失败:表明SEL已经有IMP,直接将两个SEL的IMP交换即可 method_exchangeImplementations(originalMethod, swizzledMethod); }}全量收集我们会想到 hook AppDelegate 代理方法、UIViewController 生命周期方法、按钮点击事件、手势事件、各种系统控件的点击回调方法、应用状态切换等等。动作事件App 状态的切换给 Appdelegate 添加分类,hook 生命周期UIViewController 生命周期函数给 UIViewController 添加分类,hook 生命周期UIButton 等的点击UIButton 添加分类,hook 点击事件UICollectionView、UITableView 等的在对应的 Cell 添加分类,hook 点击事件手势事件 UITapGestureRecognizer、UIControl、UIResponder相应系统事件以统计页面的打开时间和统计页面的打开、关闭的需求为例,我们对 UIViewController 进行 hookstatic char *viewController_open_time = “viewController_open_time”;static char *viewController_close_time = “viewController_close_time”;// load 方法里面添加 dispatch_once 是为了防止手动调用 load 方法。+ (void)load{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @autoreleasepool { [[self class] swizzleMethod:@selector(viewWillAppear:) swizzledSelector:@selector(viewWillAppear:)]; [[self class] swizzleMethod:@selector(viewWillDisappear:) swizzledSelector:@selector(viewWillDisappear:)]; } });}#pragma mark - add prop- (void)setOpenTime:(NSDate *)openTime{ objc_setAssociatedObject(self,&viewController_open_time, openTime, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}- (NSDate *)getOpenTime{ return objc_getAssociatedObject(self, &viewController_open_time);}- (void)setCloseTime:(NSDate *)closeTime{ objc_setAssociatedObject(self,&viewController_close_time, closeTime, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}- (NSDate *)getCloseTime{ return objc_getAssociatedObject(self, &viewController_close_time);}- (void)viewWillAppear:(BOOL)animated{ NSString *className = NSStringFromClass([self class]); NSString *refer = [NSString string]; if ([self getPageUrl:className]) { //设置打开时间 [self setOpenTime:[NSDate dateWithTimeIntervalSinceNow:0]]; if (self.navigationController) { if (self.navigationController.viewControllers.count >=2) { //获取当前vc 栈中 上一个VC UIViewController referVC = self.navigationController.viewControllers[self.navigationController.viewControllers.count-2]; refer = [self getPageUrl:NSStringFromClass([referVC class])]; } } if (!refer || refer.length == 0) { refer = @“unknown”; } [SDGDataCenter openPage:[self getPageUrl:className] fromPage:refer]; } [self viewWillAppear:animated];}- (void)viewWillDisappear:(BOOL)animated{ NSString className = NSStringFromClass([self class]); if ([self getPageUrl:className]) { [self setCloseTime:[NSDate dateWithTimeIntervalSinceNow:0]]; [SDGDataCenter leavePage:[self getPageUrl:className] spendTime:[self p_calculationTimeSpend]]; } [self viewWillDisappear:animated];}#pragma mark - private method- (NSString )p_calculationTimeSpend{ if (![self getOpenTime] || ![self getCloseTime]) { return @“unknown”; } NSTimeInterval aTimer = [[self getCloseTime] timeIntervalSinceDate:[self getOpenTime]]; int hour = (int)(aTimer/3600); int minute = (int)(aTimer - hour3600)/60; int second = aTimer - hour3600 - minute60; return [NSString stringWithFormat:@"%d",second];}@end如何唯一标识一个控件元素xpath 是移动端定义可操作区域的唯一标识。既然想通过一个字符串标识前端系统中可操作的控件,那么 xpath 需要2个指标:唯一性:在同一系统中不存在不同控件有着相同的 xpath稳定性:不同版本的系统中,在页面结构没有变动的情况下,不同版本的相同页面,相同的控件的 xpath 需要保持一致。我们想到 Naive、H5 页面等系统渲染的时候都是以树形结构去绘制和渲染,所以我们以当前的 View 到系统的根元素之间的所有关键点(UIViewController、UIView、UIView容器(UITableView、UICollectionView等)、UIButton…)串联起来这样就唯一定位了控件元素。为了精确定位元素节点,参看下图假设一个 UIView 中有三个子 view,先后顺序是:label、button1、button2,那么深度依次为: 0、1、2。假如用户做了某些操作将 label1 从父 view 中被移除了。此时 UIView 只有 2 个子view:button1、button2,而且深度变为了:0、1。可以看出仅仅由于其中某个子 view 的改变,却导致其它子 view 的深度都发生了变化。因此,在设计的时候需要注意,在新增/移除某一 view 时,尽量减少对已有 view 的深度的影响,调整了对节点的深度的计算方式:采用当前 view 位于其父 view 中的所有 与当前 view 同类型 子view 中的索引值。我们再看一下上面的这个例子,最初 label、button1、button2 的深度依次是:0、0、1。在 label 被移除后,button1、button2 的深度依次为:0、1。可以看出,在这个例子中,label 的移除并未对 button1、button2 的深度造成影响,这种调整后的计算方式在一定程度上增强了 xpath 的抗干扰性。另外,调整后的深度的计算方式是依赖于各节点的类型的,因此,此时必须要将各节点的名称放到viewPath中,而不再是仅仅为了增加可读性。在标识控件元素的层级时,需要知道「当前 view 位于其父 view 中的所有 与当前 view 同类型 子view 中的索引值」。参看上图,如果不是同类型的话,则唯一性得不到保证。有个问题,比如我们点击的元素是 UITableViewCell,那么它虽然可以定位到类似于这个标示 xxApp.GoodsViewController.GoodsTableView.GoodsCell,同类型的 Cell 有多个,所以单凭借这个字符串是没有办法定位具体的那个 Cell 被点击了。有2个解决方案利用系统提供的 accessibilityIdentifier 官方给出的解释是标识用户界面元素的字符串找出当前元素在父层同类型元素中的索引。根据当前的元素遍历当前元素的父级元素的子元素,如果出现相同的元素,则需要判断当前元素是所在层级的第几个元素/A string that identifies the user interface element.default == nil/@property(nullable, nonatomic, copy) NSString *accessibilityIdentifier NS_AVAILABLE_IOS(5_0);服务端下发唯一标识接口获取的数据,里面有当前元素的唯一标识。比如在 UITableView 的界面去请求接口拿到数据,那么在在获取到的数据源里面会有一个字段,专门用来存储动态化的经常变动的数据。cell.accessibilityIdentifier = [[[SDGGoodsCategoryServices sharedInstance].categories[indexPath.section] children][indexPath.row].spmContent yy_modelToJSONString];判断在同层级、同类型的控件元素里面的序号对当前的控件元素的父视图的全部子视图进行遍历,如果存在和当前的控件元素同类型的控件,那么需要判断当前控件元素在同类型控件元素中的所处的位置,那么则可以唯一定位。举例:GoodsCell-3.GoodsTableView.GoodsViewController.xxApp//UIResponder分类{ // if (self.xq_identifier_ka == nil) { if ([self isKindOfClass:[UIView class]]) { UIView *view = (id)self; NSString *sameViewTreeNode = [view obtainSameSuperViewSameClassViewTreeIndexPath]; NSMutableString *str = [NSMutableString string]; //特殊的 加减购 因为带有spm但是要区分加减 需要带TreeNode NSString *className = [NSString stringWithUTF8String:object_getClassName(view)]; if (!view.accessibilityIdentifier || [className isEqualToString:@“XQButton”]) { [str appendString:sameViewTreeNode]; [str appendString:@","]; } while (view.nextResponder) { [str appendFormat:@"%@,", NSStringFromClass(view.class)]; if ([view.class isSubclassOfClass:[UIViewController class]]) { break; } view = (id)view.nextResponder; } self.xq_identifier_ka = [self md5String:[NSString stringWithFormat:@"%@",str]]; // self.xq_identifier_ka = [NSString stringWithFormat:@"%@",str]; }// } return self.xq_identifier_ka;}// UIView 分类(NSString *)obtainSameSuperViewSameClassViewTreeIndexPat{ NSString *classStr = NSStringFromClass([self class]); //cell的子view //UITableView 特殊的superview (UITableViewContentView) //UICollectionViewCell BOOL shouldUseSuperView = ([classStr isEqualToString:@“UITableViewCellContentView”]) || ([[self.superview class] isKindOfClass:[UITableViewCell class]])|| ([[self.superview class] isKindOfClass:[UICollectionViewCell class]]); if (shouldUseSuperView) { return [self obtainIndexPathByView:self.superview]; }else { return [self obtainIndexPathByView:self]; }}(NSString )obtainIndexPathByView:(UIView )view{ NSInteger viewTreeNodeDepth = NSIntegerMin; NSInteger sameViewTreeNodeDepth = NSIntegerMin; NSString *classStr = NSStringFromClass([view class]); NSMutableArray *sameClassArr = [[NSMutableArray alloc]init]; //所处父view的全部subviews根节点深度 for (NSInteger index =0; index < view.superview.subviews.count; index ++) { //同类型 if ([classStr isEqualToString:NSStringFromClass([view.superview.subviews[index] class])]){ [sameClassArr addObject:view.superview.subviews[index]]; } if (view == view.superview.subviews[index]) { viewTreeNodeDepth = index; break; } } //所处父view的同类型subviews根节点深度 for (NSInteger index =0; index < sameClassArr.count; index ++) { if (view == sameClassArr[index]) { sameViewTreeNodeDepth = index; break; } } return [NSString stringWithFormat:@"%ld",sameViewTreeNodeDepth]; }## 数据的上传数据通过上面的办法收集完了,那么如何及时、高效的上传到后端,给运营分析、处理呢?App 运行期间用户会点击非常多的数据,如果实时上传的话对于网络的利用率较低,所以需要考虑一个机制去控制用户产生的埋点数据的上传。思路是这样的。对外部暴露出一个接口,用来将产生的数据往数据中心存储。用户产生的数据会先保存到 AppMonitor 的内存中去,设置一个临界值(memoryEventMax = 50),如果存储的值达到设置的临界值 memoryEventMax,那么将内存中的数据写入文件系统,以 zip 的形式保存下来,然后上传到埋点系统。如果没有达到临界值但是存在一些 App 状态切换的情况,这时候需要及时保存数据到持久化。当下次打开 App 就去从本地持久化的地方读取是否有未上传的数据,如果有就上传日志信息,成功后删除本地的日志压缩包。App 应用状态的切换策略如下:- didFinishLaunchWithOptions:内存日志信息写入硬盘- didBecomeActive:上传- willTerimate:内存日志信息写入硬盘- didEnterBackground:内存日志信息写入硬盘// 将App日志信息写入到内存中。当内存中的数量到达一定规模(超过设置的内存中存储的数量)的时候就将内存中的日志存储到文件信息中(void)joinEvent:(NSDictionary *)dictionary{if (dictionary) { NSDictionary *tmp = [self createDicWithEvent:dictionary]; if (!s_memoryArray) { s_memoryArray = [NSMutableArray array]; } [s_memoryArray addObject:tmp]; if ([s_memoryArray count] >= s_flushNum) { [self writeEventLogsInFilesCompletion:^{ [self startUploadLogFile]; }]; }}}// 外界调用的数据传递入口(App埋点统计)(void)traceEvent:(AMStatisticEvent *)event{// 线程锁,防止多处调用产生并发问题@synchronized (self) { if (event && event.userInfo) { [self joinEvent:event.userInfo]; }}}// 将内存中的数据写入到文件中,持久化存储(void)writeEventLogsInFilesCompletion:(void(^)(void))completionBlock{NSArray *tmp = nil;@synchronized (self) { tmp = s_memoryArray; s_memoryArray = nil;}if (tmp) { __weak typeof(self) weakSelf = self; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSString *jsonFilePath = [weakSelf createTraceJsonFile]; if ([weakSelf writeArr:tmp toFilePath:jsonFilePath]) { NSString *zipedFilePath = [weakSelf zipJsonFile:jsonFilePath]; if (zipedFilePath) { [AppMonotior clearCacheFile:jsonFilePath]; if (completionBlock) { completionBlock(); } } } });}}// 从App埋点统计压缩包文件夹中的每个压缩包文件上传服务端,成功后就删除本地的日志压缩包(void)startUploadLogFile{NSArray *fList = [self listFilesAtPath:[self eventJsonPath]];if (!fList || [fList count] == 0) { return;}[fList enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { if (![obj hasSuffix:@".zip"]) { return; } NSString *zipedPath = obj; unsigned long long fileSize = [[[NSFileManager defaultManager] attributesOfItemAtPath:zipedPath error:nil] fileSize]; if (!fileSize || fileSize < 1) { return; } [self uploadZipFileWithPath:zipedPath completion:^(NSString *completionResult) { if ([completionResult isEqual:@“OK”]) { [AppMonotior clearCacheFile:zipedPath]; } }];}];}总结下来关键步骤:1. hook 系统的各种事件(UIResponder、UITableView、UICollectionView代理事件、UIControl事件、UITapGestureRecognizers)、hook 应用程序、控制器生命周期。在做本来的逻辑之前添加额外的监控代码2. 对于点击的元素按照视图树生成对应的唯一标识(addCartButton.GoodsView.GoodsViewController) 的 md5 值3. 在业务开发完毕,进入埋点的编辑模式,将 md5 和关键的页面的关键事件(运营、产品想统计的关键模块:App层级、业务模块、关键页面、关键操作)给绑定起来。比如 addCartButton.GoodsView.GoodsViewController.tbApp 对应了 tbApp-商城模块-商品详情页-加入购物车功能。4. 将所需要的数据存储下来5. 设计机制等到合适的时机去上传数据 ...

April 2, 2019 · 4 min · jiezi

js无侵入埋点方案

今天给大家介绍一个js无侵入埋点方案:min版: https://github.com/aoping/tra…原版:https://github.com/Qquanwei/t…min版是我在原版的基础上进行优化和精简开发的,打包后大小只有8k(原版190k)min版提供两个api: before after这里只介绍min版使用:安装npm i trackpoint-tools -S 或npm i trackpoint-tools-min@latest -S使用代码: https://codesandbox.io/s/oxxw…喜欢的可以star

March 15, 2019 · 1 min · jiezi

关于埋点

本文主要介绍 火球买手 项目上的埋点方案(基于神策),以及一些心得。事实上在项目早期,我们的埋点完全依赖于第三方的全埋点技术,客户端开发人员只需要做一些简单的工作就能满足 BI 部门对数据的需求。但随着业务增长,对数据的准确性和精细化的要求越来越高,之后不得不转向手动埋点,当然这个也是基于第三方的。目前 BI 部门对埋点数据要求可以总结为一句话:『从哪里来到哪里去』,比如在 Timeline 中点击一篇文章进入详情页,那么 Timeline 就是『从哪里来』,详情页就是『到哪里去』,当然实际项目中『从哪里来』不只需要一个维度定位,有时候需要两三个维度才能定位。下面具体来说下 火球买手 项目上是如何埋点的,首先『频道主页』是项目中比较常见的页面,它对应的 Model 是 Channel,然后当任何点击进入的频道主页的事件触发后都需要上报以下数据{ module_name page_name channel_name channel_id}page_name指的是当前的 ViewController 名称,module_name主要用于区分同一个页面内的不同入口,这样子就能确定『从哪里来』,channel_name 和 channel_id 数据来自于 Channel,至于『到哪里去』这里就用埋点的 key 来表明,比如是 ChannelClick。频道主页在 APP 中入口众多,即使在不考虑埋点的情况下,一个统一的入口也是必要的extension UIViewController { func pushToChannelDetailController(_ id: String?) { // … }}显然这样的方法根本无法满足埋点上的需求,改造一下:extension UIViewController { func pushToChannelDetailController(_ model: Channel?) { // … let value = [ “module_name” : model.module_name, “channel_id” : model.id, “channel_name”: model.name, “page_name”: self.pageName ] SensorsAnalyticsSDK.sharedInstance()?.track(key, withProperties: value) }}需要注意的是大多数情况下入口函数只接受一个具象参数是行不通的,因为随着项目的开发业务的迭代总有一些其他的模型被加入,它们同样带有能够跳转至频道主页的 id 属性。还有 pageName 映射:extension UIViewController { var pageName: String { switch self { case is ChannelDetailController: return “频道主页” } }}最后在具体的跳转处设置 module_name@objc func buttonAction( sender: Any) { model.module_name = “Header” pushToChannelDetailController(model)}整体看下来虽然可以应付点击进入频道主页的埋点,但是还是存在以下问题入口函数不够抽象在实际开发中接收不同的数据模型跳转到同一个页面的情况应该不少见,并且入口函数也是因为埋点把参数从相对抽象的 String 替换成了具象的 Channel,所以抽象 model 是首先要做的。无论接受什么类型参数,传给 ChannelDetail 还是 id,那么让一个只有带有 id 属性的 protocol 去约束模型再合适不过了。protocol CommonModelType { var id: String { get }}然后让 Channle 遵守这个协议,利用 extension 是为了看起来更解耦extension Channel: CommonModelType{}这时候入口函数就是这样func pushToChannellDetail(_ model: CommonModelType?)可以接受任何有 id 属性的模型。为了更抽象,甚至可以让 String 也遵守这个协议extension String: CommonModelType { var id: String { return self }}当然不同其他可以用来跳转到频道主页的模型都可以这样约束。数据提供方式不够优雅埋点数据除了 pageName 不属于 model 以外,其他都属于 model 本身的属性(module_name 属于额外添加),所以和参数一样,同样用 protocol 约束 Channel ,让 Channel 拥有一个直接用于提供数据的属性。protocol AnalyticsModelType { var analytics: [String: Any] { get }}让 Channel 同时遵守这两个协议,并且添加 analytics 属性extension Channel: CommonModelType, AnalyticsModelType { var analytics: [String : Any] { let value = [ “module_name” : module_name, “channel_id” : id, “channel_name”: name ] return value }}最后完整的入口函数是这样的extension UIViewController { func pushToChannellDetail(_ model: CommonModelType?) { guard let model = model else { return } let viewController = ChannelDetailViewController() viewController.id = model.id navigationController?.pushViewController(viewController, animated: true) guard let value = model as? AnalyticsModelType else { return } var properties = value.analytics properties[“page_name”] = pageName SensorsAnalyticsSDK.sharedInstance.track(key: “ChannelClick”, properties: properties) }}总的来说入口函数够抽象,无论后期增加多少种模型只要它遵循CommonModelType即可,甚至对于不熟悉项目的人来说直接传入 id 也是可以正常跳转的。埋点的细节也被隐藏到了入口函数内,而需要上报的数据又由相应的模型负责提供只要它遵循AnalyticsModelType即可。未完待续… ...

March 6, 2019 · 2 min · jiezi

Vue项目代码埋点

主流埋点方案目前主流的埋点方案包括代码埋点可视化埋点无埋点一、代码埋点 在需要埋点的节点调用接口,携带数据上传。如百度统计等; 缺点 工作量较大,每一个组件的埋点都需要添加相应的代码,入侵业务代码,增加项目复杂度。二、可视化埋点 通过可视化交互的手段,代替代码埋点。将业务代码和埋点代码分离,提供一个可视化交互的页面,输入为业务代码,通过这个可视化系统,可以在业务代码中自定义的增加埋点事件等等,最后输出的代码耦合了业务代码和埋点代码。 可视化埋点听起来比较高大上,实际上跟代码埋点还是区别不大。也就是用一个系统来实现手动插入代码埋点的过程。 缺点: 业务属性数据,例如,订单号、金额、商品数据量等,通常要调用后台的接口,可视化埋点在这方面的支持有限; 需要借助第三方工具实现。三、无埋点无埋点并不是说不需要埋点,而是全部埋点,前端的任意一个事件都被绑定一个标识,所有的事件都别记录下来。通过定期上传记录文件,配合文件解析,解析出来我们想要的数据,并生成可视化报告供专业人员分析因此实现“无埋点”统计。 缺点 无法灵活的定制各个事件所需要上传的数据 无埋点采集全量数据,给数据传输和服务器增加压力代码埋点分类 代码埋点分为 命令式埋点和声明式埋点命令式埋点 顾名思义,开发者需要手动在需要埋点的节点处进行埋点。如点击按钮或链接后的回调函数、页面ready时进行请求的发送。大家肯定都很熟悉这样的代码:// 页面加载时发送埋点请求 $(document).ready(function(){ // … 这里存在一些业务逻辑 sendRequest(params); }); // 按钮点击时发送埋点请求 $(‘button’).click(function(){ // … 这里存在一些业务逻辑 sendRequest(params); });声明式埋点 声明式埋点对命令式埋点做了改进,将埋点的代码与具体的业务逻辑解耦。从而提高埋点的效率和代码的可维护性。代码如下:// key表示埋点的唯一标识;act表示埋点方式<button v-log="{caption:‘登录页’, paras: ‘用户点击验证码发送’}">发送验证码</button> 因为项目采用Vue框架,所以使用Vue中的自定义指令完成声明式埋点。Vue.directive(’log’, { bind( el, binding ){ el.addEventListener(‘click’, ()=>{ Axios.post }) }}); 只需要在需要记录的组件中配置使用v-log指令,加上详情参数就可以完成用户轨迹记录。如下:// caption表示埋点的模块;paras表示用户的行为<button v-log="{caption:‘登录页’, paras: ‘用户点击验证码发送’}">发送验证码</button>

March 6, 2019 · 1 min · jiezi

前端埋点统计方案思考

埋点即监控用户在应用表现层的行为,于产品迭代而言至关重要。埋点数据分析是产品需求的 来源 ,检验功能是否达预期的 佐证 。前端较服务端更接近用户,本小白将在此对前端埋点统计方案述说一二。采集埋点数据可做如下分析(以百度统计为例):将 用户属性 、 用户行为 转化各类可视化图表:不同产品对数据的关注角度不同,可按需采集。如信息流产品对停留时长的关注度更高(统计页面访问 & 跳出时间),商城类较注重“复购率”(统计新老用户),广告类更追求最大限度。埋点统计通常分两类:页面访问量统计功能点击量统计页面访问量统计页面访问量统计通常分两类:PV :页面访问人次UV :页面访问人数页面访问量,并非仅仅取决于其内容吸引力,影响因素包含入口 外观 、 位置 、 深度 等等(在此不考虑刚需)。入口外观属 UI 设计范畴,入口位置可通过分析用户点击热力图调整,入口深度可通过分析用户访问路径调整。用户点击 热力图 形如:将核心页面入口置于热力图红色区域?采集页面加载 from 、 to 以获知用户访问路径:分析可知用户普遍 访问深度 、每一深度 & 每一页面的 流失率 等,依照结果调整核心页面入口源、入口深度?页面访问量,也并非仅仅取决于产品设计。假若 PV 稳定的页面访问量 爆跌 ,便需考虑其加载成功率了(或许是枚技术 bug)。前端如何实现全局 PV 统计,以 Vue 应用为例。方案一通过在入口文件 index.js 全局定义 Router.beforeEach :import App from ‘./app’import Router from ‘./router’Router.beforeEach((to, from, next) => { App.logEvent({ type: ‘visit’, name: to.name, time: new Date().valueOf(), params: { from: { name: from.name, path: from.path, query: from.query }, to: { name: to.name, path: to.path, query: to.query } } }) next()})停留时长可通过 (跳转页 time - 当前页 time) 获知,但关闭应用时如何统计?监听应用关闭 onbeforeunload + onunload?其中 App.logEvent 为自定义 Vue 插件 App 中的 method,用于向服务器发起 埋点上报请求 :import Request from ‘./utils/request’const App = { // … logEvent (opts) { Request({ url: ‘/log/event’, method: ‘POST’, data: { type: opts.type, name: opts.name, time: opts.time, params: opts.params || {} } }) }}App.install = (Vue, options) => { Vue.prototype.$app = { // … logEvent: App.logEvent }}export default App方案二通过在入口文件 index.js 全局注册混入 beforeRouteEnter 、 beforeRouteLeave 对象:import Vue from ‘vue’Vue.mixin({ beforeRouteEnter (to, from, next) { next(vm => { vm.$app.logEvent({ type: ‘visit’, name: to.name, time: new Date().valueOf(), params: { from: { name: from.name, path: from.path, query: from.query }, to: { name: to.name, path: to.path, query: to.query } } }) }) }, beforeRouteLeave (to, from, next) { this.$app.logEvent({ type: ‘visit’, name: to.name, time: new Date().valueOf(), params: { from: { name: from.name, path: from.path, query: from.query }, to: { name: to.name, path: to.path, query: to.query } } }) next() }})关闭应用时 beforeRouteLeave 是否触发?上述方案存在明显缺陷:官方曰慎用全局混入对象!!!对于页面同名钩子函数 beforeRouteEnter 、 beforeRouteLeave ,如何 merge ?如何 next ?含子路由的页面将调用 2 次 beforeRouteEnter 、 beforeRouteLeave ,PV 无形翻倍…我猜此刻有打全局混入 created 、 destroyed 并通过 this.$route 获知访问对象主意的人了,试试看?令人不知所措的输出,打印次数与 路由表 长度一致嗷其中 this.$app.logEvent(vm.$app.logEvent) 等同方案一中 App.logEvent ,不再赘述。如何恰当选取全局 PV 统计方案?SPA 应用 :仅单入口,在入口文件全局定义 Router.beforeEach 方便可行。MPA 应用 :多入口,在每个入口文件定义 Router.beforeEach ?可封装公用逻辑(伪装单入口),免去重复构造 entry 的成本。SPA + MPA 混合应用 :emmmmmm…采用 MPA 应用的统计方案。SSR 应用 :为追求更好的 SEO 而采用服务端渲染(SSR)。以 Jinja(Python 模板)为例,调用 TemplateView 则为渲染页面(不同于前后端分离项目,服务端无法获知接口调用与页面渲染的对应关系),统计其调用次数及 TemplateName 可知页面 PV。至于 UV,统计 PV 时采集 userId 去重即可。若应用无用户管理体系,采集 IP 、 deviceId 也可粗略得知 UV(不精准)。功能点击量统计不同功能的点击量不同,同一功能不同区域的点击量也不同:按钮点击量,影响因素包含按钮 外观 、 位置 、 入口 等等(在此不考虑刚需)。按钮外观属 UI 设计范畴,按钮位置可通过分析用户点击热力图调整,按钮入口可通过分析触发源分布调整。举一实例:运营同学会将一张图片裁切成 n 个区域,点击每一区域所推荐商品不同。统计区域点击坐标,据热力图调整商品排序以求 利益最大化 。前端如何实现功能点击量统计?本人将功能点击分两类:带业务接口请求无业务接口请求方案一将埋点上报混入业务接口请求,无接口请求的点击采用自定义上报:其中 param keys 指代需上报的业务请求参数 key list (并非全部参数均需随埋点上报)。上述方案大大节约请求数,但存在明显缺陷:将埋点上报混入业务接口,上报 crash 不仅丢失统计数据,还将影响主功能。统计与业务 高耦合 ,两者尽量不混于同一服务。方案二将所有点击事件视为同一类,走统一上报接口:logEvent (opts) { Request({ url: ‘/log/event’, method: ‘POST’, data: { type: opts.type, name: opts.name, time: opts.time, params: opts.params || {} } })}上述方案也存在明显缺陷:请求量翻倍:但统计与业务服务拆分后,请求并非同一组服务器承担。待上报的点击事件函数均需调用 logEvent :封装一枚附带埋点上报的 组件 ,以 Vue 为例。<template> <div class=“vc-trace” @click=“triggerClick”> <slot></slot> </div></template><script>import Request from ‘./utils/request’export default { name: ‘Trace’, props: { type: { type: String, default: ’’ }, name: { type: String, default: ’’ }, from: { type: String, default: ’’ }, params: { type: Object, default: () => ({}) } }, methods: { triggerClick () { Request({ url: ‘XXX/log/event’, method: ‘POST’, data: { type: this.type, name: this.name, from: this.from, time: new Date().valueOf(), params: this.params } }) } }}</script>方案本无优劣,适合才更重要,需综合考虑 产品设计 、 产品使用度 、 服务利用率 等等。例使用度较低应用可将统计与业务混于同一服务以节约成本,使用度较高应用可采取 本地缓存 、 批量上报 以降低服务压力,但批量上报是否加大统计 误差 ?本文所述仅冰山一角,欢迎大家留言宝贵经验作者:呆恋小喵我的后花园:https://sunmengyuan.github.io…我的 github:https://github.com/sunmengyuan原文链接:https://sunmengyuan.github.io… ...

December 17, 2018 · 3 min · jiezi