@@ -108,12 +108,15 @@ export const useGravityAd = (options?: {
108108 /** Skip the "wait for first user message" gate. Used by the freebuff
109109 * waiting room, which has no conversation but still needs ads. */
110110 forceStart ?: boolean
111- /** Which ad network to query. Defaults to Gravity. */
111+ /** Primary ad network to query. Defaults to Gravity. */
112112 provider ?: AdProvider
113+ /** Backup ad network to try when the primary returns no fill or errors. */
114+ fallbackProvider ?: AdProvider
113115} ) : GravityAdState => {
114116 const enabled = options ?. enabled ?? true
115117 const forceStart = options ?. forceStart ?? false
116118 const provider : AdProvider = options ?. provider ?? 'gravity'
119+ const fallbackProvider = options ?. fallbackProvider
117120 const [ ad , setAd ] = useState < AdResponse | null > ( null )
118121 const [ adData , setAdData ] = useState < AdData | null > ( null )
119122 const [ isLoading , setIsLoading ] = useState ( false )
@@ -278,49 +281,63 @@ export const useGravityAd = (options?: {
278281 }
279282 }
280283
281- try {
282- const response = await fetch ( `${ WEBSITE_URL } /api/v1/ads` , {
283- method : 'POST' ,
284- headers : {
285- 'Content-Type' : 'application/json' ,
286- Authorization : `Bearer ${ authToken } ` ,
287- } ,
288- body : JSON . stringify ( {
289- provider,
290- messages : adMessages ,
291- sessionId : useChatStore . getState ( ) . chatSessionId ,
292- device : getDeviceInfo ( ) ,
293- // Carbon requires a real browser-ish useragent for targeting/fraud
294- // detection. Gravity ignores it. We source one centrally so every
295- // provider that needs it sees the same value.
296- userAgent : getAdUserAgent ( ) ,
297- } ) ,
298- } )
284+ const providersToTry =
285+ fallbackProvider && fallbackProvider !== provider
286+ ? [ provider , fallbackProvider ]
287+ : [ provider ]
299288
300- if ( ! response . ok ) {
301- logger . warn (
302- { provider, status : response . status , response : await response . json ( ) } ,
303- '[ads] Web API returned error' ,
304- )
305- return null
306- }
289+ for ( const providerToTry of providersToTry ) {
290+ try {
291+ const response = await fetch ( `${ WEBSITE_URL } /api/v1/ads` , {
292+ method : 'POST' ,
293+ headers : {
294+ 'Content-Type' : 'application/json' ,
295+ Authorization : `Bearer ${ authToken } ` ,
296+ } ,
297+ body : JSON . stringify ( {
298+ provider : providerToTry ,
299+ messages : adMessages ,
300+ sessionId : useChatStore . getState ( ) . chatSessionId ,
301+ device : getDeviceInfo ( ) ,
302+ // Carbon requires a real browser-ish useragent for targeting/fraud
303+ // detection. Gravity ignores it. We source one centrally so every
304+ // provider that needs it sees the same value.
305+ userAgent : getAdUserAgent ( ) ,
306+ } ) ,
307+ } )
307308
308- const data = await response . json ( )
309- const variant = data . variant ?? 'banner'
309+ if ( ! response . ok ) {
310+ logger . warn (
311+ {
312+ provider : providerToTry ,
313+ status : response . status ,
314+ response : await response . json ( ) ,
315+ } ,
316+ '[ads] Web API returned error' ,
317+ )
318+ continue
319+ }
310320
311- if ( variant === 'choice' && Array . isArray ( data . ads ) && data . ads . length > 0 ) {
312- return { variant : 'choice' , ads : data . ads as AdResponse [ ] }
313- }
321+ const data = await response . json ( )
322+ const variant = data . variant ?? 'banner'
314323
315- if ( data . ad ) {
316- return { variant : 'banner' , ad : data . ad as AdResponse }
317- }
324+ if (
325+ variant === 'choice' &&
326+ Array . isArray ( data . ads ) &&
327+ data . ads . length > 0
328+ ) {
329+ return { variant : 'choice' , ads : data . ads as AdResponse [ ] }
330+ }
318331
319- return null
320- } catch ( err ) {
321- logger . error ( { err } , '[ads] Failed to fetch ad' )
322- return null
332+ if ( data . ad ) {
333+ return { variant : 'banner' , ad : data . ad as AdResponse }
334+ }
335+ } catch ( err ) {
336+ logger . error ( { err, provider : providerToTry } , '[ads] Failed to fetch ad' )
337+ }
323338 }
339+
340+ return null
324341 }
325342
326343 // Update tick function (uses ref to avoid useCallback dependency issues)
@@ -413,7 +430,7 @@ export const useGravityAd = (options?: {
413430 clearInterval ( id )
414431 ctrlRef . current . intervalId = null
415432 }
416- } , [ shouldStart , shouldHideAds ] )
433+ } , [ shouldStart , shouldHideAds , provider , fallbackProvider ] )
417434
418435 // Don't return ad when ads should be hidden
419436 const visible = shouldStart && ! shouldHideAds
0 commit comments