66import argparse
77import json
88import os
9+ import sys
910from collections .abc import Sequence
11+ from pathlib import Path
1012from typing import Any
1113from urllib .parse import urlparse
1214
13- from playwright .sync_api import Browser , Request , Response , sync_playwright
15+ # Direct file execution adds ``scripts/`` rather than the repository root to
16+ # sys.path. The deployment workflow uses this entrypoint, so make its local
17+ # package imports resolvable before importing the qualifier dependencies.
18+ REPO_ROOT = Path (__file__ ).resolve ().parent .parent
19+ if str (REPO_ROOT ) not in sys .path :
20+ sys .path .insert (0 , str (REPO_ROOT ))
21+
22+ from playwright .sync_api import Browser , Request , Response , sync_playwright # noqa: E402
23+ from scripts .api_reference_release import load_release_identity # noqa: E402
24+ from scripts .check_api_reference_install import verify_public_deployment # noqa: E402
1425
1526DOCS_URL = "https://python.durable-workflow.com/"
27+ RELEASE_AUDIT_URL = f"{ DOCS_URL } release-audit.json"
1628DESTINATION_URL = "https://cloud.durable-workflow.com/early-access#source=sdk-python-reference"
1729PROMOTION_EVENT_URL = "https://cloud.durable-workflow.com/early-access/promotion-events"
1830PROMOTION_SOURCE = "sdk-python-reference"
31+ QUALIFICATION_EVENT = "qualification"
1932VIEWPORTS = (
2033 ("desktop" , 1440 , 900 ),
2134 ("intermediate" , 768 , 1024 ),
2235 ("mobile" , 390 , 844 ),
2336 ("short-height" , 640 , 360 ),
2437)
2538
39+ QUALIFICATION_REWRITE_SCRIPT = f"""
40+ (() => {{
41+ const eventUrl = { json .dumps (PROMOTION_EVENT_URL )} ;
42+ const source = { json .dumps (PROMOTION_SOURCE )} ;
43+ const qualificationEvent = { json .dumps (QUALIFICATION_EVENT )} ;
44+ const nativeFetch = window.fetch.bind(window);
45+
46+ window.fetch = function (input, init) {{
47+ const requestUrl = typeof input === 'string' ? input : input.url;
48+ if (requestUrl !== eventUrl) return nativeFetch(input, init);
49+
50+ const options = init || {{}};
51+ let initiatedPayload = null;
52+ try {{
53+ initiatedPayload = JSON.parse(options.body);
54+ }} catch (_error) {{
55+ // The qualification fails on the recorded initiation shape below.
56+ }}
57+ window.recordPromotionQualificationInitiation(initiatedPayload);
58+
59+ return nativeFetch(input, {{
60+ ...options,
61+ body: JSON.stringify({{source, event: qualificationEvent}}),
62+ }});
63+ }};
64+ }})();
65+ """
66+
2667
2768def event_payload (request : Request ) -> dict [str , Any ] | None :
2869 try :
@@ -68,6 +109,13 @@ def qualify_viewport(browser: Browser, name: str, width: int, height: int) -> No
68109 page = context .new_page ()
69110 errors : list [str ] = []
70111 promotion_requests : list [Request ] = []
112+ initiated_events : list [object ] = []
113+
114+ def record_initiated_event (payload : object ) -> None :
115+ initiated_events .append (payload )
116+
117+ page .expose_function ("recordPromotionQualificationInitiation" , record_initiated_event )
118+ page .add_init_script (QUALIFICATION_REWRITE_SCRIPT )
71119
72120 page .on (
73121 "console" ,
@@ -81,20 +129,22 @@ def qualify_viewport(browser: Browser, name: str, width: int, height: int) -> No
81129
82130 try :
83131 action = page .locator ('[data-promotion-action="early-access"]' )
84- with page .expect_response (lambda response : is_event (response , "impression" ), timeout = 30_000 ) as pending :
132+ with page .expect_response (lambda response : is_event (response , QUALIFICATION_EVENT ), timeout = 30_000 ) as pending :
85133 document = page .goto (DOCS_URL , wait_until = "domcontentloaded" , timeout = 30_000 )
86134 action .scroll_into_view_if_needed ()
87135 assert document is not None and document .ok , f"deployed docs returned HTTP { document .status } at { name } "
88- assert_event_response (pending .value , "impression" )
136+ assert_event_response (pending .value , QUALIFICATION_EVENT )
89137
90138 action .wait_for (state = "visible" )
91139 assert action .get_attribute ("href" ) == DESTINATION_URL , "promotion destination changed"
92140 with (
93- page .expect_response (lambda response : is_event (response , "click" ), timeout = 30_000 ) as click_pending ,
141+ page .expect_response (
142+ lambda response : is_event (response , QUALIFICATION_EVENT ), timeout = 30_000
143+ ) as click_pending ,
94144 page .expect_navigation (wait_until = "domcontentloaded" , timeout = 30_000 ) as navigation ,
95145 ):
96146 action .click ()
97- assert_event_response (click_pending .value , "click" )
147+ assert_event_response (click_pending .value , QUALIFICATION_EVENT )
98148
99149 destination = navigation .value
100150 assert destination is not None and destination .status == 200 , (
@@ -115,11 +165,15 @@ def qualify_viewport(browser: Browser, name: str, width: int, height: int) -> No
115165 )
116166
117167 page .wait_for_timeout (250 )
118- observed = [event_payload (request ) for request in promotion_requests ]
119- assert observed == [
168+ assert initiated_events == [
120169 {"source" : PROMOTION_SOURCE , "event" : "impression" },
121170 {"source" : PROMOTION_SOURCE , "event" : "click" },
122- ], f"promotion emitted duplicate or unbounded events at { name } : { observed } "
171+ ], f"deployed promotion initiated unexpected events at { name } : { initiated_events } "
172+ observed = [event_payload (request ) for request in promotion_requests ]
173+ assert observed == [
174+ {"source" : PROMOTION_SOURCE , "event" : QUALIFICATION_EVENT },
175+ {"source" : PROMOTION_SOURCE , "event" : QUALIFICATION_EVENT },
176+ ], f"promotion qualification emitted duplicate or unbounded events at { name } : { observed } "
123177 assert errors == [], f"promotion emitted browser errors at { name } : { errors } "
124178 finally :
125179 context .close ()
@@ -132,11 +186,36 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
132186 default = os .environ .get ("PLAYWRIGHT_CHROMIUM_EXECUTABLE" ),
133187 help = "Use a system Chromium instead of Playwright's managed browser." ,
134188 )
189+ parser .add_argument (
190+ "--source-revision" ,
191+ required = True ,
192+ help = "Exact deployed source revision required before live qualification begins." ,
193+ )
194+ parser .add_argument (
195+ "--release-audit-attempts" ,
196+ type = int ,
197+ default = 12 ,
198+ help = "Number of attempts allowed while the live release record converges." ,
199+ )
200+ parser .add_argument (
201+ "--release-audit-retry-sleep" ,
202+ type = float ,
203+ default = 10 ,
204+ help = "Seconds between live release-record attempts." ,
205+ )
135206 return parser .parse_args (argv )
136207
137208
138209def main (argv : Sequence [str ] | None = None ) -> int :
139210 args = parse_args (argv )
211+ verify_public_deployment (
212+ RELEASE_AUDIT_URL ,
213+ load_release_identity (REPO_ROOT ),
214+ args .source_revision ,
215+ attempts = args .release_audit_attempts ,
216+ retry_sleep = args .release_audit_retry_sleep ,
217+ )
218+
140219 with sync_playwright () as playwright :
141220 launch_options = {"headless" : True }
142221 if args .chromium_executable :
@@ -149,8 +228,9 @@ def main(argv: Sequence[str] | None = None) -> int:
149228 browser .close ()
150229
151230 print (
152- "Confirmed one successful source-attributed impression and click, a healthy early-access destination, "
153- "and no browser errors at desktop, intermediate, mobile, and short-height viewports."
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."
154234 )
155235 return 0
156236
0 commit comments