66import structlog
77from django .http import JsonResponse
88from django .shortcuts import get_object_or_404
9- from rest_framework import status
9+ from rest_framework import status as status_codes
1010from rest_framework .response import Response
1111from rest_framework .views import APIView
1212
1313from readthedocs .analytics .models import PageView
1414from readthedocs .api .v2 .permissions import IsAuthorizedToViewVersion
1515from readthedocs .core .mixins import CDNCacheControlMixin
16+ from readthedocs .core .unresolver import InvalidPathForVersionedProjectError
1617from readthedocs .core .unresolver import UnresolverError
17- from readthedocs .core .unresolver import unresolve
18+ from readthedocs .core .unresolver import unresolver
1819from readthedocs .core .utils .extend import SettingsOverrideObject
1920from readthedocs .core .utils .requests import is_suspicious_request
2021from readthedocs .projects .models import Project
22+ from readthedocs .proxito .views .hosting import IsAuthorizedToViewProject
2123
2224
2325log = structlog .get_logger (__name__ ) # noqa
@@ -38,7 +40,7 @@ class BaseAnalyticsView(CDNCacheControlMixin, APIView):
3840 # so we capture all views/interactions.
3941 cache_response = False
4042 http_method_names = ["get" ]
41- permission_classes = [IsAuthorizedToViewVersion ]
43+ permission_classes = [IsAuthorizedToViewProject | IsAuthorizedToViewVersion ]
4244
4345 @lru_cache (maxsize = 1 )
4446 def _get_project (self ):
@@ -50,31 +52,38 @@ def _get_project(self):
5052 def _get_version (self ):
5153 version_slug = self .request .GET .get ("version" )
5254 project = self ._get_project ()
53- version = get_object_or_404 (
54- project .versions .all (),
55- slug = version_slug ,
56- )
55+ # Do not call `get_object_or_404` because there may be some invalid URLs without versions.
56+ # We do want to track those 404 pages as well. In that case, the `filename` attribute is the `path`.
57+ version = project .versions .filter (slug = version_slug ).first ()
5758 return version
5859
5960 def get (self , request , * args , ** kwargs ):
6061 # TODO: Use absolute_uri only, we don't need project and version.
6162 project = self ._get_project ()
6263 version = self ._get_version ()
6364 absolute_uri = self .request .GET .get ("absolute_uri" )
65+ status = self .request .GET .get ("status" , "200" )
6466 if not absolute_uri :
6567 return JsonResponse (
6668 {"error" : "'absolute_uri' GET attribute is required" },
67- status = status .HTTP_400_BAD_REQUEST ,
69+ status = status_codes .HTTP_400_BAD_REQUEST ,
70+ )
71+
72+ if status not in ("200" , "404" ):
73+ return JsonResponse (
74+ {"error" : "'status' GET attribute should be 200 or 404" },
75+ status = status_codes .HTTP_400_BAD_REQUEST ,
6876 )
6977
7078 self .increase_page_view_count (
7179 project = project ,
7280 version = version ,
7381 absolute_uri = absolute_uri ,
82+ status = status ,
7483 )
75- return Response (status = status .HTTP_204_NO_CONTENT )
84+ return Response (status = status_codes .HTTP_204_NO_CONTENT )
7685
77- def increase_page_view_count (self , project , version , absolute_uri ):
86+ def increase_page_view_count (self , project , version , absolute_uri , status ):
7887 """Increase the page view count for the given project."""
7988 if is_suspicious_request (self .request ):
8089 log .info (
@@ -83,28 +92,49 @@ def increase_page_view_count(self, project, version, absolute_uri):
8392 )
8493 return
8594
95+ # Don't track 200 if the version doesn't exist
96+ if status == "200" and not version :
97+ return
98+
8699 # Don't allow tracking page views from external domains.
87100 if self .request .unresolved_domain .is_from_external_domain :
88101 return
89102
103+ # Don't track external versions.
104+ if version and version .is_external :
105+ return
106+
107+ absolute_uri_parsed = urlparse (absolute_uri )
90108 try :
91- unresolved = unresolve (absolute_uri )
109+ unresolved = unresolver .unresolve_url (absolute_uri )
110+ filename = unresolved .filename
111+ absolute_uri_project = unresolved .project
112+ except InvalidPathForVersionedProjectError as exc :
113+ # If the version is missing, we still want to log this request.
114+ #
115+ # If we don't have a version, the filename is the path,
116+ # otherwise it would be empty.
117+ filename = exc .path
118+ absolute_uri_project = exc .project
92119 except UnresolverError :
93120 # If we were unable to resolve the URL, it
94121 # isn't pointing to a valid RTD project.
95122 return
96123
97- # Don't track external versions.
98- if version .is_external or not unresolved .filename :
124+ if absolute_uri_project .slug != project .slug :
125+ log .warning (
126+ "Skipping page view count since projects don't match" ,
127+ project_slug = project .slug ,
128+ uri_project_slug = absolute_uri_project .slug ,
129+ )
99130 return
100131
101- path = urlparse (absolute_uri ).path
102132 PageView .objects .register_page_view (
103133 project = project ,
104134 version = version ,
105- filename = unresolved . filename ,
106- path = path ,
107- status = 200 ,
135+ filename = filename ,
136+ path = absolute_uri_parsed . path ,
137+ status = status ,
108138 )
109139
110140
0 commit comments