Skip to content

Conversation

@athul-rs
Copy link
Contributor

What

  • Add dashboard metrics Django app with multi-tier storage (hourly/daily/monthly tables)
  • Add core metrics library with queue backend for event capture
  • Add dedicated metrics Celery worker (worker-metrics)
  • Add live query endpoints for immediate data access from source tables
  • Add production readiness features (retry config, rate limiting, health checks)

Why

  • Enable tracking and visualization of platform usage metrics
  • Provide real-time insights into documents processed, LLM calls, API requests, etc.
  • Support dashboard reporting without impacting main application performance

How

  • Created backend/dashboard_metrics/ Django app with models, views, services, and tasks
  • Created unstract/core/src/unstract/core/metrics/ library with queue backend using Kombu
  • Used celery-batches for efficient batch processing of metric events
  • Added dedicated Celery worker consuming from dashboard_metric_events queue
  • Implemented live query endpoints that read directly from source tables (usage, page_usage, workflow_execution)

Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

  • No, this is an entirely new feature with no modifications to existing functionality
  • New Django app is isolated and only adds new API endpoints
  • New Celery worker runs on a separate queue

Database Migrations

  • 0001_initial: Creates EventMetricsHourly table
  • 0002_setup_cleanup_periodic_task: Adds periodic cleanup task
  • 0003_add_daily_monthly_tables: Creates EventMetricsDaily and EventMetricsMonthly tables
  • 0004_update_cleanup_periodic_tasks: Adds daily cleanup periodic task

Env Config

  • WORKER_METRICS_AUTOSCALE (optional): Autoscale config for metrics worker (default: 4,1)
  • DASHBOARD_METRICS_ENABLED (optional): Enable/disable metrics capture

Relevant Docs

  • N/A

Related Issues or PRs

  • UN-1798

Dependencies Versions

  • celery-batches>=0.8.0 (new dependency)

Notes on Testing

  • Run unit tests: pytest backend/dashboard_metrics/tests/
  • Verify worker starts: docker compose up worker-metrics
  • Test health endpoint: GET /api/v2/metrics/health/
  • Verify metrics queue routing in RabbitMQ

Screenshots

N/A

Checklist

I have read and understood the Contribution Guidelines.

- Implement multi-tier metrics aggregation with celery-batches
- Add hourly/daily/monthly storage tables with proper indexing
- Add dedicated metrics Celery worker (worker-metrics)
- Add live query endpoints for immediate data access
- Add production readiness: retry config, rate limiting, health checks
- Add queue routing and broker resilience configuration
- Add comprehensive unit tests for batch processing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 29, 2025

Walkthrough

This pull request introduces a comprehensive dashboard metrics system with batch event processing via Celery, Redis-backed caching, time-bucketed database aggregations (hourly/daily/monthly), REST API endpoints for metrics queries, and core metrics capture infrastructure with pluggable backends. The system enables organizations to track and query key operational metrics across documents, pages, API calls, LLM usage, and ETL executions.

Changes

Cohort / File(s) Summary
Celery Configuration & Task Processing
backend/backend/celery_config.py, backend/dashboard_metrics/tasks.py
Enhanced Celery config with connection resilience, prefetch settings, queue routing, and task batching. Added Celery tasks for batch processing metric events into hourly/daily/monthly aggregations and cleanup tasks with configurable retention periods.
Database Models & Migrations
backend/dashboard_metrics/models.py, backend/dashboard_metrics/migrations/0001_initial.py, backend/dashboard_metrics/migrations/0003_add_daily_monthly_tables.py
Three time-bucketed event metric models (EventMetricsHourly/Daily/Monthly) with UUID PKs, indexed organization+timestamp/date/month, and unique constraints. Includes GinIndex on labels JSON field and cascade deletion.
Data Migration Setup
backend/dashboard_metrics/migrations/0002_setup_cleanup_periodic_task.py, backend/dashboard_metrics/migrations/0004_update_cleanup_periodic_tasks.py
Django-Celery-Beat periodic tasks for automated cleanup of hourly (30-day) and daily (365-day) metrics at 2:00 and 3:00 UTC respectively.
Caching Layer
backend/dashboard_metrics/cache.py
Redis-backed cache with time-aware TTLs (30s current hour, 8h historical), endpoint-specific cache strategies, invalidation by org_id, and pre-warming. Decorator-based pattern for transparent caching of metric endpoints.
API Endpoints & Serializers
backend/dashboard_metrics/views.py, backend/dashboard_metrics/serializers.py, backend/dashboard_metrics/urls.py
ReadOnlyModelViewSet with summary, series, overview, live_summary, live_series, and health check endpoints. Includes query validation, rate throttling (100/hour), and serializers for time-series and summary responses.
Metrics Capture & Query Services
backend/dashboard_metrics/capture.py, backend/dashboard_metrics/services.py
MetricsCapture utility for recording events (API requests, ETL, documents, LLM, etc.) with graceful degradation. MetricsQueryService for querying source tables with aggregation by hour/day/week.
Django Configuration & Routing
backend/backend/settings/base.py, backend/backend/urls_v2.py, backend/dashboard_metrics/apps.py, backend/dashboard_metrics/__init__.py
Added dashboard_metrics to SHARED_APPS. Registered URL prefix /metrics/ with ViewSet routing. AppConfig with BigAutoField and verbose names.
Core Metrics Infrastructure
unstract/core/src/unstract/core/metrics/{__init__,types,registry,config,backends/*}.py
Type-safe MetricName and MetricType enums, MetricEvent dataclass, validation registry, and pluggable backend system: AbstractMetricBackend, NoopBackend (no-op when disabled), QueueBackend (publishes to RabbitMQ). Environment-driven configuration.
Worker & Infrastructure
backend/pyproject.toml, docker/docker-compose.yaml, backend/dashboard_metrics/tests/*
New Poe task worker-metrics for running dashboard metrics Celery worker. Docker service with dashboard_metric_events queue. Test module covering task processing, aggregation, and cleanup.

Sequence Diagrams

sequenceDiagram
    actor Client
    participant API as Metrics API<br/>(ViewSet)
    participant Cache as Redis Cache
    participant DB as Database
    participant Service as MetricsQueryService

    Client->>API: GET /metrics/summary?start=...&end=...
    API->>API: Validate query params
    Note over API: Check cache key
    API->>Cache: Check cached response
    alt Cache Hit
        Cache-->>API: Return cached data
    else Cache Miss
        API->>Service: Query aggregated metrics
        Service->>DB: SELECT from EventMetrics*
        DB-->>Service: Aggregated results
        Service-->>API: Metric summaries
        API->>Cache: Store with TTL<br/>(30s for current hour,<br/>8h for historical)
    end
    API-->>Client: MetricsResponseSerializer
Loading
sequenceDiagram
    participant App as Application<br/>(MetricsCapture)
    participant Queue as RabbitMQ
    participant Celery as Celery Worker
    participant BatchProc as Batch Processor
    participant DB as Database

    App->>App: record_api_request(<br/>org_id, labels)
    App->>Queue: Publish metric event<br/>(QueueBackend)
    
    Note over Queue: Batch accumulates<br/>(flush_every=100,<br/>flush_interval=60s)
    
    Celery->>Queue: Poll for batch
    Queue-->>Celery: Batch of events
    Celery->>BatchProc: process_dashboard_metric_events()
    
    loop For each event
        BatchProc->>BatchProc: Truncate to hour/day/month
        BatchProc->>BatchProc: Aggregate in memory
    end
    
    BatchProc->>DB: Bulk upsert hourly
    DB-->>BatchProc: Created/Updated count
    BatchProc->>DB: Bulk upsert daily
    DB-->>BatchProc: Created/Updated count
    BatchProc->>DB: Bulk upsert monthly
    DB-->>BatchProc: Created/Updated count
    
    Note over DB: Merge labels, increment counters<br/>via select_for_update
Loading
sequenceDiagram
    actor User
    participant Client as Client
    participant API as Live Series<br/>Endpoint
    participant Service as MetricsQueryService
    participant Sources as Source Tables<br/>(WorkflowFileExecution,<br/>PageUsage, etc.)
    participant Client2 as Client

    User->>Client: GET /metrics/live_series?metric_name=...
    Client->>API: Request live metrics
    API->>Service: get_*_metric(org_id,<br/>start_date, end_date)
    par Query all metrics
        Service->>Sources: Query DOCUMENTS_PROCESSED
        Sources-->>Service: Results
        Service->>Sources: Query PAGES_PROCESSED
        Sources-->>Service: Results
        Service->>Sources: Query LLM_CALLS
        Sources-->>Service: Results
    end
    Note over Service: Aggregate by granularity<br/>(hour/day/week)
    Service-->>API: Time-series data
    API->>API: Serialize to MetricSeriesSerializer
    API-->>Client2: JSON response<br/>(series, summary data)
    Client2-->>User: Display metrics dashboard
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description comprehensively covers all required template sections including What, Why, How, risk assessment, database migrations, environment config, dependencies, and testing notes. It is well-structured, detailed, and provides clear context for the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 95.24% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title '[FEAT] Dashboard metrics system' directly and clearly summarizes the main feature added—a complete dashboard metrics system. It is concise, specific, and accurately reflects the core change without extraneous noise.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

@athul-rs athul-rs marked this pull request as draft December 29, 2025 19:32
@athul-rs athul-rs marked this pull request as ready for review January 5, 2026 04:28
@github-actions
Copy link
Contributor

github-actions bot commented Jan 5, 2026

Test Results

Summary
  • Runner Tests: 11 passed, 0 failed (11 total)
  • SDK1 Tests: 66 passed, 0 failed (66 total)

Runner Tests - Full Report
filepath function $$\textcolor{#23d18b}{\tt{passed}}$$ SUBTOTAL
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_logs}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_cleanup}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_cleanup\_skip}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_client\_init}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_container\_run\_config}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_container\_run\_config\_without\_mount}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_run\_container}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image\_for\_sidecar}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_sidecar\_container}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{TOTAL}}$$ $$\textcolor{#23d18b}{\tt{11}}$$ $$\textcolor{#23d18b}{\tt{11}}$$
SDK1 Tests - Full Report
filepath function $$\textcolor{#23d18b}{\tt{passed}}$$ SUBTOTAL
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_success\_on\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retry\_on\_connection\_error}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_non\_retryable\_http\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retryable\_http\_errors}}$$ $$\textcolor{#23d18b}{\tt{3}}$$ $$\textcolor{#23d18b}{\tt{3}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_post\_method\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retry\_logging}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_success\_on\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_retry\_on\_errors}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_wrapper\_methods\_retry}}$$ $$\textcolor{#23d18b}{\tt{4}}$$ $$\textcolor{#23d18b}{\tt{4}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_connection\_error\_is\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_timeout\_is\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_retryable\_status\_codes}}$$ $$\textcolor{#23d18b}{\tt{3}}$$ $$\textcolor{#23d18b}{\tt{3}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_non\_retryable\_status\_codes}}$$ $$\textcolor{#23d18b}{\tt{5}}$$ $$\textcolor{#23d18b}{\tt{5}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_without\_response}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_os\_error\_retryable\_errno}}$$ $$\textcolor{#23d18b}{\tt{5}}$$ $$\textcolor{#23d18b}{\tt{5}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_os\_error\_non\_retryable\_errno}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_other\_exception\_not\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_exponential\_backoff\_without\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_exponential\_backoff\_with\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_max\_delay\_cap}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_max\_delay\_cap\_with\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_successful\_call\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_retry\_after\_transient\_failure}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_max\_retries\_exceeded}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_max\_time\_exceeded}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_retry\_with\_custom\_predicate}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_no\_retry\_with\_predicate\_false}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_exception\_not\_in\_tuple\_not\_retried}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_delay\_would\_exceed\_max\_time}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_default\_configuration}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_environment\_variable\_configuration}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_max\_retries}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_max\_time}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_base\_delay}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_multiplier}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_jitter\_values}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_custom\_exceptions\_only}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_custom\_predicate\_only}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_both\_exceptions\_and\_predicate}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_exceptions\_match\_but\_predicate\_false}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_retry\_platform\_service\_call\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_retry\_prompt\_service\_call\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_platform\_service\_decorator\_retries\_on\_connection\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_prompt\_service\_decorator\_retries\_on\_timeout}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_warning\_logged\_on\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_info\_logged\_on\_success\_after\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_exception\_logged\_on\_giving\_up}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{TOTAL}}$$ $$\textcolor{#23d18b}{\tt{66}}$$ $$\textcolor{#23d18b}{\tt{66}}$$

Copy link
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: 8

Fix all issues with AI Agents 🤖
In @backend/dashboard_metrics/capture.py:
- Around line 51-338: MetricsCapture methods call record() with the metric enum
as the first positional arg and keyword names like org_id= and value=, but core
record signature expects org_id first, metric_name second and metric_value
third, so change every call in record_api_request, record_etl_execution,
record_document_processed (both DOCUMENTS_PROCESSED and PAGES_PROCESSED),
record_llm_call (LLM_CALLS and LLM_USAGE), record_challenge,
record_summarization, and record_prompt_execution to match the core signature:
call record(org_id, MetricName.<X>, <metric_value>, labels=metric_labels,
project=project, tag=None) (use 1 for single-count events, pages for pages, cost
for LLM_USAGE as float), and remove the invalid value= keyword and duplicate
org_id keyword usage.

In @backend/dashboard_metrics/urls.py:
- Around line 9-15: The URL configuration is missing a mapping for the
DashboardMetricsViewSet.health action; add a new view mapping similar to the
others (e.g., create metrics_health = DashboardMetricsViewSet.as_view({"get":
"health"})) and include it in the URL patterns so GET /api/v2/metrics/health/ is
exposed; update any import/urlpatterns list references where other metrics_*
views are registered to include metrics_health.

In @backend/dashboard_metrics/views.py:
- Around line 267-273: Both live_summary and live_series call
UserContext.get_organization() and access .id directly; add the same guard used
in get_queryset(): if organization is None (or falsy) log an explanatory message
and raise PermissionDenied before accessing organization.id, then pass org_id to
MetricsQueryService.get_all_metrics_summary / get_all_metrics_series; apply the
same check for the other occurrence around lines referenced (the block using
UserContext.get_organization() / organization.id at ~310-312).

In @unstract/core/src/unstract/core/metrics/types.py:
- Around line 35-48: Add a unit test that ensures every value of the MetricName
enum has a corresponding key in METRIC_TYPE_MAP to prevent KeyError in
get_metric_type(); specifically, iterate over MetricName (or
MetricName.__members__.values()/MetricName) and assert that each .value is
present in METRIC_TYPE_MAP, referencing METRIC_TYPE_MAP and MetricName; this
complements validate_metric() and prevents missing entries that would cause
get_metric_type() to fail.

Caution

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

⚠️ Outside diff range comments (1)
backend/pyproject.toml (1)

15-21: celery-batches version too old—use >=0.9.0 for Celery 5.3.4 and Python 3.12 support

The constraint celery-batches>=0.8.0 pins to an outdated release (Jun 2023). For Celery 5.3.4 and Python 3.12, use celery-batches>=0.9.0, which is documented to support Celery ~5.0 and Python 3.9–3.13. Version 0.8.0 predates Python 3.12 and lacks testing against current Celery 5.x releases.

🧹 Nitpick comments (16)
backend/dashboard_metrics/tasks.py (2)

37-75: Time bucketing helpers look correct; naive datetimes are assumed UTC

The truncation helpers correctly normalize numeric timestamps to UTC and bucket to hour/day/month. Note that naive datetimes are implicitly treated as UTC; ensure all producers either send UTC or explicitly aware datetimes so you don’t silently mis-bucket mixed timezones.


77-206: Batched aggregation logic is solid; consider tightening event validation

The batching and in-memory aggregation into hourly/daily/monthly buckets looks good and aligns with celery-batches’ request shape. Two minor points to consider:

  • event = request.args[0] if request.args else request.kwargs.get("event", {}) assumes the first arg is always a dict; if other producers appear later, a simple isinstance(..., dict) guard could avoid noisy warnings.
  • timestamp is expected as float/datetime; if there’s any chance of ISO strings coming through, you may want to normalize them explicitly rather than relying on a generic except Exception to skip them.

Nothing blocking here; more of a future-hardening note.

backend/backend/urls_v2.py (1)

63-67: Metrics URL include is consistent with existing routing

Mounting dashboard_metrics.urls at "metrics/" follows the same pattern as other feature areas (e.g., "usage/", "notifications/"). Just ensure any external docs or clients expecting /api/v2/metrics/... are updated to match this base path if needed.

docker/docker-compose.yaml (1)

120-138: worker-metrics service wiring looks consistent with existing workers

The new worker-metrics service mirrors the existing Celery worker patterns (image, entrypoint, queue, autoscale, env) and targets the dashboard_metric_events queue as intended. If you expect metrics tasks to need shared workflow data or tool configs, consider whether it should also mount the same volumes as other workers; otherwise this setup looks good.

backend/pyproject.toml (1)

145-148: worker-metrics Poe task aligns with docker-compose configuration

The worker-metrics Poe task mirrors the docker-compose command (same app, log level, queue, autoscale), which is helpful for running the metrics worker locally outside Docker. Keep this in sync with any future queue or autoscale changes to avoid drift between CLI and compose.

backend/dashboard_metrics/tests/test_tasks.py (1)

149-164: Consider renaming unused loop variable.

The loop variable i is not used within the loop body. Following Python convention, consider renaming it to _ to indicate it's intentionally unused.

🔎 Proposed refactor
-        for i in range(5):
+        for _ in range(5):
             mock_request = MagicMock()
unstract/core/src/unstract/core/metrics/__init__.py (1)

113-115: Use logging.exception to preserve the traceback.

When catching exceptions during event creation, using logger.exception() instead of logger.error() will include the full traceback, which aids debugging.

🔎 Suggested fix
     except Exception as e:
-        logger.error(f"Failed to create metric event: {e}")
+        logger.exception(f"Failed to create metric event: {e}")
         return False
unstract/core/src/unstract/core/metrics/registry.py (1)

42-48: Non-deterministic ordering in get_all_metrics().

The function returns list(_REGISTERED_METRICS) where _REGISTERED_METRICS is a set. While this works, the order of returned metrics may vary between calls. If consistent ordering is needed for API responses or testing, consider sorting the result.

🔎 Optional fix for deterministic ordering
 def get_all_metrics() -> list[str]:
     """Get all registered metric names.

     Returns:
         List of all registered metric names
     """
-    return list(_REGISTERED_METRICS)
+    return sorted(_REGISTERED_METRICS)
backend/dashboard_metrics/migrations/0004_update_cleanup_periodic_tasks.py (1)

38-57: Clarify naming: task cleans "daily" aggregates on a weekly schedule.

The comment says "weekly cleanup task for daily metrics" but the task is named dashboard_metrics_cleanup_daily. This is technically correct (it cleans data from the daily aggregation table on a weekly schedule), but the naming could cause confusion. Consider renaming to dashboard_metrics_cleanup_daily_weekly or updating the description to clarify it runs weekly to clean daily-aggregated data.

unstract/core/src/unstract/core/metrics/types.py (1)

85-96: Consider defensive error handling in from_dict.

If required keys (org_id, metric_name, metric_value, metric_type) are missing from data, a KeyError will be raised. While the caller should ensure valid data, adding a brief check or using .get() with explicit validation could provide clearer error messages during deserialization failures.

unstract/core/src/unstract/core/metrics/backends/queue.py (1)

151-156: Use logger.exception for error logging with traceback.

Replace logger.error with logger.exception in the exception handlers to include stack traces, which aids debugging production issues.

🔎 Suggested fix
         except KombuError as e:
-            logger.error(f"Failed to queue metric {event.metric_name}: {e}")
+            logger.exception(f"Failed to queue metric {event.metric_name}: {e}")
             return False
         except Exception as e:
-            logger.error(f"Unexpected error queuing metric: {e}")
+            logger.exception(f"Unexpected error queuing metric: {e}")
             return False
backend/dashboard_metrics/views.py (1)

335-369: Improve error handling and observability in live_series metric loop.

Catching a blanket Exception for each metric is reasonable to keep the endpoint partially available, but the current logging (logger.error) loses the traceback and makes debugging failures harder.

Switching to logger.exception here will preserve the stack trace while keeping behavior the same.

Proposed logging tweak
-            except Exception as e:
-                logger.error(f"Failed to fetch metric {metric_name}: {e}")
+            except Exception:
+                logger.exception("Failed to fetch metric %s", metric_name)
                 errors.append(metric_name)
backend/dashboard_metrics/cache.py (1)

38-69: Time‑aware TTL helper is well‑designed but currently unused.

get_time_aware_cache_ttl() correctly distinguishes queries that include the current hour from purely historical ones, which would be useful to avoid over‑caching near‑real‑time data. Right now it isn’t wired into cache_metrics_response, so all endpoints rely on static TTLs.

Consider integrating get_time_aware_cache_ttl() into the decorator (e.g., deriving TTL from a parsed end_date param) for summary/series endpoints that query recent data.

backend/dashboard_metrics/models.py (3)

14-19: Avoid duplicating MetricType that already exists in core metrics

There’s already a MetricType enum in unstract/core/src/unstract/core/metrics/types.py with the same values ("counter", "histogram"). Having two different MetricType definitions (one Enum, one TextChoices) increases the chance of divergence or confusion.

Consider either:

  • Reusing the core MetricType as the single source of truth and mapping its values into a TextChoices, or
  • Renaming this one (e.g., DashboardMetricType) to make the separation explicit.

21-37: Manager inheritance can be simplified

DefaultOrganizationManagerMixin already subclasses models.Manager (per backend/utils/models/organization_mixin.py), so inheriting from both is redundant:

class EventMetricsHourlyManager(DefaultOrganizationManagerMixin):
    ...

Same applies to EventMetricsDailyManager and EventMetricsMonthlyManager.


113-130: Address Ruff RUF012 on mutable Meta attributes (indexes, constraints)

Ruff is flagging indexes and constraints as mutable class attributes (RUF012). In Django Meta classes this is common, but if you want to satisfy the linter and improve type hints you can annotate them as ClassVar:

from typing import ClassVar
from django.db import models

class Meta:
    indexes: ClassVar[list[models.Index]] = [
        ...
    ]
    constraints: ClassVar[list[models.Constraint]] = [
        ...
    ]

You can apply the same pattern to the Meta classes of all three models, or alternatively disable RUF012 for Django Meta if you prefer the standard Django style.

Also applies to: 219-236, 325-342

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 3469b02 and 62b1c3b.

⛔ Files ignored due to path filters (1)
  • backend/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (30)
  • backend/backend/celery_config.py
  • backend/backend/settings/base.py
  • backend/backend/urls_v2.py
  • backend/dashboard_metrics/__init__.py
  • backend/dashboard_metrics/apps.py
  • backend/dashboard_metrics/cache.py
  • backend/dashboard_metrics/capture.py
  • backend/dashboard_metrics/migrations/0001_initial.py
  • backend/dashboard_metrics/migrations/0002_setup_cleanup_periodic_task.py
  • backend/dashboard_metrics/migrations/0003_add_daily_monthly_tables.py
  • backend/dashboard_metrics/migrations/0004_update_cleanup_periodic_tasks.py
  • backend/dashboard_metrics/migrations/__init__.py
  • backend/dashboard_metrics/models.py
  • backend/dashboard_metrics/serializers.py
  • backend/dashboard_metrics/services.py
  • backend/dashboard_metrics/tasks.py
  • backend/dashboard_metrics/tests/__init__.py
  • backend/dashboard_metrics/tests/test_tasks.py
  • backend/dashboard_metrics/urls.py
  • backend/dashboard_metrics/views.py
  • backend/pyproject.toml
  • docker/docker-compose.yaml
  • unstract/core/src/unstract/core/metrics/__init__.py
  • unstract/core/src/unstract/core/metrics/backends/__init__.py
  • unstract/core/src/unstract/core/metrics/backends/base.py
  • unstract/core/src/unstract/core/metrics/backends/noop.py
  • unstract/core/src/unstract/core/metrics/backends/queue.py
  • unstract/core/src/unstract/core/metrics/config.py
  • unstract/core/src/unstract/core/metrics/registry.py
  • unstract/core/src/unstract/core/metrics/types.py
🧰 Additional context used
🧬 Code graph analysis (16)
unstract/core/src/unstract/core/metrics/backends/noop.py (3)
unstract/core/src/unstract/core/metrics/types.py (1)
  • MetricEvent (52-96)
unstract/core/src/unstract/core/metrics/backends/base.py (3)
  • AbstractMetricBackend (8-40)
  • record (15-24)
  • flush (27-32)
unstract/core/src/unstract/core/metrics/__init__.py (1)
  • record (58-119)
unstract/core/src/unstract/core/metrics/backends/base.py (4)
unstract/core/src/unstract/core/metrics/types.py (1)
  • MetricEvent (52-96)
unstract/core/src/unstract/core/metrics/__init__.py (1)
  • record (58-119)
unstract/core/src/unstract/core/metrics/backends/noop.py (2)
  • record (25-40)
  • flush (42-44)
unstract/core/src/unstract/core/metrics/backends/queue.py (3)
  • record (117-156)
  • flush (200-205)
  • close (207-215)
unstract/core/src/unstract/core/metrics/registry.py (2)
unstract/core/src/unstract/core/metrics/types.py (2)
  • MetricName (15-32)
  • MetricType (8-12)
backend/dashboard_metrics/models.py (1)
  • MetricType (14-18)
backend/dashboard_metrics/tasks.py (1)
backend/dashboard_metrics/models.py (4)
  • EventMetricsDaily (145-248)
  • EventMetricsHourly (39-142)
  • EventMetricsMonthly (251-354)
  • MetricType (14-18)
unstract/core/src/unstract/core/metrics/types.py (1)
backend/dashboard_metrics/models.py (1)
  • MetricType (14-18)
backend/dashboard_metrics/urls.py (1)
backend/dashboard_metrics/views.py (1)
  • DashboardMetricsViewSet (40-451)
backend/dashboard_metrics/views.py (4)
backend/permissions/permission.py (1)
  • IsOrganizationMember (17-22)
backend/utils/user_context.py (1)
  • UserContext (8-32)
backend/dashboard_metrics/cache.py (1)
  • cache_metrics_response (88-145)
backend/dashboard_metrics/services.py (2)
  • MetricsQueryService (24-447)
  • get_documents_processed (54-86)
backend/dashboard_metrics/models.py (3)
backend/utils/models/organization_mixin.py (2)
  • DefaultOrganizationManagerMixin (26-29)
  • DefaultOrganizationMixin (7-23)
unstract/core/src/unstract/core/metrics/types.py (1)
  • MetricType (8-12)
backend/dashboard_metrics/serializers.py (1)
  • Meta (110-124)
backend/dashboard_metrics/capture.py (5)
unstract/core/src/unstract/core/metrics/types.py (2)
  • MetricName (15-32)
  • MetricType (8-12)
unstract/core/src/unstract/core/metrics/__init__.py (1)
  • record (58-119)
unstract/core/src/unstract/core/metrics/backends/base.py (1)
  • record (15-24)
unstract/core/src/unstract/core/metrics/backends/noop.py (1)
  • record (25-40)
unstract/core/src/unstract/core/metrics/backends/queue.py (1)
  • record (117-156)
unstract/core/src/unstract/core/metrics/backends/queue.py (3)
unstract/core/src/unstract/core/metrics/types.py (1)
  • MetricEvent (52-96)
unstract/core/src/unstract/core/metrics/backends/base.py (4)
  • AbstractMetricBackend (8-40)
  • close (34-40)
  • record (15-24)
  • flush (27-32)
unstract/core/src/unstract/core/metrics/__init__.py (1)
  • record (58-119)
unstract/core/src/unstract/core/metrics/config.py (3)
unstract/core/src/unstract/core/metrics/backends/base.py (1)
  • AbstractMetricBackend (8-40)
unstract/core/src/unstract/core/metrics/backends/noop.py (1)
  • NoopBackend (14-44)
unstract/core/src/unstract/core/metrics/backends/queue.py (1)
  • QueueBackend (28-215)
backend/dashboard_metrics/serializers.py (2)
backend/dashboard_metrics/models.py (4)
  • EventMetricsHourly (39-142)
  • Meta (109-142)
  • Meta (215-248)
  • Meta (321-354)
backend/dashboard_metrics/views.py (2)
  • series (113-189)
  • summary (70-109)
backend/dashboard_metrics/migrations/0001_initial.py (1)
backend/dashboard_metrics/migrations/0003_add_daily_monthly_tables.py (1)
  • Migration (10-261)
unstract/core/src/unstract/core/metrics/backends/__init__.py (3)
unstract/core/src/unstract/core/metrics/backends/base.py (1)
  • AbstractMetricBackend (8-40)
unstract/core/src/unstract/core/metrics/backends/noop.py (1)
  • NoopBackend (14-44)
unstract/core/src/unstract/core/metrics/backends/queue.py (1)
  • QueueBackend (28-215)
backend/dashboard_metrics/migrations/0004_update_cleanup_periodic_tasks.py (2)
backend/dashboard_metrics/migrations/0002_setup_cleanup_periodic_task.py (1)
  • Migration (40-51)
backend/dashboard_metrics/migrations/0003_add_daily_monthly_tables.py (1)
  • Migration (10-261)
backend/dashboard_metrics/cache.py (3)
backend/utils/user_context.py (1)
  • UserContext (8-32)
backend/utils/cache_service.py (1)
  • clear_cache (41-43)
backend/dashboard_metrics/views.py (1)
  • overview (193-245)
🪛 Ruff (0.14.10)
unstract/core/src/unstract/core/metrics/backends/noop.py

25-25: Unused method argument: event

(ARG002)

unstract/core/src/unstract/core/metrics/__init__.py

39-53: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)


113-113: Do not catch blind exception: Exception

(BLE001)


114-114: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

backend/backend/celery_config.py

42-49: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


56-66: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


69-73: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

unstract/core/src/unstract/core/metrics/registry.py

38-38: Avoid specifying long messages outside the exception class

(TRY003)

backend/dashboard_metrics/migrations/0002_setup_cleanup_periodic_task.py

6-6: Unused function argument: schema_editor

(ARG001)


33-33: Unused function argument: schema_editor

(ARG001)


41-44: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


46-51: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

backend/dashboard_metrics/tasks.py

184-184: Do not catch blind exception: Exception

(BLE001)


257-257: Do not catch blind exception: Exception

(BLE001)


258-258: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


311-311: Do not catch blind exception: Exception

(BLE001)


312-312: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


365-365: Do not catch blind exception: Exception

(BLE001)


366-366: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

backend/dashboard_metrics/tests/test_tasks.py

149-149: Loop control variable i not used within loop body

Rename unused i to _i

(B007)

backend/dashboard_metrics/views.py

47-47: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


48-48: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


50-50: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


51-51: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


52-52: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


60-60: Abstract raise to an inner function

(TRY301)


60-60: Avoid specifying long messages outside the exception class

(TRY003)


65-65: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


66-66: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


66-66: Avoid specifying long messages outside the exception class

(TRY003)


193-193: Unused method argument: request

(ARG002)


359-359: Do not catch blind exception: Exception

(BLE001)


360-360: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


403-403: Unused method argument: request

(ARG002)


436-436: Consider moving this statement to an else block

(TRY300)


437-437: Do not catch blind exception: Exception

(BLE001)


438-438: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


448-448: Consider moving this statement to an else block

(TRY300)


449-449: Do not catch blind exception: Exception

(BLE001)


450-450: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

backend/dashboard_metrics/models.py

113-130: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


131-142: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


219-236: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


237-248: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


325-342: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


343-354: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

backend/dashboard_metrics/capture.py

45-45: Consider moving this statement to an else block

(TRY300)

unstract/core/src/unstract/core/metrics/backends/queue.py

95-96: try-except-pass detected, consider logging the exception

(S110)


95-95: Do not catch blind exception: Exception

(BLE001)


101-101: Consider moving this statement to an else block

(TRY300)


103-103: Do not catch blind exception: Exception

(BLE001)


113-113: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


115-115: Avoid specifying long messages outside the exception class

(TRY003)


149-149: Consider moving this statement to an else block

(TRY300)


152-152: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


154-154: Do not catch blind exception: Exception

(BLE001)


155-155: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


212-212: Do not catch blind exception: Exception

(BLE001)

backend/dashboard_metrics/migrations/0003_add_daily_monthly_tables.py

11-14: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


16-261: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

backend/dashboard_metrics/serializers.py

112-123: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

backend/dashboard_metrics/migrations/0001_initial.py

13-15: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


17-136: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

backend/dashboard_metrics/migrations/0004_update_cleanup_periodic_tasks.py

9-9: Unused function argument: schema_editor

(ARG001)


60-60: Unused function argument: schema_editor

(ARG001)


69-72: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


74-79: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

backend/dashboard_metrics/cache.py

84-84: Probable use of insecure hash functions in hashlib: md5

(S324)


163-163: Consider moving this statement to an else block

(TRY300)


164-164: Do not catch blind exception: Exception

(BLE001)


165-165: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


252-252: Do not catch blind exception: Exception

(BLE001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (28)
backend/dashboard_metrics/__init__.py (1)

1-4: LGTM!

Clean package initializer with clear documentation describing the Dashboard Metrics app's purpose.

backend/dashboard_metrics/migrations/__init__.py (1)

1-1: LGTM!

Standard migrations package initializer following Django conventions.

backend/backend/settings/base.py (1)

345-345: LGTM!

The dashboard_metrics app is correctly registered in SHARED_APPS, making it available across all organizations in the multi-tenant setup. This is appropriate for organization-scoped metrics collection and querying.

unstract/core/src/unstract/core/metrics/backends/__init__.py (1)

1-11: LGTM!

Clean public API definition for the metrics backends package. The __all__ declaration correctly matches the imported backends, providing clear control over the public interface.

backend/dashboard_metrics/apps.py (1)

6-11: LGTM!

Standard Django app configuration following Django 4.2+ best practices. The BigAutoField default is appropriate, and the app naming is correct.

backend/dashboard_metrics/tests/__init__.py (1)

1-1: Tests package init is fine

Docstring-only __init__ is harmless and documents the test package clearly.

unstract/core/src/unstract/core/metrics/backends/base.py (1)

1-40: LGTM! Clean abstract interface design.

The abstract base class follows SOLID principles with a minimal, focused interface. The concrete close() method with a default implementation calling flush() is a good design choice that simplifies backend implementations while allowing customization when needed.

unstract/core/src/unstract/core/metrics/backends/noop.py (1)

21-44: LGTM! Appropriate no-op implementation.

The NoopBackend correctly implements the abstract interface and provides helpful one-time logging to inform users that metrics are disabled. The event parameter in record() is required by the AbstractMetricBackend interface, so the static analysis hint (ARG002) is a false positive.

backend/dashboard_metrics/tests/test_tasks.py (1)

1-553: Excellent test coverage!

The test suite is comprehensive and well-organized, covering time truncation helpers, batch processing, bulk upsert operations, cleanup tasks, and full integration flows. Good use of fixtures, edge cases, and assertions to verify data integrity across all metrics tables.

backend/dashboard_metrics/migrations/0002_setup_cleanup_periodic_task.py (1)

1-51: LGTM! Migration follows Django conventions.

The migration correctly sets up a periodic cleanup task with proper forward and rollback operations. The static analysis hints (ARG001, RUF012) are false positives—the schema_editor parameter and migration class attributes follow Django's required conventions for data migrations.

unstract/core/src/unstract/core/metrics/config.py (1)

1-68: LGTM! Well-designed configuration module.

The environment-driven backend selection with singleton caching is clean and robust. The fallback to NoopBackend when metrics are disabled or misconfigured ensures safe defaults, and the logging at each decision point aids troubleshooting. The reset_backend() function is a good addition for testing scenarios.

unstract/core/src/unstract/core/metrics/__init__.py (1)

58-119: Well-structured public API.

The record() function provides a clean entry point with proper validation, enum-to-string conversion, and graceful error handling that returns False on failures rather than propagating exceptions. The design appropriately decouples the public API from backend implementation details.

unstract/core/src/unstract/core/metrics/registry.py (1)

1-60: Clean registry implementation.

The registry provides efficient O(1) validation via set lookup, clear separation between validation and type retrieval, and appropriate error handling with ValueError for unknown metrics.

backend/backend/celery_config.py (2)

36-53: Good production-ready resilience settings.

The broker connection retry configuration with exponential backoff (interval_step, interval_max), socket keepalive, and worker limits (max_tasks_per_child) will help maintain stability under broker disruptions and prevent memory leaks from long-running workers.


68-73: Task names in task_routes are correctly defined and match registered Celery tasks.

The task names (dashboard_metrics.process_events, dashboard_metrics.cleanup_hourly_data, dashboard_metrics.cleanup_daily_data) match their corresponding @shared_task definitions in backend/dashboard_metrics/tasks.py. Routing is correctly configured.

unstract/core/src/unstract/core/metrics/types.py (1)

8-32: Well-structured metric type definitions.

The dual inheritance from str and Enum enables both type safety and string comparisons. The separation of counters (summed values) and histograms (distributions) is clearly documented and follows established metrics patterns.

backend/dashboard_metrics/urls.py (1)

17-33: Clean URL configuration with proper ordering.

The URL patterns are well-organized with specific paths before the UUID capture pattern, preventing routing conflicts. Using format_suffix_patterns enables content negotiation for API responses.

backend/dashboard_metrics/migrations/0004_update_cleanup_periodic_tasks.py (1)

60-65: Reverse migration is correct as written.

Migration 0002 creates the dashboard_metrics_cleanup_hourly task. Migration 0004's forward operation uses update_or_create on that existing task (not creating a new one), adds the daily task, and deletes a legacy task. The reverse should return to the state after 0002, which means retaining the hourly task (still managed by 0002's reverse function) and removing only what 0004 added—the daily task. The current implementation is correct.

Likely an incorrect or invalid review comment.

unstract/core/src/unstract/core/metrics/backends/queue.py (1)

178-198: Celery protocol v2 format is compatible with the project's Celery version (5.3.4+).

The message construction follows the standard kombu protocol v2 format [args, kwargs, embed], which remains compatible with Celery 5.3.4+ (and 5.5.3 in the workers package). The headers correctly specify the task name and ID for Celery task routing. No compatibility concerns.

backend/dashboard_metrics/views.py (1)

402-450: Health checks look sound and side‑effect free.

health, _check_database, and _check_cache do minimal, safe operations (an exists() call and a short‑lived cache set/get) and return structured status with appropriate HTTP 200/503 responses; this is suitable for liveness/readiness probes.

backend/dashboard_metrics/migrations/0003_add_daily_monthly_tables.py (1)

16-260: Schema and indexing for daily/monthly tables look consistent and appropriate.

The daily/monthly models mirror the hourly schema (including labels default, project/tag, organization FK, unique constraints, and GIN/index coverage), which should give predictable semantics and good query performance across all granularities.

backend/dashboard_metrics/services.py (2)

383-447: get_all_metrics_summary aggregates are consistent with individual query methods.

The summary method correctly reuses the per‑metric query functions and normalizes potential NULL sums (e.g., pages and LLM usage) with or 0, producing a clean dict of totals for the live summary endpoint.


230-269: The join logic in get_deployed_api_requests is correct.

WorkflowExecution.pipeline_id is a UUIDField designed to hold IDs of either Pipeline or APIDeployment entities (as evidenced by the conditional logic in backend/workflow_manager/internal_serializers.py). The subquery filtering pipeline_id__in=Subquery(APIDeployment.objects.filter(...).values("id")) correctly extracts deployed API request counts by matching executions where the pipeline_id refers to an API deployment.

backend/dashboard_metrics/migrations/0001_initial.py (1)

17-135: Hourly metrics migration aligns with model definition and indexing strategy.

Field definitions, indexes, and the unique_hourly_metric constraint all match the EventMetricsHourly model description, so the initial schema for hourly metrics looks correct and production‑ready.

backend/dashboard_metrics/serializers.py (1)

11-124: Query and response serializers are consistent with view usage and enforce sane bounds.

MetricsQuerySerializer.validate() correctly defaults the date range, enforces start_date <= end_date, and caps queries at 90 days, while the nested series/summary serializers match the shapes constructed in DashboardMetricsViewSet. The EventMetricsHourlySerializer is read‑only and exposes the expected fields.

backend/dashboard_metrics/cache.py (2)

71-145: Caching decorator behavior and keying strategy look solid.

The decorator scopes cache entries by organization, endpoint name, and a stable hash of sorted query params, and only caches 200 responses. It also cleanly bypasses caching when no organization context is present, which is important for correctness with per‑org data.


148-167: No changes needed – code is correct.

CacheService is a class in backend/utils/cache_service.py, and clear_cache is a static method of that class. The import and call in invalidate_metrics_cache are correct. The implementation will properly invalidate cached metrics for the organization.

Likely an incorrect or invalid review comment.

backend/dashboard_metrics/models.py (1)

57-85: Confirm BaseModel doesn’t define id, and that metric_count’s default is intentional

Two small checks:

  • If BaseModel already defines an id field, redefining id here as a primary key can cause confusion or conflicts. If BaseModel only adds timestamps and no id, you’re fine; otherwise, consider centralizing the PK definition in one place.
  • metric_count defaults to 1. That makes sense if every row is always created from at least one event, but if you ever create rows with explicit metric_count or rely on DB-side aggregation, this default could overcount silently.

Please double-check that BaseModel does not define id, and that you never rely on the default metric_count for anything other than “one event just created this row”.

Comment on lines +51 to +338
class MetricsCapture:
"""Helper class for capturing metrics at integration points."""

@staticmethod
def record_api_request(
org_id: str,
api_name: str | None = None,
project: str = "default",
labels: dict[str, Any] | None = None,
) -> bool:
"""Record an API deployment request.
Args:
org_id: Organization ID
api_name: Name of the API deployment
project: Project identifier
labels: Additional labels
Returns:
True if recorded successfully
"""
if not METRICS_ENABLED:
return False

record, MetricName, _ = _get_metrics_module()
if record is None:
return False

metric_labels = labels or {}
if api_name:
metric_labels["api_name"] = api_name

return record(
MetricName.DEPLOYED_API_REQUESTS,
org_id=org_id,
value=1,
project=project,
labels=metric_labels,
)

@staticmethod
def record_etl_execution(
org_id: str,
pipeline_name: str | None = None,
project: str = "default",
labels: dict[str, Any] | None = None,
) -> bool:
"""Record an ETL pipeline execution.
Args:
org_id: Organization ID
pipeline_name: Name of the pipeline
project: Project identifier
labels: Additional labels
Returns:
True if recorded successfully
"""
if not METRICS_ENABLED:
return False

record, MetricName, _ = _get_metrics_module()
if record is None:
return False

metric_labels = labels or {}
if pipeline_name:
metric_labels["pipeline_name"] = pipeline_name

return record(
MetricName.ETL_PIPELINE_EXECUTIONS,
org_id=org_id,
value=1,
project=project,
labels=metric_labels,
)

@staticmethod
def record_document_processed(
org_id: str,
pages: int = 1,
file_type: str | None = None,
project: str = "default",
labels: dict[str, Any] | None = None,
) -> bool:
"""Record document and page processing.
Args:
org_id: Organization ID
pages: Number of pages processed
file_type: File type (pdf, docx, etc.)
project: Project identifier
labels: Additional labels
Returns:
True if recorded successfully
"""
if not METRICS_ENABLED:
return False

record, MetricName, _ = _get_metrics_module()
if record is None:
return False

metric_labels = labels or {}
if file_type:
metric_labels["file_type"] = file_type

# Record document
doc_result = record(
MetricName.DOCUMENTS_PROCESSED,
org_id=org_id,
value=1,
project=project,
labels=metric_labels,
)

# Record pages
pages_result = record(
MetricName.PAGES_PROCESSED,
org_id=org_id,
value=pages,
project=project,
labels=metric_labels,
)

return doc_result and pages_result

@staticmethod
def record_llm_call(
org_id: str,
model: str | None = None,
cost: float = 0.0,
tokens: int = 0,
project: str = "default",
labels: dict[str, Any] | None = None,
) -> bool:
"""Record an LLM API call.
Args:
org_id: Organization ID
model: LLM model name
cost: Cost in dollars
tokens: Total tokens used
project: Project identifier
labels: Additional labels
Returns:
True if recorded successfully
"""
if not METRICS_ENABLED:
return False

record, MetricName, _ = _get_metrics_module()
if record is None:
return False

metric_labels = labels or {}
if model:
metric_labels["model"] = model
if tokens:
metric_labels["tokens"] = str(tokens)

# Record the call
call_result = record(
MetricName.LLM_CALLS,
org_id=org_id,
value=1,
project=project,
labels=metric_labels,
)

# Record usage cost if provided
if cost > 0:
record(
MetricName.LLM_USAGE,
org_id=org_id,
value=cost,
project=project,
labels=metric_labels,
)

return call_result

@staticmethod
def record_challenge(
org_id: str,
challenge_type: str | None = None,
project: str = "default",
labels: dict[str, Any] | None = None,
) -> bool:
"""Record an LLM challenge call.
Args:
org_id: Organization ID
challenge_type: Type of challenge
project: Project identifier
labels: Additional labels
Returns:
True if recorded successfully
"""
if not METRICS_ENABLED:
return False

record, MetricName, _ = _get_metrics_module()
if record is None:
return False

metric_labels = labels or {}
if challenge_type:
metric_labels["type"] = challenge_type

return record(
MetricName.CHALLENGES,
org_id=org_id,
value=1,
project=project,
labels=metric_labels,
)

@staticmethod
def record_summarization(
org_id: str,
project: str = "default",
labels: dict[str, Any] | None = None,
) -> bool:
"""Record a summarization call.
Args:
org_id: Organization ID
project: Project identifier
labels: Additional labels
Returns:
True if recorded successfully
"""
if not METRICS_ENABLED:
return False

record, MetricName, _ = _get_metrics_module()
if record is None:
return False

return record(
MetricName.SUMMARIZATION_CALLS,
org_id=org_id,
value=1,
project=project,
labels=labels,
)

@staticmethod
def record_prompt_execution(
org_id: str,
prompt_name: str | None = None,
project: str = "default",
labels: dict[str, Any] | None = None,
) -> bool:
"""Record a prompt studio execution.
Args:
org_id: Organization ID
prompt_name: Name of the prompt
project: Project identifier
labels: Additional labels
Returns:
True if recorded successfully
"""
if not METRICS_ENABLED:
return False

record, MetricName, _ = _get_metrics_module()
if record is None:
return False

metric_labels = labels or {}
if prompt_name:
metric_labels["prompt_name"] = prompt_name

return record(
MetricName.PROMPT_EXECUTIONS,
org_id=org_id,
value=1,
project=project,
labels=metric_labels,
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix incompatible calls to core record() – current signature will raise at runtime.

All MetricsCapture methods call record() with MetricName as the first positional argument and keyword arguments like org_id= and value=, but the provided core signature is:

def record(
    org_id: str,
    metric_name: str | MetricName,
    metric_value: int | float,
    labels: dict[str, str] | None = None,
    project: str | None = None,
    tag: str | None = None,
) -> bool:
    ...

With the current calls, Python will bind the first positional (MetricName.*) to org_id and then also receive org_id= as a keyword, resulting in TypeError: record() got multiple values for argument 'org_id' (and value is not a valid keyword). This means metrics recording will fail at runtime and can break any call site that uses MetricsCapture.

Proposed fix pattern (apply to all `record_*` methods)

Change each call from the current pattern:

return record(
    MetricName.DEPLOYED_API_REQUESTS,
    org_id=org_id,
    value=1,
    project=project,
    labels=metric_labels,
)

to using the correct parameter names and order, for example:

-        return record(
-            MetricName.DEPLOYED_API_REQUESTS,
-            org_id=org_id,
-            value=1,
-            project=project,
-            labels=metric_labels,
-        )
+        return record(
+            org_id=org_id,
+            metric_name=MetricName.DEPLOYED_API_REQUESTS,
+            metric_value=1,
+            labels=metric_labels,
+            project=project,
+        )

Apply the same pattern to:

  • record_etl_execution (MetricName.ETL_PIPELINE_EXECUTIONS)
  • record_document_processed (both DOCUMENTS_PROCESSED and PAGES_PROCESSED)
  • record_llm_call (LLM_CALLS and LLM_USAGE)
  • record_challenge (CHALLENGES)
  • record_summarization (SUMMARIZATION_CALLS)
  • record_prompt_execution (PROMPT_EXECUTIONS)
🤖 Prompt for AI Agents
In @backend/dashboard_metrics/capture.py around lines 51-338, MetricsCapture
methods call record() with the metric enum as the first positional arg and
keyword names like org_id= and value=, but core record signature expects org_id
first, metric_name second and metric_value third, so change every call in
record_api_request, record_etl_execution, record_document_processed (both
DOCUMENTS_PROCESSED and PAGES_PROCESSED), record_llm_call (LLM_CALLS and
LLM_USAGE), record_challenge, record_summarization, and record_prompt_execution
to match the core signature: call record(org_id, MetricName.<X>, <metric_value>,
labels=metric_labels, project=project, tag=None) (use 1 for single-count events,
pages for pages, cost for LLM_USAGE as float), and remove the invalid value=
keyword and duplicate org_id keyword usage.

Comment on lines +131 to +142
constraints = [
models.UniqueConstraint(
fields=[
"organization",
"timestamp",
"metric_name",
"project",
"tag",
],
name="unique_hourly_metric",
)
]
Copy link
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

🏁 Script executed:

find . -type f -name "models.py" -path "*/dashboard_metrics/*" | head -20

Repository: Zipstack/unstract

Length of output: 98


🏁 Script executed:

# Read the models.py file to see the structure and check lines around 131-142, 237-248, 343-354
wc -l backend/dashboard_metrics/models.py

Repository: Zipstack/unstract

Length of output: 100


🏁 Script executed:

# Read the specific sections mentioned in the review
echo "=== Lines 131-142 ===" && sed -n '131,142p' backend/dashboard_metrics/models.py
echo ""
echo "=== Lines 237-248 ===" && sed -n '237,248p' backend/dashboard_metrics/models.py
echo ""
echo "=== Lines 343-354 ===" && sed -n '343,354p' backend/dashboard_metrics/models.py

Repository: Zipstack/unstract

Length of output: 1127


🏁 Script executed:

# Let's get more context around each section to understand the model definitions
echo "=== Context for first model (lines 120-160) ===" && sed -n '120,160p' backend/dashboard_metrics/models.py
echo ""
echo "=== Context for second model (lines 225-265) ===" && sed -n '225,265p' backend/dashboard_metrics/models.py
echo ""
echo "=== Context for third model (lines 330-370) ===" && sed -n '330,370p' backend/dashboard_metrics/models.py

Repository: Zipstack/unstract

Length of output: 3810


🏁 Script executed:

# Search for the tag field definition across the models to confirm it's nullable
rg "tag\s*=" backend/dashboard_metrics/models.py -A 2 -B 2 | head -40

Repository: Zipstack/unstract

Length of output: 459


🏁 Script executed:

# Get a better view of the entire models.py structure to find class definitions and tag fields
cat -n backend/dashboard_metrics/models.py | grep -E "(^[[:space:]]*[0-9]+[[:space:]]*(class|tag|null))" | head -50

Repository: Zipstack/unstract

Length of output: 990


🏁 Script executed:

# Read the whole file to understand the structure
head -400 backend/dashboard_metrics/models.py

Repository: Zipstack/unstract

Length of output: 11051


🏁 Script executed:

# Search for tag field definition with null parameter
rg "tag\s*=\s*models\." backend/dashboard_metrics/models.py -A 1 | head -20

Repository: Zipstack/unstract

Length of output: 219


🏁 Script executed:

# Search for any field named tag and check all its properties
ast-grep --pattern 'tag = models.$_($$$)' backend/dashboard_metrics/models.py

Repository: Zipstack/unstract

Length of output: 1226


🏁 Script executed:

# Read the full file content 
python3 << 'EOF'
with open('backend/dashboard_metrics/models.py', 'r') as f:
    lines = f.readlines()
    
# Print lines around the constraint locations
print("=== Lines 131-142 ===")
for i in range(130, 142):
    print(f"{i+1:4d}: {lines[i]}", end='')

print("\n\n=== Lines 237-248 ===")
for i in range(236, 248):
    print(f"{i+1:4d}: {lines[i]}", end='')
    
print("\n\n=== Lines 343-354 ===")
for i in range(342, 354):
    print(f"{i+1:4d}: {lines[i]}", end='')

# Search for tag field definition
print("\n\n=== Searching for tag field definition ===")
for i, line in enumerate(lines):
    if 'tag' in line.lower() and 'models.' in line and '=' in line:
        print(f"{i+1:4d}: {line}", end='')
        if i+1 < len(lines):
            print(f"{i+2:4d}: {lines[i+1]}", end='')
EOF

Repository: Zipstack/unstract

Length of output: 1579


🏁 Script executed:

ls -la backend/dashboard_metrics/models.py

Repository: Zipstack/unstract

Length of output: 146


🏁 Script executed:

# Read the entire models.py file
cat backend/dashboard_metrics/models.py

Repository: Zipstack/unstract

Length of output: 11051


🏁 Script executed:

find . -name "models.py" -type f | grep dashboard

Repository: Zipstack/unstract

Length of output: 98


🏁 Script executed:

find . -name "models.py" -type f | head -20

Repository: Zipstack/unstract

Length of output: 927


🏁 Script executed:

git ls-files | grep -i dashboard | grep -i models

Repository: Zipstack/unstract

Length of output: 96


🏁 Script executed:

git ls-files | grep models.py

Repository: Zipstack/unstract

Length of output: 1871


Nullable tag in unique constraint allows duplicate NULL rows in PostgreSQL

The unique constraints in all three metric models include the nullable tag field. In PostgreSQL, NULL values are considered distinct in UNIQUE constraints, allowing multiple rows with identical (organization, timestamp/date/month, metric_name, project) values when tag is NULL.

If you expect exactly one row per combination when no tag is specified, the current constraint will not enforce this.

Solutions:

  • Make tag non-nullable with a default value (e.g., "default" or "")
  • Use a partial unique index with WHERE tag IS NOT NULL plus application-level checks for NULL cases
  • Use PostgreSQL's NULLS NOT DISTINCT option (Django 4.2+) in the UniqueConstraint
Affected constraints:
# EventMetricsHourly (lines 131-142)
models.UniqueConstraint(
    fields=["organization", "timestamp", "metric_name", "project", "tag"],
    name="unique_hourly_metric",
)

# EventMetricsDaily (lines 237-248)
models.UniqueConstraint(
    fields=["organization", "date", "metric_name", "project", "tag"],
    name="unique_daily_metric",
)

# EventMetricsMonthly (lines 343-354)
models.UniqueConstraint(
    fields=["organization", "month", "metric_name", "project", "tag"],
    name="unique_monthly_metric",
)
🧰 Tools
🪛 Ruff (0.14.10)

131-142: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

Comment on lines +209 to +260
def _bulk_upsert_hourly(aggregations: dict) -> tuple[int, int]:
"""Bulk upsert hourly aggregations.
Args:
aggregations: Dict of aggregated metric data keyed by
(org_id, hour_ts_str, metric_name, project, tag)
Returns:
Tuple of (created_count, updated_count)
"""
created, updated = 0, 0
with transaction.atomic():
for key, agg in aggregations.items():
org_id, hour_ts_str, metric_name, project, tag = key
hour_ts = datetime.fromisoformat(hour_ts_str)

try:
obj, was_created = (
EventMetricsHourly.objects.select_for_update().get_or_create(
organization_id=org_id,
timestamp=hour_ts,
metric_name=metric_name,
project=project,
tag=tag,
defaults={
"metric_type": agg["metric_type"],
"metric_value": agg["value"],
"metric_count": agg["count"],
"labels": agg["labels"],
},
)
)
if was_created:
created += 1
else:
obj.metric_value = F("metric_value") + agg["value"]
obj.metric_count = F("metric_count") + agg["count"]
if agg["labels"]:
obj.labels = {**obj.labels, **agg["labels"]}
obj.save(
update_fields=[
"metric_value",
"metric_count",
"labels",
"modified_at",
]
)
updated += 1
except Exception as e:
logger.error(f"Error upserting hourly metric {key}: {e}")

return created, updated
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

DB errors are swallowed inside transactions, defeating Celery autoretry and risking silent data loss

In _bulk_upsert_hourly/_daily/_monthly and in the cleanup tasks:

  • Each function wraps DB work in transaction.atomic() but also catches a broad Exception inside the block and only logs it.
  • For the Celery tasks (process_dashboard_metric_events, cleanup_hourly_metrics, cleanup_daily_metrics) you’ve configured autoretry_for=(DatabaseError, OperationalError), but those DB exceptions are caught and converted into logs/return dicts, so Celery never sees them and won’t retry.
  • Catching DB errors inside a single atomic() block without calling transaction.set_rollback(True) or re‑raising can also leave the transaction marked for rollback and cause subsequent operations in the same block to fail, leading to partial or fully rolled-back writes while the task still reports success.

This combination can make metrics silently incomplete under DB issues (locks, timeouts, transient failures) and is at odds with the production-readiness goal.

I’d recommend:

  • In the upsert helpers: let DatabaseError/OperationalError bubble up so Celery can apply its retry/backoff policy. If you want per-key resilience, move try/except outside the atomic() block or drastically narrow the except clause and re‑raise DB errors.

  • In the cleanup tasks: don’t catch DatabaseError/OperationalError at all (or catch, log with logger.exception(...), then raise) so autoretry_for actually takes effect. Keep a separate except Exception handler only for truly unexpected non-DB errors if you still want to return a failure dict.

Example pattern for the hourly upsert (same idea applies to daily/monthly and cleanup):

with transaction.atomic():
    for key, agg in aggregations.items():
        org_id, hour_ts_str, metric_name, project, tag = key
        hour_ts = datetime.fromisoformat(hour_ts_str)
        obj, was_created = (
            EventMetricsHourly.objects.select_for_update().get_or_create(
                organization_id=org_id,
                timestamp=hour_ts,
                metric_name=metric_name,
                project=project,
                tag=tag,
                defaults={
                    "metric_type": agg["metric_type"],
                    "metric_value": agg["value"],
                    "metric_count": agg["count"],
                    "labels": agg["labels"],
                },
            )
        )
        # same update logic, no broad except here

and for cleanup:

try:
    deleted_count, _ = EventMetricsHourly.objects.filter(
        timestamp__lt=cutoff
    ).delete()
except (DatabaseError, OperationalError):
    logger.exception("Error during hourly cleanup")
    raise
except Exception:
    logger.exception("Unexpected error during hourly cleanup")
    return {"success": False, "retention_days": retention_days}

This keeps the resilience you want while letting Celery handle transient DB failures correctly.

Also applies to: 263-315, 317-369, 371-414, 424-458

🧰 Tools
🪛 Ruff (0.14.10)

257-257: Do not catch blind exception: Exception

(BLE001)


258-258: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

Comment on lines +9 to +15
metrics_list = DashboardMetricsViewSet.as_view({"get": "list"})
metrics_detail = DashboardMetricsViewSet.as_view({"get": "retrieve"})
metrics_summary = DashboardMetricsViewSet.as_view({"get": "summary"})
metrics_series = DashboardMetricsViewSet.as_view({"get": "series"})
metrics_overview = DashboardMetricsViewSet.as_view({"get": "overview"})
metrics_live_summary = DashboardMetricsViewSet.as_view({"get": "live_summary"})
metrics_live_series = DashboardMetricsViewSet.as_view({"get": "live_series"})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing health endpoint mapping.

The PR description mentions a health endpoint (GET /api/v2/metrics/health/) and the DashboardMetricsViewSet has a health action (per the relevant code snippets), but it's not wired in the URL patterns. Add the health endpoint if it should be exposed.

🔎 Add health endpoint
 metrics_live_summary = DashboardMetricsViewSet.as_view({"get": "live_summary"})
 metrics_live_series = DashboardMetricsViewSet.as_view({"get": "live_series"})
+metrics_health = DashboardMetricsViewSet.as_view({"get": "health"})

 urlpatterns = format_suffix_patterns(
     [
         # Main list endpoint
         path("", metrics_list, name="metrics-list"),
         # Summary statistics
         path("summary/", metrics_summary, name="metrics-summary"),
         # Time series data
         path("series/", metrics_series, name="metrics-series"),
         # Quick overview (last 7 days)
         path("overview/", metrics_overview, name="metrics-overview"),
         # Live data from source tables
         path("live-summary/", metrics_live_summary, name="metrics-live-summary"),
         path("live-series/", metrics_live_series, name="metrics-live-series"),
+        # Health check
+        path("health/", metrics_health, name="metrics-health"),
         # Individual metric detail
         path("<uuid:pk>/", metrics_detail, name="metrics-detail"),
     ]
 )

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @backend/dashboard_metrics/urls.py around lines 9-15, The URL configuration
is missing a mapping for the DashboardMetricsViewSet.health action; add a new
view mapping similar to the others (e.g., create metrics_health =
DashboardMetricsViewSet.as_view({"get": "health"})) and include it in the URL
patterns so GET /api/v2/metrics/health/ is exposed; update any
import/urlpatterns list references where other metrics_* views are registered to
include metrics_health.

Comment on lines +267 to +273
organization = UserContext.get_organization()
org_id = str(organization.id)

summary = MetricsQueryService.get_all_metrics_summary(
org_id,
params["start_date"],
params["end_date"],
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Guard against missing organization context in live_summary / live_series.

Both live_summary and live_series assume UserContext.get_organization() returns a valid object and immediately access .id. If the organization context is not set or lookup fails, this will raise an AttributeError and return a 500 instead of a clean 4xx error, unlike get_queryset() which explicitly handles the no‑org case.

Consider mirroring the get_queryset() pattern here (log + PermissionDenied) before using organization.id.

Proposed fix (pattern for both live endpoints)
@@
-        organization = UserContext.get_organization()
-        org_id = str(organization.id)
+        organization = UserContext.get_organization()
+        if not organization:
+            logger.warning("No organization context for live metrics request")
+            raise PermissionDenied("No organization context")
+
+        org_id = str(organization.id)
@@
-        organization = UserContext.get_organization()
-        org_id = str(organization.id)
+        organization = UserContext.get_organization()
+        if not organization:
+            logger.warning("No organization context for live metrics request")
+            raise PermissionDenied("No organization context")
+
+        org_id = str(organization.id)

Also applies to: 310-312

🤖 Prompt for AI Agents
In @backend/dashboard_metrics/views.py around lines 267-273, Both live_summary
and live_series call UserContext.get_organization() and access .id directly; add
the same guard used in get_queryset(): if organization is None (or falsy) log an
explanatory message and raise PermissionDenied before accessing organization.id,
then pass org_id to MetricsQueryService.get_all_metrics_summary /
get_all_metrics_series; apply the same check for the other occurrence around
lines referenced (the block using UserContext.get_organization() /
organization.id at ~310-312).

Comment on lines +63 to +67
def _get_connection(self) -> Connection:
"""Get or create a connection to the broker with retry logic."""
if self._connection is not None and self._connection.connected:
return self._connection
return self._get_connection_with_retry()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Connection connected property may return stale state.

The _connection.connected check at line 65 may return True even if the broker has disconnected (e.g., broker restart). Consider wrapping the connection reuse in a try-except that falls back to _get_connection_with_retry() on first publish failure, or use Kombu's ensure_connection() pattern.

🔎 Suggested improvement
 def _get_connection(self) -> Connection:
     """Get or create a connection to the broker with retry logic."""
-    if self._connection is not None and self._connection.connected:
-        return self._connection
-    return self._get_connection_with_retry()
+    if self._connection is not None:
+        try:
+            # Verify connection is actually usable
+            self._connection.ensure_connection(max_retries=1)
+            return self._connection
+        except Exception:
+            pass  # Fall through to reconnect
+    return self._get_connection_with_retry()
📝 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
def _get_connection(self) -> Connection:
"""Get or create a connection to the broker with retry logic."""
if self._connection is not None and self._connection.connected:
return self._connection
return self._get_connection_with_retry()
def _get_connection(self) -> Connection:
"""Get or create a connection to the broker with retry logic."""
if self._connection is not None:
try:
# Verify connection is actually usable
self._connection.ensure_connection(max_retries=1)
return self._connection
except Exception:
pass # Fall through to reconnect
return self._get_connection_with_retry()

Comment on lines +117 to +156
def record(self, event: MetricEvent) -> bool:
"""Send a metric event as a Celery task.
Args:
event: The MetricEvent to queue
Returns:
True if the event was queued successfully, False otherwise
"""
try:
event_data = self._create_event_data(event)
task_message = self._create_celery_message(event_data)
conn = self._get_connection()

with conn.Producer() as producer:
producer.publish(
task_message,
exchange=self.exchange,
routing_key=self.queue_name,
declare=[self.queue],
delivery_mode=2, # Persistent
content_type="application/json",
content_encoding="utf-8",
headers={
"task": self.task_name,
"id": str(uuid.uuid4()),
},
)

logger.debug(
f"Queued metric task: {event.metric_name} for org {event.org_id}"
)
return True

except KombuError as e:
logger.error(f"Failed to queue metric {event.metric_name}: {e}")
return False
except Exception as e:
logger.error(f"Unexpected error queuing metric: {e}")
return False
Copy link
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

🌐 Web query:

Kombu Connection thread-safe publishing messages concurrent

💡 Result:

Short answer: Kombu (and the underlying py-amqp transport) is not safe for concurrent publishing from multiple threads using the same Connection/Channel objects. Share a Connection only if you ensure each thread uses its own Channel (or a thread-safe wrapper/pool) or protect access with a lock; better: give each thread its own Connection/Channel or use a connection pool or a thread-safe transport implementation. [1][2][3][4]

Recommendations

  • Do not share Channel objects across threads — channels are channel-bound and can interleave AMQP frames if used concurrently. Use one channel per thread. [3][4]
  • Prefer one producer Connection/Channel per thread, or use a Connection/Channel pool to reuse resources safely. [1][4]
  • If you must share a Connection, serialize all operations on it with a threading.Lock (but this reduces concurrency). [3]
  • Consider using a project that provides a thread-safe kombu transport/wrapper (e.g., kombu-pyamqp-threadsafe) if you need shared connections with multithreading. [1]

Sources

  • kombu-pyamqp-threadsafe project (thread-safe wrapper for kombu/pyamqp). [1]
  • StackOverflow discussion on publishing from consumer/threads with Kombu (issues when mixing consuming/publishing on same thread). [2]
  • Kombu/py-amqp mailing-list / discussion about thread-safety (channels bound to threads; beware interleaved writes). [3]
  • rabbitpy documentation on multi-threaded use (recommend one channel per thread). [4]

🏁 Script executed:

# Find and examine the _get_connection method and class initialization
cd unstract/core/src/unstract/core/metrics/backends && grep -n "_get_connection\|__init__\|_connection" queue.py | head -20

Repository: Zipstack/unstract

Length of output: 828


🏁 Script executed:

cd unstract/core/src/unstract/core/metrics/backends && sed -n '35,110p' queue.py

Repository: Zipstack/unstract

Length of output: 2738


Kombu Connection is not thread-safe—add locking or use connection pooling.

The code caches self._connection as an instance variable without synchronization. Concurrent calls to record() from multiple threads will race on self._connection state (the check on line 65 is not atomic), and Kombu does not support concurrent publishing from the same Connection. For multi-threaded metrics recording, protect the connection with a threading.Lock, use thread-local connections, or implement a connection pool.

🧰 Tools
🪛 Ruff (0.14.10)

149-149: Consider moving this statement to an else block

(TRY300)


152-152: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


154-154: Do not catch blind exception: Exception

(BLE001)


155-155: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

Comment on lines +35 to +48
# Mapping of metric names to their types
METRIC_TYPE_MAP: dict[str, MetricType] = {
# Counters
MetricName.DOCUMENTS_PROCESSED.value: MetricType.COUNTER,
MetricName.PAGES_PROCESSED.value: MetricType.COUNTER,
MetricName.PROMPT_EXECUTIONS.value: MetricType.COUNTER,
MetricName.LLM_CALLS.value: MetricType.COUNTER,
MetricName.CHALLENGES.value: MetricType.COUNTER,
MetricName.SUMMARIZATION_CALLS.value: MetricType.COUNTER,
# Histograms
MetricName.DEPLOYED_API_REQUESTS.value: MetricType.HISTOGRAM,
MetricName.ETL_PIPELINE_EXECUTIONS.value: MetricType.HISTOGRAM,
MetricName.LLM_USAGE.value: MetricType.HISTOGRAM,
}
Copy link
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
# Check if tests exist to verify METRIC_TYPE_MAP completeness
rg -n 'METRIC_TYPE_MAP' --type=py -g '**/test*'

Repository: Zipstack/unstract

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Find MetricName enum definition
rg -n 'class MetricName|enum MetricName' --type=py -A 20

Repository: Zipstack/unstract

Length of output: 1805


🏁 Script executed:

#!/bin/bash
# Find get_metric_type() implementation
rg -n 'def get_metric_type' --type=py -A 10

Repository: Zipstack/unstract

Length of output: 933


🏁 Script executed:

#!/bin/bash
# Check the full types.py file to understand the structure
cat -n unstract/core/src/unstract/core/metrics/types.py

Repository: Zipstack/unstract

Length of output: 3933


🏁 Script executed:

#!/bin/bash
# Get the full get_metric_type() implementation
cat -n unstract/core/src/unstract/core/metrics/registry.py | head -50

Repository: Zipstack/unstract

Length of output: 1566


🏁 Script executed:

#!/bin/bash
# Search for test files in metrics module
fd '.*test.*\.py$' --type f | grep -i metric

Repository: Zipstack/unstract

Length of output: 106


🏁 Script executed:

#!/bin/bash
# Search for any validation or assertions related to METRIC_TYPE_MAP
rg -n 'assert.*METRIC_TYPE_MAP|METRIC_TYPE_MAP.*in|in.*METRIC_TYPE_MAP' --type=py

Repository: Zipstack/unstract

Length of output: 200


METRIC_TYPE_MAP must stay in sync with MetricName.

While validate_metric() checks if a metric exists in the enum, it does not ensure the metric is in METRIC_TYPE_MAP. If a new metric is added to MetricName without updating METRIC_TYPE_MAP, get_metric_type() will raise a KeyError when accessing the map on line 39 of registry.py. Add a unit test to verify all MetricName values have corresponding entries in METRIC_TYPE_MAP.

🤖 Prompt for AI Agents
In @unstract/core/src/unstract/core/metrics/types.py around lines 35-48, Add a
unit test that ensures every value of the MetricName enum has a corresponding
key in METRIC_TYPE_MAP to prevent KeyError in get_metric_type(); specifically,
iterate over MetricName (or MetricName.__members__.values()/MetricName) and
assert that each .value is present in METRIC_TYPE_MAP, referencing
METRIC_TYPE_MAP and MetricName; this complements validate_metric() and prevents
missing entries that would cause get_metric_type() to fail.

Copy link
Contributor

@chandrasekharan-zipstack chandrasekharan-zipstack left a comment

Choose a reason for hiding this comment

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

Changes mostly LGTM. Left some minor comments.

One main question I have is around cache invalidation of the cached, aggregated data - does it happen only on the respective TTLs? I hope possibly receiving stale data for that duration is okay in this usecase

Comment on lines +52 to +53
worker_prefetch_multiplier = 4
worker_max_tasks_per_child = 1000
Copy link
Contributor

Choose a reason for hiding this comment

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

@athul-rs any reason why this or other config was added? I hope it doesn't have any side effects on other workers

Copy link
Contributor

Choose a reason for hiding this comment

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

@athul-rs do these tests run on every PR as part of the existing unit tests? If not, please ensure that its integrated there as well otherwise your effort on these tests won't be realised

CACHE_TTL_CURRENT_HOUR = 30 # 30 seconds for current hour (updating frequently)
CACHE_TTL_HISTORICAL = 8 * 60 * 60 # 8 hours for historical data (stable)

# Legacy TTLs for compatibility with existing endpoints
Copy link
Contributor

Choose a reason for hiding this comment

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

@athul-rs what existing endpoints do we have that require such legacy TTLs? Check and correct this comment


# Cache TTL values (in seconds)
# Time-aware TTLs per documentation
CACHE_TTL_CURRENT_HOUR = 30 # 30 seconds for current hour (updating frequently)
Copy link
Contributor

Choose a reason for hiding this comment

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

NIT: @athul-rs I would suggest defining these values in settings/base.py so that they can be configured with an env too optionally with some sane defaults. Make sure the names are updated / prefixed with DASHBOARD_ to group them separately

"""
# Sort params for consistent hashing
sorted_params = json.dumps(params, sort_keys=True, default=str)
params_hash = hashlib.md5(sorted_params.encode()).hexdigest()[:12]
Copy link
Contributor

Choose a reason for hiding this comment

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

NIT: Possibility for cache collision since we only take first 12 digits, but should suffice mostly since we separate for endpoints. Do check if a change is needed here

Comment on lines +181 to +185
except KeyError as e:
logger.warning(f"Skipping event with missing required field: {e}")
errors += 1
except Exception as e:
logger.warning(f"Error processing event: {e}")
Copy link
Contributor

Choose a reason for hiding this comment

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

NIT: Consider adding a stack trace

Comment on lines +57 to +61
organization = UserContext.get_organization()
if not organization:
logger.warning("No organization context for metrics request")
raise PermissionDenied("No organization context")
return EventMetricsHourly.objects.filter(organization=organization)
Copy link
Contributor

Choose a reason for hiding this comment

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

@athul-rs check if this is required. This would be handled by the model manager itself usually

Copy link
Contributor

Choose a reason for hiding this comment

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

NIT: This is a cool idea and if refactored a bit to make it generic, we can easily extend the same to any other API / DB call that we wish to cache. Consider this suggestion if its easy to make it generic

Comment on lines +8 to +9
from ..types import MetricEvent
from .base import AbstractMetricBackend
Copy link
Contributor

Choose a reason for hiding this comment

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

NIT: Consider absolute imports instead

return self._connection
return self._get_connection_with_retry()

def _get_connection_with_retry(
Copy link
Contributor

Choose a reason for hiding this comment

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

@athul-rs why do we need to implement this? Isn't it available / handled within celery / kombu?

@github-actions
Copy link
Contributor

github-actions bot commented Jan 6, 2026

Test Results

Summary
  • Runner Tests: 11 passed, 0 failed (11 total)
  • SDK1 Tests: 66 passed, 0 failed (66 total)

Runner Tests - Full Report
filepath function $$\textcolor{#23d18b}{\tt{passed}}$$ SUBTOTAL
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_logs}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_cleanup}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_cleanup\_skip}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_client\_init}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_container\_run\_config}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_container\_run\_config\_without\_mount}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_run\_container}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image\_for\_sidecar}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_sidecar\_container}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{TOTAL}}$$ $$\textcolor{#23d18b}{\tt{11}}$$ $$\textcolor{#23d18b}{\tt{11}}$$
SDK1 Tests - Full Report
filepath function $$\textcolor{#23d18b}{\tt{passed}}$$ SUBTOTAL
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_success\_on\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retry\_on\_connection\_error}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_non\_retryable\_http\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retryable\_http\_errors}}$$ $$\textcolor{#23d18b}{\tt{3}}$$ $$\textcolor{#23d18b}{\tt{3}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_post\_method\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retry\_logging}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_success\_on\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_retry\_on\_errors}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_wrapper\_methods\_retry}}$$ $$\textcolor{#23d18b}{\tt{4}}$$ $$\textcolor{#23d18b}{\tt{4}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_connection\_error\_is\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_timeout\_is\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_retryable\_status\_codes}}$$ $$\textcolor{#23d18b}{\tt{3}}$$ $$\textcolor{#23d18b}{\tt{3}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_non\_retryable\_status\_codes}}$$ $$\textcolor{#23d18b}{\tt{5}}$$ $$\textcolor{#23d18b}{\tt{5}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_without\_response}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_os\_error\_retryable\_errno}}$$ $$\textcolor{#23d18b}{\tt{5}}$$ $$\textcolor{#23d18b}{\tt{5}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_os\_error\_non\_retryable\_errno}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_other\_exception\_not\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_exponential\_backoff\_without\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_exponential\_backoff\_with\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_max\_delay\_cap}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_max\_delay\_cap\_with\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_successful\_call\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_retry\_after\_transient\_failure}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_max\_retries\_exceeded}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_max\_time\_exceeded}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_retry\_with\_custom\_predicate}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_no\_retry\_with\_predicate\_false}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_exception\_not\_in\_tuple\_not\_retried}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_delay\_would\_exceed\_max\_time}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_default\_configuration}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_environment\_variable\_configuration}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_max\_retries}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_max\_time}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_base\_delay}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_multiplier}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_jitter\_values}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_custom\_exceptions\_only}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_custom\_predicate\_only}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_both\_exceptions\_and\_predicate}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_exceptions\_match\_but\_predicate\_false}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_retry\_platform\_service\_call\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_retry\_prompt\_service\_call\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_platform\_service\_decorator\_retries\_on\_connection\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_prompt\_service\_decorator\_retries\_on\_timeout}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_warning\_logged\_on\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_info\_logged\_on\_success\_after\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_exception\_logged\_on\_giving\_up}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{TOTAL}}$$ $$\textcolor{#23d18b}{\tt{66}}$$ $$\textcolor{#23d18b}{\tt{66}}$$

@sonarqubecloud
Copy link

sonarqubecloud bot commented Jan 6, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot
8.1% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@athul-rs athul-rs changed the title [FEAT] Add dashboard metrics system with production readiness [FEAT] Dashboard metrics system Jan 8, 2026
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.

3 participants