Skip to content

Commit b1fa260

Browse files
fix(databunny): getting null is not an object (evaluating 'context.user.role') in chat response (#79)
* fix(databunny): improve error handling and type safety in processor, chart, and metric handlers * chore: format * fix(chart-handler): ensure chartType is always defined in response
1 parent f9fce46 commit b1fa260

File tree

9 files changed

+50
-36
lines changed

9 files changed

+50
-36
lines changed

apps/api/src/agent/handlers/chart-handler.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import type { User } from '@databuddy/auth';
2+
import type { Website } from '@databuddy/shared';
13
import type { z } from 'zod';
24
import type { AIResponseJsonSchema } from '../prompts/agent';
35
import { executeQuery } from '../utils/query-executor';
46
import { validateSQL } from '../utils/sql-validator';
57
import type { StreamingUpdate } from '../utils/stream-utils';
68

7-
const getRandomMessage = (messages: string[]) =>
8-
messages[Math.floor(Math.random() * messages.length)];
9+
const getRandomMessage = (messages: string[]): string =>
10+
messages[Math.floor(Math.random() * messages.length)] ||
11+
'An error occurred while processing your request.';
912

1013
const queryFailedMessages = [
1114
'I ran into an issue getting that data. The information might not be available right now.',
@@ -20,8 +23,8 @@ const noDataMessages = [
2023
];
2124

2225
export interface ChartHandlerContext {
23-
user: any;
24-
website: any;
26+
user?: User | null;
27+
website: Website;
2528
debugInfo: Record<string, unknown>;
2629
startTime: number;
2730
aiTime: number;
@@ -35,7 +38,7 @@ export async function* handleChartResponse(
3538
yield {
3639
type: 'error',
3740
content: 'AI did not provide a query for the chart.',
38-
debugInfo: context.user.role === 'ADMIN' ? context.debugInfo : undefined,
41+
debugInfo: context.user?.role === 'ADMIN' ? context.debugInfo : undefined,
3942
};
4043
return;
4144
}
@@ -44,7 +47,7 @@ export async function* handleChartResponse(
4447
yield {
4548
type: 'error',
4649
content: 'Generated query failed security validation.',
47-
debugInfo: context.user.role === 'ADMIN' ? context.debugInfo : undefined,
50+
debugInfo: context.user?.role === 'ADMIN' ? context.debugInfo : undefined,
4851
};
4952
return;
5053
}
@@ -53,7 +56,7 @@ export async function* handleChartResponse(
5356
const queryResult = await executeQuery(parsedAiJson.sql);
5457
const totalTime = Date.now() - context.startTime;
5558

56-
if (context.user.role === 'ADMIN') {
59+
if (context.user?.role === 'ADMIN') {
5760
context.debugInfo.processing = {
5861
aiTime: context.aiTime,
5962
queryTime: Date.now() - context.startTime - context.aiTime,
@@ -65,15 +68,15 @@ export async function* handleChartResponse(
6568
type: 'complete',
6669
content:
6770
queryResult.data.length > 0
68-
? `Found ${queryResult.data.length} data points. Displaying as a ${parsedAiJson.chart_type?.replace(/_/g, ' ') || 'chart'}.`
71+
? `Found ${queryResult.data.length} data points. Displaying as a ${parsedAiJson.chart_type ? parsedAiJson.chart_type.replace(/_/g, ' ') : 'chart'}.`
6972
: getRandomMessage(noDataMessages),
7073
data: {
7174
hasVisualization: queryResult.data.length > 0,
7275
chartType: parsedAiJson.chart_type,
7376
data: queryResult.data,
7477
responseType: 'chart',
7578
},
76-
debugInfo: context.user.role === 'ADMIN' ? context.debugInfo : undefined,
79+
debugInfo: context.user?.role === 'ADMIN' ? context.debugInfo : undefined,
7780
};
7881
} catch (queryError: unknown) {
7982
console.error('❌ SQL execution error', {
@@ -83,7 +86,7 @@ export async function* handleChartResponse(
8386
yield {
8487
type: 'error',
8588
content: getRandomMessage(queryFailedMessages),
86-
debugInfo: context.user.role === 'ADMIN' ? context.debugInfo : undefined,
89+
debugInfo: context.user?.role === 'ADMIN' ? context.debugInfo : undefined,
8790
};
8891
}
8992
}

apps/api/src/agent/handlers/metric-handler.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
import type { User } from '@databuddy/auth';
2+
import type { Website } from '@databuddy/shared';
13
import type { z } from 'zod';
24
import type { AIResponseJsonSchema } from '../prompts/agent';
35
import { executeQuery } from '../utils/query-executor';
46
import { validateSQL } from '../utils/sql-validator';
57
import type { StreamingUpdate } from '../utils/stream-utils';
68

79
export interface MetricHandlerContext {
8-
user: any;
9-
website: any;
10+
user?: User | null;
11+
website: Website;
1012
debugInfo: Record<string, unknown>;
1113
}
1214

@@ -20,7 +22,7 @@ export async function* handleMetricResponse(
2022
type: 'error',
2123
content: 'Generated query failed security validation.',
2224
debugInfo:
23-
context.user.role === 'ADMIN' ? context.debugInfo : undefined,
25+
context.user?.role === 'ADMIN' ? context.debugInfo : undefined,
2426
};
2527
return;
2628
}
@@ -86,6 +88,6 @@ async function* sendMetricResponse(
8688
metricValue,
8789
metricLabel: parsedAiJson.metric_label,
8890
},
89-
debugInfo: context.user.role === 'ADMIN' ? context.debugInfo : undefined,
91+
debugInfo: context.user?.role === 'ADMIN' ? context.debugInfo : undefined,
9092
};
9193
}

apps/api/src/agent/processor.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import type { StreamingUpdate } from './utils/stream-utils';
99
import { generateThinkingSteps } from './utils/stream-utils';
1010

1111
// Simple message variation helpers
12-
const getRandomMessage = (messages: string[]) =>
13-
messages[Math.floor(Math.random() * messages.length)];
12+
const getRandomMessage = (messages: string[]): string =>
13+
messages[Math.floor(Math.random() * messages.length)] ||
14+
'An error occurred while processing your request.';
1415

1516
const parseErrorMessages = [
1617
"I'm having trouble understanding that request. Could you try asking in a different way?",
@@ -38,7 +39,7 @@ export interface AssistantRequest {
3839
}
3940

4041
export interface AssistantContext {
41-
user: User;
42+
user?: User | null;
4243
website: Website;
4344
debugInfo: Record<string, unknown>;
4445
}
@@ -56,7 +57,7 @@ export async function* processAssistantRequest(
5657
website_hostname: request.website_hostname,
5758
});
5859

59-
if (context.user.role === 'ADMIN') {
60+
if (context.user?.role === 'ADMIN') {
6061
context.debugInfo.validatedInput = {
6162
message: request.message,
6263
website_id: request.website_id,
@@ -70,7 +71,10 @@ export async function* processAssistantRequest(
7071
request.website_id,
7172
request.website_hostname,
7273
'execute_chat',
73-
request.context?.previousMessages,
74+
request.context?.previousMessages?.map((msg) => ({
75+
role: msg.role || 'user',
76+
content: msg.content,
77+
})),
7478
undefined,
7579
request.model
7680
);
@@ -90,7 +94,7 @@ export async function* processAssistantRequest(
9094
type: 'error',
9195
content: getRandomMessage(parseErrorMessages),
9296
debugInfo:
93-
context.user.role === 'ADMIN'
97+
context.user?.role === 'ADMIN'
9498
? {
9599
...context.debugInfo,
96100
parseError: parsedResponse.error,
@@ -107,7 +111,7 @@ export async function* processAssistantRequest(
107111
type: 'error',
108112
content: getRandomMessage(parseErrorMessages),
109113
debugInfo:
110-
context.user.role === 'ADMIN' ? context.debugInfo : undefined,
114+
context.user?.role === 'ADMIN' ? context.debugInfo : undefined,
111115
};
112116
return;
113117
}
@@ -131,7 +135,7 @@ export async function* processAssistantRequest(
131135
aiJson.text_response || "Here's the answer to your question.",
132136
data: { hasVisualization: false, responseType: 'text' },
133137
debugInfo:
134-
context.user.role === 'ADMIN' ? context.debugInfo : undefined,
138+
context.user?.role === 'ADMIN' ? context.debugInfo : undefined,
135139
};
136140
break;
137141

@@ -151,7 +155,7 @@ export async function* processAssistantRequest(
151155
type: 'error',
152156
content: 'Invalid chart configuration.',
153157
debugInfo:
154-
context.user.role === 'ADMIN' ? context.debugInfo : undefined,
158+
context.user?.role === 'ADMIN' ? context.debugInfo : undefined,
155159
};
156160
}
157161
break;
@@ -161,7 +165,7 @@ export async function* processAssistantRequest(
161165
type: 'error',
162166
content: 'Invalid response format from AI.',
163167
debugInfo:
164-
context.user.role === 'ADMIN' ? context.debugInfo : undefined,
168+
context.user?.role === 'ADMIN' ? context.debugInfo : undefined,
165169
};
166170
}
167171
} catch (error: unknown) {
@@ -175,7 +179,7 @@ export async function* processAssistantRequest(
175179
type: 'error',
176180
content: getRandomMessage(unexpectedErrorMessages),
177181
debugInfo:
178-
context.user.role === 'ADMIN' ? { error: errorMessage } : undefined,
182+
context.user?.role === 'ADMIN' ? { error: errorMessage } : undefined,
179183
};
180184
}
181185
}

apps/api/src/routes/assistant.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export const assistant = new Elysia({ prefix: '/v1/assistant' })
5353
};
5454

5555
const assistantContext: AssistantContext = {
56-
user: (user ?? null) as unknown as AssistantContext['user'],
56+
user: user ?? null,
5757
website,
5858
debugInfo: {},
5959
};

apps/dashboard/app/(main)/websites/[id]/_components/utils/add-filters.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
'use client';
22

3-
import { Suspense, useCallback, useState } from 'react';
4-
import { useParams } from 'next/navigation';
3+
import { type DynamicQueryFilter, filterOptions } from '@databuddy/shared';
54
import { FunnelIcon } from '@phosphor-icons/react';
6-
import { filterOptions, type DynamicQueryFilter } from '@databuddy/shared';
5+
import { useParams } from 'next/navigation';
6+
import { Suspense, useCallback, useState } from 'react';
77
import { Button } from '@/components/ui/button';
88
import {
99
Command,
@@ -31,12 +31,12 @@ import {
3131
SelectTrigger,
3232
SelectValue,
3333
} from '@/components/ui/select';
34+
import { Skeleton } from '@/components/ui/skeleton';
35+
import { operatorOptions } from '@/hooks/use-filters';
3436
import {
3537
type AutocompleteData,
3638
useAutocompleteData,
3739
} from '@/hooks/use-funnels';
38-
import { operatorOptions } from '@/hooks/use-filters';
39-
import { Skeleton } from '@/components/ui/skeleton';
4040

4141
type OperatorOption = (typeof operatorOptions)[number];
4242
type FilterOption = (typeof filterOptions)[number];
@@ -256,7 +256,7 @@ function FilterForm({
256256
<div className="flex flex-col gap-2">
257257
{Array.from({ length: Math.min(numberOfFilters, 5) }).map(
258258
(_, index) => (
259-
<Skeleton key={`filter-skeleton-${index}`} className="h-8 w-full" />
259+
<Skeleton className="h-8 w-full" key={`filter-skeleton-${index}`} />
260260
)
261261
)}
262262
</div>

apps/dashboard/app/(main)/websites/[id]/_components/utils/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import type { DateRange as BaseDateRange, DynamicQueryFilter, Website } from '@databuddy/shared';
1+
import type {
2+
DateRange as BaseDateRange,
3+
DynamicQueryFilter,
4+
Website,
5+
} from '@databuddy/shared';
26

37
// Extended date range with granularity
48
export interface DateRange extends BaseDateRange {

apps/dashboard/app/(main)/websites/[id]/assistant/components/visualization-section.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ChartLineIcon,
66
ChartPieIcon,
77
CompassIcon,
8+
DatabaseIcon,
89
DotsThreeOutlineVerticalIcon,
910
FunnelIcon,
1011
GaugeIcon,

apps/dashboard/app/(main)/websites/[id]/funnels/_components/edit-funnel-dialog.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
'use client';
22

3+
import { filterOptions } from '@databuddy/shared';
34
import {
45
DragDropContext,
56
Draggable,
67
Droppable,
78
type DropResult,
89
} from '@hello-pangea/dnd';
9-
import { filterOptions } from '@databuddy/shared';
1010
import {
1111
ChartBarIcon,
1212
FunnelIcon,
@@ -32,14 +32,14 @@ import {
3232
SheetHeader,
3333
SheetTitle,
3434
} from '@/components/ui/sheet';
35+
import { operatorOptions, useFilters } from '@/hooks/use-filters';
3536
import type {
3637
AutocompleteData,
3738
CreateFunnelData,
3839
Funnel,
3940
FunnelFilter,
4041
FunnelStep,
4142
} from '@/hooks/use-funnels';
42-
import { operatorOptions, useFilters } from '@/hooks/use-filters';
4343
import { AutocompleteInput, DraggableStep } from './funnel-components';
4444

4545
const defaultFilter: FunnelFilter = {

apps/dashboard/app/(main)/websites/[id]/goals/_components/edit-goal-dialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ import {
2727
SheetHeader,
2828
SheetTitle,
2929
} from '@/components/ui/sheet';
30+
import { operatorOptions, useFilters } from '@/hooks/use-filters';
3031
import type { AutocompleteData } from '@/hooks/use-funnels';
3132
import type { CreateGoalData, Goal } from '@/hooks/use-goals';
32-
import { operatorOptions, useFilters } from '@/hooks/use-filters';
3333
import { AutocompleteInput } from '../../funnels/_components/funnel-components';
3434

3535
const defaultFilter: GoalFilter = {

0 commit comments

Comments
 (0)