Collector REST API Reference
Welcome to the documentation for the GameAnalytics Collector REST API.
A Collector is a GameAnalytics server (one of many) that receive and collect game events being submitted using the HTTP protocol. All official GameAnalytics SDK’s are using this same API.
Requirements
The following keys are needed when submitting events.
- game key
- secret key
The game key is the unique identifier for the game. The secret key is used to protect the events from being altered as they travel to our servers.
To obtain these keys it is needed to register/create your game at our GameAnalytics tool. Locate them in the game settings after you create the game in your account.
API basics
HTTP and HTTPS is supported. Data is submitted and received as JSON strings. Gzip is supported and strongly recommended.
Production
API endpoint for production api.gameanalytics.com
Sandbox
API endpoint for sandbox sandbox-api.gameanalytics.com
- Use the sandbox-api endpoint and sandbox keys during the implementation phase.
- Switch to the production endpoint and production keys once the integration is completed.
Create your production keys in our GameAnalytics tool.
Sandbox keys | |
---|---|
game key | 5c6bcb5402204249437fb5a7a80a4959 |
secret key | 16813a12f718bc5c620f56944e1abc3ea13ccbac |
Gzip
import gzip
from StringIO import StringIO
import base64
import hmac
import hashlib
events_JSON_test = '["test":"test"]'
game_secret = '16813a12f718bc5c620f56944e1abc3ea13ccbac'
def get_gzip_string(string_for_gzip):
zip_text_file = StringIO()
zipper = gzip.GzipFile(mode='wb', fileobj=zip_text_file)
zipper.write(string_for_gzip)
zipper.close()
enc_text = zip_text_file.getvalue()
return enc_text
def hmac_auth_hash(body_string, secret_key):
return base64.b64encode(hmac.new(secret_key, body_string, digestmod=hashlib.sha256).digest())
# the gzipped payload
gzipped_events = get_gzip_string(events_JSON_test)
# the HMAC hash for the Authorization header
HMAC_from_gzip_contents = hmac_auth_hash(gzipped_events, game_secret)
It is highly recommended to gzip the data when submitting.
- Set the header
Content-Encoding
header to gzip - Gzip the events JSON string and add the data to the POST payload
- Calculate the HMAC Authorization header using the gzipped data
Look at the code example for gzip and HMAC for python. Also look at the Python example download for a more complete implementation.
Authentication
import base64
import hmac
import hashlib
def hmac_auth_hash(body_string, secret_key):
return base64.b64encode(hmac.new(secret_key, body_string, digestmod=hashlib.sha256).digest())
# use example below to verify implementation on other platforms
# body_string = '{"test": "test"}'
# secret_key = '16813a12f718bc5c620f56944e1abc3ea13ccbac'
# hmac_auth_hash(body_string, secret_key) = 'slnR8CKJtKtFDaESSrqnqQeUvp5FaVV7d5XHxt50N5A='
echo -n '<body_contents>' | openssl dgst -binary -sha256 -hmac "<game secret>" | base64
using System.Security.Cryptography;
private string GenerateHmac (string json, string secretKey)
{
var encoding = new System.Text.UTF8Encoding();
var messageBytes = encoding.GetBytes(json);
var keyByte = encoding.GetBytes(secretKey);
using (var hmacsha256 = new HMACSHA256(keyByte)) {
byte[] hashmessage = hmacsha256.ComputeHash (messageBytes);
return System.Convert.ToBase64String (hashmessage);
}
}
Authentication is handled by specifying the Authorization
header for the request.
The authentication value is a HMAC SHA-256 digest of the raw body content from the request using the secret key (private key) as the hashing key and then encoding it using base64.
Look at the code examples for both shell and python.
Headers
Header | Value | Comment |
---|---|---|
Authorization | HMAC HASH | The authentication hash. |
Content-Type | application/json | Required. |
Content-Encoding | gzip | Optional. Set only if payload is gzipped. |
Content-Length | [ length of payload ] | Optional. Set if possible. |
Routes
POST /v2/<game_key>/init
POST /v2/<game_key>/events
Try it yourself now !
Install the Postman app or Chrome extension and click the button to import some request examples to run.
Validation for JSON body content is described later in the documentation.
Init
POST /v2/<game_key>/init HTTP/1.1
Host: sandbox-api.gameanalytics.com
Authorization: <authorization_hash>
Content-Type: application/json
{"platform":"ios","os_version":"ios 8.1","sdk_version":"rest api v2"}
HTTP/1.1 200 OK
Connection: Keep-Alive
Content-Type: application/json
Access-Control-Allow-Origin: *
X-GA-Service: collect
Access-Control-Allow-Headers: Authorization, X-Requested-With
{"enabled":true,"server_ts":1431002142,"flags":[]}
curl -vv -H "Authorization: <authorization_hash>" -d '{"platform":"ios","os_version":"ios 8.1","sdk_version":"rest api v2"}' http://sandbox-api.gameanalytics.com/v2/<game_key>/init
import json
import urllib2
game_key = '5c6bcb5402204249437fb5a7a80a4959'
url_init = 'http://api.gameanalytics.com/v2/' + game_key + '/init'
init_payload = {
'platform': 'ios',
'os_version': 'ios 8.1',
'sdk_version': 'rest api v2'
}
init_payload_json = json.dumps(init_payload)
headers = {
'Authorization': hmac_auth_hash(init_payload_json, secret_key),
'Content-Encoding': 'application/json'
}
try:
request = urllib2.Request(url_init, init_payload_json, headers)
response = urllib2.urlopen(request)
except:
print "Init request failed!"
using System.Security.Cryptography;
using System.Collections.Generic;
// Unity C# example using the WWW class
void Start () {
var encoding = new System.Text.UTF8Encoding();
// json payload
string json = "{\"platform\":\"ios\", \"os_version\":\"ios 8.1\", \"sdk_version\":\"rest api v2\"}";
byte[] jsonByteData = encoding.GetBytes(json);
// sandbox-api
string gameKey = "5c6bcb5402204249437fb5a7a80a4959";
string secretKey = "16813a12f718bc5c620f56944e1abc3ea13ccbac";
string url = "http://sandbox-api.gameanalytics.com/v2/" + gameKey + "/init";
string HmacAuth = GenerateHmac (json, secretKey);
// create headers
Dictionary<string, string> headers = new Dictionary<string, string>();
headers.Add("Content-Type", "application/json");
headers.Add("Authorization", HmacAuth);
headers.Add("Content-Length", json.Length.ToString());
WWW www = new WWW(url, jsonByteData, headers);
StartCoroutine(WaitForRequest(www));
}
IEnumerator WaitForRequest(WWW www)
{
yield return www;
// check for errors
if (www.error == null)
{
Debug.Log("WWW Ok!: " + www.text);
} else {
Debug.Log("WWW Error: "+ www.error);
}
}
POST /v2/<game_key>/init
The init call should be requested when a new session starts or at least when the game is launched.
The POST request should contain a valid JSON object as in the body containing the following fields.
Field | Description |
---|---|
platform | A string representing the platform of the SDK, e.g. “ios” |
os_version | A string representing the OS version, e.g. “ios 8.1” |
sdk_version | Custom solutions should ALWAYS use the string “rest api v2” |
The server response is a JSON object with the following fields.
Field | Description |
---|---|
enabled | A boolean. Events should ONLY be sent if this field is present and set to true. If not true then deactivate. |
server_ts | An integer timestamp of the current server time in UTC (seconds since EPOCH). |
flags | An array of strings. Not used at the moment. In the future this could contain flags set by GA servers to control SDK behaviour. Make sure the code does not break if this contain values in the future. |
Adjust client timestamp
The server_ts should be used if the client clock is not configured correctly.
- Compare the value with the local client timestamp.
- Store the offset in seconds.
- Use this offset whenever an event is added to adjust the local timestamp (client_ts) for the event
Events
POST /v2/<game_key>/events HTTP/1.1
Host: sandbox-api.gameanalytics.com
Authorization: <authorization_hash>
Content-Type: application/json
[
{"category": "user", "<event_fields>": "<event_values>"},
{"category": "business", "<event_fields>": "<event_values>"}
]
HTTP/1.1 200 OK
Connection: Keep-Alive
Content-Type: application/json
Access-Control-Allow-Origin: *
X-GA-Service: collect
Access-Control-Allow-Headers: Authorization, X-Requested-With
curl -vv -H "Authorization: <authorization_hash>" -d '[<event_object1>, <event_object2>]' http://sandbox-api.gameanalytics.com/v2/<game_key>/events
import json
import urllib2
game_key = '5c6bcb5402204249437fb5a7a80a4959'
url_events = 'http://sandbox-api.gameanalytics.com/v2/' + game_key + '/events'
events_payload = [
{"category": "progression"}, # + more event object fields
{"category": "business"} # + more event object fields
]
events_payload_json = json.dumps(events_payload)
headers = {
'Authorization': hmac_auth_hash(events_payload_json, secret_key),
'Content-Encoding': 'application/json'
}
try:
request = urllib2.Request(url_init, events_payload_json, headers)
response = urllib2.urlopen(request)
except:
print "Events request failed!"
POST /v2/<game_key>/events
The events route is for submitting events.
The POST body payload is a JSON list containing 0 or more event objects. Like this example:
[
{“category”: “user”, “[event_fields]”: “[event_values]”},
{“category”: “business”, “[event_fields]”: “[event_values]”}
]
[
{"category": "user", "<event_fields>": "<event_values>"},
{"category": "business", "<event_fields>": "<event_values>"}
]
An event object contain all data related to a specific event triggered. Each type of event is defined by a unique category field and each require specific fields to be defined.
If the status code 200
is returned then the request succeeded and all events were collected.
Read more about error responses (looking for the reason when validation should fail) in the troubleshooting guide.
Event Types
Events are JSON objects with a certain set of required fields as well as optional fields. The exact requirements of a valid event depends on its category. Here are the available categories (event types).
- user (session start)
- session_end
- business
- progression
- resource
- design
- error
All the events share (inherit) a list of fields that we call the default annotations.
Default annotations (shared)
{
"description": "Schema for shared event attributes",
"id": "shared",
"type": "object",
"properties": {
"v": {
"type": "integer",
"required": true,
"minimum": 2,
"maximum": 2
},
"user_id": {
"type": "string",
"required": true
},
"ios_idfa": {
"type": "string",
"required": false
},
"ios_idfv": {
"type": "string",
"required": false
},
"google_aid": {
"type": "string",
"required": false
},
"android_id": {
"type": "string",
"required": false
},
"googleplus_id": {
"type": "string",
"required": false
},
"facebook_id": {
"type": "string",
"required": false
},
"limit_ad_tracking": {
"type": "boolean",
"enum": [true],
"required": false
},
"logon_gamecenter": {
"type": "boolean",
"enum": [true],
"required": false
},
"logon_googleplay": {
"type": "boolean",
"enum": [true],
"required": false
},
"gender": {
"type": "enum",
"required": false,
"enum": [
"male",
"female"
]
},
"birth_year": {
"type": "integer",
"pattern" : "^[0-9]{4}$",
"required": false
},
"custom_01": {
"type": "string",
"maxLength" : 32,
"required": false
},
"custom_02": {
"type": "string",
"maxLength" : 32,
"required": false
},
"custom_03": {
"type": "string",
"maxLength" : 32,
"required": false
},
"client_ts": {
"type": ["integer", "null"],
"pattern": "^([0-9]{10,11})$",
"required": false
},
"sdk_version": {
"type": "string",
"required": true,
"pattern": "^(rest api v2)$"
},
"engine_version": {
"type": "string",
"required": false,
"pattern": "^(unity|unreal|corona|marmalade|xamarin|xamarin.ios|xamarin.android|xamarin.mac|gamemaker|flash|cocos2d|monogame|stingray|cryengine|buildbox|defold|lumberyard|frvr|construct|godot|stencyl|fusion|nativescript) [0-9]{0,5}(\\.[0-9]{0,5}){0,2}$"
},
"os_version": {
"type": "string",
"pattern": "^(ios|android|windows|windows_phone|blackberry|roku|tizen|nacl|mac_osx|tvos|webplayer|ps4|xboxone|uwp_mobile|uwp_desktop|uwp_console|uwp_iot|uwp_surfacehub|webgl|xbox360|ps3|psm|vita|wiiu|samsung_tv|linux|watch_os) [0-9]{0,5}(\\.[0-9]{0,5}){0,2}$",
"required": true
},
"manufacturer": {
"type": "string",
"maxLength" : 64,
"required": true
},
"device": {
"type": "string",
"maxLength" : 64,
"required": true
},
"platform": {
"type": "enum",
"required": true,
"enum": ["ios", "android", "windows", "windows_phone", "blackberry", "roku", "tizen", "nacl", "mac_osx", "tvos", "webplayer", "ps4", "xboxone", "uwp_mobile", "uwp_desktop", "uwp_console", "uwp_iot", "uwp_surfacehub", "webgl", "xbox360", "ps3", "psm", "vita", "wiiu", "samsung_tv", "linux", "watch_os"]
},
"session_id": {
"type": "string",
"pattern": "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$",
"required": true
},
"build": {
"type": "string",
"maxLength" : 32,
"required": false
},
"session_num": {
"type": "integer",
"minimum": 1,
"required": true
},
"connection_type": {
"type": "string",
"enum": ["offline", "wwan", "wifi", "lan"],
"required": false
},
"jailbroken": {
"type": "boolean",
"enum": [true],
"required": false
}
}
}
Each event has unique fields defining the event. But all events need to include shared fields called default annotations.
The default annotation fields define information like os_version, platform etc. and they need to be included in each event object. Some are required and some are optional.
Field | Validation Type | Required | Description |
---|---|---|---|
device | short string | Yes | examples: “iPhone6.1”, “GT-I9000”. If not found then “unknown”. |
v | integer | Yes | Reflects the version of events coming in to the collectors. Current version is 2. |
user_id | string | Yes | Use the unique device id if possible. For Android it’s the AID. Should always be the same across game launches. |
client_ts | client ts integer | Yes | Timestamp when the event was created (put in queue/database) on the client. This timestamp should be a corrected one using an offset of time from server_time. 1) The SDK will get the server TS on the init call (each session) and then calculate a difference (within some limit) from the local time and store this ‘offset’. 2) When each event is created it should calculate/adjust the 'client_ts’ using the 'offset’. |
sdk_version | sdk version | Yes | The SDK is submitting events to the servers. For custom solutions ALWAYS use “rest api v2”. |
os_version | os version | Yes | Operating system version. Like “android 4.4.4”, “ios 8.1”. |
manufacturer | short string | Yes | Manufacturer of the hardware the game is played on. Like “apple”, “samsung”, “lenovo”. |
platform | platform | Yes | The platform the game is running. Platform is often a subset of os_version like “android”, “windows” etc. |
session_id | session id | Yes | Generate a random lower-case string matching the UUID format. Example: de305d54-75b4-431b-adb2-eb6b9e546014 |
session_num | unsigned integer | Yes | The SDK should count the number of sessions played since it was installed (storing locally and incrementing). The amount should include the session that is about to start. |
limit_ad_tracking | boolean | No | Send true if detected. Very important to always check this when using iOS idfa. |
logon_gamecenter | boolean | No | Send true if detected. Logged in to GameCenter. |
logon_googleplay | boolean | No | Send true if detected. Logged in to Google Play. |
jailbroken | boolean | No | If detected that device is jailbroken (hacked) or not. Should only be sent when true |
android_id | string | No | Send this if on Android and the google_aid is not available (e.g. Android phones without the play store) |
googleplus_id | string | No | Send if found. |
facebook_id | string | No | Send if found. Should be stored cross-session and sent along always after that. |
gender | gender string | No | Send if found. Should be stored cross-session and sent along always after that. |
birth_year | birthyear integer | No | Send if found. Should be stored cross-session and sent along always after that. |
custom_01 | short string | No | Send Custom dimension 1 if that is currently active/set. |
custom_02 | short string | No | Send Custom dimension 2 if that is currently active/set. |
custom_03 | short string | No | Send Custom dimension 3 if that is currently active/set. |
build | short string | No | Send if needed. A build version. Should be set before any events are sent. |
engine_version | engine version | No | Send if using engine. examples: “unreal 4.7” or “unity 5.6.10” |
ios_idfv | string | No | Send if iOS. Apple’s identifier for vendors. This is unique per app/game. |
connection_type | connection type string | No | Send if found. This will give the connection status of a device - how the device is connected to the internet (or if not). (offline, wwan, wifi, lan) |
ios_idfa | string | No | Send if iOS. Apple’s identifier for advertisers. This is the same across apps/games. Send this always (on iOS) and make sure to ALWAYS check if user has enabled “limited_ad_tracking”. If so then add the field mentioned elsewhere and targeting for that idfa will not happen. |
google_aid | string | No | Send if Android. Google’s identifier for advertisers. This is the same across apps/games. https://developer.android.com/google/play-services/id.html#get_started |
session_num
Do something like the following to track this value.
- Use a persistent DB like SqlLite for storing queued events and other information.
- Create a table called key_value with
key
value
columns. Use this column for storing all variables cross game-launch. - For counting session number use a key called session_num.
- When a session is started this row should be retrieved. Increment the value and update row.
custom_01, custom_02, custom_03
Read more about Custom Dimensions here.
Look at the JSON tab for validation schema.
User (session start)
{
"description": "Schema for user event",
"id": "user",
"type": "object",
"extends": "shared",
"properties": {
"category": {
"type": "string",
"required": true,
"pattern": "^user$"
}
}
}
As session is the concept of a user spending a period of time focused on a game.
The user event acts like a session start. It should always be the first event in the first batch sent to the collectors and added each time a session starts.
Field | Required | Description / Validation |
---|---|---|
category | Yes | user |
+ add the default annotation fields
Look at the JSON tab for validation schema.
Session end
{
"description": "Schema for session end event",
"id": "session_end",
"type": "object",
"extends": "shared",
"properties": {
"length": {
"type": "integer",
"minimum": 0,
"maximum": 172800,
"required": true
},
"category": {
"type": "string",
"required": true,
"pattern": "^session_end$"
}
}
}
Whenever a session is determined to be over the code should always attempt to add a session end event and submit all pending events immediately.
Only one session end event per session should be activated.
Field | Required | Description / Validation |
---|---|---|
category | Yes | session_end |
length | Yes | Session length in seconds |
+ add the default annotation fields
Session length
Session length is the amount of seconds spent focused on a game. Whenever a session starts the current timestamp should be stored in a variable. When session end is triggered this values is used to calculate the session length.
Detecting missing session end on game launch
Sometimes the session end event could not be added as the game closed without giving time to finish processing. It is recommended to implement code that is able to detect this on game launch and add the missing session end with correct session length.
This should be solved using a local storage that will work cross session/game-launch. In our official SDK implementation we use SqlLite. The following practise describe how to detect and submit a missing session end.
- Use a persistent DB like SqlLite for storing queued events and other information.
- Create a table called session_end with the columns
session_start_ts
session_id
last_event_default_annotations
. Each time (in that session) an event is added to the queue (excluding session end) this row is updated on the column
last_event_default_annotations
containing the shared default annotations for the particular event (including the field client_ts for the event).
If the event added is a session end, then simply delete the row matching the session.On game launch query the session_end table. If an entry exists then there is a session that did not have time to add a session end. Look inside the
last_event_default_annotations
and find client_ts. Use that value with thesession_start_ts
to calculate session length. Use thelast_event_default_annotations
to create the session end event. Add event and submit.
Look at the JSON tab for validation schema.
Business
{
"description": "Schema for business event",
"id": "business",
"type": "object",
"extends": "shared",
"properties": {
"amount": {
"type": "integer",
"required": true
},
"currency": {
"type": "string",
"pattern" : "^[A-Z]{3}$",
"required": true
},
"event_id": {
"type": "string",
"pattern" : "^[A-Za-z0-9\\s\\-_\\.\\(\\)\\!\\?]{1,64}:[A-Za-z0-9\\s\\-_\\.\\(\\)\\!\\?]{1,64}$",
"required": true
},
"cart_type": {
"type": "string",
"maxLength" : 32,
"required": false
},
"transaction_num": {
"type": "integer",
"minimum": 0,
"required": true
},
"category": {
"type": "string",
"required": true,
"pattern": "^business$"
},
"receipt_info": {
"type": "object",
"required": false,
"properties": {
"receipt": {
"type": "string",
"required": true
},
"store": {
"type": "string",
"required": true,
"pattern": "^apple|google_play|unknown$"
},
"signature": {
"type": "string",
"required": false
}
}
}
}
}
Business events are for real-money purchases.
Field | Required | Description / Validation |
---|---|---|
category | Yes | business |
event_id | Yes | A 2 part event id. [itemType]:[itemId] ! Read about unique value limitations here. |
amount | Yes | The amount of the purchase in cents (integer) |
currency | Yes | Currency need to be a 3 letter upper case string to pass validation. In addition the currency need to be a valid currency for correct rate/conversion calculation at a later stage. Look at the following link for a list valid currency values. http://openexchangerates.org/currencies.json. |
transaction_num | Yes | Similar to the session_num. Store this value locally and increment each time a business event is submitted during the lifetime (installation) of the game/app. |
cart_type | No | A string representing the cart (the location) from which the purchase was made. Could be menu_shop or end_of_level_shop. ! Read about unique value limitations here. |
receipt_info | No | A JSON object that can contain 3 fields: store , receipt and signature . Used for payment validation of receipts.Currently purchase validation is only supported for iOS and Android stores.For iOS the store is apple and the receipt is base64 encoded. For Android the store is google_play and the receipt is base64 encoded + the IAP signature is also required. |
+ add the default annotation fields
itemType & itemId
The itemType is like a category/folder for items and the ItemId is an identifier for what has been purchased. They are separated by a semicolon.
examples
- BlueGemPack:GameAnalytics.blue_gems_50
- BlueGemPack:GameAnalytics.blue_gems_100
- BlueGemPack:GameAnalytics.blue_gems_500
- Boost:MegaBoost
- Boost:SmallBoost
In the GameAnalytics tool it is possible to select the itemId and get detailed information. But it is also possible to select the itemType and thereby get aggregated values for all itemIds within. This could be visualized like a histogram for each or simply showing the total revenue for all BlueGemPacks over time.
Transaction number
Do something like the following to track this value.
- Use a persistent DB like SqlLite for storing queued events and other information.
- Create a table called key_value with
key
value
columns. Use this column for storing all variables cross game-launch. - For the counting transactions use a key called transaction_num.
- Each time a business event is triggered this row is retrieved, value is incremented and row is updated.
Purchase validation
The result of validating receipts is monetization metrics being divided into valid and non-valid in the GameAnalytics tool.
Currently purchase validation is only supported for iOS
and Android
stores. We are working on adding more ways (stores) for validating purchases. This feature is not meant to provide validation inside the game to block hackers. It is intended to provide valid numbers in GameAnalytics by flagging business events from monetizer hacks. Simply exclude the receipt_info field when using stores that are not supported yet.
Look at the JSON tab for validation schema.
Resource
{
"description": "Schema for resource event",
"id": "resource",
"type": "object",
"extends": "shared",
"properties": {
"event_id": {
"type": "string",
"pattern" : "^(Sink|Source):[A-Za-z]{1,64}:[A-Za-z0-9\\s\\-_\\.\\(\\)\\!\\?]{1,64}:[A-Za-z0-9\\s\\-_\\.\\(\\)\\!\\?]{1,64}$",
"required": true
},
"amount": {
"type": "number",
"required": true
},
"category": {
"type": "string",
"required": true,
"pattern": "^resource$"
}
}
}
Resource events are for tracking the flow of virtual currency registering the amounts users are spending (sink) and receiving (source) for a specified virtual currency.
Field | Required | Description / Validation |
---|---|---|
category | Yes | resource |
event_id | Yes | A 4 part event id string. [flowType]:[virtualCurrency]:[itemType]:[itemId] ! Read about unique value limitations here. |
amount | Yes | The amount of the in game currency (float). This value should be negative if flowType is Sink. For instance, if the players pays 100 gold for a level, the corresponding resource event will have the amount -100 added in this field. |
+ add the default annotation fields
flowType
Flow type is an enum with only 2 possible string values.
- Sink means spending virtual currency on something.
- Source means receiving virtual currency from some action.
virtualCurrency
A custom string defining the type of resource (currency) used in the event.
- Boost
- Coins
- Gems
- Lives
- Stars
itemType & itemId
The itemType functions like a category for the ItemId values.
The purpose/meaning of these values are different when using Sink or Source.
- Sink item values should represent what the virtual currency was spent on.
- Source item values should represent in what way the virtual currency was earned.
Examples | flowType | virtualCurrency | itemType | itemId |
---|---|---|---|---|
Life used to play level | Sink | life | continuity | startLevel |
Star used to continue level | Sink | star | continuity | resumeLevel |
Gold spent to buy rainbow boost | Sink | gold | boost | rainbowBoost |
Earned a life by watching a video ad | Source | life | rewardedVideo | gainLifeAdColony |
Bought gold with real money * | Source | gold | purchase | goldPack100 |
* When buying virtual currency for real money a business event should also be sent.
Look at the JSON tab for validation schema.
Progression
{
"description": "Schema for progression event",
"id": "progression",
"type": "object",
"extends": "shared",
"properties": {
"event_id": {
"type": "string",
"pattern" : "^(Start|Fail|Complete):[A-Za-z0-9\\s\\-_\\.\\(\\)\\!\\?]{1,64}(:[A-Za-z0-9\\s\\-_\\.\\(\\)\\!\\?]{1,64}){0,2}$",
"required": true
},
"attempt_num": {
"type": "integer",
"minimum": 0,
"required": false
},
"score": {
"type": "integer",
"required": false
},
"category": {
"type": "string",
"required": true,
"pattern": "^progression$"
}
}
}
Progression events are used to track attempts at completing levels in order to progress in a game. There are 3 types of progression events.
- Start
- Fail
- Complete
Field | Required | Description / Validation |
---|---|---|
category | Yes | progression |
event_id | Yes | A 2-4 part event id. [progressionStatus]:[progression1]:[progression2]:[progression3] ! Read about unique value limitations here. |
attempt_num | No | The number of attempts for this level. Add only when Status is “Complete” or “Fail”. Increment each time a progression attempt failed for this specific level. |
score | No | An optional player score for attempt. Only sent when Status is “Fail” or “Complete”. |
+ add the default annotation fields
examples
event_id | attempt_num | score |
---|---|---|
Start:PirateIsland:SandyHills | ||
Fail:PirateIsland:Sandyhills | 1 | 1234 |
Start:PirateIsland:Sandyhills | ||
Complete:PirateIsland:Sandyhills | 2 | 1234 |
progression1:progression2:progression3
The progression evnetId will end up in the tool as a selectable metric with drilldown into a hierarchy. For example…
- Progression > Fail > PirateIsland > Sandyhills.
View Fails on specific level SandyHills on PirateIsland. - Progression > Complete > PirateIsland > (all).
View Completes for all levels on PirateIsland.
It is possible to use 1, 2 or 3 values depending on your game. For example…
- PirateWorld:PirateIsland:SandyHills
- PirateIsland:SandyHills
- SandyHills
Start
The Start progression event should be called when a user is starting an attempt at completing a specific level. The attempt will stop once Fail or Complete is called.
When a Start event is called the progression event_id (excluding the progression status) should be stored locally. For example Start:PirateIsland:SandyHills should store PirateIsland:SandyHills locally.
If the Start event is called when there is an ongoing attempt already in progress the code should add a Fail event for that attempt, before adding the new Start event.
Fail
The Fail progression event should be called when a user did not complete an ongoing level attempt. Add a score value if needed.
- The user ran out of time or did not get enough points. Score screen is often shown.
- The user is exiting the game.
- A Start event was called during an ongoing attempt.
Complete
The Complete progression event should be called when a user did complete a level attempt. Add a score value if needed.
Handling attempt_num
The attempt_num is the number of times the user performed an attempt at completing a specific level (tracked for each progression event id). Once a complete is registered the counting is reset. Do something like the following to track the incrementing of progression attempts.
- Use a persistent DB like SqlLite for storing queued events and other information.
- Create a table called progression with
progression_event_id
attempt_num
columns. - On progression Fail look inside the table for the specified
progression_event_id
- if it’s there then increment
attempt_num
for the row by 1 and use this value in the event - if it’s not there then create row for
progression_event_id
withattempt_num
equal to 1 and use 1 in the event
- if it’s there then increment
- On progression Complete look inside the table for the specified
progression_event_id
- if it’s there then get the
attempt_num
value, use this value+1 in the event and delete the row - if it’s not there then use 1 as the
attempt_num
- if it’s there then get the
Look at the JSON tab for validation schema.
Design
{
"description": "Schema for design event",
"id": "design",
"type": "object",
"extends": "shared",
"properties": {
"event_id": {
"type": "string",
"pattern" : "^[A-Za-z0-9\\s\\-_\\.\\(\\)\\!\\?]{1,64}(:[A-Za-z0-9\\s\\-_\\.\\(\\)\\!\\?]{1,64}){0,4}$",
"required": true
},
"value": {
"type": "number",
"required": false
},
"category": {
"type": "string",
"required": true,
"pattern": "^design$"
}
}
}
Every game is unique! Therefore it varies what information is needed to track for each game. Some needed events might not be covered by our other event types and the design event is available for creating a custom metric using an event id hierarchy.
Field | Required | Description / Validation |
---|---|---|
category | Yes | design |
event_id | Yes | A 1-5 part event id. [part1]:[part2]:[part3]:[part4]:[part5] ! Read about unique value limitations here. |
value | No | Optional value. float. |
+ add the default annotation fields
Examples
GamePlay:Kill:[monster_type] and value equal to points gained. In the GameAnalytics explorer tool you can now select the following metrics.
Metric Selection | Description |
---|---|
GamePlay (all) | Show all count/sum etc. from all events beneath GamePlay. These metrics are not that useful, but GamePlay is defined as a group to contain all gameplay related metrics. Other root groups could be Ads, Performance, UI etc. |
GamePlay:Kill (all) | Show count/sum etc. for all things tracked under GamePlay:Kill. Show either aggregated count/sum over time or histogram with bars for each monster_type. All points earned by killing (over time), the average points earned by killing (over time) or number of times something was killed (count). |
GamePlay:Kill:AlienSmurf | As the above, but only showing the specific monster_type. |
GamePlay:Kill:(AlienSmurf + HumanRaider) | almost same as above. Show both AlienSmurf and HumanRaider values in chart and values. |
This example was a simple part of what the Explore tool can provide. Design events are used for many other things like Funnels, Segments, Cohorts etc.
Read more about limits, creating optimal event id’s and what you should track at our documentation page here.
Look at the JSON tab for validation schema.
Error
{
"description": "Schema for error event",
"id": "error",
"type": "object",
"extends": "shared",
"properties": {
"severity": {
"type": "enum",
"enum": [
"debug",
"info",
"warning",
"error",
"critical"
],
"required": true
},
"message": {
"type": "string",
"maxLength" : 8192,
"required": true
},
"category": {
"type": "string",
"required": true,
"pattern": "^error$"
}
}
}
An Error event should be sent whenever something horrible has happened in your code - some Exception/state that is not intended.
the following error types are supported.
- debug
- info
- warning
- error
- critical
Do not send more than 10 error events pr. game launch!
The error events can put a high load on the device the game is running on if every Exception is submitted. This is due to Exceptions having a lot of data and that they can be fired very frequently (1000/second is possible).
The GameAnalytics servers will block games that send excessive amounts of Error events.
Simple solution
Keep track of how many is sent and stop sending when the threshold is reached.
More advanced
Keep track of each type of Exception sent in a list. If an error event match a type already sent then ignore. If 10 types have been sent then stop sending. This will ensure that 10 similar Error events fired quickly will not result in other types not being discovered.
The idea is that developers should discover an error in the GameAnalytics tool and then fix the cause by submitting a new version of the app. Even with the limit of 10 error events this should still be possible.
Field | Required | Description / Validation |
---|---|---|
category | Yes | error |
severity | Yes | The type of error severity. |
message | Yes | Stack trace or other information detailing the error. Can be an empty string. |
+ add the default annotation fields
Look at the JSON tab for validation schema.
Custom Dimensions
Specifying custom dimensions will enable filters in the GameAnalytics tool. These filters can be used on other metrics (like DAU or Revenue for example) to view only numbers for when that dimension value was active.
To enable these you have to use the fields custom_01
custom_02
custom_03
.
Example for tracking player class
- player changes class to ninja and this is registered locally (variable and local db) as custom_01
- an event is triggered and custom_01 value is set in the event
- player changes class to wizard and this is registered locally (variable and local db) as custom_01
- an event is triggered and custom_01 value is set in the event
- session is ending and the session end event also get the custom_01 value
- session starts (for example game launch)
- custom_01 (+ the other 2) is retrieved from local db (if found)
- the user event (session start) get the retrieved custom_01 value added
- an event is triggered and custom_01 value is set in the event
- player removes class and custom_01 is set to empty (also in db)
- Custom_01 is not added on any further events
This will allow dimension custom_01 to have the filter values ninja and wizard available for selection in the tool. For example visualizing Daily Active Users (DAU) filtered by players who were playing a ninja.
- Think about the need you have for filtering data
- Decide what area each custom dimension should track
- Decide on a finite list of dimension values for each. Using too many will block your game
- Don’t use all 3 straight away unless you are certain of how to use it
DO NOT use custom dimension for these
As they are already tracked or can be tracked in other ways.
- device information
- os version
- gender
- age
- platform
- level progression (use progression)
- never use float or timestamps or other dynamic content
Popular usage for custom dimensions
- player class (ninja, wizard)
- player affiliation (horde, alliance)
- player persona (social, power_gamer)
- player level (1_4, 5_8, 9_12 .. 97_100)
Limitations
These are the technical limitations.
- size of POST request
- event field validation
- frequency of unique values
Size of POST request
The collector has a POST size limit of 1MB in the request body.
Read more in the troubleshooting section on 413 status codes.
Event field validation
When events are submitted they will each be validated. These validation rules are listed by each event in this documentation.
Read more in the troubleshooting section on 400 status code.
Frequency of unique values
GameAnalytics have some additional limitation regarding the frequency of unique values for certain events. If these thresholds are exceeded during the day then the GameAnalytics servers will start to throttle the game.
Throttle
When a game is being throttled it means that certain processing of the raw data has stopped. This is often due to events containing too many unique values that result in aggregation being hard/impossible.
It is worth specifying that the collection of events is not suspended.
The result in the tool will be metrics flatlining. Many core metrics will still be there; like DAU etc. But event types (design, progression etc.) will not be updated.
How to solve it?
This happens rarely.
Contact support and get information about why the throttle was activated and how to fix the implementation to avoid it.
Limitations
These are the recommended unique value limitations.
Default annotations
Field | Unique Limit |
---|---|
build | 100 |
platform | 30 |
device | 500 |
os_version | 255 |
progression | 100 pr. event part |
custom_01 | 50 |
custom_02 | 50 |
custom_03 | 50 |
Business Event
Field | Unique Limit |
---|---|
event_id (itemType) | 100 |
event_id (itemId) | 100 |
cart_type | 10 |
Resource Event
Field | Unique Limit |
---|---|
event_id (virtualCurrency) | 100 |
event_id (itemType) | 100 |
event_id (itemId) | 100 |
Design Event
Field | Unique Limit |
---|---|
event_id (entire string) | 50000 * |
* This is a very large threshold. The amount of tree-nodes generated will also affect if the game will be throttled. Having this many is not recommended and it can affect the Gameanalytics tool experience (downloading that much information to the browser).
Troubleshooting
The servers validate fields and reject events that do not pass. Therefore it is valuable to know which field(s) did not pass validation and the reason why.
The most common HTTP response status codes 200
, 401
, 400
.
200 : OK
The request went well. All possible events sent were collected successfully.
401 : UNAUTHORIZED
Authorization could not be verified. Either the game keys are not correct or the implementation is not done properly for calculating the HMAC hash for the Authentication header.
[
{
"errors": [
{
"error_type": "not_in_range",
"path": "/gender"
}
],
"event": {
"gender": "alien",
"connection_type": "wifi",
"session_num": 1,
"session_id": "b887216b-3cfa-11e5-b8a9-a8206618c53b",
"device": "iPhone6.1",
"manufacturer": "apple",
"category": "user",
"user_id": "AEBE52E7-03EE-455A-B3C4-E57283966239",
"client_ts": 1438948324,
"os_version": "ios 8.2",
"custom_01": "ninja",
"engine_version": "unity 5.1.0",
"platform": "ios",
"sdk_version": "rest api v2",
"build": "alpha 0.0.1",
"v": 2
}
}
]
413 : REQUEST ENTITY TOO LARGE
The collector has a size limit of 1MB in the request body.
If the post body is less than 2 times the max limit (between 1M and 2MB) you get a 413 response code.
If it is bigger than that you will get a closed connection from the collector.
Therefore when submitting a large amount of events the code should split them up. Use multiple requests one after the other. Again it is highly recommended to use gzip as this will reduce the size significantly.
400 : BAD REQUEST
This can happen in the following scenarios.
- the data sent was not valid JSON (unable to decode)
- the JSON data sent was not a list (read more here)
- the JSON contents (events) failed validation
In the first 2 cases there could be little information in the response.
The last case will happen when one or more events fail to match it’s validation schema. The servers will then not collect those failed events and reply with a JSON string containing a list of objects for each event that failed. Each error object will contain information about all the fields that might have failed validation for that specific event and also include the event fields that were submitted.
When receiving a 400
status code during implementation please review the response JSON to ascertain if it’s valid and a list. Then review what fields did not pass validation and fix.
Look at the 400
response error reply snippet example in the JSON tab.
Re-submitting events?
When queued events are sent to the collector servers they should each be marked locally as being submitted.
If the attempt failed due to no connection (no network etc.) or 413 (body too large) then the being submitted events should be put into queue again. If you get a 413 then try to split the events into even smaller batches and then submit.
For all other responses (200
, 401
, 400
etc.) these events should be wiped.
Do not keep events and resubmit based on other reasons than offline or the 413 response.
In production a 400
response should be logged and the implementation fixed for the next time the game is released.
Character encoding
Make sure your character encoding is UTF-8 for strings.
Certain event_id strings require specific characters validated by matching a regex.
An example is the progression event_id string. This requires a match for this regex:
^(Start|Fail|Complete):[A-Za-z0-9\s-.\(\)\!\?]{1,64}(:[A-Za-z0-9\s-.\(\)\!\?]{1,64}){0,2}$
The validation for this event_id can fail if using UTF-8 strings dynamically (like level names). These might contain other characters then just a-Z and numbers. Make sure to encode strings to support the requirements.
Server implementation
It is possible to submit events from a server on behalf of clients. A scenario could be a multiplayer server keeping track of sessions for all clients connected.
Even though this is possible it is not recommended.
Country lookup by IP
The GameAnalytics collectors will inspect the request IP and perform a GEO look-up for country information.
If all the events are submitted by a single server then the country for all users would be the same (the country the server is located in). This can be solved by forwarding the client IP in the request when sending events.
This is done using the standard X-Forwarded-For HTTP header. For example if your client has the IP 1.2.3.4 then the header to be included in the request should look like this…
X-Forwarded-For: 1.2.3.4
Read more about this header on wikipedia.
Request per user
As you can submit many events in the same request (a batch) it would be tempting to send events from multiple users at the same time. This is not recommended as you specify one IP per request in the header X-Forwarded-For and thus all the events submitted will be annotated with the country resolved by that single IP.
It is needed to submit a request per user and specify the IP. A way to obtain this on the server would be to…
- collect intended events for users inside buckets per IP (user)
- activate a synchronous loop (one after the other) that submit events for each bucket (user) using the IP in the forward header
- wait a few seconds (15-20) and activate the loop again submitting events collected
This will make sure each user is resolved properly by country and our servers are not spammed with requests.
Session End
The server should keep track of the session time for each user. When it is detected that a user is no longer playing the game it is important to add (submit) a session end event for that user and session_id.
Examples
Python Example
----- GameAnalytics REST V2 Example --------
Gzip enabled!
--------------------------------------------
Init call successful !
Events submitted !
Events submitted !
Download a Python example here using the sandbox api and sandbox game key and secret key. The code is implementing several vital areas.
- HMAC Authorization hash
- gzipping
- simple memory queue (use something like SQLite instead)
- server-time adjustment
- default annotations function
- init / event routes etc.
Run the example from the terminal.
python REST_v2_example.py
Example steps
- make an init call
- check if disabled
- calculate client timestamp offset from server
- start a session
- add a user event (session start) to queue
- add a business event + some design events to queue
- submit events in queue
- add some design events to queue
- add session_end event to queue
- submit events in queue
Java Example
//JAVA
Status ok. Integration initialized.
Status ok! 10 events sent!
Status ok, session_end event sent!
Download a JAVA example here using the sandbox api and sandbox game key and secret key. The code is implementing several key areas. This example can be used in non-Android games as it is not using any platform dependant library.
- HMAC Authorization hash
- simple queue (use something like SQLite instead and create a local storage)
- default annotations function
- init / event routes and basic events composition etc.
Run the example in a Java IDE i.e. Eclipse Oxygen 2
Example steps
- make an init call
- check if disabled
- add a user event (session start) to queue
- add some design events to queue
- add a business event
- add some progression events (Start, Fail, Complete)
- add some resource (Sink and Source)
- add an error event (Info)
- submit current existing events from queue
- add session_end event and send