Skip to content

Add pgamdirect Analytics Adapter#14778

Merged
patmmccann merged 2 commits intoprebid:masterfrom
mastap150:add-pgamdirect-analytics
Apr 25, 2026
Merged

Add pgamdirect Analytics Adapter#14778
patmmccann merged 2 commits intoprebid:masterfrom
mastap150:add-pgamdirect-analytics

Conversation

@mastap150
Copy link
Copy Markdown
Contributor

Summary

Companion to pgamdirectBidAdapter (merged). Publishers install this alongside the bid adapter to forward auction telemetry to the PGAM Direct SSP backend.

Why

A server-side-only ledger (what our bidder sees from RTB) can't answer two questions:

  1. Did the publisher actually render the ad? RTB says we won; client-side confirmation tells us if the user saw it.
  2. What did competitors price the same auction at? We own our DSP calls; other SSPs running through the same Prebid wrapper are invisible without client-side telemetry.

This adapter closes both gaps.

What it forwards

Four events only (deliberately narrow):

  • `AUCTION_END` — list of bidders seen + their CPMs (capped at 20 entries)
  • `BID_WON` — Prebid-layer winner, price, size, creative
  • `AD_RENDER_SUCCEEDED` — client-confirmed impression
  • `AD_RENDER_FAILED` — with reason

Payload shape

Normalised before POST — we deliberately drop raw Prebid event args which carry full FPD / user.eids / custom bidder params we don't need and shouldn't exfiltrate. Sink: `https://app.pgammedia.com/api/analytics-events\`, one POST per event, content-type text/plain (keeps CORS simple — same rationale as the bid adapter).

Config

```js
pbjs.enableAnalytics({
provider: 'pgamdirect',
options: {
orgId: '', // required
endpoint: 'https://...' // optional override
}
});
```

GVL ID

1353 (PGAM Media LLC, same as the bid adapter).

Tests

12 spec cases covering:

  • Registration + GVL ID
  • `orgId` required on enableAnalytics
  • `normalise()` transform for BID_WON, AUCTION_END, AD_RENDER_*, and unknown events
  • 20-entry cap on `bidders_seen`
  • Filters out bidders without a code

Event-emission XHR round-tripping isn't covered in this spec because the sinon mock + AnalyticsAdapter async queue interact oddly in the harness. We export `normalise()` directly so the pure transform is testable; the ajax call itself is covered by upstream AnalyticsAdapter base-class tests.

Test plan

  • `tsc --noEmit` — clean
  • `gulp test --file test/spec/modules/pgamdirectAnalyticsAdapter_spec.js` — 12/12 pass

🤖 Generated with Claude Code

Companion to modules/pgamdirectBidAdapter.ts (merged prebid#14763).
Publishers install this alongside the bid adapter to forward auction
telemetry to the PGAM Direct SSP backend.

Forwards four Prebid events, deliberately narrow:
  AUCTION_END           — competitor CPMs seen in the auction
  BID_WON               — Prebid-layer winner + price
  AD_RENDER_SUCCEEDED   — client-confirmed impression
  AD_RENDER_FAILED      — with reason (exception / timeout / etc)

The value add over a server-side-only ledger: client-confirmed render
vs. "RTB said we won", plus visibility into what other bidders priced
the same auction at (we own server-side data for our own DSP calls,
but not for the ones other SSPs made through the same Prebid wrapper).

Payload is normalised into a small fixed shape before POST — we
deliberately drop the raw Prebid event args, which carry full FPD /
user.eids / custom bidder params that we don't need and shouldn't
exfiltrate. Sink: https://app.pgammedia.com/api/analytics-events (one
POST per event; content-type text/plain to keep CORS simple).

Config:
  pbjs.enableAnalytics({
    provider: 'pgamdirect',
    options: {
      orgId: '<pgam org id>',           // required
      endpoint: 'https://...'            // optional override
    }
  });

GVL ID 1353 (PGAM Media LLC, same as the bid adapter).

Tests: 12 covering registration, orgId validation, and the pure
normalise transform across all 4 forwarded event types (including
the 20-entry bidders_seen cap and filter-out of bidders with no
code). Event-emission path is not covered in this spec because the
sinon mock + AnalyticsAdapter async queue interact oddly in the
test harness — we export normalise() directly so the transform is
verifiable without the full event pipeline. The ajax call itself is
covered by upstream AnalyticsAdapter base-class tests.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3fc5cf5674

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread modules/pgamdirectAnalyticsAdapter.ts Outdated
typeof a.bid === 'object' && a.bid
? ((a.bid as Record<string, unknown>).bidderCode as string | undefined)
: undefined,
ad_unit_code: typeof a.adId === 'string' ? a.adId : undefined,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep ad_unit_code semantics consistent across event types

normalise() sets ad_unit_code from a.adUnitCode for BID_WON but from a.adId for AD_RENDER_SUCCEEDED/AD_RENDER_FAILED, so the same field represents different identifiers depending on event type. In auctions with multiple ad units (or repeated wins from the same bidder), this prevents reliable win→render reconciliation and can misattribute render outcomes. Use the render event’s bid object (args.bid.adUnitCode) for ad_unit_code and keep adId in a separate field if needed.

Useful? React with 👍 / 👎.

Comment thread modules/pgamdirectAnalyticsAdapter.ts
@coveralls
Copy link
Copy Markdown
Collaborator

coveralls commented Apr 22, 2026

Coverage Report for CI Build 24801750822

Coverage decreased (-0.003%) to 96.394%

Details

  • Coverage decreased (-0.003%) from the base build.
  • Patch coverage: 13 uncovered changes across 1 file (98 of 111 lines covered, 88.29%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
modules/pgamdirectAnalyticsAdapter.ts 41 28 68.29%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 229853
Covered Lines: 221565
Line Coverage: 96.39%
Relevant Branches: 53362
Covered Branches: 43400
Branch Coverage: 81.33%
Branches in Coverage %: No
Coverage Strength: 72.69 hits per line

💛 - Coveralls

Two P1s flagged by Codex on the initial commit:

1. ad_unit_code semantic inconsistency

Original code pulled ad_unit_code from args.adUnitCode on BID_WON
but from args.adId on AD_RENDER_*, so the same field represented
different identifiers across event types. In auctions with multiple
ad units (or repeated wins from the same bidder), this prevented
reliable win → render reconciliation and could misattribute render
outcomes.

Fix: render events now read ad_unit_code from args.bid.adUnitCode
(stable across the BID_WON ↔ AD_RENDER_* join for the same slot).
adId moves to its own field `ad_id` so per-bid traceability is
preserved. Type definition updated with a comment explaining the
split so future contributors don't re-conflate them.

2. Missing fetch keepalive

Prebid AGENTS.md §71 requires low-priority telemetry calls to set
fetch keepalive. Without it, BID_WON + AD_RENDER_* events emitted
near page unload get dropped before reaching the endpoint — and
those are exactly the events that fire in that window.

Added `keepalive: true` to the ajax call. Prebid's ajax helper
already supports the flag (src/ajax.ts option); no adapter-side
polyfill needed.

Tests: +1 spec case covering "missing bid object on AD_RENDER_*
still captures ad_id cleanly." Existing render-event assertions
updated to verify the ad_unit_code-vs-ad_id split explicitly.
13/13 pass (was 12).
@mastap150
Copy link
Copy Markdown
Contributor Author

Thanks @chatgpt-codex-connector — both P1s addressed in commit 2758809:

  1. ad_unit_code semantic: render events now read from args.bid.adUnitCode (stable across the BID_WON ↔ AD_RENDER_* join for the same slot). adId moves to its own field ad_id so per-bid traceability is preserved. Type definition updated with a comment explaining the split so future contributors don't re-conflate them.
  2. keepalive: added keepalive: true on the ajax options per AGENTS.md §71. Same concern you raised — the most valuable events (BID_WON + AD_RENDER_*) fire exactly in the unload window.

Tests: 13/13 pass, +1 spec case covering missing-bid-object on render events.

@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@patmmccann patmmccann merged commit 37876d3 into prebid:master Apr 25, 2026
102 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants