fix: bound telemetry validator warn cache (#4106)
This commit is contained in:
parent
dd71917852
commit
9e2d275618
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue