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;
}