# 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 patterns - `server/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 checking - `src/lib/analytics/slack-alerts.ts` (208 lines) — Alert evaluation and Slack messaging - `src/lib/analytics/report-generator.ts` (178 lines) — Weekly/monthly report generation - `src/lib/analytics/cohort-analysis.ts` (140 lines) — Cohort creation and analysis - `src/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_text` type for the section instead of `mrkdwn` - Add an escape function to sanitize rule names before interpolation ```typescript export function escapeSlackMarkdown(text: string): string { return text .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)