* Add basic ExPlat initialization

* add tsx support
This commit is contained in:
Paul Sealock 2021-04-15 13:32:46 +12:00 committed by GitHub
parent 97d143b762
commit 7ab756b76c
21 changed files with 397 additions and 35 deletions

View File

@ -48,7 +48,7 @@ function getPackageName( file ) {
}
const isJsFile = ( filepath ) => {
return /.\.js$/.test( filepath );
return /.\.(js|ts|tsx)$/.test( filepath );
};
const isScssFile = ( filepath ) => {

View File

@ -12,7 +12,9 @@ const watch = require( 'node-watch' );
*/
const getPackages = require( './get-packages' );
const BUILD_CMD = `node ${ path.resolve( __dirname, './build.js' ).replace( /(\s+)/g, '\\$1' ) }`;
const BUILD_CMD = `node ${ path
.resolve( __dirname, './build.js' )
.replace( /(\s+)/g, '\\$1' ) }`;
let filesToBuild = new Map();
@ -25,7 +27,7 @@ const exists = ( filename ) => {
// Exclude deceitful source-like files, such as editor swap files.
const isSourceFile = ( filename ) => {
return /.\.(js|scss)$/.test( filename );
return /.\.(js|ts|tsx|scss)$/.test( filename );
};
const rebuild = ( filename ) => filesToBuild.set( filename, true );
@ -34,30 +36,48 @@ getPackages().forEach( ( p ) => {
const srcDir = path.resolve( p, 'src' );
try {
fs.accessSync( srcDir, fs.F_OK );
watch( path.resolve( p, 'src' ), { recursive: true }, ( event, filename ) => {
const filePath = path.resolve( srcDir, filename );
watch(
path.resolve( p, 'src' ),
{ recursive: true },
( event, filename ) => {
const filePath = path.resolve( srcDir, filename );
if ( ! isSourceFile( filename ) ) {
return;
}
if ( ! isSourceFile( filename ) ) {
return;
}
if ( ( [ 'update', 'change', 'rename' ].includes( event ) ) && exists( filePath ) ) {
// eslint-disable-next-line no-console
console.log( chalk.green( '->' ), `${ event }: ${ filename }` );
rebuild( filePath );
} else {
const buildFile = path.resolve( srcDir, '..', 'build', filename );
try {
fs.unlinkSync( buildFile );
process.stdout.write(
chalk.red( ' \u2022 ' ) +
path.relative( path.resolve( srcDir, '..', '..' ), buildFile ) +
' (deleted)' +
'\n'
if (
[ 'update', 'change', 'rename' ].includes( event ) &&
exists( filePath )
) {
// eslint-disable-next-line no-console
console.log(
chalk.green( '->' ),
`${ event }: ${ filename }`
);
} catch ( e ) {}
rebuild( filePath );
} else {
const buildFile = path.resolve(
srcDir,
'..',
'build',
filename
);
try {
fs.unlinkSync( buildFile );
process.stdout.write(
chalk.red( ' \u2022 ' ) +
path.relative(
path.resolve( srcDir, '..', '..' ),
buildFile
) +
' (deleted)' +
'\n'
);
} catch ( e ) {}
}
}
} );
);
} catch ( e ) {
// doesn't exist
}
@ -68,7 +88,12 @@ setInterval( () => {
if ( files.length ) {
filesToBuild = new Map();
try {
execSync( `${ BUILD_CMD } ${ files.map( file => file.replace( /(\s+)/g, '\\$1' ) ).join( ' ' ) }`, { stdio: [ 0, 1, 2 ] } );
execSync(
`${ BUILD_CMD } ${ files
.map( ( file ) => file.replace( /(\s+)/g, '\\$1' ) )
.join( ' ' ) }`,
{ stdio: [ 0, 1, 2 ] }
);
} catch ( e ) {}
}
}, 100 );

View File

@ -23,6 +23,7 @@ import { getSetting } from '@woocommerce/wc-admin-settings';
import { getNewPath } from '@woocommerce/navigation';
import { recordEvent } from '@woocommerce/tracks';
import { Text } from '@woocommerce/experimental';
import { Experiment } from '@woocommerce/explat';
/**
* Internal dependencies
@ -69,15 +70,24 @@ export const StatsOverview = () => {
( item ) => ! hiddenStats.includes( item.stat )
);
const HeaderText = (
<Text variant="title.small">
{ __( 'Stats overview', 'woocommerce-admin' ) }
</Text>
);
return (
<Card
size="large"
className="woocommerce-stats-overview woocommerce-homescreen-card"
>
<CardHeader size="medium">
<Text variant="title.small">
{ __( 'Stats overview', 'woocommerce-admin' ) }
</Text>
<Experiment
name="woocommerce_test_experiment"
defaultExperience={ HeaderText }
treatmentExperience={ HeaderText }
loadingExperience={ HeaderText }
/>
<EllipsisMenu
label={ __(
'Choose which values to display',

View File

@ -9,6 +9,7 @@ import interpolateComponents from 'interpolate-components';
import { Button, Modal } from '@wordpress/components';
import { Link } from '@woocommerce/components';
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
import { initializeExPlat } from '@woocommerce/explat';
class UsageModal extends Component {
constructor( props ) {
@ -70,6 +71,8 @@ class UsageModal extends Component {
return;
}
initializeExPlat();
this.setState( { isLoadingScripts: false } );
} );
} else if ( ! allowTracking ) {

View File

@ -8,6 +8,7 @@ import { withDispatch } from '@wordpress/data';
import { compose } from '@wordpress/compose';
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
import { initializeExPlat } from '@woocommerce/explat';
const BetaFeaturesTrackingModal = ( { updateOptions } ) => {
const [ isModalOpen, setIsModalOpen ] = useState( false );
@ -19,7 +20,9 @@ const BetaFeaturesTrackingModal = ( { updateOptions } ) => {
const setTracking = async ( allow ) => {
if ( typeof window.wcTracks.enable === 'function' ) {
if ( allow ) {
window.wcTracks.enable();
window.wcTracks.enable( () => {
initializeExPlat();
} );
} else {
window.wcTracks.isEnabled = false;
}

View File

@ -10,6 +10,20 @@
"integrity": "sha512-gZWaJbx3p1oennAIoJtMGluTmoM95Efk4rc44TSBxWSZZ8gH3Am2eh1o3i1NhrZmg2Zt3AiVFeZZ4AJccIpBKQ==",
"dev": true
},
"@automattic/explat-client": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@automattic/explat-client/-/explat-client-0.0.1.tgz",
"integrity": "sha512-OP0hXMqQVir+kHk4q0YAyHfN4+5MHN/g62w6J/hwYFfswes6tjOrGYsH963dXcGqmC6rPfW/0gPlOrjebmJbRg=="
},
"@automattic/explat-client-react-helpers": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@automattic/explat-client-react-helpers/-/explat-client-react-helpers-0.0.1.tgz",
"integrity": "sha512-qrUhXarn6F11dwnyqhgkoWPwZtIbaBY63ur5RmqOhq0L3DHSz1SOODI7lQovXhRCIYRrdOCkLlzUtSP6HC02Gg==",
"requires": {
"@automattic/explat-client": "^0.0.1",
"react": "^16.12.0"
}
},
"@automattic/mini-css-extract-plugin-with-rtl": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@automattic/mini-css-extract-plugin-with-rtl/-/mini-css-extract-plugin-with-rtl-0.8.0.tgz",
@ -7743,6 +7757,12 @@
"@types/node": "*"
}
},
"@types/cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg==",
"dev": true
},
"@types/expect-puppeteer": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/@types/expect-puppeteer/-/expect-puppeteer-4.4.5.tgz",
@ -10395,6 +10415,24 @@
}
}
},
"@woocommerce/explat": {
"version": "file:packages/explat",
"dev": true,
"requires": {
"@automattic/explat-client": "0.0.1",
"@automattic/explat-client-react-helpers": "0.0.1",
"cookie": "^0.4.1",
"qs": "6.9.6"
},
"dependencies": {
"cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
"dev": true
}
}
},
"@woocommerce/navigation": {
"version": "file:packages/navigation",
"dev": true,
@ -17641,12 +17679,6 @@
"safe-buffer": "~5.1.1"
}
},
"cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==",
"dev": true
},
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@ -20733,6 +20765,12 @@
"vary": "~1.1.2"
},
"dependencies": {
"cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==",
"dev": true
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",

View File

@ -90,6 +90,8 @@
}
},
"dependencies": {
"@automattic/explat-client": "0.0.1",
"@automattic/explat-client-react-helpers": "0.0.1",
"@woocommerce/e2e-environment": "0.2.1",
"@woocommerce/e2e-utils": "0.1.2",
"@wordpress/api-fetch": "2.2.8",
@ -151,6 +153,7 @@
"@testing-library/react": "11.2.6",
"@testing-library/react-hooks": "3.7.0",
"@testing-library/user-event": "12.8.3",
"@types/cookie": "0.4.0",
"@types/expect-puppeteer": "4.4.5",
"@types/history": "4.7.8",
"@types/jest": "26.0.22",
@ -158,6 +161,7 @@
"@types/puppeteer": "5.4.3",
"@types/wordpress__components": "9.8.6",
"@typescript-eslint/eslint-plugin": "4.22.0",
"@woocommerce/explat": "file:packages/explat",
"@woocommerce/api": "0.1.2",
"@woocommerce/components": "file:packages/components",
"@woocommerce/csv-export": "file:packages/csv-export",

View File

@ -1,3 +1,7 @@
# Unreleased
- Add `@woocommerce/explat` to list of packages.
# 1.4.0
- Add `@woocommerce/settings` to list of packages.

View File

@ -9,6 +9,7 @@ module.exports = [
'@woocommerce/dependency-extraction-webpack-plugin',
'@woocommerce/eslint-plugin',
'@woocommerce/experimental',
'@woocommerce/explat',
'@woocommerce/navigation',
'@woocommerce/notices',
'@woocommerce/number',

View File

@ -0,0 +1 @@
package-lock=false

View File

@ -0,0 +1,32 @@
# ExPlat
This packages includes a component and utility functions that can be used to run A/B Tests in WooCommerce dashboard and reports pages.
## Installation
Install the module
```bash
npm install @woocommerce/explat --save
```
This package assumes that your code will run in an ES2015+ environment. If you're using an environment that has limited or no support for ES2015+ such as lower versions of IE then using core-js or @babel/polyfill will add support for these methods. Learn more about it in Babel docs.
## Usage
```js
import { Experiment } from '@woocommerce/explat';
const DefaultExperience = <div>Hello World</div>;
const TreatmentExperience = <div>Hello WooCommerce!</div>;
const LoadingExperience = <div></div>;
<Experiment
name="woocommerce_example_experiment"
defaultExperience={ DefaultExperience }
treatmentExperience={ TreatmentExperience }
loadingExperience={ LoadingExperience }
/>;
```

View File

@ -0,0 +1,36 @@
{
"name": "@woocommerce/explat",
"version": "1.0.0",
"description": "WooCommerce component and utils for A/B testing.",
"author": "Automattic",
"license": "GPL-2.0-or-later",
"keywords": [
"wordpress",
"woocommerce",
"abtest",
"explat"
],
"homepage": "https://github.com/woocommerce/woocommerce-admin/tree/main/packages/explat/README.md",
"repository": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce-admin.git"
},
"bugs": {
"url": "https://github.com/woocommerce/woocommerce-admin/issues"
},
"main": "build/index.js",
"module": "build-module/index.js",
"react-native": "src/index",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@automattic/explat-client": "0.0.1",
"@automattic/explat-client-react-helpers": "0.0.1",
"cookie": "^0.4.1",
"qs": "6.9.6"
},
"devDependencies": {
"@types/cookie": "^0.4.0"
}
}

View File

@ -0,0 +1,69 @@
/**
* External dependencies
*/
import cookie from 'cookie';
/**
* setInterval, but it runs first callback immediately instead of after interval.
*/
const immediateStartSetInterval = ( f: () => void, intervalMs: number ) => {
f();
return setInterval( f, intervalMs );
};
let initializeAnonIdPromise: null | Promise< string | null > = null;
const anonIdPollingIntervalMilliseconds = 50;
const anonIdPollingIntervalMaxAttempts = 100; // 50 * 100 = 5000 = 5 seconds
/**
* Gather w.js anonymous cookie, tk_ai
*/
export const readAnonCookie = (): string | null => {
return cookie.parse( document.cookie ).tk_ai || null;
};
/**
* Initializes the anonId:
* - Polls for AnonId receival
* - Should only be called once at startup
* - Happens to be safe to call multiple times if it is necessary to reset the anonId - something like this was necessary for testing.
*
* This purely for boot-time initialization, in usual circumstances it will be retrieved within 100-300ms, it happens in parallel booting
* so should only delay experiment loading that much for boot-time experiments. In some circumstances such as a very slow connection this
* can take a lot longer.
*
* The state of initializeAnonIdPromise should be used rather than the return of this function.
* The return is only avaliable to make this easier to test.
*
* Throws on error.
*/
export const initializeAnonId = async (): Promise< string | null > => {
let attempt = 0;
initializeAnonIdPromise = new Promise( ( res ) => {
const anonIdPollingInterval = immediateStartSetInterval( () => {
const anonId = readAnonCookie();
if ( typeof anonId === 'string' && anonId !== '' ) {
clearInterval( anonIdPollingInterval );
res( anonId );
return;
}
if ( anonIdPollingIntervalMaxAttempts - 1 <= attempt ) {
clearInterval( anonIdPollingInterval );
res( null );
return;
}
attempt = attempt + 1;
}, anonIdPollingIntervalMilliseconds );
} );
return initializeAnonIdPromise;
};
export const getAnonId = async (): Promise< string | null > => {
if ( ! window.wcTracks?.isEnabled ) {
return null;
}
return await initializeAnonIdPromise;
};

View File

@ -0,0 +1,31 @@
/**
* External dependencies
*/
import { stringify } from 'qs';
const EXPLAT_VERSION = '0.1.0';
export const fetchExperimentAssignment = async ( {
experimentName,
anonId,
}: {
experimentName: string;
anonId: string | null;
} ): Promise< unknown > => {
if ( ! window.wcTracks?.isEnabled ) {
throw new Error(
`Tracking is disabled, can't fetch experimentAssignment`
);
}
const params = stringify( {
experiment_name: experimentName,
anon_id: anonId ?? undefined,
} );
const response = await window.fetch(
`https://public-api.wordpress.com/wpcom/v2/experiments/${ EXPLAT_VERSION }/assignments/woocommerce?${ params }`
);
return await response.json();
};

View File

@ -0,0 +1,47 @@
/**
* Internal dependencies
*/
import { isDevelopmentMode } from './utils';
export const logError = (
error: Record< string, string > & { message: string }
): void => {
const onLoggingError = ( e: unknown ) => {
if ( isDevelopmentMode ) {
console.error( '[ExPlat] Unable to send error to server:', e ); // eslint-disable-line no-console
}
};
try {
const { message, ...properties } = error;
const logStashError = {
message,
properties: {
...properties,
context: 'explat',
explat_client: 'woocommerce',
},
};
if ( isDevelopmentMode ) {
console.error( '[ExPlat] ', error.message, error ); // eslint-disable-line no-console
} else {
if ( ! window.wcTracks?.isEnabled ) {
throw new Error(
`Tracking is disabled, can't send error to the server`
);
}
const body = new window.FormData();
body.append( 'error', JSON.stringify( logStashError ) );
window
.fetch( 'https://public-api.wordpress.com/rest/v1.1/js-error', {
method: 'POST',
body,
} )
.catch( onLoggingError );
}
} catch ( e ) {
onLoggingError( e );
}
};

View File

@ -0,0 +1,47 @@
/**
* External dependencies
*/
import { createExPlatClient } from '@automattic/explat-client';
import createExPlatClientReactHelpers from '@automattic/explat-client-react-helpers';
/**
* Internal dependencies
*/
import { isDevelopmentMode } from './utils';
import { logError } from './error';
import { fetchExperimentAssignment } from './assignment';
import { getAnonId, initializeAnonId } from './anon';
declare global {
interface Window {
wcTracks: {
isEnabled: boolean;
};
}
}
export const initializeExPlat = (): void => {
if ( window.wcTracks?.isEnabled ) {
initializeAnonId().catch( ( e ) => logError( { message: e.message } ) );
}
};
initializeExPlat();
const exPlatClient = createExPlatClient( {
fetchExperimentAssignment,
getAnonId,
logError,
isDevelopmentMode,
} );
export const {
loadExperimentAssignment,
dangerouslyGetExperimentAssignment,
} = exPlatClient;
const exPlatClientReactHelpers = createExPlatClientReactHelpers( exPlatClient );
export const {
useExperiment,
Experiment,
ProvideExperimentData,
} = exPlatClientReactHelpers;

View File

@ -0,0 +1,4 @@
/**
* Boolean determining if environment is development.
*/
export const isDevelopmentMode = process.env.NODE_ENV === 'development';

View File

@ -86,6 +86,7 @@ Release and roadmap notes are available on the [WooCommerce Developers Blog](htt
- Tweak: Add tracking data for the preview site btn #6623
- Tweak: Update WC Payments copy on the task list #6734
- Tweak: Add check to see if value for contains is array, show warning if not. #6645
- Dev: Add A/A test #6669
- Fix: Event tracking for merchant email notes #6616
- Fix: Check active plugins before getting the PayPal onboarding status #6625
- Dev: Add support for running php unit tests in PHP 8. #6678

View File

@ -339,6 +339,7 @@ class Loader {
$css_file_version = self::get_file_version( 'css' );
$scripts = array(
'wc-explat',
'wc-customer-effort-score',
// NOTE: This should be removed when Gutenberg is updated and the notices package is removed from WooCommerce Admin.
'wc-notices',

View File

@ -27,6 +27,10 @@ const wooCommercePackages = [
'data',
];
global.wcTracks = {
isEnabled: false,
};
// aliases
global.wcSettings = {
adminUrl: 'https://vagrant.local/wp/wp-admin/',
@ -63,7 +67,7 @@ global.wcSettings = {
woocommerce_excluded_report_order_statuses: [],
},
dataEndpoints: {
countries: [],
countries: [],
performanceIndicators: [
{
chart: 'total_sales',

View File

@ -25,6 +25,7 @@ const NODE_ENV = process.env.NODE_ENV || 'development';
const WC_ADMIN_PHASE = process.env.WC_ADMIN_PHASE || 'development';
const wcAdminPackages = [
'explat',
'components',
'csv-export',
'currency',