diff --git a/analytics.js b/analytics.js index 1074bfa1..41dfb51e 100644 --- a/analytics.js +++ b/analytics.js @@ -29,11 +29,19 @@ posthog._i.push([token, config, 'posthog']); }; - // Suppress only exceptions whose frames are all identifiable third-party code. - // Missing stacks and unfamiliar paths may be first-party failures: retain them. + // Drop an exception only when its stack points at third-party code alone. + // Native and anonymous frames name no source, so skip them. + // Any first-party or unknown frame keeps the exception, so first-party failures stay visible. var OWN_HOST = String(window.location.hostname).toLowerCase(); var URL_HOST = /^(?:https?:)?\/\/([^/?#]+)/i; + function isNativeFrame(frame) { + // The browser reports built-in frames such as Array.reduce with no source file. + if (!frame || frame.in_app === true) return false; + var ref = frame.filename || frame.source; + return ref === '' || ref === '[native code]'; + } + function isThirdPartyFrame(frame) { // filename is the raw SDK location; source can be a host-stripped path. var ref = frame && (frame.filename || frame.source); @@ -49,13 +57,23 @@ return /^\/onsite\/js\//.test(ref); } + function isThirdPartyEntry(entry) { + var frames = entry && entry.stacktrace && entry.stacktrace.frames; + if (!Array.isArray(frames) || frames.length === 0) return false; + var sawThirdParty = false; + for (var i = 0; i < frames.length; i++) { + var frame = frames[i]; + if (isNativeFrame(frame)) continue; + if (!isThirdPartyFrame(frame)) return false; + sawThirdParty = true; + } + return sawThirdParty; + } + function isThirdPartyException(event) { var list = event.properties && event.properties.$exception_list; if (!Array.isArray(list) || list.length === 0) return false; - return list.every(function(entry) { - var frames = entry && entry.stacktrace && entry.stacktrace.frames; - return Array.isArray(frames) && frames.length > 0 && frames.every(isThirdPartyFrame); - }); + return list.every(isThirdPartyEntry); } function beforeSend(event) { diff --git a/tests/analytics-third-party-filter.test.js b/tests/analytics-third-party-filter.test.js index b0358dad..4fec4d22 100644 --- a/tests/analytics-third-party-filter.test.js +++ b/tests/analytics-third-party-filter.test.js @@ -73,6 +73,19 @@ describe('analytics.js before_send filter', () => { assert.strictEqual(beforeSend(event), null); }); + it('drops a Klaviyo chunk failure that interleaves a native frame', () => { + const beforeSend = loadBeforeSend(); + const event = exceptionEvent([ + { source: '/onsite/js/runtime.08a889cad67f0.js', in_app: true }, + { source: '/onsite/js/7130.b41c2e9f5a1d7.js', in_app: true }, + { function: 'Array.reduce', filename: '', in_app: false }, + { source: '/onsite/js/vendor.4d2f8a1c9b3e6.js', in_app: true }, + { filename: 'https://static.klaviyo.com/onsite/js/klaviyo.js?cb=3' }, + { source: '/onsite/js/onsite.2c7e9a4f1d8b0.js', in_app: true } + ]); + assert.strictEqual(beforeSend(event), null); + }); + it('drops third-party frames served from a foreign host', () => { const beforeSend = loadBeforeSend(); const event = exceptionEvent([