埋点原理与阿里云 SLS 说明
1. 这套埋点是什么
埋点可以理解为一条完整的数据链路:前端在用户点击、页面访问、资源加载失败、JS 报错等时机生成一条事件数据,然后通过公司封装的埋点 SDK 上报到阿里云 SLS 日志服务,后续再通过 SLS 控制台进行查询、统计、看板和告警。
当前工程主要涉及两层 SDK:
text
@core/point-map-tq 业务埋点映射层
@core/point 底层上报与 SLS 对接层
@core/point-map-tq 更偏业务:它把 pointId、router 这类业务标识映射成标准埋点元数据,比如模块、页面、事件名称、事件类型。
@core/point 更偏基础设施:它负责初始化 SLS Tracker、生成访客 ID、补充公共字段、构造日志结构,并把日志发送到阿里云 SLS。
2. 解决什么问题
埋点解决的是"用户在系统里做了什么、页面是否正常、关键流程是否顺畅、异常是否集中爆发"的问题。
在 CRM 场景里,常见用途包括:
- 统计功能使用情况,比如导入、导出、新建客户、写跟进、外勤轨迹查看。
- 分析页面访问与停留,比如首页、客户详情页、数据统计页是否被频繁访问。
- 排查线上问题,比如 JS 报错、资源加载失败、接口资源异常。
- 监控性能体验,比如 FPS 卡顿、长任务、页面加载性能。
- 支撑产品决策,比如某个入口没人点、某个功能点击量突然下降。
3. 工程里的核心文件
3.1 埋点 Excel 与生成脚本
package.json 中的脚本:
json
{
"gen-point-map-data": "xlsx-conversion-map-data ./src/point-xlsx --outdir ./src/map-data --clickKey event_id --extension .ts"
}
它负责把埋点 Excel 转成前端可直接 import 的 TS 映射文件。
输入:
text
src/point-xlsx/click.xlsx
src/point-xlsx/page.xlsx
输出:
text
src/map-data/click.ts
src/map-data/page.ts
其中 click.xlsx 通常维护点击事件,page.xlsx 通常维护页面曝光、页面访问或路由类事件。生成后的 click.ts 和 page.ts 是一个对象映射表,key 通常是 event_id 或路由,value 是事件元信息。
3.2 应用启动初始化
入口在 src/app.ts:
ts
import { initPoint } from '@core/point-map-tq';
import clickData from './map-data/click';
import pageData from './map-data/page';
initPoint({
project: 'crm-common',
pageMap: pageData,
clickMap: clickData,
SLSTracker: {
project: 'prod-xxx-web',
logstore: 'xxx-web',
region: 'xxx',
source: 'connect',
time: 2,
},
});
这段逻辑完成了三件事:
- 把
clickData和pageData注入到@core/point-map-tq。 - 设置当前项目名
crm-common。 - 把 SLS 配置传给底层
@core/point,让后续事件可以进入prod-xxx-web / xxx-web日志库。
3.3 错误和资源异常初始化
src/app.ts 里还有一段生产环境初始化:
ts
if (
ENV?.includes('prod') &&
(process.env.NODE_ENV !== 'development' || localStorage.getItem('__report'))
) {
initReport();
}
initReport() 注册两类监控:
observerResource():监听资源加载失败,比如脚本、fetch、跨域取消、接口 500 等。observerJsError():监听 JS 执行报错,并做简单去重。
示例:
ts
export function initReport() {
observerResource();
observerJsError();
setTimeout(() => {
initSLSTracker({
project: 'prod-taiqing-web',
logstore: 'taiqing-web',
region: 'cn-hangzhou',
source: isTq() ? 'taiqing-web-pc' : 'dingding-web-pc',
time: 1,
});
}, 500);
}
这里要注意:initReport() 偏线上异常监控,initPoint() 偏标准业务埋点,它们都会触达 SLS,但上报来源、事件类型和字段侧重点不同。
4. 整体数据流
渲染错误: Mermaid 渲染失败: Parse error on line 9: ...operties] H --> I[@core/point postEv ----------------------^ Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'LINK_ID'
这个流程里最重要的思想是:业务代码不应该到处手写完整埋点字段,而是只传 pointId 和必要业务属性,标准元数据来自 Excel 生成的 map。
5. 点击埋点的原理
业务里常见写法:
ts
pointFun({
pointId: 'applicationLegwork_viewFieldTrajectory-2509191119',
pointType: 'click',
});
@core/point-map-tq 的核心逻辑可以简化理解为:
ts
function pointFun({ pointId, pointType = 'click', pointData = {}, event_attr = [] }) {
const { clickMap, pageMap } = getMapData();
let meta = clickMap[pointId];
if (pointType === 'expose') {
meta = pageMap[pointId];
}
const data = {
event_id: pointId,
...meta,
properties: { ...pointData },
event_attr,
...getCompanyInfo(),
...getPublicData(),
};
postEvent(data, pointType === 'expose' ? 'click' : pointType);
}
也就是说,一次点击埋点大概会生成这样的数据:
json
{
"event_id": "applicationLegwork_viewFieldTrajectory-2509191119",
"event_type": "click",
"module": "应用中心-外勤拜访",
"page": "外勤轨迹",
"event_name": "查看外勤轨迹",
"trigger": "查看某个外勤轨迹详情时",
"company_id": "企业 ID",
"company_user_id": "企业成员 ID",
"trace_id": "本次访问链路 ID",
"properties": {}
}
这个结构的好处是:
- 业务代码只关心
pointId。 - 埋点口径由 Excel 集中维护。
- SLS 里可以用
event_id、module、page、event_name直接分析。 - 后续修改事件名称或页面归属时,可以更新 Excel 后重新生成 map。
6. 页面曝光和停留时长原理
src/app.ts 里通过 onRouteChange 接入页面上报:
ts
const pageReport = createPageReport('pageView-2510131002');
export function onRouteChange(routeObj) {
pageReport(routeObj);
}
createPageReport() 内部做了防抖和路径去重:
ts
export function createPageReport(pageId: string) {
let lastReportedPath = '';
let reportTimer: NodeJS.Timeout | null = null;
function pageReport(routeObj: any) {
const currentPath = routeObj?.location?.pathname;
if (reportTimer) clearTimeout(reportTimer);
reportTimer = setTimeout(() => {
if (currentPath === lastReportedPath) return;
reportRouteChange(pageId);
}, 1000);
}
return pageReport;
}
为什么要防抖?因为 CRM 路由可能会发生多次连续跳转,比如先访问 /sales,再重定向到第一个菜单页,还可能追加 search 参数。如果每次都上报,就会造成页面访问数据重复。
@core/point-map-tq 的停留时长追踪器会记录进入页面时间,在路由变化或页面关闭时计算停留时长:
text
进入页面 -> 记录 enterTime
路由切换 -> 当前时间 - enterTime = stay_duration
上报上一页停留时长 -> 初始化新页面状态
页面关闭 -> 先写入 localStorage 缓存,下次启动再补报
对应流程:
SLS point-map-tq createPageReport Umi 路由 用户 SLS point-map-tq createPageReport Umi 路由 用户 #mermaid-svg-xKZHVjic5jRXKR3N{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xKZHVjic5jRXKR3N .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xKZHVjic5jRXKR3N .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xKZHVjic5jRXKR3N .error-icon{fill:#552222;}#mermaid-svg-xKZHVjic5jRXKR3N .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xKZHVjic5jRXKR3N .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xKZHVjic5jRXKR3N .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xKZHVjic5jRXKR3N .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xKZHVjic5jRXKR3N .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xKZHVjic5jRXKR3N .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xKZHVjic5jRXKR3N .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xKZHVjic5jRXKR3N .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xKZHVjic5jRXKR3N .marker.cross{stroke:#333333;}#mermaid-svg-xKZHVjic5jRXKR3N svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xKZHVjic5jRXKR3N p{margin:0;}#mermaid-svg-xKZHVjic5jRXKR3N .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-xKZHVjic5jRXKR3N text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-xKZHVjic5jRXKR3N .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-xKZHVjic5jRXKR3N .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-xKZHVjic5jRXKR3N .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-xKZHVjic5jRXKR3N .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-xKZHVjic5jRXKR3N #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-xKZHVjic5jRXKR3N .sequenceNumber{fill:white;}#mermaid-svg-xKZHVjic5jRXKR3N #sequencenumber{fill:#333;}#mermaid-svg-xKZHVjic5jRXKR3N #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-xKZHVjic5jRXKR3N .messageText{fill:#333;stroke:none;}#mermaid-svg-xKZHVjic5jRXKR3N .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-xKZHVjic5jRXKR3N .labelText,#mermaid-svg-xKZHVjic5jRXKR3N .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-xKZHVjic5jRXKR3N .loopText,#mermaid-svg-xKZHVjic5jRXKR3N .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-xKZHVjic5jRXKR3N .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-xKZHVjic5jRXKR3N .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-xKZHVjic5jRXKR3N .noteText,#mermaid-svg-xKZHVjic5jRXKR3N .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-xKZHVjic5jRXKR3N .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-xKZHVjic5jRXKR3N .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-xKZHVjic5jRXKR3N .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-xKZHVjic5jRXKR3N .actorPopupMenu{position:absolute;}#mermaid-svg-xKZHVjic5jRXKR3N .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-xKZHVjic5jRXKR3N .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-xKZHVjic5jRXKR3N .actor-man circle,#mermaid-svg-xKZHVjic5jRXKR3N line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-xKZHVjic5jRXKR3N :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 切换页面onRouteChange防抖和路径去重reportRouteChange(pageId)计算上一页 stay_durationpostEvent expose 日志初始化当前页 enterTime
7. 企业和用户信息怎么补齐
埋点不能只有事件本身,还需要知道是哪个企业、哪个用户触发的。
CRM 主布局在获取 profile 后会调用:
ts
setCompanyInfo({
company_id: res?.company_id,
company_user_id: res?._id,
extId: 'phoebe',
});
这部分数据会在 pointFun 里通过 getCompanyInfo() 拼到每条标准业务埋点中。
除此之外,@core/point 在发送到 SLS 前还会兜底从 sessionStorage 里的用户信息补齐字段:
ts
const logData = {
distinct_id: data.distinct_id,
trace_id: data.trace_id,
project: data.project,
indi_user_id: data.indi_user_id || userInfoJson.indi_user_id,
company_id: data.company_id || userInfoJson.company_id,
company_user_id: data.company_user_id || userInfoJson._id,
company_user_name: data.company_user_name || userInfoJson.name,
event_id: data.event_id || data.eventId,
event_type: type || data.event_type || data.eventType,
};
这里的 distinct_id 是游客 ID,通常由 FingerprintJS 生成并缓存在 localStorage 中,用于区分浏览器访问实例。
8. 上报到 SLS 的底层过程
底层 @core/point 使用的是阿里云 SLS Web Tracking 能力。初始化时会创建 SlsTracker:
ts
slsTracker = new SlsTracker({
host: finalConfig.host,
project: finalConfig.project,
logstore: finalConfig.logstore,
time: finalConfig.time,
count: finalConfig.count,
topic: finalConfig.topic,
source: finalConfig.source,
});
关键配置含义:
| 字段 | 含义 |
|---|---|
host |
SLS 服务域名,例如 xxx.aliyuncs.com |
project |
SLS Project,类似日志项目空间 |
logstore |
SLS Logstore,真正存储日志的库 |
time |
批量发送时间间隔,单位秒 |
count |
批量发送条数阈值 |
topic |
日志主题,可用于区分日志类型 |
source |
日志来源,可用于区分太擎、钉钉、PC、H5 等来源 |
当前 CRM 配置里主要写入:
text
project: prod-xxx-web
logstore: xxx-web
region: xxx
source: connect / xx-web-pc / xxx-web-pc
8.1 STS 临时凭证
前端不能直接写死阿里云永久 AccessKey,否则风险很大。所以 SDK 内部使用 STS 临时凭证插件:
text
前端生成 nonce + timestamp + signature
请求后端 API
后端校验签名
后端返回临时 access_key_id / access_key_secret / security_token
SLS Web Tracking 使用临时凭证写入日志
凭证定时刷新
流程图:
阿里云 SLS 阿里云 STS CRM 后端 前端 SDK 阿里云 SLS 阿里云 STS CRM 后端 前端 SDK #mermaid-svg-WeVjkt3UWoN1sioT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WeVjkt3UWoN1sioT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WeVjkt3UWoN1sioT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WeVjkt3UWoN1sioT .error-icon{fill:#552222;}#mermaid-svg-WeVjkt3UWoN1sioT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WeVjkt3UWoN1sioT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WeVjkt3UWoN1sioT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WeVjkt3UWoN1sioT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WeVjkt3UWoN1sioT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WeVjkt3UWoN1sioT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WeVjkt3UWoN1sioT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WeVjkt3UWoN1sioT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WeVjkt3UWoN1sioT .marker.cross{stroke:#333333;}#mermaid-svg-WeVjkt3UWoN1sioT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WeVjkt3UWoN1sioT p{margin:0;}#mermaid-svg-WeVjkt3UWoN1sioT .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-WeVjkt3UWoN1sioT text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-WeVjkt3UWoN1sioT .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-WeVjkt3UWoN1sioT .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-WeVjkt3UWoN1sioT .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-WeVjkt3UWoN1sioT .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-WeVjkt3UWoN1sioT #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-WeVjkt3UWoN1sioT .sequenceNumber{fill:white;}#mermaid-svg-WeVjkt3UWoN1sioT #sequencenumber{fill:#333;}#mermaid-svg-WeVjkt3UWoN1sioT #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-WeVjkt3UWoN1sioT .messageText{fill:#333;stroke:none;}#mermaid-svg-WeVjkt3UWoN1sioT .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-WeVjkt3UWoN1sioT .labelText,#mermaid-svg-WeVjkt3UWoN1sioT .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-WeVjkt3UWoN1sioT .loopText,#mermaid-svg-WeVjkt3UWoN1sioT .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-WeVjkt3UWoN1sioT .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-WeVjkt3UWoN1sioT .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-WeVjkt3UWoN1sioT .noteText,#mermaid-svg-WeVjkt3UWoN1sioT .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-WeVjkt3UWoN1sioT .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-WeVjkt3UWoN1sioT .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-WeVjkt3UWoN1sioT .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-WeVjkt3UWoN1sioT .actorPopupMenu{position:absolute;}#mermaid-svg-WeVjkt3UWoN1sioT .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-WeVjkt3UWoN1sioT .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-WeVjkt3UWoN1sioT .actor-man circle,#mermaid-svg-WeVjkt3UWoN1sioT line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-WeVjkt3UWoN1sioT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 请求临时凭证 nonce/timestamp/signature校验签名和权限申请临时写入凭证返回临时凭证accessKeyId / accessKeySecret / securityToken使用临时凭证批量写入日志
这个设计的价值是:前端只拿短期有效的临时凭证,即使泄露,风险窗口也比长期密钥小得多。
9. SLS 是什么
阿里云 SLS,全称 Simple Log Service,也叫日志服务。可以把它理解成一个云上的日志采集、存储、查询、分析和告警平台。
对前端埋点来说,SLS 主要承担这些职责:
- 接收浏览器上报的事件日志。
- 按 Project 和 Logstore 存储日志。
- 为日志字段建立索引,支持快速查询。
- 提供 SQL 分析能力,支持聚合、分组、排序、时间窗口统计。
- 支持仪表盘,把查询结果做成图表。
- 支持告警,对错误量、卡顿量、接口异常量等指标设置阈值通知。
- 支持数据投递,把日志同步到 OSS、MaxCompute、实时计算等系统。
如果类比数据库:
text
SLS Project ~= 一个日志项目空间
Logstore ~= 一张日志表或日志库
Log ~= 一行事件记录
Field ~= 日志字段,如 event_id、company_id、page_url
Index ~= 查询索引配置
SQL ~= 分析语句
Dashboard ~= 可视化报表
Alert ~= 告警规则
10. SLS 对埋点的核心功能
10.1 日志采集
SLS 支持多种采集方式,前端常用的是 Web Tracking。它允许浏览器 SDK 直接把日志写入指定 Logstore。
在当前工程里,真正发送日志的是:
ts
slsTracker.send(logData);
SDK 会根据 time 和 count 做批量发送,减少网络请求数量。
10.2 日志查询
SLS 支持按字段查询,例如查某个埋点 ID:
sql
event_id: "applicationLegwork_viewFieldTrajectory-2509191119"
也可以查某个企业:
sql
company_id: "xxx"
或者查错误事件:
sql
event_id: "JsExecuteError" OR event_id: "fetchResourceError"
10.3 SQL 分析
SLS 的查询语法通常分为两段:
text
查询条件 | SQL 分析语句
例如统计最近一段时间点击量最高的事件:
sql
event_type: "click"
| SELECT event_id, event_name, count(*) AS cnt
GROUP BY event_id, event_name
ORDER BY cnt DESC
LIMIT 20
统计页面访问量:
sql
event_type: "expose"
| SELECT page, router, count(*) AS pv
GROUP BY page, router
ORDER BY pv DESC
LIMIT 50
统计 JS 报错:
sql
event_id: "JsExecuteError"
| SELECT json_extract_scalar(properties, '$.message') AS message,
count(*) AS cnt
GROUP BY message
ORDER BY cnt DESC
LIMIT 20
统计资源加载失败:
sql
event_id: "fetchResourceError"
| SELECT json_extract_scalar(properties, '$.name') AS resource,
json_extract_scalar(properties, '$.responseStatus') AS status,
count(*) AS cnt
GROUP BY resource, status
ORDER BY cnt DESC
LIMIT 50
10.4 仪表盘
SLS 仪表盘可以把 SQL 查询结果配置成图表,比如:
- 今日总 PV / UV。
- 点击量 Top 20 功能。
- JS 报错趋势。
- 资源加载失败趋势。
- 外勤功能使用趋势。
- 不同 source 的访问量对比。
- 单企业或单用户行为链路排查面板。
10.5 告警
SLS 告警可以周期性执行查询,当结果超过阈值时通知相关人员。
常见告警规则:
- 5 分钟内
JsExecuteError超过阈值。 - 某个资源 404/500 数量异常增加。
- 某个关键页面 PV 突然降为 0。
- 某个功能点击量突然异常暴涨。
- FPS 卡顿事件数量超过阈值。
10.6 数据投递
如果埋点数据后续要做 BI、离线分析、数据仓库建模,可以从 SLS 投递到:
- OSS:低成本归档。
- MaxCompute:离线数仓分析。
- Kafka 或实时计算:实时消费和二次处理。
- Elasticsearch/OpenSearch:复杂检索场景。
11. 当前工程的埋点类型
11.1 标准点击埋点
通过 pointFun 上报:
ts
pointFun({
pointId: 'crmSales_export-2509191074',
pointType: 'click',
});
适合按钮点击、确认操作、入口点击等。
11.2 页面访问和停留时长
通过 routerPoint 或 reportRouteChange 上报:
ts
routerPoint({ pathname: 'legwork/detail' });
适合页面曝光、详情页打开、页面停留分析。
11.3 JS 错误上报
通过 window.addEventListener('error') 捕获,再调用 reportViewEvent:
ts
reportViewEvent('JsExecuteError', {
name: filename,
message,
line: `${lineno},${colno}`,
desc: 'JS执行报错',
type: 'JsExecuteError',
});
11.4 资源失败上报
通过 PerformanceObserver 捕获资源加载情况:
ts
reportViewEvent('fetchResourceError', {
name,
responseStatus,
desc: isCors ? '请求被取消/跨域拦截' : '资源加载失败',
type: 'fetchResourceError',
});
11.5 性能监控
@core/point 支持性能监控能力,包括 FPS 卡顿、长任务等。当前 crm-common/src/app.ts 中配置是注释状态:
ts
// performanceMonitor: {
// enableFPSMonitor: true,
// fpsThreshold: 30,
// environment: 'production',
// minStutterDuration: 3000,
// },
如果开启,SDK 会把性能事件也作为日志写入 SLS。
12. 如何新增一个业务埋点
推荐工作流:
#mermaid-svg-cjh5u7l1wD3ZNwSV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-cjh5u7l1wD3ZNwSV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cjh5u7l1wD3ZNwSV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cjh5u7l1wD3ZNwSV .error-icon{fill:#552222;}#mermaid-svg-cjh5u7l1wD3ZNwSV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cjh5u7l1wD3ZNwSV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cjh5u7l1wD3ZNwSV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cjh5u7l1wD3ZNwSV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cjh5u7l1wD3ZNwSV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cjh5u7l1wD3ZNwSV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cjh5u7l1wD3ZNwSV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cjh5u7l1wD3ZNwSV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cjh5u7l1wD3ZNwSV .marker.cross{stroke:#333333;}#mermaid-svg-cjh5u7l1wD3ZNwSV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cjh5u7l1wD3ZNwSV p{margin:0;}#mermaid-svg-cjh5u7l1wD3ZNwSV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-cjh5u7l1wD3ZNwSV .cluster-label text{fill:#333;}#mermaid-svg-cjh5u7l1wD3ZNwSV .cluster-label span{color:#333;}#mermaid-svg-cjh5u7l1wD3ZNwSV .cluster-label span p{background-color:transparent;}#mermaid-svg-cjh5u7l1wD3ZNwSV .label text,#mermaid-svg-cjh5u7l1wD3ZNwSV span{fill:#333;color:#333;}#mermaid-svg-cjh5u7l1wD3ZNwSV .node rect,#mermaid-svg-cjh5u7l1wD3ZNwSV .node circle,#mermaid-svg-cjh5u7l1wD3ZNwSV .node ellipse,#mermaid-svg-cjh5u7l1wD3ZNwSV .node polygon,#mermaid-svg-cjh5u7l1wD3ZNwSV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cjh5u7l1wD3ZNwSV .rough-node .label text,#mermaid-svg-cjh5u7l1wD3ZNwSV .node .label text,#mermaid-svg-cjh5u7l1wD3ZNwSV .image-shape .label,#mermaid-svg-cjh5u7l1wD3ZNwSV .icon-shape .label{text-anchor:middle;}#mermaid-svg-cjh5u7l1wD3ZNwSV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-cjh5u7l1wD3ZNwSV .rough-node .label,#mermaid-svg-cjh5u7l1wD3ZNwSV .node .label,#mermaid-svg-cjh5u7l1wD3ZNwSV .image-shape .label,#mermaid-svg-cjh5u7l1wD3ZNwSV .icon-shape .label{text-align:center;}#mermaid-svg-cjh5u7l1wD3ZNwSV .node.clickable{cursor:pointer;}#mermaid-svg-cjh5u7l1wD3ZNwSV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-cjh5u7l1wD3ZNwSV .arrowheadPath{fill:#333333;}#mermaid-svg-cjh5u7l1wD3ZNwSV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cjh5u7l1wD3ZNwSV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cjh5u7l1wD3ZNwSV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cjh5u7l1wD3ZNwSV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-cjh5u7l1wD3ZNwSV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cjh5u7l1wD3ZNwSV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-cjh5u7l1wD3ZNwSV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cjh5u7l1wD3ZNwSV .cluster text{fill:#333;}#mermaid-svg-cjh5u7l1wD3ZNwSV .cluster span{color:#333;}#mermaid-svg-cjh5u7l1wD3ZNwSV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-cjh5u7l1wD3ZNwSV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-cjh5u7l1wD3ZNwSV rect.text{fill:none;stroke-width:0;}#mermaid-svg-cjh5u7l1wD3ZNwSV .icon-shape,#mermaid-svg-cjh5u7l1wD3ZNwSV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cjh5u7l1wD3ZNwSV .icon-shape p,#mermaid-svg-cjh5u7l1wD3ZNwSV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-cjh5u7l1wD3ZNwSV .icon-shape .label rect,#mermaid-svg-cjh5u7l1wD3ZNwSV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cjh5u7l1wD3ZNwSV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-cjh5u7l1wD3ZNwSV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-cjh5u7l1wD3ZNwSV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 确认埋点需求
在 click.xlsx 或 page.xlsx 新增事件
填写 event_id / module / page / event_type / event_name / trigger
执行 npm run gen-point-map-data
检查 src/map-data/click.ts 或 page.ts 是否生成
业务代码调用 pointFun 或 routerPoint
本地或测试环境触发操作
Network / SLS 查询验证数据
示例:
ts
import { pointFun } from '@core/point-map-tq';
function handleExport() {
pointFun({
pointId: 'crmSales_export-2509191074',
pointType: 'click',
pointData: {
export_type: 'customer_list',
},
});
// 原业务逻辑
...
}
这里 pointData 会进入 SLS 的 properties 字段,适合放本次操作的动态属性,比如导出类型、筛选状态、来源入口等。
13. 排查埋点不上报的路径
13.1 先确认是否初始化
检查 src/app.ts 是否执行了:
ts
initPoint({ project, pageMap, clickMap, SLSTracker })
如果是异常监控,还要确认生产环境是否执行了:
ts
initReport()
13.2 检查 map 是否有这个事件
在 src/map-data/click.ts 或 src/map-data/page.ts 搜索 pointId。
如果没有,说明 Excel 更新后没有执行:
bash
npm run gen-point-map-data
13.3 检查调用方式
点击事件:
ts
pointFun({ pointId: 'xxx', pointType: 'click' })
页面事件:
ts
routerPoint({ pathname: 'xxx' })
自定义事件或错误事件:
ts
postEvent(data, EVENT_TYPE.VIEW)
13.4 检查 SLS 初始化和凭证
如果控制台出现类似 SLS 初始化失败、STS 凭证获取失败,需要重点看:
- 后端API接口 是否正常返回。
- 请求头里的签名和时间戳是否被后端接受。
- 当前环境是否有写入对应 SLS Project / Logstore 的权限。
- 浏览器是否被插件或网络策略拦截。
13.5 检查 source 和环境
当前工程中不同路径可能写入不同 source:
text
connect
xx-web-pc
xxx-web-pc
如果 SLS 查询不到,可能不是没上报,而是查询条件里 source、时间范围、event_type 过滤错了。
14. 常见风险和注意事项
- 不要在
properties里上报敏感信息,比如手机号、客户名称、企业名称、明文 Token。 event_id要稳定,不能随便改,否则历史数据会被切断。- Excel 和代码要同步提交,否则别人拉代码后 map 可能还是旧的。
- 页面埋点要防重复,尤其是重定向、默认菜单跳转、search 参数变化的场景。
- 前端埋点不能影响业务逻辑,SDK 发送失败通常应该吞掉错误或降级。
- SLS 查询必须注意时间范围,默认时间范围太短时很容易误判"没数据"。
- 如果开启性能监控,要控制上报阈值,避免卡顿事件过多导致成本和噪音增加。