From b0f770e50af6db9563be747d9d7e34f88267bd18 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 22 Aug 2025 20:08:28 +0200 Subject: [PATCH 1/4] Feature/improve error handling in data providers (#5387) * Improve error handling * Update changelog --- CHANGELOG.md | 4 +++ .../coingecko/coingecko.service.ts | 10 +++--- .../eod-historical-data.service.ts | 8 +++-- .../financial-modeling-prep.service.ts | 10 +++--- .../ghostfolio/ghostfolio.service.ts | 34 ++++++++++++------- .../rapid-api/rapid-api.service.ts | 2 +- 6 files changed, 43 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 196fb1853..0344f8d47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Extended the data providers management of the admin control panel by every data provider in use +### Changed + +- Improved the error handling in data providers + ## 2.192.0 - 2025-08-21 ### Added diff --git a/apps/api/src/services/data-provider/coingecko/coingecko.service.ts b/apps/api/src/services/data-provider/coingecko/coingecko.service.ts index 7776ff46c..8a3adb507 100644 --- a/apps/api/src/services/data-provider/coingecko/coingecko.service.ts +++ b/apps/api/src/services/data-provider/coingecko/coingecko.service.ts @@ -78,7 +78,7 @@ export class CoinGeckoService implements DataProviderInterface { } catch (error) { let message = error; - if (error?.name === 'AbortError') { + if (['AbortError', 'TimeoutError'].includes(error?.name)) { message = `RequestError: The operation to get the asset profile for ${symbol} was aborted because the request to the data provider took more than ${( this.configurationService.get('REQUEST_TIMEOUT') / 1000 ).toFixed(3)} seconds`; @@ -196,8 +196,10 @@ export class CoinGeckoService implements DataProviderInterface { } catch (error) { let message = error; - if (error?.name === 'AbortError') { - message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${( + if (['AbortError', 'TimeoutError'].includes(error?.name)) { + message = `RequestError: The operation to get the quotes for ${symbols.join( + ', ' + )} was aborted because the request to the data provider took more than ${( this.configurationService.get('REQUEST_TIMEOUT') / 1000 ).toFixed(3)} seconds`; } @@ -237,7 +239,7 @@ export class CoinGeckoService implements DataProviderInterface { } catch (error) { let message = error; - if (error?.name === 'AbortError') { + if (['AbortError', 'TimeoutError'].includes(error?.name)) { message = `RequestError: The operation to search for ${query} was aborted because the request to the data provider took more than ${( this.configurationService.get('REQUEST_TIMEOUT') / 1000 ).toFixed(3)} seconds`; diff --git a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts index ddb94bb1a..d06071ac3 100644 --- a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts +++ b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts @@ -282,8 +282,10 @@ export class EodHistoricalDataService implements DataProviderInterface { } catch (error) { let message = error; - if (error?.name === 'AbortError') { - message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${( + if (['AbortError', 'TimeoutError'].includes(error?.name)) { + message = `RequestError: The operation to get the quotes for ${symbols.join( + ', ' + )} was aborted because the request to the data provider took more than ${( this.configurationService.get('REQUEST_TIMEOUT') / 1000 ).toFixed(3)} seconds`; } @@ -426,7 +428,7 @@ export class EodHistoricalDataService implements DataProviderInterface { } catch (error) { let message = error; - if (error?.name === 'AbortError') { + if (['AbortError', 'TimeoutError'].includes(error?.name)) { message = `RequestError: The operation to search for ${aQuery} was aborted because the request to the data provider took more than ${( this.configurationService.get('REQUEST_TIMEOUT') / 1000 ).toFixed(3)} seconds`; diff --git a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts index 2dcb689a7..ed2aa5f25 100644 --- a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts +++ b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts @@ -202,7 +202,7 @@ export class FinancialModelingPrepService implements DataProviderInterface { } catch (error) { let message = error; - if (error?.name === 'AbortError') { + if (['AbortError', 'TimeoutError'].includes(error?.name)) { message = `RequestError: The operation to get the asset profile for ${symbol} was aborted because the request to the data provider took more than ${( requestTimeout / 1000 ).toFixed(3)} seconds`; @@ -392,8 +392,10 @@ export class FinancialModelingPrepService implements DataProviderInterface { } catch (error) { let message = error; - if (error?.name === 'AbortError') { - message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${( + if (['AbortError', 'TimeoutError'].includes(error?.name)) { + message = `RequestError: The operation to get the quotes for ${symbols.join( + ', ' + )} was aborted because the request to the data provider took more than ${( requestTimeout / 1000 ).toFixed(3)} seconds`; } @@ -469,7 +471,7 @@ export class FinancialModelingPrepService implements DataProviderInterface { } catch (error) { let message = error; - if (error?.name === 'AbortError') { + if (['AbortError', 'TimeoutError'].includes(error?.name)) { message = `RequestError: The operation to search for ${query} was aborted because the request to the data provider took more than ${( this.configurationService.get('REQUEST_TIMEOUT') / 1000 ).toFixed(3)} seconds`; diff --git a/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts b/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts index 3fd9e1bea..48ba42bd4 100644 --- a/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts +++ b/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts @@ -68,14 +68,16 @@ export class GhostfolioService implements DataProviderInterface { } catch (error) { let message = error; - if (error.name === 'AbortError') { - message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${( + if (['AbortError', 'TimeoutError'].includes(error?.name)) { + message = `RequestError: The operation to get the asset profile for ${symbol} was aborted because the request to the data provider took more than ${( requestTimeout / 1000 ).toFixed(3)} seconds`; - } else if (error.response?.statusCode === StatusCodes.TOO_MANY_REQUESTS) { + } else if ( + error?.response?.statusCode === StatusCodes.TOO_MANY_REQUESTS + ) { message = 'RequestError: The daily request limit has been exceeded'; - } else if (error.response?.statusCode === StatusCodes.UNAUTHORIZED) { - if (!error.request?.options?.headers?.authorization?.includes('-')) { + } else if (error?.response?.statusCode === StatusCodes.UNAUTHORIZED) { + if (!error?.request?.options?.headers?.authorization?.includes('-')) { message = 'RequestError: The provided API key is invalid. Please update it in the Settings section of the Admin Control panel.'; } else { @@ -229,14 +231,18 @@ export class GhostfolioService implements DataProviderInterface { } catch (error) { let message = error; - if (error.name === 'AbortError') { - message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${( + if (['AbortError', 'TimeoutError'].includes(error?.name)) { + message = `RequestError: The operation to get the quotes for ${symbols.join( + ', ' + )} was aborted because the request to the data provider took more than ${( requestTimeout / 1000 ).toFixed(3)} seconds`; - } else if (error.response?.statusCode === StatusCodes.TOO_MANY_REQUESTS) { + } else if ( + error?.response?.statusCode === StatusCodes.TOO_MANY_REQUESTS + ) { message = 'RequestError: The daily request limit has been exceeded'; - } else if (error.response?.statusCode === StatusCodes.UNAUTHORIZED) { - if (!error.request?.options?.headers?.authorization?.includes('-')) { + } else if (error?.response?.statusCode === StatusCodes.UNAUTHORIZED) { + if (!error?.request?.options?.headers?.authorization?.includes('-')) { message = 'RequestError: The provided API key is invalid. Please update it in the Settings section of the Admin Control panel.'; } else { @@ -272,14 +278,16 @@ export class GhostfolioService implements DataProviderInterface { } catch (error) { let message = error; - if (error.name === 'AbortError') { + if (['AbortError', 'TimeoutError'].includes(error?.name)) { message = `RequestError: The operation to search for ${query} was aborted because the request to the data provider took more than ${( requestTimeout / 1000 ).toFixed(3)} seconds`; - } else if (error.response?.statusCode === StatusCodes.TOO_MANY_REQUESTS) { + } else if ( + error?.response?.statusCode === StatusCodes.TOO_MANY_REQUESTS + ) { message = 'RequestError: The daily request limit has been exceeded'; } else if (error.response?.statusCode === StatusCodes.UNAUTHORIZED) { - if (!error.request?.options?.headers?.authorization?.includes('-')) { + if (!error?.request?.options?.headers?.authorization?.includes('-')) { message = 'RequestError: The provided API key is invalid. Please update it in the Settings section of the Admin Control panel.'; } else { diff --git a/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts b/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts index 5675f1eb0..62b3ed71c 100644 --- a/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts +++ b/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts @@ -165,7 +165,7 @@ export class RapidApiService implements DataProviderInterface { } catch (error) { let message = error; - if (error?.name === 'AbortError') { + if (['AbortError', 'TimeoutError'].includes(error?.name)) { message = `RequestError: The operation was aborted because the request to the data provider took more than ${( this.configurationService.get('REQUEST_TIMEOUT') / 1000 ).toFixed(3)} seconds`; From b5649654b2d665e2a7a6e56f4bc2ef8895c7365f Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 22 Aug 2025 20:09:23 +0200 Subject: [PATCH 2/4] Feature/add filter by data source for asset profiles in admin control panel (#5385) * Add filter by data source * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/admin/admin.controller.ts | 2 + apps/api/src/app/admin/admin.service.ts | 16 +++-- .../admin-market-data.component.ts | 69 ++++++++++--------- libs/ui/src/lib/i18n.ts | 1 + 5 files changed, 52 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0344f8d47..61b8fcc72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added a filter by data source for the asset profiles in the admin control panel - Extended the data providers management of the admin control panel by every data provider in use ### Changed diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index e473813e9..27cc088d1 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -197,6 +197,7 @@ export class AdminController { @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async getMarketData( @Query('assetSubClasses') filterByAssetSubClasses?: string, + @Query('dataSource') filterByDataSource?: string, @Query('presetId') presetId?: MarketDataPreset, @Query('query') filterBySearchQuery?: string, @Query('skip') skip?: number, @@ -206,6 +207,7 @@ export class AdminController { ): Promise { const filters = this.apiService.buildFiltersFromQueryParams({ filterByAssetSubClasses, + filterByDataSource, filterBySearchQuery }); diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index bce603289..d07e74013 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -218,12 +218,12 @@ export class AdminService { return type === 'SEARCH_QUERY'; })?.id; - const { ASSET_SUB_CLASS: filtersByAssetSubClass } = groupBy( - filters, - ({ type }) => { - return type; - } - ); + const { + ASSET_SUB_CLASS: filtersByAssetSubClass, + DATA_SOURCE: filtersByDataSource + } = groupBy(filters, ({ type }) => { + return type; + }); const marketDataItems = await this.prismaService.marketData.groupBy({ _count: true, @@ -234,6 +234,10 @@ export class AdminService { where.assetSubClass = AssetSubClass[filtersByAssetSubClass[0].id]; } + if (filtersByDataSource) { + where.dataSource = DataSource[filtersByDataSource[0].id]; + } + if (searchQuery) { where.OR = [ { id: { mode: 'insensitive', startsWith: searchQuery } }, diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts index 6a809a10f..4e410c3a0 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts @@ -103,39 +103,46 @@ export class GfAdminMarketDataComponent @ViewChild(MatSort) sort: MatSort; public activeFilters: Filter[] = []; - public allFilters: Filter[] = Object.keys(AssetSubClass) - .filter((assetSubClass) => { - return assetSubClass !== 'CASH'; - }) - .map((assetSubClass) => { + public allFilters: Filter[] = [ + ...Object.keys(AssetSubClass) + .filter((assetSubClass) => { + return assetSubClass !== 'CASH'; + }) + .map((assetSubClass) => { + return { + id: assetSubClass.toString(), + label: translate(assetSubClass), + type: 'ASSET_SUB_CLASS' as Filter['type'] + }; + }), + ...Object.keys(DataSource).map((dataSource) => { return { - id: assetSubClass.toString(), - label: translate(assetSubClass), - type: 'ASSET_SUB_CLASS' as Filter['type'] + id: dataSource.toString(), + label: dataSource, + type: 'DATA_SOURCE' as Filter['type'] }; - }) - .concat([ - { - id: 'BENCHMARKS', - label: $localize`Benchmarks`, - type: 'PRESET_ID' as Filter['type'] - }, - { - id: 'CURRENCIES', - label: $localize`Currencies`, - type: 'PRESET_ID' as Filter['type'] - }, - { - id: 'ETF_WITHOUT_COUNTRIES', - label: $localize`ETFs without Countries`, - type: 'PRESET_ID' as Filter['type'] - }, - { - id: 'ETF_WITHOUT_SECTORS', - label: $localize`ETFs without Sectors`, - type: 'PRESET_ID' as Filter['type'] - } - ]); + }), + { + id: 'BENCHMARKS', + label: $localize`Benchmarks`, + type: 'PRESET_ID' as Filter['type'] + }, + { + id: 'CURRENCIES', + label: $localize`Currencies`, + type: 'PRESET_ID' as Filter['type'] + }, + { + id: 'ETF_WITHOUT_COUNTRIES', + label: $localize`ETFs without Countries`, + type: 'PRESET_ID' as Filter['type'] + }, + { + id: 'ETF_WITHOUT_SECTORS', + label: $localize`ETFs without Sectors`, + type: 'PRESET_ID' as Filter['type'] + } + ]; public benchmarks: Partial[]; public currentDataSource: DataSource; public currentSymbol: string; diff --git a/libs/ui/src/lib/i18n.ts b/libs/ui/src/lib/i18n.ts index 8007dc53e..e17cc6771 100644 --- a/libs/ui/src/lib/i18n.ts +++ b/libs/ui/src/lib/i18n.ts @@ -12,6 +12,7 @@ const locales = { DATA_IMPORT_AND_EXPORT_TOOLTIP_BASIC: $localize`Switch to Ghostfolio Premium or Ghostfolio Open Source easily`, DATA_IMPORT_AND_EXPORT_TOOLTIP_OSS: $localize`Switch to Ghostfolio Premium easily`, DATA_IMPORT_AND_EXPORT_TOOLTIP_PREMIUM: $localize`Switch to Ghostfolio Open Source or Ghostfolio Basic easily`, + DATA_SOURCE: $localize`Data Source`, EMERGENCY_FUND: $localize`Emergency Fund`, EXCLUDE_FROM_ANALYSIS: $localize`Exclude from Analysis`, Global: $localize`Global`, From 2fa2d38f9ef54c74c98b78cb3ebcafd97aaa95ed Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 22 Aug 2025 20:09:46 +0200 Subject: [PATCH 3/4] Feature/upgrade yahoo-finance2 to version 3.6.4 (#5389) * Upgrade yahoo-finance2 to version 3.6.4 * Update changelog --- CHANGELOG.md | 1 + package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61b8fcc72..854105676 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improved the error handling in data providers +- Upgraded `yahoo-finance2` from version `3.4.1` to `3.6.4` ## 2.192.0 - 2025-08-21 diff --git a/package-lock.json b/package-lock.json index 690ab659c..9b105cb2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -93,7 +93,7 @@ "svgmap": "2.12.2", "twitter-api-v2": "1.23.0", "uuid": "11.1.0", - "yahoo-finance2": "3.4.1", + "yahoo-finance2": "3.6.4", "zone.js": "0.15.1" }, "devDependencies": { @@ -40601,9 +40601,9 @@ } }, "node_modules/yahoo-finance2": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/yahoo-finance2/-/yahoo-finance2-3.4.1.tgz", - "integrity": "sha512-L8Ubmdsn6f+uJEuEDUUHR5n95TFcGkMiMkV0phmvPONFekAn1vWzsEzGfIDG2ODR7aYBB+aURdQg7a3HX2iUHA==", + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/yahoo-finance2/-/yahoo-finance2-3.6.4.tgz", + "integrity": "sha512-IoMU8Hb4BEaNPVnamZjRBuorTGDbaaiV/tM/m3KI8dzwrR6BGmeuT40OX+5IqRiSkMlD8g0kAwGi9E4bY3rLvg==", "license": "MIT", "dependencies": { "@deno/shim-deno": "~0.18.0", diff --git a/package.json b/package.json index 34f975ff6..3005b38ae 100644 --- a/package.json +++ b/package.json @@ -139,7 +139,7 @@ "svgmap": "2.12.2", "twitter-api-v2": "1.23.0", "uuid": "11.1.0", - "yahoo-finance2": "3.4.1", + "yahoo-finance2": "3.6.4", "zone.js": "0.15.1" }, "devDependencies": { From d545b81deb3489abf3889b7e800c85148c8ac8b5 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 22 Aug 2025 20:12:12 +0200 Subject: [PATCH 4/4] Release 2.193.0 (#5392) --- CHANGELOG.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 854105676..251a51d93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 2.193.0 - 2025-08-22 ### Added diff --git a/package-lock.json b/package-lock.json index 9b105cb2c..a86aa3c10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "2.192.0", + "version": "2.193.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "2.192.0", + "version": "2.193.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/package.json b/package.json index 3005b38ae..48a79812d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.192.0", + "version": "2.193.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio",