# 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"
```
