@clipto/reporter 0.1.0 → 0.2.1
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/README.md +86 -72
- package/dist/index.cjs +495 -76
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +133 -46
- package/dist/index.d.ts +133 -46
- package/dist/index.js +495 -76
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/index.cjs
CHANGED
|
@@ -45,38 +45,16 @@ function generateEventId() {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// src/builtins.ts
|
|
48
|
-
var SDK_VERSION = "1.0
|
|
48
|
+
var SDK_VERSION = "1.0";
|
|
49
49
|
var DB_NAME = "CliptoReportDB";
|
|
50
50
|
var STORE_NAME = "clipto-reporting";
|
|
51
51
|
var QUEUE_KEY = "pending";
|
|
52
52
|
var LS_KEY = "clipto_report_pending_queue";
|
|
53
53
|
function buildPayload(events) {
|
|
54
54
|
return {
|
|
55
|
-
|
|
56
|
-
events: events.map(
|
|
57
|
-
({
|
|
58
|
-
id,
|
|
59
|
-
name,
|
|
60
|
-
timestamp,
|
|
61
|
-
sessionId,
|
|
62
|
-
userId,
|
|
63
|
-
deviceId,
|
|
64
|
-
properties,
|
|
65
|
-
category,
|
|
66
|
-
context
|
|
67
|
-
}) => ({
|
|
68
|
-
id,
|
|
69
|
-
eventName: name,
|
|
70
|
-
timestamp,
|
|
71
|
-
sessionId,
|
|
72
|
-
userId,
|
|
73
|
-
deviceId,
|
|
74
|
-
properties,
|
|
75
|
-
category,
|
|
76
|
-
context
|
|
77
|
-
})
|
|
78
|
-
),
|
|
55
|
+
events,
|
|
79
56
|
metadata: {
|
|
57
|
+
type: "tracker_event",
|
|
80
58
|
sdkVersion: SDK_VERSION,
|
|
81
59
|
timestamp: Date.now(),
|
|
82
60
|
batchId: generateEventId()
|
|
@@ -84,17 +62,26 @@ function buildPayload(events) {
|
|
|
84
62
|
}
|
|
85
63
|
};
|
|
86
64
|
}
|
|
87
|
-
function
|
|
65
|
+
async function signHeaders(payload, signer) {
|
|
66
|
+
if (!signer) return {};
|
|
67
|
+
return signer(JSON.stringify(payload));
|
|
68
|
+
}
|
|
69
|
+
function createBrowserTransport(endpoint, headers, signer) {
|
|
88
70
|
const commonHeaders = {
|
|
89
71
|
"Content-Type": "application/json",
|
|
90
72
|
...headers ?? {}
|
|
91
73
|
};
|
|
92
74
|
return {
|
|
93
75
|
async send(events) {
|
|
76
|
+
const payload = buildPayload(events);
|
|
77
|
+
const requestHeaders = {
|
|
78
|
+
...commonHeaders,
|
|
79
|
+
...await signHeaders(payload, signer)
|
|
80
|
+
};
|
|
94
81
|
const response = await fetch(endpoint, {
|
|
95
82
|
method: "POST",
|
|
96
|
-
headers:
|
|
97
|
-
body: JSON.stringify(
|
|
83
|
+
headers: requestHeaders,
|
|
84
|
+
body: JSON.stringify(payload),
|
|
98
85
|
keepalive: true
|
|
99
86
|
});
|
|
100
87
|
if (!response.ok) {
|
|
@@ -102,7 +89,7 @@ function createBrowserTransport(endpoint, headers) {
|
|
|
102
89
|
}
|
|
103
90
|
},
|
|
104
91
|
sendSync(events) {
|
|
105
|
-
if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function") {
|
|
92
|
+
if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function" || signer) {
|
|
106
93
|
return false;
|
|
107
94
|
}
|
|
108
95
|
try {
|
|
@@ -116,12 +103,16 @@ function createBrowserTransport(endpoint, headers) {
|
|
|
116
103
|
}
|
|
117
104
|
};
|
|
118
105
|
}
|
|
119
|
-
async function createNodeTransport(endpoint, headers) {
|
|
106
|
+
async function createNodeTransport(endpoint, headers, signer) {
|
|
120
107
|
const { default: axios } = await import("axios");
|
|
121
108
|
return {
|
|
122
109
|
async send(events) {
|
|
123
|
-
|
|
124
|
-
|
|
110
|
+
const payload = buildPayload(events);
|
|
111
|
+
await axios.post(endpoint, payload, {
|
|
112
|
+
headers: {
|
|
113
|
+
...headers ?? {},
|
|
114
|
+
...await signHeaders(payload, signer)
|
|
115
|
+
},
|
|
125
116
|
timeout: 1e4
|
|
126
117
|
});
|
|
127
118
|
}
|
|
@@ -199,6 +190,167 @@ async function createNodeStorage(persistDir) {
|
|
|
199
190
|
};
|
|
200
191
|
}
|
|
201
192
|
|
|
193
|
+
// src/collect.ts
|
|
194
|
+
var import_ua_parser_js = require("ua-parser-js");
|
|
195
|
+
function parseUserAgent(ua) {
|
|
196
|
+
const parsed = new import_ua_parser_js.UAParser(ua);
|
|
197
|
+
const browser = parsed.getBrowser();
|
|
198
|
+
const os = parsed.getOS();
|
|
199
|
+
const info = {};
|
|
200
|
+
if (browser.name) {
|
|
201
|
+
info.browserName = browser.name === "Mobile Safari" ? "Safari" : browser.name === "Mobile Chrome" ? "Chrome" : browser.name;
|
|
202
|
+
}
|
|
203
|
+
if (browser.version) info.browserVersion = browser.version;
|
|
204
|
+
if (os.name) info.osName = os.name === "Mac OS" ? "macOS" : os.name;
|
|
205
|
+
if (os.version) info.osVersion = os.version;
|
|
206
|
+
return info;
|
|
207
|
+
}
|
|
208
|
+
function collectBrowserDevice() {
|
|
209
|
+
if (typeof navigator === "undefined") return {};
|
|
210
|
+
const { browserName, browserVersion, osName, osVersion } = parseUserAgent(
|
|
211
|
+
navigator.userAgent || ""
|
|
212
|
+
);
|
|
213
|
+
const device = {};
|
|
214
|
+
if (browserName) device.browserName = browserName;
|
|
215
|
+
if (browserVersion) device.browserVersion = browserVersion;
|
|
216
|
+
if (osName) device.osName = osName;
|
|
217
|
+
if (osVersion) device.osVersion = osVersion;
|
|
218
|
+
const uaData = navigator.userAgentData;
|
|
219
|
+
if (uaData?.platform) device.platform = uaData.platform;
|
|
220
|
+
else if (navigator.platform) device.platform = navigator.platform;
|
|
221
|
+
if (navigator.language) device.language = navigator.language;
|
|
222
|
+
return device;
|
|
223
|
+
}
|
|
224
|
+
async function collectNodeDevice() {
|
|
225
|
+
try {
|
|
226
|
+
const os = await import("os");
|
|
227
|
+
const device = {
|
|
228
|
+
platform: os.platform(),
|
|
229
|
+
osVersion: os.release(),
|
|
230
|
+
arch: os.arch(),
|
|
231
|
+
memSize: Math.round(os.totalmem() / 1024 ** 3)
|
|
232
|
+
// 统一 GB
|
|
233
|
+
};
|
|
234
|
+
const type = os.type();
|
|
235
|
+
device.osName = type === "Darwin" ? "macOS" : type === "Windows_NT" ? "Windows" : type;
|
|
236
|
+
const cpu = os.cpus()[0]?.model;
|
|
237
|
+
if (cpu) device.cpu = cpu;
|
|
238
|
+
return device;
|
|
239
|
+
} catch {
|
|
240
|
+
return {};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function collectPageSnapshot() {
|
|
244
|
+
if (typeof document === "undefined") return {};
|
|
245
|
+
const page = {};
|
|
246
|
+
if (location.href) page.url = location.href;
|
|
247
|
+
if (document.title) page.title = document.title;
|
|
248
|
+
if (document.referrer) page.referrer = document.referrer;
|
|
249
|
+
return page;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/page.ts
|
|
253
|
+
var SCROLL_GESTURE_GAP_MS = 500;
|
|
254
|
+
var IDLE_PAUSE_MS = 6e4;
|
|
255
|
+
var METRIC_TICK_MS = 1e3;
|
|
256
|
+
function createPageTracker(onActivity) {
|
|
257
|
+
let visit = null;
|
|
258
|
+
let interval = null;
|
|
259
|
+
const touch = () => {
|
|
260
|
+
if (!visit) return;
|
|
261
|
+
visit.lastActivityAt = Date.now();
|
|
262
|
+
onActivity();
|
|
263
|
+
};
|
|
264
|
+
const updateMaxDepth = () => {
|
|
265
|
+
if (!visit) return;
|
|
266
|
+
const doc = document.documentElement;
|
|
267
|
+
if (!doc) return;
|
|
268
|
+
const total = Math.max(doc.scrollHeight, 1);
|
|
269
|
+
const depth = Math.round(
|
|
270
|
+
(window.scrollY + window.innerHeight) / total * 100
|
|
271
|
+
);
|
|
272
|
+
visit.maxScrollDepthPct = Math.max(
|
|
273
|
+
visit.maxScrollDepthPct,
|
|
274
|
+
Math.min(depth, 100)
|
|
275
|
+
);
|
|
276
|
+
};
|
|
277
|
+
const onScroll = () => {
|
|
278
|
+
if (!visit) return;
|
|
279
|
+
const now = Date.now();
|
|
280
|
+
if (now - visit.lastScrollAt > SCROLL_GESTURE_GAP_MS) {
|
|
281
|
+
visit.scrollCount += 1;
|
|
282
|
+
}
|
|
283
|
+
visit.lastScrollAt = now;
|
|
284
|
+
touch();
|
|
285
|
+
updateMaxDepth();
|
|
286
|
+
};
|
|
287
|
+
const onInput = () => touch();
|
|
288
|
+
const tick = () => {
|
|
289
|
+
if (!visit) return;
|
|
290
|
+
if (document.visibilityState === "visible" && Date.now() - visit.lastActivityAt <= IDLE_PAUSE_MS) {
|
|
291
|
+
visit.activeMs += METRIC_TICK_MS;
|
|
292
|
+
}
|
|
293
|
+
updateMaxDepth();
|
|
294
|
+
};
|
|
295
|
+
const removeListeners = () => {
|
|
296
|
+
window.removeEventListener("scroll", onScroll);
|
|
297
|
+
window.removeEventListener("pointerdown", onInput, { capture: true });
|
|
298
|
+
window.removeEventListener("keydown", onInput, { capture: true });
|
|
299
|
+
if (interval) {
|
|
300
|
+
clearInterval(interval);
|
|
301
|
+
interval = null;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
const startVisit = () => {
|
|
305
|
+
if (visit) return;
|
|
306
|
+
const now = Date.now();
|
|
307
|
+
visit = {
|
|
308
|
+
pageViewId: `pv_${generateEventId()}`,
|
|
309
|
+
enteredAt: now,
|
|
310
|
+
snapshot: collectPageSnapshot(),
|
|
311
|
+
activeMs: 0,
|
|
312
|
+
scrollCount: 0,
|
|
313
|
+
maxScrollDepthPct: 0,
|
|
314
|
+
lastActivityAt: now,
|
|
315
|
+
lastScrollAt: 0
|
|
316
|
+
};
|
|
317
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
318
|
+
window.addEventListener("pointerdown", onInput, {
|
|
319
|
+
capture: true,
|
|
320
|
+
passive: true
|
|
321
|
+
});
|
|
322
|
+
window.addEventListener("keydown", onInput, { capture: true, passive: true });
|
|
323
|
+
interval = setInterval(tick, METRIC_TICK_MS);
|
|
324
|
+
};
|
|
325
|
+
const endVisit = (reason) => {
|
|
326
|
+
if (!visit) return void 0;
|
|
327
|
+
removeListeners();
|
|
328
|
+
const finished = visit;
|
|
329
|
+
visit = null;
|
|
330
|
+
if (document.title) {
|
|
331
|
+
finished.snapshot.title = document.title;
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
...finished.snapshot,
|
|
335
|
+
pageViewId: finished.pageViewId,
|
|
336
|
+
durationMs: Date.now() - finished.enteredAt,
|
|
337
|
+
activeMs: finished.activeMs,
|
|
338
|
+
scrollCount: finished.scrollCount,
|
|
339
|
+
maxScrollDepthPct: finished.maxScrollDepthPct,
|
|
340
|
+
endReason: reason
|
|
341
|
+
};
|
|
342
|
+
};
|
|
343
|
+
return {
|
|
344
|
+
get active() {
|
|
345
|
+
return visit !== null;
|
|
346
|
+
},
|
|
347
|
+
startVisit,
|
|
348
|
+
endVisit,
|
|
349
|
+
pageViewId: () => visit?.pageViewId,
|
|
350
|
+
dispose: removeListeners
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
202
354
|
// src/reporter.ts
|
|
203
355
|
var DEFAULT_FLUSH_INTERVAL = 2e3;
|
|
204
356
|
var DEFAULT_MAX_BATCH_SIZE = 10;
|
|
@@ -207,11 +359,33 @@ var DEFAULT_MAX_QUEUE_SIZE = 1e3;
|
|
|
207
359
|
var DEFAULT_MAX_ATTEMPTS = 3;
|
|
208
360
|
var DEFAULT_BACKOFF_MS = 1e3;
|
|
209
361
|
var STORAGE_WRITE_THROTTLE_MS = 500;
|
|
362
|
+
var VISIT_IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
363
|
+
var SESSION_SAVE_THROTTLE_MS = 1e4;
|
|
364
|
+
var ANONYMOUS_ID_KEY = "clipto_report_anonymous_id";
|
|
365
|
+
var CLIENT_ID_KEY = "clipto_report_client_id";
|
|
366
|
+
var SESSION_KEY = "clipto_report_session";
|
|
367
|
+
function normalizeContext(context) {
|
|
368
|
+
if (typeof context === "function") return context;
|
|
369
|
+
return () => context ?? {};
|
|
370
|
+
}
|
|
371
|
+
function stripAttempts(events) {
|
|
372
|
+
return events.map((event) => {
|
|
373
|
+
const { attempts: _attempts, ...rest } = event;
|
|
374
|
+
return rest;
|
|
375
|
+
});
|
|
376
|
+
}
|
|
210
377
|
var Reporter = class {
|
|
211
378
|
constructor(options) {
|
|
212
|
-
/**
|
|
213
|
-
this.
|
|
214
|
-
|
|
379
|
+
/** 会话落盘时间戳(节流用);声明在 session 之前:session 初始化会触发强制落盘 */
|
|
380
|
+
this.sessionSaveAt = 0;
|
|
381
|
+
/** 会话共享读时间戳(节流用):多 tab 共享会话,活动时定期重读 localStorage 最新值 */
|
|
382
|
+
this.sessionReadAt = 0;
|
|
383
|
+
/** Node 端设备快照(init 时采集一次,避免每次入队动态加载 os) */
|
|
384
|
+
this.nodeDevice = {};
|
|
385
|
+
/** 页面访问跟踪(browser + autoPageView 时启用) */
|
|
386
|
+
this.pageTracker = null;
|
|
387
|
+
/** history 补丁的原始引用(dispose 时还原) */
|
|
388
|
+
this.historyPatch = null;
|
|
215
389
|
/** 待发队列(内存中的事实源,持久化是它的影子) */
|
|
216
390
|
this.queue = [];
|
|
217
391
|
this.flushTimer = null;
|
|
@@ -224,13 +398,31 @@ var Reporter = class {
|
|
|
224
398
|
this.fire(this.dispose());
|
|
225
399
|
};
|
|
226
400
|
this.onPageHide = () => {
|
|
401
|
+
this.endPageVisit(this.session.visitId, "close");
|
|
227
402
|
this.fire(this.dispose());
|
|
228
403
|
};
|
|
404
|
+
this.onPageShow = () => {
|
|
405
|
+
if (document.visibilityState === "visible") {
|
|
406
|
+
this.pageTracker?.startVisit();
|
|
407
|
+
}
|
|
408
|
+
};
|
|
229
409
|
this.onVisibilityChange = () => {
|
|
230
410
|
if (document.visibilityState === "hidden") {
|
|
411
|
+
this.endPageVisit(this.session.visitId, "background");
|
|
231
412
|
this.fire(this.flush());
|
|
232
|
-
} else
|
|
233
|
-
this.
|
|
413
|
+
} else {
|
|
414
|
+
this.pageTracker?.startVisit();
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
this.onPopState = () => {
|
|
418
|
+
this.endPageVisit(this.session.visitId, "back");
|
|
419
|
+
this.pageTracker?.startVisit();
|
|
420
|
+
};
|
|
421
|
+
this.onStorage = (event) => {
|
|
422
|
+
if (event.key !== SESSION_KEY || !event.newValue) return;
|
|
423
|
+
try {
|
|
424
|
+
this.adoptSharedSession(JSON.parse(event.newValue));
|
|
425
|
+
} catch {
|
|
234
426
|
}
|
|
235
427
|
};
|
|
236
428
|
if (!options.endpoint) {
|
|
@@ -238,6 +430,7 @@ var Reporter = class {
|
|
|
238
430
|
}
|
|
239
431
|
this.opts = options;
|
|
240
432
|
this.platform = options.platform ?? (typeof window !== "undefined" ? "browser" : "node");
|
|
433
|
+
this.contextFn = normalizeContext(options.context);
|
|
241
434
|
this.flushInterval = options.flushInterval ?? DEFAULT_FLUSH_INTERVAL;
|
|
242
435
|
this.maxBatchSize = options.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
|
|
243
436
|
this.maxBatchBytes = options.maxBatchBytes ?? DEFAULT_MAX_BATCH_BYTES;
|
|
@@ -246,16 +439,20 @@ var Reporter = class {
|
|
|
246
439
|
this.maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
247
440
|
this.backoffMs = options.backoffMs ?? DEFAULT_BACKOFF_MS;
|
|
248
441
|
this.flushOnExit = options.flushOnExit ?? true;
|
|
442
|
+
this._anonymousId = this.loadOrCreateAnonymousId();
|
|
443
|
+
this._clientId = this.loadOrCreateClientId();
|
|
444
|
+
this.session = this.loadOrCreateSession();
|
|
249
445
|
if (options.transport) {
|
|
250
446
|
this.transportPromise = Promise.resolve(options.transport);
|
|
251
447
|
} else if (this.platform === "browser") {
|
|
252
448
|
this.transportPromise = Promise.resolve(
|
|
253
|
-
createBrowserTransport(options.endpoint, options.headers)
|
|
449
|
+
createBrowserTransport(options.endpoint, options.headers, options.signer)
|
|
254
450
|
);
|
|
255
451
|
} else {
|
|
256
452
|
this.transportPromise = createNodeTransport(
|
|
257
453
|
options.endpoint,
|
|
258
|
-
options.headers
|
|
454
|
+
options.headers,
|
|
455
|
+
options.signer
|
|
259
456
|
);
|
|
260
457
|
}
|
|
261
458
|
if (options.storage) {
|
|
@@ -271,35 +468,20 @@ var Reporter = class {
|
|
|
271
468
|
}
|
|
272
469
|
this.fire(this.init());
|
|
273
470
|
}
|
|
274
|
-
track(
|
|
471
|
+
track(eventName, properties, options) {
|
|
275
472
|
if (this.disposed) {
|
|
276
|
-
this.log("dispose \u540E\u5FFD\u7565\u65B0\u4E8B\u4EF6:",
|
|
473
|
+
this.log("dispose \u540E\u5FFD\u7565\u65B0\u4E8B\u4EF6:", eventName);
|
|
277
474
|
return "";
|
|
278
475
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
id: generateEventId(),
|
|
282
|
-
name,
|
|
283
|
-
properties: {
|
|
284
|
-
...this.opts.baseProperties ?? {},
|
|
285
|
-
...this.opts.context?.() ?? {},
|
|
286
|
-
...properties ?? {}
|
|
287
|
-
},
|
|
288
|
-
timestamp: options?.timestamp ?? Date.now(),
|
|
289
|
-
attempts: 0,
|
|
290
|
-
sessionId: enriched.sessionId ?? this.sessionId,
|
|
291
|
-
userId: enriched.userId ?? null,
|
|
292
|
-
deviceId: enriched.deviceId ?? this.deviceId,
|
|
293
|
-
category: options?.category ?? "custom",
|
|
294
|
-
context: enriched.context ?? {}
|
|
295
|
-
};
|
|
296
|
-
if (typeof name !== "string" || name.length === 0) {
|
|
297
|
-
this.opts.onDropped?.([event], "invalid");
|
|
476
|
+
if (typeof eventName !== "string" || eventName.length === 0) {
|
|
477
|
+
this.opts.onDropped?.([this.buildEvent("", properties)], "invalid");
|
|
298
478
|
return "";
|
|
299
479
|
}
|
|
480
|
+
this.touchSession();
|
|
481
|
+
const event = this.buildEvent(eventName, properties);
|
|
300
482
|
if (options?.immediate) {
|
|
301
|
-
this.fire(this.sendBatch([event]));
|
|
302
|
-
return event.
|
|
483
|
+
this.fire(this.sendBatch([{ ...event, attempts: 0 }]));
|
|
484
|
+
return event.eventId;
|
|
303
485
|
}
|
|
304
486
|
this.enqueue(event);
|
|
305
487
|
if (this.flushInterval <= 0) {
|
|
@@ -307,7 +489,7 @@ var Reporter = class {
|
|
|
307
489
|
} else {
|
|
308
490
|
this.scheduleFlush();
|
|
309
491
|
}
|
|
310
|
-
return event.
|
|
492
|
+
return event.eventId;
|
|
311
493
|
}
|
|
312
494
|
flush() {
|
|
313
495
|
if (!this.flushPromise) {
|
|
@@ -324,6 +506,22 @@ var Reporter = class {
|
|
|
324
506
|
get size() {
|
|
325
507
|
return this.queue.length;
|
|
326
508
|
}
|
|
509
|
+
/** 匿名 id(只读):首次生成后持久化不变;供外部转化跟踪等场景读取 */
|
|
510
|
+
get anonymousId() {
|
|
511
|
+
return this._anonymousId;
|
|
512
|
+
}
|
|
513
|
+
/** 设备 / 安装实例 id(只读):Web&PC 与 google client id 一致;供外部转化跟踪等场景读取 */
|
|
514
|
+
get clientId() {
|
|
515
|
+
return this._clientId;
|
|
516
|
+
}
|
|
517
|
+
/** 当前会话 id(只读):会话轮换后返回新值;供外部转化跟踪等场景读取 */
|
|
518
|
+
get visitId() {
|
|
519
|
+
return this.session.visitId;
|
|
520
|
+
}
|
|
521
|
+
/** 当前登录账号 id(只读):实时调用 userId provider,未提供恒为 null */
|
|
522
|
+
get userId() {
|
|
523
|
+
return this.opts.userId?.() ?? null;
|
|
524
|
+
}
|
|
327
525
|
async dispose() {
|
|
328
526
|
if (this.disposed) return;
|
|
329
527
|
this.disposed = true;
|
|
@@ -341,7 +539,16 @@ var Reporter = class {
|
|
|
341
539
|
}
|
|
342
540
|
if (this.platform === "browser") {
|
|
343
541
|
window.removeEventListener("pagehide", this.onPageHide);
|
|
542
|
+
window.removeEventListener("pageshow", this.onPageShow);
|
|
543
|
+
window.removeEventListener("storage", this.onStorage);
|
|
344
544
|
document.removeEventListener("visibilitychange", this.onVisibilityChange);
|
|
545
|
+
this.restoreHistory();
|
|
546
|
+
if (this.pageTracker) {
|
|
547
|
+
this.endPageVisit(this.session.visitId, "unknown");
|
|
548
|
+
this.pageTracker.dispose();
|
|
549
|
+
this.pageTracker = null;
|
|
550
|
+
}
|
|
551
|
+
this.saveSession(true);
|
|
345
552
|
} else {
|
|
346
553
|
const exitProcess = this.getExitProcess();
|
|
347
554
|
exitProcess.off("beforeExit", this.onExit);
|
|
@@ -357,17 +564,18 @@ var Reporter = class {
|
|
|
357
564
|
}
|
|
358
565
|
if (this.flushOnExit) {
|
|
359
566
|
const pending = this.queue.splice(0);
|
|
360
|
-
|
|
361
|
-
|
|
567
|
+
const wire = stripAttempts(pending);
|
|
568
|
+
if (transport.sendSync?.(wire) ?? false) {
|
|
569
|
+
this.opts.onFlushed?.(wire);
|
|
362
570
|
await this.persistNow([]);
|
|
363
571
|
return;
|
|
364
572
|
}
|
|
365
573
|
try {
|
|
366
|
-
await transport.send(
|
|
367
|
-
this.opts.onFlushed?.(
|
|
574
|
+
await transport.send(wire);
|
|
575
|
+
this.opts.onFlushed?.(wire);
|
|
368
576
|
await this.persistNow([]);
|
|
369
577
|
} catch (error) {
|
|
370
|
-
this.opts.onFailed?.(
|
|
578
|
+
this.opts.onFailed?.(wire, error);
|
|
371
579
|
this.queue.push(...pending);
|
|
372
580
|
await this.persistNow();
|
|
373
581
|
}
|
|
@@ -377,9 +585,12 @@ var Reporter = class {
|
|
|
377
585
|
await this.persistNow();
|
|
378
586
|
}
|
|
379
587
|
}
|
|
380
|
-
/**
|
|
588
|
+
/** 异步初始化:创建内置持久化、采集 Node 设备快照、恢复上次会话队列、挂载退出兜底 */
|
|
381
589
|
async init() {
|
|
382
590
|
this.storage = await this.storagePromise;
|
|
591
|
+
if (this.platform === "node") {
|
|
592
|
+
this.nodeDevice = await collectNodeDevice();
|
|
593
|
+
}
|
|
383
594
|
if (this.disposed) return;
|
|
384
595
|
if (this.storage) {
|
|
385
596
|
try {
|
|
@@ -393,6 +604,7 @@ var Reporter = class {
|
|
|
393
604
|
}
|
|
394
605
|
if (this.disposed) return;
|
|
395
606
|
if (this.platform === "browser") {
|
|
607
|
+
window.addEventListener("storage", this.onStorage);
|
|
396
608
|
if (this.flushOnExit) {
|
|
397
609
|
window.addEventListener("pagehide", this.onPageHide);
|
|
398
610
|
}
|
|
@@ -400,7 +612,10 @@ var Reporter = class {
|
|
|
400
612
|
document.addEventListener("visibilitychange", this.onVisibilityChange);
|
|
401
613
|
}
|
|
402
614
|
if (this.opts.autoPageView) {
|
|
403
|
-
this.
|
|
615
|
+
this.pageTracker = createPageTracker(() => this.touchSession());
|
|
616
|
+
this.pageTracker.startVisit();
|
|
617
|
+
this.patchHistory();
|
|
618
|
+
window.addEventListener("pageshow", this.onPageShow);
|
|
404
619
|
}
|
|
405
620
|
} else if (this.flushOnExit) {
|
|
406
621
|
const exitProcess = this.getExitProcess();
|
|
@@ -413,9 +628,118 @@ var Reporter = class {
|
|
|
413
628
|
this.schedulePersist();
|
|
414
629
|
}
|
|
415
630
|
}
|
|
416
|
-
/**
|
|
631
|
+
/** 组装上报事件:身份字段由 SDK 填充,业务节点 = SDK 采集 + context 合并(接入方值优先) */
|
|
632
|
+
buildEvent(eventName, properties, overrides) {
|
|
633
|
+
const context = this.contextFn();
|
|
634
|
+
const device = {
|
|
635
|
+
...this.nodeDevice,
|
|
636
|
+
...collectBrowserDevice(),
|
|
637
|
+
...context.device ?? {}
|
|
638
|
+
};
|
|
639
|
+
const page = {
|
|
640
|
+
...collectPageSnapshot(),
|
|
641
|
+
...this.pageSnapshot(),
|
|
642
|
+
...context.page ?? {}
|
|
643
|
+
};
|
|
644
|
+
const mergedProperties = { ...context.properties ?? {}, ...properties ?? {} };
|
|
645
|
+
if (!context.app) {
|
|
646
|
+
this.log("context \u672A\u63D0\u4F9B app \u8282\u70B9,\u4EE5\u7A7A\u5BF9\u8C61\u4E0A\u62A5");
|
|
647
|
+
}
|
|
648
|
+
return {
|
|
649
|
+
eventId: generateEventId(),
|
|
650
|
+
eventName,
|
|
651
|
+
userId: this.opts.userId?.() ?? null,
|
|
652
|
+
anonymousId: this._anonymousId,
|
|
653
|
+
visitId: this.session.visitId,
|
|
654
|
+
clientId: this._clientId,
|
|
655
|
+
timestamp: Date.now(),
|
|
656
|
+
app: context.app ?? {},
|
|
657
|
+
...Object.keys(device).length > 0 ? { device } : {},
|
|
658
|
+
...Object.keys(page).length > 0 ? { page } : {},
|
|
659
|
+
...context.user ? { user: context.user } : {},
|
|
660
|
+
...Object.keys(mergedProperties).length > 0 ? { properties: mergedProperties } : {},
|
|
661
|
+
...overrides
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
/** 进行中页面访问的 pageViewId(非访问期返回空) */
|
|
665
|
+
pageSnapshot() {
|
|
666
|
+
if (!this.pageTracker?.active) return {};
|
|
667
|
+
const pageViewId = this.pageTracker.pageViewId();
|
|
668
|
+
return pageViewId ? { pageViewId } : {};
|
|
669
|
+
}
|
|
670
|
+
/** 会话活跃:track 调用与页面交互均触发;超时轮换 visitId 并结束旧页面访问 */
|
|
671
|
+
touchSession() {
|
|
672
|
+
const now = Date.now();
|
|
673
|
+
if (now - this.sessionReadAt >= SESSION_SAVE_THROTTLE_MS) {
|
|
674
|
+
this.sessionReadAt = now;
|
|
675
|
+
this.refreshSessionFromShared();
|
|
676
|
+
}
|
|
677
|
+
if (now - this.session.lastActivityMs > VISIT_IDLE_TIMEOUT_MS) {
|
|
678
|
+
const oldVisitId = this.session.visitId;
|
|
679
|
+
this.endPageVisit(oldVisitId, "sessionTimeout");
|
|
680
|
+
this.session = { visitId: this.newId("visit"), lastActivityMs: now };
|
|
681
|
+
this.saveSession(true);
|
|
682
|
+
this.pageTracker?.startVisit();
|
|
683
|
+
} else {
|
|
684
|
+
this.session.lastActivityMs = now;
|
|
685
|
+
this.saveSession();
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
/** 结束当前页面访问并上报 pageView 汇总事件(复用进入页面时的 visitId) */
|
|
689
|
+
endPageVisit(visitId, reason) {
|
|
690
|
+
if (!this.pageTracker?.active) return;
|
|
691
|
+
const summary = this.pageTracker.endVisit(reason);
|
|
692
|
+
if (!summary) return;
|
|
693
|
+
const context = this.contextFn();
|
|
694
|
+
this.enqueue(
|
|
695
|
+
this.buildEvent("pageView", void 0, {
|
|
696
|
+
visitId,
|
|
697
|
+
page: { ...summary, ...context.page ?? {} }
|
|
698
|
+
})
|
|
699
|
+
);
|
|
700
|
+
this.scheduleFlush();
|
|
701
|
+
}
|
|
702
|
+
/** SPA 路由变化检测:pushState / replaceState 视为 routeChange,popstate 视为 back */
|
|
703
|
+
patchHistory() {
|
|
704
|
+
if (this.historyPatch) return;
|
|
705
|
+
const endVisit = (reason) => {
|
|
706
|
+
this.endPageVisit(this.session.visitId, reason);
|
|
707
|
+
this.pageTracker?.startVisit();
|
|
708
|
+
};
|
|
709
|
+
const pushState = history.pushState.bind(history);
|
|
710
|
+
const replaceState = history.replaceState.bind(history);
|
|
711
|
+
history.pushState = function(...args) {
|
|
712
|
+
const result = pushState(...args);
|
|
713
|
+
endVisit("routeChange");
|
|
714
|
+
return result;
|
|
715
|
+
};
|
|
716
|
+
history.replaceState = function(...args) {
|
|
717
|
+
const result = replaceState(...args);
|
|
718
|
+
endVisit("routeChange");
|
|
719
|
+
return result;
|
|
720
|
+
};
|
|
721
|
+
window.addEventListener("popstate", this.onPopState);
|
|
722
|
+
this.historyPatch = { pushState, replaceState };
|
|
723
|
+
}
|
|
724
|
+
restoreHistory() {
|
|
725
|
+
if (!this.historyPatch) return;
|
|
726
|
+
history.pushState = this.historyPatch.pushState;
|
|
727
|
+
history.replaceState = this.historyPatch.replaceState;
|
|
728
|
+
window.removeEventListener("popstate", this.onPopState);
|
|
729
|
+
this.historyPatch = null;
|
|
730
|
+
}
|
|
731
|
+
/** 恢复队列入队:历史事件放队头保证先到先发,超限按溢出丢弃;过滤旧格式(无 eventId)事件 */
|
|
417
732
|
enqueueRestored(events) {
|
|
418
|
-
let accepted = events
|
|
733
|
+
let accepted = events.filter(
|
|
734
|
+
(event) => typeof event.eventId === "string" && typeof event.eventName === "string"
|
|
735
|
+
);
|
|
736
|
+
if (accepted.length < events.length) {
|
|
737
|
+
this.log(`\u8DF3\u8FC7 ${events.length - accepted.length} \u6761\u65E7\u683C\u5F0F\u6301\u4E45\u5316\u4E8B\u4EF6`);
|
|
738
|
+
}
|
|
739
|
+
accepted = accepted.map((event) => ({
|
|
740
|
+
...event,
|
|
741
|
+
attempts: typeof event.attempts === "number" ? event.attempts : 0
|
|
742
|
+
}));
|
|
419
743
|
if (accepted.length > this.maxQueueSize) {
|
|
420
744
|
this.opts.onDropped?.(accepted.slice(this.maxQueueSize), "overflow");
|
|
421
745
|
accepted = accepted.slice(0, this.maxQueueSize);
|
|
@@ -423,6 +747,7 @@ var Reporter = class {
|
|
|
423
747
|
this.queue.unshift(...accepted);
|
|
424
748
|
}
|
|
425
749
|
enqueue(event) {
|
|
750
|
+
const queued = { ...event, attempts: 0 };
|
|
426
751
|
if (this.queue.length >= this.maxQueueSize) {
|
|
427
752
|
if (this.overflowPolicy === "drop-newest") {
|
|
428
753
|
this.opts.onDropped?.([event], "overflow");
|
|
@@ -434,7 +759,7 @@ var Reporter = class {
|
|
|
434
759
|
this.fire(this.flush());
|
|
435
760
|
}
|
|
436
761
|
}
|
|
437
|
-
this.queue.push(
|
|
762
|
+
this.queue.push(queued);
|
|
438
763
|
this.schedulePersist();
|
|
439
764
|
}
|
|
440
765
|
/** 攒批定时:延迟上报的实现,窗口内事件合并为一批 */
|
|
@@ -467,12 +792,13 @@ var Reporter = class {
|
|
|
467
792
|
}
|
|
468
793
|
async sendBatch(batch) {
|
|
469
794
|
const transport = await this.transportPromise;
|
|
795
|
+
const wire = stripAttempts(batch);
|
|
470
796
|
try {
|
|
471
|
-
await transport.send(
|
|
472
|
-
this.opts.onFlushed?.(
|
|
797
|
+
await transport.send(wire);
|
|
798
|
+
this.opts.onFlushed?.(wire);
|
|
473
799
|
this.schedulePersist();
|
|
474
800
|
} catch (error) {
|
|
475
|
-
this.opts.onFailed?.(
|
|
801
|
+
this.opts.onFailed?.(wire, error);
|
|
476
802
|
this.log("send batch failed:", error);
|
|
477
803
|
const updated = batch.map((event) => ({
|
|
478
804
|
...event,
|
|
@@ -526,6 +852,99 @@ var Reporter = class {
|
|
|
526
852
|
this.log("persist queue failed:", error);
|
|
527
853
|
}
|
|
528
854
|
}
|
|
855
|
+
// ============================================================
|
|
856
|
+
// 身份与会话持久化(browser: localStorage;Node: 进程内)。
|
|
857
|
+
// 会话存 localStorage 而非 sessionStorage:同源多 tab 共享一个会话,
|
|
858
|
+
// "连续 30 分钟无活动"是用户级语义,不应按 tab 拆分;跨 tab 同步靠
|
|
859
|
+
// storage 事件(实时)+ 活动时重读(兜底)。
|
|
860
|
+
// ============================================================
|
|
861
|
+
/** 匿名 id:localStorage 持久化,首次匿名访问生成后保留;Node 无持久化介质,进程内生成 */
|
|
862
|
+
loadOrCreateAnonymousId() {
|
|
863
|
+
const stored = this.readPersistent(ANONYMOUS_ID_KEY);
|
|
864
|
+
if (stored) return stored;
|
|
865
|
+
const fresh = this.newId("anonymous");
|
|
866
|
+
this.writePersistent(ANONYMOUS_ID_KEY, fresh);
|
|
867
|
+
return fresh;
|
|
868
|
+
}
|
|
869
|
+
/** clientId:Web&PC 与 google client id(_ga cookie)一致;否则本地生成并持久化 */
|
|
870
|
+
loadOrCreateClientId() {
|
|
871
|
+
if (typeof document !== "undefined") {
|
|
872
|
+
const ga = document.cookie.match(/(?:^|;\s*)_ga=GA1\.\d\.([^;]+)/);
|
|
873
|
+
if (ga?.[1]) return ga[1];
|
|
874
|
+
const stored = this.readPersistent(CLIENT_ID_KEY);
|
|
875
|
+
if (stored) return stored;
|
|
876
|
+
}
|
|
877
|
+
const fresh = this.newId("client");
|
|
878
|
+
this.writePersistent(CLIENT_ID_KEY, fresh);
|
|
879
|
+
return fresh;
|
|
880
|
+
}
|
|
881
|
+
/** 会话恢复:localStorage(同源多 tab 共享)中 30 分钟窗口内的会话复用,否则新建 */
|
|
882
|
+
loadOrCreateSession() {
|
|
883
|
+
const now = Date.now();
|
|
884
|
+
try {
|
|
885
|
+
const text = this.readPersistent(SESSION_KEY);
|
|
886
|
+
if (text) {
|
|
887
|
+
const stored = JSON.parse(text);
|
|
888
|
+
if (stored.visitId && now - stored.lastActivityMs <= VISIT_IDLE_TIMEOUT_MS) {
|
|
889
|
+
return stored;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
} catch {
|
|
893
|
+
}
|
|
894
|
+
const fresh = { visitId: this.newId("visit"), lastActivityMs: now };
|
|
895
|
+
this.session = fresh;
|
|
896
|
+
this.saveSession(true);
|
|
897
|
+
return fresh;
|
|
898
|
+
}
|
|
899
|
+
/** 会话落盘(localStorage):节流 10s,避免滚动等高频活动频繁写;force 用于轮换 / 退出兜底 */
|
|
900
|
+
saveSession(force = false) {
|
|
901
|
+
const now = Date.now();
|
|
902
|
+
if (!force && this.sessionSaveAt > 0 && now - this.sessionSaveAt < SESSION_SAVE_THROTTLE_MS) {
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
this.sessionSaveAt = now;
|
|
906
|
+
this.writePersistent(SESSION_KEY, JSON.stringify(this.session));
|
|
907
|
+
}
|
|
908
|
+
/** 采纳共享会话:visitId 变化视为其他 tab 轮换了会话,结束本 tab 旧页面访问并开启新访问 */
|
|
909
|
+
adoptSharedSession(stored) {
|
|
910
|
+
if (!stored.visitId || stored.visitId === this.session.visitId) {
|
|
911
|
+
if (stored.visitId) this.session = stored;
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
const oldVisitId = this.session.visitId;
|
|
915
|
+
this.endPageVisit(oldVisitId, "sessionTimeout");
|
|
916
|
+
this.pageTracker?.startVisit();
|
|
917
|
+
this.session = stored;
|
|
918
|
+
}
|
|
919
|
+
/** 活动时重读共享会话(兜底 storage 事件丢失):storage 事件只在其他 tab 触发 */
|
|
920
|
+
refreshSessionFromShared() {
|
|
921
|
+
try {
|
|
922
|
+
const text = this.readPersistent(SESSION_KEY);
|
|
923
|
+
if (text) {
|
|
924
|
+
this.adoptSharedSession(JSON.parse(text));
|
|
925
|
+
}
|
|
926
|
+
} catch {
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
readPersistent(key) {
|
|
930
|
+
if (this.platform !== "browser") return null;
|
|
931
|
+
try {
|
|
932
|
+
return localStorage.getItem(key);
|
|
933
|
+
} catch {
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
writePersistent(key, value) {
|
|
938
|
+
if (this.platform !== "browser") return;
|
|
939
|
+
try {
|
|
940
|
+
localStorage.setItem(key, value);
|
|
941
|
+
} catch {
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
/** 生成带前缀的实例 id(格式对齐旧 SDK session_ / device_) */
|
|
945
|
+
newId(prefix) {
|
|
946
|
+
return `${prefix}_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
947
|
+
}
|
|
529
948
|
log(...args) {
|
|
530
949
|
if (this.opts.debug) {
|
|
531
950
|
console.log("[reporter]", ...args);
|