日志采集模块,提供一系列方便的api供使用
- 新版2.x部分API不再兼容1.x
- 从2.1.0版本开始,不再兼容IE8及以下IE浏览器
- 从2.1.8版本开始,兼容小程序环境(new Image类发送);通过继承类,覆盖request方法,可以支持Node.js/跨端框架/小程序环境
npm install spy-client --save
CDN方式
不是一次性3个JS都引入,具体往下看
<!--增强版SDK-->
<script src="https://code.bdstatic.com/npm/spy-client@2.1.8/dist/spy-client.min.js" type="text/javascript"></script>
<!--增强版SDK spy-head-->
<script src="https://code.bdstatic.com/npm/spy-client@2.1.8/dist/spy-head.min.js" type="text/javascript"></script>
<!--基础版SDK-->
<script src="https://code.bdstatic.com/npm/spy-client@2.1.8/dist/spy-client-basic.min.js" type="text/javascript"></script>
如果对于一些指标想理解更准确,看源码是最佳方式 SDK源码
SDK的指标采集请酌情选用,不要一股脑全用上,如果只用了一项采集功能,但SDK体积太大,可以考虑自行编译,看文档最后
初始化
const SpyClient = require('spy-client');
const spy = new SpyClient({
pid: '1_1000', // 必须
lid: '', // 可选,页面的logid
sample: 1 // 可选,默认为1, 全局抽样,取值:[0-1], 所有发送接口都受到该抽样,单个发送接口的sample配置会覆盖该抽样。
});
发送性能日志
// 发送性能日志
spy.sendPerf({
// 可选, 分组,默认common,用户自定义
group: 'test',
// 必须, 指标信息,每个字段为一个指标,由用户自定义,这里的fisrtScreen、whiteScreen等都是业务自己定义,后续会在平台上配置好,平台会从该字段取对应指标信息。
// 这些指标需要你自行计算好时间再发送,不能带单位
info: {
tcp: 1200,
domReady: 600
},
// 可选,维度信息,每个字段为一个维度,由用户自定义,这里的netType、pageType都是业务自己定义,后续会在平台上配置好,平台会从该字段取对应维度信息。
dim: {
os: 'ios',
netType: 'wifi'
}
});
SDK分两种
- 基础版SDK:提供最基础和最简单的功能,如果这些功能能满足你,那么直接使用该SDK即可,因为体积较小
- 增强版SDK:除了基础版SDK功能外,集合了丰富的常用的性能和异常指标统计
接下来分别介绍
提供最基础和最简单的功能,如果这些功能能满足你,那么直接使用该SDK即可
// basic spy-client 基本用法,最简单功能
const SpyClient = require('spy-client/dist/spy-client-basic');
const spy = new SpyClient({
pid: '1_1000', // 必须
lid: '', // 可选,页面的logid
sample: 1 // 可选,默认为1, 全局抽样,取值:[0-1], 所有发送接口都受到该抽样,单个发送接口的sample配置会覆盖该抽样。
});
以下先简单列举所有可用API示例
// 发生性能日志,本质是数值型的metric数据
spy.sendPerf({
// 可选, 分组,默认common,用户自定义
group: 'test',
// 必须, 指标信息,每个字段为一个指标,由用户自定义,这里的fisrtScreen、whiteScreen等都是业务自己定义,后续会在平台上配置好,平台会从该字段取对应指标信息。
// 这些指标需要你自行计算好时间再发送,不能带单位
info: {
tcp: 1200,
domReady: 600
},
// 可选,维度信息,每个字段为一个维度,由用户自定义,这里的netType、pageType都是业务自己定义,后续会在平台上配置好,平台会从该字段取对应维度信息。
dim: {
os: 'ios',
netType: 'wifi'
}
});
// 发送异常日志
spy.sendExcept({
// 必须, 异常信息,msg字段是必须的,是异常唯一标识。其他字段作为补充信息,由用户自定义
info: {
msg: 'abc is not undefined', // msg字段是必须的,必须的,必须的,会统计相同msg的总量
stack: 'xxxxx',
file: 'xxxxxxx'
},
// 可选, 分组,默认common,用户自定义
group: 'test',
// 可选,维度信息,每个字段为一个维度,由用户自定义
dim: {
os: 'ios'
}
});
// 发送分布日志
spy.sendDist({
info: {
from: 'hao123'
},
dim: {
os: 'ios'
}
});
// 发送计数日志
spy.sendCount({
info: {
from: 'hao123'
},
dim: {
os: 'ios'
}
});
// 如果能拿到error实例,通过该方法快速上报异常,默认会获取stack等信息
spy.sendExceptForError(new Error('error'), {
dim: {
os: 'ios'
}
});
// 最基础的API,需要自行指定type字段
spy.send({
type: 'perf'
info: {
domReady: 1000
},
dim: {}
});
// 统计辅助方法
spy.startMark('playTime');
let time = spy.endMark('playTime');
console.log(time); // output: 1000
spy.startMark('pauseTime');
spy.endMark('pauseTime'); // 假设中间执行花费1s
console.log(spy.getAllMark());
// output
// {
// playTime: 1000,
// pauseTime: 1000
// }
spy.clearMark('pauseTime'); // 清除pauseTime
spy.clearAllMark(); // 清除所有mark的信息
基础版可支持小程序/Node.js/跨端框架环境
1 . 小程序 不用做任何修改,就支持采用new Image发送日志。
2 . Node.js/跨端框架环境
Node.js,跨端框架,以及小程序环境中若采用spy.send(xxx, true)
方式,则需要继承SpyClient类,覆盖request方法.
如果是Node.js,需要服务器有外网权限
const SpyClient = require('spy-client/dist/spy-client-basic');
// 若环境编译不支持umd,则可以导入es module
// const SpyClient = require('spy-client/dist/spy-client-basic.esm');
class SpyClientNode from SpyClient {
request(url: string, data?: any) {
axios({
method: data ? 'post' : 'get',
url,
data: data ? JSON.stringify(data) : data,
});
}
}
const spy = new SpyClientNode({
pid: '1_1000', // 必须
lid: '', // 可选,页面的logid
sample: 1 // 可选,默认为1, 全局抽样,取值:[0-1], 所有发送接口都受到该抽样,单个发送接口的sample配置会覆盖该抽样。
});
spy.sendPerf({
info: {
responseTime: 200
}
});
增强版SDK分成了2部分
-
spy-head:有些功能我们希望越早生效越好,比如全局JS报错监控。因此把这些功能最小集抽成一个单独JS,以便可以插入head标签内,也不会全量引入整个SDK在头部。当然,放到任何地方都是可以,开发者自行决策即可。此部分包含的功能有
- 异常:全局JS报错监控、资源加载失败监控、白屏异常监控
- 性能:Longtask等信息采集,真正的统计是在spy-client里,只是越早采集,能获取更多的longtask
-
spy-client:此部分提供了丰富的性能和异常的指标统计,其中部分功能依赖于spy-head,包含的功能有
- 性能指标采集:包含体积、卡顿、速度等60+个性能指标采集方法
- 异常:包含大于150K的大图片采集、HTTPS环境下HTTP资源采集
- 辅助方式: mark系列辅助方法
增强版SDK仅支持浏览器环境
spy-head JS可以视情况通过script内联或嵌入其他JS里
如果要启用一项异常监控功能,需要设置其抽样sample不为0
<script>
// spy-head js可以视情况通过script内联或外链,外链地址可见文档开头的CDN
const spyHead = require('spy-client/dist/spy-head');
spyHead.init({
pid: '1_1', // spy申请的pid
lid: '', // 业务的log id,可选
// 数据类型:异常,触发时间:监听的window.addEventListen('error')有资源加载失败时
// 上报信息里包含资源的标签名,资源地址,xpath
// 用不着的话,需要删掉这个配置
resourceError: {
// 发送的分组名称,可以自定义
group: 'resource',
// 抽样,禁用可以设置为0
sample: 1,
// 对发送之前的数据进行操作,如果不想发送,返回false即可
// 如果想增加维度,可以自定加上data.dim字段
// 用不着的话,可以删掉这个函数
handler: function (data) {
}
},
// 数据类型:异常,触发时间:监听的全局报错window.addEventListen('error'),有未被捕获的全局异常抛出时
// 上报信息里包含错误message,stack,之前已发生的所有错误等
// 用不着的话,需要删掉这个配置
jsError: {
// 发送的分组名称,可以自定义
group: 'js',
// 抽样,禁用可以设置为0
sample: 1,
// 对发送之前的数据进行操作,如果不想发送,返回false即可
// 如果想增加维度,可以自定加上data.dim字段
// 用不着的话,可以删掉这个函数
handler: function (data) {
}
},
// 数据类型:异常,触发时间:OnJudgeReturnFalseWhenTimeout
// 上报信息里包含之前已发生的所有错误、dns、tcp、请求响应时间(可能为负,说明该过程没有完成)、设备信息等
// 用不着的话,需要删掉这个配置
whiteScreenError: {
// 抽样,禁用可以设置为0
sample: 1,
// 发送的分组名称,可以自定义
group: 'whiteScreen',
// 一旦以下逻辑不满足,就认为白屏:document.querySelector(selector)的元素包含 document.querySelector(selector).querySelector(subSelector) 并且 document.querySelector(selector) 的高度大于屏幕高度的2/3
selector: 'body',
subSelector: 'button1',
// 单位ms,在timeout后,执行上述检测
timeout: 6000,
// 对发送之前的数据进行操作,如果不想发送,返回false即可
// 如果想增加维度,可以自定加上data.dim字段
// 用不着的话,可以删掉这个函数
handler: function(data) {
}
}
});
<script>
像Vue等组件框架会对组件代码做try catch,然后打印到console,这种错误情况是不会有全局错误的,即window.onerror不会触发。只能使用框架的全局error回调(比如Vue.config.errorHandler)去拿到错误信息,再调用spy.sendExcept上报。 所以在发现上述jsError全局异常监听失效时,请先自行调试是否能通过window.addEventListen('error')监听到
// enhanced spy-client
const SpyClient = require('spy-client');
const spy = new SpyClient({
pid: '1_1000', // 必须
lid: '', // 可选,页面的logid
sample: 1 // 可选,默认为1, 全局抽样,取值:[0-1], 所有发送接口都受到该抽样,单个发送接口的sample配置会覆盖该抽样。
});
// 类型:性能,触发时间:500MsAfterOnLoad,说明:performance timing的数据采集基本指标
spy.listenTiming(function (metric) {
spy.sendPerf({
info: metric
});
});
metric定义
export interface TimingMetric {
// dns解析
dns: number;
// tcp链接
tcp: number;
// 主文档请求
request: number;
// 主文档响应时间
response: number;
// DOM解析时间:Dom解析开始到结束时间
// 这是从页面部分数据返回,浏览器开始解析doc元素到最底部的script脚本解析执行完成
// 脚本里触发的异步方法或绑定了更靠后的事件,不再纳入范围内
parseHtml: number;
// DOM解析完成总时间:页面开始加载到Dom解析结束
// 很多事件绑定是在domContentLoaded事件里的,所以等其结束,一般页面元素的事件绑定好了,用户可以正确交互
// 当然存在在该加载事件之后绑定元素事件情况,但不再此考虑范围内
domReady: number;
// 处理所有注册的load事件函数的时间
loadEventHandle: number;
// onload完成时间
// 基本该做的都做完,资源也都加载完成了
// 当然在onload事件处理函数里启动了异步方法,不再纳入范围内
load: number;
// first-paint https://w3c.github.io/paint-timing/#sec-PerformancePaintTiming
fp?: number;
// first-contentful-paint https://w3c.github.io/paint-timing/#sec-PerformancePaintTiming
fcp?: number;
// T7内核计算的首次绘制, 单位ms
t7FirstPaint?: number;
// T7内核计算的首屏时间, 单位ms
t7FirstScreen?: number;
}
最大块内容绘制完成时间
- 依赖:spy-head.js
// 类型:性能,触发时间:500MsAfterOnLoad
spy.listenLCP(function (metric) {
spy.sendPerf({
info: metric
});
});
metric定义
export interface LCPMetric {
// Largest Contentful Paint https://web.dev/lcp/
// 在onload时间内最大块内容绘制完成时间
lcp: number;
}
首次输入延迟,衡量首次交互卡顿
- 依赖:spy-head.js
// 类型:性能,触发时间:OnInput
spy.listenFID(function (metric) {
spy.sendPerf({
group: 'fid',
info: metric
});
});
metric定义
export interface FIDMetric {
// First Input Delay https://web.dev/fid/
// 首次输入延迟
fid: number;
}
用户可完全交互时间
// 数据类型:性能,触发时间:当第一次出现5s内没有网络请求(默认排查了gif请求),且没有longtask产出时
spy.listenTTI(function (metric) {
spy.sendPerf({
group: 'tti',
info: metric
});
});
metric定义
export interface TTIMetric {
// Time to Interactive https://web.dev/tti/
// 用户可完全交互时间
tti: number;
}
获取资源的本身大小、传输大小、数量、传输、缓存率
// 数据类型:性能,触发时间:500MsAfterOnLoad
spy.listenResource(function (metric, hostMetric) {
spy.sendPerf({
info: metric
});
// 分域名进行统计的
console.log('hostMetric', hostMetric);
});
metric定义
export interface ResourceMetric {
// 页面整体大小:包括主文档、所有JS、CSS、Img、Font,单位KB
allSize: number;
// 主文档大小 KB
docSize: number;
// 主文档的响应header的大小,包含cookie等 KB
headerSize: number;
// js外链的个数
jsNum: number;
cssNum: number;
imgNum: number;
fontNum: number;
// 所有JS外链的大小
jsSize: number;
cssSize: number;
imgSize: number;
fontSize: number;
// 页面整体网络传输大小,通常来说资源有了缓存,传输大小就为0,另外有Gzip的话,传输大小相比资源本身大小也要小很多
allTransferSize: number;
// 主文档网络传输大小
docTransferSize: number;
// 所有JS外链的传输大小
jsTransferSize: number;
cssTransferSize: number;
imgTransferSize: number;
fontTransferSize: number;
// js cache率
jsCacheRate: number;
cssCacheRate: number;
imgCacheRate: number;
};
hostMetric定义
export interface ResourceHostMetric {
[host: string]: {
hostNum: number;
hostSize: number;
hostTransferSize: number;
hostDuration: number;
hostCacheRate: number;
};
};
// 数据类型:异常,触发时间:500MsAfterOnLoad
spy.listenSlowResource(function (info) {
spy.sendExcept({
info: info
});
}, {threshold: 1000});
info定义
export interface ResourceErrorInfo {
// 发生异常的资源链接
msg: string;
// 发生异常的资源元素的xpath信息,一直到body
xpath: string;
// 资源host
host: string;
// 资源类型
type: string;
// 资源耗时
dur?: number;
}
第二个参数,option定义
export interface SlowOption {
/**
* 加载时长大于该阈值,就认为是慢资源,默认1000,单位是ms
*/
threshold?: number;
/**
* 忽略指定path的资源
*/
ignorePaths?: string[];
/**
* 触发时机,在load事件触发后,还是在用户离开页面后,收集出现的加载慢的资源。,默认是load
*/
trigger?: 'load' | 'leave';
}
大于150KB(默认,第二个参数可以修改)的来自img标签的大图检测。
// 数据类型:异常,触发时间:500MsAfterOnLoad
spy.listenBigImg(function (info) {
spy.sendExcept({
info: info
});
});
info定义
export interface ResourceErrorInfo {
// 发生异常的资源链接
msg: string;
// 发生异常的资源元素的xpath信息,一直到body
xpath: string;
// 资源host
host: string;
// 资源类型
type: string;
// 资源耗时
dur?: number;
}
第二个参数,option定义
export interface BigImgOption {
/**
* 体积大于该阈值,就认为是大图,默认150,单位是kb
*/
maxSize?: number;
/**
* 忽略指定path的资源
*/
ignorePaths?: string[];
/**
* 触发时机,在load事件触发后,还是在用户离开页面后,收集出现的加载慢的资源。,默认是load
*/
trigger?: 'load' | 'leave';
}
// 数据类型:异常,触发时间:500MsAfterOnLoad
spy.listenHttpResource(function (info) {
spy.sendExcept({
info: info
});
});
info定义
export interface ResourceErrorInfo {
// 发生异常的资源链接
msg: string;
// 发生异常的资源元素的xpath信息,一直到body
xpath: string;
// 资源host
host: string;
// 资源类型
type: string;
// 资源耗时
dur?: number;
}
第二个参数,option定义
export interface HttpResOption {
/**
* 忽略指定path的资源
*/
ignorePaths?: string[];
/**
* 触发时机,在load事件触发后,还是在用户离开页面后,收集出现的加载慢的资源。,默认是load
*/
trigger?: 'load' | 'leave';
}
- 依赖:spy-head.js
// 数据类型:性能,触发时间:OnLoad
spy.listenFSPLongTask(function (metric) {
spy.sendPerf({
info: metric
});
});
metric定义
export interface FSPLongtaskMetric {
// 在T7内核首屏时间内, 每个longtask的时间总和
fspLongtaskTime: number;
// 在T7内核首屏时间内,Total Blocking Time时间总和,即Sum(每个longtask的时间 - 50)
// Total Blocking Time定义:
fspTBT: number;
// T7内核首屏时间
fspTotalTime: number;
// 在T7内核首屏时间内, Longtask率 = 100 * fspLongtaskTime / fspTotalTime
fspLongtaskRate: number;
// 在T7内核首屏时间内的Longtask数量
fspLongtaskNum: number;
}
- 依赖:spy-head.js
// 数据类型:性能,触发时间:500MsAfterOnLoad
spy.listenLCPLongTask(function (metric) {
spy.sendPerf({
info: metric
});
});
metric定义
export interface LCPLongtaskMetric {
// 在Largest Contentful Paint时间内, 每个longtask的时间总和
lcpLongtaskTime: number;
// 在Largest Contentful Paint时间内,Total Blocking Time时间总和,即Sum(每个longtask的时间 - 50)
lcpTBT: number;
// Largest Contentful Paint时间
lcpTotalTime: number;
// 在Largest Contentful Paint时间内, Longtask率 = 100 * lcpLongtaskTime / lcpTotalTime
lcpLongtaskRate: number;
// 在Largest Contentful Paint时间内的Longtask数量
lcpLongtaskNum: number;
}
- 依赖:spy-head.js
// 数据类型:性能,触发时间:OnLoad
spy.listenLoadLongTask(function (metric) {
spy.sendPerf({
info: metric
});
});
metric定义
export interface LoadLongtaskMetric {
// 在onload即页面加载完成时间内, 每个longtask的时间总和
loadLongtaskTime: number;
// 在onload即页面加载完成时间内,Total Blocking Time时间总和,即Sum(每个longtask的时间 - 50)
loadTBT: number;
// onload即页面加载完成时间
loadTotalTime: number;
// 在onload即页面加载完成时间内, Longtask率 = 100 * loadLongtaskTime / loadTotalTime
loadLongtaskRate: number;
// 在onload即页面加载完成时间内的Longtask数量
loadLongtaskNum: number;
}
开始加载页面到第一次离开页面(隐藏或点出)时间内的LongTask信息
- 依赖:spy-head.js
// 数据类型:性能,触发时间:OnLeavingPageFirstly
spy.listenPageLongTask(function (metric) {
spy.sendPerf({
info: metric
});
});
metric定义
export interface PageLongtaskMetric {
// 在开始加载页面到第一次离开页面(隐藏或点出)时间内, 每个longtask的时间总和
pageLongtaskTime: number;
// 在开始加载页面到第一次离开页面(隐藏或点出)时间内,Total Blocking Time时间总和,即Sum(每个longtask的时间 - 50)
pageTBT: number;
// 开始加载页面到第一次离开页面(隐藏或点出)时间
pageTotalTime: number;
// 在开始加载页面到第一次离开页面(隐藏或点出)时间内, Longtask率 = 100 * pageLongtaskTime / pageTotalTime
pageLongtaskRate: number;
// 在开始加载页面到第一次离开页面(隐藏或点出)时间内的Longtask数量
pageLongtaskNum: number;
// 在开始加载页面到第一次离开页面(隐藏或点出)时间内,每个来自iframe内的longtask的时间总和
pageIframeLongtaskTime: number;
// 在开始加载页面到第一次离开页面(隐藏或点出)时间内,每个来自iframe内的Longtask率 = 100 * pageIframeLongtaskTime / pageTotalTime
pageIframeLongtaskRate: number;
// 在开始加载页面到第一次离开页面(隐藏或点出)时间内,每个来自iframe内的Longtask数量
pageIframeLongtaskNum: number;
}
页面布局的变化程度, 变化过多,可能让用户觉得页面不稳定,抖动
- 依赖:spy-head.js
// 数据类型:性能,触发时间:OnLeavingPageFirstly
spy.listenLayoutShift(function (metric) {
spy.sendPerf({
info: metric
});
});
metric定义
export interface LayoutShiftMetric {
// Cumulative Layout Shift定义 https://web.dev/cls/
// 在开始加载页面到第一次离开页面(隐藏或点出)时间内的 Cumulative Layout Shift
layoutShift: number;
}
页面使用内存信息
在页面第一次离开时(隐藏或点出)触发
// 数据类型:性能,触发时间:在页面第一次离开时(隐藏或点出)
spy.listenMemory(function (metric) {
spy.sendPerf({
info: metric
});
});
metric定义
export interface MemoryMetric {
// 已使用内存, 单位KB
usedJSHeapSize: number;
// 分配给页面的内存,单位KB
totalJSHeapSize: number;
// 内存限制,单位KB
jsHeapSizeLimit: number;
// 内存使用率百分比 = 100 * usedJSHeapSize / totalJSHeapSize
usedJSHeapRate: number;
}
获取一些设备信息
// 数据类型:性能,触发时间:OnLoad
const info = spy.getNavigatorInfo();
info定义
export interface NavigatorInfoMetric {
// 网络下载速度 https://developer.mozilla.org/zh-CN/docs/Web/API/Navigator/connection
downlink?: number;
// 网络类型
effectiveType?: '2g' | '3g' | '4g' | 'slow-2g';
// 估算的往返时间
rtt?: number;
// 数据保护模式
saveData?: boolean;
// 设备内存 https://developer.mozilla.org/en-US/docs/Web/API/Navigator/deviceMemory
deviceMemory?: number;
// 设备逻辑核数 https://developer.mozilla.org/en-US/docs/Web/API/NavigatorConcurrentHardware/hardwareConcurrency
hardwareConcurrency?: number;
}
样例参考 Example
如果觉得spy-client太大,只想要部分模块,比如禁用 longtask,可以拉取源码,自行编译
git clone https://github.com/kaivean/spy-client.git
cd spy-log
npm install
比如禁用longtask,前往src/spy-client.ts
在顶部注释掉import
// import Longtask from './module/longtask';
在constructor里注释掉register
// this.register(new Longtask());
npm run build
然后找到dist/spy-client.min.js 就是构建压缩版代码
# 启动本地调试页面,进行调试
npm run example
# 进行watch 编译, 一般和上个命令配合使用
npm run watch
# lint
npm run lint
# 测试
npm run test
# production编译,产出到dist
npm run build
# development编译,产出到dist
npm run dev
# 发布
# 1. 构建测试
npm run release_pre
# 2. 提交代码
git add . && git commit -m "升级/Fix"
# 3. 发布npm包,增加tag
npm run release
# 4. 修改Readme文档里版本号
git add . && git commit -m "修改文档版本"
# 5. 把代码push到远程
npm run release_post