Gamend.Analytics (gamend_sdk v1.0.1189)

Copy Markdown View Source

The one place aggregate numbers come from. Four families:

  • Activity & retention — DAU / WAU / MAU, new users per day, strict D1 / D7 / D30 cohort retention, payer conversion. Derived from user_activity_days (one row per user per UTC day seen, written by record_activity/2 from the last_seen_at touches in Gamend.Accounts), users.inserted_at for cohorts and purchases for payers.
  • Snapshotsnapshot/0: every context's live counters (players, lobbies, parties, quests, signaling, matchmaking, tournaments) composed once and cached, so the public stats page, the admin index and the /api/v1/stats endpoint show the same numbers instead of each calling six modules.
  • Economy floweconomy_flow/2: coins granted and spent per day per ledger reason, straight from ledger_entries.
  • Daily counterscount/3 + counts/2: named per-day counters for events that have no table of their own (a level finished, a start blocked by empty hearts). The game picks the keys; the engine sums them.

Days are UTC, like every other period boundary on the server. "D7" means seen on the 7th day after the registration day, not "seen at any point in the first week" — the classic strict definition, so numbers compare with the ones stores and ad networks report.

Reads are cheap enough for a dashboard on a small database; the composed views (snapshot/0, dashboard_stats/0) cache for a minute or a few. Nothing here is exposed to players except through the operator-gated public stats page.

Note: This is an SDK stub. Calling these functions will raise an error. The actual implementation runs on the Gamend.

Summary

Functions

Distinct users seen on any day in from..to (inclusive, UTC dates).

Adds n to the counter key for the UTC day of now. One atomic upsert-increment; safe to call from hot paths that already write (a level end, a purchase), not from paths that otherwise only read.

Totals of counts/3 per key over the window, largest first.

Per-day values of one counter or a prefix family over the last days days ending on today, as %{key => %{Date => count}}. key may end in * to match a prefix ("level.started.lang:*").

One row per UTC day for the last days days ending on today, oldest first: %{day, active, new_users, d1, d7, d30}. A retention rate is nil when the cohort is empty or the horizon day has not happened yet.

summary/0 cached for a few minutes, for the admin index.

Daily active users on day (default: today, UTC).

Currency granted and spent per UTC day per ledger reason over the last days days: [%{day, currency, reason, granted, spent, entries}], newest first. granted/spent are positive integers (spent is the absolute value of negative deltas). Options: :currency to filter.

economy_flow/2 collapsed over the window: one row per {currency, reason} with granted, spent, net, sorted by absolute net.

Monthly active users: distinct users seen in the 30 days ending on day.

Accounts created on any UTC day in from..to (inclusive).

Distinct users with a completed purchase since from (UTC date, inclusive).

Records that user_id was seen on the UTC day of now. Idempotent; the common case (already recorded today) is a single cache read. Never raises — a missing user (deleted mid-request) is a silent no-op, like the last_seen_at touch this rides along with.

Every context's live counters in one map, cached for a minute

Headline numbers as of today: DAU / WAU / MAU, stickiness (DAU ÷ MAU), new users in the last 7 and 30 days, D1 / D7 / D30 pooled over every cohort of the last 60 days that is old enough to have reached the horizon, and payer conversion (distinct users with a completed purchase in the last 30 days ÷ MAU).

Weekly active users: distinct users seen in the 7 days ending on day.

Types

rate()

@type rate() :: float() | nil

Functions

active_users(from, to)

@spec active_users(Date.t(), Date.t()) :: non_neg_integer()

Distinct users seen on any day in from..to (inclusive, UTC dates).

count(key, n, now)

@spec count(String.t(), pos_integer(), DateTime.t()) :: :ok

Adds n to the counter key for the UTC day of now. One atomic upsert-increment; safe to call from hot paths that already write (a level end, a purchase), not from paths that otherwise only read.

Keys are dotted, game-owned strings up to 128 chars; put a dimension after a colon ("level.started.lang:ja") so counts/2 can pull a whole family by prefix. Never raises; a bad key is dropped.

count_totals(key, days, today)

@spec count_totals(String.t(), pos_integer(), Date.t()) :: [{String.t(), integer()}]

Totals of counts/3 per key over the window, largest first.

counts(key, days, today)

@spec counts(String.t(), pos_integer(), Date.t()) :: %{
  required(String.t()) => %{required(Date.t()) => integer()}
}

Per-day values of one counter or a prefix family over the last days days ending on today, as %{key => %{Date => count}}. key may end in * to match a prefix ("level.started.lang:*").

daily_series(days, today)

@spec daily_series(pos_integer(), Date.t()) :: [map()]

One row per UTC day for the last days days ending on today, oldest first: %{day, active, new_users, d1, d7, d30}. A retention rate is nil when the cohort is empty or the horizon day has not happened yet.

dashboard_stats()

@spec dashboard_stats() :: map()

summary/0 cached for a few minutes, for the admin index.

dau(day)

@spec dau(Date.t()) :: non_neg_integer()

Daily active users on day (default: today, UTC).

economy_flow(days, opts)

@spec economy_flow(
  pos_integer(),
  keyword()
) :: [map()]

Currency granted and spent per UTC day per ledger reason over the last days days: [%{day, currency, reason, granted, spent, entries}], newest first. granted/spent are positive integers (spent is the absolute value of negative deltas). Options: :currency to filter.

economy_totals(days, opts)

@spec economy_totals(
  pos_integer(),
  keyword()
) :: [map()]

economy_flow/2 collapsed over the window: one row per {currency, reason} with granted, spent, net, sorted by absolute net.

mau(day)

@spec mau(Date.t()) :: non_neg_integer()

Monthly active users: distinct users seen in the 30 days ending on day.

new_users(from, to)

@spec new_users(Date.t(), Date.t()) :: non_neg_integer()

Accounts created on any UTC day in from..to (inclusive).

payers_since(from)

@spec payers_since(Date.t()) :: non_neg_integer()

Distinct users with a completed purchase since from (UTC date, inclusive).

record_activity(user_id, now)

@spec record_activity(Ecto.UUID.t(), DateTime.t()) :: :ok

Records that user_id was seen on the UTC day of now. Idempotent; the common case (already recorded today) is a single cache read. Never raises — a missing user (deleted mid-request) is a silent no-op, like the last_seen_at touch this rides along with.

snapshot()

@spec snapshot() :: map()

Every context's live counters in one map, cached for a minute:

  %{
    players: %{players_online, players_total, players_offline, players_in_lobbies, players_in_parties},
    activity: %{dau, wau, mau, new_users_1d, new_users_7d, new_users_30d},
    lobbies: %{lobbies_total, by_state, spectators},
    parties: %{parties_active, players_in_parties},
    quests: %{quests_total, completed, claimed},
    signaling: %{rooms_enabled, rooms_active, peers_connected},
    matchmaking: %{queued, queues},
    tournaments: %{tournaments, entries, matches}
  }

Everything in it is safe to show publicly (the stats page is opt-in via :public_stats); matchmaking is the already-public subset. Consumers read this rather than calling the contexts, so the same number cannot drift between two pages.

summary(today)

@spec summary(Date.t()) :: map()

Headline numbers as of today: DAU / WAU / MAU, stickiness (DAU ÷ MAU), new users in the last 7 and 30 days, D1 / D7 / D30 pooled over every cohort of the last 60 days that is old enough to have reached the horizon, and payer conversion (distinct users with a completed purchase in the last 30 days ÷ MAU).

wau(day)

@spec wau(Date.t()) :: non_neg_integer()

Weekly active users: distinct users seen in the 7 days ending on day.