Skip to content
Merged
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
72 changes: 72 additions & 0 deletions workers/grouper/src/data-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ const MAX_TRAVERSAL_DEPTH = 20;
*/
const MAX_TITLE_LENGTH = 400;

/**
* Maximum number of backtrace frames to keep.
* Deep Rails stacks (200+ frames with sourceCode) inflate stored events and list API responses.
*/
const MAX_BACKTRACE_FRAMES = 20;

/**
* Maximum sourceCode lines kept per backtrace frame
*/
const MAX_SOURCE_CODE_LINES = 21;

/**
* Maximum length of a single source code line
*/
const MAX_CODE_LINE_LENGTH = 140;
Comment on lines +27 to +30

/**
* Recursively iterate through object and call function on each key
*
Expand Down Expand Up @@ -143,6 +159,7 @@ export default class DataFilter {
*/
public processEvent(event: EventData<EventAddons>): void {
this.trimEventTitle(event);
this.trimBacktrace(event);
this.sanitizeEvent(event);

unsafeFields.forEach(field => {
Expand All @@ -164,6 +181,54 @@ export default class DataFilter {
}
}

/**
* Cap backtrace frames and sourceCode size.
* It mutates the original object.
*
* @param event - event to process
*/
public trimBacktrace(event: EventData<EventAddons>): void {
if (!Array.isArray(event.backtrace)) {
return;
}

if (event.backtrace.length === 0) {
/**
* Normalize empty backtrace — empty arrays lead to visual bugs
*/
delete event.backtrace;

return;
}

if (event.backtrace.length > MAX_BACKTRACE_FRAMES) {
event.backtrace = event.backtrace.slice(0, MAX_BACKTRACE_FRAMES);
}

event.backtrace.forEach((frame) => {
if (!frame || !Array.isArray(frame.sourceCode)) {
return;
}

const sourceCode = frame.sourceCode.length > MAX_SOURCE_CODE_LINES
? frame.sourceCode.slice(0, MAX_SOURCE_CODE_LINES)
: frame.sourceCode;

frame.sourceCode = sourceCode.map((line) => {
if (!line || typeof line !== 'object') {
return line;
}

return {
...line,
content: typeof line.content === 'string'
? rightTrim(line.content, MAX_CODE_LINE_LENGTH)
: line.content,
};
});
});
}

/**
* Sanitize event fields that can contain long strings, deep objects or long arrays.
* It mutates the original object.
Expand Down Expand Up @@ -275,3 +340,10 @@ export default class DataFilter {
return value;
}
}

export {
MAX_BACKTRACE_FRAMES,
MAX_SOURCE_CODE_LINES,
MAX_CODE_LINE_LENGTH,
MAX_TITLE_LENGTH,
};
49 changes: 0 additions & 49 deletions workers/grouper/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@ import * as WorkerNames from '../../../lib/workerNames';
import * as pkg from '../package.json';
import type { GroupWorkerTask, RepetitionDelta } from '../types/group-worker-task';
import type {
EventAddons,
EventData,
GroupedEventDBScheme,
BacktraceFrame,
SourceCodeLine,
ProjectEventGroupingPatternsDBScheme,
ErrorsCatcherType
} from '@hawk.so/types';
Expand All @@ -24,8 +20,6 @@ import DataFilter from './data-filter';
import RedisHelper from './redisHelper';
import { computeDelta } from './utils/repetitionDiff';
import { bucketTimestampMs } from './utils/bucketTimestamp';
import { rightTrim } from '../../../lib/utils/string';
import { hasValue } from '../../../lib/utils/hasValue';
import GrouperMetrics from './metrics/grouperMetrics';
import GrouperMemoryMonitor from './metrics/memoryMonitor';
import SlowHandleDiagnostics, { SlowHandleSession } from './metrics/slowHandleDiagnostics';
Expand Down Expand Up @@ -58,11 +52,6 @@ const DB_DUPLICATE_KEY_ERROR = '11000';
*/
const DAILY_METRICS_RETENTION_DAYS = 90;

/**
* Maximum length for backtrace code line
*/
const MAX_CODE_LINE_LENGTH = 140;

/**
* Worker for handling Javascript events
*/
Expand Down Expand Up @@ -217,13 +206,6 @@ export default class GrouperWorker extends Worker {
let repetitionId = null;
let incrementDailyAffectedUsers = false;

await session.measureStep('preprocess', () => {
/**
* Trim source code lines to prevent memory leaks
*/
this.trimSourceCodeLines(task.payload);
});

/**
* Find similar events by grouping pattern
*/
Expand Down Expand Up @@ -494,37 +476,6 @@ export default class GrouperWorker extends Worker {
}
}

/**
* Trims source code lines in event's backtrace to prevent memory leaks
*
* @param event - event to process
*/
private trimSourceCodeLines(event: EventData<EventAddons>): void {
if (!event.backtrace) {
return;
}

event.backtrace.forEach((frame: BacktraceFrame) => {
if (!frame.sourceCode) {
return;
}

frame.sourceCode = frame.sourceCode.map((line: SourceCodeLine) => {
return {
line: line.line,
content: hasValue(line.content) ? rightTrim(line.content, MAX_CODE_LINE_LENGTH) : line.content,
};
});
});

/**
* Normalize backtrace, if backtrace equals to [] it leads to visual bugs
*/
if (event.backtrace.length === 0) {
event.backtrace = null;
}
}

/**
* Get unique hash based on event type and title
*
Expand Down
34 changes: 34 additions & 0 deletions workers/grouper/tests/data-filter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -517,4 +517,38 @@ describe('GrouperWorker', () => {
expect(event.context['self']).toBe('<circular>');
});
});

describe('Backtrace trimming', () => {
test('should cap backtrace frames and sourceCode lines', () => {
const longLine = 'x'.repeat(300);
const backtrace = Array.from({ length: 80 }, (_, index) => {
return {
file: `frame-${index}.rb`,
line: index + 1,
sourceCode: Array.from({ length: 25 }, (__, lineIndex) => {
return {
line: lineIndex + 1,
content: longLine,
};
}),
};
});
const event = generateEvent({ backtrace });

dataFilter.processEvent(event);

expect(event.backtrace).toHaveLength(20);
expect(event.backtrace?.[0].sourceCode).toHaveLength(21);
expect(event.backtrace?.[0].sourceCode?.[0].content?.endsWith('…')).toBe(true);
expect(event.backtrace?.[0].sourceCode?.[0].content?.length).toBeLessThanOrEqual(141);
});

test('should normalize empty backtrace to undefined', () => {
const event = generateEvent({ backtrace: [] });

dataFilter.processEvent(event);

expect(event.backtrace).toBeUndefined();
});
});
});
Loading