Skip to content

feat(analytics): analytics hub with aggregation endpoints and audit trail - #110

Open
BK1031 wants to merge 1 commit into
mainfrom
bk1031/analytics-hub
Open

feat(analytics): analytics hub with aggregation endpoints and audit trail#110
BK1031 wants to merge 1 commit into
mainfrom
bk1031/analytics-hub

Conversation

@BK1031

@BK1031 BK1031 commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

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)

  • New audit_event table + service.RecordAuditEvent (best-effort — never blocks/fails the request)
  • Instrumented mutations: application create/delete, application secret reveal, group member add/remove, join-request approve/reject
  • New admin-gated /analytics/* endpoints:
    • GET /analytics/overview — headline KPIs
    • GET /analytics/logins/timeseries — sign-ins + distinct members per day (gap-filled)
    • GET /analytics/logins/heatmap — weekday × hour sign-in volume
    • GET /analytics/applications/top — apps ranked by sign-ins
    • GET /analytics/users/growth — new + cumulative members per month
    • GET /analytics/members/demographics — grad year / major / graduate level
    • GET /analytics/auth-methods — distinct members per auth method
    • GET /analytics/groups/membership — membership by source (direct/conditional/discord)
    • GET /analytics/groups/join-requests — funnel + median decision time
    • GET /analytics/audit + GET /analytics/audit/summary — audit list + per-action counts
  • All analytics endpoints gated to first-party admin sessions / sentinel:all

Frontend (web)

  • Added recharts
  • Rebuilt AnalyticsPage as a tabbed hub: Overview, Sign-ins, Applications, Members, Groups, Audit
  • Charts: area (sign-in trend), CSS heatmap, horizontal/stacked bars, composed growth chart, donuts; themed tooltip + brand palette; stat cards; loading/empty states
  • Gated the Analytics nav item and page content behind admin membership

Notes / follow-ups

  • audit_event records 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.
  • "Officer" access currently means Admins-group membership; there's no separate officer role in the backend yet.
  • Login timestamps aggregate in UTC (matches the DB session timezone).

…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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread core/service/analytics.go
Comment on lines +349 to +350
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread core/service/analytics.go
for _, r := range rows {
byDay[r.Day] = r
}
out := make([]LoginPoint, 0, days)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread core/service/analytics.go
Comment on lines +39 to +43
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant