6
6
import structlog
7
7
from django .http import JsonResponse
8
8
from django .shortcuts import get_object_or_404
9
- from rest_framework import status
9
+ from rest_framework import status as status_codes
10
10
from rest_framework .response import Response
11
11
from rest_framework .views import APIView
12
12
13
13
from readthedocs .analytics .models import PageView
14
14
from readthedocs .api .v2 .permissions import IsAuthorizedToViewVersion
15
15
from readthedocs .core .mixins import CDNCacheControlMixin
16
+ from readthedocs .core .unresolver import InvalidPathForVersionedProjectError
16
17
from readthedocs .core .unresolver import UnresolverError
17
- from readthedocs .core .unresolver import unresolve
18
+ from readthedocs .core .unresolver import unresolver
18
19
from readthedocs .core .utils .extend import SettingsOverrideObject
19
20
from readthedocs .core .utils .requests import is_suspicious_request
20
21
from readthedocs .projects .models import Project
22
+ from readthedocs .proxito .views .hosting import IsAuthorizedToViewProject
21
23
22
24
23
25
log = structlog .get_logger (__name__ ) # noqa
@@ -38,7 +40,7 @@ class BaseAnalyticsView(CDNCacheControlMixin, APIView):
38
40
# so we capture all views/interactions.
39
41
cache_response = False
40
42
http_method_names = ["get" ]
41
- permission_classes = [IsAuthorizedToViewVersion ]
43
+ permission_classes = [IsAuthorizedToViewProject | IsAuthorizedToViewVersion ]
42
44
43
45
@lru_cache (maxsize = 1 )
44
46
def _get_project (self ):
@@ -50,31 +52,38 @@ def _get_project(self):
50
52
def _get_version (self ):
51
53
version_slug = self .request .GET .get ("version" )
52
54
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 ()
57
58
return version
58
59
59
60
def get (self , request , * args , ** kwargs ):
60
61
# TODO: Use absolute_uri only, we don't need project and version.
61
62
project = self ._get_project ()
62
63
version = self ._get_version ()
63
64
absolute_uri = self .request .GET .get ("absolute_uri" )
65
+ status = self .request .GET .get ("status" , "200" )
64
66
if not absolute_uri :
65
67
return JsonResponse (
66
68
{"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 ,
68
76
)
69
77
70
78
self .increase_page_view_count (
71
79
project = project ,
72
80
version = version ,
73
81
absolute_uri = absolute_uri ,
82
+ status = status ,
74
83
)
75
- return Response (status = status .HTTP_204_NO_CONTENT )
84
+ return Response (status = status_codes .HTTP_204_NO_CONTENT )
76
85
77
- def increase_page_view_count (self , project , version , absolute_uri ):
86
+ def increase_page_view_count (self , project , version , absolute_uri , status ):
78
87
"""Increase the page view count for the given project."""
79
88
if is_suspicious_request (self .request ):
80
89
log .info (
@@ -83,28 +92,49 @@ def increase_page_view_count(self, project, version, absolute_uri):
83
92
)
84
93
return
85
94
95
+ # Don't track 200 if the version doesn't exist
96
+ if status == "200" and not version :
97
+ return
98
+
86
99
# Don't allow tracking page views from external domains.
87
100
if self .request .unresolved_domain .is_from_external_domain :
88
101
return
89
102
103
+ # Don't track external versions.
104
+ if version and version .is_external :
105
+ return
106
+
107
+ absolute_uri_parsed = urlparse (absolute_uri )
90
108
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
92
119
except UnresolverError :
93
120
# If we were unable to resolve the URL, it
94
121
# isn't pointing to a valid RTD project.
95
122
return
96
123
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
+ )
99
130
return
100
131
101
- path = urlparse (absolute_uri ).path
102
132
PageView .objects .register_page_view (
103
133
project = project ,
104
134
version = version ,
105
- filename = unresolved . filename ,
106
- path = path ,
107
- status = 200 ,
135
+ filename = filename ,
136
+ path = absolute_uri_parsed . path ,
137
+ status = status ,
108
138
)
109
139
110
140
0 commit comments