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
23 changes: 23 additions & 0 deletions cdk/lib/__snapshots__/dotcom-components.test.ts.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 9 additions & 3 deletions cdk/lib/dotcom-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
GuStringParameter,
} from '@guardian/cdk/lib/constructs/core';
import {
GuAllowPolicy,
GuDynamoDBReadPolicy,
GuGetS3ObjectsPolicy,
GuPutCloudwatchMetricsPolicy,
Expand Down Expand Up @@ -176,7 +177,8 @@ chown -R dotcom-components:support /var/log/dotcom-components
`${this.stage}/channel-switches.json`,
`${this.stage}/configured-amounts-v3.json`,
`${this.stage}/guardian-weekly-propensity-test/*`,
],
`PROD/auxia-credentials.json`,
],
}),
new GuGetS3ObjectsPolicy(
this,
Expand Down Expand Up @@ -207,7 +209,11 @@ chown -R dotcom-components:support /var/log/dotcom-components
new GuDynamoDBReadPolicy(this, 'DynamoBanditReadPolicy', {
tableName: `support-bandit-${this.stage}`,
}),
];
new GuAllowPolicy(this, 'SSMGet', {
actions: ['ssm:GetParameter'],
resources: ['*'],
}),
];

const scaling: GuAsgCapacity = {
minimumInstances: this.stage === 'CODE' ? 1 : 3,
Expand Down Expand Up @@ -249,5 +255,5 @@ chown -R dotcom-components:support /var/log/dotcom-components
ec2App.autoScalingGroup.scaleOnCpuUtilization('CpuScalingPolicy', {
targetUtilizationPercent: 40,
});
}
}
}
42 changes: 21 additions & 21 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,32 +26,17 @@
"riffraff": "node-riffraff-artifact"
},
"devDependencies": {
"@guardian/eslint-config-typescript": "^7.0.0",
"@guardian/node-riffraff-artifact": "^0.3.2",
"@guardian/prettier": "^5.0.0",
"eslint": "^8.47.0",
"eslint-plugin-prettier": "5.1.3",
"eslint-plugin-react": "^7.33.2",
"prettier": "^3.0.0",
"ts-loader": "^9.2.5",
"tslib": "^2.5.3",
"typescript": "~5.1.3",
"webpack": "^5.94.0",
"zod": "3.22.4",
"@babel/core": "^7.23.2",
"@babel/preset-typescript": "^7.23.2",
"@changesets/cli": "^2.26.2",
"@guardian/eslint-config-typescript": "^7.0.0",
"@guardian/node-riffraff-artifact": "^0.3.2",
"@guardian/prettier": "^5.0.0",
"@rollup/plugin-alias": "^3.1.1",
"@rollup/plugin-babel": "^5.1.0",
"@rollup/plugin-commonjs": "^14.0.0",
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-replace": "^2.3.3",
"babel-loader": "^9.1.3",
"rollup": "^2.79.2",
"rollup-plugin-external-globals": "^0.5.0",
"rollup-plugin-filesize": "^9.0.2",
"rollup-plugin-peer-deps-external": "^2.2.3",
"rollup-plugin-terser": "^7.0.2",
"@types/body-parser": "^1.19.5",
"@types/compression": "^1.7.0",
"@types/cors": "^2.8.6",
Expand All @@ -61,17 +46,32 @@
"@types/node": "^18.14.6",
"@types/node-fetch": "^2.5.4",
"@types/seedrandom": "^3.0.1",
"babel-loader": "^9.1.3",
"body-parser": "^1.20.3",
"concurrently": "^6.2.0",
"eslint": "^8.47.0",
"eslint-plugin-prettier": "5.1.3",
"eslint-plugin-react": "^7.33.2",
"fishery": "^0.3.0",
"inquirer": "^7.0.3",
"jest": "^29.7.0",
"nodemon": "^3.1.7",
"prettier": "^3.0.0",
"rollup": "^2.79.2",
"rollup-plugin-external-globals": "^0.5.0",
"rollup-plugin-filesize": "^9.0.2",
"rollup-plugin-peer-deps-external": "^2.2.3",
"rollup-plugin-terser": "^7.0.2",
"ts-jest": "^29.1.1",
"ts-loader": "^9.2.5",
"ts-node-dev": "^2.0.0",
"tslib": "^2.5.3",
"typescript": "~5.1.3",
"typescript-json-schema": "^0.42.0",
"webpack": "^5.94.0",
"webpack-cli": "^4.7.2",
"webpack-merge": "^5.8.0",
"body-parser": "^1.20.3"
"zod": "3.22.4"
},
"dependencies": {
"@guardian/libs": "17.0.0",
Expand All @@ -88,8 +88,8 @@
"zod": "3.22.4"
},
"peerDependencies": {
"zod": "^3.22.4",
"@guardian/libs": "^17.0.0"
"@guardian/libs": "^17.0.0",
"zod": "^3.22.4"
},
"packageManager": "pnpm@8.15.7"
}
162 changes: 162 additions & 0 deletions src/server/api/auxiaProxyRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import express, { Router } from 'express';
import { getSsmValue } from '../utils/ssm';

export interface AuxiaRouterConfig {
apiKey: string;
projectId: string;
userId: string;
}

interface AuxiaApiRequestPayloadContextualAttributes {
key: string;
stringValue: string;
}

interface AuxiaApiRequestPayloadSurface {
surface: string;
minimumTreatmentCount: number;
maximumTreatmentCount: number;
}

interface AuxiaAPIRequestPayload {
projectId: string;
userId: string;
contextualAttributes: AuxiaApiRequestPayloadContextualAttributes[];
surfaces: AuxiaApiRequestPayloadSurface[];
languageCode: string;
}

interface AuxiaAPIAnswerDataUserTreatment {
treatmentId: string;
treatmentTrackingId: string;
rank: string;
contentLanguageCode: string;
treatmentContent: string;
treatmentType: string;
surface: string;
}

interface AuxiaAPIAnswerData {
responseId: string;
userTreatments: AuxiaAPIAnswerDataUserTreatment[];
}

interface AuxiaProxyResponseData {
shouldShowSignInGate: boolean;
}

const buildAuxiaAPIRequestPayload = (projectId: string, userId: string): AuxiaAPIRequestPayload => {
// For the moment we are hard coding the data provided in contextualAttributes and surfaces.
return {
projectId: projectId,
userId: userId,
contextualAttributes: [
{
key: 'profile_id',
stringValue: 'pr1234',
},
{
key: 'last_action',
stringValue: 'button_x_clicked',
},
],
surfaces: [
{
surface: 'ARTICLE_PAGE',
minimumTreatmentCount: 1,
maximumTreatmentCount: 5,
},
],
languageCode: 'en-GB',
};
};

const fetchAuxiaData = async (
apiKey: string,
projectId: string,
userId: string,
): Promise<AuxiaAPIAnswerData> => {
const url = 'https://apis.auxia.io/v1/GetTreatments';

const headers = {
'Content-Type': 'application/json',
'x-api-key': apiKey,
};

const payload = buildAuxiaAPIRequestPayload(projectId, userId);

const params = {
method: 'POST',
headers: headers,
body: JSON.stringify(payload),
};

const response = await fetch(url, params);

const responseBody = await response.json();

return Promise.resolve(responseBody as AuxiaAPIAnswerData);
};

const buildAuxiaProxyResponseData = (auxiaData: AuxiaAPIAnswerData): AuxiaProxyResponseData => {
// This is the most important function of this router, it takes the answer from auxia and
// and decides if the sign in gate should be shown or not.

// In the current interpretation we are saying that a non empty userTreatments array means
// that the sign in gate should be shown.

const shouldShowSignInGate = auxiaData.userTreatments.length > 0;

return { shouldShowSignInGate };
};

export const getAuxiaRouterConfig = async (): Promise<AuxiaRouterConfig> => {
const apiKey = await getSsmValue('PROD', 'auxia-api-key');
if (apiKey === undefined) {
throw new Error('auxia-api-key is undefined');
}

const projectId = await getSsmValue('PROD', 'auxia-projectId');
if (projectId === undefined) {
throw new Error('auxia-projectId is undefined');
}

const userId = await getSsmValue('PROD', 'auxia-userId');
if (userId === undefined) {
throw new Error('auxia-userId is undefined');
}

return Promise.resolve({
apiKey,
projectId,
userId,
});
};

export const buildAuxiaProxyRouter = (config: AuxiaRouterConfig): Router => {
const router = Router();
router.post(
'/auxia',

// We are disabling that check for now, we will re-enable it later when we have a
// better understanding of the request payload.
// bodyContainsAllFields(['tracking', 'targeting']),

async (req: express.Request, res: express.Response, next: express.NextFunction) => {
try {
const auxiaData = await fetchAuxiaData(
config.apiKey,
config.projectId,
config.userId,
);
const response = buildAuxiaProxyResponseData(auxiaData);

res.send(response);
} catch (error) {
next(error);
}
},
);

return router;
};
6 changes: 6 additions & 0 deletions src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { logError } from './utils/logging';
import { buildEpicRouter } from './api/epicRouter';
import { buildBannerRouter } from './api/bannerRouter';
import { buildHeaderRouter } from './api/headerRouter';
import { buildAuxiaProxyRouter, getAuxiaRouterConfig } from './api/auxiaProxyRouter';
import { buildAmpEpicRouter } from './api/ampEpicRouter';
import { buildChannelSwitchesReloader } from './channelSwitches';
import { buildSuperModeArticlesReloader } from './lib/superMode';
Expand Down Expand Up @@ -88,6 +89,8 @@ const buildApp = async (): Promise<Express> => {

const banditData = await buildBanditDataReloader(articleEpicTests, bannerTests);

const auxiaConfig = await getAuxiaRouterConfig();

// Build the routers
app.use(
buildEpicRouter(
Expand All @@ -113,6 +116,7 @@ const buildApp = async (): Promise<Express> => {
),
);
app.use(buildHeaderRouter(channelSwitches, headerTests));

app.use('/amp', buildAmpEpicRouter(choiceCardAmounts, tickerData, ampEpicTests));

app.use(errorHandlingMiddleware);
Expand All @@ -122,6 +126,8 @@ const buildApp = async (): Promise<Express> => {
res.send('OK');
});

app.use(buildAuxiaProxyRouter(auxiaConfig));

return Promise.resolve(app);
};

Expand Down
14 changes: 14 additions & 0 deletions src/server/utils/ssm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as AWS from 'aws-sdk';

export async function getSsmValue(stage: string, id: string): Promise<string | undefined> {
const name = `/membership/support-dotcom-components/${stage}/${id}`;
const client = new AWS.SSM({ region: 'eu-west-1' });

const response = await client
.getParameter({
Name: name,
})
.promise();

return response.Parameter?.Value;
}
Loading