1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-28 00:06:53 +01:00
unleash.unleash/src/lib/util/feature-evaluator/client.ts

216 lines
6.4 KiB
TypeScript
Raw Normal View History

feat(#1873/playground): Return detailed information on feature toggle evaluation (#1839) * Feat: return reasons why a feature evaluated to true or false Note: this is very rough and just straight ripped from the nodejs client. It will need a lot of work, but is a good place to start * Feat: add suggested shape for new payload * Chore: minor cleanup * Wip: make server compile again * Remove unused schema ref * Export new schemas * Chore: fix some tests to use sub property * Fix: fix some tests * Refactor: rename some variables, uncomment some stuff * Add segments type to bootstrap options * Add segments capability to offline feature evaluator * Fix function calls after turning params into an option abject * Feat: test strategy order, etc * Feat: add test to check that all strats are returned correctly * Feat: allow you to include strategy ids in clients * Wip: hook up segments in the offline client. Note: compared to regular clients, they still fail * Feat: add segments validation * Fix: fix test case invariant. * Chore: revert to returning only `boolean` from strategies. This _should_ make it work with custom strategies too 🤞 * Feat: make more properties of the returned feature required * Wip: add some comments and unfinished tests for edge cases * Feat: add `isEnabledInCurrentEnvironment` prop * Feat: consider more strategy failure cases * Feat: test that isenabledinenvironment matches expectations * Feat: add unknown strategies * Fix: fix property access typo * Feat: add unknown strategy for fallback purposes * Feat: test edge case: all unknown strategies * Feat: add custom strategy to arbitrary * Feat: test that features can be true, even if not enabled in env * Chore: add some comments * Wip: fix sdk tests * Remove comments, improve test logging * Feat: add descriptions and examples to playground feature schema * Switch `examples` for `example` * Update schemas with descriptions and examples * Fix: update snapshot * Fix: openapi example * Fix: merge issues * Fix: fix issue where feature evaluation state was wrong * Chore: update openapi spec * Fix: fix broken offline client tests * Refactor: move schemas into separate files * Refactor: remove "reason" for incomplete evaluation. The only instances where evaluation is incomplete is when we don't know what the strategy is. * Refactor: move unleash node client into test and dev dependencies * Wip: further removal of stuff * Chore: remove a bunch of code that we don't use * Chore: remove comment * Chore: remove unused code * Fix: fix some prettier errors * Type parameters in strategies to avoid `any` * Fix: remove commented out code * Feat: make `id` required on playground strategies * Chore: remove redundant type * Fix: remove redundant if and fix fallback evaluation * Refactor: reduce nesting and remove duplication * Fix: remove unused helper function * Refactor: type `parameters` as `unknown` * Chore: remove redundant comment * Refactor: move constraint code into a separate file * Refactor: rename `unleash` -> `feature-evaluator` * Rename class `Unleash` -> `FeatureEvaluator` * Refactor: remove this.ready and sync logic from feature evaluator * Refactor: remove unused code, rename config type * Refactor: remove event emission from the Unleash client * Remove unlistened-for events in feature evaluator * Refactor: make offline client synchronous; remove code * Fix: update openapi snapshot after adding required strategy ids * Feat: change `strategies` format. This commit changes the format of a playground feature's `strategies` properties from a list of strategies to an object with properties `result` and `data`. It looks a bit like this: ```ts type Strategies = { result: boolean | "unknown", data: Strategy[] } ``` The reason is that this allows us to avoid the breaking change that was previously suggested in the PR: `feature.isEnabled` used to be a straight boolean. Then, when we found out we couldn't necessarily evaluate all strategies (custom strats are hard!) we changed it to `boolean | 'unevaluated'`. However, this is confusing on a few levels as the playground results are no longer the same as the SDK would be, nor are they strictly boolean anymore. This change reverts the `isEnabled` functionality to what it was before (so it's always a mirror of what the SDK would show). The equivalent of `feature.isEnabled === 'unevaluated'` now becomes `feature.isEnabled && strategy.result === 'unknown'`. * Fix: Fold long string descriptions over multiple lines. * Fix: update snapshot after adding line breaks to descriptions
2022-08-04 15:41:52 +02:00
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 strategy =
this.getStrategy(strategySelector.name) ??
this.getStrategy('unknown');
const segments =
strategySelector.segments
?.map(this.getSegment(this.repository))
.filter(Boolean) ?? [];
return {
name: strategySelector.name,
id: strategySelector.id,
parameters: strategySelector.parameters,
...strategy.isEnabledWithConstraints(
strategySelector.parameters,
context,
strategySelector.constraints,
segments,
),
};
},
);
// 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,
};
}
}