diff --git a/src/components/Insights/InsightsPage/index.tsx b/src/components/Insights/InsightsPage/index.tsx index c865cce6f..1c211c3dd 100644 --- a/src/components/Insights/InsightsPage/index.tsx +++ b/src/components/Insights/InsightsPage/index.tsx @@ -11,9 +11,9 @@ import { ConfigContext } from "../../common/App/ConfigContext"; import { Card } from "../../common/Card"; import { EmptyState } from "../../common/EmptyState"; import { CardsIcon } from "../../common/icons/CardsIcon"; -import { DurationBreakdownInsight } from "../DurationBreakdownInsight"; import { EndpointQueryOptimizationInsight } from "../EndpointQueryOptimizationInsight"; import { actions } from "../actions"; +import { DurationBreakdownInsight } from "../common/insights/DurationBreakdownInsight"; import { DurationInsight } from "../common/insights/DurationInsight"; import { EndpointBottleneckInsight } from "../common/insights/EndpointBottleneckInsight"; import { EndpointNPlusOneInsight } from "../common/insights/EndpointNPlusOneInsight"; diff --git a/src/components/Insights/common/insights/DurationBreakdownInsight/DurationBreakdownInsight.stories.tsx b/src/components/Insights/common/insights/DurationBreakdownInsight/DurationBreakdownInsight.stories.tsx new file mode 100644 index 000000000..25b126d1c --- /dev/null +++ b/src/components/Insights/common/insights/DurationBreakdownInsight/DurationBreakdownInsight.stories.tsx @@ -0,0 +1,366 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { DurationBreakdownInsight } from "."; +import { InsightCategory, InsightScope, InsightType } from "../../../types"; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Insights/common/insights/DurationBreakdownInsight", + component: DurationBreakdownInsight, + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen" + } +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + insight: { + sourceSpanCodeObjectInsight: "sourceSpanCodeObjectInsightId", + id: "60b55792-8262-4c5d-9628-7cce7979ac6d", + firstDetected: "2023-12-05T17:25:47.010Z", + lastDetected: "2024-01-05T13:14:47.010Z", + criticality: 0, + firstCommitId: "b3f7b3f", + lastCommitId: "a1b2c3d", + deactivatedCommitId: null, + reopenCount: 0, + ticketLink: null, + impact: 0, + name: "Span Duration Breakdown", + type: InsightType.SpanDurationBreakdown, + category: InsightCategory.Performance, + specifity: 4, + isRecalculateEnabled: true, + importance: 6, + spanName: "ClientTester.generateInsightData", + spanCodeObjectId: + "span:io.opentelemetry.opentelemetry-instrumentation-annotations-1.16$_$ClientTester.generateInsightData", + breakdownEntries: [ + { + spanName: "GET PetClinic /SampleInsights/ErrorHotspot", + spanDisplayName: "GET PetClinic /SampleInsights/ErrorHotspot", + spanInstrumentationLibrary: "io.opentelemetry.okhttp-3.0", + spanCodeObjectId: + "span:io.opentelemetry.okhttp-3.0$_$GET PetClinic /SampleInsights/ErrorHotspot", + percentiles: [ + { + percentile: 0.5, + duration: { + value: 9.31, + unit: "ms", + raw: 9310166 + } + }, + { + percentile: 0.95, + duration: { + value: 9.31, + unit: "ms", + raw: 9310166 + } + } + ], + codeObjectId: null + }, + { + spanName: "GET PetClinic /SampleInsights/ErrorRecordedOnCurrentSpan", + spanDisplayName: + "GET PetClinic /SampleInsights/ErrorRecordedOnCurrentSpan", + spanInstrumentationLibrary: "io.opentelemetry.okhttp-3.0", + spanCodeObjectId: + "span:io.opentelemetry.okhttp-3.0$_$GET PetClinic /SampleInsights/ErrorRecordedOnCurrentSpan", + percentiles: [ + { + percentile: 0.5, + duration: { + value: 1.85, + unit: "ms", + raw: 1853959 + } + }, + { + percentile: 0.95, + duration: { + value: 1.85, + unit: "ms", + raw: 1853959 + } + } + ], + codeObjectId: null + }, + { + spanName: + "GET PetClinic /SampleInsights/ErrorRecordedOnDeeplyNestedSpan", + spanDisplayName: + "GET PetClinic /SampleInsights/ErrorRecordedOnDeeplyNestedSpan", + spanInstrumentationLibrary: "io.opentelemetry.okhttp-3.0", + spanCodeObjectId: + "span:io.opentelemetry.okhttp-3.0$_$GET PetClinic /SampleInsights/ErrorRecordedOnDeeplyNestedSpan", + percentiles: [ + { + percentile: 0.5, + duration: { + value: 2.98, + unit: "ms", + raw: 2977875 + } + }, + { + percentile: 0.95, + duration: { + value: 2.98, + unit: "ms", + raw: 2977875 + } + } + ], + codeObjectId: null + }, + { + spanName: + "GET PetClinic /SampleInsights/ErrorRecordedOnLocalRootSpan", + spanDisplayName: + "GET PetClinic /SampleInsights/ErrorRecordedOnLocalRootSpan", + spanInstrumentationLibrary: "io.opentelemetry.okhttp-3.0", + spanCodeObjectId: + "span:io.opentelemetry.okhttp-3.0$_$GET PetClinic /SampleInsights/ErrorRecordedOnLocalRootSpan", + percentiles: [ + { + percentile: 0.5, + duration: { + value: 18.64, + unit: "ms", + raw: 18642667 + } + }, + { + percentile: 0.95, + duration: { + value: 18.64, + unit: "ms", + raw: 18642667 + } + } + ], + codeObjectId: null + }, + { + spanName: "GET PetClinic /SampleInsights/HighUsage", + spanDisplayName: "GET PetClinic /SampleInsights/HighUsage", + spanInstrumentationLibrary: "io.opentelemetry.okhttp-3.0", + spanCodeObjectId: + "span:io.opentelemetry.okhttp-3.0$_$GET PetClinic /SampleInsights/HighUsage", + percentiles: [ + { + percentile: 0.5, + duration: { + value: 1.28, + unit: "sec", + raw: 1276017342 + } + }, + { + percentile: 0.95, + duration: { + value: 1.88, + unit: "sec", + raw: 1883717142 + } + } + ], + codeObjectId: null + }, + { + spanName: "GET PetClinic /SampleInsights/NPlusOneWithInternalSpan", + spanDisplayName: + "GET PetClinic /SampleInsights/NPlusOneWithInternalSpan", + spanInstrumentationLibrary: "io.opentelemetry.okhttp-3.0", + spanCodeObjectId: + "span:io.opentelemetry.okhttp-3.0$_$GET PetClinic /SampleInsights/NPlusOneWithInternalSpan", + percentiles: [ + { + percentile: 0.5, + duration: { + value: 8.62, + unit: "ms", + raw: 8622375 + } + }, + { + percentile: 0.95, + duration: { + value: 8.62, + unit: "ms", + raw: 8622375 + } + } + ], + codeObjectId: null + }, + { + spanName: "GET PetClinic /SampleInsights/NPlusOneWithoutInternalSpan", + spanDisplayName: + "GET PetClinic /SampleInsights/NPlusOneWithoutInternalSpan", + spanInstrumentationLibrary: "io.opentelemetry.okhttp-3.0", + spanCodeObjectId: + "span:io.opentelemetry.okhttp-3.0$_$GET PetClinic /SampleInsights/NPlusOneWithoutInternalSpan", + percentiles: [ + { + percentile: 0.5, + duration: { + value: 2.95, + unit: "ms", + raw: 2951917 + } + }, + { + percentile: 0.95, + duration: { + value: 2.95, + unit: "ms", + raw: 2951917 + } + } + ], + codeObjectId: null + }, + { + spanName: "GET PetClinic /SampleInsights/SlowEndpoint", + spanDisplayName: "GET PetClinic /SampleInsights/SlowEndpoint", + spanInstrumentationLibrary: "io.opentelemetry.okhttp-3.0", + spanCodeObjectId: + "span:io.opentelemetry.okhttp-3.0$_$GET PetClinic /SampleInsights/SlowEndpoint", + percentiles: [ + { + percentile: 0.5, + duration: { + value: 5.01, + unit: "sec", + raw: 5011033127 + } + }, + { + percentile: 0.95, + duration: { + value: 5.01, + unit: "sec", + raw: 5011033127 + } + } + ], + codeObjectId: null + }, + { + spanName: "GET PetClinic /SampleInsights/SpanBottleneck", + spanDisplayName: "GET PetClinic /SampleInsights/SpanBottleneck", + spanInstrumentationLibrary: "io.opentelemetry.okhttp-3.0", + spanCodeObjectId: + "span:io.opentelemetry.okhttp-3.0$_$GET PetClinic /SampleInsights/SpanBottleneck", + percentiles: [ + { + percentile: 0.5, + duration: { + value: 520.92, + unit: "ms", + raw: 520920000 + } + }, + { + percentile: 0.95, + duration: { + value: 520.92, + unit: "ms", + raw: 520920000 + } + } + ], + codeObjectId: null + }, + { + spanName: "GET PetClinic /SampleInsights/req-map-get", + spanDisplayName: "GET PetClinic /SampleInsights/req-map-get", + spanInstrumentationLibrary: "io.opentelemetry.okhttp-3.0", + spanCodeObjectId: + "span:io.opentelemetry.okhttp-3.0$_$GET PetClinic /SampleInsights/req-map-get", + percentiles: [ + { + percentile: 0.5, + duration: { + value: 1.56, + unit: "ms", + raw: 1556959 + } + }, + { + percentile: 0.95, + duration: { + value: 1.56, + unit: "ms", + raw: 1556959 + } + } + ], + codeObjectId: null + }, + { + spanName: "HTTP GET ClientTester.generateInsightData", + spanDisplayName: "HTTP GET ClientTester.generateInsightData", + spanInstrumentationLibrary: "io.opentelemetry.okhttp-3.0", + spanCodeObjectId: + "span:io.opentelemetry.okhttp-3.0$_$HTTP GET ClientTester.generateInsightData", + percentiles: [ + { + percentile: 0.5, + duration: { + value: 693.35, + unit: "ms", + raw: 693349003 + } + }, + { + percentile: 0.95, + duration: { + value: 693.35, + unit: "ms", + raw: 693349003 + } + } + ], + codeObjectId: null + } + ], + scope: InsightScope.Span, + spanInfo: { + name: "ClientTester.generateInsightData", + displayName: "ClientTester.generateInsightData", + instrumentationLibrary: + "io.opentelemetry.opentelemetry-instrumentation-annotations-1.16", + spanCodeObjectId: + "span:io.opentelemetry.opentelemetry-instrumentation-annotations-1.16$_$ClientTester.generateInsightData", + methodCodeObjectId: + "method:petclinic.client.ClientTester$_$generateInsightData", + kind: "Internal", + codeObjectId: "petclinic.client.ClientTester$_$generateInsightData" + }, + shortDisplayInfo: { + title: "", + targetDisplayName: "", + subtitle: "", + description: "" + }, + codeObjectId: "petclinic.client.ClientTester$_$generateInsightData", + decorators: null, + environment: "SAMPLE_ENV", + severity: 0, + prefixedCodeObjectId: + "method:petclinic.client.ClientTester$_$generateInsightData", + customStartTime: null, + actualStartTime: "2023-06-16T10:30:31.848Z" + } + } +}; diff --git a/src/components/Insights/common/insights/DurationBreakdownInsight/index.tsx b/src/components/Insights/common/insights/DurationBreakdownInsight/index.tsx new file mode 100644 index 000000000..fc8a15f25 --- /dev/null +++ b/src/components/Insights/common/insights/DurationBreakdownInsight/index.tsx @@ -0,0 +1,132 @@ +import { useState } from "react"; +import { usePagination } from "../../../../../hooks/usePagination"; +import { getDurationString } from "../../../../../utils/getDurationString"; +import { getPercentileLabel } from "../../../../../utils/getPercentileLabel"; +import { Pagination } from "../../../../common/v3/Pagination"; +import { Tag } from "../../../../common/v3/Tag"; +import { Tooltip } from "../../../../common/v3/Tooltip"; +import { DurationPercentile, SpanDurationBreakdownEntry } from "../../../types"; +import { InsightCard } from "../../InsightCard"; +import { ListItem } from "../../InsightCard/ListItem"; +import { PercentileViewModeToggle } from "../../InsightCard/PercentileViewModeToggle"; +import * as s from "./styles"; +import { DurationBreakdownInsightProps } from "./types"; + +const DEFAULT_PERCENTILE = 0.5; +const PAGE_SIZE = 3; + +const getPercentile = ( + entry: SpanDurationBreakdownEntry, + requestedPercentile: number +): DurationPercentile | undefined => { + for (const percentile of entry.percentiles) { + if (percentile.percentile === requestedPercentile) { + return percentile; + } + } +}; + +const getDurationTitle = (breakdownEntry: SpanDurationBreakdownEntry) => { + const sortedPercentiles = breakdownEntry.percentiles.sort( + (a, b) => a.percentile - b.percentile + ); + + let title = "Percentage of time spent in span:"; + + sortedPercentiles.forEach((percentile) => { + title += `\n${getPercentileLabel( + percentile.percentile + )}: ${getDurationString(percentile.duration)}`; + }); + + return {title}; +}; + +export const DurationBreakdownInsight = ( + props: DurationBreakdownInsightProps +) => { + const [percentileViewMode, setPercentileViewMode] = + useState(DEFAULT_PERCENTILE); + + const filteredEntries = props.insight.breakdownEntries.filter((entry) => + entry.percentiles.some( + (percentile) => percentile.percentile === percentileViewMode + ) + ); + + const sortedEntries = [...filteredEntries].sort((a, b) => { + const aPercentile = getPercentile(a, percentileViewMode); + const bPercentile = getPercentile(b, percentileViewMode); + + if (aPercentile && bPercentile) { + return bPercentile.duration.raw - aPercentile.duration.raw; + } + + return 0; + }); + + const [pageItems, page, setPage] = usePagination( + sortedEntries, + PAGE_SIZE, + props.insight.codeObjectId + ); + + const handleSpanLinkClick = (spanCodeObjectId: string) => { + props.onAssetLinkClick(spanCodeObjectId, props.insight.type); + }; + + const handlePercentileViewModeChange = (value: number) => { + setPercentileViewMode(value); + }; + + return ( + + + + + Asset + Duration + + {pageItems.map((entry) => { + const percentile = getPercentile(entry, percentileViewMode); + + const name = entry.spanDisplayName; + const spanCodeObjectId = entry.spanCodeObjectId; + + return percentile ? ( + handleSpanLinkClick(spanCodeObjectId)} + buttons={[ + + + + ]} + /> + ) : null; + })} + + + + } + onRecalculate={props.onRecalculate} + onRefresh={props.onRefresh} + /> + ); +}; diff --git a/src/components/Insights/common/insights/DurationBreakdownInsight/styles.ts b/src/components/Insights/common/insights/DurationBreakdownInsight/styles.ts new file mode 100644 index 000000000..2a89a0f05 --- /dev/null +++ b/src/components/Insights/common/insights/DurationBreakdownInsight/styles.ts @@ -0,0 +1,30 @@ +import styled from "styled-components"; +import { footnoteRegularTypography } from "../../../../common/App/typographies"; + +export const Container = styled.div` + display: flex; + flex-direction: column; + padding-top: 8px; + gap: 8px; +`; + +export const EntryList = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + padding: 4px 8px 0; +`; + +export const EntryListHeader = styled.div` + ${footnoteRegularTypography} + + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 8px; + color: ${({ theme }) => theme.colors.v3.text.secondary}; +`; + +export const DurationTitle = styled.span` + white-space: pre; +`; diff --git a/src/components/Insights/common/insights/DurationBreakdownInsight/types.ts b/src/components/Insights/common/insights/DurationBreakdownInsight/types.ts new file mode 100644 index 000000000..605502249 --- /dev/null +++ b/src/components/Insights/common/insights/DurationBreakdownInsight/types.ts @@ -0,0 +1,13 @@ +import { + InsightProps, + InsightType, + SpanDurationBreakdownInsight +} from "../../../types"; + +export interface DurationBreakdownInsightProps extends InsightProps { + insight: SpanDurationBreakdownInsight; + onAssetLinkClick: ( + spanCodeObjectId: string, + insightType: InsightType + ) => void; +} diff --git a/src/components/Insights/styles.ts b/src/components/Insights/styles.ts index 862155ba3..5993699b4 100644 --- a/src/components/Insights/styles.ts +++ b/src/components/Insights/styles.ts @@ -9,15 +9,7 @@ export const Container = styled.div` gap: 8px; height: 100%; box-sizing: border-box; - background: ${({ theme }) => { - switch (theme.mode) { - case "light": - return "#fbfdff"; - case "dark": - case "dark-jetbrains": - return "#2b2d30"; - } - }}; + background: ${({ theme }) => theme.colors.v3.surface.primary}; position: relative; `; diff --git a/src/components/Navigation/styles.ts b/src/components/Navigation/styles.ts index 4a4aaa899..ad182aff2 100644 --- a/src/components/Navigation/styles.ts +++ b/src/components/Navigation/styles.ts @@ -3,7 +3,7 @@ import styled from "styled-components"; export const Container = styled.div` width: 100%; height: 100%; - background: ${({ theme }) => theme.colors.v3.surface.primary}; + background: ${({ theme }) => theme.colors.v3.surface.secondary}; display: flex; flex-direction: column; gap: 4px; diff --git a/src/containers/Insights/styles.ts b/src/containers/Insights/styles.ts index c8455f5aa..5a50935a9 100644 --- a/src/containers/Insights/styles.ts +++ b/src/containers/Insights/styles.ts @@ -1,15 +1,7 @@ import { createGlobalStyle } from "styled-components"; export const GlobalStyle = createGlobalStyle` - body { - background: ${({ theme }) => { - switch (theme.mode) { - case "light": - return "#fbfdff"; - case "dark": - case "dark-jetbrains": - return "#3d3f41"; - } - }}; - } + /* body { + // TODO: Add global styles + } */ `;