Analytics
The Editor server reports how the pipeline is used through Unity's editor analytics
(EditorAnalytics.SendAnalytic). The data answers questions the package has no other way to
answer: whether sessions are interactive or automated, which commands clients actually reach for,
how often they fail, how long they take, and how much traffic is raw script eval rather than a
concrete command.
Three things bound what is collected:
- Editor only. A server running in a development Player reports nothing —
EditorAnalyticsdoes not exist there. - The editor's own opt-out applies. With analytics disabled in Unity preferences,
SendAnalyticdoes nothing and no event leaves the machine. There is no separate pipeline switch to turn off. - Nothing about a command's content is collected. Not its parameters, not the code passed to
script eval, not paths, not results. - Nothing a project declared about its own commands is collected. Only commands shipped in the
package report their name and tags; anything a project declared itself reports
<customUserCommand>and no tags — see What is and is not collected.
Events
Pipeline_SessionStarted
Sent once when a client executes its first command, which is the moment the pipeline actually started handling work — not when the Editor launched or the server bound its port.
| Field | Meaning |
|---|---|
batchmode |
The Editor was launched with -batchmode. |
automated |
The Editor was launched with -automated. |
browserAllowed |
The server accepts sandboxed browser clients (the limited-CORS path). Off unless enabled in the Pipeline settings. |
dynamicPortAssignation |
The port was auto-assigned from the server's range rather than pinned in the editor settings. |
hotReloadWatcherEnabled |
The .cs change watcher is running. |
A session spans a Unity process. The latch lives in SessionState, which survives domain
reloads, so a script recompile does not open a second session.
Pipeline_CommandExecuted
Sent once per command a client executes.
| Field | Meaning |
|---|---|
commandName |
The command's full name (group name, e.g. asset delete) — but only for commands that ship with the package. A project-declared command reports the literal <customUserCommand> instead. |
commandTags |
The command's tags, in declaration order — but only for commands that ship with the package. Empty for an untagged command, and always empty for a project-declared one. |
commandSuccess |
Whether the command succeeded. False both when it threw and when it returned a result carrying its own failure — script eval, script.methodreload reload, script run and test run all report failure the second way. |
commandDuration |
Milliseconds spent inside the command, excluding the wait for the one-command-at-a-time gate. |
isUserDefinedCommand |
The command was declared by the project rather than shipped in the package. |
isEval |
The command is a code evaluation: it carries the scripts/eval tag, as script eval, script eval_file and script run do. |
isEvalWithExistingCommandAvailable |
Whether the evaluated snippet touches an API that a shipped command already covers — the "you did not need eval for this" signal. Same judgement diagnostics report_evals makes, applied to one invocation: see How the eval-coverage flag is decided. |
commandId |
The calling client's invocation id, which matches the pipeline_command_id on the unity CLI's own command executed event. One CLI invocation can send several commands, so several events can share an id. 0 when the caller sent none. |
clientSource |
Who drove the client: human, agent, ci or unknown. Empty when the caller sent none. |
clientName |
Which AI client family drove the client (claude, cursor, …), or none. Empty when the caller sent none. |
See Caller context for how a client supplies these.
What counts as one execution:
- One event per
/api/execcall, including ajob: truesubmission — reported when the job actually finishes, not when its job handle is returned. - A
job batchreports one event, forjob batchitself. Its sub-operations are not reported individually. - A request rejected before anything ran (oversized body, malformed JSON, unknown command, settling host, full job queue) reports nothing: no command executed.
- The internal executions behind
/api/editor_statusand/api/test-statusreport nothing. Clients poll those endpoints, and the polling would swamp everything else.
Pipeline_SessionStopped
Sent when the Editor quits, and only if Pipeline_SessionStarted was sent — an Editor that never
served a command closes no session.
| Field | Meaning |
|---|---|
sessionDuration |
Milliseconds between the session starting and the Editor quitting. |
How the eval-coverage flag is decided
isEvalWithExistingCommandAvailable reuses the eval instrumentation from AUTHAPI-29 rather than
inventing a second opinion. Per invocation:
EvalUsageFingerprinter.Analyzeturns the snippet into API fingerprints — dotted member-access paths, with no literals or raw source in them.- Each fingerprint is looked up in
EvalCoverageMap.Default, a small curated hint map from API to the command that would cover it. - The flag is true only if a mapped command is actually in the live catalog. A mapping to a
command that does not exist yet (
AssetDatabase.Refresh→asset.refresh) is a gap fordiagnostics report_evalsto surface, not an avoidable eval.
This is deliberately the same rule as diagnostics report_evals' covered list, so the two can never disagree.
Two consequences worth knowing when reading the data:
- It is a floor, not a measure. The map has a handful of entries and matching is on the whole
fingerprint, so
PlayerSettings.GetScriptingBackend(...)matches whileUnityEditor.PlayerSettings.GetScriptingBackend(...)andPlayerSettings.GetScriptingBackend(...).ToString()do not — they fingerprint as different paths. Afalsemeans "no mapped-and-present command matched", never "no command could cover this". script runalways reportsfalse. It runs a project file through a named entry point rather than a snippet, and the fingerprinter parses script-kind bodies, so it is not fingerprinted.script eval(inline body) andscript eval_file(body read back from its file) are judged.
What is and is not collected
A command's name (its group and name) and tags are text the project's own authors wrote, so either can name an
unannounced feature or an internal system. Tags are constrained in shape
(^[a-z0-9_]+(/[a-z0-9_]+)*$) and shipped commands must use the documented taxonomy, but nothing
holds a project to it, so a tag carries a project-chosen word exactly as a name does.
So both are withheld. commandName and commandTags describe only commands that ship in the
package (Unity.Pipeline, Unity.Pipeline.Editor); every other [CliGroupCommand] reports the literal
<customUserCommand> and an empty tag array. The remaining fields — success, duration, isEval —
are facts about the execution, not about what the project called it, so they are reported for every
command.
What this costs: project-declared traffic is countable but not breakable down by area. That is the
deliberate trade — isUserDefinedCommand answers how much of the surface is project-declared, which
is the question worth asking, and nothing identifying is needed to answer it.
Command arguments are never collected. Neither is anything a command reads or writes.
Adding post-command work
Analytics is one consumer of a single seam rather than a special case. When a command that ran
ends, synchronous or a detached job, the executor's OnCommandEnded receives a
CommandExecutionInfo describing the execution. It runs on whatever thread the command ended on, so
EditorCommandExecutor hands it to the server dispatcher with Dispatcher.Post, which runs it in
ReportOnMainThread on the next main-thread pump. The raw transaction goes to the HTTP transport
instead: BasePipelineServer.OnCommandDone receives it on the HTTP thread, and
EditorPipelineServer appends it to the transaction log there (thread-safe file I/O by design).
New post-command work goes in ReportOnMainThread when it needs Unity APIs, in OnCommandEnded
when it doesn't, and in OnCommandDone only when it is about the HTTP transaction. Post is the
general fire-and-forget counterpart to Dispatcher.Invoke: it returns immediately instead of
parking the request thread until the next pump, and swallows-and-logs failures because there is no
caller left to throw to. Work still queued when the server stops is dropped, so a report can be
lost in the frame the Editor quits or reloads its domain.
See also
- Creating commands — where a command's name and tags come from.
- Connectivity — the port and CORS settings the session event reports.
- Method reload — the watcher whose state the session event reports.