@rk911/tracker-sdk 1.7.6 → 1.7.7

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.
Files changed (2) hide show
  1. package/index.js +437 -425
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1,425 +1,437 @@
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
- /**
203
- * 自动检测 userId:每次从 localStorage/cookie 实时读取
204
- * 不依赖内存缓存,确保清除存储后不会残留旧用户、切换账号后能及时更新
205
- */
206
- function _ensureUserId(tracker) {
207
- var session = tracker.getSession()
208
- // 如果当前 userId 是由 Native 拼接注入的临时账号,不被 H5 本地 Cookie/Storage 覆写
209
- if (session.isNativeUserId && session.getUserId()) {
210
- return
211
- }
212
-
213
- var adapter = tracker.getAdapter()
214
- // 从源头实时读取(支持 userId 与 unionid 双向兼容)
215
- var uid = adapter.getStorage('userId') || adapter.getStorage('unionid')
216
- // 降级:从 cookie 读(rkfeb-userId)
217
- if (!uid && typeof document !== 'undefined') {
218
- var match = document.cookie.match(/(?:^|;\s*)rkfeb-userId=([^;]+)/)
219
- if (match) uid = decodeURIComponent(match[1])
220
- }
221
- var currentUid = tracker.getSession().getUserId()
222
- if (uid && uid !== currentUid) {
223
- tracker.setUser(uid)
224
- } else if (!uid && currentUid && !session.isNativeUserId) {
225
- // 仅针对纯 H5 登录场景:若源头已清空(退出登录/清除缓存),同步清除内存中的旧值
226
- tracker.setUser(null)
227
- }
228
- }
229
-
230
- /**
231
- * Vue.use 插件入口
232
- *
233
- * 用法:
234
- * import TrackerSDK from './tracker-sdk/index.js'
235
- * Vue.use(TrackerSDK, {
236
- * appId: '1',
237
- * dadianUrl: 'https://state.renruikeji.cn',
238
- * platform: 'web',
239
- * tokenKey: 'token',
240
- * plugins: ['page-view', 'click', 'page-stay', 'js-error', 'app-lifecycle', 'api-watch', 'scroll-depth'],
241
- * legacy: true,
242
- * getAuthHeaders: () => ({ Authorization: orgToken, token: orgToken }),
243
- * })
244
- */
245
- function install(Vue, options) {
246
- if (install._installed) return
247
- install._installed = true
248
-
249
- var tracker = createTracker(options || {})
250
-
251
- // 0. 暴露 tracker 实例到 window(供验证脚本访问)
252
- if (typeof window !== 'undefined') {
253
- window.__tracker = tracker
254
- }
255
-
256
- // 0.1 设备注册(无 deviceId 时自动注册)
257
- registerDeviceIfNeeded(tracker)
258
-
259
- // 0.2 自动检测 userId(从 Cookie 或 storage 中读取已登录用户)
260
- _ensureUserId(tracker)
261
-
262
- // 0.3 注册 userId 刷新钩子:每次 flush 前从 storage 实时读取,
263
- // 避免自动事件(page_view/click)在 userId 更新前用旧值发出
264
- tracker.getQueue().setRefreshUserId(function () {
265
- _ensureUserId(tracker)
266
- })
267
-
268
- // 1. 注册 $dadian(兼容现有所有业务调用)
269
- var _rawDadian = createDadianProxy(tracker)
270
- Vue.prototype.$dadian = function (frontUrl, postParam) {
271
- _ensureUserId(tracker)
272
- return _rawDadian(frontUrl, postParam)
273
- }
274
-
275
- // 2. 注册 $track(统一的上报方法,替代 $dadian)
276
- // 行为与 $dadian 完全一致:第一个参数直接作为 frontUrl,不做任何路由拼接
277
- Vue.prototype.$track = function (frontUrl, postParam) {
278
- _ensureUserId(tracker)
279
-
280
- // postParam 处理(与 $dadian 一致)
281
- var postParamStr = ''
282
- if (postParam && Object.keys(postParam).length > 0) {
283
- var config = tracker.getConfig()
284
- if (config.platform === 'weapp' && config.eventBus) {
285
- var bus = config.eventBus
286
- postParamStr = JSON.stringify(Object.assign({}, postParam, {
287
- wxAppOnLaunch: bus.wxAppOnLaunch,
288
- pageFrom: bus.pageFrom,
289
- mallOrderExt: bus.mallOrderExt,
290
- }))
291
- } else {
292
- postParamStr = JSON.stringify(postParam)
293
- }
294
- }
295
-
296
- // 开关关(默认):旧接口也报一次(与 $dadian 一致)
297
- var config = tracker.getConfig()
298
- if (!config.useUnifiedReport) {
299
- var header = { 'content-type': 'application/json' }
300
- var hasLegacyAuth = false
301
- if (typeof config.getAuthHeaders === 'function') {
302
- var authHeaders = config.getAuthHeaders() || {}
303
- if (authHeaders.Authorization || authHeaders.token || authHeaders['rsb-token']) {
304
- hasLegacyAuth = true
305
- Object.assign(header, authHeaders)
306
- }
307
- }
308
- if (hasLegacyAuth) {
309
- tracker.getQueue().pushLegacy(
310
- config.dadianUrl + '/api/point/task/activeReport',
311
- { frontUrl: frontUrl, postParam: postParamStr },
312
- header
313
- )
314
- }
315
- }
316
-
317
- // SDK 事件入队(始终走)
318
- tracker.track('custom', {
319
- eventName: frontUrl || 'manual',
320
- frontUrl: frontUrl,
321
- postParam: postParamStr,
322
- elementId: frontUrl,
323
- })
324
-
325
- // 项目定制:从 footer 导航类 frontUrl 中提取目标路由,
326
- // 在 track() 之后更新 referrer 链(确保目标页事件能拿到正确来源页)
327
- // 必须在 track 之后:否则当前事件的 referrer 会被提前污染
328
- var rules = config.projectRules
329
- if (rules && typeof rules.extractNavTarget === 'function') {
330
- var navTarget = rules.extractNavTarget(null, frontUrl)
331
- if (navTarget) {
332
- tracker.updatePageRoute(navTarget)
333
- }
334
- }
335
- }
336
-
337
- // 3. 注册 $tracker 实例引用(含 resetSession 供登出时调用)
338
- Vue.prototype.$tracker = tracker
339
- Vue.prototype.$trackerResetSession = function () {
340
- tracker.getSession().resetSession()
341
- tracker.getQueue()._flushedEventIds.clear()
342
- }
343
-
344
- // 3.1 动态参数解析器快捷方式
345
- Vue.prototype.$registerParam = function (name, fn) {
346
- tracker.registerParam(name, fn)
347
- }
348
-
349
- // 4. 注册 v-impression 指令(兼容旧业务代码)
350
- var adapter = tracker.getAdapter()
351
- if (adapter.supportsIntersectionObserver()) {
352
- Vue.directive('impression', createImpressionDirective(tracker))
353
- }
354
-
355
- // 5. 注册 v-track-exposure 指令(新的曝光指令,走自动采集链路)
356
- var exposurePlugin = tracker.plugins.find(function (p) {
357
- return p.name === 'exposure'
358
- })
359
- if (exposurePlugin && typeof exposurePlugin.createDirective === 'function') {
360
- var trackExposureDirective = exposurePlugin.createDirective()
361
- if (trackExposureDirective) {
362
- Vue.directive('track-exposure', trackExposureDirective)
363
- }
364
- }
365
-
366
- // 6. 注册页面停留 mixin
367
- var hasPageStayPlugin = (options.plugins || []).some(function (p) {
368
- return p === 'page-stay' || (p && p.name === 'page-stay')
369
- })
370
- if (hasPageStayPlugin) {
371
- Vue.mixin(createPageStayMixin(tracker))
372
- }
373
-
374
- // 7. 注册 Vue 错误捕获(组件 render/lifecycle/watch 中的异常)
375
- var jsErrorPlugin = tracker.plugins.find(function (p) {
376
- return p.name === 'js-error'
377
- })
378
- if (jsErrorPlugin) {
379
- var _prevErrorHandler = Vue.config.errorHandler
380
- Vue.config.errorHandler = function (err, vm, info) {
381
- jsErrorPlugin.handleVueError(err, vm, info)
382
- if (_prevErrorHandler) _prevErrorHandler.call(this, err, vm, info)
383
- }
384
- }
385
-
386
- // [PROJECT-ADAPT] 小程序生命周期注册
387
- // H5 端通过 history API 监听路由变化,小程序端通过 onShow/onHide
388
- if (tracker.getConfig().platform === 'weapp') {
389
- var pageViewPlugin = tracker.plugins.find(function (p) {
390
- return p.name === 'page-view'
391
- })
392
- var lifecyclePlugin = tracker.plugins.find(function (p) {
393
- return p.name === 'app-lifecycle'
394
- })
395
-
396
- Vue.mixin({
397
- onShow: function () {
398
- if (pageViewPlugin) {
399
- pageViewPlugin.trackPageShow()
400
- }
401
- if (lifecyclePlugin) {
402
- lifecyclePlugin.trackAppShow()
403
- }
404
- },
405
- onHide: function () {
406
- if (lifecyclePlugin) {
407
- lifecyclePlugin.trackAppHide()
408
- }
409
- },
410
- })
411
- }
412
- }
413
-
414
- // 默认导出 Vue 插件对象
415
- export default {
416
- install: install,
417
- createTracker: createTracker,
418
- getTracker: getTracker,
419
- }
420
-
421
- // 具名导出
422
- export { Tracker } from './core/tracker.js'
423
- export { WebAdapter } from './platforms/web.js'
424
- export { WeappAdapter } from './platforms/weapp.js'
425
- 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
+ /**
203
+ * 自动检测 userId:每次从 localStorage/cookie 实时读取
204
+ * 不依赖内存缓存,确保清除存储后不会残留旧用户、切换账号后能及时更新
205
+ */
206
+ function _ensureUserId(tracker) {
207
+ var session = tracker.getSession()
208
+ // 若当前 userId 由 Native 容器强行注入(跨端会话),H5 端的 Cookie/Storage 绝不覆盖
209
+ if (session.isNativeUserId && session.getUserId()) {
210
+ return
211
+ }
212
+
213
+ var adapter = tracker.getAdapter()
214
+ // 1. 优先读取单字段 userId 与 unionid
215
+ var uid = adapter.getStorage('userId') || adapter.getStorage('unionid')
216
+
217
+ // 2. 关键兼容:读取 uni-app 登录时持久化的 userInfo 复合对象
218
+ if (!uid) {
219
+ try {
220
+ var rawInfo = adapter.getStorage('userInfo')
221
+ if (rawInfo) {
222
+ var info = typeof rawInfo === 'string' ? JSON.parse(rawInfo) : rawInfo
223
+ if (info && typeof info === 'object') {
224
+ uid = info.userId || info.unionid || info.id || info.uid
225
+ }
226
+ }
227
+ } catch (e) {}
228
+ }
229
+
230
+ // 3. 从 cookie 兜底 rkfeb-userId
231
+ if (!uid && typeof document !== 'undefined') {
232
+ var match = document.cookie.match(/(?:^|;\s*)rkfeb-userId=([^;]+)/)
233
+ if (match) uid = decodeURIComponent(match[1])
234
+ }
235
+
236
+ var currentUid = tracker.getSession().getUserId()
237
+ if (uid && uid !== currentUid) {
238
+ tracker.setUser(uid)
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Vue.use 插件入口
244
+ *
245
+ * 用法:
246
+ * import TrackerSDK from './tracker-sdk/index.js'
247
+ * Vue.use(TrackerSDK, {
248
+ * appId: '1',
249
+ * dadianUrl: 'https://state.renruikeji.cn',
250
+ * platform: 'web',
251
+ * tokenKey: 'token',
252
+ * plugins: ['page-view', 'click', 'page-stay', 'js-error', 'app-lifecycle', 'api-watch', 'scroll-depth'],
253
+ * legacy: true,
254
+ * getAuthHeaders: () => ({ Authorization: orgToken, token: orgToken }),
255
+ * })
256
+ */
257
+ function install(Vue, options) {
258
+ if (install._installed) return
259
+ install._installed = true
260
+
261
+ var tracker = createTracker(options || {})
262
+
263
+ // 0. 暴露 tracker 实例到 window(供验证脚本访问)
264
+ if (typeof window !== 'undefined') {
265
+ window.__tracker = tracker
266
+ }
267
+
268
+ // 0.1 设备注册(无 deviceId 时自动注册)
269
+ registerDeviceIfNeeded(tracker)
270
+
271
+ // 0.2 自动检测 userId(从 Cookie 或 storage 中读取已登录用户)
272
+ _ensureUserId(tracker)
273
+
274
+ // 0.3 注册 userId 刷新钩子:每次 flush 前从 storage 实时读取,
275
+ // 避免自动事件(page_view/click)在 userId 更新前用旧值发出
276
+ tracker.getQueue().setRefreshUserId(function () {
277
+ _ensureUserId(tracker)
278
+ })
279
+
280
+ // 1. 注册 $dadian(兼容现有所有业务调用)
281
+ var _rawDadian = createDadianProxy(tracker)
282
+ Vue.prototype.$dadian = function (frontUrl, postParam) {
283
+ _ensureUserId(tracker)
284
+ return _rawDadian(frontUrl, postParam)
285
+ }
286
+
287
+ // 2. 注册 $track(统一的上报方法,替代 $dadian)
288
+ // 行为与 $dadian 完全一致:第一个参数直接作为 frontUrl,不做任何路由拼接
289
+ Vue.prototype.$track = function (frontUrl, postParam) {
290
+ _ensureUserId(tracker)
291
+
292
+ // postParam 处理(与 $dadian 一致)
293
+ var postParamStr = ''
294
+ if (postParam && Object.keys(postParam).length > 0) {
295
+ var config = tracker.getConfig()
296
+ if (config.platform === 'weapp' && config.eventBus) {
297
+ var bus = config.eventBus
298
+ postParamStr = JSON.stringify(Object.assign({}, postParam, {
299
+ wxAppOnLaunch: bus.wxAppOnLaunch,
300
+ pageFrom: bus.pageFrom,
301
+ mallOrderExt: bus.mallOrderExt,
302
+ }))
303
+ } else {
304
+ postParamStr = JSON.stringify(postParam)
305
+ }
306
+ }
307
+
308
+ // 开关关(默认):旧接口也报一次(与 $dadian 一致)
309
+ var config = tracker.getConfig()
310
+ if (!config.useUnifiedReport) {
311
+ var header = { 'content-type': 'application/json' }
312
+ var hasLegacyAuth = false
313
+ if (typeof config.getAuthHeaders === 'function') {
314
+ var authHeaders = config.getAuthHeaders() || {}
315
+ if (authHeaders.Authorization || authHeaders.token || authHeaders['rsb-token']) {
316
+ hasLegacyAuth = true
317
+ Object.assign(header, authHeaders)
318
+ }
319
+ }
320
+ if (hasLegacyAuth) {
321
+ tracker.getQueue().pushLegacy(
322
+ config.dadianUrl + '/api/point/task/activeReport',
323
+ { frontUrl: frontUrl, postParam: postParamStr },
324
+ header
325
+ )
326
+ }
327
+ }
328
+
329
+ // SDK 事件入队(始终走)
330
+ tracker.track('custom', {
331
+ eventName: frontUrl || 'manual',
332
+ frontUrl: frontUrl,
333
+ postParam: postParamStr,
334
+ elementId: frontUrl,
335
+ })
336
+
337
+ // 项目定制:从 footer 导航类 frontUrl 中提取目标路由,
338
+ // 在 track() 之后更新 referrer 链(确保目标页事件能拿到正确来源页)
339
+ // 必须在 track 之后:否则当前事件的 referrer 会被提前污染
340
+ var rules = config.projectRules
341
+ if (rules && typeof rules.extractNavTarget === 'function') {
342
+ var navTarget = rules.extractNavTarget(null, frontUrl)
343
+ if (navTarget) {
344
+ tracker.updatePageRoute(navTarget)
345
+ }
346
+ }
347
+ }
348
+
349
+ // 3. 注册 $tracker 实例引用(含 resetSession 供登出时调用)
350
+ Vue.prototype.$tracker = tracker
351
+ Vue.prototype.$trackerResetSession = function () {
352
+ tracker.getSession().resetSession()
353
+ tracker.getQueue()._flushedEventIds.clear()
354
+ }
355
+
356
+ // 3.1 动态参数解析器快捷方式
357
+ Vue.prototype.$registerParam = function (name, fn) {
358
+ tracker.registerParam(name, fn)
359
+ }
360
+
361
+ // 4. 注册 v-impression 指令(兼容旧业务代码)
362
+ var adapter = tracker.getAdapter()
363
+ if (adapter.supportsIntersectionObserver()) {
364
+ Vue.directive('impression', createImpressionDirective(tracker))
365
+ }
366
+
367
+ // 5. 注册 v-track-exposure 指令(新的曝光指令,走自动采集链路)
368
+ var exposurePlugin = tracker.plugins.find(function (p) {
369
+ return p.name === 'exposure'
370
+ })
371
+ if (exposurePlugin && typeof exposurePlugin.createDirective === 'function') {
372
+ var trackExposureDirective = exposurePlugin.createDirective()
373
+ if (trackExposureDirective) {
374
+ Vue.directive('track-exposure', trackExposureDirective)
375
+ }
376
+ }
377
+
378
+ // 6. 注册页面停留 mixin
379
+ var hasPageStayPlugin = (options.plugins || []).some(function (p) {
380
+ return p === 'page-stay' || (p && p.name === 'page-stay')
381
+ })
382
+ if (hasPageStayPlugin) {
383
+ Vue.mixin(createPageStayMixin(tracker))
384
+ }
385
+
386
+ // 7. 注册 Vue 错误捕获(组件 render/lifecycle/watch 中的异常)
387
+ var jsErrorPlugin = tracker.plugins.find(function (p) {
388
+ return p.name === 'js-error'
389
+ })
390
+ if (jsErrorPlugin) {
391
+ var _prevErrorHandler = Vue.config.errorHandler
392
+ Vue.config.errorHandler = function (err, vm, info) {
393
+ jsErrorPlugin.handleVueError(err, vm, info)
394
+ if (_prevErrorHandler) _prevErrorHandler.call(this, err, vm, info)
395
+ }
396
+ }
397
+
398
+ // [PROJECT-ADAPT] 小程序生命周期注册
399
+ // H5 端通过 history API 监听路由变化,小程序端通过 onShow/onHide
400
+ if (tracker.getConfig().platform === 'weapp') {
401
+ var pageViewPlugin = tracker.plugins.find(function (p) {
402
+ return p.name === 'page-view'
403
+ })
404
+ var lifecyclePlugin = tracker.plugins.find(function (p) {
405
+ return p.name === 'app-lifecycle'
406
+ })
407
+
408
+ Vue.mixin({
409
+ onShow: function () {
410
+ if (pageViewPlugin) {
411
+ pageViewPlugin.trackPageShow()
412
+ }
413
+ if (lifecyclePlugin) {
414
+ lifecyclePlugin.trackAppShow()
415
+ }
416
+ },
417
+ onHide: function () {
418
+ if (lifecyclePlugin) {
419
+ lifecyclePlugin.trackAppHide()
420
+ }
421
+ },
422
+ })
423
+ }
424
+ }
425
+
426
+ // 默认导出 Vue 插件对象
427
+ export default {
428
+ install: install,
429
+ createTracker: createTracker,
430
+ getTracker: getTracker,
431
+ }
432
+
433
+ // 具名导出
434
+ export { Tracker } from './core/tracker.js'
435
+ export { WebAdapter } from './platforms/web.js'
436
+ export { WeappAdapter } from './platforms/weapp.js'
437
+ export { AppAdapter } from './platforms/app.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rk911/tracker-sdk",
3
- "version": "1.7.6",
3
+ "version": "1.7.7",
4
4
  "description": "人康多端埋点 SDK — 跨端自动化采集 + 手动埋点双写兼容",
5
5
  "main": "index.js",
6
6
  "module": "index.js",