Repository: cal-itp/benefits

Issue:
Improve analytics for auth claims flow
With the move to integer claims in #2046, IdG will return error codes in place of True (`0`) or False (`1`) when an error occurs in processing the user's eligibility. Any integer that is greater than or equal to `10` is considered an error code.

We want to improve our analytics around the auth flow, in particular to capture any errors that occur as described by IdG error codes.

## Acceptance Criteria

<!-- Remember to consider edge cases -->

- [x] The [`FinishedSignInEvent`](https://github.com/cal-itp/benefits/blob/dev/benefits/oauth/analytics.py#L32) is updated to allow an optional `error_code` event property.
- [x] The `error_code` property is the integer value of the claim error code (originally transmitted as a string)

## Additional context

In #2046, we will handle the regular True (`0`) / False (`1`) cases.

This issue has been scoped back to exclude lookup of the error message associated with a given code. This needs further discussion and design with CDT.


Discussion:

Resolving patch:
diff --git a/benefits/oauth/analytics.py b/benefits/oauth/analytics.py
--- a/benefits/oauth/analytics.py
+++ b/benefits/oauth/analytics.py
@@ -32,8 +32,10 @@ def __init__(self, request):
 class FinishedSignInEvent(OAuthEvent):
     """Analytics event representing the end of the OAuth sign in flow."""
 
-    def __init__(self, request):
+    def __init__(self, request, error=None):
         super().__init__(request, "finished sign in")
+        if error is not None:
+            self.update_event_properties(error_code=error)
 
 
 class StartedSignOutEvent(OAuthEvent):
@@ -61,9 +63,9 @@ def canceled_sign_in(request):
     core.send_event(CanceledSignInEvent(request))
 
 
-def finished_sign_in(request):
+def finished_sign_in(request, error=None):
     """Send the "finished sign in" analytics event."""
-    core.send_event(FinishedSignInEvent(request))
+    core.send_event(FinishedSignInEvent(request, error))
 
 
 def started_sign_out(request):
diff --git a/benefits/oauth/views.py b/benefits/oauth/views.py
--- a/benefits/oauth/views.py
+++ b/benefits/oauth/views.py
@@ -64,6 +64,8 @@ def authorize(request):
     verifier_claim = verifier.auth_provider.claim
     stored_claim = None
 
+    error_claim = None
+
     if verifier_claim:
         userinfo = token.get("userinfo")
 
@@ -76,10 +78,11 @@ def authorize(request):
             elif claim_value == 1:
                 # if userinfo contains our claim and the flag is 1 (true), store the *claim*
                 stored_claim = verifier_claim
+            elif claim_value >= 10:
+                error_claim = claim_value
 
     session.update(request, oauth_token=id_token, oauth_claim=stored_claim)
-
-    analytics.finished_sign_in(request)
+    analytics.finished_sign_in(request, error=error_claim)
 
     return redirect(ROUTE_CONFIRM)
 


Test patch:
diff --git a/tests/pytest/oauth/test_analytics.py b/tests/pytest/oauth/test_analytics.py
--- a/tests/pytest/oauth/test_analytics.py
+++ b/tests/pytest/oauth/test_analytics.py
@@ -1,6 +1,6 @@
 import pytest
 
-from benefits.oauth.analytics import OAuthEvent
+from benefits.oauth.analytics import OAuthEvent, FinishedSignInEvent
 
 
 @pytest.mark.django_db
@@ -22,3 +22,15 @@ def test_OAuthEvent_verifier_no_client_name_when_does_not_use_auth_verification(
     event = OAuthEvent(app_request, "event type")
 
     assert "auth_provider" not in event.event_properties
+
+
+@pytest.mark.django_db
+def test_FinishedSignInEvent_with_error(app_request):
+    event = FinishedSignInEvent(app_request, error=10)
+    assert event.event_properties["error_code"] == 10
+
+
+@pytest.mark.django_db
+def test_FinishedSignInEvent_without_error(app_request):
+    event = FinishedSignInEvent(app_request)
+    assert "error_code" not in event.event_properties
diff --git a/tests/pytest/oauth/test_views.py b/tests/pytest/oauth/test_views.py
--- a/tests/pytest/oauth/test_views.py
+++ b/tests/pytest/oauth/test_views.py
@@ -137,6 +137,27 @@ def test_authorize_success_with_claim_false(
     assert result.url == reverse(ROUTE_CONFIRM)
 
 
+@pytest.mark.django_db
+def test_authorize_success_with_claim_error(
+    mocked_session_verifier_uses_auth_verification,
+    mocked_oauth_create_client,
+    mocked_analytics_module,
+    app_request,
+):
+    verifier = mocked_session_verifier_uses_auth_verification.return_value
+    verifier.auth_provider.claim = "claim"
+    mocked_oauth_client = mocked_oauth_create_client.return_value
+    mocked_oauth_client.authorize_access_token.return_value = {"id_token": "token", "userinfo": {"claim": "10"}}
+
+    result = authorize(app_request)
+
+    mocked_oauth_client.authorize_access_token.assert_called_with(app_request)
+    mocked_analytics_module.finished_sign_in.assert_called_with(app_request, error=10)
+    assert session.oauth_claim(app_request) is None
+    assert result.status_code == 302
+    assert result.url == reverse(ROUTE_CONFIRM)
+
+
 @pytest.mark.django_db
 @pytest.mark.usefixtures("mocked_analytics_module")
 def test_authorize_success_without_verifier_claim(