@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.
@@ -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
+ }