Source code for shuup.core.telemetry

import json
import platform
import sys
from datetime import date, datetime, time, timedelta

import requests
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import Q, Sum
from django.utils.crypto import get_random_string
from django.utils.timezone import now

import shuup
from shuup import configuration
from shuup.core.models import Contact, Order, Payment, PersistentCacheEntry, Product
from shuup.utils.django_compat import force_text

User = get_user_model()

OPT_OUT_KWARGS = {"module": "telemetry", "key": "opt_out"}
INSTALLATION_KEY_KWARGS = {"module": "telemetry", "key": "installation_key"}
LAST_DATA_KWARGS = {"module": "telemetry", "key": "last_data"}


[docs] def safe_json(data_dict, indent=None): return json.dumps(data_dict, cls=DjangoJSONEncoder, sort_keys=True, indent=indent)
[docs] def get_installation_key(): """ Get the unique installation ID for this Shuup instance. If one doesn't exist, it's generated and saved at this point. :return: Installation key string :rtype: str """ try: return PersistentCacheEntry.objects.get(**INSTALLATION_KEY_KWARGS).data except ObjectDoesNotExist: key = get_random_string(48) PersistentCacheEntry.objects.create(data=key, **INSTALLATION_KEY_KWARGS) return key
[docs] def is_opt_out(): return PersistentCacheEntry.objects.filter(**OPT_OUT_KWARGS).exists()
[docs] def is_in_grace_period(): """ Return True if the telemetry module is within the 24-hours-from-installation grace period where no stats are sent. This is to "safely" allow opting out of telemetry without leaving a trace. :return: Graceness flag. :rtype: bool """ get_installation_key() # Need to initialize here installation_time = PersistentCacheEntry.objects.get(**INSTALLATION_KEY_KWARGS).time return (now() - installation_time).total_seconds() < 60 * 60 * 24
[docs] def is_telemetry_enabled(): return bool(settings.SHUUP_TELEMETRY_ENABLED)
[docs] def set_opt_out(flag): """ Set whether this installation is opted-out from telemetry submissions. :param flag: Opt-out flag. True for opt-out, false for opt-in (default) :type flag: bool :return: New flag state :rtype: bool """ if flag and not is_opt_out(): PersistentCacheEntry.objects.create(data=True, **OPT_OUT_KWARGS) return True else: PersistentCacheEntry.objects.filter(**OPT_OUT_KWARGS).delete() return False
[docs] def get_last_submission_time(): try: return PersistentCacheEntry.objects.get(**LAST_DATA_KWARGS).time except ObjectDoesNotExist: return None
[docs] def get_last_submission_data(): try: return safe_json(PersistentCacheEntry.objects.get(**LAST_DATA_KWARGS).data, indent=4) except ObjectDoesNotExist: return None
[docs] def save_telemetry_submission(data): """ Save a blob of data as the latest telemetry submission. Naturally updates the latest submission time. :param data: A blob of data. :type data: dict """ pce, _ = PersistentCacheEntry.objects.get_or_create(defaults={"data": None}, **LAST_DATA_KWARGS) pce.data = data pce.save()
[docs] def daterange(start_date, end_date): if start_date == end_date: yield start_date for n in range(int((end_date - start_date).days)): yield start_date + timedelta(n)
[docs] def get_daily_data_for_day(date): data = {"date": date.strftime("%Y-%m-%d")} data["methods"] = {} today_min = datetime.combine(date, time.min) today_max = datetime.combine(date, time.max) order_date_filter = Q(order_date__range=(today_min, today_max)) data["orders"] = Order.objects.filter(order_date_filter).count() total_sales = Order.objects.filter(order_date_filter).aggregate(total_sales=Sum("taxful_total_price_value")) data["total_sales"] = float(total_sales["total_sales"]) if total_sales["total_sales"] else 0 created_on_filter = Q(created_on__range=(today_min, today_max)) total_paid_sales = Payment.objects.filter(created_on_filter).aggregate(total_paid=Sum("amount_value")) data["total_paid_sales"] = float(total_paid_sales["total_paid"]) if total_paid_sales["total_paid"] else 0 for service_identifier in ["stripe", "checkoutfi", "paytrail"]: payment_query = created_on_filter & Q(order__payment_method__choice_identifier=service_identifier) total_sales = Payment.objects.filter(payment_query).aggregate(total_sales=Sum("amount_value")) data["methods"][service_identifier] = float(total_sales["total_sales"]) if total_sales["total_sales"] else 0 data["products"] = Product.objects.filter(created_on_filter).count() data["contacts"] = Contact.objects.filter(created_on_filter).count() return data
[docs] def get_daily_data(today): last_time = get_last_submission_time() if not last_time: return [] data = [] data_start_date = date(last_time.year, last_time.month, last_time.day) data_end_date = date(today.year, today.month, today.day) - timedelta(days=1) for i, day in enumerate(daterange(data_start_date, data_end_date)): if i > settings.SHUUP_MAX_DAYS_IN_TELEMETRY: break data.append(get_daily_data_for_day(day)) return data
[docs] def get_telemetry_data(request, indent=None): """ Get the telemetry data that would be sent. :param request: HTTP request. Optional. :type request: django.http.HttpRequest|None :return: Data blob. :rtype: str """ admin_user = User.objects.first() data_dict = { "daily_data": get_daily_data(now()), "apps": settings.INSTALLED_APPS, "debug": settings.DEBUG, "host": (request.get_host() if request else None), "key": get_installation_key(), "machine": platform.machine(), "admin_user": admin_user.email if admin_user else None, "last_login": admin_user.last_login if admin_user else None, "platform": platform.platform(), "python_version": sys.version, "shuup_version": shuup.__version__, } return safe_json(data_dict, indent)
[docs] class TelemetryNotSent(Exception):
[docs] def __init__(self, message, code): self.message = message self.code = code super().__init__(message, code)
def _send_telemetry(request, max_age_hours, force_send=False): if not is_telemetry_enabled(): raise TelemetryNotSent("Error! Telemetry not enabled.", "disabled") if not force_send: if is_opt_out(): raise TelemetryNotSent("Error! Telemetry is opted-out.", "optout") if is_in_grace_period(): raise TelemetryNotSent("Error! Telemetry in grace period.", "grace") if max_age_hours is not None: last_send_time = get_last_submission_time() if last_send_time and (now() - last_send_time).total_seconds() <= max_age_hours * 60 * 60: raise TelemetryNotSent("Trying to resend too soon", "age") data = get_telemetry_data(request) try: resp = requests.post(url=settings.SHUUP_TELEMETRY_URL, data=data, timeout=5) if ( not settings.DEBUG and resp.status_code == 200 and resp.json().get("support_id") and not configuration.get(None, "shuup_support_id") ): configuration.set(None, "shuup_support_id", resp.json().get("support_id")) except Exception as exc: data = { "data": data, "error": force_text(exc), } else: data = { "data": data, "response": force_text(resp.content, errors="replace"), "status": resp.status_code, } save_telemetry_submission(data) return data
[docs] def try_send_telemetry(request=None, max_age_hours=24, raise_on_error=False): """ Send telemetry information (unless opted-out, in grace period or disabled). Telemetry will be always sent if there is no prior sending information. :param request: HTTP request. Optional. :type request: django.http.HttpRequest|None :param max_age_hours: How many hours must have passed since the last submission to be able to resend. If None, not checked. :type max_age_hours: int|None :param raise_on_error: Raise exceptions when telemetry is not sent instead of quietly returning False. :type raise_on_error: bool :return: Sent data (possibly with response information) or False if not sent. :rtype: dict|bool """ force_send = bool(not get_last_submission_time() or not settings.DEBUG) try: return _send_telemetry(request=request, max_age_hours=max_age_hours, force_send=force_send) except TelemetryNotSent: if raise_on_error: raise return False