Skip to content

Commit d0cc3fe

Browse files
authored
[api] Add usage analytics public API endpoints (#4117)
Implements GET and POST endpoints for managing usage analytics: - GET /usage_analytics to retrieve preference - POST /usage_analytics/update to modify preference
1 parent 5f7bdc8 commit d0cc3fe

File tree

4 files changed

+207
-0
lines changed

4 files changed

+207
-0
lines changed

apps/about/src/about/api.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/usr/bin/env python
2+
# Licensed to Cloudera, Inc. under one
3+
# or more contributor license agreements. See the NOTICE file
4+
# distributed with this work for additional information
5+
# regarding copyright ownership. Cloudera, Inc. licenses this file
6+
# to you under the Apache License, Version 2.0 (the
7+
# "License"); you may not use this file except in compliance
8+
# with the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
18+
import logging
19+
20+
from rest_framework import status
21+
from rest_framework.response import Response
22+
23+
from desktop.auth.backend import is_admin
24+
from desktop.lib.conf import coerce_bool
25+
from desktop.models import Settings
26+
27+
LOG = logging.getLogger()
28+
29+
30+
def get_usage_analytics(request) -> Response:
31+
"""
32+
Retrieve the user preference for analytics settings.
33+
34+
Args:
35+
request (Request): The HTTP request object.
36+
37+
Returns:
38+
Response: JSON response containing the analytics_enabled preference or an error message.
39+
40+
Raises:
41+
403: If the user is not a Hue admin.
42+
500: If there is an error retrieving preference.
43+
"""
44+
if not is_admin(request.user):
45+
return Response({'message': "You must be a Hue admin to access this endpoint."}, status=status.HTTP_403_FORBIDDEN)
46+
47+
try:
48+
settings = Settings.get_settings()
49+
return Response({'analytics_enabled': settings.collect_usage}, status=status.HTTP_200_OK)
50+
51+
except Exception as e:
52+
message = f"Error retrieving usage analytics: {e}"
53+
LOG.error(message)
54+
return Response({'message': message}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
55+
56+
57+
def update_usage_analytics(request) -> Response:
58+
"""
59+
Update the user preference for analytics settings.
60+
61+
Args:
62+
request (Request): The HTTP request object containing 'analytics_enabled' parameter.
63+
64+
Returns:
65+
Response: JSON response with the updated analytics_enabled preference or an error message.
66+
67+
Raises:
68+
403: If the user is not a Hue admin.
69+
400: If 'analytics_enabled' parameter is missing or invalid.
70+
500: If there is an error updating preference.
71+
"""
72+
if not is_admin(request.user):
73+
return Response({'message': "You must be a Hue admin to access this endpoint."}, status=status.HTTP_403_FORBIDDEN)
74+
75+
try:
76+
analytics_enabled = request.POST.get('analytics_enabled')
77+
78+
if analytics_enabled is None:
79+
return Response({'message': 'Missing parameter: analytics_enabled is required.'}, status=status.HTTP_400_BAD_REQUEST)
80+
81+
settings = Settings.get_settings()
82+
settings.collect_usage = coerce_bool(analytics_enabled)
83+
settings.save()
84+
85+
return Response({'analytics_enabled': settings.collect_usage}, status=status.HTTP_200_OK)
86+
87+
except Exception as e:
88+
message = f"Error updating usage analytics: {e}"
89+
LOG.error(message)
90+
return Response({'message': message}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

apps/about/src/about/api_tests.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#!/usr/bin/env python
2+
# Licensed to Cloudera, Inc. under one
3+
# or more contributor license agreements. See the NOTICE file
4+
# distributed with this work for additional information
5+
# regarding copyright ownership. Cloudera, Inc. licenses this file
6+
# to you under the Apache License, Version 2.0 (the
7+
# "License"); you may not use this file except in compliance
8+
# with the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
from unittest.mock import Mock, patch
18+
19+
from rest_framework import status
20+
21+
from about.api import get_usage_analytics, update_usage_analytics
22+
23+
24+
class TestUsageAnalyticsAPI:
25+
def test_get_usage_analytics_success(self):
26+
with patch('about.api.is_admin') as mock_is_admin:
27+
with patch('about.api.Settings.get_settings') as mock_get_settings:
28+
mock_is_admin.return_value = True
29+
mock_get_settings.return_value = Mock(collect_usage=True)
30+
31+
request = Mock(method='GET', user=Mock())
32+
response = get_usage_analytics(request)
33+
34+
assert response.status_code == status.HTTP_200_OK
35+
assert response.data == {'analytics_enabled': True}
36+
37+
def test_get_usage_analytics_unauthorized(self):
38+
with patch('about.api.is_admin') as mock_is_admin:
39+
mock_is_admin.return_value = False
40+
41+
request = Mock(method='GET', user=Mock())
42+
response = get_usage_analytics(request)
43+
44+
assert response.status_code == status.HTTP_403_FORBIDDEN
45+
assert response.data['message'] == "You must be a Hue admin to access this endpoint."
46+
47+
def test_get_usage_analytics_error(self):
48+
with patch('about.api.is_admin') as mock_is_admin:
49+
with patch('about.api.Settings.get_settings') as mock_get_settings:
50+
mock_is_admin.return_value = True
51+
mock_get_settings.side_effect = Exception("Test error")
52+
53+
request = Mock(method='GET', user=Mock())
54+
response = get_usage_analytics(request)
55+
56+
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
57+
assert "Error retrieving usage analytics" in response.data['message']
58+
59+
def test_update_usage_analytics_success(self):
60+
with patch('about.api.is_admin') as mock_is_admin:
61+
with patch('about.api.Settings.get_settings') as mock_get_settings:
62+
mock_is_admin.return_value = True
63+
mock_get_settings.return_value = Mock(save=Mock())
64+
65+
request = Mock(method='POST', user=Mock(), POST={'analytics_enabled': 'true'})
66+
response = update_usage_analytics(request)
67+
68+
assert response.status_code == status.HTTP_200_OK
69+
assert mock_get_settings.return_value.save.called
70+
assert response.data == {'analytics_enabled': True}
71+
72+
def test_update_usage_analytics_unauthorized(self):
73+
with patch('about.api.is_admin') as mock_is_admin:
74+
mock_is_admin.return_value = False
75+
76+
request = Mock(method='POST', user=Mock(), data={'analytics_enabled': 'true'})
77+
response = update_usage_analytics(request)
78+
79+
assert response.status_code == status.HTTP_403_FORBIDDEN
80+
assert response.data['message'] == "You must be a Hue admin to access this endpoint."
81+
82+
def test_update_usage_analytics_missing_param(self):
83+
with patch('about.api.is_admin') as mock_is_admin:
84+
mock_is_admin.return_value = True
85+
86+
request = Mock(method='POST', user=Mock(), POST={})
87+
response = update_usage_analytics(request)
88+
89+
assert response.status_code == status.HTTP_400_BAD_REQUEST
90+
assert response.data['message'] == 'Missing parameter: analytics_enabled is required.'
91+
92+
def test_update_usage_analytics_error(self):
93+
with patch('about.api.is_admin') as mock_is_admin:
94+
with patch('about.api.Settings.get_settings') as mock_get_settings:
95+
mock_is_admin.return_value = True
96+
mock_get_settings.side_effect = Exception("Test error")
97+
98+
request = Mock(method='POST', user=Mock(), POST={'analytics_enabled': 'true'})
99+
response = update_usage_analytics(request)
100+
101+
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
102+
assert "Error updating usage analytics" in response.data['message']

desktop/core/src/desktop/api_public.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from rest_framework.decorators import api_view, authentication_classes, permission_classes
2323
from rest_framework.permissions import AllowAny
2424

25+
from about import api as about_api
2526
from beeswax import api as beeswax_api
2627
from desktop import api2 as desktop_api
2728
from desktop.auth.backend import rewrite_user
@@ -89,6 +90,18 @@ def available_app_examples(request):
8990
return desktop_api.available_app_examples(django_request)
9091

9192

93+
@api_view(["GET"])
94+
def get_usage_analytics(request):
95+
django_request = get_django_request(request)
96+
return about_api.get_usage_analytics(django_request)
97+
98+
99+
@api_view(["POST"])
100+
def update_usage_analytics(request):
101+
django_request = get_django_request(request)
102+
return about_api.update_usage_analytics(django_request)
103+
104+
92105
# Editor
93106

94107

desktop/core/src/desktop/api_public_urls_v1.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
re_path(r'^logs/download/?$', api_public.download_hue_logs, name='core_download_hue_logs'),
3636
re_path(r'^install_app_examples/?$', api_public.install_app_examples, name='core_install_app_examples'),
3737
re_path(r'^available_app_examples/?$', api_public.available_app_examples, name='core_available_app_examples'),
38+
re_path(r'^usage_analytics/?$', api_public.get_usage_analytics, name='core_get_usage_analytics'),
39+
re_path(r'^usage_analytics/update/?$', api_public.update_usage_analytics, name='core_update_usage_analytics'),
3840
re_path(r'^get_config/?$', api_public.get_config),
3941
re_path(r'^check_config/?$', api_public.check_config, name='core_check_config'),
4042
re_path(r'^get_namespaces/(?P<interface>[\w\-]+)/?$', api_public.get_context_namespaces), # To remove

0 commit comments

Comments
 (0)