Roblox
Contents
Note: This SDK is currently in beta. Please report any issues on GitHub.
This is the official PostHog SDK for Roblox. It's server-authoritative: because only the Roblox server can make outbound HTTP requests, the server owns all batching, identity, feature flags, error tracking, and HTTP. A thin client module relays capture calls and client errors to the server over a RemoteEvent.
Each player is a PostHog user, keyed by their stable Roblox UserId. The SDK keeps an in-memory event queue that's flushed on an interval and again on game:BindToClose(), which suits Roblox's ephemeral servers.
Installation
PostHog is available for Roblox through Wally (the Roblox package manager), or as a model file you import in Studio. The SDK is server-authoritative: the server owns all batching and HTTP, and each player is a PostHog user keyed by their Roblox UserId.
Requirements
- HTTP requests enabled for your experience: Game Settings > Security > Allow HTTP Requests. Only the Roblox server can make outbound HTTP requests, and only when this is on.
- A PostHog project API key (starts with
phc_).
Via Wally (recommended)
Add the dependency to your wally.toml:
Run wally install, then map the installed package into ReplicatedStorage in your Rojo project file so it replicates to clients:
Via model file
Download the latest posthog-roblox.rbxm from the releases page. In Studio, right-click ReplicatedStorage, choose Insert from File, and select the file. Make sure the inserted instance is named PostHog.
Either way, you should end up with ReplicatedStorage > PostHog, which both server and client code require.
Initialize on the server
Initialize PostHog once from a server Script (for example in ServerScriptService):
Note:
Initmust run on the server. Calling it from aLocalScriptis ignored. Requiring the same module from aLocalScriptgives you a thin client relay instead (see Capturing events).
Configuration options
Pass any of these to PostHog:Init. Only apiKey is required; everything else has a sensible default.
Capturing events
You can send custom events using capture:
On the server, every capture call takes a subject as its first argument so you can attribute the event to a specific player. Pass a Player and it resolves to their UserId:
Tip: We recommend naming events in an
[object] [verb]format, where[object]is the entity the behavior relates to, and[verb]is the behavior itself. For example,level completed,item purchased, orquest started.
The subject can be a Player, a numeric UserId, a string distinct ID, or nil for a server-scoped event that isn't attributed to any player:
Setting event properties
Optionally, you can include additional information with the event by including a properties object:
Capturing from the client
The same PostHog module works in a LocalScript, where it acts as a thin relay: calls are sent to the server over a RemoteEvent and attributed to the firing player. There's no API key on the client, and no subject argument:
The server validates, rate-limits, and attributes every relayed event, so a client can't spoof another player or send reserved ($-prefixed) event names.
Capturing screen views
Track which screens or menus a player views:
Identifying users
We highly recommend reading our section on Identifying users to better understand how to correctly use this method.
Every player already has a real identity, their Roblox UserId, so player events are attributed and create person profiles automatically. There's no anonymous-to-identified transition to manage and no reset to call. Use Identify to attach person properties to a player:
The first argument is a subject, so you can also identify by UserId or distinct ID. Pass a third argument to set properties only if they aren't already set ($set_once):
Anonymous vs identified events
PostHog captures two types of events: anonymous and identified. Identified events are attributed to a person and build a person profile; anonymous events are not.
On Roblox this is simpler than on most platforms. Every player already has a stable identity, their Roblox UserId, so events you capture for a player are identified and create a person profile automatically. There's no logged-out or guest state to reconcile, and no reset to call.
The events that stay anonymous are server-scoped events you capture with a nil subject. These describe the server or experience as a whole (for example server_started or shop_restocked) rather than any one player, so they don't create a person profile.
Tip: Anonymous events are cheaper to process than identified ones, so capture server-wide telemetry that isn't about a specific player with a
nilsubject rather than attributing it to an arbitrary player.
Controlling person profiles
By default, player events create person profiles and server-scoped events (those captured with a nil subject) do not. Change this with personProfiles:
Available options:
"identified_only"(default) – player events create profiles; server-scoped events don't."always"– every event creates or updates a person profile."never"– no event creates a person profile.
Super properties
Super properties are attached to every event the server sends. They're server-global: set once on the server, they apply to events from every player, which makes them a good fit for server-wide context like the place version or region.
Set them with Register, which takes a key and value:
Note: Super properties live in memory for the lifetime of the server. Unlike on persistent clients, they aren't stored across sessions, so register the ones you need during
Initor server startup.
Removing super properties
Group analytics
Group analytics lets you associate a player's events with a group (for example a guild, party, or server). Read the Group analytics guide for more information.
Note: This is a paid feature and is not available on the open-source or free cloud plan. Learn more on the pricing page.
- Associate a player's events with a group:
- Associate events with a group and update the group's properties:
The name is a special property used in the PostHog UI for the group's name. If you don't specify it, the group key is used instead.
Feature flags
PostHog's feature flags enable you to safely deploy and roll back new features as well as target specific users and groups with them.
Feature flags in the Roblox SDK are evaluated per player, so each flag call takes the player (or another subject) as its first argument. When preloadFeatureFlags is enabled (the default), a player's flags are fetched automatically when they join, so they're ready to read for the rest of the session.
Boolean feature flags
IsFeatureEnabled accepts an optional default that's returned when the flag hasn't loaded yet:
Multivariate feature flags
GetFeatureFlag returns a table with the flag's value, isEnabled, variant, and payload:
Feature flag payloads
Read the JSON payload attached to a flag with GetFeatureFlagPayload:
The payload is also available on the flag object returned by GetFeatureFlag:
Ensuring flags are loaded before use
When a player joins, the SDK fetches their flags in the background (unless you set preloadFeatureFlags = false). For most of a session the flags are available immediately, but the very first read right after a player joins can race the network request.
To force a fresh fetch and wait for it, call ReloadFeatureFlags. It yields until the request completes and returns whether it succeeded:
Overriding properties for flag evaluation
Set the person or group properties used to evaluate a player's flags. By default this reloads their flags; pass false as the final argument to skip the reload:
Experiments (A/B tests)
Since experiments use feature flags, the code for running an experiment is very similar to the feature flags code:
It's also possible to run experiments without feature flags.
Error tracking
The Roblox SDK captures unhandled errors automatically and sends them to PostHog as $exception events. This is enabled by default. On the server it listens to ScriptContext.Error; requiring the module on the client captures unhandled client errors and relays them to the server.
For the full setup guide, see the Roblox error tracking installation docs.
Manual exception capture
For handled errors you want to report, call CaptureException with a subject, a message, and an optional stack trace and properties:
From a LocalScript, the relay version takes no subject:
Configuration
To disable automatic capture, set captureErrors = false.
Autocapture
With captureLifecycleEvents enabled (the default), the SDK records a set of session and server events automatically, with no manual instrumentation. These give you concurrency, retention, and engagement metrics from the moment you initialize, so there's meaningful data to chart before you write a single custom event.
| Event | When it fires | Key properties |
|---|---|---|
server_started | The SDK initializes on a server | player_count, max_players, is_private_server, is_reserved_server |
server_shutdown | The server shuts down (game:BindToClose) | player_count, uptime_seconds |
player_joined | A player joins | is_teleport, player_count, account_age_days, membership_type, country_code |
player_left | A player leaves | session_duration_seconds, player_count, platform, country_code |
player_idle | Roblox reports a player has gone idle | idle_seconds |
Every event, autocaptured or manual, also carries standard context such as $session_id, $os, $device_type, place and server identifiers, and the SDK version.
To turn autocapture off:
Note:
platformis reported by the client relay, so onplayer_joined(which fires the instant a player connects) it's usually not known yet; it's populated by the timeplayer_leftfires.country_codeis resolved beforeplayer_joinedis sent.
Sessions and teleports
A session corresponds to a player's connection to a server, and every event carries its $session_id. Because experiences often teleport players between places, you can continue a player's session across a teleport so the whole journey is stitched together.
Merge the SDK's teleport data into your TeleportData before teleporting:
The destination server restores the session automatically when the player arrives, so their events keep the same $session_id.
The client relay
Requiring PostHog from a LocalScript gives you a thin relay rather than the full SDK. It can't make HTTP requests, so it forwards capture calls and client errors to the server over a RemoteEvent, where they're attributed to the firing player.
The client is treated as untrusted. The server validates every relayed message, caps the number of properties, rate-limits per player, blocks reserved ($-prefixed) event names, and always attributes events to the sender, so a client can never spoof another player or the server. Set enableClientRelay = false during Init if you only capture on the server and want no client surface at all.
Opt out of data capture
For privacy compliance, you can stop and resume capturing for a given subject at any time:
Debug mode
If you're not seeing the events or feature flags you expect, raise the log level to see what the SDK is doing in the output:
Available log levels: "none", "error", "warn", "info", "debug".
Manual flush
Events are sent in batches (every flushIntervalSeconds, or once flushAt events are queued). Force a send immediately, for example while testing:
This is asynchronous and doesn't yield. On the client, Flush is a no-op since flushing happens on the server.
Shutdown
The SDK flushes automatically on game:BindToClose(), so you rarely need to shut it down explicitly. If you want to stop autocapture, error tracking, and the relay, and flush early:
Troubleshooting
Events not appearing in PostHog
- HTTP is enabled. Turn on Game Settings > Security > Allow HTTP Requests. Without it the server can't send anything.
Initran on the server. It must be called from aScript, not aLocalScript. WithlogLevel = "debug"you should seePostHog initializedin the output.- The key and host are correct. The key starts with
phc_, and the host matches your region (https://us.i.posthog.comorhttps://eu.i.posthog.com). - You waited for a flush. Events batch every
flushIntervalSecondsor onceflushAtare queued. CallPostHog:Flush()to send immediately.
Client events not arriving
Client events are relayed to the server, so the server SDK must be initialized with enableClientRelay = true (the default). If the client logs that the relay RemoteEvent wasn't found, the server SDK isn't running.