зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1691226 - Remove personalization code from the old activity stream topstories. r=gvn
Differential Revision: https://phabricator.services.mozilla.com/D104408
This commit is contained in:
Родитель
5fff3c0a33
Коммит
9da4bf1ea8
|
@ -156,67 +156,7 @@ const PREFS_CONFIG = new Map([
|
|||
}`,
|
||||
stories_referrer: "https://getpocket.com/recommendations",
|
||||
topics_endpoint: `https://getpocket.cdn.mozilla.net/v3/firefox/trending-topics?version=2&consumer_key=$apiKey&locale_lang=${args.locale}`,
|
||||
model_keys: [
|
||||
"nmf_model_animals",
|
||||
"nmf_model_business",
|
||||
"nmf_model_career",
|
||||
"nmf_model_datascience",
|
||||
"nmf_model_design",
|
||||
"nmf_model_education",
|
||||
"nmf_model_entertainment",
|
||||
"nmf_model_environment",
|
||||
"nmf_model_fashion",
|
||||
"nmf_model_finance",
|
||||
"nmf_model_food",
|
||||
"nmf_model_health",
|
||||
"nmf_model_home",
|
||||
"nmf_model_life",
|
||||
"nmf_model_marketing",
|
||||
"nmf_model_politics",
|
||||
"nmf_model_programming",
|
||||
"nmf_model_science",
|
||||
"nmf_model_shopping",
|
||||
"nmf_model_sports",
|
||||
"nmf_model_tech",
|
||||
"nmf_model_travel",
|
||||
"nb_model_animals",
|
||||
"nb_model_books",
|
||||
"nb_model_business",
|
||||
"nb_model_career",
|
||||
"nb_model_datascience",
|
||||
"nb_model_design",
|
||||
"nb_model_economics",
|
||||
"nb_model_education",
|
||||
"nb_model_entertainment",
|
||||
"nb_model_environment",
|
||||
"nb_model_fashion",
|
||||
"nb_model_finance",
|
||||
"nb_model_food",
|
||||
"nb_model_game",
|
||||
"nb_model_health",
|
||||
"nb_model_history",
|
||||
"nb_model_home",
|
||||
"nb_model_life",
|
||||
"nb_model_marketing",
|
||||
"nb_model_military",
|
||||
"nb_model_philosophy",
|
||||
"nb_model_photography",
|
||||
"nb_model_politics",
|
||||
"nb_model_productivity",
|
||||
"nb_model_programming",
|
||||
"nb_model_psychology",
|
||||
"nb_model_science",
|
||||
"nb_model_shopping",
|
||||
"nb_model_society",
|
||||
"nb_model_space",
|
||||
"nb_model_sports",
|
||||
"nb_model_tech",
|
||||
"nb_model_travel",
|
||||
"nb_model_writing",
|
||||
],
|
||||
show_spocs: showSpocs(args),
|
||||
personalized: true,
|
||||
version: 1,
|
||||
}),
|
||||
},
|
||||
],
|
||||
|
|
|
@ -24,12 +24,6 @@ const { shortURL } = ChromeUtils.import(
|
|||
const { SectionsManager } = ChromeUtils.import(
|
||||
"resource://activity-stream/lib/SectionsManager.jsm"
|
||||
);
|
||||
const { UserDomainAffinityProvider } = ChromeUtils.import(
|
||||
"resource://activity-stream/lib/UserDomainAffinityProvider.jsm"
|
||||
);
|
||||
const { PersonalityProvider } = ChromeUtils.import(
|
||||
"resource://activity-stream/lib/PersonalityProvider/PersonalityProvider.jsm"
|
||||
);
|
||||
const { PersistentCache } = ChromeUtils.import(
|
||||
"resource://activity-stream/lib/PersistentCache.jsm"
|
||||
);
|
||||
|
@ -43,7 +37,6 @@ ChromeUtils.defineModuleGetter(
|
|||
const STORIES_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes
|
||||
const TOPICS_UPDATE_TIME = 3 * 60 * 60 * 1000; // 3 hours
|
||||
const STORIES_NOW_THRESHOLD = 24 * 60 * 60 * 1000; // 24 hours
|
||||
const MIN_DOMAIN_AFFINITIES_UPDATE_TIME = 12 * 60 * 60 * 1000; // 12 hours
|
||||
const DEFAULT_RECS_EXPIRE_TIME = 60 * 60 * 1000; // 1 hour
|
||||
const SECTION_ID = "topstories";
|
||||
const IMPRESSION_SOURCE = "TOP_STORIES";
|
||||
|
@ -53,7 +46,6 @@ const DISCOVERY_STREAM_PREF_ENABLED = "discoverystream.enabled";
|
|||
const DISCOVERY_STREAM_PREF_ENABLED_PATH =
|
||||
"browser.newtabpage.activity-stream.discoverystream.enabled";
|
||||
const REC_IMPRESSION_TRACKING_PREF = "feeds.section.topstories.rec.impressions";
|
||||
const OPTIONS_PREF = "feeds.section.topstories.options";
|
||||
const PREF_USER_TOPSTORIES = "feeds.section.topstories";
|
||||
const MAX_LIFETIME_CAP = 500; // Guard against misconfiguration on the server
|
||||
const DISCOVERY_STREAM_PREF = "discoverystream.config";
|
||||
|
@ -100,16 +92,11 @@ this.TopStoriesFeed = class TopStoriesFeed {
|
|||
);
|
||||
this.read_more_endpoint = options.read_more_endpoint;
|
||||
this.stories_referrer = options.stories_referrer;
|
||||
this.personalized = options.personalized;
|
||||
this.show_spocs = options.show_spocs;
|
||||
this.maxHistoryQueryResults = options.maxHistoryQueryResults;
|
||||
this.storiesLastUpdated = 0;
|
||||
this.topicsLastUpdated = 0;
|
||||
this.storiesLoaded = false;
|
||||
this.domainAffinitiesLastUpdated = 0;
|
||||
this.processAffinityProividerVersion(options);
|
||||
this.dispatchPocketCta(this._prefs.get("pocketCta"), false);
|
||||
Services.obs.addObserver(this, "idle-daily");
|
||||
|
||||
// Cache is used for new page loads, which shouldn't have changed data.
|
||||
// If we have changed data, cache should be cleared,
|
||||
|
@ -137,14 +124,6 @@ this.TopStoriesFeed = class TopStoriesFeed {
|
|||
SectionsManager.onceInitialized(this.onInit.bind(this));
|
||||
}
|
||||
|
||||
observe(subject, topic, data) {
|
||||
switch (topic) {
|
||||
case "idle-daily":
|
||||
this.updateDomainAffinityScores();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async clearCache() {
|
||||
await this.cache.set("stories", {});
|
||||
await this.cache.set("topics", {});
|
||||
|
@ -153,12 +132,6 @@ this.TopStoriesFeed = class TopStoriesFeed {
|
|||
|
||||
uninit() {
|
||||
this.storiesLoaded = false;
|
||||
try {
|
||||
Services.obs.removeObserver(this, "idle-daily");
|
||||
} catch (e) {
|
||||
// Attempt to remove unassociated observer which is possible when discovery stream
|
||||
// is enabled and user never used activity stream experience
|
||||
}
|
||||
SectionsManager.disableSection(SECTION_ID);
|
||||
}
|
||||
|
||||
|
@ -214,42 +187,6 @@ this.TopStoriesFeed = class TopStoriesFeed {
|
|||
this.dispatchUpdateEvent(shouldBroadcast, updateProps);
|
||||
}
|
||||
|
||||
async onPersonalityProviderInit() {
|
||||
const data = await this.cache.get();
|
||||
let stories = data.stories && data.stories.recommendations;
|
||||
this.stories = this.rotate(this.transform(stories));
|
||||
this.doContentUpdate({ stories: this.stories }, false);
|
||||
|
||||
const affinities = this.affinityProvider.getAffinities();
|
||||
this.domainAffinitiesLastUpdated = Date.now();
|
||||
affinities._timestamp = this.domainAffinitiesLastUpdated;
|
||||
this.cache.set("domainAffinities", affinities);
|
||||
}
|
||||
|
||||
affinityProividerSwitcher(...args) {
|
||||
const { affinityProviderV2 } = this;
|
||||
if (affinityProviderV2 && affinityProviderV2.use_v2) {
|
||||
const provider = this.PersonalityProvider(...args, {
|
||||
modelKeys: affinityProviderV2.model_keys,
|
||||
dispatch: this.store.dispatch,
|
||||
});
|
||||
provider.init(this.onPersonalityProviderInit.bind(this));
|
||||
return provider;
|
||||
}
|
||||
|
||||
const v1Provider = this.UserDomainAffinityProvider(...args);
|
||||
|
||||
return v1Provider;
|
||||
}
|
||||
|
||||
PersonalityProvider(...args) {
|
||||
return new PersonalityProvider(...args);
|
||||
}
|
||||
|
||||
UserDomainAffinityProvider(...args) {
|
||||
return new UserDomainAffinityProvider(...args);
|
||||
}
|
||||
|
||||
async fetchStories() {
|
||||
if (!this.stories_endpoint) {
|
||||
return null;
|
||||
|
@ -292,17 +229,6 @@ this.TopStoriesFeed = class TopStoriesFeed {
|
|||
let stories = data.stories && data.stories.recommendations;
|
||||
let topics = data.topics && data.topics.topics;
|
||||
|
||||
let affinities = data.domainAffinities;
|
||||
if (this.personalized && affinities && affinities.scores) {
|
||||
this.affinityProvider = this.affinityProividerSwitcher(
|
||||
affinities.timeSegments,
|
||||
affinities.parameterSets,
|
||||
affinities.maxHistoryQueryResults,
|
||||
affinities.version,
|
||||
affinities.scores
|
||||
);
|
||||
this.domainAffinitiesLastUpdated = affinities._timestamp;
|
||||
}
|
||||
if (stories && !!stories.length && this.storiesLastUpdated === 0) {
|
||||
this.updateSettings(data.stories.settings);
|
||||
this.stories = this.rotate(this.transform(stories));
|
||||
|
@ -348,10 +274,7 @@ this.TopStoriesFeed = class TopStoriesFeed {
|
|||
referrer: this.stories_referrer,
|
||||
url: s.url,
|
||||
min_score: s.min_score || 0,
|
||||
score:
|
||||
this.personalized && this.affinityProvider
|
||||
? this.affinityProvider.calculateItemRelevanceScore(s)
|
||||
: s.item_score || 1,
|
||||
score: s.item_score || 1,
|
||||
spoc_meta: this.show_spocs
|
||||
? { campaign_id: s.campaign_id, caps: s.caps }
|
||||
: {},
|
||||
|
@ -364,7 +287,7 @@ this.TopStoriesFeed = class TopStoriesFeed {
|
|||
|
||||
return mapped;
|
||||
})
|
||||
.sort(this.personalized ? this.compareScore : (a, b) => 0);
|
||||
.sort(this.compareScore);
|
||||
|
||||
return calcResult;
|
||||
}
|
||||
|
@ -404,59 +327,16 @@ this.TopStoriesFeed = class TopStoriesFeed {
|
|||
return b.score - a.score;
|
||||
}
|
||||
|
||||
updateSettings(settings) {
|
||||
if (!this.personalized) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.spocsPerNewTabs = settings.spocsPerNewTabs; // Probability of a new tab getting a spoc [0,1]
|
||||
this.timeSegments = settings.timeSegments;
|
||||
this.domainAffinityParameterSets = settings.domainAffinityParameterSets;
|
||||
updateSettings(settings = {}) {
|
||||
this.spocsPerNewTabs = settings.spocsPerNewTabs || 1; // Probability of a new tab getting a spoc [0,1]
|
||||
this.recsExpireTime = settings.recsExpireTime;
|
||||
this.version = settings.version;
|
||||
|
||||
if (
|
||||
this.affinityProvider &&
|
||||
this.affinityProvider.version !== this.version
|
||||
) {
|
||||
this.resetDomainAffinityScores();
|
||||
}
|
||||
}
|
||||
|
||||
updateDomainAffinityScores() {
|
||||
if (
|
||||
!this.personalized ||
|
||||
!this.domainAffinityParameterSets ||
|
||||
Date.now() - this.domainAffinitiesLastUpdated <
|
||||
MIN_DOMAIN_AFFINITIES_UPDATE_TIME
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.affinityProvider = this.affinityProividerSwitcher(
|
||||
this.timeSegments,
|
||||
this.domainAffinityParameterSets,
|
||||
this.maxHistoryQueryResults,
|
||||
this.version,
|
||||
undefined
|
||||
);
|
||||
|
||||
const affinities = this.affinityProvider.getAffinities();
|
||||
this.domainAffinitiesLastUpdated = Date.now();
|
||||
affinities._timestamp = this.domainAffinitiesLastUpdated;
|
||||
this.cache.set("domainAffinities", affinities);
|
||||
}
|
||||
|
||||
resetDomainAffinityScores() {
|
||||
delete this.affinityProvider;
|
||||
this.cache.set("domainAffinities", {});
|
||||
}
|
||||
|
||||
// If personalization is turned on, we have to rotate stories on the client so that
|
||||
// We rotate stories on the client so that
|
||||
// active stories are at the front of the list, followed by stories that have expired
|
||||
// impressions i.e. have been displayed for longer than recsExpireTime.
|
||||
rotate(items) {
|
||||
if (!this.personalized || items.length <= 3) {
|
||||
if (items.length <= 3) {
|
||||
return items;
|
||||
}
|
||||
|
||||
|
@ -717,30 +597,6 @@ this.TopStoriesFeed = class TopStoriesFeed {
|
|||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decides if we need to change the personality provider version or not.
|
||||
* Changes the version if it determines we need to.
|
||||
*
|
||||
* @param data {object} The top stories pref, we need version and model_keys
|
||||
* @return {boolean} Returns true only if the version was changed.
|
||||
*/
|
||||
processAffinityProividerVersion(data) {
|
||||
const version2 = data.version === 2 && !this.affinityProviderV2;
|
||||
const version1 = data.version === 1 && this.affinityProviderV2;
|
||||
if (version2 || version1) {
|
||||
if (version1) {
|
||||
this.affinityProviderV2 = null;
|
||||
} else {
|
||||
this.affinityProviderV2 = {
|
||||
use_v2: true,
|
||||
model_keys: data.model_keys,
|
||||
};
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
lazyLoadTopStories(options = {}) {
|
||||
let { dsPref, userPref } = options;
|
||||
if (!dsPref) {
|
||||
|
@ -837,11 +693,6 @@ this.TopStoriesFeed = class TopStoriesFeed {
|
|||
this.spocs = this.spocs.filter(s => s.url !== action.data.url);
|
||||
}
|
||||
break;
|
||||
case at.PLACES_HISTORY_CLEARED:
|
||||
if (this.personalized) {
|
||||
this.resetDomainAffinityScores();
|
||||
}
|
||||
break;
|
||||
case at.TELEMETRY_IMPRESSION_STATS: {
|
||||
// We want to make sure we only track impressions from Top Stories,
|
||||
// otherwise unexpected things that are not properly handled can happen.
|
||||
|
@ -862,12 +713,10 @@ this.TopStoriesFeed = class TopStoriesFeed {
|
|||
}
|
||||
});
|
||||
}
|
||||
if (this.personalized) {
|
||||
const topRecs = payload.tiles
|
||||
.filter(t => !this.spocCampaignMap.has(t.id))
|
||||
.map(t => t.id);
|
||||
this.recordTopRecImpressions(topRecs);
|
||||
}
|
||||
const topRecs = payload.tiles
|
||||
.filter(t => !this.spocCampaignMap.has(t.id))
|
||||
.map(t => t.id);
|
||||
this.recordTopRecImpressions(topRecs);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -891,20 +740,6 @@ this.TopStoriesFeed = class TopStoriesFeed {
|
|||
if (action.data.name === "pocketCta") {
|
||||
this.dispatchPocketCta(action.data.value, true);
|
||||
}
|
||||
if (action.data.name === OPTIONS_PREF) {
|
||||
try {
|
||||
const options = JSON.parse(action.data.value);
|
||||
if (this.processAffinityProividerVersion(options)) {
|
||||
await this.clearCache();
|
||||
this.uninit();
|
||||
this.init();
|
||||
}
|
||||
} catch (e) {
|
||||
Cu.reportError(
|
||||
`Problem initializing affinity provider v2: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -915,7 +750,6 @@ this.TOPICS_UPDATE_TIME = TOPICS_UPDATE_TIME;
|
|||
this.SECTION_ID = SECTION_ID;
|
||||
this.SPOC_IMPRESSION_TRACKING_PREF = SPOC_IMPRESSION_TRACKING_PREF;
|
||||
this.REC_IMPRESSION_TRACKING_PREF = REC_IMPRESSION_TRACKING_PREF;
|
||||
this.MIN_DOMAIN_AFFINITIES_UPDATE_TIME = MIN_DOMAIN_AFFINITIES_UPDATE_TIME;
|
||||
this.DEFAULT_RECS_EXPIRE_TIME = DEFAULT_RECS_EXPIRE_TIME;
|
||||
const EXPORTED_SYMBOLS = [
|
||||
"TopStoriesFeed",
|
||||
|
@ -923,7 +757,6 @@ const EXPORTED_SYMBOLS = [
|
|||
"TOPICS_UPDATE_TIME",
|
||||
"SECTION_ID",
|
||||
"SPOC_IMPRESSION_TRACKING_PREF",
|
||||
"MIN_DOMAIN_AFFINITIES_UPDATE_TIME",
|
||||
"REC_IMPRESSION_TRACKING_PREF",
|
||||
"DEFAULT_RECS_EXPIRE_TIME",
|
||||
];
|
||||
|
|
|
@ -9,7 +9,6 @@ describe("Top Stories Feed", () => {
|
|||
let SECTION_ID;
|
||||
let SPOC_IMPRESSION_TRACKING_PREF;
|
||||
let REC_IMPRESSION_TRACKING_PREF;
|
||||
let MIN_DOMAIN_AFFINITIES_UPDATE_TIME;
|
||||
let DEFAULT_RECS_EXPIRE_TIME;
|
||||
let instance;
|
||||
let clock;
|
||||
|
@ -50,27 +49,6 @@ describe("Top Stories Feed", () => {
|
|||
sections: new Map([["topstories", { options: FAKE_OPTIONS }]]),
|
||||
};
|
||||
|
||||
class FakeUserDomainAffinityProvider {
|
||||
constructor(
|
||||
timeSegments,
|
||||
parameterSets,
|
||||
maxHistoryQueryResults,
|
||||
version,
|
||||
scores
|
||||
) {
|
||||
this.timeSegments = timeSegments;
|
||||
this.parameterSets = parameterSets;
|
||||
this.maxHistoryQueryResults = maxHistoryQueryResults;
|
||||
this.version = version;
|
||||
this.scores = scores;
|
||||
}
|
||||
|
||||
getAffinities() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
class FakePersonalityProvider extends FakeUserDomainAffinityProvider {}
|
||||
|
||||
({
|
||||
TopStoriesFeed,
|
||||
STORIES_UPDATE_TIME,
|
||||
|
@ -78,17 +56,10 @@ describe("Top Stories Feed", () => {
|
|||
SECTION_ID,
|
||||
SPOC_IMPRESSION_TRACKING_PREF,
|
||||
REC_IMPRESSION_TRACKING_PREF,
|
||||
MIN_DOMAIN_AFFINITIES_UPDATE_TIME,
|
||||
DEFAULT_RECS_EXPIRE_TIME,
|
||||
} = injector({
|
||||
"lib/ActivityStreamPrefs.jsm": { Prefs: FakePrefs },
|
||||
"lib/ShortURL.jsm": { shortURL: shortURLStub },
|
||||
"lib/PersonalityProvider.jsm": {
|
||||
PersonalityProvider: FakePersonalityProvider,
|
||||
},
|
||||
"lib/UserDomainAffinityProvider.jsm": {
|
||||
UserDomainAffinityProvider: FakeUserDomainAffinityProvider,
|
||||
},
|
||||
"lib/SectionsManager.jsm": { SectionsManager: sectionsManagerStub },
|
||||
}));
|
||||
|
||||
|
@ -451,12 +422,11 @@ describe("Top Stories Feed", () => {
|
|||
it("should set spocs cache on fetch", async () => {
|
||||
const response = {
|
||||
recommendations: [{ id: "1" }, { id: "2" }],
|
||||
settings: { timeSegments: {}, domainAffinityParameterSets: {} },
|
||||
settings: {},
|
||||
spocs: [{ id: "spoc1" }],
|
||||
};
|
||||
|
||||
instance.show_spocs = true;
|
||||
instance.personalized = true;
|
||||
instance.stories_endpoint = "stories-endpoint";
|
||||
|
||||
let fetchStub = globals.sandbox.stub();
|
||||
|
@ -784,13 +754,12 @@ describe("Top Stories Feed", () => {
|
|||
});
|
||||
});
|
||||
describe("#personalization", () => {
|
||||
it("should sort stories if personalization is preffed on", async () => {
|
||||
it("should sort stories", async () => {
|
||||
const response = {
|
||||
recommendations: [{ id: "1" }, { id: "2" }],
|
||||
settings: { timeSegments: {}, domainAffinityParameterSets: {} },
|
||||
settings: {},
|
||||
};
|
||||
|
||||
instance.personalized = true;
|
||||
instance.compareScore = sinon.spy();
|
||||
instance.stories_endpoint = "stories-endpoint";
|
||||
|
||||
|
@ -808,36 +777,12 @@ describe("Top Stories Feed", () => {
|
|||
await instance.fetchStories();
|
||||
assert.calledOnce(instance.compareScore);
|
||||
});
|
||||
it("should not sort stories if personalization is preffed off", async () => {
|
||||
const response = `{
|
||||
"recommendations": [{"id" : "1"}, {"id" : "2"}],
|
||||
"settings": {"timeSegments": {}, "domainAffinityParameterSets": {}}
|
||||
}`;
|
||||
|
||||
instance.personalized = false;
|
||||
instance.compareScore = sinon.spy();
|
||||
instance.stories_endpoint = "stories-endpoint";
|
||||
|
||||
let fetchStub = globals.sandbox.stub();
|
||||
globals.set("fetch", fetchStub);
|
||||
globals.set("NewTabUtils", {
|
||||
blockedLinks: { isBlocked: globals.sandbox.spy() },
|
||||
});
|
||||
fetchStub.resolves({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () => Promise.resolve(response),
|
||||
});
|
||||
|
||||
await instance.fetchStories();
|
||||
assert.notCalled(instance.compareScore);
|
||||
});
|
||||
it("should sort items based on relevance score", () => {
|
||||
let items = [{ score: 0.1 }, { score: 0.2 }];
|
||||
items = items.sort(instance.compareScore);
|
||||
assert.deepEqual(items, [{ score: 0.2 }, { score: 0.1 }]);
|
||||
});
|
||||
it("should rotate items if personalization is preffed on", () => {
|
||||
it("should rotate items", () => {
|
||||
let items = [
|
||||
{ guid: "g1" },
|
||||
{ guid: "g2" },
|
||||
|
@ -846,7 +791,6 @@ describe("Top Stories Feed", () => {
|
|||
{ guid: "g5" },
|
||||
{ guid: "g6" },
|
||||
];
|
||||
instance.personalized = true;
|
||||
|
||||
// No impressions should leave items unchanged
|
||||
let rotated = instance.rotate(items);
|
||||
|
@ -896,26 +840,8 @@ describe("Top Stories Feed", () => {
|
|||
rotated
|
||||
);
|
||||
});
|
||||
it("should not rotate items if personalization is preffed off", () => {
|
||||
let items = [
|
||||
{ guid: "g1" },
|
||||
{ guid: "g2" },
|
||||
{ guid: "g3" },
|
||||
{ guid: "g4" },
|
||||
];
|
||||
|
||||
instance.personalized = false;
|
||||
|
||||
instance._prefs.get = pref =>
|
||||
pref === REC_IMPRESSION_TRACKING_PREF &&
|
||||
JSON.stringify({ g1: 1, g2: 1, g3: 1 });
|
||||
clock.tick(DEFAULT_RECS_EXPIRE_TIME + 1);
|
||||
let rotated = instance.rotate(items);
|
||||
assert.deepEqual(items, rotated);
|
||||
});
|
||||
it("should record top story impressions", async () => {
|
||||
instance._prefs = { get: pref => undefined, set: sinon.spy() };
|
||||
instance.personalized = true;
|
||||
|
||||
clock.tick(1);
|
||||
let expectedPrefValue = JSON.stringify({ 1: 1, 2: 1, 3: 1 });
|
||||
|
@ -969,7 +895,6 @@ describe("Top Stories Feed", () => {
|
|||
});
|
||||
it("should not record top story impressions for non-view impressions", async () => {
|
||||
instance._prefs = { get: pref => undefined, set: sinon.spy() };
|
||||
instance.personalized = true;
|
||||
|
||||
instance.onAction({
|
||||
type: at.TELEMETRY_IMPRESSION_STATS,
|
||||
|
@ -1017,30 +942,6 @@ describe("Top Stories Feed", () => {
|
|||
JSON.stringify({ 3: 1 })
|
||||
);
|
||||
});
|
||||
it("should re init on affinityProviderV2 pref change", async () => {
|
||||
sinon.stub(instance, "uninit");
|
||||
sinon.stub(instance, "init");
|
||||
sinon.stub(instance, "clearCache").returns(Promise.resolve());
|
||||
await instance.onAction({
|
||||
type: at.PREF_CHANGED,
|
||||
data: {
|
||||
name: "feeds.section.topstories.options",
|
||||
value: JSON.stringify({ version: 2 }),
|
||||
},
|
||||
});
|
||||
assert.calledOnce(instance.uninit);
|
||||
assert.calledOnce(instance.init);
|
||||
assert.calledOnce(instance.clearCache);
|
||||
});
|
||||
it("should use UserDomainAffinityProvider from affinityProividerSwitcher not using v2", async () => {
|
||||
instance.affinityProviderV2 = { use_v2: false };
|
||||
sinon.stub(instance, "UserDomainAffinityProvider");
|
||||
sinon.stub(instance, "PersonalityProvider");
|
||||
|
||||
await instance.affinityProividerSwitcher();
|
||||
assert.notCalled(instance.PersonalityProvider);
|
||||
assert.calledOnce(instance.UserDomainAffinityProvider);
|
||||
});
|
||||
it("should not change provider with badly formed JSON", async () => {
|
||||
sinon.stub(instance, "uninit");
|
||||
sinon.stub(instance, "init");
|
||||
|
@ -1056,141 +957,6 @@ describe("Top Stories Feed", () => {
|
|||
assert.notCalled(instance.init);
|
||||
assert.notCalled(instance.clearCache);
|
||||
});
|
||||
it("should use PersonalityProvider from affinityProividerSwitcher using v2", async () => {
|
||||
instance.affinityProviderV2 = { use_v2: true };
|
||||
sinon.stub(instance, "UserDomainAffinityProvider");
|
||||
sinon.stub(instance, "PersonalityProvider");
|
||||
instance.PersonalityProvider = () => ({ init: sinon.stub() });
|
||||
|
||||
const provider = instance.affinityProividerSwitcher();
|
||||
assert.calledOnce(provider.init);
|
||||
assert.notCalled(instance.UserDomainAffinityProvider);
|
||||
});
|
||||
it("should use init and callback from affinityProividerSwitcher using v2", async () => {
|
||||
const stories = { recommendations: {} };
|
||||
sinon.stub(instance, "doContentUpdate");
|
||||
sinon.stub(instance, "rotate").returns(stories);
|
||||
sinon.stub(instance, "transform");
|
||||
instance.cache.get = () => ({ stories });
|
||||
instance.cache.set = sinon.spy();
|
||||
instance.affinityProvider = { getAffinities: () => ({}) };
|
||||
await instance.onPersonalityProviderInit();
|
||||
|
||||
assert.calledOnce(instance.doContentUpdate);
|
||||
assert.calledWith(
|
||||
instance.doContentUpdate,
|
||||
{ stories: { recommendations: {} } },
|
||||
false
|
||||
);
|
||||
assert.calledOnce(instance.rotate);
|
||||
assert.calledOnce(instance.transform);
|
||||
const { args } = instance.cache.set.firstCall;
|
||||
assert.equal(args[0], "domainAffinities");
|
||||
assert.equal(args[1]._timestamp, 0);
|
||||
});
|
||||
it("should call dispatchUpdateEvent from affinityProividerSwitcher using v2", async () => {
|
||||
const stories = { recommendations: {} };
|
||||
sinon.stub(instance, "rotate").returns(stories);
|
||||
sinon.stub(instance, "transform");
|
||||
sinon.spy(instance, "dispatchUpdateEvent");
|
||||
instance.cache.get = () => ({ stories });
|
||||
instance.cache.set = sinon.spy();
|
||||
instance.affinityProvider = { getAffinities: () => ({}) };
|
||||
|
||||
await instance.onPersonalityProviderInit();
|
||||
|
||||
assert.calledOnce(instance.dispatchUpdateEvent);
|
||||
});
|
||||
it("should return an object for UserDomainAffinityProvider", () => {
|
||||
assert.equal(typeof instance.UserDomainAffinityProvider(), "object");
|
||||
});
|
||||
it("should return an object for PersonalityProvider", () => {
|
||||
assert.equal(typeof instance.PersonalityProvider(), "object");
|
||||
});
|
||||
it("should call affinityProividerSwitcher on loadCachedData", async () => {
|
||||
instance.affinityProviderV2 = true;
|
||||
instance.personalized = true;
|
||||
sinon
|
||||
.stub(instance, "affinityProividerSwitcher")
|
||||
.returns(Promise.resolve());
|
||||
const domainAffinities = {
|
||||
parameterSets: {
|
||||
default: {
|
||||
recencyFactor: 0.4,
|
||||
frequencyFactor: 0.5,
|
||||
combinedDomainFactor: 0.5,
|
||||
perfectFrequencyVisits: 10,
|
||||
perfectCombinedDomainScore: 2,
|
||||
multiDomainBoost: 0.1,
|
||||
itemScoreFactor: 0,
|
||||
},
|
||||
},
|
||||
scores: { "a.com": 1, "b.com": 0.9 },
|
||||
maxHistoryQueryResults: 1000,
|
||||
timeSegments: {},
|
||||
version: "v1",
|
||||
};
|
||||
|
||||
instance.cache.get = () => ({ domainAffinities });
|
||||
await instance.loadCachedData();
|
||||
assert.calledOnce(instance.affinityProividerSwitcher);
|
||||
});
|
||||
it("should change domainAffinitiesLastUpdated on loadCachedData", async () => {
|
||||
instance.affinityProviderV2 = true;
|
||||
instance.personalized = true;
|
||||
const domainAffinities = {
|
||||
parameterSets: {
|
||||
default: {
|
||||
recencyFactor: 0.4,
|
||||
frequencyFactor: 0.5,
|
||||
combinedDomainFactor: 0.5,
|
||||
perfectFrequencyVisits: 10,
|
||||
perfectCombinedDomainScore: 2,
|
||||
multiDomainBoost: 0.1,
|
||||
itemScoreFactor: 0,
|
||||
},
|
||||
},
|
||||
scores: { "a.com": 1, "b.com": 0.9 },
|
||||
maxHistoryQueryResults: 1000,
|
||||
timeSegments: {},
|
||||
version: "v1",
|
||||
};
|
||||
|
||||
instance.cache.get = () => ({ domainAffinities });
|
||||
await instance.loadCachedData();
|
||||
assert.notEqual(instance.domainAffinitiesLastUpdated, 0);
|
||||
});
|
||||
it("should return false and do nothing if v2 already set", () => {
|
||||
instance.affinityProviderV2 = { use_v2: true, model_keys: ["item1orig"] };
|
||||
const result = instance.processAffinityProividerVersion({
|
||||
version: 2,
|
||||
model_keys: ["item1"],
|
||||
});
|
||||
assert.isTrue(instance.affinityProviderV2.use_v2);
|
||||
assert.isFalse(result);
|
||||
assert.equal(instance.affinityProviderV2.model_keys[0], "item1orig");
|
||||
});
|
||||
it("should return false and do nothing if v1 already set", () => {
|
||||
instance.affinityProviderV2 = null;
|
||||
const result = instance.processAffinityProividerVersion({ version: 1 });
|
||||
assert.isFalse(result);
|
||||
assert.isNull(instance.affinityProviderV2);
|
||||
});
|
||||
it("should return true and set v2", () => {
|
||||
const result = instance.processAffinityProividerVersion({
|
||||
version: 2,
|
||||
model_keys: ["item1"],
|
||||
});
|
||||
assert.isTrue(instance.affinityProviderV2.use_v2);
|
||||
assert.isTrue(result);
|
||||
assert.equal(instance.affinityProviderV2.model_keys[0], "item1");
|
||||
});
|
||||
it("should return true and set v1", () => {
|
||||
instance.affinityProviderV2 = {};
|
||||
const result = instance.processAffinityProividerVersion({ version: 1 });
|
||||
assert.isTrue(result);
|
||||
assert.isNull(instance.affinityProviderV2);
|
||||
});
|
||||
});
|
||||
describe("#spocs", async () => {
|
||||
it("should not display expired or untimestamped spocs", async () => {
|
||||
|
@ -1243,7 +1009,6 @@ describe("Top Stories Feed", () => {
|
|||
],
|
||||
};
|
||||
|
||||
instance.personalized = true;
|
||||
instance.show_spocs = true;
|
||||
instance.stories_endpoint = "stories-endpoint";
|
||||
instance.storiesLoaded = true;
|
||||
|
@ -1316,7 +1081,6 @@ describe("Top Stories Feed", () => {
|
|||
sectionsManagerStub.sections.set("topstories", {
|
||||
options: {
|
||||
show_spocs: true,
|
||||
personalized: true,
|
||||
stories_endpoint: "stories-endpoint",
|
||||
},
|
||||
});
|
||||
|
@ -1372,7 +1136,6 @@ describe("Top Stories Feed", () => {
|
|||
sectionsManagerStub.sections.set("topstories", {
|
||||
options: {
|
||||
show_spocs: false,
|
||||
personalized: true,
|
||||
stories_endpoint: "stories-endpoint",
|
||||
},
|
||||
});
|
||||
|
@ -1428,7 +1191,6 @@ describe("Top Stories Feed", () => {
|
|||
sectionsManagerStub.sections.set("topstories", {
|
||||
options: {
|
||||
show_spocs: true,
|
||||
personalized: true,
|
||||
stories_endpoint: "stories-endpoint",
|
||||
},
|
||||
});
|
||||
|
@ -1467,7 +1229,6 @@ describe("Top Stories Feed", () => {
|
|||
sectionsManagerStub.sections.set("topstories", {
|
||||
options: {
|
||||
show_spocs: true,
|
||||
personalized: true,
|
||||
stories_endpoint: "stories-endpoint",
|
||||
},
|
||||
});
|
||||
|
@ -1531,6 +1292,7 @@ describe("Top Stories Feed", () => {
|
|||
await instance.fetchStories();
|
||||
|
||||
let expectedPrefValue = JSON.stringify({ 5: [0] });
|
||||
let expectedPrefValueCallTwo = JSON.stringify({ 2: 0, 3: 0 });
|
||||
instance.onAction({
|
||||
type: at.TELEMETRY_IMPRESSION_STATS,
|
||||
data: {
|
||||
|
@ -1543,10 +1305,16 @@ describe("Top Stories Feed", () => {
|
|||
SPOC_IMPRESSION_TRACKING_PREF,
|
||||
expectedPrefValue
|
||||
);
|
||||
assert.calledWith(
|
||||
instance._prefs.set.secondCall,
|
||||
REC_IMPRESSION_TRACKING_PREF,
|
||||
expectedPrefValueCallTwo
|
||||
);
|
||||
|
||||
clock.tick(1);
|
||||
instance._prefs.get = pref => expectedPrefValue;
|
||||
let expectedPrefValueCallTwo = JSON.stringify({ 5: [0, 1] });
|
||||
let expectedPrefValueCallThree = JSON.stringify({ 5: [0, 1] });
|
||||
let expectedPrefValueCallFour = JSON.stringify({ 2: 1, 3: 1, 5: [0] });
|
||||
instance.onAction({
|
||||
type: at.TELEMETRY_IMPRESSION_STATS,
|
||||
data: {
|
||||
|
@ -1555,13 +1323,18 @@ describe("Top Stories Feed", () => {
|
|||
},
|
||||
});
|
||||
assert.calledWith(
|
||||
instance._prefs.set.secondCall,
|
||||
instance._prefs.set.thirdCall,
|
||||
SPOC_IMPRESSION_TRACKING_PREF,
|
||||
expectedPrefValueCallTwo
|
||||
expectedPrefValueCallThree
|
||||
);
|
||||
assert.calledWith(
|
||||
instance._prefs.set.getCall(3),
|
||||
REC_IMPRESSION_TRACKING_PREF,
|
||||
expectedPrefValueCallFour
|
||||
);
|
||||
|
||||
clock.tick(1);
|
||||
instance._prefs.get = pref => expectedPrefValueCallTwo;
|
||||
instance._prefs.get = pref => expectedPrefValueCallThree;
|
||||
instance.onAction({
|
||||
type: at.TELEMETRY_IMPRESSION_STATS,
|
||||
data: {
|
||||
|
@ -1570,10 +1343,15 @@ describe("Top Stories Feed", () => {
|
|||
},
|
||||
});
|
||||
assert.calledWith(
|
||||
instance._prefs.set.thirdCall,
|
||||
instance._prefs.set.getCall(4),
|
||||
SPOC_IMPRESSION_TRACKING_PREF,
|
||||
JSON.stringify({ 5: [0, 1], 6: [2] })
|
||||
);
|
||||
assert.calledWith(
|
||||
instance._prefs.set.getCall(5),
|
||||
REC_IMPRESSION_TRACKING_PREF,
|
||||
JSON.stringify({ 2: 2, 3: 2, 5: [0, 1] })
|
||||
);
|
||||
});
|
||||
it("should not record spoc/campaign impressions for non-view impressions", async () => {
|
||||
let fetchStub = globals.sandbox.stub();
|
||||
|
@ -1667,7 +1445,7 @@ describe("Top Stories Feed", () => {
|
|||
|
||||
let expectedPrefValue = JSON.stringify({ 5: [0], 6: [0] });
|
||||
assert.calledWith(
|
||||
instance._prefs.set.secondCall,
|
||||
instance._prefs.set.thirdCall,
|
||||
SPOC_IMPRESSION_TRACKING_PREF,
|
||||
expectedPrefValue
|
||||
);
|
||||
|
@ -1688,7 +1466,7 @@ describe("Top Stories Feed", () => {
|
|||
|
||||
// should remove campaign 5 from pref as no longer active
|
||||
assert.calledWith(
|
||||
instance._prefs.set.thirdCall,
|
||||
instance._prefs.set.getCall(4),
|
||||
SPOC_IMPRESSION_TRACKING_PREF,
|
||||
JSON.stringify({ 6: [0] })
|
||||
);
|
||||
|
@ -1699,7 +1477,6 @@ describe("Top Stories Feed", () => {
|
|||
sectionsManagerStub.sections.set("topstories", {
|
||||
options: {
|
||||
show_spocs: true,
|
||||
personalized: true,
|
||||
stories_endpoint: "stories-endpoint",
|
||||
},
|
||||
});
|
||||
|
@ -1802,7 +1579,6 @@ describe("Top Stories Feed", () => {
|
|||
sectionsManagerStub.sections.set("topstories", {
|
||||
options: {
|
||||
show_spocs: true,
|
||||
personalized: true,
|
||||
stories_endpoint: "stories-endpoint",
|
||||
},
|
||||
});
|
||||
|
@ -1884,53 +1660,6 @@ describe("Top Stories Feed", () => {
|
|||
read_more_endpoint: undefined,
|
||||
});
|
||||
});
|
||||
it("should update domain affinities on idle-daily, if personalization preffed on", async () => {
|
||||
instance.init();
|
||||
instance.affinityProvider = undefined;
|
||||
instance.cache.set = sinon.spy();
|
||||
|
||||
instance.observe("", "idle-daily");
|
||||
assert.isUndefined(instance.affinityProvider);
|
||||
|
||||
instance.personalized = true;
|
||||
instance.updateSettings({
|
||||
timeSegments: {},
|
||||
domainAffinityParameterSets: {},
|
||||
});
|
||||
clock.tick(MIN_DOMAIN_AFFINITIES_UPDATE_TIME);
|
||||
await instance.observe("", "idle-daily");
|
||||
assert.isDefined(instance.affinityProvider);
|
||||
assert.calledOnce(instance.cache.set);
|
||||
assert.calledWith(
|
||||
instance.cache.set,
|
||||
"domainAffinities",
|
||||
Object.assign({}, instance.affinityProvider.getAffinities(), {
|
||||
_timestamp: MIN_DOMAIN_AFFINITIES_UPDATE_TIME,
|
||||
})
|
||||
);
|
||||
});
|
||||
it("should not update domain affinities too often", () => {
|
||||
instance.init();
|
||||
instance.affinityProvider = undefined;
|
||||
instance.cache.set = sinon.spy();
|
||||
|
||||
instance.personalized = true;
|
||||
instance.updateSettings({
|
||||
timeSegments: {},
|
||||
domainAffinityParameterSets: {},
|
||||
});
|
||||
clock.tick(MIN_DOMAIN_AFFINITIES_UPDATE_TIME);
|
||||
instance.domainAffinitiesLastUpdated = Date.now();
|
||||
instance.observe("", "idle-daily");
|
||||
assert.isUndefined(instance.affinityProvider);
|
||||
});
|
||||
it("should add idle-daily observer right away, before waiting on init data", async () => {
|
||||
const addObserver = globals.sandbox.stub();
|
||||
globals.set("Services", { obs: { addObserver } });
|
||||
const initPromise = instance.onInit();
|
||||
assert.calledOnce(addObserver);
|
||||
await initPromise;
|
||||
});
|
||||
it("should not call init and uninit if data doesn't match on options change ", () => {
|
||||
sinon.spy(instance, "init");
|
||||
sinon.spy(instance, "uninit");
|
||||
|
@ -1971,26 +1700,6 @@ describe("Top Stories Feed", () => {
|
|||
|
||||
assert.deepEqual(instance.spocs, [{ url: "not_blocked" }]);
|
||||
});
|
||||
it("should reset domain affinity scores if version changed", async () => {
|
||||
instance.init();
|
||||
instance.personalized = true;
|
||||
instance.resetDomainAffinityScores = sinon.spy();
|
||||
instance.updateSettings({
|
||||
timeSegments: {},
|
||||
domainAffinityParameterSets: {},
|
||||
version: "1",
|
||||
});
|
||||
clock.tick(MIN_DOMAIN_AFFINITIES_UPDATE_TIME);
|
||||
await instance.observe("", "idle-daily");
|
||||
assert.notCalled(instance.resetDomainAffinityScores);
|
||||
|
||||
instance.updateSettings({
|
||||
timeSegments: {},
|
||||
domainAffinityParameterSets: {},
|
||||
version: "2",
|
||||
});
|
||||
assert.calledOnce(instance.resetDomainAffinityScores);
|
||||
});
|
||||
});
|
||||
describe("#loadCachedData", () => {
|
||||
it("should update section with cached stories and topics if available", async () => {
|
||||
|
@ -2111,63 +1820,6 @@ describe("Top Stories Feed", () => {
|
|||
true
|
||||
);
|
||||
});
|
||||
it("should initialize user domain affinity provider from cache if personalization is preffed on", async () => {
|
||||
const domainAffinities = {
|
||||
parameterSets: {
|
||||
default: {
|
||||
recencyFactor: 0.4,
|
||||
frequencyFactor: 0.5,
|
||||
combinedDomainFactor: 0.5,
|
||||
perfectFrequencyVisits: 10,
|
||||
perfectCombinedDomainScore: 2,
|
||||
multiDomainBoost: 0.1,
|
||||
itemScoreFactor: 0,
|
||||
},
|
||||
},
|
||||
scores: { "a.com": 1, "b.com": 0.9 },
|
||||
maxHistoryQueryResults: 1000,
|
||||
timeSegments: {},
|
||||
version: "v1",
|
||||
};
|
||||
|
||||
instance.affinityProvider = undefined;
|
||||
instance.cache.get = () => ({ domainAffinities });
|
||||
|
||||
await instance.loadCachedData();
|
||||
assert.isUndefined(instance.affinityProvider);
|
||||
instance.personalized = true;
|
||||
await instance.loadCachedData();
|
||||
assert.isDefined(instance.affinityProvider);
|
||||
assert.deepEqual(
|
||||
instance.affinityProvider.timeSegments,
|
||||
domainAffinities.timeSegments
|
||||
);
|
||||
assert.equal(
|
||||
instance.affinityProvider.maxHistoryQueryResults,
|
||||
domainAffinities.maxHistoryQueryResults
|
||||
);
|
||||
assert.deepEqual(
|
||||
instance.affinityProvider.parameterSets,
|
||||
domainAffinities.parameterSets
|
||||
);
|
||||
assert.deepEqual(
|
||||
instance.affinityProvider.scores,
|
||||
domainAffinities.scores
|
||||
);
|
||||
assert.deepEqual(
|
||||
instance.affinityProvider.version,
|
||||
domainAffinities.version
|
||||
);
|
||||
});
|
||||
it("should clear domain affinity cache when history is cleared", () => {
|
||||
instance.cache.set = sinon.spy();
|
||||
instance.affinityProvider = {};
|
||||
instance.personalized = true;
|
||||
|
||||
instance.onAction({ type: at.PLACES_HISTORY_CLEARED });
|
||||
assert.calledWith(instance.cache.set, "domainAffinities", {});
|
||||
assert.isUndefined(instance.affinityProvider);
|
||||
});
|
||||
});
|
||||
describe("#pocket", () => {
|
||||
it("should call getPocketState when hitting NEW_TAB_REHYDRATED", () => {
|
||||
|
|
Загрузка…
Ссылка в новой задаче