Skip to content

Commit 916ab8f

Browse files
Copilotrajbos
andcommitted
Add comprehensive error handling for file validation and CSV parsing
Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com>
1 parent 24c8f0b commit 916ab8f

2 files changed

Lines changed: 98 additions & 9 deletions

File tree

src/App.tsx

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,32 @@ function App() {
3333
const processFile = useCallback((file: File) => {
3434
if (!file) return;
3535

36+
// Add basic file validation
37+
if (!file.type.includes('text') && !file.type.includes('csv') && !file.name.endsWith('.csv')) {
38+
toast.error("Please upload a valid CSV or text file.");
39+
return;
40+
}
41+
3642
const reader = new FileReader();
43+
44+
reader.onerror = () => {
45+
toast.error("Failed to read the file. The file may be corrupted or unreadable.");
46+
};
47+
3748
reader.onload = (e) => {
3849
try {
3950
const csvContent = e.target?.result as string;
40-
if (!csvContent) throw new Error("Failed to read file");
51+
if (!csvContent) throw new Error("Failed to read file content");
52+
53+
// Check if the content looks like text (not binary)
54+
if (csvContent.includes('\0')) {
55+
throw new Error("File appears to be binary. Please upload a text-based CSV file.");
56+
}
57+
58+
// Check for minimum content
59+
if (csvContent.trim().length === 0) {
60+
throw new Error("File is empty. Please upload a CSV file with data.");
61+
}
4162

4263
const parsedData = parseCSV(csvContent);
4364
setData(parsedData);
@@ -61,7 +82,32 @@ function App() {
6182
toast.success(`Loaded ${parsedData.length} records successfully`);
6283
} catch (error) {
6384
console.error("Error parsing CSV:", error);
64-
toast.error("Failed to parse CSV file. Please check the format.");
85+
let errorMessage = "Failed to parse CSV file. Please check the format.";
86+
87+
if (error instanceof Error) {
88+
// Provide more specific error messages based on the error type
89+
if (error.message.includes("missing required columns")) {
90+
errorMessage = "Invalid CSV format: " + error.message;
91+
} else if (error.message.includes("Invalid timestamp")) {
92+
errorMessage = "Invalid data format: " + error.message;
93+
} else if (error.message.includes("Invalid requests used")) {
94+
errorMessage = "Invalid data format: " + error.message;
95+
} else if (error.message.includes("Invalid exceeds quota")) {
96+
errorMessage = "Invalid data format: " + error.message;
97+
} else if (error.message.includes("Invalid CSV row format")) {
98+
errorMessage = "Invalid CSV structure: " + error.message;
99+
} else if (error.message.includes("binary")) {
100+
errorMessage = error.message;
101+
} else if (error.message.includes("empty")) {
102+
errorMessage = error.message;
103+
} else if (error.message.includes("header")) {
104+
errorMessage = "Invalid CSV format: " + error.message;
105+
} else {
106+
errorMessage = error.message;
107+
}
108+
}
109+
110+
toast.error(errorMessage);
65111
setData(null);
66112
setAggregatedData([]);
67113
setModelSummary([]);
@@ -107,10 +153,10 @@ function App() {
107153
const files = e.dataTransfer?.files;
108154
if (files && files.length > 0) {
109155
const file = files[0];
110-
if (file.type === "text/csv" || file.name.endsWith('.csv')) {
156+
if (file.type === "text/csv" || file.name.endsWith('.csv') || file.type.includes('text')) {
111157
processFile(file);
112158
} else {
113-
toast.error("Please upload a CSV file.");
159+
toast.error("Please upload a CSV file. Supported formats: .csv files or text files with CSV content.");
114160
}
115161
}
116162
}, [processFile]);

src/lib/utils.ts

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,38 @@ export function parseCSV(csv: string): CopilotUsageData[] {
2727
throw new Error('CSV must contain a header row and at least one data row');
2828
}
2929

30+
// Validate header row
31+
const headerLine = lines[0];
32+
const expectedHeaders = ['Timestamp', 'User', 'Model', 'Requests Used', 'Exceeds Monthly Quota', 'Total Monthly Quota'];
33+
34+
// Parse header row to check for expected columns
35+
const headerMatches = headerLine.match(/("([^"]*)"|([^,]*))(,|$)/g);
36+
if (!headerMatches || headerMatches.length < 6) {
37+
throw new Error('CSV header must contain at least 6 columns');
38+
}
39+
40+
const headers = headerMatches.map(m =>
41+
m.endsWith(',')
42+
? m.slice(0, -1).replace(/^"(.*)"$/, '$1')
43+
: m.replace(/^"(.*)"$/, '$1')
44+
);
45+
46+
// Check if all expected headers are present (case-insensitive)
47+
const missingHeaders = expectedHeaders.filter(expected =>
48+
!headers.some(header => header.toLowerCase().includes(expected.toLowerCase()))
49+
);
50+
51+
if (missingHeaders.length > 0) {
52+
throw new Error(`CSV is missing required columns: ${missingHeaders.join(', ')}. Expected columns: ${expectedHeaders.join(', ')}`);
53+
}
54+
3055
// Skip the header row and process data rows
31-
return lines.slice(1).map(line => {
56+
return lines.slice(1).map((line, index) => {
3257
// Handle quoted CSV properly
3358
const matches = line.match(/("([^"]*)"|([^,]*))(,|$)/g);
3459

3560
if (!matches || matches.length < 6) {
36-
throw new Error('Invalid CSV row format');
61+
throw new Error(`Invalid CSV row format at line ${index + 2}: expected 6 columns, got ${matches ? matches.length : 0}`);
3762
}
3863

3964
const values = matches.map(m =>
@@ -42,12 +67,30 @@ export function parseCSV(csv: string): CopilotUsageData[] {
4267
: m.replace(/^"(.*)"$/, '$1')
4368
);
4469

70+
// Validate timestamp
71+
const timestamp = new Date(values[0]);
72+
if (isNaN(timestamp.getTime())) {
73+
throw new Error(`Invalid timestamp format at line ${index + 2}: "${values[0]}"`);
74+
}
75+
76+
// Validate requests used
77+
const requestsUsed = parseFloat(values[3]);
78+
if (isNaN(requestsUsed)) {
79+
throw new Error(`Invalid requests used value at line ${index + 2}: "${values[3]}" must be a number`);
80+
}
81+
82+
// Validate exceeds quota
83+
const exceedsQuotaValue = values[4].toLowerCase();
84+
if (exceedsQuotaValue !== 'true' && exceedsQuotaValue !== 'false') {
85+
throw new Error(`Invalid exceeds quota value at line ${index + 2}: "${values[4]}" must be "true" or "false"`);
86+
}
87+
4588
return {
46-
timestamp: new Date(values[0]),
89+
timestamp,
4790
user: values[1],
4891
model: values[2],
49-
requestsUsed: parseFloat(values[3]),
50-
exceedsQuota: values[4].toLowerCase() === "true",
92+
requestsUsed,
93+
exceedsQuota: exceedsQuotaValue === "true",
5194
totalMonthlyQuota: values[5],
5295
};
5396
});

0 commit comments

Comments
 (0)