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:

ArgumentTypeDescription
identifierstringUnique ID for the statistic (e.g. "total_orders")
optionsobjectConfiguration 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"))
ParameterDescription
this.queryThe module's query service
inputThe full StatCalculationInput (spread with ...input)
config.entityThe entity name to query (e.g. "order", "cart")
config.fieldsArray of fields to retrieve
config.filtersOptional filters to apply
aggregatorAccumulator 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

FunctionUsageDescription
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
      ],
    },
  },
]