docs: 阶段A计划吸收审查修正——__BUILD_ID__测试期define与ambient声明、slug安全线32并补33-64向量、保留字/resolveJsonModule/null防御
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,7 @@
|
||||
{"in": "/lp?gclid=EAIaIQ&fbclid=IwAR2x", "out": "/lp?gclid=EAIaIQ&fbclid=IwAR2x"},
|
||||
{"in": "/p?token=abcdef0123456789abcdef0123456789x", "out": "/p"},
|
||||
{"in": "/p?ref=abcdefghijklmnopqrstuvwxyz012345", "out": "/p"},
|
||||
{"in": "/p?slug=abcdefghijklmnopqrstuvwxyz0123456", "out": "/p"},
|
||||
{"in": "/p?cat=sedans&sort=price", "out": "/p?cat=sedans&sort=price"},
|
||||
{"in": "/p?q=%E5%BC%A0%E4%B8%89%E7%9A%84%E9%82%AE%E7%AE%B1a%40b.com", "out": "/p"},
|
||||
{"in": "/a/b?name=zhangsan", "out": "/a/b"},
|
||||
@@ -51,7 +52,7 @@
|
||||
}
|
||||
```
|
||||
|
||||
(执行者注意:vectors 覆盖四类——业务参数保留、黑名单参数删、值判别删(@/长随机/超长)、userinfo+fragment 清理。`x=1` 值"1"是短数字→保留。)
|
||||
(执行者注意:vectors 覆盖四类——业务参数保留、黑名单参数删、值判别删(@/长随机/超长)、userinfo+fragment 清理。`x=1` 值"1"是短数字→保留。**slug 安全线定 32**:`ref=abcdefghijklmnopqrstuvwxyz012345`(32 连续字母数字)按随机 token 删——真实业务 slug 含 `._-` 分隔或更短,32+ 连续字母数字视为 token(D08 威胁模型),33-64 band 由新增的 `slug=` 向量钉死语义。)
|
||||
|
||||
- [ ] **Step 2: 写失败测试(向量驱动)**
|
||||
|
||||
@@ -59,6 +60,7 @@
|
||||
|
||||
```python
|
||||
import json, pathlib
|
||||
from tracker import constants as C
|
||||
VECTORS = json.loads((pathlib.Path(__file__).parent / "fixtures" / "sanitize_vectors.json").read_text(encoding="utf-8"))
|
||||
|
||||
def test_sanitize_url_vectors():
|
||||
@@ -84,7 +86,7 @@ URL_PARAM_DROP_PATTERNS = ("email", "phone", "mobile", "tel", "name", "user",
|
||||
"token", "key", "password", "verif", "passwd", "idcard")
|
||||
ATTRIBUTION_PARAM_WHITELIST = {"gclid", "gbraid", "wbraid", "fbclid", "msclkid",
|
||||
"yclid", "gad_source", "gad_campaignid", "dclid"}
|
||||
_SAFE_VALUE = re.compile(r"^(?:\d{1,10}|[A-Za-z0-9][A-Za-z0-9._-]{0,63})$")
|
||||
_SAFE_VALUE = re.compile(r"^(?:\d{1,10}|[A-Za-z0-9][A-Za-z0-9._-]{0,30})$") # slug≤32:32+连续字母数字视为token删
|
||||
```
|
||||
|
||||
(constants.py 顶部需 `import re`。)
|
||||
@@ -322,8 +324,8 @@ git commit -m "test(server): v1/v2混合停留聚合回归+聚合点核查适配
|
||||
**Files:**
|
||||
- Modify: `sdk/package.json`(version 0.2.0)
|
||||
- Modify: `sdk/build.mjs`(define 注入)
|
||||
- Modify: `sdk/src/config.ts`(类型)
|
||||
- Create: `sdk/src/types.ts`(归属快照类型)
|
||||
- Modify: `sdk/vitest.config.ts`(**define 注入测试值**——否则 `__BUILD_ID__` 在 dispatch 的静默 catch 里 ReferenceError,init 半途而废且零报错)
|
||||
- Create: `sdk/src/types.ts`(归属快照类型 + `__BUILD_ID__` ambient 声明)
|
||||
- Test: `sdk/test/index.test.ts` 适配
|
||||
|
||||
- [ ] **Step 1: types.ts(新建)**
|
||||
@@ -343,7 +345,15 @@ export interface Attribution {
|
||||
export const SDK_VERSION = "0.2.0"; // 与 package.json 同步手维护
|
||||
export const SCHEMA_VERSION = 2;
|
||||
export const CONFIG_VERSION = "1";
|
||||
export declare const __BUILD_ID__: string; // esbuild define 注入
|
||||
|
||||
/** build_id 由 esbuild/vitest define 注入的全局常量(ambient 声明,
|
||||
不能用模块级 export declare——esbuild 打包时会被擦除导致引用悬空) */
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var __BUILD_ID__: string | undefined;
|
||||
}
|
||||
/** 运行时兜底读取(build 注入缺失时降级 "unknown",绝不抛) */
|
||||
export const BUILD_ID: string = globalThis.__BUILD_ID__ ?? "unknown";
|
||||
```
|
||||
|
||||
- [ ] **Step 2: build.mjs 注入**
|
||||
@@ -358,6 +368,15 @@ await build({ entryPoints: ["src/index.ts"], bundle: true, minify: true, format:
|
||||
|
||||
(fallback:git 失败时用 `"dev-" + Date.now()`,try/catch 包裹——构建环境无 git 不得挂。)
|
||||
|
||||
vitest.config.ts 同步注入(否则 index.ts 引用 BUILD_ID 在测试环境 undefined——有 ?? 兜底不炸,但注入后可断言真实值):
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
define: { __BUILD_ID__: JSON.stringify("vitest") },
|
||||
test: { environment: "happy-dom" },
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 失败测试 → 实现 sdk_loaded 携带版本组 → 全量跑过 → Commit**
|
||||
|
||||
```bash
|
||||
@@ -478,18 +497,19 @@ test("v1 兼容形态不再产生(新 SDK 恒 schema_version:2 信封 + 事件
|
||||
|
||||
**Files:**
|
||||
- Modify: `sdk/src/context.ts:1-58`(URL_PARAM_DROP 表 + sanitizeUrl 重写)
|
||||
- Modify: `sdk/tsconfig.json`(`resolveJsonModule: true`——跨树 JSON import 的 typecheck 支持)
|
||||
- Test: `sdk/test/context.test.ts`(追加向量测试)
|
||||
|
||||
- [ ] **Step 1: 失败测试(读 server 的同一份 fixtures)**
|
||||
|
||||
```ts
|
||||
import vectors from "../../server/tracker/tests/fixtures/sanitize_vectors.json";
|
||||
test.each(vectors.vectors)("sanitizeUrl($in) → $out", ({in, out}) => {
|
||||
expect(sanitizeUrl(in)).toBe(out);
|
||||
test.each(vectors.vectors)("sanitizeUrl($input) → $expected", (v) => {
|
||||
expect(sanitizeUrl(v.in)).toBe(v.out);
|
||||
});
|
||||
```
|
||||
|
||||
(vitest JSON import 天然支持;确认 vitest.config.ts 的 include 覆盖 test/,相对路径跨出 sdk/ 无碍。)
|
||||
(`in` 是保留字不能作解构绑定名——用整对象参数 `v`。vitest/vite 原生支持 JSON import 且无 root 限制(已核 vitest.config.ts),跨出 sdk/ 的相对路径可解析。)
|
||||
|
||||
- [ ] **Step 2: TS 实现与服务端规则逐条对齐**(黑名单 fullmatch、归因白名单+utm_ 前缀、`_SAFE_VALUE` 同正则、userinfo/fragment 清理——TS 用 `new URL()` 处理 userinfo,注意相对路径 input 时 URL 不可用 → 沿用字符串切分法与 Python urlparse 行为对齐)
|
||||
|
||||
@@ -528,8 +548,9 @@ test.each(vectors.vectors)("sanitizeUrl($in) → $out", ({in, out}) => {
|
||||
|
||||
## 风险提示(执行者必读)
|
||||
|
||||
1. **勿动归属之外的既有不变量**:ingest.py 头注释的五条并发不变量(uq_site_event/F 表达式/Greatest ended_at/族缓存)必须保留——v2 改造叠加在其上,不得回归。
|
||||
2. **stay_ms 重算是唯一聚合改动**:duration_ms/event_count/ended_at 维持现有 F 增量;只有 stay_ms 从增量改重算。v1+v2 混合解释:v1 SUM + v2 并集相加。
|
||||
3. **SDK 红线**:任何回调异常静默(spec §4.5);PageSession/SPA hook 全部 try/catch 包裹。
|
||||
1. **勿动归属之外的既有不变量**:ingest.py 头注释五条并发不变量中 ①②④⑤ 全保留,③ 的 F 表达式模式保留——唯 stay_ms 字段按风险项 2 改重算(其余字段 duration_ms/event_count/ended_at 仍 F 增量)。
|
||||
2. **stay_ms 重算是唯一聚合改动**:duration_ms/event_count/ended_at 维持现有 F 增量;只有 stay_ms 从增量改重算。v1+v2 混合解释:v1 SUM + v2 并集相加。recompute 时对 `page_id` 非空但 `active_from_ms/active_to_ms` 为 null 的脏数据 filter 防御(validate_event v2 分支强制区间完整,双保险)。
|
||||
3. **SDK 红线**:任何回调异常静默(spec §4.5);PageSession/SPA hook 全部 try/catch 包裹。BUILD_ID 经 `globalThis.__BUILD_ID__ ?? "unknown"` 兜底——define 缺失(vitest/异常构建环境)不得炸。
|
||||
4. **测试向量是契约**:T1 的 fixtures 文件 T9 复用,两侧任何一侧改规则必须同步改向量并两侧重跑。
|
||||
5. 生产部署顺序:server 先(T11)验证 v1 无回归后才发 SDK(T12);SDK 回滚只需还原 sdk/dist,服务端不动。
|
||||
6. **测试 helper 均为新建**:`_base_event_kwargs`/`_now_iso`/`SITE_ID` 等在 server tests 中不存在——执行者创建它们而非寻找。Task 1 Step 6 点名更新既有测试 `test_pageview_referrer_truncated_after_reencode`(CJK `q` 值不再匹配 _SAFE_VALUE,改用超长 utm 值驱动同一路径)。
|
||||
|
||||
Reference in New Issue
Block a user