Skip to content

Commit 3e9c682

Browse files
yashikakhuranaYashika KhuranaYashika Khurana
authored
feat(nimbus): Use metric-config-parser for feature monitoring configs (#9229)
* feat(nimbus): Use metric-config-parser for feature monitoring configs - Replace yaml + tomllib with NimbusFeatureMonitoringSpec from metric-config-parser - Change is_events_stream bool to type string (event_stream / metrics / clients_daily) - Add --config-path CLI option to point generator at metric-hub checkout - Configs now live in metric-hub/nimbus_feature_monitoring/ as TOML files Relates to EXP-6732 * feat(nimbus): Nimbus feature monitoring * feat(nimbus): Nimbus feature monitoring * feat(nimbus): Nimbus feature monitoring * feat(nimbus): bump mozilla-metric-config-parser to 2026.4.3 * fix(nimbus): update query template to use source_table.type instead of is_events_stream * feat(nimbus): remove --config-repo option, auto-clone metric-hub via from_github_repo * revert(nimbus): remove telemetry-alerts from bqetl_marketing_analysis email list * feat(nimbus): use ConfigCollection.from_github_repo().featmon_configs instead of FeatmonSpec * fix(nimbus): use feat.nimbus_slug() for feature name to match actual Nimbus experiment slug * fix(nimbus): rename unused feat_name to _feat_name * fix(nimbus): fix filter_labels -> label_in and ratios -> feature.ratios in query template * fix(nimbus): update featmon to use FeatmonConfig.spec.* accessors and fix stale urlbar view - Update __init__.py to iterate list[FeatmonConfig] and access dataset/data_sources/features via .spec - Restore urlbar_events_daily view to point at _v2 (was inadvertently left at _v1 due to branch staleness) * fix(nimbus): revert unrelated formatting changes in __init__.py * feat(nimbus): update mozilla-metric-config-parser to 2026.4.3 in requirements.txt * fix(nimbus): rename event_stream to events_stream, add ratios from config, add tests * fix(nimbus): update mozilla-metric-config-parser to 2026.4.4 * refactor(nimbus): follow *Spec/*Configuration pattern for SourceTable Because - SourceTable re-declared fields (table_name, type, analysis_unit_id) that already exist on SourceTableSpec, causing duplication. This commit - renames SourceTable to SourceTableConfiguration, adds a spec: SourceTableSpec field, and delegates name/table_name/type/ analysis_unit_id/time_partition_field via properties. Rendering-only context (project, dataset, dimensions) stays as direct fields. * fix(nimbus): move tests to tests/sql_generators and rename spec field Because - tests in sql_generators/ don't get picked up by the test runner; spec field name was too generic. This commit - moves test_generate_queries.py to tests/sql_generators/ nimbus_feature_monitoring/ and renames SourceTableConfiguration.spec to source_table_spec per reviewer suggestion. * fix(nimbus): remove unused pytest import and apply black formatting to tests --------- Co-authored-by: Yashika Khurana <yashikakhurana@Yashikas-MBP.home.local> Co-authored-by: Yashika Khurana <yashikakhurana@Yashikas-MacBook-Pro.local>
1 parent 1584163 commit 3e9c682

6 files changed

Lines changed: 213 additions & 139 deletions

File tree

requirements.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ mdx_truly_sane_lists==1.3
2929
mkdocs==1.6.1
3030
mkdocs-material==9.7.1
3131
mkdocs-awesome-pages-plugin==2.10.1
32-
mozilla-metric-config-parser==2026.1.1
32+
mozilla-metric-config-parser==2026.4.4
3333
mozilla-schema-generator==0.5.1
3434
mypy==1.15.0
3535
openai==2.6.1

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1257,8 +1257,8 @@ more-itertools==10.4.0 \
12571257
--hash=sha256:0f7d9f83a0a8dcfa8a2694a770590d98a67ea943e3d9f5298309a484758c4e27 \
12581258
--hash=sha256:fe0e63c4ab068eac62410ab05cccca2dc71ec44ba8ef29916a0090df061cf923
12591259
# via jaraco-classes
1260-
mozilla-metric-config-parser==2026.1.1 \
1261-
--hash=sha256:0e74d8a453a839cd5eb82799655ddc1d2c79c461ce099375749c23b6ffdb4903
1260+
mozilla-metric-config-parser==2026.4.4 \
1261+
--hash=sha256:76e64aa99636cb325e8105bb5364cf5b4dbc793ef78a810c0c8a9c94e9667f31
12621262
# via -r requirements.in
12631263
mozilla-nimbus-schemas==2024.9.3 \
12641264
--hash=sha256:5e1196637308fb0241a209eee027a436e986fc6220ad416eebb06b7162e226bc \

sql_generators/nimbus_feature_monitoring/__init__.py

Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
"""Experiment monitoring materialized view generation."""
22

3-
import os
43
from dataclasses import dataclass
54
from pathlib import Path
65
from typing import Any
76

87
import click
9-
import yaml
8+
from metric_config_parser.config import ConfigCollection
9+
from metric_config_parser.featmon import SourceTableSpec
1010
from jinja2 import Environment, FileSystemLoader
1111

1212
from bigquery_etl.cli.utils import use_cloud_function_option
@@ -39,22 +39,40 @@ def __post_init__(self):
3939

4040

4141
@dataclass
42-
class SourceTable:
43-
name: str
42+
class SourceTableConfiguration:
43+
"""Wraps a SourceTableSpec with rendering context needed to generate SQL."""
44+
45+
source_table_spec: SourceTableSpec
4446
project: str
4547
dataset: str
46-
table_name: str
4748
dimensions: list[Dimension]
48-
analysis_unit_id: str = "client_info.client_id"
49-
time_partition_field: str = "submission_timestamp"
50-
is_events_stream: bool = False
5149

5250
def __post_init__(self):
5351
self.last_dimensions = [d for d in self.dimensions if d.aggregator == "last"]
5452
self.non_last_dimensions = [
5553
d for d in self.dimensions if d.aggregator != "last"
5654
]
5755

56+
@property
57+
def name(self) -> str:
58+
return self.source_table_spec.name
59+
60+
@property
61+
def table_name(self) -> str:
62+
return self.source_table_spec.table_name
63+
64+
@property
65+
def analysis_unit_id(self) -> str:
66+
return self.source_table_spec.analysis_unit_id
67+
68+
@property
69+
def type(self) -> str:
70+
return self.source_table_spec.type
71+
72+
@property
73+
def time_partition_field(self) -> str:
74+
return "submission_timestamp"
75+
5876

5977
@dataclass
6078
class Metric:
@@ -68,7 +86,7 @@ def __post_init__(self):
6886
self.is_labeled = isinstance(self, LabeledMetric)
6987

7088
@classmethod
71-
def from_(_, source_is_events_stream=False, **kwargs) -> "Metric":
89+
def from_(_, source_type="metrics", **kwargs) -> "Metric":
7290
data_type = kwargs["data_type"]
7391

7492
if (_key := "field") not in kwargs:
@@ -86,7 +104,7 @@ def from_(_, source_is_events_stream=False, **kwargs) -> "Metric":
86104
if (_key := "ping_aggregator") not in kwargs:
87105
if data_type in ("boolean", "labeled_boolean"):
88106
kwargs[_key] = "logical_or"
89-
elif data_type == "event" and source_is_events_stream:
107+
elif data_type == "event" and source_type == "events_stream":
90108
kwargs[_key] = "countif"
91109
else:
92110
kwargs[_key] = "sum"
@@ -118,7 +136,7 @@ class Feature:
118136
project: str
119137
dataset: str
120138
metrics_by_source: dict[str, list[Metric]]
121-
ratios: list[list[str, str]]
139+
ratios: list[list[str]]
122140

123141
def all_metrics(self):
124142
return [
@@ -139,37 +157,33 @@ def generate_queries(project, path, write_dir):
139157
metadata_template = env.get_template("metadata.yaml")
140158
view_template = env.get_template("view.sql")
141159
schema = (template_dir / "schema.yaml").read_text()
142-
for app_config_path in path.glob("*.yaml"):
143-
app_config = yaml.safe_load(app_config_path.read_text())
144-
dataset = app_config["dataset"]
160+
for app_config in ConfigCollection.from_github_repo().featmon_configs:
161+
dataset = app_config.spec.dataset
145162
source_tables = {}
146-
for source_name, source in app_config["source_tables"].items():
147-
if source is None:
148-
source = {}
163+
for source_name, source in app_config.spec.data_sources.items():
149164
dimensions = []
150-
for dim_name, dim in source.pop("dimensions", {}).items():
165+
for dim_name, dim in source.dimensions.items():
151166
if dim is None:
152167
dim = {}
168+
else:
169+
dim = dict(dim)
153170
dimensions.append(
154171
Dimension(
155172
name=dim_name,
156173
field=dim.pop("field", dim_name),
157174
**dim,
158175
)
159176
)
160-
source_tables[source_name] = SourceTable(
161-
name=source_name,
162-
project=source.pop("project", project),
163-
dataset=source.pop("dataset", dataset),
177+
source_tables[source_name] = SourceTableConfiguration(
178+
source_table_spec=source,
179+
project=project,
180+
dataset=dataset,
164181
dimensions=dimensions,
165-
**source,
166182
)
167183
features = []
168-
for feat_name, feat in app_config["features"].items():
184+
for _feat_name, feat in app_config.spec.features.items():
169185
metrics_by_source = {}
170-
for source_name, source_metrics in feat.pop(
171-
"metrics_by_source", {}
172-
).items():
186+
for source_name, source_metrics in feat.metrics_by_source.items():
173187
metrics_by_source[source_name] = metrics = []
174188
for data_type, data_type_metrics in source_metrics.items():
175189
if data_type == "event":
@@ -185,9 +199,9 @@ def generate_queries(project, path, write_dir):
185199
event_name=metric.pop(
186200
"event_name", metric_name
187201
),
188-
source_is_events_stream=source_tables[
202+
source_type=source_tables[
189203
source_name
190-
].is_events_stream,
204+
].type,
191205
**metric,
192206
)
193207
)
@@ -202,12 +216,11 @@ def generate_queries(project, path, write_dir):
202216
)
203217
features.append(
204218
Feature(
205-
name=feat_name,
206-
project=feat.pop("project", project),
207-
dataset=feat.pop("dataset", f"{dataset}_derived"),
208-
ratios=feat.pop("ratios", []),
219+
name=feat.nimbus_slug(),
220+
project=project,
221+
dataset=f"{dataset}_derived",
222+
ratios=feat.ratios,
209223
metrics_by_source=metrics_by_source,
210-
**feat,
211224
)
212225
)
213226

sql_generators/nimbus_feature_monitoring/templates/firefox_desktop.yaml

Lines changed: 0 additions & 98 deletions
This file was deleted.

sql_generators/nimbus_feature_monitoring/templates/nimbus_feature_monitoring_v1/query.sql

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ rollouts_cte AS (
5858
(
5959
SELECT {{ metric.label_aggregator }}(value)
6060
FROM UNNEST({{metric.field}})
61-
{% if metric.filter_labels %}
61+
{% if metric.label_in %}
6262
WHERE key IN (
6363
{% for label in metric.label_in %}
6464
"{{ label }}"{{ "," if not loop.last }}
@@ -67,7 +67,7 @@ rollouts_cte AS (
6767
{% endif %}
6868
)
6969
{% elif metric.data_type == "event" %}
70-
{% if source_table.is_events_stream %}
70+
{% if source_table.type == "events_stream" %}
7171
event_category = "{{ metric.category }}"
7272
AND event_name = "{{ metric.event_name }}"
7373
{% else %}
@@ -89,7 +89,7 @@ rollouts_cte AS (
8989
, UNNEST(
9090
ARRAY_CONCAT(
9191
COALESCE(
92-
{% if source_table.is_events_stream %}
92+
{% if source_table.type == "events_stream" %}
9393
UNNEST(JSON_KEYS(experiments, 1))
9494
{% else %}
9595
ARRAY(
@@ -198,7 +198,7 @@ FROM
198198
UNPIVOT (
199199
value FOR metric IN
200200
(
201-
{% for numerator, denominator in ratios %}
201+
{% for numerator, denominator in feature.ratios %}
202202
ratio_{{numerator}}_to_{{denominator}},
203203
{% endfor %}
204204
{% for metric, aggregator in feature.all_metrics() %}

0 commit comments

Comments
 (0)