feat(sdk): 0.2.0接线——归属快照/区间停留/清洗对齐/响应契约全链路
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+37
-8
@@ -2,7 +2,9 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
|
||||
// index.ts 是 IIFE 打包入口:模块加载即读取全局队列、替换 g.ctk 并重放。
|
||||
// 隔离手段:vi.resetModules() + 动态 import,每用例拿到全新模块级 transport 状态。
|
||||
// startPageTracking 注册的 document/window 监听跨用例存留,用独立 siteId 过滤批次。
|
||||
// startPageSession/startStayTracking 注册的 document/window 监听跨用例存留,
|
||||
// 用独立 siteId 过滤批次。0.2.0 接线(T10):初始 page_view 由 startPageSession
|
||||
// 发(seq=1),sdk_loaded 随后入队(seq=2)——队列顺序 [page_view, sdk_loaded]。
|
||||
|
||||
type Cmd = [string, unknown?];
|
||||
type CtkFn = ((cmd: string, arg?: unknown) => void) & { q?: Cmd[] };
|
||||
@@ -70,6 +72,29 @@ describe("index 入口", () => {
|
||||
expect(loaded.event_id).toMatch(UUID_RE);
|
||||
});
|
||||
|
||||
it("接线冒烟(T10):init 后 page_view+sdk_loaded 两事件均带顶层归属快照(四元组+seq+V1.1 预留位)", async () => {
|
||||
await loadSdk([["init", { siteId: "site-wire", endpoint: "/e", flushMs: 60000 }]]);
|
||||
g.ctk!("flush");
|
||||
|
||||
const bodies = bodiesOf("site-wire");
|
||||
expect(bodies).toHaveLength(1); // 两事件同归属四元组 → 单组子批
|
||||
const evs = bodies[0].events;
|
||||
expect(evs.map((e: { event_name: string }) => e.event_name)).toEqual(["page_view", "sdk_loaded"]);
|
||||
for (const e of evs as Record<string, unknown>[]) {
|
||||
expect(e.site_id).toBe("site-wire");
|
||||
expect(e.visitor_id).toMatch(UUID_RE);
|
||||
expect(e.session_id).toMatch(UUID_RE);
|
||||
expect(e.page_id).toMatch(UUID_RE);
|
||||
expect(typeof e.seq).toBe("number");
|
||||
expect(e.purpose).toBe(null); // V1.1 预留位从快照展开(非字面量)
|
||||
expect(e.epoch).toBe(null);
|
||||
expect(e.grant_ref).toBe(null);
|
||||
}
|
||||
expect(evs[0].seq).toBe(1); // 初始 page_view:段内首快照(startPageSession 内发)
|
||||
expect(evs[1].seq).toBe(2); // sdk_loaded:归属取自同一 PageSession
|
||||
expect(evs[0].page_id).toBe(evs[1].page_id); // 同段归属
|
||||
});
|
||||
|
||||
it("批次 context 含设备环境 + referrer_domain/utm/path(首触归因链路)", async () => {
|
||||
Object.defineProperty(document, "referrer", {
|
||||
value: "https://news.example/a",
|
||||
@@ -139,9 +164,11 @@ describe("index 入口", () => {
|
||||
expect(bodies).toHaveLength(1);
|
||||
expect(bodies[0].visitor_id).toMatch(UUID_RE); // 内存降级身份仍可用
|
||||
expect(bodies[0].session_id).toMatch(UUID_RE);
|
||||
// page_view 由 startPageSession 先发(seq=1),sdk_loaded 随后(seq=2);
|
||||
// 15s 的 stay 周期切分在同一 fake 时刻触发,但 flush 定时器注册更早(先发)
|
||||
expect(bodies[0].events.map((e: { event_name: string }) => e.event_name)).toEqual([
|
||||
"sdk_loaded",
|
||||
"page_view",
|
||||
"sdk_loaded",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -197,19 +224,21 @@ describe("index 入口", () => {
|
||||
expect(g.ctk!.q).toBeUndefined(); // 桩(带 q)已被 dispatch 替换
|
||||
g.ctk!("flush");
|
||||
let bodies = bodiesOf("site-8");
|
||||
expect(bodies).toHaveLength(1);
|
||||
// T8 分组拆批:归属组(page_view+sdk_loaded 同四元组)+ 兜底组(track 宿主
|
||||
// 对象无归属顶层字段,信封走 identity 旧路径)——一次 flush 两个子批
|
||||
expect(bodies).toHaveLength(2);
|
||||
expect(bodies[0].events.map((e: { event_name: string }) => e.event_name)).toEqual([
|
||||
"sdk_loaded",
|
||||
"page_view",
|
||||
"cta_click",
|
||||
"sdk_loaded",
|
||||
]);
|
||||
expect(bodies[0].events[2].properties).toEqual({ k: 1 });
|
||||
expect(bodies[1].events.map((e: { event_name: string }) => e.event_name)).toEqual(["cta_click"]);
|
||||
expect(bodies[1].events[0].properties).toEqual({ k: 1 });
|
||||
|
||||
// 重放结束后宿主新调用直接进 dispatch(不再排队丢失)
|
||||
g.ctk!("track", { ...trackEv, event_id: "e-2", event_name: "cta_click2" });
|
||||
g.ctk!("flush");
|
||||
bodies = bodiesOf("site-8");
|
||||
expect(bodies).toHaveLength(2);
|
||||
expect(bodies[1].events.map((e: { event_name: string }) => e.event_name)).toEqual(["cta_click2"]);
|
||||
expect(bodies).toHaveLength(3);
|
||||
expect(bodies[2].events.map((e: { event_name: string }) => e.event_name)).toEqual(["cta_click2"]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user