Bug 1598724 - Add event ping migration, DE layout with DS, and campaign to flight id value migration to New Tab Page r=Mardak

Differential Revision: https://phabricator.services.mozilla.com/D54348

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Scott 2019-11-22 20:06:55 +00:00
Родитель 116ada0728
Коммит 02c08eadfc
32 изменённых файлов: 3294 добавлений и 2815 удалений

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

@ -26,7 +26,7 @@ const ALLOWED_CSS_URL_PREFIXES = [
"https://img-getpocket.cdn.mozilla.net/",
];
const DUMMY_CSS_SELECTOR = "DUMMY#CSS.SELECTOR";
let rickRollCache = []; // Cache of random probability values for a spoc position
let rollCache = []; // Cache of random probability values for a spoc position
/**
* Validate a CSS declaration. The values are assumed to be normalized by CSSOM.
@ -154,7 +154,7 @@ export class _DiscoveryStreamBase extends React.PureComponent {
url,
context,
cta,
campaign_id,
flight_id,
id,
shim,
} = spoc;
@ -179,7 +179,7 @@ export class _DiscoveryStreamBase extends React.PureComponent {
cta_text={cta}
cta_url={url}
subtitle={context}
campaignId={campaign_id}
flightId={flight_id}
id={id}
pos={0}
shim={shim}
@ -264,17 +264,18 @@ export class _DiscoveryStreamBase extends React.PureComponent {
componentWillReceiveProps(oldProps) {
if (this.props.DiscoveryStream.layout !== oldProps.DiscoveryStream.layout) {
rickRollCache = [];
rollCache = [];
}
}
render() {
// Select layout render data by adding spocs and position to recommendations
const { layoutRender, spocsFill } = selectLayoutRender(
this.props.DiscoveryStream,
this.props.Prefs.values,
rickRollCache
);
const { layoutRender, spocsFill } = selectLayoutRender({
state: this.props.DiscoveryStream,
prefs: this.props.Prefs.values,
rollCache,
lang: this.props.document.documentElement.lang,
});
const { config, spocs, feeds } = this.props.DiscoveryStream;
// Send SPOCS Fill if any. Note that it should not send it again if the same
@ -412,4 +413,5 @@ export const DiscoveryStreamBase = connect(state => ({
DiscoveryStream: state.DiscoveryStream,
Prefs: state.Prefs,
Sections: state.Sections,
document: global.document,
}))(_DiscoveryStreamBase);

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

@ -20,7 +20,7 @@ export class CardGrid extends React.PureComponent {
<DSCard
key={`dscard-${rec.id}`}
pos={rec.pos}
campaignId={rec.campaign_id}
flightId={rec.flight_id}
image_src={rec.image_src}
raw_image_src={rec.raw_image_src}
title={rec.title}

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

@ -213,7 +213,7 @@ export class DSCard extends React.PureComponent {
/>
)}
<ImpressionStats
campaignId={this.props.campaignId}
flightId={this.props.flightId}
rows={[
{
id: this.props.id,
@ -238,7 +238,7 @@ export class DSCard extends React.PureComponent {
pocket_id={this.props.pocket_id}
shim={this.props.shim}
bookmarkGuid={this.props.bookmarkGuid}
campaignId={this.props.campaignId}
flightId={this.props.flightId}
/>
</div>
);

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

@ -47,7 +47,7 @@ export class DSLinkMenu extends React.PureComponent {
"OpenInPrivateWindow",
"Separator",
"BlockUrl",
...(this.props.campaignId ? ["ShowPrivacyInfo"] : []),
...(this.props.flightId ? ["ShowPrivacyInfo"] : []),
];
const type = this.props.type || "DISCOVERY_STREAM";
const title = this.props.title || this.props.source;
@ -76,7 +76,7 @@ export class DSLinkMenu extends React.PureComponent {
pocket_id: this.props.pocket_id,
shim: this.props.shim,
bookmarkGuid: this.props.bookmarkGuid,
campaign_id: this.props.campaignId,
flight_id: this.props.flightId,
}}
/>
</ContextMenuButton>

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

@ -4,6 +4,7 @@
import React from "react";
import { SafeAnchor } from "../SafeAnchor/SafeAnchor";
import { FluentOrText } from "content-src/components/FluentOrText/FluentOrText";
export class DSMessage extends React.PureComponent {
render() {
@ -17,11 +18,13 @@ export class DSMessage extends React.PureComponent {
/>
)}
{this.props.title && (
<span className="title-text">{this.props.title}</span>
<span className="title-text">
<FluentOrText message={this.props.title} />
</span>
)}
{this.props.link_text && this.props.link_url && (
<SafeAnchor className="link" url={this.props.link_url}>
{this.props.link_text}
<FluentOrText message={this.props.link_text} />
</SafeAnchor>
)}
</header>

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

@ -65,7 +65,7 @@ export class DSTextPromo extends React.PureComponent {
<p className="subtitle">{this.props.subtitle}</p>
</div>
<ImpressionStats
campaignId={this.props.campaignId}
flightId={this.props.flightId}
rows={[
{
id: this.props.id,

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

@ -62,7 +62,7 @@ export class Hero extends React.PureComponent {
<PlaceholderDSCard key={`dscard-${index}`} />
) : (
<DSCard
campaignId={rec.campaign_id}
flightId={rec.flight_id}
key={`dscard-${rec.id}`}
image_src={rec.image_src}
raw_image_src={rec.raw_image_src}
@ -117,7 +117,7 @@ export class Hero extends React.PureComponent {
/>
</div>
<ImpressionStats
campaignId={heroRec.campaign_id}
flightId={heroRec.flight_id}
rows={[
{
id: heroRec.id,
@ -142,7 +142,7 @@ export class Hero extends React.PureComponent {
pocket_id={heroRec.pocket_id}
shim={heroRec.shim}
bookmarkGuid={heroRec.bookmarkGuid}
campaignId={heroRec.campaign_id}
flightId={heroRec.flight_id}
/>
</div>
);

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

@ -90,7 +90,7 @@ export class ListItem extends React.PureComponent {
rawSource={this.props.raw_image_src}
/>
<ImpressionStats
campaignId={this.props.campaignId}
flightId={this.props.flightId}
rows={[
{
id: this.props.id,
@ -116,7 +116,7 @@ export class ListItem extends React.PureComponent {
pocket_id={this.props.pocket_id}
shim={this.props.shim}
bookmarkGuid={this.props.bookmarkGuid}
campaignId={this.props.campaignId}
flightId={this.props.flightId}
/>
)}
</li>
@ -146,7 +146,7 @@ export function _List(props) {
<ListItem
key={`ds-list-item-${rec.id}`}
dispatch={props.dispatch}
campaignId={rec.campaign_id}
flightId={rec.flight_id}
domain={rec.domain}
excerpt={rec.excerpt}
id={rec.id}

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

@ -4,6 +4,7 @@
import React from "react";
import { SafeAnchor } from "../SafeAnchor/SafeAnchor";
import { FluentOrText } from "content-src/components/FluentOrText/FluentOrText";
export class Topic extends React.PureComponent {
render() {
@ -25,7 +26,11 @@ export class Navigation extends React.PureComponent {
const header = this.props.header || {};
return (
<div className={`ds-navigation ds-navigation-${alignment}`}>
{header.title ? <div className="ds-header">{header.title}</div> : null}
{header.title ? (
<FluentOrText message={header.title}>
<div className="ds-header" />
</FluentOrText>
) : null}
<div>
<ul>
{links &&

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

@ -84,7 +84,7 @@ export class _TopSites extends React.PureComponent {
label: topSiteSpoc.sponsor,
title: topSiteSpoc.sponsor,
url: topSiteSpoc.url,
campaignId: topSiteSpoc.campaign_id,
flightId: topSiteSpoc.flight_id,
id: topSiteSpoc.id,
guid: topSiteSpoc.id,
shim: topSiteSpoc.shim,

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

@ -55,11 +55,11 @@ export class ImpressionStats extends React.PureComponent {
const { props } = this;
const cards = props.rows;
if (this.props.campaignId) {
if (this.props.flightId) {
this.props.dispatch(
ac.OnlyToMain({
type: at.DISCOVERY_STREAM_SPOC_IMPRESSION,
data: { campaignId: this.props.campaignId },
data: { flightId: this.props.flightId },
})
);
}

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

@ -271,7 +271,7 @@ export class TopSiteLink extends React.PureComponent {
{children}
{link.type === SPOC_TYPE ? (
<ImpressionStats
campaignId={link.campaignId}
flightId={link.flightId}
rows={[
{
id: link.id,

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

@ -62,9 +62,9 @@ export const LinkMenuOptions = {
userEvent: "OPEN_NEW_WINDOW",
}),
// This blocks the url for regular stories,
// but also sends a message to DiscoveryStream with campaign_id.
// If DiscoveryStream sees this message for a campaign_id
// it also blocks it on the campaign_id.
// but also sends a message to DiscoveryStream with flight_id.
// If DiscoveryStream sees this message for a flight_id
// it also blocks it on the flight_id.
BlockUrl: (site, index, eventSource) => ({
id: "newtab-menu-dismiss",
icon: "dismiss",
@ -73,7 +73,7 @@ export const LinkMenuOptions = {
data: {
url: site.open_url || site.url,
pocket_id: site.pocket_id,
...(site.campaign_id ? { campaign_id: site.campaign_id } : {}),
...(site.flight_id ? { flight_id: site.flight_id } : {}),
},
}),
impression: ac.ImpressionStats({

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

@ -2,7 +2,12 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
export const selectLayoutRender = (state, prefs, rickRollCache) => {
export const selectLayoutRender = ({
state = {},
prefs = {},
rollCache = [],
lang = "",
}) => {
const { layout, feeds, spocs } = state;
let spocIndexMap = {};
let bufferRollCache = [];
@ -23,11 +28,11 @@ export const selectLayoutRender = (state, prefs, rickRollCache) => {
// Cache random number for a position
let rickRoll;
if (!rickRollCache.length) {
if (!rollCache.length) {
rickRoll = Math.random();
bufferRollCache.push(rickRoll);
} else {
rickRoll = rickRollCache.shift();
rickRoll = rollCache.shift();
bufferRollCache.push(rickRoll);
}
@ -63,6 +68,10 @@ export const selectLayoutRender = (state, prefs, rickRollCache) => {
filterArray.push("TopSites");
}
if (!lang.startsWith("en-")) {
filterArray.push("Navigation");
}
if (!prefs["feeds.section.topstories"]) {
filterArray.push(...DS_COMPONENTS);
}
@ -213,9 +222,9 @@ export const selectLayoutRender = (state, prefs, rickRollCache) => {
const layoutRender = renderLayout();
// If empty, fill rickRollCache with random probability values from bufferRollCache
if (!rickRollCache.length) {
rickRollCache.push(...bufferRollCache);
// If empty, fill rollCache with random probability values from bufferRollCache
if (!rollCache.length) {
rollCache.push(...bufferRollCache);
}
// Generate the payload for the SPOCS Fill ping. Note that a SPOC could be rejected

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -178,7 +178,7 @@ Schema definitions/validations that can be used for tests can be found in `syste
{"id": 10000, displayed: 0, reason: "frequency_cap", full_recalc: 1},
{"id": 10001, displayed: 0, reason: "blocked_by_user", full_recalc: 1},
{"id": 10002, displayed: 0, reason: "below_min_score", full_recalc: 1},
{"id": 10003, displayed: 0, reason: "campaign_duplicate", full_recalc: 1},
{"id": 10003, displayed: 0, reason: "flight_duplicate", full_recalc: 1},
{"id": 10004, displayed: 0, reason: "probability_selection", full_recalc: 0},
{"id": 10004, displayed: 0, reason: "out_of_position", full_recalc: 0},
{"id": 10005, displayed: 1, reason: "n/a", full_recalc: 0}
@ -264,7 +264,7 @@ and losing focus. | :one:
| `message_id` | [required] A string identifier of the message in Activity Stream Router. | :one:
| `has_flow_params` | [required] One of [true, false]. A boolean identifier that indicates if Firefox Accounts flow parameters are set or unset. | :one:
| `displayed` | [required] 1: a SPOC is displayed; 0: non-displayed | :one:
| `reason` | [required] The reason if a SPOC is not displayed, "n/a" for the displayed, one of ("frequency_cap", "blocked_by_user", "campaign_duplicate", "probability_selection", "below_min_score", "out_of_position", "n/a") | :one:
| `reason` | [required] The reason if a SPOC is not displayed, "n/a" for the displayed, one of ("frequency_cap", "blocked_by_user", "flight_duplicate", "probability_selection", "below_min_score", "out_of_position", "n/a") | :one:
| `full_recalc` | [required] Is it a full SPOCS recalculation: 0: false; 1: true. Recalculation case: 1). fetch SPOCS from Pocket endpoint. Non-recalculation cases: 1). An impression updates the SPOCS; 2). Any action that triggers the `selectLayoutRender ` | :one:
**Where:**

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

@ -81,13 +81,13 @@ for more information about what methods are available.
Preferences specific to the Discovery Stream are nested under the sub-branch `browser.newtabpage.activity-stream.discoverystream` (with the exception of `browser.newtabpage.blocked`).
#### `browser.newtabpage.activity-stream.discoverystream.campaign.blocks`
#### `browser.newtabpage.activity-stream.discoverystream.flight.blocks`
- Type: `string (JSON)`
- Default: `{}`
- Pref Type: AS
Not intended for user configuration, but is programmatically updated. Used for tracking blocked campaign IDs when a user dismisses a SPOC. Keys are campaign IDs. Values don't have a specific meaning.
Not intended for user configuration, but is programmatically updated. Used for tracking blocked flight IDs when a user dismisses a SPOC. Keys are flight IDs. Values don't have a specific meaning.
#### `browser.newtabpage.blocked`
@ -108,7 +108,7 @@ Not intended for user configuration, but is programmatically updated. Used for t
"enabled": true,
"show_spocs": true,
"hardcoded_layout": true,
"personalized": false,
"personalized": true,
"layout_endpoint": "https://getpocket.cdn.mozilla.net/v3/newtab/layout?version=1&consumer_key=$apiKey&layout_variant=basic"
}
```
@ -119,6 +119,7 @@ Not intended for user configuration, but is programmatically updated. Used for t
- `hardcoded_layout` (boolean): When this is true, a hardcoded layout shipped with Firefox will be used instead of a remotely fetched layout definition.
- `personalized` (boolean): When this is `true` personalized content based on browsing history will be displayed.
- `layout_endpoint` (string): The URL for a remote layout definition that will be used if `hardcoded_layout` is `false`.
- `unused_key` (string): This is not set by default and is unused by this codebase. It's a standardized way to differentiate configurations to prevent experiment participants from being unenrolled.
#### `browser.newtabpage.activity-stream.discoverystream.enabled`

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

@ -313,13 +313,6 @@ const PREFS_CONFIG = new Map([
value: "https://incoming.telemetry.mozilla.org/submit",
},
],
[
"telemetry.ping.endpoint",
{
title: "Telemetry server endpoint",
value: "https://tiles.services.mozilla.com/v4/links/activity-stream",
},
],
[
"section.highlights.includeVisited",
{
@ -463,9 +456,9 @@ const PREFS_CONFIG = new Map([
],
// See browser/app/profile/firefox.js for other ASR preferences. They must be defined there to enable roll-outs.
[
"discoverystream.campaign.blocks",
"discoverystream.flight.blocks",
{
title: "Track campaign blocks",
title: "Track flight blocks",
skipBroadcast: true,
value: "{}",
},
@ -476,10 +469,11 @@ const PREFS_CONFIG = new Map([
title: "Configuration for the new pocket new tab",
getValue: ({ geo, locale }) => {
// PLEASE NOTE:
// hardcoded_layout in `lib/DiscoveryStreamFeed.jsm` only works for en-* and requires refactoring for non english locales
// hardcoded_layout in `lib/DiscoveryStreamFeed.jsm` only works for en-* and DE and requires refactoring for other locales
const dsEnablementMatrix = {
US: ["en-CA", "en-GB", "en-US"],
CA: ["en-CA", "en-GB", "en-US"],
DE: ["de", "de-DE", "de-AT", "de-CH"],
};
// Verify that the current geo & locale combination is enabled

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

@ -58,11 +58,12 @@ const PREF_IMPRESSION_ID = "browser.newtabpage.activity-stream.impressionId";
const PREF_ENABLED = "discoverystream.enabled";
const PREF_HARDCODED_BASIC_LAYOUT = "discoverystream.hardcoded-basic-layout";
const PREF_SPOCS_ENDPOINT = "discoverystream.spocs-endpoint";
const PREF_LANG_LAYOUT_CONFIG = "discoverystream.lang-layout-config";
const PREF_TOPSTORIES = "feeds.section.topstories";
const PREF_SPOCS_CLEAR_ENDPOINT = "discoverystream.endpointSpocsClear";
const PREF_SHOW_SPONSORED = "showSponsored";
const PREF_SPOC_IMPRESSIONS = "discoverystream.spoc.impressions";
const PREF_CAMPAIGN_BLOCKS = "discoverystream.campaign.blocks";
const PREF_FLIGHT_BLOCKS = "discoverystream.flight.blocks";
const PREF_REC_IMPRESSIONS = "discoverystream.rec.impressions";
let getHardcodedLayout;
@ -74,6 +75,7 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
// Persistent cache for remote endpoint data.
this.cache = new PersistentCache(CACHE_KEY, true);
this.locale = Services.locale.appLocaleAsLangTag;
this._impressionId = this.getOrCreateImpressionId();
// Internal in-memory cache for parsing json prefs.
this._prefCache = {};
@ -92,7 +94,7 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
* Send SPOCS Fill telemetry.
* @param {object} filteredItems An object keyed on filter reasons, and the value
* is a list of SPOCS.
* reasons: blocked_by_user, frequency_cap, below_min_score, campaign_duplicate
* reasons: blocked_by_user, frequency_cap, below_min_score, flight_duplicate
* @param {boolean} fullRecalc A boolean indicating if it's a full recalculation.
* Calling `loadSpocs` will be treated as a full recalculation.
* Whereas responding the action "DISCOVERY_STREAM_SPOC_IMPRESSION"
@ -103,8 +105,8 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
const spocsFill = [];
for (const [reason, items] of Object.entries(filteredItems)) {
items.forEach(item => {
// Only send SPOCS (i.e. it has a campaign_id)
if (item.campaign_id) {
// Only send SPOCS (i.e. it has a flight_id)
if (item.flight_id) {
spocsFill.push({ reason, full_recalc, id: item.id, displayed: 0 });
}
});
@ -207,7 +209,9 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
// The server somtimes returns this value already replaced, but we try this for two reasons:
// 1. Layout endpoints are not from the server.
// 2. Hardcoded layouts don't have this already done for us.
const endpoint = rawEndpoint.replace("$apiKey", apiKey);
const endpoint = rawEndpoint
.replace("$apiKey", apiKey)
.replace("$locale", this.locale);
try {
// Make sure the requested endpoint is allowed
@ -362,12 +366,19 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
}
if (!layoutResp || !layoutResp.layout) {
const langLayoutConfig =
this.store.getState().Prefs.values[PREF_LANG_LAYOUT_CONFIG] || "";
const isBasic =
this.config.hardcoded_basic_layout ||
this.store.getState().Prefs.values[PREF_HARDCODED_BASIC_LAYOUT] ||
!langLayoutConfig
.split(",")
.find(lang => this.locale.startsWith(lang.trim()));
// Set a hardcoded layout if one is needed.
// Changing values in this layout in memory object is unnecessary.
layoutResp = getHardcodedLayout(
this.config.hardcoded_basic_layout ||
this.store.getState().Prefs.values[PREF_HARDCODED_BASIC_LAYOUT]
);
layoutResp = getHardcodedLayout(isBasic);
}
sendUpdate({
@ -588,7 +599,7 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
},
};
this.cleanUpCampaignImpressionPref(spocsState.spocs);
this.cleanUpFlightImpressionPref(spocsState.spocs);
await this.cache.set("spocs", spocsState);
} else {
Cu.reportError("No response for spocs_endpoint prop");
@ -611,7 +622,7 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
let frequencyCapped = [];
let blockedItems = [];
let belowMinScore = [];
let campaignDupes = [];
let flightDupes = [];
this.placementsForEach(placement => {
const freshSpocs = spocsState.spocs[placement.name];
@ -619,8 +630,11 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
return;
}
// Migrate flight_id
const { data: migratedSpocs } = this.migrateFlightId(freshSpocs);
const { data: capResult, filtered: caps } = this.frequencyCapSpocs(
freshSpocs
migratedSpocs
);
frequencyCapped = [...frequencyCapped, ...caps];
@ -634,10 +648,10 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
);
let {
below_min_score: minScoreFilter,
campaign_duplicate: dupes,
flight_duplicate: dupes,
} = transformFilter;
belowMinScore = [...belowMinScore, ...minScoreFilter];
campaignDupes = [...campaignDupes, ...dupes];
flightDupes = [...flightDupes, ...dupes];
spocsState.spocs = {
...spocsState.spocs,
@ -659,7 +673,7 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
frequency_cap: frequencyCapped,
blocked_by_user: blockedItems,
below_min_score: belowMinScore,
campaign_duplicate: campaignDupes,
flight_duplicate: flightDupes,
},
true
);
@ -770,11 +784,11 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
filterBlocked(data) {
const filtered = [];
if (data && data.length) {
let campaigns = this.readDataPref(PREF_CAMPAIGN_BLOCKS);
let flights = this.readDataPref(PREF_FLIGHT_BLOCKS);
const filteredItems = data.filter(item => {
const blocked =
NewTabUtils.blockedLinks.isBlocked({ url: item.url }) ||
campaigns[item.campaign_id];
flights[item.flight_id];
if (blocked) {
filtered.push(item);
}
@ -792,33 +806,33 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
if (spocs && spocs.length) {
const spocsPerDomain =
this.store.getState().DiscoveryStream.spocs.spocs_per_domain || 1;
const campaignMap = {};
const campaignDuplicates = [];
const flightMap = {};
const flightDuplicates = [];
// This order of operations is intended.
// scoreItems must be first because it creates this.score.
const { data: items, filtered: belowMinScoreItems } = this.scoreItems(
spocs
);
// This removes campaign dupes.
// This removes flight dupes.
// We do this only after scoring and sorting because that way
// we can keep the first item we see, and end up keeping the highest scored.
const newSpocs = items.filter(s => {
if (!campaignMap[s.campaign_id]) {
campaignMap[s.campaign_id] = 1;
if (!flightMap[s.flight_id]) {
flightMap[s.flight_id] = 1;
return true;
} else if (campaignMap[s.campaign_id] < spocsPerDomain) {
campaignMap[s.campaign_id]++;
} else if (flightMap[s.flight_id] < spocsPerDomain) {
flightMap[s.flight_id]++;
return true;
}
campaignDuplicates.push(s);
flightDuplicates.push(s);
return false;
});
return {
data: newSpocs,
filtered: {
below_min_score: belowMinScoreItems,
campaign_duplicate: campaignDuplicates,
flight_duplicate: flightDuplicates,
},
};
}
@ -826,11 +840,42 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
data: spocs,
filtered: {
below_min_score: [],
campaign_duplicate: [],
flight_duplicate: [],
},
};
}
// For backwards compatibility, older spoc endpoint don't have flight_id,
// but instead had campaign_id we can use
//
// @param {Object} data An object that might have a SPOCS array.
// @returns {Object} An object with a property `data` as the result.
migrateFlightId(spocs) {
if (spocs && spocs.length) {
return {
data: spocs.map(s => {
return {
...s,
...(s.flight_id || s.campaign_id
? {
flight_id: s.flight_id || s.campaign_id,
}
: {}),
...(s.caps
? {
caps: {
...s.caps,
flight: s.caps.flight || s.caps.campaign,
},
}
: {}),
};
}),
};
}
return { data: spocs };
}
// Filter spocs based on frequency caps
//
// @param {Object} data An object that might have a SPOCS array.
@ -859,24 +904,24 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
return { data: spocs, filtered: [] };
}
// Frequency caps are based on campaigns, which may include multiple spocs.
// Frequency caps are based on flight, which may include multiple spocs.
// We currently support two types of frequency caps:
// - lifetime: Indicates how many times spocs from a campaign can be shown in total
// - period: Indicates how many times spocs from a campaign can be shown within a period
// - lifetime: Indicates how many times spocs from a flight can be shown in total
// - period: Indicates how many times spocs from a flight can be shown within a period
//
// So, for example, the feed configuration below defines that for campaign 1 no more
// So, for example, the feed configuration below defines that for flight 1 no more
// than 5 spocs can be shown in total, and no more than 2 per hour.
// "campaign_id": 1,
// "flight_id": 1,
// "caps": {
// "lifetime": 5,
// "campaign": {
// "flight": {
// "count": 2,
// "period": 3600
// }
// }
isBelowFrequencyCap(impressions, spoc) {
const campaignImpressions = impressions[spoc.campaign_id];
if (!campaignImpressions) {
const flightImpressions = impressions[spoc.flight_id];
if (!flightImpressions) {
return true;
}
@ -886,18 +931,17 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
lifetime || MAX_LIFETIME_CAP,
MAX_LIFETIME_CAP
);
const lifeTimeCapExceeded = campaignImpressions.length >= lifeTimeCap;
const lifeTimeCapExceeded = flightImpressions.length >= lifeTimeCap;
if (lifeTimeCapExceeded) {
return false;
}
const campaignCap = spoc.caps && spoc.caps.campaign;
if (campaignCap) {
const campaignCapExceeded =
campaignImpressions.filter(
i => Date.now() - i < campaignCap.period * 1000
).length >= campaignCap.count;
return !campaignCapExceeded;
const flightCap = spoc.caps && spoc.caps.flight;
if (flightCap) {
const flightCapExceeded =
flightImpressions.filter(i => Date.now() - i < flightCap.period * 1000)
.length >= flightCap.count;
return !flightCapExceeded;
}
return true;
}
@ -1140,7 +1184,7 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
resetDataPrefs() {
this.writeDataPref(PREF_SPOC_IMPRESSIONS, {});
this.writeDataPref(PREF_REC_IMPRESSIONS, {});
this.writeDataPref(PREF_CAMPAIGN_BLOCKS, {});
this.writeDataPref(PREF_FLIGHT_BLOCKS, {});
}
resetState() {
@ -1164,12 +1208,12 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
}
}
recordCampaignImpression(campaignId) {
recordFlightImpression(flightId) {
let impressions = this.readDataPref(PREF_SPOC_IMPRESSIONS);
const timeStamps = impressions[campaignId] || [];
const timeStamps = impressions[flightId] || [];
timeStamps.push(Date.now());
impressions = { ...impressions, [campaignId]: timeStamps };
impressions = { ...impressions, [flightId]: timeStamps };
this.writeDataPref(PREF_SPOC_IMPRESSIONS, impressions);
}
@ -1182,26 +1226,26 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
}
}
recordBlockCampaignId(campaignId) {
const campaigns = this.readDataPref(PREF_CAMPAIGN_BLOCKS);
if (!campaigns[campaignId]) {
campaigns[campaignId] = 1;
this.writeDataPref(PREF_CAMPAIGN_BLOCKS, campaigns);
recordBlockFlightId(flightId) {
const flights = this.readDataPref(PREF_FLIGHT_BLOCKS);
if (!flights[flightId]) {
flights[flightId] = 1;
this.writeDataPref(PREF_FLIGHT_BLOCKS, flights);
}
}
cleanUpCampaignImpressionPref(data) {
let campaignIds = [];
cleanUpFlightImpressionPref(data) {
let flightIds = [];
this.placementsForEach(placement => {
const newSpocs = data[placement.name];
if (!newSpocs) {
return;
}
campaignIds = [...campaignIds, ...newSpocs.map(s => `${s.campaign_id}`)];
flightIds = [...flightIds, ...newSpocs.map(s => `${s.flight_id}`)];
});
if (campaignIds && campaignIds.length) {
if (flightIds && flightIds.length) {
this.cleanUpImpressionPref(
id => !campaignIds.includes(id),
id => !flightIds.includes(id),
PREF_SPOC_IMPRESSIONS
);
}
@ -1303,7 +1347,7 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
break;
case at.DISCOVERY_STREAM_SPOC_IMPRESSION:
if (this.showSpocs) {
this.recordCampaignImpression(action.data.campaignId);
this.recordFlightImpression(action.data.flightId);
// Apply frequency capping to SPOCs in the redux store, only update the
// store if the SPOCs are changed.
@ -1340,7 +1384,7 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
}
}
break;
// This is fired from the browser, it has no concept of spocs, campaign or pocket.
// This is fired from the browser, it has no concept of spocs, flight or pocket.
// We match the blocked url with our available spoc urls to see if there is a match.
// I suspect we *could* instead do this in BLOCK_URL but I'm not sure.
case at.PLACES_LINK_BLOCKED:
@ -1387,12 +1431,12 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
this.uninitPrefs();
break;
case at.BLOCK_URL: {
// If we block a story that also has a campaign_id
// If we block a story that also has a flight_id
// we want to record that as blocked too.
// This is because a single campaign might have slightly different urls.
const { campaign_id } = action.data;
if (campaign_id) {
this.recordBlockCampaignId(campaign_id);
// This is because a single flight might have slightly different urls.
const { flight_id } = action.data;
if (flight_id) {
this.recordBlockFlightId(flight_id);
}
break;
}
@ -1402,6 +1446,7 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
case PREF_ENABLED:
case PREF_HARDCODED_BASIC_LAYOUT:
case PREF_SPOCS_ENDPOINT:
case PREF_LANG_LAYOUT_CONFIG:
// Clear the cached config and broadcast the newly computed value
this._prefCache.config = null;
this.store.dispatch(
@ -1452,16 +1497,24 @@ getHardcodedLayout = basic => {
{
type: "TopSites",
header: {
title: "Top Sites",
title: {
id: "newtab-section-header-topsites",
},
},
properties: {},
},
{
type: "Message",
header: {
title: "Recommended by Pocket",
title: {
id: "newtab-section-header-pocket",
values: { provider: "pocket" },
},
subtitle: "",
link_text: "Whats Pocket?",
link_text: {
id: "newtab-pocket-whats-pocket",
values: { provider: "pocket" },
},
link_url: "https://getpocket.com/firefox/new_tab_learn_more",
icon:
"resource://activity-stream/data/content/assets/glyph-pocket-16.svg",
@ -1482,7 +1535,7 @@ getHardcodedLayout = basic => {
feed: {
embed_reference: null,
url:
"https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?version=3&consumer_key=$apiKey&locale_lang=en-US&feed_variant=default_spocs_on",
"https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?version=3&consumer_key=$apiKey&locale_lang=$locale",
},
spocs: {
probability: 1,
@ -1548,7 +1601,9 @@ getHardcodedLayout = basic => {
{
type: "TopSites",
header: {
title: "Top Sites",
title: {
id: "newtab-section-header-topsites",
},
},
},
],
@ -1559,9 +1614,15 @@ getHardcodedLayout = basic => {
{
type: "Message",
header: {
title: "Recommended by Pocket",
title: {
id: "newtab-section-header-pocket",
values: { provider: "pocket" },
},
subtitle: "",
link_text: "Whats Pocket?",
link_text: {
id: "newtab-pocket-whats-pocket",
values: { provider: "pocket" },
},
link_url: "https://getpocket.com/firefox/new_tab_learn_more",
icon:
"resource://activity-stream/data/content/assets/glyph-pocket-16.svg",
@ -1587,7 +1648,7 @@ getHardcodedLayout = basic => {
feed: {
embed_reference: null,
url:
"https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?version=3&consumer_key=$apiKey&locale_lang=en-US&count=30",
"https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?version=3&consumer_key=$apiKey&locale_lang=$locale&count=30",
},
spocs: {
probability: 1,
@ -1642,7 +1703,9 @@ getHardcodedLayout = basic => {
],
},
header: {
title: "Popular Topics",
title: {
id: "newtab-pocket-read-more",
},
},
styles: {
".ds-navigation": "margin-top: -10px;",

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

@ -102,6 +102,10 @@ this.PrefsFeed = class PrefsFeed {
"browser.newtabpage.activity-stream.discoverystream.spocs-endpoint",
""
);
let discoveryStreamLangLayoutConfig = Services.prefs.getStringPref(
"browser.newtabpage.activity-stream.discoverystream.lang-layout-config",
""
);
values["discoverystream.enabled"] = discoveryStreamEnabled;
this._prefMap.set("discoverystream.enabled", {
value: discoveryStreamEnabled,
@ -116,6 +120,12 @@ this.PrefsFeed = class PrefsFeed {
this._prefMap.set("discoverystream.spocs-endpoint", {
value: discoveryStreamSpocsEndpoint,
});
values[
"discoverystream.lang-layout-config"
] = discoveryStreamLangLayoutConfig;
this._prefMap.set("discoverystream.lang-layout-config", {
value: discoveryStreamLangLayoutConfig,
});
// Set the initial state of all prefs in redux
this.store.dispatch(

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

@ -74,8 +74,6 @@ XPCOMUtils.defineLazyServiceGetters(this, {
});
const ACTIVITY_STREAM_ID = "activity-stream";
const ACTIVITY_STREAM_ENDPOINT_PREF =
"browser.newtabpage.activity-stream.telemetry.ping.endpoint";
const DOMWINDOW_OPENED_TOPIC = "domwindowopened";
const DOMWINDOW_UNLOAD_TOPIC = "unload";
const TAB_PINNED_EVENT = "TabPinned";
@ -257,10 +255,7 @@ this.TelemetryFeed = class TelemetryFeed {
*/
get pingCentre() {
Object.defineProperty(this, "pingCentre", {
value: new PingCentre({
topic: ACTIVITY_STREAM_ID,
overrideEndpointPref: ACTIVITY_STREAM_ENDPOINT_PREF,
}),
value: new PingCentre({ topic: ACTIVITY_STREAM_ID }),
});
return this.pingCentre;
}
@ -420,7 +415,6 @@ this.TelemetryFeed = class TelemetryFeed {
source,
tiles: impressionSets[source],
});
this.sendEvent(payload);
this.sendStructuredIngestionEvent(
payload,
STRUCTURED_INGESTION_NAMESPACE_AS,
@ -453,7 +447,6 @@ this.TelemetryFeed = class TelemetryFeed {
tiles,
loaded: tiles.length,
});
this.sendEvent(payload);
this.sendStructuredIngestionEvent(
payload,
STRUCTURED_INGESTION_NAMESPACE_AS,
@ -641,11 +634,27 @@ this.TelemetryFeed = class TelemetryFeed {
}
sendEvent(event_object) {
if (this.telemetryEnabled) {
this.pingCentre.sendPing(event_object, { filter: ACTIVITY_STREAM_ID });
switch (event_object.action) {
case "activity_stream_user_event":
this.sendEventPing(event_object);
break;
}
}
async sendEventPing(ping) {
delete ping.action;
ping.client_id = await this.telemetryClientId;
if (ping.value && typeof ping.value === "object") {
ping.value = JSON.stringify(ping.value);
}
this.sendStructuredIngestionEvent(
ping,
STRUCTURED_INGESTION_NAMESPACE_AS,
"events",
1
);
}
sendUTEvent(event_object, eventFunction) {
if (this.telemetryEnabled && this.eventTelemetryEnabled) {
eventFunction(event_object);
@ -686,7 +695,6 @@ this.TelemetryFeed = class TelemetryFeed {
au.getPortIdOfSender(action),
action.data
);
this.sendEvent(payload);
this.sendStructuredIngestionEvent(
payload,
STRUCTURED_INGESTION_NAMESPACE_AS,

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

@ -109,6 +109,9 @@ describe("<DiscoveryStreamBase>", () => {
"feeds.topsites": true,
},
}}
document={{
documentElement: { lang: "en-US" },
}}
Sections={[
{
id: "topstories",

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

@ -146,7 +146,7 @@ describe("<DSLinkMenu>", () => {
it("should pass through the correct menu options to LinkMenu for spocs", () => {
wrapper = shallow(
<DSLinkMenu {...ValidDSLinkMenuProps} campaignId="1234" />
<DSLinkMenu {...ValidDSLinkMenuProps} flightId="1234" />
);
wrapper
.find(ContextMenuButton)

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

@ -1,13 +1,14 @@
import { DSMessage } from "content-src/components/DiscoveryStreamComponents/DSMessage/DSMessage";
import React from "react";
import { SafeAnchor } from "content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor";
import { shallow } from "enzyme";
import { FluentOrText } from "content-src/components/FluentOrText/FluentOrText";
import { mount } from "enzyme";
describe("<DSMessage>", () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<DSMessage />);
wrapper = mount(<DSMessage />);
});
it("should render", () => {
@ -45,4 +46,30 @@ describe("<DSMessage>", () => {
SafeAnchor
);
});
it("should render a FluentOrText", () => {
wrapper.setProps({
link_text: "link_text",
title: "title",
link_url: "https://link_url.com",
});
assert.equal(
wrapper
.find(".title-text")
.children()
.at(0)
.type(),
FluentOrText
);
assert.equal(
wrapper
.find(".link a")
.children()
.at(0)
.type(),
FluentOrText
);
});
});

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

@ -132,12 +132,12 @@ describe("<ImpressionStats>", () => {
{ id: 3, pos: 2 },
]);
});
it("should send a DISCOVERY_STREAM_SPOC_IMPRESSION when the wrapped item has a campaignId", () => {
it("should send a DISCOVERY_STREAM_SPOC_IMPRESSION when the wrapped item has a flightId", () => {
const dispatch = sinon.spy();
const campaignId = "a_campaign_id";
const flightId = "a_flight_id";
const props = {
dispatch,
campaignId,
flightId,
IntersectionObserver: buildIntersectionObserver(FullIntersectEntries),
};
renderImpressionStats(props);
@ -147,7 +147,7 @@ describe("<ImpressionStats>", () => {
const [action] = dispatch.secondCall.args;
assert.equal(action.type, at.DISCOVERY_STREAM_SPOC_IMPRESSION);
assert.deepEqual(action.data, { campaignId });
assert.deepEqual(action.data, { flightId });
});
it("should send an impression when the wrapped item transiting from invisible to visible", () => {
const dispatch = sinon.spy();

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

@ -4,13 +4,14 @@ import {
} from "content-src/components/DiscoveryStreamComponents/Navigation/Navigation";
import React from "react";
import { SafeAnchor } from "content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor";
import { shallow } from "enzyme";
import { FluentOrText } from "content-src/components/FluentOrText/FluentOrText";
import { shallow, mount } from "enzyme";
describe("<Navigation>", () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<Navigation header={{}} />);
wrapper = mount(<Navigation header={{}} />);
});
it("should render", () => {
@ -23,6 +24,19 @@ describe("<Navigation>", () => {
assert.equal(wrapper.find(".ds-header").text(), "Foo");
});
it("should render a FluentOrText", () => {
wrapper.setProps({ header: { title: "Foo" } });
assert.equal(
wrapper
.find(".ds-navigation")
.children()
.at(0)
.type(),
FluentOrText
);
});
it("should render 2 Topics", () => {
wrapper.setProps({
links: [

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

@ -75,7 +75,7 @@ describe("Discovery Stream <TopSites>", () => {
url: "foo",
sponsor: "bar",
image_src: "foobar",
campaign_id: "1234",
flight_id: "1234",
id: "5678",
shim: { impression: "1011" },
};
@ -86,7 +86,7 @@ describe("Discovery Stream <TopSites>", () => {
label: "bar",
title: "bar",
url: "foo",
campaignId: "1234",
flightId: "1234",
id: "5678",
guid: "5678",
shim: {
@ -100,7 +100,7 @@ describe("Discovery Stream <TopSites>", () => {
label: "bar",
title: "bar",
url: "foo",
campaignId: "1234",
flightId: "1234",
id: "5678",
guid: "5678",
shim: {
@ -142,7 +142,7 @@ describe("Discovery Stream <TopSites>", () => {
url: "foo2",
sponsor: "bar2",
image_src: "foobar2",
campaign_id: "1234",
flight_id: "1234",
id: "5678",
shim: { impression: "1011" },
},
@ -161,7 +161,7 @@ describe("Discovery Stream <TopSites>", () => {
label: "bar2",
title: "bar2",
url: "foo2",
campaignId: "1234",
flightId: "1234",
id: "5678",
guid: "5678",
shim: {

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

@ -29,20 +29,18 @@ describe("selectLayoutRender", () => {
});
it("should return an empty array given initial state", () => {
const { layoutRender } = selectLayoutRender(
store.getState().DiscoveryStream,
{},
[]
);
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
prefs: {},
rollCache: [],
});
assert.deepEqual(layoutRender, []);
});
it("should return an empty SPOCS fill array given initial state", () => {
const { spocsFill } = selectLayoutRender(
store.getState().DiscoveryStream,
{},
[]
);
const { spocsFill } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
assert.deepEqual(spocsFill, []);
});
@ -57,11 +55,9 @@ describe("selectLayoutRender", () => {
});
store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });
const { layoutRender } = selectLayoutRender(
store.getState().DiscoveryStream,
{},
[]
);
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
assert.lengthOf(layoutRender, 1);
assert.propertyVal(layoutRender[0], "width", 3);
@ -80,11 +76,9 @@ describe("selectLayoutRender", () => {
});
store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });
const { layoutRender } = selectLayoutRender(
store.getState().DiscoveryStream,
{},
[]
);
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
assert.lengthOf(layoutRender, 1);
assert.propertyVal(layoutRender[0], "width", 3);
@ -107,11 +101,9 @@ describe("selectLayoutRender", () => {
});
store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });
const { layoutRender } = selectLayoutRender(
store.getState().DiscoveryStream,
{},
[]
);
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
assert.lengthOf(layoutRender, 1);
assert.propertyVal(layoutRender[0], "width", 3);
@ -137,11 +129,9 @@ describe("selectLayoutRender", () => {
data: { lastUpdated: 0, spocs: { spocs: [1, 2, 3] } },
});
const { layoutRender } = selectLayoutRender(
store.getState().DiscoveryStream,
{},
[]
);
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
assert.lengthOf(layoutRender, 1);
assert.propertyVal(layoutRender[0], "width", 3);
@ -167,11 +157,9 @@ describe("selectLayoutRender", () => {
});
store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });
const { layoutRender } = selectLayoutRender(
store.getState().DiscoveryStream,
{},
[]
);
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
assert.deepEqual(layoutRender[0].components[0].data, {
recommendations: [{ id: "bar" }],
@ -211,11 +199,9 @@ describe("selectLayoutRender", () => {
});
const randomStub = globals.sandbox.stub(global.Math, "random").returns(0.1);
const { spocsFill, layoutRender } = selectLayoutRender(
store.getState().DiscoveryStream,
{},
[]
);
const { spocsFill, layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
assert.calledTwice(randomStub);
assert.lengthOf(layoutRender, 1);
@ -273,11 +259,9 @@ describe("selectLayoutRender", () => {
});
const randomStub = globals.sandbox.stub(global.Math, "random").returns(0.1);
const { spocsFill, layoutRender } = selectLayoutRender(
store.getState().DiscoveryStream,
{},
[]
);
const { spocsFill, layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
assert.calledTwice(randomStub);
assert.lengthOf(layoutRender, 1);
@ -335,11 +319,10 @@ describe("selectLayoutRender", () => {
});
const randomStub = globals.sandbox.stub(global.Math, "random");
const { spocsFill, layoutRender } = selectLayoutRender(
store.getState().DiscoveryStream,
{},
[0.7, 0.3, 0.8]
);
const { spocsFill, layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
rollCache: [0.7, 0.3, 0.8],
});
assert.notCalled(randomStub);
assert.lengthOf(layoutRender, 1);
@ -404,11 +387,9 @@ describe("selectLayoutRender", () => {
});
const randomStub = globals.sandbox.stub(global.Math, "random").returns(0.6);
const { spocsFill, layoutRender } = selectLayoutRender(
store.getState().DiscoveryStream,
{},
[]
);
const { spocsFill, layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
assert.calledTwice(randomStub);
assert.lengthOf(layoutRender, 1);
@ -468,11 +449,10 @@ describe("selectLayoutRender", () => {
});
const randomStub = globals.sandbox.stub(global.Math, "random");
const { spocsFill, layoutRender } = selectLayoutRender(
store.getState().DiscoveryStream,
{},
[0.4, 0.3]
);
const { spocsFill, layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
rollCache: [0.4, 0.3],
});
assert.notCalled(randomStub);
assert.lengthOf(layoutRender, 1);
@ -530,11 +510,10 @@ describe("selectLayoutRender", () => {
});
const randomStub = globals.sandbox.stub(global.Math, "random");
const { spocsFill, layoutRender } = selectLayoutRender(
store.getState().DiscoveryStream,
{},
[0.6, 0.7]
);
const { spocsFill, layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
rollCache: [0.6, 0.7],
});
assert.notCalled(randomStub);
assert.lengthOf(layoutRender, 1);
@ -594,11 +573,10 @@ describe("selectLayoutRender", () => {
});
const randomStub = globals.sandbox.stub(global.Math, "random");
const { spocsFill, layoutRender } = selectLayoutRender(
store.getState().DiscoveryStream,
{},
[0.7, 0.2]
);
const { spocsFill, layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
rollCache: [0.7, 0.2],
});
assert.notCalled(randomStub);
assert.lengthOf(layoutRender, 1);
@ -652,11 +630,9 @@ describe("selectLayoutRender", () => {
});
store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });
const { spocsFill, layoutRender } = selectLayoutRender(
store.getState().DiscoveryStream,
{},
[]
);
const { spocsFill, layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
const { recommendations } = layoutRender[0].components[0].data;
assert.equal(recommendations.length, 4);
@ -689,11 +665,9 @@ describe("selectLayoutRender", () => {
data: { feed: { data: { recommendations: [] } }, url: "foo2.com" },
});
const { layoutRender } = selectLayoutRender(
store.getState().DiscoveryStream,
{},
[]
);
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
assert.equal(layoutRender[0].components[0].type, "foo1");
assert.equal(layoutRender[0].components[1].type, "foo2");
@ -733,11 +707,9 @@ describe("selectLayoutRender", () => {
data: { feed: { data: { recommendations: [] } }, url: "foo4.com" },
});
const { layoutRender } = selectLayoutRender(
store.getState().DiscoveryStream,
{},
[]
);
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
assert.equal(layoutRender[0].components[0].type, "foo1");
assert.equal(layoutRender[0].components[1].type, "foo2");
@ -780,11 +752,9 @@ describe("selectLayoutRender", () => {
data: { feed: { data: { recommendations: [] } }, url: "foo4.com" },
});
const { layoutRender } = selectLayoutRender(
store.getState().DiscoveryStream,
{},
[]
);
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
assert.equal(layoutRender[0].components[0].type, "foo1");
assert.equal(layoutRender[0].components[1].type, "foo2");
@ -837,11 +807,9 @@ describe("selectLayoutRender", () => {
data: fakeSpocsData,
});
const { layoutRender } = selectLayoutRender(
store.getState().DiscoveryStream,
{},
[]
);
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
assert.deepEqual(layoutRender[0].components[2].data.recommendations[0], {
name: "rec",
@ -864,11 +832,10 @@ describe("selectLayoutRender", () => {
data: { layout: fakeLayout },
});
const { layoutRender } = selectLayoutRender(
store.getState().DiscoveryStream,
{ "feeds.topsites": true },
[]
);
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
prefs: { "feeds.topsites": true },
});
assert.equal(layoutRender[0].components[0].type, "TopSites");
assert.equal(layoutRender[1], undefined);
@ -885,15 +852,42 @@ describe("selectLayoutRender", () => {
data: { layout: fakeLayout },
});
const { layoutRender } = selectLayoutRender(
store.getState().DiscoveryStream,
{ "feeds.topsites": true },
[]
);
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
prefs: { "feeds.topsites": true },
});
assert.equal(layoutRender[0].components[0].type, "TopSites");
assert.equal(layoutRender[0].components[1], undefined);
});
it("should not render a Navigation if not en-*", () => {
const fakeLayout = [
{
width: 3,
components: [
{ type: "Navigation" },
{ type: "Message" },
{ type: "TopSites" },
],
},
];
store.dispatch({
type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,
data: { layout: fakeLayout },
});
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
prefs: {
"feeds.topsites": true,
"feeds.section.topstories": true,
},
});
assert.equal(layoutRender[0].components[0].type, "Message");
assert.equal(layoutRender[0].components[1].type, "TopSites");
assert.equal(layoutRender[0].components[2], undefined);
});
it("should skip rendering a spoc in position if that spoc is blocked for that session", () => {
const fakeLayout = [
{
@ -930,22 +924,18 @@ describe("selectLayoutRender", () => {
data: fakeSpocsData,
});
const { layoutRender: layout1 } = selectLayoutRender(
store.getState().DiscoveryStream,
{},
[]
);
const { layoutRender: layout1 } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
store.dispatch({
type: at.DISCOVERY_STREAM_SPOC_BLOCKED,
data: { url: "https://foo.com" },
});
const { layoutRender: layout2 } = selectLayoutRender(
store.getState().DiscoveryStream,
{},
[]
);
const { layoutRender: layout2 } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
assert.deepEqual(layout1[0].components[0].data.recommendations[0], {
name: "spoc",

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

@ -68,6 +68,22 @@ describe("ActivityStream", () => {
const [, , action] = as.store.init.firstCall.args;
assert.equal(action.type, "UNINIT");
});
it("should clear old default discoverystream config pref", () => {
sandbox.stub(global.Services.prefs, "prefHasUserValue").returns(true);
sandbox
.stub(global.Services.prefs, "getStringPref")
.returns(
`{"api_key_pref":"extensions.pocket.oAuthConsumerKey","enabled":false,"show_spocs":true,"layout_endpoint":"https://getpocket.cdn.mozilla.net/v3/newtab/layout?version=1&consumer_key=$apiKey&layout_variant=basic"}`
);
sandbox.stub(global.Services.prefs, "clearUserPref");
as.init();
assert.calledWith(
global.Services.prefs.clearUserPref,
"browser.newtabpage.activity-stream.discoverystream.config"
);
});
});
describe("#uninit", () => {
beforeEach(() => {
@ -183,6 +199,70 @@ describe("ActivityStream", () => {
assert.calledWith(global.Services.prefs.clearUserPref, "oldPrefName");
});
});
describe("_updateDynamicPrefs Discovery Stream", () => {
it("should be true with expected en-US geo and locale", () => {
sandbox.stub(global.Services.prefs, "prefHasUserValue").returns(true);
sandbox.stub(global.Services.prefs, "getStringPref").returns("US");
sandbox
.stub(global.Services.locale, "appLocaleAsLangTag")
.get(() => "en-US");
as._updateDynamicPrefs();
assert.isTrue(
JSON.parse(PREFS_CONFIG.get("discoverystream.config").value).enabled
);
});
it("should be true with expected en-CA geo and locale", () => {
sandbox.stub(global.Services.prefs, "prefHasUserValue").returns(true);
sandbox.stub(global.Services.prefs, "getStringPref").returns("CA");
sandbox
.stub(global.Services.locale, "appLocaleAsLangTag")
.get(() => "en-CA");
as._updateDynamicPrefs();
assert.isTrue(
JSON.parse(PREFS_CONFIG.get("discoverystream.config").value).enabled
);
});
it("should be true with expected de geo and locale", () => {
sandbox.stub(global.Services.prefs, "prefHasUserValue").returns(true);
sandbox.stub(global.Services.prefs, "getStringPref").returns("DE");
sandbox
.stub(global.Services.locale, "appLocaleAsLangTag")
.get(() => "de-DE");
as._updateDynamicPrefs();
assert.isTrue(
JSON.parse(PREFS_CONFIG.get("discoverystream.config").value).enabled
);
});
it("should be false with no geo and locale", () => {
sandbox.stub(global.Services.prefs, "prefHasUserValue").returns(true);
sandbox.stub(global.Services.prefs, "getStringPref").returns("NOGEO");
as._updateDynamicPrefs();
assert.isFalse(
JSON.parse(PREFS_CONFIG.get("discoverystream.config").value).enabled
);
});
it("should be false with weird geo and locale combination", () => {
sandbox.stub(global.Services.prefs, "prefHasUserValue").returns(true);
sandbox.stub(global.Services.prefs, "getStringPref").returns("DE");
sandbox
.stub(global.Services.locale, "appLocaleAsLangTag")
.get(() => "en-US");
as._updateDynamicPrefs();
assert.isFalse(
JSON.parse(PREFS_CONFIG.get("discoverystream.config").value).enabled
);
});
});
describe("_updateDynamicPrefs topstories default value", () => {
it("should be false with no geo/locale", () => {
as._updateDynamicPrefs();

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

@ -186,6 +186,18 @@ describe("DiscoveryStreamFeed", () => {
{ credentials: "omit" }
);
});
it("should replace locales with $locale", async () => {
feed.locale = "replaced";
await feed.fetchFromEndpoint(
"https://getpocket.cdn.mozilla.net/dummy?locale_lang=$locale"
);
assert.calledWithMatch(
fetchStub,
"https://getpocket.cdn.mozilla.net/dummy?locale_lang=replaced",
{ credentials: "omit" }
);
});
it("should allow POST and with other options", async () => {
await feed.fetchFromEndpoint("https://getpocket.cdn.mozilla.net/dummy", {
method: "POST",
@ -315,6 +327,54 @@ describe("DiscoveryStreamFeed", () => {
const { layout } = feed.store.getState().DiscoveryStream;
assert.equal(layout[0].components[2].properties.items, 3);
});
it("should use 1 row layout if locale lang doesn't support 7 row layout", async () => {
feed.config.hardcoded_layout = true;
feed.store = createStore(combineReducers(reducers), {
Prefs: {
values: {
[CONFIG_PREF_NAME]: JSON.stringify({
enabled: true,
show_spocs: false,
layout_endpoint: DUMMY_ENDPOINT,
}),
[ENDPOINTS_PREF_NAME]: DUMMY_ENDPOINT,
"discoverystream.enabled": true,
"discoverystream.lang-layout-config": "en",
},
},
});
feed.locale = "de-DE";
sandbox.stub(feed, "fetchLayout").returns(Promise.resolve(""));
await feed.loadLayout(feed.store.dispatch);
const { layout } = feed.store.getState().DiscoveryStream;
assert.equal(layout[0].components[2].properties.items, 3);
});
it("should use 7 row layout if locale lang supports it", async () => {
feed.config.hardcoded_layout = true;
feed.store = createStore(combineReducers(reducers), {
Prefs: {
values: {
[CONFIG_PREF_NAME]: JSON.stringify({
enabled: true,
show_spocs: false,
layout_endpoint: DUMMY_ENDPOINT,
}),
[ENDPOINTS_PREF_NAME]: DUMMY_ENDPOINT,
"discoverystream.enabled": true,
"discoverystream.lang-layout-config": "en,de",
},
},
});
feed.locale = "de-DE";
sandbox.stub(feed, "fetchLayout").returns(Promise.resolve(""));
await feed.loadLayout(feed.store.dispatch);
const { layout } = feed.store.getState().DiscoveryStream;
assert.equal(layout[2].components[0].properties.items, 21);
});
it("should use new spocs endpoint if in the config", async () => {
feed.config.spocs_endpoint = "https://spocs.getpocket.com/spocs2";
@ -936,61 +996,61 @@ describe("DiscoveryStreamFeed", () => {
});
it("should sort based on item_score", () => {
const { data: result } = feed.transform([
{ id: 2, campaign_id: 2, item_score: 0.8, min_score: 0.1 },
{ id: 3, campaign_id: 3, item_score: 0.7, min_score: 0.1 },
{ id: 1, campaign_id: 1, item_score: 0.9, min_score: 0.1 },
{ id: 2, flight_id: 2, item_score: 0.8, min_score: 0.1 },
{ id: 3, flight_id: 3, item_score: 0.7, min_score: 0.1 },
{ id: 1, flight_id: 1, item_score: 0.9, min_score: 0.1 },
]);
assert.deepEqual(result, [
{ id: 1, campaign_id: 1, item_score: 0.9, score: 0.9, min_score: 0.1 },
{ id: 2, campaign_id: 2, item_score: 0.8, score: 0.8, min_score: 0.1 },
{ id: 3, campaign_id: 3, item_score: 0.7, score: 0.7, min_score: 0.1 },
{ id: 1, flight_id: 1, item_score: 0.9, score: 0.9, min_score: 0.1 },
{ id: 2, flight_id: 2, item_score: 0.8, score: 0.8, min_score: 0.1 },
{ id: 3, flight_id: 3, item_score: 0.7, score: 0.7, min_score: 0.1 },
]);
});
it("should remove items with scores lower than min_score", () => {
const { data: result, filtered } = feed.transform([
{ id: 2, campaign_id: 2, item_score: 0.8, min_score: 0.9 },
{ id: 3, campaign_id: 3, item_score: 0.7, min_score: 0.7 },
{ id: 1, campaign_id: 1, item_score: 0.9, min_score: 0.8 },
{ id: 2, flight_id: 2, item_score: 0.8, min_score: 0.9 },
{ id: 3, flight_id: 3, item_score: 0.7, min_score: 0.7 },
{ id: 1, flight_id: 1, item_score: 0.9, min_score: 0.8 },
]);
assert.deepEqual(result, [
{ id: 1, campaign_id: 1, item_score: 0.9, score: 0.9, min_score: 0.8 },
{ id: 3, campaign_id: 3, item_score: 0.7, score: 0.7, min_score: 0.7 },
{ id: 1, flight_id: 1, item_score: 0.9, score: 0.9, min_score: 0.8 },
{ id: 3, flight_id: 3, item_score: 0.7, score: 0.7, min_score: 0.7 },
]);
assert.deepEqual(filtered.below_min_score, [
{ id: 2, campaign_id: 2, item_score: 0.8, min_score: 0.9, score: 0.8 },
{ id: 2, flight_id: 2, item_score: 0.8, min_score: 0.9, score: 0.8 },
]);
});
it("should add a score prop to spocs", () => {
const { data: result } = feed.transform([
{ campaign_id: 1, item_score: 0.9, min_score: 0.1 },
{ flight_id: 1, item_score: 0.9, min_score: 0.1 },
]);
assert.equal(result[0].score, 0.9);
});
it("should filter out duplicate campigns", () => {
it("should filter out duplicate flights", () => {
const { data: result, filtered } = feed.transform([
{ id: 1, campaign_id: 2, item_score: 0.8, min_score: 0.1 },
{ id: 2, campaign_id: 3, item_score: 0.6, min_score: 0.1 },
{ id: 3, campaign_id: 1, item_score: 0.9, min_score: 0.1 },
{ id: 4, campaign_id: 3, item_score: 0.7, min_score: 0.1 },
{ id: 5, campaign_id: 1, item_score: 0.9, min_score: 0.1 },
{ id: 1, flight_id: 2, item_score: 0.8, min_score: 0.1 },
{ id: 2, flight_id: 3, item_score: 0.6, min_score: 0.1 },
{ id: 3, flight_id: 1, item_score: 0.9, min_score: 0.1 },
{ id: 4, flight_id: 3, item_score: 0.7, min_score: 0.1 },
{ id: 5, flight_id: 1, item_score: 0.9, min_score: 0.1 },
]);
assert.deepEqual(result, [
{ id: 3, campaign_id: 1, item_score: 0.9, score: 0.9, min_score: 0.1 },
{ id: 1, campaign_id: 2, item_score: 0.8, score: 0.8, min_score: 0.1 },
{ id: 4, campaign_id: 3, item_score: 0.7, score: 0.7, min_score: 0.1 },
{ id: 3, flight_id: 1, item_score: 0.9, score: 0.9, min_score: 0.1 },
{ id: 1, flight_id: 2, item_score: 0.8, score: 0.8, min_score: 0.1 },
{ id: 4, flight_id: 3, item_score: 0.7, score: 0.7, min_score: 0.1 },
]);
assert.deepEqual(filtered.campaign_duplicate, [
{ id: 5, campaign_id: 1, item_score: 0.9, min_score: 0.1, score: 0.9 },
{ id: 2, campaign_id: 3, item_score: 0.6, min_score: 0.1, score: 0.6 },
assert.deepEqual(filtered.flight_duplicate, [
{ id: 5, flight_id: 1, item_score: 0.9, min_score: 0.1, score: 0.9 },
{ id: 2, flight_id: 3, item_score: 0.6, min_score: 0.1, score: 0.6 },
]);
});
it("should filter out duplicate campigns while using spocs_per_domain", () => {
it("should filter out duplicate flight while using spocs_per_domain", () => {
sandbox.stub(feed.store, "getState").returns({
DiscoveryStream: {
spocs: { spocs_per_domain: 2 },
@ -998,32 +1058,32 @@ describe("DiscoveryStreamFeed", () => {
});
const { data: result, filtered } = feed.transform([
{ id: 1, campaign_id: 2, item_score: 0.8, min_score: 0.1 },
{ id: 2, campaign_id: 3, item_score: 0.6, min_score: 0.1 },
{ id: 3, campaign_id: 1, item_score: 0.6, min_score: 0.1 },
{ id: 4, campaign_id: 3, item_score: 0.7, min_score: 0.1 },
{ id: 5, campaign_id: 1, item_score: 0.9, min_score: 0.1 },
{ id: 6, campaign_id: 2, item_score: 0.6, min_score: 0.1 },
{ id: 7, campaign_id: 3, item_score: 0.7, min_score: 0.1 },
{ id: 8, campaign_id: 1, item_score: 0.8, min_score: 0.1 },
{ id: 9, campaign_id: 3, item_score: 0.7, min_score: 0.1 },
{ id: 10, campaign_id: 1, item_score: 0.8, min_score: 0.1 },
{ id: 1, flight_id: 2, item_score: 0.8, min_score: 0.1 },
{ id: 2, flight_id: 3, item_score: 0.6, min_score: 0.1 },
{ id: 3, flight_id: 1, item_score: 0.6, min_score: 0.1 },
{ id: 4, flight_id: 3, item_score: 0.7, min_score: 0.1 },
{ id: 5, flight_id: 1, item_score: 0.9, min_score: 0.1 },
{ id: 6, flight_id: 2, item_score: 0.6, min_score: 0.1 },
{ id: 7, flight_id: 3, item_score: 0.7, min_score: 0.1 },
{ id: 8, flight_id: 1, item_score: 0.8, min_score: 0.1 },
{ id: 9, flight_id: 3, item_score: 0.7, min_score: 0.1 },
{ id: 10, flight_id: 1, item_score: 0.8, min_score: 0.1 },
]);
assert.deepEqual(result, [
{ id: 5, campaign_id: 1, item_score: 0.9, score: 0.9, min_score: 0.1 },
{ id: 1, campaign_id: 2, item_score: 0.8, score: 0.8, min_score: 0.1 },
{ id: 8, campaign_id: 1, item_score: 0.8, score: 0.8, min_score: 0.1 },
{ id: 4, campaign_id: 3, item_score: 0.7, score: 0.7, min_score: 0.1 },
{ id: 7, campaign_id: 3, item_score: 0.7, score: 0.7, min_score: 0.1 },
{ id: 6, campaign_id: 2, item_score: 0.6, score: 0.6, min_score: 0.1 },
{ id: 5, flight_id: 1, item_score: 0.9, score: 0.9, min_score: 0.1 },
{ id: 1, flight_id: 2, item_score: 0.8, score: 0.8, min_score: 0.1 },
{ id: 8, flight_id: 1, item_score: 0.8, score: 0.8, min_score: 0.1 },
{ id: 4, flight_id: 3, item_score: 0.7, score: 0.7, min_score: 0.1 },
{ id: 7, flight_id: 3, item_score: 0.7, score: 0.7, min_score: 0.1 },
{ id: 6, flight_id: 2, item_score: 0.6, score: 0.6, min_score: 0.1 },
]);
assert.deepEqual(filtered.campaign_duplicate, [
{ id: 10, campaign_id: 1, item_score: 0.8, min_score: 0.1, score: 0.8 },
{ id: 9, campaign_id: 3, item_score: 0.7, min_score: 0.1, score: 0.7 },
{ id: 2, campaign_id: 3, item_score: 0.6, min_score: 0.1, score: 0.6 },
{ id: 3, campaign_id: 1, item_score: 0.6, min_score: 0.1, score: 0.6 },
assert.deepEqual(filtered.flight_duplicate, [
{ id: 10, flight_id: 1, item_score: 0.8, min_score: 0.1, score: 0.8 },
{ id: 9, flight_id: 3, item_score: 0.7, min_score: 0.1, score: 0.7 },
{ id: 2, flight_id: 3, item_score: 0.6, min_score: 0.1, score: 0.6 },
{ id: 3, flight_id: 1, item_score: 0.6, min_score: 0.1, score: 0.6 },
]);
});
});
@ -1090,10 +1150,10 @@ describe("DiscoveryStreamFeed", () => {
const fakeSpocs = [
{
id: 1,
campaign_id: "seen",
flight_id: "seen",
caps: {
lifetime: 3,
campaign: {
flight: {
count: 1,
period: 1,
},
@ -1101,10 +1161,10 @@ describe("DiscoveryStreamFeed", () => {
},
{
id: 2,
campaign_id: "not-seen",
flight_id: "not-seen",
caps: {
lifetime: 3,
campaign: {
flight: {
count: 1,
period: 1,
},
@ -1119,7 +1179,7 @@ describe("DiscoveryStreamFeed", () => {
const { data: result, filtered } = feed.frequencyCapSpocs(fakeSpocs);
assert.equal(result.length, 1);
assert.equal(result[0].campaign_id, "not-seen");
assert.equal(result[0].flight_id, "not-seen");
assert.deepEqual(filtered, [fakeSpocs[0]]);
});
it("should return simple structure and do nothing with no spocs", () => {
@ -1130,16 +1190,63 @@ describe("DiscoveryStreamFeed", () => {
});
});
describe("#migrateFlightId", () => {
it("should migrate campaign to flight if no flight exists", () => {
const fakeSpocs = [
{
id: 1,
campaign_id: "campaign",
caps: {
lifetime: 3,
campaign: {
count: 1,
period: 1,
},
},
},
];
const { data: result } = feed.migrateFlightId(fakeSpocs);
assert.deepEqual(result[0], {
id: 1,
flight_id: "campaign",
campaign_id: "campaign",
caps: {
lifetime: 3,
flight: {
count: 1,
period: 1,
},
campaign: {
count: 1,
period: 1,
},
},
});
});
it("should not migrate campaign to flight if caps or id don't exist", () => {
const fakeSpocs = [{ id: 1 }];
const { data: result } = feed.migrateFlightId(fakeSpocs);
assert.deepEqual(result[0], { id: 1 });
});
it("should return simple structure and do nothing with no spocs", () => {
const { data: result } = feed.migrateFlightId([]);
assert.equal(result.length, 0);
});
});
describe("#isBelowFrequencyCap", () => {
it("should return true if there are no campaign impressions", () => {
it("should return true if there are no flight impressions", () => {
const fakeImpressions = {
seen: [Date.now() - 1],
};
const fakeSpoc = {
campaign_id: "not-seen",
flight_id: "not-seen",
caps: {
lifetime: 3,
campaign: {
flight: {
count: 1,
period: 1,
},
@ -1150,12 +1257,12 @@ describe("DiscoveryStreamFeed", () => {
assert.isTrue(result);
});
it("should return true if there are no campaign caps", () => {
it("should return true if there are no flight caps", () => {
const fakeImpressions = {
seen: [Date.now() - 1],
};
const fakeSpoc = {
campaign_id: "seen",
flight_id: "seen",
caps: {
lifetime: 3,
},
@ -1171,10 +1278,10 @@ describe("DiscoveryStreamFeed", () => {
seen: [Date.now() - 1],
};
const fakeSpoc = {
campaign_id: "seen",
flight_id: "seen",
caps: {
lifetime: 1,
campaign: {
flight: {
count: 3,
period: 1,
},
@ -1191,10 +1298,10 @@ describe("DiscoveryStreamFeed", () => {
seen: [Date.now() - 1],
};
const fakeSpoc = {
campaign_id: "seen",
flight_id: "seen",
caps: {
lifetime: 3,
campaign: {
flight: {
count: 1,
period: 1,
},
@ -1229,12 +1336,12 @@ describe("DiscoveryStreamFeed", () => {
});
});
describe("#recordCampaignImpression", () => {
describe("#recordFlightImpression", () => {
it("should return false if time based cap is hit", () => {
sandbox.stub(feed, "readDataPref").returns({});
sandbox.stub(feed, "writeDataPref").returns();
feed.recordCampaignImpression("seen");
feed.recordFlightImpression("seen");
assert.calledWith(feed.writeDataPref, SPOC_IMPRESSION_TRACKING_PREF, {
seen: [0],
@ -1242,40 +1349,40 @@ describe("DiscoveryStreamFeed", () => {
});
});
describe("#recordBlockCampaignId", () => {
it("should call writeDataPref with new campaign id added", () => {
describe("#recordBlockFlightId", () => {
it("should call writeDataPref with new flight id added", () => {
sandbox.stub(feed, "readDataPref").returns({ "1234": 1 });
sandbox.stub(feed, "writeDataPref").returns();
feed.recordBlockCampaignId("5678");
feed.recordBlockFlightId("5678");
assert.calledOnce(feed.readDataPref);
assert.calledWith(feed.writeDataPref, "discoverystream.campaign.blocks", {
assert.calledWith(feed.writeDataPref, "discoverystream.flight.blocks", {
"1234": 1,
"5678": 1,
});
});
});
describe("#cleanUpCampaignImpressionPref", () => {
it("should remove campaign-3 because it is no longer being used", async () => {
describe("#cleanUpFlightImpressionPref", () => {
it("should remove flight-3 because it is no longer being used", async () => {
const fakeSpocs = {
spocs: [
{
campaign_id: "campaign-1",
flight_id: "flight-1",
caps: {
lifetime: 3,
campaign: {
flight: {
count: 1,
period: 1,
},
},
},
{
campaign_id: "campaign-2",
flight_id: "flight-2",
caps: {
lifetime: 3,
campaign: {
flight: {
count: 1,
period: 1,
},
@ -1284,16 +1391,16 @@ describe("DiscoveryStreamFeed", () => {
],
};
const fakeImpressions = {
"campaign-2": [Date.now() - 1],
"campaign-3": [Date.now() - 1],
"flight-2": [Date.now() - 1],
"flight-3": [Date.now() - 1],
};
sandbox.stub(feed, "readDataPref").returns(fakeImpressions);
sandbox.stub(feed, "writeDataPref").returns();
feed.cleanUpCampaignImpressionPref(fakeSpocs);
feed.cleanUpFlightImpressionPref(fakeSpocs);
assert.calledWith(feed.writeDataPref, SPOC_IMPRESSION_TRACKING_PREF, {
"campaign-2": [-1],
"flight-2": [-1],
});
});
});
@ -1416,10 +1523,10 @@ describe("DiscoveryStreamFeed", () => {
spocs: [
{
id: 1,
campaign_id: "seen",
flight_id: "seen",
caps: {
lifetime: 3,
campaign: {
flight: {
count: 1,
period: 1,
},
@ -1427,10 +1534,10 @@ describe("DiscoveryStreamFeed", () => {
},
{
id: 2,
campaign_id: "not-seen",
flight_id: "not-seen",
caps: {
lifetime: 3,
campaign: {
flight: {
count: 1,
period: 1,
},
@ -1457,10 +1564,10 @@ describe("DiscoveryStreamFeed", () => {
spocs: [
{
id: 2,
campaign_id: "not-seen",
flight_id: "not-seen",
caps: {
lifetime: 3,
campaign: {
flight: {
count: 1,
period: 1,
},
@ -1477,13 +1584,13 @@ describe("DiscoveryStreamFeed", () => {
},
];
sandbox.stub(feed, "recordCampaignImpression").returns();
sandbox.stub(feed, "recordFlightImpression").returns();
sandbox.stub(feed, "readDataPref").returns(fakeImpressions);
sandbox.spy(feed.store, "dispatch");
await feed.onAction({
type: at.DISCOVERY_STREAM_SPOC_IMPRESSION,
data: { campaign_id: "seen" },
data: { flight_id: "seen" },
});
assert.deepEqual(
@ -1498,13 +1605,13 @@ describe("DiscoveryStreamFeed", () => {
it("should not call dispatch to ac.AlsoToPreloaded if spocs were not changed by frequency capping", async () => {
Object.defineProperty(feed, "showSpocs", { get: () => true });
const fakeImpressions = {};
sandbox.stub(feed, "recordCampaignImpression").returns();
sandbox.stub(feed, "recordFlightImpression").returns();
sandbox.stub(feed, "readDataPref").returns(fakeImpressions);
sandbox.spy(feed.store, "dispatch");
await feed.onAction({
type: at.DISCOVERY_STREAM_SPOC_IMPRESSION,
data: { campaign_id: "seen" },
data: { flight_id: "seen" },
});
assert.notCalled(feed.store.dispatch);
@ -1513,7 +1620,7 @@ describe("DiscoveryStreamFeed", () => {
sandbox.restore();
Object.defineProperty(feed, "showSpocs", { get: () => true });
const fakeImpressions = {};
sandbox.stub(feed, "recordCampaignImpression").returns();
sandbox.stub(feed, "recordFlightImpression").returns();
sandbox.stub(feed, "readDataPref").returns(fakeImpressions);
sandbox.spy(feed.store, "dispatch");
sandbox.spy(feed, "frequencyCapSpocs");
@ -1522,10 +1629,10 @@ describe("DiscoveryStreamFeed", () => {
spocs: [
{
id: 2,
campaign_id: "seen-2",
flight_id: "seen-2",
caps: {
lifetime: 3,
campaign: {
flight: {
count: 1,
period: 1,
},
@ -1545,7 +1652,7 @@ describe("DiscoveryStreamFeed", () => {
await feed.onAction({
type: at.DISCOVERY_STREAM_SPOC_IMPRESSION,
data: { campaign_id: "doesn't matter" },
data: { flight_id: "doesn't matter" },
});
assert.calledOnce(feed.frequencyCapSpocs);
@ -1559,12 +1666,12 @@ describe("DiscoveryStreamFeed", () => {
spocs: [
{
id: 1,
campaign_id: "foo",
flight_id: "foo",
url: "foo.com",
},
{
id: 2,
campaign_id: "bar",
flight_id: "bar",
url: "bar.com",
},
],
@ -1638,17 +1745,17 @@ describe("DiscoveryStreamFeed", () => {
});
describe("#onAction: BLOCK_URL", () => {
it("should call recordBlockCampaignId whith BLOCK_URL", async () => {
sandbox.stub(feed, "recordBlockCampaignId").returns();
it("should call recordBlockFlightId whith BLOCK_URL", async () => {
sandbox.stub(feed, "recordBlockFlightId").returns();
await feed.onAction({
type: at.BLOCK_URL,
data: {
campaign_id: "1234",
flight_id: "1234",
},
});
assert.calledWith(feed.recordBlockCampaignId, "1234");
assert.calledWith(feed.recordBlockFlightId, "1234");
});
});
@ -2354,19 +2461,16 @@ describe("DiscoveryStreamFeed", () => {
{ id: 2, reason: "frequency_cap", displayed: 0, full_recalc: 1 },
{ id: 3, reason: "blocked_by_user", displayed: 0, full_recalc: 1 },
{ id: 4, reason: "blocked_by_user", displayed: 0, full_recalc: 1 },
{ id: 5, reason: "campaign_duplicate", displayed: 0, full_recalc: 1 },
{ id: 6, reason: "campaign_duplicate", displayed: 0, full_recalc: 1 },
{ id: 5, reason: "flight_duplicate", displayed: 0, full_recalc: 1 },
{ id: 6, reason: "flight_duplicate", displayed: 0, full_recalc: 1 },
{ id: 7, reason: "below_min_score", displayed: 0, full_recalc: 1 },
{ id: 8, reason: "below_min_score", displayed: 0, full_recalc: 1 },
];
const filtered = {
frequency_cap: [{ id: 1, campaign_id: 1 }, { id: 2, campaign_id: 2 }],
blocked_by_user: [{ id: 3, campaign_id: 3 }, { id: 4, campaign_id: 4 }],
campaign_duplicate: [
{ id: 5, campaign_id: 5 },
{ id: 6, campaign_id: 6 },
],
below_min_score: [{ id: 7, campaign_id: 7 }, { id: 8, campaign_id: 8 }],
frequency_cap: [{ id: 1, flight_id: 1 }, { id: 2, flight_id: 2 }],
blocked_by_user: [{ id: 3, flight_id: 3 }, { id: 4, flight_id: 4 }],
flight_duplicate: [{ id: 5, flight_id: 5 }, { id: 6, flight_id: 6 }],
below_min_score: [{ id: 7, flight_id: 7 }, { id: 8, flight_id: 8 }],
};
feed._sendSpocsFill(filtered, true);
@ -2382,7 +2486,7 @@ describe("DiscoveryStreamFeed", () => {
{ id: 2, reason: "frequency_cap", displayed: 0, full_recalc: 0 },
];
const filtered = {
frequency_cap: [{ id: 1, campaign_id: 1 }, { id: 2, campaign_id: 2 }],
frequency_cap: [{ id: 1, flight_id: 1 }, { id: 2, flight_id: 2 }],
};
feed._sendSpocsFill(filtered, false);
@ -2396,14 +2500,14 @@ describe("DiscoveryStreamFeed", () => {
const expected = [
{ id: 1, reason: "frequency_cap", displayed: 0, full_recalc: 1 },
{ id: 3, reason: "blocked_by_user", displayed: 0, full_recalc: 1 },
{ id: 5, reason: "campaign_duplicate", displayed: 0, full_recalc: 1 },
{ id: 5, reason: "flight_duplicate", displayed: 0, full_recalc: 1 },
{ id: 7, reason: "below_min_score", displayed: 0, full_recalc: 1 },
];
const filtered = {
frequency_cap: [{ id: 1, campaign_id: 1 }, { id: 2 }],
blocked_by_user: [{ id: 3, campaign_id: 3 }, { id: 4 }],
campaign_duplicate: [{ id: 5, campaign_id: 5 }, { id: 6 }],
below_min_score: [{ id: 7, campaign_id: 7 }, { id: 8 }],
frequency_cap: [{ id: 1, flight_id: 1 }, { id: 2 }],
blocked_by_user: [{ id: 3, flight_id: 3 }, { id: 4 }],
flight_duplicate: [{ id: 5, flight_id: 5 }, { id: 6 }],
below_min_score: [{ id: 7, flight_id: 7 }, { id: 8 }],
};
feed._sendSpocsFill(filtered, true);

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

@ -974,16 +974,53 @@ describe("TelemetryFeed", () => {
assert.propertyVal(ping, "event_context", "foo");
});
});
describe("#sendEvent", () => {
it("should call PingCentre", async () => {
FakePrefs.prototype.prefs.telemetry = true;
const event = {};
describe("#sendEventPing", () => {
it("should call sendStructuredIngestionEvent", async () => {
const data = {
action: "activity_stream_user_event",
event: "CLICK",
};
instance = new TelemetryFeed();
sandbox.stub(instance.pingCentre, "sendPing");
sandbox.spy(instance, "sendStructuredIngestionEvent");
await instance.sendEvent(event);
await instance.sendEventPing(data);
assert.calledWith(instance.pingCentre.sendPing, event);
const expectedPayload = {
client_id: FAKE_TELEMETRY_ID,
event: "CLICK",
};
assert.calledWith(instance.sendStructuredIngestionEvent, expectedPayload);
});
it("should stringify value if it is an Object", async () => {
const data = {
action: "activity_stream_user_event",
event: "CLICK",
value: { foo: "bar" },
};
instance = new TelemetryFeed();
sandbox.spy(instance, "sendStructuredIngestionEvent");
await instance.sendEventPing(data);
const expectedPayload = {
client_id: FAKE_TELEMETRY_ID,
event: "CLICK",
value: JSON.stringify({ foo: "bar" }),
};
assert.calledWith(instance.sendStructuredIngestionEvent, expectedPayload);
});
});
describe("#sendEvent", () => {
it("should call sendEventPing on activity_stream_user_event", () => {
FakePrefs.prototype.prefs.telemetry = true;
FakePrefs.prototype.prefs[STRUCTURED_INGESTION_TELEMETRY_PREF] = true;
const event = { action: "activity_stream_user_event" };
instance = new TelemetryFeed();
sandbox.spy(instance, "sendEventPing");
instance.sendEvent(event);
assert.calledOnce(instance.sendEventPing);
});
});
describe("#sendUTEvent", () => {
@ -1255,7 +1292,7 @@ describe("TelemetryFeed", () => {
assert.calledWith(sendEvent, eventCreator.returnValue);
});
it("should send an event on a TELEMETRY_IMPRESSION_STATS action", () => {
const sendEvent = sandbox.stub(instance, "sendEvent");
const sendEvent = sandbox.stub(instance, "sendStructuredIngestionEvent");
const eventCreator = sandbox.stub(instance, "createImpressionStats");
const tiles = [{ id: 10001 }, { id: 10002 }, { id: 10003 }];
const action = ac.ImpressionStats({ source: "POCKET", tiles });
@ -1438,7 +1475,7 @@ describe("TelemetryFeed", () => {
assert.notCalled(spy);
});
it("should send impression pings if there is impression data", () => {
const spy = sandbox.spy(instance, "sendEvent");
const spy = sandbox.spy(instance, "sendStructuredIngestionEvent");
const session = {
impressionSets: {
source_foo: [{ id: 1, pos: 0 }, { id: 2, pos: 1 }],
@ -1459,7 +1496,7 @@ describe("TelemetryFeed", () => {
assert.notCalled(spy);
});
it("should send loaded content pings if there is loaded content data", () => {
const spy = sandbox.spy(instance, "sendEvent");
const spy = sandbox.spy(instance, "sendStructuredIngestionEvent");
const session = {
loadedContentSets: {
source_foo: [{ id: 1, pos: 0 }, { id: 2, pos: 1 }],

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

@ -3,10 +3,6 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
ChromeUtils.defineModuleGetter(
this,
@ -18,11 +14,6 @@ ChromeUtils.defineModuleGetter(
"UpdateUtils",
"resource://gre/modules/UpdateUtils.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"ClientID",
"resource://gre/modules/ClientID.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"TelemetryEnvironment",
@ -38,7 +29,6 @@ const PREF_BRANCH = "browser.ping-centre.";
const TELEMETRY_PREF = `${PREF_BRANCH}telemetry`;
const LOGGING_PREF = `${PREF_BRANCH}log`;
const PRODUCTION_ENDPOINT_PREF = `${PREF_BRANCH}production.endpoint`;
const STRUCTURED_INGESTION_SEND_TIMEOUT = 30 * 1000; // 30 seconds
const FHR_UPLOAD_ENABLED_PREF = "datareporting.healthreport.uploadEnabled";
@ -190,7 +180,6 @@ const REGION_WHITELIST = new Set([
* @param {Object} options
* @param {string} options.topic - a unique ID for users of PingCentre to distinguish
* their data on the server side.
* @param {string} options.overrideEndpointPref - optional pref for URL where the POST is sent.
*/
class PingCentre {
constructor(options) {
@ -201,8 +190,6 @@ class PingCentre {
this._topic = options.topic;
this._prefs = Services.prefs.getBranch("");
this._setPingEndpoint(options.topic, options.overrideEndpointPref);
this._enabled = this._prefs.getBoolPref(TELEMETRY_PREF);
this._onTelemetryPrefChange = this._onTelemetryPrefChange.bind(this);
this._prefs.addObserver(TELEMETRY_PREF, this._onTelemetryPrefChange);
@ -216,30 +203,10 @@ class PingCentre {
this._prefs.addObserver(LOGGING_PREF, this._onLoggingPrefChange);
}
/**
* Lazily get the Telemetry id promise
*/
get telemetryClientId() {
Object.defineProperty(this, "telemetryClientId", {
value: ClientID.getClientID(),
});
return this.telemetryClientId;
}
get enabled() {
return this._enabled && this._fhrEnabled;
}
_setPingEndpoint(topic, overrideEndpointPref) {
const overrideValue =
overrideEndpointPref && this._prefs.getStringPref(overrideEndpointPref);
if (overrideValue) {
this._pingEndpoint = overrideValue;
} else {
this._pingEndpoint = this._prefs.getStringPref(PRODUCTION_ENDPOINT_PREF);
}
}
_onLoggingPrefChange(aSubject, aTopic, prefKey) {
this.logging = this._prefs.getBoolPref(prefKey);
}
@ -284,37 +251,6 @@ class PingCentre {
return region;
}
async _createPing(data, options) {
let filter = options && options.filter;
let experiments = TelemetryEnvironment.getActiveExperiments();
let experimentsString = this._createExperimentsString(experiments, filter);
let clientID = data.client_id || (await this.telemetryClientId);
let locale = data.locale || Services.locale.appLocaleAsLangTag;
let profileCreationDate =
TelemetryEnvironment.currentEnvironment.profile.resetDate ||
TelemetryEnvironment.currentEnvironment.profile.creationDate;
const payload = Object.assign(
{
locale,
topic: this._topic,
client_id: clientID,
version: AppConstants.MOZ_APP_VERSION,
release_channel: UpdateUtils.getUpdateChannel(false),
},
data
);
if (experimentsString) {
payload.shield_id = experimentsString;
}
if (profileCreationDate) {
payload.profile_creation_date = profileCreationDate;
}
payload.region = this._getRegion();
return payload;
}
_createStructuredIngestionPing(data, options = {}) {
let { filter } = options;
let experiments = TelemetryEnvironment.getActiveExperiments();
@ -336,39 +272,6 @@ class PingCentre {
return payload;
}
async sendPing(data, options) {
if (!this.enabled) {
return Promise.resolve();
}
const payload = await this._createPing(data, options);
if (this.logging) {
// performance related pings cause a lot of logging, so we mute them
if (data.action !== "activity_stream_performance") {
Services.console.logStringMessage(
`TELEMETRY PING: ${JSON.stringify(payload)}\n`
);
}
}
return fetch(this._pingEndpoint, {
method: "POST",
body: JSON.stringify(payload),
credentials: "omit",
})
.then(response => {
if (!response.ok) {
Cu.reportError(
`Ping failure with HTTP response code: ${response.status}`
);
}
})
.catch(e => {
Cu.reportError(`Ping failure with error: ${e}`);
});
}
static _gzipCompressString(string) {
let observer = {
buffer: "",
@ -483,7 +386,6 @@ class PingCentre {
this.PingCentre = PingCentre;
this.PingCentreConstants = {
PRODUCTION_ENDPOINT_PREF,
FHR_UPLOAD_ENABLED_PREF,
TELEMETRY_PREF,
LOGGING_PREF,