Data and Security
StatisticsService
Analytics for your experience. Roblox Analytics events with rate limiting, funnels, counters and timings, automatic session, purchase and error tracking, Discord webhooks, custom sinks and daily totals.
Getting it
local StatisticsService = Engine:GetService("StatisticsService")Overview
StatisticsService is one place to record what happens in your game and send it wherever it needs to go:
- Roblox Analytics: economy, funnel, onboarding, progression and custom events go to
AnalyticsServicethrough a queue that respects Roblox's rate limits, so events are not silently dropped during busy moments. - Counters, gauges and timings for live numbers such as "rounds played" or "average round length".
- Automatic tracking of sessions (join, leave, play time), purchases, server start and close, and every server script error.
- Discord webhooks that post selected events, batched and rate-limited, or periodic digests.
- Custom sinks: send batches of events to your own function.
- Persistence (optional): daily and lifetime totals saved through DataService.
Every recorded event fires EventLogged, so you can also react to them in code.
local StatisticsService = Engine:GetService("StatisticsService")
-- Economy: the player earned 50 coins from a quest
StatisticsService:LogEconomyEvent(player, "Source", "Coins", 50, newBalance, "Gameplay", "Quest_Forest")
-- Progression: the player finished level 3
StatisticsService:LogProgressionEvent(player, "Campaign", "Complete", 3, "Forest Temple")
-- Live counter
StatisticsService:Increment("RoundsPlayed")
-- Error alerts in Discord
StatisticsService:CreateWebhook("Alerts", DISCORD_WEBHOOK_URL, { Kinds = { "Error" } })Roblox Analytics events
These map to Roblox's AnalyticsService events and also go to webhooks, sinks and counters.
LogEconomyEvent
StatisticsService:LogEconomyEvent(player, flow: "Source" | "Sink", currency: string, amount: number, endingBalance: number, transactionType: string, itemSku: string?, fields?): Event?Money in ("Source") or out ("Sink"). transactionType is a Roblox transaction type name such as "Gameplay", "Shop", "IAP" or "ContextualPurchase" (or the enum).
LogProgressionEvent
StatisticsService:LogProgressionEvent(player, pathName: string, status: "Start" | "Complete" | "Fail", level: number, levelName: string?, fields?): Event?LogFunnelStepEvent / LogOnboardingFunnelStepEvent
StatisticsService:LogFunnelStepEvent(player, funnelName: string, step: number | string, stepName: string?, fields?): boolean
StatisticsService:LogOnboardingFunnelStepEvent(player, step: number, stepName: string?, fields?): booleanSteps of a funnel, such as a shop flow or the tutorial. Steps are logged once per funnel session. See Funnels.
LogPurchaseEvent
StatisticsService:LogPurchaseEvent(player, purchaseType: "Product" | "GamePass" | "Asset" | "Plus" | "Subscription", itemId, robux: number?, fields?): Event?MonetizationService calls this for you when LogToStatistics is on.
LogCustomEvent
StatisticsService:LogCustomEvent(player: Player?, name: string, value: number?, fields?): Event?Custom fields
Roblox events carry up to three custom fields. Engine events can carry up to MaxFieldCount named fields; map the ones Roblox should receive to its three slots:
| Method | Description |
|---|---|
:DefineCustomField(name, slot?) | Maps a field name to CustomField01..03 and returns the slot. |
:GetCustomFields() | The current mapping. |
:SetPlayerFields(player, fields) | Fields added to every event about that player, such as their platform or A/B test group. |
:GetPlayerFields(player) |
Field values are strings, numbers or booleans; long values are cut to MaxFieldLength.
Funnels
StatisticsService:CreateFunnel(name: string, steps: { string }?, options: { Onboarding?, RestartOnFirstStep? }?): FunnelA funnel object tracks each player's progress through named steps and gives every attempt a session id, which Roblox needs to connect the steps.
local shop = StatisticsService:CreateFunnel("Shop", { "Opened", "ViewedItem", "Purchased" }, { RestartOnFirstStep = true })
shop:Step(player, "Opened")
shop:Step(player, "ViewedItem", { Item = "Sword" })| Member | Description |
|---|---|
:Step(player, step, fields?, stepName?): boolean | Logs a step (by number or name). false when it was already logged in this session. |
:Restart(player) | Starts a new attempt and returns its session id. |
:GetSessionId(player), :GetProgress(player), :HasLogged(player, step) | |
:Forget(player), :Destroy() | |
StepLogged: Signal<(player, step, stepName?)> | |
Name, Steps, IsOnboarding | Read-only. |
:GetFunnel(name), :FindFunnel(name) and :GetFunnels() look funnels up.
Counters
Quick numbers kept in memory (and in the persisted totals when enabled).
| Method | Description |
|---|---|
:Increment(name, delta?) | Adds to a counter. |
:Gauge(name, value) | Sets a value that goes up and down, such as players in queue. |
:Timing(name, seconds) | Records a duration; kept as count, sum, min and max. |
:GetCounter(name), :GetGauge(name) | |
:GetSnapshot() | Everything: counters, gauges, timings, events per name, economy totals per currency, funnel step counts, errors and session totals. |
Each map keeps at most MaxKeysPerMap names; additional names are counted under "(other)".
Automatic tracking
| Setting | Records |
|---|---|
TrackSessions | A Session event when a player joins and leaves, with play time. |
TrackPurchases | A Purchase event when a game pass, asset, Premium or subscription prompt finishes with a purchase. |
TrackServer | Server events on start and close, and gauges Players, MemoryMb and HeartbeatMs every ServerSampleSeconds. |
CaptureErrors | An Error event for every server script error. The same message is folded together for ErrorDedupeSeconds; messages matching ErrorIgnorePatterns are skipped. |
| Method | Description |
|---|---|
:ReportError(message, stack?, scriptName?) | Report an error yourself, such as a caught pcall failure. |
:GetTopErrors(limit?, windowSeconds?) | The most frequent errors: { Key, Message, Stack, Script?, Count, FirstAt, LastAt }. |
Discord webhooks
CreateWebhook
StatisticsService:CreateWebhook(name: string, url: string, options: WebhookOptions?): Webhook| Option | Description |
|---|---|
Kinds | Event kinds to post automatically, e.g. { "Error", "Purchase" }. |
Names | Only events with these names. |
Username, AvatarUrl | How the messages appear. |
BatchSeconds | Collect events and post them together. |
Digest | { IntervalSeconds, Title? }: post a summary (players, uptime, counters, top errors) on an interval. |
Enabled |
| Webhook member | Description |
|---|---|
:Send(content?, embeds?), :SendEmbed(embed), :SendAsync(...) | Post your own message. |
:SendDigest(), :Flush(), :Destroy() | |
OnFormat | Set a function (event) -> Embed? to change how events look, or return nil to skip one. |
Sent, Failed | Signals. |
Enabled, QueuedCount, SentCount, FailedCount |
An Embed is { Title?, Description?, Url?, Color?, Fields?, Footer?, Timestamp?, Thumbnail?, Image? } with fields { Name, Value, Inline? }.
Note
Webhooks need Allow HTTP Requests in Game Settings. Discord blocks requests coming from Roblox game servers, so set DiscordProxyHost to a webhook proxy you trust; Engine then sends every webhook through that host. Keep webhook URLs in server-side configuration (Webhooks = { Name = { Url = "https://..." } }) and never in client scripts.
Messages are rate-limited per webhook, retried on network errors, and Discord's Retry-After is respected.
Custom sinks
StatisticsService:CreateSink(name: string, handler: (events: { Event }) -> (), options: SinkOptions?): SinkCalls your handler with batches of events (BatchSize events, or whatever arrived within FlushSeconds), filtered by Kinds and Names. Use it to forward events to your own backend through HttpService. A Sink has :Flush(), :Destroy(), Enabled, QueuedCount, DeliveredCount, DroppedCount and a Delivered signal.
Persistence
With DataStoreEnabled = true, counters, events, economy totals, funnels and (optionally) errors are added to a DataStore per UTC day and in a lifetime total every DataStoreFlushSeconds. Totals from all servers are combined.
| Method | Description |
|---|---|
:FlushAsync() | Write pending totals now. |
:GetDailyStatsAsync(date?) | One day ("YYYY-MM-DD", default today). |
:GetLifetimeStatsAsync() | All time. |
:GetRecentDaysAsync(days?) | The last N days (at most DataStoreMaxDaysRead). |
Flushed: Signal | After each write. |
Events and properties
| Member | Description |
|---|---|
EventLogged: Signal<Event> | Every recorded event. |
SinkFailed: Signal<(sink, reason, event?)> | Roblox Analytics, a webhook, the DataStore or a custom sink failed. |
Enabled | Master switch. |
RobloxAnalyticsEnabled, CaptureErrors | Settable switches. |
DataStoreEnabled, QueuedRobloxEventCount, DroppedRobloxEventCount, SentRobloxEventCount, TrackedErrorCount, Uptime, ServerId | Read-only. |
An Event is { Id, Kind, Name, Time, Clock, Player?, UserId?, PlayerName?, Value?, Fields, Data, PlaceId, JobId }. Kinds: Custom, Economy, Funnel, Onboarding, Progression, Purchase, Session, Error, Server.
On shutdown the service waits up to ShutdownFlushSeconds for the DataStore, webhooks and sinks to finish.
Configuration
DefaultConfiguration/Services/StatisticsService43 settings| Setting | Default | What it does |
|---|---|---|
| Enabled | true | Master switch; off means nothing is recorded, queued or sent anywhere. |
| MaxNameLength | 50 | Engine cap on event/funnel/counter/currency names; longer names error at the call. |
| MaxFieldLength | 100 | Engine cap on one custom field value; longer values are cut. |
| MaxFieldCount | 10 | Custom fields one event may carry; Roblox itself only receives the three mapped slots. |
| WarnOnSinkFailure | true | Also warn() when a sink (Roblox, Discord, DataStore, custom) fails, next to firing SinkFailed. |
| RobloxAnalyticsEnabled | true | Forward Economy/Funnel/Onboarding/Progression/Custom events to AnalyticsService. |
| RobloxEventsPerMinute | 120 | Ceiling on AnalyticsService calls one server makes per minute; the queue drains at this pace. |
| RobloxEventsPerPlayerPerMinute | 20 | Ceiling on AnalyticsService calls about one player per minute. |
| RobloxQueueSize | 500 | Events queued for AnalyticsService before the oldest are dropped. |
| CustomFields | {} | Field names mapped to Roblox's CustomField01..03 slots in order; :DefineCustomField adds more at runtime. |
| WarnOnUnmappedField | false | Warn once per field name that reaches a Roblox event without a mapped slot. |
| MaxKeysPerMap | 200 | Distinct names one counter map keeps before extra names fold into "(other)". |
| DataStoreEnabled | false | Persist aggregated counters to a DataStore (per UTC day + lifetime) through DataService. |
| DataStoreName | "Engine.Statistics" | DataStore the aggregates live in. |
| DataStoreScope | nil | Optional DataStore scope. |
| DataStoreFlushSeconds | 60 | Seconds between flushes of pending counter deltas to the DataStore. |
| DataStoreDayKeyFormat | "Day_%s" | string.format pattern turning a "YYYY-MM-DD" UTC date into a day key. |
| DataStoreLifetimeKey | "Lifetime" | Key the all-time aggregate lives under. |
| DataStoreMaxDaysRead | 31 | Ceiling on days :GetRecentDaysAsync walks (one GetAsync each). |
| DataStorePersistErrors | true | Include per-error counts in the persisted aggregates. |
| Webhooks | {} | Discord webhooks created at load, keyed by name. |
| DiscordProxyHost | "" | Host of a webhook proxy; when set, every webhook URL's host is swapped for it. |
| DiscordUsername | "Engine" | Default username webhook messages are posted under; a webhook's own Username wins. |
| DiscordBatchSeconds | 2 | Seconds a webhook batches auto-posted events before sending them as one message; 0 sends each at once. |
| DiscordRequestsPerSecond | 1 | Sustained HTTP requests one webhook may make per second. |
| DiscordBurst | 5 | Burst of requests one webhook may make at once. |
| DiscordQueueSize | 100 | Messages waiting to be sent per webhook before the oldest are dropped. |
| DiscordRetryAttempts | 3 | Attempts one message gets on network/5xx failures before it is dropped and Failed fires. |
| DiscordRetryBaseSeconds | 1 | Seconds before the first retry; doubles per attempt with jitter. |
| DiscordRetryMaxSeconds | 30 | Cap on the retry delay and on how long a 429 Retry-After is honoured. |
| DiscordEmbedColors | { Custom = 0x5865F2, Economy = 0x57F287, Funnel = 0xFEE75C, Onboarding = 0xFEE75C, Progression = 0xEB459E, Purchase = 0x57F287, Session = 0x99AAB5, Error = 0xED4245, Server = 0x5865F2, } | Left-border colour per event kind for the default embed (0xRRGGBB). |
| SinkBatchSize | 50 | Events a custom sink collects before its handler is called, when BatchSize is omitted. |
| SinkFlushSeconds | 5 | Seconds a custom sink waits before calling its handler with whatever it has, when FlushSeconds is omitted. |
| SinkQueueSize | 1000 | Events one custom sink may hold before the oldest are dropped. |
| CaptureErrors | true | Turn every server script error (ScriptContext.Error) into an Error event. |
| ErrorDedupeSeconds | 60 | Seconds the same error message is folded into one Error event (its Count carries the repeats). |
| MaxTrackedErrors | 200 | Distinct error messages remembered for :GetTopErrors; the least recent are forgotten first. |
| ErrorIgnorePatterns | {} | Lua patterns; an error message matching any of them is ignored entirely. |
| TrackSessions | true | Emit Session events ("Join"/"Leave" with play time) and keep the Sessions totals. |
| TrackPurchases | true | Emit Purchase events for finished game pass / asset / premium / subscription prompts. |
| TrackServer | true | Emit Server events ("Start"/"Close") and sample the gauges below. |
| ServerSampleSeconds | 60 | Seconds between samples of the Players / MemoryMb / HeartbeatMs gauges. |
| ShutdownFlushSeconds | 10 | Seconds BindToClose waits for the DataStore flush, Discord queues and custom sinks to drain. |
Types
export type Event
export type EventKind
export type Fields = { [string]: string | number | boolean }
export type Funnel
export type FunnelOptions
export type Webhook
export type WebhookOptions
export type Embed
export type EmbedField
export type Sink
export type SinkOptions
export type Snapshot
export type Aggregate = { Count: number, Sum: number, Min: number, Max: number }
export type ErrorRecord
export type PurchaseType = "Product" | "GamePass" | "Asset" | "Plus" | "Subscription"
export type EconomyFlow = "Source" | "Sink"
export type ProgressionStatus = "Start" | "Complete" | "Fail"