summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry/tests/integration
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/telemetry/tests/integration
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/telemetry/tests/integration')
-rw-r--r--toolkit/components/telemetry/tests/integration/tests/conftest.py321
-rw-r--r--toolkit/components/telemetry/tests/integration/tests/python.toml12
-rw-r--r--toolkit/components/telemetry/tests/integration/tests/resources/helloworld/helloworld.html18
-rw-r--r--toolkit/components/telemetry/tests/integration/tests/resources/helloworld/manifest.json12
-rw-r--r--toolkit/components/telemetry/tests/integration/tests/test_deletion_request_ping.py64
-rw-r--r--toolkit/components/telemetry/tests/integration/tests/test_event_ping.py50
-rw-r--r--toolkit/components/telemetry/tests/integration/tests/test_main_tab_scalars.py33
-rw-r--r--toolkit/components/telemetry/tests/integration/tests/test_search_counts_across_sessions.py169
-rw-r--r--toolkit/components/telemetry/tests/integration/tests/test_subsession_management.py147
9 files changed, 826 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/tests/integration/tests/conftest.py b/toolkit/components/telemetry/tests/integration/tests/conftest.py
new file mode 100644
index 0000000000..e9cbdeff08
--- /dev/null
+++ b/toolkit/components/telemetry/tests/integration/tests/conftest.py
@@ -0,0 +1,321 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# 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/.
+import contextlib
+import os
+import re
+import sys
+import textwrap
+import time
+
+import mozinstall
+import pytest
+from marionette_driver import By, keys
+from marionette_driver.addons import Addons
+from marionette_driver.errors import MarionetteException
+from marionette_driver.marionette import Marionette
+from marionette_driver.wait import Wait
+from six import reraise
+from telemetry_harness.ping_server import PingServer
+
+CANARY_CLIENT_ID = "c0ffeec0-ffee-c0ff-eec0-ffeec0ffeec0"
+SERVER_ROOT = "toolkit/components/telemetry/tests/marionette/harness/www"
+UUID_PATTERN = re.compile(
+ r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
+)
+
+here = os.path.abspath(os.path.dirname(__file__))
+
+"""Get a build object we need to find a Firefox binary"""
+try:
+ from mozbuild.base import MozbuildObject
+
+ build = MozbuildObject.from_environment(cwd=here)
+except ImportError:
+ build = None
+
+
+@pytest.fixture(name="binary")
+def fixture_binary():
+ """Return a Firefox binary"""
+ try:
+ return build.get_binary_path()
+ except Exception:
+ print(str(Exception))
+
+ app = "firefox"
+ bindir = os.path.join(os.environ["PYTHON_TEST_TMP"], app)
+ if os.path.isdir(bindir):
+ try:
+ return mozinstall.get_binary(bindir, app_name=app)
+ except Exception:
+ print(str(Exception))
+
+ if "GECKO_BINARY_PATH" in os.environ:
+ return os.environ["GECKO_BINARY_PATH"]
+
+
+@pytest.fixture(name="marionette")
+def fixture_marionette(binary, ping_server):
+ """Start a marionette session with specific browser prefs"""
+ server_url = "{url}pings".format(url=ping_server.get_url("/"))
+ prefs = {
+ # Clear the region detection url to
+ # * avoid net access in tests
+ # * stabilize browser.search.region to avoid an extra subsession (bug 1579840#c40)
+ "browser.region.network.url": "",
+ # Disable smart sizing because it changes prefs at startup. (bug 1547750)
+ "browser.cache.disk.smart_size.enabled": False,
+ "toolkit.telemetry.server": server_url,
+ "telemetry.fog.test.localhost_port": -1,
+ "toolkit.telemetry.initDelay": 1,
+ "toolkit.telemetry.minSubsessionLength": 0,
+ "datareporting.healthreport.uploadEnabled": True,
+ "datareporting.policy.dataSubmissionEnabled": True,
+ "datareporting.policy.dataSubmissionPolicyBypassNotification": True,
+ "toolkit.telemetry.log.level": "Trace",
+ "toolkit.telemetry.log.dump": True,
+ "toolkit.telemetry.send.overrideOfficialCheck": True,
+ "toolkit.telemetry.testing.disableFuzzingDelay": True,
+ }
+ yield Marionette(host="localhost", port=0, bin=binary, prefs=prefs)
+
+
+@pytest.fixture(name="ping_server")
+def fixture_ping_server():
+ """Run a ping server on localhost on a free port assigned by the OS"""
+ server = PingServer(SERVER_ROOT, "http://localhost:0")
+ server.start()
+ yield server
+ server.stop()
+
+
+class Browser(object):
+ def __init__(self, marionette, ping_server):
+ self.marionette = marionette
+ self.ping_server = ping_server
+ self.addon_ids = []
+
+ def disable_telemetry(self):
+ self.marionette.instance.profile.set_persistent_preferences(
+ {"datareporting.healthreport.uploadEnabled": False}
+ )
+ self.marionette.set_pref("datareporting.healthreport.uploadEnabled", False)
+
+ def enable_search_events(self):
+ """
+ Event Telemetry categories are disabled by default.
+ Search events are in the "navigation" category and are not enabled by
+ default in builds of Firefox, so we enable them here.
+ """
+
+ script = """\
+ Services.telemetry.setEventRecordingEnabled("navigation", true);
+ """
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ self.marionette.execute_script(textwrap.dedent(script))
+
+ def enable_telemetry(self):
+ self.marionette.instance.profile.set_persistent_preferences(
+ {"datareporting.healthreport.uploadEnabled": True}
+ )
+ self.marionette.set_pref("datareporting.healthreport.uploadEnabled", True)
+
+ def get_client_id(self):
+ """Return the ID of the current client."""
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ return self.marionette.execute_script(
+ """\
+ const { ClientID } = ChromeUtils.import(
+ "resource://gre/modules/ClientID.jsm"
+ );
+ return ClientID.getCachedClientID();
+ """
+ )
+
+ def get_default_search_engine(self):
+ """Retrieve the identifier of the default search engine.
+
+ We found that it's required to initialize the search service before
+ attempting to retrieve the default search engine. Not calling init
+ would result in a JavaScript error (see bug 1543960 for more
+ information).
+ """
+
+ script = """\
+ let [resolve] = arguments;
+ let searchService = Components.classes[
+ "@mozilla.org/browser/search-service;1"]
+ .getService(Components.interfaces.nsISearchService);
+ return searchService.init().then(function () {
+ resolve(searchService.defaultEngine.identifier);
+ });
+ """
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ return self.marionette.execute_async_script(textwrap.dedent(script))
+
+ def install_addon(self):
+ resources_dir = os.path.join(os.path.dirname(__file__), "resources")
+ addon_path = os.path.abspath(os.path.join(resources_dir, "helloworld"))
+
+ try:
+ # Ensure the Environment has init'd so the installed addon
+ # triggers an "environment-change" ping.
+ script = """\
+ let [resolve] = arguments;
+ const { TelemetryEnvironment } = ChromeUtils.import(
+ "resource://gre/modules/TelemetryEnvironment.jsm"
+ );
+ TelemetryEnvironment.onInitialized().then(resolve);
+ """
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ self.marionette.execute_async_script(textwrap.dedent(script))
+
+ addons = Addons(self.marionette)
+ addon_id = addons.install(addon_path, temp=True)
+ except MarionetteException as e:
+ pytest.fail("{} - Error installing addon: {} - ".format(e.cause, e))
+ else:
+ self.addon_ids.append(addon_id)
+
+ @contextlib.contextmanager
+ def new_tab(self):
+ """Perform operations in a new tab and then close the new tab."""
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ start_tab = self.marionette.current_window_handle
+ new_tab = self.open_tab(focus=True)
+ self.marionette.switch_to_window(new_tab)
+
+ yield
+
+ self.marionette.close()
+ self.marionette.switch_to_window(start_tab)
+
+ def open_tab(self, focus=False):
+ current_tabs = self.marionette.window_handles
+
+ try:
+ result = self.marionette.open(type="tab", focus=focus)
+ if result["type"] != "tab":
+ raise Exception(
+ "Newly opened browsing context is of type {} and not tab.".format(
+ result["type"]
+ )
+ )
+ except Exception:
+ exc_type, exc_value, exc_traceback = sys.exc_info()
+ reraise(
+ exc_type,
+ exc_type("Failed to trigger opening a new tab: {}".format(exc_value)),
+ exc_traceback,
+ )
+ else:
+ Wait(self.marionette).until(
+ lambda mn: len(mn.window_handles) == len(current_tabs) + 1,
+ message="No new tab has been opened",
+ )
+
+ [new_tab] = list(set(self.marionette.window_handles) - set(current_tabs))
+
+ return new_tab
+
+ def quit(self, in_app=True):
+ self.marionette.quit(in_app=in_app)
+
+ def restart(self):
+ self.marionette.restart(clean=False, in_app=True)
+
+ def search(self, text):
+ """Perform a search via the browser's URL bar."""
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ self.marionette.execute_script("gURLBar.select();")
+ urlbar = self.marionette.find_element(By.ID, "urlbar-input")
+ urlbar.send_keys(keys.Keys.DELETE)
+ urlbar.send_keys(text + keys.Keys.ENTER)
+
+ # Wait for 0.1 seconds before proceeding to decrease the chance
+ # of Firefox being shut down before Telemetry is recorded
+ time.sleep(0.1)
+
+ def search_in_new_tab(self, text):
+ """Open a new tab and perform a search via the browser's URL bar,
+ then close the new tab."""
+ with self.new_tab():
+ self.search(text)
+
+ def start_session(self):
+ self.marionette.start_session()
+
+ def wait_for_search_service_init(self):
+ script = """\
+ let [resolve] = arguments;
+ let searchService = Components.classes["@mozilla.org/browser/search-service;1"]
+ .getService(Components.interfaces.nsISearchService);
+ searchService.init().then(resolve);
+ """
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ self.marionette.execute_async_script(textwrap.dedent(script))
+
+
+@pytest.fixture(name="browser")
+def fixture_browser(marionette, ping_server):
+ """Return an instance of our Browser object"""
+ browser = Browser(marionette, ping_server)
+ browser.start_session()
+ yield browser
+ browser.quit()
+
+
+class Helpers(object):
+ def __init__(self, ping_server, marionette):
+ self.ping_server = ping_server
+ self.marionette = marionette
+
+ def assert_is_valid_uuid(self, value):
+ """Custom assertion for UUID's"""
+ assert value is not None
+ assert value != ""
+ assert value != CANARY_CLIENT_ID
+ assert re.match(UUID_PATTERN, value) is not None
+
+ def wait_for_ping(self, action_func, ping_filter):
+ [ping] = self.wait_for_pings(action_func, ping_filter, 1)
+ return ping
+
+ def wait_for_pings(self, action_func, ping_filter, count):
+ """Call the given action and wait for pings to come in and return
+ the `count` number of pings, that match the given filter."""
+ # Keep track of the current number of pings
+ current_num_pings = len(self.ping_server.pings)
+
+ # New list to store new pings that satisfy the filter
+ filtered_pings = []
+
+ def wait_func(*args, **kwargs):
+ # Ignore existing pings in self.ping_server.pings
+ new_pings = self.ping_server.pings[current_num_pings:]
+
+ # Filter pings to make sure we wait for the correct ping type
+ filtered_pings[:] = [p for p in new_pings if ping_filter(p)]
+
+ return len(filtered_pings) >= count
+
+ action_func()
+
+ try:
+ Wait(self.marionette, 60).until(wait_func)
+ except Exception as e:
+ pytest.fail("Error waiting for ping: {}".format(e))
+
+ return filtered_pings[:count]
+
+
+@pytest.fixture(name="helpers")
+def fixture_helpers(ping_server, marionette):
+ """Return an instace of our helpers object"""
+ return Helpers(ping_server, marionette)
diff --git a/toolkit/components/telemetry/tests/integration/tests/python.toml b/toolkit/components/telemetry/tests/integration/tests/python.toml
new file mode 100644
index 0000000000..e9ad98226c
--- /dev/null
+++ b/toolkit/components/telemetry/tests/integration/tests/python.toml
@@ -0,0 +1,12 @@
+[DEFAULT]
+subsuite = "telemetry-integration-tests"
+
+["test_deletion_request_ping.py"]
+
+["test_event_ping.py"]
+
+["test_main_tab_scalars.py"]
+
+["test_search_counts_across_sessions.py"]
+
+["test_subsession_management.py"]
diff --git a/toolkit/components/telemetry/tests/integration/tests/resources/helloworld/helloworld.html b/toolkit/components/telemetry/tests/integration/tests/resources/helloworld/helloworld.html
new file mode 100644
index 0000000000..146ad025d9
--- /dev/null
+++ b/toolkit/components/telemetry/tests/integration/tests/resources/helloworld/helloworld.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ body {
+ background-color: lightgrey;
+ }
+ p {
+ font-size: 25px;
+ padding: 25px 50px;
+ }
+ </style>
+ </head>
+ <body>
+ <p>Hello World!</p>
+ </body>
+</html>
diff --git a/toolkit/components/telemetry/tests/integration/tests/resources/helloworld/manifest.json b/toolkit/components/telemetry/tests/integration/tests/resources/helloworld/manifest.json
new file mode 100644
index 0000000000..14ab99fa1c
--- /dev/null
+++ b/toolkit/components/telemetry/tests/integration/tests/resources/helloworld/manifest.json
@@ -0,0 +1,12 @@
+{
+ "description": "Extension to be installed in Telemetry client integration tests.",
+ "manifest_version": 2,
+ "name": "helloworld",
+ "version": "1.0",
+ "homepage_url": "https://hg.mozilla.org/mozilla-central/",
+ "browser_action": {
+ "browser_style": true,
+ "default_title": "Hello World",
+ "default_popup": "helloworld.html"
+ }
+}
diff --git a/toolkit/components/telemetry/tests/integration/tests/test_deletion_request_ping.py b/toolkit/components/telemetry/tests/integration/tests/test_deletion_request_ping.py
new file mode 100644
index 0000000000..2eb74efe38
--- /dev/null
+++ b/toolkit/components/telemetry/tests/integration/tests/test_deletion_request_ping.py
@@ -0,0 +1,64 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# 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/.
+import mozunit
+from telemetry_harness.ping_filters import (
+ ANY_PING,
+ DELETION_REQUEST_PING,
+ MAIN_SHUTDOWN_PING,
+)
+
+
+def test_deletion_request_ping(browser, helpers):
+ """Test the "deletion-request" ping behaviour across sessions"""
+ # Get the client_id after installing an addon
+ client_id = helpers.wait_for_ping(browser.install_addon, ANY_PING)["clientId"]
+
+ # Make sure it's a valid UUID
+ helpers.assert_is_valid_uuid(client_id)
+
+ # Trigger a "deletion-request" ping.
+ ping = helpers.wait_for_ping(browser.disable_telemetry, DELETION_REQUEST_PING)
+
+ assert "clientId" in ping
+ assert "payload" in ping
+ assert "environment" not in ping["payload"]
+
+ # Close Firefox cleanly.
+ browser.quit()
+
+ # Start Firefox.
+ browser.start_session()
+
+ # Trigger an environment change, which isn't allowed to send a ping.
+ browser.install_addon()
+
+ # Ensure we've sent no pings since "optout".
+ assert browser.ping_server.pings[-1] == ping
+
+ # Turn Telemetry back on.
+ browser.enable_telemetry()
+
+ # Close Firefox cleanly, collecting its "main"/"shutdown" ping.
+ main_ping = helpers.wait_for_ping(browser.restart, MAIN_SHUTDOWN_PING)
+
+ # Ensure the "main" ping has changed its client id.
+ assert "clientId" in main_ping
+ new_client_id = main_ping["clientId"]
+ helpers.assert_is_valid_uuid(new_client_id)
+ assert new_client_id != client_id
+
+ # Ensure we note in the ping that the user opted in.
+ parent_scalars = main_ping["payload"]["processes"]["parent"]["scalars"]
+
+ assert "telemetry.data_upload_optin" in parent_scalars
+ assert parent_scalars["telemetry.data_upload_optin"] is True
+
+ # Ensure all pings sent during this test don't have the c0ffee client id.
+ for ping in browser.ping_server.pings:
+ if "clientId" in ping:
+ helpers.assert_is_valid_uuid(ping["clientId"])
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/telemetry/tests/integration/tests/test_event_ping.py b/toolkit/components/telemetry/tests/integration/tests/test_event_ping.py
new file mode 100644
index 0000000000..9209c562eb
--- /dev/null
+++ b/toolkit/components/telemetry/tests/integration/tests/test_event_ping.py
@@ -0,0 +1,50 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# 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/.
+import mozunit
+from telemetry_harness.ping_filters import EVENT_PING
+
+
+def test_event_ping(browser, helpers):
+ """
+ Barebones test for "event" ping:
+ Search, close Firefox, check "event" ping for search events.
+ """
+ browser.enable_search_events()
+ browser.wait_for_search_service_init()
+ browser.search("mozilla firefox")
+
+ payload = helpers.wait_for_ping(browser.restart, EVENT_PING)["payload"]
+
+ assert "shutdown" == payload["reason"]
+ assert 0 == payload["lostEventsCount"]
+ assert "events" in payload
+ assert "parent" in payload["events"]
+ assert find_event(payload["events"]["parent"])
+
+
+def find_event(events):
+ """Return the first event that has the expected timestamp, category method and object"""
+
+ for event in events:
+ # The event may optionally contain additonal fields
+ [timestamp, category, method, object_id] = event[:4]
+
+ assert timestamp > 0
+
+ if category != "navigation":
+ continue
+
+ if method != "search":
+ continue
+
+ if object_id != "urlbar":
+ continue
+
+ return True
+
+ return False
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/telemetry/tests/integration/tests/test_main_tab_scalars.py b/toolkit/components/telemetry/tests/integration/tests/test_main_tab_scalars.py
new file mode 100644
index 0000000000..8a7cdd77a4
--- /dev/null
+++ b/toolkit/components/telemetry/tests/integration/tests/test_main_tab_scalars.py
@@ -0,0 +1,33 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# 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/.
+import mozunit
+from telemetry_harness.ping_filters import MAIN_SHUTDOWN_PING
+
+
+def test_main_tab_scalars(browser, helpers):
+ with browser.marionette.using_context(browser.marionette.CONTEXT_CHROME):
+ start_tab = browser.marionette.current_window_handle
+ tab2 = browser.open_tab(focus=True)
+ browser.marionette.switch_to_window(tab2)
+ tab3 = browser.open_tab(focus=True)
+ browser.marionette.switch_to_window(tab3)
+ browser.marionette.close()
+ browser.marionette.switch_to_window(tab2)
+ browser.marionette.close()
+ browser.marionette.switch_to_window(start_tab)
+
+ ping = helpers.wait_for_ping(browser.restart, MAIN_SHUTDOWN_PING)
+
+ assert "main" == ping["type"]
+ assert browser.get_client_id() == ping["clientId"]
+
+ scalars = ping["payload"]["processes"]["parent"]["scalars"]
+
+ assert 3 == scalars["browser.engagement.max_concurrent_tab_count"]
+ assert 2 == scalars["browser.engagement.tab_open_event_count"]
+ assert 1 == scalars["browser.engagement.max_concurrent_window_count"]
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/telemetry/tests/integration/tests/test_search_counts_across_sessions.py b/toolkit/components/telemetry/tests/integration/tests/test_search_counts_across_sessions.py
new file mode 100644
index 0000000000..34afd305ad
--- /dev/null
+++ b/toolkit/components/telemetry/tests/integration/tests/test_search_counts_across_sessions.py
@@ -0,0 +1,169 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# 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/.
+import mozunit
+from telemetry_harness.ping_filters import (
+ MAIN_ENVIRONMENT_CHANGE_PING,
+ MAIN_SHUTDOWN_PING,
+)
+
+
+def test_search_counts(browser, helpers):
+ """Test for SEARCH_COUNTS across sessions."""
+
+ # Session S1, subsession 1:
+ # - Open browser
+ # - Open new tab
+ # - Perform search (awesome bar or search bar)
+ # - Restart browser in new session
+ search_engine = browser.get_default_search_engine()
+ browser.search_in_new_tab("mozilla firefox")
+ ping1 = helpers.wait_for_ping(browser.restart, MAIN_SHUTDOWN_PING)
+
+ # Session S2, subsession 1:
+ # - Outcome 1
+ # - Received a main ping P1 for previous session
+ # - Ping base contents:
+ # - clientId should be set
+ # - sessionId should be set
+ # - subsessionId should be set
+ # - previousSessionId should not be set
+ # - previousSubsessionId should not be set
+ # - subSessionCounter should be 1
+ # - profileSubSessionCounter should be 1
+ # - reason should be "shutdown"
+ # - Other ping contents:
+ # - SEARCH_COUNTS values should match performed search action
+
+ client_id = ping1["clientId"]
+ helpers.assert_is_valid_uuid(client_id)
+
+ ping1_info = ping1["payload"]["info"]
+ assert "shutdown" == ping1_info["reason"]
+
+ s1_session_id = ping1_info["sessionId"]
+ assert s1_session_id != ""
+
+ s1_s1_subsession_id = ping1_info["subsessionId"]
+ assert s1_s1_subsession_id != ""
+
+ assert ping1_info["previousSessionId"] is None
+ assert ping1_info["previousSubsessionId"] is None
+ assert ping1_info["subsessionCounter"] == 1
+ assert ping1_info["profileSubsessionCounter"] == 1
+
+ scalars1 = ping1["payload"]["processes"]["parent"]["scalars"]
+ assert "browser.engagement.window_open_event_count" not in scalars1
+ assert scalars1["browser.engagement.tab_open_event_count"] == 1
+
+ keyed_histograms1 = ping1["payload"]["keyedHistograms"]
+ search_counts1 = keyed_histograms1["SEARCH_COUNTS"][
+ "{}.urlbar".format(search_engine)
+ ]
+
+ assert search_counts1 == {
+ "range": [1, 2],
+ "bucket_count": 3,
+ "histogram_type": 4,
+ "values": {"1": 0, "0": 1},
+ "sum": 1,
+ }
+
+ # - Install addon
+ # Session S2, subsession 2:
+ # - Outcome 2
+ # - Received a main ping P2 for previous subsession
+ # - Ping base contents:
+ # - clientId should be set to the same value
+ # - sessionId should be set to a new value
+ # - subsessionId should be set to a new value
+ # - previousSessionId should be set to P1s sessionId value
+ # - previousSubsessionId should be set to P1s subsessionId value
+ # - subSessionCounter should be 1
+ # - profileSubSessionCounter should be 2
+ # - reason should be "environment-change"
+ # - Other ping contents:
+ # - SEARCH_COUNTS values should not be in P2
+ # - Verify that there should be no listing for tab scalar as we started a new
+ # session
+
+ ping2 = helpers.wait_for_ping(browser.install_addon, MAIN_ENVIRONMENT_CHANGE_PING)
+
+ assert client_id == ping2["clientId"]
+
+ ping2_info = ping2["payload"]["info"]
+ assert ping2_info["reason"] == "environment-change"
+
+ s2_session_id = ping2_info["sessionId"]
+ assert s2_session_id != s1_session_id
+
+ s2_s1_subsession_id = ping2_info["subsessionId"]
+ assert s2_s1_subsession_id != s1_s1_subsession_id
+
+ assert ping2_info["previousSessionId"] == s1_session_id
+ assert ping2_info["previousSubsessionId"] == s1_s1_subsession_id
+ assert ping2_info["subsessionCounter"] == 1
+ assert ping2_info["profileSubsessionCounter"] == 2
+
+ scalars2 = ping2["payload"]["processes"]["parent"]["scalars"]
+ assert "browser.engagement.window_open_event_count" not in scalars2
+ assert "browser.engagement.tab_open_event_count" not in scalars2
+
+ keyed_histograms2 = ping2["payload"]["keyedHistograms"]
+ assert "SEARCH_COUNTS" not in keyed_histograms2
+
+ # - Perform Search
+ # - Restart Browser
+
+ browser.search("mozilla telemetry")
+ browser.search("python unittest")
+ browser.search("python pytest")
+
+ ping3 = helpers.wait_for_ping(browser.restart, MAIN_SHUTDOWN_PING)
+
+ # Session S3, subsession 1:
+ # - Outcome 3
+ # - Received a main ping P3 for session 2, subsession 1
+ # - Ping base contents:
+ # - clientId should be set to the same value
+ # - sessionId should be set to P2s sessionId value
+ # - subsessionId should be set to a new value
+ # - previousSessionId should be set to P1s sessionId value
+ # - previousSubsessionId should be set to P2s subsessionId value
+ # - subSessionCounter should be 2
+ # - profileSubSessionCounter should be 3
+ # - reason should be "shutdown"
+ # - Other ping contents:
+ # - SEARCH_COUNTS values should be set per above search
+
+ assert client_id == ping3["clientId"]
+
+ ping3_info = ping3["payload"]["info"]
+ assert ping3_info["reason"] == "shutdown"
+ assert ping3_info["sessionId"] == s2_session_id
+
+ s2_s2_subsession_id = ping3_info["subsessionId"]
+ assert s2_s2_subsession_id != s1_s1_subsession_id
+ assert ping3_info["previousSessionId"] == s1_session_id
+ assert ping3_info["previousSubsessionId"] == s2_s1_subsession_id
+ assert ping3_info["subsessionCounter"] == 2
+ assert ping3_info["profileSubsessionCounter"] == 3
+
+ scalars3 = ping3["payload"]["processes"]["parent"]["scalars"]
+ assert "browser.engagement.window_open_event_count" not in scalars3
+
+ keyed_histograms3 = ping3["payload"]["keyedHistograms"]
+ search_counts3 = keyed_histograms3["SEARCH_COUNTS"][
+ "{}.urlbar".format(search_engine)
+ ]
+ assert search_counts3 == {
+ "range": [1, 2],
+ "bucket_count": 3,
+ "histogram_type": 4,
+ "values": {"1": 0, "0": 3},
+ "sum": 3,
+ }
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/telemetry/tests/integration/tests/test_subsession_management.py b/toolkit/components/telemetry/tests/integration/tests/test_subsession_management.py
new file mode 100644
index 0000000000..b8697ed98b
--- /dev/null
+++ b/toolkit/components/telemetry/tests/integration/tests/test_subsession_management.py
@@ -0,0 +1,147 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# 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/.
+import mozunit
+from telemetry_harness.ping_filters import (
+ MAIN_ENVIRONMENT_CHANGE_PING,
+ MAIN_SHUTDOWN_PING,
+)
+
+
+def test_subsession_management(browser, helpers):
+ """Test for Firefox Telemetry subsession management."""
+
+ # Session S1, subsession 1
+ # Actions:
+ # 1. Open browser
+ # 2. Open a new tab
+ # 3. Restart browser in new session
+
+ with browser.new_tab():
+ # If Firefox Telemetry is working correctly, this will
+ # be sufficient to record a tab open event.
+ pass
+
+ ping1 = helpers.wait_for_ping(browser.restart, MAIN_SHUTDOWN_PING)
+
+ # Session S2, subsession 1
+ # Outcome 1:
+ # Received a main ping P1 for previous session
+ # - Ping base contents:
+ # - clientId should be a valid UUID
+ # - reason should be "shutdown"
+ # - sessionId should be set
+ # - subsessionId should be set
+ # - previousSessionId should not be set
+ # - previousSubsessionId should not be set
+ # - subSessionCounter should be 1
+ # - profileSubSessionCounter should be 1
+ # - Other ping contents:
+ # - tab_open_event_count in scalars
+
+ client_id = ping1["clientId"]
+ helpers.assert_is_valid_uuid(client_id)
+
+ ping1_info = ping1["payload"]["info"]
+ assert ping1_info["reason"] == "shutdown"
+
+ s1_session_id = ping1_info["sessionId"]
+ assert s1_session_id != ""
+
+ s1_s1_subsession_id = ping1_info["subsessionId"]
+ assert s1_s1_subsession_id != ""
+ assert ping1_info["previousSessionId"] is None
+ assert ping1_info["previousSubsessionId"] is None
+ assert ping1_info["subsessionCounter"] == 1
+ assert ping1_info["profileSubsessionCounter"] == 1
+
+ scalars1 = ping1["payload"]["processes"]["parent"]["scalars"]
+ assert "browser.engagement.window_open_event_count" not in scalars1
+ assert scalars1["browser.engagement.tab_open_event_count"] == 1
+
+ # Actions:
+ # 1. Install addon
+
+ ping2 = helpers.wait_for_ping(browser.install_addon, MAIN_ENVIRONMENT_CHANGE_PING)
+
+ [addon_id] = browser.addon_ids # Store the addon ID for verifying ping3 later
+
+ # Session S2, subsession 2
+ # Outcome 2:
+ # Received a main ping P2 for previous subsession
+ # - Ping base contents:
+ # - clientId should be set to the same value
+ # - sessionId should be set to a new value
+ # - subsessionId should be set to a new value
+ # - previousSessionId should be set to P1s sessionId value
+ # - previousSubsessionId should be set to P1s subsessionId value
+ # - subSessionCounter should be 1
+ # - profileSubSessionCounter should be 2
+ # - reason should be "environment-change"
+ # - Other ping contents:
+ # - tab_open_event_count not in scalars
+
+ assert ping2["clientId"] == client_id
+
+ ping2_info = ping2["payload"]["info"]
+ assert ping2_info["reason"] == "environment-change"
+
+ s2_session_id = ping2_info["sessionId"]
+ assert s2_session_id != s1_session_id
+
+ s2_s1_subsession_id = ping2_info["subsessionId"]
+ assert s2_s1_subsession_id != s1_s1_subsession_id
+ assert ping2_info["previousSessionId"] == s1_session_id
+ assert ping2_info["previousSubsessionId"] == s1_s1_subsession_id
+ assert ping2_info["subsessionCounter"] == 1
+ assert ping2_info["profileSubsessionCounter"] == 2
+
+ scalars2 = ping2["payload"]["processes"]["parent"]["scalars"]
+ assert "browser.engagement.window_open_event_count" not in scalars2
+ assert "browser.engagement.tab_open_event_count" not in scalars2
+
+ # Actions
+ # 1. Restart browser in new session
+
+ ping3 = helpers.wait_for_ping(browser.restart, MAIN_SHUTDOWN_PING)
+
+ # Session S3, subsession 1
+ # Outcome 3:
+ # Received a main ping P3 for session 2, subsession 2
+ # - Ping base contents:
+ # - clientId should be set to the same value
+ # - sessionId should be set to P2s sessionId value
+ # - subsessionId should be set to a new value
+ # - previousSessionId should be set to P1s sessionId value
+ # - previousSubsessionId should be set to P2s subsessionId value
+ # - subSessionCounter should be 2
+ # - profileSubSessionCounter should be 3
+ # - reason should be "shutdown"
+ # - Other ping contents:
+ # - addon ID in activeAddons in environment
+
+ assert ping3["clientId"] == client_id
+
+ ping3_info = ping3["payload"]["info"]
+ assert ping3_info["reason"] == "shutdown"
+
+ assert ping3_info["sessionId"] == s2_session_id
+
+ s2_s2_subsession_id = ping3_info["subsessionId"]
+ assert s2_s2_subsession_id != s1_s1_subsession_id
+ assert s2_s2_subsession_id != s2_s1_subsession_id
+ assert ping3_info["previousSessionId"] == s1_session_id
+ assert ping3_info["previousSubsessionId"] == s2_s1_subsession_id
+ assert ping3_info["subsessionCounter"] == 2
+ assert ping3_info["profileSubsessionCounter"] == 3
+
+ scalars3 = ping3["payload"]["processes"]["parent"]["scalars"]
+ assert "browser.engagement.window_open_event_count" not in scalars3
+ assert "browser.engagement.tab_open_event_count" not in scalars3
+
+ active_addons = ping3["environment"]["addons"]["activeAddons"]
+ assert addon_id in active_addons
+
+
+if __name__ == "__main__":
+ mozunit.main()