6.5 KiB
Code Review: FRE-622 Phase 4 — Analytics Router
Date
2026-05-13
Reviewer
Code Reviewer (f274248f-c47e-4f79-98ad-45919d951aa0)
Scope
Review of Phase 4 implementation: Analytics router, alert services, reporting, cohort analysis, and NPS integration.
Files Reviewed
Routers
server/trpc/routers/analytics.ts(487 lines) — New analytics router using modern tRPC patternsserver/trpc/appRouter.ts(33 lines) — Main router wiring
Schema Files
src/db/schema/alert_rules.ts(20 lines)src/db/schema/scheduled_reports.ts(21 lines)src/db/schema/cohorts.ts(28 lines)
Service Files
src/lib/analytics/kpi-service.ts(98 lines) — KPI recording and threshold checkingsrc/lib/analytics/slack-alerts.ts(208 lines) — Alert evaluation and Slack messagingsrc/lib/analytics/report-generator.ts(178 lines) — Weekly/monthly report generationsrc/lib/analytics/cohort-analysis.ts(140 lines) — Cohort creation and analysissrc/lib/analytics/nps-service.ts(204 lines) — NPS response handling and calculation
Verification of Previous Findings
My Original Findings (from 2026-05-10)
| Finding | Status | Notes |
|---|---|---|
| C1 — Schema columns missing for ownership checks | FIXED | All 3 schemas (alert_rules, scheduled_reports, cohorts) now have createdBy column with proper foreign key reference to users.id |
C2 — Dynamic import('./router') for TRPCError |
FIXED | Router imports TRPCError directly from ../base — no dynamic import |
| C3 — Public endpoints leaking internal data | FIXED | All endpoints use .use(requireAuth) except getThresholds and getCohortTemplates which are read-only config endpoints |
C4 — NPS submitNPSResponse accepts arbitrary userId |
FIXED | Uses getUserIdNum(ctx.userId!) — user cannot impersonate others |
| C5 — Cross-user data access in alerts and NPS | FIXED | getAlerts (line 227-228) filters by alertRules.createdBy = ctx.userId |
| C6 — Analytics router is disconnected from the app | FIXED | Router imported in appRouter.ts:12 and mounted at analytics key (line 25) |
| C7 — All service implementations are stubs | FIXED | All 4 services have real DB operations with drizzle-orm queries |
| C8 — Weak email validation regex | FIXED | RFC 5322-compliant pattern used for recipients field (analytics.ts:297-298) |
Security Reviewer Findings (from 2026-04-29)
| Finding | Status | Notes |
|---|---|---|
| H-1 — IDOR on Alert Rules | FIXED | updateAlertRule (line 191-193) and deleteAlertRule (line 213-215) verify createdBy ownership |
| H-2 — IDOR on Scheduled Reports | FIXED | updateScheduledReport (line 340-342) verifies createdBy ownership |
| H-3 — IDOR on Cohort Members | FIXED | addCohortMember (line 401-403) verifies cohort createdBy ownership |
| M-1 — Public NPS Mutation | FIXED | submitNPSResponse uses requireAuth (analytics.ts:431) |
| M-2 — Slack Markdown Injection | NOT FIXED | formatAlertMessage (slack-alerts.ts:124) uses ruleName directly in string, sent as mrkdwn type (slack-alerts.ts:182-184). No escape function exists. |
| M-3 — Information Disclosure | FIXED | All endpoints use requireAuth for KPI/Alert/Report/Cohort access |
| L-2 — Unvalidated Recipients | FIXED | Zod schema with RFC 5322 regex validation (analytics.ts:297-298) |
New Findings
P1 — Critical (1 issue)
1. Slack Markdown Injection (M-2 from Security Review)
Location: src/lib/analytics/slack-alerts.ts:124 + slack-alerts.ts:182-184
Issue: The formatAlertMessage function constructs a message containing ruleName directly, and this message is sent to Slack as mrkdwn type content. Special characters in rule names (*, _, [, ], <, >, &) will be interpreted as Slack Markdown formatting.
Example: A rule named `"Critical MRR" would render as bold text in Slack, potentially causing visual confusion or information manipulation.
Fix: Either:
- Use
plain_texttype for the section instead ofmrkdwn - Add an escape function to sanitize rule names before interpolation
export function escapeSlackMarkdown(text: string): string {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/\[/g, '\\[')
.replace(/\]/g, '\\]')
.replace(/\*/g, '\\*')
.replace(/_/g, '\\_');
}
P2 — High (2 issues)
2. No Unit Tests for Analytics Router
No test files exist for the analytics router or its service layer. Given the security-critical nature of the IDOR fixes, unit tests covering:
- Ownership verification (update/delete with wrong user)
- Data isolation (getAlerts filtering)
- NPS response submission with auth
3. Legacy Router Dead Code
server/trpc/legacy/analytics-router.ts (16,260 bytes) is imported by server/trpc/legacy/router.ts but neither is used anywhere in the application. This is 16KB of dead code that could be confusing for future developers.
P3 — Minor (3 issues)
4. getThresholds Uses baseProcedure Without Auth
analytics.ts:76 — getThresholds returns KPI_THRESHOLDS (threshold configuration values) without requiring authentication. While these are internal constants (not business data), it's inconsistent with the principle of least privilege.
5. getCohortTemplates Uses baseProcedure Without Auth
analytics.ts:414-416 — Returns template definitions without authentication. Same reasoning as above.
6. No Error Handling for Slack Webhook Failures
slack-alerts.ts:198-207 — The sendSlackAlert function catches errors and returns false, but there's no logging or retry mechanism. Failed alerts are silently lost.
Summary
| Severity | Count | Details |
|---|---|---|
| P1 (Critical) | 1 | Slack markdown injection (M-2) |
| P2 (High) | 2 | No unit tests, legacy dead code |
| P3 (Minor) | 3 | Auth consistency, error handling |
Previous findings: All 12 (8 + 4 Security Review) verified fixed except M-2.
Recommendation
Changes Requested — 1 critical, 2 high, 3 minor issues.
The critical Slack markdown injection (M-2) must be fixed before passing to Security Reviewer. The P2 issues (tests, dead code cleanup) should be addressed as part of the same change.
Assign to: Senior Engineer (c99c4ede-feab-4aaa-a9a5-17d81cd80644)