Save game state for playtest analytics (localStorage, Unity, Godot)
Betacade reads how far people got from your game's own saved state, with no Betacade code in the game. That only works if the game keeps its state in the browser's localStorage, as JSON under readable keys. Without it you still get sessions, play time, errors and feedback, just not progress.
Keep state in localStorage#
The rules:
- Keep the game's saved state in localStorage, as JSON under readable keys, for example
saveholding{"level": 3, "deaths": 12}. - Save whenever something happens, not only at checkpoints: a level starts or ends, the player scores, dies, gains or spends something, picks up an item, changes a setting. Every save is a data point, so more saves mean finer progress, metrics and save history; a game that saves only per level shows nothing about the tester who quit halfway through one.
- While the player's input is changing the state, saving up to once a second is fine; never every frame. Betacade batches the writes: it sends each key's latest value after 2 seconds without a write, at most every 30 seconds, and at once when the page is hidden or closed.
- Write with
localStorage.setItem,removeItemandclear, notlocalStorage.foo = ..., so each change is seen as it happens. - Load it at startup, and start cleanly from empty storage: players can choose New game, which clears the game's storage and reloads it.
- Never put login tokens or secrets in these keys. Anything under a key starting with
secret:never leaves the player's browser.
A minimal version:
const KEY = "save";
const fresh = () => ({ level: 1, deaths: 0 });
let state;
try {
state = { ...fresh(), ...JSON.parse(localStorage.getItem(KEY) ?? "{}") };
} catch {
state = fresh();
}
function save() {
localStorage.setItem(KEY, JSON.stringify(state));
}
// At a checkpoint:
state.level += 1;
save();
Keep each key under 256 KB#
A value over 256 KB isn't kept, and nothing fails in the game, so it's easy to miss until a tester loses a save. One key holding the whole game grows as the game does: split state by what it is (settings, progress, world) from the start, and write only the keys that changed. A key past 128 KB warns on the game's console, and the studio's dashboard names it.
If your game saves somewhere else#
If the game keeps state in memory, cookies or its own server, mirror it into localStorage at checkpoints. This wrapper has no dependency on Betacade, so the game runs the same anywhere else:
// save.js
const KEY = "save";
export function saveGame(state) {
// Keep your existing save path too, if you have one.
localStorage.setItem(KEY, JSON.stringify(state));
}
export function loadGame(defaults = {}) {
try {
return { ...defaults, ...JSON.parse(localStorage.getItem(KEY) ?? "{}") };
} catch {
return { ...defaults };
}
}
Unity and Godot#
Unity (PlayerPrefs, Application.persistentDataPath) and Godot (user://) save to IndexedDB in web builds, which Betacade doesn't read yet. Also write the save to localStorage whenever it changes.
Unity: add a .jslib in Assets/Plugins/WebGL and call it where the game saves.
// Assets/Plugins/WebGL/BetacadeSave.jslib
mergeInto(LibraryManager.library, {
BetacadeSave: function (key, json) {
localStorage.setItem(UTF8ToString(key), UTF8ToString(json));
},
});
using System.Runtime.InteropServices;
public static class BetacadeSave {
#if UNITY_WEBGL && !UNITY_EDITOR
[DllImport("__Internal")] static extern void BetacadeSave(string key, string json);
public static void Save(object state) => BetacadeSave("save", UnityEngine.JsonUtility.ToJson(state));
#else
public static void Save(object state) {}
#endif
}
// At a checkpoint, after your own save: BetacadeSave.Save(progress);
Godot 4: at a checkpoint, after your own save.
if OS.has_feature("web"):
JavaScriptBridge.eval("localStorage.setItem('save', %s)" % JSON.stringify(JSON.stringify(progress)))
Milestones and logs (optional)#
Errors, unhandled promise rejections and the last console lines are captured for you. Two optional extras, both safe on any other host:
window.wt?.track?.("level.complete", { level: 3 }); // a milestone
console.info("boss phase 2"); // a breadcrumb on the tester's timeline
Next: once your numbers are saved, pin them as metrics.