From 9621cabae5ac30cd2645df481caf9942ad969b5c Mon Sep 17 00:00:00 2001 From: Dan Mosedale Date: Mon, 28 Jul 2025 21:11:29 -0400 Subject: [PATCH 1/2] Correct CTR references to UCTR --- __tests__/app/message-table.test.tsx | 76 ++++++++++---------- __tests__/lib/looker.test.ts | 44 ++++++------ __tests__/lib/nimbusRecipe.test.ts | 10 +-- __tests__/lib/nimbusRecipeCollection.test.ts | 6 +- app/columns.tsx | 62 ++++++++-------- app/fetchData.ts | 18 ++--- lib/looker.ts | 50 ++++++------- lib/nimbusRecipe.ts | 10 +-- lib/nimbusRecipeCollection.ts | 20 +++--- 9 files changed, 148 insertions(+), 148 deletions(-) diff --git a/__tests__/app/message-table.test.tsx b/__tests__/app/message-table.test.tsx index 756e278a..481964f8 100644 --- a/__tests__/app/message-table.test.tsx +++ b/__tests__/app/message-table.test.tsx @@ -185,7 +185,7 @@ describe("MessageTable", () => { expect(previewButton).not.toBeInTheDocument(); }); - it("displays CTR percentages when Looker dashboard exists and CTR is defined", async () => { + it("displays UCTR percentages when Looker dashboard exists and UCTR is defined", async () => { setMockPlatform("firefox-desktop"); const nimbusRecipeCollection = new NimbusRecipeCollection(); nimbusRecipeCollection.recipes = [ @@ -194,38 +194,38 @@ describe("MessageTable", () => { const recipeInfos = (await nimbusRecipeCollection.getExperimentAndBranchInfos()) as RecipeInfo[]; // Setting fake dashboard link in order to render in MessageTable - recipeInfos[0].branches[0].ctrDashboardLink = "test link"; + recipeInfos[0].branches[0].uctrDashboardLink = "test link"; render(); const toggleButton = screen.getByTestId("toggleAllRowsButton"); fireEvent.click(toggleButton); - const ctrMetrics = screen.getByText("12.35% CTR", { + const uctrMetrics = screen.getByText("12.35% UCTR", { exact: false, }); - expect(recipeInfos[0].branches[0].ctrPercent).toBe(12.35); - expect(recipeInfos[0].branches[0].ctrDashboardLink).toBeDefined(); - expect(ctrMetrics).toBeInTheDocument(); + expect(recipeInfos[0].branches[0].uctrPercent).toBe(12.35); + expect(recipeInfos[0].branches[0].uctrDashboardLink).toBeDefined(); + expect(uctrMetrics).toBeInTheDocument(); }); - it("displays 'Dashboard' when Looker dashboard exists but CTR is undefined", () => { + it("displays 'Dashboard' when Looker dashboard exists but UCTR is undefined", () => { const rawRecipe = ExperimentFakes.recipe("test-recipe"); const nimbusRecipe = new NimbusRecipe(rawRecipe); let recipeInfo = nimbusRecipe.getRecipeInfo(); // Setting fake dashboard link in order to render in MessageTable - recipeInfo.branches[0].ctrDashboardLink = "test link"; + recipeInfo.branches[0].uctrDashboardLink = "test link"; render(); const toggleButton = screen.getByTestId("toggleAllRowsButton"); fireEvent.click(toggleButton); const dashboardLink = screen.getByText("Dashboard"); - const ctrMetrics = screen.queryByText("CTR"); + const uctrMetrics = screen.queryByText("UCTR"); - expect(recipeInfo.branches[0].ctrPercent).not.toBeDefined(); - expect(recipeInfo.branches[0].ctrDashboardLink).toBeDefined(); + expect(recipeInfo.branches[0].uctrPercent).not.toBeDefined(); + expect(recipeInfo.branches[0].uctrDashboardLink).toBeDefined(); expect(dashboardLink).toBeInTheDocument(); - expect(ctrMetrics).not.toBeInTheDocument(); + expect(uctrMetrics).not.toBeInTheDocument(); }); it("doesn't display any metric when Looker dashboard doesn't exist", () => { @@ -238,14 +238,14 @@ describe("MessageTable", () => { const toggleButton = screen.getByTestId("toggleAllRowsButton"); fireEvent.click(toggleButton); - const ctrMetrics = screen.queryByText("CTR"); + const uctrMetrics = screen.queryByText("UCTR"); const dashboardLink = screen.queryByText("Dashboard"); - expect(messageTableData[0].branches[0].ctrPercent).not.toBeDefined(); + expect(messageTableData[0].branches[0].uctrPercent).not.toBeDefined(); expect( - messageTableData[0].branches[0].ctrDashboardLink, + messageTableData[0].branches[0].uctrDashboardLink, ).not.toBeDefined(); - expect(ctrMetrics).not.toBeInTheDocument(); + expect(uctrMetrics).not.toBeInTheDocument(); expect(dashboardLink).not.toBeInTheDocument(); }); @@ -340,7 +340,7 @@ describe("MessageTable", () => { }); describe("MessageColumns", () => { - it("displays CTR percentages when Looker dashboard exists and CTR is defined", async () => { + it("displays UCTR percentages when Looker dashboard exists and UCTR is defined", async () => { const fakeMsgInfo: FxMSMessageInfo = { product: "Desktop", id: "test id", @@ -348,23 +348,23 @@ describe("MessageTable", () => { surface: "test surface", segment: "test segment", metrics: "test metrics", - ctrPercent: 12.35, - ctrPercentChange: 2, - ctrDashboardLink: "test link", + uctrPercent: 12.35, + uctrPercentChange: 2, + uctrDashboardLink: "test link", impressions: 12899, }; render( , ); - const ctrMetrics = screen.getByText("12.35% CTR", { + const uctrMetrics = screen.getByText("12.35% UCTR", { exact: false, }); - expect(ctrMetrics).toBeInTheDocument(); + expect(uctrMetrics).toBeInTheDocument(); }); - it("displays 'Dashboard' when Looker dashboard exists but CTR is undefined", () => { + it("displays 'Dashboard' when Looker dashboard exists but UCTR is undefined", () => { const fakeMsgInfo: FxMSMessageInfo = { product: "Desktop", id: "test id", @@ -372,16 +372,16 @@ describe("MessageTable", () => { surface: "test surface", segment: "test segment", metrics: "test metrics", - ctrDashboardLink: "test link", + uctrDashboardLink: "test link", }; render( , ); - const ctrMetrics = screen.queryByText("CTR"); + const uctrMetrics = screen.queryByText("UCTR"); const dashboardLink = screen.getByText("Dashboard"); - expect(ctrMetrics).not.toBeInTheDocument(); + expect(uctrMetrics).not.toBeInTheDocument(); expect(dashboardLink).toBeInTheDocument(); }); @@ -398,10 +398,10 @@ describe("MessageTable", () => { , ); - const ctrMetrics = screen.queryByText("CTR"); + const uctrMetrics = screen.queryByText("UCTR"); const dashboardLink = screen.queryByText("Dashboard"); - expect(ctrMetrics).not.toBeInTheDocument(); + expect(uctrMetrics).not.toBeInTheDocument(); expect(dashboardLink).not.toBeInTheDocument(); }); @@ -434,7 +434,7 @@ describe("MessageTable", () => { surface: "test surface", segment: "test segment", metrics: "test metrics", - ctrPercent: 24.3, + uctrPercent: 24.3, impressions: parseInt(impressions!) + 100, }; const fxmsMsgInfo2: FxMSMessageInfo = { @@ -444,7 +444,7 @@ describe("MessageTable", () => { surface: "test surface", segment: "test segment", metrics: "test metrics", - ctrPercent: 24.3, + uctrPercent: 24.3, impressions: parseInt(impressions!) - 100, }; const fxmsMsgInfo3: FxMSMessageInfo = { @@ -454,7 +454,7 @@ describe("MessageTable", () => { surface: "test surface", segment: "test segment", metrics: "test metrics", - ctrPercent: 24.3, + uctrPercent: 24.3, impressions: parseInt(impressions!), }; render( @@ -485,7 +485,7 @@ describe("MessageTable", () => { surface: "Feature Callout (1st screen)", segment: "test segment", metrics: "test metrics", - ctrPercent: 24.3, + uctrPercent: 24.3, impressions: 1000, }; const fxmsMsgInfo2: FxMSMessageInfo = { @@ -495,7 +495,7 @@ describe("MessageTable", () => { surface: "Default About:Welcome Message (1st screen)", segment: "test segment", metrics: "test metrics", - ctrPercent: 24.3, + uctrPercent: 24.3, impressions: 1000, }; const fxmsMsgInfo3: FxMSMessageInfo = { @@ -505,7 +505,7 @@ describe("MessageTable", () => { surface: "Private Browsing New Tab", segment: "test segment", metrics: "test metrics", - ctrPercent: 24.3, + uctrPercent: 24.3, impressions: 1000, }; @@ -542,7 +542,7 @@ describe("MessageTable", () => { surface: "Feature Callout (1st screen)", segment: "test segment", metrics: "test metrics", - ctrPercent: 24.3, + uctrPercent: 24.3, impressions: 1000, }; const fxmsMsgInfo2: FxMSMessageInfo = { @@ -552,7 +552,7 @@ describe("MessageTable", () => { surface: "InfoBar", segment: "test segment", metrics: "test metrics", - ctrPercent: 24.3, + uctrPercent: 24.3, impressions: 1000, }; const fxmsMsgInfo3: FxMSMessageInfo = { @@ -562,7 +562,7 @@ describe("MessageTable", () => { surface: "Default About:Welcome Message (1st screen)", segment: "test segment", metrics: "test metrics", - ctrPercent: 24.3, + uctrPercent: 24.3, impressions: 1000, }; @@ -612,7 +612,7 @@ describe("MessageTable", () => { surface: "test surface", segment: "test segment", metrics: "test metrics", - ctrDashboardLink: "test link", + uctrDashboardLink: "test link", }; render( , diff --git a/__tests__/lib/looker.test.ts b/__tests__/lib/looker.test.ts index f62064ae..76e4df93 100644 --- a/__tests__/lib/looker.test.ts +++ b/__tests__/lib/looker.test.ts @@ -38,69 +38,69 @@ describe("Looker", () => { expect(queryResult).toEqual(fakeQueryResult); }); - describe("getSafeCtrPercent", () => { - it("should correctly format a CTR percentage to 2 decimal places", () => { - expect(looker.getSafeCtrPercent(0.123456789)).toEqual(12.35); - expect(looker.getSafeCtrPercent(0.1)).toEqual(10); - expect(looker.getSafeCtrPercent(0.123)).toEqual(12.3); - expect(looker.getSafeCtrPercent(0.1235)).toEqual(12.35); + describe("getSafeUctrPercent", () => { + it("should correctly format a UCTR percentage to 2 decimal places", () => { + expect(looker.getSafeUctrPercent(0.123456789)).toEqual(12.35); + expect(looker.getSafeUctrPercent(0.1)).toEqual(10); + expect(looker.getSafeUctrPercent(0.123)).toEqual(12.3); + expect(looker.getSafeUctrPercent(0.1235)).toEqual(12.35); }); it("should handle zero value", () => { - expect(looker.getSafeCtrPercent(0)).toEqual(0); + expect(looker.getSafeUctrPercent(0)).toEqual(0); }); }); - describe("getCTRPercentData", () => { - it("should return the CTR percent for a desktop message with standard template", async () => { + describe("getUCTRPercentData", () => { + it("should return the UCTR percent for a desktop message with standard template", async () => { const template = "test_template"; const platform = "firefox-desktop"; const id = "test_query_0"; setMockPlatform(platform); setMockTemplate(template); - const ctrPercentData = await looker.getCTRPercentData( + const uctrPercentData = await looker.getUCTRPercentData( id, platform, template, ); - expect(ctrPercentData?.ctrPercent).toEqual(12.35); - expect(ctrPercentData?.impressions).toEqual(12899); + expect(uctrPercentData?.uctrPercent).toEqual(12.35); + expect(uctrPercentData?.impressions).toEqual(12899); }); - it("should return the CTR percent for a desktop message with infobar template", async () => { + it("should return the UCTR percent for a desktop message with infobar template", async () => { const id = "test_query_0"; const platform = "firefox-desktop"; const template = "infobar"; setMockPlatform(platform); setMockTemplate(template); - const ctrPercentData = await looker.getCTRPercentData( + const uctrPercentData = await looker.getUCTRPercentData( id, platform, template, ); - expect(ctrPercentData?.ctrPercent).toEqual(12.35); - expect(ctrPercentData?.impressions).toEqual(8765); + expect(uctrPercentData?.uctrPercent).toEqual(12.35); + expect(uctrPercentData?.impressions).toEqual(8765); }); - it("should return the CTR percent for an android message with survey template and extrapolate impressions", async () => { + it("should return the UCTR percent for an android message with survey template and extrapolate impressions", async () => { const id = "test_query_0"; const platform = "fenix"; const template = "survey"; setMockPlatform(platform); setMockTemplate(template); - const ctrPercentData = await looker.getCTRPercentData( + const uctrPercentData = await looker.getUCTRPercentData( id, platform, template, ); - expect(ctrPercentData?.ctrPercent).toEqual(12.35); - expect(ctrPercentData?.impressions).toEqual(12890); // 1289 * 10 (extrapolated) + expect(uctrPercentData?.uctrPercent).toEqual(12.35); + expect(uctrPercentData?.impressions).toEqual(12890); // 1289 * 10 (extrapolated) }); it("should return undefined for a standard android message (non-survey template)", async () => { @@ -110,14 +110,14 @@ describe("Looker", () => { setMockPlatform(platform); setMockTemplate(template); - const ctrPercentData = await looker.getCTRPercentData( + const uctrPercentData = await looker.getUCTRPercentData( id, platform, template, ); // For non-survey Android templates, we expect undefined - expect(ctrPercentData).toBeUndefined(); + expect(uctrPercentData).toBeUndefined(); }); }); diff --git a/__tests__/lib/nimbusRecipe.test.ts b/__tests__/lib/nimbusRecipe.test.ts index 07fc7ac0..c37bda9a 100644 --- a/__tests__/lib/nimbusRecipe.test.ts +++ b/__tests__/lib/nimbusRecipe.test.ts @@ -145,8 +145,8 @@ describe("NimbusRecipe", () => { product: "Desktop", id: "test-recipe", segment: "some segment", - ctrPercent: 0.5, - ctrPercentChange: 2, + uctrPercent: 0.5, + uctrPercentChange: 2, metrics: "some metrics", experimenterLink: `https://experimenter.services.mozilla.com/nimbus/test-recipe`, userFacingName: rawRecipe.userFacingName, @@ -215,7 +215,7 @@ describe("NimbusRecipe", () => { // use deepEqual and check for the existence of object properties instead. expect(branchInfo).toEqual({ product: "Desktop", - ctrDashboardLink: dashboardLink, + uctrDashboardLink: dashboardLink, id: "feature_value_id:treatment-a", isBranch: true, nimbusExperiment: AW_RECIPE, @@ -267,7 +267,7 @@ describe("NimbusRecipe", () => { expect(branchInfo).toEqual({ product: "Desktop", - ctrDashboardLink: dashboardLink, + uctrDashboardLink: dashboardLink, id: "feature_value_id:treatment-a", isBranch: true, nimbusExperiment: AW_RECIPE_NO_SCREENS, @@ -319,7 +319,7 @@ describe("NimbusRecipe", () => { template: "survey", screenshots: ["screenshotURI"], description: "control description", - ctrDashboardLink: dashboardLink, + uctrDashboardLink: dashboardLink, }); }); diff --git a/__tests__/lib/nimbusRecipeCollection.test.ts b/__tests__/lib/nimbusRecipeCollection.test.ts index b99e206d..69a8cc26 100644 --- a/__tests__/lib/nimbusRecipeCollection.test.ts +++ b/__tests__/lib/nimbusRecipeCollection.test.ts @@ -70,7 +70,7 @@ describe("NimbusRecipeCollection", () => { }); describe("getExperimentAndBranchInfos", () => { - it("gets all the recipe infos with updated CTR percents", async () => { + it("gets all the recipe infos with updated UCTR percents", async () => { setMockPlatform("firefox-desktop"); const nimbusRecipeCollection = new NimbusRecipeCollection(); nimbusRecipeCollection.recipes = [ @@ -80,8 +80,8 @@ describe("NimbusRecipeCollection", () => { const recipeInfos = (await nimbusRecipeCollection.getExperimentAndBranchInfos()) as RecipeInfo[]; - expect(recipeInfos[0].branches[0].ctrPercent).toBe(12.35); - expect(recipeInfos[0].branches[1].ctrPercent).toBe(12.35); + expect(recipeInfos[0].branches[0].uctrPercent).toBe(12.35); + expect(recipeInfos[0].branches[1].uctrPercent).toBe(12.35); }); }); }); diff --git a/app/columns.tsx b/app/columns.tsx index 4a9bdd02..6f407a28 100644 --- a/app/columns.tsx +++ b/app/columns.tsx @@ -58,7 +58,7 @@ function OffsiteLink(href: string, linkText: any) { } // This type is used to define the shape of our data. -// NOTE: ctrPercent is undefined by default until set using getCTRPercent. It is +// NOTE: uctrPercent is undefined by default until set using getUCTRPercent. It is // made optional to help determine what's displayed in the Metrics column. export type FxMSMessageInfo = { product: "Desktop" | "Android"; @@ -66,9 +66,9 @@ export type FxMSMessageInfo = { template: string; surface: string; segment: string; - ctrPercent?: number; - ctrPercentChange?: number; - ctrDashboardLink?: string; + uctrPercent?: number; + uctrPercentChange?: number; + uctrDashboardLink?: string; previewLink?: string; metrics: string; impressions?: number; @@ -85,9 +85,9 @@ export type RecipeInfo = { template?: string; // XXX template JSON name surface?: string; // XXX template display name segment?: string; - ctrPercent?: number; - ctrPercentChange?: number; - ctrDashboardLink?: string; + uctrPercent?: number; + uctrPercentChange?: number; + uctrDashboardLink?: string; previewLink?: string; metrics?: string; experimenterLink?: string; @@ -107,9 +107,9 @@ export type BranchInfo = { slug: string; surface?: string; segment?: string; - ctrPercent?: number; - ctrPercentChange?: number; - ctrDashboardLink?: string; + uctrPercent?: number; + uctrPercentChange?: number; + uctrDashboardLink?: string; previewLink?: string; metrics?: string; experimenterLink?: string; @@ -129,20 +129,20 @@ export type RecipeOrBranchInfo = RecipeInfo | BranchInfo; /** * @returns an OffsiteLink linking to the Looker dashboard link if it exists, - * labelled with either the CTR percent or "Dashboard" + * labelled with either the UCTR percent or "Dashboard" */ -function showCTRMetrics( - ctrDashboardLink?: string, - ctrPercent?: number, +function showUCTRMetrics( + uctrDashboardLink?: string, + uctrPercent?: number, impressions?: number, ) { - if (ctrDashboardLink && ctrPercent !== undefined && impressions) { + if (uctrDashboardLink && uctrPercent !== undefined && impressions) { return (
{OffsiteLink( - ctrDashboardLink, + uctrDashboardLink, <> - {ctrPercent + "% CTR"}
+ {uctrPercent + "% UCTR"}
{impressions.toLocaleString() + " impression" + (impressions > 1 ? "s" : "")} @@ -150,8 +150,8 @@ function showCTRMetrics( )}
); - } else if (ctrDashboardLink) { - return OffsiteLink(ctrDashboardLink, "Dashboard"); + } else if (uctrDashboardLink) { + return OffsiteLink(uctrDashboardLink, "Dashboard"); } } @@ -280,9 +280,9 @@ export const fxmsMessageColumns: ColumnDef[] = [ - The CTR and impressions metrics in this table are the primary + The UCTR and impressions metrics in this table are the primary button clickthrough rates calculated over the last 30 days. - Clicking into the CTR value will direct you to the Looker + Clicking into the UCTR value will direct you to the Looker dashboard displaying the data.

} @@ -308,9 +308,9 @@ export const fxmsMessageColumns: ColumnDef[] = [ return <>; } - const metrics = showCTRMetrics( - props.row.original.ctrDashboardLink, - props.row.original.ctrPercent, + const metrics = showUCTRMetrics( + props.row.original.uctrDashboardLink, + props.row.original.uctrPercent, props.row.original.impressions, ); if (metrics) { @@ -481,9 +481,9 @@ export const experimentColumns: ColumnDef[] = [ - The CTR and impressions metrics in this table are the primary + The UCTR and impressions metrics in this table are the primary button clickthrough rates calculated over the{" "} - time that the experiment is live. Clicking into the CTR + time that the experiment is live. Clicking into the UCTR value will direct you to the Looker dashboard displaying the data.

} @@ -500,9 +500,9 @@ export const experimentColumns: ColumnDef[] = [ return <>; } - const metrics = showCTRMetrics( - props.row.original.ctrDashboardLink, - props.row.original.ctrPercent, + const metrics = showUCTRMetrics( + props.row.original.uctrDashboardLink, + props.row.original.uctrPercent, props.row.original.impressions, ); if (metrics) { @@ -661,8 +661,8 @@ export const completedExperimentColumns: ColumnDef[] = [ return <>; } - if (props.row.original.ctrDashboardLink) { - return OffsiteLink(props.row.original.ctrDashboardLink, "Dashboard"); + if (props.row.original.uctrDashboardLink) { + return OffsiteLink(props.row.original.uctrDashboardLink, "Dashboard"); } return <>; }, diff --git a/app/fetchData.ts b/app/fetchData.ts index 939ae1b5..2c129f9c 100644 --- a/app/fetchData.ts +++ b/app/fetchData.ts @@ -13,7 +13,7 @@ import { NimbusRecipeCollection } from "@/lib/nimbusRecipeCollection"; import { FxMSMessageInfo } from "./columns"; import { cleanLookerData, - getCTRPercentData, + getUCTRPercentData, mergeLookerData, runLookQuery, } from "@/lib/looker.ts"; @@ -150,7 +150,7 @@ export async function getASRouterLocalMessageInfoFromFile(): Promise< /** * 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. + * microsurvey tags, UCTR 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 */ @@ -164,8 +164,8 @@ export async function getASRouterLocalColumnFromJSON( 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 + uctrPercent: undefined, // may be populated from Looker data + uctrPercentChange: undefined, // may be populated from Looker data previewLink: getPreviewLink(maybeCreateWelcomePreview(messageDef)), impressions: undefined, // may be populated from Looker data hasMicrosurvey: messageHasMicrosurvey(messageDef.id), @@ -176,19 +176,19 @@ export async function getASRouterLocalColumnFromJSON( const platform = "firefox-desktop"; if (isLookerEnabled) { - const ctrPercentData = await getCTRPercentData( + const uctrPercentData = await getUCTRPercentData( fxmsMsgInfo.id, platform, fxmsMsgInfo.template, channel, ); - if (ctrPercentData) { - fxmsMsgInfo.ctrPercent = ctrPercentData.ctrPercent; - fxmsMsgInfo.impressions = ctrPercentData.impressions; + if (uctrPercentData) { + fxmsMsgInfo.uctrPercent = uctrPercentData.uctrPercent; + fxmsMsgInfo.impressions = uctrPercentData.impressions; } } - fxmsMsgInfo.ctrDashboardLink = getDesktopDashboardLink( + fxmsMsgInfo.uctrDashboardLink = getDesktopDashboardLink( fxmsMsgInfo.template, fxmsMsgInfo.id, channel, diff --git a/lib/looker.ts b/lib/looker.ts index 85cd30ec..0444dd20 100644 --- a/lib/looker.ts +++ b/lib/looker.ts @@ -4,20 +4,20 @@ import { getDashboardIdForSurface, getSurfaceData } from "./messageUtils"; import { getLookerSubmissionTimestampDateFilter } from "./lookerUtils"; import { Platform } from "./types"; -export type CTRData = { - ctrPercent: number; +export type UCTRData = { + uctrPercent: number; impressions: number; }; /** - * Safely formats a CTR percentage value with consistent decimal precision + * Safely formats a UCTR percentage value with consistent decimal precision * - * @param value - The raw CTR rate (typically between 0 and 1) - * @returns The formatted CTR percentage with 2 decimal places + * @param value - The raw UCTR rate (typically between 0 and 1) + * @returns The formatted UCTR percentage with 2 decimal places * * This function is used throughout the codebase to ensure consistent formatting - * of CTR percentages. It's necessary because we need consistent decimal - * precision for CTR percentages in the UI and tests. + * of UCTR percentages. It's necessary because we need consistent decimal + * precision for UCTR percentages in the UI and tests. * * Using Math.round with multiplier/divisor instead of toFixed() to avoid * floating-point precision issues in JavaScript. This approach ensures @@ -27,7 +27,7 @@ export type CTRData = { * 2. Round to nearest integer to handle the 2-decimal precision we want * 3. Divide by 100 to shift decimal point left by 2 places */ -export function getSafeCtrPercent(value: number): number { +export function getSafeUctrPercent(value: number): number { return Math.round(value * 10000) / 100; } @@ -45,7 +45,7 @@ export async function getDashboardElement0( // clear, but the code is working, so I'm inclined to leave it alone for now. SDK.search_dashboard_elements({ dashboard_id: dashboardId, - title: "CTR and User Profiles Impressed", + title: "UCTR and User Profiles Impressed", fields: "query", }), ); @@ -117,7 +117,7 @@ export async function runQueryForSurface( /** * @param id the events_count.message_id required for running the looker - * query to retrieve CTR metrics + * query to retrieve UCTR metrics * @param platform the message platform * @param template the message template * @param channel the normalized channel @@ -125,10 +125,10 @@ export async function runQueryForSurface( * @param branch the branch slug * @param startDate the experiment start date * @param endDate the experiment proposed end date - * @returns a CTR percent value for a message if the Looker query results are + * @returns a UCTR percent value for a message if the Looker query results are * defined */ -export async function getCTRPercentData( +export async function getUCTRPercentData( id: string, platform: Platform, template: string, @@ -137,10 +137,10 @@ export async function getCTRPercentData( branch?: string, startDate?: string | null, endDate?: string | null, -): Promise { +): Promise { switch (platform) { case "fenix": - return getAndroidCTRPercentData( + return getAndroidUCTRPercentData( id, template, channel, @@ -150,7 +150,7 @@ export async function getCTRPercentData( endDate, ); default: - return getDesktopCTRPercentData( + return getDesktopUCTRPercentData( id, template, channel, @@ -162,7 +162,7 @@ export async function getCTRPercentData( } } -export async function getAndroidCTRPercentData( +export async function getAndroidUCTRPercentData( id: string, template: string, channel?: string, @@ -170,7 +170,7 @@ export async function getAndroidCTRPercentData( branch?: string, startDate?: string | null, endDate?: string | null, -): Promise { +): Promise { // XXX the filters are currently defined to match the filters in getDashboard. // It would be more ideal to consider a different approach when definining // those filters to sync up the data in both places. Non-trivial changes to @@ -200,7 +200,7 @@ export async function getAndroidCTRPercentData( ); if (queryResult?.length > 0) { - // CTR percents will have 2 decimal places since this is what is expected + // UCTR percents will have 2 decimal places since this is what is expected // from Experimenter analyses. const clientCount = queryResult[0]["events.client_count"]; const eventName = clientCount["events.event_name"]; @@ -208,10 +208,10 @@ export async function getAndroidCTRPercentData( const primaryRate = queryResult[0].primary_rate; - const ctrPercent = getSafeCtrPercent(primaryRate); + const uctrPercent = getSafeUctrPercent(primaryRate); return { - ctrPercent: ctrPercent, + uctrPercent: uctrPercent, impressions: impressions * 10, // We need to extrapolate real numbers for the 10% sample }; } @@ -220,7 +220,7 @@ export async function getAndroidCTRPercentData( return undefined; } -export async function getDesktopCTRPercentData( +export async function getDesktopUCTRPercentData( id: string, template: string, channel?: string, @@ -228,7 +228,7 @@ export async function getDesktopCTRPercentData( branch?: string, startDate?: string | null, endDate?: string | null, -): Promise { +): Promise { // XXX the filters are currently defined to match the filters in getDashboard. // It would be more ideal to consider a different approach when definining // those filters to sync up the data in both places. Non-trivial changes to @@ -263,7 +263,7 @@ export async function getDesktopCTRPercentData( } if (queryResult?.length > 0) { - // CTR percents will have 2 decimal places since this is what is expected + // UCTR percents will have 2 decimal places since this is what is expected // from Experimenter analyses. let impressions; if (template === "infobar") { @@ -279,10 +279,10 @@ export async function getDesktopCTRPercentData( const primaryRate = queryResult[0].primary_rate; - const ctrPercent = getSafeCtrPercent(primaryRate); + const uctrPercent = getSafeUctrPercent(primaryRate); return { - ctrPercent: ctrPercent, + uctrPercent: uctrPercent, impressions: impressions, }; } diff --git a/lib/nimbusRecipe.ts b/lib/nimbusRecipe.ts index dec52a40..6e6d1a42 100644 --- a/lib/nimbusRecipe.ts +++ b/lib/nimbusRecipe.ts @@ -171,7 +171,7 @@ export class NimbusRecipe implements NimbusRecipeType { formattedEndDate = formatDate(branchInfo.nimbusExperiment.endDate, 1); } - branchInfo.ctrDashboardLink = getAndroidDashboardLink( + branchInfo.uctrDashboardLink = getAndroidDashboardLink( branchInfo.template as string, branchInfo.id, undefined, @@ -182,7 +182,7 @@ export class NimbusRecipe implements NimbusRecipeType { this._isCompleted, ); - console.log("Android Dashboard: ", branchInfo.ctrDashboardLink); + console.log("Android Dashboard: ", branchInfo.uctrDashboardLink); return branchInfo; } @@ -482,7 +482,7 @@ export class NimbusRecipe implements NimbusRecipeType { if (branchInfo.nimbusExperiment.endDate) { formattedEndDate = formatDate(branchInfo.nimbusExperiment.endDate, 1); } - branchInfo.ctrDashboardLink = getDesktopDashboardLink( + branchInfo.uctrDashboardLink = getDesktopDashboardLink( branch.template, branchInfo.id, undefined, @@ -537,8 +537,8 @@ export class NimbusRecipe implements NimbusRecipeType { product: "Desktop", id: this._rawRecipe.slug, segment: "some segment", - ctrPercent: 0.5, // get me from BigQuery - ctrPercentChange: 2, // get me from BigQuery + uctrPercent: 0.5, // get me from BigQuery + uctrPercentChange: 2, // get me from BigQuery metrics: "some metrics", experimenterLink: `https://experimenter.services.mozilla.com/nimbus/${this._rawRecipe.slug}`, userFacingName: this._rawRecipe.userFacingName, diff --git a/lib/nimbusRecipeCollection.ts b/lib/nimbusRecipeCollection.ts index de713886..9dfdcd3a 100644 --- a/lib/nimbusRecipeCollection.ts +++ b/lib/nimbusRecipeCollection.ts @@ -1,6 +1,6 @@ import { NimbusRecipe } from "../lib/nimbusRecipe"; import { BranchInfo, RecipeInfo, RecipeOrBranchInfo } from "@/app/columns"; -import { getCTRPercentData } from "./looker"; +import { getUCTRPercentData } from "./looker"; import { getExperimentLookerDashboardDate } from "./lookerUtils"; import { Platform } from "./types"; @@ -15,9 +15,9 @@ type NimbusRecipeCollectionType = { }; /** - * @returns an array of BranchInfo with updated CTR percents for the recipe + * @returns an array of BranchInfo with updated UCTR percents for the recipe */ -async function updateBranchesCTR(recipe: NimbusRecipe): Promise { +async function updateBranchesUCTR(recipe: NimbusRecipe): Promise { return await Promise.all( recipe .getBranchInfos() @@ -31,7 +31,7 @@ async function updateBranchesCTR(recipe: NimbusRecipe): Promise { ); // We are making all branch ids upper case to make up for // Looker being case sensitive - const ctrPercentData = await getCTRPercentData( + const uctrPercentData = await getUCTRPercentData( branchInfo.id, branchInfo.nimbusExperiment.appName, branchInfo.template!, @@ -41,9 +41,9 @@ async function updateBranchesCTR(recipe: NimbusRecipe): Promise { branchInfo.nimbusExperiment.startDate, proposedEndDate, ); - if (ctrPercentData) { - branchInfo.ctrPercent = ctrPercentData.ctrPercent; - branchInfo.impressions = ctrPercentData.impressions; + if (uctrPercentData) { + branchInfo.uctrPercent = uctrPercentData.uctrPercent; + branchInfo.impressions = uctrPercentData.impressions; } return branchInfo; }), @@ -92,15 +92,15 @@ export class NimbusRecipeCollection implements NimbusRecipeCollectionType { /** * @returns a list of RecipeInfo of recipes in this collection with updated - * ctrPercent properties + * uctrPercent properties */ async getExperimentAndBranchInfos(): Promise { return await Promise.all( this.recipes.map(async (recipe: NimbusRecipe): Promise => { let updatedRecipe = recipe.getRecipeInfo(); - // Update all branches with CTR data for the recipe - updatedRecipe.branches = await updateBranchesCTR(recipe); + // Update all branches with UCTR data for the recipe + updatedRecipe.branches = await updateBranchesUCTR(recipe); return updatedRecipe; }), From f8c74116f072e469ce2cc6363b25b2ec05b13632 Mon Sep 17 00:00:00 2001 From: Dan Mosedale Date: Tue, 29 Jul 2025 10:11:15 -0400 Subject: [PATCH 2/2] Update CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aae64430..6e9c4e2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Tuesday, July 29th, 2025 + +- Corrected the UX to change what we've been calling "CTR" to what it actually is: "UCTR". Meaning: + - Unique CTR (UCTR): Unique Users Who Clicked at Least Once / Unique Users Who Saw the Ad. Sometimes also referred to as User-level CTR. + - CTR: Clicks / Impressions + ## Tuesday, May 6th, 2025 ### Added