feat: Feature Analytics label grouping (#6067)#7215
feat: Feature Analytics label grouping (#6067)#7215talissoncosta wants to merge 24 commits intomainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.
Once credits are available, reopen this pull request to trigger a review.
8483792 to
45ee267
Compare
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Zero custom CSS — tooltip uses Bootstrap utilities + semantic tokens. ChartTooltip extracted to own file for single responsibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Data utilities extracted to analyticsUtils.ts for testability. Closes #6067 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Chart tokens were sorted alphabetically (1, 10, 2, 3...) instead of numerically (1, 2, 3...10). Use localeCompare with numeric option. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Update featureAnalytics service queryFn to return rawEntries alongside chartData, so FeatureAnalytics doesn't need to call hooks in a loop - Remove useGetEnvironmentAnalyticsQuery calls inside .map() (React rules-of-hooks violation) - Rename analyticsUtils.ts to utils.ts - Add 14 unit tests for hasLabelledData, aggregateByLabels, buildEnvColorMap - Fix natural sort order in token generator Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add useChartColors() and useChartColorMap() hooks (common/hooks/) - Remove getCSSVars/CHART_COLOURS imports from utils.ts — colors are now passed as a parameter from the hook - Remove buildEnvColorMap — replaced by useChartColorMap hook - Update tests to pass mock colors directly (no jest.mock needed) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace getCSSVars/CHART_COLOURS with useChartColorMap hook - Replace inline color/fontSize/margin styles with Bootstrap utilities Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove common/utils/getCSSVar.ts — only had one consumer - Inline resolveColors() into useChartColors hook with JSDoc explaining why we read from DOM (Recharts needs hex strings, not var() refs) - Add comment on rawEntries in service queryFn explaining the dual return (chartData for environments, rawEntries for labels) - Add comment on label priority in utils.ts - Regenerate tokens.ts with updated comment Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Create web/styles/3rdParty/_recharts.scss as proper home for recharts global styles (alongside _react-select.scss, _react-datepicker.scss) - Switch axis tick + line colours to var(--color-text-secondary) via recharts' built-in classNames — dark mode handled automatically, no hex strings threaded through props - Drop hardcoded '#656D7B' from BarChart.tsx (4 axis instances + 1 bar fallback) - Delete dead .xAxis/.yAxis rules from _tooltips.scss (no consumers) - TODO on legacy .recharts-tooltip + .dark block — removes when SingleSDKLabelsChart and OrganisationUsage migrate to <BarChart /> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tColors Generator now emits flat camelCase constants for every semantic token (e.g. colorChart1, colorTextSecondary, radiusMd) as CSS value strings — 'var(--token, #hex-fallback)'. Pass directly to recharts props, inline styles, or anywhere a CSS value is accepted; var() resolves at render and theme toggle updates colours via the CSS cascade. Charts: - BarChart uses colorTextSecondary directly for axis tick + line — no CSS classname plumbing in _recharts.scss - buildChartColorMap pure function (web/components/charts/) replaces useChartColors / useChartColorMap hooks — no DOM read, no theme-toggle staleness bug - _recharts.scss reduced to legacy rules + TODO for the two remaining raw-recharts consumers (SingleSDKLabelsChart, OrganisationUsage) Cleanup: - Delete common/hooks/useChartColors.ts (both hooks) - Delete common/theme/index.ts barrel — codebase convention is direct file imports - Drop nested tokens object + 4 unused type exports from tokens.ts; update the one MDX docs example to use a flat constant Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix inverted pluralisation in empty-state message ("environment"
takes an 's' when there are multiple selected, not the other way
around)
- Type ChartTooltip against recharts' Payload<ValueType, NameType>
instead of `el: any` — use `el.color` (typed) in place of `el.fill`
(untyped), add numericValue helper to keep the reduce typed
- Add direct unit tests for buildChartColorMap (basic mapping,
wraparound, empty array) — previously covered only transitively
via aggregateByLabels tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ull labels
Three bugs caused labelled analytics to look wrong in real data:
1. hasLabelledData was too permissive — passed `{ user_agent: null }`
as "labelled" because the key exists. Every entry then fell through
to the 'Unknown' fallback in aggregation, collapsing all data into
a single series. Tightened to require at least one entry with a
non-empty `user_agent` or `client_application_name` value.
2. chartData from aggregateByLabels had no date ordering guarantee —
dates appeared in whatever order the raw entries landed (visible
in prod as `2026-04-03` rendered AFTER `2026-04-12`). Fixed by
having aggregateByLabels seed its output from a caller-supplied
day axis (chronologically pre-built by useFeatureAnalytics for
the env-path chart).
3. Labelled chart only included days with events, so the x-axis was
sparse — whereas the env path pre-builds all 30 days. Fixed by
reusing the env-path day axis, giving both paths the same complete
date range and the same 'Do MMM' display format.
hasData check updated — labelledChartData now always has one bucket
per day (including empty ones), so `.length > 0` no longer indicates
presence of data. Switched to "any day has a non-zero count for a
label series".
Tests: 20 passing (17 utils + 3 buildChartColorMap), includes new
cases for null-label-values, caller-provided day order, and out-of-
range entries being dropped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The new BarChart introduced two regressions for the env-grouped path:
1. Legend showed raw env IDs (e.g. "22", "1848") because the new
BarChart unconditionally renders recharts' <Legend /> using each
series' dataKey. The env tags at the top already serve as a
colour legend, so a second one was redundant and broken.
- Add `showLegend?: boolean` to BarChart (default false)
- FeatureAnalytics only enables it for the labelled path (where
the filter above the chart doesn't carry colour per series)
- Stories keep `showLegend` on (no filter UI above them)
2. Bar colours didn't match the env tag chip colours. The new code
used `buildChartColorMap(environmentIds)` — which indexes into
our chart palette by selection order. Old prod used
`Utils.getTagColour(indexInProjectEnvList)` — the same function
that colours the env chip, indexed by the env's position in the
project's env list. Restored that behaviour:
- Fetch envs via `useGetEnvironmentsQuery`
- Sort selected envs by their project-list position (stable)
- Build envColorMap with `Color(Utils.getTagColour(idx)).alpha(0.75)`
- Pass sortedEnvIds as the chart series
Labelled path keeps the new `CHART_COLOURS` palette (intentional — SDK
names aren't in the tag-colour palette, and this is a net-new feature).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two remaining Feature Analytics tooltip regressions: 1. Raw dataKeys surfaced in the tooltip — env mode showed "22: 0" / "1848: 0" instead of "Production: 0" / "Staging: 0". Added a `seriesLabels?: Record<string, string>` prop to BarChart, threaded through to ChartTooltip (and recharts' <Legend formatter> when the optional legend is enabled). FeatureAnalytics builds an env-id → env-name map from useGetEnvironmentsQuery and passes it for the env path; labelled path leaves it undefined (dataKeys are already SDK names). 2. Tooltip label text used `text-secondary` which in the theme's tooltip-on-surface context rendered with poor contrast (muted yellow-ish on white). Switched label + value to `text-default` with a semibold weight on the value — keeps the label/value hierarchy while matching the tooltip header's contrast. Incidental type fixes (both introduced earlier this session): - ChartTooltip.formatNumber now accepts recharts' ValueType (was string | number | undefined — ValueType is broader and can be an array) - FeatureAnalytics passes `projectId: Number(projectId)` to useGetEnvironmentsQuery — the Req type expects number, old code was silently typechecking as string against a loose upstream signature The pre-existing `any` cluster in useFeatureAnalytics.ts is unchanged (out of scope — separate follow-up). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…to Record
Three targeted cleanups after auditing the PR for overengineering:
1. Extract useEnvChartProps({ projectId, environmentIds }) — returns
{ series, colorMap, seriesLabels } for env-grouped charts. Bundles
the env-list query, project-position-based sorting, tag-colour
mapping (Utils.getTagColour + alpha), and env-name lookup into one
hook. Real reuse ahead: the planned legacy chart migration
(SingleSDKLabelsChart, OrganisationUsage) needs the same env
colouring to match their tag chips — this hook is the single
source of that logic. FeatureAnalytics.tsx drops ~30 lines of
derivation in the process.
2. Remove BarChart's `stacked` and `height` props. Both defaulted
sensibly (stacked=true, height=400) and no consumer ever overrode
them (grep'd every <BarChart /> callsite, including the legacy
chart migration targets). YAGNI — cheaper to add them back later
than to carry unused API surface now.
3. Switch every `colorMap: Map<string, string>` to
`Record<string, string>` — BarChart, MultiSelect, aggregateByLabels,
buildChartColorMap, plus all tests and stories. Previous mix of
Map (for colours) and Record (for seriesLabels) wasn't principled,
just historical. Record is more idiomatic React, simpler to
inspect in devtools, and callsites are slightly cleaner (`m[k]`
vs `m.get(k)`). Tree-shakeable, no behavioural change.
Labelled-mode derivations (aggregateByLabels call, labelOptions,
filteredLabels) stay inline as component-local useMemos — extracting
them into a second hook would be packaging for packaging's sake, with
no reuse today.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two PR review comments from @Zaimwa9: 1. useFeatureAnalytics queryFn used Array.find() per entry to locate each entry's day bucket in preBuiltData — O(days × entries). Switch to a `day → bucket` Map, same pattern aggregateByLabels uses. O(1) lookups, matters for long periods / high entry counts. 2. Rename single-character callback vars across the PR's touched files: - ChartTooltip: `n`/`v` → `value`, `el` → `entry` - BarChart: `v` → `value` in the tick formatter - useFeatureAnalytics: `v` → `response`, `i` kept (idiomatic index) - useEnvChartProps: `e` → `env`; dedupe the findIndex call into a local helper, rename `a,b` to `idA,idB` for clarity - utils: `d` → `day` in the days.map Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- FeatureAnalytics and useEnvChartProps now accept projectId: number and featureId: number — aligned with Req types in requests.ts and the pattern in FeatureCodeReferencesContainer. Removes the Number(projectId) cast buried inside the hook. - UsageTab (the caller) stops stringifying: passes the raw number directly instead of template-literal wrapping. - aggregateByLabels uses ?? 0 instead of || 0 for accumulation — nullish coalescing is more correct (won't swallow an explicit 0). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
MultiSelect was conditionally rendered — mounting/unmounting as
isLabelled flipped when toggling environments. This caused jarring
layout shift.
- Always render the MultiSelect; disable it when no labels are
available (or only one label value, making filtering pointless)
- Full width (w-100) instead of constrained maxWidth: 400 wrapper
- Clear selectedLabels via useEffect when labelled mode deactivates
so the disabled filter shows an empty placeholder, not stale chips
- Drop the flex-fill + inline style={{ maxWidth: 400 }} wrapper
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the inline-styled empty-state div (style={{ height: 200 }})
with the existing EmptyState component — consistent with the rest of
the codebase (7+ consumers), contextual icon (bar-chart), and zero
inline styles.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
generateFakeData was using `new Date()` for the date range and
`Math.random()` for variance — Chromatic snapshots drifted daily
(date labels shifted) and between runs (random bar heights).
- Pin REFERENCE_DATE to 2026-04-15 instead of `new Date()` — date
axis stays fixed regardless of CI run time
- Replace Math.random() with a tiny deterministic hash of
`${label}-${day}` — same (label, day) always produces the same
variance, so bar heights are stable
- Use UTC throughout so timezone differences between CI runners
don't shift the labels either
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tor PR) The MultiSelect component needs a focused refactor (dark-mode contrast, disabled state, inline styles) that's out of scope for Feature Analytics. Shipping stories of the current state would document known-broken visuals as the Chromatic baseline. Defer the stories — they'll come back alongside the fixes in a dedicated MultiSelect refactor PR. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Zaimwa9
left a comment
There was a problem hiding this comment.
Looks good, sorry for one item I didn't think about last time: the sdk_usage feature flag.
Otherwise it's nice, i do like the useEnv hook. Even though let's create a ticket in order to see how we deal with the label we want to show. As soon as there are labeled data we don't have the breakdown per env. It might be an interesting data for some users (maybe a toggle to switch? I don't have the answer to whether it's valuable and how to do it) but it's worth having an issue tracking this.
Just the flag and ready to approve
| <MultiSelect | ||
| className='w-100' | ||
| label='Filter by SDK' | ||
| options={labelOptions} | ||
| selectedValues={selectedLabels} | ||
| onSelectionChange={setSelectedLabels} | ||
| colorMap={labelColorMap} | ||
| disabled={!isLabelled || labelValues.length <= 1} | ||
| /> |
There was a problem hiding this comment.
My bad, I forgot to let you know that for now it's only activated for Flagsmith team (The organisation one).
If you don't mind, let's wrap this one with the flag sdk_usage_charts and set isLabelled false when flag disabled ?
We'll release very soon!
The SDK-labelled view is behind the same feature flag that controls the organisation-level SDK usage page. When disabled, the chart falls back to env grouping regardless of whether labels are present in the API response. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
No worries @Zaimwa9, thanks for flagging that , flag gating landed in 040e3d1. I had the same concern about the env selector in labelled mode. I considered hiding it entirely since it doesn’t change the UI when labels are stacked, but kept it because it still acts as a silent data filter. Selecting fewer envs narrows the aggregated totals. The tradeoff is that users can’t see which env contributed to each SDK stack. Opened #7245 to track the UX discussion. |
Changes
Closes #6067
Feature Analytics label grouping
When the API returns labelled evaluation buckets (per-SDK via
user_agent/client_application_name), the chart stacks bars by label value with a MultiSelect filter. Falls back to environment-grouped view when no usable labels are present.Gated behind the
sdk_usage_chartsfeature flag — same flag that controls the organisation-level SDK usage view. When disabled, the chart always uses env grouping regardless of the API response.New components
BarChartseriesLabels,showLegend,barSize,verticalGrid.ChartTooltipColorSwatchbuildChartColorMapRecord<string, string>from the chart palette.useEnvChartPropsFlat token constants
Generator emits 74 camelCase constants (
colorChart1,colorTextSecondary,radiusMd, etc.) asvar(--token, #hex)strings. Browsers resolvevar()in SVG props natively — no hook, no DOM read, theme toggle reactive via CSS cascade.UX
EmptyStatecomponent — no inline stylesUtils.getTagColour; tooltip shows env names not IDsStorybook
Components/BarChart— 3 stories (WithLabelledBuckets,WithoutLabels,SingleSeries) with deterministic fake data (pinned reference date + hash-based variance) so Chromatic snapshots stay stable across runsFollow-ups
BarChart, drop legacy_recharts.scssanytypes inuseFeatureAnalytics.tsHow did you test this code?
🤖 Generated with Claude Code