Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,49 @@ describe('BreadcrumbItemContent', () => {
).toBeInTheDocument();
});

it('renders colored text without escape codes for ANSI messages', () => {
const breadcrumb: BreadcrumbTypeDefault = {
type: BreadcrumbType.DEBUG,
level: BreadcrumbLevelType.INFO,
message: '\x1B[31mfailed\x1B[0m to connect',
};
const {container} = render(<BreadcrumbItemContent breadcrumb={breadcrumb} />);
expect(container).toHaveTextContent(/^failed to connect$/);
expect(screen.getByText('failed').style.color).toContain('color-mix(in srgb,');
});

it('renders colored text without escape codes for ANSI exception values', () => {
const breadcrumb: BreadcrumbTypeDefault = {
type: BreadcrumbType.ERROR,
level: BreadcrumbLevelType.ERROR,
data: {type: 'ConnectionError', value: '\x1B[31mfailed\x1B[0m to connect'},
};
const {container} = render(<BreadcrumbItemContent breadcrumb={breadcrumb} />);
expect(container).toHaveTextContent(/^ConnectionError: failed to connect$/);
expect(screen.getByText('failed').style.color).toContain('color-mix(in srgb,');
});

it('renders annotated text for ANSI messages with meta', () => {
const breadcrumb: BreadcrumbTypeDefault = {
type: BreadcrumbType.DEBUG,
level: BreadcrumbLevelType.INFO,
message: '\x1B[31mfailed\x1B[0m to connect to [Filtered]',
};
const meta = {
message: {
'': {
chunks: [
{type: 'text', text: '\x1B[31mfailed\x1B[0m to connect to '},
{type: 'redaction', text: '[Filtered]', rule_id: '@password', remark: 's'},
],
},
},
};
render(<BreadcrumbItemContent breadcrumb={breadcrumb} meta={meta} />);
expect(screen.getByText('[Filtered]')).toBeInTheDocument();
expect(screen.queryByText('failed')).not.toBeInTheDocument();
});

it('applies item limits with fullyExpanded', () => {
const longMessage = 'longMessage'.repeat(100);
const breadcrumb: BreadcrumbTypeDefault = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {Fragment} from 'react';
import styled from '@emotion/styled';

import {openNavigateToExternalLinkModal} from 'sentry/actionCreators/modal';
import {AnsiText} from 'sentry/components/ansiText';
import {AnnotatedText} from 'sentry/components/events/meta/annotatedText';
import {StructuredData} from 'sentry/components/structuredEventData';
import {Timeline} from 'sentry/components/timeline';
Expand All @@ -13,6 +14,7 @@ import {
type BreadcrumbTypeNavigation,
type RawCrumb,
} from 'sentry/types/breadcrumbs';
import {hasAnsi} from 'sentry/utils/ansiEscapeCodes';
import {defined} from 'sentry/utils/defined';
import {isValidUrl} from 'sentry/utils/string/isValidUrl';
import {usePrismTokens} from 'sentry/utils/usePrismTokens';
Expand Down Expand Up @@ -44,7 +46,15 @@ export function BreadcrumbItemContent({

const defaultMessage = defined(bc.message) ? (
<BreadcrumbText>
<StructuredData value={bc.message} meta={meta?.message} {...structuredDataProps} />
{!meta?.message && hasAnsi(bc.message) ? (
<AnsiText>{bc.message}</AnsiText>
) : (
<StructuredData
value={bc.message}
meta={meta?.message}
{...structuredDataProps}
/>
)}
</BreadcrumbText>
) : null;

Expand Down Expand Up @@ -206,7 +216,8 @@ function ExceptionCrumbContent({
<Fragment>
<BreadcrumbText>
{type ? type : null}
{type && hasValue ? `: ${formattedValue}` : hasValue ? formattedValue : null}
{type && hasValue ? ': ' : null}
{hasValue ? <AnsiText>{formattedValue}</AnsiText> : null}
</BreadcrumbText>
{children}
{Object.keys(otherData).length > 0 ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ Message: Second request
Data: {"url":"/api/test","status_code":200}"
`);
});
it('strips ANSI codes when the message contains them', () => {
const crumb = createMockCrumb({message: '\x1B[31mGET\x1B[0m request'});
const result = formatBreadcrumbsAsText([crumb]);
expect(result).toContain('Message: GET request\n');
});
});
describe('formatBreadcrumbsAsMarkdown', () => {
it('formats breadcrumbs as a markdown table with header', () => {
Expand All @@ -74,4 +79,9 @@ describe('formatBreadcrumbsAsMarkdown', () => {
| 2024-01-15T10:30:45.123Z | http | http | info | value\\|with\\|pipes | {"key":"val\\|ue"} |"
`);
});
it('strips ANSI codes when the message contains them', () => {
const crumb = createMockCrumb({message: '\x1B[31mGET\x1B[0m request'});
const result = formatBreadcrumbsAsMarkdown([crumb]);
expect(result).toContain('| info | GET request |');
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {CopyAsDropdown} from 'sentry/components/copyAsDropdown';
import type {EnhancedCrumb} from 'sentry/components/events/breadcrumbs/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
import {stripAnsi} from 'sentry/utils/ansiEscapeCodes';
import {useOrganization} from 'sentry/utils/useOrganization';

function escapeMarkdownCell(value: string): string {
Expand All @@ -20,7 +21,7 @@ export function formatBreadcrumbsAsMarkdown(crumbs: EnhancedCrumb[]): string {
const timestamp = bc.timestamp ? new Date(bc.timestamp).toISOString() : '';
const type = bc.type;
const category = bc.category ?? '';
const message = escapeMarkdownCell(bc.message ?? '');
const message = escapeMarkdownCell(stripAnsi(bc.message ?? ''));
const data = bc.data ? escapeMarkdownCell(JSON.stringify(bc.data)) : '';

return `| ${timestamp} | ${type} | ${category} | ${bc.level} | ${message} | ${data} |`;
Expand All @@ -45,7 +46,7 @@ export function formatBreadcrumbsAsText(crumbs: EnhancedCrumb[]): string {
lines.push(`Level: ${crumb.breadcrumb.level}`);

if (crumb.breadcrumb.message) {
lines.push(`Message: ${crumb.breadcrumb.message}`);
lines.push(`Message: ${stripAnsi(crumb.breadcrumb.message)}`);
}
if (crumb.breadcrumb.data) {
lines.push(`Data: ${JSON.stringify(crumb.breadcrumb.data)}`);
Expand Down
11 changes: 11 additions & 0 deletions static/app/components/events/eventMessage.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,15 @@ describe('EventMessage', () => {
);
expect(screen.getByText('Unhandled')).toBeInTheDocument();
});

it('renders colored text without escape codes when given an ANSI message', () => {
const message = '\x1B[31mfailed\x1B[0m to connect';

render(<EventMessage message={message} type={EventOrGroupType.ERROR} />);

expect(screen.getByText('failed').style.color).toContain('color-mix(in srgb,');
expect(screen.getByText('failed').parentElement).toHaveTextContent(
/^failed to connect$/
);
});
});
5 changes: 4 additions & 1 deletion static/app/components/events/eventMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import styled from '@emotion/styled';

import {AnsiText} from 'sentry/components/ansiText';
import {ErrorLevel} from 'sentry/components/events/errorLevel';
import {UnhandledTag} from 'sentry/components/group/inboxBadges/unhandledTag';
import {t} from 'sentry/locale';
Expand All @@ -24,7 +25,9 @@ export function EventMessage({
}: Props) {
const showEventLevel = level && eventTypeHasLogLevel(type);
const renderedMessage = message ? (
<Message>{message}</Message>
<Message>
{typeof message === 'string' ? <AnsiText>{message}</AnsiText> : message}
</Message>
) : (
<NoMessage>({t('No error message')})</NoMessage>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {applyBreadcrumbSearch} from 'sentry/components/events/interfaces/breadcrumbs';
import type {BreadcrumbWithMeta} from 'sentry/components/events/interfaces/breadcrumbs/types';
import {BreadcrumbLevelType, BreadcrumbType} from 'sentry/types/breadcrumbs';

function createCrumb(id: number, message: string): BreadcrumbWithMeta {
return {
breadcrumb: {
id,
description: 'Debug',
variant: 'muted',
type: BreadcrumbType.DEBUG,
level: BreadcrumbLevelType.INFO,
message,
},
meta: {},
};
}

describe('applyBreadcrumbSearch', () => {
it('matches across escape codes when a message contains ANSI', () => {
const ansiCrumb = createCrumb(0, '\x1B[31mfailed\x1B[0m to connect');
const otherCrumb = createCrumb(1, 'connected');

const result = applyBreadcrumbSearch([ansiCrumb, otherCrumb], 'failed to');

expect(result).toEqual([ansiCrumb]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import pick from 'lodash/pick';

import type {EnhancedCrumb} from 'sentry/components/events/breadcrumbs/utils';
import {t} from 'sentry/locale';
import {stripAnsi} from 'sentry/utils/ansiEscapeCodes';
import {defined} from 'sentry/utils/defined';

import type {BreadcrumbWithMeta} from './types';
Expand Down Expand Up @@ -40,7 +41,8 @@ export function applyBreadcrumbSearch<T extends BreadcrumbListType>(
pick(breadcrumb, ['type', 'category', 'message', 'level', 'timestamp', 'data'])
).some(key => {
// @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
const info = breadcrumb[key];
const value = breadcrumb[key];
const info = typeof value === 'string' ? stripAnsi(value) : value;

if (!defined(info) || !String(info).trim()) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,42 @@ describe('Exception Content', () => {
expect(screen.queryByRole('button', {name: 'View Section'})).not.toBeInTheDocument();
});

it('renders colored text without escape codes when given an ANSI exception value', () => {
const event = EventFixture({
projectID: project.id,
entries: [
{
type: EntryType.EXCEPTION,
data: {
values: [
{
type: 'ValueError',
value: '\x1B[31mfailed\x1B[0m to connect',
stacktrace: {frames: [EventStacktraceFrameFixture()]},
},
],
},
},
],
});

render(
<Content
type={StackType.ORIGINAL}
stackView={StackView.APP}
event={event}
values={event.entries[0]!.data.values}
projectSlug={project.slug}
newestFirst
/>
);

expect(screen.getByText('failed').style.color).toContain('color-mix(in srgb,');
expect(screen.getByText('failed').closest('pre')).toHaveTextContent(
/^failed to connect$/
);
});

describe('exception groups', () => {
const event = EventFixture({
entries: [EventEntryExceptionGroupFixture()],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {Button} from '@sentry/scraps/button';
import {Container} from '@sentry/scraps/layout';
import {Tooltip} from '@sentry/scraps/tooltip';

import {AnsiText} from 'sentry/components/ansiText';
import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {StacktraceBanners} from 'sentry/components/events/interfaces/crashContent/exception/banners/stacktraceBanners';
import {
Expand Down Expand Up @@ -178,9 +179,11 @@ function InnerContent({
const exceptionValue =
type === StackType.ORIGINAL ? exception.value : exception.rawValue || exception.value;

const renderedExceptionValue = exceptionValue
? renderLinksInText({exceptionText: exceptionValue})
: null;
const renderedExceptionValue = exceptionValue ? (
<AnsiText renderText={text => renderLinksInText({exceptionText: text})}>
{exceptionValue}
</AnsiText>
) : null;
const platform = getStacktracePlatform(event, exception.stacktrace);

// The banners should appear on the top exception only
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {Placeholder} from 'sentry/components/placeholder';
import type {Event, ExceptionType} from 'sentry/types/event';
import type {PlatformKey} from 'sentry/types/platform';
import type {Project} from 'sentry/types/project';
import {stripAnsi} from 'sentry/utils/ansiEscapeCodes';
import {apiOptions} from 'sentry/utils/api/apiOptions';
import {useOrganization} from 'sentry/utils/useOrganization';

Expand Down Expand Up @@ -94,7 +95,7 @@ export function RawContent({
})
) : (
<div>
{exceptionType}: {exceptionValue}
{exceptionType}: {exceptionValue && stripAnsi(exceptionValue)}
</div>
);
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ describe('ExceptionGroupContext', () => {
).toBeInTheDocument();
});

it('renders colored text without escape codes when given ANSI exception values', () => {
const allExceptions = defaultProps.allExceptions.map(exception =>
exception.type === 'TypeError'
? {...exception, value: '\x1B[31mnested\x1B[0m failure'}
: exception
);

render(<RelatedExceptions {...defaultProps} allExceptions={allExceptions} />);

expect(
screen.getByRole('button', {name: 'TypeError: nested failure'})
).toBeInTheDocument();
expect(screen.getByText('nested').style.color).toContain('color-mix(in srgb,');
});

it('does not render for sub-exception', () => {
const {container} = render(
<RelatedExceptions {...defaultProps} mechanism={typeErrorMechanism} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import styled from '@emotion/styled';

import {Button} from '@sentry/scraps/button';

import {AnsiText} from 'sentry/components/ansiText';
import {t} from 'sentry/locale';
import type {ExceptionValue} from 'sentry/types/event';
import type {StackTraceMechanism} from 'sentry/types/stacktrace';
Expand Down Expand Up @@ -38,10 +39,20 @@ type ExceptionTreeItemProps = {

function getExceptionName(exception: ExceptionValue) {
if (exception.type) {
return exception.value ? `${exception.type}: ${exception.value}` : exception.type;
return exception.value ? (
<span>
{exception.type}: <AnsiText>{exception.value}</AnsiText>
</span>
) : (
exception.type
);
}

return exception.value ?? t('Exception');
return defined(exception.value) ? (
<AnsiText>{exception.value}</AnsiText>
) : (
t('Exception')
);
}

function ExceptionLink({exception, link, onExceptionClick}: ExceptionLinkProps) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,26 @@ describe('RawStacktraceContent', () => {
})
).toBe('IllegalStateException: Oops!');
});

it('strips ANSI codes when the exception value contains them', () => {
const exception = ExceptionValueFixture({
type: 'ValueError',
value: '\x1B[31mfailed\x1B[0m to connect',
});

expect(
displayRawContent({
data: {
hasSystemFrames: false,
framesOmitted: null,
registers: {},
frames: [],
},
platform: 'javascript',
exception,
})
).toBe('ValueError: failed to connect');
});
});

describe('render()', () => {
Expand Down
Loading
Loading