Skip to main content
Glama

Convex MCP server

Official
by get-convex
EventsForInsight.tsx19.3 kB
import { DeploymentInfoContext } from "@common/lib/deploymentContext"; import { Disclosure } from "@headlessui/react"; import { CaretDownIcon, ChevronDownIcon, ChevronUpIcon, ExternalLinkIcon, InfoCircledIcon, QuestionMarkCircledIcon, } from "@radix-ui/react-icons"; import { Insight } from "api/insights"; import { Button } from "@ui/Button"; import { FunctionNameOption } from "@common/elements/FunctionNameOption"; import { Loading } from "@ui/Loading"; import { Tooltip } from "@ui/Tooltip"; import { formatBytes, formatNumberCompact } from "@common/lib/format"; import { functionIdentifierValue } from "@common/lib/functions/generateFileTree"; import { ComponentId, useNents } from "@common/lib/useNents"; import { documentHref } from "@common/lib/utils"; import { cn } from "@ui/cn"; import Link from "next/link"; import { useContext } from "react"; // Type definitions to match what's in api/insights.ts type HourlyCount = { hour: string; count: number; }; type OccRecentEvent = { timestamp: string; id: string; request_id: string; occ_document_id: string; occ_write_source: string; occ_retry_count: number; }; type BytesReadRecentEvent = { timestamp: string; id: string; request_id: string; calls: { table_name: string; bytes_read: number; documents_read: number }[]; success: boolean; }; // Type definitions for the event data type FormattedBytesReadEvent = { timestamp: string; id: string; requestId: string; executionId: string; totalCount: number; events: { tableName: string; count: number }[]; status: string; }; type FormattedOccEvent = { timestamp: string; id: string; requestId: string; executionId: string; occDocumentId: string; occWriteSource: string; occRetryCount: number; }; // Note: Hourly data padding and sorting is handled in the useInsights hook in api/insights.ts // Type guards function isOccInsight(insight: Insight): insight is Insight & { kind: "occRetried" | "occFailedPermanently"; details: { occCalls: number; occTableName: string; hourlyCounts: HourlyCount[]; recentEvents: OccRecentEvent[]; }; } { return ( insight.kind === "occRetried" || insight.kind === "occFailedPermanently" ); } function isMetricsInsight(insight: Insight): insight is Insight & { kind: | "bytesReadLimit" | "bytesReadThreshold" | "documentsReadLimit" | "documentsReadThreshold"; details: { count: number; hourlyCounts: HourlyCount[]; recentEvents: BytesReadRecentEvent[]; }; } { return [ "bytesReadLimit", "bytesReadThreshold", "documentsReadLimit", "documentsReadThreshold", ].includes(insight.kind); } export function EventsForInsight({ insight }: { insight: Insight }) { return ( <div className="flex flex-col gap-2 overflow-y-hidden"> <div className="flex items-end justify-between"> <div className="flex items-center gap-1"> <h5>Recent Events</h5> <Tooltip tip="Recent events matching the criteria of this Insight." side="right" > <QuestionMarkCircledIcon /> </Tooltip> </div> <span className="text-xs text-content-secondary"> Data may be behind by a couple hours. </span> </div> {(() => { switch (insight.kind) { case "occFailedPermanently": return ( <OCCFailedEvents insight={insight as Insight & { kind: "occFailedPermanently" }} /> ); case "occRetried": return ( <OCCRetriedEvents insight={insight as Insight & { kind: "occRetried" }} /> ); case "bytesReadLimit": case "bytesReadThreshold": return ( <BytesReadEvents insight={ insight as Insight & { kind: "bytesReadLimit" | "bytesReadThreshold"; } } /> ); case "documentsReadLimit": case "documentsReadThreshold": return ( <DocumentsReadEvents insight={ insight as Insight & { kind: "documentsReadLimit" | "documentsReadThreshold"; } } /> ); default: { insight satisfies never; return null; } } })()} </div> ); } const eventTimestampColumn = { header: ( <div className="flex items-center gap-1"> Timestamp{" "} <Tooltip tip="Events are sorted in reverse cronological order."> <CaretDownIcon /> </Tooltip> </div> ), className: "w-40", key: "timestamp", Column: EventTimestamp, }; const eventFunctionCallColumn = { header: "Function Call ID", className: "w-32", key: "executionId", tooltip: "The unique ID of the function call that encountered this error.", Column: EventExecutionId, }; const eventRequestIdColumn = { header: "Request ID", className: "w-36", key: "requestId", tooltip: "The ID of the request that triggered this function call. Multiple function calls may have the same Request ID if they are part of the same request, or if a function was retried.", Column: EventRequestId, }; const eventOccWriteSourceColumn = { header: "Conflicting Function", className: "w-60", key: "occWriteSource", tooltip: "The function that caused the write conflict.", Column: EventOccWriteSource, }; const eventOccDocumentIdColumn = { header: "Conflicting Document ID", className: "w-[16rem]", key: "occDocumentId", tooltip: "The ID of the document that caused the write conflict.", Column: EventOccDocumentId, }; const eventStatusColumn = { header: "Status", className: "w-16", key: "status", tooltip: "Whether the function call ultimately succeeded or failed.", Column: EventStatus, }; function BytesReadEvents({ insight, }: { insight: Insight & { kind: "bytesReadLimit" | "bytesReadThreshold"; }; }) { const events = insight.details.recentEvents.map((event) => { // Map BytesReadRecentEvent to FormattedBytesReadEvent const totalCount = event.calls.reduce( (sum, call) => sum + call.bytes_read, 0, ); return { timestamp: event.timestamp, id: event.id, requestId: event.request_id, executionId: event.id, // Using id as executionId totalCount, events: event.calls.map((call) => ({ tableName: call.table_name, count: call.bytes_read, })), status: event.success ? "success" : "failure", } as FormattedBytesReadEvent; }); // Hourly counts are now pre-processed in useInsights return ( <EventsTable events={events} insight={insight} columns={[ eventRequestIdColumn, eventFunctionCallColumn, eventTimestampColumn, eventStatusColumn, { header: "Bytes Read", className: "w-60", key: "bytesRead", tooltip: "The number of bytes read by the function during this call.", Column: BytesEventReadAmount, }, ]} /> ); } function DocumentsReadEvents({ insight, }: { insight: Insight & { kind: "documentsReadLimit" | "documentsReadThreshold"; }; }) { const events = insight.details.recentEvents.map((event) => { // Map BytesReadRecentEvent to FormattedBytesReadEvent but with documents_read const totalCount = event.calls.reduce( (sum, call) => sum + call.documents_read, 0, ); return { timestamp: event.timestamp, id: event.id, requestId: event.request_id, executionId: event.id, // Using id as executionId totalCount, events: event.calls.map((call) => ({ tableName: call.table_name, count: call.documents_read, })), status: event.success ? "success" : "failure", } as FormattedBytesReadEvent; }); // Hourly counts are now pre-processed in useInsights return ( <EventsTable events={events} insight={insight} columns={[ eventRequestIdColumn, eventFunctionCallColumn, eventTimestampColumn, eventStatusColumn, { header: "Documents Read", className: "w-60", key: "bytesRead", tooltip: "The number of documents read by the function during this call.", Column: DocumentsEventReadAmount, }, ]} /> ); } function OCCFailedEvents({ insight, }: { insight: Insight & { kind: "occFailedPermanently"; }; }) { const events = insight.details.recentEvents.map( (event) => ({ timestamp: event.timestamp, id: event.id, requestId: event.request_id, executionId: event.id, // Using id as executionId occDocumentId: event.occ_document_id, occWriteSource: event.occ_write_source, occRetryCount: event.occ_retry_count, }) as FormattedOccEvent, ); // Hourly counts are now pre-processed in useInsights return ( <EventsTable events={events} insight={insight} columns={[ eventRequestIdColumn, eventFunctionCallColumn, eventTimestampColumn, eventOccDocumentIdColumn, eventOccWriteSourceColumn, ]} /> ); } function OCCRetriedEvents({ insight, }: { insight: Insight & { kind: "occRetried"; }; }) { const events = insight.details.recentEvents.map( (event) => ({ timestamp: event.timestamp, id: event.id, requestId: event.request_id, executionId: event.id, // Using id as executionId occDocumentId: event.occ_document_id, occWriteSource: event.occ_write_source, occRetryCount: event.occ_retry_count, }) as FormattedOccEvent, ); // Hourly counts are now pre-processed in useInsights return ( <EventsTable events={events} insight={insight} columns={[ eventRequestIdColumn, eventFunctionCallColumn, eventTimestampColumn, eventOccDocumentIdColumn, eventOccWriteSourceColumn, { header: "Retry #", className: "flex w-16 items-center gap-1", key: "occRetryCount", tooltip: "The number of previous attempts before this one to call the function.", Column: EventOccRetryCount, }, ]} /> ); } /** * EventsTable is a generic table for displaying events. * It takes in a list of events and a list of columns to display. * Each column has a header, a className, a key, a tooltip, and a Column component. * The Column component is a React component that takes in an event and returns a JSX element. * The EventsTable component maps over the events and columns to render the table. * The table is scrollable and has a sticky header. */ function EventsTable<T, I extends Insight>({ events, insight, columns, }: { events?: T[]; insight: I; columns: { header: React.ReactNode; className: string; key: string; tooltip?: string; Column: React.FC<{ event: T; insight: I; componentId: ComponentId | undefined; }>; }[]; }) { const { nents } = useNents(); const nentId = nents?.find((nent) => nent.path === insight.componentPath)?.id; return ( <div className="scrollbar flex max-h-full w-full flex-col overflow-y-auto rounded-sm border"> <div className="sticky top-0 z-20 flex min-w-fit gap-2 border-b bg-background-secondary px-2 pt-2 pb-1 text-xs text-content-secondary"> {columns.map((col) => ( <div key={col.key} className={cn("flex items-center gap-1", col.className)} > {col.header} {col.tooltip && ( <Tooltip tip={col.tooltip}> <QuestionMarkCircledIcon /> </Tooltip> )} </div> ))} </div> <div className="flex max-h-full grow flex-col"> {events && events.map((event, idx) => ( <div key={idx} className="flex min-w-fit animate-fadeInFromLoading gap-2 border-b p-2 text-xs last:border-b-0" > {columns.map((col) => ( <col.Column key={col.key} event={event} insight={insight} componentId={nentId} /> ))} </div> ))} {!events && Array.from( { length: Math.min( 7, isOccInsight(insight) ? insight.details.occCalls : isMetricsInsight(insight) ? insight.details.count : 7, ), }, (_, i) => i, ).map((i) => ( <div key={i} className="flex w-full gap-2 border-b p-2 last:border-b-0" > {columns.map((col) => ( <Loading className="h-4" key={col.key}> <div className={col.className} /> </Loading> ))} </div> ))} </div> </div> ); } function EventTimestamp({ event }: { event: { timestamp: string } }) { return ( <div className="min-w-40"> {(() => { try { // Handle ISO strings with or without the Z/timezone information const timestamp = event.timestamp.endsWith("Z") ? event.timestamp : `${event.timestamp}Z`; return new Date(timestamp).toLocaleString(); } catch (e) { // Fallback to more forgiving date parsing return new Date(event.timestamp).toLocaleString(); } })()} </div> ); } function EventExecutionId({ event }: { event: { executionId: string } }) { return <div className="w-32 truncate">{event.executionId}</div>; } function EventRequestId({ event, insight: _insight, componentId: _componentId, }: { event: { requestId: string }; insight: Insight; componentId: ComponentId | undefined; }) { return ( <div className="w-36"> {event.requestId || ( <span className="text-content-secondary">Unknown</span> )} </div> ); } function EventOccDocumentId({ insight, event, componentId, }: { insight: Insight & { kind: "occFailedPermanently" | "occRetried"; }; event: FormattedOccEvent; componentId: ComponentId | undefined; }) { const { deploymentsURI, captureMessage } = useContext(DeploymentInfoContext); return ( <div className="flex w-[16rem]"> {event.occDocumentId && insight.details.occTableName ? ( <Link href={documentHref({ deploymentsURI, tableName: insight.details.occTableName, id: event.occDocumentId, componentId: componentId ?? null, captureMessage, })} target="_blank" className="flex items-center gap-1 text-content-link hover:underline" > {event.occDocumentId} <ExternalLinkIcon className="size-3 shrink-0" /> </Link> ) : ( <span className="text-content-secondary">Unknown</span> )} </div> ); } function EventOccWriteSource({ insight, event, componentId: _componentId, }: { insight: Insight & { kind: "occFailedPermanently" | "occRetried"; }; event: FormattedOccEvent; componentId: ComponentId | undefined; }) { return ( <div className="w-60 truncate"> {!event.occWriteSource && ( <span className="text-content-secondary">Unknown</span> )} {event.occWriteSource && (insight.functionId === event.occWriteSource ? ( <span className="flex items-center gap-1 text-content-secondary"> Self{" "} <Tooltip tip="Two calls to the same function resulted in this write conflict."> <InfoCircledIcon /> </Tooltip> </span> ) : ( <FunctionNameOption label={functionIdentifierValue(event.occWriteSource)} oneLine /> ))} </div> ); } function EventOccRetryCount({ event, insight: _insight, componentId: _componentId, }: { event: FormattedOccEvent; insight: Insight & { kind: "occRetried" }; componentId: ComponentId | undefined; }) { return <div className="w-16">{event.occRetryCount}</div>; } function BytesEventReadAmount({ event, insight: _insight, componentId: _componentId, }: { event: FormattedBytesReadEvent; insight: Insight & { kind: "bytesReadLimit" | "bytesReadThreshold" }; componentId: ComponentId | undefined; }) { return <EventReadAmount event={event} format={formatBytes} />; } function DocumentsEventReadAmount({ event, insight: _insight, componentId: _componentId, }: { event: FormattedBytesReadEvent; insight: Insight & { kind: "documentsReadLimit" | "documentsReadThreshold"; }; componentId: ComponentId | undefined; }) { return <EventReadAmount event={event} format={formatNumberCompact} />; } function EventReadAmount({ event, format, }: { event: FormattedBytesReadEvent; format: (count: number) => string; }) { return ( <div className="w-60"> <Disclosure> {({ open }) => ( <> <div className="flex items-center gap-1"> <span className="min-w-[4.25rem]"> {format(event.totalCount)} </span> <Disclosure.Button as={Button} inline variant="neutral" size="xs" tipSide="right" tip="View breakdown" className="-my-1" > {open ? <ChevronUpIcon /> : <ChevronDownIcon />} </Disclosure.Button> </div> <Disclosure.Panel> <ul className="mt-2 flex animate-fadeInFromLoading flex-col gap-1"> {event.events.map((e, idx) => ( <li key={idx} className="list-inside list-decimal text-xs text-content-secondary" > <span className="font-semibold">{format(e.count)}</span>{" "} from <span className="font-semibold">{e.tableName}</span> </li> ))} </ul> </Disclosure.Panel> </> )} </Disclosure> </div> ); } function EventStatus({ event, insight: _insight, componentId: _componentId, }: { event: FormattedBytesReadEvent; insight: Insight & { kind: | "bytesReadLimit" | "bytesReadThreshold" | "documentsReadLimit" | "documentsReadThreshold"; }; componentId: ComponentId | undefined; }) { return ( <div className="w-16"> {event.status === "success" ? ( "Success" ) : event.status === "failure" ? ( "Failure" ) : ( <span className="text-content-secondary">Unknown</span> )} </div> ); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/get-convex/convex-backend'

If you have feedback or need assistance with the MCP directory API, please join our Discord server