Skip to content
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

Merged
merged 12 commits into from Aug 1, 2022

Conversation

rileyajones
Copy link
Contributor

  • 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 parameter enableLinkTime. 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 and enableColorGroup where both explicitly handled in a similar way.

  • Alternate designs / implementations considered
    I considered two other approaches
  1. 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.

  2. 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.

@bmd3k bmd3k self-requested a review May 25, 2022 11:27
Copy link
Contributor

@bmd3k bmd3k left a 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.

@rileyajones rileyajones marked this pull request as draft July 8, 2022 19:40
@rileyajones rileyajones force-pushed the query-params-bug branch 6 times, most recently from 926121b to c00f9ed Compare July 11, 2022 20:11
@rileyajones rileyajones marked this pull request as ready for review July 11, 2022 21:30
@bmd3k bmd3k self-requested a review July 12, 2022 15:17
@rileyajones rileyajones marked this pull request as draft July 12, 2022 17:28
tensorboard/webapp/routes/dashboard_deeplink_provider.ts Outdated Show resolved Hide resolved
queryParamOverride in currentQueryParams) {
queryParams.push({
key: queryParamOverride,
value: currentQueryParams[queryParamOverride],
Copy link
Contributor

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:

  1. This better aligns with the idea that the URL is a projection of state (that is, it is fully constructed from internal state).
  2. 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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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/routes/feature_flag_serializer.ts Outdated Show resolved Hide resolved
@rileyajones rileyajones force-pushed the query-params-bug branch 2 times, most recently from 3db9e81 to d0a7292 Compare July 18, 2022 20:34
@rileyajones rileyajones marked this pull request as ready for review July 18, 2022 22:44
tensorboard/webapp/app_routing/location.ts Outdated Show resolved Hide resolved

export type FeatureFlagMetadata<T> = {
displayName: string;
defaultValue?: T;
Copy link
Contributor

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:

http://google3/third_party/tensorboard/webapp/feature_flag/store/feature_flag_store_config_provider.ts;l=28;rcl=453264239

Which is how every other flag defines a default value.

Copy link
Contributor Author

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.

Copy link
Contributor

@bmd3k bmd3k Jul 28, 2022

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".

Copy link
Contributor Author

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(
Copy link
Contributor

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:

http://localhost:6006/?experimentalPlugin=blah&experimentalPlugin=blue&experimentalPlugin=blah&experimentalPlugin=blue

Copy link
Contributor Author

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;
Copy link
Contributor

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.

Copy link
Contributor Author

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.

tensorboard/webapp/routes/feature_flag_serializer.ts Outdated Show resolved Hide resolved
@rileyajones rileyajones force-pushed the query-params-bug branch 2 times, most recently from e6bbffa to 157e299 Compare July 28, 2022 19:30
Comment on lines 26 to 27
{} as Record<string, any>
) as FeatureFlags;
Copy link
Contributor Author

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.

Copy link
Contributor

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(
[]
Copy link
Contributor Author

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,
Copy link
Contributor Author

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

Copy link
Contributor

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.

@rileyajones rileyajones requested a review from bmd3k July 29, 2022 16:38
Comment on lines 26 to 27
{} as Record<string, any>
) as FeatureFlags;
Copy link
Contributor

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.

tensorboard/webapp/routes/feature_flag_serializer.ts Outdated Show resolved Hide resolved
}
const featureFlags: Partial<Record<keyof FeatureFlags, FeatureFlagType>> =
enableMediaQuery ? this.getPartialFeaturesFromMediaQuery() : {};
Object.entries(FeatureFlagMetadataMap).forEach(
Copy link
Contributor

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.

@@ -215,7 +289,7 @@ describe('tb_feature_flag_data_source', () => {
);
expect(dataSource.getFeatures()).toEqual({
enabledExperimentalPlugins: ['a'],
inColab: false,
inColab: true,
Copy link
Contributor

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.

…or reusability, move feature flag overrides to feature flag metadata
Copy link
Contributor

@bmd3k bmd3k left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@rileyajones rileyajones merged commit bbef530 into tensorflow:master Aug 1, 2022
bmd3k added a commit that referenced this pull request Aug 3, 2022
…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.
yatbear pushed a commit to yatbear/tensorboard that referenced this pull request Mar 27, 2023
* Stop removing query parameters when persisting application state to the url

* simplify query param serialization logic

* move feature flag overrides to feature flag metadata
yatbear pushed a commit to yatbear/tensorboard that referenced this pull request Mar 27, 2023
…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.
dna2github pushed a commit to dna2fork/tensorboard that referenced this pull request May 1, 2023
* Stop removing query parameters when persisting application state to the url

* simplify query param serialization logic

* move feature flag overrides to feature flag metadata
dna2github pushed a commit to dna2fork/tensorboard that referenced this pull request May 1, 2023
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants