diff --git a/.github/refs/v0-types.d.ts b/.github/refs/v0-types.d.ts new file mode 100644 index 00000000..89bfcb82 --- /dev/null +++ b/.github/refs/v0-types.d.ts @@ -0,0 +1,500 @@ +declare module "analytics" { + /** + * Core Analytic constants. These are exposed for third party plugins & listeners + * @property ANON_ID - Anonymous visitor Id localstorage key + * @property USER_ID - Visitor Id localstorage key + * @property USER_TRAITS - Visitor traits localstorage key + */ + type constants = { + ANON_ID: ANON_ID; + USER_ID: USER_ID; + USER_TRAITS: USER_TRAITS; + }; + + /** + * Anonymous visitor Id localstorage key + */ + type ANON_ID = string; + + /** + * Visitor Id localstorage key + */ + type USER_ID = string; + + /** + * Visitor traits localstorage key + */ + type USER_TRAITS = string; + + /** + * Analytics library configuration + * + * After the library is initialized with config, the core API is exposed & ready for use in the application. + * @example + * import Analytics from 'analytics' + * import pluginABC from 'analytics-plugin-abc' + * import pluginXYZ from 'analytics-plugin-xyz' + * + * // initialize analytics + * const analytics = Analytics({ + * app: 'my-awesome-app', + * plugins: [ + * pluginABC, + * pluginXYZ + * ] + * }) + * @param config - analytics core config + * @param [config.app] - Name of site / app + * @param [config.version] - Version of your app + * @param [config.debug] - Should analytics run in debug mode + * @param [config.plugins] - Array of analytics plugins + * @returns Analytics Instance + */ + function analytics(config: { + app?: string; + version?: string | number; + debug?: boolean; + plugins?: AnalyticsPlugin[]; + }): AnalyticsInstance; + + /** + * Async Management methods for plugins. + * + * This is also where [custom methods](https://bit.ly/329vFXy) are loaded into the instance. + * @example + * // Enable a plugin by namespace + * analytics.plugins.enable('keenio') + * + * // Disable a plugin by namespace + * analytics.plugins.disable('google-analytics') + * @property enable - Set storage value + * @property disable - Remove storage value + */ + type Plugins = { + enable: EnablePlugin; + disable: DisablePlugin; + }; + + /** + * Enable analytics plugin + * @example + * analytics.plugins.enable('google-analytics').then(() => { + * console.log('do stuff') + * }) + * + * // Enable multiple plugins at once + * analytics.plugins.enable(['google-analytics', 'segment']).then(() => { + * console.log('do stuff') + * }) + * @param plugins - name of plugins(s) to disable + * @param [callback] - callback after enable runs + */ + type EnablePlugin = (plugins: string | string[], callback?: (...params: any[]) => any) => Promise; + + /** + * Disable analytics plugin + * @example + * analytics.plugins.disable('google').then(() => { + * console.log('do stuff') + * }) + * + * analytics.plugins.disable(['google', 'segment']).then(() => { + * console.log('do stuff') + * }) + * @param plugins - name of integration(s) to disable + * @param [callback] - callback after disable runs + */ + type DisablePlugin = (plugins: string | string[], callback?: (...params: any[]) => any) => Promise; + + /** + * Analytic instance returned from initialization + * @property identify - Identify a user + * @property track - Track an analytics event + * @property page - Trigger page view + * @property user - Get user data + * @property reset - Clear information about user & reset analytics + * @property ready - Fire callback on analytics ready event + * @property on - Fire callback on analytics lifecycle events. + * @property once - Fire callback on analytics lifecycle events once. + * @property getState - Get data about user, activity, or context. + * @property storage - storage methods + * @property plugins - plugin methods + */ + export interface AnalyticsInstance { + identify: Identify; + track: Track; + page: Page; + user: User; + reset: Reset; + ready: Ready; + on: On; + once: Once; + getState: GetState; + storage: Storage; + plugins: Plugins; + } + + /** + * Identify a user. This will trigger `identify` calls in any installed plugins and will set user data in localStorage + * @example + * // Basic user id identify + * analytics.identify('xyz-123') + * + * // Identify with additional traits + * analytics.identify('xyz-123', { + * name: 'steve', + * company: 'hello-clicky' + * }) + * + * // Fire callback with 2nd or 3rd argument + * analytics.identify('xyz-123', () => { + * console.log('do this after identify') + * }) + * + * // Disable sending user data to specific analytic tools + * analytics.identify('xyz-123', {}, { + * plugins: { + * // disable sending this identify call to segment + * segment: false + * } + * }) + * + * // Send user data to only to specific analytic tools + * analytics.identify('xyz-123', {}, { + * plugins: { + * // disable this specific identify in all plugins except customerio + * all: false, + * customerio: true + * } + * }) + * @param userId - Unique ID of user + * @param [traits] - Object of user traits + * @param [options] - Options to pass to identify call + * @param [callback] - Callback function after identify completes + */ + type Identify = (userId: string, traits?: any, options?: any, callback?: (...params: any[]) => any) => Promise; + + /** + * Track an analytics event. This will trigger `track` calls in any installed plugins + * @example + * // Basic event tracking + * analytics.track('buttonClicked') + * + * // Event tracking with payload + * analytics.track('itemPurchased', { + * price: 11, + * sku: '1234' + * }) + * + * // Fire callback with 2nd or 3rd argument + * analytics.track('newsletterSubscribed', () => { + * console.log('do this after track') + * }) + * + * // Disable sending this event to specific analytic tools + * analytics.track('cartAbandoned', { + * items: ['xyz', 'abc'] + * }, { + * plugins: { + * // disable track event for segment + * segment: false + * } + * }) + * + * // Send event to only to specific analytic tools + * analytics.track('customerIoOnlyEventExample', { + * price: 11, + * sku: '1234' + * }, { + * plugins: { + * // disable this specific track call all plugins except customerio + * all: false, + * customerio: true + * } + * }) + * @param eventName - Event name + * @param [payload] - Event payload + * @param [options] - Event options + * @param [callback] - Callback to fire after tracking completes + */ + type Track = (eventName: string, payload?: any, options?: any, callback?: (...params: any[]) => any) => Promise; + + /** + * Trigger page view. This will trigger `page` calls in any installed plugins + * @example + * // Basic page tracking + * analytics.page() + * + * // Page tracking with page data overrides + * analytics.page({ + * url: 'https://google.com' + * }) + * + * // Fire callback with 1st, 2nd or 3rd argument + * analytics.page(() => { + * console.log('do this after page') + * }) + * + * // Disable sending this pageview to specific analytic tools + * analytics.page({}, { + * plugins: { + * // disable page tracking event for segment + * segment: false + * } + * }) + * + * // Send pageview to only to specific analytic tools + * analytics.page({}, { + * plugins: { + * // disable this specific page in all plugins except customerio + * all: false, + * customerio: true + * } + * }) + * @param [data] - Page data overrides. + * @param [options] - Page tracking options + * @param [callback] - Callback to fire after page view call completes + */ + type Page = (data?: PageData, options?: any, callback?: (...params: any[]) => any) => Promise; + + /** + * Get user data + * @example + * // Get all user data + * const userData = analytics.user() + * + * // Get user id + * const userId = analytics.user('userId') + * + * // Get user company name + * const companyName = analytics.user('traits.company.name') + * @param [key] - dot.prop.path of user data. Example: 'traits.company.name' + */ + type User = (key?: string) => string & any; + + /** + * Clear all information about the visitor & reset analytic state. + * @example + * // Reset current visitor + * analytics.reset() + * @param [callback] - Handler to run after reset + */ + type Reset = (callback?: (...params: any[]) => any) => Promise; + + /** + * Fire callback on analytics ready event + * @example + * analytics.ready((payload) => { + * console.log('all plugins have loaded or were skipped', payload); + * }) + * @param callback - function to trigger when all providers have loaded + */ + type Ready = (callback: (...params: any[]) => any) => DetachListeners; + + /** + * Attach an event handler function for analytics lifecycle events. + * @example + * // Fire function when 'track' calls happen + * analytics.on('track', ({ payload }) => { + * console.log('track call just happened. Do stuff') + * }) + * + * // Remove listener before it is called + * const removeListener = analytics.on('track', ({ payload }) => { + * console.log('This will never get called') + * }) + * + * // cleanup .on listener + * removeListener() + * @param name - Name of event to listen to + * @param callback - function to fire on event + */ + type On = (name: string, callback: (...params: any[]) => any) => DetachListeners; + + /** + * Detach listeners + */ + export type DetachListeners = () => void; + + /** + * Attach a handler function to an event and only trigger it once. + * @example + * // Fire function only once per 'track' + * analytics.once('track', ({ payload }) => { + * console.log('This is only triggered once when analytics.track() fires') + * }) + * + * // Remove listener before it is called + * const listener = analytics.once('track', ({ payload }) => { + * console.log('This will never get called b/c listener() is called') + * }) + * + * // cleanup .once listener before it fires + * listener() + * @param name - Name of event to listen to + * @param callback - function to fire on event + */ + type Once = (name: string, callback: (...params: any[]) => any) => DetachListeners; + + /** + * Get data about user, activity, or context. Access sub-keys of state with `dot.prop` syntax. + * @example + * // Get the current state of analytics + * analytics.getState() + * + * // Get a subpath of state + * analytics.getState('context.offline') + * @param [key] - dot.prop.path value of state + */ + type GetState = (key?: string) => any; + + /** + * Storage utilities for persisting data. + * These methods will allow you to save data in localStorage, cookies, or to the window. + * @example + * // Pull storage off analytics instance + * const { storage } = analytics + * + * // Get value + * storage.getItem('storage_key') + * + * // Set value + * storage.setItem('storage_key', 'value') + * + * // Remove value + * storage.removeItem('storage_key') + * @property getItem - Get value from storage + * @property setItem - Set storage value + * @property removeItem - Remove storage value + */ + type Storage = { + getItem: GetItem; + setItem: SetItem; + removeItem: RemoveItem; + }; + + /** + * Get value from storage + * @example + * analytics.storage.getItem('storage_key') + * @param key - storage key + * @param [options] - storage options + */ + type GetItem = (key: string, options?: any) => any; + + /** + * Set storage value + * @example + * analytics.storage.setItem('storage_key', 'value') + * @param key - storage key + * @param value - storage value + * @param [options] - storage options + */ + type SetItem = (key: string, value: any, options?: any) => void; + + /** + * Remove storage value + * @example + * analytics.storage.removeItem('storage_key') + * @param key - storage key + * @param [options] - storage options + */ + type RemoveItem = (key: string, options?: any) => void; + + /** + * Async reduce over matched plugin methods + * Fires plugin functions + */ + function processEvent(): void; + + /** + * Return array of event names + * @param eventType - original event type + * @param namespace - optional namespace postfix + * @returns - type, method, end + */ + function getEventNames(eventType: string, namespace: string): any[]; + + /** + * Generate arguments to pass to plugin methods + * @param instance - analytics instance + * @param abortablePlugins - plugins that can be cancelled by caller + * @returns function to inject plugin params + */ + function argumentFactory(instance: any, abortablePlugins: any[]): any; + + /** + * Verify plugin is not calling itself with whatever:myPluginName self refs + */ + function validateMethod(): void; + + /** + * Return the canonical URL and rmove the hash. + * @param search - search param + * @returns return current canonical URL + */ + function currentUrl(search: string): string; + + /** + * Page data for overides + * @property [title] - Page title + * @property [url] - Page url + * @property [path] - Page path + * @property [search] - Page search + * @property [width] - Page width + * @property [height] - Page height + */ + interface PageDataBase { + title?: string; + url?: string; + path?: string; + search?: string; + width?: string; + height?: string; + } + + /** + * Get information about current page + * @param [pageData = {}] - Page data overides + */ + type getPageData = (pageData?: PageData) => PageData; + + /** + * @property name - Name of plugin + * @property [EVENTS] - exposed events of plugin + * @property [config] - Configuration of plugin + * @property [initialize] - Load analytics scripts method + * @property [page] - Page visit tracking method + * @property [track] - Custom event tracking method + * @property [identify] - User identify method + * @property [loaded] - Function to determine if analytics script loaded + * @property [ready] - Fire function when plugin ready + */ + interface AnalyticsPluginBase { + name: string; + EVENTS?: any; + config?: any; + initialize?: (...params: any[]) => any; + page?: (...params: any[]) => any; + track?: (...params: any[]) => any; + identify?: (...params: any[]) => any; + loaded?: (...params: any[]) => any; + ready?: (...params: any[]) => any; + } + + + + export type PageData = PageDataBase & Record; + export type AnalyticsPlugin = AnalyticsPluginBase & string extends T + ? Record + : Record & Record; + + + export const CONSTANTS: constants; + + export const init: typeof analytics; + + export const Analytics: typeof analytics; + + export default analytics; +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index cd1db1df..72684b23 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,50 @@ types/ misc.js ### VisualStudioCode ### -.vscode/* \ No newline at end of file +.vscode/* + +output.txt +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# generated content +public/_data +public/data +# remote content +sources + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# Local Netlify folder +.netlify diff --git a/package.json b/package.json index c9717506..19dcf94a 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,6 @@ "sane": "^4.1.0", "sync-rpc": "^1.3.6", "terser": "^5.10.0", - "tsd-jsdoc": "^2.5.0", "typescript": "^4.3.5", "uglify-js": "^3.15.0" }, diff --git a/packages/analytics-core/package.json b/packages/analytics-core/package.json index b610a411..a3495463 100644 --- a/packages/analytics-core/package.json +++ b/packages/analytics-core/package.json @@ -22,7 +22,7 @@ "main": "dist/server/analytics-core.js", "module": "dist/server/analytics-core.module.js", "unpkg": "dist/server/analytics-core.umd.js", - "types": "dist/types.d.ts", + "types": "dist/index.d.ts", "browser": { "./dist/server/analytics-core.js": "./dist/client/analytics-core.js", "./dist/server/analytics-core.umd.js": "./dist/client/analytics-core.umd.js", @@ -32,11 +32,11 @@ "scripts": { "test": "ava -v", "test:watch": "ava -v --watch", - "prebuild": "rimraf dist _temp-types", + "prebuild": "rimraf dist", "build": "npm run build-client && npm run build-server && npm run types", "build-client": "cd client && npm run build", "build-server": "cd server && npm run build", - "types": "../../node_modules/.bin/jsdoc -t ../../node_modules/tsd-jsdoc/dist -r ./src/ -d _temp-types && node scripts/types.js", + "types": "tsc", "publish": "git push origin && git push origin --tags", "release:patch": "npm version patch && npm publish", "release:minor": "npm version minor && npm publish", diff --git a/packages/analytics-core/scripts/types.js b/packages/analytics-core/scripts/types.js deleted file mode 100644 index aafa234c..00000000 --- a/packages/analytics-core/scripts/types.js +++ /dev/null @@ -1,85 +0,0 @@ -// 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, '../dist/types.d.ts') -const content = fs.readFileSync(TYPES_PATH, 'utf-8') - -// const PAGE_REGEX = /type PageData = {(?:.*?|\n)*?}/gm -// const PLUGIN_REGEX = /type AnalyticsPlugin = {(?:.*?|\n)*?}/gm - -const typesFromJsDocs = content - // Remove declares - .replace(/^declare\s/gm, '') - // Export user-facing types (and where possible, convert objects to interface so they can be extended) - // via https://github.com/DavidWells/analytics/blob/2712604ff90ffd0baf4ab62c7f12267b07b0d823/packages/analytics-core/scripts/types.js#L15 - .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;') - // 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, ' & ') - // https://github.com/DavidWells/analytics/issues/218 - .replace(/string & string\[\]/gm, 'string | string[]') - .replace(/string & number/gm, 'string | number') - - - // 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; - - export const Analytics: typeof analytics; - - export default analytics; -}` - -// Fix interface semi colons (╯°□°)╯︵ ┻━┻ -const finalContent = newContent.replace(/interface (\w+)(\s+){([?\w\s\d:;()\[\]=>\.]*)};/gm, 'interface $1$2{$3}') - -mkdirp(path.dirname(OUTPUT_PATH), function (err) { - if (err) console.error(err) - fs.writeFileSync(OUTPUT_PATH, finalContent) -}) diff --git a/packages/analytics-core/src/constants.js b/packages/analytics-core/src/constants.js index b0ac3f24..2c1a6858 100644 --- a/packages/analytics-core/src/constants.js +++ b/packages/analytics-core/src/constants.js @@ -1,25 +1,30 @@ /** * Core Analytic constants. These are exposed for third party plugins & listeners - * @typedef {Object} constants - * @property {ANON_ID} ANON_ID - Anonymous visitor Id localstorage key - * @property {USER_ID} USER_ID - Visitor Id localstorage key - * @property {USER_TRAITS} USER_TRAITS - Visitor traits localstorage key */ import { PREFIX } from '@analytics/type-utils' - /** * Anonymous visitor Id localstorage key - * @typedef {String} ANON_ID + * @type {string} */ export const ANON_ID = PREFIX + 'anon_id' // __anon_id + /** - * Visitor Id localstorage key - * @typedef {String} USER_ID + * Visitor Id localstorage key + * @type {string} */ export const USER_ID = PREFIX + 'user_id' // __user_id + /** * Visitor traits localstorage key - * @typedef {String} USER_TRAITS + * @type {string} */ export const USER_TRAITS = PREFIX + 'user_traits' // __user_traits + +/** + * Core Analytic constants. These are exposed for third party plugins & listeners + * @typedef {Object} constants + * @property {ANON_ID} ANON_ID - Anonymous visitor Id localstorage key + * @property {USER_ID} USER_ID - Visitor Id localstorage key + * @property {USER_TRAITS} USER_TRAITS - Visitor traits localstorage key + */ diff --git a/packages/analytics-core/src/index.js b/packages/analytics-core/src/index.js index ba1654a6..b53e5460 100644 --- a/packages/analytics-core/src/index.js +++ b/packages/analytics-core/src/index.js @@ -21,20 +21,466 @@ import { Debug, composeWithDebug } from './utils/debug' import heartBeat from './utils/heartbeat' import ensureArray from './utils/ensureArray' import enrichMeta from './utils/enrichMeta' -import './pluginTypeDef' +/** + * Analytics library configuration + * + * After the library is initialized with config, the core API is exposed & ready for use in the application. + * @typedef {Object} AnalyticsConfig + * @property {string} [app] - Name of site / app + * @property {string|number} [version] - Version of your app + * @property {boolean} [debug] - Should analytics run in debug mode + * @property {AnalyticsPlugin[]} [plugins] - Array of analytics plugins + * @property {Object} [storage] - Custom storage implementation + * @property {Function} [storage.getItem] - Function to get storage item + * @property {Function} [storage.setItem] - Function to set storage item + * @property {Function} [storage.removeItem] - Function to remove storage item + * @property {Object} [reducers] - Custom reducers for state management + * @property {Object} [initialUser] - Initial user data + */ + +/** + * Analytics plugin base structure + * @typedef {Object} AnalyticsPluginBase + * @property {string} name - Name of plugin + * @property {Object} [EVENTS] - Exposed events of plugin + * @property {Object} [config] - Configuration of plugin + * @property {boolean} [enabled] - Whether plugin is enabled + * @property {(...params: any[]) => any} [initialize] - Load analytics scripts method + * @property {(...params: any[]) => any} [page] - Page visit tracking method + * @property {(...params: any[]) => any} [track] - Custom event tracking method + * @property {(...params: any[]) => any} [identify] - User identify method + * @property {(...params: any[]) => any} [loaded] - Function to determine if analytics script loaded + * @property {(...params: any[]) => any} [ready] - Fire function when plugin ready + * @property {Object} [methods] - Custom methods exposed by plugin + */ + +/** + * Analytics plugin with optional custom properties + * @template {string} [T=string] + * @typedef {AnalyticsPluginBase & (string extends T ? Record : Record & Record)} AnalyticsPlugin + */ + +/** + * Storage interface + * @typedef {Object} StorageInterface + * @property {GetItem} getItem - Get item from storage + * @property {SetItem} setItem - Set item in storage + * @property {RemoveItem} removeItem - Remove item from storage + */ + +/** + * Get value from storage + * @callback GetItem + * @param {string} key - storage key + * @param {any} [options] - storage options + * @returns {any} + * @example + * + * analytics.storage.getItem('storage_key') + */ + +/** + * Set storage value + * @callback SetItem + * @param {string} key - storage key + * @param {any} value - storage value + * @param {any} [options] - storage options + * @returns {void} + * @example + * + * analytics.storage.setItem('storage_key', 'value') + */ + +/** + * Remove storage value + * @callback RemoveItem + * @param {string} key - storage key + * @param {any} [options] - storage options + * @returns {void} + * @example + * + * analytics.storage.removeItem('storage_key') + */ + +/** + * Enable analytics plugin + * @callback EnablePlugin + * @param {string|string[]} plugins - name of plugins(s) to enable + * @param {(...params: any[]) => any} [callback] - callback after enable runs + * @returns {Promise} + * @example + * + * analytics.plugins.enable('google-analytics').then(() => { + * console.log('do stuff') + * }) + * + * // Enable multiple plugins at once + * analytics.plugins.enable(['google-analytics', 'segment']).then(() => { + * console.log('do stuff') + * }) + */ + +/** + * Disable analytics plugin + * @callback DisablePlugin + * @param {string|string[]} plugins - name of integration(s) to disable + * @param {(...params: any[]) => any} [callback] - callback after disable runs + * @returns {Promise} + * @example + * + * analytics.plugins.disable('google').then(() => { + * console.log('do stuff') + * }) + * + * analytics.plugins.disable(['google', 'segment']).then(() => { + * console.log('do stuff') + * }) + */ + +/** + * Async Management methods for plugins. + * + * This is also where [custom methods](https://bit.ly/329vFXy) are loaded into the instance. + * @typedef {Object} Plugins + * @property {EnablePlugin} enable - Enable plugin(s) + * @property {DisablePlugin} disable - Disable plugin(s) + * @example + * // Enable a plugin by namespace + * analytics.plugins.enable('keenio') + * + * // Disable a plugin by namespace + * analytics.plugins.disable('google-analytics') + */ + +/** + * Identify a user. This will trigger `identify` calls in any installed plugins and will set user data in localStorage + * @callback Identify + * @param {string} userId - Unique ID of user + * @param {any} [traits] - Object of user traits + * @param {any} [options] - Options to pass to identify call + * @param {(...params: any[]) => any} [callback] - Callback function after identify completes + * @returns {Promise} + * @example + * // Basic user id identify + * analytics.identify('xyz-123') + * + * // Identify with additional traits + * analytics.identify('xyz-123', { + * name: 'steve', + * company: 'hello-clicky' + * }) + * + * // Fire callback with 2nd or 3rd argument + * analytics.identify('xyz-123', () => { + * console.log('do this after identify') + * }) + * + * // Disable sending user data to specific analytic tools + * analytics.identify('xyz-123', {}, { + * plugins: { + * // disable sending this identify call to segment + * segment: false + * } + * }) + * + * // Send user data to only to specific analytic tools + * analytics.identify('xyz-123', {}, { + * plugins: { + * // disable this specific identify in all plugins except customerio + * all: false, + * customerio: true + * } + * }) + */ + +/** + * Track an analytics event. This will trigger `track` calls in any installed plugins + * @callback Track + * @param {string} eventName - Event name + * @param {any} [payload] - Event payload + * @param {any} [options] - Event options + * @param {(...params: any[]) => any} [callback] - Callback to fire after tracking completes + * @returns {Promise} + * @example + * // Basic event tracking + * analytics.track('buttonClicked') + * + * // Event tracking with payload + * analytics.track('itemPurchased', { + * price: 11, + * sku: '1234' + * }) + * + * // Fire callback with 2nd or 3rd argument + * analytics.track('newsletterSubscribed', () => { + * console.log('do this after track') + * }) + * + * // Disable sending this event to specific analytic tools + * analytics.track('cartAbandoned', { + * items: ['xyz', 'abc'] + * }, { + * plugins: { + * // disable track event for segment + * segment: false + * } + * }) + * + * // Send event to only to specific analytic tools + * analytics.track('customerIoOnlyEventExample', { + * price: 11, + * sku: '1234' + * }, { + * plugins: { + * // disable this specific track call all plugins except customerio + * all: false, + * customerio: true + * } + * }) + */ + +/** + * Trigger page view. This will trigger `page` calls in any installed plugins + * @callback Page + * @param {import('./modules/page').PageData} [data] - Page data overrides. + * @param {any} [options] - Page tracking options + * @param {(...params: any[]) => any} [callback] - Callback to fire after page view call completes + * @returns {Promise} + * @example + * // Basic page tracking + * analytics.page() + * + * // Page tracking with page data overrides + * analytics.page({ + * url: 'https://google.com' + * }) + * + * // Fire callback with 1st, 2nd or 3rd argument + * analytics.page(() => { + * console.log('do this after page') + * }) + * + * // Disable sending this pageview to specific analytic tools + * analytics.page({}, { + * plugins: { + * // disable page tracking event for segment + * segment: false + * } + * }) + * + * // Send pageview to only to specific analytic tools + * analytics.page({}, { + * plugins: { + * // disable this specific page in all plugins except customerio + * all: false, + * customerio: true + * } + * }) + */ + +/** + * Get user data + * @callback User + * @param {string} [key] - dot.prop.path of user data. Example: 'traits.company.name' + * @returns {string & any} + * @example + * // Get all user data + * const userData = analytics.user() + * + * // Get user id + * const userId = analytics.user('userId') + * + * // Get user company name + * const companyName = analytics.user('traits.company.name') + */ + +/** + * Clear all information about the visitor & reset analytic state. + * @callback Reset + * @param {(...params: any[]) => any} [callback] - Handler to run after reset + * @returns {Promise} + * @example + * // Reset current visitor + * analytics.reset() + */ + +/** + * Fire callback on analytics ready event + * @callback Ready + * @param {(...params: any[]) => any} callback - function to trigger when all providers have loaded + * @returns {DetachListeners} + * @example + * analytics.ready((payload) => { + * console.log('all plugins have loaded or were skipped', payload); + * }) + */ + +/** + * Attach an event handler function for analytics lifecycle events. + * @callback On + * @param {string} name - Name of event to listen to + * @param {(...params: any[]) => any} callback - function to fire on event + * @returns {DetachListeners} + * @example + * // Fire function when 'track' calls happen + * analytics.on('track', ({ payload }) => { + * console.log('track call just happened. Do stuff') + * }) + * + * // Remove listener before it is called + * const removeListener = analytics.on('track', ({ payload }) => { + * console.log('This will never get called') + * }) + * + * // cleanup .on listener + * removeListener() + */ + +/** + * Detach listeners function + * @callback DetachListeners + * @returns {void} + */ + +/** + * Attach a handler function to an event and only trigger it once. + * @callback Once + * @param {string} name - Name of event to listen to + * @param {(...params: any[]) => any} callback - function to fire on event + * @returns {DetachListeners} + * @example + * // Fire function only once per 'track' + * analytics.once('track', ({ payload }) => { + * console.log('This is only triggered once when analytics.track() fires') + * }) + * + * // Remove listener before it is called + * const listener = analytics.once('track', ({ payload }) => { + * console.log('This will never get called b/c listener() is called') + * }) + * + * // cleanup .once listener before it fires + * listener() + */ + +/** + * Get data about user, activity, or context. Access sub-keys of state with `dot.prop` syntax. + * @callback GetState + * @param {string} [key] - dot.prop.path value of state + * @returns {any} + * @example + * // Get the current state of analytics + * analytics.getState() + * + * // Get a subpath of state + * analytics.getState('context.offline') + */ + +/** + * Storage utilities for persisting data. + * These methods will allow you to save data in localStorage, cookies, or to the window. + * @typedef {Object} Storage + * @property {GetItem} getItem - Get value from storage + * @property {SetItem} setItem - Set storage value + * @property {RemoveItem} removeItem - Remove storage value + * @example + * // Pull storage off analytics instance + * const { storage } = analytics + * + * // Get value + * storage.getItem('storage_key') + * + * // Set value + * storage.setItem('storage_key', 'value') + * + * // Remove value + * storage.removeItem('storage_key') + */ + +/** + * Analytic instance returned from initialization + * @typedef {Object} AnalyticsInstance + * @property {Identify} identify - Identify a user + * @property {Track} track - Track an analytics event + * @property {Page} page - Trigger page view + * @property {User} user - Get user data + * @property {Reset} reset - Clear information about user & reset analytics + * @property {Ready} ready - Fire callback on analytics ready event + * @property {On} on - Fire callback on analytics lifecycle events. + * @property {Once} once - Fire callback on analytics lifecycle events once. + * @property {GetState} getState - Get data about user, activity, or context. + * @property {Storage} storage - storage methods + * @property {Plugins} plugins - plugin methods + */ + +/** + * Async reduce over matched plugin methods + * Fires plugin functions + * @function processEvent + * @returns {void} + */ + +/** + * Return array of event names + * @function getEventNames + * @param {string} eventType - original event type + * @param {string} namespace - optional namespace postfix + * @returns {any[]} - type, method, end + */ + +/** + * Generate arguments to pass to plugin methods + * @function argumentFactory + * @param {any} instance - analytics instance + * @param {any[]} abortablePlugins - plugins that can be cancelled by caller + * @returns {any} function to inject plugin params + */ + +/** + * Verify plugin is not calling itself with whatever:myPluginName self refs + * @function validateMethod + * @returns {void} + */ + +/** + * Page data base interface + * @typedef {Object} PageDataBase + * @property {string} [title] - Page title + * @property {string} [url] - Page url + * @property {string} [path] - Page path + * @property {string} [search] - Page search + * @property {string} [width] - Page width + * @property {string} [height] - Page height + */ + +/** + * Page data with optional custom properties + * @template {string} [T=string] + * @typedef {PageDataBase & Record} PageData + */ + +/** + * Get information about current page + * @function getPageData + * @param {PageData} [pageData={}] - Page data overrides + * @returns {PageData} + */ + +/** + * Return the canonical URL and remove the hash. + * @function currentUrl + * @param {string} search - search param + * @returns {string} return current canonical URL + */ /** * 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|number} [config.version] - Version of your app - * @param {boolean} [config.debug] - Should analytics run in debug mode - * @param {Array.} [config.plugins] - Array of analytics plugins - * @return {AnalyticsInstance} Analytics Instance + * @param {AnalyticsConfig} [config={}] - analytics core config + * @returns {AnalyticsInstance} Analytics Instance * @example * * import Analytics from 'analytics' @@ -54,7 +500,7 @@ import './pluginTypeDef' function analytics(config = {}) { const customReducers = config.reducers || {} const initialUser = config.initialUser || {} - // @TODO add custom value reolvers for userId and anonId + // @TODO add custom value resolvers for userId and anonId // const resolvers = config.resolvers || {} // if (BROWSER) { // console.log('INIT browser') @@ -84,7 +530,7 @@ function analytics(config = {}) { const enabledFromMerge = !(plugin.enabled === false) const enabledFromPluginConfig = !(plugin.config.enabled === false) - // top level { enabled: false } takes presidence over { config: enabled: false } + // top level { enabled: false } takes precedence over { config: enabled: false } acc.pluginEnabled[plugin.name] = enabledFromMerge && enabledFromPluginConfig delete plugin.enabled @@ -134,7 +580,7 @@ function analytics(config = {}) { const getUserProp = getUserPropFunc(storage) - // mutable intregrations object for dynamic loading + // mutable integrations object for dynamic loading let customPlugins = parsedOptions.plugins /* Grab all registered events from plugins loaded */ @@ -179,13 +625,10 @@ function analytics(config = {}) { * Async Management methods for plugins. * * This is also where [custom methods](https://bit.ly/329vFXy) are loaded into the instance. - * @typedef {Object} Plugins - * @property {EnablePlugin} enable - Set storage value - * @property {DisablePlugin} disable - Remove storage value * @example * * // Enable a plugin by namespace - * analytics.plugins.enable('keenio') + * analytics.plugins.enable('hubspot') * * // Disable a plugin by namespace * analytics.plugins.disable('google-analytics') @@ -193,10 +636,9 @@ function analytics(config = {}) { const plugins = { /** * Enable analytics plugin - * @typedef {Function} EnablePlugin - * @param {string|string[]} plugins - name of plugins(s) to disable - * @param {Function} [callback] - callback after enable runs - * @returns {Promise} + * @param {string|string[]} plugins - name of plugins(s) to enable + * @param {Callback} [callback] - callback after enable runs + * @returns {Promise} * @example * * analytics.plugins.enable('google-analytics').then(() => { @@ -219,10 +661,9 @@ function analytics(config = {}) { }, /** * Disable analytics plugin - * @typedef {Function} DisablePlugin - * @param {string|string[]} plugins - name of integration(s) to disable - * @param {Function} [callback] - callback after disable runs - * @returns {Promise} + * @param {string|string[]} plugins - name of integration(s) to disable + * @param {Callback} [callback] - callback after disable runs + * @returns {Promise} * @example * * analytics.plugins.disable('google').then(() => { @@ -278,29 +719,16 @@ function analytics(config = {}) { let readyCalled = false /** * Analytic instance returned from initialization - * @typedef {Object} AnalyticsInstance - * @property {Identify} identify - Identify a user - * @property {Track} track - Track an analytics event - * @property {Page} page - Trigger page view - * @property {User} user - Get user data - * @property {Reset} reset - Clear information about user & reset analytics - * @property {Ready} ready - Fire callback on analytics ready event - * @property {On} on - Fire callback on analytics lifecycle events. - * @property {Once} once - Fire callback on analytics lifecycle events once. - * @property {GetState} getState - Get data about user, activity, or context. - * @property {Storage} storage - storage methods - * @property {Plugins} plugins - plugin methods + * @type {AnalyticsInstance} */ const instance = { /** * Identify a user. This will trigger `identify` calls in any installed plugins and will set user data in localStorage - * @typedef {Function} Identify - * @param {String} userId - Unique ID of user - * @param {Object} [traits] - Object of user traits - * @param {Object} [options] - Options to pass to identify call - * @param {Function} [callback] - Callback function after identify completes - * @returns {Promise} - * @api public + * @param {string|Object} [userId] - Unique ID of user or traits object + * @param {Object} [traits] - Object of user traits + * @param {Object} [options] - Options to pass to identify call + * @param {Callback} [callback] - Callback function after identify completes + * @returns {Promise} * * @example * @@ -360,13 +788,11 @@ function analytics(config = {}) { }, /** * Track an analytics event. This will trigger `track` calls in any installed plugins - * @typedef {Function} Track - * @param {String} eventName - Event name - * @param {Object} [payload] - Event payload - * @param {Object} [options] - Event options - * @param {Function} [callback] - Callback to fire after tracking completes - * @returns {Promise} - * @api public + * @param {string} eventName - Event name + * @param {Object} [payload] - Event payload + * @param {Object} [options] - Event options + * @param {Callback} [callback] - Callback to fire after tracking completes + * @returns {Promise} * * @example * @@ -428,7 +854,7 @@ function analytics(config = {}) { /** * Trigger page view. This will trigger `page` calls in any installed plugins * @typedef {Function} Page - * @param {PageData} [data] - Page data overrides. + * @param {import('./modules/page').PageData} [data] - Page data overrides. * @param {Object} [options] - Page tracking options * @param {Function} [callback] - Callback to fire after page view call completes * @returns {Promise} @@ -471,7 +897,7 @@ function analytics(config = {}) { const opts = isObject(options) ? options : {} /* - // @TODO add custom value reolvers for userId and anonId + // @TODO add custom value resolvers for userId and anonId if (resolvers.getUserId) { const asyncUserId = await resolvers.getUserId() console.log('x', x) @@ -720,13 +1146,13 @@ function analytics(config = {}) { } store.dispatch(dispatchData) }, - // Do not use. Will be removed. Here for Backwards compatiblity. + // Do not use. Will be removed. Here for Backwards compatibility. // Moved to analytics.plugins.enable enablePlugin: plugins.enable, - /// Do not use. Will be removed. Here for Backwards compatiblity. + /// Do not use. Will be removed. Here for Backwards compatibility. /// Moved to analytics.plugins.disable disablePlugin: plugins.disable, - // Do not use. Will be removed. Here for Backwards compatiblity. + // Do not use. Will be removed. Here for Backwards compatibility. // New plugins api plugins: plugins, /** @@ -754,7 +1180,6 @@ function analytics(config = {}) { storage: { /** * Get value from storage - * @typedef {Function} GetItem * @param {String} key - storage key * @param {Object} [options] - storage options * @return {Any} @@ -766,7 +1191,6 @@ function analytics(config = {}) { getItem: storage.getItem, /** * Set storage value - * @typedef {Function} SetItem * @param {String} key - storage key * @param {any} value - storage value * @param {Object} [options] - storage options @@ -785,7 +1209,6 @@ function analytics(config = {}) { }, /** * Remove storage value - * @typedef {Function} RemoveItem * @param {String} key - storage key * @param {Object} [options] - storage options * @@ -874,7 +1297,7 @@ function analytics(config = {}) { const initialConfig = makeContext(config) - const intialPluginState = parsedOptions.pluginsArray.reduce((acc, plugin) => { + const initialPluginState = parsedOptions.pluginsArray.reduce((acc, plugin) => { const { name, config, loaded } = plugin const isEnabled = parsedOptions.pluginEnabled[name] acc[name] = { @@ -890,7 +1313,7 @@ function analytics(config = {}) { const initialState = { context: initialConfig, user: visitorInfo, - plugins: intialPluginState, + plugins: initialPluginState, // Todo allow for more userland defined initial state? } diff --git a/packages/analytics-core/src/modules/page.js b/packages/analytics-core/src/modules/page.js index 97b293d5..7a3c6cb5 100644 --- a/packages/analytics-core/src/modules/page.js +++ b/packages/analytics-core/src/modules/page.js @@ -24,7 +24,7 @@ function urlPath(url) { } /** - * Return the canonical URL and rmove the hash. + * Return the canonical URL and remove the hash. * @param {string} search - search param * @return {string} return current canonical URL */ @@ -35,21 +35,22 @@ function currentUrl(search) { } /** - * Page data for overides - * @typedef {object} PageData + * Page data structure + * @typedef {Object} PageData * @property {string} [title] - Page title - * @property {string} [url] - Page url + * @property {string} [url] - Page URL * @property {string} [path] - Page path - * @property {string} [search] - Page search - * @property {string} [width] - Page width - * @property {string} [height] - Page height -*/ + * @property {string} [referrer] - Page referrer + * @property {string} [search] - URL search parameters + * @property {string} [hash] - URL hash + * @property {number} [width] - Screen width + * @property {number} [height] - Screen height + */ /** * Get information about current page - * @typedef {Function} getPageData - * @param {PageData} [pageData = {}] - Page data overides - * @return {PageData} resolved page data + * @param {PageData} [pageData={}] - Page data overrides + * @returns {PageData} resolved page data */ export const getPageData = (pageData = {}) => { if (!isBrowser) return pageData @@ -72,7 +73,7 @@ export const getPageData = (pageData = {}) => { return { ...page, - /* .page() user overrrides */ + /* .page() user overrides */ ...pageData } } diff --git a/packages/analytics-core/src/modules/user.js b/packages/analytics-core/src/modules/user.js index 8c9fe06f..6deb04c2 100644 --- a/packages/analytics-core/src/modules/user.js +++ b/packages/analytics-core/src/modules/user.js @@ -3,6 +3,15 @@ import { isObject, PREFIX } from '@analytics/type-utils' import { ANON_ID, USER_ID, USER_TRAITS } from '../constants' import EVENTS from '../events' +/** + * User data structure + * @typedef {Object} UserData + * @property {string} [id] - User ID + * @property {string} [userId] - User ID (alias) + * @property {string} anonymousId - Anonymous ID + * @property {Object} [traits] - User traits/properties + */ + /* user reducer */ export default function userReducer(storage) { return function user(state = {}, action = {}) { diff --git a/packages/analytics-core/src/pluginTypeDef.js b/packages/analytics-core/src/pluginTypeDef.js deleted file mode 100644 index bf660429..00000000 --- a/packages/analytics-core/src/pluginTypeDef.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @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 - */ diff --git a/packages/analytics-core/src/types.js b/packages/analytics-core/src/types.js new file mode 100644 index 00000000..f1b9e07b --- /dev/null +++ b/packages/analytics-core/src/types.js @@ -0,0 +1,63 @@ +/** + * @fileoverview TypeScript type definitions for Analytics Core using JSDoc + */ + +/** + * Anonymous visitor Id localstorage key + * @typedef {string} ANON_ID + */ + +/** + * Visitor Id localstorage key + * @typedef {string} USER_ID + */ + +/** + * Visitor traits localstorage key + * @typedef {string} USER_TRAITS + */ + +/** + * Event payload for tracking + * @typedef {Object} TrackPayload + * @property {string} event - Event name + * @property {Object} [properties] - Event properties + * @property {Object} [options] - Event options + * @property {Object} [context] - Event context + */ + +/** + * Identify payload for user identification + * @typedef {Object} IdentifyPayload + * @property {string} [userId] - User ID + * @property {Object} [traits] - User traits + * @property {Object} [options] - Identify options + * @property {Object} [context] - Identify context + */ + +/** + * Page payload for page tracking + * @typedef {Object} PagePayload + * @property {string} [name] - Page name + * @property {Object} [properties] - Page properties + * @property {Object} [options] - Page options + * @property {Object} [context] - Page context + */ + +/** + * Callback function type + * @typedef {Function} Callback + * @param {...*} args - Callback arguments + */ + +/** + * Event listener function + * @typedef {Function} EventListener + * @param {Object} payload - Event payload + */ + +/** + * Detach listeners function + * @typedef {Function} DetachListeners + * @returns {void} + */ \ No newline at end of file diff --git a/packages/analytics-core/tsconfig.json b/packages/analytics-core/tsconfig.json new file mode 100644 index 00000000..10f30d7c --- /dev/null +++ b/packages/analytics-core/tsconfig.json @@ -0,0 +1,24 @@ +{ + "include": [ + "./src/**/*" + ], + "compilerOptions": { + "target": "ES2018", + "module": "commonjs", + "lib": ["ES2018", "DOM"], + "allowJs": true, + "checkJs": false, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": false, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "emitDeclarationOnly": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true + } +} \ No newline at end of file