@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 +23 -6
- package/core/tracker.js +37 -3
- package/index.js +2 -2
- package/native/android/AndroidTracker.java +271 -45
- package/native/android/TrackerInterceptor.java +134 -0
- package/package.json +1 -1
- package/platforms/app.js +11 -3
- package/platforms/web.js +47 -14
- package/plugins/api-watch.js +145 -31
- package/plugins/click.js +25 -1
- package/plugins/page-view.js +4 -0
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
|
-
|
|
77
|
-
|
|
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
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
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
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
697
|
-
|
|
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(
|
|
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
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
|
-
|
|
29
|
-
if (nativeInfo) {
|
|
30
|
-
|
|
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
|
|
374
|
-
|
|
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
|
|
403
|
+
// UA 未命中时按设备型号前缀兜底识别(WebView 常拿不到 UA 品牌,但原生/容器能给型号)
|
|
383
404
|
var m = (model || '').toString().toUpperCase()
|
|
384
405
|
if (m) {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
if (/^(
|
|
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 '其他型号手机'
|
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)
|
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
|
-
|
|
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
|
}
|
package/plugins/page-view.js
CHANGED
|
@@ -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
|
|