CRM埋点原理与阿里云SLS说明

埋点原理与阿里云 SLS 说明

1. 这套埋点是什么

埋点可以理解为一条完整的数据链路:前端在用户点击、页面访问、资源加载失败、JS 报错等时机生成一条事件数据,然后通过公司封装的埋点 SDK 上报到阿里云 SLS 日志服务,后续再通过 SLS 控制台进行查询、统计、看板和告警。

当前工程主要涉及两层 SDK:

text 复制代码
@core/point-map-tq  业务埋点映射层
@core/point         底层上报与 SLS 对接层

@core/point-map-tq 更偏业务:它把 pointIdrouter 这类业务标识映射成标准埋点元数据,比如模块、页面、事件名称、事件类型。

@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.tspage.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,
  },
});

这段逻辑完成了三件事:

  • clickDatapageData 注入到 @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_idmodulepageevent_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 会根据 timecount 做批量发送,减少网络请求数量。

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 页面访问和停留时长

通过 routerPointreportRouteChange 上报:

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.tssrc/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 查询必须注意时间范围,默认时间范围太短时很容易误判"没数据"。
  • 如果开启性能监控,要控制上报阈值,避免卡顿事件过多导致成本和噪音增加。