This commit is contained in:
Matt 2025-05-13 19:13:31 +02:00
parent 41105e2215
commit 0ce149bf89
2 changed files with 64 additions and 32 deletions

View File

@ -1,4 +1,4 @@
import axios from 'axios';
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
import type { OffsetOrder, Portfolio } from '../types';
import { config } from '../utils/config';
@ -44,9 +44,13 @@ const DEFAULT_PORTFOLIO: Portfolio = {
// Create API client with error handling, timeout, and retry logic
const createApiClient = () => {
if (!config.wrenApiKey) {
console.error('Wren API token is missing! Token:', config.wrenApiKey);
console.error('Environment:', window?.env ? JSON.stringify(window.env) : 'No window.env available');
throw new Error('Wren API token is not configured');
}
console.log('[wrenClient] Creating API client with key:', config.wrenApiKey ? '********' + config.wrenApiKey.slice(-4) : 'MISSING');
const client = axios.create({
baseURL: 'https://api.wren.co/v1',
headers: {
@ -54,40 +58,45 @@ const createApiClient = () => {
'Content-Type': 'application/json'
},
timeout: 10000, // 10 second timeout
validateStatus: (status) => status >= 200 && status < 500, // Handle 4xx errors gracefully
validateStatus: (status: number) => status >= 200 && status < 500, // Handle 4xx errors gracefully
});
// Add request interceptor for logging
client.interceptors.request.use(
(config) => {
if (!config.headers.Authorization) {
(config: AxiosRequestConfig) => {
if (!config.headers?.Authorization) {
throw new Error('API token is required');
}
console.log('[wrenClient] Making API request to:', config.url);
return config;
},
(error) => {
console.error('Request configuration error:', error.message);
(error: Error) => {
console.error('[wrenClient] Request configuration error:', error.message);
return Promise.reject(error);
}
);
// Add response interceptor for error handling
client.interceptors.response.use(
(response) => response,
(error) => {
(response: AxiosResponse) => {
console.log('[wrenClient] Received API response:', response.status);
return response;
},
(error: unknown) => {
if (axios.isAxiosError(error)) {
if (error.code === 'ECONNABORTED') {
console.warn('Request timeout, using fallback data');
console.warn('[wrenClient] Request timeout, using fallback data');
return Promise.resolve({ data: { portfolios: [DEFAULT_PORTFOLIO] } });
}
if (!error.response) {
console.warn('Network error, using fallback data');
console.warn('[wrenClient] Network error, using fallback data');
return Promise.resolve({ data: { portfolios: [DEFAULT_PORTFOLIO] } });
}
if (error.response.status === 401) {
console.warn('Authentication failed, using fallback data');
console.warn('[wrenClient] Authentication failed, using fallback data');
return Promise.resolve({ data: { portfolios: [DEFAULT_PORTFOLIO] } });
}
console.error('[wrenClient] API error:', error.response?.status, error.response?.data);
}
return Promise.reject(error);
}
@ -104,24 +113,26 @@ const logError = (error: unknown) => {
message: error.message,
stack: error.stack
};
console.error('API Error:', JSON.stringify(errorInfo, null, 2));
console.error('[wrenClient] API Error:', JSON.stringify(errorInfo, null, 2));
} else {
console.error('Unknown error:', String(error));
console.error('[wrenClient] Unknown error:', String(error));
}
};
export async function getPortfolios(): Promise<Portfolio[]> {
try {
if (!config.wrenApiKey) {
console.warn('No Wren API token configured, using fallback portfolio');
console.warn('[wrenClient] No Wren API token configured, using fallback portfolio');
return [DEFAULT_PORTFOLIO];
}
console.log('[wrenClient] Getting portfolios with token:', config.wrenApiKey ? '********' + config.wrenApiKey.slice(-4) : 'MISSING');
const api = createApiClient();
const response = await api.get('/portfolios');
if (!response.data?.portfolios?.length) {
console.warn('No portfolios returned from API, using fallback');
console.warn('[wrenClient] No portfolios returned from API, using fallback');
return [DEFAULT_PORTFOLIO];
}
@ -151,7 +162,7 @@ export async function getPortfolios(): Promise<Portfolio[]> {
});
} catch (error) {
logError(error);
console.warn('Failed to fetch portfolios from API, using fallback');
console.warn('[wrenClient] Failed to fetch portfolios from API, using fallback');
return [DEFAULT_PORTFOLIO];
}
}
@ -163,9 +174,12 @@ export async function createOffsetOrder(
): Promise<OffsetOrder> {
try {
if (!config.wrenApiKey) {
console.error('[wrenClient] Cannot create order - missing API token');
throw new Error('Carbon offset service is currently unavailable. Please contact support.');
}
console.log(`[wrenClient] Creating offset order: portfolio=${portfolioId}, tons=${tons}`);
const api = createApiClient();
const response = await api.post('/orders', {
portfolio_id: portfolioId,
@ -199,18 +213,31 @@ export async function createOffsetOrder(
createdAt: order.created_at,
dryRun: order.dry_run
};
} catch (error) {
} catch (error: unknown) {
logError(error);
if (axios.isAxiosError(error)) {
if (error.code === 'ECONNABORTED') {
const axiosError = error as AxiosError;
console.error('[wrenClient] Axios error details:', {
status: axiosError.response?.status,
data: axiosError.response?.data,
config: {
url: axiosError.config?.url,
method: axiosError.config?.method,
headers: axiosError.config?.headers ? 'Headers present' : 'No headers',
baseURL: axiosError.config?.baseURL
}
});
if (axiosError.code === 'ECONNABORTED') {
throw new Error('Request timed out. Please try again.');
}
if (!error.response) {
if (!axiosError.response) {
throw new Error('Network error. Please check your connection and try again.');
}
if (error.response.status === 401) {
throw new Error('Carbon offset service is currently unavailable. Please try again later or contact support.');
if (axiosError.response.status === 401) {
throw new Error('Carbon offset service authentication failed. Please check your API token.');
}
}

View File

@ -5,19 +5,23 @@ interface Config {
isProduction: boolean;
}
// First try to get from window.env (for containerized environment)
// Fall back to Vite's import.meta.env (for development)
// Get environment variables either from window.env (for Docker) or import.meta.env (for development)
const getEnv = (key: string): string => {
// Remove VITE_ prefix when checking window.env
const windowKey = key.replace('VITE_', '');
// Extract the name without VITE_ prefix
const varName = key.replace('VITE_', '');
// Check if window.env exists and has the variable
if (typeof window !== 'undefined' && window.env && window.env[windowKey]) {
return window.env[windowKey] || '';
// First check if window.env exists and has the variable
if (typeof window !== 'undefined' && window.env) {
// In Docker, the env.sh script has already removed the VITE_ prefix
const envValue = window.env[varName];
if (envValue) {
console.log(`Using ${varName} from window.env`);
return envValue;
}
}
// Fall back to Vite's import.meta.env
// Ensure we're only returning string values
// Fall back to Vite's import.meta.env (for development)
// Here we need the full name with VITE_ prefix
const value = import.meta.env[key];
return typeof value === 'string' ? value : '';
};
@ -37,7 +41,8 @@ export const config: Config = {
// Validate required environment variables
if (!config.wrenApiKey) {
console.error('Missing required environment variable: VITE_WREN_API_TOKEN');
console.error('Missing required environment variable: WREN_API_TOKEN');
console.error('Current environment:', window?.env ? JSON.stringify(window.env) : 'No window.env available');
}
// Log config in development