@rk911/tracker-sdk 1.6.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/compat.js +344 -0
- package/core/config.js +132 -0
- package/core/queue.js +461 -0
- package/core/session.js +128 -0
- package/core/tracker.js +287 -0
- package/customize/index.js +92 -0
- package/customize/modules/common.js +43 -0
- package/customize/modules/store-manager.js +56 -0
- package/index.js +403 -0
- package/package.json +21 -0
- package/platforms/app.js +36 -0
- package/platforms/weapp.js +218 -0
- package/platforms/web.js +483 -0
- package/plugins/api-watch.js +240 -0
- package/plugins/app-lifecycle.js +90 -0
- package/plugins/click.js +557 -0
- package/plugins/exposure.js +376 -0
- package/plugins/js-error.js +232 -0
- package/plugins/page-stay.js +44 -0
- package/plugins/page-view.js +172 -0
- package/plugins/performance.js +157 -0
- package/plugins/scroll-depth.js +256 -0
package/core/queue.js
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
export class ReportQueue {
|
|
2
|
+
constructor(adapter, config, session) {
|
|
3
|
+
this.adapter = adapter
|
|
4
|
+
this.config = config
|
|
5
|
+
this.session = session
|
|
6
|
+
this.queue = []
|
|
7
|
+
this.flushTimer = null
|
|
8
|
+
this.isFlushing = false
|
|
9
|
+
this._hooks = []
|
|
10
|
+
this._flushedEventIds = new Set()
|
|
11
|
+
this._legacyRetryQueue = []
|
|
12
|
+
this._currentPage = null
|
|
13
|
+
this._clickFlushTimer = null
|
|
14
|
+
this._refreshUserId = null
|
|
15
|
+
|
|
16
|
+
this._loadOfflineCache()
|
|
17
|
+
this._loadLegacyCache()
|
|
18
|
+
this._startPageFlushTimer()
|
|
19
|
+
this._setupPageCloseHandler()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
setRefreshUserId(fn) {
|
|
23
|
+
this._refreshUserId = fn
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ---- 自动采集事件入队(页面维度批量上报)----
|
|
27
|
+
|
|
28
|
+
push(event) {
|
|
29
|
+
// 未登录时不入队(allowAnonymousTrack 开启时跳过此检查)
|
|
30
|
+
if (!this.config.allowAnonymousTrack && !this.session.getUserId()) {
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
var eventPage = event.pageRoute || null
|
|
35
|
+
|
|
36
|
+
// 页面边界检测:新事件所属页面与队列中的不同 → 先 flush 旧页面的事件
|
|
37
|
+
if (eventPage && this._currentPage && eventPage !== this._currentPage && this.queue.length > 0) {
|
|
38
|
+
this.flush()
|
|
39
|
+
}
|
|
40
|
+
if (eventPage && eventPage !== '/') {
|
|
41
|
+
this._currentPage = eventPage
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.queue.push(event)
|
|
45
|
+
this._notifyHooks([event])
|
|
46
|
+
|
|
47
|
+
// 达到批量阈值(默认 10 条)→ 立即上报
|
|
48
|
+
if (this.queue.length >= this.config.queue.maxBatchSize) {
|
|
49
|
+
this.flush()
|
|
50
|
+
} else if (event.eventType === 'click') {
|
|
51
|
+
// 点击行为 → 延迟上报(等后续接口请求到齐后一起发)
|
|
52
|
+
this._scheduleClickFlush()
|
|
53
|
+
} else {
|
|
54
|
+
// 重置页面停留定时器(60s 无新事件也 flush)
|
|
55
|
+
this._resetPageFlushTimer()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
flush() {
|
|
60
|
+
if (this.isFlushing || this.queue.length === 0) return
|
|
61
|
+
|
|
62
|
+
if (this._refreshUserId) this._refreshUserId()
|
|
63
|
+
|
|
64
|
+
// [PROJECT-ADAPT] 认证判断逻辑(allowAnonymousTrack 开关可跳过)
|
|
65
|
+
if (!this.config.allowAnonymousTrack) {
|
|
66
|
+
// 检查 1: 没有 userId 不上报
|
|
67
|
+
if (!this.session.getUserId()) {
|
|
68
|
+
if (this.config.debug) {
|
|
69
|
+
console.log('[TrackerSDK] flush skipped: no userId')
|
|
70
|
+
}
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
// 检查 2: 有 getAuthHeaders 时还要验证 token
|
|
74
|
+
if (typeof this.config.getAuthHeaders === 'function') {
|
|
75
|
+
var authHeaders = this.config.getAuthHeaders() || {}
|
|
76
|
+
if (!authHeaders.Authorization && !authHeaders.token && !authHeaders['rsb-token']) {
|
|
77
|
+
if (this.config.debug) {
|
|
78
|
+
console.log('[TrackerSDK] flush skipped: no auth token')
|
|
79
|
+
}
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.isFlushing = true
|
|
86
|
+
var batch = this.queue.splice(0, this.config.queue.maxBatchSize)
|
|
87
|
+
|
|
88
|
+
var self = this
|
|
89
|
+
batch.forEach(function (evt) {
|
|
90
|
+
if (evt.eventId) self._flushedEventIds.add(evt.eventId)
|
|
91
|
+
})
|
|
92
|
+
if (this._flushedEventIds.size > 1000) {
|
|
93
|
+
var toDelete = this._flushedEventIds.size - 500
|
|
94
|
+
var iter = this._flushedEventIds.values()
|
|
95
|
+
for (var i = 0; i < toDelete; i++) {
|
|
96
|
+
this._flushedEventIds.delete(iter.next().value)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
var payload = {
|
|
101
|
+
appId: this.config.appId,
|
|
102
|
+
deviceId: this.session.getDeviceId(),
|
|
103
|
+
sessionId: this.session.getSessionId(),
|
|
104
|
+
userId: this.session.getUserId(),
|
|
105
|
+
platform: this.config.pflag !== '' ? this.config.pflag : (this.config.platform || this.adapter.getPlatformName()),
|
|
106
|
+
events: batch,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
var url = this.config.dadianUrl + this.config.batchReportUrl
|
|
110
|
+
var payloadStr = JSON.stringify(payload)
|
|
111
|
+
|
|
112
|
+
// 分层发送策略:sendBeacon → fetch+keepalive → adapter.request
|
|
113
|
+
var sent = this._sendViaBestChannel(url, payloadStr, batch)
|
|
114
|
+
|
|
115
|
+
if (sent) {
|
|
116
|
+
// sendBeacon / fetch+keepalive 已经发出,非阻塞完成
|
|
117
|
+
this.isFlushing = false
|
|
118
|
+
if (this.queue.length >= this.config.queue.maxBatchSize) {
|
|
119
|
+
this.flush()
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 顺便重试 legacy 离线缓存
|
|
124
|
+
this._retryLegacyCache()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 分层发送策略(普通 flush 用)
|
|
129
|
+
* Layer 1: fetch + keepalive(有 .catch 失败回调 → 可回灌离线缓存,不丢事件)
|
|
130
|
+
* Layer 2: navigator.sendBeacon(非阻塞、页面关闭可靠,但发出即"成功"、无失败回调)
|
|
131
|
+
* Layer 3: adapter.request(传统方式,小程序降级到这里,有 .catch)
|
|
132
|
+
*
|
|
133
|
+
* 顺序说明:普通 flush 优先 fetch —— sendBeacon 无法感知网络失败,
|
|
134
|
+
* 一旦静默丢包这批事件就永久丢失(表现为序号缺口);fetch 的 .catch
|
|
135
|
+
* 能把失败批次存入离线缓存下次回灌。页面关闭场景走 flushSync(beacon 优先)。
|
|
136
|
+
*
|
|
137
|
+
* 注意:sendBeacon 不支持自定义请求头,依赖服务端从 payload 中提取 token/appId/deviceId
|
|
138
|
+
*
|
|
139
|
+
* @returns {boolean} true=已通过非阻塞通道发送 false=走了 adapter.request 异步流程
|
|
140
|
+
*/
|
|
141
|
+
_sendViaBestChannel(url, payloadStr, batch) {
|
|
142
|
+
var self = this
|
|
143
|
+
|
|
144
|
+
// Layer 1: fetch + keepalive(失败可回灌,普通 flush 首选)
|
|
145
|
+
if (typeof fetch !== 'undefined') {
|
|
146
|
+
try {
|
|
147
|
+
var headers = {
|
|
148
|
+
'Content-Type': 'application/json',
|
|
149
|
+
token: this.adapter.getToken() || '',
|
|
150
|
+
appId: String(this.config.appId),
|
|
151
|
+
deviceId: this.session.getDeviceId() || '',
|
|
152
|
+
}
|
|
153
|
+
fetch(url, {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
body: payloadStr,
|
|
156
|
+
headers: headers,
|
|
157
|
+
keepalive: true,
|
|
158
|
+
}).then(function (res) {
|
|
159
|
+
// HTTP 层失败(5xx/4xx)也视为未送达,回灌离线缓存
|
|
160
|
+
if (res && res.ok === false) {
|
|
161
|
+
self._saveToOfflineCache(batch)
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
if (self.config.debug) {
|
|
165
|
+
console.log('[TrackerSDK] batch sent via fetch+keepalive:', batch.length)
|
|
166
|
+
}
|
|
167
|
+
}).catch(function () {
|
|
168
|
+
self._saveToOfflineCache(batch)
|
|
169
|
+
})
|
|
170
|
+
return true
|
|
171
|
+
} catch (e) {
|
|
172
|
+
// fetch 不可用或 keepalive 超限(>64KB),降级
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Layer 2: sendBeacon(fetch 不可用时的非阻塞兜底)
|
|
177
|
+
// 限制:无法设置自定义请求头 + 无失败回调;payload 超 64KB 可能静默失败,限制 60KB 留余量
|
|
178
|
+
if (typeof navigator !== 'undefined' && navigator.sendBeacon && payloadStr.length < 61440) {
|
|
179
|
+
try {
|
|
180
|
+
var blob = new Blob([payloadStr], { type: 'application/json' })
|
|
181
|
+
var sent = navigator.sendBeacon(url, blob)
|
|
182
|
+
if (sent) {
|
|
183
|
+
if (this.config.debug) {
|
|
184
|
+
console.log('[TrackerSDK] batch sent via sendBeacon:', batch.length)
|
|
185
|
+
}
|
|
186
|
+
return true
|
|
187
|
+
}
|
|
188
|
+
// sendBeacon 返回 false(通常是数据超 64KB),降级到 Layer 3
|
|
189
|
+
} catch (e) {
|
|
190
|
+
// 某些环境 sendBeacon 抛异常,降级
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Layer 3: adapter.request(小程序 / 极老浏览器降级)
|
|
195
|
+
var header = {
|
|
196
|
+
'content-type': 'application/json',
|
|
197
|
+
token: this.adapter.getToken() || '',
|
|
198
|
+
appId: String(this.config.appId),
|
|
199
|
+
deviceId: this.session.getDeviceId() || '',
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this.adapter
|
|
203
|
+
.request({ method: 'POST', url: url, data: JSON.parse(payloadStr), header: header })
|
|
204
|
+
.then(function () {
|
|
205
|
+
if (self.config.debug) {
|
|
206
|
+
console.log('[TrackerSDK] batch sent via adapter.request:', batch.length)
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
.catch(function () {
|
|
210
|
+
self._saveToOfflineCache(batch)
|
|
211
|
+
})
|
|
212
|
+
.finally(function () {
|
|
213
|
+
self.isFlushing = false
|
|
214
|
+
if (self.queue.length >= self.config.queue.maxBatchSize) {
|
|
215
|
+
self.flush()
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
return false // adapter.request 是异步的,isFlushing 由 finally 处理
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---- 手动上报(走旧接口)----
|
|
223
|
+
|
|
224
|
+
pushLegacy(url, data, header) {
|
|
225
|
+
var self = this
|
|
226
|
+
this.adapter
|
|
227
|
+
.request({ method: 'POST', url: url, data: data, header: header })
|
|
228
|
+
.then(function (res) {
|
|
229
|
+
if (self.config.debug) {
|
|
230
|
+
console.log('[TrackerSDK] legacy report:', data.frontUrl)
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
.catch(function () {
|
|
234
|
+
self._legacyRetryQueue.push({ url: url, data: data, header: header })
|
|
235
|
+
self._saveLegacyCache()
|
|
236
|
+
if (self.config.debug) {
|
|
237
|
+
console.warn('[TrackerSDK] legacy report failed, cached for retry:', data.frontUrl)
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ---- Legacy 离线缓存与重试 ----
|
|
243
|
+
|
|
244
|
+
_saveLegacyCache() {
|
|
245
|
+
try {
|
|
246
|
+
var items = this._legacyRetryQueue.slice(-50)
|
|
247
|
+
this.adapter.setStorage('_tracker_legacy_cache', items)
|
|
248
|
+
} catch (e) {}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
_loadLegacyCache() {
|
|
252
|
+
try {
|
|
253
|
+
var cached = this.adapter.getStorage('_tracker_legacy_cache')
|
|
254
|
+
if (Array.isArray(cached) && cached.length > 0) {
|
|
255
|
+
this._legacyRetryQueue = cached
|
|
256
|
+
this.adapter.removeStorage('_tracker_legacy_cache')
|
|
257
|
+
}
|
|
258
|
+
} catch (e) {}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
_retryLegacyCache() {
|
|
262
|
+
if (this._legacyRetryQueue.length === 0) return
|
|
263
|
+
var items = this._legacyRetryQueue.splice(0)
|
|
264
|
+
var self = this
|
|
265
|
+
items.forEach(function (item) {
|
|
266
|
+
self.adapter
|
|
267
|
+
.request({ method: 'POST', url: item.url, data: item.data, header: item.header })
|
|
268
|
+
.then(function () {
|
|
269
|
+
if (self.config.debug) {
|
|
270
|
+
console.log('[TrackerSDK] legacy retry success:', item.data.frontUrl)
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
.catch(function () {
|
|
274
|
+
self._legacyRetryQueue.push(item)
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
this._saveLegacyCache()
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ---- Hook(供验证脚本等外部消费)----
|
|
281
|
+
|
|
282
|
+
onReport(fn) {
|
|
283
|
+
if (typeof fn === 'function') {
|
|
284
|
+
this._hooks.push(fn)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
_notifyHooks(events) {
|
|
289
|
+
this._hooks.forEach(function (fn) {
|
|
290
|
+
try {
|
|
291
|
+
fn(events)
|
|
292
|
+
} catch (e) {}
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ---- 页面关闭前强制上报 ----
|
|
297
|
+
|
|
298
|
+
flushSync() {
|
|
299
|
+
if (this.queue.length === 0) return
|
|
300
|
+
|
|
301
|
+
if (this._refreshUserId) this._refreshUserId()
|
|
302
|
+
|
|
303
|
+
// 与 flush() 相同的身份校验:无 userId 时不上报
|
|
304
|
+
if (!this.config.allowAnonymousTrack && !this.session.getUserId()) {
|
|
305
|
+
this.queue = []
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
var self = this
|
|
310
|
+
var remaining = this.queue.filter(function (evt) {
|
|
311
|
+
return !self._flushedEventIds.has(evt.eventId)
|
|
312
|
+
})
|
|
313
|
+
this.queue = []
|
|
314
|
+
|
|
315
|
+
if (remaining.length === 0) return
|
|
316
|
+
|
|
317
|
+
remaining.forEach(function (evt) {
|
|
318
|
+
if (evt.eventId) self._flushedEventIds.add(evt.eventId)
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
var payload = {
|
|
322
|
+
appId: this.config.appId,
|
|
323
|
+
deviceId: this.session.getDeviceId(),
|
|
324
|
+
sessionId: this.session.getSessionId(),
|
|
325
|
+
userId: this.session.getUserId(),
|
|
326
|
+
platform: this.config.pflag !== '' ? this.config.pflag : (this.config.platform || this.adapter.getPlatformName()),
|
|
327
|
+
events: remaining,
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
var url = this.config.dadianUrl + this.config.batchReportUrl
|
|
331
|
+
var payloadStr = JSON.stringify(payload)
|
|
332
|
+
|
|
333
|
+
// 先备份到离线缓存:sendBeacon 无失败回调,网络失败时事件会永久丢失(序号缺口)。
|
|
334
|
+
// 备份后即使 sendBeacon 静默失败,下次启动时离线缓存可回灌。
|
|
335
|
+
// 后端按 eventId 幂等去重,不会产生重复。
|
|
336
|
+
this._saveToOfflineCache(remaining)
|
|
337
|
+
|
|
338
|
+
// 页面关闭时优先 sendBeacon(保证送达),超 60KB 跳过避免静默失败
|
|
339
|
+
if (typeof navigator !== 'undefined' && navigator.sendBeacon && payloadStr.length < 61440) {
|
|
340
|
+
try {
|
|
341
|
+
var blob = new Blob([payloadStr], { type: 'application/json' })
|
|
342
|
+
var sent = navigator.sendBeacon(url, blob)
|
|
343
|
+
if (sent) {
|
|
344
|
+
// sendBeacon 成功排队,清除刚写入的离线备份(大概率会送达)
|
|
345
|
+
this._removeOfflineEvents(remaining)
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
} catch (e) {}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// sendBeacon 失败降级为 adapter.sendBeacon(平台适配层实现)
|
|
352
|
+
this.adapter.sendBeacon(url, payload)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ---- 调度策略:页面维度定时器(停留超时 flush)----
|
|
356
|
+
|
|
357
|
+
_startPageFlushTimer() {
|
|
358
|
+
this._resetPageFlushTimer()
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
_resetPageFlushTimer() {
|
|
362
|
+
var self = this
|
|
363
|
+
if (this.flushTimer) {
|
|
364
|
+
clearTimeout(this.flushTimer)
|
|
365
|
+
}
|
|
366
|
+
this.flushTimer = setTimeout(function () {
|
|
367
|
+
self.flush()
|
|
368
|
+
// flush 后继续下一轮计时
|
|
369
|
+
self._resetPageFlushTimer()
|
|
370
|
+
}, this.config.queue.flushInterval)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// 点击后延迟 flush:等后续接口请求到齐后一起上报
|
|
374
|
+
_scheduleClickFlush() {
|
|
375
|
+
if (this._clickFlushTimer) return
|
|
376
|
+
var self = this
|
|
377
|
+
this._clickFlushTimer = setTimeout(function () {
|
|
378
|
+
self._clickFlushTimer = null
|
|
379
|
+
self.flush()
|
|
380
|
+
}, this.config.queue.clickFlushDelay)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
_stopAutoFlush() {
|
|
384
|
+
if (this.flushTimer) {
|
|
385
|
+
clearTimeout(this.flushTimer)
|
|
386
|
+
this.flushTimer = null
|
|
387
|
+
}
|
|
388
|
+
if (this._clickFlushTimer) {
|
|
389
|
+
clearTimeout(this._clickFlushTimer)
|
|
390
|
+
this._clickFlushTimer = null
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ---- 页面关闭处理 ----
|
|
395
|
+
|
|
396
|
+
_setupPageCloseHandler() {
|
|
397
|
+
var self = this
|
|
398
|
+
this.adapter.onPageHide(function () {
|
|
399
|
+
self.flushSync()
|
|
400
|
+
})
|
|
401
|
+
this.adapter.onPageClose(function () {
|
|
402
|
+
self.flushSync()
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ---- 离线缓存 ----
|
|
407
|
+
|
|
408
|
+
_saveToOfflineCache(events) {
|
|
409
|
+
try {
|
|
410
|
+
var key = this.config.queue.offlineCacheKey
|
|
411
|
+
var cached = this.adapter.getStorage(key)
|
|
412
|
+
var existing = Array.isArray(cached) ? cached : []
|
|
413
|
+
var maxOffline = 500
|
|
414
|
+
var merged = existing.concat(events)
|
|
415
|
+
if (merged.length > maxOffline) {
|
|
416
|
+
merged = merged.slice(merged.length - maxOffline)
|
|
417
|
+
}
|
|
418
|
+
this.adapter.setStorage(key, merged)
|
|
419
|
+
} catch (e) {
|
|
420
|
+
// quota 超限时清空离线缓存,保证 localStorage 可用
|
|
421
|
+
try {
|
|
422
|
+
this.adapter.removeStorage(this.config.queue.offlineCacheKey)
|
|
423
|
+
} catch (ignore) {}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
_loadOfflineCache() {
|
|
428
|
+
try {
|
|
429
|
+
var key = this.config.queue.offlineCacheKey
|
|
430
|
+
var cached = this.adapter.getStorage(key)
|
|
431
|
+
if (Array.isArray(cached) && cached.length > 0) {
|
|
432
|
+
this.queue = cached.concat(this.queue)
|
|
433
|
+
this.adapter.removeStorage(key)
|
|
434
|
+
}
|
|
435
|
+
} catch (e) {}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
_removeOfflineEvents(events) {
|
|
439
|
+
try {
|
|
440
|
+
var key = this.config.queue.offlineCacheKey
|
|
441
|
+
var cached = this.adapter.getStorage(key)
|
|
442
|
+
if (!Array.isArray(cached) || cached.length === 0) return
|
|
443
|
+
var ids = new Set()
|
|
444
|
+
events.forEach(function (evt) { if (evt.eventId) ids.add(evt.eventId) })
|
|
445
|
+
var filtered = cached.filter(function (evt) { return !ids.has(evt.eventId) })
|
|
446
|
+
if (filtered.length === 0) {
|
|
447
|
+
this.adapter.removeStorage(key)
|
|
448
|
+
} else {
|
|
449
|
+
this.adapter.setStorage(key, filtered)
|
|
450
|
+
}
|
|
451
|
+
} catch (e) {}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ---- 清理 ----
|
|
455
|
+
|
|
456
|
+
destroy() {
|
|
457
|
+
this._stopAutoFlush()
|
|
458
|
+
this.flushSync()
|
|
459
|
+
this._hooks = []
|
|
460
|
+
}
|
|
461
|
+
}
|
package/core/session.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
var DEVICE_ID_KEY = '_tracker_device_id'
|
|
2
|
+
|
|
3
|
+
export class SessionManager {
|
|
4
|
+
constructor(adapter, config) {
|
|
5
|
+
this.adapter = adapter
|
|
6
|
+
this.config = config
|
|
7
|
+
this.sessionId = null
|
|
8
|
+
this.startTime = 0
|
|
9
|
+
this.lastActivityTime = 0
|
|
10
|
+
this.sequence = 0
|
|
11
|
+
this.userId = null
|
|
12
|
+
this.deviceId = null
|
|
13
|
+
this._storageKey = config.session.storageKey || '_tracker_session'
|
|
14
|
+
this._renewListeners = []
|
|
15
|
+
|
|
16
|
+
this._initDeviceId()
|
|
17
|
+
this._restore()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_initDeviceId() {
|
|
21
|
+
var stored = this.adapter.getStorage(DEVICE_ID_KEY)
|
|
22
|
+
|| this.adapter.getStorage('deviceId')
|
|
23
|
+
if (stored) {
|
|
24
|
+
this.deviceId = stored
|
|
25
|
+
this.adapter.setStorage(DEVICE_ID_KEY, stored)
|
|
26
|
+
} else {
|
|
27
|
+
this.deviceId = this.adapter.generateId('dev')
|
|
28
|
+
this.adapter.setStorage(DEVICE_ID_KEY, this.deviceId)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getSessionId() {
|
|
33
|
+
if (!this.sessionId || this._isExpired()) {
|
|
34
|
+
var wasRenew = !!this.sessionId
|
|
35
|
+
this._createNew()
|
|
36
|
+
if (wasRenew) this._notifyRenew()
|
|
37
|
+
}
|
|
38
|
+
return this.sessionId
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
onRenew(fn) {
|
|
42
|
+
if (typeof fn === 'function') this._renewListeners.push(fn)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_notifyRenew() {
|
|
46
|
+
this._renewListeners.forEach(function (fn) { try { fn() } catch (e) {} })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
nextSequence() {
|
|
50
|
+
this.sequence++
|
|
51
|
+
return this.sequence
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
touch() {
|
|
55
|
+
this.lastActivityTime = this.adapter.getTimestamp()
|
|
56
|
+
this._save()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setUser(userId) {
|
|
60
|
+
this.userId = userId
|
|
61
|
+
this._save()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setDeviceId(deviceId) {
|
|
65
|
+
this.deviceId = deviceId
|
|
66
|
+
this.adapter.setStorage(DEVICE_ID_KEY, deviceId)
|
|
67
|
+
this._save()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getUserId() {
|
|
71
|
+
return this.userId
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getDeviceId() {
|
|
75
|
+
return this.deviceId
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_isExpired() {
|
|
79
|
+
if (!this.lastActivityTime) return true
|
|
80
|
+
var now = this.adapter.getTimestamp()
|
|
81
|
+
return now - this.lastActivityTime > this.config.session.timeout
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
_createNew() {
|
|
85
|
+
this.sessionId = this.adapter.generateId('sess')
|
|
86
|
+
this.startTime = this.adapter.getTimestamp()
|
|
87
|
+
this.lastActivityTime = this.startTime
|
|
88
|
+
this.sequence = 0
|
|
89
|
+
this._save()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_save() {
|
|
93
|
+
var data = {
|
|
94
|
+
sessionId: this.sessionId,
|
|
95
|
+
startTime: this.startTime,
|
|
96
|
+
lastActivityTime: this.lastActivityTime,
|
|
97
|
+
sequence: this.sequence,
|
|
98
|
+
}
|
|
99
|
+
this.adapter.setSessionStorage(this._storageKey, data)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_restore() {
|
|
103
|
+
var data = this.adapter.getSessionStorage(this._storageKey)
|
|
104
|
+
if (data && data.sessionId) {
|
|
105
|
+
this.sessionId = data.sessionId
|
|
106
|
+
this.startTime = data.startTime
|
|
107
|
+
this.lastActivityTime = data.lastActivityTime
|
|
108
|
+
this.sequence = data.sequence || 0
|
|
109
|
+
|
|
110
|
+
if (this._isExpired()) {
|
|
111
|
+
this._createNew()
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
this._createNew()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
resetSession() {
|
|
119
|
+
this.adapter.removeSessionStorage(this._storageKey)
|
|
120
|
+
this.userId = null
|
|
121
|
+
this._createNew()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
destroy() {
|
|
125
|
+
this.adapter.removeSessionStorage(this._storageKey)
|
|
126
|
+
this.sessionId = null
|
|
127
|
+
}
|
|
128
|
+
}
|