From 1274120609085349ec418402556661b4935dc888 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 13 Apr 2026 14:21:59 +0200 Subject: [PATCH 1/7] fix(core): Sanitize lone surrogates in log body and attributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lone UTF-16 surrogates (U+D800–U+DFFF) in log message bodies or attribute strings cause serde_json on the server to reject the entire log batch. This replaces unpaired surrogates with U+FFFD at capture time, scoped to the logs path only. Fixes getsentry/sentry-react-native#5186 Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/src/logs/internal.ts | 83 ++++++++++- packages/core/test/lib/logs/internal.test.ts | 137 ++++++++++++++++++- 2 files changed, 216 insertions(+), 4 deletions(-) diff --git a/packages/core/src/logs/internal.ts b/packages/core/src/logs/internal.ts index 097ffbb6906e..dbb40a9ad81e 100644 --- a/packages/core/src/logs/internal.ts +++ b/packages/core/src/logs/internal.ts @@ -1,3 +1,4 @@ +import type { Attributes } from '../attributes'; import { serializeAttributes } from '../attributes'; import { getGlobalSingleton } from '../carrier'; import type { Client } from '../client'; @@ -161,14 +162,14 @@ export function _INTERNAL_captureLog( const serializedLog: SerializedLog = { timestamp, level, - body: message, + body: typeof message === 'string' ? _INTERNAL_removeLoneSurrogates(message) : message, trace_id: traceContext?.trace_id, severity_number: severityNumber ?? SEVERITY_TEXT_TO_SEVERITY_NUMBER[level], - attributes: { + attributes: sanitizeLogAttributes({ ...serializeAttributes(scopeAttributes), ...serializeAttributes(logAttributes, true), [sequenceAttr.key]: sequenceAttr.value, - }, + }), }; captureSerializedLog(client, serializedLog); @@ -220,3 +221,79 @@ function _getBufferMap(): WeakMap> { // The reference to the Client <> LogBuffer map is stored on the carrier to ensure it's always the same return getGlobalSingleton('clientToLogBufferMap', () => new WeakMap>()); } + +/** + * Sanitizes serialized log attributes by replacing lone surrogates in both + * keys and string values with U+FFFD. + */ +function sanitizeLogAttributes(attributes: Attributes): Attributes { + const sanitized: Attributes = {}; + for (const [key, attr] of Object.entries(attributes)) { + const sanitizedKey = _INTERNAL_removeLoneSurrogates(key); + if (attr.type === 'string') { + sanitized[sanitizedKey] = { ...attr, value: _INTERNAL_removeLoneSurrogates(attr.value as string) }; + } else { + sanitized[sanitizedKey] = attr; + } + } + return sanitized; +} + +/** + * Replaces unpaired UTF-16 surrogates with U+FFFD (replacement character). + * + * Lone surrogates (U+D800–U+DFFF not part of a valid pair) cause `serde_json` + * on the server to reject the entire log/span batch when they appear in + * JSON-escaped form (e.g. `\uD800`). Replacing them at the SDK level ensures + * only the offending characters are lost instead of the whole payload. + */ +export function _INTERNAL_removeLoneSurrogates(str: string): string { + // Use native toWellFormed() when available (Node 20+, Safari 15.4+, Chrome 111+) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const s = str as any; + if (typeof s.isWellFormed === 'function') { + return s.isWellFormed() ? str : s.toWellFormed(); + } + + // Fast path – scan without allocating. Most strings have no surrogates at all. + let hasLoneSurrogate = false; + for (let i = 0; i < str.length; i++) { + const code = str.charCodeAt(i); + if (code >= 0xd800 && code <= 0xdfff) { + if (code <= 0xdbff && i + 1 < str.length) { + const next = str.charCodeAt(i + 1); + if (next >= 0xdc00 && next <= 0xdfff) { + // Valid surrogate pair – skip the low surrogate + i++; + continue; + } + } + hasLoneSurrogate = true; + break; + } + } + + if (!hasLoneSurrogate) { + return str; + } + + // Slow path – build a new string, replacing lone surrogates with U+FFFD. + const chars: string[] = []; + for (let i = 0; i < str.length; i++) { + const code = str.charCodeAt(i); + if (code >= 0xd800 && code <= 0xdbff) { + const next = i + 1 < str.length ? str.charCodeAt(i + 1) : 0; + if (next >= 0xdc00 && next <= 0xdfff) { + chars.push(str.charAt(i), str.charAt(i + 1)); + i++; + } else { + chars.push('\uFFFD'); + } + } else if (code >= 0xdc00 && code <= 0xdfff) { + chars.push('\uFFFD'); + } else { + chars.push(str.charAt(i)); + } + } + return chars.join(''); +} diff --git a/packages/core/test/lib/logs/internal.test.ts b/packages/core/test/lib/logs/internal.test.ts index 360485f5ca84..4d444d8934db 100644 --- a/packages/core/test/lib/logs/internal.test.ts +++ b/packages/core/test/lib/logs/internal.test.ts @@ -1,6 +1,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { fmt, Scope } from '../../../src'; -import { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer, _INTERNAL_getLogBuffer } from '../../../src/logs/internal'; +import { + _INTERNAL_captureLog, + _INTERNAL_flushLogsBuffer, + _INTERNAL_getLogBuffer, + _INTERNAL_removeLoneSurrogates, +} from '../../../src/logs/internal'; import type { Log } from '../../../src/types-hoist/log'; import * as loggerModule from '../../../src/utils/debug-logger'; import * as timeModule from '../../../src/utils/time'; @@ -1261,4 +1266,134 @@ describe('_INTERNAL_captureLog', () => { expect(buffer2?.[0]?.attributes?.['sentry.timestamp.sequence']).toEqual({ value: 0, type: 'integer' }); }); }); + + describe('lone surrogate sanitization', () => { + it('sanitizes lone surrogates in log message body', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + _INTERNAL_captureLog({ level: 'error', message: 'bad surrogate \uD800 here' }, scope); + + const logBuffer = _INTERNAL_getLogBuffer(client); + expect(logBuffer?.[0]?.body).toBe('bad surrogate \uFFFD here'); + }); + + it('sanitizes lone surrogates in log attribute values', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + _INTERNAL_captureLog( + { + level: 'error', + message: 'test', + attributes: { bad: '{"a":"\uD800"}' }, + }, + scope, + ); + + const logBuffer = _INTERNAL_getLogBuffer(client); + expect(logBuffer?.[0]?.attributes?.['bad']).toEqual({ + value: '{"a":"\uFFFD"}', + type: 'string', + }); + }); + + it('sanitizes lone surrogates in log attribute keys', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + _INTERNAL_captureLog( + { + level: 'error', + message: 'test', + attributes: { ['bad\uD800key']: 'value' }, + }, + scope, + ); + + const logBuffer = _INTERNAL_getLogBuffer(client); + expect(logBuffer?.[0]?.attributes?.['bad\uFFFDkey']).toEqual({ + value: 'value', + type: 'string', + }); + }); + + it('preserves valid emoji in log messages and attributes', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + _INTERNAL_captureLog( + { + level: 'info', + message: 'hello 😀 world', + attributes: { emoji: '🎉 party' }, + }, + scope, + ); + + const logBuffer = _INTERNAL_getLogBuffer(client); + expect(logBuffer?.[0]?.body).toBe('hello 😀 world'); + expect(logBuffer?.[0]?.attributes?.['emoji']).toEqual({ + value: '🎉 party', + type: 'string', + }); + }); + }); +}); + +describe('_INTERNAL_removeLoneSurrogates', () => { + it('returns the same string when there are no surrogates', () => { + expect(_INTERNAL_removeLoneSurrogates('hello world')).toBe('hello world'); + }); + + it('returns the same string for empty input', () => { + expect(_INTERNAL_removeLoneSurrogates('')).toBe(''); + }); + + it('preserves valid surrogate pairs (emoji)', () => { + expect(_INTERNAL_removeLoneSurrogates('hello 😀 world')).toBe('hello 😀 world'); + }); + + it('replaces a lone high surrogate with U+FFFD', () => { + expect(_INTERNAL_removeLoneSurrogates('before\uD800after')).toBe('before\uFFFDafter'); + }); + + it('replaces a lone low surrogate with U+FFFD', () => { + expect(_INTERNAL_removeLoneSurrogates('before\uDC00after')).toBe('before\uFFFDafter'); + }); + + it('replaces lone high surrogate at end of string', () => { + expect(_INTERNAL_removeLoneSurrogates('end\uD800')).toBe('end\uFFFD'); + }); + + it('replaces lone low surrogate at start of string', () => { + expect(_INTERNAL_removeLoneSurrogates('\uDC00start')).toBe('\uFFFDstart'); + }); + + it('replaces multiple lone surrogates', () => { + expect(_INTERNAL_removeLoneSurrogates('\uD800\uD801\uDC00')).toBe('\uFFFD\uD801\uDC00'); + }); + + it('handles two consecutive lone high surrogates', () => { + expect(_INTERNAL_removeLoneSurrogates('\uD800\uD800')).toBe('\uFFFD\uFFFD'); + }); + + it('handles mixed valid pairs and lone surrogates', () => { + expect(_INTERNAL_removeLoneSurrogates('\uD83D\uDE00\uD800')).toBe('😀\uFFFD'); + }); + + it('handles the exact reproduction case from issue #5186', () => { + const badValue = '{"a":"\uD800"}'; + const result = _INTERNAL_removeLoneSurrogates(badValue); + expect(result).toBe('{"a":"\uFFFD"}'); + expect(() => JSON.parse(result)).not.toThrow(); + }); }); From b3fc9dbe1d5a7d2429fa4347f8bd69bedd68e34a Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 13 Apr 2026 14:33:41 +0200 Subject: [PATCH 2/7] fix(core): Sanitize lone surrogates in parameterized (fmt) log bodies `parameterize`/`fmt` creates a `String` object via `new String()`, so `typeof message` returns `'object'` not `'string'`, bypassing the sanitization. Use `String(message)` to coerce to a primitive before sanitizing. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/src/logs/internal.ts | 2 +- packages/core/test/lib/logs/internal.test.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/core/src/logs/internal.ts b/packages/core/src/logs/internal.ts index dbb40a9ad81e..9401757d2efd 100644 --- a/packages/core/src/logs/internal.ts +++ b/packages/core/src/logs/internal.ts @@ -162,7 +162,7 @@ export function _INTERNAL_captureLog( const serializedLog: SerializedLog = { timestamp, level, - body: typeof message === 'string' ? _INTERNAL_removeLoneSurrogates(message) : message, + body: _INTERNAL_removeLoneSurrogates(String(message)), trace_id: traceContext?.trace_id, severity_number: severityNumber ?? SEVERITY_TEXT_TO_SEVERITY_NUMBER[level], attributes: sanitizeLogAttributes({ diff --git a/packages/core/test/lib/logs/internal.test.ts b/packages/core/test/lib/logs/internal.test.ts index 4d444d8934db..fd486ff02d9b 100644 --- a/packages/core/test/lib/logs/internal.test.ts +++ b/packages/core/test/lib/logs/internal.test.ts @@ -1280,6 +1280,19 @@ describe('_INTERNAL_captureLog', () => { expect(logBuffer?.[0]?.body).toBe('bad surrogate \uFFFD here'); }); + it('sanitizes lone surrogates in parameterized (fmt) log message body', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + const badValue = 'bad\uD800value'; + _INTERNAL_captureLog({ level: 'error', message: fmt`parameterized ${badValue} message` }, scope); + + const logBuffer = _INTERNAL_getLogBuffer(client); + expect(logBuffer?.[0]?.body).toBe('parameterized bad\uFFFDvalue message'); + }); + it('sanitizes lone surrogates in log attribute values', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); const client = new TestClient(options); From cae61cd32116175d34725d917a527adf49b7c35e Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 14 Apr 2026 13:59:38 +0200 Subject: [PATCH 3/7] fix(core): Use native toWellFormed() only, drop manual fallback Removes the manual charCodeAt-based fallback for surrogate sanitization to reduce bundle size impact. Uses native String.prototype.toWellFormed() which is supported in Node 20+, Chrome 111+, Safari 15.4+, Firefox 119+, and Hermes. On older runtimes the string is returned as-is. Also fixes lint errors from the `as any` cast by using bracket notation. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/src/logs/internal.ts | 54 ++++-------------------------- 1 file changed, 7 insertions(+), 47 deletions(-) diff --git a/packages/core/src/logs/internal.ts b/packages/core/src/logs/internal.ts index 9401757d2efd..72f471fe9a2b 100644 --- a/packages/core/src/logs/internal.ts +++ b/packages/core/src/logs/internal.ts @@ -246,54 +246,14 @@ function sanitizeLogAttributes(attributes: Attributes): Attributes { * on the server to reject the entire log/span batch when they appear in * JSON-escaped form (e.g. `\uD800`). Replacing them at the SDK level ensures * only the offending characters are lost instead of the whole payload. + * + * Uses the native `String.prototype.toWellFormed()` when available + * (Node 20+, Chrome 111+, Safari 15.4+, Firefox 119+, Hermes). + * On older runtimes without native support, returns the string as-is. */ export function _INTERNAL_removeLoneSurrogates(str: string): string { - // Use native toWellFormed() when available (Node 20+, Safari 15.4+, Chrome 111+) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const s = str as any; - if (typeof s.isWellFormed === 'function') { - return s.isWellFormed() ? str : s.toWellFormed(); - } - - // Fast path – scan without allocating. Most strings have no surrogates at all. - let hasLoneSurrogate = false; - for (let i = 0; i < str.length; i++) { - const code = str.charCodeAt(i); - if (code >= 0xd800 && code <= 0xdfff) { - if (code <= 0xdbff && i + 1 < str.length) { - const next = str.charCodeAt(i + 1); - if (next >= 0xdc00 && next <= 0xdfff) { - // Valid surrogate pair – skip the low surrogate - i++; - continue; - } - } - hasLoneSurrogate = true; - break; - } - } - - if (!hasLoneSurrogate) { - return str; - } - - // Slow path – build a new string, replacing lone surrogates with U+FFFD. - const chars: string[] = []; - for (let i = 0; i < str.length; i++) { - const code = str.charCodeAt(i); - if (code >= 0xd800 && code <= 0xdbff) { - const next = i + 1 < str.length ? str.charCodeAt(i + 1) : 0; - if (next >= 0xdc00 && next <= 0xdfff) { - chars.push(str.charAt(i), str.charAt(i + 1)); - i++; - } else { - chars.push('\uFFFD'); - } - } else if (code >= 0xdc00 && code <= 0xdfff) { - chars.push('\uFFFD'); - } else { - chars.push(str.charAt(i)); - } + if (typeof str['isWellFormed'] === 'function') { + return str['isWellFormed']() ? str : str['toWellFormed'](); } - return chars.join(''); + return str; } From 3572bda2ae17d8a3d657c099c3a136d98ba4f906 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 14 Apr 2026 14:10:17 +0200 Subject: [PATCH 4/7] fix(core): Fix TS build error with bracket notation for isWellFormed Use typed interface cast instead of bracket notation to avoid TS7015 (implicit any from index expression) in strict builds. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/src/logs/internal.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core/src/logs/internal.ts b/packages/core/src/logs/internal.ts index 72f471fe9a2b..25e52a1527cc 100644 --- a/packages/core/src/logs/internal.ts +++ b/packages/core/src/logs/internal.ts @@ -252,8 +252,9 @@ function sanitizeLogAttributes(attributes: Attributes): Attributes { * On older runtimes without native support, returns the string as-is. */ export function _INTERNAL_removeLoneSurrogates(str: string): string { - if (typeof str['isWellFormed'] === 'function') { - return str['isWellFormed']() ? str : str['toWellFormed'](); + const s = str as unknown as { isWellFormed?: () => boolean; toWellFormed?: () => string }; + if (typeof s.isWellFormed === 'function') { + return s.isWellFormed() ? str : (s.toWellFormed as () => string)(); } return str; } From fdfa633d0af02875aad4741a6a4dec8f05fa00d2 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 14 Apr 2026 14:33:46 +0200 Subject: [PATCH 5/7] fix(core): Fix lint error and skip surrogate tests on Node 18 Remove unnecessary type assertion to fix lint. Use describe.runIf and it.runIf to skip surrogate replacement tests on runtimes without toWellFormed() (Node 18). Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/src/logs/internal.ts | 2 +- packages/core/test/lib/logs/internal.test.ts | 29 ++++++++++++++------ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/packages/core/src/logs/internal.ts b/packages/core/src/logs/internal.ts index 25e52a1527cc..a691e74899c1 100644 --- a/packages/core/src/logs/internal.ts +++ b/packages/core/src/logs/internal.ts @@ -254,7 +254,7 @@ function sanitizeLogAttributes(attributes: Attributes): Attributes { export function _INTERNAL_removeLoneSurrogates(str: string): string { const s = str as unknown as { isWellFormed?: () => boolean; toWellFormed?: () => string }; if (typeof s.isWellFormed === 'function') { - return s.isWellFormed() ? str : (s.toWellFormed as () => string)(); + return s.isWellFormed() ? str : s.toWellFormed ? s.toWellFormed() : str; } return str; } diff --git a/packages/core/test/lib/logs/internal.test.ts b/packages/core/test/lib/logs/internal.test.ts index fd486ff02d9b..5731e827e238 100644 --- a/packages/core/test/lib/logs/internal.test.ts +++ b/packages/core/test/lib/logs/internal.test.ts @@ -1267,7 +1267,10 @@ describe('_INTERNAL_captureLog', () => { }); }); - describe('lone surrogate sanitization', () => { + // toWellFormed() is only available in Node 20+, Chrome 111+, Safari 15.4+, Firefox 119+, Hermes + const hasToWellFormed = typeof ''.isWellFormed === 'function'; + + describe.runIf(hasToWellFormed)('lone surrogate sanitization', () => { it('sanitizes lone surrogates in log message body', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); const client = new TestClient(options); @@ -1362,6 +1365,9 @@ describe('_INTERNAL_captureLog', () => { }); }); +// toWellFormed() is only available in Node 20+, Chrome 111+, Safari 15.4+, Firefox 119+, Hermes +const hasToWellFormedGlobal = typeof ''.isWellFormed === 'function'; + describe('_INTERNAL_removeLoneSurrogates', () => { it('returns the same string when there are no surrogates', () => { expect(_INTERNAL_removeLoneSurrogates('hello world')).toBe('hello world'); @@ -1375,38 +1381,43 @@ describe('_INTERNAL_removeLoneSurrogates', () => { expect(_INTERNAL_removeLoneSurrogates('hello 😀 world')).toBe('hello 😀 world'); }); - it('replaces a lone high surrogate with U+FFFD', () => { + it.runIf(hasToWellFormedGlobal)('replaces a lone high surrogate with U+FFFD', () => { expect(_INTERNAL_removeLoneSurrogates('before\uD800after')).toBe('before\uFFFDafter'); }); - it('replaces a lone low surrogate with U+FFFD', () => { + it.runIf(hasToWellFormedGlobal)('replaces a lone low surrogate with U+FFFD', () => { expect(_INTERNAL_removeLoneSurrogates('before\uDC00after')).toBe('before\uFFFDafter'); }); - it('replaces lone high surrogate at end of string', () => { + it.runIf(hasToWellFormedGlobal)('replaces lone high surrogate at end of string', () => { expect(_INTERNAL_removeLoneSurrogates('end\uD800')).toBe('end\uFFFD'); }); - it('replaces lone low surrogate at start of string', () => { + it.runIf(hasToWellFormedGlobal)('replaces lone low surrogate at start of string', () => { expect(_INTERNAL_removeLoneSurrogates('\uDC00start')).toBe('\uFFFDstart'); }); - it('replaces multiple lone surrogates', () => { + it.runIf(hasToWellFormedGlobal)('replaces multiple lone surrogates', () => { expect(_INTERNAL_removeLoneSurrogates('\uD800\uD801\uDC00')).toBe('\uFFFD\uD801\uDC00'); }); - it('handles two consecutive lone high surrogates', () => { + it.runIf(hasToWellFormedGlobal)('handles two consecutive lone high surrogates', () => { expect(_INTERNAL_removeLoneSurrogates('\uD800\uD800')).toBe('\uFFFD\uFFFD'); }); - it('handles mixed valid pairs and lone surrogates', () => { + it.runIf(hasToWellFormedGlobal)('handles mixed valid pairs and lone surrogates', () => { expect(_INTERNAL_removeLoneSurrogates('\uD83D\uDE00\uD800')).toBe('😀\uFFFD'); }); - it('handles the exact reproduction case from issue #5186', () => { + it.runIf(hasToWellFormedGlobal)('handles the exact reproduction case from issue #5186', () => { const badValue = '{"a":"\uD800"}'; const result = _INTERNAL_removeLoneSurrogates(badValue); expect(result).toBe('{"a":"\uFFFD"}'); expect(() => JSON.parse(result)).not.toThrow(); }); + + it('returns the string as-is when toWellFormed is not available', () => { + // Verify the function doesn't throw regardless of runtime support + expect(_INTERNAL_removeLoneSurrogates('normal string')).toBe('normal string'); + }); }); From cf4aa9d2edbbef23dde7bb8ccac8b910d5226da6 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 14 Apr 2026 14:50:22 +0200 Subject: [PATCH 6/7] fix(core): Fix oxlint error with type assertion approach Use Object() wrapper instead of `as unknown` cast to access isWellFormed/toWellFormed without triggering oxlint's unnecessary-assertion rule or TS strict mode errors. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/src/logs/internal.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/core/src/logs/internal.ts b/packages/core/src/logs/internal.ts index a691e74899c1..20090a1de462 100644 --- a/packages/core/src/logs/internal.ts +++ b/packages/core/src/logs/internal.ts @@ -252,9 +252,10 @@ function sanitizeLogAttributes(attributes: Attributes): Attributes { * On older runtimes without native support, returns the string as-is. */ export function _INTERNAL_removeLoneSurrogates(str: string): string { - const s = str as unknown as { isWellFormed?: () => boolean; toWellFormed?: () => string }; - if (typeof s.isWellFormed === 'function') { - return s.isWellFormed() ? str : s.toWellFormed ? s.toWellFormed() : str; + // isWellFormed/toWellFormed are ES2024 (not in our TS lib target), so we feature-detect via Object(). + const strObj: Record = Object(str); + if (typeof strObj['isWellFormed'] === 'function') { + return (strObj['isWellFormed'] as () => boolean)() ? str : (strObj['toWellFormed'] as () => string)(); } return str; } From 5394c9055c3f4b8f6cb037eb0dfeaf27dedda8fb Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 14 Apr 2026 15:34:42 +0200 Subject: [PATCH 7/7] fix(core): Remove unnecessary type assertion flagged by oxlint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove `as string` cast in sanitizeLogAttributes — TS already narrows attr.value to string after the type === 'string' check. Use Object() wrapper with Record typing and typeof guards in _INTERNAL_removeLoneSurrogates to avoid any type assertions. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/src/logs/internal.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/core/src/logs/internal.ts b/packages/core/src/logs/internal.ts index 20090a1de462..35a037d979b3 100644 --- a/packages/core/src/logs/internal.ts +++ b/packages/core/src/logs/internal.ts @@ -231,7 +231,7 @@ function sanitizeLogAttributes(attributes: Attributes): Attributes { for (const [key, attr] of Object.entries(attributes)) { const sanitizedKey = _INTERNAL_removeLoneSurrogates(key); if (attr.type === 'string') { - sanitized[sanitizedKey] = { ...attr, value: _INTERNAL_removeLoneSurrogates(attr.value as string) }; + sanitized[sanitizedKey] = { ...attr, value: _INTERNAL_removeLoneSurrogates(attr.value) }; } else { sanitized[sanitizedKey] = attr; } @@ -253,9 +253,11 @@ function sanitizeLogAttributes(attributes: Attributes): Attributes { */ export function _INTERNAL_removeLoneSurrogates(str: string): string { // isWellFormed/toWellFormed are ES2024 (not in our TS lib target), so we feature-detect via Object(). - const strObj: Record = Object(str); - if (typeof strObj['isWellFormed'] === 'function') { - return (strObj['isWellFormed'] as () => boolean)() ? str : (strObj['toWellFormed'] as () => string)(); + const strObj: Record = Object(str); + const isWellFormed = strObj['isWellFormed']; + const toWellFormed = strObj['toWellFormed']; + if (typeof isWellFormed === 'function' && typeof toWellFormed === 'function') { + return isWellFormed.call(str) ? str : toWellFormed.call(str); } return str; }