Skip to content

Commit c8e39fb

Browse files
authored
improvement(insights): clarify unassigned usage and refresh usage charts (#9265)
* improvement(insights): clarify unassigned usage and refresh usage charts * fix(insights): preserve sub-credit activity in the usage heatmap
1 parent 19f2475 commit c8e39fb

20 files changed

Lines changed: 586 additions & 147 deletions

File tree

‎apps/docs/content/docs/platform/enterprise/usage-tracking.mdx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ Organization owners and admins open **Settings → Organization → Insights**.
1616
| **Last 7 days** / **Last 30 days** | Rolling window ending now. |
1717
| **Custom range** | Selected dates, up to 92 days. |
1818

19-
The period applies to every tab. Daily bars use your browser's timezone, while billing periods begin at a fixed UTC instant. The first and last bars can be partial. Different timezones can split the same total differently across days.
19+
The period applies to every tab's charts and breakdowns. The **Usage heatmap** below the credit chart in Overview and workspace details always shows the last 365 days, including today's partial day. Hover, select, or use the arrow keys to inspect a day. A positive charge below one displayed credit reads **<1 credit** and remains colored.
20+
21+
Daily bars and heatmap cells use your browser's timezone, while billing periods begin at a fixed UTC instant. The first and last bars can be partial. Different timezones can split the same total differently across days.
2022

2123
Spend is in **credits**, where 1 credit = $0.005; the UI rounds displayed credits. [Logs](/logs-debugging/logging) and Logs API costs are in USD, while Billing and run-resource API amounts use their documented credit units. Check the endpoint contract before comparing amounts. See [cost calculation](/platform/costs).
2224

@@ -67,7 +69,7 @@ Each charge has one source. The exact **Agent block** label in Insights refers t
6769

6870
An ordinary Agent block's hosted model calls remain under Workflow. The Workflow base charge applies even without model calls. To assess a workflow using the Sim Chat block, read its Workflow charges and the separately metered Agent block source; neither charge is counted twice.
6971

70-
A hosted-key tool called inside a workflow belongs to Workflow; the same tool called directly belongs to API tool call. Chat panel, API agent, and Inbox charges cannot be separated by source. Dedicated workspace attribution can distinguish them when those surfaces actually name different workspaces. Usage without a workspace appears under **No workspace** and remains part of the organization total.
72+
A hosted-key tool called inside a workflow belongs to Workflow; the same tool called directly belongs to API tool call. Chat panel, API agent, and Inbox charges cannot be separated by source. Dedicated workspace attribution can distinguish them when those surfaces actually name different workspaces. Usage without a workspace link appears under **Unassigned usage** and remains part of the organization total. This includes organization-level activity and retained charges from permanently deleted workspaces; the row cannot distinguish between them.
7173

7274
### Reconcile totals
7375

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { getOrganizationUsageCalendarContract } from '@/lib/api/contracts/organization-usage'
2+
import {
3+
defineInternalJsonRoute,
4+
internalRateLimits,
5+
internalSessionAuth,
6+
} from '@/lib/api/server/routes'
7+
import { getOrganizationUsageCalendar } from '@/lib/billing/application/organization-usage/get-organization-usage-calendar'
8+
import { organizationUsageOperations } from '@/lib/billing/application/organization-usage/operations'
9+
import { organizationUsageErrorPolicy } from '@/app/api/organizations/[id]/usage/error-policy'
10+
11+
export const dynamic = 'force-dynamic'
12+
13+
export const GET = defineInternalJsonRoute({
14+
contract: getOrganizationUsageCalendarContract,
15+
auth: internalSessionAuth,
16+
operation: organizationUsageOperations.readCalendar,
17+
rateLimit: internalRateLimits.none({
18+
reason:
19+
'Authenticated org-admin settings read, gated on enterprise entitlement and billing authority',
20+
}),
21+
errorPolicy: organizationUsageErrorPolicy,
22+
mapInput: ({ params, query }) => ({
23+
organizationId: params.id,
24+
workspaceId: query.workspaceId,
25+
timezone: query.timezone,
26+
}),
27+
useCase: getOrganizationUsageCalendar,
28+
present: (result) => result,
29+
})

‎apps/sim/ee/organization-usage/components/activity-summary.tsx‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,12 @@ import type { OrganizationUsageWindowKey } from '@/hooks/queries/utils/organizat
2525
const CHART_HEIGHT = 180
2626

2727
/**
28-
* Outcome layers, bottom-up. Failed is the status red and sits on the stack where a
29-
* spike reads at a glance; Other (cancelled, paused, unfinished) stays neutral, in a
30-
* gray whose lightness keeps it apart from the red for color-vision deficiency.
28+
* Outcome layers, bottom-up. Failed stays red; cancelled, paused and unfinished
29+
* runs share the neutral remainder.
3130
*/
3231
const OUTCOMES = [
33-
{ id: 'completed', label: 'Completed', color: 'var(--brand-blue)' },
34-
{ id: 'failed', label: 'Failed', color: 'var(--text-error)' },
32+
{ id: 'completed', label: 'Completed', color: 'var(--usage-workflow)' },
33+
{ id: 'failed', label: 'Failed', color: 'var(--usage-failed)' },
3534
{ id: 'other', label: 'Other', color: USAGE_OTHER_COLOR },
3635
] as const
3736

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use client'
2+
3+
import { ChartFrame } from '@sim/emcn'
4+
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
5+
import { UsageHeatmap } from '@/ee/organization-usage/components/usage-heatmap'
6+
import { useOrganizationUsageCalendar } from '@/hooks/queries/organization-usage'
7+
8+
interface UsageCalendarProps {
9+
organizationId: string
10+
timezone: string
11+
workspaceId?: string
12+
}
13+
14+
/** Loads the year separately so its history cannot delay the selected period's chart. */
15+
export function UsageCalendar({ organizationId, timezone, workspaceId }: UsageCalendarProps) {
16+
const calendar = useOrganizationUsageCalendar(organizationId, timezone, workspaceId)
17+
return (
18+
<SettingsSection
19+
label='Usage heatmap'
20+
action={<span className='text-[var(--text-muted)] text-small'>Last 365 days</span>}
21+
>
22+
{calendar.isError || !calendar.data ? (
23+
<ChartFrame
24+
height={172}
25+
loading={calendar.isLoading}
26+
error={calendar.isError ? 'Could not load daily usage' : undefined}
27+
onRetry={() => void calendar.refetch()}
28+
>
29+
{null}
30+
</ChartFrame>
31+
) : (
32+
<UsageHeatmap days={calendar.data.days} />
33+
)}
34+
</SettingsSection>
35+
)
36+
}

‎apps/sim/ee/organization-usage/components/usage-consumers.tsx‎

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
11
'use client'
22

3-
import type { ComponentType } from 'react'
4-
import { Avatar, cn, disclosureChevronClass, formatChartCompactNumber } from '@sim/emcn'
5-
import { ArrowRight, ChevronDown } from '@sim/emcn/icons'
3+
import { type ComponentType, useId, useState } from 'react'
4+
import {
5+
Avatar,
6+
Button,
7+
cn,
8+
disclosureChevronClass,
9+
FloatingTooltip,
10+
formatChartCompactNumber,
11+
OverflowText,
12+
Popover,
13+
PopoverContent,
14+
PopoverTrigger,
15+
useFloatingTooltip,
16+
} from '@sim/emcn'
17+
import { ArrowRight, ChevronDown, CircleInfo } from '@sim/emcn/icons'
618
import {
719
AnthropicIcon,
820
AzureIcon,
@@ -91,6 +103,7 @@ const PROVIDER_ICONS: Readonly<Record<string, ComponentType<{ className?: string
91103

92104
interface UsageConsumerRowProps {
93105
row: OrganizationUsageBreakdownRow
106+
info?: string
94107
/** Member rows lead with the member's avatar where other rows show a provider mark. */
95108
isMember: boolean
96109
/** BYOK rows carry no cost, so tokens are the only usage they can show. */
@@ -124,12 +137,46 @@ const TRAILING_SLOT_CLASSES = {
124137
*/
125138
export const USAGE_ROW_CLASSES = 'flex w-full items-center gap-2.5 rounded-lg p-2 text-left'
126139

140+
interface UsageInfoProps {
141+
label: string
142+
info: string
143+
}
144+
145+
function UsageInfo({ label, info }: UsageInfoProps) {
146+
const [open, setOpen] = useState(false)
147+
const tooltipId = useId()
148+
const tooltip = useFloatingTooltip(() => !open, { revalidateKey: open })
149+
150+
return (
151+
<Popover open={open} onOpenChange={setOpen}>
152+
<PopoverTrigger asChild>
153+
<Button
154+
variant='quiet'
155+
size='icon'
156+
aria-label={label}
157+
aria-describedby={!open && tooltip.state.visible ? tooltipId : undefined}
158+
{...tooltip.handlers}
159+
>
160+
<CircleInfo className='size-[14px]' />
161+
</Button>
162+
</PopoverTrigger>
163+
<PopoverContent appearance='tooltip' aria-label={label}>
164+
{info}
165+
</PopoverContent>
166+
{!open && (
167+
<FloatingTooltip id={tooltipId} role='tooltip' label={info} state={tooltip.state} />
168+
)}
169+
</Popover>
170+
)
171+
}
172+
127173
/**
128174
* A tabular row, not `SettingsResourceRow` — tabular columns are the sanctioned
129175
* exception in `sim-settings-pages.md`, alongside billing invoices and credit usage.
130176
*/
131177
function UsageConsumerRow({
132178
row,
179+
info,
133180
isMember,
134181
showTokensOnly,
135182
onSelect,
@@ -158,7 +205,14 @@ function UsageConsumerRow({
158205
) : (
159206
ProviderIcon && <ProviderIcon className='size-[14px] shrink-0 text-[var(--text-icon)]' />
160207
)}
161-
<span className='min-w-0 flex-1 truncate text-[var(--text-body)] text-sm'>{row.label}</span>
208+
<div className='flex min-w-0 flex-1 items-center gap-1'>
209+
<OverflowText
210+
label={row.label}
211+
className='text-[var(--text-body)] text-sm'
212+
focusTarget={onSelect ? 'nearest-interactive' : undefined}
213+
/>
214+
{info && <UsageInfo label={`About ${row.label.toLowerCase()}`} info={info} />}
215+
</div>
162216
<div
163217
className='h-[4px] w-[64px] shrink-0 overflow-hidden rounded-full bg-[var(--border)]'
164218
aria-hidden='true'
@@ -243,9 +297,14 @@ export function UsageConsumers({
243297
<UsageConsumerRow
244298
key={`${dimension}-${row.id}`}
245299
row={row}
300+
info={
301+
dimension === 'workspace' && !row.id
302+
? 'Usage without a workspace link, including organization-level activity and permanently deleted workspaces.'
303+
: undefined
304+
}
246305
isMember={dimension === 'member'}
247306
showTokensOnly={showTokensOnly}
248-
{...(onExpandOther && trailingSlot ? { reservedTrailing: trailingSlot } : {})}
307+
{...(trailingSlot ? { reservedTrailing: trailingSlot } : {})}
249308
{...(onSelectRow && row.id ? { onSelect: onSelectRow } : {})}
250309
{...(rowActions && row.id ? { actions: rowActions(row) } : {})}
251310
/>

0 commit comments

Comments
 (0)