Skip to content

Commit 90e6615

Browse files
Sync public snapshot from freebuff-private
Source: CodebuffAI/freebuff-private@e66d73be0e1a8e4c453136e031edb6c817cceaeb
1 parent 9460045 commit 90e6615

15 files changed

Lines changed: 1015 additions & 14 deletions

bun.lock

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/src/components/__tests__/ad-banner.test.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,66 @@ import {
99
getAdDisplayLabel,
1010
getCardAdLayout,
1111
getInlineAdLayout,
12+
orderedRequestedAds,
1213
} from '../ad-banner'
1314
import { initializeThemeStore } from '../../hooks/use-theme'
1415

1516
beforeAll(() => {
1617
initializeThemeStore()
1718
})
1819

20+
describe('requested waiting-room ads', () => {
21+
test('mount order follows the canonical request and excludes duplicates/unrequested ads', () => {
22+
const ads = [
23+
{
24+
placementId: 'waiting-room-2',
25+
impUrl: 'two',
26+
adText: '',
27+
title: '',
28+
cta: '',
29+
url: '',
30+
favicon: '',
31+
clickUrl: '',
32+
},
33+
{
34+
placementId: 'waiting-room-1',
35+
impUrl: 'one',
36+
adText: '',
37+
title: '',
38+
cta: '',
39+
url: '',
40+
favicon: '',
41+
clickUrl: '',
42+
},
43+
{
44+
placementId: 'waiting-room-1',
45+
impUrl: 'duplicate',
46+
adText: '',
47+
title: '',
48+
cta: '',
49+
url: '',
50+
favicon: '',
51+
clickUrl: '',
52+
},
53+
{
54+
placementId: 'waiting-room-4',
55+
impUrl: 'hidden',
56+
adText: '',
57+
title: '',
58+
cta: '',
59+
url: '',
60+
favicon: '',
61+
clickUrl: '',
62+
},
63+
]
64+
expect(
65+
orderedRequestedAds(ads, ['waiting-room-1', 'waiting-room-2']).map(
66+
(ad) => ad.impUrl,
67+
),
68+
).toEqual(['one', 'two'])
69+
})
70+
})
71+
1972
describe('card ad layout', () => {
2073
const ad = {
2174
adText:
@@ -171,6 +224,31 @@ describe('card ad render', () => {
171224
expect(await renderCard({})).toContain('Ad')
172225
})
173226

227+
test('reports presentation only after the card mounts', async () => {
228+
const presented: string[] = []
229+
const setup = await createTestRenderer({
230+
width: 78,
231+
height: AD_CARD_HEIGHT,
232+
})
233+
const root = createRoot(setup.renderer)
234+
const mountedAd = { ...ad, provider: 'first_party' as const }
235+
236+
flushSync(() => {
237+
root.render(
238+
<AdCard
239+
ad={mountedAd}
240+
width={78}
241+
onImpression={(presentedAd) => presented.push(presentedAd.impUrl)}
242+
/>,
243+
)
244+
})
245+
await setup.renderOnce()
246+
expect(presented).toEqual(['imp-1'])
247+
248+
flushSync(() => root.unmount())
249+
setup.renderer.destroy()
250+
})
251+
174252
test('does not print the headline twice when there is no CTA', async () => {
175253
const frame = await renderCard({ cta: '' })
176254

cli/src/components/ad-banner.tsx

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
truncateToLines,
1010
truncateToWidth,
1111
} from '@codebuff/common/ads/inline-ad-layout'
12+
import { visibleWaitingRoomPlacementIds } from '@codebuff/common/ads/waiting-room-placements'
1213
import { safeOpen } from '../utils/open-url'
1314
import React, { useState, useMemo, useEffect } from 'react'
1415

@@ -21,6 +22,7 @@ import type { AdResponse } from '../hooks/use-gravity-ad'
2122

2223
interface ChoiceAdBannerProps {
2324
ads: AdResponse[]
25+
placementIds?: readonly string[]
2426
onClick?: (ad: AdResponse) => void
2527
onImpression?: (ad: AdResponse) => void
2628
}
@@ -31,7 +33,6 @@ interface ChoiceAdBannerProps {
3133
// subtracts this from the model picker's height budget.
3234
export const AD_CARD_HEIGHT = 5
3335
export const INLINE_AD_CARD_HEIGHT = 4 // border-top + header row + detail row + border-bottom
34-
const MIN_CARD_WIDTH = 60 // Minimum width per ad card to remain readable
3536

3637
// Layout lives in `common` so the advertiser campaign builder's creative
3738
// preview fits copy exactly the way this renderer does. Re-exported here
@@ -331,6 +332,7 @@ export const SingleAdBanner: React.FC<{
331332
*/
332333
export const ChoiceAdBanner: React.FC<ChoiceAdBannerProps> = ({
333334
ads,
335+
placementIds,
334336
onClick,
335337
onImpression,
336338
}) => {
@@ -340,11 +342,14 @@ export const ChoiceAdBanner: React.FC<ChoiceAdBannerProps> = ({
340342
const colAvail = terminalWidth - 2
341343

342344
// Only show as many ads as fit with a healthy minimum width; hide the rest
343-
const maxVisible = Math.max(1, Math.floor(colAvail / MIN_CARD_WIDTH))
344-
const visibleAds = useMemo(
345-
() => (ads.length > maxVisible ? ads.slice(0, maxVisible) : ads),
346-
[ads, maxVisible],
347-
)
345+
const maxVisible =
346+
placementIds?.length ?? visibleWaitingRoomPlacementIds(terminalWidth).length
347+
const visibleAds = useMemo(() => {
348+
const requested = placementIds?.length
349+
? orderedRequestedAds(ads, placementIds)
350+
: ads
351+
return requested.slice(0, maxVisible)
352+
}, [ads, maxVisible, placementIds])
348353

349354
const widths = useMemo(
350355
() => columnWidths(visibleAds.length, colAvail),
@@ -379,3 +384,14 @@ export const ChoiceAdBanner: React.FC<ChoiceAdBannerProps> = ({
379384
</box>
380385
)
381386
}
387+
388+
/** Preserve canonical request order and never mount a duplicate slot response. */
389+
export function orderedRequestedAds(
390+
ads: AdResponse[],
391+
placementIds: readonly string[],
392+
): AdResponse[] {
393+
return placementIds.flatMap((placementId) => {
394+
const ad = ads.find((candidate) => candidate.placementId === placementId)
395+
return ad ? [ad] : []
396+
})
397+
}

cli/src/components/freebuff-landing-screen.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
44

55
import { Button } from './button'
66
import { ChoiceAdBanner, AD_CARD_HEIGHT } from './ad-banner'
7+
import { visibleWaitingRoomPlacementIds } from '@codebuff/common/ads/waiting-room-placements'
78
import { FreebuffModelSelector } from './freebuff-model-selector'
89
import { ShimmerText } from './shimmer-text'
910
import {
@@ -405,13 +406,15 @@ export const FreebuffLandingScreen: React.FC<FreebuffLandingScreenProps> = ({
405406
// forceStart bypasses the "wait for first user message" gate inside the hook,
406407
// which would otherwise block ads here since no conversation exists yet.
407408
// The server tries Gravity first, then falls back to ZeroClick and Carbon.
409+
const waitingRoomPlacementIds = visibleWaitingRoomPlacementIds(terminalWidth)
408410
const { ads, recordClick, recordImpression } = useGravityAd({
409411
enabled: true,
410412
forceStart: true,
411413
provider: 'gravity',
412414
// Legacy wire name for this surface — the ads API maps it to placements,
413415
// so it must not change with the component rename.
414416
surface: 'waiting_room',
417+
placementIds: waitingRoomPlacementIds,
415418
})
416419

417420
useFreebuffCtrlCExit()
@@ -871,6 +874,7 @@ export const FreebuffLandingScreen: React.FC<FreebuffLandingScreenProps> = ({
871874
{ads ? (
872875
<ChoiceAdBanner
873876
ads={ads}
877+
placementIds={waitingRoomPlacementIds}
874878
onClick={recordClick}
875879
onImpression={recordImpression}
876880
/>

cli/src/hooks/__tests__/use-gravity-ad.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import { describe, expect, test } from 'bun:test'
22

33
import {
44
claimAdImpression,
5+
dispatchFirstPartyViewAcknowledgement,
56
isAnswerMessage,
67
isInlineAdEligibleAnswer,
78
} from '../use-gravity-ad'
89

910
import type { ChatMessage } from '../../types/chat'
11+
import type { FirstPartyViewAckRequest } from '@codebuff/common/ads/first-party-view-ack'
1012

1113
const msg = (over: Partial<ChatMessage>): ChatMessage => ({
1214
id: 'user-1',
@@ -71,3 +73,76 @@ describe('claimAdImpression', () => {
7173
expect(fired).toEqual(new Set(['imp-1', 'imp-2']))
7274
})
7375
})
76+
77+
describe('dispatchFirstPartyViewAcknowledgement', () => {
78+
const request: Omit<FirstPartyViewAckRequest, 'onAttempt'> = {
79+
token: 'opaque-imp-url',
80+
url: 'https://app.codebuff.com/api/v1/ads/impression',
81+
init: {
82+
method: 'POST',
83+
headers: {
84+
Authorization: 'Bearer cli-token',
85+
'Content-Type': 'application/json',
86+
},
87+
body: JSON.stringify({ impUrl: 'opaque-imp-url' }),
88+
},
89+
surface: 'waiting_room',
90+
placementId: 'waiting-room-1',
91+
clientFamily: 'cli',
92+
}
93+
94+
test('uses shared acknowledgement with immutable bearer request/context for first-party ads', () => {
95+
const calls: FirstPartyViewAckRequest[] = []
96+
const telemetry: unknown[] = []
97+
const dispatched = dispatchFirstPartyViewAcknowledgement(
98+
'first_party',
99+
request,
100+
(event) => telemetry.push(event),
101+
((params: FirstPartyViewAckRequest) => {
102+
calls.push(params)
103+
params.onAttempt?.({
104+
surface: 'waiting_room',
105+
placement_id: 'waiting-room-1',
106+
outcome: 'accepted',
107+
attempt: 1,
108+
duration_ms: 4,
109+
client_family: 'cli',
110+
})
111+
return Promise.resolve()
112+
}) as typeof import('@codebuff/common/ads/first-party-view-ack').acknowledgeFirstPartyView,
113+
)
114+
expect(dispatched).toBe(true)
115+
expect(calls).toHaveLength(1)
116+
expect(calls[0]).toMatchObject({
117+
token: 'opaque-imp-url',
118+
surface: 'waiting_room',
119+
placementId: 'waiting-room-1',
120+
clientFamily: 'cli',
121+
})
122+
expect(calls[0]?.init).toMatchObject({
123+
method: 'POST',
124+
headers: { Authorization: 'Bearer cli-token' },
125+
})
126+
expect(telemetry).toHaveLength(1)
127+
})
128+
129+
test('leaves third-party impressions on the legacy path and caller dedupe remains impUrl based', () => {
130+
let calls = 0
131+
const acknowledge = (() => {
132+
calls++
133+
return Promise.resolve()
134+
}) as typeof import('@codebuff/common/ads/first-party-view-ack').acknowledgeFirstPartyView
135+
expect(
136+
dispatchFirstPartyViewAcknowledgement(
137+
'gravity',
138+
request,
139+
() => {},
140+
acknowledge,
141+
),
142+
).toBe(false)
143+
const fired = new Set<string>()
144+
expect(claimAdImpression(fired, 'opaque-imp-url')).toBe(true)
145+
expect(claimAdImpression(fired, 'opaque-imp-url')).toBe(false)
146+
expect(calls).toBe(0)
147+
})
148+
})

0 commit comments

Comments
 (0)