(config: { zoneKey: string })
| 96 | } |
| 97 | |
| 98 | export function createCarbonProvider(config: { zoneKey: string }): AdProvider { |
| 99 | return { |
| 100 | id: 'carbon', |
| 101 | fetchAd: async (input: FetchAdInput): Promise<FetchAdResult> => { |
| 102 | const { clientIp, userAgent, requestUserAgent, testMode, logger, fetch } = |
| 103 | input |
| 104 | |
| 105 | if (!clientIp || !userAgent) { |
| 106 | logger.debug( |
| 107 | { hasIp: !!clientIp, hasUA: !!userAgent }, |
| 108 | '[ads:carbon] Missing required clientIp or userAgent', |
| 109 | ) |
| 110 | return null |
| 111 | } |
| 112 | |
| 113 | const params = new URLSearchParams({ |
| 114 | useragent: userAgent, |
| 115 | forwardedip: clientIp, |
| 116 | }) |
| 117 | // Carbon's `ignore=yes` loads ads without counting impressions. Use it |
| 118 | // in non-prod so we never accidentally bill advertisers for dev traffic. |
| 119 | if (testMode) params.set('ignore', 'yes') |
| 120 | |
| 121 | const url = `${CARBON_URL_BASE}/${config.zoneKey}.json?${params.toString()}` |
| 122 | |
| 123 | const fetchOne = async (): Promise<NormalizedAd | null> => { |
| 124 | const response = await fetch(url, { |
| 125 | method: 'GET', |
| 126 | headers: { |
| 127 | 'User-Agent': requestUserAgent ?? userAgent, |
| 128 | }, |
| 129 | }) |
| 130 | if (!response.ok) { |
| 131 | let body: unknown |
| 132 | try { |
| 133 | body = await response.text() |
| 134 | } catch { |
| 135 | body = 'Unable to parse error response' |
| 136 | } |
| 137 | logger.error( |
| 138 | { url, status: response.status, body }, |
| 139 | '[ads:carbon] API returned error', |
| 140 | ) |
| 141 | return null |
| 142 | } |
| 143 | const data = (await response.json()) as CarbonResponse |
| 144 | const first = data.ads?.[0] |
| 145 | if (!first) return null |
| 146 | return normalizeCarbonAd(first) |
| 147 | } |
| 148 | |
| 149 | const results = await Promise.all( |
| 150 | Array.from({ length: CARBON_CHOICE_FETCH_COUNT }, fetchOne), |
| 151 | ) |
| 152 | |
| 153 | // Dedupe by description — Carbon issues a fresh tracker URL per request |
| 154 | // even for the same creative, so clickUrl/impUrl can't serve as a |
| 155 | // stable identity key. |
no test coverage detected