Skip to content
Draft
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
76 changes: 38 additions & 38 deletions __tests__/app/message-table.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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(<MessageTable columns={experimentColumns} data={recipeInfos} />);

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(<MessageTable columns={experimentColumns} data={[recipeInfo]} />);

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", () => {
Expand All @@ -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();
});

Expand Down Expand Up @@ -340,48 +340,48 @@ 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",
template: "test template",
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(
<MessageTable columns={fxmsMessageColumns} data={[fakeMsgInfo]} />,
);

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",
template: "test template",
surface: "test surface",
segment: "test segment",
metrics: "test metrics",
ctrDashboardLink: "test link",
uctrDashboardLink: "test link",
};
render(
<MessageTable columns={fxmsMessageColumns} data={[fakeMsgInfo]} />,
);

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();
});

Expand All @@ -398,10 +398,10 @@ describe("MessageTable", () => {
<MessageTable columns={fxmsMessageColumns} data={[fxmsMsgInfo]} />,
);

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();
});

Expand Down Expand Up @@ -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 = {
Expand All @@ -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 = {
Expand All @@ -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(
Expand Down Expand Up @@ -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 = {
Expand All @@ -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 = {
Expand All @@ -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,
};

Expand Down Expand Up @@ -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 = {
Expand All @@ -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 = {
Expand All @@ -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,
};

Expand Down Expand Up @@ -612,7 +612,7 @@ describe("MessageTable", () => {
surface: "test surface",
segment: "test segment",
metrics: "test metrics",
ctrDashboardLink: "test link",
uctrDashboardLink: "test link",
};
render(
<MessageTable columns={fxmsMessageColumns} data={[fakeMsgInfo]} />,
Expand Down
44 changes: 22 additions & 22 deletions __tests__/lib/looker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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();
});
});

Expand Down
10 changes: 5 additions & 5 deletions __tests__/lib/nimbusRecipe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -319,7 +319,7 @@ describe("NimbusRecipe", () => {
template: "survey",
screenshots: ["screenshotURI"],
description: "control description",
ctrDashboardLink: dashboardLink,
uctrDashboardLink: dashboardLink,
});
});

Expand Down
Loading