feat(analytics): analytics hub with aggregation endpoints and audit trail - #110
feat(analytics): analytics hub with aggregation endpoints and audit trail#110BK1031 wants to merge 1 commit into
Conversation
…it trail Backend: - add audit_event table + best-effort RecordAuditEvent helper; instrument application create/delete/secret-reveal and group member add/remove and join-request approve/reject - add admin-gated /analytics/* endpoints: overview KPIs, login timeseries, login heatmap, top applications, member growth, member demographics, auth-method breakdown, group membership by source, join-request funnel, audit list + action summary Frontend: - add recharts; rebuild AnalyticsPage as a tabbed hub (Overview, Sign-ins, Applications, Members, Groups, Audit) with charts, stat cards, and a weekday x hour sign-in heatmap - gate the analytics nav item and page content behind admin membership
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4d979d2e06
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| db.Model(&model.GroupJoinRequest{}).Where("status = ? AND created_at >= ?", model.GroupJoinRequestStatusApproved, start).Count(&f.Approved) | ||
| db.Model(&model.GroupJoinRequest{}).Where("status = ? AND created_at >= ?", model.GroupJoinRequestStatusRejected, start).Count(&f.Rejected) |
There was a problem hiding this comment.
Filter join-request decisions by reviewed_at
When a request was created before the selected window but approved or rejected during it, these predicates exclude the decision because they filter on created_at. This makes the 7/30/90-day throughput counts—and the median query below, which uses the same filter—report activity according to request submission time rather than decision time. Use reviewed_at >= ? for the decided-request metrics.
Useful? React with 👍 / 👎.
| for _, r := range rows { | ||
| byDay[r.Day] = r | ||
| } | ||
| out := make([]LoginPoint, 0, days) |
There was a problem hiding this comment.
Bound the time-series range before allocating
A valid analytics caller can supply any positive days value, and queryInt passes it through unchanged. A request such as days=1000000000 reaches this allocation and loop, consuming excessive memory/CPU or crashing the process with an out-of-memory failure. Clamp the range to a reasonable maximum before querying or allocating; the other numeric analytics ranges should receive equivalent bounds.
Useful? React with 👍 / 👎.
| db.Model(&model.User{}).Count(&o.TotalUsers) | ||
| db.Model(&model.Entity{}).Where("type = ?", model.EntityTypeServiceAccount).Count(&o.TotalServiceAccounts) | ||
| db.Model(&model.Application{}).Count(&o.TotalApplications) | ||
| db.Model(&model.Group{}).Count(&o.TotalGroups) | ||
| db.Model(&model.User{}).Where("created_at > ?", now.AddDate(0, 0, -30)).Count(&o.NewUsers30d) |
There was a problem hiding this comment.
Return count-query failures instead of zeroed KPIs
If any KPI query fails—for example because a migration did not create a table or the database connection drops—its GORM error is discarded and the endpoint still returns HTTP 200 with that metric left at zero. This presents a database failure as valid analytics data even though the function and handler already expose an error path. Check each query's .Error and return it rather than emitting partial, plausible-looking totals.
Useful? React with 👍 / 👎.
Builds out the previously-placeholder Analytics page into a role-gated hub serving several audiences (devops: sign-ins/app usage; officers: member/subteam breakdowns; security: audit trail).
Backend (core)
audit_eventtable +service.RecordAuditEvent(best-effort — never blocks/fails the request)/analytics/*endpoints:GET /analytics/overview— headline KPIsGET /analytics/logins/timeseries— sign-ins + distinct members per day (gap-filled)GET /analytics/logins/heatmap— weekday × hour sign-in volumeGET /analytics/applications/top— apps ranked by sign-insGET /analytics/users/growth— new + cumulative members per monthGET /analytics/members/demographics— grad year / major / graduate levelGET /analytics/auth-methods— distinct members per auth methodGET /analytics/groups/membership— membership by source (direct/conditional/discord)GET /analytics/groups/join-requests— funnel + median decision timeGET /analytics/audit+GET /analytics/audit/summary— audit list + per-action countssentinel:allFrontend (web)
rechartsAnalyticsPageas a tabbed hub: Overview, Sign-ins, Applications, Members, Groups, AuditNotes / follow-ups
audit_eventrecords successful admin actions only. Failed-login and logout events aren't captured yet (would need new collection in the auth flow) — worth a phase 2 for richer security analytics.