diff --git a/.github/workflows/lint-check-spa.yml b/.github/workflows/lint-check-spa.yml index c4e31c619..8c8470e4f 100644 --- a/.github/workflows/lint-check-spa.yml +++ b/.github/workflows/lint-check-spa.yml @@ -38,10 +38,14 @@ jobs: path: node_modules key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: ${{ runner.os }}-node- + + - name: Setup Registry + run: printf "@newfold-labs:registry=https://npm.pkg.github.com/\n//npm.pkg.github.com/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc + if: steps.cache.outputs.cache-hit != 'true' # Installs @wordpress/scripts for lint checks if it does not exist in the cache. - name: Install dependencies - run: npm i @wordpress/scripts + run: npm i @wordpress/scripts @newfold-labs/js-utility-ui-analytics if: steps.cache.outputs.cache-hit != 'true' # Gets the files changed wrt to trunk and filters out the js files. diff --git a/includes/Data/Events.php b/includes/Data/Events.php index 62a457d1c..a8796031d 100644 --- a/includes/Data/Events.php +++ b/includes/Data/Events.php @@ -2,29 +2,65 @@ namespace NewfoldLabs\WP\Module\Onboarding\Data; /** - * List of Onboarding events. + * Contains data related to Onboarding Hiive Events. */ final class Events { + /** + * The category of an event. + * + * @var string + */ + protected static $category = 'wp-onboarding'; - /** - * Contains a list of events with the key being the event slug. - * - * @var array - */ - protected static $events = array( - 'nfd-module-onboarding-event-pageview' => array( - 'category' => 'Admin', - 'action' => 'pageview', - ), + /** + * List of valid actions that an event can perform. + * + * A value of true indicates that the action is valid, set it to null if you want to invalidate an action. + * + * @var array + */ + protected static $valid_actions = array( + 'pageview' => true, + 'sidebar-opened' => true, + 'sidebar-closed' => true, + 'wp-experience' => true, + 'primary-type' => true, + 'secondary-type' => true, + 'tax-information' => true, + 'selected-style' => true, + 'default-style' => true, + 'customize-design' => true, + 'font-selection' => true, + 'theme-header' => true, + 'homepage-layout' => true, + 'top-priority' => true, + 'top-priority-skipped' => true, + 'exit-to-wordpress' => true, + 'products-info' => true, + 'yith-wonder/company-page-layout' => true, + 'yith-wonder/contact-us-layout' => true, + 'yith-wonder/blog-page-layout' => true, + 'yith-wonder/testimonials-page-layout' => true, + 'site-features' => true, + 'color-selection' => true, + 'color-selection-reset' => true, ); /** - * Retrieves the active theme color variations. + * Returns the list of valid actions that an event can perform + * + * @return array + */ + public static function get_valid_actions() { + return self::$valid_actions; + } + + /** + * Valid category of on event. * - * @param array $event_slug Event data. - * @return array|boolean + * @return string */ - public static function get_event( $event_slug ) { - return self::$events[ $event_slug ] ? self::$events[ $event_slug ] : false; + public static function get_category() { + return self::$category; } } diff --git a/includes/RestApi/EventsController.php b/includes/RestApi/EventsController.php index 03b31752f..c5cffb52b 100644 --- a/includes/RestApi/EventsController.php +++ b/includes/RestApi/EventsController.php @@ -3,31 +3,32 @@ use NewfoldLabs\WP\Module\Onboarding\Data\Events; use NewfoldLabs\WP\Module\Onboarding\Permissions; +use NewfoldLabs\WP\Module\Onboarding\Services\EventService; /** - * [Class EventsController] + * Controller to send analytics events. */ class EventsController extends \WP_REST_Controller { /** - * This is the REST API namespace that will be used for our custom API + * The namespace of the controller. * * @var string */ - protected $namespace = 'newfold-onboarding/v1'; + protected $namespace = 'newfold-onboarding/v1'; - /** - * This is the REST endpoint - * - * @var string - */ - protected $rest_base = '/events'; + /** + * The REST base endpoint. + * + * @var string + */ + protected $rest_base = '/events'; - /** - * Register REST routes for EventsController class. - * - * @return void - */ + /** + * Register routes that the controller will expose. + * + * @return void + */ public function register_routes() { \register_rest_route( $this->namespace, @@ -35,68 +36,106 @@ public function register_routes() { array( array( 'methods' => \WP_REST_Server::CREATABLE, - 'callback' => array( $this, 'send_event' ), + 'callback' => array( $this, 'send' ), + 'permission_callback' => array( Permissions::class, 'rest_is_authorized_admin' ), + 'args' => $this->get_send_args(), + ), + ) + ); + + \register_rest_route( + $this->namespace, + $this->rest_base . '/batch', + array( + array( + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'send_batch' ), 'permission_callback' => array( Permissions::class, 'rest_is_authorized_admin' ), - 'args' => $this->get_send_event_args(), ), ) ); } - /** - * Get args for the send_event endpoint. - * - * @return array - */ - public function get_send_event_args() { - return array( - 'slug' => array( - 'required' => true, - 'type' => 'string', - 'sanitize_callback' => 'sanitize_text_field', - ), - 'data' => array( - 'type' => 'object', - ), - ); + /** + * Args for a single event. + * + * @return array + */ + public function get_send_args() { + return array( + 'action' => array( + 'required' => true, + 'description' => __( 'Event action', 'wp-module-onboarding' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_title', + 'validate_callback' => array( EventService::class, 'validate_action' ), + ), + 'category' => array( + 'default' => Events::get_category(), + 'description' => __( 'Event category', 'wp-module-onboarding' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_title', + 'validate_callback' => array( EventService::class, 'validate_category' ), + ), + 'data' => array( + 'description' => __( 'Event data', 'wp-module-onboarding' ), + 'type' => 'object', + ), + ); + } + + /** + * Sends a Hiive Event to the data module API. + * + * @param \WP_REST_Request $request The incoming request object. + * @return \WP_REST_Response|\WP_Error + */ + public function send( \WP_REST_Request $request ) { + return EventService::send( $request->get_params() ); } - /** - * Send events to the data module events API. - * - * @param \WP_REST_Request $request Request model. - * - * @return \WP_REST_Response|\WP_Error - */ - public function send_event( \WP_REST_Request $request ) { - $event = Events::get_event( $request->get_param( 'slug' ) ); - if ( ! $event ) { + /** + * Sends an array of Hiive Events to the data module API programmatically. + * + * @param \WP_REST_Request $request The incoming request object. + * @return \WP_REST_Response|\WP_Error + */ + public function send_batch( \WP_REST_Request $request ) { + $events = $request->get_json_params(); + if ( ! rest_is_array( $events ) ) { return new \WP_Error( - 'event-error', - 'No such event found', - array( 'status' => 404 ) + 'nfd_module_onboarding_error', + __( 'Request does not contain an array of events.', 'wp-module-onboarding' ) ); } - $event['data'] = $request->get_param( 'data' ); - - if ( ! empty( $event['data'] ) && ! empty( $event['data']['stepID'] ) ) { - $event['data']['page'] = \admin_url( '/index.php?page=nfd-onboarding#' . $event['data']['stepID'] ); + $response_errors = array(); + foreach ( $events as $index => $event ) { + $response = EventService::send( $event ); + if ( is_wp_error( $response ) ) { + array_push( + $response_errors, + array( + 'index' => $index, + 'data' => $response, + ) + ); + } } - $event_data_request = new \WP_REST_Request( - \WP_REST_Server::CREATABLE, - NFD_MODULE_DATA_EVENTS_API - ); - $event_data_request->set_body_params( $event ); - $response = \rest_do_request( $event_data_request ); - if ( $response->is_error() ) { - return $response->as_error(); + if ( ! empty( $response_errors ) ) { + return new \WP_Error( + 'nfd_module_onboarding_error', + __( 'Some events failed.', 'wp-module-onboarding' ), + array( + 'data' => $response_errors, + ) + ); } return new \WP_REST_Response( - $response, - $response->status + array(), + 202 ); } } diff --git a/includes/Services/EventService.php b/includes/Services/EventService.php new file mode 100644 index 000000000..d8b215677 --- /dev/null +++ b/includes/Services/EventService.php @@ -0,0 +1,83 @@ +set_body_params( $event ); + + $response = rest_do_request( $event_data_request ); + if ( $response->is_error() ) { + return $response->as_error(); + } + + return $response; + } + + /** + * Validates the category of an event. + * + * @param string $category The category of an event. + * @return boolean + */ + public static function validate_category( $category ) { + return Events::get_category() === $category; + } + + /** + * Validates the action performed in an event. + * + * @param string $action The action performed in an event. + * @return boolean + */ + public static function validate_action( $action ) { + $valid_actions = Events::get_valid_actions(); + if ( ! isset( $valid_actions[ $action ] ) ) { + return false; + } + + return true; + } + + /** + * Sanitizes and validates the action and category parameters of an event. + * + * @param array $event The event to sanitize and validate. + * @return array|boolean + */ + public static function validate( $event ) { + if ( ! isset( $event['action'] ) || ! self::validate_action( $event['action'] ) ) { + return false; + } + + if ( ! isset( $event['category'] ) || ! self::validate_category( $event['category'] ) ) { + return false; + } + + return $event; + } +} diff --git a/package.json b/package.json index 9ca986950..e72aac706 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "test:unit": "npx wp-env run phpunit 'phpunit -c /var/www/html/wp-content/plugins//phpunit.xml --verbose'" }, "dependencies": { + "@newfold-labs/js-utility-ui-analytics": "1.0.0", "@wordpress/interface": "^5.10.0", "@wordpress/style-engine": "^0.11.0", "classnames": "^2.3.1", diff --git a/src/OnboardingSPA/components/App/index.js b/src/OnboardingSPA/components/App/index.js index 73faaa137..79aef8539 100644 --- a/src/OnboardingSPA/components/App/index.js +++ b/src/OnboardingSPA/components/App/index.js @@ -19,6 +19,8 @@ import { useEffect, Fragment, useState } from '@wordpress/element'; import { FullscreenMode } from '@wordpress/interface'; import { API_REQUEST } from '../../../constants'; import NewfoldInterfaceSkeleton from '../NewfoldInterfaceSkeleton'; +import { HiiveAnalytics } from '@newfold-labs/js-utility-ui-analytics'; +import { trackHiiveEvent } from '../../utils/analytics'; /** * Primary app that renders the . @@ -139,6 +141,7 @@ const App = () => { setIsRequestPlaced( false ); } } + // Check if the Basic Info page was visited if ( location?.pathname.includes( 'basic-info' ) ) { setDidVisitBasicInfo( true ); @@ -223,12 +226,57 @@ const App = () => { } } + const handlePreviousStepTracking = () => { + const previousStep = window.nfdOnboarding?.previousStepID; + if ( typeof previousStep !== 'string' ) { + window.nfdOnboarding.previousStepID = location.pathname; + HiiveAnalytics.dispatchEvents(); + return; + } + + if ( previousStep.includes( 'products' ) ) { + trackHiiveEvent( 'products-info', { + productCount: + currentData.storeDetails.productInfo.product_count, + productTypes: + currentData.storeDetails.productInfo.product_types.join( + ',' + ), + } ); + } + + if ( previousStep.includes( 'site-pages' ) ) { + currentData.data.sitePages?.other?.forEach( ( sitePage ) => { + trackHiiveEvent( `${ sitePage.slug }-layout`, sitePage.slug ); + } ); + } + + if ( previousStep.includes( 'site-features' ) ) { + const siteFeatures = currentData.data?.siteFeatures; + if ( siteFeatures ) { + const siteFeaturesArray = Object.keys( siteFeatures ).filter( + ( key ) => { + return siteFeatures[ key ] !== false; + } + ); + trackHiiveEvent( + 'site-features', + siteFeaturesArray.join( ',' ) + ); + } + } + + window.nfdOnboarding.previousStepID = location.pathname; + HiiveAnalytics.dispatchEvents(); + }; + useEffect( () => { document.body.classList.add( `nfd-brand-${ newfoldBrand }` ); }, [ newfoldBrand ] ); useEffect( () => { syncStoreToDB(); + handlePreviousStepTracking(); handleColorsAndTypographyRoutes(); if ( location.pathname.includes( '/step' ) ) { setActiveFlow( onboardingFlow ); diff --git a/src/OnboardingSPA/components/Drawer/DrawerPanel/DesignColors.js b/src/OnboardingSPA/components/Drawer/DrawerPanel/DesignColors.js index fa542bdea..a2de97be9 100644 --- a/src/OnboardingSPA/components/Drawer/DrawerPanel/DesignColors.js +++ b/src/OnboardingSPA/components/Drawer/DrawerPanel/DesignColors.js @@ -8,6 +8,7 @@ import { getGlobalStyles, getThemeColors } from '../../../utils/api/themes'; import { useGlobalStylesOutput } from '../../../utils/global-styles/use-global-styles-output'; import { THEME_STATUS_ACTIVE, THEME_STATUS_INIT } from '../../../../constants'; import Animate from '../../Animate'; +import { trackHiiveEvent } from '../../../utils/analytics'; const DesignColors = () => { const customColorsResetRef = useRef( null ); @@ -84,9 +85,6 @@ const DesignColors = () => { selectedColorsLocalTemp = selectedColors, globalStylesTemp = storedPreviewSettings ) { - if ( selectedColors?.slug === colorStyle ) { - return true; - } const isCustomStyle = colorStyle === 'custom'; const selectedGlobalStyle = globalStylesTemp; const selectedThemeColorPalette = @@ -102,7 +100,7 @@ const DesignColors = () => { selectedThemeColorPalette[ idx ].color = selectedColorsLocalTemp[ slug ]; } else if ( - // Add Exception for Background.(perhaps scope to yith-wonder in future) + // Add Exception for Background. (perhaps scope to yith-wonder in future) colorPalettesTemp?.[ colorStyle ]?.[ slug ] && 'base' === slug ) { @@ -240,6 +238,9 @@ const DesignColors = () => { }, [ isLoaded, themeStatus ] ); const handleClick = ( colorStyle ) => { + if ( selectedColors?.slug === colorStyle ) { + return true; + } const customColorsTemp = customColors; for ( const custom in customColorsTemp ) { customColorsTemp[ custom ] = ''; @@ -249,6 +250,7 @@ const DesignColors = () => { saveThemeColorPalette( colorStyle ); setSelectedColorsLocal( colorPalettes[ colorStyle ] ); LocalToState( colorPalettes[ colorStyle ], colorStyle ); + trackHiiveEvent( 'color-selection', colorStyle ); }; const changeCustomPickerColor = async ( color ) => { @@ -267,6 +269,9 @@ const DesignColors = () => { saveCustomColors(); LocalToState( selectedColorsLocalCopy, 'custom' ); setSelectedColorsLocal( selectedColorsLocalCopy ); + if ( ! isCustomColorActive() ) { + trackHiiveEvent( 'color-selection', 'custom' ); + } setCustomColors( selectedColorsLocalCopy ); }; @@ -305,6 +310,7 @@ const DesignColors = () => { setSelectedColors( selectedColors ); setCurrentOnboardingData( currentData ); + trackHiiveEvent( 'color-selection-reset', selectedGlobalStyle.title ); } function buildPalettes() { diff --git a/src/OnboardingSPA/components/Drawer/DrawerPanel/DesignHeaderMenu.js b/src/OnboardingSPA/components/Drawer/DrawerPanel/DesignHeaderMenu.js index ba020515a..4b455404a 100644 --- a/src/OnboardingSPA/components/Drawer/DrawerPanel/DesignHeaderMenu.js +++ b/src/OnboardingSPA/components/Drawer/DrawerPanel/DesignHeaderMenu.js @@ -10,6 +10,7 @@ import { import { setFlow } from '../../../utils/api/flow'; import { THEME_STATUS_ACTIVE, THEME_STATUS_INIT } from '../../../../constants'; +import { trackHiiveEvent } from '../../../utils/analytics'; const DesignHeaderMenu = () => { const headerMenuSlugs = [ @@ -122,6 +123,7 @@ const DesignHeaderMenu = () => { if ( result?.error === null ) { setCurrentOnboardingData( currentData ); } + trackHiiveEvent( 'theme-header', chosenPattern.slug ); }; const buildPreviews = () => { diff --git a/src/OnboardingSPA/components/Drawer/DrawerPanel/DesignThemeStylesPreview.js b/src/OnboardingSPA/components/Drawer/DrawerPanel/DesignThemeStylesPreview.js index 580d3abc4..9ee64490a 100644 --- a/src/OnboardingSPA/components/Drawer/DrawerPanel/DesignThemeStylesPreview.js +++ b/src/OnboardingSPA/components/Drawer/DrawerPanel/DesignThemeStylesPreview.js @@ -10,6 +10,7 @@ import { LivePreviewSelectableCard, LivePreviewSkeleton, } from '../../LivePreview'; +import { trackHiiveEvent } from '../../../utils/analytics'; const DesignThemeStylesPreview = () => { const [ pattern, setPattern ] = useState(); @@ -106,6 +107,7 @@ const DesignThemeStylesPreview = () => { setSelectedStyle( selectedGlobalStyle.title ); currentData.data.theme.variation = selectedGlobalStyle.title; setCurrentOnboardingData( currentData ); + trackHiiveEvent( 'selected-style', selectedGlobalStyle.title ); }; const buildPreviews = () => { diff --git a/src/OnboardingSPA/components/Drawer/DrawerPanel/DesignTypography.js b/src/OnboardingSPA/components/Drawer/DrawerPanel/DesignTypography.js index 01cf6889a..7b049e778 100644 --- a/src/OnboardingSPA/components/Drawer/DrawerPanel/DesignTypography.js +++ b/src/OnboardingSPA/components/Drawer/DrawerPanel/DesignTypography.js @@ -6,6 +6,7 @@ import { store as nfdOnboardingStore } from '../../../store'; import { getThemeFonts } from '../../../utils/api/themes'; import { useGlobalStylesOutput } from '../../../utils/global-styles/use-global-styles-output'; import { THEME_STATUS_ACTIVE, THEME_STATUS_INIT } from '../../../../constants'; +import { trackHiiveEvent } from '../../../utils/analytics'; const DesignTypography = () => { const drawerFontOptions = useRef(); @@ -57,16 +58,17 @@ const DesignTypography = () => { fontPalettes !== undefined ) { setSelectedFont( currentData?.data?.typography?.slug ); - handleClick( currentData?.data?.typography?.slug ); + handleClick( currentData?.data?.typography?.slug, 'flow' ); } }, [ fontPalettes, storedPreviewSettings ] ); useEffect( () => { - if ( ! isLoaded && THEME_STATUS_ACTIVE === themeStatus ) + if ( ! isLoaded && THEME_STATUS_ACTIVE === themeStatus ) { getFontStylesAndPatterns(); + } }, [ isLoaded, themeStatus ] ); - const handleClick = async ( fontStyle ) => { + const handleClick = async ( fontStyle, context = 'click' ) => { if ( selectedFont === fontStyle ) { return true; } @@ -124,12 +126,17 @@ const DesignTypography = () => { useGlobalStylesOutput( globalStylesCopy, storedPreviewSettings ) ); setCurrentOnboardingData( currentData ); + if ( 'click' === context ) { + trackHiiveEvent( 'font-selection', fontStyle ); + } }; function buildPalettes() { return Object.keys( fontPalettes ).map( ( fontStyle, idx ) => { const splitLabel = fontPalettes[ fontStyle ]?.label.split( '&', 2 ); - if ( splitLabel.length === 0 ) return null; + if ( splitLabel.length === 0 ) { + return null; + } return (
{ +} ) => { const location = useLocation(); - const mainContainer = document.querySelector('.nfd-onboard-content'); + const mainContainer = document.querySelector( '.nfd-onboard-content' ); - const speakRouteTitle = ( - location, - title = 'Showing new Onboarding Page' - ) => { + const speakRouteTitle = ( title = 'Showing new Onboarding Page' ) => { // [TODO]: Determine if some routes should not speak the title - speak(title, 'assertive'); + speak( title, 'assertive' ); }; - useEffect(() => { - mainContainer?.focus({ preventScroll: true }); - speakRouteTitle(location, 'Override'); - new Event(`${NFD_ONBOARDING_EVENT_PREFIX}-pageview`, { - stepID: location.pathname, - previousStepID: window.nfdOnboarding.previousStepID - }).send(); - window.nfdOnboarding.previousStepID = location.pathname - }, [location.pathname]); + useEffect( () => { + mainContainer?.focus( { preventScroll: true } ); + speakRouteTitle( 'Override' ); + trackHiiveEvent( 'pageview', window.location.href ); + }, [ location.pathname ] ); return ( -
- {children} +
+ { children }
); }; diff --git a/src/OnboardingSPA/components/Sidebar/components/LearnMore/Menu.js b/src/OnboardingSPA/components/Sidebar/components/LearnMore/Menu.js index 355a41cc1..7da4ba233 100644 --- a/src/OnboardingSPA/components/Sidebar/components/LearnMore/Menu.js +++ b/src/OnboardingSPA/components/Sidebar/components/LearnMore/Menu.js @@ -8,6 +8,7 @@ import { SIDEBAR_MENU_SLOTFILL_PREFIX, } from '../../../../../constants'; import classNames from 'classnames'; +import { trackHiiveEvent } from '../../../../utils/analytics'; const LearnMoreMenu = () => { const { isSidebarOpened, sideBarView, currentStep } = useSelect( @@ -23,12 +24,16 @@ const LearnMoreMenu = () => { const { setIsSidebarOpened, setSidebarActiveView } = useDispatch( nfdOnboardingStore ); const toggleSidebar = () => { - setSidebarActiveView( SIDEBAR_LEARN_MORE ); - setIsSidebarOpened( + const isSidebarOpenedNew = sideBarView === SIDEBAR_LEARN_MORE ? ! isSidebarOpened - : isSidebarOpened + : isSidebarOpened; + trackHiiveEvent( + isSidebarOpenedNew ? 'sidebar-opened' : 'sidebar-closed', + window.location.href ); + setSidebarActiveView( SIDEBAR_LEARN_MORE ); + setIsSidebarOpened( isSidebarOpenedNew ); }; return ( diff --git a/src/OnboardingSPA/components/Sidebar/components/LearnMore/Sidebar.js b/src/OnboardingSPA/components/Sidebar/components/LearnMore/Sidebar.js index 0d3055c4f..d6bf8ecef 100644 --- a/src/OnboardingSPA/components/Sidebar/components/LearnMore/Sidebar.js +++ b/src/OnboardingSPA/components/Sidebar/components/LearnMore/Sidebar.js @@ -10,6 +10,7 @@ import { SIDEBAR_SLOTFILL_PREFIX, } from '../../../../../constants'; import SidebarSkeleton from './Skeleton/SidebarSkeleton'; +import { trackHiiveEvent } from '../../../../utils/analytics'; const LearnMoreSidebar = () => { const { currentStep } = useSelect( ( select ) => { @@ -22,6 +23,7 @@ const LearnMoreSidebar = () => { const closeSideBar = () => { setIsSidebarOpened( false ); + trackHiiveEvent( 'sidebar-closed', window.location.href ); }; return ( diff --git a/src/OnboardingSPA/components/SkipButton/index.js b/src/OnboardingSPA/components/SkipButton/index.js index d47cb3636..a68159a2a 100644 --- a/src/OnboardingSPA/components/SkipButton/index.js +++ b/src/OnboardingSPA/components/SkipButton/index.js @@ -8,6 +8,7 @@ import { setFlow } from '../../utils/api/flow'; import { store as nfdOnboardingStore } from '../../store'; import { getSettings, setSettings } from '../../utils/api/settings'; import { wpAdminPage, pluginDashboardPage } from '../../../constants'; +import { HiiveAnalytics } from '@newfold-labs/js-utility-ui-analytics'; const SkipButton = ( { callback = false } ) => { const navigate = useNavigate(); @@ -45,6 +46,7 @@ const SkipButton = ( { callback = false } ) => { if ( socialDataResp ) { setOnboardingSocialData( socialDataResp ); } + await HiiveAnalytics.dispatchEvents(); } setFlow( currentData ); } @@ -56,6 +58,13 @@ const SkipButton = ( { callback = false } ) => { window.location.replace( exitLink ); } + function skip() { + if ( typeof callback === 'function' ) { + callback(); + } + navigate( nextStep.path ); + } + function skipStep() { if ( isLastStep ) { return ( @@ -68,15 +77,7 @@ const SkipButton = ( { callback = false } ) => { ); } return ( - ); diff --git a/src/OnboardingSPA/components/StateHandlers/Ecommerce/index.js b/src/OnboardingSPA/components/StateHandlers/Ecommerce/index.js index af582f672..bdb5d735f 100644 --- a/src/OnboardingSPA/components/StateHandlers/Ecommerce/index.js +++ b/src/OnboardingSPA/components/StateHandlers/Ecommerce/index.js @@ -1,6 +1,6 @@ import { useViewportMatch } from '@wordpress/compose'; import { useSelect, useDispatch } from '@wordpress/data'; -import { useEffect, useState } from '@wordpress/element'; +import { useEffect } from '@wordpress/element'; import { store as nfdOnboardingStore } from '../../../store'; import { getPluginStatus } from '../../../utils/api/plugins'; @@ -22,10 +22,6 @@ const EcommerceStateHandler = ( { } ) => { const isLargeViewport = useViewportMatch( 'medium' ); - const [ woocommerceStatus, setWoocommerceStatus ] = useState( - PLUGIN_STATUS_INSTALLING - ); - const { storedPluginsStatus, brandName } = useSelect( ( select ) => { return { storedPluginsStatus: @@ -35,6 +31,7 @@ const EcommerceStateHandler = ( { }, [] ); const contents = getContents( brandName ); + const woocommerceStatus = storedPluginsStatus[ ECOMMERCE_STEPS_PLUGIN ]; const { updatePluginsStatus, @@ -58,7 +55,6 @@ const EcommerceStateHandler = ( { storedPluginsStatus[ ECOMMERCE_STEPS_PLUGIN ] = PLUGIN_STATUS_NOT_ACTIVE; updatePluginsStatus( storedPluginsStatus ); - return setWoocommerceStatus( PLUGIN_STATUS_NOT_ACTIVE ); } window.location.reload(); }, PLUGIN_INSTALL_WAIT_TIMEOUT ); @@ -98,7 +94,7 @@ const EcommerceStateHandler = ( { useEffect( () => { handleNavigationState( woocommerceStatus ); - }, [ woocommerceStatus ] ); + }, [ storedPluginsStatus ] ); const handlePluginsStatus = async ( pluginsStatus ) => { const pluginStatus = await checkPluginStatus(); @@ -111,13 +107,11 @@ const EcommerceStateHandler = ( { break; default: pluginsStatus[ ECOMMERCE_STEPS_PLUGIN ] = pluginStatus; - setWoocommerceStatus( pluginStatus ); updatePluginsStatus( pluginsStatus ); } }; useEffect( () => { - setWoocommerceStatus( storedPluginsStatus[ ECOMMERCE_STEPS_PLUGIN ] ); if ( storedPluginsStatus[ ECOMMERCE_STEPS_PLUGIN ] === PLUGIN_STATUS_INIT ) { diff --git a/src/OnboardingSPA/pages/Steps/DesignHomepageMenu/index.js b/src/OnboardingSPA/pages/Steps/DesignHomepageMenu/index.js index a4e4fab69..a23540bb6 100644 --- a/src/OnboardingSPA/pages/Steps/DesignHomepageMenu/index.js +++ b/src/OnboardingSPA/pages/Steps/DesignHomepageMenu/index.js @@ -18,6 +18,7 @@ import { LivePreviewSkeleton, GlobalStylesProvider, } from '../../../components/LivePreview'; +import { trackHiiveEvent } from '../../../utils/analytics'; const StepDesignHomepageMenu = () => { const location = useLocation(); @@ -91,11 +92,13 @@ const StepDesignHomepageMenu = () => { function saveDataForHomepage( idx ) { setSelectedHomepage( idx ); + const homepage = homepagePatternList[ idx ]; currentData.data.sitePages = { ...currentData.data.sitePages, - homepage: homepagePatternList[ idx ], + homepage, }; setCurrentOnboardingData( currentData ); + trackHiiveEvent( 'homepage-layout', homepage ); } useEffect( () => { diff --git a/src/OnboardingSPA/pages/Steps/DesignThemeStyles/Menu/index.js b/src/OnboardingSPA/pages/Steps/DesignThemeStyles/Menu/index.js index a17c0e0e6..fb8286157 100644 --- a/src/OnboardingSPA/pages/Steps/DesignThemeStyles/Menu/index.js +++ b/src/OnboardingSPA/pages/Steps/DesignThemeStyles/Menu/index.js @@ -22,6 +22,7 @@ import { LivePreviewSkeleton, } from '../../../../components/LivePreview'; import { addColorAndTypographyRoutes } from '../utils'; +import { trackHiiveEvent } from '../../../../utils/analytics'; const StepDesignThemeStylesMenu = () => { const content = getContents(); @@ -92,10 +93,19 @@ const StepDesignThemeStylesMenu = () => { setSelectedStyle( currentData.data.theme.variation ); setPattern( patternsResponse?.body ); setGlobalStyles( globalStylesResponse?.body ); + setSelectedStyle( currentData.data.theme.variation ); + if ( '' === currentData.data.theme.variation ) { + trackHiiveEvent( + 'default-style', + globalStylesResponse.body[ 0 ].title + ); + } }; useEffect( () => { - if ( themeStatus === THEME_STATUS_ACTIVE ) getStylesAndPatterns(); + if ( themeStatus === THEME_STATUS_ACTIVE ) { + getStylesAndPatterns(); + } }, [ themeStatus ] ); const handleClick = ( idx ) => { @@ -108,6 +118,7 @@ const StepDesignThemeStylesMenu = () => { currentData.data.theme.variation = selectedGlobalStyle.title; setCurrentOnboardingData( currentData ); navigate( nextStep.path ); + trackHiiveEvent( 'selected-style', selectedGlobalStyle.title ); }; const skiptoCustomPage = () => { @@ -123,7 +134,7 @@ const StepDesignThemeStylesMenu = () => { currentData.data.customDesign = true; setCurrentOnboardingData( currentData ); - + trackHiiveEvent( 'customize-design', true ); // Find the first Custom Conditional Step and navigate there navigate( conditionalSteps.designColors.path ); }; diff --git a/src/OnboardingSPA/pages/Steps/DesignThemeStyles/Preview/index.js b/src/OnboardingSPA/pages/Steps/DesignThemeStyles/Preview/index.js index fdaeb133a..adc991a3b 100644 --- a/src/OnboardingSPA/pages/Steps/DesignThemeStyles/Preview/index.js +++ b/src/OnboardingSPA/pages/Steps/DesignThemeStyles/Preview/index.js @@ -22,6 +22,7 @@ import { import { store as nfdOnboardingStore } from '../../../../store'; import { getPatterns } from '../../../../utils/api/patterns'; import { DesignStateHandler } from '../../../../components/StateHandlers'; +import { trackHiiveEvent } from '../../../../utils/analytics'; const StepDesignThemeStylesPreview = () => { const content = getContents(); @@ -109,12 +110,15 @@ const StepDesignThemeStylesPreview = () => { } if ( selected && 'click' === context ) { + trackHiiveEvent( 'customize-design', true ); navigate( conditionalSteps.designColors.path ); } }; useEffect( () => { - if ( themeStatus === THEME_STATUS_ACTIVE ) getStylesAndPatterns(); + if ( themeStatus === THEME_STATUS_ACTIVE ) { + getStylesAndPatterns(); + } }, [ themeStatus ] ); return ( diff --git a/src/OnboardingSPA/pages/Steps/Ecommerce/StepTax/index.js b/src/OnboardingSPA/pages/Steps/Ecommerce/StepTax/index.js index b6148feb9..31a45da52 100644 --- a/src/OnboardingSPA/pages/Steps/Ecommerce/StepTax/index.js +++ b/src/OnboardingSPA/pages/Steps/Ecommerce/StepTax/index.js @@ -15,6 +15,7 @@ import { store as nfdOnboardingStore } from '../../../../store'; import { useWPSettings as getWPSettings } from '../useWPSettings'; import Animate from '../../../../components/Animate'; import getContents from './contents'; +import { trackHiiveEvent } from '../../../../utils/analytics'; function createReverseLookup( state ) { return ( option ) => @@ -101,6 +102,8 @@ const StepTax = () => { }, }, } ); + + trackHiiveEvent( 'tax-information', selectedOption.content ); }; return ( diff --git a/src/OnboardingSPA/pages/Steps/GetStarted/GetStartedExperience/index.js b/src/OnboardingSPA/pages/Steps/GetStarted/GetStartedExperience/index.js index a426d0031..8b4bf0a55 100644 --- a/src/OnboardingSPA/pages/Steps/GetStarted/GetStartedExperience/index.js +++ b/src/OnboardingSPA/pages/Steps/GetStarted/GetStartedExperience/index.js @@ -13,6 +13,7 @@ import { useState, useEffect } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import Animate from '../../../../components/Animate'; import getContents from './contents'; +import { trackHiiveEvent } from '../../../../utils/analytics'; const GetStartedExperience = () => { const [ wpComfortLevel, setWpComfortLevel ] = useState( '0' ); @@ -52,6 +53,12 @@ const GetStartedExperience = () => { const currentDataCopy = currentData; currentDataCopy.data.wpComfortLevel = value || '0'; setCurrentOnboardingData( currentDataCopy ); + trackHiiveEvent( + 'wp-experience', + content.options.filter( ( option ) => { + return option.value === value; + } )[ 0 ].label + ); }; const content = getContents(); diff --git a/src/OnboardingSPA/pages/Steps/GetStarted/SiteTypeSetup/PrimarySite/index.js b/src/OnboardingSPA/pages/Steps/GetStarted/SiteTypeSetup/PrimarySite/index.js index 32d07ab69..e968550b1 100644 --- a/src/OnboardingSPA/pages/Steps/GetStarted/SiteTypeSetup/PrimarySite/index.js +++ b/src/OnboardingSPA/pages/Steps/GetStarted/SiteTypeSetup/PrimarySite/index.js @@ -13,6 +13,7 @@ import NavCardButton from '../../../../../components/Button/NavCardButton'; import NeedHelpTag from '../../../../../components/NeedHelpTag'; import Animate from '../../../../../components/Animate'; import { getSiteClassification } from '../../../../../utils/api/siteClassification'; +import { trackHiiveEvent } from '../../../../../utils/analytics'; const StepPrimarySetup = () => { const { @@ -41,6 +42,8 @@ const StepPrimarySetup = () => { const [ custom, setCustom ] = useState( false ); const [ siteClassification, setSiteClassification ] = useState(); const [ primaryCategory, setPrimaryCategory ] = useState( '' ); + // Timeout after which a custom input analytics event will be sent. + const [ typingTimeout, setTypingTimeout ] = useState(); const content = getContents(); @@ -87,6 +90,7 @@ const StepPrimarySetup = () => { currentData.data.siteType.primary.refers = 'slug'; currentData.data.siteType.primary.value = primType; setCurrentOnboardingData( currentData ); + trackHiiveEvent( 'primary-type', currentData.data.siteType.primary ); }; /** @@ -96,10 +100,21 @@ const StepPrimarySetup = () => { */ const categoryInput = ( value ) => { setCustom( true ); - setPrimaryCategory( value ); currentData.data.siteType.primary.refers = 'custom'; currentData.data.siteType.primary.value = value; setCurrentOnboardingData( currentData ); + if ( '' !== primaryCategory && primaryCategory !== value ) { + clearTimeout( typingTimeout ); + setTypingTimeout( + setTimeout( () => { + trackHiiveEvent( + 'primary-type', + currentData.data.siteType.primary + ); + }, 1000 ) + ); + } + setPrimaryCategory( value ); }; const primarySiteTypeChips = () => { diff --git a/src/OnboardingSPA/pages/Steps/GetStarted/SiteTypeSetup/SecondarySite/index.js b/src/OnboardingSPA/pages/Steps/GetStarted/SiteTypeSetup/SecondarySite/index.js index d3a1ca259..dd5b11835 100644 --- a/src/OnboardingSPA/pages/Steps/GetStarted/SiteTypeSetup/SecondarySite/index.js +++ b/src/OnboardingSPA/pages/Steps/GetStarted/SiteTypeSetup/SecondarySite/index.js @@ -13,6 +13,7 @@ import NavCardButton from '../../../../../components/Button/NavCardButton'; import NeedHelpTag from '../../../../../components/NeedHelpTag'; import Animate from '../../../../../components/Animate'; import { getSiteClassification } from '../../../../../utils/api/siteClassification'; +import { trackHiiveEvent } from '../../../../../utils/analytics'; const StepPrimarySetup = () => { const { @@ -37,6 +38,8 @@ const StepPrimarySetup = () => { const [ primaryTypesList, setPrimaryTypeList ] = useState(); const [ primaryCategory, setPrimaryCategory ] = useState(); const [ secondaryCategory, setSecondaryCategory ] = useState( '' ); + // Timeout after which a custom input analytics event will be sent. + const [ typingTimeout, setTypingTimeout ] = useState(); const { currentData } = useSelect( ( select ) => { return { @@ -125,10 +128,21 @@ const StepPrimarySetup = () => { */ const categoryInput = ( value ) => { setCustom( true ); - setSecondaryCategory( value ); currentData.data.siteType.secondary.refers = 'custom'; currentData.data.siteType.secondary.value = value; setCurrentOnboardingData( currentData ); + if ( '' !== secondaryCategory && secondaryCategory !== value ) { + clearTimeout( typingTimeout ); + setTypingTimeout( + setTimeout( () => { + trackHiiveEvent( + 'secondary-type', + currentData.data.siteType.secondary + ); + }, 1000 ) + ); + } + setSecondaryCategory( value ); }; /** @@ -137,6 +151,12 @@ const StepPrimarySetup = () => { * @param {string} secType */ const handleCategoryClick = ( secType ) => { + if ( + secondaryCategory === secType && + currentData.data.siteType.primary.value === primaryCategory + ) { + return true; + } setCustom( false ); setSecondaryCategory( secType ); currentData.data.siteType.primary.refers = 'slug'; @@ -144,29 +164,38 @@ const StepPrimarySetup = () => { currentData.data.siteType.primary.value = primaryCategory; currentData.data.siteType.secondary.value = secType; setCurrentOnboardingData( currentData ); + trackHiiveEvent( + 'secondary-type', + currentData.data.siteType.secondary + ); }; const changePrimaryType = ( direction ) => { const idx = primaryTypesList.findIndex( ( val ) => primaryCategory === val ); + let primaryType; switch ( direction ) { case 'back': // idx = ( (idx - 1 + N) % N ) - setPrimaryCategory( + primaryType = primaryTypesList[ ( idx - 1 + primaryTypesList.length ) % primaryTypesList.length - ] - ); + ]; + setPrimaryCategory( primaryType ); break; case 'next': // idx = ( (idx + 1 ) % N ) - setPrimaryCategory( - primaryTypesList[ ( idx + 1 ) % primaryTypesList.length ] - ); + primaryType = + primaryTypesList[ ( idx + 1 ) % primaryTypesList.length ]; + setPrimaryCategory( primaryType ); break; } + trackHiiveEvent( 'primary-type', { + refers: 'slug', + value: primaryType, + } ); }; const secondarySiteTypeChips = () => { @@ -211,28 +240,28 @@ const StepPrimarySetup = () => {
{ primaryTypesList && primaryTypesList.length > 1 && ( -
- - changePrimaryType( - 'back' - ) - } - onKeyUp={ () => - changePrimaryType( - 'back' - ) - } - role="button" - tabIndex={ 0 } - style={ { - backgroundImage: +
+ + changePrimaryType( + 'back' + ) + } + onKeyUp={ () => + changePrimaryType( + 'back' + ) + } + role="button" + tabIndex={ 0 } + style={ { + backgroundImage: 'var(--chevron-left-icon)', - } } - /> -
- ) } + } } + /> +
+ ) }
{
{ primaryTypesList && primaryTypesList.length > 1 && ( -
- - changePrimaryType( - 'next' - ) - } - onKeyUp={ () => - changePrimaryType( - 'next' - ) - } - role="button" - tabIndex={ 0 } - style={ { - backgroundImage: +
+ + changePrimaryType( + 'next' + ) + } + onKeyUp={ () => + changePrimaryType( + 'next' + ) + } + role="button" + tabIndex={ 0 } + style={ { + backgroundImage: 'var(--chevron-right-icon)', - } } - /> -
- ) } + } } + /> +
+ ) }
) }
diff --git a/src/OnboardingSPA/pages/Steps/TopPriority/index.js b/src/OnboardingSPA/pages/Steps/TopPriority/index.js index ffeb3ce32..06fdb23ea 100644 --- a/src/OnboardingSPA/pages/Steps/TopPriority/index.js +++ b/src/OnboardingSPA/pages/Steps/TopPriority/index.js @@ -10,6 +10,7 @@ import CommonLayout from '../../../components/Layouts/Common'; import HeadingWithSubHeading from '../../../components/HeadingWithSubHeading'; import SelectableCardList from '../../../components/SelectableCardList/selectable-card-list'; import getContents from './contents'; +import { trackHiiveEvent } from '../../../utils/analytics'; const StepTopPriority = () => { const priorityTypes = { @@ -83,6 +84,7 @@ const StepTopPriority = () => { const selectedPriorityType = priorityTypes[ selected ]; currentData.data.topPriority.priority1 = selectedPriorityType; setCurrentOnboardingData( currentData ); + trackHiiveEvent( 'top-priority', priorityTypes[ selected ] ); if ( 'selling' === selectedPriorityType ) { handleSelling(); } else { @@ -94,6 +96,10 @@ const StepTopPriority = () => { window.nfdOnboarding.newFlow = undefined; currentData.data.topPriority.priority1 = priorityTypes[ 0 ]; setCurrentOnboardingData( currentData ); + trackHiiveEvent( + 'top-priority-skipped', + priorityTypes[ 0 ] + ); }; const content = getContents(); diff --git a/src/OnboardingSPA/utils/analytics/index.js b/src/OnboardingSPA/utils/analytics/index.js new file mode 100644 index 000000000..58fd001d8 --- /dev/null +++ b/src/OnboardingSPA/utils/analytics/index.js @@ -0,0 +1,11 @@ +import { HiiveAnalytics, HiiveEvent } from '@newfold-labs/js-utility-ui-analytics'; +import { HIIVE_ANALYTICS_CATEGORY } from '../../../constants'; + +export const trackHiiveEvent = ( action, value ) => { + const hiiveEvent = new HiiveEvent( HIIVE_ANALYTICS_CATEGORY, action, { + value, + timestamp: Date.now(), + } ); + + HiiveAnalytics.track( hiiveEvent ); +}; diff --git a/src/OnboardingSPA/utils/api/events.js b/src/OnboardingSPA/utils/api/events.js deleted file mode 100644 index b4b40cec2..000000000 --- a/src/OnboardingSPA/utils/api/events.js +++ /dev/null @@ -1,25 +0,0 @@ -import apiFetch from '@wordpress/api-fetch'; - -import { onboardingRestURL } from './common'; - -class Event { - constructor( eventSlug, eventData = {} ) { - this.eventSlug = eventSlug; - this.eventData = eventData; - } - - send() { - apiFetch( { - url: onboardingRestURL( 'events' ), - method: 'POST', - data: { - slug: this.eventSlug, - data: this.eventData, - }, - } ).catch( ( error ) => { - console.error( error ); - } ); - } -} - -export default Event; diff --git a/src/constants.js b/src/constants.js index eed2f4c2b..75a0432b9 100644 --- a/src/constants.js +++ b/src/constants.js @@ -51,6 +51,7 @@ export const PLUGIN_STATUS_NOT_ACTIVE = 'inactive'; export const PLUGIN_STATUS_INSTALLING = 'installing'; export const PLUGIN_STATUS_ACTIVE = 'activated'; export const PLUGIN_INSTALL_WAIT_TIMEOUT = 30000; +export const HIIVE_ANALYTICS_CATEGORY = 'wp-onboarding'; /** * All views for the component. diff --git a/src/onboarding.js b/src/onboarding.js index 8a78591fe..0f54397e6 100644 --- a/src/onboarding.js +++ b/src/onboarding.js @@ -5,9 +5,23 @@ import { NFD_ONBOARDING_ELEMENT_ID, runtimeDataExists } from './constants'; import domReady from '@wordpress/dom-ready'; import { registerCoreBlocks } from '@wordpress/block-library'; import initializeNFDOnboarding from './OnboardingSPA'; +import { HiiveAnalytics } from '@newfold-labs/js-utility-ui-analytics'; +import { onboardingRestURL } from './OnboardingSPA/utils/api/common'; if ( runtimeDataExists ) { domReady( () => { + HiiveAnalytics.initialize( { + urls: { + single: onboardingRestURL( 'events' ), + batch: onboardingRestURL( 'events/batch' ), + }, + settings: { + debounce: { + time: 3000, + }, + }, + } ); + initializeNFDOnboarding( NFD_ONBOARDING_ELEMENT_ID, window.nfdOnboarding @@ -15,6 +29,7 @@ if ( runtimeDataExists ) { registerCoreBlocks(); } ); } else { + /* eslint-disable no-console */ console.log( 'Cannot find Newfold Onboarding runtime data to set __webpack_public_path__.' );