diff --git a/src/client/base.ts b/src/client/base.ts index 4104da7..ab6f322 100644 --- a/src/client/base.ts +++ b/src/client/base.ts @@ -4,7 +4,7 @@ import https from "node:https"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import axios, { AxiosInstance } from "axios"; +import axios, { AxiosInstance, AxiosResponse } from "axios"; import { parse as csvParse } from "csv-parse/sync"; import { z } from "zod"; @@ -153,24 +153,28 @@ export class BaseIterableClient { /** * Parse CSV response into an array of objects using csv-parse library + * @throws IterableResponseValidationError if CSV parsing fails */ - public parseCsv(data: string): any[] { - if (!data) { + public parseCsv(response: AxiosResponse): Record[] { + if (!response.data) { return []; } try { - return csvParse(data, { + return csvParse(response.data, { columns: true, // Use first line as headers skip_empty_lines: true, trim: true, }); } catch (error) { - // Log error but don't throw - return empty array for graceful degradation - logger.warn("Failed to parse CSV data", { - error: error instanceof Error ? error.message : String(error), - }); - return []; + // Throw validation error to maintain consistent error handling + // This allows callers to handle parse failures appropriately + throw new IterableResponseValidationError( + 200, + response.data, + `CSV parse error: ${error instanceof Error ? error.message : String(error)}`, + response.config?.url + ); } } diff --git a/src/client/campaigns.ts b/src/client/campaigns.ts index a9c9119..960edab 100644 --- a/src/client/campaigns.ts +++ b/src/client/campaigns.ts @@ -4,6 +4,7 @@ import { ArchiveCampaignsParams, ArchiveCampaignsResponse, ArchiveCampaignsResponseSchema, + CampaignMetricsResponse, CancelCampaignParams, CreateCampaignParams, CreateCampaignResponse, @@ -111,7 +112,7 @@ export function Campaigns>(Base: T) { async getCampaignMetrics( options: GetCampaignMetricsParams - ): Promise { + ): Promise { const params = new URLSearchParams(); params.append("campaignId", options.campaignId.toString()); if (options.startDateTime) @@ -127,7 +128,7 @@ export function Campaigns>(Base: T) { ); // Parse CSV response into array of objects - return this.parseCsv(response.data); + return this.parseCsv(response); } async scheduleCampaign( diff --git a/src/client/experiments.ts b/src/client/experiments.ts index d5e1412..8972bc7 100644 --- a/src/client/experiments.ts +++ b/src/client/experiments.ts @@ -42,7 +42,7 @@ export function Experiments>( }); // Parse CSV response into array of objects - return this.parseCsv(response.data); + return this.parseCsv(response); } }; } diff --git a/src/types/campaigns.ts b/src/types/campaigns.ts index a09a900..581b94e 100644 --- a/src/types/campaigns.ts +++ b/src/types/campaigns.ts @@ -114,6 +114,9 @@ export type GetCampaignResponse = z.infer; export type GetCampaignMetricsParams = z.infer< typeof GetCampaignMetricsParamsSchema >; +export type CampaignMetricsResponse = z.infer< + typeof CampaignMetricsResponseSchema +>; export type CreateCampaignParams = z.infer; export type CreateCampaignResponse = z.infer< typeof CreateCampaignResponseSchema @@ -136,8 +139,10 @@ export const CampaignsResponseSchema = z.object({ campaigns: z.array(CampaignDetailsSchema), }); +// The API returns CSV data which we parse into objects +// All CSV values are strings export const CampaignMetricsResponseSchema = z - .array(z.record(z.string(), z.any())) + .array(z.record(z.string(), z.string())) .describe("Parsed campaign metrics data"); // Campaign creation schemas diff --git a/src/types/experiments.ts b/src/types/experiments.ts index 6d62f00..074bcfe 100644 --- a/src/types/experiments.ts +++ b/src/types/experiments.ts @@ -8,7 +8,7 @@ import { IterableDateTimeSchema } from "./common.js"; // The API returns CSV data which we parse into objects export const ExperimentMetricsResponseSchema = z - .array(z.record(z.string(), z.any())) + .array(z.record(z.string(), z.string())) .describe("Parsed experiment metrics data"); export const GetExperimentMetricsParamsSchema = z diff --git a/tests/unit/campaigns.test.ts b/tests/unit/campaigns.test.ts index d976005..d172979 100644 --- a/tests/unit/campaigns.test.ts +++ b/tests/unit/campaigns.test.ts @@ -288,6 +288,15 @@ describe("Campaign Management", () => { { responseType: "text" } ); }); + + it("should throw IterableResponseValidationError for invalid CSV", async () => { + const mockResponse = { data: "invalid,csv\n\ndata" }; + mockAxiosInstance.get.mockResolvedValue(mockResponse); + + await expect( + client.getCampaignMetrics({ campaignId: 12345 }) + ).rejects.toThrow("CSV parse error"); + }); }); describe("getChildCampaigns", () => { diff --git a/tests/unit/experiments.test.ts b/tests/unit/experiments.test.ts index 101aa33..fa73c19 100644 --- a/tests/unit/experiments.test.ts +++ b/tests/unit/experiments.test.ts @@ -217,5 +217,14 @@ describe("Experiment Operations", () => { expect(result).toEqual(expectedParsedData); expect(Array.isArray(result)).toBe(true); }); + + it("should throw IterableResponseValidationError for invalid CSV", async () => { + const mockResponse = { data: "invalid,csv\n\ndata" }; + mockAxiosInstance.get.mockResolvedValue(mockResponse); + + await expect(client.getExperimentMetrics()).rejects.toThrow( + "CSV parse error" + ); + }); }); });