Skip to content

Instantly share code, notes, and snippets.

@ashwch
Created July 13, 2025 21:31
Show Gist options
  • Select an option

  • Save ashwch/65dc35b651c989355bb924b5bfb09bd8 to your computer and use it in GitHub Desktop.

Select an option

Save ashwch/65dc35b651c989355bb924b5bfb09bd8 to your computer and use it in GitHub Desktop.
Complete output from /bruno-api command - comprehensive API documentation, TypeScript interfaces, React Query hooks, tests, and business logic notes

User Metrics API Documentation

Generated by /bruno-api bruno/analytics/user_metrics.bru

GET /api/v2/companies/{company_id}/user-metrics/

🔐 Authentication: JWT Bearer Token Required
👮 Permissions: IsAuthenticated + CompanyMember
🏢 Multi-tenant: Company-scoped data access
Rate Limit: 100 requests/minute per company

Overview

Retrieves user engagement metrics and activity data for a specific company. This endpoint provides insights into user behavior, session counts, and activity patterns for analytics and reporting purposes.

Implementation Details

  • View: analytics/views.py:UserMetricsViewSet:45
  • Serializer: analytics/serializers.py:UserMetricSerializer:12
  • Permissions: core/permissions.py:CompanyMemberPermission:23
  • URL Pattern: analytics/urls.py:8
  • Model: analytics/models.py:UserMetric:156

Request

Path Parameters

Parameter Type Required Description
company_id UUID Yes Company identifier (validated against user permissions)

Query Parameters

Parameter Type Required Default Description
date_from string No 30 days ago Start date for metrics (YYYY-MM-DD format)
date_to string No today End date for metrics (YYYY-MM-DD format)
status string No all Filter by user status: active, inactive, pending
page integer No 1 Page number for pagination
page_size integer No 20 Results per page (max: 100)
ordering string No -last_active Sort order: last_active, -last_active, total_sessions, -total_sessions

Headers

Header Value Required Description
Authorization Bearer {jwt_token} Yes JWT authentication token
Content-Type application/json Yes Request content type

Example Request

curl -X GET \
  "https://api.example.com/api/v2/companies/f659ecfa-32be-4a9b-9c9b-6e56c3ccf29a/user-metrics/?status=active&page=1&page_size=10" \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
  -H "Content-Type: application/json"

Response

Success Response (200 OK)

{
  "count": 247,
  "next": "https://api.example.com/api/v2/companies/f659ecfa-32be-4a9b-9c9b-6e56c3ccf29a/user-metrics/?page=2",
  "previous": null,
  "results": [
    {
      "user_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "email": "[email protected]",
      "first_name": "John",
      "last_name": "Doe",
      "last_active": "2025-07-13T14:30:00Z",
      "total_sessions": 42,
      "sessions_this_month": 12,
      "avg_session_duration": "00:23:15",
      "status": "active",
      "role": "member",
      "department": "Engineering",
      "created_at": "2024-01-15T09:00:00Z",
      "last_login": "2025-07-13T14:30:00Z"
    },
    {
      "user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "email": "[email protected]",
      "first_name": "Jane",
      "last_name": "Smith",
      "last_active": "2025-07-12T16:45:00Z",
      "total_sessions": 38,
      "sessions_this_month": 15,
      "avg_session_duration": "00:31:42",
      "status": "active",
      "role": "admin",
      "department": "Marketing",
      "created_at": "2024-02-10T10:30:00Z",
      "last_login": "2025-07-12T16:45:00Z"
    }
  ]
}

Error Responses

Authentication Required (401)

{
  "detail": "Authentication credentials were not provided.",
  "code": "authentication_required",
  "timestamp": "2025-07-13T15:30:00Z"
}

Permission Denied (403)

{
  "detail": "You do not have permission to access this company's user metrics.",
  "code": "permission_denied",
  "company_id": "f659ecfa-32be-4a9b-9c9b-6e56c3ccf29a",
  "timestamp": "2025-07-13T15:30:00Z"
}

Company Not Found (404)

{
  "detail": "Company not found or you don't have access to it.",
  "code": "company_not_found",
  "timestamp": "2025-07-13T15:30:00Z"
}

Validation Error (400)

{
  "detail": "Invalid query parameters.",
  "code": "validation_error",
  "errors": {
    "date_from": ["Enter a valid date format (YYYY-MM-DD)."],
    "page_size": ["Ensure this value is less than or equal to 100."]
  },
  "timestamp": "2025-07-13T15:30:00Z"
}

Rate Limit Exceeded (429)

{
  "detail": "Rate limit exceeded. Try again in 60 seconds.",
  "code": "rate_limit_exceeded",
  "retry_after": 60,
  "timestamp": "2025-07-13T15:30:00Z"
}

TypeScript Interfaces

// Request Parameters
interface UserMetricsParams {
  date_from?: string;
  date_to?: string;
  status?: 'active' | 'inactive' | 'pending';
  page?: number;
  page_size?: number;
  ordering?: 'last_active' | '-last_active' | 'total_sessions' | '-total_sessions';
}

// Individual User Metric
interface UserMetric {
  user_id: string;
  email: string;
  first_name: string;
  last_name: string;
  last_active: string;
  total_sessions: number;
  sessions_this_month: number;
  avg_session_duration: string;
  status: 'active' | 'inactive' | 'pending';
  role: string;
  department: string | null;
  created_at: string;
  last_login: string;
}

// Paginated Response
interface UserMetricsResponse {
  count: number;
  next: string | null;
  previous: string | null;
  results: UserMetric[];
}

// Error Response
interface APIError {
  detail: string;
  code: string;
  timestamp: string;
  company_id?: string;
  errors?: Record<string, string[]>;
  retry_after?: number;
}

React Query Implementation

import { useQuery, UseQueryResult } from '@tanstack/react-query';
import { apiClient } from '../services/api';

// Custom hook for user metrics
export const useUserMetrics = (
  companyId: string,
  params?: UserMetricsParams
): UseQueryResult<UserMetricsResponse, APIError> => {
  return useQuery({
    queryKey: ['user-metrics', companyId, params],
    queryFn: async (): Promise<UserMetricsResponse> => {
      const response = await apiClient.get(
        `/api/v2/companies/${companyId}/user-metrics/`,
        { params }
      );
      return response.data;
    },
    enabled: !!companyId,
    staleTime: 2 * 60 * 1000, // 2 minutes
    retry: (failureCount, error: any) => {
      // Don't retry on authentication or permission errors
      if (error?.response?.status === 401 || error?.response?.status === 403) {
        return false;
      }
      // Don't retry on validation errors
      if (error?.response?.status === 400) {
        return false;
      }
      // Retry up to 3 times for other errors
      return failureCount < 3;
    },
    retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
  });
};

// Hook for infinite scrolling
export const useInfiniteUserMetrics = (
  companyId: string,
  params?: Omit<UserMetricsParams, 'page'>
) => {
  return useInfiniteQuery({
    queryKey: ['user-metrics-infinite', companyId, params],
    queryFn: async ({ pageParam = 1 }) => {
      const response = await apiClient.get(
        `/api/v2/companies/${companyId}/user-metrics/`,
        { params: { ...params, page: pageParam } }
      );
      return response.data;
    },
    enabled: !!companyId,
    getNextPageParam: (lastPage) => {
      if (lastPage.next) {
        const url = new URL(lastPage.next);
        return parseInt(url.searchParams.get('page') || '1');
      }
      return undefined;
    },
  });
};

// Mutation for refreshing metrics (if applicable)
export const useRefreshUserMetrics = () => {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: async (companyId: string) => {
      return apiClient.post(`/api/v2/companies/${companyId}/user-metrics/refresh/`);
    },
    onSuccess: (_, companyId) => {
      queryClient.invalidateQueries(['user-metrics', companyId]);
      queryClient.invalidateQueries(['user-metrics-infinite', companyId]);
    },
  });
};

Usage Examples

Basic Usage

const UserMetricsDashboard = ({ companyId }: { companyId: string }) => {
  const {
    data: metrics,
    isLoading,
    error,
    refetch
  } = useUserMetrics(companyId, {
    status: 'active',
    page_size: 50,
    ordering: '-last_active'
  });

  if (isLoading) return <LoadingSpinner />;
  if (error) return <ErrorAlert error={error} onRetry={refetch} />;

  return (
    <div className="user-metrics">
      <h2>User Activity ({metrics?.count} users)</h2>
      <div className="metrics-grid">
        {metrics?.results.map((user) => (
          <UserMetricCard key={user.user_id} user={user} />
        ))}
      </div>
      <Pagination
        currentPage={1}
        totalItems={metrics?.count || 0}
        pageSize={50}
      />
    </div>
  );
};

With Date Filtering

const UserActivityReport = ({ companyId }: { companyId: string }) => {
  const [dateRange, setDateRange] = useState({
    date_from: '2025-06-01',
    date_to: '2025-07-01'
  });

  const { data: metrics } = useUserMetrics(companyId, dateRange);

  return (
    <div>
      <DateRangePicker value={dateRange} onChange={setDateRange} />
      <UserMetricsChart data={metrics?.results} />
    </div>
  );
};

Business Logic Notes

Permission System

  • Company Isolation: Users can only access metrics for companies they're members of
  • Role-based Access: Requires at least 'member' role within the company
  • Multi-tenant Security: All queries are automatically scoped to the user's accessible companies

Data Processing

  • Caching Strategy: Results cached for 2 minutes to balance freshness and performance
  • Database Optimization: Uses select_related() for company and user relationships
  • Audit Trail: All access logged with django-pghistory for compliance tracking

Performance Considerations

  • Pagination: Maximum 100 items per page to prevent large payloads
  • Indexing: Database indexes on company_id, last_active, and status fields
  • Query Optimization: Avoids N+1 queries through proper eager loading

Rate Limiting

  • Per-company Limits: 100 requests per minute per company
  • Sliding Window: Uses Redis-based sliding window algorithm
  • Graceful Degradation: Returns 429 with retry-after header when exceeded

Testing

Unit Tests

describe('useUserMetrics', () => {
  it('should fetch user metrics successfully', async () => {
    const { result } = renderHook(() => 
      useUserMetrics('test-company-id')
    );
    
    await waitFor(() => {
      expect(result.current.data).toBeDefined();
      expect(result.current.data?.results).toHaveLength(2);
    });
  });

  it('should handle authentication errors', async () => {
    mockApiError(401, 'authentication_required');
    
    const { result } = renderHook(() => 
      useUserMetrics('test-company-id')
    );
    
    await waitFor(() => {
      expect(result.current.error?.code).toBe('authentication_required');
      expect(result.current.retry).toBe(false);
    });
  });
});

Integration Tests

# Test with Bruno CLI
bruno run user-metrics.bru --env local
bruno run user-metrics.bru --env staging --filter "status=active"

# Test error scenarios
bruno run user-metrics-errors.bru --env local

Related Endpoints

  • GET /api/v2/companies/{company_id}/user-metrics/summary/ - Aggregated metrics
  • GET /api/v2/companies/{company_id}/user-metrics/export/ - CSV export
  • POST /api/v2/companies/{company_id}/user-metrics/refresh/ - Trigger metrics refresh

Migration Notes

From v1 API

  • Endpoint URL changed from /api/v1/metrics/users/ to /api/v2/companies/{company_id}/user-metrics/
  • Added company-scoped access for better multi-tenancy
  • Removed deprecated include_inactive parameter (use status filter instead)
  • Response format includes additional fields: sessions_this_month, avg_session_duration

Database Changes

  • Added indexes on frequently queried fields
  • Implemented soft deletes for audit compliance
  • Added company_id foreign key for proper isolation

Monitoring and Alerting

Key Metrics to Monitor

  • Response time percentiles (p50, p95, p99)
  • Error rate by status code
  • Rate limit hit frequency
  • Cache hit/miss ratio

Recommended Alerts

  • Error rate > 5% for 5 minutes
  • p95 response time > 2 seconds
  • Rate limit hits > 50/hour per company
  • Cache miss rate > 30%
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment