Conversation
In order to gradually roll out proper Zero to production users with
instant rollback, this PR extends the feature flag system with
percentage-based rollout and adds a `proper_zero` flag.
### What changed
**Shared types** (`dotcom-shared`): Feature flags are now a
discriminated union — `BooleanFeatureFlag` (on/off for everyone) or
`PercentageFeatureFlag` (rolled out to X% of users via deterministic
FNV-1a hash of userId+flagName).
**Server** (`sync-worker`):
- `evaluateFlagForUser` gates on `enabled` first (master kill switch),
then checks `hash < percentage` for percentage flags
- Separate `getFeatureFlags` (per-user evaluated) and
`getFeatureFlagsAdmin` (raw stored values) endpoints
- Admin POST accepts partial updates (`{ enabled }` and `{ percentage }`
are independent)
**Client**:
- `fetchFeatureFlags()` is a module-level promise that fires immediately
on import (no React component or atoms needed for initial fetch).
Resolves with defaults on failure — never rejects, never hangs.
- `FeatureFlagsFetcher` component now only handles subsequent polling +
reload-on-change
- `shouldUseProperZero(flags, email)` takes flags as a parameter.
Priority: localStorage override > @tldraw.com email > server flag, with
reason logging
- `TldrawApp` accepts `flags` in `create()` and stores `useProperZero`
decision as an instance property — `preload()` uses the stored decision
instead of re-evaluating (fixes bug where @tldraw.com users with server
flag off would skip the `/init` endpoint)
- `window.zero()` dev escape hatch to toggle localStorage override
- Removed `featureFlagsAtom`, `featureFlagsLoadedAtom`, and the reactive
wait/timeout pattern
- Deleted unused `useFeatureFlags` hook
**Admin UI**: Percentage flags get a separate component with independent
checkbox (enabled) and text input (percentage) + Save button. Disabled
states with proper CSS.
### Change type
- [x] `feature`
### Test plan
1. Deploy to staging
2. In admin, toggle `proper_zero` checkbox on → confirm → flag enabled
at 0%
3. Set percentage to 10, save → 10% of users get proper Zero
4. Uncheck checkbox → flag disabled, percentage preserved
5. Re-check → previous percentage still there
6. Verify @tldraw.com users always get proper Zero
7. Verify `window.zero()` toggles localStorage override and reloads
8. Verify auto-reload when flag value changes server-side
9. Verify @tldraw.com user with server flag off: proper Zero used AND
`/init` endpoint called
- [ ] Unit tests
- [ ] End to end tests
### Release notes
- Internal infrastructure for gradual Zero rollout
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Touches rollout controls and client startup behavior for selecting the
Zero implementation; mistakes could unexpectedly flip users between
`Zero` and `ZeroPolyfill` or cause reloads, but changes are gated behind
feature flags and admin-only endpoints.
>
> **Overview**
> Adds **percentage-based feature flags** alongside boolean flags by
updating shared types to a discriminated union and introducing an
evaluated flag shape for the user-facing endpoint.
>
> On the server, feature flags now support per-user evaluation via
deterministic hashing for percentage rollouts, add a new `proper_zero`
flag, split endpoints into user-evaluated (`/app/feature-flags`) vs
admin-raw (`/app/admin/feature-flags`), and allow admin updates to set
`enabled` and/or `percentage` independently; internal flag checks (e.g.
SQLite DO storage) switch to `getFeatureFlagEnabled`.
>
> On the client, replaces the atom-based
`FeatureFlagsFetcher`/`useFeatureFlags` with `FeatureFlagPoller` + a
cached `fetchFeatureFlags()` used during app startup, wires flags+email
into `TldrawApp` to decide between proper Zero vs `ZeroPolyfill` (with a
localStorage override and clearer logging), and updates the admin
feature-flag UI to edit percentage rollouts with disabled-state styling.
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
02eb465. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: David Sheldrick <d.j.sheldrick@gmail.com>
### Change type - [ ] `bugfix` - [ ] `improvement` - [ ] `feature` - [ ] `api` - [x] `other`
followup to #8294 ### Change type - [ ] `bugfix` - [ ] `improvement` - [ ] `feature` - [ ] `api` - [x] `other`
followup again to #8319 bad bot 🤖 ### Change type - [ ] `bugfix` - [ ] `improvement` - [ ] `feature` - [ ] `api` - [x] `other`
In order to eliminate layout thrashing when resizing multiple geo shapes with text labels, this PR batches all label DOM measurements into a single pass per frame. https://github.com/user-attachments/assets/40f361f0-e619-4c75-9f64-7e302898d158 Previously, each geo shape's `onResize` independently measured its label via `measureHtml`, causing the browser to recalculate layout for every shape. With many labeled shapes selected, this created a costly read-write-read-write cycle. This PR introduces a three-part optimization: 1. **`TextManager.measureHtmlBatch`** — A new method that measures multiple HTML strings in a single batch using a pool of reusable DOM elements. All style/innerHTML writes happen first, then all measurements are read, triggering only one browser layout reflow. The pool grows on demand and caches innerHTML to skip redundant parsing. 2. **`GeoShapeUtil` measurement helpers** — `measureUnscaledLabelSize` is refactored to extract `getGeoLabelMeasurementRequest` (builds html + opts without measuring) and `getGeoResizeTargetWidth` (computes target width during resize). A module-level batch cache (`setBatchLabelSizeCache`) lets `onResize` and `getGeometry` skip individual measurements when pre-computed sizes are available. 3. **`Resizing.batchMeasureGeoLabels`** — Before each resize loop, collects measurement requests for all eligible geo shapes, batch-measures them via `measureHtmlBatch`, and populates the cache. The cache is cleared on exit. Original branch: `steveruiz/batch-resize-measurements` Before: <img width="1324" height="708" alt="image" src="https://github.com/user-attachments/assets/de0891f9-5936-4456-9af2-52da13e927b5" /> After: <img width="1212" height="499" alt="image" src="https://github.com/user-attachments/assets/ff2d625e-f85f-41c9-9249-0e1e80fadb39" /> ### Change type - [x] `improvement` ### Test plan 1. Create several geo shapes with text labels 2. Select all shapes and resize from a corner handle 3. Verify shapes resize correctly with labels wrapping as expected 4. Verify single-shape resize still works correctly 5. Verify shapes without labels resize normally ### API changes - Added `TextManager.measureHtmlBatch()` for batched DOM text measurement ### Release notes - Improve resize performance for multiple geo shapes with text labels by batching DOM measurements <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes core resize-path behavior for `geo` shapes by introducing per-frame batched label measurement and caching, which could affect label wrapping/sizing or cleanup if edge cases slip through. Adds new public `TextManager` measurement types/APIs, so downstream consumers may be impacted by signature/type changes. > > **Overview** > **Improves multi-shape resize performance** by batching `geo` label measurements into a single DOM write-then-read pass per frame, avoiding per-shape layout thrash. > > Adds `TextManager.measureHtmlBatch()` plus new public types (`BatchMeasurementRequest`, `TLMeasuredTextSize`) and updates `measureHtml`/`measureText` to return the shared type. `GeoShapeUtil` now supports building measurement requests, uses a per-editor batch cache (set during resize), and `Resizing` populates/clears this cache around the resize loop so `onResize`/geometry can reuse precomputed label sizes. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 68dfd7d. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Kostya Farber <kostya.farber@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )