Skip to content

Analytics

The analytics subsystem compiles semantic queries into SQL and runs them against PostgreSQL via the DrizzleAnalyticsAdapter. No external service is required.

For query setup and custom model registration, see the Analytics guide.


interface AnalyticsQueryParams {
measures: string[];
dimensions?: string[];
timeDimensions?: AnalyticsTimeDimension[];
filters?: AnalyticsFilter[];
order?: Record<string, "asc" | "desc">;
limit?: number;
}
PropertyRequiredDescription
measuresYesMeasure names in ModelName.measureName format
dimensionsNoDimension names for GROUP BY
timeDimensionsNoTime-based grouping and filtering
filtersNoArray of filter objects
orderNoSort order by measure or dimension
limitNoRow limit (default: 100)
{
dimension: "Orders.placedAt",
granularity: "month", // day | week | month | year
dateRange: ["2025-01-01", "2025-12-31"] // or preset string like "last 6 months"
}
{
member: "Orders.status",
operator: "equals",
values: ["confirmed", "processing"]
}
OperatorApplies toDescription
equalsstring, number, timeExact match (supports multiple values)
notEqualsstring, number, timeExcludes exact matches
containsstringCase-insensitive substring match
instring, numberValue is in the provided list
notInstring, numberValue is not in the provided list
gt / gtenumberGreater than / greater than or equal
lt / ltenumberLess than / less than or equal
beforeDatetimeBefore a timestamp
afterDatetimeAfter a timestamp
inDateRangetimeBetween two timestamps

Combine filters with and / or:

{
"filters": [
{
"or": [
{ "member": "Orders.status", "operator": "equals", "values": ["confirmed"] },
{ "member": "Orders.status", "operator": "equals", "values": ["processing"] }
]
}
]
}

Table: orders

Measures:

NameTypeDescription
Orders.countcountNumber of orders
Orders.revenuesumSum of grand totals
Orders.averageOrderValueavgAverage grand total per order
Orders.subtotalRevenuesumSum of subtotals (before tax/shipping)
Orders.taxCollectedsumSum of tax amounts
Orders.shippingRevenuesumSum of shipping charges
Orders.discountsGivensumSum of discounts applied
Orders.uniqueCustomerscountDistinctUnique customers who ordered

Dimensions:

NameTypeDescription
Orders.idstringOrder UUID
Orders.orderNumberstringHuman-readable order number
Orders.statusstringOrder status
Orders.currencystringCurrency code
Orders.placedAttimeOrder creation timestamp

Table: order_line_items (left join to orders)

Measures:

NameTypeDescription
OrderLineItems.countcountNumber of line items
OrderLineItems.itemsSoldsumTotal quantity of items sold
OrderLineItems.lineItemRevenuesumSum of line item total prices
OrderLineItems.averageUnitPriceavgAverage unit price across line items

Dimensions:

NameTypeDescription
OrderLineItems.entityTypestringEntity type (product, service, etc.)
OrderLineItems.skustringSKU at time of purchase
OrderLineItems.titlestringProduct title at time of purchase
OrderLineItems.fulfillmentStatusstringLine item fulfillment status

Table: inventory_levels

Measures:

NameTypeDescription
Inventory.totalOnHandsumTotal on-hand quantity
Inventory.totalReservedsumTotal reserved quantity
Inventory.totalAvailablesumTotal available (on hand minus reserved)
Inventory.inventoryValuesumTotal inventory value at cost
Inventory.lowStockCountcountItems below reorder threshold

Dimensions:

NameTypeDescription
Inventory.entityIdstringCatalog entity UUID
Inventory.warehouseIdstringWarehouse UUID
Inventory.lastRestockedAttimeLast restock timestamp

Segments:

NameDescription
Inventory.lowStockItems at or below reorder threshold

Table: customers

Measures:

NameTypeDescription
Customers.customerCountcountTotal customer profiles
Customers.newCustomerscountNew customer profiles
Customers.returningCustomerscountCustomers with more than one order

Dimensions:

NameTypeDescription
Customers.createdAttimeProfile creation timestamp
Customers.customerGroupstringCustomer group (from metadata)

Table: marketplace_vendor_sub_orders

MeasureDescription
VendorOrders.countNumber of vendor sub-orders
VendorOrders.revenueVendor-attributed revenue
VendorOrders.commissionPaidTotal commissions paid
VendorOrders.netPayoutNet payout after commission

Table: marketplace_vendor_balances

MeasureDescription
VendorBalance.totalCreditsTotal credits (earnings)
VendorBalance.totalDebitsTotal debits (payouts)
VendorBalance.netBalanceNet balance (credits minus debits)
MeasureDescription
VendorReviews.countNumber of reviews
VendorReviews.averageRatingMean review rating

All queries run within an AnalyticsScope that restricts data visibility based on the caller’s role. Scopes are constructed exclusively through buildAnalyticsScope(actor):

import { buildAnalyticsScope } from "@porulle/core";
const scope = buildAnalyticsScope(actor);
const result = await kernel.analytics.query(params, scope);
Actor roleData access
admin, owner, staffAll models, all rows
Vendor (actor has vendorId)Vendor models only, filtered by vendorId
customerCustomer models only, filtered by customerId
UnauthenticatedAll queries blocked

Contribute models from a plugin via analyticsModels:

import { defineCommercePlugin } from "@porulle/core";
import type { AnalyticsModel } from "@porulle/core";
const model: AnalyticsModel = {
name: "Subscriptions",
table: "subscriptions",
measures: {
count: { type: "count" },
mrr: { sql: "monthly_amount", type: "sum" },
},
dimensions: {
status: { sql: "status", type: "string" },
startedAt: { sql: "started_at", type: "time" },
},
};
export const myPlugin = defineCommercePlugin({
id: "my-plugin",
version: "1.0.0",
analyticsModels: () => [model],
});

Models appear in GET /api/analytics/meta and can be queried via GET /api/analytics/query like any built-in model.