diff --git a/static/app/views/explore/logs/tables/logsInfiniteTable.spec.tsx b/static/app/views/explore/logs/tables/logsInfiniteTable.spec.tsx
index ae714e0afd6d..8d2f0aa2ac0b 100644
--- a/static/app/views/explore/logs/tables/logsInfiniteTable.spec.tsx
+++ b/static/app/views/explore/logs/tables/logsInfiniteTable.spec.tsx
@@ -791,6 +791,35 @@ describe('LogsInfiniteTable', () => {
);
});
+ it('renders colored text without escape codes when an injected error row has an ANSI title', async () => {
+ const traceError: TraceTree.TraceError = {
+ event_id: 'abc123def456',
+ issue: 'JAVASCRIPT-1',
+ issue_id: 42,
+ level: 'error',
+ message: 'Boom happened',
+ project_id: Number(project.id),
+ project_slug: project.slug,
+ span: 'span1',
+ title: 'TypeError: \x1B[31mBoom\x1B[0m happened',
+ timestamp: new Date('2100-01-01T00:00:00Z').getTime() / 1000,
+ };
+
+ renderWithProviders(
+
+ );
+
+ const colored = await screen.findByText('Boom');
+
+ expect(colored.style.color).toContain('color-mix(in srgb,');
+ expect(screen.getByTestId('log-table-cell-error')).toHaveTextContent(
+ /^TypeError: Boom happened$/
+ );
+ });
+
it('renders injected error rows without the empty state when the logs query is empty', async () => {
MockApiClient.clearMockResponses();
MockApiClient.addMockResponse({
diff --git a/static/app/views/explore/logs/tables/logsTableRow.tsx b/static/app/views/explore/logs/tables/logsTableRow.tsx
index c416aebdc6ed..addca1e755c2 100644
--- a/static/app/views/explore/logs/tables/logsTableRow.tsx
+++ b/static/app/views/explore/logs/tables/logsTableRow.tsx
@@ -9,6 +9,7 @@ import {Button, LinkButton} from '@sentry/scraps/button';
import type {MenuItemProps} from '@sentry/scraps/dropdownMenu';
import {Flex} from '@sentry/scraps/layout';
+import {AnsiText} from 'sentry/components/ansiText';
import {EmptyStreamWrapper} from 'sentry/components/emptyStateWarning';
import ProjectBadge from 'sentry/components/idBadge/projectBadge';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
@@ -618,7 +619,7 @@ export const LogRowContent = memo(function LogRowContentImpl({
{isErrorRow ? (
- {String(dataRow[OurLogKnownFieldKey.MESSAGE] ?? '')}
+ {String(dataRow[OurLogKnownFieldKey.MESSAGE] ?? '')}
) : (
fields?.map((field, index) => {
diff --git a/static/app/views/performance/traceDetails/traceHeader/title.spec.tsx b/static/app/views/performance/traceDetails/traceHeader/title.spec.tsx
new file mode 100644
index 000000000000..89bc6ebe9f87
--- /dev/null
+++ b/static/app/views/performance/traceDetails/traceHeader/title.spec.tsx
@@ -0,0 +1,64 @@
+import {OrganizationFixture} from 'sentry-fixture/organization';
+
+import {render, screen} from 'sentry-test/reactTestingLibrary';
+
+import type {OurLogsResponseItem} from 'sentry/views/explore/logs/types';
+import {OurLogKnownFieldKey} from 'sentry/views/explore/logs/types';
+import type {TraceRootEventQueryResults} from 'sentry/views/performance/traceDetails/traceApi/useTraceRootEvent';
+import {Title} from 'sentry/views/performance/traceDetails/traceHeader/title';
+import {ErrorNode} from 'sentry/views/performance/traceDetails/traceModels/traceTreeNode/errorNode';
+import {makeTraceError} from 'sentry/views/performance/traceDetails/traceModels/traceTreeTestUtils';
+
+const rootEventResults = {
+ data: undefined,
+ isLoading: false,
+ status: 'pending',
+} as TraceRootEventQueryResults;
+
+function makeLog(message: string) {
+ return {
+ [OurLogKnownFieldKey.MESSAGE]: message,
+ [OurLogKnownFieldKey.SEVERITY]: 'info',
+ } as OurLogsResponseItem;
+}
+
+describe('Title', () => {
+ it('renders colored text without escape codes when the representative log message has ANSI codes', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('failed').style.color).toContain('color-mix(in srgb,');
+ expect(screen.getByText('failed').parentElement).toHaveTextContent(
+ /^failed to connect$/
+ );
+ });
+
+ it('renders colored text without escape codes when the representative error title has ANSI codes', () => {
+ const node = new ErrorNode(
+ null,
+ makeTraceError({title: '\x1B[31mfailed\x1B[0m to connect'}),
+ {organization: OrganizationFixture()}
+ );
+
+ render(
+
+ );
+
+ expect(screen.getByText('failed').style.color).toContain('color-mix(in srgb,');
+ expect(screen.getByText('failed').parentElement).toHaveTextContent(
+ /^failed to connect$/
+ );
+ });
+});
diff --git a/static/app/views/performance/traceDetails/traceHeader/title.tsx b/static/app/views/performance/traceDetails/traceHeader/title.tsx
index 9bf428eb1a4d..73f355d1462e 100644
--- a/static/app/views/performance/traceDetails/traceHeader/title.tsx
+++ b/static/app/views/performance/traceDetails/traceHeader/title.tsx
@@ -5,6 +5,7 @@ import {LinkButton} from '@sentry/scraps/button';
import {Flex, Stack} from '@sentry/scraps/layout';
import {Text} from '@sentry/scraps/text';
+import {AnsiText} from 'sentry/components/ansiText';
import {IconPlay} from 'sentry/icons';
import {t} from 'sentry/locale';
import {ReplayContextKey} from 'sentry/types/event';
@@ -103,12 +104,12 @@ export function Title({isLoading, representativeEvent, rootEventResults}: TitleP
return (
- {traceTitle.title}
+ {traceTitle.title}
{traceTitle.subtitle && (
- {traceTitle.subtitle}
+ {traceTitle.subtitle}
diff --git a/static/app/views/performance/traceDetails/traceModels/traceTreeNode/errorNode.spec.tsx b/static/app/views/performance/traceDetails/traceModels/traceTreeNode/errorNode.spec.tsx
index 0a1cdb116a1a..f2639f2346ba 100644
--- a/static/app/views/performance/traceDetails/traceModels/traceTreeNode/errorNode.spec.tsx
+++ b/static/app/views/performance/traceDetails/traceModels/traceTreeNode/errorNode.spec.tsx
@@ -274,6 +274,18 @@ describe('ErrorNode', () => {
expect(node.drawerTabsTitle).toBe('Test Error');
});
+ it('should strip ANSI escape codes from drawerTabsTitle', () => {
+ const extra = createMockExtra();
+ const value = makeTraceError({
+ title: '\x1B[31mTest\x1B[0m Error',
+ level: 'error',
+ });
+
+ const node = new ErrorNode(null, value, extra);
+
+ expect(node.drawerTabsTitle).toBe('Test Error');
+ });
+
it('should return fallback drawerTabsTitle without description', () => {
const extra = createMockExtra();
const value = makeTraceError({
diff --git a/static/app/views/performance/traceDetails/traceModels/traceTreeNode/errorNode.tsx b/static/app/views/performance/traceDetails/traceModels/traceTreeNode/errorNode.tsx
index 67f5fcaae628..eb7aa658c409 100644
--- a/static/app/views/performance/traceDetails/traceModels/traceTreeNode/errorNode.tsx
+++ b/static/app/views/performance/traceDetails/traceModels/traceTreeNode/errorNode.tsx
@@ -1,6 +1,7 @@
import type {Theme} from '@emotion/react';
import {t} from 'sentry/locale';
+import {stripAnsi} from 'sentry/utils/ansiEscapeCodes';
import {ErrorNodeDetails} from 'sentry/views/performance/traceDetails/traceDrawer/details/error';
import type {TraceTreeNodeDetailsProps} from 'sentry/views/performance/traceDetails/traceDrawer/tabs/traceTreeNodeDetails';
import {isTraceError} from 'sentry/views/performance/traceDetails/traceGuards';
@@ -60,7 +61,7 @@ export class ErrorNode extends BaseNode {
}
get drawerTabsTitle(): string {
- return this.description || t('Error');
+ return (this.description && stripAnsi(this.description)) || t('Error');
}
analyticsName(): string {
diff --git a/static/app/views/performance/traceDetails/traceRow/traceErrorRow.spec.tsx b/static/app/views/performance/traceDetails/traceRow/traceErrorRow.spec.tsx
new file mode 100644
index 000000000000..a056725680b7
--- /dev/null
+++ b/static/app/views/performance/traceDetails/traceRow/traceErrorRow.spec.tsx
@@ -0,0 +1,61 @@
+import {OrganizationFixture} from 'sentry-fixture/organization';
+import {ThemeFixture} from 'sentry-fixture/theme';
+
+import {render, screen} from 'sentry-test/reactTestingLibrary';
+
+import {ErrorNode} from 'sentry/views/performance/traceDetails/traceModels/traceTreeNode/errorNode';
+import {makeTraceError} from 'sentry/views/performance/traceDetails/traceModels/traceTreeTestUtils';
+import {TraceScheduler} from 'sentry/views/performance/traceDetails/traceRenderers/traceScheduler';
+import {TraceView} from 'sentry/views/performance/traceDetails/traceRenderers/traceView';
+import {VirtualizedViewManager} from 'sentry/views/performance/traceDetails/traceRenderers/virtualizedViewManager';
+import {TraceErrorRow} from 'sentry/views/performance/traceDetails/traceRow/traceErrorRow';
+
+describe('TraceErrorRow', () => {
+ it('renders colored text without escape codes when the error title has ANSI codes', () => {
+ const theme = ThemeFixture();
+ const node = new ErrorNode(
+ null,
+ makeTraceError({title: '\x1B[31mfailed\x1B[0m to connect', timestamp: 1}),
+ {organization: OrganizationFixture()}
+ );
+ const manager = new VirtualizedViewManager(
+ {list: {width: 0.5}, span_list: {width: 0.5}},
+ new TraceScheduler(),
+ new TraceView(),
+ theme
+ );
+
+ render(
+
+ );
+
+ expect(screen.getByText('failed').style.color).toContain('color-mix(in srgb,');
+ expect(screen.getByText('failed').closest('.TraceDescription')).toHaveTextContent(
+ /^failed to connect$/
+ );
+ });
+});
diff --git a/static/app/views/performance/traceDetails/traceRow/traceErrorRow.tsx b/static/app/views/performance/traceDetails/traceRow/traceErrorRow.tsx
index 1bea9b2207c4..f404e7bc7e48 100644
--- a/static/app/views/performance/traceDetails/traceRow/traceErrorRow.tsx
+++ b/static/app/views/performance/traceDetails/traceRow/traceErrorRow.tsx
@@ -1,5 +1,6 @@
import {PlatformIcon} from 'platformicons';
+import {AnsiText} from 'sentry/components/ansiText';
import {t} from 'sentry/locale';
import type {Level} from 'sentry/types/event';
import {TraceIcons} from 'sentry/views/performance/traceDetails/traceIcons';
@@ -66,8 +67,10 @@ export function TraceErrorRow(props: TraceRowProps) {
{ERROR_LEVEL_LABELS[props.node.value.level ?? 'error']}
—
- {/* oxlint-disable-next-line react/refs */}
- {description}
+
+ {/* oxlint-disable-next-line react/refs */}
+ {description ?? ''}
+
{/* oxlint-disable-next-line react/refs -- This is a React element, not a ref. */}