web-tracing-core 2.1.0
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 +112 -0
- package/__test__/recordscreen.spec.ts +50 -0
- package/__test__/utils/index.ts +99 -0
- package/__test__/utils/pollify.ts +14 -0
- package/__test__/utils.spec.ts +18 -0
- package/dist/LICENSE +21 -0
- package/dist/README.md +97 -0
- package/dist/index.cjs +15943 -0
- package/dist/index.d.ts +323 -0
- package/dist/index.iife.js +15946 -0
- package/dist/index.iife.min.js +28 -0
- package/dist/index.mjs +15913 -0
- package/dist/package.json +49 -0
- package/index.ts +75 -0
- package/package.json +49 -0
- 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 +164 -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 +49 -0
- package/src/utils/index.ts +551 -0
- package/src/utils/is.ts +78 -0
- package/src/utils/localStorage.ts +70 -0
- package/src/utils/session.ts +27 -0
package/src/lib/pv.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { AnyObj } from '../types'
|
|
2
|
+
import { baseInfo } from './base'
|
|
3
|
+
import { sendData } from './sendData'
|
|
4
|
+
import { getLocationHref, getTimestamp } from '../utils'
|
|
5
|
+
import { _global } from '../utils/global'
|
|
6
|
+
import { options } from './options'
|
|
7
|
+
import { eventBus } from './eventBus'
|
|
8
|
+
import { EVENTTYPES, SEDNEVENTTYPES, WEBPAGELOAD } from '../common'
|
|
9
|
+
|
|
10
|
+
let oldURL = getLocationHref()
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 路由Pv采集
|
|
14
|
+
*
|
|
15
|
+
* 分为以下几种情况(sdk是兼容vue2以及vue3的,但可能有些部分是取了它们的一些特性的,可能会受到后续它们的更改的影响)
|
|
16
|
+
* 针对 history/hash 以及 刷新页面/初始加载 的情况获取到的数据进行说明
|
|
17
|
+
*
|
|
18
|
+
* 1. 针对普通 html
|
|
19
|
+
* 1. 初始化项目:不触发这里的钩子,只会触发首次进入页面方法(referer=''、action=navigation)
|
|
20
|
+
* 2. 手动刷新当前页:不触发这里的钩子,只会触发首次进入页面方法(referer=''、action=reload)
|
|
21
|
+
* 3. window.history.pushState 时会触发 pushState
|
|
22
|
+
* 4. window.history.replaceState 时其会触发 replaceState
|
|
23
|
+
* 5. 手动 更改地址栏地址(或者window.location.href) 时其触发顺序: popstate -> hashchange (history 模式手动更改地址会视为初始化项目)
|
|
24
|
+
*
|
|
25
|
+
* 2. 针对 vue2(vue-router)
|
|
26
|
+
* 1. 手动刷新当前页
|
|
27
|
+
* 1. history模式:不触发这里的钩子,只会触发首次进入页面方法(referer=''、action=reload)
|
|
28
|
+
* 2. hash模式:触发首次进入页面的方法(referer=''、action=reload) + replaceState 钩子(referer字段与triggerPageUrl字段相同)
|
|
29
|
+
* 2. 初始化项目
|
|
30
|
+
* 1. history模式:不触发这里的钩子,只会触发首次进入页面方法(referer=''、action=navigation)
|
|
31
|
+
* 2. hash模式:触发首次进入页面的方法(action=reload;referer字段为网址根路径) + replaceState 钩子(action=navigation;referer字段与triggerPageUrl字段相同)
|
|
32
|
+
* 3. push 时其会触发 pushState (history、hash模式表现相同)
|
|
33
|
+
* 4. repleace 时其会触发 replaceState (history、hash模式表现相同)
|
|
34
|
+
* 5. 更改地址栏地址 时其触发顺序: popstate -> hashchange (history 模式手动更改地址会当做刷新页面,什么也不会触发)
|
|
35
|
+
* 6. 浏览器回退:popstate -> hashchange (history模式只触发 popstate)
|
|
36
|
+
*
|
|
37
|
+
* 3. 针对 vue3(vue-router)
|
|
38
|
+
* 1. 手动刷新当前页
|
|
39
|
+
* 1. history模式:触发首次进入页面的方法(action=reload;referer字段为空) + replaceState 钩子(action=navigation;referer字段与triggerPageUrl字段相同)
|
|
40
|
+
* 2. hash模式:触发首次进入页面的方法(action=reload;referer字段为空) + replaceState 钩子(action=navigation;referer字段与triggerPageUrl字段相同)
|
|
41
|
+
* 2. 初始化项目
|
|
42
|
+
* 1. history模式:触发首次进入页面的方法(action=navigation;referer字段为空) + replaceState 钩子(action=navigation;referer字段与triggerPageUrl字段相同)
|
|
43
|
+
* 2. hash模式:触发首次进入页面的方法(action=navigation;referer字段为空) + replaceState 钩子(action=navigation;referer字段与triggerPageUrl字段相同)
|
|
44
|
+
* 3. push 时其触发顺序: replaceState -> pushState (history、hash模式表现相同)
|
|
45
|
+
* 4. repleace 时其会触发 replaceState (history、hash模式表现相同)
|
|
46
|
+
* 5. 更改地址栏地址 时其触发顺序: replaceState -> popstate -> hashchange (history 模式仅触发 replaceState)
|
|
47
|
+
* 6. 浏览器回退:popstate -> hashchange (history模式只触发 popstate)
|
|
48
|
+
*/
|
|
49
|
+
function initPv() {
|
|
50
|
+
if (!options.value.pv.core) return
|
|
51
|
+
|
|
52
|
+
let lastIsPop = false // 最后一次触发路由变化是否为popState触发
|
|
53
|
+
let repetitionRoute = false // 在触发 replaceState 后 100ms 内的 pushState 会被无效记录
|
|
54
|
+
|
|
55
|
+
sendPageView({ referer: document.referrer }) // 首次进入记录url变化
|
|
56
|
+
|
|
57
|
+
eventBus.addEvent({
|
|
58
|
+
type: EVENTTYPES.HISTORYPUSHSTATE,
|
|
59
|
+
callback: () => {
|
|
60
|
+
if (repetitionRoute) return
|
|
61
|
+
lastIsPop = false
|
|
62
|
+
sendPageView({ action: 'navigation' })
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
eventBus.addEvent({
|
|
67
|
+
type: EVENTTYPES.HISTORYREPLACESTATE,
|
|
68
|
+
callback: () => {
|
|
69
|
+
repetitionRoute = true
|
|
70
|
+
lastIsPop = false
|
|
71
|
+
sendPageView({ action: 'navigation' })
|
|
72
|
+
setTimeout(() => {
|
|
73
|
+
repetitionRoute = false
|
|
74
|
+
}, 100)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
eventBus.addEvent({
|
|
79
|
+
type: EVENTTYPES.HASHCHANGE,
|
|
80
|
+
callback: () => {
|
|
81
|
+
if (repetitionRoute) return
|
|
82
|
+
if (!lastIsPop) sendPageView()
|
|
83
|
+
lastIsPop = false
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
eventBus.addEvent({
|
|
88
|
+
type: EVENTTYPES.POPSTATE,
|
|
89
|
+
callback: () => {
|
|
90
|
+
if (repetitionRoute) return
|
|
91
|
+
if (_global.location.hash !== '') {
|
|
92
|
+
const oldHost =
|
|
93
|
+
oldURL.indexOf('#') > 0 // 多页面情况下 history模式刷新还是在pv页面
|
|
94
|
+
? oldURL.slice(0, oldURL.indexOf('#'))
|
|
95
|
+
: oldURL
|
|
96
|
+
if (
|
|
97
|
+
_global.location.href.slice(0, _global.location.href.indexOf('#')) ===
|
|
98
|
+
oldHost
|
|
99
|
+
)
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
lastIsPop = true
|
|
103
|
+
sendPageView()
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// 在页面卸载时发送页面停留事件
|
|
108
|
+
eventBus.addEvent({
|
|
109
|
+
type: EVENTTYPES.BEFOREUNLOAD,
|
|
110
|
+
callback: () => {
|
|
111
|
+
const durationTime = getTimestamp() - durationStartTime
|
|
112
|
+
if (Object.values(lastSendObj).length > 0 && durationTime > 100) {
|
|
113
|
+
sendData.emit({ ...lastSendObj, durationTime }, true)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let durationStartTime = getTimestamp()
|
|
120
|
+
let lastSendObj: any = {}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 发送数据
|
|
124
|
+
* 这里会发送路由跳转时间事件 以及 上一个页面停留时间事件
|
|
125
|
+
*/
|
|
126
|
+
function sendPageView(option: AnyObj = {}) {
|
|
127
|
+
const { referer = oldURL, action, params, title } = option
|
|
128
|
+
let _action = action
|
|
129
|
+
if (!_action) {
|
|
130
|
+
_action = WEBPAGELOAD[performance.navigation.type] || ''
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ------------- 发送路由跳转时间事件 -------------
|
|
134
|
+
// 如果option.title为空,则等待框架处理document.title,延迟17ms
|
|
135
|
+
// 为什么是17ms? 一秒60Hz是基准,平均1Hz是17毫秒,只要出来了页面那就有 document.title
|
|
136
|
+
setTimeout(
|
|
137
|
+
() => {
|
|
138
|
+
oldURL = getLocationHref()
|
|
139
|
+
const sendObj = {
|
|
140
|
+
eventType: SEDNEVENTTYPES.PV,
|
|
141
|
+
eventId: baseInfo.pageId,
|
|
142
|
+
triggerPageUrl: getLocationHref(),
|
|
143
|
+
referer,
|
|
144
|
+
params,
|
|
145
|
+
title: title || document.title,
|
|
146
|
+
action: _action,
|
|
147
|
+
triggerTime: getTimestamp()
|
|
148
|
+
}
|
|
149
|
+
sendData.emit(sendObj)
|
|
150
|
+
|
|
151
|
+
// ------------- 发送上一个页面停留时间事件 -------------
|
|
152
|
+
const durationTime = getTimestamp() - durationStartTime
|
|
153
|
+
durationStartTime = getTimestamp()
|
|
154
|
+
if (Object.values(lastSendObj).length > 0 && durationTime > 100) {
|
|
155
|
+
sendData.emit({ ...lastSendObj, durationTime })
|
|
156
|
+
}
|
|
157
|
+
lastSendObj = {
|
|
158
|
+
...sendObj,
|
|
159
|
+
eventType: SEDNEVENTTYPES.PVDURATION
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
title ? 0 : 17
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 手动发送数据
|
|
168
|
+
* @param options 自定义配置信息
|
|
169
|
+
*/
|
|
170
|
+
function handleSendPageView(options: AnyObj = {}, flush = false) {
|
|
171
|
+
sendData.emit(
|
|
172
|
+
{
|
|
173
|
+
referer: oldURL,
|
|
174
|
+
title: document.title,
|
|
175
|
+
...options,
|
|
176
|
+
eventType: SEDNEVENTTYPES.PV,
|
|
177
|
+
eventId: baseInfo.pageId,
|
|
178
|
+
triggerPageUrl: getLocationHref(),
|
|
179
|
+
triggerTime: getTimestamp()
|
|
180
|
+
},
|
|
181
|
+
flush
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 卸载所有页面访问/路由监听
|
|
187
|
+
*/
|
|
188
|
+
export function destroyPv() {
|
|
189
|
+
// 清除页面访问相关的事件类型
|
|
190
|
+
eventBus.removeEvents([
|
|
191
|
+
EVENTTYPES.HISTORYPUSHSTATE,
|
|
192
|
+
EVENTTYPES.HISTORYREPLACESTATE,
|
|
193
|
+
EVENTTYPES.HASHCHANGE,
|
|
194
|
+
EVENTTYPES.POPSTATE,
|
|
195
|
+
EVENTTYPES.BEFOREUNLOAD
|
|
196
|
+
])
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export { initPv, handleSendPageView }
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { record } from 'rrweb'
|
|
2
|
+
import pako from 'pako'
|
|
3
|
+
import { Base64 } from 'js-base64'
|
|
4
|
+
import { RecordEventScope } from '../types'
|
|
5
|
+
import { getTimestamp } from '../utils'
|
|
6
|
+
import { options } from './options'
|
|
7
|
+
import { watch } from '../observer'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 只存储最近30s的所有录屏 (分为3段)
|
|
11
|
+
* 第一段:0-10
|
|
12
|
+
* 第二段:10-20
|
|
13
|
+
* 第三段:20-30
|
|
14
|
+
*
|
|
15
|
+
* 当结束 30-40 的录屏时会把 0-10 录屏推出,并将此次录屏放入
|
|
16
|
+
* 数据结构: [
|
|
17
|
+
* {scope: '1684826667798-1684826669998', eventList: [...]},
|
|
18
|
+
* {scope: '1684826679998-1684826689998', eventList: [...]},
|
|
19
|
+
* {scope: '1684826699998-1684826799998', eventList: [...]},
|
|
20
|
+
* ]
|
|
21
|
+
*
|
|
22
|
+
* 举例:当第34秒发生错误时,此时的录屏数组是这样 [
|
|
23
|
+
* {scope: '0-10', eventList: [...]},
|
|
24
|
+
* {scope: '10-20', eventList: [...]},
|
|
25
|
+
* {scope: '20-30', eventList: [...]},
|
|
26
|
+
* {scope: '30-40', eventList: [...]},
|
|
27
|
+
* ]
|
|
28
|
+
*
|
|
29
|
+
* 此时30-40的 eventList 还在不断push中,但在错误发生时,我们可以把存住此时的录屏数组
|
|
30
|
+
* 然后直接去数组的最后一位的 eventList + 最后第二位的 eventList 拼接(会导致视频时长不固定,但会在10-20,如果想缩小范围,可更改 MAXSCOPETIME)
|
|
31
|
+
* 注意:如果拼接发现 eventList 长度为0或者很少,很大可能是用户没有手动操作且系统自动报错
|
|
32
|
+
*
|
|
33
|
+
* 而真正的录屏数组在填满 30-40 的 eventList 时则删除数组第一位数据
|
|
34
|
+
*
|
|
35
|
+
* 真实效果:
|
|
36
|
+
* MAXSCOPETIME 我这边设置为 5s,所以最终录制的时长为 5s-10s
|
|
37
|
+
* 但是会有几个特殊场景导致录屏时间过短:
|
|
38
|
+
* 1. 因为录制的插件只会在用户操作网页时运作,当用户停止操作页面或者页面处于休眠状态则会停止记录
|
|
39
|
+
* 直到用户重新操作页面,所以会出现用户停止操作1分钟后2s后触发了一个错误,此时sdk只会记录这个2s的操作
|
|
40
|
+
* 2. 另外就是程序刚进来的报错也会导致录屏时间小于5s
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
const MAXSCOPETIME = 5000 // 每5s记录一个区间
|
|
44
|
+
const MAXSCOPELENGTH = 3 // 录屏数组最长长度 - 不要小于3
|
|
45
|
+
|
|
46
|
+
let recordScreen: RecordScreen | undefined
|
|
47
|
+
|
|
48
|
+
export class RecordScreen {
|
|
49
|
+
public eventList: RecordEventScope[] = [
|
|
50
|
+
{ scope: `${getTimestamp()}-`, eventList: [] }
|
|
51
|
+
]
|
|
52
|
+
private closeCallback: ReturnType<typeof record>
|
|
53
|
+
|
|
54
|
+
constructor() {
|
|
55
|
+
this.init()
|
|
56
|
+
}
|
|
57
|
+
private init() {
|
|
58
|
+
this.closeCallback = record({
|
|
59
|
+
emit: (event, isCheckout) => {
|
|
60
|
+
const lastEvents = this.eventList[this.eventList.length - 1]
|
|
61
|
+
lastEvents.eventList.push(event)
|
|
62
|
+
if (isCheckout) {
|
|
63
|
+
if (this.eventList.length > 0) {
|
|
64
|
+
this.eventList[this.eventList.length - 1].scope =
|
|
65
|
+
lastEvents.scope + getTimestamp()
|
|
66
|
+
}
|
|
67
|
+
if (this.eventList.length > MAXSCOPELENGTH) {
|
|
68
|
+
this.eventList.shift()
|
|
69
|
+
}
|
|
70
|
+
this.eventList.push({ scope: `${getTimestamp()}-`, eventList: [] })
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
recordCanvas: true,
|
|
74
|
+
checkoutEveryNms: MAXSCOPETIME // 每5s重新制作快照
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
public close() {
|
|
78
|
+
this.closeCallback?.()
|
|
79
|
+
this.closeCallback = undefined
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function initRecordScreen() {
|
|
84
|
+
watch(options, (newValue, oldValue) => {
|
|
85
|
+
if (newValue.recordScreen === oldValue.recordScreen) return
|
|
86
|
+
|
|
87
|
+
if (newValue.recordScreen) recordScreen = new RecordScreen()
|
|
88
|
+
else {
|
|
89
|
+
recordScreen?.close()
|
|
90
|
+
recordScreen = undefined
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
recordScreen = options.value.recordScreen ? new RecordScreen() : undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 获取录屏数据
|
|
98
|
+
export function getEventList() {
|
|
99
|
+
return recordScreen?.eventList ?? []
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 压缩
|
|
104
|
+
* @param data 压缩源
|
|
105
|
+
*/
|
|
106
|
+
export function zip(data: any): string {
|
|
107
|
+
if (!data) return data
|
|
108
|
+
|
|
109
|
+
// 判断数据是否需要转为JSON
|
|
110
|
+
const dataJson =
|
|
111
|
+
typeof data !== 'string' && typeof data !== 'number'
|
|
112
|
+
? JSON.stringify(data)
|
|
113
|
+
: data
|
|
114
|
+
|
|
115
|
+
// 使用Base64.encode处理字符编码,兼容中文
|
|
116
|
+
const str = Base64.encode(dataJson as string)
|
|
117
|
+
const binaryString = pako.gzip(str)
|
|
118
|
+
const arr = Array.from(binaryString)
|
|
119
|
+
let s = ''
|
|
120
|
+
arr.forEach((item: number) => {
|
|
121
|
+
s += String.fromCharCode(item)
|
|
122
|
+
})
|
|
123
|
+
return Base64.btoa(s)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 解压
|
|
128
|
+
* @param b64Data 解压源
|
|
129
|
+
*/
|
|
130
|
+
export function unzip(b64Data: string) {
|
|
131
|
+
const strData = Base64.atob(b64Data)
|
|
132
|
+
const charData = strData.split('').map(function (x) {
|
|
133
|
+
return x.charCodeAt(0)
|
|
134
|
+
})
|
|
135
|
+
const binData = new Uint8Array(charData)
|
|
136
|
+
const data: any = pako.ungzip(binData)
|
|
137
|
+
// ↓切片处理数据,防止内存溢出报错↓
|
|
138
|
+
let str = ''
|
|
139
|
+
const chunk = 8 * 1024
|
|
140
|
+
let i
|
|
141
|
+
for (i = 0; i < data.length / chunk; i++) {
|
|
142
|
+
str += String.fromCharCode.apply(
|
|
143
|
+
null,
|
|
144
|
+
data.slice(i * chunk, (i + 1) * chunk)
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
str += String.fromCharCode.apply(null, data.slice(i * chunk))
|
|
148
|
+
// ↑切片处理数据,防止内存溢出报错↑
|
|
149
|
+
const unzipStr = Base64.decode(str)
|
|
150
|
+
let result = ''
|
|
151
|
+
// 对象或数组进行JSON转换
|
|
152
|
+
try {
|
|
153
|
+
result = JSON.parse(unzipStr)
|
|
154
|
+
} catch (error: any) {
|
|
155
|
+
if (/Unexpected token o in JSON at position 0/.test(error)) {
|
|
156
|
+
// 如果没有转换成功,代表值为基本数据,直接赋值
|
|
157
|
+
result = unzipStr
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return result
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 卸载录屏监听
|
|
165
|
+
*/
|
|
166
|
+
export function destroyRecordScreen() {
|
|
167
|
+
recordScreen?.close()
|
|
168
|
+
recordScreen = undefined
|
|
169
|
+
}
|