From 4cb65642c54a09d26a5f1b4de551020efe313bd5 Mon Sep 17 00:00:00 2001 From: Juro Oravec Date: Sat, 14 Nov 2020 17:42:34 +0000 Subject: [PATCH 01/14] types(core): add typedefs for plugin hooks --- packages/analytics-core/src/pluginTypeDef.js | 508 ++++++++++++++++++- 1 file changed, 498 insertions(+), 10 deletions(-) diff --git a/packages/analytics-core/src/pluginTypeDef.js b/packages/analytics-core/src/pluginTypeDef.js index bf660429..5bae2a92 100644 --- a/packages/analytics-core/src/pluginTypeDef.js +++ b/packages/analytics-core/src/pluginTypeDef.js @@ -1,12 +1,500 @@ +////////////////// +// Plugins +////////////////// + /** - * @typedef {Object} AnalyticsPlugin - * @property {string} name - Name of plugin - * @property {Object} [EVENTS] - exposed events of plugin - * @property {Object} [config] - Configuration of plugin - * @property {function} [initialize] - Load analytics scripts method - * @property {function} [page] - Page visit tracking method - * @property {function} [track] - Custom event tracking method - * @property {function} [identify] - User identify method - * @property {function} [loaded] - Function to determine if analytics script loaded - * @property {function} [ready] - Fire function when plugin ready + * @typedef {Object} AnalyticsPlugin + * @property {string} name Name of plugin + * @property {Object} [EVENTS] Exposed events of plugin + * @property {Object} [config] Configuration of plugin + * @property {Hook} [loaded] - Function to determine if analytics script loaded + * @property {BootstrapHook} [bootstrap] Fires when analytics library starts up + * @property {ParamsHook} [params] Fires when analytics parses URL parameters + * @property {CampaignHook} [campaign] Fires if params contain "utm" parameters + * @property {InitializeHook} [initializeStart] Fires before 'initialize', allows for plugins to cancel loading of other plugins + * @property {InitializeHook} [initialize] Fires when analytics loads plugins + * @property {InitializeHook} [initializeEnd] Fires after initialize, allows for plugins to run logic after initialization methods run + * @property {ReadyHook} [ready] Fires when all analytic providers are fully loaded. This waits for 'initialize' and 'loaded' to return true + * @property {ResetHook} [resetStart] Fires if analytic.reset() is called. + * @property {ResetHook} [reset] Fires if analytic.reset() is called. + * @property {ResetHook} [resetEnd] Fires after analytic.reset() is called. + * @property {PageHook} [pageStart] Fires before 'page' events fire. + * @property {PageHook} [page] Core analytics hook for page views. + * @property {PageHook} [pageEnd] Fires after all registered 'page' methods fire. + * @property {PageHook} [pageAborted] Fires if 'page' call is cancelled by a plugin + * @property {TrackHook} [trackStart] Called before the 'track' events fires. + * @property {TrackHook} [track] Core analytics hook for event tracking. + * @property {TrackHook} [trackEnd] Fires after all registered 'track' events fire from plugins. + * @property {TrackHook} [trackAborted] Fires if 'track' call is cancelled by a plugin + * @property {IdentifyHook} [identifyStart] Called before the 'identify' events fires. + * @property {IdentifyHook} [identify] Core analytics hook for user identification. + * @property {IdentifyHook} [identifyEnd] Fires after all registered 'identify' events fire from plugins. + * @property {IdentifyHook} [identifyAborted] Fires if 'identify' call is cancelled by a plugin + * @property {UserIdChangedHook} [userIdChanged] Fires when a user id is updated + * @property {RegisterPluginsHook} [registerPlugins] Fires when analytics is registering plugins + * @property {EnablePluginHook} [enablePlugin] Fires when 'analytics.enablePlugin()' is called + * @property {EnablePluginHook} [disablePlugin] Fires when 'analytics.disablePlugin()' is called + * @property {EnablePluginHook} [loadPlugin] Fires when 'analytics.loadPlugin()' is called + * @property {EmptyHook} [online] Fires when browser network goes online. + * @property {EmptyHook} [offline] Fires when browser network goes offline. + * @property {SetItemHook} [setItemStart] Fires when analytics.storage.setItem is initialized. + * @property {SetItemHook} [setItem] Fires when analytics.storage.setItem is called. + * @property {SetItemHook} [setItemEnd] Fires when setItem storage is complete. + * @property {SetItemHook} [setItemAborted] Fires when setItem storage is cancelled by a plugin. + * @property {RemoveItemHook} [removeItemStart] Fires when analytics.storage.removeItem is initialized. + * @property {RemoveItemHook} [removeItem] Fires when analytics.storage.removeItem is called. + * @property {RemoveItemHook} [removeItemEnd] Fires when removeItem storage is complete. + * @property {RemoveItemHook} [removeItemAborted] Fires when removeItem storage is cancelled by a plugin. + */ + +/** + * @typedef {Object} PluginState State of a plugin + * @property {Object} [config] Configuration of plugin + * @property {Boolean} enabled Whether plugin is enabled + * @property {Boolean} initialized Whether plugin is initialized + * @property {Boolean} loaded Whether plugin is loaded + */ + +/** + * @typedef {Function} Abort + * @returns {void} + */ + +////////////////// +// Hooks +////////////////// + +// Hooks - Generic + +/** + * @typedef {Object} PayloadBase + * @property {String} type - Event type + */ + +/** + * @typedef {PayloadBase} EmptyPayload + */ + +/** + * @typedef {Object} EmptyContextProps + * @property {PayloadBase} payload Empty payload in hook + */ + + /** + * @typedef {HookContextCommon & EmptyContextProps} EmptyContext Context with empty payload passed to a hook + */ + + /** + * @typedef {Function} EmptyHook + * @param {EmptyContext} context Context with empty payload passed to a base hook + */ + +/** + * @typedef {Object} HookContextBase Base of the context passed to a generic plugin hook + * @property {string} hello Name of current plugin + * @property {AnalyticsInstance} instance Analytics instance + * @property {Object} [config] Configuration of plugin + */ + +/** + * @typedef {Object} HookContextPropsCommon + * @property {Object.} plugins Name of plugins(s) to disable + * @property {Abort} abort Name of plugins(s) to disable */ + +/** + * @typedef {HookContextBase & HookContextPropsCommon} HookContextCommon + */ + +/** + * @typedef {PayloadBase} HookPayload Payload in any hook + */ + +/** + * @typedef {Object} HookContextProps + * @property {Object} payload Payload in any hook + */ + +/** + * @typedef {HookContextCommon & HookContextProps} HookContext Context passed to a generic plugin hook + * @property {Object} payload Name of plugins(s) to disable + */ + +/** + * @typedef {Function} Hook Generic hook on the AnalyticsPlugin object. + * @param {HookContext} context Context passed to a plugin hook + */ + +// Hooks - Bootstrap + +/** + * @typedef {AnalyticsPlugin} BootstrapPayload Payload in any hook + */ + +/** + * @typedef {Object} BootstrapContextProps Context passed to bootstrap hook + * @property {BootstrapPayload} payload Analytics plugin + */ + + /** + * @typedef {HookContextCommon & BootstrapContextProps} BootstrapContext Context passed to bootstrap hook + */ + + /** + * @typedef {Function} BootstrapHook + * @param {BootstrapContext} context Context passed to bootstrap hook + * @returns {void} + */ + +// Hooks - Params + +/** + * @typedef {Object} ParamsPayloadProps Payload in params hook + * @property {Object.} raw Raw search params + * @property {Object.} props Props search params + * @property {Object.} traits Traits search params + * @property {String} [userId] User ID. + */ + + /** + * @typedef {PayloadBase & CampaignPayloadProps & ParamsPayloadProps} ParamsPayload Payload in params hook + */ + + /** + * @typedef {Object} ParamsContextProps + * @property {ParamsPayload} payload Payload in params hook + */ + + /** + * @typedef {HookContextCommon & ParamsContextProps} ParamsContext Context passed to params hook + */ + + /** + * @typedef {Function} ParamsHook + * @param {ParamsContext} context Context passed to params hook + */ + +// Hooks - Campaign + +/** + * @typedef {Object} CampaignPayloadProps + * @property {Object.} campaign UTM search params + */ + + /** + * @typedef {PayloadBase & CampaignPayloadProps} CampaignPayload Payload in campaign hook + */ + + /** + * @typedef {Object} CampaignContextProps + * @property {CampaignPayload} payload Payload in campaign hook + */ + + /** + * @typedef {HookContextCommon & CampaignContextProps} CampaignContext Context passed to campaign hook + */ + + /** + * @typedef {Function} CampaignHook + * @param {CampaignContext} context Context passed to campaign hook + */ + +// Hooks - Initialize + +/** + * @typedef {Object} InitializePayloadProps + * @property {Array.} plugins Names of plugins + */ + + /** + * @typedef {PayloadBase & InitializePayloadProps} InitializePayload Payload in initialize hook + */ + + /** + * @typedef {Object} InitializeContextProps + * @property {InitializePayload} payload Payload in initialize hook + */ + + /** + * @typedef {HookContextCommon & InitializeContextProps} InitializeContext Context passed to initialize hook + */ + + /** + * @typedef {Function} InitializeHook + * @param {InitializeContext} context Context passed to initialize hook + */ + +// Hooks - Ready + +/** + * @typedef {Object} ReadyPayloadProps + * @property {Array.} failed Names of failed plugins + */ + + /** + * @typedef {PayloadBase & InitializePayloadProps & ReadyPayloadProps} ReadyPayload Payload in ready hook + */ + + /** + * @typedef {Object} ReadyContextProps + * @property {ReadyPayload} payload Payload in ready hook + */ + + /** + * @typedef {HookContextCommon & ReadyContextProps} ReadyContext Context passed to ready hook + */ + +/** + * @typedef {Function} ReadyHook + * @param {ReadyContext} context Context passed to ready hook + */ + +// Hooks - Reset + +/** + * @typedef {Object} ResetPayloadProps + * @property {Number} timestamp + * @property {Function} callback + */ + + /** + * @typedef {PayloadBase & ResetPayloadProps} ResetPayload Payload in reset hook + */ + + /** + * @typedef {Object} ResetContextProps + * @property {ResetPayload} payload Payload in reset hook + */ + + /** + * @typedef {HookContextCommon & ResetContextProps} ResetContext Context passed to reset hook + */ + + /** + * @typedef {Function} ResetHook + * @param {ResetContext} context Context passed to reset hook + */ + +// Hooks - Page + +/** + * @typedef {Object} PagePayloadProps + * @property {String} anonymousId + * @property {String} userId + * @property {ResetPayloadProps} meta + * @property {Object} properties + * @property {Object} options + */ + + /** + * @typedef {PayloadBase & PagePayloadProps} PagePayload Payload in page hook + */ + + /** + * @typedef {Object} PageContextProps + * @property {PagePayload} payload Payload in page hook + */ + + /** + * @typedef {HookContextCommon & PageContextProps} PageContext Context passed to page hook + */ + + /** + * @typedef {Function} PageHook + * @param {PageContext} context Context passed to page hook + */ + +// Hooks - Track + +/** + * @typedef {Object} TrackPayloadProps + * @property {String} anonymousId + * @property {String} userId + * @property {String} event + * @property {ResetPayloadProps} meta + * @property {Object} properties + * @property {Object} options + */ + + /** + * @typedef {PayloadBase & TrackPayloadProps} TrackPayload Payload in track hook + */ + + /** + * @typedef {Object} TrackContextProps + * @property {TrackPayload} payload Payload in track hook + */ + + /** + * @typedef {HookContextCommon & TrackContextProps} TrackContext Context passed to track hook + */ + + /** + * @typedef {Function} TrackHook + * @param {TrackContext} context Context passed to track hook + */ + +// Hooks - Identify + +/** + * @typedef {Object} IdentifyPayloadProps + * @property {String} anonymousId + * @property {String} userId + * @property {ResetPayloadProps} meta + * @property {Object} traits + * @property {Object} options + */ + + /** + * @typedef {PayloadBase & IdentifyPayloadProps} IdentifyPayload Payload in identify hook + */ + + /** + * @typedef {Object} IdentifyContextProps + * @property {IdentifyPayload} payload Payload in identify hook + */ + + /** + * @typedef {HookContextCommon & IdentifyContextProps} IdentifyContext Context passed to identify hook + */ + + /** + * @typedef {Function} IdentifyHook + * @param {IdentifyContext} context Context passed to identify hook + */ + +// Hooks - UserIdChanged + +/** + * @typedef {Object} UserIdChangedState + * @property {String} userId + * @property {Object} traits + */ + + /** + * @typedef {Object} UserIdChangedPayloadProps + * @property {UserIdChangedState} new + * @property {UserIdChangedState} old + * @property {Object} options + */ + + /** + * @typedef {PayloadBase & UserIdChangedPayloadProps} UserIdChangedPayload Payload in userIdChanged hook + */ + + /** + * @typedef {Object} UserIdChangedContextProps + * @property {UserIdChangedPayload} payload Payload in userIdChanged hook + */ + + /** + * @typedef {HookContextCommon & UserIdChangedContextProps} UserIdChangedContext Context passed to userIdChanged hook + */ + + /** + * @typedef {Function} UserIdChangedHook + * @param {UserIdChangedContext} context Context passed to userIdChanged hook + */ + +// Hooks - RegisterPlugins + +/** + * @typedef {Object} RegisterPluginsPayloadProps + * @property {Array.} plugins + */ + + /** + * @typedef {PayloadBase & RegisterPluginsPayloadProps} RegisterPluginsPayload Payload in RegisterPlugins hook + */ + + /** + * @typedef {Object} RegisterPluginsContextProps + * @property {RegisterPluginsPayload} payload Payload in registerPlugins hook + */ + + /** + * @typedef {HookContextCommon & RegisterPluginsContextProps} RegisterPluginsContext Context passed to registerPlugins hook + */ + + /** + * @typedef {Function} RegisterPluginsHook + * @param {RegisterPluginsContext} context Context passed to registerPlugins hook + */ + +// Hooks - EnablePlugin + +/** + * @typedef {Object} EnablePluginPayloadProps + * @property {Function} [callback] + * @property {String} name + */ + + /** + * @typedef {PayloadBase & EnablePluginPayloadProps} EnablePluginPayload Payload in enablePlugin hook + */ + + /** + * @typedef {Object} EnablePluginContextProps + * @property {EnablePluginPayload} payload Payload in enablePlugin hook + */ + + /** + * @typedef {HookContextCommon & EnablePluginContextProps} EnablePluginContext Context passed to enablePlugin hook + */ + + /** + * @typedef {Function} EnablePluginHook + * @param {EnablePluginContext} context Context passed to enablePlugin hook + */ + +// Hooks - RemoveItem + +/** + * @typedef {Object} RemoveItemPayloadProps + * @property {String} key + * @property {Number} timestamp + * @property {Object} [options] + */ + + /** + * @typedef {PayloadBase & RemoveItemPayloadProps} RemoveItemPayload Payload in removeItem hook + */ + + /** + * @typedef {Object} RemoveItemContextProps + * @property {RemoveItemPayload} payload Payload in removeItem hook + */ + + /** + * @typedef {HookContextCommon & RemoveItemContextProps} RemoveItemContext Context passed to removeItem hook + */ + + /** + * @typedef {Function} RemoveItemHook + * @param {RemoveItemContext} context Context passed to removeItem hook + */ + +// Hooks - SetItem + +/** + * @typedef {Object} SetItemPayloadProps + * @property {*} [value] + */ + + /** + * @typedef {PayloadBase & RemoveItemPayloadProps & SetItemPayloadProps} SetItemPayload Payload in setItem hook + */ + + /** + * @typedef {Object} SetItemContextProps + * @property {SetItemPayload} payload Payload in setItem hook + */ + + /** + * @typedef {HookContextCommon & SetItemContextProps} SetItemContext Context passed to setItem hook + */ + + /** + * @typedef {Function} SetItemHook + * @param {SetItemContext} context Context passed to setItem hook + */ From bfc6ddb8ca59540fc295f87df64d719c4220c767 Mon Sep 17 00:00:00 2001 From: Juro Oravec Date: Sat, 14 Nov 2020 17:44:30 +0000 Subject: [PATCH 02/14] types(core): annotate payloads --- packages/analytics-core/src/index.js | 81 +++++++++++++------ .../analytics-core/src/middleware/identify.js | 6 +- .../src/middleware/initialize.js | 24 +++--- .../src/middleware/plugins/index.js | 7 +- .../analytics-core/src/middleware/storage.js | 16 ++-- .../analytics-core/src/modules/plugins.js | 2 + 6 files changed, 92 insertions(+), 44 deletions(-) diff --git a/packages/analytics-core/src/index.js b/packages/analytics-core/src/index.js index d2e38219..4ec35bdf 100644 --- a/packages/analytics-core/src/index.js +++ b/packages/analytics-core/src/index.js @@ -30,16 +30,21 @@ import heartBeat from './utils/heartbeat' const { setItem, removeItem } = middleware +/** + * Core Analytic constants. These are exposed for third party plugins & listeners + * @typedef {Object} AnalyticsInstanceConfig + * @property {string} [app] - Name of site / app + * @property {string} [version] - Version of your app + * @property {boolean} [debug] - Should analytics run in debug mode + * @property {Array.} [plugins] - Array of analytics plugins + */ + /** * Analytics library configuration * * After the library is initialized with config, the core API is exposed & ready for use in the application. * - * @param {object} config - analytics core config - * @param {string} [config.app] - Name of site / app - * @param {string} [config.version] - Version of your app - * @param {boolean} [config.debug] - Should analytics run in debug mode - * @param {Array.} [config.plugins] - Array of analytics plugins + * @param {AnalyticsInstanceConfig} [config] - Analytics core config * @return {AnalyticsInstance} Analytics Instance * @example * @@ -193,7 +198,9 @@ function analytics(config = {}) { * analytics.plugins.enable(['google', 'segment']) */ enable: (plugins, callback) => { - store.dispatch(enablePlugin(plugins, callback)) + /** @type {EnablePluginPayload} */ + const enablePluginPayload = enablePlugin(plugins, callback); + store.dispatch(enablePluginPayload); }, /** * Disable analytics plugin @@ -207,7 +214,9 @@ function analytics(config = {}) { * analytics.plugins.disable(['google', 'segment']) */ disable: (name, callback) => { - store.dispatch(disablePlugin(name, callback)) + /** @type {EnablePluginPayload} */ + const disablePluginPayload = disablePlugin(name, callback); + store.dispatch(disablePluginPayload); }, /* * Load registered analytic providers. @@ -217,11 +226,13 @@ function analytics(config = {}) { * analytics.plugins.load('segment') */ load: (plugins) => { - store.dispatch({ + /** @type {EnablePluginPayload} */ + const loadPluginPayload = { type: EVENTS.loadPlugin, // Todo handle multiple plugins via array plugins: (plugins) ? [plugins] : Object.keys(getPlugins()), - }) + }; + store.dispatch(loadPluginPayload); }, /* @TODO if it stays, state loaded needs to be set. Re PLUGIN_INIT above add: (newPlugin) => { @@ -312,7 +323,8 @@ function analytics(config = {}) { const resolvedId = id || data.userId || getUserProp(ID, instance, data) return new Promise((resolve, reject) => { - store.dispatch({ + /** @type {IdentifyPayload} */ + const identifyPayload = { type: EVENTS.identifyStart, userId: resolvedId, traits: data || {}, @@ -324,7 +336,8 @@ function analytics(config = {}) { timestamp: timestamp(), callback: resolvePromise(resolve, getCallback(traits, options, callback)) }, - }) + }; + store.dispatch(identifyPayload); }) }, /** @@ -384,7 +397,8 @@ function analytics(config = {}) { const opts = isObject(options) ? options : {} return new Promise((resolve, reject) => { - store.dispatch({ + /** @type {TrackPayload} */ + const trackPayload = { type: EVENTS.trackStart, event: name, properties: data, @@ -395,7 +409,8 @@ function analytics(config = {}) { timestamp: timestamp(), callback: resolvePromise(resolve, getCallback(payload, options, callback)) }, - }) + }; + store.dispatch(trackPayload); }) }, /** @@ -444,7 +459,8 @@ function analytics(config = {}) { const opts = isObject(options) ? options : {} return new Promise((resolve, reject) => { - store.dispatch({ + /** @type {PagePayload} */ + const pagePayload = { type: EVENTS.pageStart, properties: getPageData(d), options: opts, @@ -454,7 +470,8 @@ function analytics(config = {}) { timestamp: timestamp(), callback: resolvePromise(resolve, getCallback(data, options, callback)) }, - }) + }; + store.dispatch(pagePayload); }) }, /** @@ -497,11 +514,13 @@ function analytics(config = {}) { */ reset: (callback) => { return new Promise((resolve, reject) => { - store.dispatch({ + /** @type {ResetPayload} */ + const resetPayload = { type: EVENTS.resetStart, timestamp: timestamp(), callback: resolvePromise(resolve, callback) - }) + }; + store.dispatch(resetPayload); }) }, /** @@ -876,15 +895,25 @@ function analytics(config = {}) { }) /* Register analytic plugins */ - store.dispatch({ + /** @type {RegisterPluginsPayload} */ + const registerPluginsPayload = { type: EVENTS.registerPlugins, plugins: pluginKeys, - }) + }; + store.dispatch(registerPluginsPayload); parsedOptions.pluginsArray.map((plugin, i) => { // eslint-disable-line const { bootstrap, config } = plugin if (bootstrap && isFunction(bootstrap)) { - bootstrap({ instance, config, payload: plugin }) + /** @type {BootstrapContext} */ + const bootstrapContext = { + hello: plugin.name, + instance, + config, + payload: plugin, + }; + // Call EVENTS.bootstrap + bootstrap(bootstrapContext); } const lastCall = parsedOptions.pluginsArray.length === (i + 1) /* Register plugins */ @@ -896,19 +925,23 @@ function analytics(config = {}) { /* All plugins registered initialize */ if (lastCall) { - store.dispatch({ + /** @type {InitializePayload} */ + const initializeStartPayload = { type: EVENTS.initializeStart, plugins: pluginKeys - }) + } + store.dispatch(initializeStartPayload); } }) if (process.browser) { /* Watch for network events */ watch(offline => { - store.dispatch({ + /** @type {EmptyPayload} */ + const networkPayload = { type: (offline) ? EVENTS.offline : EVENTS.online, - }) + }; + store.dispatch(networkPayload); }) /* Tick heartbeat for queued events */ diff --git a/packages/analytics-core/src/middleware/identify.js b/packages/analytics-core/src/middleware/identify.js index da3a51d2..a3182a33 100644 --- a/packages/analytics-core/src/middleware/identify.js +++ b/packages/analytics-core/src/middleware/identify.js @@ -29,7 +29,8 @@ export default function identifyMiddleware(instance) { const currentTraits = getItem(USER_TRAITS) || {} if (currentId && (currentId !== userId)) { - store.dispatch({ + /** @type {UserIdChangedPayload} */ + const userIdChangedPayload = { type: EVENTS.userIdChanged, old: { userId: currentId, @@ -40,7 +41,8 @@ export default function identifyMiddleware(instance) { traits }, options: options, - }) + }; + store.dispatch(userIdChangedPayload); } /* Save user id */ diff --git a/packages/analytics-core/src/middleware/initialize.js b/packages/analytics-core/src/middleware/initialize.js index 0746df41..bcbcbbce 100644 --- a/packages/analytics-core/src/middleware/initialize.js +++ b/packages/analytics-core/src/middleware/initialize.js @@ -21,7 +21,7 @@ export default function initializeMiddleware(instance) { const paramsArray = Object.keys(action.params) if (paramsArray.length) { const { an_uid, an_event } = params - const groupedParams = paramsArray.reduce((acc, key) => { + const { campaign, props, traits } = paramsArray.reduce((acc, key) => { // match utm params & dclid (display) & gclid (cpc) if (key.match(utmRegex) || key.match(/^(d|g)clid/)) { const cleanName = key.replace(utmRegex, '') @@ -41,31 +41,37 @@ export default function initializeMiddleware(instance) { traits: {} }) - store.dispatch({ + /** @type {ParamsPayload} */ + const paramsPayload = { type: EVENTS.params, raw: params, - ...groupedParams, + campaign, + props, + traits, ...(an_uid ? { userId: an_uid } : {}), - }) + }; + store.dispatch(paramsPayload); /* If userId set, call identify */ if (an_uid) { // timeout to debounce and make sure integration is registered. Todo refactor - setTimeout(() => instance.identify(an_uid, groupedParams.traits), 0) + setTimeout(() => instance.identify(an_uid, traits), 0) } /* If tracking event set, call track */ if (an_event) { // timeout to debounce and make sure integration is registered. Todo refactor - setTimeout(() => instance.track(an_event, groupedParams.props), 0) + setTimeout(() => instance.track(an_event, props), 0) } // if url has utm params if (Object.keys(groupedParams.campaign).length) { - store.dispatch({ + /** @type {CampaignPayload} */ + const campaignPayload = { type: EVENTS.campaign, - campaign: groupedParams.campaign - }) + campaign, + }; + store.dispatch(campaignPayload); } } } diff --git a/packages/analytics-core/src/middleware/plugins/index.js b/packages/analytics-core/src/middleware/plugins/index.js index 0d3b6042..6da3f576 100644 --- a/packages/analytics-core/src/middleware/plugins/index.js +++ b/packages/analytics-core/src/middleware/plugins/index.js @@ -30,6 +30,7 @@ export default function pluginMiddleware(instance, getPlugins, systemEvents) { acc[curr] = allPlugins[curr] return acc }, {}) + /** @type {InitializePayload} */ const initializeAction = { type: EVENTS.initializeStart, plugins: action.plugins @@ -82,11 +83,13 @@ export default function pluginMiddleware(instance, getPlugins, systemEvents) { // setTimeout to ensure runs after 'page' setTimeout(() => { if (pluginsArray.length === allCalls.length) { - store.dispatch({ + /** @type {ReadyPayload} */ + const readyPayload = { type: EVENTS.ready, plugins: completed, failed: failed - }) + }; + store.dispatch(readyPayload); } }, 0) }) diff --git a/packages/analytics-core/src/middleware/storage.js b/packages/analytics-core/src/middleware/storage.js index 90ee087c..b17dad57 100644 --- a/packages/analytics-core/src/middleware/storage.js +++ b/packages/analytics-core/src/middleware/storage.js @@ -19,22 +19,24 @@ export default function storageMiddleware(storage) { } } -export const setItem = (key, value, opts) => { +export const setItem = (key, value, options) => { + /** @type {SetItemPayload} */ return { type: EVENTS.setItemStart, timestamp: timestamp(), - key: key, - value: value, - options: opts, + key, + value, + options, } } -export const removeItem = (key, opts) => { +export const removeItem = (key, options) => { + /** @type {RemoveItemPayload} */ return { type: EVENTS.removeItemStart, timestamp: timestamp(), - key: key, - options: opts, + key, + options, } } diff --git a/packages/analytics-core/src/modules/plugins.js b/packages/analytics-core/src/modules/plugins.js index 1baba047..e409090b 100644 --- a/packages/analytics-core/src/modules/plugins.js +++ b/packages/analytics-core/src/modules/plugins.js @@ -97,6 +97,7 @@ export default function createReducer(getPlugins) { } export const enablePlugin = (name, callback) => { + /** @type {EnablePluginPayload} */ return { type: EVENTS.enablePlugin, name: name, @@ -108,6 +109,7 @@ export const enablePlugin = (name, callback) => { } export const disablePlugin = (name, callback) => { + /** @type {EnablePluginPayload} */ return { type: EVENTS.disablePlugin, name: name, From 676943c9ab01f397cda79c8bc1d9099363d42197 Mon Sep 17 00:00:00 2001 From: Juro Oravec Date: Sat, 14 Nov 2020 17:44:52 +0000 Subject: [PATCH 03/14] docs(core): fix typo --- packages/analytics-core/src/events.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/analytics-core/src/events.js b/packages/analytics-core/src/events.js index b6169ca5..71971630 100644 --- a/packages/analytics-core/src/events.js +++ b/packages/analytics-core/src/events.js @@ -106,7 +106,7 @@ export const coreEvents = [ */ 'identifyEnd', /** - * `identifyAborted` - Fires if 'track' call is cancelled by a plugin + * `identifyAborted` - Fires if 'identify' call is cancelled by a plugin */ 'identifyAborted', /** From ffb8ded7825df61243b4425f78ad166b7192c2ec Mon Sep 17 00:00:00 2001 From: Juro Oravec Date: Sat, 14 Nov 2020 17:45:44 +0000 Subject: [PATCH 04/14] refactor(core): add jsdoc-plugin-intersection to jsdoc --- jsdoc.config.js | 5 +++++ package.json | 1 + packages/analytics-core/package.json | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 jsdoc.config.js diff --git a/jsdoc.config.js b/jsdoc.config.js new file mode 100644 index 00000000..e20ba8d8 --- /dev/null +++ b/jsdoc.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: [ + 'jsdoc-plugin-intersection' + ], +} diff --git a/package.json b/package.json index ccb04847..27d844aa 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "dox": "^0.9.0", "indent-string": "^4.0.0", "jsdoc": "^3.6.3", + "jsdoc-plugin-intersection": "^1.0.4", "lerna": "^3.10.7", "markdown-magic": "^0.1.25", "mkdirp": "^0.5.1", diff --git a/packages/analytics-core/package.json b/packages/analytics-core/package.json index db65c92b..97d08162 100644 --- a/packages/analytics-core/package.json +++ b/packages/analytics-core/package.json @@ -26,7 +26,7 @@ "test:watch": "ava -v --watch", "clean": "rimraf lib dist && mkdirp lib dist", "prebuild": "npm run clean && npm run types", - "types": "../../node_modules/.bin/jsdoc -t ../../node_modules/tsd-jsdoc/dist -r ./src/ -d temp-types && node scripts/types.js", + "types": "../../node_modules/.bin/jsdoc -c ../../jsdoc.config.js -t ../../node_modules/tsd-jsdoc/dist -r ./src/ -d temp-types && node scripts/types.js", "build": "node ../../scripts/build/index.js", "postbuild": "npm run minify-dist && node ./scripts/postbuild.js", "watch": "node ../../scripts/build/_watch.js", From a8af86102542c1a298a87503088fc95d5f04343d Mon Sep 17 00:00:00 2001 From: Juro Oravec Date: Sat, 14 Nov 2020 17:46:17 +0000 Subject: [PATCH 05/14] types(core): export and extend types in types.d.ts --- packages/analytics-core/scripts/types.js | 46 +++++++++++++++++++++--- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/packages/analytics-core/scripts/types.js b/packages/analytics-core/scripts/types.js index c11a6e97..6501a353 100644 --- a/packages/analytics-core/scripts/types.js +++ b/packages/analytics-core/scripts/types.js @@ -11,17 +11,55 @@ const content = fs.readFileSync(TYPES_PATH, 'utf-8') const typesFromJsDocs = content // Remove declares .replace(/^declare\s/gm, '') - // Export analytics instance - .replace(/type AnalyticsInstance =/gm, 'export type AnalyticsInstance =') + // Export user-facing types (and where possible, convert objects to interface so they can be extended) + .replace(/type AnalyticsInstance =/gm, 'export interface AnalyticsInstance ') + .replace(/type AnalyticsInstanceConfig =/gm, 'export interface AnalyticsInstanceConfig ') + .replace(/type (\w+)Payload =/gm, match => `export ${match}`) + // Exported so user doesn't have to do ReturnType to get them + .replace(/type DetachListeners =/gm, 'export type DetachListeners =') + + // Convert following types to generics to be able to pass payload type throught them + // Export hooks and convert them to generics accepting payload type + .replace( + /type (\w+)Hook =(.*)Context/gm, + (_, typeName, argsDefStart) => `export type ${typeName}Hook =${argsDefStart}Context`, + ) + // Convert contexts to generics accepting payload type + .replace( + /type (\w+)Context =(.*)ContextProps(\W)/gm, + (_, typeName, typeDefStart, typeDefEnd) => `type ${typeName}Context =${typeDefStart}ContextProps${typeDefEnd}`, + ) + // Convert context props types to generics accepting payload type + .replace( + /type (\w+)ContextProps =((.|\s)*?)payload: \w+/gm, + (_, typeName, typeDefStart) => `type ${typeName}ContextProps =${typeDefStart}payload: T`, + ) + + // Rename following types so we can set generics in their place + .replace(/type AnalyticsPlugin = /gm, 'interface AnalyticsPluginBase ') + .replace(/type PageData = /gm, 'interface PageDataBase ') // Make promises return void .replace(/\@returns \{Promise\}/gm, '@returns {Promise}') .replace(/=> Promise;/gm, '=> Promise;') - // Fix plugin interface - .replace(/plugins\?: object\[\]/gm, 'plugins?: Array') + // Convert unions ('|') to joins ('&'). + // Joins are used for modular JSDOC typedefs that support intellisense in VS Code. + // 'jsdoc' cannot parse joins, so they are temporarily transpiled to unions by 'jsdoc-plugin-intersection'. + .replace(/ \| /gm, ' & ') + +// Make types extensible +const typeExtensions = ` + + export type PageData = PageDataBase & Record; + export type AnalyticsPlugin = AnalyticsPluginBase & string extends T + ? Record + : Record & Record; +`; // Expose main API const newContent = `declare module "analytics" { ${indentString(typesFromJsDocs, 2)} +${typeExtensions} + export const CONSTANTS: constants; export const init: typeof analytics; From b388e1afa68b77fa51ebf33b24aab83e8d961313 Mon Sep 17 00:00:00 2001 From: Juro Oravec Date: Sun, 15 Nov 2020 12:25:53 +0000 Subject: [PATCH 06/14] feat(mixpanel): Add `context` mixpanel plugin config option - Move the mixpanel load script to separate file - Modify the load script to be able to specify which context object the mixpanel instance should be loaded. Defaults to `window` for backward compatibility - Add `context` mixpanel plugin config option that specifies the context that's passed the refactored load script - Update hooks to get mixpanel instance from resolveMixpanel function instead of from `window`. This decouples the use of mixpanel instance from the window object --- .../analytics-plugin-mixpanel/src/browser.js | 168 +++++++----------- .../src/browserLoadMixpanel.js | 111 ++++++++++++ 2 files changed, 173 insertions(+), 106 deletions(-) create mode 100644 packages/analytics-plugin-mixpanel/src/browserLoadMixpanel.js diff --git a/packages/analytics-plugin-mixpanel/src/browser.js b/packages/analytics-plugin-mixpanel/src/browser.js index f147741c..df571a82 100644 --- a/packages/analytics-plugin-mixpanel/src/browser.js +++ b/packages/analytics-plugin-mixpanel/src/browser.js @@ -1,122 +1,70 @@ +import browserLoadMixpanel from './browserLoadMixpanel'; + +/** + * @typedef {Object} MixpanelPluginConfig - Plugin settings for Mixpanel plugin + * @property {String} [token] - The mixpanel token associated to a mixpanel project + * @property {Object} [context] - The context object where mixpanel + * instance is found and assigned to when instantiated. Defaults to window. + */ + +/** + * Get mixpanel instance from config + * @param {MixpanelPluginConfig} config + * @returns {Object} Mixpanel instance + */ +const resolveMixpanel = (config = {}) => { + const { context = window } = config; + return context.mixpanel; +} + /** * Mixpanel Analytics plugin * @link https://getanalytics.io/plugins/mixpanel/ - * @param {object} pluginConfig - Plugin settings - * @param {string} pluginConfig.token - The mixpanel token associated to a mixpanel project - * @return {object} Analytics plugin + * @param {MixpanelPluginConfig} pluginConfig - Plugin settings + * @return {AnalyticsPlugin} Analytics plugin * @example * + * // Load Mixpanel instance to `window` * mixpanelPlugin({ - * token: 'abcdef123' - * }) + * token: 'abcdef123', + * }); + * + * // Load Mixpanel instance to `context` object instead of `window` + * const myContext = {}; + * mixpanelPlugin({ + * token: 'abcdef123', + * context: myContext, + * }); + * */ function mixpanelPlugin(pluginConfig = {}) { - return { + const plugin = { NAMESPACE: "mixpanel", config: pluginConfig, /* https://developer.mixpanel.com/docs/javascript-full-api-reference#mixpanelinit */ initialize: ({ config }) => { - const { token } = config; - if (!token) { - throw new Error("No mixpanel token defined"); - } + const { + token, + context = window, + } = config || {}; + + let mixpanel = resolveMixpanel(config); - // NoOp if mixpanel already loaded by external source or already loaded - if (typeof window.mixpanel !== 'undefined') { + const hasMixpanel = Boolean(mixpanel); + + if (hasMixpanel) { + // NoOp if mixpanel already loaded by external source or already loaded return; } - // Load mixpanel library - (function(c, a) { - if (!a.__SV) { - var b = window; - try { - var d, - m, - j, - k = b.location, - f = k.hash; - d = function(a, b) { - return (m = a.match(RegExp(b + "=([^&]*)"))) ? m[1] : null; - }; - f && - d(f, "state") && - ((j = JSON.parse(decodeURIComponent(d(f, "state")))), - "mpeditor" === j.action && - (b.sessionStorage.setItem("_mpcehash", f), - history.replaceState( - j.desiredHash || "", - c.title, - k.pathname + k.search - ))); - } catch (n) {} - var l, h; - window.mixpanel = a; - a._i = []; - a.init = function(b, d, g) { - function c(b, i) { - var a = i.split("."); - 2 == a.length && ((b = b[a[0]]), (i = a[1])); - b[i] = function() { - b.push([i].concat(Array.prototype.slice.call(arguments, 0))); - }; - } - var e = a; - "undefined" !== typeof g ? (e = a[g] = []) : (g = "mixpanel"); - e.people = e.people || []; - e.toString = function(b) { - var a = "mixpanel"; - "mixpanel" !== g && (a += "." + g); - b || (a += " (stub)"); - return a; - }; - e.people.toString = function() { - return e.toString(1) + ".people (stub)"; - }; - l = "disable time_event track track_pageview track_links track_forms track_with_groups add_group set_group remove_group register register_once alias unregister identify name_tag set_config reset opt_in_tracking opt_out_tracking has_opted_in_tracking has_opted_out_tracking clear_opt_in_out_tracking people.set people.set_once people.unset people.increment people.append people.union people.track_charge people.clear_charges people.delete_user people.remove".split( - " " - ); - for (h = 0; h < l.length; h++) c(e, l[h]); - var f = "set set_once union unset remove delete".split(" "); - e.get_group = function() { - function a(c) { - b[c] = function() { - call2_args = arguments; - call2 = [c].concat(Array.prototype.slice.call(call2_args, 0)); - e.push([d, call2]); - }; - } - for ( - var b = {}, - d = ["get_group"].concat( - Array.prototype.slice.call(arguments, 0) - ), - c = 0; - c < f.length; - c++ - ) - a(f[c]); - return b; - }; - a._i.push([b, d, g]); - }; - a.__SV = 1.2; - b = c.createElement("script"); - b.type = "text/javascript"; - b.async = !0; - b.src = - "undefined" !== typeof MIXPANEL_CUSTOM_LIB_URL - ? MIXPANEL_CUSTOM_LIB_URL - : "file:" === c.location.protocol && - "//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//) - ? "https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js" - : "//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js"; - d = c.getElementsByTagName("script")[0]; - d.parentNode.insertBefore(b, d); + if (!hasMixpanel) { + if (!token) { + throw new Error("No mixpanel token defined"); } - })(document, window.mixpanel || []); - mixpanel.init(config.token, { batch_requests: true }); + mixpanel = browserLoadMixpanel(context); + mixpanel.init(token, { batch_requests: true }); + } }, /** * Identify a visitor in mixpanel @@ -125,8 +73,10 @@ function mixpanelPlugin(pluginConfig = {}) { * Mixpanel doesn't allow to set properties directly in identify, so mixpanel.people.set is * also called if properties are passed */ - identify: ({ payload }) => { + identify: ({ config, payload }) => { + const mixpanel = resolveMixpanel(config); const { userId, traits } = payload; + if (typeof userId === "string") { mixpanel.identify(userId); } @@ -138,20 +88,24 @@ function mixpanelPlugin(pluginConfig = {}) { * Mixpanel doesn't have a "page" function, so we are using the track method by sending * the path as tracked event and search parameters as properties */ - page: ({ payload }) => { + page: ({ config, payload }) => { + const mixpanel = resolveMixpanel(config); mixpanel.track(payload.properties.path, { search: payload.properties.search, }); }, /* https://developer.mixpanel.com/docs/javascript-full-api-reference#mixpaneltrack */ - track: ({ payload }) => { + track: ({ config, payload }) => { + const mixpanel = resolveMixpanel(config); mixpanel.track(payload.event, payload.properties); }, loaded: () => { - return !!window.mixpanel; + const mixpanel = resolveMixpanel(plugin.config); + return !!mixpanel; }, /* Clears super properties and generates a new random distinct_id for this instance. Useful for clearing data when a user logs out. */ - reset: () => { + reset: ({ config }) => { + const mixpanel = resolveMixpanel(config); mixpanel.reset(); }, /* Custom methods to add .alias call */ @@ -164,10 +118,12 @@ function mixpanelPlugin(pluginConfig = {}) { * @param {string} [original] - The current identifier being used for this user. */ alias(alias, original) { + const mixpanel = resolveMixpanel(pluginConfig); mixpanel.alias(alias, original); }, }, }; + return plugin; } export default mixpanelPlugin; diff --git a/packages/analytics-plugin-mixpanel/src/browserLoadMixpanel.js b/packages/analytics-plugin-mixpanel/src/browserLoadMixpanel.js new file mode 100644 index 00000000..965444f3 --- /dev/null +++ b/packages/analytics-plugin-mixpanel/src/browserLoadMixpanel.js @@ -0,0 +1,111 @@ +/** + * Try to load mixpanel using initialization script + * + * @param {*} [ctx] Context object where the mixpanel instance should be set + * @returns {Object|undefined} Mixpanel instance + */ +const loadMixpanelByScript = (ctx = window) => { + // Load mixpanel library + (function(c, a) { + if (!a.__SV) { + var b = ctx; + try { + var d, + m, + j, + k = b.location, + f = k.hash; + d = function(a, b) { + return (m = a.match(RegExp(b + "=([^&]*)"))) ? m[1] : null; + }; + f && + d(f, "state") && + ((j = JSON.parse(decodeURIComponent(d(f, "state")))), + "mpeditor" === j.action && + (b.sessionStorage.setItem("_mpcehash", f), + history.replaceState( + j.desiredHash || "", + c.title, + k.pathname + k.search + ))); + } catch (n) {} + var l, h; + ctx.mixpanel = a; + a._i = []; + a.init = function(b, d, g) { + function c(b, i) { + var a = i.split("."); + 2 == a.length && ((b = b[a[0]]), (i = a[1])); + b[i] = function() { + b.push([i].concat(Array.prototype.slice.call(arguments, 0))); + }; + } + var e = a; + "undefined" !== typeof g ? (e = a[g] = []) : (g = "mixpanel"); + e.people = e.people || []; + e.toString = function(b) { + var a = "mixpanel"; + "mixpanel" !== g && (a += "." + g); + b || (a += " (stub)"); + return a; + }; + e.people.toString = function() { + return e.toString(1) + ".people (stub)"; + }; + l = "disable time_event track track_pageview track_links track_forms track_with_groups add_group set_group remove_group register register_once alias unregister identify name_tag set_config reset opt_in_tracking opt_out_tracking has_opted_in_tracking has_opted_out_tracking clear_opt_in_out_tracking people.set people.set_once people.unset people.increment people.append people.union people.track_charge people.clear_charges people.delete_user people.remove".split( + " " + ); + for (h = 0; h < l.length; h++) c(e, l[h]); + var f = "set set_once union unset remove delete".split(" "); + e.get_group = function() { + function a(c) { + b[c] = function() { + call2_args = arguments; + call2 = [c].concat(Array.prototype.slice.call(call2_args, 0)); + e.push([d, call2]); + }; + } + for ( + var b = {}, + d = ["get_group"].concat( + Array.prototype.slice.call(arguments, 0) + ), + c = 0; + c < f.length; + c++ + ) + a(f[c]); + return b; + }; + a._i.push([b, d, g]); + }; + a.__SV = 1.2; + b = c.createElement("script"); + b.type = "text/javascript"; + b.async = !0; + b.src = + "undefined" !== typeof ctx.MIXPANEL_CUSTOM_LIB_URL + ? ctx.MIXPANEL_CUSTOM_LIB_URL + : "file:" === c.location.protocol && + "//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//) + ? "https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js" + : "//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js"; + d = c.getElementsByTagName("script")[0]; + d.parentNode.insertBefore(b, d); + } + })(document, ctx.mixpanel || []); + + return ctx.mixpanel; +}; + +/** + * Try to load mixpanel using either npm package import or initialization script + * + * @param {*} [ctx] Context object where the mixpanel instance should be set + * @returns {Object|undefined} Mixpanel instance + */ +const loadMixpanel = (ctx) => { + return loadMixpanelByScript(ctx); +}; + +export { loadMixpanel as default, loadMixpanelByScript }; From f4e311beb40db84293abf389a3c1d6d2917af74c Mon Sep 17 00:00:00 2001 From: Juro Oravec Date: Sun, 15 Nov 2020 12:28:10 +0000 Subject: [PATCH 07/14] feat(mixpanel): Add `mixpanel` mixpanel plugin config option - Add option to be able to pass a mixpanel instance directly to mixpanel plugin instead of fetching it from mixpanel CDN --- packages/analytics-plugin-mixpanel/src/browser.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/analytics-plugin-mixpanel/src/browser.js b/packages/analytics-plugin-mixpanel/src/browser.js index df571a82..d18a6996 100644 --- a/packages/analytics-plugin-mixpanel/src/browser.js +++ b/packages/analytics-plugin-mixpanel/src/browser.js @@ -3,6 +3,9 @@ import browserLoadMixpanel from './browserLoadMixpanel'; /** * @typedef {Object} MixpanelPluginConfig - Plugin settings for Mixpanel plugin * @property {String} [token] - The mixpanel token associated to a mixpanel project + * @property {Object} [mixpanel] - The mixpanel instance - use this + * to use the plugin with a mixpanel instance instantiated elsewhere, for + * example when using a mixpanel npm package. * @property {Object} [context] - The context object where mixpanel * instance is found and assigned to when instantiated. Defaults to window. */ @@ -13,8 +16,8 @@ import browserLoadMixpanel from './browserLoadMixpanel'; * @returns {Object} Mixpanel instance */ const resolveMixpanel = (config = {}) => { - const { context = window } = config; - return context.mixpanel; + const { context = window, mixpanel: givenMixpanel } = config; + return givenMixpanel || context.mixpanel; } /** @@ -36,6 +39,11 @@ const resolveMixpanel = (config = {}) => { * context: myContext, * }); * + * // Use existing mixpanel instance + * import mixpanel from 'mixpanel-browser'; + * mixpanelPlugin({ + * mixpanel: mixpanel.init('abcdef123'), + * }); */ function mixpanelPlugin(pluginConfig = {}) { const plugin = { From 02a3ff21684ee269e6bf21bbeb39b292b4fedfe9 Mon Sep 17 00:00:00 2001 From: Juro Oravec Date: Sun, 15 Nov 2020 12:30:07 +0000 Subject: [PATCH 08/14] feat(mixpanel): Add `config` mixpanel plugin config option - Add option to pass Mixpanel config - The config is passed to mixpanel.init if the instance needs to be instantiated - The config is passed to mixpanel.set_config if the instance already exists --- .../analytics-plugin-mixpanel/src/browser.js | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/analytics-plugin-mixpanel/src/browser.js b/packages/analytics-plugin-mixpanel/src/browser.js index d18a6996..c9bbd27c 100644 --- a/packages/analytics-plugin-mixpanel/src/browser.js +++ b/packages/analytics-plugin-mixpanel/src/browser.js @@ -8,6 +8,10 @@ import browserLoadMixpanel from './browserLoadMixpanel'; * example when using a mixpanel npm package. * @property {Object} [context] - The context object where mixpanel * instance is found and assigned to when instantiated. Defaults to window. + * @property {Object} [config] - Mixpanel config passed to `mixpanel.init()` + * in `initialize` step. + * If mixpanel instance already exists, it is updated with this config + * using `mixpanel.set_config()`. */ /** @@ -44,6 +48,14 @@ const resolveMixpanel = (config = {}) => { * mixpanelPlugin({ * mixpanel: mixpanel.init('abcdef123'), * }); + * + * // Pass mixpanel config + * mixpanelPlugin({ + * token: 'abcdef123', + * config: { + * api_host: 'https://api-eu.mixpanel.com', + * }, + * }); */ function mixpanelPlugin(pluginConfig = {}) { const plugin = { @@ -54,15 +66,19 @@ function mixpanelPlugin(pluginConfig = {}) { const { token, context = window, + config: mixpanelConfig, } = config || {}; let mixpanel = resolveMixpanel(config); const hasMixpanel = Boolean(mixpanel); + const shouldApplyConfig = Boolean(mixpanelConfig); if (hasMixpanel) { // NoOp if mixpanel already loaded by external source or already loaded - return; + if (!shouldApplyConfig) return; + // Update mixpanel to config passed to the plugin + mixpanel.set_config(mixpanelConfig); } if (!hasMixpanel) { @@ -71,7 +87,7 @@ function mixpanelPlugin(pluginConfig = {}) { } mixpanel = browserLoadMixpanel(context); - mixpanel.init(token, { batch_requests: true }); + mixpanel.init(token, { batch_requests: true, ...mixpanelConfig }); } }, /** From be8c56e60f06a8127194b3e955a65b0e2889ff17 Mon Sep 17 00:00:00 2001 From: Juro Oravec Date: Sun, 15 Nov 2020 12:33:27 +0000 Subject: [PATCH 09/14] feat(mixpanel): Add `pageEvent` mixpanel plugin config option - Add option `pageEvent`, which allows to specify the name of the event that's tracked when mixpanelPlugin.page is called. Defautlts to 'page' --- packages/analytics-plugin-mixpanel/src/browser.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/analytics-plugin-mixpanel/src/browser.js b/packages/analytics-plugin-mixpanel/src/browser.js index c9bbd27c..0587700f 100644 --- a/packages/analytics-plugin-mixpanel/src/browser.js +++ b/packages/analytics-plugin-mixpanel/src/browser.js @@ -12,6 +12,9 @@ import browserLoadMixpanel from './browserLoadMixpanel'; * in `initialize` step. * If mixpanel instance already exists, it is updated with this config * using `mixpanel.set_config()`. + * @property {String} [pageEvent] - Mixpanel doesn't have a 'page' + * function, so we are using the `mixpanel.track()` method. Use this option + * to override the event name. Defaults to `'page'`. */ /** @@ -109,12 +112,12 @@ function mixpanelPlugin(pluginConfig = {}) { } }, /** - * Mixpanel doesn't have a "page" function, so we are using the track method by sending - * the path as tracked event and search parameters as properties + * Mixpanel doesn't have a "page" function, so we are using the track method. */ page: ({ config, payload }) => { const mixpanel = resolveMixpanel(config); - mixpanel.track(payload.properties.path, { + const pageEvent = config.pageEvent || 'page'; + mixpanel.track(pageEvent, { search: payload.properties.search, }); }, From 5feb2ccbd418145a9543526aa606118ebc96edde Mon Sep 17 00:00:00 2001 From: Juro Oravec Date: Sun, 15 Nov 2020 12:34:30 +0000 Subject: [PATCH 10/14] feat(mixpanel): Allow to pass all properties to mixpanel.track on mixpanelPlugin.page --- packages/analytics-plugin-mixpanel/src/browser.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/analytics-plugin-mixpanel/src/browser.js b/packages/analytics-plugin-mixpanel/src/browser.js index 0587700f..749806b4 100644 --- a/packages/analytics-plugin-mixpanel/src/browser.js +++ b/packages/analytics-plugin-mixpanel/src/browser.js @@ -117,9 +117,7 @@ function mixpanelPlugin(pluginConfig = {}) { page: ({ config, payload }) => { const mixpanel = resolveMixpanel(config); const pageEvent = config.pageEvent || 'page'; - mixpanel.track(pageEvent, { - search: payload.properties.search, - }); + mixpanel.track(pageEvent, payload.properties); }, /* https://developer.mixpanel.com/docs/javascript-full-api-reference#mixpaneltrack */ track: ({ config, payload }) => { From 9b7d5c7bcb783ab293d1642ab4ad0263a60e72df Mon Sep 17 00:00:00 2001 From: Juro Oravec Date: Sun, 15 Nov 2020 12:37:04 +0000 Subject: [PATCH 11/14] feat(mixpanel): try to load mixpanel from 'mixpanel-browser' if available - If 'mixpanel-browser' npm package is installed, the @analytics/mixpanel plugin will try to use that if a mixpanel instance doesn't exist yet --- .../analytics-plugin-mixpanel/package.json | 8 +++++++ .../src/browserLoadMixpanel.js | 24 +++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/analytics-plugin-mixpanel/package.json b/packages/analytics-plugin-mixpanel/package.json index 662cb31a..13cdc2b4 100644 --- a/packages/analytics-plugin-mixpanel/package.json +++ b/packages/analytics-plugin-mixpanel/package.json @@ -50,5 +50,13 @@ "devDependencies": { "@babel/core": "^7.2.2", "@babel/preset-env": "^7.3.1" + }, + "peerDependencies": { + "mixpanel-browser": "^2.0.0" + }, + "peerDependenciesMeta": { + "mixpanel-browser": { + "optional": true + } } } diff --git a/packages/analytics-plugin-mixpanel/src/browserLoadMixpanel.js b/packages/analytics-plugin-mixpanel/src/browserLoadMixpanel.js index 965444f3..61b405a9 100644 --- a/packages/analytics-plugin-mixpanel/src/browserLoadMixpanel.js +++ b/packages/analytics-plugin-mixpanel/src/browserLoadMixpanel.js @@ -1,3 +1,23 @@ +/** + * Try to load mixpanel using `mixpanel-browser` package + * + * @param {*} [ctx] Context object where the mixpanel instance should be set + * @returns {Object|undefined} Mixpanel instance + */ +const loadMixpanelByImport = (ctx) => { + let mixpanel; + try { + mixpanel = require('mixpanel-browser'); + } catch (er) { + /* noop */ + } + + if (mixpanel && ctx) { + ctx.mixpanel = mixpanel; + } + return mixpanel; +}; + /** * Try to load mixpanel using initialization script * @@ -105,7 +125,7 @@ const loadMixpanelByScript = (ctx = window) => { * @returns {Object|undefined} Mixpanel instance */ const loadMixpanel = (ctx) => { - return loadMixpanelByScript(ctx); + return loadMixpanelByImport(ctx) || loadMixpanelByScript(ctx); }; -export { loadMixpanel as default, loadMixpanelByScript }; +export { loadMixpanel as default, loadMixpanelByImport, loadMixpanelByScript }; From 96bf4f80a002c7f61af24e872402da75da8e5d09 Mon Sep 17 00:00:00 2001 From: Juro Oravec Date: Sun, 15 Nov 2020 12:38:28 +0000 Subject: [PATCH 12/14] feat(mixpanel): Add 'getMixpanel' plugin method that returns current mixpanel instance --- packages/analytics-plugin-mixpanel/src/browser.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/analytics-plugin-mixpanel/src/browser.js b/packages/analytics-plugin-mixpanel/src/browser.js index 749806b4..0e0bada0 100644 --- a/packages/analytics-plugin-mixpanel/src/browser.js +++ b/packages/analytics-plugin-mixpanel/src/browser.js @@ -146,6 +146,9 @@ function mixpanelPlugin(pluginConfig = {}) { const mixpanel = resolveMixpanel(pluginConfig); mixpanel.alias(alias, original); }, + getMixpanel() { + return resolveMixpanel(pluginConfig); + }, }, }; return plugin; From 84ba9223f02dc03e5f807f13286649dccb703fd3 Mon Sep 17 00:00:00 2001 From: Juro Oravec Date: Sun, 15 Nov 2020 17:11:09 +0000 Subject: [PATCH 13/14] feat(mixpanel): add types annotations --- .../analytics-plugin-mixpanel/src/browser.js | 17 ++++++++++++++--- .../src/browserLoadMixpanel.js | 6 +++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/analytics-plugin-mixpanel/src/browser.js b/packages/analytics-plugin-mixpanel/src/browser.js index 0e0bada0..ea1d0bf9 100644 --- a/packages/analytics-plugin-mixpanel/src/browser.js +++ b/packages/analytics-plugin-mixpanel/src/browser.js @@ -1,14 +1,20 @@ import browserLoadMixpanel from './browserLoadMixpanel'; +// Placeholder for Mixpanel type which is replaced postbuild +/** @typedef {Object} Mixpanel */ + +// Placeholder for MixpanelConfig type which is replaced postbuild +/** @typedef {Object} MixpanelConfig */ + /** * @typedef {Object} MixpanelPluginConfig - Plugin settings for Mixpanel plugin * @property {String} [token] - The mixpanel token associated to a mixpanel project - * @property {Object} [mixpanel] - The mixpanel instance - use this + * @property {Mixpanel} [mixpanel] - The mixpanel instance - use this * to use the plugin with a mixpanel instance instantiated elsewhere, for * example when using a mixpanel npm package. * @property {Object} [context] - The context object where mixpanel * instance is found and assigned to when instantiated. Defaults to window. - * @property {Object} [config] - Mixpanel config passed to `mixpanel.init()` + * @property {MixpanelConfig} [config] - Mixpanel config passed to `mixpanel.init()` * in `initialize` step. * If mixpanel instance already exists, it is updated with this config * using `mixpanel.set_config()`. @@ -20,7 +26,7 @@ import browserLoadMixpanel from './browserLoadMixpanel'; /** * Get mixpanel instance from config * @param {MixpanelPluginConfig} config - * @returns {Object} Mixpanel instance + * @returns {Mixpanel|undefined} Mixpanel instance */ const resolveMixpanel = (config = {}) => { const { context = window, mixpanel: givenMixpanel } = config; @@ -141,11 +147,16 @@ function mixpanelPlugin(pluginConfig = {}) { * * @param {string} [alias] - A unique identifier that you want to use for this user in the future. * @param {string} [original] - The current identifier being used for this user. + * @returns {void} */ alias(alias, original) { const mixpanel = resolveMixpanel(pluginConfig); mixpanel.alias(alias, original); }, + /** + * Get currect Mixpanel instance + * @returns {Mixpanel|undefined} The Mixpanel instance + */ getMixpanel() { return resolveMixpanel(pluginConfig); }, diff --git a/packages/analytics-plugin-mixpanel/src/browserLoadMixpanel.js b/packages/analytics-plugin-mixpanel/src/browserLoadMixpanel.js index 61b405a9..ce4b5534 100644 --- a/packages/analytics-plugin-mixpanel/src/browserLoadMixpanel.js +++ b/packages/analytics-plugin-mixpanel/src/browserLoadMixpanel.js @@ -2,7 +2,7 @@ * Try to load mixpanel using `mixpanel-browser` package * * @param {*} [ctx] Context object where the mixpanel instance should be set - * @returns {Object|undefined} Mixpanel instance + * @returns {Mixpanel|undefined} Mixpanel instance */ const loadMixpanelByImport = (ctx) => { let mixpanel; @@ -22,7 +22,7 @@ const loadMixpanelByImport = (ctx) => { * Try to load mixpanel using initialization script * * @param {*} [ctx] Context object where the mixpanel instance should be set - * @returns {Object|undefined} Mixpanel instance + * @returns {Mixpanel|undefined} Mixpanel instance */ const loadMixpanelByScript = (ctx = window) => { // Load mixpanel library @@ -122,7 +122,7 @@ const loadMixpanelByScript = (ctx = window) => { * Try to load mixpanel using either npm package import or initialization script * * @param {*} [ctx] Context object where the mixpanel instance should be set - * @returns {Object|undefined} Mixpanel instance + * @returns {Mixpanel|undefined} Mixpanel instance */ const loadMixpanel = (ctx) => { return loadMixpanelByImport(ctx) || loadMixpanelByScript(ctx); From 2712604ff90ffd0baf4ab62c7f12267b07b0d823 Mon Sep 17 00:00:00 2001 From: Juro Oravec Date: Sun, 15 Nov 2020 17:13:18 +0000 Subject: [PATCH 14/14] build(mixpanel): add postbuild types generation - Add ./scripts/types script similar to one in @analytics/core. The script postprocesses the generated types and exports plugin config and plugin type. - Add analytics as dependency so types in types.d.ts can import types - Add 'types' npm command to @analytics/mixpanel - Add 'temp-types' to .gitignore --- packages/analytics-plugin-mixpanel/.gitignore | 1 + .../analytics-plugin-mixpanel/package.json | 4 +++ .../scripts/types.js | 36 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 packages/analytics-plugin-mixpanel/scripts/types.js diff --git a/packages/analytics-plugin-mixpanel/.gitignore b/packages/analytics-plugin-mixpanel/.gitignore index 4c29c639..e6fc3842 100644 --- a/packages/analytics-plugin-mixpanel/.gitignore +++ b/packages/analytics-plugin-mixpanel/.gitignore @@ -1,4 +1,5 @@ node_modules +temp-types dist lib diff --git a/packages/analytics-plugin-mixpanel/package.json b/packages/analytics-plugin-mixpanel/package.json index 13cdc2b4..71da3ac6 100644 --- a/packages/analytics-plugin-mixpanel/package.json +++ b/packages/analytics-plugin-mixpanel/package.json @@ -23,6 +23,7 @@ "scripts": { "docs": "node ../analytics-cli/bin/run docs", "build": "node ../../scripts/build/index.js", + "types": "../../node_modules/.bin/jsdoc -t ../../node_modules/tsd-jsdoc/dist -r ./src/ -d temp-types && node ./scripts/types.js", "watch": "node ../../scripts/build/_watch.js", "release:patch": "npm version patch && npm publish", "release:minor": "npm version minor && npm publish", @@ -47,6 +48,9 @@ "type": "git", "url": "git+https://github.com/DavidWells/analytics.git" }, + "dependencies": { + "analytics": "^0.5.0" + }, "devDependencies": { "@babel/core": "^7.2.2", "@babel/preset-env": "^7.3.1" diff --git a/packages/analytics-plugin-mixpanel/scripts/types.js b/packages/analytics-plugin-mixpanel/scripts/types.js new file mode 100644 index 00000000..b1db5f38 --- /dev/null +++ b/packages/analytics-plugin-mixpanel/scripts/types.js @@ -0,0 +1,36 @@ +// https://github.com/englercj/tsd-jsdoc/issues/64#issuecomment-462020832 +const fs = require('fs') +const path = require('path') +const indentString = require('indent-string') +const mkdirp = require('mkdirp') + +const TYPES_PATH = path.resolve(__dirname, '../temp-types/types.d.ts') +const OUTPUT_PATH = path.resolve(__dirname, '../lib/types.d.ts') +const content = fs.readFileSync(TYPES_PATH, 'utf-8') + +const typesFromJsDocs = content + // Remove declares + .replace(/^declare\s/gm, '') + // Export plugin config + .replace(/type MixpanelPluginConfig =/gm, 'export interface MixpanelPluginConfig') + // Remove dummy Mixpanel interfaces + .replace(/type Mixpanel =.+;/gm, '') + .replace(/type MixpanelConfig =.+;/gm, '') + // Make Mixpanel config partial in places where it's assigned to something + .replace(/(:\s*)MixpanelConfig/gm, (_, matchStart) => `${matchStart}Partial`) + +// Expose main API +const newContent = ` +import type { AnalyticsPlugin } from 'analytics'; +import type { Mixpanel, Config as MixpanelConfig } from 'mixpanel-browser'; + +declare module "@analytics/mixpanel" { +${indentString(typesFromJsDocs, 2)} + + export default AnalyticsPlugin; +}` + +mkdirp(path.dirname(OUTPUT_PATH), function (err) { + if (err) console.error(err) + fs.writeFileSync(OUTPUT_PATH, newContent) +})