Skip to content

Commit 0ef140a

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 0ef140a

File tree

10 files changed

+120
-74
lines changed

10 files changed

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