From a4a891785da921bfcffdd5f92f664c92a98699ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Tue, 20 Oct 2026 10:51:38 +0200 Subject: [PATCH 1/4] Add the Room History API reference --- docusaurus.config.ts | 24 + static/api/room-history-openapi.json | 692 +++++++++++++++++++++++++++ 2 files changed, 716 insertions(+) create mode 100644 static/api/room-history-openapi.json diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 356746b6..0207c3f7 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -284,6 +284,7 @@ const config: Config = { items: [ { to: "/api/rest", label: "Server API" }, { to: "/api/compositions", label: "Composition API" }, + { to: "/api/room-history", label: "Room History API" }, ], }, { @@ -410,6 +411,11 @@ const config: Config = { slug: "composition", url: "/docs/api/composition-openapi.json", }, + { + title: "Room History API", + slug: "room-history", + url: "/docs/api/room-history-openapi.json", + }, ], hideSearch: true, persistAuth: true, @@ -438,6 +444,24 @@ const config: Config = { }, } as ScalarOptions, ], + [ + "@scalar/docusaurus", + { + id: "room-history-api", + label: "Room History API", + route: "/api/room-history", + showNavLink: false, + configuration: { + url: "/docs/api/room-history-openapi.json", + hideSearch: true, + persistAuth: true, + defaultOpenFirstTag: false, + authentication: { + preferredSecurityScheme: "ManagementTokenAuth", + }, + }, + } as ScalarOptions, + ], [ "docusaurus-plugin-typedoc", { diff --git a/static/api/room-history-openapi.json b/static/api/room-history-openapi.json new file mode 100644 index 00000000..c11878fc --- /dev/null +++ b/static/api/room-history-openapi.json @@ -0,0 +1,692 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Fishjam Room History API", + "version": "1.0.0", + "description": "Call quality history of the ended rooms of your Fishjam apps: which rooms had issues, which peers caused them, and each peer's bitrate, packet loss and round trip time over time. Authenticate with your management token. Data is kept for 60 days." + }, + "paths": { + "/room-history": { + "get": { + "operationId": "list_room_history", + "summary": "List rooms with their issue counts", + "parameters": [ + { + "in": "query", + "name": "start", + "schema": { + "format": "date-time", + "title": "Start", + "type": "string" + }, + "required": true + }, + { + "in": "query", + "name": "end", + "schema": { + "format": "date-time", + "title": "End", + "type": "string" + }, + "required": true + }, + { + "in": "query", + "name": "limit", + "schema": { + "default": 20, + "maximum": 1000, + "minimum": 1, + "title": "Limit", + "type": "integer" + }, + "required": false + }, + { + "in": "query", + "name": "offset", + "schema": { + "default": 0, + "maximum": 4294967295, + "minimum": 0, + "title": "Offset", + "type": "integer" + }, + "required": false + } + ], + "responses": { + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoomsPageOut" + } + } + } + } + }, + "description": "Rooms that were both created and ended inside the window, newest first.", + "tags": ["Rooms"], + "security": [ + { + "ManagementTokenAuth": [] + } + ] + } + }, + "/room-history/{room_id}": { + "get": { + "operationId": "get_room_history", + "summary": "Get a room with its peers and their issues", + "parameters": [ + { + "in": "path", + "name": "room_id", + "schema": { + "title": "Room Id", + "type": "string" + }, + "required": true + } + ], + "responses": { + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoomDetailOut" + } + } + } + } + }, + "description": "An ended room with its peers and their issues.", + "tags": ["Rooms"], + "security": [ + { + "ManagementTokenAuth": [] + } + ] + } + }, + "/room-history/{room_id}/peers/{peer_id}": { + "get": { + "operationId": "get_room_history_peer", + "summary": "Get a peer's bitrate, packet loss and RTT over time", + "parameters": [ + { + "in": "path", + "name": "room_id", + "schema": { + "title": "Room Id", + "type": "string" + }, + "required": true + }, + { + "in": "path", + "name": "peer_id", + "schema": { + "title": "Peer Id", + "type": "string" + }, + "required": true + } + ], + "responses": { + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PeerDetailOut" + } + } + } + } + }, + "description": "A peer of an ended room with its bitrate, packet loss and RTT over time.", + "tags": ["Rooms"], + "security": [ + { + "ManagementTokenAuth": [] + } + ] + } + } + }, + "components": { + "schemas": { + "PublicRoomsQueryIn": { + "properties": { + "start": { + "format": "date-time", + "title": "Start", + "type": "string" + }, + "end": { + "format": "date-time", + "title": "End", + "type": "string" + }, + "limit": { + "default": 20, + "maximum": 1000, + "minimum": 1, + "title": "Limit", + "type": "integer" + }, + "offset": { + "default": 0, + "maximum": 4294967295, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + }, + "required": ["start", "end"], + "title": "PublicRoomsQueryIn", + "type": "object" + }, + "Message": { + "properties": { + "detail": { + "title": "Detail", + "type": "string" + } + }, + "required": ["detail"], + "title": "Message", + "type": "object" + }, + "RoomOut": { + "properties": { + "room_id": { + "title": "Room Id", + "type": "string" + }, + "created_at": { + "format": "date-time", + "title": "Created At", + "type": "string" + }, + "ended_at": { + "format": "date-time", + "title": "Ended At", + "type": "string" + }, + "end_reason": { + "enum": ["deleted", "purged", "crashed"], + "title": "End Reason", + "type": "string" + }, + "peer_count": { + "title": "Peer Count", + "type": "integer" + }, + "issues_count": { + "title": "Issues Count", + "type": "integer" + } + }, + "required": [ + "room_id", + "created_at", + "ended_at", + "end_reason", + "peer_count", + "issues_count" + ], + "title": "RoomOut", + "type": "object" + }, + "RoomsPageOut": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/RoomOut" + }, + "title": "Items", + "type": "array" + }, + "count": { + "title": "Count", + "type": "integer" + } + }, + "required": ["items", "count"], + "title": "RoomsPageOut", + "type": "object" + }, + "IssueOut": { + "properties": { + "code": { + "enum": ["high_packet_loss", "high_rtt"], + "title": "Code", + "type": "string" + } + }, + "required": ["code"], + "title": "IssueOut", + "type": "object" + }, + "MetadataEntryOut": { + "properties": { + "key": { + "title": "Key", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": ["key", "value"], + "title": "MetadataEntryOut", + "type": "object" + }, + "PeerOut": { + "properties": { + "peer_id": { + "title": "Peer Id", + "type": "string" + }, + "sdk": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sdk" + }, + "joined_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Joined At" + }, + "left_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Left At" + }, + "end_reason": { + "anyOf": [ + { + "enum": ["left", "removed", "finished", "crashed"], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "End Reason" + }, + "metadata": { + "items": { + "$ref": "#/components/schemas/MetadataEntryOut" + }, + "title": "Metadata", + "type": "array" + }, + "issues": { + "items": { + "$ref": "#/components/schemas/IssueOut" + }, + "title": "Issues", + "type": "array" + } + }, + "required": [ + "peer_id", + "sdk", + "joined_at", + "left_at", + "end_reason", + "metadata", + "issues" + ], + "title": "PeerOut", + "type": "object" + }, + "RoomDetailOut": { + "properties": { + "room": { + "$ref": "#/components/schemas/RoomOut" + }, + "peers": { + "items": { + "$ref": "#/components/schemas/PeerOut" + }, + "title": "Peers", + "type": "array" + }, + "thresholds": { + "$ref": "#/components/schemas/ThresholdsOut" + } + }, + "required": ["room", "peers", "thresholds"], + "title": "RoomDetailOut", + "type": "object" + }, + "ThresholdsOut": { + "properties": { + "packet_loss_pct": { + "title": "Packet Loss Pct", + "type": "number" + }, + "rtt_ms": { + "title": "Rtt Ms", + "type": "number" + } + }, + "required": ["packet_loss_pct", "rtt_ms"], + "title": "ThresholdsOut", + "type": "object" + }, + "PeerDetailOut": { + "properties": { + "peer": { + "$ref": "#/components/schemas/PeerOut" + }, + "thresholds": { + "$ref": "#/components/schemas/ThresholdsOut" + }, + "series": { + "items": { + "$ref": "#/components/schemas/PeerSeriesPointOut" + }, + "title": "Series", + "type": "array" + }, + "rtt": { + "items": { + "$ref": "#/components/schemas/PeerRttPointOut" + }, + "title": "Rtt", + "type": "array" + } + }, + "required": ["peer", "thresholds", "series", "rtt"], + "title": "PeerDetailOut", + "type": "object" + }, + "PeerRttPointOut": { + "properties": { + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string" + }, + "rtt_ms": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Rtt Ms" + } + }, + "required": ["timestamp", "rtt_ms"], + "title": "PeerRttPointOut", + "type": "object" + }, + "PeerSeriesPointOut": { + "properties": { + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string" + }, + "direction": { + "enum": ["inbound", "outbound"], + "title": "Direction", + "type": "string" + }, + "kind": { + "title": "Kind", + "type": "string" + }, + "bitrate_bps": { + "title": "Bitrate Bps", + "type": "number" + }, + "packet_loss_pct": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Packet Loss Pct" + } + }, + "required": [ + "timestamp", + "direction", + "kind", + "bitrate_bps", + "packet_loss_pct" + ], + "title": "PeerSeriesPointOut", + "type": "object" + } + }, + "securitySchemes": { + "ManagementTokenAuth": { + "type": "http", + "scheme": "bearer" + } + } + }, + "servers": [ + { + "url": "https://fishjam.io/api/v1/public" + } + ] +} From 4bda8a85b8e440aba7cf9f9cf1c4182c9e70ed3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Tue, 20 Oct 2026 12:38:12 +0200 Subject: [PATCH 2/4] Copy the Room History API spec from the cloud submodule --- .gitmodules | 3 +++ api/cloud | 1 + scripts/update_api.sh | 8 ++++++++ static/api/room-history-openapi.json | 2 +- 4 files changed, 13 insertions(+), 1 deletion(-) create mode 160000 api/cloud diff --git a/.gitmodules b/.gitmodules index d1480a8a..b1cfbc0d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -25,3 +25,6 @@ [submodule "packages/react-native-worklets"] path = packages/react-native-worklets url = https://github.com/fishjam-cloud/fishjam-react-native-worklets.git +[submodule "api/cloud"] + path = api/cloud + url = git@github.com:fishjam-cloud/cloud.git diff --git a/api/cloud b/api/cloud new file mode 160000 index 00000000..1f5ab02b --- /dev/null +++ b/api/cloud @@ -0,0 +1 @@ +Subproject commit 1f5ab02b8dd696fc555af18b72c7557253ecfaaa diff --git a/scripts/update_api.sh b/scripts/update_api.sh index f29ea75e..3afebd26 100755 --- a/scripts/update_api.sh +++ b/scripts/update_api.sh @@ -55,6 +55,11 @@ copy_composition_openapi() { echo "Copied openapi.json of composition to the assets directory." } +copy_room_history_openapi() { + cp backend/openapi/diagnostics.json "$ASSETS_DIRECTORY/room-history-openapi.json" + echo "Copied diagnostics.json of cloud to the assets directory." +} + PROTO_FILES="server_notifications agent_notifications notifications/shared" copy_protos() { for file in $PROTO_FILES; do @@ -74,4 +79,7 @@ copy_protos checkout_submodule_branch composition $COMPOSITION_BRANCH copy_composition_openapi +checkout_submodule cloud +copy_room_history_openapi + echo $'\nSubmodule update and copy complete.' diff --git a/static/api/room-history-openapi.json b/static/api/room-history-openapi.json index c11878fc..d781b1e1 100644 --- a/static/api/room-history-openapi.json +++ b/static/api/room-history-openapi.json @@ -3,7 +3,7 @@ "info": { "title": "Fishjam Room History API", "version": "1.0.0", - "description": "Call quality history of the ended rooms of your Fishjam apps: which rooms had issues, which peers caused them, and each peer's bitrate, packet loss and round trip time over time. Authenticate with your management token. Data is kept for 60 days." + "description": "Browse your Fishjam rooms that ended in the last 60 days. For each room, see which peers had issues such as high round trip time or packet loss, and how their bitrate changed over time. Authenticate with your management token." }, "paths": { "/room-history": { From 7eafe89e49c33a66f7a1a60f0e979b42a785cbe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Tue, 20 Oct 2026 20:00:24 +0200 Subject: [PATCH 3/4] Update the Room History API spec --- static/api/room-history-openapi.json | 39 +++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/static/api/room-history-openapi.json b/static/api/room-history-openapi.json index d781b1e1..008d71e0 100644 --- a/static/api/room-history-openapi.json +++ b/static/api/room-history-openapi.json @@ -402,7 +402,12 @@ "end_reason": { "enum": ["deleted", "purged", "crashed"], "title": "End Reason", - "type": "string" + "type": "string", + "x-enumDescriptions": { + "crashed": "Room crashed on our side.", + "deleted": "Room closed normally.", + "purged": "Room closed after everyone left." + } }, "peer_count": { "title": "Peer Count", @@ -496,6 +501,7 @@ "type": "null" } ], + "description": "When the peer joined. `null` if the peer never connected.", "title": "Joined At" }, "left_at": { @@ -514,12 +520,19 @@ "anyOf": [ { "enum": ["left", "removed", "finished", "crashed"], - "type": "string" + "type": "string", + "x-enumDescriptions": { + "crashed": "Connection crashed.", + "finished": "Finished its job, e.g. a VAPI peer whose call ended.", + "left": "Left on their own.", + "removed": "Removed by your app." + } }, { "type": "null" } ], + "description": "Leave reason. `null` if no reason was recorded.", "title": "End Reason" }, "metadata": { @@ -572,10 +585,12 @@ "ThresholdsOut": { "properties": { "packet_loss_pct": { + "description": "Packet loss limit, in %. At or above it a peer has the `high_packet_loss` issue.", "title": "Packet Loss Pct", "type": "number" }, "rtt_ms": { + "description": "Round trip time limit, in ms. At or above it a peer has the `high_rtt` issue.", "title": "Rtt Ms", "type": "number" } @@ -593,6 +608,7 @@ "$ref": "#/components/schemas/ThresholdsOut" }, "series": { + "description": "Bitrate and packet loss per report, track kind and direction.", "items": { "$ref": "#/components/schemas/PeerSeriesPointOut" }, @@ -627,6 +643,7 @@ "type": "null" } ], + "description": "Round trip time, in ms. `null` if the peer's app never reported back.", "title": "Rtt Ms" } }, @@ -644,13 +661,28 @@ "direction": { "enum": ["inbound", "outbound"], "title": "Direction", - "type": "string" + "type": "string", + "x-enumDescriptions": { + "inbound": "Sent. Media the peer sent to Fishjam.", + "outbound": "Received. Media the peer received from Fishjam." + } }, "kind": { + "enum": [ + "microphone", + "camera", + "screenShareVideo", + "screenShareAudio", + "customVideo", + "customAudio", + "audio", + "video" + ], "title": "Kind", "type": "string" }, "bitrate_bps": { + "description": "Bitrate, in bits per second, since the previous report.", "title": "Bitrate Bps", "type": "number" }, @@ -663,6 +695,7 @@ "type": "null" } ], + "description": "Packet loss, in %. `null` if no packets were expected.", "title": "Packet Loss Pct" } }, From 470729fc50698439baba6af5e470591ea2426c6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Thu, 29 Oct 2026 14:24:05 +0100 Subject: [PATCH 4/4] Mention the Room History API in the docs Claude-Session: https://claude.ai/code/session_01TQjUhvtaFuvhAfc2xTw5U2 --- docs/api/reference.md | 12 +++++++++++- docs/how-to/backend/production-deployment.mdx | 4 ++++ docs/how-to/client/debug-logging.mdx | 4 ++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/api/reference.md b/docs/api/reference.md index 4cdcf079..2f6053b0 100644 --- a/docs/api/reference.md +++ b/docs/api/reference.md @@ -6,7 +6,7 @@ type: reference Describes APIs for direct interaction with Fishjam. -Fishjam publishes documentation for the Sandbox API, Fishjam Server APIs, and the Composition API. +Fishjam publishes documentation for the Sandbox API, Fishjam Server APIs, the Composition API, and the Room History API. ## Sandbox API @@ -63,3 +63,13 @@ Next, you can should subscribe to notifications by sending `SubscribeRequest` ev Calls authenticate with your Management Token, from the [**Fishjam developer panel**](https://fishjam.io/app). Request bodies are snake_case and closed: a field the endpoint does not define is rejected with `422` rather than ignored, so a misspelling fails loudly instead of silently doing nothing. + +## Room History + +The Room History API lets you browse your rooms that ended in the last 60 days. For each room, you can see which peers had issues, such as high round trip time or packet loss, and how their bitrate, packet loss, and round trip time changed over time. All requests go to `https://fishjam.io/api/v1/public`. + +### REST API + +[Room History REST API Reference](/api/room-history) + +Calls authenticate with your Management Token, from the [**Fishjam developer panel**](https://fishjam.io/app). diff --git a/docs/how-to/backend/production-deployment.mdx b/docs/how-to/backend/production-deployment.mdx index 5d8f5ca7..04926031 100644 --- a/docs/how-to/backend/production-deployment.mdx +++ b/docs/how-to/backend/production-deployment.mdx @@ -411,6 +411,10 @@ For scaling considerations: - [Understanding Fishjam Architecture](../../explanation/architecture) - [Security best practices](../../explanation/security-tokens) +For investigating call quality after a room ends: + +- [Room History API](../../api/reference#room-history) + For specific backend frameworks: - [FastAPI example](../../how-to/backend/fastapi-example) diff --git a/docs/how-to/client/debug-logging.mdx b/docs/how-to/client/debug-logging.mdx index 67b9667e..fe424ff3 100644 --- a/docs/how-to/client/debug-logging.mdx +++ b/docs/how-to/client/debug-logging.mdx @@ -71,3 +71,7 @@ When enabled, you may see logs similar to: [FISHJAM] Couldn't get camera permission: NotAllowedError ... [FISHJAM] ICE connection: disconnected ``` + +## See also + +Debug logs show what happens on one client while it runs. To check the network quality of every peer after a room ends, use the [Room History API](../../api/reference#room-history).