@rk911/tracker-sdk 1.7.17 → 1.7.18
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/index.js +554 -531
- package/native/ios/RKTrackerSDK.swift +127 -14
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,531 +1,554 @@
|
|
|
1
|
-
import { Tracker } from './core/tracker.js'
|
|
2
|
-
import {
|
|
3
|
-
createDadianProxy,
|
|
4
|
-
createImpressionDirective,
|
|
5
|
-
createPageStayMixin,
|
|
6
|
-
} from './core/compat.js'
|
|
7
|
-
import { WebAdapter } from './platforms/web.js'
|
|
8
|
-
import { WeappAdapter } from './platforms/weapp.js'
|
|
9
|
-
import { AppAdapter } from './platforms/app.js'
|
|
10
|
-
import { getProjectRules } from './customize/index.js'
|
|
11
|
-
|
|
12
|
-
// 插件注册表
|
|
13
|
-
import { PageViewPlugin } from './plugins/page-view.js'
|
|
14
|
-
import { ClickPlugin } from './plugins/click.js'
|
|
15
|
-
import { PageStayPlugin } from './plugins/page-stay.js'
|
|
16
|
-
import { ExposurePlugin } from './plugins/exposure.js'
|
|
17
|
-
import { JsErrorPlugin } from './plugins/js-error.js'
|
|
18
|
-
import { ApiWatchPlugin } from './plugins/api-watch.js'
|
|
19
|
-
import { AppLifecyclePlugin } from './plugins/app-lifecycle.js'
|
|
20
|
-
import { PerformancePlugin } from './plugins/performance.js'
|
|
21
|
-
import { ScrollDepthPlugin } from './plugins/scroll-depth.js'
|
|
22
|
-
|
|
23
|
-
const PLUGIN_MAP = {
|
|
24
|
-
'page-view': PageViewPlugin,
|
|
25
|
-
click: ClickPlugin,
|
|
26
|
-
'page-stay': PageStayPlugin,
|
|
27
|
-
exposure: ExposurePlugin,
|
|
28
|
-
'js-error': JsErrorPlugin,
|
|
29
|
-
performance: PerformancePlugin,
|
|
30
|
-
'scroll-depth': ScrollDepthPlugin,
|
|
31
|
-
'api-watch': ApiWatchPlugin,
|
|
32
|
-
'app-lifecycle': AppLifecyclePlugin,
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// 单例
|
|
36
|
-
let trackerInstance = null
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* 自动检测当前运行平台
|
|
40
|
-
*/
|
|
41
|
-
function detectPlatform() {
|
|
42
|
-
try {
|
|
43
|
-
if (
|
|
44
|
-
typeof wx !== 'undefined' &&
|
|
45
|
-
typeof wx.getSystemInfoSync === 'function' &&
|
|
46
|
-
typeof document === 'undefined'
|
|
47
|
-
) {
|
|
48
|
-
return 'weapp'
|
|
49
|
-
}
|
|
50
|
-
} catch (e) {}
|
|
51
|
-
|
|
52
|
-
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
|
53
|
-
return 'web'
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return 'web'
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* 创建平台适配器
|
|
61
|
-
*/
|
|
62
|
-
function createAdapter(platform, config) {
|
|
63
|
-
switch (platform) {
|
|
64
|
-
case 'weapp':
|
|
65
|
-
return new WeappAdapter(config)
|
|
66
|
-
case 'app':
|
|
67
|
-
return new AppAdapter(config)
|
|
68
|
-
default:
|
|
69
|
-
return new WebAdapter(config)
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* 创建 Tracker 实例
|
|
75
|
-
*/
|
|
76
|
-
export function createTracker(config) {
|
|
77
|
-
if (trackerInstance) {
|
|
78
|
-
trackerInstance.destroy()
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
var platform = config.platform || detectPlatform()
|
|
82
|
-
config.platform = platform
|
|
83
|
-
|
|
84
|
-
// 自动加载项目定制规则(未显式传入时按 appId 匹配)
|
|
85
|
-
if (!config.projectRules && config.appId) {
|
|
86
|
-
config.projectRules = getProjectRules(config.appId)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
var adapter = createAdapter(platform, config)
|
|
90
|
-
trackerInstance = new Tracker(config, adapter)
|
|
91
|
-
|
|
92
|
-
// 触发 onBeforeInit 钩子(必须在插件注册与首屏事件触发前执行,以便同步/异步注入 Native 上下文)
|
|
93
|
-
if (typeof config.onBeforeInit === 'function') {
|
|
94
|
-
try {
|
|
95
|
-
config.onBeforeInit(trackerInstance)
|
|
96
|
-
} catch (e) {
|
|
97
|
-
console.warn('[TrackerSDK] onBeforeInit error:', e)
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// 注册插件
|
|
102
|
-
var pluginNames = config.plugins || []
|
|
103
|
-
pluginNames.forEach(function (pluginName) {
|
|
104
|
-
var PluginClass = null
|
|
105
|
-
|
|
106
|
-
if (typeof pluginName === 'string') {
|
|
107
|
-
PluginClass = PLUGIN_MAP[pluginName]
|
|
108
|
-
} else if (typeof pluginName === 'function') {
|
|
109
|
-
PluginClass = pluginName
|
|
110
|
-
} else if (pluginName && typeof pluginName.install === 'function') {
|
|
111
|
-
// 直接传入插件实例
|
|
112
|
-
trackerInstance.use(pluginName)
|
|
113
|
-
return
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (PluginClass) {
|
|
117
|
-
trackerInstance.use(new PluginClass())
|
|
118
|
-
}
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
return trackerInstance
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* 获取当前 Tracker 单例
|
|
126
|
-
*/
|
|
127
|
-
export function getTracker() {
|
|
128
|
-
return trackerInstance
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* 设备注册 — 无 deviceId 时调用后端接口获取,失败重试最多 3 次
|
|
133
|
-
*/
|
|
134
|
-
function registerDeviceIfNeeded(tracker) {
|
|
135
|
-
var adapter = tracker.getAdapter()
|
|
136
|
-
var config = tracker.getConfig()
|
|
137
|
-
var session = tracker.getSession()
|
|
138
|
-
|
|
139
|
-
// 如果是由 App/Native 动态注入的临时 DeviceID,跳过 H5 本地注册逻辑
|
|
140
|
-
if (session.isNativeDeviceId && session.getDeviceId()) {
|
|
141
|
-
return
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
var existingId = session.getDeviceId() || adapter.getStorage('deviceId') || adapter.getStorage('_tracker_device_id')
|
|
145
|
-
if (existingId) {
|
|
146
|
-
tracker.setDeviceId(existingId)
|
|
147
|
-
return
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// [PROJECT-ADAPT] appId 默认值
|
|
151
|
-
// 店长端 appId=1, apph5 appId=12/13, 小程序 appId=18
|
|
152
|
-
// 各项目在初始化时传入自己的 appId,此处 ||1 为兜底
|
|
153
|
-
var deviceInfo = { appId: Number(config.appId) || 1, id: '' }
|
|
154
|
-
if (typeof adapter.getDetailedDeviceInfo === 'function') {
|
|
155
|
-
Object.assign(deviceInfo, adapter.getDetailedDeviceInfo())
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// [PROJECT-ADAPT] 设备注册请求头
|
|
159
|
-
// 当前网关要求 appId/deviceId 作为自定义 header
|
|
160
|
-
var header = { 'content-type': 'application/json', appId: String(config.appId), deviceId: '' }
|
|
161
|
-
if (typeof config.getAuthHeaders === 'function') {
|
|
162
|
-
Object.assign(header, config.getAuthHeaders() || {})
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
var registerUrl = config.dadianUrl + (config.deviceRegister.url || '/api/point/device/add')
|
|
166
|
-
var maxRetries = 3
|
|
167
|
-
|
|
168
|
-
function attempt(retryCount) {
|
|
169
|
-
adapter
|
|
170
|
-
.request({ method: 'POST', url: registerUrl, data: deviceInfo, header: header })
|
|
171
|
-
.then(function (res) {
|
|
172
|
-
if (res && res.data && res.data.code === 0 && res.data.data) {
|
|
173
|
-
adapter.setStorage('deviceId', res.data.data)
|
|
174
|
-
tracker.setDeviceId(res.data.data)
|
|
175
|
-
if (config.debug) {
|
|
176
|
-
console.log('[TrackerSDK] device registered:', res.data.data)
|
|
177
|
-
}
|
|
178
|
-
} else if (retryCount < maxRetries) {
|
|
179
|
-
var delay = 1000 * Math.pow(2, retryCount)
|
|
180
|
-
setTimeout(function () { attempt(retryCount + 1) }, delay)
|
|
181
|
-
if (config.debug) {
|
|
182
|
-
console.warn('[TrackerSDK] device register bad response, retry ' + (retryCount + 1) + '/' + maxRetries + ' in ' + delay + 'ms')
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
})
|
|
186
|
-
.catch(function (err) {
|
|
187
|
-
if (retryCount < maxRetries) {
|
|
188
|
-
var delay = 1000 * Math.pow(2, retryCount)
|
|
189
|
-
setTimeout(function () { attempt(retryCount + 1) }, delay)
|
|
190
|
-
if (config.debug) {
|
|
191
|
-
console.warn('[TrackerSDK] device register failed, retry ' + (retryCount + 1) + '/' + maxRetries + ' in ' + delay + 'ms')
|
|
192
|
-
}
|
|
193
|
-
} else if (config.debug) {
|
|
194
|
-
console.warn('[TrackerSDK] device register failed after ' + maxRetries + ' retries:', err)
|
|
195
|
-
}
|
|
196
|
-
})
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
attempt(0)
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function _parseJwtPayload(token) {
|
|
203
|
-
if (!token || typeof token !== 'string' || token.indexOf('.') === -1) return null
|
|
204
|
-
try {
|
|
205
|
-
var tokenParts = token.split('.')
|
|
206
|
-
if (tokenParts.length >= 2) {
|
|
207
|
-
var base64Url = tokenParts[1]
|
|
208
|
-
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
|
|
209
|
-
var decodedStr = ''
|
|
210
|
-
if (typeof atob === 'function') {
|
|
211
|
-
try {
|
|
212
|
-
decodedStr = decodeURIComponent(escape(atob(base64)))
|
|
213
|
-
} catch (e1) {
|
|
214
|
-
decodedStr = atob(base64)
|
|
215
|
-
}
|
|
216
|
-
} else if (typeof Buffer !== 'undefined') {
|
|
217
|
-
decodedStr = Buffer.from(base64, 'base64').toString('utf-8')
|
|
218
|
-
}
|
|
219
|
-
if (decodedStr) {
|
|
220
|
-
return JSON.parse(decodedStr)
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
} catch (e) {}
|
|
224
|
-
return null
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* 自动检测 userId:多源交叉读取,并确保不把已有的合法身份置空
|
|
229
|
-
*/
|
|
230
|
-
function _ensureUserId(tracker) {
|
|
231
|
-
var session = tracker.getSession()
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
var
|
|
284
|
-
if (
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
)
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
tracker.
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
1
|
+
import { Tracker } from './core/tracker.js'
|
|
2
|
+
import {
|
|
3
|
+
createDadianProxy,
|
|
4
|
+
createImpressionDirective,
|
|
5
|
+
createPageStayMixin,
|
|
6
|
+
} from './core/compat.js'
|
|
7
|
+
import { WebAdapter } from './platforms/web.js'
|
|
8
|
+
import { WeappAdapter } from './platforms/weapp.js'
|
|
9
|
+
import { AppAdapter } from './platforms/app.js'
|
|
10
|
+
import { getProjectRules } from './customize/index.js'
|
|
11
|
+
|
|
12
|
+
// 插件注册表
|
|
13
|
+
import { PageViewPlugin } from './plugins/page-view.js'
|
|
14
|
+
import { ClickPlugin } from './plugins/click.js'
|
|
15
|
+
import { PageStayPlugin } from './plugins/page-stay.js'
|
|
16
|
+
import { ExposurePlugin } from './plugins/exposure.js'
|
|
17
|
+
import { JsErrorPlugin } from './plugins/js-error.js'
|
|
18
|
+
import { ApiWatchPlugin } from './plugins/api-watch.js'
|
|
19
|
+
import { AppLifecyclePlugin } from './plugins/app-lifecycle.js'
|
|
20
|
+
import { PerformancePlugin } from './plugins/performance.js'
|
|
21
|
+
import { ScrollDepthPlugin } from './plugins/scroll-depth.js'
|
|
22
|
+
|
|
23
|
+
const PLUGIN_MAP = {
|
|
24
|
+
'page-view': PageViewPlugin,
|
|
25
|
+
click: ClickPlugin,
|
|
26
|
+
'page-stay': PageStayPlugin,
|
|
27
|
+
exposure: ExposurePlugin,
|
|
28
|
+
'js-error': JsErrorPlugin,
|
|
29
|
+
performance: PerformancePlugin,
|
|
30
|
+
'scroll-depth': ScrollDepthPlugin,
|
|
31
|
+
'api-watch': ApiWatchPlugin,
|
|
32
|
+
'app-lifecycle': AppLifecyclePlugin,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 单例
|
|
36
|
+
let trackerInstance = null
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 自动检测当前运行平台
|
|
40
|
+
*/
|
|
41
|
+
function detectPlatform() {
|
|
42
|
+
try {
|
|
43
|
+
if (
|
|
44
|
+
typeof wx !== 'undefined' &&
|
|
45
|
+
typeof wx.getSystemInfoSync === 'function' &&
|
|
46
|
+
typeof document === 'undefined'
|
|
47
|
+
) {
|
|
48
|
+
return 'weapp'
|
|
49
|
+
}
|
|
50
|
+
} catch (e) {}
|
|
51
|
+
|
|
52
|
+
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
|
53
|
+
return 'web'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return 'web'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 创建平台适配器
|
|
61
|
+
*/
|
|
62
|
+
function createAdapter(platform, config) {
|
|
63
|
+
switch (platform) {
|
|
64
|
+
case 'weapp':
|
|
65
|
+
return new WeappAdapter(config)
|
|
66
|
+
case 'app':
|
|
67
|
+
return new AppAdapter(config)
|
|
68
|
+
default:
|
|
69
|
+
return new WebAdapter(config)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 创建 Tracker 实例
|
|
75
|
+
*/
|
|
76
|
+
export function createTracker(config) {
|
|
77
|
+
if (trackerInstance) {
|
|
78
|
+
trackerInstance.destroy()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
var platform = config.platform || detectPlatform()
|
|
82
|
+
config.platform = platform
|
|
83
|
+
|
|
84
|
+
// 自动加载项目定制规则(未显式传入时按 appId 匹配)
|
|
85
|
+
if (!config.projectRules && config.appId) {
|
|
86
|
+
config.projectRules = getProjectRules(config.appId)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
var adapter = createAdapter(platform, config)
|
|
90
|
+
trackerInstance = new Tracker(config, adapter)
|
|
91
|
+
|
|
92
|
+
// 触发 onBeforeInit 钩子(必须在插件注册与首屏事件触发前执行,以便同步/异步注入 Native 上下文)
|
|
93
|
+
if (typeof config.onBeforeInit === 'function') {
|
|
94
|
+
try {
|
|
95
|
+
config.onBeforeInit(trackerInstance)
|
|
96
|
+
} catch (e) {
|
|
97
|
+
console.warn('[TrackerSDK] onBeforeInit error:', e)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 注册插件
|
|
102
|
+
var pluginNames = config.plugins || []
|
|
103
|
+
pluginNames.forEach(function (pluginName) {
|
|
104
|
+
var PluginClass = null
|
|
105
|
+
|
|
106
|
+
if (typeof pluginName === 'string') {
|
|
107
|
+
PluginClass = PLUGIN_MAP[pluginName]
|
|
108
|
+
} else if (typeof pluginName === 'function') {
|
|
109
|
+
PluginClass = pluginName
|
|
110
|
+
} else if (pluginName && typeof pluginName.install === 'function') {
|
|
111
|
+
// 直接传入插件实例
|
|
112
|
+
trackerInstance.use(pluginName)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (PluginClass) {
|
|
117
|
+
trackerInstance.use(new PluginClass())
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
return trackerInstance
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 获取当前 Tracker 单例
|
|
126
|
+
*/
|
|
127
|
+
export function getTracker() {
|
|
128
|
+
return trackerInstance
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 设备注册 — 无 deviceId 时调用后端接口获取,失败重试最多 3 次
|
|
133
|
+
*/
|
|
134
|
+
function registerDeviceIfNeeded(tracker) {
|
|
135
|
+
var adapter = tracker.getAdapter()
|
|
136
|
+
var config = tracker.getConfig()
|
|
137
|
+
var session = tracker.getSession()
|
|
138
|
+
|
|
139
|
+
// 如果是由 App/Native 动态注入的临时 DeviceID,跳过 H5 本地注册逻辑
|
|
140
|
+
if (session.isNativeDeviceId && session.getDeviceId()) {
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
var existingId = session.getDeviceId() || adapter.getStorage('deviceId') || adapter.getStorage('_tracker_device_id')
|
|
145
|
+
if (existingId) {
|
|
146
|
+
tracker.setDeviceId(existingId)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// [PROJECT-ADAPT] appId 默认值
|
|
151
|
+
// 店长端 appId=1, apph5 appId=12/13, 小程序 appId=18
|
|
152
|
+
// 各项目在初始化时传入自己的 appId,此处 ||1 为兜底
|
|
153
|
+
var deviceInfo = { appId: Number(config.appId) || 1, id: '' }
|
|
154
|
+
if (typeof adapter.getDetailedDeviceInfo === 'function') {
|
|
155
|
+
Object.assign(deviceInfo, adapter.getDetailedDeviceInfo())
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// [PROJECT-ADAPT] 设备注册请求头
|
|
159
|
+
// 当前网关要求 appId/deviceId 作为自定义 header
|
|
160
|
+
var header = { 'content-type': 'application/json', appId: String(config.appId), deviceId: '' }
|
|
161
|
+
if (typeof config.getAuthHeaders === 'function') {
|
|
162
|
+
Object.assign(header, config.getAuthHeaders() || {})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
var registerUrl = config.dadianUrl + (config.deviceRegister.url || '/api/point/device/add')
|
|
166
|
+
var maxRetries = 3
|
|
167
|
+
|
|
168
|
+
function attempt(retryCount) {
|
|
169
|
+
adapter
|
|
170
|
+
.request({ method: 'POST', url: registerUrl, data: deviceInfo, header: header })
|
|
171
|
+
.then(function (res) {
|
|
172
|
+
if (res && res.data && res.data.code === 0 && res.data.data) {
|
|
173
|
+
adapter.setStorage('deviceId', res.data.data)
|
|
174
|
+
tracker.setDeviceId(res.data.data)
|
|
175
|
+
if (config.debug) {
|
|
176
|
+
console.log('[TrackerSDK] device registered:', res.data.data)
|
|
177
|
+
}
|
|
178
|
+
} else if (retryCount < maxRetries) {
|
|
179
|
+
var delay = 1000 * Math.pow(2, retryCount)
|
|
180
|
+
setTimeout(function () { attempt(retryCount + 1) }, delay)
|
|
181
|
+
if (config.debug) {
|
|
182
|
+
console.warn('[TrackerSDK] device register bad response, retry ' + (retryCount + 1) + '/' + maxRetries + ' in ' + delay + 'ms')
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
.catch(function (err) {
|
|
187
|
+
if (retryCount < maxRetries) {
|
|
188
|
+
var delay = 1000 * Math.pow(2, retryCount)
|
|
189
|
+
setTimeout(function () { attempt(retryCount + 1) }, delay)
|
|
190
|
+
if (config.debug) {
|
|
191
|
+
console.warn('[TrackerSDK] device register failed, retry ' + (retryCount + 1) + '/' + maxRetries + ' in ' + delay + 'ms')
|
|
192
|
+
}
|
|
193
|
+
} else if (config.debug) {
|
|
194
|
+
console.warn('[TrackerSDK] device register failed after ' + maxRetries + ' retries:', err)
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
attempt(0)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function _parseJwtPayload(token) {
|
|
203
|
+
if (!token || typeof token !== 'string' || token.indexOf('.') === -1) return null
|
|
204
|
+
try {
|
|
205
|
+
var tokenParts = token.split('.')
|
|
206
|
+
if (tokenParts.length >= 2) {
|
|
207
|
+
var base64Url = tokenParts[1]
|
|
208
|
+
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
|
|
209
|
+
var decodedStr = ''
|
|
210
|
+
if (typeof atob === 'function') {
|
|
211
|
+
try {
|
|
212
|
+
decodedStr = decodeURIComponent(escape(atob(base64)))
|
|
213
|
+
} catch (e1) {
|
|
214
|
+
decodedStr = atob(base64)
|
|
215
|
+
}
|
|
216
|
+
} else if (typeof Buffer !== 'undefined') {
|
|
217
|
+
decodedStr = Buffer.from(base64, 'base64').toString('utf-8')
|
|
218
|
+
}
|
|
219
|
+
if (decodedStr) {
|
|
220
|
+
return JSON.parse(decodedStr)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} catch (e) {}
|
|
224
|
+
return null
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 自动检测 userId:多源交叉读取,并确保不把已有的合法身份置空
|
|
229
|
+
*/
|
|
230
|
+
function _ensureUserId(tracker) {
|
|
231
|
+
var session = tracker.getSession()
|
|
232
|
+
|
|
233
|
+
// 0. 跨端会话必须无条件同步序列号(无论是否已有 Native 用户身份,绝对不能被提前 return 阻断)
|
|
234
|
+
if (typeof window !== 'undefined') {
|
|
235
|
+
try {
|
|
236
|
+
if (window.location) {
|
|
237
|
+
var search = (window.location.search || '') + '&' + (window.location.hash || '')
|
|
238
|
+
var matchSeq = search.match(/[?&]_trk_seq=([^&]+)/)
|
|
239
|
+
if (matchSeq) {
|
|
240
|
+
session.setInitialSequence(Number(decodeURIComponent(matchSeq[1])))
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (window.NativeBridge && typeof window.NativeBridge.getNativeContextSync === 'function') {
|
|
244
|
+
var nativeRaw = window.NativeBridge.getNativeContextSync()
|
|
245
|
+
if (nativeRaw) {
|
|
246
|
+
var nativeCtx = typeof nativeRaw === 'string' ? JSON.parse(nativeRaw) : nativeRaw
|
|
247
|
+
if (nativeCtx && nativeCtx.currentSequence) {
|
|
248
|
+
session.setInitialSequence(Number(nativeCtx.currentSequence))
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} catch (e) {}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 若当前 userId 由 Native 容器强行注入(跨端会话),H5 端的 Cookie/Storage 绝不覆盖
|
|
256
|
+
if (session.isNativeUserId && session.getUserId()) {
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
var adapter = tracker.getAdapter()
|
|
261
|
+
var uid = null
|
|
262
|
+
|
|
263
|
+
// 1. 跨端 NativeBridge 同步上下文优先(如果运行在 Android/iOS WebView 中)
|
|
264
|
+
if (typeof window !== 'undefined' && window.NativeBridge && typeof window.NativeBridge.getNativeContextSync === 'function') {
|
|
265
|
+
try {
|
|
266
|
+
var nativeRaw = window.NativeBridge.getNativeContextSync()
|
|
267
|
+
if (nativeRaw) {
|
|
268
|
+
var nativeCtx = typeof nativeRaw === 'string' ? JSON.parse(nativeRaw) : nativeRaw
|
|
269
|
+
if (nativeCtx && nativeCtx.userId) {
|
|
270
|
+
uid = String(nativeCtx.userId)
|
|
271
|
+
}
|
|
272
|
+
if (nativeCtx && nativeCtx.currentSequence) {
|
|
273
|
+
session.setInitialSequence(Number(nativeCtx.currentSequence))
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} catch (e) {}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 2. URL query 跨端注入参数兜底(全面兼容标准 search 与 SPA Hash 路由后的 query)
|
|
280
|
+
if (typeof window !== 'undefined' && window.location) {
|
|
281
|
+
try {
|
|
282
|
+
var search = (window.location.search || '') + '&' + (window.location.hash || '')
|
|
283
|
+
var matchSeq = search.match(/[?&]_trk_seq=([^&]+)/)
|
|
284
|
+
if (matchSeq) {
|
|
285
|
+
session.setInitialSequence(Number(decodeURIComponent(matchSeq[1])))
|
|
286
|
+
}
|
|
287
|
+
if (!uid) {
|
|
288
|
+
var matchUid = search.match(/[?&]_trk_userId=([^&]+)/)
|
|
289
|
+
if (matchUid) {
|
|
290
|
+
uid = decodeURIComponent(matchUid[1])
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
} catch (e) {}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// 3. 存储单字段:userId / unionid / _tracker_user_id
|
|
297
|
+
if (!uid) {
|
|
298
|
+
uid = adapter.getStorage('userId')
|
|
299
|
+
|| adapter.getStorage('unionid')
|
|
300
|
+
|| adapter.getStorage('_tracker_user_id')
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 4. 读取 userInfo 复合对象
|
|
304
|
+
if (!uid) {
|
|
305
|
+
try {
|
|
306
|
+
var rawInfo = adapter.getStorage('userInfo')
|
|
307
|
+
if (rawInfo) {
|
|
308
|
+
var info = typeof rawInfo === 'string' ? JSON.parse(rawInfo) : rawInfo
|
|
309
|
+
if (info && typeof info === 'object') {
|
|
310
|
+
uid = info.userId || info.unionid || info.id || info.uid
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
} catch (e) {}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 5. Cookie 兜底:rkfeb-userId 或 static-c-token (JWT payload)
|
|
317
|
+
if (!uid && typeof document !== 'undefined') {
|
|
318
|
+
try {
|
|
319
|
+
var matchCookieUid = document.cookie.match(/(?:^|;\s*)rkfeb-userId=([^;]+)/)
|
|
320
|
+
if (matchCookieUid) uid = decodeURIComponent(matchCookieUid[1])
|
|
321
|
+
|
|
322
|
+
if (!uid) {
|
|
323
|
+
var matchCookieToken = document.cookie.match(/(?:^|;\s*)(?:token|static-c-token|appToken)=([^;]+)/)
|
|
324
|
+
if (matchCookieToken) {
|
|
325
|
+
var payloadCookie = _parseJwtPayload(decodeURIComponent(matchCookieToken[1]))
|
|
326
|
+
if (payloadCookie) {
|
|
327
|
+
uid = payloadCookie.unionid || payloadCookie.userId || payloadCookie.uid || payloadCookie.sub
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
} catch (e) {}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 6. JWT Token 存储兜底:token / Token / appToken / rsc-token / rsb-token
|
|
335
|
+
if (!uid) {
|
|
336
|
+
var tokenKeys = ['token', 'Token', 'appToken', 'rsc-token', 'rsb-token']
|
|
337
|
+
for (var i = 0; i < tokenKeys.length; i++) {
|
|
338
|
+
var tokenVal = adapter.getStorage(tokenKeys[i])
|
|
339
|
+
if (tokenVal) {
|
|
340
|
+
var payload = _parseJwtPayload(tokenVal)
|
|
341
|
+
if (payload) {
|
|
342
|
+
uid = payload.unionid || payload.userId || payload.uid || payload.sub
|
|
343
|
+
if (uid) break
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 7. 防误置空核心保护:只有获取到有效非空 uid 才更新
|
|
350
|
+
var currentUid = session.getUserId()
|
|
351
|
+
if (uid && String(uid) !== String(currentUid)) {
|
|
352
|
+
tracker.setUser(String(uid))
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Vue.use 插件入口
|
|
358
|
+
*
|
|
359
|
+
* 用法:
|
|
360
|
+
* import TrackerSDK from './tracker-sdk/index.js'
|
|
361
|
+
* Vue.use(TrackerSDK, {
|
|
362
|
+
* appId: '1',
|
|
363
|
+
* dadianUrl: 'https://state.renruikeji.cn',
|
|
364
|
+
* platform: 'web',
|
|
365
|
+
* tokenKey: 'token',
|
|
366
|
+
* plugins: ['page-view', 'click', 'page-stay', 'js-error', 'app-lifecycle', 'api-watch', 'scroll-depth'],
|
|
367
|
+
* legacy: true,
|
|
368
|
+
* getAuthHeaders: () => ({ Authorization: orgToken, token: orgToken }),
|
|
369
|
+
* })
|
|
370
|
+
*/
|
|
371
|
+
function install(Vue, options) {
|
|
372
|
+
if (install._installed) return
|
|
373
|
+
install._installed = true
|
|
374
|
+
|
|
375
|
+
var tracker = createTracker(options || {})
|
|
376
|
+
|
|
377
|
+
// 0. 暴露 tracker 实例到 window(供原生容器及验证脚本访问)
|
|
378
|
+
if (typeof window !== 'undefined') {
|
|
379
|
+
window.__tracker = tracker
|
|
380
|
+
window.__tracker_pauseStay = function () { tracker.pausePageStay() }
|
|
381
|
+
window.__tracker_resumeStay = function () { tracker.resumePageStay() }
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// 0.1 设备注册(无 deviceId 时自动注册)
|
|
385
|
+
registerDeviceIfNeeded(tracker)
|
|
386
|
+
|
|
387
|
+
// 0.2 自动检测 userId(从 Cookie 或 storage 中读取已登录用户)
|
|
388
|
+
_ensureUserId(tracker)
|
|
389
|
+
|
|
390
|
+
// 0.3 注册 userId 刷新钩子:每次 flush 前从 storage 实时读取,
|
|
391
|
+
// 避免自动事件(page_view/click)在 userId 更新前用旧值发出
|
|
392
|
+
tracker.getQueue().setRefreshUserId(function () {
|
|
393
|
+
_ensureUserId(tracker)
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
// 1. 注册 $dadian(兼容现有所有业务调用)
|
|
397
|
+
var _rawDadian = createDadianProxy(tracker)
|
|
398
|
+
Vue.prototype.$dadian = function (frontUrl, postParam) {
|
|
399
|
+
_ensureUserId(tracker)
|
|
400
|
+
return _rawDadian(frontUrl, postParam)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// 2. 注册 $track(统一的上报方法,替代 $dadian)
|
|
404
|
+
// 行为与 $dadian 完全一致:第一个参数直接作为 frontUrl,不做任何路由拼接
|
|
405
|
+
Vue.prototype.$track = function (frontUrl, postParam) {
|
|
406
|
+
_ensureUserId(tracker)
|
|
407
|
+
|
|
408
|
+
// postParam 处理(与 $dadian 一致)
|
|
409
|
+
var postParamStr = ''
|
|
410
|
+
if (postParam && Object.keys(postParam).length > 0) {
|
|
411
|
+
var config = tracker.getConfig()
|
|
412
|
+
if (config.platform === 'weapp' && config.eventBus) {
|
|
413
|
+
var bus = config.eventBus
|
|
414
|
+
postParamStr = JSON.stringify(Object.assign({}, postParam, {
|
|
415
|
+
wxAppOnLaunch: bus.wxAppOnLaunch,
|
|
416
|
+
pageFrom: bus.pageFrom,
|
|
417
|
+
mallOrderExt: bus.mallOrderExt,
|
|
418
|
+
}))
|
|
419
|
+
} else {
|
|
420
|
+
postParamStr = JSON.stringify(postParam)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// 开关关(默认):旧接口也报一次(与 $dadian 一致)
|
|
425
|
+
var config = tracker.getConfig()
|
|
426
|
+
if (!config.useUnifiedReport) {
|
|
427
|
+
var header = { 'content-type': 'application/json' }
|
|
428
|
+
var hasLegacyAuth = false
|
|
429
|
+
if (typeof config.getAuthHeaders === 'function') {
|
|
430
|
+
var authHeaders = config.getAuthHeaders() || {}
|
|
431
|
+
if (authHeaders.Authorization || authHeaders.token || authHeaders['rsb-token']) {
|
|
432
|
+
hasLegacyAuth = true
|
|
433
|
+
Object.assign(header, authHeaders)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (hasLegacyAuth) {
|
|
437
|
+
tracker.getQueue().pushLegacy(
|
|
438
|
+
config.dadianUrl + '/api/point/task/activeReport',
|
|
439
|
+
{ frontUrl: frontUrl, postParam: postParamStr },
|
|
440
|
+
header
|
|
441
|
+
)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// SDK 事件入队(始终走)
|
|
446
|
+
tracker.track('custom', {
|
|
447
|
+
eventName: frontUrl || 'manual',
|
|
448
|
+
frontUrl: frontUrl,
|
|
449
|
+
postParam: postParamStr,
|
|
450
|
+
extraData: postParam,
|
|
451
|
+
elementId: frontUrl,
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
// 项目定制:从 footer 导航类 frontUrl 中提取目标路由,
|
|
455
|
+
// 在 track() 之后更新 referrer 链(确保目标页事件能拿到正确来源页)
|
|
456
|
+
// 必须在 track 之后:否则当前事件的 referrer 会被提前污染
|
|
457
|
+
var rules = config.projectRules
|
|
458
|
+
if (rules && typeof rules.extractNavTarget === 'function') {
|
|
459
|
+
var navTarget = rules.extractNavTarget(null, frontUrl)
|
|
460
|
+
if (navTarget) {
|
|
461
|
+
tracker.updatePageRoute(navTarget)
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// 3. 注册 $tracker 实例引用(含 resetSession 供登出时调用)
|
|
467
|
+
Vue.prototype.$tracker = tracker
|
|
468
|
+
Vue.prototype.$trackerResetSession = function () {
|
|
469
|
+
tracker.getSession().resetSession()
|
|
470
|
+
tracker.getQueue()._flushedEventIds.clear()
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// 3.1 动态参数解析器快捷方式
|
|
474
|
+
Vue.prototype.$registerParam = function (name, fn) {
|
|
475
|
+
tracker.registerParam(name, fn)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// 4. 注册 v-impression 指令(兼容旧业务代码)
|
|
479
|
+
var adapter = tracker.getAdapter()
|
|
480
|
+
if (adapter.supportsIntersectionObserver()) {
|
|
481
|
+
Vue.directive('impression', createImpressionDirective(tracker))
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// 5. 注册 v-track-exposure 指令(新的曝光指令,走自动采集链路)
|
|
485
|
+
var exposurePlugin = tracker.plugins.find(function (p) {
|
|
486
|
+
return p.name === 'exposure'
|
|
487
|
+
})
|
|
488
|
+
if (exposurePlugin && typeof exposurePlugin.createDirective === 'function') {
|
|
489
|
+
var trackExposureDirective = exposurePlugin.createDirective()
|
|
490
|
+
if (trackExposureDirective) {
|
|
491
|
+
Vue.directive('track-exposure', trackExposureDirective)
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// 6. 注册页面停留 mixin
|
|
496
|
+
var hasPageStayPlugin = (options.plugins || []).some(function (p) {
|
|
497
|
+
return p === 'page-stay' || (p && p.name === 'page-stay')
|
|
498
|
+
})
|
|
499
|
+
if (hasPageStayPlugin) {
|
|
500
|
+
Vue.mixin(createPageStayMixin(tracker))
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// 7. 注册 Vue 错误捕获(组件 render/lifecycle/watch 中的异常)
|
|
504
|
+
var jsErrorPlugin = tracker.plugins.find(function (p) {
|
|
505
|
+
return p.name === 'js-error'
|
|
506
|
+
})
|
|
507
|
+
if (jsErrorPlugin) {
|
|
508
|
+
var _prevErrorHandler = Vue.config.errorHandler
|
|
509
|
+
Vue.config.errorHandler = function (err, vm, info) {
|
|
510
|
+
jsErrorPlugin.handleVueError(err, vm, info)
|
|
511
|
+
if (_prevErrorHandler) _prevErrorHandler.call(this, err, vm, info)
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// [PROJECT-ADAPT] 小程序生命周期注册
|
|
516
|
+
// H5 端通过 history API 监听路由变化,小程序端通过 onShow/onHide
|
|
517
|
+
if (tracker.getConfig().platform === 'weapp') {
|
|
518
|
+
var pageViewPlugin = tracker.plugins.find(function (p) {
|
|
519
|
+
return p.name === 'page-view'
|
|
520
|
+
})
|
|
521
|
+
var lifecyclePlugin = tracker.plugins.find(function (p) {
|
|
522
|
+
return p.name === 'app-lifecycle'
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
Vue.mixin({
|
|
526
|
+
onShow: function () {
|
|
527
|
+
if (pageViewPlugin) {
|
|
528
|
+
pageViewPlugin.trackPageShow()
|
|
529
|
+
}
|
|
530
|
+
if (lifecyclePlugin) {
|
|
531
|
+
lifecyclePlugin.trackAppShow()
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
onHide: function () {
|
|
535
|
+
if (lifecyclePlugin) {
|
|
536
|
+
lifecyclePlugin.trackAppHide()
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
})
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// 默认导出 Vue 插件对象
|
|
544
|
+
export default {
|
|
545
|
+
install: install,
|
|
546
|
+
createTracker: createTracker,
|
|
547
|
+
getTracker: getTracker,
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// 具名导出
|
|
551
|
+
export { Tracker } from './core/tracker.js'
|
|
552
|
+
export { WebAdapter } from './platforms/web.js'
|
|
553
|
+
export { WeappAdapter } from './platforms/weapp.js'
|
|
554
|
+
export { AppAdapter } from './platforms/app.js'
|
|
@@ -288,14 +288,22 @@ public class RKNetworkMonitor: NSObject {
|
|
|
288
288
|
|
|
289
289
|
@objc public func getNormalizedSystemVersion() -> String {
|
|
290
290
|
let ver = ProcessInfo.processInfo.operatingSystemVersion
|
|
291
|
-
if ver.majorVersion >= 10 && ver.majorVersion <
|
|
291
|
+
if ver.majorVersion >= 10 && ver.majorVersion < 20 {
|
|
292
292
|
if ver.patchVersion > 0 {
|
|
293
293
|
return "\(ver.majorVersion).\(ver.minorVersion).\(ver.patchVersion)"
|
|
294
294
|
}
|
|
295
295
|
return "\(ver.majorVersion).\(ver.minorVersion)"
|
|
296
296
|
}
|
|
297
|
+
// 当运行在 Mac Catalyst、iPad 模拟器或开发机 Darwin 内核基线 (如 26.5) 时,规整至真实商业版本
|
|
298
|
+
if ver.majorVersion >= 20 {
|
|
299
|
+
let mappedMajor = max(16, min(18, ver.majorVersion - 8))
|
|
300
|
+
return "\(mappedMajor).\(ver.minorVersion)"
|
|
301
|
+
}
|
|
297
302
|
let sys = UIDevice.current.systemVersion
|
|
298
|
-
|
|
303
|
+
if !sys.isEmpty && !sys.hasPrefix("2") {
|
|
304
|
+
return sys
|
|
305
|
+
}
|
|
306
|
+
return "18.2"
|
|
299
307
|
}
|
|
300
308
|
|
|
301
309
|
@objc public func getExactDeviceModel() -> String {
|
|
@@ -537,22 +545,63 @@ public class RKNetworkMonitor: NSObject {
|
|
|
537
545
|
}
|
|
538
546
|
}
|
|
539
547
|
|
|
548
|
+
private static let jsonRequestIdRegex: NSRegularExpression? = {
|
|
549
|
+
return try? NSRegularExpression(pattern: "\"requestId\"\\s*:\\s*\"([^\"\\r\\n]+)\"", options: [])
|
|
550
|
+
}()
|
|
551
|
+
|
|
552
|
+
private static let queryRequestIdRegex: NSRegularExpression? = {
|
|
553
|
+
return try? NSRegularExpression(pattern: "(?:^|[?&])requestId=([^&#\\s]+)", options: [])
|
|
554
|
+
}()
|
|
555
|
+
|
|
540
556
|
private func extractRequestIdSafe(_ reqParam: String?, _ respData: String?) -> String {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
557
|
+
let reqId = extractRequestIdFromText(reqParam)
|
|
558
|
+
if !reqId.isEmpty {
|
|
559
|
+
return reqId
|
|
560
|
+
}
|
|
561
|
+
return extractRequestIdFromText(respData)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
private func extractRequestIdFromText(_ text: String?) -> String {
|
|
565
|
+
guard let text = text, text.contains("requestId") else { return "" }
|
|
566
|
+
let nsString = text as NSString
|
|
567
|
+
let fullRange = NSRange(location: 0, length: nsString.length)
|
|
568
|
+
|
|
569
|
+
// 1. 优先匹配 JSON 格式: "requestId": "xxx"
|
|
570
|
+
if let regex = RKTrackerSDK.jsonRequestIdRegex,
|
|
571
|
+
let match = regex.firstMatch(in: text, options: [], range: fullRange),
|
|
572
|
+
match.numberOfRanges > 1 {
|
|
573
|
+
let r = match.range(at: 1)
|
|
574
|
+
if r.location != NSNotFound {
|
|
575
|
+
let candidate = nsString.substring(with: r).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
576
|
+
if isValidRequestId(candidate) {
|
|
577
|
+
return candidate
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// 2. 匹配 Query String 格式: requestId=xxx
|
|
583
|
+
if let regex = RKTrackerSDK.queryRequestIdRegex,
|
|
584
|
+
let match = regex.firstMatch(in: text, options: [], range: fullRange),
|
|
585
|
+
match.numberOfRanges > 1 {
|
|
586
|
+
let r = match.range(at: 1)
|
|
587
|
+
if r.location != NSNotFound {
|
|
588
|
+
let candidate = nsString.substring(with: r).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
589
|
+
if isValidRequestId(candidate) {
|
|
590
|
+
return candidate
|
|
550
591
|
}
|
|
551
592
|
}
|
|
552
593
|
}
|
|
594
|
+
|
|
553
595
|
return ""
|
|
554
596
|
}
|
|
555
597
|
|
|
598
|
+
private func isValidRequestId(_ val: String) -> Bool {
|
|
599
|
+
let trimmed = val.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
600
|
+
if trimmed.isEmpty { return false }
|
|
601
|
+
let lower = trimmed.lowercased()
|
|
602
|
+
return lower != "null" && lower != "undefined"
|
|
603
|
+
}
|
|
604
|
+
|
|
556
605
|
@objc public func trackFrontUrl(frontUrl: String, elementId: String?, postParam: String?) {
|
|
557
606
|
bgQueue.async { [weak self] in
|
|
558
607
|
guard let self = self else { return }
|
|
@@ -650,6 +699,68 @@ public class RKNetworkMonitor: NSObject {
|
|
|
650
699
|
}
|
|
651
700
|
}
|
|
652
701
|
|
|
702
|
+
@objc public func handleNativeViewClick(view: UIView, actionName: String?) {
|
|
703
|
+
guard isInitialized else { return }
|
|
704
|
+
if view is UITextField || view is UITextView {
|
|
705
|
+
return
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
var elementId = view.accessibilityIdentifier ?? ""
|
|
709
|
+
if elementId.isEmpty {
|
|
710
|
+
if let act = actionName, !act.isEmpty {
|
|
711
|
+
elementId = act.replacingOccurrences(of: ":", with: "")
|
|
712
|
+
} else if view.tag > 0 {
|
|
713
|
+
elementId = "tag_\(view.tag)"
|
|
714
|
+
} else {
|
|
715
|
+
elementId = String(describing: type(of: view))
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
var elementText = ""
|
|
720
|
+
if let btn = view as? UIButton {
|
|
721
|
+
elementText = btn.currentTitle ?? btn.titleLabel?.text ?? ""
|
|
722
|
+
}
|
|
723
|
+
if elementText.isEmpty {
|
|
724
|
+
elementText = findTextInView(view)
|
|
725
|
+
}
|
|
726
|
+
elementText = elementText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
727
|
+
if elementText.count > 30 {
|
|
728
|
+
elementText = String(elementText.prefix(30))
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
let key = "\(elementId)|\(elementText)"
|
|
732
|
+
let now = Date().timeIntervalSince1970
|
|
733
|
+
if key == lastClickKey && (now - lastClickTime < 0.8) {
|
|
734
|
+
return
|
|
735
|
+
}
|
|
736
|
+
lastClickKey = key
|
|
737
|
+
lastClickTime = now
|
|
738
|
+
|
|
739
|
+
let centerInWindow = view.superview?.convert(view.center, to: nil) ?? view.center
|
|
740
|
+
let pageX = centerInWindow.x
|
|
741
|
+
let pageY = centerInWindow.y
|
|
742
|
+
let viewType = String(describing: type(of: view))
|
|
743
|
+
|
|
744
|
+
bgQueue.async { [weak self] in
|
|
745
|
+
guard let self = self else { return }
|
|
746
|
+
var clickEvent = self.createBaseEvent(eventType: "click", eventName: "native_click")
|
|
747
|
+
clickEvent["elementId"] = elementId
|
|
748
|
+
if !elementText.isEmpty {
|
|
749
|
+
clickEvent["elementText"] = elementText
|
|
750
|
+
}
|
|
751
|
+
var extra = self.buildBaseExtra()
|
|
752
|
+
extra["viewType"] = viewType
|
|
753
|
+
extra["pageX"] = pageX
|
|
754
|
+
extra["pageY"] = pageY
|
|
755
|
+
extra["isNative"] = true
|
|
756
|
+
if let data = try? JSONSerialization.data(withJSONObject: extra), let str = String(data: data, encoding: .utf8) {
|
|
757
|
+
clickEvent["extraData"] = str
|
|
758
|
+
}
|
|
759
|
+
self.enqueueEvent(clickEvent)
|
|
760
|
+
self.flushAsync()
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
653
764
|
private func findTextInView(_ view: UIView) -> String {
|
|
654
765
|
for sub in view.subviews {
|
|
655
766
|
if let label = sub as? UILabel, let t = label.text, !t.isEmpty {
|
|
@@ -886,15 +997,17 @@ extension UIViewController {
|
|
|
886
997
|
if clsName.contains("Prewarming") || clsName.contains("Preview") || clsName.contains("Snapshot") {
|
|
887
998
|
return false
|
|
888
999
|
}
|
|
889
|
-
//
|
|
890
|
-
if clsName.contains("InputWindow") || clsName.contains("Compatibility") || clsName.contains("Alert") {
|
|
1000
|
+
// 过滤系统辅助/键盘相关临时控制器及三方技术容器
|
|
1001
|
+
if clsName.contains("InputWindow") || clsName.contains("Compatibility") || clsName.contains("Alert") || clsName.contains("GKPageList") {
|
|
891
1002
|
return false
|
|
892
1003
|
}
|
|
893
1004
|
let ignoredContainers = [
|
|
894
1005
|
"CYLMainRootViewController",
|
|
895
1006
|
"MainTabBarController",
|
|
896
1007
|
"RRBaseNavigationController",
|
|
897
|
-
"RRHomeRecommendPageController"
|
|
1008
|
+
"RRHomeRecommendPageController",
|
|
1009
|
+
"GKPageListContainerViewController",
|
|
1010
|
+
"GKPageListViewController"
|
|
898
1011
|
]
|
|
899
1012
|
if ignoredContainers.contains(clsName) {
|
|
900
1013
|
return false
|