Bug 1822395 - Telemetry for new PiP controls. r=mconley

Differential Revision: https://phabricator.services.mozilla.com/D173079
This commit is contained in:
Niklas Baumgardner 2023-04-04 18:47:08 +00:00
parent aabb183c44
commit 229e52a604
12 changed files with 690 additions and 245 deletions

View File

@ -117,28 +117,30 @@ export class ContextMenuChild extends JSWindowActorChild {
}
break;
case "pictureinpicture":
Services.telemetry.keyedScalarAdd(
"pictureinpicture.opened_method",
"contextmenu",
1
);
let args = {
method: "contextMenu",
firstTimeToggle: (!Services.prefs.getBoolPref(
"media.videocontrols.picture-in-picture.video-toggle.has-used"
)).toString(),
};
Services.telemetry.recordEvent(
"pictureinpicture",
"opened_method",
"method",
null,
args
);
if (!media.isCloningElementVisually) {
Services.telemetry.keyedScalarAdd(
"pictureinpicture.opened_method",
"contextmenu",
1
);
let args = {
firstTimeToggle: (!Services.prefs.getBoolPref(
"media.videocontrols.picture-in-picture.video-toggle.has-used"
)).toString(),
};
Services.telemetry.recordEvent(
"pictureinpicture",
"opened_method",
"contextMenu",
null,
args
);
}
let event = new this.contentWindow.CustomEvent(
"MozTogglePictureInPicture",
{
bubbles: true,
detail: { reason: "contextMenu" },
},
this.contentWindow
);

View File

@ -4878,13 +4878,7 @@ BrowserGlue.prototype = {
Services.telemetry.recordEvent(
"pictureinpicture.settings",
"enable",
"player"
);
} else {
Services.telemetry.recordEvent(
"pictureinpicture.settings",
"disable",
"player"
"settings"
);
}
}

View File

@ -333,6 +333,17 @@ var gMainPane = {
)
) {
document.getElementById("pictureInPictureBox").hidden = false;
setEventListener("pictureInPictureToggleEnabled", "command", function(
event
) {
if (!event.target.checked) {
Services.telemetry.recordEvent(
"pictureinpicture.settings",
"disable",
"settings"
);
}
});
}
if (AppConstants.platform == "win") {

View File

@ -136,7 +136,10 @@ export class PictureInPictureLauncherChild extends JSWindowActorChild {
switch (event.type) {
case "MozTogglePictureInPicture": {
if (event.isTrusted) {
this.togglePictureInPicture(event.target);
this.togglePictureInPicture({
video: event.target,
reason: event.detail,
});
}
break;
}
@ -158,24 +161,25 @@ export class PictureInPictureLauncherChild extends JSWindowActorChild {
* Picture-in-Picture window existing, this tells the parent to
* close it before opening the new one.
*
* @param {Element} video The <video> element to view in a Picture
* in Picture window.
* @param {Object} pipObject An object containing the video and reason
* for toggling the PiP video
*
* @return {Promise}
* @resolves {undefined} Once the new Picture-in-Picture window
* has been requested.
*/
async togglePictureInPicture(video) {
async togglePictureInPicture(pipObject) {
let { video, reason } = pipObject;
if (video.isCloningElementVisually) {
// The only way we could have entered here for the same video is if
// we are toggling via the context menu, since we hide the inline
// Picture-in-Picture toggle when a video is being displayed in
// Picture-in-Picture. Turn off PiP in this case
// we are toggling via the context menu or via the urlbar button,
// since we hide the inline Picture-in-Picture toggle when a video
// is being displayed in Picture-in-Picture. Turn off PiP in this case
const stopPipEvent = new this.contentWindow.CustomEvent(
"MozStopPictureInPicture",
{
bubbles: true,
detail: { reason: "context-menu" },
detail: reason,
}
);
video.dispatchEvent(stopPipEvent);
@ -245,7 +249,7 @@ export class PictureInPictureLauncherChild extends JSWindowActorChild {
listOfVideos.sort((a, b) => b.duration - a.duration)[0];
}
if (video) {
this.togglePictureInPicture(video);
this.togglePictureInPicture({ video });
}
}
}
@ -604,10 +608,25 @@ export class PictureInPictureToggleChild extends JSWindowActorChild {
this.eligiblePipVideos
)[0];
if (video) {
if (!video.isCloningElementVisually) {
let args = {
firstTimeToggle: (!Services.prefs.getBoolPref(
"media.videocontrols.picture-in-picture.video-toggle.has-used"
)).toString(),
};
Services.telemetry.recordEvent(
"pictureinpicture",
"opened_method",
"urlBar",
null,
args
);
}
let pipEvent = new this.contentWindow.CustomEvent(
"MozTogglePictureInPicture",
{
bubbles: true,
detail: { reason: "urlBar" },
}
);
video.dispatchEvent(pipEvent);
@ -1008,7 +1027,6 @@ export class PictureInPictureToggleChild extends JSWindowActorChild {
1
);
let args = {
method: "toggle",
firstTimeToggle: (!Services.prefs.getBoolPref(
TOGGLE_HAS_USED_PREF
)).toString(),
@ -1016,7 +1034,7 @@ export class PictureInPictureToggleChild extends JSWindowActorChild {
Services.telemetry.recordEvent(
"pictureinpicture",
"opened_method",
"method",
"toggle",
null,
args
);
@ -1861,7 +1879,7 @@ export class PictureInPictureChild extends JSWindowActorChild {
switch (event.type) {
case "MozStopPictureInPicture": {
if (event.isTrusted && event.target === this.getWeakVideo()) {
const reason = event.detail?.reason || "video-el-remove";
const reason = event.detail?.reason || "videoElRemove";
this.closePictureInPicture({ reason });
}
break;
@ -1927,7 +1945,7 @@ export class PictureInPictureChild extends JSWindowActorChild {
// close Picture-in-Picture.
this.emptiedTimeout = setTimeout(() => {
if (!video || !video.src) {
this.closePictureInPicture({ reason: "video-el-emptied" });
this.closePictureInPicture({ reason: "videoElEmptied" });
}
}, EMPTIED_TIMEOUT_MS);
break;
@ -2269,7 +2287,7 @@ export class PictureInPictureChild extends JSWindowActorChild {
// If the video element has gone away before we've had a chance to set up
// Picture-in-Picture for it, tell the parent to close the Picture-in-Picture
// window.
await this.closePictureInPicture({ reason: "setup-failure" });
await this.closePictureInPicture({ reason: "setupFailure" });
return;
}
@ -2495,7 +2513,7 @@ export class PictureInPictureChild extends JSWindowActorChild {
return;
}
this.pause();
this.closePictureInPicture({ reason: "close-player-shortcut" });
this.closePictureInPicture({ reason: "closePlayerShortcut" });
break;
case "downArrow" /* Volume decrease */:
if (this.isKeyDisabled(lazy.KEYBOARD_CONTROLS.VOLUME)) {

View File

@ -591,13 +591,11 @@ export var PictureInPicture = {
}
this.removePiPBrowserFromWeakMap(this.weakWinToBrowser.get(win));
let args = { reason };
Services.telemetry.recordEvent(
"pictureinpicture",
"closed_method",
"method",
null,
args
reason,
null
);
await this.closePipWindow(win);
},
@ -1265,6 +1263,11 @@ export var PictureInPicture = {
hideToggle() {
Services.prefs.setBoolPref(TOGGLE_ENABLED_PREF, false);
Services.telemetry.recordEvent(
"pictureinpicture.settings",
"disable",
"player"
);
},
/**

View File

@ -170,6 +170,13 @@ let Player = {
*/
deferredResize: null,
/**
* Stores the previous events for seek forward/backward to correctly
* record the sequence if 3 clicks happen within a 1 second interval
*/
backwardSequence: [],
forwardSequence: [],
/**
* Initializes the player browser, and sets up the initial state.
*
@ -236,6 +243,9 @@ let Player = {
this.scrubber.addEventListener("change", event => {
this.handleScrubbingDone(event);
});
this.scrubber.addEventListener("pointerdown", event => {
this.recordEvent("seek", {});
});
for (let radio of document.querySelectorAll(
'input[type=radio][name="cc-size"]'
@ -590,11 +600,35 @@ let Player = {
case "seekBackward": {
this.actor.sendAsyncMessage("PictureInPicture:SeekBackward");
this.recordEvent("backward", {});
let secondPreviousEvent = this.backwardSequence.at(-2);
if (
secondPreviousEvent &&
event.timeStamp - secondPreviousEvent < 2000
) {
this.recordEvent("backward_sequence", {});
this.backwardSequence = [];
} else {
this.backwardSequence.push(event.timeStamp);
}
break;
}
case "seekForward": {
this.actor.sendAsyncMessage("PictureInPicture:SeekForward");
this.recordEvent("forward", {});
let secondPreviousEvent = this.forwardSequence.at(-2);
if (
secondPreviousEvent &&
event.timeStamp - secondPreviousEvent < 2000
) {
this.recordEvent("forward_sequence", {});
this.forwardSequence = [];
} else {
this.forwardSequence.push(event.timeStamp);
}
break;
}
@ -611,6 +645,9 @@ let Player = {
case "fullscreen": {
this.fullscreenModeToggle();
this.recordEvent("fullscreen", {
enter: (!this.isFullscreen).toString(),
});
break;
}
}
@ -641,7 +678,7 @@ let Player = {
this.actor.sendAsyncMessage("PictureInPicture:Pause", {
reason: "pip-closed",
});
this.closePipWindow({ reason: "close-button" });
this.closePipWindow({ reason: "closeButton" });
},
fullscreenModeToggle() {

View File

@ -30,9 +30,9 @@ const FIRST_CONTEXT_MENU_EXPECTED_EVENTS = [
[
"pictureinpicture",
"opened_method",
"method",
"contextMenu",
null,
{ method: "contextMenu", firstTimeToggle: "true" },
{ firstTimeToggle: "true" },
],
];
@ -40,9 +40,9 @@ const SECOND_CONTEXT_MENU_EXPECTED_EVENTS = [
[
"pictureinpicture",
"opened_method",
"method",
"contextMenu",
null,
{ method: "contextMenu", firstTimeToggle: "false" },
{ firstTimeToggle: "false" },
],
];
@ -51,9 +51,9 @@ const FIRST_TOGGLE_EXPECTED_EVENTS = [
[
"pictureinpicture",
"opened_method",
"method",
"toggle",
null,
{ method: "toggle", firstTimeToggle: "true" },
{ firstTimeToggle: "true" },
],
];
@ -62,9 +62,9 @@ const SECOND_TOGGLE_EXPECTED_EVENTS = [
[
"pictureinpicture",
"opened_method",
"method",
"toggle",
null,
{ method: "toggle", firstTimeToggle: "false" },
{ firstTimeToggle: "false" },
],
];
@ -235,34 +235,20 @@ add_task(async function test_eventTelemetry() {
// open with context menu for first time
await openAndClosePipWithContextMenu(browser, videoID);
let events = Services.telemetry.snapshotEvents(
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
false
).content;
info(`Length of events is ${events?.length}`);
await TestUtils.waitForCondition(
() => {
events = Services.telemetry.snapshotEvents(
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
false
).content;
info(`Length of events is ${events?.length}`);
return events && events.length >= 1;
},
"Waiting for one opened_method pictureinpicture telemetry event.",
200,
100
let filter = {
category: "pictureinpicture",
method: "opened_method",
object: "contextMenu",
};
await waitForTelemeryEvents(
filter,
FIRST_CONTEXT_MENU_EXPECTED_EVENTS.length,
"content"
);
info(JSON.stringify(events));
TelemetryTestUtils.assertEvents(
FIRST_CONTEXT_MENU_EXPECTED_EVENTS,
{
category: "pictureinpicture",
method: "opened_method",
object: "method",
},
filter,
{ clear: true, process: "content" }
);
@ -273,84 +259,54 @@ add_task(async function test_eventTelemetry() {
await openAndClosePipWithToggle(browser, videoID);
events = Services.telemetry.snapshotEvents(
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
false
).content;
info(`Length of events is ${events?.length}`);
await TestUtils.waitForCondition(
() => {
events = Services.telemetry.snapshotEvents(
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
false
).content;
info(`Length of events is ${events?.length}`);
return events && events.length >= 2;
},
"Waiting for both pictureinpicture telemetry events.",
200,
100
filter = {
category: "pictureinpicture",
};
await waitForTelemeryEvents(
filter,
FIRST_TOGGLE_EXPECTED_EVENTS.length,
"content"
);
info(JSON.stringify(events));
TelemetryTestUtils.assertEvents(
FIRST_TOGGLE_EXPECTED_EVENTS,
{ category: "pictureinpicture" },
{ clear: true, process: "content" }
);
TelemetryTestUtils.assertEvents(FIRST_TOGGLE_EXPECTED_EVENTS, filter, {
clear: true,
process: "content",
});
// open with toggle for not first time
await openAndClosePipWithToggle(browser, videoID);
info(`Length of events is ${events?.length}`);
await TestUtils.waitForCondition(
() => {
events = Services.telemetry.snapshotEvents(
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
false
).content;
info(`Length of events is ${events?.length}`);
return events && events.length >= 2;
},
"Waiting for both pictureinpicture telemetry events.",
200,
100
filter = {
category: "pictureinpicture",
};
await waitForTelemeryEvents(
filter,
SECOND_TOGGLE_EXPECTED_EVENTS.length,
"content"
);
info(JSON.stringify(events));
TelemetryTestUtils.assertEvents(
SECOND_TOGGLE_EXPECTED_EVENTS,
{ category: "pictureinpicture" },
{ clear: true, process: "content" }
);
TelemetryTestUtils.assertEvents(SECOND_TOGGLE_EXPECTED_EVENTS, filter, {
clear: true,
process: "content",
});
// open with context menu for not first time
await openAndClosePipWithContextMenu(browser, videoID);
info(`Length of events is ${events?.length}`);
await TestUtils.waitForCondition(
() => {
events = Services.telemetry.snapshotEvents(
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
false
).content;
info(`Length of events is ${events?.length}`);
return events && events.length >= 1;
},
"Waiting for one opened_method pictureinpicture telemetry event.",
200,
100
filter = {
category: "pictureinpicture",
method: "opened_method",
object: "contextMenu",
};
await waitForTelemeryEvents(
filter,
SECOND_CONTEXT_MENU_EXPECTED_EVENTS.length,
"content"
);
info(JSON.stringify(events));
TelemetryTestUtils.assertEvents(
SECOND_CONTEXT_MENU_EXPECTED_EVENTS,
{
category: "pictureinpicture",
method: "opened_method",
object: "method",
},
filter,
{ true: false, process: "content" }
);
}

View File

@ -3,6 +3,23 @@
"use strict";
const { TelemetryTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/TelemetryTestUtils.sys.mjs"
);
const REWIND_EVENTS = [
{
category: "pictureinpicture",
method: "seek",
object: "player",
},
{
category: "pictureinpicture",
method: "seek",
object: "player",
},
];
const TEST_PAGE_LONG = TEST_ROOT + "test-video-selection.html";
const IMPROVED_CONTROLS_ENABLED_PREF =
@ -255,6 +272,20 @@ add_task(async function testVideoScrubber() {
expectedVideoTime,
"Video current time is 7.96..."
);
let filter = {
category: "pictureinpicture",
method: "seek",
object: "player",
};
await waitForTelemeryEvents(filter, REWIND_EVENTS.length, "parent");
TelemetryTestUtils.assertEvents(REWIND_EVENTS, filter, {
clear: true,
process: "parent",
});
await ensureMessageAndClosePiP(browser, videoID, pipWin, false);
}
);
});

View File

@ -3,6 +3,8 @@
"use strict";
const TEST_PAGE_LONG = TEST_ROOT + "test-video-selection.html";
const { TelemetryTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/TelemetryTestUtils.sys.mjs"
);
@ -28,22 +30,131 @@ const EXPECTED_EVENT_CREATE_WITH_TEXT_TRACKS = [
];
const EXPECTED_EVENT_CLOSED_METHOD_CLOSE_BUTTON = [
[
"pictureinpicture",
"closed_method",
"method",
null,
{ reason: "close-button" },
],
{
category: "pictureinpicture",
method: "closed_method",
object: "closeButton",
},
];
const videoID = "with-controls";
const EXPECTED_EVENT_CLOSED_METHOD_UNPIP = [
["pictureinpicture", "closed_method", "method", null, { reason: "unpip" }],
{
category: "pictureinpicture",
method: "closed_method",
object: "unpip",
},
];
add_task(async () => {
const FULLSCREEN_EVENTS = [
{
category: "pictureinpicture",
method: "fullscreen",
object: "player",
extraKey: { enter: "true" },
},
{
category: "pictureinpicture",
method: "fullscreen",
object: "player",
extraKey: { enter: "true" },
},
];
const FORWARD_EVENTS = [
{
category: "pictureinpicture",
method: "forward",
object: "player",
},
{
category: "pictureinpicture",
method: "forward",
object: "player",
},
{
category: "pictureinpicture",
method: "forward",
object: "player",
},
{
category: "pictureinpicture",
method: "forward",
object: "player",
},
{
category: "pictureinpicture",
method: "forward",
object: "player",
},
{
category: "pictureinpicture",
method: "forward",
object: "player",
},
];
const FORWARD_SEQUENCE_EVENTS = [
{
category: "pictureinpicture",
method: "forward_sequence",
object: "player",
},
{
category: "pictureinpicture",
method: "forward_sequence",
object: "player",
},
];
const BACKWARD_EVENTS = [
{
category: "pictureinpicture",
method: "backward",
object: "player",
},
{
category: "pictureinpicture",
method: "backward",
object: "player",
},
{
category: "pictureinpicture",
method: "backward",
object: "player",
},
{
category: "pictureinpicture",
method: "backward",
object: "player",
},
{
category: "pictureinpicture",
method: "backward",
object: "player",
},
{
category: "pictureinpicture",
method: "backward",
object: "player",
},
];
const BACKWARD_SEQUENCE_EVENTS = [
{
category: "pictureinpicture",
method: "backward_sequence",
object: "player",
},
{
category: "pictureinpicture",
method: "backward_sequence",
object: "player",
},
];
add_task(async function testCreateAndCloseButtonTelemetry() {
await BrowserTestUtils.withNewTab(
{
gBrowser,
@ -57,64 +168,41 @@ add_task(async () => {
let pipWin = await triggerPictureInPicture(browser, videoID);
ok(pipWin, "Got Picture-in-Picture window.");
let events = Services.telemetry.snapshotEvents(
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
false
).parent;
await TestUtils.waitForCondition(
() => {
events = Services.telemetry.snapshotEvents(
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
false
).parent;
return events && events.length >= 1;
},
"Waiting for one create pictureinpicture telemetry event.",
200,
100
let filter = {
category: "pictureinpicture",
method: "create",
object: "player",
};
await waitForTelemeryEvents(
filter,
EXPECTED_EVENT_CREATE.length,
"parent"
);
TelemetryTestUtils.assertEvents(
EXPECTED_EVENT_CREATE,
{
category: "pictureinpicture",
method: "create",
object: "player",
},
{ clear: true, process: "parent" }
);
TelemetryTestUtils.assertEvents(EXPECTED_EVENT_CREATE, filter, {
clear: true,
process: "parent",
});
let pipClosed = BrowserTestUtils.domWindowClosed(pipWin);
let closeButton = pipWin.document.getElementById("close");
EventUtils.synthesizeMouseAtCenter(closeButton, {}, pipWin);
await pipClosed;
events = Services.telemetry.snapshotEvents(
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
false
).parent;
await TestUtils.waitForCondition(
() => {
events = Services.telemetry.snapshotEvents(
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
false
).parent;
return events && events.length >= 1;
},
"Waiting for one closed_method pictureinpicture telemetry event.",
200,
100
filter = {
category: "pictureinpicture",
method: "closed_method",
object: "closeButton",
};
await waitForTelemeryEvents(
filter,
EXPECTED_EVENT_CLOSED_METHOD_CLOSE_BUTTON.length,
"parent"
);
TelemetryTestUtils.assertEvents(
EXPECTED_EVENT_CLOSED_METHOD_CLOSE_BUTTON,
{
category: "pictureinpicture",
method: "closed_method",
object: "method",
},
filter,
{ clear: true, process: "parent" }
);
@ -127,7 +215,7 @@ add_task(async () => {
);
});
add_task(async function() {
add_task(async function textTextTracksAndUnpipTelemetry() {
await SpecialPowers.pushPrefEnv({
set: [
[
@ -150,31 +238,20 @@ add_task(async function() {
let pipWin = await triggerPictureInPicture(browser, videoID);
ok(pipWin, "Got Picture-in-Picture window.");
let events = Services.telemetry.snapshotEvents(
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
false
).parent;
await TestUtils.waitForCondition(
() => {
events = Services.telemetry.snapshotEvents(
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
false
).parent;
return events && events.length >= 1;
},
"Waiting for one create pictureinpicture telemetry event.",
200,
100
let filter = {
category: "pictureinpicture",
method: "create",
object: "player",
};
await waitForTelemeryEvents(
filter,
EXPECTED_EVENT_CREATE_WITH_TEXT_TRACKS.length,
"parent"
);
TelemetryTestUtils.assertEvents(
EXPECTED_EVENT_CREATE_WITH_TEXT_TRACKS,
{
category: "pictureinpicture",
method: "create",
object: "player",
},
filter,
{ clear: true, process: "parent" }
);
@ -183,33 +260,168 @@ add_task(async function() {
EventUtils.synthesizeMouseAtCenter(unpipButton, {}, pipWin);
await pipClosed;
events = Services.telemetry.snapshotEvents(
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
false
).parent;
await TestUtils.waitForCondition(
() => {
events = Services.telemetry.snapshotEvents(
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
false
).parent;
return events && events.length >= 1;
},
"Waiting for one closed_method pictureinpicture telemetry event.",
200,
100
filter = {
category: "pictureinpicture",
method: "closed_method",
object: "unpip",
};
await waitForTelemeryEvents(
filter,
EXPECTED_EVENT_CLOSED_METHOD_UNPIP.length,
"parent"
);
TelemetryTestUtils.assertEvents(
EXPECTED_EVENT_CLOSED_METHOD_UNPIP,
{
category: "pictureinpicture",
method: "closed_method",
object: "method",
},
filter,
{ clear: true, process: "parent" }
);
}
);
});
add_task(async function test_fullscreen_events() {
await BrowserTestUtils.withNewTab(
{
url: TEST_PAGE,
gBrowser,
},
async browser => {
Services.telemetry.clearEvents();
await ensureVideosReady(browser);
let pipWin = await triggerPictureInPicture(browser, videoID);
ok(pipWin, "Got Picture-in-Picture window.");
let fullscreenBtn = pipWin.document.getElementById("fullscreen");
await promiseFullscreenEntered(pipWin, () => {
fullscreenBtn.click();
});
await promiseFullscreenExited(pipWin, () => {
fullscreenBtn.click();
});
let filter = {
category: "pictureinpicture",
method: "fullscreen",
object: "player",
};
await waitForTelemeryEvents(filter, FULLSCREEN_EVENTS.length, "parent");
TelemetryTestUtils.assertEvents(FULLSCREEN_EVENTS, filter, {
clear: true,
process: "parent",
});
await ensureMessageAndClosePiP(browser, videoID, pipWin, false);
}
);
});
add_task(async function test_seek_forward_and_backward_events() {
await BrowserTestUtils.withNewTab(
{
url: TEST_PAGE_LONG,
gBrowser,
},
async browser => {
let waitForVideoEvent = eventType => {
return BrowserTestUtils.waitForContentEvent(browser, eventType, true);
};
const videoID = "long";
Services.telemetry.clearEvents();
await ensureVideosReady(browser);
let pipWin = await triggerPictureInPicture(browser, videoID);
ok(pipWin, "Got Picture-in-Picture window.");
let seekBackwardBtn = pipWin.document.getElementById("seekBackward");
let seekForwardBtn = pipWin.document.getElementById("seekForward");
// Test seek forward
let seekedForwardPromise = waitForVideoEvent("seeked");
seekForwardBtn.click();
seekForwardBtn.click();
seekForwardBtn.click();
seekForwardBtn.click();
seekForwardBtn.click();
seekForwardBtn.click();
ok(await seekedForwardPromise, "The Forward button triggers");
let filter = {
category: "pictureinpicture",
method: "forward",
object: "player",
};
await waitForTelemeryEvents(filter, FORWARD_EVENTS.length, "parent");
TelemetryTestUtils.assertEvents(FORWARD_EVENTS, filter, {
clear: false,
process: "parent",
});
filter = {
category: "pictureinpicture",
method: "forward_sequence",
object: "player",
};
await waitForTelemeryEvents(
filter,
FORWARD_SEQUENCE_EVENTS.length,
"parent"
);
TelemetryTestUtils.assertEvents(FORWARD_SEQUENCE_EVENTS, filter, {
clear: true,
process: "parent",
});
Services.telemetry.clearEvents();
let seekedBackwardPromise = waitForVideoEvent("seeked");
seekBackwardBtn.click();
seekBackwardBtn.click();
seekBackwardBtn.click();
seekBackwardBtn.click();
seekBackwardBtn.click();
seekBackwardBtn.click();
ok(await seekedBackwardPromise, "The Forward button triggers");
filter = {
category: "pictureinpicture",
method: "backward",
object: "player",
};
await waitForTelemeryEvents(filter, BACKWARD_EVENTS.length, "parent");
TelemetryTestUtils.assertEvents(BACKWARD_EVENTS, filter, {
clear: false,
process: "parent",
});
filter = {
category: "pictureinpicture",
method: "backward_sequence",
object: "player",
};
await waitForTelemeryEvents(
filter,
BACKWARD_SEQUENCE_EVENTS.length,
"parent"
);
TelemetryTestUtils.assertEvents(BACKWARD_SEQUENCE_EVENTS, filter, {
clear: true,
process: "parent",
});
await ensureMessageAndClosePiP(browser, videoID, pipWin, false);
}
);
});

View File

@ -3,6 +3,18 @@
"use strict";
const { TelemetryTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/TelemetryTestUtils.sys.mjs"
);
const PIP_URLBAR_EVENTS = [
{
category: "pictureinpicture",
method: "opened_method",
object: "urlBar",
},
];
add_task(async function test_urlbar_toggle_multiple_contexts() {
await BrowserTestUtils.withNewTab(
{
@ -10,6 +22,7 @@ add_task(async function test_urlbar_toggle_multiple_contexts() {
gBrowser,
},
async browser => {
Services.telemetry.clearEvents();
await ensureVideosReady(browser);
await ensureVideosReady(browser.browsingContext.children[0]);
@ -59,6 +72,18 @@ add_task(async function test_urlbar_toggle_multiple_contexts() {
"video"
);
let filter = {
category: "pictureinpicture",
method: "opened_method",
object: "urlBar",
};
await waitForTelemeryEvents(filter, PIP_URLBAR_EVENTS.length, "content");
TelemetryTestUtils.assertEvents(PIP_URLBAR_EVENTS, filter, {
clear: true,
process: "content",
});
let domWindowClosed = BrowserTestUtils.domWindowClosed(win);
pipUrlbarToggle.click();
await domWindowClosed;

View File

@ -1033,3 +1033,68 @@ function overrideSavedPosition(left, top, width, height) {
xulStore.setValue(PLAYER_URI, "picture-in-picture", "width", width);
xulStore.setValue(PLAYER_URI, "picture-in-picture", "height", height);
}
/**
* Function used to filter events when waiting for the correct number
* telemetry events.
* @param {String} expected The expected string or undefined
* @param {String} actual The actual string
* @returns true if the expected is undefined or if expected matches actual
*/
function matches(expected, actual) {
if (expected === undefined) {
return true;
}
return expected === actual;
}
/**
* Function that waits for the expected number of events aftering filtering.
* @param {Object} filter An object containing optional filters
* {
* category: (optional) The category of the event. Ex. "pictureinpicture"
* method: (optional) The method of the event. Ex. "create"
* object: (optional) The object of the event. Ex. "player"
* }
* @param {Number} length The number of events to wait for
* @param {String} process Should be "content" or "parent" depending on the event
*/
async function waitForTelemeryEvents(filter, length, process) {
let {
category: filterCategory,
method: filterMethod,
object: filterObject,
} = filter;
let events = [];
await TestUtils.waitForCondition(
() => {
events = Services.telemetry.snapshotEvents(
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
false
)[process];
if (!events) {
return false;
}
let filtered = events
.map(([, /* timestamp */ category, method, object, value, extra]) => {
// We don't care about the `timestamp` value.
// Tests that examine that value should use `snapshotEvents` directly.
return [category, method, object, value, extra];
})
.filter(([category, method, object]) => {
return (
matches(filterCategory, category) &&
matches(filterMethod, method) &&
matches(filterObject, object)
);
});
info(JSON.stringify(filtered, null, 2));
return filtered && filtered.length >= length;
},
"Waiting for one create pictureinpicture telemetry event.",
200,
100
);
}

View File

@ -2891,11 +2891,10 @@ pictureinpicture:
expiry_version: "never"
release_channel_collection: opt-out
opened_method:
objects: ["method"]
objects: ["toggle", "contextMenu", "urlBar"]
description: >
Records the method for opening the Picture-in-Picture window.
extra_keys:
method: Will be "toggle" or "contextMenu"
firstTimeToggle: If the user has used Picture-in-Picture before
notification_emails:
- mconley@mozilla.com
@ -2907,11 +2906,9 @@ pictureinpicture:
expiry_version: "never"
release_channel_collection: opt-out
closed_method:
objects: ["method"]
objects: ["closeButton", "unpip", "pagehide", "fullscreen", "setupFailure", "closePlayerShortcut", "contextMenu", "videoElRemove", "videoElEmptied", "urlBar"]
description: >
Records the method for closing the Picture-in-Picture window.
extra_keys:
reason: Will be "close-button", "unpip", "pagehide", "fullscreen", "setup-failure", "close-player-shortcut", "context-menu", "video-el-remove", or "video-el-emptied"
notification_emails:
- mconley@mozilla.com
- mtigley@mozilla.com
@ -2941,10 +2938,102 @@ pictureinpicture:
- 1772546
expiry_version: "never"
release_channel_collection: opt-out
backward:
objects: ["player"]
description: >
Recorded when the user clicks the seek backward button
notification_emails:
- mconley@mozilla.com
- mhowell@mozilla.com
products:
- "firefox"
record_in_processes:
- "main"
bug_numbers:
- 1822395
expiry_version: "116"
release_channel_collection: opt-out
backward_sequence:
objects: ["player"]
description: >
Recorded when the user clicks the seek backward button 3 times in a 2 second interval
notification_emails:
- mconley@mozilla.com
- mhowell@mozilla.com
products:
- "firefox"
record_in_processes:
- "main"
bug_numbers:
- 1822395
expiry_version: "116"
release_channel_collection: opt-out
forward:
objects: ["player"]
description: >
Recorded when the user clicks the seek forward button
notification_emails:
- mconley@mozilla.com
- mhowell@mozilla.com
products:
- "firefox"
record_in_processes:
- "main"
bug_numbers:
- 1822395
expiry_version: "116"
release_channel_collection: opt-out
forward_sequence:
objects: ["player"]
description: >
Recorded when the user clicks the seek forward button 3 times in a 2 second interval
notification_emails:
- mconley@mozilla.com
- mhowell@mozilla.com
products:
- "firefox"
record_in_processes:
- "main"
bug_numbers:
- 1822395
expiry_version: "116"
release_channel_collection: opt-out
seek:
objects: ["player"]
description: >
Recorded when the user interacts with the seek bar
notification_emails:
- mconley@mozilla.com
- mhowell@mozilla.com
products:
- "firefox"
record_in_processes:
- "main"
bug_numbers:
- 1822395
expiry_version: "116"
release_channel_collection: opt-out
fullscreen:
objects: ["player"]
extra_keys:
enter: true if entering fullscreen, false if exiting fullscreen
description: >
Recorded when the user clicks the fullscreen button
notification_emails:
- mconley@mozilla.com
- mhowell@mozilla.com
products:
- "firefox"
record_in_processes:
- "main"
bug_numbers:
- 1822395
expiry_version: "never"
release_channel_collection: opt-out
pictureinpicture.settings:
enable:
objects: ["player"]
objects: ["settings"]
description: Recorded when Picture-in-Picture is enabled.
notification_emails:
- mconley@mozilla.com
@ -2957,8 +3046,10 @@ pictureinpicture.settings:
expiry_version: "never"
release_channel_collection: opt-out
disable:
objects: ["player"]
description: Recorded when Picture-in-Picture is disabled.
objects: ["player", "settings"]
description: >
Recorded with "settings" object when Picture-in-Picture is disabled via settings
Recorded with "player" object when Picture-in-Picture is disabled via PiP context menu
notification_emails:
- mconley@mozilla.com
products: