Skip to content

Commit 7f458d2

Browse files
authored
feat(profiling): Support both profile formats in transaction summary … (#75099)
…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.
1 parent e0350cf commit 7f458d2

File tree

5 files changed

+93
-39
lines changed

5 files changed

+93
-39
lines changed

static/app/components/discover/transactionsTable.tsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import {Fragment, PureComponent} from 'react';
22
import styled from '@emotion/styled';
33
import type {Location, LocationDescriptor} from 'history';
44

5+
import {LinkButton} from 'sentry/components/button';
56
import SortLink from 'sentry/components/gridEditable/sortLink';
67
import Link from 'sentry/components/links/link';
78
import LoadingIndicator from 'sentry/components/loadingIndicator';
89
import {PanelTable} from 'sentry/components/panels/panelTable';
910
import QuestionTooltip from 'sentry/components/questionTooltip';
11+
import {IconProfiling} from 'sentry/icons';
1012
import {t} from 'sentry/locale';
1113
import {space} from 'sentry/styles/space';
1214
import type {Organization} from 'sentry/types/organization';
@@ -145,23 +147,25 @@ class TransactionsTable extends PureComponent<Props> {
145147

146148
const target = generateLink?.[field]?.(organization, row, location);
147149

148-
if (target && !isEmptyObject(target)) {
150+
if (fields[index] === 'profile.id') {
151+
rendered = (
152+
<LinkButton
153+
data-test-id={`view-${fields[index]}`}
154+
disabled={!target || isEmptyObject(target)}
155+
to={target || {}}
156+
onClick={getProfileAnalyticsHandler(organization, referrer)}
157+
size="xs"
158+
>
159+
<IconProfiling size="xs" />
160+
</LinkButton>
161+
);
162+
} else if (target && !isEmptyObject(target)) {
149163
if (fields[index] === 'replayId') {
150164
rendered = (
151165
<ViewReplayLink replayId={row.replayId} to={target}>
152166
{rendered}
153167
</ViewReplayLink>
154168
);
155-
} else if (fields[index] === 'profile.id') {
156-
rendered = (
157-
<Link
158-
data-test-id={`view-${fields[index]}`}
159-
to={target}
160-
onClick={getProfileAnalyticsHandler(organization, referrer)}
161-
>
162-
{rendered}
163-
</Link>
164-
);
165169
} else {
166170
rendered = (
167171
<Link data-test-id={`view-${fields[index]}`} to={target}>

static/app/utils/dates.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,3 +304,20 @@ export function getDateWithTimezoneInUtc(date?: Date, utc?: boolean | null) {
304304
.utc()
305305
.toDate();
306306
}
307+
308+
/**
309+
* Converts a string or timestamp in milliseconds to a Date
310+
*/
311+
export function getDateFromTimestamp(value: unknown): Date | null {
312+
if (typeof value !== 'string' && typeof value !== 'number') {
313+
return null;
314+
}
315+
316+
const dateObj = new Date(value);
317+
318+
if (isNaN(dateObj.getTime())) {
319+
return null;
320+
}
321+
322+
return dateObj;
323+
}

static/app/views/performance/newTraceDetails/traceDrawer/traceProfilingLink.ts

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {Location, LocationDescriptor} from 'history';
22

3+
import {getDateFromTimestamp} from 'sentry/utils/dates';
34
import {generateContinuousProfileFlamechartRouteWithQuery} from 'sentry/utils/profiling/routes';
45
import {
56
isSpanNode,
@@ -10,20 +11,6 @@ import type {
1011
TraceTreeNode,
1112
} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
1213

13-
function toDate(value: unknown): Date | null {
14-
if (typeof value !== 'string' && typeof value !== 'number') {
15-
return null;
16-
}
17-
18-
const dateObj = new Date(value);
19-
20-
if (isNaN(dateObj.getTime())) {
21-
return null;
22-
}
23-
24-
return dateObj;
25-
}
26-
2714
function getNodeId(node: TraceTreeNode<TraceTree.NodeValue>): string | undefined {
2815
if (isTransactionNode(node)) {
2916
return node.value.event_id;
@@ -66,8 +53,10 @@ export function makeTraceContinuousProfilingLink(
6653
if (!transaction) {
6754
return null;
6855
}
69-
let start: Date | null = toDate(transaction.space[0]);
70-
let end: Date | null = toDate(transaction.space[0] + transaction.space[1]);
56+
let start: Date | null = getDateFromTimestamp(transaction.space[0]);
57+
let end: Date | null = getDateFromTimestamp(
58+
transaction.space[0] + transaction.space[1]
59+
);
7160

7261
// End timestamp is required to generate a link
7362
if (end === null || typeof profilerId !== 'string' || profilerId === '') {

static/app/views/performance/transactionSummary/transactionOverview/content.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -263,15 +263,25 @@ function SummaryContent({
263263
}
264264

265265
if (
266-
organization.features.includes('profiling') &&
267-
project &&
268266
// only show for projects that already sent a profile
269267
// once we have a more compact design we will show this for
270268
// projects that support profiling as well
271-
project.hasProfiles
269+
project?.hasProfiles &&
270+
(organization.features.includes('profiling') ||
271+
organization.features.includes('continuous-profiling'))
272272
) {
273273
transactionsListTitles.push(t('profile'));
274-
fields.push({field: 'profile.id'});
274+
275+
if (organization.features.includes('profiling')) {
276+
fields.push({field: 'profile.id'});
277+
}
278+
279+
if (organization.features.includes('continuous-profiling')) {
280+
fields.push({field: 'profiler.id'});
281+
fields.push({field: 'thread.id'});
282+
fields.push({field: 'precise.start_ts'});
283+
fields.push({field: 'precise.finish_ts'});
284+
}
275285
}
276286

277287
// update search conditions

static/app/views/performance/transactionSummary/utils.tsx

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ import type {Location, LocationDescriptor, Query} from 'history';
44

55
import {space} from 'sentry/styles/space';
66
import type {Organization} from 'sentry/types/organization';
7+
import {getDateFromTimestamp} from 'sentry/utils/dates';
78
import type {TableDataRow} from 'sentry/utils/discover/discoverQuery';
89
import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls';
910
import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
10-
import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes';
11+
import {
12+
generateContinuousProfileFlamechartRouteWithQuery,
13+
generateProfileFlamechartRoute,
14+
} from 'sentry/utils/profiling/routes';
1115
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
1216
import normalizeUrl from 'sentry/utils/url/normalizeUrl';
1317
import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils';
@@ -171,15 +175,45 @@ export function generateProfileLink() {
171175
tableRow: TableDataRow,
172176
_location: Location | undefined
173177
) => {
178+
const projectSlug = tableRow['project.name'];
179+
174180
const profileId = tableRow['profile.id'];
175-
if (!profileId) {
176-
return {};
181+
if (projectSlug && profileId) {
182+
return generateProfileFlamechartRoute({
183+
orgSlug: organization.slug,
184+
projectSlug: String(tableRow['project.name']),
185+
profileId: String(profileId),
186+
});
177187
}
178-
return generateProfileFlamechartRoute({
179-
orgSlug: organization.slug,
180-
projectSlug: String(tableRow['project.name']),
181-
profileId: String(profileId),
182-
});
188+
189+
const profilerId = tableRow['profiler.id'];
190+
const threadId = tableRow['thread.id'];
191+
const start =
192+
typeof tableRow['precise.start_ts'] === 'number'
193+
? getDateFromTimestamp(tableRow['precise.start_ts'] * 1000)
194+
: null;
195+
const finish =
196+
typeof tableRow['precise.finish_ts'] === 'number'
197+
? getDateFromTimestamp(tableRow['precise.finish_ts'] * 1000)
198+
: null;
199+
if (projectSlug && profilerId && threadId && start && finish) {
200+
const query: Record<string, string> = {tid: String(threadId)};
201+
if (tableRow.id && tableRow.trace) {
202+
query.eventId = String(tableRow.id);
203+
query.traceId = String(tableRow.trace);
204+
}
205+
206+
return generateContinuousProfileFlamechartRouteWithQuery(
207+
organization.slug,
208+
String(projectSlug),
209+
String(profilerId),
210+
start.toISOString(),
211+
finish.toISOString(),
212+
query
213+
);
214+
}
215+
216+
return {};
183217
};
184218
}
185219

0 commit comments

Comments
 (0)