-
Notifications
You must be signed in to change notification settings - Fork 117
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: allow fetching workpsace usage CSV by API if feature flag (#3829)
* allow downloading workspace usage CSV via API * rename as unsafe * add feature flag * add public api endpoint * fix --------- Co-authored-by: Henry Fontanier <[email protected]>
- Loading branch information
1 parent
2c80c72
commit 8e22320
Showing
4 changed files
with
195 additions
and
96 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import { format } from "date-fns"; | ||
import { QueryTypes } from "sequelize"; | ||
|
||
import { front_sequelize } from "@app/lib/databases"; | ||
|
||
export interface WorkpsaceUsageQueryResult { | ||
createdAt: string; | ||
conversationModelId: string; | ||
messageId: string; | ||
userMessageId: string; | ||
agentMessageId: string; | ||
userId: string; | ||
userFirstName: string; | ||
userLastName: string; | ||
assistantId: string; | ||
assistantName: string; | ||
actionType: string; | ||
source: string; | ||
} | ||
|
||
export async function unsafeGetUsageData( | ||
startDate: Date, | ||
endDate: Date, | ||
wId: string | ||
): Promise<string> { | ||
const results = await front_sequelize.query<WorkpsaceUsageQueryResult>( | ||
` | ||
SELECT | ||
TO_CHAR(m."createdAt"::timestamp, 'YYYY-MM-DD HH24:MI:SS') AS "createdAt", | ||
c."id" AS "conversationInternalId", | ||
m."sId" AS "messageId", | ||
p."sId" AS "parentMessageId", | ||
CASE | ||
WHEN um."id" IS NOT NULL THEN 'user' | ||
WHEN am."id" IS NOT NULL THEN 'assistant' | ||
WHEN cf."id" IS NOT NULL THEN 'content_fragment' | ||
END AS "messageType", | ||
um."userContextFullName" AS "userFullName", | ||
um."userContextEmail" AS "userEmail", | ||
COALESCE(ac."sId", am."agentConfigurationId") AS "assistantId", | ||
COALESCE(ac."name", am."agentConfigurationId") AS "assistantName", | ||
CASE | ||
WHEN ac."retrievalConfigurationId" IS NOT NULL THEN 'retrieval' | ||
WHEN ac."dustAppRunConfigurationId" IS NOT NULL THEN 'dustAppRun' | ||
ELSE NULL | ||
END AS "actionType", | ||
CASE | ||
WHEN um."id" IS NOT NULL THEN | ||
CASE | ||
WHEN um."userId" IS NOT NULL THEN 'web' | ||
ELSE 'slack' | ||
END | ||
END AS "source" | ||
FROM | ||
"messages" m | ||
JOIN | ||
"conversations" c ON m."conversationId" = c."id" | ||
JOIN | ||
"workspaces" w ON c."workspaceId" = w."id" | ||
LEFT JOIN | ||
"user_messages" um ON m."userMessageId" = um."id" | ||
LEFT JOIN | ||
"users" u ON um."userId" = u."id" | ||
LEFT JOIN | ||
"agent_messages" am ON m."agentMessageId" = am."id" | ||
LEFT JOIN | ||
"content_fragments" cf ON m."contentFragmentId" = cf."id" | ||
LEFT JOIN | ||
"agent_configurations" ac ON am."agentConfigurationId" = ac."sId" AND am."agentConfigurationVersion" = ac."version" | ||
LEFT JOIN | ||
"messages" p ON m."parentId" = p."id" | ||
WHERE | ||
w."sId" = :wId AND | ||
m."createdAt" >= :startDate AND m."createdAt" <= :endDate | ||
ORDER BY | ||
m."createdAt" DESC | ||
`, | ||
{ | ||
replacements: { | ||
wId, | ||
startDate: format(startDate, "yyyy-MM-dd"), // Use first day of start month | ||
endDate: format(endDate, "yyyy-MM-dd"), // Use last day of end month | ||
}, | ||
type: QueryTypes.SELECT, | ||
} | ||
); | ||
if (!results.length) { | ||
return "No data available for the selected period."; | ||
} | ||
const csvHeader = Object.keys(results[0]).join(",") + "\n"; | ||
const csvContent = results | ||
.map((row) => Object.values(row).join(",")) | ||
.join("\n"); | ||
|
||
return csvHeader + csvContent; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import { isLeft } from "fp-ts/lib/Either"; | ||
import * as t from "io-ts"; | ||
import * as reporter from "io-ts-reporters"; | ||
import type { NextApiRequest, NextApiResponse } from "next"; | ||
|
||
import { Authenticator, getAPIKey } from "@app/lib/auth"; | ||
import { unsafeGetUsageData } from "@app/lib/workspace_usage"; | ||
import { apiError, withLogging } from "@app/logger/withlogging"; | ||
|
||
const DateString = t.refinement( | ||
t.string, | ||
(s): s is string => /^\d{4}-\d{2}-\d{2}$/.test(s), | ||
"YYYY-MM-DD" | ||
); | ||
|
||
const GetWorkspaceUsageSchema = t.intersection([ | ||
t.type({ | ||
start_date: DateString, | ||
}), | ||
t.partial({ | ||
end_date: t.union([DateString, t.undefined, t.null]), | ||
}), | ||
]); | ||
|
||
async function handler( | ||
req: NextApiRequest, | ||
res: NextApiResponse | ||
): Promise<void> { | ||
const keyRes = await getAPIKey(req); | ||
if (keyRes.isErr()) { | ||
return apiError(req, res, keyRes.error); | ||
} | ||
const { auth } = await Authenticator.fromKey( | ||
keyRes.value, | ||
req.query.wId as string | ||
); | ||
|
||
const owner = auth.workspace(); | ||
if (!owner || !auth.isBuilder()) { | ||
return apiError(req, res, { | ||
status_code: 404, | ||
api_error: { | ||
type: "workspace_not_found", | ||
message: "The workspace was not found.", | ||
}, | ||
}); | ||
} | ||
|
||
if (!owner.flags.includes("usage_data_api")) { | ||
return apiError(req, res, { | ||
status_code: 403, | ||
api_error: { | ||
type: "workspace_auth_error", | ||
message: "The workspace does not have access to the usage data API.", | ||
}, | ||
}); | ||
} | ||
|
||
switch (req.method) { | ||
case "GET": | ||
const queryValidation = GetWorkspaceUsageSchema.decode(req.query); | ||
if (isLeft(queryValidation)) { | ||
const pathError = reporter.formatValidationErrors(queryValidation.left); | ||
return apiError(req, res, { | ||
api_error: { | ||
type: "invalid_request_error", | ||
message: `Invalid request query: ${pathError}`, | ||
}, | ||
status_code: 400, | ||
}); | ||
} | ||
|
||
const query = queryValidation.right; | ||
|
||
const csvData = await unsafeGetUsageData( | ||
new Date(query.start_date), | ||
query.end_date ? new Date(query.end_date) : new Date(), | ||
owner.sId | ||
); | ||
res.setHeader("Content-Type", "text/csv"); | ||
res.status(200).send(csvData); | ||
return; | ||
|
||
default: | ||
return apiError(req, res, { | ||
status_code: 405, | ||
api_error: { | ||
type: "method_not_supported_error", | ||
message: "The method passed is not supported, GET is expected.", | ||
}, | ||
}); | ||
} | ||
} | ||
|
||
export default withLogging(handler); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters