Skip to content
Draft
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 @@ -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
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 @@ -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
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {trimPackage} from 'sentry/components/events/interfaces/frame/utils';
import type {ExceptionValue, Frame} from 'sentry/types/event';
import type {StacktraceType} from 'sentry/types/stacktrace';
import {stripAnsi} from 'sentry/utils/ansiEscapeCodes';
import {defined} from 'sentry/utils/defined';

function getJavaScriptFrame(
Expand Down Expand Up @@ -386,7 +387,7 @@ export function displayRawContent({
);

if (exception) {
frames.push(getExceptionSummary(exception, platform, isMinified));
frames.push(stripAnsi(getExceptionSummary(exception, platform, isMinified)));
}

// For the raw stacktrace view on the issue details page, ignore newestFirst and order frames based on default platform behavior
Expand Down
27 changes: 27 additions & 0 deletions static/app/components/stackTrace/issueStackTrace/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,33 @@ describe('IssueStackTrace', () => {
`);
});

it('copies stack trace text without ANSI codes when the exception value contains them', async () => {
const {event, stacktrace} = makeCopyTestData();

render(
<IssueStackTrace
event={event}
values={[
{
type: 'ValueError',
value: '\x1B[31mlist index\x1B[0m out of range',
module: null,
mechanism: {handled: false, type: 'generic'},
stacktrace,
rawStacktrace: null,
threadId: null,
},
]}
/>
);

await userEvent.click(screen.getByRole('button', {name: 'Copy as'}));
await userEvent.click(await screen.findByRole('menuitemradio', {name: 'Text'}));

const copiedText = jest.mocked(navigator.clipboard.writeText).mock.calls[0]![0];
expect(copiedText).toContain('ValueError: list index out of range');
});

it('copies stack trace text including exception type and value for chained exceptions', async () => {
const {event, stacktrace} = makeCopyTestData();

Expand Down
11 changes: 11 additions & 0 deletions static/app/utils/getStacktraceBody.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ Error: an error occurred`,
expect(result).toEqual(['Something is broken']);
});

it('strips ANSI codes when the message contains them', () => {
const result = getStacktraceBody({
event: EventFixture({
entries: [
{type: 'message', data: {formatted: '\x1B[31mSomething\x1B[0m is broken'}},
],
}),
});
expect(result).toEqual(['Something is broken']);
});

it('formats with a thread', () => {
const result = getStacktraceBody({event: eventThreads});
expect(result).toEqual([
Expand Down
3 changes: 2 additions & 1 deletion static/app/utils/getStacktraceBody.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {displayRawContent as rawStacktraceContent} from 'sentry/components/events/interfaces/crashContent/stackTrace/rawContent';
import type {Event} from 'sentry/types/event';
import {stripAnsi} from 'sentry/utils/ansiEscapeCodes';

type GetStacktraceBodyArgs = {
/**
Expand Down Expand Up @@ -51,7 +52,7 @@ export function getStacktraceBody({
if (!msg) {
return [];
}
return msg?.data?.formatted && [msg.data.formatted];
return msg?.data?.formatted && [stripAnsi(msg.data.formatted)];
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,27 @@ describe('useCopyIssueDetails', () => {
expect(result).not.toContain('## Message');
});

it('strips ANSI codes from the title and message when they contain them', () => {
const result = issueAndEventToMarkdown({
group: GroupFixture({title: '\x1B[31mTypeError\x1B[0m: connection failed'}),
event: EventFixture({...event, message: '\x1B[33mretrying\x1B[0m in 5s'}),
organization,
});

expect(result).toContain('# TypeError: connection failed\n');
expect(result).toContain('## Message\n\nretrying in 5s\n');
});

it('omits the message when it is part of the title after stripping ANSI codes', () => {
const result = issueAndEventToMarkdown({
group: GroupFixture({title: 'TypeError: \x1B[31mconnection failed\x1B[0m'}),
event: EventFixture({...event, message: '\x1B[31mconnection failed\x1B[0m'}),
organization,
});

expect(result).not.toContain('## Message');
});

it('omits the message when it is empty', () => {
const result = issueAndEventToMarkdown({
group: GroupFixture({title: 'TypeError'}),
Expand Down Expand Up @@ -298,6 +319,28 @@ describe('useCopyIssueDetails', () => {
expect(result).not.toContain('**Handled:**');
});

it('strips ANSI codes from exception values when they contain them', () => {
const eventWithException = EventFixture({
...event,
entries: [
{
type: EntryType.EXCEPTION,
data: {
values: [{type: 'ValueError', value: '\x1B[31mfailed\x1B[0m to connect'}],
},
},
],
});

const result = issueAndEventToMarkdown({
group,
event: eventWithException,
organization,
});

expect(result).toContain('**Value:** failed to connect\n');
});

it('marks an unhandled exception', () => {
const eventWithUnhandled = EventFixture({
...event,
Expand Down Expand Up @@ -562,6 +605,35 @@ describe('useCopyIssueDetails', () => {
expect(result).toContain(' User clicked submit');
});

it('strips ANSI codes from breadcrumb messages when they contain them', () => {
const eventWithBreadcrumbs = EventFixture({
...event,
entries: [
{
type: EntryType.BREADCRUMBS,
data: {
values: [
{
type: 'default',
category: 'console',
level: 'info',
message: '\x1B[32mserver started\x1B[0m',
},
],
},
},
],
});

const result = issueAndEventToMarkdown({
group,
event: eventWithBreadcrumbs,
organization,
});

expect(result).toContain('- **default** `console` [info]\n server started\n');
});

it('truncates a single breadcrumb to the per-crumb character limit', () => {
const longMessage = 'x'.repeat(600);
const eventWithLongBreadcrumb = EventFixture({
Expand Down
12 changes: 7 additions & 5 deletions static/app/views/issueDetails/hooks/useCopyIssueDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {Group} from 'sentry/types/group';
import type {Organization} from 'sentry/types/organization';
import type {StacktraceType} from 'sentry/types/stacktrace';
import {trackAnalytics} from 'sentry/utils/analytics';
import {stripAnsi} from 'sentry/utils/ansiEscapeCodes';
import {getFormat, getUserTimezone} from 'sentry/utils/dates';
import {useCopyToClipboard} from 'sentry/utils/useCopyToClipboard';
import {useOrganization} from 'sentry/utils/useOrganization';
Expand Down Expand Up @@ -99,7 +100,7 @@ function formatBreadcrumbsToMarkdown(crumbs: RawCrumb[]): string {
const entries: string[] = [];

crumbs.slice(-MAX_BREADCRUMBS).forEach(crumb => {
const message = crumb.message ?? '';
const message = stripAnsi(crumb.message ?? '');

// Drop empty values, matching Seer's `{k: v for k, v in data if v}`.
const data = crumb.data
Expand Down Expand Up @@ -212,7 +213,7 @@ function formatEventToMarkdown(event: Event, activeThreadId: number | undefined)
markdownText += `**Handled:** ${handled ? 'Yes' : 'No'}\n`;
}
if (exception.value) {
markdownText += `**Value:** ${exception.value}\n\n`;
markdownText += `**Value:** ${stripAnsi(exception.value)}\n\n`;
}

// Add stacktrace if available
Expand Down Expand Up @@ -285,7 +286,8 @@ export const issueAndEventToMarkdown = ({
}

// Format the basic issue information
let markdownText = `# ${group.title}\n\n`;
const title = stripAnsi(group.title);
let markdownText = `# ${title}\n\n`;
markdownText += `**Issue ID:** ${group.id}\n`;

if (group.shortId) {
Expand Down Expand Up @@ -313,8 +315,8 @@ export const issueAndEventToMarkdown = ({

// Mirror Seer: include the event message only when it adds something beyond
// the title, since for most errors the title already is the message.
const message = event?.message?.trim();
if (message && !group.title.includes(message)) {
const message = stripAnsi(event?.message ?? '').trim();
if (message && !title.includes(message)) {
markdownText += `\n## Message\n\n${message}\n`;
}

Expand Down
Loading