AI App Report — Alignment Review
The AI App Report proposal defines three new endpoints for the AI App Usage dashboard. The proposal was designed without knowledge of the API docs and needs changes to match the standards set in the Design Guidelines and to reuse existing DTOs.
This document catalogues every deviation from the API conventions, explains why each matters, and provides the corrected pattern.
1. URL Path: Wrong namespace
Proposal:
GET /internal/v1/clients/:client_id/ai-app-report/users
GET /internal/v1/clients/:client_id/ai-app-report/recent
GET /internal/v1/clients/:client_id/ai-app-report/departments
Problem: Read-only aggregate metric endpoints belong in the /analytics namespace, not a standalone module. Path parameters use {param} (OpenAPI style), not :param (Express style).
All analytics endpoints follow the URL convention from the Analytics Overview:
/clients/{client_id}/analytics/{resource}/{analytics-kind}
Collection-level analytics endpoints (e.g. /analytics/apps/utilization) omit the {id} segment and return metrics across all resources in a paginated list.
Corrected:
GET /clients/{client_id}/analytics/apps/ai/users
GET /clients/{client_id}/analytics/apps/ai/recent
GET /clients/{client_id}/analytics/apps/ai/departments
2. Collection array: items must be data
Proposal: All three response DTOs use items for the paginated array.
Problem: v2 collection responses always use data. See Response Shape Rules:
{
"data": [ /* Resource objects */ ],
"total_count": 24,
"next_cursor": "WyJ0aWNrZXQiXQ"
}
Every reference to items across all three response DTOs must be renamed to data.
3. Summary metrics + paginated list in the same response
Proposal: Each response mixes summary card metrics (total_users, users_gained, users_lost, etc.) with a paginated collection (items, total_count, next_cursor) in a single object.
Problem: v2 separates these concerns:
- Summary endpoints return aggregate metrics with no pagination: User Summary, Device Summary, Catalog Apps Summary
- Collection endpoints return
{ data, total_count, next_cursor }with just items: App Utilization, List Apps
No existing v2 endpoint combines both in a single response.
Corrected approach — split into two endpoints per report type:
| Summary endpoint | Collection endpoint |
|---|---|
GET /analytics/apps/ai/users/summary | GET /analytics/apps/ai/users |
GET /analytics/apps/ai/recent/summary | GET /analytics/apps/ai/recent |
GET /analytics/apps/ai/departments/summary | GET /analytics/apps/ai/departments |
The summary endpoint returns the card metrics (total users, approved/unapproved counts, users gained/lost). The collection endpoint returns the paginated table with the standard { data, total_count, next_cursor } wrapper.
If the team decides to keep them combined for fewer round-trips, this should be documented as a deliberate deviation from the established pattern and the collection array must still be named data.
4. Resource IDs: Must use id, not app_id / department_id
Proposal: AppSummaryDto has app_id, AiDepartmentItemDto has department_id.
Problem: The Naming Conventions are explicit:
All resource IDs are exposed as
idin DTOs (notuser_id,department_id, etc.). The resource type is clear from context.
{ "id": "a1d97031-..." } // correct
{ "user_id": "a1d97031-..." } // incorrect
All app_id and department_id fields in DTOs must become id.
5. AppSummaryDto must not exist — reuse existing AppRef
Proposal: Defines a new AppSummaryDto with app_id, app_name, logo, category.
Problem: v2 already defines AppRef as the lightweight app reference for embedding inside analytics and cross-domain responses. It is already used by App Utilization.
{
"id": "f1a2b3c4-d5e6-7890-abcd-ef1234567890",
"canonical_name": "Slack",
"logo": "https://cdn.example.com/logos/slack.png",
"type": "desktop",
"is_favorite": true,
"is_approved": true
}
Field-by-field comparison:
Proposal (AppSummaryDto) | Existing (AppRef) | Issue |
|---|---|---|
app_id | id | Violates naming convention |
app_name | canonical_name | Wrong field name — catalog apps use canonical_name |
logo | logo | Matches |
category: string | null | (not on AppRef) | See issue 6 |
| (missing) | type | Missing existing field |
| (missing) | is_favorite | Missing existing field |
| (missing) | is_approved | Missing — then duplicated on the item DTO |
Corrected: Delete AppSummaryDto. Use AppRef from the Apps domain. Since is_approved already lives on AppRef, remove the duplicate is_approved from AiAppUsageItemDto.
The item shape becomes:
{
"app": {
"id": "f1a2b3c4-d5e6-7890-abcd-ef1234567890",
"canonical_name": "ChatGPT",
"logo": "https://cdn.example.com/logos/chatgpt.png",
"type": "browser",
"is_favorite": false,
"is_approved": true
},
"user_count": 52,
"usage_level": "heavy"
}
6. category as a flat string
Proposal: AppSummaryDto has category: string | null — a single category name.
Problem: v2 represents categories as CatalogCategory[] (array of { id, name } objects) on the full CatalogApp DTO. AppRef deliberately excludes categories to stay lightweight. No v2 DTO flattens categories to a single string.
An app can belong to multiple categories. Collapsing to a single string loses data and is inconsistent with every other place categories appear.
Corrected: Drop the category field entirely. AppRef does not include categories by design — it contains only the fields needed to render a meaningful table row. Consumers who need category details can call Get App or Get Catalog App.
If the team determines that a category indicator is needed for every row in the AI report table, this should be raised as a proposal to extend AppRef across all analytics endpoints, not added as a one-off field on a custom DTO.
7. Date parameters: start_date/end_date, not date/granularity
Proposal: Uses date + granularity (daily/weekly/monthly/quarterly/yearly) to derive the reporting period. The server computes the start/end dates.
Problem: v2 analytics endpoints use explicit date ranges. The App Utilization endpoint (the closest precedent) takes:
| Parameter | Type | Required | Description |
|---|---|---|---|
start_date | string | Yes | Start of period (YYYY-MM-DD) |
end_date | string | Yes | End of period, inclusive (YYYY-MM-DD) |
compare_start_date | string | No | Comparison period start. Must be provided with compare_end_date. |
compare_end_date | string | No | Comparison period end. Must be provided with compare_start_date. |
Explicit dates are more flexible (custom ranges, fiscal quarters, etc.) and consistent with the rest of v2. The granularity enum is a v1 pattern from the saas-utilization controller.
Corrected: Replace date + granularity with:
start_date(required) — start of the reporting periodend_date(required) — end of the reporting period, inclusivecompare_start_date(optional) — comparison period startcompare_end_date(optional) — comparison period end
When comparison dates are omitted, all change/delta fields are null.
8. ReportPeriodDto — Likely unnecessary, and wrong field names
Proposal: Every response embeds a ReportPeriodDto with period_start, period_end, comparison_period_start, comparison_period_end.
Problem: With explicit start_date/end_date query parameters, the caller already knows what period they requested — echoing it back is redundant. The existing v2 App Utilization endpoint does not echo back a period object.
If echoing dates is truly needed (e.g. for debugging or client convenience), the existing analytics pattern uses nested object style, not flat fields:
{
"period": {
"start": "2024-01-01",
"end": "2024-01-15"
}
}
Not:
{
"period_start": "2024-01-01",
"period_end": "2024-01-15"
}
Corrected: Remove ReportPeriodDto. If the team decides to echo dates, follow the existing period.start / period.end naming with an optional comparison_period object that is null when no comparison was requested.
9. MetricWithDeltaDto — Shape adopted as API-wide pattern
Proposal: Summary metrics use a nested object:
{
"total_users": {
"value": 156,
"delta": 23,
"delta_pct": 17.3
}
}
Original concern: v2 was using inline suffixed fields (_change_pct), so the nested shape was a mismatch. The absolute delta field was also flagged as non-standard.
Updated decision: The nested MetricWithDeltaDto shape has been adopted as the API-wide pattern for all metric + comparison-period pairs. The delta (absolute change) field is dropped — only change_pct is exposed. See MetricWithDeltaDto.
Adopted shape for collection items:
{
"user_count": { "value": 52, "change_pct": 32.0 }
}
Adopted shape for summary endpoints:
{
"total_users": { "value": 156, "change_pct": 17.3 },
"approved_app_count": 3,
"unapproved_app_count": 0,
"users_gained": { "value": 40, "change_pct": 33.3 },
"users_lost": { "value": 67, "change_pct": -4.3 }
}
When no comparison period is provided, all change_pct fields are null.
10. Department items — Should embed DepartmentRef
Proposal: AiDepartmentItemDto has inline department_id and department_name.
Problem: v2 already defines DepartmentRef (id, name, description) and uses the pattern of embedding a Ref DTO for identity fields. App Utilization embeds AppRef; department items should embed DepartmentRef.
Corrected:
{
"department": {
"id": "dept-0001-...",
"name": "Sales",
"description": null
},
"user_count": { "value": 52, "change_pct": 32.0 },
"approved_user_count": 31,
"unapproved_user_count": 1
}
Note that delta_pct on the item uses the MetricWithDeltaDto wrapper pattern with change_pct inside it.
11. usage_level — Computed server-side enum
Proposal: The usage_level field (heavy/medium/light) is derived from a Usage Score formula with fixed thresholds.
This is not a v2 convention violation but is a design concern worth discussing:
- Changing thresholds requires a backend deploy, not a config change
- Clients cannot customize buckets for their own UI
- No other v2 endpoint returns server-computed classification enums like this
Options:
- Keep it if the classification is a stable business rule that all consumers should apply consistently
- Return the raw score alongside or instead of the enum, so consumers can classify on their own
- Return the underlying metrics (user adoption rate, activity rate) and let the frontend compute
This should be a team discussion item. If kept, the Usage Score formula and thresholds must be documented in the endpoint spec.
12. shared/report.dto.ts — Likely unnecessary
Proposal: Creates src/shared/report.dto.ts for ReportPeriodDto, MetricWithDeltaDto, and AppSummaryDto.
After applying the corrections above:
| Proposed shared DTO | Status |
|---|---|
AppSummaryDto | Deleted — reuse AppRef |
ReportPeriodDto | Removed — explicit date params make it redundant |
MetricWithDeltaDto | Adopted as a shared class — see NestJS Class Validators |
AppSummaryDto and ReportPeriodDto are no longer needed. All reusable DTOs (AppRef, DepartmentRef, MetricWithDeltaDto) exist in the v2 DTO set documented in NestJS Class Validators.
Corrected example responses
AI App Users — Summary
GET /clients/{client_id}/analytics/apps/ai/users/summary?start_date=2026-02-01&end_date=2026-02-28&compare_start_date=2026-01-01&compare_end_date=2026-01-31
{
"total_users": { "value": 156, "change_pct": 17.3 },
"approved_app_count": 3,
"unapproved_app_count": 0,
"users_gained": { "value": 40, "change_pct": 33.3 },
"users_lost": { "value": 67, "change_pct": -4.3 }
}
AI App Users — List
GET /clients/{client_id}/analytics/apps/ai/users?start_date=2026-02-01&end_date=2026-02-28&compare_start_date=2026-01-01&compare_end_date=2026-01-31&sort_by=user_count&sort_order=desc
{
"data": [
{
"app": {
"id": "a1b2c3d4-0001-4000-8000-000000000001",
"canonical_name": "ChatGPT",
"logo": "https://cdn.example.com/logos/chatgpt.png",
"type": "browser",
"is_favorite": false,
"is_approved": true
},
"user_count": { "value": 52, "change_pct": 12.5 },
"usage_level": "heavy"
},
{
"app": {
"id": "a1b2c3d4-0002-4000-8000-000000000002",
"canonical_name": "Jasper",
"logo": "https://cdn.example.com/logos/jasper.png",
"type": "browser",
"is_favorite": false,
"is_approved": false
},
"user_count": { "value": 40, "change_pct": 8.1 },
"usage_level": "heavy"
}
],
"total_count": 4,
"next_cursor": null
}
New AI Tools — List
GET /clients/{client_id}/analytics/apps/ai/recent?start_date=2026-02-01&end_date=2026-02-28
{
"data": [
{
"app": {
"id": "a1b2c3d4-0001-4000-8000-000000000001",
"canonical_name": "ChatGPT",
"logo": "https://cdn.example.com/logos/chatgpt.png",
"type": "browser",
"is_favorite": false,
"is_approved": false
},
"user_count": { "value": 2, "change_pct": null },
"usage_level": "heavy"
}
],
"total_count": 4,
"next_cursor": null
}
AI By Department — List
GET /clients/{client_id}/analytics/apps/ai/departments?start_date=2026-02-01&end_date=2026-02-28&compare_start_date=2026-01-01&compare_end_date=2026-01-31
{
"data": [
{
"department": {
"id": "dept-0001-4000-8000-000000000001",
"name": "Sales",
"description": null
},
"user_count": { "value": 52, "change_pct": 32.0 },
"approved_user_count": 31,
"unapproved_user_count": 1
},
{
"department": {
"id": "dept-0002-4000-8000-000000000002",
"name": "Engineering",
"description": "Software development and infrastructure teams"
},
"user_count": { "value": 40, "change_pct": 11.0 },
"approved_user_count": 31,
"unapproved_user_count": 1
}
],
"total_count": 4,
"next_cursor": null
}
Quick-reference checklist
| # | Issue | Action |
|---|---|---|
| 1 | URL path .../ai-app-report/... | Move to .../analytics/apps/ai/ |
| 2 | items array | Rename to data |
| 3 | Summary + list in one response | Split into /summary + list endpoints |
| 4 | app_id, department_id | Use id per naming conventions |
| 5 | AppSummaryDto | Delete, reuse existing AppRef |
| 6 | category: string | Remove — not on AppRef, not a flat string in v2 |
| 7 | date + granularity params | Use start_date/end_date + optional comparison dates |
| 8 | ReportPeriodDto | Remove — caller already knows requested dates |
| 9 | MetricWithDeltaDto nested object | Adopted — use { value, change_pct } wrapper; drop absolute delta field |
| 10 | AiDepartmentItemDto inline fields | Embed DepartmentRef for identity; user_count uses MetricWithDeltaDto |
| 11 | usage_level server enum | Team discussion — consider exposing raw score |
| 12 | shared/report.dto.ts | MetricWithDeltaDto adopted as shared class; AppSummaryDto and ReportPeriodDto not needed |