Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/dashboard/src/insights/download/download-sessions.ts
2501 views
1
/**
2
* Copyright (c) 2024 Gitpod GmbH. All rights reserved.
3
* Licensed under the GNU Affero General Public License (AGPL).
4
* See License.AGPL.txt in the project root for license information.
5
*/
6
7
import {
8
ListWorkspaceSessionsRequest,
9
PrebuildInitializer,
10
WorkspaceSession,
11
WorkspaceSpec_WorkspaceType,
12
} from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
13
import { workspaceClient } from "../../service/public-api";
14
import dayjs from "dayjs";
15
import { useQuery, useQueryClient } from "@tanstack/react-query";
16
import { useCallback } from "react";
17
import { noPersistence } from "../../data/setup";
18
import { Duration, Timestamp } from "@bufbuild/protobuf";
19
20
const pageSize = 100;
21
const maxPages = 100; // safety limit if something goes wrong with pagination
22
23
type GetAllWorkspaceSessionsArgs = Pick<ListWorkspaceSessionsRequest, "to" | "from" | "organizationId"> & {
24
signal?: AbortSignal;
25
onProgress?: (percentage: number) => void;
26
};
27
export const getAllWorkspaceSessions = async ({
28
from,
29
to,
30
signal,
31
organizationId,
32
onProgress,
33
}: GetAllWorkspaceSessionsArgs): Promise<WorkspaceSession[]> => {
34
const records: WorkspaceSession[] = [];
35
let page = 0;
36
while (!signal?.aborted && page < maxPages) {
37
const response = await workspaceClient.listWorkspaceSessions(
38
{
39
organizationId,
40
from,
41
to,
42
pagination: {
43
page,
44
pageSize,
45
},
46
},
47
{
48
signal,
49
},
50
);
51
if (response.workspaceSessions.length === 0) {
52
break;
53
}
54
55
records.push(...response.workspaceSessions);
56
onProgress && onProgress(page);
57
58
page = page + 1;
59
}
60
61
return records;
62
};
63
64
type Args = Pick<ListWorkspaceSessionsRequest, "organizationId" | "from" | "to"> & {
65
organizationName: string;
66
signal?: AbortSignal;
67
onProgress?: (percentage: number) => void;
68
};
69
70
export type DownloadInsightsCSVResponse = {
71
blob: Blob | null;
72
filename: string;
73
count: number;
74
};
75
76
const downloadInsightsCSV = async ({
77
organizationId,
78
from,
79
to,
80
organizationName,
81
signal,
82
onProgress,
83
}: Args): Promise<DownloadInsightsCSVResponse> => {
84
const start = dayjs(from?.toDate()).format("YYYYMMDD");
85
const end = dayjs(to?.toDate()).format("YYYYMMDD");
86
const filename = `gitpod-sessions-${organizationName}-${start}-${end}.csv`;
87
88
const records = await getAllWorkspaceSessions({
89
organizationId,
90
from,
91
to,
92
signal,
93
onProgress,
94
});
95
96
if (records.length === 0) {
97
return {
98
blob: null,
99
filename,
100
count: 0,
101
};
102
}
103
104
const rows = records.map(transformSessionRecord).filter((r) => !!r);
105
const fields = Object.keys(rows[0]) as (keyof ReturnType<typeof transformSessionRecord>)[];
106
107
// TODO: look into a lib to handle this more robustly
108
// CSV Rows
109
const csvRows = rows.map((row) => {
110
const rowString = fields
111
.map((fieldName) => {
112
const value = row[fieldName];
113
if (typeof value === "bigint") {
114
return value.toString();
115
}
116
117
return JSON.stringify(row[fieldName]);
118
})
119
.join(",");
120
121
return rowString;
122
});
123
124
// Prepend Header
125
csvRows.unshift(fields.join(","));
126
127
const blob = new Blob([`\ufeff${csvRows.join("\n")}`], {
128
type: "text/csv;charset=utf-8",
129
});
130
131
return {
132
blob,
133
filename,
134
count: rows.length,
135
};
136
};
137
138
export const displayWorkspaceType = (type?: WorkspaceSpec_WorkspaceType) => {
139
switch (type) {
140
case WorkspaceSpec_WorkspaceType.PREBUILD:
141
return "prebuild" as const;
142
case WorkspaceSpec_WorkspaceType.REGULAR:
143
return "workspace" as const;
144
default:
145
return "unknown" as const;
146
}
147
};
148
149
const displayTime = (timestamp?: Timestamp) => {
150
if (!timestamp) {
151
return "";
152
}
153
154
return timestamp.toDate().toISOString();
155
};
156
157
const renderDuration = (duration?: Duration): string => {
158
if (!duration) {
159
return "";
160
}
161
162
let seconds = Number(duration.seconds);
163
seconds += duration.nanos / 1_000_000_000;
164
return seconds.toString(10);
165
};
166
167
export const transformSessionRecord = (session: WorkspaceSession) => {
168
const initializerType = session.workspace?.spec?.initializer?.specs;
169
const prebuildInitializer = initializerType?.find((i) => i.spec.case === "prebuild")?.spec.value as
170
| PrebuildInitializer
171
| undefined;
172
173
const row = {
174
id: session.id,
175
176
creationTime: displayTime(session.creationTime),
177
deployedTime: displayTime(session.deployedTime),
178
startedTime: displayTime(session.startedTime),
179
stoppingTime: displayTime(session.stoppingTime),
180
stoppedTime: displayTime(session.stoppedTime),
181
182
// draft: session.draft ? "true" : "false", // should we indicate here somehow that the ws is still running?
183
workspaceID: session?.workspace?.id,
184
configurationID: session.workspace?.metadata?.configurationId,
185
prebuildID: prebuildInitializer?.prebuildId,
186
userID: session.owner?.id,
187
userName: session.owner?.name,
188
189
contextURL: session.workspace?.metadata?.originalContextUrl,
190
contextURL_cloneURL: session.context?.repository?.cloneUrl,
191
contextURLSegment_1: session?.context?.repository?.owner,
192
contextURLSegment_2: session?.context?.repository?.name,
193
194
workspaceType: displayWorkspaceType(session.workspace?.spec?.type),
195
workspaceClass: session.workspace?.spec?.class,
196
197
workspaceImageSize: session.metrics?.workspaceImageSize,
198
workspaceImageTotalSize: session.metrics?.totalImageSize,
199
200
timeout: session.workspace?.spec?.timeout?.inactivity?.seconds,
201
editor: session.workspace?.spec?.editor?.name,
202
editorVersion: session.workspace?.spec?.editor?.version, // indicates whether user selected the stable or latest editor release channel
203
204
// initializer metrics
205
contentInitGitDuration: renderDuration(session.metrics?.initializerMetrics?.git?.duration),
206
contentInitGitSize: session.metrics?.initializerMetrics?.git?.size,
207
contentInitFileDownloadDuration: renderDuration(session.metrics?.initializerMetrics?.fileDownload?.duration),
208
contentInitFileDownloadSize: session.metrics?.initializerMetrics?.fileDownload?.size,
209
contentInitSnapshotDuration: renderDuration(session.metrics?.initializerMetrics?.snapshot?.duration),
210
contentInitSnapshotSize: session.metrics?.initializerMetrics?.snapshot?.size,
211
contentInitBackupDuration: renderDuration(session.metrics?.initializerMetrics?.backup?.duration),
212
contentInitBackupSize: session.metrics?.initializerMetrics?.backup?.size,
213
contentInitPrebuildDuration: renderDuration(session.metrics?.initializerMetrics?.prebuild?.duration),
214
contentInitPrebuildSize: session.metrics?.initializerMetrics?.prebuild?.size,
215
contentInitCompositeDuration: renderDuration(session.metrics?.initializerMetrics?.composite?.duration),
216
contentInitCompositeSize: session.metrics?.initializerMetrics?.composite?.size,
217
};
218
219
return row;
220
};
221
222
export const useDownloadSessionsCSV = (args: Args) => {
223
const client = useQueryClient();
224
const key = getDownloadInsightsCSVQueryKey(args);
225
226
const abort = useCallback(() => {
227
client.removeQueries([key]);
228
}, [client, key]);
229
230
const query = useQuery<DownloadInsightsCSVResponse, Error>(
231
key,
232
async ({ signal }) => {
233
return downloadInsightsCSV({ ...args, signal });
234
},
235
{
236
retry: false,
237
cacheTime: 0,
238
staleTime: 0,
239
},
240
);
241
242
return {
243
...query,
244
abort,
245
};
246
};
247
248
const getDownloadInsightsCSVQueryKey = (args: Args) => {
249
return noPersistence(["insights-export", args]);
250
};
251
252