Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion apps/webapp/app/components/admin/FeatureFlagsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { CheckboxWithLabel } from "~/components/primitives/Checkbox";
import { cn } from "~/utils/cn";
import {
FEATURE_FLAG,
derivedFlagsClearedWith,
ORG_LOCKED_FLAG_EFFECT,
ORG_LOCKED_FLAGS,
isUnchangedInvalidFlag,
Expand Down Expand Up @@ -96,7 +97,9 @@ export function FeatureFlagsDialog({
const [saveError, setSaveError] = useState<string | null>(null);
const [unlocked, setUnlocked] = useState(false);

const isLocked = (key: string) => !unlocked && ORG_LOCKED_FLAGS.includes(key);
const isLocked = (key: string) =>
(!unlocked || ORG_LOCKED_FLAG_EFFECT[key as FeatureFlagKey] === "derived") &&
ORG_LOCKED_FLAGS.includes(key);

useEffect(() => {
if (open && orgId) {
Expand Down Expand Up @@ -148,6 +151,9 @@ export function FeatureFlagsDialog({
setOverrides((prev) => {
const next = { ...prev };
delete next[key];
for (const derivedKey of derivedFlagsClearedWith(key)) {
delete next[derivedKey];
}
return next;
});
};
Expand Down
7 changes: 3 additions & 4 deletions apps/webapp/app/v3/featureFlags.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
FeatureFlagCatalog,
GLOBAL_LOCKED_FLAGS,
GRACED_FLAG_GROUPS,
parseStoredOrgFlags,
validatePartialFeatureFlags,
} from "~/v3/featureFlags";
import { env } from "~/env.server";
Expand Down Expand Up @@ -400,10 +401,8 @@ export function mergeOrgFeatureFlags(
globalFlags: Partial<FeatureFlagCatalog>,
orgFeatureFlags: unknown
) {
const parsed = orgFeatureFlags
? validatePartialFeatureFlags(orgFeatureFlags as Record<string, unknown>)
: ({ success: false } as const);
return { ...globalFlags, ...(parsed.success ? parsed.data : {}) };
const { valid } = parseStoredOrgFlags(orgFeatureFlags);
return { ...globalFlags, ...valid };
Comment on lines +404 to +405

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.

🟡 Customer-facing flag fix lacks release note

When mergeOrgFeatureFlags restores valid overrides, customers regain dashboard features such as Logs access. This server-only fix lacks the required customer-facing release note.

Learn more

The server changes policy requires a .server-changes/ entry for a customer-visible server-only fix. OrganizationsPresenter supplies these merged flags to the dashboard, and the side menu uses them to show customer features. The PR changes only webapp files and does not include that release entry.

Example: An org has hasLogsPageAccess: true alongside an invalid realtimeBackend. After this fix, its users regain the Logs link; the release notes do not mention the change.

Recommended fix: Add a .server-changes/ Markdown file with area: webapp, type: fix, and one user-facing sentence describing the restored org feature access.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

}

/**
Expand Down
18 changes: 18 additions & 0 deletions apps/webapp/test/storedOrgFlags.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { mergeOrgFeatureFlags } from "~/v3/featureFlags.server";

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.

🔍 Pure merge test imports server configuration

The new mergeOrgFeatureFlags test loads env.server.ts indirectly through its server module. Webapp test guidance favors separating pure functions from environment-bound modules.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

import { describe, expect, it } from "vitest";
import {
FEATURE_FLAG,
Expand Down Expand Up @@ -94,3 +95,20 @@ describe("isUnchangedInvalidFlag", () => {
expect(isUnchangedInvalidFlag(KEY, { [KEY]: { x: null } }, {})).toBe(false);
});
});

describe("mergeOrgFeatureFlags", () => {
it("keeps valid org overrides when one stored flag value is invalid", () => {
const globalFlags = {
[FEATURE_FLAG.hasLogsPageAccess]: false,
[FEATURE_FLAG.realtimeBackend]: "redis" as const,
};
const orgFeatureFlags = {
[FEATURE_FLAG.hasLogsPageAccess]: true,
[FEATURE_FLAG.realtimeBackend]: "not-a-backend",
};

const merged = mergeOrgFeatureFlags(globalFlags, orgFeatureFlags);
expect(merged[FEATURE_FLAG.hasLogsPageAccess]).toBe(true);
expect(merged[FEATURE_FLAG.realtimeBackend]).toBe("redis");
});
});