Skip to content

Commit 8331f1b

Browse files
authored
[QUO-991] Do not throw exceptions, log them (#8)
* initial commit, do not throw exceptions log them * update all code to log instead of throw * keep it 100 * add two catches that were present in python sdk * bump version for package cut
1 parent 230e1b1 commit 8331f1b

File tree

19 files changed

+564
-180
lines changed

19 files changed

+564
-180
lines changed

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "quotientai",
3-
"version": "0.0.4",
3+
"version": "0.0.5",
44
"description": "TypeScript client for QuotientAI API",
55
"main": "dist/quotientai/index.js",
66
"types": "dist/quotientai/index.d.ts",
@@ -14,7 +14,8 @@
1414
"build": "tsc",
1515
"test": "vitest run",
1616
"coverage": "vitest run --coverage",
17-
"lint": "eslint . --ext .ts"
17+
"lint": "eslint . --ext .ts",
18+
"test:watch": "vitest"
1819
},
1920
"dependencies": {
2021
"axios": "^1.6.7",

quotientai/client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as fs from 'fs';
44
import * as path from 'path';
55
import * as os from 'os';
66
import { TokenData } from './types';
7-
import { QuotientAIError } from './exceptions';
7+
import { logError, QuotientAIError } from './exceptions';
88

99
export class BaseQuotientClient {
1010
private apiKey: string;
@@ -76,7 +76,7 @@ export class BaseQuotientClient {
7676
JSON.stringify({ token, expires_at: expiry })
7777
);
7878
} catch (error) {
79-
throw new QuotientAIError('Could not create directory for token. If you see this error please notify us at [email protected]');
79+
logError(new QuotientAIError('Could not create directory for token. If you see this error please notify us at [email protected]'));
8080
}
8181
}
8282

quotientai/exceptions.ts

Lines changed: 90 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
22

3+
export function logError(error: Error, context?: string) {
4+
const timestamp = new Date().toISOString();
5+
const contextStr = context ? `[${context}] ` : '';
6+
const stack = error.stack || '';
7+
8+
console.error(`[${timestamp}] ${contextStr}${error.name}: ${error.message}`);
9+
console.error(stack);
10+
}
11+
312
export class QuotientAIError extends Error {
413
constructor(message: string) {
514
super(message);
@@ -79,34 +88,66 @@ export class APITimeoutError extends APIConnectionError {
7988

8089
export class BadRequestError extends APIStatusError {
8190
status = 400;
91+
constructor(message: string, response: AxiosResponse, body?: any) {
92+
super(message, response, body);
93+
this.name = 'BadRequestError';
94+
}
8295
}
8396

8497
export class AuthenticationError extends APIStatusError {
8598
status = 401;
99+
constructor(message: string, response: AxiosResponse, body?: any) {
100+
super(message, response, body);
101+
this.name = 'AuthenticationError';
102+
}
86103
}
87104

88105
export class PermissionDeniedError extends APIStatusError {
89106
status = 403;
107+
constructor(message: string, response: AxiosResponse, body?: any) {
108+
super(message, response, body);
109+
this.name = 'PermissionDeniedError';
110+
}
90111
}
91112

92113
export class NotFoundError extends APIStatusError {
93114
status = 404;
115+
constructor(message: string, response: AxiosResponse, body?: any) {
116+
super(message, response, body);
117+
this.name = 'NotFoundError';
118+
}
94119
}
95120

96121
export class ConflictError extends APIStatusError {
97122
status = 409;
123+
constructor(message: string, response: AxiosResponse, body?: any) {
124+
super(message, response, body);
125+
this.name = 'ConflictError';
126+
}
98127
}
99128

100129
export class UnprocessableEntityError extends APIStatusError {
101130
status = 422;
131+
constructor(message: string, response: AxiosResponse, body?: any) {
132+
super(message, response, body);
133+
this.name = 'UnprocessableEntityError';
134+
}
102135
}
103136

104137
export class RateLimitError extends APIStatusError {
105138
status = 429;
139+
constructor(message: string, response: AxiosResponse, body?: any) {
140+
super(message, response, body);
141+
this.name = 'RateLimitError';
142+
}
106143
}
107144

108145
export class InternalServerError extends APIStatusError {
109146
status = 500;
147+
constructor(message: string, response: AxiosResponse, body?: any) {
148+
super(message, response, body);
149+
this.name = 'InternalServerError';
150+
}
110151
}
111152

112153
export function parseUnprocessableEntityError(response: AxiosResponse): string {
@@ -123,9 +164,13 @@ export function parseUnprocessableEntityError(response: AxiosResponse): string {
123164
return `missing required fields: ${missingFields.join(', ')}`;
124165
}
125166
}
126-
throw new APIResponseValidationError(response, body);
167+
const error = new APIResponseValidationError(response, body);
168+
logError(error, 'parseUnprocessableEntityError');
169+
return 'Invalid response format';
127170
} catch (error) {
128-
throw new APIResponseValidationError(response, null);
171+
const apiError = new APIResponseValidationError(response, null);
172+
logError(apiError, 'parseUnprocessableEntityError');
173+
return 'Invalid response format';
129174
}
130175
}
131176

@@ -135,9 +180,13 @@ export function parseBadRequestError(response: AxiosResponse): string {
135180
if ('detail' in body) {
136181
return body.detail;
137182
}
138-
throw new APIResponseValidationError(response, body);
183+
const error = new APIResponseValidationError(response, body);
184+
logError(error, 'parseBadRequestError');
185+
return 'Invalid request format';
139186
} catch (error) {
140-
throw new APIResponseValidationError(response, null);
187+
const apiError = new APIResponseValidationError(response, null);
188+
logError(apiError, 'parseBadRequestError');
189+
return 'Invalid request format';
141190
}
142191
}
143192

@@ -153,50 +202,66 @@ export function handleErrors() {
153202
try {
154203
const response = await originalMethod.apply(this, args);
155204
return response.data;
156-
} catch (error) {
157-
if (axios.isAxiosError(error)) {
158-
const axiosError = error as AxiosError;
205+
} catch (err) {
206+
if (axios.isAxiosError(err)) {
207+
const axiosError = err as AxiosError;
159208

160209
if (axiosError.response) {
161210
const { status, data } = axiosError.response;
162211

163212
switch (status) {
164213
case 400: {
165214
const message = parseBadRequestError(axiosError.response);
166-
throw new BadRequestError(message, axiosError.response, data);
215+
const error = new BadRequestError(message, axiosError.response, data);
216+
logError(error, `${target.constructor.name}.${propertyKey}`);
217+
return null;
167218
}
168-
case 401:
169-
throw new AuthenticationError(
219+
case 401: {
220+
const error = new AuthenticationError(
170221
'unauthorized: the request requires user authentication. ensure your API key is correct.',
171222
axiosError.response,
172223
data
173224
);
174-
case 403:
175-
throw new PermissionDeniedError(
225+
logError(error, `${target.constructor.name}.${propertyKey}`);
226+
return null;
227+
}
228+
case 403: {
229+
const error = new PermissionDeniedError(
176230
'forbidden: the server understood the request, but it refuses to authorize it.',
177231
axiosError.response,
178232
data
179233
);
180-
case 404:
181-
throw new NotFoundError(
234+
logError(error, `${target.constructor.name}.${propertyKey}`);
235+
return null;
236+
}
237+
case 404: {
238+
const error = new NotFoundError(
182239
'not found: the server can not find the requested resource.',
183240
axiosError.response,
184241
data
185242
);
243+
logError(error, `${target.constructor.name}.${propertyKey}`);
244+
return null;
245+
}
186246
case 422: {
187247
const unprocessableMessage = parseUnprocessableEntityError(axiosError.response);
188-
throw new UnprocessableEntityError(
248+
const error = new UnprocessableEntityError(
189249
unprocessableMessage,
190250
axiosError.response,
191251
data
192252
);
253+
logError(error, `${target.constructor.name}.${propertyKey}`);
254+
return null;
193255
}
194-
default:
195-
throw new APIStatusError(
256+
default: {
257+
const error = new APIStatusError(
196258
`unexpected status code: ${status}. contact [email protected] for help.`,
197259
axiosError.response,
198260
data
199261
);
262+
logError(error, `${target.constructor.name}.${propertyKey}`);
263+
return null;
264+
}
200265
}
201266
}
202267

@@ -207,15 +272,20 @@ export function handleErrors() {
207272
delay *= 2; // Exponential backoff
208273
continue;
209274
}
210-
throw new APITimeoutError(axiosError.config);
275+
const error = new APITimeoutError(axiosError.config);
276+
logError(error, `${target.constructor.name}.${propertyKey}`);
277+
return null;
211278
}
212279

213-
throw new APIConnectionError(
280+
const connectionError = new APIConnectionError(
214281
'connection error. please try again later.',
215282
axiosError.config || { url: 'unknown' }
216283
);
284+
logError(connectionError, `${target.constructor.name}.${propertyKey}`);
285+
return null;
217286
}
218-
throw error;
287+
logError(err as Error, `${target.constructor.name}.${propertyKey}`);
288+
return null;
219289
}
220290
}
221291
};

quotientai/index.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,35 @@ import { ModelsResource } from './resources/models';
99
import { RunsResource } from './resources/runs';
1010
import { MetricsResource } from './resources/metrics';
1111
import { LogsResource } from './resources/logs';
12+
import { logError } from './exceptions';
1213

13-
export class QuotientAI {
14-
public auth: AuthResource;
15-
public prompts: PromptsResource;
16-
public datasets: DatasetsResource;
17-
public models: ModelsResource;
18-
public runs: RunsResource;
19-
public metrics: MetricsResource;
20-
public logs: LogsResource;
21-
public logger: QuotientLogger;
14+
export class QuotientAI {
15+
public auth: AuthResource = null!;
16+
public prompts: PromptsResource = null!;
17+
public datasets: DatasetsResource = null!;
18+
public models: ModelsResource = null!;
19+
public runs: RunsResource = null!;
20+
public metrics: MetricsResource = null!;
21+
public logs: LogsResource = null!;
22+
public logger: QuotientLogger = null!;
2223

2324
constructor(apiKey?: string) {
2425
const key = apiKey || process.env.QUOTIENT_API_KEY;
2526
if (!key) {
26-
throw new Error(
27+
const error = new Error(
2728
'Could not find API key. Either pass apiKey to QuotientAI() or ' +
2829
'set the QUOTIENT_API_KEY environment variable. ' +
2930
'If you do not have an API key, you can create one at https://app.quotientai.co in your settings page'
3031
);
32+
logError(error, 'QuotientAI.constructor');
33+
return;
34+
} else {
35+
const client = new BaseQuotientClient(key);
36+
this.initializeResources(client);
3137
}
38+
}
3239

33-
const client = new BaseQuotientClient(key);
34-
40+
private initializeResources(client: BaseQuotientClient): void {
3541
// Initialize resources
3642
this.auth = new AuthResource(client);
3743
this.prompts = new PromptsResource(client);
@@ -64,10 +70,12 @@ export class QuotientAI {
6470
);
6571

6672
if (invalidParameters.length > 0) {
67-
throw new Error(
73+
const error = new Error(
6874
`Invalid parameters: ${invalidParameters.join(', ')}. ` +
6975
`Valid parameters are: ${validParameters.join(', ')}`
7076
);
77+
logError(error, 'QuotientAI.evaluate');
78+
return null;
7179
}
7280

7381
return this.runs.create({

quotientai/logger.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { LogEntry, LoggerConfig, LogDocument } from './types';
2-
import { ValidationError } from './exceptions';
2+
import { ValidationError, logError } from './exceptions';
33

44
interface LogsResource {
55
create(params: LogEntry): Promise<any>;
@@ -32,7 +32,8 @@ export class QuotientLogger {
3232
this.configured = true;
3333

3434
if (this.sampleRate < 0 || this.sampleRate > 1) {
35-
throw new Error('sample_rate must be between 0.0 and 1.0');
35+
logError(new Error('sample_rate must be between 0.0 and 1.0'));
36+
return this;
3637
}
3738

3839
return this;
@@ -76,9 +77,9 @@ export class QuotientLogger {
7677
}
7778

7879
// Validate document format
79-
private validateDocuments(documents: (string | LogDocument)[]): void {
80+
private validateDocuments(documents: (string | LogDocument)[]): boolean {
8081
if (!documents || documents.length === 0) {
81-
return;
82+
return true;
8283
}
8384

8485
for (let i = 0; i < documents.length; i++) {
@@ -88,35 +89,43 @@ export class QuotientLogger {
8889
} else if (typeof doc === 'object' && doc !== null) {
8990
const validation = this.isValidLogDocument(doc);
9091
if (!validation.valid) {
91-
throw new ValidationError(
92+
logError(new ValidationError(
9293
`Invalid document format at index ${i}: ${validation.error}. ` +
9394
"Documents must be either strings or JSON objects with a 'page_content' string property and an optional 'metadata' object. " +
9495
"To fix this, ensure each document follows the format: { page_content: 'your text content', metadata?: { key: 'value' } }"
95-
);
96+
));
97+
return false;
9698
}
9799
} else {
98-
throw new ValidationError(
100+
logError(new ValidationError(
99101
`Invalid document type at index ${i}. Found ${typeof doc}, but documents must be either strings or JSON objects with a 'page_content' property. ` +
100102
"To fix this, provide documents as either simple strings or properly formatted objects: { page_content: 'your text content' }"
101-
);
103+
));
104+
return false;
102105
}
103106
}
107+
return true;
104108
}
105109

106110
// log a message
107111
// params: Omit<LogEntry, 'app_name' | 'environment'>
108112
async log(params: Omit<LogEntry, 'app_name' | 'environment'>): Promise<any> {
109113
if (!this.configured) {
110-
throw new Error('Logger is not configured. Please call init() before logging.');
114+
logError(new Error('Logger is not configured. Please call init() before logging.'));
115+
return null;
111116
}
112117

113118
if (!this.appName || !this.environment) {
114-
throw new Error('Logger is not properly configured. app_name and environment must be set.');
119+
logError(new Error('Logger is not properly configured. app_name and environment must be set.'));
120+
return null;
115121
}
116122

117123
// Validate documents format
118124
if (params.documents) {
119-
this.validateDocuments(params.documents);
125+
const isValid = this.validateDocuments(params.documents);
126+
if (!isValid) {
127+
return null;
128+
}
120129
}
121130

122131
// Merge default tags with any tags provided at log time

0 commit comments

Comments
 (0)