Report app stats
Your environments report each user’s current value of the metrics you declare. Renown adds them up and shows them on your app’s public page and on every user’s profile.
Open your appsHow it works
Your package code, running in an environment Vetra provisioned for a licence holder, calls Vetra with the environment’s reporting token. Vetra checks the environment, its licence and your app’s Renown identity, then forwards the value to Renown as your app. Renown keeps one current value per user and metric.
Values show up on renown.id/app/<your app DID> (stat tiles, active users over 30 days, top contributors) and in the Stats section of each user’s profile, but only for metrics you declare as public.
Declare metrics
Open your app on Vetra, go to the Profile tab and add metrics in the Metrics section: a label, the key your code reports, an optional unit, how values are combined, and whether it is public. Up to 16 metrics. Keys start with a letter and use letters, digits and _ . : - (at most 64 characters).
| Aggregation | Shows | Example |
|---|---|---|
| Total (SUM) | Adds every user’s current value. | Notes written by everyone |
| Highest (MAX) | The highest current value of any user. | Longest streak |
| Average (AVG) | The mean of users’ current values. | Average score |
| Users (COUNT_USERS) | How many users have a value above zero. | Users who published |
A key you report but have not declared is stored, but not shown. Declare it later and its history appears.
Environment
Vetra writes two variables into each licensed environment’s secrets:
| Variable | What it is |
|---|---|
VETRA_REPORTING_TOKEN | This environment’s reporting token. A secret: send it only in the header below, never log it. |
VETRA_LICENSING_URL | The endpoint to call, for example https://switchboard.vetra.io/graphql/vetra-licensing. |
The first time Vetra writes them, the environment restarts once to pick them up. Local development has neither, and the helper below then sends nothing.
Report a value
Send the token in the x-vetra-reporting-token header (not Authorization), to VETRA_LICENSING_URL directly:
mutation ReportUserStat($user: String!, $metric: String!, $value: Float!) {
vetraLicensing {
reportUserStat(user: $user, metric: $metric, value: $value)
}
}curl -s "$VETRA_LICENSING_URL" \
-H 'content-type: application/json' \
-H "x-vetra-reporting-token: $VETRA_REPORTING_TOKEN" \
-d '{"query":"mutation { vetraLicensing { reportUserStat(user: \"did:pkh:eip155:1:0xHolderAddress\", metric: \"notes\", value: 42) } }"}'user is the user’s DID, for example did:pkh:eip155:1:0x…; value is any finite number.
Semantics
- Report the current value, not an increment. The newest report per user and metric wins.
- Sending the same value again is cheap and keeps the user counted as active (any report in the last 30 days).
truemeans queued: Renown has it within seconds, and pages refresh within a minute.falsemeans not relayed: the user is not this environment’s licence holder, your app has no usable Renown identity (authorize it on the app’s Overview), or Renown refused your app in the last five minutes.- Only environments Vetra provisions for a licence can report, and only for their holder. At most 32 metrics per user per app, 600 reports per minute per app.
Helper
A dependency-free TypeScript file you can copy into your package. It reads the two variables, validates the metric and value, and resolves to true when Vetra queued the report.
import { reportUserStat } from "./report-user-stat.js";
// In a processor or subgraph of your package, after the user acted.
// Send the user's CURRENT total, not the increment.
const relayed = await reportUserStat(userDid, "notes", notesWritten).catch((error: unknown) => {
console.warn("app stat not reported", error);
return false;
});
// Report a user's current value of an app metric to Vetra, which relays it
// to Renown: the app's page (renown.id/app/<app DID>) and the user's profile.
//
// Vetra gives each licensed environment two variables:
// VETRA_REPORTING_TOKEN this environment's reporting token (a secret)
// VETRA_LICENSING_URL the endpoint to call
// Without them (local development) nothing is sent and the result is false.
//
// Dependency-free: copy this file into your package.
// Docs: https://vetra.io/docs/app-stats
const METRIC = /^[A-Za-z][A-Za-z0-9_.:-]{0,63}$/;
const MUTATION =
"mutation ReportUserStat($user: String!, $metric: String!, $value: Float!) " +
"{ vetraLicensing { reportUserStat(user: $user, metric: $metric, value: $value) } }";
export class ReportUserStatError extends Error {
readonly code: string;
constructor(code: string, message: string) {
super(message);
this.name = "ReportUserStatError";
this.code = code;
}
}
export interface ReportUserStatOptions {
/** Where the two variables are read from (default: process.env). */
env?: Record<string, string | undefined>;
fetch?: typeof fetch;
/** Default 10000 ms. */
timeoutMs?: number;
}
type Answer = {
data?: { vetraLicensing?: { reportUserStat?: unknown } | null } | null;
errors?: { message?: string; extensions?: { code?: unknown } }[];
} | null;
/**
* Sends the user's CURRENT value of `metric` (not a delta: the newest report
* wins). Resolves true when Vetra queued it for Renown, false when nothing was
* sent (not on Vetra) or Vetra did not relay it (the user is not this
* environment's licence holder, the app has no usable Renown identity, or
* Renown refused the app recently). Throws ReportUserStatError for an invalid
* metric or value (INVALID_INPUT), an unknown token (UNAUTHENTICATED), or when
* Vetra cannot be reached (NETWORK, HTTP_<status>).
*/
export async function reportUserStat(
user: string,
metric: string,
value: number,
options: ReportUserStatOptions = {},
): Promise<boolean> {
const env = options.env ?? process.env;
const url = env.VETRA_LICENSING_URL?.trim();
const token = env.VETRA_REPORTING_TOKEN?.trim();
if (!url || !token) return false;
if (!METRIC.test(metric)) {
throw new ReportUserStatError("INVALID_INPUT", "metric must match " + METRIC.source);
}
if (!Number.isFinite(value)) {
throw new ReportUserStatError("INVALID_INPUT", "value must be a finite number");
}
let res: Response;
let text: string;
try {
res = await (options.fetch ?? fetch)(url, {
method: "POST",
headers: { "content-type": "application/json", "x-vetra-reporting-token": token },
body: JSON.stringify({ query: MUTATION, variables: { user, metric, value } }),
signal: AbortSignal.timeout(options.timeoutMs ?? 10000),
});
text = await res.text();
} catch (error) {
throw new ReportUserStatError("NETWORK", error instanceof Error ? error.message : "request failed");
}
let answer: Answer = null;
try {
answer = JSON.parse(text) as Answer;
} catch {
answer = null;
}
const error = answer?.errors?.[0];
if (error) {
const code = typeof error.extensions?.code === "string" ? error.extensions.code : "ERROR";
throw new ReportUserStatError(code, error.message ?? "reportUserStat failed");
}
if (!res.ok) {
throw new ReportUserStatError("HTTP_" + String(res.status), "Vetra answered " + String(res.status));
}
return answer?.data?.vetraLicensing?.reportUserStat === true;
}
Troubleshooting
The variables are missing
Only environments provisioned for a licence get them, a few at a time; the environment restarts once when they arrive. Wait a minute and check again.
UNAUTHENTICATED: unknown reporting token
The token is from an earlier environment or was replaced. Restart the environment so it reads its current secret.
The call returns false
Report for the environment’s licence holder, and check that your app’s Renown identity is authorized on its Overview. After a refusal Renown pauses your app for five minutes.
Values arrive but nothing shows
Declare the key as a public metric in the app’s Profile tab. Keys are case-sensitive and must match exactly.