Skip to main content

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:

No existing v2 endpoint combines both in a single response.

Corrected approach — split into two endpoints:

Summary endpointCollection endpoint
GET /analytics/apps/utilization/summaryGET /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 id in DTOs (not user_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_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 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:

ParameterTypeRequiredDescription
granularitystringYesdaily, weekly, monthly, quarterly
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.

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:

  1. 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.

  2. Not yet used in v2: No existing v2 aggregate endpoint echoes the period back. However, for endpoints that accept granularity the 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_idcategory_ids
department_iddepartment_ids
vendor_idvendor_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 DTOStatus
AppSummaryDtoDeleted — reuse AppRef
ReportPeriodDtoInline period object, no separate class needed
MetricWithDeltaDtoAdopted 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

#IssueAction
1URL path .../app-usage-report/...Move to .../analytics/apps/utilization/
2items arrayRename to data
3Summary + list in one responseSplit into /utilization/summary + /utilization list endpoints
4app_idUse id per naming conventions
5AppSummaryDtoDelete, reuse existing AppRef
6category: stringRemove — not on AppRef, not a flat string in v2
7date only, no granularityUse granularity + start_date/end_date + optional comparison dates
8ReportPeriodDto with wrong field namesInclude period object with query-parameter-matching names
9MetricWithDeltaDto nested objectAdopted — use { value, change_pct } wrapper; drop absolute delta field
10delta_pct on ranked itemsUse metric.change_pct inside MetricWithDeltaDto named metric
11Singular filter IDs (category_id)Use plural (category_ids) accepting comma-separated UUIDs
12avg_time_seconds_per_dayUse avg_time_ms_per_day — consistent with active_time_ms
13shared/report.dto.tsMetricWithDeltaDto is now a shared class; AppSummaryDto deleted, ReportPeriodDto not needed