Skip to content

MeterUsageTable

A turnkey query-and-render table. Configure authentication once, then provide the source and query. No load, data, useEffect, row mapping, cursor tracking or pagination code is required. Existing MeterTable data/load modes are unchanged.

Loading interactive preview…

View codeHide code

Actual React source for this preview, including demo controls, shared styles and isolated mocks. The docs host supplies the client and theme; application installation and React / Vue usage follow below.

import { useState } from "react"
import { MeterUsageTable } from "@meterry/component-react"
import { METRICS, type PreviewEnvironment } from "./preview-data"
import { styleProps, eventColumns, RequestSummary } from "./preview-shared"
export function AutomaticTablePreview({
environment,
}: {
environment: PreviewEnvironment
}) {
const [source, setSource] = useState<"events" | "usage">("events")
return (
<div className="docs-preview-stack" data-testid="automatic-table-preview">
<label className="docs-preview-chart-select">
Table source
<select
value={source}
onChange={(event) =>
setSource(event.target.value as "events" | "usage")
}
>
<option value="events">Events · automatic cursor</option>
<option value="usage">Aggregate · one bounded result</option>
</select>
</label>
<MeterUsageTable
key={source}
{...styleProps}
source={source}
title="Usage · automatic query and pagination"
query={
source === "events"
? {
start_time: environment.startTime,
end_time: environment.endTime,
}
: {
start_time: environment.startTime,
end_time: environment.endTime,
metrics: [...METRICS],
group_by: ["bucket", "model", "metric"],
measures: ["quantity"],
}
}
columns={source === "events" ? eventColumns : undefined}
pageSize={10}
batchSize={20}
pageSizeOptions={[10, 20]}
/>
<RequestSummary environment={environment} />
<div className="docs-preview-actions">
<button type="button" onClick={() => environment.failNextRequest()}>
Fail next API request
</button>
<span className="docs-preview-filter-summary">
Then use Refresh or fetch another batch; Retry recovers.
</span>
</div>
</div>
)
}
Terminal
npm install @meterry/component @meterry/component-react echarts echarts-for-react

For default styles, optionally install @meterry/component-tailwindcss. For authentication, shared clients and theming, see Getting Started.

The demo deliberately uses 20-row API batches and 10-row UI pages. Moving to page 3 automatically fetches the next batch; returning to a cached page makes no request. The component default API batch size is 500. Use the request counter below to observe fetching; switch to Aggregate to see the limit warning. The preview uses the current raw_event_id response field.

baseUrl: '' sends Subject API requests to same-origin /v1/.... Set it to your actual API origin or proxy prefix; /api/meter/subject-token is a separate application-owned endpoint, not a built-in Meter route. The preview makes no real network requests or persistent token writes.

import { MeterClientProvider, MeterUsageTable } from '@meterry/component-react'
import type { TableColumn, UsageTableRow } from '@meterry/component'
// Current Meter detail responses use raw_event_id as their sole event identity.
const eventColumns: TableColumn<UsageTableRow>[] = [
{ key: 'raw_event_id', header: 'Event ID' },
{ key: 'occurred_at', header: 'Occurred at', format: (v) => new Date(Number(v) * 1000).toLocaleString() },
{ key: 'source', header: 'Source' },
]
export function UsagePage() {
return (
<MeterClientProvider
tokenEndpoint="/api/meter/subject-token"
options={{ baseUrl: '', credentials: 'include', tokenStorage: null }}
>
<MeterUsageTable source="events" columns={eventColumns} rangeDays={7} pageSize={20} />
</MeterClientProvider>
)
}

tokenEndpoint performs a session-authenticated GET to your application backend, which must return a Subject Token grant with access_token and optionally expires_at/expires_in. It is not a tenant API-key endpoint. client takes precedence over getJwt, which takes precedence over tokenEndpoint. Existing client and getJwt integration still works. Provider options.fetch is used for both the token endpoint and API requests. Use an authentication-scoped tokenStorageKey and clear credentials when switching users/tenants, as described in SubjectClient & Auth.

Provide the client in an ancestor’s setup:

<script setup lang="ts">
import { provideMeterClient } from '@meterry/component-vue'
import UsagePage from './UsagePage.vue'
provideMeterClient({
tokenEndpoint: '/api/meter/subject-token',
options: { baseUrl: '', credentials: 'include', tokenStorage: null },
})
</script>
<template><UsagePage /></template>

In the descendant UsagePage.vue:

<script setup lang="ts">
import { MeterUsageTable } from '@meterry/component-vue'
import type { TableColumn, UsageTableRow } from '@meterry/component'
const eventColumns: TableColumn<UsageTableRow>[] = [
{ key: 'raw_event_id', header: 'Event ID' },
{ key: 'source', header: 'Source' },
]
</script>
<template>
<MeterUsageTable source="events" :columns="eventColumns" :range-days="7" :page-size="20" />
</template>

Alternatively pass an existing client directly to either framework component.

Source Endpoint Behavior
usage (default) POST /v1/subject/usage/query Fetches one aggregate result and pages that result locally; generates columns from group_by and measures
events GET /v1/subject/usage/events/details Automatically follows next_after_id using after_id, fetching batches only when required

When a control toggles between events and usage, use <MeterUsageTable key={source} source={source} ... /> (Vue: :key="source") to remount the query-owned table. Reusing one instance can otherwise attempt to render old event rows through new aggregate accessors during the transition. The interactive preview uses a separate keyed instance per source.

Aggregate query example:

<MeterUsageTable
source="usage"
query={{
metrics: ['total_tokens'],
group_by: ['bucket', 'model', 'metric'],
measures: ['quantity'],
filters: { provider: 'openai' },
}}
/>

The current aggregate API contract has no cursor. If an aggregate response reaches batchSize, a visible warning states that more results may exist. Its exact total remains unknown; the component does not invent cursors or silently claim that the prefix is the entire result. Narrow the time range or filters to reduce the result. This limit cannot be solved solely in the UI without backend pagination support.

Events queries accept only start_time, end_time, metric and metrics. Unsupported fields produce an actionable error rather than being ignored. Compatibility: the default event ID column uses the old usage_event_id field. The current Meter API exposes only raw_event_id; use the custom columns above. This changes presentation, not the automatic pagination pipeline. Event rows retain their original API fields (labels, metrics, etc.). Aggregate rows expose both flat dimension/measure fields and the original nested dimensions/measures objects, so custom TableColumn accessors and formatters work without an application mapper.

  • pageSize is the UI page size; batchSize is the API fetch size.
  • A UI page may span multiple API batches. Short/empty batches with a next cursor do not imply the end of the dataset.
  • A fixed time window is used throughout a query session. Refresh starts a new session and recomputes relative dates.
  • Cached pages do not refetch. An LRU row budget bounds memory; revisiting an evicted batch refetches it with its saved cursor and original time window.
  • The backend detail response includes total, but this component does not use it as its pagination authority. Until the final cursor is exhausted, the footer shows the page and loaded row count, not an exact total. Next-page availability follows the cursor.
  • Once exhaustion is confirmed, the exact discovered total and last page are shown.
  • No arbitrary distant jumps or last-page button are offered for unknown totals.
  • Concurrent requests for the same batch are deduplicated. Query changes/unmount abort pending work, and stale responses are ignored even if custom fetch ignores cancellation. Page-navigation failures retain the last successful page and offer Retry. Explicit Refresh, filter changes and query changes clear the old session/rows; if that new session fails, Retry starts it again without displaying old rows.
  • Query identity uses content, not object reference. Inline equivalent queries and changed column/style objects do not reset the query cache.

Neither wrapped endpoint declares server-side sorting. Therefore this component disables sortable column controls, including sortable: true on custom columns. It never presents a cached-prefix sort as a global sort. Use MeterTable with a complete local dataset if local sorting is desired.

Aggregate dimension filters are forwarded to the service. The generated dimension columns provide text filter inputs; the service decides matching semantics. Only columns whose keys are in group_by can expose a filter. Events do not expose column filters because their current request contract only supports time/metrics. Changing a filter resets the page, cursor and cache. Query filters and column filters merge, with column values taking precedence; clearing column filters preserves base query.filters.

Prop Default Description
client Provider client Explicit client takes precedence over context
source usage usage or events
query — Source-specific API query, without limit/cursor
rangeDays 7 Default relative time range; explicit query timestamps take precedence
columns Generated Optional TableColumn<UsageTableRow>[]; format/accessor/cell slots supported
pageSize 20 Displayed rows per page
pageSizeOptions [10, 25, 50] plus current size Page-size menu
batchSize 500 Requested API batch size, subject to the backend’s supported limit
maxCachedRows 10000 LRU budget; must be at least batchSize and pageSize
refreshable true Show the built-in Refresh action
title, caption, loading, empty, error, slots, messages Existing defaults Same rendering customization as MeterTable
className, classNames, unstyled Existing defaults Same semantic classes and styling as MeterTable

The default error UI includes Retry. A custom error renderer replaces it; custom pagination slots replace the default pagination and refresh controls. New message keys are cursorPaginationSummary, usageTableTruncated, retry, and refresh. Vue component refs also expose refresh().

import { createUsageTableController } from '@meterry/component'
const controller = createUsageTableController({ client, source: 'events' })
await controller.load()
await controller.setPage(2)
// Optional framework-independent integration: subscribe/getState/getColumns.
// Cleanup:
controller.dispose()

The controller implements the existing TableController<UsageTableRow> contract and adds dispose(). state.pagination carries total: number | null, loadedCount, hasNextPage, hasPreviousPage, and truncated. Legacy state.total is the discovered count; custom cursor footers must consult state.pagination.total before claiming an exact total. load() retries the last requested page, while refresh() invalidates the query and restarts at page 1. setData and global setSort are unsupported for this query-owned controller.

The Subject Usage Details API supports more filters than the current automatic wrapper: the wrapper only accepts time and metric selectors, even though the endpoint also supports label/account/raw-ID/revocation filters. Use the client plus advanced MeterTable when those additional controls are required. The API supports up to 1000 events per request; 500 is the component default, not a universal backend maximum. With 20-row UI pages and 500-row batches, page 26 fetches the second batch automatically (provided a next cursor exists).

after_id is opaque. Never derive it from row position or assume it is a normalized usage-event ID. Count, page and continuation checks are not a snapshot across requests; concurrent ingestion or revocation may change results. A fixed time window does not provide transactional snapshot isolation. If an evicted batch no longer matches, the component asks the user to refresh.

For aggregate query fields, supported measures, the 31-day range limit and the current 1000-row backend maximum, see Subject Usage Query. Automatic tables do not make a limited aggregate response an all-data export.