Skip to content

Analytics

OxHeart provides a thin analytics abstraction — IAnalyticsService — that gives your game a single call site for telemetry without coupling it to any specific backend. When the toggle is on but no backend is registered, the engine uses NullAnalyticsService, a silent no-op.

Enabling

  1. Open your ServiceConfiguration asset.
  2. Enable Analytics Service (under Feature Services).

That's it — NullAnalyticsService is registered automatically. All TrackEvent calls will be silently discarded until you provide a real backend.

Tracking Events

var analytics = ServiceLocator.Instance.Get<IAnalyticsService>();

// Simple event
analytics.TrackEvent("game_started");

// Event with parameters
analytics.TrackEvent("level_complete", new Dictionary<string, object>
{
    { "level", 3 },
    { "score", 12500 },
    { "time_seconds", 45.2 }
});

TrackEvent is fire-and-forget. The backend owns batching, buffering, and transmission — your game code doesn't need to care.

Runtime Enable/Disable

analytics.SetEnabled(false);  // pauses dispatch (e.g. user opted out)
analytics.SetEnabled(true);   // resumes

Providing a Backend

Create your own IAnalyticsService implementation and register it via IServiceRegistrar. Registrars run after the engine's own services, so yours replaces the default NullAnalyticsService:

public class MyAnalyticsService : IAnalyticsService
{
    public bool IsEnabled { get; private set; } = true;

    public void SetEnabled(bool enabled) => IsEnabled = enabled;

    public void TrackEvent(string eventName, IReadOnlyDictionary<string, object> parameters = null)
    {
        if (!IsEnabled) return;
        // Forward to your telemetry backend
    }

    public void Initialize() { /* SDK init */ }

    // NOTE: the engine does NOT call this on a service you registered yourself.
    // Call it from your own teardown - see "Lifecycle" below.
    public void Shutdown()   { /* flush/teardown */ }
}

[Preserve]
public class MyAnalyticsRegistrar : IServiceRegistrar
{
    public int Priority => 0;

    public void RegisterServices(IServiceLocator serviceLocator, ServiceConfiguration config)
    {
        if (!config.EnableAnalyticsService) return;

        var service = new MyAnalyticsService();
        service.Initialize();
        serviceLocator.Register<IAnalyticsService>(service);
    }
}

See Samples~/Scripts/13_AnalyticsSample.cs for the TrackEvent call site in isolation, and Samples~/Analytics/ for a complete interactive scene — buffering backend, opt-out toggle, and a Simulate Quit button that shows Shutdown() flushing the batch.

What Your Backend Owns

The interface is deliberately minimal, so several things are the backend's responsibility rather than the engine's. Worth knowing before you write the wrapper:

  • Asynchronous start-up. Initialize() is synchronous, and the engine has no "backend ready" concept. Telemetry SDKs commonly need an asynchronous start — dependency checks, service initialization — before they will accept an event. Buffer events inside your implementation until your SDK is ready, then flush. The Samples~/Analytics/ backend already has exactly that shape; see Lifecycle below for the flush-on-shutdown half.
  • Consent. OxHeart neither gathers nor stores consent, and never starts collection on your behalf. If your backend requires an opt-in before collecting, gate it inside your implementation — SetEnabled() is the natural place to hang "start/stop collecting".
  • Event-name and parameter rules. The engine passes eventName and parameters through unchanged and validates neither. Backends impose their own limits: name character sets and lengths, parameter counts, and accepted value types. IReadOnlyDictionary<string, object> is deliberately wider than most backends allow, so convert — and drop or reject what your backend will not take — in your wrapper.
  • Batching, retries, and offline queueing. Not provided; they belong in the backend.

Lifecycle

A service you register through IServiceRegistrar is yours by default: you construct it, you initialize it, and the engine does not call LateInitialize() or Shutdown() on it. That matters more for analytics than for most services — a backend that batches events in memory and relies on Shutdown() to flush will silently lose the last batch of every session if nothing calls it.

Two ways to handle it. Preferred — hand the service to the engine with RegisterManaged:

public void RegisterServices(IServiceLocator serviceLocator, ServiceConfiguration config)
{
    if (!config.EnableAnalyticsService) return;

    var service = new MyAnalyticsService();
    service.Initialize();

    // RegisterManaged also enrols the service in the engine's lifecycle, so it receives
    // LateInitialize() (safe to resolve other services) and Shutdown() on engine shutdown.
    serviceLocator.RegisterManaged<IAnalyticsService>(service);
}

RegisterManaged registers exactly like Register, and additionally enrols the instance in the engine's service lifecycle. It never calls Initialize() — registrars run after service creation, so initialize your service yourself before registering, as above. It also retires the default NullAnalyticsService it replaces, so nothing is left half-alive.

Or keep ownership and tear down yourself, if you would rather not depend on engine ordering:

    serviceLocator.Register<IAnalyticsService>(service);
    Application.quitting += service.Shutdown;   // the engine will not call it for you

Either is fine — just don't do both, or Shutdown() runs twice. Make your Shutdown() idempotent regardless; it is cheap insurance and matches how the engine's own services behave. If your SDK already registers its own quit handler, you may not need either.

Two smaller notes on the replacement:

  • Registering over the default logs one Service of type IAnalyticsService ... is already registered. Overwriting. warning at startup. That is expected and harmless — it is the engine reporting that your backend won. With RegisterManaged you will also see a line confirming the default NullAnalyticsService was shut down.
  • If you never intend to use the no-op default, you can leave Analytics Service switched off in ServiceConfiguration and register your backend unconditionally. The toggle exists so that game code can call Get<IAnalyticsService>() safely before a backend is wired up, not because the engine requires it.

Design Notes

  • No config ScriptableObject. Unlike Audio or Localization, analytics has no engine-level config knobs — the backend owns all configuration. The toggle in ServiceConfiguration is sufficient.
  • No batching or session tracking. The engine abstraction is intentionally minimal. Batching, offline queues, and session management belong in the backend implementation, not the interface.
  • Null-object default (ADR-018). When enabled without a backend, every TrackEvent call is a no-op — no exceptions, no warnings.