Skip to content

Refactor usage metrics to stateless publisher pattern#11449

Merged
loks0n merged 2 commits into1.8.xfrom
refactor/usage-publishing
Mar 13, 2026
Merged

Refactor usage metrics to stateless publisher pattern#11449
loks0n merged 2 commits into1.8.xfrom
refactor/usage-publishing

Conversation

@loks0n
Copy link
Copy Markdown
Member

@loks0n loks0n commented Mar 4, 2026

Overview

This PR refactors the usage metrics system from a stateful event-based pattern to a cleaner, stateless publisher pattern that aligns with how all other events work in the codebase.

The Problem

The StatsUsage event class was architecturally inconsistent with other event types:

StatsUsage (Inconsistent):

  • Stateful: Accumulated metrics internally
  • Self-publishing: Called trigger() on itself
  • Mixed responsibilities: Both accumulator and publisher

Other Events (Correct Pattern):

  • Stateless: Pure message DTOs
  • External publishing: Publishers handle enqueueing
  • Single responsibility: Events are just data

This inconsistency made the codebase harder to understand and maintain.

The Solution

Split responsibilities into three components matching the existing event pattern:

1. Context - Mutable accumulator (request/worker scope)
2. Message\Usage - Immutable DTO (data snapshot)
3. Publisher\Usage - Stateless publisher (handles enqueueing)

Key Changes

During Request/Worker Execution

Before:

$queueForStatsUsage
    ->setProject($project)  // Must set project context
    ->addMetric(METRIC_USERS, 1);

After:

$usage->addMetric(METRIC_USERS, 1);  // Just accumulate, no project needed

The new pattern is simpler during execution - you just accumulate metrics without worrying about project context or publishing.

At Shutdown/Completion

Before:

// In shutdown hook
$queueForStatsUsage
    ->trigger();  // Event publishes itself

After:

// In shutdown hook
$message = new Message\Usage(
    project: $project,
    metrics: $usage->getMetrics(),
    reduce: $usage->getReduce()
);
$publisherForUsage->enqueue($message);  // Publisher handles enqueueing

The new pattern is explicit and consistent with how all other events work (Audit, Events, Webhooks, etc.).

Disabled Metrics Filtering

An additional improvement - filtering is now decoupled and happens at publish time:

// Filter disabled metrics (API keys can disable specific metrics)
$disabledMetrics = $apiKey?->getDisabledMetrics() ?? [];
if (!empty($disabledMetrics)) {
    $metrics = array_values(array_filter($metrics, function ($metric) use ($disabledMetrics) {
        foreach ($disabledMetrics as $pattern) {
            if (str_ends_with($metric['key'], $pattern)) {
                return false;
            }
        }
        return true;
    }));
}

Previously this filtering was tightly coupled to the StatsUsage event object. Now it's a simple inline filter that's easy to modify or extend.

Benefits

🎯 Architectural Consistency

All events now follow the same pattern - no special cases to remember

🔍 Clearer Data Flow

Accumulation → Snapshot → Publish is explicit at each step

🔓 Decoupled Filtering

API key concerns separated from metric collection

🧪 Better Testability

Stateless components are easier to test and debug

📦 Transparent Dependencies

Workers explicitly inject what they need, no hidden state

Real-World Example: Builds Worker

Before:

protected function sendUsage(..., StatsUsage $queue) {
    $queue
        ->addMetric(METRIC_BUILDS_SUCCESS, 1)
        ->addMetric(METRIC_BUILDS_COMPUTE_SUCCESS, $duration)
        ->setProject($project)
        ->trigger();  // Self-publishing, unclear when this happens
}

After:

protected function sendUsage(..., Context $usage, UsagePublisher $publisherForUsage) {
    $usage
        ->addMetric(METRIC_BUILDS_SUCCESS, 1)
        ->addMetric(METRIC_BUILDS_COMPUTE_SUCCESS, $duration);
    
    // Explicit publish step
    $message = new Message\Usage(
        project: $project,
        metrics: $usage->getMetrics(),
        reduce: $usage->getReduce()
    );
    $publisherForUsage->enqueue($message);
    $usage->reset();  // Ready for next build
}

The new pattern makes it crystal clear: accumulate metrics, create immutable message, publish via publisher, reset for reuse.

Testing

All usage metrics tests passing:

  • ✅ 13/13 tests
  • ✅ 1607 assertions
  • ✅ Covers users, storage, databases, functions, sites, custom domains

Test Improvements

  • Updated polling to use console headers (privileged) to avoid inflating metrics
  • Fixed attribute availability polling in database tests

Migration Impact

This is an internal refactoring with no breaking changes:

  • ✅ Public APIs unchanged
  • ✅ Database schema unchanged
  • ✅ Message queue format unchanged
  • ✅ Worker behavior unchanged

The refactoring is completely transparent to API consumers.

Files Changed

  • Removed: src/Appwrite/Event/StatsUsage.php (old stateful event)
  • Simplified: src/Appwrite/Usage/Context.php (pure accumulator)
  • Fixed: src/Appwrite/Event/Publisher/Usage.php (readonly property issue)
  • Updated: API shutdown hook (new publish pattern)
  • Updated: Builds worker (migrated to new pattern)
  • Improved: Usage tests (fixed metric inflation)

Net change: -99 lines of code


🤖 Generated with Claude Code

@loks0n loks0n changed the base branch from main to 1.8.x March 4, 2026 16:55
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 4, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces the StatsUsage queue/class with a usage Context and publisher/message model. Adds Appwrite\Usage\Context, Appwrite\Event\Message\Base, Appwrite\Event\Message\Usage, Appwrite\Event\Publisher\Base and Appwrite\Event\Publisher\Usage. Introduces public resources usage and publisherForUsage, removes src/Appwrite/Event/StatsUsage.php, and updates dependency injections, method signatures, and metric calls across CLI, init, worker, controllers, platform modules, workers, listeners, and tests to accumulate metrics in Context, serialize to Usage messages, enqueue via Usage publisher, then reset the Context.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.93% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and clearly summarizes the main change: refactoring the usage metrics system to use a stateless publisher pattern.
Description check ✅ Passed The description is comprehensive, well-structured, and directly related to the changeset. It explains the problem, solution, implementation details, benefits, and testing status.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/usage-publishing
📝 Coding Plan for PR comments
  • Generate coding plan

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 4, 2026

Security Scan Results for PR

Docker Image Scan Results

Package Version Vulnerability Severity
imagemagick 7.1.2.13-r0 CVE-2026-25897 CRITICAL
imagemagick 7.1.2.13-r0 CVE-2026-25898 CRITICAL
imagemagick 7.1.2.13-r0 CVE-2026-25968 CRITICAL
imagemagick 7.1.2.13-r0 CVE-2026-25971 CRITICAL
imagemagick 7.1.2.13-r0 CVE-2026-25983 CRITICAL
imagemagick 7.1.2.13-r0 CVE-2026-25986 CRITICAL
imagemagick 7.1.2.13-r0 CVE-2026-25987 CRITICAL
imagemagick 7.1.2.13-r0 CVE-2026-26284 CRITICAL
imagemagick 7.1.2.13-r0 CVE-2026-24481 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-24485 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25794 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25795 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25796 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25798 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25799 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25965 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25966 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25967 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25969 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25970 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25985 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25988 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25989 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-26066 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-26283 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-27798 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25897 CRITICAL
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25898 CRITICAL
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25968 CRITICAL
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25971 CRITICAL
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25983 CRITICAL
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25986 CRITICAL
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25987 CRITICAL
imagemagick-c++ 7.1.2.13-r0 CVE-2026-26284 CRITICAL
imagemagick-c++ 7.1.2.13-r0 CVE-2026-24481 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-24485 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25794 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25795 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25796 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25798 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25799 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25965 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25966 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25967 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25969 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25970 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25985 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25988 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25989 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-26066 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-26283 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-27798 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25897 CRITICAL
imagemagick-dev 7.1.2.13-r0 CVE-2026-25898 CRITICAL
imagemagick-dev 7.1.2.13-r0 CVE-2026-25968 CRITICAL
imagemagick-dev 7.1.2.13-r0 CVE-2026-25971 CRITICAL
imagemagick-dev 7.1.2.13-r0 CVE-2026-25983 CRITICAL
imagemagick-dev 7.1.2.13-r0 CVE-2026-25986 CRITICAL
imagemagick-dev 7.1.2.13-r0 CVE-2026-25987 CRITICAL
imagemagick-dev 7.1.2.13-r0 CVE-2026-26284 CRITICAL
imagemagick-dev 7.1.2.13-r0 CVE-2026-24481 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-24485 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25794 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25795 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25796 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25798 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25799 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25965 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25966 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25967 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25969 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25970 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25985 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25988 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25989 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-26066 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-26283 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-27798 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25897 CRITICAL
imagemagick-heic 7.1.2.13-r0 CVE-2026-25898 CRITICAL
imagemagick-heic 7.1.2.13-r0 CVE-2026-25968 CRITICAL
imagemagick-heic 7.1.2.13-r0 CVE-2026-25971 CRITICAL
imagemagick-heic 7.1.2.13-r0 CVE-2026-25983 CRITICAL
imagemagick-heic 7.1.2.13-r0 CVE-2026-25986 CRITICAL
imagemagick-heic 7.1.2.13-r0 CVE-2026-25987 CRITICAL
imagemagick-heic 7.1.2.13-r0 CVE-2026-26284 CRITICAL
imagemagick-heic 7.1.2.13-r0 CVE-2026-24481 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-24485 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25794 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25795 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25796 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25798 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25799 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25965 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25966 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25967 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25969 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25970 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25985 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25988 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25989 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-26066 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-26283 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-27798 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25897 CRITICAL
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25898 CRITICAL
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25968 CRITICAL
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25971 CRITICAL
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25983 CRITICAL
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25986 CRITICAL
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25987 CRITICAL
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-26284 CRITICAL
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-24481 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-24485 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25794 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25795 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25796 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25798 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25799 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25965 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25966 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25967 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25969 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25970 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25985 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25988 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25989 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-26066 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-26283 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-27798 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25897 CRITICAL
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25898 CRITICAL
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25968 CRITICAL
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25971 CRITICAL
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25983 CRITICAL
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25986 CRITICAL
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25987 CRITICAL
imagemagick-jxl 7.1.2.13-r0 CVE-2026-26284 CRITICAL
imagemagick-jxl 7.1.2.13-r0 CVE-2026-24481 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-24485 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25794 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25795 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25796 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25798 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25799 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25965 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25966 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25967 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25969 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25970 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25985 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25988 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25989 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-26066 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-26283 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-27798 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25897 CRITICAL
imagemagick-libs 7.1.2.13-r0 CVE-2026-25898 CRITICAL
imagemagick-libs 7.1.2.13-r0 CVE-2026-25968 CRITICAL
imagemagick-libs 7.1.2.13-r0 CVE-2026-25971 CRITICAL
imagemagick-libs 7.1.2.13-r0 CVE-2026-25983 CRITICAL
imagemagick-libs 7.1.2.13-r0 CVE-2026-25986 CRITICAL
imagemagick-libs 7.1.2.13-r0 CVE-2026-25987 CRITICAL
imagemagick-libs 7.1.2.13-r0 CVE-2026-26284 CRITICAL
imagemagick-libs 7.1.2.13-r0 CVE-2026-24481 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-24485 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25794 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25795 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25796 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25798 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25799 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25965 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25966 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25967 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25969 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25970 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25985 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25988 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25989 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-26066 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-26283 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-27798 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25897 CRITICAL
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25898 CRITICAL
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25968 CRITICAL
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25971 CRITICAL
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25983 CRITICAL
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25986 CRITICAL
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25987 CRITICAL
imagemagick-tiff 7.1.2.13-r0 CVE-2026-26284 CRITICAL
imagemagick-tiff 7.1.2.13-r0 CVE-2026-24481 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-24485 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25794 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25795 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25796 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25798 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25799 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25965 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25966 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25967 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25969 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25970 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25985 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25988 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25989 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-26066 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-26283 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-27798 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25897 CRITICAL
imagemagick-webp 7.1.2.13-r0 CVE-2026-25898 CRITICAL
imagemagick-webp 7.1.2.13-r0 CVE-2026-25968 CRITICAL
imagemagick-webp 7.1.2.13-r0 CVE-2026-25971 CRITICAL
imagemagick-webp 7.1.2.13-r0 CVE-2026-25983 CRITICAL
imagemagick-webp 7.1.2.13-r0 CVE-2026-25986 CRITICAL
imagemagick-webp 7.1.2.13-r0 CVE-2026-25987 CRITICAL
imagemagick-webp 7.1.2.13-r0 CVE-2026-26284 CRITICAL
imagemagick-webp 7.1.2.13-r0 CVE-2026-24481 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-24485 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25794 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25795 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25796 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25798 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25799 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25965 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25966 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25967 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25969 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25970 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25985 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25988 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25989 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-26066 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-26283 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-27798 HIGH
libecpg 18.1-r0 CVE-2026-2004 HIGH
libecpg 18.1-r0 CVE-2026-2005 HIGH
libecpg 18.1-r0 CVE-2026-2006 HIGH
libecpg 18.1-r0 CVE-2026-2007 HIGH
libecpg-dev 18.1-r0 CVE-2026-2004 HIGH
libecpg-dev 18.1-r0 CVE-2026-2005 HIGH
libecpg-dev 18.1-r0 CVE-2026-2006 HIGH
libecpg-dev 18.1-r0 CVE-2026-2007 HIGH
libheif 1.20.2-r1 CVE-2025-68431 HIGH
libpng 1.6.54-r0 CVE-2026-25646 HIGH
libpng-dev 1.6.54-r0 CVE-2026-25646 HIGH
libpq 18.1-r0 CVE-2026-2004 HIGH
libpq 18.1-r0 CVE-2026-2005 HIGH
libpq 18.1-r0 CVE-2026-2006 HIGH
libpq 18.1-r0 CVE-2026-2007 HIGH
libpq-dev 18.1-r0 CVE-2026-2004 HIGH
libpq-dev 18.1-r0 CVE-2026-2005 HIGH
libpq-dev 18.1-r0 CVE-2026-2006 HIGH
libpq-dev 18.1-r0 CVE-2026-2007 HIGH
postgresql18-dev 18.1-r0 CVE-2026-2004 HIGH
postgresql18-dev 18.1-r0 CVE-2026-2005 HIGH
postgresql18-dev 18.1-r0 CVE-2026-2006 HIGH
postgresql18-dev 18.1-r0 CVE-2026-2007 HIGH
zlib 1.3.1-r2 CVE-2026-22184 CRITICAL
zlib-dev 1.3.1-r2 CVE-2026-22184 CRITICAL

Source Code Scan Results

🎉 No vulnerabilities found!

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 4, 2026

✨ Benchmark results

  • Requests per second: 1,735
  • Requests with 200 status code: 312,321
  • P99 latency: 0.097933546

⚡ Benchmark Comparison

Metric This PR Latest version
RPS 1,735 1,195
200 312,321 215,220
P99 0.097933546 0.193741247

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Appwrite/Platform/Modules/Functions/Workers/Builds.php (1)

1181-1186: ⚠️ Potential issue | 🔴 Critical

Fix sendUsage() type mismatch: signature declares StatsUsage but receives Context.

Line 1185 passes $usage (a Context instance) to sendUsage(queue: ...), but the method signature at line 1190 declares StatsUsage $queue. This will fail with a TypeError at runtime when strict type checking is enforced. The method body correctly uses ->addMetric(), which Context implements.

Fix
-    protected function sendUsage(Document $resource, Document $deployment, Document $project, StatsUsage $queue): void
+    protected function sendUsage(Document $resource, Document $deployment, Document $project, Context $queue): void
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Modules/Functions/Workers/Builds.php` around lines 1181
- 1186, The call to sendUsage passes $usage (a Context instance) but the method
signature expects StatsUsage; update the sendUsage method signature to accept
Context $queue (instead of StatsUsage) and update any related type hints/imports
so the parameter type matches the object being passed ($usage), ensuring the
body can still call ->addMetric() on $queue; alternatively, if you prefer to
keep StatsUsage, change the caller to pass a StatsUsage instance, but the
simplest fix is to change the sendUsage signature from StatsUsage to Context to
match the usage.
🧹 Nitpick comments (2)
src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php (1)

145-145: Remove unused $usage from closure capture.

$usage is captured in the withTransaction closure but not used there.

♻️ Optional cleanup
-                $dbForProject->withTransaction(function () use ($dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $queueForEvents, $usage, $queueForRealtime, $queueForFunctions, $queueForWebhooks, $authorization) {
+                $dbForProject->withTransaction(function () use ($dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $queueForEvents, $queueForRealtime, $queueForFunctions, $queueForWebhooks, $authorization) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php`
at line 145, The closure passed to $dbForProject->withTransaction in Update.php
unnecessarily captures $usage; remove $usage from the closure's use(...) capture
list in the withTransaction call (the anonymous function starting with function
() use ($dbForProject, $transactionState, ...)) so the closure only captures
variables it actually uses, and run a quick search for $usage inside that
closure to confirm it isn't referenced before committing the change.
src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php (1)

102-102: Consider removing unused $timelimit and $plan from this action signature.

They are currently injected but not referenced, which adds avoidable noise to the endpoint contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php` at line 102,
The action method signature for Memberships::action currently includes unused
parameters $timelimit and $plan; remove these two parameters from the
action(string $teamId, ...) signature in Create::action to keep the endpoint
contract minimal, and update any docblocks and unit tests referencing
Create::action; also search for any caller or DI/invocation sites that pass
$timelimit or $plan to Create::action and adjust those calls to no longer supply
them so the parameter removal stays consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/controllers/shared/api.php`:
- Around line 972-981: The shutdown path is enqueuing usage messages
unconditionally when $usage->isEmpty() is false, bypassing the global
_APP_USAGE_STATS flag; update the shutdown logic in
app/controllers/shared/api.php to check the _APP_USAGE_STATS configuration (or
equivalent feature flag) before creating/enqueuing the
\Appwrite\Event\Message\Usage and calling $publisherForUsage->enqueue($message),
returning early if usage stats are disabled; make the same change for the second
similar block that enqueues usage messages (the block referencing $usage and
$publisherForUsage around the later lines) so non-`groups(['usage'])` requests
do not publish metrics when stats are turned off.

In
`@src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php`:
- Around line 78-84: Remove the unused user injection and parameter: delete the
->inject('user') call from the route builder and remove the Document $user
parameter from the action method signature (public function action(... Document
$user, ...)), updating the parameter list order accordingly so other parameters
(e.g., Context $usage, TransactionState $transactionState, Authorization
$authorization) remain in place and types/signatures still match.

In `@src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php`:
- Line 10: The file imports the Appwrite\Usage\Context symbol twice causing a
fatal "already in use" error; open Create.php, find the duplicate import
statements for Appwrite\Usage\Context (the repeated "use
Appwrite\Usage\Context;") and remove the second occurrence so only a single use
Appwrite\Usage\Context; remains; ensure any references to Context in methods
like the membership creation handler still work after removing the duplicate
import.

In `@src/Appwrite/Platform/Workers/Messaging.php`:
- Around line 242-243: The Appwrite\Usage\Context class is missing the
setProject(Document $project): self and trigger(): void methods that are called
where $usage->setProject($project) and $usage->trigger() are used; add these
methods to Context (implement setProject to store the Document $project and
return $this, and implement trigger to flush/process queued metrics using
existing addMetric/addReduce internals) or alternatively change the callers in
Messaging.php to use the existing API (e.g., pass the project into
addMetric/addReduce directly and invoke whatever flush/reset method already
exists); locate the call sites around Messaging.php where $usage is referenced
and update either Context or those call sites accordingly so runtime fatal
errors are eliminated.

In `@src/Appwrite/Platform/Workers/Migrations.php`:
- Around line 107-109: The migration stats publisher ($publisherForUsage) is
received in action() but not passed into processMigration() or
processMigrationResourceStats(), causing an undefined variable when calling
$publisherForUsage->enqueue($message); update the method signatures of
processMigration(...) and processMigrationResourceStats(...) to accept an
additional \Appwrite\Event\Publisher\Usage $publisherForUsage parameter,
propagate that argument from action() into processMigration(), and from
processMigration() into processMigrationResourceStats(); then replace the
undefined $publisherForUsage usage inside processMigrationResourceStats() with
the new parameter so the enqueue call uses the passed-in publisher instance.

In `@src/Appwrite/Usage/Context.php`:
- Around line 61-68: The filtered metrics use array_filter in the return from
Context.php which preserves original numeric keys and can produce sparse arrays
that json_encode serializes as objects; fix by wrapping the array_filter result
with array_values so the returned value from the function (the filtered metrics)
is reindexed and always serializes as a JSON array (update the return that
currently calls array_filter on $this->metrics).

---

Outside diff comments:
In `@src/Appwrite/Platform/Modules/Functions/Workers/Builds.php`:
- Around line 1181-1186: The call to sendUsage passes $usage (a Context
instance) but the method signature expects StatsUsage; update the sendUsage
method signature to accept Context $queue (instead of StatsUsage) and update any
related type hints/imports so the parameter type matches the object being passed
($usage), ensuring the body can still call ->addMetric() on $queue;
alternatively, if you prefer to keep StatsUsage, change the caller to pass a
StatsUsage instance, but the simplest fix is to change the sendUsage signature
from StatsUsage to Context to match the usage.

---

Nitpick comments:
In
`@src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php`:
- Line 145: The closure passed to $dbForProject->withTransaction in Update.php
unnecessarily captures $usage; remove $usage from the closure's use(...) capture
list in the withTransaction call (the anonymous function starting with function
() use ($dbForProject, $transactionState, ...)) so the closure only captures
variables it actually uses, and run a quick search for $usage inside that
closure to confirm it isn't referenced before committing the change.

In `@src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php`:
- Line 102: The action method signature for Memberships::action currently
includes unused parameters $timelimit and $plan; remove these two parameters
from the action(string $teamId, ...) signature in Create::action to keep the
endpoint contract minimal, and update any docblocks and unit tests referencing
Create::action; also search for any caller or DI/invocation sites that pass
$timelimit or $plan to Create::action and adjust those calls to no longer supply
them so the parameter removal stays consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2ead9333-7fe9-4aeb-aef8-41a48474aafc

📥 Commits

Reviewing files that changed from the base of the PR and between 4574385 and 2a16657.

📒 Files selected for processing (48)
  • CONTRIBUTING.md
  • app/cli.php
  • app/controllers/api/account.php
  • app/controllers/shared/api.php
  • app/init/resources.php
  • app/worker.php
  • src/Appwrite/Bus/Listeners/Usage.php
  • src/Appwrite/Event/Message/Base.php
  • src/Appwrite/Event/Message/Usage.php
  • src/Appwrite/Event/Publisher/Base.php
  • src/Appwrite/Event/Publisher/Usage.php
  • src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php
  • src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Delete.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Delete.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Delete.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Update.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Decrement.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Increment.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Create.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Delete.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Get.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Update.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Upsert.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/XList.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php
  • src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
  • src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
  • src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Failed/Get.php
  • src/Appwrite/Platform/Modules/Health/Http/Health/Queue/StatsUsage/Get.php
  • src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php
  • src/Appwrite/Platform/Workers/Messaging.php
  • src/Appwrite/Platform/Workers/Migrations.php
  • src/Appwrite/Platform/Workers/Webhooks.php
  • src/Appwrite/Usage/Context.php

Comment on lines +78 to +84
->inject('usage')
->inject('transactionState')
->inject('authorization')
->callback($this->action(...));
}

public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, int $ttl, UtopiaResponse $response, Database $dbForProject, Document $user, StatsUsage $queueForStatsUsage, TransactionState $transactionState, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, int $ttl, UtopiaResponse $response, Database $dbForProject, Document $user, Context $usage, TransactionState $transactionState, Authorization $authorization): void
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's confirm the file exists and check the method signature
cat -n src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php | sed -n '70,90p'

Repository: appwrite/appwrite

Length of output: 2415


🏁 Script executed:

#!/bin/bash
# Search for all usages of $user in the XList.php file
rg -n --type=php '\$user\b' src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php

Repository: appwrite/appwrite

Length of output: 356


🏁 Script executed:

#!/bin/bash
# Check for ->inject('user') in the constructor
rg -n --type=php "->inject\('user'\)" src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php

Repository: appwrite/appwrite

Length of output: 465


🏁 Script executed:

#!/bin/bash
# Get a broader view of the action method signature and implementation
cat -n src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php | sed -n '78,150p'

Repository: appwrite/appwrite

Length of output: 4449


🏁 Script executed:

#!/bin/bash
# Search for any usage of $user in the entire method body after line 84
rg -n '\$user' src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php | head -20

Repository: appwrite/appwrite

Length of output: 356


🏁 Script executed:

#!/bin/bash
# Get the rest of the method to check for any $user usage
wc -l src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php

Repository: appwrite/appwrite

Length of output: 151


🏁 Script executed:

#!/bin/bash
# Check the entire action method (likely ends before next public/private method)
cat -n src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php | sed -n '84,250p'

Repository: appwrite/appwrite

Length of output: 9213


Remove the unused $user injection and signature parameter.

The $user parameter is injected (line 77) but never used in the method body. Remove both ->inject('user') and the Document $user parameter from the method signature (line 84).

🧰 Tools
🪛 PHPMD (2.15.0)

[warning] 84-84: Avoid unused parameters such as '$user'. (undefined)

(UnusedFormalParameter)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php`
around lines 78 - 84, Remove the unused user injection and parameter: delete the
->inject('user') call from the route builder and remove the Document $user
parameter from the action method signature (public function action(... Document
$user, ...)), updating the parameter list order accordingly so other parameters
(e.g., Context $usage, TransactionState $transactionState, Authorization
$authorization) remain in place and types/signatures still match.

Comment on lines +61 to +68
return array_filter($this->metrics, function ($metric) {
foreach ($this->disabled as $disabledMetric) {
if (str_ends_with($metric['key'], $disabledMetric)) {
return false;
}
}
return true;
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

PHP 8.x array_filter preserve numeric keys sparse array

💡 Result:

In PHP 8.x, array_filter() preserves the original array keys (including numeric keys). If you filter an indexed array, you can end up with a sparse array (gaps in numeric indexes). The manual explicitly notes this, and suggests array_values() to reindex. [1]

$a = [10, 20, 30, 40];

$b = array_filter($a, fn($v) => $v > 20);
// $b is [2 => 30, 3 => 40]  (sparse numeric keys)

$c = array_values($b);
// $c is [0 => 30, 1 => 40]  (reindexed)

If you want to filter by key, use the $mode flags like ARRAY_FILTER_USE_KEY / ARRAY_FILTER_USE_BOTH—they change what the callback receives, not the key-preservation behavior. [1]

Sources: [1]


🌐 Web query:

PHP json_encode sparse numeric array JSON object behavior

💡 Result:

In PHP, json_encode() decides whether to output a JSON array ([...]) or JSON object ({...}) based on the array’s keys:

  • If the PHP array’s keys are a continuous, zero-based integer sequence (0,1,2,...), it encodes as a JSON array.
  • If the keys are non-sequential (sparse), don’t start at 0, or include any string key, it encodes as a JSON object, and the keys appear as strings in JSON (because JSON object member names are strings). (php.net)

Example (sparse numeric keys become an object): (php.net)

$a = ["first", "second", "third"];
unset($a[1]);
echo json_encode($a); // {"0":"first","2":"third"}

How to force JSON array output

Reindex before encoding:

echo json_encode(array_values($a)); // ["first","third"]

array_values() discards the original keys and rebuilds 0..n-1, causing json_encode() to emit a JSON array. (stackoverflow.com)

If you actually want an object

You can explicitly force object encoding:

echo json_encode($a, JSON_FORCE_OBJECT);

This preserves keys (as JSON object properties) but produces {...} even for sequential arrays. (app.studyraid.com)

Citations:


Reindex filtered metrics to maintain JSON array shape.

array_filter() preserves numeric keys, so filtering metrics leaves sparse arrays with gaps. When json_encode() serializes sparse numeric-key arrays, they become JSON objects instead of arrays, changing the message shape for downstream consumers.

Proposed fix
     public function getMetrics(): array
     {
-        return array_filter($this->metrics, function ($metric) {
+        return array_values(array_filter($this->metrics, function ($metric) {
             foreach ($this->disabled as $disabledMetric) {
                 if (str_ends_with($metric['key'], $disabledMetric)) {
                     return false;
                 }
             }
             return true;
-        });
+        }));
     }

Wrap array_filter() with array_values() to reindex the result and ensure it always serializes as a JSON array.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return array_filter($this->metrics, function ($metric) {
foreach ($this->disabled as $disabledMetric) {
if (str_ends_with($metric['key'], $disabledMetric)) {
return false;
}
}
return true;
});
return array_values(array_filter($this->metrics, function ($metric) {
foreach ($this->disabled as $disabledMetric) {
if (str_ends_with($metric['key'], $disabledMetric)) {
return false;
}
}
return true;
}));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Usage/Context.php` around lines 61 - 68, The filtered metrics
use array_filter in the return from Context.php which preserves original numeric
keys and can produce sparse arrays that json_encode serializes as objects; fix
by wrapping the array_filter result with array_values so the returned value from
the function (the filtered metrics) is reindexed and always serializes as a JSON
array (update the return that currently calls array_filter on $this->metrics).

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Refactors “usage” metric recording/publishing to separate metric accumulation (via a new Appwrite\Usage\Context) from queue publishing (via new Appwrite\Event\Publisher\Usage + Appwrite\Event\Message\Usage), and updates many HTTP endpoints/workers to use the new wiring.

Changes:

  • Introduces Appwrite\Usage\Context for accumulating metrics/reduce docs and filtering disabled metrics.
  • Adds a publisher/message abstraction for usage queue publishing (Event\Publisher\Usage, Event\Message\Usage).
  • Rewires many HTTP routes, workers, health endpoints, and bootstrap resources to inject usage/publisherForUsage instead of queueForStatsUsage.

Reviewed changes

Copilot reviewed 48 out of 48 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
src/Appwrite/Usage/Context.php New usage metric accumulation context.
src/Appwrite/Platform/Workers/Webhooks.php Worker switched to Context injection for usage.
src/Appwrite/Platform/Workers/Migrations.php Worker partially updated to publish via new usage message/publisher.
src/Appwrite/Platform/Workers/Messaging.php Worker switched to Context injection for usage.
src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php HTTP endpoint switched to Context for usage metrics.
src/Appwrite/Platform/Modules/Health/Http/Health/Queue/StatsUsage/Get.php Health endpoint now uses publisherForUsage->getSize().
src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Failed/Get.php Failed-queue endpoint maps stats-usage to usage publisher.
src/Appwrite/Platform/Modules/Functions/Workers/Builds.php Builds worker switched to Context injection for usage.
src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php Function execution endpoint switched to Context for usage metrics.
src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php TablesDB transaction update now injects usage.
src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/XList.php TablesDB rows list now injects usage.
src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Upsert.php TablesDB rows upsert now injects usage.
src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Update.php TablesDB rows update now injects usage.
src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Get.php TablesDB rows get now injects usage.
src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Delete.php TablesDB rows delete now injects usage.
src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Create.php TablesDB rows create now injects usage.
src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Increment.php TablesDB column increment now injects usage.
src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Decrement.php TablesDB column decrement now injects usage.
src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php TablesDB bulk upsert now injects usage.
src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Update.php TablesDB bulk update now injects usage.
src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Delete.php TablesDB bulk delete now injects usage.
src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Delete.php TablesDB delete now injects usage.
src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php DB transaction update switched to Context injection.
src/Appwrite/Platform/Modules/Databases/Http/Databases/Delete.php DB delete now injects usage.
src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php Documents list now injects usage.
src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php Document upsert now injects usage.
src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php Document update now injects usage.
src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php Document get now injects usage.
src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php Document delete now injects usage.
src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php Document create now injects usage.
src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php Bulk document upsert now injects usage.
src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php Bulk document update now injects usage.
src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php Bulk document delete now injects usage.
src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php Attribute increment now injects usage.
src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php Attribute decrement now injects usage.
src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php Screenshots endpoint now injects usage.
src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php MFA challenge creation now injects usage.
src/Appwrite/Event/Publisher/Usage.php New publisher wrapper for the usage queue.
src/Appwrite/Event/Publisher/Base.php New base publisher abstraction.
src/Appwrite/Event/Message/Usage.php New usage queue message DTO/serializer.
src/Appwrite/Event/Message/Base.php New base message abstraction.
src/Appwrite/Bus/Listeners/Usage.php Listener updated to publish via new message/publisher or accumulate in context.
app/worker.php Worker bootstrap now registers usage + publisherForUsage resources.
app/init/resources.php HTTP bootstrap now registers usage + publisherForUsage and rewires DB usage listener.
app/controllers/shared/api.php API shutdown hook now publishes accumulated usage via publisherForUsage.
app/controllers/api/account.php Account controller routes switched to Context injection for usage.
app/cli.php CLI resource renamed to usage (currently still returns StatsUsage).
CONTRIBUTING.md Docs updated for new context/publisher usage flow.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 196 to 198
$usage
->setProject($project)
->trigger();
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

Context doesn't define setProject()/trigger() (those were on Event\StatsUsage). This will fatally error at runtime. For workers, inject publisherForUsage, build an Event\Message\Usage from the context (project/metrics/reduce), enqueue it, and reset the context (or switch back to StatsUsage).

Copilot uses AI. Check for mistakes.
metrics: $usage->getMetrics(),
reduce: $usage->getReduce()
);
$publisherForUsage->enqueue($message);
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

$publisherForUsage is used here but is not defined in this method's scope (and not passed as a parameter), which will cause an undefined variable error. Pass the publisher into processMigrationResourceStats(...) (or store it on the class) and use that reference to enqueue the message.

Copilot uses AI. Check for mistakes.
Comment on lines +1181 to +1185
@@ -1182,7 +1182,7 @@ protected function buildDeployment(
resource:$resource,
deployment: $deployment,
project: $project,
queue: $queueForStatsUsage
queue: $usage
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

sendUsage(...) is still type-hinted to accept StatsUsage, but the caller now passes a Context ($usage). This will cause a type error/undefined class issue (the StatsUsage import was removed) and the method still relies on trigger(). Update sendUsage to work with Context + publisherForUsage (publish and reset), or keep using StatsUsage consistently.

Copilot uses AI. Check for mistakes.
app/cli.php Outdated
return $publisher;
}, ['publisher']);
$setResource('queueForStatsUsage', function (Publisher $publisher) {
$setResource('usage', function (Publisher $publisher) {
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

The usage resource is now named like the new Appwrite\Usage\Context, but it still returns Event\StatsUsage. This is inconsistent with the rest of the refactor (and will break commands that inject usage expecting a Context). Either keep this as queueForStatsUsage (returning StatsUsage) or update CLI resources to provide both usage (Context) and publisherForUsage (new publisher).

Suggested change
$setResource('usage', function (Publisher $publisher) {
$setResource('queueForStatsUsage', function (Publisher $publisher) {

Copilot uses AI. Check for mistakes.
public function toArray(): array
{
return [
'project' => $this->project->getArrayCopy(),
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

Usage::toArray() serializes the full project document via getArrayCopy(). Previously, Event::trigger() trimmed the project payload down to $id, $sequence, and database (Event::trimPayload()), which avoids queue bloat and reduces the risk of leaking project configuration data. Consider trimming the serialized project fields here (or passing a trimmed Document into the message).

Suggested change
'project' => $this->project->getArrayCopy(),
'project' => [
'$id' => $this->project->getAttribute('$id'),
'$sequence' => $this->project->getAttribute('$sequence'),
'database' => $this->project->getAttribute('database'),
],

Copilot uses AI. Check for mistakes.
Comment on lines +311 to 313
$usage
->setProject($project)
->addMetric(METRIC_MESSAGES, ($deliveredTotal + $errorTotal))
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

Context doesn't have setProject(). This code path will error after replacing StatsUsage with Context. Either remove setProject() usage here (accumulate only) and publish via the API shutdown hook (HTTP) or, since this is a worker, inject publisherForUsage and publish an Event\Message\Usage explicitly.

Copilot uses AI. Check for mistakes.
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Text;
use Appwrite\Usage\Context;
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

Duplicate use Appwrite\Usage\Context; import. In PHP this causes a fatal error (name already in use). Remove one of the two import statements.

Copilot uses AI. Check for mistakes.
Comment on lines 412 to 415
$usage
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
->setProject($project)
->trigger();
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

Context doesn't define setProject()/trigger(). After replacing StatsUsage with Context, this will fail. For HTTP routes, just accumulate metrics in the context and let the API shutdown hook publish; for non-HTTP execution, publish via publisherForUsage and reset the context.

Copilot uses AI. Check for mistakes.
Comment on lines 211 to 213
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
->setProject($project)
->trigger();
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

Context doesn't have setProject()/trigger(). This code will error after the switch from StatsUsage to Context. For HTTP requests, only accumulate metrics on $usage and rely on the shared API shutdown hook to publish them.

Suggested change
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
->setProject($project)
->trigger();
->addMetric(METRIC_AUTH_METHOD_PHONE, 1);

Copilot uses AI. Check for mistakes.
@loks0n loks0n changed the title refactor/usage publishing Refactor usage metrics to stateless publisher pattern Mar 4, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (1)
app/controllers/shared/api.php (1)

964-988: ⚠️ Potential issue | 🟠 Major

Usage publishing still bypasses _APP_USAGE_STATS in API shutdown.

This same issue persists: Line 995 checks the usage route group, but Line 964-Line 988 publish usage from the general api shutdown path without checking the global usage flag.

🔧 Proposed fix
-            // Publish usage metrics if context has data
-            if (!$usage->isEmpty()) {
+            $usageStatsEnabled = System::getEnv('_APP_USAGE_STATS', 'enabled') === 'enabled';
+            // Publish usage metrics only when enabled and context has data
+            if ($usageStatsEnabled && !$usage->isEmpty()) {
                 $metrics = $usage->getMetrics();

                 // Filter out API key disabled metrics using suffix pattern matching
                 $disabledMetrics = $apiKey?->getDisabledMetrics() ?? [];
                 if (!empty($disabledMetrics)) {
@@
                         return true;
                     }));
                 }
+
+                if (empty($metrics) && empty($usage->getReduce())) {
+                    return;
+                }

                 $message = new \Appwrite\Event\Message\Usage(
                     project: $project,
                     metrics: $metrics,
                     reduce: $usage->getReduce()
                 );

                 $publisherForUsage->enqueue($message);
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/shared/api.php` around lines 964 - 988, The shutdown path
publishes usage metrics regardless of the global usage toggle; update the block
that builds and enqueues the Usage message (references: $usage,
$usage->isEmpty(), $usage->getMetrics(), $usage->getReduce(),
$apiKey?->getDisabledMetrics(), $publisherForUsage->enqueue) to first check the
global flag (e.g. _APP_USAGE_STATS or the existing configuration getter used
elsewhere for the "usage" route) and skip building/enqueueing when usage
publishing is disabled; ensure the same feature-flag check used in the `usage`
route group is applied here before filtering metrics and creating the
\Appwrite\Event\Message\Usage message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/controllers/shared/api.php`:
- Line 40: The new import Appwrite\Usage\Context is out of PSR-12 alphabetical
order; update the use statements in app/controllers/shared/api.php so imports
are alphabetically ordered (and grouped) per PSR-12, placing
Appwrite\Usage\Context in its correct position among other Appwrite\* imports;
adjust any empty lines between groups if necessary to match the project's import
grouping conventions.

In `@app/init/resources.php`:
- Line 74: The new import "UsageContext" is out of PSR-12 ordered_imports order
and causes lint to fail; reorder the use statements in app/init/resources.php so
that "use Appwrite\Usage\Context as UsageContext;" is placed alphabetically
among the other use/import lines (keeping the same alias UsageContext) to
satisfy PSR-12 ordering and rerun the linter.
- Around line 153-156: The publisherForUsage resource currently constructs the
queue with new \Utopia\Queue\Queue($queueName) which omits the driver and can
cause mismatched backends; update the factory for publisherForUsage to pass the
explicit 'utopia-queue' driver when instantiating \Utopia\Queue\Queue (i.e.,
call the constructor with the driver parameter alongside $queueName) so it
matches the other instantiations in app/worker.php and app/cli.php and ensures
the Usage publisher and workers use the same queue backend.

In `@app/worker.php`:
- Line 50: The PSR-12 linter flags an out-of-order import: move the
Appwrite\Usage\Context use statement so imports are alphabetically ordered and
grouped consistently with the existing import block; update the import list near
where Context is declared (the use Appwrite\Usage\Context line) to its correct
position among other Appwrite\* imports to satisfy ordered_imports.

In `@src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php`:
- Around line 411-414: The code calls undefined methods setProject() and
trigger() on the Appwrite\Usage\Context instance ($usage) which will fatal;
change the chain so you only call $usage->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
(i.e., remove ->setProject($project)->trigger()) in Create.php (function
handling membership creation) and apply the same fix in
src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php
where the identical pattern appears.

---

Duplicate comments:
In `@app/controllers/shared/api.php`:
- Around line 964-988: The shutdown path publishes usage metrics regardless of
the global usage toggle; update the block that builds and enqueues the Usage
message (references: $usage, $usage->isEmpty(), $usage->getMetrics(),
$usage->getReduce(), $apiKey?->getDisabledMetrics(),
$publisherForUsage->enqueue) to first check the global flag (e.g.
_APP_USAGE_STATS or the existing configuration getter used elsewhere for the
"usage" route) and skip building/enqueueing when usage publishing is disabled;
ensure the same feature-flag check used in the `usage` route group is applied
here before filtering metrics and creating the \Appwrite\Event\Message\Usage
message.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9b8b29ef-948f-4d52-ab07-69516a7235eb

📥 Commits

Reviewing files that changed from the base of the PR and between 2a16657 and 6f45f2e.

📒 Files selected for processing (11)
  • app/cli.php
  • app/controllers/shared/api.php
  • app/init/resources.php
  • app/worker.php
  • src/Appwrite/Event/Publisher/Base.php
  • src/Appwrite/Event/Publisher/Usage.php
  • src/Appwrite/Event/StatsUsage.php
  • src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
  • src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php
  • src/Appwrite/Usage/Context.php
  • tests/e2e/General/UsageTest.php
💤 Files with no reviewable changes (1)
  • src/Appwrite/Event/StatsUsage.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/Appwrite/Event/Publisher/Base.php
  • src/Appwrite/Event/Publisher/Usage.php

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 4, 2026

🔄 PHP-Retry Summary

Flaky tests detected across commits:

Commit 06f5325 - 3 flaky tests
Test Retries Total Time Details
UsageTest::testDatabaseStatsCollectionsAPI 1 10.28s Logs
RealtimeCustomClientQueryTest::testDatabaseChannelWithQuery 1 42.57s Logs
RealtimeCustomClientTest::testChannelsTablesDB 1 30.27s Logs
Commit 9179be3 - 5 flaky tests
Test Retries Total Time Details
UsageTest::testDatabaseStatsCollectionsAPI 1 10.27s Logs
LegacyConsoleClientTest::testEnforceCollectionAndDocumentPermissions 1 240.38s Logs
LegacyCustomServerTest::testAttributeResponseModels 1 240.79s Logs
TablesDBCustomClientTest::testSpatialColCreateOnExistingDataWithDefaults 1 241.26s Logs
TablesDBTransactionPermissionsCustomClientTest::testCollectionCreatePermissionDenied 1 241.29s Logs
Commit 704fbe0 - 2 flaky tests
Test Retries Total Time Details
UsageTest::testDatabaseStatsCollectionsAPI 1 10.28s Logs
TablesDBTransactionsConsoleClientTest::testCommit 1 240.66s Logs
Commit a804cba - 1 flaky test
Test Retries Total Time Details
UsageTest::testDatabaseStatsCollectionsAPI 1 10.26s Logs
Commit 0295e27 - 4 flaky tests
Test Retries Total Time Details
UsageTest::testDatabaseStatsCollectionsAPI 1 10.34s Logs
TablesDBCustomServerTest::testInvalidDocumentStructure 1 241.73s Logs
LegacyTransactionsConsoleClientTest::testBulkUpsert 1 241.08s Logs
LegacyTransactionsCustomClientTest::testConcurrentTransactionConflicts 1 240.86s Logs

Note: Flaky test results are tracked for the last 5 commits

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/e2e/General/UsageTest.php (1)

46-77: ⚠️ Potential issue | 🟡 Minor

Remove duplicate trait alias declarations.

Several SitesBase method aliases are declared twice. While PHP accepts duplicate aliases, this appears to be a copy-paste error.

Duplicate lines:

  • Line 51 duplicates line 50 (listVariables as listVariablesSite)
  • Line 53 duplicates line 52 (updateVariable as updateVariableSite)
  • Line 55 duplicates line 54 (deleteVariable as deleteVariableSite)
  • Line 57 duplicates line 56 (getDeployment as getDeploymentSite)
  • Line 59 duplicates line 58 (listDeployments as listDeploymentsSite)
  • Line 61 duplicates line 60 (deleteDeployment as deleteDeploymentSite)
  • Line 63 duplicates line 62 (setupDuplicateDeployment as setupDuplicateDeploymentSite)
  • Line 65 duplicates line 64 (createDuplicateDeployment as createDuplicateDeploymentSite)
  • Line 67 duplicates line 66 (createTemplateDeployment as createTemplateDeploymentSite)
  • Line 69 duplicates line 68 (getUsage as getUsageSite)
  • Line 71 duplicates line 70 (getTemplate as getTemplateSite)
  • Line 73 duplicates line 72 (getDeploymentDownload as getDeploymentDownloadSite)
  • Line 75 duplicates line 74 (cancelDeployment as cancelDeploymentSite)
🧹 Proposed fix to remove duplicates
         SitesBase::createDeployment as createDeploymentSite;
         SitesBase::setupDeployment as setupDeploymentSite;
         SitesBase::createVariable as createVariableSite;
         SitesBase::getVariable as getVariableSite;
         SitesBase::listVariables as listVariablesSite;
-        SitesBase::listVariables as listVariablesSite;
         SitesBase::updateVariable as updateVariableSite;
-        SitesBase::updateVariable as updateVariableSite;
         SitesBase::deleteVariable as deleteVariableSite;
-        SitesBase::deleteVariable as deleteVariableSite;
         SitesBase::getDeployment as getDeploymentSite;
-        SitesBase::getDeployment as getDeploymentSite;
         SitesBase::listDeployments as listDeploymentsSite;
-        SitesBase::listDeployments as listDeploymentsSite;
         SitesBase::deleteDeployment as deleteDeploymentSite;
-        SitesBase::deleteDeployment as deleteDeploymentSite;
         SitesBase::setupDuplicateDeployment as setupDuplicateDeploymentSite;
-        SitesBase::setupDuplicateDeployment as setupDuplicateDeploymentSite;
         SitesBase::createDuplicateDeployment as createDuplicateDeploymentSite;
-        SitesBase::createDuplicateDeployment as createDuplicateDeploymentSite;
         SitesBase::createTemplateDeployment as createTemplateDeploymentSite;
-        SitesBase::createTemplateDeployment as createTemplateDeploymentSite;
         SitesBase::getUsage as getUsageSite;
-        SitesBase::getUsage as getUsageSite;
         SitesBase::getTemplate as getTemplateSite;
-        SitesBase::getTemplate as getTemplateSite;
         SitesBase::getDeploymentDownload as getDeploymentDownloadSite;
-        SitesBase::getDeploymentDownload as getDeploymentDownloadSite;
         SitesBase::cancelDeployment as cancelDeploymentSite;
-        SitesBase::cancelDeployment as cancelDeploymentSite;
         SitesBase::listSpecifications as listSpecificationsSite;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/General/UsageTest.php` around lines 46 - 77, The trait alias block
contains duplicate alias declarations from SitesBase (e.g., "listVariables as
listVariablesSite", "updateVariable as updateVariableSite", "deleteVariable as
deleteVariableSite", "getDeployment as getDeploymentSite", "listDeployments as
listDeploymentsSite", "deleteDeployment as deleteDeploymentSite",
"setupDuplicateDeployment as setupDuplicateDeploymentSite",
"createDuplicateDeployment as createDuplicateDeploymentSite",
"createTemplateDeployment as createTemplateDeploymentSite", "getUsage as
getUsageSite", "getTemplate as getTemplateSite", "getDeploymentDownload as
getDeploymentDownloadSite", "cancelDeployment as cancelDeploymentSite"); remove
the duplicated lines so each alias appears only once in the trait use block
referencing SitesBase, leaving a single alias declaration for each unique
method.
♻️ Duplicate comments (4)
src/Appwrite/Platform/Workers/Migrations.php (1)

107-108: ⚠️ Potential issue | 🔴 Critical

Critical: $publisherForUsage not propagated through the call chain.

The $publisherForUsage parameter is received in action() (line 108) but is never passed to processMigration() (lines 148-155), which in turn doesn't pass it to processMigrationResourceStats() (line 798). However, line 853 attempts to use $publisherForUsage->enqueue($message), which will cause a fatal "undefined variable" error when completed migrations attempt to publish usage statistics.

Required fix to propagate $publisherForUsage
             $this->processMigration(
                 $migration,
                 $queueForRealtime,
                 $queueForMails,
                 $usage,
+                $publisherForUsage,
                 $platform,
                 $authorization
             );
@@
     protected function processMigration(
         Document $migration,
         Realtime $queueForRealtime,
         Mail $queueForMails,
         Context $usage,
+        \Appwrite\Event\Publisher\Usage $publisherForUsage,
         array $platform,
         Authorization $authorization,
     ): void {
@@
                         $this->processMigrationResourceStats(
                             $resource,
                             $usage,
                             $project,
                             $migration->getAttribute('source'),
                             $authorization,
-                            $migration->getAttribute('resourceId')
+                            $migration->getAttribute('resourceId'),
+                            $publisherForUsage
                         );
@@
-    private function processMigrationResourceStats(array $resources, Context $usage, Document $projectDocument, string $source, Authorization $authorization, ?string $resourceId)
+    private function processMigrationResourceStats(array $resources, Context $usage, Document $projectDocument, string $source, Authorization $authorization, ?string $resourceId, \Appwrite\Event\Publisher\Usage $publisherForUsage): void

Also applies to: 148-155, 340-340, 798-798, 848-854

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Workers/Migrations.php` around lines 107 - 108, The
$publisherForUsage parameter passed into action() is not forwarded and thus is
undefined where used; update the call chain to accept and propagate this
dependency: add a $publisherForUsage parameter to processMigration() and any
intermediate callers (and signatures that currently call processMigration()),
and then pass that same $publisherForUsage into processMigrationResourceStats();
finally ensure processMigrationResourceStats() signature accepts and uses
$publisherForUsage when calling ->enqueue($message). Update all affected
function signatures and call sites (action(), processMigration(),
processMigrationResourceStats() and any callers) to include and forward
$publisherForUsage.
src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php (1)

411-414: ⚠️ Potential issue | 🔴 Critical

Remove invalid method chain on Context usage accumulator.

Line 413 and Line 414 call methods that do not belong to Appwrite\Usage\Context, which will fatal on the phone-invite flow.

Proposed fix
                 $usage
-                    ->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
-                    ->setProject($project)
-                    ->trigger();
+                    ->addMetric(METRIC_AUTH_METHOD_PHONE, 1);
#!/bin/bash
set -euo pipefail

echo "=== Context API surface ==="
rg -n --type=php 'function (addMetric|setProject|trigger)\(' src/Appwrite/Usage/Context.php

echo
echo "=== Usage chain in memberships create ==="
sed -n '405,416p' src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php | cat -n
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php` around lines
411 - 414, The chain on the Context accumulator
($usage->addMetric(...)->setProject(...)->trigger()) is invalid because
Appwrite\Usage\Context does not implement setProject or trigger; remove the
chained calls and only call the methods that exist on the Context instance
(e.g., call $usage->addMetric(METRIC_AUTH_METHOD_PHONE, 1);). If you need to set
the project or trigger processing, invoke those operations using the correct
API/instance that exposes setProject/trigger (consult Appwrite\Usage\Context
class for the proper methods) rather than chaining them off $usage.
src/Appwrite/Platform/Workers/Messaging.php (1)

311-323: ⚠️ Potential issue | 🔴 Critical

Context is used with removed publish APIs (setProject/trigger) and can fatal at runtime.

Line 312 and Line 322 call methods that are not part of the new accumulator-style Appwrite\Usage\Context. This external-send path will break when executed. Keep this block to metric accumulation only, then publish via Appwrite\Event\Publisher\Usage + Appwrite\Event\Message\Usage at worker completion.

Suggested refactor direction
- $usage
-     ->setProject($project)
-     ->addMetric(...)
-     ...
-     ->trigger();
+ $usage
+     ->addMetric(...)
+     ...
+ ;
+
+ // after processing (once):
+ // $publisherForUsage->enqueue(new UsageMessage(
+ //     project: $project,
+ //     metrics: $usage->getMetrics(),
+ //     reduce: $usage->getReduce()
+ // ));
+ // $usage->reset();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Workers/Messaging.php` around lines 311 - 323, The
block using $usage currently calls removed publish APIs setProject() and
trigger()—remove those calls and keep only the accumulation of metrics on $usage
(the addMetric(...) calls that reference METRIC_MESSAGES, METRIC_MESSAGES_SENT,
METRIC_MESSAGES_FAILED, METRIC_MESSAGES_TYPE*, METRIC_MESSAGES_TYPE_PROVIDER*
and use $provider->getAttribute('type') and ->getAttribute('provider') as well
as $deliveredTotal/$errorTotal); then ensure publishing is done later at worker
completion via the new publisher APIs Appwrite\Event\Publisher\Usage and
Appwrite\Event\Message\Usage (i.e. stop calling ->setProject(...) and
->trigger() here and move publication to the worker teardown using the Usage
publishers).
app/init/resources.php (1)

154-159: ⚠️ Potential issue | 🟠 Major

Use consistent queue driver for publisherForUsage.

The Queue constructor at line 156 is missing the 'utopia-queue' driver parameter. Based on past review comments, both app/worker.php and app/cli.php instantiate the same resource with 'utopia-queue', which could cause usage messages to be published to a different backend than the workers consume from.

Suggested fix
 Http::setResource('publisherForUsage', function (Publisher $publisher) {
     $queueName = System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME);
-    $queue = new \Utopia\Queue\Queue($queueName);
+    $queue = new \Utopia\Queue\Queue($queueName, 'utopia-queue');
 
     return new \Appwrite\Event\Publisher\Usage($publisher, $queue);
 }, ['publisher']);

,

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/init/resources.php` around lines 154 - 159, The publisherForUsage
resource constructs a Queue without the driver, causing inconsistent backends;
update the factory that returns new \Appwrite\Event\Publisher\Usage in
publisherForUsage so the \Utopia\Queue\Queue is instantiated with the explicit
'utopia-queue' driver (i.e. pass the driver as the second argument to the Queue
constructor) so the Usage publisher uses the same queue driver as app/worker.php
and app/cli.php.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php`:
- Around line 210-213: The code is calling nonexistent Context methods
setProject() and trigger() on the $usage object after addMetric, causing a fatal
error; update the usage accumulation to only call
$usage->addMetric(METRIC_AUTH_METHOD_PHONE, 1) (no chaining) so metrics are
added to the Context and rely on the existing shutdown/completion flow to
publish metrics, keeping references to $usage, addMetric,
METRIC_AUTH_METHOD_PHONE and $project intact but removing setProject() and
trigger() calls.

In `@src/Appwrite/Platform/Modules/Functions/Workers/Builds.php`:
- Line 1256: Remove the unused local variable $key by deleting the assignment
line that sets $key = $resource->getAttribute('runtime'); in the Builds.php
worker code (the assignment is the only use of the local $key variable); if the
runtime attribute is needed elsewhere, access it directly where required (e.g.,
use $resource->getAttribute('runtime') inline) otherwise delete the assignment
to eliminate the dead variable.

In `@src/Appwrite/Platform/Workers/Webhooks.php`:
- Around line 196-198: The code incorrectly uses Context-style fluent calls
($usage->setProject(...)->trigger()) which don't exist for the new
Appwrite\Usage\Context accumulator; replace these with accumulating metrics via
the Context instance's addMetric() calls and then publish using
publisherForUsage with a Message\Usage payload. Specifically, stop calling
setProject and trigger on $usage, instead call addMetric(...) on the
Context/usage accumulator to register the project-related metric(s) and invoke
publisherForUsage(...)->publish(new Message\Usage(...)) (or the existing
publisher API) to emit the usage message. Ensure you reference the Context
accumulator variable ($usage), the addMetric() method for accumulation, and
publisherForUsage plus Message\Usage for publishing so webhook execution no
longer relies on nonexistent setProject/trigger methods.

In `@src/Appwrite/Usage/Context.php`:
- Around line 16-24: The Context object is being used with non-existent methods
setProject() and trigger() in Memberships/Create.php; replace that flow with the
established usage pattern: accumulate metrics on the Context via addMetric() and
addReduce(), then build a Usage message using $context->getMetrics() and
$context->getReduce(), enqueue it using $publisherForUsage->enqueue($message),
and finally call $context->reset(); ensure you remove calls to setProject() and
trigger() and follow the same sequence as other modules (see Context::addMetric,
Context::addReduce, Context::getMetrics, Context::getReduce, Context::reset and
publisherForUsage->enqueue for reference).

In `@tests/e2e/General/UsageTest.php`:
- Around line 188-189: The #[Depends] attribute values reference old camelCase
test names and must be updated to the actual snake_case method names so PHPUnit
can resolve dependencies; update each attribute (e.g., change
#[Depends('testPrepareUsersStats')] to #[Depends('test_prepare_users_stats')]
and similarly for testUsersStats → test_users_stats, testPrepareStorageStats →
test_prepare_storage_stats, testStorageStats → test_storage_stats,
testPrepareDatabaseStatsCollectionsAPI →
test_prepare_database_stats_collections_api, testDatabaseStatsCollectionsAPI →
test_database_stats_collections_api, testPrepareDatabaseStatsTablesAPI →
test_prepare_database_stats_tables_api, testDatabaseStatsTablesAPI →
test_database_stats_tables_api, testPrepareFunctionsStats →
test_prepare_functions_stats, testPrepareSitesStats → test_prepare_sites_stats,
and testFunctionsStats → test_functions_stats so each #[Depends] string exactly
matches its corresponding test method name (look for the attributes near the
methods test_users_stats, test_prepare_users_stats, test_storage_stats,
test_prepare_storage_stats, test_database_stats_collections_api,
test_prepare_database_stats_collections_api, test_database_stats_tables_api,
test_prepare_database_stats_tables_api, test_functions_stats,
test_prepare_functions_stats, and test_prepare_sites_stats).

---

Outside diff comments:
In `@tests/e2e/General/UsageTest.php`:
- Around line 46-77: The trait alias block contains duplicate alias declarations
from SitesBase (e.g., "listVariables as listVariablesSite", "updateVariable as
updateVariableSite", "deleteVariable as deleteVariableSite", "getDeployment as
getDeploymentSite", "listDeployments as listDeploymentsSite", "deleteDeployment
as deleteDeploymentSite", "setupDuplicateDeployment as
setupDuplicateDeploymentSite", "createDuplicateDeployment as
createDuplicateDeploymentSite", "createTemplateDeployment as
createTemplateDeploymentSite", "getUsage as getUsageSite", "getTemplate as
getTemplateSite", "getDeploymentDownload as getDeploymentDownloadSite",
"cancelDeployment as cancelDeploymentSite"); remove the duplicated lines so each
alias appears only once in the trait use block referencing SitesBase, leaving a
single alias declaration for each unique method.

---

Duplicate comments:
In `@app/init/resources.php`:
- Around line 154-159: The publisherForUsage resource constructs a Queue without
the driver, causing inconsistent backends; update the factory that returns new
\Appwrite\Event\Publisher\Usage in publisherForUsage so the \Utopia\Queue\Queue
is instantiated with the explicit 'utopia-queue' driver (i.e. pass the driver as
the second argument to the Queue constructor) so the Usage publisher uses the
same queue driver as app/worker.php and app/cli.php.

In `@src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php`:
- Around line 411-414: The chain on the Context accumulator
($usage->addMetric(...)->setProject(...)->trigger()) is invalid because
Appwrite\Usage\Context does not implement setProject or trigger; remove the
chained calls and only call the methods that exist on the Context instance
(e.g., call $usage->addMetric(METRIC_AUTH_METHOD_PHONE, 1);). If you need to set
the project or trigger processing, invoke those operations using the correct
API/instance that exposes setProject/trigger (consult Appwrite\Usage\Context
class for the proper methods) rather than chaining them off $usage.

In `@src/Appwrite/Platform/Workers/Messaging.php`:
- Around line 311-323: The block using $usage currently calls removed publish
APIs setProject() and trigger()—remove those calls and keep only the
accumulation of metrics on $usage (the addMetric(...) calls that reference
METRIC_MESSAGES, METRIC_MESSAGES_SENT, METRIC_MESSAGES_FAILED,
METRIC_MESSAGES_TYPE*, METRIC_MESSAGES_TYPE_PROVIDER* and use
$provider->getAttribute('type') and ->getAttribute('provider') as well as
$deliveredTotal/$errorTotal); then ensure publishing is done later at worker
completion via the new publisher APIs Appwrite\Event\Publisher\Usage and
Appwrite\Event\Message\Usage (i.e. stop calling ->setProject(...) and
->trigger() here and move publication to the worker teardown using the Usage
publishers).

In `@src/Appwrite/Platform/Workers/Migrations.php`:
- Around line 107-108: The $publisherForUsage parameter passed into action() is
not forwarded and thus is undefined where used; update the call chain to accept
and propagate this dependency: add a $publisherForUsage parameter to
processMigration() and any intermediate callers (and signatures that currently
call processMigration()), and then pass that same $publisherForUsage into
processMigrationResourceStats(); finally ensure processMigrationResourceStats()
signature accepts and uses $publisherForUsage when calling ->enqueue($message).
Update all affected function signatures and call sites (action(),
processMigration(), processMigrationResourceStats() and any callers) to include
and forward $publisherForUsage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2d1e1f6b-2665-4e04-8990-812222cfcdbe

📥 Commits

Reviewing files that changed from the base of the PR and between 6f45f2e and e9a1923.

📒 Files selected for processing (31)
  • app/cli.php
  • app/controllers/api/account.php
  • app/controllers/shared/api.php
  • app/init/resources.php
  • app/worker.php
  • src/Appwrite/Event/Message/Usage.php
  • src/Appwrite/Event/Publisher/Base.php
  • src/Appwrite/Event/Publisher/Usage.php
  • src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php
  • src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
  • src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
  • src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
  • src/Appwrite/Platform/Modules/Health/Http/Health/Queue/StatsUsage/Get.php
  • src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php
  • src/Appwrite/Platform/Workers/Messaging.php
  • src/Appwrite/Platform/Workers/Migrations.php
  • src/Appwrite/Platform/Workers/Webhooks.php
  • src/Appwrite/Usage/Context.php
  • tests/e2e/General/UsageTest.php
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php
  • src/Appwrite/Event/Publisher/Usage.php
  • app/controllers/api/account.php
  • src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php

{
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
$key = $resource->getAttribute('runtime');
$key = $resource->getAttribute('runtime');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove unused local variable $key.

Line 1256 assigns $key but never uses it.

Proposed fix
-        $key = $resource->getAttribute('runtime');
         $runtime = match ($resource->getCollection()) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$key = $resource->getAttribute('runtime');
$runtime = match ($resource->getCollection()) {
🧰 Tools
🪛 PHPMD (2.15.0)

[warning] 1256-1256: Avoid unused local variables such as '$key'. (undefined)

(UnusedLocalVariable)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Modules/Functions/Workers/Builds.php` at line 1256,
Remove the unused local variable $key by deleting the assignment line that sets
$key = $resource->getAttribute('runtime'); in the Builds.php worker code (the
assignment is the only use of the local $key variable); if the runtime attribute
is needed elsewhere, access it directly where required (e.g., use
$resource->getAttribute('runtime') inline) otherwise delete the assignment to
eliminate the dead variable.

@loks0n loks0n force-pushed the refactor/usage-publishing branch from e9a1923 to 38b2eed Compare March 11, 2026 14:10
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

♻️ Duplicate comments (7)
src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php (1)

416-419: ⚠️ Potential issue | 🔴 Critical

Context has no setProject() or trigger() methods—this will cause a fatal error at runtime.

The code still calls undefined methods on the Context instance. Per the new pattern, only addMetric() should be called. The publishing is handled separately by the shutdown hook via Publisher\Usage.

Proposed fix
-                $usage
-                    ->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
-                    ->setProject($project)
-                    ->trigger();
+                $usage
+                    ->addMetric(METRIC_AUTH_METHOD_PHONE, 1);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php` around lines
416 - 419, The code is calling undefined methods setProject() and trigger() on
the $usage Context instance; remove those calls and only call
$usage->addMetric(METRIC_AUTH_METHOD_PHONE, 1) so the Context usage metric is
recorded and publishing is left to the shutdown hook (Publisher\Usage). Update
the block around $usage (the Context instance used for usage metrics in
Create.php) to stop chaining setProject() and trigger(), leaving a single
addMetric(...) call.
src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php (1)

77-84: ⚠️ Potential issue | 🟡 Minor

Remove the dead $user dependency from this action.

Document $user is still unused after the usage-context swap, so ->inject('user') just widens the signature and keeps PHPMD unhappy.

🧹 Suggested cleanup
-            ->inject('user')
             ->inject('usage')
@@
-    public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, int $ttl, UtopiaResponse $response, Database $dbForProject, Document $user, Context $usage, TransactionState $transactionState, Authorization $authorization): void
+    public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, int $ttl, UtopiaResponse $response, Database $dbForProject, Context $usage, TransactionState $transactionState, Authorization $authorization): void
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php`
around lines 77 - 84, Remove the dead user dependency: delete the
->inject('user') call from the collection setup and remove the unused Document
$user parameter from the action method signature (public function action(...,
Context $usage, TransactionState $transactionState, Authorization
$authorization): void) and any corresponding references, so the action no longer
accepts or expects a user object and PHPMD warnings are resolved.
src/Appwrite/Platform/Workers/Migrations.php (1)

148-155: ⚠️ Potential issue | 🔴 Critical

Pass the usage publisher through this call chain.

action() now receives $publisherForUsage, but this call still drops it. The completed-migration path later does $publisherForUsage->enqueue(...) out of scope in processMigrationResourceStats(), so successful migrations will crash as soon as they try to publish usage stats.

🛠️ Proposed fix
             $this->processMigration(
                 $migration,
                 $queueForRealtime,
                 $queueForMails,
                 $usage,
+                $publisherForUsage,
                 $platform,
                 $authorization
             );
@@
     protected function processMigration(
         Document $migration,
         Realtime $queueForRealtime,
         Mail $queueForMails,
         Context $usage,
+        \Appwrite\Event\Publisher\Usage $publisherForUsage,
         array $platform,
         Authorization $authorization,
     ): void {
@@
                         $this->processMigrationResourceStats(
                             $resource,
                             $usage,
                             $project,
                             $migration->getAttribute('source'),
                             $authorization,
-                            $migration->getAttribute('resourceId')
+                            $migration->getAttribute('resourceId'),
+                            $publisherForUsage
                         );
@@
-    private function processMigrationResourceStats(array $resources, Context $usage, Document $projectDocument, string $source, Authorization $authorization, ?string $resourceId)
+    private function processMigrationResourceStats(
+        array $resources,
+        Context $usage,
+        Document $projectDocument,
+        string $source,
+        Authorization $authorization,
+        ?string $resourceId,
+        \Appwrite\Event\Publisher\Usage $publisherForUsage,
+    ): void
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Workers/Migrations.php` around lines 148 - 155, The
call to processMigration is not forwarding the new $publisherForUsage, causing
action() to receive no publisher and later processMigrationResourceStats() to
call $publisherForUsage->enqueue(...) on null; update the processMigration(...)
invocation to include the $publisherForUsage argument and then propagate that
parameter through processMigration's signature and any internal calls to
action(), ensuring action() and processMigrationResourceStats() receive and use
the same $publisherForUsage variable.
src/Appwrite/Platform/Workers/Messaging.php (1)

311-323: ⚠️ Potential issue | 🔴 Critical

Replace the old self-publishing usage API here.

This block still treats Context like StatsUsage. In the new design, Context is only an accumulator, so setProject() / trigger() are no longer the right API. This path will fail at runtime unless it builds a \Appwrite\Event\Message\Usage and enqueues it through \Appwrite\Event\Publisher\Usage.

🛠️ Proposed fix
-            ->inject('usage')
+            ->inject('usage')
+            ->inject('publisherForUsage')
             ->callback($this->action(...));
@@
-        Device $deviceForFiles,
-        Context $usage
+        Device $deviceForFiles,
+        Context $usage,
+        \Appwrite\Event\Publisher\Usage $publisherForUsage
     ): void {
@@
-                    $this->sendExternalMessage($dbForProject, $message, $deviceForFiles, $project, $usage);
+                    $this->sendExternalMessage($dbForProject, $message, $deviceForFiles, $project, $usage, $publisherForUsage);
@@
-        Document $project,
-        Context $usage
+        Document $project,
+        Context $usage,
+        \Appwrite\Event\Publisher\Usage $publisherForUsage
     ): void {
@@
-                            $usage
-                                ->setProject($project)
-                                ->addMetric(METRIC_MESSAGES, ($deliveredTotal + $errorTotal))
+                            $usage
+                                ->addMetric(METRIC_MESSAGES, ($deliveredTotal + $errorTotal))
                                 ->addMetric(METRIC_MESSAGES_SENT, $deliveredTotal)
                                 ->addMetric(METRIC_MESSAGES_FAILED, $errorTotal)
                                 ->addMetric(str_replace('{type}', $provider->getAttribute('type'), METRIC_MESSAGES_TYPE), ($deliveredTotal + $errorTotal))
                                 ->addMetric(str_replace('{type}', $provider->getAttribute('type'), METRIC_MESSAGES_TYPE_SENT), $deliveredTotal)
                                 ->addMetric(str_replace('{type}', $provider->getAttribute('type'), METRIC_MESSAGES_TYPE_FAILED), $errorTotal)
                                 ->addMetric(str_replace(['{type}', '{provider}'], [$provider->getAttribute('type'), $provider->getAttribute('provider')], METRIC_MESSAGES_TYPE_PROVIDER), ($deliveredTotal + $errorTotal))
                                 ->addMetric(str_replace(['{type}', '{provider}'], [$provider->getAttribute('type'), $provider->getAttribute('provider')], METRIC_MESSAGES_TYPE_PROVIDER_SENT), $deliveredTotal)
-                                ->addMetric(str_replace(['{type}', '{provider}'], [$provider->getAttribute('type'), $provider->getAttribute('provider')], METRIC_MESSAGES_TYPE_PROVIDER_FAILED), $errorTotal)
-                                ->trigger();
+                                ->addMetric(str_replace(['{type}', '{provider}'], [$provider->getAttribute('type'), $provider->getAttribute('provider')], METRIC_MESSAGES_TYPE_PROVIDER_FAILED), $errorTotal);
+
+                            $publisherForUsage->enqueue(new \Appwrite\Event\Message\Usage(
+                                project: $project,
+                                metrics: $usage->getMetrics(),
+                                reduce: $usage->getReduce(),
+                            ));
+                            $usage->reset();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Workers/Messaging.php` around lines 311 - 323, The
current block is still using the old StatsUsage API (calling setProject() and
trigger() on the Context/usage object) which is invalid with the new
Context-as-accumulator design; replace this by constructing a
\Appwrite\Event\Message\Usage instance populated with project, metrics
(METRIC_MESSAGES, METRIC_MESSAGES_SENT, METRIC_MESSAGES_FAILED, the type- and
provider-scoped metrics) and totals (use $deliveredTotal/$errorTotal), then
enqueue that message via \Appwrite\Event\Publisher\Usage instead of calling
setProject()/trigger() on the Context/usage object; update the code that
references setProject(), trigger(), and $usage to use the new Usage message and
Publisher API.
src/Appwrite/Platform/Workers/Webhooks.php (1)

38-39: ⚠️ Potential issue | 🔴 Critical

Context still uses the removed self-publishing API here.

setProject() / trigger() do not belong to the new accumulator, so this will fatal on webhook delivery. Also, action() reuses the same $usage across multiple matching webhooks, so publish once from action() via publisherForUsage before the final exception check, or reset after each enqueue to avoid double-counting.

🔧 One safe way to apply the new pattern
+use Appwrite\Event\Message\Usage as UsageMessage;
+use Appwrite\Event\Publisher\Usage as UsagePublisher;
 use Appwrite\Usage\Context;
@@
-            ->inject('usage')
+            ->inject('publisherForUsage')
+            ->inject('usage')
             ->inject('log')
             ->inject('plan')
             ->callback($this->action(...));
@@
-    public function action(Message $message, Document $project, Database $dbForPlatform, Mail $queueForMails, Context $usage, Log $log, array $plan): void
+    public function action(Message $message, Document $project, Database $dbForPlatform, Mail $queueForMails, UsagePublisher $publisherForUsage, Context $usage, Log $log, array $plan): void
     {
         $this->errors = [];
@@
         foreach ($project->getAttribute('webhooks', []) as $webhook) {
             if (array_intersect($webhook->getAttribute('events', []), $events)) {
                 $this->execute($events, $webhookPayload, $webhook, $user, $project, $dbForPlatform, $queueForMails, $usage, $plan);
             }
         }
+
+        if ($usage->getMetrics() !== []) {
+            $publisherForUsage->enqueue(new UsageMessage(
+                project: $project,
+                metrics: $usage->getMetrics(),
+                reduce: $usage->getReduce(),
+            ));
+            $usage->reset();
+        }
 
         if (!empty($this->errors)) {
             throw new Exception(\implode(" / \n\n", $this->errors));
         }
@@
-        $usage
-            ->setProject($project)
-            ->trigger();
     }
#!/bin/bash
set -euo pipefail

echo "[Context API]"
rg -n --type=php 'function\s+(addMetric|addReduce|getMetrics|getReduce|reset|setProject|trigger)\b' src/Appwrite/Usage/Context.php

echo
echo "[Current Webhooks calls that should disappear]"
rg -n --type=php '\->(setProject|trigger)\(' src/Appwrite/Platform/Workers/Webhooks.php

echo
echo "[Current usage publisher pattern elsewhere in the repo]"
rg -n --type=php 'publisherForUsage|new UsageMessage|->enqueue\(' app src/Appwrite | sed -n '1,120p'

Expected result: Context.php should expose accumulator methods like addMetric, getMetrics, getReduce, and reset, not setProject / trigger; Webhooks.php should stop matching the second grep after the fix.

Also applies to: 55-55, 200-202

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Workers/Webhooks.php` around lines 38 - 39, The
Webhooks worker still calls the removed Context self-publishing API methods
(setProject, trigger) and reuses the same $usage accumulator across multiple
matching webhooks causing double-counting; update Webhooks.php to stop calling
Context::setProject and Context::trigger and instead use the new accumulator
methods (e.g., addMetric, getMetrics, getReduce, reset) on the Context/usage
object, and ensure publishing happens once per webhook action by invoking
publisherForUsage (or creating a new UsageMessage and enqueue) from action()
before the final exception check or by resetting $usage after each enqueue to
avoid reuse; locate references to setProject, trigger, action,
publisherForUsage, and $usage in the file and replace with the new accumulator
and publish pattern accordingly.
tests/e2e/General/UsageTest.php (1)

188-188: ⚠️ Potential issue | 🟠 Major

Update the stale #[Depends] targets to the renamed snake_case tests.

These attributes still point at the old camelCase method names, so PHPUnit will not resolve the dependency chain for the renamed tests.

🔧 Example fix pattern
-    #[Depends('testPrepareUsersStats')]
+    #[Depends('test_prepare_users_stats')]
     public function test_users_stats(array $data): array

-    #[Depends('testPrepareStorageStats')]
+    #[Depends('test_prepare_storage_stats')]
     public function test_storage_stats(array $data): array

-    #[Depends('testFunctionsStats')]
+    #[Depends('test_functions_stats')]
     public function test_custom_domains_function_stats(array $data): void

Apply the same snake_case rename to the remaining #[Depends] strings in this file.

#!/bin/bash
set -euo pipefail

# Verify that every #[Depends('...')] target exists as a declared method name.
python - <<'PY'
import pathlib, re

path = pathlib.Path("tests/e2e/General/UsageTest.php")
text = path.read_text()

methods = set(re.findall(r'public function (\w+)\s*\(', text))
pairs = re.findall(r"#\[Depends\('([^']+)'\)\]\s*public function (\w+)", text, re.S)

for dep, owner in pairs:
    status = "OK" if dep in methods else "MISSING"
    print(f"{owner}: depends on {dep} -> {status}")
PY

Expected result: every dependency reports -> OK.

Also applies to: 232-232, 365-365, 422-422, 597-597, 671-671, 852-852, 931-931, 1095-1095, 1233-1233, 1309-1309

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/General/UsageTest.php` at line 188, Update stale PHPUnit dependency
attributes: replace camelCase Depends targets with the renamed snake_case test
method names. Find occurrences like #[Depends('testPrepareUsersStats')] and
change the string to the actual declared method name (e.g.
test_prepare_users_stats) so PHPUnit can resolve dependencies; do the same for
all other #[Depends('...')] entries in this file that still reference camelCase
names, ensuring each Depends target exactly matches the corresponding public
function method name.
app/init/resources.php (1)

154-158: ⚠️ Potential issue | 🟠 Major

Pass the explicit queue driver into publisherForUsage.

Line 156 is still constructing the usage queue without the explicit 'utopia-queue' driver. If the worker/CLI side uses a driver-qualified queue, this publisher can write usage messages to a different backend than the consumers read from.

Suggested fix
 Http::setResource('publisherForUsage', function (Publisher $publisher) {
     $queueName = System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME);
-    $queue = new \Utopia\Queue\Queue($queueName);
+    $queue = new \Utopia\Queue\Queue($queueName, 'utopia-queue');
 
     return new \Appwrite\Event\Publisher\Usage($publisher, $queue);
 }, ['publisher']);

Verify that the usage queue is constructed consistently everywhere; expect app/cli.php and app/worker.php to pass 'utopia-queue' explicitly while this factory does not.

#!/bin/bash
set -euo pipefail

echo "=== publisherForUsage factory ==="
sed -n '151,159p' app/init/resources.php

echo
echo "=== usage queue constructions across app/ ==="
rg -n "STATS_USAGE_QUEUE_NAME|new \\\\Utopia\\\\Queue\\\\Queue\\(" app -C2

echo
echo "=== usage publisher implementation ==="
sed -n '1,160p' src/Appwrite/Event/Publisher/Usage.php
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/init/resources.php` around lines 154 - 158, The publisherForUsage factory
currently constructs the queue with new \Utopia\Queue\Queue($queueName) which
omits the explicit driver; update the Http::setResource('publisherForUsage')
factory to construct the queue with the explicit 'utopia-queue' driver (e.g.
pass the driver argument when creating \Utopia\Queue\Queue) so the returned new
\Appwrite\Event\Publisher\Usage($publisher, $queue) writes to the same backend
as the workers; mirror the same construction used in app/cli.php and
app/worker.php to ensure consistency.
🧹 Nitpick comments (2)
src/Appwrite/Platform/Modules/Databases/Http/Databases/Delete.php (1)

63-64: Drop the unused usage injection here.

action() never accepts a Context, so this route resolves a dependency it does not consume. Either wire Context $usage into the callback or remove the injection; right now this reads like an incomplete migration.

♻️ Suggested cleanup
-            ->inject('usage')
             ->callback($this->action(...));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Modules/Databases/Http/Databases/Delete.php` around
lines 63 - 64, The route definition is injecting 'usage' but the handler method
action() does not accept a Context or any parameter, so remove the unnecessary
injection to avoid resolving an unused dependency; locate the route builder call
that chains ->inject('usage') and ->callback($this->action(...)) and delete the
->inject('usage') call (or alternatively update the action method signature to
accept Context $usage if you intend to use it), ensuring the callback and
injection match.
app/controllers/shared/api.php (1)

971-996: Skip enqueueing when API-key filtering removes every metric.

After the suffix filter, $metrics can be empty even though $usage was not. Enqueuing an empty usage message just adds avoidable queue work at shutdown.

Proposed fix
-                $message = new \Appwrite\Event\Message\Usage(
-                    project: $project,
-                    metrics: $metrics,
-                    reduce: $usage->getReduce()
-                );
-
-                $publisherForUsage->enqueue($message);
+                if (! empty($metrics)) {
+                    $message = new \Appwrite\Event\Message\Usage(
+                        project: $project,
+                        metrics: $metrics,
+                        reduce: $usage->getReduce()
+                    );
+
+                    $publisherForUsage->enqueue($message);
+                }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/shared/api.php` around lines 971 - 996, After filtering out
disabled metrics, $metrics may become empty and you should avoid
creating/enqueueing an empty usage message; update the block handling $usage
(use symbols $metrics, $usage, $disabledMetrics, str_ends_with,
\Appwrite\Event\Message\Usage, and $publisherForUsage->enqueue) to check if
array is non-empty after the suffix filtering and only construct the Usage
message and call $publisherForUsage->enqueue when $metrics is not empty.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CONTRIBUTING.md`:
- Around line 503-506: The snippet constructs a UsageMessage with project:
$project but never shows where $project comes from; before calling new
UsageMessage(...) add the line that obtains the project from the worker flow
(for example, retrieve it from the incoming message payload or worker context
such as $payload['project'] or $worker->getProject()), then pass that variable
into UsageMessage; update the example to show the project extraction step that
precedes UsageMessage so readers can copy the flow end-to-end and reference the
same symbol ($project) used in the constructor.
- Around line 494-500: The compute metrics are currently multiplied by 1000,
producing milliseconds while the docs expect seconds; update both occurrences
that set METRIC_BUILDS_COMPUTE and METRIC_FUNCTION_ID_BUILDS_COMPUTE so they use
the raw duration in seconds (e.g., (int)$build->getAttribute('duration', 0) or
equivalent) instead of multiplying by 1000, ensuring both the
METRIC_BUILDS_COMPUTE and METRIC_FUNCTION_ID_BUILDS_COMPUTE assignments are
changed.
- Around line 502-509: The publish block can skip $usage->reset() if
$publisherForUsage->enqueue($message) throws, causing stale metrics to leak into
the next worker job; wrap the enqueue call in a try/finally so that
$usage->reset() always runs (create the UsageMessage as before, call
$publisherForUsage->enqueue($message) inside the try, and call $usage->reset()
in the finally block) to ensure metrics are cleared even on enqueue failure.

In `@src/Appwrite/Platform/Modules/Functions/Workers/Builds.php`:
- Around line 1231-1239: The MB-seconds metrics currently multiply by
$spec['memory'] which underreports builds when memory was bumped to a minimum;
change those three metrics (METRIC_BUILDS_MB_SECONDS,
METRIC_RESOURCE_TYPE_BUILDS_MB_SECONDS,
METRIC_RESOURCE_TYPE_ID_BUILDS_MB_SECONDS) to use the resolved memory value used
to create the runtime (i.e., the result of max($spec['memory'], $minMemory) –
the same resolved memory used when building the runtime) instead of
$spec['memory'], keeping the rest of the calculations (buildDuration, cpus)
unchanged and update both the resource-type and resource-id metric lines that
reference MB_SECONDS.
- Around line 396-403: The mv command is double-quoting arguments because the
code wraps escapeshellarg($from) and escapeshellarg($to) in extra double quotes,
causing literal quotes to be passed; update the Console::execute call in the
block handling $rootDirectory with spaces to call mv using the unquoted
escapeshellarg values (i.e., remove the surrounding " characters around
escapeshellarg($from) and escapeshellarg($to)) so mv receives correctly escaped,
quoted tokens for $from and $to.

In `@src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php`:
- Line 101: The action method in Create.php declares unused parameters
$timelimit and $plan; remove these two parameters from the action(string
$teamId, ...) signature in the Create class (and any related docblock/type
hints) so the method only accepts the used arguments, and update all call sites
that invoke Create::action to stop passing those two arguments; also update any
tests or factory/invocation code and imports that reference those parameters to
keep signatures consistent.

---

Duplicate comments:
In `@app/init/resources.php`:
- Around line 154-158: The publisherForUsage factory currently constructs the
queue with new \Utopia\Queue\Queue($queueName) which omits the explicit driver;
update the Http::setResource('publisherForUsage') factory to construct the queue
with the explicit 'utopia-queue' driver (e.g. pass the driver argument when
creating \Utopia\Queue\Queue) so the returned new
\Appwrite\Event\Publisher\Usage($publisher, $queue) writes to the same backend
as the workers; mirror the same construction used in app/cli.php and
app/worker.php to ensure consistency.

In
`@src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php`:
- Around line 77-84: Remove the dead user dependency: delete the
->inject('user') call from the collection setup and remove the unused Document
$user parameter from the action method signature (public function action(...,
Context $usage, TransactionState $transactionState, Authorization
$authorization): void) and any corresponding references, so the action no longer
accepts or expects a user object and PHPMD warnings are resolved.

In `@src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php`:
- Around line 416-419: The code is calling undefined methods setProject() and
trigger() on the $usage Context instance; remove those calls and only call
$usage->addMetric(METRIC_AUTH_METHOD_PHONE, 1) so the Context usage metric is
recorded and publishing is left to the shutdown hook (Publisher\Usage). Update
the block around $usage (the Context instance used for usage metrics in
Create.php) to stop chaining setProject() and trigger(), leaving a single
addMetric(...) call.

In `@src/Appwrite/Platform/Workers/Messaging.php`:
- Around line 311-323: The current block is still using the old StatsUsage API
(calling setProject() and trigger() on the Context/usage object) which is
invalid with the new Context-as-accumulator design; replace this by constructing
a \Appwrite\Event\Message\Usage instance populated with project, metrics
(METRIC_MESSAGES, METRIC_MESSAGES_SENT, METRIC_MESSAGES_FAILED, the type- and
provider-scoped metrics) and totals (use $deliveredTotal/$errorTotal), then
enqueue that message via \Appwrite\Event\Publisher\Usage instead of calling
setProject()/trigger() on the Context/usage object; update the code that
references setProject(), trigger(), and $usage to use the new Usage message and
Publisher API.

In `@src/Appwrite/Platform/Workers/Migrations.php`:
- Around line 148-155: The call to processMigration is not forwarding the new
$publisherForUsage, causing action() to receive no publisher and later
processMigrationResourceStats() to call $publisherForUsage->enqueue(...) on
null; update the processMigration(...) invocation to include the
$publisherForUsage argument and then propagate that parameter through
processMigration's signature and any internal calls to action(), ensuring
action() and processMigrationResourceStats() receive and use the same
$publisherForUsage variable.

In `@src/Appwrite/Platform/Workers/Webhooks.php`:
- Around line 38-39: The Webhooks worker still calls the removed Context
self-publishing API methods (setProject, trigger) and reuses the same $usage
accumulator across multiple matching webhooks causing double-counting; update
Webhooks.php to stop calling Context::setProject and Context::trigger and
instead use the new accumulator methods (e.g., addMetric, getMetrics, getReduce,
reset) on the Context/usage object, and ensure publishing happens once per
webhook action by invoking publisherForUsage (or creating a new UsageMessage and
enqueue) from action() before the final exception check or by resetting $usage
after each enqueue to avoid reuse; locate references to setProject, trigger,
action, publisherForUsage, and $usage in the file and replace with the new
accumulator and publish pattern accordingly.

In `@tests/e2e/General/UsageTest.php`:
- Line 188: Update stale PHPUnit dependency attributes: replace camelCase
Depends targets with the renamed snake_case test method names. Find occurrences
like #[Depends('testPrepareUsersStats')] and change the string to the actual
declared method name (e.g. test_prepare_users_stats) so PHPUnit can resolve
dependencies; do the same for all other #[Depends('...')] entries in this file
that still reference camelCase names, ensuring each Depends target exactly
matches the corresponding public function method name.

---

Nitpick comments:
In `@app/controllers/shared/api.php`:
- Around line 971-996: After filtering out disabled metrics, $metrics may become
empty and you should avoid creating/enqueueing an empty usage message; update
the block handling $usage (use symbols $metrics, $usage, $disabledMetrics,
str_ends_with, \Appwrite\Event\Message\Usage, and $publisherForUsage->enqueue)
to check if array is non-empty after the suffix filtering and only construct the
Usage message and call $publisherForUsage->enqueue when $metrics is not empty.

In `@src/Appwrite/Platform/Modules/Databases/Http/Databases/Delete.php`:
- Around line 63-64: The route definition is injecting 'usage' but the handler
method action() does not accept a Context or any parameter, so remove the
unnecessary injection to avoid resolving an unused dependency; locate the route
builder call that chains ->inject('usage') and ->callback($this->action(...))
and delete the ->inject('usage') call (or alternatively update the action method
signature to accept Context $usage if you intend to use it), ensuring the
callback and injection match.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 66078bc2-c8ec-443b-a381-ea4354d7779f

📥 Commits

Reviewing files that changed from the base of the PR and between e9a1923 and 38b2eed.

📒 Files selected for processing (50)
  • CONTRIBUTING.md
  • app/cli.php
  • app/controllers/api/account.php
  • app/controllers/shared/api.php
  • app/init/resources.php
  • app/worker.php
  • src/Appwrite/Bus/Listeners/Usage.php
  • src/Appwrite/Event/Message/Base.php
  • src/Appwrite/Event/Message/Usage.php
  • src/Appwrite/Event/Publisher/Base.php
  • src/Appwrite/Event/Publisher/Usage.php
  • src/Appwrite/Event/StatsUsage.php
  • src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php
  • src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Delete.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Delete.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Delete.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Update.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Decrement.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Increment.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Create.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Delete.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Get.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Update.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Upsert.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/XList.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php
  • src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
  • src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
  • src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Failed/Get.php
  • src/Appwrite/Platform/Modules/Health/Http/Health/Queue/StatsUsage/Get.php
  • src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php
  • src/Appwrite/Platform/Workers/Messaging.php
  • src/Appwrite/Platform/Workers/Migrations.php
  • src/Appwrite/Platform/Workers/Webhooks.php
  • src/Appwrite/Usage/Context.php
  • tests/e2e/General/UsageTest.php
💤 Files with no reviewable changes (1)
  • src/Appwrite/Event/StatsUsage.php
🚧 Files skipped from review as they are similar to previous changes (11)
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/XList.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Decrement.php
  • src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Update.php
  • src/Appwrite/Event/Publisher/Base.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Delete.php
  • src/Appwrite/Event/Message/Usage.php
  • src/Appwrite/Usage/Context.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Update.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Increment.php
  • src/Appwrite/Event/Message/Base.php

Comment on lines +502 to +509
// Publish the accumulated metrics (workers don't have shutdown hooks)
$message = new UsageMessage(
project: $project,
metrics: $usage->getMetrics(),
reduce: $usage->getReduce()
);
$publisherForUsage->enqueue($message);
$usage->reset();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reset the usage context in a finally block.

If enqueue() fails here, $usage->reset() is skipped and the next worker job can inherit stale metrics. The guide should model this as try/finally to avoid cross-job leakage.

Proposed doc fix
 // Publish the accumulated metrics (workers don't have shutdown hooks)
 $message = new UsageMessage(
     project: $project,
     metrics: $usage->getMetrics(),
     reduce: $usage->getReduce()
 );
-$publisherForUsage->enqueue($message);
-$usage->reset();
+try {
+    $publisherForUsage->enqueue($message);
+} finally {
+    $usage->reset();
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Publish the accumulated metrics (workers don't have shutdown hooks)
$message = new UsageMessage(
project: $project,
metrics: $usage->getMetrics(),
reduce: $usage->getReduce()
);
$publisherForUsage->enqueue($message);
$usage->reset();
// Publish the accumulated metrics (workers don't have shutdown hooks)
$message = new UsageMessage(
project: $project,
metrics: $usage->getMetrics(),
reduce: $usage->getReduce()
);
try {
$publisherForUsage->enqueue($message);
} finally {
$usage->reset();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CONTRIBUTING.md` around lines 502 - 509, The publish block can skip
$usage->reset() if $publisherForUsage->enqueue($message) throws, causing stale
metrics to leak into the next worker job; wrap the enqueue call in a try/finally
so that $usage->reset() always runs (create the UsageMessage as before, call
$publisherForUsage->enqueue($message) inside the try, and call $usage->reset()
in the finally block) to ensure metrics are cleared even on enqueue failure.

@loks0n loks0n force-pushed the refactor/usage-publishing branch 4 times, most recently from cd9fe93 to 936d4ce Compare March 11, 2026 15:47
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/Appwrite/Platform/Modules/Databases/Http/Databases/Delete.php (1)

63-67: ⚠️ Potential issue | 🟡 Minor

Mismatch between injected dependency and action signature.

Line 63 injects 'usage' but the action() method signature at line 67 doesn't include a Context $usage parameter. This injection is effectively unused.

If database deletion should track usage metrics, add the parameter:

-    public function action(string $databaseId, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
+    public function action(string $databaseId, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Context $usage): void

If no metrics tracking is needed for this endpoint, remove the injection:

             ->inject('queueForEvents')
-            ->inject('usage')
             ->callback($this->action(...));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Modules/Databases/Http/Databases/Delete.php` around
lines 63 - 67, The route builder injects 'usage' but the handler signature for
action() does not accept it, so either add a Context $usage parameter to the
action method (public function action(string $databaseId, UtopiaResponse
$response, Database $dbForProject, EventDatabase $queueForDatabase, Event
$queueForEvents, Context $usage): void) so the injected usage context is
consumed when tracking deletion metrics, or remove the ->inject('usage') call
from the route chain to avoid an unused injection; update references inside
action() accordingly (use $usage for metrics if added, or delete any code
assuming it if you remove the injection).
src/Appwrite/Platform/Workers/Messaging.php (1)

311-322: ⚠️ Potential issue | 🔴 Critical

Context class has no setProject() or trigger() methods—this will fatal at runtime.

The Appwrite\Usage\Context class only provides accumulation methods (addMetric(), addReduce(), etc.), not publishing methods. Lines 312 and 322 call setProject() and trigger() which don't exist and will cause fatal errors during message processing.

For workers, the correct pattern (per the CONTRIBUTING.md documentation) is to:

  1. Accumulate metrics via $usage->addMetric(...)
  2. Construct a UsageMessage with the accumulated metrics
  3. Publish via $publisherForUsage->enqueue($message)
  4. Reset with $usage->reset()

This worker needs publisherForUsage injected and the publishing logic refactored.

🔧 Suggested refactor direction
 public function __construct()
 {
     $this
         ->desc('Messaging worker')
         ->inject('message')
         ->inject('project')
         ->inject('log')
         ->inject('dbForProject')
         ->inject('deviceForFiles')
         ->inject('usage')
+        ->inject('publisherForUsage')
         ->callback($this->action(...));
 }

Then in the batch closure, replace:

-                        $usage
-                            ->setProject($project)
-                            ->addMetric(...)
-                            ...
-                            ->trigger();
+                        $usage
+                            ->addMetric(...)
+                            ...;

And publish accumulated metrics at the end of sendExternalMessage():

$message = new UsageMessage(
    project: $project,
    metrics: $usage->getMetrics(),
    reduce: $usage->getReduce()
);
try {
    $publisherForUsage->enqueue($message);
} finally {
    $usage->reset();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Workers/Messaging.php` around lines 311 - 322, The code
incorrectly calls non-existent Context methods setProject() and trigger() on
$usage; replace this publish pattern in sendExternalMessage() by accumulating
metrics with $usage->addMetric(...), then build a UsageMessage using
UsageMessage(project: $project, metrics: $usage->getMetrics(), reduce:
$usage->getReduce()), enqueue it via the injected
publisherForUsage->enqueue($message) inside a try/finally, and call
$usage->reset() in the finally block; remove calls to $usage->setProject() and
$usage->trigger() and ensure publisherForUsage is injected into the worker.
♻️ Duplicate comments (13)
src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php (1)

77-84: ⚠️ Potential issue | 🟡 Minor

Remove the unused $user injection and parameter.

The $user parameter is injected (line 77) but never used in the method body. Remove both ->inject('user') and the Document $user parameter from the method signature.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php`
around lines 77 - 84, Remove the unused user injection and parameter: delete the
->inject('user') call in the service builder chain and remove the Document $user
parameter from the action(...) method signature (the action method defined as
public function action(..., Document $user, Context $usage, ...)). Update any
related parameter ordering/typing in the action declaration to keep Context
$usage and subsequent parameters in the correct positions and ensure no
references to $user remain in the method body.
src/Appwrite/Platform/Workers/Migrations.php (1)

148-155: ⚠️ Potential issue | 🔴 Critical

Undefined variable: $publisherForUsage not propagated through call chain.

$publisherForUsage is received in action() (line 108) but never passed to processMigration() or processMigrationResourceStats(). At line 862, $publisherForUsage->enqueue($message) will fail with an undefined variable error when completed migrations attempt to publish usage statistics.

Proposed fix
         try {
             $this->processMigration(
                 $migration,
                 $queueForRealtime,
                 $queueForMails,
                 $usage,
+                $publisherForUsage,
                 $platform,
                 $authorization
             );
     protected function processMigration(
         Document $migration,
         Realtime $queueForRealtime,
         Mail $queueForMails,
         Context $usage,
+        \Appwrite\Event\Publisher\Usage $publisherForUsage,
         array $platform,
         Authorization $authorization,
     ): void {
                     foreach ($aggregatedResources as $resource) {
                         $this->processMigrationResourceStats(
                             $resource,
                             $usage,
                             $project,
                             $migration->getAttribute('source'),
                             $authorization,
-                            $migration->getAttribute('resourceId')
+                            $migration->getAttribute('resourceId'),
+                            $publisherForUsage
                         );
                     }
-    private function processMigrationResourceStats(array $resources, Context $usage, Document $projectDocument, string $source, Authorization $authorization, ?string $resourceId)
+    private function processMigrationResourceStats(array $resources, Context $usage, Document $projectDocument, string $source, Authorization $authorization, ?string $resourceId, \Appwrite\Event\Publisher\Usage $publisherForUsage): void

Also applies to: 346-352, 503-510, 807-807, 857-863

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Workers/Migrations.php` around lines 148 - 155, The
variable $publisherForUsage from action() is not passed into processMigration()
or processMigrationResourceStats(), causing an undefined variable when
processMigrationResourceStats() tries to call $publisherForUsage->enqueue(...).
Fix by adding a $publisherForUsage parameter to processMigration() and
processMigrationResourceStats(), update all internal calls (e.g., where
processMigration(...) is invoked) to pass the $publisherForUsage value through,
and ensure any helper calls within processMigration that call
processMigrationResourceStats also forward the same $publisherForUsage parameter
so the enqueue call uses the propagated publisher instance.
src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php (1)

209-212: ⚠️ Potential issue | 🔴 Critical

Fatal error: setProject() and trigger() do not exist on Context.

The Context class only provides addMetric(), addReduce(), getMetrics(), getReduce(), isEmpty(), and reset() methods. Lines 211-212 call setProject($project)->trigger() which will cause a runtime fatal error.

In the new pattern, metrics should be accumulated via Context without calling setProject() or trigger(). The publishing is handled by the API shutdown hook.

Proposed fix
                 $usage
-                    ->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
-                    ->setProject($project)
-                    ->trigger();
+                    ->addMetric(METRIC_AUTH_METHOD_PHONE, 1);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php`
around lines 209 - 212, The code is calling non-existent Context methods
setProject() and trigger() on $usage; remove the chaining and only call
$usage->addMetric(METRIC_AUTH_METHOD_PHONE, 1) so metrics are accumulated by the
Context, not published here—do not call setProject() or trigger(), the API
shutdown hook will handle publishing. Ensure no other code in Create.php tries
to chain setProject() or trigger() on the Context instance.
CONTRIBUTING.md (3)

500-507: ⚠️ Potential issue | 🟠 Major

Reset the usage context in a finally block.

If enqueue() throws, $usage->reset() is skipped and the next worker job may inherit stale metrics. Model this as try/finally to prevent cross-job leakage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CONTRIBUTING.md` around lines 500 - 507, Wrap the enqueue call in a
try/finally so $usage->reset() always runs even if
$publisherForUsage->enqueue($message) throws: create the UsageMessage as shown,
call $publisherForUsage->enqueue($message) inside a try block, and move
$usage->reset() into the finally block to guarantee the usage context is cleared
between jobs.

501-504: ⚠️ Potential issue | 🟡 Minor

Show where $project comes from in the worker flow.

UsageMessage is constructed with project: $project, but this example never introduces that variable. Show how the worker obtains the project so the snippet is copy-paste ready.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CONTRIBUTING.md` around lines 501 - 504, The snippet constructs a
UsageMessage with project: $project but never shows how $project is obtained;
before the UsageMessage creation, add and show the assignment that populates
$project in the worker flow (e.g., retrieving it from the job payload or context
such as $project = $job->getProject() or $project =
$this->getProjectFromContext()), so readers can copy-paste the example;
reference the UsageMessage construction and the exact variable name $project so
the source of that variable is clear.

492-498: ⚠️ Potential issue | 🟡 Minor

Align the compute metric units.

The example records duration * 1000 (milliseconds), but the metrics table at lines 388-390 documents builds.compute / executions.compute as seconds. The snippet and table should agree.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CONTRIBUTING.md` around lines 492 - 498, The compute metric units are
inconsistent: the example multiplies duration by 1000 (milliseconds) while the
docs list builds.compute/executions.compute as seconds; update the example so
both uses of duration (the calls to addMetric with METRIC_BUILDS_COMPUTE and
METRIC_FUNCTION_ID_BUILDS_COMPUTE) record seconds instead of milliseconds by
removing the * 1000 on $build->getAttribute('duration', 0) so the sample matches
the documented units.
src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php (1)

415-418: ⚠️ Potential issue | 🔴 Critical

Context class has no setProject() or trigger() methods—this will fatal at runtime.

The Appwrite\Usage\Context class only provides addMetric(), addReduce(), getMetrics(), getReduce(), isEmpty(), and reset(). Lines 417-418 call undefined methods which will cause a fatal error when this code path executes.

Remove the chained setProject() and trigger() calls. In the new pattern, metrics are accumulated via addMetric() and published automatically by the shutdown hook (for API requests) or explicitly via UsagePublisher (for workers).

🐛 Proposed fix
                 $usage
-                    ->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
-                    ->setProject($project)
-                    ->trigger();
+                    ->addMetric(METRIC_AUTH_METHOD_PHONE, 1);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php` around lines
415 - 418, Remove the invalid chained calls on the Context instance: replace the
chain that calls setProject() and trigger() after addMetric() with a single call
to addMetric() on the $usage object (i.e., call
$usage->addMetric(METRIC_AUTH_METHOD_PHONE, 1) only), since
Appwrite\Usage\Context does not implement setProject() or trigger(); metrics
will be published by the existing shutdown hook or via UsagePublisher where
appropriate.
src/Appwrite/Platform/Workers/Webhooks.php (1)

200-202: ⚠️ Potential issue | 🔴 Critical

Context class has no setProject() or trigger() methods—this will fatal at runtime.

The Appwrite\Usage\Context class doesn't have setProject() or trigger() methods. This code will cause a fatal error when webhooks are processed.

For workers, metrics should be accumulated via addMetric() and published via UsagePublisher. This worker needs publisherForUsage injected and the publishing logic updated.

🔧 Suggested refactor
  1. Inject publisherForUsage in the constructor
  2. Replace the invalid calls:
-        $usage
-            ->setProject($project)
-            ->trigger();
+        // Publish accumulated metrics
+        $message = new \Appwrite\Event\Message\Usage(
+            project: $project,
+            metrics: $usage->getMetrics(),
+            reduce: $usage->getReduce()
+        );
+        try {
+            $publisherForUsage->enqueue($message);
+        } finally {
+            $usage->reset();
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Workers/Webhooks.php` around lines 200 - 202, The code
incorrectly calls nonexistent Context::setProject() and Context::trigger();
update the worker to accumulate metrics using Context::addMetric() and publish
via the UsagePublisher instance instead: inject a UsagePublisher (named
publisherForUsage) into the constructor, replace the
$usage->setProject($project)->trigger() sequence by adding appropriate metrics
to the Context via $usage->addMetric(...) and then call
$this->publisherForUsage->publish($usage) (or the publisher's publish method) to
send metrics; ensure you reference the existing Context instance ($usage) and
the injected publisherForUsage in the worker class.
tests/e2e/General/UsageTest.php (1)

188-189: ⚠️ Potential issue | 🔴 Critical

Update the #[Depends] targets to the renamed snake_case tests.

These attributes still point at the old camelCase method names. PHPUnit resolves #[Depends] by exact method name, so the renamed tests in this file will not satisfy these dependencies.

🔧 Example fix
-    #[Depends('testPrepareUsersStats')]
+    #[Depends('test_prepare_users_stats')]
     public function test_users_stats(array $data): array
-    #[Depends('testUsersStats')]
+    #[Depends('test_users_stats')]
     public function test_prepare_storage_stats(array $data): array

Apply the same rename to the other #[Depends] strings in this file.

#!/bin/bash
python - <<'PY'
from pathlib import Path
import re

path = Path("tests/e2e/General/UsageTest.php")
text = path.read_text()

methods = set(re.findall(r'public function (\w+)\s*\(', text))
depends = re.findall(r"#\[Depends\('([^']+)'\)\]", text)

for dep in depends:
    if dep not in methods:
        print(f"unresolved Depends target: {dep}")
PY

Also applies to: 232-233, 365-366, 422-423, 597-598, 671-672, 852-854, 931-932, 1095-1096, 1233-1234, 1309-1310

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/General/UsageTest.php` around lines 188 - 189, Update all
#[Depends] attribute targets that still reference old camelCase test method
names to the renamed snake_case method names so PHPUnit can resolve them; for
example change #[Depends('testPrepareUsersStats')] to
#[Depends('test_prepare_users_stats')] for the dependency used by public
function test_users_stats, and apply the same pattern for the other mismatched
targets in this file (ensure each Depends string exactly matches its
corresponding public function name like test_prepare_*), then run the supplied
verification script to confirm there are no unresolved Depends entries.
app/init/resources.php (1)

154-158: ⚠️ Potential issue | 🟠 Major

Pass the explicit usage queue driver here.

This factory still builds the usage queue without the 'utopia-queue' driver. The worker/CLI wiring uses the driver-specific constructor, so this can publish usage messages to a different backend than the usage worker is polling.

Suggested fix
 Http::setResource('publisherForUsage', function (Publisher $publisher) {
     $queueName = System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME);
-    $queue = new \Utopia\Queue\Queue($queueName);
+    $queue = new \Utopia\Queue\Queue($queueName, 'utopia-queue');
 
     return new \Appwrite\Event\Publisher\Usage($publisher, $queue);
 }, ['publisher']);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/init/resources.php` around lines 154 - 158, The factory
Http::setResource('publisherForUsage', ...) currently constructs the usage queue
without specifying a driver; update it to pass the explicit 'utopia-queue'
driver to the \Utopia\Queue\Queue constructor (so the Usage publisher created by
new \Appwrite\Event\Publisher\Usage($publisher, $queue) will publish to the same
backend the worker polls). Locate the anonymous function creating the queue and
supply the driver identifier as the first constructor argument when constructing
\Utopia\Queue\Queue.
src/Appwrite/Platform/Modules/Functions/Workers/Builds.php (3)

1296-1296: ⚠️ Potential issue | 🟡 Minor

Drop the unused $key assignment.

Line 1296 assigns $key and never reads it. It’s just dead state at this point.

Proposed fix
-        $key = $resource->getAttribute('runtime');
         $runtime = match ($resource->getCollection()) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Modules/Functions/Workers/Builds.php` at line 1296,
Remove the dead assignment to $key by deleting the statement that assigns
$resource->getAttribute('runtime') (i.e., the "$key =
$resource->getAttribute('runtime');" line) inside the Builds.php worker code so
no unused variable remains; ensure no other code references $key before or after
removal and run tests to confirm no regressions.

396-403: ⚠️ Potential issue | 🟠 Major

Remove the extra quotes around escapeshellarg() in mv.

Line 400 still wraps escapeshellarg($from) and escapeshellarg($to) in " again. That makes mv receive literal quote characters, so the rename path for directories with spaces fails instead of fixing them.

Proposed fix
-                    $exit = Console::execute('mv "'.\escapeshellarg($from).'" "'.\escapeshellarg($to).'"', '', $stdout, $stderr);
+                    $exit = Console::execute('mv '.\escapeshellarg($from).' '.\escapeshellarg($to), '', $stdout, $stderr);
#!/bin/bash
sed -n '396,402p' src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Modules/Functions/Workers/Builds.php` around lines 396
- 403, The mv command is incorrectly wrapping the outputs of
escapeshellarg($from) and escapeshellarg($to) in additional double quotes,
causing literal quote characters to be passed; update the Console::execute call
in the block handling $rootDirectory with spaces so it uses mv
'.escapeshellarg($from).' '.escapeshellarg($to) (i.e., remove the extra '"'
wrappers around escapeshellarg results) while keeping escapeshellarg to properly
escape $from and $to variables and preserve the existing error handling (the
surrounding code using $tmpDirectory, $rootDirectoryWithoutSpaces, $from, $to
and the subsequent exception should remain unchanged).

1231-1239: ⚠️ Potential issue | 🟠 Major

Use the resolved build memory for *_MB_SECONDS.

The runtime is created with the floored memory from Line 593, but Lines 1231, 1235, and 1239 still multiply only the raw spec memory. That underreports sites and frameworks whose builds are bumped to the minimum memory floor.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Modules/Functions/Workers/Builds.php` around lines 1231
- 1239, The MB-seconds metrics currently multiply by the raw spec memory
expression (($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT)) which underreports
when the runtime memory was floored earlier; locate the variable used when the
runtime was created (the resolved/floored memory computed around the runtime
creation at ~line 593 — e.g. the variable like $memory, $runtimeMemory or
$runtime->getAttribute('memory')) and replace the raw spec expression in the
three addMetric calls (the METRIC_RESOURCE_TYPE_BUILDS_MB_SECONDS,
METRIC_RESOURCE_TYPE_ID_BUILDS_MB_SECONDS and the first
METRIC_BUILDS_MB_SECONDS) so they multiply by that resolved/floored memory value
instead.
🧹 Nitpick comments (2)
src/Appwrite/Platform/Workers/Webhooks.php (1)

91-94: Missing @param Context $usage in docblock.

The execute() method signature includes Context $usage but the docblock only lists @param array $plan. Add the missing parameter documentation for consistency.

📝 Proposed fix
     /**
      * `@param` array $events
      * `@param` string $payload
      * `@param` Document $webhook
      * `@param` Document $user
      * `@param` Document $project
      * `@param` Database $dbForPlatform
      * `@param` Mail $queueForMails
+     * `@param` Context $usage
      * `@param` array $plan
      * `@return` void
      */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Workers/Webhooks.php` around lines 91 - 94, The
docblock for the execute() method is missing the `@param` Context $usage entry;
update the method's PHPDoc above execute(array $events, string $payload,
Document $webhook, Document $user, Document $project, Database $dbForPlatform,
Mail $queueForMails, Context $usage, array $plan): void to include a line
documenting the Context $usage parameter (e.g., "@param Context $usage
Usage/context for the webhook execution") alongside the existing `@param` array
$plan to keep docblock consistent with the signature.
src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php (1)

101-101: Remove unused injected parameters $timelimit and $plan.

Both parameters are declared in the method signature but never referenced in the method body. Removing them simplifies the signature and avoids confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php` at line 101,
The action method signature declares unused injected parameters $timelimit and
$plan in Create::action; remove these two parameters from the method signature
to simplify and avoid confusion, then update any callers or dependency
injection/route bindings that reference this signature (e.g., controller
instantiation, service wiring) to match the new signature so no runtime
parameter mismatch occurs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/e2e/General/UsageTest.php`:
- Around line 853-854: The test uses the attribute #[Retry(count: 1)] but misses
its import, causing PHP to resolve it to the wrong namespace; add the import
"use Appwrite\Tests\Retry;" to the top import block of UsageTest.php so the
Retry attribute used on test_database_stats_tables_api (and any other tests in
this file) references the correct class Appwrite\Tests\Retry.

---

Outside diff comments:
In `@src/Appwrite/Platform/Modules/Databases/Http/Databases/Delete.php`:
- Around line 63-67: The route builder injects 'usage' but the handler signature
for action() does not accept it, so either add a Context $usage parameter to the
action method (public function action(string $databaseId, UtopiaResponse
$response, Database $dbForProject, EventDatabase $queueForDatabase, Event
$queueForEvents, Context $usage): void) so the injected usage context is
consumed when tracking deletion metrics, or remove the ->inject('usage') call
from the route chain to avoid an unused injection; update references inside
action() accordingly (use $usage for metrics if added, or delete any code
assuming it if you remove the injection).

In `@src/Appwrite/Platform/Workers/Messaging.php`:
- Around line 311-322: The code incorrectly calls non-existent Context methods
setProject() and trigger() on $usage; replace this publish pattern in
sendExternalMessage() by accumulating metrics with $usage->addMetric(...), then
build a UsageMessage using UsageMessage(project: $project, metrics:
$usage->getMetrics(), reduce: $usage->getReduce()), enqueue it via the injected
publisherForUsage->enqueue($message) inside a try/finally, and call
$usage->reset() in the finally block; remove calls to $usage->setProject() and
$usage->trigger() and ensure publisherForUsage is injected into the worker.

---

Duplicate comments:
In `@app/init/resources.php`:
- Around line 154-158: The factory Http::setResource('publisherForUsage', ...)
currently constructs the usage queue without specifying a driver; update it to
pass the explicit 'utopia-queue' driver to the \Utopia\Queue\Queue constructor
(so the Usage publisher created by new
\Appwrite\Event\Publisher\Usage($publisher, $queue) will publish to the same
backend the worker polls). Locate the anonymous function creating the queue and
supply the driver identifier as the first constructor argument when constructing
\Utopia\Queue\Queue.

In `@CONTRIBUTING.md`:
- Around line 500-507: Wrap the enqueue call in a try/finally so $usage->reset()
always runs even if $publisherForUsage->enqueue($message) throws: create the
UsageMessage as shown, call $publisherForUsage->enqueue($message) inside a try
block, and move $usage->reset() into the finally block to guarantee the usage
context is cleared between jobs.
- Around line 501-504: The snippet constructs a UsageMessage with project:
$project but never shows how $project is obtained; before the UsageMessage
creation, add and show the assignment that populates $project in the worker flow
(e.g., retrieving it from the job payload or context such as $project =
$job->getProject() or $project = $this->getProjectFromContext()), so readers can
copy-paste the example; reference the UsageMessage construction and the exact
variable name $project so the source of that variable is clear.
- Around line 492-498: The compute metric units are inconsistent: the example
multiplies duration by 1000 (milliseconds) while the docs list
builds.compute/executions.compute as seconds; update the example so both uses of
duration (the calls to addMetric with METRIC_BUILDS_COMPUTE and
METRIC_FUNCTION_ID_BUILDS_COMPUTE) record seconds instead of milliseconds by
removing the * 1000 on $build->getAttribute('duration', 0) so the sample matches
the documented units.

In
`@src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php`:
- Around line 209-212: The code is calling non-existent Context methods
setProject() and trigger() on $usage; remove the chaining and only call
$usage->addMetric(METRIC_AUTH_METHOD_PHONE, 1) so metrics are accumulated by the
Context, not published here—do not call setProject() or trigger(), the API
shutdown hook will handle publishing. Ensure no other code in Create.php tries
to chain setProject() or trigger() on the Context instance.

In
`@src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php`:
- Around line 77-84: Remove the unused user injection and parameter: delete the
->inject('user') call in the service builder chain and remove the Document $user
parameter from the action(...) method signature (the action method defined as
public function action(..., Document $user, Context $usage, ...)). Update any
related parameter ordering/typing in the action declaration to keep Context
$usage and subsequent parameters in the correct positions and ensure no
references to $user remain in the method body.

In `@src/Appwrite/Platform/Modules/Functions/Workers/Builds.php`:
- Line 1296: Remove the dead assignment to $key by deleting the statement that
assigns $resource->getAttribute('runtime') (i.e., the "$key =
$resource->getAttribute('runtime');" line) inside the Builds.php worker code so
no unused variable remains; ensure no other code references $key before or after
removal and run tests to confirm no regressions.
- Around line 396-403: The mv command is incorrectly wrapping the outputs of
escapeshellarg($from) and escapeshellarg($to) in additional double quotes,
causing literal quote characters to be passed; update the Console::execute call
in the block handling $rootDirectory with spaces so it uses mv
'.escapeshellarg($from).' '.escapeshellarg($to) (i.e., remove the extra '"'
wrappers around escapeshellarg results) while keeping escapeshellarg to properly
escape $from and $to variables and preserve the existing error handling (the
surrounding code using $tmpDirectory, $rootDirectoryWithoutSpaces, $from, $to
and the subsequent exception should remain unchanged).
- Around line 1231-1239: The MB-seconds metrics currently multiply by the raw
spec memory expression (($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT)) which
underreports when the runtime memory was floored earlier; locate the variable
used when the runtime was created (the resolved/floored memory computed around
the runtime creation at ~line 593 — e.g. the variable like $memory,
$runtimeMemory or $runtime->getAttribute('memory')) and replace the raw spec
expression in the three addMetric calls (the
METRIC_RESOURCE_TYPE_BUILDS_MB_SECONDS,
METRIC_RESOURCE_TYPE_ID_BUILDS_MB_SECONDS and the first
METRIC_BUILDS_MB_SECONDS) so they multiply by that resolved/floored memory value
instead.

In `@src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php`:
- Around line 415-418: Remove the invalid chained calls on the Context instance:
replace the chain that calls setProject() and trigger() after addMetric() with a
single call to addMetric() on the $usage object (i.e., call
$usage->addMetric(METRIC_AUTH_METHOD_PHONE, 1) only), since
Appwrite\Usage\Context does not implement setProject() or trigger(); metrics
will be published by the existing shutdown hook or via UsagePublisher where
appropriate.

In `@src/Appwrite/Platform/Workers/Migrations.php`:
- Around line 148-155: The variable $publisherForUsage from action() is not
passed into processMigration() or processMigrationResourceStats(), causing an
undefined variable when processMigrationResourceStats() tries to call
$publisherForUsage->enqueue(...). Fix by adding a $publisherForUsage parameter
to processMigration() and processMigrationResourceStats(), update all internal
calls (e.g., where processMigration(...) is invoked) to pass the
$publisherForUsage value through, and ensure any helper calls within
processMigration that call processMigrationResourceStats also forward the same
$publisherForUsage parameter so the enqueue call uses the propagated publisher
instance.

In `@src/Appwrite/Platform/Workers/Webhooks.php`:
- Around line 200-202: The code incorrectly calls nonexistent
Context::setProject() and Context::trigger(); update the worker to accumulate
metrics using Context::addMetric() and publish via the UsagePublisher instance
instead: inject a UsagePublisher (named publisherForUsage) into the constructor,
replace the $usage->setProject($project)->trigger() sequence by adding
appropriate metrics to the Context via $usage->addMetric(...) and then call
$this->publisherForUsage->publish($usage) (or the publisher's publish method) to
send metrics; ensure you reference the existing Context instance ($usage) and
the injected publisherForUsage in the worker class.

In `@tests/e2e/General/UsageTest.php`:
- Around line 188-189: Update all #[Depends] attribute targets that still
reference old camelCase test method names to the renamed snake_case method names
so PHPUnit can resolve them; for example change
#[Depends('testPrepareUsersStats')] to #[Depends('test_prepare_users_stats')]
for the dependency used by public function test_users_stats, and apply the same
pattern for the other mismatched targets in this file (ensure each Depends
string exactly matches its corresponding public function name like
test_prepare_*), then run the supplied verification script to confirm there are
no unresolved Depends entries.

---

Nitpick comments:
In `@src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php`:
- Line 101: The action method signature declares unused injected parameters
$timelimit and $plan in Create::action; remove these two parameters from the
method signature to simplify and avoid confusion, then update any callers or
dependency injection/route bindings that reference this signature (e.g.,
controller instantiation, service wiring) to match the new signature so no
runtime parameter mismatch occurs.

In `@src/Appwrite/Platform/Workers/Webhooks.php`:
- Around line 91-94: The docblock for the execute() method is missing the `@param`
Context $usage entry; update the method's PHPDoc above execute(array $events,
string $payload, Document $webhook, Document $user, Document $project, Database
$dbForPlatform, Mail $queueForMails, Context $usage, array $plan): void to
include a line documenting the Context $usage parameter (e.g., "@param Context
$usage Usage/context for the webhook execution") alongside the existing `@param`
array $plan to keep docblock consistent with the signature.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1669772c-0982-40cc-bc59-3082c7c89535

📥 Commits

Reviewing files that changed from the base of the PR and between 38b2eed and 2af13c5.

📒 Files selected for processing (50)
  • CONTRIBUTING.md
  • app/cli.php
  • app/controllers/api/account.php
  • app/controllers/shared/api.php
  • app/init/resources.php
  • app/worker.php
  • src/Appwrite/Bus/Listeners/Usage.php
  • src/Appwrite/Event/Message/Base.php
  • src/Appwrite/Event/Message/Usage.php
  • src/Appwrite/Event/Publisher/Base.php
  • src/Appwrite/Event/Publisher/Usage.php
  • src/Appwrite/Event/StatsUsage.php
  • src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php
  • src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Delete.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Delete.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Delete.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Update.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Decrement.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Increment.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Create.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Delete.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Get.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Update.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Upsert.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/XList.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php
  • src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
  • src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
  • src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Failed/Get.php
  • src/Appwrite/Platform/Modules/Health/Http/Health/Queue/StatsUsage/Get.php
  • src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php
  • src/Appwrite/Platform/Workers/Messaging.php
  • src/Appwrite/Platform/Workers/Migrations.php
  • src/Appwrite/Platform/Workers/Webhooks.php
  • src/Appwrite/Usage/Context.php
  • tests/e2e/General/UsageTest.php
💤 Files with no reviewable changes (1)
  • src/Appwrite/Event/StatsUsage.php
🚧 Files skipped from review as they are similar to previous changes (15)
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Delete.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Update.php
  • src/Appwrite/Event/Message/Base.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Upsert.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Update.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Delete.php
  • src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php
  • src/Appwrite/Event/Publisher/Usage.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Increment.php
  • src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Create.php

@loks0n loks0n force-pushed the refactor/usage-publishing branch 11 times, most recently from df4e6e3 to a403059 Compare March 11, 2026 17:04
@loks0n loks0n force-pushed the refactor/usage-publishing branch 7 times, most recently from 9179be3 to 704fbe0 Compare March 12, 2026 00:24
@loks0n loks0n force-pushed the refactor/usage-publishing branch from 704fbe0 to a804cba Compare March 12, 2026 15:37
…arg double-quoting

Use $memory (which includes minMemory floor) and $cpus instead of raw
spec values in MB-seconds metrics, fixing underreporting for sites and
frameworks bumped to the minimum memory. Also remove redundant double
quotes around escapeshellarg() calls in mv command.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@loks0n loks0n merged commit 021c5fc into 1.8.x Mar 13, 2026
67 of 71 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants