Skip to content

Commit 2cd4966

Browse files
Sync public snapshot from freebuff-private
Source: CodebuffAI/freebuff-private@f5d374366a16dd9dc17e2158bb613a27bd41d568
1 parent 6a0445f commit 2cd4966

4 files changed

Lines changed: 129 additions & 5 deletions

File tree

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ import {
2020
PLACEMENT_SLOTS,
2121
SPONSOR_BREAK_CREATIVE_LIMITS,
2222
SPONSOR_BREAK_FORMATS,
23+
INTERRUPTING_BREAK_FORMATS,
24+
isInterruptingBreakFormat,
25+
isInterruptingBreakPlacement,
26+
isSponsorBreakFormat,
2327
SPONSOR_BREAK_HERO_SPEC,
2428
TRACKED_LINK_PLACEMENT_ID,
2529
isSponsorBreakPlacement,
@@ -271,6 +275,33 @@ describe('copy and configuration', () => {
271275
])
272276
})
273277

278+
it('caps the two INTERRUPTING breaks and never Showcase (COD-455)', () => {
279+
// Two predicates, two questions. The creative rules (hero required, no
280+
// text-only house floor) are the same for all three; the daily cap, the
281+
// min-session floor and the fail-closed-with-no-Redis are the price of
282+
// TAKING THE SCREEN AWAY, which Showcase does not do -- it is the same
283+
// above-composer slot drawn taller, on the same rotation as the banner it
284+
// is being compared against. Capping it at one a day would leave the
285+
// treatment arm byte-identical to control for all but one rotation, so the
286+
// format test would measure nothing.
287+
expect(INTERRUPTING_BREAK_FORMATS).toEqual(['spotlight', 'intermission'])
288+
expect(isInterruptingBreakFormat('showcase')).toBe(false)
289+
expect(isSponsorBreakFormat('showcase')).toBe(true)
290+
291+
const interrupting = PLACEMENT_SLOTS.filter((slot) =>
292+
isInterruptingBreakPlacement(slot.id),
293+
).map((slot) => slot.id)
294+
expect(interrupting).toEqual(['Desktop-Spotlight', 'Desktop-Intermission'])
295+
expect(isInterruptingBreakPlacement('Desktop-Showcase')).toBe(false)
296+
297+
// Still the conservative fallback for an unknown id, in both directions.
298+
expect(isInterruptingBreakPlacement('some-future-grain')).toBe(false)
299+
// Every interrupting format is a break; the reverse does not hold.
300+
for (const format of INTERRUPTING_BREAK_FORMATS) {
301+
expect([format, isSponsorBreakFormat(format)]).toEqual([format, true])
302+
}
303+
})
304+
274305
it('answers inline for an id it does not know, never undefined', () => {
275306
// The conservative direction. An unknown id guessed as a break would turn
276307
// one typo into a full-screen interruption; guessed as inline it renders

common/src/ads/__tests__/sponsor-break-events.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import {
77
buildSponsorBreakEvent,
88
clampDwellMs,
99
isSponsorBreakCloseMethod,
10+
isSponsorBreakEvent,
1011
readDwellMs,
1112
SPONSOR_BREAK_ACCIDENTAL_CLICK_MS,
1213
SPONSOR_BREAK_CLOSE_METHODS,
1314
SPONSOR_BREAK_DWELL_MAX_MS,
15+
SPONSOR_BREAK_EVENTS,
1416
} from '../sponsor-break-events'
1517

1618
import {
@@ -247,3 +249,27 @@ describe('the Axiom allowlist', () => {
247249
}
248250
})
249251
})
252+
253+
describe('isSponsorBreakEvent', () => {
254+
test('admits the three names and nothing else', () => {
255+
// A CLOSED set at the boundary, for the same reason the close methods
256+
// are: these become the `event` field every break readout groups on, and
257+
// a client that could invent one would widen that field's cardinality
258+
// with rows nothing queries.
259+
for (const event of SPONSOR_BREAK_EVENTS) {
260+
expect(isSponsorBreakEvent(event)).toBe(true)
261+
}
262+
for (const value of [
263+
'ads.break_exploded',
264+
'ads.fetch_completed',
265+
'',
266+
' ads.break_shown',
267+
null,
268+
undefined,
269+
42,
270+
{ event: 'ads.break_shown' },
271+
]) {
272+
expect(isSponsorBreakEvent(value)).toBe(false)
273+
}
274+
})
275+
})

common/src/ads/sponsor-break-events.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,23 @@ export const SPONSOR_BREAK_EVENTS = [
4646
] as const
4747
export type SponsorBreakEvent = (typeof SPONSOR_BREAK_EVENTS)[number]
4848

49+
/**
50+
* Whether an untrusted value names one of the three break events.
51+
*
52+
* A CLOSED set at the boundary, for the same reason
53+
* {@link isSponsorBreakCloseMethod} is: these names become the `event` field
54+
* every break readout groups on, and a client that could invent one would
55+
* widen that field's cardinality with rows nothing queries.
56+
*/
57+
export function isSponsorBreakEvent(
58+
value: unknown,
59+
): value is SponsorBreakEvent {
60+
return (
61+
typeof value === 'string' &&
62+
(SPONSOR_BREAK_EVENTS as readonly string[]).includes(value)
63+
)
64+
}
65+
4966
/**
5067
* How a break ended. A CLOSED vocabulary, like the census codes: these are a
5168
* histogram dimension and an open set would let a new renderer widen the
@@ -165,11 +182,18 @@ export interface SponsorBreakEventPayload {
165182
timer_ms?: number
166183
timer_completed?: boolean
167184
/**
168-
* SPOTLIGHT'S DISMISS LOCK (COD-454): how long the ways OUT were held, in
169-
* milliseconds of VISIBLE time. Separate from `timer_ms` because the two
170-
* hold different things -- the countdown gates the whole card, the lock only
171-
* the dismissals -- and pooling them would make the readout unable to say
172-
* which format a row came from without joining on `format`.
185+
* THE DISMISS LOCK: how long the ways OUT were held, in milliseconds of
186+
* VISIBLE time. Spotlight (COD-454) and Showcase (COD-455) both hold their
187+
* dismissals, so this is ONE field across the formats and `format` is what
188+
* separates them.
189+
*
190+
* Separate from `timer_ms` because the two hold different things -- the
191+
* countdown gates the whole card, the lock only the dismissals -- and
192+
* pooling them would make the readout unable to say which format a row came
193+
* from without joining on `format`. Concretely: Intermission's `timer_ms`
194+
* counts down to a Continue button the person is waiting ON, while this
195+
* counts down to a control they may never look for, on a card that is fully
196+
* clickable and fully ignorable throughout.
173197
*/
174198
dismiss_lock_ms?: number
175199
/**

common/src/constants/freebuff-placements.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,40 @@ export function isSponsorBreakFormat(format: PlacementFormat): boolean {
120120
return format !== 'inline'
121121
}
122122

123+
/**
124+
* The formats that INTERRUPT, which is a strictly narrower question than
125+
* {@link isSponsorBreakFormat} and the only one the daily cap may ask.
126+
*
127+
* The two predicates exist because COD-453 conflated two properties that
128+
* happen to coincide for two of the three break formats:
129+
*
130+
* - WHAT A CREATIVE MUST LOOK LIKE. Image-led, hero required, tight copy, and
131+
* never the text-only house floor. True of all three, which is what
132+
* {@link isSponsorBreakFormat} answers, and it stays the predicate every
133+
* creative-shape rule reads.
134+
* - WHAT IT COSTS THE READER. `spotlight` and `intermission` take the screen
135+
* away from the work in front of them, which is why they are rationed: one
136+
* per UTC day, never inside the first ten minutes of a session, and never at
137+
* all on a deployment with no Redis to count them. `showcase` takes nothing
138+
* away -- it is the SAME slot above the composer that already holds a
139+
* sponsored banner every sixty seconds, drawn taller (COD-455).
140+
*
141+
* Capping Showcase makes its own experiment unrunnable rather than merely
142+
* conservative: the treatment arm would see the tall card for at most one
143+
* rotation a day and be byte-identical to control for the rest of it, so the
144+
* format test would measure nothing and the null result would read as "the
145+
* format made no difference". A frequency cap on a unit whose frequency is
146+
* fixed by the arm it is being compared against is not a safety margin.
147+
*/
148+
export const INTERRUPTING_BREAK_FORMATS: readonly PlacementFormat[] = [
149+
'spotlight',
150+
'intermission',
151+
]
152+
153+
export function isInterruptingBreakFormat(format: PlacementFormat): boolean {
154+
return INTERRUPTING_BREAK_FORMATS.includes(format)
155+
}
156+
123157
/**
124158
* Placements an advertiser can buy.
125159
*
@@ -301,6 +335,15 @@ export function isSponsorBreakPlacement(placementId: string): boolean {
301335
return isSponsorBreakFormat(placementFormat(placementId))
302336
}
303337

338+
/**
339+
* Whether this placement id INTERRUPTS, and is therefore subject to the daily
340+
* cap. See {@link INTERRUPTING_BREAK_FORMATS} for why this is not the same
341+
* question as {@link isSponsorBreakPlacement}.
342+
*/
343+
export function isInterruptingBreakPlacement(placementId: string): boolean {
344+
return isInterruptingBreakFormat(placementFormat(placementId))
345+
}
346+
304347
/**
305348
* What a creative must look like to fill a SPONSOR BREAK.
306349
*

0 commit comments

Comments
 (0)