Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 63 additions & 17 deletions tests/ffe/test_flag_eval_metrics.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
"""Test feature flag evaluation metrics via OTel Metrics API."""

import json
import time

from utils import (
weblog,
interfaces,
Expand Down Expand Up @@ -78,6 +81,50 @@ def get_tag_value(tags: list[str], key: str):
return None


def find_eval_metric_with_tags(flag_key: str, expected_tags: dict[str, str]):
"""Find a matching evaluation metric series for the flag and expected tags."""
for point in find_eval_metrics(flag_key):
tags = point.get("tags", [])
if all(get_tag_value(tags, key) == value for key, value in expected_tags.items()):
return point

return None


def post_string_eval(flag_key: str, *, targeting_key: str = "user-1", attributes: dict | None = None):
return weblog.post(
"/ffe",
json={
"flag": flag_key,
"variationType": "STRING",
"defaultValue": "default",
"targetingKey": targeting_key,
"attributes": attributes or {},
},
)


def wait_for_string_eval(flag_key: str, *, variant: str = "on", timeout: float = 10.0):
"""Wait until the weblog evaluates through a ready provider.

Remote Config ACK confirms sidecar delivery, not immediate tracer install.
Some tracers can still need one evaluation cycle before the provider reports
ready, especially immediately after scenario startup.
"""
deadline = time.monotonic() + timeout
response = None
while time.monotonic() <= deadline:
response = post_string_eval(flag_key)
if response.status_code == 200:
body = json.loads(response.text)
if body.get("variant") == variant and body.get("reason") != "ERROR":
return response
time.sleep(0.25)

assert response is not None, "Expected at least one FFE evaluation response"
return response


@scenarios.feature_flagging_and_experimentation
@features.feature_flags_eval_metrics
class Test_FFE_Eval_Metric_Basic:
Expand All @@ -88,29 +135,28 @@ def setup_ffe_eval_metric_basic(self):
self.flag_key = "eval-metric-basic-flag"
rc.tracer_rc_state.reset().set_config(f"{RC_PATH}/{config_id}/config", make_ufc_fixture(self.flag_key)).apply()

self.r = weblog.post(
"/ffe",
json={
"flag": self.flag_key,
"variationType": "STRING",
"defaultValue": "default",
"targetingKey": "user-1",
"attributes": {},
},
)
self.r = wait_for_string_eval(self.flag_key)

def test_ffe_eval_metric_basic(self):
"""Test that flag evaluation produces a metric with correct tags."""
assert self.r.status_code == 200, f"Flag evaluation failed: {self.r.text}"

metrics = find_eval_metrics(self.flag_key)
assert len(metrics) > 0, (
f"Expected at least one feature_flag.evaluations metric for flag '{self.flag_key}', "
f"but found none. All eval metrics: {find_eval_metrics()}"
result = json.loads(self.r.text)
assert result["variant"] == "on", f"Expected evaluated variant 'on', got response: {result}"
assert result["value"] == "on-value", f"Expected evaluated value 'on-value', got response: {result}"

point = find_eval_metric_with_tags(
self.flag_key,
{
"feature_flag.result.variant": "on",
"feature_flag.result.reason": "static",
"feature_flag.result.allocation_key": "default-allocation",
},
)
assert point is not None, (
f"Expected a successful feature_flag.evaluations metric for flag '{self.flag_key}', "
f"but found none. Matching flag metrics: {find_eval_metrics(self.flag_key)}"
)

# Verify tags on the first matching metric point
point = metrics[0]
tags = point.get("tags", [])

assert get_tag_value(tags, "feature_flag.key") == self.flag_key, (
Expand Down
Loading