mirror of
https://github.com/Unleash/unleash.git
synced 2024-11-01 19:07:38 +01:00
e55ad1a21e
* 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
279 lines
9.5 KiB
TypeScript
279 lines
9.5 KiB
TypeScript
import fc, { Arbitrary } from 'fast-check';
|
|
|
|
import { ALL_OPERATORS } from '../lib/util/constants';
|
|
import { ClientFeatureSchema } from '../lib/openapi/spec/client-feature-schema';
|
|
import { IVariant, WeightType } from '../lib/types/model';
|
|
import { FeatureStrategySchema } from '../lib/openapi/spec/feature-strategy-schema';
|
|
import { ConstraintSchema } from 'lib/openapi/spec/constraint-schema';
|
|
import { SegmentSchema } from 'lib/openapi/spec/segment-schema';
|
|
|
|
export const urlFriendlyString = (): Arbitrary<string> =>
|
|
fc
|
|
.array(
|
|
fc.oneof(
|
|
fc.integer({ min: 0x30, max: 0x39 }).map(String.fromCharCode), // numbers
|
|
fc.integer({ min: 0x41, max: 0x5a }).map(String.fromCharCode), // UPPERCASE LETTERS
|
|
fc.integer({ min: 0x61, max: 0x7a }).map(String.fromCharCode), // lowercase letters
|
|
fc.constantFrom('-', '_', '~', '.'), // rest
|
|
fc.lorem({ maxCount: 1 }), // random words for more 'realistic' names
|
|
),
|
|
{ minLength: 1 },
|
|
)
|
|
.map((arr) => arr.join(''));
|
|
|
|
export const commonISOTimestamp = (): Arbitrary<string> =>
|
|
fc
|
|
.date({
|
|
min: new Date('1900-01-01T00:00:00.000Z'),
|
|
max: new Date('9999-12-31T23:59:59.999Z'),
|
|
})
|
|
.map((timestamp) => timestamp.toISOString());
|
|
|
|
export const strategyConstraint = (): Arbitrary<ConstraintSchema> =>
|
|
fc.record({
|
|
contextName: urlFriendlyString(),
|
|
operator: fc.constantFrom(...ALL_OPERATORS),
|
|
caseInsensitive: fc.boolean(),
|
|
inverted: fc.boolean(),
|
|
values: fc.array(fc.string()),
|
|
value: fc.string(),
|
|
});
|
|
|
|
const strategyConstraints = (): Arbitrary<ConstraintSchema[]> =>
|
|
fc.array(strategyConstraint());
|
|
|
|
export const strategy = (
|
|
name: string,
|
|
parameters?: Arbitrary<Record<string, string>>,
|
|
): Arbitrary<FeatureStrategySchema> =>
|
|
parameters
|
|
? fc.record(
|
|
{
|
|
name: fc.constant(name),
|
|
id: fc.uuid(),
|
|
parameters,
|
|
segments: fc.uniqueArray(fc.integer({ min: 1 })),
|
|
constraints: strategyConstraints(),
|
|
},
|
|
{ requiredKeys: ['name', 'parameters', 'id'] },
|
|
)
|
|
: fc.record(
|
|
{
|
|
id: fc.uuid(),
|
|
name: fc.constant(name),
|
|
segments: fc.uniqueArray(fc.integer({ min: 1 })),
|
|
constraints: strategyConstraints(),
|
|
},
|
|
{ requiredKeys: ['name', 'id'] },
|
|
);
|
|
|
|
export const segment = (): Arbitrary<SegmentSchema> =>
|
|
fc.record({
|
|
id: fc.integer({ min: 1 }),
|
|
name: urlFriendlyString(),
|
|
constraints: strategyConstraints(),
|
|
});
|
|
|
|
export const strategies = (): Arbitrary<FeatureStrategySchema[]> =>
|
|
fc.uniqueArray(
|
|
fc.oneof(
|
|
strategy('default'),
|
|
strategy(
|
|
'flexibleRollout',
|
|
fc.record({
|
|
groupId: fc.lorem({ maxCount: 1 }),
|
|
rollout: fc.nat({ max: 100 }).map(String),
|
|
stickiness: fc.constantFrom(
|
|
'default',
|
|
'userId',
|
|
'sessionId',
|
|
),
|
|
}),
|
|
),
|
|
strategy(
|
|
'applicationHostname',
|
|
fc.record({
|
|
hostNames: fc
|
|
.uniqueArray(fc.domain())
|
|
.map((domains) => domains.join(',')),
|
|
}),
|
|
),
|
|
|
|
strategy(
|
|
'userWithId',
|
|
fc.record({
|
|
userIds: fc
|
|
.uniqueArray(fc.emailAddress())
|
|
.map((ids) => ids.join(',')),
|
|
}),
|
|
),
|
|
strategy(
|
|
'remoteAddress',
|
|
fc.record({
|
|
IPs: fc.uniqueArray(fc.ipV4()).map((ips) => ips.join(',')),
|
|
}),
|
|
),
|
|
strategy(
|
|
'custom-strategy',
|
|
fc.record({
|
|
customParam: fc
|
|
.uniqueArray(fc.lorem())
|
|
.map((words) => words.join(',')),
|
|
}),
|
|
),
|
|
),
|
|
{ selector: (generatedStrategy) => generatedStrategy.id },
|
|
);
|
|
|
|
export const variant = (): Arbitrary<IVariant> =>
|
|
fc.record(
|
|
{
|
|
name: urlFriendlyString(),
|
|
weight: fc.nat({ max: 1000 }),
|
|
weightType: fc.constant(WeightType.VARIABLE),
|
|
stickiness: fc.constant('default'),
|
|
payload: fc.option(
|
|
fc.oneof(
|
|
fc.record({
|
|
type: fc.constant('json' as 'json'),
|
|
value: fc.json(),
|
|
}),
|
|
fc.record({
|
|
type: fc.constant('csv' as 'csv'),
|
|
value: fc
|
|
.array(fc.lorem())
|
|
.map((words) => words.join(',')),
|
|
}),
|
|
fc.record({
|
|
type: fc.constant('string' as 'string'),
|
|
value: fc.string(),
|
|
}),
|
|
),
|
|
{ nil: undefined },
|
|
),
|
|
},
|
|
{ requiredKeys: ['name', 'weight', 'weightType', 'stickiness'] },
|
|
);
|
|
|
|
export const variants = (): Arbitrary<IVariant[]> =>
|
|
fc
|
|
.uniqueArray(variant(), {
|
|
maxLength: 1000,
|
|
selector: (variantInstance) => variantInstance.name,
|
|
})
|
|
.map((allVariants) =>
|
|
allVariants.map((variantInstance) => ({
|
|
...variantInstance,
|
|
weight: Math.round(1000 / allVariants.length),
|
|
})),
|
|
);
|
|
|
|
export const clientFeature = (name?: string): Arbitrary<ClientFeatureSchema> =>
|
|
fc.record(
|
|
{
|
|
name: name ? fc.constant(name) : urlFriendlyString(),
|
|
type: fc.constantFrom(
|
|
'release',
|
|
'kill-switch',
|
|
'experiment',
|
|
'operational',
|
|
'permission',
|
|
),
|
|
description: fc.lorem(),
|
|
project: urlFriendlyString(),
|
|
enabled: fc.boolean(),
|
|
createdAt: commonISOTimestamp(),
|
|
lastSeenAt: commonISOTimestamp(),
|
|
stale: fc.boolean(),
|
|
impressionData: fc.option(fc.boolean()),
|
|
strategies: strategies(),
|
|
variants: variants(),
|
|
},
|
|
{ requiredKeys: ['name', 'enabled', 'project', 'strategies'] },
|
|
);
|
|
|
|
export const clientFeatures = (constraints?: {
|
|
minLength?: number;
|
|
}): Arbitrary<ClientFeatureSchema[]> =>
|
|
fc.uniqueArray(clientFeature(), {
|
|
...constraints,
|
|
selector: (v) => v.name,
|
|
});
|
|
|
|
export const clientFeaturesAndSegments = (featureConstraints?: {
|
|
minLength?: number;
|
|
}): Arbitrary<{
|
|
features: ClientFeatureSchema[];
|
|
segments: SegmentSchema[];
|
|
}> => {
|
|
const segments = () =>
|
|
fc.uniqueArray(segment(), {
|
|
selector: (generatedSegment) => generatedSegment.id,
|
|
});
|
|
|
|
// create segments and make sure that all strategies reference segments that
|
|
// exist
|
|
return fc
|
|
.tuple(segments(), clientFeatures(featureConstraints))
|
|
.map(([generatedSegments, generatedFeatures]) => {
|
|
const renumberedSegments = generatedSegments.map(
|
|
(generatedSegment, index) => ({
|
|
...generatedSegment,
|
|
id: index + 1,
|
|
}),
|
|
);
|
|
|
|
const features: ClientFeatureSchema[] = generatedFeatures.map(
|
|
(feature) => ({
|
|
...feature,
|
|
...(feature.strategies && {
|
|
strategies: feature.strategies.map(
|
|
(generatedStrategy) => ({
|
|
...generatedStrategy,
|
|
...(generatedStrategy.segments && {
|
|
segments:
|
|
renumberedSegments.length > 0
|
|
? [
|
|
...new Set(
|
|
generatedStrategy.segments.map(
|
|
(generatedSegment) =>
|
|
(generatedSegment %
|
|
renumberedSegments.length) +
|
|
1,
|
|
),
|
|
),
|
|
]
|
|
: [],
|
|
}),
|
|
}),
|
|
),
|
|
}),
|
|
}),
|
|
);
|
|
|
|
return {
|
|
features,
|
|
segments: renumberedSegments,
|
|
};
|
|
});
|
|
};
|
|
|
|
// TEST ARBITRARIES
|
|
|
|
test('url-friendly strings are URL-friendly', () =>
|
|
fc.assert(
|
|
fc.property(urlFriendlyString(), (input: string) =>
|
|
/^[\w~.-]+$/.test(input),
|
|
),
|
|
));
|
|
|
|
test('variant payloads are either present or undefined; never null', () =>
|
|
fc.assert(
|
|
fc.property(
|
|
variant(),
|
|
(generatedVariant) =>
|
|
!!generatedVariant.payload ||
|
|
generatedVariant.payload === undefined,
|
|
),
|
|
));
|