Back

97.14% Statements 34/35
87.5% Branches 14/16
92.85% Functions 13/14
100% Lines 34/34

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160                                                          4x 2x 2x 1x     1x       20x       2x                       4x 4x 4x   4x 10x   8x 4x               4x 4x   4x                         4x             4x 4x             4x                   2x 1x       1x     1x         1x 1x 3x     1x   3x 3x 3x                     1x                 1x          
import { observe } from './stores/facade.ts';
import { selectors } from './stores/store.ts';
import { MeasurableImageStore } from './stores/MeasurableImageStore.ts';
 
/**
 * Image properties sent to tracks.
 */
type ImageProperties = {
	severity: 'red' | 'yellow' | 'green';
	oversized_ratio: number;
	file_width: number;
	file_height: number;
	size_on_page_width: number;
	size_on_page_height: number;
	expected_width: number;
	expected_height: number;
	potential_savings: number | null;
	image_url: string;
};
 
export type TracksCallback = ( event: string, props: { [ key: string ]: string | number } ) => void;
 
/**
 * Return a 'red', 'green' or 'yellow' severity based on the oversized ratio.
 *
 * @param { number } oversizedRatio - The ratio of the image size to the expected size.
 * @return { 'red' | 'yellow' | 'green' } The severity of the image.
 */
function getSeverity( oversizedRatio: number ) {
	if ( oversizedRatio > 4 ) {
		return 'red';
	} else if ( oversizedRatio > 2.5 ) {
		return 'yellow';
	}
 
	return 'green';
}
 
export default class ImageGuideAnalytics {
	static trackingComplete = false;
	static tracksCallback: TracksCallback;
 
	public static setTracksCallback( callback ) {
		ImageGuideAnalytics.tracksCallback = callback;
	}
 
	/**
	 * Track the image guide analytics for a single image.
	 *
	 * @param { MeasurableImageStore } imageStore - The image store to track.
	 * @return { Promise< ImageProperties > } Promise that resolves with the properties of the image.
	 */
	public static async trackImageOutcome(
		imageStore: MeasurableImageStore
	): Promise< ImageProperties > {
		return new Promise( resolve => {
			let unsubscribe = () => {};
			let emitted = false;
			// Wait until the image is loaded and then track the state.
			unsubscribe = observe(
				() => selectors.getImageFacts( imageStore.id ).loading,
				loading => {
					if ( ! loading && ! emitted ) {
						emitted = true;
						const {
							oversizedRatio,
							fileSize,
							sizeOnPage,
							expectedSize,
							potentialSavings,
							url: imageURL,
						} = imageStore.getSnapshot();
						const severity = getSeverity( oversizedRatio );
 
						const props: ImageProperties = {
							severity,
							oversized_ratio: oversizedRatio,
							file_width: fileSize.width,
							file_height: fileSize.height,
							size_on_page_width: sizeOnPage.width,
							size_on_page_height: sizeOnPage.height,
							expected_width: expectedSize.width,
							expected_height: expectedSize.height,
							potential_savings: potentialSavings,
							image_url: imageURL,
						};
 
						ImageGuideAnalytics.tracksCallback( 'image_guide_image_outcome', {
							...props,
							window_width: window.innerWidth,
							window_height: window.innerHeight,
							device_pixel_ratio: window.devicePixelRatio,
						} );
 
						resolve( props );
						unsubscribe();
					}
				},
				undefined,
				undefined,
				imageStore.id
			);
			Iif ( emitted ) unsubscribe();
		} );
	}
 
	/**
	 * Track events to record the outcome of the image guide for the current page.
	 *
	 * @param {MeasurableImageStore[]} imageStores - The image stores to track.
	 */
	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.potential_savings || 0 );
		}, 0 );
 
		ImageGuideAnalytics.tracksCallback( 'image_guide_page_outcome', {
			total_potential_savings: totalPotentialSavings,
			red_severity_count: results.filter( result => result.severity === 'red' ).length,
			yellow_severity_count: results.filter( result => result.severity === 'yellow' ).length,
			green_severity_count: results.filter( result => result.severity === 'green' ).length,
			window_width: window.innerWidth,
			window_height: window.innerHeight,
			device_pixel_ratio: window.devicePixelRatio,
		} );
	}
 
	/**
	 * Track the state of the UI when the user loads a page.
	 */
	public static trackInitialState() {
		ImageGuideAnalytics.tracksCallback( 'image_guide_initial_ui_state', {
			image_guide_state: selectors.getGuideState(),
		} );
	}
 
	/**
	 * Track the state of the UI when the user changes it.
	 */
	public static trackUIStateChange() {
		ImageGuideAnalytics.tracksCallback( 'image_guide_ui_state_change', {
			image_guide_state: selectors.getGuideState(),
		} );
	}
}