summary refs log tree commit diff
path: root/packages/overlays/matrix-synapse/patches/0022-Add-a-unit-test-for-the-phone-home-stats-18463.patch
diff options
context:
space:
mode:
Diffstat (limited to 'packages/overlays/matrix-synapse/patches/0022-Add-a-unit-test-for-the-phone-home-stats-18463.patch')
-rw-r--r--packages/overlays/matrix-synapse/patches/0022-Add-a-unit-test-for-the-phone-home-stats-18463.patch384
1 files changed, 0 insertions, 384 deletions
diff --git a/packages/overlays/matrix-synapse/patches/0022-Add-a-unit-test-for-the-phone-home-stats-18463.patch b/packages/overlays/matrix-synapse/patches/0022-Add-a-unit-test-for-the-phone-home-stats-18463.patch
deleted file mode 100644

index b00e1c4..0000000 --- a/packages/overlays/matrix-synapse/patches/0022-Add-a-unit-test-for-the-phone-home-stats-18463.patch +++ /dev/null
@@ -1,384 +0,0 @@ -From 4b1d9d5d0e3df7a3151c07f9d42b02dad13a27bf Mon Sep 17 00:00:00 2001 -From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> -Date: Tue, 20 May 2025 16:26:45 +0100 -Subject: [PATCH 22/34] Add a unit test for the phone home stats (#18463) - ---- - changelog.d/18463.misc | 1 + - .../reporting_homeserver_usage_statistics.md | 6 +- - synapse/app/phone_stats_home.py | 33 ++- - tests/metrics/test_phone_home_stats.py | 263 ++++++++++++++++++ - 4 files changed, 296 insertions(+), 7 deletions(-) - create mode 100644 changelog.d/18463.misc - create mode 100644 tests/metrics/test_phone_home_stats.py - -diff --git a/changelog.d/18463.misc b/changelog.d/18463.misc -new file mode 100644 -index 0000000000..1264758d7c ---- /dev/null -+++ b/changelog.d/18463.misc -@@ -0,0 +1 @@ -+Add unit tests for homeserver usage statistics. -\ No newline at end of file -diff --git a/docs/usage/administration/monitoring/reporting_homeserver_usage_statistics.md b/docs/usage/administration/monitoring/reporting_homeserver_usage_statistics.md -index 4c0dbb5acd..a8a717e2a2 100644 ---- a/docs/usage/administration/monitoring/reporting_homeserver_usage_statistics.md -+++ b/docs/usage/administration/monitoring/reporting_homeserver_usage_statistics.md -@@ -30,7 +30,7 @@ The following statistics are sent to the configured reporting endpoint: - | `python_version` | string | The Python version number in use (e.g "3.7.1"). Taken from `sys.version_info`. | - | `total_users` | int | The number of registered users on the homeserver. | - | `total_nonbridged_users` | int | The number of users, excluding those created by an Application Service. | --| `daily_user_type_native` | int | The number of native users created in the last 24 hours. | -+| `daily_user_type_native` | int | The number of native, non-guest users created in the last 24 hours. | - | `daily_user_type_guest` | int | The number of guest users created in the last 24 hours. | - | `daily_user_type_bridged` | int | The number of users created by Application Services in the last 24 hours. | - | `total_room_count` | int | The total number of rooms present on the homeserver. | -@@ -50,8 +50,8 @@ The following statistics are sent to the configured reporting endpoint: - | `cache_factor` | int | The configured [`global factor`](../../configuration/config_documentation.md#caching) value for caching. | - | `event_cache_size` | int | The configured [`event_cache_size`](../../configuration/config_documentation.md#caching) value for caching. | - | `database_engine` | string | The database engine that is in use. Either "psycopg2" meaning PostgreSQL is in use, or "sqlite3" for SQLite3. | --| `database_server_version` | string | The version of the database server. Examples being "10.10" for PostgreSQL server version 10.0, and "3.38.5" for SQLite 3.38.5 installed on the system. | --| `log_level` | string | The log level in use. Examples are "INFO", "WARNING", "ERROR", "DEBUG", etc. | -+| `database_server_version` | string | The version of the database server. Examples being "10.10" for PostgreSQL server version 10.0, and "3.38.5" for SQLite 3.38.5 installed on the system. | -+| `log_level` | string | The log level in use. Examples are "INFO", "WARNING", "ERROR", "DEBUG", etc. | - - - [^1]: Native matrix users and guests are always counted. If the -diff --git a/synapse/app/phone_stats_home.py b/synapse/app/phone_stats_home.py -index f602bbbeea..bb450a235c 100644 ---- a/synapse/app/phone_stats_home.py -+++ b/synapse/app/phone_stats_home.py -@@ -34,6 +34,22 @@ if TYPE_CHECKING: - - logger = logging.getLogger("synapse.app.homeserver") - -+ONE_MINUTE_SECONDS = 60 -+ONE_HOUR_SECONDS = 60 * ONE_MINUTE_SECONDS -+ -+MILLISECONDS_PER_SECOND = 1000 -+ -+INITIAL_DELAY_BEFORE_FIRST_PHONE_HOME_SECONDS = 5 * ONE_MINUTE_SECONDS -+""" -+We wait 5 minutes to send the first set of stats as the server can be quite busy the -+first few minutes -+""" -+ -+PHONE_HOME_INTERVAL_SECONDS = 3 * ONE_HOUR_SECONDS -+""" -+Phone home stats are sent every 3 hours -+""" -+ - # Contains the list of processes we will be monitoring - # currently either 0 or 1 - _stats_process: List[Tuple[int, "resource.struct_rusage"]] = [] -@@ -185,12 +201,14 @@ def start_phone_stats_home(hs: "HomeServer") -> None: - # If you increase the loop period, the accuracy of user_daily_visits - # table will decrease - clock.looping_call( -- hs.get_datastores().main.generate_user_daily_visits, 5 * 60 * 1000 -+ hs.get_datastores().main.generate_user_daily_visits, -+ 5 * ONE_MINUTE_SECONDS * MILLISECONDS_PER_SECOND, - ) - - # monthly active user limiting functionality - clock.looping_call( -- hs.get_datastores().main.reap_monthly_active_users, 1000 * 60 * 60 -+ hs.get_datastores().main.reap_monthly_active_users, -+ ONE_HOUR_SECONDS * MILLISECONDS_PER_SECOND, - ) - hs.get_datastores().main.reap_monthly_active_users() - -@@ -221,7 +239,12 @@ def start_phone_stats_home(hs: "HomeServer") -> None: - - if hs.config.metrics.report_stats: - logger.info("Scheduling stats reporting for 3 hour intervals") -- clock.looping_call(phone_stats_home, 3 * 60 * 60 * 1000, hs, stats) -+ clock.looping_call( -+ phone_stats_home, -+ PHONE_HOME_INTERVAL_SECONDS * MILLISECONDS_PER_SECOND, -+ hs, -+ stats, -+ ) - - # We need to defer this init for the cases that we daemonize - # otherwise the process ID we get is that of the non-daemon process -@@ -229,4 +252,6 @@ def start_phone_stats_home(hs: "HomeServer") -> None: - - # We wait 5 minutes to send the first set of stats as the server can - # be quite busy the first few minutes -- clock.call_later(5 * 60, phone_stats_home, hs, stats) -+ clock.call_later( -+ INITIAL_DELAY_BEFORE_FIRST_PHONE_HOME_SECONDS, phone_stats_home, hs, stats -+ ) -diff --git a/tests/metrics/test_phone_home_stats.py b/tests/metrics/test_phone_home_stats.py -new file mode 100644 -index 0000000000..5339d649df ---- /dev/null -+++ b/tests/metrics/test_phone_home_stats.py -@@ -0,0 +1,263 @@ -+# -+# This file is licensed under the Affero General Public License (AGPL) version 3. -+# -+# Copyright (C) 2025 New Vector, Ltd -+# -+# This program is free software: you can redistribute it and/or modify -+# it under the terms of the GNU Affero General Public License as -+# published by the Free Software Foundation, either version 3 of the -+# License, or (at your option) any later version. -+# -+# See the GNU Affero General Public License for more details: -+# <https://www.gnu.org/licenses/agpl-3.0.html>. -+ -+import logging -+from unittest.mock import AsyncMock -+ -+from twisted.test.proto_helpers import MemoryReactor -+ -+from synapse.app.phone_stats_home import ( -+ PHONE_HOME_INTERVAL_SECONDS, -+ start_phone_stats_home, -+) -+from synapse.rest import admin, login, register, room -+from synapse.server import HomeServer -+from synapse.types import JsonDict -+from synapse.util import Clock -+ -+from tests import unittest -+from tests.server import ThreadedMemoryReactorClock -+ -+TEST_REPORT_STATS_ENDPOINT = "https://fake.endpoint/stats" -+TEST_SERVER_CONTEXT = "test-server-context" -+ -+ -+class PhoneHomeStatsTestCase(unittest.HomeserverTestCase): -+ servlets = [ -+ admin.register_servlets_for_client_rest_resource, -+ room.register_servlets, -+ register.register_servlets, -+ login.register_servlets, -+ ] -+ -+ def make_homeserver( -+ self, reactor: ThreadedMemoryReactorClock, clock: Clock -+ ) -> HomeServer: -+ # Configure the homeserver to enable stats reporting. -+ config = self.default_config() -+ config["report_stats"] = True -+ config["report_stats_endpoint"] = TEST_REPORT_STATS_ENDPOINT -+ -+ # Configure the server context so we can check it ends up being reported -+ config["server_context"] = TEST_SERVER_CONTEXT -+ -+ # Allow guests to be registered -+ config["allow_guest_access"] = True -+ -+ hs = self.setup_test_homeserver(config=config) -+ -+ # Replace the proxied http client with a mock, so we can inspect outbound requests to -+ # the configured stats endpoint. -+ self.put_json_mock = AsyncMock(return_value={}) -+ hs.get_proxied_http_client().put_json = self.put_json_mock # type: ignore[method-assign] -+ return hs -+ -+ def prepare( -+ self, reactor: MemoryReactor, clock: Clock, homeserver: HomeServer -+ ) -> None: -+ self.store = homeserver.get_datastores().main -+ -+ # Wait for the background updates to add the database triggers that keep the -+ # `event_stats` table up-to-date. -+ self.wait_for_background_updates() -+ -+ # Force stats reporting to occur -+ start_phone_stats_home(hs=homeserver) -+ -+ super().prepare(reactor, clock, homeserver) -+ -+ def _get_latest_phone_home_stats(self) -> JsonDict: -+ # Wait for `phone_stats_home` to be called again + a healthy margin (50s). -+ self.reactor.advance(2 * PHONE_HOME_INTERVAL_SECONDS + 50) -+ -+ # Extract the reported stats from our http client mock -+ mock_calls = self.put_json_mock.call_args_list -+ report_stats_calls = [] -+ for call in mock_calls: -+ if call.args[0] == TEST_REPORT_STATS_ENDPOINT: -+ report_stats_calls.append(call) -+ -+ self.assertGreaterEqual( -+ (len(report_stats_calls)), -+ 1, -+ "Expected at-least one call to the report_stats endpoint", -+ ) -+ -+ # Extract the phone home stats from the call -+ phone_home_stats = report_stats_calls[0].args[1] -+ -+ return phone_home_stats -+ -+ def _perform_user_actions(self) -> None: -+ """ -+ Perform some actions on the homeserver that would bump the phone home -+ stats. -+ -+ This creates a few users (including a guest), a room, and sends some messages. -+ Expected number of events: -+ - 10 unencrypted messages -+ - 5 encrypted messages -+ - 24 total events (including room state, etc) -+ """ -+ -+ # Create some users -+ user_1_mxid = self.register_user( -+ username="test_user_1", -+ password="test", -+ ) -+ user_2_mxid = self.register_user( -+ username="test_user_2", -+ password="test", -+ ) -+ # Note: `self.register_user` does not support guest registration, and updating the -+ # Admin API it calls to add a new parameter would cause the `mac` parameter to fail -+ # in a backwards-incompatible manner. Hence, we make a manual request here. -+ _guest_user_mxid = self.make_request( -+ method="POST", -+ path="/_matrix/client/v3/register?kind=guest", -+ content={ -+ "username": "guest_user", -+ "password": "test", -+ }, -+ shorthand=False, -+ ) -+ -+ # Log in to each user -+ user_1_token = self.login(username=user_1_mxid, password="test") -+ user_2_token = self.login(username=user_2_mxid, password="test") -+ -+ # Create a room between the two users -+ room_1_id = self.helper.create_room_as( -+ is_public=False, -+ tok=user_1_token, -+ ) -+ -+ # Mark this room as end-to-end encrypted -+ self.helper.send_state( -+ room_id=room_1_id, -+ event_type="m.room.encryption", -+ body={ -+ "algorithm": "m.megolm.v1.aes-sha2", -+ "rotation_period_ms": 604800000, -+ "rotation_period_msgs": 100, -+ }, -+ state_key="", -+ tok=user_1_token, -+ ) -+ -+ # User 1 invites user 2 -+ self.helper.invite( -+ room=room_1_id, -+ src=user_1_mxid, -+ targ=user_2_mxid, -+ tok=user_1_token, -+ ) -+ -+ # User 2 joins -+ self.helper.join( -+ room=room_1_id, -+ user=user_2_mxid, -+ tok=user_2_token, -+ ) -+ -+ # User 1 sends 10 unencrypted messages -+ for _ in range(10): -+ self.helper.send( -+ room_id=room_1_id, -+ body="Zoinks Scoob! A message!", -+ tok=user_1_token, -+ ) -+ -+ # User 2 sends 5 encrypted "messages" -+ for _ in range(5): -+ self.helper.send_event( -+ room_id=room_1_id, -+ type="m.room.encrypted", -+ content={ -+ "algorithm": "m.olm.v1.curve25519-aes-sha2", -+ "sender_key": "some_key", -+ "ciphertext": { -+ "some_key": { -+ "type": 0, -+ "body": "encrypted_payload", -+ }, -+ }, -+ }, -+ tok=user_2_token, -+ ) -+ -+ def test_phone_home_stats(self) -> None: -+ """ -+ Test that the phone home stats contain the stats we expect based on -+ the scenario carried out in `prepare` -+ """ -+ # Do things to bump the stats -+ self._perform_user_actions() -+ -+ # Wait for the stats to be reported -+ phone_home_stats = self._get_latest_phone_home_stats() -+ -+ self.assertEqual( -+ phone_home_stats["homeserver"], self.hs.config.server.server_name -+ ) -+ -+ self.assertTrue(isinstance(phone_home_stats["memory_rss"], int)) -+ self.assertTrue(isinstance(phone_home_stats["cpu_average"], int)) -+ -+ self.assertEqual(phone_home_stats["server_context"], TEST_SERVER_CONTEXT) -+ -+ self.assertTrue(isinstance(phone_home_stats["timestamp"], int)) -+ self.assertTrue(isinstance(phone_home_stats["uptime_seconds"], int)) -+ self.assertTrue(isinstance(phone_home_stats["python_version"], str)) -+ -+ # We expect only our test users to exist on the homeserver -+ self.assertEqual(phone_home_stats["total_users"], 3) -+ self.assertEqual(phone_home_stats["total_nonbridged_users"], 3) -+ self.assertEqual(phone_home_stats["daily_user_type_native"], 2) -+ self.assertEqual(phone_home_stats["daily_user_type_guest"], 1) -+ self.assertEqual(phone_home_stats["daily_user_type_bridged"], 0) -+ self.assertEqual(phone_home_stats["total_room_count"], 1) -+ self.assertEqual(phone_home_stats["daily_active_users"], 2) -+ self.assertEqual(phone_home_stats["monthly_active_users"], 2) -+ self.assertEqual(phone_home_stats["daily_active_rooms"], 1) -+ self.assertEqual(phone_home_stats["daily_active_e2ee_rooms"], 1) -+ self.assertEqual(phone_home_stats["daily_messages"], 10) -+ self.assertEqual(phone_home_stats["daily_e2ee_messages"], 5) -+ self.assertEqual(phone_home_stats["daily_sent_messages"], 10) -+ self.assertEqual(phone_home_stats["daily_sent_e2ee_messages"], 5) -+ -+ # Our users have not been around for >30 days, hence these are all 0. -+ self.assertEqual(phone_home_stats["r30v2_users_all"], 0) -+ self.assertEqual(phone_home_stats["r30v2_users_android"], 0) -+ self.assertEqual(phone_home_stats["r30v2_users_ios"], 0) -+ self.assertEqual(phone_home_stats["r30v2_users_electron"], 0) -+ self.assertEqual(phone_home_stats["r30v2_users_web"], 0) -+ self.assertEqual( -+ phone_home_stats["cache_factor"], self.hs.config.caches.global_factor -+ ) -+ self.assertEqual( -+ phone_home_stats["event_cache_size"], -+ self.hs.config.caches.event_cache_size, -+ ) -+ self.assertEqual( -+ phone_home_stats["database_engine"], -+ self.hs.config.database.databases[0].config["name"], -+ ) -+ self.assertEqual( -+ phone_home_stats["database_server_version"], -+ self.hs.get_datastores().main.database_engine.server_version, -+ ) -+ -+ synapse_logger = logging.getLogger("synapse") -+ log_level = synapse_logger.getEffectiveLevel() -+ self.assertEqual(phone_home_stats["log_level"], logging.getLevelName(log_level)) --- -2.49.0 -