Repository: mdn/kuma

Issue:
Wasted bytes on falsy whoamis
**Summary**
On every MDN page we run the XHR request to `/api/v1/whoami` and the response looks like this:

```json
{
  "username": null,
  "timezone": "US/Pacific",
  "is_authenticated": false,
  "is_staff": false,
  "is_superuser": false,
  "is_beta_tester": false,
  "avatar_url": null,
  "is_subscriber": false,
  "waffle": {
    "flags": {
      "kumaediting": false,
      "page_move": false,
      "section_edit": false,
      "spam_checks_enabled": true,
      "spam_submissions_enabled": false,
      "spam_admin_override": false,
      "spam_spammer_override": false,
      "spam_testing_mode": false,
      "sg_task_completion": false,
      "contrib_beta": false,
      "recurring_payment_beta": false,
      "subscription": false,
      "subscription_banner": false
    },
    "switches": {
      "welcome_email": true,
      "application_ACAO": true,
      "store_revision_ips": true,
      "foundation_callout": false,
      "helpful-survey-2": true,
      "wiki_spam_training": true,
      "registration_disabled": false,
      "developer_needs": false
    },
    "samples": {}
  }
}
```

That's 809 bytes worth of traffic flowing from our Django servers, through CloudFront, and to each and every browsers on every MDN page. A fraction of those have a lot more interesting things in them for people who are signed in. 

In a given month, based on pageviews I estimate we're sending about 30GB of data just on `/api/vi/whoami`. 

The minimal amount of truthy information actually looks like this:

```json
{
  "waffle": {
    "flags": {
      "spam_checks_enabled": true
    },
    "switches": {
      "welcome_email": true,
      "application_ACAO": true,
      "store_revision_ips": true,
      "helpful-survey-2": true,
      "wiki_spam_training": true
    },
    "samples": {}
  }
}
```

That one is only 213 bytes which means about 8GB worth of data. 

Remember; it's not just a matter of **minimizing our carbon footprint** and our AWS bill. But it's applicable for everyone and everywhere. And making this smaller will also mean slightly less data to download in terms of network I/O overheads for performance. 

If you gzip these, you can save even more! I calculated it to be:
```
▶ ls -ltr whoami.*
-rw-r--r--  1 peterbe  wheel  387 Mar 25 13:37 whoami.full.json.gz
-rw-r--r--  1 peterbe  wheel  809 Mar 25 13:37 whoami.full.json
-rw-r--r--  1 peterbe  wheel  179 Mar 25 13:39 whoami.minimal.json.gz
-rw-r--r--  1 peterbe  wheel  213 Mar 25 13:39 whoami.minimal.json
```

I.e. if you gzip and go for the minimal, you'd be sending only 7GB per month from us to the world. Instead of 30GB which is the status quo. 

Note: gzip should ideally be handled by the CDN. The way you accomplish that is to configure the CDN to do it on-the-fly based on headers and config and stuff. But that stuff depends on caching which is tricker (think waffle flags that use randomization)

The way MDN works at the moment is that the JSON API is consumed by a `fetch` call in `user-provider.jsx` that [does something like this](https://github.com/mdn/kuma/blob/41c1696af88fb67d8976ae37bfa9cd423f4e8ef3/kuma/javascript/src/user-provider.jsx#L59-L72):
```javascript
let userData = {
    username: data.username,
    isAuthenticated: data.is_authenticated,
    ...
```
And if it's needed, we can change that to:
```javascript
let userData = {
    username: data.username || null,
    isAuthenticated: data.is_authenticated || false,
    ...
```
...to avoid the `undefined`s. 

So there's really no need to send these falsy values as JSON over the internet. 
By keeping the struct of `userData` intact we don't even need to worry about breaking existing code too much. 

**Rationale**
Less data is better for the planet, for our users, for our AWS bill. 

**Audience**
Everyone!


**Proposal**
In the `kuma/api/v1/views.py` 's `whoami` Django view, we write some code so that the key and value is NOT added to the struct if the value is falsy (`None` or `False` in Python)


**Additional context**
It's not a user story. It's neither tech debt, or developer experience. It's just the right thing to do. 


Discussion:

Resolving patch:
diff --git a/kuma/api/v1/views.py b/kuma/api/v1/views.py
--- a/kuma/api/v1/views.py
+++ b/kuma/api/v1/views.py
@@ -230,28 +230,20 @@ def whoami(request):
         data = {
             "username": user.username,
             "is_authenticated": True,
-            "is_staff": user.is_staff,
-            "is_superuser": user.is_superuser,
-            "is_beta_tester": user.is_beta_tester,
             "avatar_url": get_avatar_url(user),
-            "is_subscriber": UserSubscription.objects.filter(
-                user=user, canceled__isnull=True
-            ).exists(),
             "email": user.email,
             "subscriber_number": user.subscriber_number,
         }
+        if UserSubscription.objects.filter(user=user, canceled__isnull=True).exists():
+            data["is_subscriber"] = True
+        if user.is_staff:
+            data["is_staff"] = True
+        if user.is_superuser:
+            data["is_superuser"] = True
+        if user.is_beta_tester:
+            data["is_beta_tester"] = True
     else:
-        data = {
-            "username": None,
-            "is_authenticated": False,
-            "is_staff": False,
-            "is_superuser": False,
-            "is_beta_tester": False,
-            "avatar_url": None,
-            "is_subscriber": False,
-            "email": None,
-            "subscriber_number": None,
-        }
+        data = {}
 
     # Add waffle data to the dict we're going to be returning.
     # This is what the waffle.wafflejs() template tag does, but we're
@@ -264,9 +256,9 @@ def whoami(request):
     #    get_waffle_flag_model().get_all()
     #
     data["waffle"] = {
-        "flags": {f.name: f.is_active(request) for f in Flag.get_all()},
-        "switches": {s.name: s.is_active() for s in Switch.get_all()},
-        "samples": {s.name: s.is_active() for s in Sample.get_all()},
+        "flags": {f.name: True for f in Flag.get_all() if f.is_active(request)},
+        "switches": {s.name: True for s in Switch.get_all() if s.is_active()},
+        "samples": {s.name: True for s in Sample.get_all() if s.is_active()},
     }
     return JsonResponse(data)
 


Test patch:
diff --git a/kuma/api/v1/tests/test_views.py b/kuma/api/v1/tests/test_views.py
--- a/kuma/api/v1/tests/test_views.py
+++ b/kuma/api/v1/tests/test_views.py
@@ -210,26 +210,11 @@ def test_whoami_anonymous(client, settings):
     assert response.status_code == 200
     assert response["content-type"] == "application/json"
     assert response.json() == {
-        "username": None,
-        "is_authenticated": False,
-        "is_staff": False,
-        "is_superuser": False,
-        "is_beta_tester": False,
-        "avatar_url": None,
-        "subscriber_number": None,
         "waffle": {
-            "flags": {
-                "section_edit": False,
-                "flag_all": True,
-                "flag_none": False,
-                "subscription": False,
-                "subscription_banner": False,
-            },
-            "switches": {"switch_on": True, "switch_off": False},
-            "samples": {"sample_always": True, "sample_never": False},
+            "flags": {"flag_all": True},
+            "switches": {"switch_on": True},
+            "samples": {"sample_always": True},
         },
-        "is_subscriber": False,
-        "email": None,
     }
     assert_no_cache_header(response)
 
@@ -269,28 +254,28 @@ def test_whoami(
     response = user_client.get(url)
     assert response.status_code == 200
     assert response["content-type"] == "application/json"
-    assert response.json() == {
+    expect = {
         "username": wiki_user.username,
         "is_authenticated": True,
-        "is_staff": is_staff,
-        "is_superuser": is_superuser,
-        "is_beta_tester": is_beta_tester,
         "avatar_url": wiki_user_github_account.get_avatar_url(),
         "subscriber_number": None,
         "waffle": {
-            "flags": {
-                "section_edit": True,
-                "flag_all": True,
-                "flag_none": False,
-                "subscription": is_staff,
-                "subscription_banner": is_staff,
-            },
-            "switches": {"switch_on": True, "switch_off": False},
-            "samples": {"sample_always": True, "sample_never": False},
+            "flags": {"section_edit": True, "flag_all": True},
+            "switches": {"switch_on": True},
+            "samples": {"sample_always": True},
         },
-        "is_subscriber": False,
         "email": "wiki_user@example.com",
     }
+    if is_staff:
+        expect["is_staff"] = True
+        expect["waffle"]["flags"]["subscription"] = True
+        expect["waffle"]["flags"]["subscription_banner"] = True
+    if is_superuser:
+        expect["is_superuser"] = True
+    if is_beta_tester:
+        expect["is_beta_tester"] = True
+
+    assert response.json() == expect
     assert_no_cache_header(response)
 
 
@@ -303,7 +288,7 @@ def test_whoami_subscriber(
     url = reverse("api.v1.whoami")
     response = user_client.get(url)
     assert response.status_code == 200
-    assert response.json()["is_subscriber"] is False
+    assert "is_subscriber" not in response.json()
 
     UserSubscription.set_active(wiki_user, "abc123")
     response = user_client.get(url)
@@ -314,7 +299,7 @@ def test_whoami_subscriber(
     UserSubscription.set_canceled(wiki_user, "abc123")
     response = user_client.get(url)
     assert response.status_code == 200
-    assert response.json()["is_subscriber"] is False
+    assert "is_subscriber" not in response.json()
     assert response.json()["subscriber_number"] == 1
 
 
diff --git a/kuma/javascript/src/user-provider.test.js b/kuma/javascript/src/user-provider.test.js
--- a/kuma/javascript/src/user-provider.test.js
+++ b/kuma/javascript/src/user-provider.test.js
@@ -45,7 +45,7 @@ describe('UserProvider', () => {
 
         const waffleFlags = {
             flags: { section_edit: true },
-            switches: { bar: false },
+            switches: {},
             samples: {},
         };
 
@@ -66,12 +66,7 @@ describe('UserProvider', () => {
                     Promise.resolve({
                         username: 'testing',
                         is_authenticated: true,
-                        is_beta_tester: false,
                         is_staff: true,
-                        is_super_user: false,
-                        is_subscriber: false,
-                        subscriber_number: null,
-                        avatar_url: null,
                         waffle: waffleFlags,
                         email: 'testuser@mail.com',
                     }),
diff --git a/kuma/users/tests/test_views.py b/kuma/users/tests/test_views.py
--- a/kuma/users/tests/test_views.py
+++ b/kuma/users/tests/test_views.py
@@ -1653,8 +1653,8 @@ def test_delete_user_donate_attributions(
     whoami_url = reverse("api.v1.whoami")
     response = user_client.get(whoami_url)
     assert response.status_code == 200
-    assert not response.json()["username"]
-    assert not response.json()["is_authenticated"]
+    assert "username" not in response.json()
+    assert "is_authenticated" not in response.json()
 
 
 @mock.patch("kuma.users.signal_handlers.cancel_stripe_customer_subscription")
@@ -1786,8 +1786,8 @@ def test_delete_user_keep_attributions(
     whoami_url = reverse("api.v1.whoami")
     response = user_client.get(whoami_url)
     assert response.status_code == 200
-    assert not response.json()["username"]
-    assert not response.json()["is_authenticated"]
+    assert "username" not in response.json()
+    assert "is_authenticated" not in response.json()
 
     # There should be no Key left
     assert not Key.objects.all().exists()
@@ -1808,7 +1808,7 @@ def test_delete_user_keep_attributions_and_cancel_subscriptions(
     ]
 
     # Also, pretend that the user has a rich profile
-    User.objects.filter(id=wiki_user.id).update(stripe_customer_id="cus_12345",)
+    User.objects.filter(id=wiki_user.id).update(stripe_customer_id="cus_12345")
     UserSubscription.set_active(wiki_user, subscription_id)
 
     revision = root_doc.revisions.first()