Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions apps/docs/content/docs/platform/enterprise/usage-tracking.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ Organization owners and admins open **Settings → Organization → Insights**.
| **Last 7 days** / **Last 30 days** | Rolling window ending now. |
| **Custom range** | Selected dates, up to 92 days. |

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

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.

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

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

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.

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

### Reconcile totals

Expand Down
29 changes: 29 additions & 0 deletions apps/sim/app/api/organizations/[id]/usage/calendar/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { getOrganizationUsageCalendarContract } from '@/lib/api/contracts/organization-usage'
import {
defineInternalJsonRoute,
internalRateLimits,
internalSessionAuth,
} from '@/lib/api/server/routes'
import { getOrganizationUsageCalendar } from '@/lib/billing/application/organization-usage/get-organization-usage-calendar'
import { organizationUsageOperations } from '@/lib/billing/application/organization-usage/operations'
import { organizationUsageErrorPolicy } from '@/app/api/organizations/[id]/usage/error-policy'

export const dynamic = 'force-dynamic'

export const GET = defineInternalJsonRoute({
contract: getOrganizationUsageCalendarContract,
auth: internalSessionAuth,
operation: organizationUsageOperations.readCalendar,
rateLimit: internalRateLimits.none({
reason:
'Authenticated org-admin settings read, gated on enterprise entitlement and billing authority',
}),
errorPolicy: organizationUsageErrorPolicy,
mapInput: ({ params, query }) => ({
organizationId: params.id,
workspaceId: query.workspaceId,
timezone: query.timezone,
}),
useCase: getOrganizationUsageCalendar,
present: (result) => result,
})
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,12 @@ import type { OrganizationUsageWindowKey } from '@/hooks/queries/utils/organizat
const CHART_HEIGHT = 180

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

Expand Down
36 changes: 36 additions & 0 deletions apps/sim/ee/organization-usage/components/usage-calendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use client'

import { ChartFrame } from '@sim/emcn'
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
import { UsageHeatmap } from '@/ee/organization-usage/components/usage-heatmap'
import { useOrganizationUsageCalendar } from '@/hooks/queries/organization-usage'

interface UsageCalendarProps {
organizationId: string
timezone: string
workspaceId?: string
}

/** Loads the year separately so its history cannot delay the selected period's chart. */
export function UsageCalendar({ organizationId, timezone, workspaceId }: UsageCalendarProps) {
const calendar = useOrganizationUsageCalendar(organizationId, timezone, workspaceId)
return (
<SettingsSection
label='Usage heatmap'
action={<span className='text-[var(--text-muted)] text-small'>Last 365 days</span>}
>
{calendar.isError || !calendar.data ? (
<ChartFrame
height={172}
loading={calendar.isLoading}
error={calendar.isError ? 'Could not load daily usage' : undefined}
onRetry={() => void calendar.refetch()}
>
{null}
</ChartFrame>
) : (
<UsageHeatmap days={calendar.data.days} />
)}
</SettingsSection>
)
}
69 changes: 64 additions & 5 deletions apps/sim/ee/organization-usage/components/usage-consumers.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
'use client'

import type { ComponentType } from 'react'
import { Avatar, cn, disclosureChevronClass, formatChartCompactNumber } from '@sim/emcn'
import { ArrowRight, ChevronDown } from '@sim/emcn/icons'
import { type ComponentType, useId, useState } from 'react'
import {
Avatar,
Button,
cn,
disclosureChevronClass,
FloatingTooltip,
formatChartCompactNumber,
OverflowText,
Popover,
PopoverContent,
PopoverTrigger,
useFloatingTooltip,
} from '@sim/emcn'
import { ArrowRight, ChevronDown, CircleInfo } from '@sim/emcn/icons'
import {
AnthropicIcon,
AzureIcon,
Expand Down Expand Up @@ -91,6 +103,7 @@ const PROVIDER_ICONS: Readonly<Record<string, ComponentType<{ className?: string

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

interface UsageInfoProps {
label: string
info: string
}

function UsageInfo({ label, info }: UsageInfoProps) {
const [open, setOpen] = useState(false)
const tooltipId = useId()
const tooltip = useFloatingTooltip(() => !open, { revalidateKey: open })

return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant='quiet'
size='icon'
aria-label={label}
aria-describedby={!open && tooltip.state.visible ? tooltipId : undefined}
{...tooltip.handlers}
>
<CircleInfo className='size-[14px]' />
</Button>
</PopoverTrigger>
<PopoverContent appearance='tooltip' aria-label={label}>
{info}
</PopoverContent>
{!open && (
<FloatingTooltip id={tooltipId} role='tooltip' label={info} state={tooltip.state} />
)}
</Popover>
)
}

/**
* A tabular row, not `SettingsResourceRow` — tabular columns are the sanctioned
* exception in `sim-settings-pages.md`, alongside billing invoices and credit usage.
*/
function UsageConsumerRow({
row,
info,
isMember,
showTokensOnly,
onSelect,
Expand Down Expand Up @@ -158,7 +205,14 @@ function UsageConsumerRow({
) : (
ProviderIcon && <ProviderIcon className='size-[14px] shrink-0 text-[var(--text-icon)]' />
)}
<span className='min-w-0 flex-1 truncate text-[var(--text-body)] text-sm'>{row.label}</span>
<div className='flex min-w-0 flex-1 items-center gap-1'>
<OverflowText
label={row.label}
className='text-[var(--text-body)] text-sm'
focusTarget={onSelect ? 'nearest-interactive' : undefined}
/>
{info && <UsageInfo label={`About ${row.label.toLowerCase()}`} info={info} />}
</div>
<div
className='h-[4px] w-[64px] shrink-0 overflow-hidden rounded-full bg-[var(--border)]'
aria-hidden='true'
Expand Down Expand Up @@ -243,9 +297,14 @@ export function UsageConsumers({
<UsageConsumerRow
key={`${dimension}-${row.id}`}
row={row}
info={
dimension === 'workspace' && !row.id
? 'Usage without a workspace link, including organization-level activity and permanently deleted workspaces.'
: undefined
}
isMember={dimension === 'member'}
showTokensOnly={showTokensOnly}
{...(onExpandOther && trailingSlot ? { reservedTrailing: trailingSlot } : {})}
{...(trailingSlot ? { reservedTrailing: trailingSlot } : {})}
{...(onSelectRow && row.id ? { onSelect: onSelectRow } : {})}
{...(rowActions && row.id ? { actions: rowActions(row) } : {})}
/>
Expand Down
Loading
Loading