Activity Tracking
docs/DESIGN_DECISIONS.md.Optional Redis-backed unique-user sets so dashboards can show calendar DAU /
MAU / QAU without treating User.last_login as “active today.”
What you get
| Metric | Meaning |
|---|---|
| DAU | Unique users active on a calendar day (UTC) |
| MAU | Unique users active in a calendar month |
| QAU | Unique users active in a calendar quarter |
WAU is not implemented (no weekly keys or queries).
These are calendar periods, not rolling windows: MAU is “active in January,” not “active in the last 30 days.”
last_login alone undercounts: long-lived refresh tokens mean users stay active
for days without a fresh login. Activity tracking records presence on
authenticated traffic instead.
Requirements
redis_url— required when tracking is on (Redis Sets)enable_activity_tracking=True— default isFalse
auth = EnterpriseRBAC( # or SimpleRBAC / OutlabsAuth
database_url=...,
secret_key=...,
redis_url="redis://localhost:6379/0",
enable_activity_tracking=True,
)
If tracking is enabled without Redis, initialization fails with a clear error. Leave the flag off when you do not need these metrics.
How it works
- On authenticated requests (and some login paths), the library schedules
activity_tracker.track_activity_detached(user_id)— non-blocking. - Redis Sets hold the current day / month / quarter memberships (O(1) add).
- A maintenance sync writes counts into Postgres
activity_metricsfor history and optionally updatesUser.last_activity.
Production APIs default to background_job_mode="disabled". One external
maintenance owner should call run_maintenance_once() at the host-selected
cadence. The 30-minute activity_sync_interval applies only to explicitly
enabled embedded development loops.
You rarely talk to Redis yourself for dashboards older than the Redis TTL —
query activity_metrics for history; use tracker helpers for “right now.”
Configuration knobs
| Setting | Default | Purpose |
|---|---|---|
enable_activity_tracking | False | Master switch (needs Redis) |
activity_sync_interval | 1800 | Seconds between Redis → Postgres sync |
activity_update_user_model | True | Sync User.last_activity in the background job |
activity_store_user_ids | False | No effect today — passed through but unused |
activity_ttl_days | 90 | Intended retention; cleanup still uses a hard-coded ~90-day cutoff |
Querying
Access the tracker after await auth.initialize() via auth.activity_tracker
(only when enabled).
Live (Redis)
from datetime import date
dau = await auth.activity_tracker.get_daily_active_users()
mau = await auth.activity_tracker.get_monthly_active_users()
qau = await auth.activity_tracker.get_quarterly_active_users()
# Specific periods
dau_past = await auth.activity_tracker.get_daily_active_users(date(2025, 1, 15))
mau_past = await auth.activity_tracker.get_monthly_active_users(2025, 1)
qau_past = await auth.activity_tracker.get_quarterly_active_users(2025, 1)
Redis answers only while keys remain within TTL (roughly days for DAU, months for MAU, longer for QAU). Older periods → Postgres.
Historical (Postgres)
from datetime import date, timedelta
from sqlmodel import select
from outlabs_auth.models.sql.activity_metric import ActivityMetric
end = date.today()
start = end - timedelta(days=30)
result = await session.execute(
select(ActivityMetric)
.where(
ActivityMetric.metric_type == "dau",
ActivityMetric.metric_date >= start,
ActivityMetric.metric_date <= end,
)
.order_by(ActivityMetric.metric_date)
)
for row in result.scalars():
print(row.metric_date, row.count)
Monthly rows are keyed on the 1st of the month.
There is no packaged HTTP router for DAU/MAU — expose your own admin
endpoint that calls the tracker or reads activity_metrics.
Ops notes
- Production sync runs through one external maintenance owner; enabling
tracking does not start a loop while
background_job_mode="disabled". - Prometheus / structured logs may emit activity-related signals when observability is on — see Observability and Metrics Reference.
- For “last seen” UX, prefer
User.last_activity(when sync updates it) over inventing your own counter.
Related
- Configuration
- Background Maintenance
- JWT Tokens — why refresh lifetime skews
last_login - Design decision DD-049 — maintainer depth