-
Notifications
You must be signed in to change notification settings - Fork 12
Show requested, fill, and observed slot sizes in GPT diagnostics #1032
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -106,6 +106,10 @@ function deliveryLabel(cycle: GptDiagnosticsRequestCycle): string | undefined { | |
| } | ||
| } | ||
|
|
||
| function formatSizes(sizes: ReadonlyArray<readonly [number, number]>): string { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ⛏ nitpick — |
||
| return sizes.map((size) => `${size[0]}×${size[1]}`).join(', '); | ||
| } | ||
|
|
||
| function badgeText(cycle: GptDiagnosticsRequestCycle): string { | ||
| const firstLine: string[] = []; | ||
| if (cycle.isEmpty === true) firstLine.push('Empty'); | ||
|
|
@@ -115,7 +119,13 @@ 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) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ♻️ refactor — const shown = cycle.requestedSlotSizes.slice(0, 3);
const extra = cycle.requestedSlotSizes.length - shown.length;
firstLine.push(`Requested ${formatSizes(shown)}${extra > 0 ? ` +${extra}` : ''}`); |
||
| firstLine.push(`Requested ${formatSizes(cycle.requestedSlotSizes)}`); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ♻️ refactor — the badge line can carry up to 16 sizes.
|
||
| } | ||
| if (cycle.size) firstLine.push(`GPT fill ${cycle.size[0]}×${cycle.size[1]}`); | ||
| if (cycle.observedSlotSize) { | ||
| firstLine.push(`Outer box ${cycle.observedSlotSize[0]}×${cycle.observedSlotSize[1]}`); | ||
| } | ||
|
|
||
| const timingLine: string[] = []; | ||
| const response = formatMilliseconds(cycle.durations.requestToResponseMs); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| import type { Size } from '../../core/types'; | ||
|
|
||
| import type { GptDiagnosticsBindingManager } from './binding'; | ||
| import type { GptDiagnosticsStoreSnapshot } from './store'; | ||
|
|
||
| interface SlotSizeStore { | ||
| snapshot(): GptDiagnosticsStoreSnapshot; | ||
| recordObservedSlotSize(runtimeSlotNumber: number, requestNumber: number, size: Size): void; | ||
| subscribe(listener: () => void): () => void; | ||
| } | ||
|
|
||
| interface SlotSizeBindings { | ||
| get: GptDiagnosticsBindingManager['get']; | ||
| subscribe(listener: () => void): () => void; | ||
| } | ||
|
|
||
| type SlotSizeWindow = Window & { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔧 wrench — Fix: type SlotSizeWindow = Window & {
HTMLElement: typeof HTMLElement;
ResizeObserver?: typeof ResizeObserver;
}; |
||
| ResizeObserver?: typeof ResizeObserver; | ||
| }; | ||
|
|
||
| interface SlotSizeObserverOptions { | ||
| window?: SlotSizeWindow; | ||
| scheduleFrame?: (callback: () => void) => void; | ||
| } | ||
|
|
||
| interface ObservedCycle { | ||
| runtimeSlotNumber: number; | ||
| requestNumber: number; | ||
| } | ||
|
|
||
| function defaultScheduleFrame(callback: () => void): void { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ⛏ nitpick — |
||
| if (typeof requestAnimationFrame === 'function') { | ||
| requestAnimationFrame(() => callback()); | ||
| } else { | ||
| queueMicrotask(callback); | ||
| } | ||
| } | ||
|
|
||
| function latestFilledCycle( | ||
| slot: GptDiagnosticsStoreSnapshot['slots'][number] | ||
| ): ObservedCycle | undefined { | ||
| const cycle = slot.requests[slot.requests.length - 1]; | ||
| if (!cycle || cycle.isEmpty !== false || cycle.renderAtMs === undefined) return undefined; | ||
| return { runtimeSlotNumber: slot.runtimeSlotNumber, requestNumber: cycle.requestNumber }; | ||
| } | ||
|
|
||
| /** | ||
| * Observes the outer CSS boxes of uniquely bound elements after filled GPT renders. | ||
| * | ||
| * Measurements remain separately labelled from GPT's reported creative size and | ||
| * are conditionally written with the runtime-slot and request-cycle identity that | ||
| * was current when the measurement was scheduled. | ||
| */ | ||
| export class GptDiagnosticsSlotSizeObserver { | ||
| private readonly store: SlotSizeStore; | ||
| private readonly bindings: SlotSizeBindings; | ||
| private readonly window: SlotSizeWindow; | ||
| private readonly scheduleFrame: (callback: () => void) => void; | ||
| private readonly unsubscribeStore: () => void; | ||
| private readonly unsubscribeBindings: () => void; | ||
| private resizeObserver?: ResizeObserver; | ||
| private refreshScheduled = false; | ||
| private destroyed = false; | ||
|
|
||
| constructor( | ||
| store: SlotSizeStore, | ||
| bindings: SlotSizeBindings, | ||
| options: SlotSizeObserverOptions = {} | ||
| ) { | ||
| this.store = store; | ||
| this.bindings = bindings; | ||
| this.window = options.window ?? (window as unknown as SlotSizeWindow); | ||
| this.scheduleFrame = options.scheduleFrame ?? defaultScheduleFrame; | ||
| this.unsubscribeStore = this.store.subscribe(this.scheduleRefresh); | ||
| this.unsubscribeBindings = this.bindings.subscribe(this.scheduleRefresh); | ||
| this.refresh(); | ||
| } | ||
|
|
||
| destroy(): void { | ||
| if (this.destroyed) return; | ||
| this.destroyed = true; | ||
| this.unsubscribeStore(); | ||
| this.unsubscribeBindings(); | ||
| this.resizeObserver?.disconnect(); | ||
| } | ||
|
|
||
| private readonly scheduleRefresh = (): void => { | ||
| if (this.destroyed || this.refreshScheduled) return; | ||
| this.refreshScheduled = true; | ||
| this.scheduleFrame(() => { | ||
| this.refreshScheduled = false; | ||
| this.refresh(); | ||
| }); | ||
| }; | ||
|
|
||
| private refresh(): void { | ||
| if (this.destroyed) return; | ||
| this.resizeObserver?.disconnect(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 thinking — Every store/bindings notification tears down and re-creates the |
||
| const observations = new Map<HTMLElement, ObservedCycle>(); | ||
| const ResizeObserverConstructor = this.window.ResizeObserver; | ||
| if (typeof ResizeObserverConstructor === 'function') { | ||
| this.resizeObserver = new ResizeObserverConstructor((entries) => { | ||
| for (const entry of entries) { | ||
| const element = entry.target; | ||
| if (!(element instanceof this.window.HTMLElement)) continue; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔧 wrench —
Not reachable through type SlotSizeWindow = Window & {
HTMLElement: typeof HTMLElement;
ResizeObserver?: typeof ResizeObserver;
}; |
||
| const cycle = observations.get(element); | ||
| if (cycle) this.scheduleMeasure(element, cycle); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| for (const slot of this.store.snapshot().slots) { | ||
| const cycle = latestFilledCycle(slot); | ||
| const binding = this.bindings.get(slot.runtimeSlotNumber); | ||
| if (!cycle || binding.binding.status !== 'bound' || !binding.element?.isConnected) continue; | ||
| observations.set(binding.element, cycle); | ||
| this.resizeObserver?.observe(binding.element); | ||
| this.scheduleMeasure(binding.element, cycle); | ||
| } | ||
| } | ||
|
|
||
| private scheduleMeasure(element: HTMLElement, cycle: ObservedCycle): void { | ||
| this.scheduleFrame(() => this.measure(element, cycle)); | ||
| } | ||
|
|
||
| private measure(element: HTMLElement, cycle: ObservedCycle): void { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ♻️ refactor — Fix: add |
||
| const binding = this.bindings.get(cycle.runtimeSlotNumber); | ||
| if (binding.binding.status !== 'bound' || binding.element !== element || !element.isConnected) { | ||
| return; | ||
| } | ||
|
|
||
| const rectangle = element.getBoundingClientRect(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 thinking — A filled render whose container is |
||
| if ( | ||
| !Number.isFinite(rectangle.width) || | ||
| !Number.isFinite(rectangle.height) || | ||
| rectangle.width < 0 || | ||
| rectangle.height < 0 | ||
| ) { | ||
| return; | ||
| } | ||
| this.store.recordObservedSlotSize(cycle.runtimeSlotNumber, cycle.requestNumber, [ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔧 wrench — Fix: this.store.recordObservedSlotSize(cycle.runtimeSlotNumber, cycle.requestNumber, [
Math.round(rectangle.width),
Math.round(rectangle.height),
]); |
||
| rectangle.width, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ♻️ refactor — round the measured box.
this.store.recordObservedSlotSize(cycle.runtimeSlotNumber, cycle.requestNumber, [
Math.round(rectangle.width),
Math.round(rectangle.height),
]); |
||
| rectangle.height, | ||
| ]); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔧 wrench —
requestedSlotSizesrecords sizes GPT never received on publisher-defined slots.slot.formatsis passed unconditionally, but Trusted Server only supplies formats to GPT on thedefineSlotpath above (line 1023). WhenexistingSlotis found — the publisher defined the div first — TS reuses that slot and never applies sizes to it; there is nodefineSizeMapping/setSizesanywhere in the integration. On those slots the panel showsRequested slot sizes 300×250, 728×90for a list GPT never saw, and the docs assert it is "the configuredAuctionSlot.formatslist Trusted Server supplied to GPT for that request". That is the exact misdirection #1031 exists to remove.Note
tsOwned ? slot.formats : undefinedis not the fix either: refresh cycles re-find TS's own slot throughgetSlots(), sotsOwnedis false there and the accurate list would be dropped. Keying on the handoff record survives refreshes:Alternatively keep recording it but relabel the field, badge, panel, and docs as Trusted Server's configured sizes rather than what GPT was given.