Skip to content

feat: Feature Analytics label grouping (#6067)#7215

Open
talissoncosta wants to merge 24 commits intomainfrom
feat/feature-analytics-6067
Open

feat: Feature Analytics label grouping (#6067)#7215
talissoncosta wants to merge 24 commits intomainfrom
feat/feature-analytics-6067

Conversation

@talissoncosta
Copy link
Copy Markdown
Contributor

@talissoncosta talissoncosta commented Apr 10, 2026

  • I have read the Contributing Guide.
  • I have filled in the "Changes" section below.
  • I have filled in the "How did you test this code" section below.

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_charts feature 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

Component Purpose
BarChart Reusable stacked bar chart wrapping recharts. Supports seriesLabels, showLegend, barSize, verticalGrid.
ChartTooltip Typed tooltip with per-series breakdown + display-name mapping.
ColorSwatch Colour indicator (sm/md/lg).
buildChartColorMap Pure helper → Record<string, string> from the chart palette.
useEnvChartProps Env chart config hook (tag-colour matching, env-name labels, stable sort). Reusable — see #7223.

Flat token constants

Generator emits 74 camelCase constants (colorChart1, colorTextSecondary, radiusMd, etc.) as var(--token, #hex) strings. Browsers resolve var() in SVG props natively — no hook, no DOM read, theme toggle reactive via CSS cascade.

UX

  • SDK filter always visible (disabled when no labels) — no layout shift when toggling envs
  • Empty state uses the EmptyState component — no inline styles
  • Env-mode colours match tag chips via Utils.getTagColour; tooltip shows env names not IDs

Storybook

  • Components/BarChart — 3 stories (WithLabelledBuckets, WithoutLabels, SingleSeries) with deterministic fake data (pinned reference date + hash-based variance) so Chromatic snapshots stay stable across runs

Follow-ups

How did you test this code?

  • Storybook: BarChart — 3 stories, light + dark
  • App: Usage tab — env colours match chips, labelled data stacks per-SDK (flag on), filter disables/enables cleanly
  • Real data: validated on Vercel preview with prod data (post InfluxDB downsample fix)
  • Automated: 20 unit tests, 0 lint errors, 0 new type errors, 11 E2E passing, 16 visual regression screenshots

🤖 Generated with Claude Code

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
flagsmith-frontend-preview Ready Ready Preview, Comment Apr 15, 2026 4:20pm
flagsmith-frontend-staging Ready Ready Preview, Comment Apr 15, 2026 4:20pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Ignored Ignored Preview Apr 15, 2026 4:20pm

Request Review

@github-actions github-actions bot added front-end Issue related to the React Front End Dashboard feature New feature or request labels Apr 10, 2026
@github-actions github-actions bot added feature New feature or request and removed feature New feature or request labels Apr 10, 2026
@github-actions github-actions bot added feature New feature or request and removed feature New feature or request labels Apr 10, 2026
@github-actions github-actions bot added feature New feature or request and removed feature New feature or request labels Apr 10, 2026
@github-actions github-actions bot added feature New feature or request and removed feature New feature or request labels Apr 10, 2026
@github-actions github-actions bot added feature New feature or request and removed feature New feature or request labels Apr 10, 2026
@talissoncosta talissoncosta marked this pull request as ready for review April 10, 2026 18:49
@talissoncosta talissoncosta requested a review from a team as a code owner April 10, 2026 18:49
@talissoncosta talissoncosta requested review from kyle-ssg and removed request for a team April 10, 2026 18:49
Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

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

⚠️ Code review skipped — your organization's overage spend limit has been reached.

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.

talissoncosta and others added 23 commits April 15, 2026 09:09
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>
Copy link
Copy Markdown
Contributor

@Zaimwa9 Zaimwa9 left a comment

Choose a reason for hiding this comment

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

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

Comment on lines +106 to +114
<MultiSelect
className='w-100'
label='Filter by SDK'
options={labelOptions}
selectedValues={selectedLabels}
onSelectionChange={setSelectedLabels}
colorMap={labelColorMap}
disabled={!isLabelled || labelValues.length <= 1}
/>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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>
@talissoncosta
Copy link
Copy Markdown
Contributor Author

talissoncosta commented Apr 15, 2026

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request front-end Issue related to the React Front End Dashboard

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement dashboard functionality for displaying SDK metrics in Feature Analytics charts

2 participants