@rk911/tracker-sdk 1.7.2 → 1.7.3

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/tracker.js CHANGED
@@ -96,6 +96,17 @@ export class Tracker {
96
96
  this._referrer = this._currentPage
97
97
  }
98
98
  this._currentPage = declaredPage
99
+
100
+ // 跨端信息携带:当在 Android/iOS 原生容器内运行时,将 H5 页面路由与标题同步给端上
101
+ if (typeof window !== 'undefined' && window.NativeBridge && window.NativeBridge.postMessage) {
102
+ try {
103
+ window.NativeBridge.postMessage('tracker.syncPageState', JSON.stringify({
104
+ pageRoute: declaredPage,
105
+ pageTitle: eventData.pageTitle || '',
106
+ timestamp: Date.now()
107
+ }), '')
108
+ } catch (e) {}
109
+ }
99
110
  }
100
111
 
101
112
  if (!this._currentPage) {
@@ -202,6 +213,14 @@ export class Tracker {
202
213
 
203
214
  setUser(userId, isNative = false) {
204
215
  this.session.setUser(userId, isNative)
216
+ // 跨端信息携带:当在原生 WebView 登录或切换用户时,将身份变更同步到 Native 端
217
+ if (!isNative && typeof window !== 'undefined' && window.NativeBridge && window.NativeBridge.postMessage) {
218
+ try {
219
+ window.NativeBridge.postMessage('tracker.syncUserIdentity', JSON.stringify({
220
+ userId: userId
221
+ }), '')
222
+ } catch (e) {}
223
+ }
205
224
  }
206
225
 
207
226
  setDeviceId(deviceId, isNative = false) {
@@ -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
@@ -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,6 +228,31 @@ 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
+ /**
236
+ * H5 路由状态同步 (由 CommonWebPresenter 通过 NativeBridge 触发)
237
+ */
238
+ public void updateH5PageState(final String h5Route, final String h5Title) {
239
+ postBgTask(new Runnable() {
240
+ @Override
241
+ public void run() {
242
+ try {
243
+ if (TextUtils.isEmpty(h5Route)) return;
244
+ String referrer = currentPageRoute;
245
+ currentPageRoute = h5Route;
246
+ referrerPageRoute = referrer;
247
+ lastPageStartTime = System.currentTimeMillis();
248
+ Log.i(TAG, "===> [H5 Page State Synced to Native] Route: " + currentPageRoute + ", Title: " + h5Title + ", Referrer: " + referrerPageRoute);
249
+ } catch (Exception e) {
250
+ Log.e(TAG, "===> [updateH5PageState Error]", e);
251
+ }
252
+ }
253
+ });
254
+ }
255
+
196
256
  public void resetSession() {
197
257
  this.sessionId = "android_" + System.currentTimeMillis() + "_" + (int) (Math.random() * 10000);
198
258
  Log.i(TAG, "===> [Native Tracker Session Reset] New SessionId: " + this.sessionId);
@@ -270,6 +330,80 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
270
330
  });
271
331
  }
272
332
 
333
+ /**
334
+ * 接口请求与异常自动化上报 (S6 API Tracking - 0 阻塞、异步持久化)
335
+ */
336
+ public void trackApi(final String url, final String method, final int statusCode,
337
+ final long duration, final String reqParam, final String respData, final String error) {
338
+ if (TextUtils.isEmpty(url)) return;
339
+ // 严格防递归:SDK 自身上报地址直接跳过
340
+ if (url.contains("/api/point/task/trackBatch") || (serverUrl != null && url.contains(serverUrl))) {
341
+ return;
342
+ }
343
+
344
+ postBgTask(new Runnable() {
345
+ @Override
346
+ public void run() {
347
+ try {
348
+ String eventType = (statusCode >= 400 || !TextUtils.isEmpty(error)) ? "api_error" : "api_request";
349
+ // 聚合优化:提取纯净的 API 路由路径作为 eventName(例如 /app/user/getUserInfo),方便在报表按接口聚合
350
+ String apiPath = extractApiPath(url);
351
+ JSONObject event = createBaseEvent(eventType, apiPath);
352
+
353
+ JSONObject extra = new JSONObject();
354
+ extra.put("url", url);
355
+ extra.put("apiPath", apiPath);
356
+ extra.put("method", method != null ? method.toUpperCase() : "GET");
357
+ extra.put("statusCode", statusCode);
358
+ extra.put("duration", duration);
359
+
360
+ // 字段容量防膨胀安全截断:防止过大 JSON 导致 SQLite CursorWindow 溢出或 HTTP 413
361
+ if (!TextUtils.isEmpty(reqParam)) {
362
+ extra.put("requestParams", reqParam.length() > 2048 ? reqParam.substring(0, 2048) : reqParam);
363
+ }
364
+ if (!TextUtils.isEmpty(respData)) {
365
+ extra.put("responseData", respData.length() > 2048 ? respData.substring(0, 2048) : respData);
366
+ }
367
+ if (!TextUtils.isEmpty(error)) {
368
+ extra.put("error", error.length() > 512 ? error.substring(0, 512) : error);
369
+ }
370
+ if (!TextUtils.isEmpty(phone)) extra.put("phone", phone);
371
+ if (!TextUtils.isEmpty(storeName)) extra.put("storeName", storeName);
372
+ if (!TextUtils.isEmpty(userId)) extra.put("userId", userId);
373
+ extra.put("platform", "android");
374
+
375
+ event.put("extraData", extra.toString());
376
+ enqueueEvent(event);
377
+
378
+ // 接口异常立即上报排查;正常接口由 3s 定时器批量聚合上报
379
+ if ("api_error".equals(eventType)) {
380
+ flushAsync();
381
+ }
382
+ } catch (Exception e) {
383
+ Log.e(TAG, "===> [Native Tracker trackApi Failed]", e);
384
+ }
385
+ }
386
+ });
387
+ }
388
+
389
+ private static String extractApiPath(String url) {
390
+ if (TextUtils.isEmpty(url)) return "";
391
+ try {
392
+ int qIdx = url.indexOf('?');
393
+ String clean = qIdx >= 0 ? url.substring(0, qIdx) : url;
394
+ int schemeIdx = clean.indexOf("://");
395
+ if (schemeIdx >= 0) {
396
+ int pathIdx = clean.indexOf('/', schemeIdx + 3);
397
+ if (pathIdx >= 0) {
398
+ return clean.substring(pathIdx);
399
+ }
400
+ }
401
+ return clean;
402
+ } catch (Exception e) {
403
+ return url;
404
+ }
405
+ }
406
+
273
407
  private JSONObject createBaseEvent(String eventType, String eventName) throws Exception {
274
408
  sequence++;
275
409
  JSONObject event = new JSONObject();
@@ -283,6 +417,9 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
283
417
  event.put("referrer", referrerPageRoute);
284
418
  }
285
419
  event.put("os", "Android");
420
+ event.put("deviceBrand", android.os.Build.BRAND != null ? android.os.Build.BRAND : "");
421
+ event.put("deviceModel", android.os.Build.MODEL != null ? android.os.Build.MODEL : "");
422
+ event.put("osVersion", android.os.Build.VERSION.RELEASE != null ? android.os.Build.VERSION.RELEASE : "");
286
423
 
287
424
  JSONObject extra = new JSONObject();
288
425
  if (!TextUtils.isEmpty(phone)) extra.put("phone", phone);
@@ -340,12 +477,24 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
340
477
  }
341
478
 
342
479
  public void flushSync() {
480
+ if (bgThread != null && Thread.currentThread() == bgThread) {
481
+ flushInternal();
482
+ return;
483
+ }
484
+ final java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(1);
343
485
  postBgTask(new Runnable() {
344
486
  @Override
345
487
  public void run() {
346
- flushInternal();
488
+ try {
489
+ flushInternal();
490
+ } finally {
491
+ latch.countDown();
492
+ }
347
493
  }
348
494
  });
495
+ try {
496
+ latch.await(1500, java.util.concurrent.TimeUnit.MILLISECONDS);
497
+ } catch (InterruptedException ignored) {}
349
498
  }
350
499
 
351
500
  private void flushInternal() {
@@ -493,13 +642,16 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
493
642
  @Override
494
643
  public void run() {
495
644
  try {
645
+ if (fragmentRoute.equals(currentPageRoute) && (System.currentTimeMillis() - lastPageStartTime < 500)) {
646
+ return;
647
+ }
496
648
  String referrer = currentPageRoute;
497
649
  currentPageRoute = fragmentRoute;
498
650
  referrerPageRoute = referrer;
499
651
  lastPageStartTime = System.currentTimeMillis();
500
652
  Log.i(TAG, "===> [Native Fragment Resumed] Page: " + currentPageRoute + ", Referrer: " + referrerPageRoute);
501
653
  JSONObject event = createBaseEvent("page_view", "page_view");
502
- event.put("pageTitle", currentPageRoute);
654
+ event.put("pageTitle", getFriendlyPageTitle(fragmentRoute));
503
655
  enqueueEvent(event);
504
656
  flushAsync();
505
657
  } catch (Exception e) {
@@ -516,7 +668,9 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
516
668
  Window window = activity.getWindow();
517
669
  if (window != null) {
518
670
  final Window.Callback origCb = window.getCallback();
519
- window.setCallback(new WindowCallbackWrapper(origCb, activity));
671
+ if (origCb != null && !(origCb instanceof WindowCallbackWrapper)) {
672
+ window.setCallback(new WindowCallbackWrapper(origCb, activity));
673
+ }
520
674
  }
521
675
  } catch (Exception e) {
522
676
  Log.w(TAG, "===> [Native Window Callback Install Failed]", e);
@@ -558,13 +712,45 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
558
712
  return root;
559
713
  }
560
714
 
715
+ private String extractViewText(View view) {
716
+ if (view == null) return "";
717
+ try {
718
+ // 1. 优先读取 contentDescription
719
+ CharSequence desc = view.getContentDescription();
720
+ if (!TextUtils.isEmpty(desc)) {
721
+ return desc.toString().trim();
722
+ }
723
+ // 2. 本身是 TextView / Button
724
+ if (view instanceof TextView) {
725
+ CharSequence text = ((TextView) view).getText();
726
+ if (!TextUtils.isEmpty(text)) {
727
+ return text.toString().trim();
728
+ }
729
+ }
730
+ // 3. ViewGroup 穿透提取子 View 文本
731
+ if (view instanceof ViewGroup) {
732
+ ViewGroup group = (ViewGroup) view;
733
+ int count = group.getChildCount();
734
+ for (int i = 0; i < count; i++) {
735
+ View child = group.getChildAt(i);
736
+ if (child != null && child.getVisibility() == View.VISIBLE) {
737
+ String childText = extractViewText(child);
738
+ if (!TextUtils.isEmpty(childText)) {
739
+ return childText;
740
+ }
741
+ }
742
+ }
743
+ }
744
+ } catch (Exception e) {}
745
+ return "";
746
+ }
747
+
561
748
  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 = "";
749
+ String extracted = extractViewText(target);
750
+ if (TextUtils.isEmpty(extracted) && target.getParent() instanceof View) {
751
+ extracted = extractViewText((View) target.getParent());
567
752
  }
753
+ final String elementText = extracted;
568
754
 
569
755
  final String elementId;
570
756
  if (target.getId() != View.NO_ID) {
@@ -617,13 +803,18 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
617
803
  @Override
618
804
  public void run() {
619
805
  try {
806
+ final String route = activity.getClass().getName();
807
+ if (route.equals(currentPageRoute) && (System.currentTimeMillis() - lastPageStartTime < 500)) {
808
+ return;
809
+ }
620
810
  String referrer = currentPageRoute;
621
- currentPageRoute = activity.getClass().getName();
811
+ currentPageRoute = route;
622
812
  referrerPageRoute = referrer;
623
813
  lastPageStartTime = System.currentTimeMillis();
624
814
  Log.i(TAG, "===> [Native Activity Resumed] Page: " + currentPageRoute + ", Referrer: " + referrerPageRoute);
625
815
  JSONObject event = createBaseEvent("page_view", "page_view");
626
- event.put("pageTitle", activity.getTitle() != null ? activity.getTitle().toString() : currentPageRoute);
816
+ String activityTitle = activity.getTitle() != null ? activity.getTitle().toString() : "";
817
+ event.put("pageTitle", !TextUtils.isEmpty(activityTitle) ? activityTitle : getFriendlyPageTitle(route));
627
818
  enqueueEvent(event);
628
819
  flushAsync();
629
820
  } catch (Exception e) {
@@ -641,11 +832,13 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
641
832
  try {
642
833
  if (lastPageStartTime > 0) {
643
834
  long duration = System.currentTimeMillis() - lastPageStartTime;
644
- JSONObject event = createBaseEvent("page_stay", "page_stay");
645
- event.put("duration", duration);
646
- enqueueEvent(event);
835
+ if (duration >= 500) {
836
+ JSONObject event = createBaseEvent("page_stay", "page_stay");
837
+ event.put("duration", duration);
838
+ enqueueEvent(event);
839
+ flushAsync();
840
+ }
647
841
  lastPageStartTime = 0;
648
- flushAsync();
649
842
  }
650
843
  } catch (Exception e) {
651
844
  Log.e(TAG, "===> [Native Tracker onActivityPaused Error]", e);
@@ -662,14 +855,14 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
662
855
  // --- Window.Callback 代理包装器 (带 S3 滑动防误触过滤) ---
663
856
  private class WindowCallbackWrapper implements Window.Callback {
664
857
  private final Window.Callback mWrapped;
665
- private final Activity mActivity;
858
+ private final java.lang.ref.WeakReference<Activity> mActivityRef;
666
859
  private float downX, downY;
667
860
  private long downTime;
668
861
  private int touchSlop = -1;
669
862
 
670
863
  public WindowCallbackWrapper(Window.Callback wrapped, Activity activity) {
671
864
  this.mWrapped = wrapped;
672
- this.mActivity = activity;
865
+ this.mActivityRef = new java.lang.ref.WeakReference<>(activity);
673
866
  if (activity != null) {
674
867
  try {
675
868
  this.touchSlop = ViewConfiguration.get(activity).getScaledTouchSlop();
@@ -693,8 +886,9 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
693
886
  int slopThreshold = touchSlop > 0 ? touchSlop : 24;
694
887
  if (distance <= slopThreshold && duration < 1500) {
695
888
  try {
696
- if (mActivity != null && mActivity.getWindow() != null) {
697
- View decorView = mActivity.getWindow().getDecorView();
889
+ Activity act = mActivityRef != null ? mActivityRef.get() : null;
890
+ if (act != null && act.getWindow() != null) {
891
+ View decorView = act.getWindow().getDecorView();
698
892
  View leaf = findTouchedLeafView(decorView, (int) event.getRawX(), (int) event.getRawY());
699
893
  View clickableTarget = null;
700
894
  View curr = leaf;
@@ -707,7 +901,7 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
707
901
  curr = (View) curr.getParent();
708
902
  }
709
903
  if (clickableTarget != null) {
710
- handleNativeViewClick(mActivity, clickableTarget, event.getRawX(), event.getRawY());
904
+ handleNativeViewClick(act, clickableTarget, event.getRawX(), event.getRawY());
711
905
  }
712
906
  }
713
907
  } catch (Exception e) {
@@ -715,30 +909,30 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
715
909
  }
716
910
  }
717
911
  }
718
- return mWrapped.dispatchTouchEvent(event);
912
+ return mWrapped != null ? mWrapped.dispatchTouchEvent(event) : false;
719
913
  }
720
914
 
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); }
915
+ @Override public boolean dispatchKeyEvent(KeyEvent event) { return mWrapped != null && mWrapped.dispatchKeyEvent(event); }
916
+ @Override public boolean dispatchKeyShortcutEvent(KeyEvent event) { return mWrapped != null && mWrapped.dispatchKeyShortcutEvent(event); }
917
+ @Override public boolean dispatchTrackballEvent(MotionEvent event) { return mWrapped != null && mWrapped.dispatchTrackballEvent(event); }
918
+ @Override public boolean dispatchGenericMotionEvent(MotionEvent event) { return mWrapped != null && mWrapped.dispatchGenericMotionEvent(event); }
919
+ @Override public boolean dispatchPopulateAccessibilityEvent(android.view.accessibility.AccessibilityEvent event) { return mWrapped != null && mWrapped.dispatchPopulateAccessibilityEvent(event); }
920
+ @Nullable @Override public View onCreatePanelView(int featureId) { return mWrapped != null ? mWrapped.onCreatePanelView(featureId) : null; }
921
+ @Override public boolean onCreatePanelMenu(int featureId, @NonNull Menu menu) { return mWrapped != null && mWrapped.onCreatePanelMenu(featureId, menu); }
922
+ @Override public boolean onPreparePanel(int featureId, @Nullable View view, @NonNull Menu menu) { return mWrapped != null && mWrapped.onPreparePanel(featureId, view, menu); }
923
+ @Override public boolean onMenuOpened(int featureId, @NonNull Menu menu) { return mWrapped != null && mWrapped.onMenuOpened(featureId, menu); }
924
+ @Override public boolean onMenuItemSelected(int featureId, @NonNull MenuItem item) { return mWrapped != null && mWrapped.onMenuItemSelected(featureId, item); }
925
+ @Override public void onWindowAttributesChanged(WindowManager.LayoutParams attrs) { if (mWrapped != null) mWrapped.onWindowAttributesChanged(attrs); }
926
+ @Override public void onContentChanged() { if (mWrapped != null) mWrapped.onContentChanged(); }
927
+ @Override public void onWindowFocusChanged(boolean hasFocus) { if (mWrapped != null) mWrapped.onWindowFocusChanged(hasFocus); }
928
+ @Override public void onAttachedToWindow() { if (mWrapped != null) mWrapped.onAttachedToWindow(); }
929
+ @Override public void onDetachedFromWindow() { if (mWrapped != null) mWrapped.onDetachedFromWindow(); }
930
+ @Override public void onPanelClosed(int featureId, @NonNull Menu menu) { if (mWrapped != null) mWrapped.onPanelClosed(featureId, menu); }
931
+ @Override public boolean onSearchRequested() { return mWrapped != null && mWrapped.onSearchRequested(); }
932
+ @Override public boolean onSearchRequested(SearchEvent searchEvent) { return mWrapped != null && mWrapped.onSearchRequested(searchEvent); }
933
+ @Nullable @Override public ActionMode onWindowStartingActionMode(ActionMode.Callback callback) { return mWrapped != null ? mWrapped.onWindowStartingActionMode(callback) : null; }
934
+ @Nullable @Override public ActionMode onWindowStartingActionMode(ActionMode.Callback callback, int type) { return mWrapped != null ? mWrapped.onWindowStartingActionMode(callback, type) : null; }
935
+ @Override public void onActionModeStarted(ActionMode mode) { if (mWrapped != null) mWrapped.onActionModeStarted(mode); }
936
+ @Override public void onActionModeFinished(ActionMode mode) { if (mWrapped != null) mWrapped.onActionModeFinished(mode); }
743
937
  }
744
938
  }
@@ -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.3",
4
4
  "description": "人康多端埋点 SDK — 跨端自动化采集 + 手动埋点双写兼容",
5
5
  "main": "index.js",
6
6
  "module": "index.js",
@@ -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)