From 2aa393dd657d93bc6acd1015e4b909c8cb9f7eaa Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 22 May 2026 08:17:58 +0800 Subject: [PATCH] =?UTF-8?q?fix(miniprogram):=20Analytics=20PII=20=E6=B8=85?= =?UTF-8?q?=E7=90=86=20=E2=80=94=20=E7=A7=BB=E9=99=A4=20userId/patientId?= =?UTF-8?q?=20=E5=AD=97=E6=AE=B5=20+=20sanitizeProperties=20(S2-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 AnalyticsEvent 接口中的 userId/patientId 字段 - 新增 sanitizeProperties 运行时过滤 14 种 PII 标识字段 - trackEvent 自动清理 properties 中的 PII - 3 个单元测试覆盖 PII 过滤场景 --- .../__tests__/services/analytics-pii.test.ts | 99 +++++++++++++++++++ apps/miniprogram/src/services/analytics.ts | 24 ++++- 2 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 apps/miniprogram/__tests__/services/analytics-pii.test.ts diff --git a/apps/miniprogram/__tests__/services/analytics-pii.test.ts b/apps/miniprogram/__tests__/services/analytics-pii.test.ts new file mode 100644 index 0000000..9d5c1cd --- /dev/null +++ b/apps/miniprogram/__tests__/services/analytics-pii.test.ts @@ -0,0 +1,99 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +vi.mock('@tarojs/taro', () => ({ + default: { + getStorageSync: vi.fn(() => []), + setStorage: vi.fn(), + removeStorageSync: vi.fn(), + }, +})); + +vi.mock('@/services/request', () => ({ + api: { post: vi.fn().mockResolvedValue({ success: true }) }, +})); + +describe('Analytics PII 清理', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('flushEvents 发送的 batch 不含 PII', async () => { + const { trackEvent, flushEvents } = await import('@/services/analytics'); + const { api } = await import('@/services/request'); + + trackEvent('page_view', { + page: 'health', + userId: 'should-be-removed', + patientId: 'should-be-removed', + user_name: 'should-be-removed', + phone: 'should-be-removed', + id_card: 'should-be-removed', + }); + + await flushEvents(); + + const postCall = vi.mocked(api.post).mock.calls[0]; + const body = postCall[1] as { events: Array> }; + const evt = body.events[0]; + + // 事件级别不应有 userId/patientId + expect(evt).not.toHaveProperty('userId'); + expect(evt).not.toHaveProperty('patientId'); + + // properties 中不应有 PII 字段 + const props = evt.properties as Record; + expect(props).not.toHaveProperty('userId'); + expect(props).not.toHaveProperty('patientId'); + expect(props).not.toHaveProperty('user_name'); + expect(props).not.toHaveProperty('phone'); + expect(props).not.toHaveProperty('id_card'); + + // 正常字段保留 + expect(props.page).toBe('health'); + }); + + it('trackEvent 不在事件级别包含 userId/patientId', async () => { + const { trackEvent, flushEvents } = await import('@/services/analytics'); + const { api } = await import('@/services/request'); + + trackEvent('test_event'); + await flushEvents(); + + const postCall = vi.mocked(api.post).mock.calls[0]; + const body = postCall[1] as { events: Array> }; + const evt = body.events[0]; + + expect(evt).not.toHaveProperty('userId'); + expect(evt).not.toHaveProperty('patientId'); + }); + + it('sanitizeProperties 过滤所有 PII 标识字段', async () => { + const { trackEvent, flushEvents } = await import('@/services/analytics'); + const { api } = await import('@/services/request'); + + trackEvent('test', { + openid: 'oXXX', + access_token: 'tok', + refresh_token: 'ref', + email: 'test@test.com', + address: '某地', + mobile: '13800001111', + page: 'settings', // 非 PII 字段 + }); + + await flushEvents(); + + const postCall = vi.mocked(api.post).mock.calls[0]; + const body = postCall[1] as { events: Array> }; + const props = body.events[0].properties as Record; + + // 全部 PII 被过滤,只剩 page + expect(props).not.toHaveProperty('openid'); + expect(props).not.toHaveProperty('access_token'); + expect(props).not.toHaveProperty('refresh_token'); + expect(props).not.toHaveProperty('email'); + expect(props).not.toHaveProperty('address'); + expect(props).not.toHaveProperty('mobile'); + expect(props.page).toBe('settings'); + }); +}); diff --git a/apps/miniprogram/src/services/analytics.ts b/apps/miniprogram/src/services/analytics.ts index 8d60f2f..f35b1c8 100644 --- a/apps/miniprogram/src/services/analytics.ts +++ b/apps/miniprogram/src/services/analytics.ts @@ -17,12 +17,30 @@ type EventName = | 'family_add' | 'profile_edit'; +const PII_KEYS = new Set([ + 'userId', 'user_id', 'patientId', 'patient_id', + 'user_name', 'username', 'phone', 'mobile', + 'id_card', 'id_number', 'email', 'address', + 'openid', 'access_token', 'refresh_token', +]); + +function sanitizeProperties( + properties?: Record, +): Record | undefined { + if (!properties) return undefined; + const cleaned: Record = {}; + for (const [key, value] of Object.entries(properties)) { + if (!PII_KEYS.has(key)) { + cleaned[key] = value; + } + } + return Object.keys(cleaned).length > 0 ? cleaned : undefined; +} + interface AnalyticsEvent { event: EventName | string; properties?: Record; timestamp: number; - userId?: string; - patientId?: string; } const STORAGE_KEY = 'analytics_queue'; @@ -51,7 +69,7 @@ export function trackEvent(event: EventName | string, properties?: Record