Merge pull request #700 from getagentseal/fix/telemetry-flush-4xx

fix: telemetry flush drops 4xx-rejected batches
This commit is contained in:
Resham Joshi 2026-07-16 12:11:22 -07:00 committed by GitHub
commit 584340d8bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 17 additions and 3 deletions

View file

@ -137,9 +137,9 @@ describe('events', () => {
expect(telemetry.queueLength).toBe(before + 1)
})
it('keeps the queue on a failed POST and clears it on success', async () => {
it('keeps the queue on a transient failure (5xx) and clears it on success', async () => {
let ok = false
const fetchFn = vi.fn(async () => ({ ok })) as unknown as typeof fetch
const fetchFn = vi.fn(async () => ({ ok, status: 503 })) as unknown as typeof fetch
const { telemetry } = make({ fetchFn })
telemetry.completeOnboarding(true)
expect(await telemetry.flush()).toBe(false)
@ -149,6 +149,14 @@ describe('events', () => {
expect(telemetry.queueLength).toBe(0)
})
it('drops a permanently rejected batch (4xx) instead of wedging the queue', async () => {
const fetchFn = vi.fn(async () => ({ ok: false, status: 400 })) as unknown as typeof fetch
const { telemetry } = make({ fetchFn })
telemetry.completeOnboarding(true)
expect(await telemetry.flush()).toBe(false)
expect(telemetry.queueLength).toBe(0)
})
it('batches with the wire contract: schema, installId, app block, events', async () => {
const { telemetry, posts } = make()
telemetry.completeOnboarding(true)

View file

@ -244,7 +244,13 @@ export class Telemetry {
headers: { 'content-type': 'application/json' },
body,
})
if (!res.ok) return false
if (!res.ok) {
// 4xx is a permanent rejection of this batch (schema drift, bad shape):
// retrying the same payload forever would wedge the queue at its cap.
// Drop it. 5xx/network are transient — keep the batch for the next beat.
if (res.status >= 400 && res.status < 500) this.queue = this.queue.filter(e => !events.includes(e))
return false
}
// Only drop what was sent; events tracked mid-flight stay queued.
this.queue = this.queue.filter(e => !events.includes(e))
return true