Repository: mdn/kuma

Issue:
T - Subscriptions: server-side GA tracking (phase 1)
Description: This is phase 1 (of possibly several) for implementing server-side GA tracking of Subscriptions. A different task will address client-side GA tracking (steps 1-5). 

6. Measure successful subscription on the back-end (web-hook)
`monthly payments | successful subscription | successful-subscription-back-end | 5`
7. Measure cancellations on the back-end 
`monthly payments | cancellation | web-hook`


Discussion:
@tobinmori I put an estimate on it but I think this'll be the 4th instance now where the dollar amount is hardcoded. We need to pay down some tech debt so I'm making this be blocked on #6654
@atopal ...

Regarding (6), perhaps I'm being very dim but all we have is CATEGORY | ACTION | LABEL.
Also, I think it makes much more sense to call it `monthly payments | subscription created | 5.00`
Note the past tense verb instead. And by using a 2 decimal number we're futureproofing ourselves for currency. It might become, someday, that we decide 5 US bucks but that comes out to EUR 4.87. Not sure if it's relevant but it just feels more sustainable.

Regarding (7), it's not possible to distinguish between the cancellation on the web UI vs the Stripe dashboard. Both trigger the exact same Webhook. 
So, for that reason, the event label doesn't really make a whole lot of sense. At least it's accurate in that the tracked event is happening as part of the webhook. 

You OK with that? Also, Yolo and MVP in mind. 
Also, the same I said about the Webhook is applicable with the act of successfully creating/setting up/signing up. If you use the MDN UI to sign up, it triggers a webhook. Or, suppose that someone uses the Stripe dashboard to create or re-enable a subscription it all goes through the Webhook eventually. 

Also, remember the context; god forbid our Webhook fails. Stripe will retry the Webhook if, our AWS cluster is experiencing an unlucky mini-downtime. But the worst that can happen is, and it's very unlikely, that we miss a GA event tracking. 

Resolving patch:
diff --git a/kuma/core/ga_tracking.py b/kuma/core/ga_tracking.py
--- a/kuma/core/ga_tracking.py
+++ b/kuma/core/ga_tracking.py
@@ -54,6 +54,12 @@
 # existing based on a *different* (already created profile) provider.
 ACTION_SOCIAL_AUTH_ADD = "social-auth-add"
 
+CATEGORY_MONTHLY_PAYMENTS = "monthly payments"
+# When a subscription is successfully set up
+ACTION_SUBSCRIPTION_CREATED = "subscription created"
+# When it's canceled either by webhook or by UI
+ACTION_SUBSCRIPTION_CANCELED = "subscription canceled"
+
 
 def track_event(
     event_category,
diff --git a/kuma/settings/common.py b/kuma/settings/common.py
--- a/kuma/settings/common.py
+++ b/kuma/settings/common.py
@@ -1732,6 +1732,15 @@ def get_user_url(user):
 CONTRIBUTION_SUPPORT_EMAIL = config(
     "CONTRIBUTION_SUPPORT_EMAIL", default="mdn-support@mozilla.com"
 )
+
+# The default amount suggested for monthly subscription payments.
+# As of March 2020, we only have 1 plan and the number is fixed.
+# In the future, we might have multiple plans and this might a dict of amount
+# per plan.
+# The reason it's not an environment variable is to simply indicate that it
+# can't be overridden at the moment based on the environment.
+CONTRIBUTION_AMOUNT_USD = 5.0
+
 CSP_CONNECT_SRC.append("https://checkout.stripe.com")
 CSP_FRAME_SRC.append("https://checkout.stripe.com")
 CSP_IMG_SRC.append("https://*.stripe.com")
diff --git a/kuma/users/views.py b/kuma/users/views.py
--- a/kuma/users/views.py
+++ b/kuma/users/views.py
@@ -47,6 +47,9 @@
     ACTION_PROFILE_AUDIT,
     ACTION_PROFILE_EDIT,
     ACTION_PROFILE_EDIT_ERROR,
+    ACTION_SUBSCRIPTION_CANCELED,
+    ACTION_SUBSCRIPTION_CREATED,
+    CATEGORY_MONTHLY_PAYMENTS,
     CATEGORY_SIGNUP_FLOW,
     track_event,
 )
@@ -914,10 +917,17 @@ def stripe_hooks(request):
             payment_intent.created,
             payment_intent.invoice_pdf,
         )
+        track_event(
+            CATEGORY_MONTHLY_PAYMENTS,
+            ACTION_SUBSCRIPTION_CREATED,
+            f"{settings.CONTRIBUTION_AMOUNT_USD:.2f}",
+        )
+
     elif event.type == "customer.subscription.deleted":
         obj = event.data.object
         for user in User.objects.filter(stripe_customer_id=obj.customer):
             UserSubscription.set_canceled(user, obj.id)
+        track_event(CATEGORY_MONTHLY_PAYMENTS, ACTION_SUBSCRIPTION_CANCELED, "webhook")
 
     else:
         return HttpResponseBadRequest(


Test patch:
diff --git a/kuma/settings/testing.py b/kuma/settings/testing.py
--- a/kuma/settings/testing.py
+++ b/kuma/settings/testing.py
@@ -95,3 +95,9 @@
 STRIPE_PUBLIC_KEY = "testing"
 STRIPE_SECRET_KEY = "testing"
 STRIPE_PLAN_ID = "testing"
+
+# Amount for the monthly subscription.
+# It's hardcoded here in case some test depends on the number and it futureproofs
+# our tests to not deviate when the actual number changes since that number
+# change shouldn't affect the tests.
+CONTRIBUTION_AMOUNT_USD = 4.99
diff --git a/kuma/users/tests/test_stripe_subscription.py b/kuma/users/tests/test_stripe_subscription.py
--- a/kuma/users/tests/test_stripe_subscription.py
+++ b/kuma/users/tests/test_stripe_subscription.py
@@ -12,6 +12,11 @@
 from stripe.error import APIError
 from waffle.testutils import override_flag
 
+from kuma.core.ga_tracking import (
+    ACTION_SUBSCRIPTION_CANCELED,
+    ACTION_SUBSCRIPTION_CREATED,
+    CATEGORY_MONTHLY_PAYMENTS,
+)
 from kuma.core.utils import safer_pyquery as pq
 from kuma.users.models import User, UserSubscription
 
@@ -210,3 +215,69 @@ def test_stripe_hook_stripe_api_error(mock1, client):
         reverse("users.stripe_hooks"), content_type="application/json", data={},
     )
     assert response.status_code == 400
+
+
+@patch("kuma.users.views.track_event")
+@patch("stripe.Event.construct_from")
+@pytest.mark.django_db
+def test_stripe_payment_succeeded_sends_ga_tracking(
+    mock1, track_event_mock_signals, client, settings
+):
+    settings.GOOGLE_ANALYTICS_ACCOUNT = "UA-XXXX-1"
+    settings.GOOGLE_ANALYTICS_TRACKING_RAISE_ERRORS = True
+
+    mock1.return_value = SimpleNamespace(
+        type="invoice.payment_succeeded",
+        data=SimpleNamespace(
+            object=SimpleNamespace(
+                customer="cus_mock_testuser",
+                created=1583842724,
+                invoice_pdf="https://developer.mozilla.org/mock-invoice-pdf-url",
+            )
+        ),
+    )
+    user(
+        save=True,
+        username="testuser",
+        email="testuser@example.com",
+        stripe_customer_id="cus_mock_testuser",
+    )
+    response = client.post(
+        reverse("users.stripe_hooks"), content_type="application/json", data={},
+    )
+    assert response.status_code == 200
+
+    track_event_mock_signals.assert_called_with(
+        CATEGORY_MONTHLY_PAYMENTS,
+        ACTION_SUBSCRIPTION_CREATED,
+        f"{settings.CONTRIBUTION_AMOUNT_USD:.2f}",
+    )
+
+
+@patch("kuma.users.views.track_event")
+@patch("stripe.Event.construct_from")
+@pytest.mark.django_db
+def test_stripe_subscription_canceled_sends_ga_tracking(
+    mock1, track_event_mock_signals, client
+):
+    mock1.return_value = SimpleNamespace(
+        type="customer.subscription.deleted",
+        data=SimpleNamespace(
+            object=SimpleNamespace(customer="cus_mock_testuser", id="sub_123456789")
+        ),
+    )
+
+    user(
+        save=True,
+        username="testuser",
+        email="testuser@example.com",
+        stripe_customer_id="cus_mock_testuser",
+    )
+    response = client.post(
+        reverse("users.stripe_hooks"), content_type="application/json", data={},
+    )
+    assert response.status_code == 200
+
+    track_event_mock_signals.assert_called_with(
+        CATEGORY_MONTHLY_PAYMENTS, ACTION_SUBSCRIPTION_CANCELED, "webhook"
+    )