@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,240 @@
|
|
|
1
|
+
export class ApiWatchPlugin {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.name = 'api-watch'
|
|
4
|
+
this.tracker = null
|
|
5
|
+
this._interceptorAdded = false
|
|
6
|
+
// 后台切换计数:请求期间计数变化说明 app 被挂起过,duration 含后台时间
|
|
7
|
+
this._bgCount = 0
|
|
8
|
+
this._totalHiddenTime = 0
|
|
9
|
+
this._lastHideTime = 0
|
|
10
|
+
this._bgDebounceTimer = null
|
|
11
|
+
this._visibilityHandler = null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
install(tracker) {
|
|
15
|
+
this.tracker = tracker
|
|
16
|
+
this._setupBgWatch()
|
|
17
|
+
this._setupInterceptor()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 记录切后台次数和累计后台时长,用于修正 duration
|
|
21
|
+
_setupBgWatch() {
|
|
22
|
+
if (typeof document === 'undefined') return
|
|
23
|
+
var self = this
|
|
24
|
+
this._visibilityHandler = function () {
|
|
25
|
+
if (document.visibilityState === 'hidden') {
|
|
26
|
+
var hideTime = Date.now()
|
|
27
|
+
if (self._bgDebounceTimer) clearTimeout(self._bgDebounceTimer)
|
|
28
|
+
self._bgDebounceTimer = setTimeout(function () {
|
|
29
|
+
self._bgDebounceTimer = null
|
|
30
|
+
self._bgCount++
|
|
31
|
+
self._lastHideTime = hideTime
|
|
32
|
+
}, 500)
|
|
33
|
+
} else {
|
|
34
|
+
if (self._bgDebounceTimer) {
|
|
35
|
+
clearTimeout(self._bgDebounceTimer)
|
|
36
|
+
self._bgDebounceTimer = null
|
|
37
|
+
} else if (self._lastHideTime) {
|
|
38
|
+
self._totalHiddenTime += Date.now() - self._lastHideTime
|
|
39
|
+
self._lastHideTime = 0
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
document.addEventListener('visibilitychange', this._visibilityHandler)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// [PROJECT-ADAPT] API 拦截方式
|
|
47
|
+
// 当前使用 uni.addInterceptor(uni-app 运行时专属)
|
|
48
|
+
// 非 uni-app 项目需替换为 XMLHttpRequest/fetch 拦截
|
|
49
|
+
_setupInterceptor() {
|
|
50
|
+
var self = this
|
|
51
|
+
var config = this.tracker.getConfig()
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
uni.addInterceptor('request', {
|
|
55
|
+
invoke: function (args) {
|
|
56
|
+
// 给请求打上开始时间标记
|
|
57
|
+
args._trackStartTime = Date.now()
|
|
58
|
+
args._trackBgCount = self._bgCount
|
|
59
|
+
args._trackHiddenTimeStart = self._totalHiddenTime + (self._lastHideTime ? Date.now() - self._lastHideTime : 0)
|
|
60
|
+
return args
|
|
61
|
+
},
|
|
62
|
+
success: function (res, args) {
|
|
63
|
+
self._reportApi(args, res, null)
|
|
64
|
+
},
|
|
65
|
+
fail: function (err, args) {
|
|
66
|
+
self._reportApi(args, null, err)
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
this._interceptorAdded = true
|
|
70
|
+
} catch (e) {
|
|
71
|
+
if (config.debug) {
|
|
72
|
+
console.warn('[TrackerSDK] api-watch: failed to add interceptor', e)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_reportApi(args, res, err) {
|
|
78
|
+
if (!args || !args.url) return
|
|
79
|
+
|
|
80
|
+
var config = this.tracker.getConfig()
|
|
81
|
+
|
|
82
|
+
// [PROJECT-ADAPT] 自身请求过滤列表
|
|
83
|
+
if (
|
|
84
|
+
args.url.indexOf('/api/point/task/activeReport') >= 0 ||
|
|
85
|
+
args.url.indexOf(config.batchReportUrl) >= 0 ||
|
|
86
|
+
args.url.indexOf('/api/track/collect') >= 0
|
|
87
|
+
) {
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
var duration = args._trackStartTime
|
|
92
|
+
? Date.now() - args._trackStartTime
|
|
93
|
+
: 0
|
|
94
|
+
|
|
95
|
+
var data = {
|
|
96
|
+
url: args.url,
|
|
97
|
+
method: (args.method || 'GET').toUpperCase(),
|
|
98
|
+
duration: duration,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 请求期间发生过切后台 → 修正 duration 减去后台挂起时间
|
|
102
|
+
if (args._trackBgCount !== undefined && this._bgCount !== args._trackBgCount) {
|
|
103
|
+
data.bgSuspended = true
|
|
104
|
+
var hiddenNow = this._totalHiddenTime + (this._lastHideTime ? Date.now() - this._lastHideTime : 0)
|
|
105
|
+
var hiddenDuring = hiddenNow - (args._trackHiddenTimeStart || 0)
|
|
106
|
+
if (hiddenDuring > 0) {
|
|
107
|
+
data.rawDuration = duration
|
|
108
|
+
data.duration = Math.max(0, duration - hiddenDuring)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 请求参数
|
|
113
|
+
if (args.data) {
|
|
114
|
+
try {
|
|
115
|
+
data.requestParams = typeof args.data === 'string' ? args.data : JSON.stringify(args.data)
|
|
116
|
+
} catch (e) {}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 请求头(过滤敏感字段)
|
|
120
|
+
if (args.header) {
|
|
121
|
+
data.requestHeaders = this._filterHeaders(args.header)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (res) {
|
|
125
|
+
data.statusCode = res.statusCode || 0
|
|
126
|
+
// 响应头
|
|
127
|
+
if (res.header) {
|
|
128
|
+
data.responseHeaders = this._filterHeaders(res.header)
|
|
129
|
+
}
|
|
130
|
+
// 响应数据(过滤图片/base64/超长响应,避免污染轨迹表)
|
|
131
|
+
if (res.data) {
|
|
132
|
+
var rd = this._extractResponseData(res)
|
|
133
|
+
if (rd !== undefined) data.responseData = rd
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (err) {
|
|
138
|
+
data.statusCode = 0
|
|
139
|
+
data.error = err.errMsg || String(err)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this.tracker.track('api_request', {
|
|
143
|
+
eventName: data.url,
|
|
144
|
+
extraData: data,
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 提取响应数据用于上报,剔除对分析无价值且体积巨大的内容:
|
|
150
|
+
* - 图片/音视频/二进制响应(content-type 判断)→ 只留占位
|
|
151
|
+
* - 含 base64 内联数据(海报二维码等)→ 只留字节数占位
|
|
152
|
+
* - 超长文本 → 截断并标注(避免留半截 JSON 被误当完整数据)
|
|
153
|
+
* @returns {string|undefined} undefined 表示无可用响应数据(不写字段)
|
|
154
|
+
*/
|
|
155
|
+
_extractResponseData(res) {
|
|
156
|
+
// content-type:非文本类直接省略
|
|
157
|
+
var ct = ''
|
|
158
|
+
if (res.header) {
|
|
159
|
+
for (var k in res.header) {
|
|
160
|
+
if (res.header.hasOwnProperty(k) && k.toLowerCase() === 'content-type') {
|
|
161
|
+
ct = String(res.header[k]).toLowerCase()
|
|
162
|
+
break
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (ct && (ct.indexOf('image/') >= 0 || ct.indexOf('audio/') >= 0 ||
|
|
167
|
+
ct.indexOf('video/') >= 0 || ct.indexOf('octet-stream') >= 0)) {
|
|
168
|
+
return '[非文本响应:' + ct + ',已省略]'
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
var str
|
|
172
|
+
try {
|
|
173
|
+
str = typeof res.data === 'string' ? res.data : JSON.stringify(res.data)
|
|
174
|
+
} catch (e) {
|
|
175
|
+
return undefined
|
|
176
|
+
}
|
|
177
|
+
if (!str) return undefined
|
|
178
|
+
|
|
179
|
+
// 含 base64 内联数据(二维码/海报图片等)→ 省略,避免污染轨迹表
|
|
180
|
+
if (str.indexOf(';base64,') >= 0 || str.indexOf('data:image') >= 0) {
|
|
181
|
+
return '[含base64内联数据,已省略 ' + str.length + ' 字节]'
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
var MAX = 1024
|
|
185
|
+
if (str.length > MAX) {
|
|
186
|
+
return str.substring(0, MAX) + '...[已截断,共' + str.length + '字节]'
|
|
187
|
+
}
|
|
188
|
+
return str
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
_filterHeaders(headers) {
|
|
192
|
+
if (!headers || typeof headers !== 'object') return null
|
|
193
|
+
var filtered = {}
|
|
194
|
+
var sensitiveKeys = ['authorization', 'token', 'rsb-token', 'cookie', 'set-cookie']
|
|
195
|
+
for (var key in headers) {
|
|
196
|
+
if (!headers.hasOwnProperty(key)) continue
|
|
197
|
+
if (sensitiveKeys.indexOf(key.toLowerCase()) >= 0) {
|
|
198
|
+
filtered[key] = this._encryptValue(headers[key])
|
|
199
|
+
} else {
|
|
200
|
+
filtered[key] = headers[key]
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return filtered
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
_encryptValue(value) {
|
|
207
|
+
if (!value) return ''
|
|
208
|
+
try {
|
|
209
|
+
var encoded = encodeURIComponent(value)
|
|
210
|
+
var result = ''
|
|
211
|
+
for (var i = 0; i < encoded.length; i++) {
|
|
212
|
+
result += String.fromCharCode(encoded.charCodeAt(i) ^ 0x5A)
|
|
213
|
+
}
|
|
214
|
+
var full = btoa(result)
|
|
215
|
+
if (full.length > 16) {
|
|
216
|
+
return 'ENC:' + full.substring(0, 16) + '...'
|
|
217
|
+
}
|
|
218
|
+
return 'ENC:' + full
|
|
219
|
+
} catch (e) {
|
|
220
|
+
return ''
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
destroy() {
|
|
225
|
+
if (this._bgDebounceTimer) {
|
|
226
|
+
clearTimeout(this._bgDebounceTimer)
|
|
227
|
+
this._bgDebounceTimer = null
|
|
228
|
+
}
|
|
229
|
+
if (this._interceptorAdded) {
|
|
230
|
+
try {
|
|
231
|
+
uni.removeInterceptor('request')
|
|
232
|
+
} catch (e) {}
|
|
233
|
+
}
|
|
234
|
+
if (this._visibilityHandler && typeof document !== 'undefined') {
|
|
235
|
+
document.removeEventListener('visibilitychange', this._visibilityHandler)
|
|
236
|
+
}
|
|
237
|
+
this._visibilityHandler = null
|
|
238
|
+
this.tracker = null
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export class AppLifecyclePlugin {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.name = 'app-lifecycle'
|
|
4
|
+
this.tracker = null
|
|
5
|
+
this._visibilityHandler = null
|
|
6
|
+
this._lastState = 'foreground'
|
|
7
|
+
this._debounceTimer = null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
install(tracker) {
|
|
11
|
+
this.tracker = tracker
|
|
12
|
+
var adapter = tracker.getAdapter()
|
|
13
|
+
var platform = adapter.getPlatformName()
|
|
14
|
+
var self = this
|
|
15
|
+
|
|
16
|
+
if (platform === 'web' || platform === 'app') {
|
|
17
|
+
this._setupWebLifecycle()
|
|
18
|
+
}
|
|
19
|
+
// 小程序的 App.onShow/onHide 通过 mixin 在 index.js 中注册处理
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
_setupWebLifecycle() {
|
|
23
|
+
if (typeof document === 'undefined') return
|
|
24
|
+
|
|
25
|
+
var self = this
|
|
26
|
+
this._visibilityHandler = function () {
|
|
27
|
+
var state = document.visibilityState
|
|
28
|
+
if (state === 'hidden') {
|
|
29
|
+
// 延迟 500ms 上报,系统通知/来电横幅等瞬时隐藏会被后续 visible 取消
|
|
30
|
+
if (self._debounceTimer) {
|
|
31
|
+
clearTimeout(self._debounceTimer)
|
|
32
|
+
}
|
|
33
|
+
self._debounceTimer = setTimeout(function () {
|
|
34
|
+
self._debounceTimer = null
|
|
35
|
+
if (self._lastState !== 'background') {
|
|
36
|
+
self._lastState = 'background'
|
|
37
|
+
self.tracker.track('app_lifecycle', {
|
|
38
|
+
eventName: 'background',
|
|
39
|
+
})
|
|
40
|
+
self.tracker.getQueue().flush()
|
|
41
|
+
}
|
|
42
|
+
}, 500)
|
|
43
|
+
} else if (state === 'visible') {
|
|
44
|
+
if (self._debounceTimer) {
|
|
45
|
+
clearTimeout(self._debounceTimer)
|
|
46
|
+
self._debounceTimer = null
|
|
47
|
+
}
|
|
48
|
+
if (self._lastState !== 'foreground') {
|
|
49
|
+
self._lastState = 'foreground'
|
|
50
|
+
self.tracker.track('app_lifecycle', {
|
|
51
|
+
eventName: 'foreground',
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
document.addEventListener('visibilitychange', this._visibilityHandler)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 供小程序端 mixin 调用
|
|
60
|
+
trackAppShow() {
|
|
61
|
+
if (this._lastState !== 'foreground') {
|
|
62
|
+
this._lastState = 'foreground'
|
|
63
|
+
this.tracker.track('app_lifecycle', {
|
|
64
|
+
eventName: 'foreground',
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
trackAppHide() {
|
|
70
|
+
if (this._lastState !== 'background') {
|
|
71
|
+
this._lastState = 'background'
|
|
72
|
+
this.tracker.track('app_lifecycle', {
|
|
73
|
+
eventName: 'background',
|
|
74
|
+
})
|
|
75
|
+
this.tracker.getQueue().flush()
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
destroy() {
|
|
80
|
+
if (this._debounceTimer) {
|
|
81
|
+
clearTimeout(this._debounceTimer)
|
|
82
|
+
this._debounceTimer = null
|
|
83
|
+
}
|
|
84
|
+
if (this._visibilityHandler && typeof document !== 'undefined') {
|
|
85
|
+
document.removeEventListener('visibilitychange', this._visibilityHandler)
|
|
86
|
+
}
|
|
87
|
+
this._visibilityHandler = null
|
|
88
|
+
this.tracker = null
|
|
89
|
+
}
|
|
90
|
+
}
|