@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 +19 -0
- package/native/android/AndroidTracker.java +236 -42
- package/native/android/TrackerInterceptor.java +134 -0
- package/package.json +1 -1
- package/plugins/api-watch.js +145 -31
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
|
-
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
563
|
-
if (target instanceof
|
|
564
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
697
|
-
|
|
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(
|
|
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
package/plugins/api-watch.js
CHANGED
|
@@ -50,44 +50,149 @@ export class ApiWatchPlugin {
|
|
|
50
50
|
var self = this
|
|
51
51
|
var config = this.tracker.getConfig()
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
78
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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)
|