Bug 1550300 - Add top source, unblank discovery and bug fixes to Activity Stream r=r1cky
authorEd Lee <edilee@mozilla.com>
Thu, 09 May 2019 01:42:56 +0000
changeset 535047 d94d1e2c6f436c33725709bf93c99f6930fd8e99
parent 535042 59314da6bb6b37f3242e8a3b68dcb3849728818c
child 535048 c2250a23fd66e3c812cb023c2d12f6f5e8d5a2f2
push id2082
push userffxbld-merge
push dateMon, 01 Jul 2019 08:34:18 +0000
treeherdermozilla-release@2fb19d0466d2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersr1cky
bugs1550300
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1550300 - Add top source, unblank discovery and bug fixes to Activity Stream r=r1cky Differential Revision: https://phabricator.services.mozilla.com/D30438
browser/components/newtab/common/Actions.jsm
browser/components/newtab/content-src/asrouter/components/ModalOverlay/ModalOverlay.jsx
browser/components/newtab/content-src/asrouter/templates/Trailhead/Trailhead.jsx
browser/components/newtab/content-src/asrouter/templates/Trailhead/_Trailhead.scss
browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss
browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/Hero.jsx
browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/_Hero.scss
browser/components/newtab/content-src/lib/selectLayoutRender.js
browser/components/newtab/css/activity-stream-linux.css
browser/components/newtab/css/activity-stream-mac.css
browser/components/newtab/css/activity-stream-windows.css
browser/components/newtab/data/content/activity-stream.bundle.js
browser/components/newtab/data/content/assets/sync-devices-trailhead.svg
browser/components/newtab/docs/v2-system-addon/data_events.md
browser/components/newtab/lib/ASRouter.jsm
browser/components/newtab/lib/OnboardingMessageProvider.jsm
browser/components/newtab/lib/TelemetryFeed.jsm
browser/components/newtab/lib/UTEventReporting.jsm
browser/components/newtab/locales-src/he/strings.properties
browser/components/newtab/locales-src/it/strings.properties
browser/components/newtab/locales-src/zh-TW/strings.properties
browser/components/newtab/prerendered/locales/he/activity-stream-strings.js
browser/components/newtab/prerendered/locales/it/activity-stream-strings.js
browser/components/newtab/prerendered/locales/zh-TW/activity-stream-strings.js
browser/components/newtab/test/schemas/pings.js
browser/components/newtab/test/unit/asrouter/ASRouter.test.js
browser/components/newtab/test/unit/asrouter/templates/Trailhead.test.jsx
browser/components/newtab/test/unit/content-src/lib/selectLayoutRender.test.js
browser/components/newtab/test/unit/lib/TelemetryFeed.test.js
browser/components/newtab/test/unit/lib/UTEventReporting.test.js
--- a/browser/components/newtab/common/Actions.jsm
+++ b/browser/components/newtab/common/Actions.jsm
@@ -127,16 +127,17 @@ for (const type of [
   "TOP_SITES_INSERT",
   "TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL",
   "TOP_SITES_PIN",
   "TOP_SITES_PREFS_UPDATED",
   "TOP_SITES_UNPIN",
   "TOP_SITES_UPDATED",
   "TOTAL_BOOKMARKS_REQUEST",
   "TOTAL_BOOKMARKS_RESPONSE",
+  "TRAILHEAD_ENROLL_EVENT",
   "UNINIT",
   "UPDATE_PINNED_SEARCH_SHORTCUTS",
   "UPDATE_SEARCH_SHORTCUTS",
   "UPDATE_SECTION_PREFS",
   "WEBEXT_CLICK",
   "WEBEXT_DISMISS",
 ]) {
   actionTypes[type] = type;
--- a/browser/components/newtab/content-src/asrouter/components/ModalOverlay/ModalOverlay.jsx
+++ b/browser/components/newtab/content-src/asrouter/components/ModalOverlay/ModalOverlay.jsx
@@ -3,17 +3,17 @@ import React from "react";
 export class ModalOverlayWrapper extends React.PureComponent {
   constructor(props) {
     super(props);
     this.onKeyDown = this.onKeyDown.bind(this);
   }
 
   onKeyDown(event) {
     if (event.key === "Escape") {
-      this.props.onClose();
+      this.props.onClose(event);
     }
   }
 
   componentWillMount() {
     this.props.document.addEventListener("keydown", this.onKeyDown);
     this.props.document.body.classList.add("modal-open");
   }
 
--- a/browser/components/newtab/content-src/asrouter/templates/Trailhead/Trailhead.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/Trailhead/Trailhead.jsx
@@ -118,23 +118,28 @@ export class _Trailhead extends React.Pu
   }
 
   onSubmit() {
     this.props.dispatch(ac.UserEvent({event: "SUBMIT_EMAIL", ...this._getFormInfo()}));
 
     global.addEventListener("visibilitychange", this.closeModal);
   }
 
-  closeModal() {
+  closeModal(ev) {
     global.removeEventListener("visibilitychange", this.closeModal);
     this.props.document.body.classList.remove("welcome");
     this.props.document.getElementById("root").removeAttribute("aria-hidden");
     this.setState({isModalOpen: false});
     this.revealCards();
-    this.props.dispatch(ac.UserEvent({event: "SKIPPED_SIGNIN", ...this._getFormInfo()}));
+
+    // If closeModal() was triggered by a visibilitychange event, the user actually
+    // submitted the email form so we don't send a SKIPPED_SIGNIN ping.
+    if (!ev || ev.type !== "visibilitychange") {
+      this.props.dispatch(ac.UserEvent({event: "SKIPPED_SIGNIN", ...this._getFormInfo()}));
+    }
   }
 
   /**
    * Report to telemetry additional information about the form submission.
    */
   _getFormInfo() {
     const value = {has_flow_params: this.state.flowId.length > 0};
     return {value};
@@ -218,17 +223,17 @@ export class _Trailhead extends React.Pu
           <ul className="trailheadBenefits">
             {content.benefits.map(item => (
               <li key={item.id} className={item.id}>
                 <h3 data-l10n-id={item.title.string_id}>{this.getStringValue(item.title)}</h3>
                 <p data-l10n-id={item.text.string_id}>{this.getStringValue(item.text)}</p>
               </li>
             ))}
           </ul>
-          <a className="trailheadLearn" data-l10n-id={content.learn.text.string_id} href={this.addUtmParams(content.learn.url)}>
+          <a className="trailheadLearn" data-l10n-id={content.learn.text.string_id} href={this.addUtmParams(content.learn.url)} target="_blank" rel="noopener noreferrer">
             {this.getStringValue(content.learn.text)}
           </a>
         </div>
         <div className="trailheadForm">
           <h3 data-l10n-id={content.form.title.string_id}>{this.getStringValue(content.form.title)}</h3>
           <p data-l10n-id={content.form.text.string_id}>{this.getStringValue(content.form.text)}</p>
           <form method="get" action={this.props.fxaEndpoint} target="_blank" rel="noopener noreferrer" onSubmit={this.onSubmit}>
             <input name="service" type="hidden" value="sync" />
@@ -242,23 +247,23 @@ export class _Trailhead extends React.Pu
             <input name="flow_begin_time" type="hidden" value={this.state.flowBeginTime} />
             <input name="style" type="hidden" value="trailhead" />
             <p data-l10n-id="onboarding-join-form-email-error" className="error" />
             <input
               data-l10n-id={content.form.email.string_id}
               placeholder={this.getStringValue(content.form.email)}
               name="email"
               type="email"
-              required="true"
+              required="required"
               onInvalid={this.onInputInvalid}
               onChange={this.onInputChange} />
             <p className="trailheadTerms" data-l10n-id="onboarding-join-form-legal">
-              <a data-l10n-name="terms"
+              <a data-l10n-name="terms" target="_blank" rel="noopener noreferrer"
                 href={this.addUtmParams("https://accounts.firefox.com/legal/terms")} />
-              <a data-l10n-name="privacy"
+              <a data-l10n-name="privacy" target="_blank" rel="noopener noreferrer"
                 href={this.addUtmParams("https://accounts.firefox.com/legal/privacy")} />
             </p>
             <button data-l10n-id={content.form.button.string_id} type="submit">
               {this.getStringValue(content.form.button)}
             </button>
           </form>
         </div>
       </div>
--- a/browser/components/newtab/content-src/asrouter/templates/Trailhead/_Trailhead.scss
+++ b/browser/components/newtab/content-src/asrouter/templates/Trailhead/_Trailhead.scss
@@ -58,17 +58,17 @@
     }
 
     .trailheadInner {
       grid-template-columns: 4fr 3fr;
     }
 
     .trailheadContent {
       .trailheadBenefits {
-        background: url('#{$image-path}sync-devices.svg');
+        background: url('#{$image-path}sync-devices-trailhead.svg');
         background-position: center center;
         background-repeat: no-repeat;
         background-size: contain;
         height: 200px;
         margin-inline-end: 60px;
       }
 
       .trailheadLearn {
@@ -396,34 +396,30 @@
     bottom: 0;
     left: 0;
     width: 100%;
     text-align: center;
   }
 }
 
 .inline-onboarding {
+  &.activity-stream.welcome {
+    overflow: scroll;
+  }
+
+  .modalOverlayInner {
+    position: absolute;
+  }
+
   .outer-wrapper {
     position: relative;
 
     .prefs-button {
       button {
         position: absolute;
       }
     }
   }
 
   .asrouter-toggle {
     position: absolute;
   }
 }
-
-// If the window is too short or narrow, we need to allow scrolling so user can get to Start Browsing button.
-@media (max-height: 760px), (max-width: 924px) {
-  .activity-stream.welcome.inline-onboarding {
-    overflow: auto;
-  }
-
-  .trailhead {
-    position: absolute;
-    top: 20px;
-  }
-}
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
@@ -36,28 +36,23 @@ export class DSCard extends React.PureCo
           dispatch={this.props.dispatch}
           onLinkClick={!this.props.placeholder ? this.onLinkClick : undefined}
           url={this.props.url}>
           <div className="img-wrapper">
             <DSImage extraClassNames="img" source={this.props.image_src} rawSource={this.props.raw_image_src} />
           </div>
           <div className="meta">
             <div className="info-wrap">
+              <p className="source">{this.props.source}</p>
               <header className="title">{this.props.title}</header>
               {this.props.excerpt && <p className="excerpt">{this.props.excerpt}</p>}
             </div>
-            <p>
-              {this.props.context && (
-                <span>
-                  <span className="context">{this.props.context}</span>
-                  <br />
-                </span>
-              )}
-              <span className="source">{this.props.source}</span>
-            </p>
+            {this.props.context && (
+              <p className="context">{this.props.context}</p>
+            )}
           </div>
           <ImpressionStats
             campaignId={this.props.campaignId}
             rows={[{id: this.props.id, pos: this.props.pos}]}
             dispatch={this.props.dispatch}
             source={this.props.type} />
         </SafeAnchor>
         {!this.props.placeholder && <DSLinkMenu
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss
@@ -85,16 +85,20 @@
       font-weight: 600;
     }
 
     .excerpt {
       // show only 3 lines of copy
       @include limit-visibile-lines(3, $excerpt-line-height, $excerpt-font-size);
     }
 
+    .source {
+      margin-bottom: 2px;
+    }
+
     .context,
     .source {
       @include dark-theme-only {
         color: $grey-40;
       }
 
       font-size: 13px;
       color: $grey-50;
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/Hero.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/Hero.jsx
@@ -76,24 +76,24 @@ export class Hero extends React.PureComp
             dispatch={this.props.dispatch}
             onLinkClick={this.onLinkClick}
             url={heroRec.url}>
             <div className="img-wrapper">
               <DSImage extraClassNames="img" source={heroRec.image_src} rawSource={heroRec.raw_image_src} />
             </div>
             <div className="meta">
               <div className="header-and-excerpt">
+                {heroRec.context ? (
+                  <p className="context">{heroRec.context}</p>
+                ) : (
+                  <p className="source">{heroRec.domain}</p>
+                )}
                 <header>{heroRec.title}</header>
                 <p className="excerpt">{heroRec.excerpt}</p>
               </div>
-              {heroRec.context ? (
-                <p className="context">{heroRec.context}</p>
-              ) : (
-                <p className="source">{heroRec.domain}</p>
-              )}
             </div>
             <ImpressionStats
               campaignId={heroRec.campaignId}
               rows={[{id: heroRec.id, pos: heroRec.pos}]}
               dispatch={this.props.dispatch}
               source={this.props.type} />
           </SafeAnchor>
           <DSLinkMenu
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/_Hero.scss
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/_Hero.scss
@@ -122,16 +122,21 @@
           color: $white;
         }
 
         @include limit-visibile-lines(4, 28, 22);
         color: $grey-90;
         margin-bottom: 0;
       }
 
+      .context,
+      .source {
+        margin: 0 0 4px;
+      }
+
       .context {
         @include dark-theme-only {
           color: $teal-10;
         }
 
         color: $teal-70;
       }
 
--- a/browser/components/newtab/content-src/lib/selectLayoutRender.js
+++ b/browser/components/newtab/content-src/lib/selectLayoutRender.js
@@ -94,23 +94,23 @@ export const selectLayoutRender = (state
       data.recommendations[i].pos = positions[component.type]++;
     }
 
     return {...component, data};
   };
 
   const renderLayout = () => {
     const renderedLayoutArray = [];
-    for (const row of layout.filter(r => r.components.length)) {
+    for (const row of layout.filter(r => r.components.filter(c => !filterArray.includes(c.type)).length)) {
       let components = [];
       renderedLayoutArray.push({
         ...row,
         components,
       });
-      for (const component of row.components.filter(c => !filterArray.includes(c.type))) {
+      for (const component of row.components) {
         if (component.feed) {
           const spocsConfig = component.spocs;
           // Are we still waiting on a feed/spocs, render what we have, and bail out early.
           if (!feeds.data[component.feed.url] ||
             (spocsConfig && spocsConfig.positions && spocsConfig.positions.length && !spocs.loaded)) {
             return renderedLayoutArray;
           }
           components.push(handleComponent(component));
--- a/browser/components/newtab/css/activity-stream-linux.css
+++ b/browser/components/newtab/css/activity-stream-linux.css
@@ -2033,16 +2033,19 @@ main {
         font-size: 22px;
         line-height: 28px;
         max-height: 5.09091em;
         overflow: hidden;
         color: #0C0C0D;
         margin-bottom: 0; }
         [lwt-newtab-brighttext] .ds-hero .wrapper .meta header {
           color: #FFF; }
+      .ds-hero .wrapper .meta .context,
+      .ds-hero .wrapper .meta .source {
+        margin: 0 0 4px; }
       .ds-hero .wrapper .meta .context {
         color: #008EA4; }
         [lwt-newtab-brighttext] .ds-hero .wrapper .meta .context {
           color: #A7FFFE; }
       .ds-hero .wrapper .meta .source {
         font-size: 13px;
         color: #737373;
         margin-bottom: 0;
@@ -2638,16 +2641,18 @@ main {
       max-height: 4.23529em;
       overflow: hidden;
       font-weight: 600; }
     .ds-card .meta .excerpt {
       font-size: 14px;
       line-height: 20px;
       max-height: 4.28571em;
       overflow: hidden; }
+    .ds-card .meta .source {
+      margin-bottom: 2px; }
     .ds-card .meta .context,
     .ds-card .meta .source {
       font-size: 13px;
       color: #737373; }
       [lwt-newtab-brighttext] .ds-card .meta .context, [lwt-newtab-brighttext]
       .ds-card .meta .source {
         color: #B1B1B3; }
   .ds-card header {
@@ -3741,17 +3746,17 @@ a.firstrun-link {
     width: 860px; }
     @media (max-width: 860px) {
       .trailhead.syncCohort {
         left: 0;
         width: 100%; } }
     .trailhead.syncCohort .trailheadInner {
       grid-template-columns: 4fr 3fr; }
     .trailhead.syncCohort .trailheadContent .trailheadBenefits {
-      background: url("../data/content/assets/sync-devices.svg");
+      background: url("../data/content/assets/sync-devices-trailhead.svg");
       background-position: center center;
       background-repeat: no-repeat;
       background-size: contain;
       height: 200px;
       margin-inline-end: 60px; }
     .trailhead.syncCohort .trailheadContent .trailheadLearn {
       margin-inline-start: 0; }
   .trailhead .trailheadBenefits {
@@ -3965,22 +3970,21 @@ a.firstrun-link {
   .trailheadCard .onboardingButtonContainer {
     height: 60px;
     position: absolute;
     bottom: 0;
     left: 0;
     width: 100%;
     text-align: center; }
 
+.inline-onboarding.activity-stream.welcome {
+  overflow: scroll; }
+
+.inline-onboarding .modalOverlayInner {
+  position: absolute; }
+
 .inline-onboarding .outer-wrapper {
   position: relative; }
   .inline-onboarding .outer-wrapper .prefs-button button {
     position: absolute; }
 
 .inline-onboarding .asrouter-toggle {
   position: absolute; }
-
-@media (max-height: 760px), (max-width: 924px) {
-  .activity-stream.welcome.inline-onboarding {
-    overflow: auto; }
-  .trailhead {
-    position: absolute;
-    top: 20px; } }
--- a/browser/components/newtab/css/activity-stream-mac.css
+++ b/browser/components/newtab/css/activity-stream-mac.css
@@ -2036,16 +2036,19 @@ main {
         font-size: 22px;
         line-height: 28px;
         max-height: 5.09091em;
         overflow: hidden;
         color: #0C0C0D;
         margin-bottom: 0; }
         [lwt-newtab-brighttext] .ds-hero .wrapper .meta header {
           color: #FFF; }
+      .ds-hero .wrapper .meta .context,
+      .ds-hero .wrapper .meta .source {
+        margin: 0 0 4px; }
       .ds-hero .wrapper .meta .context {
         color: #008EA4; }
         [lwt-newtab-brighttext] .ds-hero .wrapper .meta .context {
           color: #A7FFFE; }
       .ds-hero .wrapper .meta .source {
         font-size: 13px;
         color: #737373;
         margin-bottom: 0;
@@ -2641,16 +2644,18 @@ main {
       max-height: 4.23529em;
       overflow: hidden;
       font-weight: 600; }
     .ds-card .meta .excerpt {
       font-size: 14px;
       line-height: 20px;
       max-height: 4.28571em;
       overflow: hidden; }
+    .ds-card .meta .source {
+      margin-bottom: 2px; }
     .ds-card .meta .context,
     .ds-card .meta .source {
       font-size: 13px;
       color: #737373; }
       [lwt-newtab-brighttext] .ds-card .meta .context, [lwt-newtab-brighttext]
       .ds-card .meta .source {
         color: #B1B1B3; }
   .ds-card header {
@@ -3744,17 +3749,17 @@ a.firstrun-link {
     width: 860px; }
     @media (max-width: 860px) {
       .trailhead.syncCohort {
         left: 0;
         width: 100%; } }
     .trailhead.syncCohort .trailheadInner {
       grid-template-columns: 4fr 3fr; }
     .trailhead.syncCohort .trailheadContent .trailheadBenefits {
-      background: url("../data/content/assets/sync-devices.svg");
+      background: url("../data/content/assets/sync-devices-trailhead.svg");
       background-position: center center;
       background-repeat: no-repeat;
       background-size: contain;
       height: 200px;
       margin-inline-end: 60px; }
     .trailhead.syncCohort .trailheadContent .trailheadLearn {
       margin-inline-start: 0; }
   .trailhead .trailheadBenefits {
@@ -3968,22 +3973,21 @@ a.firstrun-link {
   .trailheadCard .onboardingButtonContainer {
     height: 60px;
     position: absolute;
     bottom: 0;
     left: 0;
     width: 100%;
     text-align: center; }
 
+.inline-onboarding.activity-stream.welcome {
+  overflow: scroll; }
+
+.inline-onboarding .modalOverlayInner {
+  position: absolute; }
+
 .inline-onboarding .outer-wrapper {
   position: relative; }
   .inline-onboarding .outer-wrapper .prefs-button button {
     position: absolute; }
 
 .inline-onboarding .asrouter-toggle {
   position: absolute; }
-
-@media (max-height: 760px), (max-width: 924px) {
-  .activity-stream.welcome.inline-onboarding {
-    overflow: auto; }
-  .trailhead {
-    position: absolute;
-    top: 20px; } }
--- a/browser/components/newtab/css/activity-stream-windows.css
+++ b/browser/components/newtab/css/activity-stream-windows.css
@@ -2033,16 +2033,19 @@ main {
         font-size: 22px;
         line-height: 28px;
         max-height: 5.09091em;
         overflow: hidden;
         color: #0C0C0D;
         margin-bottom: 0; }
         [lwt-newtab-brighttext] .ds-hero .wrapper .meta header {
           color: #FFF; }
+      .ds-hero .wrapper .meta .context,
+      .ds-hero .wrapper .meta .source {
+        margin: 0 0 4px; }
       .ds-hero .wrapper .meta .context {
         color: #008EA4; }
         [lwt-newtab-brighttext] .ds-hero .wrapper .meta .context {
           color: #A7FFFE; }
       .ds-hero .wrapper .meta .source {
         font-size: 13px;
         color: #737373;
         margin-bottom: 0;
@@ -2638,16 +2641,18 @@ main {
       max-height: 4.23529em;
       overflow: hidden;
       font-weight: 600; }
     .ds-card .meta .excerpt {
       font-size: 14px;
       line-height: 20px;
       max-height: 4.28571em;
       overflow: hidden; }
+    .ds-card .meta .source {
+      margin-bottom: 2px; }
     .ds-card .meta .context,
     .ds-card .meta .source {
       font-size: 13px;
       color: #737373; }
       [lwt-newtab-brighttext] .ds-card .meta .context, [lwt-newtab-brighttext]
       .ds-card .meta .source {
         color: #B1B1B3; }
   .ds-card header {
@@ -3741,17 +3746,17 @@ a.firstrun-link {
     width: 860px; }
     @media (max-width: 860px) {
       .trailhead.syncCohort {
         left: 0;
         width: 100%; } }
     .trailhead.syncCohort .trailheadInner {
       grid-template-columns: 4fr 3fr; }
     .trailhead.syncCohort .trailheadContent .trailheadBenefits {
-      background: url("../data/content/assets/sync-devices.svg");
+      background: url("../data/content/assets/sync-devices-trailhead.svg");
       background-position: center center;
       background-repeat: no-repeat;
       background-size: contain;
       height: 200px;
       margin-inline-end: 60px; }
     .trailhead.syncCohort .trailheadContent .trailheadLearn {
       margin-inline-start: 0; }
   .trailhead .trailheadBenefits {
@@ -3965,22 +3970,21 @@ a.firstrun-link {
   .trailheadCard .onboardingButtonContainer {
     height: 60px;
     position: absolute;
     bottom: 0;
     left: 0;
     width: 100%;
     text-align: center; }
 
+.inline-onboarding.activity-stream.welcome {
+  overflow: scroll; }
+
+.inline-onboarding .modalOverlayInner {
+  position: absolute; }
+
 .inline-onboarding .outer-wrapper {
   position: relative; }
   .inline-onboarding .outer-wrapper .prefs-button button {
     position: absolute; }
 
 .inline-onboarding .asrouter-toggle {
   position: absolute; }
-
-@media (max-height: 760px), (max-width: 924px) {
-  .activity-stream.welcome.inline-onboarding {
-    overflow: auto; }
-  .trailhead {
-    position: absolute;
-    top: 20px; } }
--- a/browser/components/newtab/data/content/activity-stream.bundle.js
+++ b/browser/components/newtab/data/content/activity-stream.bundle.js
@@ -192,17 +192,17 @@ const globalImportContext = typeof Windo
 
 // Create an object that avoids accidental differing key/value pairs:
 // {
 //   INIT: "INIT",
 //   UNINIT: "UNINIT"
 // }
 const actionTypes = {};
 
-for (const type of ["ADDONS_INFO_REQUEST", "ADDONS_INFO_RESPONSE", "ARCHIVE_FROM_POCKET", "AS_ROUTER_INITIALIZED", "AS_ROUTER_PREF_CHANGED", "AS_ROUTER_TELEMETRY_USER_EVENT", "BLOCK_URL", "BOOKMARK_URL", "COPY_DOWNLOAD_LINK", "DELETE_BOOKMARK_BY_ID", "DELETE_FROM_POCKET", "DELETE_HISTORY_URL", "DIALOG_CANCEL", "DIALOG_OPEN", "DISCOVERY_STREAM_CONFIG_CHANGE", "DISCOVERY_STREAM_CONFIG_SETUP", "DISCOVERY_STREAM_CONFIG_SET_VALUE", "DISCOVERY_STREAM_FEEDS_UPDATE", "DISCOVERY_STREAM_FEED_UPDATE", "DISCOVERY_STREAM_IMPRESSION_STATS", "DISCOVERY_STREAM_LAYOUT_RESET", "DISCOVERY_STREAM_LAYOUT_UPDATE", "DISCOVERY_STREAM_LINK_BLOCKED", "DISCOVERY_STREAM_LOADED_CONTENT", "DISCOVERY_STREAM_OPT_OUT", "DISCOVERY_STREAM_SPOCS_CAPS", "DISCOVERY_STREAM_SPOCS_ENDPOINT", "DISCOVERY_STREAM_SPOCS_FILL", "DISCOVERY_STREAM_SPOCS_UPDATE", "DISCOVERY_STREAM_SPOC_IMPRESSION", "DOWNLOAD_CHANGED", "FAKE_FOCUS_SEARCH", "FILL_SEARCH_TERM", "HANDOFF_SEARCH_TO_AWESOMEBAR", "HIDE_SEARCH", "INIT", "NEW_TAB_INIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_REHYDRATED", "NEW_TAB_STATE_REQUEST", "NEW_TAB_UNLOAD", "OPEN_DOWNLOAD_FILE", "OPEN_LINK", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "OPEN_WEBEXT_SETTINGS", "PAGE_PRERENDERED", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINKS_CHANGED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PLACES_SAVED_TO_POCKET", "POCKET_CTA", "POCKET_LINK_DELETED_OR_ARCHIVED", "POCKET_LOGGED_IN", "POCKET_WAITING_FOR_SPOC", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "PREVIEW_REQUEST", "PREVIEW_REQUEST_CANCEL", "PREVIEW_RESPONSE", "REMOVE_DOWNLOAD_FILE", "RICH_ICON_MISSING", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_DISABLE", "SECTION_ENABLE", "SECTION_MOVE", "SECTION_OPTIONS_CHANGED", "SECTION_REGISTER", "SECTION_UPDATE", "SECTION_UPDATE_CARD", "SETTINGS_CLOSE", "SETTINGS_OPEN", "SET_PREF", "SHOW_DOWNLOAD_FILE", "SHOW_FIREFOX_ACCOUNTS", "SHOW_SEARCH", "SKIPPED_SIGNIN", "SNIPPETS_BLOCKLIST_CLEARED", "SNIPPETS_BLOCKLIST_UPDATED", "SNIPPETS_DATA", "SNIPPETS_PREVIEW_MODE", "SNIPPETS_RESET", "SNIPPET_BLOCKED", "SUBMIT_EMAIL", "SYSTEM_TICK", "TELEMETRY_IMPRESSION_STATS", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_CANCEL_EDIT", "TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL", "TOP_SITES_EDIT", "TOP_SITES_INSERT", "TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL", "TOP_SITES_PIN", "TOP_SITES_PREFS_UPDATED", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "TOTAL_BOOKMARKS_REQUEST", "TOTAL_BOOKMARKS_RESPONSE", "UNINIT", "UPDATE_PINNED_SEARCH_SHORTCUTS", "UPDATE_SEARCH_SHORTCUTS", "UPDATE_SECTION_PREFS", "WEBEXT_CLICK", "WEBEXT_DISMISS"]) {
+for (const type of ["ADDONS_INFO_REQUEST", "ADDONS_INFO_RESPONSE", "ARCHIVE_FROM_POCKET", "AS_ROUTER_INITIALIZED", "AS_ROUTER_PREF_CHANGED", "AS_ROUTER_TELEMETRY_USER_EVENT", "BLOCK_URL", "BOOKMARK_URL", "COPY_DOWNLOAD_LINK", "DELETE_BOOKMARK_BY_ID", "DELETE_FROM_POCKET", "DELETE_HISTORY_URL", "DIALOG_CANCEL", "DIALOG_OPEN", "DISCOVERY_STREAM_CONFIG_CHANGE", "DISCOVERY_STREAM_CONFIG_SETUP", "DISCOVERY_STREAM_CONFIG_SET_VALUE", "DISCOVERY_STREAM_FEEDS_UPDATE", "DISCOVERY_STREAM_FEED_UPDATE", "DISCOVERY_STREAM_IMPRESSION_STATS", "DISCOVERY_STREAM_LAYOUT_RESET", "DISCOVERY_STREAM_LAYOUT_UPDATE", "DISCOVERY_STREAM_LINK_BLOCKED", "DISCOVERY_STREAM_LOADED_CONTENT", "DISCOVERY_STREAM_OPT_OUT", "DISCOVERY_STREAM_SPOCS_CAPS", "DISCOVERY_STREAM_SPOCS_ENDPOINT", "DISCOVERY_STREAM_SPOCS_FILL", "DISCOVERY_STREAM_SPOCS_UPDATE", "DISCOVERY_STREAM_SPOC_IMPRESSION", "DOWNLOAD_CHANGED", "FAKE_FOCUS_SEARCH", "FILL_SEARCH_TERM", "HANDOFF_SEARCH_TO_AWESOMEBAR", "HIDE_SEARCH", "INIT", "NEW_TAB_INIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_REHYDRATED", "NEW_TAB_STATE_REQUEST", "NEW_TAB_UNLOAD", "OPEN_DOWNLOAD_FILE", "OPEN_LINK", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "OPEN_WEBEXT_SETTINGS", "PAGE_PRERENDERED", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINKS_CHANGED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PLACES_SAVED_TO_POCKET", "POCKET_CTA", "POCKET_LINK_DELETED_OR_ARCHIVED", "POCKET_LOGGED_IN", "POCKET_WAITING_FOR_SPOC", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "PREVIEW_REQUEST", "PREVIEW_REQUEST_CANCEL", "PREVIEW_RESPONSE", "REMOVE_DOWNLOAD_FILE", "RICH_ICON_MISSING", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_DISABLE", "SECTION_ENABLE", "SECTION_MOVE", "SECTION_OPTIONS_CHANGED", "SECTION_REGISTER", "SECTION_UPDATE", "SECTION_UPDATE_CARD", "SETTINGS_CLOSE", "SETTINGS_OPEN", "SET_PREF", "SHOW_DOWNLOAD_FILE", "SHOW_FIREFOX_ACCOUNTS", "SHOW_SEARCH", "SKIPPED_SIGNIN", "SNIPPETS_BLOCKLIST_CLEARED", "SNIPPETS_BLOCKLIST_UPDATED", "SNIPPETS_DATA", "SNIPPETS_PREVIEW_MODE", "SNIPPETS_RESET", "SNIPPET_BLOCKED", "SUBMIT_EMAIL", "SYSTEM_TICK", "TELEMETRY_IMPRESSION_STATS", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_CANCEL_EDIT", "TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL", "TOP_SITES_EDIT", "TOP_SITES_INSERT", "TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL", "TOP_SITES_PIN", "TOP_SITES_PREFS_UPDATED", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "TOTAL_BOOKMARKS_REQUEST", "TOTAL_BOOKMARKS_RESPONSE", "TRAILHEAD_ENROLL_EVENT", "UNINIT", "UPDATE_PINNED_SEARCH_SHORTCUTS", "UPDATE_SEARCH_SHORTCUTS", "UPDATE_SECTION_PREFS", "WEBEXT_CLICK", "WEBEXT_DISMISS"]) {
   actionTypes[type] = type;
 } // These are acceptable actions for AS Router messages to have. They can show up
 // as call-to-action buttons in snippets, onboarding tour, etc.
 
 
 const ASRouterActions = {};
 
 for (const type of ["INSTALL_ADDON_FROM_URL", "OPEN_APPLICATIONS_MENU", "OPEN_PRIVATE_BROWSER_WINDOW", "OPEN_URL", "OPEN_ABOUT_PAGE", "OPEN_PREFERENCES_PAGE", "SHOW_FIREFOX_ACCOUNTS", "PIN_CURRENT_TAB"]) {
@@ -2702,17 +2702,17 @@ class OnboardingMessage extends react__W
 class ModalOverlayWrapper extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureComponent {
   constructor(props) {
     super(props);
     this.onKeyDown = this.onKeyDown.bind(this);
   }
 
   onKeyDown(event) {
     if (event.key === "Escape") {
-      this.props.onClose();
+      this.props.onClose(event);
     }
   }
 
   componentWillMount() {
     this.props.document.addEventListener("keydown", this.onKeyDown);
     this.props.document.body.classList.add("modal-open");
   }
 
@@ -3412,28 +3412,32 @@ class _Trailhead extends react__WEBPACK_
   onSubmit() {
     this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({
       event: "SUBMIT_EMAIL",
       ...this._getFormInfo()
     }));
     global.addEventListener("visibilitychange", this.closeModal);
   }
 
-  closeModal() {
+  closeModal(ev) {
     global.removeEventListener("visibilitychange", this.closeModal);
     this.props.document.body.classList.remove("welcome");
     this.props.document.getElementById("root").removeAttribute("aria-hidden");
     this.setState({
       isModalOpen: false
     });
-    this.revealCards();
-    this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({
-      event: "SKIPPED_SIGNIN",
-      ...this._getFormInfo()
-    }));
+    this.revealCards(); // If closeModal() was triggered by a visibilitychange event, the user actually
+    // submitted the email form so we don't send a SKIPPED_SIGNIN ping.
+
+    if (!ev || ev.type !== "visibilitychange") {
+      this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({
+        event: "SKIPPED_SIGNIN",
+        ...this._getFormInfo()
+      }));
+    }
   }
   /**
    * Report to telemetry additional information about the form submission.
    */
 
 
   _getFormInfo() {
     const value = {
@@ -3551,17 +3555,19 @@ class _Trailhead extends react__WEBPACK_
       className: item.id
     }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("h3", {
       "data-l10n-id": item.title.string_id
     }, this.getStringValue(item.title)), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("p", {
       "data-l10n-id": item.text.string_id
     }, this.getStringValue(item.text))))), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("a", {
       className: "trailheadLearn",
       "data-l10n-id": content.learn.text.string_id,
-      href: this.addUtmParams(content.learn.url)
+      href: this.addUtmParams(content.learn.url),
+      target: "_blank",
+      rel: "noopener noreferrer"
     }, this.getStringValue(content.learn.text))), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("div", {
       className: "trailheadForm"
     }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("h3", {
       "data-l10n-id": content.form.title.string_id
     }, this.getStringValue(content.form.title)), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("p", {
       "data-l10n-id": content.form.text.string_id
     }, this.getStringValue(content.form.text)), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("form", {
       method: "get",
@@ -3612,27 +3618,31 @@ class _Trailhead extends react__WEBPACK_
     }), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("p", {
       "data-l10n-id": "onboarding-join-form-email-error",
       className: "error"
     }), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("input", {
       "data-l10n-id": content.form.email.string_id,
       placeholder: this.getStringValue(content.form.email),
       name: "email",
       type: "email",
-      required: "true",
+      required: "required",
       onInvalid: this.onInputInvalid,
       onChange: this.onInputChange
     }), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("p", {
       className: "trailheadTerms",
       "data-l10n-id": "onboarding-join-form-legal"
     }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("a", {
       "data-l10n-name": "terms",
+      target: "_blank",
+      rel: "noopener noreferrer",
       href: this.addUtmParams("https://accounts.firefox.com/legal/terms")
     }), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("a", {
       "data-l10n-name": "privacy",
+      target: "_blank",
+      rel: "noopener noreferrer",
       href: this.addUtmParams("https://accounts.firefox.com/legal/privacy")
     })), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("button", {
       "data-l10n-id": content.form.button.string_id,
       type: "submit"
     }, this.getStringValue(content.form.button))))), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("button", {
       className: "trailheadStart",
       "data-l10n-id": content.skipButton.string_id,
       onBlur: this.onStartBlur,
@@ -7902,25 +7912,25 @@ class DSCard_DSCard extends external_Rea
     }, external_React_default.a.createElement(DSImage_DSImage, {
       extraClassNames: "img",
       source: this.props.image_src,
       rawSource: this.props.raw_image_src
     })), external_React_default.a.createElement("div", {
       className: "meta"
     }, external_React_default.a.createElement("div", {
       className: "info-wrap"
-    }, external_React_default.a.createElement("header", {
+    }, external_React_default.a.createElement("p", {
+      className: "source"
+    }, this.props.source), external_React_default.a.createElement("header", {
       className: "title"
     }, this.props.title), this.props.excerpt && external_React_default.a.createElement("p", {
       className: "excerpt"
-    }, this.props.excerpt)), external_React_default.a.createElement("p", null, this.props.context && external_React_default.a.createElement("span", null, external_React_default.a.createElement("span", {
+    }, this.props.excerpt)), this.props.context && external_React_default.a.createElement("p", {
       className: "context"
-    }, this.props.context), external_React_default.a.createElement("br", null)), external_React_default.a.createElement("span", {
-      className: "source"
-    }, this.props.source))), external_React_default.a.createElement(ImpressionStats["ImpressionStats"], {
+    }, this.props.context)), external_React_default.a.createElement(ImpressionStats["ImpressionStats"], {
       campaignId: this.props.campaignId,
       rows: [{
         id: this.props.id,
         pos: this.props.pos
       }],
       dispatch: this.props.dispatch,
       source: this.props.type
     })), !this.props.placeholder && external_React_default.a.createElement(DSLinkMenu, {
@@ -8290,23 +8300,23 @@ class Hero_Hero extends external_React_d
     }, external_React_default.a.createElement(DSImage_DSImage, {
       extraClassNames: "img",
       source: heroRec.image_src,
       rawSource: heroRec.raw_image_src
     })), external_React_default.a.createElement("div", {
       className: "meta"
     }, external_React_default.a.createElement("div", {
       className: "header-and-excerpt"
-    }, external_React_default.a.createElement("header", null, heroRec.title), external_React_default.a.createElement("p", {
-      className: "excerpt"
-    }, heroRec.excerpt)), heroRec.context ? external_React_default.a.createElement("p", {
+    }, heroRec.context ? external_React_default.a.createElement("p", {
       className: "context"
     }, heroRec.context) : external_React_default.a.createElement("p", {
       className: "source"
-    }, heroRec.domain)), external_React_default.a.createElement(ImpressionStats["ImpressionStats"], {
+    }, heroRec.domain), external_React_default.a.createElement("header", null, heroRec.title), external_React_default.a.createElement("p", {
+      className: "excerpt"
+    }, heroRec.excerpt))), external_React_default.a.createElement(ImpressionStats["ImpressionStats"], {
       campaignId: heroRec.campaignId,
       rows: [{
         id: heroRec.id,
         pos: heroRec.pos
       }],
       dispatch: this.props.dispatch,
       source: this.props.type
     })), external_React_default.a.createElement(DSLinkMenu, {
@@ -8521,23 +8531,23 @@ const selectLayoutRender = (state, prefs
     return { ...component,
       data
     };
   };
 
   const renderLayout = () => {
     const renderedLayoutArray = [];
 
-    for (const row of layout.filter(r => r.components.length)) {
+    for (const row of layout.filter(r => r.components.filter(c => !filterArray.includes(c.type)).length)) {
       let components = [];
       renderedLayoutArray.push({ ...row,
         components
       });
 
-      for (const component of row.components.filter(c => !filterArray.includes(c.type))) {
+      for (const component of row.components) {
         if (component.feed) {
           const spocsConfig = component.spocs; // Are we still waiting on a feed/spocs, render what we have, and bail out early.
 
           if (!feeds.data[component.feed.url] || spocsConfig && spocsConfig.positions && spocsConfig.positions.length && !spocs.loaded) {
             return renderedLayoutArray;
           }
 
           components.push(handleComponent(component));
@@ -11904,17 +11914,17 @@ class CachedIterable {
 
     if (seen.length === 0 || seen[seen.length - 1].done === false) {
       seen.push(iterator.next());
     }
   }
 
 }
 // CONCATENATED MODULE: ./node_modules/fluent/src/fallback.js
-function _asyncIterator(iterable) { var method; if (typeof Symbol === "function") { if (Symbol.asyncIterator) { method = iterable[Symbol.asyncIterator]; if (method != null) return method.call(iterable); } if (Symbol.iterator) { method = iterable[Symbol.iterator]; if (method != null) return method.call(iterable); } } throw new TypeError("Object is not async iterable"); }
+function _asyncIterator(iterable) { var method; if (typeof Symbol !== "undefined") { if (Symbol.asyncIterator) { method = iterable[Symbol.asyncIterator]; if (method != null) return method.call(iterable); } if (Symbol.iterator) { method = iterable[Symbol.iterator]; if (method != null) return method.call(iterable); } } throw new TypeError("Object is not async iterable"); }
 
 /*
  * @overview
  *
  * Functions for managing ordered sequences of MessageContexts.
  *
  * An ordered iterable of MessageContext instances can represent the current
  * negotiated fallback chain of languages.  This iterable can be used to find
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/sync-devices-trailhead.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" viewBox="0 0 234.4 93.4"><style>.st0{fill:#592acb}.st4{fill:#f9f9fa}</style><g id="right-inner"><path d="M167 78.3c-5.5 0-10.9-1.4-15.6-4.2-1-.4-1.5-1.6-1.1-2.6.4-1 1.6-1.5 2.6-1.1.1.1.3.1.4.2 4.1 2.4 8.8 3.6 13.6 3.6 1.1 0 2 .9 2 2 .1 1.2-.8 2.1-1.9 2.1zm7.6-1c-1.1 0-2-.9-2-2 0-.9.6-1.7 1.5-1.9 4.6-1.2 8.8-3.7 12.2-7.1.8-.8 2.1-.8 2.9 0s.8 2.1 0 2.9c-3.9 3.9-8.7 6.7-14 8.1h-.6zm-28.3-7.6c-.5 0-1-.2-1.4-.6-3.9-3.9-6.7-8.7-8.1-14-.4-1 .2-2.2 1.2-2.6 1-.4 2.2.2 2.6 1.2 0 .1.1.2.1.3 1.3 4.6 3.7 8.8 7.1 12.2.8.8.8 2 0 2.8-.4.5-.9.7-1.5.7zm46-6c-1.1 0-2-.9-2-2 0-.4.1-.7.3-1 2.4-4.1 3.6-8.8 3.6-13.6V47c.1-1.1 1-1.9 2.1-1.9 1 .1 1.8.9 1.9 1.9 0 5.5-1.4 10.9-4.2 15.7-.3.6-1 1-1.7 1zM137.7 49c-1.1 0-2-.9-2-2 0-5.5 1.4-10.8 4.2-15.6.5-1 1.7-1.4 2.7-.9 1 .5 1.4 1.7.9 2.7 0 .1-.1.1-.1.2-2.4 4.1-3.6 8.8-3.6 13.6 0 1.1-.9 2-2.1 2 .1 0 0 0 0 0zm57.5-7.7c-.9 0-1.7-.6-1.9-1.5-1.3-4.6-3.7-8.8-7.1-12.2-.8-.8-.8-2.1 0-2.9.8-.8 2.1-.8 2.9 0 3.9 3.9 6.7 8.7 8.1 14 .3 1.1-.3 2.2-1.4 2.5-.2.1-.4.1-.6.1zm-48.9-12.9c-.5 0-1-.2-1.4-.6-.8-.8-.8-2.1 0-2.8 3.9-3.9 8.7-6.7 14-8.1 1.1-.2 2.1.6 2.3 1.7.1.9-.4 1.8-1.2 2.1-4.6 1.2-8.8 3.7-12.2 7.1-.5.3-1 .6-1.5.6zm35.2-4.8c-.3 0-.7-.1-1-.3-4.1-2.4-8.8-3.6-13.5-3.6h-.2c-1.1.1-2.1-.8-2.1-1.9-.1-1.1.8-2.1 1.9-2.1h.3c5.5 0 10.8 1.4 15.6 4.1 1 .5 1.3 1.8.8 2.7-.4.7-1.1 1.1-1.8 1.1z" class="st0"/><animateTransform attributeName="transform" attributeType="XML" dur="10s" from="360 167 47" repeatCount="indefinite" to="0 167 47" type="rotate"/></g><g id="right-outer"><path d="M167 92c-7.9 0-15.6-2.1-22.4-6-1.4-.8-1.9-2.6-1.1-3.9.8-1.4 2.6-1.9 3.9-1.1 5.9 3.4 12.7 5.2 19.6 5.2 1.6 0 2.9 1.3 2.9 2.9 0 1.6-1.3 2.9-2.9 2.9zm10.9-1.4c-1.6 0-2.9-1.3-2.9-2.9 0-1.3.9-2.5 2.1-2.8 6.6-1.8 12.7-5.3 17.6-10.2 1.1-1.2 2.9-1.2 4.1-.1 1.2 1.1 1.2 2.9.1 4.1l-.1.1c-5.6 5.6-12.5 9.6-20.1 11.7-.3 0-.5.1-.8.1zm-40.6-10.9c-.8 0-1.5-.3-2-.8-5.6-5.6-9.6-12.5-11.7-20.1-.4-1.6.6-3.1 2.2-3.5 1.5-.3 3 .5 3.4 2 1.8 6.6 5.3 12.7 10.2 17.6 1.1 1.1 1.1 3 0 4.1-.6.4-1.4.7-2.1.7zm66.2-8.8c-1.6 0-2.9-1.3-2.9-2.9 0-.5.1-1 .4-1.4 3.4-6 5.2-12.7 5.2-19.6v-.2c0-1.6 1.3-2.9 2.9-2.9s2.9 1.3 2.9 2.9v.2c0 7.9-2.1 15.7-6 22.5-.5.9-1.5 1.4-2.5 1.4zm-78.6-21c-1.6 0-2.9-1.3-2.9-2.9 0-7.9 2.1-15.6 6-22.4.8-1.4 2.6-1.9 4-1.1 1.4.8 1.9 2.6 1.1 4-3.4 5.9-5.2 12.7-5.2 19.5-.1 1.5-1.3 2.9-3 2.9zm82.7-11.1c-1.3 0-2.5-.9-2.8-2.1-1.8-6.6-5.3-12.7-10.2-17.5-1.2-1-1.4-2.8-.4-4.1s2.8-1.4 4.1-.4c.1.1.3.2.4.4 5.6 5.6 9.7 12.5 11.7 20.1.4 1.5-.5 3.1-2 3.5-.3.1-.5.1-.8.1zm-70.5-18.6c-.8 0-1.5-.3-2.1-.8-1.1-1.1-1.1-3 0-4.1 5.6-5.6 12.5-9.6 20.1-11.7 1.5-.4 3.1.5 3.5 2.1.4 1.5-.5 3-2 3.5-6.6 1.8-12.7 5.3-17.5 10.2-.4.5-1.2.8-2 .8zm50.8-6.9c-.5 0-1-.1-1.4-.4-5.9-3.4-12.7-5.2-19.5-5.2h-.1c-1.6 0-2.9-1.3-2.9-2.9s1.3-2.9 2.9-2.9h.1c7.9 0 15.6 2 22.4 6 1.4.8 1.9 2.6 1.1 3.9-.6 1-1.5 1.5-2.6 1.5z" class="st0"/><animateTransform attributeName="transform" attributeType="XML" dur="10s" from="0 167 47" repeatCount="indefinite" to="360 167 47" type="rotate"/></g><g id="left-inner"><path d="M79 78.3c-5.5 0-10.9-1.4-15.6-4.2-1-.4-1.5-1.6-1.1-2.6.4-1 1.6-1.5 2.6-1.1.1.1.3.1.4.2 4.1 2.4 8.8 3.6 13.6 3.6 1.1 0 2 .9 2 2 .1 1.2-.8 2.1-1.9 2.1zm7.6-1c-1.1 0-2-.9-2-2 0-.9.6-1.7 1.5-1.9 4.6-1.2 8.8-3.7 12.2-7.1.8-.8 2.1-.8 2.9 0 .8.8.8 2.1 0 2.9-3.9 3.9-8.7 6.7-14 8.1h-.6zm-28.3-7.6c-.5 0-1-.2-1.4-.6-3.9-3.9-6.7-8.7-8.1-14-.4-1 .2-2.2 1.2-2.6 1-.4 2.2.2 2.6 1.2 0 .1.1.2.1.3 1.3 4.6 3.7 8.8 7.1 12.2.8.8.8 2 0 2.8-.4.5-.9.7-1.5.7zm46-6c-1.1 0-2-.9-2-2 0-.4.1-.7.3-1 2.4-4.1 3.6-8.8 3.6-13.6V47c.1-1.1 1-1.9 2.1-1.9 1 .1 1.8.9 1.9 1.9 0 5.5-1.4 10.9-4.2 15.7-.3.6-1 1-1.7 1zM49.7 49c-1.1 0-2-.9-2-2 0-5.5 1.4-10.8 4.2-15.6.5-1 1.7-1.4 2.7-.9s1.4 1.7.9 2.7c0 .1-.1.1-.1.2-2.4 4.1-3.6 8.8-3.6 13.6 0 1.1-.9 2-2.1 2 .1 0 0 0 0 0zm57.5-7.7c-.9 0-1.7-.6-1.9-1.5-1.3-4.6-3.7-8.8-7.1-12.2-.8-.8-.8-2.1 0-2.9.8-.8 2.1-.8 2.9 0 3.9 3.9 6.7 8.7 8.1 14 .3 1.1-.3 2.2-1.4 2.5-.2.1-.4.1-.6.1zM58.3 28.4c-.5 0-1-.2-1.4-.6-.8-.8-.8-2.1 0-2.8 3.9-3.9 8.7-6.7 14-8.1 1.1-.2 2.1.6 2.3 1.7.1.9-.4 1.8-1.2 2.1-4.6 1.2-8.8 3.7-12.2 7.1-.5.3-1 .6-1.5.6zm35.2-4.8c-.3 0-.7-.1-1-.3-4.1-2.4-8.8-3.6-13.5-3.6h-.2c-1.1.1-2.1-.8-2.1-1.9-.1-1.1.8-2.1 1.9-2.1h.3c5.5 0 10.8 1.4 15.6 4.1 1 .5 1.3 1.8.8 2.7-.4.7-1.1 1.1-1.8 1.1z" class="st0"/><animateTransform attributeName="transform" attributeType="XML" dur="10s" from="360 79 47" repeatCount="indefinite" to="0 79 47" type="rotate"/></g><g id="left-outer"><path d="M79 92c-7.9 0-15.6-2.1-22.4-6-1.4-.8-1.9-2.6-1.1-3.9.8-1.4 2.6-1.9 3.9-1.1 5.9 3.4 12.7 5.2 19.6 5.2 1.6 0 2.9 1.3 2.9 2.9 0 1.6-1.3 2.9-2.9 2.9zm10.9-1.4c-1.6 0-2.9-1.3-2.9-2.9 0-1.3.9-2.5 2.1-2.8 6.6-1.8 12.7-5.3 17.6-10.2 1.1-1.2 2.9-1.2 4.1-.1 1.2 1.1 1.2 2.9.1 4.1l-.1.1c-5.6 5.6-12.5 9.6-20.1 11.7-.3 0-.5.1-.8.1zM49.3 79.7c-.8 0-1.5-.3-2-.8-5.6-5.6-9.6-12.5-11.7-20.1-.4-1.6.6-3.1 2.2-3.5 1.5-.3 3 .5 3.4 2C43 63.9 46.5 70 51.4 74.9c1.1 1.1 1.1 3 0 4.1-.6.4-1.4.7-2.1.7zm66.2-8.8c-1.6 0-2.9-1.3-2.9-2.9 0-.5.1-1 .4-1.4 3.4-6 5.2-12.7 5.2-19.6v-.2c0-1.6 1.3-2.9 2.9-2.9 1.6 0 2.9 1.3 2.9 2.9v.2c0 7.9-2.1 15.7-6 22.5-.5.9-1.5 1.4-2.5 1.4zm-78.6-21c-1.6 0-2.9-1.3-2.9-2.9 0-7.9 2.1-15.6 6-22.4.8-1.4 2.6-1.9 4-1.1 1.4.8 1.9 2.6 1.1 4-3.4 5.9-5.2 12.7-5.2 19.5-.1 1.5-1.3 2.9-3 2.9zm82.7-11.1c-1.3 0-2.5-.9-2.8-2.1-1.8-6.6-5.3-12.7-10.2-17.5-1.2-1-1.4-2.8-.4-4.1 1-1.2 2.8-1.4 4.1-.4.1.1.3.2.4.4 5.6 5.6 9.7 12.5 11.7 20.1.4 1.5-.5 3.1-2 3.5-.3.1-.5.1-.8.1zM49.1 20.2c-.8 0-1.5-.3-2.1-.8-1.1-1.1-1.1-3 0-4.1 5.6-5.6 12.5-9.6 20.1-11.7 1.5-.4 3.1.5 3.5 2.1.4 1.5-.5 3-2 3.5C62 11 55.9 14.5 51.1 19.4c-.4.5-1.2.8-2 .8zm50.8-6.9c-.5 0-1-.1-1.4-.4C92.6 9.6 85.8 7.8 79 7.8h-.1c-1.6 0-2.9-1.3-2.9-2.9S77.3 2 78.9 2h.1c7.9 0 15.6 2 22.4 6 1.4.8 1.9 2.6 1.1 3.9-.6.9-1.5 1.4-2.6 1.4z" class="st0"/><animateTransform attributeName="transform" attributeType="XML" dur="10s" from="0 79 47" repeatCount="indefinite" to="360 79 47" type="rotate"/></g><path fill="#ff848b" d="M27.1 68.3h26.5V76H27.1z"/><linearGradient id="SVGID_1_" x1="-13.76" x2="75.674" y1="104.809" y2="15.375" gradientTransform="translate(.932 -.95)" gradientUnits="userSpaceOnUse"><stop offset=".165" stop-color="#b833e1"/><stop offset=".958" stop-color="#ff4f5e"/></linearGradient><path fill="url(#SVGID_1_)" d="M78.9 70.6h-1.7V23c0-1.9-1.5-3.4-3.4-3.4H6.9c-1.9 0-3.4 1.5-3.4 3.4v47.6H1.7c-.9 0-1.7.8-1.7 1.7v3.5c0 .9.8 1.7 1.7 1.7h77.2c.9 0 1.7-.8 1.7-1.7v-3.5c0-.9-.8-1.7-1.7-1.7zM47.2 74H33.4v-3.4h13.7V74z"/><path fill="#bc24bc" d="M72 24.4H8.6v41.9H72V24.4z" opacity=".5"/><path d="M57 41.9c-.4 0-.7.3-.7.7v.8c-1.8-1.8-4.7-1.9-6.5-.1-.6.6-1.1 1.4-1.3 2.2-.1.4.1.7.5.8h.2c.3 0 .6-.2.6-.5.4-1.5 1.7-2.5 3.2-2.5 1 0 2 .5 2.6 1.3h-1.3c-.4 0-.7.3-.7.6 0 .4.3.7.6.7H57c.4 0 .7-.3.7-.7v-2.7c0-.3-.3-.6-.7-.6zm.1 5c-.4-.1-.7.1-.8.5-.4 1.5-1.7 2.5-3.2 2.5-1 0-2-.5-2.6-1.3h1.3c.4 0 .6-.3.6-.7 0-.3-.3-.6-.6-.6h-2.7c-.4 0-.7.3-.7.7v2.7c0 .4.3.7.6.7.4 0 .7-.3.7-.6v-.9c1.8 1.8 4.7 1.9 6.5.1.6-.6 1.1-1.4 1.3-2.2.1-.5-.1-.8-.4-.9zM28 41.5c-2.8 0-5.1 2.3-5.1 5.1s2.3 5.1 5.1 5.1 5.1-2.3 5.1-5.1-2.3-5.1-5.1-5.1zm0 8.9c-2.1 0-3.8-1.7-3.8-3.8s1.7-3.8 3.8-3.8 3.8 1.7 3.8 3.8c0 2.1-1.7 3.8-3.8 3.8zm2.2-3.8H28v-2.2c0-.2-.1-.3-.3-.3-.2 0-.3.1-.3.3v2.5c0 .2.1.3.3.3h2.5c.2 0 .3-.1.3-.3s-.1-.3-.3-.3zm7.7 4.9c-.4 0-.7-.3-.7-.7v-.1l.4-2.7-1.8-2c-.3-.3-.2-.7 0-1 .1-.1.2-.1.3-.2l2.5-.5 1.2-2.4c.2-.3.6-.5.9-.3l.3.3 1.2 2.4 2.5.5c.4.1.6.4.6.8 0 .1-.1.2-.2.3L43.4 48l.4 2.7c.1.4-.2.7-.6.8-.1 0-.3 0-.4-.1l-2.3-1.2-2.3 1.2c-.1 0-.2.1-.3.1zm-.5-5.5l1.5 1.6-.3 2.2 1.9-1 1.9 1-.3-2.2 1.5-1.6-2.1-.4-1-1.9-1 1.9-2.1.4z" class="st4"/><linearGradient id="SVGID_2_" x1="157.53" x2="261.245" y1="152.614" y2="48.899" gradientTransform="translate(-89.178 -48.95)" gradientUnits="userSpaceOnUse"><stop offset=".28" stop-color="#7542e5"/><stop offset=".417" stop-color="#824deb"/><stop offset=".789" stop-color="#a067fa"/><stop offset="1" stop-color="#ab71ff"/></linearGradient><path fill="url(#SVGID_2_)" d="M135.8 20.7h-24.6c-3.4 0-6.1 2.8-6.1 6.1v43.3c0 3.4 2.8 6.1 6.1 6.1h24.6c3.4 0 6.1-2.8 6.1-6.1V26.8c0-3.3-2.7-6.1-6.1-6.1z"/><path fill="#ab71ff" d="M120 67.2h6.9v3.4H120z"/><path d="M127.5 51.5c-.4 0-.7.3-.7.7v.8c-1.8-1.8-4.7-1.9-6.5-.1-.6.6-1.1 1.4-1.3 2.2-.1.4.1.7.5.8h.2c.3 0 .6-.2.6-.5.4-1.5 1.7-2.5 3.2-2.5 1 0 2 .5 2.6 1.3h-1.3c-.4 0-.7.3-.7.6 0 .4.3.7.6.7h2.8c.4 0 .7-.3.7-.7v-2.7c0-.3-.3-.6-.7-.6zm0 5c-.4-.1-.7.1-.8.5-.4 1.5-1.7 2.5-3.2 2.5-1 0-2-.5-2.6-1.3h1.3c.4 0 .6-.3.6-.7 0-.3-.3-.6-.6-.6h-2.7c-.4 0-.7.3-.7.7v2.7c0 .4.3.7.6.7.4 0 .7-.3.7-.6v-.9c1.8 1.8 4.7 1.9 6.5.1.6-.6 1.1-1.4 1.3-2.2.2-.5 0-.8-.4-.9zm-4-29.4c-2.8 0-5.1 2.3-5.1 5.1s2.3 5.1 5.1 5.1 5.1-2.3 5.1-5.1-2.3-5.1-5.1-5.1zm0 8.9c-2.1 0-3.8-1.7-3.8-3.8 0-2.1 1.7-3.8 3.8-3.8 2.1 0 3.8 1.7 3.8 3.8 0 2.1-1.7 3.8-3.8 3.8zm2.2-3.8h-2.2V30c0-.2-.1-.3-.3-.3-.2 0-.3.1-.3.3v2.5c0 .2.1.3.3.3h2.5c.2 0 .3-.1.3-.3s-.1-.3-.3-.3zm-4.8 16.9c-.4 0-.7-.3-.7-.7v-.1l.4-2.7-1.8-1.9c-.3-.3-.2-.7 0-1 .1-.1.2-.1.3-.2l2.5-.5 1.2-2.4c.2-.3.6-.5.9-.3l.3.3 1.2 2.4 2.5.5c.4.1.6.4.6.8 0 .1-.1.2-.2.3l-1.8 1.9.4 2.7c.1.4-.2.7-.6.8-.1 0-.3 0-.4-.1l-2.3-1.2-2.3 1.2c0 .2-.1.2-.2.2zm-.5-5.5l1.5 1.6-.3 2.2 1.9-1 1.9 1-.3-2.2 1.5-1.6-2.1-.4-1-1.9-1 1.9-2.1.4z" class="st4"/><path fill="#7542e5" d="M135.5 23.7h-24c-1.9 0-3.4 1.5-3.4 3.4v37.1H139V27.1c-.1-1.9-1.6-3.4-3.5-3.4z" opacity=".5"/><linearGradient id="SVGID_3_" x1="258.012" x2="323.576" y1="130.654" y2="65.09" gradientTransform="translate(-89.178 -48.95)" gradientUnits="userSpaceOnUse"><stop offset=".432" stop-color="#00b3f4"/><stop offset=".609" stop-color="#00bbf6"/><stop offset=".891" stop-color="#00d2fc"/><stop offset="1" stop-color="#0df"/></linearGradient><path fill="url(#SVGID_3_)" d="M229.8 22.2h-55.6c-2.5 0-4.6 2.1-4.6 4.6v43.3c0 2.5 2.1 4.6 4.6 4.6h55.6c2.5 0 4.6-2.1 4.6-4.6V26.9c0-2.6-2-4.7-4.6-4.7z"/><path fill="#0df" d="M227.3 44.2h4.3v8.5h-4.3z"/><path fill="#0090ed" d="M225.4 25.2h-51.1c-.9 0-1.7.7-1.7 1.7V70c0 .9.7 1.7 1.7 1.7h51.1V25.2z" opacity=".5"/><path d="M127.5 51.5c-.4 0-.7.3-.7.7v.8c-1.8-1.8-4.7-1.9-6.5-.1-.6.6-1.1 1.4-1.3 2.2-.1.4.1.7.5.8h.2c.3 0 .6-.2.6-.5.4-1.5 1.7-2.5 3.2-2.5 1 0 2 .5 2.6 1.3h-1.3c-.4 0-.7.3-.7.6 0 .4.3.7.6.7h2.8c.4 0 .7-.3.7-.7v-2.7c0-.3-.3-.6-.7-.6zm0 5c-.4-.1-.7.1-.8.5-.4 1.5-1.7 2.5-3.2 2.5-1 0-2-.5-2.6-1.3h1.3c.4 0 .6-.3.6-.7 0-.3-.3-.6-.6-.6h-2.7c-.4 0-.7.3-.7.7v2.7c0 .4.3.7.6.7.4 0 .7-.3.7-.6v-.9c1.8 1.8 4.7 1.9 6.5.1.6-.6 1.1-1.4 1.3-2.2.2-.5 0-.8-.4-.9zm-4-29.4c-2.8 0-5.1 2.3-5.1 5.1s2.3 5.1 5.1 5.1 5.1-2.3 5.1-5.1-2.3-5.1-5.1-5.1zm0 8.9c-2.1 0-3.8-1.7-3.8-3.8 0-2.1 1.7-3.8 3.8-3.8 2.1 0 3.8 1.7 3.8 3.8 0 2.1-1.7 3.8-3.8 3.8zm2.2-3.8h-2.2V30c0-.2-.1-.3-.3-.3-.2 0-.3.1-.3.3v2.5c0 .2.1.3.3.3h2.5c.2 0 .3-.1.3-.3s-.1-.3-.3-.3zm-4.8 16.9c-.4 0-.7-.3-.7-.7v-.1l.4-2.7-1.8-1.9c-.3-.3-.2-.7 0-1 .1-.1.2-.1.3-.2l2.5-.5 1.2-2.4c.2-.3.6-.5.9-.3l.3.3 1.2 2.4 2.5.5c.4.1.6.4.6.8 0 .1-.1.2-.2.3l-1.8 1.9.4 2.7c.1.4-.2.7-.6.8-.1 0-.3 0-.4-.1l-2.3-1.2-2.3 1.2c0 .2-.1.2-.2.2zm-.5-5.5l1.5 1.6-.3 2.2 1.9-1 1.9 1-.3-2.2 1.5-1.6-2.1-.4-1-1.9-1 1.9-2.1.4zm94 .2c-.4 0-.7.3-.7.7v.8c-1.8-1.8-4.7-1.9-6.5-.1-.6.6-1.1 1.4-1.3 2.2-.1.4.1.7.5.8h.2c.3 0 .6-.2.6-.5.4-1.5 1.7-2.5 3.2-2.5 1 0 2 .5 2.6 1.3h-1.3c-.4 0-.7.3-.7.6 0 .4.3.7.6.7h2.8c.4 0 .7-.3.7-.7v-2.7c0-.3-.3-.6-.7-.6zm.1 5c-.4-.1-.7.1-.8.5-.4 1.5-1.7 2.5-3.2 2.5-1 0-2-.5-2.6-1.3h1.3c.4 0 .6-.3.6-.7 0-.3-.3-.6-.6-.6h-2.7c-.4 0-.7.3-.7.7v2.7c0 .4.3.7.6.7.4 0 .7-.3.7-.6v-.9c1.8 1.8 4.7 1.9 6.5.1.6-.6 1.1-1.4 1.3-2.2.1-.5-.1-.8-.4-.9zm-29.1-5.4c-2.8 0-5.1 2.3-5.1 5.1s2.3 5.1 5.1 5.1 5.1-2.3 5.1-5.1-2.3-5.1-5.1-5.1zm0 8.9c-2.1 0-3.8-1.7-3.8-3.8 0-2.1 1.7-3.8 3.8-3.8 2.1 0 3.8 1.7 3.8 3.8 0 2.1-1.7 3.8-3.8 3.8zm2.2-3.8h-2.2v-2.2c0-.2-.1-.3-.3-.3-.2 0-.3.1-.3.3v2.5c0 .2.1.3.3.3h2.5c.2 0 .3-.1.3-.3s-.1-.3-.3-.3zm7.7 4.9c-.4 0-.7-.3-.7-.7v-.1l.4-2.7-1.8-1.9c-.3-.3-.2-.7 0-1 .1-.1.2-.1.3-.2l2.5-.5 1.2-2.4c.2-.3.6-.5.9-.3l.3.3 1.2 2.4 2.5.5c.4.1.6.4.6.8 0 .1-.1.2-.2.3l-1.8 1.9.4 2.7c.1.4-.2.7-.6.8-.1 0-.3 0-.4-.1l-2.3-1.2-2.3 1.2c0 .2-.1.2-.2.2zm-.5-5.5l1.5 1.6-.3 2.2 1.9-1 1.9 1-.3-2.2 1.5-1.6-2.1-.4-1-1.9-1 1.9-2.1.4z" class="st4"/></svg>
\ No newline at end of file
--- a/browser/components/newtab/docs/v2-system-addon/data_events.md
+++ b/browser/components/newtab/docs/v2-system-addon/data_events.md
@@ -1048,8 +1048,25 @@ This reports a failure in the Remote Set
   "addon_version": "20180710100040",
   "locale": "en-US",
   "user_prefs": 7,
   "event": ["ASR_RS_NO_MESSAGES" | "ASR_RS_ERROR"],
   // The value is set to the ID of the message provider. For example: remote-cfr, remote-onboarding, etc.
   "value": "REMOTE_PROVIDER_ID"
 }
 ```
+
+## Trailhead experiment enrollment ping
+
+This reports an enrollment ping when a user gets enrolled in a Trailhead experiment. Note that this ping is only collected through the Mozilla Events telemetry pipeline.
+
+```js
+{
+  "category": "activity_stream",
+  "method": "enroll",
+  "object": "preference_study"
+  "value": "activity-stream-firstup-trailhead-interrupts",
+  "extra_keys": {
+    "experimentType": "as-firstrun",
+    "branch": ["supercharge" | "join" | "sync" | "privacy" ...]
+  }
+}
+```
--- a/browser/components/newtab/lib/ASRouter.jsm
+++ b/browser/components/newtab/lib/ASRouter.jsm
@@ -37,16 +37,17 @@ ChromeUtils.defineModuleGetter(this, "Cl
   "resource://normandy/lib/ClientEnvironment.jsm");
 ChromeUtils.defineModuleGetter(this, "Sampling",
   "resource://gre/modules/components-utils/Sampling.jsm");
 
 const TRAILHEAD_CONFIG = {
   OVERRIDE_PREF: "trailhead.firstrun.branches",
   DID_SEE_ABOUT_WELCOME_PREF: "trailhead.firstrun.didSeeAboutWelcome",
   INTERRUPTS_EXPERIMENT_PREF: "trailhead.firstrun.interruptsExperiment",
+  TRIPLETS_ENROLLED_PREF: "trailhead.firstrun.tripletsEnrolled",
   BRANCHES: {
     interrupts: [
       ["control"],
       ["join"],
       ["sync"],
       ["nofirstrun"],
       ["cards"],
     ],
@@ -727,38 +728,56 @@ class _ASRouter {
     } else {
       // If the user is not in a trailhead-compabtible locale, return the control experience and no experiment.
       interrupt = "control";
     }
 
     return {experiment, interrupt, triplet};
   }
 
+  // Dispatch a TRAILHEAD_ENROLL_EVENT action
+  _sendTrailheadEnrollEvent(data) {
+    this.dispatchToAS({
+      type: at.TRAILHEAD_ENROLL_EVENT,
+      data,
+    });
+  }
+
   async setupTrailhead() {
     // Don't initialize
     if (this.state.trailheadInitialized || !Services.prefs.getBoolPref(TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF, false)) {
       return;
     }
 
     const {experiment, interrupt, triplet} = await this._generateTrailheadBranches();
     await this.setState({trailheadInitialized: true, trailheadInterrupt: interrupt, trailheadTriplet: triplet});
 
     if (experiment) {
+      // In order for ping centre to pick this up, it MUST contain a substring activity-stream
+      const experimentName = `activity-stream-firstrun-trailhead-${experiment}`;
+
       TelemetryEnvironment.setExperimentActive(
-        // In order for ping centre to pick this up, it MUST start with activity-stream
-        `activity-stream-firstrun-trailhead-${experiment}`,
+        experimentName,
         experiment === "interrupts" ? interrupt : triplet,
         {type: "as-firstrun"}
       );
 
       // On the first time setting the interrupts experiment, expose the branch
-      // for normandy to target for survey study.
+      // for normandy to target for survey study, and send out the enrollment ping.
       if (experiment === "interrupts" &&
           !Services.prefs.prefHasUserValue(TRAILHEAD_CONFIG.INTERRUPTS_EXPERIMENT_PREF)) {
         Services.prefs.setStringPref(TRAILHEAD_CONFIG.INTERRUPTS_EXPERIMENT_PREF, interrupt);
+        this._sendTrailheadEnrollEvent({experiment: experimentName, type: "as-firstrun", branch: interrupt});
+      }
+
+      // On the first time setting the triplets experiment, send out the enrollment ping.
+      if (experiment === "triplets" &&
+          !Services.prefs.getBoolPref(TRAILHEAD_CONFIG.TRIPLETS_ENROLLED_PREF, false)) {
+        Services.prefs.setBoolPref(TRAILHEAD_CONFIG.TRIPLETS_ENROLLED_PREF, true);
+        this._sendTrailheadEnrollEvent({experiment: experimentName, type: "as-firstrun", branch: triplet});
       }
     }
   }
 
   // Return an object containing targeting parameters used to select messages
   _getMessagesContext() {
     const {previousSessionEnd, trailheadInterrupt, trailheadTriplet} = this.state;
 
--- a/browser/components/newtab/lib/OnboardingMessageProvider.jsm
+++ b/browser/components/newtab/lib/OnboardingMessageProvider.jsm
@@ -338,17 +338,17 @@ const ONBOARDING_MESSAGES = async () => 
     content: {
       title: {string_id: "onboarding-send-tabs-title"},
       text: {string_id: "onboarding-send-tabs-text"},
       icon: "sendtab",
       primary_button: {
         label: {string_id: "onboarding-send-tabs-button"},
         action: {
           type: "OPEN_URL",
-          data: {args: "https://blog.mozilla.org/firefox/send-tabs-a-better-way/", where: "tabshifted"},
+          data: {args: "https://support.mozilla.org/kb/send-tab-firefox-desktop-mobile", where: "tabshifted"},
         },
       },
     },
     targeting: "trailheadTriplet == 'multidevice'",
     trigger: {id: "showOnboarding"},
   },
   {
     id: "TRAILHEAD_CARD_8",
--- a/browser/components/newtab/lib/TelemetryFeed.jsm
+++ b/browser/components/newtab/lib/TelemetryFeed.jsm
@@ -658,16 +658,24 @@ this.TelemetryFeed = class TelemetryFeed
     let event = this.createASRouterEvent(action);
     this.sendASRouterEvent(event);
   }
 
   handleUndesiredEvent(action) {
     this.sendEvent(this.createUndesiredEvent(action));
   }
 
+  handleTrailheadEnrollEvent(action) {
+    // Unlike `sendUTEvent`, we always send the event if AS's telemetry is enabled
+    // regardless of `this.eventTelemetryEnabled`.
+    if (this.telemetryEnabled) {
+      this.utEvents.sendTrailheadEnrollEvent(action.data);
+    }
+  }
+
   async sendPageTakeoverData() {
     if (this.telemetryEnabled) {
       const value = {};
       let newtabAffected = false;
       let homeAffected = false;
 
       // Check whether or not about:home and about:newtab are set to a custom URL.
       // If so, classify them.
@@ -759,16 +767,19 @@ this.TelemetryFeed = class TelemetryFeed
         this.handleUserEvent(action);
         break;
       case at.AS_ROUTER_TELEMETRY_USER_EVENT:
         this.handleASRouterUserEvent(action);
         break;
       case at.TELEMETRY_PERFORMANCE_EVENT:
         this.sendEvent(this.createPerformanceEvent(action));
         break;
+      case at.TRAILHEAD_ENROLL_EVENT:
+        this.handleTrailheadEnrollEvent(action);
+        break;
       case at.UNINIT:
         this.uninit();
         break;
     }
   }
 
   /**
    * Handle impression stats actions from Discovery Stream. The data will be
--- a/browser/components/newtab/lib/UTEventReporting.jsm
+++ b/browser/components/newtab/lib/UTEventReporting.jsm
@@ -13,16 +13,17 @@ const {Services} = ChromeUtils.import("r
   */
 const EXTRAS_FIELD_NAMES = ["addon_version", "session_id", "page", "user_prefs", "action_position"];
 
 this.UTEventReporting = class UTEventReporting {
   constructor() {
     Services.telemetry.setEventRecordingEnabled("activity_stream", true);
     this.sendUserEvent = this.sendUserEvent.bind(this);
     this.sendSessionEndEvent = this.sendSessionEndEvent.bind(this);
+    this.sendTrailheadEnrollEvent = this.sendTrailheadEnrollEvent.bind(this);
   }
 
   _createExtras(data) {
     // Make a copy of the given data and delete/modify it as needed.
     let utExtras = Object.assign({}, data);
     for (let field of Object.keys(utExtras)) {
       if (EXTRAS_FIELD_NAMES.includes(field)) {
         utExtras[field] = String(utExtras[field]);
@@ -48,14 +49,27 @@ this.UTEventReporting = class UTEventRep
     Services.telemetry.recordEvent(
       "activity_stream",
       "end",
       "session",
       String(data.session_duration),
       this._createExtras(data));
   }
 
+  sendTrailheadEnrollEvent(data) {
+    Services.telemetry.recordEvent(
+      "activity_stream",
+      "enroll",
+      "preference_study",
+      data.experiment,
+      {
+        experimentType: data.type,
+        branch: data.branch,
+      }
+    );
+  }
+
   uninit() {
     Services.telemetry.setEventRecordingEnabled("activity_stream", false);
   }
 };
 
 const EXPORTED_SYMBOLS = ["UTEventReporting"];
--- a/browser/components/newtab/locales-src/he/strings.properties
+++ b/browser/components/newtab/locales-src/he/strings.properties
@@ -29,18 +29,18 @@ type_label_downloaded=התקבל
 # LOCALIZATION NOTE (menu_action_bookmark): Bookmark is a verb, as in "Add to
 # bookmarks"
 menu_action_bookmark=הוספת סימנייה
 menu_action_remove_bookmark=הסרת סימנייה
 menu_action_open_new_window=פתיחה בחלון חדש
 menu_action_open_private_window=פתיחה בלשונית פרטית חדשה
 menu_action_dismiss=הסרה
 menu_action_delete=מחיקה מההיסטוריה
-menu_action_pin=הצמדה
-menu_action_unpin=ביטול הצמדה
+menu_action_pin=נעיצה
+menu_action_unpin=ביטול נעיצה
 confirm_history_delete_p1=למחוק כל עותק של העמוד הזה מההיסטוריה שלך?
 # LOCALIZATION NOTE (confirm_history_delete_notice_p2): this string is displayed in
 # the same dialog as confirm_history_delete_p1. "This action" refers to deleting a
 # page from history.
 confirm_history_delete_notice_p2=לא ניתן לבטל פעולה זו.
 menu_action_save_to_pocket=שמירה ל־Pocket
 menu_action_delete_pocket=מחיקה מ־Pocket
 menu_action_archive_pocket=העברה לארכיון ב־Pocket
@@ -88,16 +88,17 @@ section_disclaimer_topstories_buttontext=בסדר, הבנתי
 # for a "Firefox Home" section. "Firefox" should be treated as a brand and kept
 # in English, while "Home" should be localized matching the about:preferences
 # sidebar mozilla-central string for the panel that has preferences related to
 # what is shown for the homepage, new windows, and new tabs.
 prefs_home_header=תוכן מסך הבית של Firefox
 prefs_home_description=בחירת תוכן שיוצג במסך הבית של Firefox.
 
 prefs_content_discovery_header=מסך הבית של Firefox
+
 prefs_content_discovery_description=גילוי תוכן במסך הבית של Firefox מאפשר לך לגלות מאמרים רלוונטים ובאיכות גבוהה מכל רחבי הרשת.
 prefs_content_discovery_button=השבתת גילוי תוכן
 
 # LOCALIZATION NOTE (prefs_section_rows_option): This is a semi-colon list of
 # plural forms used in a drop down of multiple row options (1 row, 2 rows).
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
 prefs_section_rows_option=שורה אחת;{num} שורות
 prefs_search_header=חיפוש ברשת
@@ -146,16 +147,17 @@ topsites_form_image_validation=טעינת התמונה נכשלה. נא לנסות כתובת שונה.
 # trending stories section and precedes a list of links to popular topics.
 pocket_read_more=נושאים פופולריים:
 # LOCALIZATION NOTE (pocket_read_even_more): This is shown as a link at the
 # end of the list of popular topic links.
 pocket_read_even_more=צפייה בחדשות נוספות
 pocket_more_reccommendations=המלצות נוספות
 pocket_how_it_works=איך זה עובד
 pocket_cta_button=קבלת Pocket
+pocket_cta_text=שמירת הסיפורים שאהבת ב־Pocket על מנת למלא את מחשבתך בקריאה מרתקת.
 
 highlights_empty_state=ניתן להתחיל בגלישה ואנו נציג בפניך מספר כתבות, סרטונים ועמודים שונים מעולים בהם ביקרת לאחרונה או שהוספת לסימניות.
 # LOCALIZATION NOTE (topstories_empty_state): When there are no recommendations,
 # in the space that would have shown a few stories, this is shown instead.
 # {provider} is replaced by the name of the content provider for this section.
 topstories_empty_state=התעדכנת בכל הסיפורים. כדאי לנסות שוב מאוחר יותר כדי לקבל עוד סיפורים מובילים מאת {provider}. לא רוצה לחכות? ניתן לבחור נושא נפוץ כדי למצוא עוד סיפורים נפלאים מרחבי הרשת.
 
 # LOCALIZATION NOTE (manual_migration_explanation2): This message is shown to encourage users to
--- a/browser/components/newtab/locales-src/it/strings.properties
+++ b/browser/components/newtab/locales-src/it/strings.properties
@@ -97,18 +97,18 @@ section_menu_action_manage_section=Gesti
 section_menu_action_manage_webext=Gestisci estensione
 section_menu_action_add_topsite=Aggiungi sito principale
 section_menu_action_add_search_engine=Aggiungi motore di ricerca
 section_menu_action_move_up=Sposta su
 section_menu_action_move_down=Sposta giù
 section_menu_action_privacy_notice=Informativa sulla privacy
 firstrun_title=Porta Firefox con te
 firstrun_content=Ritrova segnalibri, cronologia, password e altre impostazioni su tutti i tuoi dispositivi.
-firstrun_learn_more_link=Scopri di più sull’account Firefox
-firstrun_form_header=Inserisci email
+firstrun_learn_more_link=Ulteriori informazioni sull’account Firefox
+firstrun_form_header=Inserisci la tua email
 firstrun_form_sub_header=per utilizzare Firefox Sync.
 firstrun_email_input_placeholder=Email
 firstrun_invalid_input=Inserire un indirizzo email valido
 firstrun_extra_legal_links=Proseguendo, accetto le {terms} e l’{privacy}.
 firstrun_terms_of_service=condizioni di utilizzo del servizio
 firstrun_privacy_notice=informativa sulla privacy
 firstrun_continue_to_login=Continua
 firstrun_skip_login=Ignora questo passaggio
--- a/browser/components/newtab/locales-src/zh-TW/strings.properties
+++ b/browser/components/newtab/locales-src/zh-TW/strings.properties
@@ -88,16 +88,17 @@ section_disclaimer_topstories_buttontext=好的,知道了
 # for a "Firefox Home" section. "Firefox" should be treated as a brand and kept
 # in English, while "Home" should be localized matching the about:preferences
 # sidebar mozilla-central string for the panel that has preferences related to
 # what is shown for the homepage, new windows, and new tabs.
 prefs_home_header=Firefox 首頁內容
 prefs_home_description=選擇要在您的 Firefox 首頁顯示哪些內容。
 
 prefs_content_discovery_header=Firefox 首頁
+
 prefs_content_discovery_description=Firefox Home 的內容探索功能可隨您上網,為您尋找高品質而與您有關的文章。
 prefs_content_discovery_button=關閉內容探索功能
 
 # LOCALIZATION NOTE (prefs_section_rows_option): This is a semi-colon list of
 # plural forms used in a drop down of multiple row options (1 row, 2 rows).
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
 prefs_section_rows_option={num} 行
 prefs_search_header=網頁搜尋
@@ -185,17 +186,17 @@ section_menu_action_add_topsite=新增熱門網站
 section_menu_action_add_search_engine=新增搜尋引擎
 section_menu_action_move_up=上移
 section_menu_action_move_down=下移
 section_menu_action_privacy_notice=隱私權公告
 
 # LOCALIZATION NOTE (firstrun_*). These strings are displayed only once, on the
 # firstrun of the browser, they give an introduction to Firefox and Sync.
 firstrun_title=Firefox 隨身帶著走
-firstrun_content=在您的任何裝置上取得書籤、瀏覽紀錄、密碼及其他設定。
+firstrun_content=在您的各種裝置上同步書籤、瀏覽紀錄、登入資訊及其他設定。
 firstrun_learn_more_link=了解 Firefox Accounts 的更多資訊
 
 # LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
 # firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
 # firstrun_form_header is displayed more boldly as the call to action.
 firstrun_form_header=輸入您的電子郵件地址
 firstrun_form_sub_header=繼續前往 Firefox Sync
 
--- a/browser/components/newtab/prerendered/locales/he/activity-stream-strings.js
+++ b/browser/components/newtab/prerendered/locales/he/activity-stream-strings.js
@@ -12,18 +12,18 @@ window.gActivityStreamStrings = {
   "type_label_pocket": "נשמר ל־Pocket",
   "type_label_downloaded": "התקבל",
   "menu_action_bookmark": "הוספת סימנייה",
   "menu_action_remove_bookmark": "הסרת סימנייה",
   "menu_action_open_new_window": "פתיחה בחלון חדש",
   "menu_action_open_private_window": "פתיחה בלשונית פרטית חדשה",
   "menu_action_dismiss": "הסרה",
   "menu_action_delete": "מחיקה מההיסטוריה",
-  "menu_action_pin": "הצמדה",
-  "menu_action_unpin": "ביטול הצמדה",
+  "menu_action_pin": "נעיצה",
+  "menu_action_unpin": "ביטול נעיצה",
   "confirm_history_delete_p1": "למחוק כל עותק של העמוד הזה מההיסטוריה שלך?",
   "confirm_history_delete_notice_p2": "לא ניתן לבטל פעולה זו.",
   "menu_action_save_to_pocket": "שמירה ל־Pocket",
   "menu_action_delete_pocket": "מחיקה מ־Pocket",
   "menu_action_archive_pocket": "העברה לארכיון ב־Pocket",
   "menu_action_show_file_mac_os": "הצגה ב־Finder",
   "menu_action_show_file_windows": "פתיחת תיקייה מכילה",
   "menu_action_show_file_linux": "פתיחת תיקייה מכילה",
@@ -73,17 +73,17 @@ window.gActivityStreamStrings = {
   "topsites_form_cancel_button": "ביטול",
   "topsites_form_url_validation": "נדרשת כתובת תקינה",
   "topsites_form_image_validation": "טעינת התמונה נכשלה. נא לנסות כתובת שונה.",
   "pocket_read_more": "נושאים פופולריים:",
   "pocket_read_even_more": "צפייה בחדשות נוספות",
   "pocket_more_reccommendations": "המלצות נוספות",
   "pocket_how_it_works": "איך זה עובד",
   "pocket_cta_button": "קבלת Pocket",
-  "pocket_cta_text": "Save the stories you love in Pocket, and fuel your mind with fascinating reads.",
+  "pocket_cta_text": "שמירת הסיפורים שאהבת ב־Pocket על מנת למלא את מחשבתך בקריאה מרתקת.",
   "highlights_empty_state": "ניתן להתחיל בגלישה ואנו נציג בפניך מספר כתבות, סרטונים ועמודים שונים מעולים בהם ביקרת לאחרונה או שהוספת לסימניות.",
   "topstories_empty_state": "התעדכנת בכל הסיפורים. כדאי לנסות שוב מאוחר יותר כדי לקבל עוד סיפורים מובילים מאת {provider}. לא רוצה לחכות? ניתן לבחור נושא נפוץ כדי למצוא עוד סיפורים נפלאים מרחבי הרשת.",
   "error_fallback_default_info": "אופס, משהו השתבש בעת טעינת התוכן הזה.",
   "error_fallback_default_refresh_suggestion": "נא לרענן את הדף כדי לנסות שוב.",
   "section_menu_action_remove_section": "הסרת מדור",
   "section_menu_action_collapse_section": "צמצום מדור",
   "section_menu_action_expand_section": "הרחבת מדור",
   "section_menu_action_manage_section": "ניהול מדור",
--- a/browser/components/newtab/prerendered/locales/it/activity-stream-strings.js
+++ b/browser/components/newtab/prerendered/locales/it/activity-stream-strings.js
@@ -90,18 +90,18 @@ window.gActivityStreamStrings = {
   "section_menu_action_manage_webext": "Gestisci estensione",
   "section_menu_action_add_topsite": "Aggiungi sito principale",
   "section_menu_action_add_search_engine": "Aggiungi motore di ricerca",
   "section_menu_action_move_up": "Sposta su",
   "section_menu_action_move_down": "Sposta giù",
   "section_menu_action_privacy_notice": "Informativa sulla privacy",
   "firstrun_title": "Porta Firefox con te",
   "firstrun_content": "Ritrova segnalibri, cronologia, password e altre impostazioni su tutti i tuoi dispositivi.",
-  "firstrun_learn_more_link": "Scopri di più sull’account Firefox",
-  "firstrun_form_header": "Inserisci email",
+  "firstrun_learn_more_link": "Ulteriori informazioni sull’account Firefox",
+  "firstrun_form_header": "Inserisci la tua email",
   "firstrun_form_sub_header": "per utilizzare Firefox Sync.",
   "firstrun_email_input_placeholder": "Email",
   "firstrun_invalid_input": "Inserire un indirizzo email valido",
   "firstrun_extra_legal_links": "Proseguendo, accetto le {terms} e l’{privacy}.",
   "firstrun_terms_of_service": "condizioni di utilizzo del servizio",
   "firstrun_privacy_notice": "informativa sulla privacy",
   "firstrun_continue_to_login": "Continua",
   "firstrun_skip_login": "Ignora questo passaggio",
--- a/browser/components/newtab/prerendered/locales/zh-TW/activity-stream-strings.js
+++ b/browser/components/newtab/prerendered/locales/zh-TW/activity-stream-strings.js
@@ -89,17 +89,17 @@ window.gActivityStreamStrings = {
   "section_menu_action_manage_section": "管理段落",
   "section_menu_action_manage_webext": "管理擴充套件",
   "section_menu_action_add_topsite": "新增熱門網站",
   "section_menu_action_add_search_engine": "新增搜尋引擎",
   "section_menu_action_move_up": "上移",
   "section_menu_action_move_down": "下移",
   "section_menu_action_privacy_notice": "隱私權公告",
   "firstrun_title": "Firefox 隨身帶著走",
-  "firstrun_content": "在您的任何裝置上取得書籤、瀏覽紀錄、密碼及其他設定。",
+  "firstrun_content": "在您的各種裝置上同步書籤、瀏覽紀錄、登入資訊及其他設定。",
   "firstrun_learn_more_link": "了解 Firefox Accounts 的更多資訊",
   "firstrun_form_header": "輸入您的電子郵件地址",
   "firstrun_form_sub_header": "繼續前往 Firefox Sync",
   "firstrun_email_input_placeholder": "電子郵件",
   "firstrun_invalid_input": "必須輸入有效的電子郵件地址",
   "firstrun_extra_legal_links": "若繼續,代表您同意{terms}及{privacy}。",
   "firstrun_terms_of_service": "服務條款",
   "firstrun_privacy_notice": "隱私權公告",
--- a/browser/components/newtab/test/schemas/pings.js
+++ b/browser/components/newtab/test/schemas/pings.js
@@ -242,16 +242,29 @@ export const ASRouterEventPing = Joi.obj
 export const UTSessionPing = Joi.array().items(
   Joi.string().required().valid("activity_stream"),
   Joi.string().required().valid("end"),
   Joi.string().required().valid("session"),
   Joi.string().required(),
   eventsTelemetryExtraKeys
 );
 
+export const trailheadEnrollExtraKeys = Joi.object().keys({
+  experimentType: Joi.string().required(),
+  branch: Joi.string().required(),
+}).options({allowUnknown: false});
+
+export const UTTrailheadEnrollPing = Joi.array().items(
+  Joi.string().required().valid("activity_stream"),
+  Joi.string().required().valid("enroll"),
+  Joi.string().required().valid("preference_study"),
+  Joi.string().required(),
+  trailheadEnrollExtraKeys
+);
+
 export function chaiAssertions(_chai, utils) {
   const {Assertion} = _chai;
 
   Assertion.addMethod("validate", function(schema, schemaName) {
     const {error} = Joi.validate(this._obj, schema, {allowUnknown: false});
     this.assert(
       !error,
       `Expected to be ${schemaName ? `a valid ${schemaName}` : "valid"} but there were errors: ${error}`
--- a/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
+++ b/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
@@ -1596,23 +1596,28 @@ describe("ASRouter", () => {
 
       assert.propertyVal(Router._getMessagesContext(), "trailheadInterrupt", "join");
       assert.propertyVal(Router._getMessagesContext(), "trailheadTriplet", "privacy");
     });
 
     describe(".setupTrailhead", () => {
       let getBoolPrefStub;
       let setStringPrefStub;
+      let setBoolPrefStub;
 
       beforeEach(() => {
-        getBoolPrefStub = sandbox.stub(global.Services.prefs, "getBoolPref").withArgs(TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF).returns(true);
+        getBoolPrefStub = sandbox.stub(global.Services.prefs, "getBoolPref");
+        getBoolPrefStub.withArgs(TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF).returns(true);
+        getBoolPrefStub.withArgs(TRAILHEAD_CONFIG.TRIPLETS_ENROLLED_PREF).returns(false);
         setStringPrefStub = sandbox.stub(global.Services.prefs, "setStringPref");
+        setBoolPrefStub = sandbox.stub(global.Services.prefs, "setBoolPref");
       });
 
-      const configWithExperiment = {experiment: "interrupts", interrupt: "join", triplet: "privacy"};
+      const configWithInterruptsExperiment = {experiment: "interrupts", interrupt: "join", triplet: "privacy"};
+      const configWithTripletsExperiment = {experiment: "triplets", interrupt: "join", triplet: "privacy"};
       const configWithoutExperiment = {experiment: "", interrupt: "control", triplet: ""};
 
       it("should generates an experiment/branch configuration and update Router.state", async () => {
         const config = configWithoutExperiment;
         sandbox.stub(Router, "_generateTrailheadBranches").resolves(config);
 
         await Router.setupTrailhead();
 
@@ -1632,24 +1637,45 @@ describe("ASRouter", () => {
       it("should return early if DID_SEE_ABOUT_WELCOME_PREF is false", async () => {
         getBoolPrefStub.withArgs(TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF).returns(false);
 
         await Router.setupTrailhead();
 
         sandbox.spy(Router, "setState");
         assert.notCalled(Router.setState);
       });
-      it("should set active experiment if one is defined", async () => {
-        sandbox.stub(Router, "_generateTrailheadBranches").resolves(configWithExperiment);
+      it("should set active interrupts experiment if one is defined", async () => {
+        sandbox.stub(Router, "_generateTrailheadBranches").resolves(configWithInterruptsExperiment);
         sandbox.stub(global.TelemetryEnvironment, "setExperimentActive");
+        sandbox.spy(Router, "_sendTrailheadEnrollEvent");
 
         await Router.setupTrailhead();
 
         assert.calledOnce(global.TelemetryEnvironment.setExperimentActive);
         assert.calledWith(setStringPrefStub, TRAILHEAD_CONFIG.INTERRUPTS_EXPERIMENT_PREF, "join");
+        assert.calledWith(Router._sendTrailheadEnrollEvent, {
+          experiment: "activity-stream-firstrun-trailhead-interrupts",
+          type: "as-firstrun",
+          branch: "join",
+        });
+      });
+      it("should set active triplets experiment if one is defined", async () => {
+        sandbox.stub(Router, "_generateTrailheadBranches").resolves(configWithTripletsExperiment);
+        sandbox.stub(global.TelemetryEnvironment, "setExperimentActive");
+        sandbox.spy(Router, "_sendTrailheadEnrollEvent");
+
+        await Router.setupTrailhead();
+
+        assert.calledOnce(global.TelemetryEnvironment.setExperimentActive);
+        assert.calledWith(setBoolPrefStub, TRAILHEAD_CONFIG.TRIPLETS_ENROLLED_PREF, true);
+        assert.calledWith(Router._sendTrailheadEnrollEvent, {
+          experiment: "activity-stream-firstrun-trailhead-triplets",
+          type: "as-firstrun",
+          branch: "privacy",
+        });
       });
       it("should not set an active experiment if no experiment is defined", async () => {
         sandbox.stub(Router, "_generateTrailheadBranches").resolves(configWithoutExperiment);
         sandbox.stub(global.TelemetryEnvironment, "setExperimentActive");
 
         await Router.setupTrailhead();
 
         assert.notCalled(global.TelemetryEnvironment.setExperimentActive);
--- a/browser/components/newtab/test/unit/asrouter/templates/Trailhead.test.jsx
+++ b/browser/components/newtab/test/unit/asrouter/templates/Trailhead.test.jsx
@@ -58,16 +58,21 @@ describe("<Trailhead>", () => {
     assert.ok(skipButton.exists());
     skipButton.simulate("click");
 
     assert.calledOnce(dispatch);
     assert.isUserEventAction(dispatch.firstCall.args[0]);
     assert.calledWith(dispatch, ac.UserEvent({event: at.SKIPPED_SIGNIN, value: {has_flow_params: false}}));
   });
 
+  it("should NOT emit UserEvent SKIPPED_SIGNIN when closeModal is triggered by visibilitychange event", () => {
+    wrapper.instance().closeModal({type: "visibilitychange"});
+    assert.notCalled(dispatch);
+  });
+
   it("should emit UserEvent SUBMIT_EMAIL when you submit the form", () => {
     let form = wrapper.find("form");
     assert.ok(form.exists());
     form.simulate("submit");
 
     assert.calledOnce(dispatch);
     assert.isUserEventAction(dispatch.firstCall.args[0]);
     assert.calledWith(dispatch, ac.UserEvent({event: at.SUBMIT_EMAIL, value: {has_flow_params: false}}));
--- a/browser/components/newtab/test/unit/content-src/lib/selectLayoutRender.test.js
+++ b/browser/components/newtab/test/unit/content-src/lib/selectLayoutRender.test.js
@@ -348,9 +348,28 @@ describe("selectLayoutRender", () => {
     store.dispatch({type: at.DISCOVERY_STREAM_FEED_UPDATE, data: {feed: {data: {recommendations: [{name: "rec"}]}}, url: "foo3.com"}});
     store.dispatch({type: at.DISCOVERY_STREAM_FEED_UPDATE, data: {feed: {data: {recommendations: []}}, url: "foo4.com"}});
     store.dispatch({type: at.DISCOVERY_STREAM_SPOCS_UPDATE, data: fakeSpocsData});
 
     const {layoutRender} = selectLayoutRender(store.getState().DiscoveryStream, {}, []);
 
     assert.deepEqual(layoutRender[0].components[2].data.recommendations[0], {name: "rec", pos: 0});
   });
+  it("should not render a row if no components exist after filter in that row", () => {
+    const fakeLayout = [{
+      width: 3,
+      components: [
+        {type: "TopSites"},
+      ],
+    }, {
+      width: 3,
+      components: [
+        {type: "Message"},
+      ],
+    }];
+    store.dispatch({type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, data: {layout: fakeLayout}});
+
+    const {layoutRender} = selectLayoutRender(store.getState().DiscoveryStream, {"feeds.topsites": true}, []);
+
+    assert.equal(layoutRender[0].components[0].type, "TopSites");
+    assert.equal(layoutRender[1], undefined);
+  });
 });
--- a/browser/components/newtab/test/unit/lib/TelemetryFeed.test.js
+++ b/browser/components/newtab/test/unit/lib/TelemetryFeed.test.js
@@ -24,17 +24,22 @@ describe("TelemetryFeed", () => {
   let expectedUserPrefs;
   let browser = {getAttribute() { return "true"; }};
   let instance;
   let clock;
   let fakeHomePageUrl;
   let fakeHomePage;
   let fakeExtensionSettingsStore;
   class PingCentre {sendPing() {} uninit() {} sendStructuredIngestionPing() {}}
-  class UTEventReporting {sendUserEvent() {} sendSessionEndEvent() {} uninit() {}}
+  class UTEventReporting {
+    sendUserEvent() {}
+    sendSessionEndEvent() {}
+    sendTrailheadEnrollEvent() {}
+    uninit() {}
+  }
   class PerfService {
     getMostRecentAbsMarkStartByName() { return 1234; }
     mark() {}
     absNow() { return 123; }
     get timeOrigin() { return 123456; }
   }
   const perfService = new PerfService();
   const {
@@ -1127,16 +1132,25 @@ describe("TelemetryFeed", () => {
       ];
       const action = ac.DiscoveryStreamSpocsFill({spoc_fills: spocFills});
 
       instance.onAction(action);
 
       assert.calledWith(eventCreator, action.data);
       assert.calledWith(sendEvent, eventCreator.returnValue);
     });
+    it("should call .handleTrailheadEnrollEvent on a TRAILHEAD_ENROLL_EVENT action", () => {
+      const data = {experiment: "foo", type: "bar", branch: "baz"};
+      const action = {type: at.TRAILHEAD_ENROLL_EVENT, data};
+      sandbox.spy(instance, "handleTrailheadEnrollEvent");
+
+      instance.onAction(action);
+
+      assert.calledWith(instance.handleTrailheadEnrollEvent, action);
+    });
   });
   describe("#handlePagePrerendered", () => {
     it("should not throw if there is no session for the given port ID", () => {
       assert.doesNotThrow(() => instance.handlePagePrerendered("doesn't exist"));
     });
     it("should set the session as prerendered on a PAGE_PRERENDERED action", () => {
       const session = {perf: {}};
       sandbox.stub(instance.sessions, "get").returns(session);
@@ -1358,9 +1372,31 @@ describe("TelemetryFeed", () => {
       FakePrefs.prototype.prefs[STRUCTURED_INGESTION_ENDPOINT_PREF] = fakeEndpoint;
       sandbox.stub(global.gUUIDGenerator, "generateUUID").returns(fakeUUID);
       const feed = new TelemetryFeed();
       const url = feed._generateStructuredIngestionEndpoint("testPingType", "1");
 
       assert.equal(url, `${fakeEndpoint}/testPingType/1/${fakeUUIDWithoutBraces}`);
     });
   });
+  describe("#handleTrailheadEnrollEvent", () => {
+    it("should send a TRAILHEAD_ENROLL_EVENT if the telemetry is enabled", () => {
+      FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;
+      const data = {experiment: "foo", type: "bar", branch: "baz"};
+      instance = new TelemetryFeed();
+      sandbox.stub(instance.utEvents, "sendTrailheadEnrollEvent");
+
+      instance.handleTrailheadEnrollEvent({data});
+
+      assert.calledWith(instance.utEvents.sendTrailheadEnrollEvent, data);
+    });
+    it("should not send TRAILHEAD_ENROLL_EVENT if the telemetry is disabled", () => {
+      FakePrefs.prototype.prefs[TELEMETRY_PREF] = false;
+      const data = {experiment: "foo", type: "bar", branch: "baz"};
+      instance = new TelemetryFeed();
+      sandbox.stub(instance.utEvents, "sendTrailheadEnrollEvent");
+
+      instance.handleTrailheadEnrollEvent({data});
+
+      assert.notCalled(instance.utEvents.sendTrailheadEnrollEvent);
+    });
+  });
 });
--- a/browser/components/newtab/test/unit/lib/UTEventReporting.test.js
+++ b/browser/components/newtab/test/unit/lib/UTEventReporting.test.js
@@ -1,10 +1,11 @@
 import {
   UTSessionPing,
+  UTTrailheadEnrollPing,
   UTUserEventPing,
 } from "test/schemas/pings";
 import {GlobalOverrider} from "test/unit/utils";
 import {UTEventReporting} from "lib/UTEventReporting.jsm";
 
 const FAKE_EVENT_PING_PC = {
   "event": "CLICK",
   "source": "TOP_SITES",
@@ -35,16 +36,27 @@ const FAKE_EVENT_PING_UT = [
 const FAKE_SESSION_PING_UT = [
   "activity_stream", "end", "session", "1234", {
     "addon_version": "123",
     "user_prefs": "63",
     "session_id": "abc",
     "page": "about:newtab",
   },
 ];
+const FAKE_TRAILHEAD_ENROLL_EVENT = {
+  experiment: "activity-stream-trailhead-firstrun-interrupts",
+  type: "as-firstrun",
+  branch: "supercharge",
+};
+const FAKE_TRAILHEAD_ENROLL_EVENT_UT = [
+  "activity_stream", "enroll", "preference_study", "activity-stream-trailhead-firstrun-interrupts", {
+    experimentType: "as-firstrun",
+    branch: "supercharge",
+  },
+];
 
 describe("UTEventReporting", () => {
   let globals;
   let sandbox;
   let utEvents;
 
   beforeEach(() => {
     globals = new GlobalOverrider();
@@ -74,16 +86,26 @@ describe("UTEventReporting", () => {
       utEvents.sendSessionEndEvent(FAKE_SESSION_PING_PC);
       assert.calledWithExactly(global.Services.telemetry.recordEvent, ...FAKE_SESSION_PING_UT);
 
       let ping = global.Services.telemetry.recordEvent.firstCall.args;
       assert.validate(ping, UTSessionPing);
     });
   });
 
+  describe("#sendTrailheadEnrollEvent()", () => {
+    it("should queue up the correct data to send to Events Telemetry", async () => {
+      utEvents.sendTrailheadEnrollEvent(FAKE_TRAILHEAD_ENROLL_EVENT);
+      assert.calledWithExactly(global.Services.telemetry.recordEvent, ...FAKE_TRAILHEAD_ENROLL_EVENT_UT);
+
+      let ping = global.Services.telemetry.recordEvent.firstCall.args;
+      assert.validate(ping, UTTrailheadEnrollPing);
+    });
+  });
+
   describe("#uninit()", () => {
     it("should call setEventRecordingEnabled with a false value", () => {
       assert.equal(global.Services.telemetry.setEventRecordingEnabled.firstCall.args[0], "activity_stream");
       assert.equal(global.Services.telemetry.setEventRecordingEnabled.firstCall.args[1], true);
 
       utEvents.uninit();
       assert.equal(global.Services.telemetry.setEventRecordingEnabled.secondCall.args[0], "activity_stream");
       assert.equal(global.Services.telemetry.setEventRecordingEnabled.secondCall.args[1], false);