Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 364
feat(web): record service ping history and add usage report download#1348
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
1983edbec50d015c5271e222b07ae8bbbaaFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| -- CreateTable | ||
| CREATE TABLE "ServicePingEvent" ( | ||
| "id" TEXT NOT NULL, | ||
| "payload" JSONB NOT NULL, | ||
| "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| "orgId" INTEGER NOT NULL, | ||
| CONSTRAINT "ServicePingEvent_pkey" PRIMARY KEY ("id") | ||
| ); | ||
| -- AddForeignKey | ||
| ALTER TABLE "ServicePingEvent" ADD CONSTRAINT "ServicePingEvent_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "Org"("id") ON DELETE CASCADE ON UPDATE CASCADE; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -318,6 +318,7 @@ model Org { | ||
| mcpServers McpServer[] | ||
| license License? | ||
| servicePingEvents ServicePingEvent[] | ||
| } | ||
| model License { | ||
| @@ -358,6 +359,15 @@ model License { | ||
| updatedAt DateTime @updatedAt | ||
| } | ||
| model ServicePingEvent { | ||
| id String @id @default(cuid()) | ||
| payload Json | ||
| createdAt DateTime @default(now()) | ||
| org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) | ||
| orgId Int | ||
| } | ||
brendan-kellam marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| enum OrgRole { | ||
| OWNER | ||
| MEMBER | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| 'use server'; | ||
| import { sew } from "@/middleware/sew"; | ||
| import { withAuth } from "@/middleware/withAuth"; | ||
| import { withMinimumOrgRole } from "@/middleware/withMinimumOrgRole"; | ||
| import { OrgRole } from "@sourcebot/db"; | ||
| import { ServiceError } from "@/lib/serviceError"; | ||
| export interface ServicePingHistoryEntry { | ||
| createdAt: string; | ||
| payload: unknown; | ||
| } | ||
| // Returns the recorded Service Ping history so offline deployments can export | ||
| // it and send it back to us out-of-band (they can't reach Lighthouse directly). | ||
| export const getServicePingHistory = async (): Promise<ServicePingHistoryEntry[] | ServiceError> => sew(() => | ||
| withAuth(async ({ org, role, prisma }) => | ||
| withMinimumOrgRole(role, OrgRole.OWNER, async () => { | ||
| const events = await prisma.servicePingEvent.findMany({ | ||
| where: { orgId: org.id }, | ||
| orderBy: { createdAt: 'asc' }, | ||
| }); | ||
brendan-kellam marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| return events.map((event) => ({ | ||
| createdAt: event.createdAt.toISOString(), | ||
| payload: event.payload, | ||
| })); | ||
| }) | ||
| ) | ||
| ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| 'use client'; | ||
| import { useCallback, useState } from "react"; | ||
| import { Download } from "lucide-react"; | ||
| import { LoadingButton } from "@/components/ui/loading-button"; | ||
| import { useToast } from "@/components/hooks/use-toast"; | ||
| import { isServiceError } from "@/lib/utils"; | ||
| import { getServicePingHistory } from "./actions"; | ||
| export function DownloadServicePingHistoryButton() { | ||
| const [isLoading, setIsLoading] = useState(false); | ||
| const { toast } = useToast(); | ||
| const handleDownload = useCallback(async () => { | ||
| setIsLoading(true); | ||
| try { | ||
| const result = await getServicePingHistory(); | ||
| if (isServiceError(result)) { | ||
| toast({ | ||
| description: "Failed to export service ping history. Please try again.", | ||
| variant: "destructive", | ||
| }); | ||
| return; | ||
| } | ||
| if (result.length === 0) { | ||
| toast({ | ||
| description: "No service ping history has been recorded yet.", | ||
| }); | ||
| return; | ||
| } | ||
| const blob = new Blob([JSON.stringify(result, null, 2)], { | ||
| type: "application/json", | ||
| }); | ||
| const url = URL.createObjectURL(blob); | ||
| const anchor = document.createElement("a"); | ||
| anchor.href = url; | ||
| anchor.download = `${new Date().toISOString().slice(0, 10)}-usage-history.json`; | ||
| document.body.appendChild(anchor); | ||
| anchor.click(); | ||
| document.body.removeChild(anchor); | ||
| URL.revokeObjectURL(url); | ||
| } finally { | ||
| setIsLoading(false); | ||
| } | ||
| }, [toast]); | ||
| return ( | ||
| <LoadingButton | ||
| variant="outline" | ||
| size="sm" | ||
| onClick={handleDownload} | ||
| loading={isLoading} | ||
| > | ||
| {!isLoading && <Download className="h-3.5 w-3.5" />} | ||
| Download usage report | ||
| </LoadingButton> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -2,7 +2,13 @@ import { existsSync } from "fs"; | ||
| import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; | ||
| import { isServiceError } from "@/lib/utils"; | ||
| import { __unsafePrisma } from "@/prisma"; | ||
| import { createLogger, decryptActivationCode, env, SOURCEBOT_VERSION } from "@sourcebot/shared"; | ||
| import { | ||
| createLogger, | ||
| decryptActivationCode, | ||
| env, | ||
| SOURCEBOT_VERSION, | ||
| isValidOfflineLicenseActive | ||
| } from "@sourcebot/shared"; | ||
| import { client } from "./client"; | ||
| import { ServicePingRequest } from "./types"; | ||
| import { ServiceErrorException } from "@/lib/serviceError"; | ||
| @@ -83,6 +89,13 @@ export const syncWithLighthouse = async (orgId: number) => { | ||
| ...(activationCode && { activationCode }), | ||
| }; | ||
| await recordServicePingInDB(orgId, payload); | ||
| if (isValidOfflineLicenseActive()) { | ||
| logger.debug('Skipping service ping: active offline license detected.'); | ||
| return; | ||
| } | ||
| const response = await client.ping(payload); | ||
brendan-kellam marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| if (isServiceError(response)) { | ||
| logger.error(`Service ping failed:\n ${JSON.stringify(response, null, 2)}`) | ||
| @@ -172,3 +185,21 @@ const inferDeploymentType = (): string => { | ||
| } | ||
| return 'other'; | ||
| }; | ||
| const recordServicePingInDB = async (orgId: number, payload: ServicePingRequest) => { | ||
| // Strip the activation code before persisting. | ||
| const { activationCode: _activationCode, ...sanitizedPayload } = payload; | ||
| try { | ||
| await __unsafePrisma.servicePingEvent.create({ | ||
| data: { | ||
| orgId, | ||
| payload: sanitizedPayload, | ||
| }, | ||
| }); | ||
| } catch (error) { | ||
| // Recording the ping is best-effort: a failure here must not prevent | ||
| // the actual ping from being sent to Lighthouse. | ||
| logger.error(`Failed to record service ping in database:\n ${error}`); | ||
| } | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.