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

fix: upgrade unleash-client to v5.3.0 (#5800)

This commit is contained in:
Tymoteusz Czech 2024-01-24 09:12:07 +01:00 committed by GitHub
parent 3957abf0f6
commit 68eb3dec07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 230 additions and 81 deletions

View File

@ -23,6 +23,7 @@ test('should render environment table', async () => {
type: 'string',
value: 'variantValue',
},
feature_enabled: true,
},
environment: 'dev',
context: {

View File

@ -14,6 +14,8 @@ import type { AdvancedPlaygroundEnvironmentFeatureSchemaVariantPayload } from '.
export type AdvancedPlaygroundEnvironmentFeatureSchemaVariant = {
/** Whether the variant is enabled or not. If the feature is disabled or if it doesn't have variants, this property will be `false` */
enabled: boolean;
/** Whether the feature is enabled or not. If the feature is disabled, this property will be `false` */
feature_enabled?: boolean;
/** The variant's name. If there is no variant or if the toggle is disabled, this will be `disabled` */
name: string;
/** An optional payload attached to the variant. */

View File

@ -11,6 +11,8 @@ import type { ProxyFeatureSchemaVariantPayload } from './proxyFeatureSchemaVaria
export type ProxyFeatureSchemaVariant = {
/** Whether the variant is enabled or not. */
enabled: boolean;
/** Whether the feature is enabled or not. */
feature_enabled?: boolean;
/** The variants name. Is unique for this feature toggle */
name: string;
/** Extra data configured for this variant */

View File

@ -15,4 +15,5 @@ export const ProxyFeatureSchemaVariantPayloadType = {
json: 'json',
csv: 'csv',
string: 'string',
number: 'number',
} as const;

View File

@ -11,6 +11,8 @@ import type { VariantFlagSchemaPayload } from './variantFlagSchemaPayload';
export interface VariantFlagSchema {
/** Whether the variant is enabled or not. */
enabled?: boolean;
/** Whether the feature is enabled or not. */
feature_enabled?: boolean;
/** The name of the variant. Will always be disabled if `enabled` is false. */
name?: string;
/** Additional data associated with this variant. */

View File

@ -15,4 +15,5 @@ export const VariantFlagSchemaPayloadType = {
string: 'string',
json: 'json',
csv: 'csv',
number: 'number',
} as const;

View File

@ -145,7 +145,7 @@
"stoppable": "^1.1.0",
"ts-toolbelt": "^9.6.0",
"type-is": "^1.6.18",
"unleash-client": "5.0.0",
"unleash-client": "5.3.1",
"uuid": "^9.0.0"
},
"devDependencies": {

View File

@ -120,6 +120,7 @@ test('advanced playground evaluation with parent dependency', async () => {
expect(child.variant).toEqual({
name: 'disabled',
enabled: false,
feature_enabled: false,
});
expect(parent.hasUnsatisfiedDependency).toBe(false);
expect(parent.isEnabled).toBe(false);
@ -298,18 +299,17 @@ test('advanced playground evaluation happy path', async () => {
});
});
test('show matching variant from variants selection only for enabled toggles', async () => {
const variants = [
{
stickiness: 'random',
name: 'a',
weight: 1000,
payload: {
type: 'string',
value: 'aval',
},
weightType: 'variable',
const variant = {
stickiness: 'random',
name: 'a',
weight: 1000,
payload: {
type: 'string',
value: 'aval',
},
];
weightType: 'variable',
};
await createFeatureToggleWithStrategy(
'test-playground-feature-with-variants',
{
@ -320,7 +320,7 @@ test('show matching variant from variants selection only for enabled toggles', a
stickiness: 'random',
groupId: 'test-playground-feature-with-variants',
},
variants,
variants: [variant],
},
);
@ -347,7 +347,7 @@ test('show matching variant from variants selection only for enabled toggles', a
enabledFeatures.forEach((feature) => {
expect(feature.variant?.name).toBe('a');
expect(feature.variants).toMatchObject(variants);
expect(feature.variants).toMatchObject([variant]);
});
disabledFeatures.forEach((feature) => {
expect(feature.variant?.name).toBe('disabled');

View File

@ -11,17 +11,18 @@ import { Context } from './context';
import { SegmentForEvaluation } from './strategy/strategy';
import { PlaygroundStrategySchema } from '../../../openapi/spec/playground-strategy-schema';
import { playgroundStrategyEvaluation } from '../../../openapi/spec/playground-strategy-schema';
export type StrategyEvaluationResult = Pick<
PlaygroundStrategySchema,
'result' | 'segments' | 'constraints'
>;
import { randomId } from '../../../util';
export type EvaluatedPlaygroundStrategy = Omit<
PlaygroundStrategySchema,
'links'
>;
export type StrategyEvaluationResult = Pick<
EvaluatedPlaygroundStrategy,
'result' | 'segments' | 'constraints'
>;
export type FeatureStrategiesEvaluationResult = {
result: boolean | typeof playgroundStrategyEvaluation.unknownResult;
variant?: Variant;
@ -144,26 +145,32 @@ export default class UnleashClient {
const strategies = feature.strategies.map(
(strategySelector): EvaluatedPlaygroundStrategy => {
const getStrategy = () => {
const getStrategy = (): Strategy => {
// assume that 'unknown' strategy is always present
const unknownStrategy = this.getStrategy(
'unknown',
) as Strategy;
// 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 unknownStrategy;
}
return (
this.getStrategy(strategySelector.name) ??
this.getStrategy('unknown')
unknownStrategy
);
};
const strategy = getStrategy();
const segments =
strategySelector.segments
(strategySelector.segments
?.map(this.getSegment(this.repository))
.filter(Boolean) ?? [];
.filter(Boolean) as SegmentForEvaluation[]) ?? [];
const evaluationResult = strategy.isEnabledWithConstraints(
strategySelector.parameters,
@ -176,7 +183,7 @@ export default class UnleashClient {
return {
name: strategySelector.name,
id: strategySelector.id,
id: strategySelector.id || randomId(),
title: strategySelector.title,
disabled: strategySelector.disabled || false,
parameters: strategySelector.parameters,
@ -189,7 +196,7 @@ export default class UnleashClient {
const overallStrategyResult = (): [
boolean | typeof playgroundStrategyEvaluation.unknownResult,
VariantDefinition[] | undefined,
Variant | undefined | null,
Variant | undefined,
] => {
// if at least one strategy is enabled, then the feature is enabled
const enabledStrategy = strategies.find(
@ -202,7 +209,7 @@ export default class UnleashClient {
return [
true,
enabledStrategy.result.variants,
enabledStrategy.result.variant,
enabledStrategy.result.variant || undefined,
];
}
@ -284,7 +291,11 @@ export default class UnleashClient {
'result' | 'variant'
>,
): Variant {
const fallback = fallbackVariant || getDefaultVariant();
const fallback = {
feature_enabled: false,
featureEnabled: false,
...(fallbackVariant || getDefaultVariant()),
};
const feature = this.repository.getToggle(name);
if (
@ -294,13 +305,14 @@ export default class UnleashClient {
return fallback;
}
let enabled = true;
const result =
forcedResult ??
this.isFeatureEnabled(feature, context, () =>
fallbackVariant ? fallbackVariant.enabled : false,
);
enabled = result.result === true;
const enabled = result.result === true;
fallback.feature_enabled = fallbackVariant?.feature_enabled ?? enabled;
fallback.featureEnabled = fallback.feature_enabled;
const strategyVariant = result.variant;
if (enabled && strategyVariant) {
return strategyVariant;
@ -330,6 +342,8 @@ export default class UnleashClient {
name: variant.name,
payload: variant.payload,
enabled,
feature_enabled: true,
featureEnabled: true,
};
}
}

View File

@ -11,6 +11,7 @@ import {
resolveBootstrapProvider,
} from './repository/bootstrap-provider';
import { StorageProvider } from './repository/storage-provider';
import InMemStorageProvider from './repository/storage-provider-in-mem';
export { Strategy };
@ -41,7 +42,7 @@ export class FeatureEvaluator {
strategies = [],
repository,
bootstrap = { data: [] },
storageProvider,
storageProvider = new InMemStorageProvider(),
}: FeatureEvaluatorConfig) {
this.staticContext = { appName, environment };
@ -52,7 +53,7 @@ export class FeatureEvaluator {
new Repository({
appName,
bootstrapProvider,
storageProvider: storageProvider,
storageProvider,
});
// setup client

View File

@ -26,6 +26,11 @@ export interface Variant {
name: string;
enabled: boolean;
payload?: Payload;
featureEnabled?: boolean;
/**
* @deprecated use featureEnabled
*/
feature_enabled?: boolean;
}
export function getDefaultVariant(): Variant {

View File

@ -27,7 +27,7 @@ export const offlineUnleashClientNode = async ({
storageProvider: new InMemStorageProviderNode(),
bootstrap: {
data: mapFeaturesForClient(features),
segments: mapSegmentsForClient(segments),
segments: mapSegmentsForClient(segments || []),
},
});

View File

@ -5,8 +5,9 @@ import { Segment } from './feature-evaluator/strategy/strategy';
import { ISegment } from '../../types/model';
import { serializeDates } from '../../types/serialize-dates';
import { Operator } from './feature-evaluator/constraint';
import { FeatureInterface } from 'unleash-client/lib/feature';
import { PayloadType } from 'unleash-client';
import { FeatureInterface } from 'unleash-client/lib/feature';
import { FeatureInterface as PlaygroundFeatureInterface } from './feature-evaluator/feature';
type NonEmptyList<T> = [T, ...T[]];
@ -28,6 +29,8 @@ export const mapFeaturesForClient = (
strategies: feature.strategies.map((strategy) => ({
parameters: {},
...strategy,
title: strategy.title ?? undefined,
disabled: strategy.disabled ?? false,
variants: (strategy.variants || []).map((variant) => ({
...variant,
payload: variant.payload && {
@ -35,12 +38,13 @@ export const mapFeaturesForClient = (
type: variant.payload.type as PayloadType,
},
})),
constraints: strategy.constraints?.map((constraint) => ({
inverted: false,
values: [],
...constraint,
operator: constraint.operator as unknown as Operator,
})),
constraints:
strategy.constraints?.map((constraint) => ({
inverted: false,
values: [],
...constraint,
operator: constraint.operator as unknown as Operator,
})) || [],
})),
dependencies: feature.dependencies,
}));
@ -65,8 +69,11 @@ export const offlineUnleashClient = async ({
appName: context.appName,
storageProvider: new InMemStorageProvider(),
bootstrap: {
data: mapFeaturesForClient(features),
segments: mapSegmentsForClient(segments),
// FIXME: mismatch between playground and proxy types
data: mapFeaturesForClient(
features,
) as PlaygroundFeatureInterface[],
segments: mapSegmentsForClient(segments || []),
},
});

View File

@ -203,6 +203,17 @@ export class PlaygroundService {
feature.enabled &&
!hasUnsatisfiedDependency;
const variant = {
...(isEnabled
? client.forceGetVariant(
feature.name,
strategyEvaluationResult,
clientContext,
)
: getDefaultVariant()),
feature_enabled: isEnabled,
};
return {
isEnabled,
isEnabledInCurrentEnvironment: feature.enabled,
@ -212,13 +223,7 @@ export class PlaygroundService {
data: strategyEvaluationResult.strategies,
},
projectId: featureProject[feature.name],
variant: isEnabled
? client.forceGetVariant(
feature.name,
strategyEvaluationResult,
clientContext,
)
: getDefaultVariant(),
variant,
name: feature.name,
environment,
context,

View File

@ -127,6 +127,11 @@ export const advancedPlaygroundEnvironmentFeatureSchema = {
},
},
},
feature_enabled: {
type: 'boolean',
description:
'Whether the feature is enabled or not. If the feature is disabled, this property will be `false`',
},
},
nullable: true,
example: { name: 'green', enabled: true },

View File

@ -121,6 +121,17 @@ export const playgroundFeatureSchema = {
},
},
},
feature_enabled: {
type: 'boolean',
description: 'Use `featureEnabled` instead.',
example: true,
},
featureEnabled: {
deprecated: true,
type: 'boolean',
description: 'Whether the feature is enabled or not.',
example: true,
},
},
nullable: true,
example: { name: 'green', enabled: true },

View File

@ -50,7 +50,7 @@ export const proxyFeatureSchema = {
type: {
type: 'string',
description: 'The format of the payload.',
enum: ['json', 'csv', 'string'],
enum: ['json', 'csv', 'string', 'number'],
},
value: {
type: 'string',
@ -58,6 +58,17 @@ export const proxyFeatureSchema = {
},
},
},
feature_enabled: {
type: 'boolean',
description: 'Whether the feature is enabled or not.',
example: true,
},
featureEnabled: {
deprecated: true,
type: 'boolean',
description: 'Use `feature_enabled` instead.',
example: true,
},
},
},
},

View File

@ -25,7 +25,7 @@ export const variantFlagSchema = {
type: {
description: 'The type of data contained.',
type: 'string',
enum: ['string', 'json', 'csv'],
enum: ['string', 'json', 'csv', 'number'],
example: 'json',
},
value: {
@ -35,6 +35,17 @@ export const variantFlagSchema = {
},
},
},
feature_enabled: {
type: 'boolean',
description: 'Whether the feature is enabled or not.',
example: true,
},
featureEnabled: {
deprecated: true,
type: 'boolean',
description: 'Use `feature_enabled` instead.',
example: true,
},
},
components: {},
} as const;

View File

@ -449,13 +449,23 @@ test('should filter features by enabled/disabled', async () => {
name: 'enabledFeature1',
enabled: true,
impressionData: false,
variant: { enabled: false, name: 'disabled' },
variant: {
enabled: false,
name: 'disabled',
feature_enabled: true,
featureEnabled: true,
},
},
{
name: 'enabledFeature2',
enabled: true,
impressionData: false,
variant: { enabled: false, name: 'disabled' },
variant: {
enabled: false,
name: 'disabled',
feature_enabled: true,
featureEnabled: true,
},
},
],
});
@ -486,7 +496,12 @@ test('should filter features by strategies', async () => {
name: 'featureWithMultipleStrategies',
enabled: true,
impressionData: false,
variant: { enabled: false, name: 'disabled' },
variant: {
enabled: false,
name: 'disabled',
feature_enabled: true,
featureEnabled: true,
},
},
],
});
@ -629,7 +644,12 @@ test('should filter features by project', async () => {
name: 'featureInProjectDefault',
enabled: true,
impressionData: false,
variant: { enabled: false, name: 'disabled' },
variant: {
enabled: false,
name: 'disabled',
feature_enabled: true,
featureEnabled: true,
},
},
],
});
@ -646,7 +666,12 @@ test('should filter features by project', async () => {
name: 'featureInProjectA',
enabled: true,
impressionData: false,
variant: { enabled: false, name: 'disabled' },
variant: {
enabled: false,
name: 'disabled',
feature_enabled: true,
featureEnabled: true,
},
},
],
});
@ -663,13 +688,23 @@ test('should filter features by project', async () => {
name: 'featureInProjectA',
enabled: true,
impressionData: false,
variant: { enabled: false, name: 'disabled' },
variant: {
enabled: false,
name: 'disabled',
feature_enabled: true,
featureEnabled: true,
},
},
{
name: 'featureInProjectB',
enabled: true,
impressionData: false,
variant: { enabled: false, name: 'disabled' },
variant: {
enabled: false,
name: 'disabled',
feature_enabled: true,
featureEnabled: true,
},
},
],
});
@ -743,7 +778,12 @@ test('should filter features by environment', async () => {
name: 'featureInEnvironmentDefault',
enabled: true,
impressionData: false,
variant: { enabled: false, name: 'disabled' },
variant: {
enabled: false,
name: 'disabled',
feature_enabled: true,
featureEnabled: true,
},
},
],
});
@ -760,7 +800,12 @@ test('should filter features by environment', async () => {
name: 'featureInEnvironmentA',
enabled: true,
impressionData: false,
variant: { enabled: false, name: 'disabled' },
variant: {
enabled: false,
name: 'disabled',
feature_enabled: true,
featureEnabled: true,
},
},
],
});
@ -777,7 +822,12 @@ test('should filter features by environment', async () => {
name: 'featureInEnvironmentB',
enabled: true,
impressionData: false,
variant: { enabled: false, name: 'disabled' },
variant: {
enabled: false,
name: 'disabled',
feature_enabled: true,
featureEnabled: true,
},
},
],
});
@ -1040,7 +1090,12 @@ test('should evaluate strategies when returning toggles', async () => {
name: 'enabledFeature',
enabled: true,
impressionData: false,
variant: { enabled: false, name: 'disabled' },
variant: {
enabled: false,
name: 'disabled',
feature_enabled: true,
featureEnabled: true,
},
},
],
});
@ -1095,13 +1150,23 @@ test('should not return all features', async () => {
name: 'enabledFeature1',
enabled: true,
impressionData: false,
variant: { enabled: false, name: 'disabled' },
variant: {
enabled: false,
name: 'disabled',
feature_enabled: true,
featureEnabled: true,
},
},
{
name: 'enabledFeature2',
enabled: true,
impressionData: false,
variant: { enabled: false, name: 'disabled' },
variant: {
enabled: false,
name: 'disabled',
feature_enabled: true,
featureEnabled: true,
},
},
],
});
@ -1197,7 +1262,12 @@ test('should NOT evaluate disabled strategies when returning toggles', async ()
name: 'enabledFeature',
enabled: true,
impressionData: false,
variant: { enabled: false, name: 'disabled' },
variant: {
enabled: false,
name: 'disabled',
feature_enabled: true,
featureEnabled: true,
},
},
],
});

View File

@ -177,13 +177,12 @@ export const seedDatabaseForPlaygroundTest = async (
};
describe('the playground service (e2e)', () => {
const isDisabledVariant = ({
name,
enabled,
}: {
name: string;
enabled: boolean;
}) => name === 'disabled' && !enabled;
const isDisabledVariant = (
variant?: {
name: string;
enabled: boolean;
} | null,
) => variant?.name === 'disabled' && !variant?.enabled;
const insertAndEvaluateFeatures = async ({
features,
@ -290,12 +289,12 @@ describe('the playground service (e2e)', () => {
ctx.log(JSON.stringify(enabledStateMatches));
ctx.log(
JSON.stringify(
feature.variant.name === 'disabled',
feature.variant?.name === 'disabled',
),
);
ctx.log(
JSON.stringify(
feature.variant.enabled === false,
feature.variant?.enabled === false,
),
);
return (
@ -704,7 +703,7 @@ describe('the playground service (e2e)', () => {
},
);
feature.strategies.forEach(
feature.strategies?.forEach(
({ segments, ...strategy }) => {
expect(cleanedReceivedStrategies).toEqual(
expect.arrayContaining([
@ -845,7 +844,7 @@ describe('the playground service (e2e)', () => {
features: features.map((feature) => ({
...feature,
// remove any constraints and use a name that doesn't exist
strategies: feature.strategies.map(
strategies: feature.strategies?.map(
(strategy) => ({
...strategy,
name: 'bogus-strategy',
@ -907,7 +906,7 @@ describe('the playground service (e2e)', () => {
features: features.map((feature) => ({
...feature,
// use a constraint that will never be true
strategies: feature.strategies.map(
strategies: feature.strategies?.map(
(strategy) => ({
...strategy,
name: 'bogusStrategy',
@ -1223,6 +1222,7 @@ describe('the playground service (e2e)', () => {
expect(feature.variant).toEqual({
name: 'disabled',
enabled: false,
feature_enabled: false,
});
}
});

View File

@ -7134,10 +7134,10 @@ universalify@^0.2.0:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0"
integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==
unleash-client@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/unleash-client/-/unleash-client-5.0.0.tgz#b6725723b38f5572b4b1c5261c1bb2254148ec58"
integrity sha512-9x3SOpHTnMDY0CosKwy/0Hi9gIjw65+i2fsC76bvYaUIVTlqDoPdCfosBokNIZ/IjCF0TjEgSZefSWNGY5SC9A==
unleash-client@5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/unleash-client/-/unleash-client-5.3.1.tgz#8647ea5f8905f21e5fdcb87a79ce91c27a92b605"
integrity sha512-S5WtDNJa9j/JC+tXB/LoBNDMwhtuQ5CxFWge8igoVSspyyypcrKtyd98TSODBeyfzKW0daCyLmE7Pr+jDfROLQ==
dependencies:
ip "^1.1.8"
make-fetch-happen "^10.2.1"