diff --git a/.github/workflows/cross-browser-testing-beta.yml b/.github/workflows/cross-browser-testing-beta.yml new file mode 100644 index 000000000..c4eb7ecd6 --- /dev/null +++ b/.github/workflows/cross-browser-testing-beta.yml @@ -0,0 +1,40 @@ +name: 'BrowserStack Beta Browsers Test' +on: [push, workflow_dispatch] + +jobs: + browserstack-beta-test: + name: 'BrowserStack Beta Browsers Test' + runs-on: ubuntu-latest + steps: + - name: 'BrowserStack Env Setup' + uses: browserstack/github-actions/setup-env@master + with: + username: ${{ secrets.BROWSERSTACK_USERNAME }} + access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + + - name: 'BrowserStack Local Tunnel Setup' + uses: browserstack/github-actions/setup-local@master + with: + local-testing: start + local-identifier: random + + - name: 'Checkout the repository' + uses: actions/checkout@v3 + + - name: 'Run NPM CI' + run: npm ci + + - name: Run Build IIFE + run: npm run build:iife + + - name: 'Run Build test bundle' + run: npm run build:test-bundle + + - name: 'Run BrowserStack Beta Browsers Test' + run: npm run test:browserstack-beta + + - name: 'BrowserStackLocal Stop' + uses: browserstack/github-actions/setup-local@master + with: + local-testing: stop + diff --git a/CHANGELOG.md b/CHANGELOG.md index 29eb7e39d..317eb37b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +# [2.50.0](https://github.com/mParticle/mparticle-web-sdk/compare/v2.49.0...v2.50.0) (2025-11-13) + + +### Features + +* SDKE-221 Support hashedEmailUserIdentityType for "other" identity type ([#1052](https://github.com/mParticle/mparticle-web-sdk/issues/1052)) ([3fb8cb1](https://github.com/mParticle/mparticle-web-sdk/commit/3fb8cb123e6213d6a832a9455f5285e1c9989271)) + +# [2.49.0](https://github.com/mParticle/mparticle-web-sdk/compare/v2.48.0...v2.49.0) (2025-11-10) + + +### Features + +* add BrowserStack beta suite and workflow ([#1074](https://github.com/mParticle/mparticle-web-sdk/issues/1074)) ([8c5749d](https://github.com/mParticle/mparticle-web-sdk/commit/8c5749d42bdfe3c4a2ef0ca8e58d17705132218e)) + # [2.48.0](https://github.com/mParticle/mparticle-web-sdk/compare/v2.47.1...v2.48.0) (2025-10-27) diff --git a/dist/mparticle.common.js b/dist/mparticle.common.js index 65856afe8..0fa140b4e 100644 --- a/dist/mparticle.common.js +++ b/dist/mparticle.common.js @@ -15,7 +15,7 @@ map:function map(a,b){var c,d,e;if(null===this)throw new TypeError(" this is nul filter:function filter(a/*, thisArg*/){if(void 0===this||null===this)throw new TypeError;var b=Object(this),c=b.length>>>0;if("function"!=typeof a)throw new TypeError;for(var d=[],e=2<=arguments.length?arguments[1]:void 0,f=0;ff);o++)n=h[o],b[n]?(a.Logger.verb if(d[g].mpid){var h=d[g].ui;for(var i in h)if(e===i&&b.userIdentities[e]===h[i]){c=g;break}}c&&f.storeDataInMemory(d,c);},this.encodePersistence=function(b){for(var c in b=JSON.parse(b),b.gs)b.gs.hasOwnProperty(c)&&(Base64CookieKeys[c]?b.gs[c]?Array.isArray(b.gs[c])&&b.gs[c].length||a._Helpers.isObject(b.gs[c])&&Object.keys(b.gs[c]).length?b.gs[c]=Base64.encode(JSON.stringify(b.gs[c])):delete b.gs[c]:delete b.gs[c]:"ie"===c?b.gs[c]=b.gs[c]?1:0:!b.gs[c]&&delete b.gs[c]);for(var d in b)if(b.hasOwnProperty(d)&&!SDKv2NonMPIDCookieKeys[d])for(c in b[d])b[d].hasOwnProperty(c)&&Base64CookieKeys[c]&&(a._Helpers.isObject(b[d][c])&&Object.keys(b[d][c]).length?b[d][c]=Base64.encode(JSON.stringify(b[d][c])):delete b[d][c]);return createCookieString(JSON.stringify(b))},this.decodePersistence=function(b){try{if(b){if(b=JSON.parse(revertCookieString(b)),a._Helpers.isObject(b)&&Object.keys(b).length){for(var c in b.gs)b.gs.hasOwnProperty(c)&&(Base64CookieKeys[c]?b.gs[c]=JSON.parse(Base64.decode(b.gs[c])):"ie"===c&&(b.gs[c]=!!b.gs[c]));for(var d in b)if(b.hasOwnProperty(d))if(!SDKv2NonMPIDCookieKeys[d])for(c in b[d])b[d].hasOwnProperty(c)&&Base64CookieKeys[c]&&b[d][c].length&&(b[d][c]=JSON.parse(Base64.decode(b[d][c])));else "l"===d&&(b[d]=!!b[d]);}return JSON.stringify(b)}}catch(b){a.Logger.error("Problem with decoding cookie",b);}},this.getCookieDomain=function(){if(a._Store.SDKConfig.cookieDomain)return a._Store.SDKConfig.cookieDomain;var b=f.getDomain(document,location.hostname);return ""===b?"":"."+b},this.getDomain=function(a,b){var c,d,e=b.split(".");for(c=e.length-1;0<=c;c--)if(d=e.slice(c).join("."),a.cookie="mptest=cookie;domain=."+d+";",-1All of the following methods can be called on the primary mParticle class. In version 2.10.0, we introduced multiple instances. If you are using multiple instances (self hosted environments only), you should call these methods on each instance.

@@ -1654,6 +1655,6 @@ Array.prototype.forEach||(Array.prototype.forEach=Polyfill.forEach),Array.protot * @param {String} apiKey your mParticle assigned API key * @param {Object} [config] an options object for additional configuration * @param {String} [instanceName] If you are self hosting the JS SDK and working with multiple instances, you would pass an instanceName to `init`. This instance will be selected when invoking other methods. See the above link to the doc site for more info and examples. - */this.Store={},this._instances={},this.IdentityType=IdentityType,this.EventType=EventType,this.CommerceEventType=CommerceEventType,this.PromotionType=PromotionActionType,this.ProductActionType=ProductActionType,this.MPSideloadedKit=MPSideloadedKit,"undefined"!=typeof window&&(this.isIOS=!!(window.mParticle&&window.mParticle.isIOS)&&window.mParticle.isIOS,this.config=window.mParticle&&window.mParticle.config?window.mParticle.config:{}),this.init=function(b,c,d){!c&&window.mParticle&&window.mParticle.config&&(console.warn("You did not pass a config object to mParticle.init(). Attempting to use the window.mParticle.config if it exists. Please note that in a future release, this may not work and mParticle will not initialize properly"),c=window.mParticle?window.mParticle.config:{}),d=(d&&0!==d.length?d:Constants.DefaultInstance).toLowerCase();var e=a._instances[d];e===void 0&&(e=new mParticleInstance(d),a._instances[d]=e),e.captureTiming(PerformanceMarkType.SdkStart),e.setLauncherInstanceGuid(),e.init(b,c,d);},this.captureTiming=function(b){a.getInstance().captureTiming(b);},this.getInstance=function(b){var c;return b?(c=a._instances[b.toLowerCase()],c?c:(console.log("You tried to initialize an instance named "+b+". This instance does not exist. Check your instance name or initialize a new instance with this name before calling it."),null)):(b=Constants.DefaultInstance,c=a._instances[b],c||(c=new mParticleInstance(b),a._instances[Constants.DefaultInstance]=c),c)},this.Rokt=a.getInstance()._RoktManager,this.getDeviceId=function(){return a.getInstance().getDeviceId()},this.setDeviceId=function(b){return a.getInstance().setDeviceId(b)},this.isInitialized=function(){return a.getInstance().isInitialized()},this.startNewSession=function(){a.getInstance().startNewSession();},this.endSession=function(){a.getInstance().endSession();},this.setLogLevel=function(b){a.getInstance().setLogLevel(b);},this.ready=function(b){a.getInstance().ready(b);},this.setAppVersion=function(b){a.getInstance().setAppVersion(b);},this.getAppName=function(){return a.getInstance().getAppName()},this.setAppName=function(b){a.getInstance().setAppName(b);},this.getAppVersion=function(){return a.getInstance().getAppVersion()},this.getEnvironment=function(){return a.getInstance().getEnvironment()},this.stopTrackingLocation=function(){a.getInstance().stopTrackingLocation();},this.startTrackingLocation=function(b){a.getInstance().startTrackingLocation(b);},this.setPosition=function(b,c){a.getInstance().setPosition(b,c);},this.startNewSession=function(){a.getInstance().startNewSession();},this.endSession=function(){a.getInstance().endSession();},this.logBaseEvent=function(b,c){a.getInstance().logBaseEvent(b,c);},this.logEvent=function(b,c,d,e,f){a.getInstance().logEvent(b,c,d,e,f);},this.logError=function(b,c){a.getInstance().logError(b,c);},this.logLink=function(b,c,d,e){a.getInstance().logLink(b,c,d,e);},this.logForm=function(b,c,d,e){a.getInstance().logForm(b,c,d,e);},this.logPageView=function(b,c,d,e){a.getInstance().logPageView(b,c,d,e);},this.upload=function(){a.getInstance().upload();},this.eCommerce={Cart:{add:function add(b,c){a.getInstance().eCommerce.Cart.add(b,c);},remove:function remove(b,c){a.getInstance().eCommerce.Cart.remove(b,c);},clear:function clear(){a.getInstance().eCommerce.Cart.clear();}},setCurrencyCode:function setCurrencyCode(b){a.getInstance().eCommerce.setCurrencyCode(b);},createProduct:function createProduct(b,c,d,e,f,g,h,i,j,k){return a.getInstance().eCommerce.createProduct(b,c,d,e,f,g,h,i,j,k)},createPromotion:function createPromotion(b,c,d,e){return a.getInstance().eCommerce.createPromotion(b,c,d,e)},createImpression:function createImpression(b,c){return a.getInstance().eCommerce.createImpression(b,c)},createTransactionAttributes:function createTransactionAttributes(b,c,d,e,f,g){return a.getInstance().eCommerce.createTransactionAttributes(b,c,d,e,f,g)},logCheckout:function logCheckout(b,c,d,e){a.getInstance().eCommerce.logCheckout(b,c,d,e);},logProductAction:function logProductAction(b,c,d,e,f,g){a.getInstance().eCommerce.logProductAction(b,c,d,e,f,g);},logPurchase:function logPurchase(b,c,d,e,f){a.getInstance().eCommerce.logPurchase(b,c,d,e,f);},logPromotion:function logPromotion(b,c,d,e,f){a.getInstance().eCommerce.logPromotion(b,c,d,e,f);},logImpression:function logImpression(b,c,d,e){a.getInstance().eCommerce.logImpression(b,c,d,e);},logRefund:function logRefund(b,c,d,e,f){a.getInstance().eCommerce.logRefund(b,c,d,e,f);},expandCommerceEvent:function expandCommerceEvent(b){return a.getInstance().eCommerce.expandCommerceEvent(b)}},this.setSessionAttribute=function(b,c){a.getInstance().setSessionAttribute(b,c);},this.setOptOut=function(b){a.getInstance().setOptOut(b);},this.setIntegrationAttribute=function(b,c){a.getInstance().setIntegrationAttribute(b,c);},this.getIntegrationAttributes=function(b){return a.getInstance().getIntegrationAttributes(b)},this.Identity={HTTPCodes:Constants.HTTPCodes,aliasUsers:function aliasUsers(b,c){a.getInstance().Identity.aliasUsers(b,c);},createAliasRequest:function createAliasRequest(b,c){return a.getInstance().Identity.createAliasRequest(b,c)},getCurrentUser:function getCurrentUser(){return a.getInstance().Identity.getCurrentUser()},getUser:function getUser(b){return a.getInstance().Identity.getUser(b)},getUsers:function getUsers(){return a.getInstance().Identity.getUsers()},identify:function identify(b,c){a.getInstance().Identity.identify(b,c);},login:function login(b,c){a.getInstance().Identity.login(b,c);},logout:function logout(b,c){a.getInstance().Identity.logout(b,c);},modify:function modify(b,c){a.getInstance().Identity.modify(b,c);}},this.sessionManager={getSession:function getSession(){return a.getInstance()._SessionManager.getSession()}},this.Consent={createConsentState:function createConsentState(){return a.getInstance().Consent.createConsentState()},createGDPRConsent:function createGDPRConsent(b,c,d,e,f){return a.getInstance().Consent.createGDPRConsent(b,c,d,e,f)},createCCPAConsent:function createCCPAConsent(b,c,d,e,f){return a.getInstance().Consent.createGDPRConsent(b,c,d,e,f)}},this.reset=function(){a.getInstance().reset(a.getInstance());},this._resetForTests=function(b,c){"boolean"==typeof c?a.getInstance()._resetForTests(b,c,a.getInstance()):a.getInstance()._resetForTests(b,!1,a.getInstance());},this.configurePixel=function(b){a.getInstance().configurePixel(b);},this._setIntegrationDelay=function(b,c){a.getInstance()._setIntegrationDelay(b,c);},this._getIntegrationDelays=function(){return a.getInstance()._getIntegrationDelays()},this.getVersion=function(){return a.getInstance().getVersion()},this.generateHash=function(b){return a.getInstance().generateHash(b)},this.addForwarder=function(b){a.getInstance().addForwarder(b);},this._getActiveForwarders=function(){return a.getInstance()._getActiveForwarders()},this._setWrapperSDKInfo=function(b,c){a.getInstance()._setWrapperSDKInfo(b,c);};}var mParticleManager=new mParticleInstanceManager;"undefined"!=typeof window&&(window.mParticle=mParticleManager,window.mParticle._BatchValidator=new _BatchValidator); + */this.Store={},this._instances={},this.IdentityType=IdentityType,this.EventType=EventType,this.CommerceEventType=CommerceEventType,this.PromotionType=PromotionActionType,this.ProductActionType=ProductActionType,this.MPSideloadedKit=MPSideloadedKit,"undefined"!=typeof window&&(this.isIOS=!!(window.mParticle&&window.mParticle.isIOS)&&window.mParticle.isIOS,this.config=window.mParticle&&window.mParticle.config?window.mParticle.config:{}),this.init=function(b,c,d){!c&&window.mParticle&&window.mParticle.config&&(console.warn("You did not pass a config object to mParticle.init(). Attempting to use the window.mParticle.config if it exists. Please note that in a future release, this may not work and mParticle will not initialize properly"),c=window.mParticle?window.mParticle.config:{}),d=(d&&0!==d.length?d:Constants.DefaultInstance).toLowerCase();var e=a._instances[d];e===void 0&&(e=new mParticleInstance(d),a._instances[d]=e),e.captureTiming(PerformanceMarkType.SdkStart),e.setLauncherInstanceGuid(),e.init(b,c,d);},this.captureTiming=function(b){a.getInstance().captureTiming(b);},this.getInstance=function(b){var c;return b?(c=a._instances[b.toLowerCase()],c?c:(console.log("You tried to initialize an instance named "+b+". This instance does not exist. Check your instance name or initialize a new instance with this name before calling it."),null)):(b=Constants.DefaultInstance,c=a._instances[b],c||(c=new mParticleInstance(b),a._instances[Constants.DefaultInstance]=c),c)},this.Rokt=a.getInstance()._RoktManager,this.getDeviceId=function(){return a.getInstance().getDeviceId()},this.setDeviceId=function(b){return a.getInstance().setDeviceId(b)},this.isInitialized=function(){return a.getInstance().isInitialized()},this.startNewSession=function(){a.getInstance().startNewSession();},this.endSession=function(){a.getInstance().endSession();},this.setLogLevel=function(b){a.getInstance().setLogLevel(b);},this.ready=function(b){a.getInstance().ready(b);},this.setAppVersion=function(b){a.getInstance().setAppVersion(b);},this.getAppName=function(){return a.getInstance().getAppName()},this.setAppName=function(b){a.getInstance().setAppName(b);},this.getAppVersion=function(){return a.getInstance().getAppVersion()},this.getEnvironment=function(){return a.getInstance().getEnvironment()},this.stopTrackingLocation=function(){a.getInstance().stopTrackingLocation();},this.startTrackingLocation=function(b){a.getInstance().startTrackingLocation(b);},this.setPosition=function(b,c){a.getInstance().setPosition(b,c);},this.logBaseEvent=function(b,c){a.getInstance().logBaseEvent(b,c);},this.logEvent=function(b,c,d,e,f){a.getInstance().logEvent(b,c,d,e,f);},this.logError=function(b,c){a.getInstance().logError(b,c);},this.logLink=function(b,c,d,e){a.getInstance().logLink(b,c,d,e);},this.logForm=function(b,c,d,e){a.getInstance().logForm(b,c,d,e);},this.logPageView=function(b,c,d,e){a.getInstance().logPageView(b,c,d,e);},this.upload=function(){a.getInstance().upload();},this.eCommerce={Cart:{add:function add(b,c){a.getInstance().eCommerce.Cart.add(b,c);},remove:function remove(b,c){a.getInstance().eCommerce.Cart.remove(b,c);},clear:function clear(){a.getInstance().eCommerce.Cart.clear();}},setCurrencyCode:function setCurrencyCode(b){a.getInstance().eCommerce.setCurrencyCode(b);},createProduct:function createProduct(b,c,d,e,f,g,h,i,j,k){return a.getInstance().eCommerce.createProduct(b,c,d,e,f,g,h,i,j,k)},createPromotion:function createPromotion(b,c,d,e){return a.getInstance().eCommerce.createPromotion(b,c,d,e)},createImpression:function createImpression(b,c){return a.getInstance().eCommerce.createImpression(b,c)},createTransactionAttributes:function createTransactionAttributes(b,c,d,e,f,g){return a.getInstance().eCommerce.createTransactionAttributes(b,c,d,e,f,g)},logCheckout:function logCheckout(b,c,d,e){a.getInstance().eCommerce.logCheckout(b,c,d,e);},logProductAction:function logProductAction(b,c,d,e,f,g){a.getInstance().eCommerce.logProductAction(b,c,d,e,f,g);},logPurchase:function logPurchase(b,c,d,e,f){a.getInstance().eCommerce.logPurchase(b,c,d,e,f);},logPromotion:function logPromotion(b,c,d,e,f){a.getInstance().eCommerce.logPromotion(b,c,d,e,f);},logImpression:function logImpression(b,c,d,e){a.getInstance().eCommerce.logImpression(b,c,d,e);},logRefund:function logRefund(b,c,d,e,f){a.getInstance().eCommerce.logRefund(b,c,d,e,f);},expandCommerceEvent:function expandCommerceEvent(b){return a.getInstance().eCommerce.expandCommerceEvent(b)}},this.setSessionAttribute=function(b,c){a.getInstance().setSessionAttribute(b,c);},this.setOptOut=function(b){a.getInstance().setOptOut(b);},this.setIntegrationAttribute=function(b,c){a.getInstance().setIntegrationAttribute(b,c);},this.getIntegrationAttributes=function(b){return a.getInstance().getIntegrationAttributes(b)},this.Identity={HTTPCodes:Constants.HTTPCodes,aliasUsers:function aliasUsers(b,c){a.getInstance().Identity.aliasUsers(b,c);},createAliasRequest:function createAliasRequest(b,c){return a.getInstance().Identity.createAliasRequest(b,c)},getCurrentUser:function getCurrentUser(){return a.getInstance().Identity.getCurrentUser()},getUser:function getUser(b){return a.getInstance().Identity.getUser(b)},getUsers:function getUsers(){return a.getInstance().Identity.getUsers()},identify:function identify(b,c){a.getInstance().Identity.identify(b,c);},login:function login(b,c){a.getInstance().Identity.login(b,c);},logout:function logout(b,c){a.getInstance().Identity.logout(b,c);},modify:function modify(b,c){a.getInstance().Identity.modify(b,c);}},this.sessionManager={getSession:function getSession(){return a.getInstance()._SessionManager.getSession()}},this.Consent={createConsentState:function createConsentState(){return a.getInstance().Consent.createConsentState()},createGDPRConsent:function createGDPRConsent(b,c,d,e,f){return a.getInstance().Consent.createGDPRConsent(b,c,d,e,f)},createCCPAConsent:function createCCPAConsent(b,c,d,e,f){return a.getInstance().Consent.createGDPRConsent(b,c,d,e,f)}},this.reset=function(){a.getInstance().reset(a.getInstance());},this._resetForTests=function(b,c){"boolean"==typeof c?a.getInstance()._resetForTests(b,c,a.getInstance()):a.getInstance()._resetForTests(b,!1,a.getInstance());},this.configurePixel=function(b){a.getInstance().configurePixel(b);},this._setIntegrationDelay=function(b,c){a.getInstance()._setIntegrationDelay(b,c);},this._getIntegrationDelays=function(){return a.getInstance()._getIntegrationDelays()},this.getVersion=function(){return a.getInstance().getVersion()},this.generateHash=function(b){return a.getInstance().generateHash(b)},this.addForwarder=function(b){a.getInstance().addForwarder(b);},this._getActiveForwarders=function(){return a.getInstance()._getActiveForwarders()},this._setWrapperSDKInfo=function(b,c){a.getInstance()._setWrapperSDKInfo(b,c);};}var mParticleManager=new mParticleInstanceManager;"undefined"!=typeof window&&(window.mParticle=mParticleManager,window.mParticle._BatchValidator=new _BatchValidator); module.exports = mParticleManager; diff --git a/dist/mparticle.esm.js b/dist/mparticle.esm.js index 5262679c0..757b3a97c 100644 --- a/dist/mparticle.esm.js +++ b/dist/mparticle.esm.js @@ -15,7 +15,7 @@ map:function map(a,b){var c,d,e;if(null===this)throw new TypeError(" this is nul filter:function filter(a/*, thisArg*/){if(void 0===this||null===this)throw new TypeError;var b=Object(this),c=b.length>>>0;if("function"!=typeof a)throw new TypeError;for(var d=[],e=2<=arguments.length?arguments[1]:void 0,f=0;ff);o++)n=h[o],b[n]?(a.Logger.verb if(d[g].mpid){var h=d[g].ui;for(var i in h)if(e===i&&b.userIdentities[e]===h[i]){c=g;break}}c&&f.storeDataInMemory(d,c);},this.encodePersistence=function(b){for(var c in b=JSON.parse(b),b.gs)b.gs.hasOwnProperty(c)&&(Base64CookieKeys[c]?b.gs[c]?Array.isArray(b.gs[c])&&b.gs[c].length||a._Helpers.isObject(b.gs[c])&&Object.keys(b.gs[c]).length?b.gs[c]=Base64.encode(JSON.stringify(b.gs[c])):delete b.gs[c]:delete b.gs[c]:"ie"===c?b.gs[c]=b.gs[c]?1:0:!b.gs[c]&&delete b.gs[c]);for(var d in b)if(b.hasOwnProperty(d)&&!SDKv2NonMPIDCookieKeys[d])for(c in b[d])b[d].hasOwnProperty(c)&&Base64CookieKeys[c]&&(a._Helpers.isObject(b[d][c])&&Object.keys(b[d][c]).length?b[d][c]=Base64.encode(JSON.stringify(b[d][c])):delete b[d][c]);return createCookieString(JSON.stringify(b))},this.decodePersistence=function(b){try{if(b){if(b=JSON.parse(revertCookieString(b)),a._Helpers.isObject(b)&&Object.keys(b).length){for(var c in b.gs)b.gs.hasOwnProperty(c)&&(Base64CookieKeys[c]?b.gs[c]=JSON.parse(Base64.decode(b.gs[c])):"ie"===c&&(b.gs[c]=!!b.gs[c]));for(var d in b)if(b.hasOwnProperty(d))if(!SDKv2NonMPIDCookieKeys[d])for(c in b[d])b[d].hasOwnProperty(c)&&Base64CookieKeys[c]&&b[d][c].length&&(b[d][c]=JSON.parse(Base64.decode(b[d][c])));else "l"===d&&(b[d]=!!b[d]);}return JSON.stringify(b)}}catch(b){a.Logger.error("Problem with decoding cookie",b);}},this.getCookieDomain=function(){if(a._Store.SDKConfig.cookieDomain)return a._Store.SDKConfig.cookieDomain;var b=f.getDomain(document,location.hostname);return ""===b?"":"."+b},this.getDomain=function(a,b){var c,d,e=b.split(".");for(c=e.length-1;0<=c;c--)if(d=e.slice(c).join("."),a.cookie="mptest=cookie;domain=."+d+";",-1All of the following methods can be called on the primary mParticle class. In version 2.10.0, we introduced multiple instances. If you are using multiple instances (self hosted environments only), you should call these methods on each instance.

@@ -1654,6 +1655,6 @@ Array.prototype.forEach||(Array.prototype.forEach=Polyfill.forEach),Array.protot * @param {String} apiKey your mParticle assigned API key * @param {Object} [config] an options object for additional configuration * @param {String} [instanceName] If you are self hosting the JS SDK and working with multiple instances, you would pass an instanceName to `init`. This instance will be selected when invoking other methods. See the above link to the doc site for more info and examples. - */this.Store={},this._instances={},this.IdentityType=IdentityType,this.EventType=EventType,this.CommerceEventType=CommerceEventType,this.PromotionType=PromotionActionType,this.ProductActionType=ProductActionType,this.MPSideloadedKit=MPSideloadedKit,"undefined"!=typeof window&&(this.isIOS=!!(window.mParticle&&window.mParticle.isIOS)&&window.mParticle.isIOS,this.config=window.mParticle&&window.mParticle.config?window.mParticle.config:{}),this.init=function(b,c,d){!c&&window.mParticle&&window.mParticle.config&&(console.warn("You did not pass a config object to mParticle.init(). Attempting to use the window.mParticle.config if it exists. Please note that in a future release, this may not work and mParticle will not initialize properly"),c=window.mParticle?window.mParticle.config:{}),d=(d&&0!==d.length?d:Constants.DefaultInstance).toLowerCase();var e=a._instances[d];e===void 0&&(e=new mParticleInstance(d),a._instances[d]=e),e.captureTiming(PerformanceMarkType.SdkStart),e.setLauncherInstanceGuid(),e.init(b,c,d);},this.captureTiming=function(b){a.getInstance().captureTiming(b);},this.getInstance=function(b){var c;return b?(c=a._instances[b.toLowerCase()],c?c:(console.log("You tried to initialize an instance named "+b+". This instance does not exist. Check your instance name or initialize a new instance with this name before calling it."),null)):(b=Constants.DefaultInstance,c=a._instances[b],c||(c=new mParticleInstance(b),a._instances[Constants.DefaultInstance]=c),c)},this.Rokt=a.getInstance()._RoktManager,this.getDeviceId=function(){return a.getInstance().getDeviceId()},this.setDeviceId=function(b){return a.getInstance().setDeviceId(b)},this.isInitialized=function(){return a.getInstance().isInitialized()},this.startNewSession=function(){a.getInstance().startNewSession();},this.endSession=function(){a.getInstance().endSession();},this.setLogLevel=function(b){a.getInstance().setLogLevel(b);},this.ready=function(b){a.getInstance().ready(b);},this.setAppVersion=function(b){a.getInstance().setAppVersion(b);},this.getAppName=function(){return a.getInstance().getAppName()},this.setAppName=function(b){a.getInstance().setAppName(b);},this.getAppVersion=function(){return a.getInstance().getAppVersion()},this.getEnvironment=function(){return a.getInstance().getEnvironment()},this.stopTrackingLocation=function(){a.getInstance().stopTrackingLocation();},this.startTrackingLocation=function(b){a.getInstance().startTrackingLocation(b);},this.setPosition=function(b,c){a.getInstance().setPosition(b,c);},this.startNewSession=function(){a.getInstance().startNewSession();},this.endSession=function(){a.getInstance().endSession();},this.logBaseEvent=function(b,c){a.getInstance().logBaseEvent(b,c);},this.logEvent=function(b,c,d,e,f){a.getInstance().logEvent(b,c,d,e,f);},this.logError=function(b,c){a.getInstance().logError(b,c);},this.logLink=function(b,c,d,e){a.getInstance().logLink(b,c,d,e);},this.logForm=function(b,c,d,e){a.getInstance().logForm(b,c,d,e);},this.logPageView=function(b,c,d,e){a.getInstance().logPageView(b,c,d,e);},this.upload=function(){a.getInstance().upload();},this.eCommerce={Cart:{add:function add(b,c){a.getInstance().eCommerce.Cart.add(b,c);},remove:function remove(b,c){a.getInstance().eCommerce.Cart.remove(b,c);},clear:function clear(){a.getInstance().eCommerce.Cart.clear();}},setCurrencyCode:function setCurrencyCode(b){a.getInstance().eCommerce.setCurrencyCode(b);},createProduct:function createProduct(b,c,d,e,f,g,h,i,j,k){return a.getInstance().eCommerce.createProduct(b,c,d,e,f,g,h,i,j,k)},createPromotion:function createPromotion(b,c,d,e){return a.getInstance().eCommerce.createPromotion(b,c,d,e)},createImpression:function createImpression(b,c){return a.getInstance().eCommerce.createImpression(b,c)},createTransactionAttributes:function createTransactionAttributes(b,c,d,e,f,g){return a.getInstance().eCommerce.createTransactionAttributes(b,c,d,e,f,g)},logCheckout:function logCheckout(b,c,d,e){a.getInstance().eCommerce.logCheckout(b,c,d,e);},logProductAction:function logProductAction(b,c,d,e,f,g){a.getInstance().eCommerce.logProductAction(b,c,d,e,f,g);},logPurchase:function logPurchase(b,c,d,e,f){a.getInstance().eCommerce.logPurchase(b,c,d,e,f);},logPromotion:function logPromotion(b,c,d,e,f){a.getInstance().eCommerce.logPromotion(b,c,d,e,f);},logImpression:function logImpression(b,c,d,e){a.getInstance().eCommerce.logImpression(b,c,d,e);},logRefund:function logRefund(b,c,d,e,f){a.getInstance().eCommerce.logRefund(b,c,d,e,f);},expandCommerceEvent:function expandCommerceEvent(b){return a.getInstance().eCommerce.expandCommerceEvent(b)}},this.setSessionAttribute=function(b,c){a.getInstance().setSessionAttribute(b,c);},this.setOptOut=function(b){a.getInstance().setOptOut(b);},this.setIntegrationAttribute=function(b,c){a.getInstance().setIntegrationAttribute(b,c);},this.getIntegrationAttributes=function(b){return a.getInstance().getIntegrationAttributes(b)},this.Identity={HTTPCodes:Constants.HTTPCodes,aliasUsers:function aliasUsers(b,c){a.getInstance().Identity.aliasUsers(b,c);},createAliasRequest:function createAliasRequest(b,c){return a.getInstance().Identity.createAliasRequest(b,c)},getCurrentUser:function getCurrentUser(){return a.getInstance().Identity.getCurrentUser()},getUser:function getUser(b){return a.getInstance().Identity.getUser(b)},getUsers:function getUsers(){return a.getInstance().Identity.getUsers()},identify:function identify(b,c){a.getInstance().Identity.identify(b,c);},login:function login(b,c){a.getInstance().Identity.login(b,c);},logout:function logout(b,c){a.getInstance().Identity.logout(b,c);},modify:function modify(b,c){a.getInstance().Identity.modify(b,c);}},this.sessionManager={getSession:function getSession(){return a.getInstance()._SessionManager.getSession()}},this.Consent={createConsentState:function createConsentState(){return a.getInstance().Consent.createConsentState()},createGDPRConsent:function createGDPRConsent(b,c,d,e,f){return a.getInstance().Consent.createGDPRConsent(b,c,d,e,f)},createCCPAConsent:function createCCPAConsent(b,c,d,e,f){return a.getInstance().Consent.createGDPRConsent(b,c,d,e,f)}},this.reset=function(){a.getInstance().reset(a.getInstance());},this._resetForTests=function(b,c){"boolean"==typeof c?a.getInstance()._resetForTests(b,c,a.getInstance()):a.getInstance()._resetForTests(b,!1,a.getInstance());},this.configurePixel=function(b){a.getInstance().configurePixel(b);},this._setIntegrationDelay=function(b,c){a.getInstance()._setIntegrationDelay(b,c);},this._getIntegrationDelays=function(){return a.getInstance()._getIntegrationDelays()},this.getVersion=function(){return a.getInstance().getVersion()},this.generateHash=function(b){return a.getInstance().generateHash(b)},this.addForwarder=function(b){a.getInstance().addForwarder(b);},this._getActiveForwarders=function(){return a.getInstance()._getActiveForwarders()},this._setWrapperSDKInfo=function(b,c){a.getInstance()._setWrapperSDKInfo(b,c);};}var mParticleManager=new mParticleInstanceManager;"undefined"!=typeof window&&(window.mParticle=mParticleManager,window.mParticle._BatchValidator=new _BatchValidator); + */this.Store={},this._instances={},this.IdentityType=IdentityType,this.EventType=EventType,this.CommerceEventType=CommerceEventType,this.PromotionType=PromotionActionType,this.ProductActionType=ProductActionType,this.MPSideloadedKit=MPSideloadedKit,"undefined"!=typeof window&&(this.isIOS=!!(window.mParticle&&window.mParticle.isIOS)&&window.mParticle.isIOS,this.config=window.mParticle&&window.mParticle.config?window.mParticle.config:{}),this.init=function(b,c,d){!c&&window.mParticle&&window.mParticle.config&&(console.warn("You did not pass a config object to mParticle.init(). Attempting to use the window.mParticle.config if it exists. Please note that in a future release, this may not work and mParticle will not initialize properly"),c=window.mParticle?window.mParticle.config:{}),d=(d&&0!==d.length?d:Constants.DefaultInstance).toLowerCase();var e=a._instances[d];e===void 0&&(e=new mParticleInstance(d),a._instances[d]=e),e.captureTiming(PerformanceMarkType.SdkStart),e.setLauncherInstanceGuid(),e.init(b,c,d);},this.captureTiming=function(b){a.getInstance().captureTiming(b);},this.getInstance=function(b){var c;return b?(c=a._instances[b.toLowerCase()],c?c:(console.log("You tried to initialize an instance named "+b+". This instance does not exist. Check your instance name or initialize a new instance with this name before calling it."),null)):(b=Constants.DefaultInstance,c=a._instances[b],c||(c=new mParticleInstance(b),a._instances[Constants.DefaultInstance]=c),c)},this.Rokt=a.getInstance()._RoktManager,this.getDeviceId=function(){return a.getInstance().getDeviceId()},this.setDeviceId=function(b){return a.getInstance().setDeviceId(b)},this.isInitialized=function(){return a.getInstance().isInitialized()},this.startNewSession=function(){a.getInstance().startNewSession();},this.endSession=function(){a.getInstance().endSession();},this.setLogLevel=function(b){a.getInstance().setLogLevel(b);},this.ready=function(b){a.getInstance().ready(b);},this.setAppVersion=function(b){a.getInstance().setAppVersion(b);},this.getAppName=function(){return a.getInstance().getAppName()},this.setAppName=function(b){a.getInstance().setAppName(b);},this.getAppVersion=function(){return a.getInstance().getAppVersion()},this.getEnvironment=function(){return a.getInstance().getEnvironment()},this.stopTrackingLocation=function(){a.getInstance().stopTrackingLocation();},this.startTrackingLocation=function(b){a.getInstance().startTrackingLocation(b);},this.setPosition=function(b,c){a.getInstance().setPosition(b,c);},this.logBaseEvent=function(b,c){a.getInstance().logBaseEvent(b,c);},this.logEvent=function(b,c,d,e,f){a.getInstance().logEvent(b,c,d,e,f);},this.logError=function(b,c){a.getInstance().logError(b,c);},this.logLink=function(b,c,d,e){a.getInstance().logLink(b,c,d,e);},this.logForm=function(b,c,d,e){a.getInstance().logForm(b,c,d,e);},this.logPageView=function(b,c,d,e){a.getInstance().logPageView(b,c,d,e);},this.upload=function(){a.getInstance().upload();},this.eCommerce={Cart:{add:function add(b,c){a.getInstance().eCommerce.Cart.add(b,c);},remove:function remove(b,c){a.getInstance().eCommerce.Cart.remove(b,c);},clear:function clear(){a.getInstance().eCommerce.Cart.clear();}},setCurrencyCode:function setCurrencyCode(b){a.getInstance().eCommerce.setCurrencyCode(b);},createProduct:function createProduct(b,c,d,e,f,g,h,i,j,k){return a.getInstance().eCommerce.createProduct(b,c,d,e,f,g,h,i,j,k)},createPromotion:function createPromotion(b,c,d,e){return a.getInstance().eCommerce.createPromotion(b,c,d,e)},createImpression:function createImpression(b,c){return a.getInstance().eCommerce.createImpression(b,c)},createTransactionAttributes:function createTransactionAttributes(b,c,d,e,f,g){return a.getInstance().eCommerce.createTransactionAttributes(b,c,d,e,f,g)},logCheckout:function logCheckout(b,c,d,e){a.getInstance().eCommerce.logCheckout(b,c,d,e);},logProductAction:function logProductAction(b,c,d,e,f,g){a.getInstance().eCommerce.logProductAction(b,c,d,e,f,g);},logPurchase:function logPurchase(b,c,d,e,f){a.getInstance().eCommerce.logPurchase(b,c,d,e,f);},logPromotion:function logPromotion(b,c,d,e,f){a.getInstance().eCommerce.logPromotion(b,c,d,e,f);},logImpression:function logImpression(b,c,d,e){a.getInstance().eCommerce.logImpression(b,c,d,e);},logRefund:function logRefund(b,c,d,e,f){a.getInstance().eCommerce.logRefund(b,c,d,e,f);},expandCommerceEvent:function expandCommerceEvent(b){return a.getInstance().eCommerce.expandCommerceEvent(b)}},this.setSessionAttribute=function(b,c){a.getInstance().setSessionAttribute(b,c);},this.setOptOut=function(b){a.getInstance().setOptOut(b);},this.setIntegrationAttribute=function(b,c){a.getInstance().setIntegrationAttribute(b,c);},this.getIntegrationAttributes=function(b){return a.getInstance().getIntegrationAttributes(b)},this.Identity={HTTPCodes:Constants.HTTPCodes,aliasUsers:function aliasUsers(b,c){a.getInstance().Identity.aliasUsers(b,c);},createAliasRequest:function createAliasRequest(b,c){return a.getInstance().Identity.createAliasRequest(b,c)},getCurrentUser:function getCurrentUser(){return a.getInstance().Identity.getCurrentUser()},getUser:function getUser(b){return a.getInstance().Identity.getUser(b)},getUsers:function getUsers(){return a.getInstance().Identity.getUsers()},identify:function identify(b,c){a.getInstance().Identity.identify(b,c);},login:function login(b,c){a.getInstance().Identity.login(b,c);},logout:function logout(b,c){a.getInstance().Identity.logout(b,c);},modify:function modify(b,c){a.getInstance().Identity.modify(b,c);}},this.sessionManager={getSession:function getSession(){return a.getInstance()._SessionManager.getSession()}},this.Consent={createConsentState:function createConsentState(){return a.getInstance().Consent.createConsentState()},createGDPRConsent:function createGDPRConsent(b,c,d,e,f){return a.getInstance().Consent.createGDPRConsent(b,c,d,e,f)},createCCPAConsent:function createCCPAConsent(b,c,d,e,f){return a.getInstance().Consent.createGDPRConsent(b,c,d,e,f)}},this.reset=function(){a.getInstance().reset(a.getInstance());},this._resetForTests=function(b,c){"boolean"==typeof c?a.getInstance()._resetForTests(b,c,a.getInstance()):a.getInstance()._resetForTests(b,!1,a.getInstance());},this.configurePixel=function(b){a.getInstance().configurePixel(b);},this._setIntegrationDelay=function(b,c){a.getInstance()._setIntegrationDelay(b,c);},this._getIntegrationDelays=function(){return a.getInstance()._getIntegrationDelays()},this.getVersion=function(){return a.getInstance().getVersion()},this.generateHash=function(b){return a.getInstance().generateHash(b)},this.addForwarder=function(b){a.getInstance().addForwarder(b);},this._getActiveForwarders=function(){return a.getInstance()._getActiveForwarders()},this._setWrapperSDKInfo=function(b,c){a.getInstance()._setWrapperSDKInfo(b,c);};}var mParticleManager=new mParticleInstanceManager;"undefined"!=typeof window&&(window.mParticle=mParticleManager,window.mParticle._BatchValidator=new _BatchValidator); export { mParticleManager as default }; diff --git a/dist/mparticle.js b/dist/mparticle.js index c7a56f9fa..0a29dcbf3 100644 --- a/dist/mparticle.js +++ b/dist/mparticle.js @@ -203,7 +203,7 @@ var mParticle = (function () { Base64: Base64$1 }; - var version = "2.48.0"; + var version = "2.50.0"; var Constants = { sdkVersion: version, @@ -5612,11 +5612,7 @@ var mParticle = (function () { self.update(); }; this.resetPersistence = function () { - removeLocalStorage(StorageNames.localStorageName); - removeLocalStorage(StorageNames.localStorageNameV3); - removeLocalStorage(StorageNames.localStorageNameV4); - removeLocalStorage(mpInstance._Store.storageName); - removeLocalStorage(StorageNames.localStorageProductsV4); + localStorage.clear(); self.expireCookies(StorageNames.cookieName); self.expireCookies(StorageNames.cookieNameV2); self.expireCookies(StorageNames.cookieNameV3); @@ -9713,10 +9709,14 @@ var mParticle = (function () { * @throws Logs error to console if placementAttributesMapping parsing fails */ RoktManager.prototype.init = function (roktConfig, filteredUser, identityService, store, logger, options) { - var _a = roktConfig || {}, - userAttributeFilters = _a.userAttributeFilters, - settings = _a.settings; - var placementAttributesMapping = (settings || {}).placementAttributesMapping; + var _a; + var _b = roktConfig || {}, + userAttributeFilters = _b.userAttributeFilters, + settings = _b.settings; + var _c = settings || {}, + placementAttributesMapping = _c.placementAttributesMapping, + hashedEmailUserIdentityType = _c.hashedEmailUserIdentityType; + this.mappedEmailShaIdentityType = (_a = hashedEmailUserIdentityType === null || hashedEmailUserIdentityType === void 0 ? void 0 : hashedEmailUserIdentityType.toLowerCase()) !== null && _a !== void 0 ? _a : null; this.identityService = identityService; this.store = store; this.logger = logger; @@ -9766,7 +9766,7 @@ var mParticle = (function () { RoktManager.prototype.selectPlacements = function (options) { var _a, _b; return __awaiter(this, void 0, void 0, function () { - var attributes, sandboxValue, mappedAttributes, currentUserIdentities_1, currentEmail, newEmail_1, error_1, enrichedAttributes, enrichedOptions, error_2; + var attributes, sandboxValue, mappedAttributes, currentUserIdentities_1, currentEmail, newEmail, currentHashedEmail, newHashedEmail, emailChanged, hashedEmailChanged, newIdentities_1, error_1, enrichedAttributes, enrichedOptions, error_2; var _this = this; return __generator(this, function (_c) { switch (_c.label) { @@ -9784,19 +9784,34 @@ var mParticle = (function () { this.currentUser = this.identityService.getCurrentUser(); currentUserIdentities_1 = ((_b = (_a = this.currentUser) === null || _a === void 0 ? void 0 : _a.getUserIdentities()) === null || _b === void 0 ? void 0 : _b.userIdentities) || {}; currentEmail = currentUserIdentities_1.email; - newEmail_1 = mappedAttributes.email; - if (!(newEmail_1 && (!currentEmail || currentEmail !== newEmail_1))) return [3 /*break*/, 5]; - if (currentEmail && currentEmail !== newEmail_1) { - this.logger.warning("Email mismatch detected. Current email, ".concat(currentEmail, " differs from email passed to selectPlacements call, ").concat(newEmail_1, ". Proceeding to call identify with ").concat(newEmail_1, ". Please verify your implementation.")); + newEmail = mappedAttributes.email; + currentHashedEmail = void 0; + newHashedEmail = void 0; + // Hashed email identity is valid if it is set to Other-Other10 + if (this.mappedEmailShaIdentityType && IdentityType.getIdentityType(this.mappedEmailShaIdentityType) !== false) { + currentHashedEmail = currentUserIdentities_1[this.mappedEmailShaIdentityType]; + newHashedEmail = mappedAttributes['emailsha256'] || mappedAttributes[this.mappedEmailShaIdentityType] || undefined; + } + emailChanged = this.hasIdentityChanged(currentEmail, newEmail); + hashedEmailChanged = this.hasIdentityChanged(currentHashedEmail, newHashedEmail); + newIdentities_1 = {}; + if (emailChanged) { + newIdentities_1.email = newEmail; + if (newEmail) { + this.logger.warning("Email mismatch detected. Current email differs from email passed to selectPlacements call. Proceeding to call identify with email from selectPlacements call. Please verify your implementation."); + } } + if (hashedEmailChanged) { + newIdentities_1[this.mappedEmailShaIdentityType] = newHashedEmail; + this.logger.warning("emailsha256 mismatch detected. Current mParticle hashedEmail differs from hashedEmail passed to selectPlacements call. Proceeding to call identify with hashedEmail from selectPlacements call. Please verify your implementation."); + } + if (!!isEmpty(newIdentities_1)) return [3 /*break*/, 5]; _c.label = 2; case 2: _c.trys.push([2, 4,, 5]); return [4 /*yield*/, new Promise(function (resolve, reject) { _this.identityService.identify({ - userIdentities: __assign(__assign({}, currentUserIdentities_1), { - email: newEmail_1 - }) + userIdentities: __assign(__assign({}, currentUserIdentities_1), newIdentities_1) }, function () { resolve(); }); @@ -9954,6 +9969,28 @@ var mParticle = (function () { } this.messageQueue["delete"](messageId); }; + /** + * Checks if an identity value has changed by comparing current and new values + * + * @param {string | undefined} currentValue - The current identity value + * @param {string | undefined} newValue - The new identity value to compare against + * @returns {boolean} True if the identity has changed (new value exists and differs from current), false otherwise + */ + RoktManager.prototype.hasIdentityChanged = function (currentValue, newValue) { + if (!newValue) { + return false; + } + if (!currentValue) { + return true; // New value exists but no current value + } + + if (currentValue !== newValue) { + return true; // Values are different + } + + return false; // Values are the same + }; + return RoktManager; }(); @@ -11400,12 +11437,6 @@ var mParticle = (function () { this.setPosition = function (lat, lng) { self.getInstance().setPosition(lat, lng); }; - this.startNewSession = function () { - self.getInstance().startNewSession(); - }; - this.endSession = function () { - self.getInstance().endSession(); - }; this.logBaseEvent = function (event, eventOptions) { self.getInstance().logBaseEvent(event, eventOptions); }; diff --git a/package-lock.json b/package-lock.json index 745ade2a3..44ed71906 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@mparticle/web-sdk", - "version": "2.48.0", + "version": "2.50.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@mparticle/web-sdk", - "version": "2.48.0", + "version": "2.50.0", "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.23.2" diff --git a/package.json b/package.json index 3aea987fc..174d62dc1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mparticle/web-sdk", - "version": "2.48.0", + "version": "2.50.0", "description": "mParticle core SDK for web applications", "license": "Apache-2.0", "keywords": [ @@ -43,6 +43,8 @@ "test": "npm run build && npm run build:test-bundle && cross-env DEBUG=false karma start test/karma.config.js", "test:browserstack": "karma start test/cross-browser-testing/browserstack.karma.config.js", "test:browserstack:debug": "cross-env DEBUG=true karma start test/cross-browser-testing/browserstack.karma.config.js", + "test:browserstack-beta": "karma start test/cross-browser-testing/browserstack.karma.beta.config.js", + "test:browserstack-beta:debug": "cross-env DEBUG=true karma start test/cross-browser-testing/browserstack.karma.beta.config.js", "test:debug": "cross-env DEBUG=true karma start test/karma.config.js", "test:stub": "cross-env TESTTYPE=stub ENVIRONMENT=prod rollup --config rollup.test.config.js && karma start test/stub/karma.stub.config.js", "test:integrations": "npm run test:requirejs && npm run test:integrations:cjs && npm run test:integrations:module", diff --git a/src/persistence.js b/src/persistence.js index 256595434..fb0aec710 100644 --- a/src/persistence.js +++ b/src/persistence.js @@ -949,11 +949,7 @@ export default function _Persistence(mpInstance) { }; this.resetPersistence = function() { - removeLocalStorage(StorageNames.localStorageName); - removeLocalStorage(StorageNames.localStorageNameV3); - removeLocalStorage(StorageNames.localStorageNameV4); - removeLocalStorage(mpInstance._Store.storageName); - removeLocalStorage(StorageNames.localStorageProductsV4); + localStorage.clear(); self.expireCookies(StorageNames.cookieName); self.expireCookies(StorageNames.cookieNameV2); diff --git a/src/roktManager.ts b/src/roktManager.ts index bdb8089e4..5b17381c4 100644 --- a/src/roktManager.ts +++ b/src/roktManager.ts @@ -8,10 +8,13 @@ import { generateUniqueId, isFunction, AttributeValue, + isEmpty, } from "./utils"; import { SDKIdentityApi } from "./identity.interfaces"; import { SDKLoggerApi } from "./sdkRuntimeModels"; import { IStore, LocalSessionAttributes } from "./store"; +import { UserIdentities } from "@mparticle/web-sdk"; +import { IdentityType } from "./types"; // https://docs.rokt.com/developers/integration-guides/web/library/attributes export interface IRoktPartnerAttributes { @@ -96,6 +99,7 @@ export default class RoktManager { private launcherOptions?: IRoktLauncherOptions; private logger: SDKLoggerApi; private domain?: string; + private mappedEmailShaIdentityType?: string | null; /** * Initializes the RoktManager with configuration settings and user data. * @@ -116,7 +120,8 @@ export default class RoktManager { options?: IRoktOptions ): void { const { userAttributeFilters, settings } = roktConfig || {}; - const { placementAttributesMapping } = settings || {}; + const { placementAttributesMapping, hashedEmailUserIdentityType } = settings || {}; + this.mappedEmailShaIdentityType = hashedEmailUserIdentityType?.toLowerCase() ?? null; this.identityService = identityService; this.store = store; @@ -182,23 +187,43 @@ export default class RoktManager { // Get current user identities this.currentUser = this.identityService.getCurrentUser(); const currentUserIdentities = this.currentUser?.getUserIdentities()?.userIdentities || {}; + const currentEmail = currentUserIdentities.email; const newEmail = mappedAttributes.email as string; - // https://go.mparticle.com/work/SQDSDKS-7338 - // Check if email exists and differs - if (newEmail && (!currentEmail || currentEmail !== newEmail)) { - if (currentEmail && currentEmail !== newEmail) { - this.logger.warning(`Email mismatch detected. Current email, ${currentEmail} differs from email passed to selectPlacements call, ${newEmail}. Proceeding to call identify with ${newEmail}. Please verify your implementation.`); + let currentHashedEmail: string | undefined; + let newHashedEmail: string | undefined; + + // Hashed email identity is valid if it is set to Other-Other10 + if(this.mappedEmailShaIdentityType && IdentityType.getIdentityType(this.mappedEmailShaIdentityType) !== false) { + currentHashedEmail = currentUserIdentities[this.mappedEmailShaIdentityType]; + newHashedEmail = mappedAttributes['emailsha256'] as string || mappedAttributes[this.mappedEmailShaIdentityType] as string || undefined; + } + + const emailChanged = this.hasIdentityChanged(currentEmail, newEmail); + const hashedEmailChanged = this.hasIdentityChanged(currentHashedEmail, newHashedEmail); + + const newIdentities: UserIdentities = {}; + if (emailChanged) { + newIdentities.email = newEmail; + if (newEmail) { + this.logger.warning(`Email mismatch detected. Current email differs from email passed to selectPlacements call. Proceeding to call identify with email from selectPlacements call. Please verify your implementation.`); } + } + if (hashedEmailChanged) { + newIdentities[this.mappedEmailShaIdentityType] = newHashedEmail; + this.logger.warning(`emailsha256 mismatch detected. Current mParticle hashedEmail differs from hashedEmail passed to selectPlacements call. Proceeding to call identify with hashedEmail from selectPlacements call. Please verify your implementation.`); + } + + if (!isEmpty(newIdentities)) { // Call identify with the new user identities try { await new Promise((resolve, reject) => { this.identityService.identify({ userIdentities: { ...currentUserIdentities, - email: newEmail + ...newIdentities } }, () => { resolve(); @@ -239,6 +264,28 @@ export default class RoktManager { } } + /** + * Hashes a single value using SHA-256 + * Accepts the same types as IRoktPartnerAttributes values + * + * @param {string | number | boolean | undefined | null} attribute - The value to hash + * @returns {Promise} The SHA-256 hex digest of the normalized value + * + */ + public async hashSha256(attribute: string | number | boolean | undefined | null): Promise { + try { + if (attribute === undefined || attribute === null) { + return Promise.reject(new Error('Value cannot be null or undefined')); + } + const normalizedValue = String(attribute).trim().toLocaleLowerCase(); + return await this.sha256Hex(normalizedValue); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + this.logger.error('Failed hashSha256: ' + errorMessage); + return Promise.reject(new Error(String(error))); + } + } + public setExtensionData(extensionData: IRoktPartnerExtensionData): void { if (!this.isReady()) { this.deferredCall('setExtensionData', extensionData); @@ -321,6 +368,7 @@ export default class RoktManager { this.messageQueue.forEach((message) => { if(!(message.methodName in this) || !isFunction(this[message.methodName])) { this.logger?.error(`RoktManager: Method ${message.methodName} not found`); + return; } @@ -373,4 +421,58 @@ export default class RoktManager { this.messageQueue.delete(messageId); } + + /** + * Hashes a string input using SHA-256 and returns the hex digest + * Uses the Web Crypto API for secure hashing + * + * @param {string} input - The string to hash + * @returns {Promise} The SHA-256 hash as a hexadecimal string + */ + private async sha256Hex(input: string): Promise { + const encoder = new TextEncoder(); + const encodedInput = encoder.encode(input); + const digest = await crypto.subtle.digest('SHA-256', encodedInput); + return this.arrayBufferToHex(digest); + } + + /** + * Converts an ArrayBuffer to a hexadecimal string representation + * Each byte is converted to a 2-character hex string with leading zeros + * + * @param {ArrayBuffer} buffer - The buffer to convert + * @returns {string} The hexadecimal string representation + */ + private arrayBufferToHex(buffer: ArrayBuffer): string { + const bytes = new Uint8Array(buffer); + let hexString = ''; + for (let i = 0; i < bytes.length; i++) { + const hexByte = bytes[i].toString(16).padStart(2, '0'); + hexString += hexByte; + } + return hexString; + } + + /** + * Checks if an identity value has changed by comparing current and new values + * + * @param {string | undefined} currentValue - The current identity value + * @param {string | undefined} newValue - The new identity value to compare against + * @returns {boolean} True if the identity has changed (new value exists and differs from current), false otherwise + */ + private hasIdentityChanged(currentValue: string | undefined, newValue: string | undefined): boolean { + if (!newValue) { + return false; + } + + if (!currentValue) { + return true; // New value exists but no current value + } + + if (currentValue !== newValue) { + return true; // Values are different + } + + return false; // Values are the same + } } diff --git a/test/cross-browser-testing/browserstack.karma.beta.config.js b/test/cross-browser-testing/browserstack.karma.beta.config.js new file mode 100644 index 000000000..9887d171e --- /dev/null +++ b/test/cross-browser-testing/browserstack.karma.beta.config.js @@ -0,0 +1,127 @@ +const { DEBUG } = process.env; + +const files = [ + '../lib/geomock.js', + '../../dist/mparticle.js', + '../test-bundle.js', +]; + +let captureConsole = false; +let browserConsoleLogOptions = {}; + +if (DEBUG === 'true') { + browserConsoleLogOptions = { + level: 'log', + format: '%b %T: %m', + terminal: true, + }; + captureConsole = true; +} else { + browserConsoleLogOptions = { + terminal: false, + }; +} + +const customLaunchers = { + bs_chrome_mac_tahoe_beta: { + base: 'BrowserStack', + browser: 'chrome', + browser_version: 'latest-beta', + os: 'OS X', + os_version: 'Tahoe' // macOS 26 + }, + bs_chrome_mac_sequoia_beta: { + base: 'BrowserStack', + browser: 'chrome', + browser_version: 'latest-beta', + os: 'OS X', + os_version: 'Sequoia' // macOS 15 + }, + bs_chrome_win_beta: { + base: 'BrowserStack', + browser: 'chrome', + browser_version: 'latest', + os: 'Windows', + os_version: '11' + }, + bs_firefox_mac_tahoe_beta: { + base: 'BrowserStack', + browser: 'firefox', + browser_version: 'latest-beta', + os: 'OS X', + os_version: 'Tahoe' // macOS 26 + }, + bs_firefox_mac_sequoia_beta: { + base: 'BrowserStack', + browser: 'firefox', + browser_version: 'latest-beta', + os: 'OS X', + os_version: 'Sequoia' // macOS 15 + }, + bs_firefox_mac_catalina_beta: { + base: 'BrowserStack', + browser: 'firefox', + browser_version: 'latest-beta', + os: 'OS X', + os_version: 'Catalina' // macOS 10.15 + }, + bs_firefox_win_latest: { + base: 'BrowserStack', + browser: 'firefox', + browser_version: 'latest', + os: 'Windows', + os_version: '11', + }, + bs_edge_mac_tahoe_beta: { + base: 'BrowserStack', + browser: 'edge', + browser_version: 'latest-beta', + os: 'OS X', + os_version: 'Tahoe' // macOS 26 + }, + bs_edge_mac_sequoia_beta: { + base: 'BrowserStack', + browser: 'edge', + browser_version: 'latest-beta', + os: 'OS X', + os_version: 'Sequoia' // macOS 15 + }, + bs_edge_win_beta: { + base: 'BrowserStack', + browser: 'edge', + browser_version: 'latest-beta', + os: 'Windows', + os_version: '11' + }, +}; + +module.exports = function(config) { + config.set({ + browserStack: { + username: process.env.BS_USERNAME, + accessKey: process.env.BS_ACCESS_KEY + }, + autoWatch: false, + customLaunchers, + browsers: Object.keys(customLaunchers), + frameworks: ['mocha', 'should'], + files, + reporters: ['progress', 'junit'], + colors: true, + singleRun: true, + debug: true, + logLevel: config.LOG_INFO, + browserConsoleLogOptions, + client: { + captureConsole, + }, + junitReporter: { + outputDir: 'reports/', + outputFile: 'test-karma-beta.xml', + }, + browserDisconnectTimeout: 50000, + browserDisconnectTolerance: 5, + concurrency: 5, + }); +}; + diff --git a/test/jest/roktManager.spec.ts b/test/jest/roktManager.spec.ts index 3c3bccae2..06818fe63 100644 --- a/test/jest/roktManager.spec.ts +++ b/test/jest/roktManager.spec.ts @@ -2,7 +2,7 @@ import { IKitConfigs } from "../../src/configAPIClient"; import { IMParticleUser } from "../../src/identity-user-interfaces"; import { SDKIdentityApi } from "../../src/identity.interfaces"; import { IMParticleWebSDKInstance } from "../../src/mp-instance"; -import RoktManager, { IRoktKit, IRoktSelectPlacementsOptions } from "../../src/roktManager"; +import RoktManager, { IRoktKit, IRoktSelectPlacementsOptions, IRoktPartnerAttributes, IRoktLauncher, IRoktSelection } from "../../src/roktManager"; import { testMPID } from '../src/config/constants'; const resolvePromise = () => new Promise(resolve => setTimeout(resolve, 0)); @@ -76,41 +76,68 @@ describe('RoktManager', () => { }); describe('#hashAttributes', () => { + interface Hasher { + sha256Hex(input: string): Promise + } + beforeEach(() => { roktManager['currentUser'] = currentUser; }); - it('should call kit.hashAttributes with empty attributes', () => { - const kit: IRoktKit = { - launcher: { - selectPlacements: jest.fn(), - hashAttributes: jest.fn(), - use: jest.fn(), - }, - filters: undefined, - filteredUser: undefined, - hashAttributes: jest.fn(), - selectPlacements: jest.fn(), - setExtensionData: jest.fn(), - use: jest.fn(), - userAttributes: undefined, - }; + it('should not hash when calling roktManager.hashAttributes with empty attributes and no kit', async () => { + const shaSpy = jest.spyOn(roktManager as unknown as Hasher, 'sha256Hex'); + const attributes: IRoktPartnerAttributes = {}; + const messageQueueSizeBefore = roktManager['messageQueue'].size; + await roktManager.hashAttributes(attributes); + const messageQueueSizeAfter = roktManager['messageQueue'].size; + expect(messageQueueSizeAfter).toBe(messageQueueSizeBefore); + expect(shaSpy).not.toHaveBeenCalled(); + shaSpy.mockRestore(); + }); - roktManager.attachKit(kit); + it('should hash when calling roktManager.hashAttributes without kit', async () => { + const nodeCrypto = require('crypto'); + const shaSpy = jest.spyOn(roktManager as unknown as Hasher, 'sha256Hex'); + shaSpy.mockImplementation((s: any) => + Promise.resolve(nodeCrypto.createHash('sha256').update(String(s)).digest('hex')), + ); + + const attributes: IRoktPartnerAttributes = { email: 'Jane.Doe@Gmail.com', phone: ' 1234567890 ' }; + const messageQueueSizeBefore = roktManager['messageQueue'].size; + + await roktManager.hashAttributes(attributes); + + const messageQueueSizeAfter = roktManager['messageQueue'].size; + expect(messageQueueSizeAfter).toBe(messageQueueSizeBefore); + + expect(shaSpy).toHaveBeenCalledWith('jane.doe@gmail.com'); + expect(shaSpy).toHaveBeenCalledWith('1234567890'); + expect(shaSpy).toHaveBeenCalledTimes(2); - const attributes = {}; + shaSpy.mockRestore(); + }); + + it('should not queue when calling roktManager.hashAttributes with no kit attached', async () => { + const shaSpy = jest.spyOn(roktManager as unknown as Hasher, 'sha256Hex').mockResolvedValue('deadbeef'); + + const attributes: IRoktPartnerAttributes = { email: 'test@example.com' }; + const messageQueueSizeBefore = roktManager['messageQueue'].size; + await roktManager.hashAttributes(attributes); + const messageQueueSizeAfter = roktManager['messageQueue'].size; + expect(messageQueueSizeAfter).toBe(messageQueueSizeBefore); - roktManager.hashAttributes(attributes); - expect(kit.hashAttributes).toHaveBeenCalledWith(attributes); + shaSpy.mockRestore(); }); - it('should call kit.hashAttributes with passed in attributes', () => { + it('should remain non-deferred before and after attaching kit', async () => { + const nodeCrypto = require('crypto'); + const shaSpy = jest.spyOn(roktManager as unknown as Hasher, 'sha256Hex'); + shaSpy.mockImplementation((s: any) => + Promise.resolve(nodeCrypto.createHash('sha256').update(String(s)).digest('hex')), + ); + const kit: IRoktKit = { - launcher: { - selectPlacements: jest.fn(), - hashAttributes: jest.fn(), - use: jest.fn(), - }, + launcher: { selectPlacements: jest.fn(), hashAttributes: jest.fn(), use: jest.fn() }, filters: undefined, filteredUser: undefined, hashAttributes: jest.fn(), @@ -120,93 +147,161 @@ describe('RoktManager', () => { userAttributes: undefined, }; + const attributes: IRoktPartnerAttributes = { email: 'test@example.com' }; + + const sizeBefore = roktManager['messageQueue'].size; + await roktManager.hashAttributes(attributes); + expect(roktManager['messageQueue'].size).toBe(sizeBefore); + roktManager.attachKit(kit); + const sizeAfterAttach = roktManager['messageQueue'].size; + await roktManager.hashAttributes(attributes); + expect(roktManager['messageQueue'].size).toBe(sizeAfterAttach); - const attributes = { - email: 'test@example.com', - phone: '1234567890' - }; + shaSpy.mockRestore(); + }); - roktManager.hashAttributes(attributes); - expect(kit.hashAttributes).toHaveBeenCalledWith(attributes); + it('should allow calling launcher.hashAttributes directly when kit is attached', async () => { + const launcher: IRoktLauncher = { + selectPlacements: jest.fn().mockResolvedValue({ close: jest.fn(), getPlacements: jest.fn().mockResolvedValue([]) } as unknown as IRoktSelection), + hashAttributes: jest.fn(), + use: jest.fn().mockResolvedValue(undefined as unknown as never), + }; + const kit: Partial = { + launcher, + hashAttributes: jest.fn(), + }; + roktManager.attachKit(kit as IRoktKit); + const attributes: IRoktPartnerAttributes = { email: 'test@example.com', phone: '1234567890' }; + await launcher.hashAttributes(attributes); + expect(launcher.hashAttributes).toHaveBeenCalledWith(attributes); }); - it('should queue the hashAttributes method if no launcher or kit is attached', () => { - const attributes = { - email: 'test@example.com' + it('should match node crypto output', async () => { + const nodeCrypto = require('crypto'); + const shaSpy = jest.spyOn(roktManager as unknown as Hasher, 'sha256Hex'); + shaSpy.mockImplementation((s: any) => + Promise.resolve(nodeCrypto.createHash('sha256').update(String(s)).digest('hex')), + ); + const attributes: IRoktPartnerAttributes = { + email: ' Jane.DOE@GMAIL.com ', + phone: ' 1234567890 ', + blank: ' ', + }; + const fromManager = await roktManager.hashAttributes(attributes); + const expected = { + emailsha256: nodeCrypto.createHash('sha256').update('jane.doe@gmail.com').digest('hex'), + phonesha256: nodeCrypto.createHash('sha256').update('1234567890').digest('hex'), + blanksha256: nodeCrypto.createHash('sha256').update('').digest('hex'), }; + expect(fromManager).toStrictEqual(expected); + shaSpy.mockRestore(); + }); + }); - roktManager.hashAttributes(attributes); + describe('#hashSha256', () => { + interface Hasher { + sha256Hex(input: string): Promise + } - expect(roktManager['kit']).toBeNull(); - expect(roktManager['messageQueue'].size).toBe(1); - const queuedMessage = Array.from(roktManager['messageQueue'].values())[0]; - expect(queuedMessage.methodName).toBe('hashAttributes'); - expect(queuedMessage.payload).toBe(attributes); + const nodeCrypto = require('crypto'); + let shaSpy: jest.SpyInstance; + + beforeEach(() => { + shaSpy = jest.spyOn(roktManager as unknown as Hasher, 'sha256Hex'); + shaSpy.mockImplementation((s: any) => + Promise.resolve(nodeCrypto.createHash('sha256').update(String(s)).digest('hex')), + ); }); - it('should process queued hashAttributes calls once the launcher and kit are attached', () => { - const kit: IRoktKit = { - launcher: { - selectPlacements: jest.fn(), - hashAttributes: jest.fn(), - use: jest.fn(), - }, - filters: undefined, - filteredUser: undefined, - hashAttributes: jest.fn(), - selectPlacements: jest.fn(), - setExtensionData: jest.fn(), - use: jest.fn(), - userAttributes: undefined, - }; + afterEach(() => { + shaSpy.mockRestore(); + }); - const attributes = { - email: 'test@example.com' - }; + it('should hash a single string value using SHA-256', async () => { + const result = await roktManager.hashSha256('test@example.com'); + const expected = nodeCrypto.createHash('sha256').update('test@example.com').digest('hex'); + + expect(result).toBe(expected); + expect(shaSpy).toHaveBeenCalledWith('test@example.com'); + expect(shaSpy).toHaveBeenCalledTimes(1); + }); - roktManager.hashAttributes(attributes); + it('should hash values without kit being attached', async () => { + // Verify kit is not attached expect(roktManager['kit']).toBeNull(); - expect(roktManager['messageQueue'].size).toBe(1); - const queuedMessage = Array.from(roktManager['messageQueue'].values())[0]; - expect(queuedMessage.methodName).toBe('hashAttributes'); - expect(queuedMessage.payload).toBe(attributes); - expect(kit.hashAttributes).not.toHaveBeenCalled(); - roktManager.attachKit(kit); - expect(roktManager['kit']).not.toBeNull(); - expect(roktManager['messageQueue'].size).toBe(0); - expect(kit.hashAttributes).toHaveBeenCalledWith(attributes); + const result = await roktManager.hashSha256('user@example.com'); + const expected = nodeCrypto.createHash('sha256').update('user@example.com').digest('hex'); + + expect(result).toBe(expected); }); - it('should pass through the correct attributes to kit.launcher.hashAttributes', async () => { - const kit: Partial = { - launcher: { - selectPlacements: jest.fn(), - hashAttributes: jest.fn(), - use: jest.fn(), - }, + it('should handle empty string', async () => { + const emptyStringHash = await roktManager.hashSha256(''); + + // Empty string after trim becomes '', hash of empty string + expect(emptyStringHash).toBe('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'); + }); - // We are mocking the hashAttributes method to return the - // launcher's hashAttributes method and verify that - // both the kit's and the launcher's methods - // are called with the correct attributes. - // This will happen through the Web Kit's hashAttributes method - hashAttributes: jest.fn().mockImplementation((attributes) => { - return kit.launcher.hashAttributes(attributes); - }) - }; + it('should reject when value is null', async () => { + await expect(roktManager.hashSha256(null)).rejects.toThrow('Value cannot be null or undefined'); + }); - roktManager.attachKit(kit as IRoktKit); + it('should reject when value is undefined', async () => { + await expect(roktManager.hashSha256(undefined)).rejects.toThrow('Value cannot be null or undefined'); + }); - const attributes = { - email: 'test@example.com', - phone: '1234567890' - }; + it('should log error when hashing fails', async () => { + shaSpy.mockRejectedValue(new Error('Hash failed')); + + await expect(roktManager.hashSha256('test@example.com')).rejects.toThrow(); + expect(mockMPInstance.Logger.error).toHaveBeenCalledWith(expect.stringContaining('Failed hashSha256')); + }); + + it('should hash firstName to known SHA-256 value', async () => { + const hashedFirstName = await roktManager.hashSha256('jane'); + + // Expected SHA-256 hash of 'jane' + expect(hashedFirstName).toBe('81f8f6dde88365f3928796ec7aa53f72820b06db8664f5fe76a7eb13e24546a2'); + }); + + it('should produce same hash for different case and whitespace variations', async () => { + const lowercaseEmail = await roktManager.hashSha256('jane.doe@gmail.com'); + const mixedCaseEmail = await roktManager.hashSha256('Jane.Doe@gmail.com'); + const emailWithWhitespace = await roktManager.hashSha256(' jane.doe@gmail.com '); + + // All should normalize to same hash + expect(lowercaseEmail).toBe(mixedCaseEmail); + expect(mixedCaseEmail).toBe(emailWithWhitespace); + expect(lowercaseEmail).toBe('831f6494ad6be4fcb3a724c3d5fef22d3ceffa3c62ef3a7984e45a0ea177f982'); + }); + + it('should handle numeric values and match known SHA-256', async () => { + const hashedNumber = await roktManager.hashSha256(42); + const hashedString = await roktManager.hashSha256('42'); - roktManager.hashAttributes(attributes); - expect(kit.hashAttributes).toHaveBeenCalledWith(attributes); - expect(kit.launcher.hashAttributes).toHaveBeenCalledWith(attributes); + // Numeric value should be converted to string and produce same hash + expect(hashedNumber).toBe(hashedString); + // Expected SHA-256 hash of '42' + expect(hashedNumber).toBe('73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049'); + }); + + it('should handle boolean values and match known SHA-256', async () => { + const hashedBoolean = await roktManager.hashSha256(true); + const hashedString = await roktManager.hashSha256('true'); + + // Boolean value should be converted to string and produce same hash + expect(hashedBoolean).toBe(hashedString); + // Expected SHA-256 hash of 'true' + expect(hashedBoolean).toBe('b5bea41b6c623f7c09f1bf24dcae58ebab3c0cdd90ad966bc43a45b44867e12b'); + }); + + it('should hash phone number to known SHA-256 value', async () => { + const hashedPhone = await roktManager.hashSha256('1234567890'); + + // Expected SHA-256 hash of '1234567890' + expect(hashedPhone).toBe('c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646'); }); }); @@ -364,6 +459,17 @@ describe('RoktManager', () => { ); expect(roktManager['domain']).toBe(domain); }); + + it('should set mappedEmailShaIdentityType as a lowercase hashedEmailUserIdentityType when passed as a setting', () => { + roktManager.init( + {settings: {hashedEmailUserIdentityType: 'Other5'}} as unknown as IKitConfigs, + undefined, + mockMPInstance.Identity, + mockMPInstance._Store, + mockMPInstance.Logger, + ); + expect(roktManager['mappedEmailShaIdentityType']).toBe('other5'); + }); }); describe('#attachKit', () => { @@ -426,7 +532,7 @@ describe('RoktManager', () => { expect(kit.selectPlacements).toHaveBeenCalledTimes(3); }); - it('should call RoktManager methods (not kit methods directly) when processing queue', () => { + it('should call RoktManager methods (not kit methods directly) when processing queue', async () => { // Queue some calls before kit is ready (these will be deferred) const selectOptions = { attributes: { test: 'value' } } as IRoktSelectPlacementsOptions; const hashAttrs = { email: 'test@example.com' }; @@ -434,14 +540,12 @@ describe('RoktManager', () => { const useName = 'TestExtension'; roktManager.selectPlacements(selectOptions); - roktManager.hashAttributes(hashAttrs); roktManager.setExtensionData(extensionData); roktManager.use(useName); // Verify calls were queued - expect(roktManager['messageQueue'].size).toBe(4); + expect(roktManager['messageQueue'].size).toBe(3); expect(kit.selectPlacements).not.toHaveBeenCalled(); // Kit methods not called yet - expect(kit.hashAttributes).not.toHaveBeenCalled(); // Kit methods not called yet expect(kit.setExtensionData).not.toHaveBeenCalled(); // Kit methods not called yet expect(kit.use).not.toHaveBeenCalled(); // Kit methods not called yet @@ -451,6 +555,8 @@ describe('RoktManager', () => { const setExtensionDataSpy = jest.spyOn(roktManager, 'setExtensionData'); const useSpy = jest.spyOn(roktManager, 'use'); + const shaSpy = jest.spyOn(roktManager as any, 'sha256Hex').mockResolvedValue('deadbeef'); + await roktManager.hashAttributes(hashAttrs); // Attach kit (triggers processMessageQueue) roktManager.attachKit(kit); @@ -460,7 +566,7 @@ describe('RoktManager', () => { expect(hashAttributesSpy).toHaveBeenCalledTimes(1); expect(hashAttributesSpy).toHaveBeenCalledWith(hashAttrs); - + expect(setExtensionDataSpy).toHaveBeenCalledTimes(1); expect(setExtensionDataSpy).toHaveBeenCalledWith(extensionData); @@ -475,6 +581,7 @@ describe('RoktManager', () => { hashAttributesSpy.mockRestore(); setExtensionDataSpy.mockRestore(); useSpy.mockRestore(); + shaSpy.mockRestore(); }); it('should preserve RoktManager preprocessing logic when processing deferred selectPlacements calls', () => { @@ -554,6 +661,7 @@ describe('RoktManager', () => { describe('#selectPlacements', () => { beforeEach(() => { roktManager['currentUser'] = currentUser; + jest.clearAllMocks(); }); it('should call kit.selectPlacements with empty attributes', () => { @@ -629,6 +737,7 @@ describe('RoktManager', () => { }); it('should process queued selectPlacements calls once the launcher and kit are attached', async () => { + const shaSpy = jest.spyOn(roktManager as any, 'sha256Hex').mockResolvedValue('deadbeef'); const expectedResult = { placements: ['placement1', 'placement2'] }; const kit: IRoktKit = { launcher: { @@ -675,6 +784,7 @@ describe('RoktManager', () => { expect(roktManager['messageQueue'].size).toBe(0); expect(kit.selectPlacements).toHaveBeenCalledWith(options); expect(result).toEqual(expectedResult); + shaSpy.mockRestore(); }); it('should pass through the correct attributes to kit.selectPlacements', () => { @@ -1018,7 +1128,7 @@ describe('RoktManager', () => { } }, expect.any(Function)); expect(mockMPInstance.Logger.warning).toHaveBeenCalledWith( - 'Email mismatch detected. Current email, old@example.com differs from email passed to selectPlacements call, new@example.com. Proceeding to call identify with new@example.com. Please verify your implementation.' + 'Email mismatch detected. Current email differs from email passed to selectPlacements call. Proceeding to call identify with email from selectPlacements call. Please verify your implementation.' ); }); @@ -1110,6 +1220,234 @@ describe('RoktManager', () => { email: 'new@example.com' } }, expect.any(Function)); + expect(mockMPInstance.Logger.warning).toHaveBeenCalled(); + }); + + it('should not call identify when user has current email but no email is passed to selectPlacements', async () => { + const kit: Partial = { + launcher: { + selectPlacements: jest.fn(), + hashAttributes: jest.fn(), + use: jest.fn(), + }, + selectPlacements: jest.fn(), + hashAttributes: jest.fn(), + setExtensionData: jest.fn(), + }; + + roktManager['placementAttributesMapping'] = []; + roktManager.kit = kit as IRoktKit; + + const mockIdentity = { + getCurrentUser: jest.fn().mockReturnValue({ + getUserIdentities: () => ({ + userIdentities: { + email: 'existing@example.com' + } + }), + setUserAttributes: jest.fn() + }), + identify: jest.fn().mockImplementation((data, callback) => { + // Call callback with no error to simulate success + callback(); + }) + } as unknown as SDKIdentityApi; + + roktManager['identityService'] = mockIdentity; + + const options: IRoktSelectPlacementsOptions = { + attributes: { + // No email attribute passed + // customAttribute: 'some-value' + } + }; + + await roktManager.selectPlacements(options); + + expect(mockIdentity.identify).not.toHaveBeenCalled(); + expect(mockMPInstance.Logger.warning).not.toHaveBeenCalled(); + }); + + it('should call identify with emailsha256 mapped to other5 when it differs from current user other5 identity', async () => { + const kit: Partial = { + launcher: { + selectPlacements: jest.fn(), + hashAttributes: jest.fn(), + use: jest.fn(), + }, + selectPlacements: jest.fn().mockResolvedValue({}), + hashAttributes: jest.fn(), + setExtensionData: jest.fn(), + }; + + roktManager.kit = kit as IRoktKit; + roktManager['mappedEmailShaIdentityType'] ='other5'; + + // Set up fresh mocks for this test + const mockIdentity = { + getCurrentUser: jest.fn().mockReturnValue({ + getUserIdentities: () => ({ + userIdentities: { + other5: 'old-other-value' + } + }), + setUserAttributes: jest.fn() + }), + identify: jest.fn().mockImplementation((data, callback) => { + // Call callback with no error to simulate success + callback(); + }) + } as unknown as SDKIdentityApi; + + roktManager['identityService'] = mockIdentity; + + const options: IRoktSelectPlacementsOptions = { + attributes: { + emailsha256: 'new-emailsha256-value' + } + }; + + await roktManager.selectPlacements(options); + + expect(mockIdentity.identify).toHaveBeenCalledWith({ + userIdentities: { + other5: 'new-emailsha256-value' + } + }, expect.any(Function)); + expect(mockMPInstance.Logger.warning).toHaveBeenCalledWith( + "emailsha256 mismatch detected. Current mParticle hashedEmail differs from hashedEmail passed to selectPlacements call. Proceeding to call identify with hashedEmail from selectPlacements call. Please verify your implementation." + ); + }); + + it('should not call identify when emailsha256 matches current user other5 identity', () => { + const kit: Partial = { + launcher: { + selectPlacements: jest.fn(), + hashAttributes: jest.fn(), + use: jest.fn(), + }, + selectPlacements: jest.fn(), + hashAttributes: jest.fn(), + setExtensionData: jest.fn(), + }; + + roktManager.kit = kit as IRoktKit; + roktManager['mappedEmailShaIdentityType'] = 'other5'; + + // Set up fresh mocks for this test + const mockIdentity = { + getCurrentUser: jest.fn().mockReturnValue({ + getUserIdentities: () => ({ + userIdentities: { + other5: 'same-emailsha256-value' + } + }), + setUserAttributes: jest.fn() + }), + identify: jest.fn() + }; + + roktManager['identityService'] = mockIdentity as unknown as SDKIdentityApi; + + const options: IRoktSelectPlacementsOptions = { + attributes: { + emailsha256: 'same-emailsha256-value' + } + }; + + roktManager.selectPlacements(options); + + expect(mockIdentity.identify).not.toHaveBeenCalled(); + expect(mockMPInstance.Logger.warning).not.toHaveBeenCalled(); + }); + + it('should call identify with emailsha256 mapped to other when current user has no other identity', async () => { + const kit: Partial = { + launcher: { + selectPlacements: jest.fn(), + hashAttributes: jest.fn(), + use: jest.fn(), + }, + selectPlacements: jest.fn(), + hashAttributes: jest.fn(), + setExtensionData: jest.fn(), + }; + + roktManager.kit = kit as IRoktKit; + roktManager['mappedEmailShaIdentityType'] = 'other'; + + const mockIdentity = { + getCurrentUser: jest.fn().mockReturnValue({ + getUserIdentities: () => ({ + userIdentities: {} + }), + setUserAttributes: jest.fn() + }), + identify: jest.fn().mockImplementation((data, callback) => { + // Call callback with no error to simulate success + callback(); + }) + } as unknown as SDKIdentityApi; + + roktManager['identityService'] = mockIdentity; + + const options: IRoktSelectPlacementsOptions = { + attributes: { + emailsha256: 'new-emailsha256-value' + } + }; + + await roktManager.selectPlacements(options); + + expect(mockIdentity.identify).toHaveBeenCalledWith({ + userIdentities: { + other: 'new-emailsha256-value' + } + }, expect.any(Function)); + expect(mockMPInstance.Logger.warning).toHaveBeenCalledWith( + "emailsha256 mismatch detected. Current mParticle hashedEmail differs from hashedEmail passed to selectPlacements call. Proceeding to call identify with hashedEmail from selectPlacements call. Please verify your implementation." + ); + }); + + it('should not call identify when current user has other identity but emailsha256 is null', () => { + const kit: Partial = { + launcher: { + selectPlacements: jest.fn(), + hashAttributes: jest.fn(), + use: jest.fn(), + }, + selectPlacements: jest.fn(), + hashAttributes: jest.fn(), + setExtensionData: jest.fn(), + }; + + roktManager.kit = kit as IRoktKit; + roktManager['hashedEmailUserIdentityType'] = 'Other'; + + // Set up fresh mocks for this test + const mockIdentity = { + getCurrentUser: jest.fn().mockReturnValue({ + getUserIdentities: () => ({ + userIdentities: { + other: 'existing-other-value' + } + }), + setUserAttributes: jest.fn() + }), + identify: jest.fn() + }; + + roktManager['identityService'] = mockIdentity as unknown as SDKIdentityApi; + + const options: IRoktSelectPlacementsOptions = { + attributes: { + // emailsha256 is not provided (null/undefined) + } + }; + + roktManager.selectPlacements(options); + + expect(mockIdentity.identify).not.toHaveBeenCalled(); expect(mockMPInstance.Logger.warning).not.toHaveBeenCalled(); }); @@ -1463,4 +1801,70 @@ describe('RoktManager', () => { }); }); + describe('#hasIdentityChanged', () => { + it('should return false when newValue is null', () => { + const result = roktManager['hasIdentityChanged']('current@example.com', null); + expect(result).toBe(false); + }); + + it('should return false when newValue is undefined', () => { + const result = roktManager['hasIdentityChanged']('current@example.com', undefined); + expect(result).toBe(false); + }); + + it('should return false when newValue is empty string', () => { + const result = roktManager['hasIdentityChanged']('current@example.com', ''); + expect(result).toBe(false); + }); + + it('should return true when currentValue is null and newValue exists', () => { + const result = roktManager['hasIdentityChanged'](null, 'new@example.com'); + expect(result).toBe(true); + }); + + it('should return true when currentValue is undefined and newValue exists', () => { + const result = roktManager['hasIdentityChanged'](undefined, 'new@example.com'); + expect(result).toBe(true); + }); + + it('should return true when currentValue is empty string and newValue exists', () => { + const result = roktManager['hasIdentityChanged']('', 'new@example.com'); + expect(result).toBe(true); + }); + + it('should return true when currentValue and newValue are different', () => { + const result = roktManager['hasIdentityChanged']('old@example.com', 'new@example.com'); + expect(result).toBe(true); + }); + + it('should return false when currentValue and newValue are the same', () => { + const result = roktManager['hasIdentityChanged']('same@example.com', 'same@example.com'); + expect(result).toBe(false); + }); + + it('should return false when both currentValue and newValue are null', () => { + const result = roktManager['hasIdentityChanged'](null, null); + expect(result).toBe(false); + }); + + it('should return false when both currentValue and newValue are undefined', () => { + const result = roktManager['hasIdentityChanged'](undefined, undefined); + expect(result).toBe(false); + }); + + it('should return false when both currentValue and newValue are empty strings', () => { + const result = roktManager['hasIdentityChanged']('', ''); + expect(result).toBe(false); + }); + + it('should handle whitespace-only strings as valid values', () => { + const result = roktManager['hasIdentityChanged']('old@example.com', ' '); + expect(result).toBe(true); + }); + + it('should be case sensitive', () => { + const result = roktManager['hasIdentityChanged']('test@example.com', 'TEST@EXAMPLE.COM'); + expect(result).toBe(true); + }); + }); }); \ No newline at end of file