Skip to content

Commit b888d8e

Browse files
Keep deployed Python promotion qualification out of customer metrics
1 parent 29ebfb0 commit b888d8e

2 files changed

Lines changed: 62 additions & 4 deletions

File tree

scripts/qualify-docs-promotion.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
sys.path.insert(0, str(REPO_ROOT))
2121

2222
from playwright.sync_api import Browser, Request, Response, sync_playwright # noqa: E402
23+
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError # noqa: E402
2324
from scripts.api_reference_release import load_release_identity # noqa: E402
2425
from scripts.check_api_reference_install import verify_public_deployment # noqa: E402
2526

@@ -29,6 +30,7 @@
2930
PROMOTION_EVENT_URL = "https://cloud.durable-workflow.com/early-access/promotion-events"
3031
PROMOTION_SOURCE = "sdk-python-reference"
3132
QUALIFICATION_EVENT = "qualification"
33+
VIEWPORT_ATTEMPTS = 3
3234
VIEWPORTS = (
3335
("desktop", 1440, 900),
3436
("intermediate", 768, 1024),
@@ -104,7 +106,7 @@ def assert_event_response(response: Response, event: str) -> None:
104106
assert "no-store" in response.headers.get("cache-control", ""), f"{event} promotion response was cacheable"
105107

106108

107-
def qualify_viewport(browser: Browser, name: str, width: int, height: int) -> None:
109+
def qualify_viewport_once(browser: Browser, name: str, width: int, height: int) -> None:
108110
context = browser.new_context(viewport={"width": width, "height": height}, reduced_motion="reduce")
109111
page = context.new_page()
110112
errors: list[str] = []
@@ -179,6 +181,21 @@ def record_initiated_event(payload: object) -> None:
179181
context.close()
180182

181183

184+
def qualify_viewport(browser: Browser, name: str, width: int, height: int) -> None:
185+
"""Qualify one viewport, retrying only transient browser transport timeouts."""
186+
timeouts: list[str] = []
187+
for attempt in range(1, VIEWPORT_ATTEMPTS + 1):
188+
try:
189+
qualify_viewport_once(browser, name, width, height)
190+
return
191+
except PlaywrightTimeoutError as error:
192+
timeouts.append(f"attempt {attempt}: {error}")
193+
194+
raise AssertionError(
195+
f"promotion qualification timed out at {name} after {VIEWPORT_ATTEMPTS} attempts: " + "; ".join(timeouts)
196+
)
197+
198+
182199
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
183200
parser = argparse.ArgumentParser(description=__doc__)
184201
parser.add_argument(
@@ -228,9 +245,10 @@ def main(argv: Sequence[str] | None = None) -> int:
228245
browser.close()
229246

230247
print(
231-
f"Confirmed deployed revision {args.source_revision}, two non-aggregating qualification requests, "
232-
"the source-attributed impression/click initiation and destination behavior, and no browser errors at "
233-
"desktop, intermediate, mobile, and short-height viewports."
248+
f"Confirmed deployed revision {args.source_revision}, two successful non-aggregating qualification "
249+
"requests per viewport with bounded transport retries, the source-attributed impression/click initiation "
250+
"and destination behavior, and no browser errors at desktop, intermediate, mobile, and short-height "
251+
"viewports."
234252
)
235253
return 0
236254

tests/test_docs_promotion_qualification.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,46 @@ def verify_public_deployment(
112112
]
113113

114114

115+
def test_live_viewport_retries_transient_transport_timeout(
116+
qualifier: ModuleType,
117+
monkeypatch: pytest.MonkeyPatch,
118+
) -> None:
119+
attempts: list[tuple[object, str, int, int]] = []
120+
browser = object()
121+
122+
def qualify_once(observed_browser: object, name: str, width: int, height: int) -> None:
123+
attempts.append((observed_browser, name, width, height))
124+
if len(attempts) == 1:
125+
raise qualifier.PlaywrightTimeoutError("receiver response was temporarily unavailable")
126+
127+
monkeypatch.setattr(qualifier, "qualify_viewport_once", qualify_once)
128+
129+
qualifier.qualify_viewport(browser, "mobile", 390, 844)
130+
131+
assert attempts == [
132+
(browser, "mobile", 390, 844),
133+
(browser, "mobile", 390, 844),
134+
]
135+
136+
137+
def test_live_viewport_fails_after_bounded_transport_timeouts(
138+
qualifier: ModuleType,
139+
monkeypatch: pytest.MonkeyPatch,
140+
) -> None:
141+
attempts: list[int] = []
142+
143+
def time_out(*_args: object) -> None:
144+
attempts.append(len(attempts) + 1)
145+
raise qualifier.PlaywrightTimeoutError("receiver response was unavailable")
146+
147+
monkeypatch.setattr(qualifier, "qualify_viewport_once", time_out)
148+
149+
with pytest.raises(AssertionError, match="timed out at desktop after 3 attempts"):
150+
qualifier.qualify_viewport(object(), "desktop", 1440, 900)
151+
152+
assert attempts == [1, 2, 3]
153+
154+
115155
def test_live_receiver_contract_accepts_only_bounded_qualification_payload(qualifier: ModuleType) -> None:
116156
request = SimpleNamespace(
117157
method="POST",

0 commit comments

Comments
 (0)