Creating Custom Statistics Providers
June 29, 2026 · 5 min read
Statistics providers are the foundation of medusa-stats. This guide will walk you through creating your own custom provider using the decorator-based API to calculate business-specific metrics.
#Providers
A statistics provider is a class that extends AbstractStatisticsProvider and uses the @StatFn decorator to declare available statistics. Each method can supply new data points based on commerce entities, external APIs, or any other source of information.
#Basic Provider Structure
Here's the minimal structure of a statistics provider:
import { ModuleProvider } from "@medusajs/framework/utils"
import {
AbstractStatisticsProvider,
StatFn,
type StatCalculationInput,
type StatisticResult,
} from "medusa-stats"
import { z } from "zod"
const totalOrdersSchema = z.object({
status: z.enum(["all", "completed", "pending", "canceled"]).default("completed"),
})
class MyStatisticsProvider extends AbstractStatisticsProvider {
static identifier = "my-statistics"
@StatFn("total_orders", {
schema: totalOrdersSchema,
})
async totalOrders({ parameters, periodStart, periodEnd, interval }: StatCalculationInput): Promise<StatisticResult> {
/* ... */
}
}
export default ModuleProvider("statistics", {
services: [MyStatisticsProvider],
})
#Defining Schemas
Each statistic is paired with a Zod schema defining its parameters. Schemas live outside the class as standalone z.object() definitions:
import { z } from "zod"
const averageSaleValueSchema = z.object({
currency_code: z.string().optional(),
include_tax: z.boolean().default(true),
})
const topProductsSchema = z.object({
limit: z.number().min(1).max(100).default(20),
metric: z.enum(["quantity", "revenue"]).default("quantity"),
})
// For statistics with no parameters, use an empty schema:
const emptySchema = z.object({})
#The @StatFn Decorator
The @StatFn decorator registers a method as an available statistic. It takes two arguments:
| Argument | Type | Description |
|---|---|---|
identifier | string | Unique ID for the statistic (e.g. "total_orders") |
options | object | Configuration with schema and dimension |
The options.dimension field determines how the statistic is visualized:
"time"— time-series data (line/area/bar charts over time)"category"— categorical breakdown (pie charts, bar charts)
Every @StatFn-decorated method receives this input:
interface StatCalculationInput {
parameters: Record<string, any>
periodStart: Date | string
periodEnd: Date | string
interval: number // interval in seconds
}
Your method must return a value matching this type:
interface StatisticResult {
value: TimeSeriesPoint[] | CategoryPoint[] | any
metadata?: Record<string, any>
}
interface TimeSeriesPoint {
x: string
value: number
}
interface CategoryPoint {
x: string
value: number
}
#Helper Functions
#createQueryTimeSeries()
The recommended approach for time-series statistics. It queries data via this.query and automatically buckets results by time intervals:
const timeSeries = await createQueryTimeSeries(this.query, input, {
entity: "order",
fields: ["id", "created_at", "total"],
filters: { currency_code: "usd" },
}, sum("total"))
| Parameter | Description |
|---|---|
this.query | The module's query service |
input | The full StatCalculationInput (spread with ...input) |
config.entity | The entity name to query (e.g. "order", "cart") |
config.fields | Array of fields to retrieve |
config.filters | Optional filters to apply |
aggregator | Accumulator function: count(), sum("field"), average("field"), or a custom function |
#createTimeSeries()
Use this when you've already fetched data yourself and need to bucket it:
const { data: orders } = await this.query.graph({ /* ... */ })
const timeSeries = createTimeSeries(
orders,
periodStart,
periodEnd,
interval,
sum("total")
)
#Aggregator Functions
| Function | Usage | Description |
|---|---|---|
count() | count() | Counts the number of items in each time bucket |
sum(field) | sum("total") | Sums a numeric field across items in each bucket |
average(field) | average("total") | Averages a numeric field across items in each bucket |
You can also pass a custom function that receives an array of items and returns a number:
(items: any[]) => {
const totalSales = items.reduce((s, o) => s + (o.total || 0), 0)
const totalRefunds = items.reduce((s, o) => s + (o.refunded_total || 0), 0)
return totalSales > 0 ? (totalRefunds / totalSales) * 100 : 0
}
#Registering Your Provider
Add your custom provider to medusa-config.ts:
modules: [
{
resolve: "medusa-stats/modules/statistics",
options: {
providers: [
{ resolve: "medusa-stats/providers/common" },
{ resolve: "medusa-stats/providers/composite" },
{ resolve: "./src/providers/statistics/cart-statistics" }, // Your provider
],
},
},
]