Merge pull request #1602 from mozilla/fxa-monitor-exp
Opt-in/out FxA Experiment
This commit is contained in:
Коммит
84b5b5bdb7
|
@ -13,6 +13,12 @@ async function home(req, res) {
|
|||
csrfToken: req.csrfToken(),
|
||||
};
|
||||
|
||||
const coinFlipNumber = Math.random() * 100;
|
||||
const experimentBranch = getExperimentBranch(req, coinFlipNumber);
|
||||
|
||||
const isUserInExperiment = (experimentBranch === "vb");
|
||||
const experimentBranchB = (experimentBranch === "vb" && isUserInExperiment);
|
||||
|
||||
let featuredBreach = null;
|
||||
let scanFeaturedBreach = false;
|
||||
|
||||
|
@ -41,6 +47,8 @@ async function home(req, res) {
|
|||
scanFeaturedBreach,
|
||||
pageToken: formTokens.pageToken,
|
||||
csrfToken: formTokens.csrfToken,
|
||||
experimentBranch,
|
||||
experimentBranchB,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -50,9 +58,39 @@ async function home(req, res) {
|
|||
scanFeaturedBreach,
|
||||
pageToken: formTokens.pageToken,
|
||||
csrfToken: formTokens.csrfToken,
|
||||
experimentBranch,
|
||||
isUserInExperiment,
|
||||
experimentBranchB,
|
||||
});
|
||||
}
|
||||
|
||||
function getExperimentBranch(req, sorterNum) {
|
||||
|
||||
if (req.headers && !req.headers["accept-language"].includes("en") ){
|
||||
return false;
|
||||
}
|
||||
|
||||
// If URL param has experimentBranch entry, use that branch;
|
||||
if (req.query.experimentBranch) {
|
||||
if (!["va", "vb"].includes(req.query.experimentBranch)) {
|
||||
return false;
|
||||
}
|
||||
req.session.experimentBranch = req.query.experimentBranch;
|
||||
return req.query.experimentBranch;
|
||||
}
|
||||
|
||||
// If user was already assigned a branch, stay in that branch;
|
||||
if (req.session.experimentBranch) { return req.session.experimentBranch; }
|
||||
|
||||
// Split into two categories
|
||||
if (sorterNum <= 50) {
|
||||
req.session.experimentBranch = "vb";
|
||||
return "vb";
|
||||
}
|
||||
|
||||
return "va";
|
||||
}
|
||||
|
||||
function getAllBreaches(req, res) {
|
||||
return res.render("top-level-page", {
|
||||
title: "Firefox Monitor",
|
||||
|
|
|
@ -12,9 +12,14 @@ const HIBP = require("../hibp");
|
|||
const mozlog = require("../log");
|
||||
const sha1 = require("../sha1-utils");
|
||||
|
||||
|
||||
const log = mozlog("controllers.oauth");
|
||||
|
||||
const utmArray = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
|
||||
|
||||
function getUTMNames() {
|
||||
return utmArray;
|
||||
}
|
||||
|
||||
function init(req, res, next, client = FxAOAuthClient) {
|
||||
// Set a random state string in a cookie so that we can verify
|
||||
// the user when they're redirected back to us after auth.
|
||||
|
@ -23,12 +28,18 @@ function init(req, res, next, client = FxAOAuthClient) {
|
|||
const url = new URL(client.code.getUri({state}));
|
||||
const fxaParams = new URL(req.url, AppConstants.SERVER_URL);
|
||||
|
||||
req.session.utmContents = {};
|
||||
|
||||
url.searchParams.append("access_type", "offline");
|
||||
url.searchParams.append("action", "email");
|
||||
|
||||
for (const param of fxaParams.searchParams.keys()) {
|
||||
if (utmArray.includes(param)) {
|
||||
req.session.utmContents[param] = fxaParams.searchParams.get(param);
|
||||
}
|
||||
url.searchParams.append(param, fxaParams.searchParams.get(param));
|
||||
}
|
||||
|
||||
res.redirect(url);
|
||||
}
|
||||
|
||||
|
@ -53,6 +64,16 @@ async function confirmed(req, res, next, client = FxAOAuthClient) {
|
|||
const existingUser = await DB.getSubscriberByEmail(email);
|
||||
req.session.user = existingUser;
|
||||
|
||||
const returnURL = new URL("/user/dashboard", AppConstants.SERVER_URL);
|
||||
|
||||
if (req.session.utmContents) {
|
||||
getUTMNames().forEach(param => {
|
||||
if (req.session.utmContents[param]) {
|
||||
returnURL.searchParams.append(param, req.session.utmContents[param]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user is signing up or signing in,
|
||||
// then add new users to db and send email.
|
||||
if (!existingUser || existingUser.fxa_refresh_token === null) {
|
||||
|
@ -67,7 +88,7 @@ async function confirmed(req, res, next, client = FxAOAuthClient) {
|
|||
unsafeBreachesForEmail = await HIBP.getBreachesForEmail(
|
||||
sha1(email),
|
||||
req.app.locals.breaches,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
const utmID = "report";
|
||||
|
@ -90,11 +111,12 @@ async function confirmed(req, res, next, client = FxAOAuthClient) {
|
|||
}
|
||||
);
|
||||
req.session.user = verifiedSubscriber;
|
||||
return res.redirect("/user/dashboard");
|
||||
|
||||
return res.redirect(returnURL.pathname + returnURL.search);
|
||||
}
|
||||
// Update existing user's FxA data
|
||||
await DB._updateFxAData(existingUser, fxaUser.accessToken, fxaUser.refreshToken, fxaProfileData);
|
||||
res.redirect("/user/dashboard");
|
||||
res.redirect(returnURL.pathname + returnURL.search);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -552,7 +552,15 @@ async function getBreachStats(req, res) {
|
|||
|
||||
|
||||
function logout(req, res) {
|
||||
req.session.reset();
|
||||
if (req.session.experimentBranch) {
|
||||
// Persist experimentBranch across session reset
|
||||
const experimentBranch = req.session.experimentBranch;
|
||||
req.session.reset();
|
||||
req.session.experimentBranch = experimentBranch;
|
||||
} else {
|
||||
req.session.reset();
|
||||
}
|
||||
|
||||
res.redirect("/");
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
.temp-exp {
|
||||
position: fixed;
|
||||
background: purple;
|
||||
color: white;
|
||||
width: auto;
|
||||
top: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
z-index: 9999;
|
||||
padding: 0.5em 1em;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.input-group.create-fxa-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 248px;
|
||||
margin: 4px auto 16px;
|
||||
}
|
||||
|
||||
.create-fxa-wrapper p {
|
||||
text-align: left;
|
||||
color: var(--grey3);
|
||||
margin: 0;
|
||||
padding: 0 0 0 12px;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.create-fxa-checkbox-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.create-fxa-checkbox-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.create-fxa-checkbox-checkmark:hover {
|
||||
background: #cfcfd8;
|
||||
}
|
||||
|
||||
.create-fxa-checkbox-checkmark {
|
||||
position: relative;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.create-fxa-checkbox-checkmark::after {
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: none;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-radius: 4px;
|
||||
background-size: 100% auto;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url("/img/svg/purple-check.svg");
|
||||
}
|
||||
|
||||
.create-fxa-checkbox-input:checked ~ .create-fxa-checkbox-checkmark::after {
|
||||
display: block;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#9059ff" d="M6 14a1 1 0 0 1-.707-.293l-3-3a1 1 0 0 1 1.414-1.414l2.157 2.157 6.316-9.023a1 1 0 0 1 1.639 1.146l-7 10a1 1 0 0 1-.732.427A.863.863 0 0 1 6 14z"></path></svg>
|
После Ширина: | Высота: | Размер: 267 B |
|
@ -153,7 +153,7 @@ function sendRecommendationPings(ctaSelector) {
|
|||
});
|
||||
|
||||
|
||||
document.querySelectorAll(".open-oauth").forEach( async(el) => {
|
||||
document.querySelectorAll(".open-oauth, .add-metrics-flow-values").forEach( async(el) => {
|
||||
const fxaUrl = new URL("/metrics-flow?", document.body.dataset.fxaAddress);
|
||||
|
||||
try {
|
||||
|
@ -204,5 +204,3 @@ function sendRecommendationPings(ctaSelector) {
|
|||
});
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
|
|
|
@ -36,17 +36,96 @@ function isValidEmail(val) {
|
|||
}
|
||||
|
||||
|
||||
function doOauth(el) {
|
||||
function doOauth(el, {emailWatch = false} = {}) {
|
||||
// Growth Experiment: To sidestep the breach scans form, we have to check if
|
||||
// there is an email address entered into #scan-user-email form.
|
||||
// If so, we set it similiar to what would happen on form submit.
|
||||
//
|
||||
// Options was added to limit how the watched email input field is injected
|
||||
// in this function. It only moves it if options.emailWatch is set to TRUE;
|
||||
|
||||
let url = new URL("/oauth/init", document.body.dataset.serverUrl);
|
||||
url = getFxaUtms(url);
|
||||
["flowId", "flowBeginTime", "entrypoint"].forEach(key => {
|
||||
url.searchParams.append(key, encodeURIComponent(el.dataset[key]));
|
||||
});
|
||||
if (sessionStorage && sessionStorage.length > 0) {
|
||||
const lastScannedEmail = sessionStorage.getItem(`scanned_${sessionStorage.length}`);
|
||||
if (lastScannedEmail) {
|
||||
url.searchParams.append("email", lastScannedEmail);
|
||||
|
||||
["flowId", "flowBeginTime", "entrypoint", "entrypoint_experiment", "entrypoint_variation", "form_type"].forEach(key => {
|
||||
if (el.dataset[key]) {
|
||||
url.searchParams.append(key, encodeURIComponent(el.dataset[key]));
|
||||
}
|
||||
});
|
||||
|
||||
if (!sessionStorage) {
|
||||
window.location.assign(url);
|
||||
return;
|
||||
}
|
||||
|
||||
const scannedEmailId = document.querySelector("#scan-user-email input[name=scannedEmailId]");
|
||||
|
||||
// Preserve entire control function
|
||||
if (!emailWatch) {
|
||||
if (sessionStorage && sessionStorage.length > 0) {
|
||||
|
||||
const lastScannedEmail = sessionStorage.getItem("lastScannedEmail");
|
||||
if (lastScannedEmail) {
|
||||
url.searchParams.append("email", lastScannedEmail);
|
||||
}
|
||||
}
|
||||
window.location.assign(url);
|
||||
return;
|
||||
}
|
||||
|
||||
// Growth Experiment: This logic is complex to handle the different scenarios of users logging into FxA.
|
||||
let email = false;
|
||||
|
||||
if (document.querySelector("#scan-user-email input[type=email]")) {
|
||||
email = document.querySelector("#scan-user-email input[type=email]").value;
|
||||
|
||||
if (!isValidEmail(email)) {
|
||||
email = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Growth Experiment: Reset UTMs from in-line body tag data elements.
|
||||
["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content" ].forEach(key => {
|
||||
if (document.body.dataset[key]) {
|
||||
url.searchParams.delete(key);
|
||||
url.searchParams.append(key, document.body.dataset[key]);
|
||||
}
|
||||
});
|
||||
|
||||
if (sessionStorage && sessionStorage.length > 0) {
|
||||
|
||||
const lastScannedEmail = sessionStorage.getItem("lastScannedEmail");
|
||||
|
||||
if (email && lastScannedEmail) {
|
||||
switch (email) {
|
||||
case lastScannedEmail:
|
||||
// The last saved email address and the current entry match, so route it to FxA
|
||||
email = lastScannedEmail;
|
||||
break;
|
||||
case !lastScannedEmail:
|
||||
// The last saved email address and the current entry DIFFER, so create
|
||||
// a new entry, launch a new FxA login session with new email prefilled.
|
||||
sessionStorage.removeItem("lastScannedEmail");
|
||||
sessionStorage.setItem("lastScannedEmail", email);
|
||||
scannedEmailId.value = sessionStorage.length;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (lastScannedEmail) {
|
||||
// Control method. User set this by checking breach results
|
||||
email = lastScannedEmail;
|
||||
}
|
||||
}
|
||||
} else if (email && sessionStorage) {
|
||||
// Applies to first time user in experiment has no previous FxA ties.
|
||||
sessionStorage.removeItem("lastScannedEmail");
|
||||
sessionStorage.setItem("lastScannedEmail", email);
|
||||
scannedEmailId.value = sessionStorage.length;
|
||||
}
|
||||
|
||||
// Append whichever email was set, and start OAuth flow!
|
||||
if (email) {
|
||||
url.searchParams.append("email", email);
|
||||
}
|
||||
window.location.assign(url);
|
||||
}
|
||||
|
@ -369,4 +448,72 @@ function addBentoObserver(){
|
|||
|
||||
const dropDownMenu = document.querySelector(".mobile-nav.show-mobile");
|
||||
dropDownMenu.addEventListener("click", () => toggleDropDownMenu(dropDownMenu));
|
||||
|
||||
if (document.body.dataset.experiment) {
|
||||
const submitBtn = document.querySelector("#scan-user-email input[type='submit']");
|
||||
const createFxaCheckbox = document.getElementById("createFxaCheckbox");
|
||||
|
||||
if (createFxaCheckbox) {
|
||||
createFxaCheckbox.addEventListener("change", (e)=> {
|
||||
document.body.dataset.utm_content = "opt-out";
|
||||
if (event.target.checked) {
|
||||
document.body.dataset.utm_content = "opt-in";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
submitBtn.addEventListener("click", (e)=> {
|
||||
document.body.dataset.utm_content = "opt-out";
|
||||
|
||||
// Email Validation
|
||||
const scanForm = document.getElementById("scan-user-email");
|
||||
const scanFormEmailValue = document.querySelector("#scan-user-email input[type='email']").value;
|
||||
|
||||
if (scanFormEmailValue.length < 1 || !isValidEmail(scanFormEmailValue)) {
|
||||
scanForm.classList.add("invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
if (createFxaCheckbox && createFxaCheckbox.checked) {
|
||||
// Applies only to Branches VB and VC, if the checkbox is CHECKED.
|
||||
e.preventDefault();
|
||||
|
||||
// Analytics
|
||||
document.body.dataset.utm_content = "opt-in";
|
||||
e.target.dataset.entrypoint = "fx-monitor-alert-me-blue-link";
|
||||
if (typeof(ga) !== "undefined") {
|
||||
ga("send", {
|
||||
hitType: "event",
|
||||
eventCategory: "growthuserflow1",
|
||||
eventAction: "opt-in",
|
||||
eventLabel: "fx-monitor-alert-me-blue-link",
|
||||
});
|
||||
}
|
||||
|
||||
doOauth(e.target, {emailWatch: true});
|
||||
return;
|
||||
}
|
||||
|
||||
const scanFormActionURL = new URL(scanForm.action);
|
||||
|
||||
["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content" ].forEach(key => {
|
||||
if (document.body.dataset[key]) {
|
||||
scanFormActionURL.searchParams.append(key, document.body.dataset[key]);
|
||||
}
|
||||
});
|
||||
|
||||
const revisedActionURL = scanFormActionURL.pathname + scanFormActionURL.search;
|
||||
|
||||
scanForm.action = revisedActionURL;
|
||||
|
||||
if (typeof(ga) !== "undefined") {
|
||||
ga("send", {
|
||||
hitType: "event",
|
||||
eventCategory: "growthuserflow1",
|
||||
eventAction: "opt-out",
|
||||
eventLabel: "fx-monitor-alert-me-blue-link",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -31,8 +31,14 @@ async function hashEmailAndSend(emailFormSubmitEvent) {
|
|||
// set unhashed email in client's sessionStorage and send key to server
|
||||
// so we can pluck these out later in scan-results and not lose them on back clicks
|
||||
if (sessionStorage) {
|
||||
sessionStorage.setItem(`scanned_${(sessionStorage.length + 1)}`, userEmail);
|
||||
emailForm.querySelector("input[name=scannedEmailId]").value = sessionStorage.length;
|
||||
|
||||
const lastScannedEmail = sessionStorage.getItem("lastScannedEmail");
|
||||
|
||||
if (!lastScannedEmail || lastScannedEmail !== userEmail) {
|
||||
sessionStorage.removeItem("lastScannedEmail");
|
||||
sessionStorage.setItem("lastScannedEmail", userEmail);
|
||||
emailForm.querySelector("input[name=scannedEmailId]").value = sessionStorage.length;
|
||||
}
|
||||
}
|
||||
|
||||
// clear input, send ping, and submit
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
(() => {
|
||||
if (document.getElementById("scannedEmail")) {
|
||||
const scannedEmail = document.getElementById("scannedEmail");
|
||||
const emailId = scannedEmail.dataset.scannedEmailId;
|
||||
scannedEmail.textContent = sessionStorage.getItem(`scanned_${emailId}`);
|
||||
scannedEmail.textContent = sessionStorage.getItem("lastScannedEmail");
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -95,6 +95,7 @@ const imgSrc = [
|
|||
"'self'",
|
||||
"https://www.google-analytics.com",
|
||||
"https://firefoxusercontent.com",
|
||||
"https://mozillausercontent.com/",
|
||||
"https://monitor.cdn.mozilla.net/",
|
||||
];
|
||||
|
||||
|
@ -103,6 +104,7 @@ const connectSrc = [
|
|||
"https://code.cdn.mozilla.net/fonts/",
|
||||
"https://www.google-analytics.com",
|
||||
"https://accounts.firefox.com",
|
||||
"https://accounts.stage.mozaws.net/metrics-flow",
|
||||
];
|
||||
|
||||
if (AppConstants.FXA_ENABLED) {
|
||||
|
|
|
@ -261,6 +261,10 @@ test("user add request with invalid email throws error", async () => {
|
|||
|
||||
test("user verify request with valid token but no session renders email verified page", async () => {
|
||||
const validToken = TEST_EMAIL_ADDRESSES.unverified_email_on_firefox_account.verification_token;
|
||||
const mockReturnedBreaches = testBreaches.slice(0,2);
|
||||
HIBP.subscribeHash = jest.fn();
|
||||
HIBP.getBreachesForEmail = jest.fn();
|
||||
HIBP.getBreachesForEmail.mockReturnValue(mockReturnedBreaches);
|
||||
|
||||
const req = httpMocks.createRequest({
|
||||
method: "GET",
|
||||
|
@ -283,6 +287,10 @@ test("user verify request with valid token verifies user and redirects to dashbo
|
|||
const validToken = TEST_EMAIL_ADDRESSES.unverified_email_on_firefox_account.verification_token;
|
||||
const testSubscriberEmail = "firefoxaccount@test.com";
|
||||
const testSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail);
|
||||
const mockReturnedBreaches = testBreaches.slice(0,2);
|
||||
HIBP.subscribeHash = jest.fn();
|
||||
HIBP.getBreachesForEmail = jest.fn();
|
||||
HIBP.getBreachesForEmail.mockReturnValue(mockReturnedBreaches);
|
||||
|
||||
const req = httpMocks.createRequest({
|
||||
method: "GET",
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<link rel="icon" href="/img/favicons/favicon-128.png" sizes="128x128" />
|
||||
<link rel="icon" href="/img/favicons/favicon-256.png" sizes="256x256" />
|
||||
</head>
|
||||
<body {{> analytics/default_dataset }} data-bento-app-id="fx-monitor">
|
||||
<body {{> analytics/default_dataset }} data-bento-app-id="fx-monitor" {{#if experimentBranch }} {{> analytics/experiment }} {{/if}}>
|
||||
{{> header/header }}
|
||||
{{{ body }}}
|
||||
{{> footer }}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
data-server-url="{{ SERVER_URL }}"
|
||||
data-logos-origin="{{ LOGOS_ORIGIN }}"
|
||||
|
||||
data-fxa-enabled="fxa-enabled"
|
||||
data-fxa-address={{ getFxaUrl }}
|
||||
data-utm_source="{{ SERVER_URL }}"
|
||||
data-utm_campaign="fx-monitor-fxa-integration"
|
||||
|
||||
|
||||
{{#if req.session.experimentBranch}}
|
||||
data-utm_source="{{ UTM_SOURCE }}"
|
||||
{{else}}
|
||||
data-utm_source="{{ SERVER_URL }}"
|
||||
{{/if}}
|
||||
|
||||
{{#if req.session.user}}
|
||||
data-signed-in-user="true"
|
||||
{{else}}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
data-utm_term={{experimentBranch}}
|
||||
data-experiment={{experimentBranch}}
|
||||
data-utm_content="{{#if experimentBranchB }}opt-out{{else}}opt-in{{/if}}"
|
||||
data-utm_campaign="growthuserflow1"
|
|
@ -7,6 +7,15 @@
|
|||
<input id="scan-email" class="input-group-field email-to-hash" type="email" name="email" placeholder="{{ getString "scan-placeholder" }}" aria-label="{{ getString "scan-placeholder" }}" autocomplete="off" />
|
||||
<span id="invalid-email-message" class="error-message">{{ getString "scan-error" }}</span>
|
||||
</div>
|
||||
{{#if isUserInExperiment}}
|
||||
<div class="input-group create-fxa-wrapper">
|
||||
<label for="createFxaCheckbox" class="create-fxa-checkbox-wrapper">
|
||||
<input {{#unless experimentBranchB }} checked {{/unless}} class="create-fxa-checkbox-input" id="createFxaCheckbox" type="checkbox" />
|
||||
<span class="create-fxa-checkbox-checkmark"></span>
|
||||
</label>
|
||||
<p>Stay safe: get automated alerts when an account is breached.</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
<input type="hidden" name="emailHash" />
|
||||
{{#if scanFeaturedBreach}}
|
||||
<input id="featured" type="hidden" name="featuredBreach" value="{{ featuredBreach.Name }}"/>
|
||||
|
@ -15,7 +24,7 @@
|
|||
{{#if featuredBreach}}
|
||||
<input type="submit" value="{{ getString "find-out" }}"/>
|
||||
{{else}}
|
||||
<input type="submit" value="{{ getString "check-for-breaches" }}"/>
|
||||
<input {{#if isUserInExperiment}} class="add-metrics-flow-values" data-entrypoint_experiment="growthuserflow1" data-entrypoint_variation={{ experimentBranch }} data-form_type="other" {{> analytics/fxa id="fx-monitor-check-for-breaches-blue-btn" }} {{/if}} type="submit" value="{{ getString "check-for-breaches" }}"/>
|
||||
{{/if}}
|
||||
{{> forms/loader }}
|
||||
</div>
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
<link rel="stylesheet" href="/css/breach-detail.css">
|
||||
<link rel="stylesheet" href="/css/breach-stats.css">
|
||||
<link rel="stylesheet" href="/css/dashboard.css">
|
||||
<link rel="stylesheet" href="/css/experiment.css">
|
||||
<link rel="stylesheet" href="/css/email-card.css">
|
||||
<link rel="stylesheet" href="/css/feature-tip-group.css">
|
||||
<link rel="stylesheet" href="/css/footer.css">
|
||||
|
|
Загрузка…
Ссылка в новой задаче