Skip to content

Duplicate GPT slots per ad unit (TS container slot competes with publisher inner-div slot) #944

Description

@jevansnyc

Summary

TS defines a slot on a ...-container div alongside the publisher's own inner-div slot on the same GAM unit path (/88059007/prospect-a/homepage), yielding ~9 registered slots for ~3-4 real placements. Risk of competing defineSlot / double ad requests.

Evidence

googletag.pubads().getSlots() shows both ad-header-0-_R_...-container (TS) and ad-header-0-_r_1_ (publisher) for the same unit; same pattern for fixed_bottom and in_content. Observed on a live Next.js App Router publisher with TS enabled.

Verification — confirmed production impact

A fresh headless Chrome capture of the live Autoblog homepage on 2026-07-24 confirmed this is not just duplicate getSlots() bookkeeping: both slot sets issued successful GAM requests.

  1. At 16:52:49.929Z, GAM received a successful SRA request for three TS-owned container slots, all carrying ts_initial=1:
    • ad-header-0-_R_…-container
    • ad-fixed_bottom-0-_R_…-container
    • ad-in_content-…-in_content-0-container
  2. At 16:52:53.419Z (~3.5 seconds later), GAM received another successful SRA request for publisher-owned inner slots, including:
    • ad-header-0-_r_1_
    • ad-fixed_bottom-0-_r_f_
    • publisher in_content slots and the interstitial
  3. GPT emitted slotRequested for both groups. The initial TS in_content slot and the later publisher header/fixed-bottom slots returned non-empty renders.

SRA combines slots within each request, but these are separate GAM requests. Header and fixed-bottom are clearly requested twice for the same semantic placement. The content element IDs change during React hydration, so the exact one-to-one content mapping needs a DOM-lifecycle trace; it does not change the confirmed header/footer duplication.

Root cause

The current TS implementation deliberately takes this path when the publisher has not yet defined the inner slot:

// Use outer container div for TS's slot when publisher hasn't defined
// theirs yet — keeps both slots on separate divs so publisher's
// later defineSlot on the inner div doesn't conflict.
const containerEl = document.getElementById(`${actualDivId}-container`);
const slotDivId = containerEl?.id ?? actualDivId;
const defined = g.defineSlot?.(slot.gam_unit_path, slot.formats, slotDivId);

Current source, lines 503–556 then calls display() for the TS-defined slot, whereas reused publisher slots are refreshed. This intentionally prevents a GPT conflict but creates two independently requestable slots when the publisher later defines the inner div.

Impact

  • Extra GAM requests and slot lifecycle work for the same user-visible placement.
  • Potential duplicate auction/creative rendering and viewability/revenue-reporting distortion.
  • Extra page/network cost. A duplicate GAM ad-unit path is valid GPT usage when it represents genuinely distinct placements; here the header and fixed-bottom pairs are not distinct placements.

Acceptance

  • One registered/requested GPT slot per placement, or
  • an explicitly documented dual-ownership design that guarantees only one of the container/inner slots is displayed or refreshed and therefore only one GAM request is sent per placement.

Suggested regression coverage

Model the observed ordering: TS adInit() executes before the publisher’s defineSlot(innerDiv). Assert that the later publisher definition cannot result in a second displayed/refreshed GAM slot for the same configured placement, and capture the corresponding slotRequested events (not only getSlots()).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions