Skip to content

feat: Deliver events with retries, a bounded buffer and a close timeout - #325

Merged
Zaimwa9 merged 7 commits into
mainfrom
openhands/sdk-spec-v0.9.0
Nov 13, 2026
Merged

Zaimwa9 merged 7 commits into
mainfrom
openhands/sdk-spec-v0.9.0

Conversation

@flagsmithsmith

Copy link
Copy Markdown
Contributor

Contributes to Flagsmith/sdk-spec#13
Contributes to Flagsmith/sdk-spec#23

renovate Bot and others added 4 commits November 11, 2026 15:33
Its entry in Flagsmith/sdk-spec's compatibility matrix at v0.9.0: `core events local-evaluation`.
…c v0.6.0

Spec v0.6.0 adds @events scenarios for getExperimentFlag, trackEvent and
trackExposureEvent, plus a "Create the SDK client with events enabled" step.
The SDK has supported these since 9.1.0, so the steps call the existing
public API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 6a108ea)
(cherry picked from commit 9fa2ae1)
(cherry picked from commit 93abd3f)
The getExperimentFlag step returned the feature name it was asked for,
so the spec could not tell whether the SDK resolved the right flag.
Report the evaluated flag's featureName instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit d8e4216)
(cherry picked from commit deb8b37)
@flagsmithsmith flagsmithsmith Bot added the sdk-spec Bumps of the Flagsmith SDK behavioural spec label Nov 11, 2026
@coderabbitai

coderabbitai Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: d0d9f70f-8dd6-4e93-b62c-1f3db3f5a9bb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@codspeed

codspeed Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 15 untouched benchmarks


Comparing openhands/sdk-spec-v0.9.0 (be7f8b8) with main (65ae046)

Open in CodSpeed

Implements event delivery as the SDK spec v0.9.0 describes it:

- Retry a failed batch up to 3 times per flush, backing off from 1s,
  doubling, with full jitter.
- Keep a batch whose last attempt finds the events API unavailable
  (network error, timeout, 408, 429, 502, 503, 504) for the next flush,
  ahead of newer events; drop it after a server error or a 4xx.
- Stop tracking events once the events API refuses the environment key
  with a 401 or 403.
- Run flushes one at a time, so a single batch is in flight.
- Drop the oldest event once the buffer holds maxBuffer events, rather
  than flushing early.
- Time out events API requests after 3 seconds by default.
- Accept a timeout in `close()`, aborting the request in flight once it
  has passed.

Wires the spec's flush, close, numbered events and local evaluation with
events steps to these.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@flagsmithsmith

Copy link
Copy Markdown
Contributor Author

Spec v0.9.0's event delivery and close scenarios, and the local evaluation scenarios with events, failed because the SDK lacked the behaviour they describe. This push implements that behaviour in EventProcessor and wires the spec steps to it.

SDK changes (sdk/events.ts, sdk/index.ts, sdk/types.ts)

  • Retries: a failed batch gets up to 3 attempts per flush. The backoff starts at 1s and doubles each time, with full jitter. Before, it got 2 attempts with a fixed backoff.
  • Keep or drop after the last attempt: if the last attempt finds the events API unavailable (a network error, a timeout, or a 408, 429, 502, 503 or 504), the batch is kept and sent with the next flush, ahead of newer events. A batch whose last attempt gets any other server error is dropped. A 4xx drops the batch at once, with no retry.
  • 401/403: the events API refused the environment key, so the processor drops what it has buffered and stops tracking events.
  • One batch in flight: flushes now run one after the other.
  • Bounded buffer: once maxBuffer events are buffered (1000 by default), each new event drops the oldest. The buffer no longer flushes early when it fills up: the "full buffer drops its oldest event" scenario fails if it does.
  • Request timeout: events API requests time out after 3s by default, instead of using the client's 10s timeout. eventProcessorConfig.requestTimeoutMs still overrides it.
  • close(timeoutMs?): takes an optional timeout. When it runs out, close aborts the request and any backoff in flight, then returns. Without a timeout it waits for the flush, as before.

Steps (sdk-spec/steps.mts)
Adds the steps Create the SDK client with local evaluation and events enabled, tracks events "event_{first}" to "event_{last}", flushes events and closes the client with a timeout of {timeout} seconds. Each one calls the public SDK API.

Tests
The unit tests in tests/sdk/events.test.ts now cover the backoff bounds and jitter, the keep/drop rules for each kind of failure, the 401/403 stop, the bounded buffer and how a kept batch is trimmed to fit it, flushes running one at a time, the default request timeout, and stop with a timeout. Tests for the old early flush on a full buffer and the old 2-attempt retry are replaced.

Locally, all 24 spec scenarios for core events local-evaluation pass against the pinned Flagsmith stack, and npm test passes (490 tests).

@flagsmithsmith
flagsmithsmith Bot marked this pull request as ready for review November 11, 2026 16:41
@flagsmithsmith
flagsmithsmith Bot requested a review from a team as a code owner November 11, 2026 16:41
@flagsmithsmith
flagsmithsmith Bot requested review from emyller and removed request for a team November 11, 2026 16:41

@Zaimwa9 Zaimwa9 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting changes. Spec passes, but close sends a request after returning, interval flushes pile up during an outage, and removing the full-buffer send loses events above ~100/s. Please also retitle: the tracking methods are already on main.

Comment thread sdk/events.ts Resolved
Comment thread sdk/events.ts Resolved
Comment thread sdk/events.ts Resolved
Comment thread sdk/events.ts Resolved
Comment thread sdk/events.ts Resolved
Addresses review feedback on event delivery:

- Send no further attempt once a close timeout aborts the backoff: the
  next attempt's request was not tied to the already aborted signal.
- Skip a flush timer tick while a flush is running or queued, so ticks
  no longer pile up behind a batch that keeps failing.
- Flush the buffer when an event is tracked while it is full, unless a
  batch is in flight or kept from a failed flush: only then does the new
  event drop the oldest one.
- Retry only network errors, timeouts and 408, 429, 500, 502, 503 and
  504 responses. Any other failing status drops the batch at once.
- Ignore events tracked and flushes requested after `close()`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@flagsmithsmith flagsmithsmith Bot changed the title feat: Support getExperimentFlag, trackEvent and trackExposure methods feat: Deliver events with retries, a bounded buffer and a close timeout Nov 12, 2026
@flagsmithsmith

Copy link
Copy Markdown
Contributor Author

Thanks for the review. All of it is addressed in ae69b84. Replies are on each inline comment; here are the review-level points:

Spec passes, but close sends a request after returning,

Fixed. Each attempt now checks the abort signal before it sends, so a close timeout that hits during the backoff stops the retries.

interval flushes pile up during an outage,

Fixed. A timer tick is skipped while a flush is running or queued.

and removing the full-buffer send loses events above ~100/s.

Fixed. Tracking an event into a full buffer flushes it again. The new event drops the oldest one only when a batch is in flight or kept from a failed flush. Because the flush triggers when an event arrives at a full buffer, not when the buffer reaches maxBuffer, the spec's A full buffer drops its oldest event scenario still passes. All 24 spec scenarios pass locally.

Please also retitle: the tracking methods are already on main.

Retitled to "feat: Deliver events with retries, a bounded buffer and a close timeout".

close() is now terminal as well, and retries use the explicit status set 408, 429, 500, 502, 503 and 504. npm test passes (499 tests).

@Zaimwa9

Zaimwa9 commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

@themis-blindfold review

@themis-blindfold

Copy link
Copy Markdown

Review failed after 2 attempt(s) (OutputError). Check the worker logs.

@flagsmithsmith

Copy link
Copy Markdown
Contributor Author

@themis-blindfold review

The review bot didn't produce a review: it replied "Review failed after 2 attempt(s) (OutputError)", so there are no findings to address, and I haven't changed any code. The branch is still at ae69b84, which addresses your earlier review, and every check on it passes. Whether to run the bot again is up to you.

@Zaimwa9

Zaimwa9 commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

@themis-blindfold review

Comment thread sdk/events.ts Resolved
Comment thread sdk/events.ts Resolved
@themis-blindfold

Copy link
Copy Markdown

⚖️ Themis review: 🟠 Fix before merge

The event processor adds bounded buffering, retries and a shutdown timeout, but its handling of HTTP 500 conflicts with the delivery requirement, and its exposure deduplication state can outlive events evicted from the buffer. The recorded CI checks passed, including the SDK specification and Node build-and-test jobs.

Area Score
🎯 Correctness 2/5
🧪 Test coverage 3/5
📐 Code quality 3/5
🚀 Product impact 4/5

🟠 Majors

  • sdk/events.ts:30 — HTTP 500 responses are retried contrary to the stated delivery requirement.
  • sdk/events.ts:339 — Evicted exposures remain in the deduplication set.
📝 Walkthrough
  • Event delivery — serialises flushes, retries selected failures and retains unavailable batches.
  • Buffer and shutdown — caps buffered events and adds a terminal close with an optional timeout.
  • Specification — upgrades the shared SDK scenarios and adds event step implementations.
🧪 How to verify
  • Return HTTP 500 from the events service and check that a flush makes only one request.
  • Fill a retained buffer, evict an exposure, then track that exposure again and inspect the next batch.
  • Exercise close while a request or retry backoff is in progress and check its timeout.
  • Automate: add regression tests for HTTP 500 and exposure eviction during an outage.

Product take: More resilient event delivery matters for experiment measurements, especially during transient outages; accurate buffering and retry decisions are central to that benefit.

🧭 Assumptions & unverified claims

No unverified assumptions or claims.

May the next flush be uneventful · reviewed at ae69b84

An exposure evicted from a full buffer kept its deduplication key until
the next flush started, so tracking it again during an outage discarded
it although no copy remained, and keys could outgrow the buffer.

Evicting or trimming an exposure now forgets its key. Restoring a kept
batch rebuilds the keys from the buffer, keeping the batch's copy of an
exposure tracked again while it was in flight.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@flagsmithsmith

Copy link
Copy Markdown
Contributor Author

@themis-blindfold review

The bot's review (Fix before merge) had two findings. I've replied to each inline:

  • Drop HTTP 500 without retrying: not changed. The released v0.9.0 scenarios require retrying a 500: A batch whose last attempt fails otherwise is kept, after server errors expects the same batch to be answered 500, 500, 503, then 202. Your earlier review also asked for this set. sdk-spec#23 says otherwise, so the issue and the scenarios disagree. I'll leave it to you whether to raise that in sdk-spec.
  • Evicted exposures stay in the deduplication set: fixed in be7f8b8, with regression tests. The keys now always match the exposures in the buffer.

npm test passes (502 tests), and all 24 spec scenarios pass locally.

@Zaimwa9
Zaimwa9 merged commit 0a9b9ed into main Nov 13, 2026
7 checks passed
khvn26 pushed a commit that referenced this pull request Nov 13, 2026
🤖 I have created a release *beep* *boop*
---


##
[9.4.0](v9.3.0...v9.4.0)
(2026-11-13)


### Features

* Deliver events with retries, a bounded buffer and a close timeout
([#325](#325))
([0a9b9ed](0a9b9ed))


### Other

* **deps-dev:** bump source-map-js from 1.2.1 to 1.2.2
([#306](#306))
([65ae046](65ae046))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sdk-spec Bumps of the Flagsmith SDK behavioural spec

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants