diff --git a/projects/plugins/boost/app/assets/src/js/utils/analytics.ts b/projects/plugins/boost/app/assets/src/js/utils/analytics.ts index 524dbaddb779..a670e94e6d7f 100644 --- a/projects/plugins/boost/app/assets/src/js/utils/analytics.ts +++ b/projects/plugins/boost/app/assets/src/js/utils/analytics.ts @@ -7,16 +7,24 @@ export async function recordBoostEvent( eventProp: TracksEventProperties ): Promise< RecordSuccess > { const defaultProps: { [ key: string ]: string } = {}; - if ( 'version' in Jetpack_Boost ) { - defaultProps.boost_version = Jetpack_Boost.version; - } - if ( 'connection' in Jetpack_Boost ) { - defaultProps.jetpack_connection = Jetpack_Boost.connection.connected - ? 'connected' - : 'disconnected'; - } - if ( 'optimizations' in Jetpack_Boost ) { - defaultProps.optimizations = JSON.stringify( Jetpack_Boost.optimizations ); + + /** + * Jetpack Boost constant is not available on the front end. + * + * So we need to check if it exists before using it in case this function is called from the front end. + */ + if ( typeof Jetpack_Boost !== 'undefined' ) { + if ( 'version' in Jetpack_Boost ) { + defaultProps.boost_version = Jetpack_Boost.version; + } + if ( 'connection' in Jetpack_Boost ) { + defaultProps.jetpack_connection = Jetpack_Boost.connection.connected + ? 'connected' + : 'disconnected'; + } + if ( 'optimizations' in Jetpack_Boost ) { + defaultProps.optimizations = JSON.stringify( Jetpack_Boost.optimizations ); + } } eventProp = { ...defaultProps, ...eventProp }; diff --git a/projects/plugins/boost/app/features/image-guide/Image_Guide.php b/projects/plugins/boost/app/features/image-guide/Image_Guide.php index 90a5f1351401..345f301c5f7b 100644 --- a/projects/plugins/boost/app/features/image-guide/Image_Guide.php +++ b/projects/plugins/boost/app/features/image-guide/Image_Guide.php @@ -4,6 +4,7 @@ use Automattic\Jetpack_Boost\Admin\Admin; use Automattic\Jetpack_Boost\Contracts\Feature; +use Automattic\Jetpack_Boost\Lib\Analytics; class Image_Guide implements Feature { @@ -23,6 +24,10 @@ public function setup() { add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) ); + // Enqueue the tracks library. + $tracks = Analytics::get_tracking(); + add_action( 'wp_enqueue_scripts', array( $tracks, 'enqueue_tracks_scripts' ) ); + /** * The priority determines where the admin bar menu item is placed. */ diff --git a/projects/plugins/boost/app/features/image-guide/src/analytics.ts b/projects/plugins/boost/app/features/image-guide/src/analytics.ts new file mode 100644 index 000000000000..391376e0b9cd --- /dev/null +++ b/projects/plugins/boost/app/features/image-guide/src/analytics.ts @@ -0,0 +1,122 @@ +import { get } from 'svelte/store'; +import { recordBoostEvent } from '../../../assets/src/js/utils/analytics'; +import { guideState } from './stores/GuideState'; +import { MeasurableImageStore } from './stores/MeasurableImageStore'; + +type ImageProperties = { + severity: 'red' | 'yellow' | 'green'; + oversizedRatio: number; + fileWeight: number; + fileWidth: number; + fileHeight: number; + sizeOnPageWidth: number; + sizeOnPageHeight: number; + expectedWidth: number; + expectedHeight: number; + potentialSavings: number | null; + imageURL: string; +}; + +export default class ImageGuideAnalytics { + static trackingComplete = false; + + /** + * Track the image guide analytics for a single image. + * + * @param { MeasurableImageStore } imageStore + */ + public static async trackImageOutcome( + imageStore: MeasurableImageStore + ): Promise< ImageProperties > { + return new Promise( resolve => { + // Wait until the image is loaded and then track the state. + imageStore.loading.subscribe( loading => { + if ( ! loading ) { + const oversizedRatio = get( imageStore.oversizedRatio ); + const severity = oversizedRatio > 4 ? 'red' : oversizedRatio > 2.5 ? 'yellow' : 'green'; + const fileSize = get( imageStore.fileSize ); + const sizeOnPage = get( imageStore.sizeOnPage ); + const expectedSize = get( imageStore.expectedSize ); + const potentialSavings = get( imageStore.potentialSavings ); + const imageURL = get( imageStore.url ); + + const props: ImageProperties = { + severity, + oversizedRatio, + fileWeight: fileSize.weight, + fileWidth: fileSize.width, + fileHeight: fileSize.height, + sizeOnPageWidth: sizeOnPage.width, + sizeOnPageHeight: sizeOnPage.height, + expectedWidth: expectedSize.width, + expectedHeight: expectedSize.height, + potentialSavings, + imageURL, + }; + + recordBoostEvent( 'image_guide_image_outcome', { + ...props, + windowWidth: window.innerWidth, + windowHeight: window.innerHeight, + devicePixelRatio: window.devicePixelRatio, + } ); + + resolve( props ); + } + } ); + } ); + } + + /** + * Track events to record the outcome of the image guide for the current page. + * + * @param imageStores + */ + public static async trackPage( imageStores: MeasurableImageStore[] ) { + if ( ! imageStores.length || ImageGuideAnalytics.trackingComplete ) { + return; + } + + // Flag to indicate that tracking has already ran for the current page load + ImageGuideAnalytics.trackingComplete = true; + + // Record events for each image and collect promises with properties of image that will resolve when the image is loaded. + const promises: Promise< ImageProperties >[] = imageStores.map( + ImageGuideAnalytics.trackImageOutcome + ); + + // Wait for all images to be loaded and then track the overall outcome. + const results = await Promise.all( promises ); + const totalPotentialSavings = results.reduce( ( total, result ) => { + return total + ( result.potentialSavings || 0 ); + }, 0 ); + + recordBoostEvent( 'image_guide_page_outcome', { + totalPotentialSavings, + redSeverityCount: results.filter( result => result.severity === 'red' ).length, + yellowSeverityCount: results.filter( result => result.severity === 'yellow' ).length, + greenSeverityCount: results.filter( result => result.severity === 'green' ).length, + windowWidth: window.innerWidth, + windowHeight: window.innerHeight, + devicePixelRatio: window.devicePixelRatio, + } ); + } + + /** + * Track the state of the UI when the user loads a page. + */ + public static trackInitialState() { + recordBoostEvent( 'image_guide_initial_ui_state', { + imageGuideState: get( guideState ), + } ); + } + + /** + * Track the state of the UI when the user changes it. + */ + public static trackUIStateChange() { + recordBoostEvent( 'image_guide_ui_state_change', { + imageGuideState: get( guideState ), + } ); + } +} diff --git a/projects/plugins/boost/app/features/image-guide/src/index.ts b/projects/plugins/boost/app/features/image-guide/src/index.ts index dcbd36ff62c9..13b94497bd38 100644 --- a/projects/plugins/boost/app/features/image-guide/src/index.ts +++ b/projects/plugins/boost/app/features/image-guide/src/index.ts @@ -1,4 +1,5 @@ import { getMeasurableImages } from '@automattic/jetpack-image-guide'; +import ImageGuideAnalytics from './analytics'; import { attachGuides } from './initialize'; import { guideState } from './stores/GuideState'; import AdminBarToggle from './ui/AdminBarToggle.svelte'; @@ -78,6 +79,8 @@ function debounceDimensionUpdates() { * to look for new images. */ function initialize() { + ImageGuideAnalytics.trackInitialState(); + guideState.subscribe( async $state => { if ( $state === 'paused' ) { return; @@ -91,6 +94,8 @@ function initialize() { ); const filteredImages = discardSmallImages( measurableImages ); stores.push( ...attachGuides( filteredImages ) ); + + ImageGuideAnalytics.trackPage( stores ); } ); } diff --git a/projects/plugins/boost/app/features/image-guide/src/ui/AdminBarToggle.svelte b/projects/plugins/boost/app/features/image-guide/src/ui/AdminBarToggle.svelte index 2aac710763c5..0be1402dbded 100644 --- a/projects/plugins/boost/app/features/image-guide/src/ui/AdminBarToggle.svelte +++ b/projects/plugins/boost/app/features/image-guide/src/ui/AdminBarToggle.svelte @@ -1,4 +1,5 @@ diff --git a/projects/plugins/boost/changelog/add-image-guide-tracks b/projects/plugins/boost/changelog/add-image-guide-tracks new file mode 100644 index 000000000000..9baa990daec1 --- /dev/null +++ b/projects/plugins/boost/changelog/add-image-guide-tracks @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Added tracking that helps understand the effectiveness of image-guide