зеркало из https://github.com/mozilla/kitsune.git
[bug 727086] Add unique visitors chart to KPI dashboard.
* Created a webtrends API helper. * Refactored existing API calls in dashboards app to use it for getting wiki reports. * Cron job to call webtrends API and save the data to the metrics model. * API call to get this data. * UI to display the chart.
This commit is contained in:
Родитель
791d26f076
Коммит
4a9aacc024
|
@ -1,6 +1,5 @@
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from urllib2 import HTTPBasicAuthHandler, build_opener
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
|
@ -11,22 +10,13 @@ from tower import ugettext_lazy as _lazy
|
||||||
from dashboards import THIS_WEEK, ALL_TIME, PERIODS
|
from dashboards import THIS_WEEK, ALL_TIME, PERIODS
|
||||||
from dashboards.personal import GROUP_DASHBOARDS
|
from dashboards.personal import GROUP_DASHBOARDS
|
||||||
from sumo.models import ModelBase
|
from sumo.models import ModelBase
|
||||||
|
from sumo.webtrends import Webtrends, StatsException
|
||||||
from wiki.models import Document
|
from wiki.models import Document
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger('k.dashboards')
|
log = logging.getLogger('k.dashboards')
|
||||||
|
|
||||||
|
|
||||||
class StatsException(Exception):
|
|
||||||
"""An error in the stats returned by the third-party analytics package"""
|
|
||||||
def __init__(self, msg):
|
|
||||||
self.msg = msg
|
|
||||||
|
|
||||||
|
|
||||||
class StatsIOError(IOError):
|
|
||||||
"""An error communicating with WebTrends"""
|
|
||||||
|
|
||||||
|
|
||||||
def period_dates():
|
def period_dates():
|
||||||
"""Return when each period begins and ends, relative to now.
|
"""Return when each period begins and ends, relative to now.
|
||||||
|
|
||||||
|
@ -119,27 +109,9 @@ class WikiDocumentVisits(ModelBase):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def json_for(cls, period):
|
def json_for(cls, period):
|
||||||
"""Return the JSON-formatted WebTrends stats for the given period.
|
"""Return the JSON-formatted WebTrends stats for the given period."""
|
||||||
|
|
||||||
Make one attempt to fetch and reload the data. If something fails, it's
|
|
||||||
the caller's responsibility to retry.
|
|
||||||
|
|
||||||
"""
|
|
||||||
auth_handler = HTTPBasicAuthHandler()
|
|
||||||
auth_handler.add_password(realm=settings.WEBTRENDS_REALM,
|
|
||||||
uri=settings.WEBTRENDS_WIKI_REPORT_URL,
|
|
||||||
user=settings.WEBTRENDS_USER,
|
|
||||||
passwd=settings.WEBTRENDS_PASSWORD)
|
|
||||||
opener = build_opener(auth_handler)
|
|
||||||
start, end = period_dates()[period]
|
start, end = period_dates()[period]
|
||||||
url = (settings.WEBTRENDS_WIKI_REPORT_URL +
|
return Webtrends.wiki_report(start, end)
|
||||||
'&start_period=%s&end_period=%s' % (start, end))
|
|
||||||
try:
|
|
||||||
# TODO: A wrong username or password results in a recursion depth
|
|
||||||
# error.
|
|
||||||
return opener.open(url).read()
|
|
||||||
except IOError, e:
|
|
||||||
raise StatsIOError(*e.args)
|
|
||||||
|
|
||||||
|
|
||||||
class GroupDashboard(ModelBase):
|
class GroupDashboard(ModelBase):
|
||||||
|
|
|
@ -4,9 +4,9 @@ from django.conf import settings
|
||||||
from mock import patch
|
from mock import patch
|
||||||
from nose.tools import raises, eq_
|
from nose.tools import raises, eq_
|
||||||
|
|
||||||
from dashboards.models import (WikiDocumentVisits, StatsException, THIS_WEEK,
|
from dashboards.models import WikiDocumentVisits, THIS_WEEK
|
||||||
StatsIOError)
|
|
||||||
from sumo.tests import TestCase
|
from sumo.tests import TestCase
|
||||||
|
from sumo.webtrends import StatsException, StatsIOError
|
||||||
from wiki.tests import document, revision
|
from wiki.tests import document, revision
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ from tastypie.cache import SimpleCache
|
||||||
from tastypie.resources import Resource
|
from tastypie.resources import Resource
|
||||||
|
|
||||||
from customercare.models import Reply
|
from customercare.models import Reply
|
||||||
from kpi.models import Metric, MetricKind
|
from kpi.models import Metric, MetricKind, VISITORS_METRIC_CODE
|
||||||
from questions.models import Question, Answer, AnswerVote
|
from questions.models import Question, Answer, AnswerVote
|
||||||
from wiki.models import HelpfulVote, Revision
|
from wiki.models import HelpfulVote, Revision
|
||||||
|
|
||||||
|
@ -167,7 +167,7 @@ class ElasticClickthroughResource(SearchClickthroughResource):
|
||||||
|
|
||||||
|
|
||||||
class SolutionResource(CachedResource):
|
class SolutionResource(CachedResource):
|
||||||
"""Returns the number of questions maked as the solution."""
|
"""Returns the number of questions marked as solved."""
|
||||||
date = fields.DateField('date')
|
date = fields.DateField('date')
|
||||||
solved = fields.IntegerField('solved', default=0)
|
solved = fields.IntegerField('solved', default=0)
|
||||||
questions = fields.IntegerField('questions', default=0)
|
questions = fields.IntegerField('questions', default=0)
|
||||||
|
@ -364,6 +364,24 @@ class ArmyOfAwesomeContributorResource(CachedResource):
|
||||||
allowed_methods = ['get']
|
allowed_methods = ['get']
|
||||||
|
|
||||||
|
|
||||||
|
class VisitorsResource(CachedResource):
|
||||||
|
"""Returns the number of unique visitors per day."""
|
||||||
|
date = fields.DateField('date')
|
||||||
|
visitors = fields.IntegerField('visitors', default=0)
|
||||||
|
|
||||||
|
def get_object_list(self, request):
|
||||||
|
# Set up the query for the data we need
|
||||||
|
kind = MetricKind.objects.get(code=VISITORS_METRIC_CODE)
|
||||||
|
qs = Metric.objects.filter(kind=kind).order_by('-start')
|
||||||
|
|
||||||
|
return [Struct(date=m.start, visitors=m.value) for m in qs]
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
cache = SimpleCache()
|
||||||
|
resource_name = 'kpi_visitors'
|
||||||
|
allowed_methods = ['get']
|
||||||
|
|
||||||
|
|
||||||
def _monthly_qs_for(model_cls):
|
def _monthly_qs_for(model_cls):
|
||||||
"""Return a queryset with the extra select for month and year."""
|
"""Return a queryset with the extra select for month and year."""
|
||||||
return model_cls.objects.filter(created__gte=_start_date()).extra(
|
return model_cls.objects.filter(created__gte=_start_date()).extra(
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
from datetime import datetime, date, timedelta
|
||||||
|
|
||||||
|
import cronjobs
|
||||||
|
|
||||||
|
from kpi.models import Metric, MetricKind, VISITORS_METRIC_CODE
|
||||||
|
from sumo.webtrends import Webtrends
|
||||||
|
|
||||||
|
|
||||||
|
@cronjobs.register
|
||||||
|
def update_visitors_metric():
|
||||||
|
"""Get new visitor data from webtrends and save."""
|
||||||
|
try:
|
||||||
|
# Get the latest metric value.
|
||||||
|
last_metric = Metric.objects.filter(
|
||||||
|
kind__code=VISITORS_METRIC_CODE).order_by('-start')[0]
|
||||||
|
# Start updating the day after the last updated.
|
||||||
|
start = last_metric.start + timedelta(days=1)
|
||||||
|
except IndexError:
|
||||||
|
# There are no metrics yet, start from 2011-01-01
|
||||||
|
start = date(2011, 01, 01)
|
||||||
|
|
||||||
|
# Collect up until yesterday
|
||||||
|
end = date.today() - timedelta(days=1)
|
||||||
|
|
||||||
|
# Get the visitor data from webtrends.
|
||||||
|
visitors = Webtrends.visits(start, end)
|
||||||
|
|
||||||
|
# Create the metrics.
|
||||||
|
metric_kind = MetricKind.objects.get(code=VISITORS_METRIC_CODE)
|
||||||
|
for date_str, visits in visitors.items():
|
||||||
|
day = datetime.strptime(date_str,"%Y-%m-%d").date()
|
||||||
|
Metric.objects.create(
|
||||||
|
kind=metric_kind,
|
||||||
|
start=day,
|
||||||
|
end=day + timedelta(days=1),
|
||||||
|
value=visits)
|
|
@ -4,6 +4,9 @@ from django.db.models import (CharField, DateField, ForeignKey,
|
||||||
from sumo.models import ModelBase
|
from sumo.models import ModelBase
|
||||||
|
|
||||||
|
|
||||||
|
VISITORS_METRIC_CODE = 'general keymetrics:visitors'
|
||||||
|
|
||||||
|
|
||||||
class MetricKind(ModelBase):
|
class MetricKind(ModelBase):
|
||||||
"""A programmer-readable identifier of a metric, like 'clicks: search'"""
|
"""A programmer-readable identifier of a metric, like 'clicks: search'"""
|
||||||
code = CharField(max_length=255, unique=True)
|
code = CharField(max_length=255, unique=True)
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
data-aoa-contributors-url="{{ url('api_dispatch_list', resource_name='kpi_active_aoa_contributors', api_name ='v1') }}"
|
data-aoa-contributors-url="{{ url('api_dispatch_list', resource_name='kpi_active_aoa_contributors', api_name ='v1') }}"
|
||||||
data-sphinx-ctr-url="{{ url('api_dispatch_list', resource_name='sphinx-clickthrough-rate', api_name ='v1') }}"
|
data-sphinx-ctr-url="{{ url('api_dispatch_list', resource_name='sphinx-clickthrough-rate', api_name ='v1') }}"
|
||||||
data-elastic-ctr-url="{{ url('api_dispatch_list', resource_name='elastic-clickthrough-rate', api_name ='v1') }}"
|
data-elastic-ctr-url="{{ url('api_dispatch_list', resource_name='elastic-clickthrough-rate', api_name ='v1') }}"
|
||||||
|
data-visitors-url="{{ url('api_dispatch_list', resource_name='kpi_visitors', api_name ='v1') }}"
|
||||||
data-vote-url="{{ url('api_dispatch_list', resource_name='kpi_vote', api_name ='v1') }}">
|
data-vote-url="{{ url('api_dispatch_list', resource_name='kpi_vote', api_name ='v1') }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import json
|
||||||
from nose.tools import eq_
|
from nose.tools import eq_
|
||||||
|
|
||||||
from customercare.tests import reply
|
from customercare.tests import reply
|
||||||
from kpi.models import Metric
|
from kpi.models import Metric, VISITORS_METRIC_CODE
|
||||||
from kpi.tests import metric, metric_kind
|
from kpi.tests import metric, metric_kind
|
||||||
from sumo.tests import TestCase, LocalizingClient
|
from sumo.tests import TestCase, LocalizingClient
|
||||||
from sumo.urlresolvers import reverse
|
from sumo.urlresolvers import reverse
|
||||||
|
@ -209,3 +209,20 @@ class KpiApiTests(TestCase):
|
||||||
|
|
||||||
# Correspnding ElasticSearch APIs are likely correct by dint
|
# Correspnding ElasticSearch APIs are likely correct by dint
|
||||||
# of factoring.
|
# of factoring.
|
||||||
|
|
||||||
|
def test_visitors(self):
|
||||||
|
"""Test unique visitors API call."""
|
||||||
|
# Create a reply
|
||||||
|
kind = metric_kind(code=VISITORS_METRIC_CODE, save=True)
|
||||||
|
metric(kind=kind, start=date.today(), end=date.today(), value=42,
|
||||||
|
save=True)
|
||||||
|
|
||||||
|
# There should be only one active contributor.
|
||||||
|
url = reverse('api_dispatch_list',
|
||||||
|
kwargs={'resource_name': 'kpi_visitors',
|
||||||
|
'api_name': 'v1'})
|
||||||
|
|
||||||
|
response = self.client.get(url + '?format=json')
|
||||||
|
eq_(200, response.status_code)
|
||||||
|
r = json.loads(response.content)
|
||||||
|
eq_(r['objects'][0]['visitors'], 42)
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from mock import patch
|
||||||
|
from nose.tools import eq_
|
||||||
|
|
||||||
|
from kpi.cron import update_visitors_metric, Webtrends
|
||||||
|
from kpi.models import Metric, VISITORS_METRIC_CODE
|
||||||
|
from kpi.tests import metric_kind
|
||||||
|
from sumo.tests import TestCase
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateVisitorsTests(TestCase):
|
||||||
|
@patch.object(Webtrends, 'visits')
|
||||||
|
def test_update_visitors_cron(self, visits):
|
||||||
|
"""Verify the cron job inserts the right rows."""
|
||||||
|
visitor_kind = metric_kind(code=VISITORS_METRIC_CODE, save=True)
|
||||||
|
visits.return_value = {'2012-01-13': 42,
|
||||||
|
'2012-01-14': 193,
|
||||||
|
'2012-01-15': 33}
|
||||||
|
|
||||||
|
update_visitors_metric()
|
||||||
|
|
||||||
|
metrics = Metric.objects.filter(kind=visitor_kind)
|
||||||
|
eq_(3, len(metrics))
|
||||||
|
eq_(42, metrics[0].value)
|
||||||
|
eq_(193, metrics[1].value)
|
||||||
|
eq_(date(2012, 01, 15), metrics[2].start)
|
|
@ -4,7 +4,7 @@ from tastypie.api import Api
|
||||||
from kpi.api import (SolutionResource, VoteResource, FastResponseResource,
|
from kpi.api import (SolutionResource, VoteResource, FastResponseResource,
|
||||||
ActiveKbContributorsResource, ActiveAnswerersResource,
|
ActiveKbContributorsResource, ActiveAnswerersResource,
|
||||||
SphinxClickthroughResource, ElasticClickthroughResource,
|
SphinxClickthroughResource, ElasticClickthroughResource,
|
||||||
ArmyOfAwesomeContributorResource)
|
ArmyOfAwesomeContributorResource, VisitorsResource)
|
||||||
|
|
||||||
v1_api = Api(api_name='v1')
|
v1_api = Api(api_name='v1')
|
||||||
v1_api.register(SolutionResource())
|
v1_api.register(SolutionResource())
|
||||||
|
@ -15,6 +15,7 @@ v1_api.register(ActiveAnswerersResource())
|
||||||
v1_api.register(SphinxClickthroughResource())
|
v1_api.register(SphinxClickthroughResource())
|
||||||
v1_api.register(ElasticClickthroughResource())
|
v1_api.register(ElasticClickthroughResource())
|
||||||
v1_api.register(ArmyOfAwesomeContributorResource())
|
v1_api.register(ArmyOfAwesomeContributorResource())
|
||||||
|
v1_api.register(VisitorsResource())
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = patterns('kpi.views',
|
urlpatterns = patterns('kpi.views',
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from mock import patch
|
||||||
|
from nose.tools import eq_
|
||||||
|
|
||||||
|
from sumo.tests import TestCase
|
||||||
|
from sumo.webtrends import Webtrends
|
||||||
|
|
||||||
|
|
||||||
|
KEY_METRICS_JSON_RESPONSE = '{ "definition" : { "accountID" : 123 , "profileID" : "ABC123" , "ID" : "ProfileMetrics" , "name" : "Profile Metrics" , "description" : "" , "language" : null , "timezone" : "UTC -1" , "dimensions" : [ { "ID" : "Profile ID" , "name" : "Profile ID" } ] , "measures" : [ { "name" : "PageViews" , "ID" : "PageViews" , "columnID" : 0 , "measureFormatType" : null },{ "name" : "Visits" , "ID" : "Visits" , "columnID" : 1 , "measureFormatType" : null },{ "name" : "Visitors" , "ID" : "Visitors" , "columnID" : 2 , "measureFormatType" : null },{ "name" : "NewVisitors" , "ID" : "NewVisitors" , "columnID" : 6 , "measureFormatType" : null },{ "name" : "BounceRate" , "ID" : "BounceRate" , "columnID" : 9 , "measureFormatType" : "percent" },{ "name" : "AvgTimeonSite" , "ID" : "AvgTimeOnSite" , "columnID" : 10 , "measureFormatType" : "time_seconds" },{ "name" : "AvgVisitorsperDay" , "ID" : "AvgVisitorsPerDay" , "columnID" : 11 , "measureFormatType" : null },{ "name" : "PageViewsperVisit" , "ID" : "PageViewsPerVisit" , "columnID" : 12 , "measureFormatType" : null },{ "name" : "AvgTimeonSiteperVisitor" , "ID" : "AvgSiteTimePerVisitor" , "columnID" : 13 , "measureFormatType" : "time_seconds" } ] } ,"data" : [ { "ABC123" : { "attributes" : { } , "measures" : { "PageViews" : 10523913 , "Visits" : 4274456 , "Visitors" : 4044284 , "NewVisitors" : 2078406 , "BounceRate" : 65.4970831375969 , "AvgTimeonSite" : 274.68279239068 , "AvgVisitorsperDay" : 577754.857142857 , "PageViewsperVisit" : 2.46204733421048 , "AvgTimeonSiteperVisitor" : 96.5800146577243 } , "SubRows" : [ { "period" : "Day" , "start_date" : "2012-01-01" , "end_date" : "2012-01-01" , "measures" : { "PageViews" : 1258143 , "Visits" : 524606 , "Visitors" : 495974 , "NewVisitors" : 254034 , "BounceRate" : 63.9746781394037 , "AvgTimeonSite" : 276.706781356731 , "AvgVisitorsperDay" : 495974 , "PageViewsperVisit" : 2.39826269619486 , "AvgTimeonSiteperVisitor" : 97.029864468702 } , "SubRows" : null } , { "period" : "Day" , "start_date" : "2012-01-02" , "end_date" : "2012-01-02" , "measures" : { "PageViews" : 1576014 , "Visits" : 649237 , "Visitors" : 614465 , "NewVisitors" : 320101 , "BounceRate" : 65.3790526417934 , "AvgTimeonSite" : 275.88984425623 , "AvgVisitorsperDay" : 614465 , "PageViewsperVisit" : 2.427486418673 , "AvgTimeonSiteperVisitor" : 97.7010993303117 } , "SubRows" : null } , { "period" : "Day" , "start_date" : "2012-01-03" , "end_date" : "2012-01-03" , "measures" : { "PageViews" : 1628215 , "Visits" : 664809 , "Visitors" : 629484 , "NewVisitors" : 326187 , "BounceRate" : 65.9521757376931 , "AvgTimeonSite" : 274.289682249817 , "AvgVisitorsperDay" : 629484 , "PageViewsperVisit" : 2.44914704824995 , "AvgTimeonSiteperVisitor" : 95.4439064376537 } , "SubRows" : null } , { "period" : "Day" , "start_date" : "2012-01-04" , "end_date" : "2012-01-04" , "measures" : { "PageViews" : 1622072 , "Visits" : 648066 , "Visitors" : 613411 , "NewVisitors" : 315226 , "BounceRate" : 65.8422136017011 , "AvgTimeonSite" : 272.315030946065 , "AvgVisitorsperDay" : 613411 , "PageViewsperVisit" : 2.50294260152515 , "AvgTimeonSiteperVisitor" : 95.3973388152478 } , "SubRows" : null } , { "period" : "Day" , "start_date" : "2012-01-05" , "end_date" : "2012-01-05" , "measures" : { "PageViews" : 1561292 , "Visits" : 619564 , "Visitors" : 585760 , "NewVisitors" : 298016 , "BounceRate" : 65.9460523852257 , "AvgTimeonSite" : 274.38456436392 , "AvgVisitorsperDay" : 585760 , "PageViewsperVisit" : 2.51998502172495 , "AvgTimeonSiteperVisitor" : 96.0481818492215 } , "SubRows" : null } , { "period" : "Day" , "start_date" : "2012-01-06" , "end_date" : "2012-01-06" , "measures" : { "PageViews" : 1520252 , "Visits" : 608327 , "Visitors" : 575889 , "NewVisitors" : 294054 , "BounceRate" : 65.7352049144621 , "AvgTimeonSite" : 271.518740910547 , "AvgVisitorsperDay" : 575889 , "PageViewsperVisit" : 2.49907040128089 , "AvgTimeonSiteperVisitor" : 95.6368657848995 } , "SubRows" : null } , { "period" : "Day" , "start_date" : "2012-01-07" , "end_date" : "2012-01-07" , "measures" : { "PageViews" : 1357925 , "Visits" : 559847 , "Visitors" : 529301 , "NewVisitors" : 270788 , "BounceRate" : 65.3650015093409 , "AvgTimeonSite" : 278.304308416466 , "AvgVisitorsperDay" : 529301 , "PageViewsperVisit" : 2.42552876053636 , "AvgTimeonSiteperVisitor" : 99.1935042631697 } , "SubRows" : null } ] } } ] }'
|
||||||
|
|
||||||
|
|
||||||
|
class WebtrendsTests(TestCase):
|
||||||
|
"""Tests for the Webtrends API helper."""
|
||||||
|
|
||||||
|
@patch.object(Webtrends, 'key_metrics')
|
||||||
|
def test_visits(self, key_metrics):
|
||||||
|
"""Test Webtrends.visits()."""
|
||||||
|
key_metrics.return_value = KEY_METRICS_JSON_RESPONSE
|
||||||
|
|
||||||
|
visits = Webtrends.visits(date(2012, 01, 01), date(2012, 01, 07))
|
||||||
|
|
||||||
|
eq_(7, len(visits))
|
||||||
|
eq_(495974, visits['2012-01-01'])
|
||||||
|
eq_(529301, visits['2012-01-07'])
|
|
@ -0,0 +1,82 @@
|
||||||
|
from datetime import datetime, date
|
||||||
|
import json
|
||||||
|
from urllib2 import HTTPBasicAuthHandler, build_opener
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from sumo.helpers import urlparams
|
||||||
|
|
||||||
|
|
||||||
|
class StatsException(Exception):
|
||||||
|
"""An error in the stats returned by the third-party analytics package"""
|
||||||
|
def __init__(self, msg):
|
||||||
|
self.msg = msg
|
||||||
|
|
||||||
|
|
||||||
|
class StatsIOError(IOError):
|
||||||
|
"""An error communicating with WebTrends"""
|
||||||
|
|
||||||
|
|
||||||
|
class Webtrends(object):
|
||||||
|
"""Webtrends API helper."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def request(cls, url, start, end, realm='Webtrends Basic Authentication'):
|
||||||
|
"""Make an authed request to the webtrends API.
|
||||||
|
|
||||||
|
Make one attempt to fetch and reload the data. If something fails, it's
|
||||||
|
the caller's responsibility to retry.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# If start and/or end are date or datetime, convert to string.
|
||||||
|
if isinstance(start, (date, datetime)):
|
||||||
|
start = start.strftime('%Ym%md%d')
|
||||||
|
if isinstance(end, (date, datetime)):
|
||||||
|
end = end.strftime('%Ym%md%d')
|
||||||
|
|
||||||
|
auth_handler = HTTPBasicAuthHandler()
|
||||||
|
auth_handler.add_password(realm=realm,
|
||||||
|
uri=url,
|
||||||
|
user=settings.WEBTRENDS_USER,
|
||||||
|
passwd=settings.WEBTRENDS_PASSWORD)
|
||||||
|
opener = build_opener(auth_handler)
|
||||||
|
url = urlparams(url, start_period=start, end_period=end)
|
||||||
|
try:
|
||||||
|
# TODO: A wrong username or password results in a recursion depth
|
||||||
|
# error.
|
||||||
|
return opener.open(url).read()
|
||||||
|
except IOError, e:
|
||||||
|
raise StatsIOError(*e.args)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def wiki_report(cls, start, end):
|
||||||
|
"""Return the json for the wiki article visits report."""
|
||||||
|
return cls.request(settings.WEBTRENDS_WIKI_REPORT_URL, start, end)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def key_metrics(cls, start, end):
|
||||||
|
"""Return the json result for the KeyMetrics API call."""
|
||||||
|
url = ('https://ws.webtrends.com/v3/Reporting/profiles/{profile_id}'
|
||||||
|
'/KeyMetrics/?period_type=trend')
|
||||||
|
url = url.format(profile_id=settings.WEBTRENDS_PROFILE_ID)
|
||||||
|
return cls.request(url, start, end, realm='DX')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def visits(cls, start, end):
|
||||||
|
"""Return the number of unique visitors.
|
||||||
|
|
||||||
|
Returns a dict with daily numbers:
|
||||||
|
{u'2012-01-22': 404971,
|
||||||
|
u'2012-01-23': 434618,
|
||||||
|
u'2012-01-24': 501687,...}
|
||||||
|
"""
|
||||||
|
data = json.loads(cls.key_metrics(start, end))
|
||||||
|
rows = data['data'][0][settings.WEBTRENDS_PROFILE_ID]['SubRows']
|
||||||
|
if not isinstance(rows, list):
|
||||||
|
rows = [rows]
|
||||||
|
|
||||||
|
visits = {}
|
||||||
|
for row in rows:
|
||||||
|
visits[row['start_date']] = row['measures']['Visitors']
|
||||||
|
|
||||||
|
return visits
|
|
@ -193,8 +193,6 @@ window.StockChartView = Backbone.View.extend({
|
||||||
style: {
|
style: {
|
||||||
width: 200
|
width: 200
|
||||||
},
|
},
|
||||||
yDecimals: 1,
|
|
||||||
ySuffix: '%',
|
|
||||||
shared: true,
|
shared: true,
|
||||||
pointFormat: '<span style="color:{series.color}">{series.prettyName}</span>: <b>{point.y}</b><br/>'
|
pointFormat: '<span style="color:{series.color}">{series.prettyName}</span>: <b>{point.y}</b><br/>'
|
||||||
},
|
},
|
||||||
|
@ -217,6 +215,14 @@ window.StockChartView = Backbone.View.extend({
|
||||||
},
|
},
|
||||||
series: []
|
series: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.options.percent) {
|
||||||
|
this.chartOptions.yAxis.title = {
|
||||||
|
text: '%'
|
||||||
|
};
|
||||||
|
this.chartOptions.tooltip.ySuffix = '%';
|
||||||
|
this.chartOptions.tooltip.yDecimals = 1;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
@ -225,47 +231,68 @@ window.StockChartView = Backbone.View.extend({
|
||||||
|
|
||||||
if(data) {
|
if(data) {
|
||||||
_.each(this.options.series, function(series) {
|
_.each(this.options.series, function(series) {
|
||||||
var seriesData;
|
var mapper = series.mapper,
|
||||||
|
seriesData;
|
||||||
|
if (!mapper) {
|
||||||
|
mapper = function(o){
|
||||||
|
return {
|
||||||
|
x: Date.parse(o['date']),
|
||||||
|
y: o[series.numerator] / o[series.denominator] * 100
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
seriesData = _.map(data, function(o){
|
seriesData = _.map(data, mapper);
|
||||||
return [Date.parse(o['date']),
|
|
||||||
o[series.numerator] / o[series.denominator] * 100];
|
|
||||||
});
|
|
||||||
seriesData.reverse();
|
seriesData.reverse();
|
||||||
|
|
||||||
// Add the series with 3 different possible groupings:
|
if (!series.addGroupings) {
|
||||||
// daily, weekly, monthly
|
self.chartOptions.series.push({
|
||||||
self.chartOptions.series.push({
|
name: series.name,
|
||||||
name: gettext('Daily'),
|
data: seriesData,
|
||||||
data: seriesData,
|
dataGrouping: {
|
||||||
dataGrouping: {
|
enabled: false
|
||||||
enabled: false
|
}
|
||||||
}
|
});
|
||||||
});
|
} else {
|
||||||
self.chartOptions.series.push({
|
// Add the series with 3 different possible groupings:
|
||||||
name: gettext('Weekly'),
|
// daily, weekly, monthly
|
||||||
data: seriesData,
|
self.chartOptions.series.push({
|
||||||
dataGrouping: {
|
name: gettext('Daily'),
|
||||||
forced: true,
|
data: seriesData,
|
||||||
units: [['week', [1]]]
|
dataGrouping: {
|
||||||
}
|
enabled: false
|
||||||
});
|
}
|
||||||
self.chartOptions.series.push({
|
});
|
||||||
name: gettext('Monthly'),
|
self.chartOptions.series.push({
|
||||||
data: seriesData,
|
name: gettext('Weekly'),
|
||||||
dataGrouping: {
|
data: seriesData,
|
||||||
forced: true,
|
dataGrouping: {
|
||||||
units: [['month', [1]]]
|
forced: true,
|
||||||
}
|
units: [['week', [1]]]
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
self.chartOptions.series.push({
|
||||||
|
name: gettext('Monthly'),
|
||||||
|
data: seriesData,
|
||||||
|
dataGrouping: {
|
||||||
|
forced: true,
|
||||||
|
units: [['month', [1]]]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
self.chart = new Highcharts.StockChart(self.chartOptions);
|
self.chart = new Highcharts.StockChart(self.chartOptions);
|
||||||
|
|
||||||
self.chart.series[0].prettyName = self.chart.series[1].prettyName = self.chart.series[2].prettyName = series.name;
|
if (!series.addGroupings) {
|
||||||
|
self.chart.series[0].prettyName = series.name;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.chart.series[0].prettyName = self.chart.series[1].prettyName = self.chart.series[2].prettyName = series.name;
|
||||||
|
|
||||||
// Hide the weekly and monthly series.
|
// Hide the weekly and monthly series.
|
||||||
self.chart.series[1].hide();
|
self.chart.series[1].hide();
|
||||||
self.chart.series[2].hide();
|
self.chart.series[2].hide();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
|
@ -313,6 +340,10 @@ window.KpiDashboard = Backbone.View.extend({
|
||||||
});
|
});
|
||||||
this.elasticCtrChart.name = 'Elastic';
|
this.elasticCtrChart.name = 'Elastic';
|
||||||
|
|
||||||
|
this.visitorsChart = new ChartModel([], {
|
||||||
|
url: $(this.el).data('visitors-url')
|
||||||
|
});
|
||||||
|
|
||||||
// Create the views.
|
// Create the views.
|
||||||
this.solvedChartView = new StockChartView({
|
this.solvedChartView = new StockChartView({
|
||||||
model: this.solvedChart,
|
model: this.solvedChart,
|
||||||
|
@ -321,7 +352,8 @@ window.KpiDashboard = Backbone.View.extend({
|
||||||
series: [{
|
series: [{
|
||||||
name: gettext('Solved'),
|
name: gettext('Solved'),
|
||||||
numerator: 'solved',
|
numerator: 'solved',
|
||||||
denominator: 'questions'
|
denominator: 'questions',
|
||||||
|
addGroupings: true
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -356,7 +388,8 @@ window.KpiDashboard = Backbone.View.extend({
|
||||||
series: [{
|
series: [{
|
||||||
name: gettext('Responsed'),
|
name: gettext('Responsed'),
|
||||||
numerator: 'responded',
|
numerator: 'responded',
|
||||||
denominator: 'questions'
|
denominator: 'questions',
|
||||||
|
addGroupings: true
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -425,6 +458,20 @@ window.KpiDashboard = Backbone.View.extend({
|
||||||
});
|
});
|
||||||
this.ctrView.addModel(this.elasticCtrChart);
|
this.ctrView.addModel(this.elasticCtrChart);
|
||||||
|
|
||||||
|
this.visitorsView = new StockChartView({
|
||||||
|
model: this.visitorsChart,
|
||||||
|
title: gettext('Daily Unique Visitors'),
|
||||||
|
series: [{
|
||||||
|
name: gettext('Visitors'),
|
||||||
|
mapper: function(o) {
|
||||||
|
return {
|
||||||
|
x: Date.parse(o['date']),
|
||||||
|
y: o['visitors']
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
// Render the views.
|
// Render the views.
|
||||||
$(this.el)
|
$(this.el)
|
||||||
.append(this.solvedChartView.render().el)
|
.append(this.solvedChartView.render().el)
|
||||||
|
@ -433,7 +480,8 @@ window.KpiDashboard = Backbone.View.extend({
|
||||||
.append(this.activeKbContributorsView.render().el)
|
.append(this.activeKbContributorsView.render().el)
|
||||||
.append(this.activeAnswerersView.render().el)
|
.append(this.activeAnswerersView.render().el)
|
||||||
.append(this.aoaContributorsView.render().el)
|
.append(this.aoaContributorsView.render().el)
|
||||||
.append(this.ctrView.render().el);
|
.append(this.ctrView.render().el)
|
||||||
|
.append(this.visitorsView.render().el);
|
||||||
|
|
||||||
|
|
||||||
// Load up the models.
|
// Load up the models.
|
||||||
|
@ -445,6 +493,7 @@ window.KpiDashboard = Backbone.View.extend({
|
||||||
this.voteChart.fetch();
|
this.voteChart.fetch();
|
||||||
this.sphinxCtrChart.fetch();
|
this.sphinxCtrChart.fetch();
|
||||||
this.elasticCtrChart.fetch();
|
this.elasticCtrChart.fetch();
|
||||||
|
this.visitorsChart.fetch();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
insert into kpi_metrickind (code) values
|
||||||
|
('general keymetrics:visitors');
|
|
@ -28,6 +28,7 @@ HOME = /tmp
|
||||||
42 0 * * * {{ cron }} update_top_contributors
|
42 0 * * * {{ cron }} update_top_contributors
|
||||||
0 21 * * * {{ cron }} cache_most_unhelpful_kb_articles
|
0 21 * * * {{ cron }} cache_most_unhelpful_kb_articles
|
||||||
47 2 * * * {{ cron }} remove_expired_registration_profiles
|
47 2 * * * {{ cron }} remove_expired_registration_profiles
|
||||||
|
0 9 * * * {{ cron }} update_visitors_metric
|
||||||
|
|
||||||
# Twice per week.
|
# Twice per week.
|
||||||
#05 01 * * 1,4 {{ cron }} update_weekly_votes
|
#05 01 * * 1,4 {{ cron }} update_weekly_votes
|
||||||
|
|
|
@ -71,6 +71,7 @@ HOME = /tmp
|
||||||
42 0 * * * $CRON update_top_contributors
|
42 0 * * * $CRON update_top_contributors
|
||||||
0 21 * * * $CRON cache_most_unhelpful_kb_articles
|
0 21 * * * $CRON cache_most_unhelpful_kb_articles
|
||||||
47 2 * * * $CRON remove_expired_registration_profiles
|
47 2 * * * $CRON remove_expired_registration_profiles
|
||||||
|
0 9 * * * $CRON update_visitors_metric
|
||||||
|
|
||||||
# Twice per week.
|
# Twice per week.
|
||||||
#05 01 * * 1,4 $CRON update_weekly_votes
|
#05 01 * * 1,4 $CRON update_weekly_votes
|
||||||
|
|
|
@ -28,6 +28,7 @@ HOME = /tmp
|
||||||
42 0 * * * cd /data/www/support.mozilla.com/kitsune; /usr/bin/python26 manage.py cron update_top_contributors
|
42 0 * * * cd /data/www/support.mozilla.com/kitsune; /usr/bin/python26 manage.py cron update_top_contributors
|
||||||
0 21 * * * cd /data/www/support.mozilla.com/kitsune; /usr/bin/python26 manage.py cron cache_most_unhelpful_kb_articles
|
0 21 * * * cd /data/www/support.mozilla.com/kitsune; /usr/bin/python26 manage.py cron cache_most_unhelpful_kb_articles
|
||||||
47 2 * * * cd /data/www/support.mozilla.com/kitsune; /usr/bin/python26 manage.py cron remove_expired_registration_profiles
|
47 2 * * * cd /data/www/support.mozilla.com/kitsune; /usr/bin/python26 manage.py cron remove_expired_registration_profiles
|
||||||
|
0 9 * * * cd /data/www/support.mozilla.com/kitsune; /usr/bin/python26 manage.py cron update_visitors_metric
|
||||||
|
|
||||||
# Twice per week.
|
# Twice per week.
|
||||||
#05 01 * * 1,4 cd /data/www/support.mozilla.com/kitsune; /usr/bin/python26 manage.py cron update_weekly_votes
|
#05 01 * * 1,4 cd /data/www/support.mozilla.com/kitsune; /usr/bin/python26 manage.py cron update_weekly_votes
|
||||||
|
|
|
@ -28,6 +28,7 @@ HOME = /tmp
|
||||||
42 0 * * * cd /data/www/support.allizom.org/kitsune; /usr/bin/python26 manage.py cron update_top_contributors
|
42 0 * * * cd /data/www/support.allizom.org/kitsune; /usr/bin/python26 manage.py cron update_top_contributors
|
||||||
0 21 * * * cd /data/www/support.allizom.org/kitsune; /usr/bin/python26 manage.py cron cache_most_unhelpful_kb_articles
|
0 21 * * * cd /data/www/support.allizom.org/kitsune; /usr/bin/python26 manage.py cron cache_most_unhelpful_kb_articles
|
||||||
47 2 * * * cd /data/www/support.allizom.org/kitsune; /usr/bin/python26 manage.py cron remove_expired_registration_profiles
|
47 2 * * * cd /data/www/support.allizom.org/kitsune; /usr/bin/python26 manage.py cron remove_expired_registration_profiles
|
||||||
|
0 9 * * * cd /data/www/support.allizom.org/kitsune; /usr/bin/python26 manage.py cron update_visitors_metric
|
||||||
|
|
||||||
# Twice per week.
|
# Twice per week.
|
||||||
#05 01 * * 1,4 cd /data/www/support.allizom.org/kitsune; /usr/bin/python26 manage.py cron update_weekly_votes
|
#05 01 * * 1,4 cd /data/www/support.allizom.org/kitsune; /usr/bin/python26 manage.py cron update_weekly_votes
|
||||||
|
|
|
@ -28,6 +28,7 @@ HOME = /tmp
|
||||||
42 0 * * * cd /data/www/support-release.allizom.org/kitsune; /usr/bin/python26 manage.py cron update_top_contributors
|
42 0 * * * cd /data/www/support-release.allizom.org/kitsune; /usr/bin/python26 manage.py cron update_top_contributors
|
||||||
0 21 * * * cd /data/www/support-release.allizom.org/kitsune; /usr/bin/python26 manage.py cron cache_most_unhelpful_kb_articles
|
0 21 * * * cd /data/www/support-release.allizom.org/kitsune; /usr/bin/python26 manage.py cron cache_most_unhelpful_kb_articles
|
||||||
47 2 * * * cd /data/www/support-release.allizom.org/kitsune; /usr/bin/python26 manage.py cron remove_expired_registration_profiles
|
47 2 * * * cd /data/www/support-release.allizom.org/kitsune; /usr/bin/python26 manage.py cron remove_expired_registration_profiles
|
||||||
|
0 9 * * * cd /data/www/support-release.allizom.org/kitsune; /usr/bin/python26 manage.py cron update_visitors_metric
|
||||||
|
|
||||||
# Twice per week.
|
# Twice per week.
|
||||||
#05 01 * * 1,4 cd /data/www/support-release.allizom.org/kitsune; /usr/bin/python26 manage.py cron update_weekly_votes
|
#05 01 * * 1,4 cd /data/www/support-release.allizom.org/kitsune; /usr/bin/python26 manage.py cron update_weekly_votes
|
||||||
|
|
|
@ -769,12 +769,12 @@ TIDINGS_REVERSE = 'sumo.urlresolvers.reverse'
|
||||||
CHAT_SERVER = 'https://chat-support.mozilla.com:9091'
|
CHAT_SERVER = 'https://chat-support.mozilla.com:9091'
|
||||||
CHAT_CACHE_KEY = 'sumo-chat-queue-status'
|
CHAT_CACHE_KEY = 'sumo-chat-queue-status'
|
||||||
|
|
||||||
|
WEBTRENDS_PROFILE_ID = 'ABC123' # Profile id for SUMO
|
||||||
WEBTRENDS_WIKI_REPORT_URL = 'https://example.com/see_production.rst'
|
WEBTRENDS_WIKI_REPORT_URL = 'https://example.com/see_production.rst'
|
||||||
WEBTRENDS_USER = r'someaccount\someusername'
|
WEBTRENDS_USER = r'someaccount\someusername'
|
||||||
WEBTRENDS_PASSWORD = 'password'
|
WEBTRENDS_PASSWORD = 'password'
|
||||||
WEBTRENDS_EPOCH = date(2010, 8, 1) # When WebTrends started gathering stats on
|
WEBTRENDS_EPOCH = date(2010, 8, 1) # When WebTrends started gathering stats on
|
||||||
# the KB
|
# the KB
|
||||||
WEBTRENDS_REALM = 'Webtrends Basic Authentication'
|
|
||||||
|
|
||||||
MOBILE_COOKIE = 'msumo'
|
MOBILE_COOKIE = 'msumo'
|
||||||
|
|
||||||
|
|
|
@ -20,3 +20,6 @@ REDIS_BACKENDS = {
|
||||||
'karma': 'redis://localhost:6383?socket_timeout=0.5&db=2',
|
'karma': 'redis://localhost:6383?socket_timeout=0.5&db=2',
|
||||||
'helpfulvotes': 'redis://localhost:6383?socket_timeout=0.5&db=2',
|
'helpfulvotes': 'redis://localhost:6383?socket_timeout=0.5&db=2',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Use fake webtrends settings.
|
||||||
|
WEBTRENDS_PROFILE_ID = 'ABC123'
|
||||||
|
|
Загрузка…
Ссылка в новой задаче