fix: bound telemetry validator warn cache (#4106)

This commit is contained in:
Neil 2026-05-31 04:26:26 -07:00 committed by GitHub
parent dd71917852
commit 9e2d275618
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 64 additions and 2 deletions

View File

@ -0,0 +1,37 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
_getValidatorWarnCacheSizeForTests,
_resetValidatorWarnCacheForTests,
validate
} from './validator'
describe('telemetry validator warn cache', () => {
beforeEach(() => {
_resetValidatorWarnCacheForTests()
vi.spyOn(console, 'warn').mockImplementation(() => {})
})
afterEach(() => {
vi.restoreAllMocks()
})
it('bounds warn rate-limit entries for unique invalid event names', () => {
for (let i = 0; i < 300; i++) {
validate(`not_a_real_event_${i}` as never, {})
}
expect(_getValidatorWarnCacheSizeForTests()).toBeLessThanOrEqual(256)
})
it('prunes expired warn rate-limit entries', () => {
vi.spyOn(Date, 'now').mockReturnValue(0)
validate('not_a_real_event' as never, {})
expect(_getValidatorWarnCacheSizeForTests()).toBe(1)
vi.spyOn(Date, 'now').mockReturnValue(60_000)
validate('another_fake_event' as never, {})
expect(_getValidatorWarnCacheSizeForTests()).toBe(1)
})
})

View File

@ -34,15 +34,36 @@ export type ValidationResult<N extends EventName> =
| { ok: false; reason: string }
const WARN_WINDOW_MS = 60_000
const WARN_CACHE_MAX_ENTRIES = 256
const lastWarnAt = new Map<string, number>()
function pruneWarnCache(now: number): void {
for (const [key, at] of lastWarnAt) {
if (now - at >= WARN_WINDOW_MS) {
lastWarnAt.delete(key)
}
}
while (lastWarnAt.size > WARN_CACHE_MAX_ENTRIES) {
const oldest = lastWarnAt.keys().next()
if (oldest.done) {
break
}
lastWarnAt.delete(oldest.value)
}
}
function warnRateLimited(key: string, message: string): void {
const now = Date.now()
const prev = lastWarnAt.get(key) ?? 0
if (now - prev < WARN_WINDOW_MS) {
pruneWarnCache(now)
const prev = lastWarnAt.get(key)
if (prev !== undefined && now - prev < WARN_WINDOW_MS) {
return
}
// Why: renderer-originated event names are untrusted; keep the rate-limit
// table bounded even if a bad caller sends unique invalid names forever.
lastWarnAt.delete(key)
lastWarnAt.set(key, now)
pruneWarnCache(now)
console.warn(`[telemetry] ${message}`)
}
@ -76,6 +97,10 @@ export function _resetValidatorWarnCacheForTests(): void {
lastWarnAt.clear()
}
export function _getValidatorWarnCacheSizeForTests(): number {
return lastWarnAt.size
}
// Re-exported so `client.ts` can re-validate the merged outgoing payload
// without reaching into `src/shared/telemetry-events.ts` directly. Keeps the
// validator as the single surface the client depends on.