Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(telemetry): serverless-aware event delivery for TS SDKs#301

Merged
KIvanow merged 3 commits into
masterfrom
fix/ts-telemetry-serverless-delivery
Jul 3, 2026
Merged

fix(telemetry): serverless-aware event delivery for TS SDKs#301
KIvanow merged 3 commits into
masterfrom
fix/ts-telemetry-serverless-delivery

Conversation

@KIvanow

@KIvanow KIvanow commented Jul 3, 2026

Copy link
Copy Markdown
Member

Problem

TS SDK PostHog telemetry stopped being delivered from the chat's Vercel deployment after the library upgrades. On Vercel/Next the event loop freezes the instant the HTTP response returns, so:

  1. Init delivery: the *_init flush dequeues the event then is suspended mid-send → dropped.
  2. Snapshots: stats_snapshot is driven by setInterval, which never fires on a frozen lambda → snapshots never generated.

This is why agent-memory (one-shot, no periodic loop) went effectively dark while the caches — which keep the process alive via their stats loop — still showed events.

Fix (library-layer, TS only)

All 4 packages/*/src/analytics.ts:

  • getRequestWaitUntil() reads the ambient @vercel/request-context / @next/request-context symbol and returns the per-request waitUntil.
  • deliver() hands flush() to waitUntil under a request (keeps the invocation alive until sent), else flushes inline. capture() now self-delivers, so every event is delivered serverless-aware. init() no longer eager-flushes (capture handles it).
  • registerSnapshot(intervalMs, fn) + onActivity() drive stats snapshots from request traffic when setInterval is frozen; setInterval kept as the long-lived-server backstop.

Wiring:

  • agent-cache / semantic-cache: captureStatsSnapshot() returns a Promise and registers the snapshot; onActivity() called on the cache hot paths (LlmCache.check, ToolCache.check, SessionStore.get/set, SemanticCache.check/store).
  • agent-memory / retrieval: interface parity only; the capture → deliver change alone fixes their init/session delivery.

Python is intentionally untouched: no Vercel/Next analog, and its init flush is already inline-awaited inside an awaited operation.

Versions

  • @betterdb/agent-cache 0.9.0 → 0.10.0
  • @betterdb/semantic-cache 0.8.1 → 0.9.0
  • @betterdb/agent-memory 0.4.0 → 0.5.0
  • @betterdb/retrieval 0.4.0 → 0.5.0

Tests

describe('serverless delivery (waitUntil)') added to agent-cache/agent-memory/retrieval analytics.test.ts: init hands flush to waitUntil, onActivity emits a snapshot after the interval elapses (fake timers), no-op without a request context. Suites green: agent-cache 255, semantic-cache 200, agent-memory 160, retrieval 128. All 4 tsc --noEmit clean.


Note

Low Risk
Changes are confined to optional PostHog telemetry delivery and lightweight onActivity hooks on existing request paths; cache and retrieval behavior are unchanged aside from analytics side effects.

Overview
Restores PostHog product telemetry on Vercel/Next serverless, where the runtime freezes as soon as the HTTP response is sent and buffered flushes or setInterval stats loops were being dropped or never running.

Across agent-cache, semantic-cache, agent-memory, and retrieval, analytics now resolves the request-scoped waitUntil from @vercel/request-context / @next/request-context and routes PostHog flush() through it when inside a request so the invocation stays alive until events are sent. capture() always triggers delivery after enqueueing; init() still awaits an inline flush on CLIs and short-lived processes when there is no request context.

For periodic stats_snapshot events, registerSnapshot, onActivity, and snapshotTick share a single throttle clock: agent-cache and semantic-cache register snapshots, drive the interval timer via snapshotTick, and call onActivity from cache hot paths (LLM/tool/session checks and semantic check/store). agent-memory and retrieval get the same analytics API and capture → deliver behavior without extra hot-path wiring.

Package versions are bumped (e.g. agent-cache 0.10.0, semantic-cache 0.9.0). New serverless delivery (waitUntil) tests cover init flush delegation, traffic-driven snapshots, and duplicate-snapshot prevention.

Reviewed by Cursor Bugbot for commit 276a587. Bugbot is set up for automated code reviews on this repo. Configure here.

On Vercel/Next the event loop freezes when the response returns, so the
init flush was dropped and setInterval-driven stats_snapshot never fired.

- analytics.ts (all 4 pkgs): getRequestWaitUntil() reads the ambient
  @vercel/@next request-context; deliver() hands flush() to waitUntil under
  a request, else flushes inline; capture() self-delivers; init() no longer
  eager-flushes. Add registerSnapshot()/onActivity() to drive stats
  snapshots from request traffic when setInterval is frozen.
- agent-cache/semantic-cache: captureStatsSnapshot() returns a Promise and
  registers the snapshot; onActivity() wired into the cache hot paths.

Bump agent-cache 0.10.0, semantic-cache 0.9.0, agent-memory 0.5.0,
retrieval 0.5.0.
Comment thread packages/agent-cache/src/analytics.ts
Bugbot: removing the awaited flush from init() let `await init()` resolve
before PostHog I/O completed. A short-lived non-serverless process that
exits right after init (e.g. via process.exit(), which skips beforeExit)
could drop the init event again.

Restore an awaited flush in init(), guarded to the non-serverless path only
(under a request, capture()'s deliver() already hands the flush to waitUntil).
Mirrors the Python init's inline-awaited flush. Add a regression test in
agent-cache/agent-memory/retrieval asserting init awaits flush completion.
Comment thread packages/agent-cache/src/analytics.ts
…Activity

Bugbot: onActivity throttled traffic-driven snapshots with lastSnapshotAt,
but the cache's parallel setInterval called captureStatsSnapshot directly and
never consulted that clock. On a warm/thawed serverless invocation both paths
could emit a stats_snapshot for the same interval, inflating telemetry.

Route both through a single throttled runner in the analytics layer:
- Add snapshotTick() (interface + NOOP + PostHogAnalytics); extract the shared
  emitSnapshotIfDue(deliver) used by both onActivity (waitUntil delivery) and
  snapshotTick (inline delivery). Both consult/update the one lastSnapshotAt.
- agent-cache/semantic-cache setInterval now calls analytics.snapshotTick()
  instead of captureStatsSnapshot() directly.

Add a regression test asserting snapshotTick + onActivity don't double-emit
within one interval.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 276a587. Configure here.

this.emitSnapshotIfDue((p) => {
void p;
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Serverless snapshotTick skips waitUntil

Medium Severity

On warm serverless invocations, snapshotTick can run while a request is active but always schedules snapshot work with a no-op deliver instead of the request waitUntil. That updates the shared lastSnapshotAt, so onActivity skips the same interval and cannot deliver the snapshot before the invocation freezes.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 276a587. Configure here.

@KIvanow KIvanow merged commit fc046c3 into master Jul 3, 2026
2 of 3 checks passed
@KIvanow KIvanow deleted the fix/ts-telemetry-serverless-delivery branch July 3, 2026 13:30
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 3, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant