analytics.v2)POST/collectapplication/jsonAnalyticsEvent[]2xxeventIdtype AnalyticsEvent = {
schemaVersion: "analytics.v2";
eventId: string;
eventName: "feature_used" | "error_occurred";
occurredAt: string; // ISO timestamp
installationId: string; // installation-level identity (stable across shared server-token)
instanceId: string; // instance-level identity (distinguishes multi-instance same machine)
deploymentMode:
| "source"
| "docker"
| "curl_installer"
| "desktop_installer"
| "npm_binary"
| "unknown";
sessionId?: string; // 24-char hex hash of internal sessionKey (not the raw key)
commitHash: string; // app/runtime commit hash
appVersion: string;
platform: string; // process.platform
properties: Record<string, unknown>;
};
analytics.v2 adds explicit source attribution: ownerModule, executionKind, and phase.module = "router" now describes router/tokenSaver/judge/fallback health only; real provider requests are reported through module = "session" unless they belong to memory/judge direct LLM calls.module = "session" may include reused agent-loop executions for Always-On, subagents, compaction, or tool-secondary model calls. Use ownerModule and executionKind to isolate ordinary user sessions.projectCommitHash.projectPath (no filesystem paths in outbound events).sessionId is now a hashed anonymous id, not the raw sessionKey (which may embed paths).error_occurred no longer includes message or stack; only classification fields below.app_started, session_active (DAU uses any feature_used / error_occurred).feature_used Two-Layer ModelFor eventName = "feature_used", properties follows:
type FeatureUsedProperties = {
/** Business surface being measured. */
module: "router" | "always_on" | "memory" | "cron_job" | "session";
/**
* Business owner of an execution event. For ordinary user chat this is
* "session"; for Always-On agent loops it is "always_on" even though
* `module` remains "session".
*/
ownerModule?: "router" | "always_on" | "memory" | "cron_job" | "session";
executionKind?:
| "user_session"
| "subagent"
| "always_on"
| "router_judge"
| "memory"
| "cron_job"
| "compaction"
| "tool_secondary";
/** Business phase, e.g. router judge/decision/fallback or Always-On discovery/workspace/execution/report/apply. */
phase?: string;
loopStage:
| "module_event"
| "loop_start"
| "model_request"
| "model_response"
| "tool_prepare"
| "tool_call"
| "permission_check"
| "loop_end";
outcome?: "success" | "failed" | "aborted" | "timeout" | "denied";
errorCategory?:
| "model_request_error"
| "permission_error"
| "tool_param_error"
| "tool_runtime_error"
| "tool_result_parse_error"
| "loop_error"
| "runtime_error";
provider?: string;
model?: string;
/** HTTP(S) API base from provider config (no userinfo, query, or fragment). */
providerBaseUrl?: string;
// plus other module-specific metadata (path-like keys stripped client-side)
[key: string]: unknown;
};
Session model_request events are emitted after routing, when the real provider request starts (model_event → request_started), not at turn submit time.
Ordinary user chat should be queried as:
properties.module = 'session'
AND properties.ownerModule = 'session'
AND properties.executionKind = 'user_session'
Always-On reused agent-loop health should be queried as module = 'session' AND ownerModule = 'always_on', grouped by phase.
error_occurred Propertiesmodule: same module space as above plus runtime/ui contexts.ownerModule, executionKind, phase: same attribution semantics as feature_used.loopStage: where the error occurred.errorCategory: normalized category.code: error code (if available).No message, stack, or caller-supplied metadata is included.
path, cwd, root, etc.) and absolute-path string values are stripped before upload.installationId per day with any event (feature_used or error_occurred).instanceId per day.properties.module.module=session + ownerModule=session + executionKind=user_session.properties.module + properties.loopStage + properties.outcome.sessionId.