diff --git a/sdk/build.mjs b/sdk/build.mjs index 5ed9fe4..3583922 100644 --- a/sdk/build.mjs +++ b/sdk/build.mjs @@ -2,5 +2,5 @@ import { build } from "esbuild"; await build({ entryPoints: ["src/index.ts"], bundle: true, minify: true, format: "iife", - outfile: "dist/v1.js", target: ["es2018"], + outfile: "dist/v1.js", target: ["es2020"], // 代码依赖 globalThis(es2018 目标名不副实) }); diff --git a/sdk/src/events.ts b/sdk/src/events.ts index 06936af..f681472 100644 --- a/sdk/src/events.ts +++ b/sdk/src/events.ts @@ -10,7 +10,11 @@ export function startPageTracking(t: Transport) { let activeMs = 0; let lastAct = Date.now(); const act = () => { - lastAct = Date.now(); + try { + lastAct = Date.now(); + } catch { + /* 静默(spec §4.5 红线:监听器异常不得冒泡到宿主页) */ + } }; ["pointerdown", "keydown", "wheel", "touchstart"].forEach((e) => document.addEventListener(e, act, { passive: true, capture: true }) @@ -59,7 +63,11 @@ export function startPageTracking(t: Transport) { } }; document.addEventListener("visibilitychange", () => { - if (document.hidden) close(); + try { + if (document.hidden) close(); + } catch { + /* 静默:document.hidden 读取本身也不得冒泡异常(close 内部已自包) */ + } }); window.addEventListener("pagehide", close); } diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 84efd7a..e142d60 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -49,6 +49,19 @@ function dispatch(cmd: string, arg?: unknown) { } } -// 必须先替换全局入口再重放队列:重放期间宿主新调用直接进 dispatch,不再排队 +// 必须先替换全局入口再重放队列:重放期间宿主新调用直接进 dispatch,不再排队。 +// 宿主队列数据不可信(非可迭代项等)——模块顶层也不得抛(spec §4.5 唯一未保护路径); +// 单个坏条目只跳过自身,不吞掉后续排队的 init/track g.ctk = dispatch; -q.forEach(([c, a]) => dispatch(c, a)); +try { + q.forEach((entry) => { + try { + const [c, a] = entry as Cmd; + dispatch(c, a); + } catch { + /* 静默:坏条目跳过,继续重放 */ + } + }); +} catch { + /* 静默:q 本身不可迭代/无 forEach——放弃重放,入口已接管 */ +} diff --git a/sdk/src/transport.ts b/sdk/src/transport.ts index 113bc66..710c0cb 100644 --- a/sdk/src/transport.ts +++ b/sdk/src/transport.ts @@ -49,10 +49,12 @@ export class Transport { events, }); if (beacon && navigator.sendBeacon) { - navigator.sendBeacon( + // 返回 false=浏览器拒收(队列满/载荷超 64KB 等)→ 整批丢,须计 dropped + const ok = navigator.sendBeacon( this.cfg.endpoint, new Blob([body], { type: "text/plain" }) ); + if (!ok) this.dropped += n; return; } this.send(body, events.length); diff --git a/sdk/test/events.test.ts b/sdk/test/events.test.ts index d441b26..623e615 100644 --- a/sdk/test/events.test.ts +++ b/sdk/test/events.test.ts @@ -118,6 +118,21 @@ describe("events / startPageTracking", () => { expect(fetchMock).not.toHaveBeenCalled(); }); + it("30s 无活动不累计:单次活动后空闲 35s → active_ms 恰为 29000 后停止", async () => { + window.history.pushState({}, "", "/pidle"); + const t = mkTransport("site-idle"); + startPageTracking(t); + document.dispatchEvent(new Event("pointerdown")); // 仅此一次活动 + await vi.advanceTimersByTimeAsync(35_000); + + window.dispatchEvent(new Event("pagehide")); + const bodies = await beaconBodies("site-idle"); + expect(bodies).toHaveLength(1); + const stay = bodies[0].events.find((e) => e.event_name === "page_stay")!; + // tick 于 +1s..+29s 时 Date.now()-lastAct<30000 成立(+30s 起等于 30000 不成立) + expect(stay.properties.active_ms).toBe(29000); + }); + it("隐藏→恢复→再隐藏:两段 stay 各自正确(第二段从 0 重新累计)", async () => { window.history.pushState({}, "", "/p6"); const t = mkTransport("site-t6"); diff --git a/sdk/test/transport.test.ts b/sdk/test/transport.test.ts index 689574c..5356ff7 100644 --- a/sdk/test/transport.test.ts +++ b/sdk/test/transport.test.ts @@ -177,6 +177,16 @@ describe("transport", () => { expect(t.dropped).toBe(1); // maxRetry=0 耗尽 → 按事件数丢弃 }); + it("sendBeacon 返回 false(队列满/载荷超限)按本批事件数计 dropped", () => { + vi.stubGlobal("navigator", { sendBeacon: vi.fn().mockReturnValue(false) }); + const t = new Transport(mkCfg({ batchSize: 10 })); + t.push({ event_id: "e1" }); + t.push({ event_id: "e2" }); + t.flush(true); + expect(t.dropped).toBe(2); // 信标被浏览器拒收(64KB 上限等)→ 整批丢 + expect(fetchMock).not.toHaveBeenCalled(); + }); + it("stringify 抛错(track 引入宿主可控对象):已换出批次按事件数计 dropped", () => { const t = new Transport(mkCfg({ batchSize: 10, flushMs: 60000 })); const bad: { event_id: string; self?: unknown } = { event_id: "bad" };