From 3cae53b4e59a28f6751bc06aa67e2739914de58a Mon Sep 17 00:00:00 2001 From: shrouti1507 Date: Tue, 22 Oct 2024 10:14:06 +0530 Subject: [PATCH 1/6] feat: add session replay for mixpanel --- .../src/integrations/Mixpanel/browser.js | 20 ++++++++++++++++++- .../src/integrations/Mixpanel/util.js | 15 +++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/analytics-js-integrations/src/integrations/Mixpanel/browser.js b/packages/analytics-js-integrations/src/integrations/Mixpanel/browser.js index 24d918c0fc..031e2233e5 100644 --- a/packages/analytics-js-integrations/src/integrations/Mixpanel/browser.js +++ b/packages/analytics-js-integrations/src/integrations/Mixpanel/browser.js @@ -16,6 +16,7 @@ import { inverseObjectArrays, getConsolidatedPageCalls, generatePageCustomEventName, + getDestinationOptions, } from './util'; import { loadNativeSdk } from './nativeSdkLoader'; @@ -66,13 +67,14 @@ class Mixpanel { this.ignoreDnt = config.ignoreDnt || false; this.useUserDefinedPageEventName = config.useUserDefinedPageEventName || false; this.userDefinedPageEventTemplate = config.userDefinedPageEventTemplate; + this.sessionReplayPercentage = config.sessionReplayPercentage; this.isNativeSDKLoaded = false; } init() { // eslint-disable-next-line no-var loadNativeSdk(); - const options = { + let options = { cross_subdomain_cookie: this.crossSubdomainCookie || false, secure_cookie: this.secureCookie || false, }; @@ -94,6 +96,22 @@ class Mixpanel { if (this.ignoreDnt) { options.ignore_dnt = true; } + + const mixpanelIntgConfig = getDestinationOptions(this.analytics.loadOnlyIntegrations); + // ref : https://docs.mixpanel.com/docs/tracking-methods/sdks/javascript#session-replay + if (mixpanelIntgConfig) { + const sessionReplayConfig = { + record_sessions_percent : this.sessionReplayPercentage, + record_block_class : mixpanelIntgConfig?.recordBlockClass, + record_collect_fonts : mixpanelIntgConfig?.recordCollectFonts, + record_idle_timeout_ms : mixpanelIntgConfig?.recordIdleTimeout, + record_mask_text_class : mixpanelIntgConfig?.recordMaskTextClass, + record_mask_text_selector : mixpanelIntgConfig?.recordMaskTextSelector, + record_max_ms : mixpanelIntgConfig?.recordMaxMs, + record_min_ms : mixpanelIntgConfig?.recordMinMs + }; + options = {...options,...removeUndefinedAndNullValues(sessionReplayConfig)} + } options.loaded = () => { this.isNativeSDKLoaded = true; }; diff --git a/packages/analytics-js-integrations/src/integrations/Mixpanel/util.js b/packages/analytics-js-integrations/src/integrations/Mixpanel/util.js index e6ce2d0feb..5bc2309ff2 100644 --- a/packages/analytics-js-integrations/src/integrations/Mixpanel/util.js +++ b/packages/analytics-js-integrations/src/integrations/Mixpanel/util.js @@ -2,7 +2,7 @@ /* eslint-disable no-restricted-syntax */ /* eslint-disable no-prototype-builtins */ import get from 'get-value'; -import { DISPLAY_NAME } from '@rudderstack/analytics-js-common/constants/integrations/Mixpanel/constants'; +import { DISPLAY_NAME, NAME } from '@rudderstack/analytics-js-common/constants/integrations/Mixpanel/constants'; import Logger from '../../utils/logger'; import { getDefinedTraits, extractCustomFields, isDefinedAndNotNull } from '../../utils/utils'; @@ -273,6 +273,18 @@ const generatePageCustomEventName = (message, userDefinedEventTemplate) => { return eventName; }; +/** + * Get destination specific options from integrations options + * By default, it will return options for the destination using its display name + * If display name is not present, it will return options for the destination using its name + * The fallback is only for backward compatibility with SDK versions < v1.1 + * @param {object} integrationsOptions Integrations options object + * @returns destination specific options + */ +const getDestinationOptions = integrationsOptions => + integrationsOptions && (integrationsOptions[DISPLAY_NAME] || integrationsOptions[NAME]); + + export { mapTraits, unionArrays, @@ -285,4 +297,5 @@ export { filterSetOnceTraits, unset, generatePageCustomEventName, + getDestinationOptions }; From 274007dbeb0e44518a0df5a55fae815e6fba058b Mon Sep 17 00:00:00 2001 From: shrouti1507 Date: Tue, 22 Oct 2024 19:16:30 +0530 Subject: [PATCH 2/6] feat: edit in logic and adding test cases --- .../integrations/Mixpanel/browser.test.js | 137 ++++++++++++++++++ .../src/integrations/Mixpanel/browser.js | 35 +++-- 2 files changed, 162 insertions(+), 10 deletions(-) diff --git a/packages/analytics-js-integrations/__tests__/integrations/Mixpanel/browser.test.js b/packages/analytics-js-integrations/__tests__/integrations/Mixpanel/browser.test.js index f086279b08..daa385f0b2 100644 --- a/packages/analytics-js-integrations/__tests__/integrations/Mixpanel/browser.test.js +++ b/packages/analytics-js-integrations/__tests__/integrations/Mixpanel/browser.test.js @@ -76,6 +76,143 @@ describe('Init tests', () => { loaded: expect.any(Function), }); }); + + test('Session replay configuration', () => { + const analytics = { + loadOnlyIntegrations: { + Mixpanel: { + recordBlockClass: 'block-class', + recordCollectFonts: true, + recordIdleTimeout: 5000, + recordMaskTextClass: 'mask-text', + recordMaskTextSelector: '.sensitive', + recordMaxMs: 30000, + recordMinMs: 1000, + }, + }, + logLevel: 'debug', + }; + + mixpanel = new Mixpanel( + { + persistenceType: 'localStorage', + persistenceName: 'test', + sessionReplayPercentage: 50, + }, + analytics, + { logLevel: 'debug' }, + ); + mixpanel.init(); + + expect(window.mixpanel._i[0][1]).toEqual({ + cross_subdomain_cookie: false, + secure_cookie: false, + persistence: 'localStorage', + persistence_name: 'test', + record_block_class: 'block-class', + record_collect_fonts: true, + record_idle_timeout_ms: 5000, + record_mask_text_class: 'mask-text', + record_mask_text_selector: '.sensitive', + record_max_ms: 30000, + record_min_ms: 1000, + record_sessions_percent: 50, + loaded: expect.any(Function), + }); + }); + + test('Session replay configuration with partial options', () => { + const analytics = { + loadOnlyIntegrations: { + Mixpanel: { + recordBlockClass: 'block-class', + recordCollectFonts: true, + }, + }, + logLevel: 'debug', + }; + + mixpanel = new Mixpanel( + { + persistenceType: 'localStorage', + persistenceName: 'test', + }, + analytics, + { logLevel: 'debug' }, + ); + mixpanel.init(); + + expect(window.mixpanel._i[0][1]).toEqual({ + cross_subdomain_cookie: false, + secure_cookie: false, + persistence: 'localStorage', + persistence_name: 'test', + record_block_class: 'block-class', + record_collect_fonts: true, + loaded: expect.any(Function), + }); + }); + + test('Session replay configuration without loadOnlyIntegrations', () => { + const analytics = { + logLevel: 'debug', + }; + + mixpanel = new Mixpanel( + { + persistenceType: 'localStorage', + persistenceName: 'test', + sessionReplayPercentage: 75, + }, + analytics, + { logLevel: 'debug' }, + ); + mixpanel.init(); + + expect(window.mixpanel._i[0][1]).toEqual({ + cross_subdomain_cookie: false, + secure_cookie: false, + persistence: 'localStorage', + persistence_name: 'test', + record_sessions_percent: 75, + loaded: expect.any(Function), + }); + }); + + test('Session replay configuration with invalid percentage', () => { + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + const analytics = { + logLevel: 'debug', + }; + + mixpanel = new Mixpanel( + { + persistenceType: 'localStorage', + persistenceName: 'test', + sessionReplayPercentage: '101', + }, + analytics, + { logLevel: 'debug' }, + ); + mixpanel.init(); + + expect(window.mixpanel._i[0][1]).toEqual({ + cross_subdomain_cookie: false, + secure_cookie: false, + persistence: 'localStorage', + persistence_name: 'test', + loaded: expect.any(Function), + }); + + expect(consoleSpy).toHaveBeenCalledWith( + '%c RS SDK - Mixpanel %c Invalid sessionReplayPercentage: 101. It should be a string matching the pattern "^(100|[1-9]?[0-9])$"', + 'font-weight: bold; background: black; color: white;', + 'font-weight: normal;', + ); + + consoleSpy.mockRestore(); + }); }); describe('isLoaded and isReady tests', () => { diff --git a/packages/analytics-js-integrations/src/integrations/Mixpanel/browser.js b/packages/analytics-js-integrations/src/integrations/Mixpanel/browser.js index 031e2233e5..e492742bb3 100644 --- a/packages/analytics-js-integrations/src/integrations/Mixpanel/browser.js +++ b/packages/analytics-js-integrations/src/integrations/Mixpanel/browser.js @@ -5,7 +5,12 @@ import { DISPLAY_NAME, } from '@rudderstack/analytics-js-common/constants/integrations/Mixpanel/constants'; import Logger from '../../utils/logger'; -import { pick, removeUndefinedAndNullValues, isNotEmpty } from '../../utils/commonUtils'; +import { + pick, + removeUndefinedAndNullValues, + isNotEmpty, + isDefinedAndNotNull, +} from '../../utils/commonUtils'; import { mapTraits, unionArrays, @@ -101,16 +106,26 @@ class Mixpanel { // ref : https://docs.mixpanel.com/docs/tracking-methods/sdks/javascript#session-replay if (mixpanelIntgConfig) { const sessionReplayConfig = { - record_sessions_percent : this.sessionReplayPercentage, - record_block_class : mixpanelIntgConfig?.recordBlockClass, - record_collect_fonts : mixpanelIntgConfig?.recordCollectFonts, - record_idle_timeout_ms : mixpanelIntgConfig?.recordIdleTimeout, - record_mask_text_class : mixpanelIntgConfig?.recordMaskTextClass, - record_mask_text_selector : mixpanelIntgConfig?.recordMaskTextSelector, - record_max_ms : mixpanelIntgConfig?.recordMaxMs, - record_min_ms : mixpanelIntgConfig?.recordMinMs + record_block_class: mixpanelIntgConfig?.recordBlockClass, + record_collect_fonts: mixpanelIntgConfig?.recordCollectFonts, + record_idle_timeout_ms: mixpanelIntgConfig?.recordIdleTimeout, + record_mask_text_class: mixpanelIntgConfig?.recordMaskTextClass, + record_mask_text_selector: mixpanelIntgConfig?.recordMaskTextSelector, + record_max_ms: mixpanelIntgConfig?.recordMaxMs, + record_min_ms: mixpanelIntgConfig?.recordMinMs, }; - options = {...options,...removeUndefinedAndNullValues(sessionReplayConfig)} + options = { ...options, ...removeUndefinedAndNullValues(sessionReplayConfig) }; + } + + if (isDefinedAndNotNull(this.sessionReplayPercentage)) { + const percentageInt = parseInt(this.sessionReplayPercentage, 10); + if (percentageInt >= 0 && percentageInt <= 100) { + options.record_sessions_percent = percentageInt; + } else { + logger.warn( + `Invalid sessionReplayPercentage: ${this.sessionReplayPercentage}. It should be a string matching the pattern "^(100|[1-9]?[0-9])$"`, + ); + } } options.loaded = () => { this.isNativeSDKLoaded = true; From 4462d1d4e1df6b23c915cdf50f3ee032040ef116 Mon Sep 17 00:00:00 2001 From: shrouti1507 Date: Tue, 22 Oct 2024 19:39:27 +0530 Subject: [PATCH 3/6] feat: increasing code coverage --- .../integrations/Mixpanel/browser.test.js | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/analytics-js-integrations/__tests__/integrations/Mixpanel/browser.test.js b/packages/analytics-js-integrations/__tests__/integrations/Mixpanel/browser.test.js index daa385f0b2..685f932193 100644 --- a/packages/analytics-js-integrations/__tests__/integrations/Mixpanel/browser.test.js +++ b/packages/analytics-js-integrations/__tests__/integrations/Mixpanel/browser.test.js @@ -124,7 +124,7 @@ describe('Init tests', () => { test('Session replay configuration with partial options', () => { const analytics = { loadOnlyIntegrations: { - Mixpanel: { + MP: { recordBlockClass: 'block-class', recordCollectFonts: true, }, @@ -213,6 +213,35 @@ describe('Init tests', () => { consoleSpy.mockRestore(); }); + + test('Session replay configuration', () => { + const analytics = { + loadOnlyIntegrations: { + Mixpanel: {}, + }, + logLevel: 'debug', + }; + + mixpanel = new Mixpanel( + { + persistenceType: 'localStorage', + persistenceName: 'test', + sessionReplayPercentage: 50, + }, + analytics, + { logLevel: 'debug' }, + ); + mixpanel.init(); + + expect(window.mixpanel._i[0][1]).toEqual({ + cross_subdomain_cookie: false, + secure_cookie: false, + persistence: 'localStorage', + persistence_name: 'test', + record_sessions_percent: 50, + loaded: expect.any(Function), + }); + }); }); describe('isLoaded and isReady tests', () => { From 71defdbc1baba5736ad4d685ca8bfb7308bf8d69 Mon Sep 17 00:00:00 2001 From: shrouti1507 Date: Tue, 22 Oct 2024 19:55:35 +0530 Subject: [PATCH 4/6] feat: increasing code coverage --- .../integrations/Mixpanel/browser.test.js | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/analytics-js-integrations/__tests__/integrations/Mixpanel/browser.test.js b/packages/analytics-js-integrations/__tests__/integrations/Mixpanel/browser.test.js index 685f932193..9cc3a624ce 100644 --- a/packages/analytics-js-integrations/__tests__/integrations/Mixpanel/browser.test.js +++ b/packages/analytics-js-integrations/__tests__/integrations/Mixpanel/browser.test.js @@ -214,7 +214,7 @@ describe('Init tests', () => { consoleSpy.mockRestore(); }); - test('Session replay configuration', () => { + test('Session replay configuration with emppty load integration', () => { const analytics = { loadOnlyIntegrations: { Mixpanel: {}, @@ -242,6 +242,51 @@ describe('Init tests', () => { loaded: expect.any(Function), }); }); + + test('Session replay configuration with all options', () => { + const analytics = { + loadOnlyIntegrations: { + Mixpanel: { + recordBlockClass: 'block-class', + recordCollectFonts: true, + recordIdleTimeout: 5000, + recordMaskTextClass: 'mask-text', + recordMaskTextSelector: '.sensitive', + recordMaxMs: 30000, + recordMinMs: 1000, + }, + }, + logLevel: 'debug', + }; + + mixpanel = new Mixpanel( + { + persistenceType: 'localStorage', + persistenceName: 'test', + sessionReplayPercentage: 50, + }, + analytics, + { logLevel: 'debug' }, + ); + mixpanel.init(); + + expect(window.mixpanel._i[0][1]).toEqual({ + cross_subdomain_cookie: false, + secure_cookie: false, + persistence: 'localStorage', + persistence_name: 'test', + record_block_class: 'block-class', + record_collect_fonts: true, + record_idle_timeout_ms: 5000, + record_mask_text_class: 'mask-text', + record_mask_text_selector: '.sensitive', + record_max_ms: 30000, + record_min_ms: 1000, + record_sessions_percent: 50, + loaded: expect.any(Function), + }); + }); + }); describe('isLoaded and isReady tests', () => { From 7c22dbc4c799436fbc46b2d8cb2c4b893041ccc3 Mon Sep 17 00:00:00 2001 From: shrouti1507 Date: Tue, 22 Oct 2024 20:07:16 +0530 Subject: [PATCH 5/6] feat: increasing code coverage --- .../src/integrations/Mixpanel/browser.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/analytics-js-integrations/src/integrations/Mixpanel/browser.js b/packages/analytics-js-integrations/src/integrations/Mixpanel/browser.js index e492742bb3..24bd1eb5a4 100644 --- a/packages/analytics-js-integrations/src/integrations/Mixpanel/browser.js +++ b/packages/analytics-js-integrations/src/integrations/Mixpanel/browser.js @@ -106,13 +106,13 @@ class Mixpanel { // ref : https://docs.mixpanel.com/docs/tracking-methods/sdks/javascript#session-replay if (mixpanelIntgConfig) { const sessionReplayConfig = { - record_block_class: mixpanelIntgConfig?.recordBlockClass, - record_collect_fonts: mixpanelIntgConfig?.recordCollectFonts, - record_idle_timeout_ms: mixpanelIntgConfig?.recordIdleTimeout, - record_mask_text_class: mixpanelIntgConfig?.recordMaskTextClass, - record_mask_text_selector: mixpanelIntgConfig?.recordMaskTextSelector, - record_max_ms: mixpanelIntgConfig?.recordMaxMs, - record_min_ms: mixpanelIntgConfig?.recordMinMs, + record_block_class: mixpanelIntgConfig.recordBlockClass, + record_collect_fonts: mixpanelIntgConfig.recordCollectFonts, + record_idle_timeout_ms: mixpanelIntgConfig.recordIdleTimeout, + record_mask_text_class: mixpanelIntgConfig.recordMaskTextClass, + record_mask_text_selector: mixpanelIntgConfig.recordMaskTextSelector, + record_max_ms: mixpanelIntgConfig.recordMaxMs, + record_min_ms: mixpanelIntgConfig.recordMinMs, }; options = { ...options, ...removeUndefinedAndNullValues(sessionReplayConfig) }; } From c23e83aa8c9fec843618fffef78e1183645a49b5 Mon Sep 17 00:00:00 2001 From: shrouti1507 Date: Thu, 24 Oct 2024 19:35:14 +0530 Subject: [PATCH 6/6] fix: code review addressed --- .../integrations/Mixpanel/browser.test.js | 3 -- .../src/integrations/Mixpanel/browser.js | 28 ++++++++++--------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/packages/analytics-js-integrations/__tests__/integrations/Mixpanel/browser.test.js b/packages/analytics-js-integrations/__tests__/integrations/Mixpanel/browser.test.js index 9cc3a624ce..c8fb19346e 100644 --- a/packages/analytics-js-integrations/__tests__/integrations/Mixpanel/browser.test.js +++ b/packages/analytics-js-integrations/__tests__/integrations/Mixpanel/browser.test.js @@ -147,8 +147,6 @@ describe('Init tests', () => { secure_cookie: false, persistence: 'localStorage', persistence_name: 'test', - record_block_class: 'block-class', - record_collect_fonts: true, loaded: expect.any(Function), }); }); @@ -286,7 +284,6 @@ describe('Init tests', () => { loaded: expect.any(Function), }); }); - }); describe('isLoaded and isReady tests', () => { diff --git a/packages/analytics-js-integrations/src/integrations/Mixpanel/browser.js b/packages/analytics-js-integrations/src/integrations/Mixpanel/browser.js index 24bd1eb5a4..c70c40da1c 100644 --- a/packages/analytics-js-integrations/src/integrations/Mixpanel/browser.js +++ b/packages/analytics-js-integrations/src/integrations/Mixpanel/browser.js @@ -103,30 +103,32 @@ class Mixpanel { } const mixpanelIntgConfig = getDestinationOptions(this.analytics.loadOnlyIntegrations); - // ref : https://docs.mixpanel.com/docs/tracking-methods/sdks/javascript#session-replay - if (mixpanelIntgConfig) { - const sessionReplayConfig = { - record_block_class: mixpanelIntgConfig.recordBlockClass, - record_collect_fonts: mixpanelIntgConfig.recordCollectFonts, - record_idle_timeout_ms: mixpanelIntgConfig.recordIdleTimeout, - record_mask_text_class: mixpanelIntgConfig.recordMaskTextClass, - record_mask_text_selector: mixpanelIntgConfig.recordMaskTextSelector, - record_max_ms: mixpanelIntgConfig.recordMaxMs, - record_min_ms: mixpanelIntgConfig.recordMinMs, - }; - options = { ...options, ...removeUndefinedAndNullValues(sessionReplayConfig) }; - } + // ref : https://docs.mixpanel.com/docs/tracking-methods/sdks/javascript#session-replay if (isDefinedAndNotNull(this.sessionReplayPercentage)) { const percentageInt = parseInt(this.sessionReplayPercentage, 10); if (percentageInt >= 0 && percentageInt <= 100) { options.record_sessions_percent = percentageInt; + + if (mixpanelIntgConfig) { + const sessionReplayConfig = removeUndefinedAndNullValues({ + record_block_class: mixpanelIntgConfig.recordBlockClass, + record_collect_fonts: mixpanelIntgConfig.recordCollectFonts, + record_idle_timeout_ms: mixpanelIntgConfig.recordIdleTimeout, + record_mask_text_class: mixpanelIntgConfig.recordMaskTextClass, + record_mask_text_selector: mixpanelIntgConfig.recordMaskTextSelector, + record_max_ms: mixpanelIntgConfig.recordMaxMs, + record_min_ms: mixpanelIntgConfig.recordMinMs, + }); + options = { ...options, ...sessionReplayConfig }; + } } else { logger.warn( `Invalid sessionReplayPercentage: ${this.sessionReplayPercentage}. It should be a string matching the pattern "^(100|[1-9]?[0-9])$"`, ); } } + options.loaded = () => { this.isNativeSDKLoaded = true; };