@rk911/tracker-sdk 1.6.4
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/core/compat.js +344 -0
- package/core/config.js +132 -0
- package/core/queue.js +461 -0
- package/core/session.js +128 -0
- package/core/tracker.js +287 -0
- package/customize/index.js +92 -0
- package/customize/modules/common.js +43 -0
- package/customize/modules/store-manager.js +56 -0
- package/index.js +403 -0
- package/package.json +21 -0
- package/platforms/app.js +36 -0
- package/platforms/weapp.js +218 -0
- package/platforms/web.js +483 -0
- package/plugins/api-watch.js +240 -0
- package/plugins/app-lifecycle.js +90 -0
- package/plugins/click.js +557 -0
- package/plugins/exposure.js +376 -0
- package/plugins/js-error.js +232 -0
- package/plugins/page-stay.js +44 -0
- package/plugins/page-view.js +172 -0
- package/plugins/performance.js +157 -0
- package/plugins/scroll-depth.js +256 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 元素曝光插件(增强版)
|
|
3
|
+
*
|
|
4
|
+
* 能力:
|
|
5
|
+
* 1. v-track-exposure 指令 — 走 SDK 自动采集链路
|
|
6
|
+
* 2. 停留时长验证 — 元素必须完全可见超过 validExposureTime 才算有效曝光
|
|
7
|
+
* 3. 退出比例判定 — 可见比例低于 exitRatio 时取消计时
|
|
8
|
+
* 4. MutationObserver — 动态 DOM 元素自动挂载观察
|
|
9
|
+
* 5. v-impression 兼容 — 在 compat.js 中单独处理,与本插件并存
|
|
10
|
+
*
|
|
11
|
+
* 配置(通过 tracker config.exposure 传入):
|
|
12
|
+
* validExposureTime: number — 有效曝光最小停留时长(ms),默认 1000
|
|
13
|
+
* exitRatio: number — 低于此比例判定为离开视口,默认 0.7
|
|
14
|
+
* threshold: number — 普通模式的可见比例阈值,默认 0.5
|
|
15
|
+
* useDuration: boolean — 是否启用停留时长验证模式,默认 false
|
|
16
|
+
* observeDynamicElements: boolean — 是否监控动态 DOM,默认 true
|
|
17
|
+
*/
|
|
18
|
+
export class ExposurePlugin {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.name = 'exposure'
|
|
21
|
+
this.tracker = null
|
|
22
|
+
this._observers = null
|
|
23
|
+
this._reported = null
|
|
24
|
+
this._entryTimers = null
|
|
25
|
+
this._intersectionObserver = null
|
|
26
|
+
this._mutationObserver = null
|
|
27
|
+
this._config = null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
install(tracker) {
|
|
31
|
+
this.tracker = tracker
|
|
32
|
+
var adapter = tracker.getAdapter()
|
|
33
|
+
|
|
34
|
+
if (!adapter.supportsIntersectionObserver()) return
|
|
35
|
+
|
|
36
|
+
this._observers = new WeakMap()
|
|
37
|
+
this._reported = new WeakSet()
|
|
38
|
+
this._entryTimers = new WeakMap()
|
|
39
|
+
|
|
40
|
+
// 读取曝光配置
|
|
41
|
+
var trackerConfig = tracker.getConfig()
|
|
42
|
+
this._config = {
|
|
43
|
+
validExposureTime: 1000,
|
|
44
|
+
exitRatio: 0.7,
|
|
45
|
+
threshold: 0.5,
|
|
46
|
+
useDuration: false,
|
|
47
|
+
observeDynamicElements: true,
|
|
48
|
+
...(trackerConfig.exposure || {}),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 在 H5 端自动监控 data-exposure 属性元素
|
|
52
|
+
if (adapter.supportsDOM()) {
|
|
53
|
+
this._initAutoExposure()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 自动监控页面上带 data-exposure / data-scroll-exposure 属性的元素
|
|
59
|
+
*/
|
|
60
|
+
_initAutoExposure() {
|
|
61
|
+
var self = this
|
|
62
|
+
var config = this._config
|
|
63
|
+
var tracker = this.tracker
|
|
64
|
+
var adapter = tracker.getAdapter()
|
|
65
|
+
|
|
66
|
+
// 计算 IntersectionObserver 阈值
|
|
67
|
+
var thresholds
|
|
68
|
+
if (config.useDuration) {
|
|
69
|
+
thresholds = [0, config.exitRatio, 1.0]
|
|
70
|
+
} else {
|
|
71
|
+
thresholds = [0, config.threshold]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this._intersectionObserver = new IntersectionObserver(
|
|
75
|
+
function (entries) {
|
|
76
|
+
entries.forEach(function (entry) {
|
|
77
|
+
var el = entry.target
|
|
78
|
+
if (self._reported.has(el)) return
|
|
79
|
+
|
|
80
|
+
if (config.useDuration) {
|
|
81
|
+
self._handleDurationExposure(el, entry)
|
|
82
|
+
} else {
|
|
83
|
+
self._handleSimpleExposure(el, entry)
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
},
|
|
87
|
+
{ root: null, rootMargin: '0px', threshold: thresholds }
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
// 观察现有元素
|
|
91
|
+
this._observeExistingElements()
|
|
92
|
+
|
|
93
|
+
// MutationObserver 监控动态元素
|
|
94
|
+
if (config.observeDynamicElements && typeof MutationObserver !== 'undefined') {
|
|
95
|
+
this._initMutationObserver()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 普通模式:可见比例超过阈值即上报
|
|
101
|
+
*/
|
|
102
|
+
_handleSimpleExposure(el, entry) {
|
|
103
|
+
if (
|
|
104
|
+
entry.isIntersecting &&
|
|
105
|
+
entry.intersectionRatio >= this._config.threshold
|
|
106
|
+
) {
|
|
107
|
+
this._reportExposure(el, entry.intersectionRatio)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 停留时长验证模式:元素 100% 可见 + 持续 N 毫秒才算有效曝光
|
|
113
|
+
* 移植自 smart-tracker 的 _handleDurationBasedExposure
|
|
114
|
+
*/
|
|
115
|
+
_handleDurationExposure(el, entry) {
|
|
116
|
+
var self = this
|
|
117
|
+
var config = this._config
|
|
118
|
+
|
|
119
|
+
if (entry.intersectionRatio >= 1.0) {
|
|
120
|
+
// 元素完全进入视口 — 开始计时
|
|
121
|
+
if (this._entryTimers.has(el)) return
|
|
122
|
+
|
|
123
|
+
var timerId = setTimeout(function () {
|
|
124
|
+
if (self._reported.has(el)) {
|
|
125
|
+
self._entryTimers.delete(el)
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
self._reportExposure(el, 1.0)
|
|
129
|
+
self._entryTimers.delete(el)
|
|
130
|
+
}, config.validExposureTime)
|
|
131
|
+
|
|
132
|
+
this._entryTimers.set(el, timerId)
|
|
133
|
+
} else if (entry.intersectionRatio < config.exitRatio) {
|
|
134
|
+
// 可见比例低于退出阈值 — 取消计时
|
|
135
|
+
var timerId = this._entryTimers.get(el)
|
|
136
|
+
if (timerId) {
|
|
137
|
+
clearTimeout(timerId)
|
|
138
|
+
this._entryTimers.delete(el)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 上报曝光事件
|
|
145
|
+
*/
|
|
146
|
+
_reportExposure(el, visibleRatio) {
|
|
147
|
+
this._reported.add(el)
|
|
148
|
+
|
|
149
|
+
// 尝试 unobserve
|
|
150
|
+
if (this._intersectionObserver) {
|
|
151
|
+
this._intersectionObserver.unobserve(el)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
var adapter = this.tracker.getAdapter()
|
|
155
|
+
var trackData = {
|
|
156
|
+
trackId: (el.dataset && el.dataset.trackId) || undefined,
|
|
157
|
+
trackName:
|
|
158
|
+
(el.dataset && el.dataset.trackName) ||
|
|
159
|
+
(el.dataset && el.dataset.exposure) ||
|
|
160
|
+
undefined,
|
|
161
|
+
text: (el.innerText || '').trim().substring(0, 50),
|
|
162
|
+
xpath: adapter.getXPath(el),
|
|
163
|
+
visibleRatio: visibleRatio,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 如果元素上有自定义数据
|
|
167
|
+
if (el._trackExposureData) {
|
|
168
|
+
trackData.customData = el._trackExposureData
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.tracker.track('exposure', trackData)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 观察页面上已有的 data-exposure 元素
|
|
176
|
+
*/
|
|
177
|
+
_observeExistingElements() {
|
|
178
|
+
if (!this._intersectionObserver || typeof document === 'undefined') return
|
|
179
|
+
|
|
180
|
+
var elements = document.querySelectorAll(
|
|
181
|
+
'[data-exposure], [data-scroll-exposure]'
|
|
182
|
+
)
|
|
183
|
+
var self = this
|
|
184
|
+
elements.forEach(function (el) {
|
|
185
|
+
self._intersectionObserver.observe(el)
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* MutationObserver:监控动态添加的带曝光属性的元素
|
|
191
|
+
* 移植自 smart-tracker 的 _observeDOMChanges
|
|
192
|
+
*/
|
|
193
|
+
_initMutationObserver() {
|
|
194
|
+
if (typeof document === 'undefined' || !document.body) return
|
|
195
|
+
|
|
196
|
+
var self = this
|
|
197
|
+
this._mutationObserver = new MutationObserver(function (mutations) {
|
|
198
|
+
mutations.forEach(function (mutation) {
|
|
199
|
+
mutation.addedNodes.forEach(function (node) {
|
|
200
|
+
if (node.nodeType !== 1) return // 只处理元素节点
|
|
201
|
+
|
|
202
|
+
// 检查节点本身
|
|
203
|
+
if (
|
|
204
|
+
node.hasAttribute &&
|
|
205
|
+
(node.hasAttribute('data-exposure') ||
|
|
206
|
+
node.hasAttribute('data-scroll-exposure'))
|
|
207
|
+
) {
|
|
208
|
+
if (!self._reported.has(node)) {
|
|
209
|
+
self._intersectionObserver.observe(node)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 检查子元素
|
|
214
|
+
if (node.querySelectorAll) {
|
|
215
|
+
var children = node.querySelectorAll(
|
|
216
|
+
'[data-exposure], [data-scroll-exposure]'
|
|
217
|
+
)
|
|
218
|
+
children.forEach(function (child) {
|
|
219
|
+
if (!self._reported.has(child)) {
|
|
220
|
+
self._intersectionObserver.observe(child)
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
this._mutationObserver.observe(document.body, {
|
|
229
|
+
childList: true,
|
|
230
|
+
subtree: true,
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 返回 v-track-exposure 指令定义(Vue 2 格式)
|
|
236
|
+
* 用法:v-track-exposure="{ name: '商品卡片', id: goodsId }"
|
|
237
|
+
*/
|
|
238
|
+
createDirective() {
|
|
239
|
+
var self = this
|
|
240
|
+
var observers = this._observers
|
|
241
|
+
var reported = this._reported
|
|
242
|
+
var entryTimers = this._entryTimers
|
|
243
|
+
var tracker = this.tracker
|
|
244
|
+
var config = this._config
|
|
245
|
+
|
|
246
|
+
if (!observers || !reported || !tracker) return null
|
|
247
|
+
|
|
248
|
+
function handleIntersection(entries, observer) {
|
|
249
|
+
entries.forEach(function (entry) {
|
|
250
|
+
var el = entry.target
|
|
251
|
+
if (reported.has(el)) return
|
|
252
|
+
|
|
253
|
+
if (config && config.useDuration) {
|
|
254
|
+
// 停留时长验证模式
|
|
255
|
+
if (entry.intersectionRatio >= 1.0) {
|
|
256
|
+
if (entryTimers.has(el)) return
|
|
257
|
+
var timerId = setTimeout(function () {
|
|
258
|
+
if (reported.has(el)) {
|
|
259
|
+
entryTimers.delete(el)
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
reported.add(el)
|
|
263
|
+
observer.unobserve(el)
|
|
264
|
+
|
|
265
|
+
var data = el._trackExposureData || {}
|
|
266
|
+
var adapter = tracker.getAdapter()
|
|
267
|
+
tracker.track('exposure', {
|
|
268
|
+
trackId:
|
|
269
|
+
(el.dataset && el.dataset.trackId) || data.id || undefined,
|
|
270
|
+
trackName:
|
|
271
|
+
(el.dataset && el.dataset.trackName) ||
|
|
272
|
+
data.name ||
|
|
273
|
+
undefined,
|
|
274
|
+
text: (el.innerText || '').trim().substring(0, 50),
|
|
275
|
+
xpath: adapter.getXPath(el),
|
|
276
|
+
visibleRatio: 1.0,
|
|
277
|
+
viewDuration: config.validExposureTime,
|
|
278
|
+
customData: data,
|
|
279
|
+
})
|
|
280
|
+
entryTimers.delete(el)
|
|
281
|
+
}, config.validExposureTime)
|
|
282
|
+
entryTimers.set(el, timerId)
|
|
283
|
+
} else if (entry.intersectionRatio < config.exitRatio) {
|
|
284
|
+
var tid = entryTimers.get(el)
|
|
285
|
+
if (tid) {
|
|
286
|
+
clearTimeout(tid)
|
|
287
|
+
entryTimers.delete(el)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
// 普通模式
|
|
292
|
+
var threshold = (config && config.threshold) || 0.5
|
|
293
|
+
if (entry.isIntersecting && entry.intersectionRatio >= threshold) {
|
|
294
|
+
reported.add(el)
|
|
295
|
+
observer.unobserve(el)
|
|
296
|
+
|
|
297
|
+
var data = el._trackExposureData || {}
|
|
298
|
+
var adapter = tracker.getAdapter()
|
|
299
|
+
tracker.track('exposure', {
|
|
300
|
+
trackId:
|
|
301
|
+
(el.dataset && el.dataset.trackId) || data.id || undefined,
|
|
302
|
+
trackName:
|
|
303
|
+
(el.dataset && el.dataset.trackName) ||
|
|
304
|
+
data.name ||
|
|
305
|
+
undefined,
|
|
306
|
+
text: (el.innerText || '').trim().substring(0, 50),
|
|
307
|
+
xpath: adapter.getXPath(el),
|
|
308
|
+
visibleRatio: entry.intersectionRatio,
|
|
309
|
+
customData: data,
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 计算阈值
|
|
317
|
+
var thresholds
|
|
318
|
+
if (config && config.useDuration) {
|
|
319
|
+
thresholds = [0, config.exitRatio || 0.7, 1.0]
|
|
320
|
+
} else {
|
|
321
|
+
thresholds = [(config && config.threshold) || 0.5]
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
bind: function (el, binding) {
|
|
326
|
+
el._trackExposureData = binding.value || {}
|
|
327
|
+
|
|
328
|
+
var observer = new IntersectionObserver(handleIntersection, {
|
|
329
|
+
root: null,
|
|
330
|
+
rootMargin: '0px',
|
|
331
|
+
threshold: thresholds,
|
|
332
|
+
})
|
|
333
|
+
observer.observe(el)
|
|
334
|
+
observers.set(el, observer)
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
update: function (el, binding) {
|
|
338
|
+
if (binding.value) {
|
|
339
|
+
el._trackExposureData = binding.value
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
unbind: function (el) {
|
|
344
|
+
var observer = observers.get(el)
|
|
345
|
+
if (observer) {
|
|
346
|
+
observer.unobserve(el)
|
|
347
|
+
observers.delete(el)
|
|
348
|
+
}
|
|
349
|
+
reported.delete(el)
|
|
350
|
+
// 清除可能存在的计时器
|
|
351
|
+
var tid = entryTimers.get(el)
|
|
352
|
+
if (tid) {
|
|
353
|
+
clearTimeout(tid)
|
|
354
|
+
entryTimers.delete(el)
|
|
355
|
+
}
|
|
356
|
+
delete el._trackExposureData
|
|
357
|
+
},
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
destroy() {
|
|
362
|
+
if (this._intersectionObserver) {
|
|
363
|
+
this._intersectionObserver.disconnect()
|
|
364
|
+
this._intersectionObserver = null
|
|
365
|
+
}
|
|
366
|
+
if (this._mutationObserver) {
|
|
367
|
+
this._mutationObserver.disconnect()
|
|
368
|
+
this._mutationObserver = null
|
|
369
|
+
}
|
|
370
|
+
// 清除所有待执行的计时器
|
|
371
|
+
this._entryTimers = null
|
|
372
|
+
this._observers = null
|
|
373
|
+
this._reported = null
|
|
374
|
+
this.tracker = null
|
|
375
|
+
}
|
|
376
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
export class JsErrorPlugin {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.name = 'js-error'
|
|
4
|
+
this.tracker = null
|
|
5
|
+
this._errorHandler = null
|
|
6
|
+
this._rejectionHandler = null
|
|
7
|
+
this._recentErrors = {}
|
|
8
|
+
this._errorCount = 0
|
|
9
|
+
this._maxErrorsPerSession = 50
|
|
10
|
+
this._dedupWindowMs = 10000
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
install(tracker) {
|
|
14
|
+
this.tracker = tracker
|
|
15
|
+
var adapter = tracker.getAdapter()
|
|
16
|
+
var platform = adapter.getPlatformName()
|
|
17
|
+
var self = this
|
|
18
|
+
|
|
19
|
+
if (platform === 'web' || platform === 'app') {
|
|
20
|
+
this._errorHandler = function (event) {
|
|
21
|
+
if (event.target && event.target !== window && event.target.tagName) {
|
|
22
|
+
self._handleResourceError(event)
|
|
23
|
+
} else {
|
|
24
|
+
self._handleError(event)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
this._rejectionHandler = function (event) {
|
|
28
|
+
self._handleRejection(event)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
adapter.addErrorListener(this._errorHandler)
|
|
32
|
+
adapter.addUnhandledRejectionListener(this._rejectionHandler)
|
|
33
|
+
} else if (platform === 'weapp') {
|
|
34
|
+
this._errorHandler = function (errMsg) {
|
|
35
|
+
self._handleWeappError(errMsg)
|
|
36
|
+
}
|
|
37
|
+
adapter.addErrorListener(this._errorHandler)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Vue.config.errorHandler 调用入口
|
|
43
|
+
* 由 index.js install() 中注册,捕获 Vue 组件 render/lifecycle/watch 中的错误
|
|
44
|
+
*/
|
|
45
|
+
handleVueError(err, vm, info) {
|
|
46
|
+
if (!this.tracker || !err) return
|
|
47
|
+
|
|
48
|
+
var message = err.message || String(err)
|
|
49
|
+
var stack = err.stack ? err.stack.substring(0, 1000) : ''
|
|
50
|
+
var componentName = ''
|
|
51
|
+
|
|
52
|
+
if (vm) {
|
|
53
|
+
var options = vm.$options || {}
|
|
54
|
+
componentName = options.name || options.__file || (options._componentTag) || ''
|
|
55
|
+
if (!componentName && vm.$parent) {
|
|
56
|
+
componentName = '(anonymous component)'
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
var fingerprint = 'vue|' + message + '|' + componentName + '|' + (info || '')
|
|
61
|
+
|
|
62
|
+
if (this._isDuplicate(fingerprint)) return
|
|
63
|
+
if (!this._shouldReport()) return
|
|
64
|
+
|
|
65
|
+
this.tracker.track('js_error', {
|
|
66
|
+
eventName: 'vue_error',
|
|
67
|
+
errorMessage: message,
|
|
68
|
+
errorStack: stack,
|
|
69
|
+
extraData: {
|
|
70
|
+
type: 'vue',
|
|
71
|
+
componentName: componentName,
|
|
72
|
+
lifecycleHook: info || '',
|
|
73
|
+
route: this._getCurrentRoute(),
|
|
74
|
+
referrer: this._getReferrer(),
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_isDuplicate(fingerprint) {
|
|
80
|
+
var now = Date.now()
|
|
81
|
+
var last = this._recentErrors[fingerprint]
|
|
82
|
+
if (last && now - last < this._dedupWindowMs) return true
|
|
83
|
+
this._recentErrors[fingerprint] = now
|
|
84
|
+
|
|
85
|
+
var keys = Object.keys(this._recentErrors)
|
|
86
|
+
if (keys.length > 100) {
|
|
87
|
+
var cutoff = now - this._dedupWindowMs
|
|
88
|
+
for (var i = 0; i < keys.length; i++) {
|
|
89
|
+
if (this._recentErrors[keys[i]] < cutoff) {
|
|
90
|
+
delete this._recentErrors[keys[i]]
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
_shouldReport() {
|
|
98
|
+
if (this._errorCount >= this._maxErrorsPerSession) return false
|
|
99
|
+
this._errorCount++
|
|
100
|
+
return true
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
_getCurrentRoute() {
|
|
104
|
+
try {
|
|
105
|
+
return this.tracker._currentPage || this.tracker.getAdapter().getCurrentPagePath() || ''
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return ''
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_getReferrer() {
|
|
112
|
+
try {
|
|
113
|
+
return this.tracker._referrer || ''
|
|
114
|
+
} catch (e) {
|
|
115
|
+
return ''
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
_handleError(event) {
|
|
120
|
+
if (!this.tracker || !event) return
|
|
121
|
+
|
|
122
|
+
var message = event.message || String(event)
|
|
123
|
+
var stack = (event.error && event.error.stack) ? event.error.stack.substring(0, 1000) : ''
|
|
124
|
+
|
|
125
|
+
// 跨域脚本报错:浏览器出于安全屏蔽详情,message 固定为 "Script error."
|
|
126
|
+
// 且 filename/lineno/stack 全空,无任何定位价值 → 丢弃,避免污染错误统计
|
|
127
|
+
if (/^script error\.?$/i.test(message) && !event.filename && !stack) {
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
var fingerprint = message + '|' + (event.filename || '') + '|' + (event.lineno || 0)
|
|
132
|
+
|
|
133
|
+
if (this._isDuplicate(fingerprint)) return
|
|
134
|
+
if (!this._shouldReport()) return
|
|
135
|
+
|
|
136
|
+
this.tracker.track('js_error', {
|
|
137
|
+
eventName: 'runtime_error',
|
|
138
|
+
errorMessage: message,
|
|
139
|
+
errorStack: stack,
|
|
140
|
+
extraData: {
|
|
141
|
+
type: 'runtime',
|
|
142
|
+
filename: event.filename || '',
|
|
143
|
+
lineno: event.lineno || 0,
|
|
144
|
+
colno: event.colno || 0,
|
|
145
|
+
route: this._getCurrentRoute(),
|
|
146
|
+
referrer: this._getReferrer(),
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
_handleRejection(event) {
|
|
152
|
+
if (!this.tracker || !event) return
|
|
153
|
+
|
|
154
|
+
var reason = event.reason || event
|
|
155
|
+
var message = reason ? (reason.message || String(reason)) : 'Unknown rejection'
|
|
156
|
+
var stack = (reason && reason.stack) ? reason.stack.substring(0, 1000) : ''
|
|
157
|
+
var fingerprint = 'rejection|' + message
|
|
158
|
+
|
|
159
|
+
if (this._isDuplicate(fingerprint)) return
|
|
160
|
+
if (!this._shouldReport()) return
|
|
161
|
+
|
|
162
|
+
this.tracker.track('js_error', {
|
|
163
|
+
eventName: 'unhandled_rejection',
|
|
164
|
+
errorMessage: message,
|
|
165
|
+
errorStack: stack,
|
|
166
|
+
extraData: {
|
|
167
|
+
type: 'unhandledRejection',
|
|
168
|
+
route: this._getCurrentRoute(),
|
|
169
|
+
referrer: this._getReferrer(),
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
_handleResourceError(event) {
|
|
175
|
+
if (!this.tracker || !event || !event.target) return
|
|
176
|
+
|
|
177
|
+
var el = event.target
|
|
178
|
+
var tagName = (el.tagName || '').toLowerCase()
|
|
179
|
+
var resourceUrl = el.src || el.href || ''
|
|
180
|
+
|
|
181
|
+
if (!resourceUrl) return
|
|
182
|
+
|
|
183
|
+
var fingerprint = 'resource|' + tagName + '|' + resourceUrl
|
|
184
|
+
if (this._isDuplicate(fingerprint)) return
|
|
185
|
+
if (!this._shouldReport()) return
|
|
186
|
+
|
|
187
|
+
this.tracker.track('js_error', {
|
|
188
|
+
eventName: 'resource_error',
|
|
189
|
+
errorMessage: tagName + ' load failed: ' + resourceUrl,
|
|
190
|
+
extraData: {
|
|
191
|
+
type: 'resource',
|
|
192
|
+
tagName: tagName,
|
|
193
|
+
resourceUrl: resourceUrl,
|
|
194
|
+
route: this._getCurrentRoute(),
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
_handleWeappError(errMsg) {
|
|
200
|
+
if (!this.tracker) return
|
|
201
|
+
|
|
202
|
+
var message = typeof errMsg === 'string' ? errMsg.substring(0, 1000) : String(errMsg)
|
|
203
|
+
var fingerprint = 'weapp|' + message
|
|
204
|
+
|
|
205
|
+
if (this._isDuplicate(fingerprint)) return
|
|
206
|
+
if (!this._shouldReport()) return
|
|
207
|
+
|
|
208
|
+
this.tracker.track('js_error', {
|
|
209
|
+
eventName: 'weapp_error',
|
|
210
|
+
errorMessage: message,
|
|
211
|
+
extraData: {
|
|
212
|
+
type: 'weapp',
|
|
213
|
+
route: this._getCurrentRoute(),
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
destroy() {
|
|
219
|
+
if (typeof window !== 'undefined') {
|
|
220
|
+
if (this._errorHandler) {
|
|
221
|
+
window.removeEventListener('error', this._errorHandler, true)
|
|
222
|
+
}
|
|
223
|
+
if (this._rejectionHandler) {
|
|
224
|
+
window.removeEventListener('unhandledrejection', this._rejectionHandler)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
this._errorHandler = null
|
|
228
|
+
this._rejectionHandler = null
|
|
229
|
+
this.tracker = null
|
|
230
|
+
this._recentErrors = {}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 页面停留时长插件
|
|
3
|
+
* 作为独立插件提供额外的停留追踪能力
|
|
4
|
+
* 注意:核心的页面停留 mixin 已在 compat.js 的 createPageStayMixin 中实现
|
|
5
|
+
* 本插件提供的是 H5 端基于 visibilitychange 的补充监听
|
|
6
|
+
*/
|
|
7
|
+
export class PageStayPlugin {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.name = 'page-stay'
|
|
10
|
+
this.tracker = null
|
|
11
|
+
this._beforeUnloadHandler = null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
install(tracker) {
|
|
15
|
+
this.tracker = tracker
|
|
16
|
+
var adapter = tracker.getAdapter()
|
|
17
|
+
|
|
18
|
+
if (adapter.supportsDOM()) {
|
|
19
|
+
this._setupVisibilityTracking()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
_setupVisibilityTracking() {
|
|
24
|
+
if (typeof document === 'undefined') return
|
|
25
|
+
|
|
26
|
+
var self = this
|
|
27
|
+
|
|
28
|
+
// beforeunload 确保关闭标签页时强制同步刷新
|
|
29
|
+
if (typeof window !== 'undefined') {
|
|
30
|
+
this._beforeUnloadHandler = function () {
|
|
31
|
+
self.tracker.getQueue().flushSync()
|
|
32
|
+
}
|
|
33
|
+
window.addEventListener('beforeunload', this._beforeUnloadHandler)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
destroy() {
|
|
38
|
+
if (this._beforeUnloadHandler && typeof window !== 'undefined') {
|
|
39
|
+
window.removeEventListener('beforeunload', this._beforeUnloadHandler)
|
|
40
|
+
}
|
|
41
|
+
this._beforeUnloadHandler = null
|
|
42
|
+
this.tracker = null
|
|
43
|
+
}
|
|
44
|
+
}
|