Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { AuthContextData } from '../../../contexts/AuthContext';
import AuthContext from '../../../contexts/AuthContext';
import type { AdSlots } from '../../../features/monetization/kueez';
import { useFeature } from '../../GrowthBookProvider';
import { featureOrganicAds } from '../../../lib/featureManagement';
import { PHONE_TOP_AD_HEIGHT_VAR, PhoneTopAdStrip } from './PhoneTopAdStrip';
import { ORGANIC_SLOT, READ_SLOT } from './slots';

Expand Down Expand Up @@ -55,7 +56,9 @@ beforeEach(() => {

unobserve = jest.fn();
} as unknown as typeof ResizeObserver;
mockUseFeature.mockImplementation((feature) => feature.defaultValue);
mockUseFeature.mockImplementation((feature) =>
feature === featureOrganicAds ? true : feature.defaultValue,
);
mockSlotMaps.ORGANIC_AD_SLOTS = {
[ORGANIC_SLOT.topLeaderboardPhone]: { sizes: [[320, 50]] },
};
Expand Down
34 changes: 29 additions & 5 deletions packages/shared/src/components/post/read/ReadAdSlot.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ import {
renderPrebidBid,
requestKueezBid,
} from '../../../features/monetization/prebid';
import { featureReadAds } from '../../../lib/featureManagement';
import {
featureOrganicAds,
featureReadAds,
} from '../../../lib/featureManagement';
import { useFeature } from '../../GrowthBookProvider';
import { ORGANIC_SLOT } from './slots';

Expand Down Expand Up @@ -69,7 +72,7 @@ const mockRequestBid = jest.mocked(requestKueezBid);
const mockConfigurePrebid = jest.mocked(configurePrebid);
const mockRenderBid = jest.mocked(renderPrebidBid);

const flags = { read: true };
const flags = { read: true, organic: true };

const BID: PrebidBid = {
adId: 'ad-1',
Expand Down Expand Up @@ -141,16 +144,21 @@ beforeEach(() => {
window.history.replaceState({ key: expect.getState().currentTestName }, '');
mockConstants.isDevelopment = false;
flags.read = true;
flags.organic = true;
mockSlotMaps.READ_AD_SLOTS = {};
mockSlotMaps.ORGANIC_AD_SLOTS = {};
leaveAuctionPending();
mockRenderBid.mockImplementation((container: HTMLElement) => {
container.appendChild(document.createElement('iframe'));
return true;
});
mockUseFeature.mockImplementation((feature) =>
feature === featureReadAds ? flags.read : feature.defaultValue,
);
mockUseFeature.mockImplementation((feature) => {
if (feature === featureReadAds) {
return flags.read;
}

return feature === featureOrganicAds ? flags.organic : feature.defaultValue;
});
});

describe('ReadAdSlot', () => {
Expand Down Expand Up @@ -410,6 +418,22 @@ describe('ReadAdSlot on the organic surface', () => {
expect(container).toBeEmptyDOMElement();
});

it('goes dark when the organic_ads kill switch is off', () => {
flags.organic = false;
setOrganicSlots(organicFixture);
const { container } = render(
<ReadAdSlot
surface="organic"
slot={ORGANIC_SLOT.topLeaderboard}
format={ReadAdFormat.Leaderboard}
eager
/>,
);

expect(container).toBeEmptyDOMElement();
expect(mockRequestBid).not.toHaveBeenCalled();
});

it('ignores the read flag and map entirely', () => {
setSlots({ [ORGANIC_SLOT.topLeaderboard]: {} });
const { container } = render(
Expand Down
3 changes: 2 additions & 1 deletion packages/shared/src/components/post/read/ReadAdSlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ function OrganicSurfaceAdSlot(props: ReadAdSlotProps): ReactElement | null {
/**
* A programmatic ad slot. Live only while its surface's hook says so (the
* /read template sits behind the read_ads kill switch, the organic post page
* is anonymous-only) AND its hardcoded map (slots.ts) lists this slot number.
* behind organic_ads, both anonymous-only) AND its hardcoded map (slots.ts)
* lists this slot number.
* Everything else collapses to nothing, so visitors get a clean page. The
* dashed density-review placeholder only ever appears in local development
* builds of the /read template.
Expand Down
11 changes: 8 additions & 3 deletions packages/shared/src/components/post/read/useReadAdSlots.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { useContext } from 'react';
import { featureReadAds } from '../../../lib/featureManagement';
import {
featureOrganicAds,
featureReadAds,
} from '../../../lib/featureManagement';
import AuthContext from '../../../contexts/AuthContext';
import { useActivePostContext } from '../../../contexts/ActivePostContext';
import type { Source } from '../../../graphql/sources';
Expand Down Expand Up @@ -59,11 +62,13 @@ export const useReadAdSlots = (post?: AdSlotsPost): AdSlots => {
/**
* The organic post page's slots, permanent since the post_adsense experiment
* won: anonymous visitors only, so any logged-in user (member or Plus) never
* sees programmatic ads on their post pages.
* sees programmatic ads on their post pages. The `organic_ads` kill switch
* turns them all off.
*/
export const useOrganicAdSlots = (post?: AdSlotsPost): AdSlots => {
const isAnonymous = useIsAnonymous();
const enabled = useFeature(featureOrganicAds);
const isAdFree = useIsAdFreePost(post);

return isAnonymous && !isAdFree ? ORGANIC_AD_SLOTS : NO_SLOTS;
return enabled && isAnonymous && !isAdFree ? ORGANIC_AD_SLOTS : NO_SLOTS;
};
4 changes: 4 additions & 0 deletions packages/shared/src/lib/featureManagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,10 @@ export const featurePlusSale = new Feature<PlusSaleConfig>(
// and an ISR revalidation cycle. Never ramp or target with this flag.
export const featureReadAds = new Feature('read_ads', true);

// Kill switch for the post page's programmatic (Kueez) slots, off for now.
// Turning it on in GrowthBook brings them back without a deploy.
export const featureOrganicAds = new Feature('organic_ads', false);

// Kill switch for the batched GraphQL transport (`graphql/batch.ts`). Off is
// the control: the API only accepts batched bodies once its own change ships.
export const featureGqlBatching = new Feature('gql_batching', false);
Expand Down
15 changes: 15 additions & 0 deletions packages/webapp/__tests__/PostPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,21 @@ jest.mock('@dailydotdev/shared/src/hooks/useConditionalFeature', () => ({
},
}));

// The organic ad units sit behind the organic_ads kill switch, off by default.
jest.mock('@dailydotdev/shared/src/components/GrowthBookProvider', () => {
const actual = jest.requireActual(
'@dailydotdev/shared/src/components/GrowthBookProvider',
);

return {
...actual,
useFeature: (feature: { id: string }) => {
const value = actual.useFeature(feature);
return feature.id === 'organic_ads' ? true : value;
},
};
});

// The ad-teardown effect subscribes to router events on anonymous renders,
// so the mock has to carry the emitter surface.
const routerEvents = { on: jest.fn(), off: jest.fn(), emit: jest.fn() };
Expand Down
Loading