[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:
Ricky Rosario 2012-02-23 16:12:10 -05:00
Родитель 791d26f076
Коммит 4a9aacc024
20 изменённых файлов: 318 добавлений и 77 удалений

Просмотреть файл

@ -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(

36
apps/kpi/cron.py Normal file
Просмотреть файл

@ -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'])

82
apps/sumo/webtrends.py Normal file
Просмотреть файл

@ -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'