feat(platform): add workflow usage & performance metrics dashboard#1574
Conversation
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.
📝 WalkthroughWalkthroughThis pull request introduces a comprehensive metrics dashboard for workflow automations. The changes include a new route at Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (2)
bun.lockis excluded by!**/*.lockservices/platform/convex/_generated/api.d.tsis excluded by!**/_generated/**
📒 Files selected for processing (15)
services/platform/app/features/automations/components/automations-table.tsxservices/platform/app/features/automations/metrics/execution-trend-chart.tsxservices/platform/app/features/automations/metrics/format-duration.tsservices/platform/app/features/automations/metrics/metrics-page.tsxservices/platform/app/features/automations/metrics/metrics-summary-cards.tsxservices/platform/app/features/automations/metrics/status-breakdown.tsxservices/platform/app/features/automations/metrics/top-workflows-table.tsxservices/platform/app/routeTree.gen.tsservices/platform/app/routes/dashboard/$id/automations/metrics.tsxservices/platform/convex/wf_executions/queries.tsservices/platform/convex/workflows/executions/get_org_workflow_metrics.tsservices/platform/messages/de.jsonservices/platform/messages/en.jsonservices/platform/messages/fr.jsonservices/platform/package.json
| function shortLabel(dateKey: string): string { | ||
| const [, month, day] = dateKey.split('-'); | ||
| return `${month}-${day}`; | ||
| } |
There was a problem hiding this comment.
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',
+ });
}📝 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.
| 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.
| 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`; |
There was a problem hiding this comment.
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.
| 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.
| <Text as="h3" variant="label" className="text-lg font-semibold"> | ||
| {t('metrics.title')} | ||
| </Text> |
There was a problem hiding this comment.
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>📝 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.
| <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".
| <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} | ||
| /> |
There was a problem hiding this comment.
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.
| 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)', |
There was a problem hiding this comment.
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.
| 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.
| 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], |
There was a problem hiding this comment.
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).
| cell: ({ row }) => ( | ||
| <div className="text-right font-mono text-xs"> | ||
| {row.original.total > 0 | ||
| ? `${row.original.successRate.toFixed(1)}%` | ||
| : '—'} |
There was a problem hiding this comment.
🧩 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 +85Repository: 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 -20Repository: tale-project/tale
Length of output: 105
🏁 Script executed:
# Search for useFormatDate hook
rg "useFormatDate" --type ts --type tsx -A 3Repository: tale-project/tale
Length of output: 88
🏁 Script executed:
# Check formatDistanceToNow imports and usage
rg "formatDistanceToNow" --type ts --type tsx -B 2 -A 2Repository: 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 -20Repository: tale-project/tale
Length of output: 88
🏁 Script executed:
cd services/platform && head -n 20 app/features/automations/metrics/top-workflows-table.tsxRepository: tale-project/tale
Length of output: 794
🏁 Script executed:
cd services/platform && cat lib/utils/date/format.tsRepository: 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 -15Repository: tale-project/tale
Length of output: 312
🏁 Script executed:
cd services/platform && grep -n "useLocale\|useI18n" app/features/automations/metrics/top-workflows-table.tsxRepository: tale-project/tale
Length of output: 43
🏁 Script executed:
cd services/platform && head -n 30 lib/utils/format/number.tsRepository: 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 -40Repository: 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 -30Repository: 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.
| 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 }); | ||
| } |
There was a problem hiding this comment.
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.
| 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).
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| 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", |
There was a problem hiding this comment.
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.
| "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.
Summary
/dashboard/:id/automations/metricspage 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.?period=. Gated by the same ability check used for the automations list.getOrgWorkflowMetricsRPC aggregateswfExecutionswith a 5,000-row scan cap, pre-seeds daily series buckets so empty days render as zero, and groups per-workflow stats. Addsrechartsfor the charts.Test plan
bunx tsc --noEmitandbunx oxlint --type-awarefromservices/platform/— both clean/dashboard/:id/automations, click View metrics — page renders with 4 stat cards, trend chart, status donut, and top-workflows table?period=updates in URLde/fr— all labels translated, no raw i18n keys leakwriteonwfDefinitions, visit/automations/metrics—<AccessDenied />rendersSummary by CodeRabbit
Release Notes
New Features
Chores