Files
resolutionflow/docs/plans/archive/plan-improvements-for-analytics.md
chihlasm 932927b9df chore: archive old plan docs + add survey foundation files
Move completed plan docs to docs/plans/archive/. Add survey migration 046
and reference HTML/plan files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 02:03:38 -05:00

365 lines
13 KiB
Markdown

# Post-Implementation Improvements — Analytics & Feedback
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Context:** The analytics & feedback feature has just been implemented from `docs/plans/2026-02-15-analytics-feedback-implementation.md`. These are five targeted improvements identified from a comparative review. Apply them to the existing code on the current branch (`feat/analytics-feedback`).
**Reference files you'll need to modify:**
- `backend/app/api/endpoints/analytics.py` — analytics query logic
- `backend/app/schemas/analytics.py` — response schemas
- `backend/app/api/endpoints/ratings.py` — rating endpoints
- `backend/app/api/endpoints/steps.py` — existing step rating routes
- `frontend/src/types/analytics.ts` — TypeScript types
- `frontend/src/pages/TeamAnalyticsPage.tsx` — team dashboard
---
## Task 1: Switch from Average to Median Duration
**Why:** One 3-hour debugging session shouldn't skew stats for a flow that normally takes 8 minutes. Median is the standard for time-based metrics in analytics.
**Backend changes:**
In `backend/app/schemas/analytics.py`:
- Rename `avg_duration_minutes``median_duration_minutes` in `AnalyticsSummary`, `TopFlow`, and `TopEngineer`
In `backend/app/api/endpoints/analytics.py`:
- Replace `func.avg(...)` duration calculations with a median approach
- PostgreSQL supports `percentile_cont(0.5) WITHIN GROUP (ORDER BY ...)` for median
- Example replacement for the `_build_summary` helper:
```python
from sqlalchemy import text
# Replace the avg duration query with:
duration_q = await db.execute(
select(
func.percentile_cont(0.5).within_group(
func.extract('epoch', Session.completed_at - Session.started_at) / 60
)
).where(*base_filter, Session.completed_at.isnot(None))
)
median_duration = round(float(duration_q.scalar() or 0), 1)
```
- Apply the same change to the top_flows and top_engineers subqueries (replace `func.avg` with `percentile_cont(0.5)` for duration)
- Update all references from `avg_duration` to `median_duration` in the response construction
**Frontend changes:**
In `frontend/src/types/analytics.ts`:
- Rename `avg_duration_minutes``median_duration_minutes` in `TopFlow`, `TopEngineer`, and `AnalyticsSummary`
In `frontend/src/pages/TeamAnalyticsPage.tsx` and `frontend/src/pages/MyAnalyticsPage.tsx`:
- Update stat card label from "Avg Duration" → "Median Duration"
- Update any references to `avg_duration_minutes``median_duration_minutes`
In `frontend/src/components/analytics/FlowAnalyticsPanel.tsx`:
- Same rename for the flow summary stat card
**Verify:** `cd backend && python -m pytest --override-ini="addopts=" -v` and `cd frontend && npm run build`
**Commit:** `git commit -am "refactor: use median instead of average for duration metrics"`
---
## Task 2: Add Step Dropoff Metrics to Flow Analytics
**Why:** Dropoff data is passive (requires no user action) and tells flow authors exactly where engineers get stuck and abandon sessions. This is the most actionable metric for flow improvement.
**Backend changes:**
In `backend/app/schemas/analytics.py`:
- Add fields to `StepFeedbackSummary`:
```python
class StepFeedbackSummary(BaseModel):
node_id: str
node_title: str
helpful_yes: int
helpful_no: int
helpful_rate: float
visit_count: int = 0 # NEW
dropoff_count: int = 0 # NEW
dropoff_rate: float = 0.0 # NEW
```
In `backend/app/api/endpoints/analytics.py`, in the `get_flow_analytics` function:
- After the existing step_feedback section, add dropoff calculation logic:
```python
# Step dropoff analysis — build from session decisions JSONB
# For each session in the period for this tree:
# - Extract all node_ids from the decisions array
# - The LAST node_id in an incomplete session = dropoff point
# - Count visits per node and dropoffs per node
from sqlalchemy.dialects.postgresql import JSONB
# Get all sessions for this tree in period
sessions_q = await db.execute(
select(Session.id, Session.decisions, Session.completed_at)
.where(Session.tree_id == tree_id, Session.started_at >= period_start)
)
sessions_data = sessions_q.all()
# Build node visit and dropoff maps
node_visits: dict[str, int] = {}
node_dropoffs: dict[str, int] = {}
for sess in sessions_data:
decisions = sess.decisions or []
for decision in decisions:
node_id = decision.get("node_id") or decision.get("nodeId", "")
if node_id:
node_visits[node_id] = node_visits.get(node_id, 0) + 1
# If session not completed, last decision node is a dropoff
if not sess.completed_at and decisions:
last_decision = decisions[-1]
last_node = last_decision.get("node_id") or last_decision.get("nodeId", "")
if last_node:
node_dropoffs[last_node] = node_dropoffs.get(last_node, 0) + 1
# Merge dropoff data into step_feedback list
# Get node titles from the tree's content JSONB
tree_result = await db.execute(select(Tree.content).where(Tree.id == tree_id))
tree_content = tree_result.scalar_one_or_none() or {}
nodes = tree_content.get("nodes", [])
node_title_map = {n.get("id", ""): n.get("title", n.get("label", "Unnamed")) for n in nodes}
# Build combined step feedback with visits + dropoffs + thumbs
all_node_ids = set(list(node_visits.keys()) + [sf.node_id for sf in step_feedback])
combined_feedback = []
for nid in all_node_ids:
visits = node_visits.get(nid, 0)
dropoffs = node_dropoffs.get(nid, 0)
# Find existing thumbs data if any
existing = next((sf for sf in step_feedback if sf.node_id == nid), None)
combined_feedback.append(StepFeedbackSummary(
node_id=nid,
node_title=node_title_map.get(nid, "Unknown Step"),
helpful_yes=existing.helpful_yes if existing else 0,
helpful_no=existing.helpful_no if existing else 0,
helpful_rate=existing.helpful_rate if existing else 0.0,
visit_count=visits,
dropoff_count=dropoffs,
dropoff_rate=round(dropoffs / visits, 3) if visits > 0 else 0.0,
))
# Sort by dropoff_rate descending so worst steps are first
combined_feedback.sort(key=lambda x: x.dropoff_rate, reverse=True)
step_feedback = combined_feedback
```
**Frontend changes:**
In `frontend/src/types/analytics.ts`:
- Add to `StepFeedbackSummary`:
```typescript
export interface StepFeedbackSummary {
node_id: string
node_title: string
helpful_yes: number
helpful_no: number
helpful_rate: number
visit_count: number // NEW
dropoff_count: number // NEW
dropoff_rate: number // NEW
}
```
In `frontend/src/components/analytics/FlowAnalyticsPanel.tsx`:
- Add dropoff columns to the step feedback table: "Visits", "Dropoffs", "Dropoff Rate"
- Highlight rows where `dropoff_rate > 0.2` with a subtle red/warning background
**Verify:** `cd backend && python -m pytest --override-ini="addopts=" -v` and `cd frontend && npm run build`
**Commit:** `git commit -am "feat: add step dropoff metrics to flow analytics"`
---
## Task 3: Add Backward-Compatible /ratings Alias Routes
**Why:** The existing backend uses `/{step_id}/rate` but the frontend uses `/{step_id}/ratings`. This is a real inconsistency that needs fixing.
**Backend changes:**
In `backend/app/api/endpoints/steps.py`:
- Find the existing routes: `POST /{step_id}/rate`, `PUT /{step_id}/rate`, `DELETE /{step_id}/rate`
- Add alias routes that point to the same handler functions:
```python
# After the existing rate_step function:
@router.post("/{step_id}/ratings", response_model=StepRatingResponse, status_code=status.HTTP_201_CREATED,
include_in_schema=False)
async def rate_step_alias(
step_id: UUID,
rating_data: StepRatingCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Alias for POST /{step_id}/rate — backward compatibility."""
return await rate_step(step_id, rating_data, db, current_user)
@router.put("/{step_id}/ratings", response_model=StepRatingResponse,
include_in_schema=False)
async def update_rating_alias(
step_id: UUID,
rating_data: StepRatingUpdate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Alias for PUT /{step_id}/rate — backward compatibility."""
return await update_rating(step_id, rating_data, db, current_user)
@router.delete("/{step_id}/ratings", status_code=204,
include_in_schema=False)
async def delete_rating_alias(
step_id: UUID,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Alias for DELETE /{step_id}/rate — backward compatibility."""
return await delete_rating(step_id, db, current_user)
```
Note: Use `include_in_schema=False` to keep the OpenAPI docs clean — only the canonical `/rate` routes appear in docs.
**Verify:** `cd backend && python -m pytest --override-ini="addopts=" -v`
**Commit:** `git commit -am "fix: add /ratings alias routes for backward compatibility"`
---
## Task 4: Anonymize Feedback Comments in Analytics
**Why:** In a team tool where managers see feedback, engineers will give more honest feedback if their identity isn't attached. Anonymous feedback = better signal.
**Backend changes:**
In `backend/app/schemas/analytics.py`:
- Remove `user_name` from `FlowRatingItem`:
```python
class FlowRatingItem(BaseModel):
rating: int
comment: Optional[str]
created_at: datetime
# user_name removed — feedback is anonymous in analytics views
```
In `backend/app/api/endpoints/analytics.py`, in `get_flow_analytics`:
- Remove the `User.name` join from the recent_comments query:
```python
# Recent comments — anonymous
comments_q = await db.execute(
select(SessionRating.rating, SessionRating.comment, SessionRating.created_at)
.where(
SessionRating.tree_id == tree_id,
SessionRating.comment.isnot(None),
SessionRating.comment != "",
)
.order_by(SessionRating.created_at.desc())
.limit(10)
)
recent_comments = [
FlowRatingItem(
rating=row.rating,
comment=row.comment,
created_at=row.created_at,
)
for row in comments_q.all()
]
```
**Frontend changes:**
In `frontend/src/types/analytics.ts`:
- Remove `user_name` from `FlowRatingItem`:
```typescript
export interface FlowRatingItem {
rating: number
comment?: string
created_at: string
// user_name removed — anonymous feedback
}
```
In `frontend/src/components/analytics/FlowAnalyticsPanel.tsx`:
- Remove any rendering of `user_name` in the recent comments list
- Comments should show: star rating + comment text + relative timestamp only
**Verify:** `cd backend && python -m pytest --override-ini="addopts=" -v` and `cd frontend && npm run build`
**Commit:** `git commit -am "privacy: anonymize feedback comments in analytics views"`
---
## Task 5: Add Explicit active_engineers Metric
**Why:** "How many of my techs actually used this tool this month" is a key adoption metric for MSP managers.
**Backend changes:**
In `backend/app/schemas/analytics.py`:
- Verify `AnalyticsSummary` already has an `active_engineers` field. If not, add:
```python
class AnalyticsSummary(BaseModel):
total_sessions: int
completed_sessions: int
completion_rate: float
median_duration_minutes: float
active_engineers: int = 0 # ADD if missing
outcome_breakdown: OutcomeBreakdown
```
In `backend/app/api/endpoints/analytics.py`:
- In the `_build_summary` helper (or the team analytics endpoint), add the active engineers count:
```python
# Active engineers: distinct users with >= 1 session in window
active_q = await db.execute(
select(func.count(func.distinct(Session.user_id)))
.where(*base_filter)
)
active_engineers = active_q.scalar() or 0
```
- Include `active_engineers=active_engineers` in the `AnalyticsSummary(...)` construction
- For personal analytics (`/me`), set `active_engineers=1` (it's always just the requesting user)
**Frontend changes:**
In `frontend/src/types/analytics.ts`:
- Verify `AnalyticsSummary` has `active_engineers: number`. Add if missing.
In `frontend/src/pages/TeamAnalyticsPage.tsx`:
- Verify there's a stat card for "Active Engineers". If missing, add one using the `summary.active_engineers` value.
**Verify:** `cd backend && python -m pytest --override-ini="addopts=" -v` and `cd frontend && npm run build`
**Commit:** `git commit -am "feat: add explicit active_engineers metric to team analytics"`
---
## Final: Full Test Suite + Build Verification
```bash
cd backend && python -m pytest --override-ini="addopts=" -v
cd frontend && npm run build
```
Fix any failures, then:
```bash
git add -A
git commit -m "fix: post-improvement test and build fixes"
```