npm.io
1.0.20 • Published 5d ago

@autoxing/front-trace

Licence
MIT
Version
1.0.20
Deps
0
Size
226 kB
Vulns
0
Weekly
284

FrontTrace SDK 技术文档

版本:v1.0.14(npm:@autoxing/front-trace@1.0.14) 更新日期:2026-06-05


目录

  1. 架构概览
  2. 上下文生命周期
  3. 环境兼容性
  4. 多框架接入指南
  5. API 参考
  6. 使用示例
  7. 与 Go Tracer 的链路对接
  8. 上报数据格式
  9. 配置项说明
  10. 性能分析
  11. 已知局限与改进建议

一、架构概览

业务代码
  │
  ├── 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-trace

4.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,自动恢复 currentCtxstatus 默认 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) UserIDuid user_id 登录后调用一次,持久存在,直到退出登录
$trace.setBusinessId(id) BusinessIDbid business_id 事件触发时设置,事件结束后调用 clearEventContext() 清空
$trace.setDeviceId(id) DeviceIDdid 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.vueonHide 中调用,触发页面隐藏时的立即上报。

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。


六、使用示例

四条核心规则

  1. startSpan(null, name) → 有 currentCtx 则挂在当前链路,否则自动创建新根链路
  2. 相同 ctx 传给多个 startSpan并行(兄弟节点)
  3. childCtx 作为下一个 startSpan 的父 → 嵌套(父子节点)
  4. HTTP 拦截器自动挂在 currentCtxfinish 后自动恢复,最终保证为 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)

重要: userIdbusinessIddeviceId 是 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/did

6.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_idbusiness_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 收到后:

  1. 从请求中提取真实客户端 IP,注入每条 trace 的 cip 字段(自动处理反向代理)
  2. 批量 InsertMany 直接入库ax_raw_spans MongoDB 集合)
  3. 与 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 转为纳秒,与 Go time.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/warnfalse=完全静默)

推荐生产配置:

// 单地址
$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 手动注入链路头较繁琐,可封装辅助方法

Keywords