From 51d1315de9a2c9fb8926317d34650a1aec9fbd7a Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Fri, 26 Jul 2024 16:07:39 -0400 Subject: [PATCH] feat(profiling): Support both profile formats in transaction summary overview Without changing the end user experience, we're changing the profile column to render an profile button that links to the underlying profile regardless of if it's a transaction profile or a continuous profile. --- .../components/discover/transactionsTable.tsx | 26 ++++++---- static/app/utils/dates.tsx | 17 +++++++ .../traceDrawer/traceProfilingLink.ts | 21 ++------ .../transactionOverview/content.tsx | 18 +++++-- .../performance/transactionSummary/utils.tsx | 50 ++++++++++++++++--- 5 files changed, 93 insertions(+), 39 deletions(-) diff --git a/static/app/components/discover/transactionsTable.tsx b/static/app/components/discover/transactionsTable.tsx index 4ee472420d58e9..756d46557ef509 100644 --- a/static/app/components/discover/transactionsTable.tsx +++ b/static/app/components/discover/transactionsTable.tsx @@ -2,11 +2,13 @@ import {Fragment, PureComponent} from 'react'; import styled from '@emotion/styled'; import type {Location, LocationDescriptor} from 'history'; +import {LinkButton} from 'sentry/components/button'; import SortLink from 'sentry/components/gridEditable/sortLink'; import Link from 'sentry/components/links/link'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import {PanelTable} from 'sentry/components/panels/panelTable'; import QuestionTooltip from 'sentry/components/questionTooltip'; +import {IconProfiling} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Organization} from 'sentry/types/organization'; @@ -145,23 +147,25 @@ class TransactionsTable extends PureComponent { const target = generateLink?.[field]?.(organization, row, location); - if (target && !isEmptyObject(target)) { + if (fields[index] === 'profile.id') { + rendered = ( + + + + ); + } else if (target && !isEmptyObject(target)) { if (fields[index] === 'replayId') { rendered = ( {rendered} ); - } else if (fields[index] === 'profile.id') { - rendered = ( - - {rendered} - - ); } else { rendered = ( diff --git a/static/app/utils/dates.tsx b/static/app/utils/dates.tsx index fa18f43875a239..1f61da2780af1f 100644 --- a/static/app/utils/dates.tsx +++ b/static/app/utils/dates.tsx @@ -304,3 +304,20 @@ export function getDateWithTimezoneInUtc(date?: Date, utc?: boolean | null) { .utc() .toDate(); } + +/** + * Converts a string or timestamp in milliseconds to a Date + */ +export function getDateFromTimestamp(value: unknown): Date | null { + if (typeof value !== 'string' && typeof value !== 'number') { + return null; + } + + const dateObj = new Date(value); + + if (isNaN(dateObj.getTime())) { + return null; + } + + return dateObj; +} diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/traceProfilingLink.ts b/static/app/views/performance/newTraceDetails/traceDrawer/traceProfilingLink.ts index 03bc9456c67c73..9f1eade71747af 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/traceProfilingLink.ts +++ b/static/app/views/performance/newTraceDetails/traceDrawer/traceProfilingLink.ts @@ -1,5 +1,6 @@ import type {Location, LocationDescriptor} from 'history'; +import {getDateFromTimestamp} from 'sentry/utils/dates'; import {generateContinuousProfileFlamechartRouteWithQuery} from 'sentry/utils/profiling/routes'; import { isSpanNode, @@ -10,20 +11,6 @@ import type { TraceTreeNode, } from 'sentry/views/performance/newTraceDetails/traceModels/traceTree'; -function toDate(value: unknown): Date | null { - if (typeof value !== 'string' && typeof value !== 'number') { - return null; - } - - const dateObj = new Date(value); - - if (isNaN(dateObj.getTime())) { - return null; - } - - return dateObj; -} - function getNodeId(node: TraceTreeNode): string | undefined { if (isTransactionNode(node)) { return node.value.event_id; @@ -66,8 +53,10 @@ export function makeTraceContinuousProfilingLink( if (!transaction) { return null; } - let start: Date | null = toDate(transaction.space[0]); - let end: Date | null = toDate(transaction.space[0] + transaction.space[1]); + let start: Date | null = getDateFromTimestamp(transaction.space[0]); + let end: Date | null = getDateFromTimestamp( + transaction.space[0] + transaction.space[1] + ); // End timestamp is required to generate a link if (end === null || typeof profilerId !== 'string' || profilerId === '') { diff --git a/static/app/views/performance/transactionSummary/transactionOverview/content.tsx b/static/app/views/performance/transactionSummary/transactionOverview/content.tsx index 2dbb8d7ce93d27..6514bd778d0be5 100644 --- a/static/app/views/performance/transactionSummary/transactionOverview/content.tsx +++ b/static/app/views/performance/transactionSummary/transactionOverview/content.tsx @@ -263,15 +263,25 @@ function SummaryContent({ } if ( - organization.features.includes('profiling') && - project && // only show for projects that already sent a profile // once we have a more compact design we will show this for // projects that support profiling as well - project.hasProfiles + project?.hasProfiles && + (organization.features.includes('profiling') || + organization.features.includes('continuous-profiling')) ) { transactionsListTitles.push(t('profile')); - fields.push({field: 'profile.id'}); + + if (organization.features.includes('profiling')) { + fields.push({field: 'profile.id'}); + } + + if (organization.features.includes('continuous-profiling')) { + fields.push({field: 'profiler.id'}); + fields.push({field: 'thread.id'}); + fields.push({field: 'precise.start_ts'}); + fields.push({field: 'precise.finish_ts'}); + } } // update search conditions diff --git a/static/app/views/performance/transactionSummary/utils.tsx b/static/app/views/performance/transactionSummary/utils.tsx index 8926bee3c46785..a6d20169704fb6 100644 --- a/static/app/views/performance/transactionSummary/utils.tsx +++ b/static/app/views/performance/transactionSummary/utils.tsx @@ -4,10 +4,14 @@ import type {Location, LocationDescriptor, Query} from 'history'; import {space} from 'sentry/styles/space'; import type {Organization} from 'sentry/types/organization'; +import {getDateFromTimestamp} from 'sentry/utils/dates'; import type {TableDataRow} from 'sentry/utils/discover/discoverQuery'; import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes'; -import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes'; +import { + generateContinuousProfileFlamechartRouteWithQuery, + generateProfileFlamechartRoute, +} from 'sentry/utils/profiling/routes'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import normalizeUrl from 'sentry/utils/url/normalizeUrl'; import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils'; @@ -171,15 +175,45 @@ export function generateProfileLink() { tableRow: TableDataRow, _location: Location | undefined ) => { + const projectSlug = tableRow['project.name']; + const profileId = tableRow['profile.id']; - if (!profileId) { - return {}; + if (projectSlug && profileId) { + return generateProfileFlamechartRoute({ + orgSlug: organization.slug, + projectSlug: String(tableRow['project.name']), + profileId: String(profileId), + }); } - return generateProfileFlamechartRoute({ - orgSlug: organization.slug, - projectSlug: String(tableRow['project.name']), - profileId: String(profileId), - }); + + const profilerId = tableRow['profiler.id']; + const threadId = tableRow['thread.id']; + const start = + typeof tableRow['precise.start_ts'] === 'number' + ? getDateFromTimestamp(tableRow['precise.start_ts'] * 1000) + : null; + const finish = + typeof tableRow['precise.finish_ts'] === 'number' + ? getDateFromTimestamp(tableRow['precise.finish_ts'] * 1000) + : null; + if (projectSlug && profilerId && threadId && start && finish) { + const query: Record = {tid: String(threadId)}; + if (tableRow.id && tableRow.trace) { + query.eventId = String(tableRow.id); + query.traceId = String(tableRow.trace); + } + + return generateContinuousProfileFlamechartRouteWithQuery( + organization.slug, + String(projectSlug), + String(profilerId), + start.toISOString(), + finish.toISOString(), + query + ); + } + + return {}; }; }