From ad58e6b3f3bf56f3b8b3156641a3dc3315554b9c Mon Sep 17 00:00:00 2001 From: Mehmood Ahmad <31419912+mehmoodak@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:36:23 +0500 Subject: [PATCH] Add Tracks Analytics on frontend (#185) Add Tracks Analytics by using the `@automattic/calypso-analytics` package. With each track event, the following properties will be tracked: - Properties related to VIP Platform (e.g. org_id, env_id). - Properties related to Remote Data Blocks (e.g. plugin_version). - Event specific properties. **Note:** This tracking will only work if site is hosted on VIP Platform or if the analytics is enabled via filter. --- .eslintrc.js | 1 + .husky/pre-commit | 4 +- .lintstagedrc.json | 2 +- .../BlockManagement/BlockRegistration.php | 3 + package-lock.json | 61 +++++++++++++---- package.json | 5 +- .../components/BlockBindingControls.tsx | 22 +++++- .../field-shortcode/FieldShortcodeButton.tsx | 22 +++++- .../FieldShortcodeSelectExisting.tsx | 4 +- .../FieldShortcodeSelectFieldPopover.tsx | 17 +++-- .../FieldShortcodeSelectMeta.tsx | 4 +- .../FieldShortcodeSelection.tsx | 2 + .../components/modals/InputModal.tsx | 12 +++- .../components/modals/SearchModal.tsx | 13 +++- .../components/panels/DataPanel.tsx | 20 +++++- .../components/panels/OverridesPanel.tsx | 8 +++ .../pattern-selection/PatternSelection.tsx | 19 +++++- src/blocks/remote-data-container/edit.tsx | 10 +-- .../filters/withBlockBinding.tsx | 1 + .../utils/tracks.spec.ts | 68 +++++++++++++++++++ .../remote-data-container/utils/tracks.ts | 53 +++++++++++++++ .../components/PatternEditorSettingsPanel.tsx | 14 ++-- src/utils/localized-block-data.ts | 15 ++++ .../components/modals/InputModal.test.tsx | 11 +++ .../filters/withBlockBinding.test.tsx | 3 +- types/localized-block-data.d.ts | 2 + types/remote-data.d.ts | 2 + types/tracks.d.ts | 13 ++++ 28 files changed, 365 insertions(+), 46 deletions(-) create mode 100644 src/blocks/remote-data-container/utils/tracks.spec.ts create mode 100644 src/blocks/remote-data-container/utils/tracks.ts create mode 100644 types/tracks.d.ts diff --git a/.eslintrc.js b/.eslintrc.js index e58b1c2a..031c4818 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,5 +4,6 @@ module.exports = { extends: [ 'plugin:@automattic/wpvip/recommended' ], globals: { REMOTE_DATA_BLOCKS: 'readonly', + REMOTE_DATA_BLOCKS_SETTINGS: 'readonly', }, }; diff --git a/.husky/pre-commit b/.husky/pre-commit index db1cd24e..6be2ae0a 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,6 +1,6 @@ if [ -d "$(git rev-parse --git-path rebase-merge)" ] || [ -d "$(git rev-parse --git-path rebase-apply)" ]; then - echo "Rebase in progress. Skipping pre-commit hook.\n"; - exit 0; + echo "Rebase in progress. Skipping pre-commit hook.\n" + exit 0 fi lint-staged diff --git a/.lintstagedrc.json b/.lintstagedrc.json index b9468ecc..4ae5671f 100644 --- a/.lintstagedrc.json +++ b/.lintstagedrc.json @@ -1,6 +1,6 @@ { "*.css": [ "npm run lint:css" ], - "*.{js,jsx,ts,tsx}": [ "npm run lint:js" ], + "*.{js,jsx,ts,tsx}": [ "npm run lint" ], "*.php": [ "npm run lint:php" ], "*.{js,json,jsx,md,ts,tsx,yml,yaml}": [ "npm run format:check" ] } diff --git a/inc/Editor/BlockManagement/BlockRegistration.php b/inc/Editor/BlockManagement/BlockRegistration.php index 990b7679..0b57fd6c 100644 --- a/inc/Editor/BlockManagement/BlockRegistration.php +++ b/inc/Editor/BlockManagement/BlockRegistration.php @@ -4,6 +4,7 @@ defined( 'ABSPATH' ) || exit(); +use RemoteDataBlocks\Analytics\TracksAnalytics; use RemoteDataBlocks\Editor\BlockPatterns\BlockPatterns; use RemoteDataBlocks\Editor\DataBinding\BlockBindings; use RemoteDataBlocks\REST\RemoteDataController; @@ -77,6 +78,7 @@ public static function register_blocks(): void { 'availableBindings' => $available_bindings, 'loop' => $config['loop'], 'name' => $block_name, + 'dataSourceType' => ConfigStore::get_data_source_type( $block_name ), 'overrides' => $formatted_overrides, 'patterns' => $config['patterns'], 'selectors' => $config['selectors'], @@ -110,6 +112,7 @@ public static function register_blocks(): void { wp_localize_script( $script_handle, 'REMOTE_DATA_BLOCKS', [ 'config' => $remote_data_blocks_config, 'rest_url' => RemoteDataController::get_url(), + 'tracks_global_properties' => TracksAnalytics::get_global_properties(), ] ); } } diff --git a/package-lock.json b/package-lock.json index 2f7b5ae4..59bb70c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "remote-data-blocks", "hasInstallScript": true, "license": "GPL-2.0-or-later", + "dependencies": { + "@automattic/calypso-analytics": "^1.1.2" + }, "devDependencies": { "@automattic/eslint-plugin-wpvip": "0.13.0", "@babel/preset-react": "7.25.9", @@ -107,6 +110,19 @@ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@automattic/calypso-analytics": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@automattic/calypso-analytics/-/calypso-analytics-1.1.2.tgz", + "integrity": "sha512-+pPM41BL8XYxHKoEidk3/J3XjKxinVZfB0/t8E2KPBqGclDSVg7PXxslOzWsQsNju64rSWcjIY5+c8fnnWRW5g==", + "dependencies": { + "@automattic/load-script": "^1.0.0", + "cookie": "^0.4.1", + "debug": "^4.3.3", + "hash.js": "^1.1.7", + "tslib": "^2.3.0", + "uuid": "^9.0.1" + } + }, "node_modules/@automattic/eslint-plugin-wpvip": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@automattic/eslint-plugin-wpvip/-/eslint-plugin-wpvip-0.13.0.tgz", @@ -137,6 +153,23 @@ "eslint": "^8.56.0" } }, + "node_modules/@automattic/load-script": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@automattic/load-script/-/load-script-1.0.0.tgz", + "integrity": "sha512-Hc1mRmTK12OKrONnGhe7Ht1Gpo4B/ls8WQ1IZ1/qBws1bUZ6u7Crnpv3HZkN4UI7irG3OU4l4Pn1TXtoJLcKRw==", + "dependencies": { + "@babel/runtime": "^7.4.4", + "debug": "^3.2.6" + } + }, + "node_modules/@automattic/load-script/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/@babel/code-frame": { "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.0.tgz", @@ -1954,7 +1987,6 @@ "version": "7.25.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz", "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -11729,7 +11761,6 @@ "version": "0.4.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -12420,7 +12451,6 @@ "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -15898,6 +15928,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -16373,8 +16412,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ini": { "version": "1.3.8", @@ -21110,8 +21148,7 @@ "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "node_modules/minimatch": { "version": "9.0.5", @@ -21263,8 +21300,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/multicast-dns": { "version": "7.2.5", @@ -24073,8 +24109,7 @@ "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regenerator-transform": { "version": "0.15.2", @@ -27049,8 +27084,7 @@ "node_modules/tslib": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -27568,7 +27602,6 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true, "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" diff --git a/package.json b/package.json index 9ecae94d..3ddfbe65 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "lint:fix": "npm run cmd:lint . -- --fix", "lint:css:fix": "wp-scripts lint-style --fix", "lint:css": "wp-scripts lint-style", - "lint:js": "npm run lint && npm run check-types", + "lint:js": "npm run lint", "lint:php": "npm run lint:phpcs && npm run lint:psalm", "lint:phpcs": "vendor/bin/phpcs --cache", "lint:phpcs:fix": "vendor/bin/phpcbf", @@ -99,5 +99,8 @@ }, "stylelint": { "extends": "./node_modules/@wordpress/scripts/config/.stylelintrc.json" + }, + "dependencies": { + "@automattic/calypso-analytics": "^1.1.2" } } diff --git a/src/blocks/remote-data-container/components/BlockBindingControls.tsx b/src/blocks/remote-data-container/components/BlockBindingControls.tsx index a115e31c..d7f4936e 100644 --- a/src/blocks/remote-data-container/components/BlockBindingControls.tsx +++ b/src/blocks/remote-data-container/components/BlockBindingControls.tsx @@ -1,6 +1,8 @@ import { CheckboxControl, SelectControl } from '@wordpress/components'; import { TEXT_FIELD_TYPES } from '@/blocks/remote-data-container/config/constants'; +import { sendTracksEvent } from '@/blocks/remote-data-container/utils/tracks'; +import { getBlockDataSourceType } from '@/utils/localized-block-data'; interface BlockBindingFieldControlProps { availableBindings: AvailableBindings; @@ -35,12 +37,14 @@ interface BlockBindingControlsProps { attributes: RemoteDataInnerBlockAttributes; availableBindings: AvailableBindings; blockName: string; + remoteDataName: string; removeBinding: ( target: string ) => void; updateBinding: ( target: string, args: Omit< RemoteDataBlockBindingArgs, 'block' > ) => void; } export function BlockBindingControls( props: BlockBindingControlsProps ) { - const { attributes, availableBindings, blockName, removeBinding, updateBinding } = props; + const { attributes, availableBindings, blockName, remoteDataName, removeBinding, updateBinding } = + props; const contentArgs = attributes.metadata?.bindings?.content?.args; const contentField = contentArgs?.field ?? ''; const imageAltField = attributes.metadata?.bindings?.alt?.args?.field ?? ''; @@ -51,11 +55,23 @@ export function BlockBindingControls( props: BlockBindingControlsProps ) { function updateFieldBinding( target: string, field: string ): void { if ( ! field ) { removeBinding( target ); + sendTracksEvent( 'remotedatablocks_remote_data_container_actions', { + action: 'remove_binding', + data_source_type: getBlockDataSourceType( remoteDataName ), + block_target_attribute: target, + } ); + return; } const args = attributes.metadata?.bindings?.[ target ]?.args ?? {}; updateBinding( target, { ...args, field } ); + sendTracksEvent( 'remotedatablocks_remote_data_container_actions', { + action: 'update_binding', + data_source_type: getBlockDataSourceType( remoteDataName ), + remote_data_field: field, + block_target_attribute: target, + } ); } function updateFieldLabel( showLabel: boolean ): void { @@ -68,6 +84,10 @@ export function BlockBindingControls( props: BlockBindingControlsProps ) { ? Object.entries( availableBindings ).find( ( [ key ] ) => key === contentField )?.[ 1 ]?.name : undefined; updateBinding( 'content', { ...contentArgs, field: contentField, label } ); + sendTracksEvent( 'remotedatablocks_remote_data_container_actions', { + action: showLabel ? 'show_label' : 'hide_label', + data_source_type: getBlockDataSourceType( remoteDataName ), + } ); } switch ( blockName ) { diff --git a/src/blocks/remote-data-container/components/field-shortcode/FieldShortcodeButton.tsx b/src/blocks/remote-data-container/components/field-shortcode/FieldShortcodeButton.tsx index 1a2f77fe..3be0959a 100644 --- a/src/blocks/remote-data-container/components/field-shortcode/FieldShortcodeButton.tsx +++ b/src/blocks/remote-data-container/components/field-shortcode/FieldShortcodeButton.tsx @@ -11,6 +11,8 @@ import { import { FieldShortcodeSelectFieldPopover } from '@/blocks/remote-data-container/components/field-shortcode/FieldShortcodeSelectFieldPopover'; import { FieldShortcodeSelectTabs } from '@/blocks/remote-data-container/components/field-shortcode/FieldShortcodeSelectTabs'; import { FieldShortcodeSelectField } from '@/blocks/remote-data-container/components/field-shortcode/FieldShortcodeSelection'; +import { sendTracksEvent } from '@/blocks/remote-data-container/utils/tracks'; +import { getBlockDataSourceType } from '@/utils/localized-block-data'; function parseDataQuery( dataQuery?: string ): FieldSelection | null { if ( ! dataQuery ) { @@ -38,6 +40,7 @@ export function FieldShortcodeButton( props: WPFormatEditProps ) { function onClick() { setShowUI( ! showUI ); + sendTracksEvent( 'remotedatablocks_field_shortcode', { action: 'toolbar_icon_clicked' } ); } function onClose() { @@ -76,11 +79,20 @@ export function FieldShortcodeButton( props: WPFormatEditProps ) { function onSelectField( data: FieldSelection, fieldValue: string ) { updateOrInsertField( data, fieldValue ); onClose(); + sendTracksEvent( 'remotedatablocks_field_shortcode', { + action: data.action, + data_source_type: getBlockDataSourceType( data.remoteData?.blockName ), + selection_path: data.selectionPath, + } ); } - function resetField() { + function resetField( blockName?: string ): void { updateOrInsertField( null, 'Unbound field' ); setQueryInput( null ); + sendTracksEvent( 'remotedatablocks_field_shortcode', { + action: 'reset_field_shortcode', + data_source_type: getBlockDataSourceType( blockName ), + } ); } useEffect( () => { @@ -118,7 +130,9 @@ export function FieldShortcodeButton( props: WPFormatEditProps ) { { queryInput && ( + onSelectField( { ...data, selectionPath: 'select_new_tab' }, fieldValue ) + } queryInput={ queryInput.queryInput } fieldType="field" /> @@ -131,7 +145,9 @@ export function FieldShortcodeButton( props: WPFormatEditProps ) { fieldSelection={ fieldSelection } formatTypeSettings={ formatTypeSettings } onClose={ onClose } - onSelectField={ onSelectField } + onSelectField={ ( data, fieldValue ) => + onSelectField( { ...data, selectionPath: 'popover' }, fieldValue ) + } resetField={ resetField } /> ) } diff --git a/src/blocks/remote-data-container/components/field-shortcode/FieldShortcodeSelectExisting.tsx b/src/blocks/remote-data-container/components/field-shortcode/FieldShortcodeSelectExisting.tsx index af27ff53..1abd6270 100644 --- a/src/blocks/remote-data-container/components/field-shortcode/FieldShortcodeSelectExisting.tsx +++ b/src/blocks/remote-data-container/components/field-shortcode/FieldShortcodeSelectExisting.tsx @@ -31,7 +31,9 @@ export function FieldShortcodeSelectExisting( props: FieldShortcodeSelectExistin + props.onSelectField( { ...data, selectionPath: 'select_existing_tab' }, fieldValue ) + } remoteData={ remoteData } /> diff --git a/src/blocks/remote-data-container/components/field-shortcode/FieldShortcodeSelectFieldPopover.tsx b/src/blocks/remote-data-container/components/field-shortcode/FieldShortcodeSelectFieldPopover.tsx index 6979d4e4..743ae59a 100644 --- a/src/blocks/remote-data-container/components/field-shortcode/FieldShortcodeSelectFieldPopover.tsx +++ b/src/blocks/remote-data-container/components/field-shortcode/FieldShortcodeSelectFieldPopover.tsx @@ -17,7 +17,7 @@ interface FieldShortcodeSelectFieldPopoverProps { formatTypeSettings: WPFormat; onSelectField: ( data: FieldSelection, fieldValue: string ) => void; onClose: () => void; - resetField: () => void; + resetField: ( blockName?: string ) => void; } export function FieldShortcodeSelectFieldPopover( props: FieldShortcodeSelectFieldPopoverProps ) { @@ -25,6 +25,7 @@ export function FieldShortcodeSelectFieldPopover( props: FieldShortcodeSelectFie editableContentElement: props.contentRef.current, settings: props.formatTypeSettings, } ); + const { remoteData, selectedField, type } = props.fieldSelection; return ( + props.onSelectField( { ...data, action: 'update_field_shortcode' }, fieldValue ) + } + queryInput={ remoteData?.queryInput ?? {} } + selectedField={ selectedField } /> - diff --git a/src/blocks/remote-data-container/components/field-shortcode/FieldShortcodeSelectMeta.tsx b/src/blocks/remote-data-container/components/field-shortcode/FieldShortcodeSelectMeta.tsx index b90146a2..6892c471 100644 --- a/src/blocks/remote-data-container/components/field-shortcode/FieldShortcodeSelectMeta.tsx +++ b/src/blocks/remote-data-container/components/field-shortcode/FieldShortcodeSelectMeta.tsx @@ -29,7 +29,9 @@ export function FieldShortcodeSelectMeta( props: FieldShortcodeSelectMetaProps ) + props.onSelectField( { ...data, selectionPath: 'select_meta_tab' }, fieldValue ) + } remoteData={ remoteData } /> diff --git a/src/blocks/remote-data-container/components/field-shortcode/FieldShortcodeSelection.tsx b/src/blocks/remote-data-container/components/field-shortcode/FieldShortcodeSelection.tsx index d8d55508..0c8eb3b5 100644 --- a/src/blocks/remote-data-container/components/field-shortcode/FieldShortcodeSelection.tsx +++ b/src/blocks/remote-data-container/components/field-shortcode/FieldShortcodeSelection.tsx @@ -22,9 +22,11 @@ export function FieldSelection( props: FieldSelectionProps ) { <> { Object.entries( props.fields ).map( ( [ fieldName, fieldDetails ], index ) => { const fieldSelection: FieldSelection = { + action: 'add_field_shortcode', selectedField: fieldName, remoteData: props.remoteData, type: props.fieldType, + selectionPath: 'select_new_tab', }; return ( diff --git a/src/blocks/remote-data-container/components/modals/InputModal.tsx b/src/blocks/remote-data-container/components/modals/InputModal.tsx index 75181345..d559f58d 100644 --- a/src/blocks/remote-data-container/components/modals/InputModal.tsx +++ b/src/blocks/remote-data-container/components/modals/InputModal.tsx @@ -3,9 +3,12 @@ import { useState } from '@wordpress/element'; import { ModalWithButtonTrigger } from '@/blocks/remote-data-container/components/modals/BaseModal'; import { useModalState } from '@/blocks/remote-data-container/hooks/useModalState'; +import { sendTracksEvent } from '@/blocks/remote-data-container/utils/tracks'; import { __ } from '@/utils/i18n'; +import { getBlockDataSourceType } from '@/utils/localized-block-data'; interface InputModalProps { + blockName: string; headerImage?: string; inputs: InputVariable[]; onSelect: ( data: RemoteDataQueryInput ) => void; @@ -25,9 +28,14 @@ export function InputModal( props: InputModalProps ) { setInputState( { ...inputState, [ field ]: value } ); } - function wrappedOnSelect(): void { + function onSelectItem(): void { props.onSelect( inputState ); close(); + sendTracksEvent( 'remotedatablocks_add_block', { + action: 'select_item', + selected_option: 'manual_input', + data_source_type: getBlockDataSourceType( props.blockName ), + } ); } return ( @@ -52,7 +60,7 @@ export function InputModal( props: InputModalProps ) { style={ { marginBottom: '8px' } } /> ) ) } - diff --git a/src/blocks/remote-data-container/components/modals/SearchModal.tsx b/src/blocks/remote-data-container/components/modals/SearchModal.tsx index c95bb7c5..66d4485f 100644 --- a/src/blocks/remote-data-container/components/modals/SearchModal.tsx +++ b/src/blocks/remote-data-container/components/modals/SearchModal.tsx @@ -2,6 +2,8 @@ import { SearchControl } from '@wordpress/components'; import { ItemListModal } from '@/blocks/remote-data-container/components/modals/ItemListModal'; import { useSearchResults } from '@/blocks/remote-data-container/hooks/useSearchResults'; +import { sendTracksEvent } from '@/blocks/remote-data-container/utils/tracks'; +import { getBlockDataSourceType } from '@/utils/localized-block-data'; interface SearchModalProps { blockName: string; @@ -19,6 +21,15 @@ export function SearchModal( props: SearchModalProps ) { queryKey, } ); + function onSelectItem( data: RemoteDataQueryInput ): void { + onSelect( data ); + sendTracksEvent( 'remotedatablocks_add_block', { + action: 'select_item', + selected_option: 'search_from_list', + data_source_type: getBlockDataSourceType( blockName ), + } ); + } + return ( diff --git a/src/blocks/remote-data-container/components/panels/DataPanel.tsx b/src/blocks/remote-data-container/components/panels/DataPanel.tsx index 4bbd46da..9476277b 100644 --- a/src/blocks/remote-data-container/components/panels/DataPanel.tsx +++ b/src/blocks/remote-data-container/components/panels/DataPanel.tsx @@ -7,6 +7,9 @@ import { import { useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +import { sendTracksEvent } from '@/blocks/remote-data-container/utils/tracks'; +import { getBlockDataSourceType } from '@/utils/localized-block-data'; + interface DataPanelProps { refreshRemoteData: () => void; remoteData: RemoteData; @@ -18,9 +21,21 @@ export function DataPanel( props: DataPanelProps ) { const [ isResetConfirmOpen, setResetConfirmOpen ] = useState< boolean >( false ); + function onRefreshRemoteData(): void { + refreshRemoteData(); + sendTracksEvent( 'remotedatablocks_remote_data_container_actions', { + action: 'refresh_block_data', + data_source_type: getBlockDataSourceType( remoteData.blockName ), + } ); + } + function resetBlock(): void { resetRemoteData(); setResetConfirmOpen( false ); + sendTracksEvent( 'remotedatablocks_remote_data_container_actions', { + action: 'reset_block_data', + data_source_type: getBlockDataSourceType( remoteData.blockName ), + } ); } if ( ! remoteData ) { @@ -31,7 +46,7 @@ export function DataPanel( props: DataPanelProps ) { { isResetConfirmOpen && ( setResetConfirmOpen( false ) } onConfirm={ resetBlock } style={ { diff --git a/src/blocks/remote-data-container/components/panels/OverridesPanel.tsx b/src/blocks/remote-data-container/components/panels/OverridesPanel.tsx index 481d9910..17657062 100644 --- a/src/blocks/remote-data-container/components/panels/OverridesPanel.tsx +++ b/src/blocks/remote-data-container/components/panels/OverridesPanel.tsx @@ -1,6 +1,9 @@ import { PanelBody, SelectControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +import { sendTracksEvent } from '@/blocks/remote-data-container/utils/tracks'; +import { getBlockDataSourceType } from '@/utils/localized-block-data'; + interface OverridesPanelProps { blockConfig: BlockConfig; remoteData: RemoteData; @@ -43,6 +46,11 @@ export function OverridesPanel( props: OverridesPanelProps ) { ...remoteData, queryInputOverrides: copyOfQueryInputOverrides, } ); + sendTracksEvent( 'remotedatablocks_remote_data_container_override', { + data_source_type: getBlockDataSourceType( remoteData.blockName ), + override_type: overrides?.type, + override_target: overrides?.target, + } ); } return ( diff --git a/src/blocks/remote-data-container/components/pattern-selection/PatternSelection.tsx b/src/blocks/remote-data-container/components/pattern-selection/PatternSelection.tsx index 8417e360..2b16d369 100644 --- a/src/blocks/remote-data-container/components/pattern-selection/PatternSelection.tsx +++ b/src/blocks/remote-data-container/components/pattern-selection/PatternSelection.tsx @@ -5,8 +5,11 @@ import { __ } from '@wordpress/i18n'; import { blockDefault } from '@wordpress/icons'; import { PatternSelectionModal } from '@/blocks/remote-data-container/components/pattern-selection/PatternSelectionModal'; +import { sendTracksEvent } from '@/blocks/remote-data-container/utils/tracks'; +import { getBlockDataSourceType } from '@/utils/localized-block-data'; interface PatternSelectionProps { + blockName: string; insertPatternBlocks: ( pattern: BlockPattern ) => void; onCancel: () => void; supportedPatterns: BlockPattern[]; @@ -18,12 +21,26 @@ export function PatternSelection( props: PatternSelectionProps ) { function onClickPattern( pattern: BlockPattern ) { props.insertPatternBlocks( pattern ); setShowModal( false ); + sendTracksEvent( 'remotedatablocks_add_block', { + action: 'select_pattern', + selected_option: 'select_from_list', + data_source_type: getBlockDataSourceType( props.blockName ), + } ); } function onClose() { setShowModal( false ); } + function onClickManualEdit(): void { + props.onCancel(); + sendTracksEvent( 'remotedatablocks_add_block', { + action: 'select_pattern', + selected_option: 'manual_edit', + data_source_type: getBlockDataSourceType( props.blockName ), + } ); + } + if ( showModal ) { return ( setShowModal( true ) } variant="primary"> { __( 'Choose a pattern' ) } - diff --git a/src/blocks/remote-data-container/edit.tsx b/src/blocks/remote-data-container/edit.tsx index b1daa824..eeb733b9 100644 --- a/src/blocks/remote-data-container/edit.tsx +++ b/src/blocks/remote-data-container/edit.tsx @@ -19,10 +19,11 @@ import { getBlockConfig } from '@/utils/localized-block-data'; import './editor.scss'; export function Edit( props: BlockEditProps< RemoteDataBlockAttributes > ) { - const blockConfig = getBlockConfig( props.name ); + const blockName = props.name; + const blockConfig = getBlockConfig( blockName ); if ( ! blockConfig ) { - throw new Error( `Block configuration not found for block: ${ props.name }` ); + throw new Error( `Block configuration not found for block: ${ blockName }` ); } const rootClientId = props.clientId; @@ -35,8 +36,8 @@ export function Edit( props: BlockEditProps< RemoteDataBlockAttributes > ) { markReadyForInsertion, resetReadyForInsertion, showPatternSelection, - } = usePatterns( props.name, rootClientId ); - const { execute } = useRemoteData( props.name, DISPLAY_QUERY_KEY ); + } = usePatterns( blockName, rootClientId ); + const { execute } = useRemoteData( blockName, DISPLAY_QUERY_KEY ); const [ initialLoad, setInitialLoad ] = useState< boolean >( true ); function fetchRemoteData( input: RemoteDataQueryInput, insertBlocks = true ) { @@ -105,6 +106,7 @@ export function Edit( props: BlockEditProps< RemoteDataBlockAttributes > ) { return (
diff --git a/src/blocks/remote-data-container/utils/tracks.spec.ts b/src/blocks/remote-data-container/utils/tracks.spec.ts new file mode 100644 index 00000000..a6e60c50 --- /dev/null +++ b/src/blocks/remote-data-container/utils/tracks.spec.ts @@ -0,0 +1,68 @@ +import { recordTracksEvent } from '@automattic/calypso-analytics'; +import { describe, expect, it, vi, beforeEach } from 'vitest'; + +import { sendTracksEvent } from '@/blocks/remote-data-container/utils/tracks'; +import { getTracksGlobalProperties } from '@/utils/localized-block-data'; + +vi.mock( '@automattic/calypso-analytics', () => ( { + recordTracksEvent: vi.fn(), +} ) ); + +vi.mock( '@/utils/localized-block-data', () => ( { + getTracksGlobalProperties: vi.fn(), +} ) ); + +describe( 'sendTracksEvent', () => { + const defaultTracksGlobalProps: TracksGlobalProperties = { + plugin_version: '0.2', + + // "Tracks" library properties. + vipgo_env: 'local', + vipgo_org: 1, + is_vip_user: false, + hosting_provider: 'vip', + is_multisite: false, + wp_version: '6.6', + _ui: '1', + _ut: 'anon', + }; + + beforeEach( () => { + window.REMOTE_DATA_BLOCKS = { + config: {}, + rest_url: '', + tracks_global_properties: defaultTracksGlobalProps, + }; + vi.clearAllMocks(); + } ); + + it( 'should not record event if Tracks global properties is not defined', () => { + window.REMOTE_DATA_BLOCKS = { config: {}, rest_url: '', tracks_global_properties: undefined }; + + sendTracksEvent( 'remotedatablocks_field_shortcode', { action: 'value' } ); + + expect( recordTracksEvent ).not.toHaveBeenCalled(); + } ); + + it( 'should not track if vipgo_env is local', () => { + sendTracksEvent( 'remotedatablocks_field_shortcode', { action: 'value' } ); + + expect( recordTracksEvent ).not.toHaveBeenCalled(); + } ); + + it( 'should call recordTracksEvent with the correct event name and properties', () => { + vi.mocked( getTracksGlobalProperties ).mockReturnValue( { + ...defaultTracksGlobalProps, + vipgo_env: 'production', + } ); + + sendTracksEvent( 'remotedatablocks_field_shortcode', { action: 'actionName' } ); + + expect( recordTracksEvent ).toHaveBeenCalledTimes( 1 ); + expect( recordTracksEvent ).toHaveBeenCalledWith( 'remotedatablocks_field_shortcode', { + ...defaultTracksGlobalProps, + vipgo_env: 'production', + action: 'actionName', + } ); + } ); +} ); diff --git a/src/blocks/remote-data-container/utils/tracks.ts b/src/blocks/remote-data-container/utils/tracks.ts new file mode 100644 index 00000000..3ccda6e5 --- /dev/null +++ b/src/blocks/remote-data-container/utils/tracks.ts @@ -0,0 +1,53 @@ +import { recordTracksEvent } from '@automattic/calypso-analytics'; + +import { getTracksGlobalProperties } from '@/utils/localized-block-data'; + +interface TRACKS_EVENTS { + remotedatablocks_remote_data_container_actions: { + action: string; + block_target_attribute?: string; + data_source_type: string; + remote_data_field?: string; + }; + remotedatablocks_field_shortcode: { + action: string; + data_source_type?: string; + selection_path?: string; + }; + remotedatablocks_add_block: { + action: string; + selected_option: string; + data_source_type: string; + }; + remotedatablocks_remote_data_container_override: { + data_source_type: string; + override_type?: string; + override_target?: string; + }; + remotedatablocks_associate_block_type_to_pattern: { + data_source_type: string; + is_pattern_synced: boolean; + }; +} + +/** + * Send a tracks event with the given name and properties. + */ +export function sendTracksEvent< K extends keyof TRACKS_EVENTS >( + eventName: K, + eventProps: TRACKS_EVENTS[ K ] +): void { + const globalProps = getTracksGlobalProperties(); + + // Do not track if the props are not available i.e. user is not on VIP platform. + if ( ! globalProps ) { + return; + } + + // Do not track on local environments. + if ( globalProps.vipgo_env === 'local' ) { + return; + } + + recordTracksEvent( eventName, { ...globalProps, ...eventProps } ); +} diff --git a/src/pattern-editor/components/PatternEditorSettingsPanel.tsx b/src/pattern-editor/components/PatternEditorSettingsPanel.tsx index 5af609a9..9fc5b75a 100644 --- a/src/pattern-editor/components/PatternEditorSettingsPanel.tsx +++ b/src/pattern-editor/components/PatternEditorSettingsPanel.tsx @@ -1,16 +1,18 @@ import { SelectControl } from '@wordpress/components'; import { PluginDocumentSettingPanel } from '@wordpress/editor'; +import { sendTracksEvent } from '@/blocks/remote-data-container/utils/tracks'; import { PATTERN_BLOCK_TYPE_POST_META_KEY } from '@/config/constants'; import { useEditedPostAttribute } from '@/hooks/useEditedPostAttribute'; import { usePostMeta } from '@/hooks/usePostMeta'; import { __ } from '@/utils/i18n'; -import { getBlocksConfig } from '@/utils/localized-block-data'; +import { getBlockDataSourceType, getBlocksConfig } from '@/utils/localized-block-data'; export function PatternEditorSettingsPanel() { - const { postId, postType } = useEditedPostAttribute( getEditedPostAttribute => ( { + const { postId, postType, isSynced } = useEditedPostAttribute( getEditedPostAttribute => ( { postId: getEditedPostAttribute< number >( 'id' ) ?? 0, postType: getEditedPostAttribute< string >( 'type' ) ?? '', + isSynced: getEditedPostAttribute< string >( 'wp_pattern_sync_status' ) !== 'unsynced', } ) ); const { postMeta, updatePostMeta } = usePostMeta( postId, postType ); @@ -21,8 +23,12 @@ export function PatternEditorSettingsPanel() { const blocksConfig = getBlocksConfig(); const blockType = String( postMeta?.[ PATTERN_BLOCK_TYPE_POST_META_KEY ] ?? '' ); - function updateBlockTypes( newBlockType: string ) { - updatePostMeta( { ...postMeta, [ PATTERN_BLOCK_TYPE_POST_META_KEY ]: newBlockType } ); + function updateBlockTypes( blockName: string ): void { + updatePostMeta( { ...postMeta, [ PATTERN_BLOCK_TYPE_POST_META_KEY ]: blockName } ); + sendTracksEvent( 'remotedatablocks_associate_block_type_to_pattern', { + data_source_type: getBlockDataSourceType( blockName ), + is_pattern_synced: isSynced, + } ); } const options = Object.entries( blocksConfig ).map( ( [ value, blockConfig ] ) => { diff --git a/src/utils/localized-block-data.ts b/src/utils/localized-block-data.ts index 7a42aaf6..76c98533 100644 --- a/src/utils/localized-block-data.ts +++ b/src/utils/localized-block-data.ts @@ -6,6 +6,14 @@ export function getBlockConfig( blockName: string ): BlockConfig | undefined { return window.REMOTE_DATA_BLOCKS?.config?.[ blockName ]; } +export function getBlockDataSourceType( blockName?: string ): string { + if ( ! blockName ) { + return ''; + } + + return getBlockConfig( blockName )?.dataSourceType ?? ''; +} + export function getBlocksConfig(): BlocksConfig { return window.REMOTE_DATA_BLOCKS?.config ?? {}; } @@ -13,3 +21,10 @@ export function getBlocksConfig(): BlocksConfig { export function getRestUrl(): string { return window.REMOTE_DATA_BLOCKS?.rest_url ?? 'http://127.0.0.1:9999'; } + +/** + * Return global `Tracks` properties to be sent with every event. + */ +export function getTracksGlobalProperties(): TracksGlobalProperties | undefined { + return window.REMOTE_DATA_BLOCKS?.tracks_global_properties; +} diff --git a/tests/src/blocks/remote-data-container/components/modals/InputModal.test.tsx b/tests/src/blocks/remote-data-container/components/modals/InputModal.test.tsx index 81ce7934..9c031ba9 100644 --- a/tests/src/blocks/remote-data-container/components/modals/InputModal.test.tsx +++ b/tests/src/blocks/remote-data-container/components/modals/InputModal.test.tsx @@ -2,10 +2,16 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { InputModal } from '@/blocks/remote-data-container/components/modals/InputModal'; +import { sendTracksEvent } from '@/blocks/remote-data-container/utils/tracks'; + +vi.mock( '@/blocks/remote-data-container/utils/tracks', () => ( { + sendTracksEvent: vi.fn(), +} ) ); describe( 'InputModal', () => { const mockOnSelect = vi.fn(); const defaultProps = { + blockName: 'test-block', inputs: [ { slug: 'input1', name: 'Input 1', required: true, type: 'text' }, { slug: 'input2', name: 'Input 2', required: false, type: 'text' }, @@ -60,5 +66,10 @@ describe( 'InputModal', () => { input1: 'Test Value 1', input2: 'Test Value 2', } ); + expect( sendTracksEvent ).toHaveBeenCalledWith( 'remotedatablocks_add_block', { + action: 'select_item', + data_source_type: '', + selected_option: 'manual_input', + } ); } ); } ); diff --git a/tests/src/blocks/remote-data-container/filters/withBlockBinding.test.tsx b/tests/src/blocks/remote-data-container/filters/withBlockBinding.test.tsx index 2d2f677a..e2ebf417 100644 --- a/tests/src/blocks/remote-data-container/filters/withBlockBinding.test.tsx +++ b/tests/src/blocks/remote-data-container/filters/withBlockBinding.test.tsx @@ -27,10 +27,11 @@ vi.mock( '@/hooks/useEditedPostAttribute', () => ( { describe( 'withBlockBinding', () => { const MockBlockEdit = vi.fn( () =>
); const WrappedComponent = withBlockBinding( MockBlockEdit ); - const testBlockConfig = { + const testBlockConfig: LocalizedBlockData = { config: { 'test/block': { availableBindings: { field1: { name: 'Field 1', type: 'string' } }, + dataSourceType: 'test-source', loop: false, name: 'test/block', overrides: {}, diff --git a/types/localized-block-data.d.ts b/types/localized-block-data.d.ts index f1392c29..ceb6141e 100644 --- a/types/localized-block-data.d.ts +++ b/types/localized-block-data.d.ts @@ -16,6 +16,7 @@ interface InputVariable { interface BlockConfig { availableBindings: AvailableBindings; + dataSourceType: string; loop: boolean; name: string; overrides: Record< string, InputVariableOverrides >; @@ -44,4 +45,5 @@ interface BlocksConfig { interface LocalizedBlockData { config: BlocksConfig; rest_url: string; + tracks_global_properties?: TracksGlobalProperties; } diff --git a/types/remote-data.d.ts b/types/remote-data.d.ts index afb02a77..313ceab9 100644 --- a/types/remote-data.d.ts +++ b/types/remote-data.d.ts @@ -30,7 +30,9 @@ interface RemoteDataBlockAttributes { interface FieldSelection extends RemoteDataBlockAttributes { selectedField: string; + action: 'add_field_shortcode' | 'update_field_shortcode' | 'reset_field_shortcode'; type: 'field' | 'meta'; + selectionPath: 'select_new_tab' | 'select_existing_tab' | 'select_meta_tab' | 'popover'; } interface MetaFieldSelection extends FieldSelection { diff --git a/types/tracks.d.ts b/types/tracks.d.ts new file mode 100644 index 00000000..f74a18c3 --- /dev/null +++ b/types/tracks.d.ts @@ -0,0 +1,13 @@ +interface TracksGlobalProperties { + plugin_version: string; + + // "Tracks" library properties. + hosting_provider: string; + is_vip_user: boolean; + is_multisite: boolean; + vipgo_env: string; + vipgo_org: number; + wp_version: string; + _ui: string; // User ID + _ut: string; // User Type +}