From e8922ec54237bbc45faf13cd1ebdee2cdee7601b Mon Sep 17 00:00:00 2001 From: Jaissica Date: Thu, 4 Dec 2025 14:27:07 -0500 Subject: [PATCH 1/5] feat: Add Mixpanel Session Replay settings support --- src/MixpanelEventForwarder.js | 89 ++++- test/src/tests.js | 595 ++++++++++++++++++++++++++++++++++ 2 files changed, 677 insertions(+), 7 deletions(-) diff --git a/src/MixpanelEventForwarder.js b/src/MixpanelEventForwarder.js index 567b1c0..110dbc4 100644 --- a/src/MixpanelEventForwarder.js +++ b/src/MixpanelEventForwarder.js @@ -62,13 +62,88 @@ var constructor = function () { if (!testMode) { renderSnippet(); } - mixpanel.init( - settings.token, - { - api_host: forwarderSettings.baseUrl, - }, - 'mparticle' - ); + // Build init options object + var initOptions = { + api_host: forwarderSettings.baseUrl, + }; + + if (forwarderSettings.recordSessionsPercent != null) { + var sessionPercent = parseInt( + forwarderSettings.recordSessionsPercent, + 10 + ); + if ( + !isNaN(sessionPercent) && + sessionPercent >= 0 && + sessionPercent <= 100 + ) { + initOptions.record_sessions_percent = sessionPercent; + } + } + + if (forwarderSettings.recordHeatmapData != null) { + initOptions.record_heatmap_data = + forwarderSettings.recordHeatmapData === 'True'; + } + + if (forwarderSettings.autocapture != null) { + initOptions.autocapture = + forwarderSettings.autocapture === 'True'; + } + + // Privacy and masking settings + if (forwarderSettings.recordMaskTextSelector) { + initOptions.record_mask_text_selector = + forwarderSettings.recordMaskTextSelector; + } + + if (forwarderSettings.recordBlockSelector) { + initOptions.record_block_selector = + forwarderSettings.recordBlockSelector; + } + + if (forwarderSettings.recordBlockClass) { + initOptions.record_block_class = + forwarderSettings.recordBlockClass; + } + + if (forwarderSettings.recordMaskTextClass) { + initOptions.record_mask_text_class = + forwarderSettings.recordMaskTextClass; + } + + // Canvas recording (experimental) + if (forwarderSettings.recordCanvas != null) { + initOptions.record_canvas = + forwarderSettings.recordCanvas === 'True'; + } + + // Timing settings + if (forwarderSettings.recordIdleTimeoutMs != null) { + var idleTimeoutMs = parseInt( + forwarderSettings.recordIdleTimeoutMs, + 10 + ); + if (!isNaN(idleTimeoutMs)) { + initOptions.record_idle_timeout_ms = idleTimeoutMs; + } + } + + if (forwarderSettings.recordMaxMs != null) { + var maxMs = parseInt(forwarderSettings.recordMaxMs, 10); + if (!isNaN(maxMs)) { + initOptions.record_max_ms = maxMs; + } + } + + if (forwarderSettings.recordMinMs != null) { + var minMs = parseInt(forwarderSettings.recordMinMs, 10); + if (!isNaN(minMs)) { + initOptions.record_min_ms = minMs; + } + } + + mixpanel.init(settings.token, initOptions, 'mparticle'); isInitialized = true; diff --git a/test/src/tests.js b/test/src/tests.js index dc631af..f127d74 100644 --- a/test/src/tests.js +++ b/test/src/tests.js @@ -202,6 +202,601 @@ describe('Mixpanel Forwarder', function () { done(); }); + it('should initialize with Session Replay settings', function (done) { + window.mixpanel = new MPMock(); + mParticle.forwarder.init( + { + token: 'token123', + baseUrl: API_HOST, + recordSessionsPercent: '20', + recordHeatmapData: 'True', + autocapture: 'False', + }, + reportService.cb, + true + ); + + window.mixpanel.settings.should.have.property( + 'record_sessions_percent', + 20 + ); + window.mixpanel.settings.should.have.property( + 'record_heatmap_data', + true + ); + window.mixpanel.settings.should.have.property('autocapture', false); + + done(); + }); + + it('should handle invalid recordSessionsPercent values', function (done) { + window.mixpanel = new MPMock(); + mParticle.forwarder.init( + { + token: 'token123', + baseUrl: API_HOST, + recordSessionsPercent: 'invalid', + }, + reportService.cb, + true + ); + + window.mixpanel.settings.should.not.have.property( + 'record_sessions_percent' + ); + + done(); + }); + + it('should initialize with privacy and masking settings', function (done) { + window.mixpanel = new MPMock(); + mParticle.forwarder.init( + { + token: 'token123', + baseUrl: API_HOST, + recordMaskTextSelector: '.pii', + recordBlockSelector: 'img, video', + recordMaskTextClass: 'sensitive', + recordBlockClass: 'blocked', + }, + reportService.cb, + true + ); + + window.mixpanel.settings.should.have.property( + 'record_mask_text_selector', + '.pii' + ); + window.mixpanel.settings.should.have.property( + 'record_block_selector', + 'img, video' + ); + window.mixpanel.settings.should.have.property( + 'record_mask_text_class', + 'sensitive' + ); + window.mixpanel.settings.should.have.property( + 'record_block_class', + 'blocked' + ); + + done(); + }); + + it('should handle boolean settings for Session Replay correctly', function (done) { + window.mixpanel = new MPMock(); + mParticle.forwarder.init( + { + token: 'token123', + baseUrl: API_HOST, + recordHeatmapData: 'False', + autocapture: 'True', + recordCanvas: 'True', + }, + reportService.cb, + true + ); + + // Verify all boolean settings are actual booleans, not strings + window.mixpanel.settings.should.have.property( + 'record_heatmap_data', + false + ); + window.mixpanel.settings.record_heatmap_data.should.be.type( + 'boolean' + ); + + window.mixpanel.settings.should.have.property('autocapture', true); + window.mixpanel.settings.autocapture.should.be.type('boolean'); + + window.mixpanel.settings.should.have.property( + 'record_canvas', + true + ); + window.mixpanel.settings.record_canvas.should.be.type('boolean'); + + done(); + }); + + it('should handle numeric settings for Session Replay correctly', function (done) { + window.mixpanel = new MPMock(); + mParticle.forwarder.init( + { + token: 'token123', + baseUrl: API_HOST, + recordSessionsPercent: '50', + recordIdleTimeoutMs: '1800000', + recordMaxMs: '86400000', + recordMinMs: '1000', + }, + reportService.cb, + true + ); + + // Verify all integer settings are actual numbers, not strings + window.mixpanel.settings.should.have.property( + 'record_sessions_percent', + 50 + ); + window.mixpanel.settings.record_sessions_percent.should.be.type( + 'number' + ); + + window.mixpanel.settings.should.have.property( + 'record_idle_timeout_ms', + 1800000 + ); + window.mixpanel.settings.record_idle_timeout_ms.should.be.type( + 'number' + ); + + window.mixpanel.settings.should.have.property( + 'record_max_ms', + 86400000 + ); + window.mixpanel.settings.record_max_ms.should.be.type('number'); + + window.mixpanel.settings.should.have.property( + 'record_min_ms', + 1000 + ); + window.mixpanel.settings.record_min_ms.should.be.type('number'); + + done(); + }); + + it('should handle invalid timing values gracefully', function (done) { + window.mixpanel = new MPMock(); + mParticle.forwarder.init( + { + token: 'token123', + baseUrl: API_HOST, + recordIdleTimeoutMs: 'invalid', + recordMaxMs: 'not-a-number', + recordMinMs: 'not-a-number', + }, + reportService.cb, + true + ); + + window.mixpanel.settings.should.not.have.property( + 'record_idle_timeout_ms' + ); + window.mixpanel.settings.should.not.have.property('record_max_ms'); + window.mixpanel.settings.should.not.have.property('record_min_ms'); + + done(); + }); + + it('should not set empty string privacy selectors', function (done) { + window.mixpanel = new MPMock(); + mParticle.forwarder.init( + { + token: 'token123', + baseUrl: API_HOST, + recordMaskTextSelector: '', + recordBlockClass: '', + }, + reportService.cb, + true + ); + + window.mixpanel.settings.should.not.have.property( + 'record_mask_text_selector' + ); + window.mixpanel.settings.should.not.have.property( + 'record_block_class' + ); + + done(); + }); + + it('should accept recordSessionsPercent with lower boundary 0', function (done) { + window.mixpanel = new MPMock(); + mParticle.forwarder.init( + { + token: 'token123', + baseUrl: API_HOST, + recordSessionsPercent: '0', + }, + reportService.cb, + true + ); + + window.mixpanel.settings.should.have.property( + 'record_sessions_percent', + 0 + ); + window.mixpanel.settings.record_sessions_percent.should.be.type( + 'number' + ); + + done(); + }); + + it('should accept recordSessionsPercent upper boundary 100', function (done) { + window.mixpanel = new MPMock(); + mParticle.forwarder.init( + { + token: 'token123', + baseUrl: API_HOST, + recordSessionsPercent: '100', + }, + reportService.cb, + true + ); + + window.mixpanel.settings.should.have.property( + 'record_sessions_percent', + 100 + ); + window.mixpanel.settings.record_sessions_percent.should.be.type( + 'number' + ); + + done(); + }); + + it('should reject negative recordSessionsPercent', function (done) { + window.mixpanel = new MPMock(); + mParticle.forwarder.init( + { + token: 'token123', + baseUrl: API_HOST, + recordSessionsPercent: '-10', + }, + reportService.cb, + true + ); + + window.mixpanel.settings.should.not.have.property( + 'record_sessions_percent' + ); + + done(); + }); + + it('should reject recordSessionsPercent above range (> 100)', function (done) { + window.mixpanel = new MPMock(); + mParticle.forwarder.init( + { + token: 'token123', + baseUrl: API_HOST, + recordSessionsPercent: '150', + }, + reportService.cb, + true + ); + + window.mixpanel.settings.should.not.have.property( + 'record_sessions_percent' + ); + + done(); + }); + + it('should handle null and undefined for integer settings', function (done) { + window.mixpanel = new MPMock(); + mParticle.forwarder.init( + { + token: 'token123', + baseUrl: API_HOST, + recordSessionsPercent: null, + recordIdleTimeoutMs: undefined, + }, + reportService.cb, + true + ); + + window.mixpanel.settings.should.not.have.property( + 'record_sessions_percent' + ); + window.mixpanel.settings.should.not.have.property( + 'record_idle_timeout_ms' + ); + + done(); + }); + + it('should handle null and undefined for string settings', function (done) { + window.mixpanel = new MPMock(); + mParticle.forwarder.init( + { + token: 'token123', + baseUrl: API_HOST, + recordMaskTextSelector: null, + recordBlockSelector: undefined, + recordMaskTextClass: null, + }, + reportService.cb, + true + ); + + window.mixpanel.settings.should.not.have.property( + 'record_mask_text_selector' + ); + window.mixpanel.settings.should.not.have.property( + 'record_block_selector' + ); + window.mixpanel.settings.should.not.have.property( + 'record_mask_text_class' + ); + + done(); + }); + + it('should handle null and undefined for boolean settings', function (done) { + window.mixpanel = new MPMock(); + mParticle.forwarder.init( + { + token: 'token123', + baseUrl: API_HOST, + recordHeatmapData: null, + autocapture: undefined, + }, + reportService.cb, + true + ); + + window.mixpanel.settings.should.not.have.property( + 'record_heatmap_data' + ); + window.mixpanel.settings.should.not.have.property('autocapture'); + + done(); + }); + + it('should accept recordIdleTimeoutMs with large positive value', function (done) { + window.mixpanel = new MPMock(); + mParticle.forwarder.init( + { + token: 'token123', + baseUrl: API_HOST, + recordIdleTimeoutMs: '18000000000', + }, + reportService.cb, + true + ); + + window.mixpanel.settings.should.have.property( + 'record_idle_timeout_ms', + 18000000000 + ); + window.mixpanel.settings.record_idle_timeout_ms.should.be.type( + 'number' + ); + + done(); + }); + + it('should preserve string values for selector settings without type conversion', function (done) { + window.mixpanel = new MPMock(); + mParticle.forwarder.init( + { + token: 'token123', + baseUrl: API_HOST, + recordMaskTextSelector: '.pii, .sensitive, [data-private]', + recordBlockSelector: 'iframe, img, video', + recordMaskTextClass: 'mp-mask-text', + recordBlockClass: 'mp-block-element', + }, + reportService.cb, + true + ); + + // Verify strings are preserved as-is + window.mixpanel.settings.record_mask_text_selector.should.equal( + '.pii, .sensitive, [data-private]' + ); + window.mixpanel.settings.record_mask_text_selector.should.be.type( + 'string' + ); + window.mixpanel.settings.record_block_selector.should.equal( + 'iframe, img, video' + ); + window.mixpanel.settings.record_block_selector.should.be.type( + 'string' + ); + window.mixpanel.settings.record_mask_text_class.should.equal( + 'mp-mask-text' + ); + window.mixpanel.settings.record_mask_text_class.should.be.type( + 'string' + ); + window.mixpanel.settings.record_block_class.should.equal( + 'mp-block-element' + ); + window.mixpanel.settings.record_block_class.should.be.type( + 'string' + ); + + done(); + }); + + it('should handle mixed valid and invalid integer values', function (done) { + window.mixpanel = new MPMock(); + mParticle.forwarder.init( + { + token: 'token123', + baseUrl: API_HOST, + recordSessionsPercent: '50', + recordIdleTimeoutMs: 'invalid', + recordMaxMs: '-100', + recordMinMs: '1000', + }, + reportService.cb, + true + ); + + window.mixpanel.settings.should.have.property( + 'record_sessions_percent', + 50 + ); + window.mixpanel.settings.should.not.have.property( + 'record_idle_timeout_ms' + ); + window.mixpanel.settings.should.have.property( + 'record_max_ms', + -100 + ); + window.mixpanel.settings.should.have.property( + 'record_min_ms', + 1000 + ); + + done(); + }); + + it('should handle all Session Replay settings together with correct types', function (done) { + window.mixpanel = new MPMock(); + mParticle.forwarder.init( + { + token: 'token123', + baseUrl: API_HOST, + recordSessionsPercent: '75', + recordHeatmapData: 'True', + autocapture: 'True', + recordCanvas: 'False', + recordMaskTextSelector: '.secret', + recordBlockSelector: 'video', + recordMaskTextClass: 'masked', + recordBlockClass: 'blocked', + recordIdleTimeoutMs: '1800000', + recordMaxMs: '86400000', + recordMinMs: '5000', + }, + reportService.cb, + true + ); + + // Verify all settings with correct types + window.mixpanel.settings.record_sessions_percent.should.equal(75); + window.mixpanel.settings.record_sessions_percent.should.be.type( + 'number' + ); + + window.mixpanel.settings.record_heatmap_data.should.equal(true); + window.mixpanel.settings.record_heatmap_data.should.be.type( + 'boolean' + ); + + window.mixpanel.settings.autocapture.should.equal(true); + window.mixpanel.settings.autocapture.should.be.type('boolean'); + + window.mixpanel.settings.record_canvas.should.equal(false); + window.mixpanel.settings.record_canvas.should.be.type('boolean'); + + window.mixpanel.settings.record_mask_text_selector.should.equal( + '.secret' + ); + window.mixpanel.settings.record_mask_text_selector.should.be.type( + 'string' + ); + window.mixpanel.settings.record_block_selector.should.equal( + 'video' + ); + window.mixpanel.settings.record_block_selector.should.be.type( + 'string' + ); + window.mixpanel.settings.record_mask_text_class.should.equal( + 'masked' + ); + window.mixpanel.settings.record_mask_text_class.should.be.type( + 'string' + ); + window.mixpanel.settings.record_block_class.should.equal('blocked'); + window.mixpanel.settings.record_block_class.should.be.type( + 'string' + ); + window.mixpanel.settings.record_idle_timeout_ms.should.equal( + 1800000 + ); + window.mixpanel.settings.record_idle_timeout_ms.should.be.type( + 'number' + ); + window.mixpanel.settings.record_max_ms.should.equal(86400000); + window.mixpanel.settings.record_max_ms.should.be.type('number'); + + window.mixpanel.settings.record_min_ms.should.equal(5000); + window.mixpanel.settings.record_min_ms.should.be.type('number'); + + done(); + }); + + it('should handle extreme timing values within valid ranges', function (done) { + window.mixpanel = new MPMock(); + mParticle.forwarder.init( + { + token: 'token123', + baseUrl: API_HOST, + recordIdleTimeoutMs: '2147483647', // Max 32-bit int + recordMaxMs: '86400000', // 24 hours in ms + recordMinMs: '1', + }, + reportService.cb, + true + ); + + window.mixpanel.settings.should.have.property( + 'record_idle_timeout_ms', + 2147483647 + ); + window.mixpanel.settings.should.have.property( + 'record_max_ms', + 86400000 + ); + window.mixpanel.settings.should.have.property('record_min_ms', 1); + + done(); + }); + + it('should handle special characters in selector settings', function (done) { + window.mixpanel = new MPMock(); + mParticle.forwarder.init( + { + token: 'token123', + baseUrl: API_HOST, + recordMaskTextSelector: + '[data-sensitive="true"], .pii, #secret', + recordBlockSelector: + 'iframe[src*="youtube"], video:not(.public)', + }, + reportService.cb, + true + ); + + window.mixpanel.settings.record_mask_text_selector.should.equal( + '[data-sensitive="true"], .pii, #secret' + ); + window.mixpanel.settings.record_block_selector.should.equal( + 'iframe[src*="youtube"], video:not(.public)' + ); + + done(); + }); + }); + + describe('Logging events', function () { it('should log a page view with "Viewed" prepended to the event name', function (done) { mParticle.forwarder.process({ EventDataType: MessageType.PageView, From 0bee482675af57063aaab197161a864543c6fc80 Mon Sep 17 00:00:00 2001 From: Jaissica Date: Mon, 15 Dec 2025 09:26:21 -0500 Subject: [PATCH 2/5] add Session Replay settings validation tests --- test/src/tests.js | 440 +++++++++++++--------------------------------- 1 file changed, 124 insertions(+), 316 deletions(-) diff --git a/test/src/tests.js b/test/src/tests.js index f127d74..a47bc61 100644 --- a/test/src/tests.js +++ b/test/src/tests.js @@ -201,184 +201,141 @@ describe('Mixpanel Forwarder', function () { done(); }); + }); - it('should initialize with Session Replay settings', function (done) { + describe('Session Replay Configuration', function () { + it('should convert numeric setting strings to integer values', function (done) { window.mixpanel = new MPMock(); mParticle.forwarder.init( { token: 'token123', baseUrl: API_HOST, - recordSessionsPercent: '20', - recordHeatmapData: 'True', - autocapture: 'False', + recordSessionsPercent: '75', + recordIdleTimeoutMs: '1800000', + recordMaxMs: '86400000', + recordMinMs: '5000', }, reportService.cb, true ); - window.mixpanel.settings.should.have.property( - 'record_sessions_percent', - 20 - ); - window.mixpanel.settings.should.have.property( - 'record_heatmap_data', - true + // Verify all numeric settings with correct types + window.mixpanel.settings.record_sessions_percent.should.equal(75); + window.mixpanel.settings.record_sessions_percent.should.be.type( + 'number' ); - window.mixpanel.settings.should.have.property('autocapture', false); - done(); - }); - - it('should handle invalid recordSessionsPercent values', function (done) { - window.mixpanel = new MPMock(); - mParticle.forwarder.init( - { - token: 'token123', - baseUrl: API_HOST, - recordSessionsPercent: 'invalid', - }, - reportService.cb, - true + window.mixpanel.settings.record_idle_timeout_ms.should.equal( + 1800000 ); - - window.mixpanel.settings.should.not.have.property( - 'record_sessions_percent' + window.mixpanel.settings.record_idle_timeout_ms.should.be.type( + 'number' ); - done(); - }); - - it('should initialize with privacy and masking settings', function (done) { - window.mixpanel = new MPMock(); - mParticle.forwarder.init( - { - token: 'token123', - baseUrl: API_HOST, - recordMaskTextSelector: '.pii', - recordBlockSelector: 'img, video', - recordMaskTextClass: 'sensitive', - recordBlockClass: 'blocked', - }, - reportService.cb, - true - ); + window.mixpanel.settings.record_max_ms.should.equal(86400000); + window.mixpanel.settings.record_max_ms.should.be.type('number'); - window.mixpanel.settings.should.have.property( - 'record_mask_text_selector', - '.pii' - ); - window.mixpanel.settings.should.have.property( - 'record_block_selector', - 'img, video' - ); - window.mixpanel.settings.should.have.property( - 'record_mask_text_class', - 'sensitive' - ); - window.mixpanel.settings.should.have.property( - 'record_block_class', - 'blocked' - ); + window.mixpanel.settings.record_min_ms.should.equal(5000); + window.mixpanel.settings.record_min_ms.should.be.type('number'); done(); }); - it('should handle boolean settings for Session Replay correctly', function (done) { + it('should convert boolean setting strings to boolean values', function (done) { window.mixpanel = new MPMock(); mParticle.forwarder.init( { token: 'token123', baseUrl: API_HOST, - recordHeatmapData: 'False', - autocapture: 'True', + recordHeatmapData: 'True', + autocapture: 'False', recordCanvas: 'True', }, reportService.cb, true ); - // Verify all boolean settings are actual booleans, not strings - window.mixpanel.settings.should.have.property( - 'record_heatmap_data', - false - ); + // Verify all boolean settings with correct types + window.mixpanel.settings.record_heatmap_data.should.equal(true); window.mixpanel.settings.record_heatmap_data.should.be.type( 'boolean' ); - window.mixpanel.settings.should.have.property('autocapture', true); + window.mixpanel.settings.autocapture.should.equal(false); window.mixpanel.settings.autocapture.should.be.type('boolean'); - window.mixpanel.settings.should.have.property( - 'record_canvas', - true - ); + window.mixpanel.settings.record_canvas.should.equal(true); window.mixpanel.settings.record_canvas.should.be.type('boolean'); done(); }); - it('should handle numeric settings for Session Replay correctly', function (done) { + it('should preserve string values for privacy selector settings', function (done) { window.mixpanel = new MPMock(); mParticle.forwarder.init( { token: 'token123', baseUrl: API_HOST, - recordSessionsPercent: '50', - recordIdleTimeoutMs: '1800000', - recordMaxMs: '86400000', - recordMinMs: '1000', + recordMaskTextSelector: '.pii, .sensitive', + recordBlockSelector: 'iframe, img, video', + recordMaskTextClass: 'mp-mask-text', + recordBlockClass: 'mp-block-element', }, reportService.cb, true ); - // Verify all integer settings are actual numbers, not strings - window.mixpanel.settings.should.have.property( - 'record_sessions_percent', - 50 + // Verify all string settings with correct types + window.mixpanel.settings.record_mask_text_selector.should.equal( + '.pii, .sensitive' ); - window.mixpanel.settings.record_sessions_percent.should.be.type( - 'number' + window.mixpanel.settings.record_mask_text_selector.should.be.type( + 'string' ); - window.mixpanel.settings.should.have.property( - 'record_idle_timeout_ms', - 1800000 + window.mixpanel.settings.record_block_selector.should.equal( + 'iframe, img, video' ); - window.mixpanel.settings.record_idle_timeout_ms.should.be.type( - 'number' + window.mixpanel.settings.record_block_selector.should.be.type( + 'string' ); - window.mixpanel.settings.should.have.property( - 'record_max_ms', - 86400000 + window.mixpanel.settings.record_mask_text_class.should.equal( + 'mp-mask-text' + ); + window.mixpanel.settings.record_mask_text_class.should.be.type( + 'string' ); - window.mixpanel.settings.record_max_ms.should.be.type('number'); - window.mixpanel.settings.should.have.property( - 'record_min_ms', - 1000 + window.mixpanel.settings.record_block_class.should.equal( + 'mp-block-element' + ); + window.mixpanel.settings.record_block_class.should.be.type( + 'string' ); - window.mixpanel.settings.record_min_ms.should.be.type('number'); done(); }); - it('should handle invalid timing values gracefully', function (done) { + it('should reject invalid numeric values', function (done) { window.mixpanel = new MPMock(); mParticle.forwarder.init( { token: 'token123', baseUrl: API_HOST, - recordIdleTimeoutMs: 'invalid', - recordMaxMs: 'not-a-number', - recordMinMs: 'not-a-number', + recordSessionsPercent: 'invalid', + recordIdleTimeoutMs: 'not-a-number', + recordMaxMs: 'abc', + recordMinMs: 'xyz', }, reportService.cb, true ); + // All invalid strings should be rejected (not set) + window.mixpanel.settings.should.not.have.property( + 'record_sessions_percent' + ); window.mixpanel.settings.should.not.have.property( 'record_idle_timeout_ms' ); @@ -388,76 +345,72 @@ describe('Mixpanel Forwarder', function () { done(); }); - it('should not set empty string privacy selectors', function (done) { + it('should handle boundary and edge case for numeric values', function (done) { window.mixpanel = new MPMock(); mParticle.forwarder.init( { token: 'token123', baseUrl: API_HOST, - recordMaskTextSelector: '', - recordBlockClass: '', + recordSessionsPercent: '0', + recordIdleTimeoutMs: '2147483647', + recordMaxMs: '-100', + recordMinMs: '1', }, reportService.cb, true ); - window.mixpanel.settings.should.not.have.property( - 'record_mask_text_selector' + window.mixpanel.settings.should.have.property( + 'record_sessions_percent', + 0 ); - window.mixpanel.settings.should.not.have.property( - 'record_block_class' + window.mixpanel.settings.should.have.property( + 'record_idle_timeout_ms', + 2147483647 ); + window.mixpanel.settings.should.have.property( + 'record_max_ms', + -100 + ); + window.mixpanel.settings.should.have.property('record_min_ms', 1); done(); }); - it('should accept recordSessionsPercent with lower boundary 0', function (done) { + it('should validate recordSessionsPercent range (0-100)', function (done) { + // Test valid upper boundary: 100 window.mixpanel = new MPMock(); mParticle.forwarder.init( { token: 'token123', baseUrl: API_HOST, - recordSessionsPercent: '0', + recordSessionsPercent: '100', }, reportService.cb, true ); - window.mixpanel.settings.should.have.property( 'record_sessions_percent', - 0 - ); - window.mixpanel.settings.record_sessions_percent.should.be.type( - 'number' + 100 ); - done(); - }); - - it('should accept recordSessionsPercent upper boundary 100', function (done) { + // Test valid lower boundary: 0 window.mixpanel = new MPMock(); mParticle.forwarder.init( { token: 'token123', baseUrl: API_HOST, - recordSessionsPercent: '100', + recordSessionsPercent: '0', }, reportService.cb, true ); - window.mixpanel.settings.should.have.property( 'record_sessions_percent', - 100 - ); - window.mixpanel.settings.record_sessions_percent.should.be.type( - 'number' + 0 ); - done(); - }); - - it('should reject negative recordSessionsPercent', function (done) { + // Test invalid: negative window.mixpanel = new MPMock(); mParticle.forwarder.init( { @@ -468,15 +421,11 @@ describe('Mixpanel Forwarder', function () { reportService.cb, true ); - window.mixpanel.settings.should.not.have.property( 'record_sessions_percent' ); - done(); - }); - - it('should reject recordSessionsPercent above range (> 100)', function (done) { + // Test invalid: > 100 window.mixpanel = new MPMock(); mParticle.forwarder.init( { @@ -487,7 +436,6 @@ describe('Mixpanel Forwarder', function () { reportService.cb, true ); - window.mixpanel.settings.should.not.have.property( 'record_sessions_percent' ); @@ -495,7 +443,7 @@ describe('Mixpanel Forwarder', function () { done(); }); - it('should handle null and undefined for integer settings', function (done) { + it('should ignore null and undefined values for all setting types', function (done) { window.mixpanel = new MPMock(); mParticle.forwarder.init( { @@ -503,209 +451,132 @@ describe('Mixpanel Forwarder', function () { baseUrl: API_HOST, recordSessionsPercent: null, recordIdleTimeoutMs: undefined, + recordHeatmapData: null, + autocapture: undefined, + recordMaskTextSelector: null, + recordBlockSelector: undefined, }, reportService.cb, true ); + // Nothing should be set window.mixpanel.settings.should.not.have.property( 'record_sessions_percent' ); window.mixpanel.settings.should.not.have.property( 'record_idle_timeout_ms' ); - - done(); - }); - - it('should handle null and undefined for string settings', function (done) { - window.mixpanel = new MPMock(); - mParticle.forwarder.init( - { - token: 'token123', - baseUrl: API_HOST, - recordMaskTextSelector: null, - recordBlockSelector: undefined, - recordMaskTextClass: null, - }, - reportService.cb, - true + window.mixpanel.settings.should.not.have.property( + 'record_heatmap_data' ); - + window.mixpanel.settings.should.not.have.property('autocapture'); window.mixpanel.settings.should.not.have.property( 'record_mask_text_selector' ); window.mixpanel.settings.should.not.have.property( 'record_block_selector' ); - window.mixpanel.settings.should.not.have.property( - 'record_mask_text_class' - ); done(); }); - it('should handle null and undefined for boolean settings', function (done) { + it('should ignore empty string values for privacy selectors', function (done) { window.mixpanel = new MPMock(); mParticle.forwarder.init( { token: 'token123', baseUrl: API_HOST, - recordHeatmapData: null, - autocapture: undefined, + recordMaskTextSelector: '', + recordBlockClass: '', }, reportService.cb, true ); window.mixpanel.settings.should.not.have.property( - 'record_heatmap_data' - ); - window.mixpanel.settings.should.not.have.property('autocapture'); - - done(); - }); - - it('should accept recordIdleTimeoutMs with large positive value', function (done) { - window.mixpanel = new MPMock(); - mParticle.forwarder.init( - { - token: 'token123', - baseUrl: API_HOST, - recordIdleTimeoutMs: '18000000000', - }, - reportService.cb, - true - ); - - window.mixpanel.settings.should.have.property( - 'record_idle_timeout_ms', - 18000000000 + 'record_mask_text_selector' ); - window.mixpanel.settings.record_idle_timeout_ms.should.be.type( - 'number' + window.mixpanel.settings.should.not.have.property( + 'record_block_class' ); done(); }); - it('should preserve string values for selector settings without type conversion', function (done) { + it('should preserve complex CSS selectors with special characters', function (done) { window.mixpanel = new MPMock(); mParticle.forwarder.init( { token: 'token123', baseUrl: API_HOST, - recordMaskTextSelector: '.pii, .sensitive, [data-private]', - recordBlockSelector: 'iframe, img, video', - recordMaskTextClass: 'mp-mask-text', - recordBlockClass: 'mp-block-element', + recordMaskTextSelector: + '[data-sensitive="true"], .pii, #secret', + recordBlockSelector: + 'iframe[src*="youtube"], video:not(.public)', }, reportService.cb, true ); - // Verify strings are preserved as-is window.mixpanel.settings.record_mask_text_selector.should.equal( - '.pii, .sensitive, [data-private]' - ); - window.mixpanel.settings.record_mask_text_selector.should.be.type( - 'string' + '[data-sensitive="true"], .pii, #secret' ); window.mixpanel.settings.record_block_selector.should.equal( - 'iframe, img, video' - ); - window.mixpanel.settings.record_block_selector.should.be.type( - 'string' - ); - window.mixpanel.settings.record_mask_text_class.should.equal( - 'mp-mask-text' - ); - window.mixpanel.settings.record_mask_text_class.should.be.type( - 'string' - ); - window.mixpanel.settings.record_block_class.should.equal( - 'mp-block-element' - ); - window.mixpanel.settings.record_block_class.should.be.type( - 'string' - ); - - done(); - }); - - it('should handle mixed valid and invalid integer values', function (done) { - window.mixpanel = new MPMock(); - mParticle.forwarder.init( - { - token: 'token123', - baseUrl: API_HOST, - recordSessionsPercent: '50', - recordIdleTimeoutMs: 'invalid', - recordMaxMs: '-100', - recordMinMs: '1000', - }, - reportService.cb, - true - ); - - window.mixpanel.settings.should.have.property( - 'record_sessions_percent', - 50 - ); - window.mixpanel.settings.should.not.have.property( - 'record_idle_timeout_ms' - ); - window.mixpanel.settings.should.have.property( - 'record_max_ms', - -100 - ); - window.mixpanel.settings.should.have.property( - 'record_min_ms', - 1000 + 'iframe[src*="youtube"], video:not(.public)' ); done(); }); - it('should handle all Session Replay settings together with correct types', function (done) { + it('should handle all Session Replay settings with correct type conversions', function (done) { window.mixpanel = new MPMock(); mParticle.forwarder.init( { token: 'token123', baseUrl: API_HOST, + // Numeric settings recordSessionsPercent: '75', + recordIdleTimeoutMs: '1800000', + recordMaxMs: '86400000', + recordMinMs: '5000', + // Boolean settings recordHeatmapData: 'True', autocapture: 'True', recordCanvas: 'False', + // String settings recordMaskTextSelector: '.secret', recordBlockSelector: 'video', recordMaskTextClass: 'masked', recordBlockClass: 'blocked', - recordIdleTimeoutMs: '1800000', - recordMaxMs: '86400000', - recordMinMs: '5000', }, reportService.cb, true ); - // Verify all settings with correct types + // Verify all settings are present with correct values and types window.mixpanel.settings.record_sessions_percent.should.equal(75); window.mixpanel.settings.record_sessions_percent.should.be.type( 'number' ); - + window.mixpanel.settings.record_idle_timeout_ms.should.equal( + 1800000 + ); + window.mixpanel.settings.record_idle_timeout_ms.should.be.type( + 'number' + ); + window.mixpanel.settings.record_max_ms.should.equal(86400000); + window.mixpanel.settings.record_max_ms.should.be.type('number'); + window.mixpanel.settings.record_min_ms.should.equal(5000); + window.mixpanel.settings.record_min_ms.should.be.type('number'); window.mixpanel.settings.record_heatmap_data.should.equal(true); window.mixpanel.settings.record_heatmap_data.should.be.type( 'boolean' ); - window.mixpanel.settings.autocapture.should.equal(true); window.mixpanel.settings.autocapture.should.be.type('boolean'); - window.mixpanel.settings.record_canvas.should.equal(false); window.mixpanel.settings.record_canvas.should.be.type('boolean'); - window.mixpanel.settings.record_mask_text_selector.should.equal( '.secret' ); @@ -728,69 +599,6 @@ describe('Mixpanel Forwarder', function () { window.mixpanel.settings.record_block_class.should.be.type( 'string' ); - window.mixpanel.settings.record_idle_timeout_ms.should.equal( - 1800000 - ); - window.mixpanel.settings.record_idle_timeout_ms.should.be.type( - 'number' - ); - window.mixpanel.settings.record_max_ms.should.equal(86400000); - window.mixpanel.settings.record_max_ms.should.be.type('number'); - - window.mixpanel.settings.record_min_ms.should.equal(5000); - window.mixpanel.settings.record_min_ms.should.be.type('number'); - - done(); - }); - - it('should handle extreme timing values within valid ranges', function (done) { - window.mixpanel = new MPMock(); - mParticle.forwarder.init( - { - token: 'token123', - baseUrl: API_HOST, - recordIdleTimeoutMs: '2147483647', // Max 32-bit int - recordMaxMs: '86400000', // 24 hours in ms - recordMinMs: '1', - }, - reportService.cb, - true - ); - - window.mixpanel.settings.should.have.property( - 'record_idle_timeout_ms', - 2147483647 - ); - window.mixpanel.settings.should.have.property( - 'record_max_ms', - 86400000 - ); - window.mixpanel.settings.should.have.property('record_min_ms', 1); - - done(); - }); - - it('should handle special characters in selector settings', function (done) { - window.mixpanel = new MPMock(); - mParticle.forwarder.init( - { - token: 'token123', - baseUrl: API_HOST, - recordMaskTextSelector: - '[data-sensitive="true"], .pii, #secret', - recordBlockSelector: - 'iframe[src*="youtube"], video:not(.public)', - }, - reportService.cb, - true - ); - - window.mixpanel.settings.record_mask_text_selector.should.equal( - '[data-sensitive="true"], .pii, #secret' - ); - window.mixpanel.settings.record_block_selector.should.equal( - 'iframe[src*="youtube"], video:not(.public)' - ); done(); }); From 52866e628941ebcd632f2088213755668e2ceb7e Mon Sep 17 00:00:00 2001 From: Jaissica Date: Fri, 19 Dec 2025 10:02:36 -0500 Subject: [PATCH 3/5] remove range check from sessionPercent and update test --- src/MixpanelEventForwarder.js | 7 ++-- test/src/tests.js | 66 ----------------------------------- 2 files changed, 2 insertions(+), 71 deletions(-) diff --git a/src/MixpanelEventForwarder.js b/src/MixpanelEventForwarder.js index 110dbc4..2273f85 100644 --- a/src/MixpanelEventForwarder.js +++ b/src/MixpanelEventForwarder.js @@ -72,11 +72,8 @@ var constructor = function () { forwarderSettings.recordSessionsPercent, 10 ); - if ( - !isNaN(sessionPercent) && - sessionPercent >= 0 && - sessionPercent <= 100 - ) { + + if (!isNaN(sessionPercent)) { initOptions.record_sessions_percent = sessionPercent; } } diff --git a/test/src/tests.js b/test/src/tests.js index a47bc61..adbb235 100644 --- a/test/src/tests.js +++ b/test/src/tests.js @@ -377,72 +377,6 @@ describe('Mixpanel Forwarder', function () { done(); }); - it('should validate recordSessionsPercent range (0-100)', function (done) { - // Test valid upper boundary: 100 - window.mixpanel = new MPMock(); - mParticle.forwarder.init( - { - token: 'token123', - baseUrl: API_HOST, - recordSessionsPercent: '100', - }, - reportService.cb, - true - ); - window.mixpanel.settings.should.have.property( - 'record_sessions_percent', - 100 - ); - - // Test valid lower boundary: 0 - window.mixpanel = new MPMock(); - mParticle.forwarder.init( - { - token: 'token123', - baseUrl: API_HOST, - recordSessionsPercent: '0', - }, - reportService.cb, - true - ); - window.mixpanel.settings.should.have.property( - 'record_sessions_percent', - 0 - ); - - // Test invalid: negative - window.mixpanel = new MPMock(); - mParticle.forwarder.init( - { - token: 'token123', - baseUrl: API_HOST, - recordSessionsPercent: '-10', - }, - reportService.cb, - true - ); - window.mixpanel.settings.should.not.have.property( - 'record_sessions_percent' - ); - - // Test invalid: > 100 - window.mixpanel = new MPMock(); - mParticle.forwarder.init( - { - token: 'token123', - baseUrl: API_HOST, - recordSessionsPercent: '150', - }, - reportService.cb, - true - ); - window.mixpanel.settings.should.not.have.property( - 'record_sessions_percent' - ); - - done(); - }); - it('should ignore null and undefined values for all setting types', function (done) { window.mixpanel = new MPMock(); mParticle.forwarder.init( From f2c2ed04a8f3ffdb812e40ed655a8e731dc52d02 Mon Sep 17 00:00:00 2001 From: Jaissica Date: Fri, 19 Dec 2025 10:20:59 -0500 Subject: [PATCH 4/5] refactor boolean, numeric and string session replay settings --- src/MixpanelEventForwarder.js | 132 ++++++++++++++++------------------ 1 file changed, 63 insertions(+), 69 deletions(-) diff --git a/src/MixpanelEventForwarder.js b/src/MixpanelEventForwarder.js index 2273f85..ce575c2 100644 --- a/src/MixpanelEventForwarder.js +++ b/src/MixpanelEventForwarder.js @@ -67,78 +67,72 @@ var constructor = function () { api_host: forwarderSettings.baseUrl, }; - if (forwarderSettings.recordSessionsPercent != null) { - var sessionPercent = parseInt( - forwarderSettings.recordSessionsPercent, - 10 - ); - - if (!isNaN(sessionPercent)) { - initOptions.record_sessions_percent = sessionPercent; - } - } - - if (forwarderSettings.recordHeatmapData != null) { - initOptions.record_heatmap_data = - forwarderSettings.recordHeatmapData === 'True'; - } - - if (forwarderSettings.autocapture != null) { - initOptions.autocapture = - forwarderSettings.autocapture === 'True'; - } - - // Privacy and masking settings - if (forwarderSettings.recordMaskTextSelector) { - initOptions.record_mask_text_selector = - forwarderSettings.recordMaskTextSelector; - } - - if (forwarderSettings.recordBlockSelector) { - initOptions.record_block_selector = - forwarderSettings.recordBlockSelector; - } - - if (forwarderSettings.recordBlockClass) { - initOptions.record_block_class = - forwarderSettings.recordBlockClass; - } - - if (forwarderSettings.recordMaskTextClass) { - initOptions.record_mask_text_class = - forwarderSettings.recordMaskTextClass; - } - - // Canvas recording (experimental) - if (forwarderSettings.recordCanvas != null) { - initOptions.record_canvas = - forwarderSettings.recordCanvas === 'True'; - } - - // Timing settings - if (forwarderSettings.recordIdleTimeoutMs != null) { - var idleTimeoutMs = parseInt( - forwarderSettings.recordIdleTimeoutMs, - 10 - ); - if (!isNaN(idleTimeoutMs)) { - initOptions.record_idle_timeout_ms = idleTimeoutMs; + // Session Replay boolean settings + var boolSettings = [ + { key: 'recordHeatmapData', mappedKey: 'record_heatmap_data' }, + { key: 'autocapture', mappedKey: 'autocapture' }, + { key: 'recordCanvas', mappedKey: 'record_canvas' }, + ]; + + // Session Replay numeric settings + var numericSettings = [ + { + key: 'recordSessionsPercent', + mappedKey: 'record_sessions_percent', + }, + { + key: 'recordIdleTimeoutMs', + mappedKey: 'record_idle_timeout_ms', + }, + { key: 'recordMaxMs', mappedKey: 'record_max_ms' }, + { key: 'recordMinMs', mappedKey: 'record_min_ms' }, + ]; + + // Session Replay string settings + var stringSettings = [ + { + key: 'recordMaskTextSelector', + mappedKey: 'record_mask_text_selector', + }, + { + key: 'recordBlockSelector', + mappedKey: 'record_block_selector', + }, + { key: 'recordBlockClass', mappedKey: 'record_block_class' }, + { + key: 'recordMaskTextClass', + mappedKey: 'record_mask_text_class', + }, + ]; + + // Process boolean settings + boolSettings.forEach(function (setting) { + if (forwarderSettings[setting.key] != null) { + initOptions[setting.mappedKey] = + forwarderSettings[setting.key] === 'True'; } - } - - if (forwarderSettings.recordMaxMs != null) { - var maxMs = parseInt(forwarderSettings.recordMaxMs, 10); - if (!isNaN(maxMs)) { - initOptions.record_max_ms = maxMs; + }); + + // Process numeric settings + numericSettings.forEach(function (setting) { + if (forwarderSettings[setting.key] != null) { + var numericValue = parseInt( + forwarderSettings[setting.key], + 10 + ); + if (!isNaN(numericValue)) { + initOptions[setting.mappedKey] = numericValue; + } } - } + }); - if (forwarderSettings.recordMinMs != null) { - var minMs = parseInt(forwarderSettings.recordMinMs, 10); - if (!isNaN(minMs)) { - initOptions.record_min_ms = minMs; + // Process string settings + stringSettings.forEach(function (setting) { + if (forwarderSettings[setting.key]) { + initOptions[setting.mappedKey] = + forwarderSettings[setting.key]; } - } + }); mixpanel.init(settings.token, initOptions, 'mparticle'); @@ -265,7 +259,7 @@ var constructor = function () { // When mParticle identifies a user, because the user might // actually be anonymous, we only want to send an // identify request to Mixpanel if the user is - // actually known. If a user has any user identities, they are + // actually known. If a user has any user identities, they are // considered to be "known" users. var userIdentities = getUserIdentities(user); From d3ae4f2c19a889043b66bfbc77948196291fbac1 Mon Sep 17 00:00:00 2001 From: Jaissica Date: Fri, 19 Dec 2025 12:19:58 -0500 Subject: [PATCH 5/5] added utility function parseIntSafe --- src/MixpanelEventForwarder.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/MixpanelEventForwarder.js b/src/MixpanelEventForwarder.js index ce575c2..bd33cf5 100644 --- a/src/MixpanelEventForwarder.js +++ b/src/MixpanelEventForwarder.js @@ -115,14 +115,9 @@ var constructor = function () { // Process numeric settings numericSettings.forEach(function (setting) { - if (forwarderSettings[setting.key] != null) { - var numericValue = parseInt( - forwarderSettings[setting.key], - 10 - ); - if (!isNaN(numericValue)) { - initOptions[setting.mappedKey] = numericValue; - } + var numericValue = parseIntSafe(forwarderSettings[setting.key]); + if (numericValue !== undefined) { + initOptions[setting.mappedKey] = numericValue; } }); @@ -460,6 +455,11 @@ function isObject(val) { ); } +function parseIntSafe(value) { + var n = parseInt(value, 10); + return isNaN(n) ? undefined : n; +} + if (typeof window !== 'undefined') { if (window && window.mParticle && window.mParticle.addForwarder) { window.mParticle.addForwarder({