import { useParams } from "react-router-dom"; import { graphql, useLazyLoadQuery, useSubscription } from "react-relay"; import { useEffect, useMemo, useState } from "react"; import type { GraphQLSubscriptionConfig } from "relay-runtime"; import type { SliceJetstreamQuery } from "../__generated__/SliceJetstreamQuery.graphql.ts"; import type { SliceJetstreamLogsQuery } from "../__generated__/SliceJetstreamLogsQuery.graphql.ts"; import type { SliceJetstreamSubscription } from "../__generated__/SliceJetstreamSubscription.graphql.ts"; import Layout from "../components/Layout.tsx"; import { Avatar } from "../components/Avatar.tsx"; import { SliceSubNav } from "../components/SliceSubNav.tsx"; import { ChevronRight } from "lucide-react"; import { useSessionContext } from "../lib/useSession.ts"; import { isSliceOwner } from "../lib/permissions.ts"; // Use the GraphQL-generated type for log entries type LogEntry = SliceJetstreamLogsQuery["response"]["jetstreamLogs"][number]; export default function SliceJetstream() { const { handle, rkey } = useParams<{ handle: string; rkey: string }>(); const { session } = useSessionContext(); const [realtimeLogs, setRealtimeLogs] = useState([]); const [expandedLogs, setExpandedLogs] = useState>(new Set()); // First query to get the slice URI const data = useLazyLoadQuery( graphql` query SliceJetstreamQuery($where: NetworkSlicesSliceWhereInput) { networkSlicesSlices(first: 1, where: $where) { edges { node { uri name domain actorHandle did networkSlicesActorProfile { avatar { url(preset: "avatar") } } } } } } `, { where: { actorHandle: { eq: handle }, uri: { contains: rkey }, }, }, ); const slice = data.networkSlicesSlices.edges[0]?.node; const isOwner = isSliceOwner(slice, session); // Second query for logs using the actual slice URI const logsData = useLazyLoadQuery( graphql` query SliceJetstreamLogsQuery($slice: String) { jetstreamLogs(slice: $slice, limit: 50) { id createdAt level message metadata sliceUri } } `, { slice: slice?.uri, }, { fetchPolicy: "store-and-network", }, ); // Setup subscription for real-time logs const subscriptionConfig = useMemo< GraphQLSubscriptionConfig >( () => ({ subscription: graphql` subscription SliceJetstreamSubscription($slice: String) { jetstreamLogsCreated(slice: $slice) { id createdAt level message metadata sliceUri } } `, variables: { slice: slice?.uri || "", }, onNext: (response) => { if (response?.jetstreamLogsCreated) { const newLog = response.jetstreamLogsCreated; setRealtimeLogs((prev) => [newLog, ...prev].slice(0, 100)); // Keep last 100 } }, }), [slice?.uri], ); useSubscription(subscriptionConfig); // Combine historical and real-time logs const allLogs = useMemo(() => { const historical = logsData.jetstreamLogs || []; // Merge and dedupe by id, real-time logs take precedence const logsMap = new Map(); [...historical, ...realtimeLogs].filter(Boolean).forEach((log) => { if (log?.id) { logsMap.set(log.id, log); } }); return Array.from(logsMap.values()).sort( (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), ); }, [logsData.jetstreamLogs, realtimeLogs]); const getLevelColor = (level: string) => { switch (level.toLowerCase()) { case "error": return "text-red-400 bg-red-950/30"; case "warn": return "text-yellow-400 bg-yellow-950/30"; case "info": return "text-blue-400 bg-blue-950/30"; default: return "text-zinc-400 bg-zinc-900"; } }; const formatTimestamp = (timestamp: string) => { const date = new Date(timestamp); return date.toLocaleString(); }; const toggleExpanded = (logId: string) => { setExpandedLogs((prev) => { const next = new Set(prev); if (next.has(logId)) { next.delete(logId); } else { next.add(logId); } return next; }); }; // Expand all logs with metadata by default useEffect(() => { const logsWithMetadata = allLogs .filter((log) => log.metadata) .map((log) => log.id); setExpandedLogs(new Set(logsWithMetadata)); }, [allLogs]); // Highlight NSIDs in log messages const highlightNsid = (message: string) => { // Match NSID pattern (e.g., fm.teal.alpha.feed.play or app.bsky.feed.post) const nsidPattern = /\b([a-z]+\.[a-z0-9]+(?:\.[a-z0-9]+)+)\b/gi; const parts = message.split(nsidPattern); return parts.map((part, index) => { // Check if this part matches an NSID pattern if (index % 2 === 1) { return ( {part} ); } return part; }); }; return ( } >

Jetstream Logs

Live

Real-time event streaming for {slice?.name}

{allLogs.length === 0 ? (
No logs yet. Logs will appear here in real-time.
) : ( allLogs.map((log) => { const isExpanded = expandedLogs.has(log.id); return (
log.metadata && toggleExpanded(log.id)} > {log.metadata ? ( ) :
} {log.level.toUpperCase()} {highlightNsid(log.message)} {formatTimestamp(log.createdAt)}
{log.metadata && isExpanded && (
                      {JSON.stringify(log.metadata, null, 2)}
                      
)}
); }) )}
); }