1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00
unleash.unleash/src/lib/util/feature-evaluator/client.ts
andreas-unleash 097dd8ae56
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>
2023-04-21 12:09:07 +03:00

231 lines
7.2 KiB
TypeScript

import { Strategy } from './strategy';
import { FeatureInterface } from './feature';
import { RepositoryInterface } from './repository';
import {
Variant,
getDefaultVariant,
VariantDefinition,
selectVariant,
} from './variant';
import { Context } from './context';
import { SegmentForEvaluation } from './strategy/strategy';
import { PlaygroundStrategySchema } from 'lib/openapi/spec/playground-strategy-schema';
import { playgroundStrategyEvaluation } from '../../openapi/spec/playground-strategy-schema';
export type StrategyEvaluationResult = Pick<
PlaygroundStrategySchema,
'result' | 'segments' | 'constraints'
>;
export type FeatureStrategiesEvaluationResult = {
result: boolean | typeof playgroundStrategyEvaluation.unknownResult;
strategies: PlaygroundStrategySchema[];
};
export default class UnleashClient {
private repository: RepositoryInterface;
private strategies: Strategy[];
constructor(repository: RepositoryInterface, strategies: Strategy[]) {
this.repository = repository;
this.strategies = strategies || [];
this.strategies.forEach((strategy: Strategy) => {
if (
!strategy ||
!strategy.name ||
typeof strategy.name !== 'string' ||
!strategy.isEnabled ||
typeof strategy.isEnabled !== 'function'
) {
throw new Error('Invalid strategy data / interface');
}
});
}
private getStrategy(name: string): Strategy | undefined {
return this.strategies.find(
(strategy: Strategy): boolean => strategy.name === name,
);
}
isEnabled(
name: string,
context: Context,
fallback: Function,
): FeatureStrategiesEvaluationResult {
const feature = this.repository.getToggle(name);
return this.isFeatureEnabled(feature, context, fallback);
}
isFeatureEnabled(
feature: FeatureInterface,
context: Context,
fallback: Function,
): FeatureStrategiesEvaluationResult {
if (!feature) {
return fallback();
}
if (!Array.isArray(feature.strategies)) {
return {
result: false,
strategies: [],
};
}
if (feature.strategies.length === 0) {
return {
result: feature.enabled,
strategies: [],
};
}
const strategies = feature.strategies.map(
(strategySelector): PlaygroundStrategySchema => {
const getStrategy = () => {
// the application hostname strategy relies on external
// variables to calculate its result. As such, we can't
// evaluate it in a way that makes sense. So we'll
// use the 'unknown' strategy instead.
if (strategySelector.name === 'applicationHostname') {
return this.getStrategy('unknown');
}
return (
this.getStrategy(strategySelector.name) ??
this.getStrategy('unknown')
);
};
const strategy = getStrategy();
const segments =
strategySelector.segments
?.map(this.getSegment(this.repository))
.filter(Boolean) ?? [];
return {
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,
),
};
},
);
// Feature evaluation
const overallStrategyResult = () => {
// if at least one strategy is enabled, then the feature is enabled
if (
strategies.some((strategy) => strategy.result.enabled === true)
) {
return true;
}
// if at least one strategy is unknown, then the feature _may_ be enabled
if (
strategies.some(
(strategy) => strategy.result.enabled === 'unknown',
)
) {
return playgroundStrategyEvaluation.unknownResult;
}
return false;
};
const evalResults: FeatureStrategiesEvaluationResult = {
result: overallStrategyResult(),
strategies,
};
return evalResults;
}
getSegment(repo: RepositoryInterface) {
return (segmentId: number): SegmentForEvaluation | undefined => {
const segment = repo.getSegment(segmentId);
if (!segment) {
return undefined;
}
return {
name: segment.name,
id: segmentId,
constraints: segment.constraints,
};
};
}
getVariant(
name: string,
context: Context,
fallbackVariant?: Variant,
): Variant {
return this.resolveVariant(name, context, true, fallbackVariant);
}
// This function is intended to close an issue in the proxy where feature enabled
// state gets checked twice when resolving a variant with random stickiness and
// gradual rollout. This is not intended for general use, prefer getVariant instead
forceGetVariant(
name: string,
context: Context,
fallbackVariant?: Variant,
): Variant {
return this.resolveVariant(name, context, false, fallbackVariant);
}
private resolveVariant(
name: string,
context: Context,
checkToggle: boolean,
fallbackVariant?: Variant,
): Variant {
const fallback = fallbackVariant || getDefaultVariant();
const feature = this.repository.getToggle(name);
if (
typeof feature === 'undefined' ||
!feature.variants ||
!Array.isArray(feature.variants) ||
feature.variants.length === 0 ||
!feature.enabled
) {
return fallback;
}
let enabled = true;
if (checkToggle) {
enabled =
this.isFeatureEnabled(feature, context, () =>
fallbackVariant ? fallbackVariant.enabled : false,
).result === true;
if (!enabled) {
return fallback;
}
}
const variant: VariantDefinition | null = selectVariant(
feature,
context,
);
if (variant === null) {
return fallback;
}
return {
name: variant.name,
payload: variant.payload,
enabled: !checkToggle || enabled,
};
}
}