@rk911/tracker-sdk 1.7.2 → 1.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/core/session.js CHANGED
@@ -63,8 +63,11 @@ export class SessionManager {
63
63
  setUser(userId, isNative = false) {
64
64
  this.userId = userId
65
65
  this.isNativeUserId = !!isNative
66
+ // 纯 H5 登录才写入永久 localStorage;Native 注入仅保存在会话级 sessionStorage,防止 Native 退出登录后 H5 残留旧账号
66
67
  if (userId && !isNative) {
67
68
  this.adapter.setStorage('userId', userId)
69
+ } else if (!userId && !isNative) {
70
+ this.adapter.removeStorage('userId')
68
71
  }
69
72
  this._save()
70
73
  }
@@ -73,10 +76,8 @@ export class SessionManager {
73
76
  if (deviceId) {
74
77
  this.deviceId = deviceId
75
78
  this.isNativeDeviceId = !!isNative
76
- if (!isNative) {
77
- this.adapter.setStorage(DEVICE_ID_KEY, deviceId)
78
- this.adapter.setStorage('deviceId', deviceId)
79
- }
79
+ this.adapter.setStorage(DEVICE_ID_KEY, deviceId)
80
+ this.adapter.setStorage('deviceId', deviceId)
80
81
  this._save()
81
82
  }
82
83
  }
@@ -90,6 +91,13 @@ export class SessionManager {
90
91
  }
91
92
  }
92
93
 
94
+ setInitialSequence(seq) {
95
+ if (typeof seq === 'number' && !isNaN(seq) && seq > this.sequence) {
96
+ this.sequence = seq
97
+ this._save()
98
+ }
99
+ }
100
+
93
101
  getUserId() {
94
102
  return this.userId
95
103
  }
@@ -119,24 +127,33 @@ export class SessionManager {
119
127
  lastActivityTime: this.lastActivityTime,
120
128
  sequence: this.sequence,
121
129
  isNativeSessionId: !!this.isNativeSessionId,
130
+ userId: this.userId,
131
+ isNativeUserId: !!this.isNativeUserId,
122
132
  }
123
133
  this.adapter.setSessionStorage(this._storageKey, data)
124
134
  }
125
135
 
126
136
  _restore() {
127
137
  var data = this.adapter.getSessionStorage(this._storageKey)
128
- if (data && data.sessionId && !data.isNativeSessionId) {
138
+ if (data && data.sessionId) {
129
139
  this.sessionId = data.sessionId
130
140
  this.startTime = data.startTime
131
141
  this.lastActivityTime = data.lastActivityTime
132
142
  this.sequence = data.sequence || 0
133
-
143
+ this.isNativeSessionId = !!data.isNativeSessionId
144
+ this.isNativeUserId = !!data.isNativeUserId
145
+ if (data.userId) {
146
+ this.userId = data.userId
147
+ }
134
148
  if (this._isExpired()) {
135
149
  this._createNew()
136
150
  }
137
151
  } else {
138
152
  this._createNew()
139
153
  }
154
+ if (!this.userId) {
155
+ this.userId = this.adapter.getStorage('userId') || null
156
+ }
140
157
  }
141
158
 
142
159
  resetSession() {
package/core/tracker.js CHANGED
@@ -40,16 +40,25 @@ export class Tracker {
40
40
 
41
41
  _getDeviceFields() {
42
42
  var info = this.adapter.getDeviceInfo() || {}
43
+ if (this._overrideDeviceInfo) {
44
+ info = Object.assign({}, info, this._overrideDeviceInfo)
45
+ }
43
46
  return {
44
- deviceBrand: info.brand || null,
45
- deviceModel: info.model || null,
47
+ deviceBrand: info.brand || info.deviceBrand || null,
48
+ deviceModel: info.model || info.deviceModel || null,
46
49
  os: info.platform || info.os || null,
47
- osVersion: info.system || null,
50
+ osVersion: info.system || info.osVersion || null,
48
51
  screenWidth: info.screenWidth || null,
49
52
  screenHeight: info.screenHeight || null,
50
53
  }
51
54
  }
52
55
 
56
+ setDeviceInfo(info) {
57
+ if (info && typeof info === 'object') {
58
+ this._overrideDeviceInfo = Object.assign(this._overrideDeviceInfo || {}, info)
59
+ }
60
+ }
61
+
53
62
  // ---- 插件管理 ----
54
63
 
55
64
  use(plugin) {
@@ -96,6 +105,17 @@ export class Tracker {
96
105
  this._referrer = this._currentPage
97
106
  }
98
107
  this._currentPage = declaredPage
108
+
109
+ // 跨端信息携带:当在 Android/iOS 原生容器内运行时,将 H5 页面路由与标题同步给端上
110
+ if (typeof window !== 'undefined' && window.NativeBridge && window.NativeBridge.postMessage) {
111
+ try {
112
+ window.NativeBridge.postMessage('tracker.syncPageState', JSON.stringify({
113
+ pageRoute: declaredPage,
114
+ pageTitle: eventData.pageTitle || '',
115
+ timestamp: Date.now()
116
+ }), '')
117
+ } catch (e) {}
118
+ }
99
119
  }
100
120
 
101
121
  if (!this._currentPage) {
@@ -202,6 +222,14 @@ export class Tracker {
202
222
 
203
223
  setUser(userId, isNative = false) {
204
224
  this.session.setUser(userId, isNative)
225
+ // 跨端信息携带:当在原生 WebView 登录或切换用户时,将身份变更同步到 Native 端
226
+ if (!isNative && typeof window !== 'undefined' && window.NativeBridge && window.NativeBridge.postMessage) {
227
+ try {
228
+ window.NativeBridge.postMessage('tracker.syncUserIdentity', JSON.stringify({
229
+ userId: userId
230
+ }), '')
231
+ } catch (e) {}
232
+ }
205
233
  }
206
234
 
207
235
  setDeviceId(deviceId, isNative = false) {
@@ -212,6 +240,12 @@ export class Tracker {
212
240
  this.session.setSessionId(sessionId, isNative)
213
241
  }
214
242
 
243
+ setInitialSequence(seq) {
244
+ if (this.session && typeof this.session.setInitialSequence === 'function') {
245
+ this.session.setInitialSequence(seq)
246
+ }
247
+ }
248
+
215
249
  getDeviceId() {
216
250
  return this.session.getDeviceId()
217
251
  }
package/index.js CHANGED
@@ -221,8 +221,8 @@ function _ensureUserId(tracker) {
221
221
  var currentUid = tracker.getSession().getUserId()
222
222
  if (uid && uid !== currentUid) {
223
223
  tracker.setUser(uid)
224
- } else if (!uid && currentUid) {
225
- // 源头已清空(退出登录/清除缓存),同步清除内存中的旧值
224
+ } else if (!uid && currentUid && !session.isNativeUserId) {
225
+ // 仅针对纯 H5 登录场景:若源头已清空(退出登录/清除缓存),同步清除内存中的旧值
226
226
  tracker.setUser(null)
227
227
  }
228
228
  }
@@ -43,7 +43,9 @@ import java.io.OutputStream;
43
43
  import java.net.HttpURLConnection;
44
44
  import java.net.URL;
45
45
  import java.util.ArrayList;
46
+ import java.util.HashMap;
46
47
  import java.util.List;
48
+ import java.util.Map;
47
49
 
48
50
  /**
49
51
  * 行业级 Native Android Tracker SDK
@@ -71,7 +73,7 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
71
73
  private volatile String userId;
72
74
  private volatile String phone;
73
75
  private volatile String storeName;
74
- private int sequence = 0;
76
+ private volatile int sequence = 0;
75
77
 
76
78
  private HandlerThread bgThread;
77
79
  private Handler bgHandler;
@@ -84,6 +86,39 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
84
86
  private String lastClickKey = "";
85
87
  private long lastClickTime = 0;
86
88
 
89
+ private static final Map<String, String> NATIVE_TITLE_MAP = new HashMap<>();
90
+ static {
91
+ NATIVE_TITLE_MAP.put("com.renrui.main.activity.MainActivity", "APP主页");
92
+ NATIVE_TITLE_MAP.put("com.renrui.main.fragment.MainTabFragment", "首页Tab");
93
+ NATIVE_TITLE_MAP.put("com.renrui.main.fragment.MainRecommentFragment", "推荐精选/视频");
94
+ NATIVE_TITLE_MAP.put("com.renrui.main.fragment.MainWebFragment", "首页Web容器");
95
+ NATIVE_TITLE_MAP.put("com.renrui.mine.fragment.MineFragment", "个人中心/我的");
96
+ NATIVE_TITLE_MAP.put("com.renrui.mine.activity.MineWebActivity", "我的Web容器");
97
+ NATIVE_TITLE_MAP.put("com.renrui.mine.fragment.MineWebFragment", "我的Web页面");
98
+ NATIVE_TITLE_MAP.put("com.renrui.wz.activity.SplashActivity", "启动闪屏页");
99
+ NATIVE_TITLE_MAP.put("com.renrui.web.CommonWebActivity", "通用H5容器");
100
+ NATIVE_TITLE_MAP.put("com.renrui.web.CommonWebFragment", "通用H5页面");
101
+ }
102
+
103
+ public static String getFriendlyPageTitle(String className) {
104
+ if (TextUtils.isEmpty(className)) return "";
105
+ if (NATIVE_TITLE_MAP.containsKey(className)) {
106
+ return NATIVE_TITLE_MAP.get(className);
107
+ }
108
+ int lastDot = className.lastIndexOf('.');
109
+ if (lastDot >= 0 && lastDot < className.length() - 1) {
110
+ String simple = className.substring(lastDot + 1);
111
+ if (simple.contains("MainTab")) return "首页/主Tab";
112
+ if (simple.contains("Recomment")) return "推荐精选";
113
+ if (simple.contains("Mine")) return "个人中心";
114
+ if (simple.contains("Splash")) return "启动闪屏页";
115
+ if (simple.contains("Login")) return "登录页";
116
+ if (simple.contains("Web")) return "Web视图";
117
+ return simple;
118
+ }
119
+ return className;
120
+ }
121
+
87
122
  private static class EventRecord {
88
123
  final long id;
89
124
  final JSONObject json;
@@ -193,7 +228,37 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
193
228
  return deviceId;
194
229
  }
195
230
 
231
+ public void setUserId(String userId) {
232
+ updateUser(userId, null, null);
233
+ }
234
+
235
+ public synchronized int getCurrentSequence() {
236
+ return sequence;
237
+ }
238
+
239
+ /**
240
+ * H5 路由状态同步 (由 CommonWebPresenter 通过 NativeBridge 触发)
241
+ */
242
+ public void updateH5PageState(final String h5Route, final String h5Title) {
243
+ postBgTask(new Runnable() {
244
+ @Override
245
+ public void run() {
246
+ try {
247
+ if (TextUtils.isEmpty(h5Route)) return;
248
+ String referrer = currentPageRoute;
249
+ currentPageRoute = h5Route;
250
+ referrerPageRoute = referrer;
251
+ lastPageStartTime = System.currentTimeMillis();
252
+ Log.i(TAG, "===> [H5 Page State Synced to Native] Route: " + currentPageRoute + ", Title: " + h5Title + ", Referrer: " + referrerPageRoute);
253
+ } catch (Exception e) {
254
+ Log.e(TAG, "===> [updateH5PageState Error]", e);
255
+ }
256
+ }
257
+ });
258
+ }
259
+
196
260
  public void resetSession() {
261
+ this.sequence = 0;
197
262
  this.sessionId = "android_" + System.currentTimeMillis() + "_" + (int) (Math.random() * 10000);
198
263
  Log.i(TAG, "===> [Native Tracker Session Reset] New SessionId: " + this.sessionId);
199
264
  }
@@ -270,6 +335,80 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
270
335
  });
271
336
  }
272
337
 
338
+ /**
339
+ * 接口请求与异常自动化上报 (S6 API Tracking - 0 阻塞、异步持久化)
340
+ */
341
+ public void trackApi(final String url, final String method, final int statusCode,
342
+ final long duration, final String reqParam, final String respData, final String error) {
343
+ if (TextUtils.isEmpty(url)) return;
344
+ // 严格防递归:SDK 自身上报地址直接跳过
345
+ if (url.contains("/api/point/task/trackBatch") || (serverUrl != null && url.contains(serverUrl))) {
346
+ return;
347
+ }
348
+
349
+ postBgTask(new Runnable() {
350
+ @Override
351
+ public void run() {
352
+ try {
353
+ String eventType = (statusCode >= 400 || !TextUtils.isEmpty(error)) ? "api_error" : "api_request";
354
+ // 聚合优化:提取纯净的 API 路由路径作为 eventName(例如 /app/user/getUserInfo),方便在报表按接口聚合
355
+ String apiPath = extractApiPath(url);
356
+ JSONObject event = createBaseEvent(eventType, apiPath);
357
+
358
+ JSONObject extra = new JSONObject();
359
+ extra.put("url", url);
360
+ extra.put("apiPath", apiPath);
361
+ extra.put("method", method != null ? method.toUpperCase() : "GET");
362
+ extra.put("statusCode", statusCode);
363
+ extra.put("duration", duration);
364
+
365
+ // 字段容量防膨胀安全截断:防止过大 JSON 导致 SQLite CursorWindow 溢出或 HTTP 413
366
+ if (!TextUtils.isEmpty(reqParam)) {
367
+ extra.put("requestParams", reqParam.length() > 2048 ? reqParam.substring(0, 2048) : reqParam);
368
+ }
369
+ if (!TextUtils.isEmpty(respData)) {
370
+ extra.put("responseData", respData.length() > 2048 ? respData.substring(0, 2048) : respData);
371
+ }
372
+ if (!TextUtils.isEmpty(error)) {
373
+ extra.put("error", error.length() > 512 ? error.substring(0, 512) : error);
374
+ }
375
+ if (!TextUtils.isEmpty(phone)) extra.put("phone", phone);
376
+ if (!TextUtils.isEmpty(storeName)) extra.put("storeName", storeName);
377
+ if (!TextUtils.isEmpty(userId)) extra.put("userId", userId);
378
+ extra.put("platform", "android");
379
+
380
+ event.put("extraData", extra.toString());
381
+ enqueueEvent(event);
382
+
383
+ // 接口异常立即上报排查;正常接口由 3s 定时器批量聚合上报
384
+ if ("api_error".equals(eventType)) {
385
+ flushAsync();
386
+ }
387
+ } catch (Exception e) {
388
+ Log.e(TAG, "===> [Native Tracker trackApi Failed]", e);
389
+ }
390
+ }
391
+ });
392
+ }
393
+
394
+ private static String extractApiPath(String url) {
395
+ if (TextUtils.isEmpty(url)) return "";
396
+ try {
397
+ int qIdx = url.indexOf('?');
398
+ String clean = qIdx >= 0 ? url.substring(0, qIdx) : url;
399
+ int schemeIdx = clean.indexOf("://");
400
+ if (schemeIdx >= 0) {
401
+ int pathIdx = clean.indexOf('/', schemeIdx + 3);
402
+ if (pathIdx >= 0) {
403
+ return clean.substring(pathIdx);
404
+ }
405
+ }
406
+ return clean;
407
+ } catch (Exception e) {
408
+ return url;
409
+ }
410
+ }
411
+
273
412
  private JSONObject createBaseEvent(String eventType, String eventName) throws Exception {
274
413
  sequence++;
275
414
  JSONObject event = new JSONObject();
@@ -283,6 +422,9 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
283
422
  event.put("referrer", referrerPageRoute);
284
423
  }
285
424
  event.put("os", "Android");
425
+ event.put("deviceBrand", android.os.Build.BRAND != null ? android.os.Build.BRAND : "");
426
+ event.put("deviceModel", android.os.Build.MODEL != null ? android.os.Build.MODEL : "");
427
+ event.put("osVersion", android.os.Build.VERSION.RELEASE != null ? android.os.Build.VERSION.RELEASE : "");
286
428
 
287
429
  JSONObject extra = new JSONObject();
288
430
  if (!TextUtils.isEmpty(phone)) extra.put("phone", phone);
@@ -340,12 +482,24 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
340
482
  }
341
483
 
342
484
  public void flushSync() {
485
+ if (bgThread != null && Thread.currentThread() == bgThread) {
486
+ flushInternal();
487
+ return;
488
+ }
489
+ final java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(1);
343
490
  postBgTask(new Runnable() {
344
491
  @Override
345
492
  public void run() {
346
- flushInternal();
493
+ try {
494
+ flushInternal();
495
+ } finally {
496
+ latch.countDown();
497
+ }
347
498
  }
348
499
  });
500
+ try {
501
+ latch.await(1500, java.util.concurrent.TimeUnit.MILLISECONDS);
502
+ } catch (InterruptedException ignored) {}
349
503
  }
350
504
 
351
505
  private void flushInternal() {
@@ -493,13 +647,16 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
493
647
  @Override
494
648
  public void run() {
495
649
  try {
650
+ if (fragmentRoute.equals(currentPageRoute) && (System.currentTimeMillis() - lastPageStartTime < 500)) {
651
+ return;
652
+ }
496
653
  String referrer = currentPageRoute;
497
654
  currentPageRoute = fragmentRoute;
498
655
  referrerPageRoute = referrer;
499
656
  lastPageStartTime = System.currentTimeMillis();
500
657
  Log.i(TAG, "===> [Native Fragment Resumed] Page: " + currentPageRoute + ", Referrer: " + referrerPageRoute);
501
658
  JSONObject event = createBaseEvent("page_view", "page_view");
502
- event.put("pageTitle", currentPageRoute);
659
+ event.put("pageTitle", getFriendlyPageTitle(fragmentRoute));
503
660
  enqueueEvent(event);
504
661
  flushAsync();
505
662
  } catch (Exception e) {
@@ -516,7 +673,9 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
516
673
  Window window = activity.getWindow();
517
674
  if (window != null) {
518
675
  final Window.Callback origCb = window.getCallback();
519
- window.setCallback(new WindowCallbackWrapper(origCb, activity));
676
+ if (origCb != null && !(origCb instanceof WindowCallbackWrapper)) {
677
+ window.setCallback(new WindowCallbackWrapper(origCb, activity));
678
+ }
520
679
  }
521
680
  } catch (Exception e) {
522
681
  Log.w(TAG, "===> [Native Window Callback Install Failed]", e);
@@ -558,14 +717,57 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
558
717
  return root;
559
718
  }
560
719
 
561
- private void handleNativeViewClick(final Activity activity, final View target, final float rawX, final float rawY) {
562
- final String elementText;
563
- if (target instanceof TextView) {
564
- elementText = ((TextView) target).getText().toString().trim();
565
- } else {
566
- elementText = "";
567
- }
720
+ private String extractViewText(View view) {
721
+ if (view == null) return "";
722
+ try {
723
+ // 1. 优先读取 contentDescription
724
+ CharSequence desc = view.getContentDescription();
725
+ if (!TextUtils.isEmpty(desc)) {
726
+ return desc.toString().trim();
727
+ }
728
+ // 2. 本身是 TextView / Button
729
+ if (view instanceof TextView) {
730
+ CharSequence text = ((TextView) view).getText();
731
+ if (!TextUtils.isEmpty(text)) {
732
+ return text.toString().trim();
733
+ }
734
+ }
735
+ // 3. ViewGroup 穿透提取子 View 文本
736
+ if (view instanceof ViewGroup) {
737
+ ViewGroup group = (ViewGroup) view;
738
+ int count = group.getChildCount();
739
+ for (int i = 0; i < count; i++) {
740
+ View child = group.getChildAt(i);
741
+ if (child != null && child.getVisibility() == View.VISIBLE) {
742
+ String childText = extractViewText(child);
743
+ if (!TextUtils.isEmpty(childText)) {
744
+ return childText;
745
+ }
746
+ }
747
+ }
748
+ }
749
+ } catch (Exception e) {}
750
+ return "";
751
+ }
568
752
 
753
+ private String getPresetTextForElementId(String elementId) {
754
+ if (TextUtils.isEmpty(elementId)) return "";
755
+ String lower = elementId.toLowerCase();
756
+ if (lower.contains("to_top") || lower.contains("scroll_top")) return "置顶";
757
+ if (!lower.contains("background") && !lower.contains("feedback") && !lower.contains("fallback") && !lower.contains("callback")
758
+ && (lower.startsWith("back") || lower.endsWith("back") || lower.contains("_back") || lower.contains("back_") || lower.equals("back"))) return "返回";
759
+ if (lower.contains("close") || lower.contains("cloess") || lower.contains("dismiss")) return "关闭";
760
+ if (lower.contains("search")) return "搜索";
761
+ if (lower.contains("refresh")) return "刷新";
762
+ if (lower.contains("share")) return "分享";
763
+ if (lower.contains("more")) return "更多";
764
+ if (lower.contains("award")) return "领奖/关注";
765
+ if ((lower.contains("_phone") || lower.contains("phone_") || lower.startsWith("phone") || lower.endsWith("phone") || lower.contains("mobile"))
766
+ && !lower.contains("headphone") && !lower.contains("microphone")) return "手机号";
767
+ return "";
768
+ }
769
+
770
+ private void handleNativeViewClick(final Activity activity, final View target, final float rawX, final float rawY) {
569
771
  final String elementId;
570
772
  if (target.getId() != View.NO_ID) {
571
773
  String resName = "";
@@ -577,6 +779,22 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
577
779
  elementId = target.getClass().getSimpleName();
578
780
  }
579
781
 
782
+ String extracted = extractViewText(target);
783
+ if (TextUtils.isEmpty(extracted)) {
784
+ // 1. 优先使用知名图标 ID 映射(解决置顶、返回、关闭等图标抓取不到或被误抓的问题)
785
+ String preset = getPresetTextForElementId(elementId);
786
+ if (!TextUtils.isEmpty(preset)) {
787
+ extracted = preset;
788
+ } else if (target.getParent() instanceof ViewGroup) {
789
+ ViewGroup parentGroup = (ViewGroup) target.getParent();
790
+ // 2. 严格限制父容器:仅在直接小容器(子 View <= 4 且高度不超过 400px)内检索,禁止向整屏根容器穿透扩散
791
+ if (parentGroup.getChildCount() <= 4 && parentGroup.getHeight() < 400) {
792
+ extracted = extractViewText(parentGroup);
793
+ }
794
+ }
795
+ }
796
+ final String elementText = extracted;
797
+
580
798
  final String key = elementId + "|" + elementText;
581
799
  final long now = System.currentTimeMillis();
582
800
  if (key.equals(lastClickKey) && (now - lastClickTime < 800)) {
@@ -617,13 +835,18 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
617
835
  @Override
618
836
  public void run() {
619
837
  try {
838
+ final String route = activity.getClass().getName();
839
+ if (route.equals(currentPageRoute) && (System.currentTimeMillis() - lastPageStartTime < 500)) {
840
+ return;
841
+ }
620
842
  String referrer = currentPageRoute;
621
- currentPageRoute = activity.getClass().getName();
843
+ currentPageRoute = route;
622
844
  referrerPageRoute = referrer;
623
845
  lastPageStartTime = System.currentTimeMillis();
624
846
  Log.i(TAG, "===> [Native Activity Resumed] Page: " + currentPageRoute + ", Referrer: " + referrerPageRoute);
625
847
  JSONObject event = createBaseEvent("page_view", "page_view");
626
- event.put("pageTitle", activity.getTitle() != null ? activity.getTitle().toString() : currentPageRoute);
848
+ String activityTitle = activity.getTitle() != null ? activity.getTitle().toString() : "";
849
+ event.put("pageTitle", !TextUtils.isEmpty(activityTitle) ? activityTitle : getFriendlyPageTitle(route));
627
850
  enqueueEvent(event);
628
851
  flushAsync();
629
852
  } catch (Exception e) {
@@ -641,11 +864,13 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
641
864
  try {
642
865
  if (lastPageStartTime > 0) {
643
866
  long duration = System.currentTimeMillis() - lastPageStartTime;
644
- JSONObject event = createBaseEvent("page_stay", "page_stay");
645
- event.put("duration", duration);
646
- enqueueEvent(event);
867
+ if (duration >= 500) {
868
+ JSONObject event = createBaseEvent("page_stay", "page_stay");
869
+ event.put("duration", duration);
870
+ enqueueEvent(event);
871
+ flushAsync();
872
+ }
647
873
  lastPageStartTime = 0;
648
- flushAsync();
649
874
  }
650
875
  } catch (Exception e) {
651
876
  Log.e(TAG, "===> [Native Tracker onActivityPaused Error]", e);
@@ -662,14 +887,14 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
662
887
  // --- Window.Callback 代理包装器 (带 S3 滑动防误触过滤) ---
663
888
  private class WindowCallbackWrapper implements Window.Callback {
664
889
  private final Window.Callback mWrapped;
665
- private final Activity mActivity;
890
+ private final java.lang.ref.WeakReference<Activity> mActivityRef;
666
891
  private float downX, downY;
667
892
  private long downTime;
668
893
  private int touchSlop = -1;
669
894
 
670
895
  public WindowCallbackWrapper(Window.Callback wrapped, Activity activity) {
671
896
  this.mWrapped = wrapped;
672
- this.mActivity = activity;
897
+ this.mActivityRef = new java.lang.ref.WeakReference<>(activity);
673
898
  if (activity != null) {
674
899
  try {
675
900
  this.touchSlop = ViewConfiguration.get(activity).getScaledTouchSlop();
@@ -693,8 +918,9 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
693
918
  int slopThreshold = touchSlop > 0 ? touchSlop : 24;
694
919
  if (distance <= slopThreshold && duration < 1500) {
695
920
  try {
696
- if (mActivity != null && mActivity.getWindow() != null) {
697
- View decorView = mActivity.getWindow().getDecorView();
921
+ Activity act = mActivityRef != null ? mActivityRef.get() : null;
922
+ if (act != null && act.getWindow() != null) {
923
+ View decorView = act.getWindow().getDecorView();
698
924
  View leaf = findTouchedLeafView(decorView, (int) event.getRawX(), (int) event.getRawY());
699
925
  View clickableTarget = null;
700
926
  View curr = leaf;
@@ -707,7 +933,7 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
707
933
  curr = (View) curr.getParent();
708
934
  }
709
935
  if (clickableTarget != null) {
710
- handleNativeViewClick(mActivity, clickableTarget, event.getRawX(), event.getRawY());
936
+ handleNativeViewClick(act, clickableTarget, event.getRawX(), event.getRawY());
711
937
  }
712
938
  }
713
939
  } catch (Exception e) {
@@ -715,30 +941,30 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
715
941
  }
716
942
  }
717
943
  }
718
- return mWrapped.dispatchTouchEvent(event);
944
+ return mWrapped != null ? mWrapped.dispatchTouchEvent(event) : false;
719
945
  }
720
946
 
721
- @Override public boolean dispatchKeyEvent(KeyEvent event) { return mWrapped.dispatchKeyEvent(event); }
722
- @Override public boolean dispatchKeyShortcutEvent(KeyEvent event) { return mWrapped.dispatchKeyShortcutEvent(event); }
723
- @Override public boolean dispatchTrackballEvent(MotionEvent event) { return mWrapped.dispatchTrackballEvent(event); }
724
- @Override public boolean dispatchGenericMotionEvent(MotionEvent event) { return mWrapped.dispatchGenericMotionEvent(event); }
725
- @Override public boolean dispatchPopulateAccessibilityEvent(android.view.accessibility.AccessibilityEvent event) { return mWrapped.dispatchPopulateAccessibilityEvent(event); }
726
- @Nullable @Override public View onCreatePanelView(int featureId) { return mWrapped.onCreatePanelView(featureId); }
727
- @Override public boolean onCreatePanelMenu(int featureId, @NonNull Menu menu) { return mWrapped.onCreatePanelMenu(featureId, menu); }
728
- @Override public boolean onPreparePanel(int featureId, @Nullable View view, @NonNull Menu menu) { return mWrapped.onPreparePanel(featureId, view, menu); }
729
- @Override public boolean onMenuOpened(int featureId, @NonNull Menu menu) { return mWrapped.onMenuOpened(featureId, menu); }
730
- @Override public boolean onMenuItemSelected(int featureId, @NonNull MenuItem item) { return mWrapped.onMenuItemSelected(featureId, item); }
731
- @Override public void onWindowAttributesChanged(WindowManager.LayoutParams attrs) { mWrapped.onWindowAttributesChanged(attrs); }
732
- @Override public void onContentChanged() { mWrapped.onContentChanged(); }
733
- @Override public void onWindowFocusChanged(boolean hasFocus) { mWrapped.onWindowFocusChanged(hasFocus); }
734
- @Override public void onAttachedToWindow() { mWrapped.onAttachedToWindow(); }
735
- @Override public void onDetachedFromWindow() { mWrapped.onDetachedFromWindow(); }
736
- @Override public void onPanelClosed(int featureId, @NonNull Menu menu) { mWrapped.onPanelClosed(featureId, menu); }
737
- @Override public boolean onSearchRequested() { return mWrapped.onSearchRequested(); }
738
- @Override public boolean onSearchRequested(SearchEvent searchEvent) { return mWrapped.onSearchRequested(searchEvent); }
739
- @Nullable @Override public ActionMode onWindowStartingActionMode(ActionMode.Callback callback) { return mWrapped.onWindowStartingActionMode(callback); }
740
- @Nullable @Override public ActionMode onWindowStartingActionMode(ActionMode.Callback callback, int type) { return mWrapped.onWindowStartingActionMode(callback, type); }
741
- @Override public void onActionModeStarted(ActionMode mode) { mWrapped.onActionModeStarted(mode); }
742
- @Override public void onActionModeFinished(ActionMode mode) { mWrapped.onActionModeFinished(mode); }
947
+ @Override public boolean dispatchKeyEvent(KeyEvent event) { return mWrapped != null && mWrapped.dispatchKeyEvent(event); }
948
+ @Override public boolean dispatchKeyShortcutEvent(KeyEvent event) { return mWrapped != null && mWrapped.dispatchKeyShortcutEvent(event); }
949
+ @Override public boolean dispatchTrackballEvent(MotionEvent event) { return mWrapped != null && mWrapped.dispatchTrackballEvent(event); }
950
+ @Override public boolean dispatchGenericMotionEvent(MotionEvent event) { return mWrapped != null && mWrapped.dispatchGenericMotionEvent(event); }
951
+ @Override public boolean dispatchPopulateAccessibilityEvent(android.view.accessibility.AccessibilityEvent event) { return mWrapped != null && mWrapped.dispatchPopulateAccessibilityEvent(event); }
952
+ @Nullable @Override public View onCreatePanelView(int featureId) { return mWrapped != null ? mWrapped.onCreatePanelView(featureId) : null; }
953
+ @Override public boolean onCreatePanelMenu(int featureId, @NonNull Menu menu) { return mWrapped != null && mWrapped.onCreatePanelMenu(featureId, menu); }
954
+ @Override public boolean onPreparePanel(int featureId, @Nullable View view, @NonNull Menu menu) { return mWrapped != null && mWrapped.onPreparePanel(featureId, view, menu); }
955
+ @Override public boolean onMenuOpened(int featureId, @NonNull Menu menu) { return mWrapped != null && mWrapped.onMenuOpened(featureId, menu); }
956
+ @Override public boolean onMenuItemSelected(int featureId, @NonNull MenuItem item) { return mWrapped != null && mWrapped.onMenuItemSelected(featureId, item); }
957
+ @Override public void onWindowAttributesChanged(WindowManager.LayoutParams attrs) { if (mWrapped != null) mWrapped.onWindowAttributesChanged(attrs); }
958
+ @Override public void onContentChanged() { if (mWrapped != null) mWrapped.onContentChanged(); }
959
+ @Override public void onWindowFocusChanged(boolean hasFocus) { if (mWrapped != null) mWrapped.onWindowFocusChanged(hasFocus); }
960
+ @Override public void onAttachedToWindow() { if (mWrapped != null) mWrapped.onAttachedToWindow(); }
961
+ @Override public void onDetachedFromWindow() { if (mWrapped != null) mWrapped.onDetachedFromWindow(); }
962
+ @Override public void onPanelClosed(int featureId, @NonNull Menu menu) { if (mWrapped != null) mWrapped.onPanelClosed(featureId, menu); }
963
+ @Override public boolean onSearchRequested() { return mWrapped != null && mWrapped.onSearchRequested(); }
964
+ @Override public boolean onSearchRequested(SearchEvent searchEvent) { return mWrapped != null && mWrapped.onSearchRequested(searchEvent); }
965
+ @Nullable @Override public ActionMode onWindowStartingActionMode(ActionMode.Callback callback) { return mWrapped != null ? mWrapped.onWindowStartingActionMode(callback) : null; }
966
+ @Nullable @Override public ActionMode onWindowStartingActionMode(ActionMode.Callback callback, int type) { return mWrapped != null ? mWrapped.onWindowStartingActionMode(callback, type) : null; }
967
+ @Override public void onActionModeStarted(ActionMode mode) { if (mWrapped != null) mWrapped.onActionModeStarted(mode); }
968
+ @Override public void onActionModeFinished(ActionMode mode) { if (mWrapped != null) mWrapped.onActionModeFinished(mode); }
743
969
  }
744
970
  }
@@ -0,0 +1,134 @@
1
+ package com.renrui.tracker;
2
+
3
+ import android.text.TextUtils;
4
+ import android.util.Log;
5
+
6
+ import java.io.IOException;
7
+ import java.nio.charset.Charset;
8
+ import java.nio.charset.StandardCharsets;
9
+
10
+ import okhttp3.Interceptor;
11
+ import okhttp3.MediaType;
12
+ import okhttp3.Request;
13
+ import okhttp3.RequestBody;
14
+ import okhttp3.Response;
15
+ import okhttp3.ResponseBody;
16
+ import okio.Buffer;
17
+
18
+ /**
19
+ * 行业级 Native OkHttp 埋点无侵入网络拦截器
20
+ * 1. 0 侵入:仅安全只读提取(peekBody),绝对不消耗或关闭原始 Stream
21
+ * 2. 0 阻塞:主线程 0ms 开销,数据采集后直接抛给 HandlerThread 异步队列
22
+ * 3. 严格防递归:过滤 SDK 自身上报 URL,杜绝死循环
23
+ * 4. 异常原样抛出:记录超时/断网 api_error 后原样 throw e,不破坏原有业务逻辑
24
+ */
25
+ public class TrackerInterceptor implements Interceptor {
26
+
27
+ private static final String TAG = "TrackerInterceptor";
28
+ private static final int MAX_PARAM_LENGTH = 4096;
29
+ private static final int MAX_RESP_PREVIEW_LENGTH = 2048;
30
+
31
+ @Override
32
+ public Response intercept(Chain chain) throws IOException {
33
+ Request request = chain.request();
34
+ String url = request.url() != null ? request.url().toString() : "";
35
+
36
+ // 1. 过滤自身打点接口,防止递归死循环
37
+ if (isTrackerInternalUrl(url)) {
38
+ return chain.proceed(request);
39
+ }
40
+
41
+ String reqParam = extractRequestParams(request);
42
+ long startTime = System.currentTimeMillis();
43
+ Response response;
44
+
45
+ try {
46
+ response = chain.proceed(request);
47
+ } catch (IOException e) {
48
+ // 网络异常(如超时、无法连接),采集并原样抛出;过滤客户端主动 cancel 引起的异常
49
+ long duration = System.currentTimeMillis() - startTime;
50
+ boolean isCanceled = "Canceled".equalsIgnoreCase(e.getMessage());
51
+ if (!isCanceled) {
52
+ try {
53
+ AndroidTracker.getInstance().trackApi(url, request.method(), 0, duration, reqParam, null, e.getMessage());
54
+ } catch (Exception ex) {
55
+ Log.e(TAG, "trackApi error", ex);
56
+ }
57
+ }
58
+ throw e;
59
+ }
60
+
61
+ long duration = System.currentTimeMillis() - startTime;
62
+ String respData = null;
63
+
64
+ // 2. 使用 OkHttp 官方推荐的 peekBody 做安全无损预览(不破坏业务层 response.body() 流)
65
+ try {
66
+ if (response.body() != null) {
67
+ MediaType mediaType = response.body().contentType();
68
+ if (mediaType != null) {
69
+ String sub = mediaType.subtype();
70
+ if (sub != null && (sub.contains("json") || sub.contains("text") || sub.contains("xml") || sub.contains("html"))) {
71
+ ResponseBody peek = response.peekBody(MAX_RESP_PREVIEW_LENGTH);
72
+ respData = peek.string();
73
+ }
74
+ }
75
+ }
76
+ } catch (Throwable ignored) {}
77
+
78
+ // 3. 异步送入 AndroidTracker,后台 HandlerThread 处理,0 阻塞当前网络线程
79
+ try {
80
+ AndroidTracker.getInstance().trackApi(url, request.method(), response.code(), duration, reqParam, respData, null);
81
+ } catch (Exception e) {
82
+ Log.e(TAG, "trackApi error", e);
83
+ }
84
+
85
+ return response;
86
+ }
87
+
88
+ private boolean isTrackerInternalUrl(String url) {
89
+ if (TextUtils.isEmpty(url)) return true;
90
+ String lower = url.toLowerCase();
91
+ return lower.contains("/api/point/task/trackBatch")
92
+ || lower.contains("/point/task/trackBatch")
93
+ || lower.contains("/point/task/activeReport")
94
+ || lower.contains("/api/point/task/activeReport")
95
+ || lower.contains("/api/track/collect")
96
+ || lower.contains(".png")
97
+ || lower.contains(".jpg")
98
+ || lower.contains(".jpeg")
99
+ || lower.contains(".webp")
100
+ || lower.contains(".gif")
101
+ || lower.contains(".mp4")
102
+ || lower.contains(".svg");
103
+ }
104
+
105
+ private String extractRequestParams(Request request) {
106
+ try {
107
+ if ("GET".equalsIgnoreCase(request.method())) {
108
+ String query = request.url().query();
109
+ return !TextUtils.isEmpty(query) ? query : "";
110
+ }
111
+ RequestBody body = request.body();
112
+ if (body != null && body.contentLength() > 0 && body.contentLength() <= MAX_PARAM_LENGTH) {
113
+ MediaType contentType = body.contentType();
114
+ if (contentType != null) {
115
+ String type = contentType.type();
116
+ String sub = contentType.subtype();
117
+ if ("multipart".equalsIgnoreCase(type) || "image".equalsIgnoreCase(type)
118
+ || "video".equalsIgnoreCase(type) || "audio".equalsIgnoreCase(type)
119
+ || (sub != null && sub.contains("octet-stream"))) {
120
+ return "[binary/multipart: " + body.contentLength() + " bytes]";
121
+ }
122
+ }
123
+ Buffer buffer = new Buffer();
124
+ body.writeTo(buffer);
125
+ Charset charset = StandardCharsets.UTF_8;
126
+ if (contentType != null) {
127
+ charset = contentType.charset(StandardCharsets.UTF_8);
128
+ }
129
+ return buffer.readString(charset != null ? charset : StandardCharsets.UTF_8);
130
+ }
131
+ } catch (Throwable ignored) {}
132
+ return "";
133
+ }
134
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rk911/tracker-sdk",
3
- "version": "1.7.2",
3
+ "version": "1.7.4",
4
4
  "description": "人康多端埋点 SDK — 跨端自动化采集 + 手动埋点双写兼容",
5
5
  "main": "index.js",
6
6
  "module": "index.js",
package/platforms/app.js CHANGED
@@ -25,9 +25,17 @@ export class AppAdapter extends WebAdapter {
25
25
  // 尝试从 JSBridge 获取更多原生设备信息
26
26
  try {
27
27
  if (typeof window !== 'undefined' && window.NativeBridge && window.NativeBridge.getDeviceInfo) {
28
- const nativeInfo = window.NativeBridge.getDeviceInfo()
29
- if (nativeInfo) {
30
- return { ...baseInfo, ...nativeInfo }
28
+ let nativeInfo = window.NativeBridge.getDeviceInfo()
29
+ if (typeof nativeInfo === 'string') {
30
+ try { nativeInfo = JSON.parse(nativeInfo) } catch (e) {}
31
+ }
32
+ if (nativeInfo && typeof nativeInfo === 'object') {
33
+ return {
34
+ ...baseInfo,
35
+ ...nativeInfo,
36
+ brand: nativeInfo.brand || nativeInfo.deviceBrand || baseInfo.brand,
37
+ model: nativeInfo.model || nativeInfo.deviceModel || baseInfo.model,
38
+ }
31
39
  }
32
40
  }
33
41
  } catch (e) {}
package/platforms/web.js CHANGED
@@ -1,3 +1,20 @@
1
+ export const ROUTE_TITLE_MAP = {
2
+ '/pages/circle/circleOfFriends/details': '圈子动态详情',
3
+ '/pages/circle/index': '银友圈',
4
+ '/pages/circle/circleOfFriends/sendFriends/sendFriends': '发布动态',
5
+ '/pages/circle/circleOfFriends/sendFriends/circleClass/circleClass': '圈子分类',
6
+ '/healthFu/index/index': '健康福/AI助手',
7
+ '/pages/mall/goods/goodsList': '商品列表',
8
+ '/pages/mall/goods/goodsDetail': '商品详情',
9
+ '/pages/mall/goods/order': '商品下单',
10
+ '/pages/my/store/index': '我的小店',
11
+ '/pages/my/store/withdrawalList': '提现记录',
12
+ '/pages/integralMall/integralMall': '积分商城',
13
+ '/pages/integralMall/integralGoodsInfo': '积分商品详情',
14
+ '/pages/my/setting/setting': '设置',
15
+ '/pages/selfInvoice/index': '自助开票',
16
+ }
17
+
1
18
  export class WebAdapter {
2
19
  constructor(config) {
3
20
  this.config = config
@@ -149,12 +166,16 @@ export class WebAdapter {
149
166
  return path === '/' ? '' : path.replace(/^\//, '')
150
167
  }
151
168
 
152
- getPageTitle() {
169
+ getPageTitle(route) {
153
170
  try {
154
- if (typeof document !== 'undefined' && document.title) {
171
+ if (typeof document !== 'undefined' && document.title && document.title !== 'uni-app') {
155
172
  return document.title
156
173
  }
157
174
  } catch (e) {}
175
+ var r = route || this.getCurrentPagePath()
176
+ if (r && ROUTE_TITLE_MAP[r]) {
177
+ return ROUTE_TITLE_MAP[r]
178
+ }
158
179
  return null
159
180
  }
160
181
 
@@ -217,8 +238,8 @@ export class WebAdapter {
217
238
  return {
218
239
  brand: info.brand || this._judgeBrand(ua, info.model),
219
240
  model: info.model || '',
220
- platform: info.platform || '',
221
- system: info.system || '',
241
+ platform: (info.platform || '').toLowerCase() === 'android' ? 'Android' : (info.platform || ''),
242
+ system: (info.system || '').toLowerCase().startsWith('android') ? ('Android ' + (info.system.split(' ')[1] || '')) : (info.system || ''),
222
243
  screenWidth: info.screenWidth || 0,
223
244
  screenHeight: info.screenHeight || 0,
224
245
  userAgent:
@@ -365,27 +386,39 @@ export class WebAdapter {
365
386
  }
366
387
 
367
388
  // [PROJECT-ADAPT] 设备品牌/类型识别
368
- // 当前偏向中国市场手机品牌,国际化项目需扩展
389
+ // 全面覆盖中国市场主流手机品牌(OPPO/一加/真我、vivo/iQOO、小米/Redmi、华为/荣耀、三星等)
369
390
  // 返回的 deviceType 数值需与后台枚举对齐
370
391
  _judgeBrand(ua, model) {
371
392
  if (ua.indexOf('iphone') > -1) return 'iPhone'
372
- if (ua.indexOf('huawei') > -1 || ua.indexOf('honor') > -1) return 'Huawei'
373
- if (ua.indexOf('oppo') > -1 || ua.indexOf('pacm00') > -1) return 'Oppo'
374
- if (ua.indexOf('vivo') > -1) return 'vivo'
393
+ if (ua.indexOf('huawei') > -1 || ua.indexOf('honor') > -1 || ua.indexOf('harmonyos') > -1) return 'Huawei'
394
+ if (ua.indexOf('oppo') > -1 || ua.indexOf('pacm00') > -1 || ua.indexOf('heytap') > -1 ||
395
+ ua.indexOf('oplus') > -1 || ua.indexOf('oneplus') > -1 || ua.indexOf('realme') > -1) return 'Oppo'
396
+ if (ua.indexOf('vivo') > -1 || ua.indexOf('iqoo') > -1) return 'vivo'
375
397
  // 精确匹配小米,避免裸子串 'mi' 命中微信/企业微信 UA 里的 "MicroMessenger"
376
- // (曾把微信内所有非小米安卓机误判成小米)。/\bmi[\s-]?\d/ 匹配 "Mi 11"/"MI9",不匹配 "micromessenger"
377
398
  if (ua.indexOf('xiaomi') > -1 || ua.indexOf('redmi') > -1 ||
378
399
  ua.indexOf('miui') > -1 || /\bmi[\s-]?\d/.test(ua)) return 'mi'
379
400
  if (ua.indexOf('sm-') > -1) return 'Samsung'
380
401
  if (ua.indexOf('lg-') > -1 || ua.indexOf('lm-') > -1) return 'LG'
381
402
 
382
- // UA 未命中时按设备型号前缀兜底识别(微信 webview 常拿不到 UA 品牌,但 uni 能给型号)
403
+ // UA 未命中时按设备型号前缀兜底识别(WebView 常拿不到 UA 品牌,但原生/容器能给型号)
383
404
  var m = (model || '').toString().toUpperCase()
384
405
  if (m) {
385
- if (/^V\d{3,}/.test(m)) return 'vivo' // vivo: V2283A / V2055A ...
386
- if (/^(PA|PB|PC|PD|PE|CPH)/.test(m)) return 'Oppo' // oppo/一加型号前缀
387
- if (/^(MI |MI-|M\d{4}|2\d{6})/.test(m) || m.indexOf('REDMI') > -1) return 'mi'
388
- if (/^(ELS|NOH|LIO|ANA|TAS|VOG|BRQ)/.test(m)) return 'Huawei'
406
+ // OPPO / 一加 / Realme:
407
+ // 国内型号格式多为 P + 字母 + 数字 (如 PKV110, PHY110, PJE110, PGEM10, PFUM10, PBAM00, PACM00 等)
408
+ // 海外型号多为 CPH + 数字,Realme 多为 RMX + 数字
409
+ if (/^P[A-Z]{1,2}\d{2,}/.test(m) || /^(CPH|RMX)\d{3,}/.test(m) ||
410
+ m.indexOf('OPPO') > -1 || m.indexOf('ONEPLUS') > -1 || m.indexOf('REALME') > -1) {
411
+ return 'Oppo'
412
+ }
413
+ // vivo / iQOO:
414
+ if (/^V\d{3,}/.test(m) || m.indexOf('VIVO') > -1 || m.indexOf('IQOO') > -1) return 'vivo'
415
+ // 小米 / Redmi:
416
+ if (/^(MI |MI-|M\d{4}|2\d{4,})/.test(m) || m.indexOf('REDMI') > -1 || m.indexOf('XIAOMI') > -1) return 'mi'
417
+ // 华为 / 荣耀:
418
+ if (/^(ELS|NOH|LIO|ANA|TAS|VOG|BRQ|ALN|HBN|PGT|FRI|MAA|ANY|BND)/.test(m) ||
419
+ m.indexOf('HUAWEI') > -1 || m.indexOf('HONOR') > -1) {
420
+ return 'Huawei'
421
+ }
389
422
  if (m.indexOf('SM-') === 0) return 'Samsung'
390
423
  }
391
424
  return '其他型号手机'
@@ -50,44 +50,149 @@ export class ApiWatchPlugin {
50
50
  var self = this
51
51
  var config = this.tracker.getConfig()
52
52
 
53
- try {
54
- uni.addInterceptor('request', {
55
- invoke: function (args) {
56
- // 给请求打上开始时间标记
57
- args._trackStartTime = Date.now()
58
- args._trackBgCount = self._bgCount
59
- args._trackHiddenTimeStart = self._totalHiddenTime + (self._lastHideTime ? Date.now() - self._lastHideTime : 0)
60
- return args
61
- },
62
- success: function (res, args) {
63
- self._reportApi(args, res, null)
64
- },
65
- fail: function (err, args) {
66
- self._reportApi(args, null, err)
67
- },
68
- })
69
- this._interceptorAdded = true
70
- } catch (e) {
71
- if (config.debug) {
72
- console.warn('[TrackerSDK] api-watch: failed to add interceptor', e)
53
+ // 1. uni-app 运行时环境:优先使用官方 uni.addInterceptor
54
+ if (typeof uni !== 'undefined' && typeof uni.addInterceptor === 'function') {
55
+ try {
56
+ uni.addInterceptor('request', {
57
+ invoke: function (args) {
58
+ args._trackStartTime = Date.now()
59
+ args._trackBgCount = self._bgCount
60
+ args._trackHiddenTimeStart = self._totalHiddenTime + (self._lastHideTime ? Date.now() - self._lastHideTime : 0)
61
+ return args
62
+ },
63
+ success: function (res, args) {
64
+ self._reportApi(args, res, null)
65
+ },
66
+ fail: function (err, args) {
67
+ self._reportApi(args, null, err)
68
+ },
69
+ })
70
+ this._interceptorAdded = true
71
+ return
72
+ } catch (e) {
73
+ if (config.debug) {
74
+ console.warn('[TrackerSDK] api-watch: failed to add uni interceptor', e)
75
+ }
73
76
  }
74
77
  }
78
+
79
+ // 2. 标准浏览器/WebView 环境:拦截 window.fetch 与 XMLHttpRequest
80
+ if (typeof window !== 'undefined') {
81
+ this._setupWebInterceptors()
82
+ }
75
83
  }
76
84
 
77
- _reportApi(args, res, err) {
78
- if (!args || !args.url) return
85
+ _isInternalUrl(url) {
86
+ if (!url || typeof url !== 'string') return true
87
+ var config = this.tracker ? this.tracker.getConfig() : {}
88
+ return (
89
+ url.indexOf('trackBatch') >= 0 ||
90
+ url.indexOf('activeReport') >= 0 ||
91
+ url.indexOf('/api/track/collect') >= 0 ||
92
+ url.indexOf('/api/point/device/add') >= 0 ||
93
+ (config && config.batchReportUrl && url.indexOf(config.batchReportUrl) >= 0)
94
+ )
95
+ }
79
96
 
80
- var config = this.tracker.getConfig()
97
+ _setupWebInterceptors() {
98
+ var self = this
99
+
100
+ // 2.1 拦截 window.fetch
101
+ if (typeof window.fetch === 'function') {
102
+ var origFetch = window.fetch
103
+ this._origFetch = origFetch
104
+ window.fetch = function (input, init) {
105
+ var url = typeof input === 'string' ? input : (input && input.url ? input.url : '')
106
+ if (self._isInternalUrl(url)) {
107
+ return origFetch.apply(this, arguments)
108
+ }
109
+
110
+ var startTime = Date.now()
111
+ var method = (init && init.method) || (input && input.method) || 'GET'
112
+ var requestParams = (init && init.body) || null
81
113
 
82
- // [PROJECT-ADAPT] 自身请求过滤列表
83
- if (
84
- args.url.indexOf('/api/point/task/activeReport') >= 0 ||
85
- args.url.indexOf(config.batchReportUrl) >= 0 ||
86
- args.url.indexOf('/api/track/collect') >= 0
87
- ) {
88
- return
114
+ var args = {
115
+ url: url,
116
+ method: method,
117
+ data: requestParams,
118
+ _trackStartTime: startTime
119
+ }
120
+
121
+ return origFetch.apply(this, arguments).then(function (res) {
122
+ try {
123
+ var clone = res.clone()
124
+ clone.text().then(function (text) {
125
+ self._reportApi(args, {
126
+ statusCode: res.status,
127
+ data: text,
128
+ header: {}
129
+ }, null)
130
+ }).catch(function () {
131
+ self._reportApi(args, {
132
+ statusCode: res.status,
133
+ data: null,
134
+ header: {}
135
+ }, null)
136
+ })
137
+ } catch (e) {
138
+ self._reportApi(args, { statusCode: res.status }, null)
139
+ }
140
+ return res
141
+ }, function (err) {
142
+ self._reportApi(args, null, err)
143
+ throw err
144
+ })
145
+ }
89
146
  }
90
147
 
148
+ // 2.2 拦截 XMLHttpRequest
149
+ if (typeof window.XMLHttpRequest === 'function') {
150
+ var origOpen = window.XMLHttpRequest.prototype.open
151
+ var origSend = window.XMLHttpRequest.prototype.send
152
+ this._origXhrOpen = origOpen
153
+ this._origXhrSend = origSend
154
+
155
+ window.XMLHttpRequest.prototype.open = function (method, url) {
156
+ this._trackMethod = method
157
+ this._trackUrl = url
158
+ return origOpen.apply(this, arguments)
159
+ }
160
+
161
+ window.XMLHttpRequest.prototype.send = function (data) {
162
+ var xhr = this
163
+ var url = xhr._trackUrl || ''
164
+ if (self._isInternalUrl(url)) {
165
+ return origSend.apply(this, arguments)
166
+ }
167
+
168
+ var startTime = Date.now()
169
+ var method = xhr._trackMethod || 'GET'
170
+
171
+ var args = {
172
+ url: url,
173
+ method: method,
174
+ data: data,
175
+ _trackStartTime: startTime
176
+ }
177
+
178
+ xhr.addEventListener('loadend', function () {
179
+ try {
180
+ self._reportApi(args, {
181
+ statusCode: xhr.status,
182
+ data: xhr.responseType === '' || xhr.responseType === 'text' ? xhr.responseText : null
183
+ }, xhr.status === 0 ? 'Network Error' : null)
184
+ } catch (e) {}
185
+ })
186
+
187
+ return origSend.apply(this, arguments)
188
+ }
189
+ }
190
+ this._interceptorAdded = true
191
+ }
192
+
193
+ _reportApi(args, res, err) {
194
+ if (!args || !args.url || this._isInternalUrl(args.url)) return
195
+
91
196
  var duration = args._trackStartTime
92
197
  ? Date.now() - args._trackStartTime
93
198
  : 0
@@ -210,8 +315,17 @@ export class ApiWatchPlugin {
210
315
  }
211
316
  if (this._interceptorAdded) {
212
317
  try {
213
- uni.removeInterceptor('request')
318
+ if (typeof uni !== 'undefined' && uni.removeInterceptor) {
319
+ uni.removeInterceptor('request')
320
+ }
214
321
  } catch (e) {}
322
+ if (this._origFetch && typeof window !== 'undefined') {
323
+ window.fetch = this._origFetch
324
+ }
325
+ if (this._origXhrOpen && typeof window !== 'undefined') {
326
+ window.XMLHttpRequest.prototype.open = this._origXhrOpen
327
+ window.XMLHttpRequest.prototype.send = this._origXhrSend
328
+ }
215
329
  }
216
330
  if (this._visibilityHandler && typeof document !== 'undefined') {
217
331
  document.removeEventListener('visibilitychange', this._visibilityHandler)
package/plugins/click.js CHANGED
@@ -279,11 +279,26 @@ export class ClickPlugin {
279
279
  // [PROJECT-ADAPT] 设计工具命名过滤
280
280
  // 当前规则基于 Sketch/Figma 中文命名导出,英文命名项目需调整
281
281
  if (/^[\s\S]*(?:编组|矩形|备份|椭圆|路径)[\s\S]*$/.test(text) && text.indexOf('编组') > -1) return ''
282
+ // 符号转为可读语义
283
+ var normalized = this._normalizeSymbolText(text)
284
+ if (normalized !== text) return normalized
282
285
  // 文字太长说明是整个区域的内容,不是按钮文字
283
286
  if (text.length > 30) return ''
284
287
  return text.substring(0, 50)
285
288
  }
286
289
 
290
+ _normalizeSymbolText(text) {
291
+ if (!text) return ''
292
+ var trimmed = text.trim()
293
+ if (trimmed === '▼' || trimmed === '▽') return '展开/下拉'
294
+ if (trimmed === '▲' || trimmed === '△') return '收起'
295
+ if (trimmed === '›' || trimmed === '>' || trimmed === '»') return '查看/更多'
296
+ if (trimmed === '‹' || trimmed === '<' || trimmed === '«') return '返回'
297
+ if (trimmed === '×' || trimmed === '✕') return '关闭'
298
+ if (trimmed === '+') return '添加/增加'
299
+ return text
300
+ }
301
+
287
302
  /**
288
303
  * 检查容器内 image/img 标签的 src,提取文件名
289
304
  */
@@ -296,7 +311,16 @@ export class ClickPlugin {
296
311
  // 提取文件名(去掉路径和后缀)
297
312
  var match = src.match(/\/([^/]+)\.\w+$/)
298
313
  if (match && match[1]) {
299
- return '[img:' + match[1] + ']'
314
+ var name = match[1]
315
+ var lower = name.toLowerCase()
316
+ if (lower.indexOf('back') > -1 || lower === 'label_2') return '返回'
317
+ if (lower.indexOf('close') > -1 || lower.indexOf('cloess') > -1) return '关闭'
318
+ if (lower.indexOf('search') > -1) return '搜索'
319
+ if (lower.indexOf('share') > -1) return '分享'
320
+ if (lower.indexOf('refresh') > -1) return '刷新'
321
+ if (lower.indexOf('cart') > -1) return '购物车'
322
+ if (lower.indexOf('like') > -1 || lower.indexOf('favor') > -1) return '点赞/收藏'
323
+ return '[img:' + name + ']'
300
324
  }
301
325
  return ''
302
326
  }
@@ -113,6 +113,7 @@ export class PageViewPlugin {
113
113
 
114
114
  function getBestTitle() {
115
115
  var t = typeof document !== 'undefined' ? document.title : ''
116
+ if (t === 'uni-app') t = ''
116
117
  if (!t) {
117
118
  try {
118
119
  var pages = getCurrentPages()
@@ -123,6 +124,9 @@ export class PageViewPlugin {
123
124
  }
124
125
  } catch (e) {}
125
126
  }
127
+ if (!t && adapter && typeof adapter.getPageTitle === 'function') {
128
+ t = adapter.getPageTitle(currentPage) || ''
129
+ }
126
130
  return t
127
131
  }
128
132