Add type definitions for Stage and FeatureEntry JSON representation (#2844)

* Add type definitions for stage and feature JSON

* remove extra whitespace

* Move TypedDicts to separate file

* Fix rollout merge changes

* TODO comment

* remove unused import

* display "None" for empty intent field values
This commit is contained in:
Daniel Smith 2023-03-27 17:52:37 -07:00 коммит произвёл GitHub
Родитель 82e930c8a7
Коммит 5beeeeda53
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 820 добавлений и 289 удалений

Просмотреть файл

@ -14,11 +14,12 @@
# limitations under the License.
import datetime
from typing import Any
from typing import Any, TypedDict
from google.cloud import ndb # type: ignore
from internals.core_enums import *
from internals.core_models import FeatureEntry, MilestoneSet, Stage
from internals.data_types import StageDict, VerboseFeatureDict
from internals.review_models import Vote, Gate
from internals import approval_defs
@ -72,29 +73,38 @@ def _date_to_str(date: Optional[datetime.datetime]) -> Optional[str]:
return str(date) if date is not None else None
def _val_to_list(items: Optional[list]) -> list:
def _val_to_list(items: list | None) -> list:
"""Returns the given list, or returns an empty list if null."""
return items if items is not None else []
def _stage_attr(
stage: Optional[Stage], field: str, is_mstone: bool=False) -> Optional[Any]:
def _stage_attr(stage: Stage | None, field: str) -> Any:
"""Returns a specified field of a Stage entity."""
if stage is None:
return None
if not is_mstone:
return getattr(stage, field)
return None if stage is None else getattr(stage, field)
if stage.milestones is None:
def _get_milestone_attr(stage: Stage | None, field: str) -> int | None:
"""Returns a specified milestone field of a Stage entity."""
if stage is None or stage.milestones is None:
return None
return getattr(stage.milestones, field)
def _prep_stage_gate_info(
fe: FeatureEntry, d: dict,
# Return type for _prep_stage_info function.
class StagePrepResponse(TypedDict):
proto: Stage | None
dev_trial: Stage | None
ot: Stage | None
extend: Stage | None
ship: Stage | None
rollout: Stage | None
all_stages: list[StageDict]
def _prep_stage_info(
fe: FeatureEntry,
prefetched_stages: list[Stage] | None=None
) -> dict[str, Optional[Stage]]:
"""Adds stage and gate info to the dict and returns major stage info."""
) -> StagePrepResponse:
"""Prepares stage info of a feature to help create JSON dictionaries."""
proto_type = STAGE_TYPES_PROTOTYPE[fe.feature_type]
dev_trial_type = STAGE_TYPES_DEV_TRIAL[fe.feature_type]
ot_type = STAGE_TYPES_ORIGIN_TRIAL[fe.feature_type]
@ -107,18 +117,18 @@ def _prep_stage_gate_info(
prefetched_stages.sort(key=lambda s: s.stage_type)
stages = prefetched_stages
else:
stages = Stage.query(Stage.feature_id == d['id']).order(Stage.stage_type)
stages = Stage.query(
Stage.feature_id == fe.key.integer_id()).order(Stage.stage_type)
major_stages: dict[str, Optional[Stage]] = {
stage_info: StagePrepResponse = {
'proto': None,
'dev_trial': None,
'ot': None,
'extend': None,
'ship': None,
'rollout': None}
# Write a list of stages associated with the feature.
d['stages'] = []
'rollout': None,
# Write a list of all stages associated with the feature.
'all_stages': []}
# Keep track of trial stage indexes so that we can add trial extension
# stages as a property of the trial stage later.
@ -127,296 +137,368 @@ def _prep_stage_gate_info(
stage_dict = stage_to_json_dict(s, fe.feature_type)
# Keep major stages for referencing additional fields.
if s.stage_type == proto_type:
major_stages['proto'] = s
stage_info['proto'] = s
elif s.stage_type == dev_trial_type:
major_stages['dev_trial'] = s
stage_info['dev_trial'] = s
elif s.stage_type == ot_type:
# Keep the stage's index to add trial extensions later.
ot_stage_indexes[s.key.integer_id()] = len(d['stages'])
ot_stage_indexes[s.key.integer_id()] = len(stage_info['all_stages'])
stage_dict['extensions'] = []
major_stages['ot'] = s
stage_info['ot'] = s
elif s.stage_type == extend_type:
# Trial extensions are kept as a list on the associated trial stage dict.
if s.ot_stage_id and s.ot_stage_id in ot_stage_indexes:
(d['stages'][ot_stage_indexes[s.ot_stage_id]]['extensions']
(stage_info['all_stages'][ot_stage_indexes[s.ot_stage_id]]['extensions']
.append(stage_dict))
major_stages['extend'] = s
stage_info['extend'] = s
# No need to append the extension stage to the overall stages list.
continue
elif s.stage_type == ship_type:
major_stages['ship'] = s
stage_info['ship'] = s
elif s.stage_type == rollout_type:
major_stages['rollout'] = s
d['stages'].append(stage_dict)
stage_info['rollout'] = s
stage_info['all_stages'].append(stage_dict)
return major_stages
# This function is a workaround to reference extension stage info on
# origin trial stages.
# TODO(danielrsmith): Remove this once users can manually add trial extension
# stages.
def _add_ot_extension_fields(d: dict):
"""Adds info from the trial extension stage to the OT stage dict."""
extension_stage = Stage.query(
Stage.ot_stage_id == d['id']).get()
if extension_stage is None:
return
d['experiment_extension_reason'] = extension_stage.experiment_extension_reason
d['intent_to_extend_experiment_url'] = (
extension_stage.intent_thread_url)
return stage_info
def stage_to_json_dict(
stage: Stage, feature_type: int | None=None) -> dict[str, Any]:
stage: Stage, feature_type: int | None=None) -> StageDict:
"""Convert a stage entity into a JSON dict."""
# Get feature type if not supplied.
if feature_type is None:
f = FeatureEntry.get_by_id(stage.feature_id)
feature_type = f.feature_type
d: StageDict = {
'id': stage.key.integer_id(),
'feature_id': stage.feature_id,
'stage_type': stage.stage_type,
'intent_stage': INTENT_STAGES_BY_STAGE_TYPE.get(
stage.stage_type, INTENT_NONE),
'pm_emails': stage.pm_emails,
'tl_emails': stage.tl_emails,
'ux_emails': stage.ux_emails,
'te_emails': stage.te_emails,
'intent_thread_url': stage.intent_thread_url,
d: dict[str, Any] = {}
d['id'] = stage.key.integer_id()
d['feature_id'] = stage.feature_id
d['stage_type'] = stage.stage_type
d['intent_stage'] = INTENT_STAGES_BY_STAGE_TYPE.get(
d['stage_type'], INTENT_NONE)
'announcement_url': stage.announcement_url,
'intent_to_experiment_url': stage.intent_thread_url,
'experiment_goals': stage.experiment_goals,
'experiment_risks': stage.experiment_risks,
'origin_trial_feedback_url': stage.origin_trial_feedback_url,
'extensions': [],
'experiment_extension_reason': stage.experiment_extension_reason,
'intent_to_extend_experiment_url': stage.intent_thread_url,
'ot_stage_id': stage.ot_stage_id,
'intent_to_ship_url': stage.intent_thread_url,
'finch_url': stage.finch_url,
# Determine the stage type and handle stage-specific fields.
# TODO(danielrsmith): Change client to use new field names.
milestone_field_names: list[dict] | None = None
'rollout_milestone': stage.rollout_milestone,
'rollout_platforms': stage.rollout_platforms,
'rollout_details': stage.rollout_details,
'rollout_impact': stage.rollout_impact,
'enterprise_policies': stage.enterprise_policies,
# Milestone fields to be populated later.
'desktop_first': None,
'android_first': None,
'ios_first': None,
'webview_first': None,
'desktop_last': None,
'android_last': None,
'ios_last': None,
'webview_last': None,
# TODO(danielrsmith): Change client to use new field names.
# Legacy field names.
'intent_to_implement_url': None,
'intent_to_experiment_url': None,
'intent_to_extend_experiment_url': None,
'intent_to_ship_url': None,
'ready_for_trial_url': stage.announcement_url,
# Legacy milestone field names.
'shipped_milestone': None,
'shipped_android_milestone': None,
'shipped_ios_milestone': None,
'shipped_webview_milestone': None,
'ot_milestone_desktop_start': None,
'ot_milestone_desktop_end': None,
'ot_milestone_android_start': None,
'ot_milestone_android_end': None,
'ot_milestone_webview_start': None,
'ot_milestone_webview_end': None,
'dt_milestone_desktop_start': None,
'dt_milestone_android_start': None,
'dt_milestone_ios_start': None,
'dt_milestone_webview_start': None,
}
# Determine milestone fields to use and intent fields to populate.
milestone_field_names: list[dict[str, str]] | None = None
if d['stage_type'] == STAGE_TYPES_PROTOTYPE[feature_type]:
d['intent_to_implement_url'] = stage.intent_thread_url
elif d['stage_type'] == STAGE_TYPES_DEV_TRIAL[feature_type]:
d['ready_for_trial_url'] = stage.announcement_url
milestone_field_names = MilestoneSet.DEV_TRIAL_MILESTONE_FIELD_NAMES
elif d['stage_type'] == STAGE_TYPES_ORIGIN_TRIAL[feature_type]:
d['intent_to_experiment_url'] = stage.intent_thread_url
d['experiment_goals'] = stage.experiment_goals
d['experiment_risks'] = stage.experiment_risks
d['origin_trial_feedback_url'] = stage.origin_trial_feedback_url
_add_ot_extension_fields(d)
milestone_field_names = MilestoneSet.OT_MILESTONE_FIELD_NAMES
elif d['stage_type'] == STAGE_TYPES_EXTEND_ORIGIN_TRIAL[feature_type]:
d['experiment_extension_reason'] = stage.experiment_extension_reason
d['intent_to_extend_experiment_url'] = stage.intent_thread_url
d['ot_stage_id'] = stage.ot_stage_id
milestone_field_names = MilestoneSet.OT_EXTENSION_MILESTONE_FIELD_NAMES
elif d['stage_type'] == STAGE_TYPES_SHIPPING[feature_type]:
d['intent_to_ship_url'] = stage.intent_thread_url
d['finch_url'] = stage.finch_url
milestone_field_names = MilestoneSet.SHIPPING_MILESTONE_FIELD_NAMES
elif d['stage_type'] == STAGE_TYPES_ROLLOUT[feature_type]:
d['rollout_impact'] = stage.rollout_impact
d['rollout_milestone'] = stage.rollout_milestone
d['rollout_platforms'] = stage.rollout_platforms
d['rollout_details'] = stage.rollout_details
d['enterprise_policies'] = stage.enterprise_policies
# Add milestone fields
# Add milestone fields.
if stage.milestones is not None and milestone_field_names is not None:
for name_info in milestone_field_names:
# The old val name is still used on the client side.
# TODO(danielrsmith): Change client to use new field names.
d[name_info['old']] = getattr(stage.milestones, name_info['new'])
d[name_info['new']] = getattr(stage.milestones, name_info['new'])
d['pm_emails'] = stage.pm_emails
d['tl_emails'] = stage.tl_emails
d['ux_emails'] = stage.ux_emails
d['te_emails'] = stage.te_emails
d['intent_thread_url'] = stage.intent_thread_url
# Must be type ignored until this is (eventually) removed.
d[name_info['old']] = getattr(stage.milestones, name_info['new']) # type: ignore
d[name_info['new']] = getattr(stage.milestones, name_info['new']) # type: ignore
return d
def feature_entry_to_json_verbose(
fe: FeatureEntry, prefetched_stages: list[Stage] | None=None
) -> dict[str, Any]:
) -> VerboseFeatureDict:
"""Returns a verbose dictionary with all info about a feature."""
# Do not convert to JSON if the entity has not been saved.
if not fe.key:
return {}
raise Exception('Unsaved FeatureEntry cannot be converted.')
d: dict[str, Any] = fe.to_dict()
id: int = fe.key.integer_id()
d['id'] = fe.key.integer_id()
# Get stage info, returning it to be more explicitly added.
stage_info = _prep_stage_info(
fe, prefetched_stages=prefetched_stages)
# Get stage and gate info, returning stage info to be more explicitly added.
stages = _prep_stage_gate_info(fe, d, prefetched_stages=prefetched_stages)
# Prototype stage fields.
d['intent_to_implement_url'] = _stage_attr(
stages['proto'], 'intent_thread_url')
# Dev trial stage fields.
d['dt_milestone_desktop_start'] = _stage_attr(
stages['dev_trial'], 'desktop_first', True)
d['dt_milestone_android_start'] = _stage_attr(
stages['dev_trial'], 'android_first', True)
d['dt_milestone_ios_start'] = _stage_attr(
stages['dev_trial'], 'ios_first', True)
d['dt_milestone_webview_start'] = _stage_attr(
stages['dev_trial'], 'webview_first', True)
d['ready_for_trial_url'] = _stage_attr(
stages['dev_trial'], 'announcement_url')
# Origin trial stage fields.
d['ot_milestone_desktop_start'] = _stage_attr(
stages['ot'], 'desktop_first', True)
d['ot_milestone_android_start'] = _stage_attr(
stages['ot'], 'android_first', True)
d['ot_milestone_webview_start'] = _stage_attr(
stages['ot'], 'webview_first', True)
d['ot_milestone_desktop_end'] = _stage_attr(
stages['ot'], 'desktop_last', True)
d['ot_milestone_android_end'] = _stage_attr(
stages['ot'], 'android_last', True)
d['ot_milestone_webview_end'] = _stage_attr(
stages['ot'], 'webview_last', True)
d['origin_trial_feeback_url'] = _stage_attr(
stages['ot'], 'origin_trial_feedback_url')
d['intent_to_experiment_url'] = _stage_attr(
stages['ot'], 'intent_thread_url')
d['experiment_goals'] = _stage_attr(stages['ot'], 'experiment_goals')
d['experiment_risks'] = _stage_attr(stages['ot'], 'experiment_risks')
d['announcement_url'] = _stage_attr(stages['ot'], 'announcement_url')
# Extend origin trial stage fields.
d['experiment_extension_reason'] = _stage_attr(
stages['extend'], 'experiment_extension_reason')
d['intent_to_extend_experiment_url'] = _stage_attr(
stages['extend'], 'intent_thread_url')
# Ship stage fields.
d['intent_to_ship_url'] = _stage_attr(stages['ship'], 'intent_thread_url')
d['finch_url'] = _stage_attr(stages['ship'], 'finch_url')
d['rollout_impact'] = _stage_attr(stages['rollout'], 'rollout_impact')
d['rollout_milestone'] = _stage_attr(stages['rollout'], 'rollout_milestone')
d['rollout_platforms'] = _stage_attr(stages['rollout'], 'rollout_platforms')
d['rollout_details'] = _stage_attr(stages['rollout'], 'rollout_details')
d['enterprise_policies'] = _stage_attr(stages['rollout'], 'enterprise_policies')
# TODO(danielrsmith): Adjust the references to this JSON to use
# the new renamed field names.
impl_status_chrome = d.pop('impl_status_chrome', None)
standard_maturity = d.pop('standard_maturity', None)
d['is_released'] = fe.impl_status_chrome in RELEASE_IMPL_STATES
d['category'] = FEATURE_CATEGORIES[fe.category]
d['category_int'] = fe.category
d['enterprise_feature_categories'] = d.pop('enterprise_feature_categories', [])
if fe.feature_type is not None:
d['feature_type'] = FEATURE_TYPES[fe.feature_type]
d['feature_type_int'] = fe.feature_type
d['is_enterprise_feature'] = fe.feature_type == FEATURE_TYPE_ENTERPRISE_ID
if fe.intent_stage is not None:
d['intent_stage'] = INTENT_STAGES.get(
fe.intent_stage, INTENT_STAGES[INTENT_NONE])
d['intent_stage_int'] = fe.intent_stage
d['active_stage_id'] = fe.active_stage_id
d['created'] = {
'by': d.pop('creator_email', None),
'when': _date_to_str(fe.created),
}
d['updated'] = {
'by': d.pop('updater_email', None),
'when': _date_to_str(fe.updated),
}
d['accurate_as_of'] = _date_to_str(fe.accurate_as_of)
d['standards'] = {
'spec': fe.spec_link,
'maturity': {
'text': STANDARD_MATURITY_CHOICES.get(standard_maturity),
'short_text': STANDARD_MATURITY_SHORT.get(standard_maturity),
'val': standard_maturity,
d: VerboseFeatureDict = {
'id': id,
'name': fe.name,
'summary': fe.summary,
'blink_components': fe.blink_components or [],
'star_count': fe.star_count,
'search_tags': fe.search_tags or [],
'created': {
'by': fe.creator_email,
'when': _date_to_str(fe.created),
},
}
d['spec_mentors'] = fe.spec_mentor_emails
d['tag_review_status'] = REVIEW_STATUS_CHOICES[fe.tag_review_status]
d['tag_review_status_int'] = fe.tag_review_status
d['security_review_status'] = REVIEW_STATUS_CHOICES[
fe.security_review_status]
d['security_review_status_int'] = fe.security_review_status
d['privacy_review_status'] = REVIEW_STATUS_CHOICES[fe.privacy_review_status]
d['privacy_review_status_int'] = fe.privacy_review_status
d['resources'] = {
'samples': _val_to_list(fe.sample_links),
'docs': _val_to_list(fe.doc_links),
}
d['tags'] = d.pop('search_tags', None)
d['editors'] = d.pop('editor_emails', [])
d['cc_recipients'] = d.pop('cc_emails', [])
d['creator'] = fe.creator_email
d['comments'] = d.pop('feature_notes', None)
'updated': {
'by': fe.updater_email,
'when': _date_to_str(fe.updated),
},
'category': FEATURE_CATEGORIES[fe.category],
'category_int': fe.category,
'feature_notes': fe.feature_notes,
'enterprise_feature_categories': fe.enterprise_feature_categories or [],
'stages': stage_info['all_stages'],
'accurate_as_of': _date_to_str(fe.accurate_as_of),
'creator_email': fe.creator_email,
'updater_email': fe.updater_email,
'owner_emails': fe.owner_emails or [],
'editor_emails': fe.editor_emails or [],
'cc_emails': fe.cc_emails or [],
'spec_mentor_emails': fe.spec_mentor_emails or [],
'unlisted': fe.unlisted,
'deleted': fe.deleted,
'editors': fe.editor_emails or [],
'cc_recipients': fe.cc_emails or [],
'spec_mentors': fe.spec_mentor_emails or [],
'creator': fe.creator_email,
'feature_type': FEATURE_TYPES[fe.feature_type],
'feature_type_int': fe.feature_type,
'intent_stage': INTENT_STAGES.get(
fe.intent_stage, INTENT_STAGES[INTENT_NONE]),
'intent_stage_int': fe.intent_stage,
'active_stage_id': fe.active_stage_id,
'bug_url': fe.bug_url,
'launch_bug_url': fe.launch_bug_url,
'new_crbug_url': None,
'breaking_change': fe.breaking_change,
'flag_name': fe.flag_name,
'ongoing_constraints': fe.ongoing_constraints,
'motivation': fe.motivation,
'devtrial_instructions': fe.devtrial_instructions,
'activation_risks': fe.activation_risks,
'measurement': fe.measurement,
'availability_expectation': fe.availability_expectation,
'adoption_expectation': fe.adoption_expectation,
'adoption_plan': fe.adoption_plan,
'initial_public_proposal_url': fe.initial_public_proposal_url,
'explainer_links': fe.explainer_links,
'requires_embedder_support': fe.requires_embedder_support,
'spec_link': fe.spec_link,
'api_spec': fe.api_spec,
'interop_compat_risks': fe.interop_compat_risks,
'all_platforms': fe.all_platforms,
'all_platforms_descr': fe.all_platforms_descr,
'non_oss_deps': fe.non_oss_deps,
'anticipated_spec_changes': fe.anticipated_spec_changes,
'security_risks': fe.security_risks,
'ergonomics_risks': fe.ergonomics_risks,
'wpt': fe.wpt,
'wpt_descr': fe.wpt_descr,
'webview_risks': fe.webview_risks,
'devrel_emails': fe.devrel_emails or [],
'debuggability': fe.debuggability,
'doc_links': fe.doc_links or [],
'sample_links': fe.sample_links or [],
'prefixed': fe.prefixed,
'tags': fe.search_tags,
'tag_review': fe.tag_review,
'tag_review_status': REVIEW_STATUS_CHOICES[fe.tag_review_status],
'tag_review_status_int': fe.tag_review_status,
'security_review_status': REVIEW_STATUS_CHOICES[fe.security_review_status],
'security_review_status_int': fe.security_review_status,
'privacy_review_status': REVIEW_STATUS_CHOICES[fe.privacy_review_status],
'privacy_review_status_int': fe.privacy_review_status,
'updated_display': None,
'resources': {
'samples': fe.sample_links or [],
'docs': fe.doc_links or [],
},
'comments': fe.feature_notes,
'ff_views': fe.ff_views or NO_PUBLIC_SIGNALS,
'safari_views': fe.safari_views or NO_PUBLIC_SIGNALS,
'web_dev_views': fe.web_dev_views or DEV_NO_SIGNALS,
'browsers': {
'chrome': {
'bug': fe.bug_url,
'blink_components': fe.blink_components or [],
'devrel': fe.devrel_emails or [],
'owners': fe.owner_emails or [],
'origintrial': fe.impl_status_chrome == ORIGIN_TRIAL,
'intervention': fe.impl_status_chrome == INTERVENTION,
'prefixed': fe.prefixed,
'flag': fe.impl_status_chrome == BEHIND_A_FLAG,
'status': {
'text': IMPLEMENTATION_STATUS[fe.impl_status_chrome],
'val': fe.impl_status_chrome,
'milestone_str': None
},
# TODO(danielrsmith): Find out if these are used and delete if not.
'desktop': _get_milestone_attr(stage_info['ship'], 'desktop_first'),
'android': _get_milestone_attr(stage_info['ship'], 'android_first'),
'webview': _get_milestone_attr(stage_info['ship'], 'webview_first'),
'ios': _get_milestone_attr(stage_info['ship'], 'ios_first'),
ff_views = d.pop('ff_views', NO_PUBLIC_SIGNALS)
safari_views = d.pop('safari_views', NO_PUBLIC_SIGNALS)
web_dev_views = d.pop('web_dev_views', DEV_NO_SIGNALS)
d['browsers'] = {
'chrome': {
'bug': fe.bug_url,
'blink_components': d.pop('blink_components', []),
'devrel': _val_to_list(fe.devrel_emails),
'owners': d.pop('owner_emails', []),
'origintrial': fe.impl_status_chrome == ORIGIN_TRIAL,
'intervention': fe.impl_status_chrome == INTERVENTION,
'prefixed': fe.prefixed,
'flag': fe.impl_status_chrome == BEHIND_A_FLAG,
'status': {
'text': IMPLEMENTATION_STATUS[impl_status_chrome],
'val': impl_status_chrome
},
'desktop': _stage_attr(stages['ship'], 'desktop_first', True),
'android': _stage_attr(stages['ship'], 'android_first', True),
'webview': _stage_attr(stages['ship'], 'webview_first', True),
'ios': _stage_attr(stages['ship'], 'ios_first', True),
'ff': {
'view': {
'text': VENDOR_VIEWS.get(
fe.ff_views, VENDOR_VIEWS_COMMON[NO_PUBLIC_SIGNALS]),
'val': fe.ff_views if fe.ff_views in VENDOR_VIEWS else NO_PUBLIC_SIGNALS,
'url': fe.ff_views_link,
'notes': fe.ff_views_notes,
},
},
'safari': {
'view': {
'text': VENDOR_VIEWS.get(
fe.safari_views,VENDOR_VIEWS_COMMON[NO_PUBLIC_SIGNALS]),
'val': (fe.safari_views if fe.safari_views in VENDOR_VIEWS
else NO_PUBLIC_SIGNALS),
'url': fe.safari_views_link,
'notes': fe.safari_views_notes,
},
},
'webdev': {
'view': {
'text': WEB_DEV_VIEWS.get(fe.web_dev_views, WEB_DEV_VIEWS[DEV_NO_SIGNALS]),
'val': (fe.web_dev_views if fe.web_dev_views in WEB_DEV_VIEWS
else DEV_NO_SIGNALS),
'url': fe.web_dev_views_link,
'notes': fe.web_dev_views_notes,
},
},
'other': {
'view': {
'text': None,
'val': None,
'url': None,
'notes':fe.other_views_notes,
},
},
},
'ff': {
'view': {
'text': VENDOR_VIEWS.get(ff_views,
VENDOR_VIEWS_COMMON[NO_PUBLIC_SIGNALS]),
'val': ff_views if ff_views in VENDOR_VIEWS else NO_PUBLIC_SIGNALS,
'url': d.pop('ff_views_link', None),
'notes': d.pop('ff_views_notes'),
}
},
'safari': {
'view': {
'text': VENDOR_VIEWS.get(safari_views,
VENDOR_VIEWS_COMMON[NO_PUBLIC_SIGNALS]),
'val': (safari_views if safari_views in VENDOR_VIEWS
else NO_PUBLIC_SIGNALS),
'url': d.pop('safari_views_link', None),
'notes': d.pop('safari_views_notes', None),
}
},
'webdev': {
'view': {
'text': WEB_DEV_VIEWS.get(web_dev_views,
WEB_DEV_VIEWS[DEV_NO_SIGNALS]),
'val': (web_dev_views if web_dev_views in WEB_DEV_VIEWS
else DEV_NO_SIGNALS),
'url': d.pop('web_dev_views_link', None),
'notes': d.pop('web_dev_views_notes', None),
}
},
'other': {
'view': {
'notes': d.pop('other_views_notes', None),
}
'enterprise_feature_categories': fe.enterprise_feature_categories or [],
'standards': {
'spec': fe.spec_link,
'maturity': {
'text': STANDARD_MATURITY_CHOICES.get(fe.standard_maturity),
'short_text': STANDARD_MATURITY_SHORT.get(fe.standard_maturity),
'val': fe.standard_maturity,
},
},
'is_released': fe.impl_status_chrome in RELEASE_IMPL_STATES,
'is_enterprise_feature': fe.feature_type == FEATURE_TYPE_ENTERPRISE_ID,
'experiment_timeline': fe.experiment_timeline,
# TODO(danielrsmith): Adjust the references to this JSON to use
# the new renamed field names.
# Prototype stage fields.
'intent_to_implement_url': _stage_attr(
stage_info['proto'], 'intent_thread_url'),
# Dev trial stage fields.
'dt_milestone_desktop_start': _get_milestone_attr(
stage_info['dev_trial'], 'desktop_first'),
'dt_milestone_android_start': _get_milestone_attr(
stage_info['dev_trial'], 'android_first'),
'dt_milestone_ios_start': _get_milestone_attr(
stage_info['dev_trial'], 'ios_first'),
'dt_milestone_webview_start': _get_milestone_attr(
stage_info['dev_trial'], 'webview_first'),
'ready_for_trial_url': _stage_attr(
stage_info['dev_trial'], 'announcement_url'),
# Origin trial stage fields.
'ot_milestone_desktop_start': _get_milestone_attr(
stage_info['ot'], 'desktop_first'),
'ot_milestone_android_start': _get_milestone_attr(
stage_info['ot'], 'android_first'),
'ot_milestone_webview_start': _get_milestone_attr(
stage_info['ot'], 'webview_first'),
'ot_milestone_desktop_end': _get_milestone_attr(
stage_info['ot'], 'desktop_last'),
'ot_milestone_android_end': _get_milestone_attr(
stage_info['ot'], 'android_last'),
'ot_milestone_webview_end': _get_milestone_attr(
stage_info['ot'], 'webview_last'),
'origin_trial_feeback_url': _stage_attr(
stage_info['ot'], 'origin_trial_feedback_url'),
'intent_to_experiment_url': _stage_attr(
stage_info['ot'], 'intent_thread_url'),
'experiment_goals': _stage_attr(stage_info['ot'], 'experiment_goals'),
'experiment_risks': _stage_attr(stage_info['ot'], 'experiment_risks'),
'announcement_url': _stage_attr(stage_info['ot'], 'announcement_url'),
# Extend origin trial stage fields.
'experiment_extension_reason': _stage_attr(
stage_info['extend'], 'experiment_extension_reason'),
'intent_to_extend_experiment_url': _stage_attr(
stage_info['extend'], 'intent_thread_url'),
# Ship stage fields.
'intent_to_ship_url': _stage_attr(stage_info['ship'], 'intent_thread_url'),
'finch_url': _stage_attr(stage_info['ship'], 'finch_url'),
'rollout_milestone': _stage_attr(stage_info['rollout'], 'rollout_milestone'),
'rollout_details': _stage_attr(stage_info['rollout'], 'rollout_details'),
'rollout_impact': _stage_attr(stage_info['rollout'], 'rollout_impact'),
}
if d['is_released'] and _stage_attr(stages['ship'], 'desktop_first', True):
d['browsers']['chrome']['status']['milestone_str'] = (
_stage_attr(stages['ship'], 'desktop_first', True))
elif d['is_released'] and _stage_attr(stages['ship'], 'android_first', True):
d['browsers']['chrome']['status']['milestone_str'] = (
_stage_attr(stages['ship'], 'android_first', True))
if (d['is_released'] and
_get_milestone_attr(stage_info['ship'], 'desktop_first')):
d['browsers']['chrome']['status']['milestone_str'] = str(
_get_milestone_attr(stage_info['ship'], 'desktop_first'))
elif (d['is_released'] and
_get_milestone_attr(stage_info['ship'], 'android_first')):
d['browsers']['chrome']['status']['milestone_str'] = str(
_get_milestone_attr(stage_info['ship'], 'android_first'))
else:
d['browsers']['chrome']['status']['milestone_str'] = (
d['browsers']['chrome']['status']['text'])
del_none(d) # Further prune response by removing null/[] values.
return d

Просмотреть файл

@ -248,9 +248,9 @@ class FeatureConvertersTest(testing_config.CustomTestCase):
'other': {
'view': {
'notes': 'other notes',
}
}
}
},
},
},
}
self.assertEqual(result, expected)
@ -333,7 +333,6 @@ class FeatureConvertersTest(testing_config.CustomTestCase):
'when': str(self.date)
},
'accurate_as_of': str(self.date),
'outstanding_notifications': 0,
'resources': {
'samples': ['https://example.com/samples'],
'docs': ['https://example.com/docs'],
@ -347,6 +346,64 @@ class FeatureConvertersTest(testing_config.CustomTestCase):
'val': 1,
},
},
'activation_risks': None,
'active_stage_id': None,
'adoption_expectation': None,
'adoption_plan': None,
'all_platforms': None,
'all_platforms_descr': None,
'anticipated_spec_changes': None,
'availability_expectation': None,
'blink_components': ['Blink'],
'cc_emails': [],
'cc_recipients': [],
'creator_email': 'creator@example.com',
'debuggability': None,
'devtrial_instructions': None,
'dt_milestone_ios_start': None,
'dt_milestone_webview_start': None,
'editor_emails': ['feature_editor@example.com', 'owner_1@example.com'],
'enterprise_feature_categories': [],
'ergonomics_risks': None,
'experiment_timeline': None,
'explainer_links': [],
'feature_notes': 'notes',
'ff_views': 5,
'flag_name': None,
'initial_public_proposal_url': None,
'interop_compat_risks': None,
'measurement': None,
'motivation': None,
'new_crbug_url': None,
'non_oss_deps': None,
'ongoing_constraints': None,
'origin_trial_feeback_url': None,
'ot_milestone_android_end': None,
'ot_milestone_webview_end': None,
'ot_milestone_webview_start': None,
'owner_emails': ['feature_owner@example.com'],
'ot_milestone_webview_end': None,
'ot_milestone_webview_start': None,
'owner_emails': ['feature_owner@example.com'],
'ready_for_trial_url': None,
'rollout_details': None,
'rollout_milestone': None,
'safari_views': 1,
'search_tags': [],
'security_risks': None,
'spec_mentor_emails': [],
'spec_mentors': [],
'tag_review': None,
'tags': [],
'updated_display': None,
'updater_email': 'updater@example.com',
'web_dev_views': 1,
'webview_risks': None,
'wpt': None,
'wpt_descr': None,
'tag_review_status': 'Pending',
'tag_review_status_int': 1,
'security_review_status': 'Issues open',
@ -364,12 +421,15 @@ class FeatureConvertersTest(testing_config.CustomTestCase):
'owners':['feature_owner@example.com'],
'desktop': 1,
'android': 1,
'ios': None,
'origintrial': False,
'intervention': False,
'prefixed': False,
'flag': False,
'webview': None,
'status': {
'milestone_str': 1,
'milestone_str': '1',
'text': 'Enabled by default',
'val': 5
}
@ -392,6 +452,7 @@ class FeatureConvertersTest(testing_config.CustomTestCase):
},
'webdev': {
'view': {
'notes': None,
'text': 'Strongly positive',
'val': 1,
'url': 'https://example.com/web_dev',
@ -399,10 +460,13 @@ class FeatureConvertersTest(testing_config.CustomTestCase):
},
'other': {
'view': {
'notes': 'other notes',
}
}
}
'notes': 'other notes',
'text': None,
'url': None,
'val': None,
},
},
},
}
self.assertEqual(result, expected)
@ -430,9 +494,8 @@ class FeatureConvertersTest(testing_config.CustomTestCase):
"""Function handles an empty feature."""
empty_fe = FeatureEntry()
result = converters.feature_entry_to_json_verbose(empty_fe)
self.assertEqual(result, {})
with self.assertRaises(Exception):
converters.feature_entry_to_json_verbose(empty_fe)
class VoteConvertersTest(testing_config.CustomTestCase):

Просмотреть файл

@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Any, Optional
from typing import Optional, TypedDict
from api import converters
from framework import basehandlers
@ -22,19 +22,21 @@ from framework import rediscache
from framework import users
from internals.core_enums import *
from internals.core_models import FeatureEntry
from internals.data_types import VerboseFeatureDict
from internals import feature_helpers
from internals import search
class FeaturesAPI(basehandlers.APIHandler):
"""Features are the the main records that we track."""
def get_one_feature(self, feature_id: int) -> dict[str, Any]:
def get_one_feature(self, feature_id: int) -> VerboseFeatureDict:
feature = FeatureEntry.get_by_id(feature_id)
if not feature:
self.abort(404, msg='Feature %r not found' % feature_id)
return converters.feature_entry_to_json_verbose(feature)
def do_search(self) -> dict[str, Any]:
def do_search(self):
user = users.get_current_user()
# Show unlisted features to site editors or admins.
show_unlisted_features = permissions.can_edit_any_feature(user)
@ -69,8 +71,11 @@ class FeaturesAPI(basehandlers.APIHandler):
'features': features_on_page,
}
def do_get(self, **kwargs) -> dict[str, Any]:
def do_get(self, **kwargs):
"""Handle GET requests for a single feature or a search."""
# TODO(danielrsmith): This request gives two independent return types
# based on whether a feature_id was specified. Determine the best
# way to handle this in a strictly-typed manner and implement it.
feature_id = kwargs.get('feature_id', None)
if feature_id:
return self.get_one_feature(feature_id)

Просмотреть файл

@ -74,31 +74,55 @@ class StagesAPITest(testing_config.CustomTestCase):
self.stage_5.put()
self.expected_stage_1 = {
'id': 10,
'feature_id': 1,
'stage_type': 150,
'intent_stage': 3,
'pm_emails': [],
'tl_emails': [],
'ux_emails': ['ux_person@example.com'],
'te_emails': [],
'intent_thread_url': 'https://example.com/intent',
'intent_to_experiment_url': 'https://example.com/intent',
'desktop_first': 100,
'desktop_last': None,
'android_first': None,
'android_last': None,
'webview_first': None,
'webview_last': None,
'announcement_url': None,
'desktop_first': 100,
'desktop_last': None,
'dt_milestone_android_start': None,
'dt_milestone_desktop_start': None,
'dt_milestone_ios_start': None,
'dt_milestone_webview_start': None,
'enterprise_policies': [],
'experiment_extension_reason': None,
'experiment_goals': 'To be the very best.',
'experiment_risks': None,
'extensions': [],
'feature_id': 1,
'finch_url': None,
'id': 10,
'intent_stage': 3,
'intent_thread_url': 'https://example.com/intent',
'intent_to_experiment_url': 'https://example.com/intent',
'intent_to_extend_experiment_url': None,
'intent_to_implement_url': None,
'intent_to_ship_url': None,
'ios_first': None,
'ios_last': None,
'origin_trial_feedback_url': None,
'ot_milestone_android_end': None,
'ot_milestone_android_start': None,
'ot_milestone_desktop_end': None,
'ot_milestone_desktop_start': 100,
'ot_milestone_webview_end': None,
'ot_milestone_webview_start': None,
'origin_trial_feedback_url': None}
'ot_stage_id': None,
'pm_emails': [],
'ready_for_trial_url': None,
'rollout_details': None,
'rollout_impact': 2,
'rollout_milestone': None,
'rollout_platforms': [],
'shipped_android_milestone': None,
'shipped_ios_milestone': None,
'shipped_milestone': None,
'shipped_webview_milestone': None,
'stage_type': 150,
'te_emails': [],
'tl_emails': [],
'ux_emails': ['ux_person@example.com'],
'webview_first': None,
'webview_last': None}
self.handler = stages_api.StagesAPI()
self.request_path = '/api/v0/features/'
@ -164,7 +188,31 @@ class StagesAPITest(testing_config.CustomTestCase):
'ot_milestone_desktop_start': 100,
'ot_milestone_webview_end': None,
'ot_milestone_webview_start': None,
'origin_trial_feedback_url': None}
'origin_trial_feedback_url': None,
'announcement_url': None,
'dt_milestone_android_start': None,
'dt_milestone_desktop_start': None,
'dt_milestone_ios_start': None,
'dt_milestone_webview_start': None,
'enterprise_policies': [],
'experiment_extension_reason': None,
'extensions': [],
'finch_url': None,
'intent_to_extend_experiment_url': None,
'intent_to_implement_url': None,
'intent_to_ship_url': None,
'ios_first': None,
'ios_last': None,
'ot_stage_id': 40,
'ready_for_trial_url': None,
'rollout_details': None,
'rollout_impact': 2,
'rollout_milestone': None,
'rollout_platforms': [],
'shipped_android_milestone': None,
'shipped_ios_milestone': None,
'shipped_milestone': None,
'shipped_webview_milestone': None}
expect = {
'id': 40,
@ -177,7 +225,7 @@ class StagesAPITest(testing_config.CustomTestCase):
'te_emails': [],
'intent_thread_url': 'https://example.com/intent',
'intent_to_experiment_url': 'https://example.com/intent',
'intent_to_extend_experiment_url': 'https://example.com/intent',
'intent_to_extend_experiment_url': None,
'desktop_first': 100,
'desktop_last': None,
'android_first': None,
@ -188,13 +236,36 @@ class StagesAPITest(testing_config.CustomTestCase):
'experiment_goals': 'To be the very best.',
'experiment_risks': None,
'extensions': [extension],
'announcement_url': None,
'ot_milestone_android_end': None,
'ot_milestone_android_start': None,
'ot_milestone_desktop_end': None,
'ot_milestone_desktop_start': 100,
'ot_milestone_webview_end': None,
'ot_milestone_webview_start': None,
'origin_trial_feedback_url': None}
'dt_milestone_android_start': None,
'dt_milestone_desktop_start': None,
'dt_milestone_ios_start': None,
'dt_milestone_webview_start': None,
'enterprise_policies': [],
'origin_trial_feedback_url': None,
'ready_for_trial_url': None,
'rollout_details': None,
'rollout_impact': 2,
'rollout_milestone': None,
'rollout_platforms': [],
'shipped_android_milestone': None,
'shipped_ios_milestone': None,
'shipped_milestone': None,
'shipped_webview_milestone': None,
'ot_stage_id': None,
'intent_to_implement_url': None,
'intent_to_ship_url': None,
'ios_first': None,
'ios_last': None,
'finch_url': None,
}
with test_app.test_request_context(f'{self.request_path}1/stages/10'):
actual = self.handler.do_get(feature_id=1, stage_id=40)

Просмотреть файл

@ -171,8 +171,6 @@ class MilestoneSet(ndb.Model): # copy from milestone fields of Feature
'ot_milestone_desktop_end': 'desktop_last',
'ot_milestone_android_start': 'android_first',
'ot_milestone_android_end': 'android_last',
'ot_milestone_ios_start': 'ios_first',
'ot_milestone_ios_end': 'ios_last',
'ot_milestone_webview_start': 'webview_first',
'ot_milestone_webview_end': 'webview_last',
'dt_milestone_desktop_start': 'desktop_first',

312
internals/data_types.py Normal file
Просмотреть файл

@ -0,0 +1,312 @@
# -*- coding: utf-8 -*-
# Copyright 2023 Google Inc.
#
# 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 needed to reference a class within its own class method.
# https://stackoverflow.com/a/33533514
from __future__ import annotations
from typing import TypedDict
from internals.core_enums import *
# JSON representation of Stage entity data.
class StageDict(TypedDict):
id: int
feature_id: int
stage_type: int
intent_stage: int
intent_thread_url: str | None
# Dev trial specific fields.
ready_for_trial_url: str | None
announcement_url: str | None
# Origin trial specific fields.
experiment_goals: str | None
experiment_risks: str | None
extensions: list[StageDict] # type: ignore
origin_trial_feedback_url: str | None
# Trial extension specific fields.
ot_stage_id: int | None
experiment_extension_reason: str | None
# Ship specific fields
finch_url: str | None
# Enterprise specific fields
rollout_details: str | None
rollout_impact: int | None
rollout_milestone: int | None
rollout_platforms: list[str]
enterprise_policies: list[str]
# Email information
pm_emails: list[str]
tl_emails: list[str]
ux_emails: list[str]
te_emails: list[str]
# Milestone fields
desktop_first: int | None
android_first: int | None
ios_first: int | None
webview_first: int | None
desktop_last: int | None
android_last: int | None
ios_last: int | None
webview_last: int | None
# Legacy fields or fields that now live on stage entities.
# TODO(danielrsmith): stop representing these on the feature dict
# and references them direct from stage entities.
intent_to_implement_url: str | None
intent_to_experiment_url: str | None
intent_to_extend_experiment_url: str | None
intent_to_ship_url: str | None
# Legacy milestone fields that now live on stage entities.
dt_milestone_desktop_start: int | None
dt_milestone_android_start: int | None
dt_milestone_ios_start: int | None
dt_milestone_webview_start: int | None
ot_milestone_desktop_start: int | None
ot_milestone_android_start: int | None
ot_milestone_webview_start: int | None
ot_milestone_desktop_end: int | None
ot_milestone_android_end: int | None
ot_milestone_webview_end: int | None
shipped_milestone: int | None
shipped_android_milestone: int | None
shipped_ios_milestone: int | None
shipped_webview_milestone: int | None
#############################
## FeatureDict definitions ##
#############################
# Nested JSON type definitions.
class FeatureDictInnerResourceInfo(TypedDict):
samples: list[str]
docs: list[str]
class FeatureDictInnerStandardsInfo(TypedDict):
spec: str | None
maturity: FeatureDictInnerMaturityInfo
class FeatureDictInnerMaturityInfo(TypedDict):
text: str | None
short_text: str | None
val: int
class FeatureDictInnerBrowserStatus(TypedDict):
text: str | None
val: str | None
milestone_str: str | None
class FeatureDictInnerViewInfo(TypedDict):
text: str | None
val: int | None
url: str | None
notes: str | None
class FeatureDictInnerChromeBrowserInfo(TypedDict):
bug: str | None
blink_components: list[str] | None
devrel: list[str] | None
owners: list[str] | None
origintrial: bool | None
intervention: bool | None
prefixed: bool | None
flag: bool | None
status: FeatureDictInnerBrowserStatus
desktop: int | None
android: int | None
webview: int | None
ios: int | None
class FeatureDictInnerSingleBrowserInfo(TypedDict):
view: FeatureDictInnerViewInfo | None
class FeatureBrowsersInfo(TypedDict):
chrome: FeatureDictInnerChromeBrowserInfo
ff: FeatureDictInnerSingleBrowserInfo
safari: FeatureDictInnerSingleBrowserInfo
webdev: FeatureDictInnerSingleBrowserInfo
other: FeatureDictInnerSingleBrowserInfo
# Basic user info displayed for create/update attributes in
# FeatureEntry edit information.
class FeatureDictInnerUserEditInfo(TypedDict):
by: str | None
when: str | None
# JSON representation of FeatureEntry entity. Created from
# converters.feature_entry_to_json_verbose().
class VerboseFeatureDict(TypedDict):
# Metadata: Creation and updates.
id: int
created: FeatureDictInnerUserEditInfo
updated: FeatureDictInnerUserEditInfo
accurate_as_of: str | None
creator_email: str | None
updater_email: str | None
# Metadata: Access controls
owner_emails: list[str]
editor_emails: list[str]
cc_emails: list[str]
spec_mentor_emails: list[str]
unlisted: bool
deleted: bool
# Renamed metadata fields
editors: list[str]
cc_recipients: list[str]
spec_mentors: list[str]
creator: str | None
# Descriptive info.
name: str
summary: str
category: str
category_int: int
blink_components: list[str]
star_count: int
search_tags: list[str]
feature_notes: str | None
enterprise_feature_categories: list[str]
# Metadata: Process information
feature_type: str
feature_type_int: int
intent_stage: str
intent_stage_int: int
active_stage_id: int | None
bug_url: str | None
launch_bug_url: str | None
breaking_change: bool
# Implementation in Chrome
flag_name: str | None
ongoing_constraints: str | None
# Topic: Adoption
motivation: str | None
devtrial_instructions: str | None
activation_risks: str | None
measurement: str | None
availability_expectation: str | None
adoption_expectation: str | None
adoption_plan: str | None
# Gate: Standardization and Interop
initial_public_proposal_url: str | None
explainer_links: list[str]
requires_embedder_support: bool
spec_link: str | None
api_spec: str | None
prefixed: bool | None
interop_compat_risks: str | None
all_platforms: bool | None
all_platforms_descr: bool | None
tag_review: str | None
non_oss_deps: str | None
anticipated_spec_changes: str | None
# Gate: Security & Privacy
security_risks: str | None
tags: list[str]
tag_review_status: str
tag_review_status_int: int | None
security_review_status: str
security_review_status_int: int | None
privacy_review_status: str
privacy_review_status_int: int | None
# Gate: Testing / Regressions
ergonomics_risks: str | None
wpt: bool | None
wpt_descr: str | None
webview_risks: str | None
# Gate: Devrel & Docs
devrel_emails: list[str]
debuggability: str | None
doc_links: list[str]
sample_links: list[str]
stages: list[StageDict]
# Legacy fields or fields that now live on stage entities.
# TODO(danielrsmith): stop representing these on the feature dict
# and references them direct from stage entities.
intent_to_implement_url: str | None
intent_to_experiment_url: str | None
intent_to_extend_experiment_url: str | None
intent_to_ship_url: str | None
ready_for_trial_url: str | None
origin_trial_feeback_url: str | None
experiment_goals: str | None
experiment_risks: str | None
announcement_url: str | None
experiment_extension_reason: str | None
rollout_details: str | None
rollout_impact: int | None
rollout_milestone: int | None
experiment_timeline: str | None
resources: FeatureDictInnerResourceInfo
comments: str | None # feature_notes
# Repeated in 'browsers' section. TODO(danielrsmith): delete these?
ff_views: int
safari_views: int
web_dev_views: int
browsers: FeatureBrowsersInfo
# Legacy milestone fields that now live on stage entities.
dt_milestone_desktop_start: int | None
dt_milestone_android_start: int | None
dt_milestone_ios_start: int | None
dt_milestone_webview_start: int | None
ot_milestone_desktop_start: int | None
ot_milestone_android_start: int | None
ot_milestone_webview_start: int | None
ot_milestone_desktop_end: int | None
ot_milestone_android_end: int | None
ot_milestone_webview_end: int | None
finch_url: str | None
standards: FeatureDictInnerStandardsInfo
is_released: bool
is_enterprise_feature: bool
updated_display: str | None
new_crbug_url: str | None

Просмотреть файл

@ -167,7 +167,7 @@ limitations under the License.
<br><br><h4>Specification</h4>
None
@ -181,16 +181,16 @@ limitations under the License.
<br><br><h4>Motivation</h4>
<p class="preformatted"></p>
<p class="preformatted">None</p>
<br><br><h4>Initial public proposal</h4>
None
<br><br><h4>TAG review</h4>
None
<br><br><h4>TAG review status</h4>
@ -201,7 +201,7 @@ limitations under the License.
<br><br><h4>Risks</h4>
<div style="margin-left: 4em;">
<br><br><h4>Interoperability and Compatibility</h4>
<p class="preformatted"></p>
<p class="preformatted">None</p>
<br><br><i>Gecko</i>: No signal
@ -229,14 +229,14 @@ limitations under the License.
Does this intent deprecate or change behavior of existing APIs,
such that it has potentially high risk for Android WebView-based
applications?</p>
<p class="preformatted"></p>
<p class="preformatted">None</p>
</div> <!-- end risks -->
<br><br><h4>Debuggability</h4>
<p class="preformatted"></p>
<p class="preformatted">None</p>
@ -247,7 +247,7 @@ No
<br><br><h4>Flag name</h4>
None
<br><br><h4>Requires code in //chrome?</h4>
False