App Usage Report — Alignment Review
The App Usage Report proposal defines two new endpoints for the App Usage dashboard (App Metrics and Top App Rankings widgets). The proposal was written without knowledge of the v2 API design and needs changes to match the standards in the Design Guidelines and to reuse existing DTOs.
This document catalogues every deviation from the v2 conventions, explains why each matters, and provides the corrected pattern. Three v2-wide convention decisions were also made during this review and are reflected both here and in the Design Guidelines.
1. URL Path: Wrong namespace
Proposal:
GET /internal/v1/clients/:client_id/app-usage-report/rankings
GET /internal/v1/clients/:client_id/app-usage-report/metrics
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}
Corrected:
GET /clients/{client_id}/analytics/apps/utilization
GET /clients/{client_id}/analytics/apps/utilization/summary
GET /clients/{client_id}/analytics/apps/utilization/rankings
The two proposed endpoints become three, following the same summary + list separation established by the AI Apps domain (see issue 3). The list endpoint is /utilization itself — not a nested /overview sub-resource — keeping the paths flat and siblings.
2. Collection array: items must be data
Proposal: The metrics response DTO uses 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"
}
items must be renamed to data.
3. Summary metrics + paginated list in the same response
Proposal: The single metrics endpoint returns summary card counts (apps_used, apps_discovered, apps_dropped_off) alongside a paginated app list (items, total_count, next_cursor) in one response.
Problem: v2 separates these concerns:
- Summary endpoints return aggregate metrics with no pagination: User Summary, Device Summary, Catalog Apps Summary, AI App Users Summary
- Collection endpoints return
{ data, total_count, next_cursor }with just items: App Utilization, AI App Users
No existing v2 endpoint combines both in a single response.
Corrected approach — split into two endpoints:
| Summary endpoint | Collection endpoint |
|---|---|
GET /analytics/apps/utilization/summary | GET /analytics/apps/utilization |
The summary endpoint returns the card metrics. The collection endpoint returns the paginated table with the standard { period, data, total_count, next_cursor } wrapper. Both paths are flat siblings under /utilization — there is no /overview sub-resource.
4. Resource IDs: Must use id, not app_id
Proposal: AppSummaryDto has app_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
{ "app_id": "a1d97031-..." } // incorrect
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 and all AI Apps endpoints.
{
"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 existing field |
Corrected: Delete AppSummaryDto. Use AppRef from the Apps domain. The existing getSummary() repository query needs to expand its join through apps and catalog_applications to populate type, is_favorite, and is_approved.
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 rankings or overview 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: Include both granularity and explicit date range
Proposal: Uses date + granularity (a single reference date) where the server derives the period bounds.
Problem: v2 analytics endpoints use explicit date ranges. The App Utilization endpoint (the closest precedent) takes start_date / end_date. A single date with server-side derivation is opaque — callers cannot control the exact period boundary or use custom ranges.
However, granularity itself is valid and necessary. It determines how metrics are aggregated within the period (e.g. average per day vs. total for the period), which affects how rankings and per-day time values are calculated.
Corrected: Use explicit dates plus granularity:
| Parameter | Type | Required | Description |
|---|---|---|---|
granularity | string | Yes | daily, weekly, monthly, quarterly |
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. |
This is a net-new v2 convention. See the updated Design Guidelines.
8. ReportPeriodDto — Needed, but wrong field names
Proposal: Every response embeds a ReportPeriodDto with period_start, period_end, comparison_period_start, comparison_period_end.
Problem: Two issues:
-
Wrong field names: The field names must mirror the query parameters exactly —
start_date,end_date,compare_start_date,compare_end_date— not a different set of names. Inconsistency between request params and response fields adds unnecessary cognitive load. -
Not yet used in v2: No existing v2 aggregate endpoint echoes the period back. However, for endpoints that accept
granularitythe resolved period is genuinely non-obvious to the caller (especially when validation rounds or adjusts dates), so echoing it back is justified as a new convention for these endpoints.
Corrected: Include a period object using query-parameter-matching field names:
{
"period": {
"start_date": "2026-02-01",
"end_date": "2026-02-28",
"compare_start_date": "2026-01-01",
"compare_end_date": "2026-01-31"
}
}
When no comparison dates were requested, compare_start_date and compare_end_date are null. This is adopted as an API-wide convention for all aggregate analytics endpoints. See Design Guidelines.
9. MetricWithDeltaDto — Shape adopted as API-wide pattern
Proposal: Summary metrics use a nested wrapper object:
{
"apps_used": {
"value": 125,
"delta": 4,
"delta_pct": 50.0
}
}
Original concern: v2 was using inline suffixed fields (apps_used_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:
{
"apps_used": { "value": 125, "change_pct": 50.0 },
"apps_discovered": { "value": 12, "change_pct": 33.3 },
"apps_dropped_off": { "value": 4, "change_pct": null }
}
When no comparison period is provided, all change_pct fields are null.
10. Change percentage field name: delta_pct must be change_pct
Proposal: RankedAppDto uses delta_pct for the per-item change percentage.
Problem: delta_pct is non-standard. With the adoption of MetricWithDeltaDto (see item 9 above), the change percentage is always change_pct inside the wrapper object.
Corrected: RankedApp wraps the metric value and its change percentage together in a MetricWithDeltaDto named metric:
{
"metric": { "value": 210, "change_pct": 12.5 }
}
11. Filter parameters: Singular IDs must become plural arrays (API-wide)
Proposal: Uses category_ids and department_ids (plural arrays). The existing App Utilization and List Apps endpoints use singular category_id, department_id, vendor_id.
Decision: The plural form is correct and the v2 API is being updated to adopt this. All existing singular filter parameters are renamed to their plural form and accept comma-separated UUID lists. Existing singular params are supported as deprecated aliases during the transition.
| Old (singular) | New (plural) |
|---|---|
category_id | category_ids |
department_id | department_ids |
vendor_id | vendor_ids |
The new App Usage endpoints use category_ids and department_ids from the start.
12. Time units: avg_time_seconds_per_day must use milliseconds
Proposal: AppMetricsItemDto uses avg_time_seconds_per_day.
Problem: The DB column is total_active_ms (milliseconds) and the existing App Utilization endpoint exposes active_time_ms. Introducing a seconds-denominated field alongside millisecond fields in the same domain creates unit inconsistency. Presentation formatting (converting ms to "2h 30m" etc.) is the client's responsibility.
Corrected: avg_time_ms_per_day — consistent with active_time_ms on App Utilization.
13. shared/report.dto.ts — Partially adopted
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 | Inline period object, no separate class needed |
MetricWithDeltaDto | Adopted as a shared class — see NestJS Class Validators |
All reusable DTOs (AppRef, DepartmentRef, MetricWithDeltaDto) exist in the v2 DTO set documented in NestJS Class Validators.
Corrected example responses
App Usage Rankings
GET /clients/{client_id}/analytics/apps/utilization/rankings?granularity=monthly&start_date=2026-02-01&end_date=2026-02-28&compare_start_date=2026-01-01&compare_end_date=2026-01-31
{
"period": {
"start_date": "2026-02-01",
"end_date": "2026-02-28",
"compare_start_date": "2026-01-01",
"compare_end_date": "2026-01-31"
},
"by_interactions": {
"leader": {
"app": {
"id": "f1a2b3c4-d5e6-7890-abcd-ef1234567890",
"canonical_name": "MS Teams",
"logo": "https://cdn.example.com/logos/msteams.png",
"type": "desktop",
"is_favorite": true,
"is_approved": true
},
"metric": { "value": 210, "change_pct": 12.5 }
},
"biggest_increase": {
"app": {
"id": "a2b3c4d5-e6f7-8901-bcde-f12345678901",
"canonical_name": "Kaseya",
"logo": "https://cdn.example.com/logos/kaseya.png",
"type": "browser",
"is_favorite": false,
"is_approved": true
},
"metric": { "value": 103, "change_pct": 45.1 }
},
"biggest_decrease": {
"app": {
"id": "b3c4d5e6-f7a8-9012-cdef-123456789012",
"canonical_name": "Strategy Overview",
"logo": "https://cdn.example.com/logos/strategy.png",
"type": "browser",
"is_favorite": false,
"is_approved": false
},
"metric": { "value": 7, "change_pct": -34.2 }
}
},
"by_time": {
"leader": {
"app": {
"id": "c4d5e6f7-a8b9-0123-defa-234567890123",
"canonical_name": "Datto",
"logo": "https://cdn.example.com/logos/datto.png",
"type": "desktop",
"is_favorite": false,
"is_approved": true
},
"metric": { "value": 8460000, "change_pct": 5.3 }
},
"biggest_increase": null,
"biggest_decrease": null
},
"by_active_users": {
"leader": {
"app": {
"id": "f1a2b3c4-d5e6-7890-abcd-ef1234567890",
"canonical_name": "MS Teams",
"logo": "https://cdn.example.com/logos/msteams.png",
"type": "desktop",
"is_favorite": true,
"is_approved": true
},
"metric": { "value": 25, "change_pct": 8.0 }
},
"biggest_increase": null,
"biggest_decrease": null
}
}
App Utilization Summary
GET /clients/{client_id}/analytics/apps/utilization/summary?granularity=monthly&start_date=2026-02-01&end_date=2026-02-28&compare_start_date=2026-01-01&compare_end_date=2026-01-31
{
"period": {
"start_date": "2026-02-01",
"end_date": "2026-02-28",
"compare_start_date": "2026-01-01",
"compare_end_date": "2026-01-31"
},
"apps_used": { "value": 125, "change_pct": null },
"apps_discovered": { "value": 12, "change_pct": 50.0 },
"apps_dropped_off": { "value": 4, "change_pct": 33.3 }
}
App Utilization List
GET /clients/{client_id}/analytics/apps/utilization?granularity=monthly&start_date=2026-02-01&end_date=2026-02-28&compare_start_date=2026-01-01&compare_end_date=2026-01-31
{
"period": {
"start_date": "2026-02-01",
"end_date": "2026-02-28",
"compare_start_date": "2026-01-01",
"compare_end_date": "2026-01-31"
},
"data": [
{
"app": {
"id": "f1a2b3c4-d5e6-7890-abcd-ef1234567890",
"canonical_name": "Salesforce",
"logo": "https://cdn.example.com/logos/salesforce.png",
"type": "browser",
"is_favorite": true,
"is_approved": true
},
"unique_users": { "value": 5, "change_pct": 25.0 },
"active_time_ms": { "value": 64800000, "change_pct": -3.1 },
"avg_time_ms_per_day": { "value": 2160000, "change_pct": -3.1 },
"session_count": { "value": 42, "change_pct": 10.5 }
},
{
"app": {
"id": "g2b3c4d5-e6f7-8901-bcde-f12345678901",
"canonical_name": "Asana",
"logo": "https://cdn.example.com/logos/asana.png",
"type": "browser",
"is_favorite": false,
"is_approved": true
},
"unique_users": { "value": 4, "change_pct": null },
"active_time_ms": { "value": 21600000, "change_pct": null },
"avg_time_ms_per_day": { "value": 720000, "change_pct": null },
"session_count": { "value": 18, "change_pct": null }
}
],
"total_count": 125,
"next_cursor": "eyJsYXN0X2lkIjoiZzJiM2M0ZDUifQ"
}
Quick-reference checklist
| # | Issue | Action |
|---|---|---|
| 1 | URL path .../app-usage-report/... | Move to .../analytics/apps/utilization/ |
| 2 | items array | Rename to data |
| 3 | Summary + list in one response | Split into /utilization/summary + /utilization list endpoints |
| 4 | app_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 only, no granularity | Use granularity + start_date/end_date + optional comparison dates |
| 8 | ReportPeriodDto with wrong field names | Include period object with query-parameter-matching names |
| 9 | MetricWithDeltaDto nested object | Adopted — use { value, change_pct } wrapper; drop absolute delta field |
| 10 | delta_pct on ranked items | Use metric.change_pct inside MetricWithDeltaDto named metric |
| 11 | Singular filter IDs (category_id) | Use plural (category_ids) accepting comma-separated UUIDs |
| 12 | avg_time_seconds_per_day | Use avg_time_ms_per_day — consistent with active_time_ms |
| 13 | shared/report.dto.ts | MetricWithDeltaDto is now a shared class; AppSummaryDto deleted, ReportPeriodDto not needed |