Remove 15 completed/superseded plan documents. Add analytics improvements reference and visual QA design migration notes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
13 KiB
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 logicbackend/app/schemas/analytics.py— response schemasbackend/app/api/endpoints/ratings.py— rating endpointsbackend/app/api/endpoints/steps.py— existing step rating routesfrontend/src/types/analytics.ts— TypeScript typesfrontend/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_minutesinAnalyticsSummary,TopFlow, andTopEngineer
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_summaryhelper:
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.avgwithpercentile_cont(0.5)for duration) - Update all references from
avg_durationtomedian_durationin the response construction
Frontend changes:
In frontend/src/types/analytics.ts:
- Rename
avg_duration_minutes→median_duration_minutesinTopFlow,TopEngineer, andAnalyticsSummary
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:
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:
# 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:
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.2with 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:
# 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_namefromFlowRatingItem:
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.namejoin from the recent_comments query:
# 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_namefromFlowRatingItem:
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_namein 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
AnalyticsSummaryalready has anactive_engineersfield. If not, add:
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_summaryhelper (or the team analytics endpoint), add the active engineers count:
# 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_engineersin theAnalyticsSummary(...)construction - For personal analytics (
/me), setactive_engineers=1(it's always just the requesting user)
Frontend changes:
In frontend/src/types/analytics.ts:
- Verify
AnalyticsSummaryhasactive_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_engineersvalue.
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
cd backend && python -m pytest --override-ini="addopts=" -v
cd frontend && npm run build
Fix any failures, then:
git add -A
git commit -m "fix: post-improvement test and build fixes"