首先回答一下什么是埋点:
埋点是一种用于跟踪用户在网站或应用中行为的数据采集技术 ,通过记录点击 、浏览 等操作,帮助团队进行用户行为分析、AB 实验、错误监听,指导优化方向和资源分配
为了让读者能够从头到尾彻底学会埋点,我会分别从 埋点的概念与使用、如何实现一个埋点 sdk 入手 这篇文章核心围绕埋点的概念与使用 展开,涉及到几个维度:
我们直接进入主题,相信能给大家带来不少的收获
一、埋点的基本概念在业务开发中,埋点是一定要做的,那作为一名合格的研发,我们先要了解埋点都有什么类型,可以有效解决什么问题,这一部分的内容比较基础,网上也有很多精彩的文章,有一定基础的同学可以直接跳过~
我们会从几个维度进行基本概念的介绍:
1. 监控类型基于我们要监控的内容,可以分为:数据监控、性能监控、异常监控等三个部分 我们先讲述一下几个类型的基本概念,后面会详细讲解如何实现的
1.1 数据监控 特点:数据监控主要指对用户行为、业务数据等进行埋点监控,采集并上报关键信息。他的目的是: => 通过收集用户在系统中的行为数据,帮助产品经理、运营人员更好地分析用户行为,优化产品决策
举例:用户操作数据 :点击某个按钮、页面的浏览路径、搜索的关键词、用户停留时间等。电商平台 :监控用户的商品浏览记录、加入购物车、购买行为、支付情况等数据。社交平台 :记录用户点赞、评论、分享、发布动态等操作。例如,在一个电商网站中,当用户点击了“购买”按钮,系统会埋点记录这次点击事件,包括用户 ID、商品 ID、点击时间等,都会被上报到服务器,用于分析转化率、购物路径优化等
优缺点:为业务决策提供有力的数据支持,帮助进行 AB 实验、用户行为分析等 1.2 性能监控 特点:性能监控关注的是系统性能的表现,重点监控页面的加载时间、渲染时间、资源请求时间。他的目的是: => 帮助开发者了解项目的性能数据,为性能优化做导向
举例:监控页面从开始加载到完全展示各阶段的时间,如 FP、FCP、LCP、JS 初始化、主接口加载 等
优缺点:采集的部分性能指标(如两秒开率 等)可能需要与服务端结合,增加开发复杂度 1.3 异常监控 特点:异常监控主要用于捕获系统中的异常情况,包括 JavaScript 错误、接口请求失败、未处理的 Promise 错误、资源加载失败等,它的目的是: => 通过及时捕获并上报异常信息,帮助开发者迅速定位问题、解决问题
举例:JavaScript 错误 :捕获页面中运行时 的 JavaScript 错误,如未定义变量。例如,当我访问某个页面时,某个 JavaScript 模块发生了错误,导致页面无法正常展示,异常监控会将错误信息、错误堆栈、用户操作等信息记录并上报接口请求失败 :监控 API 请求的状态码、超时情况等,如请求返回 500 错误或出现网络超时等问题资源加载失败 :监控页面中图片、视频、CSS、JS 文件的加载失败情况,帮助开发者检查资源引用路径或网络问题 优缺点:实时捕获异常,帮助开发者迅速发现并修复问题,提高用户体验 捕获到的异常信息可能过多,数据筛选和处理的难度较高 需要额外的策略来区分重要异常和无关紧要的异常,以避免过多无效的报警 2. 上报方式在了解了埋点的监控类型后,我们再看看埋点的上报都有哪些方式
2.1 手动上报 特点:手动上报是指我们基于业务需求,在代码中显式地添加埋点逻辑 每当需要记录用户行为或系统事件时,手动调用上报函数,将数据发送至服务器,这种方式的可控性强,开发者可以精确地控制埋点位置和上报时机
举例:在用户点击某个按钮时,开发者会在按钮的点击事件中调用埋点上报函数,如: button.addEventListener('click', () => {
sendEvent('click_button', { userId: '12345', time: Date.now() });
});
这种方式能够精确记录用户点击的时间、操作对象等详细信息。
页面展示 :在页面加载完成时,埋点记录页面的展示情况window.addEventListener('load', () => {
sendEvent('page_view', { page: 'homepage', time: Date.now() });
});
组件 DOM 超出 50% 曝光 :使用 IntersectionObserver
API 监测页面上特定组件的可见性,当组件的可见部分超过 50% 时,自动上报组件的曝光情况const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && entry.intersectionRatio > 0.5) {
sendEvent('component_exposure', { component: 'banner', time: Date.now() });
}
});
}, {
threshold: [0.5] // 超过50%可见时触发
});
const component = document.querySelector('#banner');
observer.observe(component);
IntersectionObserver - Web API | MDN (mozilla.org)
优缺点:如果埋点较多,容易遗漏或出现冗余埋点,影响代码质量,甚至导致数据不置信 2.2 可视化上报 特点:可视化上报是通过图形界面或后台工具来配置埋点,无需开发人员手动编写代码。在业务人员或测试人员通过可视化工具对页面上的元素(如按钮、链接等)进行选择和配置后,系统自动生成埋点逻辑并上报数据。这种方式降低了技术门槛,使非开发人员也能够参与埋点配置
大家可能好奇这种可视化的操作是如何实现的,这个会在后续的全流程实现 模块中详细介绍~
举例:使用一些埋点平台(如 GrowingIO、神策数据等),业务人员可以在后台系统的可视化界面上点击页面元素,配置该元素的埋点逻辑。系统会自动捕获该元素的操作,并将数据上报至服务器 优缺点:埋点配置灵活,可以根据需要随时调整,无需发布新的代码 灵活性不如手动上报,复杂的业务逻辑可能无法通过可视化工具实现 2.3 自动上报(无埋点) 特点:自动上报(无埋点)是指系统通过框架或插件自动捕获用户行为或系统事件,无需手动埋点
举例:使用无埋点方案时,项目初始化阶段会注册相关的监听,这种方式依赖于 SDK 的能力,自动监听页面上的交互事件、网络请求、资源加载等,并将数据上报到后台进行分析 优缺点:减少了开发和运维成本,开发人员无需为每个事件手动编写埋点逻辑 灵活性较低,自动捕获的行为数据可能无法满足某些特定业务需求,且不容易精确控制上报的内容和时机 当然,一个优秀且完整的企业级埋点系统,是灵活支持各类监控类型的,不同的类型专门服务不同的场景,实现对目标项目的全方位监控
3. 埋点的生命周期在了解了埋点类型和上报方式后,我们再来学习一下:一个需求中埋点的全流程
如果再精确到研发主要负责的范围 ,我们可以给出一个更精细化的图:
虽然该流程覆盖了埋点从需求确认到数据消费的各个步骤,但在需求确认、开发自测、数据监控等环节可能存在数据遗漏、埋点不准确、数据质量监控不足 等问题:
需求确认与埋点设计环节容易遗漏场景 :埋点需求的确认和埋点方案设计环节,产品经理对用户行为理解不全面,或者开发人员没有参与设计环节,可能会遗漏一些关键场景或不常见的边缘操作
开发和自测环节容易出现数据不一致 :在埋点开发和自测阶段,开发人员可能会因为疏忽或理解偏差,导致埋点的事件数据与需求不符。例如,埋点位置不准确、上报参数不完整、或埋点上报的时间点不对
埋点对性能的影响未充分考虑 :如果埋点过多或者不合理设置上报频率,可能会导致页面性能下降,例如页面加载变慢、用户操作卡顿等问题,影响用户体验
二、埋点研发流程优化我们可以通过自动化工具
、加强测试覆盖
等方式为了提高埋点质量问题 ,同时确保埋点对性能的影响最小化,至于具体怎么做,下面会给出一些实践经验:
1. 类型代码约束我们可以通过 Typescript
为埋点的上报提供类型约束,它能带来的收益很明确: 尽可能避免埋点过程中出现数据格式、字段不一致 等问题(你要是不管 error,咱也没办法😀)
了解了收益后,我们再来学习一下类型代码的消费方式 ,以及一个合理的类型代码结构
我们先提供一套最基本的埋点上报函数的封装(sendEvent):
type LogParams = Record;
export const sendEvent = (event: string, params?: LogParams) => {
// 埋点上传
};
然而这种类型提示非常的粗糙,对于上报时的检测效果很不理想,那如果要实现非常严格的代码检查,我们可以考虑用 ts 类型体操实现升级:
export const sendEvent = (
event: T,
params?: TrackInterfaces[T]
) => {
// 埋点上传
};
export interface TrackInterfaces {
main_picture_click: MainPictureClick;
page_view: PageView;
}
export type EventNames = "main_picture_click" | "page_view";
/**
* 页面浏览
*/
export interface PageView {
/**
* 应用名称
*/
appName: string;
/**
* 平台
*/
device_platform: string;
/**
* 来源
*/
enter_from: string;
/**
* 链接
*/
page_url: string;
/**
* 页面名称
*/
page_name: string;
}
可以看到我们通过一个枚举类型,维护了项目所有包含的类型名称,通过 T extends EventNames
获取了函数中具体的事件名,再通过 TrackInterfaces
映射到了具体的一个类型,比如:
// 事件名 page_view => 映射类型 PageView
sendEvent('page_view',{
enter_form: 'douyin'
page_name: 'home'
// 参数……
})
话都说到这里了,我们可以再深入一下,对于通参(公共参数,比如操作系统、手机型号等),如果每次都要传递,是不是有点麻烦,那如果我们要抽离出来,该怎么做呢?代码如下:
export interface TrackInterfaces {
main_picture_click: MainPictureClick;
page_view: PageView;
}
export type EventNames = "main_picture_click" | "page_view";
// 公共属性
type CommonParams = {
device_platform: string;
// ……
};
type trackParams = Partial & Omit;
export const sendEvent = (
event: T,
params?: trackParams[T]
) => {
// 埋点上传
};
通过这种方式,我们把通参从具体的类型中剔除,可以放在一个独立的方法(比如叫 addCommonParams)中维护(一个变量,如 commonParams),在其他事件上报时插入即可
话说回来,维护一个类型代码结构也是相当困难啊,尤其是事件很多的时候,这种情况下,就必须要我们通过一些自动化工具来实现埋点数据转类型代码 了
这个问题是我在团队中遇到的,基于这个问题,我开发了一套基于 VSCode
的埋点类型转换插件 ,核心借助 quick_type 实现,核心的实现逻辑如下:
从埋点管理平台获取事件数据结构 => 事件数据格式解析 => 投喂 quick_type
进行转换 => 进一步加工成合适的格式 => 插入项目代码
大家要是感兴趣也可以实现一下,后续我会考虑把这个插件重写一份并开源出来,也可以用 sdk 的方式提高适用面
2. 埋点测试用例测试用例可以用很多种方式进行,比如单元测试、抓包测试 等,其中单元测试可以通过如 Jest
或 Mocha
实现,存在一定 coding 成本,我们这边展开讲讲抓包测试的实现方式
最简单粗暴的,我们可以使用浏览器开发者工具 、Charles 等工具,捕获网页上的网络请求,观察上报的参数是否符合埋点事件的要求 然而这样的方式比较笨,批量检查起来会比较麻烦,因此一般的埋点管理平台会提供一个界面,能够实时显示埋点上报的具体内容,类似于抓包工具,可以有效地提高测试阶段的效率与准确率
对于流程的优化,还有很多的思路,比如聚合上报埋点事件(单位时间内事件打包成一个请求进行上报),可以有效降低请求带来的性能影响,具体的实现就先不在这里详细讲述了
三、埋点数据的消费经验高效消费埋点数据是数据驱动决策的重要环节,下面我将从多个角度来解释如何高效消费埋点数据,并进行有效的场景分析
等等,你说这都是产品或数据分析师的活?研发应该努力跳出自己的舒适圈 ,从各方面提高自己的业务能力、解决问题的能力,尤其在如今数据驱动业务 的环境下,能做好这些的研发会更有核心竞争力
PV 和 UVUV(Unique Visitor) :独立访客数我们可以通过 PV 和 UV 评估不同页面的访问量。可以通过聚合查询每日/每周的 PV 和 UV 进行趋势分析:
SELECT page_name, COUNT(DISTINCT user_id) AS UV, COUNT(*) AS PV
FROM page_view_events
WHERE date BETWEEN '2024-09-01' AND '2024-09-08'
GROUP BY page_name;
当然 SQL
这一步,埋点平台都会做好了,我们不用关心具体的实现,这里展示只是为了更好说明逻辑
事件行为分析事件行为分析通过埋点捕捉用户的操作习惯和行为路径,是我们最常用的分析模式,常用的操作如下:
1. 分析 click_button 事件中按钮名称为某个具体值的 PVSELECT
button_name,
COUNT(*) AS PV
FROM events
WHERE event_name = 'click_button'
AND button_name = 'submit' -- 按钮名称为 'submit'
GROUP BY button_name;
WHERE event_name = 'click_button'
:过滤出 click_button
事件,表示按钮点击行为AND button_name = 'submit'
:只筛选按钮名称为 submit
的点击事件COUNT(*) AS PV
:计算按钮被点击的总次数,即该按钮的 PV 结果:假设返回的 PV 为 500,表示 submit
按钮被点击了 500 次
2. 分析 purchase
事件的用户停留时长的平均数和 90 分位数90 分位数 (90th percentile)表示的是在一组数据中,90% 的数值小于等于这个数值,而另外 10% 的数值大于这个数值
SELECT
AVG(duration) AS avg_duration, -- 计算平均持续时间
PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY duration) AS p90_duration -- 计算 90 分位数
FROM purchase_events
WHERE event_name = 'purchase';
AVG(duration)
:计算所有 purchase
事件的 duration
字段的平均值PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY duration)
:计算 duration
的第 90 分位数WHERE event_name = 'purchase'
:只过滤出购买事件的数据 结果:avg_duration
:平均持续时间,例如返回 180
秒,表示用户平均花费 180 秒完成购买流程p90_duration
:90 分位数的持续时间,例如返回 300
秒,表示 90% 的用户花费少于 300 秒完成购买 路径转换分析路径转换分析用于跟踪用户从进入系统到完成某个目标(如购买、注册、下单)的转化过程,可以查看用户经过的关键步骤 ,发现在哪一步可能有大量用户流失
转换分析应该按用户 ID 逐个跟踪,确保每个用户的行为链可以被完整记录下来
假设我们有一个电商平台,目标是分析从用户进入首页 到完成购买 的路径转化率
步骤 1: 定义事件表结构假设数据库中存储了用户的事件数据,每个事件都有 user_id
和 event_name
:
CREATE TABLE user_events (
user_id VARCHAR(50), -- 用户 ID
event_name VARCHAR(50), -- 事件名,如 "page_view"、"add_to_cart"
event_time TIMESTAMP -- 事件发生时间
);
步骤 2: 查询每个用户在各个步骤的转化情况为了简单起见,假设路径有三个关键步骤:
下面的 SQL 查询可以计算每个用户是否完成了从 page_view
到 add_to_cart
再到 purchase
的全路径。
SELECT
user_id,
MAX(CASE WHEN event_name = 'page_view' THEN 1 ELSE 0 END) AS viewed_product,
MAX(CASE WHEN event_name = 'add_to_cart' THEN 1 ELSE 0 END) AS added_to_cart,
MAX(CASE WHEN event_name = 'purchase' THEN 1 ELSE 0 END) AS purchased
FROM user_events
WHERE event_name IN ('page_view', 'add_to_cart', 'purchase')
GROUP BY user_id;
步骤 3: 计算路径中的转化率上面的查询会生成一个结果表,显示每个用户是否完成了某个操作步骤。基于这些数据,可以进一步计算转化率。例如,下面的 SQL 可以计算从查看产品
到加入购物车
再到完成购买
的转化率。
WITH conversion_data AS (
SELECT
user_id,
MAX(CASE WHEN event_name = 'page_view' THEN 1 ELSE 0 END) AS viewed_product,
MAX(CASE WHEN event_name = 'add_to_cart' THEN 1 ELSE 0 END) AS added_to_cart,
MAX(CASE WHEN event_name = 'purchase' THEN 1 ELSE 0 END) AS purchased
FROM user_events
WHERE event_name IN ('page_view', 'add_to_cart', 'purchase')
GROUP BY user_id
)
SELECT
COUNT(*) AS total_users,
SUM(CASE WHEN viewed_product = 1 THEN 1 ELSE 0 END) AS total_viewed_product,
SUM(CASE WHEN viewed_product = 1 AND added_to_cart = 1 THEN 1 ELSE 0 END) AS total_added_to_cart,
SUM(CASE WHEN viewed_product = 1 AND added_to_cart = 1 AND purchased = 1 THEN 1 ELSE 0 END) AS total_purchased,
(SUM(CASE WHEN viewed_product = 1 AND added_to_cart = 1 THEN 1 ELSE 0 END) * 100.0) / SUM(CASE WHEN viewed_product = 1 THEN 1 ELSE 0 END) AS cart_conversion_rate,
(SUM(CASE WHEN viewed_product = 1 AND added_to_cart = 1 AND purchased = 1 THEN 1 ELSE 0 END) * 100.0) / SUM(CASE WHEN viewed_product = 1 AND added_to_cart = 1 THEN 1 ELSE 0 END) AS purchase_conversion_rate
FROM conversion_data;
输出结果:total_viewed_product
:访问了产品详情页的用户数total_added_to_cart
:添加了购物车的用户数cart_conversion_rate
:从查看产品到加入购物车的转化率purchase_conversion_rate
:从加入购物车到完成购买的转化率最终的结果一般会以漏斗图的形式展现,如:
通过这些结果,我们可以直观地看到每个关键节点的用户流失情况 ,并根据这些转化率数据进一步优化流程或产品设计 比如,add_to_cart
的转化率较高,但 purchase
的转化率较低,这可能意味着在支付环节出现了问题
异常值与极端行为分析有时,我们需要分析极端的用户行为(如长时间不操作或过于频繁操作 )。通过分析这些异常行为,研发可以定位到系统中的潜在问题或优化点
例如,某个用户反复多次点击某个按钮,这可能是系统卡顿或用户困惑导致的行为。
SELECT user_id, COUNT(*) AS click_count
FROM event_log
WHERE event_name = 'click_button'
GROUP BY user_id
HAVING click_count > 10;
数据变更归因在埋点数据分析过程中,数据变更归因 也是非常重要的,归因分析的场景可以应用于多种情况,如性能优化效果、A/B 测试结果等
以下是几个常见的数据变更归因分析 的应用场景:
1. 某次性能优化,1.5 秒开率增长远大于 2 秒开率增长在一次性能优化后,发现产品的秒开率 (即页面在 1.5 秒内完全加载的比例)有了显著提升,但相比之下,2 秒内的开率 增长较为缓慢
其中,优化前 2s 开率 > 50%
用户的秒开率呈近似正态分布的形式,一次性能优化可以理解为分布左移,表现如下(图画的有点粗糙,请见谅):
image.png 可见有时候数据分析是需要经过严谨的推论才能给出置信的答案的
这时候又要问了:这不应该是数据分析师(DA)做的事吗?汇报的时候 DA 不一定会帮你分析,很多时候都得依赖自己,还是那个标准:跳出自己的舒适圈~
2. A/B 实验增长、下跌分析归因A/B 实验是常用的用增手段,用来验证某个新功能或设计的效果(在 C 端业务比较常见) 通过对比 A 组(原始版本)和 B 组(新版本)的转化率,可以分析出某些指标的增长或下跌原因
我们可以分析两个版本在关键指标上的表现差异,找出新版本带来的改进点或不足之处 例如:
B 组的页面停留时间增加,说明新版本可能增强了用户的兴趣 B 组的转化率下降,可能是用户在新版本的交互上出现了问题