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.

warningLuaudit check: client-reported-analytics

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 AnalyticsService call: LogOnboardingFunnelStepEvent, LogFunnelStepEvent, LogEconomyEvent, LogProgressionEvent and LogCustomEvent. 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 AnalyticsService are judged by the secrets-http checks.