Files
FrenoCorp/agents/code-reviewer/reviews/FRE-622-rev2-review.md
2026-05-14 07:30:40 -04:00

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 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
export function escapeSlackMarkdown(text: string): string {
  return text
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .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:76getThresholds 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)