UNPKG

@segment/analytics.js-integration-amplitude

Version:
504 lines (431 loc) 16.1 kB
'use strict'; /** * Module dependencies. */ var bind = require('component-bind'); var integration = require('@segment/analytics.js-integration'); var topDomain = require('@segment/top-domain'); var when = require('do-when'); var is = require('is'); var each = require('@ndhoule/each'); var Track = require('segmentio-facade').Track; /** * UMD? */ var umd = typeof window.define === 'function' && window.define.amd; /** * Source. */ var src = 'https://cdn.amplitude.com/libs/amplitude-5.2.2-min.gz.js'; /** * Expose `Amplitude` integration. */ var Amplitude = (module.exports = integration('Amplitude') .global('amplitude') .option('apiKey', '') .option('trackAllPages', false) .option('trackNamedPages', true) .option('trackCategorizedPages', true) .option('trackUtmProperties', true) .option('trackReferrer', false) .option('batchEvents', false) .option('eventUploadThreshold', 30) .option('eventUploadPeriodMillis', 30000) .option('useLogRevenueV2', false) .option('forceHttps', false) .option('trackGclid', false) .option('saveParamsReferrerOncePerSession', true) .option('deviceIdFromUrlParam', false) .option('mapQueryParams', {}) .option('trackRevenuePerProduct', false) .option('preferAnonymousIdForDeviceId', false) .option('traitsToSetOnce', []) .option('traitsToIncrement', []) .option('appendFieldsToEventProps', {}) .option('unsetParamsReferrerOnNewSession', false) .option('trackProductsOnce', false) .option('versionName', '') .option('useAmplitudeReferral', false) .tag('<script src="' + src + '">')); /** * Initialize. * * https://github.com/amplitude/Amplitude-Javascript * * @api public */ Amplitude.prototype.initialize = function() { /* prettier-ignore */ // amplitude snippet (lines loading amplitude cdn-served script are removed as that is already achieved via Segment tag and load methods) /* eslint-disable */ /* istanbul ignore next */ (function(e,t){var n=e.amplitude||{_q:[],_iq:{}};;function s(e,t){e.prototype[t]=function(){this._q.push([t].concat(Array.prototype.slice.call(arguments,0)));return this}}var o=function(){this._q=[];return this};var a=["add","append","clearAll","prepend","set","setOnce","unset"];for(var u=0;u<a.length;u++){s(o,a[u])}n.Identify=o;var c=function(){this._q=[];return this};var l=["setProductId","setQuantity","setPrice","setRevenueType","setEventProperties"];for(var p=0;p<l.length;p++){s(c,l[p])}n.Revenue=c;var d=["init","logEvent","logRevenue","setUserId","setUserProperties","setOptOut","setVersionName","setDomain","setDeviceId","setGlobalUserProperties","identify","clearUserProperties","setGroup","logRevenueV2","regenerateDeviceId","groupIdentify","onInit","logEventWithTimestamp","logEventWithGroups","setSessionId","resetSessionId"];function v(e){function t(t){e[t]=function(){e._q.push([t].concat(Array.prototype.slice.call(arguments,0)))}}for(var n=0;n<d.length;n++){t(d[n])}}v(n);n.getInstance=function(e){e=(!e||e.length===0?"$default_instance":e).toLowerCase();if(!n._iq.hasOwnProperty(e)){n._iq[e]={_q:[]};v(n._iq[e])}return n._iq[e]};e.amplitude=n})(window,document); /* eslint-enable */ this.setDomain(window.location.href); window.amplitude.getInstance().init(this.options.apiKey, null, { includeUtm: this.options.trackUtmProperties, batchEvents: this.options.batchEvents, eventUploadThreshold: this.options.eventUploadThreshold, eventUploadPeriodMillis: this.options.eventUploadPeriodMillis, forceHttps: this.options.forceHttps, includeGclid: this.options.trackGclid, saveParamsReferrerOncePerSession: this.options .saveParamsReferrerOncePerSession, includeReferrer: this.options.useAmplitudeReferral, deviceIdFromUrlParam: this.options.deviceIdFromUrlParam, unsetParamsReferrerOnNewSession: this.options .unsetParamsReferrerOnNewSession, deviceId: this.options.preferAnonymousIdForDeviceId && this.analytics && this.analytics.user() && this.analytics.user().anonymousId() }); // Initialize the amplitute with user specified site version if any. // https://help.amplitude.com/hc/en-us/articles/115001361248-JavaScript-SDK-Installation#setting-version-name var versionName = this.options.versionName; if (versionName) { window.amplitude.getInstance().setVersionName(versionName); } var loaded = bind(this, this.loaded); var ready = this.ready; // FIXME (wcjohnson11): Refactor the load method to include this logic // to better support if UMD present /* istanbul ignore if */ if (umd) { window.require([src], function(amplitude) { window.amplitude = amplitude; when(loaded, function() { window.amplitude.runQueuedFunctions(); ready(); }); }); return; } this.load(function() { if (window.amplitude.runQueuedFunctions) { window.amplitude.runQueuedFunctions(); ready(); } }); }; /** * Loaded? * * @api private * @return {boolean} */ Amplitude.prototype.loaded = function() { return !!(window.amplitude && window.amplitude.getInstance().options); }; /** * Page. * * @api public * @param {Page} page */ Amplitude.prototype.page = function(page) { this.setDeviceIdFromAnonymousId(page); if (this.options.trackReferrer && !this.options.useAmplitudeReferral) this.sendReferrer(); var category = page.category(); var name = page.fullName(); var opts = this.options; // all pages if (opts.trackAllPages) { this.track(page.track()); } // categorized pages if (category && opts.trackCategorizedPages) { this.track(page.track(category)); } // named pages if (name && opts.trackNamedPages) { this.track(page.track(name)); } }; /** * Identify. * * @api public * @param {Facade} identify */ Amplitude.prototype.identify = function(identify) { this.setDeviceIdFromAnonymousId(identify); var id = identify.userId(); var traits = identify.traits(); if (id) window.amplitude.getInstance().setUserId(id); // map query params from context url if opted in var mapQueryParams = this.options.mapQueryParams; var query = identify.proxy('context.page.search'); if (!is.empty(mapQueryParams)) { // since we accept any arbitrary property name and we dont have conditional UI components // in the app where we can limit users to only add a single mapping, so excuse the temporary jank each(function(value, key) { traits[key] = query; }, mapQueryParams); } this.setTraits(traits); // Set user groups: https://amplitude.zendesk.com/hc/en-us/articles/115001361248#setting-user-groups var groups = identify.options(this.name).groups; if (groups && is.object(groups)) { for (var group in groups) { if (groups.hasOwnProperty(group)) window.amplitude.getInstance().setGroup(group, groups[group]); } } }; Amplitude.prototype.setTraits = function(traits) { var traitsToIncrement = this.options.traitsToIncrement || []; var traitsToSetOnce = this.options.traitsToSetOnce || []; var amplitudeIdentify = new window.amplitude.Identify(); for (var trait in traits) { if (!traits.hasOwnProperty(trait)) continue; var shouldIncrement = traitsToIncrement.indexOf(trait) >= 0; var shouldSetOnce = traitsToSetOnce.indexOf(trait) >= 0; if (shouldIncrement) { amplitudeIdentify.add(trait, traits[trait]); } if (shouldSetOnce) { amplitudeIdentify.setOnce(trait, traits[trait]); } if (!shouldIncrement && !shouldSetOnce) { amplitudeIdentify.set(trait, traits[trait]); } } window.amplitude.identify(amplitudeIdentify); }; /** * Track. * * @api public * @param {Track} event */ Amplitude.prototype.track = logEvent; function logEvent(track, dontSetRevenue) { this.setDeviceIdFromAnonymousId(track); var props = track.properties(); var options = track.options(this.name); var event = track.event(); // map query params from context url if opted in var mapQueryParams = this.options.mapQueryParams; var query = track.proxy('context.page.search'); if (!is.empty(mapQueryParams)) { var params = {}; var type; // since we accept any arbitrary property name and we dont have conditional UI components // in the app where we can limit users to only add a single mapping, so excuse the temporary jank each(function(value, key) { // add query params to either `user_properties` or `event_properties` type = value; if (type === 'user_properties') { params[key] = query; } else { props[key] = query; } }, mapQueryParams); if (type === 'user_properties') window.amplitude.getInstance().setUserProperties(params); } // Append extra fields to event_props each(function(prop, field) { props[prop] = track.proxy(field); }, this.options.appendFieldsToEventProps); // track the event if (options.groups) { window.amplitude .getInstance() .logEventWithGroups(event, props, options.groups); } else { window.amplitude.getInstance().logEvent(event, props); } // Ideally, user's will track revenue using an Order Completed event. // However, we have previously setRevenue for any event given it had a revenue property. // We need to keep this behavior around for backwards compatibility. if (track.revenue() && !dontSetRevenue) this.setRevenue(mapRevenueAttributes(track)); } Amplitude.prototype.orderCompleted = function(track) { this.setDeviceIdFromAnonymousId(track); var products = track.products(); var clonedTrack = track.json(); var trackRevenuePerProduct = this.options.trackRevenuePerProduct; var revenueType = track.proxy('properties.revenueType'); var revenue = track.revenue(); var trackProductsOnce = this.options.trackProductsOnce; if (trackProductsOnce) { var allProducts = []; // products is object var productKeys = Object.keys(products); for (var index = 0; index < productKeys.length; index++) { var eachProduct = new Track({ properties: products[index] }); allProducts.push(mapProductAttributes(eachProduct)); } clonedTrack.properties.products = allProducts; logEvent.call(this, new Track(clonedTrack), trackRevenuePerProduct); return; } // Our Order Completed event however uses a products array for product level tracking. // We need to remove this before logging the event and then use it to track revenue. delete clonedTrack.properties.products; // There are two ways to track revenue with Amplitude: // 1) Log a single Revenue event for all products in the order. // 2) Log a Revenue event for each product in the order. // If the user has chosen the second option, we pass a dontSetRevenue flag to logEvent. logEvent.call(this, new Track(clonedTrack), trackRevenuePerProduct); // Loop through products array. each( function(product) { var price = product.price; var quantity = product.quantity; clonedTrack.properties = product; clonedTrack.event = 'Product Purchased'; // Price and quantity are both required by Amplitude: // https://amplitude.zendesk.com/hc/en-us/articles/115001361248#tracking-revenue // Price could potentially be 0 so handle that edge case. if (trackRevenuePerProduct && price != null && quantity) { // Add revenueType if exists, to be able to override. if (revenueType) { clonedTrack.properties.revenueType = revenueType; } if (revenue) { clonedTrack.properties.revenue = revenue; } this.setRevenue(mapRevenueAttributes(new Track(clonedTrack))); } logEvent.call(this, new Track(clonedTrack), trackRevenuePerProduct); }.bind(this), products ); }; /** * Group. * * @api public * @param {Group} group */ Amplitude.prototype.group = function(group) { this.setDeviceIdFromAnonymousId(group); var groupType = group.traits()[this.options.groupTypeTrait]; var groupValue = group.traits()[this.options.groupValueTrait]; if (groupType && groupValue) { window.amplitude.getInstance().setGroup(groupType, groupValue); } else { var groupId = group.groupId(); if (groupId) { window.amplitude.getInstance().setGroup('[Segment] Group', groupId); } } }; /** * Set domain name to root domain in Amplitude. * * @api private * @param {string} href */ Amplitude.prototype.setDomain = function(href) { var domain = topDomain(href); window.amplitude.setDomain(domain); }; /** * If enabled by settings, set the device ID from the Segment anonymous ID. * * This logic cannot be performed at initialization time, because customers may * want to modify anonymous IDs between initializing Segment and sending their * first event. * * @api private * @param {Fadade} facade to get anonymousId from. */ Amplitude.prototype.setDeviceIdFromAnonymousId = function(facade) { if (this.options.preferAnonymousIdForDeviceId) { this.setDeviceId(facade.anonymousId()); } }; /** * Override device ID in Amplitude. * * @api private * @param {string} deviceId */ Amplitude.prototype.setDeviceId = function(deviceId) { if (deviceId) window.amplitude.getInstance().setDeviceId(deviceId); }; Amplitude.prototype.setRevenue = function(properties) { var price = properties.price; var productId = properties.productId; var revenueType = properties.revenueType; var quantity = properties.quantity; var eventProps = properties.eventProps; var revenue = properties.revenue; if (this.options.useLogRevenueV2) { // This is to support backwards compatibility with a legacy revenue tracking strategy. // Using a properly formatted Order Completed event is the recommended strategy now. // If it is properly formatted, this voodoo will not happen. if (!price) { price = revenue; quantity = 1; } var ampRevenue = new window.amplitude.Revenue() .setPrice(price) .setQuantity(quantity) .setEventProperties(eventProps); if (revenueType) ampRevenue.setRevenueType(revenueType); if (productId) ampRevenue.setProductId(productId); window.amplitude.getInstance().logRevenueV2(ampRevenue); } else { window.amplitude .getInstance() .logRevenue(revenue || price * quantity, quantity, productId); } }; // equivalent of sending includeReferrer=true in the amplitude config object when we initialize Amplitude.prototype.sendReferrer = function() { var identify = new window.amplitude.Identify(); var referrer = this.getReferrer(); if (!referrer || referrer.length === 0) return; identify.setOnce('initial_referrer', referrer); identify.set('referrer', referrer); var parts = referrer.split('/'); if (parts.length >= 3) { var referringDomain = parts[2]; identify.setOnce('initial_referring_domain', referringDomain); identify.set('referring_domain', referringDomain); } window.amplitude.getInstance().identify(identify); }; // wrapper for testing purposes Amplitude.prototype.getReferrer = function() { return document.referrer; }; function mapRevenueAttributes(track) { // Revenue type can be anything such as Refund, Tax, etc. // Using mapper here to support future ecomm event => revenue mappings (Order Refund, etc.) var mapRevenueType = { 'order completed': 'Purchase', 'completed order': 'Purchase', 'product purchased': 'Purchase' }; return { price: track.price(), productId: track.productId(), revenueType: track.proxy('properties.revenueType') || mapRevenueType[track.event().toLowerCase()], quantity: track.quantity(), eventProps: track.properties(), revenue: track.revenue() }; } // map product for Order Completed event // REF: https://segment.com/docs/destinations/amplitude/#order-completed function mapProductAttributes(product) { return { productId: product.productId(), sku: product.sku(), name: product.name(), price: product.price(), quantity: product.quantity(), category: product.category() }; }