diff --git a/scripts/collect_coder_analytics.py b/scripts/collect_coder_analytics.py index d5a6432..0dbad89 100755 --- a/scripts/collect_coder_analytics.py +++ b/scripts/collect_coder_analytics.py @@ -166,6 +166,56 @@ def calculate_workspace_total_usage(builds: list[dict[str, Any]]) -> float: return total_hours +def fetch_user_activity_insights( + api_url: str, session_token: str, start_time: str, end_time: str +) -> dict[str, float]: + """Fetch user activity insights from Coder API. + + Parameters + ---------- + api_url : str + The Coder API base URL + session_token : str + The Coder session token for authentication + start_time : str + Start time in ISO 8601 format (e.g., "2025-11-01T00:00:00Z") + end_time : str + End time in ISO 8601 format (e.g., "2025-12-10T00:00:00Z") + + Returns + ------- + dict[str, float] + Mapping of username (lowercase) -> active_hours + """ + url = f"{api_url}/api/v2/insights/user-activity" + headers = {"Coder-Session-Token": session_token} + params = {"start_time": start_time, "end_time": end_time} + + try: + response = requests.get(url, headers=headers, params=params, timeout=30) + response.raise_for_status() + data = response.json() + + # Create mapping of username -> active hours + activity_map = {} + users = data.get("report", {}).get("users", []) + for user in users: + username = user.get("username", "").lower() + seconds = user.get("seconds", 0) + hours = round(seconds / 3600.0, 2) + activity_map[username] = hours + + return activity_map + except requests.RequestException as e: + print(f"Warning: Failed to fetch user activity insights: {e}") + try: + error_details = response.json() if response else {} + print(f"Error details: {error_details}") + except Exception: + pass + return {} + + def get_team_mappings() -> dict[str, str]: """Get team mappings from Firestore. @@ -198,7 +248,7 @@ def get_team_mappings() -> dict[str, str]: def fetch_workspaces( team_mappings: dict[str, str], api_url: str, session_token: str ) -> list[dict[str, Any]]: - """Fetch all workspaces using Coder CLI and enrich with build history. + """Fetch workspaces using Coder CLI and enrich with build data. Parameters ---------- @@ -212,7 +262,7 @@ def fetch_workspaces( Returns ------- list[dict[str, Any]] - List of workspace objects with builds and usage hours + List of workspace objects with builds, usage hours, and active hours """ print("Fetching workspaces from Coder...") workspaces = run_command(["coder", "list", "-a", "-o", "json"]) @@ -239,8 +289,35 @@ def fetch_workspaces( print(f"✓ Fetched {len(filtered_workspaces)} workspaces") + # Fetch user activity insights (active hours) + # Use a wide time range to capture all activity + # Find earliest workspace creation date + print("Fetching user activity insights...") + earliest_created = min( + ( + datetime.fromisoformat(ws.get("created_at", "").replace("Z", "+00:00")) + for ws in filtered_workspaces + if ws.get("created_at") + ), + default=datetime.now(UTC), + ) + # Normalize to midnight (00:00:00) as required by the API + start_time = earliest_created.replace( + hour=0, minute=0, second=0, microsecond=0 + ).strftime("%Y-%m-%dT%H:%M:%SZ") + # Normalize end time to the start of the current hour (required by API) + now = datetime.now(UTC) + end_time = now.replace(minute=0, second=0, microsecond=0).strftime( + "%Y-%m-%dT%H:%M:%SZ" + ) + + activity_map = fetch_user_activity_insights( + api_url, session_token, start_time, end_time + ) + print(f"✓ Fetched activity data for {len(activity_map)} users") + # Enrich workspaces with full build history and usage hours - print("Enriching workspaces with build history...") + print("Enriching workspaces with build history and active hours...") for i, workspace in enumerate(filtered_workspaces, 1): workspace_id = workspace.get("id") if workspace_id: @@ -252,11 +329,17 @@ def fetch_workspaces( total_usage_hours = calculate_workspace_total_usage(builds) workspace["total_usage_hours"] = round(total_usage_hours, 2) + # Add active hours from activity insights + owner_name = workspace.get("owner_name", "").lower() + workspace["active_hours"] = activity_map.get(owner_name, 0.0) + # Progress indicator if i % 10 == 0: print(f" Processed {i}/{len(filtered_workspaces)} workspaces...") - print(f"✓ Enriched {len(filtered_workspaces)} workspaces with build history") + print( + f"✓ Enriched {len(filtered_workspaces)} workspaces with build history and active hours" + ) return filtered_workspaces diff --git a/services/analytics/app/components/templates-table.tsx b/services/analytics/app/components/templates-table.tsx index 730b52f..1ab606d 100644 --- a/services/analytics/app/components/templates-table.tsx +++ b/services/analytics/app/components/templates-table.tsx @@ -52,10 +52,15 @@ export function TemplatesTable({ templates }: TemplatesTableProps) { - + Total Hours + + + Active Hours + + {/* Actions */} @@ -103,6 +108,11 @@ export function TemplatesTable({ templates }: TemplatesTableProps) { {template.total_workspace_hours.toLocaleString()}h + + + {template.total_active_hours.toLocaleString()}h + + + handleSort('total_active_hours')} + > + +
+ Active Hours + {getSortIcon('total_active_hours')} +
+
+ handleSort('active_days')} @@ -334,6 +345,11 @@ export default function TemplateTeamsContent({ user, templateName }: TemplateTea {team.total_workspace_hours.toLocaleString()}h + + + {team.total_active_hours.toLocaleString()}h + + {team.active_days} diff --git a/services/analytics/lib/metrics.ts b/services/analytics/lib/metrics.ts index c8eeadd..e7bce4a 100644 --- a/services/analytics/lib/metrics.ts +++ b/services/analytics/lib/metrics.ts @@ -83,6 +83,19 @@ function getWorkspaceUsageHours(workspace: CoderWorkspace): number { } } +/** + * Get workspace active hours from pre-calculated field + * The collection script fetches this from Coder Insights API + */ +function getWorkspaceActiveHours(workspace: CoderWorkspace): number { + // Use pre-calculated active_hours from collection script + if (workspace.active_hours !== undefined && workspace.active_hours !== null) { + return workspace.active_hours; + } + + return 0; +} + /** * Classify activity status based on days since last active */ @@ -207,6 +220,7 @@ export function enrichWorkspaceData( const daysSinceActive = daysBetween(new Date(lastActive), now); const daysSinceCreated = daysBetween(new Date(workspace.created_at), now); const workspaceHours = getWorkspaceUsageHours(workspace); + const activeHours = getWorkspaceActiveHours(workspace); // Determine full name let ownerName = workspace.owner_name; @@ -231,6 +245,7 @@ export function enrichWorkspaceData( days_since_created: daysSinceCreated, days_since_active: daysSinceActive, workspace_hours: workspaceHours, + active_hours: activeHours, total_builds: workspace.latest_build.build_number, last_build_status: workspace.latest_build.job?.status || 'unknown', activity_status: classifyActivityStatus(daysSinceActive), @@ -263,6 +278,9 @@ export function aggregateByTeam(workspaces: WorkspaceMetrics[]): TeamMetrics[] { // Total workspace hours (sum of all workspace lifetime hours) const totalWorkspaceHours = teamWorkspaces.reduce((sum, w) => sum + w.workspace_hours, 0); + // Total active hours (sum of actual interaction hours from Insights API) + const totalActiveHours = teamWorkspaces.reduce((sum, w) => sum + w.active_hours, 0); + // Average workspace hours const avgWorkspaceHours = teamWorkspaces.length > 0 ? totalWorkspaceHours / teamWorkspaces.length : 0; @@ -319,6 +337,7 @@ export function aggregateByTeam(workspaces: WorkspaceMetrics[]): TeamMetrics[] { total_workspaces: teamWorkspaces.length, unique_active_users: activeUsers.size, total_workspace_hours: Math.round(totalWorkspaceHours), + total_active_hours: Math.round(totalActiveHours), avg_workspace_hours: Math.round(avgWorkspaceHours * 10) / 10, active_days: activeDates.size, template_distribution: templateDistribution, @@ -413,6 +432,9 @@ export function calculateTemplateMetrics( // Total workspace hours (sum of all workspace lifetime hours) const totalWorkspaceHours = templateWorkspaces.reduce((sum, w) => sum + w.workspace_hours, 0); + // Total active hours (sum of actual interaction hours from Insights API) + const totalActiveHours = templateWorkspaces.reduce((sum, w) => sum + w.active_hours, 0); + // Average workspace hours const avgWorkspaceHours = templateWorkspaces.length > 0 ? totalWorkspaceHours / templateWorkspaces.length : 0; @@ -438,6 +460,7 @@ export function calculateTemplateMetrics( active_workspaces: activeWorkspaces.length, unique_active_users: activeUsers.size, total_workspace_hours: Math.round(totalWorkspaceHours), + total_active_hours: Math.round(totalActiveHours), avg_workspace_hours: Math.round(avgWorkspaceHours * 10) / 10, team_distribution: teamDistribution, }; diff --git a/services/analytics/lib/types.ts b/services/analytics/lib/types.ts index e1ef771..8a0031d 100644 --- a/services/analytics/lib/types.ts +++ b/services/analytics/lib/types.ts @@ -15,6 +15,7 @@ export interface CoderWorkspace { template_icon?: string; name?: string; total_usage_hours?: number; // Total usage hours across all builds (added by collection script) + active_hours?: number; // Active interaction hours from Insights API (added by collection script) all_builds?: any[]; // Full build history (added by collection script) latest_build: { id: string; @@ -126,6 +127,7 @@ export interface WorkspaceMetrics { days_since_created: number; days_since_active: number; workspace_hours: number; // Total usage hours (from first connection to last connection) + active_hours: number; // Actual active interaction hours from Insights API // Build metrics total_builds: number; @@ -144,6 +146,7 @@ export interface TeamMetrics { // Time-based metrics total_workspace_hours: number; // Sum of all workspace usage hours (first to last connection) + total_active_hours: number; // Sum of actual active interaction hours from Insights API avg_workspace_hours: number; // Average workspace usage hours (first to last connection) active_days: number; // Number of unique days with workspace activity @@ -192,6 +195,7 @@ export interface TemplateMetrics { active_workspaces: number; unique_active_users: number; // Number of unique users with activity in last 7 days total_workspace_hours: number; // Sum of workspace usage hours (first to last connection) for this template + total_active_hours: number; // Sum of actual active interaction hours from Insights API avg_workspace_hours: number; // Average workspace usage hours (first to last connection) team_distribution: Record; }