Skip to content

Commit

Permalink
Merge pull request #26 from SolidStateGroup/feature/get-org-usage
Browse files Browse the repository at this point in the history
Feature/get org usage
  • Loading branch information
matthewelwell authored May 15, 2019
2 parents a0e814c + 948df49 commit 31e6b16
Show file tree
Hide file tree
Showing 19 changed files with 446 additions and 236 deletions.
2 changes: 2 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ numpy = "*"
django-simple-history = "*"
twisted = {version = "*", extras = ["tls"]}
django-debug-toolbar = "*"
google-api-python-client = "*"
"oauth2client" = "*"
499 changes: 267 additions & 232 deletions Pipfile.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ The application relies on the following environment variables to run:
* `DATABASE_URL`: required by develop and master environments, should be a standard format database url e.g. postgres://user:password@host:port/db_name
* `DJANGO_SECRET_KEY`: see 'Creating a secret key' section below
* `GOOGLE_ANALYTICS_KEY`: if google analytics is required, add your tracking code
* `GOOGLE_SERVICE_ACCOUNT`: service account json for accessing the google API, used for getting usage of an organisation - needs access to analytics.readonly scope
* `GA_TABLE_ID`: GA table ID (view) to query when looking for organisation usage

### Creating a secret key
It is important to also set an environment variable on whatever platform you are using for
Expand Down
2 changes: 1 addition & 1 deletion src/analytics/middleware.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from threading import Thread

from .utils import track_request
from .track import track_request


class GoogleAnalyticsMiddleware:
Expand Down
37 changes: 37 additions & 0 deletions src/analytics/query.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import json

from apiclient.discovery import build
from django.conf import settings
from google.oauth2 import service_account

GA_SCOPES = ['https://www.googleapis.com/auth/analytics.readonly']
GA_API_NAME = 'analytics'
GA_API_VERSION = 'v3'


def get_service():
"""
Get the google service object to use to query the API
"""
credentials = service_account.Credentials.from_service_account_info(
json.loads(settings.GOOGLE_SERVICE_ACCOUNT), scopes=GA_SCOPES)

# Build the service object.
return build(GA_API_NAME, GA_API_VERSION, credentials=credentials)


def get_events_for_organisation(organisation):
"""
Get number of tracked events for last 30 days for an organisation
:return: number of events as integer
"""
ga_response = get_service().data().ga().get(
ids=settings.GA_TABLE_ID,
start_date='30daysAgo',
end_date='today',
metrics='ga:totalEvents',
dimensions='ga:date',
filters=f'ga:eventCategory=={organisation.get_unique_slug()}').execute()

return int(ga_response['totalsForAllResults']['ga:totalEvents'])
4 changes: 4 additions & 0 deletions src/analytics/utils.py → src/analytics/track.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,20 @@
GOOGLE_ANALYTICS_BATCH_URL = GOOGLE_ANALYTICS_BASE_URL + "/batch"
DEFAULT_DATA = "v=1&tid=" + settings.GOOGLE_ANALYTICS_KEY


def postpone(function):
def decorator(*args, **kwargs):
t = Thread(target = function, args=args, kwargs=kwargs)
t.daemon = True
t.start()
return decorator


@postpone
def post_async(url, data):
requests.post(GOOGLE_ANALYTICS_COLLECT_URL, data=data)


def track_request(uri):
"""
Utility function to track a request to the API with the specified URI
Expand All @@ -31,6 +34,7 @@ def track_request(uri):
data = DEFAULT_DATA + "t=pageview&dp=" + quote(uri, safe='')
post_async(GOOGLE_ANALYTICS_COLLECT_URL, data=data)


def track_event(category, action, label='', value=''):
data = DEFAULT_DATA + "&t=event" + \
"&ec=" + category + \
Expand Down
7 changes: 7 additions & 0 deletions src/app/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@

# Google Analytics Configuration
GOOGLE_ANALYTICS_KEY = os.environ.get('GOOGLE_ANALYTICS_KEY', '')
GOOGLE_SERVICE_ACCOUNT = os.environ.get('GOOGLE_SERVICE_ACCOUNT')
if not GOOGLE_SERVICE_ACCOUNT:
warnings.warn("GOOGLE_SERVICE_ACCOUNT not configured, getting organisation usage will not work")
GA_TABLE_ID = os.environ.get('GA_TABLE_ID')
if not GA_TABLE_ID:
warnings.warn("GA_TABLE_ID not configured, getting organisation usage will not work")

if 'DJANGO_ALLOWED_HOSTS' in os.environ:
ALLOWED_HOSTS = os.environ['DJANGO_ALLOWED_HOSTS'].split(',')
Expand Down Expand Up @@ -81,6 +87,7 @@
'projects',
'environments',
'features',
'segments',
'rest_framework_swagger',
'docs',
'e2etests',
Expand Down
26 changes: 26 additions & 0 deletions src/features/migrations/0012_auto_20190424_1555.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-04-24 15:55
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('features', '0011_historicalfeature_squashed_0012_historicalfeaturestate_historicalfeaturestatevalue'),
]

operations = [
migrations.AlterField(
model_name='historicalfeature',
name='project',
field=models.ForeignKey(blank=True, db_constraint=False, help_text='Changing the project selected will remove previous Feature States for the previously associated projects Environments that are related to this Feature. New default Feature States will be created for the new selected projects Environments for this Feature.', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='projects.Project'),
),
migrations.AlterField(
model_name='historicalfeaturestate',
name='identity',
field=models.ForeignKey(blank=True, db_constraint=False, default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='environments.Identity'),
),
]
6 changes: 3 additions & 3 deletions src/features/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from rest_framework.response import Response
from rest_framework.schemas import AutoSchema

from analytics.utils import track_event
from analytics.track import track_event
from environments.models import Environment, Identity
from projects.models import Project
from .models import FeatureState, Feature
Expand Down Expand Up @@ -224,14 +224,14 @@ def get(self, request, identifier=None, *args, **kwargs):
return Response(error_response, status=status.HTTP_400_BAD_REQUEST)

if identifier:
track_event(environment.project.organisation.name, "identity_flags")
track_event(environment.project.organisation.get_unique_slug(), "identity_flags")

identity, _ = Identity.objects.get_or_create(
identifier=identifier,
environment=environment,
)
else:
track_event(environment.project.organisation.name, "flags")
track_event(environment.project.organisation.get_unique_slug(), "flags")
identity = None

kwargs = {
Expand Down
4 changes: 4 additions & 0 deletions src/organisations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ class Organisation(models.Model):
free_to_use_subscription = models.BooleanField(default=True)
plan = models.CharField(max_length=20, null=True, blank=True)
subscription_date = models.DateTimeField('SubscriptionDate', blank=True, null=True)

class Meta:
ordering = ['id']

def __str__(self):
return "Org %s" % self.name

def get_unique_slug(self):
return str(self.id) + "-" + self.name
15 changes: 15 additions & 0 deletions src/organisations/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from rest_framework.exceptions import ValidationError
from rest_framework.response import Response

from analytics.query import get_events_for_organisation
from projects.serializers import ProjectSerializer
from organisations.serializers import OrganisationSerializer
from users.models import Invite
Expand Down Expand Up @@ -74,6 +75,20 @@ def invite(self, request, pk):
else:
raise ValidationError(invites_serializer.errors)

@action(detail=True, methods=["GET"])
def usage(self, request, pk):
organisation = self.get_object()

try:
events = get_events_for_organisation(organisation)
except (TypeError, ValueError):
# TypeError can be thrown when getting service account if not configured
# ValueError can be thrown if GA returns a value that cannot be converted to integer
return Response({"error": "Couldn't get number of events for organisation."},
status=status.HTTP_500_INTERNAL_SERVER_ERROR)

return Response({"events": events}, status=status.HTTP_200_OK)


class InviteViewSet(viewsets.ModelViewSet):
serializer_class = InviteListSerializer
Expand Down
Empty file added src/segments/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions src/segments/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.contrib import admin

# Register your models here.
5 changes: 5 additions & 0 deletions src/segments/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class SegmentsConfig(AppConfig):
name = 'segments'
35 changes: 35 additions & 0 deletions src/segments/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-04-24 15:55
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='Segment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=2000)),
('description', models.TextField(blank=True, null=True)),
],
),
migrations.CreateModel(
name='SegmentCondition',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('trait_key', models.CharField(max_length=200)),
('condition_type', models.CharField(choices=[('ExactMatch', 'ExactMatch'), ('GreaterThan', 'GreaterThan'), ('LessThan', 'LessThan')], default='ExactMatch', max_length=20)),
('match_value', models.TextField(blank=True, null=True)),
('segment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='segment_conditions', to='segments.Segment')),
],
),
]
Empty file.
29 changes: 29 additions & 0 deletions src/segments/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from django.db import models

from django.utils.encoding import python_2_unicode_compatible

# Condition Operation Value Types
EXACT_MATCH = "ExactMatch"
GREATER_THAN = "GreaterThan"
LESS_THAN = "LessThan"

@python_2_unicode_compatible
class Segment(models.Model):
name = models.CharField(max_length=2000)
description = models.TextField(null=True, blank=True)

@python_2_unicode_compatible
class SegmentCondition(models.Model):
CONDITION_OPERATION_TYPES = (
(EXACT_MATCH, 'ExactMatch'),
(GREATER_THAN, 'GreaterThan'),
(LESS_THAN, 'LessThan')
)

segment = models.ForeignKey(Segment, related_name='segment_conditions')
trait_key = models.CharField(max_length=200)
condition_type = models.CharField(max_length=20, choices=CONDITION_OPERATION_TYPES, default=EXACT_MATCH,
null=False, blank=False)
match_value = models.TextField(null=True, blank=True)

3 changes: 3 additions & 0 deletions src/segments/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.test import TestCase

# Create your tests here.
3 changes: 3 additions & 0 deletions src/segments/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.shortcuts import render

# Create your views here.

0 comments on commit 31e6b16

Please sign in to comment.