Path: blob/main/components/dashboard/src/insights/download/download-sessions.ts
2501 views
/**1* Copyright (c) 2024 Gitpod GmbH. All rights reserved.2* Licensed under the GNU Affero General Public License (AGPL).3* See License.AGPL.txt in the project root for license information.4*/56import {7ListWorkspaceSessionsRequest,8PrebuildInitializer,9WorkspaceSession,10WorkspaceSpec_WorkspaceType,11} from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";12import { workspaceClient } from "../../service/public-api";13import dayjs from "dayjs";14import { useQuery, useQueryClient } from "@tanstack/react-query";15import { useCallback } from "react";16import { noPersistence } from "../../data/setup";17import { Duration, Timestamp } from "@bufbuild/protobuf";1819const pageSize = 100;20const maxPages = 100; // safety limit if something goes wrong with pagination2122type GetAllWorkspaceSessionsArgs = Pick<ListWorkspaceSessionsRequest, "to" | "from" | "organizationId"> & {23signal?: AbortSignal;24onProgress?: (percentage: number) => void;25};26export const getAllWorkspaceSessions = async ({27from,28to,29signal,30organizationId,31onProgress,32}: GetAllWorkspaceSessionsArgs): Promise<WorkspaceSession[]> => {33const records: WorkspaceSession[] = [];34let page = 0;35while (!signal?.aborted && page < maxPages) {36const response = await workspaceClient.listWorkspaceSessions(37{38organizationId,39from,40to,41pagination: {42page,43pageSize,44},45},46{47signal,48},49);50if (response.workspaceSessions.length === 0) {51break;52}5354records.push(...response.workspaceSessions);55onProgress && onProgress(page);5657page = page + 1;58}5960return records;61};6263type Args = Pick<ListWorkspaceSessionsRequest, "organizationId" | "from" | "to"> & {64organizationName: string;65signal?: AbortSignal;66onProgress?: (percentage: number) => void;67};6869export type DownloadInsightsCSVResponse = {70blob: Blob | null;71filename: string;72count: number;73};7475const downloadInsightsCSV = async ({76organizationId,77from,78to,79organizationName,80signal,81onProgress,82}: Args): Promise<DownloadInsightsCSVResponse> => {83const start = dayjs(from?.toDate()).format("YYYYMMDD");84const end = dayjs(to?.toDate()).format("YYYYMMDD");85const filename = `gitpod-sessions-${organizationName}-${start}-${end}.csv`;8687const records = await getAllWorkspaceSessions({88organizationId,89from,90to,91signal,92onProgress,93});9495if (records.length === 0) {96return {97blob: null,98filename,99count: 0,100};101}102103const rows = records.map(transformSessionRecord).filter((r) => !!r);104const fields = Object.keys(rows[0]) as (keyof ReturnType<typeof transformSessionRecord>)[];105106// TODO: look into a lib to handle this more robustly107// CSV Rows108const csvRows = rows.map((row) => {109const rowString = fields110.map((fieldName) => {111const value = row[fieldName];112if (typeof value === "bigint") {113return value.toString();114}115116return JSON.stringify(row[fieldName]);117})118.join(",");119120return rowString;121});122123// Prepend Header124csvRows.unshift(fields.join(","));125126const blob = new Blob([`\ufeff${csvRows.join("\n")}`], {127type: "text/csv;charset=utf-8",128});129130return {131blob,132filename,133count: rows.length,134};135};136137export const displayWorkspaceType = (type?: WorkspaceSpec_WorkspaceType) => {138switch (type) {139case WorkspaceSpec_WorkspaceType.PREBUILD:140return "prebuild" as const;141case WorkspaceSpec_WorkspaceType.REGULAR:142return "workspace" as const;143default:144return "unknown" as const;145}146};147148const displayTime = (timestamp?: Timestamp) => {149if (!timestamp) {150return "";151}152153return timestamp.toDate().toISOString();154};155156const renderDuration = (duration?: Duration): string => {157if (!duration) {158return "";159}160161let seconds = Number(duration.seconds);162seconds += duration.nanos / 1_000_000_000;163return seconds.toString(10);164};165166export const transformSessionRecord = (session: WorkspaceSession) => {167const initializerType = session.workspace?.spec?.initializer?.specs;168const prebuildInitializer = initializerType?.find((i) => i.spec.case === "prebuild")?.spec.value as169| PrebuildInitializer170| undefined;171172const row = {173id: session.id,174175creationTime: displayTime(session.creationTime),176deployedTime: displayTime(session.deployedTime),177startedTime: displayTime(session.startedTime),178stoppingTime: displayTime(session.stoppingTime),179stoppedTime: displayTime(session.stoppedTime),180181// draft: session.draft ? "true" : "false", // should we indicate here somehow that the ws is still running?182workspaceID: session?.workspace?.id,183configurationID: session.workspace?.metadata?.configurationId,184prebuildID: prebuildInitializer?.prebuildId,185userID: session.owner?.id,186userName: session.owner?.name,187188contextURL: session.workspace?.metadata?.originalContextUrl,189contextURL_cloneURL: session.context?.repository?.cloneUrl,190contextURLSegment_1: session?.context?.repository?.owner,191contextURLSegment_2: session?.context?.repository?.name,192193workspaceType: displayWorkspaceType(session.workspace?.spec?.type),194workspaceClass: session.workspace?.spec?.class,195196workspaceImageSize: session.metrics?.workspaceImageSize,197workspaceImageTotalSize: session.metrics?.totalImageSize,198199timeout: session.workspace?.spec?.timeout?.inactivity?.seconds,200editor: session.workspace?.spec?.editor?.name,201editorVersion: session.workspace?.spec?.editor?.version, // indicates whether user selected the stable or latest editor release channel202203// initializer metrics204contentInitGitDuration: renderDuration(session.metrics?.initializerMetrics?.git?.duration),205contentInitGitSize: session.metrics?.initializerMetrics?.git?.size,206contentInitFileDownloadDuration: renderDuration(session.metrics?.initializerMetrics?.fileDownload?.duration),207contentInitFileDownloadSize: session.metrics?.initializerMetrics?.fileDownload?.size,208contentInitSnapshotDuration: renderDuration(session.metrics?.initializerMetrics?.snapshot?.duration),209contentInitSnapshotSize: session.metrics?.initializerMetrics?.snapshot?.size,210contentInitBackupDuration: renderDuration(session.metrics?.initializerMetrics?.backup?.duration),211contentInitBackupSize: session.metrics?.initializerMetrics?.backup?.size,212contentInitPrebuildDuration: renderDuration(session.metrics?.initializerMetrics?.prebuild?.duration),213contentInitPrebuildSize: session.metrics?.initializerMetrics?.prebuild?.size,214contentInitCompositeDuration: renderDuration(session.metrics?.initializerMetrics?.composite?.duration),215contentInitCompositeSize: session.metrics?.initializerMetrics?.composite?.size,216};217218return row;219};220221export const useDownloadSessionsCSV = (args: Args) => {222const client = useQueryClient();223const key = getDownloadInsightsCSVQueryKey(args);224225const abort = useCallback(() => {226client.removeQueries([key]);227}, [client, key]);228229const query = useQuery<DownloadInsightsCSVResponse, Error>(230key,231async ({ signal }) => {232return downloadInsightsCSV({ ...args, signal });233},234{235retry: false,236cacheTime: 0,237staleTime: 0,238},239);240241return {242...query,243abort,244};245};246247const getDownloadInsightsCSVQueryKey = (args: Args) => {248return noPersistence(["insights-export", args]);249};250251252