@rk911/tracker-sdk 1.7.16 → 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 CHANGED
@@ -1,529 +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
- // 若当前 userId 由 Native 容器强行注入(跨端会话),H5 端的 Cookie/Storage 绝不覆盖
233
- if (session.isNativeUserId && session.getUserId()) {
234
- return
235
- }
236
-
237
- var adapter = tracker.getAdapter()
238
- var uid = null
239
-
240
- // 1. 跨端 NativeBridge 同步上下文优先(如果运行在 Android/iOS WebView 中)
241
- if (typeof window !== 'undefined' && window.NativeBridge && typeof window.NativeBridge.getNativeContextSync === 'function') {
242
- try {
243
- var nativeRaw = window.NativeBridge.getNativeContextSync()
244
- if (nativeRaw) {
245
- var nativeCtx = typeof nativeRaw === 'string' ? JSON.parse(nativeRaw) : nativeRaw
246
- if (nativeCtx && nativeCtx.userId) {
247
- uid = String(nativeCtx.userId)
248
- }
249
- if (nativeCtx && nativeCtx.currentSequence) {
250
- session.setInitialSequence(Number(nativeCtx.currentSequence))
251
- }
252
- }
253
- } catch (e) {}
254
- }
255
-
256
- // 2. URL query 跨端注入参数兜底
257
- if (!uid && typeof window !== 'undefined' && window.location && window.location.search) {
258
- try {
259
- var search = window.location.search
260
- var matchSeq = search.match(/[?&]_trk_seq=([^&]+)/)
261
- if (matchSeq) {
262
- session.setInitialSequence(Number(decodeURIComponent(matchSeq[1])))
263
- }
264
- var matchUid = search.match(/[?&]_trk_userId=([^&]+)/)
265
- if (matchUid) {
266
- uid = decodeURIComponent(matchUid[1])
267
- }
268
- } catch (e) {}
269
- }
270
-
271
- // 3. 存储单字段:userId / unionid / _tracker_user_id
272
- if (!uid) {
273
- uid = adapter.getStorage('userId')
274
- || adapter.getStorage('unionid')
275
- || adapter.getStorage('_tracker_user_id')
276
- }
277
-
278
- // 4. 读取 userInfo 复合对象
279
- if (!uid) {
280
- try {
281
- var rawInfo = adapter.getStorage('userInfo')
282
- if (rawInfo) {
283
- var info = typeof rawInfo === 'string' ? JSON.parse(rawInfo) : rawInfo
284
- if (info && typeof info === 'object') {
285
- uid = info.userId || info.unionid || info.id || info.uid
286
- }
287
- }
288
- } catch (e) {}
289
- }
290
-
291
- // 5. Cookie 兜底:rkfeb-userId 或 static-c-token (JWT payload)
292
- if (!uid && typeof document !== 'undefined') {
293
- try {
294
- var matchCookieUid = document.cookie.match(/(?:^|;\s*)rkfeb-userId=([^;]+)/)
295
- if (matchCookieUid) uid = decodeURIComponent(matchCookieUid[1])
296
-
297
- if (!uid) {
298
- var matchCookieToken = document.cookie.match(/(?:^|;\s*)(?:token|static-c-token|appToken)=([^;]+)/)
299
- if (matchCookieToken) {
300
- var payloadCookie = _parseJwtPayload(decodeURIComponent(matchCookieToken[1]))
301
- if (payloadCookie) {
302
- uid = payloadCookie.unionid || payloadCookie.userId || payloadCookie.uid || payloadCookie.sub
303
- }
304
- }
305
- }
306
- } catch (e) {}
307
- }
308
-
309
- // 6. JWT Token 存储兜底:token / Token / appToken / rsc-token / rsb-token
310
- if (!uid) {
311
- var tokenKeys = ['token', 'Token', 'appToken', 'rsc-token', 'rsb-token']
312
- for (var i = 0; i < tokenKeys.length; i++) {
313
- var tokenVal = adapter.getStorage(tokenKeys[i])
314
- if (tokenVal) {
315
- var payload = _parseJwtPayload(tokenVal)
316
- if (payload) {
317
- uid = payload.unionid || payload.userId || payload.uid || payload.sub
318
- if (uid) break
319
- }
320
- }
321
- }
322
- }
323
-
324
- // 7. 防误置空核心保护:只有获取到有效非空 uid 才更新
325
- var currentUid = session.getUserId()
326
- if (uid && String(uid) !== String(currentUid)) {
327
- tracker.setUser(String(uid))
328
- }
329
- }
330
-
331
- /**
332
- * Vue.use 插件入口
333
- *
334
- * 用法:
335
- * import TrackerSDK from './tracker-sdk/index.js'
336
- * Vue.use(TrackerSDK, {
337
- * appId: '1',
338
- * dadianUrl: 'https://state.renruikeji.cn',
339
- * platform: 'web',
340
- * tokenKey: 'token',
341
- * plugins: ['page-view', 'click', 'page-stay', 'js-error', 'app-lifecycle', 'api-watch', 'scroll-depth'],
342
- * legacy: true,
343
- * getAuthHeaders: () => ({ Authorization: orgToken, token: orgToken }),
344
- * })
345
- */
346
- function install(Vue, options) {
347
- if (install._installed) return
348
- install._installed = true
349
-
350
- var tracker = createTracker(options || {})
351
-
352
- // 0. 暴露 tracker 实例到 window(供原生容器及验证脚本访问)
353
- if (typeof window !== 'undefined') {
354
- window.__tracker = tracker
355
- window.__tracker_pauseStay = function () { tracker.pausePageStay() }
356
- window.__tracker_resumeStay = function () { tracker.resumePageStay() }
357
- }
358
-
359
- // 0.1 设备注册(无 deviceId 时自动注册)
360
- registerDeviceIfNeeded(tracker)
361
-
362
- // 0.2 自动检测 userId(从 Cookie 或 storage 中读取已登录用户)
363
- _ensureUserId(tracker)
364
-
365
- // 0.3 注册 userId 刷新钩子:每次 flush 前从 storage 实时读取,
366
- // 避免自动事件(page_view/click)在 userId 更新前用旧值发出
367
- tracker.getQueue().setRefreshUserId(function () {
368
- _ensureUserId(tracker)
369
- })
370
-
371
- // 1. 注册 $dadian(兼容现有所有业务调用)
372
- var _rawDadian = createDadianProxy(tracker)
373
- Vue.prototype.$dadian = function (frontUrl, postParam) {
374
- _ensureUserId(tracker)
375
- return _rawDadian(frontUrl, postParam)
376
- }
377
-
378
- // 2. 注册 $track(统一的上报方法,替代 $dadian)
379
- // 行为与 $dadian 完全一致:第一个参数直接作为 frontUrl,不做任何路由拼接
380
- Vue.prototype.$track = function (frontUrl, postParam) {
381
- _ensureUserId(tracker)
382
-
383
- // postParam 处理(与 $dadian 一致)
384
- var postParamStr = ''
385
- if (postParam && Object.keys(postParam).length > 0) {
386
- var config = tracker.getConfig()
387
- if (config.platform === 'weapp' && config.eventBus) {
388
- var bus = config.eventBus
389
- postParamStr = JSON.stringify(Object.assign({}, postParam, {
390
- wxAppOnLaunch: bus.wxAppOnLaunch,
391
- pageFrom: bus.pageFrom,
392
- mallOrderExt: bus.mallOrderExt,
393
- }))
394
- } else {
395
- postParamStr = JSON.stringify(postParam)
396
- }
397
- }
398
-
399
- // 开关关(默认):旧接口也报一次(与 $dadian 一致)
400
- var config = tracker.getConfig()
401
- if (!config.useUnifiedReport) {
402
- var header = { 'content-type': 'application/json' }
403
- var hasLegacyAuth = false
404
- if (typeof config.getAuthHeaders === 'function') {
405
- var authHeaders = config.getAuthHeaders() || {}
406
- if (authHeaders.Authorization || authHeaders.token || authHeaders['rsb-token']) {
407
- hasLegacyAuth = true
408
- Object.assign(header, authHeaders)
409
- }
410
- }
411
- if (hasLegacyAuth) {
412
- tracker.getQueue().pushLegacy(
413
- config.dadianUrl + '/api/point/task/activeReport',
414
- { frontUrl: frontUrl, postParam: postParamStr },
415
- header
416
- )
417
- }
418
- }
419
-
420
- // SDK 事件入队(始终走)
421
- tracker.track('custom', {
422
- eventName: frontUrl || 'manual',
423
- frontUrl: frontUrl,
424
- postParam: postParamStr,
425
- extraData: postParam,
426
- elementId: frontUrl,
427
- })
428
-
429
- // 项目定制:从 footer 导航类 frontUrl 中提取目标路由,
430
- // 在 track() 之后更新 referrer 链(确保目标页事件能拿到正确来源页)
431
- // 必须在 track 之后:否则当前事件的 referrer 会被提前污染
432
- var rules = config.projectRules
433
- if (rules && typeof rules.extractNavTarget === 'function') {
434
- var navTarget = rules.extractNavTarget(null, frontUrl)
435
- if (navTarget) {
436
- tracker.updatePageRoute(navTarget)
437
- }
438
- }
439
- }
440
-
441
- // 3. 注册 $tracker 实例引用(含 resetSession 供登出时调用)
442
- Vue.prototype.$tracker = tracker
443
- Vue.prototype.$trackerResetSession = function () {
444
- tracker.getSession().resetSession()
445
- tracker.getQueue()._flushedEventIds.clear()
446
- }
447
-
448
- // 3.1 动态参数解析器快捷方式
449
- Vue.prototype.$registerParam = function (name, fn) {
450
- tracker.registerParam(name, fn)
451
- }
452
-
453
- // 4. 注册 v-impression 指令(兼容旧业务代码)
454
- var adapter = tracker.getAdapter()
455
- if (adapter.supportsIntersectionObserver()) {
456
- Vue.directive('impression', createImpressionDirective(tracker))
457
- }
458
-
459
- // 5. 注册 v-track-exposure 指令(新的曝光指令,走自动采集链路)
460
- var exposurePlugin = tracker.plugins.find(function (p) {
461
- return p.name === 'exposure'
462
- })
463
- if (exposurePlugin && typeof exposurePlugin.createDirective === 'function') {
464
- var trackExposureDirective = exposurePlugin.createDirective()
465
- if (trackExposureDirective) {
466
- Vue.directive('track-exposure', trackExposureDirective)
467
- }
468
- }
469
-
470
- // 6. 注册页面停留 mixin
471
- var hasPageStayPlugin = (options.plugins || []).some(function (p) {
472
- return p === 'page-stay' || (p && p.name === 'page-stay')
473
- })
474
- if (hasPageStayPlugin) {
475
- Vue.mixin(createPageStayMixin(tracker))
476
- }
477
-
478
- // 7. 注册 Vue 错误捕获(组件 render/lifecycle/watch 中的异常)
479
- var jsErrorPlugin = tracker.plugins.find(function (p) {
480
- return p.name === 'js-error'
481
- })
482
- if (jsErrorPlugin) {
483
- var _prevErrorHandler = Vue.config.errorHandler
484
- Vue.config.errorHandler = function (err, vm, info) {
485
- jsErrorPlugin.handleVueError(err, vm, info)
486
- if (_prevErrorHandler) _prevErrorHandler.call(this, err, vm, info)
487
- }
488
- }
489
-
490
- // [PROJECT-ADAPT] 小程序生命周期注册
491
- // H5 端通过 history API 监听路由变化,小程序端通过 onShow/onHide
492
- if (tracker.getConfig().platform === 'weapp') {
493
- var pageViewPlugin = tracker.plugins.find(function (p) {
494
- return p.name === 'page-view'
495
- })
496
- var lifecyclePlugin = tracker.plugins.find(function (p) {
497
- return p.name === 'app-lifecycle'
498
- })
499
-
500
- Vue.mixin({
501
- onShow: function () {
502
- if (pageViewPlugin) {
503
- pageViewPlugin.trackPageShow()
504
- }
505
- if (lifecyclePlugin) {
506
- lifecyclePlugin.trackAppShow()
507
- }
508
- },
509
- onHide: function () {
510
- if (lifecyclePlugin) {
511
- lifecyclePlugin.trackAppHide()
512
- }
513
- },
514
- })
515
- }
516
- }
517
-
518
- // 默认导出 Vue 插件对象
519
- export default {
520
- install: install,
521
- createTracker: createTracker,
522
- getTracker: getTracker,
523
- }
524
-
525
- // 具名导出
526
- export { Tracker } from './core/tracker.js'
527
- export { WebAdapter } from './platforms/web.js'
528
- export { WeappAdapter } from './platforms/weapp.js'
529
- export { AppAdapter } from './platforms/app.js'
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'