Declarative Metrics with defineMetrics
defineMetrics is the recommended high-level API for creating analytics domains. It takes a typed metric definition and returns a fully-wired object with stores, initialization, and query methods — all with inferred TypeScript types.
Basic usage
import { defineMetrics } from "redis-analytics/schema";
const tippingMetrics = defineMetrics({
prefix: "analytics:tipping",
metrics: {
tips: {
type: "timeseries",
config: { duplicatePolicy: "SUM" },
aggregations: {
tips_usd_total: "SUM",
tips_total: "COUNT",
tips_usd_avg: "AVG",
},
},
fees: {
type: "timeseries",
config: { duplicatePolicy: "SUM" },
aggregations: {
fees_usd_total: "SUM",
fees_usd_avg: "AVG",
},
},
unique_tippers: { type: "hll" },
unique_tippees: { type: "hll" },
},
});What you get
stores
Direct access to each store instance, typed by metric type:
tippingMetrics.stores.tips // → TimeseriesStore
tippingMetrics.stores.fees // → TimeseriesStore
tippingMetrics.stores.unique_tippers // → HllStore
tippingMetrics.stores.unique_tippees // → HllStoreinit()
Initializes all stores in parallel:
await tippingMetrics.init();getStats(scope)
Returns aggregated stats for all metrics. Pass a timeframe string or a date range object. The return type is fully inferred from your definition:
// By timeframe
const stats = await tippingMetrics.getStats("24h");
// Type: {
// tips_usd_total: number;
// tips_total: number;
// tips_usd_avg: number;
// fees_usd_total: number;
// fees_usd_avg: number;
// unique_tippers: number;
// unique_tippees: number;
// }
// By date range
const rangeStats = await tippingMetrics.getStats({
start: new Date("2024-01-01"),
end: new Date("2024-01-31"),
});getSeries(scope, bucket)
Returns bucketed time series for all metrics:
const series = await tippingMetrics.getSeries("1w", "d");
// Type: {
// tips_usd_total: AnalyticBucket[];
// tips_total: AnalyticBucket[];
// ...
// }
// By date range
const rangeSeries = await tippingMetrics.getSeries(
{ start: new Date("2024-01-01"), end: new Date("2024-01-31") },
"d"
);Metric types
timeseries
Wraps a TimeseriesStore. Each key in aggregations becomes a stat key in the output.
events: {
type: "timeseries",
config: { duplicatePolicy: "SUM" },
aggregations: {
events_total: "COUNT",
events_sum: "SUM",
events_avg: "AVG",
},
}hll
Wraps an HllStore for approximate unique counting. The metric name is the stat key.
unique_users: { type: "hll" }
// → stats.unique_users: numberbloom-counter
Wraps a BloomCounterStore for first-seen detection. The metric name is the stat key.
new_users: {
type: "bloom-counter",
bloom: { error_rate: 0.01, space: 1_000_000 },
}
// → stats.new_users: numberQuery scope
Both getStats and getSeries accept a MetricScope — either a timeframe string or a date range:
type MetricScope = Timeframe | DateRange;Where:
Timeframe="24h" | "1w" | "1m" | "1y" | "lifetime"DateRange={ start: Date; end: Date }Bucket="h" | "d" | "m"(forgetSeriesonly)
Migration from manual stores
Before:
const tipsStore = new TimeseriesStore("analytics:tipping:tips", { duplicatePolicy: "SUM" });
const feesStore = new TimeseriesStore("analytics:tipping:fees", { duplicatePolicy: "SUM" });
const uniqueTippersStore = new HllStore("analytics:tipping:tippers");
const tippingQuery = tsQuery({
tips_total: ts(tipsStore.key, "COUNT"),
tips_usd_total: ts(tipsStore.key, "SUM"),
fees_usd_total: ts(feesStore.key, "SUM"),
});
// init
await Promise.all([tipsStore.init(), feesStore.init()]);
// stats - manual branching + manual merging
async function getStats(query) {
if (query.type === "timeframe") {
const [tsStats, tippers] = await Promise.all([
tippingQuery.timeframe(query.params),
uniqueTippersStore.get(query.params),
]);
return { ...tsStats, unique_tippers: tippers };
} else {
const [tsStats, tippers] = await Promise.all([
tippingQuery.range(query.params),
uniqueTippersStore.total(query.params),
]);
return { ...tsStats, unique_tippers: tippers };
}
}After:
const tippingMetrics = defineMetrics({
prefix: "analytics:tipping",
metrics: {
tips: {
type: "timeseries",
config: { duplicatePolicy: "SUM" },
aggregations: { tips_total: "COUNT", tips_usd_total: "SUM" },
},
fees: {
type: "timeseries",
config: { duplicatePolicy: "SUM" },
aggregations: { fees_usd_total: "SUM" },
},
unique_tippers: { type: "hll" },
},
});
await tippingMetrics.init();
const stats = await tippingMetrics.getStats("24h");
const series = await tippingMetrics.getSeries("1w", "d");