Account Issue Model and Resolution Flow + Troubleshooting guides #1090

Merged
zachgoll merged 8 commits from 1049-add-default-troubleshooting-template-for-missing-exchange-rates into main 2024-08-17 00:13:48 +08:00
zachgoll commented 2024-08-15 02:36:52 +08:00 (Migrated from github.com)

After revisiting the existing "guides" feature for resolving common account data issues, this PR replaces those with a more integrated approach to detecting issues, showing them to the user, and giving them a highly contextual guide on how to solve the data issue themselves. For example, we might recognize that exchange rates are not being fetched correctly, so we show the user a contextual guide that allows them to set their exchange rate provider API key directly in the app (this is only relevant and enabled for self hosted instances since our future hosted app will have all this configured already):

https://github.com/user-attachments/assets/7cc98c79-9b72-4260-bc17-907bc33b1390

This is important because this app follows a "self service" + "no data providers by default" model. In other words, a self hosted app should run perfectly fine without configuring any data providers. Because of this model (and data provider errors in general), we'll need a robust system for troubleshooting various data and app configuration errors.

Notes

Implementation

After going back and forth with several possible solutions, I ended up with an STI pattern for an Issue model. This is essentially a barebones issue tracking system (somewhat inspired by Sentry, but obviously far less complex).

The broad idea here is that each issue defines its own class, which has a stale? method, accepts some JSONB :data, and defines some validations on that JSON data.

Each issue type knows the exact conditions (based on :data) that need to be met in order to "resolve" itself, which is implemented in the stale? method. For example, we can verify that the user properly configured their Exchange Rate provider:

class Issue::ExchangeRateProviderMissing < Issue
  def stale?
    ExchangeRate.provider_healthy?
  end
end

Or in a more complex validation, we can verify that missing exchange rates are now present:

class Issue::ExchangeRatesMissing < Issue
  store_accessor :data, :from_currency, :to_currency, :dates

  validates :from_currency, :to_currency, :dates, presence: true

  def stale?
    if dates.length == 1
      ExchangeRate.find_rate(from: from_currency, to: to_currency, date: dates.first).present?
    else
      sorted_dates = dates.sort
      rates = ExchangeRate.find_rates(from: from_currency, to: to_currency, start_date: sorted_dates.first, end_date: sorted_dates.last)
      rates.length == dates.length
    end
  end
end

Furthermore, each issue can be attached to any "Issuable", which for now, is just the Account model. For the account model's case, we use the Account::Sync process to facilitate the raising/resolution of issues.

Calling account.resolve_stale_issues will loop through all the current issues, run the stale? method, and if the issue is fixed, automatically resolve it.

Users cannot resolve or dismiss issues because we want it to be blatantly obvious if there is something wrong with an Account. Suppressing problems simply leads to users confused why their aggregated data is incorrect and does more harm than good. We'd rather be a little annoying with these issues than show incorrect results without any indication that something went wrong.

No I18n translations

The issue templates here have lots of text. Eventually, these should be translated to different languages, but for now, it's simply too cumbersome to manage this given the early stage of the project; especially given how subject to change many of these troubleshooting guides are right now.

After revisiting the existing "guides" feature for resolving common account data issues, this PR replaces those with a more integrated approach to detecting issues, showing them to the user, and giving them a highly contextual guide on how to solve the data issue themselves. For example, we might recognize that exchange rates are not being fetched correctly, so we show the user a contextual guide that allows them to set their exchange rate provider API key directly in the app (this is only relevant and enabled for self hosted instances since our future hosted app will have all this configured already): https://github.com/user-attachments/assets/7cc98c79-9b72-4260-bc17-907bc33b1390 This is important because this app follows a "self service" + "no data providers by default" model. In other words, a self hosted app should run perfectly fine without configuring _any_ data providers. Because of this model (and data provider errors in general), we'll need a robust system for troubleshooting various data and app configuration errors. ## Notes ### Implementation After going back and forth with several possible solutions, I ended up with an STI pattern for an `Issue` model. This is essentially a barebones issue tracking system (somewhat inspired by Sentry, but obviously far less complex). The broad idea here is that each issue defines its own class, which has a `stale?` method, accepts some JSONB `:data`, and defines some validations on that JSON data. Each issue type knows the exact conditions (based on `:data`) that need to be met in order to "resolve" itself, which is implemented in the `stale?` method. For example, we can verify that the user properly configured their Exchange Rate provider: ```rb class Issue::ExchangeRateProviderMissing < Issue def stale? ExchangeRate.provider_healthy? end end ``` Or in a more complex validation, we can verify that missing exchange rates are now present: ```rb class Issue::ExchangeRatesMissing < Issue store_accessor :data, :from_currency, :to_currency, :dates validates :from_currency, :to_currency, :dates, presence: true def stale? if dates.length == 1 ExchangeRate.find_rate(from: from_currency, to: to_currency, date: dates.first).present? else sorted_dates = dates.sort rates = ExchangeRate.find_rates(from: from_currency, to: to_currency, start_date: sorted_dates.first, end_date: sorted_dates.last) rates.length == dates.length end end end ``` Furthermore, each issue can be attached to any "Issuable", which for now, is just the `Account` model. For the account model's case, we use the `Account::Sync` process to facilitate the raising/resolution of issues. Calling `account.resolve_stale_issues` will loop through all the current issues, run the `stale?` method, and if the issue is fixed, automatically resolve it. Users cannot resolve or dismiss issues because we want it to be blatantly obvious if there is something wrong with an `Account`. Suppressing problems simply leads to users confused why their aggregated data is incorrect and does more harm than good. We'd rather be a little annoying with these issues than show incorrect results without any indication that something went wrong. ### No I18n translations The issue templates here have lots of text. Eventually, these should be translated to different languages, but for now, it's simply too cumbersome to manage this given the early stage of the project; especially given how subject to change many of these troubleshooting guides are right now.
Sign in to join this conversation.