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
1 change: 1 addition & 0 deletions dashboard/src/content/copy.csv
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ dashboard.projects.limit_top_3,ui,ProjectUsagePanel,ProjectUsagePanel,limit_top_
dashboard.projects.limit_top_6,ui,ProjectUsagePanel,ProjectUsagePanel,limit_top_6,TOP 6,,active
dashboard.projects.limit_top_10,ui,ProjectUsagePanel,ProjectUsagePanel,limit_top_10,TOP 10,,active
dashboard.projects.empty,ui,ProjectUsagePanel,ProjectUsagePanel,empty,No public repositories,,active
dashboard.projects.unattributed,ui,ProjectUsagePanel,ProjectUsagePanel,unattributed,Not counted per repo:,Prefix for the list of sources with usage but no project attribution,active
dashboard.widgets.title,dashboard,DashboardPage,WidgetOnboardingCard,title,Desktop Widgets,,active
dashboard.widgets.hint,dashboard,DashboardPage,WidgetOnboardingCard,hint,"Right-click your desktop → Edit Widgets → search ""TokenTracker""",,active
dashboard.widgets.dismiss_aria,dashboard,DashboardPage,WidgetOnboardingCard,dismiss_aria,Dismiss widget intro,,active
Expand Down
10 changes: 9 additions & 1 deletion dashboard/src/hooks/use-project-usage-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export function useProjectUsageSummary({
tzOffsetMinutes,
}: any = {}) {
const [entries, setEntries] = useState<any[]>([]);
// Sources with usage in this window that carry no project attribution. The
// panel names them: their absence otherwise reads as "that tool cost nothing
// here", which under-reports while looking complete.
const [unattributedSources, setUnattributedSources] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

Expand All @@ -30,10 +34,14 @@ export function useProjectUsageSummary({
tzOffsetMinutes,
});
setEntries(Array.isArray(res?.entries) ? res.entries : []);
setUnattributedSources(
Array.isArray(res?.unattributed_sources) ? res.unattributed_sources : [],
);
} catch (err) {
const message = (err as any)?.message || String(err);
setError(message);
setEntries([]);
setUnattributedSources([]);
} finally {
setLoading(false);
}
Expand All @@ -43,5 +51,5 @@ export function useProjectUsageSummary({
refresh();
}, [refresh]);

return { entries, loading, error, refresh };
return { entries, unattributedSources, loading, error, refresh };
}
2 changes: 2 additions & 0 deletions dashboard/src/pages/DashboardPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ export function DashboardPage({
const [projectUsageLimit, setProjectUsageLimit] = useState(10);
const {
entries: projectUsageEntries,
unattributedSources: projectUnattributedSources,
loading: projectUsageLoading,
refresh: refreshProjectUsage,
} = useProjectUsageSummary({
Expand Down Expand Up @@ -1097,6 +1098,7 @@ export function DashboardPage({
identitySubscriptions={identitySubscriptions}
identityScrambleDurationMs={identityScrambleDurationMs}
projectUsageEntries={projectUsageEntries}
projectUnattributedSources={projectUnattributedSources}
projectUsageLimit={projectUsageLimit}
setProjectUsageLimit={setProjectUsageLimit}
pulse={pulse}
Expand Down
15 changes: 15 additions & 0 deletions dashboard/src/ui/dashboard/components/DataDetails.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function ProjectAvatar({ projectKey, projectRef }) {
export function DataDetails({
// Project props
projectEntries = [],
projectUnattributedSources = [],
projectLimit = 3,
onProjectLimitChange,
// Daily breakdown props
Expand Down Expand Up @@ -57,6 +58,11 @@ export function DataDetails({
setDetailsPage,
}) {
const [activeTab, setActiveTab] = useState("daily");
// A named boolean rather than an inline `length > 0 ?`: the ui-hardcode
// scanner reads `0 ? (` as a JSX text node and would ratchet on it. Same false
// positive class as a three-digit issue reference, which it reads as a hex
// colour — this very comment tripped that on the first attempt.
const hasUnattributedSources = projectUnattributedSources.length > 0;

function getDailyCellClass(row, column, index) {
if (index === 0) return "text-oai-gray-500 dark:text-oai-gray-300";
Expand Down Expand Up @@ -162,6 +168,15 @@ export function DataDetails({
</div>
);
})}
{/* Naming what this tab cannot account for. projectBucketsQueued
exists in 7 parsers; Cursor, Copilot, Zed, Goose and Kiro have no
per-repo story, and without this line their absence reads as "that
tool cost nothing here" rather than "we cannot attribute it". */}
{hasUnattributedSources ? (
<div className="pt-2 oai-text-caption text-oai-gray-500 dark:text-oai-gray-400">
{`${copy("dashboard.projects.unattributed")} ${projectUnattributedSources.join(", ")}`}
</div>
) : null}
</div>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,20 @@ describe("DataDetails — Projects empty state", () => {
expect(screen.getByText("dashboard.projects.empty")).toBeInTheDocument();
});
});

describe("DataDetails — sources with no per-repo attribution", () => {
it("names them, so their absence does not read as zero spend", () => {
// projectBucketsQueued exists in 7 parsers. Cursor, Copilot, Zed, Goose and
// Kiro have no per-repo story at all, and the panel used to just omit them —
// which looks identical to "that tool cost nothing here".
renderDetails({ projectUnattributedSources: ["cursor", "zed"] });
fireEvent.click(screen.getByText("Project Usage"));
expect(screen.getByText(/cursor, zed/)).toBeTruthy();
});

it("says nothing when everything is attributed", () => {
renderDetails({ projectUnattributedSources: [] });
fireEvent.click(screen.getByText("Project Usage"));
expect(screen.queryByText("dashboard.projects.unattributed")).toBeNull();
});
});
2 changes: 2 additions & 0 deletions dashboard/src/ui/dashboard/views/DashboardView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export function DashboardView(props) {
identitySubscriptions,
identityScrambleDurationMs,
projectUsageEntries,
projectUnattributedSources = [],
projectUsageLimit,
setProjectUsageLimit,
pulse,
Expand Down Expand Up @@ -287,6 +288,7 @@ export function DashboardView(props) {
<FadeIn delay={0.43}>
<DataDetails
projectEntries={projectUsageEntries}
projectUnattributedSources={projectUnattributedSources}
projectLimit={projectUsageLimit}
onProjectLimitChange={setProjectUsageLimit}
copy={copy}
Expand Down
26 changes: 13 additions & 13 deletions openwiki-facts/source-facts.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,87 +44,87 @@
"methods": [
"POST"
],
"evidence": "src/lib/local-api.js:985",
"evidence": "src/lib/local-api.js:1139",
"mutation": true
},
{
"path": "/functions/tokentracker-wrapped",
"methods": [
"GET"
],
"evidence": "src/lib/local-api.js:1019",
"evidence": "src/lib/local-api.js:1173",
"mutation": false
},
{
"path": "/functions/tokentracker-usage-summary",
"methods": [
"GET"
],
"evidence": "src/lib/local-api.js:1030",
"evidence": "src/lib/local-api.js:1184",
"mutation": false
},
{
"path": "/functions/tokentracker-usage-daily",
"methods": [
"GET"
],
"evidence": "src/lib/local-api.js:1115",
"evidence": "src/lib/local-api.js:1269",
"mutation": false
},
{
"path": "/functions/tokentracker-usage-heatmap",
"methods": [
"GET"
],
"evidence": "src/lib/local-api.js:1126",
"evidence": "src/lib/local-api.js:1280",
"mutation": false
},
{
"path": "/functions/tokentracker-usage-model-breakdown",
"methods": [
"GET"
],
"evidence": "src/lib/local-api.js:1194",
"evidence": "src/lib/local-api.js:1348",
"mutation": false
},
{
"path": "/functions/tokentracker-usage-category-breakdown",
"methods": [
"GET"
],
"evidence": "src/lib/local-api.js:1273",
"evidence": "src/lib/local-api.js:1427",
"mutation": false
},
{
"path": "/functions/tokentracker-project-usage-summary",
"methods": [
"GET"
],
"evidence": "src/lib/local-api.js:1319",
"evidence": "src/lib/local-api.js:1473",
"mutation": false
},
{
"path": "/functions/tokentracker-user-status",
"methods": [
"GET"
],
"evidence": "src/lib/local-api.js:1396",
"evidence": "src/lib/local-api.js:1486",
"mutation": false
},
{
"path": "/functions/tokentracker-usage-hourly",
"methods": [
"GET"
],
"evidence": "src/lib/local-api.js:1406",
"evidence": "src/lib/local-api.js:1496",
"mutation": false
},
{
"path": "/functions/tokentracker-usage-monthly",
"methods": [
"GET"
],
"evidence": "src/lib/local-api.js:1416",
"evidence": "src/lib/local-api.js:1506",
"mutation": false
},
{
Expand All @@ -133,15 +133,15 @@
"GET",
"POST"
],
"evidence": "src/lib/local-api.js:1450",
"evidence": "src/lib/local-api.js:1540",
"mutation": true
},
{
"path": "/functions/tokentracker-usage-limits",
"methods": [
"GET"
],
"evidence": "src/lib/local-api.js:1600",
"evidence": "src/lib/local-api.js:1690",
"mutation": false
}
]
Expand Down
Loading