@rk911/tracker-sdk 1.6.8 → 1.7.0
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 +30 -6
- package/core/tracker.js +16 -4
- package/index.js +23 -1
- package/native/android/AndroidTracker.java +277 -0
- package/native/ios/RKTrackerSDK.swift +148 -0
- package/package.json +4 -3
package/core/session.js
CHANGED
|
@@ -10,6 +10,9 @@ export class SessionManager {
|
|
|
10
10
|
this.sequence = 0
|
|
11
11
|
this.userId = null
|
|
12
12
|
this.deviceId = null
|
|
13
|
+
this.isNativeSessionId = false
|
|
14
|
+
this.isNativeDeviceId = false
|
|
15
|
+
this.isNativeUserId = false
|
|
13
16
|
this._storageKey = config.session.storageKey || '_tracker_session'
|
|
14
17
|
this._renewListeners = []
|
|
15
18
|
|
|
@@ -32,6 +35,7 @@ export class SessionManager {
|
|
|
32
35
|
getSessionId() {
|
|
33
36
|
if (!this.sessionId || this._isExpired()) {
|
|
34
37
|
var wasRenew = !!this.sessionId
|
|
38
|
+
this.isNativeSessionId = false
|
|
35
39
|
this._createNew()
|
|
36
40
|
if (wasRenew) this._notifyRenew()
|
|
37
41
|
}
|
|
@@ -56,15 +60,34 @@ export class SessionManager {
|
|
|
56
60
|
this._save()
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
setUser(userId) {
|
|
63
|
+
setUser(userId, isNative = false) {
|
|
60
64
|
this.userId = userId
|
|
65
|
+
this.isNativeUserId = !!isNative
|
|
66
|
+
if (userId && !isNative) {
|
|
67
|
+
this.adapter.setStorage('userId', userId)
|
|
68
|
+
}
|
|
61
69
|
this._save()
|
|
62
70
|
}
|
|
63
71
|
|
|
64
|
-
setDeviceId(deviceId) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
72
|
+
setDeviceId(deviceId, isNative = false) {
|
|
73
|
+
if (deviceId) {
|
|
74
|
+
this.deviceId = deviceId
|
|
75
|
+
this.isNativeDeviceId = !!isNative
|
|
76
|
+
if (!isNative) {
|
|
77
|
+
this.adapter.setStorage(DEVICE_ID_KEY, deviceId)
|
|
78
|
+
this.adapter.setStorage('deviceId', deviceId)
|
|
79
|
+
}
|
|
80
|
+
this._save()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
setSessionId(sessionId, isNative = false) {
|
|
85
|
+
if (sessionId) {
|
|
86
|
+
this.sessionId = sessionId
|
|
87
|
+
this.isNativeSessionId = !!isNative
|
|
88
|
+
this.lastActivityTime = this.adapter.getTimestamp()
|
|
89
|
+
this._save()
|
|
90
|
+
}
|
|
68
91
|
}
|
|
69
92
|
|
|
70
93
|
getUserId() {
|
|
@@ -95,13 +118,14 @@ export class SessionManager {
|
|
|
95
118
|
startTime: this.startTime,
|
|
96
119
|
lastActivityTime: this.lastActivityTime,
|
|
97
120
|
sequence: this.sequence,
|
|
121
|
+
isNativeSessionId: !!this.isNativeSessionId,
|
|
98
122
|
}
|
|
99
123
|
this.adapter.setSessionStorage(this._storageKey, data)
|
|
100
124
|
}
|
|
101
125
|
|
|
102
126
|
_restore() {
|
|
103
127
|
var data = this.adapter.getSessionStorage(this._storageKey)
|
|
104
|
-
if (data && data.sessionId) {
|
|
128
|
+
if (data && data.sessionId && !data.isNativeSessionId) {
|
|
105
129
|
this.sessionId = data.sessionId
|
|
106
130
|
this.startTime = data.startTime
|
|
107
131
|
this.lastActivityTime = data.lastActivityTime
|
package/core/tracker.js
CHANGED
|
@@ -200,12 +200,24 @@ export class Tracker {
|
|
|
200
200
|
|
|
201
201
|
// ---- 用户标识 ----
|
|
202
202
|
|
|
203
|
-
setUser(userId) {
|
|
204
|
-
this.session.setUser(userId)
|
|
203
|
+
setUser(userId, isNative = false) {
|
|
204
|
+
this.session.setUser(userId, isNative)
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
setDeviceId(deviceId) {
|
|
208
|
-
this.session.setDeviceId(deviceId)
|
|
207
|
+
setDeviceId(deviceId, isNative = false) {
|
|
208
|
+
this.session.setDeviceId(deviceId, isNative)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
setSessionId(sessionId, isNative = false) {
|
|
212
|
+
this.session.setSessionId(sessionId, isNative)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
getDeviceId() {
|
|
216
|
+
return this.session.getDeviceId()
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
getSessionId() {
|
|
220
|
+
return this.session.getSessionId()
|
|
209
221
|
}
|
|
210
222
|
|
|
211
223
|
setUserProperties(properties) {
|
package/index.js
CHANGED
|
@@ -89,6 +89,15 @@ export function createTracker(config) {
|
|
|
89
89
|
var adapter = createAdapter(platform, config)
|
|
90
90
|
trackerInstance = new Tracker(config, adapter)
|
|
91
91
|
|
|
92
|
+
// 触发 onBeforeInit 钩子(必须在插件注册与首屏事件触发前执行,以便同步/异步注入 Native 上下文)
|
|
93
|
+
if (typeof config.onBeforeInit === 'function') {
|
|
94
|
+
try {
|
|
95
|
+
config.onBeforeInit(trackerInstance)
|
|
96
|
+
} catch (e) {
|
|
97
|
+
console.warn('[TrackerSDK] onBeforeInit error:', e)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
92
101
|
// 注册插件
|
|
93
102
|
var pluginNames = config.plugins || []
|
|
94
103
|
pluginNames.forEach(function (pluginName) {
|
|
@@ -125,7 +134,14 @@ export function getTracker() {
|
|
|
125
134
|
function registerDeviceIfNeeded(tracker) {
|
|
126
135
|
var adapter = tracker.getAdapter()
|
|
127
136
|
var config = tracker.getConfig()
|
|
128
|
-
var
|
|
137
|
+
var session = tracker.getSession()
|
|
138
|
+
|
|
139
|
+
// 如果是由 App/Native 动态注入的临时 DeviceID,跳过 H5 本地注册逻辑
|
|
140
|
+
if (session.isNativeDeviceId && session.getDeviceId()) {
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
var existingId = session.getDeviceId() || adapter.getStorage('deviceId') || adapter.getStorage('_tracker_device_id')
|
|
129
145
|
if (existingId) {
|
|
130
146
|
tracker.setDeviceId(existingId)
|
|
131
147
|
return
|
|
@@ -188,6 +204,12 @@ function registerDeviceIfNeeded(tracker) {
|
|
|
188
204
|
* 不依赖内存缓存,确保清除存储后不会残留旧用户、切换账号后能及时更新
|
|
189
205
|
*/
|
|
190
206
|
function _ensureUserId(tracker) {
|
|
207
|
+
var session = tracker.getSession()
|
|
208
|
+
// 如果当前 userId 是由 Native 拼接注入的临时账号,不被 H5 本地 Cookie/Storage 覆写
|
|
209
|
+
if (session.isNativeUserId && session.getUserId()) {
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
191
213
|
var adapter = tracker.getAdapter()
|
|
192
214
|
// 从源头实时读取(登录页写入了 uni.setStorageSync('userId', ...))
|
|
193
215
|
var uid = adapter.getStorage('userId')
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
package com.renrui.tracker;
|
|
2
|
+
|
|
3
|
+
import android.app.Activity;
|
|
4
|
+
import android.app.Application;
|
|
5
|
+
import android.os.Bundle;
|
|
6
|
+
import android.os.Handler;
|
|
7
|
+
import android.os.HandlerThread;
|
|
8
|
+
import android.text.TextUtils;
|
|
9
|
+
import android.util.Log;
|
|
10
|
+
|
|
11
|
+
import org.json.JSONArray;
|
|
12
|
+
import org.json.JSONObject;
|
|
13
|
+
|
|
14
|
+
import java.io.OutputStream;
|
|
15
|
+
import java.net.HttpURLConnection;
|
|
16
|
+
import java.net.URL;
|
|
17
|
+
import java.util.ArrayList;
|
|
18
|
+
import java.util.List;
|
|
19
|
+
import java.util.UUID;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 行业级 Native Android Tracker SDK
|
|
23
|
+
* 独立后台 HandlerThread 处理打点,主线程耗时 0ms
|
|
24
|
+
*/
|
|
25
|
+
public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
|
|
26
|
+
|
|
27
|
+
private static final String TAG = "AndroidTracker";
|
|
28
|
+
private static final int BATCH_SIZE = 20;
|
|
29
|
+
private static final long FLUSH_INTERVAL_MS = 5000;
|
|
30
|
+
|
|
31
|
+
private static volatile AndroidTracker instance;
|
|
32
|
+
private Application application;
|
|
33
|
+
private String serverUrl;
|
|
34
|
+
private String appId = "12"; // 12: Android App
|
|
35
|
+
private String sessionId;
|
|
36
|
+
private String deviceId;
|
|
37
|
+
private String userId;
|
|
38
|
+
private String phone;
|
|
39
|
+
private String storeName;
|
|
40
|
+
private int sequence = 0;
|
|
41
|
+
|
|
42
|
+
private HandlerThread bgThread;
|
|
43
|
+
private Handler bgHandler;
|
|
44
|
+
private final List<JSONObject> eventQueue = new ArrayList<>();
|
|
45
|
+
private long lastPageStartTime = 0;
|
|
46
|
+
private String currentPageRoute = "";
|
|
47
|
+
|
|
48
|
+
private AndroidTracker() {}
|
|
49
|
+
|
|
50
|
+
public static AndroidTracker getInstance() {
|
|
51
|
+
if (instance == null) {
|
|
52
|
+
synchronized (AndroidTracker.class) {
|
|
53
|
+
if (instance == null) {
|
|
54
|
+
instance = new AndroidTracker();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return instance;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 初始化 SDK
|
|
63
|
+
*/
|
|
64
|
+
public void init(Application app, String serverUrl, String deviceId, String userId, String phone, String storeName) {
|
|
65
|
+
this.application = app;
|
|
66
|
+
this.serverUrl = serverUrl;
|
|
67
|
+
this.deviceId = deviceId;
|
|
68
|
+
this.userId = userId;
|
|
69
|
+
this.phone = phone;
|
|
70
|
+
this.storeName = storeName;
|
|
71
|
+
this.sessionId = "android_" + System.currentTimeMillis() + "_" + (int)(Math.random() * 10000);
|
|
72
|
+
|
|
73
|
+
// 创建独立后台线程处理队列与网络请求,保障 UI 0 卡顿
|
|
74
|
+
bgThread = new HandlerThread("TrackerBgThread");
|
|
75
|
+
bgThread.start();
|
|
76
|
+
bgHandler = new Handler(bgThread.getLooper());
|
|
77
|
+
|
|
78
|
+
// 注册 Activity 生命周期自动采集
|
|
79
|
+
app.registerActivityLifecycleCallbacks(this);
|
|
80
|
+
|
|
81
|
+
// 启动定时 Flush 任务
|
|
82
|
+
scheduleTimerFlush();
|
|
83
|
+
Log.d(TAG, "AndroidTracker initialized with sessionId: " + sessionId);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public void updateUser(String userId, String phone, String storeName) {
|
|
87
|
+
if (!TextUtils.isEmpty(userId)) this.userId = userId;
|
|
88
|
+
if (!TextUtils.isEmpty(phone)) this.phone = phone;
|
|
89
|
+
if (!TextUtils.isEmpty(storeName)) this.storeName = storeName;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public String getSessionId() {
|
|
93
|
+
return sessionId;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 手动自定义埋点 (Custom Tracking)
|
|
98
|
+
*/
|
|
99
|
+
public void track(final String eventName, final JSONObject customProperties) {
|
|
100
|
+
postBgTask(new Runnable() {
|
|
101
|
+
@Override
|
|
102
|
+
public void run() {
|
|
103
|
+
try {
|
|
104
|
+
JSONObject event = createBaseEvent("custom", eventName);
|
|
105
|
+
if (customProperties != null) {
|
|
106
|
+
event.put("extraData", customProperties.toString());
|
|
107
|
+
}
|
|
108
|
+
enqueueEvent(event);
|
|
109
|
+
} catch (Exception e) {
|
|
110
|
+
Log.e(TAG, "track custom event failed", e);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 旧 $dadian 点位兼容方法 (自动匹配后端双写 point.log)
|
|
118
|
+
*/
|
|
119
|
+
public void trackFrontUrl(final String frontUrl, final String elementId, final String postParam) {
|
|
120
|
+
postBgTask(new Runnable() {
|
|
121
|
+
@Override
|
|
122
|
+
public void run() {
|
|
123
|
+
try {
|
|
124
|
+
JSONObject event = createBaseEvent("click", "legacy_dadian");
|
|
125
|
+
event.put("frontUrl", frontUrl);
|
|
126
|
+
event.put("elementId", elementId);
|
|
127
|
+
if (postParam != null) event.put("postParam", postParam);
|
|
128
|
+
enqueueEvent(event);
|
|
129
|
+
} catch (Exception e) {
|
|
130
|
+
Log.e(TAG, "trackFrontUrl failed", e);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private JSONObject createBaseEvent(String eventType, String eventName) throws Exception {
|
|
137
|
+
sequence++;
|
|
138
|
+
JSONObject event = new JSONObject();
|
|
139
|
+
event.put("eventId", "e_" + System.currentTimeMillis() + "_" + (int)(Math.random() * 1000));
|
|
140
|
+
event.put("eventType", eventType);
|
|
141
|
+
event.put("eventName", eventName);
|
|
142
|
+
event.put("timestamp", System.currentTimeMillis());
|
|
143
|
+
event.put("sequence", sequence);
|
|
144
|
+
event.put("pageRoute", currentPageRoute);
|
|
145
|
+
event.put("os", "Android");
|
|
146
|
+
return event;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private void enqueueEvent(JSONObject event) {
|
|
150
|
+
synchronized (eventQueue) {
|
|
151
|
+
eventQueue.add(event);
|
|
152
|
+
if (eventQueue.size() >= BATCH_SIZE) {
|
|
153
|
+
flushAsync();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private void scheduleTimerFlush() {
|
|
159
|
+
if (bgHandler == null) return;
|
|
160
|
+
bgHandler.postDelayed(new Runnable() {
|
|
161
|
+
@Override
|
|
162
|
+
public void run() {
|
|
163
|
+
flushAsync();
|
|
164
|
+
scheduleTimerFlush();
|
|
165
|
+
}
|
|
166
|
+
}, FLUSH_INTERVAL_MS);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
public void flushAsync() {
|
|
170
|
+
postBgTask(new Runnable() {
|
|
171
|
+
@Override
|
|
172
|
+
public void run() {
|
|
173
|
+
List<JSONObject> toSend;
|
|
174
|
+
synchronized (eventQueue) {
|
|
175
|
+
if (eventQueue.isEmpty()) return;
|
|
176
|
+
toSend = new ArrayList<>(eventQueue);
|
|
177
|
+
eventQueue.clear();
|
|
178
|
+
}
|
|
179
|
+
sendBatch(toSend);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private void sendBatch(List<JSONObject> events) {
|
|
185
|
+
if (TextUtils.isEmpty(serverUrl) || events.isEmpty()) return;
|
|
186
|
+
HttpURLConnection conn = null;
|
|
187
|
+
try {
|
|
188
|
+
JSONObject body = new JSONObject();
|
|
189
|
+
body.put("appId", Integer.parseInt(appId));
|
|
190
|
+
body.put("sessionId", sessionId);
|
|
191
|
+
body.put("deviceId", deviceId);
|
|
192
|
+
body.put("userId", userId);
|
|
193
|
+
body.put("phone", phone);
|
|
194
|
+
body.put("storeName", storeName);
|
|
195
|
+
body.put("platform", "android");
|
|
196
|
+
|
|
197
|
+
JSONArray eventsArr = new JSONArray();
|
|
198
|
+
for (JSONObject e : events) {
|
|
199
|
+
eventsArr.put(e);
|
|
200
|
+
}
|
|
201
|
+
body.put("events", eventsArr);
|
|
202
|
+
|
|
203
|
+
URL url = new URL(serverUrl);
|
|
204
|
+
conn = (HttpURLConnection) url.openConnection();
|
|
205
|
+
conn.setRequestMethod("POST");
|
|
206
|
+
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
|
207
|
+
conn.setDoOutput(true);
|
|
208
|
+
conn.setConnectTimeout(5000);
|
|
209
|
+
conn.setReadTimeout(5000);
|
|
210
|
+
|
|
211
|
+
byte[] bytes = body.toString().getBytes("UTF-8");
|
|
212
|
+
OutputStream os = conn.getOutputStream();
|
|
213
|
+
os.write(bytes);
|
|
214
|
+
os.flush();
|
|
215
|
+
os.close();
|
|
216
|
+
|
|
217
|
+
int code = conn.getResponseCode();
|
|
218
|
+
Log.d(TAG, "sendBatch response code: " + code + ", count: " + events.size());
|
|
219
|
+
} catch (Exception e) {
|
|
220
|
+
Log.e(TAG, "sendBatch failed", e);
|
|
221
|
+
} finally {
|
|
222
|
+
if (conn != null) conn.disconnect();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private void postBgTask(Runnable runnable) {
|
|
227
|
+
if (bgHandler != null) {
|
|
228
|
+
bgHandler.post(runnable);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// --- Activity Lifecycle Auto-Tracking ---
|
|
233
|
+
|
|
234
|
+
@Override
|
|
235
|
+
public void onActivityResumed(final Activity activity) {
|
|
236
|
+
postBgTask(new Runnable() {
|
|
237
|
+
@Override
|
|
238
|
+
public void run() {
|
|
239
|
+
try {
|
|
240
|
+
currentPageRoute = activity.getClass().getName();
|
|
241
|
+
lastPageStartTime = System.currentTimeMillis();
|
|
242
|
+
JSONObject event = createBaseEvent("page_view", "page_view");
|
|
243
|
+
event.put("pageTitle", activity.getTitle() != null ? activity.getTitle().toString() : currentPageRoute);
|
|
244
|
+
enqueueEvent(event);
|
|
245
|
+
} catch (Exception e) {
|
|
246
|
+
Log.e(TAG, "onActivityResumed failed", e);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@Override
|
|
253
|
+
public void onActivityPaused(final Activity activity) {
|
|
254
|
+
postBgTask(new Runnable() {
|
|
255
|
+
@Override
|
|
256
|
+
public void run() {
|
|
257
|
+
try {
|
|
258
|
+
if (lastPageStartTime > 0) {
|
|
259
|
+
long duration = System.currentTimeMillis() - lastPageStartTime;
|
|
260
|
+
JSONObject event = createBaseEvent("page_stay", "page_stay");
|
|
261
|
+
event.put("duration", duration);
|
|
262
|
+
enqueueEvent(event);
|
|
263
|
+
lastPageStartTime = 0;
|
|
264
|
+
}
|
|
265
|
+
} catch (Exception e) {
|
|
266
|
+
Log.e(TAG, "onActivityPaused failed", e);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
@Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}
|
|
273
|
+
@Override public void onActivityStarted(Activity activity) {}
|
|
274
|
+
@Override public void onActivityStopped(Activity activity) { flushAsync(); }
|
|
275
|
+
@Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
|
|
276
|
+
@Override public void onActivityDestroyed(Activity activity) {}
|
|
277
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
//
|
|
2
|
+
// RKTrackerSDK.swift
|
|
3
|
+
// RKTrackerSDK
|
|
4
|
+
// iOS Native Tracker SDK for tracker-sdk repo
|
|
5
|
+
//
|
|
6
|
+
|
|
7
|
+
import UIKit
|
|
8
|
+
|
|
9
|
+
@objc public class RKTrackerSDK: NSObject {
|
|
10
|
+
@objc public static let shared = RKTrackerSDK()
|
|
11
|
+
|
|
12
|
+
private var serverUrl: String = ""
|
|
13
|
+
private var appId: String = "13" // 13: iOS App
|
|
14
|
+
private var sessionId: String = ""
|
|
15
|
+
private var deviceId: String = ""
|
|
16
|
+
private var userId: String = ""
|
|
17
|
+
private var phone: String = ""
|
|
18
|
+
private var storeName: String = ""
|
|
19
|
+
private var sequence: Int = 0
|
|
20
|
+
|
|
21
|
+
private let bgQueue = DispatchQueue(label: "cn.renruikeji.tracker.bgQueue", qos: .utility)
|
|
22
|
+
private var eventQueue: [[String: Any]] = []
|
|
23
|
+
private var flushTimer: Timer?
|
|
24
|
+
|
|
25
|
+
private override init() {
|
|
26
|
+
super.init()
|
|
27
|
+
self.sessionId = "ios_\(Int(Date().timeIntervalSince1970))_\(Int.random(in: 1000...9999))"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@objc public func initSDK(serverUrl: String, deviceId: String, userId: String, phone: String, storeName: String) {
|
|
31
|
+
self.serverUrl = serverUrl
|
|
32
|
+
self.deviceId = deviceId.isEmpty ? (UIDevice.current.identifierForVendor?.uuidString ?? "") : deviceId
|
|
33
|
+
self.userId = userId
|
|
34
|
+
self.phone = phone
|
|
35
|
+
self.storeName = storeName
|
|
36
|
+
|
|
37
|
+
swizzleUIViewController()
|
|
38
|
+
startTimer()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@objc public func getSessionId() -> String {
|
|
42
|
+
return sessionId
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@objc public func track(eventName: String, properties: [String: Any]?) {
|
|
46
|
+
bgQueue.async { [weak self] in
|
|
47
|
+
guard let self = self else { return }
|
|
48
|
+
var event = self.createBaseEvent(eventType: "custom", eventName: eventName)
|
|
49
|
+
if let props = properties, let jsonData = try? JSONSerialization.data(withJSONObject: props), let str = String(data: jsonData, encoding: .utf8) {
|
|
50
|
+
event["extraData"] = str
|
|
51
|
+
}
|
|
52
|
+
self.enqueueEvent(event)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@objc public func trackFrontUrl(frontUrl: String, elementId: String?, postParam: String?) {
|
|
57
|
+
bgQueue.async { [weak self] in
|
|
58
|
+
guard let self = self else { return }
|
|
59
|
+
var event = self.createBaseEvent(eventType: "click", eventName: "legacy_dadian")
|
|
60
|
+
event["frontUrl"] = frontUrl
|
|
61
|
+
if let elem = elementId { event["elementId"] = elem }
|
|
62
|
+
if let post = postParam { event["postParam"] = post }
|
|
63
|
+
self.enqueueEvent(event)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private func createBaseEvent(eventType: String, eventName: String) -> [String: Any] {
|
|
68
|
+
sequence += 1
|
|
69
|
+
var event: [String: Any] = [
|
|
70
|
+
"eventId": "e_\(Int(Date().timeIntervalSince1970 * 1000))_\(Int.random(in: 1000...9999))",
|
|
71
|
+
"eventType": eventType,
|
|
72
|
+
"eventName": eventName,
|
|
73
|
+
"timestamp": Int(Date().timeIntervalSince1970 * 1000),
|
|
74
|
+
"sequence": sequence,
|
|
75
|
+
"os": "iOS"
|
|
76
|
+
]
|
|
77
|
+
return event
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private func enqueueEvent(_ event: [String: Any]) {
|
|
81
|
+
eventQueue.append(event)
|
|
82
|
+
if eventQueue.count >= 20 {
|
|
83
|
+
flushAsync()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private func startTimer() {
|
|
88
|
+
DispatchQueue.main.async {
|
|
89
|
+
self.flushTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
|
|
90
|
+
self?.flushAsync()
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@objc public func flushAsync() {
|
|
96
|
+
bgQueue.async { [weak self] in
|
|
97
|
+
guard let self = self, !self.eventQueue.isEmpty, !self.serverUrl.isEmpty else { return }
|
|
98
|
+
let batchEvents = self.eventQueue
|
|
99
|
+
self.eventQueue.removeAll()
|
|
100
|
+
self.sendBatch(events: batchEvents)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private func sendBatch(events: [[String: Any]]) {
|
|
105
|
+
guard let url = URL(string: serverUrl) else { return }
|
|
106
|
+
var request = URLRequest(url: url)
|
|
107
|
+
request.httpMethod = "POST"
|
|
108
|
+
request.setValue("application/json; charset=UTF-8", forHTTPHeaderField: "Content-Type")
|
|
109
|
+
|
|
110
|
+
let body: [String: Any] = [
|
|
111
|
+
"appId": Int(appId) ?? 13,
|
|
112
|
+
"sessionId": sessionId,
|
|
113
|
+
"deviceId": deviceId,
|
|
114
|
+
"userId": userId,
|
|
115
|
+
"phone": phone,
|
|
116
|
+
"storeName": storeName,
|
|
117
|
+
"platform": "ios",
|
|
118
|
+
"events": events
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
guard let jsonData = try? JSONSerialization.data(withJSONObject: body) else { return }
|
|
122
|
+
request.httpBody = jsonData
|
|
123
|
+
|
|
124
|
+
let task = URLSession.shared.dataTask(with: request) { _, _, _ in }
|
|
125
|
+
task.resume()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- Swizzling for UIViewController Auto-Tracking ---
|
|
129
|
+
private func swizzleUIViewController() {
|
|
130
|
+
let originalSelector = #selector(UIViewController.viewDidAppear(_:))
|
|
131
|
+
let swizzledSelector = #selector(UIViewController.rk_viewDidAppear(_:))
|
|
132
|
+
|
|
133
|
+
guard let originalMethod = class_getInstanceMethod(UIViewController.self, originalSelector),
|
|
134
|
+
let swizzledMethod = class_getInstanceMethod(UIViewController.self, swizzledSelector) else { return }
|
|
135
|
+
|
|
136
|
+
method_exchangeImplementations(originalMethod, swizzledMethod)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
extension UIViewController {
|
|
141
|
+
@objc func rk_viewDidAppear(_ animated: Bool) {
|
|
142
|
+
self.rk_viewDidAppear(animated)
|
|
143
|
+
let pageRoute = String(describing: type(of: self))
|
|
144
|
+
if !pageRoute.hasPrefix("UI") {
|
|
145
|
+
RKTrackerSDK.shared.track(eventName: "page_view", properties: ["pageRoute": pageRoute])
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rk911/tracker-sdk",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "人康多端埋点 SDK —
|
|
3
|
+
"version": "1.7.0",
|
|
4
|
+
"description": "人康多端埋点 SDK — 跨端自动化采集 + 手动埋点双写兼容",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.js",
|
|
7
7
|
"type": "module",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"core/",
|
|
11
11
|
"platforms/",
|
|
12
12
|
"plugins/",
|
|
13
|
-
"customize/"
|
|
13
|
+
"customize/",
|
|
14
|
+
"native/"
|
|
14
15
|
],
|
|
15
16
|
"keywords": [
|
|
16
17
|
"tracker",
|