-
-
Notifications
You must be signed in to change notification settings - Fork 658
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(#1873/playground): Return detailed information on feature toggle evaluation #1839
Conversation
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
The latest updates on your projects. Learn more about Vercel for Git ↗︎ 1 Ignored Deployment
|
|
||
const prefix = info | ||
? info.username | ||
: `generated-${Math.round(Math.random() * 1000000)}-${process.pid}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
opt.semgrep.node_insecure_random_generator: crypto.pseudoRandomBytes()/Math.random() is a cryptographically weak random number generator.
Reply with "@sonatype-lift help" for info about LiftBot commands.
Reply with "@sonatype-lift ignore" to tell LiftBot to leave out the above finding from this PR.
Reply with "@sonatype-lift ignoreall" to tell LiftBot to leave out all the findings from this PR and from the status bar in Github.
When talking to LiftBot, you need to refresh the page to see its response. Click here to get to know more about LiftBot commands.
Was this a good recommendation?
[ 🙁 Not relevant ] - [ 😕 Won't fix ] - [ 😑 Not critical, will fix ] - [ 🙂 Critical, will fix ] - [ 😊 Critical, fixing now ]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sonatype-lift ignore
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've recorded this as ignored for this pull request. If you change your mind, just comment @sonatype-lift unignore
.
|
||
export default class FlexibleRolloutStrategy extends Strategy { | ||
private randomGenerator: Function = () => | ||
`${Math.round(Math.random() * 100) + 1}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
opt.semgrep.node_insecure_random_generator: crypto.pseudoRandomBytes()/Math.random() is a cryptographically weak random number generator.
Reply with "@sonatype-lift help" for info about LiftBot commands.
Reply with "@sonatype-lift ignore" to tell LiftBot to leave out the above finding from this PR.
Reply with "@sonatype-lift ignoreall" to tell LiftBot to leave out all the findings from this PR and from the status bar in Github.
When talking to LiftBot, you need to refresh the page to see its response. Click here to get to know more about LiftBot commands.
Was this a good recommendation?
[ 🙁 Not relevant ] - [ 😕 Won't fix ] - [ 😑 Not critical, will fix ] - [ 🙂 Critical, will fix ] - [ 😊 Critical, fixing now ]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sonatype-lift ignore
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've recorded this as ignored for this pull request. If you change your mind, just comment @sonatype-lift unignore
.
|
||
export default class GradualRolloutRandomStrategy extends Strategy { | ||
private randomGenerator: Function = () => | ||
Math.floor(Math.random() * 100) + 1; |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sonatype-lift ignore
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've recorded this as ignored for this pull request. If you change your mind, just comment @sonatype-lift unignore
.
} | ||
|
||
function randomString() { | ||
return String(Math.round(Math.random() * 100000)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
opt.semgrep.node_insecure_random_generator: crypto.pseudoRandomBytes()/Math.random() is a cryptographically weak random number generator.
Reply with "@sonatype-lift help" for info about LiftBot commands.
Reply with "@sonatype-lift ignore" to tell LiftBot to leave out the above finding from this PR.
Reply with "@sonatype-lift ignoreall" to tell LiftBot to leave out all the findings from this PR and from the status bar in Github.
When talking to LiftBot, you need to refresh the page to see its response. Click here to get to know more about LiftBot commands.
Was this a good recommendation?
[ 🙁 Not relevant ] - [ 😕 Won't fix ] - [ 😑 Not critical, will fix ] - [ 🙂 Critical, will fix ] - [ 😊 Critical, fixing now ]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sonatype-lift ignore
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've recorded this as ignored for this pull request. If you change your mind, just comment @sonatype-lift unignore
.
Note: compared to regular clients, they still fail
This _should_ make it work with custom strategies too 🤞
4c033e0
to
e071bda
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With @andreas-unleash
src/lib/util/feature-evaluator/strategy/application-hostname-strategy.ts
Outdated
Show resolved
Hide resolved
@andreas-unleash Suggestion for the whole How about we make the type feature = {
// ... other props
strategies: {
result: boolean | 'unknown',
data: Strategy[]
}
} We keep Actually; doing it this way, we could leave |
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'`.
@andreas-unleash Further to the comment above, 440cf4c introduces the necessary changes to do that. We haven't discussed this fully, so I would be happy to revert that change if you disagree, but I do think we need an alternative to changing With these changes, the frontend equivalents would be:
Does this sound sensible to you? Let me know your thoughts. |
result: { | ||
description: 'Whether this was evaluated as true or false.', | ||
type: 'boolean', | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't followed this PR so feel free to ignore, but extending the constraint schema seems complicated, both here and on the frontend. Maybe we could add stuff next to the constraints and segments instead of extending?
{
constraints: [...], // Regular constraints.
segments: [...], // Regular segments.
results: [
{
constraintIndex: 1, // A constraint ID would be nice.
enabled: true,
},
{
constraintIndex: 1,
segmentId: 2,
enabled: true,
}
// ...
]
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, yes and no? We could definitely do that, but I'm not sure I agree. This might just be a personal thing, but it strikes me as natural to put the result right next to what it's evaluating. But you do make a good point that it does add more data models. Without it, we could potentially reuse some of the types from before. That said, from an API consumer view, I think I'd prefer to have the result colocated 🤷🏼
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, makes sense. I do want to see if we can avoid the constraint: IConstraint | PlaygroundConstraintSchema
stuff on the frontend though. Would be nice if the playground-related types were kept there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, that's interesting. I haven't seen the frontend implementation, so I don't know. Why do you need that? Couldn't you just use IConstraint? A playgroundconstraintschema should fit into an IConstraint, doesn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure, haven't looked at it in detail. Might just be because it's IConstraint
and not ConstraintSchema
. 🤷
strategies: PlaygroundStrategySchema[]; | ||
}; | ||
|
||
export default class UnleashClient { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could it make sense to extend the node SDK itself with the ability to explain its results, instead of duplicating the SDK? Sounds like a nice SDK feature. Not sure how much code has been copied over though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, actually, yeah. Extending the node SDK might be a better way to go. I hadn't thought of that 🙈 Basically, all the code has been copied over, and then I've removed as much as possible. In theory, extending the node SDK instead could be a (glorious) refactor later. That said, there may be a few things that make it harder. It should be fine to override the isEnabled
functionality as we've done here, but overriding strategy implementations might be harder (and necessary).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
This change adds detailed information about why a feature evaluates the way it does to the playground response payload (relates to #1873). To achieve this, it:
To make this as useful as possible to the end user, we want to provide as much information as we can. That means that every strategy should be evaluated, regardless of whether any preceding strategies have resolved to
true
.Review guide: what to look at and to look for
Woah, this is a massive PR! But it's not as much new code as it actually looks like. Here's a rough outline of what kind of changes are located where:
src/lib/...
Practically all changes are under this directory, so let's dig a bit deeper:
db
As described inline and under "other changes", this is a change that allows the playground client to get IDs on its strategies.
openapi
These are all the new OpenAPI schemas and their direct tests. Feel free to have a look through these. There shouldn't be any executing code here, just definitions and tests.
routes/admin-api
This is just a minor code adjustment (remove a redundant type annotation).
services
Minor changes related to adding new services to the playground service and the change allowing you to get strategies with IDs from the store.
changes in the playground service implementation to allow returning the new and more detailed response objects.
types/stores
Same change regarding strategies with IDs as mentioned before.
util/feature-evaluator
This is the ported node SDK client. It's a whooooole lotta code. Most of it is more or less the same, but it's been reworked to evaluate all toggles to return information about evaluation. Probably the largest actual code change in this PR.
Note that the custom client (the
feature-evaluator
) is tested against the actual node client to ensure that its evaluations are accurate. There may of course be bugs in the tests, but so far, they seem catch whenever I do something wrong.util/offline-client
This is the entry point for the feature evaluator.
test
A fairly comprehensive test suite based on property based testing. I reckon there's about 50 or so tests mainly enforcing different invariants about the code. There's some unfortunate duplication here and probably some opportunities for refactoring / joining some test suites, so I'm happy to rework this. This is also about a third of all the changed lines in this PR.
Evaluate everything always
One of the main changes from the actual SDK implementation is that we always evaluate all strategies. The SDK will return as soon as it finds a strategy that resolves to
true
. This new client, however, keeps going until it has gone over every strategy a feature has.API change
With the added payload, the API gets a significant change. It's further described in the next section, but the biggest takeaway is that each
feature
now gets an additionalstrategy
property, which contains information about all its strategies.Further, the
isEnabled
property changes a bit (more about this in the discussions section and in the next section), as it now indicates whether a strategy is enabled based on its strategies and does not necessarily mirror the SDK'sisEnabled
property. Afeature
also gains a newisEnabledInCurrentEnvironment
property, which tells you whether the feature is enabled in the current environment or not.Payload description
Hidden in the next details element is a full sample response
Full playground response
feature.strategies
Each feature now has a list of
strategies
attached to it. The strategy contains the following properties:name
id
parameters
result
constraints
segments
A sample response is show next. (Note: it's manually edited, so might not be consistent in enabled/disabled states)
We'll discuss constraints and segments more in the next section, but first, let's consider the
result
property.The
result
property tells you about how a strategy evaluates. It is an object containing two subproperties:enabled
andevaluationStatus.
The
evaluationStatus
property can be either'complete'
or'incomplete'
. If it iscomplete
, that means that Unleash has evaluated the strategy without issues and thatenabled
will be eithertrue
orfalse
.evaluationStatus
will (currently) only be'incomplete'
if you use a custom strategy that Unleash doesn't recognize. When it is'incomplete'
,enabled
will be one offalse
and'unknown'
. Unleash can't know whether a strategy it doesn't know about would be resolve to true or false, so it can never guarantee that it would be true. However, because a custom strategy can have constraints and segments applied, Unleash can guarantee that the strategy will be false if at least one constraint/segment fails.Constraints and segments
A strategy's
constraints
property is a list ofconstraints
with an addedresult
property. This property can be eithertrue
orfalse
, indicating whether the constraint passed validation.A strategy's
segments
property contains each segment (in full, expanded form) that is applied to the strategy. Each segment has the sameconstraints
property as a strategy (that is, with an addedresult
on each constraint). Additionally, each segment also has aresult
property to indicate whether the segment on a whole is true or false.The
feature.isEnabled
propertyBecause we may have strategies that we can't fully evaluate, the
feature.isEnabled
property is now one oftrue
,false
, and'unevaluated'
. It will only be'unevaluated'
if all its strategies are eitherfalse
or'unknown'
as described above.Further, because we want the playground to be able to distinguish between features that are enabled in the current environment and features that would be enabled in the current environment if you turned it on, the
isEnabled
property now only takes the feature's strategies into account. In other words, it disregards whether a feature is enabled in the current environment or not. To account for this, I've also introduced thefeature.isEnabledInCurrentEnvironment
prop, which has that information. That means that what's considered enabled by a regular SDK is a combination ofisEnabledInCurrentEnvironment
andisEnabled
:feature.isEnabled && feature.isEnabledInCurrentEnvironment === sdkClient.isEnabled(feature.name)
More about ramifications of this in the discussions section.
Other changes
Adding
includeStrategyIds
option insrc/lib/db/feature-toggle-client-store.ts
The feature toggle client store can return strategy ids under certain situations, but it's rare. Because we need strategy IDs for the payload (and because it makes a number of the operations significantly easier), I added it as an optional parameter.
Still left to do
There's still some minor things to do. It's mostly cleanup, but important nonetheless.
Discussions
Is changing
isEnabled
a breaking change?Technically, yes! There's two different aspects that change it, though, so let's look at both:
Adding the
'unevaluated'
optionThis changes the type of the endpoint from strict
boolean
toboolean | 'unevaluated'
. This could break clients that expect it to be a boolean. Another solution could be to add another property to thefeature
object, sayevaluationStatus: 'certain' | 'uncertain'
, which would be'uncertain'
when the feature'sisEnabled
is'unevaluated'
. At the time of writing, this strikes me as a worthwhile change.Relying on
isEnabledInCurrentEnvironment
to get the same results as the SDKThis is a bit confusing and might break clients expecting it to be the same as the SDK. An option would be to flip it on its head and instead use a property
wouldBeEnabledInTheCurrentEnvironment
and leaveisEnabled
as the same as the SDK uses. In this case,{ wouldBeEnabledInTheCurrentEnvironment: true, isEnabled: false }
would indicate that the SDK says no, but that it could be enabled if you flipped the switch. However, this version introduces an 'impossible' variant:{ wouldBeEnabledInTheCurrentEnvironment: false, isEnabled: true }
, so I think it needs a bit more thought.Adding custom strategies
As of today, you can't add custom strategy implementations to Unleash, which means that these can't be fully evaluated. It might be worth thinking about a way to add impls to the instance itself, but that would require a lot of thought.
A potential improvement on the current handling, however, might be to check whether a custom strategy is defined on the unleash instance and differentiating between custom strategies without implementations and custom strategies that it doesn't recognize at all.