mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-31 20:13:46 +00:00 
			
		
		
		
	Fixes #2665. Regenerated by tabbott with `lint --fix` after a rebase and change in parameters. Note from tabbott: In a few cases, this converts technical debt in the form of unsorted imports into different technical debt in the form of our largest files having very long, ugly import sequences at the start. I expect this change will increase pressure for us to split those files, which isn't a bad thing. Signed-off-by: Anders Kaseorg <anders@zulip.com>
		
			
				
	
	
		
			170 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			170 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| import logging
 | |
| import urllib
 | |
| from typing import Any, Dict, List, Optional, Tuple, Union
 | |
| 
 | |
| import requests
 | |
| import ujson
 | |
| from django.conf import settings
 | |
| from django.forms.models import model_to_dict
 | |
| from django.utils.translation import ugettext as _
 | |
| 
 | |
| 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
 | |
| 
 | |
| class PushNotificationBouncerRetryLaterError(JsonableError):
 | |
|     http_status_code = 502
 | |
| 
 | |
| def send_to_push_bouncer(method: str,
 | |
|                          endpoint: str,
 | |
|                          post_data: Union[str, Dict[str, Any]],
 | |
|                          extra_headers: Optional[Dict[str, Any]]=None) -> Dict[str, Any]:
 | |
|     """While it does actually send the notice, this function has a lot of
 | |
|     code and comments around error handling for the push notifications
 | |
|     bouncer.  There are several classes of failures, each with its own
 | |
|     potential solution:
 | |
| 
 | |
|     * Network errors with requests.request.  We raise an exception to signal
 | |
|       it to the callers.
 | |
| 
 | |
|     * 500 errors from the push bouncer or other unexpected responses;
 | |
|       we don't try to parse the response, but do make clear the cause.
 | |
| 
 | |
|     * 400 errors from the push bouncer.  Here there are 2 categories:
 | |
|       Our server failed to connect to the push bouncer (should throw)
 | |
|       vs. client-side errors like and invalid token.
 | |
| 
 | |
|     """
 | |
|     url = urllib.parse.urljoin(settings.PUSH_NOTIFICATION_BOUNCER_URL,
 | |
|                                '/api/v1/remotes/' + endpoint)
 | |
|     api_auth = requests.auth.HTTPBasicAuth(settings.ZULIP_ORG_ID,
 | |
|                                            settings.ZULIP_ORG_KEY)
 | |
| 
 | |
|     headers = {"User-agent": f"ZulipServer/{ZULIP_VERSION}"}
 | |
|     if extra_headers is not None:
 | |
|         headers.update(extra_headers)
 | |
| 
 | |
|     try:
 | |
|         res = requests.request(method,
 | |
|                                url,
 | |
|                                data=post_data,
 | |
|                                auth=api_auth,
 | |
|                                timeout=30,
 | |
|                                verify=True,
 | |
|                                headers=headers)
 | |
|     except (requests.exceptions.Timeout, requests.exceptions.SSLError,
 | |
|             requests.exceptions.ConnectionError) as e:
 | |
|         raise PushNotificationBouncerRetryLaterError(
 | |
|             f"{e.__class__.__name__} while trying to connect to push notification bouncer")
 | |
| 
 | |
|     if res.status_code >= 500:
 | |
|         # 500s should be resolved by the people who run the push
 | |
|         # notification bouncer service, and they'll get an appropriate
 | |
|         # error notification from the server. We raise an exception to signal
 | |
|         # to the callers that the attempt failed and they can retry.
 | |
|         error_msg = "Received 500 from push notification bouncer"
 | |
|         logging.warning(error_msg)
 | |
|         raise PushNotificationBouncerRetryLaterError(error_msg)
 | |
|     elif res.status_code >= 400:
 | |
|         # If JSON parsing errors, just let that exception happen
 | |
|         result_dict = ujson.loads(res.content)
 | |
|         msg = result_dict['msg']
 | |
|         if 'code' in result_dict and result_dict['code'] == 'INVALID_ZULIP_SERVER':
 | |
|             # Invalid Zulip server credentials should email this server's admins
 | |
|             raise PushNotificationBouncerException(
 | |
|                 _("Push notifications bouncer error: %s") % (msg,))
 | |
|         else:
 | |
|             # But most other errors coming from the push bouncer
 | |
|             # server are client errors (e.g. never-registered token)
 | |
|             # and should be handled as such.
 | |
|             raise JsonableError(msg)
 | |
|     elif res.status_code != 200:
 | |
|         # Anything else is unexpected and likely suggests a bug in
 | |
|         # this version of Zulip, so we throw an exception that will
 | |
|         # email the server admins.
 | |
|         raise PushNotificationBouncerException(
 | |
|             f"Push notification bouncer returned unexpected status code {res.status_code}")
 | |
| 
 | |
|     # If we don't throw an exception, it's a successful bounce!
 | |
|     return ujson.loads(res.content)
 | |
| 
 | |
| def send_json_to_push_bouncer(method: str, endpoint: str, post_data: Dict[str, Any]) -> None:
 | |
|     send_to_push_bouncer(
 | |
|         method,
 | |
|         endpoint,
 | |
|         ujson.dumps(post_data),
 | |
|         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,
 | |
|                          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(row) for row in
 | |
|         realm_count_query.order_by("id")[0:MAX_CLIENT_BATCH_SIZE]
 | |
|     ]
 | |
|     data['analytics_installationcount'] = [
 | |
|         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')
 | |
|     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
 | |
|     try:
 | |
|         result = send_to_push_bouncer("GET", "server/analytics/status", {})
 | |
|     except PushNotificationBouncerRetryLaterError as e:
 | |
|         logging.warning(e.msg)
 | |
|         return
 | |
| 
 | |
|     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, 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),
 | |
|         realmauditlog_query=RealmAuditLog.objects.filter(
 | |
|             event_type__in=RealmAuditLog.SYNCED_BILLING_EVENTS,
 | |
|             id__gt=last_acked_realmauditlog_id))
 | |
| 
 | |
|     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),
 | |
|     }
 | |
| 
 | |
|     # Gather only entries with an ID greater than last_realm_count_id
 | |
|     try:
 | |
|         send_to_push_bouncer("POST", "server/analytics", request)
 | |
|     except JsonableError as e:
 | |
|         logging.warning(e.msg)
 |