Repository: cal-itp/benefits

Issue:
Add custom properties for UTM code information
We are proposing to use UTM codes in query strings for various Benefits outreach campaigns. Read more about UTM codes in this article from Hubspot: https://blog.hubspot.com/marketing/what-are-utm-tracking-codes-ht

We need to implement these properties into the Amplitude analytics tracking to be able to later gauge campaign effectiveness.

The proposal is to add them as `event_properties` and `user_properties` in the `viewed page` event. This is likely the first event that a user following a tracked URL will generate. And having the properties on both the event and the user makes later analysis in Amplitude more flexible.

## Acceptance Criteria

<!-- Remember to consider edge cases -->

- [x] The [`viewed page` event](https://github.com/cal-itp/benefits/blob/dev/benefits/core/analytics.py#L90) is modified to capture UTM code information
- [x] Custom `event_properties` are added for each of the UTM codes:
  - [ ] `utm_campaign`
  - [ ] `utm_source`
  - [ ] `utm_medium`
  - [ ] `utm_content`
  - [ ] `utm_id`
- [x] Custom `user_properties` are added for each of the UTM codes:
  - [ ] `utm_campaign`
  - [ ] `utm_source`
  - [ ] `utm_medium`
  - [ ] `utm_content`
  - [ ] `utm_id`

## Additional context

UTM code information comes from the query string, accessible via the Django [`request.GET`](https://docs.djangoproject.com/en/5.0/ref/request-response/#django.http.HttpRequest.GET) attribute, an instance of [`QueryDict`](https://docs.djangoproject.com/en/5.0/ref/request-response/#django.http.QueryDict).



Discussion:

Resolving patch:
diff --git a/benefits/core/analytics.py b/benefits/core/analytics.py
--- a/benefits/core/analytics.py
+++ b/benefits/core/analytics.py
@@ -92,6 +92,18 @@ class ViewedPageEvent(Event):
 
     def __init__(self, request):
         super().__init__(request, "viewed page")
+        # Add UTM codes
+        utm_campaign = request.GET.get("utm_campaign")
+        utm_source = request.GET.get("utm_source")
+        utm_medium = request.GET.get("utm_medium")
+        utm_content = request.GET.get("utm_content")
+        utm_id = request.GET.get("utm_id")
+        self.update_event_properties(
+            utm_campaign=utm_campaign, utm_source=utm_source, utm_medium=utm_medium, utm_content=utm_content, utm_id=utm_id
+        )
+        self.update_user_properties(
+            utm_campaign=utm_campaign, utm_source=utm_source, utm_medium=utm_medium, utm_content=utm_content, utm_id=utm_id
+        )
 
 
 class ChangedLanguageEvent(Event):


Test patch:
diff --git a/tests/pytest/core/test_analytics.py b/tests/pytest/core/test_analytics.py
--- a/tests/pytest/core/test_analytics.py
+++ b/tests/pytest/core/test_analytics.py
@@ -1,7 +1,41 @@
 import pytest
 
 import benefits.core.analytics
-from benefits.core.analytics import Event
+from benefits.core.analytics import Event, ViewedPageEvent
+from django.contrib.sessions.middleware import SessionMiddleware
+from django.middleware.locale import LocaleMiddleware
+
+
+@pytest.fixture
+def app_request_utm(rf):
+    """
+    Fixture creates and initializes a new Django request object similar to a real application request with UTM codes.
+    """
+    # create a request for the path with UTM codes, initialize
+    app_request = rf.get(
+        "/some/arbitrary/path?utm_campaign=campaign&utm_source=source&utm_medium=medium&utm_content=content&utm_id=id"
+    )
+
+    # https://stackoverflow.com/a/55530933/358804
+    middleware = [SessionMiddleware(lambda x: x), LocaleMiddleware(lambda x: x)]
+    for m in middleware:
+        m.process_request(app_request)
+
+    app_request.session.save()
+    benefits.core.analytics.session.reset(app_request)
+
+    return app_request
+
+
+@pytest.fixture
+def utm_code_data():
+    return {
+        "utm_campaign": "campaign",
+        "utm_source": "source",
+        "utm_medium": "medium",
+        "utm_content": "content",
+        "utm_id": "id",
+    }
 
 
 @pytest.mark.django_db
@@ -80,3 +114,21 @@ def test_Event_update_user_properties(app_request):
 
     assert key in event.user_properties
     assert event.user_properties[key] == value
+
+
+@pytest.mark.django_db
+def test_ViewedPageEvent_with_UTM(app_request_utm, utm_code_data):
+    event = ViewedPageEvent(app_request_utm)
+
+    for key, value in utm_code_data.items():
+        assert event.event_properties[key] == value
+        assert event.user_properties[key] == value
+
+
+@pytest.mark.django_db
+def test_ViewedPageEvent_without_UTM(app_request, utm_code_data):
+    event = ViewedPageEvent(app_request)
+
+    for key in utm_code_data:
+        assert event.event_properties[key] is None
+        assert event.user_properties[key] is None