Skip to content

connection tester#1533

Open
arnaud-robin wants to merge 2 commits into
mainfrom
connection-tester
Open

connection tester#1533
arnaud-robin wants to merge 2 commits into
mainfrom
connection-tester

Conversation

@arnaud-robin

@arnaud-robin arnaud-robin commented Jul 24, 2026

Copy link
Copy Markdown
Member

Purpose

Users currently have no reliable way to check browser, media devices, and LiveKit connectivity before joining a room. Support also lacks a simple diagnostic report when people hit network issues (e.g. could not establish pc connection).

Proposal

Add a connection test page that runs local device checks plus LiveKit ConnectionCheck (WebSocket, WebRTC, TURN, publish audio/video, etc.), backed by a throttled GET /api/v1.0/connection-test/ endpoint that issues a short-lived token for an ephemeral room (including for anonymous users). When the run finishes, users can download a JSON report of the results.

Comment thread src/backend/core/tests/test_api_connection_test.py Fixed
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add connection test feature (backend API + frontend page)

✨ Enhancement 🕐 40+ Minutes

Grey Divider

AI Description

• Adds a new GET /api/v1.0/connection-test/ endpoint that issues short-lived, throttled LiveKit
 tokens for ephemeral diagnostic rooms, usable even by anonymous users.
• Adds a frontend "Test your configuration" page that runs browser/device/network checks via
 LiveKit's ConnectionCheck API and lets users download a JSON report.
• Ensures room_started/room_finished webhook events for connection-test rooms are ignored so
 they don't interfere with normal room lifecycle handling.
• Wires up new route, footer link, and localized strings (English/French) for the connection test
 page.
Diagram

sequenceDiagram
    actor User
    participant FE as ConnectionTest Page
    participant Hook as useConnectionTestRunner
    participant API as connection-test API
    participant LK as LiveKit Server
    User->>FE: Open /test-connection
    FE->>Hook: runTest()
    Hook->>API: GET /connection-test/
    API->>API: generate_token(ttl=10m)
    API-->>Hook: livekit url, room, token
    Hook->>LK: ConnectionCheck (ws, webrtc, turn, publish)
    LK-->>Hook: check results
    Hook-->>FE: step statuses
    FE-->>User: Download report
Loading
High-Level Assessment

The design reuses existing token generation and throttling infrastructure (adding a TTL parameter and dedicated throttle scopes) rather than building a parallel mechanism, and reuses LiveKit's built-in ConnectionCheck API on the frontend instead of reimplementing WebRTC/TURN diagnostics. This is the standard, lowest-risk approach for this feature; no better alternative was evident.

Files changed (23) +802 / -3

Enhancement (12) +608 / -3
connection_test.pyNew connection-test API view +41/-0

New connection-test API view

• Adds a throttled GET endpoint that creates an ephemeral LiveKit room and returns a short-lived token for diagnostics, including for anonymous users.

src/backend/core/api/connection_test.py

throttling.pyAdd throttle classes for connection test endpoint +12/-0

Add throttle classes for connection test endpoint

• Introduces ConnectionTestUserRateThrottle and ConnectionTestAnonRateThrottle scoped to 'connection_test' to limit abuse of the new endpoint.

src/backend/core/api/throttling.py

urls.pyRegister connection-test URL route +6/-0

Register connection-test URL route

• Wires the new get_connection_test_config view to the /connection-test/ path.

src/backend/core/urls.py

utils.pySupport custom TTL in generate_token +8/-3

Support custom TTL in generate_token

• Adds an optional ttl parameter to generate_token to allow issuing short-lived tokens via AccessToken.with_ttl.

src/backend/core/utils.py

fetchConnectionTestToken.tsAdd API client for fetching connection test token +13/-0

Add API client for fetching connection test token

• New helper calling the backend /connection-test/ endpoint and typing the response.

src/frontend/src/features/connection-test/api/fetchConnectionTestToken.ts

useConnectionTestRunner.tsAdd connection test runner hook +245/-0

Add connection test runner hook

• Implements a stateful hook that sequentially runs browser, microphone, camera, device, and LiveKit connectivity checks, managing step statuses and abort/reset logic.

src/frontend/src/features/connection-test/hooks/useConnectionTestRunner.ts

ConnectionTest.tsxAdd Connection Test page component +155/-0

Add Connection Test page component

• New route component rendering the test UI, step results, video preview, and report download button.

src/frontend/src/features/connection-test/routes/ConnectionTest.tsx

types.tsAdd connection test types and initial step factory +47/-0

Add connection test types and initial step factory

• Defines step ids, statuses, log/result types, and a helper to build the initial pending steps list.

src/frontend/src/features/connection-test/types.ts

downloadConnectionTestReport.tsAdd utility to build and download JSON test report +49/-0

Add utility to build and download JSON test report

• Builds a JSON report from step results and triggers a browser download with a timestamped filename.

src/frontend/src/features/connection-test/utils/downloadConnectionTestReport.ts

Footer.tsxAdd connection test link to footer +10/-0

Add connection test link to footer

• Adds a new footer link pointing to the /test-connection route.

src/frontend/src/layout/Footer.tsx

routes.tsRegister /test-connection route +9/-0

Register /test-connection route

• Adds a lazily-loaded ConnectionTest route entry to the app's route table.

src/frontend/src/routes.ts

index.cssHide LiveKit-injected video element during publish check +13/-0

Hide LiveKit-injected video element during publish check

• Adds CSS to keep the temporary <video> element LiveKit appends during publishVideo check decodable but visually hidden.

src/frontend/src/styles/index.css

Bug fix (1) +19 / -0
livekit_events.pyIgnore webhook events for connection-test rooms +19/-0

Ignore webhook events for connection-test rooms

• Adds a helper to detect ephemeral connection-test rooms by prefix and short-circuits room_started/room_finished handling for them to avoid unnecessary DB lookups and errors.

src/backend/core/services/livekit_events.py

Tests (2) +93 / -0
test_livekit_events.pyTests for ignoring connection-test room webhook events +27/-0

Tests for ignoring connection-test room webhook events

• Adds tests verifying room_started and room_finished handlers skip processing for connection-test rooms.

src/backend/core/tests/services/test_livekit_events.py

test_api_connection_test.pyTests for connection-test API endpoint +66/-0

Tests for connection-test API endpoint

• Adds tests verifying each request returns a unique ephemeral room and a token with the configured short TTL.

src/backend/core/tests/test_api_connection_test.py

Documentation (7) +67 / -0
CHANGELOG.mdAdd changelog entry for connection test feature +1/-0

Add changelog entry for connection test feature

• Documents the new connection test feature in the Added section.

CHANGELOG.md

connectionTest.jsonAdd English locale for connection test page +31/-0

Add English locale for connection test page

• New translation file with strings for the connection test UI.

src/frontend/src/locales/en/connectionTest.json

global.jsonAdd connectionTest link label to English global locale +1/-0

Add connectionTest link label to English global locale

• Adds a footer link label for the connection test page.

src/frontend/src/locales/en/global.json

home.jsonAdd connectionTestLink string to English home locale +1/-0

Add connectionTestLink string to English home locale

• Adds a home page link label for the connection test feature.

src/frontend/src/locales/en/home.json

connectionTest.jsonAdd French locale for connection test page +31/-0

Add French locale for connection test page

• New French translation file mirroring the English connection test strings.

src/frontend/src/locales/fr/connectionTest.json

global.jsonAdd connectionTest link label to French global locale +1/-0

Add connectionTest link label to French global locale

• Adds a French footer link label for the connection test page.

src/frontend/src/locales/fr/global.json

home.jsonAdd connectionTestLink string to French home locale +1/-0

Add connectionTestLink string to French home locale

• Adds a French home page link label for the connection test feature.

src/frontend/src/locales/fr/home.json

Other (1) +15 / -0
settings.pyAdd settings for connection test throttle rate, TTL, and room prefix +15/-0

Add settings for connection test throttle rate, TTL, and room prefix

• Introduces CONNECTION_TEST_THROTTLE_RATES, CONNECTION_TEST_TOKEN_TTL_SECONDS, and CONNECTION_TEST_ROOM_PREFIX configuration values.

src/backend/meet/settings.py

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Missing unmount cleanup 🐞 Bug ☼ Reliability
Description
useConnectionTestRunner starts async work and may create local media tracks, but it never
aborts/stops them on component unmount; navigating away mid-run can leave the camera capture running
and keep background work alive. Cleanup currently only happens via reset() or the finally block when
not aborted, and the ConnectionTest route never calls reset() on unmount.
Code

src/frontend/src/features/connection-test/hooks/useConnectionTestRunner.ts[R117-236]

+  const runTest = async () => {
+    abortRef.current?.abort()
+    const controller = new AbortController()
+    abortRef.current = controller
+    const { signal } = controller
+
+    setIsRunning(true)
+    setSteps(createInitialSteps())
+    stopVideoTrack()
+
+    try {
+      await runStep('browser', signal, async () => {
+        const browser = getBrowser()
+        if (!browser) throw new Error('Browser not detected')
+        return {
+          summary: `${browser.name} ${browser.version}`,
+          data: {
+            name: browser.name,
+            version: browser.version,
+            os: browser.os,
+            osVersion: browser.osVersion,
+          },
+        }
+      })
+      if (signal.aborted) return
+
+      const microphoneOk = await runStep('microphone', signal, async () => {
+        const track = await createLocalAudioTrack()
+        const label =
+          track.mediaStreamTrack.label ||
+          track.mediaStreamTrack.getSettings().deviceId ||
+          ''
+        track.stop()
+        return { summary: label, data: { label } }
+      })
+      if (signal.aborted) return
+      if (!microphoneOk) openPermissionsDialog('audioinput')
+
+      const cameraOk = await runStep('camera', signal, async () => {
+        const track = await createLocalVideoTrack()
+        videoTrackRef.current = track
+        setVideoTrack(track)
+        const settings = track.mediaStreamTrack.getSettings()
+        const label = track.mediaStreamTrack.label || ''
+        return {
+          summary: label,
+          data: {
+            label,
+            width: settings.width,
+            height: settings.height,
+          },
+        }
+      })
+      if (signal.aborted) return
+      if (!cameraOk) openPermissionsDialog('videoinput')
+
+      await runStep('devices', signal, async () => {
+        const devices = await navigator.mediaDevices.enumerateDevices()
+        return {
+          summary: String(devices.length),
+          data: groupDevicesByKind(devices),
+        }
+      })
+      if (signal.aborted) return
+
+      let checker: ConnectionCheck
+      try {
+        const { livekit } = await fetchConnectionTestToken()
+        checker = new ConnectionCheck(livekit.url, livekit.token)
+      } catch (error) {
+        skipSteps(
+          LIVEKIT_STEP_IDS,
+          getErrorMessage(error, 'Failed to fetch test token')
+        )
+        return
+      }
+
+      await runStep('websocket', signal, async () =>
+        fromCheckInfo(await checker.checkWebsocket())
+      )
+      await runStep('webrtc', signal, async () =>
+        fromCheckInfo(await checker.checkWebRTC())
+      )
+      await runStep('turn', signal, async () =>
+        fromCheckInfo(await checker.checkTURN())
+      )
+      await runStep('reconnect', signal, async () =>
+        fromCheckInfo(await checker.checkReconnect())
+      )
+
+      if (!microphoneOk) {
+        skipSteps(['publishAudio'], 'Microphone permission required')
+      } else {
+        await runStep('publishAudio', signal, async () =>
+          fromCheckInfo(await checker.checkPublishAudio())
+        )
+      }
+
+      if (!cameraOk) {
+        skipSteps(['publishVideo'], 'Camera permission required')
+      } else {
+        stopVideoTrack()
+        await runStep('publishVideo', signal, async () =>
+          fromCheckInfo(await checker.checkPublishVideo())
+        )
+      }
+    } finally {
+      if (!signal.aborted) {
+        stopVideoTrack()
+        setIsRunning(false)
+      }
+    }
+  }
+
+  const reset = () => {
+    abortRef.current?.abort()
+    stopVideoTrack()
+    setSteps(createInitialSteps())
+    setIsRunning(false)
+  }
Evidence
The hook creates/holds an AbortController and media tracks, but provides no unmount cleanup; the
route only uses runTest (not reset), so leaving the page does not abort/stop tracks. The finally
block also intentionally skips stopVideoTrack/setIsRunning(false) when aborted, making cleanup
depend on explicit reset or successful completion.

src/frontend/src/features/connection-test/hooks/useConnectionTestRunner.ts[59-236]
src/frontend/src/features/connection-test/routes/ConnectionTest.tsx[71-116]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The connection test runner can keep capturing camera/video and continue async work if the user navigates away while the test is running, because the hook does not register any unmount cleanup to abort the current run and stop tracks.

### Issue Context
- `runTest()` creates `AbortController` and can create a `LocalVideoTrack`, but there is no `useEffect(() => () => ...)` cleanup in the hook.
- Cleanup is conditional (`finally` only stops track when `!signal.aborted`) and `reset()` is not called by the route on unmount.

### Fix Focus Areas
- src/frontend/src/features/connection-test/hooks/useConnectionTestRunner.ts[59-236]
- src/frontend/src/features/connection-test/routes/ConnectionTest.tsx[71-116]

### Suggested implementation notes
- Add a `useEffect` in `useConnectionTestRunner` with a cleanup function that:
 - calls `abortRef.current?.abort()`
 - stops any current `videoTrackRef.current` (and clears it)
 - prevents further state updates (e.g., via an `isMounted` ref or by checking `signal.aborted` plus a `didUnmount` flag).
- Alternatively (or additionally), have `ConnectionTest.tsx` destructure `reset` and call it in an unmount cleanup effect.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Ruff checks will fail 🐞 Bug ⚙ Maintainability
Description
Backend changes introduce Ruff format/isort violations (notably in core.utils.generate_token
indentation and standard-library import ordering, plus import sectioning in the new connection_test
module), which will fail the CI steps that run ruff format . --diff and ruff check .. This
blocks merge because the backend workflow enforces Ruff formatting and linting.
Code

src/backend/core/utils.py[R124-139]

    token = (
        AccessToken(
-            api_key=settings.LIVEKIT_CONFIGURATION["api_key"],
-            api_secret=settings.LIVEKIT_CONFIGURATION["api_secret"],
-        )
+        api_key=settings.LIVEKIT_CONFIGURATION["api_key"],
+        api_secret=settings.LIVEKIT_CONFIGURATION["api_secret"],
+    )
        .with_grants(video_grants)
        .with_identity(identity)
        .with_name(display_name)
        .with_attributes(
            {"color": color, "room_admin": "true" if is_admin_or_owner else "false"}
        )
    )
+    if ttl is not None:
+        token = token.with_ttl(ttl)
Evidence
core/utils.py shows mis-indented formatting inside the AccessToken(...) call (which Ruff formatter
would rewrite) and non-isort ordering of standard-library imports; the new connection_test.py mixes
django and rest_framework imports without section separation; and the backend CI explicitly runs
Ruff format and Ruff check, making these violations merge-blocking.

src/backend/core/utils.py[9-18]
src/backend/core/utils.py[125-139]
src/backend/core/api/connection_test.py[3-15]
src/backend/core/tests/test_api_connection_test.py[3-12]
.github/workflows/meet.yml[113-136]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Some backend Python changes are not compliant with the repository's enforced Ruff formatter and Ruff isort rules (import ordering/sectioning and formatting/indentation). CI runs Ruff formatting in diff mode and Ruff lint checks, so these files will fail.

### Issue Context
The backend GitHub workflow runs:
- `ruff format . --diff`
- `ruff check .`

### Fix Focus Areas
- src/backend/core/utils.py[9-18]
- src/backend/core/utils.py[125-139]
- src/backend/core/api/connection_test.py[3-15]
- src/backend/core/tests/test_api_connection_test.py[3-12]
- .github/workflows/meet.yml[113-136]

### Suggested implementation notes
- Run `ruff format` on `src/backend` and commit the result.
- Fix import ordering/sectioning to satisfy `ruff check` (isort rules), e.g.:
 - ensure standard-library `from ... import ...` lines are ordered correctly (`datetime` vs `functools`),
 - separate `django` imports from other third-party imports with a blank line as Ruff/isort expects.
- After edits, re-run `ruff format` and `ruff check` locally to confirm clean output.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +117 to +236
const runTest = async () => {
abortRef.current?.abort()
const controller = new AbortController()
abortRef.current = controller
const { signal } = controller

setIsRunning(true)
setSteps(createInitialSteps())
stopVideoTrack()

try {
await runStep('browser', signal, async () => {
const browser = getBrowser()
if (!browser) throw new Error('Browser not detected')
return {
summary: `${browser.name} ${browser.version}`,
data: {
name: browser.name,
version: browser.version,
os: browser.os,
osVersion: browser.osVersion,
},
}
})
if (signal.aborted) return

const microphoneOk = await runStep('microphone', signal, async () => {
const track = await createLocalAudioTrack()
const label =
track.mediaStreamTrack.label ||
track.mediaStreamTrack.getSettings().deviceId ||
''
track.stop()
return { summary: label, data: { label } }
})
if (signal.aborted) return
if (!microphoneOk) openPermissionsDialog('audioinput')

const cameraOk = await runStep('camera', signal, async () => {
const track = await createLocalVideoTrack()
videoTrackRef.current = track
setVideoTrack(track)
const settings = track.mediaStreamTrack.getSettings()
const label = track.mediaStreamTrack.label || ''
return {
summary: label,
data: {
label,
width: settings.width,
height: settings.height,
},
}
})
if (signal.aborted) return
if (!cameraOk) openPermissionsDialog('videoinput')

await runStep('devices', signal, async () => {
const devices = await navigator.mediaDevices.enumerateDevices()
return {
summary: String(devices.length),
data: groupDevicesByKind(devices),
}
})
if (signal.aborted) return

let checker: ConnectionCheck
try {
const { livekit } = await fetchConnectionTestToken()
checker = new ConnectionCheck(livekit.url, livekit.token)
} catch (error) {
skipSteps(
LIVEKIT_STEP_IDS,
getErrorMessage(error, 'Failed to fetch test token')
)
return
}

await runStep('websocket', signal, async () =>
fromCheckInfo(await checker.checkWebsocket())
)
await runStep('webrtc', signal, async () =>
fromCheckInfo(await checker.checkWebRTC())
)
await runStep('turn', signal, async () =>
fromCheckInfo(await checker.checkTURN())
)
await runStep('reconnect', signal, async () =>
fromCheckInfo(await checker.checkReconnect())
)

if (!microphoneOk) {
skipSteps(['publishAudio'], 'Microphone permission required')
} else {
await runStep('publishAudio', signal, async () =>
fromCheckInfo(await checker.checkPublishAudio())
)
}

if (!cameraOk) {
skipSteps(['publishVideo'], 'Camera permission required')
} else {
stopVideoTrack()
await runStep('publishVideo', signal, async () =>
fromCheckInfo(await checker.checkPublishVideo())
)
}
} finally {
if (!signal.aborted) {
stopVideoTrack()
setIsRunning(false)
}
}
}

const reset = () => {
abortRef.current?.abort()
stopVideoTrack()
setSteps(createInitialSteps())
setIsRunning(false)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Missing unmount cleanup 🐞 Bug ☼ Reliability

useConnectionTestRunner starts async work and may create local media tracks, but it never
aborts/stops them on component unmount; navigating away mid-run can leave the camera capture running
and keep background work alive. Cleanup currently only happens via reset() or the finally block when
not aborted, and the ConnectionTest route never calls reset() on unmount.
Agent Prompt
### Issue description
The connection test runner can keep capturing camera/video and continue async work if the user navigates away while the test is running, because the hook does not register any unmount cleanup to abort the current run and stop tracks.

### Issue Context
- `runTest()` creates `AbortController` and can create a `LocalVideoTrack`, but there is no `useEffect(() => () => ...)` cleanup in the hook.
- Cleanup is conditional (`finally` only stops track when `!signal.aborted`) and `reset()` is not called by the route on unmount.

### Fix Focus Areas
- src/frontend/src/features/connection-test/hooks/useConnectionTestRunner.ts[59-236]
- src/frontend/src/features/connection-test/routes/ConnectionTest.tsx[71-116]

### Suggested implementation notes
- Add a `useEffect` in `useConnectionTestRunner` with a cleanup function that:
  - calls `abortRef.current?.abort()`
  - stops any current `videoTrackRef.current` (and clears it)
  - prevents further state updates (e.g., via an `isMounted` ref or by checking `signal.aborted` plus a `didUnmount` flag).
- Alternatively (or additionally), have `ConnectionTest.tsx` destructure `reset` and call it in an unmount cleanup effect.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/backend/core/utils.py
Comment on lines 124 to 139

token = (
AccessToken(
api_key=settings.LIVEKIT_CONFIGURATION["api_key"],
api_secret=settings.LIVEKIT_CONFIGURATION["api_secret"],
)
api_key=settings.LIVEKIT_CONFIGURATION["api_key"],
api_secret=settings.LIVEKIT_CONFIGURATION["api_secret"],
)
.with_grants(video_grants)
.with_identity(identity)
.with_name(display_name)
.with_attributes(
{"color": color, "room_admin": "true" if is_admin_or_owner else "false"}
)
)
if ttl is not None:
token = token.with_ttl(ttl)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Ruff checks will fail 🐞 Bug ⚙ Maintainability

Backend changes introduce Ruff format/isort violations (notably in core.utils.generate_token
indentation and standard-library import ordering, plus import sectioning in the new connection_test
module), which will fail the CI steps that run ruff format . --diff and ruff check .. This
blocks merge because the backend workflow enforces Ruff formatting and linting.
Agent Prompt
### Issue description
Some backend Python changes are not compliant with the repository's enforced Ruff formatter and Ruff isort rules (import ordering/sectioning and formatting/indentation). CI runs Ruff formatting in diff mode and Ruff lint checks, so these files will fail.

### Issue Context
The backend GitHub workflow runs:
- `ruff format . --diff`
- `ruff check .`

### Fix Focus Areas
- src/backend/core/utils.py[9-18]
- src/backend/core/utils.py[125-139]
- src/backend/core/api/connection_test.py[3-15]
- src/backend/core/tests/test_api_connection_test.py[3-12]
- .github/workflows/meet.yml[113-136]

### Suggested implementation notes
- Run `ruff format` on `src/backend` and commit the result.
- Fix import ordering/sectioning to satisfy `ruff check` (isort rules), e.g.:
  - ensure standard-library `from ... import ...` lines are ordered correctly (`datetime` vs `functools`),
  - separate `django` imports from other third-party imports with a blank line as Ruff/isort expects.
- After edits, re-run `ruff format` and `ruff check` locally to confirm clean output.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Currently users have no way to reliably test their connection before
joining a room. We then want to create a connection test page
to adress this issue.

The testing will require a dedicated LiveKit token without going
through the room API, which is tied to registered meetings, lobby rules,
and longer-lived access tokens.

We introduce a new API endpoint GET /api/v1.0/connection-test/
to issue a dedicated token for diagnostics, even for anonymous users.
Each request creates a new room so users never share
the same LiveKit room during tests. Tokens are short-lived
(default 10 minutes) to limit reuse,
and the endpoint is throttled to prevent abuse.
Introduce a new connection test page to allow users to verify
their device and network compatibility with the application.
The feature also supports generating and downloading a detailed report
of the test results.
@sonarqubecloud

Copy link
Copy Markdown

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.

1 participant