Studio Backend

Analytics Suite

The Studio’s Analytics page and the analytics tools of the MCP server read the conversation records the engine stores in MongoDB. AnalyticsRestController exposes one endpoint per card, all computed in AnalyticalService with aggregations in SessionAnalyticsRepository and EvaluationResultAnalyticsRepository. Every endpoint requires the Viewer role on the project.

Filters

The card endpoints take the same body (AnalyticsQuery):

Field Meaning

agentSelection

{"mode": "ALL"}, or INCLUDE / EXCLUDE with a list of {ref, revisions} — an agent ref and, optionally, which of its revisions.

agentState

Restrict to sessions held with an agent revision in one state, for example only Published.

periodDays, from, to

The window: the last periodDays days, or an explicit range.

userId

Only the conversations of one end user (the Filter by user control).

Changes ("+12 %") compare the window with the window of the same length right before it.

Who Is Counted

Simulated users are not traffic of the project’s agents: a selection that does not name its agents — ALL, or EXCLUDE — leaves out every TEST agent of the project (AnalyticsAgentScope). An INCLUDE selection is read as given, so a simulated user can still be inspected on purpose. Sessions in test mode — the tested agent’s side of a test-suite run — are always excluded, and so are the results of the evaluations run on them.

The conversations of Promethist staff accounts — usernames ending in @promethist.ai, the people who build, test and demo an account’s agents — count only for a viewer who is staff too. For anyone else, every read leaves them out, also when the user filter names one of them (AnalyticsPopulation, which caches the list of staff accounts for five minutes). This is the same exception staff have from the account privacy settings (Account Settings That Apply).

Cards and Endpoints

All paths are under /api/v1/project/{projectId}.

Endpoint Computes

POST /analytics/conversations/query

Sessions in the window and their change, sessions bucketed by turn count, the lifetime total.

POST /analytics/conversations/length/query

Total and average conversation length (seconds) with change, average turn count, and the same figures over the project’s lifetime.

POST /analytics/users/query

Unique end users in the window with change, lifetime unique users, and a trend series whose bucket width follows the window.

POST /analytics/users/conversations/query

A histogram: how many users had one, two, three…​ conversations in the window.

POST /analytics/agents/usage/query

Sessions per agent, every revision of an agent counted together, most used first.

GET /analytics/users/list

The end users the analytics count (Who Is Counted), paged — the options of the user filter.

POST /analytics/multimodal/query

Per interactive-content element: how often it was executed, with parameter and action counts (read from each session’s multimodalActions).

POST /analytics/evaluations/query

Aggregates of the evaluation results: featured metrics, boolean shares, number statistics, text samples and category histograms, plus per-element user statistics.

POST /analytics/evaluations/featured/query

Featured evaluation outcomes per user.

POST /analytics/export

The report as a file, see Exports.

How the Figures Are Computed

Sessions

Sessions come from the session collection, filtered by the project’s ref, the window, the agent selection, the state of the agent revision the session ran on and the user, without the sessions left out under Who Is Counted. Session and user counts skip sessions in which no turn completed; the engine’s turnCount runs one ahead of the stored turns, which TurnMath in util/Functions.kt accounts for.

Conversation length

A conversation’s length is its connected time: the sum of the duration of the session’s Realtime records in the engine’s edr collection — one per realtime connection, written when the connection closes, and the records billing charges for. A user who left and came back days later adds only the time actually connected. A session without such a record, usually one still connected, has no length yet: it is left out of the total and of the average’s denominator instead of counting as zero, while its turns still count towards the average turn count. EdrIndexMigration ensures a partial {sessionId, duration} index on edr that answers the lookup from the index alone; the cost is one lookup per session. Exports report the same per-session figure, empty while none is recorded.

Session end

The end shown in exports is the created time of the session’s newest stored turn plus a fixed allowance for that turn — a timestamp only, which is why the length is not end minus start. The newest turn is looked up by sessionId, not by the session’s lastTurnId, which runs one turn ahead of what is stored, because the engine opens the next turn as soon as a turn’s output completes. TurnIndexMigration ensures the {sessionId, created} index on turn that makes the lookup an index seek.

Lifetime figures

The lifetime variants cover every session the project ever had, so they are computed once per filter and cached for ten minutes. A request waits at most five seconds for them and otherwise answers without them (or with the previous value) while the computation finishes in the background for the next request.

Evaluations

Only evaluations that run at the end of a conversation and are admin-facing reach the analytics. The featured-outcome card reads, for the users with a featured result in the window, their results over their whole history — someone who passed in June and came back this week counts as passed. Agent evaluations (testMode) are left out, and so are the results of the sessions and users left out under Who Is Counted. EvaluationResultIndexMigration ensures the indexes these reads use.

A conversation’s evaluation results

The Conversations panel shows the selected conversation’s results in place of its transcript (GET /api/v1/project/{projectId}/sessions/{sessionId}/evaluations in SessionController, behind the transcript’s gates; SessionEvaluationService): the latest conversation-end run of each of the evaluations above, its elements in the evaluation’s order and worded by their definitions. A result is matched to the evaluation’s current element by id, then by ref, because saving an evaluation recreates its elements. A session or user left out under Who Is Counted gets no results. While the engine has yet to mark the session evaluated (evaluationResultsState still NOT_READY, the session changed within the last 15 minutes), the answer is pending, and the Studio asks again every ten seconds while it is.

Account Settings That Apply

Setting Effect on the analytics

hideSingleUserEvaluationResults

Every evaluation aggregate that fewer than two users stand behind is withheld and counted instead; the Studio shows how many were withheld. Featured outcomes of evaluations too few users tried are left out as well, and a conversation’s own results, always one user’s, are withheld entirely (the answer is only withheld).

includeEvaluationResultsInExports

When off, exports contain no evaluation results.

displayFullConversationData

When off, exports contain no transcripts, and the conversation list, transcripts and a conversation’s evaluation results are not readable (SessionController answers 403).

Staff are not restricted by these settings (Account Privacy Settings).

Exports

POST /api/v1/project/{projectId}/analytics/export (AnalyticsExportController, AnalyticsExportService) produces the report for a mandatory from–to range:

Format Content

xlsx

Sheets Summary (the filters and the conversation, user and length figures), Sessions (one row per session: id, user, agent, start and end, turns, duration and, when allowed, the transcript) and Evaluations (the per-session evaluation results, when allowed).

csv

A zip archive with one flat CSV per table, ready for pandas, Excel or BI tools.

json

The same report as one JSON document.

A caller can leave transcripts out even where the account would allow them (includeTranscript: false). Text cells are protected against spreadsheet formula injection.

The report is streamed end to end, so neither the rows nor the file are ever held in memory whole:

  • Everything that decides the export is settled before the first byte: the range and the format are checked (400), a range with more than 100,000 sessions is refused (413, with a request to narrow it), and the account settings decide whether transcripts and evaluation results are included. A refusal therefore still arrives as an error response.

  • The session rows and evaluation results are then read in small batches while the file is written. The workbook is Apache POI’s streaming variant, which keeps only the last 100 rows of a sheet in memory and moves the rest to temporary files.

  • An xlsx export with more evaluation results than one sheet holds (1,048,575 rows below the header) is refused with 413 as well, pointing to CSV or JSON. That is only known once the rows are read, but a workbook sends nothing before it is complete, so this refusal also arrives as an error response.

  • The response goes out in 32 KB chunks as they are produced, with no Content-Length. A failure after the first chunk can only cut the download short.

  • An Excel cell holds at most 32,767 characters. A longer text — a very long transcript — is cut to fit and ends with a marker that gives its full length; the CSV and JSON exports carry it whole.

The Studio saves the downloaded file through a temporary object URL that stays valid for 60 seconds (lib/trigger-browser-download.ts, shared with the test-suite export), because revoking it right after the click can cancel a large download.

The MCP tool export_analytics builds the same report. JSON is returned inline; xlsx and csv files are stored as they are rendered — at most 200 MB per file — and returned as a download link (Capability URLs).

Conversation Data for the MCP Server

Beyond the aggregated analytics, the MCP server gives Claude read access to the raw records (McpConversationDataTools, ConversationDataService):

  • list_sessions and get_session_transcript go through SessionController, with its role check and the full-conversation-data setting. Like the Studio’s list, list_sessions skips test-mode sessions, very short ones and, for a caller who is not staff, those of staff accounts.

  • search_turns, search_llm_logs, describe_conversation_data and export_conversation_data read the engine’s Elasticsearch indexes through the index patterns promethist.mcp.data-views.*. The engine writes one index per scope (the project ref) and month, {scope}-{turn|llmlog}-{namespace}-{yyyy.MM}, so a pattern spans all of them. Every query is restricted to the accounts in which the caller holds an account-level role — project-level rows are deliberately not used, because the documents carry only a tenant id — and an account with full conversation data switched off drops out of that set. Documents without a tenant id, written before the field existed, are never returned.

  • export_conversation_data streams up to 100,000 documents (10,000 by default) as NDJSON, or as CSV for turns, into GridFS — at most 200 MB per file — and returns a 24-hour download link.

  • fields, on export_conversation_data and search_llm_logs, is passed to Elasticsearch as _source includes, so a narrowed document never leaves the cluster whole and never counts against the 200 MB cap. Wildcards are allowed; created and the identifiers are added to whatever is asked for; includes replace the default exclude list rather than combining with it, because Elasticsearch applies excludes last. It is refused for CSV, whose columns are fixed, and alongside include_full, which asks for the opposite. An llmlog record of a relational planner call is around 290 KB, nearly all of it the agent’s configuration repeated per call, so this is the difference between a pilot’s model calls fitting in one export and in hundreds.