Plane Analytics and User Activity Tracking Architecture Explained

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, 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, 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 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.

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 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. 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.


# 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 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, 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 and authentication handlers in 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—the system enqueues events rather than processing them synchronously. The 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. 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.

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, 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, and calls build_graph_plot or burndown_plot from 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) manage UI filters and cache API responses for reactive updates
  • Django utilities (analytics_plot.py) perform heavy ORM aggregations using Count, Sum, and ExtractMonth annotations
  • Activity events are defined centrally in analytics_events.py and processed asynchronously via event_tracking_task.py
  • The service layer (analytics.service.ts, 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 and triggered from various Django views like workspace/invite.py. The actual persistence happens asynchronously through 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. 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →