Guide

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 apps

How 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).

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:

VETRA_REPORTING_TOKENThis environment’s reporting token. A secret: send it only in the header below, never log it.
VETRA_LICENSING_URLThe 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:

reportUserStat.graphql
mutation ReportUserStat($user: String!, $metric: String!, $value: Float!) {
  vetraLicensing {
    reportUserStat(user: $user, metric: $metric, value: $value)
  }
}
shell
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).
  • true means queued: Renown has it within seconds, and pages refresh within a minute.
  • false means 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.

usage.ts
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-user-stat.ts
// 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.