Skip to content

feat(analytics): print session events and participant sessions under --experimental-auth - #1024

Merged
Topherhindman merged 8 commits into
mainfrom
devx-800-cli-participant-sessions-events
Oct 10, 2026
Merged

Topherhindman merged 8 commits into
mainfrom
devx-800-cli-participant-sessions-events

Conversation

@Topherhindman

@Topherhindman Topherhindman commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Fixes DEVX-800
Stacked on #1023.
Depends on livekit/public-api-server#56.

lk analytics session events SESSION_ID prints a page of a session's events, such as participants joining and leaving or tracks being published. lk analytics session participant list and lk analytics session get now also print each participant's participant sessions, one per connection, with the client it connected from. Both run only under --experimental-auth.

What changed

  • Regenerated client (chore(public)): oapi.gen.go is regenerated from livekit/public-api-server@6e037c6, livekit/public-api-server#56's merge commit on main. It picks up three server changes:

    • Session events: ListSessionEvents filters by repeated types (AnalyticsEventType names), a participantSessionId (PA_...) and a sortOrder, and pages by keyset cursor with no total. Each event names the participant session it's about, and its payload is the server's allowlist of fields, which no longer includes an API call's twirpErrorMessage. With a participantSessionId and no types, the read defaults to the eleven types the dashboard's participant events table shows, track events included.
    • Participant sessions: each ParticipantInfo carries its participant sessions, at most 50, one per connection. Each has its participantSessionId, client (OS, browser, device, SDK, connection type and time), location, region and duration. ParticipantInfo loses roomId, and GetParticipant is gone.
    • Session list rows lose bandwidthIn, bandwidthOut and connectionSeconds. The server never filled them on list rows, and GetSession's detail still carries all three.

    Nothing hand-written reads the removed fields or calls the removed RPC, so only oapi.gen.go changes.

  • Client (feat(public)):

    • ListSessionEvents returns one page of a session's events and the cursor for the next.
    • EventOptions embeds PageOptions and adds the types to keep, one participant session to follow, and the sort order.
    • Types are friendly names: LiveKit's analytics event types in lowercase, such as participant_joined or track_published. EventTypeNames lists them, and EventTypeName makes one from the API's.
    • Validate rejects an unknown type (listing the names it knows), an unknown sort order, a negative limit, or a participant session id that doesn't start with PA_, before any request is sent. That last one catches an identity passed by mistake.
    • Events are the generated type, their payload marshalling back to the API's own JSON. An unknown session is NotFound.
  • Command (feat(analytics)):

    • Participant sessions: session participant list and session get print a second table after the participants. It shows the identity, the participant session id, when it joined and left, how long it lasted, the client (OS, browser, device model and SDK version, each only when reported), the transport and how long it took to connect, and the location. A participant whose participant sessions couldn't be read has no rows there. With none at all, the table is left out. --json still prints the API's rows as sent, participant sessions nested.
    • Event lines: each event prints its time to the millisecond, its type by friendly name, and the participant identity and participant session it's about, or dashes for a room event or an API call. Then its payload as key=value pairs sorted by key, clipped on a line.
    • JSON: --json prints {items, nextCursor} with the events as the API sent them.
    • Types: with no --type, the command prints the dashboard's seven defaults: participant_joined, participant_left, participant_active, participant_resumed, room_created, room_ended and api_call. With --participant and no --type, it prints the dashboard's eleven participant types: the four participant events plus track_muted, track_unmuted, track_published, track_unpublished, track_subscribed, track_subscribe_requested and track_subscribe_failed. --type is repeatable. An unknown one fails before the project lookup and lists the names it knows.
    • Participant: --participant takes a participant session id, as lk analytics session participant list now prints it, and refuses an identity.
    • Order and paging: --sort-order asc|desc orders by time, oldest first by default. The next-page hint says to re-run with its --cursor. The user's own command already repeats the types, participant session and order the cursor was made for.
    • Empty page: it says why: no events of the types or for the participant session asked for, none kept (events last 60 days), or no more pages.
    • Errors go through sessionReadError: an unknown session says there's no such session in the project, and names the project, and a permission denial says the user doesn't have access to the project. Events are a read-level read with no user data recording gate, so there's no recording-off case.
    • Runs only under --experimental-auth, through sessionRead.

Usage

lk --experimental-auth analytics --experimental session events SESSION_ID \
    [--type TYPE ...] [--participant PA_ID] [--sort-order asc|desc] [--limit N] [--json]
  • --limit defaults to 50, the server's page size. --cursor is hidden.
11:00:00.000  room_created        -      -
11:00:01.250  participant_joined  alice  PA_aaaaaaaaaaaa  connectionType=UDP isMigration=false participantKind=STANDARD
11:00:02.000  track_published     alice  PA_aaaaaaaaaaaa  mimeType=audio/opus muted=false trackId=TR_1 trackSource=MICROPHONE trackType=AUDIO
11:00:03.000  api_call            -      -                durationNs=1500000 method=UpdateRoomMetadata service=RoomService status=0
11:05:00.000  room_ended          -      -                reason="departure timeout"
More events available — re-run with --cursor c2

The participant sessions table that session participant list and session get print after the participants:

┌──────────┬─────────────────────┬──────────────────┬──────────────────┬──────────┬────────────────────────┬─────────────┬──────────┐
│ Identity │ Participant Session │ Joined           │ Left             │ Duration │ Client                 │ Connection  │ Location │
├──────────┼─────────────────────┼──────────────────┼──────────────────┼──────────┼────────────────────────┼─────────────┼──────────┤
│ alice    │ PA_alice1           │ 2026-10-07 11:00 │ 2026-10-07 11:01 │ 1m0s     │ mac, chrome, SDK 2.6.1 │ UDP (120ms) │ Canada   │
│ alice    │ PA_alice2           │ 2026-10-07 11:01 │ -                │ 1h2m3s   │ ios, iPhone 15         │ TURN        │ Canada   │
└──────────┴─────────────────────┴──────────────────┴──────────────────┴──────────┴────────────────────────┴─────────────┴──────────┘

Since review

  • 6811756 fix(analytics): name the participant list command in full in --participant's help
  • dc1a9c1 test(analytics): drop twirpErrorMessage from the events fixture
  • 8c2cfa8 fix(analytics): say a participant's events default to the dashboard's eleven
  • bcb53de fix(analytics): strip terminal escapes from events and participant sessions

Testing

  • go build ./..., go vet ./pkg/... ./cmd/lk/ and go test ./pkg/... ./cmd/lk/ pass on every commit.
  • New tests:
    • pkg/public: TestListSessionEvents, TestSessionEventJSON, TestListSessionEventsRejectsBadOptions, TestEventTypeNames and TestListSessionEventsUnknownSession.
    • pkg/public/render: TestSessionParticipantsPageNoSessions, TestSessionEventsText, TestEventPayloadClips, TestSessionEventsEmpty and TestSessionEventsJSON. TestSessionDetailText and TestSessionParticipantsPage now also check the participant sessions table.
    • cmd/lk: TestSessionEventsCommand, TestSessionEventsRequiresExperimentalAuth, TestEventOptions (an identity passed to --participant is refused), TestFetchSessionEvents (each empty-page reason), TestFetchSessionEventsJSON and TestFetchSessionEventsErrors.
  • The commands above were run on staging on 2026-10-08, against public-api-server's stack run locally, with a real sign-in and staging data.
  • Compared side by side with the staging dashboard on 2026-10-09 for two sessions: session detail, participants, events, transcript, agent logs and traces match, down to timestamps, turn latencies, redactions and log levels. The dashboard's "Dispatch" trace row is built in the browser from job_entrypoint's span events, which these reads return. Metrics are the known exception: the aggregator files several token and TTS series under one name, which the dashboard avoids by deriving usage from the transcript and traces.

Not in this PR

  • lk analytics session participant get: there isn't one. lk room participant covers looking up one participant.

@Topherhindman
Topherhindman force-pushed the devx-800-cli-participant-sessions-events branch from ef763a7 to 77c654f Compare October 9, 2026 02:57
@Topherhindman
Topherhindman force-pushed the devx-798-cli-session-metrics branch 2 times, most recently from 32f6772 to 279bb7d Compare October 9, 2026 05:11
@Topherhindman
Topherhindman force-pushed the devx-800-cli-participant-sessions-events branch from 77c654f to 1663a2f Compare October 9, 2026 05:11
@Topherhindman
Topherhindman marked this pull request as ready for review October 9, 2026 06:27
@Topherhindman
Topherhindman force-pushed the devx-798-cli-session-metrics branch from 279bb7d to bbfc89f Compare October 9, 2026 20:26
@Topherhindman
Topherhindman force-pushed the devx-800-cli-participant-sessions-events branch 2 times, most recently from d8e9433 to bcb53de Compare October 9, 2026 20:50
@Topherhindman
Topherhindman force-pushed the devx-798-cli-session-metrics branch from bbfc89f to a17eb17 Compare October 9, 2026 20:50
@Topherhindman
Topherhindman force-pushed the devx-800-cli-participant-sessions-events branch from bcb53de to ae6a1dd Compare October 10, 2026 03:11
@Topherhindman
Topherhindman force-pushed the devx-798-cli-session-metrics branch 2 times, most recently from 411b38b to 79dae47 Compare October 10, 2026 03:12
@Topherhindman
Topherhindman force-pushed the devx-800-cli-participant-sessions-events branch from ae6a1dd to 2d0e47e Compare October 10, 2026 03:12
@Topherhindman
Topherhindman force-pushed the devx-798-cli-session-metrics branch from 79dae47 to 9b007b1 Compare October 10, 2026 03:48
@Topherhindman
Topherhindman force-pushed the devx-800-cli-participant-sessions-events branch from 2d0e47e to 9c1422c Compare October 10, 2026 03:48
@Topherhindman
Topherhindman force-pushed the devx-798-cli-session-metrics branch from 9b007b1 to 39ae30b Compare October 10, 2026 04:21
@Topherhindman
Topherhindman force-pushed the devx-800-cli-participant-sessions-events branch 2 times, most recently from 2759435 to 9eb6d33 Compare October 10, 2026 04:37
@Topherhindman
Topherhindman force-pushed the devx-798-cli-session-metrics branch 2 times, most recently from 0eb7b58 to d73b173 Compare October 10, 2026 04:53
@Topherhindman
Topherhindman force-pushed the devx-800-cli-participant-sessions-events branch from 9eb6d33 to e200807 Compare October 10, 2026 04:53
Base automatically changed from devx-798-cli-session-metrics to main October 10, 2026 05:02
Picks up ListSessionEvents as the server now serves it: the request
filters by repeated types (AnalyticsEventType names), a
participantSessionId (PA_...) and a sortOrder, and pages by keyset
cursor with no total. Each event gains the participant session it is
about, and its payload is the server's allowlist of fields, which no
longer includes an API call's twirpErrorMessage. With a
participantSessionId and no types, the read defaults to the eleven
event types the dashboard's participant events table shows, track
events included, rather than the session's seven.

Also picks up the participants change: each ParticipantInfo carries its
participant sessions, at most 50, one per connection, each with its
participantSessionId (was participantId), client (OS, browser, device,
SDK, connection type and time), location, region and duration.
ParticipantInfo loses roomId, and GetParticipant and its
ParticipantDetail are gone.

And the session list row loses bandwidthIn, bandwidthOut and
connectionSeconds: the server never filled them on list rows, and
GetSession's detail still carries all three.

Nothing hand-written reads the removed fields or calls the removed RPC
(the session renderer reads bandwidth and connection time from the
detail, and the legacy analytics command decodes its own response
type), so only oapi.gen.go changes.

Generated from livekit/public-api-server@6e037c6
The Public API now returns each participant with its participant
sessions, one per connection, each with the client it connected from.
`lk analytics session participant list` and `lk analytics session get`
print them in a second table after the participants: the participant's
identity, the participant session id (PA_...), when it joined and left,
how long it lasted, the client (OS, browser, device model and SDK
version, each only when reported), the transport and how long it took
to connect, and the location. A participant whose participant sessions
couldn't be read has no rows there; with none at all the table is left
out.

--json still prints the API's rows as sent, participant sessions
nested.
ListSessionEvents returns one page of a session's events, such as
participants joining and leaving or tracks being published, and the
cursor for the next. EventOptions embeds PageOptions and adds the types
to keep, one participant session to follow and the sort order.

Types are friendly names: LiveKit's analytics event types in lowercase,
such as participant_joined or track_published, which EventTypeNames
lists and EventTypeName makes from the API's. Validate rejects an
unknown type, with the names it knows, an unknown sort order, a
negative limit, or a participant session id that doesn't start with
PA_ (an identity passed by mistake) before any request is sent.

Events are the generated type, their allowlisted payload marshalling
back to the API's own JSON. An unknown session is NotFound.
`lk analytics session events SESSION_ID` prints a page of the session's
events, one line per event: its time to the millisecond, its type by
its friendly name, the participant identity and participant session
(PA_...) it is about, or dashes for a room event or an API call, and
its payload as key=value pairs sorted by key, clipped on a line. --json
prints {items, nextCursor} with the events as the API sent them.

With no --type it prints the dashboard's seven defaults. --type is
repeatable and takes the API's event types in lowercase, such as
track_published; an unknown one fails before the project lookup and
lists the names it knows. --participant takes a participant session id,
as `session participant list` now prints it, and refuses an identity.
--sort-order asc|desc orders by time, oldest first by default. --limit
(default 50, the server's page size) and a hidden --cursor page through
it, and a next page says to re-run with its --cursor: the user's own
command already repeats the types, participant session and order a
cursor was made for.

An empty page says why: no events of the types or for the participant
session asked for, none kept (events last 60 days), or no more pages.
An unknown session says there's no such session in the project, and a
permission denial says the user doesn't have access to the project,
through sessionReadError. Events are a READ-level read with no user
data recording gate, so there is no recording-off case.

Only the Public API serves session events, so the command runs only
under --experimental-auth, through sessionRead, with the signed-in
user's session token, and refuses to run otherwise before reading its
arguments.
…ipant's help

--participant's help pointed at `session participant list` without the
`lk analytics` prefix. It now names the full command, like the events
description and other lk hints.
The server's API_CALL payload allowlist no longer includes
twirpErrorMessage, so the events render fixture no longer sends it.
… eleven

With a participant session and no types, the server now returns the
eleven event types the dashboard's participant events table shows,
track events included, not the session's seven. The events command's
description, its --type and --participant help, and EventOptions.Types
now say so.
…ssions

An event's participant identity, and payload values such as a
participant's name or metadata, are what a participant chose, and the
events read printed them as they came, so in a terminal an escape
sequence in one could set the window title or clear the screen. The same
went for what a participant's client reported about itself in the
participant sessions table: its OS, browser, device, SDK version and
connection type.

The identity and participant session id now go through dashText, and
are measured after stripping so the payloads still line up.
payloadValue strips a value's control characters before deciding
whether to quote it; a value it prints as JSON has them escaped already,
but loses any DEL or C1 control too. The participant sessions table runs
its identity, participant session id, client fields, connection type
and location through dashText or oneLine.
@Topherhindman
Topherhindman force-pushed the devx-800-cli-participant-sessions-events branch from e200807 to 0a431da Compare October 10, 2026 05:03
@Topherhindman
Topherhindman merged commit 4436fab into main Oct 10, 2026
26 checks passed
@Topherhindman
Topherhindman deleted the devx-800-cli-participant-sessions-events branch October 10, 2026 05:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants