app/fetchData.ts (156 lines of code) (raw):

// XXX ultimately, this wants to live in lib/fetchData.ts, but we need to get rid of our dependency on columns.tsx first. import { compareSurfacesFn, getDesktopDashboardLink, getPreviewLink, getSurfaceData, getTemplateFromMessage, maybeCreateWelcomePreview, messageHasMicrosurvey, } from "@/lib/messageUtils"; import { NimbusRecipe } from "@/lib/nimbusRecipe"; import { NimbusRecipeCollection } from "@/lib/nimbusRecipeCollection"; import { FxMSMessageInfo } from "./columns"; import { cleanLookerData, getCTRPercentData, mergeLookerData, runLookQuery, } from "@/lib/looker.ts"; import { Platform } from "@/lib/types"; const isLookerEnabled = process.env.IS_LOOKER_ENABLED === "true"; /** * A function to fetch the data to render in Dashboard components in pages. * @param platform A specified Platform (ie. fenix, ios, or firefox-desktop) * @returns any local live message data, experiment data, total number of * experiments, rollout data, and total number of rollouts for a given * platform. */ export async function fetchData(platform: Platform) { // XXX at some point, once the completed experiments get ported to use // the new <Dashboard> infra including this, we're going to need to do // something better than just pass "false" as the first param here. const recipeCollection = new NimbusRecipeCollection(false, platform); await recipeCollection.fetchRecipes(); console.log("recipeCollection.length = ", recipeCollection.recipes.length); const localData = (await getASRouterLocalMessageInfoFromFile()).sort( compareSurfacesFn, ); const msgExpRecipeCollection = await getMsgExpRecipeCollection(recipeCollection); const msgRolloutRecipeCollection = await getMsgRolloutCollection(recipeCollection); const experimentAndBranchInfo = isLookerEnabled ? await msgExpRecipeCollection.getExperimentAndBranchInfos() : msgExpRecipeCollection.recipes.map((recipe: NimbusRecipe) => recipe.getRecipeInfo(), ); const totalExperiments = msgExpRecipeCollection.recipes.length; const msgRolloutInfo = isLookerEnabled ? await msgRolloutRecipeCollection.getExperimentAndBranchInfos() : msgRolloutRecipeCollection.recipes.map((recipe: NimbusRecipe) => recipe.getRecipeInfo(), ); const totalRolloutExperiments = msgRolloutRecipeCollection.recipes.length; return { localData, experimentAndBranchInfo, totalExperiments, msgRolloutInfo, totalRolloutExperiments, }; } /** * A function to fetch a collection of Nimbus experiments. * @param recipeCollection a collection of Nimbus recipes * @returns recipeCollection after filtering out any rollouts, filtering out * non accepting feature ids, and sorted based on dates. */ export async function getMsgExpRecipeCollection( recipeCollection: NimbusRecipeCollection, ): Promise<NimbusRecipeCollection> { const expOnlyCollection = new NimbusRecipeCollection(); expOnlyCollection.recipes = recipeCollection.recipes.filter((recipe) => recipe.isExpRecipe(), ); console.log("expOnlyCollection.length = ", expOnlyCollection.recipes.length); const msgExpRecipeCollection = new NimbusRecipeCollection(); msgExpRecipeCollection.recipes = expOnlyCollection.recipes .filter((recipe) => recipe.usesMessagingFeatures()) .sort(compareDatesFn); console.log( "msgExpRecipeCollection.length = ", msgExpRecipeCollection.recipes.length, ); return msgExpRecipeCollection; } /** * A function to fetch a collection of Nimbus rollouts. * @param recipeCollection a collection of Nimbus recipes * @returns recipeCollection after filtering out any experiments, filtering out * non accepting feature ids, and sorted based on dates. */ export async function getMsgRolloutCollection( recipeCollection: NimbusRecipeCollection, ): Promise<NimbusRecipeCollection> { const msgRolloutRecipeCollection = new NimbusRecipeCollection(); msgRolloutRecipeCollection.recipes = recipeCollection.recipes .filter((recipe) => recipe.usesMessagingFeatures() && !recipe.isExpRecipe()) .sort(compareDatesFn); console.log( "msgRolloutRecipeCollection.length = ", msgRolloutRecipeCollection.recipes.length, ); return msgRolloutRecipeCollection; } /** * @returns message data in the form of FxMSMessageInfo from * lib/asrouter-local-prod-messages/data.json and also FxMS telemetry data if * Looker credentials are enabled. */ export async function getASRouterLocalMessageInfoFromFile(): Promise< FxMSMessageInfo[] > { const fs = require("fs"); let data = fs.readFileSync( "lib/asrouter-local-prod-messages/data.json", "utf8", ); let json_data = JSON.parse(data); if (isLookerEnabled) { json_data = await appendFxMSTelemetryData(json_data); } let messages = await Promise.all( json_data.map(async (messageDef: any): Promise<FxMSMessageInfo> => { return await getASRouterLocalColumnFromJSON(messageDef); }), ); return messages; } /** * Given a message JSON, this function fetches the message data as an * FxMSMessageInfo object and populating it with surface data, preview links, * microsurvey tags, CTR data, and dashboard links when available. * @param messageDef the JSON for a single message collected from local data * @returns the information in messageDef in FxMSMessageInfo type */ export async function getASRouterLocalColumnFromJSON( messageDef: any, ): Promise<FxMSMessageInfo> { let fxmsMsgInfo: FxMSMessageInfo = { product: "Desktop", id: messageDef.id, template: messageDef.template, surface: getSurfaceData(getTemplateFromMessage(messageDef)).surface, segment: "some segment", metrics: "some metrics", ctrPercent: undefined, // may be populated from Looker data ctrPercentChange: undefined, // may be populated from Looker data previewLink: getPreviewLink(maybeCreateWelcomePreview(messageDef)), impressions: undefined, // may be populated from Looker data hasMicrosurvey: messageHasMicrosurvey(messageDef.id), hidePreview: messageDef.hidePreview, }; const channel = "release"; const platform = "firefox-desktop"; if (isLookerEnabled) { const ctrPercentData = await getCTRPercentData( fxmsMsgInfo.id, platform, fxmsMsgInfo.template, channel, ); if (ctrPercentData) { fxmsMsgInfo.ctrPercent = ctrPercentData.ctrPercent; fxmsMsgInfo.impressions = ctrPercentData.impressions; } } fxmsMsgInfo.ctrDashboardLink = getDesktopDashboardLink( fxmsMsgInfo.template, fxmsMsgInfo.id, channel, ); return fxmsMsgInfo; } /** * Appends any FxMS telemetry message data from the query in Look * https://mozilla.cloud.looker.com/looks/2162 that does not already exist (ie. * no duplicate message ids) in existingMessageData and returns the result. The * message data is also cleaned up to match the message data objects from * ASRouter, remove any test messages, and update templates. */ export async function appendFxMSTelemetryData(existingMessageData: any) { // Get Looker message data (taken from the query in Look // https://mozilla.cloud.looker.com/looks/2162) const lookId = "2162"; let lookerData = await runLookQuery(lookId); // Clean and merge Looker data with existing data let jsonLookerData = cleanLookerData(lookerData); let mergedData = mergeLookerData(existingMessageData, jsonLookerData); return mergedData; } /** * A sorting function to sort messages by their start dates in descending order. * If one or both of the recipes is missing a start date, they will be ordered * identically since there's not enough information to properly sort them by * date. * * @param a Nimbus recipe to compare with `b`. * @param b Nimbus recipe to compare with `a`. * @returns -1 if the start date for message a is after the start date for * message b, zero if they're equal, and 1 otherwise. */ export function compareDatesFn(a: NimbusRecipe, b: NimbusRecipe): number { if (a._rawRecipe.startDate && b._rawRecipe.startDate) { if (a._rawRecipe.startDate > b._rawRecipe.startDate) { return -1; } else if (a._rawRecipe.startDate < b._rawRecipe.startDate) { return 1; } } // a must be equal to b return 0; }