Skip to content
Merged
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
Expand Up @@ -285,14 +285,24 @@ window.ucTag.renderAd(document, { adId: ${JSON.stringify(adId)}, pubUrl: ${JSON.
const slot = document.getElementById("div-aps")!;
slot.style.width = "1px";
slot.style.height = "1px";
const outerShell = document.createElement("div");
outerShell.id = "aps-outer-shell";
outerShell.style.width = "1px";
outerShell.style.height = "1px";
const innerShell = document.createElement("div");
innerShell.id = "aps-inner-shell";
innerShell.style.width = "1px";
innerShell.style.height = "1px";
const frame = document.createElement("iframe");
frame.id = "google_ads_iframe_fictional_0";
frame.width = "1";
frame.height = "1";
frame.style.width = "1px";
frame.style.height = "1px";
frame.src = outerUrl;
slot.appendChild(frame);
innerShell.appendChild(frame);
outerShell.appendChild(innerShell);
slot.appendChild(outerShell);

const other = document.getElementById("div-other")!;
const otherFrame = document.createElement("iframe");
Expand Down Expand Up @@ -333,6 +343,22 @@ window.ucTag.renderAd(document, { adId: ${JSON.stringify(adId)}, pubUrl: ${JSON.
);
await expect(page.locator("#div-aps")).toHaveCSS("width", "300px");
await expect(page.locator("#div-aps")).toHaveCSS("height", "250px");
await expect(page.locator("#aps-outer-shell")).toHaveCSS(
"width",
"300px",
);
await expect(page.locator("#aps-outer-shell")).toHaveCSS(
"height",
"250px",
);
await expect(page.locator("#aps-inner-shell")).toHaveCSS(
"width",
"300px",
);
await expect(page.locator("#aps-inner-shell")).toHaveCSS(
"height",
"250px",
);
await expect(page.locator("#div-other iframe")).toHaveCSS(
"width",
"1px",
Expand Down
45 changes: 32 additions & 13 deletions crates/trusted-server-js/lib/src/core/first_impression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,15 @@ function pruneFirstImpressionState(ts: TsjsApi, now = Date.now()): FirstImpressi
continue;
}
for (const [token, auction] of Object.entries(claim.publisherAuctions)) {
if (auction.expiresAt <= now) removePublisherAuction(state, claim, token, now);
// A TS-owned losing publisher auction remains a fail-closed tombstone for
// this physical element and navigation. Its callback can arrive long after
// the nominal auction lease and must never become an unrelated refresh.
if (
auction.expiresAt <= now &&
!(claim.owner === 'trusted_server' && auction.suppressDelivery)
) {
removePublisherAuction(state, claim, token, now);
}
}
if (
claim.owner === 'publisher' &&
Expand Down Expand Up @@ -155,10 +163,11 @@ export function claimFirstImpressionForTrustedServer(
}

function schedulePublisherAuctionExpiry(ts: TsjsApi, token: string): void {
window.setTimeout(
() => releasePublisherFirstImpressionAuction(ts, token),
FIRST_IMPRESSION_LEASE_MS
);
window.setTimeout(() => {
// Pruning releases ordinary publisher claims. TS-owned suppression tokens
// deliberately survive as bounded tombstones until navigation/element change.
findPublisherAuction(ts, token);
}, FIRST_IMPRESSION_LEASE_MS);
}

/** Release a TS claim when slot setup failed before any request could start. */
Expand Down Expand Up @@ -211,7 +220,10 @@ export function registerPublisherFirstImpressionAuctions(
) {
continue;
}
if (claim.owner === 'trusted_server' && (claim.suppressionConsumed || claim.expiresAt <= now)) {
if (
claim.owner === 'trusted_server' &&
(claim.publisherRegistrationClosed || claim.expiresAt <= now)
) {
continue;
}
if (Object.keys(claim.publisherAuctions).length >= MAX_PUBLISHER_AUCTIONS_PER_SLOT) continue;
Expand Down Expand Up @@ -275,6 +287,10 @@ export function releasePublisherFirstImpressionAuction(
): void {
const found = findPublisherAuction(ts, token, now);
if (!found) return;
if (found.claim.owner === 'trusted_server' && found.auction.suppressDelivery) {
found.claim.publisherRegistrationClosed = true;
return;
}
found.auction.expiresAt = Math.min(found.auction.expiresAt, now);
if (
found.claim.owner === 'publisher' &&
Expand All @@ -295,13 +311,9 @@ export function consumePublisherFirstImpressionDelivery(
const found = findPublisherAuction(ts, token, now);
if (!found) return false;

const suppress =
found.claim.owner === 'trusted_server' &&
found.auction.suppressDelivery &&
!found.claim.suppressionConsumed &&
found.claim.expiresAt > now;
const suppress = found.claim.owner === 'trusted_server' && found.auction.suppressDelivery;
delete found.claim.publisherAuctions[token];
if (suppress) found.claim.suppressionConsumed = true;
if (suppress) found.claim.publisherRegistrationClosed = true;
return suppress;
}

Expand Down Expand Up @@ -329,7 +341,14 @@ export function observeFirstImpressionGptLifecycle(
}

claim.phase = phase;
if (claim.owner === 'publisher') claim.expiresAt = Number.POSITIVE_INFINITY;
if (claim.owner === 'publisher') {
claim.expiresAt = Number.POSITIVE_INFINITY;
} else {
// Once TS has committed a GPT request, only publisher auctions that were
// already registered can still represent an overlapping first impression.
// New publisher refreshes are ordinary later impressions and must proceed.
claim.publisherRegistrationClosed = true;
}
}

/** Reserve the only Trusted Server fallback allowed for this physical slot and generation. */
Expand Down
3 changes: 2 additions & 1 deletion crates/trusted-server-js/lib/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,8 @@ export interface FirstImpressionSlotClaim {
phase: FirstImpressionPhase;
expiresAt: number;
publisherAuctions: Record<string, FirstImpressionPublisherAuction>;
suppressionConsumed?: boolean;
/** No later publisher auction may join this TS-owned first impression. */
publisherRegistrationClosed?: boolean;
targeting?: Record<string, string | string[]>;
}

Expand Down
129 changes: 92 additions & 37 deletions crates/trusted-server-js/lib/src/integrations/gpt/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,22 @@ function usesFixedPositioning(element: HTMLElement): boolean {

const MAX_CREATIVE_SHELL_DIMENSION = 10_000;

function creativeFrameIsCurrent(
source: MessageEventSource | null,
frame: MessageSourceFrame,
generation: number,
stillOwnsCreative: () => boolean
): boolean {
return (
(window.tsjs?.navGeneration ?? 0) === generation &&
stillOwnsCreative() &&
frame.iframe.isConnected &&
frame.root.isConnected &&
frame.root.contains(frame.iframe) &&
frame.iframe.contentWindow === source
);
}

/** Resize only the authenticated source iframe for a still-current collapsed display shell. */
function resizeCollapsedCreativeFrame(
source: MessageEventSource | null,
Expand All @@ -290,18 +306,13 @@ function resizeCollapsedCreativeFrame(
stillOwnsCreative: () => boolean
): void {
if (
(window.tsjs?.navGeneration ?? 0) !== generation ||
!stillOwnsCreative() ||
!creativeFrameIsCurrent(source, frame, generation, stillOwnsCreative) ||
!Number.isFinite(width) ||
!Number.isFinite(height) ||
width <= 0 ||
height <= 0 ||
width > MAX_CREATIVE_SHELL_DIMENSION ||
height > MAX_CREATIVE_SHELL_DIMENSION ||
!frame.iframe.isConnected ||
!frame.root.isConnected ||
!frame.root.contains(frame.iframe) ||
frame.iframe.contentWindow !== source ||
frame.iframe.getAttribute('width') !== '1' ||
frame.iframe.getAttribute('height') !== '1' ||
!hasCollapsedDimension(frame.iframe, 'width') ||
Expand All @@ -314,24 +325,37 @@ function resizeCollapsedCreativeFrame(
return;
}

const wrapper = frame.iframe.parentElement;
if (
!wrapper ||
wrapper === document.body ||
wrapper === document.documentElement ||
!frame.root.contains(wrapper) ||
usesFixedPositioning(wrapper)
) {
return;
const collapsedAncestors: HTMLElement[] = [];
let reachedRoot = false;
for (let ancestor = frame.iframe.parentElement; ancestor; ancestor = ancestor.parentElement) {
if (
ancestor === document.body ||
ancestor === document.documentElement ||
!ancestor.isConnected ||
usesFixedPositioning(ancestor) ||
ancestor.matches(
'ins[data-anchor-status], [data-google-interstitial], [data-vignette-loaded]'
)
) {
return;
}
if (hasCollapsedDimension(ancestor, 'width') || hasCollapsedDimension(ancestor, 'height')) {
collapsedAncestors.push(ancestor);
}
if (ancestor === frame.root) {
reachedRoot = true;
break;
}
}
if (!reachedRoot) return;

frame.iframe.width = String(width);
frame.iframe.height = String(height);
frame.iframe.style.width = `${width}px`;
frame.iframe.style.height = `${height}px`;
if (hasCollapsedDimension(wrapper, 'width') && hasCollapsedDimension(wrapper, 'height')) {
wrapper.style.width = `${width}px`;
wrapper.style.height = `${height}px`;
for (const ancestor of collapsedAncestors) {
ancestor.style.width = `${width}px`;
ancestor.style.height = `${height}px`;
}
}

Expand Down Expand Up @@ -1992,6 +2016,12 @@ export function installTsRenderBridge(): void {
trustedServer: (validatedRenderer) => {
const rendererUrl = apsRendererUrl();
if (!rendererUrl) return false;
const stillOwnsCreative = () =>
sourceFrameForAdUnit(e.source, prebidRendererEntry.adUnitCode)?.iframe ===
sourceFrame.iframe;
if (!creativeFrameIsCurrent(e.source, sourceFrame, generation, stillOwnsCreative)) {
return false;
}
try {
port.postMessage(
JSON.stringify({
Expand All @@ -2011,11 +2041,9 @@ export function installTsRenderBridge(): void {
validatedRenderer.width,
validatedRenderer.height,
generation,
() =>
sourceFrameForAdUnit(e.source, prebidRendererEntry.adUnitCode)?.iframe ===
sourceFrame.iframe
stillOwnsCreative
);
return true;
return creativeFrameIsCurrent(e.source, sourceFrame, generation, stillOwnsCreative);
} catch (err) {
log.warn(`[tsjs-gpt] APS Prebid response post failed for '${adId}'`, err);
return false;
Expand Down Expand Up @@ -2066,6 +2094,13 @@ export function installTsRenderBridge(): void {
trustedServer: (validatedRenderer) => {
const rendererUrl = apsRendererUrl();
if (!rendererUrl) return false;
const stillOwnsCreative = () =>
window.tsjs?.bids?.[slotId] === matchedBid &&
matchedBid.hb_adid === adId &&
sourceFrameForSlotId(e.source, slotId)?.iframe === sourceSlotFrame.iframe;
if (!creativeFrameIsCurrent(e.source, sourceSlotFrame, generation, stillOwnsCreative)) {
return false;
}
try {
port.postMessage(
JSON.stringify({
Expand All @@ -2085,12 +2120,14 @@ export function installTsRenderBridge(): void {
validatedRenderer.width,
validatedRenderer.height,
generation,
() =>
window.tsjs?.bids?.[slotId] === matchedBid &&
matchedBid.hb_adid === adId &&
sourceFrameForSlotId(e.source, slotId)?.iframe === sourceSlotFrame.iframe
stillOwnsCreative
);
return creativeFrameIsCurrent(
e.source,
sourceSlotFrame,
generation,
stillOwnsCreative
);
return true;
} catch (err) {
log.warn(`[tsjs-gpt] APS server response post failed for '${slotId}'`, err);
return false;
Expand Down Expand Up @@ -2120,6 +2157,15 @@ export function installTsRenderBridge(): void {

if (inlineAdm) {
e.stopImmediatePropagation();
const stillOwnsCreative = () =>
Boolean(
window.tsjs?.bids?.[slotId] === matchedBid &&
matchedBid.hb_adid === adId &&
sourceFrameForSlotId(e.source, slotId)?.iframe === sourceSlotFrame.iframe
);
if (!creativeFrameIsCurrent(e.source, sourceSlotFrame, generation, stillOwnsCreative)) {
return;
}
try {
port.postMessage(
JSON.stringify({
Expand All @@ -2136,13 +2182,15 @@ export function installTsRenderBridge(): void {
log.warn(`[tsjs-gpt] pbRender bridge: response post failed for '${slotId}'`, err);
return;
}
resizeCollapsedCreativeFrame(e.source, sourceSlotFrame, width, height, generation, () =>
Boolean(
window.tsjs?.bids?.[slotId] === matchedBid &&
matchedBid.hb_adid === adId &&
sourceFrameForSlotId(e.source, slotId)?.iframe === sourceSlotFrame.iframe
)
resizeCollapsedCreativeFrame(
e.source,
sourceSlotFrame,
width,
height,
generation,
stillOwnsCreative
);
if (!creativeFrameIsCurrent(e.source, sourceSlotFrame, generation, stillOwnsCreative)) return;
safelyRecordCreativeResponse(attemptId);
fireWinBillingBeacons(slotId, matchedBid);
log.debug(`[tsjs-gpt] pbRender bridge served '${slotId}' from inline adm`);
Expand Down Expand Up @@ -2194,6 +2242,13 @@ export function installTsRenderBridge(): void {
: cached.adm;
const cachedWidth = cached.width ?? width;
const cachedHeight = cached.height ?? height;
const stillOwnsCreative = () =>
window.tsjs?.bids?.[slotId] === matchedBid &&
matchedBid.hb_adid === adId &&
sourceFrameForSlotId(e.source, slotId)?.iframe === sourceSlotFrame.iframe;
if (!creativeFrameIsCurrent(e.source, sourceSlotFrame, generation, stillOwnsCreative)) {
return;
}
try {
port.postMessage(
JSON.stringify({
Expand All @@ -2211,16 +2266,16 @@ export function installTsRenderBridge(): void {
cachedWidth,
cachedHeight,
generation,
() =>
window.tsjs?.bids?.[slotId] === matchedBid &&
matchedBid.hb_adid === adId &&
sourceFrameForSlotId(e.source, slotId)?.iframe === sourceSlotFrame.iframe
stillOwnsCreative
);
} catch (err) {
safelyRecordCreativeFailure(attemptId, 'response_post_failed');
log.warn(`[tsjs-gpt] pbRender bridge: response post failed for '${slotId}'`, err);
return;
}
if (!creativeFrameIsCurrent(e.source, sourceSlotFrame, generation, stillOwnsCreative)) {
return;
}
safelyRecordCreativeResponse(attemptId);
// Beacons carry the server-expanded ${AUCTION_PRICE} from the auction's
// clearing price, not `cached.price` — the auction result is the
Expand Down
Loading
Loading