Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ dependencies = [
"pandas>=2.3.3",
"urllib3==2.6.3", # Pinning version to address vulnerabilities GHSA-gm62-xv2j-4w53 and GHSA-2xpw-w6gg-jr37
"filelock==3.20.3",
"authlib==1.6.6",
"authlib==1.6.7", # Pinning version to address vulnerability CVE-2026-28802
"virtualenv==20.36.1",
"cryptography>=46.0.5", # Pinning version to address vulnerability CVE-2026-26007
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { AnalyticsSnapshot, TeamMetrics } from '@/lib/types';
import { Tooltip } from '@/app/components/tooltip';
import { aggregateByCompany } from '@/lib/company-utils';

type SortColumn = 'team_name' | 'workspaces_for_template' | 'unique_active_users' | 'total_workspace_hours' | 'total_active_hours' | 'active_days';
type SortColumn = 'team_name' | 'workspaces_for_template' | 'unique_active_users' | 'template_active_hours' | 'active_days';
type SortDirection = 'asc' | 'desc';

interface TemplateTeamsContentProps {
Expand Down Expand Up @@ -61,15 +61,16 @@ export default function TemplateTeamsContent({ user, templateName }: TemplateTea
const template = data?.template_metrics.find(t => t.template_name === decodeURIComponent(templateName));

// Filter team metrics for this template
const templateTeams: (TeamMetrics & { workspaces_for_template: number })[] = useMemo(() => {
const templateTeams: (TeamMetrics & { workspaces_for_template: number; template_active_hours: number })[] = useMemo(() => {
if (!data || !template) return [];

return data.team_metrics
.map(team => {
const workspacesForTemplate = template.team_distribution[team.team_name] || 0;
return {
...team,
workspaces_for_template: workspacesForTemplate
workspaces_for_template: workspacesForTemplate,
template_active_hours: template.team_active_hours?.[team.team_name] ?? 0,
};
})
.filter(team => team.workspaces_for_template > 0);
Expand Down Expand Up @@ -343,12 +344,12 @@ export default function TemplateTeamsContent({ user, templateName }: TemplateTea
</th>
<th
className="px-6 py-4 text-right text-xs font-semibold text-slate-700 dark:text-slate-300 uppercase tracking-wider cursor-pointer hover:bg-slate-200 dark:hover:bg-slate-700 transition-colors"
onClick={() => handleSort('total_active_hours')}
onClick={() => handleSort('template_active_hours')}
>
<Tooltip content="Total hours with active workspace app connections (IDE, terminal, etc.) - measured from Coder Insights API">
<div className="flex items-center justify-end gap-2">
Active Hours
{getSortIcon('total_active_hours')}
{getSortIcon('template_active_hours')}
</div>
</Tooltip>
</th>
Expand Down Expand Up @@ -386,7 +387,7 @@ export default function TemplateTeamsContent({ user, templateName }: TemplateTea
</td>
<td className="px-6 py-4 text-right text-sm">
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-800">
{(template.team_active_hours?.[team.team_name] ?? 0).toLocaleString()}h
{team.template_active_hours.toLocaleString()}h
</span>
</td>
<td className="px-6 py-4 text-right text-sm text-vector-turquoise">
Expand Down
8 changes: 5 additions & 3 deletions services/analytics/lib/company-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ export function extractCompanyName(teamName: string): string {
* - Calculate active_days as union of all dates (not sum)
*/
export function aggregateByCompany(
teams: (TeamMetrics & { workspaces_for_template: number })[]
): (TeamMetrics & { workspaces_for_template: number })[] {
teams: (TeamMetrics & { workspaces_for_template: number; template_active_hours: number })[]
): (TeamMetrics & { workspaces_for_template: number; template_active_hours: number })[] {
// Group teams by company using Map
const companyMap = new Map<string, (TeamMetrics & { workspaces_for_template: number })[]>();
const companyMap = new Map<string, (TeamMetrics & { workspaces_for_template: number; template_active_hours: number })[]>();

teams.forEach(team => {
const company = extractCompanyName(team.team_name);
Expand All @@ -70,6 +70,7 @@ export function aggregateByCompany(
const total_workspace_hours = companyTeams.reduce((sum, t) => sum + t.total_workspace_hours, 0);
const total_active_hours = companyTeams.reduce((sum, t) => sum + t.total_active_hours, 0);
const workspaces_for_template = companyTeams.reduce((sum, t) => sum + t.workspaces_for_template, 0);
const template_active_hours = companyTeams.reduce((sum, t) => sum + t.template_active_hours, 0);
const avg_workspace_hours = total_workspaces > 0 ? total_workspace_hours / total_workspaces : 0;

// Deduplicate members by github_handle, keep most recent activity
Expand Down Expand Up @@ -119,6 +120,7 @@ export function aggregateByCompany(
avg_workspace_hours: Math.round(avg_workspace_hours * 10) / 10,
active_days: activeDates.size,
workspaces_for_template,
template_active_hours: Math.round(template_active_hours),
template_distribution,
members
};
Expand Down
96 changes: 1 addition & 95 deletions services/analytics/lib/firestore.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Firestore } from '@google-cloud/firestore';
import type { ParticipantData, TeamData } from './types';
import type { TeamData } from './types';

const projectId = process.env.GCP_PROJECT_ID || 'coderd';
const databaseId = process.env.FIRESTORE_DATABASE_ID || 'onboarding';
Expand All @@ -10,40 +10,6 @@ const db = new Firestore({
databaseId,
});

/**
* Get team mappings for all participants
* Maps GitHub handle (lowercase) to participant data including team_name
* @returns Map of github_handle -> ParticipantData
*/
export async function getTeamMappings(): Promise<Map<string, ParticipantData>> {
try {
const snapshot = await db.collection('participants').get();
const mappings = new Map<string, ParticipantData>();

snapshot.forEach((doc) => {
const data = doc.data();
const participantData: ParticipantData = {
github_handle: doc.id,
team_name: data.team_name || 'Unassigned',
first_name: data.first_name,
last_name: data.last_name,
email: data.email,
onboarded: data.onboarded,
onboarded_at: data.onboarded_at,
};

// Store with lowercase key for case-insensitive matching
mappings.set(doc.id.toLowerCase(), participantData);
});

console.log(`Loaded ${mappings.size} participant mappings from Firestore`);
return mappings;
} catch (error) {
console.error('Error fetching team mappings from Firestore:', error);
throw new Error(`Failed to fetch team mappings: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}

/**
* Get all teams with their participant lists
* @returns Array of team data
Expand Down Expand Up @@ -74,63 +40,3 @@ export async function getAllTeams(): Promise<TeamData[]> {
throw new Error(`Failed to fetch teams: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}

/**
* Get a single participant by GitHub handle
* @param githubHandle GitHub username (case-insensitive)
* @returns ParticipantData or null if not found
*/
export async function getParticipant(githubHandle: string): Promise<ParticipantData | null> {
try {
const normalizedHandle = githubHandle.toLowerCase();
const doc = await db.collection('participants').doc(normalizedHandle).get();

if (!doc.exists) {
return null;
}

const data = doc.data()!;
return {
github_handle: doc.id,
team_name: data.team_name || 'Unassigned',
first_name: data.first_name,
last_name: data.last_name,
email: data.email,
onboarded: data.onboarded,
onboarded_at: data.onboarded_at,
};
} catch (error) {
console.error(`Error fetching participant ${githubHandle}:`, error);
return null;
}
}

/**
* Get a single team by name
* @param teamName Team name
* @returns TeamData or null if not found
*/
export async function getTeam(teamName: string): Promise<TeamData | null> {
try {
const doc = await db.collection('teams').doc(teamName).get();

if (!doc.exists) {
return null;
}

const data = doc.data()!;
return {
team_name: doc.id,
participants: data.participants || [],
openai_api_key: data.openai_api_key,
openai_api_key_name: data.openai_api_key_name,
langfuse_secret_key: data.langfuse_secret_key,
langfuse_public_key: data.langfuse_public_key,
created_at: data.created_at,
updated_at: data.updated_at,
};
} catch (error) {
console.error(`Error fetching team ${teamName}:`, error);
return null;
}
}
10 changes: 0 additions & 10 deletions services/analytics/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
// ===== Firestore Types =====

export interface ParticipantData {
github_handle: string;
team_name: string;
first_name?: string;
last_name?: string;
email?: string;
onboarded?: boolean;
onboarded_at?: string;
}

export interface TeamData {
team_name: string;
participants: string[]; // Array of github_handles
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const ORG_NAME = 'AI-Engineering-Platform';
const statusCache = new Map<string, { status: string; timestamp: number }>();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes

export type GitHubStatus = 'member' | 'pending' | 'not_invited';
type GitHubStatus = 'member' | 'pending' | 'not_invited';

interface GitHubStatusResponse {
github_handle: string;
Expand Down
1 change: 0 additions & 1 deletion services/onboarding-status-web/app/dashboard-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ interface Summary {

interface ApiResponse {
participants: Participant[];
summary: Summary;
}

interface DashboardContentProps {
Expand Down
Loading