Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function getNumber(span: SigNozListItem, key: string, fallback = 0): number {
return Number.isFinite(n) ? n : fallback;
}

async function signozQuery(payload: any): Promise<SigNozResp | null> {
async function signozQuery(payload: any): Promise<SigNozResp> {
const logger = getLogger('signoz-query');
try {
logger.info({ payload }, 'SigNoz payload');
Expand All @@ -63,11 +63,31 @@ async function signozQuery(payload: any): Promise<SigNozResp | null> {
return json;
} catch (e) {
logger.error({ error: e }, 'SigNoz query error');
return null;

// Re-throw the error with more context for proper error handling
if (axios.isAxiosError(e)) {
if (e.code === 'ECONNREFUSED' || e.code === 'ENOTFOUND' || e.code === 'ETIMEDOUT') {
throw new Error(`SigNoz service unavailable: ${e.message}`);
}
if (e.response?.status === 401 || e.response?.status === 403) {
throw new Error(`SigNoz authentication failed: ${e.response.statusText}`);
}
if (e.response?.status === 400) {
throw new Error(`Invalid SigNoz query: ${e.response.statusText}`);
}
if (e.response?.status === 429) {
throw new Error(`SigNoz rate limit exceeded: ${e.response.statusText}`);
}
if (e.response?.status && e.response.status >= 500) {
throw new Error(`SigNoz server error: ${e.response.statusText}`);
}
throw new Error(`SigNoz request failed: ${e.message}`);
}
throw new Error(`SigNoz query failed: ${e instanceof Error ? e.message : 'Unknown error'}`);
}
}

function parseList(resp: SigNozResp | null, name: string): SigNozListItem[] {
function parseList(resp: SigNozResp, name: string): SigNozListItem[] {
const list = resp?.data?.result?.find((r) => r?.queryName === name)?.list ?? [];
return Array.isArray(list) ? list : [];
}
Expand Down Expand Up @@ -858,6 +878,25 @@ export async function GET(
} catch (error) {
const logger = getLogger('conversation-details');
logger.error({ error }, 'Error fetching conversation details');
return NextResponse.json({ error: 'Failed to fetch conversation details' }, { status: 500 });

// Provide more specific error responses based on the error type
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch conversation details';

if (errorMessage.includes('SigNoz service unavailable')) {
return NextResponse.json({ error: errorMessage }, { status: 503 });
}
if (errorMessage.includes('SigNoz authentication failed')) {
return NextResponse.json({ error: errorMessage }, { status: 502 });
}
if (errorMessage.includes('Invalid SigNoz query')) {
return NextResponse.json({ error: errorMessage }, { status: 400 });
}
if (errorMessage.includes('SigNoz rate limit exceeded')) {
return NextResponse.json({ error: errorMessage }, { status: 429 });
}
if (errorMessage.includes('SigNoz server error')) {
return NextResponse.json({ error: errorMessage }, { status: 502 });
}
return NextResponse.json({ error: errorMessage }, { status: 500 });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ export const Playground = ({
const {
chatActivities,
isPolling,
error: _error,
error,
startPolling,
stopPolling,
retryConnection,
} = useChatActivitiesPolling({
conversationId,
});
Expand Down Expand Up @@ -58,6 +59,8 @@ export const Playground = ({
isPolling={isPolling}
conversation={chatActivities}
enableAutoScroll={true}
error={error}
retryConnection={retryConnection}
/>
</ResizablePanelGroup>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import { TOOL_TYPES } from '@/components/traces/timeline/types';
import { renderPanelContent } from '@/components/traces/timeline/render-panel-content';
import { StickToBottom } from 'use-stick-to-bottom';
import { ResizableHandle, ResizablePanel } from '@/components/ui/resizable';
import { Loader2, ChevronDown, ChevronUp } from 'lucide-react';
import { Loader2, ChevronDown, ChevronUp, RefreshCw, AlertTriangle } from 'lucide-react';
import { ConversationTracesLink } from '@/components/traces/signoz-link';
import { Button } from '@/components/ui/button';
import { Alert, AlertTitle } from '@/components/ui/alert';

function panelTitle(selected: SelectedPanel) {
switch (selected.type) {
Expand Down Expand Up @@ -49,9 +50,32 @@ interface TimelineWrapperProps {
conversation?: ConversationDetail | null;
enableAutoScroll?: boolean;
isPolling?: boolean;
error?: string | null;
retryConnection?: () => void;
}

function EmptyTimeline({ isPolling }: { isPolling: boolean }) {
function EmptyTimeline({ isPolling, error, retryConnection }: { isPolling: boolean, error?: string | null, retryConnection?: () => void }) {
if (error) {
return (
<div className="flex flex-col gap-4 h-full justify-center items-center px-6">
<Alert variant="destructive" className="max-w-md">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>{error}</AlertTitle>
</Alert>
{retryConnection && (
<Button
variant="outline"
size="sm"
onClick={retryConnection}
className="flex items-center gap-2"
>
<RefreshCw className="h-4 w-4" />
Retry Connection
</Button>
)}
</div>
);
}
return (
<div className="flex flex-col gap-2 h-full justify-center items-center">
{isPolling ? (
Expand All @@ -72,6 +96,8 @@ export function TimelineWrapper({
conversation,
enableAutoScroll = false,
isPolling = false,
error,
retryConnection,
}: TimelineWrapperProps) {
const [selected, setSelected] = useState<SelectedPanel | null>(null);
const [panelVisible, setPanelVisible] = useState(false);
Expand Down Expand Up @@ -239,7 +265,7 @@ export function TimelineWrapper({
</div>
<div className="p-0 flex-1 min-h-0">
{sortedActivities.length === 0 ? (
<EmptyTimeline isPolling={isPolling} />
<EmptyTimeline isPolling={isPolling} error={error} retryConnection={retryConnection} />
) : enableAutoScroll ? (
<StickToBottom
className="h-full [&>div]:overflow-y-auto [&>div]:scrollbar-thin [&>div]:scrollbar-thumb-muted-foreground/30 [&>div]:scrollbar-track-transparent dark:[&>div]:scrollbar-thumb-muted-foreground/50"
Expand Down
66 changes: 66 additions & 0 deletions agents-manage-ui/src/components/ui/alert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
},
defaultVariants: {
variant: "default",
},
}
)

function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
)
}

function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className
)}
{...props}
/>
)
}

function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className
)}
{...props}
/>
)
}

export { Alert, AlertTitle, AlertDescription }
30 changes: 23 additions & 7 deletions agents-manage-ui/src/hooks/use-chat-activities-polling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface UseChatActivitiesPollingReturn {
error: string | null;
startPolling: () => void;
stopPolling: () => void;
retryConnection: () => void;
}

export const useChatActivitiesPolling = ({
Expand All @@ -31,11 +32,6 @@ export const useChatActivitiesPolling = ({
try {
setError(null);

// Cancel any previous request
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}

// Create new abort controller for this request
abortControllerRef.current = new AbortController();
const currentConversationId = conversationId; // Capture current ID
Expand All @@ -46,7 +42,9 @@ export const useChatActivitiesPolling = ({

if (!response.ok) {
// If conversation doesn't exist yet, that's fine - just return
if (response.status === 404) return;
if (response.status === 404) {
return;
}
throw new Error('Failed to fetch chat activities');
}

Expand All @@ -68,8 +66,18 @@ export const useChatActivitiesPolling = ({
}

if (isComponentMountedRef.current) {
console.warn('Error fetching chat activities:', err);
setError(err instanceof Error ? err.message : 'An error occurred');
// Stop polling on error to prevent repeated failed requests
setIsPolling(false);
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
// Cancel any pending requests
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
}
}
}
}, [conversationId, lastActivityCount]);
Expand Down Expand Up @@ -103,6 +111,13 @@ export const useChatActivitiesPolling = ({
}
}, []);

// Retry connection - clears error and restarts polling
const retryConnection = useCallback(() => {
setError(null);
stopPolling();
startPolling();
}, [startPolling, stopPolling]);

// Cleanup on unmount
useEffect(() => {
isComponentMountedRef.current = true;
Expand Down Expand Up @@ -141,5 +156,6 @@ export const useChatActivitiesPolling = ({
error,
startPolling,
stopPolling,
retryConnection,
};
};
Loading