diff --git a/src/MixpanelEventForwarder.js b/src/MixpanelEventForwarder.js index 775bd1e..73c8178 100644 --- a/src/MixpanelEventForwarder.js +++ b/src/MixpanelEventForwarder.js @@ -13,6 +13,15 @@ // See the License for the specific language governing permissions and // limitations under the License. +// ** Glossary of terms +// Mixpanel Terms: +// user_id: A unique identifier used by the Mixpanel.identify method to identify a unique user. This will map to what an mParticle customer selects in the UI, which translates to the forwarderSettings.userIdentificationType. +// device_id: A unique identifier used by Mixpanel to identify an anonymous user, usually a guid. mParticle and Mixpnael generate their own device ids. +// distinct_id: A unique identifier that Mixpanel uses to bridge a user and a device. Usually the +// distinct_id has a prefix of $device:guid to denote an anonymouse user +// and this will be replaced by the user_id once the user has been identified by +// a request to Mixpanel.identify + // eslint-disable-next-line no-redeclare var name = 'MixpanelEventForwarder', moduleId = 10, @@ -137,11 +146,89 @@ var constructor = function () { } } - function onUserIdentified(user) { - var idForMixpanel; - var userIdentities = user.getUserIdentities() + function getUserIdentities(user) { + return user.getUserIdentities() ? user.getUserIdentities().userIdentities : {}; + } + + function onLoginComplete(user) { + var userIdentities = getUserIdentities(user); + + // When mParticle identifies a user, the user might + // actually be anonymous. We only want to send an + // identify request to Mixpanel if the user is + // actually known. Only known (logged in) users + // will have userIdentities. + if (!isEmpty(userIdentities)) { + sendIdentifyRequest(user, userIdentities); + } else { + return 'Logged in user does not have user identities and will not be sent to Mixpanel to Identify'; + } + } + + function onLogoutComplete() { + // For all Identity Requests, we run mixpanel.identify() + // as per Mixpanel's documentation https://docs.mixpanel.com/docs/tracking-methods/identifying-users + // except when a user logs out, where we run mixpanel.reset() to + // detach the Mixpanel distinct_id from the Mixpanel device_id + + if (!isInitialized) { + return ( + 'Cannot call logout on forwarder: ' + name + ', not initialized' + ); + } + + try { + mixpanel.mparticle.reset(); + + return 'Successfully called reset on forwarder: ' + name; + } catch (e) { + return 'Cannot call reset on forwarder: ' + name + ': ' + e; + } + } + + function onIdentifyComplete(user) { + // Mixpanel considers any user with an identity to be a known user. + // In mParticle, a user will always have an MPID even if they are anonymous. + // When mParticle identifies a user, because the user might + // actually be anonymous, we only want to send an + // identify request to Mixpanel if the user is + // actually known. If a user has any user identities, they are + // considered to be "known" users. + var userIdentities = getUserIdentities(user); + + if (!isEmpty(userIdentities)) { + sendIdentifyRequest(user, userIdentities); + } else { + return 'Identified user does not have user identities and will not be sent to Mixpanel to Identify'; + } + } + + function onModifyComplete(user) { + // Mixpanel does not have the concept of modifying a + // user's identity. For the time being, we will rely on + // doing a simple Mixpanel.identify request for backwards compatibility. However + // this method may be deprecated in a future release + var userIdentities = getUserIdentities(user); + + if (!isEmpty(userIdentities)) { + sendIdentifyRequest(user, userIdentities); + } else { + return 'Modified user does not have user identities and will not be sent to Mixpanel to Identify'; + } + } + + function sendIdentifyRequest(user, userIdentities) { + // We should only make Mixpanel.identify requests + // when a user has userIdentities and is therefore + // a known mParticle user and not anonymous + if (isEmpty(userIdentities)) { + return; + } + + var idForMixpanel; + switch (forwarderSettings.userIdentificationType) { case 'CustomerId': idForMixpanel = userIdentities.customerid; @@ -245,8 +332,12 @@ var constructor = function () { this.process = processEvent; this.setUserAttribute = setUserAttribute; this.setUserIdentity = setUserIdentity; - this.onUserIdentified = onUserIdentified; this.removeUserAttribute = removeUserAttribute; + + this.onIdentifyComplete = onIdentifyComplete; + this.onLoginComplete = onLoginComplete; + this.onLogoutComplete = onLogoutComplete; + this.onModifyComplete = onModifyComplete; }; function getId() { @@ -283,6 +374,10 @@ function register(config) { ); } +function isEmpty(value) { + return value == null || !(Object.keys(value) || value).length; +} + function isObject(val) { return ( val != null && typeof val === 'object' && Array.isArray(val) === false diff --git a/test/src/tests.js b/test/src/tests.js index a99a286..b538dd7 100644 --- a/test/src/tests.js +++ b/test/src/tests.js @@ -104,6 +104,10 @@ describe('Mixpanel Forwarder', function () { setCalledAttributes(data, 'identifyCalled'); }; + this.mparticle.reset = function () { + setCalledAttributes(null, 'resetCalled'); + }; + this.mparticle.alias = function (data) { setCalledAttributes(data, 'aliasCalled'); }; @@ -144,7 +148,34 @@ describe('Mixpanel Forwarder', function () { } var API_HOST = 'https://api.mixpanel.com'; - before(function () { + var identificationTypes = [ + { + userIdentificationType: 'MPID', + expectedProperty: 'mpid1', + }, + { + userIdentificationType: 'CustomerId', + expectedProperty: 'cust1', + }, + { + userIdentificationType: 'Other', + expectedProperty: 'other1', + }, + { + userIdentificationType: 'Other2', + expectedProperty: 'other2', + }, + { + userIdentificationType: 'Other3', + expectedProperty: 'other3', + }, + { + userIdentificationType: 'Other4', + expectedProperty: 'other4', + }, + ]; + + beforeEach(function () { window.mixpanel = new MPMock(); mParticle.forwarder.init( { @@ -284,15 +315,7 @@ describe('Mixpanel Forwarder', function () { done(); }); - it('should identify user (mParticle SDK v2)', function (done) { - mParticle.forwarder.init( - { - includeUserAttributes: 'True', - userIdentificationType: 'CustomerId', - }, - reportService.cb, - true - ); + it('should log in a user (mParticle SDK v2)', function (done) { var user = { getUserIdentities: function () { return { @@ -310,92 +333,165 @@ describe('Mixpanel Forwarder', function () { }, }; - mParticle.forwarder.onUserIdentified(user); - window.mixpanel.mparticle.should.have.property( - 'identifyCalled', - true - ); - window.mixpanel.mparticle.should.have.property('data', 'cust1'); - - mParticle.forwarder.init( - { - includeUserAttributes: 'True', - userIdentificationType: 'MPID', - }, - reportService.cb, - true - ); + identificationTypes.forEach(function (identificationType) { + mParticle.forwarder.init( + { + includeUserAttributes: 'True', + userIdentificationType: + identificationType.userIdentificationType, + }, + reportService.cb, + true + ); + + mParticle.forwarder.onLoginComplete(user); + window.mixpanel.mparticle.should.have.property( + 'identifyCalled', + true + ); + window.mixpanel.mparticle.should.have.property( + 'data', + identificationType.expectedProperty + ); + }); - mParticle.forwarder.onUserIdentified(user); - window.mixpanel.mparticle.should.have.property( - 'identifyCalled', - true - ); - window.mixpanel.mparticle.should.have.property('data', 'mpid1'); + done(); + }); - mParticle.forwarder.init( - { - includeUserAttributes: 'True', - userIdentificationType: 'Other', + it('should NOT log in a user if they do not have user identities (mParticle SDK v2)', function (done) { + var user = { + getUserIdentities: function () { + return { + userIdentities: {}, + }; }, - reportService.cb, - true - ); + getMPID: function () { + return 'anon-mpid1'; + }, + }; - mParticle.forwarder.onUserIdentified(user); - window.mixpanel.mparticle.should.have.property( - 'identifyCalled', - true - ); - window.mixpanel.mparticle.should.have.property('data', 'other1'); + identificationTypes.forEach(function (identificationType) { + mParticle.forwarder.init( + { + includeUserAttributes: 'True', + userIdentificationType: + identificationType.userIdentificationType, + }, + reportService.cb, + true + ); + + mParticle.forwarder.onLoginComplete(user); + window.mixpanel.mparticle.should.have.property( + 'identifyCalled', + false + ); + window.mixpanel.mparticle.should.not.have.property( + 'data', + identificationType.expectedProperty + ); + }); + done(); + }); - mParticle.forwarder.init( - { - includeUserAttributes: 'True', - userIdentificationType: 'Other2', + it('should identify a user (mParticle SDK v2)', function (done) { + var user = { + getUserIdentities: function () { + return { + userIdentities: { + customerid: 'cust1', + other: 'other1', + other2: 'other2', + other3: 'other3', + other4: 'other4', + }, + }; }, - reportService.cb, - true - ); + getMPID: function () { + return 'mpid1'; + }, + }; - mParticle.forwarder.onUserIdentified(user); - window.mixpanel.mparticle.should.have.property( - 'identifyCalled', - true - ); - window.mixpanel.mparticle.should.have.property('data', 'other2'); + identificationTypes.forEach(function (identificationType) { + mParticle.forwarder.init( + { + includeUserAttributes: 'True', + userIdentificationType: + identificationType.userIdentificationType, + }, + reportService.cb, + true + ); + + mParticle.forwarder.onIdentifyComplete(user); + window.mixpanel.mparticle.should.have.property( + 'identifyCalled', + true + ); + window.mixpanel.mparticle.should.have.property( + 'data', + identificationType.expectedProperty + ); + }); - mParticle.forwarder.init( - { - includeUserAttributes: 'True', - userIdentificationType: 'Other3', + done(); + }); + + it('should modify a user identity (mParticle SDK v2)', function (done) { + var user = { + getUserIdentities: function () { + return { + userIdentities: { + customerid: 'cust1', + other: 'other1', + other2: 'other2', + other3: 'other3', + other4: 'other4', + }, + }; }, - reportService.cb, - true - ); + getMPID: function () { + return 'mpid1'; + }, + }; - mParticle.forwarder.onUserIdentified(user); - window.mixpanel.mparticle.should.have.property( - 'identifyCalled', - true - ); - window.mixpanel.mparticle.should.have.property('data', 'other3'); + identificationTypes.forEach(function (identificationType) { + mParticle.forwarder.init( + { + includeUserAttributes: 'True', + userIdentificationType: + identificationType.userIdentificationType, + }, + reportService.cb, + true + ); + + mParticle.forwarder.onModifyComplete(user); + window.mixpanel.mparticle.should.have.property( + 'identifyCalled', + true + ); + window.mixpanel.mparticle.should.have.property( + 'data', + identificationType.expectedProperty + ); + }); + + done(); + }); + it('should log out a user (mParticle SDK v2)', function (done) { mParticle.forwarder.init( { includeUserAttributes: 'True', - userIdentificationType: 'Other4', + userIdentificationType: 'MPID', }, reportService.cb, true ); - mParticle.forwarder.onUserIdentified(user); - window.mixpanel.mparticle.should.have.property( - 'identifyCalled', - true - ); - window.mixpanel.mparticle.should.have.property('data', 'other4'); + mParticle.forwarder.onLogoutComplete(); + window.mixpanel.mparticle.should.have.property('resetCalled', true); done(); });