Setup
A Collector is a GameAnalytics server (one of many) that receive and collect game events being submitted using the HTTPS protocol. All official GameAnalytics SDK’s are using this same API.
Game Keys
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 Endpoints
- Only 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.
Do NOT use production keys on the sandbox-api; it will only authorise the sandbox game keys.
Sandbox Keys
Key | Code |
---|---|
Game Key | 5c6bcb5402204249437fb5a7a80a4959 |
Secret Key | 16813a12f718bc5c620f56944e1abc3ea13ccbac |
Event Processing
Events should be ingested and visible using the Live Events view inside the Realtime page within a few seconds. After a few minutes, events should also be visible within the Realtime dashboard and also across the tool.
Gzip
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
The collector has a POST size limit of 1MB.
Look at the code example for gzip and HMAC in python below. We also provide a full example with python at the end of this document.
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)
Authentication
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 shell, python and C#.
- Shell
- Python
- C#
echo -n '<body_contents>' | openssl dgst -binary -sha256 -hmac "<game secret>" | base64
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='
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);
}
}
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
2 routes are supported.
- Init:
POST /v2/<game_key>/init
- Events:
POST /v2/<game_key>/events
The API version is part of the URL. Currently it’s v2. Replace the <game_key>
in the URL with your specific game key.
Validation for JSON body content is described later in the documentation.
Init / Remote configs
The init call should be requested whenever a new session starts or at least when the game is launched.
Use the init call to retrive remote configs, A/B testing id, A/B testing variant id and to adjust client timestamp.
First run (no configs hash)
POST /remote_configs/v1/init?game_key=<game_key>&interval_seconds=<int>
On first run when you haven’t have received a configs hash yet send the init request without the configs_hash parameter.
After first run (using configs hash)
POST /remote_configs/v1/init?game_key=<game_key>&interval_seconds=<int>&configs_hash=<configs_hash>
The response from the first init request will contain a configs_hash value which you need to save for the next time you send the init request.
- HTTPS
- Shell
- Python
- C#
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"}' https://sandbox-api.gameanalytics.com/v2/<game_key>/init
import json
import urllib2
game_key = '5c6bcb5402204249437fb5a7a80a4959'
url_init = 'https://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 = "https://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);
}
}
The POST request should contain a valid JSON object as in the body containing the following fields.
Query string
Parameter | Description |
---|---|
game_key | String. The game key expressed in lower case hexadecimal alphabet |
interval_seconds | Int. The duration of the requested effective range interval in seconds as a non-negative integer. An absent value will be interpreted as 0. If the interval is 0 then a configuration for the timestamp_start instant will be returned. |
configs_hash | String. optional configs_hash value that might have been returned in a previous request to Remote Config service. |
JSON body
Field | Description |
---|---|
user_id | A unique identifier of the user. This is used to detect if the user should be enrolled for A/B testing |
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” |
build | Optional. Build version of the app. |
random_salt | Random salt used for encryption. |
Server responses
A “big response” with HTTP 201 Created, or “small response” with HTTP 200 OK, if the ‘configs_hash’ indicates that the SDK still holds the relevant version of the configuration.
The server response is a JSON object with the following fields.
Field | Description |
---|---|
server_ts | Int (epoch). |
configs | Array. An array of SDK configuration objects. 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. Only sent for HTTP status 201. |
key | String. key. Only sent for HTTP status 201. |
value | String. value. Only sent for HTTP status 201. |
start_ts | Int (epoch). Start timestamp for configuration. Only sent for HTTP status 201. |
end_ts | Int (epoch). End timestamp for configuration. Only sent for HTTP status 201. |
configs_hash | String. Configs hash. Save for next time calling init request. Only sent for HTTP status 201. |
ab_id | String. A/B testing id. Only added if part of an A/B testing experiment. Only sent for HTTP status 201. |
ab_variant_id | String. A/B testing variant id. Only added if part of an A/B testing experiment. Only sent for HTTP status 201. |
Example responses
A user does not participate in an experiment. Only Global configs are received.
HTTP 201 Created
{
"server_ts": 1546300000,
"configs": [
{
"key": "enable_rocket_launcher",
"value": "true",
"end_ts": null
},
{
"key": "gravity",
"value": "80",
"start_ts": 1546300111,
"end_ts": null
},
{
"key": "mushroom_power",
"value": "32",
"end_ts": 1546399999
}
],
"configs_hash": "1448877966322221"
}
A user participates in an experiment. Only variant config is received (no Global configs).
HTTP 201 Created
{
"server_ts": 1546300000,
"configs": [
{
"key": "background_color",
"value": "#ff0000",
"end_ts": null
}
],
"configs_hash": "124595093888900",
"ab_id": "aaabc",
"ab_variant_id": "1"
}
A user is in the control group. No configs are received, only experiment and variant IDs.
HTTP 201 Created
{
"server_ts": 1546300000,
"configs": [],
"configs_hash": "3105646955558893",
"ab_id": "aaabc",
"ab_variant_id": "ctrl"
}
The ‘configs_hash’ hasn’t changed. SDK should follow the Configs from the last response.
HTTP 200 OK
{
"server_ts": 1546300000
}
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
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]"
}
]
Events always need to be in a list even if you are sending only 1 event.
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.
When your game implementation is running it should cache events (local db recommended) and periodically (each 20 seconds for example) submit all stored events using the events route.
When offline the code should also queue events and submit once the connection is restored. Recommended to define a maximum size for the cache that (when exceeded) will activate a trim deleting all events for the oldest sessions.
- HTTPS
- Shell
- Python
POST /v2/<game_key>/events HTTP/1.1
Host: sandbox-api.gameanalytics.com
Authorization: <authorization_hash>
Content-Type: application/json
[
{"category": "user", "<event_fields>": ""},
{"category": "business", "<event_fields>": ""}
]
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>]' https://sandbox-api.gameanalytics.com/v2/<game_key>/events
import json
import urllib2
game_key = '5c6bcb5402204249437fb5a7a80a4959'
url_events = 'https://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!"
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.
Note that GameAnalytics will not store the IP on our servers. It is only used to resolve to a country abbreviation string like US.
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.
Python Example
Download a Python example here using the sandbox api and sandbox game key and secret. The code is implementing several key areas.
- HMAC Authorization hash
- gzipping
- simple memory queue (use something like SqlLite 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
- add some design events to queue
- add session_end event to queue
- submit events in queue
Java Example
Download a Java example here using the sandbox api and sandbox game key and secret key. The code is implementing several core areas.
- HMAC Authorization hash
- simple queue (use something like SqlLite instead and create a local storage)
- default annotations function
- init / event routes and basic events composition
Run the example in a Java IDE i.e. Eclipse Oxygen 2
Example steps
- make an init call
- check if disabled
- start a session (session_id generation)
- 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