Dashboard

Tutorial funnel example

Log each step of a first-time tutorial and see exactly where new players drop off.

Last updated Sep 29, 2026

This example records four steps of a tutorial. The event names match the built-in Tutorial activation funnel, so you get conversion numbers in Funnels as soon as players run it.

The funnel

StepEventRecorded whenShown in the dashboard as
1session_startedA player joins (automatic)Joined game
2stage_startedThey touch the part with Step 1Started tutorial
3checkpoint_reachedThey touch the part with Step 2Reached checkpoint
4stage_completedThey touch the part with Step 3Completed tutorial

Set up the parts in Studio

  1. Add three Parts along your tutorial route, in the order players meet them. Turn on Anchored and turn off CanCollide.
  2. Open View → Tag Editor and give each part the tag TutorialStep.
  3. Add a number attribute called Step to each part: 1, 2 and 3.

The script

Create a server Script in ServerScriptService. It assumes you followed the Quick start, including the RoStructureSecrets module in ServerStorage.

ServerScriptService/TutorialAnalytics.server.luau
local CollectionService = game:GetService("CollectionService")
local Players = game:GetService("Players")
local ServerScriptService = game:GetService("ServerScriptService")
local ServerStorage = game:GetService("ServerStorage")

local RoStructure = require(ServerScriptService.RoStructure)
local secrets = require(ServerStorage.RoStructureSecrets)

local analytics = RoStructure.init({
	projectId = "my_game",
	ingestionKey = secrets.ingestionKey,
	buildId = "v" .. game.PlaceVersion,
	endpoint = "https://api.rostructure.com",
})

local TUTORIAL_STAGE = "tutorial"
local NEXT_STAGE = "stage_1"

-- Funnel steps after "session_started", which the SDK records when a player joins.
-- The event names match the built-in "Tutorial activation" funnel.
local STEPS = {
	{ event = "stage_started", label = "Started tutorial" },
	{ event = "checkpoint_reached", label = "Reached checkpoint" },
	{ event = "stage_completed", label = "Completed tutorial" },
}

-- The last step each player has completed. 0 means none yet.
local progress: { [Player]: number } = {}

local function playerFromHit(hit: BasePart): Player?
	local model = hit:FindFirstAncestorOfClass("Model")
	return model and Players:GetPlayerFromCharacter(model) or nil
end

local function complete(player: Player, step: number)
	-- The server decides: a step counts once, and only after the step before it.
	if progress[player] ~= step - 1 then
		return
	end
	progress[player] = step

	local definition = STEPS[step]
	analytics:track(player, definition.event, {
		stageId = TUTORIAL_STAGE,
		position = true,
		metadata = { step = step, label = definition.label },
	})

	if step == #STEPS then
		-- Later events, including deaths, now belong to the next stage.
		analytics:setStage(player, NEXT_STAGE)
	end
end

local function bindStepPart(part: Instance)
	if not part:IsA("BasePart") then
		return
	end
	local step = part:GetAttribute("Step")
	if type(step) ~= "number" or STEPS[step] == nil then
		warn("Tutorial part " .. part:GetFullName() .. " needs a Step attribute from 1 to " .. #STEPS)
		return
	end
	part.Touched:Connect(function(hit)
		local player = playerFromHit(hit)
		if player then
			complete(player, step)
		end
	end)
end

for _, part in CollectionService:GetTagged("TutorialStep") do
	bindStepPart(part)
end
CollectionService:GetInstanceAddedSignal("TutorialStep"):Connect(bindStepPart)

local function onPlayerAdded(player: Player)
	progress[player] = 0
	-- Deaths and quits before the tutorial is finished are attributed to it.
	analytics:setStage(player, TUTORIAL_STAGE)
end

Players.PlayerAdded:Connect(onPlayerAdded)
for _, player in Players:GetPlayers() do
	onPlayerAdded(player)
end

Players.PlayerRemoving:Connect(function(player)
	progress[player] = nil
end)

Read the funnel

  • Open Funnels and choose Tutorial activation. Each card shows how many sessions reached that step and how many dropped off before the next one.
  • Biggest leak names the step where the most players stop. Click any step to see conversion, the median time from the previous step and which device drops off most.
  • In a step's panel choose View drop-offs spatially to open those sessions on the map. Their last recorded positions come from position = true in the script.
  • A drop-off means the next step was not reached within the conversion window (30 minutes). It does not mean the player left the game.

How it works

  • The server decides. complete ignores a step that was already counted or arrives out of order, so a player cannot skip ahead or count twice.
  • Stages. setStage attributes a player's later events, including deaths and quits, to the tutorial until it is finished, then to stage_1.
  • Cleanup. Per-player state is removed on PlayerRemoving.
  • Your own funnel. Keep the event names to match the preset, or use any names and build a funnel with your own steps in the Funnels page.