Skip to content

Commit 7d7b8ad

Browse files
authored
fix(utils): Truncate aggregate exception values (LinkedErrors) (#8593)
Aggregate exceptions' `exception.value` strings weren't truncated correctly in our event preparation pipeline. The tricky part here is that the `LinkedErrors` integration adds the linked/child exceptions to the event in an event processor which is applied only after we truncate the original event. Hence, we need to also truncate the values in the event processor itself.
1 parent da07542 commit 7d7b8ad

File tree

6 files changed

+147
-21
lines changed

6 files changed

+147
-21
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const wat = new Error(`This is a very long message that should be truncated and will be,
2+
this is a very long message that should be truncated and will be,
3+
this is a very long message that should be truncated and will be,
4+
this is a very long message that should be truncated and will be,
5+
this is a very long message that should be truncated and will be`);
6+
7+
wat.cause = new Error(`This is a very long message that should be truncated and hopefully will be,
8+
this is a very long message that should be truncated and hopefully will be,
9+
this is a very long message that should be truncated and hopefully will be,
10+
this is a very long message that should be truncated and hopefully will be,
11+
this is a very long message that should be truncated and hopefully will be,`);
12+
13+
Sentry.captureException(wat);
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers';
6+
7+
sentryTest('should capture a linked error with messages', async ({ getLocalTestPath, page }) => {
8+
const url = await getLocalTestPath({ testDir: __dirname });
9+
10+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
11+
12+
expect(eventData.exception?.values).toHaveLength(2);
13+
expect(eventData.exception?.values?.[0]).toMatchObject({
14+
type: 'Error',
15+
value: `This is a very long message that should be truncated and hopefully will be,
16+
this is a very long message that should be truncated and hopefully will be,
17+
this is a very long message that should be truncated and hopefully will be,
18+
this is a very long me...`,
19+
mechanism: {
20+
type: 'chained',
21+
handled: true,
22+
},
23+
stacktrace: {
24+
frames: expect.any(Array),
25+
},
26+
});
27+
expect(eventData.exception?.values?.[1]).toMatchObject({
28+
type: 'Error',
29+
value: `This is a very long message that should be truncated and will be,
30+
this is a very long message that should be truncated and will be,
31+
this is a very long message that should be truncated and will be,
32+
this is a very long message that should be truncated...`,
33+
mechanism: {
34+
type: 'generic',
35+
handled: true,
36+
},
37+
stacktrace: {
38+
frames: expect.any(Array),
39+
},
40+
});
41+
});

packages/browser/src/integrations/linkederrors.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,11 @@ export class LinkedErrors implements Integration {
5454
return event;
5555
}
5656

57+
const options = client.getOptions();
5758
applyAggregateErrorsToEvent(
5859
exceptionFromError,
59-
client.getOptions().stackParser,
60+
options.stackParser,
61+
options.maxValueLength,
6062
self._key,
6163
self._limit,
6264
event,

packages/node/src/integrations/linkederrors.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,12 @@ export class LinkedErrors implements Integration {
5050
return event;
5151
}
5252

53+
const options = client.getOptions();
54+
5355
applyAggregateErrorsToEvent(
5456
exceptionFromError,
55-
client.getOptions().stackParser,
57+
options.stackParser,
58+
options.maxValueLength,
5659
self._key,
5760
self._limit,
5861
event,

packages/utils/src/aggregate-errors.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import type { Event, EventHint, Exception, ExtendedError, StackParser } from '@sentry/types';
22

33
import { isInstanceOf } from './is';
4+
import { truncate } from './string';
45

56
/**
67
* Creates exceptions inside `event.exception.values` for errors that are nested on properties based on the `key` parameter.
78
*/
89
export function applyAggregateErrorsToEvent(
910
exceptionFromErrorImplementation: (stackParser: StackParser, ex: Error) => Exception,
1011
parser: StackParser,
12+
maxValueLimit: number = 250,
1113
key: string,
1214
limit: number,
1315
event: Event,
@@ -23,15 +25,18 @@ export function applyAggregateErrorsToEvent(
2325

2426
// We only create exception grouping if there is an exception in the event.
2527
if (originalException) {
26-
event.exception.values = aggregateExceptionsFromError(
27-
exceptionFromErrorImplementation,
28-
parser,
29-
limit,
30-
hint.originalException as ExtendedError,
31-
key,
32-
event.exception.values,
33-
originalException,
34-
0,
28+
event.exception.values = truncateAggregateExceptions(
29+
aggregateExceptionsFromError(
30+
exceptionFromErrorImplementation,
31+
parser,
32+
limit,
33+
hint.originalException as ExtendedError,
34+
key,
35+
event.exception.values,
36+
originalException,
37+
0,
38+
),
39+
maxValueLimit,
3540
);
3641
}
3742
}
@@ -123,3 +128,17 @@ function applyExceptionGroupFieldsForChildException(
123128
parent_id: parentId,
124129
};
125130
}
131+
132+
/**
133+
* Truncate the message (exception.value) of all exceptions in the event.
134+
* Because this event processor is ran after `applyClientOptions`,
135+
* we need to truncate the message of the added exceptions here.
136+
*/
137+
function truncateAggregateExceptions(exceptions: Exception[], maxValueLength: number): Exception[] {
138+
return exceptions.map(exception => {
139+
if (exception.value) {
140+
exception.value = truncate(exception.value, maxValueLength);
141+
}
142+
return exception;
143+
});
144+
}

packages/utils/test/aggregate-errors.test.ts

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ describe('applyAggregateErrorsToEvent()', () => {
1111
test('should not do anything if event does not contain an exception', () => {
1212
const event: Event = { exception: undefined };
1313
const eventHint: EventHint = { originalException: new Error() };
14-
applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint);
14+
applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, 'cause', 100, event, eventHint);
1515

1616
// no changes
1717
expect(event).toStrictEqual({ exception: undefined });
@@ -20,15 +20,15 @@ describe('applyAggregateErrorsToEvent()', () => {
2020
test('should not do anything if event does not contain exception values', () => {
2121
const event: Event = { exception: { values: undefined } };
2222
const eventHint: EventHint = { originalException: new Error() };
23-
applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint);
23+
applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, 'cause', 100, event, eventHint);
2424

2525
// no changes
2626
expect(event).toStrictEqual({ exception: { values: undefined } });
2727
});
2828

2929
test('should not do anything if event does not contain an event hint', () => {
3030
const event: Event = { exception: { values: [] } };
31-
applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, undefined);
31+
applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, 'cause', 100, event, undefined);
3232

3333
// no changes
3434
expect(event).toStrictEqual({ exception: { values: [] } });
@@ -37,7 +37,7 @@ describe('applyAggregateErrorsToEvent()', () => {
3737
test('should not do anything if the event hint does not contain an original exception', () => {
3838
const event: Event = { exception: { values: [] } };
3939
const eventHint: EventHint = { originalException: undefined };
40-
applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint);
40+
applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, 'cause', 100, event, eventHint);
4141

4242
// no changes
4343
expect(event).toStrictEqual({ exception: { values: [] } });
@@ -52,7 +52,7 @@ describe('applyAggregateErrorsToEvent()', () => {
5252
const event: Event = { exception: { values: [exceptionFromError(stackParser, originalException)] } };
5353
const eventHint: EventHint = { originalException };
5454

55-
applyAggregateErrorsToEvent(exceptionFromError, stackParser, key, 100, event, eventHint);
55+
applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, key, 100, event, eventHint);
5656
expect(event).toStrictEqual({
5757
exception: {
5858
values: [
@@ -97,7 +97,7 @@ describe('applyAggregateErrorsToEvent()', () => {
9797
const event: Event = { exception: { values: [exceptionFromError(stackParser, originalException)] } };
9898
const eventHint: EventHint = { originalException };
9999

100-
applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint);
100+
applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, 'cause', 100, event, eventHint);
101101

102102
// no changes
103103
expect(event).toStrictEqual({ exception: { values: [exceptionFromError(stackParser, originalException)] } });
@@ -116,7 +116,7 @@ describe('applyAggregateErrorsToEvent()', () => {
116116
}
117117

118118
const eventHint: EventHint = { originalException };
119-
applyAggregateErrorsToEvent(exceptionFromError, stackParser, key, 5, event, eventHint);
119+
applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, key, 5, event, eventHint);
120120

121121
// 6 -> one for original exception + 5 linked
122122
expect(event.exception?.values).toHaveLength(5 + 1);
@@ -140,7 +140,7 @@ describe('applyAggregateErrorsToEvent()', () => {
140140
const event: Event = { exception: { values: [exceptionFromError(stackParser, fakeAggregateError)] } };
141141
const eventHint: EventHint = { originalException: fakeAggregateError };
142142

143-
applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint);
143+
applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, 'cause', 100, event, eventHint);
144144
expect(event.exception?.values?.[event.exception.values.length - 1].mechanism?.type).toBe('instrument');
145145
});
146146

@@ -155,7 +155,7 @@ describe('applyAggregateErrorsToEvent()', () => {
155155
const event: Event = { exception: { values: [exceptionFromError(stackParser, fakeAggregateError1)] } };
156156
const eventHint: EventHint = { originalException: fakeAggregateError1 };
157157

158-
applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint);
158+
applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, 'cause', 100, event, eventHint);
159159
expect(event).toStrictEqual({
160160
exception: {
161161
values: [
@@ -234,7 +234,7 @@ describe('applyAggregateErrorsToEvent()', () => {
234234
const event: Event = { exception: { values: [exceptionFromError(stackParser, originalException)] } };
235235
const eventHint: EventHint = { originalException };
236236

237-
applyAggregateErrorsToEvent(exceptionFromError, stackParser, key, 100, event, eventHint);
237+
applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, key, 100, event, eventHint);
238238
expect(event).toStrictEqual({
239239
exception: {
240240
values: [
@@ -272,4 +272,52 @@ describe('applyAggregateErrorsToEvent()', () => {
272272
},
273273
});
274274
});
275+
276+
test('should truncate the exception values if they exceed the `maxValueLength` option', () => {
277+
const originalException: ExtendedError = new Error('Root Error with long message');
278+
originalException.cause = new Error('Nested Error 1 with longer message');
279+
originalException.cause.cause = new Error('Nested Error 2 with longer message with longer message');
280+
281+
const event: Event = { exception: { values: [exceptionFromError(stackParser, originalException)] } };
282+
const eventHint: EventHint = { originalException };
283+
284+
const maxValueLength = 15;
285+
applyAggregateErrorsToEvent(exceptionFromError, stackParser, maxValueLength, 'cause', 10, event, eventHint);
286+
expect(event).toStrictEqual({
287+
exception: {
288+
values: [
289+
{
290+
value: 'Nested Error 2 ...',
291+
mechanism: {
292+
exception_id: 2,
293+
handled: true,
294+
parent_id: 1,
295+
source: 'cause',
296+
type: 'chained',
297+
},
298+
},
299+
{
300+
value: 'Nested Error 1 ...',
301+
mechanism: {
302+
exception_id: 1,
303+
handled: true,
304+
parent_id: 0,
305+
is_exception_group: true,
306+
source: 'cause',
307+
type: 'chained',
308+
},
309+
},
310+
{
311+
value: 'Root Error with...',
312+
mechanism: {
313+
exception_id: 0,
314+
handled: true,
315+
is_exception_group: true,
316+
type: 'instrument',
317+
},
318+
},
319+
],
320+
},
321+
});
322+
});
275323
});

0 commit comments

Comments
 (0)