How to keep analytics data clean from exploiters
Logging funnel steps and economy amounts the client reports lets exploiters fill your dashboards with false data. How to check events on the server.
What it looks like
AnalyticsService events can only be sent from the server, so the client fires a remote and the server logs whatever it was told: a funnel step, an economy amount, a custom event name or custom fields.
Luau
remotes.TutorialStep.OnServerEvent:Connect(function(player, step, name) AnalyticsService:LogOnboardingFunnelStepEvent(player, step, name) end)
How to detect it
- Find every
AnalyticsServicecall:LogOnboardingFunnelStepEvent,LogFunnelStepEvent,LogEconomyEvent,LogProgressionEventandLogCustomEvent. Trace each argument back to its source. - Roblox's funnel events guide says to validate data on the server before logging, so exploiters can't send invalid data to your analytics. Its example checks the step number before logging an onboarding step.
- Step numbers from the client must be whole numbers in the funnel's range. Better still, the server should work out the step from what it saw the player do.
- Names, item SKUs and custom field values from the client must come from a fixed list. Free text fills reports with junk, and the client can put anything in it.
- Economy events should log the amounts the server actually added or spent, and the balance it holds, never figures sent by the client.
- A remote that only feeds analytics still needs a rate limit. See Remote with no rate limit.
What an exploiter could do
They send made-up steps, amounts and names. Funnels show steps nobody reached, economy reports show currency that never existed, and custom fields fill with whatever text they choose. The developer then makes design and pricing decisions from false data. Nothing in the game changes, which is why this is a warning.
How to fix it
- Log analytics from the server code that makes the change, using the server's own values.
- Where a client event is the only signal, such as closing a tutorial page, accept only a fixed set of values and check them.
- Log each funnel step once per player, in order.
Vulnerable example
Vulnerable
local AnalyticsService = game:GetService("AnalyticsService") local ReplicatedStorage = game:GetService("ReplicatedStorage") ReplicatedStorage.Remotes.TutorialStep.OnServerEvent:Connect(function(player: Player, step: unknown, stepName: unknown) if typeof(step) == "number" and typeof(stepName) == "string" then AnalyticsService:LogOnboardingFunnelStepEvent(player, step, stepName) end end)
Fixed example
Fixed
local AnalyticsService = game:GetService("AnalyticsService") local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local steps = { "Spawned", "Moved", "Equipped tool", "Finished tutorial" } local reached: { [Player]: number } = {} ReplicatedStorage.Remotes.TutorialStep.OnServerEvent:Connect(function(player: Player, step: unknown) if typeof(step) ~= "number" or step ~= (reached[player] or 0) + 1 or step > #steps then return end reached[player] = step AnalyticsService:LogOnboardingFunnelStepEvent(player, step, steps[step]) end) Players.PlayerRemoving:Connect(function(player) reached[player] = nil end)
Common false positives
- Events logged from server-side changes, such as an economy event inside
ProcessReceipt, are already trustworthy. - A client value checked against a fixed list or range before logging, like the fixed example, is the fix.
- Analytics sent to your own web service rather than
AnalyticsServiceare judged by thesecrets-httpchecks.