Skip to content
29 changes: 29 additions & 0 deletions static/app/views/explore/logs/tables/logsInfiniteTable.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<LogsInfiniteTable
analyticsPageSource={LogsAnalyticsPageSource.EXPLORE_LOGS}
injectedErrorRows={[createErrorLogRow(traceError)]}
/>
);

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({
Expand Down
3 changes: 2 additions & 1 deletion static/app/views/explore/logs/tables/logsTableRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -618,7 +619,7 @@ export const LogRowContent = memo(function LogRowContentImpl({
</LogsTableBodyFirstCell>
{isErrorRow ? (
<LogErrorLabelCell data-test-id="log-table-cell-error">
{String(dataRow[OurLogKnownFieldKey.MESSAGE] ?? '')}
<AnsiText>{String(dataRow[OurLogKnownFieldKey.MESSAGE] ?? '')}</AnsiText>
</LogErrorLabelCell>
) : (
fields?.map((field, index) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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(
<Title
isLoading={false}
representativeEvent={{
event: makeLog('\x1B[31mfailed\x1B[0m to connect'),
dataset: null,
}}
rootEventResults={rootEventResults}
/>
);

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(
<Title
isLoading={false}
representativeEvent={{event: node, dataset: null}}
rootEventResults={rootEventResults}
/>
);

expect(screen.getByText('failed').style.color).toContain('color-mix(in srgb,');
expect(screen.getByText('failed').parentElement).toHaveTextContent(
/^failed to connect$/
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -103,12 +104,12 @@ export function Title({isLoading, representativeEvent, rootEventResults}: TitleP
return (
<Stack align="start" width="100%">
<Text size="xl" bold ellipsis>
{traceTitle.title}
<AnsiText>{traceTitle.title}</AnsiText>
</Text>
{traceTitle.subtitle && (
<Flex align="center" gap="sm" width="100%">
<Text size="md" ellipsis variant="muted">
{traceTitle.subtitle}
<AnsiText>{traceTitle.subtitle}</AnsiText>
</Text>
<ContextBadges rootEventResults={rootEventResults} />
</Flex>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -60,7 +61,7 @@ export class ErrorNode extends BaseNode<TraceTree.TraceErrorIssue> {
}

get drawerTabsTitle(): string {
return this.description || t('Error');
return (this.description && stripAnsi(this.description)) || t('Error');
}

analyticsName(): string {
Expand Down
Original file line number Diff line number Diff line change
@@ -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(
<TraceErrorRow
index={0}
listColumnClassName=""
listColumnStyle={{}}
manager={manager}
node={node}
onExpand={jest.fn()}
onExpandDoubleClick={jest.fn()}
onRowClick={jest.fn()}
onRowDoubleClick={jest.fn()}
onRowKeyDown={jest.fn()}
onSpanArrowClick={jest.fn()}
previouslyFocusedNodeRef={{current: null}}
projects={{}}
registerListColumnRef={jest.fn()}
registerSpanArrowRef={jest.fn()}
registerSpanColumnRef={jest.fn()}
rowSearchClassName=""
spanColumnClassName=""
style={{}}
tabIndex={-1}
theme={theme}
trace_id={undefined}
virtualized_index={0}
/>
);

expect(screen.getByText('failed').style.color).toContain('color-mix(in srgb,');
expect(screen.getByText('failed').closest('.TraceDescription')).toHaveTextContent(
/^failed to connect$/
);
});
});
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -66,8 +67,10 @@ export function TraceErrorRow(props: TraceRowProps<ErrorNode>) {
{ERROR_LEVEL_LABELS[props.node.value.level ?? 'error']}
</span>
<strong className="TraceEmDash"> — </strong>
{/* oxlint-disable-next-line react/refs */}
<span className="TraceDescription">{description}</span>
<span className="TraceDescription">
{/* oxlint-disable-next-line react/refs */}
<AnsiText>{description ?? ''}</AnsiText>
</span>
</div>
</div>
{/* oxlint-disable-next-line react/refs -- This is a React element, not a ref. */}
Expand Down
Loading