Skip to content

Commit ebe8b8e

Browse files
committed
Generate Normandy IDs to guarantee enrollment
Replaces the Force Enroll with a new Actions column that contains a drop down menu with several actions, like "Force enroll" and "generate test ids".
1 parent db5ac2c commit ebe8b8e

13 files changed

+239
-108
lines changed

src/apis/nimbus.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,16 @@ var nimbus = class extends ExtensionAPI {
270270
throw error;
271271
}
272272
},
273+
274+
async generateTestIds(recipe, branchSlug) {
275+
try {
276+
const result = await ExperimentManager.generateTestIds(recipe);
277+
return result[branchSlug];
278+
} catch (error) {
279+
console.error(error);
280+
throw error;
281+
}
282+
},
273283
},
274284
},
275285
};

src/apis/nimbus.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,26 @@
170170
"description": "The slug of the experiment to delete from the store."
171171
}
172172
]
173+
},
174+
175+
{
176+
"name": "generateTestIds",
177+
"type": "function",
178+
"description": "Generate Normandy UserId respective to a branch for a specific experiment.",
179+
"async": true,
180+
"parameters": [
181+
{
182+
"name": "recipe",
183+
"type": "object",
184+
"additionalProperties": true,
185+
"description": "The recipe of the experiment to generate the UserId for."
186+
},
187+
{
188+
"name": "branchSlug",
189+
"type": "string",
190+
"description": "The specific branch slug of the experiment."
191+
}
192+
]
173193
}
174194
],
175195
"events": []

src/types/global.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,6 @@ declare namespace browser.experiments.nimbus {
8585
function unenroll(slug: string): Promise<void>;
8686

8787
function deleteInactiveEnrollment(slug: string): Promise<void>;
88+
89+
function generateTestIds(recipe: object, branchSlug: string): Promise<string>;
8890
}

src/ui/components/DropdownMenu.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ const DropdownMenu: FC<DropdownMenuProps> = ({ onSelectFeatureConfigId }) => {
1717
const configs = await browser.experiments.nimbus.getFeatureConfigs();
1818
setFeatureConfigs(configs);
1919
} catch (error) {
20-
addToast(
21-
`Error fetching feature configurations: ${(error as Error).message ?? String(error)}`,
22-
"danger",
23-
);
20+
addToast({
21+
message: `Error fetching feature configurations: ${(error as Error).message ?? String(error)}`,
22+
variant: "danger",
23+
});
2424
}
2525
})();
2626
}, [addToast]);

src/ui/components/ExperimentBrowserPage.tsx

Lines changed: 115 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import { FC, useCallback, useEffect, useState } from "react";
22
import { NimbusExperiment } from "@mozilla/nimbus-schemas";
3-
import { Table, Button, Container, Form, Row, Col } from "react-bootstrap";
3+
import {
4+
Table,
5+
Button,
6+
Container,
7+
Form,
8+
Row,
9+
Col,
10+
Dropdown,
11+
} from "react-bootstrap";
412

513
import { useToastsContext } from "../hooks/useToasts";
614

@@ -39,10 +47,10 @@ const ExperimentBrowserPage: FC = () => {
3947
);
4048
setExperiments(fetchedExperiments);
4149
} catch (error) {
42-
addToast(
43-
`Error fetching experiments: ${(error as Error).message ?? String(error)}`,
44-
"danger",
45-
);
50+
addToast({
51+
message: `Error fetching experiments: ${(error as Error).message ?? String(error)}`,
52+
variant: "danger",
53+
});
4654
}
4755
},
4856
[environment, status, addToast],
@@ -61,18 +69,56 @@ const ExperimentBrowserPage: FC = () => {
6169
branchSlug,
6270
);
6371
if (result) {
64-
addToast("Enrollment successful", "success");
72+
addToast({ message: "Enrollment successful", variant: "success" });
6573
} else {
66-
addToast("Enrollment failed", "danger");
74+
addToast({ message: "Enrollment failed", variant: "danger" });
6775
}
6876
} catch (error) {
69-
addToast(
70-
`Error enrolling into experiment: ${(error as Error).message ?? String(error)}`,
71-
"danger",
77+
addToast({
78+
message: `Error enrolling into experiment: ${(error as Error).message ?? String(error)}`,
79+
variant: "danger",
80+
});
81+
}
82+
} else {
83+
addToast({
84+
message: "Select a branch before enrolling",
85+
variant: "danger",
86+
});
87+
}
88+
};
89+
90+
const handleGenerateTestIds = async (
91+
experimentId: string,
92+
branchSlug: string,
93+
) => {
94+
if (branchSlug) {
95+
const recipe = experiments.find((exp) => exp.id === experimentId);
96+
try {
97+
const result = await browser.experiments.nimbus.generateTestIds(
98+
recipe,
99+
branchSlug,
72100
);
101+
if (result) {
102+
await navigator.clipboard.writeText(result);
103+
addToast({
104+
message: `Id successfully generated and copied to clipboard. Test Id: ${result}`,
105+
variant: "success",
106+
autohide: true,
107+
});
108+
} else {
109+
addToast({ message: "Test Id generation failed", variant: "danger" });
110+
}
111+
} catch (error) {
112+
addToast({
113+
message: `Error generating test Id: ${(error as Error).message ?? String(error)}`,
114+
variant: "danger",
115+
});
73116
}
74117
} else {
75-
addToast("Select a branch before enrolling", "danger");
118+
addToast({
119+
message: "Select a branch before generating test Id",
120+
variant: "danger",
121+
});
76122
}
77123
};
78124

@@ -129,7 +175,7 @@ const ExperimentBrowserPage: FC = () => {
129175
<th className="text-center primary-fg light-bg">Channel</th>
130176
<th className="text-center primary-fg light-bg">Version</th>
131177
<th className="text-center primary-fg light-bg">Status</th>
132-
<th className="text-center primary-fg light-bg">Force Enroll</th>
178+
<th className="text-center primary-fg light-bg">Actions</th>
133179
</tr>
134180
</thead>
135181
<tbody>
@@ -151,28 +197,63 @@ const ExperimentBrowserPage: FC = () => {
151197
: "Enrollment Paused"}
152198
</td>
153199
<td className="text-end align-middle wide-column">
154-
<Form.Select
155-
value={selectedBranches[experiment.id]}
156-
onChange={(e) =>
157-
handleBranchChange(experiment.id, e.target.value)
158-
}
159-
className="grey-border small-font rounded p-2 m-0 font-monospace"
160-
>
161-
<option value="">Select branch</option>
162-
{experiment.branches?.map((branch) => (
163-
<option key={branch.slug} value={branch.slug}>
164-
{branch.slug}
165-
</option>
166-
))}
167-
</Form.Select>
168-
<Button
169-
className="option-button primary-fg py-1 my-1 rounded small-font fw-bold grey-border light-bg"
170-
onClick={() =>
171-
handleEnroll(experiment.id, selectedBranches[experiment.id])
172-
}
173-
>
174-
Enroll
175-
</Button>
200+
<Container className="d-flex align-items-center">
201+
<Form.Select
202+
value={selectedBranches[experiment.id]}
203+
onChange={(e) =>
204+
handleBranchChange(experiment.id, e.target.value)
205+
}
206+
className="grey-border small-font rounded p-2 m-0 font-monospace"
207+
>
208+
<option value="">Select branch</option>
209+
{experiment.branches?.map((branch) => (
210+
<option key={branch.slug} value={branch.slug}>
211+
{branch.slug}
212+
</option>
213+
))}
214+
</Form.Select>
215+
{status === "Live" ? (
216+
<Dropdown>
217+
<Dropdown.Toggle className="option-button primary-fg py-2 my-1 mx-2 rounded small-font fw-bold grey-border light-bg">
218+
Actions
219+
</Dropdown.Toggle>
220+
<Dropdown.Menu>
221+
<Dropdown.Item
222+
onClick={() =>
223+
handleEnroll(
224+
experiment.id,
225+
selectedBranches[experiment.id],
226+
)
227+
}
228+
>
229+
Force Enroll
230+
</Dropdown.Item>
231+
<Dropdown.Item
232+
onClick={() =>
233+
handleGenerateTestIds(
234+
experiment.id,
235+
selectedBranches[experiment.id],
236+
)
237+
}
238+
>
239+
Generate Test IDs
240+
</Dropdown.Item>
241+
</Dropdown.Menu>
242+
</Dropdown>
243+
) : (
244+
<Button
245+
className="option-button primary-fg py-0 my-1 mx-1 rounded small-font fw-bold grey-border light-bg"
246+
onClick={() =>
247+
handleEnroll(
248+
experiment.id,
249+
selectedBranches[experiment.id],
250+
)
251+
}
252+
>
253+
Force Enroll
254+
</Button>
255+
)}
256+
</Container>
176257
</td>
177258
</tr>
178259
))}

src/ui/components/ExperimentStorePage.tsx

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ const ExperimentStorePage: FC = () => {
2222
await browser.experiments.nimbus.getExperimentStore();
2323
setExperiments(experimentStore as NimbusEnrollment[]);
2424
} catch (error) {
25-
addToast(
26-
`Error fetching experiments: ${(error as Error).message ?? String(error)}`,
27-
"danger",
28-
);
25+
addToast({
26+
message: `Error fetching experiments: ${(error as Error).message ?? String(error)}`,
27+
variant: "danger",
28+
});
2929
}
3030
}, [experiments, addToast]);
3131

@@ -37,13 +37,13 @@ const ExperimentStorePage: FC = () => {
3737
async (slug: string) => {
3838
try {
3939
await browser.experiments.nimbus.unenroll(slug);
40-
addToast("Unenrollment successful", "success");
40+
addToast({ message: "Unenrollment successful", variant: "success" });
4141
await fetchExperiments();
4242
} catch (error) {
43-
addToast(
44-
`Error unenrolling from experiment: ${(error as Error).message ?? String(error)}`,
45-
"danger",
46-
);
43+
addToast({
44+
message: `Error unenrolling from experiment: ${(error as Error).message ?? String(error)}`,
45+
variant: "danger",
46+
});
4747
}
4848
},
4949
[fetchExperiments, addToast],
@@ -53,13 +53,13 @@ const ExperimentStorePage: FC = () => {
5353
async (slug: string) => {
5454
try {
5555
await browser.experiments.nimbus.deleteInactiveEnrollment(slug);
56-
addToast("Deletion successful", "success");
56+
addToast({ message: "Deletion successful", variant: "success" });
5757
await fetchExperiments();
5858
} catch (error) {
59-
addToast(
60-
`Error deleting experiment: ${(error as Error).message ?? String(error)}`,
61-
"danger",
62-
);
59+
addToast({
60+
message: `Error deleting experiment: ${(error as Error).message ?? String(error)}`,
61+
variant: "danger",
62+
});
6363
}
6464
},
6565
[fetchExperiments, addToast],

src/ui/components/FeatureConfigPage.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ const FeatureConfigPage: FC = () => {
3030

3131
const handleEnrollClick = useCallback(async () => {
3232
if (selectedFeatureId === "") {
33-
addToast("Invalid Input: Select feature", "danger");
33+
addToast({ message: "Invalid Input: Select feature", variant: "danger" });
3434
} else if (jsonInput === "") {
35-
addToast("Invalid Input: Enter JSON", "danger");
35+
addToast({ message: "Invalid Input: Enter JSON", variant: "danger" });
3636
} else {
3737
try {
3838
const result = await browser.experiments.nimbus.enrollWithFeatureConfig(
@@ -42,21 +42,21 @@ const FeatureConfigPage: FC = () => {
4242
);
4343

4444
if (result) {
45-
addToast("Enrollment successful", "success");
45+
addToast({ message: "Enrollment successful", variant: "success" });
4646
} else {
47-
addToast("Enrollment failed", "danger");
47+
addToast({ message: "Enrollment failed", variant: "danger" });
4848
}
4949
} catch (error) {
50-
addToast(
51-
`Error enrolling into experiment: ${(error as Error).message ?? String(error)}`,
52-
"danger",
53-
);
50+
addToast({
51+
message: `Error enrolling into experiment: ${(error as Error).message ?? String(error)}`,
52+
variant: "danger",
53+
});
5454
}
5555
}
5656
}, [jsonInput, selectedFeatureId, isRollout, addToast]);
5757

5858
return (
59-
<Container className="main-content p-2">
59+
<Container className="main-content p-2 overflow-hidden">
6060
<Form>
6161
<Row className="align-items-stretch">
6262
<Col md={9}>

src/ui/components/JEXLDebuggerPage.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ const JEXLDebuggerPage: FC = () => {
2222
const context = await browser.experiments.nimbus.getClientContext();
2323
setClientContext(context);
2424
} catch (error) {
25-
addToast(
26-
`Error fetching client context: ${(error as Error).message ?? String(error)}`,
27-
"danger",
28-
);
25+
addToast({
26+
message: `Error fetching client context: ${(error as Error).message ?? String(error)}`,
27+
variant: "danger",
28+
});
2929
}
3030
}, [addToast]);
3131

@@ -50,10 +50,10 @@ const JEXLDebuggerPage: FC = () => {
5050
}
5151
} catch (error) {
5252
setOutput("Error evaluating expression");
53-
addToast(
54-
`Error evaluating expression: ${(error as Error).message ?? String(error)}`,
55-
"danger",
56-
);
53+
addToast({
54+
message: `Error evaluating expression: ${(error as Error).message ?? String(error)}`,
55+
variant: "danger",
56+
});
5757
}
5858
}, [jexlExpression, clientContext, addToast]);
5959

0 commit comments

Comments
 (0)