Satori Analytics Module¶
The Satori module exposes Heroic Labs Satori–style live-ops and analytics through Nakama RPCs. The Unity client sends events, reads identity properties, audiences, feature flags, experiments, live events, inbox messages, and metrics; the server validates and forwards to your configured Satori pipeline. All traffic uses the same authenticated Nakama session as the rest of IntelliVerseX.
Overview¶
| Namespace | IntelliVerseX.Satori |
| Assembly | IntelliVerseX.Satori |
| Dependencies | IntelliVerseX.Core, IntelliVerseX.Backend, Nakama Unity SDK, Newtonsoft.Json |
| Entry point | IVXSatoriClient (singleton MonoBehaviour) |
| RPC client | IVXSatoriRpcClient (low-level RPC wrapper; constructed inside Initialize) |
Components¶
| Component | Responsibility |
|---|---|
IVXSatoriClient | Public API: events, identity, audiences, flags, experiments, live events, messages, metrics |
IVXSatoriRpcClient | CallAsync<T>, CallVoidAsync, UpdateSession; JSON envelope SatoriRpcResponse<T> |
Setup¶
Prerequisites¶
- Nakama Unity SDK (
com.heroiclabs.nakama-unity) withINTELLIVERSEX_HAS_NAKAMAdefined. - A valid Nakama
IClientand authenticatedISession(see Backend module). - Server-side RPC handlers registered for the IDs used by
IVXSatoriClient(e.g.satori_event,satori_flags_get, …)—see guides/satori-integration.md.
Initialization contract¶
IVXSatoriClient must be initialized before any async API runs. Initialize is synchronous and immediately sets the internal IVXSatoriRpcClient and raises OnInitialized(true). RefreshSession swaps the session on the RPC client after token refresh.
public void Initialize(IClient client, ISession session);
public void RefreshSession(ISession session);
Initialize throws ArgumentNullException if client or session is null.
Path A — IVXBootstrap (component ensured, then wire Nakama)¶
With EnableSatori enabled on your IVXBootstrapConfig (default true), IVXBootstrap.InitializeAsync creates an IVX_SatoriClient child object and adds IVXSatoriClient when needed. The bootstrap does not call Initialize(IClient, ISession); you supply Nakama client and session once auth is ready.
Typical pattern: subscribe to OnBootstrapComplete, rebuild IClient from the same host/port/key as the bootstrap config, restore ISession from the bootstrap auth token, then initialize Satori (and Hiro) together:
using IntelliVerseX.Bootstrap;
using IntelliVerseX.Hiro;
using IntelliVerseX.Satori;
using Nakama;
using UnityEngine;
public sealed class PostBootstrapNakamaWireup : MonoBehaviour
{
private void OnEnable()
{
IVXBootstrap.Instance.OnBootstrapComplete += OnBootstrapComplete;
}
private void OnDisable()
{
if (IVXBootstrap.Instance != null)
IVXBootstrap.Instance.OnBootstrapComplete -= OnBootstrapComplete;
}
private void OnBootstrapComplete(bool success)
{
if (!success) return;
#if INTELLIVERSEX_HAS_NAKAMA
var boot = IVXBootstrap.Instance;
var cfg = boot.Config;
var scheme = cfg.UseSSL ? "https" : "http";
var client = new Client(scheme, cfg.ServerHost, cfg.ServerPort, cfg.ServerKey);
var session = Session.Restore(boot.AuthToken);
IVXHiroCoordinator.Instance.InitializeSystems(client, session);
IVXSatoriClient.Instance.Initialize(client, session);
#endif
}
}
If you disable AutoDeviceAuth and call SetAuth before InitializeAsync, you are responsible for consistent IClient / ISession values when calling Initialize.
Path B — Manual (no bootstrap wiring)¶
Add IVXSatoriClient to a persistent scene object (or rely on bootstrap only to create the GameObject), then call Initialize after Nakama authentication—same order as in the integration prompt:
using IntelliVerseX.Satori;
using Nakama;
using UnityEngine;
public sealed class ManualSatoriBootstrap : MonoBehaviour
{
private async void Start()
{
#if INTELLIVERSEX_HAS_NAKAMA
var client = new Client("http", "your-server.com", 7350, "defaultkey");
var session = await client.AuthenticateDeviceAsync(SystemInfo.deviceUniqueIdentifier);
if (IVXSatoriClient.Instance == null)
{
var go = new GameObject("IVX_SatoriClient");
DontDestroyOnLoad(go);
go.AddComponent<IVXSatoriClient>();
}
IVXSatoriClient.Instance.Initialize(client, session);
#endif
}
}
Session refresh¶
After Nakama issues a new session (e.g. token refresh):
Event capture¶
Single event¶
await IVXSatoriClient.Instance.CaptureEventAsync("level_complete");
await IVXSatoriClient.Instance.CaptureEventAsync("purchase_started", new Dictionary<string, string>
{
["sku"] = "gem_pack_small",
["currency"] = "USD"
});
Batch¶
var events = new List<IVXSatoriEvent>
{
new IVXSatoriEvent("tutorial_step", new Dictionary<string, string> { ["step"] = "1" }),
new IVXSatoriEvent("tutorial_step", new Dictionary<string, string> { ["step"] = "2" })
};
IVXSatoriEventBatchResponse batch =
await IVXSatoriClient.Instance.CaptureEventsBatchAsync(events);
Debug.Log($"submitted={batch.submitted}, captured={batch.captured}");
IVXSatoriEvent exposes name, timestamp, and metadata. IVXSatoriEventBatchResponse exposes submitted and captured.
Identity properties¶
Fetch merged identity state (default, custom, and computed properties as returned by the server):
IVXSatoriIdentity id = await IVXSatoriClient.Instance.GetIdentityAsync();
foreach (var kv in id.defaultProperties)
Debug.Log($"default.{kv.Key}={kv.Value}");
foreach (var kv in id.customProperties)
Debug.Log($"custom.{kv.Key}={kv.Value}");
foreach (var kv in id.computedProperties)
Debug.Log($"computed.{kv.Key}={kv.Value}");
Update writable properties:
await IVXSatoriClient.Instance.UpdateIdentityAsync(
defaultProperties: new Dictionary<string, string> { ["platform"] = Application.platform.ToString() },
customProperties: new Dictionary<string, string> { ["favorite_mode"] = "ranked" });
Either dictionary may be null if you only need to update one side.
Audiences¶
List<string> audienceIds = await IVXSatoriClient.Instance.GetAudienceMembershipsAsync();
bool inVip = audienceIds.Contains("vip_segment");
Feature flags¶
Read one flag¶
IVXSatoriFlag flag = await IVXSatoriClient.Instance.GetFlagAsync("new_store_ui", defaultValue: "false");
Debug.Log($"{flag.name} value={flag.value} enabled={flag.enabled}");
IVXSatoriFlag exposes name, value, and enabled.
Read all flags (optional name filter)¶
List<IVXSatoriFlag> all = await IVXSatoriClient.Instance.GetAllFlagsAsync();
List<IVXSatoriFlag> subset = await IVXSatoriClient.Instance.GetAllFlagsAsync(
new List<string> { "ads_enabled", "seasonal_theme" });
Gating features in game code¶
Use enabled and/or parse value for thresholds. Always provide a safe default path when RPC fails (see Error handling).
IVXSatoriFlag beta = await IVXSatoriClient.Instance.GetFlagAsync("beta_hud", "0");
bool showBetaHud = beta.enabled && (beta.value == "1" || beta.value.Equals("true", StringComparison.OrdinalIgnoreCase));
if (showBetaHud)
EnableBetaHud();
else
EnableStableHud();
Experiments / A–B testing¶
All experiments and assignments¶
IVXSatoriExperimentsResponse resp = await IVXSatoriClient.Instance.GetExperimentsAsync();
foreach (IVXSatoriExperiment ex in resp.experiments)
{
Debug.Log($"{ex.name} ({ex.id}) status={ex.status} variant={ex.variant?.name}");
}
IVXSatoriExperiment exposes id, name, description, status, and variant (IVXSatoriExperimentVariant).
Single experiment variant¶
IVXSatoriExperimentVariant variant =
await IVXSatoriClient.Instance.GetExperimentVariantAsync("exp_onboarding_flow");
if (variant != null)
{
string layout = variant.config.TryGetValue("layout", out var v) ? v : "control";
ApplyOnboardingLayout(layout);
}
IVXSatoriExperimentVariant exposes id, name, and config (Dictionary<string, string>).
Live events¶
List events (optional filter by names)¶
IVXSatoriLiveEventsResponse live = await IVXSatoriClient.Instance.GetLiveEventsAsync();
foreach (IVXSatoriLiveEvent ev in live.events)
{
if (ev.IsActive)
ShowLiveEventCard(ev.name, ev.description);
else if (ev.IsUpcoming)
ScheduleReminder(ev.startAt);
}
// Filtered fetch
var onlyThese = new List<string> { "weekend_double_xp" };
IVXSatoriLiveEventsResponse filtered =
await IVXSatoriClient.Instance.GetLiveEventsAsync(onlyThese);
IVXSatoriLiveEvent includes id, name, description, startAt, endAt, status, joined, claimed, hasReward, config, and helpers IsActive, IsUpcoming, IsEnded.
Join and claim¶
bool joined = await IVXSatoriClient.Instance.JoinLiveEventAsync(eventId);
if (!joined)
ShowJoinFailedToast();
IVXSatoriLiveEventReward reward =
await IVXSatoriClient.Instance.ClaimLiveEventAsync(eventId, gameId: null);
foreach (var c in reward.currencies)
CreditCurrency(c.Key, c.Value);
foreach (var item in reward.items)
GrantItem(item.Key, item.Value);
Messages (inbox)¶
IVXSatoriMessagesResponse inbox = await IVXSatoriClient.Instance.GetMessagesAsync();
foreach (IVXSatoriMessage msg in inbox.messages)
{
if (msg.IsConsumed) continue;
RenderInboxRow(msg.title, msg.body, msg.imageUrl, msg.hasReward);
}
IVXSatoriMessageReadResponse read =
await IVXSatoriClient.Instance.ReadMessageAsync(messageId, gameId: null);
if (read?.reward != null)
ApplyReward(read.reward);
bool deleted = await IVXSatoriClient.Instance.DeleteMessageAsync(messageId);
IVXSatoriMessage exposes id, title, body, imageUrl, metadata, hasReward, createdAt, expiresAt, readAt, consumedAt, plus IsRead and IsConsumed. IVXSatoriMessageReadResponse exposes message and reward (IVXSatoriLiveEventReward).
Metrics¶
Query aggregated metric series with optional range and granularity (format is whatever your server expects and documents):
IVXSatoriMetricQueryResponse series = await IVXSatoriClient.Instance.QueryMetricAsync(
metricId: "daily_active_users",
startDate: "2026-03-01",
endDate: "2026-03-31",
granularity: "day");
foreach (IVXSatoriMetricDataPoint pt in series.dataPoints)
{
Debug.Log($"bucket={pt.bucket} count={pt.count} sum={pt.sum} min={pt.min} max={pt.max} avg={pt.avg}");
}
IVXSatoriMetricQueryResponse exposes metricId and dataPoints. Each IVXSatoriMetricDataPoint exposes bucket, count, sum, min, max, and avg.
Events and callbacks¶
| Event | Signature | When it fires |
|---|---|---|
OnInitialized | Action<bool> | Immediately after a successful Initialize; argument is true (failure before init throws or never reaches this path for success) |
Subscribe before or right before Initialize:
IVXSatoriClient.Instance.OnInitialized += ok =>
{
if (ok)
Debug.Log("Satori RPC client ready.");
};
IVXSatoriClient.Instance.Initialize(client, session);
IsInitialized is true after Initialize completes.
Error handling¶
-
Not initialized — Any public async method calls
EnsureReady()internally. IfInitializewas not called, it throwsInvalidOperationException: "IVXSatoriClient is not initialized. Call Initialize() first." -
Initializearguments —ArgumentNullExceptionifclientorsessionis null. -
RPC layer (
IVXSatoriRpcClient) —CallAsync/CallVoidAsyncthrowInvalidOperationExceptionif the session is null or expired. On transport or parse failures,CallAsyncreturnsdefault(nullfor reference types) and logs errors;CallVoidAsyncreturnsfalseon failure. Server-side errors in the JSON envelope are logged as warnings; failed calls return defaults. -
Defensive game code — Null-check reference results (
GetExperimentVariantAsync,ReadMessageAsync) and treat empty DTOs as “no data”:
var variant = await IVXSatoriClient.Instance.GetExperimentVariantAsync(id);
if (variant == null)
UseControlExperience();
var read = await IVXSatoriClient.Instance.ReadMessageAsync(mid);
if (read?.message == null)
RefreshInboxUi();
- Debug logging — Toggle RPC trace logs:
IVXSatoriRpcClient.EnableDebugLogs = falsefor release builds.
Cross-platform status¶
| Platform | Support |
|---|---|
| Unity | Y — Full IVXSatoriClient / Nakama RPC path |
| Unreal | S — Stub / separate native integration (see platform SDKs) |
| Godot | S |
| Defold | S |
| Cocos2d-x | S |
| JavaScript / TypeScript | S |
| C++ | S |
| Java (Android) | S |
| Flutter | S |
| Web3 | S |
“S” means not provided by this Unity module; use the corresponding IntelliVerseX SDK or custom bridge for that stack.
API summary (IVXSatoriClient)¶
| Area | Methods |
|---|---|
| Initialization | Initialize(IClient, ISession), RefreshSession(ISession) |
| Events | CaptureEventAsync(string, Dictionary<string,string>?), CaptureEventsBatchAsync(List<IVXSatoriEvent>) |
| Identity | GetIdentityAsync(), UpdateIdentityAsync(Dictionary<string,string>?, Dictionary<string,string>?) |
| Audiences | GetAudienceMembershipsAsync() |
| Flags | GetFlagAsync(string, string), GetAllFlagsAsync(List<string>?) |
| Experiments | GetExperimentsAsync(), GetExperimentVariantAsync(string) |
| Live events | GetLiveEventsAsync(List<string>?), JoinLiveEventAsync(string), ClaimLiveEventAsync(string, string?) |
| Messages | GetMessagesAsync(), ReadMessageAsync(string, string?), DeleteMessageAsync(string) |
| Metrics | QueryMetricAsync(string, string?, string?, string?) |
See also¶
- Satori integration guide — server RPCs, environment setup, and operational checklist
- Satori API reference — detailed request/response contracts
- Feature coverage matrix — platform and module coverage
- Backend module — Nakama client, session, and auth
- Hiro module — live-ops systems that may consume Satori-driven personalization