> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qwacks.com/llms.txt
> Use this file to discover all available pages before exploring further.

# What a playtest collects

> Video, performance windows, exceptions and feedback forms from a Unity playtest build, and the C# calls for each.

Each of these runs only while the playtest turns it on in Protokite **and the player has allowed it**. None
of them needs code: the C# calls on this page are for a game that wants its own menus or buttons around
them, and they are safe in every build — with no playtest running they answer false, null or `TurnedOff`.
Only three work without one: a test video, the player's consent answer, and the Steam ID, which is kept for
the session to come. Setting the build up is on the [Unity package](/protokite/unity-package) page.

## What the player allowed

The build asks its player what this playtest may collect before it collects anything, and their answer
decides what the rest of this page does:

| Their answer | Video recording | Heavy analytics |
| - | - | - |
| Record my screen and collect play data | Runs | Runs |
| Record my screen only | Runs | Off |
| Collect play data only | Off | Runs |
| Collect nothing | Off | Off, and no session is started at all |

`ProtokitePlaytest.IsFeatureEnabled` already answers for both at once: a feature the playtest turns on and
the player left out reads as off, everywhere. Exception capturing follows play data there, but
[exceptions](#exceptions) themselves are the Flock SDK's, reported with its own setting whatever the player
answered.

```csharp theme={null}
if (ProtokitePlaytest.IsFeatureEnabled(ProtokitePlaytestFeatures.VideoRecording))
    ShowTheRecordingIndicator();
```

<Note>
  **The feedback form is not covered by that answer.** It is sent only when a player fills it in and presses
  Send, which is their own doing either way — and a form an earlier launch could not send is still sent
  later, whatever they answered since.
</Note>

## Video recording

The game's screen is recorded from the moment the playtest loads and the player has allowed it, before
anyone signs in: one recording per launch, written as it records into a WebM file a browser plays with
nothing installed. Time the game spends in the background is left out.

The settings are in **Protokite → Playtest → Settings**, under **Video recording (64-bit Windows)**:

| Setting | Default | What it does |
| - | - | - |
| Video Codec | VP8 | VP8 costs a slow PC least; VP9 makes smaller files for more processor time |
| Video Width, Video Height | 1280, 720 | The largest the video is. The screen's shape is kept, a smaller window is not enlarged, and each side is rounded down to a multiple of 16 |
| Video Frames Per Second | 15 | Frames recorded each second of play |
| Video Bitrate Kbps | 1500 | The video's bitrate |
| Encoder Threads | 1 | One costs the game least |
| Use Codec Default Speed, Encoder Speed | on, 12 | Off: the speed you set (VP8 -16 to 16, VP9 -9 to 9; higher is faster and looks worse) |
| Encoder Below Game Priority | on | The encoder gives way to the game when the processor is busy |
| Max Recording Minutes | 60 | A recording stops for good after this many minutes of play |
| Max Recording Size Mb | 1536 | A recording stops for good before its file passes this size |
| Recordings Disk Budget Mb | 4096 | The most every recording kept on the machine may take together |

The frame is copied, scaled and converted on the graphics card and read back without waiting for it;
encoding and writing each run on a thread of their own, and a frame that cannot keep up is dropped rather
than stalling the game. When a recording ends, the Console says what it holds and how many frames were
dropped, and why.

### When a recording is uploaded

A recording is uploaded to its session once its file is finished **and** the session has started,
whichever comes second. A file is finished when:

* it reaches **Max Recording Minutes** or **Max Recording Size Mb**;
* the player presses **Upload your recording** on the feedback form, or your game calls
  `ProtokitePlaytest.StopRecordingAndSendIt()`;
* your game stops it for good with `ProtokitePlaytest.StopVideoRecording()`.

It is sent straight from disk, never whole in memory, and counts as uploaded only when the storage accepts the
file itself. A failed upload is tried once more. An uploaded recording is deleted from the machine; one that
was not is kept with its session, and the Console says why.

<Note>
  **Quitting does not upload.** A whole recording cannot be sent while the game closes, so a recording still
  going at quit is finished and kept, and the next launch sends it. A later launch sends every recording an
  earlier one kept — a failed upload, a quit, a crash — oldest first, **even with Playtesting Enabled off**, so a
  release build never strands what a playtest build recorded. A recording no session ever started for is
  deleted, since it can never be sent.
</Note>

Offer the player a button of your own only while `ProtokitePlaytest.CanSendTheRecording` is true. It is false
when no session has started for the recording, and a button pressed then would stop the recording and send
nothing:

```csharp theme={null}
if (ProtokitePlaytest.CanSendTheRecording && GUILayout.Button("Send my recording"))
    ProtokitePlaytest.StopRecordingAndSendIt();
```

### Room on the player's disk

**Recordings Disk Budget Mb** is the most every recording kept on the machine may take together. Before a
recording starts, recordings earlier launches left are deleted until it fits: test videos first, then
recordings waiting to upload, the oldest first. One being uploaded, or still held by a copy of the game that is
running, is never deleted. A recording makes room only for what its length limit records at its bitrate,
with a quarter to spare (about 845 MB at the defaults), so a short recording never deletes one it would fit
beside. With less than 1 MB left, that launch records no video, and a warning names the setting to raise.

A recording cut off by a crash, a power cut or a killed game is finished by the next launch with every whole
frame it holds, then uploaded to its session like any other; one whose session never started is deleted.

## Heavy analytics

The playtest sends these through the Flock SDK's analytics, under the `playtest` category, and they read on
**Dashboards → Game Metrics**:

* `performance_window`, for every ten seconds of play: the median, 95th and 99th percentile frame time,
  `hitches` (frames that took **Hitch Frame Time Ms**, 60 by default, or longer), memory used and at its
  peak, and `map`, the active scene. A frame time is the real time from one frame to the next, so slow
  motion or a paused game is measured as the frames the player saw.
* `level_loaded`, for every scene the game loads in place of the one before, with the scene before it and
  `load_seconds`, how long the load held the game up.
* **Your own events**, on the same timeline and in the same category:

```csharp theme={null}
ProtokitePlaytest.RecordPlaytestEvent("boss_fight_started",
    new Dictionary<string, object> { { "boss", "hydra" } });
```

`RecordPlaytestEvent` answers true when the event was queued, can be called from any thread, and sends only
while heavy analytics runs, which starts once the playtest has loaded; an event recorded earlier in the launch
answers false and is not kept. The two names the playtest sends itself are refused.

Heavy analytics needs the Flock SDK's **Analytics Enabled**: with it off, nothing is measured and the Console
says so once. The events are Flock events like any other, so they wait for a signed-in player and follow the
Flock SDK's own analytics consent. See [Analytics & events](/guides/analytics-and-events) for how the category
and the dashboards fit together.

<Tip>
  Played offline, six windows a minute fill the Flock SDK's offline queue (**Analytics Max Cached Events**, 1000
  by default) in under three hours. Raise it for playtests your testers play offline.
</Tip>

## Exceptions

Exceptions stay the Flock SDK's. It captures the game's exceptions from every thread, and faulted tasks nobody
awaited, with **Analytics Capture Exceptions** in **Flock → Settings → Advanced Settings** (on by default), and
reports them on **Diagnostics → Errors**. A playtest's exception switch never turns that on or off.

The same exception thrown again and again reaches the dashboard as one report, then one repeat report counting
the others once the repeat window (**Analytics Exception Repeat Window**, 60 seconds) closes. `Debug.LogError`
lines are not exceptions and are not reported.

## The feedback form

When the playtest publishes a feedback form in Protokite, the player opens it over the game with **F9** and
fills it in. It is built from what you published — every question, label, help text and option — so editing
the form in Protokite needs no new build.

<Frame caption="A published form, drawn from the playtest: every question, and the button that sends the recording.">
  <img src="https://mintcdn.com/qwacks/ByDUWjWU5EEoXzc3/images/protokite/unity/feedback-form.png?fit=max&auto=format&n=ByDUWjWU5EEoXzc3&q=85&s=049f714321c2de7029604f58c0b800b2" alt="A playtest feedback form over a Unity game, with a rating, a choice, text answers and an Upload your recording button" width="1440" height="810" data-path="images/protokite/unity/feedback-form.png" />
</Frame>

| Setting | Default | What it does |
| - | - | - |
| Feedback Form Key | F9 | Opens the form and closes it again. Press **Detect Key** and then the key you want, or pick it from the list. **None** leaves opening it to your game |
| Pause The Game While The Form Is Open | off | Sets the time scale to 0 while the form is open, and puts it back when it closes |

* **Send checks the answers the way Protokite does**, and shows every problem against its question before
  anything is sent: a required question left empty, a rating out of range, an option the question does not
  offer.
* **A sent form is kept on the device first and sent from there**, so neither a closed game nor a lost network
  loses it: one that could not go now is sent when the network comes back, two minutes later, or by a later
  launch. Sending again in the same session replaces the earlier answers.
* Closing it without sending keeps what was typed for the rest of the launch.
* **Upload your recording** appears while this launch records the screen and its session has started: it stops
  the recording and sends it now. Opening the form never stops the recording on its own.

<Warning>
  **Your game still reads its own keys while the form is open.** A game that moves on WASD, or opens a menu on
  Escape, should ignore its input while `ProtokitePlaytest.IsFeedbackFormOpen` is true, or turn on **Pause The
  Game While The Form Is Open**.
</Warning>

Open it from your own pause menu, and leave that entry out when the playtest published no form:

```csharp theme={null}
if (ProtokitePlaytest.CanOpenFeedbackForm && GUILayout.Button("Give feedback"))
    ProtokitePlaytest.OpenFeedbackForm();
```

### A form of your own

`ProtokitePlaytest.FeedbackForm` hands your UI the questions: each field's `Id`, `Type`, `Label`, `HelpText`,
`Options` and whether it is `Required`. Answer them, check them and send them through the same checking and
keeping as the built-in form:

```csharp theme={null}
ProtokitePlaytestFormAnswers answers = new ProtokitePlaytestFormAnswers();
answers.SetRating("rating", 4);
answers.SetChosenOption("category", "Bug");
answers.SetText("title", "Fell through the floor");
answers.SetChecked("contact", false);
foreach (ProtokitePlaytestFormProblem problem in answers.FindProblems(ProtokitePlaytest.FeedbackForm))
    Debug.Log(problem.FieldId + ": " + problem.Message);
ProtokitePlaytest.SendFeedbackForm(answers);   // false, with a warning, when there are problems
```

Compare a field's `Type` with the `ProtokitePlaytestFormFieldTypes` constants rather than typing it, and treat a
type you do not know as text, which is how the server reads it.

<Warning>
  Two rules are easy to miss. **Set every checkbox**, ticked or not: the server counts an unticked box as an
  answer, and a needed checkbox that was never set is missing. And **a chosen option must match letter for
  letter** — the server compares options exactly, so `bug` is not `Bug`.
</Warning>

## Calls your game can make

All on `ProtokitePlaytest`, in the `Protokite.Playtest` namespace. Call them from the main thread;
`RecordPlaytestEvent` also works from any other.

| Call | Answers or does |
| - | - |
| `Status`, `Describe(status)`, `Config` | Whether the playtest can run, why not, and what it turns on |
| `IsFeatureEnabled(name)`, with `ProtokitePlaytestFeatures.VideoRecording` / `HeavyAnalytics` / `ExceptionCapturing` | Whether a feature runs, the player's answer included |
| `PlaytestConsent`, `PlayersConsentAnswer`, `Describe(answer)` | What the player let this playtest collect |
| `SetPlaytestConsent(choice)`, `AskForPlaytestConsent()`, `IsConsentQuestionOpen` | Answering from your own screens, and asking again |
| `PlaytestSessionId` | The launch's Protokite session, once started |
| `SetSteamId(id, name)` | Names the player by their Steam ID instead of the device ID, before the session starts |
| `RecordPlaytestEvent(name, properties)` | Records one of your own playtest events |
| `IsRecordingVideo`, `StopVideoRecording()` | The launch's recording |
| `CanSendTheRecording`, `StopRecordingAndSendIt()` | Sending the recording now |
| `CanOpenFeedbackForm`, `OpenFeedbackForm()`, `CloseFeedbackForm()`, `IsFeedbackFormOpen` | The built-in form |
| `FeedbackForm`, `SendFeedbackForm(answers)` | A form of your own |
| `RecordTestVideo(seconds, out whyNot)`, `TestVideoState`, `FinishedTestVideoPath`, `TestVideoProblem` | A test video, never uploaded |

The package's sample, `Samples/PlaytestSample/ProtokitePlaytestSample.cs`, puts every one of these on one
screen: add it to a GameObject and press **Play**.

## Trying it in the editor

The setup window, **Protokite → Playtest → Setup Checks And Test Video**, has everything for testing in its
lower half. In **Play Mode** it looks like this:

<Frame caption="The setup window in Play Mode: the checks, this machine's answer, a test video and the live self-test.">
  <img src="https://mintcdn.com/qwacks/ByDUWjWU5EEoXzc3/images/protokite/unity/setup-window-play-mode.png?fit=max&auto=format&n=ByDUWjWU5EEoXzc3&q=85&s=0c5f2d70dc2f1c2030953097cc9e1688" alt="The Protokite Playtest setup window during Play Mode, with the While testing, Test video and Live self-test sections" width="540" height="354" data-path="images/protokite/unity/setup-window-play-mode.png" />
</Frame>

* **Forget This Machine's Answer** makes the next Play ask the consent question again.
* **Open Feedback Form** opens the form the way its key does.
* **Record Test Video** records the Game view for the seconds you set, with the video settings and **no
  playtest needed**: playtesting off and nobody signed in is fine. A test video is saved under
  `ProtokitePlaytest/Recordings/TestVideos/` in the game's persistent data folder, never uploaded, and the window
  shows where it went. It is refused while the playtest's own recording runs, and gives way to it. Like all
  video, it records on 64-bit Windows only.
* **Run Live Self-Test** checks the whole playtest against your real Protokite (below).

A game can record a test video itself, from a debug menu's "record a clip" button for instance:

```csharp theme={null}
if (!ProtokitePlaytest.RecordTestVideo(30.0, out string whyNot))
    Debug.Log("No test video: " + whyNot);
```

## Checking a playtest build

The live self-test checks everything a playtest build does against your real Protokite in one go, and logs a
line per step and a count at the end. Run it in Play Mode with **Run Live Self-Test**, or from code in the
editor or a development build (a release build refuses it):

```csharp theme={null}
ProtokitePlaytestSelfTestReport report = await ProtokitePlaytestSelfTest.RunAsync();
Debug.Log($"{report.Passed} passed, {report.Failed} failed, {report.Skipped} skipped");
```

Each check sits beside a request Protokite must refuse: a wrong API key, a missing one, a version no playtest is
linked to, a session start that names no player, a form missing a needed answer or choosing an option that is
not on the list, and a form, an upload link and an end for a session that does not exist. A check that only
ever succeeds cannot tell a working build from a broken one, so a refusal passes only with its own HTTP status
and, for a form, the question it names.

* **Sign a player in first, and answer the consent question.** It signs nobody in; steps that need a session or
  an answer are skipped, saying why.
* **Run it in a Play or launch of its own.** It stops this launch's recording to upload it, sends a filled-in
  form that replaces one the player sent this launch, and raises two exceptions on purpose — so with **Error
  Pause** on in the Console, Play pauses there until you resume it.
* **It leaves a trace on your dashboards:** one filled-in form on the session, one `playtest_self_test` event,
  one exception raised twice so its repeat is counted, and the launch's recording. Each carries the run's id
  (`report.RunId`), so you can find them in Flock and Protokite.
* **A step is skipped, saying why,** when the playtest does not turn its feature on, when the build has no video
  encoder (every platform but 64-bit Windows), and for a closed playtest unless you name one: pass a closed
  playtest's Game Version ID to `RunAsync`, or type it into **Closed Playtest Version ID** in the window.

## What stays on the player's machine

Under `ProtokitePlaytest/` in the game's persistent data folder (`Application.persistentDataPath`):

| Path | Holds | Until |
| - | - | - |
| `Recordings/Playtest/` | Playtest recordings, each in a folder with the session it belongs to | Uploaded, or deleted for disk room, oldest last |
| `Recordings/TestVideos/` | Test videos | Deleted for disk room, oldest first |
| `FeedbackForms/` | Answers that could not be sent yet, with no API key in them | Sent, or refused by the server |
| `device_id.txt` | This install's device ID, when no Steam ID is set | Kept, so one install stays one player |
| `playtest_consent.json` | What the player let this playtest collect | Kept, so they are asked once and not every launch |

## Platforms

Everything on this page runs wherever the Flock SDK runs, except video.

**Video is recorded on 64-bit Windows only**, in the editor and in players, Mono and IL2CPP alike. It runs on
Direct3D 11 and 12, Vulkan and OpenGL, in the Built-in Render Pipeline and URP, in Linear and Gamma colour, and
needs a graphics card that runs compute shaders.

**"No video on this platform"** means exactly that and nothing more: a build for any other platform — macOS,
Linux, mobile, WebGL, or 32-bit or ARM64 Windows — leaves the video encoder out, says once in the
Console that it records no video, and runs everything else: the session, the consent question, heavy analytics,
exceptions and the feedback form. The setup window tells you in advance: "Players built for … record no video". A
game that draws nothing (a server build, or one started with `-nographics` or `-batchmode`) records nothing
either, and cannot show the consent question: answer it from code with `SetPlaytestConsent`, or turn asking
off.

**On WebGL** the playtest's files — the consent answer, the device ID and feedback forms waiting to be sent — are
copied to the browser's storage after every change, so they are there on the player's next visit wherever the
browser keeps the site's data. A private window forgets them when it closes.
