Merge pull request #1797 from mozilla/1789-join-the-party-experiment

Growth Experiment #5 - "Join the Party" Share Link
This commit is contained in:
Maxx Crawford 2020-08-17 15:23:50 -05:00 коммит произвёл GitHub
Родитель 929381c880 2fd1c68448
Коммит 978d4e1e3b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
34 изменённых файлов: 736 добавлений и 11 удалений

Просмотреть файл

@ -2,8 +2,11 @@
const HIBP = require("../hibp");
const DB = require("../db/DB");
const AppConstants = require("../app-constants");
const { changePWLinks } = require("../lib/changePWLinks");
const { getAllEmailsAndBreaches } = require("./user");
const { getExperimentFlags } = require("./utils");
const EXPERIMENTS_ENABLED = (AppConstants.EXPERIMENT_ACTIVE === "1");
async function getBreachDetail(req, res) {
const allBreaches = req.app.locals.breaches;
@ -35,11 +38,15 @@ async function getBreachDetail(req, res) {
}
const changePWLink = getChangePWLink(featuredBreach);
const experimentFlags = getExperimentFlags(req, EXPERIMENTS_ENABLED);
res.render("breach-detail", {
title: req.fluentFormat("home-title"),
featuredBreach,
changePWLink,
affectedEmails,
experimentFlags,
});
}

Просмотреть файл

@ -4,7 +4,12 @@ const AppConstants = require("../app-constants");
const DB = require("../db/DB");
const HIBP = require("../hibp");
const { scanResult } = require("../scan-results");
const { generatePageToken, getExperimentFlags } = require("./utils");
const {
generatePageToken,
getExperimentBranch,
getExperimentFlags,
getUTMContents,
} = require("./utils");
const EXPERIMENTS_ENABLED = (AppConstants.EXPERIMENT_ACTIVE === "1");
@ -17,7 +22,6 @@ function _getFeaturedBreach(allBreaches, breachQueryValue) {
}
async function home(req, res) {
const formTokens = {
pageToken: AppConstants.PAGE_TOKEN_TIMER > 0 ? generatePageToken(req) : "",
csrfToken: req.csrfToken(),
@ -30,9 +34,31 @@ async function home(req, res) {
return res.redirect("/user/dashboard");
}
// Rewrites the /share/{COLOR} links to /
if (req.session.redirectHome) {
req.session.redirectHome = false;
return res.redirect("/");
}
// Note - If utmOverrides get set, they are unenrolled from the experiment
const utmOverrides = getUTMContents(req);
const experimentFlags = getExperimentFlags(req, EXPERIMENTS_ENABLED);
// Growth Experiment
if (EXPERIMENTS_ENABLED) {
getExperimentBranch(req, false, ["en"], {
"va": 50,
"vb": 50,
});
}
if (req.params && req.params.breach) {
req.query.breach = req.params.breach;
}
if (req.query.breach) {
featuredBreach = _getFeaturedBreach(req.app.locals.breaches, req.query.breach);
if (!featuredBreach) {
@ -53,6 +79,7 @@ async function home(req, res) {
pageToken: formTokens.pageToken,
csrfToken: formTokens.csrfToken,
experimentFlags,
utmOverrides,
});
}
@ -63,6 +90,7 @@ async function home(req, res) {
pageToken: formTokens.pageToken,
csrfToken: formTokens.csrfToken,
experimentFlags,
utmOverrides,
});
}

Просмотреть файл

@ -14,6 +14,13 @@ const sha1 = require("../sha1-utils");
const log = mozlog("controllers.oauth");
// Growth Experiment
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.
@ -28,6 +35,10 @@ function init(req, res, next, client = FxAOAuthClient) {
url.searchParams.append("action", "email");
for (const param of fxaParams.searchParams.keys()) {
// Growth Experiment
if (utmArray.includes(param)) {
req.session.utmContents[param] = fxaParams.searchParams.get(param);
}
url.searchParams.append(param, fxaParams.searchParams.get(param));
}
@ -57,6 +68,15 @@ async function confirmed(req, res, next, client = FxAOAuthClient) {
const returnURL = new URL("/user/dashboard", AppConstants.SERVER_URL);
// Growth Experiment
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) {

Просмотреть файл

@ -12,7 +12,7 @@ const { resultsSummary } = require("../scan-results");
const sha1 = require("../sha1-utils");
const EXPERIMENTS_ENABLED = (AppConstants.EXPERIMENT_ACTIVE === "1");
const { getExperimentFlags } = require("./utils");
const { getExperimentFlags, getUTMContents } = require("./utils");
const FXA_MONITOR_SCOPE = "https://identity.mozilla.com/apps/monitor";
@ -229,7 +229,9 @@ async function getDashboard(req, res) {
const user = req.user;
const allBreaches = req.app.locals.breaches;
const { verifiedEmails, unverifiedEmails } = await getAllEmailsAndBreaches(user, allBreaches);
const utmOverrides = getUTMContents(req);
// Growth Experiment
const experimentFlags = getExperimentFlags(req, EXPERIMENTS_ENABLED);
let lastAddedEmail = null;
@ -248,6 +250,7 @@ async function getDashboard(req, res) {
unverifiedEmails,
whichPartial: "dashboards/breaches-dash",
experimentFlags,
utmOverrides,
});
}
@ -561,6 +564,18 @@ async function getBreachStats(req, res) {
function logout(req, res) {
// Growth Experiment
if (EXPERIMENTS_ENABLED && req.session.experimentFlags) {
// Persist experimentBranch across session reset
const sessionExperimentFlags = req.session.experimentFlags;
req.session.reset();
req.session.experimentFlags = sessionExperimentFlags;
// Return
res.redirect("/");
return;
}
req.session.reset();
res.redirect("/");
}

Просмотреть файл

@ -182,9 +182,23 @@ function getExperimentBranch(req, sorterNum = false, language = false, variation
}
function getUTMContents(req) {
if (!req) {
throw new Error("No request available");
}
// If UTMs are set previously, set them again.
if (req.session.utmOverrides) {
return req.session.utmOverrides;
}
req.session.utmOverrides = false;
return false;
}
function getExperimentFlags(req, EXPERIMENTS_ENABLED) {
if (!req) {
throw new Error("No request availabe");
throw new Error("No request available");
}
if (req.session.experimentFlags && EXPERIMENTS_ENABLED) {
@ -208,4 +222,5 @@ module.exports = {
hasUserSignedUpForRelay,
getExperimentBranch,
getExperimentFlags,
getUTMContents,
};

20
docs/experiments.md Normal file
Просмотреть файл

@ -0,0 +1,20 @@
_Last updated: Aug 8, 2020_
# Firefox Monitor Experiment Deployment Process
This is the experiment process plan for Firefox Monitor. It documents how to deploy experiments and take them down.
## ENV Variable
Use the variable `EXPERIMENT_ACTIVE` to enable experiments in your environment.
- If set to `1` - Experiment code is visible and active.
- If NOT set `1` – Experiment code is hidden and not active.
## Tagging System
To deploy with the ENV variable set to `1`, add `-exp` to your version tag.
Example: `v12.14.2-exp`
Any tag without the `-exp` will disable experiments at an ENV level.

Просмотреть файл

@ -15,6 +15,7 @@ const { FXA } = require("./lib/fxa");
const { FluentError } = require("./locale-utils");
const mozlog = require("./log");
const HIBP = require("./hibp");
const log = mozlog("middleware");
@ -173,6 +174,63 @@ async function requireSessionUser(req, res, next) {
next();
}
function getShareUTMs(req, res, next) {
// Step 1: See if the user needs to be redirected to the homepage or to a breach-detail page.
const generalShareUrls = [
"/share/orange", //Header
"/share/purple", // Footer
"/share/blue", // user/dashboard
"/share/",
];
if (generalShareUrls.includes(req.url)) {
// If not breach specific, redirect to "/"
req.session.redirectHome = true;
}
const inNotInActiveExperiment = (!req.session.experimentFlags);
// Excluse user from experiment if they don't have any experimentFlags set already.
if (inNotInActiveExperiment) {
// Step 2: Determine if user needs to have share-link UTMs set
const colors = [
"orange", //Header
"purple", // Footer
"blue", // user/dashboard
];
const urlArray = req.url.split("/");
const color = urlArray.slice(-1)[0];
req.session.utmOverrides = {
campaignName: "shareLinkTraffic",
campaignTerm: "default",
};
// Set Color Var in UTM
if (color.length && colors.includes(color)) {
req.session.utmOverrides.campaignTerm = color;
}
if (color.length && !colors.includes(color)) {
const allBreaches = req.app.locals.breaches;
const breachName = color;
const featuredBreach = HIBP.getBreachByName(allBreaches, breachName);
if (featuredBreach) {
req.session.utmOverrides.campaignTerm = featuredBreach.Name;
}
}
// Exclude share users
req.session.experimentFlags = {
excludeFromExperiment: true,
};
}
next();
}
module.exports = {
@ -185,4 +243,5 @@ module.exports = {
clientErrorHandler,
errorHandler,
requireSessionUser,
getShareUTMs,
};

232
public/css/share.css Normal file
Просмотреть файл

@ -0,0 +1,232 @@
.email-cards.experiment .share-monitor--dashboard {
display: block;
}
.email-cards.experiment .share-monitor--dashboard ~ .share-monitor--dashboard {
display: none;
}
.share-monitor--dashboard {
margin: 0 0 3rem;
display: none;
}
.share-monitor--dashboard h3 {
margin-bottom: 1rem;
}
.share-monitor--dashboard ~ .share-monitor--dashboard {
display: none;
}
.share-monitor--dashboard img {
display: block;
margin: 0 auto 1rem;
max-width: 100%;
}
.share-monitor--dashboard a {
font-size: 20px;
}
.share-monitor--breach {
margin: calc(var(--padding) * 2.5) auto 0;
border-radius: 0.7rem;
background: var(--monitorGradient);
padding: 2px;
}
.share-monitor--breach img {
margin-right: 2rem;
}
.share-monitor--breach .inset {
background: rgba(255, 255, 255, 1);
border-radius: 0.575rem;
padding: calc(var(--padding) * 1.5);
display: flex;
align-items: center;
justify-content: flex-start;
}
.share-monitor--breach h3 {
margin-bottom: 0.5rem;
}
.share-monitor--breach a {
font-size: 16px;
}
.share-monitor--modal {
padding: 0;
height: 100vh;
width: 100vw;
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 1000;
background: rgba(255, 255, 255, 0.8);
display: none;
box-sizing: border-box;
}
.share-monitor--modal-content {
background-color: var(--inkDark);
max-width: 800px;
width: 80vw;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 4px;
padding: 2rem;
color: #ededf0;
display: flex;
align-items: center;
justify-content: center;
background-image: url("/img/modal-bg-left.png"), url("/img/modal-bg-right.png");
background-repeat: no-repeat, no-repeat;
background-position: bottom left, bottom right;
/* 6:10 ratio */
background-size: 120px auto, 200px auto;
}
.share-monitor--modal-text {
max-width: 560px;
width: 100%;
padding: 1em 0;
}
.share-monitor--modal-text .hidden {
display: none;
}
.share-monitor--modal-text h2 {
color: #ffffff;
font-size: 32px;
margin: 0;
}
.share-monitor--modal-text p {
text-align: left;
display: inline-block;
}
.button-modal-close {
position: absolute;
top: 16px;
right: 16px;
height: 32px;
width: 32px;
border: none;
background-image: url("/img/svg/x-close-white.svg");
background-size: contain;
background-repeat: no-repeat;
background-position: center center;
border-radius: 50%;
}
.copy-form {
display: flex;
max-width: 400px;
width: 100%;
margin: 1em auto 0.5em;
}
.copy-form-btn {
background: var(--blue3);
color: white;
appearance: none;
-moz-appearance: none;
border: none;
padding: 0 1rem;
font-size: 16px;
height: 50px;
line-height: 50px;
display: block;
width: 80px;
text-align: center;
border-radius: 0 4px 4px 0;
}
.copy-form-btn span {
display: none;
}
.copy-form-input {
background: #ffffff;
padding: 0 0 0 1em;
appearance: none;
-moz-appearance: none;
display: block;
width: calc(100% - 80px);
max-width: 320px;
font-size: 16px;
border: none;
outline: none;
height: 50px;
line-height: 50px;
border-radius: 4px 0 0 4px;
}
.share-monitor--modal-content .disclaimer {
font-size: 0.75rem;
max-width: 340px;
margin-left: auto;
margin-right: auto;
text-align: center;
}
@media screen and (max-width: 600px) {
.share-monitor--dashboard img {
margin-top: 2rem;
}
.share-monitor--dashboard h3 {
font-size: 20px;
}
.share-monitor--dashboard a {
font-size: 18px;
}
.share-monitor--breach .inset {
display: block;
text-align: center;
}
.share-monitor--breach img {
margin: 0 0 1rem;
}
.share-monitor--modal-content {
width: 90vw;
background-image: none;
}
.copy-form {
display: block;
}
.copy-form-input {
width: 100%;
border-radius: 4px;
padding: 1em;
height: auto;
line-height: normal;
max-width: 100%;
}
.copy-form-btn {
margin-top: 1rem;
width: 100%;
border-radius: 4px;
height: auto;
line-height: normal;
padding: 1em 12px;
}
.copy-form-btn span {
display: inline;
}
.share-monitor--modal-text h2 {
font-size: 28px;
}
}

Двоичные данные
public/img/modal-bg-left.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 6.1 KiB

Двоичные данные
public/img/modal-bg-right.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 4.7 KiB

Просмотреть файл

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="64px" height="64px" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>present</title>
<g id="Primary-Flow" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Resolving-a-Breach---Return-Visit" transform="translate(-652.000000, -1337.000000)" fill-rule="nonzero">
<g id="Email-Addresses" transform="translate(273.000000, 753.000000)">
<g id="Breach-List" transform="translate(0.000000, 76.000000)">
<g id="Promo-/-Condensed-/-Lockwise" transform="translate(99.000000, 508.000000)">
<g id="Group" transform="translate(107.000000, 0.000000)">
<g id="firefox_illustrations-fullcolor_recommendation-64" transform="translate(173.000000, 0.000000)">
<path d="M64,20.5111111 C64,19.4310876 63.124468,18.5555556 62.0444444,18.5555556 L1.95555556,18.5555556 C0.879554894,18.5652064 0.00965081065,19.4351104 0,20.5111111 L0,32.7777778 L64,32.7777778 L64,20.5111111 Z" id="Path" fill="#FFEA80"></path>
<path d="M5.33332985,32.7777778 L5.33332985,61.3288889 C5.3323905,61.7912107 5.52301906,62.2332651 5.85988024,62.5499146 C6.19674143,62.8665641 6.64973114,63.0295109 7.11111111,63 L56.9955556,63 C57.9184847,63 58.6666667,62.2518181 58.6666667,61.3288889 L58.6666667,32.7777778 L5.33332985,32.7777778 Z" id="Path" fill="#FFD567"></path>
<polygon id="Path" fill="#FF8A50" points="23.1111111 18.5555556 40.8888889 18.5555556 40.8888889 32.7777778 23.1111111 32.7777778"></polygon>
<polygon id="Path" fill="#FF7139" points="23.1111111 32.7777778 40.8888889 32.7777778 40.8888889 63 23.1111111 63"></polygon>
<polygon id="Path" fill="#E25920" points="23.1111111 32.7777778 40.8888889 32.7777778 40.8888889 38.1111111 23.1111111 38.1111111"></polygon>
<polygon id="Path" fill="#FFBD4F" points="5.33333333 32.7777778 23.1111111 32.7777778 23.1111111 38.1111111 5.33333333 38.1111111"></polygon>
<polygon id="Path" fill="#FFBD4F" points="40.8888889 32.7777778 58.6666667 32.7777778 58.6666667 38.1111111 40.8888889 38.1111111"></polygon>
<path d="M33.1733333,17.1155556 C21.8666667,11.5866667 19.3955556,6.32444444 20.32,2.52 C16.96,6.07555556 16.3911111,12.9911111 31.3955556,20.2977778 C39.52,16.3155556 43.0755556,12.4933333 44.1066667,9.24 C41.0736406,12.6238459 37.3436905,15.310623 33.1733333,17.1155556 Z" id="Path" fill="#E31587"></path>
<path d="M33.1733333,17.1155556 C37.3403654,15.3209947 41.0700462,12.6465407 44.1066667,9.27555556 C44.9555031,6.60772154 44.080198,3.69239078 41.9022222,1.93333333 C38.0622222,-1.51555556 33.1911111,1.61333333 31.4311111,4.97333333 C29.6533333,1.61333333 24.7822222,-1.51555556 20.9422222,1.93333333 C20.7335743,2.11534289 20.5375651,2.31135203 20.3555556,2.52 C19.3955556,6.32444444 21.8666667,11.5866667 33.1733333,17.1155556 L33.1733333,17.1155556 Z" id="Path" fill="#FF298A"></path>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

После

Ширина:  |  Высота:  |  Размер: 3.3 KiB

Просмотреть файл

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" class="x-close" width="16" height="16" viewBox="0 0 16 16"><path fill="#ffffff" d="M9.061 8l3.47-3.47a.75.75 0 0 0-1.061-1.06L8 6.939 4.53 3.47a.75.75 0 1 0-1.06 1.06L6.939 8 3.47 11.47a.75.75 0 1 0 1.06 1.06L8 9.061l3.47 3.47a.75.75 0 0 0 1.06-1.061z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 307 B

101
public/js/experiment.js Normal file
Просмотреть файл

@ -0,0 +1,101 @@
"use strict";
/* global ga */
function selectURL(e, skipAnalyticsPing = false) {
const shareModalInput = document.getElementById("shareModalInput");
// If an a user-init'd focus, send analytics ping
if (!skipAnalyticsPing) {
sendShareModalPing(shareModalInput);
}
shareModalInput.select();
shareModalInput.setSelectionRange(0, 99999);
}
function copyURL(e) {
sendShareModalPing(e.target);
const shareModalInput = document.getElementById("shareModalInput");
// Remove select listener after this event is fired. (Dup engagement)
shareModalInput.removeEventListener("focus", selectURL);
selectURL(null, true);
document.execCommand("copy");
}
function keyPress(e) {
if(e.key === "Escape") {
closeShareModal();
document.removeEventListener("keydown", keyPress);
}
}
function closeShareModal() {
const shareModal = document.getElementById("shareModal");
shareModal.style.display = "none";
}
function sendShareModalPing(el) {
if (typeof(ga) !== "undefined") {
const eventCategory = "[v2] exp5-share-modal";
const eventAction = el.dataset.eventAction;
const eventLabel = `Link ID: ${el.dataset.eventLabel}`;
const options = {};
return ga("send", "event", eventCategory, eventAction, eventLabel, options);
}
}
function initShareModal(target, breachText) {
const shareModal = document.getElementById("shareModal");
const shareModalInput = document.getElementById("shareModalInput");
const shareModalCopy = document.getElementById("shareModalCopy");
const btnCloseShareModal = document.getElementById("closeShareModal");
const shareTextBreach = document.getElementById("shareTextBreach");
const shareTextGeneral = document.getElementById("shareTextGeneral");
const shareModalLocation = target.dataset.eventLabel;
shareModalCopy.dataset.eventLabel = shareModalLocation;
shareModalInput.dataset.eventLabel = shareModalLocation;
// Set input to correct URL value;
shareModalInput.value = target.href;
// Make modal visible
shareModal.style.display = "block";
if (breachText) {
shareTextGeneral.classList.add("hidden");
shareTextBreach.classList.remove("hidden");
} else {
shareTextGeneral.classList.remove("hidden");
shareTextBreach.classList.add("hidden");
}
document.addEventListener("keydown", keyPress);
shareModalInput.addEventListener("focus", selectURL);
shareModalCopy.addEventListener("click", copyURL);
btnCloseShareModal.addEventListener("click", closeShareModal);
shareModal.addEventListener("click", (e)=>{
// If the click is INSIDE the modal, ignore it.
if (e.target !== shareModal) { return; }
closeShareModal();
});
}
function openShareModal(target, breach = false) {
initShareModal(target, breach);
}
const btnOpenShareModal = document.querySelectorAll(".js-share-modal");
if (btnOpenShareModal) {
btnOpenShareModal.forEach( el => {
el.addEventListener("click", (e)=> {
e.preventDefault();
openShareModal(e.target, e.target.dataset.breach);
});
});
}

Просмотреть файл

@ -155,6 +155,29 @@ function setGAListeners(){
});
});
// Growth Experiment
if (document.body.dataset.experiment) {
document.querySelectorAll(".ga-growth-ping").forEach((el) => {
el.addEventListener("click", async(e) => {
// Overwrite current event category for active OAuth buttons
if (el.dataset.eventCategory !== "fxa-oauth") {
el.dataset.eventCategory = "fxa-oauth";
}
await sendPing(el, "Click", el.dataset.eventLabel, {transport: "beacon"});
});
});
// Set Share Modal GA Params on individual elements
document.querySelectorAll(".js-share-modal, .js-share-modal-breach").forEach((el) => {
if (el.dataset.label) {
el.dataset.eventLabel = el.dataset.label;
}
if (el.dataset.eventCategory !== "exp5-share-modal") {
el.dataset.eventCategory = "exp5-share-modal";
}
});
}
}
window.sessionStorage.setItem("gaInit", true);
@ -201,6 +224,29 @@ function setGAListeners(){
ga("set", "anonymizeIp", true);
ga("set", "dimension6", `${document.body.dataset.signedInUser}`);
// Set Share URL UTMs
if ( document.body.dataset.utm_campaign && !document.body.dataset.experiment ) {
// campaignName
ga("set", "campaignName", `${document.body.dataset.utm_campaign}`);
ga("set", "dimension9", `${document.body.dataset.utm_campaign}`);
// campaignKeyword / term
ga("set", "campaignKeyword", `${document.body.dataset.utm_term}`);
ga("set", "dimension8", `${document.body.dataset.utm_term}`);
}
// Growth Experiment
if (document.body.dataset.experiment) {
// If an experiment is active, set the "Growth Experiment Version"
// Custom Dimension to whichever branch is active.
ga("set", "dimension7", `${document.body.dataset.experiment}`);
ga("set", "dimension8", `${document.body.dataset.experiment}`);
ga("set", "dimension9", `${document.body.dataset.utm_campaign}`);
ga("set", "campaignName", `${document.body.dataset.utm_campaign}`);
ga("set", "campaignKeyword", `${document.body.dataset.utm_term}`);
}
ga("send", "pageview", {
hitCallback: function() {
removeUtmsFromUrl();

Просмотреть файл

@ -6,6 +6,8 @@
/* global sendRecommendationPings */
/* global ga */
const utmParams = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content" ];
if (typeof TextEncoder === "undefined") {
const cryptoScript = document.createElement("script");
const scripts = document.getElementsByTagName("script")[0];
@ -62,6 +64,35 @@ function doOauth(el, {emailWatch = false} = {}) {
}
});
// Growth Experiment: OAuth Entry Point IDs are unique to the experiment.
const oAuthEntryPointIds = [
"fx-monitor-check-for-breaches-blue-btn",
"fx-monitor-find-out-blue-btn",
"fx-monitor-alert-me-blue-btn-top",
"fx-monitor-alert-me-blue-btn-bottom",
];
if (oAuthEntryPointIds.includes(el.dataset.entrypoint)) {
// Growth Experiment: Reset UTMs from in-line body tag data elements.
utmParams.forEach(key => {
if (document.body.dataset[key]) {
url.searchParams.delete(key);
url.searchParams.append(key, document.body.dataset[key]);
}
});
if (typeof(ga) !== "undefined" && document.body.dataset.experiment) {
ga("send", {
hitType: "event",
eventCategory: document.body.dataset.utm_campaign,
eventAction: document.body.dataset.experiment,
eventLabel: el.dataset.entrypoint,
transport: "beacon",
});
}
}
if (!sessionStorage) {
window.location.assign(url);
return;
@ -141,6 +172,26 @@ function handleFormSubmits(formEvent) {
const formClassList = thisForm.classList;
// Growth Experiment
if (formClassList.contains("skip")) {
return;
}
// Growth Experiment
if (document.body.dataset.experiment) {
const scanFormActionURL = new URL(thisForm.action);
utmParams.forEach(key => {
if (document.body.dataset[key]) {
scanFormActionURL.searchParams.append(key, document.body.dataset[key]);
}
});
const revisedActionURL = scanFormActionURL.pathname + scanFormActionURL.search;
thisForm.action = revisedActionURL.toString();
}
if (thisForm.email && !isValidEmail(email)) {
sendPing(thisForm, "Failure");
formClassList.add("invalid");

Просмотреть файл

@ -5,10 +5,17 @@ const csrf = require("csurf");
const {home, getAboutPage, getAllBreaches, getBentoStrings, getSecurityTips, notFound} = require("../controllers/home");
const { getShareUTMs } = require("../middleware");
const router = express.Router();
const csrfProtection = csrf();
router.get("/", csrfProtection, home);
router.get("/share/orange", csrfProtection, getShareUTMs, home);
router.get("/share/purple", csrfProtection, getShareUTMs, home);
router.get("/share/blue", csrfProtection, getShareUTMs, home);
router.get("/share/:breach", csrfProtection, getShareUTMs, home);
router.get("/share/", csrfProtection, getShareUTMs, home);
router.get("/about", getAboutPage);
router.get("/breaches", getAllBreaches);
router.get("/security-tips", getSecurityTips);

Просмотреть файл

@ -5,6 +5,7 @@ const { URL } = require("url");
const HIBP = require("./hibp");
const sha1 = require("./sha1-utils");
// Growth Experiment
const AppConstants = require("./app-constants");
const EXPERIMENTS_ENABLED = (AppConstants.EXPERIMENT_ACTIVE === "1");
const { getExperimentFlags } = require("./controllers/utils");
@ -14,6 +15,7 @@ const scanResult = async(req, selfScan=false) => {
const allBreaches = req.app.locals.breaches;
let scannedEmail = null;
// Growth Experiment
const experimentFlags = getExperimentFlags(req, EXPERIMENTS_ENABLED);
const title = req.fluentFormat("scan-title");

Просмотреть файл

@ -62,9 +62,12 @@ function getBreachCategory(breach) {
function getSortedDataClasses(locales, breach, isUserBrowserFirefox=false, isUserLocaleEnUs=false, changePWLink=false) {
const priorityDataClasses = getAllPriorityDataClasses(isUserBrowserFirefox, isUserLocaleEnUs, changePWLink);
const experimentFlags = breach.experimentFlags;
const sortedDataClasses = {
priority: [],
lowerPriority: [],
experimentFlags: experimentFlags,
};
const dataClasses = breach.DataClasses;
@ -108,6 +111,7 @@ function getGenericFillerRecs(locales, numberOfRecsNeeded) {
}
function getBreachDetail(args) {
const experimentFlags = args.data.root.experimentFlags;
const { locales, breach, changePWLink, isUserBrowserFirefox } = getVars(args);
const { sortedDataClasses, recommendations } = getSortedDataClassesAndRecs(locales, breach, isUserBrowserFirefox, changePWLink);
const breachCategory = getBreachCategory(breach);
@ -141,6 +145,8 @@ function getBreachDetail(args) {
copy: breachExposedPasswords ? localize(locales, "rec-section-subhead") : localize(locales, "rec-section-subhead-no-pw"),
recommendationsList: recommendations,
},
experimentFlags: experimentFlags,
};
// Add correct "What is a ... breach" copy.

Просмотреть файл

@ -13,6 +13,7 @@ function userIsOnRelayWaitList(args) {
}
function getBreachesDashboard(args) {
const experimentFlags = args.data.root.experimentFlags;
const verifiedEmails = args.data.root.verifiedEmails;
const locales = args.data.root.req.supportedLocales;
let breachesFound = false;
@ -72,6 +73,7 @@ function getBreachesDashboard(args) {
const emailCards = {
verifiedEmails: verifiedEmails,
breachesFound: breachesFound,
experimentFlags: experimentFlags,
};
return args.fn(emailCards);

Просмотреть файл

@ -3,8 +3,13 @@
const { getStrings } = require("./hbs-helpers");
const { LocaleUtils } = require("./../locale-utils");
const AppConstants = require("../app-constants");
const EXPERIMENTS_ENABLED = (AppConstants.EXPERIMENT_ACTIVE === "1");
const { getExperimentFlags } = require("../controllers/utils");
function getFooterLinks(args) {
const locales = args.data.root.req.supportedLocales;
const footerLinks = [
{
title: "About Firefox Monitor",
@ -28,6 +33,21 @@ function getFooterLinks(args) {
},
];
// Growth Experiment: Only add the footer share line if user is on VB branch
const experimentFlags = getExperimentFlags(args.data.root.req, EXPERIMENTS_ENABLED);
if (EXPERIMENTS_ENABLED && experimentFlags.treatmentBranch) {
const shareMonitorFooter = {
title: "Share Monitor",
stringId: "share-monitor",
href: "/share/purple",
experiment: "js-share-modal",
label: "share-modal--footer",
};
footerLinks.splice(2, 0, shareMonitorFooter);
}
return getStrings(footerLinks, locales);
}

Просмотреть файл

@ -30,7 +30,12 @@ function getString (id, args) {
function getStrings(stringArr, locales) {
stringArr.forEach(string => {
const stringId = string.stringId;
string.stringId =LocaleUtils.fluentFormat(locales, stringId);
// Growth Experiment: Catch EN* Experimental Strings
if (stringId === "share-monitor") {
string.stringId = "Share Monitor";
} else {
string.stringId =LocaleUtils.fluentFormat(locales, stringId);
}
});
return stringArr;
}

Просмотреть файл

@ -3,6 +3,9 @@
const { getStrings, getFxaUrl } = require("./hbs-helpers");
const { LocaleUtils } = require("./../locale-utils");
const AppConstants = require("../app-constants");
const EXPERIMENTS_ENABLED = (AppConstants.EXPERIMENT_ACTIVE === "1");
function getSignedInAs(args) {
const locales = args.data.root.req.supportedLocales;
const userEmail = args.data.root.req.session.user.primary_email;
@ -59,6 +62,19 @@ function fxaMenuLinks(args) {
},
];
// Growth Experiment: Only add the avatar share link if user is on VB branch
if (EXPERIMENTS_ENABLED && args.data.root.req.session.experimentFlags.treatmentBranch) {
const shareMonitorFooter = {
title: "Share Monitor",
stringId: "share-monitor",
href: "/share/orange",
experiment: "js-share-modal",
label: "share-modal--header",
};
fxaLinks.unshift(shareMonitorFooter);
}
return getStrings(fxaLinks, locales);
}

Просмотреть файл

@ -25,7 +25,7 @@
<!--Exposed Data Classes -->
<section class="detail-section bg-white jst-cntr flx flx-col">
{{> breach-detail-content-group dataClasses sectionId="dataClasses"}}
{{> breach-detail-content-group dataClasses sectionId="dataClasses" name=./breach.Name treatmentBranch=./experimentFlags.treatmentBranch}}
</section>
<!-- What to do tips -->

Просмотреть файл

@ -21,7 +21,9 @@
<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 experimentFlags.experimentBranch }} {{> analytics/experiment }} {{/if}}
{{#if utmOverrides.campaignName }} {{> analytics/share }} {{/if}} >
{{> header/header }}
{{{ body }}}
{{> footer }}

Просмотреть файл

@ -0,0 +1,3 @@
data-utm_term={{experimentFlags.experimentBranch}}
data-experiment={{experimentFlags.experimentBranch}}
data-utm_campaign="growthuserflow5"

Просмотреть файл

@ -0,0 +1,2 @@
data-utm_term="{{utmOverrides.campaignTerm}}"
data-utm_campaign="{{utmOverrides.campaignName}}"

Просмотреть файл

@ -48,5 +48,8 @@
</div>
</div>
{{/if}}
{{#if treatmentBranch}}
{{> experiment--share-promo-breach breachName=./breach }}
{{/if}}
{{/if}}
</div>

Просмотреть файл

@ -38,9 +38,10 @@
<h2 class="pref-headline">{{ getString "email-addresses-title" }}</h2>
{{> dashboards/manage-email-link variableClass="hide-mobile"}}
</div>
<div class="email-cards flx flx-col jst-cntr">
<div class="email-cards flx flx-col jst-cntr {{#if experimentFlags.treatmentBranch}}experiment{{/if}}">
{{#each this.verifiedEmails }}
{{> email-card }}
{{> experiment--share-promo-dashboard}}
{{/each}}
{{#if breachesFound }}
{{> hibp-attribution variableClasses="dash-attribution txt-cntr"}}

Просмотреть файл

@ -0,0 +1,15 @@
<div id="shareModal" class="share-monitor--modal">
<div class="share-monitor--modal-content txt-cntr">
<button class="button-modal-close" id="closeShareModal" type="button" name="button"></button>
<div class="share-monitor--modal-text">
<h2>Share Monitor</h2>
<p id="shareTextGeneral" class="share-monitor--modal-promo">Copy this link to share Monitor with people you care about. You can paste the link into email, text messages, social media - wherever you&nbsp;want.</p>
<p id="shareTextBreach" class="hidden share-monitor--modal-promo">Share this link with someone you know who may have been involved in this breach. You can paste the link into email, text messages, social media - wherever you&nbsp;want.</p>
<label class="copy-form" for="shareModalCopy">
<input class="copy-form-input" data-event-action="Focus/Select on URL Input" id="shareModalInput" type="text" name="" value="" readonly="readonly">
<button class="copy-form-btn" data-event-action="Click Copy URL Button" id="shareModalCopy" type="button" name="button">Copy <span>Link</span> </button>
</label>
<p class="disclaimer">Monitor wont track whether or not you share this link. They&nbsp;can&nbsp;check their results for free. No&nbsp;account&nbsp;required.</p>
</div>
</div>
</div>

Просмотреть файл

@ -0,0 +1,10 @@
<!-- {{ name }} -->
<div class="share-monitor--breach">
<div class="inset">
<img src="/img/svg/present.svg" alt="Gift present with a bow">
<div class="text">
<h3 class="txt-purple7">Know someone who may have been affected by this&nbsp;breach?</h3>
<a class="blue-link js-share-modal" data-ga-link="" data-breach="true" data-event-category="exp5-share-modal" data-event-label="share-modal--breach-detail" href="/share/{{ name }}">Send them an invitation to check their email&nbsp;address</a>
</div>
</div>
</div>

Просмотреть файл

@ -0,0 +1,5 @@
<div class="share-monitor--dashboard txt-cntr">
<img class="" src="/img/svg/present.svg" alt="Gift present with a bow">
<h3 class="section-headline txt-purple7">Help protect your family and&nbsp;friends</h3>
<a class="blue-link js-share-modal" data-ga-link="" data-event-category="exp5-share-modal" data-event-label="share-modal--dashboard" href="/share/blue">Send them an invitation to check their email&nbsp;address</a>
</div>

Просмотреть файл

@ -7,10 +7,12 @@
</li>
{{#each (getFooterLinks)}}
<li class="footer-link-wrapper">
<a class="footer-link" href="{{ this.href }}" {{> analytics/outbound-link eventLabel=this.title }} {{#if openNewWindow}} target="_blank" {{/if}} rel="noopener">
<a class="footer-link {{this.experiment}}" href="{{ this.href }}" {{> analytics/outbound-link eventLabel=this.title }} {{#if this.label}} data-label="{{ this.label }}" {{/if}} {{#if openNewWindow}} target="_blank" {{/if}} rel="noopener">
{{ this.stringId }}
</a>
</li>
{{/each}}
</ul>
</footer>
{{> experiment--share-modal}}

Просмотреть файл

@ -1,5 +1,5 @@
<div class="fxa-menu-wrapper">
<div id="avatar-wrapper" class="avatar-wrapper {{ addClass }}" tabindex="0" aria-label="{{ getString "open-fxa-menu" }}">
<div id="avatar-wrapper" data-ga-link="" data-event-category="avatar-menu" data-event-label="open-fxa-menu" class="avatar-wrapper {{ addClass }}" tabindex="0" aria-label="{{ getString "open-fxa-menu" }}">
<img alt="{{ req.session.user.primary_email }}" class="avatar" src="{{ req.session.user.fxa_profile_json.avatar }}"/>
</div>
<div id="fxa-menu" class="fxa-menu">
@ -8,7 +8,7 @@
<span class="signed-in-as">{{{ getSignedInAs }}}</span>
</div>
{{#each (fxaMenuLinks)}}
<a class="fxa-menu-link" {{> analytics/internal-link eventLabel=this.title }} href="{{ this.href }}">{{ this.stringId }}</a>
<a class="fxa-menu-link {{this.experiment}}" {{> analytics/internal-link eventLabel=this.title }} {{#if this.label}} data-label="{{ this.label }}" {{/if}} href="{{ this.href }}">{{ this.stringId }}</a>
{{/each}}
</div>
</div>

Просмотреть файл

@ -23,6 +23,7 @@
<link rel="stylesheet" href="/css/security-tips.css">
<link rel="stylesheet" href="/css/sign-up-banner.css">
<link rel="stylesheet" href="/css/subpage.css">
<link rel="stylesheet" href="/css/share.css">
<script type="text/javascript" src="/js/all-breaches/all-breaches.js"></script>
<script type="text/javascript" src="/js/analytics_dnt-helper.js" defer></script>
@ -36,6 +37,7 @@
<script type="text/javascript" src="/js/privacy-defender.js" defer></script>
<script type="text/javascript" src="/js/scan-email.js" defer></script>
<script type="text/javascript" src="/js/scan-results.js" defer></script>
<script type="text/javascript" src="/js/experiment.js" defer></script>
{{else}}
<link rel="stylesheet" href="/dist/app.min.css">