diff --git a/src/components/Insights/common/InsightCard/index.tsx b/src/components/Insights/common/InsightCard/index.tsx index 060671e2f..1a8033724 100644 --- a/src/components/Insights/common/InsightCard/index.tsx +++ b/src/components/Insights/common/InsightCard/index.tsx @@ -138,6 +138,7 @@ export const InsightCard = (props: InsightCardProps) => { onTicketInfoButtonClick={props.onJiraButtonClick} ticketLink={props.jiraTicketInfo?.ticketLink} isHintEnabled={props.jiraTicketInfo?.isHintEnabled} + spanCodeObjectId={props.jiraTicketInfo?.spanCodeObjectId} /> )} {props.onPin && } diff --git a/src/components/Insights/common/InsightCard/types.ts b/src/components/Insights/common/InsightCard/types.ts index 928cb56dc..58eb94a14 100644 --- a/src/components/Insights/common/InsightCard/types.ts +++ b/src/components/Insights/common/InsightCard/types.ts @@ -25,6 +25,7 @@ export interface InsightCardProps { jiraTicketInfo?: { ticketLink?: string | null; isHintEnabled?: boolean; + spanCodeObjectId?: string; }; - onJiraButtonClick?: (event: string) => void; + onJiraButtonClick?: (spanCodeObjectId: string, event: string) => void; } diff --git a/src/components/Insights/common/insights/EndpointNPlusOneInsight/EndpointNPlusOneInsight.stories.tsx b/src/components/Insights/common/insights/EndpointNPlusOneInsight/EndpointNPlusOneInsight.stories.tsx new file mode 100644 index 000000000..201adf1b1 --- /dev/null +++ b/src/components/Insights/common/insights/EndpointNPlusOneInsight/EndpointNPlusOneInsight.stories.tsx @@ -0,0 +1,23 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { EndpointNPlusOneInsight } from "."; +import { mockedEndpointNPlusOneInsight } from "./mockData"; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Insights/common/insights/EndpointNPlusOneInsight", + component: EndpointNPlusOneInsight, + 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: mockedEndpointNPlusOneInsight + } +}; diff --git a/src/components/Insights/common/insights/EndpointNPlusOneInsight/index.tsx b/src/components/Insights/common/insights/EndpointNPlusOneInsight/index.tsx new file mode 100644 index 000000000..1c2a605b3 --- /dev/null +++ b/src/components/Insights/common/insights/EndpointNPlusOneInsight/index.tsx @@ -0,0 +1,106 @@ +import { useContext } from "react"; +import { getDurationString } from "../../../../../utils/getDurationString"; +import { sendTrackingEvent } from "../../../../../utils/sendTrackingEvent"; +import { ConfigContext } from "../../../../common/App/ConfigContext"; +import { InfoCircleIcon } from "../../../../common/icons/InfoCircleIcon"; +import { Tooltip } from "../../../../common/v3/Tooltip"; +import { trackingEvents } from "../../../tracking"; +import { InsightType, Trace } from "../../../types"; +import { InsightCard } from "../../InsightCard"; +import { ColumnsContainer } from "../../InsightCard/ColumnsContainer"; +import { KeyValue } from "../../InsightCard/KeyValue"; +import { ContentContainer, Description, Details } from "../styles"; +import * as s from "./styles"; +import { EndpointNPlusOneInsightProps } from "./types"; + +export const EndpointNPlusOneInsight = ( + props: EndpointNPlusOneInsightProps +) => { + const config = useContext(ConfigContext); + const { span } = props.insight; + + const handleSpanLinkClick = (spanCodeObjectId: string) => { + props.onAssetLinkClick(spanCodeObjectId, props.insight.type); + }; + + const handleTicketInfoButtonClick = ( + spanCodeObjectId: string, + event: string + ) => { + sendTrackingEvent(trackingEvents.JIRA_TICKET_INFO_BUTTON_CLICKED, { + insightType: props.insight.type + }); + props.onJiraTicketCreate && + props.onJiraTicketCreate(props.insight, spanCodeObjectId, event); + }; + + const handleTraceButtonClick = ( + trace: Trace, + insightType: InsightType, + spanCodeObjectId: string + ) => { + props.onTraceButtonClick(trace, insightType, spanCodeObjectId); + }; + + const spanInfo = span.internalSpan || span.clientSpan; + const spanName = spanInfo.displayName; + + return ( + + handleTraceButtonClick( + { + name: spanName, + id: span.traceId + }, + props.insight.type, + spanInfo.spanCodeObjectId + ) + : undefined + } + jiraTicketInfo={{ + ticketLink: span.ticketLink, + isHintEnabled: props.isJiraHintEnabled, + spanCodeObjectId: spanInfo.spanCodeObjectId + }} + content={ + +
+ Assets + handleSpanLinkClick(spanInfo.spanCodeObjectId)} + /> +
+ + {span.occurrences} + + +
Requests
+ +
+ + } + > + {span.requestPercentage}% +
+ + {getDurationString(span.duration)} + +
+
+ } + onRecalculate={props.onRecalculate} + onRefresh={props.onRefresh} + /> + ); +}; diff --git a/src/components/Insights/common/insights/EndpointNPlusOneInsight/mockData.ts b/src/components/Insights/common/insights/EndpointNPlusOneInsight/mockData.ts new file mode 100644 index 000000000..8b5cce804 --- /dev/null +++ b/src/components/Insights/common/insights/EndpointNPlusOneInsight/mockData.ts @@ -0,0 +1,90 @@ +import { + EndpointSpanNPlusOneInsight, + InsightCategory, + InsightScope, + InsightType +} from "../../../types"; + +export const mockedEndpointNPlusOneInsight: EndpointSpanNPlusOneInsight = { + id: "60b55792-8262-4c5d-9628-7cce7919ad6d", + 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: "Suspected N+1 Query", + type: InsightType.EndpointSpanNPlusOneV2, + category: InsightCategory.Performance, + specifity: 2, + importance: 3, + span: { + occurrences: 200, + internalSpan: null, + clientSpan: { + name: "1D138649EB4FFA92C0E3C8103404F2", + displayName: "select * from users where id = :id", + instrumentationLibrary: "SampleInsightsController", + spanCodeObjectId: + "span:SampleInsightsController$_$1D138649EB4FFA92C0E3C8103404F2", + methodCodeObjectId: null, + kind: "Client", + codeObjectId: null + }, + traceId: "9C510BC1E1CD59DD7E820BC3E8DFD4C4", + duration: { + value: 70.08, + unit: "μs", + raw: 70081 + }, + fraction: 0.08985711281727758, + criticality: 0.3, + impact: 0, + severity: 0, + ticketLink: "https://digma.ai/1", + requestPercentage: 98 + }, + scope: InsightScope.EntrySpan, + endpointSpan: "HTTP GET /SampleInsights/NPlusOneWithoutInternalSpan", + spanCodeObjectId: + "span:io.opentelemetry.tomcat-10.0$_$HTTP GET /SampleInsights/NPlusOneWithoutInternalSpan", + route: "epHTTP:HTTP GET /SampleInsights/NPlusOneWithoutInternalSpan", + serviceName: "PetClinic", + spanInfo: { + name: "HTTP GET /SampleInsights/NPlusOneWithoutInternalSpan", + displayName: "HTTP GET /SampleInsights/NPlusOneWithoutInternalSpan", + instrumentationLibrary: "io.opentelemetry.tomcat-10.0", + spanCodeObjectId: + "span:io.opentelemetry.tomcat-10.0$_$HTTP GET /SampleInsights/NPlusOneWithoutInternalSpan", + methodCodeObjectId: + "method:org.springframework.samples.petclinic.sample.SampleInsightsController$_$genNPlusOneWithoutInternalSpan", + kind: "Server", + codeObjectId: + "org.springframework.samples.petclinic.sample.SampleInsightsController$_$genNPlusOneWithoutInternalSpan" + }, + shortDisplayInfo: { + title: "", + targetDisplayName: "", + subtitle: "", + description: "" + }, + codeObjectId: + "org.springframework.samples.petclinic.sample.SampleInsightsController$_$genNPlusOneWithoutInternalSpan", + decorators: [ + { + title: "N+1 Suspected", + description: "Supected NPlus One" + } + ], + environment: "SAMPLE_ENV", + severity: 0, + isRecalculateEnabled: true, + prefixedCodeObjectId: + "method:org.springframework.samples.petclinic.sample.SampleInsightsController$_$genNPlusOneWithoutInternalSpan", + customStartTime: null, + actualStartTime: "2023-06-16T10:30:33.027Z", + sourceSpanCodeObjectInsight: "" +}; diff --git a/src/components/Insights/common/insights/EndpointNPlusOneInsight/styles.ts b/src/components/Insights/common/insights/EndpointNPlusOneInsight/styles.ts new file mode 100644 index 000000000..f1cafeb5a --- /dev/null +++ b/src/components/Insights/common/insights/EndpointNPlusOneInsight/styles.ts @@ -0,0 +1,12 @@ +import styled from "styled-components"; +import { ListItem } from "../../InsightCard/ListItem"; + +export const InfoContainer = styled.div` + display: flex; + gap: 4px; + align-items: center; +`; + +export const SpanListItem = styled(ListItem)` + padding: 4px; +`; diff --git a/src/components/Insights/common/insights/EndpointNPlusOneInsight/types.ts b/src/components/Insights/common/insights/EndpointNPlusOneInsight/types.ts new file mode 100644 index 000000000..4362c2b9d --- /dev/null +++ b/src/components/Insights/common/insights/EndpointNPlusOneInsight/types.ts @@ -0,0 +1,19 @@ +import { + EndpointSpanNPlusOneInsight, + InsightProps, + InsightType, + Trace +} from "../../../types"; + +export interface EndpointNPlusOneInsightProps extends InsightProps { + insight: EndpointSpanNPlusOneInsight; + onAssetLinkClick: ( + spanCodeObjectId: string, + insightType: InsightType + ) => void; + onTraceButtonClick: ( + trace: Trace, + insightType: InsightType, + spanCodeObjectId: string + ) => void; +} diff --git a/src/components/Insights/common/insights/ExcessiveAPICallsInsight/index.tsx b/src/components/Insights/common/insights/ExcessiveAPICallsInsight/index.tsx index 36cc36715..d228fe104 100644 --- a/src/components/Insights/common/insights/ExcessiveAPICallsInsight/index.tsx +++ b/src/components/Insights/common/insights/ExcessiveAPICallsInsight/index.tsx @@ -6,6 +6,7 @@ import { Button } from "../../../../common/v3/Button"; import { Pagination } from "../../../../common/v3/Pagination"; import { InsightType, Trace } from "../../../types"; import { InsightCard } from "../../InsightCard"; +import { ContentContainer, Description, ListContainer } from "../styles"; import * as s from "./styles"; import { ExcessiveAPICallsInsightProps } from "./types"; @@ -38,11 +39,11 @@ export const ExcessiveAPICallsInsight = ( - + + Excessive API calls to specific endpoint found - - + + {pageItems.map((span) => { const spanName = span.clientSpan.displayName; const traceId = span.traceId; @@ -81,8 +82,8 @@ export const ExcessiveAPICallsInsight = ( onPageChange={setPage} withDescription={true} /> - - + + } onRecalculate={props.onRecalculate} onRefresh={props.onRefresh} diff --git a/src/components/Insights/common/insights/ExcessiveAPICallsInsight/styles.ts b/src/components/Insights/common/insights/ExcessiveAPICallsInsight/styles.ts index d80182dfc..39395a748 100644 --- a/src/components/Insights/common/insights/ExcessiveAPICallsInsight/styles.ts +++ b/src/components/Insights/common/insights/ExcessiveAPICallsInsight/styles.ts @@ -1,25 +1,6 @@ import styled from "styled-components"; -import { caption1RegularTypography } from "../../../../common/App/typographies"; import { ListItem } from "../../InsightCard/ListItem"; -export const ContentContainer = styled.div` - display: flex; - flex-direction: column; - gap: 4px; -`; - -export const List = styled.div` - display: flex; - flex-direction: column; - gap: 4px; -`; - -export const Description = styled.div` - color: ${({ theme }) => theme.colors.v3.text.tertiary}; - - ${caption1RegularTypography} -`; - export const SpanListItem = styled(ListItem)` padding: 4px; `; diff --git a/src/components/Insights/common/insights/HighNumberOfQueriesInsight/index.tsx b/src/components/Insights/common/insights/HighNumberOfQueriesInsight/index.tsx index 49d6e1806..104dc4b77 100644 --- a/src/components/Insights/common/insights/HighNumberOfQueriesInsight/index.tsx +++ b/src/components/Insights/common/insights/HighNumberOfQueriesInsight/index.tsx @@ -2,12 +2,12 @@ import { sendTrackingEvent } from "../../../../../utils/sendTrackingEvent"; import { InfoCircleIcon } from "../../../../common/icons/InfoCircleIcon"; import { Tag } from "../../../../common/v3/Tag"; import { Tooltip } from "../../../../common/v3/Tooltip"; -import { Description } from "../../../styles"; import { trackingEvents } from "../../../tracking"; import { InsightType, Trace } from "../../../types"; import { InsightCard } from "../../InsightCard"; import { ColumnsContainer } from "../../InsightCard/ColumnsContainer"; import { KeyValue } from "../../InsightCard/KeyValue"; +import { ContentContainer, Description } from "../styles"; import * as s from "./styles"; import { HighNumberOfQueriesInsightProps } from "./types"; @@ -37,7 +37,7 @@ export const HighNumberOfQueriesInsight = ( + {insight.quantile === 0.95 && ( Affecting the slowest 5% of requests. )} @@ -64,14 +64,15 @@ export const HighNumberOfQueriesInsight = ( - + } onRecalculate={props.onRecalculate} onRefresh={props.onRefresh} onJiraButtonClick={handleCreateJiraTicketButtonClick} jiraTicketInfo={{ ticketLink: insight.ticketLink, - isHintEnabled: props.isJiraHintEnabled + isHintEnabled: props.isJiraHintEnabled, + spanCodeObjectId: props.insight.spanInfo?.spanCodeObjectId }} onGoToTrace={ traceId diff --git a/src/components/Insights/common/insights/HighNumberOfQueriesInsight/styles.ts b/src/components/Insights/common/insights/HighNumberOfQueriesInsight/styles.ts index 917de6764..5b16399ca 100644 --- a/src/components/Insights/common/insights/HighNumberOfQueriesInsight/styles.ts +++ b/src/components/Insights/common/insights/HighNumberOfQueriesInsight/styles.ts @@ -1,48 +1,9 @@ import styled from "styled-components"; -export const ContentContainer = styled.div` - display: flex; - flex-direction: column; - gap: 8px; -`; - -export const Stats = styled.div` - display: flex; - border-radius: 4px; - gap: 20px; - justify-content: space-between; -`; - -export const Stat = styled.div` - display: flex; - flex-direction: column; - gap: 8px; - overflow: hidden; -`; - -export const KeyContainer = styled.span` - display: flex; - align-items: center; - gap: 4px; -`; - -export const Key = styled.span` - font-size: 14px; - font-weight: 510; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; -`; - export const IconContainer = styled.span` display: flex; `; -export const ActionsContainer = styled.div` - display: flex; - gap: 8px; -`; - export const TypicalLabel = styled.div` display: flex; gap: 4px; diff --git a/src/components/Insights/common/insights/SlowEndpointInsight/index.tsx b/src/components/Insights/common/insights/SlowEndpointInsight/index.tsx index a64c31701..273024d6c 100644 --- a/src/components/Insights/common/insights/SlowEndpointInsight/index.tsx +++ b/src/components/Insights/common/insights/SlowEndpointInsight/index.tsx @@ -4,6 +4,7 @@ import { Tag } from "../../../../common/v3/Tag"; import { InsightCard } from "../../InsightCard"; import { ColumnsContainer } from "../../InsightCard/ColumnsContainer"; import { KeyValue } from "../../InsightCard/KeyValue"; +import { Description } from "../styles"; import * as s from "./styles"; import { SlowEndpointInsightProps } from "./types"; @@ -19,12 +20,12 @@ export const SlowEndpointInsight = (props: SlowEndpointInsightProps) => { content={ - + {`On average requests are slower than other endpoints by ${roundTo( diff, 2 )}%`} - + diff --git a/src/components/Insights/common/insights/SlowEndpointInsight/styles.ts b/src/components/Insights/common/insights/SlowEndpointInsight/styles.ts index 7b6ffa5e6..451869f09 100644 --- a/src/components/Insights/common/insights/SlowEndpointInsight/styles.ts +++ b/src/components/Insights/common/insights/SlowEndpointInsight/styles.ts @@ -1,11 +1,5 @@ import styled from "styled-components"; -import { caption1RegularTypography } from "../../../../common/App/typographies"; export const ContentContainer = styled.div` padding: 8px 0; `; - -export const Description = styled.div` - color: ${({ theme }) => theme.colors.v3.text.secondary}; - ${caption1RegularTypography} -`; diff --git a/src/components/Insights/common/insights/SpanNPlusOneInsight/SpanNPlusOneInsight.stories.tsx b/src/components/Insights/common/insights/SpanNPlusOneInsight/SpanNPlusOneInsight.stories.tsx new file mode 100644 index 000000000..1f88ebf90 --- /dev/null +++ b/src/components/Insights/common/insights/SpanNPlusOneInsight/SpanNPlusOneInsight.stories.tsx @@ -0,0 +1,29 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { SpanNPlusOneInsight } from "."; +import { mockedNPlusOneInsight } from "./mockData"; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Insights/common/insights/SpanNPlusOneInsight", + component: SpanNPlusOneInsight, + 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: mockedNPlusOneInsight + } +}; + +export const LinkedJira: Story = { + args: { + insight: { ...mockedNPlusOneInsight, ticketLink: "https://digma.ai/1" } + } +}; diff --git a/src/components/Insights/common/insights/SpanNPlusOneInsight/index.tsx b/src/components/Insights/common/insights/SpanNPlusOneInsight/index.tsx new file mode 100644 index 000000000..62d989b72 --- /dev/null +++ b/src/components/Insights/common/insights/SpanNPlusOneInsight/index.tsx @@ -0,0 +1,140 @@ +import { useContext, useState } from "react"; +import { getDurationString } from "../../../../../utils/getDurationString"; +import { sendTrackingEvent } from "../../../../../utils/sendTrackingEvent"; +import { trimEndpointScheme } from "../../../../../utils/trimEndpointScheme"; +import { ConfigContext } from "../../../../common/App/ConfigContext"; +import { InfoCircleIcon } from "../../../../common/icons/InfoCircleIcon"; +import { Tooltip } from "../../../../common/v3/Tooltip"; +import { trackingEvents } from "../../../tracking"; +import { InsightType, Trace } from "../../../types"; +import { InsightCard } from "../../InsightCard"; +import { ColumnsContainer } from "../../InsightCard/ColumnsContainer"; +import { KeyValue } from "../../InsightCard/KeyValue"; +import { ListItem } from "../../InsightCard/ListItem"; +import { Select } from "../../InsightCard/Select"; +import { ContentContainer, Description, Details } from "../styles"; +import * as s from "./styles"; +import { SpanNPlusOneInsightProps } from "./types"; + +export const SpanNPlusOneInsight = (props: SpanNPlusOneInsightProps) => { + const { + insight: { type, endpoints, ticketLink } + } = props; + + const config = useContext(ConfigContext); + const [selectedEndpoint, setSelectedEndpoint] = useState( + props.insight.endpoints.length ? props.insight.endpoints[0] : null + ); + + const handleSpanLinkClick = (spanCodeObjectId?: string) => { + spanCodeObjectId && props.onAssetLinkClick(spanCodeObjectId, type); + }; + + const handleTraceButtonClick = ( + trace: Trace, + insightType: InsightType, + spanCodeObjectId?: string + ) => { + props.onTraceButtonClick(trace, insightType, spanCodeObjectId); + }; + + const handleCreateJiraTicketButtonClick = (event: string) => { + sendTrackingEvent(trackingEvents.JIRA_TICKET_INFO_BUTTON_CLICKED, { + insightType: type + }); + props.onJiraTicketCreate && + props.onJiraTicketCreate(props.insight, undefined, event); + }; + + const spanName = props.insight.clientSpanName || undefined; + const spanCodeObjectId = props.insight.clientSpanCodeObjectId || undefined; + const traceId = props.insight.traceId; + + return ( + + handleTraceButtonClick( + { + name: spanName, + id: traceId + }, + props.insight.type, + spanCodeObjectId + ) + : undefined + } + content={ + +
+ Effected Endpoints ({endpoints.length}) +