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 type { OffsetOrder, Portfolio } from '../types';
import { config } from '../utils/config'; import { config } from '../utils/config';
@ -44,9 +44,13 @@ const DEFAULT_PORTFOLIO: Portfolio = {
// Create API client with error handling, timeout, and retry logic // Create API client with error handling, timeout, and retry logic
const createApiClient = () => { const createApiClient = () => {
if (!config.wrenApiKey) { 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'); 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({ const client = axios.create({
baseURL: 'https://api.wren.co/v1', baseURL: 'https://api.wren.co/v1',
headers: { headers: {
@ -54,40 +58,45 @@ const createApiClient = () => {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
timeout: 10000, // 10 second timeout 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 // Add request interceptor for logging
client.interceptors.request.use( client.interceptors.request.use(
(config) => { (config: AxiosRequestConfig) => {
if (!config.headers.Authorization) { if (!config.headers?.Authorization) {
throw new Error('API token is required'); throw new Error('API token is required');
} }
console.log('[wrenClient] Making API request to:', config.url);
return config; return config;
}, },
(error) => { (error: Error) => {
console.error('Request configuration error:', error.message); console.error('[wrenClient] Request configuration error:', error.message);
return Promise.reject(error); return Promise.reject(error);
} }
); );
// Add response interceptor for error handling // Add response interceptor for error handling
client.interceptors.response.use( client.interceptors.response.use(
(response) => response, (response: AxiosResponse) => {
(error) => { console.log('[wrenClient] Received API response:', response.status);
return response;
},
(error: unknown) => {
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
if (error.code === 'ECONNABORTED') { 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] } }); return Promise.resolve({ data: { portfolios: [DEFAULT_PORTFOLIO] } });
} }
if (!error.response) { 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] } }); return Promise.resolve({ data: { portfolios: [DEFAULT_PORTFOLIO] } });
} }
if (error.response.status === 401) { 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] } }); return Promise.resolve({ data: { portfolios: [DEFAULT_PORTFOLIO] } });
} }
console.error('[wrenClient] API error:', error.response?.status, error.response?.data);
} }
return Promise.reject(error); return Promise.reject(error);
} }
@ -104,24 +113,26 @@ const logError = (error: unknown) => {
message: error.message, message: error.message,
stack: error.stack stack: error.stack
}; };
console.error('API Error:', JSON.stringify(errorInfo, null, 2)); console.error('[wrenClient] API Error:', JSON.stringify(errorInfo, null, 2));
} else { } else {
console.error('Unknown error:', String(error)); console.error('[wrenClient] Unknown error:', String(error));
} }
}; };
export async function getPortfolios(): Promise<Portfolio[]> { export async function getPortfolios(): Promise<Portfolio[]> {
try { try {
if (!config.wrenApiKey) { 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]; return [DEFAULT_PORTFOLIO];
} }
console.log('[wrenClient] Getting portfolios with token:', config.wrenApiKey ? '********' + config.wrenApiKey.slice(-4) : 'MISSING');
const api = createApiClient(); const api = createApiClient();
const response = await api.get('/portfolios'); const response = await api.get('/portfolios');
if (!response.data?.portfolios?.length) { 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]; return [DEFAULT_PORTFOLIO];
} }
@ -151,7 +162,7 @@ export async function getPortfolios(): Promise<Portfolio[]> {
}); });
} catch (error) { } catch (error) {
logError(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]; return [DEFAULT_PORTFOLIO];
} }
} }
@ -163,9 +174,12 @@ export async function createOffsetOrder(
): Promise<OffsetOrder> { ): Promise<OffsetOrder> {
try { try {
if (!config.wrenApiKey) { if (!config.wrenApiKey) {
console.error('[wrenClient] Cannot create order - missing API token');
throw new Error('Carbon offset service is currently unavailable. Please contact support.'); 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 api = createApiClient();
const response = await api.post('/orders', { const response = await api.post('/orders', {
portfolio_id: portfolioId, portfolio_id: portfolioId,
@ -199,21 +213,34 @@ export async function createOffsetOrder(
createdAt: order.created_at, createdAt: order.created_at,
dryRun: order.dry_run dryRun: order.dry_run
}; };
} catch (error) { } catch (error: unknown) {
logError(error); logError(error);
if (axios.isAxiosError(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.'); 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.'); throw new Error('Network error. Please check your connection and try again.');
} }
if (error.response.status === 401) { if (axiosError.response.status === 401) {
throw new Error('Carbon offset service is currently unavailable. Please try again later or contact support.'); throw new Error('Carbon offset service authentication failed. Please check your API token.');
} }
} }
throw new Error('Failed to create offset order. Please try again.'); throw new Error('Failed to create offset order. Please try again.');
} }
} }

View File

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