diff --git a/.changeset/runtime-customer-apps-consumption.md b/.changeset/runtime-customer-apps-consumption.md new file mode 100644 index 0000000..3bc5fc5 --- /dev/null +++ b/.changeset/runtime-customer-apps-consumption.md @@ -0,0 +1,23 @@ +--- +'@cdot65/prisma-airs-cli': minor +--- + +Add `airs runtime customer-apps consumption [appName]` for per-app token consumption + violation breakdown, sourced from the SCM AI Security > Runtime > API Applications dashboard endpoints (via the new `mgmt.dashboard` SDK namespace). + +``` +# pretty (default): per-app sections with tokens, sessions, firing detectors +airs runtime customer-apps consumption chatbot + +# all apps in tenant (omit appName) +airs runtime customer-apps consumption + +# 60-day window instead of default 30 +airs runtime customer-apps consumption chatbot --time-interval 60 + +# structured outputs (table / csv / json / yaml) — one row per detector per app +airs runtime customer-apps consumption --output csv > consumption.csv +``` + +The API enforces an enum for `--time-interval`: only `7`, `30`, and `60` are accepted (verified live 2026-05-28; the CLI validates client-side before calling). The dashboard endpoints require both `appId` and `appName`, so the CLI resolves the UUID from the `customer-apps list` endpoint internally - users only supply the human-readable app name. + +Closes #222. diff --git a/docs/cli/examples/runtime.yaml b/docs/cli/examples/runtime.yaml index b6ec682..7a68125 100644 --- a/docs/cli/examples/runtime.yaml +++ b/docs/cli/examples/runtime.yaml @@ -1510,6 +1510,109 @@ name: example-other-app description: +"runtime customer-apps consumption": + examples: + - note: Pretty output (default) — single app, default 30-day window. Only firing detectors are shown. + input: airs runtime customer-apps consumption example-app + output: | + Prisma AIRS — Runtime Configuration + Security profile and topic management + + + example-app (00000000-0000-0000-0000-000000000001) + Monitoring since: 2026-05-25T16:42:52Z + Source: api + Cloud: other + Profiles: AI Gateway - Strict + + Token consumption: + Daily avg: 37 + Monthly total: 37 + + Sessions: + Total: 2 + Violating: 1 + + Detectors (1 violating, 1/8 firing): + tc 1 c=0 h=0 m=1 l=0 + - note: Table output — one row per detector, app-level context repeated on every row. + input: airs runtime customer-apps consumption example-app --output table + output: | + App │ AppId │ MonitoringSince │ DailyAvg │ MonthlyTotal │ Sessions │ Violating │ Detector │ C │ H │ M │ L │ Total + ────────────┼──────────────────────────────────────┼──────────────────────┼──────────┼──────────────┼──────────┼───────────┼────────────────┼───┼───┼───┼───┼─────── + example-app │ 00000000-0000-0000-0000-000000000001 │ 2026-05-25T16:42:52Z │ 37 │ 37 │ 2 │ 1 │ agent_security │ 0 │ 0 │ 0 │ 0 │ 0 + example-app │ 00000000-0000-0000-0000-000000000001 │ 2026-05-25T16:42:52Z │ 37 │ 37 │ 2 │ 1 │ dbs │ 0 │ 0 │ 0 │ 0 │ 0 + example-app │ 00000000-0000-0000-0000-000000000001 │ 2026-05-25T16:42:52Z │ 37 │ 37 │ 2 │ 1 │ dlp │ 0 │ 0 │ 0 │ 0 │ 0 + example-app │ 00000000-0000-0000-0000-000000000001 │ 2026-05-25T16:42:52Z │ 37 │ 37 │ 2 │ 1 │ malicious_code │ 0 │ 0 │ 0 │ 0 │ 0 + example-app │ 00000000-0000-0000-0000-000000000001 │ 2026-05-25T16:42:52Z │ 37 │ 37 │ 2 │ 1 │ pi │ 0 │ 0 │ 0 │ 0 │ 0 + example-app │ 00000000-0000-0000-0000-000000000001 │ 2026-05-25T16:42:52Z │ 37 │ 37 │ 2 │ 1 │ source_code │ 0 │ 0 │ 0 │ 0 │ 0 + example-app │ 00000000-0000-0000-0000-000000000001 │ 2026-05-25T16:42:52Z │ 37 │ 37 │ 2 │ 1 │ tc │ 0 │ 0 │ 1 │ 0 │ 1 + example-app │ 00000000-0000-0000-0000-000000000001 │ 2026-05-25T16:42:52Z │ 37 │ 37 │ 2 │ 1 │ uf │ 0 │ 0 │ 0 │ 0 │ 0 + - note: CSV output — pipe straight into spreadsheets or BigQuery. Self-contained rows; no join needed. + input: airs runtime customer-apps consumption example-app --output csv + output: | + App,AppId,MonitoringSince,DailyAvg,MonthlyTotal,Sessions,Violating,Detector,C,H,M,L,Total + example-app,00000000-0000-0000-0000-000000000001,2026-05-25T16:42:52Z,37,37,2,1,agent_security,0,0,0,0,0 + example-app,00000000-0000-0000-0000-000000000001,2026-05-25T16:42:52Z,37,37,2,1,dbs,0,0,0,0,0 + example-app,00000000-0000-0000-0000-000000000001,2026-05-25T16:42:52Z,37,37,2,1,dlp,0,0,0,0,0 + example-app,00000000-0000-0000-0000-000000000001,2026-05-25T16:42:52Z,37,37,2,1,malicious_code,0,0,0,0,0 + example-app,00000000-0000-0000-0000-000000000001,2026-05-25T16:42:52Z,37,37,2,1,pi,0,0,0,0,0 + example-app,00000000-0000-0000-0000-000000000001,2026-05-25T16:42:52Z,37,37,2,1,source_code,0,0,0,0,0 + example-app,00000000-0000-0000-0000-000000000001,2026-05-25T16:42:52Z,37,37,2,1,tc,0,0,1,0,1 + example-app,00000000-0000-0000-0000-000000000001,2026-05-25T16:42:52Z,37,37,2,1,uf,0,0,0,0,0 + - note: JSON output — one object per detector per app; full app context repeated for self-contained records. + input: airs runtime customer-apps consumption example-app --output json + output: | + [ + { + "app_name": "example-app", + "app_id": "00000000-0000-0000-0000-000000000001", + "monitoring_since": "2026-05-25T16:42:52Z", + "daily_avg": "37", + "monthly_total": "37", + "sessions_total": 2, + "sessions_violating": 1, + "detector": "tc", + "critical": 0, + "high": 0, + "medium": 1, + "low": 0, + "total": 1 + } + ] + - note: YAML output — multi-doc stream, one document per detector per app. + input: airs runtime customer-apps consumption example-app --output yaml + output: | + app_name: example-app + app_id: 00000000-0000-0000-0000-000000000001 + monitoring_since: 2026-05-25T16:42:52Z + daily_avg: '37' + monthly_total: '37' + sessions_total: 2 + sessions_violating: 1 + detector: tc + critical: 0 + high: 0 + medium: 1 + low: 0 + total: 1 + - note: Alternate time window — `--time-interval` accepts 7, 30, or 60 days (server-enforced enum). + input: airs runtime customer-apps consumption example-app --time-interval 60 + - note: All-apps loop — omit `appName` to scan every customer app in the tenant. Errors on individual apps are reported per-app; the loop continues past failures. Zero-traffic apps render `no detector violations in window`. + input: airs runtime customer-apps consumption + - note: Invalid time-interval rejected client-side before incurring an API call. + input: airs runtime customer-apps consumption example-app --time-interval 14 + output: | + Error: --time-interval must be 7, 30, or 60 (the API rejects other values) + - note: Unknown app name returns a helpful error pointing at `customer-apps list`. + input: airs runtime customer-apps consumption no-such-app + output: | + Prisma AIRS — Runtime Configuration + Security profile and topic management + + + Error: Customer app not found: "no-such-app". Run `airs runtime customer-apps list` to see available apps. + "runtime api-keys create": examples: - note: Create a new API key from a config fixture (no JSON output flag — pretty only). The full secret is echoed exactly once; subsequent `list` only shows `last8`. diff --git a/docs/cli/runtime/customer-apps.md b/docs/cli/runtime/customer-apps.md index c2d065d..7a5175f 100644 --- a/docs/cli/runtime/customer-apps.md +++ b/docs/cli/runtime/customer-apps.md @@ -139,3 +139,176 @@ airs runtime customer-apps delete [options] !!! warning "Example needed" No curated input/output example for this command yet. + +--- + +### runtime customer-apps consumption + +Show per-app token consumption + violation breakdown (SCM dashboard). Omit appName to scan all apps. + +```text +airs runtime customer-apps consumption [options] [appName] +``` + +#### Arguments + +- `appName` (optional) — + +#### Options + +| Flag | Required | Default | Description | +|------|:--------:|---------|-------------| +| `--time-interval ` | No | `30` | Window in days: 7, 30, or 60 | +| `--output ` | No | `pretty` | Output format: pretty, table, csv, json, yaml | + +#### Examples + +*Pretty output (default) — single app, default 30-day window. Only firing detectors are shown.* + +```bash +airs runtime customer-apps consumption example-app +``` + +```text +Prisma AIRS — Runtime Configuration +Security profile and topic management + + +example-app (00000000-0000-0000-0000-000000000001) + Monitoring since: 2026-05-25T16:42:52Z + Source: api + Cloud: other + Profiles: AI Gateway - Strict + + Token consumption: + Daily avg: 37 + Monthly total: 37 + + Sessions: + Total: 2 + Violating: 1 + + Detectors (1 violating, 1/8 firing): + tc 1 c=0 h=0 m=1 l=0 +``` + +*Table output — one row per detector, app-level context repeated on every row.* + +```bash +airs runtime customer-apps consumption example-app --output table +``` + +```text +App │ AppId │ MonitoringSince │ DailyAvg │ MonthlyTotal │ Sessions │ Violating │ Detector │ C │ H │ M │ L │ Total +────────────┼──────────────────────────────────────┼──────────────────────┼──────────┼──────────────┼──────────┼───────────┼────────────────┼───┼───┼───┼───┼─────── +example-app │ 00000000-0000-0000-0000-000000000001 │ 2026-05-25T16:42:52Z │ 37 │ 37 │ 2 │ 1 │ agent_security │ 0 │ 0 │ 0 │ 0 │ 0 +example-app │ 00000000-0000-0000-0000-000000000001 │ 2026-05-25T16:42:52Z │ 37 │ 37 │ 2 │ 1 │ dbs │ 0 │ 0 │ 0 │ 0 │ 0 +example-app │ 00000000-0000-0000-0000-000000000001 │ 2026-05-25T16:42:52Z │ 37 │ 37 │ 2 │ 1 │ dlp │ 0 │ 0 │ 0 │ 0 │ 0 +example-app │ 00000000-0000-0000-0000-000000000001 │ 2026-05-25T16:42:52Z │ 37 │ 37 │ 2 │ 1 │ malicious_code │ 0 │ 0 │ 0 │ 0 │ 0 +example-app │ 00000000-0000-0000-0000-000000000001 │ 2026-05-25T16:42:52Z │ 37 │ 37 │ 2 │ 1 │ pi │ 0 │ 0 │ 0 │ 0 │ 0 +example-app │ 00000000-0000-0000-0000-000000000001 │ 2026-05-25T16:42:52Z │ 37 │ 37 │ 2 │ 1 │ source_code │ 0 │ 0 │ 0 │ 0 │ 0 +example-app │ 00000000-0000-0000-0000-000000000001 │ 2026-05-25T16:42:52Z │ 37 │ 37 │ 2 │ 1 │ tc │ 0 │ 0 │ 1 │ 0 │ 1 +example-app │ 00000000-0000-0000-0000-000000000001 │ 2026-05-25T16:42:52Z │ 37 │ 37 │ 2 │ 1 │ uf │ 0 │ 0 │ 0 │ 0 │ 0 +``` + +*CSV output — pipe straight into spreadsheets or BigQuery. Self-contained rows; no join needed.* + +```bash +airs runtime customer-apps consumption example-app --output csv +``` + +```text +App,AppId,MonitoringSince,DailyAvg,MonthlyTotal,Sessions,Violating,Detector,C,H,M,L,Total +example-app,00000000-0000-0000-0000-000000000001,2026-05-25T16:42:52Z,37,37,2,1,agent_security,0,0,0,0,0 +example-app,00000000-0000-0000-0000-000000000001,2026-05-25T16:42:52Z,37,37,2,1,dbs,0,0,0,0,0 +example-app,00000000-0000-0000-0000-000000000001,2026-05-25T16:42:52Z,37,37,2,1,dlp,0,0,0,0,0 +example-app,00000000-0000-0000-0000-000000000001,2026-05-25T16:42:52Z,37,37,2,1,malicious_code,0,0,0,0,0 +example-app,00000000-0000-0000-0000-000000000001,2026-05-25T16:42:52Z,37,37,2,1,pi,0,0,0,0,0 +example-app,00000000-0000-0000-0000-000000000001,2026-05-25T16:42:52Z,37,37,2,1,source_code,0,0,0,0,0 +example-app,00000000-0000-0000-0000-000000000001,2026-05-25T16:42:52Z,37,37,2,1,tc,0,0,1,0,1 +example-app,00000000-0000-0000-0000-000000000001,2026-05-25T16:42:52Z,37,37,2,1,uf,0,0,0,0,0 +``` + +*JSON output — one object per detector per app; full app context repeated for self-contained records.* + +```bash +airs runtime customer-apps consumption example-app --output json +``` + +```text +[ + { + "app_name": "example-app", + "app_id": "00000000-0000-0000-0000-000000000001", + "monitoring_since": "2026-05-25T16:42:52Z", + "daily_avg": "37", + "monthly_total": "37", + "sessions_total": 2, + "sessions_violating": 1, + "detector": "tc", + "critical": 0, + "high": 0, + "medium": 1, + "low": 0, + "total": 1 + } +] +``` + +*YAML output — multi-doc stream, one document per detector per app.* + +```bash +airs runtime customer-apps consumption example-app --output yaml +``` + +```text +app_name: example-app +app_id: 00000000-0000-0000-0000-000000000001 +monitoring_since: 2026-05-25T16:42:52Z +daily_avg: '37' +monthly_total: '37' +sessions_total: 2 +sessions_violating: 1 +detector: tc +critical: 0 +high: 0 +medium: 1 +low: 0 +total: 1 +``` + +*Alternate time window — `--time-interval` accepts 7, 30, or 60 days (server-enforced enum).* + +```bash +airs runtime customer-apps consumption example-app --time-interval 60 +``` + +*All-apps loop — omit `appName` to scan every customer app in the tenant. Errors on individual apps are reported per-app; the loop continues past failures. Zero-traffic apps render `no detector violations in window`.* + +```bash +airs runtime customer-apps consumption +``` + +*Invalid time-interval rejected client-side before incurring an API call.* + +```bash +airs runtime customer-apps consumption example-app --time-interval 14 +``` + +```text +Error: --time-interval must be 7, 30, or 60 (the API rejects other values) +``` + +*Unknown app name returns a helpful error pointing at `customer-apps list`.* + +```bash +airs runtime customer-apps consumption no-such-app +``` + +```text +Prisma AIRS — Runtime Configuration +Security profile and topic management + + +Error: Customer app not found: "no-such-app". Run `airs runtime customer-apps list` to see available apps. +``` diff --git a/docs/developers/api/classes/SdkManagementService.md b/docs/developers/api/classes/SdkManagementService.md index cfdb765..3340e46 100644 --- a/docs/developers/api/classes/SdkManagementService.md +++ b/docs/developers/api/classes/SdkManagementService.md @@ -1,6 +1,6 @@ # Class: SdkManagementService -Defined in: [src/airs/management.ts:28](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L28) +Defined in: [src/airs/management.ts:30](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L30) Wraps the SDK's ManagementClient to implement our ManagementService interface. OAuth2 token management, caching, and retry are handled by the SDK. @@ -15,7 +15,7 @@ OAuth2 token management, caching, and retry are handled by the SDK. > **new SdkManagementService**(`opts?`): `SdkManagementService` -Defined in: [src/airs/management.ts:31](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L31) +Defined in: [src/airs/management.ts:33](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L33) #### Parameters @@ -33,7 +33,7 @@ Defined in: [src/airs/management.ts:31](https://github.com/cdot65/prisma-airs-cl > **assignTopicsToProfile**(`profileName`, `topics`, `guardrailAction?`): `Promise`\<`void`\> -Defined in: [src/airs/management.ts:93](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L93) +Defined in: [src/airs/management.ts:95](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L95) Sets one or more custom topics on a profile's topic-guardrails config. Replaces any existing topics — previous runs' stale topics are cleared. @@ -71,7 +71,7 @@ it defaults to revision 0 (original content), not the latest. > **assignTopicToProfile**(`profileName`, `topicId`, `topicName`, `action`): `Promise`\<`void`\> -Defined in: [src/airs/management.ts:75](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L75) +Defined in: [src/airs/management.ts:77](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L77) Sets a single custom topic on a profile's topic-guardrails config. Delegates to [assignTopicsToProfile](#assigntopicstoprofile) for backward compatibility. @@ -108,7 +108,7 @@ Delegates to [assignTopicsToProfile](#assigntopicstoprofile) for backward compat > **createApiKey**(`request`): `Promise`\<`ApiKeyInfo`\> -Defined in: [src/airs/management.ts:313](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L313) +Defined in: [src/airs/management.ts:315](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L315) #### Parameters @@ -130,7 +130,7 @@ Defined in: [src/airs/management.ts:313](https://github.com/cdot65/prisma-airs-c > **createProfile**(`request`): `Promise`\<`SecurityProfileInfo`\> -Defined in: [src/airs/management.ts:265](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L265) +Defined in: [src/airs/management.ts:267](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L267) Create a security profile. @@ -154,7 +154,7 @@ Create a security profile. > **createTopic**(`request`): `Promise`\<`objectOutputType`\<\{ `active`: `ZodOptional`\<`ZodBoolean`\>; `created_by`: `ZodOptional`\<`ZodString`\>; `created_ts`: `ZodOptional`\<`ZodString`\>; `description`: `ZodString`; `examples`: `ZodArray`\<`ZodString`, `"many"`\>; `last_modified_ts`: `ZodOptional`\<`ZodString`\>; `revision`: `ZodNumber`; `topic_id`: `ZodOptional`\<`ZodString`\>; `topic_name`: `ZodString`; `updated_by`: `ZodOptional`\<`ZodString`\>; \}, `ZodTypeAny`, `"passthrough"`\>\> -Defined in: [src/airs/management.ts:35](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L35) +Defined in: [src/airs/management.ts:37](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L37) Create a new custom topic. @@ -178,7 +178,7 @@ Create a new custom topic. > **deleteApiKey**(`apiKeyName`, `updatedBy`): `Promise`\<`DeleteResponse`\> -Defined in: [src/airs/management.ts:323](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L323) +Defined in: [src/airs/management.ts:325](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L325) #### Parameters @@ -204,7 +204,7 @@ Defined in: [src/airs/management.ts:323](https://github.com/cdot65/prisma-airs-c > **deleteCustomerApp**(`appName`, `updatedBy`): `Promise`\<`CustomerAppInfo`\> -Defined in: [src/airs/management.ts:364](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L364) +Defined in: [src/airs/management.ts:366](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L366) #### Parameters @@ -230,7 +230,7 @@ Defined in: [src/airs/management.ts:364](https://github.com/cdot65/prisma-airs-c > **deleteProfile**(`profileId`): `Promise`\<`DeleteResponse`\> -Defined in: [src/airs/management.ts:278](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L278) +Defined in: [src/airs/management.ts:280](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L280) Delete a security profile. @@ -254,7 +254,7 @@ Delete a security profile. > **deleteTopic**(`topicId`): `Promise`\<`void`\> -Defined in: [src/airs/management.ts:43](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L43) +Defined in: [src/airs/management.ts:45](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L45) Delete a custom topic by ID. @@ -278,7 +278,7 @@ Delete a custom topic by ID. > **forceDeleteProfile**(`profileId`, `updatedBy`): `Promise`\<`DeleteResponse`\> -Defined in: [src/airs/management.ts:283](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L283) +Defined in: [src/airs/management.ts:285](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L285) Force-delete a security profile (removes from referencing policies). @@ -306,7 +306,7 @@ Force-delete a security profile (removes from referencing policies). > **forceDeleteTopic**(`topicId`, `updatedBy?`): `Promise`\<`DeleteResponse`\> -Defined in: [src/airs/management.ts:47](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L47) +Defined in: [src/airs/management.ts:49](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L49) Force-delete a custom topic (removes from all referencing profiles). @@ -334,7 +334,7 @@ Force-delete a custom topic (removes from all referencing profiles). > **getCustomerApp**(`appName`): `Promise`\<`CustomerAppInfo`\> -Defined in: [src/airs/management.ts:351](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L351) +Defined in: [src/airs/management.ts:353](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L353) #### Parameters @@ -352,11 +352,39 @@ Defined in: [src/airs/management.ts:351](https://github.com/cdot65/prisma-airs-c *** +### getCustomerAppConsumption() + +> **getCustomerAppConsumption**(`appName`, `opts?`): `Promise`\<`CustomerAppConsumption`\> + +Defined in: [src/airs/management.ts:371](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L371) + +Get per-app token consumption + violation breakdown from the SCM dashboard endpoints. + +#### Parameters + +##### appName + +`string` + +##### opts? + +`ConsumptionQueryOptions` + +#### Returns + +`Promise`\<`CustomerAppConsumption`\> + +#### Implementation of + +`ManagementService.getCustomerAppConsumption` + +*** + ### getProfile() > **getProfile**(`profileId`): `Promise`\<`SecurityProfileInfo`\> -Defined in: [src/airs/management.ts:245](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L245) +Defined in: [src/airs/management.ts:247](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L247) Get a single security profile by UUID. @@ -380,7 +408,7 @@ Get a single security profile by UUID. > **getProfileByName**(`profileName`): `Promise`\<`SecurityProfileInfo`\> -Defined in: [src/airs/management.ts:250](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L250) +Defined in: [src/airs/management.ts:252](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L252) Get a single security profile by name (returns highest revision). @@ -404,7 +432,7 @@ Get a single security profile by name (returns highest revision). > **getProfileTopics**(`profileName`): `Promise`\<[`ProfileTopic`](../interfaces/ProfileTopic.md)[]\> -Defined in: [src/airs/management.ts:173](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L173) +Defined in: [src/airs/management.ts:175](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L175) List all topics configured in a profile with full details. @@ -428,7 +456,7 @@ List all topics configured in a profile with full details. > **getTopic**(`topicId`): `Promise`\<`objectOutputType`\<\{ `active`: `ZodOptional`\<`ZodBoolean`\>; `created_by`: `ZodOptional`\<`ZodString`\>; `created_ts`: `ZodOptional`\<`ZodString`\>; `description`: `ZodString`; `examples`: `ZodArray`\<`ZodString`, `"many"`\>; `last_modified_ts`: `ZodOptional`\<`ZodString`\>; `revision`: `ZodNumber`; `topic_id`: `ZodOptional`\<`ZodString`\>; `topic_name`: `ZodString`; `updated_by`: `ZodOptional`\<`ZodString`\>; \}, `ZodTypeAny`, `"passthrough"`\>\> -Defined in: [src/airs/management.ts:57](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L57) +Defined in: [src/airs/management.ts:59](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L59) Get a single custom topic by ID. @@ -452,7 +480,7 @@ Get a single custom topic by ID. > **getTopicByName**(`topicName`): `Promise`\<`objectOutputType`\<\{ `active`: `ZodOptional`\<`ZodBoolean`\>; `created_by`: `ZodOptional`\<`ZodString`\>; `created_ts`: `ZodOptional`\<`ZodString`\>; `description`: `ZodString`; `examples`: `ZodArray`\<`ZodString`, `"many"`\>; `last_modified_ts`: `ZodOptional`\<`ZodString`\>; `revision`: `ZodNumber`; `topic_id`: `ZodOptional`\<`ZodString`\>; `topic_name`: `ZodString`; `updated_by`: `ZodOptional`\<`ZodString`\>; \}, `ZodTypeAny`, `"passthrough"`\>\> -Defined in: [src/airs/management.ts:64](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L64) +Defined in: [src/airs/management.ts:66](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L66) Get a single custom topic by name. @@ -476,7 +504,7 @@ Get a single custom topic by name. > **listApiKeys**(`opts?`): `Promise`\<`ApiKeyListResult`\> -Defined in: [src/airs/management.ts:303](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L303) +Defined in: [src/airs/management.ts:305](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L305) #### Parameters @@ -498,7 +526,7 @@ Defined in: [src/airs/management.ts:303](https://github.com/cdot65/prisma-airs-c > **listCustomerApps**(`opts?`): `Promise`\<`CustomerAppListResult`\> -Defined in: [src/airs/management.ts:341](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L341) +Defined in: [src/airs/management.ts:343](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L343) #### Parameters @@ -520,7 +548,7 @@ Defined in: [src/airs/management.ts:341](https://github.com/cdot65/prisma-airs-c > **listDeploymentProfiles**(`opts?`): `Promise`\<`DeploymentProfileInfo`[]\> -Defined in: [src/airs/management.ts:373](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L373) +Defined in: [src/airs/management.ts:438](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L438) #### Parameters @@ -544,7 +572,7 @@ Defined in: [src/airs/management.ts:373](https://github.com/cdot65/prisma-airs-c > **listProfiles**(`opts?`): `Promise`\<`SecurityProfileListResult`\> -Defined in: [src/airs/management.ts:255](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L255) +Defined in: [src/airs/management.ts:257](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L257) List security profiles. @@ -568,7 +596,7 @@ List security profiles. > **listTopics**(): `Promise`\<`objectOutputType`\<\{ `active`: `ZodOptional`\<`ZodBoolean`\>; `created_by`: `ZodOptional`\<`ZodString`\>; `created_ts`: `ZodOptional`\<`ZodString`\>; `description`: `ZodString`; `examples`: `ZodArray`\<`ZodString`, `"many"`\>; `last_modified_ts`: `ZodOptional`\<`ZodString`\>; `revision`: `ZodNumber`; `topic_id`: `ZodOptional`\<`ZodString`\>; `topic_name`: `ZodString`; `updated_by`: `ZodOptional`\<`ZodString`\>; \}, `ZodTypeAny`, `"passthrough"`\>[]\> -Defined in: [src/airs/management.ts:52](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L52) +Defined in: [src/airs/management.ts:54](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L54) List all custom topics. @@ -586,7 +614,7 @@ List all custom topics. > **queryScanLogs**(`opts`): `Promise`\<`ScanLogQueryResult`\> -Defined in: [src/airs/management.ts:384](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L384) +Defined in: [src/airs/management.ts:449](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L449) #### Parameters @@ -608,7 +636,7 @@ Defined in: [src/airs/management.ts:384](https://github.com/cdot65/prisma-airs-c > **regenerateApiKey**(`apiKeyId`, `request`): `Promise`\<`ApiKeyInfo`\> -Defined in: [src/airs/management.ts:318](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L318) +Defined in: [src/airs/management.ts:320](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L320) #### Parameters @@ -634,7 +662,7 @@ Defined in: [src/airs/management.ts:318](https://github.com/cdot65/prisma-airs-c > **updateCustomerApp**(`appId`, `request`): `Promise`\<`CustomerAppInfo`\> -Defined in: [src/airs/management.ts:356](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L356) +Defined in: [src/airs/management.ts:358](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L358) #### Parameters @@ -660,7 +688,7 @@ Defined in: [src/airs/management.ts:356](https://github.com/cdot65/prisma-airs-c > **updateProfile**(`profileId`, `request`): `Promise`\<`SecurityProfileInfo`\> -Defined in: [src/airs/management.ts:270](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L270) +Defined in: [src/airs/management.ts:272](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L272) Update a security profile. @@ -688,7 +716,7 @@ Update a security profile. > **updateTopic**(`topicId`, `request`): `Promise`\<`objectOutputType`\<\{ `active`: `ZodOptional`\<`ZodBoolean`\>; `created_by`: `ZodOptional`\<`ZodString`\>; `created_ts`: `ZodOptional`\<`ZodString`\>; `description`: `ZodString`; `examples`: `ZodArray`\<`ZodString`, `"many"`\>; `last_modified_ts`: `ZodOptional`\<`ZodString`\>; `revision`: `ZodNumber`; `topic_id`: `ZodOptional`\<`ZodString`\>; `topic_name`: `ZodString`; `updated_by`: `ZodOptional`\<`ZodString`\>; \}, `ZodTypeAny`, `"passthrough"`\>\> -Defined in: [src/airs/management.ts:39](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L39) +Defined in: [src/airs/management.ts:41](https://github.com/cdot65/prisma-airs-cli/blob/main/src/airs/management.ts#L41) Update an existing custom topic by ID. diff --git a/package.json b/package.json index db2ad41..f102f66 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "license": "MIT", "dependencies": { "@anthropic-ai/vertex-sdk": "^0.14.4", - "@cdot65/prisma-airs-sdk": "^0.10.0", + "@cdot65/prisma-airs-sdk": "^0.11.0", "@inquirer/prompts": "^8.3.0", "@langchain/anthropic": "^1.3.25", "@langchain/aws": "^1.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 550b393..f6f3603 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^0.14.4 version: 0.14.4(zod@3.25.76) '@cdot65/prisma-airs-sdk': - specifier: ^0.10.0 - version: 0.10.0 + specifier: ^0.11.0 + version: 0.11.0 '@inquirer/prompts': specifier: ^8.3.0 version: 8.3.0(@types/node@22.19.13) @@ -334,8 +334,8 @@ packages: cpu: [x64] os: [win32] - '@cdot65/prisma-airs-sdk@0.10.0': - resolution: {integrity: sha512-nRTfxduC69kGfdJd5xTYZN5VuaH8Qq0ZjgtadVaQynygvlF8RFU4f2bNUEK9Y3iLiBF/KguuzbKCOAsg8tRyrg==} + '@cdot65/prisma-airs-sdk@0.11.0': + resolution: {integrity: sha512-rZ5vIfTQaFRfu4ypsiHMNr0btQRMJY57SRu84vy1lXYHd9DByqstLHnNGl25CpuVnMviUHWZ/EYIedxMkySA7w==} engines: {node: '>=18'} '@cfworker/json-schema@4.1.1': @@ -3069,7 +3069,7 @@ snapshots: '@biomejs/cli-win32-x64@2.4.5': optional: true - '@cdot65/prisma-airs-sdk@0.10.0': + '@cdot65/prisma-airs-sdk@0.11.0': dependencies: zod: 3.25.76 diff --git a/src/airs/management.ts b/src/airs/management.ts index 39273eb..d21efa3 100644 --- a/src/airs/management.ts +++ b/src/airs/management.ts @@ -9,6 +9,8 @@ import type { ProfileTopic } from '../audit/types.js'; import type { ApiKeyInfo, ApiKeyListResult, + ConsumptionQueryOptions, + CustomerAppConsumption, CustomerAppInfo, CustomerAppListResult, DeleteResponse, @@ -366,6 +368,69 @@ export class SdkManagementService implements ManagementService { return this.normalizeCustomerApp(response as unknown as Record); } + async getCustomerAppConsumption( + appName: string, + opts?: ConsumptionQueryOptions, + ): Promise { + // The dashboard endpoints need BOTH appId and appName; the only way to resolve appId + // from a name is via the list endpoint, so do that first. + const list = (await this.client.customerApps.list({ + offset: 0, + limit: 100, + })) as unknown as { customer_apps?: Array> }; + const apps = list.customer_apps ?? []; + const target = apps.find((a) => (a.app_name as string) === appName); + const appId = target?.customer_appId as string | undefined; + if (!appId) { + throw new Error( + `Customer app not found: "${appName}". Run \`airs runtime customer-apps list\` to see available apps.`, + ); + } + + const timeInterval = opts?.timeInterval ?? 30; + const query = { appId, appName, timeInterval } as const; + + const [overview, breakdown] = await Promise.all([ + this.client.dashboard.application(query), + this.client.dashboard.applicationViolationBreakdown(query), + ]); + + const ts = overview.token_stats ?? {}; + const ss = overview.session_stats ?? {}; + const detectors = (breakdown.detection_type_violation_breakdown ?? []).map((entry) => { + const vb = entry.violation_breakdown ?? {}; + return { + type: entry.detection_type ?? 'unknown', + critical: vb.critical ?? 0, + high: vb.high ?? 0, + medium: vb.medium ?? 0, + low: vb.low ?? 0, + total: vb.total ?? 0, + }; + }); + + return { + appId, + appName, + cloud: overview.cloud ?? undefined, + source: overview.source ?? undefined, + monitoringSince: overview.created_at ?? undefined, + profiles: overview.profiles ?? [], + tokens: { + dailyAverage: ts.average_daily_tokens ?? undefined, + dailyAverageScale: ts.average_daily_tokens_scale ?? undefined, + monthlyTotal: ts.monthly_total_tokens ?? undefined, + monthlyTotalScale: ts.monthly_total_tokens_scale ?? undefined, + }, + sessions: { + total: ss.total ?? 0, + violating: ss.violating ?? 0, + }, + detectors, + totalViolating: breakdown.total_violating ?? 0, + }; + } + // ------------------------------------------------------------------------- // Deployment Profiles (read-only) // ------------------------------------------------------------------------- diff --git a/src/airs/types.ts b/src/airs/types.ts index 909e168..c15e357 100644 --- a/src/airs/types.ts +++ b/src/airs/types.ts @@ -836,6 +836,52 @@ export interface CustomerAppListResult { nextOffset?: number; } +/** + * Per-app consumption + violation snapshot, normalized from the SDK's dashboard endpoints. + * Time window is fixed at construction. + */ +export interface CustomerAppConsumption { + appId: string; + appName: string; + cloud?: string; + source?: string; + /** ISO timestamp of first monitoring (corresponds to SCM panel's "Monitoring Since"). */ + monitoringSince?: string; + /** Attached security profile names. */ + profiles: string[]; + /** Token consumption stats with scale qualifier (K = thousands, M = millions). */ + tokens: { + dailyAverage?: number; + dailyAverageScale?: string; + monthlyTotal?: number; + monthlyTotalScale?: string; + }; + /** Session activity counts over the window. */ + sessions: { + total: number; + violating: number; + }; + /** Per-detector violation severity counts, one entry per detection_type. */ + detectors: Array<{ + type: string; + critical: number; + high: number; + medium: number; + low: number; + total: number; + }>; + /** Sum of violating sessions across all detectors (mirrors SCM panel's badge). */ + totalViolating: number; +} + +/** Allowed values for `--time-interval`. The API enforces this enum (other values return 400). */ +export type ConsumptionTimeInterval = 7 | 30 | 60; + +/** Options for {@link ManagementService.getCustomerAppConsumption}. */ +export interface ConsumptionQueryOptions { + timeInterval?: ConsumptionTimeInterval; +} + // --------------------------------------------------------------------------- // Deployment profile types // --------------------------------------------------------------------------- @@ -931,6 +977,11 @@ export interface ManagementService { getCustomerApp(appName: string): Promise; updateCustomerApp(appId: string, request: Record): Promise; deleteCustomerApp(appName: string, updatedBy: string): Promise; + /** Get per-app token consumption + violation breakdown from the SCM dashboard endpoints. */ + getCustomerAppConsumption( + appName: string, + opts?: ConsumptionQueryOptions, + ): Promise; // Deployment profiles listDeploymentProfiles(opts?: { unactivated?: boolean }): Promise; diff --git a/src/cli/commands/runtime.ts b/src/cli/commands/runtime.ts index 25e89a5..5d28695 100644 --- a/src/cli/commands/runtime.ts +++ b/src/cli/commands/runtime.ts @@ -18,6 +18,7 @@ import { type OutputFormat, renderApiKeyDetail, renderApiKeyList, + renderCustomerAppConsumption, renderCustomerAppDetail, renderCustomerAppList, renderDeploymentProfileList, @@ -314,6 +315,56 @@ export function registerRuntimeCommand(program: Command): void { } }); + customerApps + .command('consumption [appName]') + .description( + 'Show per-app token consumption + violation breakdown (SCM dashboard). Omit appName to scan all apps.', + ) + .option('--time-interval ', 'Window in days: 7, 30, or 60', '30') + .option('--output ', 'Output format: pretty, table, csv, json, yaml', 'pretty') + .action(async (appName: string | undefined, opts) => { + try { + const fmt = opts.output as OutputFormat; + const interval = Number.parseInt(opts.timeInterval, 10); + if (interval !== 7 && interval !== 30 && interval !== 60) { + renderError('--time-interval must be 7, 30, or 60 (the API rejects other values)'); + process.exit(1); + } + if (fmt === 'pretty') renderRuntimeConfigHeader(); + + const service = await createMgmtService(); + + // Single app mode: explicit name was given. + if (appName) { + const data = await service.getCustomerAppConsumption(appName, { + timeInterval: interval, + }); + renderCustomerAppConsumption(data, fmt); + return; + } + + // All-apps mode: loop the list and emit one record per app. + const list = await service.listCustomerApps({ limit: 100 }); + if (list.apps.length === 0) { + console.log(' No customer apps found.'); + return; + } + for (const app of list.apps) { + try { + const data = await service.getCustomerAppConsumption(app.name, { + timeInterval: interval, + }); + renderCustomerAppConsumption(data, fmt); + } catch (err) { + renderError(`[${app.name}] ${err instanceof Error ? err.message : String(err)}`); + } + } + } catch (err) { + renderError(err instanceof Error ? err.message : String(err)); + process.exit(1); + } + }); + // ----------------------------------------------------------------------- // runtime deployment-profiles — read-only listing // ----------------------------------------------------------------------- diff --git a/src/cli/renderer/runtime.ts b/src/cli/renderer/runtime.ts index b97f12f..7dbfbae 100644 --- a/src/cli/renderer/runtime.ts +++ b/src/cli/renderer/runtime.ts @@ -422,6 +422,115 @@ export function renderCustomerAppDetail(app: { console.log(); } +/** Render per-app consumption + violation breakdown. */ +export function renderCustomerAppConsumption( + data: { + appId: string; + appName: string; + cloud?: string; + source?: string; + monitoringSince?: string; + profiles: string[]; + tokens: { + dailyAverage?: number; + dailyAverageScale?: string; + monthlyTotal?: number; + monthlyTotalScale?: string; + }; + sessions: { total: number; violating: number }; + detectors: Array<{ + type: string; + critical: number; + high: number; + medium: number; + low: number; + total: number; + }>; + totalViolating: number; + }, + format: OutputFormat = 'pretty', +): void { + const fmt = (n?: number, scale?: string) => (n == null ? '-' : `${n}${scale ?? ''}`); + + if (format !== 'pretty') { + // Per-detector rows for table/csv/json/yaml. Adds app-level fields so each row is + // self-contained (useful for piping into reporting tools). + const rows = data.detectors.map((d) => ({ + app_name: data.appName, + app_id: data.appId, + monitoring_since: data.monitoringSince ?? '', + daily_avg: fmt(data.tokens.dailyAverage, data.tokens.dailyAverageScale), + monthly_total: fmt(data.tokens.monthlyTotal, data.tokens.monthlyTotalScale), + sessions_total: data.sessions.total, + sessions_violating: data.sessions.violating, + detector: d.type, + critical: d.critical, + high: d.high, + medium: d.medium, + low: d.low, + total: d.total, + })); + console.log( + formatOutput( + rows, + [ + { key: 'app_name', label: 'App' }, + { key: 'app_id', label: 'AppId' }, + { key: 'monitoring_since', label: 'MonitoringSince' }, + { key: 'daily_avg', label: 'DailyAvg' }, + { key: 'monthly_total', label: 'MonthlyTotal' }, + { key: 'sessions_total', label: 'Sessions' }, + { key: 'sessions_violating', label: 'Violating' }, + { key: 'detector', label: 'Detector' }, + { key: 'critical', label: 'C' }, + { key: 'high', label: 'H' }, + { key: 'medium', label: 'M' }, + { key: 'low', label: 'L' }, + { key: 'total', label: 'Total' }, + ], + format, + ), + ); + return; + } + + console.log(chalk.bold(`\n ${data.appName} ${chalk.dim('(' + data.appId + ')')}`)); + if (data.monitoringSince) console.log(` Monitoring since: ${chalk.dim(data.monitoringSince)}`); + if (data.source) console.log(` Source: ${data.source}`); + if (data.cloud) console.log(` Cloud: ${data.cloud}`); + if (data.profiles.length > 0) { + console.log(` Profiles: ${data.profiles.join(', ')}`); + } + + console.log(chalk.bold('\n Token consumption:')); + console.log( + ` Daily avg: ${fmt(data.tokens.dailyAverage, data.tokens.dailyAverageScale)}`, + ); + console.log( + ` Monthly total: ${fmt(data.tokens.monthlyTotal, data.tokens.monthlyTotalScale)}`, + ); + + console.log(chalk.bold('\n Sessions:')); + console.log(` Total: ${data.sessions.total}`); + console.log(` Violating: ${data.sessions.violating}`); + + const firing = data.detectors.filter((d) => d.total > 0); + console.log( + chalk.bold( + `\n Detectors (${data.totalViolating} violating, ${firing.length}/${data.detectors.length} firing):`, + ), + ); + if (firing.length === 0) { + console.log(chalk.dim(' no detector violations in window')); + } else { + for (const d of firing) { + const sev = `c=${d.critical} h=${d.high} m=${d.medium} l=${d.low}`; + console.log(` ${d.type.padEnd(20)} ${String(d.total).padStart(5)} ${chalk.dim(sev)}`); + } + } + console.log(); +} + /** Render deployment profile list. */ export function renderDeploymentProfileList( profiles: Array<{ raw: Record }>, diff --git a/tests/unit/airs/management.spec.ts b/tests/unit/airs/management.spec.ts index 3a8d86b..07d67b3 100644 --- a/tests/unit/airs/management.spec.ts +++ b/tests/unit/airs/management.spec.ts @@ -47,6 +47,8 @@ const mockCustomerAppsUpdate = vi.fn(); const mockCustomerAppsDelete = vi.fn(); const mockDeploymentProfilesList = vi.fn(); const mockScanLogsQuery = vi.fn(); +const mockDashboardApplication = vi.fn(); +const mockDashboardViolationBreakdown = vi.fn(); vi.mock('@cdot65/prisma-airs-sdk', () => ({ ManagementClient: vi.fn().mockImplementation(() => ({ @@ -84,6 +86,10 @@ vi.mock('@cdot65/prisma-airs-sdk', () => ({ scanLogs: { query: mockScanLogsQuery, }, + dashboard: { + application: mockDashboardApplication, + applicationViolationBreakdown: mockDashboardViolationBreakdown, + }, })), })); @@ -1179,6 +1185,125 @@ describe('SdkManagementService', () => { }); }); }); + + describe('getCustomerAppConsumption', () => { + function primeApps() { + mockCustomerAppsList.mockResolvedValue({ + customer_apps: [ + { customer_appId: 'uuid-chatbot', app_name: 'chatbot' }, + { customer_appId: 'uuid-litellm', app_name: 'litellm' }, + ], + next_offset: 0, + }); + } + + it('resolves appId from list, calls both dashboard endpoints, normalizes the result', async () => { + primeApps(); + mockDashboardApplication.mockResolvedValue({ + id: 'uuid-chatbot', + name: 'chatbot', + cloud: 'other', + source: 'api', + created_at: '2026-04-29T17:04:52Z', + profiles: ['ms-tuned', 'golden-v2'], + token_stats: { + average_daily_tokens: 744.233, + average_daily_tokens_scale: 'K', + monthly_total_tokens: 17.71, + monthly_total_tokens_scale: 'M', + }, + session_stats: { total: 56935, violating: 31136 }, + }); + mockDashboardViolationBreakdown.mockResolvedValue({ + detection_type_violation_breakdown: [ + { + detection_type: 'topic_guardrails', + violation_breakdown: { critical: 0, high: 0, medium: 3, low: 0, total: 3 }, + }, + { + detection_type: 'dlp', + violation_breakdown: { critical: 0, high: 0, medium: 0, low: 0, total: 0 }, + }, + ], + total_violating: 3, + }); + + const result = await service.getCustomerAppConsumption('chatbot'); + + expect(result.appId).toBe('uuid-chatbot'); + expect(result.appName).toBe('chatbot'); + expect(result.tokens.dailyAverage).toBe(744.233); + expect(result.tokens.dailyAverageScale).toBe('K'); + expect(result.tokens.monthlyTotalScale).toBe('M'); + expect(result.sessions.total).toBe(56935); + expect(result.sessions.violating).toBe(31136); + expect(result.profiles).toEqual(['ms-tuned', 'golden-v2']); + expect(result.totalViolating).toBe(3); + expect(result.detectors).toHaveLength(2); + const tg = result.detectors.find((d) => d.type === 'topic_guardrails'); + expect(tg).toEqual({ + type: 'topic_guardrails', + critical: 0, + high: 0, + medium: 3, + low: 0, + total: 3, + }); + + expect(mockDashboardApplication).toHaveBeenCalledWith({ + appId: 'uuid-chatbot', + appName: 'chatbot', + timeInterval: 30, + }); + expect(mockDashboardViolationBreakdown).toHaveBeenCalledWith({ + appId: 'uuid-chatbot', + appName: 'chatbot', + timeInterval: 30, + }); + }); + + it('passes through the timeInterval option', async () => { + primeApps(); + mockDashboardApplication.mockResolvedValue({}); + mockDashboardViolationBreakdown.mockResolvedValue({}); + + await service.getCustomerAppConsumption('litellm', { timeInterval: 60 }); + + expect(mockDashboardApplication).toHaveBeenCalledWith({ + appId: 'uuid-litellm', + appName: 'litellm', + timeInterval: 60, + }); + }); + + it('throws a clear error when the app name is not found in the list', async () => { + mockCustomerAppsList.mockResolvedValue({ customer_apps: [], next_offset: 0 }); + await expect(service.getCustomerAppConsumption('nonexistent')).rejects.toThrow( + /Customer app not found.*nonexistent/i, + ); + expect(mockDashboardApplication).not.toHaveBeenCalled(); + expect(mockDashboardViolationBreakdown).not.toHaveBeenCalled(); + }); + + it('returns zeros for missing/null fields rather than throwing', async () => { + primeApps(); + mockDashboardApplication.mockResolvedValue({ + name: null, + token_stats: null, + session_stats: null, + profiles: null, + }); + mockDashboardViolationBreakdown.mockResolvedValue({}); + + const result = await service.getCustomerAppConsumption('chatbot'); + expect(result.tokens.dailyAverage).toBeUndefined(); + expect(result.sessions.total).toBe(0); + expect(result.sessions.violating).toBe(0); + expect(result.profiles).toEqual([]); + expect(result.detectors).toEqual([]); + expect(result.totalViolating).toBe(0); + }); + }); }); describe('getOrCreateManagementClient', () => {