Things that go wrong when wiring up a Playloop SDK, ordered roughly by how often they bite. Each section names the symptom you see, the underlying cause, and the fix. Where the diagnosis is engine-specific (Unity Console vs. Godot Output panel vs. Unreal Output Log, for example), the per-engine note is called out inline.
Before reading further, hit Playloop > Send Test Event in your editor (Unity and Unreal: top-level Playloop menu; Godot: Project > Tools > Playloop). The action force-flushes a synthetic event so any HTTP error comes back in a dialog instead of getting swallowed by the auto-batch. If you see "Sent OK", your wiring is fine and the rest of this page is for the next thing that breaks. If you see an HTTP code, the dialog already includes the known-causes block; the sections below expand on each.
Full Send Test Event behavior is documented on the editor tooling page.
Symptom:Send Test Event returns 401. Status pill in the Settings window reads "● Invalid key". Production silently never delivers events.
Cause: The API key in your settings asset is either expired, revoked, or pasted with leading/trailing whitespace. Less common: you pasted a management key into a slot expecting an ingest key, or vice versa.
Fix: Rotate the key at playloop.gg/settings and paste the fresh value into the Settings window. The window's "Paste" button strips whitespace defensively, so a clipboard paste through that button is safer than typing the value in. Verify with the pill before moving on.
Symptom:Status pill reads "⚠ Scope mismatch." The key authenticates but the call gets rejected.
Cause: The dashboard issues two key types with different scopes. Ingest keys are scoped to telemetry only (writes to /api/telemetry). Management keysare scoped to read/write management endpoints (event config, sessions, builds). You pasted the wrong one for the surface you're using.
Fix:The SDK runtime needs an ingest key. The Event Config window also accepts an ingest key (it's a read on the same endpoint). If you're hand-rolling management API calls, you need a management key. Both live under playloop.gg/settings; they're labeled clearly.
Symptom:Status pill reads "⚠ Game not found." Send Test Event returns 404 with a body like { "error": "game_not_found" }.
Cause: The gameSlug (Unity: PlayloopOptions.GameSlug; Godot: PlayloopSettings.game_slug; Unreal: Project Settings, Plugins, Playloop, GameSlug) doesn't match any game on your account. Common reasons: you used the game's display name instead of its URL-friendly slug, or you typo'd the slug, or you haven't created the game in the dashboard yet.
Fix: Open playloop.gg/dashboard and confirm the slug. It's the lowercase, kebab-case identifier in the URL (e.g. necromancers-army), not the display name ("Necromancer's Army"). Update the settings asset to match, then Verify.
Symptom: Unity Console / Godot Output panel / Unreal Output Log emits a single warning at startup: [Playloop] heartbeatSec clamped from 15 to 30 (server minimum).
Cause:You set a heartbeat cadence below the SDK's 30-second floor. The 30s floor exists to keep per-session network cost bounded; values below the floor get silently clamped, but the SDK does emit the warning so you know it happened.
Fix: Use 30 or higher. The default is 60. If you have a real reason to need sub-30s tick rates, let us know; the floor is a policy choice, not a technical limit.
Symptom:Your game emits events all session, the editor pill says "Connected," but the dashboard is empty.
Step 1: environment mismatch.The most common cause. The dashboard's environment filter defaults to production, but your build is emitting under demo or play_test. Flip the env pill on playloop.gg/games/<slug>/dashboard to match what your build is sending.
Step 2: check the SDK logs. Both SDKs log every flush attempt at INFO level. Look for [Playloop] flushed N event(s) in Xms lines in the Unity Console, Godot Output panel, or Unreal Output Log (filter to LogPlayloop). If you see them, the wire is fine and the issue is dashboard filtering. If you don't see them, the auto-batch loop isn't running.
Step 3: confirm the client is alive. In Unity, check that PlayloopClient.Current is non-null. In Godot, check Playloop.current. In Unreal, check that UPlayloopClient::GetCurrent() returns a valid pointer. If all are null, your bootstrap path never ran. Look for an exception during initialization that swallowed the constructor call. For Unreal, also confirm bAutoStart is on in Project Settings or that you constructed the client yourself.
Symptom: A session ended with the player force-quitting (task killed, browser crash, abrupt OS shutdown). The session appears on the dashboard but the session_summary payload is missing or incomplete.
Cause: Auto-flush on quit is best-effort. Unity registers Application.quitting; Godot listens for NOTIFICATION_WM_CLOSE_REQUEST; Unreal flushes from UPlayloopGameInstanceSubsystem::Deinitialize. A force-quit bypasses all three hooks. The session is still partial-recoverable from heartbeats if you fired one or more before the crash; the last heartbeat's state snapshot becomes the digest's resolution block.
Fix: The behavior is by design. To minimize loss, call setState / set_state on every meaningful state change so the most recent heartbeat carries the freshest snapshot. Auto-flush guarantees are documented on the SDK contract page.
Symptom: The device loses connectivity mid-session. You expect events to queue locally and flush on reconnect.
What actually happens: The SDKs use an exponential-backoff retry policy with three attempts by default (configurable via retryAttempts / retry_attempts), starting at retryBaseMs (default 250) and capped at retryMaxMs (default 5000). After exhausting retries, the events stay in the in-memory buffer until either the next successful flush or the buffer hits its cap (default 200 entries), at which point the oldest entries get dropped.
Fix: For long-offline sessions, bump telemetryMaxBufferSize in the settings asset. The buffer is per-session in-memory; on app restart it's gone. There's no on-disk outbox today, but it's worth knowing the trade-off when choosing buffer sizes.
| Engine | Where to look | Filter by |
|---|---|---|
| Unity | Unity Console window | Filter by the [Playloop] prefix. All SDK logs use it. |
| Godot | Output panel at the bottom of the editor | Same [Playloop] prefix. Godot's push_warning + push_error also show in the Errors tab of the Debugger panel. |
| Unreal | Output Log (Window, Output Log) | Filter the category dropdown to LogPlayloop. All SDK log calls route through that category. Cooked-build logs land at <ProjectSavedDir>/Logs/<Project>.log with the same category tag. |
| Python | Standard logging | The SDK uses Python's logging module under the playloop logger. Set the level to DEBUG for the full picture. |
| TypeScript / Node | stdout / stderr | All SDK logs prefix with [Playloop]. In browser builds, check the DevTools Console. |
Plugin did not load after install. Unreal only picks up new plugins on the next module-rebuild. After dropping the plugin into <Project>/Plugins/PlayloopSDK/, regenerate project files (right-click the .uproject, choose Generate Visual Studio / Xcode project files), then open the project so Unreal prompts to rebuild. If you see "Plugin Playloop failed to load" on next launch, check the Output Log for the missing-module reason and rebuild.
Settings did not persist. Values edited under Project Settings, Plugins, Playloop write to Config/DefaultPlayloop.ini via UDeveloperSettings::SaveConfig(). If the change does not stick across editor restarts, confirm the file exists and is writable, and that the project is not opened in read-only mode (e.g. from a Perforce checkout). The ApiKey field intentionally never echoes back into the editor once saved; trust the "Send Test Event" verification rather than reading the field back.
Crash trap did not fire on a real crash. FCoreDelegates::OnHandleSystemError fires for OS-level crashes (segfaults, asserts, unhandled exceptions). It does NOT fire for managed Blueprint errors, soft scripting failures, or PIE-only soft asserts. For caught exceptions in your gameplay code, call Client->ReportCrash(Message, StackTrace) yourself. Both paths are gated by bEnableCrashHandler; if the flag is off the trap installs but nothing is attributed or drained.
Auto-batch / heartbeat silently disabled in headless / dedicated server. Timers need a UWorld. In commandlet or dedicated-server contexts where GEngine->GameViewport->GetWorld() returns null, the SDK logs a warning and you must call Flush() manually on a cadence of your choosing.
Two paths:
session_start (e.g. playloop-unity@0.5.0), and a copy-paste of the relevant log lines. We read every one.These docs are evolving. Playloop is in active development ahead of launch, so APIs and details may change as we polish.