# Plane Analytics and User Activity Tracking Architecture Explained

> Discover the dual-layered architecture of Plane analytics and user activity tracking. Learn how React/MobX frontend and Django backend handle data aggregation and event processing.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: architecture
- Published: 2026-06-22

---

**Plane implements a dual-layered architecture where a React/MobX frontend manages UI state and API communication, while a Django backend handles heavy data aggregation via optimized ORM queries and asynchronous background task processing for activity events.**

Plane (https://github.com/makeplane/plane) uses a sophisticated dual-layered approach for analytics and user activity tracking that separates reactive frontend concerns from server-side computational heavy lifting. The architecture ensures real-time UI responsiveness while delegating complex data aggregation and event persistence to the Django backend. This design allows the system to process large datasets efficiently through database-level annotations rather than in-memory manipulation.

## Frontend Analytics Architecture

### State Management with MobX Stores

The frontend state lives in [`apps/web/core/store/analytics.store.ts`](https://github.com/makeplane/plane/blob/main/apps/web/core/store/analytics.store.ts), which implements a MobX store containing observables for `selectedDuration`, `selectedProjects`, and other filter dimensions. When users interact with analytics filters, actions like `updateSelectedDuration` execute inside `runInAction` blocks to update state and trigger recomputation.

Components access this state through [`apps/web/core/hooks/store/use-analytics.ts`](https://github.com/makeplane/plane/blob/main/apps/web/core/hooks/store/use-analytics.ts), which exposes the `useAnalytics()` hook. UI components such as `AnalyticsWrapper` read from this hook and render charts based on the store's cached data, ensuring reactive updates when underlying observables change.

### Service Layer Communication

The `AnalyticsService` in [`apps/web/core/services/analytics.service.ts`](https://github.com/makeplane/plane/blob/main/apps/web/core/services/analytics.service.ts) constructs URLs for three primary endpoints: `advance-analytics`, `advance-analytics-stats`, and `advance-analytics-charts`. When store actions trigger data fetches, the service builds parameterized requests and returns typed responses to the MobX store for caching.

```typescript
import { useAnalytics } from '@/plane-web/hooks/store/use-analytics';

function DurationFilter() {
  const analytics = useAnalytics();

  const onSelect = (value: DurationType) => {
    analytics.updateSelectedDuration(value); // updates MobX store
    analytics.fetchAnalytics();            // triggers service call
  };

  return <Select onChange={onSelect} ... />;
}

```

## Backend Analytics Engine

### REST API Endpoints

The `AdvanceAnalyticsView` class in [`apps/api/plane/app/views/analytic/advance.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/views/analytic/advance.py) handles REST endpoints at `/workspaces/{slug}/advance-analytics/`. This view receives filter parameters from the frontend, validates them, and delegates to specialized utility functions for data processing.

### Data Aggregation Utilities

Core computation happens in [`apps/api/plane/utils/analytics_plot.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/utils/analytics_plot.py). The `build_graph_plot` function validates axes against `VALID_ANALYTICS_FIELDS`, then uses Django ORM annotations like `Count`, `Sum`, and `ExtractMonth` to aggregate data efficiently at the database level. For burndown visualizations, `burndown_plot` performs similar optimized queries without loading full model instances into memory.

```python

# apps/api/plane/utils/analytics_plot.py

def build_graph_plot(queryset, x_axis, y_axis, segment=None):
    # validate axes

    if x_axis not in VALID_ANALYTICS_FIELDS:
        raise ValueError(...)
    # extract the x‑axis dimension (date -> month string)

    queryset, x_axis = extract_axis(queryset, x_axis)
    # group by the dimension and compute counts or estimates

    queryset = queryset.values(x_axis)
    if y_axis == "issue_count":
        queryset = queryset.annotate(count=Count("*"))
    else:
        queryset = queryset.annotate(estimate=Sum(Cast("estimate_point__value", FloatField())))

    # materialise and group the result

    result = list(queryset)
    grouped = {str(k): list(v) for k, v in groupby(result, key=lambda x: x["dimension"])}
    return sort_data(grouped, x_axis)

```

### Date Range Handling

Supporting functions in [`apps/api/plane/utils/date_utils.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/utils/date_utils.py) calculate filter ranges and provide `get_analytics_filters` for constructing query parameters that the views pass to the aggregation utilities.

## User Activity Tracking System

### Event Definitions and Constants

Activity events are centralized in [`apps/api/plane/utils/analytics_events.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/utils/analytics_events.py), which exports constants like `USER_JOINED_WORKSPACE`. These constants ensure consistent event naming across workspace invitation flows in [`apps/api/plane/app/views/workspace/invite.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/views/workspace/invite.py) and authentication handlers in [`apps/api/plane/authentication/utils/workspace_project_join.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/authentication/utils/workspace_project_join.py).

### Asynchronous Background Processing

When significant actions occur—such as workspace creation in [`apps/api/plane/app/views/workspace/base.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/views/workspace/base.py)—the system enqueues events rather than processing them synchronously. The [`event_tracking_task.py`](https://github.com/makeplane/plane/blob/main/event_tracking_task.py) background worker processes these queues and persists events to the database, preventing request-time latency for user actions.

### Frontend Activity Services

The frontend retrieves activity streams via [`apps/web/core/services/user.service.ts`](https://github.com/makeplane/plane/blob/main/apps/web/core/services/user.service.ts). The `getUserProfileActivity` method calls `/api/workspaces/{workspaceSlug}/user-activity/{userId}/` and returns `IUserActivityResponse` objects. For exports, `downloadProfileActivity` POSTs to `/user-activity/{userId}/export/` to generate CSV downloads.

```typescript
import userService from '@/services/user.service';

async function loadActivity(workspaceSlug: string, userId: string) {
  const resp = await userService.getUserProfileActivity(workspaceSlug, userId, {
    per_page: 20,
  });
  return resp.activities; // array of activity objects for rendering
}

```

## End-to-End Data Flow

A typical analytics request flows through the system as follows:

1. **Filter Change**: A component calls `updateSelectedDuration` in [`analytics.store.ts`](https://github.com/makeplane/plane/blob/main/analytics.store.ts), updating MobX observables inside a `runInAction` block.
2. **API Request**: The store triggers `analyticsService.getAdvanceAnalytics`, which builds the URL and sends a GET request to the Django backend.
3. **Server Processing**: `AdvanceAnalyticsView` receives the request, constructs filters via [`date_utils.py`](https://github.com/makeplane/plane/blob/main/date_utils.py), and calls `build_graph_plot` or `burndown_plot` from [`analytics_plot.py`](https://github.com/makeplane/plane/blob/main/analytics_plot.py).
4. **Data Aggregation**: The utility functions annotate querysets with `Count` and `Sum`, group results by dimension, and return JSON-serializable dictionaries.
5. **Response Handling**: The frontend store caches the response, and React components re-render with the new chart data.

For activity tracking, workspace joins trigger `USER_JOINED_WORKSPACE` constants in the Django views, background tasks queue and persist these events, and the frontend polls `UserService.getUserProfileActivity` to display paginated activity feeds.

## Summary

- Plane uses a **dual-layered architecture** separating React/MobX frontend state from Django backend aggregation
- **MobX stores** ([`analytics.store.ts`](https://github.com/makeplane/plane/blob/main/analytics.store.ts)) manage UI filters and cache API responses for reactive updates
- **Django utilities** ([`analytics_plot.py`](https://github.com/makeplane/plane/blob/main/analytics_plot.py)) perform heavy ORM aggregations using `Count`, `Sum`, and `ExtractMonth` annotations
- **Activity events** are defined centrally in [`analytics_events.py`](https://github.com/makeplane/plane/blob/main/analytics_events.py) and processed asynchronously via [`event_tracking_task.py`](https://github.com/makeplane/plane/blob/main/event_tracking_task.py)
- The **service layer** ([`analytics.service.ts`](https://github.com/makeplane/plane/blob/main/analytics.service.ts), [`user.service.ts`](https://github.com/makeplane/plane/blob/main/user.service.ts)) provides typed API wrappers ensuring consistent communication between layers

## Frequently Asked Questions

### How does Plane handle real-time analytics updates?

The frontend MobX store maintains local state for filters and cached results. When filters change, the store calls the analytics service, which fetches fresh data from Django endpoints. The backend computes aggregations on-demand using optimized ORM queries rather than maintaining real-time connections, ensuring data consistency while keeping the UI responsive.

### Where are activity tracking events stored in Plane?

Events are defined as constants in [`apps/api/plane/utils/analytics_events.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/utils/analytics_events.py) and triggered from various Django views like [`workspace/invite.py`](https://github.com/makeplane/plane/blob/main/workspace/invite.py). The actual persistence happens asynchronously through [`apps/api/plane/bgtasks/event_tracking_task.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/bgtasks/event_tracking_task.py), which processes queued events and stores them in the database, preventing request-time latency for user actions.

### What database queries does Plane use for analytics aggregation?

The backend uses Django ORM annotations in [`apps/api/plane/utils/analytics_plot.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/utils/analytics_plot.py). Specifically, `build_graph_plot` applies `Count("*")` for issue counts and `Sum(Cast("estimate_point__value", FloatField()))` for estimates, along with `ExtractMonth` for date-based grouping. These compile to efficient SQL aggregations that handle large datasets without loading full model instances.

### Can I export user activity data from Plane?

Yes, the frontend service in [`apps/web/core/services/user.service.ts`](https://github.com/makeplane/plane/blob/main/apps/web/core/services/user.service.ts) provides `downloadProfileActivity`, which POSTs to `/user-activity/{userId}/export/`. This endpoint generates a CSV export of the activity stream that was retrieved via `getUserProfileActivity`, allowing administrators to download comprehensive user interaction histories.