Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.

Commit dc29e9a

Browse files
committed
Bug 1690312 - Remove checks for exposure ping and allow it to be sent multiple times r=k88hudson
Differential Revision: https://phabricator.services.mozilla.com/D103941
1 parent beef5aa commit dc29e9a

File tree

12 files changed

+90
-178
lines changed

12 files changed

+90
-178
lines changed

browser/components/BrowserGlue.jsm

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3849,7 +3849,6 @@ BrowserGlue.prototype = {
38493849
);
38503850
let isFeatureEnabled = ExperimentAPI.getExperiment({
38513851
featureId: "infobar",
3852-
sendExposurePing: false,
38533852
})?.branch.feature.enabled;
38543853
if (willPrompt) {
38553854
// Prevent the related notification from appearing and

browser/components/newtab/aboutwelcome/AboutWelcomeChild.jsm

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,8 +310,6 @@ class AboutWelcomeChild extends JSWindowActorChild {
310310
// about:welcome loads outside of the "FirstStartup" scenario this will likely not be ready
311311
let experimentData = ExperimentAPI.getExperiment({
312312
featureId: "aboutwelcome",
313-
// Telemetry handled in AboutNewTabService.jsm
314-
sendExposurePing: false,
315313
});
316314

317315
if (experimentData?.slug) {

browser/components/newtab/lib/ASRouter.jsm

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -348,10 +348,7 @@ const MessageLoaderUtils = {
348348

349349
let experiments = [];
350350
for (const featureId of provider.messageGroups) {
351-
let experimentData = ExperimentAPI.getExperiment({
352-
featureId,
353-
sendExposurePing: false,
354-
});
351+
let experimentData = ExperimentAPI.getExperiment({ featureId });
355352
// Not enrolled in any experiment for this feature, we can skip
356353
if (!experimentData) {
357354
continue;
@@ -364,7 +361,6 @@ const MessageLoaderUtils = {
364361
if (featureData.enabled) {
365362
experiments.push({
366363
forExposureEvent: {
367-
sent: experimentData.exposurePingSent,
368364
experimentSlug: experimentData.slug,
369365
branchSlug: experimentData.branch.slug,
370366
},
@@ -1698,12 +1694,13 @@ class _ASRouter {
16981694
// Exposure events only apply to messages that come from the
16991695
// messaging-experiments provider
17001696
if (nonReachMessages.length && nonReachMessages[0].forExposureEvent) {
1701-
ExperimentAPI.recordExposureEvent(
1697+
ExperimentAPI.recordExposureEvent({
17021698
// Any message processed by ASRouter will report the exposure event
17031699
// as `cfr`
1704-
"cfr",
1705-
nonReachMessages[0].forExposureEvent
1706-
);
1700+
featureId: "cfr",
1701+
// experimentSlug and branchSlug
1702+
...nonReachMessages[0].forExposureEvent,
1703+
});
17071704
}
17081705

17091706
return this.routeCFRMessage(

browser/components/newtab/test/unit/asrouter/ASRouter.test.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1976,11 +1976,10 @@ describe("ASRouter", () => {
19761976
});
19771977

19781978
assert.calledOnce(global.ExperimentAPI.recordExposureEvent);
1979-
assert.calledWithExactly(
1980-
global.ExperimentAPI.recordExposureEvent,
1981-
"cfr",
1982-
messages[0].forExposureEvent
1983-
);
1979+
assert.calledWithExactly(global.ExperimentAPI.recordExposureEvent, {
1980+
featureId: "cfr",
1981+
...messages[0].forExposureEvent,
1982+
});
19841983
});
19851984
});
19861985

@@ -2659,7 +2658,6 @@ describe("ASRouter", () => {
26592658
assert.calledOnce(global.ExperimentAPI.getExperiment);
26602659
assert.calledWithExactly(global.ExperimentAPI.getExperiment, {
26612660
featureId: "asrouter",
2662-
sendExposurePing: false,
26632661
});
26642662
});
26652663
it("should handle the case of no experiments in the ExperimentAPI", async () => {

toolkit/components/messaging-system/experiments/ExperimentAPI.jsm

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ XPCOMUtils.defineLazyPreferenceGetter(
6565
COLLECTION_ID_PREF,
6666
COLLECTION_ID_FALLBACK
6767
);
68+
const EXPOSURE_EVENT_CATEGORY = "normandy";
69+
const EXPOSURE_EVENT_METHOD = "expose";
6870

6971
function parseJSON(value) {
7072
if (value) {
@@ -91,7 +93,7 @@ const ExperimentAPI = {
9193
*
9294
* @param {{slug?: string, featureId?: string}} options slug = An experiment identifier
9395
* or feature = a stable identifier for a type of experiment
94-
* @returns {{slug: string, active: bool, exposurePingSent: bool}} A matching experiment if one is found.
96+
* @returns {{slug: string, active: bool}} A matching experiment if one is found.
9597
*/
9698
getExperiment({ slug, featureId, sendExposurePing } = {}) {
9799
if (!slug && !featureId) {
@@ -113,7 +115,6 @@ const ExperimentAPI = {
113115
return {
114116
slug: experimentData.slug,
115117
active: experimentData.active,
116-
exposurePingSent: experimentData.exposurePingSent,
117118
branch: this.activateBranch({ featureId, sendExposurePing }),
118119
};
119120
}
@@ -146,7 +147,6 @@ const ExperimentAPI = {
146147
return {
147148
slug: experimentData.slug,
148149
active: experimentData.active,
149-
exposurePingSent: experimentData.exposurePingSent,
150150
branch: { slug: experimentData.branch.slug },
151151
};
152152
}
@@ -159,7 +159,7 @@ const ExperimentAPI = {
159159
* @param {{slug: string, featureId: string, sendExposurePing: bool}}
160160
* @returns {Branch | null}
161161
*/
162-
activateBranch({ slug, featureId, sendExposurePing = true }) {
162+
activateBranch({ slug, featureId, sendExposurePing }) {
163163
let experiment = null;
164164
try {
165165
if (slug) {
@@ -176,9 +176,9 @@ const ExperimentAPI = {
176176
}
177177

178178
if (sendExposurePing) {
179-
this._store._emitExperimentExposure({
179+
this.recordExposureEvent({
180180
experimentSlug: experiment.slug,
181-
branchSlug: experiment?.branch?.slug,
181+
branchSlug: experiment.branch.slug,
182182
featureId,
183183
});
184184
}
@@ -285,23 +285,22 @@ const ExperimentAPI = {
285285
return recipe?.branches;
286286
},
287287

288-
recordExposureEvent(name, { sent, experimentSlug, branchSlug }) {
289-
if (!IS_MAIN_PROCESS) {
290-
Cu.reportError("Need to call from Parent process");
291-
return false;
292-
}
293-
if (sent) {
294-
return false;
288+
recordExposureEvent({ featureId, experimentSlug, branchSlug }) {
289+
Services.telemetry.setEventRecordingEnabled(EXPOSURE_EVENT_CATEGORY, true);
290+
try {
291+
Services.telemetry.recordEvent(
292+
EXPOSURE_EVENT_CATEGORY,
293+
EXPOSURE_EVENT_METHOD,
294+
"feature_study",
295+
experimentSlug,
296+
{
297+
branchSlug,
298+
featureId,
299+
}
300+
);
301+
} catch (e) {
302+
Cu.reportError(e);
295303
}
296-
297-
// Notify listener to record that the ping was sent
298-
this._store._emitExperimentExposure({
299-
featureId: name,
300-
experimentSlug,
301-
branchSlug,
302-
});
303-
304-
return true;
305304
},
306305
};
307306

toolkit/components/messaging-system/experiments/ExperimentManager.jsm

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,6 @@ const TELEMETRY_EXPERIMENT_TYPE_PREFIX = "normandy-";
4444
// Also included in telemetry
4545
const DEFAULT_EXPERIMENT_TYPE = "messaging_experiment";
4646
const STUDIES_OPT_OUT_PREF = "app.shield.optoutstudies.enabled";
47-
const EXPOSURE_EVENT_CATEGORY = "normandy";
48-
const EXPOSURE_EVENT_METHOD = "expose";
4947

5048
/**
5149
* A module for processes Experiment recipes, choosing and storing enrollment state,
@@ -56,7 +54,6 @@ class _ExperimentManager {
5654
this.id = id;
5755
this.store = store || new ExperimentStore();
5856
this.sessions = new Map();
59-
this._onExposureEvent = this._onExposureEvent.bind(this);
6057
Services.prefs.addObserver(STUDIES_OPT_OUT_PREF, this);
6158
}
6259

@@ -88,7 +85,6 @@ class _ExperimentManager {
8885
*/
8986
async onStartup() {
9087
await this.store.init();
91-
this.store.on("exposure", this._onExposureEvent);
9288
const restoredExperiments = this.store.getAllActive();
9389

9490
for (const experiment of restoredExperiments) {
@@ -228,8 +224,6 @@ class _ExperimentManager {
228224
slug,
229225
branch,
230226
active: true,
231-
// Sent first time feature value is used
232-
exposurePingSent: false,
233227
enrollmentId,
234228
experimentType,
235229
source,
@@ -343,32 +337,6 @@ class _ExperimentManager {
343337
});
344338
}
345339

346-
async _onExposureEvent(event, experimentData) {
347-
await this.store.ready();
348-
this.store.updateExperiment(experimentData.experimentSlug, {
349-
exposurePingSent: true,
350-
});
351-
// featureId is not validated and might be rejected by recordEvent if not
352-
// properly defined in Events.yaml. Saving experiment state regardless of
353-
// the result, no use retrying.
354-
try {
355-
Services.telemetry.recordEvent(
356-
EXPOSURE_EVENT_CATEGORY,
357-
EXPOSURE_EVENT_METHOD,
358-
"feature_study",
359-
experimentData.experimentSlug,
360-
{
361-
branchSlug: experimentData.branchSlug,
362-
featureId: experimentData.featureId,
363-
}
364-
);
365-
} catch (e) {
366-
Cu.reportError(e);
367-
}
368-
369-
log.debug(`Experiment exposure: ${experimentData.experimentSlug}`);
370-
}
371-
372340
/**
373341
* Sets Telemetry when activating an experiment.
374342
*

toolkit/components/messaging-system/experiments/ExperimentStore.jsm

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,6 @@ class ExperimentStore extends SharedDataMap {
142142
this.off(`featureUpdate:${featureId}`, callback);
143143
}
144144

145-
/**
146-
* @param {{featureId: string, experimentSlug: string, branchSlug: string}} experimentData
147-
*/
148-
_emitExperimentExposure(experimentData) {
149-
this.emit("exposure", experimentData);
150-
}
151-
152145
/**
153146
* @param {Enrollment} experiment
154147
*/

toolkit/components/messaging-system/test/unit/test_ExperimentAPI.js

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ const { ExperimentFakes } = ChromeUtils.import(
99
const { TestUtils } = ChromeUtils.import(
1010
"resource://testing-common/TestUtils.jsm"
1111
);
12+
const { TelemetryTestUtils } = ChromeUtils.import(
13+
"resource://testing-common/TelemetryTestUtils.jsm"
14+
);
1215
const COLLECTION_ID_PREF = "messaging-system.rsexperimentloader.collection_id";
1316

1417
/**
@@ -64,6 +67,7 @@ add_task(async function test_getExperimentMetaData() {
6467
const sandbox = sinon.createSandbox();
6568
const manager = ExperimentFakes.manager();
6669
const expected = ExperimentFakes.experiment("foo");
70+
let exposureStub = sandbox.stub(ExperimentAPI, "recordExposureEvent");
6771

6872
await manager.onStartup();
6973
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
@@ -84,11 +88,14 @@ add_task(async function test_getExperimentMetaData() {
8488
"Should have the slug prop"
8589
);
8690

91+
Assert.ok(exposureStub.notCalled, "Not called for this method");
92+
8793
sandbox.restore();
8894
});
8995

9096
add_task(function test_getExperimentMetaData_safe() {
9197
const sandbox = sinon.createSandbox();
98+
let exposureStub = sandbox.stub(ExperimentAPI, "recordExposureEvent");
9299

93100
sandbox.stub(ExperimentAPI._store, "get").throws();
94101
sandbox.stub(ExperimentAPI._store, "getExperimentForFeature").throws();
@@ -114,6 +121,8 @@ add_task(function test_getExperimentMetaData_safe() {
114121
"Sanity check"
115122
);
116123

124+
Assert.ok(exposureStub.notCalled, "Not called for this feature");
125+
117126
sandbox.restore();
118127
});
119128

@@ -131,6 +140,7 @@ add_task(async function test_getExperiment_feature() {
131140
await manager.onStartup();
132141

133142
sandbox.stub(ExperimentAPI, "_store").get(() => ExperimentFakes.childStore());
143+
let exposureStub = sandbox.stub(ExperimentAPI, "recordExposureEvent");
134144

135145
manager.store.addExperiment(expected);
136146

@@ -146,6 +156,12 @@ add_task(async function test_getExperiment_feature() {
146156
"should return an experiment by featureId"
147157
);
148158

159+
Assert.ok(exposureStub.notCalled, "Not called by default");
160+
161+
ExperimentAPI.getExperiment({ featureId: "cfr", sendExposurePing: true });
162+
163+
Assert.ok(exposureStub.calledOnce, "Called explicitly.");
164+
149165
sandbox.restore();
150166
});
151167

@@ -385,15 +401,24 @@ add_task(async function test_activateBranch_activationEvent() {
385401
await store.init();
386402
store.addExperiment(experiment);
387403
// Adding stub later because `addExperiment` emits update events
388-
const stub = sandbox.stub(store, "emit");
389-
// Call activateBranch to trigger an activation event
404+
const stub = sandbox.stub(ExperimentAPI, "recordExposureEvent");
390405
ExperimentAPI.activateBranch({ featureId: "green" });
406+
Assert.equal(
407+
stub.callCount,
408+
0,
409+
"Exposure is not sent by default by activateBranch"
410+
);
411+
412+
ExperimentAPI.activateBranch({ featureId: "green", sendExposurePing: true });
391413

392414
Assert.equal(stub.callCount, 1, "Called by doing activateBranch");
393-
Assert.equal(stub.firstCall.args[0], "exposure", "Has correct event name");
394-
Assert.equal(
395-
stub.firstCall.args[1].experimentSlug,
396-
experiment.slug,
415+
Assert.deepEqual(
416+
stub.firstCall.args[0],
417+
{
418+
featureId: experiment.branch.feature.featureId,
419+
experimentSlug: experiment.slug,
420+
branchSlug: experiment.branch.slug,
421+
},
397422
"Has correct payload"
398423
);
399424
sandbox.restore();
@@ -442,8 +467,26 @@ add_task(async function test_activateBranch_noActivationEvent() {
442467
// Adding stub later because `addExperiment` emits update events
443468
const stub = sandbox.stub(store, "emit");
444469
// Call activateBranch to trigger an activation event
445-
ExperimentAPI.activateBranch({ featureId: "green", sendExposurePing: false });
470+
ExperimentAPI.activateBranch({ featureId: "green" });
446471

447472
Assert.equal(stub.callCount, 0, "Not called: sendExposurePing is false");
448473
sandbox.restore();
449474
});
475+
476+
add_task(function test_recordExposureEvent() {
477+
Services.telemetry.clearScalars();
478+
479+
ExperimentAPI.recordExposureEvent({
480+
featureId: "aboutwelcome",
481+
experimentSlug: "my-xpcshell-experiment",
482+
branchSlug: "treatment-a",
483+
});
484+
485+
const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
486+
TelemetryTestUtils.assertKeyedScalar(
487+
scalars,
488+
"telemetry.event_counts",
489+
"normandy#expose#feature_study",
490+
1
491+
);
492+
});

0 commit comments

Comments
 (0)