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
6 changes: 3 additions & 3 deletions packages/shared/src/components/Feed.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ function renderComponent(

mocks.forEach(mockGraphQL);
nock('http://localhost:3000')
.get('/v1/a?active=false&gdpr=0')
.get(`/v1/a?active=false&registered=${!!resolvedUser}&gdpr=0`)
.reply(200, [ad]);
const settingsContext: SettingsContextData = {
...baseSettingsContext,
Expand Down Expand Up @@ -2014,10 +2014,10 @@ const renderWithHighlightLayout = ({
// First ad page uses active=false, subsequent pages use active=true. Mock
// both up to a handful of refills so multi-ad scenarios don't run dry.
nock('http://localhost:3000')
.get('/v1/a?active=false&gdpr=0')
.get('/v1/a?active=false&registered=true&gdpr=0')
.reply(200, [ad]);
nock('http://localhost:3000')
.get('/v1/a?active=true&gdpr=0')
.get('/v1/a?active=true&registered=true&gdpr=0')
.times(10)
.reply(200, [ad]);

Expand Down
16 changes: 13 additions & 3 deletions packages/shared/src/features/monetization/useAdQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { fetchAdByPlacement, resolveAdFetchOptions } from '../../lib/ads';
import type { FetchAdByPlacementOptions } from '../../lib/ads';
import { featurePostBoostAds } from '../../lib/featureManagement';
import { useAdMacroContext } from './useAdMacroContext';
import { useAuthContext } from '../../contexts/AuthContext';

interface UseAdQueryOptions {
queryKey: QueryKey;
Expand All @@ -27,20 +28,29 @@ export const useAdQuery = ({
}: UseAdQueryOptions) => {
const boostsEnabled = useFeature(featurePostBoostAds);
const consent = useAdMacroContext(enabled) ?? undefined;
const { isAuthReady, isLoggedIn } = useAuthContext();
const registered = isAuthReady ? isLoggedIn : undefined;
const fetchOptions = useMemo(
() =>
resolveAdFetchOptions({
placement,
active,
boostsEnabled,
consent,
registered,
}),
[placement, active, boostsEnabled, consent],
[placement, active, boostsEnabled, consent, registered],
);

return useQuery<Ad | null>({
// consent fingerprint so ads refetch when the user answers the CMP banner
queryKey: [...queryKey, consent?.gdprApplies, consent?.consentString ?? ''],
// consent fingerprint so ads refetch when the user answers the CMP banner,
// and login state so signing in swaps an anonymous-only ad
queryKey: [
...queryKey,
consent?.gdprApplies,
consent?.consentString ?? '',
isLoggedIn,
],
queryFn: () => fetchAdByPlacement(fetchOptions),
enabled,
staleTime,
Expand Down
6 changes: 5 additions & 1 deletion packages/shared/src/features/monetization/useFetchAd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '../../lib/ads';
import { featurePostBoostAds } from '../../lib/featureManagement';
import { useAdMacroContext } from './useAdMacroContext';
import { useAuthContext } from '../../contexts/AuthContext';

interface UseFetchAds {
fetchAd: (params: {
Expand All @@ -19,6 +20,8 @@ interface UseFetchAds {
export const useFetchAd = (): UseFetchAds => {
const boostsEnabled = useFeature(featurePostBoostAds);
const consent = useAdMacroContext(true) ?? undefined;
const { isAuthReady, isLoggedIn } = useAuthContext();
const registered = isAuthReady ? isLoggedIn : undefined;

const fetchAdQuery: UseFetchAds['fetchAd'] = useCallback(
({ active, placement = AdPlacement.Feed }) => {
Expand All @@ -28,10 +31,11 @@ export const useFetchAd = (): UseFetchAds => {
active,
boostsEnabled,
consent,
registered,
}),
);
},
[boostsEnabled, consent],
[boostsEnabled, consent, registered],
);

return {
Expand Down
46 changes: 46 additions & 0 deletions packages/shared/src/lib/ads.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,52 @@ describe('ads', () => {
);
});

it.each([
{
placement: AdPlacement.Feed,
url: 'http://localhost:3000/v1/a?active=false&registered=true&gdpr=0',
registered: true,
},
{
placement: AdPlacement.PostSidebar,
url: 'http://localhost:3000/v1/a?active=false&registered=false&gdpr=0',
registered: false,
},
{
placement: AdPlacement.PostComment,
url: 'http://localhost:3000/v1/a/post?registered=false&gdpr=0',
registered: false,
},
{
placement: AdPlacement.SquadDirectory,
url: 'http://localhost:3000/v1/a/squads_directory?registered=true&gdpr=0',
registered: true,
},
])(
'should send registered=$registered for $placement',
async ({ placement, url, registered }) => {
await fetchAdByPlacement({
placement,
registered,
consent: { gdprApplies: false },
});

expect(global.fetch).toHaveBeenCalledWith(url, {
credentials: 'include',
});
},
);

it('should pass registered through every placement', () => {
Object.values(AdPlacement)
.filter((placement) => placement !== AdPlacement.SponsorStrip)
.forEach((placement) => {
expect(
resolveAdFetchOptions({ placement, registered: false }).registered,
).toBe(false);
});
});

it('should send gdpr=0 without consent strings outside GDPR scope', async () => {
await fetchAdByPlacement({
placement: AdPlacement.PostComment,
Expand Down
35 changes: 33 additions & 2 deletions packages/shared/src/lib/ads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,26 @@ export interface FetchAdByPlacementOptions {
allowPostBoost?: boolean;
allowSquadBoost?: boolean;
consent?: AdMacroContext;
/**
* Whether the viewer is signed in. Undefined while auth is still loading,
* so the ad server never gets a guess.
*/
registered?: boolean;
}

// Skadi matches any query param against ad set `REQUEST_METADATA` targeting,
// so campaigns can target `{"registered":"true"}` or `{"registered":"false"}`.
export const appendRegisteredParam = (
params: URLSearchParams,
registered?: boolean,
): URLSearchParams => {
if (typeof registered !== 'undefined') {
params.set('registered', registered ? 'true' : 'false');
}

return params;
};

// IAB-standard consent params; the ad server promotes them to typed request
// metadata, so names must match its extraction (gdpr, gdpr_consent,
// addtl_consent).
Expand Down Expand Up @@ -125,27 +143,31 @@ export const resolveAdFetchOptions = ({
active,
boostsEnabled = false,
consent,
registered,
}: {
placement?: AdPlacement;
active?: boolean;
boostsEnabled?: boolean;
consent?: AdMacroContext;
registered?: boolean;
}): FetchAdByPlacementOptions => {
switch (placement) {
case AdPlacement.PostComment:
return { placement, consent };
return { placement, consent, registered };
case AdPlacement.SquadDirectory:
return {
placement,
allowSquadBoost: true,
consent,
registered,
};
case AdPlacement.PostSidebar:
return {
placement,
active,
allowSquadBoost: boostsEnabled,
consent,
registered,
};
case AdPlacement.Feed:
default:
Expand All @@ -155,6 +177,7 @@ export const resolveAdFetchOptions = ({
allowPostBoost: boostsEnabled,
allowSquadBoost: boostsEnabled,
consent,
registered,
};
}
};
Expand All @@ -165,19 +188,25 @@ export const fetchAdByPlacement = async ({
allowPostBoost = false,
allowSquadBoost = false,
consent,
registered,
}: FetchAdByPlacementOptions): Promise<Ad | null> => {
switch (placement) {
case AdPlacement.PostComment:
return fetchAdRequest({
path: '/v1/a/post',
params: appendConsentParams(new URLSearchParams(), consent),
params: appendConsentParams(
appendRegisteredParam(new URLSearchParams(), registered),
consent,
),
});
case AdPlacement.SquadDirectory: {
const params = new URLSearchParams();
if (allowSquadBoost) {
params.set('allow_squad_boost', 'true');
}

appendRegisteredParam(params, registered);

return fetchAdRequest({
path: '/v1/a/squads_directory',
params: appendConsentParams(params, consent),
Expand All @@ -198,6 +227,8 @@ export const fetchAdByPlacement = async ({
params.append('allow_squad_boost', 'true');
}

appendRegisteredParam(params, registered);

return fetchAdRequest({
path: '/v1/a',
params: appendConsentParams(params, consent),
Expand Down
2 changes: 1 addition & 1 deletion packages/webapp/__tests__/BookmarksPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ function renderComponent(

mocks.forEach(mockGraphQL);
nock('http://localhost:3000')
.get('/v1/a?active=false&gdpr=0')
.get('/v1/a?active=false&registered=true&gdpr=0')
.reply(200, [ad]);

return render(
Expand Down
2 changes: 1 addition & 1 deletion packages/webapp/__tests__/MyFeedPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ function renderComponent(

mocks.forEach(mockGraphQL);
nock('http://localhost:3000')
.get('/v1/a?active=false&gdpr=0')
.get('/v1/a?active=false&registered=true&gdpr=0')
.reply(200, [ad]);
return render(
<TestBootProvider
Expand Down
2 changes: 1 addition & 1 deletion packages/webapp/__tests__/SourcePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ const renderComponent = (

mocks.forEach(mockGraphQL);
nock('http://localhost:3000')
.get('/v1/a?active=false&gdpr=0')
.get(`/v1/a?active=false&registered=${!!user}&gdpr=0`)
.reply(200, [ad]);
return render(
<QueryClientProvider client={client}>
Expand Down
2 changes: 1 addition & 1 deletion packages/webapp/__tests__/TagPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ const renderComponent = (
(mocks ?? [createFeedMock(), createTagsSettingsMock()]).forEach(mockGraphQL);
mockGraphQL(createArchiveIndexMock());
nock('http://localhost:3000')
.get('/v1/a?active=false&gdpr=0')
.get(`/v1/a?active=false&registered=${!!user}&gdpr=0`)
.reply(200, [ad]);
const settingsContext: SettingsContextData = {
spaciness: 'eco',
Expand Down
Loading