Skip to content

Commit 39d25bd

Browse files
Sync public snapshot from freebuff-private
Source: CodebuffAI/freebuff-private@5527cbaec64bfa87b28bf0d6cf2b4a29fa2dc393
1 parent 887e968 commit 39d25bd

6 files changed

Lines changed: 227 additions & 29 deletions

File tree

common/src/__tests__/freebuff-placements.test.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
PRIMARY_METRICS,
1616
UNDERSPEND_COPY,
1717
UNDERSPEND_REASONS,
18+
avgCpa,
1819
avgCpc,
1920
costPerActivation,
2021
ctr,
@@ -47,24 +48,24 @@ describe('derived metrics', () => {
4748
totals({
4849
impressionsServed: 2_000,
4950
impressionsViewed: 1_000,
50-
billableClicks: 10,
51+
clicks: 10,
5152
}),
5253
)
5354

5455
expect(value).toBe(0.01)
5556
})
5657

57-
it('counts billable clicks in CTR, not raw clicks', () => {
58-
// The invoice bills billable clicks, so the dashboard's rate has to use
59-
// the same numerator or the two disagree in front of the advertiser.
58+
it('keeps CTR diagnostic and counts raw clicks, not only billed clicks', () => {
59+
// CPA does not bill a click. CTR must remain a diagnostic of actual click
60+
// behaviour across both billing models rather than a disguised invoice rate.
6061
const value = ctr(
6162
totals({ impressionsViewed: 1_000, clicks: 20, billableClicks: 10 }),
6263
)
6364

64-
expect(value).toBe(0.01)
65+
expect(value).toBe(0.02)
6566
})
6667

67-
it('computes cost per activation, average CPC and eCPM in dollars', () => {
68+
it('computes CPA, CPC and eCPM in dollars', () => {
6869
const measured = totals({
6970
activations: 4,
7071
impressionsViewed: 10_000,
@@ -74,6 +75,7 @@ describe('derived metrics', () => {
7475
})
7576

7677
expect(costPerActivation(measured)).toBe(15)
78+
expect(avgCpa(measured)).toBe(15)
7779
expect(avgCpc(measured)).toBe(1.5)
7880
expect(ecpm(measured)).toBe(6)
7981
expect(spendUsd(measured)).toBe(60)
@@ -201,10 +203,11 @@ describe('copy and configuration', () => {
201203
// transcript alone is twice the waiting room's slot count.
202204
const bySurface = (surface: string) =>
203205
PLACEMENT_SLOTS.filter((slot) => slot.surface === surface).length
204-
// CLI transcript + Desktop inline. NOT the eight `CLI-Chat-Inline-N` ids:
206+
// CLI transcript + its legacy single slot + both Desktop units. NOT the
207+
// eight `CLI-Chat-Inline-N` ids:
205208
// no shipping client requests those, so selling them would be selling a
206209
// decaying legacy path.
207-
expect(bySurface('cli_chat')).toBe(2)
210+
expect(bySurface('cli_chat')).toBe(4)
208211
expect(bySurface('waiting_room')).toBe(4)
209212
expect(bySurface('freebuff_web_chat')).toBe(2)
210213
expect(bySurface('chat_assistant')).toBe(1)
@@ -227,6 +230,16 @@ describe('copy and configuration', () => {
227230
}
228231
})
229232

233+
it('exposes both CPA and CPC primary facts for model-aware dashboards', () => {
234+
expect(PRIMARY_METRICS).toEqual([
235+
'billableClicks',
236+
'activations',
237+
'spend',
238+
'avgCpc',
239+
'avgCpa',
240+
])
241+
})
242+
230243
it('never labels impressions as a purchasable unit', () => {
231244
// eCPM is a derived yield figure for comparison against CPM inventory the
232245
// advertiser already buys. We do not sell impressions.

common/src/constants/freebuff-placements.ts

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ import type { AdCampaignStatus } from './freebuff-ads'
2626
* `off`), read by `placementsAudience()` in
2727
* `freebuff/web/src/server/advertisers/placements/access.ts`. What survives
2828
* here is the implementation-state assertion the knob is set against. Delivery
29-
* rollups are intentionally honest zeros until aggregation lands; they are not
30-
* fixtures.
29+
* rollups expose coverage alongside delivery; an uncovered day is unknown,
30+
* not a zero and never fixture data.
3131
*/
3232
export const PLACEMENTS_CONSOLE_ENABLED = true
3333

@@ -88,9 +88,10 @@ export const ATTRIBUTION_WINDOW_COPY = `Activation counts within ${ACTIVATION_AT
8888
* exclusivity term. That term does not exist, so chat is sellable -- and it is
8989
* where nearly all the volume is.
9090
*
91-
* `Single-Ad-Unit-1` is deliberately absent: it serves callers that named no
92-
* surface, and first-party targeting is expressed in surfaces, so a creative
93-
* could never match it.
91+
* `Single-Ad-Unit-1` and `Desktop-Below-Chat` are live legacy slots as well as
92+
* the primary inline units. They remain sellable until their clients retire;
93+
* leaving either out would route an advertiser's inventory straight past a
94+
* rendered surface.
9495
*/
9596
export const PLACEMENT_SLOTS = [
9697
{ id: 'waiting-room-1', surface: 'waiting_room', available: true },
@@ -101,6 +102,8 @@ export const PLACEMENT_SLOTS = [
101102
{ id: 'CLI-Chat-Inline', surface: 'cli_chat', available: true },
102103
// Desktop's inline slot -- the largest single placement by fill volume.
103104
{ id: 'Desktop-Inline-Chat', surface: 'cli_chat', available: true },
105+
{ id: 'Desktop-Below-Chat', surface: 'cli_chat', available: true },
106+
{ id: 'Single-Ad-Unit-1', surface: 'cli_chat', available: true },
104107
{
105108
id: 'Web-Chat-After-User-Message',
106109
surface: 'freebuff_web_chat',
@@ -127,32 +130,39 @@ export const PLACEMENT_SLOTS = [
127130
* network already ships.
128131
*/
129132
export const PRIMARY_METRICS = [
133+
'billableClicks',
130134
'activations',
131-
'costPerActivation',
132135
'spend',
136+
'avgCpc',
137+
'avgCpa',
133138
] as const
134139
export const DIAGNOSTIC_METRICS = [
135140
'impressions',
136141
'clicks',
137-
'billableClicks',
138142
'ctr',
139-
'avgCpc',
140143
'ecpm',
141144
] as const
142145

143146
export type PrimaryMetric = (typeof PRIMARY_METRICS)[number]
144147
export type DiagnosticMetric = (typeof DIAGNOSTIC_METRICS)[number]
145-
export type PlacementMetric = PrimaryMetric | DiagnosticMetric
148+
/** Legacy conversion labels remain addressable while conversion reporting is
149+
* intentionally hidden from the CPC MVP UI. */
150+
export type PlacementMetric =
151+
| PrimaryMetric
152+
| DiagnosticMetric
153+
| 'activations'
154+
| 'costPerActivation'
146155

147156
export const PLACEMENT_METRIC_LABELS: Record<PlacementMetric, string> = {
148-
activations: 'Activations',
149-
costPerActivation: 'Cost per activation',
157+
activations: 'Billable activations',
158+
costPerActivation: 'Avg CPA',
150159
spend: 'Spend',
151160
impressions: 'Impressions',
152161
clicks: 'Clicks',
153162
billableClicks: 'Billable',
154163
ctr: 'CTR',
155164
avgCpc: 'Avg CPC',
165+
avgCpa: 'Avg CPA',
156166
ecpm: 'Effective CPM',
157167
}
158168

@@ -195,10 +205,10 @@ function ratio(numerator: number, denominator: number): number | null {
195205
* a viewability claim we sell against.
196206
*/
197207
export function ctr(totals: PlacementTotals): number | null {
198-
return ratio(totals.billableClicks, totals.impressionsViewed)
208+
return ratio(totals.clicks, totals.impressionsViewed)
199209
}
200210

201-
/** Spend per activation — the number an advertiser actually judges us on. */
211+
/** Conversion attribution is not part of the CPC MVP. Kept for broad types. */
202212
export function costPerActivation(totals: PlacementTotals): number | null {
203213
const perActivation = ratio(totals.spendCents, totals.activations)
204214
return perActivation === null ? null : perActivation / 100
@@ -210,6 +220,11 @@ export function avgCpc(totals: PlacementTotals): number | null {
210220
return perClick === null ? null : perClick / 100
211221
}
212222

223+
/** Spend per verified, payable activation. */
224+
export function avgCpa(totals: PlacementTotals): number | null {
225+
return costPerActivation(totals)
226+
}
227+
213228
/**
214229
* Effective CPM — yield per thousand viewed impressions.
215230
*

common/src/util/__tests__/ad-experiment.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, test } from 'bun:test'
22

33
import {
4+
DEFAULT_FIRST_PARTY_BACKFILL,
45
DEFAULT_FIRST_PARTY_PRIMARY_PERCENT,
56
IMPREZIA_EXPERIMENT_PERCENT,
67
adExperimentArmForUser,
@@ -38,6 +39,17 @@ describe('imprezia experiment arm', () => {
3839
})
3940

4041
describe('first-party request routing', () => {
42+
test('keeps an absent runtime configuration on the paid-network-only path', () => {
43+
expect(DEFAULT_FIRST_PARTY_PRIMARY_PERCENT).toBe(0)
44+
expect(DEFAULT_FIRST_PARTY_BACKFILL).toBe(false)
45+
expect(
46+
firstPartyAdRouteForUser('user-42', {
47+
primaryPercent: DEFAULT_FIRST_PARTY_PRIMARY_PERCENT,
48+
backfill: DEFAULT_FIRST_PARTY_BACKFILL,
49+
}),
50+
).toBe('paid_network_only')
51+
})
52+
4153
test('never routes a missing user id into first-party inventory', () => {
4254
for (const id of [null, undefined, '']) {
4355
expect(

common/src/util/__tests__/axiom-only-log.test.ts

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { describe, expect, test } from 'bun:test'
22

33
import {
44
ADS_FETCH_COMPLETED_EVENT,
5+
ADS_FIRST_PARTY_DECISION_EVENT,
6+
ADS_FIRST_PARTY_SETTLEMENT_EVENT,
57
CONTEXT_PRUNING_COMPLETED_EVENT,
68
getAxiomOnlyLogEvent,
79
STREAM_RECOVERY_EVENT,
@@ -61,7 +63,10 @@ describe('getAxiomOnlyLogEvent', () => {
6163
}),
6264
).toBeNull()
6365
expect(
64-
getAxiomOnlyLogEvent({ prompt: 'must not be silently dropped' }, poisonEvent),
66+
getAxiomOnlyLogEvent(
67+
{ prompt: 'must not be silently dropped' },
68+
poisonEvent,
69+
),
6570
).toBeNull()
6671
}
6772
})
@@ -139,30 +144,119 @@ describe('getAxiomOnlyLogEvent', () => {
139144
})
140145
})
141146

142-
test('sanitizes ad-fetch metadata', () => {
147+
test('preserves bounded scalar ad-routing metadata and drops identifiers', () => {
143148
expect(
144149
getAxiomOnlyLogEvent({
145150
axiomEvent: ADS_FETCH_COMPLETED_EVENT,
146151
outcome: 'fill',
147152
requested_provider: 'gravity',
148-
served_provider: 'carbon',
153+
served_provider: 'first_party',
154+
attempted_provider_chain: 'gravity>first_party',
155+
experiment_arm: 'treatment',
156+
first_party_route: 'gravity_then_first_party',
157+
first_party_primary_percent: 10,
158+
first_party_backfill_enabled: true,
159+
selection_reason: 'gravity_no_fill_backfill',
149160
ad_count: 1,
161+
surface: 'cli',
150162
placement_id: 'CLI-Chat-Inline',
151163
duration_ms: 42,
152164
client_ua_product: 'freebuff-cli',
165+
client_ua_version: '1.2.3',
166+
// Arrays must be producer-encoded as attempted_provider_chain.
153167
attempted_providers: ['gravity', 'carbon'],
168+
// High-cardinality identifiers and content do not reach Axiom.
169+
userId: 'user-123',
170+
advertiser_id: 'advertiser-123',
171+
chat_session_id: 'session-123',
172+
campaign_ids: ['campaign-123'],
173+
creative_ids: ['creative-123'],
174+
ad_url: 'https://example.com/secret',
154175
messages: [{ role: 'user', content: 'secret' }],
155176
}),
156177
).toEqual({
157178
event: ADS_FETCH_COMPLETED_EVENT,
158179
data: {
159180
outcome: 'fill',
160181
requested_provider: 'gravity',
161-
served_provider: 'carbon',
182+
served_provider: 'first_party',
183+
attempted_provider_chain: 'gravity>first_party',
184+
experiment_arm: 'treatment',
185+
first_party_route: 'gravity_then_first_party',
186+
first_party_primary_percent: 10,
187+
first_party_backfill_enabled: true,
188+
selection_reason: 'gravity_no_fill_backfill',
162189
ad_count: 1,
190+
surface: 'cli',
163191
placement_id: 'CLI-Chat-Inline',
164192
duration_ms: 42,
165193
client_ua_product: 'freebuff-cli',
194+
client_ua_version: '1.2.3',
195+
},
196+
})
197+
})
198+
199+
test('names and sanitizes first-party selection telemetry', () => {
200+
expect(
201+
getAxiomOnlyLogEvent({
202+
axiomEvent: ADS_FIRST_PARTY_DECISION_EVENT,
203+
outcome: 'no_fill',
204+
no_fill_reason: 'no_eligible_campaign',
205+
placement_count: 2,
206+
candidate_count: 4,
207+
candidate_load_ms: 8,
208+
frequency_status: 'unavailable',
209+
frequency_unavailable_cause: 'timeout',
210+
frequency_reservation_ms: 75,
211+
duration_ms: 11,
212+
campaign_ids: ['campaign-123'],
213+
creative_ids: ['creative-123'],
214+
placement_ids: ['CLI-Chat-Inline'],
215+
userId: 'user-123',
216+
reasons: ['budget_exhausted'],
217+
nested: { private: true },
218+
}),
219+
).toEqual({
220+
event: ADS_FIRST_PARTY_DECISION_EVENT,
221+
data: {
222+
outcome: 'no_fill',
223+
no_fill_reason: 'no_eligible_campaign',
224+
placement_count: 2,
225+
candidate_count: 4,
226+
candidate_load_ms: 8,
227+
frequency_status: 'unavailable',
228+
frequency_unavailable_cause: 'timeout',
229+
frequency_reservation_ms: 75,
230+
duration_ms: 11,
231+
},
232+
})
233+
})
234+
235+
test('names and sanitizes first-party settlement telemetry', () => {
236+
expect(
237+
getAxiomOnlyLogEvent(
238+
{
239+
billing_model: 'cpa',
240+
settlement_status: 'charged',
241+
amount_cents: 75,
242+
balance_cents: 925,
243+
duration_ms: 6,
244+
userId: 'user-123',
245+
advertiser_id: 'advertiser-123',
246+
campaign_id: 'campaign-123',
247+
ad_impression_id: 'impression-123',
248+
error: { message: 'private failure detail' },
249+
},
250+
ADS_FIRST_PARTY_SETTLEMENT_EVENT,
251+
),
252+
).toEqual({
253+
event: ADS_FIRST_PARTY_SETTLEMENT_EVENT,
254+
data: {
255+
billing_model: 'cpa',
256+
settlement_status: 'charged',
257+
amount_cents: 75,
258+
balance_cents: 925,
259+
duration_ms: 6,
166260
},
167261
})
168262
})

common/src/util/ad-experiment.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,12 @@ export const IMPREZIA_EXPERIMENT_PERCENT = 10
3333
export const FIRST_PARTY_ROUTING_EXPERIMENT =
3434
'ads_first_party_before_paid_networks_2026_08'
3535

36-
/** Unset env values preserve the initial launch allocation. */
37-
export const DEFAULT_FIRST_PARTY_PRIMARY_PERCENT = 10
38-
export const DEFAULT_FIRST_PARTY_BACKFILL = true
36+
/**
37+
* An absent runtime knob is a dark deploy. Allocation is deliberately opt-in:
38+
* a missing Infisical value must not take paid-network inventory.
39+
*/
40+
export const DEFAULT_FIRST_PARTY_PRIMARY_PERCENT = 0
41+
export const DEFAULT_FIRST_PARTY_BACKFILL = false
3942

4043
export type FirstPartyAdRoute =
4144
| 'paid_network_only'

0 commit comments

Comments
 (0)