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 {}; }; }