@rk911/tracker-sdk 1.7.9 → 1.7.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/core/compat.js CHANGED
@@ -155,6 +155,8 @@ export function createPageStayMixin(tracker) {
155
155
  data: function () {
156
156
  return {
157
157
  _pageStayStartTime: 0,
158
+ _accumulatedDuration: 0,
159
+ _isStayPaused: false,
158
160
  _pageStayRoute: '',
159
161
  _pageStayTitle: '',
160
162
  }
@@ -188,6 +190,8 @@ export function createPageStayMixin(tracker) {
188
190
  }
189
191
  } catch (e) {}
190
192
  this._pageStayStartTime = Date.now()
193
+ this._accumulatedDuration = 0
194
+ this._isStayPaused = false
191
195
  this._pageShowReported = false
192
196
  },
193
197
 
@@ -201,15 +205,35 @@ export function createPageStayMixin(tracker) {
201
205
  }
202
206
  window.addEventListener('pageshow', this._pageshowHandler)
203
207
 
208
+ // 监听 tracker 的挂起与恢复事件(如 App 切后台、多 Tab/多 WebView 切换)
209
+ this._stayPauseHandler = function () {
210
+ if (!self._pageStayRoute) return
211
+ if (self._pageStayStartTime > 0) {
212
+ self._accumulatedDuration += (Date.now() - self._pageStayStartTime)
213
+ self._pageStayStartTime = 0
214
+ }
215
+ self._isStayPaused = true
216
+ }
217
+ this._stayResumeHandler = function () {
218
+ if (!self._pageStayRoute) return
219
+ if (self._isTopStayPage()) {
220
+ self._pageStayStartTime = Date.now()
221
+ self._isStayPaused = false
222
+ }
223
+ }
224
+ tracker.onPauseStay(this._stayPauseHandler)
225
+ tracker.onResumeStay(this._stayResumeHandler)
226
+
204
227
  // H5 下 app 前后台切换(visibilitychange)不总触发页面 onShow/onHide,
205
- // 切回前台后停留起点未重置 → 下次 onHide 因 start=0 早退 → 停留漏测。
206
- // 用 visibilitychange 兜底(app-lifecycle 已证明它可靠触发):
228
+ // 用 visibilitychange 兜底:
207
229
  // 仅栈顶页面响应,与 onShow/onHide 幂等共存(重复触发因 start=0 早退,不会重报)。
208
230
  if (typeof document !== 'undefined') {
209
231
  this._pageVisHandler = function () {
210
232
  if (!self._isTopStayPage()) return
211
233
  if (document.visibilityState === 'visible') {
212
- self._pageStayStartTime = Date.now()
234
+ if (!self._isStayPaused) {
235
+ self._pageStayStartTime = Date.now()
236
+ }
213
237
  } else if (document.visibilityState === 'hidden') {
214
238
  self._pageStayReport()
215
239
  }
@@ -221,6 +245,7 @@ export function createPageStayMixin(tracker) {
221
245
  onShow: function () {
222
246
  if (!this._pageStayRoute) return
223
247
  this._pageStayStartTime = Date.now()
248
+ this._isStayPaused = false
224
249
  if (!this._pageShowReported) {
225
250
  this._pageShowReported = true
226
251
  this._pageShowReport('onShow')
@@ -257,15 +282,20 @@ export function createPageStayMixin(tracker) {
257
282
  // 仅栈顶页响应,避免后台页面误报停留。
258
283
  _isTopStayPage: function () {
259
284
  try {
260
- var pages = getCurrentPages()
261
- if (!pages || !pages.length) return false
262
- var top = pages[pages.length - 1]
263
- var route = top && top.route
264
- ? (top.route.charAt(0) === '/' ? top.route : '/' + top.route)
265
- : ''
266
- return route === this._pageStayRoute
285
+ if (typeof getCurrentPages === 'function') {
286
+ var pages = getCurrentPages()
287
+ if (pages && pages.length) {
288
+ var top = pages[pages.length - 1]
289
+ var route = top && top.route
290
+ ? (top.route.charAt(0) === '/' ? top.route : '/' + top.route)
291
+ : ''
292
+ return route === this._pageStayRoute
293
+ }
294
+ }
295
+ var currentPath = tracker.getAdapter().getCurrentPagePath()
296
+ return currentPath === this._pageStayRoute
267
297
  } catch (e) {
268
- return false
298
+ return true
269
299
  }
270
300
  },
271
301
 
@@ -302,12 +332,18 @@ export function createPageStayMixin(tracker) {
302
332
  },
303
333
 
304
334
  _pageStayReport: function () {
305
- if (!this._pageStayStartTime) return
306
- var duration = Date.now() - this._pageStayStartTime
335
+ var duration = this._accumulatedDuration
336
+ if (this._pageStayStartTime > 0) {
337
+ duration += (Date.now() - this._pageStayStartTime)
338
+ }
307
339
  this._pageStayStartTime = 0
340
+ this._accumulatedDuration = 0
341
+ this._isStayPaused = false
342
+
343
+ if (duration <= 0) return
308
344
 
309
345
  var config = tracker.getConfig()
310
- // 最小停留阈值:iOS 前后台抖动会产生大量 1~2s 的碎停留,默认 2000ms 过滤噪音
346
+ // 最小停留阈值:iOS/切后台抖动会产生大量 1~2s 的碎停留,默认 500ms 过滤噪音
311
347
  var minDuration = (config.pageStay || {}).minStayDuration || 500
312
348
  if (duration < minDuration) return
313
349
 
package/core/session.js CHANGED
@@ -61,13 +61,20 @@ export class SessionManager {
61
61
  }
62
62
 
63
63
  setUser(userId, isNative = false) {
64
- this.userId = userId
64
+ if (!userId && this.userId && !isNative) {
65
+ // 防误置空:外部未准备好用户身份传空时,若内存中已有合法 userId,绝不置空;清空需调用 resetSession
66
+ return
67
+ }
68
+ this.userId = userId || null
65
69
  this.isNativeUserId = !!isNative
66
- // 纯 H5 登录才写入永久 localStorage;Native 注入仅保存在会话级 sessionStorage,防止 Native 退出登录后 H5 残留旧账号
67
- if (userId && !isNative) {
68
- this.adapter.setStorage('userId', userId)
70
+ if (userId) {
71
+ this.adapter.setStorage('_tracker_user_id', userId)
72
+ if (!isNative) {
73
+ this.adapter.setStorage('userId', userId)
74
+ }
69
75
  } else if (!userId && !isNative) {
70
76
  this.adapter.removeStorage('userId')
77
+ this.adapter.removeStorage('_tracker_user_id')
71
78
  }
72
79
  this._save()
73
80
  }
@@ -92,7 +99,7 @@ export class SessionManager {
92
99
  }
93
100
 
94
101
  setInitialSequence(seq) {
95
- if (typeof seq === 'number' && !isNaN(seq) && seq > this.sequence) {
102
+ if (typeof seq === 'number' && !isNaN(seq) && seq >= this.sequence) {
96
103
  this.sequence = seq
97
104
  this._save()
98
105
  }
@@ -152,13 +159,20 @@ export class SessionManager {
152
159
  this._createNew()
153
160
  }
154
161
  if (!this.userId) {
155
- this.userId = this.adapter.getStorage('userId') || this.adapter.getStorage('unionid') || null
162
+ this.userId = this.adapter.getStorage('userId')
163
+ || this.adapter.getStorage('unionid')
164
+ || this.adapter.getStorage('_tracker_user_id')
165
+ || null
156
166
  }
157
167
  }
158
168
 
159
169
  resetSession() {
160
170
  this.adapter.removeSessionStorage(this._storageKey)
171
+ this.adapter.removeStorage('_tracker_user_id')
161
172
  this.userId = null
173
+ this.isNativeUserId = false
174
+ this.isNativeSessionId = false
175
+ this.isNativeDeviceId = false
162
176
  this._createNew()
163
177
  }
164
178
 
package/core/tracker.js CHANGED
@@ -150,6 +150,7 @@ export class Tracker {
150
150
  eventName: eventData.eventName || eventType,
151
151
  timestamp: this.adapter.getTimestamp(),
152
152
  sequence: seq,
153
+ userId: 'userId' in eventData ? eventData.userId : (this.session.getUserId() || null),
153
154
  pageRoute: 'pageRoute' in eventData ? (eventData.pageRoute || null) : (this._currentPage || this.adapter.getCurrentPagePath()),
154
155
  pageTitle: 'pageTitle' in eventData ? (eventData.pageTitle || null) : (this.adapter.getPageTitle ? this.adapter.getPageTitle() : null),
155
156
  elementId: eventData.elementId || null,
@@ -162,7 +163,7 @@ export class Tracker {
162
163
  extKey: eventData.extKey || null,
163
164
  extValue: eventData.extValue || null,
164
165
  extIntValue: eventData.extIntValue || null,
165
- frontUrl: eventData.frontUrl || null,
166
+ frontUrl: eventData.frontUrl || (typeof window !== 'undefined' && window.location ? window.location.href : null),
166
167
  postParam: eventData.postParam != null && eventData.postParam !== ''
167
168
  ? (typeof eventData.postParam === 'string'
168
169
  ? eventData.postParam
@@ -293,6 +294,34 @@ export class Tracker {
293
294
  return this._referrer
294
295
  }
295
296
 
297
+ // ---- 页面停留挂起与恢复控制 ----
298
+
299
+ onPauseStay(fn) {
300
+ if (!this._stayPauseListeners) this._stayPauseListeners = []
301
+ if (typeof fn === 'function') this._stayPauseListeners.push(fn)
302
+ }
303
+
304
+ onResumeStay(fn) {
305
+ if (!this._stayResumeListeners) this._stayResumeListeners = []
306
+ if (typeof fn === 'function') this._stayResumeListeners.push(fn)
307
+ }
308
+
309
+ pausePageStay() {
310
+ if (this._stayPauseListeners) {
311
+ this._stayPauseListeners.forEach(function (fn) {
312
+ try { fn() } catch (e) {}
313
+ })
314
+ }
315
+ }
316
+
317
+ resumePageStay() {
318
+ if (this._stayResumeListeners) {
319
+ this._stayResumeListeners.forEach(function (fn) {
320
+ try { fn() } catch (e) {}
321
+ })
322
+ }
323
+ }
324
+
296
325
  // ---- 动态参数解析器 ----
297
326
 
298
327
  registerParam(name, fn) {
package/index.js CHANGED
@@ -199,9 +199,33 @@ function registerDeviceIfNeeded(tracker) {
199
199
  attempt(0)
200
200
  }
201
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
+
202
227
  /**
203
- * 自动检测 userId:每次从 localStorage/cookie 实时读取
204
- * 不依赖内存缓存,确保清除存储后不会残留旧用户、切换账号后能及时更新
228
+ * 自动检测 userId:多源交叉读取,并确保不把已有的合法身份置空
205
229
  */
206
230
  function _ensureUserId(tracker) {
207
231
  var session = tracker.getSession()
@@ -211,10 +235,47 @@ function _ensureUserId(tracker) {
211
235
  }
212
236
 
213
237
  var adapter = tracker.getAdapter()
214
- // 1. 优先读取单字段 userId 与 unionid
215
- var uid = adapter.getStorage('userId') || adapter.getStorage('unionid')
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
+ }
216
270
 
217
- // 2. 关键兼容:读取 uni-app 登录时持久化的 userInfo 复合对象
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 复合对象
218
279
  if (!uid) {
219
280
  try {
220
281
  var rawInfo = adapter.getStorage('userInfo')
@@ -227,43 +288,43 @@ function _ensureUserId(tracker) {
227
288
  } catch (e) {}
228
289
  }
229
290
 
230
- // 3. 从 cookie 兜底 rkfeb-userId
291
+ // 5. Cookie 兜底:rkfeb-userId 或 static-c-token (JWT payload)
231
292
  if (!uid && typeof document !== 'undefined') {
232
- var match = document.cookie.match(/(?:^|;\s*)rkfeb-userId=([^;]+)/)
233
- if (match) uid = decodeURIComponent(match[1])
234
- }
235
-
236
- // 4. 关键兜底:从 token / appToken (JWT payload) 中解析 unionid 或 userId
237
- if (!uid) {
238
293
  try {
239
- var token = adapter.getStorage('token') || adapter.getStorage('appToken')
240
- if (token && typeof token === 'string' && token.indexOf('.') > -1) {
241
- var tokenParts = token.split('.')
242
- if (tokenParts.length >= 2) {
243
- var base64Url = tokenParts[1]
244
- var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
245
- var decodedStr = ''
246
- if (typeof atob === 'function') {
247
- try {
248
- decodedStr = decodeURIComponent(escape(atob(base64)))
249
- } catch (e1) {
250
- decodedStr = atob(base64)
251
- }
252
- } else if (typeof Buffer !== 'undefined') {
253
- decodedStr = Buffer.from(base64, 'base64').toString('utf-8')
254
- }
255
- if (decodedStr) {
256
- var payload = JSON.parse(decodedStr)
257
- uid = payload.unionid || payload.userId || payload.uid || payload.sub
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
258
303
  }
259
304
  }
260
305
  }
261
306
  } catch (e) {}
262
307
  }
263
308
 
264
- var currentUid = tracker.getSession().getUserId()
265
- if (uid && uid !== currentUid) {
266
- tracker.setUser(uid)
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))
267
328
  }
268
329
  }
269
330
 
@@ -288,9 +349,11 @@ function install(Vue, options) {
288
349
 
289
350
  var tracker = createTracker(options || {})
290
351
 
291
- // 0. 暴露 tracker 实例到 window(供验证脚本访问)
352
+ // 0. 暴露 tracker 实例到 window(供原生容器及验证脚本访问)
292
353
  if (typeof window !== 'undefined') {
293
354
  window.__tracker = tracker
355
+ window.__tracker_pauseStay = function () { tracker.pausePageStay() }
356
+ window.__tracker_resumeStay = function () { tracker.resumePageStay() }
294
357
  }
295
358
 
296
359
  // 0.1 设备注册(无 deviceId 时自动注册)
@@ -45,6 +45,7 @@ import java.net.URL;
45
45
  import java.util.ArrayList;
46
46
  import java.util.HashMap;
47
47
  import java.util.List;
48
+ import java.util.Iterator;
48
49
  import java.util.Map;
49
50
 
50
51
  /**
@@ -85,19 +86,32 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
85
86
  private String referrerPageRoute = "";
86
87
  private String lastClickKey = "";
87
88
  private long lastClickTime = 0;
89
+ private String lastLegacyClickKey = "";
90
+ private long lastLegacyClickTime = 0;
88
91
 
89
92
  private static final Map<String, String> NATIVE_TITLE_MAP = new HashMap<>();
90
93
  static {
91
94
  NATIVE_TITLE_MAP.put("com.renrui.main.activity.MainActivity", "APP主页");
92
95
  NATIVE_TITLE_MAP.put("com.renrui.main.fragment.MainTabFragment", "首页Tab");
93
- NATIVE_TITLE_MAP.put("com.renrui.main.fragment.MainRecommentFragment", "推荐精选/视频");
96
+ NATIVE_TITLE_MAP.put("com.renrui.main.fragment.MainRecommentFragment", "推荐精选/短视频");
94
97
  NATIVE_TITLE_MAP.put("com.renrui.main.fragment.MainWebFragment", "首页Web容器");
95
98
  NATIVE_TITLE_MAP.put("com.renrui.mine.fragment.MineFragment", "个人中心/我的");
96
99
  NATIVE_TITLE_MAP.put("com.renrui.mine.activity.MineWebActivity", "我的Web容器");
97
100
  NATIVE_TITLE_MAP.put("com.renrui.mine.fragment.MineWebFragment", "我的Web页面");
101
+ NATIVE_TITLE_MAP.put("com.renrui.wz.activity.PageActivity", "APP主页");
98
102
  NATIVE_TITLE_MAP.put("com.renrui.wz.activity.SplashActivity", "启动闪屏页");
103
+ NATIVE_TITLE_MAP.put("com.renrui.wz.activity.LoginActivity", "登录页");
104
+ NATIVE_TITLE_MAP.put("com.renrui.wz.activity.LoginPhoneActivity", "手机号验证码登录");
105
+ NATIVE_TITLE_MAP.put("com.dongsh.zxinglibrary.android.CaptureActivity", "扫码页");
99
106
  NATIVE_TITLE_MAP.put("com.renrui.web.CommonWebActivity", "通用H5容器");
100
107
  NATIVE_TITLE_MAP.put("com.renrui.web.CommonWebFragment", "通用H5页面");
108
+ NATIVE_TITLE_MAP.put("com.renrui.home.fragment.HomeFragment", "首页短视频");
109
+ NATIVE_TITLE_MAP.put("com.renrui.home.fragment.HomeReCommendFragment", "小视频推荐");
110
+ NATIVE_TITLE_MAP.put("com.renrui.home.activity.TikTokActivity", "小视频首页");
111
+ NATIVE_TITLE_MAP.put("com.renrui.home.activity.HomePlayletHistoryActivity", "短剧历史记录");
112
+ NATIVE_TITLE_MAP.put("com.renrui.home.fragment.HomePlayletHistoryFragment", "短剧历史列表");
113
+ NATIVE_TITLE_MAP.put("com.renrui.home.activity.HomeVideoPlayActivity", "短视频全屏播放页");
114
+ NATIVE_TITLE_MAP.put("com.renrui.home.activity.HomePlayletPlayActivity", "短剧播放页");
101
115
  }
102
116
 
103
117
  public static String getFriendlyPageTitle(String className) {
@@ -109,8 +123,11 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
109
123
  if (lastDot >= 0 && lastDot < className.length() - 1) {
110
124
  String simple = className.substring(lastDot + 1);
111
125
  if (simple.contains("MainTab")) return "首页/主Tab";
112
- if (simple.contains("Recomment")) return "推荐精选";
126
+ if (simple.contains("Recomment") || simple.contains("ReCommend") || simple.contains("Recommend")) return "小视频推荐";
113
127
  if (simple.contains("Mine")) return "个人中心";
128
+ if (simple.contains("PlayletHistory")) return "短剧历史记录";
129
+ if (simple.contains("PlayletPlay")) return "短剧播放页";
130
+ if (simple.contains("VideoPlay")) return "短视频全屏播放页";
114
131
  if (simple.contains("Splash")) return "启动闪屏页";
115
132
  if (simple.contains("Login")) return "登录页";
116
133
  if (simple.contains("Web")) return "Web视图";
@@ -296,6 +313,58 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
296
313
  /**
297
314
  * 手动自定义埋点 (Custom Tracking)
298
315
  */
316
+ /**
317
+ * 视频全生命周期行为埋点 (短视频起播、滑走、互动)
318
+ * @param eventName 动作名: video_play_start, video_play_end, video_like, video_unlike, video_share, video_collect 等
319
+ * @param videoId 视频ID
320
+ * @param videoTitle 视频标题 (直接赋予 elementText,保证看板直观可读)
321
+ * @param customProperties 扩展参数 (分类、作者、播放时长等)
322
+ */
323
+ public void trackVideo(final String eventName, final long videoId, final String videoTitle, final JSONObject customProperties) {
324
+ postBgTask(new Runnable() {
325
+ @Override
326
+ public void run() {
327
+ try {
328
+ JSONObject event = createBaseEvent("video", eventName);
329
+ event.put("elementId", "video_" + videoId);
330
+ if (!TextUtils.isEmpty(videoTitle)) {
331
+ event.put("elementText", videoTitle);
332
+ } else {
333
+ event.put("elementText", "video_" + videoId);
334
+ }
335
+ event.put("pageTitle", "小视频推荐");
336
+
337
+ JSONObject extra = new JSONObject();
338
+ String baseExtraStr = event.optString("extraData");
339
+ if (!TextUtils.isEmpty(baseExtraStr)) {
340
+ try {
341
+ extra = new JSONObject(baseExtraStr);
342
+ } catch (Exception ignored) {}
343
+ }
344
+ extra.put("videoId", videoId);
345
+ if (!TextUtils.isEmpty(videoTitle)) {
346
+ extra.put("videoTitle", videoTitle);
347
+ }
348
+ if (customProperties != null) {
349
+ Iterator<String> keys = customProperties.keys();
350
+ while (keys.hasNext()) {
351
+ String k = keys.next();
352
+ extra.put(k, customProperties.opt(k));
353
+ }
354
+ }
355
+ if (customProperties != null && customProperties.has("playDurationMs")) {
356
+ event.put("duration", customProperties.optLong("playDurationMs"));
357
+ }
358
+ event.put("extraData", extra.toString());
359
+ enqueueEvent(event);
360
+ flushAsync();
361
+ } catch (Exception e) {
362
+ Log.e(TAG, "===> [Native trackVideo Failed]", e);
363
+ }
364
+ }
365
+ });
366
+ }
367
+
299
368
  public void track(final String eventName, final JSONObject customProperties) {
300
369
  postBgTask(new Runnable() {
301
370
  @Override
@@ -318,6 +387,14 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
318
387
  * 旧 $dadian 点位兼容方法 (自动匹配后端双写 point.log)
319
388
  */
320
389
  public void trackFrontUrl(final String frontUrl, final String elementId, final String postParam) {
390
+ final String key = (frontUrl != null ? frontUrl : "") + "|" + (elementId != null ? elementId : "");
391
+ final long now = System.currentTimeMillis();
392
+ if (key.equals(lastLegacyClickKey) && (now - lastLegacyClickTime < 500)) {
393
+ return;
394
+ }
395
+ lastLegacyClickKey = key;
396
+ lastLegacyClickTime = now;
397
+
321
398
  postBgTask(new Runnable() {
322
399
  @Override
323
400
  public void run() {
@@ -409,6 +486,32 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
409
486
  }
410
487
  }
411
488
 
489
+ public String getNetworkType() {
490
+ try {
491
+ if (application == null) return "";
492
+ android.net.ConnectivityManager cm = (android.net.ConnectivityManager) application.getSystemService(Context.CONNECTIVITY_SERVICE);
493
+ if (cm == null) return "";
494
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
495
+ android.net.Network activeNetwork = cm.getActiveNetwork();
496
+ if (activeNetwork == null) return "";
497
+ android.net.NetworkCapabilities caps = cm.getNetworkCapabilities(activeNetwork);
498
+ if (caps == null) return "";
499
+ if (caps.hasTransport(android.net.NetworkCapabilities.TRANSPORT_WIFI)) return "wifi";
500
+ if (caps.hasTransport(android.net.NetworkCapabilities.TRANSPORT_CELLULAR)) return "4g";
501
+ if (caps.hasTransport(android.net.NetworkCapabilities.TRANSPORT_ETHERNET)) return "ethernet";
502
+ return "other";
503
+ } else {
504
+ android.net.NetworkInfo info = cm.getActiveNetworkInfo();
505
+ if (info == null || !info.isConnected()) return "";
506
+ if (info.getType() == android.net.ConnectivityManager.TYPE_WIFI) return "wifi";
507
+ if (info.getType() == android.net.ConnectivityManager.TYPE_MOBILE) return "4g";
508
+ return "other";
509
+ }
510
+ } catch (Throwable e) {
511
+ return "";
512
+ }
513
+ }
514
+
412
515
  private JSONObject createBaseEvent(String eventType, String eventName) throws Exception {
413
516
  sequence++;
414
517
  JSONObject event = new JSONObject();
@@ -418,6 +521,10 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
418
521
  event.put("timestamp", System.currentTimeMillis());
419
522
  event.put("sequence", sequence);
420
523
  event.put("pageRoute", currentPageRoute);
524
+ String autoTitle = getFriendlyPageTitle(currentPageRoute);
525
+ if (!TextUtils.isEmpty(autoTitle)) {
526
+ event.put("pageTitle", autoTitle);
527
+ }
421
528
  if (!TextUtils.isEmpty(referrerPageRoute)) {
422
529
  event.put("referrer", referrerPageRoute);
423
530
  }
@@ -425,6 +532,10 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
425
532
  event.put("deviceBrand", android.os.Build.BRAND != null ? android.os.Build.BRAND : "");
426
533
  event.put("deviceModel", android.os.Build.MODEL != null ? android.os.Build.MODEL : "");
427
534
  event.put("osVersion", android.os.Build.VERSION.RELEASE != null ? android.os.Build.VERSION.RELEASE : "");
535
+ String net = getNetworkType();
536
+ if (!TextUtils.isEmpty(net)) {
537
+ event.put("networkType", net);
538
+ }
428
539
 
429
540
  JSONObject extra = new JSONObject();
430
541
  if (!TextUtils.isEmpty(phone)) extra.put("phone", phone);
@@ -642,6 +753,10 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
642
753
  @Override
643
754
  public void onFragmentResumed(FragmentManager fm, Fragment f) {
644
755
  super.onFragmentResumed(fm, f);
756
+ // 过滤嵌套子 Fragment(如 ViewPager 内的 ChildFragment),由主 Fragment 统一承担页面路由和时长计算,防止同屏惊群割裂卡片
757
+ if (f.getParentFragment() != null) {
758
+ return;
759
+ }
645
760
  final String fragmentRoute = f.getClass().getName();
646
761
  postBgTask(new Runnable() {
647
762
  @Override
@@ -665,6 +780,35 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
665
780
  }
666
781
  });
667
782
  }
783
+
784
+ @Override
785
+ public void onFragmentPaused(FragmentManager fm, Fragment f) {
786
+ super.onFragmentPaused(fm, f);
787
+ if (f.getParentFragment() != null) {
788
+ return;
789
+ }
790
+ final String fragmentRoute = f.getClass().getName();
791
+ postBgTask(new Runnable() {
792
+ @Override
793
+ public void run() {
794
+ try {
795
+ if (fragmentRoute.equals(currentPageRoute) && lastPageStartTime > 0) {
796
+ long duration = System.currentTimeMillis() - lastPageStartTime;
797
+ if (duration >= 500) {
798
+ JSONObject event = createBaseEvent("page_stay", "page_stay");
799
+ event.put("duration", duration);
800
+ event.put("pageTitle", getFriendlyPageTitle(fragmentRoute));
801
+ enqueueEvent(event);
802
+ flushAsync();
803
+ }
804
+ lastPageStartTime = 0;
805
+ }
806
+ } catch (Exception e) {
807
+ Log.e(TAG, "===> [Native Fragment Paused Error]", e);
808
+ }
809
+ }
810
+ });
811
+ }
668
812
  }, true);
669
813
  }
670
814
 
@@ -846,7 +990,11 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
846
990
  Log.i(TAG, "===> [Native Activity Resumed] Page: " + currentPageRoute + ", Referrer: " + referrerPageRoute);
847
991
  JSONObject event = createBaseEvent("page_view", "page_view");
848
992
  String activityTitle = activity.getTitle() != null ? activity.getTitle().toString() : "";
849
- event.put("pageTitle", !TextUtils.isEmpty(activityTitle) ? activityTitle : getFriendlyPageTitle(route));
993
+ String friendly = getFriendlyPageTitle(route);
994
+ if (TextUtils.isEmpty(activityTitle) || "银友圈".equals(activityTitle)) {
995
+ activityTitle = friendly;
996
+ }
997
+ event.put("pageTitle", activityTitle);
850
998
  enqueueEvent(event);
851
999
  flushAsync();
852
1000
  } catch (Exception e) {
@@ -867,6 +1015,9 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
867
1015
  if (duration >= 500) {
868
1016
  JSONObject event = createBaseEvent("page_stay", "page_stay");
869
1017
  event.put("duration", duration);
1018
+ String actTitle = activity.getTitle() != null ? activity.getTitle().toString() : "";
1019
+ String friendly = getFriendlyPageTitle(activity.getClass().getName());
1020
+ event.put("pageTitle", !TextUtils.isEmpty(friendly) ? friendly : actTitle);
870
1021
  enqueueEvent(event);
871
1022
  flushAsync();
872
1023
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rk911/tracker-sdk",
3
- "version": "1.7.9",
3
+ "version": "1.7.10",
4
4
  "description": "人康多端埋点 SDK — 跨端自动化采集 + 手动埋点双写兼容",
5
5
  "main": "index.js",
6
6
  "module": "index.js",
package/platforms/web.js CHANGED
@@ -44,21 +44,50 @@ export class WebAdapter {
44
44
 
45
45
  getStorage(key) {
46
46
  try {
47
- return uni.getStorageSync(key)
48
- } catch (e) {
49
- return null
50
- }
47
+ if (typeof uni !== 'undefined' && typeof uni.getStorageSync === 'function') {
48
+ const val = uni.getStorageSync(key)
49
+ if (val !== undefined && val !== null && val !== '') return val
50
+ }
51
+ } catch (e) {}
52
+ try {
53
+ if (typeof localStorage !== 'undefined') {
54
+ const val = localStorage.getItem(key)
55
+ if (val !== null && val !== undefined && val !== '') {
56
+ try {
57
+ return JSON.parse(val)
58
+ } catch (e1) {
59
+ return val
60
+ }
61
+ }
62
+ }
63
+ } catch (e) {}
64
+ return null
51
65
  }
52
66
 
53
67
  setStorage(key, value) {
54
68
  try {
55
- uni.setStorageSync(key, value)
69
+ if (typeof uni !== 'undefined' && typeof uni.setStorageSync === 'function') {
70
+ uni.setStorageSync(key, value)
71
+ }
72
+ } catch (e) {}
73
+ try {
74
+ if (typeof localStorage !== 'undefined') {
75
+ const strVal = typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value)
76
+ localStorage.setItem(key, strVal)
77
+ }
56
78
  } catch (e) {}
57
79
  }
58
80
 
59
81
  removeStorage(key) {
60
82
  try {
61
- uni.removeStorageSync(key)
83
+ if (typeof uni !== 'undefined' && typeof uni.removeStorageSync === 'function') {
84
+ uni.removeStorageSync(key)
85
+ }
86
+ } catch (e) {}
87
+ try {
88
+ if (typeof localStorage !== 'undefined') {
89
+ localStorage.removeItem(key)
90
+ }
62
91
  } catch (e) {}
63
92
  }
64
93
 
@@ -193,7 +222,21 @@ export class WebAdapter {
193
222
  }
194
223
 
195
224
  getToken() {
196
- return this.getStorage(this.config.tokenKey || 'token')
225
+ var key = this.config.tokenKey || 'token'
226
+ var t = this.getStorage(key)
227
+ if (t) return t
228
+ var candidates = ['token', 'Token', 'appToken', 'rsc-token', 'rsb-token', 'Authorization']
229
+ for (var i = 0; i < candidates.length; i++) {
230
+ var candidate = candidates[i]
231
+ if (candidate === key) continue
232
+ t = this.getStorage(candidate)
233
+ if (t) return t
234
+ }
235
+ if (typeof document !== 'undefined') {
236
+ var match = document.cookie.match(/(?:^|;\s*)(?:token|static-c-token|appToken)=([^;]+)/)
237
+ if (match) return decodeURIComponent(match[1])
238
+ }
239
+ return null
197
240
  }
198
241
 
199
242
  // [PROJECT-ADAPT] 网络请求实现
@@ -234,9 +277,17 @@ export class WebAdapter {
234
277
  // ---- 网络状态 ----
235
278
 
236
279
  getNetworkType() {
280
+ try {
281
+ if (typeof window !== 'undefined' && window.NativeBridge && typeof window.NativeBridge.getNetworkType === 'function') {
282
+ const net = window.NativeBridge.getNetworkType()
283
+ if (net) return net
284
+ }
285
+ } catch (e) {}
237
286
  try {
238
287
  if (typeof navigator !== 'undefined' && navigator.connection) {
239
- return navigator.connection.effectiveType || 'unknown'
288
+ if (navigator.connection.type && navigator.connection.type !== 'unknown') {
289
+ return navigator.connection.type
290
+ }
240
291
  }
241
292
  } catch (e) {}
242
293
  return typeof navigator !== 'undefined' && navigator.onLine ? 'wifi' : 'offline'
@@ -246,15 +297,20 @@ export class WebAdapter {
246
297
 
247
298
  getDeviceInfo() {
248
299
  try {
249
- const info = uni.getSystemInfoSync()
300
+ const info = (typeof uni !== 'undefined' && typeof uni.getSystemInfoSync === 'function')
301
+ ? (uni.getSystemInfoSync() || {})
302
+ : {}
250
303
  var ua = (typeof navigator !== 'undefined' ? navigator.userAgent : '').toLowerCase()
251
304
  var rawBrand = info.brand || this._judgeBrand(ua, info.model) || ''
252
305
  var brand = rawBrand.toUpperCase() === 'OPPO' ? 'OPPO' : rawBrand
306
+ var rawSystem = info.system || ''
307
+ // 去除重复的 "Android " 前缀,与 Native 端对齐为纯版本号(如 "16")
308
+ var cleanSystem = rawSystem.replace(/^Android\s*/i, '').trim() || rawSystem
253
309
  return {
254
310
  brand: brand,
255
311
  model: info.model || '',
256
312
  platform: (info.platform || '').toLowerCase() === 'android' ? 'Android' : (info.platform || ''),
257
- system: (info.system || '').toLowerCase().startsWith('android') ? ('Android ' + (info.system.split(' ')[1] || '')) : (info.system || ''),
313
+ system: cleanSystem,
258
314
  screenWidth: info.screenWidth || 0,
259
315
  screenHeight: info.screenHeight || 0,
260
316
  userAgent:
@@ -99,9 +99,9 @@ export class PageViewPlugin {
99
99
  // 锁定进页时刻的时间戳(避免被后方的 title 异步重试链延迟 100~500ms)
100
100
  var entryTimestamp = adapter.getTimestamp()
101
101
 
102
- // 防抖:同路由 500ms 内不重复上报(防框架路由重入导致的竞态)
102
+ // 防抖:同路由 800ms 内不重复上报(防框架路由重入或组件双重挂载导致的竞态)
103
103
  var now = Date.now()
104
- if (this._lastFiredRoute === currentPage && now - this._lastFiredTime < 500) return
104
+ if (this._lastFiredRoute === currentPage && now - this._lastFiredTime < 800) return
105
105
 
106
106
  this._lastPage = currentPage
107
107
  this._lastFiredRoute = currentPage