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
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.
- 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
| Parameter | Type | Required | Description |
|---|---|---|---|
| company_id | UUID | Yes | Company identifier (validated against user permissions) |
| 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 |
| Header | Value | Required | Description |
|---|---|---|---|
| Authorization | Bearer {jwt_token} |
Yes | JWT authentication token |
| Content-Type | application/json |
Yes | Request content type |
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"{
"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"
}
]
}{
"detail": "Authentication credentials were not provided.",
"code": "authentication_required",
"timestamp": "2025-07-13T15:30:00Z"
}{
"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"
}{
"detail": "Company not found or you don't have access to it.",
"code": "company_not_found",
"timestamp": "2025-07-13T15:30:00Z"
}{
"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"
}{
"detail": "Rate limit exceeded. Try again in 60 seconds.",
"code": "rate_limit_exceeded",
"retry_after": 60,
"timestamp": "2025-07-13T15:30:00Z"
}// 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;
}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]);
},
});
};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>
);
};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>
);
};- 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
- 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-pghistoryfor compliance tracking
- Pagination: Maximum 100 items per page to prevent large payloads
- Indexing: Database indexes on
company_id,last_active, andstatusfields - Query Optimization: Avoids N+1 queries through proper eager loading
- 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
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);
});
});
});# 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 localGET /api/v2/companies/{company_id}/user-metrics/summary/- Aggregated metricsGET /api/v2/companies/{company_id}/user-metrics/export/- CSV exportPOST /api/v2/companies/{company_id}/user-metrics/refresh/- Trigger metrics refresh
- 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_inactiveparameter (usestatusfilter instead) - Response format includes additional fields:
sessions_this_month,avg_session_duration
- Added indexes on frequently queried fields
- Implemented soft deletes for audit compliance
- Added
company_idforeign key for proper isolation
- Response time percentiles (p50, p95, p99)
- Error rate by status code
- Rate limit hit frequency
- Cache hit/miss ratio
- Error rate > 5% for 5 minutes
- p95 response time > 2 seconds
- Rate limit hits > 50/hour per company
- Cache miss rate > 30%