|
1 | 1 | import { beforeEach, describe, expect, it, vi } from 'vitest'; |
2 | 2 | import { fmt, Scope } from '../../../src'; |
3 | | -import { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer, _INTERNAL_getLogBuffer } from '../../../src/logs/internal'; |
| 3 | +import { |
| 4 | + _INTERNAL_captureLog, |
| 5 | + _INTERNAL_flushLogsBuffer, |
| 6 | + _INTERNAL_getLogBuffer, |
| 7 | + _INTERNAL_removeLoneSurrogates, |
| 8 | +} from '../../../src/logs/internal'; |
4 | 9 | import type { Log } from '../../../src/types-hoist/log'; |
5 | 10 | import * as loggerModule from '../../../src/utils/debug-logger'; |
6 | 11 | import * as timeModule from '../../../src/utils/time'; |
@@ -1261,4 +1266,134 @@ describe('_INTERNAL_captureLog', () => { |
1261 | 1266 | expect(buffer2?.[0]?.attributes?.['sentry.timestamp.sequence']).toEqual({ value: 0, type: 'integer' }); |
1262 | 1267 | }); |
1263 | 1268 | }); |
| 1269 | + |
| 1270 | + describe('lone surrogate sanitization', () => { |
| 1271 | + it('sanitizes lone surrogates in log message body', () => { |
| 1272 | + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); |
| 1273 | + const client = new TestClient(options); |
| 1274 | + const scope = new Scope(); |
| 1275 | + scope.setClient(client); |
| 1276 | + |
| 1277 | + _INTERNAL_captureLog({ level: 'error', message: 'bad surrogate \uD800 here' }, scope); |
| 1278 | + |
| 1279 | + const logBuffer = _INTERNAL_getLogBuffer(client); |
| 1280 | + expect(logBuffer?.[0]?.body).toBe('bad surrogate \uFFFD here'); |
| 1281 | + }); |
| 1282 | + |
| 1283 | + it('sanitizes lone surrogates in log attribute values', () => { |
| 1284 | + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); |
| 1285 | + const client = new TestClient(options); |
| 1286 | + const scope = new Scope(); |
| 1287 | + scope.setClient(client); |
| 1288 | + |
| 1289 | + _INTERNAL_captureLog( |
| 1290 | + { |
| 1291 | + level: 'error', |
| 1292 | + message: 'test', |
| 1293 | + attributes: { bad: '{"a":"\uD800"}' }, |
| 1294 | + }, |
| 1295 | + scope, |
| 1296 | + ); |
| 1297 | + |
| 1298 | + const logBuffer = _INTERNAL_getLogBuffer(client); |
| 1299 | + expect(logBuffer?.[0]?.attributes?.['bad']).toEqual({ |
| 1300 | + value: '{"a":"\uFFFD"}', |
| 1301 | + type: 'string', |
| 1302 | + }); |
| 1303 | + }); |
| 1304 | + |
| 1305 | + it('sanitizes lone surrogates in log attribute keys', () => { |
| 1306 | + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); |
| 1307 | + const client = new TestClient(options); |
| 1308 | + const scope = new Scope(); |
| 1309 | + scope.setClient(client); |
| 1310 | + |
| 1311 | + _INTERNAL_captureLog( |
| 1312 | + { |
| 1313 | + level: 'error', |
| 1314 | + message: 'test', |
| 1315 | + attributes: { ['bad\uD800key']: 'value' }, |
| 1316 | + }, |
| 1317 | + scope, |
| 1318 | + ); |
| 1319 | + |
| 1320 | + const logBuffer = _INTERNAL_getLogBuffer(client); |
| 1321 | + expect(logBuffer?.[0]?.attributes?.['bad\uFFFDkey']).toEqual({ |
| 1322 | + value: 'value', |
| 1323 | + type: 'string', |
| 1324 | + }); |
| 1325 | + }); |
| 1326 | + |
| 1327 | + it('preserves valid emoji in log messages and attributes', () => { |
| 1328 | + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); |
| 1329 | + const client = new TestClient(options); |
| 1330 | + const scope = new Scope(); |
| 1331 | + scope.setClient(client); |
| 1332 | + |
| 1333 | + _INTERNAL_captureLog( |
| 1334 | + { |
| 1335 | + level: 'info', |
| 1336 | + message: 'hello 😀 world', |
| 1337 | + attributes: { emoji: '🎉 party' }, |
| 1338 | + }, |
| 1339 | + scope, |
| 1340 | + ); |
| 1341 | + |
| 1342 | + const logBuffer = _INTERNAL_getLogBuffer(client); |
| 1343 | + expect(logBuffer?.[0]?.body).toBe('hello 😀 world'); |
| 1344 | + expect(logBuffer?.[0]?.attributes?.['emoji']).toEqual({ |
| 1345 | + value: '🎉 party', |
| 1346 | + type: 'string', |
| 1347 | + }); |
| 1348 | + }); |
| 1349 | + }); |
| 1350 | +}); |
| 1351 | + |
| 1352 | +describe('_INTERNAL_removeLoneSurrogates', () => { |
| 1353 | + it('returns the same string when there are no surrogates', () => { |
| 1354 | + expect(_INTERNAL_removeLoneSurrogates('hello world')).toBe('hello world'); |
| 1355 | + }); |
| 1356 | + |
| 1357 | + it('returns the same string for empty input', () => { |
| 1358 | + expect(_INTERNAL_removeLoneSurrogates('')).toBe(''); |
| 1359 | + }); |
| 1360 | + |
| 1361 | + it('preserves valid surrogate pairs (emoji)', () => { |
| 1362 | + expect(_INTERNAL_removeLoneSurrogates('hello 😀 world')).toBe('hello 😀 world'); |
| 1363 | + }); |
| 1364 | + |
| 1365 | + it('replaces a lone high surrogate with U+FFFD', () => { |
| 1366 | + expect(_INTERNAL_removeLoneSurrogates('before\uD800after')).toBe('before\uFFFDafter'); |
| 1367 | + }); |
| 1368 | + |
| 1369 | + it('replaces a lone low surrogate with U+FFFD', () => { |
| 1370 | + expect(_INTERNAL_removeLoneSurrogates('before\uDC00after')).toBe('before\uFFFDafter'); |
| 1371 | + }); |
| 1372 | + |
| 1373 | + it('replaces lone high surrogate at end of string', () => { |
| 1374 | + expect(_INTERNAL_removeLoneSurrogates('end\uD800')).toBe('end\uFFFD'); |
| 1375 | + }); |
| 1376 | + |
| 1377 | + it('replaces lone low surrogate at start of string', () => { |
| 1378 | + expect(_INTERNAL_removeLoneSurrogates('\uDC00start')).toBe('\uFFFDstart'); |
| 1379 | + }); |
| 1380 | + |
| 1381 | + it('replaces multiple lone surrogates', () => { |
| 1382 | + expect(_INTERNAL_removeLoneSurrogates('\uD800\uD801\uDC00')).toBe('\uFFFD\uD801\uDC00'); |
| 1383 | + }); |
| 1384 | + |
| 1385 | + it('handles two consecutive lone high surrogates', () => { |
| 1386 | + expect(_INTERNAL_removeLoneSurrogates('\uD800\uD800')).toBe('\uFFFD\uFFFD'); |
| 1387 | + }); |
| 1388 | + |
| 1389 | + it('handles mixed valid pairs and lone surrogates', () => { |
| 1390 | + expect(_INTERNAL_removeLoneSurrogates('\uD83D\uDE00\uD800')).toBe('😀\uFFFD'); |
| 1391 | + }); |
| 1392 | + |
| 1393 | + it('handles the exact reproduction case from issue #5186', () => { |
| 1394 | + const badValue = '{"a":"\uD800"}'; |
| 1395 | + const result = _INTERNAL_removeLoneSurrogates(badValue); |
| 1396 | + expect(result).toBe('{"a":"\uFFFD"}'); |
| 1397 | + expect(() => JSON.parse(result)).not.toThrow(); |
| 1398 | + }); |
1264 | 1399 | }); |
0 commit comments