Skip to content

Commit 73779c8

Browse files
committed
fix: address critical issues from phase 2 review
- Fix memory leak in query-cache.ts: clean up generations Map when entries are deleted - Fix potential data leak in analytics/log-helpers.ts: use explicit allowlist instead of spread operator - Add tests for PII filtering in analytics
1 parent 7811d6a commit 73779c8

File tree

4 files changed

+126
-5
lines changed

4 files changed

+126
-5
lines changed

cli/src/utils/query-cache.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@ export function deleteCacheEntryCore(key: string): void {
171171
cache.refCounts.delete(key)
172172
snapshotMemo.delete(key)
173173
notifyKeyListeners(key)
174+
// Clean up generation counter after deletion is complete.
175+
// The bump above invalidates any in-flight requests; now we can free the memory.
176+
generations.delete(key)
174177
}
175178

176179
export function resetCache(): void {

common/src/analytics/log-helpers.ts

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,92 @@ export function getAnalyticsEventId(data: unknown): AnalyticsEvent | null {
3838
: null
3939
}
4040

41+
// Allowlist of properties safe to send to analytics.
42+
// Be conservative - only include properties that are clearly non-PII.
43+
const SAFE_ANALYTICS_PROPERTIES = new Set([
44+
// Event metadata
45+
'eventId',
46+
'level',
47+
'msg',
48+
// Timing/metrics
49+
'duration',
50+
'durationMs',
51+
'latency',
52+
'latencyMs',
53+
'timestamp',
54+
// Counts/sizes
55+
'count',
56+
'size',
57+
'length',
58+
'total',
59+
// Status/type identifiers
60+
'status',
61+
'type',
62+
'action',
63+
'source',
64+
'target',
65+
'category',
66+
// Agent/model info
67+
'agentId',
68+
'agentType',
69+
'modelId',
70+
'modelName',
71+
// Feature flags/versions
72+
'version',
73+
'feature',
74+
'variant',
75+
// Error info (without stack traces or sensitive details)
76+
'errorCode',
77+
'errorType',
78+
// Boolean flags
79+
'success',
80+
'enabled',
81+
'cached',
82+
// Run/step identifiers
83+
'runId',
84+
'stepNumber',
85+
'stepId',
86+
])
87+
88+
// Properties that should never be sent to analytics (PII/sensitive)
89+
const BLOCKED_ANALYTICS_PROPERTIES = new Set([
90+
'userId',
91+
'user_id',
92+
'user',
93+
'email',
94+
'name',
95+
'password',
96+
'token',
97+
'apiKey',
98+
'secret',
99+
'authorization',
100+
'cookie',
101+
'session',
102+
'ip',
103+
'ipAddress',
104+
'fingerprint',
105+
'deviceId',
106+
])
107+
108+
function extractSafeProperties(
109+
record: AnalyticsLogData,
110+
): Record<string, unknown> {
111+
const safeProps: Record<string, unknown> = {}
112+
113+
for (const [key, value] of Object.entries(record)) {
114+
// Skip blocked properties
115+
if (BLOCKED_ANALYTICS_PROPERTIES.has(key)) continue
116+
// Skip complex objects that might contain PII
117+
if (value !== null && typeof value === 'object') continue
118+
// Only include properties in the allowlist
119+
if (SAFE_ANALYTICS_PROPERTIES.has(key)) {
120+
safeProps[key] = value
121+
}
122+
}
123+
124+
return safeProps
125+
}
126+
41127
export function toTrackableAnalyticsPayload({
42128
data,
43129
level,
@@ -69,7 +155,7 @@ export function toTrackableAnalyticsPayload({
69155
event: eventId,
70156
userId,
71157
properties: {
72-
...record,
158+
...extractSafeProperties(record),
73159
level,
74160
msg,
75161
},

common/src/util/__tests__/analytics-dispatcher.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@ describe('analytics dispatcher', () => {
3737
event: AnalyticsEvent.APP_LAUNCHED,
3838
userId: 'u',
3939
properties: expect.objectContaining({
40-
userId: 'u',
40+
eventId: AnalyticsEvent.APP_LAUNCHED,
4141
level,
4242
msg,
4343
}),
4444
})
45+
// PII fields should NOT be in properties (security fix)
46+
expect(out[0].properties).not.toHaveProperty('userId')
4547
})
4648

4749
it('buffers when no user and flushes once user appears', () => {

common/src/util/__tests__/analytics-log.test.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,19 +51,49 @@ describe('analytics-log helpers', () => {
5151

5252
it('builds payload when event and userId exist', () => {
5353
const payload = toTrackableAnalyticsPayload({
54-
data: { eventId: AnalyticsEvent.APP_LAUNCHED, userId: 'u1', extra: 123 },
54+
data: { eventId: AnalyticsEvent.APP_LAUNCHED, userId: 'u1', duration: 123 },
5555
level: baseLevel,
5656
msg: baseMsg,
5757
})!
5858

5959
expect(payload.event).toBe(AnalyticsEvent.APP_LAUNCHED)
6060
expect(payload.userId).toBe('u1')
61+
// Only allowlisted properties are included (userId is extracted separately, not spread)
6162
expect(payload.properties).toMatchObject({
62-
userId: 'u1',
63-
extra: 123,
63+
eventId: AnalyticsEvent.APP_LAUNCHED,
64+
duration: 123,
6465
level: baseLevel,
6566
msg: baseMsg,
6667
})
68+
// PII fields should NOT be in properties
69+
expect(payload.properties).not.toHaveProperty('userId')
70+
})
71+
72+
it('filters out PII and unknown properties', () => {
73+
const payload = toTrackableAnalyticsPayload({
74+
data: {
75+
eventId: AnalyticsEvent.APP_LAUNCHED,
76+
userId: 'u1',
77+
email: 'test@example.com',
78+
password: 'secret',
79+
unknownField: 'value',
80+
duration: 500,
81+
success: true,
82+
},
83+
level: baseLevel,
84+
msg: baseMsg,
85+
})!
86+
87+
// Safe properties are included
88+
expect(payload.properties.duration).toBe(500)
89+
expect(payload.properties.success).toBe(true)
90+
expect(payload.properties.eventId).toBe(AnalyticsEvent.APP_LAUNCHED)
91+
// PII is excluded
92+
expect(payload.properties).not.toHaveProperty('userId')
93+
expect(payload.properties).not.toHaveProperty('email')
94+
expect(payload.properties).not.toHaveProperty('password')
95+
// Unknown properties are excluded
96+
expect(payload.properties).not.toHaveProperty('unknownField')
6797
})
6898

6999
it('falls back to nested and underscored user ids', () => {

0 commit comments

Comments
 (0)