Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/node-flush-drains-spans.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'posthog-node': minor
---

`flush()` now drains queued tracing spans as well as events, so a serverless handler that calls `flush()` before returning no longer leaves ended spans sitting in the queue until the container is reused. Events and spans are flushed concurrently, and a failed span export leaves the spans queued for the next flush rather than rejecting.
19 changes: 14 additions & 5 deletions packages/node/src/__tests__/traces.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,15 +297,24 @@ describe('PostHog traces', () => {
})

describe('flush cycle', () => {
it('flushes on its own interval, not with the events pipeline', async () => {
// Traces are a separate pipeline with their own queue and endpoint.
// `posthog.flush()` drains events only; wiring traces into it is a
// deliberate follow-up because it changes that method's contract.
it('drains queued spans', async () => {
posthog.startSpan('checkout').end()
await posthog.flush()
expect(traceRequests()).toHaveLength(0)

expect(sentSpans()).toHaveLength(1)
})

it('resolves when the span export fails', async () => {
mockedFetch.mockRejectedValue(new Error('network down'))
posthog.startSpan('checkout').end()

await expect(posthog.flush()).resolves.toBeUndefined()
})

it('still flushes on its own interval', async () => {
posthog.startSpan('checkout').end()
await flushTraces()

expect(sentSpans()).toHaveLength(1)
})
})
Expand Down
17 changes: 15 additions & 2 deletions packages/node/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,21 @@ export abstract class PostHogBackendClient extends PostHogCoreStateless implemen
this.scheduleDebouncedFlush()
}

/**
* Concurrent so a serverless handler waits for one round trip, not two. A
* failed span export leaves the spans queued rather than rejecting, since
* callers already treat `flush()` as safe to leave unwrapped.
*/
private _flushEventsAndSpans(): Promise<void> {
const events = this.flushWithPendingPromises()
if (!this._traces) {
return events
}
return Promise.all([events, this._traces.flush().catch(() => {})]).then(() => undefined)
}

override async flush(): Promise<void> {
const flushPromise = this.flushWithPendingPromises()
const flushPromise = this._flushEventsAndSpans()
const waitUntil = this.options.waitUntil
// Only register when no debounce promise is already keeping runtime alive
if (waitUntil && !this._waitUntilCycle) {
Expand Down Expand Up @@ -359,7 +372,7 @@ export abstract class PostHogBackendClient extends PostHogCoreStateless implemen
private async resolveWaitUntilFlush(): Promise<void> {
const resolve = this._consumeWaitUntilCycle()
try {
await this.flushWithPendingPromises()
await this._flushEventsAndSpans()
} catch {
// Flush errors are already logged by flush() internals
} finally {
Expand Down