web-tracing-core 2.1.1 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__test__/css/performance.css +3 -0
- package/__test__/err-batch.spec.ts +47 -0
- package/__test__/err.spec.ts +82 -0
- package/__test__/event.spec.ts +62 -0
- package/__test__/html/performance.html +57 -0
- package/__test__/html/recordscreen.html +39 -0
- package/__test__/http.spec.ts +143 -0
- package/__test__/img/performance.png +0 -0
- package/__test__/js/performance.js +3 -0
- package/__test__/performance.spec.ts +115 -0
- package/__test__/recordscreen.spec.ts +50 -0
- package/__test__/utils/index.ts +132 -0
- package/__test__/utils/pollify.ts +14 -0
- package/__test__/utils.spec.ts +18 -0
- package/{index.cjs → dist/index.cjs} +1 -1
- package/{index.iife.js → dist/index.iife.js} +1 -1
- package/{index.iife.min.js → dist/index.iife.min.js} +1 -1
- package/{index.mjs → dist/index.mjs} +1 -1
- package/dist/package.json +49 -0
- package/index.ts +76 -0
- package/package.json +9 -9
- package/src/common/config.ts +13 -0
- package/src/common/constant.ts +57 -0
- package/src/common/index.ts +2 -0
- package/src/lib/base.ts +129 -0
- package/src/lib/err-batch.ts +134 -0
- package/src/lib/err.ts +323 -0
- package/src/lib/event-dwell.ts +63 -0
- package/src/lib/event.ts +252 -0
- package/src/lib/eventBus.ts +97 -0
- package/src/lib/exportMethods.ts +208 -0
- package/src/lib/http.ts +197 -0
- package/src/lib/intersectionObserver.ts +169 -0
- package/src/lib/line-status.ts +45 -0
- package/src/lib/options.ts +325 -0
- package/src/lib/performance.ts +302 -0
- package/src/lib/pv.ts +199 -0
- package/src/lib/recordscreen.ts +169 -0
- package/src/lib/replace.ts +371 -0
- package/src/lib/sendData.ts +264 -0
- package/src/observer/computed.ts +52 -0
- package/src/observer/config.ts +1 -0
- package/src/observer/dep.ts +21 -0
- package/src/observer/index.ts +91 -0
- package/src/observer/ref.ts +80 -0
- package/src/observer/types.ts +22 -0
- package/src/observer/watch.ts +19 -0
- package/src/observer/watcher.ts +88 -0
- package/src/types/index.ts +126 -0
- package/src/utils/debug.ts +17 -0
- package/src/utils/element.ts +47 -0
- package/src/utils/fingerprintjs.ts +2132 -0
- package/src/utils/getIps.ts +127 -0
- package/src/utils/global.ts +50 -0
- package/src/utils/index.ts +552 -0
- package/src/utils/is.ts +78 -0
- package/src/utils/localStorage.ts +70 -0
- package/src/utils/polyfill.ts +11 -0
- package/src/utils/session.ts +29 -0
- /package/{LICENSE → dist/LICENSE} +0 -0
- /package/{README.md → dist/README.md} +0 -0
- /package/{index.d.ts → dist/index.d.ts} +0 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import type { InternalOptions, AnyFun, InitOptions, VoidFun } from '../types'
|
|
2
|
+
import { typeofAny, deepAssign } from '../utils'
|
|
3
|
+
import { isEmpty } from '../utils/is'
|
|
4
|
+
import { _support } from '../utils/global'
|
|
5
|
+
import { logError } from '../utils/debug'
|
|
6
|
+
import { ref } from '../observer'
|
|
7
|
+
import type { ObserverValue } from '../observer/types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 管理全局的参数
|
|
11
|
+
*/
|
|
12
|
+
export class Options implements InternalOptions {
|
|
13
|
+
dsn = '' // 上报地址
|
|
14
|
+
appName = '' // 应用名称
|
|
15
|
+
appCode = '' // 应用code
|
|
16
|
+
appVersion = '' // 应用版本
|
|
17
|
+
userUuid = '' // 用户id(外部填充进来的id)
|
|
18
|
+
sdkUserUuid = '' // 用户id(sdk内部生成的id)
|
|
19
|
+
debug = false // 是否开启调试模式(控制台会输出sdk动作)
|
|
20
|
+
pv = {
|
|
21
|
+
core: false // 页面跳转-是否自动发送页面跳转相关数据
|
|
22
|
+
}
|
|
23
|
+
performance = {
|
|
24
|
+
core: false, // 性能数据-是否采集静态资源、接口的相关数据
|
|
25
|
+
firstResource: false, // 性能数据-是否采集首次进入页面的数据(ps: tcp连接耗时,HTML加载完成时间,首次可交互时间)
|
|
26
|
+
server: false // 接口请求-是否采集接口请求(成功的才会采集)
|
|
27
|
+
}
|
|
28
|
+
error = {
|
|
29
|
+
core: false, // 是否采集异常数据(ps: 资源引入错误,promise错误,控制台输出错误)
|
|
30
|
+
server: false // 接口请求-是否采集报错接口数据
|
|
31
|
+
}
|
|
32
|
+
event = {
|
|
33
|
+
core: false // 页面点击-是否采集点击事件
|
|
34
|
+
}
|
|
35
|
+
recordScreen = true // 是否启动录屏
|
|
36
|
+
timeout = 5000 // 日志上报超时时间(毫秒)
|
|
37
|
+
maxQueueLength = 200 // 上报接口异常,日志队列最大缓存数
|
|
38
|
+
checkRecoverInterval = 1 // 多长时间检测一次上报接口是否恢复(分钟)
|
|
39
|
+
|
|
40
|
+
ext = {} // 自定义全局附加参数(放在baseInfo中)
|
|
41
|
+
tracesSampleRate = 1 // 抽样发送
|
|
42
|
+
|
|
43
|
+
cacheMaxLength = 5 // 上报数据最大缓存数
|
|
44
|
+
cacheWatingTime = 5000 // 上报数据最大等待时间
|
|
45
|
+
ignoreErrors = [] // 错误类型事件过滤
|
|
46
|
+
ignoreRequest: Array<string | RegExp> = [] // 请求类型事件过滤
|
|
47
|
+
scopeError = false // 当某个时间段报错时,会将此类错误转为特殊错误类型,会新增错误持续时间范围
|
|
48
|
+
localization = false // 是否本地化:sdk不再主动发送事件,事件都存储在本地,由用户手动调用方法发送
|
|
49
|
+
sendTypeByXmlBody = false // 是否强制指定发送形式为xml,body请求方式
|
|
50
|
+
|
|
51
|
+
// whiteScreen = false // 开启白屏检测
|
|
52
|
+
|
|
53
|
+
// 添加到行为列表前的 hook (在这里面可以给出错误类型,然后就能达到用户想拿到是何种事件类型的触发)
|
|
54
|
+
beforePushEventList: AnyFun[] = []
|
|
55
|
+
|
|
56
|
+
// 数据上报前的 hook
|
|
57
|
+
beforeSendData: AnyFun[] = []
|
|
58
|
+
|
|
59
|
+
// 数据上报后的 hook
|
|
60
|
+
afterSendData: VoidFun[] = []
|
|
61
|
+
|
|
62
|
+
// 本地化存储溢出后的回调
|
|
63
|
+
localizationOverFlow: VoidFun = () => {
|
|
64
|
+
// do something
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
constructor(initOptions: InitOptions) {
|
|
68
|
+
const _options = this.transitionOptions(initOptions)
|
|
69
|
+
_options.ignoreRequest.push(new RegExp(_options.dsn))
|
|
70
|
+
deepAssign<Options>(this, _options)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 对入参配置项进行转换
|
|
75
|
+
*/
|
|
76
|
+
private transitionOptions(options: InitOptions): Options {
|
|
77
|
+
const _options = deepAssign<Options>({}, this, options)
|
|
78
|
+
const { beforePushEventList, beforeSendData, afterSendData } = options
|
|
79
|
+
const { pv, performance, error, event } = _options
|
|
80
|
+
|
|
81
|
+
if (typeof pv === 'boolean') {
|
|
82
|
+
_options.pv = {
|
|
83
|
+
core: pv
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (typeof performance === 'boolean') {
|
|
87
|
+
_options.performance = {
|
|
88
|
+
core: performance,
|
|
89
|
+
firstResource: performance,
|
|
90
|
+
server: performance
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (typeof error === 'boolean') {
|
|
94
|
+
_options.error = {
|
|
95
|
+
core: error,
|
|
96
|
+
server: error
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (typeof event === 'boolean') {
|
|
100
|
+
_options.event = {
|
|
101
|
+
core: event
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (beforePushEventList) {
|
|
106
|
+
_options.beforePushEventList = [beforePushEventList]
|
|
107
|
+
}
|
|
108
|
+
if (beforeSendData) {
|
|
109
|
+
_options.beforeSendData = [beforeSendData]
|
|
110
|
+
}
|
|
111
|
+
if (afterSendData) {
|
|
112
|
+
_options.afterSendData = [afterSendData]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (options.timeout !== undefined) {
|
|
116
|
+
_options.timeout = options.timeout
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (options.maxQueueLength !== undefined) {
|
|
120
|
+
_options.maxQueueLength = options.maxQueueLength
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (options.checkRecoverInterval !== undefined) {
|
|
124
|
+
_options.checkRecoverInterval = options.checkRecoverInterval
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return _options
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function _validateInitOption(options: InitOptions) {
|
|
132
|
+
const {
|
|
133
|
+
dsn,
|
|
134
|
+
appName,
|
|
135
|
+
appCode,
|
|
136
|
+
appVersion,
|
|
137
|
+
userUuid,
|
|
138
|
+
debug,
|
|
139
|
+
recordScreen,
|
|
140
|
+
pv,
|
|
141
|
+
performance,
|
|
142
|
+
error,
|
|
143
|
+
event,
|
|
144
|
+
ext,
|
|
145
|
+
tracesSampleRate,
|
|
146
|
+
cacheMaxLength,
|
|
147
|
+
cacheWatingTime,
|
|
148
|
+
ignoreErrors,
|
|
149
|
+
ignoreRequest,
|
|
150
|
+
scopeError,
|
|
151
|
+
localization,
|
|
152
|
+
sendTypeByXmlBody,
|
|
153
|
+
// whiteScreen,
|
|
154
|
+
beforePushEventList,
|
|
155
|
+
beforeSendData,
|
|
156
|
+
timeout,
|
|
157
|
+
maxQueueLength,
|
|
158
|
+
checkRecoverInterval
|
|
159
|
+
} = options
|
|
160
|
+
|
|
161
|
+
const validateFunList = []
|
|
162
|
+
|
|
163
|
+
if (pv && typeof pv === 'object') {
|
|
164
|
+
validateFunList.push(validateOption(pv.core, 'pv.core', 'boolean'))
|
|
165
|
+
} else {
|
|
166
|
+
validateFunList.push(validateOption(pv, 'pv', 'boolean'))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (performance && typeof performance === 'object') {
|
|
170
|
+
validateFunList.push(
|
|
171
|
+
validateOption(performance.core, 'performance.core', 'boolean'),
|
|
172
|
+
validateOption(
|
|
173
|
+
performance.firstResource,
|
|
174
|
+
'performance.firstResource',
|
|
175
|
+
'boolean'
|
|
176
|
+
),
|
|
177
|
+
validateOption(performance.server, 'performance.server', 'boolean')
|
|
178
|
+
)
|
|
179
|
+
} else {
|
|
180
|
+
validateFunList.push(validateOption(performance, 'performance', 'boolean'))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (error && typeof error === 'object') {
|
|
184
|
+
validateFunList.push(
|
|
185
|
+
validateOption(error.core, 'error.core', 'boolean'),
|
|
186
|
+
validateOption(error.server, 'error.server', 'boolean')
|
|
187
|
+
)
|
|
188
|
+
} else {
|
|
189
|
+
validateFunList.push(validateOption(error, 'error', 'boolean'))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (event && typeof event === 'object') {
|
|
193
|
+
validateFunList.push(validateOption(event.core, 'event.core', 'boolean'))
|
|
194
|
+
} else {
|
|
195
|
+
validateFunList.push(validateOption(event, 'event', 'boolean'))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const validateList = [
|
|
199
|
+
validateOption(dsn, 'dsn', 'string'),
|
|
200
|
+
validateOption(appName, 'appName', 'string'),
|
|
201
|
+
validateOption(appCode, 'appCode', 'string'),
|
|
202
|
+
validateOption(appVersion, 'appVersion', 'string'),
|
|
203
|
+
validateOption(userUuid, 'userUuid', 'string'),
|
|
204
|
+
validateOption(debug, 'debug', 'boolean'),
|
|
205
|
+
validateOption(recordScreen, 'recordScreen', 'boolean'),
|
|
206
|
+
|
|
207
|
+
validateOption(ext, 'ext', 'object'),
|
|
208
|
+
validateOption(tracesSampleRate, 'tracesSampleRate', 'number'),
|
|
209
|
+
|
|
210
|
+
validateOption(cacheMaxLength, 'cacheMaxLength', 'number'),
|
|
211
|
+
validateOption(cacheWatingTime, 'cacheWatingTime', 'number'),
|
|
212
|
+
|
|
213
|
+
validateOption(ignoreErrors, 'ignoreErrors', 'array'),
|
|
214
|
+
validateOptionArray(ignoreErrors, 'ignoreErrors', ['string', 'regexp']),
|
|
215
|
+
|
|
216
|
+
validateOption(ignoreRequest, 'ignoreRequest', 'array'),
|
|
217
|
+
validateOptionArray(ignoreRequest, 'ignoreRequest', ['string', 'regexp']),
|
|
218
|
+
|
|
219
|
+
validateOption(scopeError, 'scopeError', 'boolean'),
|
|
220
|
+
validateOption(localization, 'localization', 'boolean'),
|
|
221
|
+
validateOption(sendTypeByXmlBody, 'sendTypeByXmlBody', 'boolean'),
|
|
222
|
+
// validateOption(whiteScreen, 'whiteScreen', 'boolean'),
|
|
223
|
+
validateOption(beforePushEventList, 'beforePushEventList', 'function'),
|
|
224
|
+
validateOption(beforeSendData, 'beforeSendData', 'function'),
|
|
225
|
+
validateOption(timeout, 'timeout', 'number'),
|
|
226
|
+
validateOption(maxQueueLength, 'maxQueueLength', 'number'),
|
|
227
|
+
validateOption(checkRecoverInterval, 'checkRecoverInterval', 'number')
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
return validateList.every(res => !!res)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 验证必填项
|
|
235
|
+
* @param options 入参对象
|
|
236
|
+
*/
|
|
237
|
+
function _validateMustFill(options: InitOptions) {
|
|
238
|
+
const validateList = [
|
|
239
|
+
validateOptionMustFill(options.appName, 'appName'),
|
|
240
|
+
validateOptionMustFill(options.dsn, 'dsn')
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
return validateList.every(res => !!res)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 验证必填项
|
|
248
|
+
* @param target 属性值
|
|
249
|
+
* @param targetName 属性名
|
|
250
|
+
* @returns 是否通过验证
|
|
251
|
+
*/
|
|
252
|
+
function validateOptionMustFill(target: any, targetName: string): boolean {
|
|
253
|
+
if (isEmpty(target)) {
|
|
254
|
+
logError(`【${targetName}】参数必填`)
|
|
255
|
+
return false
|
|
256
|
+
}
|
|
257
|
+
return true
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 验证选项的类型是否符合要求
|
|
262
|
+
* @param target 源对象
|
|
263
|
+
* @param targetName 对象名
|
|
264
|
+
* @param expectType 期望类型
|
|
265
|
+
* @returns 是否通过验证
|
|
266
|
+
*/
|
|
267
|
+
function validateOption(
|
|
268
|
+
target: any,
|
|
269
|
+
targetName: string,
|
|
270
|
+
expectType: string
|
|
271
|
+
): boolean | void {
|
|
272
|
+
if (!target || typeofAny(target) === expectType) return true
|
|
273
|
+
logError(
|
|
274
|
+
`TypeError:【${targetName}】期望传入${expectType}类型,目前是${typeofAny(
|
|
275
|
+
target
|
|
276
|
+
)}类型`
|
|
277
|
+
)
|
|
278
|
+
return false
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* 验证选项的类型 - 针对数组内容类型的验证
|
|
283
|
+
* @param target 源对象
|
|
284
|
+
* @param targetName 对象名
|
|
285
|
+
* @param expectTypes 期望类型
|
|
286
|
+
* @returns 是否通过验证
|
|
287
|
+
*/
|
|
288
|
+
function validateOptionArray(
|
|
289
|
+
target: any[] | undefined,
|
|
290
|
+
targetName: string,
|
|
291
|
+
expectTypes: string[]
|
|
292
|
+
): boolean | void {
|
|
293
|
+
if (!target) return true
|
|
294
|
+
let pass = true
|
|
295
|
+
|
|
296
|
+
target.forEach(item => {
|
|
297
|
+
if (!expectTypes.includes(typeofAny(item))) {
|
|
298
|
+
logError(
|
|
299
|
+
`TypeError:【${targetName}】数组内的值期望传入${expectTypes.join(
|
|
300
|
+
'|'
|
|
301
|
+
)}类型,目前值${item}是${typeofAny(item)}类型`
|
|
302
|
+
)
|
|
303
|
+
pass = false
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
return pass
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export let options: ObserverValue<InternalOptions>
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* 初始化参数
|
|
314
|
+
* @param initOptions 原始参数
|
|
315
|
+
* @returns 是否初始化成功
|
|
316
|
+
*/
|
|
317
|
+
export function initOptions(initOptions: InitOptions): boolean {
|
|
318
|
+
// 必传校验 && 入参类型校验
|
|
319
|
+
if (!_validateMustFill(initOptions) || !_validateInitOption(initOptions))
|
|
320
|
+
return false
|
|
321
|
+
|
|
322
|
+
options = ref(new Options(initOptions))
|
|
323
|
+
_support.options = options
|
|
324
|
+
return true
|
|
325
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { sendData } from './sendData'
|
|
2
|
+
import { eventBus } from './eventBus'
|
|
3
|
+
import { EVENTTYPES, SEDNEVENTTYPES, SENDID } from '../common'
|
|
4
|
+
import { AnyObj } from '../types'
|
|
5
|
+
import {
|
|
6
|
+
on,
|
|
7
|
+
getLocationHref,
|
|
8
|
+
normalizeObj,
|
|
9
|
+
isValidKey,
|
|
10
|
+
sendReaconImageList,
|
|
11
|
+
getTimestamp
|
|
12
|
+
} from '../utils'
|
|
13
|
+
import { _global, _support } from '../utils/global'
|
|
14
|
+
import { options } from './options'
|
|
15
|
+
|
|
16
|
+
// 兼容判断
|
|
17
|
+
const supported = {
|
|
18
|
+
performance: !!_global.performance,
|
|
19
|
+
getEntriesByType: !!(
|
|
20
|
+
_global.performance && _global.performance.getEntriesByType
|
|
21
|
+
),
|
|
22
|
+
PerformanceObserver: 'PerformanceObserver' in _global,
|
|
23
|
+
MutationObserver: 'MutationObserver' in _global,
|
|
24
|
+
PerformanceNavigationTiming: 'PerformanceNavigationTiming' in _global
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 资源属性
|
|
28
|
+
const performanceEntryAttrs = {
|
|
29
|
+
initiatorType: '',
|
|
30
|
+
transferSize: 0,
|
|
31
|
+
encodedBodySize: 0,
|
|
32
|
+
decodedBodySize: 0,
|
|
33
|
+
duration: 0,
|
|
34
|
+
redirectStart: 0,
|
|
35
|
+
redirectEnd: 0,
|
|
36
|
+
startTime: 0,
|
|
37
|
+
fetchStart: 0,
|
|
38
|
+
domainLookupStart: 0,
|
|
39
|
+
domainLookupEnd: 0,
|
|
40
|
+
connectStart: 0,
|
|
41
|
+
connectEnd: 0,
|
|
42
|
+
requestStart: 0,
|
|
43
|
+
responseStart: 0,
|
|
44
|
+
responseEnd: 0,
|
|
45
|
+
workerStart: 0
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 发送页面追踪资源加载性能数据
|
|
50
|
+
* (支持getEntriesByType的情况下才追踪)
|
|
51
|
+
*/
|
|
52
|
+
function traceResourcePerformance(performance: PerformanceObserverEntryList) {
|
|
53
|
+
// 排除xmlhttprequest类型,服务器有响应便会记录,包括404的请求,转由http-request模块负责记录请求数据,区分请求状态
|
|
54
|
+
// 同时也会排除一些其他类型,比如在引入一个script后会触发一次性能监控,它的类型是beacon,这一次的要排除
|
|
55
|
+
const observerTypeList = ['img', 'script', 'link', 'audio', 'video', 'css']
|
|
56
|
+
|
|
57
|
+
const entries = performance.getEntriesByType(
|
|
58
|
+
'resource'
|
|
59
|
+
) as PerformanceResourceTiming[]
|
|
60
|
+
const records: any[] = []
|
|
61
|
+
|
|
62
|
+
entries.forEach(entry => {
|
|
63
|
+
// initiatorType含义:通过某种方式请求的资源,例如script,link..
|
|
64
|
+
const { initiatorType = '' } = entry
|
|
65
|
+
|
|
66
|
+
// 只记录observerTypeList中列出的资源类型请求,不在列表中则跳过
|
|
67
|
+
if (observerTypeList.indexOf(initiatorType.toLowerCase()) < 0) return
|
|
68
|
+
|
|
69
|
+
// sdk内部 img 发送请求的错误不会记录
|
|
70
|
+
if (sendReaconImageList.length) {
|
|
71
|
+
const index = sendReaconImageList.findIndex(
|
|
72
|
+
item => item.src === entry.name
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if (index !== -1) {
|
|
76
|
+
sendReaconImageList.splice(index, 1)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const value: AnyObj = {}
|
|
82
|
+
Object.keys(performanceEntryAttrs).forEach(attr => {
|
|
83
|
+
if (isValidKey(attr, entry)) {
|
|
84
|
+
value[attr] = entry[attr]
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
records.push(
|
|
89
|
+
normalizeObj({
|
|
90
|
+
...value,
|
|
91
|
+
eventType: SEDNEVENTTYPES.PERFORMANCE,
|
|
92
|
+
eventId: SENDID.RESOURCE,
|
|
93
|
+
requestUrl: entry.name,
|
|
94
|
+
triggerTime: getTimestamp(),
|
|
95
|
+
triggerPageUrl: getLocationHref()
|
|
96
|
+
})
|
|
97
|
+
)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
if (records.length) sendData.emit(records)
|
|
101
|
+
return records
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let mutationObserver: MutationObserver | null = null
|
|
105
|
+
let performanceObserver: PerformanceObserver | null = null
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 监听 - 异步插入的script、link、img, DOM更新操作记录
|
|
109
|
+
*/
|
|
110
|
+
function observeSourceInsert() {
|
|
111
|
+
const tags = ['img', 'script', 'link']
|
|
112
|
+
// 检测异步插入的script、link、img,会有一些延迟,一些连接建立、包体大小的数据会丢失,精度下降
|
|
113
|
+
// MutationObserver DOM3 Events规范,是个异步监听,只有在全部DOM操作完成之后才会调用callback
|
|
114
|
+
mutationObserver = new MutationObserver(mutationsList => {
|
|
115
|
+
for (let i = 0; i < mutationsList.length; i += 1) {
|
|
116
|
+
const startTime = getTimestamp()
|
|
117
|
+
const { addedNodes = [] } = mutationsList[i]
|
|
118
|
+
addedNodes.forEach((node: Node & { src?: string; href?: string }) => {
|
|
119
|
+
const { nodeName } = node
|
|
120
|
+
if (tags.indexOf(nodeName.toLowerCase()) !== -1) {
|
|
121
|
+
on(node as Document, EVENTTYPES.LOAD, function () {
|
|
122
|
+
sendData.emit(
|
|
123
|
+
normalizeObj({
|
|
124
|
+
eventType: SEDNEVENTTYPES.PERFORMANCE,
|
|
125
|
+
eventId: SENDID.RESOURCE,
|
|
126
|
+
requestUrl: node.src || node.href,
|
|
127
|
+
duration: getTimestamp() - startTime,
|
|
128
|
+
triggerTime: getTimestamp(),
|
|
129
|
+
triggerPageUrl: getLocationHref()
|
|
130
|
+
})
|
|
131
|
+
)
|
|
132
|
+
})
|
|
133
|
+
on(node as Document, EVENTTYPES.ERROR, function () {
|
|
134
|
+
sendData.emit(
|
|
135
|
+
normalizeObj({
|
|
136
|
+
eventType: SEDNEVENTTYPES.PERFORMANCE,
|
|
137
|
+
eventId: SENDID.RESOURCE,
|
|
138
|
+
requestUrl: node.src || node.href,
|
|
139
|
+
responseStatus: 'error',
|
|
140
|
+
duration: getTimestamp() - startTime,
|
|
141
|
+
triggerTime: getTimestamp(),
|
|
142
|
+
triggerPageUrl: getLocationHref()
|
|
143
|
+
})
|
|
144
|
+
)
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
mutationObserver.observe(_global.document, {
|
|
151
|
+
subtree: true, // 目标以及目标的后代改变都会观察
|
|
152
|
+
childList: true // 表示观察目标子节点的变化,比如添加或者删除目标子节点,不包括修改子节点以及子节点后代的变化
|
|
153
|
+
// attributes: true, // 观察属性变动
|
|
154
|
+
// attributeFilter: ['src', 'href'] // 要观察的属性
|
|
155
|
+
})
|
|
156
|
+
// observer.disconnect()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 发送页面性能数据
|
|
161
|
+
*/
|
|
162
|
+
function observeNavigationTiming() {
|
|
163
|
+
const times: AnyObj = {}
|
|
164
|
+
const { performance } = _global
|
|
165
|
+
let t: any = performance.timing
|
|
166
|
+
|
|
167
|
+
times.fmp = 0 // 首屏时间 (渲染节点增量最大的时间点)
|
|
168
|
+
if (supported.getEntriesByType) {
|
|
169
|
+
const paintEntries = performance.getEntriesByType('paint')
|
|
170
|
+
if (paintEntries.length) {
|
|
171
|
+
times.fmp = paintEntries[paintEntries.length - 1].startTime
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 优先使用 navigation v2 https://www.w3.org/TR/navigation-timing-2/
|
|
175
|
+
if (supported.PerformanceNavigationTiming) {
|
|
176
|
+
const nt2Timing = performance.getEntriesByType('navigation')[0]
|
|
177
|
+
if (nt2Timing) t = nt2Timing
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 从开始发起这个页面的访问开始算起,减去重定向跳转的时间,在performanceV2版本下才进行计算
|
|
182
|
+
// v1版本的fetchStart是时间戳而不是相对于访问起始点的相对时间
|
|
183
|
+
if (times.fmp && supported.PerformanceNavigationTiming) {
|
|
184
|
+
times.fmp -= t.fetchStart
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 白屏时间 (从请求开始到浏览器开始解析第一批HTML文档字节的时间差)
|
|
188
|
+
// times.fpt = t.responseEnd - t.fetchStart;
|
|
189
|
+
|
|
190
|
+
times.tti = t.domInteractive - t.fetchStart // 首次可交互时间
|
|
191
|
+
|
|
192
|
+
times.ready = t.domContentLoadedEventEnd - t.fetchStart // HTML加载完成时间
|
|
193
|
+
|
|
194
|
+
times.loadon = t.loadEventStart - t.fetchStart // 页面完全加载时间
|
|
195
|
+
|
|
196
|
+
times.firstbyte = t.responseStart - t.domainLookupStart // 首包时间
|
|
197
|
+
|
|
198
|
+
times.dns = t.domainLookupEnd - t.domainLookupStart // dns查询耗时
|
|
199
|
+
|
|
200
|
+
times.appcache = t.domainLookupStart - t.fetchStart // dns缓存时间
|
|
201
|
+
|
|
202
|
+
times.tcp = t.connectEnd - t.connectStart // tcp连接耗时
|
|
203
|
+
|
|
204
|
+
times.ttfb = t.responseStart - t.requestStart // 请求响应耗时
|
|
205
|
+
|
|
206
|
+
times.trans = t.responseEnd - t.responseStart // 内容传输耗时
|
|
207
|
+
|
|
208
|
+
times.dom = t.domInteractive - t.responseEnd // dom解析耗时
|
|
209
|
+
|
|
210
|
+
times.res = t.loadEventStart - t.domContentLoadedEventEnd // 同步资源加载耗时
|
|
211
|
+
|
|
212
|
+
times.ssllink = t.connectEnd - t.secureConnectionStart // SSL安全连接耗时
|
|
213
|
+
|
|
214
|
+
times.redirect = t.redirectEnd - t.redirectStart // 重定向时间
|
|
215
|
+
|
|
216
|
+
times.unloadTime = t.unloadEventEnd - t.unloadEventStart // 上一个页面的卸载耗时
|
|
217
|
+
|
|
218
|
+
const resultInfo = { ...times, triggerPageUrl: getLocationHref() }
|
|
219
|
+
|
|
220
|
+
_support.firstScreen = { ...resultInfo }
|
|
221
|
+
|
|
222
|
+
sendData.emit(
|
|
223
|
+
normalizeObj({
|
|
224
|
+
...resultInfo,
|
|
225
|
+
eventType: SEDNEVENTTYPES.PERFORMANCE,
|
|
226
|
+
eventId: SENDID.PAGE
|
|
227
|
+
})
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 页面资源加载性能数据
|
|
233
|
+
*/
|
|
234
|
+
function observeResource() {
|
|
235
|
+
if (supported.performance && options.value.performance.firstResource) {
|
|
236
|
+
observeNavigationTiming()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (supported.performance && options.value.performance.core) {
|
|
240
|
+
traceResourcePerformance(_global.performance)
|
|
241
|
+
|
|
242
|
+
if (supported.PerformanceObserver) {
|
|
243
|
+
// 监听异步资源加载性能数据 chrome≥52
|
|
244
|
+
performanceObserver = new PerformanceObserver(traceResourcePerformance)
|
|
245
|
+
performanceObserver.observe({ entryTypes: ['resource'] })
|
|
246
|
+
} else if (supported.MutationObserver) {
|
|
247
|
+
// 监听资源、DOM更新操作记录 chrome≥26 ie≥11
|
|
248
|
+
observeSourceInsert()
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function initPerformance() {
|
|
254
|
+
if (
|
|
255
|
+
!options.value.performance.firstResource &&
|
|
256
|
+
!options.value.performance.core
|
|
257
|
+
)
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
// 初始化方法可能在onload事件之后才执行,此时不会触发load事件了 (例如delayInit)
|
|
261
|
+
// 检查document.readyState属性来判断onload事件是否会被触发
|
|
262
|
+
if (document.readyState === 'complete') {
|
|
263
|
+
observeResource()
|
|
264
|
+
} else {
|
|
265
|
+
eventBus.addEvent({
|
|
266
|
+
type: EVENTTYPES.LOAD,
|
|
267
|
+
callback: () => {
|
|
268
|
+
observeResource()
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* 主动触发性能事件上报
|
|
276
|
+
* @param options 自定义配置信息
|
|
277
|
+
*/
|
|
278
|
+
function handleSendPerformance(options = {}, flush = false) {
|
|
279
|
+
const record = {
|
|
280
|
+
...options,
|
|
281
|
+
triggerTime: getTimestamp(),
|
|
282
|
+
triggerPageUrl: getLocationHref(),
|
|
283
|
+
eventType: SEDNEVENTTYPES.PERFORMANCE
|
|
284
|
+
}
|
|
285
|
+
sendData.emit(normalizeObj(record), flush)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* 销毁性能监听
|
|
290
|
+
*/
|
|
291
|
+
export function destroyPerformance() {
|
|
292
|
+
if (mutationObserver) {
|
|
293
|
+
mutationObserver.disconnect()
|
|
294
|
+
mutationObserver = null
|
|
295
|
+
}
|
|
296
|
+
if (performanceObserver) {
|
|
297
|
+
performanceObserver.disconnect()
|
|
298
|
+
performanceObserver = null
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export { initPerformance, handleSendPerformance }
|