2. 埋点数据生命周期:全链路合规要点
埋点数据从产生到销毁,是一条完整的链条。我见过太多团队只盯着采集环节,觉得「只要用户同意了就行」。结果呢?传输没加密、存储超期、使用范围失控……任何一个环节出问题,合规就破了。
今天咱们把这五个阶段拆开揉碎,一个一个过。
2.1 数据采集:合规的起点
采集是埋点的第一道关。我个人习惯把采集合规拆成三个层面:合法性、最小化、透明度。
2.1.1 合法性基础
说白了,你得有「正当理由」才能采。GDPR 要求有合法利益或明确同意,个人信息保护法要求告知-同意。我在项目中遇到过产品经理说「先采了再说,用户又不知道」——这种想法很危险。监管查的就是你有没有告知、有没有授权。
| 场景 | 合法依据 | 典型示例 |
|---|---|---|
| 基础功能必需 | 合同履行必要 | 用户登录时间、页面加载性能 |
| 产品优化分析 | 用户同意(Opt-in) | 点击热力图、功能使用频率 |
| 个性化推荐 | 单独同意 | 浏览历史、搜索记录 |
| 第三方共享 | 单独同意 + 对外说明 | 广告归因、数据分析服务商 |
2.1.2 最小化采集
只采你真正需要的数据。我见过一个埋点事件带了 30 个字段,其中一半是「以后可能有用」。嗯,这里要注意——「可能有用」不等于「现在需要」。合规审查时,监管会问你:这个字段的业务目的是什么?答不上来就是违规。
2.1.3 透明度与告知
用户有权知道你在采什么、为什么采、采了给谁用。隐私政策里不能只写「我们可能会收集您的使用数据」,要具体到事件类型、字段列表、采集频率。
// 示例:埋点采集前的同意检查
function trackEvent(eventName, properties) {
// 1. 检查用户是否已同意该事件类型
if (!consentManager.isConsented(eventName)) {
console.warn(`[合规] 事件 ${eventName} 未获得用户同意,已丢弃`);
return;
}
// 2. 检查字段是否在允许列表中
const allowedFields = getEventSchema(eventName).fields;
const filteredProps = {};
for (const key of Object.keys(properties)) {
if (allowedFields.includes(key)) {
filteredProps[key] = properties[key];
} else {
console.warn(`[合规] 字段 ${key} 不在事件 ${eventName} 的 schema 中,已过滤`);
}
}
// 3. 发送数据
sendToServer(eventName, filteredProps);
}
2.2 数据传输:加密是底线
数据从客户端到服务端,中间经过的每一跳都是风险点。我记得有一次做安全审计,发现某个内部工具的数据传输用的是 HTTP 明文——虽然是在内网,但合规要求一视同仁。
2.2.1 传输加密
- 全链路 HTTPS:客户端到服务端、服务端到服务端,全部强制 TLS 1.2+。不要只在登录页用 HTTPS,埋点接口同样需要。
- 证书校验:客户端要校验服务端证书,防止中间人攻击。我见过一些 SDK 为了「省事」关闭了证书校验——这是给自己埋雷。
- 数据完整性:使用 HMAC 或数字签名,确保数据在传输过程中没有被篡改。
2.2.2 传输协议选择
| 协议 | 适用场景 | 合规要点 |
|---|---|---|
| HTTP/HTTPS POST | 实时事件上报 | 必须 HTTPS,请求体加密 |
| WebSocket | 实时流式数据 | WSS 协议,消息级别加密 |
| gRPC | 服务间数据传输 | mTLS 双向认证 |
| 批量文件传输 | 离线数据同步 | 文件加密 + 传输通道加密 |
2.3 数据存储:分级与保护
数据存下来之后,怎么存、存多久、谁能访问,这些都是合规问题。我建议按数据敏感度分级管理。
2.3.1 数据分级存储
- L1 - 公开数据:如页面浏览数、设备型号(非唯一标识)。可以明文存储,但需要访问控制。
- L2 - 内部数据:如用户行为序列、功能使用频率。需要加密存储,脱敏后使用。
- L3 - 敏感数据:如用户 ID、设备 ID、精确位置。必须加密存储,严格权限控制,定期审计。
2.3.2 存储加密策略
-- 示例:MySQL 字段级加密存储敏感数据
CREATE TABLE user_behavior (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
-- 用户ID使用 AES-256 加密存储
user_id_encrypted VARBINARY(256) NOT NULL,
event_name VARCHAR(128) NOT NULL,
event_time DATETIME NOT NULL,
-- 非敏感字段明文存储
page_url VARCHAR(512),
-- 敏感字段加密
ip_address_encrypted VARBINARY(128),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 创建加密函数
CREATE FUNCTION encrypt_field(plaintext VARCHAR(255), key_str VARCHAR(64))
RETURNS VARBINARY(256) DETERMINISTIC
BEGIN
RETURN AES_ENCRYPT(plaintext, key_str);
END;
2.3.3 存储期限管理
数据不能无限期保留。每个数据类别都要有明确的保留期限,到期自动删除或匿名化。我习惯在存储层加一个 retention_days 字段,配合定时任务做清理。
2.4 数据使用:权限与审计
数据存好了,谁来用、怎么用、用了多少,这些都要管。你想想看,如果每个工程师都能直接查原始埋点数据,那合规就形同虚设了。
2.4.1 访问控制
- 最小权限原则:只给需要的人、需要的数据。分析师只能看脱敏后的聚合数据,工程师只能看自己负责模块的数据。
- 角色分级:数据管理员、数据分析师、数据工程师,每个角色有独立的权限矩阵。
- 动态授权:临时访问需要审批,到期自动回收。
2.4.2 使用审计
谁在什么时间、从哪个 IP、查询了哪些数据、返回了多少条记录——这些都要记录下来。我曾经在审计日志里发现一个异常:某个离职员工的账号在凌晨 3 点批量导出了用户行为数据。幸好有审计日志,我们及时冻结了账号。
// 示例:数据查询审计日志结构
{
"audit_id": "audit_20250321_001",
"user_id": "analyst_zhang",
"user_role": "data_analyst",
"action": "query",
"resource": "user_behavior_table",
"query_conditions": {
"date_range": ["2025-03-01", "2025-03-20"],
"event_type": "page_view",
"limit": 10000
},
"result_count": 8932,
"ip_address": "10.12.34.56",
"timestamp": "2025-03-21T14:30:00Z",
"approval_id": "approval_20250321_002"
}
2.5 数据删除:不是简单的 DELETE
数据删除是生命周期的终点,但很多人把它想简单了。你以为执行了 DELETE FROM table WHERE user_id = ? 就完事了?没那么简单。
2.5.1 删除的三种模式
| 模式 | 说明 | 适用场景 |
|---|---|---|
| 物理删除 | 从存储介质中彻底擦除,不可恢复 | 用户要求删除、数据超期 |
| 逻辑删除 | 标记为已删除,但数据仍在 | 需要保留审计痕迹、数据恢复能力 |
| 匿名化 | 去除所有可识别信息,保留统计价值 | 长期趋势分析、模型训练 |
2.5.2 删除的难点
数据可能分散在多个地方:主数据库、缓存、搜索引擎、数据仓库、备份文件……每个地方都要删干净。我遇到过最头疼的情况:用户要求删除数据,但备份文件里的数据要等 30 天备份过期才能自动清除。怎么办?
2.5.3 删除验证
删除操作完成后,要验证数据是否真的不可访问。我建议写自动化测试脚本,定期检查已删除用户的数据是否还能被查询到。
// 示例:删除验证脚本(伪代码)
function verifyDeletion(userId) {
const dataSources = ['mysql', 'redis', 'elasticsearch', 'clickhouse'];
for (const source of dataSources) {
const result = queryDataSource(source, `SELECT * FROM events WHERE user_id = '${userId}'`);
if (result.length > 0) {
throw new Error(`[删除失败] ${source} 中仍存在用户 ${userId} 的数据`);
}
console.log(`[删除成功] ${source} 中用户 ${userId} 的数据已清除`);
}
}