Skip to content

Commit 5d391c1

Browse files
committed
Analytics: move page views to its own endpoint
This allow us to cache the footer response and keep receiving page views.
1 parent 9b25dbe commit 5d391c1

File tree

10 files changed

+116
-74
lines changed

10 files changed

+116
-74
lines changed

readthedocs/analytics/apps.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
# -*- coding: utf-8 -*-
2-
31
"""Django app config for the analytics app."""
42

53
from django.apps import AppConfig
@@ -11,6 +9,3 @@ class AnalyticsAppConfig(AppConfig):
119

1210
name = 'readthedocs.analytics'
1311
verbose_name = 'Analytics'
14-
15-
def ready(self):
16-
from . import signals # noqa

readthedocs/analytics/proxied_api.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from django.db.models import F
2+
from django.shortcuts import get_object_or_404
3+
from django.utils import timezone
4+
from rest_framework.response import Response
5+
from rest_framework.views import APIView
6+
7+
from readthedocs.analytics.models import PageView
8+
from readthedocs.api.v2.permissions import IsAuthorizedToViewVersion
9+
from readthedocs.core.unresolver import unresolve_from_request
10+
from readthedocs.core.utils.extend import SettingsOverrideObject
11+
from readthedocs.projects.models import Feature, Project
12+
13+
14+
class BaseAnalyticsView(APIView):
15+
16+
"""
17+
Track page views.
18+
19+
Query parameters:
20+
21+
- project
22+
- version
23+
- absolute_uri: Full path with domain.
24+
"""
25+
26+
http_method_names = ['get']
27+
permission_classes = [IsAuthorizedToViewVersion]
28+
29+
def _get_project(self):
30+
cache_key = '__cached_project'
31+
project = getattr(self, cache_key, None)
32+
33+
if not project:
34+
project_slug = self.request.GET.get('project')
35+
project = get_object_or_404(Project, slug=project_slug)
36+
setattr(self, cache_key, project)
37+
38+
return project
39+
40+
def _get_version(self):
41+
cache_key = '__cached_version'
42+
version = getattr(self, cache_key, None)
43+
44+
if not version:
45+
version_slug = self.request.GET.get('version')
46+
project = self._get_project()
47+
version = get_object_or_404(
48+
project.versions.all(),
49+
slug=version_slug,
50+
)
51+
setattr(self, cache_key, version)
52+
53+
return version
54+
55+
def get(self, request, **kwargs):
56+
project = self._get_project()
57+
version = self._get_version()
58+
absolute_uri = self.request.GET.get('absolute_uri')
59+
self.increase_page_view_count(
60+
request=request,
61+
project=project,
62+
version=version,
63+
absolute_uri=absolute_uri,
64+
)
65+
return Response(status=200)
66+
67+
def increase_page_view_count(self, request, project, version, absolute_uri):
68+
"""Increase the page view count for the given project."""
69+
if not absolute_uri or not project.has_feature(Feature.STORE_PAGEVIEWS):
70+
return
71+
72+
unresolved = unresolve_from_request(request, absolute_uri)
73+
if not unresolved:
74+
return
75+
76+
path = unresolved.filename
77+
78+
fields = dict(
79+
project=project,
80+
version=version,
81+
path=path,
82+
date=timezone.now().date(),
83+
)
84+
page_view = PageView.objects.filter(**fields).first()
85+
if page_view:
86+
page_view.view_count = F('view_count') + 1
87+
page_view.save(update_fields=['view_count'])
88+
else:
89+
PageView.objects.create(**fields, view_count=1)
90+
91+
92+
class AnalyticsView(SettingsOverrideObject):
93+
94+
_default_class = BaseAnalyticsView

readthedocs/analytics/signals.py

Lines changed: 0 additions & 44 deletions
This file was deleted.

readthedocs/analytics/tests.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ def setUp(self):
109109
self.absolute_uri = f'https://{self.project.slug}.readthedocs.io/en/latest/index.html'
110110
self.host = f'{self.project.slug}.readthedocs.io'
111111
self.url = (
112-
reverse('footer_html') +
113-
f'?project={self.project.slug}&version={self.version.slug}&page=index&docroot=/docs/' +
112+
reverse('analytics_api') +
113+
f'?project={self.project.slug}&version={self.version.slug}'
114114
f'&absolute_uri={self.absolute_uri}'
115115
)
116116

readthedocs/api/v2/proxied_urls.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
88
from django.conf import settings
99
from django.conf.urls import include, url
1010

11-
from .views.proxied import ProxiedFooterHTML
11+
from readthedocs.analytics.proxied_api import AnalyticsView
1212
from readthedocs.search.proxied_api import ProxiedPageSearchAPIView
1313

14+
from .views.proxied import ProxiedFooterHTML
15+
1416
api_footer_urls = [
1517
url(r'footer_html/', ProxiedFooterHTML.as_view(), name='footer_html'),
1618
url(r'search/$', ProxiedPageSearchAPIView.as_view(), name='search_api'),
19+
url(r'analytics/$', AnalyticsView.as_view(), name='analytics_api'),
1720
]
1821

1922
urlpatterns = api_footer_urls

readthedocs/api/v2/signals.py

Lines changed: 0 additions & 8 deletions
This file was deleted.

readthedocs/api/v2/views/footer_views.py

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@
1111
from rest_framework_jsonp.renderers import JSONPRenderer
1212

1313
from readthedocs.api.v2.permissions import IsAuthorizedToViewVersion
14-
from readthedocs.api.v2.signals import footer_response
1514
from readthedocs.builds.constants import LATEST, TAG
1615
from readthedocs.builds.models import Version
1716
from readthedocs.core.utils.extend import SettingsOverrideObject
1817
from readthedocs.projects.constants import MKDOCS, SPHINX_HTMLDIR
19-
from readthedocs.projects.models import Feature, Project
18+
from readthedocs.projects.models import Project
2019
from readthedocs.projects.version_handling import (
2120
highest_version,
2221
parse_version_failsafe,
@@ -243,16 +242,6 @@ def get(self, request, format=None):
243242
'version_supported': version.supported,
244243
}
245244

246-
# Allow folks to hook onto the footer response for various information
247-
# collection, or to modify the resp_data.
248-
footer_response.send(
249-
sender=None,
250-
request=request,
251-
context=context,
252-
response_data=resp_data,
253-
absolute_uri=self.request.GET.get('absolute_uri'),
254-
)
255-
256245
return Response(resp_data)
257246

258247

readthedocs/core/static-src/core/js/doc-embed/footer.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,19 @@ function init() {
8383
console.error('Error loading Read the Docs footer');
8484
}
8585
});
86+
87+
// Register page view.
88+
$.ajax({
89+
url: rtd.proxied_api_host + "/api/v2/analytics/",
90+
data: {
91+
project: rtd['project'],
92+
version: rtd['version'],
93+
absolute_uri: window.location.href,
94+
},
95+
error: function () {
96+
console.error('Error registering page view');
97+
}
98+
});
8699
}
87100

88101
module.exports = {

readthedocs/core/static/core/js/readthedocs-doc-embed.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

readthedocs/rtd_tests/tests/test_footer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ def test_highest_version_without_tags(self):
427427
class TestFooterPerformance(TestCase):
428428
# The expected number of queries for generating the footer
429429
# This shouldn't increase unless we modify the footer API
430-
EXPECTED_QUERIES = 18
430+
EXPECTED_QUERIES = 14
431431

432432
def setUp(self):
433433
self.pip = get(

0 commit comments

Comments
 (0)