1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

chore!: removing userId strategy for new installations of Unleash (#9800)

This removes a strategy that was already deprecated, but only for new
installations.

I tested starting with an installation with this strategy being used and
then updating, and I was still able to edit the strategy, so this should
not impact current users.

On a fresh install the strategy is no longer available.

---------

Co-authored-by: Nuno Góis <github@nunogois.com>
This commit is contained in:
Gastón Fournier 2025-06-04 09:30:13 +02:00 committed by GitHub
parent 8050f25add
commit 5019f4fcbc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 41 additions and 279 deletions

View File

@ -54,10 +54,6 @@ declare namespace Cypress {
deleteSegment_UI(segmentName: string, id: string): Chainable;
// STRATEGY
addUserIdStrategyToFeature_UI(
featureName: string,
projectName: string,
): Chainable;
addFlexibleRolloutStrategyToFeature_UI(
options: AddFlexibleRolloutStrategyOptions,
): Chainable;

View File

@ -257,57 +257,6 @@ export const deleteFeatureStrategy_UI = (
return cy.wait('@deleteUserStrategy');
};
export const addUserIdStrategyToFeature_UI = (
featureToggleName: string,
projectName: string,
): Chainable<any> => {
const project = projectName || 'default';
cy.visit(
`/projects/${project}/features/${featureToggleName}/strategies/create?environmentId=development&strategyName=userWithId`,
);
if (ENTERPRISE) {
cy.get('[data-testid=ADD_CONSTRAINT_ID]').click();
cy.get('[data-testid=CONSTRAINT_AUTOCOMPLETE_ID]')
.type('{downArrow}'.repeat(1))
.type('{enter}');
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
}
cy.get('[data-testid=STRATEGY_INPUT_LIST]')
.type('user1')
.type('{enter}')
.type('user2')
.type('{enter}');
cy.get('[data-testid=ADD_TO_STRATEGY_INPUT_LIST]').click();
cy.intercept(
'POST',
`/api/admin/projects/default/features/${featureToggleName}/environments/*/strategies`,
(req) => {
expect(req.body.name).to.equal('userWithId');
expect(req.body.parameters.userIds.length).to.equal(11);
if (ENTERPRISE) {
expect(req.body.constraints.length).to.equal(1);
} else {
expect(req.body.constraints.length).to.equal(0);
}
req.continue((res) => {
strategyId = res.body.id;
});
},
).as('addStrategyToFeature');
// this one needs to wait until the dropdown selector of stickiness is set, that's why waitForAnimations: true
cy.get(`[data-testid=STRATEGY_FORM_SUBMIT_ID]`)
.first()
.click({ waitForAnimations: true });
return cy.wait('@addStrategyToFeature');
};
export const logout_UI = (): Chainable<any> => {
return cy.visit('/logout');
};

View File

@ -10,7 +10,6 @@ import {
deleteSegment_UI,
deleteFeatureStrategy_UI,
addFlexibleRolloutStrategyToFeature_UI,
addUserIdStrategyToFeature_UI,
updateFlexibleRolloutStrategy_UI,
do_login,
} from './UI.ts';
@ -45,10 +44,6 @@ Cypress.Commands.add('updateUserPassword_API', updateUserPassword_API);
Cypress.Commands.add('createFeature_UI', createFeature_UI);
Cypress.Commands.add('deleteFeatureStrategy_UI', deleteFeatureStrategy_UI);
Cypress.Commands.add('createFeature_API', createFeature_API);
Cypress.Commands.add(
'addUserIdStrategyToFeature_UI',
addUserIdStrategyToFeature_UI,
);
Cypress.Commands.add(
'addFlexibleRolloutStrategyToFeature_UI',
addFlexibleRolloutStrategyToFeature_UI,

View File

@ -98,17 +98,20 @@ const setupOtherRoutes = (feature: string) => {
deprecated: false,
},
{
displayName: 'UserIDs',
name: 'userWithId',
displayName: 'Gradual rollout',
name: 'flexibleRollout',
editable: false,
description:
'Enable the feature for a specific set of userIds.',
'The gradual rollout strategy allows you to gradually roll out a feature to a percentage of users.',
parameters: [
{
name: 'userIds',
type: 'list',
description: '',
required: false,
name: 'rollout',
},
{
name: 'stickiness',
},
{
name: 'groupId',
},
],
deprecated: false,
@ -214,12 +217,10 @@ const UnleashUiSetup: FC<{
</SWRConfig>
);
const strategiesAreDisplayed = async (
firstStrategy: string,
secondStrategy: string,
) => {
await screen.findByText(firstStrategy);
await screen.findByText(secondStrategy);
const strategiesAreDisplayed = async (strategies: string[]) => {
for (const strategy of strategies) {
await screen.findByText(strategy);
}
};
const getDeleteButtons = async () => {
@ -299,7 +300,7 @@ test('open mode + non-project member can perform basic change request actions',
const featureName = 'test';
featureEnvironments(featureName, [
{ name: 'development', strategies: [] },
{ name: 'production', strategies: ['userWithId'] },
{ name: 'production', strategies: ['flexibleRollout'] },
{ name: 'custom', strategies: ['default'] },
]);
userIsMemberOfProjects([]);
@ -318,7 +319,7 @@ test('open mode + non-project member can perform basic change request actions',
);
await openEnvironments(['development', 'production', 'custom']);
await strategiesAreDisplayed('UserIDs', 'Standard');
await strategiesAreDisplayed(['Gradual rollout', 'Standard']);
await deleteButtonsActiveInChangeRequestEnv();
await copyButtonsActiveInOtherEnv();
});
@ -328,7 +329,7 @@ test('protected mode + project member can perform basic change request actions',
const featureName = 'test';
featureEnvironments(featureName, [
{ name: 'development', strategies: [] },
{ name: 'production', strategies: ['userWithId'] },
{ name: 'production', strategies: ['flexibleRollout'] },
{ name: 'custom', strategies: ['default'] },
]);
userIsMemberOfProjects([project]);
@ -348,7 +349,7 @@ test('protected mode + project member can perform basic change request actions',
await openEnvironments(['development', 'production', 'custom']);
await strategiesAreDisplayed('UserIDs', 'Standard');
await strategiesAreDisplayed(['Gradual rollout', 'Standard']);
await deleteButtonsActiveInChangeRequestEnv();
await copyButtonsActiveInOtherEnv();
});
@ -358,7 +359,7 @@ test.skip('protected mode + non-project member cannot perform basic change reque
const featureName = 'test';
featureEnvironments(featureName, [
{ name: 'development', strategies: [] },
{ name: 'production', strategies: ['userWithId'] },
{ name: 'production', strategies: ['flexibleRollout'] },
{ name: 'custom', strategies: ['default'] },
]);
userIsMemberOfProjects([]);
@ -378,7 +379,7 @@ test.skip('protected mode + non-project member cannot perform basic change reque
await openEnvironments(['development', 'production', 'custom']);
await strategiesAreDisplayed('UserIDs', 'Standard');
await strategiesAreDisplayed(['Gradual rollout', 'Standard']);
await deleteButtonsInactiveInChangeRequestEnv();
await copyButtonsActiveInOtherEnv();
});

View File

@ -1,7 +1,6 @@
import type { IFeatureStrategy, IStrategy } from 'interfaces/strategy';
import DefaultStrategy from 'component/feature/StrategyTypes/DefaultStrategy/DefaultStrategy';
import FlexibleStrategy from 'component/feature/StrategyTypes/FlexibleStrategy/FlexibleStrategy';
import UserWithIdStrategy from 'component/feature/StrategyTypes/UserWithIdStrategy/UserWithId';
import GeneralStrategy from 'component/feature/StrategyTypes/GeneralStrategy/GeneralStrategy';
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
import produce from 'immer';
@ -52,15 +51,6 @@ export const FeatureStrategyType = ({
errors={errors}
/>
);
case 'userWithId':
return (
<UserWithIdStrategy
parameters={strategy.parameters ?? {}}
updateParameter={updateParameter}
editable={hasAccess}
errors={errors}
/>
);
default:
return (
<GeneralStrategy

View File

@ -1,32 +0,0 @@
import type { IFeatureStrategyParameters } from 'interfaces/strategy';
import StrategyInputList from '../StrategyInputList/StrategyInputList.tsx';
import { parseParameterStrings } from 'utils/parseParameter';
import type { IFormErrors } from 'hooks/useFormErrors';
interface IUserWithIdStrategyProps {
parameters: IFeatureStrategyParameters;
updateParameter: (field: string, value: string) => void;
editable: boolean;
errors: IFormErrors;
}
const UserWithIdStrategy = ({
editable,
parameters,
updateParameter,
errors,
}: IUserWithIdStrategyProps) => {
return (
<div>
<StrategyInputList
name='userIds'
list={parseParameterStrings(parameters.userIds)}
disabled={!editable}
setConfig={updateParameter}
errors={errors}
/>
</div>
);
};
export default UserWithIdStrategy;

View File

@ -3,7 +3,6 @@ import type { IReleasePlanMilestoneStrategy } from 'interfaces/releasePlans';
import type { IStrategy } from 'interfaces/strategy';
import { MilestoneStrategyTypeFlexible } from './MilestoneStrategyTypeFlexible.tsx';
import GeneralStrategy from 'component/feature/StrategyTypes/GeneralStrategy/GeneralStrategy';
import UserWithIdStrategy from 'component/feature/StrategyTypes/UserWithIdStrategy/UserWithId';
import DefaultStrategy from 'component/feature/StrategyTypes/DefaultStrategy/DefaultStrategy';
interface IMilestoneStrategyTypeProps {
@ -36,15 +35,6 @@ export const MilestoneStrategyType = ({
editable={true}
/>
);
case 'userWithId':
return (
<UserWithIdStrategy
editable={true}
parameters={strategy.parameters ?? {}}
updateParameter={updateParameter}
errors={errors}
/>
);
default:
return (
<GeneralStrategy

View File

@ -1,7 +1,6 @@
import type { FC, SVGProps } from 'react';
import { SvgIcon, useTheme } from '@mui/material';
import LocationOnIcon from '@mui/icons-material/LocationOn';
import PeopleIcon from '@mui/icons-material/People';
import LanguageIcon from '@mui/icons-material/Language';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import CodeIcon from '@mui/icons-material/Code';
@ -28,8 +27,6 @@ export const getFeatureStrategyIcon = (strategyName?: string) => {
return LanguageIcon;
case 'flexibleRollout':
return RolloutSvgIcon;
case 'userWithId':
return PeopleIcon;
case 'applicationHostname':
return LocationOnIcon;
case 'releasePlanTemplate':
@ -47,7 +44,6 @@ export const BuiltInStrategies = [
'gradualRolloutSessionId',
'gradualRolloutUserId',
'remoteAddress',
'userWithId',
];
export const GetFeatureStrategyIcon: FC<{ strategyName: string }> = ({
@ -66,5 +62,4 @@ export const formattedStrategyNames: Record<string, string> = {
gradualRolloutSessionId: 'Sessions',
gradualRolloutUserId: 'Users',
remoteAddress: 'IPs',
userWithId: 'UserIDs',
};

View File

@ -239,11 +239,3 @@ exports[`Should format specialised text for events when strategy removed 1`] = `
"url": "unleashUrl/projects/my-other-project/features/new-feature",
}
`;
exports[`Should format specialised text for events when userIds changed 1`] = `
{
"label": "Flag strategy updated",
"text": "*user@company.com* updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *[my-other-project](unleashUrl/projects/my-other-project)* by updating strategy *userWithId* in *production* userIds from empty set of userIds to [a,b]; constraints from empty set of constraints to [appName is one of (x,y)]",
"url": "unleashUrl/projects/my-other-project/features/new-feature",
}
`;

View File

@ -352,46 +352,6 @@ const testCases: [string, IEvent][] = [
},
],
),
[
'when userIds changed',
{
id: 920,
type: FEATURE_STRATEGY_UPDATE,
createdBy: 'user@company.com',
createdByUserId: SYSTEM_USER_ID,
createdAt: new Date('2022-06-01T10:03:11.549Z'),
data: {
name: 'userWithId',
constraints: [
{
values: ['x', 'y'],
inverted: false,
operator: IN,
contextName: 'appName',
caseInsensitive: false,
},
],
parameters: {
userIds: 'a,b',
},
sortOrder: 9999,
id: '9a995d94-5944-4897-a82f-0f7e65c2fb3f',
},
preData: {
name: 'userWithId',
constraints: [],
parameters: {
userIds: '',
},
sortOrder: 9999,
id: '9a995d94-5944-4897-a82f-0f7e65c2fb3f',
},
tags: [],
featureName: 'new-feature',
project: 'my-other-project',
environment: 'production',
},
],
[
'when IPs changed',
{

View File

@ -135,8 +135,6 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter {
return this.flexibleRolloutStrategyChangeText(event);
case 'default':
return this.defaultStrategyChangeText(event);
case 'userWithId':
return this.userWithIdStrategyChangeText(event);
case 'remoteAddress':
return this.remoteAddressStrategyChangeText(event);
case 'applicationHostname':
@ -162,10 +160,6 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter {
return this.listOfValuesStrategyChangeText(event, 'IPs');
}
private userWithIdStrategyChangeText(event: IEvent) {
return this.listOfValuesStrategyChangeText(event, 'userIds');
}
private listOfValuesStrategyChangeText(
event: IEvent,
propertyName: string,

View File

@ -63,9 +63,9 @@ exports[`should match snapshot from /api/client/features 1`] = `
},
],
"meta": {
"etag": ""61824cd0:20"",
"etag": ""61824cd0:19"",
"queryHash": "61824cd0",
"revisionId": 20,
"revisionId": 19,
},
"query": {
"environment": "default",

View File

@ -354,7 +354,10 @@ class EventStore implements IEventStore {
.select(EVENT_COLUMNS)
.from(TABLE)
.limit(100)
.orderBy('created_at', 'desc');
.orderBy([
{ column: 'created_at', order: 'desc' },
{ column: 'id', order: 'desc' },
]);
if (query) {
qB = qB.where(query);
}

View File

@ -2,7 +2,6 @@ import DefaultStrategy from './default-strategy.js';
import GradualRolloutRandomStrategy from './gradual-rollout-random.js';
import GradualRolloutUserIdStrategy from './gradual-rollout-user-id.js';
import GradualRolloutSessionIdStrategy from './gradual-rollout-session-id.js';
import UserWithIdStrategy from './user-with-id-strategy.js';
import RemoteAddressStrategy from './remote-address-strategy.js';
import FlexibleRolloutStrategy from './flexible-rollout-strategy.js';
import type { Strategy } from './strategy.js';
@ -18,7 +17,6 @@ export const defaultStrategies: Array<Strategy> = [
new GradualRolloutRandomStrategy(),
new GradualRolloutUserIdStrategy(),
new GradualRolloutSessionIdStrategy(),
new UserWithIdStrategy(),
new RemoteAddressStrategy(),
new FlexibleRolloutStrategy(),
new UnknownStrategy(),

View File

@ -1,17 +0,0 @@
import { Strategy } from './strategy.js';
import type { Context } from '../context.js';
export default class UserWithIdStrategy extends Strategy {
constructor() {
super('userWithId');
}
isEnabled(parameters: { userIds?: string }, context: Context): boolean {
const userIdList = parameters.userIds
? parameters.userIds.split(/\s*,\s*/)
: [];
return (
context.userId !== undefined && userIdList.includes(context.userId)
);
}
}

View File

@ -432,13 +432,6 @@ describe('offline client', () => {
stickiness: 'userId',
},
},
{
name: 'userWithId',
constraints: [],
parameters: {
userIds: 'uoea,ueoa',
},
},
{
name: 'remoteAddress',
constraints: [],

View File

@ -48,7 +48,6 @@ test('clientApplicationSchema go-sdk request', () => {
"gradualRolloutSessionId",
"gradualRolloutUserId",
"remoteAddress",
"userWithId",
"flexibleRollout"
],
"started": "2022-06-24T09:59:12.822607943+02:00",
@ -75,7 +74,6 @@ test('clientApplicationSchema node-sdk request', () => {
"gradualRolloutRandom",
"gradualRolloutUserId",
"gradualRolloutSessionId",
"userWithId",
"remoteAddress",
"flexibleRollout"
],

View File

@ -95,14 +95,6 @@ const playgroundStrategies = (): Arbitrary<PlaygroundStrategySchema[]> =>
}),
),
playgroundStrategy(
'userWithId',
fc.record({
userIds: fc
.uniqueArray(fc.emailAddress())
.map((ids) => ids.join(',')),
}),
),
playgroundStrategy(
'remoteAddress',
fc.record({

View File

@ -6,7 +6,6 @@ exports.up = function (db, cb) {
ALTER TABLE strategies ADD COLUMN sort_order integer DEFAULT 9999;
UPDATE strategies SET sort_order = 0 WHERE name = 'default';
UPDATE strategies SET sort_order = 1 WHERE name = 'flexibleRollout';
UPDATE strategies SET sort_order = 2 WHERE name = 'userWithId';
UPDATE strategies SET sort_order = 3 WHERE name = 'remoteAddress';
UPDATE strategies SET sort_order = 4 WHERE name = 'applicationHostname';
`,

View File

@ -6,7 +6,6 @@ exports.up = function (db, cb) {
ALTER TABLE strategies ADD COLUMN display_name text;
UPDATE strategies SET display_name = 'Standard', description = 'The standard strategy is strictly on / off for your entire userbase.' WHERE name = 'default';
UPDATE strategies SET display_name = 'Gradual rollout', description = 'Roll out to a percentage of your userbase, and ensure that the experience is the same for the user on each visit.' WHERE name = 'flexibleRollout';
UPDATE strategies SET display_name = 'UserIDs', description = 'Enable the feature for a specific set of userIds.' WHERE name = 'userWithId';
UPDATE strategies SET display_name = 'IPs', description = 'Enable the feature for a specific set of IP addresses.' WHERE name = 'remoteAddress';
UPDATE strategies SET display_name = 'Hosts', description = 'Enable the feature for a specific set of hostnames.' WHERE name = 'applicationHostname';
`,

View File

@ -9,13 +9,9 @@ exports.up = function (db, callback) {
and deprecated
and not exists (select * from feature_strategies where strategy_name = name limit 1);
-- deprecate strategies on v5
update strategies set deprecated = true where name in ('userWithId');
-- update strategy descriptions and sort order
update strategies set sort_order = 1, description = 'This strategy turns on / off for your entire userbase. Prefer using "Gradual rollout" strategy (100%=on, 0%=off).' WHERE name = 'default';
update strategies set sort_order = 0 WHERE name = 'flexibleRollout';
update strategies set description = 'Enable the feature for a specific set of userIds. Prefer using "Gradual rollout" strategy with user id constraints.' WHERE name = 'userWithId';
`,
callback,
);

View File

@ -4,18 +4,6 @@
"description": "Default on/off strategy.",
"parameters": []
},
{
"name": "userWithId",
"description": "Active for users with a userId defined in the userIds-list",
"parameters": [
{
"name": "userIds",
"type": "list",
"description": "",
"required": false
}
]
},
{
"name": "applicationHostname",
"description": "Active for client instances with a hostName in the hostNames-list.",

View File

@ -107,14 +107,6 @@ export const strategies = (): Arbitrary<FeatureStrategySchema[]> =>
}),
),
strategy(
'userWithId',
fc.record({
userIds: fc
.uniqueArray(fc.emailAddress())
.map((ids) => ids.join(',')),
}),
),
strategy(
'remoteAddress',
fc.record({

View File

@ -152,26 +152,26 @@ describe.each([
.expect(200);
if (etagVariant.feature_enabled) {
expect(res.headers.etag).toBe(`"61824cd0:17:${etagVariant.name}"`);
expect(res.headers.etag).toBe(`"61824cd0:16:${etagVariant.name}"`);
expect(res.body.meta.etag).toBe(
`"61824cd0:17:${etagVariant.name}"`,
`"61824cd0:16:${etagVariant.name}"`,
);
} else {
expect(res.headers.etag).toBe('"61824cd0:17"');
expect(res.body.meta.etag).toBe('"61824cd0:17"');
expect(res.headers.etag).toBe('"61824cd0:16"');
expect(res.body.meta.etag).toBe('"61824cd0:16"');
}
});
test(`returns ${etagVariant.feature_enabled ? 200 : 304} for pre-calculated hash${etagVariant.feature_enabled ? ' because hash changed' : ''}`, async () => {
const res = await app.request
.get('/api/client/features')
.set('if-none-match', '"61824cd0:17"')
.set('if-none-match', '"61824cd0:16"')
.expect(etagVariant.feature_enabled ? 200 : 304);
if (etagVariant.feature_enabled) {
expect(res.headers.etag).toBe(`"61824cd0:17:${etagVariant.name}"`);
expect(res.headers.etag).toBe(`"61824cd0:16:${etagVariant.name}"`);
expect(res.body.meta.etag).toBe(
`"61824cd0:17:${etagVariant.name}"`,
`"61824cd0:16:${etagVariant.name}"`,
);
}
});
@ -193,13 +193,13 @@ describe.each([
.expect(200);
if (etagVariant.feature_enabled) {
expect(res.headers.etag).toBe(`"61824cd0:17:${etagVariant.name}"`);
expect(res.headers.etag).toBe(`"61824cd0:16:${etagVariant.name}"`);
expect(res.body.meta.etag).toBe(
`"61824cd0:17:${etagVariant.name}"`,
`"61824cd0:16:${etagVariant.name}"`,
);
} else {
expect(res.headers.etag).toBe('"61824cd0:17"');
expect(res.body.meta.etag).toBe('"61824cd0:17"');
expect(res.headers.etag).toBe('"61824cd0:16"');
expect(res.body.meta.etag).toBe('"61824cd0:16"');
}
});
});

View File

@ -180,7 +180,6 @@ This endpoint gives insight into details about application seen per feature flag
"abTest",
"default",
"betaUser",
"userWithId",
"byHostName",
"gradualRolloutWithSessionId",
"gradualRollout",

View File

@ -21,18 +21,6 @@ Used to fetch all defined strategies and their defined parameters.
"description": "Default on/off strategy.",
"parameters": []
},
{
"name": "userWithId",
"description": "Active for userId specified in the comma seperated 'userIds' parameter.",
"parameters": [
{
"name": "userIds",
"type": "list",
"description": "List of unique userIds the feature should be active for.",
"required": true
}
]
},
{
"name": "gradualRollout",
"description": "Gradual rollout to logged in users",

View File

@ -8,6 +8,10 @@ Predefined strategy types are a legacy implementation. Please use the [default s
## UserIDs
:::warning
The `userWithId` strategy was removed in Unleash v7.0.0 for new installations. Instead use a gradual rollout strategy with a user ID stickiness and constraints.
:::
The `userWithId` strategy is active for users with a `userId` defined in the `userIds` list.
**Parameters:**