@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 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
- this.deviceId = deviceId
66
- this.adapter.setStorage(DEVICE_ID_KEY, deviceId)
67
- this._save()
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 existingId = adapter.getStorage('deviceId')
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.6.8",
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",