sunda-tracker 1.0.8
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/.babelrc +23 -0
- package/.npmrc.exclude +1 -0
- package/.prettierrc.json +9 -0
- package/.yarnrc.exclude +1 -0
- package/README.md +263 -0
- package/dist/sunda-tracker.es.js +806 -0
- package/dist/sunda-tracker.umd.js +5 -0
- package/eslint.config.js +25 -0
- package/package.json +30 -0
- package/src/core/ApiSender.js +185 -0
- package/src/core/Device.js +291 -0
- package/src/core/ElasticsearchSender.js +215 -0
- package/src/core/Queue.js +21 -0
- package/src/core/Storage.js +34 -0
- package/src/core/Track.js +53 -0
- package/src/index.js +45 -0
- package/src/plugins/AutoTrack.js +47 -0
- package/src/plugins/Performance.js +189 -0
- package/src/utils/constants.js +21 -0
- package/src/utils/helpers.js +216 -0
- package/vite.config.js +33 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { moment } from '@/utils/helpers'
|
|
2
|
+
|
|
3
|
+
import { DEFAULT_ES_OPTIONS } from '@/utils/constants'
|
|
4
|
+
|
|
5
|
+
export default class ElasticsearchSender {
|
|
6
|
+
/**
|
|
7
|
+
* @param {Object} config 配置项
|
|
8
|
+
* @param {string} config.node ES服务器地址
|
|
9
|
+
* @param {string} config.index 索引名称
|
|
10
|
+
* @param {string} [config.username] 用户名
|
|
11
|
+
* @param {string} [config.password] 密码
|
|
12
|
+
* @param {number} [config.batchSize=100] 批量发送大小
|
|
13
|
+
* @param {number} [config.flushInterval=5000] 自动刷新间隔(ms)
|
|
14
|
+
*/
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.config = {
|
|
17
|
+
batchSize: 100,
|
|
18
|
+
flushInterval: 5000,
|
|
19
|
+
...DEFAULT_ES_OPTIONS,
|
|
20
|
+
...config
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this.queue = []
|
|
24
|
+
this.headers = this.buildHeaders()
|
|
25
|
+
this.timer = null
|
|
26
|
+
this.startAutoFlush()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
buildHeaders() {
|
|
30
|
+
const headers = {
|
|
31
|
+
'Content-Type': 'application/x-ndjson',
|
|
32
|
+
Accept: 'application/json'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const auth = this.config.encoded
|
|
36
|
+
headers['Authorization'] = `ApiKey ${auth}`
|
|
37
|
+
|
|
38
|
+
return headers
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 发送单条数据
|
|
43
|
+
* @param {Object} data
|
|
44
|
+
*/
|
|
45
|
+
async send(data) {
|
|
46
|
+
try {
|
|
47
|
+
const docWithTimestamp = {
|
|
48
|
+
...data,
|
|
49
|
+
'@timestamp': new Date().toISOString()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.queue.push(docWithTimestamp)
|
|
53
|
+
|
|
54
|
+
if (this.queue.length >= this.config.batchSize) {
|
|
55
|
+
await this.flush()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return true
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Error sending data to Elasticsearch:', error)
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 批量发送数据
|
|
67
|
+
* @param {Array} dataArray 数据数组
|
|
68
|
+
*/
|
|
69
|
+
async sendBulk(dataArray) {
|
|
70
|
+
try {
|
|
71
|
+
const timestamp = new Date().toISOString()
|
|
72
|
+
const docsWithTimestamp = dataArray.map((data) => ({
|
|
73
|
+
...data,
|
|
74
|
+
'@timestamp': timestamp
|
|
75
|
+
}))
|
|
76
|
+
|
|
77
|
+
this.queue.push(...docsWithTimestamp)
|
|
78
|
+
|
|
79
|
+
if (this.queue.length >= this.config.batchSize) {
|
|
80
|
+
await this.flush()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return true
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error('Error sending bulk data to Elasticsearch:', error)
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 构建批量请求体
|
|
92
|
+
* @param {Array} documents 文档数组
|
|
93
|
+
* @returns {string} NDJSON格式的请求体
|
|
94
|
+
*/
|
|
95
|
+
buildBulkRequestBody(documents) {
|
|
96
|
+
return (
|
|
97
|
+
documents
|
|
98
|
+
.flatMap((doc) => [
|
|
99
|
+
JSON.stringify({
|
|
100
|
+
index: {
|
|
101
|
+
_index: `${this.config.index}-${moment().format('YYYY.MM.DD')}`
|
|
102
|
+
}
|
|
103
|
+
}),
|
|
104
|
+
JSON.stringify(doc)
|
|
105
|
+
])
|
|
106
|
+
.join('\n') + '\n'
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async flush() {
|
|
111
|
+
if (this.queue.length === 0) return
|
|
112
|
+
|
|
113
|
+
const documents = [...this.queue]
|
|
114
|
+
this.queue = []
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const body = this.buildBulkRequestBody(documents)
|
|
118
|
+
const response = await fetch(
|
|
119
|
+
`${this.config.node}/${this.config.index}-${moment().format(
|
|
120
|
+
'YYYY.MM.DD'
|
|
121
|
+
)}/_bulk`,
|
|
122
|
+
{
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: this.headers,
|
|
125
|
+
body: body
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
throw new Error(`HTTP error! status: ${response.status}`)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const result = await response.json()
|
|
134
|
+
|
|
135
|
+
if (result.errors) {
|
|
136
|
+
const failedItems = result.items.filter(
|
|
137
|
+
(item) => item.index.status >= 400
|
|
138
|
+
)
|
|
139
|
+
if (failedItems.length > 0) {
|
|
140
|
+
console.error('Some documents failed to index:', failedItems)
|
|
141
|
+
this.handleFailedDocuments(failedItems, documents)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return true
|
|
146
|
+
} catch (error) {
|
|
147
|
+
this.stopAutoFlush()
|
|
148
|
+
console.error('Error flushing data to Elasticsearch:', error)
|
|
149
|
+
this.queue.unshift(...documents)
|
|
150
|
+
return false
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
handleFailedDocuments(failedItems, originalDocuments) {
|
|
155
|
+
failedItems.forEach((item) => {
|
|
156
|
+
const failedDoc = originalDocuments[item.index._id]
|
|
157
|
+
this.storeFailedDocument(failedDoc)
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
storeFailedDocument(document) {
|
|
162
|
+
try {
|
|
163
|
+
const failedDocs = JSON.parse(
|
|
164
|
+
localStorage.getItem('es_failed_docs') || '[]'
|
|
165
|
+
)
|
|
166
|
+
failedDocs.push({
|
|
167
|
+
document,
|
|
168
|
+
timestamp: new Date().toISOString(),
|
|
169
|
+
retryCount: 0
|
|
170
|
+
})
|
|
171
|
+
localStorage.setItem('es_failed_docs', JSON.stringify(failedDocs))
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error('Error storing failed document:', error)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async retryFailedDocuments() {
|
|
178
|
+
try {
|
|
179
|
+
const failedDocs = JSON.parse(
|
|
180
|
+
localStorage.getItem('es_failed_docs') || '[]'
|
|
181
|
+
)
|
|
182
|
+
if (failedDocs.length === 0) return
|
|
183
|
+
|
|
184
|
+
const docsToRetry = failedDocs.filter((doc) => doc.retryCount < 3)
|
|
185
|
+
const remainingDocs = failedDocs.filter((doc) => doc.retryCount >= 3)
|
|
186
|
+
docsToRetry.forEach((doc) => doc.retryCount++)
|
|
187
|
+
|
|
188
|
+
await this.sendBulk(docsToRetry.map((doc) => doc.document))
|
|
189
|
+
localStorage.setItem('es_failed_docs', JSON.stringify(remainingDocs))
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error('Error retrying failed documents:', error)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
startAutoFlush() {
|
|
196
|
+
this.timer = setInterval(() => {
|
|
197
|
+
if (this.queue.length > 0) {
|
|
198
|
+
this.flush()
|
|
199
|
+
}
|
|
200
|
+
this.retryFailedDocuments()
|
|
201
|
+
}, this.config.flushInterval)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
stopAutoFlush() {
|
|
205
|
+
if (this.timer) {
|
|
206
|
+
clearInterval(this.timer)
|
|
207
|
+
this.timer = null
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
destroy() {
|
|
212
|
+
this.stopAutoFlush()
|
|
213
|
+
return this.flush()
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export default class Queue {
|
|
2
|
+
constructor(options) {
|
|
3
|
+
this.options = options
|
|
4
|
+
this.cache = []
|
|
5
|
+
this.retryQueue = new Map()
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async send(data) {
|
|
9
|
+
// TODO:
|
|
10
|
+
console.log('send track data to server ====>', data)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
addToRetryQueue(data) {
|
|
14
|
+
// TODO: 重试队列逻辑
|
|
15
|
+
console.log('retry ====>', data)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async processRetryQueue() {
|
|
19
|
+
// TODO: 处理重试队列
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export default class Storage {
|
|
2
|
+
constructor({ prefix = 'track_' }) {
|
|
3
|
+
this.prefix = prefix
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
setItem(key, value) {
|
|
7
|
+
localStorage.setItem(this.prefix + key, JSON.stringify(value))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
getItem(key) {
|
|
11
|
+
const value = localStorage.getItem(this.prefix + key)
|
|
12
|
+
return value ? JSON.parse(value) : null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
removeItem(key) {
|
|
16
|
+
localStorage.removeItem(this.prefix + key)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getSessionId() {
|
|
20
|
+
return this.getItem('session_id')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
setSessionId(sessionId) {
|
|
24
|
+
this.setItem('session_id', sessionId)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
setUser(userInfo) {
|
|
28
|
+
this.setItem('user', userInfo)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getUser() {
|
|
32
|
+
return this.getItem('user')
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import AutoTrack from '@/plugins/AutoTrack'
|
|
2
|
+
import Performance from '@/plugins/Performance'
|
|
3
|
+
import Device from './Device'
|
|
4
|
+
import ElasticsearchSender from './ElasticsearchSender'
|
|
5
|
+
import ApiSender from './ApiSender'
|
|
6
|
+
// import Queue from './Queue'
|
|
7
|
+
import Storage from './Storage'
|
|
8
|
+
|
|
9
|
+
export default class Track {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.options = options
|
|
12
|
+
// this.queue = new Queue(options)
|
|
13
|
+
this.storage = new Storage(options)
|
|
14
|
+
this.device = new Device(options)
|
|
15
|
+
this.sender =
|
|
16
|
+
options.type === 'es'
|
|
17
|
+
? new ElasticsearchSender(options)
|
|
18
|
+
: new ApiSender(options)
|
|
19
|
+
|
|
20
|
+
this.init()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
init() {
|
|
24
|
+
this.sessionId = this.storage.getSessionId() || this.device.generateUUID()
|
|
25
|
+
this.storage.setSessionId(this.sessionId)
|
|
26
|
+
|
|
27
|
+
this.deviceInfo = this.device.getInfo()
|
|
28
|
+
this.initPlugins()
|
|
29
|
+
// this.initListeners()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setUser(userInfo) {
|
|
33
|
+
this.storage.setUser(userInfo)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
initPlugins() {
|
|
37
|
+
if (this.options.autoTrack) {
|
|
38
|
+
this.autoTrack = new AutoTrack(this)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (this.options.enablePerformance) {
|
|
42
|
+
this.performance = new Performance(this)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
track(type, data) {
|
|
47
|
+
this.sender.send({
|
|
48
|
+
// type,
|
|
49
|
+
...data,
|
|
50
|
+
sessionId: this.sessionId
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import Track from '@/core/Track'
|
|
2
|
+
import { DEFAULT_OPTIONS } from '@/utils/constants'
|
|
3
|
+
|
|
4
|
+
const getInstance = (options = {}) => {
|
|
5
|
+
if (!window._trackInstance) {
|
|
6
|
+
window._trackInstance = new Track({
|
|
7
|
+
...DEFAULT_OPTIONS,
|
|
8
|
+
...options
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return window._trackInstance
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default getInstance
|
|
16
|
+
|
|
17
|
+
export const VueTracker = {
|
|
18
|
+
install: (app, options = {}) => {
|
|
19
|
+
const trackInstance = getInstance(options)
|
|
20
|
+
|
|
21
|
+
app.config.globalProperties.$track = {
|
|
22
|
+
track(eventName, eventData) {
|
|
23
|
+
return trackInstance.track(eventName, eventData)
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
setUser(userInfo) {
|
|
27
|
+
return trackInstance.setUser(userInfo)
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
clearUser() {
|
|
31
|
+
return trackInstance.clearUser()
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
trackDeviceInfo() {
|
|
35
|
+
return trackInstance.track('device', trackInstance.deviceInfo)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const TrackMixin = {
|
|
42
|
+
created() {
|
|
43
|
+
this.$trackInstance = this.$track
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { kebabToCamelCase } from '@/utils/helpers'
|
|
2
|
+
|
|
3
|
+
export default class AutoTrack {
|
|
4
|
+
constructor(track) {
|
|
5
|
+
this.track = track
|
|
6
|
+
this.init()
|
|
7
|
+
this.options = track.options
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
init() {
|
|
11
|
+
this.bindClickEvents()
|
|
12
|
+
this.observeRouteChanges()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
bindClickEvents() {
|
|
16
|
+
document.addEventListener('click', (event) => {
|
|
17
|
+
const target = event.target
|
|
18
|
+
const trackElement = target.closest('[data-track-type]')
|
|
19
|
+
|
|
20
|
+
if (trackElement) {
|
|
21
|
+
const trackType = trackElement.getAttribute('data-track-type')
|
|
22
|
+
|
|
23
|
+
const trackArgs = {}
|
|
24
|
+
for (const attr of trackElement.attributes) {
|
|
25
|
+
if (attr.name.startsWith('data-track-args-')) {
|
|
26
|
+
const key = kebabToCamelCase(
|
|
27
|
+
attr.name.replace('data-track-args-', '')
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
trackArgs[key] = attr.value
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (trackType) {
|
|
35
|
+
this.track.track(trackType, {
|
|
36
|
+
...trackArgs,
|
|
37
|
+
sessionId: this.track.sessionId
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
observeRouteChanges() {
|
|
45
|
+
// 路由变化追踪逻辑
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { throttle } from '@/utils/helpers'
|
|
2
|
+
|
|
3
|
+
export default class Performance {
|
|
4
|
+
constructor(track) {
|
|
5
|
+
this.track = track
|
|
6
|
+
this.metrics = {}
|
|
7
|
+
this.isSupported = this.checkSupport()
|
|
8
|
+
|
|
9
|
+
if (this.isSupported) {
|
|
10
|
+
this.init()
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
checkSupport() {
|
|
15
|
+
return (
|
|
16
|
+
typeof window !== 'undefined' &&
|
|
17
|
+
window.performance &&
|
|
18
|
+
window.performance.timing &&
|
|
19
|
+
window.performance.getEntriesByType
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
init() {
|
|
24
|
+
this.observePageLoad()
|
|
25
|
+
this.observeResources()
|
|
26
|
+
this.observeLongTasks()
|
|
27
|
+
this.observePaint()
|
|
28
|
+
this.observeErrors()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
observePageLoad() {
|
|
32
|
+
window.addEventListener('load', () => {
|
|
33
|
+
setTimeout(() => {
|
|
34
|
+
const timing = performance.getEntriesByType('navigation')[0]
|
|
35
|
+
const metrics = {
|
|
36
|
+
dnsTime: timing.domainLookupEnd - timing.domainLookupStart,
|
|
37
|
+
tcpTime: timing.connectEnd - timing.connectStart,
|
|
38
|
+
sslTime:
|
|
39
|
+
timing.secureConnectionStart > 0 ?
|
|
40
|
+
timing.connectEnd - timing.secureConnectionStart :
|
|
41
|
+
0,
|
|
42
|
+
ttfb: timing.responseStart - timing.requestStart,
|
|
43
|
+
responseTime: timing.responseEnd - timing.responseStart,
|
|
44
|
+
domParseTime: timing.domInteractive - timing.responseEnd,
|
|
45
|
+
domReadyTime: timing.domContentLoadedEventEnd - timing.startTime,
|
|
46
|
+
loadTime: timing.loadEventEnd - timing.startTime
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.track.track('page_performance', metrics)
|
|
50
|
+
}, 0)
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
observeResources() {
|
|
55
|
+
const observer = new PerformanceObserver(
|
|
56
|
+
throttle((list) => {
|
|
57
|
+
const entries = list.getEntries()
|
|
58
|
+
entries.forEach((entry) => {
|
|
59
|
+
if (
|
|
60
|
+
entry.initiatorType !== 'fetch' &&
|
|
61
|
+
entry.initiatorType !== 'xmlhttprequest'
|
|
62
|
+
) {
|
|
63
|
+
this.track.track('resource_performance', {
|
|
64
|
+
name: entry.name,
|
|
65
|
+
type: entry.initiatorType,
|
|
66
|
+
duration: entry.duration,
|
|
67
|
+
size: entry.transferSize,
|
|
68
|
+
protocol: entry.nextHopProtocol
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
}, 3000)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
observer.observe({ entryTypes: ['resource'] })
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
observeLongTasks() {
|
|
79
|
+
if (typeof PerformanceLongTaskTiming !== 'undefined') {
|
|
80
|
+
const observer = new PerformanceObserver((list) => {
|
|
81
|
+
list.getEntries().forEach((entry) => {
|
|
82
|
+
this.track.track('long_task', {
|
|
83
|
+
duration: entry.duration,
|
|
84
|
+
startTime: entry.startTime,
|
|
85
|
+
name: entry.name
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
observer.observe({ entryTypes: ['longtask'] })
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
observePaint() {
|
|
95
|
+
const observer = new PerformanceObserver((list) => {
|
|
96
|
+
const entries = list.getEntries()
|
|
97
|
+
entries.forEach((entry) => {
|
|
98
|
+
this.track.track('paint_timing', {
|
|
99
|
+
name: entry.name,
|
|
100
|
+
startTime: entry.startTime
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
observer.observe({ entryTypes: ['paint'] })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
observeErrors() {
|
|
109
|
+
window.addEventListener(
|
|
110
|
+
'error',
|
|
111
|
+
(event) => {
|
|
112
|
+
this.track.track('js_error', {
|
|
113
|
+
message: event.message,
|
|
114
|
+
filename: event.filename,
|
|
115
|
+
lineno: event.lineno,
|
|
116
|
+
colno: event.colno,
|
|
117
|
+
error: event.error ? event.error.stack : null
|
|
118
|
+
})
|
|
119
|
+
},
|
|
120
|
+
true
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
124
|
+
this.track.track('promise_error', {
|
|
125
|
+
message: event.reason.message || event.reason,
|
|
126
|
+
stack: event.reason.stack
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getFirstScreenTime() {
|
|
132
|
+
return new Promise((resolve) => {
|
|
133
|
+
if (document.readyState === 'complete') {
|
|
134
|
+
this.calculateFirstScreen(resolve)
|
|
135
|
+
} else {
|
|
136
|
+
window.addEventListener('load', () => {
|
|
137
|
+
this.calculateFirstScreen(resolve)
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
calculateFirstScreen(callback) {
|
|
144
|
+
const images = document.querySelectorAll('img')
|
|
145
|
+
let maxTime = performance.timing.domContentLoadedEventEnd
|
|
146
|
+
|
|
147
|
+
if (images.length === 0) {
|
|
148
|
+
callback(maxTime)
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let completed = 0
|
|
153
|
+
const imageLoaded = () => {
|
|
154
|
+
completed++
|
|
155
|
+
if (completed === images.length) {
|
|
156
|
+
callback(maxTime)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
images.forEach((img) => {
|
|
161
|
+
if (img.complete) {
|
|
162
|
+
imageLoaded()
|
|
163
|
+
} else {
|
|
164
|
+
img.addEventListener('load', () => {
|
|
165
|
+
const loadTime = performance.now()
|
|
166
|
+
maxTime = Math.max(maxTime, loadTime)
|
|
167
|
+
imageLoaded()
|
|
168
|
+
})
|
|
169
|
+
img.addEventListener('error', imageLoaded)
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
getCurrentMetrics() {
|
|
175
|
+
if (!this.isSupported) return null
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
...this.metrics,
|
|
179
|
+
memory: performance.memory ?
|
|
180
|
+
{
|
|
181
|
+
usedJSHeapSize: performance.memory.usedJSHeapSize,
|
|
182
|
+
totalJSHeapSize: performance.memory.totalJSHeapSize
|
|
183
|
+
} :
|
|
184
|
+
null,
|
|
185
|
+
navigation: performance.getEntriesByType('navigation')[0],
|
|
186
|
+
timing: performance.timing
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const DEFAULT_OPTIONS = {
|
|
2
|
+
serverUrl: '',
|
|
3
|
+
appId: '',
|
|
4
|
+
cacheLimit: 10,
|
|
5
|
+
autoTrack: false,
|
|
6
|
+
enablePerformance: false,
|
|
7
|
+
debug: false,
|
|
8
|
+
maxRetryTimes: 3,
|
|
9
|
+
timeout: 5000,
|
|
10
|
+
type: 'es', // es, api
|
|
11
|
+
collectUrl: '',
|
|
12
|
+
collectMethod: 'post'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_ES_OPTIONS = {
|
|
16
|
+
// node: import.meta.env.VITE_APP_ES_NODE,
|
|
17
|
+
// id: 'SEGwE48BDXHCyTKWTBXE',
|
|
18
|
+
// index: import.meta.env.VITE_APP_ES_INDEX_NAME,
|
|
19
|
+
// api_key: '3HLDi5PYTfymTnAj7MmG-w',
|
|
20
|
+
// encoded: import.meta.env.VITE_APP_ES_ENCODED
|
|
21
|
+
}
|