Overview
Reporting is extended by adding vocabulary, not by writing report classes. Register a metric or a dimension in an initializer and it becomes available everywhere at once: the Reports builder, saved reports, CSV export and the schema endpoint. Read how reporting works first — this guide assumes the query contract and the metric/dimension split.Add a metric
A metric is an aggregate over one registered base —:orders, :line_items,
:payments or :stock_movements:
config/initializers/spree.rb
GET /reporting/schema, in
the builder’s metric list, and can be saved into a report.
Give it a label so merchants do not read the raw name:
config/locales/en.yml
Derived metrics
A ratio is computed after aggregation, so it stays correct for both rows and totals — which a plainAVG() would not:
Add a dimension
A dimension groups and filters. The simplest kind is a column on the base table:A dimension that identifies records
When the keys are IDs rather than plain values, declare four things together: how to reach the table, how to resolve a filter value, how to render the key, and who may see it.joinsis applied only to the grouped query, so the Total row stays the store’s real figure even when the join fans out.resolvelets a filter accept the prefixed IDs clients already hold. Scope it through the store, so an ID from another store is a 404.hydrateis batched once per dimension per response, not per row.subjectandkey_scopeare mandatory together. Staff need:readon the subject; API keys need the scope. Omittingkey_scoperaises at registration rather than shipping a member that only guards one axis.
Status-like dimensions
Publish the values so the builder can offer checkboxes instead of a text box:spree.reporting.values.<dimension>.<value>; they are translated server-side,
so a plugin needs no dashboard locale edit.
Verify it
Query the contract directly:spec/lib/my_extension/reporting_spec.rb
Add a counter
A counter is a point-in-time number for the home screen’s Operations card: no time range, no currency, no base. It takes the store and the channel the merchant is looking at and returns an integer.admin.pages.home.operations.counters.orders_on_hold.label, and optionally
.description, to your dashboard locale files; a counter with no entry falls
back to its humanized key, so it reads sensibly before you translate it.
Pass nav: with a sidebar entry’s key to badge that entry with this count.
Declare link only when the list filter shows exactly the rows counted — the
dashboard resolves resource to one of its lists (orders, returns,
exchanges, claims, products, inventory) and appends the channel filter
itself for order lists.
Overriding a built-in member
Registration refuses a duplicate name unless you say so explicitly:What you cannot do yet
These are limits of the compiler, not of registration:- The
%{table}map is fixed. Registering a base over your own table needs an entry added to the adapter’s interpolation map — the one place a new table still has to be named in core. - A dimension’s column must be a plain identifier. Computed groupings
such as
SUBSTR(email, ...)are refused; add a real column, or a database view, if you need one. - Custom
lookupvalues fall back to an ID input in the builder, since the dashboard maps known lookups to pickers.
currency argument is nil when the query contains no money
metric, and a base must then not filter by it — a count restricted to one
currency is a wrong answer, not a narrower one. Guard the filter rather than
passing nil into where, which would match nothing:
Related
- Reporting — the concepts
- Permissions — scopes and subjects
- Imports and exports — row-level CSV

