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
Stop removing experiment related query parameters #5717
Conversation
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.
For posterity, some of the things we discussed offline:
- There is a more general need to associate metadata with individual feature flags. This is a good opportunity to build the foundation for that.
- Riley suggested some changes to FeatureFlags type in general and will play around with that.
- There is the consideration that specific TensorBoard instances (like TensorBoard.dev, TensorBoard.corp) may extend the FeatureFlags type to include additional feature flags.
db6b03d
to
f262ce9
Compare
f262ce9
to
0d9d8a6
Compare
bfce175
to
6e74acc
Compare
tensorboard/webapp/webapp_data_source/tb_feature_flag_data_source.ts
Outdated
Show resolved
Hide resolved
926121b
to
c00f9ed
Compare
c00f9ed
to
5dc2e03
Compare
5dc2e03
to
b13aabf
Compare
tensorboard/webapp/webapp_data_source/tb_feature_flag_data_source.ts
Outdated
Show resolved
Hide resolved
queryParamOverride in currentQueryParams) { | ||
queryParams.push({ | ||
key: queryParamOverride, | ||
value: currentQueryParams[queryParamOverride], |
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.
So the strategy seems to be: If we determine a query param in the current URL represents a feature flag, then we copy the query param and its value into the new URL. The strategy doesn't actually depend on feature flag state.
I think we should consider depending on feature flag state to generate this part of the URL. You could use the data from getOverriddenFeatureFlags() to write the query params.
Some reasoning:
- This better aligns with the idea that the URL is a projection of state (that is, it is fully constructed from internal state).
- With the upcoming support for feature flags in local storage, the URL will include those overrides after page load. I think that is desirable behavior.
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.
Simply making the URL a reflection of the feature flag state would be easier to do and thus would simplify the code. I was previously under the impression that reflecting localStorage
was considered undesirable.
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.
Having the URL reflect the localStorage overrides would be a good reminder to the user that they are in some special state.
It will help us quickly troubleshoot problems like "why can't I see feature A after feature A has launched?" when user forgot they set enableFeatureA=false in local storage. I am certain this will happen all the time.
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.
This makes lots of sense to me. That also ensures that sharing a link will result in the other person seeing the same thing. I've gone ahead and made this change and will be pushing a new commit quite soon.
tensorboard/webapp/webapp_data_source/tb_feature_flag_data_source.ts
Outdated
Show resolved
Hide resolved
tensorboard/webapp/webapp_data_source/tb_feature_flag_query_parameters.ts
Outdated
Show resolved
Hide resolved
tensorboard/webapp/webapp_data_source/tb_feature_flag_query_parameters.ts
Outdated
Show resolved
Hide resolved
tensorboard/webapp/webapp_data_source/tb_feature_flag_query_parameters.ts
Outdated
Show resolved
Hide resolved
3db9e81
to
d0a7292
Compare
d0a7292
to
f2dac24
Compare
…ture flags in the query params
f2dac24
to
efa2aa3
Compare
3caf67c
to
6f6a14d
Compare
|
||
export type FeatureFlagMetadata<T> = { | ||
displayName: string; | ||
defaultValue?: T; |
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.
Is there a need for defaultValue? inColab
, the one property that uses it, has its default defined here:
Which is how every other flag defines a default value.
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.
This might not be a good variable name. This is the value when the param is present but no value is given. TBH I don't like that there is any inconsistency here but I didn't want to effect existing behavior if possible.
In this case ?inColab
SHOULD parse to false
while others e.g. ?enableColorGroup
would parse to true
.
Given the confusing naming and strange UX surrounding this I am inclined to change the behavior of inColab
when the value is not set. LMK if you'd rather I leave it as is and change the variable name.
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.
Yes, I'm fine to change the behavior of inColab. I did a quick survey of usage internally and it looks like usage is always "tensorboardColab=true" and not just "tensorboardColab".
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'm going to update inColab
. I've noticed that the url can get a bit cluttered if you start enabling lots of feature flags in the UI so I'm going to preempt the issue and only serialize feature flags with non default values.
@@ -126,7 +97,52 @@ export class DashboardDeepLinkProvider extends DeepLinkProvider { | |||
return [{key: TAG_FILTER_KEY, value: filterText}]; | |||
}) | |||
), | |||
this.getFeatureFlagStates(store), | |||
store.select(getEnabledExperimentalPlugins).pipe( |
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.
L100-L108, handling experimentalPlugin in its own section seems redundant with L109-L145 and leads to twice as many experimentalPlugin query params as you want.
For example, if you load with the following:
http://localhost:6006/?experimentalPlugin=blah&experimentalPlugin=blue
The end result will be:
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.
🤯 in a previous version I didn't have a solution to array types. Looks like I somehow missed this after cleaning that up! Fantastic catch.
.flat() | ||
.filter( | ||
({key, value}) => key && value !== undefined | ||
) as SerializableQueryParams; |
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.
Is there any need to put as SerializableQueryParams
? The return value of the function is already specified as SerializableQueryParams so the compiler should already check this.
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.
For some reason TypeScript wouldn't believe that key
and value
were always defined even after the filter.
e6bbffa
to
157e299
Compare
157e299
to
9ddcbbc
Compare
…rsist feature flags with non default values
9a62e92
to
566bcda
Compare
{} as Record<string, any> | ||
) as FeatureFlags; |
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.
This is a pretty gross cast... The issue is that Object.entries
(and all the other Object
methods) drops the typing. This cast seems preferable to casting the values but I am open to alternatives.
I COULD just enumerate the feature flags below but my current approach seems to scale better as more feature flags are added.
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.
No problem for me. If you look hard enough there are several grosser secret casts in the FeatureFlag code base.
{key: 'enableColorGroup', value: 'true'}, | ||
]); | ||
expect(queryParamsSerialized[queryParamsSerialized.length - 1]).toEqual( | ||
[] |
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.
enableColorGroup
defaults to true
so persisting it is redundant.
@@ -215,7 +289,7 @@ describe('tb_feature_flag_data_source', () => { | |||
); | |||
expect(dataSource.getFeatures()).toEqual({ | |||
enabledExperimentalPlugins: ['a'], | |||
inColab: false, | |||
inColab: 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.
Calling out that this behavior IS changing
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.
Yes, that's fine. Thanks.
tensorboard/webapp/feature_flag/store/feature_flag_store_config_provider.ts
Outdated
Show resolved
Hide resolved
{} as Record<string, any> | ||
) as FeatureFlags; |
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.
No problem for me. If you look hard enough there are several grosser secret casts in the FeatureFlag code base.
} | ||
const featureFlags: Partial<Record<keyof FeatureFlags, FeatureFlagType>> = | ||
enableMediaQuery ? this.getPartialFeaturesFromMediaQuery() : {}; | ||
Object.entries(FeatureFlagMetadataMap).forEach( |
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.
Can this also be refactored into something reusable? I think you were already intending on reusing getFeatureValue() but maybe we don't have to stop there. Especially given how this mangles all type information, would be nice to just have to write that code once.
tensorboard/webapp/webapp_data_source/tb_feature_flag_data_source.ts
Outdated
Show resolved
Hide resolved
tensorboard/webapp/webapp_data_source/tb_feature_flag_data_source.ts
Outdated
Show resolved
Hide resolved
@@ -215,7 +289,7 @@ describe('tb_feature_flag_data_source', () => { | |||
); | |||
expect(dataSource.getFeatures()).toEqual({ | |||
enabledExperimentalPlugins: ['a'], | |||
inColab: false, | |||
inColab: 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.
Yes, that's fine. Thanks.
tensorboard/webapp/webapp_data_source/tb_feature_flag_data_source_test.ts
Show resolved
Hide resolved
…or reusability, move feature flag overrides to feature flag metadata
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.
Thanks!
…the feature flag infrastructure. (#5836) When we introduced `FeatureFlagMetadata` in #5717, we included the`isArray' property, which allowed us to support feature flags that (1) have array type and (2) can be overriden with query parameters. There is only a single feature flag that satisfies this - enabledExperimentalPlugins/experimentalPlugin. The isArray support adds complexity to the feature flag framework - parsing and encoding logic need special handling for array types in addition to basic types. I wondered if we could eliminate the complexity from the feature flag framework and instead isolate the array-handling to a narrower scope. The idea: We can simplify the `experimentalPlugin` query parameter and then simplify FeatureFlagMetadata and the surrounding infrastructure. We change experimentalPlugin query parameter to be a single comma-delimited value instead of multiple query parameters with single values. That is, we would now write `experimentalPlugin=plugin1,plugin2,plugin3` where we previously would have written `experimentalPlugin=plugin1&experimentalPlugin=plugin2&experimentalPlugin=plugin3`. Once experimentalPlugin is just a single query parameter there is a path towards removing `isArray` and simplifying the framework. We remove `isArray`. enabledExperimentalPlugins specifies a function for parseValue that knows how to convert strings of type 'val1,val2,val3' into a string[]. And, fortunately, we can rely on string[].toString() to encode the array value back to the query parameter string. The array-specific logic in the greater framework is removed. The complexity is now isolated to the definition of enabledExperimentalPlugins.
* Stop removing query parameters when persisting application state to the url * simplify query param serialization logic * move feature flag overrides to feature flag metadata
…the feature flag infrastructure. (tensorflow#5836) When we introduced `FeatureFlagMetadata` in tensorflow#5717, we included the`isArray' property, which allowed us to support feature flags that (1) have array type and (2) can be overriden with query parameters. There is only a single feature flag that satisfies this - enabledExperimentalPlugins/experimentalPlugin. The isArray support adds complexity to the feature flag framework - parsing and encoding logic need special handling for array types in addition to basic types. I wondered if we could eliminate the complexity from the feature flag framework and instead isolate the array-handling to a narrower scope. The idea: We can simplify the `experimentalPlugin` query parameter and then simplify FeatureFlagMetadata and the surrounding infrastructure. We change experimentalPlugin query parameter to be a single comma-delimited value instead of multiple query parameters with single values. That is, we would now write `experimentalPlugin=plugin1,plugin2,plugin3` where we previously would have written `experimentalPlugin=plugin1&experimentalPlugin=plugin2&experimentalPlugin=plugin3`. Once experimentalPlugin is just a single query parameter there is a path towards removing `isArray` and simplifying the framework. We remove `isArray`. enabledExperimentalPlugins specifies a function for parseValue that knows how to convert strings of type 'val1,val2,val3' into a string[]. And, fortunately, we can rely on string[].toString() to encode the array value back to the query parameter string. The array-specific logic in the greater framework is removed. The complexity is now isolated to the definition of enabledExperimentalPlugins.
* Stop removing query parameters when persisting application state to the url * simplify query param serialization logic * move feature flag overrides to feature flag metadata
…the feature flag infrastructure. (tensorflow#5836) When we introduced `FeatureFlagMetadata` in tensorflow#5717, we included the`isArray' property, which allowed us to support feature flags that (1) have array type and (2) can be overriden with query parameters. There is only a single feature flag that satisfies this - enabledExperimentalPlugins/experimentalPlugin. The isArray support adds complexity to the feature flag framework - parsing and encoding logic need special handling for array types in addition to basic types. I wondered if we could eliminate the complexity from the feature flag framework and instead isolate the array-handling to a narrower scope. The idea: We can simplify the `experimentalPlugin` query parameter and then simplify FeatureFlagMetadata and the surrounding infrastructure. We change experimentalPlugin query parameter to be a single comma-delimited value instead of multiple query parameters with single values. That is, we would now write `experimentalPlugin=plugin1,plugin2,plugin3` where we previously would have written `experimentalPlugin=plugin1&experimentalPlugin=plugin2&experimentalPlugin=plugin3`. Once experimentalPlugin is just a single query parameter there is a path towards removing `isArray` and simplifying the framework. We remove `isArray`. enabledExperimentalPlugins specifies a function for parseValue that knows how to convert strings of type 'val1,val2,val3' into a string[]. And, fortunately, we can rely on string[].toString() to encode the array value back to the query parameter string. The array-specific logic in the greater framework is removed. The complexity is now isolated to the definition of enabledExperimentalPlugins.
Motivation for features / changes
Navigating to TensorBoard used to result in all provided query parameters where being removed when the application state was persisted to the query parameters. This would lead to refreshed of the page not having the experiment value set correctly.
Technical description of changes
Unfortunately there is neither a one to one mapping of query parameter overrides to experiment nor do the overrides exactly reflect the experiment they are overriding. i.e.
enabledLinkTime
is overridden by the parameterenableLinkTime
. To resolve this issue I manually created a mapping experiment to their query parameter overrides which I then checked when the application state is persisted to the query parameters.Screenshots of UI changes
No UI change
Detailed steps to verify changes work correctly (as executed by you)
Visit the dashboard with an experiment overridden via a query parameter and note that it is not removed.
Note that prior to this
enableColorGroupByRegex
andenableColorGroup
where both explicitly handled in a similar way.I considered two other approaches
The manual mapping could be avoided by changing the query parameter overrides (or else adding additional parameters to overrides). This could result in unexpected behavior for existing users unaware of the change but would be easier to maintain as more experiments are added.
Instead of replacing the all the query parameters, we could just update or remove those that had changed. This would be a bigger change to the application though and would risk introducing other issues.