Task: read ad-group/ad performance tables (spend, CTR, conversions) out of ads.google.com and adsmanager.facebook.com from a hidden tab in the user's logged-in Chrome (browser relay; never foreground the tab), using only Runtime.evaluate + DOM-dispatched MouseEvents.
Findings that cost real time, verified 2026-08-26:
Google Ads (aw/adgroups):
- Toolbar buttons (Segment, Columns, Download) are Angular
MATERIAL-BUTTON[role=button]elements, not<button>/div[role=button]. Selector must be[role=button]. Their innerText includes the icon ligature ("segment\nSegment"), so match with a normalizer, e.g.textContent.replace('segment','').trim() === 'Segment'. - Menus (
[role=menuitem]) are ephemeral: they dismiss when the driving cell ends. The whole flow (open Segment menu -> hover/click 'Time' -> click 'Day') must run in ONE evaluate-driven cell with sleeps between steps; splitting across cells finds nothing. - Deep-link
https://ads.google.com/aw/adgroups?campaignId=<id>&ocid=<ocid>&authuser=..&__u=..&__c=..works; steal ocid/__u/__c from whatever Ads tab is already open. - Segment-by-day turns the table into adgroup rows followed by per-day subrows. The table IS virtualized, but scrolling the single
div.mainscroll container while harvesting.particle-table-row, [role=row]innerText per step (dedupe by row text) captures everything. - The day-segmented dump gives you everything a UI CSV export gives, without touching the download flow (which would land in the user's Downloads dir).
Meta Ads Manager (adsmanager/manage/ads):
- Default date range EXCLUDES today and resets on every navigation. A live campaign in its first day shows $0.00 spent everywhere until you set the preset (e.g. 'Maximum') again on each page. Read the range label before trusting any number.
- The ads grid is virtualized and does NOT respond to wheel events, element.scrollTop, or CDP
Input.dispatchMouseEvent mouseWheelon a hidden/background tab; no scrollable element is even detectable (scrollHeight == clientHeighteverywhere). Only ~10 rows ever exist in the DOM. - Workaround: don't scroll. Drill down to smaller entity sets instead: the ad-set level view (
/adsets?...selected_campaign_ids=<id>) fits its rows on one screen, and per-ad detail comes from/ads?...selected_adset_ids=<id>(4 ads per set fits). URL-drivenPage.navigatebetween these views is an SPA soft-nav trap (URLs differ only by query string) — set awindow.__sent=1sentinel, navigate, andPage.reloadif the sentinel survives. - On/off state of a campaign is readable without any click:
input[type=checkbox]/[role=switch]with aria-label 'On/off', property.checked. - After
Page.setWebLifecycleState {state:'active'}+Emulation.setFocusEmulationEnabled {enabled:true}, both consoles render tables fine on hidden tabs; keep re-asserting lifecycle active while polling (Chrome re-freezes long-backgrounded tabs).
Cross-check tip: reconcile the scraped numbers against an independent channel before reporting (analytics pageviews with the campaign's UTMs). Meta LPVs matched PostHog pageviews 252 vs 255 same-day; a big gap means the scrape read the wrong date window.