Skip to content

Commit 21cb047

Browse files
authored
fix(sentry): replay skipping improved (#503)
* fix sentry replay skipping * lint code * Update index.test.ts
1 parent 8844840 commit 21cb047

File tree

10 files changed

+146
-21
lines changed

10 files changed

+146
-21
lines changed

lib/memoize/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function memoize(options: MemoizeOptions = {}): MethodDecorator {
4545
max = 50,
4646
ttl = 1000 * 60 * 30,
4747
strategy = 'concat',
48-
skipCache = []
48+
skipCache = [],
4949
} = options;
5050
/* eslint-enable */
5151

workers/archiver/src/index.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export default class ArchiverWorker extends Worker {
8686

8787
const projects = await this.projectCollection.find({}).project({
8888
_id: 1,
89-
name: 1
89+
name: 1,
9090
});
9191
const projectsData: ReportDataByProject[] = [];
9292

@@ -155,11 +155,11 @@ export default class ArchiverWorker extends Worker {
155155
await this.projectCollection.updateOne({
156156
_id: project._id,
157157
},
158-
{
159-
$inc: {
160-
archivedEventsCount: deletedCount,
161-
},
162-
});
158+
{
159+
$inc: {
160+
archivedEventsCount: deletedCount,
161+
},
162+
});
163163
}
164164

165165
/**
@@ -351,7 +351,7 @@ export default class ArchiverWorker extends Worker {
351351
this.logger.info('Report notification response:', {
352352
status: response?.status,
353353
statusText: response?.statusText,
354-
data: response?.data
354+
data: response?.data,
355355
});
356356
}
357357

workers/email/scripts/emailOverview.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ class EmailTestServer {
156156
tariffPlanId: '5f47f031ff71510040f433c1',
157157
password: '1as2eadd321a3cDf',
158158
plan: {
159-
name: 'Корпоративный'
159+
name: 'Корпоративный',
160160
},
161161
workspaceName: workspace.name,
162162
};

workers/javascript/tests/index.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,5 +442,4 @@ describe('JavaScript event worker', () => {
442442

443443
await worker.finish();
444444
});
445-
446445
});

workers/limiter/tests/dbHelper.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ describe('DbHelper', () => {
304304
/**
305305
* Act
306306
*/
307-
await dbHelper.updateWorkspacesEventsCountAndIsBlocked([updatedWorkspace]);
307+
await dbHelper.updateWorkspacesEventsCountAndIsBlocked([ updatedWorkspace ]);
308308

309309
/**
310310
* Assert

workers/paymaster/src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,13 +160,17 @@ export default class PaymasterWorker extends Worker {
160160

161161
/**
162162
* Finds plan by id from cached plans
163+
*
164+
* @param planId
163165
*/
164166
private findPlanById(planId: WorkspaceDBScheme['tariffPlanId']): PlanDBScheme | undefined {
165167
return this.plans.find((plan) => plan._id.toString() === planId.toString());
166168
}
167169

168170
/**
169171
* Returns workspace plan, refreshes cache when plan is missing
172+
*
173+
* @param workspace
170174
*/
171175
private async getWorkspacePlan(workspace: WorkspaceDBScheme): Promise<PlanDBScheme> {
172176
let currentPlan = this.findPlanById(workspace.tariffPlanId);
@@ -413,7 +417,6 @@ export default class PaymasterWorker extends Worker {
413417
});
414418
}
415419

416-
417420
/**
418421
* Sends reminder emails to blocked workspace admins
419422
*

workers/paymaster/tests/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@ describe('PaymasterWorker', () => {
317317
}
318318

319319
MockDate.reset();
320+
320321
return addTaskSpy;
321322
};
322323

workers/release/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ export default class ReleaseWorker extends Worker {
281281
/**
282282
* Some bundlers could skip file in the source map content since it duplicates in map name
283283
* Like map name bundle.js.map is a source map for a bundle.js
284+
*
284285
* @see https://sourcemaps.info/spec.html - format
285286
*/
286287
originFileName: mapContent.file ?? file.name.replace(/\.map$/, ''),

workers/sentry/src/index.ts

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,12 @@ export default class SentryEventWorker extends Worker {
6767

6868
/**
6969
* Filter out binary items that crash parseEnvelope
70+
* Also filters out all Sentry Replay events (replay_event and replay_recording)
7071
*/
7172
private filterOutBinaryItems(rawEvent: string): string {
7273
const lines = rawEvent.split('\n');
7374
const filteredLines = [];
75+
let isInReplayBlock = false;
7476

7577
for (let i = 0; i < lines.length; i++) {
7678
const line = lines[i];
@@ -90,17 +92,42 @@ export default class SentryEventWorker extends Worker {
9092
// Try to parse as JSON to check if it's a header
9193
const parsed = JSON.parse(line);
9294

93-
// If it's a replay header, skip this line and the next one (payload)
95+
// Check if this is a replay event type
9496
if (parsed.type === 'replay_recording' || parsed.type === 'replay_event') {
95-
// Skip the next line too (which would be the payload)
96-
i++;
97+
// Mark that we're in a replay block and skip this line
98+
isInReplayBlock = true;
9799
continue;
98100
}
99101

100-
// Keep valid headers and other JSON data
101-
filteredLines.push(line);
102+
// If we're in a replay block, check if this is still part of it
103+
if (isInReplayBlock) {
104+
// Check if this line is part of replay data (segment_id, length, etc.)
105+
if ('segment_id' in parsed || ('length' in parsed && parsed.type !== 'event') || 'replay_id' in parsed) {
106+
// Still in replay block, skip this line
107+
continue;
108+
}
109+
110+
// If it's a new envelope item (like event), we've exited the replay block
111+
if (parsed.type === 'event' || parsed.type === 'transaction' || parsed.type === 'session') {
112+
isInReplayBlock = false;
113+
} else {
114+
// Unknown type, assume we're still in replay block
115+
continue;
116+
}
117+
}
118+
119+
// Keep valid headers and other JSON data (not in replay block)
120+
if (!isInReplayBlock) {
121+
filteredLines.push(line);
122+
}
102123
} catch {
103-
// If line doesn't parse as JSON, it might be binary data - skip it
124+
// If line doesn't parse as JSON, it might be binary data
125+
// If we're in a replay block, skip it (it's part of replay recording)
126+
if (isInReplayBlock) {
127+
continue;
128+
}
129+
130+
// If not in replay block and not JSON, it might be corrupted data - skip it
104131
continue;
105132
}
106133
}

workers/sentry/tests/index.test.ts

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -806,12 +806,18 @@ describe('SentryEventWorker', () => {
806806
event_id: '4c40fee730194a989439a86bf75634111',
807807
sent_at: '2025-08-29T10:59:29.952Z',
808808
/* eslint-enable @typescript-eslint/naming-convention */
809-
sdk: { name: 'sentry.javascript.react', version: '9.10.1' },
809+
sdk: {
810+
name: 'sentry.javascript.react',
811+
version: '9.10.1',
812+
},
810813
}),
811814
// Event item header
812815
JSON.stringify({ type: 'event' }),
813816
// Event item payload
814-
JSON.stringify({ message: 'Test event', level: 'error' }),
817+
JSON.stringify({
818+
message: 'Test event',
819+
level: 'error',
820+
}),
815821
// Replay event item header - should be filtered out
816822
JSON.stringify({ type: 'replay_event' }),
817823
// Replay event item payload - should be filtered out
@@ -822,7 +828,10 @@ describe('SentryEventWorker', () => {
822828
/* eslint-enable @typescript-eslint/naming-convention */
823829
}),
824830
// Replay recording item header - should be filtered out
825-
JSON.stringify({ type: 'replay_recording', length: 343 }),
831+
JSON.stringify({
832+
type: 'replay_recording',
833+
length: 343,
834+
}),
826835
// Replay recording binary payload - should be filtered out
827836
'binary-data-here-that-is-not-json',
828837
];
@@ -841,6 +850,7 @@ describe('SentryEventWorker', () => {
841850
expect(mockedAmqpChannel.sendToQueue).toHaveBeenCalledTimes(1);
842851

843852
const addedTaskPayload = getAddTaskPayloadFromLastCall();
853+
844854
expect(addedTaskPayload).toMatchObject({
845855
payload: expect.objectContaining({
846856
addons: {
@@ -852,6 +862,90 @@ describe('SentryEventWorker', () => {
852862
}),
853863
});
854864
});
865+
866+
it('should ignore envelope with only replay_event and replay_recording items', async () => {
867+
/**
868+
* Test case based on real-world scenario where envelope contains only replay data
869+
* This should not crash with "Unexpected end of JSON input" error
870+
*/
871+
const envelopeLines = [
872+
// Envelope header
873+
JSON.stringify({
874+
/* eslint-disable @typescript-eslint/naming-convention */
875+
event_id: '62680958b3ab4497886375e06533d86a',
876+
sent_at: '2025-12-24T13:16:34.580Z',
877+
/* eslint-enable @typescript-eslint/naming-convention */
878+
sdk: {
879+
name: 'sentry.javascript.react',
880+
version: '10.22.0',
881+
},
882+
}),
883+
// Replay event item header - should be filtered out
884+
JSON.stringify({ type: 'replay_event' }),
885+
// Replay event item payload (large JSON) - should be filtered out
886+
JSON.stringify({
887+
/* eslint-disable @typescript-eslint/naming-convention */
888+
type: 'replay_event',
889+
replay_start_timestamp: 1766582182.757,
890+
timestamp: 1766582194.579,
891+
error_ids: [],
892+
trace_ids: [],
893+
urls: ['https://my.huntio.ru/applicants', 'https://my.huntio.ru/applicants/1270067'],
894+
replay_id: '62680958b3ab4497886375e06533d86a',
895+
segment_id: 1,
896+
replay_type: 'session',
897+
request: {
898+
url: 'https://my.huntio.ru/applicants/1270067',
899+
headers: {
900+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
901+
},
902+
},
903+
event_id: '62680958b3ab4497886375e06533d86a',
904+
environment: 'production',
905+
release: '1.0.7',
906+
sdk: {
907+
integrations: ['InboundFilters', 'FunctionToString', 'BrowserApiErrors', 'Breadcrumbs'],
908+
name: 'sentry.javascript.react',
909+
version: '10.22.0',
910+
settings: { infer_ip: 'auto' },
911+
},
912+
user: {
913+
id: 487,
914+
email: 'npr@unicorn-resources.pro',
915+
username: 'Прохорова Наталья',
916+
},
917+
contexts: { react: { version: '19.1.0' } },
918+
transaction: '/applicants/1270067',
919+
platform: 'javascript',
920+
/* eslint-enable @typescript-eslint/naming-convention */
921+
}),
922+
// Replay recording item header - should be filtered out
923+
JSON.stringify({
924+
type: 'replay_recording',
925+
length: 16385,
926+
}),
927+
/* eslint-disable @typescript-eslint/naming-convention */
928+
// Segment ID - should be filtered out
929+
JSON.stringify({ segment_id: 1 }),
930+
/* eslint-enable @typescript-eslint/naming-convention */
931+
// Binary data (simulated) - should be filtered out
932+
'xnFWy@v$xAlJ=&fS~¾˶IJ<Dڒ%8yX]]ˣ·9V|JGd!+%fF',
933+
];
934+
935+
const envelopeString = envelopeLines.join('\n');
936+
937+
// Should not throw "Unexpected end of JSON input" error
938+
await worker.handle({
939+
payload: {
940+
envelope: b64encode(envelopeString),
941+
},
942+
projectId: '123',
943+
catcherType: 'external/sentry',
944+
});
945+
946+
// Should not send any tasks since all items were replay-related and filtered out
947+
expect(mockedAmqpChannel.sendToQueue).not.toHaveBeenCalled();
948+
});
855949
});
856950

857951
describe('envelope parsing', () => {

0 commit comments

Comments
 (0)