Skip to content

Commit

Permalink
Implements the feature flags reading from GCS + exposing it in a glob…
Browse files Browse the repository at this point in the history
…al var declared in base.py. Creates a helper to interact with them from both the TS and py code. Reads the flags for autocomplete, place page experiment and place page GA.
  • Loading branch information
gmechali committed Jan 21, 2025
1 parent 2de4eea commit 96d06dd
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 6 deletions.
1 change: 1 addition & 0 deletions server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@ def create_app(nl_root=DEFAULT_NL_ROOT):
app.config['NL_CHART_TITLES'] = libutil.get_nl_chart_titles()
app.config['TOPIC_CACHE'] = topic_cache.load(app.config['NL_CHART_TITLES'])
app.config['SDG_PERCENT_VARS'] = libutil.get_sdg_percent_vars()
app.config['FEATURE_FLAGS'] = libutil.load_feature_flags()
app.config['SPECIAL_DC_NON_COUNTRY_ONLY_VARS'] = \
libutil.get_special_dc_non_countery_only_vars()
# TODO: need to handle singular vs plural in the titles
Expand Down
7 changes: 7 additions & 0 deletions server/lib/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import re
from typing import Set
from flask import current_app

import server.lib.fetch as fetch

Expand Down Expand Up @@ -243,3 +244,9 @@ def is_percapita_relevant(sv_dcid: str, nopc_svs: Set[str]) -> bool:
if skip_phrase in sv_dcid:
return False
return True


def is_feature_enabled(feature_name: str) -> bool:
"""Returns whether the feature with `feature_name` is enabled."""
feature_flags = current_app.config['FEATURE_FLAGS']
return feature_name in feature_flags and feature_flags[feature_name]
43 changes: 43 additions & 0 deletions server/lib/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
from flask import jsonify
from flask import make_response
from flask import request
from google.cloud import storage
from google.cloud.exceptions import NotFound
from google.protobuf import text_format

from server.config import subject_page_pb2
Expand Down Expand Up @@ -394,6 +396,47 @@ def get_nl_no_percapita_vars():
return nopc_vars


def get_feature_flag_bucket_name() -> str:
"""Returns the bucket name containing the feature flags."""
env_for_bucket = os.environ.get('FLASK_ENV')
if env_for_bucket == 'local':
env_for_bucket = 'autopush'
elif env_for_bucket == 'production':
env_for_bucket = 'prod'
return 'datcom-website-' + env_for_bucket + '-resources'


def load_feature_flags():
"""Loads the feature flags into app config."""
storage_client = storage.Client()
bucket_name = get_feature_flag_bucket_name()
try:
bucket = storage_client.get_bucket(bucket_name)
except NotFound:
logging.error("Bucket not found: " + bucket_name)
return {}

blob = bucket.get_blob("feature_flags.json")
data = {}
if blob:
try:
data = json.loads(blob.download_as_bytes())
except json.JSONDecodeError:
logging.warning("Loading feature flags failed to decode JSON.")
except TypeError:
logging.warning("Loading feature flags encountered a TypeError.")
else:
logging.warning("Feature flag file not found in the bucket.")

# Create the dictionary using a dictionary comprehension
feature_flag_dict = {
flag["name"]: flag["enabled"]
for flag in data
if 'name' in flag and 'enabled' in flag
}
return feature_flag_dict


# Returns a set of SVs that have percentage units.
# (Generated from http://gpaste/6422443047518208)
def get_sdg_percent_vars():
Expand Down
7 changes: 6 additions & 1 deletion server/routes/place/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from server.lib.config import GLOBAL_CONFIG_BUCKET
from server.lib.i18n import AVAILABLE_LANGUAGES
from server.lib.i18n import DEFAULT_LOCALE
from server.lib.shared import is_feature_enabled
import server.routes.dev_place.utils as utils
import server.routes.shared_api.place as place_api
import shared.lib.gcs as gcs
Expand Down Expand Up @@ -302,6 +303,9 @@ def is_seo_experiment_enabled(place_dcid: str, category: str,
def is_dev_place_experiment_enabled(place_dcid: str, locale: str,
request_args: MultiDict[str, str]) -> bool:
"""Determine if dev place experiment should be enabled for the page"""
if not is_feature_enabled("dev_place_experiment"):
return False

# Disable dev place experiment for non-dev environments
# TODO(dwnoble): Remove this before prod release
if os.environ.get('FLASK_ENV') not in [
Expand All @@ -326,7 +330,8 @@ def is_dev_place_experiment_enabled(place_dcid: str, locale: str,
@bp.route('/<path:place_dcid>')
@cache.cached(query_string=True)
def place(place_dcid=None):
if is_dev_place_experiment_enabled(place_dcid, g.locale, flask.request.args):
if is_feature_enabled("dev_place_ga") or is_dev_place_experiment_enabled(
place_dcid, g.locale, flask.request.args):
return dev_place(place_dcid=place_dcid)
redirect_args = dict(flask.request.args)

Expand Down
1 change: 1 addition & 0 deletions server/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
<script>
globalThis.isCustomDC = {{ config['CUSTOM']|int }};
globalThis.STAT_VAR_HIERARCHY_CONFIG = {{ config['STAT_VAR_HIERARCHY_CONFIG'] | tojson }};
globalThis.FEATURE_FLAGS = {{ config['FEATURE_FLAGS'] | tojson }};
globalThis.enableBQ = {{ config['ENABLE_BQ']|int }};
</script>
<script>
Expand Down
17 changes: 12 additions & 5 deletions static/js/components/nl_search_bar/nl_search_bar_header_inline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
* Inline-header version of the NL Search Component - used in Version 2 of the header
*/

import React, { ReactElement } from "react";
import React, { ReactElement, useEffect, useState } from "react";

import { isFeatureEnabled } from "../../shared/util";
import { NlSearchBarImplementationProps } from "../nl_search_bar";
import { AutoCompleteInput } from "./auto_complete_input";

Expand All @@ -32,16 +33,22 @@ const NlSearchBarHeaderInline = ({
onSearch,
shouldAutoFocus,
}: NlSearchBarImplementationProps): ReactElement => {
const [autocompleteEnabled, setAutoCompleteEnabled] = useState(false);
const urlParams = new URLSearchParams(window.location.search);
const isAutopushEnv = window.location.hostname == "autopush.datacommons.org";
const enableAutoComplete =
isAutopushEnv ||
(urlParams.has("ac_on") && urlParams.get("ac_on") == "true");

useEffect(() => {
setAutoCompleteEnabled(
isFeatureEnabled("autocomplete") ||
isAutopushEnv ||
(urlParams.has("ac_on") && urlParams.get("ac_on") == "true")
);
}, []);

return (
<div className="header-search-section">
<AutoCompleteInput
enableAutoComplete={enableAutoComplete}
enableAutoComplete={autocompleteEnabled}
value={value}
invalid={invalid}
placeholder={placeholder}
Expand Down
14 changes: 14 additions & 0 deletions static/js/shared/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,17 @@ export function stripPatternFromQuery(query: string, pattern: string): string {
// returns "population of "
return query.replace(regex, "");
}

export const FEATURE_FLAGS = globalThis.FEATURE_FLAGS;

/**
* Helper method to interact with feature flags.
* @param featureName name of feature for which we want status.
* @returns Bool describing if the feature is enabled
*/
export function isFeatureEnabled(featureName: string): boolean {
if (featureName in FEATURE_FLAGS) {
return FEATURE_FLAGS[featureName];
}
return false;
}

0 comments on commit 96d06dd

Please sign in to comment.