# Install in Roblox Studio (/docs/getting-started/install)

<CopyPrompt preset="install">
  Let your coding agent do it. It sets up the module in your server code, wraps ProcessReceipt, adds your key events and tells you how to check they arrive.
</CopyPrompt>

The RoChart module runs on the **game server** only. It sends events with `HttpService`, batched, so nothing runs on players' devices except a tiny script that reports their platform. You need your **project token** (`rc_…`) from **Project settings → Data collection**. The token can only send events and read flags. Never put a secret API key (`rc_sk_…`) in your game.

## Set up the module [#set-up-the-module]

<Steps>

### Allow HTTP requests [#allow-http-requests]

**Game Settings → Security → Allow HTTP Requests**, then publish. Without it, every request fails and nothing arrives.

### Put RoChart in ServerScriptService [#put-rochart-in-serverscriptservice]

Get the **RoChart** ModuleScript from **Project settings → Data collection** and insert it into **ServerScriptService**. Keep it on the server: ModuleScripts in ReplicatedStorage are visible to clients.

### Start it [#start-it]

In a Script in ServerScriptService, require the module and call `init` once, as early as possible:

```luau title="ServerScriptService/Analytics.server.luau"
local RoChart = require(game.ServerScriptService.RoChart)

RoChart.init({
  token = "rc_your_project_token",
  host = "https://app.rochart.co",
})
```

</Steps>

### What it sends on its own [#what-it-sends-on-its-own]

| Event | When |
| --- | --- |
| `$session_start` | A player joins. Carries the join source, campaign, account age, Premium, server type and place version. |
| `$session_end` | A player leaves, or the server shuts down. Carries `$duration` in seconds. |
| `$server_heartbeat` | Every 60 seconds per running server, with the player count and server health: FPS, memory, heartbeat time and the players' average ping. Concurrent players are built from it, and a server whose heartbeats stop without a shutdown counts as a crash. |
| `$server_shutdown` | The server closes (`BindToClose`). |
| `$error` | A script error, on the server or on a player's device. Identical errors are grouped for up to a minute and sent once with a `$count`. At most 30 per server per minute. |
| `$purchase` | A developer product is granted through `processReceipt`, or a game pass is bought in-game. |
| `$feature_flag_called` | A flag was read for a player. |

Every player event carries the session, server, place, platform (`Computer`, `Phone`, `Tablet`, `Console`, `VR`), country and locale. Each player is a person in RoChart, keyed by their **UserId**, with their name, display name, account age, Premium, country and platform as properties. See [Players and properties](/docs/getting-started/identify).

Roblox doesn't give games age or gender, so RoChart doesn't have them either. The full contract is in [Game analytics](/docs/game-analytics#where-the-numbers-come-from).

## Record purchases [#record-purchases]

Roblox lets a game set `MarketplaceService.ProcessReceipt` only once. Hand your callback to RoChart instead, and it records a `$purchase` with the Robux amount whenever you return `PurchaseGranted`:

```luau
RoChart.processReceipt(function(receiptInfo)
  local player = game.Players:GetPlayerByUserId(receiptInfo.PlayerId)
  if not player then
    return Enum.ProductPurchaseDecision.NotProcessedYet
  end
  grantProduct(player, receiptInfo.ProductId) -- your code
  return Enum.ProductPurchaseDecision.PurchaseGranted
end)
```

Game passes bought in-game are recorded without any extra code.

## Send your own events [#send-your-own-events]

```luau
RoChart.track(player, "match_ended", { mode = "duos", placement = 2, kills = 4, duration = 312 })
RoChart.track(player, "tutorial_step", { step = 3, name = "equip_sword" })
RoChart.trackServer("round_started", { map = "Volcano", players = #game.Players:GetPlayers() })
```

- `track(player, …)` is for something a player did. It's attached to that player and their session.
- `trackServer(…)` is for something the server did, such as a round starting or a boss spawning. It doesn't belong to any player.

Name events after what happened, in `snake_case` and past tense: `quest_completed`, `pet_hatched`, `round_won`. Put variants in properties, so one `quest_completed` with `quest = "dragon"` instead of `dragon_quest_completed`. Send numbers as numbers (`kills = 4`, not `"4"`) so you can sum and average them.

Set your own player properties, such as level or the team they picked:

```luau
RoChart.setUserProperties(player, { level = 12, class = "mage" })
```

## Economy, funnels and live config [#economy-funnels-and-live-config]

Record in-game currency the way Roblox's `AnalyticsService:LogEconomyEvent` does, so you can see where it comes from and where it goes:

```luau
RoChart.economy(player, { flow = "source", currency = "Gold", amount = 50, balance = gold, transactionType = "Gameplay", itemSku = "match_reward" })
RoChart.economy(player, { flow = "sink", currency = "Gold", amount = 300, balance = gold, transactionType = "Shop", itemSku = "artillery" })
```

Mark the steps of a funnel. A funnel the player can go through again, such as a shop visit, takes an id per visit:

```luau
RoChart.funnelStep(player, "onboarding", 2, "Picked country")

local visit = HttpService:GenerateGUID(false)
RoChart.funnelStep(player, "shop", 1, "Opened shop", visit)
RoChart.funnelStep(player, "shop", 2, "Viewed item", visit)
```

Tune the game without publishing: give a flag a JSON payload per variant and read it with `getConfig`:

```luau
local prices = RoChart.getConfig(player, "shop-prices") or { artillery = 300 }
```

## Player feedback [#player-feedback]

Send what players tell you to **Feedback**:

```luau
RoChart.feedback(player, message, { rating = 4, category = "bug", board = "bugs" })
```

Or turn on the built-in button with `feedbackButton = true` in `init`. It shows a small **Feedback** tab on the right edge of the screen with a 1–5 rating and a text box.

All functions are in the [SDK reference](/docs/reference/sdk#luau).

## Check that it works [#check-that-it-works]

Start a server and join. Open **Live**: you should see `$server_heartbeat` right away and your own `$session_start` within five seconds. If nothing arrives, check the server output (**Developer Console → Server** in a live game) for errors, that **Allow HTTP Requests** is on for the published place, and that the token is your project's.

## Websites, Discord bots and backends [#websites-discord-bots-and-backends]

The same project can take events from outside the game, such as your game's website, a Discord bot or a backend that grants rewards. Use the same `distinct_id` as the game, the player's UserId as a string, so it lands on the same player.

<Tabs items={['Browser', 'Node / Bun', 'HTTP']}>
<Tab value="Browser">

```html title="index.html"
<script src="https://app.rochart.co/sdk/rochart.min.js"></script>
<script>
  rochart.init('rc_your_project_token', { host: 'https://app.rochart.co' })
</script>
```

This captures pageviews, sessions, referrers and UTM campaigns. See the [SDK reference](/docs/reference/sdk#browser).

</Tab>
<Tab value="Node / Bun">

Download [`node.js`](https://app.rochart.co/sdk/node.js) into your project:

```ts title="bot.ts"
import { RoChart } from './vendor/rochart-node.js'

export const rochart = new RoChart({ token: process.env.ROCHART_TOKEN!, host: 'https://app.rochart.co' })

rochart.capture({ distinctId: String(robloxUserId), event: 'discord_linked', properties: { guild: 'main' } })
```

In scripts and serverless functions, `await rochart.shutdown()` before the process exits.

</Tab>
<Tab value="HTTP">

Send a JSON batch of up to 500 events to `POST /i/batch`:

```bash
curl -X POST https://app.rochart.co/i/batch \
  -H 'content-type: application/json' \
  -d '{
    "token": "rc_your_project_token",
    "batch": [{
      "uuid": "'"$(uuidgen | tr A-Z a-z)"'",
      "event": "discord_linked",
      "distinct_id": "123456789",
      "timestamp": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'",
      "properties": { "guild": "main" }
    }]
  }'
```

Generate a new `uuid` per event and reuse it on retries, so a retry never counts twice. Retry on `429`, `5xx` and network errors. All fields are in the [SDK reference](/docs/reference/sdk#http-endpoints).

</Tab>
</Tabs>
