hx-web-tracker
v0.2.3
Published
Lightweight web tracking SDK (SLS client) extracted from growth project
Maintainers
Readme
hx-web-tracker
轻量级前端埋点 SDK,基于阿里云 SLS(Simple Log Service)日志服务,提供自动化的用户行为追踪能力。
核心特性:
- 🎯 自动采集用户行为:点击、输入、页面浏览、HTTP 请求
- 📦 批量上报与离线缓存:支持本地持久化,网络恢复后自动重试
- 🔐 安全的数据脱敏:内置字段掩码策略,保护敏感信息
- 🎨 丰富的上下文增强:自动附加会话、用户、设备、视图等上下文信息
- 🔗 请求追踪与动作关联:支持 trace_id 和 action_id 串联前后端日志
- Session Replay 回放:可按采样录制用户会话,独立队列上报,不影响普通埋点链路
源码结构: 核心代码位于 core/(追踪引擎)、collectors/(行为采集器)、context/(上下文管理)三个目录。
安装
npm i hx-web-tracker快速集成示例
在你的项目里新增一个本地封装(例如 src/utils/logger.ts),负责初始化、注册采集器、提供 track/setView/refreshUser 等入口。下面是一份可直接拷贝的示例,包含必要的 JSDoc 说明,按注释替换占位符即可开箱使用。
import type { ViewInfo, UserInfo } from 'hx-web-tracker';
import {
init as trackerInit,
track as coreTrack,
setContextView,
refreshContextUser,
setTraceId,
startReplayRecording,
stopReplayRecording,
getReplayState,
registerCollector,
createClickCollector,
createInputCollector,
createRequestCollector,
createViewCollector,
createReplayCollector,
} from 'hx-web-tracker';
import router from '@/router';
import axios from '@/utils/request'; // 替换为你的 axios 实例
const ENV = import.meta.env;
const LOG_ENDPOINT = '<你的 SLS endpoint>'; // 例:cn-beijing.log.aliyuncs.com
const LOG_PROJECT = '<你的 project>';
const LOG_LOGSTORE = ENV.MODE === 'development' ? '<test_logstore>' : '<prod_logstore>';
const LOG_TOPIC = '<可选 topic>';
const REPLAY_ENABLED = ENV.VITE_TRACKER_REPLAY_ENABLED === 'true';
const REPLAY_SAMPLE_RATE = Number(ENV.VITE_TRACKER_REPLAY_SAMPLE_RATE ?? 0.01);
const REPLAY_START_MANUALLY = ENV.VITE_TRACKER_REPLAY_START_MANUALLY === 'true';
/**
* 获取临时凭证(请接入你们的鉴权服务,不要硬编码 AK/SK)。
*/
async function fetchCredential() {
return {
accessKeyId: '',
accessKeySecret: '',
securityToken: '',
expireAt: Date.now() + 3600_000,
};
}
/**
* 兼容 Vue Router 3(currentRoute 为对象)与 4(currentRoute 为 ref)。
* 将路由对象转换为视图信息,用于 view_start/view_end。
*/
function unwrapRoute(route: any) {
if (!route || typeof route !== 'object') return route;
return 'value' in route ? (route as any).value : route;
}
function toRouteInfo(route: any): ViewInfo {
const r = unwrapRoute(route);
if (!r || typeof r !== 'object') {
return {
id: typeof window !== 'undefined' ? window.location.pathname : 'unknown',
};
}
const fullPath = r.fullPath ?? r.path ?? 'unknown';
const metaTitle = r.meta?.title_name ?? r.meta?.title;
return {
id: String(fullPath),
name: metaTitle ? String(metaTitle) : undefined,
title: metaTitle ? String(metaTitle) : undefined,
path: r.path ? String(r.path) : undefined,
};
}
/**
* 追加自定义字段:页面 URL/标题与视口大小。
* page_title 优先取 document.title,page_url 优先用视图 id/path 构造。
*/
function createCustomFieldResolvers() {
return [
(context: { view?: ViewInfo }) => {
const hasWindow = typeof window !== 'undefined';
const hasDocument = typeof document !== 'undefined';
const viewportWidth = hasWindow && typeof window.innerWidth === 'number' ? window.innerWidth : -1;
const viewportHeight = hasWindow && typeof window.innerHeight === 'number' ? window.innerHeight : -1;
const view = context.view ?? {};
const viewTitle = (view.title ?? view.name) as string | undefined;
const docTitle = hasDocument && typeof document.title === 'string' ? document.title : '';
const pageTitle =
docTitle.trim().length > 0
? docTitle
: typeof viewTitle === 'string' && viewTitle.trim().length > 0
? viewTitle
: '';
const viewId = typeof view.id === 'string' ? view.id : '';
const viewPath = typeof view.path === 'string' ? view.path : '';
const isAbsoluteUrl = /^https?:\/\//i.test(viewId);
const pageUrl =
isAbsoluteUrl && viewId
? viewId
: hasWindow && viewPath
? `${window.location.origin}${viewPath}`
: hasWindow && viewId
? `${window.location.origin}${viewId}`
: hasWindow
? window.location.href
: viewId || viewPath;
return {
page_url: pageUrl,
page_title: pageTitle,
viewport_width: viewportWidth,
viewport_height: viewportHeight,
};
},
];
}
let initialized = false;
let initPromise: Promise<void> | null = null;
let manualViewSetter: ((view: ViewInfo) => void) | null = null;
function registerBuiltInCollectors() {
registerCollector('click', createClickCollector(), { priority: 80, override: true });
registerCollector('input', createInputCollector({ maskStrategy: 'none' }), { priority: 70, override: true });
registerCollector(
'request',
createRequestCollector({
instrumentFetch: true,
instrumentXHR: true,
axiosInstances: [axios as any],
ignoredUrls: [/\\.js$/i, /\\.css$/i, /\\.map$/i, /__vite_ping/],
}),
{ priority: 60, override: true },
);
registerCollector(
'view',
createViewCollector({
router,
getRouteInfo: toRouteInfo, // 兼容 v3/v4 的 currentRoute/afterEach 入参
registerManualSetter: (setter) => {
manualViewSetter = setter;
},
}),
{ priority: 90, override: true },
);
registerCollector(
'replay',
createReplayCollector({
stopOnPageHide: true,
}),
{ priority: 50, override: true },
);
}
/**
* 初始化埋点 SDK(幂等);可在应用入口直接调用,或在首次 track/send 前懒加载。
*/
export async function initTracker(): Promise<void> {
if (initialized) return;
if (initPromise) return initPromise;
registerBuiltInCollectors();
initPromise = trackerInit({
endpoint: LOG_ENDPOINT,
project: LOG_PROJECT,
logstore: LOG_LOGSTORE,
topic: LOG_TOPIC,
debug: !!ENV.DEV,
fetchCredential,
context: {
getUserInfo: async (): Promise<UserInfo> => ({ id: '', name: '', role: '' }), // TODO: 返回真实用户
resolveView: () => toRouteInfo(router.currentRoute),
customFieldResolvers: createCustomFieldResolvers(),
},
replay: {
enabled: REPLAY_ENABLED,
sampleRate: REPLAY_SAMPLE_RATE,
startManually: REPLAY_START_MANUALLY,
maxDurationMs: 10 * 60 * 1000,
maxReplayRawBytes: 8 * 1024 * 1024,
flushIntervalMs: 5000,
maxChunkBytes: 128 * 1024,
maxSingleEventBytes: 1024 * 1024,
maskAllInputs: true,
privacyLevel: 'strict',
uploadBatchSize: 5,
uploadFlushIntervalMs: 2000,
uploadMaxRetries: 5,
uploadStorageKey: 'tracker-replay-queue',
uploadMaxQueueBytes: 4 * 1024 * 1024,
uploadMaxPersistedBytes: 4 * 1024 * 1024,
},
})
.then(() => {
initialized = true;
})
.catch((error) => {
initPromise = null;
throw error;
});
return initPromise;
}
/**
* 统一导出的 tracker 对象,便于业务侧调用。
*/
export const tracker = {
/**
* 手动初始化,可选(track 时会自动懒加载)。
*/
init: initTracker,
/**
* 上报业务事件。
*/
track(eventType: string, payload?: Record<string, unknown>) {
return initTracker().then(() => {
coreTrack({ event_type: eventType, payload });
});
},
/**
* 更新当前视图(若需手动埋点页面切换)。
*/
setView(view: ViewInfo) {
return initTracker().then(() => {
if (manualViewSetter) {
manualViewSetter(view);
} else {
setContextView(view);
}
});
},
/**
* 刷新用户上下文(登录/切换用户后调用)。
*/
refreshUser(user?: Partial<UserInfo>) {
return initTracker().then(() => refreshContextUser(user as UserInfo));
},
/**
* 手动开始回放录制;仅在 replay.enabled=true 后生效。
*/
startReplay(options?: { force?: boolean; reason?: string }) {
return initTracker().then(() => startReplayRecording(options));
},
/**
* 手动停止回放录制,并触发最终分片收尾。
*/
stopReplay(reason = 'manual_stop') {
stopReplayRecording({ reason });
},
/**
* 获取当前回放状态,可用于调试或展示 replay_id。
*/
getReplayState,
/**
* 设置 traceId 以串联前后端日志。
*/
setTraceId,
};使用方式:
- 在应用入口(如
main.ts)调用void tracker.init(),或在首次tracker.track()前懒加载。 - 路由已自动注册视图埋点,如需手动控制页面视图可调用
tracker.setView({ id, title, path })。 - 登录/切换用户后调用
tracker.refreshUser(user)刷新上下文。 - 需要链路追踪时调用
tracker.setTraceId(traceId)。 - 需要开启回放时,将
VITE_TRACKER_REPLAY_ENABLED设为true,再按灰度比例设置VITE_TRACKER_REPLAY_SAMPLE_RATE。 - 若设置
VITE_TRACKER_REPLAY_START_MANUALLY=true,SDK 不会自动录制,需要业务在合适时机调用tracker.startReplay({ force: true, reason: 'debug' }),结束时调用tracker.stopReplay()。
Session Replay 接入指南
Replay 默认建议按“注册采集器、配置开关、小流量灰度、后端确认可查”四步接入:
- 注册
createReplayCollector(),并在trackerInit()中配置replay。 - 默认保持
replay.enabled=false,先在开发环境开启验证。 - 生产环境先用低采样率灰度,例如
sampleRate=0.01,确认普通埋点成功率、页面性能、replay 上传成功率无明显异常后再扩大。 - 后端确认能按
replay_id查回replay_session_start、replay_chunk、replay_session_end后,前端播放器即可重建回放。
常用配置建议
| 场景 | 配置建议 | 说明 |
|---|---|---|
| 默认关闭 | enabled: false | 不产生 replay 事件,不影响普通埋点 |
| 小流量灰度 | enabled: true, sampleRate: 0.01 | 自动录制命中采样的会话 |
| 指定用户排查 | enabled: true, startManually: true | 登录后按用户、路由或调试按钮调用 startReplay({ force: true }) |
| 隐私优先 | maskAllInputs: true, privacyLevel: 'strict' | 默认遮罩输入内容,避免敏感信息明文进入回放数据 |
| 控制单次录制 | maxDurationMs: 10 * 60 * 1000, maxReplayRawBytes: 8 * 1024 * 1024 | 到时或达到累计体积上限后自动停止并续录 |
| 控制分片大小 | maxChunkBytes: 128 * 1024, flushIntervalMs: 5000 | 按大小或时间切片,减少单条日志过大 |
| 观测超大事件 | maxSingleEventBytes: 1024 * 1024 | 单个 rrweb event 过大时上报诊断,不直接丢弃 |
回放数据如何使用
- 开始录制后可通过
tracker.getReplayState().replayId拿到当前replay_id。 - 前端不会上传 MP4,而是上传 rrweb 事件流分片。
- 后端只需要按
replay_id返回完整事件流,播放器按segment_id + chunk_seq排序、按idempotency_key去重、解码payload_data后即可播放。 - 后端配合要求见
docs/replay-backend-collaboration.md。
埋点字段说明
通用字段
每条日志都会包含以下通用字段:
| 字段 | 描述 | 示例值 | 备注 |
|------|------|--------|------|
| timestamp | 格式化时间字符串,格式为 YYYY-MM-DD HH:mm:ss | 2025-09-22 10:54:22 | 用于页面展示,24 小时制,无毫秒 |
| timestamp_ms | 事件产生时的 Unix 毫秒时间戳 | 1758509662127 | 供后端做时间排序/计算 |
| topic | 日志主题标签,SDK 初始化时配置 | web-tracker | 用于给上报数据分类 |
| event_type | 事件类型标识 | ui_click / http_request / view_start 等 | 详见下方事件类型说明 |
event_type 详细说明
| 事件类型 | 说明 | 触发场景 |
|---------|------|----------|
| session_start | 会话启动 | SDK 初始化时自动落一次,标记新会话启动 |
| view_start / view_end | 页面浏览 | 路由切换或手动调用 setView 时记录页面进入/离开及停留时长 |
| ui_click | 点击行为 | 内置采集器捕捉左键/右键/双击/长按等行为 |
| ui_input | 表单输入 | 输入框/文本域/下拉框失焦时,读取当前值(默认忽略 password/hidden/file),做截断或脱敏后上报 |
| http_request | 网络请求 | Axios/fetch/XHR 请求结束时落一条,包含耗时、状态码、错误信息等 |
| replay_session_start | 回放会话开始 | 启用 Replay 且命中采样或手动启动后,开始录制时上报 |
| replay_chunk | 回放分片 | 录制过程中按时间/大小切片上报 rrweb 事件流 |
| replay_session_end | 回放会话结束 | 停止录制、超时、页面隐藏/卸载等结束时上报 |
| replay_recording_error | 回放错误 | 回放初始化/录制/编码阶段出现异常时上报 |
| adjust_column_width | 表格列宽调整 | 通用表格列宽调整成功时手动上报,记录表名/字段/新宽度 |
| filter_change | 筛选条件变更 | 筛选项变动时手动上报,覆盖静态筛选面板和动态筛选面板 |
| download_table | 表格导出 | 表格导出操作时手动上报,附带导出表名与查询条件 |
| no_permission | 无权限访问 | 无权限页面被访问时手动上报,用于排查权限配置 |
| custom_event | 自定义事件 | tracker.send 若未传 event_name 会退回到此默认值(保留能力,目前未实际使用) |
自动附加的上下文字段
每个事件都会自动附加以下上下文信息:
会话上下文
| 字段名 | 描述 | 示例值 | 备注 |
|-------|------|--------|------|
| ctx_session_id | 当前浏览器会话 ID | "6501c4-6b6c" | 基于 sessionStorage,30 分钟无操作会刷新 |
| ctx_session_start | 本次会话开始的毫秒时间戳 | 1762805400000 | 与 ctx_elapsed_sec 配合判断会话持续时长 |
| ctx_window_id | 当前标签页 ID | "w-98f2..." | 同一账号多标签页同时操作时用于区分 |
| ctx_timestamp | 事件捕获时间(毫秒) | 1762809111574 | 若网络阻塞,可与 timestamp_ms 对照 |
| ctx_elapsed_sec | 会话开始到事件触发的秒数 | 3200 | 用于分析用户停留时长 |
| ctx_event_id | 埋点事件唯一 ID | "a1b2c3...-1762809111574-42" | 由会话 ID + 时间戳 + 计数器拼接 |
页面上下文
路由存在时自动携带的页面视图信息:
| 字段名 | 描述 | 示例值 | 备注 |
|-------|------|--------|------|
| ctx_view_id | 当前视图唯一标识(通常为路由 fullPath) | "/settings/profile?tab=basic" | 必带,包含查询参数 |
| ctx_view_name | 路由 meta.title 或手动设置的页面名称 | "用户设置" | 若未配置标题则为空 |
| ctx_view_path | 路由 path(不含查询参数) | "/settings/profile" | 便于做权限/菜单映射 |
用户上下文
| 字段名 | 描述 | 示例值 | 备注 |
|-------|------|--------|------|
| ctx_user_id | 当前登录用户 ID | "u_123" | 用户唯一标识 |
| ctx_user_name | 用户姓名 | "张三" | 空字符串表示尚未拿到用户信息 |
| ctx_user_role | 用户角色名称 | "admin" | 按业务侧返回 |
| ctx_user_department | 用户所属部门 | "研发部门" | 后端若无该字段则为空 |
设备上下文
| 字段名 | 描述 | 示例值 | 备注 |
|-------|------|--------|------|
| ctx_device_user_agent | 完整 UA 字符串 | "Mozilla/5.0 (...) Chrome/128.0.0.0 Safari/537.36" | 用于兼容性诊断。UA 是 "User-Agent",浏览器在请求网页时发送给服务器的一段标识字符串,里面包含浏览器类型、版本、操作系统、设备等信息 |
| ctx_device_browser_name | 浏览器名称 | "Chrome" | 基于 UA 解析 |
| ctx_device_browser_version | 浏览器版本 | "128.0.0.0" | 基于 UA 解析 |
| ctx_device_browser_engine | 浏览器渲染内核 | "Blink" | 区分 WebKit/Gecko 等 |
| ctx_device_os_name | 操作系统名称 | "macOS" | 移动端会返回 Android、iOS 等 |
| ctx_device_os_version | 操作系统版本 | "14.5.1" | 若 UA 无法解析则为 unknown |
| ctx_device_device_type | 终端类型 | "desktop" | 支持 mobile/tablet/tv |
| ctx_device_screen_width | 屏幕宽度(px) | 1440 | 取自 window.screen.width,无法获取时为 -1 |
| ctx_device_screen_height | 屏幕高度(px) | 900 | 取自 window.screen.height,无法获取时为 -1 |
| ctx_device_language | 浏览器首选语言 | "zh-CN" | 可用于语言偏好分析 |
追踪上下文
| 字段名 | 描述 | 示例值 | 备注 |
|-------|------|--------|------|
| trace_id | 全局追踪 ID | "f4b2c8a1-9d3e-4f12-b7a6-1e8c9d0f5a2b" | 关联前后端日志,由 setTraceId() 设置 |
| action_id | 当前用户操作的唯一编号 | "d3a9c1f4-7b2c-4c5e-9a10-2f3e8c5d6b7a" | 点击触发后,在行动存活期内的事件/请求都会带上,用于把"同一波操作"串起来 |
| action_type | 行动类型 | "click" / "form_submit" | 示例:click(由点击自动生成),业务手动 startAction('submit') 时会出现对应类型 |
| action_meta | 行动元信息(JSON 结构) | "{\"track_id\":\"search_btn\",\"track_name\":\"查询\",\"tag\":\"BUTTON\"}" | 便于知道是哪个控件触发了这次行动 |
扩展上下文
自定义字段解析器(customFieldResolvers)可以添加额外的上下文信息,字段前缀为 ctx_extra_*:
| 字段名 | 描述 | 示例值 | 备注 |
|-------|------|--------|------|
| ctx_extra_page_url | 事件触发时页面的完整 URL | "https://example.com/path?query=1#hash" | 包含协议、域名、路径、查询参数和 hash,方便后端还原用户的实际访问地址 |
| ctx_extra_page_title | 当前页面设置的浏览器显示标题 | "用户管理 - 后台系统" | 依赖浏览器 document.title |
| ctx_extra_viewport_width | 浏览器可视区域宽度 | 1440 | 记录事件发生时的视口宽度(像素) |
| ctx_extra_viewport_height | 浏览器可视区域高度 | 900 | 记录事件发生时的视口高度(像素) |
自动采集事件详情
SDK 会自动采集以下用户行为事件,无需手动调用:
会话事件
| 事件类型 | 触发时机 | 主要字段 |
|---------|---------|---------|
| session_start | SDK 初始化完成 | 会话开始标记 |
页面浏览事件
| 事件类型 | 触发时机 | 主要字段 |
|---------|---------|---------|
| view_start | 进入新页面/路由 | view_id, view_name, view_path, render_duration_ms(渲染耗时) |
| view_end | 离开当前页面 | view_id, duration_ms(停留时长) |
页面浏览事件字段说明:
| 字段名 | 描述 | 示例值 | 备注 |
|-------|------|--------|------|
| view_id | 页面唯一标识 | "/home" | 通常为路由 fullPath;includeQuery=false 时会去掉 query |
| view_name | 页面名称 | "首页" | 优先取路由 meta title 或自定义 view 信息 |
| view_path | 页面路径 | "/home" | 路由 path |
| render_duration_ms | 页面渲染耗时(毫秒) | 128.4 | 主要出现在 view_start。首次加载优先取 FCP;不可用时降级到 DOMContentLoaded/load;SPA 路由切换取进入路由到下一帧渲染的耗时 |
| duration_ms | 页面停留时长(毫秒) | 15320 | 仅 view_end 有意义,表示从进入页面到离开页面的停留时间 |
用户交互事件
| 事件类型 | 触发时机 | 主要字段 |
|---------|---------|---------|
| ui_click | 点击/长按/右键/双击 | click_type, track_id, track_name, element_tag, page_x, page_y |
| ui_input | 输入框失焦 | field_name, field_type, value_masked, value_length, form_name |
ui_click 事件详细字段说明:
| 字段名 | 描述 | 示例值 | 备注 |
|-------|------|--------|------|
| click_type | 点击类型 | "left" / "right" / "double" / "longpress" | 区分不同的点击行为 |
| element_tag | 触发元素的标签名 | "BUTTON" / "DIV" / "A" | 大写形式 |
| element_id | 元素的 id 属性 | "submitBtn" / "" | 无 id 时为空字符串 |
| element_classes | 元素 class 列表 | "el-button primary" | 以空格拼接的完整 class 字符串 |
| page_x | 点击点相对页面左上角的 X 坐标 | 842 | 单位为像素 |
| page_y | 点击点相对页面顶端的 Y 坐标 | 412 | 单位为像素 |
| text_mask | 目标元素文本内容的脱敏结果 | "保存修改" / "操作按钮..." | 最多 120 字符,超出截断并追加 ... |
| track_id | 元素上 data-track-id 的值 | "btn-save" / "" | 用于区分被点击元素,业务侧可自定义设置 |
| track_name | 元素展示文案或 data-track-name | "保存修改" | 供业务阅读,优先取 data-track-name,否则取元素文本 |
ui_input 事件详细字段说明:
| 字段名 | 描述 | 示例值 | 备注 |
|-------|------|--------|------|
| field_name | 失焦输入控件的名称/定位信息 | "account_name" / "email" / "search_keyword" | 优先取 data-track-field,其次 name/id/placeholder |
| field_type | 表单控件类型 | "text" / "select" / "textarea" / "checkbox" | 区分不同类型的输入控件 |
| form_name | 输入控件所在表单的 name 或 id | "login_form" / "user_profile_form" / "" | 用于关联同一表单内的多个字段,无表单时为空 |
| value_masked | 当前值的脱敏结果 | "***" / "[length:8]" / "option1,option2" | 根据 mask 策略脱敏。多选值用逗号连接 |
| previous_masked | 上次上报时脱敏后的旧值 | "***" / "[length:5]" / "" | 用于对比值是否发生变化,首次上报时为空 |
| value_length | 当前值长度 | 8 / 15 / 2 | 字符串长度或数组项数,帮助判断输入内容的规模 |
网络请求事件
| 事件类型 | 触发时机 | 主要字段 |
|---------|---------|---------|
| http_request | 发起 HTTP 请求(fetch/XHR/axios) | method, url, status, duration_ms, success, response_size |
http_request 事件详细字段说明:
| 字段名 | 描述 | 示例值 | 备注 |
|-------|------|--------|------|
| request_id | 每次请求埋点的唯一标识 | "e7f3b9dc-2a16-4d1d-9f2d-5de8a2e1c42b" | 用于串联同一条日志,类型为 string |
| method | HTTP 请求方法,表示这条 http_request 埋点对应的接口调用类型 | "GET" / "POST" / "PUT" / "DELETE" | 值来源于实际请求,已自动转成大写;常见包括 GET、POST、PUT、DELETE、PATCH、OPTIONS,若业务里发起其他方法(如 HEAD、TRACE)也会按原样上报 |
| url | 请求的完整地址 | "https://api.example.com/report?date=2025-09-10" | 包含协议、域名、路径和查询参数 |
| status | HTTP 状态码 | 200 / 401 / 500 | 请求返回的 HTTP 状态码,请求失败时为 0 |
| duration_ms | 请求耗时(毫秒) | 352 | 自发出请求到收到响应的总耗时,等价于 Network 面板里显示的 Time |
| success | 请求是否成功 | true / false | 请求是否视为成功(状态码在 200–399 且未抛错),类型为 boolean |
| request_body_digest | 对本次请求 body 的简化摘要 | "{\"name\":\"test\",\"status\"..." | 默认取原始内容或其 JSON 字符串,并在 200 个字符后截断,超出部分以 ... 结尾。帮助排查接口调用时传了哪些核心参数,同时通过截断/掩码降低敏感数据泄露风险 |
| response_digest | 响应体的摘要 | "{\"result\":\"ok\",\"list\":[...]}" / "" / "[unserializable]" | 默认截取前 200 个字符,超长追加 ...,类型为 string 或 null |
| response_size | 响应体大小估算 | 1250 / undefined | 单位是字节(字符数)。优先级:encoded > decoded > transfer > content-length > 估算。类型为 number 或 undefined(无法计算时) |
| response_size_transfer | 传输总大小(含头,压缩后) | 1856 | 单位是字节。来源 Performance API 的 transferSize。跨域无 TAO 或缓存命中时可能为 0/undefined |
| response_size_encoded | 压缩后的响应体大小 | 1720 | 单位是字节。Performance 的 encodedBodySize。压缩请求时与 decoded 不同;未压缩时它们相等 |
| response_size_decoded | 解压后的响应体大小 | 4096 | 单位是字节。Performance 的 decodedBodySize。可视为原始体积;缓存命中仍可能有值 |
| response_size_content_length | 响应头中的 Content-Length 值 | 1720 | 单位是字节。chunked/流式或未返回该头时为空;通常代表压缩后体积 |
| response_size_source | 大小取值来源 | "performance" / "header" | 可取 "performance" 或 "header";缺失表示无法确定 |
| response_compressed | 是否启用压缩 | true / false | 根据 Content-Encoding 判断。无法读头时为空 |
| response_content_encoding | Content-Encoding 值 | "gzip" / "br" / "deflate" | 无法读头或未压缩则为空 |
| response_cache_hit | 是否疑似缓存命中 | true / false | 启发式标记(transferSize=0 且有 body size),非严格判定 |
| error_message | 请求失败时的错误信息 | "Network Error" / "timeout of 5000ms exceeded" | 仅在请求失败时出现。请求失败(网络错误、状态码非 2xx/3xx、Axios reject 等)时,会尝试提取 error.message、响应体中的报错内容等写入,帮助后端排查接口异常。请求成功时该字段为空或不存在 |
| trace_id | 全局追踪 ID | "f4b2c8a1-9d3e-4f12-b7a6-1e8c9d0f5a2b" | 通过请求头 x-trace-id 发送,与 payload 中的 trace_id 对应,便于后端链路关联 |
| action_id | 当前操作 ID | "d3a9c1f4-7b2c-4c5e-9a10-2f3e8c5d6b7a" | 通过请求头 x-action-id 发送,与 payload 中的 action_id 对应。同一行动内的请求头值一致 |
回放事件(Session Replay,可选)
只有在注册 createReplayCollector 且配置 replay.enabled=true 后,才会产生以下事件。
| 事件类型 | 触发时机 | 主要字段 |
|---------|---------|---------|
| replay_session_start | 开始录制时 | replay_id, segment_id, recording_mode |
| replay_chunk | 录制中按时间/大小切片时 | replay_id, segment_id, chunk_seq, chunk_start_ms, chunk_end_ms, payload_data, is_final_chunk |
| replay_session_end | 停止录制/超时/页面隐藏或离开时 | replay_id, segment_id, total_chunks, duration_ms, end_reason |
| replay_recording_error | 回放初始化、录制、编码等异常时 | replay_id, stage, error_code, error_message, fatal |
| replay_stats | 回放性能周期上报 / 配置变更 / 降级变更时 | replay_id, stats_reason + 窗口字段(window_*)+ 计数字段(record_* / chunk_* / playable_*)+ 比率字段(*_rate)+ 性能字段(avg_chunk_size, replay_overhead_*)及队列/降级现有字段 |
| replay_degrade_change | 回放降级级别变化时 | replay_id, previous_level, next_level, reason |
replay_session_start 事件详细字段说明:
| 字段名 | 描述 | 示例值 | 备注 |
|-------|------|--------|------|
| replay_id | 回放会话 ID | "c34c8f54-5f0a-4bc8-8a8a-5c08d6930f11" | 同一回放会话内保持不变 |
| segment_id | 回放段 ID | "seg-1" | 当前实现固定为 seg-1 |
| recording_mode | 启动模式 | "sampled" / "forced" | 采样命中为 sampled,强制启动为 forced |
| idempotency_key | 回放上传幂等键 | "c34c8f54-...:seg-1:replay_session_start" | 由 SDK replay 上传层补充,用于服务端去重 |
replay_chunk 事件详细字段说明:
| 字段名 | 描述 | 示例值 | 备注 |
|-------|------|--------|------|
| replay_id | 回放会话 ID | "c34c8f54-5f0a-4bc8-8a8a-5c08d6930f11" | 用于关联同一会话 |
| segment_id | 回放段 ID | "seg-1" | 当前实现固定为 seg-1 |
| chunk_id | 分片 ID | "seg-1-3" | 由 segment_id 与 chunk_seq 拼接 |
| chunk_seq | 分片序号 | 3 | 段内递增,1 开始 |
| chunk_start_ms | 分片开始时间戳 | 1758509662127 | 毫秒时间戳 |
| chunk_end_ms | 分片结束时间戳 | 1758509667358 | 毫秒时间戳 |
| payload_data | 分片数据内容 | "[{...rrweb event...}]" | rrweb 事件数组的 JSON 字符串 |
| is_final_chunk | 是否最后一个分片 | false / true | 会话结束前最后一次 flush 为 true |
| event_count | 分片内事件数 | 42 | 当前分片包含的 rrweb 事件数量 |
| flush_reason | 切片触发原因 | "interval" / "size" / "buffer_limit" / "pagehide" / "beforeunload" / "timeout" / "manual" | 根据触发来源写入 |
| payload_bytes_raw | 原始 payload 字节数(近似) | 48321 | 基于字符串长度估算 |
| payload_bytes_encoded | 编码后字节数 | 48321 | 当前编码为 json,与 raw 相同 |
| payload_schema | 数据协议版本 | "rrweb-v1" | 当前固定值 |
| encoding | 编码方式 | "json" | 当前固定值 |
| encode_ms | 当前 chunk 编码耗时(毫秒) | 12 | 用于 replay 性能治理和降级判断 |
| idempotency_key | 回放上传幂等键 | "c34c8f54-...:seg-1:3" | 对 replay_chunk 通常使用 replay_id:segment_id:chunk_seq |
replay_session_end 事件详细字段说明:
| 字段名 | 描述 | 示例值 | 备注 |
|-------|------|--------|------|
| replay_id | 回放会话 ID | "c34c8f54-5f0a-4bc8-8a8a-5c08d6930f11" | 与 start/chunk 对齐 |
| segment_id | 回放段 ID | "seg-1" | 当前实现固定为 seg-1 |
| total_chunks | 总分片数 | 6 | 本会话实际成功上报的 chunk 数量 |
| duration_ms | 回放持续时长 | 31240 | 录制开始到结束的毫秒差 |
| end_reason | 结束原因 | "manual_stop" / "timeout" / "size_limit" / "normal" / "error" | size_limit 表示达到 maxReplayRawBytes 后自动切段 |
| idempotency_key | 回放上传幂等键 | "c34c8f54-...:seg-1:replay_session_end" | 由 SDK replay 上传层补充,用于服务端去重 |
replay_recording_error 事件详细字段说明:
| 字段名 | 描述 | 示例值 | 备注 |
|-------|------|--------|------|
| replay_id | 回放会话 ID | "c34c8f54-5f0a-4bc8-8a8a-5c08d6930f11" / undefined | 初始化失败时可能为空 |
| stage | 错误阶段 | "init" / "record" / "encode" / "persist" / "upload" | 当前实现主要使用 init/record/encode |
| error_code | 错误码 | "REPLAY_INIT_RECORDER_START_FAILED" | 便于聚合分析 |
| error_message | 错误信息 | "window/document unavailable" | 会截断(最大约 500 字符) |
| fatal | 是否致命 | true / false | 致命错误通常会中断当前录制 |
| dropped_count | 丢弃条数 | 12 | 背压丢弃时出现(通常 stage=persist) |
| dropped_bytes | 丢弃字节估算 | 32768 | 背压丢弃时出现(近似值) |
| drop_reason | 丢弃原因 | "count" / "bytes" / "count_and_bytes" / "forced_drop_inflight" | 当发生过租约 inflight 强制丢弃时为 forced_drop_inflight |
| drop_overflow_reason | 队列溢出原因 | "count" / "bytes" / "count_and_bytes" | 用于保留本次背压触发维度 |
| drop_candidate_source_primary | 本次丢弃主来源 | "pending_oldest" / "expired_inflight" / "oldest_fallback" | 按优先级选择的首个候选来源 |
| dropped_pending_count | 丢弃的 pending 条数 | 10 | 本次背压循环内统计 |
| dropped_expired_inflight_count | 丢弃的过租约 inflight 条数 | 2 | 本次背压循环内统计 |
| dropped_fallback_oldest_count | 兜底最旧丢弃条数 | 0 | 本次背压循环内统计 |
| drop_scope | 丢弃触发位置 | "enqueue" / "persist" | 背压检查触发点 |
| drop_prefer | 丢弃优先级策略 | "pending_first" | 当前实现固定为该值 |
| upload_max_queue_size | 队列条数上限 | 2000 | 便于定位触发阈值 |
| upload_max_queue_bytes | 队列字节上限 | 4194304 | 便于定位触发阈值 |
| upload_max_persisted_bytes | 持久化字节上限 | 4194304 | 便于定位触发阈值 |
| idempotency_key | 回放上传幂等键 | "c34c8f54-...:seg-1:replay_recording_error:re-..." | 由 SDK replay 上传层补充;错误事件默认带唯一后缀避免误去重 |
replay_stats 事件关键字段(趋势分析):
| 字段名 | 描述 |
|-------|------|
| replay_id | 当前回放会话 ID(若存在) |
| stats_reason | 上报原因(如 init / interval / session_end / config_update / degrade_change) |
| window_start_ms / window_end_ms / window_duration_ms | 统计窗口起止与窗口时长(毫秒) |
| record_start_count | 窗口内 replay_session_start 数量 |
| record_success_count | 窗口内“至少产出 1 个 chunk”的会话数量(按 replay_id) |
| chunk_produced_count | 窗口内产出的 chunk 数 |
| chunk_upload_attempt_count / chunk_upload_success_count | 窗口内 chunk 上传尝试数 / 成功数 |
| chunk_drop_count | 窗口内被丢弃的 chunk 数(背压丢弃 + 重试耗尽) |
| playable_session_count | 窗口内满足 start + end + final_chunk 的可回放会话数 |
| chunk_upload_success_rate | chunk_upload_success_count / chunk_upload_attempt_count |
| chunk_drop_rate | chunk_drop_count / chunk_produced_count |
| replay_playable_rate | playable_session_count / record_start_count |
| chunk_encoded_bytes_sum / avg_chunk_size | 窗口内 chunk 编码字节总量 / 平均 chunk 体积 |
| encode_time_sum_ms / flush_time_sum_ms | 窗口内编码总耗时 / flush 总耗时 |
| replay_overhead_cpu | (encode_time_sum_ms + flush_time_sum_ms) / window_duration_ms |
| replay_overhead_memory | usedJSHeapSize_end - usedJSHeapSize_start(浏览器不支持时为空) |
| replay_queue_bytes | 当前 replay 队列字节估算 |
| replay_persisted_bytes | 最近一次持久化字节估算 |
| replay_retry_backlog | 当前重试积压条数 |
| replay_dropped_count | 当前运行周期累计丢弃条数 |
| replay_last_encode_ms / replay_max_encode_ms | 编码耗时指标 |
| replay_last_flush_ms / replay_max_flush_ms | flush 耗时指标 |
| replay_longtask_count / replay_longtask_window_count | long task 指标 |
| replay_degrade_level / replay_degrade_reason | 当前降级级别与原因 |
replay_degrade_change 事件关键字段:
| 字段名 | 描述 |
|-------|------|
| replay_id | 当前回放会话 ID(若存在) |
| previous_level | 变更前降级级别 |
| next_level | 变更后降级级别 |
| reason | 触发变更原因 |
调试播放器可观测事件(Replay Debug Viewer,可选)
以下事件由 tools/replay-debug-viewer 在 onTrack 回调里产出,便于联调阶段做看板聚合;它们不属于内置 collector 自动采集事件。
replay_player_load:
| 字段名 | 描述 |
|---|---|
| replay_id | 当前加载的回放会话 ID |
| load_ms | 加载 + 组装 + 解码 + 渲染耗时(毫秒) |
| chunk_count | 组装后的 chunk 总数 |
| decoded_count | 成功解码的 chunk 数 |
| decode_fail_count | 解码失败 chunk 数 |
| playability | 可播放等级:playable / partial_playable / unplayable |
| playable | 兼容字段,与 playability 值相同 |
| from / to | 加载时使用的时间范围参数(可选) |
replay_player_error:
| 字段名 | 描述 |
|---|---|
| replay_id | 回放会话 ID(若可获取) |
| stage | 错误阶段:load / assemble / decode / render |
| error_code | 稳定错误码(用于聚合) |
| error_message | 错误描述 |
replay_player_error.error_code(当前实现)
| 错误码 | 含义 |
|---|---|
| REPLAY_FETCH_FAILED | 回放数据拉取失败(DataSource / API) |
| REPLAY_FETCH_LIMIT_EXCEEDED | 回放加载在 fetch 阶段触发硬上限(事件数或字节)并中止 |
| REPLAY_ASSEMBLE_FAILED | 回放事件组装失败 |
| REPLAY_DECODE_FAILED | 回放解码流程抛错 |
| REPLAY_PLAYER_RENDER_FAILED | Replayer 渲染失败 |
| REPLAY_PLAYER_LOAD_FAILED | load 阶段兜底错误码(非结构化错误) |
| REPLAY_PLAYER_ASSEMBLE_FAILED | assemble 阶段兜底错误码(非结构化错误) |
| REPLAY_PLAYER_DECODE_FAILED | decode 阶段兜底错误码(非结构化错误) |
| REPLAY_PLAYER_NO_EVENTS | 返回无事件,无法播放 |
| REPLAY_PLAYER_DECODE_EMPTY | chunk 存在但解码结果为空 |
| REPLAY_PLAYER_MISSING_START | 缺少 replay_session_start |
| REPLAY_PLAYER_MISSING_END | 缺少 replay_session_end |
| REPLAY_PLAYER_DECODE_PARTIAL | 解码不完整(部分可播放) |
| REPLAY_PLAYER_CHUNK_COUNT_LIMIT_EXCEEDED | 超过单次加载允许的最大 chunk 数,已中止解码 |
| REPLAY_PLAYER_TOTAL_BYTES_LIMIT_EXCEEDED | 超过单次加载允许的最大总字节,已中止后续解码 |
| REPLAY_PLAYER_DECODE_FAILURES_TRUNCATED | 解码失败过多,播放器错误事件只上报前 N 条 |
另外,当发生 chunk 级解码失败时,replay_player_error.error_code 可能直接使用 decode/* 系列编码(如 decode/chunk_too_large、decode/json_parse_failed),并附带 segment_id/chunk_seq/chunk_id/idempotency_key。
Replay 独立上传队列行为(当前实现)
- Replay 事件通过
trackReplay(event)写入独立队列,不进入普通state.queue。 - Replay 队列使用独立定时器与独立 flush,不占用普通埋点的 flush 锁。
- 页面
beforeunload/pagehide/visibilitychange=hidden时,Replay 队列走独立 lifecycle flush(优先sendBeacon);同一监听内普通队列也会独立执行自身 beacon 兜底,两条通道互不阻塞。 - 调用
flush()/forceFlush()时,会并行处理普通队列与 Replay 队列。 - 当
replay.enabled=false时:不会产生 replay 事件,trackReplay会直接跳过;初始化或运行时关闭 replay 时会清理 replay 队列。 - Replay 队列对
replay_chunk/replay_session_start/replay_session_end启用幂等键去重;重复幂等键不会重复入队。 - 现有普通埋点(如
ui_click/ui_input/http_request/view_*)字段契约保持不变,Replay 走独立队列,不会改写这些事件结构。 shutdown()会先尝试一次 replay quick beacon(reason=shutdown,quick=true),随后执行 replay 队列独立收尾:清理 replay timer、将剩余inflight回收为pending并持久化;下次init()可继续补传。- 如需彻底丢弃 replay 未发数据,可调用
shutdown({ dropReplayQueue: true })。
Replay 上传配置项(独立于录制分片配置)
以下配置位于 TrackerConfig.replay,默认值已内置,不配置也可运行:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| uploadBatchSize | number | 5 | 每次 replay flush 最多发送的事件数(按 replay 事件条数切片) |
| uploadFlushIntervalMs | number | 2000 | replay 队列自动 flush 间隔(毫秒) |
| uploadMaxRetries | number | 5 | 单条 replay 事件最大重试次数 |
| uploadRetryBaseDelayMs | number | 1500 | replay 重试基础退避时长(毫秒),指数退避 |
| uploadStorageKey | string | "tracker-replay-queue" | replay 队列本地持久化 key |
| uploadMaxQueueSize | number | 2000 | replay 队列最大长度,超出会丢弃最早数据 |
| uploadMaxQueueBytes | number | 4194304 | replay 队列估算字节上限(默认 4MB),超限触发背压丢弃 |
| uploadMaxPersistedBytes | number | 4194304 | replay 队列持久化字节上限(默认 4MB);落盘前按 min(uploadMaxQueueBytes, uploadMaxPersistedBytes) 执行背压裁剪 |
| uploadDropPolicy | string | "drop_oldest" | 背压丢弃策略;当前仅支持丢弃最旧数据 |
| uploadDropPrefer | string | "pending_first" | 丢弃优先级;当前顺序为“最旧 pending -> 过租约 inflight -> 全队列最旧兜底” |
| uploadLifecycleBeaconBatchSize | number | 3 | 生命周期 beacon flush 的最大条数阈值(会再受 uploadBatchSize 约束) |
| uploadLifecycleBeaconMaxBytes | number | 49152 | 生命周期 beacon flush 的最大字节阈值(默认 48KB,内部上限 60KB) |
| degradeEnabled | boolean | false | 是否启用 replay 自动降级状态机(L0-L3);仅启用后才会挂载 long task 性能观测 |
| degradeCooldownMs | number | 30000 | 降级恢复冷却窗口(毫秒) |
| maxMainThreadTaskMs | number | 60 | long task 计数阈值(毫秒) |
| maxEncodeMs | number | 50 | 单个 replay chunk 编码耗时预算(毫秒) |
| maxReplayRawBytes | number | 8388608 | 单个 replay 原始 rrweb payload 累计字节上限,超限以 end_reason=size_limit 结束并自动续录 |
| maxSingleEventBytes | number | 1048576 | 单个 rrweb event 体积观测阈值,超限上报 REPLAY_RECORD_OVERSIZED_EVENT,但不丢弃事件 |
| maxRecordBufferBytes | number | 524288 | replay collector 内存 buffer 上限,超限触发 flush_reason=buffer_limit |
| maxLongTaskPerMinute | number | 30 | 60 秒窗口内 long task 阈值,超限参与降级判定 |
| maxRetryBacklog | number | 200 | replay 重试积压阈值,超限参与降级判定 |
| statsIntervalMs | number | 60000 | replay_stats 趋势事件上报周期(毫秒) |
Replay 失败与重试语义(当前实现)
- 可重试状态码:
0、408、429、5xx;若无法提取状态码,默认按可重试处理。 - 不可重试或重试超限后,事件会被丢弃,并上报
replay_recording_error(stage=upload)。 - 重试策略为指数退避:
uploadRetryBaseDelayMs * 2^n(n为已重试次数)。 - 鉴权失败(
401/403,或错误信息包含signature/token/sts/unauthorized)会触发凭证刷新;刷新成功后 replay 队列立即重试。 - 队列背压(超过
uploadMaxQueueSize、uploadMaxQueueBytes,或持久化阶段超过uploadMaxPersistedBytes)会按uploadDropPolicy=drop_oldest执行丢弃;当前uploadDropPrefer=pending_first时按“最旧pending-> 过租约inflight-> 全队列最旧兜底”顺序选择候选,并上报replay_recording_error(stage=persist,error_code=REPLAY_PERSIST_QUEUE_OVERFLOW,drop_reason/dropped_count等扩展字段)。
Replay 状态可观测(getStateSnapshot())
用于调试和看板聚合的 Replay 相关快照字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| replayQueueLength | number | 当前 replay 队列长度(条数) |
| replayQueueBytes | number | 当前 replay 队列估算字节数 |
| replayPersistedBytes | number | undefined | 当前 replay 队列最近一次持久化字节估算(存储未初始化时为空) |
| replayIsFlushing | boolean | replay 队列是否正在 flush |
| replayNextRetryAt | number | undefined | 最近一次待重试时间戳(毫秒) |
| replayNextRetryInMs | number | undefined | 距离下一次重试的毫秒数 |
| replayDroppedCount | number | 当前运行周期内累计丢弃条数 |
| replayLastDropReason | string | undefined | 最近一次丢弃原因(当前值:count/bytes/count_and_bytes) |
| replayDegradeLevel | number | 当前 replay 降级级别(0~3) |
| replayDegradeReason | string | undefined | 最近一次降级级别变化原因 |
| replayDegradeUpdatedAt | number | undefined | 最近一次降级级别变化时间戳(毫秒) |
| replayConsecutiveFailures | number | replay 传输连续失败次数 |
| replayEncodeOverBudgetStreak | number | replay chunk 编码超预算连续次数 |
| replayRetryBacklog | number | replay 队列中已进入重试状态的积压条数 |
| replayLastEncodeMs | number | undefined | 最近一次 replay chunk 编码耗时(毫秒) |
| replayMaxEncodeMs | number | undefined | 当前运行周期内最大 replay chunk 编码耗时(毫秒) |
| replayLastFlushMs | number | undefined | 最近一次 replay flush 耗时(毫秒) |
| replayMaxFlushMs | number | undefined | 当前运行周期内最大 replay flush 耗时(毫秒) |
| replayLongTaskCount | number | 当前运行周期累计 long task 数(按阈值过滤) |
| replayLongTaskWindowCount | number | 最近 60 秒 long task 数 |
| replayStatsWindowStartMs / replayStatsWindowDurationMs | number | undefined | replay_stats 当前聚合窗口起点与窗口时长 |
| replayStatsRecordStartCount / replayStatsRecordSuccessCount | number | replay_stats 窗口内会话开始数 / 成功产出 chunk 的会话数 |
| replayStatsChunkProducedCount / replayStatsChunkDropCount | number | replay_stats 窗口内产出 chunk 数 / 丢弃 chunk 数 |
| replayStatsChunkUploadAttemptCount / replayStatsChunkUploadSuccessCount | number | replay_stats 窗口内 chunk 上传尝试数 / 成功数 |
| replayStatsChunkUploadSuccessRate / replayStatsChunkDropRate | number | replay_stats 窗口内 chunk 上传成功率 / 丢弃率 |
| replayStatsPlayableSessionCount / replayStatsPlayableRate | number | replay_stats 窗口内可回放会话数 / 可回放率 |
| replayStatsChunkEncodedBytesSum / replayStatsAvgChunkSize | number | replay_stats 窗口内 chunk 编码总字节 / 平均 chunk 体积 |
| replayStatsEncodeTimeSumMs / replayStatsFlushTimeSumMs | number | replay_stats 窗口内编码总耗时 / flush 总耗时 |
| replayStatsOverheadCpu / replayStatsOverheadMemory | number / number|undefined | replay_stats 计算出的 CPU 代理开销与内存增量 |
手动埋点
除了自动采集,你也可以手动上报自定义事件:
// 简单事件
tracker.track('button_click', {
button_name: 'signup',
button_text: '立即注册'
});
// 复杂业务事件
tracker.track('order_submit', {
order_id: 'ORD_123456',
amount: 99.99,
currency: 'CNY',
item_count: 3
});表格列宽/拖拽埋点示例(通用写法)
SDK 不强制表格埋点,以下为通用示例,字段请替换为你的业务含义:
VXE Table 列宽拖拽(vxe-table)
<!-- template -->
<vxe-table @column-resizable-change="handleResize" ...>
...
</vxe-table>import tracker from '@/utils/logger'; // 你的业务封装
const handleResize = ({ column }: { column: any }) => {
tracker.track('adjust_column_width', {
table: 'example_table', // 自定义表名
field: column?.property || column?.field || '',
field_label: column?.title || '',
width: column?.resizeWidth,
});
};VXE Table 行拖拽(Sortable)
import Sortable from 'sortablejs';
import tracker from '@/utils/logger';
const rowDrop = () => {
const $table = tableRef.value;
Sortable.create($table.$el.querySelector('.vxe-table--body-wrapper tbody'), {
handle: '.drag-btn',
onEnd({ oldIndex, newIndex }) {
tracker.track('table_row_reorder', {
table: 'example_table',
from_index: oldIndex,
to_index: newIndex,
row_id: rows[oldIndex]?.id,
});
},
});
};Element Plus el-table 列宽拖拽
<!-- template -->
<el-table :data="rows" border @header-dragend="handleHeaderDragend">
...
</el-table>import tracker from '@/utils/logger';
const handleHeaderDragend = (newWidth: number, oldWidth: number, column: any) => {
tracker.track('adjust_column_width', {
table: 'example_table',
field: column?.property || column?.prop || '',
field_label: column?.label || '',
width: newWidth,
old_width: oldWidth,
});
};以上仅为示例,请用你的表名/业务字段替换占位符。
完整事件示例
{
"event_type": "ui_click",
"timestamp": "2024-05-20 10:24:11",
"timestamp_ms": "1716181451000",
"click_type": "left",
"track_id": "ad-card-cta",
"track_name": "立即报名",
"element_tag": "BUTTON",
"element_id": "signup-btn",
"element_classes": "btn btn-primary",
"text_mask": "立即报名",
"page_x": 812,
"page_y": 364,
"ctx_session_id": "6501c4-6b6c",
"ctx_session_start": "1716178251000",
"ctx_window_id": "a7f3e9-2c1d",
"ctx_timestamp": "1716181451000",
"ctx_elapsed_sec": "3200",
"ctx_event_id": "6501c4-6b6c-1716181451000-42",
"ctx_view_id": "/campaign/list",
"ctx_view_name": "Campaign List",
"ctx_view_path": "/campaign/list",
"ctx_user_id": "u_123",
"ctx_user_name": "张三",
"ctx_user_role": "admin",
"ctx_device_browser_name": "Chrome",
"ctx_device_browser_version": "124.0.6367.118",
"ctx_device_os_name": "macOS",
"action_id": "f4b2c8a1-9d3e-4f12-b7a6-1e8c9d0f5a2b",
"action_type": "click"
}💡 提示: 完整的字段定义和数据契约请参考 operation-log-reference.mdx
关键配置说明
endpoint/project/logstore/fetchCredential:必填,由使用方提供,勿硬编码敏感值到 SDK 内。router:用于路由级视图埋点;非路由视图可调用setView。axiosInstances/instrumentFetch/instrumentXHR:启用请求埋点并传入实例。ignoredUrls:可过滤静态资源或不需埋点的接口。context.getUserInfo/refreshContextUser:提供当前用户信息,可加掩码处理。
发布提示
- 发布前检查
.npmignore/files确保只带源码与类型;无敏感配置。 - 可在 CI 中加入 git-secrets/敏感信息扫描。
- 根据需要补充构建脚本(rollup/tsc)与类型打包。
