-
Notifications
You must be signed in to change notification settings - Fork 429
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(analytics/usage-data): Add ability to store usage data in Postgr…
…es (#1849) * feat(analytics-tracking): add data model * update datamodel * Add tasks * merge migrations * use different database for analytics data * Add middleware for storing analytics locally * update get_from_resource_name * skip tests if analytics database is not configured * Add analytics db url * Add analytics environments variables to processor * use a different model to register recurring tasks * Add validation check for overlapping bucket * Add recurring task to admin * Add tests for recurring tasks * Add test for the decorator * update migrate_analytics_db to migrate only if db exists * Add comment
- Loading branch information
1 parent
627ef8a
commit 6b21513
Showing
45 changed files
with
2,114 additions
and
109 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
from datetime import date, timedelta | ||
from typing import List | ||
|
||
from app_analytics.dataclasses import FeatureEvaluationData, UsageData | ||
from app_analytics.influxdb_wrapper import get_events_for_organisation | ||
from app_analytics.influxdb_wrapper import ( | ||
get_feature_evaluation_data as get_feature_evaluation_data_from_influxdb, | ||
) | ||
from app_analytics.influxdb_wrapper import ( | ||
get_usage_data as get_usage_data_from_influxdb, | ||
) | ||
from app_analytics.models import ( | ||
APIUsageBucket, | ||
FeatureEvaluationBucket, | ||
Resource, | ||
) | ||
from django.conf import settings | ||
from django.db.models import Sum | ||
from django.utils import timezone | ||
|
||
from environments.models import Environment | ||
from features.models import Feature | ||
|
||
ANALYTICS_READ_BUCKET_SIZE = 15 | ||
|
||
|
||
def get_usage_data( | ||
organisation, environment_id=None, project_id=None | ||
) -> List[UsageData]: | ||
if settings.USE_POSTGRES_FOR_ANALYTICS: | ||
return get_usage_data_from_local_db( | ||
organisation, environment_id=environment_id, project_id=project_id | ||
) | ||
return get_usage_data_from_influxdb( | ||
organisation, environment_id=environment_id, project_id=project_id | ||
) | ||
|
||
|
||
def get_usage_data_from_local_db( | ||
organisation, environment_id=None, project_id=None, period: int = 30 | ||
) -> List[UsageData]: | ||
qs = APIUsageBucket.objects.filter( | ||
environment_id__in=_get_environment_ids_for_org(organisation), | ||
bucket_size=ANALYTICS_READ_BUCKET_SIZE, | ||
) | ||
if project_id: | ||
qs = qs.filter(project_id=project_id) | ||
if environment_id: | ||
qs = qs.filter(environment_id=environment_id) | ||
|
||
qs = ( | ||
qs.filter( | ||
created_at__date__lte=timezone.now(), | ||
created_at__date__gt=timezone.now() - timedelta(days=30), | ||
) | ||
.order_by("created_at") | ||
.values("created_at__date", "resource") | ||
.annotate(count=Sum("total_count")) | ||
) | ||
data_by_day = {} | ||
for row in qs: | ||
day = row["created_at__date"] | ||
if day not in data_by_day: | ||
data_by_day[day] = UsageData(day=day) | ||
setattr( | ||
data_by_day[day], | ||
Resource.get_lowercased_name(row["resource"]), | ||
row["count"], | ||
) | ||
|
||
return data_by_day.values() | ||
|
||
|
||
def get_total_events_count(organisation) -> int: | ||
""" | ||
Return total number of events for an organisation in the last 30 days | ||
""" | ||
if settings.USE_POSTGRES_FOR_ANALYTICS: | ||
count = APIUsageBucket.objects.filter( | ||
environment_id__in=_get_environment_ids_for_org(organisation), | ||
created_at__date__lte=date.today(), | ||
created_at__date__gt=date.today() - timedelta(days=30), | ||
bucket_size=ANALYTICS_READ_BUCKET_SIZE, | ||
).aggregate(total_count=Sum("total_count"))["total_count"] | ||
else: | ||
count = get_events_for_organisation(organisation.id) | ||
return count | ||
|
||
|
||
def get_feature_evaluation_data( | ||
feature: Feature, environment_id: int, period: int = 30 | ||
) -> List[FeatureEvaluationData]: | ||
if settings.USE_POSTGRES_FOR_ANALYTICS: | ||
return get_feature_evaluation_data_from_local_db( | ||
feature, environment_id, period | ||
) | ||
return get_feature_evaluation_data_from_influxdb( | ||
feature_name=feature.name, environment_id=environment_id, period=f"{period}d" | ||
) | ||
|
||
|
||
def get_feature_evaluation_data_from_local_db( | ||
feature: Feature, environment_id: int, period: int = 30 | ||
) -> List[FeatureEvaluationData]: | ||
feature_evaluation_data = ( | ||
FeatureEvaluationBucket.objects.filter( | ||
environment_id=environment_id, | ||
bucket_size=ANALYTICS_READ_BUCKET_SIZE, | ||
created_at__date__lte=timezone.now(), | ||
created_at__date__gt=timezone.now() - timedelta(days=period), | ||
) | ||
.order_by("created_at") | ||
.values("created_at__date", "feature_name", "environment_id") | ||
.annotate(count=Sum("total_count")) | ||
) | ||
usage_list = [] | ||
for data in feature_evaluation_data: | ||
usage_list.append( | ||
FeatureEvaluationData( | ||
day=data["created_at__date"], | ||
count=data["count"], | ||
) | ||
) | ||
return usage_list | ||
|
||
|
||
def _get_environment_ids_for_org(organisation) -> List[int]: | ||
# We need to do this to prevent Django from generating a query that | ||
# references the environments and projects tables, | ||
# as they do not exist in the analytics database. | ||
return [ | ||
e.id for e in Environment.objects.filter(project__organisation=organisation) | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from django.apps import AppConfig | ||
|
||
|
||
class AppAnalyticsConfig(AppConfig): | ||
default_auto_field = "django.db.models.BigAutoField" | ||
name = "app_analytics" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
from dataclasses import dataclass | ||
from datetime import date | ||
|
||
|
||
@dataclass | ||
class UsageData: | ||
day: date | ||
flags: int = 0 | ||
traits: int = 0 | ||
identities: int = 0 | ||
environment_document: int = 0 | ||
|
||
|
||
@dataclass | ||
class FeatureEvaluationData: | ||
day: date | ||
count: int = 0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
from marshmallow import EXCLUDE, Schema, fields, post_load, pre_load | ||
|
||
from .dataclasses import FeatureEvaluationData, UsageData | ||
|
||
|
||
class FeatureEvaluationDataSchema(Schema): | ||
count = fields.Integer(allow_none=True) | ||
day = fields.Date(allow_none=True) | ||
|
||
class Meta: | ||
unknown = EXCLUDE | ||
|
||
@pre_load | ||
def preprocess(self, data, **kwargs): | ||
# the data returned by influx db looks like this: | ||
# { | ||
# "datetime": "2021-01-01", | ||
# "some_feature_name": 10 | ||
# } | ||
day = data.pop("datetime") | ||
# Use popitem because we don't know the name of the feature | ||
# and it's the only item left in the dict | ||
_, count = data.popitem() | ||
return {"day": day, "count": count} | ||
|
||
@post_load | ||
def make_usage_data(self, data, **kwargs): | ||
return FeatureEvaluationData(**data) | ||
|
||
|
||
class UsageDataSchema(Schema): | ||
flags = fields.Integer(data_key="Flags", allow_none=True) | ||
traits = fields.Integer(data_key="Traits", allow_none=True) | ||
identities = fields.Integer(data_key="Identities", allow_none=True) | ||
environment_document = fields.Integer( | ||
data_key="Environment-document", allow_none=True | ||
) | ||
day = fields.Date(data_key="name", allow_none=True) | ||
|
||
class Meta: | ||
unknown = EXCLUDE | ||
|
||
@post_load | ||
def make_usage_data(self, data, **kwargs): | ||
return UsageData(**data) |
Oops, something went wrong.