基于
@sentry/browser源码(packages/browser)及官方开发者文档整理
一、整体架构
browserTracingIntegration 本质上是一个插件容器 ,它在 afterAllSetup 钩子被调用时(即 Sentry Client 完全初始化后),批量注册多个底层监听器和 Monkey Patch,从而实现零侵入的自动追踪。
scss
Sentry.init()
└── afterAllSetup(client) 被调用
├── 1. 启动 Pageload Span(回溯时间戳到浏览器请求开始)
├── 2. 监听 History API → Navigation Span
├── 3. Monkey Patch fetch / XHR → HTTP Spans
├── 4. PerformanceObserver → Web Vitals Spans
└── 5. PerformanceObserver → 资源加载 Spans
二、Pageload Span:页面加载追踪
实现方式
javascript
// packages/browser/src/tracing/browserTracingIntegration.ts(简化)
function afterAllSetup(client) {
if (options.instrumentPageLoad) {
// 1. 立即创建 pageload idle span
const pageloadSpan = startIdleSpan({
name: window.location.pathname,
op: 'pageload',
// 2. 关键:将开始时间回溯到浏览器真实发起请求的时刻
startTime: getDocumentStartTime(),
});
}
}
function getDocumentStartTime(): number {
// 使用 Navigation Timing API 获取浏览器真实请求时间
// performance.timing.navigationStart 或
// performance.getEntriesByType('navigation')[0].startTime
const navEntry = performance.getEntriesByType('navigation')[0];
return navEntry ? navEntry.startTime / 1000 : Date.now() / 1000;
}
关键设计:Idle Span(空闲 Span)
Pageload Span 不会被显式结束,而是通过空闲机制自动关闭:
ini
Span 创建
│
├── 子 Span 活跃期间:保持开启
│
├── 无新子 Span 超过 idleTimeout(默认 1s)→ 自动结束
│
└── 超过 finalTimeout(默认 30s)→ 强制结束
最早结束时机:document.readyState === 'interactive' | 'complete'
为什么需要 Idle Span?因为浏览器没有一个统一的"页面加载完毕"事件,不同应用(SSR、SPA、懒加载)的结束时机各不相同。
三、Navigation Span:路由变化追踪
实现方式:Monkey Patch History API
SPA 路由跳转不会触发页面刷新,Sentry 通过劫持 History API 来感知路由变化:
javascript
// packages/utils/src/instrument/history.ts(简化)
const originalPushState = window.history.pushState.bind(window.history);
const originalReplaceState = window.history.replaceState.bind(window.history);
window.history.pushState = function (...args) {
// 先执行原始方法
const result = originalPushState.apply(this, args);
// 触发 Sentry 内部事件
triggerHandlers('history', { from: oldHref, to: window.location.href });
return result;
};
window.history.replaceState = function (...args) {
const result = originalReplaceState.apply(this, args);
triggerHandlers('history', { from: oldHref, to: window.location.href });
return result;
};
// 同时监听浏览器前进/后退(popstate 事件)
window.addEventListener('popstate', () => {
triggerHandlers('history', { from: oldHref, to: window.location.href });
});
自动重定向检测(v9.37.0+)
Sentry 能区分用户主动导航 和程序自动重定向 (如未登录跳转到 /login):
arduino
导航发生
│
├── 在 pageload 进行中 + 极短时间内触发 → 视为"重定向",不新建 Span
│
└── 用户交互后触发(有 click/keydown 事件) → 视为"导航",新建 Navigation Span
框架集成:参数化路由名
通用方案只能拿到原始 URL(如 /users/12345),框架 SDK 通过直接挂钩路由器获得参数化名称:
bash
// @sentry/react 中的路由感知(简化)
// 可将 /users/12345 → /users/:id,便于在 Sentry UI 中聚合分析
browserTracingIntegration({
beforeStartSpan: (context) => ({
...context,
name: location.pathname.replace(//\d+/g, '/:id'),
}),
});
四、HTTP 请求 Spans:fetch / XHR 追踪
fetch Monkey Patch
javascript
// packages/utils/src/instrument/fetch.ts(简化)
const originalFetch = window.fetch.bind(window);
window.fetch = function (input, init) {
const url = getUrlFromInput(input);
// 创建子 Span
const span = startInactiveSpan({
name: `${method} ${url}`,
op: 'http.client',
attributes: {
'http.method': method,
'http.url': url,
},
});
// 注入分布式追踪 Header(传播 traceId 到后端)
const headers = {
...init?.headers,
'sentry-trace': spanToSentryTrace(span),
'baggage': spanToBaggage(span),
};
return originalFetch(input, { ...init, headers })
.then((response) => {
// 请求完成,结束 Span
span.setAttribute('http.status_code', response.status);
span.end();
return response;
})
.catch((error) => {
// 请求失败
span.setStatus({ code: SpanStatusCode.ERROR });
span.end();
throw error;
});
};
XMLHttpRequest Monkey Patch
kotlin
// packages/utils/src/instrument/xhr.ts(简化)
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url) {
// 记录请求信息到实例上,send 时使用
this._sentryData = { method, url };
return originalOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function (body) {
const span = startInactiveSpan({
name: `${this._sentryData.method} ${this._sentryData.url}`,
op: 'http.client',
});
this.addEventListener('readystatechange', () => {
if (this.readyState === XMLHttpRequest.DONE) {
span.setAttribute('http.status_code', this.status);
span.end();
}
});
// 注入追踪 Header
this.setRequestHeader('sentry-trace', spanToSentryTrace(span));
this.setRequestHeader('baggage', spanToBaggage(span));
return originalSend.apply(this, arguments);
};
shouldCreateSpanForRequest 过滤器
可以精细控制哪些请求需要创建 Span:
javascript
browserTracingIntegration({
shouldCreateSpanForRequest: (url) => {
// 不追踪健康检查和埋点请求
return !url.match(//(health|analytics)/?$/);
},
});
五、Web Vitals 追踪
实现方式:PerformanceObserver
php
// packages/browser/src/metrics/index.ts(简化)
// LCP(最大内容绘制)
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1] as LargestContentfulPaint;
// 将 LCP 值记录为当前 pageload span 的属性
setMeasurement('lcp', lastEntry.startTime, 'millisecond');
});
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
// CLS(累积布局偏移)
const clsObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!(entry as LayoutShift).hadRecentInput) {
clsValue += (entry as LayoutShift).value;
}
}
});
clsObserver.observe({ type: 'layout-shift', buffered: true });
// FCP(首次内容绘制)
const fcpObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
setMeasurement('fcp', entry.startTime, 'millisecond');
}
}
});
fcpObserver.observe({ type: 'paint', buffered: true });
// TTFB(首字节时间)------ 从 Navigation Timing API 直接读取
const navEntry = performance.getEntriesByType('navigation')[0];
if (navEntry) {
setMeasurement('ttfb', navEntry.responseStart, 'millisecond');
}
INP(交互到下一帧,v8.x+ 默认启用)
php
// INP 需要持续监听所有用户交互
const inpObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// 每次用户交互(click/keydown/pointerdown)都创建一个独立 Span
startSpan({
name: `${entry.name} ${entry.target}`,
op: 'ui.interaction',
attributes: {
'inp.value': entry.duration,
},
});
}
});
inpObserver.observe({ type: 'event', durationThreshold: 40, buffered: true });
LCP/CLS 独立 Span(v10 实验性特性)
旧版本在 pageload span 结束时才上报 LCP/CLS,可能错过最终值。新版将其解耦为独立 Span:
php
// 实验性配置
browserTracingIntegration({
_experiments: {
enableStandaloneLcpSpans: true,
enableStandaloneClsSpans: true,
},
});
六、资源加载 Spans
通过 PerformanceObserver 监听资源加载(JS、CSS、图片、字体等):
javascript
const resourceObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries() as PerformanceResourceTiming[]) {
startInactiveSpan({
name: entry.name, // 资源 URL
op: `resource.${getResourceType(entry)}`, // resource.script / resource.css / resource.img
startTime: entry.startTime / 1000,
attributes: {
'http.response_content_length': entry.transferSize,
'resource.render_blocking_status': entry.renderBlockingStatus,
},
}).end(entry.responseEnd / 1000); // 直接用真实结束时间
}
});
resourceObserver.observe({ type: 'resource', buffered: true });
七、分布式追踪:Header 传播
每个 HTTP Span 创建时,会在请求头中注入追踪上下文,实现前后端链路打通:
yaml
浏览器发起请求
│
├── 自动注入 Header:
│ sentry-trace: {traceId}-{spanId}-{sampled}
│ baggage: sentry-trace_id=xxx,sentry-environment=prod,...
│
后端 Sentry SDK 读取 Header
│
└── 将后端 Span 挂载到同一 traceId 下 → 完整链路视图
配置允许传播的域名:
javascript
browserTracingIntegration({
// 只对这些域名注入追踪 Header,防止泄露到第三方
tracePropagationTargets: ['localhost', /^https://api.myapp.com/],
});
八、总结:各功能的底层 API 对应关系
| 功能 | 底层 Browser API | 核心技术 |
|---|---|---|
| Pageload Span | performance.getEntriesByType('navigation') |
Navigation Timing API + Idle Span |
| Navigation Span | history.pushState / popstate |
Monkey Patch |
| fetch 追踪 | window.fetch |
Monkey Patch |
| XHR 追踪 | XMLHttpRequest.prototype.open/send |
Monkey Patch |
| LCP / FCP / CLS | new PerformanceObserver(...) |
PerformanceObserver API |
| INP | PerformanceObserver({ type: 'event' }) |
PerformanceObserver API |
| TTFB | performance.getEntriesByType('navigation')[0].responseStart |
Navigation Timing API |
| 资源加载 | PerformanceObserver({ type: 'resource' }) |
PerformanceObserver API |
| 分布式追踪 | HTTP Header 注入 | sentry-trace / baggage Header |