Repository navigation
Sprint2 2. Analytics Pipeline
Nicolás Salazar edited this page Oct 3, 2026
·
1 revision
SENECApp answers its 14 business questions (BQs) from one pipeline. Two kinds of events go into the same table: events the app reports itself, and events the backend records while it serves the app's normal requests. SQL queries over that table and the operational tables turn them into answers, served to a dashboard.
flowchart LR
subgraph phone["Flutter app (student's phone)"]
ui["Screens<br/>TrackScreen wrapper"]
queue["Analytics queue<br/>(saved on the phone,<br/>sent in batches)"]
api_client["ApiClient<br/>Bearer token + X-* context headers"]
ui -- "screen_view (load_time_ms)<br/>app_error<br/>join_form_opened" --> queue
ui -- "regular use:<br/>search, open, save, join,<br/>check in, open/dismiss notification" --> api_client
end
subgraph backend["Backend (FastAPI, Docker)"]
ingest["POST /analytics/events<br/>validate + de-duplicate by event_id"]
routes["API routes + services"]
track["track(): server event<br/>in the same DB transaction"]
jobs["Scheduled jobs (APScheduler)<br/>train_recommender 03:00<br/>re-engagement jobs"]
bq["BQ queries<br/>app/analytics/questions.py<br/>BQ1-BQ14 (SQL)"]
bq_api["GET /analytics/bq/{id}<br/>admins only"]
dashboard["/dashboard"]
end
subgraph db["PostgreSQL"]
events[("analytics_events<br/>source = client | server")]
ops[("Operational tables<br/>recommendation_logs, memberships,<br/>event_attendance, notifications,<br/>user_interests, reengagement_cases,<br/>releases")]
models[("recommender_models")]
end
queue -- "every 30 s, on background,<br/>before sign-out (max 500/batch)" --> ingest
api_client --> routes
ingest --> events
routes --> track --> events
routes -- "domain writes<br/>(joins, check-ins, impressions...)" --> ops
jobs -- "read logs + joins" --> ops
jobs -- "new weights" --> models
jobs -- "re-engagement cases,<br/>reminders" --> ops
events --> bq
ops --> bq
bq --> bq_api --> dashboard
Rationale
- Two sources, one table. The app only reports what the backend can't see: what's on screen, how long it took to load, errors, and opening the join form. Everything else (searches, views, saves, joins, check-ins, notification opens) is recorded by the backend while it serves the request, inside the same database transaction. A recorded join therefore always matches a real membership. Both apps (Flutter and Kotlin) also get those events for free, without tracking code of their own.
-
One context for both. Every request carries the same
X-Session-Id,X-App,X-App-Version,X-Platform,X-Device-ModelandX-OS-Versionheaders the app puts on its own events. Server-side and client-side events can then be broken down the same way: by device for BQ11, and by app version for BQ14. -
Offline-tolerant client queue. Events are saved on the phone the moment they happen and sent in batches. They survive having no signal on campus and app crashes: a crash is reported on the next launch. The backend de-duplicates by
event_id, so a batch re-sent after a timeout is stored once. - Questions are queries, not stored aggregates. Each BQ is a SQL query run on demand. Adding or changing a question doesn't need a data migration, and the dashboard always reflects the latest data.
- The pipeline also feeds the product. The nightly job retrains the group recommender on its own logs (recommendation shown -> joined within 14 days). The same data that answers BQ2 is what improves recommendations.
- Privacy by design. No names, emails, message text or coordinates go into event properties. Location is used only with the student's consent, and only the nearest building or a distance is stored. Only the backend's own error messages are sent; other exception texts are dropped.
| BQ | Type | Data |
|---|---|---|
| BQ1 crashes and errors by feature or screen | 1 | client screen_view, app_error (fatal, feature) |
| BQ11 load time by screen, device and OS | 1 | client screen_view.load_time_ms + device headers |
| BQ2 recommended groups students join | 2 |
recommendation_logs (impressions) + memberships, attributed with rec_request_id
|
| BQ3 free-time event suggestions: when and where | 2 |
recommendation_logs (free-now context) + server event_viewed (entry_point=free_now) + attendance |
| BQ12 filters used before opening or joining | 2 | server group_searched (filters used) -> group_viewed / group_joined with entry_point=search
|
| BQ13 categories with the most views and saves in Explore | 2 | server group_viewed (entry_point=explore) + group_saved (source=explore) |
| BQ4 profile fields vs saves and joins | 3 | server group_viewed (snapshot of filled fields) + saves + joins |
| BQ6 recommendations vs search: which brings more joins | 3 | memberships entry_point
|
| BQ7 where the join flow is abandoned | 3 |
group_viewed -> client join_form_opened -> group_joined (same join_attempt_id) |
| BQ8 notification interaction by type | 3 |
notifications.opened_at / dismissed_at (tap = open, swipe or "Mark all read" = dismiss) |
| BQ5 interests with the best search-to-join ratio | 4 |
group_searched.interest_ids + joins via search + user_interests
|
| BQ9 opted-in students per active group, by interest | 4 |
user_interests + approved groups' tags |
| BQ10 declining groups: reminders vs messages | 5 | weekly attendance + reengagement_cases (randomized arms) |
| BQ14 crash/error rates after feature updates | 5 | client app_error + sessions per app_version, against releases
|
Contributors: Daniel Vergara, Camilo Castilla, Nicolas Salazar, Jose Manuel Fonseca, Daniel Vargas, Luciana Bacci.