Skip to content

feature flags#1010

Open
goldflag wants to merge 8 commits into
masterfrom
feature-flags
Open

feature flags#1010
goldflag wants to merge 8 commits into
masterfrom
feature-flags

Conversation

@goldflag
Copy link
Copy Markdown
Collaborator

@goldflag goldflag commented May 27, 2026

Summary by CodeRabbit

  • New Features

    • Full Feature Flags system: admin UI to create/edit/delete flags (boolean, multivariate, remote-config) with targeting, variants, rollouts; new Feature Flags page and conditional sidebar entry
    • Client/public API to read flags/payloads (flag/flags/flagPayload/flagPayloads) and onReady hook; visitor identity persisted and flags included in tracking payloads with exposure events
  • Tests

    • Added unit/integration tests for evaluator, tracking, and config flows
  • Chores

    • Regenerated/updated localization files across multiple languages; added editor dependency for JSON payload editing

Review Change Stack

@vercel
Copy link
Copy Markdown

vercel Bot commented May 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
rybbit Ready Ready Preview, Comment May 27, 2026 3:42am

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an end-to-end feature-flag system: DB migration and schema, server-side evaluation and CRUD APIs with validation, client admin UI and typed endpoints/hooks, analytics-script visitor evaluation and tracker exposure events, ClickHouse/tracking schema updates, and multi-locale message updates.

Changes

Feature Flags System

Layer / File(s) Summary
Client API endpoints & hooks
client/src/api/analytics/endpoints/featureFlags.ts, client/src/api/analytics/endpoints/index.ts, client/src/api/analytics/hooks/featureFlags/useFeatureFlags.ts
Typed feature-flag models and authenticated fetch/create/update/delete endpoints with React Query hooks that invalidate ["feature-flags", site] on mutations.
Client admin UI
client/src/app/[site]/feature-flags/page.tsx, client/src/app/[site]/feature-flags/components/*, client/src/app/[site]/feature-flags/lib/*
Admin UI: FeatureFlagsPage, dialog editors, ConditionSetsEditor, TargetingRulesEditor, VariantsEditor, FeatureFlagTable, JsonEditor, form state helpers and labels, and validation/buildPayload logic.
Routing, sidebar, translations
client/src/lib/siteRoute.ts, client/src/app/[site]/components/Sidebar/Sidebar.tsx, client/messages/*.json
Adds feature-flags to synced routes, conditionally renders “Feature Flags” nav item for non-private sites, and updates multiple locale message catalogs (including zh.json).
DB migration & snapshot
server/drizzle/0006_first_logan.sql, server/drizzle/meta/*, server/drizzle/meta/_journal.json
Adds migration to create feature_flags table and corresponding Drizzle snapshot and journal entry.
Postgres schema & ClickHouse change
server/src/db/postgres/schema.ts, server/src/db/clickhouse/clickhouse.ts
Exports Drizzle featureFlags table/types and alters ClickHouse events to include feature_flags Map(String,String).
Server API schemas & validation
server/src/api/featureFlags/schemas.ts
Zod schemas & cross-field validators for flag creation/update/evaluation including recursive payload value validation and multivariate/condition-set constraints.
Feature flags API endpoints
server/src/api/featureFlags/index.ts
Fastify CRUD and evaluation handlers: list with ClickHouse stats, create/update/delete with validation/optimistic versioning, evaluate endpoints that build context and call evaluator.
Evaluator & tests
server/src/services/featureFlags/evaluator.ts, server/src/services/featureFlags/evaluator.test.ts
Evaluation engine implementing deterministic SHA-256 bucketing, rule matching, variant selection, runtime gating, and comprehensive tests validating expected behaviors and determinism.
Analytics filter support
server/src/api/analytics/utils/getFilterStatement.ts, server/src/api/analytics/utils/getFilterStatement.test.ts, server/src/api/analytics/utils/query-validation.ts, shared/src/filters.ts
Adds feature_flag:<key> as valid filter parameter (runtime-validated pattern) and maps it to SQL feature_flags['key'] in analytics queries.
Analytics script config & visitor identity
server/src/analytics-script/config.ts, server/src/analytics-script/config.test.ts
Adds per-namespace visitorId stored in localStorage, fetchFeatureFlags POST helper to evaluate flags for context, and refactors parseScriptConfig to populate featureFlags.
Tracker feature flag support
server/src/analytics-script/tracking.ts, server/src/analytics-script/types.ts, server/src/analytics-script/tracking.test.ts
Tracker refreshes flags, stores assignments in config, includes feature_flags in base payloads, exposes getters for flag values/payloads, and emits deduped one-time feature_flag_exposure events.
Analytics script public API & public bundles
server/src/analytics-script/index.ts, server/public/script-full.js, server/public/script.js
Exposes window[namespace].flag()/flagPayload()/flags()/flagPayloads() and onReady(callback); early-call queuing and opt-out stubs updated; bundled scripts fetch/evaluate flags and track exposures.
Tracking payload schema & queue mapping
server/src/services/tracker/trackEvent.ts, server/src/services/tracker/types.ts, server/src/services/tracker/utils.ts, server/src/services/tracker/pageviewQueue.ts
Adds optional feature_flags (Record<string,string>) to tracking payload schemas, types, and pageview ClickHouse mapping.
Client dependency
client/package.json
Adds @monaco-editor/react dependency for JSON editor used in admin UI.

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant AnalyticsScript
  participant Server
  participant Postgres
  participant ClickHouse
  Browser->>AnalyticsScript: Load tracking script
  AnalyticsScript->>AnalyticsScript: Generate/load visitorId
  AnalyticsScript->>Server: POST /sites/:siteId/feature-flags/evaluate (context)
  Server->>Postgres: fetch flags for site
  Server->>Server: evaluateFeatureFlagsForSite(context)
  Server->>AnalyticsScript: return assignments
  AnalyticsScript->>AnalyticsScript: store in config.featureFlags
  Browser->>AnalyticsScript: window[ns].flag(key)
  AnalyticsScript->>Server: track feature_flag_exposure event
  Server->>ClickHouse: insert event with feature_flags
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • rybbit-io/rybbit#832: Related edits to the analytics tracking payload/event pipeline that intersect with feature-flag payload handling.
  • rybbit-io/rybbit#918: Overlapping edits to client/messages/zh.json and onboarding/subscription i18n changes.
  • rybbit-io/rybbit#597: Prior changes to parseScriptConfig/analytics script flow that this PR extends.

Poem

🐇 I found a flag beneath a log,
Rules stitched like stitches in a cog.
Visitors numbered, rollouts set,
Experiments hop, no regret.
Hooray — flags bloom across the bog!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature-flags

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
server/drizzle/0006_first_logan.sql (1)

1-25: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use Drizzle push workflow instead of committing manual SQL migration artifacts.

This migration file conflicts with the repo’s established Postgres schema change process. Please apply this schema change via npm run db:push and keep schema state aligned through that flow rather than manual SQL migration files.

Based on learnings: "In this repo (rybbit-io/rybbit), schema updates for PostgreSQL should be synced using npm run db:push (Drizzle push) rather than adding/maintaining SQL migration files or manually issuing ALTER TYPE statements."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/drizzle/0006_first_logan.sql` around lines 1 - 25, The PR adds a
manual SQL migration creating the "feature_flags" table and its constraints
(e.g., CONSTRAINT "feature_flags_site_key_unique",
"feature_flags_rollout_check", "feature_flags_type_check", FK
"feature_flags_site_id_sites_site_id_fk") which conflicts with the repo’s
Drizzle push workflow; instead remove this manual SQL artifact and apply the
schema change through the Drizzle workflow by updating your Drizzle schema
(model for feature_flags) and running npm run db:push so the table, indexes
(feature_flags_site_idx) and constraints are generated and tracked by Drizzle
rather than committing raw SQL migration files.
server/src/analytics-script/config.test.ts (1)

115-164: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing mock for the feature flags fetch in failure scenarios.

When the tracking config fetch fails (lines 120-123) or throws (line 171), fetchFeatureFlags is still called afterward (per the implementation). These tests only mock one fetch response, so the second call will either return undefined or throw unexpectedly. Add a second mock response for the feature flags endpoint:

Proposed fix for "API call fails" test
     // Mock failed API response
-    (global.fetch as any).mockResolvedValueOnce({
-      ok: false,
-      status: 404,
-    });
+    (global.fetch as any)
+      .mockResolvedValueOnce({
+        ok: false,
+        status: 404,
+      })
+      .mockResolvedValueOnce({
+        ok: false,
+      });
Proposed fix for "network error" test
     // Mock network error
-    (global.fetch as any).mockRejectedValueOnce(new Error("Network error"));
+    (global.fetch as any)
+      .mockRejectedValueOnce(new Error("Network error"))
+      .mockResolvedValueOnce({ ok: false });

Also applies to: 166-212

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/analytics-script/config.test.ts` around lines 115 - 164, The test
for parseScriptConfig does not mock the second fetch call to fetchFeatureFlags,
causing unexpected behavior; update the "should use defaults when API call
fails" and the analogous "network error" test to add a second global.fetch
mockResolvedValueOnce that returns a successful feature-flags response (e.g., {
ok: true, json: async () => ({}) }) after the first failing/throwing response so
fetchFeatureFlags receives a valid response; reference parseScriptConfig and
fetchFeatureFlags to locate where the dual fetch calls occur and ensure both
tests add the extra mockResolvedValueOnce for the feature flags call.
🧹 Nitpick comments (3)
server/src/analytics-script/tracking.ts (1)

22-22: 💤 Low value

exposedFeatureFlags Set can grow unbounded in long-running SPAs.

The Set is never cleared, so every unique key:version:value combination accumulates indefinitely. In long-lived sessions with frequent flag changes, this could cause memory growth. Consider clearing it on page changes or limiting its size.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/analytics-script/tracking.ts` at line 22, The exposedFeatureFlags
Set on the Tracking class (exposedFeatureFlags) is unbounded and can grow
indefinitely; modify the code that manages exposedFeatureFlags to prevent
unbounded growth by either (a) clearing it on navigation/page-change events
(listen for popstate/history changes or framework router events and call
exposedFeatureFlags.clear()), or (b) enforce a fixed capacity (implement an
eviction strategy such as simple FIFO/LRU: when adding in the method that
constructs the "key:version:value" string, if exposedFeatureFlags.size >=
MAX_FLAGS then remove the oldest entry before adding the new one). Update the
add/check logic where exposedFeatureFlags is used so it uses the new
clear-on-navigation or capped-set behavior.
server/src/analytics-script/config.ts (1)

47-84: ⚡ Quick win

Consider adding a timeout to the feature flags fetch.

fetchFeatureFlags has no timeout, so a slow or hanging server could delay script initialization indefinitely. An AbortController with a short timeout (e.g., 2-3 seconds) would prevent blocking the page while still allowing best-effort evaluation.

Proposed fix
 async function fetchFeatureFlags(
   analyticsHost: string,
   siteId: string,
   namespace: string,
   visitorId: string
 ): Promise<ScriptConfig["featureFlags"]> {
   try {
     const url = new URL(window.location.href);
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 3000);
+
     const response = await fetch(`${analyticsHost}/site/${siteId}/feature-flags/evaluate`, {
       method: "POST",
       headers: {
         "Content-Type": "application/json",
       },
       credentials: "omit",
+      signal: controller.signal,
       body: JSON.stringify({
         // ...
       }),
     });
+    clearTimeout(timeoutId);
 
     if (!response.ok) {
       return {};
     }
     // ...
   } catch (e) {
     return {};
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/analytics-script/config.ts` around lines 47 - 84,
fetchFeatureFlags currently can hang waiting for fetch; use an AbortController
with a short timeout (e.g., 2500ms) to cancel the request and avoid blocking
script init: create an AbortController, pass controller.signal to fetch, start a
timer that calls controller.abort() after ~2500ms, and clear the timer in
finally; ensure the existing catch (which returns {}) still handles the aborted
request gracefully. Reference the fetchFeatureFlags function, the fetch call,
and the catch/finally block when making these changes.
server/src/analytics-script/index.ts (1)

54-60: ⚡ Quick win

Tighten queued argument typing (any[] → safer typed/unknown args).

Using any[] here bypasses strict-mode guarantees on the public API queue path.

Suggested fix
-  type QueueEntry = [string, any[]];
+  type QueueEntry = [string, unknown[]];
   const earlyQueue: QueueEntry[] = [];
   const queueMethod =
     (method: string) =>
-    (...args: any[]) => {
+    (...args: unknown[]) => {
       earlyQueue.push([method, args]);
     };
As per coding guidelines: `**/*.{ts,tsx}`: Use TypeScript with strict typing throughout both client and server.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/analytics-script/index.ts` around lines 54 - 60, The tuple type
QueueEntry and queueMethod currently use any[] which bypasses strict typing;
change the second element from any[] to unknown[] (or a narrower union of
allowed argument types) and update usages that read earlyQueue/QueueEntry to
properly narrow or validate those unknowns (e.g., where entries are
replayed/consumed, add type guards or casts). Specifically update the type alias
QueueEntry, the earlyQueue declaration, and the queueMethod signature so it
returns (...args: unknown[]) => void, and fix downstream consumers to
handle/validate unknown[] instead of assuming any.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@client/messages/zh.json`:
- Line 10: The translation key "+R0VMp" currently has an empty value and must be
populated with the correct Chinese translation (or a temporary Chinese fallback
such as the English source) so the UI won't render blank; locate the key
"+R0VMp" in client/messages/zh.json and replace the empty string with the proper
translated text, and then scan other changed locale keys (the other empty
entries flagged in the review) in client/messages/*.json to ensure each key has
a non-empty zh value and matches the same keys used in other locales.

In `@client/src/api/analytics/hooks/featureFlags/useFeatureFlags.ts`:
- Around line 22-55: The mutation hooks call
createFeatureFlag/updateFeatureFlag/deleteFeatureFlag with an unguarded site
from useStore, which can be undefined during hydration; update
useCreateFeatureFlag, useUpdateFeatureFlag and useDeleteFeatureFlag to validate
site before calling the API by checking if (!site) and returning a rejected
Promise or throwing an Error inside the mutationFn (e.g., in
useUpdateFeatureFlag's mutationFn before calling updateFeatureFlag(site,...)),
so the mutation fails fast with a clear error instead of hitting
/sites/undefined/*; keep the existing onSuccess invalidation logic unchanged.

In `@client/src/app/`[site]/feature-flags/page.tsx:
- Around line 740-745: The "sessions" label is hardcoded in the span that
renders {stat.sessions.toLocaleString()} sessions; replace the literal with a
localized string using next-intl's useTranslations() (e.g., call const t =
useTranslations('FeatureFlags') at the top of the component in
client/src/app/[site]/feature-flags/page.tsx and render t('sessions') instead of
the hardcoded word), update the messages/ FeatureFlags translation JSON files to
include the "sessions" key (and plural forms if needed), and ensure the span
concatenates/localizes properly (e.g., use t('sessions', { count: stat.sessions
}) or format as required) so the label is translatable.
- Around line 32-33: The page is rendering hardcoded UI strings ("(empty)" and
"sessions")—use the existing next-intl extraction pattern instead of raw text:
obtain a translator (e.g., via useExtracted/translated messages or a t function
used in this file) and replace the literal "(empty)" and "sessions" usages with
localized calls (e.g., t('featureFlags.empty') and t('featureFlags.sessions'))
so they are extracted by the extractor; keep the useExtracted pattern already
imported and referenced in this file (page.tsx / feature-flags render logic) and
add appropriate message keys to the translation catalog.

In `@server/src/analytics-script/config.ts`:
- Around line 16-29: getOrCreateVisitorId currently returns a new
createVisitorId() whenever localStorage access throws, causing visitor IDs to
change on every page load; modify getOrCreateVisitorId to, on localStorage
failure, attempt to read/write the same key from sessionStorage and only if
sessionStorage is unavailable fall back to an in-memory (module-scoped) cached
id so the id is stable for the page session; update references to
localStorage.getItem/setItem to use this fallback sequence and ensure
createVisitorId() is only called once per session when no persisted value
exists.

In `@server/src/analytics-script/index.ts`:
- Line 223: The onReady invoker currently calls the consumer callback directly
(onReady: (callback: (api: RybbitAPI) => void) =>
callback(window[config.namespace])) which lets any exception in user code
propagate and break the tracker; change onReady to call the callback inside a
try/catch (and optionally schedule it asynchronously via setTimeout or
Promise.resolve().then(...)) so exceptions are caught and logged (e.g.,
console.error) and do not bubble up, using the same RybbitAPI instance from
window[config.namespace].

In `@server/src/analytics-script/tracking.ts`:
- Around line 70-93: refreshFeatureFlags can race when multiple calls overlap
and later responses overwrite config.featureFlags; modify refreshFeatureFlags
(and any caller state like identify()/clearUserId()) to track a unique request
token or use an AbortController so only the latest request updates
config.featureFlags: generate a monotonic requestId before fetch (or create an
AbortController and store it on the instance), attach it to the pending request,
and when the response arrives verify the requestId matches the current token (or
that the controller wasn't aborted) before assigning this.config.featureFlags;
ensure aborted/stale responses are ignored and any prior controller is aborted
when starting a new fetch.

In `@server/src/api/featureFlags/index.ts`:
- Around line 197-213: The current flow reads version via
db.query.featureFlags.findFirst into updateData.version, increments it in
memory, then writes—this is racy; instead perform an atomic DB update that
increments the stored version and returns the updated row (or use an optimistic
concurrency check): replace the two-step read+increment+update with a single
update against featureFlags that either (a) sets version = version + 1 in the DB
and returns the row (using the same .update(featureFlags).set(...).returning()
call), or (b) uses the current version in the WHERE clause
(eq(featureFlags.version, updateData.version)) so the update only succeeds if no
concurrent change happened and you can detect a 0-rows-updated and return a 409;
update code paths that reference updateData.version,
db.query.featureFlags.findFirst, and db.update(featureFlags).set(...)
accordingly so version is bumped atomically and concurrent updates are
detected/handled.

In `@server/src/index.ts`:
- Line 290: The POST route registering evaluateFeatureFlags currently bypasses
site access checks; update the registration to use the same site-scoped
middleware/grouping pattern used elsewhere in src/index.ts by wrapping or
registering the route inside the existing site plugin that runs resolveSiteId
and access checks (the same middleware that protects other site routes), so that
evaluateFeatureFlags is called only after resolveSiteId and the site access
verifier have run; locate the route registering evaluateFeatureFlags and move it
into the site plugin/group block or add the resolveSiteId + access-check
middleware to that route registration to enforce site-scoped access.

In `@server/src/services/featureFlags/evaluator.ts`:
- Around line 89-96: The regex branch in
server/src/services/featureFlags/evaluator.ts currently compiles untrusted
patterns per-request using new RegExp(value).test(actual) (inside the case
"regex" / expected.some callback), which is vulnerable to ReDoS; change the flow
to stop compiling at request time by prevalidating and precompiling patterns
when rules are created/loaded (reject patterns that safe-regex flags or compile
into a safe engine), cache those compiled patterns (or use a safe engine like
the re2 package to construct RE2 instances once) and then here (case "regex")
use the cached compiled object(s) to run .test(actual) instead of new
RegExp(value) — also ensure rule creation logic validates patterns with
safe-regex or rejects/limits dangerous patterns so runtime cannot receive
catastrophic regexes.

---

Outside diff comments:
In `@server/drizzle/0006_first_logan.sql`:
- Around line 1-25: The PR adds a manual SQL migration creating the
"feature_flags" table and its constraints (e.g., CONSTRAINT
"feature_flags_site_key_unique", "feature_flags_rollout_check",
"feature_flags_type_check", FK "feature_flags_site_id_sites_site_id_fk") which
conflicts with the repo’s Drizzle push workflow; instead remove this manual SQL
artifact and apply the schema change through the Drizzle workflow by updating
your Drizzle schema (model for feature_flags) and running npm run db:push so the
table, indexes (feature_flags_site_idx) and constraints are generated and
tracked by Drizzle rather than committing raw SQL migration files.

In `@server/src/analytics-script/config.test.ts`:
- Around line 115-164: The test for parseScriptConfig does not mock the second
fetch call to fetchFeatureFlags, causing unexpected behavior; update the "should
use defaults when API call fails" and the analogous "network error" test to add
a second global.fetch mockResolvedValueOnce that returns a successful
feature-flags response (e.g., { ok: true, json: async () => ({}) }) after the
first failing/throwing response so fetchFeatureFlags receives a valid response;
reference parseScriptConfig and fetchFeatureFlags to locate where the dual fetch
calls occur and ensure both tests add the extra mockResolvedValueOnce for the
feature flags call.

---

Nitpick comments:
In `@server/src/analytics-script/config.ts`:
- Around line 47-84: fetchFeatureFlags currently can hang waiting for fetch; use
an AbortController with a short timeout (e.g., 2500ms) to cancel the request and
avoid blocking script init: create an AbortController, pass controller.signal to
fetch, start a timer that calls controller.abort() after ~2500ms, and clear the
timer in finally; ensure the existing catch (which returns {}) still handles the
aborted request gracefully. Reference the fetchFeatureFlags function, the fetch
call, and the catch/finally block when making these changes.

In `@server/src/analytics-script/index.ts`:
- Around line 54-60: The tuple type QueueEntry and queueMethod currently use
any[] which bypasses strict typing; change the second element from any[] to
unknown[] (or a narrower union of allowed argument types) and update usages that
read earlyQueue/QueueEntry to properly narrow or validate those unknowns (e.g.,
where entries are replayed/consumed, add type guards or casts). Specifically
update the type alias QueueEntry, the earlyQueue declaration, and the
queueMethod signature so it returns (...args: unknown[]) => void, and fix
downstream consumers to handle/validate unknown[] instead of assuming any.

In `@server/src/analytics-script/tracking.ts`:
- Line 22: The exposedFeatureFlags Set on the Tracking class
(exposedFeatureFlags) is unbounded and can grow indefinitely; modify the code
that manages exposedFeatureFlags to prevent unbounded growth by either (a)
clearing it on navigation/page-change events (listen for popstate/history
changes or framework router events and call exposedFeatureFlags.clear()), or (b)
enforce a fixed capacity (implement an eviction strategy such as simple
FIFO/LRU: when adding in the method that constructs the "key:version:value"
string, if exposedFeatureFlags.size >= MAX_FLAGS then remove the oldest entry
before adding the new one). Update the add/check logic where exposedFeatureFlags
is used so it uses the new clear-on-navigation or capped-set behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4a19c00b-26bc-4053-9c39-ab763959035a

📥 Commits

Reviewing files that changed from the base of the PR and between e1facee and cfc2c26.

📒 Files selected for processing (44)
  • client/messages/cs.json
  • client/messages/de.json
  • client/messages/en.json
  • client/messages/es.json
  • client/messages/fr.json
  • client/messages/it.json
  • client/messages/ja.json
  • client/messages/ko.json
  • client/messages/pl.json
  • client/messages/pt.json
  • client/messages/uk.json
  • client/messages/zh.json
  • client/src/api/analytics/endpoints/featureFlags.ts
  • client/src/api/analytics/endpoints/index.ts
  • client/src/api/analytics/hooks/featureFlags/useFeatureFlags.ts
  • client/src/app/[site]/components/Sidebar/Sidebar.tsx
  • client/src/app/[site]/feature-flags/page.tsx
  • client/src/lib/siteRoute.ts
  • server/drizzle/0006_first_logan.sql
  • server/drizzle/meta/0006_snapshot.json
  • server/drizzle/meta/_journal.json
  • server/public/script-full.js
  • server/public/script.js
  • server/src/analytics-script/config.test.ts
  • server/src/analytics-script/config.ts
  • server/src/analytics-script/index.ts
  • server/src/analytics-script/tracking.test.ts
  • server/src/analytics-script/tracking.ts
  • server/src/analytics-script/types.ts
  • server/src/api/analytics/utils/getFilterStatement.test.ts
  • server/src/api/analytics/utils/getFilterStatement.ts
  • server/src/api/analytics/utils/query-validation.ts
  • server/src/api/featureFlags/index.ts
  • server/src/api/featureFlags/schemas.ts
  • server/src/db/clickhouse/clickhouse.ts
  • server/src/db/postgres/schema.ts
  • server/src/index.ts
  • server/src/services/featureFlags/evaluator.test.ts
  • server/src/services/featureFlags/evaluator.ts
  • server/src/services/tracker/pageviewQueue.ts
  • server/src/services/tracker/trackEvent.ts
  • server/src/services/tracker/types.ts
  • server/src/services/tracker/utils.ts
  • shared/src/filters.ts

Comment thread client/messages/zh.json Outdated
Comment on lines +22 to +55
export function useCreateFeatureFlag() {
const queryClient = useQueryClient();
const { site } = useStore();

return useMutation({
mutationFn: (payload: FeatureFlagPayload) => createFeatureFlag(site, payload),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["feature-flags", site] });
},
});
}

export function useUpdateFeatureFlag() {
const queryClient = useQueryClient();
const { site } = useStore();

return useMutation({
mutationFn: ({ flagId, payload }: { flagId: number; payload: FeatureFlagUpdatePayload }) =>
updateFeatureFlag(site, flagId, payload),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["feature-flags", site] });
},
});
}

export function useDeleteFeatureFlag() {
const queryClient = useQueryClient();
const { site } = useStore();

return useMutation({
mutationFn: (flagId: number) => deleteFeatureFlag(site, flagId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["feature-flags", site] });
},
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard mutation calls when site context is missing.

Line [27], Line [39], and Line [52] call endpoints with site unguarded. If site is null/undefined during store hydration or route transitions, these hit invalid URLs (/sites/undefined/...).

Suggested fix
 export function useCreateFeatureFlag() {
   const queryClient = useQueryClient();
   const { site } = useStore();

   return useMutation({
-    mutationFn: (payload: FeatureFlagPayload) => createFeatureFlag(site, payload),
+    mutationFn: (payload: FeatureFlagPayload) => {
+      if (site == null) throw new Error("Site is not selected");
+      return createFeatureFlag(site, payload);
+    },
     onSuccess: () => {
       queryClient.invalidateQueries({ queryKey: ["feature-flags", site] });
     },
   });
 }
@@
   return useMutation({
-    mutationFn: ({ flagId, payload }: { flagId: number; payload: FeatureFlagUpdatePayload }) =>
-      updateFeatureFlag(site, flagId, payload),
+    mutationFn: ({ flagId, payload }: { flagId: number; payload: FeatureFlagUpdatePayload }) => {
+      if (site == null) throw new Error("Site is not selected");
+      return updateFeatureFlag(site, flagId, payload);
+    },
     onSuccess: () => {
       queryClient.invalidateQueries({ queryKey: ["feature-flags", site] });
     },
   });
 }
@@
   return useMutation({
-    mutationFn: (flagId: number) => deleteFeatureFlag(site, flagId),
+    mutationFn: (flagId: number) => {
+      if (site == null) throw new Error("Site is not selected");
+      return deleteFeatureFlag(site, flagId);
+    },
     onSuccess: () => {
       queryClient.invalidateQueries({ queryKey: ["feature-flags", site] });
     },
   });
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function useCreateFeatureFlag() {
const queryClient = useQueryClient();
const { site } = useStore();
return useMutation({
mutationFn: (payload: FeatureFlagPayload) => createFeatureFlag(site, payload),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["feature-flags", site] });
},
});
}
export function useUpdateFeatureFlag() {
const queryClient = useQueryClient();
const { site } = useStore();
return useMutation({
mutationFn: ({ flagId, payload }: { flagId: number; payload: FeatureFlagUpdatePayload }) =>
updateFeatureFlag(site, flagId, payload),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["feature-flags", site] });
},
});
}
export function useDeleteFeatureFlag() {
const queryClient = useQueryClient();
const { site } = useStore();
return useMutation({
mutationFn: (flagId: number) => deleteFeatureFlag(site, flagId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["feature-flags", site] });
},
export function useCreateFeatureFlag() {
const queryClient = useQueryClient();
const { site } = useStore();
return useMutation({
mutationFn: (payload: FeatureFlagPayload) => {
if (site == null) throw new Error("Site is not selected");
return createFeatureFlag(site, payload);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["feature-flags", site] });
},
});
}
export function useUpdateFeatureFlag() {
const queryClient = useQueryClient();
const { site } = useStore();
return useMutation({
mutationFn: ({ flagId, payload }: { flagId: number; payload: FeatureFlagUpdatePayload }) => {
if (site == null) throw new Error("Site is not selected");
return updateFeatureFlag(site, flagId, payload);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["feature-flags", site] });
},
});
}
export function useDeleteFeatureFlag() {
const queryClient = useQueryClient();
const { site } = useStore();
return useMutation({
mutationFn: (flagId: number) => {
if (site == null) throw new Error("Site is not selected");
return deleteFeatureFlag(site, flagId);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["feature-flags", site] });
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/api/analytics/hooks/featureFlags/useFeatureFlags.ts` around lines
22 - 55, The mutation hooks call
createFeatureFlag/updateFeatureFlag/deleteFeatureFlag with an unguarded site
from useStore, which can be undefined during hydration; update
useCreateFeatureFlag, useUpdateFeatureFlag and useDeleteFeatureFlag to validate
site before calling the API by checking if (!site) and returning a rejected
Promise or throwing an Error inside the mutationFn (e.g., in
useUpdateFeatureFlag's mutationFn before calling updateFeatureFlag(site,...)),
so the mutation fails fast with a clear error instead of hitting
/sites/undefined/*; keep the existing onSuccess invalidation logic unchanged.

Comment thread client/src/app/[site]/feature-flags/page.tsx Outdated
Comment on lines +740 to +745
<div key={`${stat.flag_value}-${stat.sessions}`} className="flex items-center gap-2 text-xs">
<span className="max-w-24 truncate font-mono text-neutral-700 dark:text-neutral-200">
{stat.flag_value || "(empty)"}
</span>
<span className="text-neutral-500 dark:text-neutral-400">{stat.sessions.toLocaleString()} sessions</span>
</div>
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize the hardcoded “sessions” label.

Line [744] renders sessions as a raw English string, so this cell won't translate for non-English locales.

As per coding guidelines: client/src/**/*.{ts,tsx}: "Use next-intl's 'useTranslations()' hook for i18n; maintain translation files in messages/ directory."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/app/`[site]/feature-flags/page.tsx around lines 740 - 745, The
"sessions" label is hardcoded in the span that renders
{stat.sessions.toLocaleString()} sessions; replace the literal with a localized
string using next-intl's useTranslations() (e.g., call const t =
useTranslations('FeatureFlags') at the top of the component in
client/src/app/[site]/feature-flags/page.tsx and render t('sessions') instead of
the hardcoded word), update the messages/ FeatureFlags translation JSON files to
include the "sessions" key (and plural forms if needed), and ensure the span
concatenates/localizes properly (e.g., use t('sessions', { count: stat.sessions
}) or format as required) so the label is translatable.

Comment on lines +16 to +29
function getOrCreateVisitorId(namespace: string): string {
const key = `${namespace}-visitor-id`;

try {
const stored = localStorage.getItem(key);
if (stored) return stored;

const visitorId = createVisitorId();
localStorage.setItem(key, visitorId);
return visitorId;
} catch (e) {
return createVisitorId();
}
}
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Visitors without localStorage get a new ID on every page load.

When localStorage is unavailable (line 27), a fresh visitor ID is created each time. This breaks feature flag consistency for those users—they may see different flag assignments on every navigation. Consider falling back to a session-level ID stored in sessionStorage or in-memory, so at minimum the ID remains stable for the page session.

Proposed fix
+let inMemoryVisitorId: string | null = null;
+
 function getOrCreateVisitorId(namespace: string): string {
   const key = `${namespace}-visitor-id`;
 
   try {
     const stored = localStorage.getItem(key);
     if (stored) return stored;
 
     const visitorId = createVisitorId();
     localStorage.setItem(key, visitorId);
     return visitorId;
   } catch (e) {
-    return createVisitorId();
+    // localStorage unavailable - use in-memory fallback for session stability
+    if (!inMemoryVisitorId) {
+      inMemoryVisitorId = createVisitorId();
+    }
+    return inMemoryVisitorId;
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function getOrCreateVisitorId(namespace: string): string {
const key = `${namespace}-visitor-id`;
try {
const stored = localStorage.getItem(key);
if (stored) return stored;
const visitorId = createVisitorId();
localStorage.setItem(key, visitorId);
return visitorId;
} catch (e) {
return createVisitorId();
}
}
let inMemoryVisitorId: string | null = null;
function getOrCreateVisitorId(namespace: string): string {
const key = `${namespace}-visitor-id`;
try {
const stored = localStorage.getItem(key);
if (stored) return stored;
const visitorId = createVisitorId();
localStorage.setItem(key, visitorId);
return visitorId;
} catch (e) {
// localStorage unavailable - use in-memory fallback for session stability
if (!inMemoryVisitorId) {
inMemoryVisitorId = createVisitorId();
}
return inMemoryVisitorId;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/analytics-script/config.ts` around lines 16 - 29,
getOrCreateVisitorId currently returns a new createVisitorId() whenever
localStorage access throws, causing visitor IDs to change on every page load;
modify getOrCreateVisitorId to, on localStorage failure, attempt to read/write
the same key from sessionStorage and only if sessionStorage is unavailable fall
back to an in-memory (module-scoped) cached id so the id is stable for the page
session; update references to localStorage.getItem/setItem to use this fallback
sequence and ensure createVisitorId() is only called once per session when no
persisted value exists.

flagPayload: <T = unknown>(key: string, fallback?: T) => tracker.getFeatureFlagPayload<T>(key, fallback),
flags: () => tracker.getFeatureFlags(),
flagPayloads: () => tracker.getFeatureFlagPayloads(),
onReady: (callback: (api: RybbitAPI) => void) => callback(window[config.namespace]),
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard onReady callback execution to avoid consumer exceptions breaking the tracker.

onReady executes arbitrary user callback code directly; an exception there can break downstream script flow. Wrap it to isolate failures.

Suggested fix
-    onReady: (callback: (api: RybbitAPI) => void) => callback(window[config.namespace]),
+    onReady: (callback: (api: RybbitAPI) => void) => {
+      try {
+        callback(window[config.namespace]);
+      } catch (error) {
+        console.error("onReady callback failed:", error);
+      }
+    },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onReady: (callback: (api: RybbitAPI) => void) => callback(window[config.namespace]),
onReady: (callback: (api: RybbitAPI) => void) => {
try {
callback(window[config.namespace]);
} catch (error) {
console.error("onReady callback failed:", error);
}
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/analytics-script/index.ts` at line 223, The onReady invoker
currently calls the consumer callback directly (onReady: (callback: (api:
RybbitAPI) => void) => callback(window[config.namespace])) which lets any
exception in user code propagate and break the tracker; change onReady to call
the callback inside a try/catch (and optionally schedule it asynchronously via
setTimeout or Promise.resolve().then(...)) so exceptions are caught and logged
(e.g., console.error) and do not bubble up, using the same RybbitAPI instance
from window[config.namespace].

Comment on lines +70 to +93
private async refreshFeatureFlags(): Promise<void> {
try {
const response = await fetch(`${this.config.analyticsHost}/site/${this.config.siteId}/feature-flags/evaluate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
anonymousId: this.config.visitorId,
identifiedUserId: this.customUserId || undefined,
...this.getCurrentUrlContext(),
}),
mode: "cors",
credentials: "omit",
keepalive: true,
});

if (!response.ok) return;
const data = await response.json();
this.config.featureFlags = data?.flags && typeof data.flags === "object" ? data.flags : {};
} catch (e) {
// Feature flag refresh is best-effort and should never affect analytics collection.
}
}
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Concurrent refreshFeatureFlags calls can cause a race condition.

If refreshFeatureFlags is called rapidly (e.g., identify() then clearUserId() in quick succession), responses may arrive out of order and overwrite config.featureFlags incorrectly. Consider using a request ID or aborting stale requests:

Proposed fix using request ID
+  private latestFlagRequestId = 0;
+
   private async refreshFeatureFlags(): Promise<void> {
+    const requestId = ++this.latestFlagRequestId;
     try {
       const response = await fetch(`${this.config.analyticsHost}/site/${this.config.siteId}/feature-flags/evaluate`, {
         // ...
       });
 
       if (!response.ok) return;
+      if (requestId !== this.latestFlagRequestId) return; // Stale response
       const data = await response.json();
       this.config.featureFlags = data?.flags && typeof data.flags === "object" ? data.flags : {};
     } catch (e) {
       // Feature flag refresh is best-effort and should never affect analytics collection.
     }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/analytics-script/tracking.ts` around lines 70 - 93,
refreshFeatureFlags can race when multiple calls overlap and later responses
overwrite config.featureFlags; modify refreshFeatureFlags (and any caller state
like identify()/clearUserId()) to track a unique request token or use an
AbortController so only the latest request updates config.featureFlags: generate
a monotonic requestId before fetch (or create an AbortController and store it on
the instance), attach it to the pending request, and when the response arrives
verify the requestId matches the current token (or that the controller wasn't
aborted) before assigning this.config.featureFlags; ensure aborted/stale
responses are ignored and any prior controller is aborted when starting a new
fetch.

Comment thread server/src/api/featureFlags/index.ts
Comment thread server/src/index.ts
Comment thread server/src/services/featureFlags/evaluator.ts
…time types

This commit adds support for condition sets in feature flags, allowing for more granular control over feature rollout. It introduces a new runtime type to specify whether a feature flag is applicable on the client, server, or both. Additionally, the database schema is updated to accommodate these changes, ensuring that the feature flag system is more flexible and robust.
…d filter management

This commit updates the language files for Czech, German, English, Spanish, French, Italian, and Japanese, adding new keys and translations related to API endpoints, filters, and error messages. The changes improve the user experience by providing clearer instructions and feedback in various languages, ensuring consistency across the application.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/src/api/featureFlags/schemas.ts`:
- Around line 163-166: The current check using hasConditionSetVariants lets a
flag pass when only some conditionSets define their own variants; change the
validation so that if any conditionSet defines variants (compute e.g.
hasConditionSetWithVariants = conditionSets.some(cs => (cs.variants?.length ??
0) > 0)), then require the top-level variants array to have at least two
entries; update the conditional that calls ctx.addIssue to use this new
predicate (instead of the old hasConditionSetVariants) and ensure the error
message reflects that top-level variants must be multivariate when any condition
set relies on the fallback.
- Around line 240-246: The validator currently only rejects
conditionSet.rolloutPercentage for multivariate sets when setVariants.length >
0; change the check so any conditionSet.rolloutPercentage !== undefined triggers
ctx.addIssue when flagType === "multivariate" (remove the setVariants.length > 0
requirement). Update the if guard around the ctx.addIssue call that references
conditionSet.rolloutPercentage, flagType, setVariants to only test
conditionSet.rolloutPercentage !== undefined && flagType === "multivariate",
preserving the same ctx.addIssue call (with message "Multivariate condition sets
use variant rollout percentages" and path [...path, "rolloutPercentage"]).

In `@server/src/services/featureFlags/evaluator.ts`:
- Around line 139-145: The selection loop over variants can miss a match when
bucket === 100; update the condition inside the for (const variant of variants)
loop (which uses cumulativeRollout, clampPercentage, variant.rolloutPercentage
and bucket) so that a variant is returned when bucket < cumulativeRollout OR
when bucket === 100 && cumulativeRollout === 100 (i.e., treat the 100 boundary
as included), ensuring totalRollout still uses Math.min(100, cumulativeRollout).
- Around line 112-129: getConditionSets currently fabricates a default condition
set for flags that don't have persisted conditionSets, causing downstream
consumers to see a synthetic conditionSet and getConditionSetName to return
"condition_1"; change getConditionSets to only return flag.conditionSets when
Array.isArray(flag.conditionSets) && flag.conditionSets.length > 0 and otherwise
return an empty array (do not construct a synthetic set), and ensure
getConditionSetName is only relied on when a real condition set exists (i.e.
callers should not call getConditionSetName for empty results); update any
callers that currently assume a returned set exists to handle an empty array
instead.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f9d9331f-c76b-4fd1-91fa-0b7f38b013a1

📥 Commits

Reviewing files that changed from the base of the PR and between cfc2c26 and 0e73c1f.

📒 Files selected for processing (24)
  • client/messages/cs.json
  • client/messages/de.json
  • client/messages/en.json
  • client/messages/es.json
  • client/messages/fr.json
  • client/messages/it.json
  • client/messages/ja.json
  • client/messages/ko.json
  • client/messages/pl.json
  • client/messages/pt.json
  • client/messages/uk.json
  • client/messages/zh.json
  • client/src/api/analytics/endpoints/featureFlags.ts
  • client/src/api/analytics/endpoints/index.ts
  • client/src/app/[site]/feature-flags/page.tsx
  • server/drizzle/0006_first_logan.sql
  • server/drizzle/meta/0006_snapshot.json
  • server/src/analytics-script/types.ts
  • server/src/api/featureFlags/index.ts
  • server/src/api/featureFlags/schemas.ts
  • server/src/db/postgres/schema.ts
  • server/src/index.ts
  • server/src/services/featureFlags/evaluator.test.ts
  • server/src/services/featureFlags/evaluator.ts
✅ Files skipped from review due to trivial changes (3)
  • client/src/api/analytics/endpoints/index.ts
  • server/drizzle/meta/0006_snapshot.json
  • client/messages/zh.json
🚧 Files skipped from review as they are similar to previous changes (6)
  • server/drizzle/0006_first_logan.sql
  • server/src/index.ts
  • client/src/api/analytics/endpoints/featureFlags.ts
  • server/src/api/featureFlags/index.ts
  • client/src/app/[site]/feature-flags/page.tsx
  • server/src/analytics-script/types.ts

Comment on lines +163 to +166
const hasConditionSetVariants = conditionSets.some(conditionSet => (conditionSet.variants?.length ?? 0) > 0);

if (!hasConditionSetVariants && variants.length < 2) {
ctx.addIssue({
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate fallback top-level variants when some condition sets omit their own variants.

hasConditionSetVariants becomes true as soon as one condition set defines variants, but the evaluator still falls back to the flag-level variants for matching sets that don't. That lets a multivariate flag pass validation with a single top-level variant and then behave like a one-arm experiment for those audiences.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/api/featureFlags/schemas.ts` around lines 163 - 166, The current
check using hasConditionSetVariants lets a flag pass when only some
conditionSets define their own variants; change the validation so that if any
conditionSet defines variants (compute e.g. hasConditionSetWithVariants =
conditionSets.some(cs => (cs.variants?.length ?? 0) > 0)), then require the
top-level variants array to have at least two entries; update the conditional
that calls ctx.addIssue to use this new predicate (instead of the old
hasConditionSetVariants) and ensure the error message reflects that top-level
variants must be multivariate when any condition set relies on the fallback.

Comment on lines +240 to +246
if (conditionSet.rolloutPercentage !== undefined && flagType === "multivariate" && setVariants.length > 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Multivariate condition sets use variant rollout percentages",
path: [...path, "rolloutPercentage"],
});
}
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject rolloutPercentage on all multivariate condition sets.

The evaluator never reads conditionSet.rolloutPercentage in the multivariate path, including when a set inherits top-level variants. Right now configs like { rolloutPercentage: 10 } are accepted here but ignored at runtime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/api/featureFlags/schemas.ts` around lines 240 - 246, The validator
currently only rejects conditionSet.rolloutPercentage for multivariate sets when
setVariants.length > 0; change the check so any conditionSet.rolloutPercentage
!== undefined triggers ctx.addIssue when flagType === "multivariate" (remove the
setVariants.length > 0 requirement). Update the if guard around the ctx.addIssue
call that references conditionSet.rolloutPercentage, flagType, setVariants to
only test conditionSet.rolloutPercentage !== undefined && flagType ===
"multivariate", preserving the same ctx.addIssue call (with message
"Multivariate condition sets use variant rollout percentages" and path [...path,
"rolloutPercentage"]).

Comment on lines +112 to +129
function getConditionSets(flag: FeatureFlagRow): FeatureFlagConditionSet[] {
if (Array.isArray(flag.conditionSets) && flag.conditionSets.length > 0) {
return flag.conditionSets;
}

return [
{
rules: Array.isArray(flag.rules) ? flag.rules : [],
rolloutPercentage: flag.rolloutPercentage,
variants: Array.isArray(flag.variants) ? flag.variants : [],
payload: flag.payload,
},
];
}

function getConditionSetName(conditionSet: FeatureFlagConditionSet, index: number) {
return conditionSet.name || `condition_${index + 1}`;
}
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't expose a synthetic conditionSet for flags without configured sets.

getConditionSets() fabricates a default set and getConditionSetName() turns it into "condition_1". That means plain flags now report a conditionSet value even though no real set exists, which can leak fake segmentation into downstream consumers or exposure analytics. Only populate conditionSet when the flag actually has persisted condition sets.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/services/featureFlags/evaluator.ts` around lines 112 - 129,
getConditionSets currently fabricates a default condition set for flags that
don't have persisted conditionSets, causing downstream consumers to see a
synthetic conditionSet and getConditionSetName to return "condition_1"; change
getConditionSets to only return flag.conditionSets when
Array.isArray(flag.conditionSets) && flag.conditionSets.length > 0 and otherwise
return an empty array (do not construct a synthetic set), and ensure
getConditionSetName is only relied on when a real condition set exists (i.e.
callers should not call getConditionSetName for empty results); update any
callers that currently assume a returned set exists to handle an empty array
instead.

Comment on lines +139 to +145
for (const variant of variants) {
cumulativeRollout += clampPercentage(variant.rolloutPercentage, 0);
if (bucket < cumulativeRollout) {
return {
variant,
totalRollout: Math.min(100, cumulativeRollout),
};
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle the bucket === 100 boundary in variant selection.

bucketPercentage() can return exactly 100, but this branch only matches bucket < cumulativeRollout. On that boundary, a variant allocation totaling 100% still falls through instead of assigning a variant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/services/featureFlags/evaluator.ts` around lines 139 - 145, The
selection loop over variants can miss a match when bucket === 100; update the
condition inside the for (const variant of variants) loop (which uses
cumulativeRollout, clampPercentage, variant.rolloutPercentage and bucket) so
that a variant is returned when bucket < cumulativeRollout OR when bucket ===
100 && cumulativeRollout === 100 (i.e., treat the 100 boundary as included),
ensuring totalRollout still uses Math.min(100, cumulativeRollout).

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
client/src/app/[site]/feature-flags/page.tsx (1)

16-17: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Switch this page to next-intl useTranslations() for localized user-facing text (incl. the title).

client/src/app/[site]/feature-flags/page.tsx lines 16-17 currently use useExtracted() and hardcode "Feature Flags"; for production UI i18n, replace useExtracted() with useTranslations() and get the page title from a message key (instead of an English literal).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/app/`[site]/feature-flags/page.tsx around lines 16 - 17, Replace
the non-i18n hook useExtracted() with next-intl's useTranslations() and fetch
the page title from the locale messages instead of the hardcoded string;
specifically, import and call useTranslations (e.g., const t =
useTranslations('FeatureFlags') or appropriate namespace), then pass t('title')
(or the agreed message key, e.g., 'featureFlags.title') into useSetPageTitle
instead of "Feature Flags"; also update any other user-facing literals in this
file to use t(...) so the page is fully localized.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@client/src/app/`[site]/feature-flags/components/ConditionSetsEditor.tsx:
- Around line 11-27: The component imports and calls useExtracted (in
ConditionSetsEditor) which is incorrect for next-intl; replace the import and
hook call with useTranslations from next-intl and ensure the variable usage
stays correct (e.g., const t = useTranslations(); then keep using t("...") for
conditionSetLabel), updating the import line that currently references
useExtracted and any types if necessary so translation strings resolve at build
time.

In `@client/src/app/`[site]/feature-flags/components/FeatureFlagTable.tsx:
- Line 136: The container div in FeatureFlagTable.tsx uses light-first Tailwind
classes (border-neutral-100 bg-white) with dark: overrides; change to dark-first
by making the default classes the dark-theme ones (e.g., border-neutral-850
bg-neutral-900) and add light: variants for the light theme (e.g.,
light:border-neutral-100 light:bg-white) so the className on the div reflects
dark-first styling.

In `@client/src/app/`[site]/feature-flags/lib/form.ts:
- Around line 77-247: The code currently performs manual runtime validation
inside parseOptionalPayload, parseRequiredPayload, parseRuleValue and
buildPayload (buildRules, buildVariants, validateVariants) — move these checks
into Zod schemas and call schema.parse at submit time instead of manual
branching: create Zod schemas for FeatureFlagPayload, FeatureFlagRule,
VariantFormState, ConditionSetFormState and FlagFormState that capture required
fields, types, array element constraints, rollout bounds, and unique-keys
constraint (use refinements for uniqueness and sum checks), replace
parseRuleValue/parseOptionalPayload/parseRequiredPayload logic with schema
coercions/parsers, and update buildPayload to simply transform the
already-validated parsed object into the API shape (or call schema.parse within
buildPayload once) rather than performing manual validation inside
buildRules/buildVariants/validateVariants.
- Around line 249-251: The buildPayload function currently returns key:
form.key.trim() even when that trimmed string is empty; modify buildPayload to
trim the key first, validate that the trimmed value is not an empty string, and
fail-fast (e.g., throw a clear Error or return a rejected result) if it is empty
so the dialog flow can surface the validation immediately; update the code
around the existing key: form.key.trim() return (reference buildPayload and
form.key) to perform this check before constructing and returning the payload.

---

Outside diff comments:
In `@client/src/app/`[site]/feature-flags/page.tsx:
- Around line 16-17: Replace the non-i18n hook useExtracted() with next-intl's
useTranslations() and fetch the page title from the locale messages instead of
the hardcoded string; specifically, import and call useTranslations (e.g., const
t = useTranslations('FeatureFlags') or appropriate namespace), then pass
t('title') (or the agreed message key, e.g., 'featureFlags.title') into
useSetPageTitle instead of "Feature Flags"; also update any other user-facing
literals in this file to use t(...) so the page is fully localized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1b37fa16-7d49-4a8b-b771-79834716653c

📥 Commits

Reviewing files that changed from the base of the PR and between 0e73c1f and 0a8a8fe.

📒 Files selected for processing (23)
  • client/messages/cs.json
  • client/messages/de.json
  • client/messages/en.json
  • client/messages/es.json
  • client/messages/fr.json
  • client/messages/it.json
  • client/messages/ja.json
  • client/messages/ko.json
  • client/messages/pl.json
  • client/messages/pt.json
  • client/messages/uk.json
  • client/messages/zh.json
  • client/src/app/[site]/feature-flags/components/ConditionSetsEditor.tsx
  • client/src/app/[site]/feature-flags/components/FeatureFlagDialog.tsx
  • client/src/app/[site]/feature-flags/components/FeatureFlagSkeleton.tsx
  • client/src/app/[site]/feature-flags/components/FeatureFlagTable.tsx
  • client/src/app/[site]/feature-flags/components/TargetingRulesEditor.tsx
  • client/src/app/[site]/feature-flags/components/VariantsEditor.tsx
  • client/src/app/[site]/feature-flags/lib/constants.ts
  • client/src/app/[site]/feature-flags/lib/form.ts
  • client/src/app/[site]/feature-flags/lib/labels.ts
  • client/src/app/[site]/feature-flags/lib/types.ts
  • client/src/app/[site]/feature-flags/page.tsx
💤 Files with no reviewable changes (11)
  • client/messages/pt.json
  • client/messages/ko.json
  • client/messages/de.json
  • client/messages/es.json
  • client/messages/pl.json
  • client/messages/en.json
  • client/messages/it.json
  • client/messages/uk.json
  • client/messages/ja.json
  • client/messages/fr.json
  • client/messages/cs.json
✅ Files skipped from review due to trivial changes (3)
  • client/src/app/[site]/feature-flags/components/FeatureFlagSkeleton.tsx
  • client/src/app/[site]/feature-flags/lib/labels.ts
  • client/messages/zh.json

Comment on lines +11 to +27
import { useExtracted } from "next-intl";
import { createEmptyConditionSet } from "../lib/form";
import type { ConditionSetFormState } from "../lib/types";
import { TargetingRulesEditor } from "./TargetingRulesEditor";
import { VariantsEditor } from "./VariantsEditor";

export function ConditionSetsEditor({
flagType,
conditionSets,
onChange,
}: {
flagType: FeatureFlagType;
conditionSets: ConditionSetFormState[];
onChange: (conditionSets: ConditionSetFormState[]) => void;
}) {
const t = useExtracted();
const conditionSetLabel = flagType === "remote_config" ? t("Targeted configs") : t("Release conditions");
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace useExtracted with useTranslations for i18n hooks.

next-intl usage at Line 11/Line 26 appears incorrect and likely breaks typing/build; in this path we also require useTranslations().

In next-intl v4.8.3, does a hook named `useExtracted` exist? What is the correct hook for translating UI strings in client components?

As per coding guidelines, "Use next-intl's 'useTranslations()' hook for i18n; maintain translation files in messages/ directory (en.json, de.json, etc.)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/app/`[site]/feature-flags/components/ConditionSetsEditor.tsx
around lines 11 - 27, The component imports and calls useExtracted (in
ConditionSetsEditor) which is incorrect for next-intl; replace the import and
hook call with useTranslations from next-intl and ensure the variable usage
stays correct (e.g., const t = useTranslations(); then keep using t("...") for
conditionSetLabel), updating the import line that currently references
useExtracted and any types if necessary so translation strings resolve at build
time.

};

return (
<div className="rounded-lg border border-neutral-100 bg-white dark:border-neutral-850 dark:bg-neutral-900">
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use dark-first classes for default theme.

Line 136 sets a light default (bg-white) and only overrides in dark mode; this should be inverted so dark is default.

As per coding guidelines, "Dark mode is default theme".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/app/`[site]/feature-flags/components/FeatureFlagTable.tsx at line
136, The container div in FeatureFlagTable.tsx uses light-first Tailwind classes
(border-neutral-100 bg-white) with dark: overrides; change to dark-first by
making the default classes the dark-theme ones (e.g., border-neutral-850
bg-neutral-900) and add light: variants for the light theme (e.g.,
light:border-neutral-100 light:bg-white) so the className on the div reflects
dark-first styling.

Comment on lines +77 to +247
function parseOptionalPayload(value: string): FeatureFlagPayloadValue | undefined {
const trimmed = value.trim();
if (!trimmed) return undefined;
return JSON.parse(trimmed) as FeatureFlagPayloadValue;
}

function parseRequiredPayload(value: string): FeatureFlagPayloadValue {
const parsed = parseOptionalPayload(value);
if (parsed === undefined) {
throw new Error("Payload is required");
}
return parsed;
}

function formatRuleValue(value: FeatureFlagRule["value"]) {
if (Array.isArray(value)) return JSON.stringify(value);
return String(value);
}

function parseRuleValue(value: string): FeatureFlagRule["value"] {
const trimmed = value.trim();
if (trimmed.startsWith("[")) {
const parsed = JSON.parse(trimmed);
if (
Array.isArray(parsed) &&
parsed.every(item => typeof item === "string" || typeof item === "number" || typeof item === "boolean")
) {
return parsed;
}
throw new Error("Rule arrays can only contain strings, numbers, or booleans");
}
return trimmed;
}

function toRuleFormState(rule: FeatureFlagRule): RuleFormState {
return {
id: createRuleId(),
field: rule.field,
key: rule.key || "",
operator: rule.operator,
value: formatRuleValue(rule.value),
};
}

function toVariantFormState(variant: FeatureFlag["variants"][number]): VariantFormState {
return {
id: createRuleId(),
key: variant.key,
name: variant.name || "",
rolloutPercentage: variant.rolloutPercentage,
payload: formatPayloadValue(variant.payload),
};
}

function toConditionSetFormState(
conditionSet: FeatureFlag["conditionSets"][number],
flagType: FeatureFlagType,
index: number
): ConditionSetFormState {
return {
id: createRuleId(),
name: conditionSet.name || (index === 0 ? "Default" : `Condition ${index + 1}`),
rules: (conditionSet.rules || []).map(toRuleFormState),
rolloutPercentage: conditionSet.rolloutPercentage ?? 100,
variants:
conditionSet.variants && conditionSet.variants.length > 0
? conditionSet.variants.map(toVariantFormState)
: flagType === "multivariate"
? [createEmptyVariant(0), createEmptyVariant(1)]
: [],
payload: formatPayloadValue(conditionSet.payload),
};
}

function fallbackConditionSetFromFlag(flag: FeatureFlag): ConditionSetFormState {
return {
id: createRuleId(),
name: "Default",
rules: (flag.rules || []).map(toRuleFormState),
rolloutPercentage: flag.rolloutPercentage,
variants: flag.flagType === "multivariate" ? (flag.variants || []).map(toVariantFormState) : [],
payload: formatPayloadValue(flag.payload),
};
}

export function toFormState(flag?: FeatureFlag): FlagFormState {
if (!flag) return createEmptyForm();

return {
key: flag.key,
description: flag.description || "",
enabled: flag.enabled,
runtime: flag.runtime,
flagType: flag.flagType,
conditionSets:
flag.conditionSets && flag.conditionSets.length > 0
? flag.conditionSets.map((conditionSet, index) => toConditionSetFormState(conditionSet, flag.flagType, index))
: [fallbackConditionSetFromFlag(flag)],
};
}

export function buildPayload(form: FlagFormState): FeatureFlagPayload {
const buildRules = (rules: RuleFormState[]) =>
rules.map(rule => {
const requiresKey = rule.field === "query" || rule.field === "trait";
if (requiresKey && !rule.key.trim()) {
throw new Error("Rule key is required");
}
if (!rule.value.trim()) {
throw new Error("Rule value is required");
}

return {
field: rule.field,
key: requiresKey ? rule.key.trim() : undefined,
operator: rule.operator,
value: parseRuleValue(rule.value),
};
});

const buildVariants = (variants: VariantFormState[]) =>
variants.map(variant => ({
key: variant.key.trim(),
name: variant.name.trim() || undefined,
rolloutPercentage: variant.rolloutPercentage,
payload: parseOptionalPayload(variant.payload),
}));

const validateVariants = (variants: ReturnType<typeof buildVariants>) => {
const variantKeys = new Set(variants.map(variant => variant.key));
const variantRolloutTotal = variants.reduce((sum, variant) => sum + variant.rolloutPercentage, 0);

if (variants.length < 2) {
throw new Error("Multiple variant flags need at least two variants");
}
if (variants.some(variant => !variant.key)) {
throw new Error("Variant key is required");
}
if (variantKeys.size !== variants.length) {
throw new Error("Variant keys must be unique");
}
if (variantRolloutTotal > 100) {
throw new Error("Variant rollout percentages cannot exceed 100");
}
};

const conditionSets = form.conditionSets.map(conditionSet => {
if (
!Number.isInteger(conditionSet.rolloutPercentage) ||
conditionSet.rolloutPercentage < 0 ||
conditionSet.rolloutPercentage > 100
) {
throw new Error("Rollout must be between 0 and 100");
}

const variants = form.flagType === "multivariate" ? buildVariants(conditionSet.variants) : [];
if (form.flagType === "multivariate") {
validateVariants(variants);
}

return {
name: conditionSet.name.trim() || undefined,
rules: buildRules(conditionSet.rules),
rolloutPercentage: form.flagType === "boolean" ? conditionSet.rolloutPercentage : undefined,
variants: form.flagType === "multivariate" ? variants : undefined,
payload:
form.flagType === "remote_config"
? parseRequiredPayload(conditionSet.payload)
: parseOptionalPayload(conditionSet.payload),
};
});
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.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Move runtime validation to Zod schemas instead of manual parsing branches.

This file currently hand-rolls validation/parsing logic for rules, payloads, and variants. In this client path, runtime validation should be expressed with Zod schemas and parsed once at submit time.

As per coding guidelines, "Use TypeScript strict mode throughout; use Zod for runtime validation".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/app/`[site]/feature-flags/lib/form.ts around lines 77 - 247, The
code currently performs manual runtime validation inside parseOptionalPayload,
parseRequiredPayload, parseRuleValue and buildPayload (buildRules,
buildVariants, validateVariants) — move these checks into Zod schemas and call
schema.parse at submit time instead of manual branching: create Zod schemas for
FeatureFlagPayload, FeatureFlagRule, VariantFormState, ConditionSetFormState and
FlagFormState that capture required fields, types, array element constraints,
rollout bounds, and unique-keys constraint (use refinements for uniqueness and
sum checks), replace parseRuleValue/parseOptionalPayload/parseRequiredPayload
logic with schema coercions/parsers, and update buildPayload to simply transform
the already-validated parsed object into the API shape (or call schema.parse
within buildPayload once) rather than performing manual validation inside
buildRules/buildVariants/validateVariants.

Comment on lines +249 to +251
return {
key: form.key.trim(),
description: form.description.trim() || null,
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject empty feature-flag keys before returning payload.

key: form.key.trim() at Line 250 allows an empty key through. Add a required-key check in buildPayload to fail fast in the dialog flow.

Suggested fix
 export function buildPayload(form: FlagFormState): FeatureFlagPayload {
+  const normalizedKey = form.key.trim();
+  if (!normalizedKey) {
+    throw new Error("Key is required");
+  }
+
   const buildRules = (rules: RuleFormState[]) =>
@@
   return {
-    key: form.key.trim(),
+    key: normalizedKey,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return {
key: form.key.trim(),
description: form.description.trim() || null,
export function buildPayload(form: FlagFormState): FeatureFlagPayload {
const normalizedKey = form.key.trim();
if (!normalizedKey) {
throw new Error("Key is required");
}
const buildRules = (rules: RuleFormState[]) =>
// ... rest of function
return {
key: normalizedKey,
description: form.description.trim() || null,
// ... rest of return object
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/app/`[site]/feature-flags/lib/form.ts around lines 249 - 251, The
buildPayload function currently returns key: form.key.trim() even when that
trimmed string is empty; modify buildPayload to trim the key first, validate
that the trimmed value is not an empty string, and fail-fast (e.g., throw a
clear Error or return a rejected result) if it is empty so the dialog flow can
surface the validation immediately; update the code around the existing key:
form.key.trim() return (reference buildPayload and form.key) to perform this
check before constructing and returning the payload.

This commit updates language files for multiple languages, adding new keys and translations to improve user experience. Additionally, it enhances the UI of the feature flag editor by introducing helper text for condition sets, improving layout consistency, and adding icons for better visual representation. These changes aim to provide clearer instructions and a more intuitive interface for managing feature flags.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
client/messages/ja.json (1)

78-78: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Populate the new Japanese entries before merge.

Changed keys are still empty (for example, Line [78], Line [249], Line [416]). That will surface blank text in Japanese UI.

As per coding guidelines, client/messages/**/*.json: "Keep locale keys consistent across messages/*.json."

Also applies to: 249-250, 259-262, 266-274, 276-276, 281-282, 297-298, 301-302, 306-306, 308-308, 416-416

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/messages/ja.json` at line 78, The Japanese locale file
client/messages/ja.json contains empty translation values (e.g., key "mZDrWI"
and other keys around lines referenced) which will render blank text in the UI;
populate each empty value with the correct Japanese translation matching the
corresponding keys in other locale files (keep keys identical to source keys
used in messages/*.json), ensuring all changed keys listed (including the groups
around lines 249-250, 259-262, 266-274, 276, 281-282, 297-298, 301-302, 306,
308, and 416) are filled with appropriate Japanese strings and follow the
existing formatting/escaping conventions.
client/messages/fr.json (1)

78-78: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not leave newly introduced French keys empty.

New keys are added with empty values (e.g., Line [78], Line [249], Line [416]). That creates blank UI copy in affected feature flows.

As per coding guidelines, client/messages/**/*.json: "Keep locale keys consistent across messages/*.json."

Also applies to: 249-250, 259-262, 266-274, 276-276, 281-282, 297-298, 301-302, 306-306, 308-308, 416-416

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/messages/fr.json` at line 78, The French locale file has newly
introduced keys left empty (e.g., "mZDrWI" and the other keys at the locations
noted); update client/messages/fr.json by adding proper French translations for
each new key to match the corresponding entries in the default locale (or source
messages file) so UI copy is not blank—locate the untranslated keys such as
"mZDrWI" and the other empty keys referenced in the comment and provide accurate
French strings consistent with existing message keys and formatting conventions.
client/messages/pl.json (1)

78-78: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fill newly added Polish strings instead of shipping empty values.

Several newly added keys are "" (for example, Line [78], Line [249], Line [416]). These will render blank labels/messages in UI paths that use them.

As per coding guidelines, client/messages/**/*.json: "Keep locale keys consistent across messages/*.json."

Also applies to: 249-250, 259-262, 266-274, 276-276, 281-282, 297-298, 301-302, 306-306, 308-308, 416-416

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/messages/pl.json` at line 78, Replace all newly added Polish locale
entries that are empty (for example the key "mZDrWI" and any other keys
currently set to "") with their proper Polish translations, mirroring the intent
and phrasing from the corresponding keys in other locale files so locale keys
remain consistent; update each empty string value to a meaningful Polish string
matching the source/EN text and keep the JSON structure intact while preserving
the existing keys.
client/messages/es.json (1)

78-78: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Complete missing Spanish translations.

Approximately 23 translation keys have empty string values, which will render as blank labels in the Spanish UI. This breaks the user experience for Spanish-speaking users who won't see critical text for navigation, buttons, or messages.

Examples:

  • Line 78: "mZDrWI": ""
  • Lines 249-274: Multiple funnel/editor-related keys
  • Lines 281-282, 297-298, 301-302, 306, 308: Additional empty values

Ensure all keys have proper Spanish translations before merging.

Also applies to: 249-250, 259-262, 266-274, 276-276, 281-282, 297-298, 301-302, 306-306, 308-308

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/messages/es.json` at line 78, Several Spanish translation keys
currently have empty values (e.g., "mZDrWI" and the funnel/editor-related keys
around the 249–274, 281–282, 297–302, 306, 308 ranges), resulting in blank UI
labels; replace each empty string with an accurate Spanish translation
preserving any interpolation/placeholders/ICU tokens exactly as in the source
keys (do not change key names like "mZDrWI"), ensure the JSON remains valid
(quoting/commas intact), and run the i18n/lint/tests to verify no missing
translations remain before merging.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@client/messages/cs.json`:
- Around line 249-250: Several Czech locale entries are empty (e.g., keys
"U5hCuK", "wmdo1a" and the other keys noted), which will render blank UI text;
populate each of these keys in client/messages/cs.json with a proper Czech
translation (or temporarily copy the English value from the source
locale/messages file as a clear fallback) so no string value is "" and locale
key sets remain consistent across messages/*.json; update all occurrences the
reviewer flagged (lines with keys around 249, 259–262, 266–274, 276, 281–282,
297–298, 301–302, 306, 308) ensuring each key has a non-empty value.

In `@client/messages/uk.json`:
- Around line 249-250: Several message keys (e.g., "U5hCuK", "wmdo1a" and other
empty keys listed: ranges around keys at positions 259-262, 266-274, 276,
281-282, 297-298, 301-302, 306, 308) in the Ukrainian locale are empty; replace
each empty string with the correct Ukrainian translation matching the intent
from the canonical locale (usually en.json) and ensure every key present in
other locales exists and is non-empty here; update the corresponding entries in
client/messages/uk.json by providing Ukrainian text for each empty key and keep
key names exactly as in other locales so the messages/*.json key set stays
consistent across locales.

---

Outside diff comments:
In `@client/messages/es.json`:
- Line 78: Several Spanish translation keys currently have empty values (e.g.,
"mZDrWI" and the funnel/editor-related keys around the 249–274, 281–282,
297–302, 306, 308 ranges), resulting in blank UI labels; replace each empty
string with an accurate Spanish translation preserving any
interpolation/placeholders/ICU tokens exactly as in the source keys (do not
change key names like "mZDrWI"), ensure the JSON remains valid (quoting/commas
intact), and run the i18n/lint/tests to verify no missing translations remain
before merging.

In `@client/messages/fr.json`:
- Line 78: The French locale file has newly introduced keys left empty (e.g.,
"mZDrWI" and the other keys at the locations noted); update
client/messages/fr.json by adding proper French translations for each new key to
match the corresponding entries in the default locale (or source messages file)
so UI copy is not blank—locate the untranslated keys such as "mZDrWI" and the
other empty keys referenced in the comment and provide accurate French strings
consistent with existing message keys and formatting conventions.

In `@client/messages/ja.json`:
- Line 78: The Japanese locale file client/messages/ja.json contains empty
translation values (e.g., key "mZDrWI" and other keys around lines referenced)
which will render blank text in the UI; populate each empty value with the
correct Japanese translation matching the corresponding keys in other locale
files (keep keys identical to source keys used in messages/*.json), ensuring all
changed keys listed (including the groups around lines 249-250, 259-262,
266-274, 276, 281-282, 297-298, 301-302, 306, 308, and 416) are filled with
appropriate Japanese strings and follow the existing formatting/escaping
conventions.

In `@client/messages/pl.json`:
- Line 78: Replace all newly added Polish locale entries that are empty (for
example the key "mZDrWI" and any other keys currently set to "") with their
proper Polish translations, mirroring the intent and phrasing from the
corresponding keys in other locale files so locale keys remain consistent;
update each empty string value to a meaningful Polish string matching the
source/EN text and keep the JSON structure intact while preserving the existing
keys.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1ba42928-e6f3-4dc1-ab15-d9020a80ef90

📥 Commits

Reviewing files that changed from the base of the PR and between 0a8a8fe and 1dc303f.

📒 Files selected for processing (16)
  • client/messages/cs.json
  • client/messages/de.json
  • client/messages/en.json
  • client/messages/es.json
  • client/messages/fr.json
  • client/messages/it.json
  • client/messages/ja.json
  • client/messages/ko.json
  • client/messages/pl.json
  • client/messages/pt.json
  • client/messages/uk.json
  • client/messages/zh.json
  • client/src/app/[site]/feature-flags/components/ConditionSetsEditor.tsx
  • client/src/app/[site]/feature-flags/components/FeatureFlagDialog.tsx
  • client/src/app/[site]/feature-flags/components/TargetingRulesEditor.tsx
  • client/src/app/[site]/feature-flags/components/VariantsEditor.tsx
🚧 Files skipped from review as they are similar to previous changes (9)
  • client/src/app/[site]/feature-flags/components/ConditionSetsEditor.tsx
  • client/src/app/[site]/feature-flags/components/TargetingRulesEditor.tsx
  • client/src/app/[site]/feature-flags/components/VariantsEditor.tsx
  • client/src/app/[site]/feature-flags/components/FeatureFlagDialog.tsx
  • client/messages/en.json
  • client/messages/de.json
  • client/messages/it.json
  • client/messages/pt.json
  • client/messages/ko.json

Comment thread client/messages/cs.json
Comment on lines +249 to +250
"U5hCuK": "",
"wmdo1a": "",
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid shipping empty Czech translations for newly added keys.

A large part of the newly added Czech key set is still "" (e.g., Line [249], Line [266], Line [301]). These map to visible controls and can render blank UI text.

As per coding guidelines, client/messages/**/*.json: "Keep locale keys consistent across messages/*.json."

Also applies to: 259-262, 266-274, 276-276, 281-282, 297-298, 301-302, 306-306, 308-308

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/messages/cs.json` around lines 249 - 250, Several Czech locale entries
are empty (e.g., keys "U5hCuK", "wmdo1a" and the other keys noted), which will
render blank UI text; populate each of these keys in client/messages/cs.json
with a proper Czech translation (or temporarily copy the English value from the
source locale/messages file as a clear fallback) so no string value is "" and
locale key sets remain consistent across messages/*.json; update all occurrences
the reviewer flagged (lines with keys around 249, 259–262, 266–274, 276,
281–282, 297–298, 301–302, 306, 308) ensuring each key has a non-empty value.

Comment thread client/messages/uk.json
Comment on lines +249 to +250
"U5hCuK": "",
"wmdo1a": "",
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Complete the new Ukrainian translations in this block.

Many changed keys are still empty strings (for example, Line [249], Line [266], Line [301]). In production this can yield blank UI text for Ukrainian users.

As per coding guidelines, client/messages/**/*.json: "Keep locale keys consistent across messages/*.json."

Also applies to: 259-262, 266-274, 276-276, 281-282, 297-298, 301-302, 306-306, 308-308

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/messages/uk.json` around lines 249 - 250, Several message keys (e.g.,
"U5hCuK", "wmdo1a" and other empty keys listed: ranges around keys at positions
259-262, 266-274, 276, 281-282, 297-298, 301-302, 306, 308) in the Ukrainian
locale are empty; replace each empty string with the correct Ukrainian
translation matching the intent from the canonical locale (usually en.json) and
ensure every key present in other locales exists and is non-empty here; update
the corresponding entries in client/messages/uk.json by providing Ukrainian text
for each empty key and keep key names exactly as in other locales so the
messages/*.json key set stays consistent across locales.

This commit introduces a new JSON payload editor in the feature flag management interface, allowing users to add and edit payloads more intuitively. It also enhances the UI by improving layout consistency and adding icons for better visual representation. Additionally, translations for multiple languages are updated to reflect these changes, ensuring a cohesive user experience across the application.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
client/src/app/[site]/feature-flags/components/FeatureFlagDialog.tsx (1)

14-40: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Switch FeatureFlagDialog to use useTranslations() (not useExtracted())

File: client/src/app/[site]/feature-flags/components/FeatureFlagDialog.tsx
The dialog follows the project/i18n rule to use next-intl’s useTranslations() for UI strings. useExtracted is an experimental extraction API and requires special configuration, so it shouldn’t be used here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/app/`[site]/feature-flags/components/FeatureFlagDialog.tsx around
lines 14 - 40, The component FeatureFlagDialog currently imports and calls
useExtracted() to get translations; replace that with next-intl's stable
useTranslations() by updating the import and the hook call (replace useExtracted
import and the const t = useExtracted(); line with useTranslations and const t =
useTranslations(/* optional namespace if used */)); ensure any downstream usage
of t remains compatible. Locate FLAG_TYPE_ICONS, FeatureFlagDialog, and the t
variable to make the swap consistently across the file.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@client/src/app/`[site]/feature-flags/components/FeatureFlagDialog.tsx:
- Around line 14-40: The component FeatureFlagDialog currently imports and calls
useExtracted() to get translations; replace that with next-intl's stable
useTranslations() by updating the import and the hook call (replace useExtracted
import and the const t = useExtracted(); line with useTranslations and const t =
useTranslations(/* optional namespace if used */)); ensure any downstream usage
of t remains compatible. Locate FLAG_TYPE_ICONS, FeatureFlagDialog, and the t
variable to make the swap consistently across the file.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0a469b44-efdf-47fd-bbae-56ecc19bf579

📥 Commits

Reviewing files that changed from the base of the PR and between 1dc303f and cbb0a1b.

⛔ Files ignored due to path filters (1)
  • client/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (19)
  • client/messages/cs.json
  • client/messages/de.json
  • client/messages/en.json
  • client/messages/es.json
  • client/messages/fr.json
  • client/messages/it.json
  • client/messages/ja.json
  • client/messages/ko.json
  • client/messages/pl.json
  • client/messages/pt.json
  • client/messages/uk.json
  • client/messages/zh.json
  • client/package.json
  • client/src/app/[site]/feature-flags/components/ConditionSetsEditor.tsx
  • client/src/app/[site]/feature-flags/components/FeatureFlagDialog.tsx
  • client/src/app/[site]/feature-flags/components/FeatureFlagTable.tsx
  • client/src/app/[site]/feature-flags/components/JsonEditor.tsx
  • client/src/app/[site]/feature-flags/components/VariantsEditor.tsx
  • client/src/app/[site]/feature-flags/lib/form.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • client/src/app/[site]/feature-flags/components/VariantsEditor.tsx
  • client/src/app/[site]/feature-flags/components/FeatureFlagTable.tsx
  • client/messages/ko.json
  • client/messages/ja.json
  • client/messages/en.json
  • client/messages/cs.json
  • client/messages/pl.json
  • client/messages/de.json
  • client/src/app/[site]/feature-flags/lib/form.ts
  • client/messages/it.json
  • client/messages/zh.json
  • client/messages/fr.json

This commit introduces comprehensive documentation for feature flags, including usage examples in the tracking script and API specifications. It also updates the tagging documentation to reference feature flags for runtime value selection. Additionally, the server-side implementation is enhanced with regex validation for feature flag rules and improved evaluation methods, ensuring robust handling of feature flags in analytics.
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.

1 participant