Skip to content

frontend: SemVer build metadata breaks version timeline reporting #1588

Description

@nyxsky404

Description

VersionCountTimeline uses a cleaned SemVer string as the Recharts series key, but the timeline data returned by the API remains keyed by the original version string. For valid versions containing build metadata, such as 3510.2.0+test, the generated series key becomes 3510.2.0 and no longer matches the data object.

The result is an empty chart series, a blank instance count, and 0.0% even though the API returned instances for that version.

Verified Upstream State

  • Repository: flatcar/nebraska
  • Branch inspected: main
  • Verified on latest upstream commit: ed76f06d27da9a8bd4fff34b1b0ecd843b5bd2ee
  • Still reproducible on upstream: Yes
  • The upstream branch was fetched immediately before reproduction; this is not caused by stale fork code.

Reproduction Evidence

I rendered the unmodified VersionCountTimeline component in Storybook with a mocked GroupChartsStore returning this valid timeline response:

{
  "2026-08-15T00:00:00Z": {
    "3510.2.0+test": 7
  },
  "2026-08-15T01:00:00Z": {
    "3510.2.0+test": 7
  }
}
Image

Observed output from the real component:

  • API version: 3510.2.0+test
  • API count: 7
  • Displayed version: 3510.2.0
  • Displayed count: blank
  • Displayed percentage: 0.0
  • Chart: axes render, but no data area is drawn

The browser accessibility tree likewise contains a row for 3510.2.0 and 0.0, with no value in the Count cell.

Expected Behavior

The version should retain its identity throughout the reporting pipeline:

  • Version: 3510.2.0+test
  • Count: 7
  • Percentage: 100.0
  • A visible chart series backed by the raw API key

Build metadata variants such as 1.2.3+aws and 1.2.3+azure should not collapse into one series identity.

Actual Behavior

The frontend removes build metadata before creating the chart/table keys. It then looks up the cleaned key in an object that still contains only the raw key. The count becomes undefined, the total becomes NaN, and the percentage falls back to 0.0.

Code Locations and Root Cause

Timeline data preserves the raw API version key

frontend/src/components/Groups/GroupCharts/VersionCountTimeline.tsx:48-55

const versions = groupTimeline[timestamp];
return {
  index: i,
  timestamp: timestamp,
  ...versions,
};

For the fixture above, the resulting data object has a 3510.2.0+test property.

Series discovery rewrites that key

frontend/src/components/Groups/GroupCharts/VersionCountTimeline.tsx:70-90

Object.keys(Object.values(timeline)[0]).forEach(version => {
  const cleanedVersion = cleanSemverVersion(version);
  if (semver.valid(cleanedVersion)) {
    versions.push(cleanedVersion);
  }
});

cleanSemverVersion removes everything following +:

frontend/src/utils/helpers.ts:87-93

export function cleanSemverVersion(version: string) {
  let shortVersion = version;
  if (version.includes('+')) {
    shortVersion = version.split('+')[0];
  }
  return shortVersion;
}

The chart therefore receives 3510.2.0 as its dataKey, while its data still contains only 3510.2.0+test.

The table repeats the mismatched lookup

frontend/src/components/Groups/GroupCharts/VersionCountTimeline.tsx:107-132

for (const version of timelineChartData.keys) {
  const versionCount = entries[version];

  total += versionCount;

  version_breakdown.push({
    version: version,
    instances: versionCount,
    percentage: 0,
  });
}

entries["3510.2.0"] is undefined because the entry is stored under 3510.2.0+test.

The backend explicitly accepts these versions

This is therefore not malformed input being passed to an unsupported frontend path.

Steps to Reproduce Locally

  1. Check out commit ed76f06d27da9a8bd4fff34b1b0ecd843b5bd2ee from flatcar/nebraska.

  2. Save the reproduction harness below as:
    frontend/src/components/Groups/GroupCharts/SemverMetadataRepro.stories.tsx

  3. Install frontend dependencies and start Storybook:

    cd frontend
    npm ci
    npm run storybook -- --host 127.0.0.1
  4. Open:

    http://127.0.0.1:6006/iframe.html?id=groups-semvermetadatarepro--metadata-version-loses-its-count&viewMode=story
    
  5. Compare the mocked payload at the top with the real component output below it.

Minimal Storybook reproduction harness
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { Meta } from '@storybook/react-vite';
import { MemoryRouter } from 'react-router';

import { Group } from '../../../api/apiDataTypes';
import GroupChartsStore from '../../../stores/GroupChartsStore';
import { groupChartStoreContext } from '../../../stores/Stores';
import VersionCountTimeline from './VersionCountTimeline';

export default {
  title: 'groups/SemverMetadataRepro',
  parameters: { layout: 'fullscreen' },
} as Meta;

const versionCountTimeline = {
  '2026-08-15T00:00:00Z': { '3510.2.0+test': 7 },
  '2026-08-15T01:00:00Z': { '3510.2.0+test': 7 },
};

const group = {
  id: 'group-semver-metadata',
  name: 'SemVer metadata reproduction',
  application_id: 'app-semver-metadata',
  channel: {
    id: 'channel-semver-metadata',
    name: 'stable',
    color: '#14b9d6',
    application_id: 'app-semver-metadata',
    package: {
      id: 'package-semver-metadata',
      version: '3510.2.0+test',
      application_id: 'app-semver-metadata',
    },
  },
} as Group;

class SemverMetadataStore extends GroupChartsStore {
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  async getGroupVersionCountTimeline(_appID: string, _groupID: string, _duration: string) {
    return versionCountTimeline;
  }
}

export const MetadataVersionLosesItsCount = {
  render: function SemverMetadataReproduction() {
    const ChartStoreContext = groupChartStoreContext();

    return (
      <ChartStoreContext.Provider value={new SemverMetadataStore()}>
        <MemoryRouter>
          <Box sx={{ bgcolor: 'background.default', minHeight: '100vh', p: 4 }}>
            <Stack spacing={3} sx={{ maxWidth: 1050, mx: 'auto' }}>
              <Box>
                <Typography variant="h4" component="h1" gutterBottom>
                  SemVer build metadata reproduction
                </Typography>
                <Typography color="text.secondary">
                  Unmodified VersionCountTimeline on upstream commit ed76f06d.
                </Typography>
              </Box>

              <Paper variant="outlined" sx={{ p: 2 }}>
                <Typography variant="h6" component="h2" gutterBottom>
                  Mocked API response
                </Typography>
                <Box
                  component="pre"
                  sx={{ bgcolor: '#f4f6f8', m: 0, overflowX: 'auto', p: 2, fontSize: 15 }}
                >
                  {JSON.stringify(versionCountTimeline, null, 2)}
                </Box>
              </Paper>

              <Alert severity="info">
                Expected: version <strong>3510.2.0+test</strong>, count <strong>7</strong>, percentage{' '}
                <strong>100.0%</strong>.
              </Alert>

              <Paper variant="outlined" sx={{ p: 3 }}>
                <Typography variant="h6" component="h2" gutterBottom>
                  Actual output from the real component
                </Typography>
                <VersionCountTimeline
                  group={group}
                  duration={{ displayValue: '1 hour', queryValue: '1h', disabled: false }}
                  isAnimationActive={false}
                />
              </Paper>
            </Stack>
          </Box>
        </MemoryRouter>
      </ChartStoreContext.Provider>
    );
  },
};

The reproduction story passes ESLint and a full Storybook production build on the verified commit.

Duplicate Search

Searches were run across open and closed issues, open and merged PRs, and default-branch commit messages.

Keywords used:

  • build metadata chart
  • semver metadata
  • version metadata chart
  • cleanSemverVersion
  • version +meta
  • metadata version breakdown
  • version chart

No exact duplicate or in-progress fix was found.

Related items reviewed:

  • #1451 / #1452 — version-breakdown percentages use the wrong SQL denominator when bracketed instance IDs are present. This report concerns frontend object-key identity for valid SemVer metadata.
  • #389 — an old Helm chart deployment produced a blank page because of a JSON parsing failure. It is unrelated to version values or Recharts data keys.
  • #1572 / #1573 — timeline components do not refetch when the selected group changes. PR Fix group timeline refetch on group change #1573 changes effect dependencies and cancellation behavior, but does not change raw-version/data-key mapping or SemVer metadata handling.
  • #1195 — multi-step package floors support nonstandard versions, but it does not cover reporting-chart rendering.

Because #1573 modifies VersionCountTimeline.tsx, an implementation for this issue should rebase after it if necessary. The two fixes are materially independent.

Suggested Fix

  1. Preserve the raw API version string as the immutable chart/table identity and Recharts dataKey.

  2. Separate series identity from presentation, for example:

    interface VersionSeries {
      dataKey: string;
      displayLabel: string;
      color: string;
    }
  3. Validate and order versions using semver, but do not replace the object key with a normalized display value.

  4. When SemVer precedence compares equal because only build metadata differs, use the raw string as a deterministic secondary sort key.

  5. Key the color map by the same raw identity used by the timeline data.

  6. Display the full raw version, at least when multiple metadata variants share the same core version, so 1.2.3+aws and 1.2.3+azure remain distinguishable.

  7. Add component regression tests covering:

    • a single build-metadata version;
    • two versions with the same core version and different metadata;
    • a prerelease version;
    • an ordinary Flatcar version without metadata.

Why This Matters

The version timeline is an operational reporting surface. Nebraska accepts and stores valid metadata-bearing versions, but the dashboard can silently turn their nonzero instance counts into blank/zero output. Operators could incorrectly conclude that a version has no instances or that a rollout has no data.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions