questionnaire docs
Ads

Country-Specific KVs

Country-based KV overrides for ADX/GAM ads.

Country-Specific KVs

Resolve the correct waterfall KV arrays per ad placement based on the visitor's country.

Entry: lib/utils/countrySpecificKvs.js, lib/utils/adConfig.js

Goal

At page render, resolve the correct waterfall KV arrays per ad placement based on the visitor's country, using adConfig.countrySpecificKvs from the CMS API when that level's adConfig.kvMode === "country", otherwise falling back to the existing time-specific / static behavior.

The CMS stores country-bound KV sets at three levels:

LevelSource on pageLocation
Tenant defaultsAlways availabletenant.adConfig.countrySpecificKvs + tenant.adConfig.kvMode
Category (URL)Category pagescategory.adConfig.countrySpecificKvs + category.adConfig.kvMode
Article (URL)Article pagesarticle.adConfig.countrySpecificKvs + article.adConfig.kvMode

The kvMode toggle

Each adConfig carries kvMode: "time" | "country" (absent = "time"). It selects which KV system is active at that level:

  • "time" → evaluate timeSpecificKvs (existing behavior, unchanged).
  • "country" → evaluate countrySpecificKvs.
  • The inactive system's data may still be present in the payload — ignore it.

When every level has kvMode: "time" (or no kvMode), resolution is identical to the time-specific contract.


Visitor country detection

  • Country is an ISO 3166-1 alpha-2 code, uppercase ("US", "IN", "AE").
  • Prefer the CDN/edge header already available to the app, e.g. CloudFront-Viewer-Country, Cloudflare CF-IPCountry, or Vercel x-vercel-ip-country. Fall back to a geo-IP lookup only if no header exists.
  • Normalize: String(code).trim().toUpperCase().
  • Unknown / undetectable country (missing header, "XX", "T1", etc.) → treat as no matching set → static fallback.

API payload shape

countrySpecificKvs is an optional array on adConfig. Each set:

type CountrySpecificKvSet = {
  label?: string; // dashboard label, informational only
  countries: string[]; // ISO alpha-2, e.g. ["US","CA"]; unique across sets per level
  [kvField: string]: string[] | undefined; // per-ad-type waterfall values (two-decimal strings)

  // CMS-internal fields — IGNORE on the frontend:
  useHighestPrice?: boolean; // how the arrays were authored (generated vs manual)
};

The frontend only reads countries and the KV arrays.

Tenant adConfig.countrySpecificKvs — KV field keys

KeyUsed on
displayKv1ValuesArticleArticle pages (tenant default)
displayKv1ValuesCategoryCategory pages (tenant default)
interstitialKv1ValuesInterstitial
rewardedKv1ValuesRewarded (first page)
rewardedKv1ValuesSecondPageRewarded (second page)
collapsibleKv1ValuesCollapsible

Category / Article adConfig.countrySpecificKvs — KV field keys

KeyPlacement
displayKv1ValuesDisplay 1
displayKv1Values2Display 2
interstitialKv1ValuesInterstitial
rewardedKv1ValuesRewarded (first page)
rewardedKv1ValuesSecondPageRewarded (second page)
stickyKv1ValuesSticky
skyscrapperLeftKv1ValuesSkyscraper left
skyscrapperRightKv1ValuesSkyscraper right
collapsibleKv1ValuesCollapsible

(Same keys as time-specific.)


Resolution rules

  1. Detect the visitor country CC (uppercase alpha-2, or null if unknown).
  2. Pick the active source level (whole-replace, mode-aware):
    • activeSets(adConfig) = adConfig.kvMode === "country" ? adConfig.countrySpecificKvs : adConfig.timeSpecificKvs
    • On category/article pages: if activeSets(urlAdConfig) exists and length > 0 → source = URL level (its mode, its sets). Tenant sets of both systems are ignored entirely.
    • Otherwise if activeSets(tenantAdConfig) is non-empty → source = tenant level (its mode, its sets).
    • Otherwise → static fields only (current behavior).
  3. If the source level's mode is "time" → run the existing time-specific resolution against those sets.
  4. If the source level's mode is "country":
    • Find active set: first set whose countries includes CC.
    • Per KV field:
      • Active set exists and field is a non-empty array → use it.
      • Active set exists and field missing/empty → no KV (do not fall back to static).
      • No set matches CC (or CC unknown) → use the existing static field from adConfig (URL static → tenant static).
  5. CMS guarantees a country appears in at most one set per level; if bad data has duplicates, first matching set wins.
  6. label, useHighestPrice, *HighestPrice, tier*, priceCount are never used in ad logic.
  7. AdSense pages: no change (no KV waterfalls).

Reference resolver

Extends the existing resolveTimeSpecificKvField from the time doc.

function normalizeCountry(code) {
  if (!code) return null;
  const value = String(code).trim().toUpperCase();
  return /^[A-Z]{2}$/.test(value) ? value : null;
}

function findActiveCountrySet(sets, country) {
  if (!Array.isArray(sets) || sets.length === 0 || !country) return null;
  return (
    sets.find(
      (set) =>
          Array.isArray(set?.countries) &&
          set.countries.some((c) => normalizeCountry(c) === country),
    ) ?? null
  );
}

function activeSets(adConfig) {
  if (!adConfig) return null;
  const sets =
      adConfig.kvMode === "country"
        ? adConfig.countrySpecificKvs
        : adConfig.timeSpecificKvs;
  return Array.isArray(sets) && sets.length > 0 ? sets : null;
}

/**
 * Resolve one KV field, honoring adConfig.kvMode per level.
 */
export function resolveKvField({
  kvField,
  urlAdConfig,
  tenantAdConfig,
  country,
  hour,
}) {
  const staticFallback = () =>
    urlAdConfig?.[kvField] ?? tenantAdConfig?.[kvField];

  const urlSets = activeSets(urlAdConfig);
  const sourceConfig = urlSets
    ? urlAdConfig
    : activeSets(tenantAdConfig)
      ? tenantAdConfig
      : null;
  if (!sourceConfig) return staticFallback();

  if (sourceConfig.kvMode !== "country") {
    return resolveTimeSpecificKvField({
      kvField,
      urlAdConfig,
      tenantAdConfig,
      hour,
    });
  }

  const set = findActiveCountrySet(
    sourceConfig.countrySpecificKvs,
    normalizeCountry(country),
  );
  if (!set) return staticFallback();

  const values = set[kvField];
  if (Array.isArray(values) && values.length > 0) return values;

  return undefined; // matched set, empty field → no KV, no static fallback
}

Integration guide

  • Replace direct calls to resolveTimeSpecificKvField in the KV merge path with resolveKvField, passing the detected country.
  • Country detection (client): CountryProvider exposes { country, settled }. It always starts unsettled on SSR and the first client paint (do not read document.cookie in useState — that causes hydration mismatches when country-mode ads return null on the server and a slot on the client). After mount it settles from the visitor_country cookie, geo API, or timeout.
  • Category page: urlAdConfig = category.adConfig; tenant static display field is displayKv1ValuesCategory.
  • Article page: urlAdConfig = article.adConfig; tenant static display field is displayKv1ValuesArticle.
  • Rewarded second page: kvField = "rewardedKv1ValuesSecondPage".

On this page