Querying and time series

Build a custom report by choosing the events to analyze, the measurements to calculate, and the dimensions to group by.

Examples use an existing analytics engine and the ZoneTree.Analytics namespace. Each recipe is independent; choose the one that matches your report rather than pasting the whole page into one method.

For a complete application workflow, see product analytics or service telemetry.

Compare acquisition sources

This report compares traffic and conversion by source:

var result = await analytics.QueryAsync(new AnalyticsQuery
{
    Range = DateRange.LastDays(30),
    GroupBy = { StandardDimensions.Source },
    Metrics = { Metric.Visitors, Metric.BounceRate, Metric.ConversionRate("purchase") },
    OrderBy = { Order.By(Metric.Visitors) },
    Limit = 10,
});

foreach (var row in result.Rows)
    Console.WriteLine($"{row[StandardDimensions.Source]}: {row[Metric.Visitors]} visitors, " +
        $"{row[Metric.BounceRate]:P0} bounce, {row[Metric.ConversionRate("purchase")]:P1} converted");

Console.WriteLine($"{result.Totals[Metric.Visitors]} visitors in total");

Event selects the base activity, page views by default. Metrics selects the measures, and GroupBy determines one or more categories in each result row.

For purchases, set Event="purchase" and choose purchase counts or revenue metrics. For telemetry, choose the recorded job or measurement event.

Narrow the audience

Add event filters to select the audience or activity your report should cover. Filters in the list combine with AND; use a nested group when an OR condition is needed:

var report = await analytics.QueryAsync(new AnalyticsQuery
{
    Range = DateRange.LastDays(30),
    GroupBy = { StandardDimensions.Source },
    Filters =
    {
        Filter.AnyOf(
            Filter.Equal(StandardDimensions.Country, "DE"),
            Filter.Equal(StandardDimensions.Country, "AT")),
    },
    Metrics = { Metric.Visitors, Metric.PageViews },
});

A filter selects matching events. A behavioral segment instead selects visitors or sessions based on another action.

Follow changes over time

Set an interval to return a time series. This purchase report combines average order value, cumulative revenue, a moving average, and purchase rate:

var averageOrder = Metric.Sum("revenue") / Metric.Events;

var result = await analytics.QueryAsync(new AnalyticsQuery
{
    Event = "purchase",
    Range = DateRange.ThisMonth,
    Interval = TimeInterval.Day,                              // or TimeInterval.Minutes(5), TimeInterval.Hours(6)
    Metrics =
    {
        averageOrder,
        Metric.Cumulative(Metric.Sum("revenue")),             // month to date
        Metric.MovingAverage(Metric.Visitors, 7),             // 7-day moving average
        Metric.Rate(Metric.Events, TimeSpan.FromHours(1)),    // purchases per hour
    },
});

Choose an interval that matches the decision: hourly for recent activity, daily for trends, or monthly for longer comparisons. Calendar buckets follow the report's time zone.

Calculate a focused metric

A conditional metric measures only matching contributions while retaining the surrounding report context. For example, an error rate can combine all requests with the subset carrying an error status.

var errors = Metric.Events.Where(Filter.GreaterThanOrEqual("status", 500));
var report = await analytics.QueryAsync(new AnalyticsQuery
{
    Event = "request_completed",
    GroupBy = { "route" },
    Metrics = { Metric.Events, errors, errors / Metric.Events },
});

Here status is a dimension containing a numeric HTTP status. The query's base event is request_completed, so both the numerator and denominator measure requests. See service telemetry for collection and rollup configuration.

Formulas apply to aggregate values. A total average order value is total revenue divided by total orders, rather than the average of displayed row averages.

Shape the output

Use OrderBy, Limit, and Offset for sorted, paged reports. Use Having when rows must meet an aggregate threshold, such as a minimum visitor count.

For the page with the highest recorded load time on each day, use LimitPerBucket. The events must carry a numeric load_time value in a consistent unit:

var slowestPages = await analytics.QueryAsync(new AnalyticsQuery
{
    Range = DateRange.LastDays(30),
    Interval = TimeInterval.Day,
    GroupBy = { StandardDimensions.Page },
    Metrics = { Metric.Max("load_time") },
    OrderBy = { Order.ByBucket(), Order.By(Metric.Max("load_time")) },
    LimitPerBucket = 1,
});

Pagination limits returned rows. It does not reduce every group the engine must aggregate.

Read rows and totals

Rows contain the selected grouping values and metrics. Totals covers the full report before pagination, including de-duplicated visitors. This example reads the result from the acquisition-source recipe:

foreach (var row in result.Rows)
{
    Console.WriteLine($"{row[StandardDimensions.Source]}: {row[Metric.Visitors]} visitors");
    if (row.Previous is { } previous)
        Console.WriteLine($"Previous: {previous[Metric.Visitors]} visitors");
}
Console.WriteLine($"Total: {result.Totals[Metric.Visitors]} visitors");

With a comparison, rows can carry Previous values. A missing comparison is different from a measured zero.

Check the report's context

Read From, To, and Plan.Warnings when presenting historical reports. Retention and available aggregates can affect coverage.

If a report is slow, follow its query plan. The query reference provides the full metric, filter, fill, and result contracts.