Skip to content
Engine

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.

ServiceServer

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 AnalyticsService through 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?): boolean

Steps 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:

MethodDescription
: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? }?): Funnel

A 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" })
MemberDescription
:Step(player, step, fields?, stepName?): booleanLogs 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, IsOnboardingRead-only.

:GetFunnel(name), :FindFunnel(name) and :GetFunnels() look funnels up.

Counters

Quick numbers kept in memory (and in the persisted totals when enabled).

MethodDescription
: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

SettingRecords
TrackSessionsA Session event when a player joins and leaves, with play time.
TrackPurchasesA Purchase event when a game pass, asset, Premium or subscription prompt finishes with a purchase.
TrackServerServer events on start and close, and gauges Players, MemoryMb and HeartbeatMs every ServerSampleSeconds.
CaptureErrorsAn Error event for every server script error. The same message is folded together for ErrorDedupeSeconds; messages matching ErrorIgnorePatterns are skipped.
MethodDescription
: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
OptionDescription
KindsEvent kinds to post automatically, e.g. { "Error", "Purchase" }.
NamesOnly events with these names.
Username, AvatarUrlHow the messages appear.
BatchSecondsCollect events and post them together.
Digest{ IntervalSeconds, Title? }: post a summary (players, uptime, counters, top errors) on an interval.
Enabled
Webhook memberDescription
:Send(content?, embeds?), :SendEmbed(embed), :SendAsync(...)Post your own message.
:SendDigest(), :Flush(), :Destroy()
OnFormatSet a function (event) -> Embed? to change how events look, or return nil to skip one.
Sent, FailedSignals.
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?): Sink

Calls 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.

MethodDescription
: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: SignalAfter each write.

Events and properties

MemberDescription
EventLogged: Signal<Event>Every recorded event.
SinkFailed: Signal<(sink, reason, event?)>Roblox Analytics, a webhook, the DataStore or a custom sink failed.
EnabledMaster switch.
RobloxAnalyticsEnabled, CaptureErrorsSettable switches.
DataStoreEnabled, QueuedRobloxEventCount, DroppedRobloxEventCount, SentRobloxEventCount, TrackedErrorCount, Uptime, ServerIdRead-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

Defaults from DefaultConfiguration/Services/StatisticsService43 settings
SettingDefaultWhat it does
EnabledtrueMaster switch; off means nothing is recorded, queued or sent anywhere.
MaxNameLength50Engine cap on event/funnel/counter/currency names; longer names error at the call.
MaxFieldLength100Engine cap on one custom field value; longer values are cut.
MaxFieldCount10Custom fields one event may carry; Roblox itself only receives the three mapped slots.
WarnOnSinkFailuretrueAlso warn() when a sink (Roblox, Discord, DataStore, custom) fails, next to firing SinkFailed.
RobloxAnalyticsEnabledtrueForward Economy/Funnel/Onboarding/Progression/Custom events to AnalyticsService.
RobloxEventsPerMinute120Ceiling on AnalyticsService calls one server makes per minute; the queue drains at this pace.
RobloxEventsPerPlayerPerMinute20Ceiling on AnalyticsService calls about one player per minute.
RobloxQueueSize500Events 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.
WarnOnUnmappedFieldfalseWarn once per field name that reaches a Roblox event without a mapped slot.
MaxKeysPerMap200Distinct names one counter map keeps before extra names fold into "(other)".
DataStoreEnabledfalsePersist aggregated counters to a DataStore (per UTC day + lifetime) through DataService.
DataStoreName"Engine.Statistics"DataStore the aggregates live in.
DataStoreScopenilOptional DataStore scope.
DataStoreFlushSeconds60Seconds 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.
DataStoreMaxDaysRead31Ceiling on days :GetRecentDaysAsync walks (one GetAsync each).
DataStorePersistErrorstrueInclude 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.
DiscordBatchSeconds2Seconds a webhook batches auto-posted events before sending them as one message; 0 sends each at once.
DiscordRequestsPerSecond1Sustained HTTP requests one webhook may make per second.
DiscordBurst5Burst of requests one webhook may make at once.
DiscordQueueSize100Messages waiting to be sent per webhook before the oldest are dropped.
DiscordRetryAttempts3Attempts one message gets on network/5xx failures before it is dropped and Failed fires.
DiscordRetryBaseSeconds1Seconds before the first retry; doubles per attempt with jitter.
DiscordRetryMaxSeconds30Cap 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).
SinkBatchSize50Events a custom sink collects before its handler is called, when BatchSize is omitted.
SinkFlushSeconds5Seconds a custom sink waits before calling its handler with whatever it has, when FlushSeconds is omitted.
SinkQueueSize1000Events one custom sink may hold before the oldest are dropped.
CaptureErrorstrueTurn every server script error (ScriptContext.Error) into an Error event.
ErrorDedupeSeconds60Seconds the same error message is folded into one Error event (its Count carries the repeats).
MaxTrackedErrors200Distinct error messages remembered for :GetTopErrors; the least recent are forgotten first.
ErrorIgnorePatterns{}Lua patterns; an error message matching any of them is ignored entirely.
TrackSessionstrueEmit Session events ("Join"/"Leave" with play time) and keep the Sessions totals.
TrackPurchasestrueEmit Purchase events for finished game pass / asset / premium / subscription prompts.
TrackServertrueEmit Server events ("Start"/"Close") and sample the gauges below.
ServerSampleSeconds60Seconds between samples of the Players / MemoryMb / HeartbeatMs gauges.
ShutdownFlushSeconds10Seconds 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"