Skip to content
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

for MPP-3755: use Thread to send event data to GA async #4633

Merged
merged 2 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
80 changes: 77 additions & 3 deletions privaterelay/tests/views_tests.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import json
import logging
from collections.abc import Iterator
from collections.abc import Callable, Iterator
from copy import deepcopy
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Any, Literal
from unittest.mock import Mock, patch
from unittest.mock import ANY, Mock, patch
from uuid import uuid4

from django.contrib.auth.models import User
Expand Down Expand Up @@ -35,7 +35,7 @@

from ..apps import PrivateRelayConfig
from ..fxa_utils import NoSocialToken
from ..views import _update_all_data, fxa_verifying_keys
from ..views import _update_all_data, fxa_verifying_keys, send_ga_ping


def test_no_social_token():
Expand Down Expand Up @@ -562,3 +562,77 @@ def test_lbheartbeat_view(client) -> None:
response = client.get("/__lbheartbeat__")
assert response.status_code == 200
assert response.content == b""


@pytest.fixture
def mock_metrics_thread_and_report() -> (
Iterator[dict[Literal["thread", "report"], Mock]]
):
"""
Setup mocks for metrics event.

Replace google_measurement_protocol.report with a Mock
Replace Thread with a mock version that calls immediately.
"""

with (
patch("privaterelay.views.threading.Thread", spec=True) as mock_thread_cls,
patch("privaterelay.views.report") as mock_report,
):

mock_thread = Mock(spec_set=["start"])

def create_thread(
target: Callable[[str, str, Any], None],
args: tuple[str, str, Any],
daemon: bool,
) -> Mock:
assert target == send_ga_ping
assert daemon

def call_send_ga_ping() -> None:
target(*args)

mock_thread.start.side_effect = call_send_ga_ping
return mock_thread

mock_thread_cls.side_effect = create_thread
yield {"thread": mock_thread, "report": mock_report}


def test_metrics_event_GET(client, mock_metrics_thread_and_report) -> None:
response = client.get("/metrics-event")
assert response.status_code == 405
mock_metrics_thread_and_report["thread"].start.assert_not_called()
mock_metrics_thread_and_report["report"].assert_not_called()


def test_metrics_event_POST_non_json(client, mock_metrics_thread_and_report) -> None:
response = client.post("/metrics-event")
assert response.status_code == 415
mock_metrics_thread_and_report["thread"].start.assert_not_called()
mock_metrics_thread_and_report["report"].assert_not_called()


def test_metrics_event_POST_json_no_ga_uuid(
client, mock_metrics_thread_and_report
) -> None:
response = client.post(
"/metrics-event", {"category": "addon"}, content_type="application/json"
)
assert response.status_code == 404
mock_metrics_thread_and_report["thread"].start.assert_not_called()
mock_metrics_thread_and_report["report"].assert_not_called()


def test_metrics_event_POST_json_ga_uuid_ok(
client, mock_metrics_thread_and_report, settings
) -> None:
response = client.post(
"/metrics-event", {"ga_uuid": "anything-is-ok"}, content_type="application/json"
)
assert response.status_code == 200
mock_metrics_thread_and_report["thread"].start.assert_called_once_with()
mock_metrics_thread_and_report["report"].assert_called_once_with(
settings.GOOGLE_ANALYTICS_ID, "anything-is-ok", ANY
)
21 changes: 15 additions & 6 deletions privaterelay/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import logging
import threading
from collections.abc import Iterable
from datetime import UTC, datetime
from functools import cache
Expand Down Expand Up @@ -94,9 +95,16 @@ def profile_subdomain(request):
return JsonResponse({"message": e.message, "subdomain": subdomain}, status=400)


def send_ga_ping(ga_id: str, ga_uuid: str, data: Any) -> None:
try:
report(ga_id, ga_uuid, data)
except Exception as e:
logger.error("metrics_event", extra={"error": e})


@csrf_exempt
@require_http_methods(["POST"])
def metrics_event(request):
def metrics_event(request: HttpRequest) -> JsonResponse:
try:
request_data = json.loads(request.body)
except json.JSONDecodeError:
Expand All @@ -115,11 +123,12 @@ def metrics_event(request):
dimension5=request_data.get("dimension5", None),
dimension7=request_data.get("dimension7", "website"),
)
try:
report(settings.GOOGLE_ANALYTICS_ID, request_data.get("ga_uuid"), event_data)
except Exception as e:
logger.error("metrics_event", extra={"error": e})
return JsonResponse({"msg": "Unable to report metrics event."}, status=500)
t = threading.Thread(
target=send_ga_ping,
args=[settings.GOOGLE_ANALYTICS_ID, request_data.get("ga_uuid"), event_data],
daemon=True,
)
t.start()
return JsonResponse({"msg": "OK"}, status=200)


Expand Down