From 6f9d424e369af22ec7232664d169f577925b2d2a Mon Sep 17 00:00:00 2001 From: Adnan Haque Date: Thu, 22 Dec 2022 11:14:21 +0600 Subject: [PATCH 1/5] Track image-guide ui toggle --- .../app/assets/src/js/utils/analytics.ts | 28 ++++++++++++------- .../app/features/image-guide/Image_Guide.php | 5 ++++ .../image-guide/src/ui/AdminBarToggle.svelte | 2 ++ 3 files changed, 25 insertions(+), 10 deletions(-) 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/ui/AdminBarToggle.svelte b/projects/plugins/boost/app/features/image-guide/src/ui/AdminBarToggle.svelte index 2aac710763c5..7fb72bdd7653 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 @@ From 4152736a6be8c7f7f4f260a9ade91f0839b11a02 Mon Sep 17 00:00:00 2001 From: Adnan Haque Date: Thu, 22 Dec 2022 15:38:05 +0600 Subject: [PATCH 2/5] Track image guide images and page Signed-off-by: Adnan Haque --- .../app/features/image-guide/src/analytics.ts | 96 +++++++++++++++++++ .../app/features/image-guide/src/index.ts | 3 + 2 files changed, 99 insertions(+) create mode 100644 projects/plugins/boost/app/features/image-guide/src/analytics.ts 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..c61fcc92e1cf --- /dev/null +++ b/projects/plugins/boost/app/features/image-guide/src/analytics.ts @@ -0,0 +1,96 @@ +import { get } from 'svelte/store'; +import { recordBoostEvent } from '../../../assets/src/js/utils/analytics'; +import { MeasurableImageStore } from './stores/MeasurableImageStore'; +import type { Dimensions, Weight } from '@automattic/jetpack-image-guide'; + +type ImageProperties = { + severity: 'red' | 'yellow' | 'green'; + oversizedRatio: number; + fileSize: Dimensions & Weight; + sizeOnPage: Dimensions; + expectedSize: Dimensions; + 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, + fileSize, + sizeOnPage, + expectedSize, + potentialSavings, + imageURL, + }; + + recordBoostEvent( 'image_guide_image_state', { + ...props, + windowDimensions: { + width: window.innerWidth, + height: 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, + windowDimensions: { + width: window.innerWidth, + height: window.innerHeight, + }, + devicePixelRatio: window.devicePixelRatio, + } ); + } +} \ No newline at end of file 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..a3491cd644ef 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'; @@ -91,6 +92,8 @@ function initialize() { ); const filteredImages = discardSmallImages( measurableImages ); stores.push( ...attachGuides( filteredImages ) ); + + ImageGuideAnalytics.trackPage( stores ); } ); } From 7659aee82b08222b6f4ddef2ee7b063298a78d79 Mon Sep 17 00:00:00 2001 From: Adnan Haque Date: Thu, 22 Dec 2022 15:53:46 +0600 Subject: [PATCH 3/5] Add initial state and page impact tracking to image guide Signed-off-by: Adnan Haque --- .../app/features/image-guide/src/analytics.ts | 21 ++++++++++++++++++- .../app/features/image-guide/src/index.ts | 2 ++ .../image-guide/src/ui/AdminBarToggle.svelte | 4 ++-- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/projects/plugins/boost/app/features/image-guide/src/analytics.ts b/projects/plugins/boost/app/features/image-guide/src/analytics.ts index c61fcc92e1cf..b99e7f1ba64d 100644 --- a/projects/plugins/boost/app/features/image-guide/src/analytics.ts +++ b/projects/plugins/boost/app/features/image-guide/src/analytics.ts @@ -1,5 +1,6 @@ import { get } from 'svelte/store'; import { recordBoostEvent } from '../../../assets/src/js/utils/analytics'; +import { guideState } from './stores/GuideState'; import { MeasurableImageStore } from './stores/MeasurableImageStore'; import type { Dimensions, Weight } from '@automattic/jetpack-image-guide'; @@ -44,7 +45,7 @@ export default class ImageGuideAnalytics { imageURL, }; - recordBoostEvent( 'image_guide_image_state', { + recordBoostEvent( 'image_guide_image_outcome', { ...props, windowDimensions: { width: window.innerWidth, @@ -93,4 +94,22 @@ export default class ImageGuideAnalytics { 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 ) + } ); + } } \ No newline at end of file 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 a3491cd644ef..13b94497bd38 100644 --- a/projects/plugins/boost/app/features/image-guide/src/index.ts +++ b/projects/plugins/boost/app/features/image-guide/src/index.ts @@ -79,6 +79,8 @@ function debounceDimensionUpdates() { * to look for new images. */ function initialize() { + ImageGuideAnalytics.trackInitialState(); + guideState.subscribe( async $state => { if ( $state === 'paused' ) { return; 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 7fb72bdd7653..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,5 +1,5 @@ From 59c495e45b484623b3cb7f6016eec6dd0be898c1 Mon Sep 17 00:00:00 2001 From: Adnan Haque Date: Thu, 22 Dec 2022 15:57:37 +0600 Subject: [PATCH 4/5] changelog --- projects/plugins/boost/changelog/add-image-guide-tracks | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/plugins/boost/changelog/add-image-guide-tracks 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 From ad4df57f768e12406859c3d349c6e91e6e88f7d0 Mon Sep 17 00:00:00 2001 From: Adnan Haque Date: Thu, 22 Dec 2022 17:06:00 +0600 Subject: [PATCH 5/5] Fix build issues --- .../app/features/image-guide/src/analytics.ts | 67 ++++++++++--------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/projects/plugins/boost/app/features/image-guide/src/analytics.ts b/projects/plugins/boost/app/features/image-guide/src/analytics.ts index b99e7f1ba64d..391376e0b9cd 100644 --- a/projects/plugins/boost/app/features/image-guide/src/analytics.ts +++ b/projects/plugins/boost/app/features/image-guide/src/analytics.ts @@ -2,14 +2,17 @@ import { get } from 'svelte/store'; import { recordBoostEvent } from '../../../assets/src/js/utils/analytics'; import { guideState } from './stores/GuideState'; import { MeasurableImageStore } from './stores/MeasurableImageStore'; -import type { Dimensions, Weight } from '@automattic/jetpack-image-guide'; type ImageProperties = { severity: 'red' | 'yellow' | 'green'; oversizedRatio: number; - fileSize: Dimensions & Weight; - sizeOnPage: Dimensions; - expectedSize: Dimensions; + fileWeight: number; + fileWidth: number; + fileHeight: number; + sizeOnPageWidth: number; + sizeOnPageHeight: number; + expectedWidth: number; + expectedHeight: number; potentialSavings: number | null; imageURL: string; }; @@ -22,11 +25,13 @@ export default class ImageGuideAnalytics { * * @param { MeasurableImageStore } imageStore */ - public static async trackImageOutcome( imageStore: MeasurableImageStore ) : Promise < ImageProperties > { + 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 ) { + if ( ! loading ) { const oversizedRatio = get( imageStore.oversizedRatio ); const severity = oversizedRatio > 4 ? 'red' : oversizedRatio > 2.5 ? 'yellow' : 'green'; const fileSize = get( imageStore.fileSize ); @@ -34,63 +39,65 @@ export default class ImageGuideAnalytics { const expectedSize = get( imageStore.expectedSize ); const potentialSavings = get( imageStore.potentialSavings ); const imageURL = get( imageStore.url ); - + const props: ImageProperties = { severity, oversizedRatio, - fileSize, - sizeOnPage, - expectedSize, + 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, - windowDimensions: { - width: window.innerWidth, - height: window.innerHeight, - }, + 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 ) { + 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 ); - + 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); + 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, - windowDimensions: { - width: window.innerWidth, - height: window.innerHeight, - }, + windowWidth: window.innerWidth, + windowHeight: window.innerHeight, devicePixelRatio: window.devicePixelRatio, } ); } @@ -100,7 +107,7 @@ export default class ImageGuideAnalytics { */ public static trackInitialState() { recordBoostEvent( 'image_guide_initial_ui_state', { - imageGuideState: get( guideState ) + imageGuideState: get( guideState ), } ); } @@ -109,7 +116,7 @@ export default class ImageGuideAnalytics { */ public static trackUIStateChange() { recordBoostEvent( 'image_guide_ui_state_change', { - imageGuideState: get( guideState ) + imageGuideState: get( guideState ), } ); } -} \ No newline at end of file +}