Wardx Unity SDK
Use this skill for Unity-specific integration and runtime changes. Read references/package.md for enum usage, package internals, and verification. Use wardx-csharp for non-Unity applications and wardx-server for catalog, experiment definitions, and MCP operations.
Integrate
Install com.wardx.sdk from the intended Git revision or disk. Create the client
on the Unity main thread, or use WardxBehaviour. Its empty AppVersion uses
Application.version; supply all other required fields, including PrivacySalt.
Unity runtime code is selected by UNITY_5_3_OR_NEWER.
When enabled, required WardxOptions fields: Endpoint, ProjectKey, Project, Role,
AppVersion, Environment, and PrivacySalt. Use the application's configuration;
do not invent deployment values or derive the privacy salt from the key.
Project must match the credential's server mapping. Role is an open name other
than *; the server authorizes it against the credential's allowed roles.
Keep PrivacySalt stable across clients in the same project. Operational defaults
live in SdkDefaults, aligned with packages/core/defaults.json.
using Wardx;
public sealed class MatchTelemetry
{
readonly ICounter completed;
readonly IHistogram duration;
public MatchTelemetry(WardxClient wardx, string mode)
{
completed = wardx.Counter("match.completed", Dims.Of("mode", mode));
duration = wardx.Histogram("match.duration_ms", Dims.Of("mode", mode),
new double[] { 30000, 60000, 180000, 600000, 1800000 });
}
public void OnCompleted(double durationMs)
{
completed.Inc();
duration.Observe(durationMs);
}
}
Create this recorder once after creating the client and keep it as an application
field (one per supported mode). In Unity, initialize it from the existing
bootstrap or Awake after the persistent client is available, not in Update
or on each match callback. The recorder does not own client shutdown.
Measurements are synchronous memory updates, not Tasks or network calls. Bootstrap sync starts immediately; later syncs use the configured interval and jitter. Delivery is at-most-once: failed batches are discarded, without disk queues or retries of the same frames. Use a different mechanism for lossless data.
Reuse metric handles
Create metric handles once per client and stable name/dimension combination, then reuse them in handlers, callbacks, and loops. This avoids repeated dimension validation, series-key construction, and registry lookup. Normal aggregation windows and flushes reset values, not handles. Rebind when replacing the client; do not mutate a dimension dictionary to retarget an existing handle.
For varying dimensions, bind one recorder per application-owned, bounded value set (mode, region, source). Never build an unbounded handle cache keyed by user IDs or arbitrary input. Keep histogram buckets fixed. Timer tokens measure one operation: create a fresh token for each operation, not one token for the client. For a hot duration path, reuse a histogram and observe an application-measured elapsed duration instead. Events, logs, retention, and experiment goals remain per-occurrence calls.
WardxClient.Create(new WardxOptions { Enabled = false }) returns an inert
client with usable handles, no engine/transport/scheduler, and immediate flush
and shutdown. It needs no connection options; config reads return the caller's
fallback. This mode is fixed at creation.
Choose a signal
| Question | API |
|---|---|
| Count or amount | Counter(name, dims).Inc() / .Add(n) |
| Latest value | Gauge(name, dims).Set(value) |
| Distribution | Histogram(name, dims, buckets).Observe(value, attrs) |
| Approximate unique count | Distinct(name, dims).Add(identifier) |
| Elapsed time | var timer = Timer(name, dims); then timer.Stop(endDims) |
| Discrete fact | Event(name, attrs) |
| Diagnostic detail | Log.Error(message, attrs) or another log level |
| Explicit user activity for retention | RetentionActivity(userId) |
Counters are window deltas; gauges are absent in windows without a Set.
Reuse handles in loops. Prefer counters for high-volume totals; add events/logs
when bounded detail is useful, not automatically for every count.
Use low-cardinality string, number, boolean, or enum dimension values via
Dims.Of(...). Never use user IDs, emails, or unique transaction IDs as dimensions.
Count/length/series limits yield no-op series and increment cardinality drops;
invalid value types or enum values throw. Histogram bounds are fixed per series;
attrs retain only the window-max exemplar. Redact diagnostic attrs as needed.
Prefer ICounter, IGauge, IHistogram, and IDistinct fields initialized
in the owning component or service setup. Wire-name strings belong there;
measurement sites call the fields directly. Do not introduce enums solely to
avoid repeated metric strings. Enums remain optional for shared application
names; preserve existing enum integrations and wire names. Read the enum
reference before using mappings or overloads in a version-pinned consumer.
Distinct counts send salted HLL sketches, not identifiers. Volume funnels compare
separately named steps, not unique-user journeys; event attrs do not split
historical counts. SDK sessionId is envelope identity, not a session clock.
For duration use an application-owned monotonic clock. If heartbeats add elapsed
deltas to a time counter, add only the remaining delta at the end; observe the
full duration once in a histogram. Choose an experiment goal from the product
question rather than always using session duration.
Remote Config and experiments
Config.Get(key, fallback, subjectId) reads the last local snapshot without
network I/O. Missing keys use the caller's fallback. Remote Config contains no
secrets; signal descriptions belong in the server catalog.
Role controls which keys and experiments this instance receives. Server-side conditions resolve visible base values; an applicable A/B variant overrides that base. Knobs are the adjustable config keys.
Use optional WardxOptions.Attributes or wardx.SetAttributes(map) with a flat
IReadOnlyDictionary<string, object> of strings, finite numbers, or booleans.
The client copies the map; the setter replaces it and an empty map clears it.
These attributes belong to the instance, not the experiment subject. The wire
platform is unity, not Android or iOS: provide OS/build/channel as attributes
when the app needs those conditions, without creating a role per platform.
WardxBehaviour has no attribute inspector fields; use manual options when the
bootstrap must include them. Read the package reference for sync semantics.
On a single-user instance, Identify(userId) sets the default experiment subject;
Identify(null) clears it. For multiple users, pass subjectId per call instead
of changing shared identity. Neither ordinary metrics nor base Remote Config
requires identification. Do not assume every Unity process has only one user.
var delayMs = wardx.Config.Get("message.delayMs", 1000, userId);
// At the actual matching outcome, after exposure:
wardx.Experiment.Goal("message.sent", userId, 1);
Assignment is local and deterministic for the subject and experiment plan.
A matching config read can emit exposure. Without a subject, the read returns
base Remote Config without exposure. Experiment.Goal throws without a subject
and emits only for a matching goalMetric exposed in this instance. The SDK
hashes the subject; do not add the raw ID to dimensions or attrs. Do not define
experiments or persist chosen variants in application code. After a shipped
experiment's disabled state reaches the snapshot, no new exposure is expected.
Shipping updates the stored base and preserves conditional rules, which can
take precedence again after disablement.
Retention
Call RetentionActivity(userId) on the activity that defines a return. It requires
an explicit nonblank stable ID; Identify does not supply it. Use a consistent
activity definition and stable project salt across devices. The server stores
UTC cohorts and received-user returns on D1/D7/D30. Lost batches can bias counts;
delayed activity can revise cohorts. Query semantics belong to the server skill.
Lifecycle and diagnosis
FlushAsync() syncs even with no pending frames, without stopping scheduling.
ShutdownAsync() stops scheduling, waits for the current sync, attempts a final
flush, and closes transport. Await it while the Unity player loop is still alive
when the application requires a final send attempt.
Stop() stops scheduling and cancels/closes transport without a final flush.
Unity quit/destroy callbacks use this nonblocking path; it does not guarantee
last-frame delivery. Never block the main thread with .Wait() or .Result
on HTTP. Keep one intended persistent client across scenes; do not accidentally
create a second client through both WardxBehaviour and manual startup.
Use ConsoleTracer for local diagnosis, or implement ITracer / subclass
TracerBase; hooks are Measure, Event, Log, Frame, and Sync. Tracing is
not sent to the server; do not expose secrets through it.
For missing data, inspect failed/dropped frame counters, endpoint, credentials,
role, and capacity. For config fallbacks, check snapshot delivery and visibility.
Preserve synchronous measurement and at-most-once delivery when modifying the SDK.