From 360cd7f1474e26b4e784868b1c794c7c48282b1d Mon Sep 17 00:00:00 2001 From: Rishi Gupta Date: Wed, 2 Oct 2019 17:01:36 -0700 Subject: [PATCH] remote data: Send RealmAuditLog data. --- tools/linter_lib/custom_check.py | 1 + zerver/lib/remote_server.py | 32 +++- zerver/models.py | 57 ++++--- zerver/tests/test_push_notifications.py | 151 +++++++++++++++--- .../migrations/0018_remoterealmauditlog.py | 29 ++++ zilencer/models.py | 15 +- zilencer/urls.py | 2 +- zilencer/views.py | 30 +++- 8 files changed, 253 insertions(+), 64 deletions(-) create mode 100644 zilencer/migrations/0018_remoterealmauditlog.py diff --git a/tools/linter_lib/custom_check.py b/tools/linter_lib/custom_check.py index d319b82e68..4d6666115f 100644 --- a/tools/linter_lib/custom_check.py +++ b/tools/linter_lib/custom_check.py @@ -264,6 +264,7 @@ python_rules = RuleList( 'bad_lines': ['a =b', 'asdf =42']}, {'pattern': r'":\w[^"]*$', 'description': 'Missing whitespace after ":"', + 'exclude': set(['zerver/tests/test_push_notifications.py']), 'good_lines': ['"foo": bar', '"some:string:with:colons"'], 'bad_lines': ['"foo":bar', '"foo":1']}, {'pattern': r"':\w[^']*$", diff --git a/zerver/lib/remote_server.py b/zerver/lib/remote_server.py index 01b9d32029..6572a336a8 100644 --- a/zerver/lib/remote_server.py +++ b/zerver/lib/remote_server.py @@ -12,6 +12,7 @@ from analytics.models import InstallationCount, RealmCount from version import ZULIP_VERSION from zerver.lib.exceptions import JsonableError from zerver.lib.export import floatify_datetime_fields +from zerver.models import RealmAuditLog class PushNotificationBouncerException(Exception): pass @@ -91,24 +92,34 @@ def send_json_to_push_bouncer(method: str, endpoint: str, post_data: Dict[str, A extra_headers={"Content-type": "application/json"}, ) +REALMAUDITLOG_PUSHED_FIELDS = ['id', 'realm', 'event_time', 'backfilled', 'extra_data', 'event_type'] + def build_analytics_data(realm_count_query: Any, - installation_count_query: Any) -> Tuple[List[Dict[str, Any]], - List[Dict[str, Any]]]: + installation_count_query: Any, + realmauditlog_query: Any) -> Tuple[List[Dict[str, Any]], + List[Dict[str, Any]], + List[Dict[str, Any]]]: # We limit the batch size on the client side to avoid OOM kills timeouts, etc. MAX_CLIENT_BATCH_SIZE = 10000 data = {} data['analytics_realmcount'] = [ - model_to_dict(realm_count) for realm_count in + model_to_dict(row) for row in realm_count_query.order_by("id")[0:MAX_CLIENT_BATCH_SIZE] ] data['analytics_installationcount'] = [ - model_to_dict(count) for count in + model_to_dict(row) for row in installation_count_query.order_by("id")[0:MAX_CLIENT_BATCH_SIZE] ] + data['zerver_realmauditlog'] = [ + model_to_dict(row, fields=REALMAUDITLOG_PUSHED_FIELDS) for row in + realmauditlog_query.order_by("id")[0:MAX_CLIENT_BATCH_SIZE] + ] floatify_datetime_fields(data, 'analytics_realmcount') floatify_datetime_fields(data, 'analytics_installationcount') - return (data['analytics_realmcount'], data['analytics_installationcount']) + floatify_datetime_fields(data, 'zerver_realmauditlog') + return (data['analytics_realmcount'], data['analytics_installationcount'], + data['zerver_realmauditlog']) def send_analytics_to_remote_server() -> None: # first, check what's latest @@ -118,19 +129,24 @@ def send_analytics_to_remote_server() -> None: last_acked_realm_count_id = result['last_realm_count_id'] last_acked_installation_count_id = result['last_installation_count_id'] + last_acked_realmauditlog_id = result['last_realmauditlog_id'] - (realm_count_data, installation_count_data) = build_analytics_data( + (realm_count_data, installation_count_data, realmauditlog_data) = build_analytics_data( realm_count_query=RealmCount.objects.filter( id__gt=last_acked_realm_count_id), installation_count_query=InstallationCount.objects.filter( - id__gt=last_acked_installation_count_id)) + id__gt=last_acked_installation_count_id), + realmauditlog_query=RealmAuditLog.objects.filter( + event_type__in=RealmAuditLog.SYNCED_BILLING_EVENTS, + id__gt=last_acked_realmauditlog_id)) - if len(realm_count_data) == 0 and len(installation_count_data) == 0: + if len(realm_count_data) + len(installation_count_data) + len(realmauditlog_data) == 0: return request = { 'realm_counts': ujson.dumps(realm_count_data), 'installation_counts': ujson.dumps(installation_count_data), + 'realmauditlog_rows': ujson.dumps(realmauditlog_data), 'version': ujson.dumps(ZULIP_VERSION), } diff --git a/zerver/models.py b/zerver/models.py index 1afc54a6a4..241a6d53e7 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -2541,32 +2541,8 @@ EMAIL_TYPES = { 'invitation_reminder': ScheduledEmail.INVITATION_REMINDER, } -class RealmAuditLog(models.Model): - """ - RealmAuditLog tracks important changes to users, streams, and - realms in Zulip. It is intended to support both - debugging/introspection (e.g. determining when a user's left a - given stream?) as well as help with some database migrations where - we might be able to do a better data backfill with it. Here are a - few key details about how this works: - - * acting_user is the user who initiated the state change - * modified_user (if present) is the user being modified - * modified_stream (if present) is the stream being modified - - For example: - * When a user subscribes another user to a stream, modified_user, - acting_user, and modified_stream will all be present and different. - * When an administrator changes an organization's realm icon, - acting_user is that administrator and both modified_user and - modified_stream will be None. - """ - realm = models.ForeignKey(Realm, on_delete=CASCADE) # type: Realm - acting_user = models.ForeignKey(UserProfile, null=True, related_name='+', on_delete=CASCADE) # type: Optional[UserProfile] - modified_user = models.ForeignKey(UserProfile, null=True, related_name='+', on_delete=CASCADE) # type: Optional[UserProfile] - modified_stream = models.ForeignKey(Stream, null=True, on_delete=CASCADE) # type: Optional[Stream] - event_last_message_id = models.IntegerField(null=True) # type: Optional[int] - +class AbstractRealmAuditLog(models.Model): + """Defines fields common to RealmAuditLog and RemoteRealmAuditLog.""" event_time = models.DateTimeField(db_index=True) # type: datetime.datetime # If True, event_time is an overestimate of the true time. Can be used # by migrations when introducing a new event_type. @@ -2627,6 +2603,35 @@ class RealmAuditLog(models.Model): USER_CREATED, USER_ACTIVATED, USER_DEACTIVATED, USER_REACTIVATED, USER_ROLE_CHANGED, REALM_DEACTIVATED, REALM_REACTIVATED] + class Meta: + abstract = True + +class RealmAuditLog(AbstractRealmAuditLog): + """ + RealmAuditLog tracks important changes to users, streams, and + realms in Zulip. It is intended to support both + debugging/introspection (e.g. determining when a user's left a + given stream?) as well as help with some database migrations where + we might be able to do a better data backfill with it. Here are a + few key details about how this works: + + * acting_user is the user who initiated the state change + * modified_user (if present) is the user being modified + * modified_stream (if present) is the stream being modified + + For example: + * When a user subscribes another user to a stream, modified_user, + acting_user, and modified_stream will all be present and different. + * When an administrator changes an organization's realm icon, + acting_user is that administrator and both modified_user and + modified_stream will be None. + """ + realm = models.ForeignKey(Realm, on_delete=CASCADE) # type: Realm + acting_user = models.ForeignKey(UserProfile, null=True, related_name='+', on_delete=CASCADE) # type: Optional[UserProfile] + modified_user = models.ForeignKey(UserProfile, null=True, related_name='+', on_delete=CASCADE) # type: Optional[UserProfile] + modified_stream = models.ForeignKey(Stream, null=True, on_delete=CASCADE) # type: Optional[Stream] + event_last_message_id = models.IntegerField(null=True) # type: Optional[int] + def __str__(self) -> str: if self.modified_user is not None: return "" % ( diff --git a/zerver/tests/test_push_notifications.py b/zerver/tests/test_push_notifications.py index 24ea783984..de32f86743 100644 --- a/zerver/tests/test_push_notifications.py +++ b/zerver/tests/test_push_notifications.py @@ -33,6 +33,7 @@ from zerver.models import ( get_realm, get_stream, Recipient, + RealmAuditLog, Stream, Subscription, ) @@ -67,7 +68,7 @@ from zerver.lib.test_classes import ( ) from zilencer.models import RemoteZulipServer, RemotePushDeviceToken, \ - RemoteRealmCount, RemoteInstallationCount + RemoteRealmCount, RemoteInstallationCount, RemoteRealmAuditLog from django.utils.timezone import now ZERVER_DIR = os.path.dirname(os.path.dirname(__file__)) @@ -319,6 +320,23 @@ class AnalyticsBouncerTest(BouncerTestCase): user = self.example_user('hamlet') end_time = self.TIME_ZERO + # Send any existing data over, so that we can start the test with a "clean" slate + audit_log_max_id = RealmAuditLog.objects.all().order_by('id').last().id + send_analytics_to_remote_server() + self.assertEqual(mock_request.call_count, 2) + remote_audit_log_count = RemoteRealmAuditLog.objects.count() + self.assertEqual(RemoteRealmCount.objects.count(), 0) + self.assertEqual(RemoteInstallationCount.objects.count(), 0) + + def check_counts(mock_request_call_count: int, remote_realm_count: int, + remote_installation_count: int, remote_realm_audit_log: int) -> None: + self.assertEqual(mock_request.call_count, mock_request_call_count) + self.assertEqual(RemoteRealmCount.objects.count(), remote_realm_count) + self.assertEqual(RemoteInstallationCount.objects.count(), remote_installation_count) + self.assertEqual(RemoteRealmAuditLog.objects.count(), + remote_audit_log_count + remote_realm_audit_log) + + # Create some rows we'll send to remote server realm_stat = LoggingCountStat('invites_sent::day', RealmCount, CountStat.DAY) RealmCount.objects.create( realm=user.realm, property=realm_stat.property, end_time=end_time, value=5) @@ -327,48 +345,63 @@ class AnalyticsBouncerTest(BouncerTestCase): # We set a subgroup here to work around: # https://github.com/zulip/zulip/issues/12362 subgroup="test_subgroup") - + # Event type in SYNCED_BILLING_EVENTS -- should be included + RealmAuditLog.objects.create( + realm=user.realm, modified_user=user, event_type=RealmAuditLog.USER_CREATED, + event_time=end_time, extra_data='data') + # Event type not in SYNCED_BILLING_EVENTS -- should not be included + RealmAuditLog.objects.create( + realm=user.realm, modified_user=user, event_type=RealmAuditLog.REALM_LOGO_CHANGED, + event_time=end_time, extra_data='data') self.assertEqual(RealmCount.objects.count(), 1) self.assertEqual(InstallationCount.objects.count(), 1) + self.assertEqual(RealmAuditLog.objects.filter(id__gt=audit_log_max_id).count(), 2) - self.assertEqual(RemoteRealmCount.objects.count(), 0) - self.assertEqual(RemoteInstallationCount.objects.count(), 0) send_analytics_to_remote_server() - self.assertEqual(mock_request.call_count, 2) - self.assertEqual(RemoteRealmCount.objects.count(), 1) - self.assertEqual(RemoteInstallationCount.objects.count(), 1) - send_analytics_to_remote_server() - self.assertEqual(mock_request.call_count, 3) - self.assertEqual(RemoteRealmCount.objects.count(), 1) - self.assertEqual(RemoteInstallationCount.objects.count(), 1) + check_counts(4, 1, 1, 1) + # Test having no new rows + send_analytics_to_remote_server() + check_counts(5, 1, 1, 1) + + # Test only having new RealmCount rows RealmCount.objects.create( realm=user.realm, property=realm_stat.property, end_time=end_time + datetime.timedelta(days=1), value=6) RealmCount.objects.create( realm=user.realm, property=realm_stat.property, end_time=end_time + datetime.timedelta(days=2), value=9) - self.assertEqual(RemoteRealmCount.objects.count(), 1) - self.assertEqual(mock_request.call_count, 3) send_analytics_to_remote_server() - self.assertEqual(mock_request.call_count, 5) - self.assertEqual(RemoteRealmCount.objects.count(), 3) - self.assertEqual(RemoteInstallationCount.objects.count(), 1) + check_counts(7, 3, 1, 1) + # Test only having new InstallationCount rows InstallationCount.objects.create( property=realm_stat.property, end_time=end_time + datetime.timedelta(days=1), value=6) - InstallationCount.objects.create( - property=realm_stat.property, end_time=end_time + datetime.timedelta(days=2), value=9) send_analytics_to_remote_server() - self.assertEqual(mock_request.call_count, 7) - self.assertEqual(RemoteRealmCount.objects.count(), 3) - self.assertEqual(RemoteInstallationCount.objects.count(), 3) + check_counts(9, 3, 2, 1) + + # Test only having new RealmAuditLog rows + # Non-synced event + RealmAuditLog.objects.create( + realm=user.realm, modified_user=user, event_type=RealmAuditLog.REALM_LOGO_CHANGED, + event_time=end_time, extra_data='data') + send_analytics_to_remote_server() + check_counts(10, 3, 2, 1) + # Synced event + RealmAuditLog.objects.create( + realm=user.realm, modified_user=user, event_type=RealmAuditLog.USER_REACTIVATED, + event_time=end_time, extra_data='data') + send_analytics_to_remote_server() + check_counts(12, 3, 2, 2) (realm_count_data, - installation_count_data) = build_analytics_data(RealmCount.objects.all(), - InstallationCount.objects.all()) + installation_count_data, + realmauditlog_data) = build_analytics_data(RealmCount.objects.all(), + InstallationCount.objects.all(), + RealmAuditLog.objects.all()) result = self.api_post(self.server_uuid, '/api/v1/remotes/server/analytics', {'realm_counts': ujson.dumps(realm_count_data), - 'installation_counts': ujson.dumps(installation_count_data)}, + 'installation_counts': ujson.dumps(installation_count_data), + 'realmauditlog_rows': ujson.dumps(realmauditlog_data)}, subdomain="") self.assert_json_error(result, "Data is out of order.") @@ -381,7 +414,8 @@ class AnalyticsBouncerTest(BouncerTestCase): self.server_uuid, '/api/v1/remotes/server/analytics', {'realm_counts': ujson.dumps(realm_count_data), - 'installation_counts': ujson.dumps(installation_count_data)}, + 'installation_counts': ujson.dumps(installation_count_data), + 'realmauditlog_rows': ujson.dumps(realmauditlog_data)}, subdomain="") self.assert_json_error(result, "Invalid data.") @@ -407,6 +441,73 @@ class AnalyticsBouncerTest(BouncerTestCase): log_warning.assert_called_once() self.assertEqual(RemoteRealmCount.objects.count(), 0) + # Servers on Zulip 2.0.6 and earlier only send realm_counts and installation_counts data, + # and don't send realmauditlog_rows. Make sure that continues to work. + @override_settings(PUSH_NOTIFICATION_BOUNCER_URL='https://push.zulip.org.example.com') + @mock.patch('zerver.lib.push_notifications.requests.request') + def test_old_two_table_format(self, mock_request: Any) -> None: + mock_request.side_effect = self.bounce_request + # Send fixture generated with Zulip 2.0 code + send_to_push_bouncer('POST', 'server/analytics', { + 'realm_counts': '[{"id":1,"property":"invites_sent::day","subgroup":null,"end_time":574300800.0,"value":5,"realm":2}]', # lint:ignore + 'installation_counts': '[]', + 'version': '"2.0.6+git"'}) + self.assertEqual(mock_request.call_count, 1) + self.assertEqual(RemoteRealmCount.objects.count(), 1) + self.assertEqual(RemoteInstallationCount.objects.count(), 0) + self.assertEqual(RemoteRealmAuditLog.objects.count(), 0) + + # Make sure we aren't sending data we don't mean to, even if we don't store it. + @override_settings(PUSH_NOTIFICATION_BOUNCER_URL='https://push.zulip.org.example.com') + @mock.patch('zerver.lib.push_notifications.requests.request') + def test_only_sending_intended_realmauditlog_data(self, mock_request: Any) -> None: + mock_request.side_effect = self.bounce_request + user = self.example_user('hamlet') + # Event type in SYNCED_BILLING_EVENTS -- should be included + RealmAuditLog.objects.create( + realm=user.realm, modified_user=user, event_type=RealmAuditLog.USER_REACTIVATED, + event_time=self.TIME_ZERO, extra_data='data') + # Event type not in SYNCED_BILLING_EVENTS -- should not be included + RealmAuditLog.objects.create( + realm=user.realm, modified_user=user, event_type=RealmAuditLog.REALM_LOGO_CHANGED, + event_time=self.TIME_ZERO, extra_data='data') + + def check_for_unwanted_data(*args: Any) -> Any: + if check_for_unwanted_data.first_call: # type: ignore + check_for_unwanted_data.first_call = False # type: ignore + else: + # Test that we're respecting SYNCED_BILLING_EVENTS + self.assertIn('"event_type":{}'.format(RealmAuditLog.USER_REACTIVATED), str(args)) + self.assertNotIn('"event_type":{}'.format(RealmAuditLog.REALM_LOGO_CHANGED), str(args)) + # Test that we're respecting REALMAUDITLOG_PUSHED_FIELDS + self.assertIn('backfilled', str(args)) + self.assertNotIn('modified_user', str(args)) + return send_to_push_bouncer(*args) + + # send_analytics_to_remote_server calls send_to_push_bouncer twice. + # We need to distinguish the first and second calls. + check_for_unwanted_data.first_call = True # type: ignore + with mock.patch('zerver.lib.remote_server.send_to_push_bouncer', + side_effect=check_for_unwanted_data): + send_analytics_to_remote_server() + + @override_settings(PUSH_NOTIFICATION_BOUNCER_URL='https://push.zulip.org.example.com') + @mock.patch('zerver.lib.push_notifications.requests.request') + def test_realmauditlog_data_mapping(self, mock_request: Any) -> None: + mock_request.side_effect = self.bounce_request + user = self.example_user('hamlet') + log_entry = RealmAuditLog.objects.create( + realm=user.realm, modified_user=user, backfilled=True, + event_type=RealmAuditLog.USER_REACTIVATED, event_time=self.TIME_ZERO, extra_data='data') + send_analytics_to_remote_server() + remote_log_entry = RemoteRealmAuditLog.objects.order_by('id').last() + self.assertEqual(remote_log_entry.server.uuid, self.server_uuid) + self.assertEqual(remote_log_entry.remote_id, log_entry.id) + self.assertEqual(remote_log_entry.event_time, self.TIME_ZERO) + self.assertEqual(remote_log_entry.backfilled, True) + self.assertEqual(remote_log_entry.extra_data, 'data') + self.assertEqual(remote_log_entry.event_type, RealmAuditLog.USER_REACTIVATED) + class PushNotificationTest(BouncerTestCase): def setUp(self) -> None: super().setUp() diff --git a/zilencer/migrations/0018_remoterealmauditlog.py b/zilencer/migrations/0018_remoterealmauditlog.py new file mode 100644 index 0000000000..e0b36a872e --- /dev/null +++ b/zilencer/migrations/0018_remoterealmauditlog.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.24 on 2019-10-03 00:10 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('zilencer', '0017_installationcount_indexes'), + ] + + operations = [ + migrations.CreateModel( + name='RemoteRealmAuditLog', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('realm_id', models.IntegerField(db_index=True)), + ('remote_id', models.IntegerField(db_index=True)), + ('event_time', models.DateTimeField(db_index=True)), + ('backfilled', models.BooleanField(default=False)), + ('extra_data', models.TextField(null=True)), + ('event_type', models.PositiveSmallIntegerField()), + ('server', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='zilencer.RemoteZulipServer')), + ], + ), + ] diff --git a/zilencer/models.py b/zilencer/models.py index 94c775de79..149e1bbfe1 100644 --- a/zilencer/models.py +++ b/zilencer/models.py @@ -2,7 +2,7 @@ import datetime from django.db import models -from zerver.models import AbstractPushDeviceToken +from zerver.models import AbstractPushDeviceToken, AbstractRealmAuditLog from analytics.models import BaseCount def get_remote_server_by_uuid(uuid: str) -> 'RemoteZulipServer': @@ -35,6 +35,19 @@ class RemotePushDeviceToken(AbstractPushDeviceToken): def __str__(self) -> str: return "" % (self.server, self.user_id) +class RemoteRealmAuditLog(AbstractRealmAuditLog): + """Synced audit data from a remote Zulip server, used primarily for + billing. See RealmAuditLog and AbstractRealmAuditLog for details. + """ + server = models.ForeignKey(RemoteZulipServer, on_delete=models.CASCADE) # type: RemoteZulipServer + realm_id = models.IntegerField(db_index=True) # type: int + # The remote_id field lets us deduplicate data from the remote server + remote_id = models.IntegerField(db_index=True) # type: int + + def __str__(self) -> str: + return "" % ( + self.server, self.event_type, self.event_time, self.id) + class RemoteInstallationCount(BaseCount): server = models.ForeignKey(RemoteZulipServer, on_delete=models.CASCADE) # type: RemoteZulipServer # The remote_id field lets us deduplicate data from the remote server diff --git a/zilencer/urls.py b/zilencer/urls.py index 5bb6376203..8337a28ef6 100644 --- a/zilencer/urls.py +++ b/zilencer/urls.py @@ -19,7 +19,7 @@ v1_api_and_json_patterns = [ # Push signup doesn't use the REST API, since there's no auth. url('^remotes/server/register$', zilencer.views.register_remote_server), - # For receiving InstallationCount data and similar analytics. + # For receiving table data used in analytics and billing url('^remotes/server/analytics$', rest_dispatch, {'POST': 'zilencer.views.remote_server_post_analytics'}), url('^remotes/server/analytics/status$', rest_dispatch, diff --git a/zilencer/views.py b/zilencer/views.py index 3728b20d07..311baeb5da 100644 --- a/zilencer/views.py +++ b/zilencer/views.py @@ -20,11 +20,11 @@ from zerver.lib.request import REQ, has_request_variables from zerver.lib.response import json_error, json_success from zerver.lib.validator import check_int, check_string, \ check_capped_string, check_string_fixed_length, check_float, check_none_or, \ - check_dict_only, check_list + check_dict_only, check_list, check_bool from zerver.models import UserProfile from zerver.views.push_notifications import validate_token from zilencer.models import RemotePushDeviceToken, RemoteZulipServer, \ - RemoteRealmCount, RemoteInstallationCount + RemoteRealmCount, RemoteInstallationCount, RemoteRealmAuditLog def validate_entity(entity: Union[UserProfile, RemoteZulipServer]) -> RemoteZulipServer: if not isinstance(entity, RemoteZulipServer): @@ -192,11 +192,22 @@ def remote_server_post_analytics(request: HttpRequest, ('end_time', check_float), ('subgroup', check_none_or(check_string)), ('value', check_int), - ])))) -> HttpResponse: + ]))), + realmauditlog_rows: Optional[List[Dict[str, Any]]]=REQ( + validator=check_list(check_dict_only([ + ('id', check_int), + ('realm', check_int), + ('event_time', check_float), + ('backfilled', check_bool), + ('extra_data', check_none_or(check_string)), + ('event_type', check_int), + ])), default=None)) -> HttpResponse: server = validate_entity(entity) validate_incoming_table_data(server, RemoteRealmCount, realm_counts, True) validate_incoming_table_data(server, RemoteInstallationCount, installation_counts, True) + if realmauditlog_rows is not None: + validate_incoming_table_data(server, RemoteRealmAuditLog, realmauditlog_rows) row_objects = [RemoteRealmCount( property=row['property'], @@ -217,6 +228,17 @@ def remote_server_post_analytics(request: HttpRequest, value=row['value']) for row in installation_counts] batch_create_table_data(server, RemoteInstallationCount, row_objects) + if realmauditlog_rows is not None: + row_objects = [RemoteRealmAuditLog( + realm_id=row['realm'], + remote_id=row['id'], + server=server, + event_time=datetime.datetime.fromtimestamp(row['event_time'], tz=timezone_utc), + backfilled=row['backfilled'], + extra_data=row['extra_data'], + event_type=row['event_type']) for row in realmauditlog_rows] + batch_create_table_data(server, RemoteRealmAuditLog, row_objects) + return json_success() def get_last_id_from_server(server: RemoteZulipServer, model: Any) -> int: @@ -234,5 +256,7 @@ def remote_server_check_analytics(request: HttpRequest, 'last_realm_count_id': get_last_id_from_server(server, RemoteRealmCount), 'last_installation_count_id': get_last_id_from_server( server, RemoteInstallationCount), + 'last_realmauditlog_id': get_last_id_from_server( + server, RemoteRealmAuditLog), } return json_success(result)