Skip to content

Analytics: move page views to its own endpoint #7739

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 10, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions readthedocs/analytics/apps.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-

"""Django app config for the analytics app."""

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

name = 'readthedocs.analytics'
verbose_name = 'Analytics'

def ready(self):
from . import signals # noqa
98 changes: 98 additions & 0 deletions readthedocs/analytics/proxied_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""Analytics views that are severd from the same domain as the docs."""

from django.db.models import F
from django.shortcuts import get_object_or_404
from django.utils import timezone
from rest_framework.response import Response
from rest_framework.views import APIView

from readthedocs.analytics.models import PageView
from readthedocs.api.v2.permissions import IsAuthorizedToViewVersion
from readthedocs.core.unresolver import unresolve_from_request
from readthedocs.core.utils.extend import SettingsOverrideObject
from readthedocs.projects.models import Feature, Project


class BaseAnalyticsView(APIView):

"""
Track page views.

Query parameters:

- project
- version
- absolute_uri: Full path with domain.
"""

http_method_names = ['get']
permission_classes = [IsAuthorizedToViewVersion]

def _get_project(self):
cache_key = '__cached_project'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have this code in a few places now, we should probably move it to a mixin.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do another PR with that

project = getattr(self, cache_key, None)

if not project:
project_slug = self.request.GET.get('project')
project = get_object_or_404(Project, slug=project_slug)
setattr(self, cache_key, project)

return project

def _get_version(self):
cache_key = '__cached_version'
version = getattr(self, cache_key, None)

if not version:
version_slug = self.request.GET.get('version')
project = self._get_project()
version = get_object_or_404(
project.versions.all(),
slug=version_slug,
)
setattr(self, cache_key, version)

return version

# pylint: disable=unused-argument
def get(self, request, *args, **kwargs):
project = self._get_project()
version = self._get_version()
absolute_uri = self.request.GET.get('absolute_uri')
self.increase_page_view_count(
request=request,
project=project,
version=version,
absolute_uri=absolute_uri,
)
return Response(status=200)

# pylint: disable=no-self-use
def increase_page_view_count(self, request, project, version, absolute_uri):
"""Increase the page view count for the given project."""
if not absolute_uri or not project.has_feature(Feature.STORE_PAGEVIEWS):
return

unresolved = unresolve_from_request(request, absolute_uri)
if not unresolved:
return

path = unresolved.filename

fields = dict(
project=project,
version=version,
path=path,
date=timezone.now().date(),
)
page_view = PageView.objects.filter(**fields).first()
if page_view:
page_view.view_count = F('view_count') + 1
page_view.save(update_fields=['view_count'])
else:
PageView.objects.create(**fields, view_count=1)


class AnalyticsView(SettingsOverrideObject):

_default_class = BaseAnalyticsView
44 changes: 0 additions & 44 deletions readthedocs/analytics/signals.py

This file was deleted.

4 changes: 2 additions & 2 deletions readthedocs/analytics/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ def setUp(self):
self.absolute_uri = f'https://{self.project.slug}.readthedocs.io/en/latest/index.html'
self.host = f'{self.project.slug}.readthedocs.io'
self.url = (
reverse('footer_html') +
f'?project={self.project.slug}&version={self.version.slug}&page=index&docroot=/docs/' +
reverse('analytics_api') +
f'?project={self.project.slug}&version={self.version.slug}'
f'&absolute_uri={self.absolute_uri}'
)

Expand Down
5 changes: 4 additions & 1 deletion readthedocs/api/v2/proxied_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@
from django.conf import settings
from django.conf.urls import include, url

from .views.proxied import ProxiedFooterHTML
from readthedocs.analytics.proxied_api import AnalyticsView
from readthedocs.search.proxied_api import ProxiedPageSearchAPIView

from .views.proxied import ProxiedFooterHTML

api_footer_urls = [
url(r'footer_html/', ProxiedFooterHTML.as_view(), name='footer_html'),
url(r'search/$', ProxiedPageSearchAPIView.as_view(), name='search_api'),
url(r'analytics/$', AnalyticsView.as_view(), name='analytics_api'),
]

urlpatterns = api_footer_urls
Expand Down
8 changes: 0 additions & 8 deletions readthedocs/api/v2/signals.py

This file was deleted.

13 changes: 1 addition & 12 deletions readthedocs/api/v2/views/footer_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@
from rest_framework_jsonp.renderers import JSONPRenderer

from readthedocs.api.v2.permissions import IsAuthorizedToViewVersion
from readthedocs.api.v2.signals import footer_response
from readthedocs.builds.constants import LATEST, TAG
from readthedocs.builds.models import Version
from readthedocs.core.utils.extend import SettingsOverrideObject
from readthedocs.projects.constants import MKDOCS, SPHINX_HTMLDIR
from readthedocs.projects.models import Feature, Project
from readthedocs.projects.models import Project
from readthedocs.projects.version_handling import (
highest_version,
parse_version_failsafe,
Expand Down Expand Up @@ -243,16 +242,6 @@ def get(self, request, format=None):
'version_supported': version.supported,
}

# Allow folks to hook onto the footer response for various information
# collection, or to modify the resp_data.
footer_response.send(
sender=None,
request=request,
context=context,
response_data=resp_data,
absolute_uri=self.request.GET.get('absolute_uri'),
)

Comment on lines -246 to -255
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we keep sending this signal here?

We were only using it for pageviews here, but it could be useful for other things as well. Besides, it may have an impact downstream.

Copy link
Member Author

@stsewd stsewd Dec 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think this is useful, it also hides additional queries. The class can be extended already with OverrideSettginsObject, that's better than using signals for this case.

@readthedocs/core thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My only worry is about custom installs that might be using it, but I don't expect it to be heavily used there. We plan to cache it in prod, but not all users will necessarily do that. I think I'm OK removing it, and we can re-add it if someone yells :)

return Response(resp_data)


Expand Down
13 changes: 13 additions & 0 deletions readthedocs/core/static-src/core/js/doc-embed/footer.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,19 @@ function init() {
console.error('Error loading Read the Docs footer');
}
});

// Register page view.
$.ajax({
url: rtd.proxied_api_host + "/api/v2/analytics/",
data: {
project: rtd['project'],
version: rtd['version'],
absolute_uri: window.location.href,
},
error: function () {
console.error('Error registering page view');
}
});
}

module.exports = {
Expand Down
2 changes: 1 addition & 1 deletion readthedocs/core/static/core/js/readthedocs-doc-embed.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion readthedocs/rtd_tests/tests/test_footer.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ def test_highest_version_without_tags(self):
class TestFooterPerformance(TestCase):
# The expected number of queries for generating the footer
# This shouldn't increase unless we modify the footer API
EXPECTED_QUERIES = 18
EXPECTED_QUERIES = 14
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️


def setUp(self):
self.pip = get(
Expand Down