FrontTrace SDK 技术文档
版本:v1.0.14(npm:
@autoxing/front-trace@1.0.14) 更新日期:2026-06-05
目录
一、架构概览
业务代码
│
├── HTTP 拦截(自动,三选一或叠加使用)
│ ├── initAxisInterceptor() → 拦截 Axis 全局对象
│ ├── initUniRequestInterceptor() → 拦截 uni.request(UniApp 专用)
│ └── initAxiosInterceptor() → 拦截 axios 实例或全局 axios
│ ├── 读取 currentCtx → 创建子 Span(HTTP 状态码在响应中精确记录)
│ └── 注入链路头 x-trace-id / x-span-id / traceparent / x-trace-sample
│ ↑ 仅注入到出站请求,不写入上报数据包
│
└── 手动 startSpan(可选)
├── 自动更新 currentCtx(HTTP 拦截器自动挂在此下)
├── 自动 finish(微任务,同步 Span 专用)
└── 手动 span.finish()(异步 Span 精确计时)
│
└── 自动恢复 currentCtx 为父级
│
▼
┌ ─ ─ ─ ─ ─ 采样判定 ─ ─ ─ ─ ─ ─ ─ ┐
│ _shouldSample(trace) │
│ ├─ SAMPLE_RATE 概率采样 │
│ ├─ SAMPLE_STATUS_CODES 状态码采样 │
│ ├─ SAMPLE_MIN_STATUS_CODE 阈值采样 │
│ └─ SAMPLE_RES_MIN_DURATION 耗时采样 │
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
│ 通过
▼
内存队列 _memQueue(扁平 Span[])
│
▼ 定时 / 进程/页面退出
┌──────────┴────────────────────┐
│ │
Worker 模式(浏览器) 降级模式(Worker 不可用 / Node.js / UniApp 原生)
Worker 内执行: 主线程执行:
├─ _groupToTraces() ├─ _groupToTraces()
├─ IDB 持久化 ├─ localStorage/内存持久化
└─ fetch 上报 └─ fetch/uni.request 上报
│ │
└──────────────┬────────────────┘
│
┌─────────▼─────────┐
│ HTTP POST 上报 │
│ 解析响应 body 提取 │
│ data.traceControl │
└─────────┬─────────┘
│
┌─────────▼─────────┐
│ _parseControlFrom │
│ Response() │
│ → _applyControl │
└─────────┬─────────┘
│
┌─────────▼─────────┐
│ _retryQueue │ ← 失败时入队重试
│ 内存兜底重试队列 │
└───────────────────┘
运行模式自动降级链路:
ENABLE=false?
└── 是 → 所有方法静默,零副作用,零内存占用(短路返回)
Node.js 环境?
├── 是 → 内存降级模式(无 Worker / 无 localStorage)
│ 退出钩子:process.on('beforeExit') + SIGTERM + SIGINT
└── 否 → UniApp 环境?(uni 对象存在)
├── 是 → Web Worker 可用?(H5 / WebView)
│ ├── 是 → Blob URL 内联 Worker(优先,无路径依赖)
│ │ → CSP 限制 blob: 时降级到 WORKER_PATH 外部文件
│ │ 存储:IndexedDB → 内存
│ └── 否 → 主线程降级模式(App/小程序原生层)
│ 存储:uni.setStorageSync
│ 上报:uni.request
└── 否 → Web Worker 可用?
├── 是 → Blob URL 内联 Worker(优先,无路径依赖)
│ → CSP 限制 blob: 时降级到 WORKER_PATH 外部文件
│ 存储:IndexedDB → 内存
└── 否 → 主线程降级模式
存储:localStorage → 内存
二、上下文生命周期
这是理解 SDK 行为的核心。SDK 维护一个全局属性 currentCtx,它在 startSpan/finish 之间自动流转,保证 HTTP 拦截器请求始终挂在当前业务 Span 下。
2.1 自动流转规则
| 时机 | currentCtx 变化 |
|---|---|
| SDK 初始化 | null |
startSpan(parentCtx, name) 被调用 |
更新为本次 Span 的 childCtx,同时记录 prevCtx = 调用前的 currentCtx |
span.finish() 被调用 |
恢复为 prevCtx(进入前的值) |
startSpan(null, name) 且 currentCtx 不为 null |
挂在当前上下文下(不创建新链路) |
startSpan(null, name) 且 currentCtx 为 null |
创建新根上下文,开启独立链路 |
2.2 完整推演
场景 A:有业务 Span + HTTP 拦截(标准用法)
初始: currentCtx = null
startSpan(null, '加载用户列表')
resolvedCtx = createRootCtx() = {T1, ''}
prevCtx_op = null
currentCtx = {T1, S_op}
axios.get('/api/users') [axios 拦截器]
ctx = currentCtx = {T1, S_op}
startSpan({T1,S_op}, 'GET:/api/users')
prevCtx_ax = {T1, S_op}
currentCtx = {T1, S_ax}
_noAutoFinish = true
[响应拦截器] span_ax._sc = 200, span_ax.finish()
currentCtx → {T1, S_op} ✅
span_op.finish()
currentCtx → null ✅
场景 B:无业务 Span,纯 HTTP 拦截
初始: currentCtx = null
axios.get('/api/users')
ctx = createRootCtx() = {T1, ''}
startSpan({T1,''}, 'GET:/api/users')
prevCtx_ax = null
currentCtx = {T1, S_ax}, _noAutoFinish = true
[响应拦截器] span_ax.finish()
currentCtx → null ✅
场景 C:为什么 HTTP 拦截 Span 要 _noAutoFinish(Bug 复盘)
❌ 旧行为(已修复):
axios.get() → 创建 span,微任务 M1 调度 auto-finish
[M1 早于网络回调运行]
M1: span.finish(OK) → _done=true, _sc=0(状态码未知)
[响应拦截器] span._done=true → 直接跳过 → HTTP 状态码丢失!
✅ 新行为(_noAutoFinish = true):
axios.get() → 创建 span,_noAutoFinish=true
[M1 检测到 _noAutoFinish=true,跳过]
[响应拦截器] span._sc=200, span.finish(OK) → 精确记录 ✅
2.3 并行场景注意事项
并行场景下 currentCtx 会被最后一个 startSpan 覆盖,并行时应显式传入 parentCtx:
// ✅ 并行推荐写法
const [rootCtx, rootSpan] = $trace.startSpan(null, 'root');
const [, spanA] = $trace.startSpan(rootCtx, 'A');
const [, spanB] = $trace.startSpan(rootCtx, 'B');三、环境兼容性
| 环境 | Worker 模式 | 降级模式 | 链路头注入 | 说明 |
|---|---|---|---|---|
| 现代浏览器(Chrome/Firefox/Safari) | — | |||
| Android WebView ≥ 4.4 | — | |||
| Android WebView < 4.4 | (localStorage) | |||
| iOS WKWebView | — | |||
| iOS UIWebView(已废弃) | (localStorage) | |||
| Electron Renderer | — | |||
| 微信小程序 WebView | (内存) | |||
| Node.js ≥ 18 | (内存) | fetch 原生支持 | ||
| Node.js 16–17 | (内存) | 需全局注入 fetch polyfill | ||
| UniApp H5 | — | Worker 模式,与浏览器一致 | ||
| UniApp App(原生) | (WebView ) | (uni.setStorageSync) | WebView 中 H5 页面支持 Worker | |
| UniApp 微信小程序 | (WebView ) | (uni.setStorageSync) | WebView 中 H5 页面支持 Worker | |
| UniApp 支付宝小程序 | (uni.setStorageSync) |
Node.js 注意: 始终走内存降级模式;
SIGKILL时数据不可避免丢失。
UniApp 注意: Worker 仅 H5 端生效;存储/上报层自动切换;页面隐藏需手动调用
onAppHide()。
四、多框架接入指南
SDK 已发布至 npm:@autoxing/front-trace
安装
npm install @autoxing/front-trace4.1 浏览器 Script 标签(CDN,无需安装)
<script src="https://unpkg.com/@autoxing/front-trace/dist/trace-sdk.umd.min.js"></script>
<script>
window.$trace.init({
ENABLE: true,
SERVICE_NAME: 'my-web-app',
UPLOAD_URL: 'https://trace.example.com/trace/v1.0/upload',
UPLOAD_INTERVAL: 5000,
});
// 如使用 axios
window.$trace.initAxiosInterceptor(axios);
</script>4.2 npm / CommonJS(Node.js / webpack)
const $trace = require('@autoxing/front-trace');
$trace.init({
ENABLE: true,
SERVICE_NAME: 'my-node-service',
UPLOAD_URL: 'https://trace.example.com/trace/v1.0/upload',
});
// Node.js 16-17 需先注入 fetch polyfill
// globalThis.fetch = require('node-fetch');4.3 Vue 插件(Vue 2 / Vue 3)
Vue 3(推荐):
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import axios from 'axios';
import $trace from '@autoxing/front-trace';
$trace.init({
ENABLE: true,
SERVICE_NAME: 'my-vue3-app',
UPLOAD_URL: 'https://trace.example.com/trace/v1.0/upload',
});
$trace.initAxiosInterceptor(axios);
const app = createApp(App);
app.use($trace); // 注册 Vue 插件,组件内可用 this.$trace
app.mount('#app');注意:
app.use($trace, opts)内部会调用init(opts),与手动init()二选一即可。推荐先手动init()+initAxiosInterceptor(),初始化位置更明确。
Vue 2:
import Vue from 'vue';
import axios from 'axios';
import $trace from '@autoxing/front-trace';
$trace.init({ ENABLE: true, SERVICE_NAME: 'my-vue2-app', UPLOAD_URL: '...' });
$trace.initAxiosInterceptor(axios);
Vue.use($trace);4.4 UniApp(全端兼容)
① main.js 初始化(最早执行位置):
// main.js(UniApp Vue 3)
import { createSSRApp } from 'vue';
import App from './App.vue';
import $trace from '@autoxing/front-trace';
$trace.init({
ENABLE: true,
SERVICE_NAME: 'my-uniapp',
UPLOAD_URL: 'https://trace.example.com/trace/v1.0/upload',
});
// init() 在 UniApp 环境下自动调用 initUniRequestInterceptor(),拦截 uni.request,无需手动调用
// ⚠️ 如果项目中同时使用了 axios,需额外手动调用:
// import axios from 'axios';
// $trace.initAxiosInterceptor(axios);
export function createApp() {
const app = createSSRApp(App);
return { app };
}② App.vue 桥接页面隐藏事件(重要):
<!-- App.vue -->
<script>
import $trace from '@autoxing/front-trace';
export default {
onHide() {
// UniApp 小程序/App 无 pagehide,需手动桥接触发上报
$trace.onAppHide();
},
};
</script>UniApp 各端能力对比:
| 端 | Worker | 持久化 | 上报方式 | 卸载事件 |
|---|---|---|---|---|
| H5 | IndexedDB → localStorage | fetch | pagehide(自动) | |
| App(原生) | uni.setStorageSync | uni.request | 手动 onAppHide() | |
| 微信小程序 | uni.setStorageSync | uni.request | 手动 onAppHide() | |
| 支付宝小程序 | uni.setStorageSync | uni.request | 手动 onAppHide() |
五、API 参考
5.1 $trace.init(options?)
初始化 SDK。ENABLE 默认为 false,必须显式设为 true 才会真正启动。
npm / UniApp 模式下必须手动调用;浏览器 Script 引入时已自动调用(但仍需配置 ENABLE: true)。
支持多次调用,后调用的配置合并覆盖。配置项详见第九章。
5.2 $trace.initAxisInterceptor()
拦截全局 Axis 对象(window.Axis / global.Axis),自动注入链路头并记录 Span。ENABLE=false 时直接 return。
5.3 $trace.initAxiosInterceptor(axiosInstance?)
拦截 axios,通过 axios.interceptors.request/response.use 注入。ENABLE=false 时直接 return。
// 方式1:拦截全局 axios(自动查找 window.axios / 全局 axios)
$trace.initAxiosInterceptor();
// 方式2:直接传入 import 的 axios 对象(推荐,适用于 Vue/UniApp 等模块化项目)
import axios from 'axios';
$trace.initAxiosInterceptor(axios);
// 方式3:拦截自定义 axios 实例
const instance = axios.create({ baseURL: 'https://api.example.com' });
$trace.initAxiosInterceptor(instance);5.4 $trace.initUniRequestInterceptor()
拦截 uni.request(UniApp 专用)。init() 在 UniApp 环境下自动调用,通常无需手动调用。ENABLE=false 时直接 return。
5.5 $trace.startSpan(parentCtx, name)
开启一个 Span,返回 [childCtx, span]。ENABLE=false 时返回无副作用的 noop span(所有方法均可正常调用,但不记录任何数据)。
| 参数 | 类型 | 说明 |
|---|---|---|
parentCtx |
object | null |
父上下文。传 null 时:若 currentCtx 不为 null 则挂在当前上下文,否则创建新根链路 |
name |
string |
Span 名称 |
| 返回值 | 说明 |
|---|---|
childCtx |
传给下一个 startSpan:传相同 ctx → 并行;传 childCtx → 嵌套 |
span |
同步操作默认微任务自动 finish;异步操作手动调用 span.finish() 精确计时 |
5.6 Span 方法一览
所有方法均支持链式调用(返回 this),且 ENABLE=false 时均为 noop,不会报错。
| 方法 | 对应 Go 策略 | 上报字段 | 说明 |
|---|---|---|---|
span.finish(status?) |
span.End() |
— | 结束 Span,自动恢复 currentCtx。status 默认 SpanStatus.OK |
span.setTag(key, value) |
StartSpan(tags) |
ops[].tgs |
设置单个 Tag |
span.setTags({k:v,...}) |
StartSpan(tags) |
ops[].tgs |
批量设置 Tags |
span.addEvent(name, data?) |
AddSpanEvent() |
ops[].evs[]{n,t,d} |
记录事件,t 为 Unix 纳秒 |
span.recordError(err, attrs?) |
RecordError() |
ops[].ers[]{er,t,as} |
记录错误,同时将首条错误写入顶层 em |
span.setBizStatus(code, msg) |
SetBizResponse() |
顶层 bs + bm |
设置业务状态码和消息,仅根 Span 生效 |
5.7 $trace.SpanStatus
$trace.SpanStatus.OK // 'ok'
$trace.SpanStatus.ERROR // 'error'5.8 业务身份 API(权限检索的核心)
业务身份字段是 trace-worker 诊断系统按权限检索链路的核心维度。正确设置这三个 ID,可以:
- 按用户检索:查找某用户的所有历史调用链路
- 按业务/楼层检索:定位某业务模块的异常分布
- 按设备/机器人检索:追踪特定机器的所有操作记录
- 按用户+设备组合过滤:精确分析某用户操作某机器时的完整行为链路
SDK 提供三个身份设置方法,分别对应 trace-worker 中的检索索引,建议在全链路关键节点都正确设置:
| 方法 | 对应 Go diagnosis.Span 字段 |
trace-worker 检索字段 | 生命周期 |
|---|---|---|---|
$trace.setUserId(id) |
UserID(uid) |
user_id |
登录后调用一次,持久存在,直到退出登录 |
$trace.setBusinessId(id) |
BusinessID(bid) |
business_id |
事件触发时设置,事件结束后调用 clearEventContext() 清空 |
$trace.setDeviceId(id) |
DeviceID(did) |
device_id |
事件触发时设置,事件结束后调用 clearEventContext() 清空 |
$trace.clearEventContext() |
— | — | 清空 businessId + deviceId,不影响 userId |
设计原则(重要):
userId— 登录态唯一标识(如 openId),调用setUserId()后在当前页面生命周期内持续有效,每次上报的 Trace 对象中自动附带uid字段。退出登录时调用setUserId('')清除。businessId/deviceId— 属于 事件级上下文,在触发特定业务操作(如选中某楼层、绑定某台机器人)时设置,操作完成后必须调用clearEventContext()清空,否则会污染后续无关事件的 Span(导致诊断系统将不相关的操作归因到错误的业务/设备维度)。- 三者可以同时存在,诊断系统支持按任意维度组合过滤。例如
userId + deviceId的组合可精确检索"某用户在某机器人上的全部操作链路"。
典型业务身份注入链路示例:
// 1. 用户登录 → 全局设置 userId(持续有效)
$trace.setUserId(userInfo.openId);
// 2. 用户进入某个业务楼层 → 设置 businessId
$trace.setBusinessId(currentBiz.id);
// 此时所有 Span 自动携带 uid + bid
// 3. 用户选择某台机器人执行任务 → 设置 deviceId
$trace.setDeviceId(selectedRobot.serialNo);
// 此时所有 Span 携带 uid + bid + did
// 4. 执行任务操作...
const [, span] = $trace.startSpan(null, 'sendTask');
await axios.post('/api/robot/task', { ... });
span.finish();
// 5. 任务完成 → 清空事件级上下文(userId 保留)
$trace.clearEventContext();
// 后续 Span 只携带 uid,不再携带 bid/did
// 6. 用户退出登录 → 清除 userId
$trace.setUserId('');5.9 $trace.onAppHide()(UniApp 专用)
在 App.vue 的 onHide 中调用,触发页面隐藏时的立即上报。
5.10 $trace.install(app, options?)(Vue 插件)
Vue 插件接口,等价于 init(options) + 挂载 this.$trace,通过 app.use() / Vue.use() 调用。
5.11 $trace.setCtx(ctx)(高级)
强制设置 currentCtx,用于 SPA 路由切换等需要手动管理上下文的场景。ENABLE=false 时直接 return。
六、使用示例
四条核心规则
startSpan(null, name)→ 有currentCtx则挂在当前链路,否则自动创建新根链路- 相同 ctx 传给多个
startSpan→ 并行(兄弟节点)- 用
childCtx作为下一个startSpan的父 → 嵌套(父子节点)- HTTP 拦截器自动挂在
currentCtx下,finish后自动恢复,最终保证为 null
6.1 最简用法(HTTP 拦截自动追踪,零侵入)
// init + initAxiosInterceptor 后,所有 axios 请求自动追踪,无需业务侧任何改动
axios.get('/api/user/list');
axios.post('/api/order/create', { items: [] });6.2 业务 Span + HTTP 请求自动挂载
async function handleLogin(username, password) {
const [, loginSpan] = $trace.startSpan(null, 'login');
try {
await axios.post('/api/auth/login', { username, password });
loginSpan.finish($trace.SpanStatus.OK);
} catch (e) {
loginSpan.recordError(e).finish($trace.SpanStatus.ERROR);
throw e;
}
}链路图:
login
└── POST:/api/auth/login ← axios 自动挂在 login 下
6.3 嵌套追踪(父子 Span)
async function handleLogin(username, password) {
const [ctx1, validateSpan] = $trace.startSpan(null, 'login:validateParams');
validateInput(username, password); // 同步,微任务自动 finish
const [ctx2, loginSpan] = $trace.startSpan(ctx1, 'login:request');
await axios.post('/api/auth/login', { username, password })
.then(() => loginSpan.finish($trace.SpanStatus.OK))
.catch(e => { loginSpan.recordError(e).finish($trace.SpanStatus.ERROR); throw e; });
const [, profileSpan] = $trace.startSpan(ctx2, 'login:fetchProfile');
await axios.get('/api/user/profile')
.then(() => profileSpan.finish($trace.SpanStatus.OK))
.catch(e => { profileSpan.recordError(e).finish($trace.SpanStatus.ERROR); });
}链路图:
login:validateParams
└── login:request
└── login:fetchProfile
6.4 并行追踪(兄弟 Span)
async function loadUserPage(userId) {
const [rootCtx, rootSpan] = $trace.startSpan(null, 'loadUserPage');
const [, profileSpan] = $trace.startSpan(rootCtx, 'fetchProfile');
const [, ordersSpan] = $trace.startSpan(rootCtx, 'fetchOrders');
await Promise.allSettled([
axios.get('/api/user/profile', { params: { userId } })
.then(() => profileSpan.finish($trace.SpanStatus.OK))
.catch(e => profileSpan.recordError(e).finish($trace.SpanStatus.ERROR)),
axios.get('/api/user/orders', { params: { userId } })
.then(() => ordersSpan.finish($trace.SpanStatus.OK))
.catch(e => ordersSpan.recordError(e).finish($trace.SpanStatus.ERROR)),
]);
rootSpan.finish($trace.SpanStatus.OK);
}链路图:
loadUserPage
├── fetchProfile
└── fetchOrders
6.5 Tags / Event / Error / BizStatus(全功能示例)
async function sendRobotTask(robot, task) {
const [, span] = $trace.startSpan(null, 'sendRobotTask');
// 设置 Tags(对应 Go StartSpan(tags) → ops[].tgs)
span.setTags({ robotId: robot.serialNo, taskType: task.type, priority: task.priority });
// 记录事件(对应 Go AddSpanEvent → ops[].evs)
span.addEvent('task.prepared', { taskId: task.id });
try {
const res = await axios.post('/api/robot/task', task);
span.addEvent('task.submitted', { serverTaskId: res.data.taskId });
// 设置业务状态码(对应 Go SetBizResponse → trace.bs + trace.bm)
// 仅根 Span 生效,写入顶层 trace 对象
if (res.data.code !== 200) {
span.setBizStatus(res.data.code, res.data.message);
}
span.finish($trace.SpanStatus.OK);
} catch (e) {
// 记录错误(对应 Go RecordError → ops[].ers)
span.recordError(e, { phase: 'http', robotId: robot.serialNo });
span.finish($trace.SpanStatus.ERROR);
throw e;
}
}6.6 业务身份注入(userId / businessId / deviceId)
重要:
userId、businessId、deviceId是 trace-worker 诊断系统按权限检索链路的核心字段。 正确设置这三个 ID 才能在后端实现按用户、楼层、机器人的链路检索和异常分析。 三者关系:userId登录态持久有效,businessId/deviceId事件级需及时清空。
// ─── 登录成功后(全局执行一次)────────────────────────────────────
$trace.setUserId(userInfo.openId); // 持久存在,整个会话有效
// ─── 用户选择业务楼层 + 机器人,开始操作 ─────────────────────────
$trace.setBusinessId(currentBiz.id);
$trace.setDeviceId(selectedRobot.serialNo);
// 此后所有 Span 自动携带 uid/bid/did
const [, span] = $trace.startSpan(null, 'sendTask');
await axios.post('/api/robot/task', payload);
span.finish($trace.SpanStatus.OK);
// ─── 操作完成,清空事件级上下文 ─────────────────────────────────
$trace.clearEventContext(); // businessId + deviceId 清空;userId 保留
// 此后的 Span 只携带 uid,不携带 bid/did6.7 SPA 路由切换追踪
let _routeSpan = null;
router.beforeEach((to, from, next) => {
const [routeCtx, routeSpan] = $trace.startSpan(null, `navigate:${to.path}`);
$trace.setCtx(routeCtx);
_routeSpan = routeSpan;
next();
});
router.afterEach(() => {
if (_routeSpan) {
_routeSpan.finish($trace.SpanStatus.OK);
_routeSpan = null;
}
});七、与 Go Tracer 的链路对接
7.1 链路头对照
| 前端注入头 | Go 常量 | 后端行为 |
|---|---|---|
x-trace-id: {traceId} |
liteTraceHeaderTraceID |
后端提取作为本次请求的 TraceID |
x-span-id: {spanId} |
liteTraceHeaderSpanID |
后端以此作为 ParentSpanID,前端 Span 成为后端 Span 的父节点 |
traceparent: 00-{tid}-{sid}-01 |
W3C traceparent |
兼容 APISIX 网关、OpenTelemetry 生态 |
x-trace-sample: 1 |
liteTraceHeaderSample |
告知后端强制采样此请求 |
重要: 这些链路头只注入到出站 HTTP 请求中,不写入上报数据包(
hs字段已移除)。
7.2 前后端链路连接原理
前端 Span(traceId=AAA, spanId=111, parentSpanId='')
└── HTTP 请求携带 x-trace-id=AAA, x-span-id=111
│
▼
Go 后端 Middleware
├── 提取 x-trace-id=AAA → 本次 TraceID=AAA
└── 提取 x-span-id=111 → ParentSpanID=111
│
▼
后端 Span(traceId=AAA, spanId=222, parentSpanId=111)
7.3 业务身份字段传递路径与权限检索
前端 SDK 直接写入 uid/bid/did 字段上报;后端服务通过 HTTP Header(x-openid / x-business-id / x-device-id)传递。trace-worker 的 toSpans 转换时优先取直接字段,为空时从 Header 回落:
前端上报:{ uid: "user123", bid: "biz456", did: "robot789" }
│
▼
trace-worker toSpans
│ ct.UserID != "" → 直接使用
│ ct.UserID == "" → headers["x-openid"]
│ ct.BusinessID != "" → 直接使用
│ ct.BusinessID == "" → headers["x-business-id"]
│ ct.DeviceID != "" → 直接使用
│ ct.DeviceID == "" → headers["x-device-id"]
▼
diagnosis.Span { UserID: "user123", BusinessID: "biz456", DeviceID: "robot789" }
权限检索说明: trace-worker 诊断系统对
uid/bid/did字段建立了独立索引,支持以下三种检索模式:
- 按用户检索(
user_id索引):查询指定用户的所有历史链路,用于用户行为回溯和故障排查- 按业务/楼层检索(
business_id索引):定位特定业务模块的异常分布和性能基线- 按设备/机器人检索(
device_id索引):追踪特定设备的全量操作记录- 组合检索:支持
user_id + device_id、business_id + device_id等多维组合过滤因此前端 SDK 正确设置
userId/businessId/deviceId是诊断系统正常运行的前提。 建议在全链路的关键操作节点(登录、选业务、选设备、事件完成)都调用对应 API,确保身份维度不遗漏。
7.4 上报接口(trace-worker)
前端 SDK 上报到 trace-worker 服务:
POST /trace/v1.0/upload
Content-Type: application/json
Body: [childTrace, ...] ← JSON 数组,批量上报
trace-worker 收到后:
- 从请求中提取真实客户端 IP,注入每条 trace 的
cip字段(自动处理反向代理) - 批量 InsertMany 直接入库(
ax_raw_spansMongoDB 集合) - 与 Kafka 消息路径完全等价,经 WorkerManager 重放到诊断流程
八、上报数据格式
每次上报的 JSON Body 为 Trace 对象数组,与 Go Tracer Kafka 消息(childTrace)格式完全兼容:
[
{
"tid": "ce6e59474608400281a57beb3da5576f",
"st": 1779263662070000000,
"et": 1779263662829100000,
"m": "POST",
"p": "/api/robot/task",
"sc": 200,
"pid": "",
"sid": "e9f9e784a3ab40b380d21a233ea35dcc",
"sn": "robot-h5",
"sip": "frontend",
"uid": "oHrGS5bNcK0waXofc5WGNm8TJXxY",
"bid": "biz-floor-3",
"did": "robot-SN-00123",
"bs": 4001,
"bm": "任务队列已满",
"ops": [
{
"n": "sendRobotTask",
"sid": "a1b2c3d4e5f60001",
"pid": "",
"st": 1779263662070000000,
"et": 1779263662100000000,
"tgs": { "taskType": "navigate", "priority": 1 },
"evs": [
{ "n": "task.prepared", "t": 1779263662075000000, "d": { "taskId": "T001" } },
{ "n": "task.submitted", "t": 1779263662095000000, "d": { "serverTaskId": "S999" } }
]
}
]
}
]8.1 顶层 Trace 字段
| 字段 | 类型 | Go childTrace |
SDK 填写 | 说明 |
|---|---|---|---|---|
tid |
string | tid |
TraceID(32位hex) | |
st |
int64 | st |
开始时间,Unix 纳秒(JS ms × 1,000,000) | |
et |
int64 | et |
结束时间,Unix 纳秒 | |
m |
string | m |
HTTP 方法(GET/POST/...)或 FRONTEND(手动 Span) |
|
p |
string | p |
URL 路径部分(去掉 scheme://host),或操作名 | |
sc |
int | sc |
HTTP 状态码(由响应拦截器精确记录) | |
pid |
string | pid |
上游 SpanID(空 = 根链路) | |
sid |
string | sid |
本 Span 的 SpanID | |
sn |
string | sn |
服务名(CONFIG.SERVICE_NAME) |
|
sip |
string | sip |
固定值 "frontend",标识来源为前端 |
|
cip |
string | cip |
服务端注入 | 客户端真实 IP,由 trace-worker 从请求头提取 |
em |
string | em |
(可选) | 错误信息,recordError() 首条或 finish(ERROR) 时写入 |
bs |
int | bs |
(可选) | 业务状态码,setBizStatus(code, msg) 写入,仅根 Span 生效 |
bm |
string | bm |
(可选) | 业务状态消息,配合 bs 使用 |
uid |
string | uid |
(可选) | 用户 ID,setUserId() 设置后自动附带 |
bid |
string | bid |
(可选) | 业务 ID,setBusinessId() 设置后自动附带 |
did |
string | did |
(可选) | 设备/机器人 ID,setDeviceId() 设置后自动附带 |
hs |
— | hs |
不上报 | W3C 链路头只注入出站请求,不写入数据包 |
ops |
array | ops |
子操作列表 |
8.2 ops[] 子操作字段(对应 Go childOp)
| 字段 | 类型 | Go childOp |
说明 |
|---|---|---|---|
n |
string | n |
子操作名称 |
sid |
string | sid |
子 Span ID |
pid |
string | pid |
父 Span ID |
st |
int64 | st |
开始时间,Unix 纳秒 |
et |
int64 | et |
结束时间,Unix 纳秒 |
tgs |
object | tgs |
Tags,setTag/setTags() 写入(可选) |
evs |
array | evs |
事件列表,addEvent() 写入(可选) |
evs[].n |
string | n |
事件名称 |
evs[].t |
int64 | t |
事件时间,Unix 纳秒 |
evs[].d |
object | d |
事件附加数据 |
ers |
array | ers |
错误列表,recordError() 写入(可选) |
ers[].er |
string | er |
错误描述 |
ers[].t |
int64 | t |
错误发生时间,Unix 纳秒 |
ers[].as |
object | as |
错误附加属性 |
时间精度: JS
Date.now()精度为毫秒,统一 ×1,000,000转为纳秒,与 Gotime.Now().UnixNano()一致。
九、配置项说明
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
ENABLE |
boolean | false |
总开关。默认关闭,代码集成后功能完全静默;显式设为 true 才启动 |
SERVICE_NAME |
string | 'frontend' |
服务名,写入 sn 字段,建议按项目设置(如 'robot-h5') |
MAX_SPANS |
number | 500 |
内存队列最大 Span 数,超限按完整链路丢弃最旧 |
UPLOAD_INTERVAL |
number | 3000 |
定时上报间隔(ms) |
UPLOAD_BATCH |
number | 50 |
单次上报最大 Trace 条数 |
EXPIRE |
number | 604800000(7天) |
持久化数据过期时间(ms) |
UPLOAD_URL |
string | string[] | '/trace/v1.0/upload' |
上报接口地址,支持单地址字符串或多地址数组 |
UPLOAD_URL_SELECTION |
string | 'random' |
多地址选择策略:'random'(随机)/ 'round-robin'(轮询)/ 'primary'(主节点,仅失败时切换) |
UPLOAD_HEADERS |
object | {'Authorization': 'APPCODE c328ac72cc814008a5c664c637cab8ab'} |
上报请求自定义 Header,未填时自动补充默认 Authorization |
WORKER_PATH |
string | './trace.worker.js' |
Worker 文件路径,仅当 CSP 禁止 blob: 时才需配置 |
SAMPLE_RATE |
number | 1 |
采样率(1=全量采集;<1 按比例随机采样,如 0.1=10%) |
SAMPLE_MIN_STATUS_CODE |
number | 0 |
最小强制采样状态码(如 400=所有 >=400 的状态码强制采样;0=不启用) |
SAMPLE_STATUS_CODES |
number[] | [] |
特定状态码强制采样列表,如 [499, 502, 503] |
SAMPLE_STATUS_CODE_FILTERS |
number[] | [] |
从强制采样中排除的状态码,如与 SAMPLE_MIN_STATUS_CODE 配合排除特定码 |
SAMPLE_RES_MIN_DURATION |
number | 0 |
最小强制采样耗时(ms),超过该时长的请求强制采样(0=不启用) |
DEBUG |
boolean | false |
调试日志开关(true=输出 console.debug/warn;false=完全静默) |
推荐生产配置:
// 单地址
$trace.init({
ENABLE: true,
SERVICE_NAME: 'your-project-name',
UPLOAD_URL: 'https://trace.example.com/trace/v1.0/upload',
UPLOAD_INTERVAL: 5000,
});
// 多地址(随机负载均衡)
$trace.init({
ENABLE: true,
SERVICE_NAME: 'your-project-name',
UPLOAD_URL: ['https://trace1.example.com/upload', 'https://trace2.example.com/upload'],
UPLOAD_URL_SELECTION: 'random',
});
// 多地址(主备模式)
$trace.init({
ENABLE: true,
SERVICE_NAME: 'your-project-name',
UPLOAD_URL: ['https://primary.example.com/upload', 'https://backup.example.com/upload'],
UPLOAD_URL_SELECTION: 'primary',
});
// 采样控制 + 调试日志
$trace.init({
ENABLE: true,
SERVICE_NAME: 'robot-h5',
UPLOAD_URL: 'https://trace.example.com/trace/v1.0/upload',
UPLOAD_INTERVAL: 5000,
SAMPLE_RATE: 0.5, // 50% 随机采样
SAMPLE_MIN_STATUS_CODE: 400, // 所有 4xx/5xx 强制采样
SAMPLE_RES_MIN_DURATION: 3000, // 超过 3 秒的请求强制采样
DEBUG: false, // 关闭调试日志
});按环境动态控制:
$trace.init({
ENABLE: process.env.NODE_ENV === 'production',
SERVICE_NAME: 'robot-h5',
UPLOAD_URL: process.env.VUE_APP_TRACE_URL,
});十、性能分析
10.1 主线程影响(Worker 模式)
| 操作 | 执行位置 | 主线程耗时 |
|---|---|---|
genId() |
主线程 | ≈ 0.01ms |
startSpan() |
主线程 | ≈ 0.05ms |
| 拦截器注入(axios/uni.request) | 主线程 | ≈ 0.1ms,仅追加 4 个 Header |
postMessage() |
主线程→Worker | ≈ 0.2ms |
_shouldSample()(采样判定) |
主线程 | ≈ 0.01ms |
_groupToTraces()(分组排序+字段映射) |
Worker | 零主线程影响 |
| IndexedDB 读写 / fetch 上报 | Worker | 零主线程影响 |
Worker 模式下,主线程除 startSpan() 和 postMessage() 外无任何数据计算开销,对 60FPS(16.7ms/帧)的影响可以忽略。
10.2 降级模式(无 Worker)主线程影响
| 操作 | 主线程耗时 |
|---|---|
_groupToTraces()(50条 Span) |
≈ 0.5ms |
_httpPost() / sendBeacon |
异步,不阻塞 |
| localStorage 读写 | ≈ 0.1ms |
10.2 内存占用估算
| 项目 | 单条大小 | 500条上限 |
|---|---|---|
| Span 对象(内存队列) | ≈ 300 字节 | ≈ 150 KB |
| Trace 对象(持久化) | ≈ 500 字节 | ≈ 250 KB |
| _retryQueue(重试队列) | ≈ 500 字节 | ≈ 250 KB(与持久化共享) |
10.3 数据可靠性
| 场景 | 机制 | 可靠性 |
|---|---|---|
| 正常关闭页面(浏览器) | sendBeacon |
|
| 移动端切换 App(bfcache) | pagehide + sendBeacon |
|
| 网络断开时关闭页面 | IDB / localStorage 持久化,下次启动重试 | |
| 上报失败(无持久化环境) | _retryQueue 内存重试队列,下次定时周期重试 |
|
| Worker 崩溃 | 主线程降级自动接管 | |
| Node.js 正常退出 | process.on('beforeExit') 异步上报 |
|
| Node.js SIGTERM/SIGINT | 同步清理 + process.exit() |
|
| Node.js SIGKILL | 不可避免丢失 | — |
十一、已知局限与改进建议
| 优先级 | 问题 | 当前状态 / 建议方案 |
|---|---|---|
| — | 采样率控制 | v1.0.14 已实现:SAMPLE_RATE + SAMPLE_STATUS_CODES + SAMPLE_MIN_STATUS_CODE + SAMPLE_RES_MIN_DURATION |
| 高 | Node.js 无持久化 | _retryQueue 内存重试队列已兜底;进程崩溃仍不可避免丢失;可进一步定期序列化写文件 |
| 中 | 并行 HTTP 请求 currentCtx 无法精确分配 | 并行场景需显式传 ctx,见2.3 节 |
| 中 | 不支持原生 fetch / XMLHttpRequest 拦截 |
增加 initFetchInterceptor() / initXHRInterceptor() |
| 低 | 无 $trace.getHeaders(span) 辅助 |
Node.js 手动注入链路头较繁琐,可封装辅助方法 |