diff --git a/zerver/lib/remote_server.py b/zerver/lib/remote_server.py index a51b03aa18..ce136571a0 100644 --- a/zerver/lib/remote_server.py +++ b/zerver/lib/remote_server.py @@ -10,7 +10,7 @@ from django.utils.translation import gettext as _ from pydantic import UUID4, BaseModel, ConfigDict, Field, Json, field_validator from analytics.models import InstallationCount, RealmCount -from version import ZULIP_VERSION +from version import API_FEATURE_LEVEL, ZULIP_VERSION from zerver.lib.exceptions import JsonableError, MissingRemoteRealmError from zerver.lib.outgoing_http import OutgoingSession from zerver.lib.queue import queue_json_publish @@ -92,6 +92,7 @@ class AnalyticsRequest(BaseModel): realmauditlog_rows: Optional[Json[List[RealmAuditLogDataForAnalytics]]] = None realms: Json[List[RealmDataForAnalytics]] version: Optional[Json[str]] + api_feature_level: Optional[Json[int]] class UserDataForRemoteBilling(BaseModel): @@ -335,6 +336,7 @@ def send_analytics_to_push_bouncer() -> None: realmauditlog_rows=realmauditlog_data, realms=get_realms_info_for_push_bouncer(), version=ZULIP_VERSION, + api_feature_level=API_FEATURE_LEVEL, ) try: @@ -350,6 +352,7 @@ def send_realms_only_to_push_bouncer() -> Dict[str, RemoteRealmDictValue]: installation_counts=[], realms=get_realms_info_for_push_bouncer(), version=ZULIP_VERSION, + api_feature_level=API_FEATURE_LEVEL, ) # We don't catch JsonableError here, because we want it to propagate further diff --git a/zerver/tests/test_push_notifications.py b/zerver/tests/test_push_notifications.py index ac84a3b556..e8b94f3e62 100644 --- a/zerver/tests/test_push_notifications.py +++ b/zerver/tests/test_push_notifications.py @@ -24,7 +24,7 @@ from typing_extensions import override from analytics.lib.counts import CountStat, LoggingCountStat from analytics.models import InstallationCount, RealmCount from corporate.models import CustomerPlan -from version import ZULIP_VERSION +from version import API_FEATURE_LEVEL, ZULIP_VERSION from zerver.actions.message_delete import do_delete_messages from zerver.actions.message_flags import do_mark_stream_messages_as_read, do_update_message_flags from zerver.actions.realm_settings import ( @@ -1358,11 +1358,12 @@ class AnalyticsBouncerTest(BouncerTestCase): realmauditlog_rows=realmauditlog_data, realms=[], version=None, + api_feature_level=None, ) result = self.uuid_post( self.server_uuid, "/api/v1/remotes/server/analytics", - request.model_dump(round_trip=True, exclude={"realms", "version"}), + request.model_dump(round_trip=True, exclude={"realms", "version", "api_feature_level"}), subdomain="", ) self.assert_json_error(result, "Data is out of order.") @@ -1396,6 +1397,7 @@ class AnalyticsBouncerTest(BouncerTestCase): realmauditlog_rows=[], realms=realms_data, version=None, + api_feature_level=None, ) result = self.uuid_post( self.server_uuid, @@ -1447,11 +1449,12 @@ class AnalyticsBouncerTest(BouncerTestCase): realmauditlog_rows=realmauditlog_data, realms=[], version=None, + api_feature_level=None, ) result = self.uuid_post( self.server_uuid, "/api/v1/remotes/server/analytics", - request.model_dump(round_trip=True, exclude={"version"}), + request.model_dump(round_trip=True, exclude={"version", "api_feature_level"}), subdomain="", ) self.assert_json_error(result, "Invalid event type.") @@ -1472,11 +1475,12 @@ class AnalyticsBouncerTest(BouncerTestCase): realmauditlog_rows=realmauditlog_data, realms=[], version=None, + api_feature_level=None, ) result = self.uuid_post( self.server_uuid, "/api/v1/remotes/server/analytics", - request.model_dump(round_trip=True, exclude={"version"}), + request.model_dump(round_trip=True, exclude={"version", "api_feature_level"}), subdomain="", ) self.assert_json_success(result) @@ -1876,6 +1880,7 @@ class AnalyticsBouncerTest(BouncerTestCase): [dict(realm_data) for realm_data in get_realms_info_for_push_bouncer()] ).decode(), "version": orjson.dumps(ZULIP_VERSION).decode(), + "api_feature_level": orjson.dumps(API_FEATURE_LEVEL).decode(), } mock_send_to_push_bouncer.assert_called_with( "POST", diff --git a/zilencer/migrations/0048_remotezulipserver_last_api_feature_level.py b/zilencer/migrations/0048_remotezulipserver_last_api_feature_level.py new file mode 100644 index 0000000000..39ae0bed4f --- /dev/null +++ b/zilencer/migrations/0048_remotezulipserver_last_api_feature_level.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.8 on 2023-12-09 18:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("zilencer", "0047_preregistrationremoteserverbillinguser_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="remotezulipserver", + name="last_api_feature_level", + field=models.PositiveIntegerField(null=True), + ), + ] diff --git a/zilencer/models.py b/zilencer/models.py index 9acf025e01..f329de72fe 100644 --- a/zilencer/models.py +++ b/zilencer/models.py @@ -48,6 +48,7 @@ class RemoteZulipServer(models.Model): contact_email = models.EmailField(blank=True, null=False) last_updated = models.DateTimeField("last updated", auto_now=True) last_version = models.CharField(max_length=VERSION_MAX_LENGTH, null=True) + last_api_feature_level = models.PositiveIntegerField(null=True) # Whether the server registration has been deactivated. deactivated = models.BooleanField(default=False) diff --git a/zilencer/views.py b/zilencer/views.py index 35d11705b6..5e16382588 100644 --- a/zilencer/views.py +++ b/zilencer/views.py @@ -675,6 +675,7 @@ def remote_server_post_analytics( realmauditlog_rows: Optional[Json[List[RealmAuditLogDataForAnalytics]]] = None, realms: Optional[Json[List[RealmDataForAnalytics]]] = None, version: Optional[Json[str]] = None, + api_feature_level: Optional[Json[int]] = None, ) -> HttpResponse: # Lock the server, preventing this from racing with other # duplicate submissions of the data @@ -683,9 +684,10 @@ def remote_server_post_analytics( remote_server_version_updated = False if version is not None: version = version[0 : RemoteZulipServer.VERSION_MAX_LENGTH] - if version != server.last_version: + if version != server.last_version or api_feature_level != server.last_api_feature_level: server.last_version = version - server.save(update_fields=["last_version"]) + server.last_api_feature_level = api_feature_level + server.save(update_fields=["last_version", "last_api_feature_level"]) remote_server_version_updated = True validate_incoming_table_data(