зеркало из 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 logging
|
||||
from urllib2 import HTTPBasicAuthHandler, build_opener
|
||||
|
||||
from django.conf import settings
|
||||
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.personal import GROUP_DASHBOARDS
|
||||
from sumo.models import ModelBase
|
||||
from sumo.webtrends import Webtrends, StatsException
|
||||
from wiki.models import Document
|
||||
|
||||
|
||||
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():
|
||||
"""Return when each period begins and ends, relative to now.
|
||||
|
||||
|
@ -119,27 +109,9 @@ class WikiDocumentVisits(ModelBase):
|
|||
|
||||
@classmethod
|
||||
def json_for(cls, 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)
|
||||
"""Return the JSON-formatted WebTrends stats for the given period."""
|
||||
start, end = period_dates()[period]
|
||||
url = (settings.WEBTRENDS_WIKI_REPORT_URL +
|
||||
'&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)
|
||||
return Webtrends.wiki_report(start, end)
|
||||
|
||||
|
||||
class GroupDashboard(ModelBase):
|
||||
|
|
|
@ -4,9 +4,9 @@ from django.conf import settings
|
|||
from mock import patch
|
||||
from nose.tools import raises, eq_
|
||||
|
||||
from dashboards.models import (WikiDocumentVisits, StatsException, THIS_WEEK,
|
||||
StatsIOError)
|
||||
from dashboards.models import WikiDocumentVisits, THIS_WEEK
|
||||
from sumo.tests import TestCase
|
||||
from sumo.webtrends import StatsException, StatsIOError
|
||||
from wiki.tests import document, revision
|
||||
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ from tastypie.cache import SimpleCache
|
|||
from tastypie.resources import Resource
|
||||
|
||||
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 wiki.models import HelpfulVote, Revision
|
||||
|
||||
|
@ -167,7 +167,7 @@ class ElasticClickthroughResource(SearchClickthroughResource):
|
|||
|
||||
|
||||
class SolutionResource(CachedResource):
|
||||
"""Returns the number of questions maked as the solution."""
|
||||
"""Returns the number of questions marked as solved."""
|
||||
date = fields.DateField('date')
|
||||
solved = fields.IntegerField('solved', default=0)
|
||||
questions = fields.IntegerField('questions', default=0)
|
||||
|
@ -364,6 +364,24 @@ class ArmyOfAwesomeContributorResource(CachedResource):
|
|||
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):
|
||||
"""Return a queryset with the extra select for month and year."""
|
||||
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
|
||||
|
||||
|
||||
VISITORS_METRIC_CODE = 'general keymetrics:visitors'
|
||||
|
||||
|
||||
class MetricKind(ModelBase):
|
||||
"""A programmer-readable identifier of a metric, like 'clicks: search'"""
|
||||
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-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-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') }}">
|
||||
</div>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import json
|
|||
from nose.tools import eq_
|
||||
|
||||
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 sumo.tests import TestCase, LocalizingClient
|
||||
from sumo.urlresolvers import reverse
|
||||
|
@ -209,3 +209,20 @@ class KpiApiTests(TestCase):
|
|||
|
||||
# Correspnding ElasticSearch APIs are likely correct by dint
|
||||
# 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,
|
||||
ActiveKbContributorsResource, ActiveAnswerersResource,
|
||||
SphinxClickthroughResource, ElasticClickthroughResource,
|
||||
ArmyOfAwesomeContributorResource)
|
||||
ArmyOfAwesomeContributorResource, VisitorsResource)
|
||||
|
||||
v1_api = Api(api_name='v1')
|
||||
v1_api.register(SolutionResource())
|
||||
|
@ -15,6 +15,7 @@ v1_api.register(ActiveAnswerersResource())
|
|||
v1_api.register(SphinxClickthroughResource())
|
||||
v1_api.register(ElasticClickthroughResource())
|
||||
v1_api.register(ArmyOfAwesomeContributorResource())
|
||||
v1_api.register(VisitorsResource())
|
||||
|
||||
|
||||
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: {
|
||||
width: 200
|
||||
},
|
||||
yDecimals: 1,
|
||||
ySuffix: '%',
|
||||
shared: true,
|
||||
pointFormat: '<span style="color:{series.color}">{series.prettyName}</span>: <b>{point.y}</b><br/>'
|
||||
},
|
||||
|
@ -217,6 +215,14 @@ window.StockChartView = Backbone.View.extend({
|
|||
},
|
||||
series: []
|
||||
};
|
||||
|
||||
if (this.options.percent) {
|
||||
this.chartOptions.yAxis.title = {
|
||||
text: '%'
|
||||
};
|
||||
this.chartOptions.tooltip.ySuffix = '%';
|
||||
this.chartOptions.tooltip.yDecimals = 1;
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
@ -225,47 +231,68 @@ window.StockChartView = Backbone.View.extend({
|
|||
|
||||
if(data) {
|
||||
_.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){
|
||||
return [Date.parse(o['date']),
|
||||
o[series.numerator] / o[series.denominator] * 100];
|
||||
});
|
||||
seriesData = _.map(data, mapper);
|
||||
seriesData.reverse();
|
||||
|
||||
// Add the series with 3 different possible groupings:
|
||||
// daily, weekly, monthly
|
||||
self.chartOptions.series.push({
|
||||
name: gettext('Daily'),
|
||||
data: seriesData,
|
||||
dataGrouping: {
|
||||
enabled: false
|
||||
}
|
||||
});
|
||||
self.chartOptions.series.push({
|
||||
name: gettext('Weekly'),
|
||||
data: seriesData,
|
||||
dataGrouping: {
|
||||
forced: true,
|
||||
units: [['week', [1]]]
|
||||
}
|
||||
});
|
||||
self.chartOptions.series.push({
|
||||
name: gettext('Monthly'),
|
||||
data: seriesData,
|
||||
dataGrouping: {
|
||||
forced: true,
|
||||
units: [['month', [1]]]
|
||||
}
|
||||
});
|
||||
if (!series.addGroupings) {
|
||||
self.chartOptions.series.push({
|
||||
name: series.name,
|
||||
data: seriesData,
|
||||
dataGrouping: {
|
||||
enabled: false
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Add the series with 3 different possible groupings:
|
||||
// daily, weekly, monthly
|
||||
self.chartOptions.series.push({
|
||||
name: gettext('Daily'),
|
||||
data: seriesData,
|
||||
dataGrouping: {
|
||||
enabled: false
|
||||
}
|
||||
});
|
||||
self.chartOptions.series.push({
|
||||
name: gettext('Weekly'),
|
||||
data: seriesData,
|
||||
dataGrouping: {
|
||||
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.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.
|
||||
self.chart.series[1].hide();
|
||||
self.chart.series[2].hide();
|
||||
// Hide the weekly and monthly series.
|
||||
self.chart.series[1].hide();
|
||||
self.chart.series[2].hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
return this;
|
||||
|
@ -313,6 +340,10 @@ window.KpiDashboard = Backbone.View.extend({
|
|||
});
|
||||
this.elasticCtrChart.name = 'Elastic';
|
||||
|
||||
this.visitorsChart = new ChartModel([], {
|
||||
url: $(this.el).data('visitors-url')
|
||||
});
|
||||
|
||||
// Create the views.
|
||||
this.solvedChartView = new StockChartView({
|
||||
model: this.solvedChart,
|
||||
|
@ -321,7 +352,8 @@ window.KpiDashboard = Backbone.View.extend({
|
|||
series: [{
|
||||
name: gettext('Solved'),
|
||||
numerator: 'solved',
|
||||
denominator: 'questions'
|
||||
denominator: 'questions',
|
||||
addGroupings: true
|
||||
}]
|
||||
});
|
||||
|
||||
|
@ -356,7 +388,8 @@ window.KpiDashboard = Backbone.View.extend({
|
|||
series: [{
|
||||
name: gettext('Responsed'),
|
||||
numerator: 'responded',
|
||||
denominator: 'questions'
|
||||
denominator: 'questions',
|
||||
addGroupings: true
|
||||
}]
|
||||
});
|
||||
|
||||
|
@ -425,6 +458,20 @@ window.KpiDashboard = Backbone.View.extend({
|
|||
});
|
||||
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.
|
||||
$(this.el)
|
||||
.append(this.solvedChartView.render().el)
|
||||
|
@ -433,7 +480,8 @@ window.KpiDashboard = Backbone.View.extend({
|
|||
.append(this.activeKbContributorsView.render().el)
|
||||
.append(this.activeAnswerersView.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.
|
||||
|
@ -445,6 +493,7 @@ window.KpiDashboard = Backbone.View.extend({
|
|||
this.voteChart.fetch();
|
||||
this.sphinxCtrChart.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
|
||||
0 21 * * * {{ cron }} cache_most_unhelpful_kb_articles
|
||||
47 2 * * * {{ cron }} remove_expired_registration_profiles
|
||||
0 9 * * * {{ cron }} update_visitors_metric
|
||||
|
||||
# Twice per week.
|
||||
#05 01 * * 1,4 {{ cron }} update_weekly_votes
|
||||
|
|
|
@ -71,6 +71,7 @@ HOME = /tmp
|
|||
42 0 * * * $CRON update_top_contributors
|
||||
0 21 * * * $CRON cache_most_unhelpful_kb_articles
|
||||
47 2 * * * $CRON remove_expired_registration_profiles
|
||||
0 9 * * * $CRON update_visitors_metric
|
||||
|
||||
# Twice per week.
|
||||
#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
|
||||
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
|
||||
0 9 * * * cd /data/www/support.mozilla.com/kitsune; /usr/bin/python26 manage.py cron update_visitors_metric
|
||||
|
||||
# Twice per week.
|
||||
#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
|
||||
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
|
||||
0 9 * * * cd /data/www/support.allizom.org/kitsune; /usr/bin/python26 manage.py cron update_visitors_metric
|
||||
|
||||
# Twice per week.
|
||||
#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
|
||||
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
|
||||
0 9 * * * cd /data/www/support-release.allizom.org/kitsune; /usr/bin/python26 manage.py cron update_visitors_metric
|
||||
|
||||
# Twice per week.
|
||||
#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_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_USER = r'someaccount\someusername'
|
||||
WEBTRENDS_PASSWORD = 'password'
|
||||
WEBTRENDS_EPOCH = date(2010, 8, 1) # When WebTrends started gathering stats on
|
||||
# the KB
|
||||
WEBTRENDS_REALM = 'Webtrends Basic Authentication'
|
||||
|
||||
MOBILE_COOKIE = 'msumo'
|
||||
|
||||
|
|
|
@ -20,3 +20,6 @@ REDIS_BACKENDS = {
|
|||
'karma': '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'
|
||||
|
|
Загрузка…
Ссылка в новой задаче