Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Snippets #3

Merged
merged 2 commits into from
Nov 1, 2023
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
21 changes: 15 additions & 6 deletions src/http/get-benefits/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const { getDefinitions } = require("@architect/shared/s3");
const { generateHtml } = require("@architect/shared/templates");
const { matchHostDef } = require("@architect/shared/hosts");
const { getThrottles } = require("@architect/shared/throttles");
const { assembleSnippets } = require("@architect/shared/snippets.js");

// Definitions will be loaded from benefits-recs-defs.json in S3.
// We keep it outside the handler to cache it between Lambda runs.
Expand All @@ -23,16 +24,24 @@ exports.handler = arc.http.async(async (req) => {
targets: targetDefs = [],
hosts: hostDefs = [],
throttles: throttleDefs = [],
snippets: snippetDefs = [],
} = definitions;

// Grab data from headers and URL query parameters.
const host = decodeURIComponent(req.query.host || "");
const language = req.query.language || "en";
const hostQuery = decodeURIComponent(req.query.host || "");
const langQuery = req.query.language || "en";
const acceptHeader = req.headers?.accept;

// Unless it's Chinese, strip the language code down to two characters.
// We need to preserve the Chinese code to display Traditional vs. Simplified.
const language = langQuery.startsWith("zh")
? langQuery
: langQuery.slice(0, 2);

// Process metadata.
const hostDef = matchHostDef(host, hostDefs);
const hostDef = matchHostDef(hostQuery, hostDefs);
const throttles = await getThrottles(throttleDefs);
const snippets = assembleSnippets(snippetDefs, language, hostDef);

// Create target links.
const allLinks = assembleLinks(targetDefs, language, hostDef);
Expand All @@ -47,9 +56,9 @@ exports.handler = arc.http.async(async (req) => {
}

const data = {
header: "Apply for more benefits!",
tagline: "You might be able to get:",
experimentName: "2023-08-01-resume-tracking",
header: snippets.header || "Apply for more benefits!",
tagline: snippets.tagline || "You might be able to get:",
experimentName: snippets.experimentName || "2023-08-01-resume-tracking",
experimentVariation: links.map((link) => link.id).join("-"),
links,
};
Expand Down
29 changes: 29 additions & 0 deletions src/scheduled/airtable/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ const apiSources = [
key: "hosts",
url: "https://api.airtable.com/v0/app2OnBmhVD4GZ68L/Hosts",
},
{
key: "snippets",
url: "https://api.airtable.com/v0/app2OnBmhVD4GZ68L/Snippets",
},
];

/**
Expand Down Expand Up @@ -119,6 +123,30 @@ exports.handler = async (req) => {
};
});

// Reformat snippets for downstream usage.
const snippets = airData.snippets.records
.filter((snippet) => {
const { id, placement, text } = snippet.fields;

// Filter out snippets that are incomplete.
const checks = [
id && id.trim() !== "",
placement && placement.trim() !== "",
text && text.trim() !== "",
];

return checks.every((check) => check === true);
})
.map((snippet) => {
const { placement, text, language, host_ids } = snippet.fields;
return {
placement,
text,
language,
host_ids,
};
});

// Reformat target links for downstream usage.
// Note: we combine the translations into the target objects here.
const targets = airData.targets.records
Expand Down Expand Up @@ -188,6 +216,7 @@ exports.handler = async (req) => {
targets,
throttles,
hosts,
snippets,
};

const s3LiveCommand = new PutObjectCommand({
Expand Down
25 changes: 9 additions & 16 deletions src/shared/links.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,37 +62,30 @@ const addAnalytics = (linkUrl, analytics, hostDef) => {
* A processed list of target links in the requested language.
*/
exports.assembleLinks = (targets, language, hostDef) => {
// Unless it's Chinese, strip the language code down to two characters.
// We need to preserve the Chinese code to display Traditional vs. Simplified.
const langKey = language.startsWith("zh") ? language : language.slice(0, 2);

// Return a single set of values for each link, based on language.
// Default to English where values are unavailable.
const links = targets.map((target) => {
const { id, translations } = target;

const translation = translations[language];
const { en } = translations;

// Get the SVG markup for the icon.
const iconKey = translations[langKey]?.icon || translations.en.icon;
const iconKey = translation?.icon || en.icon;
const graphic = icons[iconKey];

const lead = translations[langKey]?.lead || translations.en.lead || "";

const catalyst =
translations[langKey]?.catalyst || translations.en.catalyst || "";

const linkUrl = translations[langKey]?.url || translations.en.url || "";

const analytics =
translations[langKey]?.analytics || translations.en.analytics || "";

const lead = translation?.lead || en.lead || "";
const catalyst = translation?.catalyst || en.catalyst || "";
const linkUrl = translation?.url || en.url || "";
const analytics = translation?.analytics || en.analytics || "";
const urlWithAnalytics = addAnalytics(linkUrl, analytics, hostDef);

return {
lead,
catalyst,
url: urlWithAnalytics,
graphic,
language: langKey,
language,
id,
};
});
Expand Down
15 changes: 15 additions & 0 deletions src/shared/s3.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const s3 = new S3Client({});
* A list of active target link throttles.
* @property {Host[]} hosts
* A list of hosts where the widget is placed.
* @property {Snippet[]} snippets
* A list of misc. values for use within the widget.
*/

/**
Expand Down Expand Up @@ -81,6 +83,19 @@ const s3 = new S3Client({});
* The URLs associated with this host.
*/

/**
* A *Snippet* is a small misc. value that appears somewhere in the widget.
* @typedef {object} Snippet
* @property {string} placement
* The placement spot in the widget.
* @property {string} text
* The value of this snippet.
* @property {string} language
* The language code for this snippet.
* @property {string[]} host_ids
* Host IDs on which this snippet should specifically be used.
*/

/**
* Grab the definitions file from S3.
* @returns {Promise<Definitions>}
Expand Down
47 changes: 47 additions & 0 deletions src/shared/snippets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Assembles snippets for this request based on language and host.
* @param {import('./s3.js').Snippet[]} snippetDefs
* Raw snippet definitions from the S3 definitons file.
* @param {string} language
* The language code for this request.
* @param {import('./s3.js').Host} hostDef
* The identified host definition for this request.
* @returns {object}
*/
exports.assembleSnippets = (snippetDefs, language, hostDef) => {
// Sort all snippet definitions based on relevance to language and host.
const sortedSnippetDefs = snippetDefs
.map((snippetDef) => {
// Let's rank 'em.
let rank = 0;

// Language supercedes all.
if (language === snippetDef?.language) rank += 5;

// Give non-host-specific snippets a small boost, to make them default.
if (!snippetDef.host_ids) rank += 1;

// Give host-match snippets a bigger boost, to overcome defaults.
if (hostDef && snippetDef?.host_ids?.includes(hostDef?.id)) rank += 2;

return { rank, snippetDef };
})
.sort((a, b) => b.rank - a.rank)
.map((rankedSnippetDef) => rankedSnippetDef.snippetDef);

// Find highest ranked snippet for each placement.

const header = sortedSnippetDefs.find(
(snippetDef) => snippetDef.placement === "header"
)?.text;

const tagline = sortedSnippetDefs.find(
(snippetDef) => snippetDef.placement === "tagline"
)?.text;

const experimentName = sortedSnippetDefs.find(
(snippetDef) => snippetDef.placement === "experiment_name"
)?.text;

return { header, tagline, experimentName };
};