Skip to content

fix: do not truncate bucket ranges to 4 decimals - #184

Closed
gazzdingo wants to merge 3 commits into
growthbook:mainfrom
gazzdingo:fix/bucket-range-rounding
Closed

gazzdingo wants to merge 3 commits into
growthbook:mainfrom
gazzdingo:fix/bucket-range-rounding

Conversation

@gazzdingo

@gazzdingo gazzdingo commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

getBucketRanges passed every range start and end through roundTo(4), which is (x * 10000).toInt() / 10000, so it truncates rather than rounds: 0.344 became 0.3439 and 0.172 became 0.1719. The JS SDK (the reference) keeps the raw doubles. A user whose hash lands on or just inside a range end therefore got a different variation than in JS.

This came out of a cross-SDK bucketing audit. The shared spec is getting 7 new run cases for users whose hash lands exactly on a range end (growthbook/growthbook#7375); 4 of them failed here.

  • gb_utils.dart: push [start, end] unrounded.
  • test/test_cases/test_case.dart: the 7 new run cases, 2 rollout-boundary feature cases and 9 hash pins appended.
  • DoubleExt.roundTo is deprecated (public API, no longer used by the SDK).
  • gb_utils_test.dart: isCompareBucket compared exactly, which can't hold for raw doubles (0.2 + 0.06 ≠ the spec's 0.26); it now mirrors the JS spec test (floor(n * 1e8) / 1e8 on both sides).

Testing (flutter test in ghcr.io/cirruslabs/flutter:stable): main is 341 passed. With the new cases on the old code, the run spec test fails with 4 failing cases. With the fix: 341 passed, 0 failed.

@vazarkevych vazarkevych left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed: LGTM.

Correctness

  • After this change getBucketRanges matches the JS SDK (util.ts) exactly: same weight/coverage arithmetic, equal weights 1/n, and chooseVariation uses the same n >= start && n < end test.
  • isCompareBucket now compares at floor(n * 1e8) / 1e8, the same tolerance as json.test.ts in the JS SDK. Exact comparison can't hold for unrounded doubles (0.2 + 0.06 != 0.26), so this is the right fix rather than a loosened test.
  • Verified that the new run cases fail with the old gb_utils.dart and pass with the fix. CI is green.
  • No other SDK code calls DoubleExt.roundTo: the roundTo in gb_utils.dart is a separate extension on num. So the deprecation won't trip deprecated_member_use_from_same_package on the CI's Flutter 3.27 analyzer.

Compatibility

  • No API break: roundTo is deprecated, not removed.
  • Behavior: only users whose hash lies within 0.0001 below a range boundary change variation (about 0.01% per boundary), and they move to the variation the JS SDK and other SDKs already assign. Users with a sticky-bucket assignment are unaffected. We treat this as a fix (minor), but suggest one line in the release notes: "bucket range boundaries now match the JS SDK exactly".

Notes

  • The 7 run cases, 2 feature cases and 9 hash pins are not in cases.json 0.9.0 yet; they come from growthbook/growthbook#7375, which is still open. If that PR changes, these cases need to be resynced.
  • The 2 rollout-boundary feature cases also pass on the old code (rollouts don't go through getBucketRanges). That's fine as regression pins.
  • Merge order: see #192. This PR conflicts with #178 and #174 in test/test_cases/test_case.dart; when those are resolved, these cases must be kept, since they are not in the 0.9.0 spec.

@andywhite37 andywhite37 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.

No findings; the boundary cases pass and GitHub CI passes. All 341 tests pass serially; the parallel run failed the unchanged 304 cache test while tests shared a cache directory. Device testing was not performed.

andywhite37 added a commit that referenced this pull request Oct 26, 2026
…ypes

Brings in growthbook-flutter#184 by gazzdingo: bucket ranges are no
longer truncated to 4 decimals, so users whose hash lands on a range end
are assigned like the JS SDK (spec 0.11.2 range-end cases from
growthbook/growthbook#7375).

test/test_cases/test_case.dart keeps this branch's verbatim spec 0.11.2
snapshot. #184's added "rollout includes the user at exactly coverage"
cases are superseded by growthbook/growthbook#7425 (n < coverage).

BREAKING CHANGE: users whose hash falls between a truncated and an exact
bucket range end can be assigned a different variation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants