NAV
http shell json python csharp

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.

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

  1. Use the sandbox-api endpoint and sandbox keys during the implementation phase.
  2. 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.

  1. Set the header Content-Encoding header to gzip
  2. Gzip the events JSON string and add the data to the POST payload
  3. 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.

Run in Postman

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.

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).

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.

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.


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

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.

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.

virtualCurrency

A custom string defining the type of resource (currency) used in the event.

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.

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.

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…

It is possible to use 1, 2 or 3 values depending on your game. For example…

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.

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.


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.


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

  1. player changes class to ninja and this is registered locally (variable and local db) as custom_01
  2. an event is triggered and custom_01 value is set in the event
  3. player changes class to wizard and this is registered locally (variable and local db) as custom_01
  4. an event is triggered and custom_01 value is set in the event
  5. session is ending and the session end event also get the custom_01 value
  6. 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
  7. an event is triggered and custom_01 value is set in the event
  8. player removes class and custom_01 is set to empty (also in db)
  9. 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.

DO NOT use custom dimension for these

As they are already tracked or can be tracked in other ways.


Limitations

These are the technical limitations.

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.

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…

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.


Run the example from the terminal.

python REST_v2_example.py

Example steps

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.


Run the example in a Java IDE i.e. Eclipse Oxygen 2

Example steps