Skip to content

Commit d6eb259

Browse files
committed
feat: add engagement ads support to boot
1 parent bdda803 commit d6eb259

6 files changed

Lines changed: 252 additions & 24 deletions

File tree

__tests__/boot.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ const BASE_BODY = {
112112
},
113113
exp: { f: 'enc', e: [], a: {} },
114114
geo: {},
115+
engagementCreatives: [],
115116
};
116117

117118
const LOGGED_IN_BODY = {
@@ -2236,3 +2237,124 @@ describe('boot profile completion', () => {
22362237
});
22372238
});
22382239
});
2240+
2241+
describe('engagement creatives', () => {
2242+
const GENERATION_ID = 'test-generation-id';
2243+
2244+
const skadiEngagementPayload = {
2245+
promoted_name: 'Test Brand',
2246+
promoted_body: 'Test body',
2247+
promoted_cta: 'Try now',
2248+
promoted_url: 'https://example.com',
2249+
promoted_logo_img: {
2250+
dark: 'https://example.com/logo-dark.png',
2251+
light: 'https://example.com/logo-light.png',
2252+
},
2253+
promoted_icon_img: {
2254+
dark: 'https://example.com/icon-dark.png',
2255+
light: 'https://example.com/icon-light.png',
2256+
},
2257+
promoted_gradient_start: { dark: '#FF0000', light: '#CC0000' },
2258+
promoted_gradient_end: { dark: '#0000FF', light: '#0000CC' },
2259+
tools: ['tool1', 'tool2'],
2260+
keywords: ['keyword1', 'keyword2'],
2261+
tags: ['tag1', 'tag2'],
2262+
};
2263+
2264+
const skadiResponse = {
2265+
generation_id: GENERATION_ID,
2266+
value: { engagement: skadiEngagementPayload },
2267+
};
2268+
2269+
const expectedCreative = {
2270+
...skadiEngagementPayload,
2271+
gen_id: GENERATION_ID,
2272+
};
2273+
2274+
afterEach(() => {
2275+
nock.cleanAll();
2276+
});
2277+
2278+
it('should not fetch engagement creatives when ega param is missing', async () => {
2279+
const scope = nock(process.env.SKADI_ORIGIN)
2280+
.post('/private')
2281+
.reply(200, skadiResponse);
2282+
2283+
const res = await request(app.server)
2284+
.get(BASE_PATH)
2285+
.set('User-Agent', TEST_UA)
2286+
.set('Cookie', await mockLoggedInCookie())
2287+
.expect(200);
2288+
2289+
expect(res.body.engagementCreatives).toEqual([]);
2290+
expect(scope.isDone()).toBe(false);
2291+
});
2292+
2293+
it('should return engagement creatives when ega=1 for logged in user', async () => {
2294+
nock(process.env.SKADI_ORIGIN)
2295+
.post('/private', {
2296+
placement: 'default_engagement',
2297+
metadata: { USERID: '1' },
2298+
})
2299+
.reply(200, skadiResponse);
2300+
2301+
const res = await request(app.server)
2302+
.get(`${BASE_PATH}?ega=1`)
2303+
.set('User-Agent', TEST_UA)
2304+
.set('Cookie', await mockLoggedInCookie())
2305+
.expect(200);
2306+
2307+
expect(res.body.engagementCreatives).toEqual([expectedCreative]);
2308+
});
2309+
2310+
it('should return engagement creatives when ega=1 for anonymous user', async () => {
2311+
nock(process.env.SKADI_ORIGIN).post('/private').reply(200, skadiResponse);
2312+
2313+
const res = await request(app.server)
2314+
.get(`${BASE_PATH}?ega=1`)
2315+
.set('User-Agent', TEST_UA)
2316+
.expect(200);
2317+
2318+
expect(res.body.engagementCreatives).toEqual([expectedCreative]);
2319+
});
2320+
2321+
it('should return empty array when skadi returns no generation_id', async () => {
2322+
nock(process.env.SKADI_ORIGIN)
2323+
.post('/private')
2324+
.reply(200, { value: { engagement: skadiEngagementPayload } });
2325+
2326+
const res = await request(app.server)
2327+
.get(`${BASE_PATH}?ega=1`)
2328+
.set('User-Agent', TEST_UA)
2329+
.set('Cookie', await mockLoggedInCookie())
2330+
.expect(200);
2331+
2332+
expect(res.body.engagementCreatives).toEqual([]);
2333+
});
2334+
2335+
it('should return empty array when skadi returns empty response', async () => {
2336+
nock(process.env.SKADI_ORIGIN).post('/private').reply(200, {});
2337+
2338+
const res = await request(app.server)
2339+
.get(`${BASE_PATH}?ega=1`)
2340+
.set('User-Agent', TEST_UA)
2341+
.set('Cookie', await mockLoggedInCookie())
2342+
.expect(200);
2343+
2344+
expect(res.body.engagementCreatives).toEqual([]);
2345+
});
2346+
2347+
it('should return empty array when skadi returns error', async () => {
2348+
nock(process.env.SKADI_ORIGIN)
2349+
.post('/private')
2350+
.reply(500, 'Internal Server Error');
2351+
2352+
const res = await request(app.server)
2353+
.get(`${BASE_PATH}?ega=1`)
2354+
.set('User-Agent', TEST_UA)
2355+
.set('Cookie', await mockLoggedInCookie())
2356+
.expect(200);
2357+
2358+
expect(res.body.engagementCreatives).toEqual([]);
2359+
});
2360+
});

src/integrations/garmr.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
Policy,
1111
retry,
1212
SamplingBreaker,
13+
timeout,
14+
TimeoutStrategy,
1315
wrap,
1416
} from 'cockatiel';
1517
import { logger } from '../logger';
@@ -55,6 +57,7 @@ export class GarmrService implements IGarmrService {
5557
breakerOpts,
5658
retryOpts,
5759
limits,
60+
timeoutMs,
5861
events,
5962
}: {
6063
service: string;
@@ -73,6 +76,7 @@ export class GarmrService implements IGarmrService {
7376
maxRequests: number;
7477
queuedRequests?: number;
7578
};
79+
timeoutMs?: number;
7680
events?: Partial<{
7781
onBreak?: (props: {
7882
event: FailureReason<Error>;
@@ -151,7 +155,17 @@ export class GarmrService implements IGarmrService {
151155
events?.onReset?.({ meta: instanceMeta });
152156
});
153157

154-
this.instance = wrap(retryPolicy, circuitBreakerPolicy, bulkheadPolicy);
158+
const policies: IPolicy[] = [
159+
retryPolicy,
160+
circuitBreakerPolicy,
161+
bulkheadPolicy,
162+
];
163+
164+
if (timeoutMs && timeoutMs > 0) {
165+
policies.push(timeout(timeoutMs, TimeoutStrategy.Cooperative));
166+
}
167+
168+
this.instance = wrap(...policies);
155169
}
156170

157171
execute<T>(

src/integrations/skadi/clients.ts

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { RequestInit } from 'node-fetch';
2-
import { ISkadiClient, SkadiResponse } from './types';
2+
import {
3+
ISkadiClient,
4+
SkadiResponse,
5+
type EngagementCreative,
6+
type SkadiAd,
7+
} from './types';
38
import { GarmrNoopService, IGarmrService, GarmrService } from '../garmr';
49
import { fetchOptions as globalFetchOptions } from '../../http';
510
import { fetchParse } from '../retry';
611
import { counters } from '../../telemetry';
712

8-
export class SkadiClient implements ISkadiClient {
13+
export class SkadiClient<TValue> implements ISkadiClient<TValue> {
914
private readonly fetchOptions: RequestInit;
1015
private readonly garmr: IGarmrService;
1116

@@ -30,8 +35,8 @@ export class SkadiClient implements ISkadiClient {
3035
metadata: {
3136
USERID: string;
3237
},
33-
): Promise<SkadiResponse> {
34-
return this.garmr.execute(() => {
38+
): Promise<SkadiResponse<TValue>> {
39+
return this.garmr.execute(({ signal }) => {
3540
return fetchParse(`${this.url}/private`, {
3641
...this.fetchOptions,
3742
method: 'POST',
@@ -42,6 +47,7 @@ export class SkadiClient implements ISkadiClient {
4247
placement,
4348
metadata,
4449
}),
50+
signal,
4551
});
4652
});
4753
}
@@ -86,9 +92,27 @@ const garmrSkadiPersonalizedDigestService = new GarmrService({
8692
},
8793
});
8894

89-
export const skadiPersonalizedDigestClient = new SkadiClient(
90-
process.env.SKADI_ORIGIN,
91-
{
92-
garmr: garmrSkadiPersonalizedDigestService,
95+
export const skadiPersonalizedDigestClient = new SkadiClient<{
96+
digest: SkadiAd;
97+
}>(process.env.SKADI_ORIGIN, {
98+
garmr: garmrSkadiPersonalizedDigestService,
99+
});
100+
101+
const garmrSkadiEngagementService = new GarmrService({
102+
service: `${SkadiClient.name}Engagement`,
103+
breakerOpts: {
104+
halfOpenAfter: 5 * 1000,
105+
threshold: 0.1,
106+
duration: 10 * 1000,
93107
},
94-
);
108+
retryOpts: {
109+
maxAttempts: 0,
110+
},
111+
timeoutMs: 600,
112+
});
113+
114+
export const skadiEngagementClient = new SkadiClient<{
115+
engagement: EngagementCreative;
116+
}>(process.env.SKADI_ORIGIN, {
117+
garmr: garmrSkadiEngagementService,
118+
});

src/integrations/skadi/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
export * from './types';
2-
export { SkadiClient, skadiPersonalizedDigestClient } from './clients';
2+
export {
3+
SkadiClient,
4+
skadiPersonalizedDigestClient,
5+
skadiEngagementClient,
6+
} from './clients';
37
export * from './api/v2';
48
export * from './api/common';

src/integrations/skadi/types.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,38 @@ export type SkadiAd = {
77
company_logo: string;
88
call_to_action: string;
99
};
10-
export type SkadiResponse = Partial<{
10+
export type SkadiResponse<TValue> = Partial<{
1111
type: string;
12-
value: {
13-
digest: SkadiAd;
14-
};
12+
value: TValue;
1513
pixels: string[];
14+
generation_id: string;
1615
}>;
1716

18-
export interface ISkadiClient {
17+
export type ThemedValue = {
18+
dark: string;
19+
light: string;
20+
};
21+
22+
export type EngagementCreative = {
23+
gen_id: string;
24+
promoted_name: string;
25+
promoted_body: string;
26+
promoted_cta: string;
27+
promoted_url: string;
28+
promoted_logo_img: ThemedValue;
29+
promoted_icon_img: ThemedValue;
30+
promoted_gradient_start: ThemedValue;
31+
promoted_gradient_end: ThemedValue;
32+
tools: string[];
33+
keywords: string[];
34+
tags: string[];
35+
};
36+
37+
export interface ISkadiClient<TValue> {
1938
getAd(
2039
placement: string,
2140
metadata: {
2241
USERID: string;
2342
},
24-
): Promise<SkadiResponse>;
43+
): Promise<SkadiResponse<TValue>>;
2544
}

0 commit comments

Comments
 (0)