Appearance
Appearance
Metaplay supports streaming Analytics Events from the Game Servers to various External Storages. This allows integrating with various 3rd party or custom-made analytics pipelines and ETL solutions to process and visualize the Events. Metaplay itself doesn't implement any ETL capabilities directly.
To better understand how to start collecting Analytics Events in your game, see Implementing Analytics Events in Game Logic.
The flow of Analytics Events from the source to the Sinks is described below. This is mainly for informational purposes, to better understand the system and the latencies involved.
PlayerModel.EventStream.Event(new PlayerEventXyz(...))
or directly on the server via, eg, PlayerActor._analyticsEventBatcher.Enqueue(new PlayerEventXyz(..))
.INFO
Currently, collecting Analytics Events is only supported in PlayerActor
and GuildActor
. In the future, this will be generalized to more Entities.
The metadata for an Event is represented by the AnalyticsEventEnvelope
class, with the following members (matching the JSON):
Source
is the EntityId
of the Game Entity where the Event was originated. Events are always associated with an Entity.ModelTime
is the logical time when the Event happens (taken from PlayerModel.CurrentTime
). Due to buffering and network latencies, the actual time when the event is captured on the server might actually happen somewhat after the logical time.CollectedAt
is the timestamp when the Event was collected at the Game Server. This is currently disabled, as it wasn't considered to be useful. If it feels useful, let us know and we can enable it.)UniqueId
is a fully unique identifier assigned to the Event. It's mainly intended for de-duplicating Events in case the Sink implementation can cause duplicates due to retry-based error handling. It can also be used to correlate Events in the Entity's Event Log with those outputted by the Sinks.EventType
is the name of the C# class that represents the Event type.SchemaVersion
is the version of the Event. It is useful for making it easier to handle changes in the Event structure when processing in an ETL pipeline.Payload
contains the Event-specific parameters.Context
contains the Source
-specific context data. For players, this contains the current sessionNumber
or null
if event was emitted outside a session, and the active experiments
and for each the active variant.Labels
contains the arbitrary custom data emitted by the source Game Entity.Some example Events (after conversion to JSON & pretty-printed on multiple lines):
Player example event:
{
"source": "Player:X9Pq78AMqb",
"modelTime": "2021-09-28T15:11:28.6980000Z",
"uniqueId": "0000017C2CF66B0A-3E044240ECE36CAC",
"eventType": "PlayerEventClientDisconnected",
"schemaVersion": 1,
"payload": {
"sessionToken": "54579D4B2A2EEF2B"
},
"context": {
"sessionNumber": 2,
"experiments": {
"_egf": "_fast"
}
},
"labels": {
"Location": "FI"
}
}
Guild example event:
{
"source": "Guild:ExeVdW76C3",
"modelTime": "2021-09-28T15:33:46.8140000Z",
"uniqueId": "0000017C2D0A85B3-51862C7D1EF5F073",
"eventType": "GuildEventMemberPoked",
"schemaVersion": 1,
"payload": {
"pokingPlayerId": "Player:X9Pq78AMqb",
"pokingPlayerMemberInstanceId": 0,
"pokingPlayerName": "Guest 80142",
"pokedPlayerId": "Player:X9Pq78AMqb",
"pokedPlayerMemberInstanceId": 0,
"pokedPlayerName": "Guest 80142",
"pokedCountAfter": 1
},
"context": {},
"labels": {}
}
The purpose of the Sinks is to take the incoming Event Batches, convert them to desired format (eg, JSON or Parquet), optionally collect multiple Batches to larger Chunks, and then flush the Chunks out to the External Storage of choice (eg, S3, Kinesis, BigQuery, Amplitude, or similar).
It is up to the Sink how this process should be done, what formats and External Storage it supports, what (if any) compression methods it supports, how it handles failures and retries, and so on. Metaplay ships with a built-in JSON-to-S3 and a built-in BigQuery Sinks. The JSON-to-S3 is additionally a reference implementation of a Sink that works as an example of how to solve the common problems in a realistic manner and acts as a starting point for implementing your custom Sinks.
Metaplay ships with a built-in Sink AnalyticsDispatcherSinkJsonBlobStorage
that supports writing JSON data into an S3 bucket.
It is mainly intended to be a reference implementation of how a Sink can be implemented, but may also be useful in some production scenarios.
Instead of writing directly to S3, the Sink uses the IBlobStorage
interface, which will write to the disk when running the server locally. The files are written to a configurable path, by default Game.Server/bin/PrivateBlobStorage
with paths like ServerAnalytics/2021/12/31/<hostname>-235901-b7ad85cd.json.gz
.
It demonstrates the following key concepts:
MemoryStreams
) to write out data in the background, in parallel. The uploading is retried a few times to avoid temporary glitches from causing data loss. A fixed number of buffers is kept to force an upper limit of memory usage for the Events, to avoid the server running out of memory. The implementation can only handle short periods of inability to write before it starts dropping events.While the JSON-to-S3 Sink is mainly intended as a reference implementation for building custom Sinks, it may still be useful in real-life production settings as well. For example, it it possible to hook an AWS Lambda Function to trigger when files are written to the S3 bucket, and then perform initial processing of the Events and write them out to any External Storage.
The sink can be configured via the Runtime Options, e.g. Options.base.yaml
:
AnalyticsSinkJsonBlobStorage:
Enabled: false
FilePath: "ServerAnalytics/{Year}/{Month}/{Day}/{HostName}-{Hour}{Minute}{Second}-{UniqueId}.json{CompressSuffix}"
CompressMode: Gzip # None to disable compression
EventsPerChunk: 100000 # Target number of events per chunk (100k ~= 20MB uncompressed, assuming 200 bytes/event)
MaxPendingDuration: 00:05:00 # Maximum amount of time an event can be buffered before the batch is flushed
NumChunkBuffers: 10 # Number of chunk upload buffers to use (if all become filled due to slow/failed uploads, subsequent events are dropped)
# Use these to write to a custom S3 bucket
#S3BucketName: custom-bucket-name
#S3Region: eu-west-1
The data is then stored in the {project-name}-{environment}.p1.metaplay.io-private
s3 bucket. You can get credentials to access it using the following command:
metaplay-auth get-aws-credentials
You can check out the metaplay-auth
and aws
section of the Manually Deploying a Game Server to the Metaplay-Managed Cloud page for more details.
Metaplay ships with a built-in Sink AnalyticsDispatcherSinkBigQuery
that supports writing analytics event data into an BigQuery table. Each event is stored in the BigQuery table as a single row. Since this row-format does not support arbitrarily nested data structures, all Payload data is flattened into a list of key-value pairs. The format is described in BigQuery Analytics Event Format.
Private repo access needed
Metaplay's Terraform modules are available in the private metaplay-shared Github organization. Please reach out to your account representative for access to it.
To set up the target BigQuery table, you can use the Metaplay BigQuery Analytics infra module. The process is described in the repository documentation. Using the built-in module is recommended as it will automatically set a proper schema table and set up the table with sensible default values.
The sink can be configured via the Runtime Options. If you are using the built-in infra modules for setting up the table, as defined above, the Project, Dataset and Table information and Credentials will be automatically set.
AnalyticsSinkBigQuery:
Enabled: true
# Maximum number of events per insert batch
EventsPerChunk: 100
# Maximum amount of time an event can be buffered before the batch is flushed
MaxPendingDuration: 00:00:30
# Number of chunk upload buffers to use (if all become filled due to slow/failed
# uploads, subsequent events are dropped)
NumChunkBuffers: 10
# Enables the native BigQuery row deduplication based on insertId. If this is
# disabled, data processor should deduplicate events based on event_id column.
# This is enabled by default.
BigQueryEnableRowDeduplication: false
# Target table
BigQueryProjectId: "myproject"
BigQueryDatasetId: "mydataset"
BigQueryTableId: "mytable"
# file path to the credentials, or aws-sm:// url for credential from AWS Secrets
# Manager
BigQueryCredentialsJsonPath: xxx
The simplest way to get started with a custom Sink is to copy the existing AnalyticsDispatcherSinkJsonBlobStorage
and use it as a basis. It already solves many common issues that Sinks need to address, so no need to re-invent the wheel on those.
An instance of the custom sink needs to be registered to the AnalyticsDispatcher
on server start. This is done by introducing a class that derives from AnalyticsDispatcherSinkFactory
and returns the initialized custom sink in its CreateSinksAsync
method:
public class MyAnalyticsDispatcherSinkFactory : AnalyticsDispatcherSinkFactory
{
public override Task<IEnumerable<AnalyticsDispatcherSinkBase>> CreateSinksAsync()
{
AnalyticsDispatcherSinkBase customSink = new MyCustomAnalyticsDispatcher();
return Task.FromResult(Enumerable.Repeat(customSink, 1));
}
}
The main interface for a Sink is the EnqueueBatches()
method. The Analytics Dispatcher uses it to append any new Event Batches to the Sink. The method is called frequently (once per second) even if no Events have occurred, so it can be used to implement time-based flushing. It is up to the Sink to do whatever it wishes with the Events.
There's a convenience ChunkBufferManager
class, which can be used to keep a fixed-size pool of buffers (MemoryStreams
) for writing the Events into, and then flushing to the External Storage in the background.
Configuring the Sink is best done using the Runtime Options. Unfortunately, there's no reference for this at this time.
Things to consider:
In addition to game-specific events, Metaplay emits certain metadata events to enable integration with Analytics Pipelines. For these events, the Source
entity may not be defined. Current metadata events are:
PlayerDeleted
event is emitted when Player is being deleted, possibly as a result to a GDPR data removal request (Right to Erasure). Upon receiving this event, the consuming pipeline should make sure all Personal Data of this player is deleted. The Source
of this event is the PlayerId of the player being removed.
Note that Metaplay does not remove any events from this player from BigQuery Table automatically. It is expected that the developer has either:
PlayerEventFacebookAuthenticationRevoked
with Source
= DataDeletionRequest
is emitted when a User that logged in with a Facebook Account chooses to Submit Facebook Data Deletion Request on Facebook Settings & Privacy > Settings > Apps and Websites page. Upon receiving this event, the consuming pipeline should make sure all Facebook-sourced data of this player is deleted. The Source
of this event is the PlayerId of the player user has logged on to.
Currently only data collected is the App-Scoped User Id which may be present in Social Login related Analytics Events. As the User ID is App-Scoped, i.e. all Apps use a separate User ID namespaces, it cannot be associated with any natural person and is not Personal Data.
ServerEventExperimentInfo
and ServerEventExperimentVariantInfo
events are emitted on server startup and whenever Game Configs are changed. ServerEventExperimentInfo
is emitted for each Experiment and it contains information about the experiment, such as the human-readable display name, description, internal Experiment Id, and the Analytics ID. Similarly, ServerEventExperimentVariantInfo
is emitted for each variant of each active Experiment, and it contains both the Analytics ID and the internal Id of the experiment.