Skip to content

Commit 50a6f3b

Browse files
Report MAU, not just "quarterly active users" (#4827)
* prepare to send MAU * Add changelog entry and positive project-level MAU test Document the new monthly-active-users metric in the changelog and add a project-level assertion covering a user active within the trailing 30-day window (complementing the existing zero-boundary test). --------- Co-authored-by: Stuart Corbishley <corbish@gmail.com>
1 parent 4cc01f5 commit 50a6f3b

9 files changed

Lines changed: 331 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ and this project adheres to
1717

1818
### Added
1919

20+
- Report monthly active users (MAU) — distinct users active in the trailing 30
21+
days — in the usage tracker submission, alongside the existing 90-day active
22+
user count. Reported at both instance and project level, and bumps the usage
23+
report schema to version 3.
24+
[#4826](https://github.com/OpenFn/lightning/issues/4826)
2025
- Single Sign-On (SSO) sign-in with GitHub and Google. Users can sign in with an
2126
external identity provider and link or unlink providers from their profile
2227
settings. [#4621](https://github.com/OpenFn/lightning/issues/4621)

lib/lightning/usage_tracking/project_metrics_service.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ defmodule Lightning.UsageTracking.ProjectMetricsService do
2727

2828
%{
2929
no_of_active_users: UserService.no_of_active_users(date, users),
30+
no_of_monthly_active_users:
31+
UserService.no_of_monthly_active_users(date, users),
3032
no_of_users: UserService.no_of_users(date, users),
3133
workflows: instrument_workflows(project, cleartext_enabled, date)
3234
}

lib/lightning/usage_tracking/report_data.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ defmodule Lightning.UsageTracking.ReportData do
1515
instance: instrument_instance(configuration, cleartext_enabled, date),
1616
projects: instrument_projects(cleartext_enabled, date),
1717
report_date: date,
18-
version: "2"
18+
version: "3"
1919
}
2020
end
2121

@@ -26,6 +26,7 @@ defmodule Lightning.UsageTracking.ReportData do
2626
|> instrument_identity(cleartext_enabled)
2727
|> Map.merge(%{
2828
no_of_active_users: UserService.no_of_active_users(date),
29+
no_of_monthly_active_users: UserService.no_of_monthly_active_users(date),
2930
no_of_users: UserService.no_of_users(date),
3031
operating_system: operating_system_name(),
3132
version: UsageTracking.lightning_version()

lib/lightning/usage_tracking/user_queries.ex

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,45 @@ defmodule Lightning.UsageTracking.UserQueries do
99
alias Lightning.Accounts.User
1010
alias Lightning.Accounts.UserToken
1111

12+
# Trailing window (in days) used for the two active-user metrics. The 90-day
13+
# figure is the original `no_of_active_users` series; the 30-day figure backs
14+
# the standard SaaS "monthly active users" (MAU) metric.
15+
@active_window_days 90
16+
@monthly_active_window_days 30
17+
1218
def existing_users(date) do
1319
report_time = report_date_as_time(date)
1420

1521
from u in User, where: u.inserted_at <= ^report_time
1622
end
1723

1824
def existing_users(date, user_list) do
19-
list_ids = user_list |> Enum.map(& &1.id)
20-
21-
from eu in existing_users(date), where: eu.id in ^list_ids
25+
existing_users(date) |> filter_by_users(user_list)
2226
end
2327

2428
def active_users(date) do
29+
active_users_within(date, @active_window_days)
30+
end
31+
32+
def active_users(date, user_list) do
33+
active_users_within(date, @active_window_days) |> filter_by_users(user_list)
34+
end
35+
36+
def monthly_active_users(date) do
37+
active_users_within(date, @monthly_active_window_days)
38+
end
39+
40+
def monthly_active_users(date, user_list) do
41+
active_users_within(date, @monthly_active_window_days)
42+
|> filter_by_users(user_list)
43+
end
44+
45+
defp active_users_within(date, window_days) do
2546
report_time = report_date_as_time(date)
2647

2748
{:ok, threshold_time, _offset} =
2849
date
29-
|> Date.add(-90)
50+
|> Date.add(-window_days)
3051
|> then(&"#{&1}T23:59:59Z")
3152
|> DateTime.from_iso8601()
3253

@@ -38,10 +59,10 @@ defmodule Lightning.UsageTracking.UserQueries do
3859
where: ut.inserted_at > ^threshold_time and ut.inserted_at <= ^report_time
3960
end
4061

41-
def active_users(date, user_list) do
62+
defp filter_by_users(query, user_list) do
4263
list_ids = user_list |> Enum.map(& &1.id)
4364

44-
from au in active_users(date), where: au.id in ^list_ids
65+
from u in query, where: u.id in ^list_ids
4566
end
4667

4768
defp report_date_as_time(date) do

lib/lightning/usage_tracking/user_service.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,12 @@ defmodule Lightning.UsageTracking.UserService do
2222
def no_of_active_users(date, user_list) do
2323
UserQueries.active_users(date, user_list) |> Repo.aggregate(:count)
2424
end
25+
26+
def no_of_monthly_active_users(date) do
27+
UserQueries.monthly_active_users(date) |> Repo.aggregate(:count)
28+
end
29+
30+
def no_of_monthly_active_users(date, user_list) do
31+
UserQueries.monthly_active_users(date, user_list) |> Repo.aggregate(:count)
32+
end
2533
end

test/lightning/usage_tracking/project_metrics_service_test.exs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,50 @@ defmodule Lightning.UsageTracking.ProjectMetricsServiceTest do
136136
} = ProjectMetricsService.generate_metrics(project, enabled, date)
137137
end
138138

139+
test "includes the number of monthly active users (trailing 30 days)", %{
140+
date: date,
141+
enabled: enabled,
142+
project: project
143+
} do
144+
# The project's active users last logged in at 2023-11-08, which is
145+
# within the 90-day window but outside the 30-day monthly window.
146+
assert %{
147+
no_of_monthly_active_users: 0
148+
} = ProjectMetricsService.generate_metrics(project, enabled, date)
149+
end
150+
151+
test "counts project users active within the trailing 30 days", %{
152+
date: date,
153+
enabled: enabled
154+
} do
155+
{:ok, within_30_days, _offset} =
156+
DateTime.from_iso8601("#{Date.add(date, -10)}T10:00:00Z")
157+
158+
project =
159+
insert(:project,
160+
project_users: [
161+
build(:project_user,
162+
user: fn ->
163+
user = insert(:user, inserted_at: ~U[2024-02-04 01:00:00Z])
164+
165+
insert(:user_token,
166+
context: "session",
167+
user: user,
168+
inserted_at: within_30_days
169+
)
170+
171+
user
172+
end
173+
)
174+
]
175+
)
176+
|> Repo.preload([:users, workflows: [:jobs, :runs]])
177+
178+
assert %{
179+
no_of_monthly_active_users: 1
180+
} = ProjectMetricsService.generate_metrics(project, enabled, date)
181+
end
182+
139183
test "includes data for workflows existing on or before date", %{
140184
date: date,
141185
eligible_workflow_1: eligible_workflow_1,

test/lightning/usage_tracking/report_data_test.exs

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,39 @@ defmodule Lightning.UsageTracking.ReportDataTest do
144144
} = ReportData.generate(report_config, enabled, date)
145145
end
146146

147+
test "includes the number of monthly active users (trailing 30 days)", %{
148+
cleartext_enabled: enabled,
149+
config: report_config,
150+
date: date
151+
} do
152+
{:ok, within_30_days, _offset} =
153+
DateTime.from_iso8601("#{Date.add(date, -29)}T10:00:00Z")
154+
155+
{:ok, within_90_but_not_30_days, _offset} =
156+
DateTime.from_iso8601("#{Date.add(date, -45)}T10:00:00Z")
157+
158+
monthly_active_user = insert(:user, inserted_at: ~U[2024-02-04 01:00:00Z])
159+
160+
insert(:user_token,
161+
context: "session",
162+
inserted_at: within_30_days,
163+
user: monthly_active_user
164+
)
165+
166+
quarterly_only_user = insert(:user, inserted_at: ~U[2024-02-04 01:00:00Z])
167+
168+
insert(:user_token,
169+
context: "session",
170+
inserted_at: within_90_but_not_30_days,
171+
user: quarterly_only_user
172+
)
173+
174+
# The 90-day window counts both; only one is active within 30 days.
175+
assert %{
176+
instance: %{no_of_active_users: 2, no_of_monthly_active_users: 1}
177+
} = ReportData.generate(report_config, enabled, date)
178+
end
179+
147180
test "includes the operating system details", %{
148181
cleartext_enabled: enabled,
149182
config: report_config,
@@ -196,7 +229,7 @@ defmodule Lightning.UsageTracking.ReportDataTest do
196229
config: report_config,
197230
date: date
198231
} do
199-
assert %{version: "2"} = ReportData.generate(report_config, enabled, date)
232+
assert %{version: "3"} = ReportData.generate(report_config, enabled, date)
200233
end
201234

202235
test "indicates the applicable report date", %{
@@ -399,7 +432,7 @@ defmodule Lightning.UsageTracking.ReportDataTest do
399432
config: report_config,
400433
date: date
401434
} do
402-
assert %{version: "2"} = ReportData.generate(report_config, enabled, date)
435+
assert %{version: "3"} = ReportData.generate(report_config, enabled, date)
403436
end
404437

405438
test "indicates the applicable report date", %{

test/lightning/usage_tracking/user_queries_test.exs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,147 @@ defmodule Lightning.UsageTracking.UserQueriesTest do
231231
end
232232
end
233233

234+
describe "monthly_active_users/1" do
235+
test "returns users that have logged in in the last 30 days" do
236+
user_within_window =
237+
insert(:user, inserted_at: ~U[2024-02-04 01:00:00Z])
238+
239+
_active_token =
240+
insert(
241+
:user_token,
242+
context: "session",
243+
inserted_at: ~U[2024-01-07 00:00:00Z],
244+
user: user_within_window
245+
)
246+
247+
user_on_report_date =
248+
insert(:user, inserted_at: ~U[2024-02-04 01:00:00Z])
249+
250+
_active_token =
251+
insert(
252+
:user_token,
253+
context: "session",
254+
inserted_at: ~U[2024-02-05 23:59:59Z],
255+
user: user_on_report_date
256+
)
257+
258+
user_older_than_30_days =
259+
insert(:user, inserted_at: ~U[2024-02-04 01:00:00Z])
260+
261+
_ineligible_token_older_than_30_days =
262+
insert(
263+
:user_token,
264+
context: "session",
265+
inserted_at: ~U[2024-01-06 23:59:59Z],
266+
user: user_older_than_30_days
267+
)
268+
269+
user_newer_than_report_date =
270+
insert(:user, inserted_at: ~U[2024-02-04 01:00:00Z])
271+
272+
_ineligible_token_newer_than_report_date =
273+
insert(
274+
:user_token,
275+
context: "session",
276+
inserted_at: ~U[2024-02-06 00:00:01Z],
277+
user: user_newer_than_report_date
278+
)
279+
280+
user_with_non_session_token =
281+
insert(:user, inserted_at: ~U[2024-02-04 01:00:00Z])
282+
283+
_ineligible_token_not_session =
284+
insert(
285+
:user_token,
286+
context: "api",
287+
inserted_at: ~U[2024-02-05 00:00:01Z],
288+
user: user_with_non_session_token
289+
)
290+
291+
result = UserQueries.monthly_active_users(@date) |> Repo.all()
292+
293+
assert(result |> contains(user_within_window))
294+
assert(result |> contains(user_on_report_date))
295+
refute(result |> contains(user_older_than_30_days))
296+
refute(result |> contains(user_newer_than_report_date))
297+
refute(result |> contains(user_with_non_session_token))
298+
end
299+
300+
test "if user has more than one token, only includes user once" do
301+
user =
302+
insert(:user, inserted_at: ~U[2024-02-04 01:00:00Z])
303+
304+
_active_token_1 =
305+
insert(
306+
:user_token,
307+
context: "session",
308+
inserted_at: ~U[2024-01-07 00:00:00Z],
309+
user: user
310+
)
311+
312+
_active_token_2 =
313+
insert(
314+
:user_token,
315+
context: "session",
316+
inserted_at: ~U[2024-01-07 00:00:01Z],
317+
user: user
318+
)
319+
320+
result = UserQueries.monthly_active_users(@date) |> Repo.all()
321+
322+
assert(result |> contains(user))
323+
assert(length(result) == 1)
324+
end
325+
end
326+
327+
describe "monthly_active_users/2" do
328+
test "returns subset of user list that have logged in the last 30 days" do
329+
user_in_list_within_window =
330+
insert(:user, inserted_at: ~U[2024-02-04 01:00:00Z])
331+
332+
_active_token =
333+
insert(
334+
:user_token,
335+
context: "session",
336+
inserted_at: ~U[2024-01-07 00:00:00Z],
337+
user: user_in_list_within_window
338+
)
339+
340+
user_not_in_list =
341+
insert(:user, inserted_at: ~U[2024-02-04 01:00:00Z])
342+
343+
_active_token =
344+
insert(
345+
:user_token,
346+
context: "session",
347+
inserted_at: ~U[2024-01-07 00:00:00Z],
348+
user: user_not_in_list
349+
)
350+
351+
user_in_list_older_than_30_days =
352+
insert(:user, inserted_at: ~U[2024-02-04 01:00:00Z])
353+
354+
_ineligible_token_older_than_30_days =
355+
insert(
356+
:user_token,
357+
context: "session",
358+
inserted_at: ~U[2024-01-06 23:59:59Z],
359+
user: user_in_list_older_than_30_days
360+
)
361+
362+
user_list = [
363+
user_in_list_within_window,
364+
user_in_list_older_than_30_days
365+
]
366+
367+
result = UserQueries.monthly_active_users(@date, user_list) |> Repo.all()
368+
369+
assert(result |> contains(user_in_list_within_window))
370+
refute(result |> contains(user_not_in_list))
371+
refute(result |> contains(user_in_list_older_than_30_days))
372+
end
373+
end
374+
234375
defp contains(result, desired_user) do
235376
result |> Enum.find(fn user -> user.id == desired_user.id end)
236377
end

0 commit comments

Comments
 (0)