Header photo by Lucas Pezeta

In the previous article we covered the why of analytics and the funnel we defined for the Dinopirates Playtest. Now comes the how: from configuring the Unity Dashboard to the initialization code.

The goal of this article is to get Unity Analytics running in your project with good practices from the start.

Configuration in Unity Dashboard

Before writing a single line of code, the project needs to be configured in dashboard.unity3d.com.

Linking the project

In the Unity Dashboard, once the project is initialized, the Analytics service should activate automatically. To validate it, go to the Services tab and scroll to the bottom of the page until you find the Analytics service.

alt text

In Unity Editor, you can verify the project is linked correctly by going to Edit → Project Settings → Services. The Project ID should appear there — the unique identifier Unity uses to associate the events your game sends with the dashboard.

alt text

NOTE: In the early days of Dinopirates development, we evaluated using GitHub or Plastic SCM as our version control system. We chose Plastic since, being Unity’s own solution, it handles conflicts and .meta file management better. One byproduct of that decision is seamless integration with Unity Services, including Analytics. If you use a different version control system, make sure to configure the Project ID correctly in your project.

Environment management

A good practice we implemented from the start in Dinopirates is separating data by environment. Unity Dashboard allows you to create multiple environments per project. We use three:

EnvironmentWhen it’s used
developDebug builds, local editor testing
stagingInternal QA builds. This is the environment Playtest users interact with
productionPublic builds — eventually the Demo or final release

This prevents test traffic from contaminating real data. Seeing 500 deaths in develop when they’re actually runs from the team testing balance is exactly the kind of noise that ruins a dataset.

In the Dashboard, on the project home, look for the Environments tab before starting the integration.

alt text

Installing the package

In Unity 6, install the package from Window → Package Manager → Unity Registry → Analytics, or add it directly to Packages/manifest.json:

"com.unity.services.analytics": "6.3.0"

alt text

Verify the latest version in the Package Manager. The API changed significantly between version 4.x and 6.x. This article covers 6.x.

Initialization with environment selection

Initialization goes in the first system that loads in your game. The key point is passing the correct environment name to InitializationOptions, so that events reach the right environment based on the build type.

async void InitAnalytics()
{
    var options = new InitializationOptions();
    options.SetEnvironmentName(GetCurrentEnvironment());
    await UnityServices.InitializeAsync(options);

    if (consentAlreadyGranted)
        OptInAnalytics();
}

string GetCurrentEnvironment()
{
    if (Debug.isDebugBuild)
        return "develop";

#if TANGARA_QA_ENABLED
    return "staging";
#else
    return "production";
#endif
}

Separating environments by Debug.isDebugBuild and scripting symbols ensures no test data reaches production, without having to remember to change anything manually between builds.

The TANGARA_QA_ENABLED variable is a custom scripting symbol we set specifically for the Playtest build. It’s configured in the build profiles.

alt text

Unity Analytics does not collect data by default. The SDK expects you to explicitly call opt-in after the user accepts. If you skip this step, nothing reaches the dashboard.

This isn’t an arbitrary restriction: it’s compliance with GDPR and similar privacy regulations. You must ask the player for permission before collecting any data. At Tangara Studio we also provide our users with a declaration about data usage and its purpose. It’s important to have a privacy policy in place.

The correct flow

Game starts

Do we have a saved answer?
    ├── Yes, Granted → Direct OptIn, no alert shown
    ├── Yes, Denied  → Don't collect, continue
    └── No           → Show consent alert
                          ├── Accepts → save Granted, OptIn
                          └── Declines → save Denied

Implementation

In Dinopirates, the TSAnalyticsManager class handles this by reading the saved state from player settings before initializing services. Below is this class, which you can adapt to your own project:

// TSAnalyticsManager.cs
using System;
using UnityEngine;
using UnityEngine.UnityConsent;
using Unity.Services.Core;
using Unity.Services.Analytics;
using Unity.Services.Core.Environments;
using MoreMountains.Tools;

[Serializable]
public enum TSAnalyticConsent
{
    None,
    Granted,
    Denied,
}

public enum DPEnvironment
{
    Develop,
    Staging,
    Production
}

public class TSAnalyticsManager : MMPersistentSingleton<TSAnalyticsManager>
{
    [MMReadOnly] public TSAnalyticConsent CurrentAnalyticConsent = TSAnalyticConsent.None;

    public static DPEnvironment CurrentEnvironment() {
        if (Debug.isDebugBuild == true)
        {
            return DPEnvironment.Develop;
        }
        else
        {
#if TANGARA_QA_ENABLED
            return DPEnvironment.Staging;
#else
            return DPEnvironment.Production;
#endif
        }
    }

    public void ValidateConsent(Action<TSAnalyticConsent> callback)
    {
        LoadSettings();
        InitAnalytics(); // Async: initializes services and calls OptIn if consent was already granted

        callback(CurrentAnalyticConsent);
    }

    async void InitAnalytics()
    {
        var options = new InitializationOptions();
        options.SetEnvironmentName(CurrentEnvironment().ToString().ToLower());
        await UnityServices.InitializeAsync(options);

        if (CurrentAnalyticConsent == TSAnalyticConsent.Granted)
        {
            OptInAnalytics();
        }
    }

    void LoadSettings()
    {
        // Load Settings
        // TSSaveLoadManager is a class we use to load and save player preferences. In this case,
        // analytic consent is saved in the settings so we don't have to ask the player every time they start the game.
        var settings = TSSaveLoadManager.LoadSettings();
        if (settings != null)
        {
            CurrentAnalyticConsent = settings.AnalyticConsent;
        }
    }

    // Opt In/Out & Delete request
    public void ChangeAndSaveNewConsentState(TSAnalyticConsent newState)
    {
        CurrentAnalyticConsent = newState;
        TSSaveLoadManager.SaveSettings(new() {
            AnalyticConsent = CurrentAnalyticConsent,
        });
    }

    public void OptOutAnalytics()
    {
        ChangeAndSaveNewConsentState(TSAnalyticConsent.Denied);
        var consentState = EndUserConsent.GetConsentState();
        consentState.AnalyticsIntent = ConsentStatus.Denied;
        EndUserConsent.SetConsentState(consentState);
    }

    public void OptInAnalytics()
    {
        var consentState = EndUserConsent.GetConsentState();
        consentState.AnalyticsIntent = ConsentStatus.Granted;
        EndUserConsent.SetConsentState(consentState);
    }

    public void RequestDataDeletion()
    {
        var consentState = EndUserConsent.GetConsentState();
        consentState.AnalyticsIntent = ConsentStatus.Denied;
        EndUserConsent.SetConsentState(consentState);
        AnalyticsService.Instance.RequestDataDeletion();
    }
}

In practice, the first time a player opens the game, a simple alert is shown before the main menu. If they accept, Granted is saved and OptIn is called. On subsequent launches, the state is already saved and the prompt doesn’t appear again.

alt text

  • Clear, brief text. Something like: “Help us improve the game by sharing anonymous session data?” is enough.
  • Both buttons (accept and decline) equally visible. Don’t hide the rejection.
  • Save the preference. Nobody wants to see that screen every time they open the game.
  • Respect the decision. If they decline, don’t degrade the experience in any way.

What you get with the base integration

With this working —without having defined any custom events yet— Unity Dashboard already starts showing data. The most useful for an indie:

MetricWhat it measures
DAU / WAU / MAUActive users per day, week, and month
New vs Returning UsersHow many players are new each day
Average Session LengthAverage duration of each session
Sessions per DAUHow many sessions each active user averages
CountriesWhere players are playing from
Operating SystemsDistribution of Windows, macOS, Linux
Hardware (CPU / GPU)What hardware players have

For a Playtest, this already answers important questions: are people coming back to play? How much time do they spend per session? Where are they playing from? Does your audience’s hardware match your performance targets?

In the dashboard, the information will look similar to this.

For Countries, Operating Systems, or Hardware data, you need to generate custom reports.

alt text

As an example, here are all the parameters stored in the clientDevice event. As you’ll notice, there’s a lot of other valuable information available, such as RAM state, screen resolution, or timezone. It’s important to review what data the SDK already provides before adding custom parameters that could be redundant.

{
  "batteryLoad": 1,
  "buildGUUID": "2c82e431b0ec48d6807d4XXXXXXXXX",
  "clientVersion": "0.1",
  "collectInsertedTimestamp": "2026-05-28 20:45:45.309",
  "cpuCores": 16,
  "cpuType": "AMD Ryzen Z1 Extreme",
  "eventDate": "2026-05-28 00:00:00.000",
  "eventID": 3526528638864123123,
  "eventLevel": 0,
  "eventName": "clientDevice",
  "eventTimestamp": "2026-05-28 20:45:44.966",
  "eventUUID": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
  "eventVersion": 1,
  "gaUserAcquisitionChannel": "None",
  "gaUserAgeGroup": "UNKNOWN",
  "gaUserCountry": "CL",
  "gaUserGender": "UNKNOWN",
  "gaUserStartDate": "2026-05-22 00:00:00.000",
  "gpuType": "AMD Ryzen Z1 Extreme (RADV PHOENIX)",
  "idfv": "de824a99e67b1d937c83a0c1ce68b87XXXXXXXXX",
  "mainEventID": 352652863886123123,
  "msSinceLastEvent": 28,
  "platform": "PC_CLIENT",
  "projectID": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
  "ramTotal": 11645,
  "screenHeight": 1080,
  "screenResolution": 96,
  "screenWidth": 1920,
  "sdkMethod": "com.unity.services.analytics.Events.Startup",
  "sessionID": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
  "timezoneOffset": "-0400",
  "unityInstallationID": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
  "userCountry": "CL",
  "userID": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
}

With the base up and running, the next step is defining the events that matter for your specific game. That’s what the next article covers: custom events, how to structure them in code, and how to build the funnel in the Dashboard.


Series: Analytics, you can check the other parts here pt1 and pt3