Skip to main content

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:

No existing v2 endpoint combines both in a single response.

Corrected approach — split into two endpoints per report type:

Summary endpointCollection endpoint
GET /analytics/apps/ai/users/summaryGET /analytics/apps/ai/users
GET /analytics/apps/ai/recent/summaryGET /analytics/apps/ai/recent
GET /analytics/apps/ai/departments/summaryGET /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 id in DTOs (not user_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_ididViolates naming convention
app_namecanonical_nameWrong field name — catalog apps use canonical_name
logologoMatches
category: string | null(not on AppRef)See issue 6
(missing)typeMissing existing field
(missing)is_favoriteMissing existing field
(missing)is_approvedMissing — 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:

ParameterTypeRequiredDescription
start_datestringYesStart of period (YYYY-MM-DD)
end_datestringYesEnd of period, inclusive (YYYY-MM-DD)
compare_start_datestringNoComparison period start. Must be provided with compare_end_date.
compare_end_datestringNoComparison 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 period
  • end_date (required) — end of the reporting period, inclusive
  • compare_start_date (optional) — comparison period start
  • compare_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 DTOStatus
AppSummaryDtoDeleted — reuse AppRef
ReportPeriodDtoRemoved — explicit date params make it redundant
MetricWithDeltaDtoAdopted 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

#IssueAction
1URL path .../ai-app-report/...Move to .../analytics/apps/ai/
2items arrayRename to data
3Summary + list in one responseSplit into /summary + list endpoints
4app_id, department_idUse id per naming conventions
5AppSummaryDtoDelete, reuse existing AppRef
6category: stringRemove — not on AppRef, not a flat string in v2
7date + granularity paramsUse start_date/end_date + optional comparison dates
8ReportPeriodDtoRemove — caller already knows requested dates
9MetricWithDeltaDto nested objectAdopted — use { value, change_pct } wrapper; drop absolute delta field
10AiDepartmentItemDto inline fieldsEmbed DepartmentRef for identity; user_count uses MetricWithDeltaDto
11usage_level server enumTeam discussion — consider exposing raw score
12shared/report.dto.tsMetricWithDeltaDto adopted as shared class; AppSummaryDto and ReportPeriodDto not needed