From 7644e0fe48eb0a2dbba50d6db71d4c41a8560d50 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 3 Mar 2026 18:48:37 +0530 Subject: [PATCH 01/11] Add realtime metrics for connections, messages, and bandwidth in project usage --- app/controllers/api/project.php | 34 ++++ app/init/constants.php | 6 + app/realtime.php | 107 ++++++++++- .../Utopia/Response/Model/UsageProject.php | 39 ++++ tests/e2e/General/UsageTest.php | 172 ++++++++++++++++++ 5 files changed, 355 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/project.php b/app/controllers/api/project.php index d24519e3fb..1fd33a5db0 100644 --- a/app/controllers/api/project.php +++ b/app/controllers/api/project.php @@ -72,6 +72,10 @@ Http::get('/v1/project/usage') METRIC_DATABASES_OPERATIONS_READS, METRIC_DATABASES_OPERATIONS_WRITES, METRIC_FILES_IMAGES_TRANSFORMED, + METRIC_REALTIME_CONNECTIONS, + METRIC_REALTIME_CONNECTIONS_MESSAGES_SENT, + METRIC_REALTIME_INBOUND, + METRIC_REALTIME_OUTBOUND, ], 'period' => [ METRIC_NETWORK_REQUESTS, @@ -85,6 +89,10 @@ Http::get('/v1/project/usage') METRIC_DATABASES_OPERATIONS_READS, METRIC_DATABASES_OPERATIONS_WRITES, METRIC_FILES_IMAGES_TRANSFORMED, + METRIC_REALTIME_CONNECTIONS, + METRIC_REALTIME_CONNECTIONS_MESSAGES_SENT, + METRIC_REALTIME_INBOUND, + METRIC_REALTIME_OUTBOUND, ] ]; @@ -347,6 +355,26 @@ Http::get('/v1/project/usage') ]; } + // Realtime bandwidth = realtime.inbound + realtime.outbound (per bucket) + $realtimeProjectBandwidth = []; + foreach ($usage[METRIC_REALTIME_INBOUND] as $item) { + $realtimeProjectBandwidth[$item['date']] ??= 0; + $realtimeProjectBandwidth[$item['date']] += $item['value']; + } + + foreach ($usage[METRIC_REALTIME_OUTBOUND] as $item) { + $realtimeProjectBandwidth[$item['date']] ??= 0; + $realtimeProjectBandwidth[$item['date']] += $item['value']; + } + + $realtimeBandwidth = []; + foreach ($realtimeProjectBandwidth as $date => $value) { + $realtimeBandwidth[] = [ + 'date' => $date, + 'value' => $value + ]; + } + $response->dynamic(new Document([ 'requests' => ($usage[METRIC_NETWORK_REQUESTS]), 'network' => $network, @@ -367,10 +395,16 @@ Http::get('/v1/project/usage') 'deploymentsStorageTotal' => $total[METRIC_DEPLOYMENTS_STORAGE], 'databasesReadsTotal' => $total[METRIC_DATABASES_OPERATIONS_READS], 'databasesWritesTotal' => $total[METRIC_DATABASES_OPERATIONS_WRITES], + 'realtimeConnectionsTotal' => $total[METRIC_REALTIME_CONNECTIONS], + 'realtimeMessagesTotal' => $total[METRIC_REALTIME_CONNECTIONS_MESSAGES_SENT], + 'realtimeBandwidthTotal' => ($total[METRIC_REALTIME_INBOUND] ?? 0) + ($total[METRIC_REALTIME_OUTBOUND] ?? 0), 'executionsBreakdown' => $executionsBreakdown, 'bucketsBreakdown' => $bucketsBreakdown, 'databasesReads' => $usage[METRIC_DATABASES_OPERATIONS_READS], 'databasesWrites' => $usage[METRIC_DATABASES_OPERATIONS_WRITES], + 'realtimeConnections' => $usage[METRIC_REALTIME_CONNECTIONS], + 'realtimeMessages' => $usage[METRIC_REALTIME_CONNECTIONS_MESSAGES_SENT], + 'realtimeBandwidth' => $realtimeBandwidth, 'databasesStorageBreakdown' => $databasesStorageBreakdown, 'executionsMbSecondsBreakdown' => $executionsMbSecondsBreakdown, 'buildsMbSecondsBreakdown' => $buildsMbSecondsBreakdown, diff --git a/app/init/constants.php b/app/init/constants.php index c6a5bc853e..cd77b05746 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -361,6 +361,12 @@ const METRIC_AVATARS_SCREENSHOTS_GENERATED = 'avatars.screenshotsGenerated'; const METRIC_FUNCTIONS_RUNTIME = 'functions.runtimes.{runtime}'; const METRIC_SITES_FRAMEWORK = 'sites.frameworks.{framework}'; +// Realtime metrics +const METRIC_REALTIME_CONNECTIONS = 'realtime.connections'; +const METRIC_REALTIME_CONNECTIONS_MESSAGES_SENT = 'realtime.messages.sent'; +const METRIC_REALTIME_INBOUND = 'realtime.inbound'; +const METRIC_REALTIME_OUTBOUND = 'realtime.outbound'; + // Resource types const RESOURCE_TYPE_PROJECTS = 'projects'; const RESOURCE_TYPE_FUNCTIONS = 'functions'; diff --git a/app/realtime.php b/app/realtime.php index 7ec24d03c8..8a066bcbab 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -1,5 +1,6 @@ getSequence()])) { + return $ctx['queueForStatsUsage'][$project->getSequence()]; + } + + global $register; + + /** @var Group $pools */ + $pools = $register->get('pools'); + + $queue = new StatsUsage(new BrokerPool(publisher: $pools->get('publisher'))); + $queue->setProject($project); + + return $ctx['queueForStatsUsage'][$project->getSequence()] = $queue; + } +} + $realtime = getRealtime(); /** @@ -545,20 +572,59 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, } $total = 0; + $outboundBytes = 0; + foreach ($groups as $group) { $data = $event['data']; $data['subscriptions'] = $group['subscriptions']; - $server->send($group['ids'], json_encode([ + $payloadJson = json_encode([ 'type' => 'event', 'data' => $data - ])); - $total += count($group['ids']); + ]); + + $server->send($group['ids'], $payloadJson); + + $count = count($group['ids']); + $total += $count; + $outboundBytes += strlen($payloadJson) * $count; } if ($total > 0) { $register->get('telemetry.messageSentCounter')->add($total); $stats->incr($event['project'], 'messages', $total); + + $projectId = $event['project'] ?? null; + + if (!empty($projectId)) { + try { + $consoleDB = getConsoleDB(); + /** @var Document $project */ + $project = $consoleDB->getAuthorization()->skip( + fn () => $consoleDB->getDocument('projects', $projectId) + ); + + if (!$project->isEmpty()) { + $queueForStatsUsage = getQueueForStatsUsageForProject($project); + + $queueForStatsUsage->addMetric( + METRIC_REALTIME_CONNECTIONS_MESSAGES_SENT, + $total + ); + + if ($outboundBytes > 0) { + $queueForStatsUsage->addMetric( + METRIC_REALTIME_OUTBOUND, + $outboundBytes + ); + } + + $queueForStatsUsage->trigger(); + } + } catch (Throwable $th) { + logError($th, 'realtimeUsageOutbound', tags: ['projectId' => $projectId]); + } + } } }); } catch (Throwable $th) { @@ -707,6 +773,16 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, ]); $stats->incr($project->getId(), 'connections'); $stats->incr($project->getId(), 'connectionsTotal'); + + try { + $queueForStatsUsage = getQueueForStatsUsageForProject($project); + $queueForStatsUsage + ->addMetric(METRIC_REALTIME_CONNECTIONS, 1) + ->trigger(); + } catch (\Throwable $th) { + logError($th, 'realtimeUsageConnections', project: $project); + } + } catch (Throwable $th) { logError($th, 'realtime', project: $project, user: $logUser, authorization: $authorization); @@ -748,6 +824,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re $authorization = null; try { + $rawSize = \strlen($message); $response = new Response(new SwooleResponse()); $projectId = $realtime->connections[$connection]['projectId'] ?? null; @@ -786,6 +863,18 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re throw new Exception(Exception::REALTIME_TOO_MANY_MESSAGES, 'Too many messages.'); } + // Record realtime inbound bytes for this project + if ($project !== null && !$project->isEmpty()) { + try { + $queueForStatsUsage = getQueueForStatsUsageForProject($project); + $queueForStatsUsage + ->addMetric(METRIC_REALTIME_INBOUND, $rawSize) + ->trigger(); + } catch (Throwable $th) { + logError($th, 'realtimeUsageInbound', project: $project); + } + } + $message = json_decode($message, true); if (is_null($message) || (!array_key_exists('type', $message) && !array_key_exists('data', $message))) { @@ -905,6 +994,18 @@ $server->onClose(function (int $connection) use ($realtime, $stats, $register) { if (array_key_exists($connection, $realtime->connections)) { $stats->decr($realtime->connections[$connection]['projectId'], 'connectionsTotal'); $register->get('telemetry.connectionCounter')->add(-1); + + $projectId = $realtime->connections[$connection]['projectId']; + + $consoleDB = getConsoleDB(); + $project = $consoleDB->getAuthorization()->skip( + fn () => $consoleDB->getDocument('projects', $projectId) + ); + + if (!$project->isEmpty()) { + $queue = getQueueForStatsUsageForProject($project); + $queue->addMetric(METRIC_REALTIME_CONNECTIONS, -1)->trigger(); + } } $realtime->unsubscribe($connection); diff --git a/src/Appwrite/Utopia/Response/Model/UsageProject.php b/src/Appwrite/Utopia/Response/Model/UsageProject.php index ee644aa845..c219a35f29 100644 --- a/src/Appwrite/Utopia/Response/Model/UsageProject.php +++ b/src/Appwrite/Utopia/Response/Model/UsageProject.php @@ -100,6 +100,24 @@ class UsageProject extends Model 'default' => 0, 'example' => 0, ]) + ->addRule('realtimeConnectionsTotal', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Current aggregated number of open Realtime connections.', + 'default' => 0, + 'example' => 0, + ]) + ->addRule('realtimeMessagesTotal', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Total number of Realtime messages sent to clients.', + 'default' => 0, + 'example' => 0, + ]) + ->addRule('realtimeBandwidthTotal', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Total consumed Realtime bandwidth (in bytes).', + 'default' => 0, + 'example' => 0, + ]) ->addRule('requests', [ 'type' => Response::MODEL_METRIC, 'description' => 'Aggregated number of requests per period.', @@ -114,6 +132,27 @@ class UsageProject extends Model 'example' => [], 'array' => true ]) + ->addRule('realtimeConnections', [ + 'type' => Response::MODEL_METRIC, + 'description' => 'Aggregated number of open Realtime connections per period.', + 'default' => [], + 'example' => [], + 'array' => true + ]) + ->addRule('realtimeMessages', [ + 'type' => Response::MODEL_METRIC, + 'description' => 'Aggregated number of Realtime messages sent to clients per period.', + 'default' => [], + 'example' => [], + 'array' => true + ]) + ->addRule('realtimeBandwidth', [ + 'type' => Response::MODEL_METRIC, + 'description' => 'Aggregated consumed Realtime bandwidth (in bytes) per period.', + 'default' => [], + 'example' => [], + 'array' => true + ]) ->addRule('users', [ 'type' => Response::MODEL_METRIC, 'description' => 'Aggregated number of users per period.', diff --git a/tests/e2e/General/UsageTest.php b/tests/e2e/General/UsageTest.php index e7b5b1d844..29229ae2bd 100644 --- a/tests/e2e/General/UsageTest.php +++ b/tests/e2e/General/UsageTest.php @@ -11,6 +11,7 @@ use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideServer; use Tests\E2E\Services\Functions\FunctionsBase; +use Tests\E2E\Services\Realtime\RealtimeBase; use Tests\E2E\Services\Sites\SitesBase; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; @@ -23,6 +24,7 @@ class UsageTest extends Scope use ProjectCustom; use SideServer; use FunctionsBase; + use RealtimeBase; use SitesBase { FunctionsBase::createDeployment insteadof SitesBase; FunctionsBase::setupDeployment insteadof SitesBase; @@ -1408,6 +1410,176 @@ class UsageTest extends Scope }); } + public function testRealtimeUsageMetrics(): void + { + $user = $this->getUser(); + $session = $user['session'] ?? ''; + $projectId = $this->getProject()['$id']; + + // Baseline realtime usage before opening a new connection + $baseline = $this->client->call( + Client::METHOD_GET, + '/project/usage', + $this->getConsoleHeaders(), + [ + 'period' => '1h', + 'startDate' => self::getToday(), + 'endDate' => self::getTomorrow(), + ] + ); + + $connectionsBefore = $baseline['body']['realtimeConnectionsTotal'] ?? 0; + $messagesBefore = $baseline['body']['realtimeMessagesTotal'] ?? 0; + + $connectionCount = 3; + $clients = []; + + for ($i = 0; $i < $connectionCount; $i++) { + $client = $this->getWebsocket(['documents'], [ + 'origin' => 'http://localhost', + 'cookie' => 'a_session_' . $projectId . '=' . $session, + ], null); + + $connected = json_decode($client->receive(), true); + $this->assertEquals('connected', $connected['type']); + + $clients[] = $client; + } + + try { + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Realtime Usage DB', + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'collectionId' => ID::unique(), + 'name' => 'Realtime Usage Collection', + 'permissions' => [ + Permission::create(Role::user($user['$id'])), + ], + 'documentSecurity' => true, + ]); + + $collectionId = $collection['body']['$id']; + + $attribute = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->assertEquals(202, $attribute['headers']['status-code']); + + $this->assertEventually(function () use ($databaseId, $collectionId, $projectId) { + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/name', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + $this->assertEquals('available', $response['body']['status']); + }, 30000, 250); + + $document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ + 'name' => 'Realtime Usage Doc', + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $this->assertEquals(201, $document['headers']['status-code']); + + $event = json_decode($clients[0]->receive(), true); + $this->assertEquals('event', $event['type']); + + // After creating a document we expect all connections to receive an event + $this->assertEventually(function () use ($connectionsBefore, $messagesBefore, $connectionCount) { + $response = $this->client->call( + Client::METHOD_GET, + '/project/usage', + $this->getConsoleHeaders(), + [ + 'period' => '1h', + 'startDate' => self::getToday(), + 'endDate' => self::getTomorrow(), + ] + ); + + $this->assertEquals(200, $response['headers']['status-code']); + + $this->assertArrayHasKey('realtimeConnectionsTotal', $response['body']); + $this->assertArrayHasKey('realtimeMessagesTotal', $response['body']); + $this->assertArrayHasKey('realtimeBandwidthTotal', $response['body']); + $this->assertArrayHasKey('realtimeConnections', $response['body']); + $this->assertArrayHasKey('realtimeMessages', $response['body']); + $this->assertArrayHasKey('realtimeBandwidth', $response['body']); + + // We expect exactly $connectionCount additional open connections and $connectionCount additional message deliveries + $this->assertEquals($connectionsBefore + $connectionCount, $response['body']['realtimeConnectionsTotal']); + $this->assertEquals($messagesBefore + $connectionCount, $response['body']['realtimeMessagesTotal']); + $this->assertGreaterThan(0, $response['body']['realtimeBandwidthTotal']); + + $this->validateDates($response['body']['realtimeConnections']); + $this->validateDates($response['body']['realtimeMessages']); + $this->validateDates($response['body']['realtimeBandwidth']); + }, 60000, 2000); + + // Now close a single connection and ensure the counters reflect it + $clients[0]->close(); + + $this->assertEventually(function () use ($connectionsBefore, $messagesBefore, $connectionCount) { + $response = $this->client->call( + Client::METHOD_GET, + '/project/usage', + $this->getConsoleHeaders(), + [ + 'period' => '1h', + 'startDate' => self::getToday(), + 'endDate' => self::getTomorrow(), + ] + ); + + $this->assertEquals(200, $response['headers']['status-code']); + + $this->assertArrayHasKey('realtimeConnectionsTotal', $response['body']); + $this->assertArrayHasKey('realtimeMessagesTotal', $response['body']); + $this->assertArrayHasKey('realtimeBandwidthTotal', $response['body']); + + // One of the connections is closed, so we expect one less open connection. + // Messages and bandwidth are cumulative and should not decrease. + $this->assertEquals($connectionsBefore + $connectionCount - 1, $response['body']['realtimeConnectionsTotal']); + $this->assertEquals($messagesBefore + $connectionCount, $response['body']['realtimeMessagesTotal']); + $this->assertGreaterThan(0, $response['body']['realtimeBandwidthTotal']); + }, 60000, 2000); + } finally { + foreach ($clients as $client) { + $client->close(); + } + } + } + public function tearDown(): void { $this->projectId = ''; From 82db411517641559f9ebbf52d2f3a809f74e8830 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 3 Mar 2026 19:36:49 +0530 Subject: [PATCH 02/11] updated --- app/realtime.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/realtime.php b/app/realtime.php index 8a066bcbab..33833d27ea 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -226,7 +226,7 @@ if (!function_exists('getTelemetry')) { } } -if (!function_exists('queueForStatsUsage')) { +if (!function_exists('getQueueForStatsUsageForProject')) { function getQueueForStatsUsageForProject(Document $project): StatsUsage { $ctx = Coroutine::getContext(); From 9fdd7c1c6e55181eea155a820352e870b5fe0e8d Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 5 Mar 2026 11:30:15 +0530 Subject: [PATCH 03/11] added try catch connection close metric --- app/realtime.php | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 33833d27ea..76a84e1d9d 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -997,14 +997,18 @@ $server->onClose(function (int $connection) use ($realtime, $stats, $register) { $projectId = $realtime->connections[$connection]['projectId']; - $consoleDB = getConsoleDB(); - $project = $consoleDB->getAuthorization()->skip( - fn () => $consoleDB->getDocument('projects', $projectId) - ); + try { + $consoleDB = getConsoleDB(); + $project = $consoleDB->getAuthorization()->skip( + fn () => $consoleDB->getDocument('projects', $projectId) + ); - if (!$project->isEmpty()) { - $queue = getQueueForStatsUsageForProject($project); - $queue->addMetric(METRIC_REALTIME_CONNECTIONS, -1)->trigger(); + if (!$project->isEmpty()) { + $queue = getQueueForStatsUsageForProject($project); + $queue->addMetric(METRIC_REALTIME_CONNECTIONS, -1)->trigger(); + } + } catch (Throwable $th) { + logError($th, 'realtimeUsageConnectionClose', tags: ['projectId' => $projectId]); } } $realtime->unsubscribe($connection); From b5c2cc971610df03dabe1045624eb20b2a2d2953 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 5 Mar 2026 18:10:09 +0530 Subject: [PATCH 04/11] Add triggerStats function for realtime usage metrics tracking --- app/realtime.php | 160 +++++++++++++++++++++++++++++------------------ 1 file changed, 100 insertions(+), 60 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 76a84e1d9d..8250a395a9 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -251,6 +251,43 @@ if (!function_exists('getQueueForStatsUsageForProject')) { } } +if (!function_exists('triggerStats')) { + /** + * Trigger realtime usage stats with a generic metric map. + * + * @param array $event Metrics in the form METRIC_CONSTANT => value + */ + function triggerStats(array $event, string $projectId): void + { + if (empty($projectId)) { + return; + } + + try { + $consoleDB = getConsoleDB(); + /** @var Document $project */ + $project = $consoleDB->getAuthorization()->skip( + fn () => $consoleDB->getDocument('projects', $projectId) + ); + + if ($project->isEmpty()) { + return; + } + + $queueForStatsUsage = getQueueForStatsUsageForProject($project); + + foreach ($event as $metric => $value) { + $queueForStatsUsage->addMetric($metric, $value); + } + + $queueForStatsUsage->trigger(); + $queueForStatsUsage->reset(); + } catch (Throwable $th) { + logError($th, 'realtimeStats', tags: ['projectId' => $projectId]); + } + } +} + $realtime = getRealtime(); /** @@ -597,33 +634,15 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $projectId = $event['project'] ?? null; if (!empty($projectId)) { - try { - $consoleDB = getConsoleDB(); - /** @var Document $project */ - $project = $consoleDB->getAuthorization()->skip( - fn () => $consoleDB->getDocument('projects', $projectId) - ); + $metrics = [ + METRIC_REALTIME_CONNECTIONS_MESSAGES_SENT => $total, + ]; - if (!$project->isEmpty()) { - $queueForStatsUsage = getQueueForStatsUsageForProject($project); - - $queueForStatsUsage->addMetric( - METRIC_REALTIME_CONNECTIONS_MESSAGES_SENT, - $total - ); - - if ($outboundBytes > 0) { - $queueForStatsUsage->addMetric( - METRIC_REALTIME_OUTBOUND, - $outboundBytes - ); - } - - $queueForStatsUsage->trigger(); - } - } catch (Throwable $th) { - logError($th, 'realtimeUsageOutbound', tags: ['projectId' => $projectId]); + if ($outboundBytes > 0) { + $metrics[METRIC_REALTIME_OUTBOUND] = $outboundBytes; } + + triggerStats($metrics, $projectId); } } }); @@ -701,6 +720,19 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, throw new Exception(Exception::REALTIME_TOO_MANY_MESSAGES, 'Too many requests'); } + // Record realtime inbound bytes for this project (WS handshake + query params) + try { + $rawSize = $request->getSize(); + } catch (Throwable) { + $rawSize = \strlen((string) $request->getURI()); + } + + if ($rawSize > 0) { + triggerStats([ + METRIC_REALTIME_INBOUND => $rawSize, + ], $project->getId()); + } + /* * Validate Client Domain - Check to avoid CSRF attack. * Adding Appwrite API domains to allow XDOMAIN communication. @@ -755,14 +787,16 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $user = empty($user->getId()) ? null : $response->output($user, Response::MODEL_ACCOUNT); - $server->send([$connection], json_encode([ + $connectedPayloadJson = json_encode([ 'type' => 'connected', 'data' => [ 'channels' => $names, 'subscriptions' => $mapping, 'user' => $user ] - ])); + ]); + + $server->send([$connection], $connectedPayloadJson); $register->get('telemetry.connectionCounter')->add(1); $register->get('telemetry.connectionCreatedCounter')->add(1); @@ -774,14 +808,10 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $stats->incr($project->getId(), 'connections'); $stats->incr($project->getId(), 'connectionsTotal'); - try { - $queueForStatsUsage = getQueueForStatsUsageForProject($project); - $queueForStatsUsage - ->addMetric(METRIC_REALTIME_CONNECTIONS, 1) - ->trigger(); - } catch (\Throwable $th) { - logError($th, 'realtimeUsageConnections', project: $project); - } + $connectedOutboundBytes = \strlen($connectedPayloadJson); + + triggerStats([METRIC_REALTIME_CONNECTIONS => 1, METRIC_REALTIME_OUTBOUND => $connectedOutboundBytes], $project->getId()); + } catch (Throwable $th) { logError($th, 'realtime', project: $project, user: $logUser, authorization: $authorization); @@ -865,14 +895,9 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re // Record realtime inbound bytes for this project if ($project !== null && !$project->isEmpty()) { - try { - $queueForStatsUsage = getQueueForStatsUsageForProject($project); - $queueForStatsUsage - ->addMetric(METRIC_REALTIME_INBOUND, $rawSize) - ->trigger(); - } catch (Throwable $th) { - logError($th, 'realtimeUsageInbound', project: $project); - } + triggerStats([ + METRIC_REALTIME_INBOUND => $rawSize, + ], $project->getId()); } $message = json_decode($message, true); @@ -883,9 +908,21 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re switch ($message['type']) { case 'ping': - $server->send([$connection], json_encode([ + $pongPayloadJson = json_encode([ 'type' => 'pong' - ])); + ]); + + $server->send([$connection], $pongPayloadJson); + + if ($project !== null && !$project->isEmpty()) { + $pongOutboundBytes = \strlen($pongPayloadJson); + + if ($pongOutboundBytes > 0) { + triggerStats([ + METRIC_REALTIME_OUTBOUND => $pongOutboundBytes, + ], $project->getId()); + } + } break; case 'authentication': @@ -946,14 +983,27 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re } $user = $response->output($user, Response::MODEL_ACCOUNT); - $server->send([$connection], json_encode([ + + $authResponsePayloadJson = json_encode([ 'type' => 'response', 'data' => [ 'to' => 'authentication', 'success' => true, 'user' => $user ] - ])); + ]); + + $server->send([$connection], $authResponsePayloadJson); + + if ($project !== null && !$project->isEmpty()) { + $authOutboundBytes = \strlen($authResponsePayloadJson); + + if ($authOutboundBytes > 0) { + triggerStats([ + METRIC_REALTIME_OUTBOUND => $authOutboundBytes, + ], $project->getId()); + } + } break; @@ -997,19 +1047,9 @@ $server->onClose(function (int $connection) use ($realtime, $stats, $register) { $projectId = $realtime->connections[$connection]['projectId']; - try { - $consoleDB = getConsoleDB(); - $project = $consoleDB->getAuthorization()->skip( - fn () => $consoleDB->getDocument('projects', $projectId) - ); - - if (!$project->isEmpty()) { - $queue = getQueueForStatsUsageForProject($project); - $queue->addMetric(METRIC_REALTIME_CONNECTIONS, -1)->trigger(); - } - } catch (Throwable $th) { - logError($th, 'realtimeUsageConnectionClose', tags: ['projectId' => $projectId]); - } + triggerStats([ + METRIC_REALTIME_CONNECTIONS => -1, + ], $projectId); } $realtime->unsubscribe($connection); From 03d7bccde2a4465d651a5575b4a3321990ced6fe Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 5 Mar 2026 18:14:16 +0530 Subject: [PATCH 05/11] updated tests --- tests/e2e/General/UsageTest.php | 55 +++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/e2e/General/UsageTest.php b/tests/e2e/General/UsageTest.php index 29229ae2bd..2eb2673c8a 100644 --- a/tests/e2e/General/UsageTest.php +++ b/tests/e2e/General/UsageTest.php @@ -1546,6 +1546,61 @@ class UsageTest extends Scope $this->validateDates($response['body']['realtimeBandwidth']); }, 60000, 2000); + // Capture a snapshot of usage after the broadcasted document event + $afterEventUsage = $this->client->call( + Client::METHOD_GET, + '/project/usage', + $this->getConsoleHeaders(), + [ + 'period' => '1h', + 'startDate' => self::getToday(), + 'endDate' => self::getTomorrow(), + ] + ); + + $this->assertEquals(200, $afterEventUsage['headers']['status-code']); + + $connectionsAfterEvent = $afterEventUsage['body']['realtimeConnectionsTotal'] ?? 0; + $messagesAfterEvent = $afterEventUsage['body']['realtimeMessagesTotal'] ?? 0; + $bandwidthAfterEvent = $afterEventUsage['body']['realtimeBandwidthTotal'] ?? 0; + + // Send a ping over an existing connection to exercise the ping/pong + // realtime usage metrics path (inbound + outbound bytes) without + // generating additional "messages sent" usage. + $clients[1]->send(json_encode([ + 'type' => 'ping', + ])); + + $firstMessage = json_decode($clients[1]->receive(), true); + // Depending on timing, the first frame we see here can be either + // a broadcast "event" (from another operation) or the "pong" + // response. Both are valid, and in either case the ping/pong + // traffic still exercises the realtime usage metrics paths. + $this->assertContains($firstMessage['type'], ['event']); + + // We expect: + // - connections count to remain the same + // - messages count to remain the same (no new broadcast events) + // - bandwidth total to increase because of ping/pong traffic + $this->assertEventually(function () use ($connectionsAfterEvent, $messagesAfterEvent, $bandwidthAfterEvent) { + $response = $this->client->call( + Client::METHOD_GET, + '/project/usage', + $this->getConsoleHeaders(), + [ + 'period' => '1h', + 'startDate' => self::getToday(), + 'endDate' => self::getTomorrow(), + ] + ); + + $this->assertEquals(200, $response['headers']['status-code']); + + $this->assertEquals($connectionsAfterEvent, $response['body']['realtimeConnectionsTotal']); + $this->assertEquals($messagesAfterEvent, $response['body']['realtimeMessagesTotal']); + $this->assertGreaterThan($bandwidthAfterEvent, $response['body']['realtimeBandwidthTotal']); + }, 60000, 2000); + // Now close a single connection and ensure the counters reflect it $clients[0]->close(); From ece5b8173277ff0d41cbbb0b56c798baf0a0f2fe Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 6 Mar 2026 15:27:30 +0530 Subject: [PATCH 06/11] updated test --- tests/e2e/General/UsageTest.php | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/tests/e2e/General/UsageTest.php b/tests/e2e/General/UsageTest.php index 2eb2673c8a..f51b04b9c6 100644 --- a/tests/e2e/General/UsageTest.php +++ b/tests/e2e/General/UsageTest.php @@ -1571,12 +1571,23 @@ class UsageTest extends Scope 'type' => 'ping', ])); - $firstMessage = json_decode($clients[1]->receive(), true); - // Depending on timing, the first frame we see here can be either - // a broadcast "event" (from another operation) or the "pong" - // response. Both are valid, and in either case the ping/pong - // traffic still exercises the realtime usage metrics paths. - $this->assertContains($firstMessage['type'], ['event']); + // A broadcast document "event" can still be queued for this connection. + // Read until we see the pong response (and fail fast on unexpected frames). + $pong = null; + for ($i = 0; $i < 5; $i++) { + $frame = json_decode($clients[1]->receive(), true); + $this->assertIsArray($frame); + $this->assertArrayHasKey('type', $frame); + + if ($frame['type'] === 'pong') { + $pong = $frame; + break; + } + + $this->assertEquals('event', $frame['type']); + } + + $this->assertNotNull($pong, 'Expected to receive a pong frame after ping.'); // We expect: // - connections count to remain the same From 0ea196d21c3a76fcee5d04fc3024fba1d6d6eadd Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 6 Mar 2026 15:37:30 +0530 Subject: [PATCH 07/11] updated inbound raw size to the request size --- app/realtime.php | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 8250a395a9..9ab6601ed7 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -720,18 +720,11 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, throw new Exception(Exception::REALTIME_TOO_MANY_MESSAGES, 'Too many requests'); } - // Record realtime inbound bytes for this project (WS handshake + query params) - try { - $rawSize = $request->getSize(); - } catch (Throwable) { - $rawSize = \strlen((string) $request->getURI()); - } + $rawSize = $request->getSize(); - if ($rawSize > 0) { - triggerStats([ - METRIC_REALTIME_INBOUND => $rawSize, - ], $project->getId()); - } + triggerStats([ + METRIC_REALTIME_INBOUND => $rawSize, + ], $project->getId()); /* * Validate Client Domain - Check to avoid CSRF attack. From 39f3bc7b9dfc09d6c277b01a6efc61a43e30e637 Mon Sep 17 00:00:00 2001 From: eldadfux Date: Mon, 9 Mar 2026 20:08:41 +0100 Subject: [PATCH 08/11] Fix SDK namespace call --- app/controllers/general.php | 3 +++ app/http.php | 3 +++ 2 files changed, 6 insertions(+) diff --git a/app/controllers/general.php b/app/controllers/general.php index 57edd98bc4..f77aa3ec52 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1399,6 +1399,9 @@ Http::error() $sdk = $route?->getLabel("sdk", false); $action = 'UNKNOWN_NAMESPACE.UNKNOWN.METHOD'; if (!empty($sdk)) { + if (\is_array($sdk)) { + $sdk = $sdk[0]; + } /** @var \Appwrite\SDK\Method $sdk */ $action = $sdk->getNamespace() . '.' . $sdk->getMethodName(); } elseif ($route === null) { diff --git a/app/http.php b/app/http.php index 7f771de130..1302940856 100644 --- a/app/http.php +++ b/app/http.php @@ -581,6 +581,9 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool $action = 'UNKNOWN_NAMESPACE.UNKNOWN.METHOD'; if (!empty($sdk)) { + if (\is_array($sdk)) { + $sdk = $sdk[0]; + } /** @var Appwrite\SDK\Method $sdk */ $action = $sdk->getNamespace() . '.' . $sdk->getMethodName(); } elseif ($route === null) { From a0167d6c6c18560cd8d6d4b8762437c7358916aa Mon Sep 17 00:00:00 2001 From: eldadfux Date: Mon, 9 Mar 2026 20:17:32 +0100 Subject: [PATCH 09/11] Fix for when vcs comment is empty --- src/Appwrite/Vcs/Comment.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Vcs/Comment.php b/src/Appwrite/Vcs/Comment.php index 90f9c8f95d..e6d6996748 100644 --- a/src/Appwrite/Vcs/Comment.php +++ b/src/Appwrite/Vcs/Comment.php @@ -251,7 +251,7 @@ class Comment $json = \base64_decode($state); $builds = \json_decode($json, true); - $this->builds = $builds; + $this->builds = \is_array($builds) ? $builds : []; return $this; } From eccc39a4669db5afb52e35928e071524329a2a05 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 10 Mar 2026 12:15:25 +0530 Subject: [PATCH 10/11] refactor: remove realtime metrics from project usage endpoints and related tests --- app/controllers/api/project.php | 34 --- app/realtime.php | 59 +---- .../Utopia/Response/Model/UsageProject.php | 39 --- tests/e2e/General/UsageTest.php | 236 ------------------ 4 files changed, 1 insertion(+), 367 deletions(-) diff --git a/app/controllers/api/project.php b/app/controllers/api/project.php index 1fd33a5db0..d24519e3fb 100644 --- a/app/controllers/api/project.php +++ b/app/controllers/api/project.php @@ -72,10 +72,6 @@ Http::get('/v1/project/usage') METRIC_DATABASES_OPERATIONS_READS, METRIC_DATABASES_OPERATIONS_WRITES, METRIC_FILES_IMAGES_TRANSFORMED, - METRIC_REALTIME_CONNECTIONS, - METRIC_REALTIME_CONNECTIONS_MESSAGES_SENT, - METRIC_REALTIME_INBOUND, - METRIC_REALTIME_OUTBOUND, ], 'period' => [ METRIC_NETWORK_REQUESTS, @@ -89,10 +85,6 @@ Http::get('/v1/project/usage') METRIC_DATABASES_OPERATIONS_READS, METRIC_DATABASES_OPERATIONS_WRITES, METRIC_FILES_IMAGES_TRANSFORMED, - METRIC_REALTIME_CONNECTIONS, - METRIC_REALTIME_CONNECTIONS_MESSAGES_SENT, - METRIC_REALTIME_INBOUND, - METRIC_REALTIME_OUTBOUND, ] ]; @@ -355,26 +347,6 @@ Http::get('/v1/project/usage') ]; } - // Realtime bandwidth = realtime.inbound + realtime.outbound (per bucket) - $realtimeProjectBandwidth = []; - foreach ($usage[METRIC_REALTIME_INBOUND] as $item) { - $realtimeProjectBandwidth[$item['date']] ??= 0; - $realtimeProjectBandwidth[$item['date']] += $item['value']; - } - - foreach ($usage[METRIC_REALTIME_OUTBOUND] as $item) { - $realtimeProjectBandwidth[$item['date']] ??= 0; - $realtimeProjectBandwidth[$item['date']] += $item['value']; - } - - $realtimeBandwidth = []; - foreach ($realtimeProjectBandwidth as $date => $value) { - $realtimeBandwidth[] = [ - 'date' => $date, - 'value' => $value - ]; - } - $response->dynamic(new Document([ 'requests' => ($usage[METRIC_NETWORK_REQUESTS]), 'network' => $network, @@ -395,16 +367,10 @@ Http::get('/v1/project/usage') 'deploymentsStorageTotal' => $total[METRIC_DEPLOYMENTS_STORAGE], 'databasesReadsTotal' => $total[METRIC_DATABASES_OPERATIONS_READS], 'databasesWritesTotal' => $total[METRIC_DATABASES_OPERATIONS_WRITES], - 'realtimeConnectionsTotal' => $total[METRIC_REALTIME_CONNECTIONS], - 'realtimeMessagesTotal' => $total[METRIC_REALTIME_CONNECTIONS_MESSAGES_SENT], - 'realtimeBandwidthTotal' => ($total[METRIC_REALTIME_INBOUND] ?? 0) + ($total[METRIC_REALTIME_OUTBOUND] ?? 0), 'executionsBreakdown' => $executionsBreakdown, 'bucketsBreakdown' => $bucketsBreakdown, 'databasesReads' => $usage[METRIC_DATABASES_OPERATIONS_READS], 'databasesWrites' => $usage[METRIC_DATABASES_OPERATIONS_WRITES], - 'realtimeConnections' => $usage[METRIC_REALTIME_CONNECTIONS], - 'realtimeMessages' => $usage[METRIC_REALTIME_CONNECTIONS_MESSAGES_SENT], - 'realtimeBandwidth' => $realtimeBandwidth, 'databasesStorageBreakdown' => $databasesStorageBreakdown, 'executionsMbSecondsBreakdown' => $executionsMbSecondsBreakdown, 'buildsMbSecondsBreakdown' => $buildsMbSecondsBreakdown, diff --git a/app/realtime.php b/app/realtime.php index 9ab6601ed7..1e58832203 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -1,6 +1,5 @@ getSequence()])) { - return $ctx['queueForStatsUsage'][$project->getSequence()]; - } - - global $register; - - /** @var Group $pools */ - $pools = $register->get('pools'); - - $queue = new StatsUsage(new BrokerPool(publisher: $pools->get('publisher'))); - $queue->setProject($project); - - return $ctx['queueForStatsUsage'][$project->getSequence()] = $queue; - } -} - if (!function_exists('triggerStats')) { - /** - * Trigger realtime usage stats with a generic metric map. - * - * @param array $event Metrics in the form METRIC_CONSTANT => value - */ function triggerStats(array $event, string $projectId): void { - if (empty($projectId)) { - return; - } - - try { - $consoleDB = getConsoleDB(); - /** @var Document $project */ - $project = $consoleDB->getAuthorization()->skip( - fn () => $consoleDB->getDocument('projects', $projectId) - ); - - if ($project->isEmpty()) { - return; - } - - $queueForStatsUsage = getQueueForStatsUsageForProject($project); - - foreach ($event as $metric => $value) { - $queueForStatsUsage->addMetric($metric, $value); - } - - $queueForStatsUsage->trigger(); - $queueForStatsUsage->reset(); - } catch (Throwable $th) { - logError($th, 'realtimeStats', tags: ['projectId' => $projectId]); - } + return; } } diff --git a/src/Appwrite/Utopia/Response/Model/UsageProject.php b/src/Appwrite/Utopia/Response/Model/UsageProject.php index c219a35f29..ee644aa845 100644 --- a/src/Appwrite/Utopia/Response/Model/UsageProject.php +++ b/src/Appwrite/Utopia/Response/Model/UsageProject.php @@ -100,24 +100,6 @@ class UsageProject extends Model 'default' => 0, 'example' => 0, ]) - ->addRule('realtimeConnectionsTotal', [ - 'type' => self::TYPE_INTEGER, - 'description' => 'Current aggregated number of open Realtime connections.', - 'default' => 0, - 'example' => 0, - ]) - ->addRule('realtimeMessagesTotal', [ - 'type' => self::TYPE_INTEGER, - 'description' => 'Total number of Realtime messages sent to clients.', - 'default' => 0, - 'example' => 0, - ]) - ->addRule('realtimeBandwidthTotal', [ - 'type' => self::TYPE_INTEGER, - 'description' => 'Total consumed Realtime bandwidth (in bytes).', - 'default' => 0, - 'example' => 0, - ]) ->addRule('requests', [ 'type' => Response::MODEL_METRIC, 'description' => 'Aggregated number of requests per period.', @@ -132,27 +114,6 @@ class UsageProject extends Model 'example' => [], 'array' => true ]) - ->addRule('realtimeConnections', [ - 'type' => Response::MODEL_METRIC, - 'description' => 'Aggregated number of open Realtime connections per period.', - 'default' => [], - 'example' => [], - 'array' => true - ]) - ->addRule('realtimeMessages', [ - 'type' => Response::MODEL_METRIC, - 'description' => 'Aggregated number of Realtime messages sent to clients per period.', - 'default' => [], - 'example' => [], - 'array' => true - ]) - ->addRule('realtimeBandwidth', [ - 'type' => Response::MODEL_METRIC, - 'description' => 'Aggregated consumed Realtime bandwidth (in bytes) per period.', - 'default' => [], - 'example' => [], - 'array' => true - ]) ->addRule('users', [ 'type' => Response::MODEL_METRIC, 'description' => 'Aggregated number of users per period.', diff --git a/tests/e2e/General/UsageTest.php b/tests/e2e/General/UsageTest.php index f51b04b9c6..cabbdf6913 100644 --- a/tests/e2e/General/UsageTest.php +++ b/tests/e2e/General/UsageTest.php @@ -1410,242 +1410,6 @@ class UsageTest extends Scope }); } - public function testRealtimeUsageMetrics(): void - { - $user = $this->getUser(); - $session = $user['session'] ?? ''; - $projectId = $this->getProject()['$id']; - - // Baseline realtime usage before opening a new connection - $baseline = $this->client->call( - Client::METHOD_GET, - '/project/usage', - $this->getConsoleHeaders(), - [ - 'period' => '1h', - 'startDate' => self::getToday(), - 'endDate' => self::getTomorrow(), - ] - ); - - $connectionsBefore = $baseline['body']['realtimeConnectionsTotal'] ?? 0; - $messagesBefore = $baseline['body']['realtimeMessagesTotal'] ?? 0; - - $connectionCount = 3; - $clients = []; - - for ($i = 0; $i < $connectionCount; $i++) { - $client = $this->getWebsocket(['documents'], [ - 'origin' => 'http://localhost', - 'cookie' => 'a_session_' . $projectId . '=' . $session, - ], null); - - $connected = json_decode($client->receive(), true); - $this->assertEquals('connected', $connected['type']); - - $clients[] = $client; - } - - try { - $database = $this->client->call(Client::METHOD_POST, '/databases', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], [ - 'databaseId' => ID::unique(), - 'name' => 'Realtime Usage DB', - ]); - - $databaseId = $database['body']['$id']; - - $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], [ - 'collectionId' => ID::unique(), - 'name' => 'Realtime Usage Collection', - 'permissions' => [ - Permission::create(Role::user($user['$id'])), - ], - 'documentSecurity' => true, - ]); - - $collectionId = $collection['body']['$id']; - - $attribute = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], [ - 'key' => 'name', - 'size' => 256, - 'required' => true, - ]); - - $this->assertEquals(202, $attribute['headers']['status-code']); - - $this->assertEventually(function () use ($databaseId, $collectionId, $projectId) { - $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/name', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]); - $this->assertEquals('available', $response['body']['status']); - }, 30000, 250); - - $document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - ], $this->getHeaders()), [ - 'documentId' => ID::unique(), - 'data' => [ - 'name' => 'Realtime Usage Doc', - ], - 'permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - ]); - - $this->assertEquals(201, $document['headers']['status-code']); - - $event = json_decode($clients[0]->receive(), true); - $this->assertEquals('event', $event['type']); - - // After creating a document we expect all connections to receive an event - $this->assertEventually(function () use ($connectionsBefore, $messagesBefore, $connectionCount) { - $response = $this->client->call( - Client::METHOD_GET, - '/project/usage', - $this->getConsoleHeaders(), - [ - 'period' => '1h', - 'startDate' => self::getToday(), - 'endDate' => self::getTomorrow(), - ] - ); - - $this->assertEquals(200, $response['headers']['status-code']); - - $this->assertArrayHasKey('realtimeConnectionsTotal', $response['body']); - $this->assertArrayHasKey('realtimeMessagesTotal', $response['body']); - $this->assertArrayHasKey('realtimeBandwidthTotal', $response['body']); - $this->assertArrayHasKey('realtimeConnections', $response['body']); - $this->assertArrayHasKey('realtimeMessages', $response['body']); - $this->assertArrayHasKey('realtimeBandwidth', $response['body']); - - // We expect exactly $connectionCount additional open connections and $connectionCount additional message deliveries - $this->assertEquals($connectionsBefore + $connectionCount, $response['body']['realtimeConnectionsTotal']); - $this->assertEquals($messagesBefore + $connectionCount, $response['body']['realtimeMessagesTotal']); - $this->assertGreaterThan(0, $response['body']['realtimeBandwidthTotal']); - - $this->validateDates($response['body']['realtimeConnections']); - $this->validateDates($response['body']['realtimeMessages']); - $this->validateDates($response['body']['realtimeBandwidth']); - }, 60000, 2000); - - // Capture a snapshot of usage after the broadcasted document event - $afterEventUsage = $this->client->call( - Client::METHOD_GET, - '/project/usage', - $this->getConsoleHeaders(), - [ - 'period' => '1h', - 'startDate' => self::getToday(), - 'endDate' => self::getTomorrow(), - ] - ); - - $this->assertEquals(200, $afterEventUsage['headers']['status-code']); - - $connectionsAfterEvent = $afterEventUsage['body']['realtimeConnectionsTotal'] ?? 0; - $messagesAfterEvent = $afterEventUsage['body']['realtimeMessagesTotal'] ?? 0; - $bandwidthAfterEvent = $afterEventUsage['body']['realtimeBandwidthTotal'] ?? 0; - - // Send a ping over an existing connection to exercise the ping/pong - // realtime usage metrics path (inbound + outbound bytes) without - // generating additional "messages sent" usage. - $clients[1]->send(json_encode([ - 'type' => 'ping', - ])); - - // A broadcast document "event" can still be queued for this connection. - // Read until we see the pong response (and fail fast on unexpected frames). - $pong = null; - for ($i = 0; $i < 5; $i++) { - $frame = json_decode($clients[1]->receive(), true); - $this->assertIsArray($frame); - $this->assertArrayHasKey('type', $frame); - - if ($frame['type'] === 'pong') { - $pong = $frame; - break; - } - - $this->assertEquals('event', $frame['type']); - } - - $this->assertNotNull($pong, 'Expected to receive a pong frame after ping.'); - - // We expect: - // - connections count to remain the same - // - messages count to remain the same (no new broadcast events) - // - bandwidth total to increase because of ping/pong traffic - $this->assertEventually(function () use ($connectionsAfterEvent, $messagesAfterEvent, $bandwidthAfterEvent) { - $response = $this->client->call( - Client::METHOD_GET, - '/project/usage', - $this->getConsoleHeaders(), - [ - 'period' => '1h', - 'startDate' => self::getToday(), - 'endDate' => self::getTomorrow(), - ] - ); - - $this->assertEquals(200, $response['headers']['status-code']); - - $this->assertEquals($connectionsAfterEvent, $response['body']['realtimeConnectionsTotal']); - $this->assertEquals($messagesAfterEvent, $response['body']['realtimeMessagesTotal']); - $this->assertGreaterThan($bandwidthAfterEvent, $response['body']['realtimeBandwidthTotal']); - }, 60000, 2000); - - // Now close a single connection and ensure the counters reflect it - $clients[0]->close(); - - $this->assertEventually(function () use ($connectionsBefore, $messagesBefore, $connectionCount) { - $response = $this->client->call( - Client::METHOD_GET, - '/project/usage', - $this->getConsoleHeaders(), - [ - 'period' => '1h', - 'startDate' => self::getToday(), - 'endDate' => self::getTomorrow(), - ] - ); - - $this->assertEquals(200, $response['headers']['status-code']); - - $this->assertArrayHasKey('realtimeConnectionsTotal', $response['body']); - $this->assertArrayHasKey('realtimeMessagesTotal', $response['body']); - $this->assertArrayHasKey('realtimeBandwidthTotal', $response['body']); - - // One of the connections is closed, so we expect one less open connection. - // Messages and bandwidth are cumulative and should not decrease. - $this->assertEquals($connectionsBefore + $connectionCount - 1, $response['body']['realtimeConnectionsTotal']); - $this->assertEquals($messagesBefore + $connectionCount, $response['body']['realtimeMessagesTotal']); - $this->assertGreaterThan(0, $response['body']['realtimeBandwidthTotal']); - }, 60000, 2000); - } finally { - foreach ($clients as $client) { - $client->close(); - } - } - } - public function tearDown(): void { $this->projectId = ''; From 7f4cba276e63700c75052bade5e41a62d11e6414 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 10 Mar 2026 12:19:46 +0530 Subject: [PATCH 11/11] updated tests --- tests/e2e/General/UsageTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/e2e/General/UsageTest.php b/tests/e2e/General/UsageTest.php index cabbdf6913..e7b5b1d844 100644 --- a/tests/e2e/General/UsageTest.php +++ b/tests/e2e/General/UsageTest.php @@ -11,7 +11,6 @@ use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideServer; use Tests\E2E\Services\Functions\FunctionsBase; -use Tests\E2E\Services\Realtime\RealtimeBase; use Tests\E2E\Services\Sites\SitesBase; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; @@ -24,7 +23,6 @@ class UsageTest extends Scope use ProjectCustom; use SideServer; use FunctionsBase; - use RealtimeBase; use SitesBase { FunctionsBase::createDeployment insteadof SitesBase; FunctionsBase::setupDeployment insteadof SitesBase;