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/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). 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/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 new file mode 100644 index 00000000..008d71e0 --- /dev/null +++ b/static/api/room-history-openapi.json @@ -0,0 +1,725 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Fishjam Room History API", + "version": "1.0.0", + "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": { + "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", + "x-enumDescriptions": { + "crashed": "Room crashed on our side.", + "deleted": "Room closed normally.", + "purged": "Room closed after everyone left." + } + }, + "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" + } + ], + "description": "When the peer joined. `null` if the peer never connected.", + "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", + "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": { + "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": { + "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" + } + }, + "required": ["packet_loss_pct", "rtt_ms"], + "title": "ThresholdsOut", + "type": "object" + }, + "PeerDetailOut": { + "properties": { + "peer": { + "$ref": "#/components/schemas/PeerOut" + }, + "thresholds": { + "$ref": "#/components/schemas/ThresholdsOut" + }, + "series": { + "description": "Bitrate and packet loss per report, track kind and direction.", + "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" + } + ], + "description": "Round trip time, in ms. `null` if the peer's app never reported back.", + "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", + "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" + }, + "packet_loss_pct": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "description": "Packet loss, in %. `null` if no packets were expected.", + "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" + } + ] +}