Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* Any copyright is dedicated to the Public Domain.
// Tests that a recorded engagement survives the Urlbar actor's
// `RecordEngagement` message and that recording it parent-side from the
// deserialized payload produces the same Glean event the in-process path would.
// The child builds the Glean event content-side; the picked result rides along
// in wire form (structured-clone drops its private-field data unless reduced) so
// the parent can run the provider notifications.
"use strict";
const { UrlbarTelemetryUtils } = ChromeUtils.importESModule(
"chrome://browser/content/urlbar/UrlbarTelemetryUtils.mjs"
);
function makeResult(url) {
return new UrlbarResult({
type: UrlbarShared.RESULT_TYPE.URL,
source: UrlbarShared.RESULT_SOURCE.HISTORY,
payload: { url, title: "Title for " + url },
});
}
// Builds a recorded engagement the way the child collector would: collectSnapshot()
// from a (faked) DOM event/element and the picked result, then the Glean event
// via buildEventInfo(), leaving the parent-only fields to the recorder.
function makeRecordedEngagement({ exposures = null } = {}) {
let picked = makeResult("https://example.com/picked");
picked.rowIndex = 2;
let snapshot = UrlbarTelemetryUtils.collectSnapshot(
{ type: "keydown" },
{
result: picked,
element: { dataset: { action: "pick_action" } },
searchString: "foo",
selType: "history",
searchSource: "urlbar",
windowMode: "classic",
},
{ timeStamp: 0, interactionType: "typed", searchString: "foo" }
);
let visibleResults = [picked, makeResult("https://example.com/other")];
let { internalDetails } = snapshot;
let { interaction } = UrlbarTelemetryUtils.getInteractionType(
snapshot.method,
snapshot.startEventInfo,
internalDetails.searchSource,
snapshot.searchWords,
null,
null
);
let built = UrlbarTelemetryUtils.buildEventInfo({
method: snapshot.method,
action: snapshot.action,
interaction,
numChars: snapshot.numChars,
numWords: snapshot.numWords,
provider: internalDetails.provider,
searchSource: internalDetails.searchSource,
searchMode: null,
selIndex: internalDetails.selIndex,
visibleResults,
viewIsOpen: true,
selType: internalDetails.selType,
pickedActionKey: internalDetails.pickedActionKey,
windowMode: internalDetails.windowMode,
});
return { snapshot, built, internalDetails, visibleResults, exposures };
}
// The wire payload a `RecordEngagement` message carries.
function toWire({ built, internalDetails, exposures }) {
return UrlbarTelemetryUtils.recordedEngagementToWire({
built,
method: "engagement",
searchSource: internalDetails.searchSource,
internalDetails,
exposures,
});
}
add_setup(async function () {
Services.fog.initializeFOG();
// The engagement recording reads the default engine's telemetry id.
await SearchTestUtils.installSearchExtension(
{ name: "Test" },
{ setAsDefault: true }
);
});
add_task(function test_bare_structuredClone_loses_results() {
let { snapshot } = makeRecordedEngagement();
let cloned = structuredClone(snapshot);
Assert.equal(
cloned.internalDetails.result.type,
undefined,
"A bare structuredClone of the picked result loses its private-field data"
);
});
add_task(function test_recordedEngagement_roundtrip() {
let engagement = makeRecordedEngagement();
Assert.ok(
engagement.internalDetails.event,
"internalDetails carries the live event before serialization"
);
let restored = UrlbarTelemetryUtils.recordedEngagementFromWire(
structuredClone(toWire(engagement))
);
// The DOM event and element can't cross and are dropped.
Assert.equal(
restored.internalDetails.event,
null,
"event dropped on the wire"
);
Assert.equal(
restored.internalDetails.element,
null,
"element dropped on the wire"
);
// The built Glean event survives as primitives.
Assert.equal(restored.built.metric, "engagement", "metric preserved");
Assert.equal(restored.built.eventInfo.n_chars, "3", "n_chars preserved");
Assert.equal(restored.built.eventInfo.n_results, "2", "n_results preserved");
// The picked result is reconstructed as a UrlbarResult.
let result = restored.internalDetails.result;
Assert.ok(result instanceof UrlbarResult, "picked result reconstructed");
Assert.equal(
result.payload.url,
"picked result payload preserved"
);
Assert.equal(result.rowIndex, 2, "picked result rowIndex preserved");
});
add_task(function test_recordFromChild_records_engagement() {
Services.fog.testResetFOG();
let controller = UrlbarTestUtils.newMockController();
// Hand the controller the payload exactly as the actor message would.
controller.recordEngagement(
structuredClone(toWire(makeRecordedEngagement()))
);
let events = Glean.urlbar.engagement.testGetValue();
Assert.equal(events?.length, 1, "recorded one engagement over the wire");
Assert.equal(events[0].extra.engagement_type, "enter", "engagement_type");
Assert.equal(events[0].extra.sap, "urlbar", "sap");
Assert.equal(events[0].extra.n_chars, "3", "n_chars");
Assert.equal(events[0].extra.n_results, "2", "n_results from visibleResults");
});
add_task(function test_exposure_helpers() {
let result = makeResult("https://example.com/exposed");
let visible = [result, makeResult("https://example.com/other")];
let { resultType } = UrlbarTelemetryUtils.exposureEntry(result, {
isPrivate: false,
});
Assert.equal(typeof resultType, "string", "exposureEntry returns a type");
Assert.ok(
UrlbarTelemetryUtils.exposureTerminal(result, null, visible),
"a visible result is terminal"
);
Assert.ok(
!UrlbarTelemetryUtils.exposureTerminal(
makeResult("https://example.com/gone"),
null,
visible
),
"a result no longer visible is not terminal"
);
});
add_task(function test_collectBounceSnapshot() {
let result = makeResult("https://example.com/picked");
result.rowIndex = 1;
let visibleResults = [result, makeResult("https://example.com/other")];
let snapshot = UrlbarTelemetryUtils.collectBounceSnapshot(
{ type: "keydown" },
{
result,
searchString: "foo",
selType: "history",
searchSource: "urlbar",
windowMode: "classic",
},
{ timeStamp: 0, interactionType: "typed", searchString: "foo" },
visibleResults
);
// The action is resolved content-side; the event is not retained.
Assert.equal(snapshot.action, "enter", "action resolved from the event");
Assert.ok(!("event" in snapshot), "the DOM event is not stored");
Assert.equal(snapshot.numChars, "3", "numChars");
Assert.equal(snapshot.selIndex, 1, "selIndex from the picked result");
Assert.equal(
snapshot.visibleResults,
visibleResults,
"visible results captured next to selIndex"
);
Assert.equal(snapshot.searchSource, "urlbar", "searchSource");
Assert.equal(
UrlbarTelemetryUtils.collectBounceSnapshot(null, {}, null),
null,
"no snapshot without a session"
);
});
add_task(function test_recordFromChild_records_exposures() {
Services.fog.testResetFOG();
let controller = UrlbarTestUtils.newMockController();
// The child collector resolves exposures content-side and bundles the list
// into the engagement payload.
let engagement = makeRecordedEngagement({
exposures: [
{ resultType: "history", keyword: null, terminal: true },
{ resultType: "search_suggest", keyword: null, terminal: false },
],
});
controller.recordEngagement(structuredClone(toWire(engagement)));
let events = Glean.urlbar.exposure.testGetValue();
Assert.equal(events?.length, 1, "recorded one exposure event over the wire");
// Tuples are sorted by result type.
Assert.equal(events[0].extra.results, "history,search_suggest", "results");
Assert.equal(events[0].extra.terminal, "true,false", "terminal flags");
Assert.equal(events[0].extra.sap, "urlbar", "sap");
});