diff --git a/.changeset/transport-http-requeue.md b/.changeset/transport-http-requeue.md new file mode 100644 index 0000000..11755f8 --- /dev/null +++ b/.changeset/transport-http-requeue.md @@ -0,0 +1,7 @@ +--- +'@smooai/observability': patch +--- + +TypeScript: re-queue ingest batches when native `fetch` resolves a non-2xx response. + +Native `fetch` resolves normally for HTTP 4xx and 5xx responses. The transport only handled rejected promises, so a non-2xx ingest response removed the batch from the queue permanently instead of retrying it. The transport now treats `response.ok === false` as a failure and pushes the batch back onto the queue for the existing retry path. diff --git a/packages/core/src/__tests__/transport.test.ts b/packages/core/src/__tests__/transport.test.ts index f00c4f4..1865015 100644 --- a/packages/core/src/__tests__/transport.test.ts +++ b/packages/core/src/__tests__/transport.test.ts @@ -93,4 +93,14 @@ describe('Transport', () => { // the queue after the throw rather than being dropped. expect(t._queueSize()).toBe(1); }); + + it('re-queues the batch when native fetch resolves a non-2xx response', async () => { + const fetcher = vi.fn().mockResolvedValue({ ok: false, status: 503 }); + const t = new Transport({ dsn: 'https://example.com', maxBatchSize: 1, flushIntervalMs: 1000 }, { canBeacon: false, fetcher }); + t.enqueue(evt('a')); + await vi.runOnlyPendingTimersAsync(); + await Promise.resolve(); + expect(fetcher).toHaveBeenCalled(); + expect(t._queueSize()).toBe(1); + }); }); diff --git a/packages/core/src/transport.ts b/packages/core/src/transport.ts index 2032a1c..174dfeb 100644 --- a/packages/core/src/transport.ts +++ b/packages/core/src/transport.ts @@ -81,12 +81,13 @@ export class Transport { // (`@smooai/fetch`) — it owns retries/timeouts/circuit-breaking. // Fall back to global fetch when no fetcher was injected (tests). const fetcher = this.adapter.fetcher ?? ((url, init) => fetch(url, init)); - await fetcher(this.opts.dsn, { + const response = await fetcher(this.opts.dsn, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload), keepalive: true, }); + if (response.ok === false) throw new Error('ingest request failed'); } catch { // Best-effort: push events back to the front of the queue for next attempt. this.queue.unshift(...batch);