Most game-analytics implementations get the SDK installed in an afternoon and the event taxonomy wrong for the next two years. The cost is real: dashboards that nobody trusts, funnels that cannot be drawn because the events do not stitch together, retention cohorts that drift because someone renamed a key. This guide is the event-design discipline we recommend to studios starting fresh with the RoLearn SDK — naming conventions, payload shape, the half-dozen event categories every game needs, and the anti-patterns that ruin every dashboard six months in. Applies to the Roblox Lua SDK, but the same patterns translate cleanly to Unity, Steamworks, or any other platform you ship on.
Naming convention: snake_case, action-first, no synonyms
The single highest-leverage decision in event design is the naming convention, and the only thing that matters is consistency. We recommend:
- snake_case throughout. Matches the auto-emitted events (
session_start,session_end,purchase) so your dashboard filters work uniformly. Mixed case is the most common preventable mistake. - Object_action, not action_object.
level_completedsorts next tolevel_startedandlevel_failedin any alphabetical list, which makes funnel construction trivial.completed_levelburies it under "completed". - Past tense for things that happened.
quest_claimed, notclaim_quest. The event records a thing that occurred; tense follows. - No synonyms. Pick
purchasedORboughtORacquired, not all three. Document the choice in a one-page wiki entry that every contributor can grep.
Bad vs good, ten lines:
| Bad | Good |
|---|---|
LevelComplete | level_completed |
boughtGamepass | gamepass_purchased |
questDone | quest_claimed |
startedMatch | match_started |
shopOpen | shop_opened |
Payload shape: flat, typed, schema-stable
The event payload is a small JSON object. The discipline:
- Flat, not nested. Nested payloads survive in the database but make dashboards harder. Prefer
{"level": 5, "score": 1840}over{"metadata": {"level": 5, "score": 1840}}. - Typed values, not stringified. Send
"score": 1840as a number, not"score": "1840". The dashboard's numeric aggregations (sum, p50, p95) refuse to operate on strings. - Schema-stable. Once a key is in production, do not change its type.
scoreas a number stays a number forever. If you need a string version, addscore_grade; do not repurpose the field. - Bounded cardinality on enum fields.
"result": "win" | "loss" | "abandon"is great."result": "<freeform string>"produces a dashboard with 4,000 unique values, none of which group meaningfully. - Under 1 KB serialized per event. The ingest endpoint accepts more, but anything beyond ~1 KB starts to slow batch sends and inflate storage. If you have larger telemetry, log to your own logging stack and send only the summary to RoLearn.
The six event categories every game needs
Beyond the four auto-emitted event types (session, purchase, performance, crash), most games converge on six categories of custom events. Coverage of all six is enough for the majority of analytics questions; adding more usually adds noise.
1. Funnel events
The sequence of user actions you want to measure conversion through. For an obby: spawn → checkpoint_1 → checkpoint_2 → completed. For a first-purchase funnel: session_start → shop_opened → item_inspected → purchase (auto). Three rules:
- Emit the same event type at the same point in the flow on every code path. Branching codepaths that emit different event names destroy the funnel.
- Funnel events do not need a per-event ID — the player_id + timestamp is enough for the analytics layer to stitch the sequence.
- Include the step number in the payload (
{"step": 2}) for funnels with many steps. It makes the SDK Funnel view filterable by completion-depth.
2. Progression events
Events that mark the player's advancement through the game's content. level_completed, chapter_started, prestige, character_unlocked. Payload should include:
- The thing they progressed to (
level,character_id). - The time it took (
duration_s) for cohort-comparable progression speed. - The outcome (
result) on bounded enum. - Whether they used a paid boost (
used_boostboolean) for monetization correlation.
3. Engagement events
Voluntary player actions that signal investment in the game beyond passive play. friend_invited, party_joined, chat_message_sent, cosmetic_equipped. Engagement events correlate strongly with retention; they are usually the leading indicator a dashboard surface should track.
4. Economy events
In-game currency flows, including non-Robux currencies (gold, gems, energy, whatever your game uses). The pattern:
SDK.track("currency_earned", {
currency = "gold",
amount = 250,
source = "quest_reward_daily",
balance = 4830, -- post-transaction balance
}, player)
SDK.track("currency_spent", {
currency = "gold",
amount = 1200,
sink = "shop_purchase_sword_upgrade",
balance = 3630,
}, player)Tracking source + sink separately (rather than as a single "transaction" event with positive/negative amounts) makes the economy-health dashboards meaningfully easier to build.
5. A/B test assignment events
When you ship an A/B test, emit an assignment event the first time a player is bucketed:
SDK.track("ab_assignment", {
experiment = "lobby_layout_v3",
variant = "B",
assigned_at_session = session_count,
}, player)Then on the dashboard you can segment any other event by experiment + variant. Two rules: emit assignment exactly once per player per experiment (use a server-side cache to prevent re-emission), and never emit a variant value that was not in the experiment's published variant list (lost events are easier to debug than mysterious extra variants).
6. Lifecycle/system events
Server-side events that mark important moments not driven by a single player. server_started, round_began, boss_spawned, event_window_opened. Emit without a player argument (the SDK accepts a nil player). Use sparingly — these are noise on per-player dashboards.
Five anti-patterns that ruin dashboards
- Stringly-typed enums.
"difficulty": "Easy"one day,"difficulty": "EASY"another day,"difficulty": "easy mode"a third. Lowercase or UPPERCASE, never mixed, and never freeform. - Unbounded payload cardinality. Sending the player's full inventory in every purchase event creates tens of thousands of unique values in the inventory field. Send the SKU being purchased, not the whole inventory.
- Duplicate events for the same action. Two different code paths emit
level_completedANDlevel_finished. Funnels double-count, retention inflates. Pick one, deprecate the other, grep the codebase. - Tracking things you do not need. Server-side
tickevents at 60 Hz, debug events left in production, instrumentation of every function call. The free SDK tier gives you a meaningful event budget; the paid tiers have monthly caps. Spend the budget on signals you actually use. - Changing event semantics without renaming.
match_completedpreviously fired only on win; now it fires on win and loss. Six months of historical data now mixes two different definitions silently. If the semantics change, the name should change too.
A reference event catalog (paste into your wiki)
Here is a starter catalog for a typical Roblox PvP game. Copy it into your team's documentation, modify, and commit. The important part is that EVERY contributor reads it before adding a new event.
| Event name | When it fires | Payload keys |
|---|---|---|
queue_entered | Player presses Play in lobby | mode (ranked/casual), party_size |
match_started | Server begins round | match_id, mode, map, player_count |
match_completed | Server ends round (any outcome) | match_id, result (win/loss/draw/abandon), kills, deaths, duration_s |
character_unlocked | Player meets unlock conditions | character_id, source (free/purchase/quest) |
cosmetic_equipped | Player equips a cosmetic to character | character_id, slot, item_id, rarity |
shop_opened | Player opens any shop UI | shop_id, source (button/cta/promo) |
item_inspected | Player clicks an item to view details | shop_id, item_id, price, currency |
currency_earned | Any non-purchase currency gain | currency, amount, source, balance |
currency_spent | Any non-purchase currency spend | currency, amount, sink, balance |
quest_claimed | Player redeems quest reward | quest_id, type (daily/weekly/event), reward_summary |
friend_invited | Player sends a Roblox friend invite from in-game | method (party_button/match_end_screen) |
ab_assignment | Player bucketed into an experiment for the first time | experiment, variant, assigned_at_session |
Twelve events plus the four auto-instrumented categories (session, purchase, performance, crash) is enough to answer ~95% of the analytics questions a typical Roblox PvP game gets asked. Resist the urge to add more before you have a specific question only a new event can answer.
Evolving the schema over time
Schemas grow. Two rules for evolving without breaking historical analytics:
- Add fields, don't repurpose them. If you need a new dimension on an existing event, add a key. Renaming or retyping an existing key fragments your historical data.
- Deprecate, then delete, never replace. When retiring an event, stop emitting it on a release boundary, announce the deprecation in your team docs, leave the historical data in place. Replacing the event in-place breaks dashboards that filter on the old name.
Where to go next
- Building Custom Dashboards — once your events are clean, this is where you build the views your team uses every day.
- SDK integration reference — full event schema definitions, validation rules, ingest response codes.
- Roblox SDK Quickstart — if you haven't installed the SDK yet, start there.
- GDPR Compliance with the RoLearn SDK — how to design your event taxonomy to make GDPR data-deletion requests easy to fulfill.
