diff --git a/static/app/utils/dashboards/issueFieldRenderers.tsx b/static/app/utils/dashboards/issueFieldRenderers.tsx
index 72777ce29891a9..05f282db9dc36a 100644
--- a/static/app/utils/dashboards/issueFieldRenderers.tsx
+++ b/static/app/utils/dashboards/issueFieldRenderers.tsx
@@ -21,6 +21,8 @@ import {Container, FieldShortId, OverflowLink} from 'sentry/utils/discover/style
import {SavedQueryDatasets} from 'sentry/utils/discover/types';
import {hasDatasetSelector} from 'sentry/views/dashboards/utils';
import {FieldKey} from 'sentry/views/dashboards/widgetBuilder/issueWidget/fields';
+import {QuickContextHoverWrapper} from 'sentry/views/discover/table/quickContext/quickContextWrapper';
+import {ContextType} from 'sentry/views/discover/table/quickContext/utils';
/**
* Types, functions and definitions for rendering fields in discover results.
@@ -79,9 +81,15 @@ const SPECIAL_FIELDS: SpecialFields = {
return (
-
-
-
+
+
+
+
+
);
},
diff --git a/static/app/utils/discover/eventView.spec.tsx b/static/app/utils/discover/eventView.spec.tsx
index 6d708f34e60996..4bb5199845c51d 100644
--- a/static/app/utils/discover/eventView.spec.tsx
+++ b/static/app/utils/discover/eventView.spec.tsx
@@ -1184,7 +1184,7 @@ describe('EventView.generateQueryStringObject()', function () {
name: 'best query',
fields: [
{field: 'count()', width: 123},
- {field: 'project.id', width: 456},
+ {field: 'issue', width: 456},
],
sorts: generateSorts([AggregationKey.COUNT]),
query: 'event.type:error',
@@ -1203,7 +1203,7 @@ describe('EventView.generateQueryStringObject()', function () {
const expected = {
id: '1234',
name: 'best query',
- field: ['count()', 'project.id'],
+ field: ['count()', 'issue'],
widths: ['123', '456'],
sort: '-count',
query: 'event.type:error',
@@ -1659,7 +1659,7 @@ describe('EventView.toNewQuery()', function () {
name: 'best query',
fields: [
{field: 'count()', width: 123},
- {field: 'project.id', width: 456},
+ {field: 'issue', width: 456},
],
sorts: generateSorts([AggregationKey.COUNT]),
query: 'event.type:error',
@@ -1681,7 +1681,7 @@ describe('EventView.toNewQuery()', function () {
version: 2,
id: '1234',
name: 'best query',
- fields: ['count()', 'project.id'],
+ fields: ['count()', 'issue'],
widths: ['123', '456'],
orderby: '-count',
query: 'event.type:error',
@@ -1713,7 +1713,7 @@ describe('EventView.toNewQuery()', function () {
version: 2,
id: '1234',
name: 'best query',
- fields: ['count()', 'project.id'],
+ fields: ['count()', 'issue'],
widths: ['123', '456'],
orderby: '-count',
projects: [42],
@@ -1744,7 +1744,7 @@ describe('EventView.toNewQuery()', function () {
version: 2,
id: '1234',
name: 'best query',
- fields: ['count()', 'project.id'],
+ fields: ['count()', 'issue'],
widths: ['123', '456'],
orderby: '-count',
projects: [42],
@@ -1765,7 +1765,7 @@ describe('EventView.isValid()', function () {
it('event view is valid when there is at least one field', function () {
const eventView = new EventView({
...REQUIRED_CONSTRUCTOR_PROPS,
- fields: [{field: 'count()'}, {field: 'project.id'}],
+ fields: [{field: 'count()'}, {field: 'issue'}],
sorts: [],
project: [],
});
@@ -1791,7 +1791,7 @@ describe('EventView.getWidths()', function () {
...REQUIRED_CONSTRUCTOR_PROPS,
fields: [
{field: 'count()', width: COL_WIDTH_UNDEFINED},
- {field: 'project.id', width: 2020},
+ {field: 'issue', width: 2020},
{field: 'title', width: COL_WIDTH_UNDEFINED},
{field: 'time', width: 420},
{field: 'lcp', width: 69},
@@ -1817,12 +1817,12 @@ describe('EventView.getFields()', function () {
it('returns fields', function () {
const eventView = new EventView({
...REQUIRED_CONSTRUCTOR_PROPS,
- fields: [{field: 'count()'}, {field: 'project.id'}],
+ fields: [{field: 'count()'}, {field: 'issue'}],
sorts: [],
project: [],
});
- expect(eventView.getFields()).toEqual(['count()', 'project.id']);
+ expect(eventView.getFields()).toEqual(['count()', 'issue']);
});
});
@@ -1832,7 +1832,7 @@ describe('EventView.numOfColumns()', function () {
const eventView = new EventView({
...REQUIRED_CONSTRUCTOR_PROPS,
- fields: [{field: 'count()'}, {field: 'project.id'}],
+ fields: [{field: 'count()'}, {field: 'issue'}],
sorts: [],
project: [],
});
@@ -1893,7 +1893,7 @@ describe('EventView.clone()', function () {
...REQUIRED_CONSTRUCTOR_PROPS,
id: '1234',
name: 'best query',
- fields: [{field: 'count()'}, {field: 'project.id'}],
+ fields: [{field: 'count()'}, {field: 'issue'}],
sorts: generateSorts([AggregationKey.COUNT]),
query: 'event.type:error',
project: [42],
@@ -1928,7 +1928,7 @@ describe('EventView.withColumns()', function () {
name: 'best query',
fields: [
{field: 'count()', width: 30},
- {field: 'project.id', width: 99},
+ {field: 'issue', width: 99},
{field: 'failure_count()', width: 30},
],
yAxis: 'failure_count()',
@@ -1946,7 +1946,7 @@ describe('EventView.withColumns()', function () {
const newView = eventView.withColumns([
{kind: 'field', field: 'title'},
{kind: 'function', function: [AggregationKey.COUNT, '', undefined, undefined]},
- {kind: 'field', field: 'project.id'},
+ {kind: 'field', field: 'issue'},
{kind: 'field', field: 'culprit'},
]);
// Views should be different.
@@ -1954,7 +1954,7 @@ describe('EventView.withColumns()', function () {
expect(newView.fields).toEqual([
{field: 'title', width: COL_WIDTH_UNDEFINED},
{field: 'count()', width: COL_WIDTH_UNDEFINED},
- {field: 'project.id', width: COL_WIDTH_UNDEFINED},
+ {field: 'issue', width: COL_WIDTH_UNDEFINED},
{field: 'culprit', width: COL_WIDTH_UNDEFINED},
]);
});
@@ -1976,14 +1976,14 @@ describe('EventView.withColumns()', function () {
it('inherits widths from existing columns when names match', function () {
const newView = eventView.withColumns([
{kind: 'function', function: [AggregationKey.COUNT, '', undefined, undefined]},
- {kind: 'field', field: 'project.id'},
+ {kind: 'field', field: 'issue'},
{kind: 'field', field: 'title'},
{kind: 'field', field: 'time'},
]);
expect(newView.fields).toEqual([
{field: 'count()', width: 30},
- {field: 'project.id', width: 99},
+ {field: 'issue', width: 99},
{field: 'title', width: COL_WIDTH_UNDEFINED},
{field: 'time', width: COL_WIDTH_UNDEFINED},
]);
@@ -2016,12 +2016,12 @@ describe('EventView.withColumns()', function () {
it('updates yAxis if column is dropped', function () {
const newView = eventView.withColumns([
{kind: 'field', field: 'count()'},
- {kind: 'field', field: 'project.id'},
+ {kind: 'field', field: 'issue'},
]);
expect(newView.fields).toEqual([
{field: 'count()', width: 30},
- {field: 'project.id', width: 99},
+ {field: 'issue', width: 99},
]);
expect(eventView.yAxis).toBe('failure_count()');
@@ -2036,7 +2036,7 @@ describe('EventView.withNewColumn()', function () {
name: 'best query',
fields: [
{field: 'count()', width: 30},
- {field: 'project.id', width: 99},
+ {field: 'issue', width: 99},
],
sorts: generateSorts([AggregationKey.COUNT]),
query: 'event.type:error',
@@ -2118,7 +2118,7 @@ describe('EventView.withResizedColumn()', function () {
...REQUIRED_CONSTRUCTOR_PROPS,
id: '1234',
name: 'best query',
- fields: [{field: 'count()'}, {field: 'project.id'}],
+ fields: [{field: 'count()'}, {field: 'issue'}],
sorts: generateSorts([AggregationKey.COUNT]),
query: 'event.type:error',
project: [42],
@@ -2146,7 +2146,7 @@ describe('EventView.withUpdatedColumn()', function () {
...REQUIRED_CONSTRUCTOR_PROPS,
id: '1234',
name: 'best query',
- fields: [{field: 'count()'}, {field: 'project.id'}],
+ fields: [{field: 'count()'}, {field: 'issue'}],
sorts: generateSorts([AggregationKey.COUNT]),
query: 'event.type:error',
project: [42],
@@ -2335,7 +2335,7 @@ describe('EventView.withUpdatedColumn()', function () {
// this column is expected to be non-sortable
const newColumn: Column = {
kind: 'field',
- field: 'project.id',
+ field: 'issue',
};
const eventView2 = eventView.withUpdatedColumn(0, newColumn, meta);
@@ -2347,7 +2347,7 @@ describe('EventView.withUpdatedColumn()', function () {
const nextState = {
...state,
sorts: [{field: 'title', kind: 'desc'}],
- fields: [{field: 'project.id'}, {field: 'title'}],
+ fields: [{field: 'issue'}, {field: 'title'}],
};
expect(eventView2).toMatchObject(nextState);
@@ -2364,7 +2364,7 @@ describe('EventView.withUpdatedColumn()', function () {
// this column is expected to be non-sortable
const newColumn: Column = {
kind: 'field',
- field: 'project.id',
+ field: 'issue',
};
const eventView2 = eventView.withUpdatedColumn(0, newColumn, meta);
@@ -2376,7 +2376,7 @@ describe('EventView.withUpdatedColumn()', function () {
const nextState = {
...state,
sorts: [],
- fields: [{field: 'project.id'}],
+ fields: [{field: 'issue'}],
};
expect(eventView2).toMatchObject(nextState);
@@ -2389,7 +2389,7 @@ describe('EventView.withDeletedColumn()', function () {
...REQUIRED_CONSTRUCTOR_PROPS,
id: '1234',
name: 'best query',
- fields: [{field: 'count()'}, {field: 'project.id'}],
+ fields: [{field: 'count()'}, {field: 'issue'}],
sorts: generateSorts([AggregationKey.COUNT]),
query: 'event.type:error',
project: [42],
@@ -2454,7 +2454,7 @@ describe('EventView.withDeletedColumn()', function () {
const nextState = {
...state,
- // we expect sorts to be empty since project.id is non-sortable
+ // we expect sorts to be empty since issue is non-sortable
sorts: [],
fields: [state.fields[1]],
};
@@ -2465,7 +2465,7 @@ describe('EventView.withDeletedColumn()', function () {
it('has a remaining sortable column', function () {
const modifiedState: ConstructorParameters[0] = {
...state,
- fields: [{field: 'count()'}, {field: 'project.id'}, {field: 'title'}],
+ fields: [{field: 'count()'}, {field: 'issue'}, {field: 'title'}],
};
const eventView = new EventView(modifiedState);
@@ -2478,7 +2478,7 @@ describe('EventView.withDeletedColumn()', function () {
const nextState = {
...state,
sorts: [{field: 'title', kind: 'desc'}],
- fields: [{field: 'project.id'}, {field: 'title'}],
+ fields: [{field: 'issue'}, {field: 'title'}],
};
expect(eventView2).toMatchObject(nextState);
@@ -2539,7 +2539,7 @@ describe('EventView.getSorts()', function () {
it('returns fields', function () {
const eventView = new EventView({
...REQUIRED_CONSTRUCTOR_PROPS,
- fields: [{field: 'count()'}, {field: 'project.id'}],
+ fields: [{field: 'count()'}, {field: 'issue'}],
sorts: generateSorts([AggregationKey.COUNT]),
project: [],
});
@@ -2611,7 +2611,7 @@ describe('EventView.sortForField()', function () {
...REQUIRED_CONSTRUCTOR_PROPS,
id: '1234',
name: 'best query',
- fields: [{field: 'count()'}, {field: 'project.id'}],
+ fields: [{field: 'count()'}, {field: 'issue'}],
sorts: generateSorts([AggregationKey.COUNT]),
query: 'event.type:error',
project: [42],
@@ -2638,7 +2638,7 @@ describe('EventView.sortForField()', function () {
it('returns undefined when selected field is not sorted', function () {
const field = {
- field: 'project.id',
+ field: 'issue',
};
expect(eventView.sortForField(field, meta)).toBeUndefined();
@@ -2646,7 +2646,7 @@ describe('EventView.sortForField()', function () {
it('returns undefined when no meta is provided', function () {
const field = {
- field: 'project.id',
+ field: 'issue',
};
expect(eventView.sortForField(field, undefined)).toBeUndefined();
@@ -2658,7 +2658,7 @@ describe('EventView.sortOnField()', function () {
...REQUIRED_CONSTRUCTOR_PROPS,
id: '1234',
name: 'best query',
- fields: [{field: 'count()'}, {field: 'project.id'}],
+ fields: [{field: 'count()'}, {field: 'issue'}],
sorts: generateSorts([AggregationKey.COUNT]),
query: 'event.type:error',
project: [42],
@@ -2839,7 +2839,7 @@ describe('EventView.isEqualTo()', function () {
...REQUIRED_CONSTRUCTOR_PROPS,
id: '1234',
name: 'best query',
- fields: [{field: 'count()'}, {field: 'project.id'}],
+ fields: [{field: 'count()'}, {field: 'issue'}],
sorts: generateSorts([AggregationKey.COUNT]),
query: 'event.type:error',
project: [42],
@@ -2870,7 +2870,7 @@ describe('EventView.isEqualTo()', function () {
...REQUIRED_CONSTRUCTOR_PROPS,
id: '1234',
name: 'best query',
- fields: [{field: 'count()'}, {field: 'project.id'}],
+ fields: [{field: 'count()'}, {field: 'issue'}],
sorts: generateSorts([AggregationKey.COUNT]),
query: 'event.type:error',
project: [42],
@@ -2894,7 +2894,7 @@ describe('EventView.isEqualTo()', function () {
...REQUIRED_CONSTRUCTOR_PROPS,
id: '1234',
name: 'best query',
- fields: [{field: 'count()'}, {field: 'project.id'}],
+ fields: [{field: 'count()'}, {field: 'issue'}],
sorts: generateSorts([AggregationKey.COUNT]),
query: 'event.type:error',
project: [42],
@@ -2910,7 +2910,7 @@ describe('EventView.isEqualTo()', function () {
const differences = {
id: '12',
name: 'new query',
- fields: [{field: 'project.id'}, {field: 'count()'}],
+ fields: [{field: 'issue'}, {field: 'count()'}],
sorts: [{field: AggregationKey.COUNT, kind: 'asc'}],
query: 'event.type:transaction',
project: [24],
@@ -2938,7 +2938,7 @@ describe('EventView.isEqualTo()', function () {
...REQUIRED_CONSTRUCTOR_PROPS,
id: '1234',
name: 'best query',
- fields: [{field: 'count()'}, {field: 'project.id'}],
+ fields: [{field: 'count()'}, {field: 'issue'}],
sorts: generateSorts([AggregationKey.COUNT]),
query: 'event.type:error',
project: [42],
@@ -2977,7 +2977,7 @@ describe('EventView.getResultsViewUrlTarget()', function () {
...REQUIRED_CONSTRUCTOR_PROPS,
id: '1234',
name: 'best query',
- fields: [{field: 'count()'}, {field: 'project.id'}],
+ fields: [{field: 'count()'}, {field: 'issue'}],
sorts: generateSorts([AggregationKey.COUNT]),
query: 'event.type:error',
project: [42],
@@ -3033,7 +3033,7 @@ describe('EventView.getResultsViewShortUrlTarget()', function () {
...REQUIRED_CONSTRUCTOR_PROPS,
id: '1234',
name: 'best query',
- fields: [{field: 'count()'}, {field: 'project.id'}],
+ fields: [{field: 'count()'}, {field: 'issue'}],
sorts: generateSorts([AggregationKey.COUNT]),
query: 'event.type:error',
project: [42],
@@ -3098,7 +3098,7 @@ describe('EventView.getPerformanceTransactionEventsViewUrlTarget()', function ()
...REQUIRED_CONSTRUCTOR_PROPS,
id: '1234',
name: 'best query',
- fields: [{field: 'count()'}, {field: 'project.id'}],
+ fields: [{field: 'count()'}, {field: 'issue'}],
sorts: generateSorts([AggregationKey.COUNT]),
query: 'event.type:error',
project: [42],
@@ -3564,7 +3564,7 @@ describe('isAPIPayloadSimilar', function () {
...REQUIRED_CONSTRUCTOR_PROPS,
id: '1234',
name: 'best query',
- fields: [{field: 'count()'}, {field: 'project.id'}],
+ fields: [{field: 'count()'}, {field: 'issue'}],
sorts: generateSorts([AggregationKey.COUNT]),
query: 'event.type:error',
project: [42],
@@ -3759,7 +3759,7 @@ describe('isAPIPayloadSimilar', function () {
const equationField = {field: 'equation|failure_count() / count()'};
const otherEquationField = {field: 'equation|failure_count() / 2'};
state.fields = [
- {field: 'project.id'},
+ {field: 'issue'},
{field: 'count()'},
equationField,
otherEquationField,
@@ -3770,7 +3770,7 @@ describe('isAPIPayloadSimilar', function () {
state.fields = [
equationField,
- {field: 'project.id'},
+ {field: 'issue'},
{field: 'count()'},
otherEquationField,
];
@@ -3787,7 +3787,7 @@ describe('isAPIPayloadSimilar', function () {
const equationField = {field: 'equation|failure_count() / count()'};
const otherEquationField = {field: 'equation|failure_count() / 2'};
state.fields = [
- {field: 'project.id'},
+ {field: 'issue'},
{field: 'count()'},
equationField,
otherEquationField,
@@ -3797,7 +3797,7 @@ describe('isAPIPayloadSimilar', function () {
const thisAPIPayload = thisEventView.getEventsAPIPayload(location);
state.fields = [
- {field: 'project.id'},
+ {field: 'issue'},
{field: 'count()'},
otherEquationField,
equationField,
diff --git a/static/app/utils/discover/fieldRenderers.tsx b/static/app/utils/discover/fieldRenderers.tsx
index 824cc372299bee..7ab9251036d568 100644
--- a/static/app/utils/discover/fieldRenderers.tsx
+++ b/static/app/utils/discover/fieldRenderers.tsx
@@ -13,6 +13,7 @@ import {deviceNameMapper} from 'sentry/components/deviceName';
import type {MenuItemProps} from 'sentry/components/dropdownMenu';
import {DropdownMenu} from 'sentry/components/dropdownMenu';
import Duration from 'sentry/components/duration';
+import {ContextIcon} from 'sentry/components/events/contexts/contextIcon';
import FileSize from 'sentry/components/fileSize';
import BadgeDisplayName from 'sentry/components/idBadge/badgeDisplayName';
import ProjectBadge from 'sentry/components/idBadge/projectBadge';
@@ -45,6 +46,7 @@ import {
SPAN_OP_BREAKDOWN_FIELDS,
SPAN_OP_RELATIVE_BREAKDOWN_FIELD,
} from 'sentry/utils/discover/fields';
+import ViewReplayLink from 'sentry/utils/discover/viewReplayLink';
import {getShortEventId} from 'sentry/utils/events';
import {formatRate} from 'sentry/utils/formatters';
import getDynamicText from 'sentry/utils/getDynamicText';
@@ -60,15 +62,18 @@ import {ContextType} from 'sentry/views/discover/table/quickContext/utils';
import {PerformanceBadge} from 'sentry/views/insights/browser/webVitals/components/performanceBadge';
import {PercentChangeCell} from 'sentry/views/insights/common/components/tableCells/percentChangeCell';
import {ResponseStatusCodeCell} from 'sentry/views/insights/common/components/tableCells/responseStatusCodeCell';
+import {SpanDescriptionCell} from 'sentry/views/insights/common/components/tableCells/spanDescriptionCell';
import {StarredSegmentCell} from 'sentry/views/insights/common/components/tableCells/starredSegmentCell';
import {TimeSpentCell} from 'sentry/views/insights/common/components/tableCells/timeSpentCell';
-import {SpanFields, SpanMetricsField} from 'sentry/views/insights/types';
+import {ModuleName, SpanFields, SpanMetricsField} from 'sentry/views/insights/types';
import {
filterToLocationQuery,
SpanOperationBreakdownFilter,
stringToFilter,
} from 'sentry/views/performance/transactionSummary/filter';
+import {makeProjectsPathname} from 'sentry/views/projects/pathname';
import {ADOPTION_STAGE_LABELS} from 'sentry/views/releases/utils';
+import {makeReplaysPathname} from 'sentry/views/replays/pathnames';
import ArrayValue from './arrayValue';
import {
@@ -77,6 +82,7 @@ import {
FieldDateTime,
FieldShortId,
FlexContainer,
+ IconContainer,
NumberContainer,
OverflowFieldShortId,
OverflowLink,
@@ -134,7 +140,9 @@ const EmptyValueContainer = styled('span')`
color: ${p => p.theme.subText};
`;
const emptyValue = {t('(no value)')};
-const emptyStringValue = {t('(empty string)')};
+export const emptyStringValue = (
+ {t('(empty string)')}
+);
const missingUserMisery = tct(
'We were unable to calculate User Misery. A likely cause of this is that the user was not set. [link:Read the docs]',
{
@@ -143,6 +151,9 @@ const missingUserMisery = tct(
),
}
);
+const userAgentLocking = t(
+ 'This operating system does not provide detailed version information in the User-Agent HTTP header. The exact operating system version is unknown.'
+);
export function nullableValue(value: string | null): string | React.ReactElement {
switch (value) {
@@ -367,35 +378,6 @@ type SpecialField = {
sortField: string | null;
};
-type SpecialFields = {
- adoption_stage: SpecialField;
- 'apdex()': SpecialField;
- attachments: SpecialField;
- 'count_unique(user)': SpecialField;
- device: SpecialField;
- 'error.handled': SpecialField;
- id: SpecialField;
- [SpanFields.IS_STARRED_TRANSACTION]: SpecialField;
- issue: SpecialField;
- 'issue.id': SpecialField;
- minidump: SpecialField;
- 'performance_score(measurements.score.total)': SpecialField;
- 'profile.id': SpecialField;
- project: SpecialField;
- release: SpecialField;
- replayId: SpecialField;
- 'span.description': SpecialField;
- 'span.status_code': SpecialField;
- span_id: SpecialField;
- team_key_transaction: SpecialField;
- 'timestamp.to_day': SpecialField;
- 'timestamp.to_hour': SpecialField;
- trace: SpecialField;
- 'trend_percentage()': SpecialField;
- user: SpecialField;
- 'user.display': SpecialField;
-};
-
const DownloadCount = styled('span')`
padding-left: ${space(0.75)};
`;
@@ -409,7 +391,7 @@ const RightAlignedContainer = styled('span')`
* "Special fields" either do not map 1:1 to an single column in the event database,
* or they require custom UI formatting that can't be handled by the datatype formatters.
*/
-const SPECIAL_FIELDS: SpecialFields = {
+const SPECIAL_FIELDS: Record = {
// This is a custom renderer for a field outside discover
// TODO - refactor code and remove from this file or add ability to query for attachments in Discover
'apdex()': {
@@ -496,9 +478,8 @@ const SPECIAL_FIELDS: SpecialFields = {
renderFunc: data => {
const id: string | unknown = data?.id;
if (typeof id !== 'string') {
- return null;
+ return {emptyStringValue};
}
-
return {getShortEventId(id)};
},
},
@@ -516,7 +497,24 @@ const SPECIAL_FIELDS: SpecialFields = {
'span.description': {
sortField: 'span.description',
renderFunc: data => {
- const value = data['span.description'];
+ const value = data[SpanFields.SPAN_DESCRIPTION];
+ const op: string = data[SpanFields.SPAN_OP];
+ const projectId =
+ typeof data[SpanFields.PROJECT_ID] === 'number'
+ ? data[SpanFields.PROJECT_ID]
+ : parseInt(data[SpanFields.PROJECT_ID], 10) || -1;
+ const spanGroup: string | undefined = data[SpanFields.SPAN_GROUP];
+
+ if (op === ModuleName.DB || op === ModuleName.RESOURCE) {
+ return (
+
+ );
+ }
return (
{
+ renderFunc: (data, {organization}) => {
const replayId = data?.replayId;
if (typeof replayId !== 'string' || !replayId) {
return emptyValue;
}
- return {getShortEventId(replayId)};
+ const target = makeReplaysPathname({
+ path: `/${replayId}/`,
+ organization,
+ });
+
+ return (
+
+
+ {getShortEventId(replayId)}
+
+
+ );
},
},
'profile.id': {
@@ -649,6 +658,21 @@ const SPECIAL_FIELDS: SpecialFields = {
);
},
},
+ // Two different project ID fields are being used right now. `project_id` is shared between all datasets, but `project.id` is the new one used in spans
+ project_id: {
+ sortField: 'project_id',
+ renderFunc: (data, baggage) => {
+ const projectId = data.project_id;
+ return getProjectIdLink(projectId, baggage);
+ },
+ },
+ 'project.id': {
+ sortField: 'project.id',
+ renderFunc: (data, baggage) => {
+ const projectId = data['project.id'];
+ return getProjectIdLink(projectId, baggage);
+ },
+ },
user: {
sortField: 'user',
renderFunc: data => {
@@ -795,6 +819,23 @@ const SPECIAL_FIELDS: SpecialFields = {
),
},
+ timestamp: {
+ sortField: 'timestamp',
+ renderFunc: data => {
+ const timestamp = data.timestamp;
+ if (!timestamp) {
+ return {emptyStringValue};
+ }
+ const date = new Date(data.timestamp);
+ return (
+
+
+
+
+
+ );
+ },
+ },
'timestamp.to_hour': {
sortField: 'timestamp.to_hour',
renderFunc: data => (
@@ -843,6 +884,126 @@ const SPECIAL_FIELDS: SpecialFields = {
);
},
},
+ 'browser.name': {
+ sortField: 'browser.name',
+ renderFunc: data => {
+ const browserName = data['browser.name'];
+ if (typeof browserName !== 'string') {
+ return {emptyStringValue};
+ }
+
+ return (
+
+ {getContextIcon(browserName, false)}
+ {browserName}
+
+ );
+ },
+ },
+ browser: {
+ sortField: 'browser',
+ renderFunc: data => {
+ const browser = data.browser;
+ if (typeof browser !== 'string') {
+ return {emptyStringValue};
+ }
+
+ return (
+
+ {getContextIcon(browser, true)}
+ {browser}
+
+ );
+ },
+ },
+ 'os.name': {
+ sortField: 'os.name',
+ renderFunc: data => {
+ const osName = data['os.name'];
+ if (typeof osName !== 'string') {
+ return {emptyStringValue};
+ }
+
+ return (
+
+ {getContextIcon(osName, false)}
+ {osName}
+
+ );
+ },
+ },
+ os: {
+ sortField: 'os',
+ renderFunc: data => {
+ const os = data.os;
+ if (typeof os !== 'string') {
+ return {emptyStringValue};
+ }
+
+ const hasUserAgentLocking = os.includes('>=');
+
+ return (
+
+ {getContextIcon(os, true)}
+ {hasUserAgentLocking ? (
+
+ {os}
+
+ ) : (
+ os
+ )}
+
+ );
+ },
+ },
+};
+
+/**
+ * Returns an icon component for fields that use logo icons
+ * @param value the text to map to an icon
+ * @param dropVersion drops the last part of the value (the version)
+ */
+const getContextIcon = (value: string, dropVersion?: boolean) => {
+ const valueArray = value.split(' ');
+
+ // Some fields have the number version attached, so it needs to be removed
+ if (dropVersion) {
+ valueArray.pop();
+ }
+
+ const formattedValue = valueArray.join('-').toLocaleLowerCase();
+
+ return ;
+};
+
+const getProjectIdLink = (
+ projectId: number | string | undefined,
+ {organization}: RenderFunctionBaggage
+) => {
+ const parsedId = typeof projectId === 'string' ? parseInt(projectId, 10) : projectId;
+ if (!parsedId || isNaN(parsedId)) {
+ return {emptyValue};
+ }
+
+ // TODO: Component has been deprecated in favour of hook, need to refactor this
+ return (
+
+
+ {({projects}) => {
+ const project = projects.find(p => p.id === parsedId?.toString());
+ if (!project) {
+ return emptyValue;
+ }
+ const target = makeProjectsPathname({
+ path: `/${project?.slug}/?project=${parsedId}/`,
+ organization,
+ });
+
+ return {parsedId};
+ }}
+
+
+ );
};
type SpecialFunctionFieldRenderer = (
@@ -951,8 +1112,8 @@ export function getSortField(
field: string,
tableMeta: MetaType | undefined
): string | null {
- if (SPECIAL_FIELDS.hasOwnProperty(field)) {
- return SPECIAL_FIELDS[field as keyof typeof SPECIAL_FIELDS].sortField;
+ if (Object.hasOwn(SPECIAL_FIELDS, field)) {
+ return SPECIAL_FIELDS[field]!.sortField;
}
if (!tableMeta) {
@@ -971,7 +1132,7 @@ export function getSortField(
}
const fieldType = tableMeta[field];
- if (FIELD_FORMATTERS.hasOwnProperty(fieldType)) {
+ if (Object.hasOwn(FIELD_FORMATTERS, fieldType)) {
return FIELD_FORMATTERS[fieldType as keyof typeof FIELD_FORMATTERS].isSortable
? field
: null;
diff --git a/static/app/utils/discover/styles.tsx b/static/app/utils/discover/styles.tsx
index 1ae2f3c4c159e6..31dff1513cbe18 100644
--- a/static/app/utils/discover/styles.tsx
+++ b/static/app/utils/discover/styles.tsx
@@ -62,3 +62,10 @@ export const UserIcon = styled(IconUser)`
margin-left: ${space(1)};
color: ${p => p.theme.gray400};
`;
+
+export const IconContainer = styled('div')`
+ display: flex;
+ gap: ${space(1)};
+ overflow: hidden;
+ text-overflow: ellipsis;
+`;
diff --git a/static/app/views/dashboards/datasetConfig/errorsAndTransactions.tsx b/static/app/views/dashboards/datasetConfig/errorsAndTransactions.tsx
index 48c245e2376f33..2ce9a1a899a6e6 100644
--- a/static/app/views/dashboards/datasetConfig/errorsAndTransactions.tsx
+++ b/static/app/views/dashboards/datasetConfig/errorsAndTransactions.tsx
@@ -19,12 +19,12 @@ import {defined} from 'sentry/utils';
import type {CustomMeasurementCollection} from 'sentry/utils/customMeasurements/customMeasurements';
import {getTimeStampFromTableDateField} from 'sentry/utils/dates';
import type {EventsTableData, TableData} from 'sentry/utils/discover/discoverQuery';
-import type {MetaType} from 'sentry/utils/discover/eventView';
+import type {EventData, MetaType} from 'sentry/utils/discover/eventView';
import type {
FieldFormatterRenderFunctionPartial,
RenderFunctionBaggage,
} from 'sentry/utils/discover/fieldRenderers';
-import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
+import {emptyStringValue, getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
import type {AggregationOutputType, QueryFieldValue} from 'sentry/utils/discover/fields';
import {
errorsAndTransactionsAggregateFunctionOutputType,
@@ -360,7 +360,7 @@ function getSeriesResultType(
}
export function renderEventIdAsLinkable(
- data: any,
+ data: EventData,
{eventView, organization}: RenderFunctionBaggage
) {
const id: string | unknown = data?.id;
@@ -387,12 +387,12 @@ export function renderEventIdAsLinkable(
export function renderTraceAsLinkable(widget?: Widget) {
return function (
- data: any,
+ data: EventData,
{eventView, organization, location}: RenderFunctionBaggage
) {
const id: string | unknown = data?.trace;
if (!eventView || typeof id !== 'string') {
- return null;
+ return emptyStringValue;
}
const dateSelection = eventView.normalizeDateSelection(location);
const target = getTraceDetailsUrl({
diff --git a/static/app/views/dashboards/datasetConfig/spans.tsx b/static/app/views/dashboards/datasetConfig/spans.tsx
index d6d9194c1c3aab..53b7ebf986c09a 100644
--- a/static/app/views/dashboards/datasetConfig/spans.tsx
+++ b/static/app/views/dashboards/datasetConfig/spans.tsx
@@ -2,6 +2,7 @@ import pickBy from 'lodash/pickBy';
import {doEventsRequest} from 'sentry/actionCreators/events';
import type {Client} from 'sentry/api';
+import {Link} from 'sentry/components/core/link';
import type {PageFilters} from 'sentry/types/core';
import type {TagCollection} from 'sentry/types/group';
import type {
@@ -13,7 +14,9 @@ import type {
import toArray from 'sentry/utils/array/toArray';
import type {CustomMeasurementCollection} from 'sentry/utils/customMeasurements/customMeasurements';
import type {EventsTableData, TableData} from 'sentry/utils/discover/discoverQuery';
-import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
+import type {EventData} from 'sentry/utils/discover/eventView';
+import type {RenderFunctionBaggage} from 'sentry/utils/discover/fieldRenderers';
+import {emptyStringValue, getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
import type {Aggregation, QueryFieldValue} from 'sentry/utils/discover/fields';
import {
type DiscoverQueryExtras,
@@ -21,6 +24,8 @@ import {
doDiscoverQuery,
} from 'sentry/utils/discover/genericDiscoverQuery';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
+import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls';
+import {getShortEventId} from 'sentry/utils/events';
import {
AggregationKey,
ALLOWED_EXPLORE_VISUALIZE_AGGREGATES,
@@ -35,6 +40,7 @@ import {
import {
getTableSortOptions,
getTimeseriesSortOptions,
+ renderTraceAsLinkable,
transformEventsResponseToTable,
} from 'sentry/views/dashboards/datasetConfig/errorsAndTransactions';
import {getSeriesRequestData} from 'sentry/views/dashboards/datasetConfig/utils/getSeriesRequestData';
@@ -46,6 +52,7 @@ import type {FieldValueOption} from 'sentry/views/discover/table/queryField';
import {FieldValueKind} from 'sentry/views/discover/table/types';
import {generateFieldOptions} from 'sentry/views/discover/utils';
import type {SamplingMode} from 'sentry/views/explore/hooks/useProgressiveQuery';
+import {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceHeader/breadcrumbs';
const DEFAULT_WIDGET_QUERY: WidgetQuery = {
name: '',
@@ -170,7 +177,13 @@ export const SpansConfig: DatasetConfig<
transformTable: transformEventsResponseToTable,
transformSeries: transformEventsResponseToSeries,
filterAggregateParams,
- getCustomFieldRenderer: (field, meta, _organization) => {
+ getCustomFieldRenderer: (field, meta, widget, _organization) => {
+ if (field === 'id') {
+ return renderEventInTraceView;
+ }
+ if (field === 'trace') {
+ return renderTraceAsLinkable(widget);
+ }
return getFieldRenderer(field, meta, false);
},
};
@@ -352,3 +365,24 @@ function filterSeriesSortOptions(columns: Set) {
return columns.has(option.value.meta.name);
};
}
+
+function renderEventInTraceView(
+ data: EventData,
+ {location, organization}: RenderFunctionBaggage
+) {
+ const spanId = data.id;
+ if (!spanId || typeof spanId !== 'string') {
+ return emptyStringValue;
+ }
+ const target = generateLinkToEventInTraceView({
+ traceSlug: data.trace,
+ timestamp: data.timestamp,
+ targetId: data['transaction.span_id'],
+ organization,
+ location,
+ spanId,
+ source: TraceViewSources.DASHBOARDS,
+ });
+
+ return {getShortEventId(spanId)};
+}