Skip to content

Commit 5881e3e

Browse files
authored
Add Matomo retention dimension: Days Since Registration (#12227)
* Add Matomo retention dimension: Days Since Registration (issue #12144) - Add get_patron_status(user) helper that buckets patron account age into visit-scoped Matomo dimension values: visitor, d0, d1+, d7+, d14+, d30+, d90+ - Set web.ctx.patron_status per-request in setup_contextvars processor - Add patron_status default ('visitor') to context.defaults - Push _paq.setCustomDimension(1, patron_status) in head.html before the Matomo Tag Manager container fires trackPageView - Add unit tests covering all bucket boundaries and error cases
1 parent 7618cfd commit 5881e3e

4 files changed

Lines changed: 113 additions & 1 deletion

File tree

openlibrary/accounts/__init__.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import datetime
12
from typing import TYPE_CHECKING
23

34
import web
@@ -61,6 +62,41 @@ def get_current_user() -> "User | None":
6162
return site.get().get_user()
6263

6364

65+
@public
66+
def get_days_registered(user) -> str:
67+
"""Return a Matomo custom dimension value for patron account age.
68+
69+
Buckets for visit-scoped dimension 1 ("Days Since Registration"):
70+
visitor — not logged in
71+
d0 — account created today (UTC)
72+
d1+ — 1-6 days since registration
73+
d7+ — 7-13 days
74+
d14+ — 14-29 days
75+
d30+ — 30-89 days
76+
d90+ — 90+ days (also the safe fallback on any error)
77+
"""
78+
if not user:
79+
return 'visitor'
80+
try:
81+
reg_date = user.created.date()
82+
days = (datetime.datetime.now(datetime.UTC).date() - reg_date).days
83+
except (AttributeError, TypeError):
84+
# If the date is incorrectly encoded, assume the patron
85+
# registered a long time ago before we had this set up.
86+
return 'd90+'
87+
if days <= 0:
88+
return 'd0'
89+
elif days < 7:
90+
return 'd1+'
91+
elif days < 14:
92+
return 'd7+'
93+
elif days < 30:
94+
return 'd14+'
95+
elif days < 90:
96+
return 'd30+'
97+
return 'd90+'
98+
99+
64100
def find(
65101
username: str | None = None, lusername: str | None = None, email: str | None = None
66102
) -> Account | None:

openlibrary/plugins/openlibrary/code.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1298,7 +1298,13 @@ def get_supported_languages():
12981298
def setup_context_defaults():
12991299
from infogami.utils import context
13001300

1301-
context.defaults.update({'features': [], 'user': None, 'MAX_VISIBLE_BOOKS': 5})
1301+
context.defaults.update(
1302+
{
1303+
'features': [],
1304+
'user': None,
1305+
'MAX_VISIBLE_BOOKS': 5,
1306+
}
1307+
)
13021308

13031309

13041310
def setup():
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Tests for get_days_registered — Matomo retention dimension helper."""
2+
3+
import datetime
4+
from unittest.mock import MagicMock
5+
6+
import pytest
7+
8+
from openlibrary.accounts import get_days_registered
9+
10+
11+
def _user(days_ago):
12+
"""Return a mock user whose account was created `days_ago` days ago (UTC)."""
13+
user = MagicMock()
14+
user.created = datetime.datetime.now(datetime.UTC) - datetime.timedelta(days=days_ago)
15+
return user
16+
17+
18+
# ---------------------------------------------------------------------------
19+
# Unauthenticated / missing data
20+
# ---------------------------------------------------------------------------
21+
22+
23+
def test_no_user_returns_visitor():
24+
assert get_days_registered(None) == "visitor"
25+
26+
27+
def test_falsy_user_returns_visitor():
28+
assert get_days_registered(0) == "visitor"
29+
assert get_days_registered("") == "visitor"
30+
31+
32+
def test_missing_created_returns_d90_plus():
33+
user = MagicMock()
34+
user.created = None
35+
assert get_days_registered(user) == "d90+"
36+
37+
38+
def test_invalid_created_returns_d90_plus():
39+
user = MagicMock()
40+
user.created = "not-a-date"
41+
assert get_days_registered(user) == "d90+"
42+
43+
44+
# ---------------------------------------------------------------------------
45+
# Bucket boundaries
46+
# ---------------------------------------------------------------------------
47+
48+
49+
@pytest.mark.parametrize(
50+
("days_ago", "expected"),
51+
[
52+
(0, "d0"),
53+
(1, "d1+"),
54+
(6, "d1+"),
55+
(7, "d7+"),
56+
(13, "d7+"),
57+
(14, "d14+"),
58+
(29, "d14+"),
59+
(30, "d30+"),
60+
(89, "d30+"),
61+
(90, "d90+"),
62+
(365, "d90+"),
63+
],
64+
)
65+
def test_day_buckets(days_ago, expected):
66+
assert get_days_registered(_user(days_ago)) == expected

openlibrary/templates/site/head.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@
7070
</noscript>
7171
<script>
7272
$if not is_bot():
73+
// Dimension 1 (visit-scoped): "Days Since Registration" — ID must match Matomo admin config (issue #12144)
74+
var _paq = window._paq = window._paq || [];
75+
_paq.push(['setCustomDimension', 1, $:dumps(get_days_registered(ctx.user))]);
76+
7377
var _mtm = window._mtm = window._mtm || [];
7478
_mtm.push({'mtm.startTime': (new Date().getTime()), 'event': 'mtm.Start'});
7579
(function() {

0 commit comments

Comments
 (0)