remote_server: Send API feature level along with Zulip version.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
Anders Kaseorg 2023-12-08 12:38:01 -08:00 committed by Tim Abbott
parent 0400614a48
commit f86becfc94
5 changed files with 35 additions and 7 deletions

View File

@ -10,7 +10,7 @@ from django.utils.translation import gettext as _
from pydantic import UUID4, BaseModel, ConfigDict, Field, Json, field_validator from pydantic import UUID4, BaseModel, ConfigDict, Field, Json, field_validator
from analytics.models import InstallationCount, RealmCount 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.exceptions import JsonableError, MissingRemoteRealmError
from zerver.lib.outgoing_http import OutgoingSession from zerver.lib.outgoing_http import OutgoingSession
from zerver.lib.queue import queue_json_publish from zerver.lib.queue import queue_json_publish
@ -92,6 +92,7 @@ class AnalyticsRequest(BaseModel):
realmauditlog_rows: Optional[Json[List[RealmAuditLogDataForAnalytics]]] = None realmauditlog_rows: Optional[Json[List[RealmAuditLogDataForAnalytics]]] = None
realms: Json[List[RealmDataForAnalytics]] realms: Json[List[RealmDataForAnalytics]]
version: Optional[Json[str]] version: Optional[Json[str]]
api_feature_level: Optional[Json[int]]
class UserDataForRemoteBilling(BaseModel): class UserDataForRemoteBilling(BaseModel):
@ -335,6 +336,7 @@ def send_analytics_to_push_bouncer() -> None:
realmauditlog_rows=realmauditlog_data, realmauditlog_rows=realmauditlog_data,
realms=get_realms_info_for_push_bouncer(), realms=get_realms_info_for_push_bouncer(),
version=ZULIP_VERSION, version=ZULIP_VERSION,
api_feature_level=API_FEATURE_LEVEL,
) )
try: try:
@ -350,6 +352,7 @@ def send_realms_only_to_push_bouncer() -> Dict[str, RemoteRealmDictValue]:
installation_counts=[], installation_counts=[],
realms=get_realms_info_for_push_bouncer(), realms=get_realms_info_for_push_bouncer(),
version=ZULIP_VERSION, version=ZULIP_VERSION,
api_feature_level=API_FEATURE_LEVEL,
) )
# We don't catch JsonableError here, because we want it to propagate further # We don't catch JsonableError here, because we want it to propagate further

View File

@ -24,7 +24,7 @@ from typing_extensions import override
from analytics.lib.counts import CountStat, LoggingCountStat from analytics.lib.counts import CountStat, LoggingCountStat
from analytics.models import InstallationCount, RealmCount from analytics.models import InstallationCount, RealmCount
from corporate.models import CustomerPlan 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_delete import do_delete_messages
from zerver.actions.message_flags import do_mark_stream_messages_as_read, do_update_message_flags from zerver.actions.message_flags import do_mark_stream_messages_as_read, do_update_message_flags
from zerver.actions.realm_settings import ( from zerver.actions.realm_settings import (
@ -1358,11 +1358,12 @@ class AnalyticsBouncerTest(BouncerTestCase):
realmauditlog_rows=realmauditlog_data, realmauditlog_rows=realmauditlog_data,
realms=[], realms=[],
version=None, version=None,
api_feature_level=None,
) )
result = self.uuid_post( result = self.uuid_post(
self.server_uuid, self.server_uuid,
"/api/v1/remotes/server/analytics", "/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="", subdomain="",
) )
self.assert_json_error(result, "Data is out of order.") self.assert_json_error(result, "Data is out of order.")
@ -1396,6 +1397,7 @@ class AnalyticsBouncerTest(BouncerTestCase):
realmauditlog_rows=[], realmauditlog_rows=[],
realms=realms_data, realms=realms_data,
version=None, version=None,
api_feature_level=None,
) )
result = self.uuid_post( result = self.uuid_post(
self.server_uuid, self.server_uuid,
@ -1447,11 +1449,12 @@ class AnalyticsBouncerTest(BouncerTestCase):
realmauditlog_rows=realmauditlog_data, realmauditlog_rows=realmauditlog_data,
realms=[], realms=[],
version=None, version=None,
api_feature_level=None,
) )
result = self.uuid_post( result = self.uuid_post(
self.server_uuid, self.server_uuid,
"/api/v1/remotes/server/analytics", "/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="", subdomain="",
) )
self.assert_json_error(result, "Invalid event type.") self.assert_json_error(result, "Invalid event type.")
@ -1472,11 +1475,12 @@ class AnalyticsBouncerTest(BouncerTestCase):
realmauditlog_rows=realmauditlog_data, realmauditlog_rows=realmauditlog_data,
realms=[], realms=[],
version=None, version=None,
api_feature_level=None,
) )
result = self.uuid_post( result = self.uuid_post(
self.server_uuid, self.server_uuid,
"/api/v1/remotes/server/analytics", "/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="", subdomain="",
) )
self.assert_json_success(result) 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()] [dict(realm_data) for realm_data in get_realms_info_for_push_bouncer()]
).decode(), ).decode(),
"version": orjson.dumps(ZULIP_VERSION).decode(), "version": orjson.dumps(ZULIP_VERSION).decode(),
"api_feature_level": orjson.dumps(API_FEATURE_LEVEL).decode(),
} }
mock_send_to_push_bouncer.assert_called_with( mock_send_to_push_bouncer.assert_called_with(
"POST", "POST",

View File

@ -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),
),
]

View File

@ -48,6 +48,7 @@ class RemoteZulipServer(models.Model):
contact_email = models.EmailField(blank=True, null=False) contact_email = models.EmailField(blank=True, null=False)
last_updated = models.DateTimeField("last updated", auto_now=True) last_updated = models.DateTimeField("last updated", auto_now=True)
last_version = models.CharField(max_length=VERSION_MAX_LENGTH, null=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. # Whether the server registration has been deactivated.
deactivated = models.BooleanField(default=False) deactivated = models.BooleanField(default=False)

View File

@ -675,6 +675,7 @@ def remote_server_post_analytics(
realmauditlog_rows: Optional[Json[List[RealmAuditLogDataForAnalytics]]] = None, realmauditlog_rows: Optional[Json[List[RealmAuditLogDataForAnalytics]]] = None,
realms: Optional[Json[List[RealmDataForAnalytics]]] = None, realms: Optional[Json[List[RealmDataForAnalytics]]] = None,
version: Optional[Json[str]] = None, version: Optional[Json[str]] = None,
api_feature_level: Optional[Json[int]] = None,
) -> HttpResponse: ) -> HttpResponse:
# Lock the server, preventing this from racing with other # Lock the server, preventing this from racing with other
# duplicate submissions of the data # duplicate submissions of the data
@ -683,9 +684,10 @@ def remote_server_post_analytics(
remote_server_version_updated = False remote_server_version_updated = False
if version is not None: if version is not None:
version = version[0 : RemoteZulipServer.VERSION_MAX_LENGTH] 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.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 remote_server_version_updated = True
validate_incoming_table_data( validate_incoming_table_data(