├── .npmignore ├── .gitignore ├── src ├── api │ ├── trackEvent.js │ ├── updateUserProfile.js │ └── sendMixpanelRequest.js └── index.js ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build files 2 | lib 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 30 | node_modules 31 | -------------------------------------------------------------------------------- /src/api/trackEvent.js: -------------------------------------------------------------------------------- 1 | import sendMixpanelRequest from './sendMixpanelRequest' 2 | 3 | // Configuration Constants 4 | const MIXPANEL_TRACK_ENDPOINT = '/track' 5 | 6 | export default function trackEvent ({ token, eventName, distinctId, eventData = {} }) { 7 | // Build event properties 8 | const eventProperties = { 9 | token, 10 | 'distinct_id': distinctId, 11 | ...eventData, 12 | } 13 | 14 | // Build request data for event track request 15 | const trackRequestData = { 16 | event: eventName, 17 | properties: eventProperties, 18 | } 19 | 20 | return sendMixpanelRequest({ 21 | endpoint: MIXPANEL_TRACK_ENDPOINT, 22 | data: trackRequestData, 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /src/api/updateUserProfile.js: -------------------------------------------------------------------------------- 1 | import sendMixpanelRequest from './sendMixpanelRequest' 2 | 3 | // Configuration Constants 4 | const MIXPANEL_ENGAGE_ENDPOINT = '/engage' 5 | 6 | export default function updateUserProfile ({ token, distinctId, userProfileData }, once = false) { 7 | // Build request data for engage request 8 | const engageRequestData = { 9 | '$token': token, 10 | '$distinct_id': distinctId, 11 | } 12 | 13 | if (once) { 14 | engageRequestData['$set_once'] = userProfileData 15 | } else { 16 | engageRequestData['$set'] = userProfileData 17 | } 18 | 19 | return sendMixpanelRequest({ 20 | endpoint: MIXPANEL_ENGAGE_ENDPOINT, 21 | data: engageRequestData, 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/api/sendMixpanelRequest.js: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer' 2 | import request from 'superagent' 3 | 4 | // Configuration Constants 5 | const DEBUG = typeof process === 'object' && process.env && process.env['NODE_ENV'] === 'development' 6 | 7 | // Mixpanel Service Constants 8 | const MIXPANEL_REQUEST_PROTOCOL = 'https' 9 | const MIXPANEL_HOST = 'api.mixpanel.com' 10 | 11 | export default function sendMixpanelRequest ({ endpoint, data }) { 12 | const requestDataString = JSON.stringify(data) 13 | const requestDataBase64String = new Buffer(requestDataString).toString('base64') 14 | 15 | const requestUrl = `${MIXPANEL_REQUEST_PROTOCOL}://${MIXPANEL_HOST}${endpoint}?ip=1` 16 | const req = request 17 | .get(requestUrl) 18 | .query(`data=${requestDataBase64String}`) 19 | .end((error, res) => { 20 | if (!DEBUG) { 21 | return 22 | } 23 | 24 | if (error) { 25 | console.log('mixpanel error:', error) 26 | } 27 | }) 28 | 29 | return req 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rn-redux-mixpanel", 3 | "version": "1.3.0", 4 | "description": "Configurable redux middleware that sends your actions & user profile data to Mixpanel.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "babel --out-dir lib --presets es2015,react --plugins transform-object-rest-spread -- src", 8 | "clean": "rm -rf lib && mkdir lib", 9 | "prepublish": "npm run clean && npm run build", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/danscan/redux-mixpanel.git" 15 | }, 16 | "keywords": [ 17 | "react", 18 | "react-native", 19 | "react-component", 20 | "mixpanel", 21 | "redux", 22 | "middleware" 23 | ], 24 | "author": "Dan Scanlon", 25 | "license": "ISC", 26 | "bugs": { 27 | "url": "https://github.com/danscan/redux-mixpanel/issues" 28 | }, 29 | "homepage": "https://github.com/danscan/redux-mixpanel#readme", 30 | "dependencies": { 31 | "buffer": "^5.0.8", 32 | "superagent": "^3.7.0" 33 | }, 34 | "devDependencies": { 35 | "babel-cli": "^6.5.1", 36 | "babel-core": "^6.5.2", 37 | "babel-plugin-transform-object-rest-spread": "^6.5.0", 38 | "babel-preset-es2015": "^6.5.0", 39 | "babel-preset-react": "^6.5.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import trackEvent from './api/trackEvent' 2 | import updateUserProfile from './api/updateUserProfile' 3 | 4 | export default function mixpanel({ 5 | token, 6 | selectDistinctId = () => null, 7 | selectUserProfileData = () => null, 8 | selectUserProfileDataOnce = () => null, 9 | selectEventName = (action) => action.type, 10 | selectProperties = () => null, 11 | ignoreAction = (action) => false, 12 | }) { 13 | return store => next => action => { 14 | // Don't track falsy actions or actions that should be ignored 15 | if (!action.type || ignoreAction(action)) { 16 | return next(action) 17 | } 18 | 19 | // Get store state; select distinct id for action & state 20 | const state = store.getState() 21 | const distinctId = selectDistinctId(action, state) 22 | const eventName = selectEventName(action, state) 23 | const properties = selectProperties(action, state) 24 | 25 | // Track action event with Mixpanel 26 | trackEvent({ 27 | token, 28 | distinctId, 29 | eventName: eventName, 30 | eventData: properties 31 | }) 32 | 33 | // Select user profile data for action; if it selects truthy data, 34 | // update user profile on Mixpanel 35 | const userProfileData = selectUserProfileData(action, state) 36 | const userProfileDataOnce = selectUserProfileDataOnce(action, state) 37 | if (userProfileData) { 38 | updateUserProfile({ 39 | token, 40 | distinctId, 41 | userProfileData, 42 | }) 43 | } 44 | if (userProfileDataOnce) { 45 | updateUserProfile({ 46 | token, 47 | distinctId, 48 | userProfileData: userProfileDataOnce, 49 | }, true) 50 | } 51 | 52 | return next(action) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rn-redux-mixpanel 2 | Configurable redux middleware that sends your actions & user profile data to Mixpanel. It also works with React Native ;) 3 | 4 | ### Installation 5 | ``` 6 | npm install --save rn-redux-mixpanel 7 | ``` 8 | * * * 9 | 10 | 11 | ### Example 12 | ```javascript 13 | // store/index.js 14 | import mixpanel from 'rn-redux-mixpanel' 15 | import { INIT_PERSISTENCE, HYDRATE, SESSION_ACTIVITY, SIGN_IN } from '../../constants/ActionTypes' 16 | import humanize from 'underscore.string' 17 | 18 | // define a blacklist to be used in the ignoreAction filter 19 | const blacklist = [ 20 | INIT_PERSISTENCE, 21 | HYDRATE, 22 | SESSION_ACTIVITY, 23 | ]; 24 | 25 | // Export configured mixpanel redux middleware 26 | export default mixpanel({ 27 | 28 | // add ignore action filter 29 | ignoreAction: (action) => { 30 | return blacklist.indexOf(action.type) > -1; 31 | }, 32 | 33 | // Mixpanel Token 34 | token: YOUR_MIXPANEL_TOKEN, 35 | 36 | // derive Mixpanel event name from action and/or state 37 | selectEventName: (action, state) => humanize(action.type), 38 | 39 | // Per-action selector: Mixpanel event `distinct_id` 40 | selectDistinctId: (action, state) => { 41 | if (state.session && state.session.userId) { 42 | return state.session.userId 43 | } else if (SIGN_IN === action.type && action.user) { 44 | return action.user._id 45 | } 46 | }, 47 | 48 | // Per-action selector: Mixpanel Engage user profile data 49 | selectUserProfileData: (action, state) => { 50 | const user = action.user 51 | 52 | // Only update user profile data on SIGN_IN action type 53 | if (SIGN_IN === action.type && user) { 54 | // User data to `$set` via Mixpanel Engage request 55 | const userProfileData = { 56 | '$first_name': user['first_name'], 57 | '$last_name': user['last_name'], 58 | '$email': user['email_address'], 59 | '$created': user['date_created'], 60 | } 61 | 62 | return userProfileData 63 | } 64 | }, 65 | 66 | // Per-action selector: Mixpanel Engage user profile set data once 67 | selectUserProfileDataOnce: (action, state) => { 68 | const user = action.user 69 | 70 | // Only update user profile data on SIGN_IN action type 71 | if (SIGN_IN === action.type && user) { 72 | // User data to `$set_once` via Mixpanel Engage request 73 | return { 74 | 'Has Logged In': true, 75 | } 76 | } 77 | } 78 | }) 79 | ``` 80 | 81 | 82 | ### Usage 83 | Configure the `mixpanel` redux middleware by invoking with an options object, containing: 84 | 85 | 1. `token` – Your Mixpanel application token. 86 | 2. `ignoreAction` – An optional function, that receives an action and returns a truthy value, if it should be ignored. 87 | 3. `selectDistinctId` – A selector function that returns the `distinct_id` (user id), given the action and store state. 88 | 4. `selectUserProfileData` – A selector function that returns user profile data for a Mixpanel Engage request, given the action and store state. 89 | 5. `selectUserProfileDataOnce` - A selector that returns people properties and sets it for once. (uses `$set_once` people property) 90 | 6. `selectEventName` – A optional selector function that returns the Mixpanel event name, given the action and store state. By default action.type. 91 | 7. `selectProperties` - An optional selector function that returns Mixpanel properties to add to the request, given the action and store state. 92 | --------------------------------------------------------------------------------