Skip to content

feat(platform): add workflow usage & performance metrics dashboard#1574

Merged
larryro merged 3 commits into
mainfrom
feat/workflow-metrics
Apr 19, 2026
Merged

feat(platform): add workflow usage & performance metrics dashboard#1574
larryro merged 3 commits into
mainfrom
feat/workflow-metrics

Conversation

@larryro
Copy link
Copy Markdown
Collaborator

@larryro larryro commented Apr 19, 2026

Summary

  • New /dashboard/:id/automations/metrics page with org-wide KPIs (total runs, success rate, avg duration, failed runs), a daily runs-over-time bar chart, a status donut, and a top-workflows table whose rows link to the existing per-workflow executions view.
  • 7 / 30 / 90-day period selector, URL-persistent via ?period=. Gated by the same ability check used for the automations list.
  • Backend: getOrgWorkflowMetrics RPC aggregates wfExecutions with a 5,000-row scan cap, pre-seeds daily series buckets so empty days render as zero, and groups per-workflow stats. Adds recharts for the charts.

Test plan

  • Run bunx tsc --noEmit and bunx oxlint --type-aware from services/platform/ — both clean
  • Open /dashboard/:id/automations, click View metrics — page renders with 4 stat cards, trend chart, status donut, and top-workflows table
  • Change period selector (7 / 30 / 90) — data refetches and ?period= updates in URL
  • Click a row in top-workflows — navigates to that workflow's existing executions tab
  • Verify org with zero runs in window shows clean empty states (no chart noise, empty table with CTA)
  • Switch locale to de / fr — all labels translated, no raw i18n keys leak
  • As a user without write on wfDefinitions, visit /automations/metrics<AccessDenied /> renders

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Automation Metrics dashboard displaying execution trends, status breakdown, and top workflows overview
    • Added selectable reporting periods (7, 30, and 90 days) for flexible metric analysis
    • Added quick access link in automations table to view detailed metrics
  • Chores

    • Added charting library for metrics visualization

larryro added 3 commits April 19, 2026 14:20
Adds a dedicated /automations/metrics page that surfaces org-wide workflow
KPIs (total runs, success rate, average duration, failed runs), a daily
runs-over-time chart, a status breakdown, and a top-workflows table whose
rows link into the existing per-workflow executions view. A 7/30/90-day
period selector is URL-persistent via ?period=, and access is gated by
the same ability check used for the automations list.

Backend: new getOrgWorkflowMetrics helper + RPC that aggregates runs from
wfExecutions with a 5,000-row scan cap, pre-seeds daily series buckets so
gaps render as zero, and groups per-workflow stats.

Frontend: recharts added for the trend bar chart and status donut. New
"View metrics" secondary button on the automations list opens the page.

i18n keys added under automations.metrics.* for en, de, fr.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 19, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a comprehensive metrics dashboard for workflow automations. The changes include a new route at /dashboard/$id/automations/metrics with associated React components that display organization-level workflow execution metrics (total runs, success rate, average duration, failed runs) visualized through summary cards, execution trend charts, status breakdowns, and a top workflows table. A backend Convex query helper computes metrics over selectable periods (7, 30, or 90 days) by aggregating execution records and bucketing data into time series. The automations table now includes a link button to access this metrics page, and localization strings are added for English, German, and French. The recharts library is added as a dependency for chart rendering.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a workflow metrics dashboard with usage and performance analytics for the platform.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/workflow-metrics

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@services/platform/app/features/automations/metrics/execution-trend-chart.tsx`:
- Around line 27-30: The shortLabel function currently slices the dateKey string
manually (function shortLabel) which is not locale-aware; instead parse dateKey
into a Date and format it via the project's shared utilities—either call the
React hook useFormatDate() inside the component that renders shortLabel or
import formatDate from lib/utils/date/format and return formatDate(parsedDate, {
pattern: 'MM-dd' } or the equivalent option your util accepts); replace the
manual split logic with that call so formatting uses the shared, locale-aware
formatter.

In `@services/platform/app/features/automations/metrics/format-duration.ts`:
- Around line 1-11: formatDurationSeconds currently emits hardcoded unit labels
("s", "m", "h") and may print fractional seconds; update it to use the app's
translation utilities (e.g., the i18n/t hook or translation function) instead of
literal unit text and ensure seconds are rounded or floored to whole units
before formatting; specifically, change formatDurationSeconds to call the
translation function for second/minute/hour labels (using appropriate
pluralization keys) and format numeric values as integers (no raw fractional
seconds) so all user-visible labels come from translations.

In `@services/platform/app/features/automations/metrics/metrics-page.tsx`:
- Around line 54-56: The page heading is rendered with <Text as="h3" ...> which
violates the route-level accessibility guideline requiring exactly one h1;
change the heading component to render as an h1 (e.g., update the <Text> usage
in metrics-page.tsx to use as="h1" or replace with an h1 element) for the
primary title produced by t('metrics.title') so the page has a single, top-level
<h1> and preserve existing styling/variant="label" and className="text-lg
font-semibold".
- Around line 79-103: Don't render synthetic zero metrics while the query is
pending: update the metrics region that contains MetricsSummaryCards,
ExecutionTrendChart, StatusBreakdown and TopWorkflowsTable to check isLoading
(or presence of summary/series/topWorkflows) and when loading render a loading
state/skeleton and set aria-busy="true" on the container; once data is resolved
pass real values (or omit props) instead of using the ?? 0 fallbacks (e.g., stop
passing summary?.total ?? 0, summary?.successRate ?? 0, etc.) so consumers like
MetricsSummaryCards and StatusBreakdown receive undefined/empty and can render
appropriately after load.

In `@services/platform/app/features/automations/metrics/status-breakdown.tsx`:
- Around line 55-60: The tooltip inline style in the contentStyle object (used
in the StatusBreakdown component) currently references raw CSS channel variables
for border and background; update contentStyle so the border and background use
valid color expressions by wrapping the channel variables with hsl(...), e.g.
border: `1px solid hsl(var(--border))` and background: `hsl(var(--popover))`,
leaving fontSize and borderRadius unchanged so the tooltip picks up themed
colors correctly.
- Around line 27-37: The chart slice colors currently use undefined CSS vars
(--color-chart-success/failure/neutral) so they always fall back; update the
color properties in the status breakdown slice objects (the objects that set
name: t('metrics.chart.success'|'failed'|'running'), value:
success|failed|running) to reference the actual design tokens used across the
app (replace the undefined vars with the project's canonical theme tokens for
success, failure, and neutral — e.g. the defined CSS variables or token names
your theme exposes such as
--color-success-500/--color-danger-500/--color-gray-500 or the equivalent token
helpers) so the slices follow the theme consistently.

In `@services/platform/app/features/automations/metrics/top-workflows-table.tsx`:
- Around line 48-57: Top workflows contains rows where
TopWorkflowRow.workflowSlug can be null but handleRowClick is globally attached,
producing dead clicks; either filter out slugless rows before passing data to
the table or make those rows explicitly non-interactive: remove entries with
workflowSlug == null from the data source used by this component (or in the
function that computes rows) or, if you must keep them, update the row render to
skip adding onClick/cursor styles for rows where workflowSlug is null and ensure
handleRowClick early-return logic still references workflowSlug and
slugToUrlParam when invoking navigate so only slugs trigger navigation; also
apply the same fix to the other table block referenced (the block using the same
pattern around handleRowClick usage).
- Around line 93-97: The success-rate cell currently uses
row.original.successRate.toFixed(1) which hardcodes the decimal separator;
replace that with the app's locale-aware number formatter (the existing pattern
used in the "failed" column) — call the shared formatNumber/number formatter (or
the hook used there) to format successRate with one decimal and append '%' so
locales like de/fr show "99,5%". For the "last-run" cell that calls
formatDistanceToNow(), obtain the locale/formatter from useFormatDate() (or the
same hook you use elsewhere) and pass that locale into formatDistanceToNow(...)
so the relative time string is localized; reference row.original.lastRun and
ensure you mirror the "failed" column's localization pattern for both changes.

In `@services/platform/convex/workflows/executions/get_org_workflow_metrics.ts`:
- Around line 118-136: The bucketKey construction collapses different identifier
namespaces; change the logic that builds bucketKey (variable bucketKey) to
namespace values so IDs and slugs cannot collide (e.g., prefix wfDefinitionId
with something like "id:" and workflowSlug with "slug:", and use a distinct
"unknown" namespace such as "unknown:unknown" when neither exists). Update the
creation of the bucket object (where wfDefinitionId and workflowSlug are set and
buckets.set(bucketKey, bucket) is called) to continue using the same namespacing
scheme so lookups on the buckets Map remain consistent and unique per identifier
namespace.
- Around line 76-93: The rolling windowStart (now - args.periodDays * DAY_MS) is
misaligned with the UTC-day buckets seeded into seriesMap; replace windowStart
with the UTC-aligned start used by the series: compute windowStart = todayStart
- (args.periodDays - 1) * DAY_MS (using todayStart = utcDayStart(now)) so the
summary/topWorkflows query covers exactly the same day buckets as seriesMap
(update the windowStart variable where it’s declared and remove the old
now-based subtraction).

In `@services/platform/messages/fr.json`:
- Line 1825: Replace the English table title value "Top workflows" in
services/platform/messages/fr.json (the JSON entry with "title": "Top
workflows") with a French translation; update the "title" value to something
like "Principaux flux de travail" so the French locale shows a fully translated
label.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 23f6717b-c475-475b-9293-7959b715bcfc

📥 Commits

Reviewing files that changed from the base of the PR and between e5aacf5 and 033b7d7.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • services/platform/convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (15)
  • services/platform/app/features/automations/components/automations-table.tsx
  • services/platform/app/features/automations/metrics/execution-trend-chart.tsx
  • services/platform/app/features/automations/metrics/format-duration.ts
  • services/platform/app/features/automations/metrics/metrics-page.tsx
  • services/platform/app/features/automations/metrics/metrics-summary-cards.tsx
  • services/platform/app/features/automations/metrics/status-breakdown.tsx
  • services/platform/app/features/automations/metrics/top-workflows-table.tsx
  • services/platform/app/routeTree.gen.ts
  • services/platform/app/routes/dashboard/$id/automations/metrics.tsx
  • services/platform/convex/wf_executions/queries.ts
  • services/platform/convex/workflows/executions/get_org_workflow_metrics.ts
  • services/platform/messages/de.json
  • services/platform/messages/en.json
  • services/platform/messages/fr.json
  • services/platform/package.json

Comment on lines +27 to +30
function shortLabel(dateKey: string): string {
const [, month, day] = dateKey.split('-');
return `${month}-${day}`;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Replace manual date slicing with shared date formatting utilities.

Manual MM-DD string construction is not locale-aware and bypasses the project’s date formatting utilities.

📅 Suggested fix
+import { formatDate } from '@/lib/utils/date/format';
...
 function shortLabel(dateKey: string): string {
-  const [, month, day] = dateKey.split('-');
-  return `${month}-${day}`;
+  return formatDate(new Date(`${dateKey}T00:00:00Z`), {
+    month: '2-digit',
+    day: '2-digit',
+  });
 }
As per coding guidelines: `DO NOT use toLocaleDateString(), toLocaleTimeString(), or toLocaleString(). Use useFormatDate() hook (React) or formatDate() from lib/utils/date/format instead`.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function shortLabel(dateKey: string): string {
const [, month, day] = dateKey.split('-');
return `${month}-${day}`;
}
import { formatDate } from '@/lib/utils/date/format';
function shortLabel(dateKey: string): string {
return formatDate(new Date(`${dateKey}T00:00:00Z`), {
month: '2-digit',
day: '2-digit',
});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/app/features/automations/metrics/execution-trend-chart.tsx`
around lines 27 - 30, The shortLabel function currently slices the dateKey
string manually (function shortLabel) which is not locale-aware; instead parse
dateKey into a Date and format it via the project's shared utilities—either call
the React hook useFormatDate() inside the component that renders shortLabel or
import formatDate from lib/utils/date/format and return formatDate(parsedDate, {
pattern: 'MM-dd' } or the equivalent option your util accepts); replace the
manual split logic with that call so formatting uses the shared, locale-aware
formatter.

Comment on lines +1 to +11
export function formatDurationSeconds(seconds: number): string {
if (!seconds || seconds <= 0) return '0s';
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
const remainder = seconds % 60;
if (minutes < 60) {
return remainder > 0 ? `${minutes}m ${remainder}s` : `${minutes}m`;
}
const hours = Math.floor(minutes / 60);
const remainderMinutes = minutes % 60;
return remainderMinutes > 0 ? `${hours}h ${remainderMinutes}m` : `${hours}h`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Localize duration unit labels instead of hardcoded unit text.

The formatter returns user-visible strings with hardcoded s/m/h, which will bypass locale-specific wording. It also renders fractional seconds directly.

Proposed fix
+interface DurationUnitLabels {
+  second: string;
+  minute: string;
+  hour: string;
+}
+
+const defaultDurationUnitLabels: DurationUnitLabels = {
+  second: 's',
+  minute: 'm',
+  hour: 'h',
+};
+
-export function formatDurationSeconds(seconds: number): string {
-  if (!seconds || seconds <= 0) return '0s';
-  if (seconds < 60) return `${seconds}s`;
-  const minutes = Math.floor(seconds / 60);
-  const remainder = seconds % 60;
+export function formatDurationSeconds(
+  seconds: number,
+  labels: DurationUnitLabels = defaultDurationUnitLabels,
+): string {
+  if (!Number.isFinite(seconds) || seconds <= 0) return `0${labels.second}`;
+  const totalSeconds = Math.floor(seconds);
+  if (totalSeconds < 60) return `${totalSeconds}${labels.second}`;
+  const minutes = Math.floor(totalSeconds / 60);
+  const remainder = totalSeconds % 60;
   if (minutes < 60) {
-    return remainder > 0 ? `${minutes}m ${remainder}s` : `${minutes}m`;
+    return remainder > 0
+      ? `${minutes}${labels.minute} ${remainder}${labels.second}`
+      : `${minutes}${labels.minute}`;
   }
   const hours = Math.floor(minutes / 60);
   const remainderMinutes = minutes % 60;
-  return remainderMinutes > 0 ? `${hours}h ${remainderMinutes}m` : `${hours}h`;
+  return remainderMinutes > 0
+    ? `${hours}${labels.hour} ${remainderMinutes}${labels.minute}`
+    : `${hours}${labels.hour}`;
 }

As per coding guidelines, "Do NOT hardcode text, use the translation hooks/functions instead for user-facing UI".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function formatDurationSeconds(seconds: number): string {
if (!seconds || seconds <= 0) return '0s';
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
const remainder = seconds % 60;
if (minutes < 60) {
return remainder > 0 ? `${minutes}m ${remainder}s` : `${minutes}m`;
}
const hours = Math.floor(minutes / 60);
const remainderMinutes = minutes % 60;
return remainderMinutes > 0 ? `${hours}h ${remainderMinutes}m` : `${hours}h`;
interface DurationUnitLabels {
second: string;
minute: string;
hour: string;
}
const defaultDurationUnitLabels: DurationUnitLabels = {
second: 's',
minute: 'm',
hour: 'h',
};
export function formatDurationSeconds(
seconds: number,
labels: DurationUnitLabels = defaultDurationUnitLabels,
): string {
if (!Number.isFinite(seconds) || seconds <= 0) return `0${labels.second}`;
const totalSeconds = Math.floor(seconds);
if (totalSeconds < 60) return `${totalSeconds}${labels.second}`;
const minutes = Math.floor(totalSeconds / 60);
const remainder = totalSeconds % 60;
if (minutes < 60) {
return remainder > 0
? `${minutes}${labels.minute} ${remainder}${labels.second}`
: `${minutes}${labels.minute}`;
}
const hours = Math.floor(minutes / 60);
const remainderMinutes = minutes % 60;
return remainderMinutes > 0
? `${hours}${labels.hour} ${remainderMinutes}${labels.minute}`
: `${hours}${labels.hour}`;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/app/features/automations/metrics/format-duration.ts` around
lines 1 - 11, formatDurationSeconds currently emits hardcoded unit labels ("s",
"m", "h") and may print fractional seconds; update it to use the app's
translation utilities (e.g., the i18n/t hook or translation function) instead of
literal unit text and ensure seconds are rounded or floored to whole units
before formatting; specifically, change formatDurationSeconds to call the
translation function for second/minute/hour labels (using appropriate
pluralization keys) and format numeric values as integers (no raw fractional
seconds) so all user-visible labels come from translations.

Comment on lines +54 to +56
<Text as="h3" variant="label" className="text-lg font-semibold">
{t('metrics.title')}
</Text>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Promote the page title to an h1 for route-level accessibility.

This is the primary heading for the page content but is rendered as h3, which breaks expected heading structure for page navigation.

🧭 Suggested fix
-          <Text as="h3" variant="label" className="text-lg font-semibold">
+          <Text as="h1" variant="label" className="text-lg font-semibold">
             {t('metrics.title')}
           </Text>
As per coding guidelines: `**/app/**/*.{ts,tsx}`: `ENSURE each page has exactly one h1`.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Text as="h3" variant="label" className="text-lg font-semibold">
{t('metrics.title')}
</Text>
<Text as="h1" variant="label" className="text-lg font-semibold">
{t('metrics.title')}
</Text>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/app/features/automations/metrics/metrics-page.tsx` around
lines 54 - 56, The page heading is rendered with <Text as="h3" ...> which
violates the route-level accessibility guideline requiring exactly one h1;
change the heading component to render as an h1 (e.g., update the <Text> usage
in metrics-page.tsx to use as="h1" or replace with an h1 element) for the
primary title produced by t('metrics.title') so the page has a single, top-level
<h1> and preserve existing styling/variant="label" and className="text-lg
font-semibold".

Comment on lines +79 to +103
<MetricsSummaryCards
total={summary?.total ?? 0}
successRate={summary?.successRate ?? 0}
avgExecutionTimeSeconds={summary?.avgExecutionTimeSeconds ?? 0}
failed={summary?.failed ?? 0}
/>

<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
<div className="lg:col-span-2">
<ExecutionTrendChart series={series} />
</div>
<div>
<StatusBreakdown
completed={summary?.completed ?? 0}
failed={summary?.failed ?? 0}
running={summary?.running ?? 0}
/>
</div>
</div>

<TopWorkflowsTable
organizationId={organizationId}
rows={topWorkflows}
isLoading={isLoading}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid rendering synthetic zero metrics during loading; expose loading semantics.

Rendering 0 fallbacks while the query is pending can mislead users. Gate metric rendering behind resolved data and mark the region as busy while loading.

As per coding guidelines: USE aria-busy="true" on containers whose content is loading.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/app/features/automations/metrics/metrics-page.tsx` around
lines 79 - 103, Don't render synthetic zero metrics while the query is pending:
update the metrics region that contains MetricsSummaryCards,
ExecutionTrendChart, StatusBreakdown and TopWorkflowsTable to check isLoading
(or presence of summary/series/topWorkflows) and when loading render a loading
state/skeleton and set aria-busy="true" on the container; once data is resolved
pass real values (or omit props) instead of using the ?? 0 fallbacks (e.g., stop
passing summary?.total ?? 0, summary?.successRate ?? 0, etc.) so consumers like
MetricsSummaryCards and StatusBreakdown receive undefined/empty and can render
appropriately after load.

Comment on lines +27 to +37
color: 'var(--color-chart-success, #16a34a)',
},
{
name: t('metrics.chart.failed'),
value: failed,
color: 'var(--color-chart-failure, #dc2626)',
},
{
name: t('metrics.chart.running'),
value: running,
color: 'var(--color-chart-neutral, #3b82f6)',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use defined design tokens for chart slice colors.

--color-chart-success, --color-chart-failure, and --color-chart-neutral are not defined in the global theme, so these values always fall back and won’t follow theme tokens consistently.

🎨 Suggested token-aligned fix
-      color: 'var(--color-chart-success, `#16a34a`)',
+      color: 'hsl(var(--success))',
...
-      color: 'var(--color-chart-failure, `#dc2626`)',
+      color: 'hsl(var(--destructive))',
...
-      color: 'var(--color-chart-neutral, `#3b82f6`)',
+      color: 'hsl(var(--info-foreground))',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
color: 'var(--color-chart-success, #16a34a)',
},
{
name: t('metrics.chart.failed'),
value: failed,
color: 'var(--color-chart-failure, #dc2626)',
},
{
name: t('metrics.chart.running'),
value: running,
color: 'var(--color-chart-neutral, #3b82f6)',
color: 'hsl(var(--success))',
},
{
name: t('metrics.chart.failed'),
value: failed,
color: 'hsl(var(--destructive))',
},
{
name: t('metrics.chart.running'),
value: running,
color: 'hsl(var(--info-foreground))',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/app/features/automations/metrics/status-breakdown.tsx`
around lines 27 - 37, The chart slice colors currently use undefined CSS vars
(--color-chart-success/failure/neutral) so they always fall back; update the
color properties in the status breakdown slice objects (the objects that set
name: t('metrics.chart.success'|'failed'|'running'), value:
success|failed|running) to reference the actual design tokens used across the
app (replace the undefined vars with the project's canonical theme tokens for
success, failure, and neutral — e.g. the defined CSS variables or token names
your theme exposes such as
--color-success-500/--color-danger-500/--color-gray-500 or the equivalent token
helpers) so the slices follow the theme consistently.

Comment on lines +48 to +57
const handleRowClick = useCallback(
(row: Row<TopWorkflowRow>) => {
const slug = row.original.workflowSlug;
if (!slug) return;
void navigate({
to: '/dashboard/$id/automations/$amId/executions',
params: { id: organizationId, amId: slugToUrlParam(slug) },
});
},
[navigate, organizationId],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid dead-click rows when workflowSlug is missing.

TopWorkflowRow allows workflowSlug: null, but the table still wires a global row click and then silently returns for those rows. That leaves visible entries in “Top workflows” that cannot complete the promised click-through to executions. Filter slugless rows out of this table or render them as explicitly non-interactive.

Also applies to: 149-157

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/app/features/automations/metrics/top-workflows-table.tsx`
around lines 48 - 57, Top workflows contains rows where
TopWorkflowRow.workflowSlug can be null but handleRowClick is globally attached,
producing dead clicks; either filter out slugless rows before passing data to
the table or make those rows explicitly non-interactive: remove entries with
workflowSlug == null from the data source used by this component (or in the
function that computes rows) or, if you must keep them, update the row render to
skip adding onClick/cursor styles for rows where workflowSlug is null and ensure
handleRowClick early-return logic still references workflowSlug and
slugToUrlParam when invoking navigate so only slugs trigger navigation; also
apply the same fix to the other table block referenced (the block using the same
pattern around handleRowClick usage).

Comment on lines +93 to +97
cell: ({ row }) => (
<div className="text-right font-mono text-xs">
{row.original.total > 0
? `${row.original.successRate.toFixed(1)}%`
: '—'}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, read the file to see the actual code
cd services/platform && head -n 160 app/features/automations/metrics/top-workflows-table.tsx | tail -n +85

Repository: tale-project/tale

Length of output: 2297


🏁 Script executed:

# Check if locale formatting utilities exist
find . -path "*/lib/utils/date/format*" -o -path "*/lib/utils/formatDate*" | head -20

Repository: tale-project/tale

Length of output: 105


🏁 Script executed:

# Search for useFormatDate hook
rg "useFormatDate" --type ts --type tsx -A 3

Repository: tale-project/tale

Length of output: 88


🏁 Script executed:

# Check formatDistanceToNow imports and usage
rg "formatDistanceToNow" --type ts --type tsx -B 2 -A 2

Repository: tale-project/tale

Length of output: 88


🏁 Script executed:

# Search for locale context
rg "locale|i18n|translation" --type ts --type tsx lib/ | grep -i "context\|provider\|hook" | head -20

Repository: tale-project/tale

Length of output: 88


🏁 Script executed:

cd services/platform && head -n 20 app/features/automations/metrics/top-workflows-table.tsx

Repository: tale-project/tale

Length of output: 794


🏁 Script executed:

cd services/platform && cat lib/utils/date/format.ts

Repository: tale-project/tale

Length of output: 5890


🏁 Script executed:

cd services/platform && rg "formatNumber|formatDistanceToNow" --type-add ts:*.ts --type-add tsx:*.tsx -B 2 -A 2 lib/

Repository: tale-project/tale

Length of output: 1198


🏁 Script executed:

cd services/platform && rg "useLocale|useI18n|useTranslation" --type-add ts:*.ts --type-add tsx:*.tsx lib/ | head -15

Repository: tale-project/tale

Length of output: 312


🏁 Script executed:

cd services/platform && grep -n "useLocale\|useI18n" app/features/automations/metrics/top-workflows-table.tsx

Repository: tale-project/tale

Length of output: 43


🏁 Script executed:

cd services/platform && head -n 30 lib/utils/format/number.ts

Repository: tale-project/tale

Length of output: 905


🏁 Script executed:

cd services/platform && rg "useLocale" app/ --type-add ts:*.ts --type-add tsx:*.tsx -A 5 -B 2 | head -40

Repository: tale-project/tale

Length of output: 2886


🏁 Script executed:

cd services/platform && rg "formatDate.*locale" app/ --type-add ts:*.ts --type-add tsx:*.tsx -B 2 -A 2 | head -30

Repository: tale-project/tale

Length of output: 1996


Localize the numeric and date cell values to match the app's locale.

The success rate cell at line 95 uses toFixed(1), which hardcodes . as the decimal separator. After switching to de/fr locale, this will still render 99.5% instead of the locale-appropriate 99,5%. Similarly, the last-run cell at line 131 calls formatDistanceToNow() without passing a locale, so it will stay in English regardless of the active locale—inconsistent with the translated headers.

Use the useFormatDate() hook (which provides locale-aware formatting) or formatNumber() with locale for the success rate. The pattern already exists in this file for the "failed" column.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/app/features/automations/metrics/top-workflows-table.tsx`
around lines 93 - 97, The success-rate cell currently uses
row.original.successRate.toFixed(1) which hardcodes the decimal separator;
replace that with the app's locale-aware number formatter (the existing pattern
used in the "failed" column) — call the shared formatNumber/number formatter (or
the hook used there) to format successRate with one decimal and append '%' so
locales like de/fr show "99,5%". For the "last-run" cell that calls
formatDistanceToNow(), obtain the locale/formatter from useFormatDate() (or the
same hook you use elsewhere) and pass that locale into formatDistanceToNow(...)
so the relative time string is localized; reference row.original.lastRun and
ensure you mirror the "failed" column's localization pattern for both changes.

Comment on lines +76 to +93
const now = Date.now();
const windowStart = now - args.periodDays * DAY_MS;

let total = 0;
let completed = 0;
let failed = 0;
let running = 0;
let durationSumMs = 0;
let durationCount = 0;
let lastExecution: number | null = null;

const seriesMap = new Map<string, OrgWorkflowSeriesPoint>();
const todayStart = utcDayStart(now);
for (let i = args.periodDays - 1; i >= 0; i--) {
const dayTs = todayStart - i * DAY_MS;
const key = utcDateKey(dayTs);
seriesMap.set(key, { dateKey: key, completed: 0, failed: 0, running: 0 });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Align windowStart with the seeded UTC day buckets.

windowStart is a rolling now - periodDays * DAY_MS, but seriesMap is seeded from UTC midnight. Midday requests will therefore count executions from the partial extra day before the first bucket into summary/topWorkflows while omitting them from series, so the chart no longer matches the totals.

Proposed fix
 export async function getOrgWorkflowMetrics(
   ctx: QueryCtx,
   args: GetOrgWorkflowMetricsArgs,
 ): Promise<OrgWorkflowMetrics> {
   const now = Date.now();
-  const windowStart = now - args.periodDays * DAY_MS;
+  const todayStart = utcDayStart(now);
+  const windowStart = todayStart - (args.periodDays - 1) * DAY_MS;
@@
   const seriesMap = new Map<string, OrgWorkflowSeriesPoint>();
-  const todayStart = utcDayStart(now);
   for (let i = args.periodDays - 1; i >= 0; i--) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const now = Date.now();
const windowStart = now - args.periodDays * DAY_MS;
let total = 0;
let completed = 0;
let failed = 0;
let running = 0;
let durationSumMs = 0;
let durationCount = 0;
let lastExecution: number | null = null;
const seriesMap = new Map<string, OrgWorkflowSeriesPoint>();
const todayStart = utcDayStart(now);
for (let i = args.periodDays - 1; i >= 0; i--) {
const dayTs = todayStart - i * DAY_MS;
const key = utcDateKey(dayTs);
seriesMap.set(key, { dateKey: key, completed: 0, failed: 0, running: 0 });
}
const now = Date.now();
const todayStart = utcDayStart(now);
const windowStart = todayStart - (args.periodDays - 1) * DAY_MS;
let total = 0;
let completed = 0;
let failed = 0;
let running = 0;
let durationSumMs = 0;
let durationCount = 0;
let lastExecution: number | null = null;
const seriesMap = new Map<string, OrgWorkflowSeriesPoint>();
for (let i = args.periodDays - 1; i >= 0; i--) {
const dayTs = todayStart - i * DAY_MS;
const key = utcDateKey(dayTs);
seriesMap.set(key, { dateKey: key, completed: 0, failed: 0, running: 0 });
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/workflows/executions/get_org_workflow_metrics.ts`
around lines 76 - 93, The rolling windowStart (now - args.periodDays * DAY_MS)
is misaligned with the UTC-day buckets seeded into seriesMap; replace
windowStart with the UTC-aligned start used by the series: compute windowStart =
todayStart - (args.periodDays - 1) * DAY_MS (using todayStart =
utcDayStart(now)) so the summary/topWorkflows query covers exactly the same day
buckets as seriesMap (update the windowStart variable where it’s declared and
remove the old now-based subtraction).

Comment on lines +118 to +136
const bucketKey =
(typeof e.wfDefinitionId === 'string' ? e.wfDefinitionId : null) ??
e.workflowSlug ??
'unknown';
let bucket = buckets.get(bucketKey);
if (!bucket) {
bucket = {
wfDefinitionId:
typeof e.wfDefinitionId === 'string' ? e.wfDefinitionId : null,
workflowSlug: e.workflowSlug ?? null,
total: 0,
completed: 0,
failed: 0,
durationSumMs: 0,
durationCount: 0,
lastExecution: 0,
};
buckets.set(bucketKey, bucket);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use namespaced bucket keys for aggregation.

wfDefinitionId ?? workflowSlug ?? 'unknown' collapses different identifier namespaces into the same map key. A workflow slug with the same text as an ID, or a real slug equal to 'unknown', will be merged into the wrong bucket and skew the top-workflows table.

Proposed fix
-    const bucketKey =
-      (typeof e.wfDefinitionId === 'string' ? e.wfDefinitionId : null) ??
-      e.workflowSlug ??
-      'unknown';
+    const wfDefinitionId =
+      typeof e.wfDefinitionId === 'string' ? e.wfDefinitionId : null;
+    const workflowSlug = e.workflowSlug ?? null;
+    const bucketKey =
+      wfDefinitionId !== null
+        ? `id:${wfDefinitionId}`
+        : workflowSlug !== null
+          ? `slug:${workflowSlug}`
+          : 'unknown';
     let bucket = buckets.get(bucketKey);
     if (!bucket) {
       bucket = {
-        wfDefinitionId:
-          typeof e.wfDefinitionId === 'string' ? e.wfDefinitionId : null,
-        workflowSlug: e.workflowSlug ?? null,
+        wfDefinitionId,
+        workflowSlug,
         total: 0,
         completed: 0,
         failed: 0,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const bucketKey =
(typeof e.wfDefinitionId === 'string' ? e.wfDefinitionId : null) ??
e.workflowSlug ??
'unknown';
let bucket = buckets.get(bucketKey);
if (!bucket) {
bucket = {
wfDefinitionId:
typeof e.wfDefinitionId === 'string' ? e.wfDefinitionId : null,
workflowSlug: e.workflowSlug ?? null,
total: 0,
completed: 0,
failed: 0,
durationSumMs: 0,
durationCount: 0,
lastExecution: 0,
};
buckets.set(bucketKey, bucket);
}
const wfDefinitionId =
typeof e.wfDefinitionId === 'string' ? e.wfDefinitionId : null;
const workflowSlug = e.workflowSlug ?? null;
const bucketKey =
wfDefinitionId !== null
? `id:${wfDefinitionId}`
: workflowSlug !== null
? `slug:${workflowSlug}`
: 'unknown';
let bucket = buckets.get(bucketKey);
if (!bucket) {
bucket = {
wfDefinitionId,
workflowSlug,
total: 0,
completed: 0,
failed: 0,
durationSumMs: 0,
durationCount: 0,
lastExecution: 0,
};
buckets.set(bucketKey, bucket);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/workflows/executions/get_org_workflow_metrics.ts`
around lines 118 - 136, The bucketKey construction collapses different
identifier namespaces; change the logic that builds bucketKey (variable
bucketKey) to namespace values so IDs and slugs cannot collide (e.g., prefix
wfDefinitionId with something like "id:" and workflowSlug with "slug:", and use
a distinct "unknown" namespace such as "unknown:unknown" when neither exists).
Update the creation of the bucket object (where wfDefinitionId and workflowSlug
are set and buckets.set(bucketKey, bucket) is called) to continue using the same
namespacing scheme so lookups on the buckets Map remain consistent and unique
per identifier namespace.

"running": "En cours"
},
"table": {
"title": "Top workflows",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Translate the remaining English table title in French locale.

"Top workflows" is still English in fr.json, so French users will see a mixed-language UI label.

🌐 Suggested translation fix
-        "title": "Top workflows",
+        "title": "Principaux workflows",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"title": "Top workflows",
"title": "Principaux workflows",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/messages/fr.json` at line 1825, Replace the English table
title value "Top workflows" in services/platform/messages/fr.json (the JSON
entry with "title": "Top workflows") with a French translation; update the
"title" value to something like "Principaux flux de travail" so the French
locale shows a fully translated label.

@larryro larryro merged commit 2e08e05 into main Apr 19, 2026
25 of 26 checks passed
@larryro larryro deleted the feat/workflow-metrics branch April 19, 2026 06:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant