Using GameAnalytics (Retention and Monetisation) (Ez mode)

I’m barely scratching the surface of the capabilities of Game Analytics. This module will allow all functionality of GameAnalytics to be used, and I’ll leave links so clever people can go do that

This is a long tutorial but all the steps are super easy, and for most capabilities, very little scripting knowledge is needed

Set Up . . .

Okay, step 1, get my analytics module!

Okay, once you’ve done that, put it in a game, preferably a blank baseplate whilst you get use to this.
The only thing I want you to do right now is to go to the first line of the module and change it somewhere in replicated storage when I can put events and you won’t delete them or what ever (You can probably just leave it if you want)

Make sure HTTP service is enabled!!

Okay, so the next step is to go to GameAnalytics and create an account. Then do the following

  1. Get rid of the annoying “Download an SDK” thing, lol no one integrates Roblox support by default so that’s not for us

  2. Go to your email and verify your account and follow that line

  3. Create a game (this is blindingly obvious but the picture to text ratio is low) and press the orange text which says “My game is not available in an app store”, Roblox doesn’t count :c

  1. Name your game and press “Other” for the platform it’s on

  2. Add benchmarks genres, they are mostly mobile games to compare yours to, and so not a great comparison but I get a kick out of it

  1. Next you should have a screen like this (if you don’t uhhh, well done you). Look at the Game Key And Secret Key. Don’t give this out to people!! (iCool Studio’s game Herp De Derp is an exception). Anyways, take this codes and copy and paste them to something.

Then click the skip guide button, and set up is complete!

Doing stuff!

So uhhh, GameAnalytics and me did pretty much all the heavy lifting in this area :stuck_out_tongue:
However, the first thing you need to do is somewhere on the server write

Analytics = require(game.ReplicatedStorage.AnalyticsModule) -- The module doesn't have to be here 
Analytics.ServerInit() -- Call THIS ONCE IN THE ENTIRE GAME!!!!!!!

And then, between the two brackets for ServerInit, insert the GameKey, and the SecretKey, in that order!!!

Analytics = require(game.ReplicatedStorage.AnalyticsModule) -- The module doesn't have to be here 
Analytics.ServerInit("d39c482fd8604b680a7f5440b8e60286", "80d0dd29e81cfbd34c4b4536ffd1b095894e5568") -- Call THIS ONCE IN THE ENTIRE GAME!!!!!!!

Then on a client, and this needs to run very close to the client joining, preferable straight away

Analytics = require(game.ReplicatedStorage.AnalyticsModule) -- The module doesn't have to be here 
Analytics.ClientInit() -- Call THIS ONCE PER CLIENT!!!!!

This code right here will give you full analytics for retention, playtime and some other stuff. Whilst your testing it and setting it up, I recommend opening up the module (spoopy!!) and going to line 5, and decrease it to around 1. This is how long it waits to send you events, and so when you’re debugging it’ll take less time for the events to start to send so you don’t have to hang around for a minute waiting for it to stuff.

Finding your visit

To verify that worked, go to Realtime (top left) to verify it worked, it takes 15 minutes to process on small games, and 45 minutes if the site is having a slow down or what ever

Finding your analytics . . .

The code I gave you will populate the following places:

N.B Because of Roblox, acquisition is not accurate so uhhh, don’t use that.

Monetisation Analytics

To record money stuff, I wrote a super special function for you. Just write

Analytics = require(game.ReplicatedStorage.AnalyticsModule) 
Analytics.RecordTransaction(Player, Price, Thing) 

The player should be the player object, the price is a int value in ROBUX

About “Thing”, that should be a string value. It offers classes of item. The way it should work and is formatted is “Catagory:Specific”, so if I sell pets the catagory is Pet, and the Specific is a dragon, if a dragon has just been brought.

Just to clarify, in NONE of the string arguments (the ones which are plain text), don’t put a space that’ll break it.

Analytics = require(game.ReplicatedStorage.AnalyticsModule) 
Analytics.RecordTransaction(Player,  500,  "Pet:Dragon")
Analytics.RecordTransaction(Player,  1000000,  "Pet:UhhhNotADragon")
Analytics.RecordTransaction(Player,  1.5 * 10 ^ 1000,  "Sword:Derp") -- Idk if you can send events in standard form plz don't try
Analytics.RecordTransaction(Player,  9999999999999999999999,  "Starterpack")

Note that you can do “catagory:subcatagory:subcatagory:thing”. Not limit to how many subcatagorys you have, and it is not compulsory to have a catagory at all as shown in the starterpack example

If you want it to record it in USD or deduct the tax (or both), still pass the arguments in USD and don’t do anything weird with it. Just open up the module, and in Settings (right at the top), just set ApplyTax and ConvertedToUSD to true or false

And this will populate a bunch of other stuff such as (All can be found under Dashboards>Monetisation):

Sending Resource Flow Events

First, what is a resource flow event? :thinking: What it is, is when a player in your game loses or gains a currency. And game analytics allows you to track this, and the sources and sinks of your currency. How kind of them!?

I have also provided an inbuilt API to do this for you, how kind of me!?
Here is the data you must provide:

Analytics.RecordResource(Player, Amount, FlowType, Currency, ItemType, ItemId)

So lets talk about what they are.

  • Player is the player object, fairly obviously.

  • Amount is a number value which denotes how much currency has been spent or earnt. Stick to sending integers

  • FlowType is a string value, and must either be “Sink” or “Source”. Source if they are earning money, Sink if they are losing it.

  • Currency is a string value which denotes the ingame currency used. For example “Gems” or “Gold”

  • ItemType is quite hard to understand. It’s what type of thing gave them the money, or the what type of item they spent it on. So this could be a mission giving it to them, or a gun they’re spending it on. The rule about no spaces applys here.

  • ItemId is what specifically you sent. So if you set a mission to be the ItemType, you might say “KillZombies”, or if it was a gun, you might put “M4A1”

More on ItemId

Much like with revenue, you can go deeper with item type. To do that here, expand ItemId. So for example you might want it to be a KillZombies mission on hard, you’d set ItemId to be “KillZombies:Hard”

Resource analytics is a little hard to understand at first, so I took the game analytic’s website examples and Lua-fied them (yes that’s the technical term)

Examples

Example 1: Life used to play level.
Analytics.RecordResource(Player, 1, “Sink”, “Life”, “continuity”, “startLevel”)

Example 2: Gold spent to buy rainbow boost
Analytics.RecordResource(Player, 50, “Sink”, “Gold”, “Boost”, “Rainbow”)

Example 3: Gold spent to buy big rainbow boost
Analytics.RecordResource(Player, 100, “Sink”, “Gold”, “Boost”, “Rainbow:Big”)

Example 4: Earning gold by doing a zombie killing mission
Analytics.RecordResource(Player, 100, “Source”, “Gold”, “Mission”, “ZombieKilling”)

Example 4: Earning gold by doing a hard doggo killing mission
Analytics.RecordResource(Player, 1337, “Source”, “Gold”, “Mission”, “DogeCleansing:Hard”)

Sending Other Events

To find the API for this, go here: GameAnalytics Collector REST API Reference
Code example of how to do this:

Analytics = require(game.ReplicatedStorage.AnalyticsModule) 
Analytics.ServerEvent(
{
["category"] = "resource";
["event_id"] = "Sink:Gems";
["amount"] = 100000;
},
Player
 )

Analytics.ServerEvent(Event, Player)

It’s self explanatory when you read their notes. Just a note about ServerEvent function. Mobile Analytics assumes there will always be a player, but if you leave the Player bit blank, it’ll send it as a random player. There must be a player tied to all events.

Okay, peace my dudes.

PSA: This was updated recently! Find the updates here :smiley:

  • Added automatic error reporting. Partial credit to @rcouret
  • Added better platform detection. Total credit to @Velibor (literally copied and pasted his code. worked great, thanks!)
  • Added events being submitted on game shutdown. Partial credit to @Tim7775
  • Added huge performance bonuses. Total credit to @Widgeon
  • Improved security. Thanks to @Merely for pointing this out.
  • Added settings to convert to USD, deduct tax, and change intervals between events being submitted.
  • Added in-built functionality to send resource events (see above)
  • Improved in-built debugging prints
257 Likes

Whoops, if you’ve just taken the module it wasn’t the most recent version. Fixed that lol

2 Likes

Lovely tutorial, looks like something which could help a lot of people when it comes to analytics.

Adds more than just the default ROBLOX developer stats.

Wow, thank you! I’ve been using some old free model GameAnalytics module I found for my game, but the purchasing didn’t record right and was making in game purchases fail so I had to turn that off. It’s kind of useless without the revenue stats imo. Fun to check every once in a while for retention and stuff. I will switch to yours though!

Did you request access to get their REST API to figure this out?

Interesting that they make you sha256 hmac the body with secret key every request, haven’t seen other APIs do that :open_mouth:

EDIT: Oh I bet it’s so you don’t actually send secret key with request, just sign the data

7 Likes

Yeah I did.

I’ve never actually heard of hmac encoding, rcouret did all of that for me lol

Exactly, you don’t want to send your secret key with every single request. That is why the developers of GA want that you encode your body with the SHA256 HMAC using your secret key :wink:

For people saying it couldn’t parse the JSON, set HTTP Service to true, lol forgot to add that it to begin with

You can add compatibility for it to send error reports to the server. If I added this as an in-built function, would people use it?

  • Yes
  • No

0 voters

1 Like

wow thanks for making this! I’ll use it and I think several of my friends will be interested too! :smiley:

This is how my current one works and it is very helpful for finding bugs. Please do!

Its also important to mention that there can only be 100,000 unique event_ids per day, so nesting all the information into the event_id may not be a good idea.

Just to clarify what he means by this is not that there is a limit of 100k events a day.

For resource events (the only type this tutorial shows how to use which you can affect in terms of event ids) these are two events with separate ids and count as two to your 100k limit

Analytics.RecordTransaction(Player, 500, “Pet:Dragon”)
Analytics.RecordTransaction(Player, 1000000, “Pet:UhhhNotADragon”)

However, if I was to fire the first one twice, they’d have the same id, and count as one towards your 100k limit.

There is no limit to the amount of data you can send to game analytics, and it’s all free!

no clue how they make money plz don’t go out of business

6 Likes

Oh yay game analytics, I love using it. All the custom events are super useful, for example I keep track of average times for each level in my game (ie I know that it takes a user on average 110 min to reach level 2 etc) and then you can graph those, and like do cool things like compare different weeks worth of data to eachother etc. Although I use @ByDefault’s Module as I found it works well enough but digging through yours seems pretty well written as well. One thing I would like to note about anyone putting this in their place, while encoding strings to be sent to the game analytics server, server CPU usage spikes with lots of pending requests. To combat this I modified by sha2_256 file to include some heartbeat waits, this reduces lag spikes significantly to like 5% CPU usage, if you want to see how bad it is watch F9 Server Script Usage, spikes to 50% sometimes causing replication issues depending on whatever your game analytics refresh is set to:


local Bit = require(lockbox.util.bit);
local String = string;
local Math = math;
local Queue = require(lockbox.util.queue);

local CONSTANTS = {
   0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
   0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
   0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
   0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
   0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
   0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
   0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
   0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2  };

local AND = Bit.band;
local OR  = Bit.bor;
local NOT = Bit.bnot;
local XOR = Bit.bxor;
local LROT = Bit.lrotate;
local RROT = Bit.rrotate;
local LSHIFT = Bit.lshift;
local RSHIFT = Bit.rshift;

--SHA2 is big-endian
local bytes2word = function(b0,b1,b2,b3)
	local i = b0; i = LSHIFT(i,8);
	i = OR(i,b1); i = LSHIFT(i,8);
	i = OR(i,b2); i = LSHIFT(i,8);
	i = OR(i,b3);
	return i;
end

local word2bytes = function(word)
	local b0,b1,b2,b3;
	b3 = AND(word,0xFF); word = RSHIFT(word,8);
	b2 = AND(word,0xFF); word = RSHIFT(word,8);
	b1 = AND(word,0xFF); word = RSHIFT(word,8);
	b0 = AND(word,0xFF);
	return b0,b1,b2,b3;
end

local bytes2dword = function(b0,b1,b2,b3,b4,b5,b6,b7)
	local i = bytes2word(b0,b1,b2,b3);
	local j = bytes2word(b4,b5,b6,b7);
	return (i*0x100000000)+j;
end

local dword2bytes = function(i)
	local b4,b5,b6,b7 = word2bytes(i);
	local b0,b1,b2,b3 = word2bytes(Math.floor(i/0x100000000));
	return b0,b1,b2,b3,b4,b5,b6,b7;
end 




local SHA2_256 = function()

	local queue = Queue();

	local h0 = 0x6a09e667;
	local h1 = 0xbb67ae85;
	local h2 = 0x3c6ef372;
	local h3 = 0xa54ff53a;
	local h4 = 0x510e527f;
	local h5 = 0x9b05688c;
	local h6 = 0x1f83d9ab;
	local h7 = 0x5be0cd19;

	local public = {};

	local processBlock = function()
		
		local a = h0;
		local b = h1;
		local c = h2;
		local d = h3;
		local e = h4;
		local f = h5;
		local g = h6;
		local h = h7;
		
		local w = {};
		for i=0,15 do
			w[i] = bytes2word(queue.pop(),queue.pop(),queue.pop(),queue.pop());
		end

		game:GetService("RunService").Heartbeat:Wait()

		for i=16,63 do
			if i%20 == 0 then
				game:GetService("RunService").Heartbeat:Wait()
			end
			local s0 = XOR(RROT(w[i-15],7), XOR(RROT(w[i-15],18), RSHIFT(w[i-15],3)));
			local s1 = XOR(RROT(w[i-2],17), XOR(RROT(w[i-2], 19), RSHIFT(w[i-2],10)));
			w[i] = AND(w[i-16] + s0 + w[i-7] + s1, 0xFFFFFFFF);
		end

		for i=0,63 do
			if i%12 == 0 then
				game:GetService("RunService").Heartbeat:Wait()
			end
			local s1 = XOR(RROT(e,6), XOR(RROT(e,11),RROT(e,25)));
			local ch = XOR(AND(e,f), AND(NOT(e),g));
			local temp1 = h + s1 + ch + CONSTANTS[i+1] + w[i];
			local s0 = XOR(RROT(a,2), XOR(RROT(a,13), RROT(a,22)));
			local maj = XOR(AND(a,b), XOR(AND(a,c), AND(b,c)));
			local temp2 = s0 + maj;

			h = g;
			g = f;
			f = e;
			e = d + temp1;
			d = c;
			c = b;
			b = a;
			a = temp1 + temp2;
		end

		h0 = AND(h0 + a, 0xFFFFFFFF);
		h1 = AND(h1 + b, 0xFFFFFFFF);
		h2 = AND(h2 + c, 0xFFFFFFFF);
		h3 = AND(h3 + d, 0xFFFFFFFF);
		h4 = AND(h4 + e, 0xFFFFFFFF);
		h5 = AND(h5 + f, 0xFFFFFFFF);
		h6 = AND(h6 + g, 0xFFFFFFFF);
		h7 = AND(h7 + h, 0xFFFFFFFF);
		game:GetService("RunService").Heartbeat:Wait()
	end

	public.init = function()
		queue.reset();

		h0 = 0x6a09e667;
		h1 = 0xbb67ae85;
		h2 = 0x3c6ef372;
		h3 = 0xa54ff53a;
		h4 = 0x510e527f;
		h5 = 0x9b05688c;
		h6 = 0x1f83d9ab;
		h7 = 0x5be0cd19;

		return public;
	end

	public.update = function(bytes)
		
		for b in bytes do
			queue.push(b);
			if queue.size() >= 64 then processBlock(); end
		end
		
		return public;
	end

	public.finish = function()
		local bits = queue.getHead() * 8;

		queue.push(0x80);
		while ((queue.size()+7) % 64) < 63 do
			queue.push(0x00);
		end

		local b0,b1,b2,b3,b4,b5,b6,b7 = dword2bytes(bits);

		queue.push(b0);
		queue.push(b1);
		queue.push(b2);
		queue.push(b3);
		queue.push(b4);
		queue.push(b5);
		queue.push(b6);
		queue.push(b7);

		while queue.size() > 0 do
			processBlock();
		end

		return public;
	end
	
	public.asBytes = function()
		local  b0, b1, b2, b3 = word2bytes(h0);
		local  b4, b5, b6, b7 = word2bytes(h1);
		local  b8, b9,b10,b11 = word2bytes(h2);
		local b12,b13,b14,b15 = word2bytes(h3);
		local b16,b17,b18,b19 = word2bytes(h4);
		local b20,b21,b22,b23 = word2bytes(h5);
		local b24,b25,b26,b27 = word2bytes(h6);
		local b28,b29,b30,b31 = word2bytes(h7);


		return {  b0, b1, b2, b3, b4, b5, b6, b7, b8, b9,b10,b11,b12,b13,b14,b15
				,b16,b17,b18,b19,b20,b21,b22,b23,b24,b25,b26,b27,b28,b29,b30,b31};	
	end

	public.asHex = function()
		local  b0, b1, b2, b3 = word2bytes(h0);
		local  b4, b5, b6, b7 = word2bytes(h1);
		local  b8, b9,b10,b11 = word2bytes(h2);
		local b12,b13,b14,b15 = word2bytes(h3);
		local b16,b17,b18,b19 = word2bytes(h4);
		local b20,b21,b22,b23 = word2bytes(h5);
		local b24,b25,b26,b27 = word2bytes(h6);
		local b28,b29,b30,b31 = word2bytes(h7);

		local fmt = "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"		

		return String.format(fmt, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9,b10,b11,b12,b13,b14,b15
				,b16,b17,b18,b19,b20,b21,b22,b23,b24,b25,b26,b27,b28,b29,b30,b31);
	end

	return public;

end

return SHA2_256;

One thing I do also is send any local errors to the server, then the server sends those local errors to game analytics, so I can keep track of local player errors without asking people to open up their F9.

9 Likes

Thanks man.

I’ll add that + bug catching support to my module tomorrow

I really don’t like how the credentials are stored in the same ModuleScript that is given to clients. Why not have those in a separate “Credentials” script that can be placed in ServerStorage, since only the server needs access to them?

4 Likes

I updated my version to do Analytics.ServerInit(GameKey, SecretKey).

Been rushed for time so didn’t publish the changes (my version is different to the public ones). Will do it today however, thanks for reminding me

1 Like

And I did it. Highly recommend you use the new script!

1 Like

What’s this mean? All of my settings for studio-API+HTTP enabled are on

Figured it out. Had to hard-code the keys into your script. Just putting them into the initialization function doesn’t work.