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:
Scott 2021-02-23 00:45:24 +00:00
Родитель 5fff3c0a33
Коммит 9da4bf1ea8
3 изменённых файлов: 38 добавлений и 613 удалений

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

@ -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", () => {