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
- The frontend queries Impac API to 'discover' (#index) the available kpis.
- 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. - Some kpi engine class methods are required, other are optional. More on this further below, in the building a kpi engine section.
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 } ] }
- The available kpis are built by loading all the
When a User selects a kpi template (impac api #index action response), a post request to MnoHub API is made, saving the kpi.
- 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.
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" } }
- Once a kpi template has been added data is fetched from Impac API for the kpi.
- This request is a intented to be a GET request with inline-parameters. For parameters see Impac! API docs.
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 } }
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.
- 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 achannel_notification_worker
which queues up the appropriatealerts_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 Method | Definition |
---|---|
.calculate | REQUIRED 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 |
#watchables | REQUIRED 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_placeholder | REQUIRED ...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" |
#attachables | helps to determine what Impac! element the KPI can be attached to: the dashboard, one or several particular widgets |
#possible_extra_params | used when some KPIs need to pass more information than just a watchable (say an account id for example) |
#kpi_name | name of the KPI as displayed in the KPIs bar |
#icon | icon displayed in the KPIs bar for this KPI |
#caption | what will be displayed on the first line of the KPI in the KPIs bar |
#emphasis_triggered | what will be displayed on the second (bold) line of the KPI in the KPIs bar if the KPI is triggered |
#emphasis_ok | what will be displayed on the second (bold) line of the KPI in the KPIs bar if the KPI is NOT triggered |