Bug 1963213 - Allow messaging on only one profile in a multiprofile selectable group r=pdahiya,jhirsch,omc-reviewers,profiles-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D247459
This commit is contained in:
Emily McMinn
2025-05-20 18:35:13 +00:00
committed by emcminn@mozilla.com
parent 9ea62cb6aa
commit c75f64e314
11 changed files with 295 additions and 10 deletions

View File

@@ -23,7 +23,8 @@ Please note that some targeting attributes require stricter controls on the tele
* [canCreateSelectableProfiles](#cancreateselectableprofiles) * [canCreateSelectableProfiles](#cancreateselectableprofiles)
* [creditCardsSaved](#creditcardssaved) * [creditCardsSaved](#creditcardssaved)
* [currentDate](#currentdate) * [currentDate](#currentdate)
* [currentTabGroups](#currentTabGroups) * [currentTabGroups](#currenttabgroups)
* [currentProfileId](#currentprofileid)
* [defaultPDFHandler](#defaultpdfhandler) * [defaultPDFHandler](#defaultpdfhandler)
* [devToolsOpenedCount](#devtoolsopenedcount) * [devToolsOpenedCount](#devtoolsopenedcount)
* [distributionId](#distributionid) * [distributionId](#distributionid)
@@ -1113,7 +1114,10 @@ declare const systemArch: string | null;
Returns the number of times a user has completed a search in the URL Bar. The number is arbitrarily capped at 100. Returns the number of times a user has completed a search in the URL Bar. The number is arbitrarily capped at 100.
### `profileGroupId` ### `profileGroupId`
Returns the stable profile group ID used for data reporting. Returns the stable profile group ID used for data reporting.
### `currentProfileId`
The integer-valued identifier of the current selectable profile, as reported by `SelectableProfileService`, converted to a string.

View File

@@ -60,6 +60,20 @@ ChromeUtils.defineESModuleGetters(lazy, {
ToolbarBadgeHub: "resource:///modules/asrouter/ToolbarBadgeHub.sys.mjs", ToolbarBadgeHub: "resource:///modules/asrouter/ToolbarBadgeHub.sys.mjs",
}); });
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"messagingProfileId",
"messaging-system.profile.messagingProfileId",
""
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"disableSingleProfileMessaging",
"messaging-system.profile.singleProfileMessaging.disable",
false
);
XPCOMUtils.defineLazyServiceGetters(lazy, { XPCOMUtils.defineLazyServiceGetters(lazy, {
BrowserHandler: ["@mozilla.org/browser/clh;1", "nsIBrowserHandler"], BrowserHandler: ["@mozilla.org/browser/clh;1", "nsIBrowserHandler"],
}); });
@@ -1658,6 +1672,29 @@ export class _ASRouter {
return impressions; return impressions;
} }
// Determine whether the current profile is using Selectable profiles;
// if yes, ensure we only message a single profile in the group.
shouldShowMessagesToProfile() {
// If the pref for this mitigation is disabled, skip these checks.
if (lazy.disableSingleProfileMessaging) {
return true;
}
// If multiple profiles aren't enabled or aren't being used,
// then always show messages.
if (
!lazy.ASRouterTargeting.Environment.canCreateSelectableProfiles ||
!lazy.ASRouterTargeting.Environment.hasSelectableProfiles
) {
return true;
}
// if multiple profiles exist and messagingProfileID is set,
// then show messages when profileID matches.
return (
lazy.messagingProfileId ===
lazy.ASRouterTargeting.Environment.currentProfileId
);
}
handleMessageRequest({ handleMessageRequest({
messages: candidates, messages: candidates,
triggerId, triggerId,
@@ -1668,6 +1705,13 @@ export class _ASRouter {
ordered = false, ordered = false,
returnAll = false, returnAll = false,
}) { }) {
// If using a selectable profile, return no messages
if (!this.shouldShowMessagesToProfile()) {
lazy.ASRouterPreferences.console.debug(
"Selectable profile in use; skip loading messages"
);
return returnAll ? [] : null;
}
let shouldCache; let shouldCache;
lazy.ASRouterPreferences.console.debug( lazy.ASRouterPreferences.console.debug(
"in handleMessageRequest, arguments = ", "in handleMessageRequest, arguments = ",

View File

@@ -2,6 +2,18 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// eslint-disable-next-line mozilla/use-static-import
const { XPCOMUtils } = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs"
);
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
SelectableProfileService:
"resource:///modules/profiles/SelectableProfileService.sys.mjs",
});
const PROVIDER_PREF_BRANCH = const PROVIDER_PREF_BRANCH =
"browser.newtabpage.activity-stream.asrouter.providers."; "browser.newtabpage.activity-stream.asrouter.providers.";
const DEVTOOLS_PREF = const DEVTOOLS_PREF =
@@ -15,6 +27,9 @@ const DEVTOOLS_PREF =
const DEBUG_PREF = "browser.newtabpage.activity-stream.asrouter.debugLogLevel"; const DEBUG_PREF = "browser.newtabpage.activity-stream.asrouter.debugLogLevel";
const FXA_USERNAME_PREF = "services.sync.username"; const FXA_USERNAME_PREF = "services.sync.username";
// To observe changes to Selectable Profiles
const SELECTABLE_PROFILES_UPDATED = "sps-profiles-updated";
const MESSAGING_PROFILE_ID_PREF = "messaging-system.profile.messagingProfileId";
const DEFAULT_STATE = { const DEFAULT_STATE = {
_initialized: false, _initialized: false,
@@ -30,6 +45,30 @@ const USER_PREFERENCES = {
"browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features", "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features",
}; };
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"messagingProfileId",
MESSAGING_PROFILE_ID_PREF,
""
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"disableSingleProfileMessaging",
"messaging-system.profile.singleProfileMessaging.disable",
false,
async prefVal => {
if (!prefVal) {
return;
}
// unset the user value of the profile ID pref
Services.prefs.clearUserPref(MESSAGING_PROFILE_ID_PREF);
await lazy.SelectableProfileService.flushSharedPrefToDatabase(
MESSAGING_PROFILE_ID_PREF
);
}
);
// Preferences that influence targeting attributes. When these change we need // Preferences that influence targeting attributes. When these change we need
// to re-evaluate if the message targeting still matches // to re-evaluate if the message targeting still matches
export const TARGETING_PREFERENCES = [FXA_USERNAME_PREF]; export const TARGETING_PREFERENCES = [FXA_USERNAME_PREF];
@@ -158,6 +197,48 @@ export class _ASRouterPreferences {
} }
} }
async _maybeSetMessagingProfileID() {
// If the pref for this mitigation is disabled, skip these checks.
if (lazy.disableSingleProfileMessaging) {
return;
}
await lazy.SelectableProfileService.init();
let currentProfileID =
lazy.SelectableProfileService.currentProfile?.id?.toString();
// if multiple profiles exist and messagingProfileID isn't set,
// set it and copy it around to the rest of the profile group.
try {
if (!lazy.messagingProfileId && currentProfileID) {
Services.prefs.setStringPref(
MESSAGING_PROFILE_ID_PREF,
currentProfileID
);
await lazy.SelectableProfileService.trackPref(
MESSAGING_PROFILE_ID_PREF
);
}
// if multiple profiles exist and messagingProfileID is set, make
// sure that a profile with that ID exists.
if (
lazy.messagingProfileId &&
lazy.SelectableProfileService.initialized
) {
let messagingProfile = await lazy.SelectableProfileService.getProfile(
parseInt(lazy.messagingProfileId, 10)
);
if (!messagingProfile) {
// the messaging profile got deleted; set the current profile instead
Services.prefs.setStringPref(
MESSAGING_PROFILE_ID_PREF,
currentProfileID
);
}
}
} catch (e) {
console.error(`Could not set profile ID: ${e}`);
}
}
get devtoolsEnabled() { get devtoolsEnabled() {
if (!this._initialized || this._devtoolsEnabled === null) { if (!this._initialized || this._devtoolsEnabled === null) {
this._devtoolsEnabled = Services.prefs.getBoolPref( this._devtoolsEnabled = Services.prefs.getBoolPref(
@@ -213,12 +294,17 @@ export class _ASRouterPreferences {
this._migrateProviderPrefs(); this._migrateProviderPrefs();
Services.prefs.addObserver(this._providerPrefBranch, this); Services.prefs.addObserver(this._providerPrefBranch, this);
Services.prefs.addObserver(this._devtoolsPref, this); Services.prefs.addObserver(this._devtoolsPref, this);
Services.obs.addObserver(
this._maybeSetMessagingProfileID,
SELECTABLE_PROFILES_UPDATED
);
for (const id of Object.keys(USER_PREFERENCES)) { for (const id of Object.keys(USER_PREFERENCES)) {
Services.prefs.addObserver(USER_PREFERENCES[id], this); Services.prefs.addObserver(USER_PREFERENCES[id], this);
} }
for (const targetingPref of TARGETING_PREFERENCES) { for (const targetingPref of TARGETING_PREFERENCES) {
Services.prefs.addObserver(targetingPref, this); Services.prefs.addObserver(targetingPref, this);
} }
this._maybeSetMessagingProfileID();
this._initialized = true; this._initialized = true;
} }

View File

@@ -1200,6 +1200,13 @@ const TargetingGetters = {
return QueryCache.getters.profileGroupId.get(); return QueryCache.getters.profileGroupId.get();
}, },
get currentProfileId() {
if (!lazy.SelectableProfileService.currentProfile) {
return "";
}
return lazy.SelectableProfileService.currentProfile.id.toString();
},
get buildId() { get buildId() {
return parseInt(AppConstants.MOZ_BUILDID, 10); return parseInt(AppConstants.MOZ_BUILDID, 10);
}, },

View File

@@ -170,6 +170,9 @@ describe("ASRouter", () => {
scoreThreshold: 5000, scoreThreshold: 5000,
isChinaRepack: false, isChinaRepack: false,
userId: "adsf", userId: "adsf",
currentProfileId: "1",
canCreateSelectableProfiles: false,
hasSelectableProfiles: false,
}, },
}; };
gBrowser = { gBrowser = {
@@ -1129,6 +1132,38 @@ describe("ASRouter", () => {
providers: [{ id: "cfr" }, { id: "badge" }], providers: [{ id: "cfr" }, { id: "badge" }],
})); }));
}); });
it("should return no messages if shouldShowMessagesToProfile returns false", async () => {
sandbox.stub(Router, "shouldShowMessagesToProfile").returns(false);
await Router.setState(() => ({
messages: [
{ id: "foo", provider: "cfr", groups: ["cfr"] },
{ id: "bar", provider: "cfr", groups: ["cfr"] },
],
}));
const result = await Router.handleMessageRequest({
provider: "cfr",
});
assert.isNull(result);
});
it("should return messages if shouldShowMessagesToProfile returns true", async () => {
sandbox.stub(Router, "shouldShowMessagesToProfile").returns(true);
await Router.setState(() => ({
messages: [
{ id: "foo", provider: "cfr", groups: ["cfr"] },
{ id: "bar", provider: "cfr", groups: ["cfr"] },
],
}));
const result = await Router.handleMessageRequest({
provider: "cfr",
});
assert.isNotNull(result);
assert.calledWithMatch(ASRouterTargeting.findMatchingMessage, {
messages: [
{ id: "foo", provider: "cfr", groups: ["cfr"] },
{ id: "bar", provider: "cfr", groups: ["cfr"] },
],
});
});
it("should not return a blocked message", async () => { it("should not return a blocked message", async () => {
// Block all messages except the first // Block all messages except the first
await Router.setState(() => ({ await Router.setState(() => ({

View File

@@ -1,15 +1,22 @@
/* Any copyright is dedicated to the Public Domain. /* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */ http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from ../../../../../toolkit/profile/test/xpcshell/head.js */
/* import-globals-from ../../../../../browser/components/profiles/tests/unit/head.js */
"use strict"; "use strict";
const { XPCOMUtils } = ChromeUtils.importESModule( const { XPCOMUtils } = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs" "resource://gre/modules/XPCOMUtils.sys.mjs"
); );
const { sinon } = ChromeUtils.importESModule(
"resource://testing-common/Sinon.sys.mjs"
);
const lazy = {}; ChromeUtils.defineESModuleGetters(this, {
ChromeUtils.defineESModuleGetters(lazy, {
JsonSchema: "resource://gre/modules/JsonSchema.sys.mjs", JsonSchema: "resource://gre/modules/JsonSchema.sys.mjs",
SelectableProfileService:
"resource:///modules/profiles/SelectableProfileService.sys.mjs",
}); });
function assertValidates(validator, obj, msg) { function assertValidates(validator, obj, msg) {
@@ -31,7 +38,7 @@ async function fetchSchema(uri) {
async function schemaValidatorFor(uri, { common = false } = {}) { async function schemaValidatorFor(uri, { common = false } = {}) {
const schema = await fetchSchema(uri); const schema = await fetchSchema(uri);
const validator = new lazy.JsonSchema.Validator(schema); const validator = new JsonSchema.Validator(schema);
if (common) { if (common) {
const commonSchema = await fetchSchema( const commonSchema = await fetchSchema(

View File

@@ -0,0 +1,51 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
const { ASRouterPreferences } = ChromeUtils.importESModule(
"resource:///modules/asrouter/ASRouterPreferences.sys.mjs"
);
add_task(async function test_maybeSetMessagingProfileID() {
await initSelectableProfileService();
let currentProfile = sinon
.stub(SelectableProfileService, "currentProfile")
.value({ id: 1 });
sinon.stub(SelectableProfileService, "trackPref").resolves();
// If the Profile ID pref is unset and a profile exists, set it
Services.prefs.setStringPref(
"messaging-system.profile.messagingProfileId",
""
);
await ASRouterPreferences._maybeSetMessagingProfileID();
Assert.equal(
"1",
Services.prefs.getStringPref("messaging-system.profile.messagingProfileId")
);
// Once the ID has been set, check to see if a profile exists
currentProfile.value({ id: 2 });
let messagingProfile = sinon
.stub(SelectableProfileService, "getProfile")
.returns({ id: 1 });
// If the profile exists, do nothing
await ASRouterPreferences._maybeSetMessagingProfileID();
Assert.equal(
"1",
Services.prefs.getStringPref("messaging-system.profile.messagingProfileId")
);
// If the profile does not exist, reset the Profile ID pref
messagingProfile.returns(null);
await ASRouterPreferences._maybeSetMessagingProfileID();
Assert.equal(
"2",
Services.prefs.getStringPref("messaging-system.profile.messagingProfileId")
);
});

View File

@@ -17,7 +17,6 @@ ChromeUtils.defineESModuleGetters(this, {
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
JsonSchemaValidator: JsonSchemaValidator:
"resource://gre/modules/components-utils/JsonSchemaValidator.sys.mjs", "resource://gre/modules/components-utils/JsonSchemaValidator.sys.mjs",
sinon: "resource://testing-common/Sinon.sys.mjs",
TelemetryController: "resource://gre/modules/TelemetryController.sys.mjs", TelemetryController: "resource://gre/modules/TelemetryController.sys.mjs",
UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs", UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
}); });

View File

@@ -0,0 +1,46 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
const { ASRouter } = ChromeUtils.importESModule(
"resource:///modules/asrouter/ASRouter.sys.mjs"
);
const { ASRouterTargeting } = ChromeUtils.importESModule(
"resource:///modules/asrouter/ASRouterTargeting.sys.mjs"
);
add_task(async function test_shouldShowMessagesToProfile() {
let sandbox = sinon.createSandbox();
// shouldShowMessages should return true if the Selectable Profile Service is not enabled
Services.prefs.setBoolPref("browser.profiles.enabled", false);
Assert.equal(ASRouter.shouldShowMessagesToProfile(), true);
// should return true if the Selectable Profile Service is enabled but no profiles have been created
Services.prefs.setBoolPref("browser.profiles.enabled", true);
Assert.equal(ASRouter.shouldShowMessagesToProfile(), true);
// should return false if the Selectable Profile Service is enabled, and there is a profile but the profile IDs don't match
await initSelectableProfileService();
Services.prefs.setBoolPref("browser.profiles.created", true);
Services.prefs.setStringPref(
"messaging-system.profile.messagingProfileId",
"2"
);
sandbox.replaceGetter(
ASRouterTargeting.Environment,
"currentProfileId",
function () {
return "1";
}
);
Assert.equal(ASRouter.shouldShowMessagesToProfile(), false);
// should return true if the Selectable Profile Service is enabled, and the profile IDs match
Services.prefs.setStringPref(
"messaging-system.profile.messagingProfileId",
"1"
);
Assert.equal(ASRouter.shouldShowMessagesToProfile(), true);
});

View File

@@ -4,9 +4,6 @@
const { OnboardingMessageProvider } = ChromeUtils.importESModule( const { OnboardingMessageProvider } = ChromeUtils.importESModule(
"resource:///modules/asrouter/OnboardingMessageProvider.sys.mjs" "resource:///modules/asrouter/OnboardingMessageProvider.sys.mjs"
); );
const { sinon } = ChromeUtils.importESModule(
"resource://testing-common/Sinon.sys.mjs"
);
function getOnboardingScreenById(screens, screenId) { function getOnboardingScreenById(screens, screenId) {
return screens.find(screen => { return screens.find(screen => {

View File

@@ -1,7 +1,12 @@
[DEFAULT] [DEFAULT]
head = "head.js" head = "../../../../../toolkit/profile/test/xpcshell/head.js ../../../../../browser/components/profiles/tests/unit/head.js head.js"
firefox-appdir = "browser" firefox-appdir = "browser"
prefs = [
"browser.profiles.enabled=true",
"browser.profiles.created=false",
]
["test_ASRouterTargeting_attribution.js"] ["test_ASRouterTargeting_attribution.js"]
run-if = ["os == 'mac'"] # osx specific tests run-if = ["os == 'mac'"] # osx specific tests
@@ -12,6 +17,10 @@ support-files = ["../schemas/*.schema.json"]
["test_ASRouter_getTargetingParameters.js"] ["test_ASRouter_getTargetingParameters.js"]
["test_ASRouter_shouldShowMessagesToProfile.js"]
["test_ASRouterPreferences_maybeSetMessagingProfileID.js"]
["test_CFRMessageProvider.js"] ["test_CFRMessageProvider.js"]
["test_InflightAssetsMessageProvider.js"] ["test_InflightAssetsMessageProvider.js"]