diff --git a/frontend/snapshots.yml b/frontend/snapshots.yml index d0a773147c74..2f2abd82c126 100644 --- a/frontend/snapshots.yml +++ b/frontend/snapshots.yml @@ -6745,13 +6745,17 @@ snapshots: scenes-app-engineering-analytics-delivery-pipeline--without-deploys--light: hash: v1.k794b7964.5eaee687a224ef9cd7d3707391948732cacb11769bdd619f315a101abcb85de1.ymQC-4XF77SeJ0AN5SrtlyYKs8Ve29MBfz4J8FGLWtg scenes-app-engineering-analytics-health--health--dark: - hash: v1.k794b7964.f360c920cb938b520b6be36feb1c1f09e17bf75c9fc1867062c9196d3095648c.p0RcxBr1mTWBq9zZRmCKht04bJMXAB0Gw1gOtEvctL0 + hash: v1.k794b7964.b44984309f95ae7f109b15a9fae848c4b4946590788e71a8e4387710dfb936d6.OoB3EM50krQphJ342Rw0f3OstTYHV2NjyjS7JCFrzuk scenes-app-engineering-analytics-health--health--light: - hash: v1.k794b7964.c8f5e1bd6dd76487d604f6047196430fcdb1089c50b9f39596ca80757c59740e.a-xn5Ef0rSu9fYwDJVgIuLzafToROwBE8jWojJaEOmw + hash: v1.k794b7964.35c7ca733e40d8700368971cab0385c5c0e9299b7e2bdcdc959c4021d1e5dd9a.JY2t4yraYrMoI7dKNjGbOrOZoTLFYIrNqoyj9HTnxWs + scenes-app-engineering-analytics-health--health-with-limited-deployment-coverage--dark: + hash: v1.k794b7964.91d4d18da5f4bf6784bed887fd0a7e2ff10c278fd1c6ae2330911541aaeb9f1e.t7zP7abNW6GN2rtotpz5VGWkzKBbmaSqSqwLuoKbJ1I + scenes-app-engineering-analytics-health--health-with-limited-deployment-coverage--light: + hash: v1.k794b7964.cea50b9a0c73e0868c0b4b59dbc0e0c8301fb8c08fb33bab1d2bfdfaa6c58143.4KlzrYv4MgqmhfXlrKavy5FXTDxsFa1A_T275D4jDIU scenes-app-engineering-analytics-health--health-without-attributed-pull-requests--dark: - hash: v1.k794b7964.ade08d76e0e86e94837e2336d129092631dc8be77dff7c9b0049c6011b482b84.MhzKqJFl7699ZEH8SnnqEgzPTlKZ1ztmWsRR2_L0ZEc + hash: v1.k794b7964.712465012bc894bfbf77066f2b3287bbb28c684d8992bfb6e6824e04a4ff3d0f.AIi4PIwjarrVtkm9Rn9pSyAWRL7IITCCaAywR7a3p9A scenes-app-engineering-analytics-health--health-without-attributed-pull-requests--light: - hash: v1.k794b7964.f2d943eccccec7f01157f2c7dc4806db7ccb408be9f33b854dba0e19db54fab1.Wo_17c5n-muEgIRJEYEARPH8Li6c2H3kS2Ai6CVyaHo + hash: v1.k794b7964.916a98ec25fe104a20e7364b41baad00b75c67456d07a457c96cf31cbefcd330.vm_ef_S4gyq4TggAyQQOsNQ2BUqZyNK1nYiDkdWF8UU scenes-app-engineering-analytics-health--health-without-deploy-data--dark: hash: v1.k794b7964.19d11f17e9a84ed24f41001e49e34211992af3fd06e22d3d8e4956ea54451692.bHLFcWqNX7duZvoYdWlAWdK6Hbf3hNcKuEofZN8Jeg4 scenes-app-engineering-analytics-health--health-without-deploy-data--light: diff --git a/products/engineering_analytics/frontend/components/WindowComparisonCard.tsx b/products/engineering_analytics/frontend/components/WindowComparisonCard.tsx index 7410279375db..a54416fb68f0 100644 --- a/products/engineering_analytics/frontend/components/WindowComparisonCard.tsx +++ b/products/engineering_analytics/frontend/components/WindowComparisonCard.tsx @@ -9,8 +9,17 @@ import { ReactNode } from 'react' import { LemonCard, LemonSkeleton, Tooltip } from '@posthog/lemon-ui' +import { cn } from 'lib/utils/css-classes' + import { percent } from '../lib/format' -import { DeltaBadge, percentChange, pointChange } from './MetricTile' +import { DeltaBadge, percentChange, pointChange, type TileBenchmark } from './MetricTile' + +const BENCHMARK_EDGE_CLASS: Record = { + elite: 'border-l-success', + high: 'border-l-purple', + medium: 'border-l-warning', + low: 'border-l-danger', +} function MagnitudeBar({ fraction, @@ -100,6 +109,7 @@ export function WindowComparisonCard({ deltaUnit, deltaPrecision, tooltip, + benchmark, marker, markerPrevious, markerLabel, @@ -121,6 +131,7 @@ export function WindowComparisonCard({ deltaPrecision?: number /** Definition or methodology, shown on title hover. */ tooltip?: ReactNode + benchmark?: TileBenchmark | null /** A companion figure (e.g. p90) pinned as a tick on each magnitude bar, on the same scale as * the value. Ignored in `share` mode, which has no scale to pin against. */ marker?: number | null @@ -129,14 +140,27 @@ export function WindowComparisonCard({ loading?: boolean emptyText: string }): JSX.Element { - const max = Math.max(value ?? 0, previousValue ?? 0, marker ?? 0, markerPrevious ?? 0) + const max = Math.max(...[value, previousValue, marker, markerPrevious].map((number) => number ?? 0)) const delta = deltaUnit === 'pt' ? pointChange(value, previousValue) : percentChange(value, previousValue) + const tooltipContent = benchmark ? ( +
+ {tooltip} +
+ DORA band: {benchmark.label.toLowerCase()}. {benchmark.tooltip} +
+
+ ) : ( + tooltip + ) return ( - +

- {tooltip ? ( - + {tooltipContent ? ( + {title} ) : ( diff --git a/products/engineering_analytics/frontend/scenes/DoraDeploymentHealth.tsx b/products/engineering_analytics/frontend/scenes/DoraDeploymentHealth.tsx index c3db172ba7cb..5d997525be29 100644 --- a/products/engineering_analytics/frontend/scenes/DoraDeploymentHealth.tsx +++ b/products/engineering_analytics/frontend/scenes/DoraDeploymentHealth.tsx @@ -2,6 +2,7 @@ import { useValues } from 'kea' import { Section } from '../components/Section' import { WindowComparisonCard } from '../components/WindowComparisonCard' +import { changeFailureBenchmark, deploymentFrequencyBenchmark, restoreTimeBenchmark } from '../lib/doraBenchmark' import { compactAgeLabel } from '../lib/format' import { doraLogic } from './doraLogic' @@ -17,6 +18,7 @@ export function DoraDeploymentHealth(): JSX.Element { title="Deployment frequency" tooltip="Successful deployments per day in the selected environment scope. Each regional deployment counts separately." value={dora?.deployments_per_day} + benchmark={deploymentFrequencyBenchmark(dora?.deployments_per_day)} previousValue={dora?.deployments_per_day_prev} formatValue={(value) => `${value.toFixed(1)}/day`} emptyText="No successful deployments in this window." @@ -26,6 +28,7 @@ export function DoraDeploymentHealth(): JSX.Element { title="Failed deployment share" tooltip="Deployments with a failure or error status divided by deployments that reached an outcome. A change failure proxy: successful deploys that broke production are not counted because no incident data is linked." value={dora?.failed_deployment_share} + benchmark={changeFailureBenchmark(dora?.failed_deployment_share)} previousValue={dora?.failed_deployment_share_prev} formatValue={(value) => `${(value * 100).toFixed(1)}%`} deltaUnit="pt" @@ -37,6 +40,7 @@ export function DoraDeploymentHealth(): JSX.Element { title="Failed deploy to next success" tooltip="Median wait from a deployment's first failure status to the next successful deployment in the same environment. A time to restore proxy: recovery without a deploy is invisible, and unrecovered failures are excluded." value={dora?.median_failed_deploy_to_next_success_seconds} + benchmark={restoreTimeBenchmark(dora?.median_failed_deploy_to_next_success_seconds)} previousValue={dora?.median_failed_deploy_to_next_success_seconds_prev} formatValue={compactAgeLabel} goodWhenDown diff --git a/products/engineering_analytics/frontend/scenes/DoraLeadTimeDistributionContent.tsx b/products/engineering_analytics/frontend/scenes/DoraLeadTimeDistributionContent.tsx index 33f88700f728..6da9fe274e11 100644 --- a/products/engineering_analytics/frontend/scenes/DoraLeadTimeDistributionContent.tsx +++ b/products/engineering_analytics/frontend/scenes/DoraLeadTimeDistributionContent.tsx @@ -6,7 +6,6 @@ import { LemonButton, LemonSkeleton } from '@posthog/lemon-ui' import { LeadTimeBoxPlot } from '../components/LeadTimeBoxPlot' import { compactAgeLabel } from '../lib/format' import { doraLogic } from './doraLogic' -import { DoraUnattributedNotice } from './DoraUnattributedNotice' export function DoraLeadTimeDistributionContent(): JSX.Element { const { @@ -94,7 +93,6 @@ export function DoraLeadTimeDistributionContent(): JSX.Element { : 'Show open to merge and merge to deploy'} - ) } diff --git a/products/engineering_analytics/frontend/scenes/DoraLeadTimeDistributions.tsx b/products/engineering_analytics/frontend/scenes/DoraLeadTimeDistributions.tsx index 62fa0641f356..88977117fdfa 100644 --- a/products/engineering_analytics/frontend/scenes/DoraLeadTimeDistributions.tsx +++ b/products/engineering_analytics/frontend/scenes/DoraLeadTimeDistributions.tsx @@ -25,12 +25,7 @@ export function DoraLeadTimeDistributions(): JSX.Element { /> } > -
+
diff --git a/products/engineering_analytics/frontend/scenes/DoraLeadTimeSection.tsx b/products/engineering_analytics/frontend/scenes/DoraLeadTimeSection.tsx index 06036b0fe820..5ebccae2923f 100644 --- a/products/engineering_analytics/frontend/scenes/DoraLeadTimeSection.tsx +++ b/products/engineering_analytics/frontend/scenes/DoraLeadTimeSection.tsx @@ -1,6 +1,6 @@ import { useActions, useValues } from 'kea' -import { LemonSelect } from '@posthog/lemon-ui' +import { LemonBanner, LemonSelect } from '@posthog/lemon-ui' import { ScopePanel } from '../components/ScopePanel' import { DoraLeadTimeDistributions } from './DoraLeadTimeDistributions' @@ -8,7 +8,7 @@ import { DoraLeadTimeSummary } from './DoraLeadTimeSummary' import { doraLogic } from './doraLogic' export function DoraLeadTimeSection(): JSX.Element { - const { dora, doraLoading, githubTeam, githubTeamOptions } = useValues(doraLogic) + const { dora, doraLoading, githubTeam, githubTeamOptions, showUnattributedWarning } = useValues(doraLogic) const { setGithubTeam } = useActions(doraLogic) return ( @@ -27,6 +27,15 @@ export function DoraLeadTimeSection(): JSX.Element { /> } > + {showUnattributedWarning && ( +
+ + More than 10% of PRs merged in this window have no successful deployment attributed in the + selected environments. Lead-time results exclude unmatched PRs. Check the source sync or allow + more time for deployments. + +
+ )} diff --git a/products/engineering_analytics/frontend/scenes/DoraLeadTimeSummary.tsx b/products/engineering_analytics/frontend/scenes/DoraLeadTimeSummary.tsx index eaaaadac8962..69d3191644ea 100644 --- a/products/engineering_analytics/frontend/scenes/DoraLeadTimeSummary.tsx +++ b/products/engineering_analytics/frontend/scenes/DoraLeadTimeSummary.tsx @@ -2,6 +2,7 @@ import { useValues } from 'kea' import { Section } from '../components/Section' import { WindowComparisonCard } from '../components/WindowComparisonCard' +import { leadTimeBenchmark } from '../lib/doraBenchmark' import { compactAgeLabel } from '../lib/format' import { doraLogic } from './doraLogic' @@ -15,6 +16,7 @@ export function DoraLeadTimeSummary(): JSX.Element { title="Open to deploy" tooltip={`Median from a PR's open to the first successful deployment containing it in any selected environment, over ${dora?.deployed_pr_count ?? 0} deployed PRs (bots and drafts excluded). Each PR counts once. The box plots split it into open to merge and merge to deploy.`} value={dora?.median_open_to_deploy_seconds} + benchmark={leadTimeBenchmark(dora?.median_open_to_deploy_seconds)} previousValue={dora?.median_open_to_deploy_seconds_prev} formatValue={compactAgeLabel} goodWhenDown diff --git a/products/engineering_analytics/frontend/scenes/DoraUnattributedNotice.tsx b/products/engineering_analytics/frontend/scenes/DoraUnattributedNotice.tsx deleted file mode 100644 index 55ee7114fb31..000000000000 --- a/products/engineering_analytics/frontend/scenes/DoraUnattributedNotice.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { useValues } from 'kea' - -import { doraLogic } from './doraLogic' - -export function DoraUnattributedNotice(): JSX.Element | null { - const { dora } = useValues(doraLogic) - - if (dora?.unattributed_merged_pr_share == null || dora.unattributed_merged_pr_share <= 0) { - return null - } - - return ( -
- {(dora.unattributed_merged_pr_share * 100).toFixed(1)}% of the {dora.merged_pr_count} PRs merged in this - window have no deploy attributed yet, usually because their deploy hasn't happened or synced. -
- ) -} diff --git a/products/engineering_analytics/frontend/scenes/EngineeringAnalyticsHealth.stories.tsx b/products/engineering_analytics/frontend/scenes/EngineeringAnalyticsHealth.stories.tsx index b1e46451417e..d5f222b515c5 100644 --- a/products/engineering_analytics/frontend/scenes/EngineeringAnalyticsHealth.stories.tsx +++ b/products/engineering_analytics/frontend/scenes/EngineeringAnalyticsHealth.stories.tsx @@ -187,6 +187,20 @@ export const Health: Story = { parameters: { pageUrl: urls.engineeringAnalyticsHealth() }, } +export const HealthWithLimitedDeploymentCoverage: Story = { + ...Health, + decorators: [ + mswDecorator({ + get: { + 'api/projects/:team_id/engineering_analytics/dora/': { + ...DORA, + unattributed_merged_pr_share: 0.25, + } satisfies DoraOverviewApi, + }, + }), + ], +} + export const HealthWithoutAttributedPullRequests: Story = { render: () => , parameters: { @@ -202,6 +216,7 @@ export const HealthWithoutAttributedPullRequests: Story = { ...DORA, deployed_pr_count: 0, deployed_pr_count_prev: 0, + unattributed_merged_pr_share: 1, median_merge_to_deploy_seconds: null, median_merge_to_deploy_seconds_prev: null, median_open_to_deploy_seconds: null, diff --git a/products/engineering_analytics/frontend/scenes/doraLogic.test.ts b/products/engineering_analytics/frontend/scenes/doraLogic.test.ts index 8a6f578bf547..8b5de0d8d090 100644 --- a/products/engineering_analytics/frontend/scenes/doraLogic.test.ts +++ b/products/engineering_analytics/frontend/scenes/doraLogic.test.ts @@ -181,6 +181,20 @@ describe('doraLogic', () => { expect(logic.values.environmentOptions.map(({ key }) => key)).toEqual(['dev', 'prod-us', 'prod-eu']) }) + it.each([ + [null, false], + [0, false], + [0.1, false], + [0.1001, true], + [0.25, true], + [1, true], + ])('warns only above 10%% unattributed merged PRs (%s)', async (share, showWarning) => { + await expectLogic(logic).toDispatchActions(['loadDoraSuccess']) + logic.actions.loadDoraSuccess({ ...DORA, unattributed_merged_pr_share: share }) + + expect(logic.values.showUnattributedWarning).toBe(showWarning) + }) + it('sends the visible selections as repeated parameters and restores production when cleared', async () => { await expectLogic(logic).toDispatchActions(['loadDoraSuccess']) await expectLogic(logic, () => logic.actions.setEnvironments(['prod-eu', 'dev'])).toDispatchActions([ diff --git a/products/engineering_analytics/frontend/scenes/doraLogic.ts b/products/engineering_analytics/frontend/scenes/doraLogic.ts index 1716d8f144a6..7cbc59d548cc 100644 --- a/products/engineering_analytics/frontend/scenes/doraLogic.ts +++ b/products/engineering_analytics/frontend/scenes/doraLogic.ts @@ -76,6 +76,7 @@ export interface doraLogicValues { openToMergeBuckets: BoxPlotBucket[] selectedEnvironments: string[] showAllLeadTimeStages: boolean + showUnattributedWarning: boolean } // Generated by kea-typegen. Update if you're an agent, ignore if you're human. @@ -121,6 +122,7 @@ export interface doraLogicActions { // Generated by kea-typegen. Update if you're an agent, ignore if you're human. export interface doraLogicMeta { __keaTypeGenInternalSelectorTypes: { + showUnattributedWarning: (dora: DoraOverviewApi | null) => boolean selectedEnvironments: (dora: DoraOverviewApi | null, doraLoading: boolean, environments: string[]) => string[] boxPlotBuckets: (dora: DoraOverviewApi | null) => BoxPlotBucket[] openToMergeBuckets: (dora: DoraOverviewApi | null) => BoxPlotBucket[] @@ -244,6 +246,10 @@ export const doraLogic = kea([ })), selectors({ + showUnattributedWarning: [ + (s) => [s.dora], + (dora: DoraOverviewApi | null): boolean => (dora?.unattributed_merged_pr_share ?? 0) > 0.1, + ], selectedEnvironments: [ (s) => [s.dora, s.doraLoading, s.environments], (dora: DoraOverviewApi | null, doraLoading: boolean, environments: string[]): string[] => {