How-to: Build a KPI engine


Impac Kpis & Alerting

Impac KPI's (key performance indicators) are set of calculations of which Users can set targets for and receive in-app Alerts & Emails when these targets have been met.

The overview architecture of the KPI & Alerting

  1. The frontend queries Impac API to 'discover' (#index) the available kpis.
    1. The available kpis are built by loading all the Kpis::Base.descendants (which is the kpi engine classes), and mapping attributes to a hash by accessing class methods of the model.
    2. Some kpi engine class methods are required, other are optional. More on this further below, in the building a kpi engine section.
    3. Example response:

        // GET api/v2/impac/kpis
        // -------------------------------
        {
          "kpis": [
            {
              "name": "Overweighted Assets",
              // The endpoint is a path to the kpi engine which contains the calculations. This is
              // used to dynamically select which Ruby class to load.
              "endpoint": "accounting/asset_overweight",
              // Watchables are the available calculations for this kpi. For example, asset_overweight
              // can be calculated by ratio and/or by balance. The zeroth watchable is considered
              // the primary.
              "watchables": [
                "ratio",
                "balance"
              ],
              // Attachables is a list of widgets that are compatible with this kpi. In the
              // "attach kpi onto widget" feature.
              "attachables": [
                "accounts/balance"
              ],
              // Kpis have the ability to have targets set on kpis by watchable (saved in mno-hub).
              // Target placeholders are used to either suggest or force kpi target input
              // values, this is depending on the front-end configuration. These are hardcoded in each
              // kpi engine within the impac api.
              "target_placeholder": {
                "ratio": {
                  // Targets have a mode of either "min" or "max". E.g target is considered triggered
                  // if the calculation value of the watchable "ratio" is greater than 20%.
                  "mode": "max",
                  "value": 20,
                  "unit": "%"
                },
                "balance": {
                  "mode": "max",
                  "value": 50000,
                  "unit": "currency"
                }
              },
              // Depending on the kpi, it may contain a hash of extra_params, like a list of possible
              // accounts to base the calculations from.
              "extra_params": null
            }
          ]
        }


  2. When a User selects a kpi template (impac api #index action response), a post request to MnoHub API is made, saving the kpi.

    1. Note that a saved kpi hash and a kpi template hash are different. When fetching available kpis, the data from the selected available kpi is used to form the kpi. Which is then persisted.
    2. Example response:

        // POST /api/v2/impac/dashboards/:dashboard_id/kpis
        // -------------------------------
        {
          // determines whether the kpi is an impac kpi or a 'local' kpi. Local kpis are kpis saved
          // from external sources.
          source: "impac",
          // endpoint selects which Impac API kpi engine to dynamically load.
          endpoint: "accounting/revenue",
          // The same as the "watchables" from the available kpis hash, but the "element_watched"
          // is saved as the primary watchable for this kpi.
          element_watched: "total",
          // Also "watchables", but an additional list of watchables that are to be saved. These
          // are considered secondary watchables, used for displaying and triggering alerts from
          // multiple targets by watchables. To enable `multiple_watchables_mode` on impac-angular
          // see, developing for multiple watchables section.
          extra_watchables: [
            "evolution"
          ],
          // User defined targets by watchable. Used to calculate whether alerts should be sent, or
          // simply to display that the kpis target is met.
          targets: {
            "total": [
              { "max": 500 }
            ],
            "evolution": [
              { "min": 70 }
            ]
          },
          // extra_params used for kpi settings, e.g a selected bank account.
          extra_params: {
            account: 'some-account-uid'
          },
          // stores metadata such as currency for the selected kpi.
          metadata: {
            currency: "AUD"
          }
        }


  3. Once a kpi template has been added data is fetched from Impac API for the kpi.
    1. This request is a intented to be a GET request with inline-parameters. For parameters see Impac! API docs.
    2. Example response:

      // GET /api/v2/kpis/accounting/revenue/total
      // -------------------------------
      {
        // results calculated by the kpi engine for each watchable.
        "calculation": {
          "total": {
            // Represents the triggered or not triggered targets for this watchable.
            "triggers": [false],
            "unit": "currency",
            "value": 6700
          },
          "evolution": {
            ...
          }
        },
        // Configurations are where important parameters should be kept. For example, when asking
        // impac to calculate a kpis data and determine whether targets have been met, we thought
        // it would be appropriate to include the targets in the impac API #show response.
        "configuration": {
          "targets": { ... }
        },
        // Layout configurations are used to dynamically configure the appearance and phrasing
        // of kpis.
        "layout": {
          "icon": {
            "type": "font-awesome",
            "value": "fa-tag"
          },
          "target_placeholder": {
            // same structure as the impac api index response above.
            ...
          },
          "text": {
            caption: "Revenue from 2016-07-01",
            emphasis: "below 77 AUD",
            alert: "Alert me when I'm below"
          }
          // A triggered state, determine by whether any targets by watchable have been triggered.
          "triggered": true
        }
      }
    3. The response is then merged with the kpi response from MnoHub, and the final result is the javascript Object bound to the front-end angular model.

  4. When app provider sends data through connec that the User is subscribed to, it triggers a webhook event which calls Impac API which enqueues a batch_notification_worker for burst request batching, and then a channel_notification_worker which queues up the appropriate alerts_dispatcher worker (in_app or email) if any targets for any of the watchables have been triggered.

Building a KPI engine

KPI engines are an an interface where some functions need to be implimented to "create a KPI". KPI engines must inherit from Kpis::Base, where most of the core logic lives.

Below is an example of a simple engine, only using the required overridable methods (there are optionally overridable methods for more customisation).

# models/kpis/accounting/turnover
class Kpis::Accounting::Turnover < Kpis::Base

  def self.watchables
    %w(total ratio)
  end

  def self.kpi_name
    'Turnover'
  end

  def self.target_placeholder(watchable)
    case watchable
    when 'total'
      # when working with currency, use the string 'currency' as it will be replace in the front-end with the selected dashboard currency.
      {mode: 'max', value: 50000, unit: 'currency'}
    when 'ratio'
      {mode: 'min', value: 50, unit: '%'}
    end
  end

  # calculate is not defined on self as it is required to be instantiated before being invoked.
  def calculate

    # Access any instance variables & methods from Kpis::Base to help you create your calculations.
    from_date = @from
    to_date = @to

    # Follow this convention, as the output structure is crucial.
    calculations = @selected_watchables.map do |watchable|
      case watchable
      when "total"
        # Apply real calculations to gather a value.
        value = 1000
        # Method in Kpis::Base
        unit = currency

      when "evolution"
        value = 50
        unit = "%"

      end
      { watchable: watchable, data: { value: real_value, unit: unit } }
    end

    return prepare_response(calculations)
  end
end

A table describing each of the overridable methods
Overridable MethodDefinition
.calculateREQUIRED calls Connec!, retrieves the source data, and calculates the KPI "value", that will be compared against the target defined by the user to determine wether the KPI is triggered
#watchablesREQUIRED stands for different "variations" of the KPI calculation. For example, the KPI "Debtor Days" can watch the "max" watchable (= will calculate the maximum number of days an invoice is due) or the watchable "average" (= average number of days for all the due invoices). In the case of a KPI attached to a widget, you should only define one watchable per KPI: it can be 'amount', 'ratio'...
#target_placeholderREQUIRED ...is proposing a default target for the KPI. The frontend is using the target_placeholder to force the "mode" of the KPIs that are in the KPs bar. Indeed, you cannot choose if your target has to be a min (="keep the KPI over ...") or a max(="keep the KPI above ...") for KPIs attached to the dashboard. It is not the case for KPIs that are attached to a widget, where you can in fact choose between "keep above" of "keep below"
#attachableshelps to determine what Impac! element the KPI can be attached to: the dashboard, one or several particular widgets
#possible_extra_paramsused when some KPIs need to pass more information than just a watchable (say an account id for example)
#kpi_namename of the KPI as displayed in the KPIs bar
#iconicon displayed in the KPIs bar for this KPI
#captionwhat will be displayed on the first line of the KPI in the KPIs bar
#emphasis_triggeredwhat will be displayed on the second (bold) line of the KPI in the KPIs bar if the KPI is triggered
#emphasis_okwhat will be displayed on the second (bold) line of the KPI in the KPIs bar if the KPI is NOT triggered