Collecting Your Historical Visits/Revenue Data from Developer Stats

Roblox has been working on phasing out the legacy Developer Stats page for some time now, with the recent upcoming removal being the exportable Game Stats sheets. Robux Revenue has also been removed from the page, and I didn’t get the time to collect this information on some of my other games. There is also an issue where you can only receive the last four years worth of Visits/Revenue data, and many of my games are older than that.

While this feature request isn’t guaranteed to work forever if they remove the endpoints in the future, which I assume they will at some point, I hope this can serve use to anyone who is in a similar position to me, as I was looking through the API and it turns out they do keep all of this historical data, and you can access any of it! I have used this method to get historical data for my other games, as you can see below sorted by month, for data inaccessible otherwise, six years old!

image


How to access historical Developer Stats Data

There is an API page for this, but I’ll be going over how to read and access it from the direct url.

Let’s start with getting the link for your place, and understanding each parameter.

The base of your link will look like this: https://develop.roblox.com/v1/places/123456789/stats/type?granularity=Monthly

  • 123456789” should be the Place Id that you are going to use. Note that universe Ids do not work, and it must be a place, most likely being your start place.

  • "type" can be either “Visits”, “Revenue”, “RevenuePerVisit”, or “AverageVisitLength”, depending on which type of data you want to view.

  • granularity” is a parameter that specifies how it’s aggregated - this can be Hourly, Daily, or Monthly, broken down into a maximum of the last 48 hours, ~1 1/2 months, and 4 years respectively.

Optional Parameters

You can also add optional parameters to this link. The two you can use are “divisionType” and “startTime” and “endTime”. Make sure to format each new parameter like &name=value.

  • divisionType” can be either “Device” or “Age”, and will divide stats by those demographics, just like on Developer Stats.
    image

  • startTime” and “endTime” should be used together, and is how you access historical data, especially monthly, from over 4 years ago.


Collecting this information

Using this information, you can access both old data not available on the Developer Stats page, as well as historical Revenue data.

As an example, the link below will give me a json table for my placeId 380305410, with data on Visits, aggregated by Daily, divided by Device type, from January 2017 to February 2017.
https://develop.roblox.com/v1/places/380305410/stats/Visits?granularity=Daily&divisionType=Device&startTime=2017-01-01&endTime=2017-02-01

Obviously this can be tedious to copy information from for your own records, so I highly recommend either using the formatted response in the API page, or using an extension to beautify API responses, which is a life saver. If you need a higher resolution, I recommend writing a script or program to export this data.

A final thing, is that the data has the key as the Unix millisecond timestamp of the data, and the value is the data. Use a website like unix timestamp to decipher which unix timestamp is when in a readable format, as it’s not easily readable to the human eyes like it is when presented in chart form, and do note that some empty values may not be recorded, so watch out for gaps!

image

image

As you can tell, I had collected 5,598 visits on January 1st. Do remember that you can switch Daily to Monthly to see the visits for that entire month, not just individual days!


I hope this feature request can help other fellow developers who are missing complete historical data for their projects. If you have any questions, please feel free to reply to this post and I can try to help you out. Thank you!

15 Likes

Just wanted to bump this for a final time since Developer Stats will be sunset as a whole by January 10, 2024, so if you wanted to collect any of your game’s historical data (especially for those older than 4 years), now is the time to do so.

As far as my knowledge goes, you should be able to collect any data since your game was created, so daily visits/revenue from March 15 2015 should work, but it’s highly recommended to create a script to automate exporting this data, especially if you plan to do anything more precise than Monthly. I did this in Studio by authenticating one of my alts that have access to my group’s analytics data.

I have no clue if these endpoints will still be accessible after Jan 10th or not, but it’s better to be safe than sorry.

6 Likes

Thanks for providing this resource. It helped a lot and saved me a lot of time :slight_smile:

Roblox should make the other statistics available for more than a year. It’s sad to lose so much interesting data every day.

2 Likes

Thanks for posting this resource!

Do you have happen to have a script you could share that would assist in automating the data export? If so, I would be extremely grateful! I don’t have programming skills, and it would be tedious to do manually.

1 Like

I have a script I made a while back that prints them in the Studio output with CSV format. It might be a little wonky but it worked for me.

Unfortunately for these scripts I made you do need to verify with your Roblox cookie through a public proxy, hence why I made a throwaway account with analytics permissions, so in the event it’s used maliciously, little harm would be done. I highly recommend you do the same as well as public proxies aren’t guaranteed to be safe.

Both scripts require this ‘Proxy’ module, so as long as you have some knowledge to tweak values and paths you should be able to figure it out hopefully!

Proxy

--> Services
local HttpService = game:GetService("HttpService")

--> Configuration
local Cookie = "PASTE-COOKIE-HERE-JUST-THE-LETTERS"

--> Variables
local Headers = {
	Cookie = `.ROBLOSECURITY={Cookie}`
}

--------------------------------------------------------------------------------

-- 401 = Invalid Cookie
-- 403 = Token Validation Failure

local function Reauthenticate()
	while true do
		local Success, Response = xpcall(function()
			return HttpService:RequestAsync({
				Method = "POST",
				Url = `https://auth.roproxy.com`,
				Headers = Headers
			})
		end, warn)
		
		if Success and Response.StatusCode == 403 then
			print("Authenticated!")
			Headers["x-csrf-token"] = Response.Headers["x-csrf-token"]
			break
		else
			warn("Unknown error occurred. Retrying in 3 seconds.")
			warn(Response)
			task.wait(3)
		end
	end
end

---- EXPOSED API ---------------------------------------------------------------

local Proxy = {}

function Proxy:MakeRequest(Method: string?, Url: string)
	while true do
		local Success, Response = xpcall(function()
			return HttpService:RequestAsync({
				Method = Method,
				Url = Url,
				Headers = Headers
			})
		end, warn)
		
		if not Success or not Response.Success then
			if Response.StatusCode == 403 then
				print("Request doesn't have a valid token! Authenticating...")
				Reauthenticate()
				task.wait(1)
			else
				warn("Unknown error occurred. Retrying in 3 seconds.")
				print(Response.Body)
				task.wait(3)
			end
		else
			return Response.Body
		end
	end
end

return Proxy

Get Legacy Developer Stats (Division: Age)

Summary
--> Services
local ServerStorage = game:GetService("ServerStorage")
local HttpService = game:GetService("HttpService")

--> Dependencies
local Proxy = require(game:GetService("ServerStorage").Proxy)

--> Configuration
local placeId = 380305410

--------------------------------------------------------------------------------

local Months = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
local function GetMonthFromIsoDate(IsoDate: string): number
	return DateTime.fromIsoDate(IsoDate):ToUniversalTime().Month
end

local function GetPage(Start: DateTime, End: DateTime)
	local Response = Proxy:MakeRequest("GET", `https://develop.roproxy.com/v1/places/{placeId}/stats/Visits?granularity=Daily&divisionType=Age&startTime={Start:ToIsoDate()}&endTime={End:ToIsoDate()}`)
	return HttpService:JSONDecode(Response)
end

local function GetValue(t, isoDate: string): number|string
	if t then
		for _, Value in t do
			if Value[1] == isoDate then
				return Value[2]
			end
		end
	end
	return ""
end

local LastDay = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}

for _, Year in {2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023} do
	for Month = 1, 12 do
		local isLeapMonth = (Month == 2) and (Year % 4 == 0)
		local Start = DateTime.fromUniversalTime(Year, Month) 
		local End = DateTime.fromUniversalTime(Year, Month, LastDay[Month] + (isLeapMonth and 1 or 0))
		local Page = GetPage(Start, End)
		
		warn(`{Start:ToIsoDate()} ::: {End:ToIsoDate()}`)
		
		-- Sort data into arrays
		local arrays = {}
		for _, Category in Page.data do
			local t = {}
			for date, value in Category.data do
				table.insert(t, {DateTime.fromUnixTimestampMillis(tonumber(date)):ToIsoDate(), value})
			end
			table.sort(t, function(A, B)
				return A[1] < B[1]
			end)
			arrays[Category.type] = t
		end
		
		-- Log
		for n, totalData in arrays.Total do
			local IsoDate = totalData[1]
			local placeId = Page.placeId
			local Total = arrays.Total[n][2]
			local SixOrUnder = GetValue(arrays.SixOrUnder, IsoDate)
			local SevenAndEight = GetValue(arrays.SevenAndEight, IsoDate)
			local NineAndTen = GetValue(arrays.NineAndTen, IsoDate)
			local ElevenAndTwelve = GetValue(arrays.ElevenAndTwelve, IsoDate)
			local ThirteenToFifteen = GetValue(arrays.ThirteenToFifteen, IsoDate)
			local SixteenAndSeventeen = GetValue(arrays.SixteenAndSeventeen, IsoDate)
			local EighteenOrOver = GetValue(arrays.EighteenOrOver, IsoDate)
			
			print(`{IsoDate},{placeId},{Total},{SixOrUnder},{SevenAndEight},{NineAndTen},{ElevenAndTwelve},{ThirteenToFifteen},{SixteenAndSeventeen},{EighteenOrOver}`)
		end
		
		task.wait(2)
	end
end

Get Legacy Developer Stats (Division: Device)

Summary
--> Services
local ServerStorage = game:GetService("ServerStorage")
local HttpService = game:GetService("HttpService")

--> Dependencies
local Proxy = require(game:GetService("ServerStorage").Proxy)

--> Configuration
local placeId = 380305410

--------------------------------------------------------------------------------

local Months = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
local function GetMonthFromIsoDate(IsoDate: string): number
	return DateTime.fromIsoDate(IsoDate):ToUniversalTime().Month
end

local function GetPage(Start: DateTime, End: DateTime)
	local Response = Proxy:MakeRequest("GET", `https://develop.roproxy.com/v1/places/{placeId}/stats/Visits?granularity=Daily&divisionType=Device&startTime={Start:ToIsoDate()}&endTime={End:ToIsoDate()}`)
	return HttpService:JSONDecode(Response)
end

local function GetValue(t, isoDate: string): number|string
	if t then
		for _, Value in t do
			if Value[1] == isoDate then
				return Value[2]
			end
		end
	end
	return ""
end

local LastDay = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}

for _, Year in {2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023} do
	for Month = 1, 12 do
		local isLeapMonth = (Month == 2) and (Year % 4 == 0)
		local Start = DateTime.fromUniversalTime(Year, Month) 
		local End = DateTime.fromUniversalTime(Year, Month, LastDay[Month] + (isLeapMonth and 1 or 0))
		local Page = GetPage(Start, End)
		
		warn(`{Start:ToIsoDate()} ::: {End:ToIsoDate()}`)
		
		-- Sort data into arrays
		local arrays = {}
		for _, Category in Page.data do
			local t = {}
			for date, value in Category.data do
				table.insert(t, {DateTime.fromUnixTimestampMillis(tonumber(date)):ToIsoDate(), value})
			end
			table.sort(t, function(A, B)
				return A[1] < B[1]
			end)
			arrays[Category.type] = t
		end
		
		-- Log
		for n, totalData in arrays.Total do
			local IsoDate = totalData[1]
			local placeId = Page.placeId
			local Total = arrays.Total[n][2]
			local Computer = GetValue(arrays.Computer, IsoDate)
			local Tablet = GetValue(arrays.Tablet, IsoDate)
			local Phone = GetValue(arrays.Phone, IsoDate)
			local Console = GetValue(arrays.Console, IsoDate)
			
			print(`{IsoDate},{placeId},{Total},{Computer},{Tablet},{Phone},{Console}`)
		end
		
		task.wait(2)
	end
end

Both scripts print out a CSV format like the one shown below. By importing it into excel (just save it into a notepad as .csv, but you may be able to paste this format into a .csv file itself), it’ll have each column represent the values that print at the bottom, and each row the day. Make sure to hide the warnings too if you wish.

image

Make sure to disable the timestamp/sources in the output settings to make this easier to copy & paste! For example, the format shown below is:

Date,PlaceId,Total,SixOrUnder,SevenAndEight,NineAndTen,ElevenAndTwelve,ThirteenToFifteen,SixteenAndSeventeen,EighteenOrOver

If your game is old enough, some columns/values may be empty due to Roblox not implementing sources like breakdown by age yet. This is normal.

image

Finally, both scripts are printing out Visits at a granularity of Daily. You can change the source to any available, but I didn’t make these to support other granularities - so unfortunately getting Hourly may be a hassle, even though you don’t really need it. Monthly can be calculated pretty easily. Also, Revenue is gross revenue - unlike the new dashboard which shows net revenue (after marketplace/commission fees).

image

4 Likes

Also, even though Engagement Payouts is already moved to the new dashboard so you can finally export the data, I might as well share the script I’ve been using to gather this data for each month. If there’s incomplete (projected) payouts for any day, it marks the month/year as incomplete, meaning you probably shouldn’t back it up since projected payouts are subject to change.

Get Accrued Premium Payouts

Summary
--> Services
local ServerStorage = game:GetService("ServerStorage")
local HttpService = game:GetService("HttpService")

--> Dependencies
local Proxy = require(game:GetService("ServerStorage").Proxy)

--> Configuration
local universeId = 630584997

--------------------------------------------------------------------------------

local Months = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
local function GetMonthFromIsoDate(IsoDate: string): number
	return DateTime.fromIsoDate(IsoDate):ToUniversalTime().Month
end

local function GetPageForYear(Year: number)
	local startDate, endDate = `{Year}-01-01`, `{Year}-12-31`
	local Response = Proxy:MakeRequest("GET", `https://engagementpayouts.roproxy.com/v1/universe-payout-history?universeId={universeId}&startDate={startDate}&endDate={endDate}`)
	return HttpService:JSONDecode(Response)
end

for _, Year in {2020, 2021, 2022, 2023} do
	local Page = GetPageForYear(Year)
	
	local TotalForYear = 0
	local TotalByMonth = {}
	local doesYearHaveIncompletePayouts = false
	
	-- Calculate monthly & yearly income
	for IsoDate, Datapoint in Page do
		local Month = GetMonthFromIsoDate(IsoDate)
		if Datapoint.eligibilityType == "Eligible" then
			if Datapoint.payoutType == "Actual" then
				if not TotalByMonth[Month] then
					TotalByMonth[Month] = {
						Total = 0
					}
				end
				TotalByMonth[Month].Total += Datapoint.payoutInRobux
				TotalForYear += Datapoint.payoutInRobux
			elseif TotalByMonth[Month] and Datapoint.payoutInRobux ~= 0 then -- Any payout that is 0 for a day states it's Projected in the API, regardless of the time passed.
				TotalByMonth[Month].IncompletePayouts = true
				doesYearHaveIncompletePayouts = true
			end
		end
	end
	
	-- Log
	for Month, t in TotalByMonth do
		print(`Accrued Premium Payouts for {Year}/{Months[Month]}: {math.floor(t.Total)} Robux{t.IncompletePayouts and " (Incomplete Payout(s))" or ""}`)
	end
	warn(`Accrued Premium Payouts for {Year}: {math.floor(TotalForYear)} Robux{doesYearHaveIncompletePayouts and " (Incomplete Payout(s)" or ""}`)
end

Example Output

image

Aside from the ease of use, it’s nice because it prints ALL of your experience’s Engagement Payouts history - not just the last year, which you’re limited to on the analytics dashboard.

4 Likes