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
13 changes: 11 additions & 2 deletions crates/trusted-server-js/lib/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,15 @@ export interface GptDiagnosticsRequestCycle {
viewableAtMs?: number;
durations: GptDiagnosticsDurations;
isEmpty?: boolean;
/** Configured sizes Trusted Server supplied to GPT for this request. */
requestedSlotSizes?: ReadonlyArray<Size>;
/** Exact fill size fact GPT reported in its `slotRenderEnded` callback. */
size?: Size;
/**
* Outer CSS box observed on the uniquely bound, connected slot element after
* a filled GPT render. This is not an assertion about internal creative pixels.
*/
observedSlotSize?: Size;
isBackfill?: boolean;
slotContentChanged?: boolean;
incompleteSequence: boolean;
Expand Down Expand Up @@ -318,12 +326,13 @@ export interface GptDiagnosticsApi {
* and stops the writers from becoming part of the public contract.
*/
export interface GptDiagnosticsRecorder {
/** Record Trusted Server's creative opportunity for an associated GPT slot. */
/** Record Trusted Server's creative opportunity and configured sizes for an associated GPT slot. */
recordTrustedServerOpportunity(
slot: GptDiagnosticsSlotHandle,
auctionSlotId: string,
opportunity: GptDiagnosticsTrustedServerOpportunity,
trustedServerAuctionId?: string
trustedServerAuctionId?: string,
requestedSlotSizes?: ReadonlyArray<Size>
): void;
/** Mark slots whose next observed GPT request follows the Prebid refresh path. */
recordPrebidRefresh(slots: GptDiagnosticsSlotHandle[]): void;
Expand Down
22 changes: 8 additions & 14 deletions crates/trusted-server-js/lib/src/integrations/gpt/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1073,21 +1073,15 @@ export function installTsAdInit(): void {
// Diagnostics are observational only. A missing or malformed debug
// implementation must never interrupt slot mapping or delivery.
try {
const requestedSlotSizes = ts.gptSlotHandoffs?.[slotDivId2]?.formats;
const opportunity = trustedServerOpportunity(bid);
if (bid.hb_auction_id !== undefined) {
ts.gptDiagnosticsRecorder?.recordTrustedServerOpportunity(
gptSlot,
slot.id,
opportunity,
bid.hb_auction_id
);
} else {
ts.gptDiagnosticsRecorder?.recordTrustedServerOpportunity(
gptSlot,
slot.id,
opportunity
);
}
ts.gptDiagnosticsRecorder?.recordTrustedServerOpportunity(
gptSlot,
slot.id,
opportunity,
bid.hb_auction_id,
requestedSlotSizes
);
} catch {
// Diagnostics must not alter ad delivery.
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ interface ApiStore {
slot: GptDiagnosticsSlotHandle,
auctionSlotId: string,
opportunity: GptDiagnosticsTrustedServerOpportunity,
trustedServerAuctionId?: string
trustedServerAuctionId?: string,
requestedSlotSizes?: ReadonlyArray<readonly [number, number]>
): void;
recordPrebidRefresh(slots: GptDiagnosticsSlotHandle[]): void;
recordTrustedServerCreativeRequest(auctionSlotId: string): number | undefined;
Expand Down Expand Up @@ -63,7 +64,9 @@ function cloneExportSnapshot(snapshot: GptDiagnosticsExportV1): GptDiagnosticsEx
requests: slot.requests.map((cycle) => ({
...cycle,
durations: { ...cycle.durations },
requestedSlotSizes: cycle.requestedSlotSizes?.map((size) => [...size]),
size: cycle.size ? [...cycle.size] : undefined,
observedSlotSize: cycle.observedSlotSize ? [...cycle.observedSlotSize] : undefined,
adManager: cycle.adManager
? {
...cycle.adManager,
Expand Down Expand Up @@ -149,18 +152,21 @@ export class GptDiagnosticsApiController {
};

this.recorder = {
recordTrustedServerOpportunity: (slot, auctionSlotId, opportunity, trustedServerAuctionId) =>
recordTrustedServerOpportunity: (
slot,
auctionSlotId,
opportunity,
trustedServerAuctionId,
requestedSlotSizes
) =>
safelyRecord(() => {
if (trustedServerAuctionId === undefined) {
this.store.recordTrustedServerOpportunity(slot, auctionSlotId, opportunity);
} else {
this.store.recordTrustedServerOpportunity(
slot,
auctionSlotId,
opportunity,
trustedServerAuctionId
);
}
this.store.recordTrustedServerOpportunity(
slot,
auctionSlotId,
opportunity,
trustedServerAuctionId,
requestedSlotSizes
);
Comment thread
ChristianPavilonis marked this conversation as resolved.
}),
recordPrebidRefresh: (slots) => safelyRecord(() => this.store.recordPrebidRefresh(slots)),
recordTrustedServerCreativeRequest: (auctionSlotId) =>
Expand Down Expand Up @@ -191,7 +197,9 @@ export class GptDiagnosticsApiController {
requests: slot.requests.map((cycle) => ({
...cycle,
durations: { ...cycle.durations },
requestedSlotSizes: cycle.requestedSlotSizes?.map((size) => [...size]),
size: cycle.size ? [...cycle.size] : undefined,
observedSlotSize: cycle.observedSlotSize ? [...cycle.observedSlotSize] : undefined,
adManager: cycle.adManager
? {
...cycle.adManager,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { GptDiagnosticsRequestCycle } from '../../core/types';

import type { GptDiagnosticsBindingManager } from './binding';
import { unhandledCase } from './exhaustive';
import { formatSizes, scheduleFrame } from './presentation_helpers';
import type {
GptDiagnosticsBindingInput,
GptDiagnosticsStoreSlotSnapshot,
Expand All @@ -26,21 +27,14 @@ type BadgeWindow = Window & {

const BADGE_MAX_WIDTH_PX = 260;
const BADGE_EDGE_GUTTER_PX = 4;
const MAX_BADGE_REQUESTED_SLOT_SIZES = 3;

interface BadgeOptions {
window?: BadgeWindow;
document?: Document;
scheduleFrame?: (callback: () => void) => void;
}

function defaultScheduleFrame(callback: () => void): void {
if (typeof requestAnimationFrame === 'function') {
requestAnimationFrame(() => callback());
} else {
queueMicrotask(callback);
}
}

function intersectsViewport(rectangle: DOMRect, window: Window): boolean {
return (
rectangle.width > 0 &&
Expand Down Expand Up @@ -115,7 +109,17 @@ function badgeText(cycle: GptDiagnosticsRequestCycle): string {
const delivery = deliveryLabel(cycle);
if (delivery) firstLine.push(delivery);
if (cycle.requestPath === 'competing') firstLine.push('Competing paths');
if (cycle.size) firstLine.push(`${cycle.size[0]}×${cycle.size[1]}`);
if (cycle.requestedSlotSizes) {
Comment thread
ChristianPavilonis marked this conversation as resolved.
const displayedSizes = cycle.requestedSlotSizes.slice(0, MAX_BADGE_REQUESTED_SLOT_SIZES);
const remainingSizeCount = cycle.requestedSlotSizes.length - displayedSizes.length;
firstLine.push(
`Req ${formatSizes(displayedSizes)}${remainingSizeCount > 0 ? ` +${remainingSizeCount}` : ''}`
);
}
if (cycle.size) firstLine.push(`Fill ${cycle.size[0]}×${cycle.size[1]}`);
if (cycle.observedSlotSize) {
firstLine.push(`Box ${cycle.observedSlotSize[0]}×${cycle.observedSlotSize[1]}`);
}

const timingLine: string[] = [];
const response = formatMilliseconds(cycle.durations.requestToResponseMs);
Expand Down Expand Up @@ -159,7 +163,8 @@ export class GptDiagnosticsBadgeManager {
this.bindings = bindings;
this.window = options.window ?? (window as unknown as BadgeWindow);
this.document = options.document ?? document;
this.scheduleFrame = options.scheduleFrame ?? defaultScheduleFrame;
this.scheduleFrame =
options.scheduleFrame ?? ((callback) => scheduleFrame(this.window, callback));
this.refreshSlotElementIds();
this.unsubscribeStore = this.store.subscribe(() => {
this.refreshSlotElementIds();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { GptDiagnosticsBinding, GptDiagnosticsSlotExport } from '../../core/types';

import { scheduleFrame } from './presentation_helpers';
import type { GptDiagnosticsBindingInput } from './store';

interface BindingStore {
Expand Down Expand Up @@ -27,14 +28,6 @@ export interface GptDiagnosticsBindingView {

type BindingListener = () => void;

function defaultScheduleFrame(callback: () => void): void {
if (typeof requestAnimationFrame === 'function') {
requestAnimationFrame(() => callback());
} else {
queueMicrotask(callback);
}
}

function isVisibleInViewport(element: HTMLElement, window: BindingWindow): boolean {
const rectangle = element.getBoundingClientRect();
if (rectangle.width <= 0 || rectangle.height <= 0) return false;
Expand Down Expand Up @@ -97,7 +90,8 @@ export class GptDiagnosticsBindingManager {
this.store = store;
this.document = options.document ?? document;
this.window = options.window ?? (window as unknown as BindingWindow);
this.scheduleFrame = options.scheduleFrame ?? defaultScheduleFrame;
this.scheduleFrame =
options.scheduleFrame ?? ((callback) => scheduleFrame(this.window, callback));
this.unsubscribeStore = this.store.subscribe(() => this.scheduleRefresh());

this.window.addEventListener('scroll', this.scheduleRefresh, { passive: true });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { GptDiagnosticsBindingManager } from './binding';
import { GptDiagnosticsObserver } from './observer';
import type { GptObserverWindow } from './observer';
import { GptDiagnosticsOverlay } from './overlay';
import { GptDiagnosticsSlotSizeObserver } from './slot_size_observer';
import { GptDiagnosticsStore } from './store';

interface GptDiagnosticsRuntime {
Expand Down Expand Up @@ -44,6 +45,7 @@ export function installGptDiagnosticsRuntime(
let bindings: GptDiagnosticsBindingManager | undefined;
let badges: GptDiagnosticsBadgeManager | undefined;
let overlay: GptDiagnosticsOverlay | undefined;
let slotSizeObserver: GptDiagnosticsSlotSizeObserver | undefined;
let apiController: GptDiagnosticsApiController | undefined;

try {
Expand All @@ -59,6 +61,7 @@ export function installGptDiagnosticsRuntime(
window: target,
document: target.document,
});
slotSizeObserver = new GptDiagnosticsSlotSizeObserver(store, bindings, { window: target });
overlay = new GptDiagnosticsOverlay(store, bindings, {
window: target,
document: target.document,
Expand All @@ -83,6 +86,7 @@ export function installGptDiagnosticsRuntime(
apiController?.destroy();
overlay?.destroy();
badges?.destroy();
slotSizeObserver?.destroy();
bindings?.destroy();
delete target.__tsjs_gpt_diagnostics_runtime;
},
Expand All @@ -95,6 +99,7 @@ export function installGptDiagnosticsRuntime(
apiController?.destroy();
overlay?.destroy();
badges?.destroy();
slotSizeObserver?.destroy();
bindings?.destroy();
log.warn('gpt diagnostics: runtime installation failed', error);
return undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { GptDiagnosticsRequestCycle } from '../../core/types';

import type { GptDiagnosticsBindingManager } from './binding';
import { unhandledCase } from './exhaustive';
import { formatSizes, scheduleFrame } from './presentation_helpers';
import type { GptDiagnosticsStoreSlotSnapshot, GptDiagnosticsStoreSnapshot } from './store';

export const GPT_DIAGNOSTICS_HOST_ID = 'trusted-server-gpt-diagnostics';
Expand Down Expand Up @@ -99,14 +100,6 @@ const PANEL_STYLES = `
}
`;

function defaultScheduleFrame(callback: () => void): void {
if (typeof requestAnimationFrame === 'function') {
requestAnimationFrame(() => callback());
} else {
queueMicrotask(callback);
}
}

function latestCycle(
slot: GptDiagnosticsStoreSlotSnapshot
): GptDiagnosticsRequestCycle | undefined {
Expand Down Expand Up @@ -277,7 +270,13 @@ function cycleFacts(cycle: GptDiagnosticsRequestCycle): string[] {
if (cycle.loadAtMs !== undefined) facts.push('GPT slot onload observed');
if (cycle.viewableAtMs !== undefined) facts.push('GPT impressionViewable observed');
if (cycle.incompleteSequence) facts.push('Incomplete sequence');
if (cycle.size) facts.push(`Rendered size ${cycle.size[0]}×${cycle.size[1]}`);
if (cycle.requestedSlotSizes) {
facts.push(`Requested slot sizes ${formatSizes(cycle.requestedSlotSizes)}`);
}
if (cycle.size) facts.push(`GPT-reported fill size ${cycle.size[0]}×${cycle.size[1]}`);
Comment thread
ChristianPavilonis marked this conversation as resolved.
if (cycle.observedSlotSize) {
facts.push(`Observed outer slot box ${cycle.observedSlotSize[0]}×${cycle.observedSlotSize[1]}`);
}
if (cycle.isBackfill !== undefined) facts.push(`Backfill ${cycle.isBackfill ? 'yes' : 'no'}`);
if (cycle.slotContentChanged !== undefined) {
facts.push(`Slot content changed ${cycle.slotContentChanged ? 'yes' : 'no'}`);
Expand Down Expand Up @@ -358,7 +357,8 @@ export class GptDiagnosticsOverlay {
this.bindings = bindings;
this.window = options.window ?? (window as unknown as OverlayWindow);
this.document = options.document ?? document;
this.scheduleFrame = options.scheduleFrame ?? defaultScheduleFrame;
this.scheduleFrame =
options.scheduleFrame ?? ((callback) => scheduleFrame(this.window, callback));
this.onExport = options.onExport ?? (() => undefined);
this.onShadowRoot = options.onShadowRoot;
this.onBadgeLayerChange = options.onBadgeLayerChange;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Size } from '../../core/types';

/** Formats CSS sizes consistently across diagnostics presentation surfaces. */
export function formatSizes(sizes: ReadonlyArray<Size>): string {
return sizes.map((size) => `${size[0]}×${size[1]}`).join(', ');
}

/** Schedules presentation work in the target window's next animation frame. */
export function scheduleFrame(
window: Pick<Window, 'requestAnimationFrame'>,
callback: () => void
): void {
if (typeof window.requestAnimationFrame === 'function') {
window.requestAnimationFrame(() => callback());
} else {
queueMicrotask(callback);
}
}
Loading
Loading