DEV Community

98IP Proxy
98IP Proxy

Posted on

Build a Per-Frame Ad Verification Record in Playwright

Chrome DevTools 152 adds clickable ad iframe Element IDs and a per-frame metrics table to Application → Ads. That makes manual inspection more precise, but it does not solve the evidence-model problem: how do we relate a frame to a region, proxy route, consent state, viewport, and decision without storing secrets or manufacturing ad traffic?

This article proposes a small JavaScript record and a Playwright collection boundary. It intentionally does not click ads, loop page loads, bypass consent, or claim that DevTools metrics are billable-impression data.

Disclosure: I work with 98IP. The implementation pattern below is educational and should be used only on pages, routes, accounts, and campaigns you are authorized to test.

Start with the decision, not the scraper

Define the questions before opening a browser:

  • Was the approved proxy route verified?
  • Did the expected placement frame exist within the observation window?
  • Was the creative present, blank, delayed, blocked, or outside the viewport?
  • Was only one variable changed from the control run?
  • Is the evidence sufficient for pass, fail, or inconclusive?

If your record cannot answer those questions, collecting more DOM data usually adds noise rather than confidence.

A compact evidence schema

/** @typedef {'present'|'blank'|'delayed'|'blocked'|'out_of_view'|'unknown'} VisualResult */
/** @typedef {'pass'|'fail'|'inconclusive'} Decision */

/**
 * @typedef {Object} FrameEvidence
 * @property {string} runId
 * @property {string} startedAtUtc
 * @property {{version:string, viewport:string, locale:string, timezone:string}} browser
 * @property {{label:string, intendedRegion:string, verified:boolean, noDirectFallback:boolean}} route
 * @property {string} consentState
 * @property {string} pageCase
 * @property {string} frameRef
 * @property {string} framePathHash
 * @property {string} placement
 * @property {Record<string, number|string|boolean|null>} metrics
 * @property {VisualResult} visualResult
 * @property {Decision} decision
 * @property {string} reasonCode
 */
Enter fullscreen mode Exit fullscreen mode

Notice what is missing: cookies, tokens, complete URLs, account IDs, raw HTML, and creative click-through parameters. Those fields are rarely needed to decide whether a placement was observed correctly.

Make redaction the default

import { createHash } from 'node:crypto';

const sha256 = value => createHash('sha256').update(value).digest('hex');

function safeFramePath(frameUrl, ordinal) {
  const url = new URL(frameUrl);
  return sha256(`${url.origin}${url.pathname}#${ordinal}`);
}

function safePageCase(rawUrl, approvedCases) {
  const url = new URL(rawUrl);
  const key = `${url.origin}${url.pathname}`;
  if (!approvedCases.has(key)) throw new Error('Page is outside the approved test set');
  return approvedCases.get(key);
}
Enter fullscreen mode Exit fullscreen mode

Hashing is not magic anonymization. A predictable input can still be guessed. Use hashes here for correlation, not as permission to ingest unrestricted personal data. If even the path is sensitive, map it to an internal case ID before the run.

Enforce one navigation and zero clicks

import { chromium } from 'playwright';

export async function observeCase({ browserOptions, contextOptions, pageUrl, windowMs }) {
  const browser = await chromium.launch(browserOptions);
  const context = await browser.newContext(contextOptions);
  const page = await context.newPage();

  let navigations = 0;
  page.on('request', request => {
    if (request.isNavigationRequest() && request.frame() === page.mainFrame()) navigations += 1;
    if (navigations > 1) throw new Error('Navigation budget exceeded');
  });

  await page.goto(pageUrl, { waitUntil: 'domcontentloaded' });
  await page.waitForTimeout(windowMs);

  const frames = page.frames().map((frame, ordinal) => ({
    ordinal,
    name: frame.name() || null,
    pathHash: safeFramePath(frame.url(), ordinal),
    parentOrdinal: frame.parentFrame()
      ? page.frames().indexOf(frame.parentFrame())
      : null
  }));

  await context.close();
  await browser.close();
  return frames;
}
Enter fullscreen mode Exit fullscreen mode

This code only inventories frames. Chrome 152's Ads-panel metrics still need to be reviewed or exported through an authorized diagnostic path. Do not reverse-engineer private ad APIs merely to automate the table.

Keep the route assertion separate

A proxy configuration is not proof that the request used the intended exit. Build a preflight that returns a minimal route assertion:

const route = {
  label: 'eu-west-a',
  intendedRegion: 'EU',
  verified: true,
  noDirectFallback: true
};

if (!route.verified || !route.noDirectFallback) {
  throw new Error('Route is not suitable for a regional conclusion');
}
Enter fullscreen mode Exit fullscreen mode

In production, obtain that assertion from your approved route-verification service or sanitized proxy logs. Do not let the page under test define its own location proof.

Merge manual frame metrics deterministically

function mergeMetrics(inventory, reviewedRows) {
  const byOrdinal = new Map(reviewedRows.map(row => [row.ordinal, row]));

  return inventory.map(frame => {
    const reviewed = byOrdinal.get(frame.ordinal);
    return {
      ...frame,
      metrics: reviewed?.metrics ?? {},
      visualResult: reviewed?.visualResult ?? 'unknown',
      reviewComplete: Boolean(reviewed)
    };
  });
}
Enter fullscreen mode Exit fullscreen mode

Do not use a volatile iframe Element ID as a cross-run primary key. It is useful inside one captured session. For durable correlation, combine a run ID, placement definition, timestamp, frame ordinal or hierarchy, and a sanitized path hash.

Decide conservatively

function decide({ route, reviewComplete, expectedFrame, visualResult }) {
  if (!route.verified || !route.noDirectFallback) {
    return { decision: 'inconclusive', reasonCode: 'ROUTE_UNVERIFIED' };
  }
  if (!reviewComplete) {
    return { decision: 'inconclusive', reasonCode: 'FRAME_REVIEW_MISSING' };
  }
  if (!expectedFrame) {
    return { decision: 'fail', reasonCode: 'EXPECTED_FRAME_ABSENT' };
  }
  if (visualResult === 'present') {
    return { decision: 'pass', reasonCode: 'EXPECTED_CREATIVE_PRESENT' };
  }
  if (visualResult === 'delayed') {
    return { decision: 'inconclusive', reasonCode: 'OUTSIDE_OBSERVATION_WINDOW' };
  }
  return { decision: 'fail', reasonCode: `FRAME_${visualResult.toUpperCase()}` };
}
Enter fullscreen mode Exit fullscreen mode

The point is not that these exact reason codes fit every campaign. The point is that route failure, missing evidence, frame absence, and a blank creative must not collapse into the same Boolean.

Operational checklist

  • Use an approved destination, account, route, region, and sample size.
  • Verify the exit and test that proxy failure does not fall back to direct access.
  • Hold viewport, locale, timezone, consent, and profile policy constant.
  • Set a single observation window before navigation.
  • Capture one run; do not create a refresh loop.
  • Never click the creative as part of routine verification.
  • Associate Chrome 152 frame metrics with a run-local reference.
  • Redact query strings, credentials, cookies, and personal data.
  • Change one variable between control and comparison.
  • Return inconclusive whenever route or evidence is incomplete.

What this gives you

The Chrome 152 UI improvement closes an important inspection gap: it is easier to see which frame produced which evidence. The schema closes the operational gap by binding that observation to controlled context. Together, they produce a reviewable test without pretending that a proxy alone represents a complete user location or that a browser metric automatically represents a billable impression.

For more proxy engineering and compliant regional testing resources, visit 98IP.

Use this pattern only for authorized quality assurance. Respect publisher terms, consent choices, privacy requirements, destination limits, and advertising measurement rules. Do not use it to create artificial impressions, bypass access controls, or conceal promotional activity.

Top comments (0)