mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-31 00:16:47 +01:00
Feat/enable disable strategies (#3566)
<!-- Thanks for creating a PR! To make it easier for reviewers and everyone else to understand what your changes relate to, please add some relevant content to the headings below. Feel free to ignore or delete sections that you don't think are relevant. Thank you! ❤️ --> Adds enabled field to feature strategies Filter out disabled strategies when returning/evaluating ## About the changes <!-- Describe the changes introduced. What are they and why are they being introduced? Feel free to also add screenshots or steps to view the changes if they're visual. --> <!-- Does it close an issue? Multiple? --> Closes # [1-865](https://linear.app/unleash/issue/1-865/allow-for-enablingdisabling-strategies-in-place-backend) <!-- (For internal contributors): Does it relate to an issue on public roadmap? --> <!-- Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item: # --> ### Important files <!-- PRs can contain a lot of changes, but not all changes are equally important. Where should a reviewer start looking to get an overview of the changes? Are any files particularly important? --> ## Discussion points <!-- Anything about the PR you'd like to discuss before it gets merged? Got any questions or doubts? --> --------- Signed-off-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
parent
29907d8d9f
commit
097dd8ae56
@ -35,6 +35,7 @@ const COLUMNS = [
|
||||
'parameters',
|
||||
'constraints',
|
||||
'created_at',
|
||||
'disabled',
|
||||
];
|
||||
/*
|
||||
const mapperToColumnNames = {
|
||||
@ -62,6 +63,7 @@ interface IFeatureStrategiesTable {
|
||||
constraints: string;
|
||||
sort_order: number;
|
||||
created_at?: Date;
|
||||
disabled?: boolean | null;
|
||||
}
|
||||
|
||||
export interface ILoadFeatureToggleWithEnvsParams {
|
||||
@ -83,6 +85,7 @@ function mapRow(row: IFeatureStrategiesTable): IFeatureStrategy {
|
||||
constraints: (row.constraints as unknown as IConstraint[]) || [],
|
||||
createdAt: row.created_at,
|
||||
sortOrder: row.sort_order,
|
||||
disabled: row.disabled,
|
||||
};
|
||||
}
|
||||
|
||||
@ -98,6 +101,7 @@ function mapInput(input: IFeatureStrategy): IFeatureStrategiesTable {
|
||||
constraints: JSON.stringify(input.constraints || []),
|
||||
created_at: input.createdAt,
|
||||
sort_order: input.sortOrder,
|
||||
disabled: input.disabled,
|
||||
};
|
||||
}
|
||||
|
||||
@ -106,6 +110,7 @@ interface StrategyUpdate {
|
||||
parameters: object;
|
||||
constraints: string;
|
||||
title?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function mapStrategyUpdate(
|
||||
@ -121,6 +126,9 @@ function mapStrategyUpdate(
|
||||
if (input.title !== null) {
|
||||
update.title = input.title;
|
||||
}
|
||||
if (input.disabled !== null) {
|
||||
update.disabled = input.disabled;
|
||||
}
|
||||
update.constraints = JSON.stringify(input.constraints || []);
|
||||
return update;
|
||||
}
|
||||
@ -590,6 +598,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
||||
sortOrder: r.sort_order,
|
||||
id: r.strategy_id,
|
||||
title: r.strategy_title || '',
|
||||
disabled: r.strategy_disabled || false,
|
||||
};
|
||||
if (!includeId) {
|
||||
delete strategy.id;
|
||||
|
@ -4,36 +4,24 @@ import { DB_TIME } from '../metric-events';
|
||||
import { Logger, LogProvider } from '../logger';
|
||||
import {
|
||||
IFeatureToggleClient,
|
||||
IFeatureToggleClientStore,
|
||||
IFeatureToggleQuery,
|
||||
IStrategyConfig,
|
||||
ITag,
|
||||
} from '../types/model';
|
||||
import { IFeatureToggleClientStore } from '../types/stores/feature-toggle-client-store';
|
||||
import { DEFAULT_ENV } from '../util/constants';
|
||||
import { PartialDeep } from '../types/partial';
|
||||
PartialDeep,
|
||||
} from '../types';
|
||||
import { DEFAULT_ENV, ensureStringValue, mapValues } from '../util';
|
||||
import EventEmitter from 'events';
|
||||
import FeatureToggleStore from './feature-toggle-store';
|
||||
import { ensureStringValue } from '../util/ensureStringValue';
|
||||
import { mapValues } from '../util/map-values';
|
||||
import Raw = Knex.Raw;
|
||||
import { Db } from './db';
|
||||
|
||||
export interface FeaturesTable {
|
||||
name: string;
|
||||
description: string;
|
||||
type: string;
|
||||
stale: boolean;
|
||||
variants: string;
|
||||
project: string;
|
||||
last_seen_at?: Date;
|
||||
created_at?: Date;
|
||||
}
|
||||
import Raw = Knex.Raw;
|
||||
|
||||
export interface IGetAllFeatures {
|
||||
featureQuery?: IFeatureToggleQuery;
|
||||
archived: boolean;
|
||||
isAdmin: boolean;
|
||||
includeStrategyIds?: boolean;
|
||||
includeDisabledStrategies?: boolean;
|
||||
userId?: number;
|
||||
}
|
||||
|
||||
@ -67,6 +55,7 @@ export default class FeatureToggleClientStore
|
||||
archived,
|
||||
isAdmin,
|
||||
includeStrategyIds,
|
||||
includeDisabledStrategies,
|
||||
userId,
|
||||
}: IGetAllFeatures): Promise<IFeatureToggleClient[]> {
|
||||
const environment = featureQuery?.environment || DEFAULT_ENV;
|
||||
@ -86,6 +75,7 @@ export default class FeatureToggleClientStore
|
||||
'fe.environment as environment',
|
||||
'fs.id as strategy_id',
|
||||
'fs.strategy_name as strategy_name',
|
||||
'fs.disabled as strategy_disabled',
|
||||
'fs.parameters as parameters',
|
||||
'fs.constraints as constraints',
|
||||
'segments.id as segment_id',
|
||||
@ -180,7 +170,7 @@ export default class FeatureToggleClientStore
|
||||
strategies: [],
|
||||
};
|
||||
if (this.isUnseenStrategyRow(feature, r)) {
|
||||
feature.strategies.push(
|
||||
feature.strategies?.push(
|
||||
FeatureToggleClientStore.rowToStrategy(r),
|
||||
);
|
||||
}
|
||||
@ -221,6 +211,12 @@ export default class FeatureToggleClientStore
|
||||
FeatureToggleClientStore.removeIdsFromStrategies(features);
|
||||
}
|
||||
|
||||
if (!includeDisabledStrategies) {
|
||||
// We should not send disabled strategies from the client API,
|
||||
// as this breaks the way SDKs evaluate the status of the feature.
|
||||
return this.removeDisabledStrategies(features);
|
||||
}
|
||||
|
||||
return features;
|
||||
}
|
||||
|
||||
@ -228,6 +224,8 @@ export default class FeatureToggleClientStore
|
||||
return {
|
||||
id: row.strategy_id,
|
||||
name: row.strategy_name,
|
||||
title: row.strategy_title,
|
||||
disabled: row.strategy_disabled,
|
||||
constraints: row.constraints || [],
|
||||
parameters: mapValues(row.parameters || {}, ensureStringValue),
|
||||
};
|
||||
@ -248,13 +246,26 @@ export default class FeatureToggleClientStore
|
||||
});
|
||||
}
|
||||
|
||||
private removeDisabledStrategies(
|
||||
features: IFeatureToggleClient[],
|
||||
): IFeatureToggleClient[] {
|
||||
const filtered: IFeatureToggleClient[] = [];
|
||||
features.forEach((feature) => {
|
||||
const filteredStrategies = feature.strategies.filter(
|
||||
(strategy) => !strategy.disabled,
|
||||
);
|
||||
filtered.push({ ...feature, strategies: filteredStrategies });
|
||||
});
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private isUnseenStrategyRow(
|
||||
feature: PartialDeep<IFeatureToggleClient>,
|
||||
row: Record<string, any>,
|
||||
): boolean {
|
||||
return (
|
||||
row.strategy_id &&
|
||||
!feature.strategies.find((s) => s.id === row.strategy_id)
|
||||
!feature.strategies?.find((s) => s?.id === row.strategy_id)
|
||||
);
|
||||
}
|
||||
|
||||
@ -276,7 +287,7 @@ export default class FeatureToggleClientStore
|
||||
row.tag_value &&
|
||||
!feature.tags?.some(
|
||||
(tag) =>
|
||||
tag.type === row.tag_type && tag.value === row.tag_value,
|
||||
tag?.type === row.tag_type && tag?.value === row.tag_value,
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -286,16 +297,16 @@ export default class FeatureToggleClientStore
|
||||
row: Record<string, any>,
|
||||
) {
|
||||
feature.strategies
|
||||
.find((s) => s.id === row.strategy_id)
|
||||
?.constraints.push(...row.segment_constraints);
|
||||
?.find((s) => s?.id === row.strategy_id)
|
||||
?.constraints?.push(...row.segment_constraints);
|
||||
}
|
||||
|
||||
private addSegmentIdsToStrategy(
|
||||
feature: PartialDeep<IFeatureToggleClient>,
|
||||
row: Record<string, any>,
|
||||
) {
|
||||
const strategy = feature.strategies.find(
|
||||
(s) => s.id === row.strategy_id,
|
||||
const strategy = feature.strategies?.find(
|
||||
(s) => s?.id === row.strategy_id,
|
||||
);
|
||||
if (!strategy) {
|
||||
return;
|
||||
@ -309,12 +320,14 @@ export default class FeatureToggleClientStore
|
||||
async getClient(
|
||||
featureQuery?: IFeatureToggleQuery,
|
||||
includeStrategyIds?: boolean,
|
||||
includeDisabledStrategies?: boolean,
|
||||
): Promise<IFeatureToggleClient[]> {
|
||||
return this.getAll({
|
||||
featureQuery,
|
||||
archived: false,
|
||||
isAdmin: false,
|
||||
includeStrategyIds,
|
||||
includeDisabledStrategies,
|
||||
});
|
||||
}
|
||||
|
||||
@ -323,7 +336,13 @@ export default class FeatureToggleClientStore
|
||||
userId,
|
||||
archived,
|
||||
}: IGetAdminFeatures): Promise<IFeatureToggleClient[]> {
|
||||
return this.getAll({ featureQuery, archived, isAdmin: true, userId });
|
||||
return this.getAll({
|
||||
featureQuery,
|
||||
archived: Boolean(archived),
|
||||
isAdmin: true,
|
||||
userId,
|
||||
includeDisabledStrategies: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,13 @@ export const createFeatureStrategySchema = {
|
||||
description: 'A descriptive title for the strategy',
|
||||
example: 'Gradual Rollout 25-Prod',
|
||||
},
|
||||
disabled: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'A toggle to disable the strategy. defaults to false. Disabled strategies are not evaluated or returned to the SDKs',
|
||||
example: false,
|
||||
nullable: true,
|
||||
},
|
||||
sortOrder: {
|
||||
type: 'number',
|
||||
description: 'The order of the strategy in the list',
|
||||
|
@ -26,6 +26,13 @@ export const featureStrategySchema = {
|
||||
example: 'Gradual Rollout 25-Prod',
|
||||
nullable: true,
|
||||
},
|
||||
disabled: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'A toggle to disable the strategy. defaults to false. Disabled strategies are not evaluated or returned to the SDKs',
|
||||
example: false,
|
||||
nullable: true,
|
||||
},
|
||||
featureName: {
|
||||
type: 'string',
|
||||
description: 'The name or feature the strategy is attached to',
|
||||
|
@ -64,6 +64,7 @@ const playgroundStrategy = (
|
||||
parameters,
|
||||
constraints: playgroundStrategyConstraints(),
|
||||
segments: fc.array(playgroundSegment()),
|
||||
disabled: fc.boolean(),
|
||||
});
|
||||
|
||||
const playgroundStrategies = (): Arbitrary<PlaygroundStrategySchema[]> =>
|
||||
|
@ -60,7 +60,15 @@ export const playgroundStrategySchema = {
|
||||
$id: '#/components/schemas/playgroundStrategySchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['id', 'name', 'result', 'segments', 'constraints', 'parameters'],
|
||||
required: [
|
||||
'id',
|
||||
'name',
|
||||
'result',
|
||||
'segments',
|
||||
'constraints',
|
||||
'parameters',
|
||||
'disabled',
|
||||
],
|
||||
properties: {
|
||||
name: {
|
||||
description: "The strategy's name.",
|
||||
@ -79,6 +87,13 @@ export const playgroundStrategySchema = {
|
||||
description: `The strategy's evaluation result. If the strategy is a custom strategy that Unleash can't evaluate, \`evaluationStatus\` will be \`${playgroundStrategyEvaluation.unknownResult}\`. Otherwise, it will be \`true\` or \`false\``,
|
||||
...strategyEvaluationResults,
|
||||
},
|
||||
disabled: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
"The strategy's status. Disabled strategies are not evaluated",
|
||||
example: false,
|
||||
nullable: true,
|
||||
},
|
||||
segments: {
|
||||
type: 'array',
|
||||
description:
|
||||
|
@ -18,6 +18,19 @@ export const updateFeatureStrategySchema = {
|
||||
$ref: '#/components/schemas/constraintSchema',
|
||||
},
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'A descriptive title for the strategy',
|
||||
example: 'Gradual Rollout 25-Prod',
|
||||
},
|
||||
disabled: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'A toggle to disable the strategy. defaults to true. Disabled strategies are not evaluated or returned to the SDKs',
|
||||
example: false,
|
||||
nullable: true,
|
||||
},
|
||||
parameters: {
|
||||
$ref: '#/components/schemas/parametersSchema',
|
||||
},
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
EnvironmentVariantEvent,
|
||||
FEATURE_UPDATED,
|
||||
FeatureArchivedEvent,
|
||||
@ -12,20 +13,38 @@ import {
|
||||
FeatureStrategyAddEvent,
|
||||
FeatureStrategyRemoveEvent,
|
||||
FeatureStrategyUpdateEvent,
|
||||
FeatureToggle,
|
||||
FeatureToggleDTO,
|
||||
FeatureToggleLegacy,
|
||||
FeatureToggleWithEnvironment,
|
||||
FeatureVariantEvent,
|
||||
IConstraint,
|
||||
IEventStore,
|
||||
IFeatureEnvironmentInfo,
|
||||
IFeatureEnvironmentStore,
|
||||
IFeatureOverview,
|
||||
IFeatureStrategy,
|
||||
IFeatureTagStore,
|
||||
IFeatureToggleClientStore,
|
||||
IFeatureToggleQuery,
|
||||
IFeatureToggleStore,
|
||||
IFlagResolver,
|
||||
IProjectStore,
|
||||
ISegment,
|
||||
IStrategyConfig,
|
||||
IUnleashConfig,
|
||||
IUnleashStores,
|
||||
IVariant,
|
||||
Saved,
|
||||
SKIP_CHANGE_REQUEST,
|
||||
Unsaved,
|
||||
WeightType,
|
||||
} from '../types';
|
||||
import { Logger } from '../logger';
|
||||
import BadDataError from '../error/bad-data-error';
|
||||
import NameExistsError from '../error/name-exists-error';
|
||||
import InvalidOperationError from '../error/invalid-operation-error';
|
||||
import { FOREIGN_KEY_VIOLATION } from '../error';
|
||||
import { FOREIGN_KEY_VIOLATION, OperationDeniedError } from '../error';
|
||||
import {
|
||||
constraintSchema,
|
||||
featureMetadataSchema,
|
||||
@ -37,32 +56,14 @@ import {
|
||||
FeatureConfigurationClient,
|
||||
IFeatureStrategiesStore,
|
||||
} from '../types/stores/feature-strategies-store';
|
||||
import {
|
||||
FeatureToggle,
|
||||
FeatureToggleDTO,
|
||||
FeatureToggleLegacy,
|
||||
FeatureToggleWithEnvironment,
|
||||
IConstraint,
|
||||
IFeatureEnvironmentInfo,
|
||||
IFeatureOverview,
|
||||
IFeatureStrategy,
|
||||
IFeatureToggleQuery,
|
||||
ISegment,
|
||||
IStrategyConfig,
|
||||
IVariant,
|
||||
WeightType,
|
||||
} from '../types/model';
|
||||
import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store';
|
||||
import { IFeatureToggleClientStore } from '../types/stores/feature-toggle-client-store';
|
||||
import {
|
||||
DATE_OPERATORS,
|
||||
DEFAULT_ENV,
|
||||
NUM_OPERATORS,
|
||||
SEMVER_OPERATORS,
|
||||
STRING_OPERATORS,
|
||||
} from '../util/constants';
|
||||
} from '../util';
|
||||
import { applyPatch, deepClone, Operation } from 'fast-json-patch';
|
||||
import { OperationDeniedError } from '../error/operation-denied-error';
|
||||
import {
|
||||
validateDate,
|
||||
validateLegalValues,
|
||||
@ -71,15 +72,10 @@ import {
|
||||
validateString,
|
||||
} from '../util/validators/constraint-types';
|
||||
import { IContextFieldStore } from 'lib/types/stores/context-field-store';
|
||||
import { Saved, Unsaved } from '../types/saved';
|
||||
import { SetStrategySortOrderSchema } from 'lib/openapi/spec/set-strategy-sort-order-schema';
|
||||
import { getDefaultStrategy } from '../util/feature-evaluator/helpers';
|
||||
import { AccessService } from './access-service';
|
||||
import { User } from '../server-impl';
|
||||
import {
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
SKIP_CHANGE_REQUEST,
|
||||
} from '../types/permissions';
|
||||
import NoAccessError from '../error/no-access-error';
|
||||
import { IFeatureProjectUserParams } from '../routes/admin-api/project/project-features';
|
||||
import { unique } from '../util/unique';
|
||||
@ -356,6 +352,7 @@ class FeatureToggleService {
|
||||
id: featureStrategy.id,
|
||||
name: featureStrategy.strategyName,
|
||||
title: featureStrategy.title,
|
||||
disabled: featureStrategy.disabled,
|
||||
constraints: featureStrategy.constraints || [],
|
||||
parameters: featureStrategy.parameters,
|
||||
segments: segments.map((segment) => segment.id) ?? [],
|
||||
@ -418,6 +415,7 @@ class FeatureToggleService {
|
||||
await this.featureStrategiesStore.createStrategyFeatureEnv({
|
||||
strategyName: strategyConfig.name,
|
||||
title: strategyConfig.title,
|
||||
disabled: strategyConfig.disabled,
|
||||
constraints: strategyConfig.constraints || [],
|
||||
parameters: strategyConfig.parameters || {},
|
||||
sortOrder: strategyConfig.sortOrder,
|
||||
@ -474,6 +472,7 @@ class FeatureToggleService {
|
||||
* @param updates
|
||||
* @param context - Which context does this strategy live in (projectId, featureName, environment)
|
||||
* @param userName - Human readable id of the user performing the update
|
||||
* @param user - Optional User object performing the action
|
||||
*/
|
||||
async updateStrategy(
|
||||
id: string,
|
||||
@ -677,6 +676,8 @@ class FeatureToggleService {
|
||||
name: strat.strategyName,
|
||||
constraints: strat.constraints,
|
||||
parameters: strat.parameters,
|
||||
title: strat.title,
|
||||
disabled: strat.disabled,
|
||||
sortOrder: strat.sortOrder,
|
||||
segments,
|
||||
});
|
||||
@ -752,10 +753,12 @@ class FeatureToggleService {
|
||||
async getClientFeatures(
|
||||
query?: IFeatureToggleQuery,
|
||||
includeIds?: boolean,
|
||||
includeDisabledStrategies?: boolean,
|
||||
): Promise<FeatureConfigurationClient[]> {
|
||||
const result = await this.featureToggleClientStore.getClient(
|
||||
query || {},
|
||||
includeIds,
|
||||
includeDisabledStrategies,
|
||||
);
|
||||
if (this.flagResolver.isEnabled('cleanClientApi')) {
|
||||
return result.map(
|
||||
@ -1013,6 +1016,7 @@ class FeatureToggleService {
|
||||
parameters: strategy.parameters,
|
||||
segments: [],
|
||||
title: strategy.title,
|
||||
disabled: strategy.disabled,
|
||||
};
|
||||
|
||||
if (segments && segments.length > 0) {
|
||||
|
@ -41,6 +41,7 @@ export class PlaygroundService {
|
||||
environment,
|
||||
},
|
||||
true,
|
||||
true,
|
||||
),
|
||||
this.segmentService.getActive(),
|
||||
]);
|
||||
|
@ -6,6 +6,7 @@ const strategySchema = joi
|
||||
.keys({
|
||||
name: nameType,
|
||||
title: joi.string().allow(null).allow('').optional(),
|
||||
disabled: joi.boolean().allow(null).optional(),
|
||||
editable: joi.boolean().default(true),
|
||||
deprecated: joi.boolean().default(false),
|
||||
description: joi.string().allow(null).allow('').optional(),
|
||||
|
@ -28,6 +28,7 @@ export interface IStrategyConfig {
|
||||
parameters?: { [key: string]: string };
|
||||
sortOrder?: number;
|
||||
title?: string | null;
|
||||
disabled?: boolean | null;
|
||||
}
|
||||
export interface IFeatureStrategy {
|
||||
id: string;
|
||||
@ -41,6 +42,7 @@ export interface IFeatureStrategy {
|
||||
createdAt?: Date;
|
||||
segments?: number[];
|
||||
title?: string | null;
|
||||
disabled?: boolean | null;
|
||||
}
|
||||
|
||||
export interface FeatureToggleDTO {
|
||||
|
@ -5,6 +5,7 @@ export interface IFeatureToggleClientStore {
|
||||
getClient(
|
||||
featureQuery: Partial<IFeatureToggleQuery>,
|
||||
includeStrategyIds?: boolean,
|
||||
includeDisabledStrategies?: boolean,
|
||||
): Promise<IFeatureToggleClient[]>;
|
||||
|
||||
// @Deprecated
|
||||
|
@ -109,12 +109,14 @@ export default class UnleashClient {
|
||||
name: strategySelector.name,
|
||||
id: strategySelector.id,
|
||||
title: strategySelector.title,
|
||||
disabled: strategySelector.disabled || false,
|
||||
parameters: strategySelector.parameters,
|
||||
...strategy.isEnabledWithConstraints(
|
||||
strategySelector.parameters,
|
||||
context,
|
||||
strategySelector.constraints,
|
||||
segments,
|
||||
strategySelector.disabled,
|
||||
),
|
||||
};
|
||||
},
|
||||
|
@ -13,6 +13,7 @@ export type SegmentForEvaluation = {
|
||||
export interface StrategyTransportInterface {
|
||||
name: string;
|
||||
title?: string;
|
||||
disabled?: boolean;
|
||||
parameters: any;
|
||||
constraints: Constraint[];
|
||||
segments?: number[];
|
||||
@ -107,9 +108,7 @@ export class Strategy {
|
||||
});
|
||||
|
||||
return {
|
||||
result: resolvedSegments.every(
|
||||
(segment) => segment.result === true,
|
||||
),
|
||||
result: resolvedSegments.every((segment) => segment.result),
|
||||
segments: resolvedSegments,
|
||||
};
|
||||
}
|
||||
@ -118,7 +117,8 @@ export class Strategy {
|
||||
parameters: unknown,
|
||||
context: Context,
|
||||
constraints: Iterable<Constraint>,
|
||||
segments: SegmentForEvaluation[],
|
||||
segments: Array<SegmentForEvaluation>,
|
||||
disabled?: boolean,
|
||||
): StrategyEvaluationResult {
|
||||
const constraintResults = this.checkConstraints(context, constraints);
|
||||
const enabledResult = this.isEnabled(parameters, context);
|
||||
@ -128,7 +128,10 @@ export class Strategy {
|
||||
constraintResults.result && enabledResult && segmentResults.result;
|
||||
|
||||
return {
|
||||
result: { enabled: overallResult, evaluationStatus: 'complete' },
|
||||
result: {
|
||||
enabled: disabled ? false : overallResult,
|
||||
evaluationStatus: 'complete',
|
||||
},
|
||||
constraints: constraintResults.constraints,
|
||||
segments: segmentResults.segments,
|
||||
};
|
||||
|
@ -0,0 +1,85 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function (db, callback) {
|
||||
db.runSql(
|
||||
`
|
||||
ALTER TABLE feature_strategies ADD COLUMN IF NOT EXISTS disabled BOOLEAN default false;
|
||||
|
||||
CREATE OR REPLACE VIEW features_view AS
|
||||
SELECT
|
||||
features.name as name,
|
||||
features.description as description,
|
||||
features.type as type,
|
||||
features.project as project,
|
||||
features.stale as stale,
|
||||
feature_environments.variants as variants,
|
||||
features.impression_data as impression_data,
|
||||
features.created_at as created_at,
|
||||
features.last_seen_at as last_seen_at,
|
||||
features.archived_at as archived_at,
|
||||
feature_environments.enabled as enabled,
|
||||
feature_environments.environment as environment,
|
||||
environments.name as environment_name,
|
||||
environments.type as environment_type,
|
||||
environments.sort_order as environment_sort_order,
|
||||
feature_strategies.id as strategy_id,
|
||||
feature_strategies.strategy_name as strategy_name,
|
||||
feature_strategies.parameters as parameters,
|
||||
feature_strategies.constraints as constraints,
|
||||
feature_strategies.sort_order as sort_order,
|
||||
fss.segment_id as segments,
|
||||
feature_strategies.title as strategy_title,
|
||||
feature_strategies.disabled as strategy_disabled
|
||||
FROM
|
||||
features
|
||||
LEFT JOIN feature_environments ON feature_environments.feature_name = features.name
|
||||
LEFT JOIN feature_strategies ON feature_strategies.feature_name = feature_environments.feature_name
|
||||
and feature_strategies.environment = feature_environments.environment
|
||||
LEFT JOIN environments ON feature_environments.environment = environments.name
|
||||
LEFT JOIN feature_strategy_segment as fss ON fss.feature_strategy_id = feature_strategies.id;
|
||||
`,
|
||||
callback,
|
||||
);
|
||||
};
|
||||
|
||||
exports.down = function (db, callback) {
|
||||
db.runSql(
|
||||
`
|
||||
DROP VIEW features_view;
|
||||
CREATE VIEW features_view AS
|
||||
SELECT
|
||||
features.name as name,
|
||||
features.description as description,
|
||||
features.type as type,
|
||||
features.project as project,
|
||||
features.stale as stale,
|
||||
feature_environments.variants as variants,
|
||||
features.impression_data as impression_data,
|
||||
features.created_at as created_at,
|
||||
features.last_seen_at as last_seen_at,
|
||||
features.archived_at as archived_at,
|
||||
feature_environments.enabled as enabled,
|
||||
feature_environments.environment as environment,
|
||||
environments.name as environment_name,
|
||||
environments.type as environment_type,
|
||||
environments.sort_order as environment_sort_order,
|
||||
feature_strategies.id as strategy_id,
|
||||
feature_strategies.strategy_name as strategy_name,
|
||||
feature_strategies.parameters as parameters,
|
||||
feature_strategies.constraints as constraints,
|
||||
feature_strategies.sort_order as sort_order,
|
||||
fss.segment_id as segments
|
||||
feature_strategies.title as strategy_title
|
||||
FROM
|
||||
features
|
||||
LEFT JOIN feature_environments ON feature_environments.feature_name = features.name
|
||||
LEFT JOIN feature_strategies ON feature_strategies.feature_name = feature_environments.feature_name
|
||||
and feature_strategies.environment = feature_environments.environment
|
||||
LEFT JOIN environments ON feature_environments.environment = environments.name
|
||||
LEFT JOIN feature_strategy_segment as fss ON fss.feature_strategy_id = feature_strategies.id;
|
||||
|
||||
ALTER TABLE feature_strategies DROP COLUMN IF EXISTS disabled;
|
||||
`,
|
||||
callback,
|
||||
);
|
||||
};
|
@ -121,7 +121,7 @@ describe('Playground API E2E', () => {
|
||||
|
||||
// assign strategies
|
||||
await Promise.all(
|
||||
(feature.strategies || []).map((strategy) =>
|
||||
(feature.strategies || []).map((strategy, index) =>
|
||||
database.stores.featureStrategiesStore.createStrategyFeatureEnv(
|
||||
{
|
||||
parameters: {},
|
||||
@ -130,6 +130,7 @@ describe('Playground API E2E', () => {
|
||||
featureName: feature.name,
|
||||
environment,
|
||||
strategyName: strategy.name,
|
||||
disabled: !!(index % 2),
|
||||
projectId: feature.project,
|
||||
},
|
||||
),
|
||||
|
@ -2823,3 +2823,102 @@ test('Should batch stale features', async () => {
|
||||
.expect(200);
|
||||
expect(body.stale).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should return disabled strategies', async () => {
|
||||
const toggle = { name: uuidv4(), impressionData: false };
|
||||
await app.request
|
||||
.post('/api/admin/projects/default/features')
|
||||
.send({
|
||||
name: toggle.name,
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
const { body: strategyOne } = await app.request
|
||||
.post(
|
||||
`/api/admin/projects/default/features/${toggle.name}/environments/default/strategies`,
|
||||
)
|
||||
.send({
|
||||
name: 'default',
|
||||
parameters: {
|
||||
userId: 'string',
|
||||
},
|
||||
disabled: true,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const { body: strategyTwo } = await app.request
|
||||
.post(
|
||||
`/api/admin/projects/default/features/${toggle.name}/environments/default/strategies`,
|
||||
)
|
||||
.send({
|
||||
name: 'gradualrollout',
|
||||
parameters: {
|
||||
userId: 'string',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const { body: strategies } = await app.request.get(
|
||||
`/api/admin/projects/default/features/${toggle.name}/environments/default/strategies`,
|
||||
);
|
||||
|
||||
expect(strategies[0].id).toBe(strategyOne.id);
|
||||
expect(strategies[0].disabled).toBe(strategyOne.disabled);
|
||||
expect(strategies[1].id).toBe(strategyTwo.id);
|
||||
expect(strategies[1].disabled).toBe(strategyTwo.disabled);
|
||||
});
|
||||
|
||||
test('should disable strategies in place', async () => {
|
||||
const toggle = { name: uuidv4(), impressionData: false };
|
||||
await app.request
|
||||
.post('/api/admin/projects/default/features')
|
||||
.send({
|
||||
name: toggle.name,
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
const { body: strategyOne } = await app.request
|
||||
.post(
|
||||
`/api/admin/projects/default/features/${toggle.name}/environments/default/strategies`,
|
||||
)
|
||||
.send({
|
||||
name: 'flexibleRollout',
|
||||
constraints: [],
|
||||
parameters: {
|
||||
rollout: '100',
|
||||
stickiness: 'default',
|
||||
groupId: 'some-new',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const { body: strategies } = await app.request.get(
|
||||
`/api/admin/projects/default/features/${toggle.name}/environments/default/strategies`,
|
||||
);
|
||||
|
||||
expect(strategies[0].id).toBe(strategyOne.id);
|
||||
expect(strategies[0].disabled).toBe(false);
|
||||
|
||||
const { body: updatedStrategyOne } = await app.request
|
||||
.put(
|
||||
`/api/admin/projects/default/features/${toggle.name}/environments/default/strategies/${strategyOne.id}`,
|
||||
)
|
||||
.send({
|
||||
name: 'flexibleRollout',
|
||||
constraints: [],
|
||||
disabled: true,
|
||||
parameters: {
|
||||
rollout: '100',
|
||||
stickiness: 'default',
|
||||
groupId: 'some-new',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const { body: updatedStrategies } = await app.request.get(
|
||||
`/api/admin/projects/default/features/${toggle.name}/environments/default/strategies`,
|
||||
);
|
||||
|
||||
expect(updatedStrategies[0].id).toBe(updatedStrategyOne.id);
|
||||
expect(updatedStrategies[0].disabled).toBe(updatedStrategyOne.disabled);
|
||||
});
|
||||
|
@ -1373,6 +1373,12 @@ The provider you choose for your addon dictates what properties the \`parameters
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
"disabled": {
|
||||
"description": "A toggle to disable the strategy. defaults to false. Disabled strategies are not evaluated or returned to the SDKs",
|
||||
"example": false,
|
||||
"nullable": true,
|
||||
"type": "boolean",
|
||||
},
|
||||
"name": {
|
||||
"description": "The name or type of strategy",
|
||||
"example": "flexibleRollout",
|
||||
@ -2089,6 +2095,12 @@ The provider you choose for your addon dictates what properties the \`parameters
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
"disabled": {
|
||||
"description": "A toggle to disable the strategy. defaults to false. Disabled strategies are not evaluated or returned to the SDKs",
|
||||
"example": false,
|
||||
"nullable": true,
|
||||
"type": "boolean",
|
||||
},
|
||||
"featureName": {
|
||||
"description": "The name or feature the strategy is attached to",
|
||||
"example": "myAwesomeFeature",
|
||||
@ -3220,6 +3232,12 @@ The provider you choose for your addon dictates what properties the \`parameters
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
"disabled": {
|
||||
"description": "The strategy's status. Disabled strategies are not evaluated",
|
||||
"example": false,
|
||||
"nullable": true,
|
||||
"type": "boolean",
|
||||
},
|
||||
"id": {
|
||||
"description": "The strategy's id.",
|
||||
"type": "string",
|
||||
@ -3315,6 +3333,7 @@ The provider you choose for your addon dictates what properties the \`parameters
|
||||
"segments",
|
||||
"constraints",
|
||||
"parameters",
|
||||
"disabled",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
@ -4617,6 +4636,12 @@ Stats are divided into current and previous **windows**.
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
"disabled": {
|
||||
"description": "A toggle to disable the strategy. defaults to true. Disabled strategies are not evaluated or returned to the SDKs",
|
||||
"example": false,
|
||||
"nullable": true,
|
||||
"type": "boolean",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
},
|
||||
@ -4626,6 +4651,12 @@ Stats are divided into current and previous **windows**.
|
||||
"sortOrder": {
|
||||
"type": "number",
|
||||
},
|
||||
"title": {
|
||||
"description": "A descriptive title for the strategy",
|
||||
"example": "Gradual Rollout 25-Prod",
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
|
@ -1105,3 +1105,104 @@ test('should not return all features', async () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should NOT evaluate disabled strategies when returning toggles', async () => {
|
||||
const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
|
||||
await createFeatureToggle({
|
||||
name: 'enabledFeature',
|
||||
enabled: true,
|
||||
strategies: [
|
||||
{
|
||||
name: 'flexibleRollout',
|
||||
constraints: [],
|
||||
parameters: {
|
||||
rollout: '100',
|
||||
stickiness: 'default',
|
||||
groupId: 'some-new',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
await createFeatureToggle({
|
||||
name: 'disabledFeature',
|
||||
enabled: false,
|
||||
strategies: [
|
||||
{
|
||||
name: 'flexibleRollout',
|
||||
constraints: [],
|
||||
disabled: true,
|
||||
parameters: {
|
||||
rollout: '100',
|
||||
stickiness: 'default',
|
||||
groupId: 'some-new',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
await createFeatureToggle({
|
||||
name: 'disabledFeature3',
|
||||
enabled: true,
|
||||
strategies: [
|
||||
{
|
||||
name: 'flexibleRollout',
|
||||
constraints: [],
|
||||
disabled: true,
|
||||
parameters: {
|
||||
rollout: '100',
|
||||
stickiness: 'default',
|
||||
groupId: 'some-new',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'flexibleRollout',
|
||||
constraints: [],
|
||||
disabled: false,
|
||||
parameters: {
|
||||
rollout: '0',
|
||||
stickiness: 'default',
|
||||
groupId: 'some-new',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
await createFeatureToggle({
|
||||
name: 'enabledFeature2',
|
||||
enabled: true,
|
||||
strategies: [
|
||||
{
|
||||
name: 'flexibleRollout',
|
||||
constraints: [],
|
||||
disabled: true,
|
||||
parameters: {
|
||||
rollout: '100',
|
||||
stickiness: 'default',
|
||||
groupId: 'some-new',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await app.request
|
||||
.get('/api/frontend')
|
||||
.set('Authorization', frontendToken.secret)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body).toEqual({
|
||||
toggles: [
|
||||
{
|
||||
name: 'enabledFeature',
|
||||
enabled: true,
|
||||
impressionData: false,
|
||||
variant: { enabled: false, name: 'disabled' },
|
||||
},
|
||||
{
|
||||
name: 'enabledFeature2',
|
||||
enabled: true,
|
||||
impressionData: false,
|
||||
variant: { enabled: false, name: 'disabled' },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -709,6 +709,8 @@ describe('the playground service (e2e)', () => {
|
||||
expect.arrayContaining([
|
||||
{
|
||||
...strategy,
|
||||
title: undefined,
|
||||
disabled: false,
|
||||
constraints:
|
||||
strategy.constraints ?? [],
|
||||
parameters:
|
||||
|
Loading…
Reference in New Issue
Block a user