Skip to content

Commit

Permalink
Adds mechanism to include feature flags in Angular HTTP requests. (#5718
Browse files Browse the repository at this point in the history
)

This adds a mechanism that dumps the client-side FeatureFlag object into each HTTP request that originates from the TB Angular code base.

(Googlers, see: http://go/tb-client-feature-flags-in-data-provider for the motivation and high level design.)

The data is dumped into a custom header. The format is effectively:
X-TensorBoard-Feature-Flags: JSON.stringify(currentFeatureFlags)

We implement this by introducing an HttpInterceptor. It allows us to intercept each request in the Angular HTTP pipeline and inject the new header. The HttpInterceptor is provided in the existing FeatureFlagModule so all instances of TensorBoard will receive this behavior without any additional configuration.

Additional things worth noting:

* This will send ALL feature flags in the request header rather than a curated subset of them. In a subsequent PR, we'll add support to mark certain feature flags as "sendToServer" and only send that subset. This PR unblocks the main use case at the cost of there being some temporary overhead in HTTP requests.

* In a subsequent PR We will add similar functionality to the TB Polymer code base.

* In a subsequent PR we will add support on the TB HTTP Server to consume these feature flags and trigger logic based on the values.

* I moved feature_flags tests into their own "karma_test" target separate from the big top-level karma_test target. stephanwlee advocated for breaking up the top-level karma_test target in the months before he left the team.
  • Loading branch information
bmd3k committed May 26, 2022
1 parent c5ccc59 commit 28ff87e
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 3 deletions.
2 changes: 0 additions & 2 deletions tensorboard/webapp/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,6 @@ tf_ng_web_test_suite(
"//tensorboard/webapp/core/views:test_lib",
"//tensorboard/webapp/customization:customization_test_lib",
"//tensorboard/webapp/deeplink:deeplink_test_lib",
"//tensorboard/webapp/feature_flag/effects:effects_test_lib",
"//tensorboard/webapp/feature_flag/store:store_test_lib",
"//tensorboard/webapp/header:test_lib",
"//tensorboard/webapp/metrics:integration_test",
"//tensorboard/webapp/metrics:test_lib",
Expand Down
2 changes: 2 additions & 0 deletions tensorboard/webapp/angular/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ tf_ts_library(
name = "expect_angular_common_http",
srcs = [],
visibility = [
"//tensorboard/webapp/feature_flag:__subpackages__",
"//tensorboard/webapp/webapp_data_source:__subpackages__",
],
deps = [
Expand All @@ -23,6 +24,7 @@ tf_ts_library(
name = "expect_angular_common_http_testing",
srcs = [],
visibility = [
"//tensorboard/webapp/feature_flag:__subpackages__",
"//tensorboard/webapp/webapp_data_source:__subpackages__",
],
deps = [
Expand Down
13 changes: 12 additions & 1 deletion tensorboard/webapp/feature_flag/BUILD
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
load("//tensorboard/defs:defs.bzl", "tf_ng_module", "tf_ts_library")
load("//tensorboard/defs:defs.bzl", "tf_ng_module", "tf_ng_web_test_suite", "tf_ts_library")

package(default_visibility = ["//tensorboard:internal"])

Expand All @@ -9,7 +9,9 @@ tf_ng_module(
],
deps = [
":force_svg_data_source",
"//tensorboard/webapp/angular:expect_angular_common_http",
"//tensorboard/webapp/feature_flag/effects",
"//tensorboard/webapp/feature_flag/http",
"//tensorboard/webapp/feature_flag/store",
"//tensorboard/webapp/feature_flag/store:types",
"//tensorboard/webapp/persistent_settings",
Expand Down Expand Up @@ -54,3 +56,12 @@ tf_ts_library(
"@npm//@types/jasmine",
],
)

tf_ng_web_test_suite(
name = "karma_test",
deps = [
"//tensorboard/webapp/feature_flag/effects:effects_test_lib",
"//tensorboard/webapp/feature_flag/http:http_test_lib",
"//tensorboard/webapp/feature_flag/store:store_test_lib",
],
)
7 changes: 7 additions & 0 deletions tensorboard/webapp/feature_flag/feature_flag_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/

import {HTTP_INTERCEPTORS} from '@angular/common/http';
import {NgModule} from '@angular/core';
import {EffectsModule} from '@ngrx/effects';
import {createSelector, StoreModule} from '@ngrx/store';
Expand All @@ -24,6 +25,7 @@ import {
import {TBFeatureFlagModule} from '../webapp_data_source/tb_feature_flag_module';
import {FeatureFlagEffects} from './effects/feature_flag_effects';
import {ForceSvgDataSourceModule} from './force_svg_data_source_module';
import {FeatureFlagHttpInterceptor} from './http/feature_flag_http_interceptor';
import {reducers} from './store/feature_flag_reducers';
import {getEnableDarkModeOverride} from './store/feature_flag_selectors';
import {
Expand Down Expand Up @@ -63,6 +65,11 @@ export function getThemeSettingSelector() {
provide: FEATURE_FLAG_STORE_CONFIG_TOKEN,
useFactory: getConfig,
},
{
provide: HTTP_INTERCEPTORS,
useClass: FeatureFlagHttpInterceptor,
multi: true,
},
],
})
export class FeatureFlagModule {}
42 changes: 42 additions & 0 deletions tensorboard/webapp/feature_flag/http/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
load("//tensorboard/defs:defs.bzl", "tf_ng_module", "tf_ts_library")

package(default_visibility = ["//tensorboard:internal"])

tf_ng_module(
name = "http",
srcs = [
"feature_flag_http_interceptor.ts",
],
deps = [
"//tensorboard/webapp/angular:expect_angular_common_http",
"//tensorboard/webapp/feature_flag/store",
"//tensorboard/webapp/feature_flag/store:types",
"@npm//@angular/core",
"@npm//@ngrx/store",
"@npm//rxjs",
],
)

tf_ts_library(
name = "http_test_lib",
testonly = True,
srcs = [
"feature_flag_http_interceptor_test.ts",
],
deps = [
":http",
"//tensorboard/webapp/angular:expect_angular_common_http",
"//tensorboard/webapp/angular:expect_angular_common_http_testing",
"//tensorboard/webapp/angular:expect_angular_core_testing",
"//tensorboard/webapp/angular:expect_ngrx_store_testing",
"//tensorboard/webapp/feature_flag:testing",
"//tensorboard/webapp/feature_flag:types",
"//tensorboard/webapp/feature_flag/store",
"//tensorboard/webapp/feature_flag/store:types",
"@npm//@angular/core",
"@npm//@ngrx/effects",
"@npm//@ngrx/store",
"@npm//@types/jasmine",
"@npm//rxjs",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/* Copyright 2022 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
import {
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
} from '@angular/common/http';
import {Injectable} from '@angular/core';
import {select, Store} from '@ngrx/store';
import {Observable} from 'rxjs';
import {first, switchMap} from 'rxjs/operators';
import {getFeatureFlags} from '../store/feature_flag_selectors';
import {State as FeatureFlagState} from '../store/feature_flag_types';

export const FEATURE_FLAGS_HEADER_NAME = 'X-TensorBoard-Feature-Flags';

/**
* HttpInterceptor for injecting feature flags into each HTTP request
* originating from the Angular TensorBoard code base.
*/
@Injectable()
export class FeatureFlagHttpInterceptor implements HttpInterceptor {
constructor(private readonly store: Store<FeatureFlagState>) {}

intercept(
request: HttpRequest<unknown>,
next: HttpHandler
): Observable<HttpEvent<unknown>> {
return this.store.pipe(
select(getFeatureFlags),
first(),
switchMap((featureFlags) => {
// Add feature flags to the headers.
request = request.clone({
headers: request.headers.set(
FEATURE_FLAGS_HEADER_NAME,
JSON.stringify(featureFlags)
),
});
// Delegate to next Interceptor.
return next.handle(request);
})
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/* Copyright 2022 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/

import {HttpClient, HttpHeaders, HTTP_INTERCEPTORS} from '@angular/common/http';
import {
HttpClientTestingModule,
HttpTestingController,
} from '@angular/common/http/testing';
import {TestBed} from '@angular/core/testing';
import {provideMockActions} from '@ngrx/effects/testing';
import {Store} from '@ngrx/store';
import {MockStore, provideMockStore} from '@ngrx/store/testing';
import {of} from 'rxjs';
import {getFeatureFlags} from '../store/feature_flag_selectors';
import {State as FeatureFlagState} from '../store/feature_flag_types';
import {buildFeatureFlag} from '../testing';
import {FeatureFlagHttpInterceptor} from './feature_flag_http_interceptor';

describe('FeatureFlagHttpInterceptor', () => {
let store: MockStore<FeatureFlagState>;
let httpClient: HttpClient;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
provideMockActions(() => of()),
provideMockStore(),
{
provide: HTTP_INTERCEPTORS,
useClass: FeatureFlagHttpInterceptor,
multi: true,
},
],
}).compileComponents();

store = TestBed.inject<Store<FeatureFlagState>>(
Store
) as MockStore<FeatureFlagState>;
store.overrideSelector(getFeatureFlags, buildFeatureFlag());

// Note that we do not test FeatureFlagHttpInterceptor directly. We instead
// test it indirectly by firing Http requests and examining the final
// request recorded by the HttpTestingController.
httpClient = TestBed.inject(HttpClient);
});

it('injects feature flags into the HTTP request', () => {
store.overrideSelector(getFeatureFlags, buildFeatureFlag({inColab: true}));
httpClient.get('/data/hello').subscribe();
const request = TestBed.inject(HttpTestingController).expectOne(
'/data/hello'
);
expect(request.request.headers).toEqual(
new HttpHeaders().set(
'X-TensorBoard-Feature-Flags',
JSON.stringify(buildFeatureFlag({inColab: true}))
)
);
});
});

0 comments on commit 28ff87e

Please sign in to comment.