Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add opt-in usage tracking for blocks added to post content #2140

Merged
merged 11 commits into from
Aug 3, 2017
17 changes: 15 additions & 2 deletions components/button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,24 @@ class Button extends Component {
}

render() {
const { href, target, isPrimary, isLarge, isToggled, className, disabled, ...additionalProps } = this.props;
const {
href,
target,
isPrimary,
isSecondary,
isLarge,
isSmall,
isToggled,
className,
disabled,
...additionalProps, // eslint-disable-line comma-dangle
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the eslint-disable-line here, eslint reports this line as an error. This seems like a bug.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that sort of makes sense, though it would be nice to have a better error message. I'll incorporate this change into #2205.

} = this.props;
const classes = classnames( 'components-button', className, {
button: ( isPrimary || isLarge ),
button: ( isPrimary || isSecondary || isLarge ),
'button-primary': isPrimary,
'button-secondary': isSecondary,
'button-large': isLarge,
'button-small': isSmall,
'is-toggled': isToggled,
} );

Expand Down
16 changes: 15 additions & 1 deletion components/button/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ describe( 'Button', () => {
expect( button.hasClass( 'button-large' ) ).toBe( false );
expect( button.hasClass( 'button-primary' ) ).toBe( false );
expect( button.hasClass( 'is-toggled' ) ).toBe( false );
expect( button.hasClass( 'is-borderless' ) ).toBe( false );
expect( button.prop( 'disabled' ) ).toBeUndefined();
expect( button.prop( 'type' ) ).toBe( 'button' );
expect( button.type() ).toBe( 'button' );
Expand All @@ -30,13 +29,28 @@ describe( 'Button', () => {
expect( button.hasClass( 'button-primary' ) ).toBe( true );
} );

it( 'should render a button element with button-secondary class', () => {
const button = shallow( <Button isSecondary /> );
expect( button.hasClass( 'button' ) ).toBe( true );
expect( button.hasClass( 'button-large' ) ).toBe( false );
expect( button.hasClass( 'button-secondary' ) ).toBe( true );
} );

it( 'should render a button element with button-large class', () => {
const button = shallow( <Button isLarge /> );
expect( button.hasClass( 'button' ) ).toBe( true );
expect( button.hasClass( 'button-large' ) ).toBe( true );
expect( button.hasClass( 'button-primary' ) ).toBe( false );
} );

it( 'should render a button element with button-small class', () => {
const button = shallow( <Button isSmall /> );
expect( button.hasClass( 'button' ) ).toBe( false );
expect( button.hasClass( 'button-large' ) ).toBe( false );
expect( button.hasClass( 'button-small' ) ).toBe( true );
expect( button.hasClass( 'button-primary' ) ).toBe( false );
} );

it( 'should render a button element with is-toggled without button class', () => {
const button = shallow( <Button isToggled /> );
expect( button.hasClass( 'button' ) ).toBe( false );
Expand Down
2 changes: 1 addition & 1 deletion components/notice/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
top: 40px;
right: 0;
min-width: 300px;
max-width: 400px;
max-width: 650px;
z-index: z-index( ".components-notice-list" );
}
1 change: 1 addition & 0 deletions editor/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,5 +234,6 @@ export function removeNotice( id ) {
}

export const createSuccessNotice = partial( createNotice, 'success' );
export const createInfoNotice = partial( createNotice, 'info' );
export const createErrorNotice = partial( createNotice, 'error' );
export const createWarningNotice = partial( createNotice, 'warning' );
65 changes: 65 additions & 0 deletions editor/enable-tracking-prompt/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* External dependencies
*/
import { connect } from 'react-redux';

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';

/**
* Internal dependencies
*/
import './style.scss';
import { bumpStat } from '../utils/tracking';
import { removeNotice } from '../actions';

export const TRACKING_PROMPT_NOTICE_ID = 'notice:enable-tracking-prompt';

export function EnableTrackingPrompt( props ) {
function dismissTrackingPrompt( enableTracking ) {
window.setUserSetting(
'gutenberg_tracking',
enableTracking ? 'on' : 'off'
);
if ( enableTracking ) {
bumpStat( 'tracking', 'opt-in' );
}
props.removeNotice( TRACKING_PROMPT_NOTICE_ID );
}

return (
<div className="enable-tracking-prompt">
<div className="enable-tracking-prompt__message">
{ __( 'Can Gutenberg collect data about your usage of the editor?' ) }
<div className="enable-tracking-prompt__buttons">
<Button
isPrimary
isSmall
onClick={ () => dismissTrackingPrompt( true ) }
>
{ __( 'Yes' ) }
</Button>
<Button
isSecondary
isSmall
onClick={ () => dismissTrackingPrompt( false ) }
>
{ __( 'No' ) }
</Button>
</div>
</div>
<div className="enable-tracking-prompt__clarification">
{ __( 'Usage data is completely anonymous, does not include your post content, and will only be used to improve the editor.' ) }
</div>
</div>
);
}

export default connect(
undefined,
{ removeNotice }
)( EnableTrackingPrompt );

21 changes: 21 additions & 0 deletions editor/enable-tracking-prompt/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.enable-tracking-prompt {
.enable-tracking-prompt__message {
font-weight: bold;
margin-top: 8px;

.enable-tracking-prompt__buttons {
float: right;

.button {
margin-left: 6px;
}
}
}

.enable-tracking-prompt__clarification {
clear: both;
margin-bottom: 8px;
font-size: 90%;
font-style: italic;
}
}
79 changes: 79 additions & 0 deletions editor/enable-tracking-prompt/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* External dependencies
*/
import { mount } from 'enzyme';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case I think we could have used shallow rendering which tends to be more performant (doesn't mount into DOM, doesn't render descendants).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shallow doesn't work here because we reach inside a Button element and grab its text. Open to other suggestions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shallow doesn't work here because we reach inside a Button element and grab its text. Open to other suggestions.

Remarked at #2140 (comment). It wasn't entirely as simple, but close:

diff --git a/editor/enable-tracking-prompt/test/index.js b/editor/enable-tracking-prompt/test/index.js
index 686fa0ec..098b95e0 100644
--- a/editor/enable-tracking-prompt/test/index.js
+++ b/editor/enable-tracking-prompt/test/index.js
@@ -1,7 +1,7 @@
 /**
  * External dependencies
  */
-import { mount } from 'enzyme';
+import { shallow } from 'enzyme';
 
 /**
  * Internal dependencies
@@ -29,13 +29,13 @@ describe( 'EnableTrackingPrompt', () => {
 	} );
 
 	it( 'should render a prompt with Yes and No buttons', () => {
-		const prompt = mount(
+		const prompt = shallow(
 			<EnableTrackingPrompt />
 		);
-		const buttons = prompt.find( '.button' );
+		const buttons = prompt.find( 'Button' );
 		expect( buttons.length ).toBe( 2 );
-		expect( buttons.at( 0 ).text() ).toBe( 'Yes' );
-		expect( buttons.at( 1 ).text() ).toBe( 'No' );
+		expect( buttons.at( 0 ).children().text() ).toBe( 'Yes' );
+		expect( buttons.at( 1 ).children().text() ).toBe( 'No' );
 
 		expect( window.setUserSetting )
 			.not.toHaveBeenCalled();
@@ -46,11 +46,11 @@ describe( 'EnableTrackingPrompt', () => {
 	} );
 
 	it( 'should enable tracking when clicking Yes', () => {
-		const prompt = mount(
+		const prompt = shallow(
 			<EnableTrackingPrompt removeNotice={ removeNotice } />
 		);
-		const buttonYes = prompt.find( '.button' )
-			.filterWhere( node => node.text() === 'Yes' );
+		const buttonYes = prompt.find( 'Button' )
+			.filterWhere( node => node.children().text() === 'Yes' );
 		buttonYes.simulate( 'click' );
 
 		expect( window.setUserSetting )
@@ -62,11 +62,11 @@ describe( 'EnableTrackingPrompt', () => {
 	} );
 
 	it( 'should disable tracking when clicking No', () => {
-		const prompt = mount(
+		const prompt = shallow(
 			<EnableTrackingPrompt removeNotice={ removeNotice } />
 		);
-		const buttonNo = prompt.find( '.button' )
-			.filterWhere( node => node.text() === 'No' );
+		const buttonNo = prompt.find( 'Button' )
+			.filterWhere( node => node.children().text() === 'No' );
 		buttonNo.simulate( 'click' );
 
 		expect( window.setUserSetting )

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still having problems getting this to work, specifically with the it( 'should show and hide a popover when clicking More info' ) test not covered in the above diff. See 861218d.


/**
* Internal dependencies
*/
import {
EnableTrackingPrompt,
TRACKING_PROMPT_NOTICE_ID,
} from '../';

describe( 'EnableTrackingPrompt', () => {
const tracking = require( '../../utils/tracking' ); // no default export
const originalSetUserSetting = window.setUserSetting;
const originalBumpStat = tracking.bumpStat;
let removeNotice;

beforeEach( () => {
window.setUserSetting = jest.fn();
tracking.bumpStat = jest.fn();
removeNotice = jest.fn();
} );

afterEach( () => {
window.setUserSetting = originalSetUserSetting;
tracking.bumpStat = originalBumpStat;
} );

it( 'should render a prompt with Yes and No buttons', () => {
const prompt = mount(
<EnableTrackingPrompt />
);
const buttons = prompt.find( '.button' );
expect( buttons.length ).toBe( 2 );
expect( buttons.at( 0 ).text() ).toBe( 'Yes' );
expect( buttons.at( 1 ).text() ).toBe( 'No' );

expect( window.setUserSetting )
.not.toHaveBeenCalled();
expect( tracking.bumpStat )
.not.toHaveBeenCalled();
expect( removeNotice )
.not.toHaveBeenCalled();
} );

it( 'should enable tracking when clicking Yes', () => {
const prompt = mount(
<EnableTrackingPrompt removeNotice={ removeNotice } />
);
const buttonYes = prompt.find( '.button' )
.filterWhere( node => node.text() === 'Yes' );
buttonYes.simulate( 'click' );

expect( window.setUserSetting )
.toHaveBeenCalledWith( 'gutenberg_tracking', 'on' );
expect( tracking.bumpStat )
.toHaveBeenCalledWith( 'tracking', 'opt-in' );
expect( removeNotice )
.toHaveBeenCalledWith( TRACKING_PROMPT_NOTICE_ID );
} );

it( 'should disable tracking when clicking No', () => {
const prompt = mount(
<EnableTrackingPrompt removeNotice={ removeNotice } />
);
const buttonNo = prompt.find( '.button' )
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To point above, this selector would not work with shallow rendering, but then again, it is not the place of the EnableTrackingPrompt to know that an element of class .button is rendered by the Button component. Replacing this with 'Button' should work just as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found that we couldn't look inside a shallow-rendered Button element to find its text:

console.log( { text: prompt.find( 'Button' ).at( 0 ).text() } );

{ text: '<Button />' }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've incorporated the change to Button instead of .button into #2205.

.filterWhere( node => node.text() === 'No' );
buttonNo.simulate( 'click' );

expect( window.setUserSetting )
.toHaveBeenCalledWith( 'gutenberg_tracking', 'off' );
expect( tracking.bumpStat )
.not.toHaveBeenCalled();
expect( removeNotice )
.toHaveBeenCalledWith( TRACKING_PROMPT_NOTICE_ID );
} );
} );
10 changes: 9 additions & 1 deletion editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import { settings } from '@wordpress/date';
import './assets/stylesheets/main.scss';
import Layout from './layout';
import { createReduxStore } from './state';
import { undo } from './actions';
import { undo, createInfoNotice } from './actions';
import EnableTrackingPrompt, { TRACKING_PROMPT_NOTICE_ID } from './enable-tracking-prompt';
import EditorSettingsProvider from './settings/provider';

/**
Expand Down Expand Up @@ -98,6 +99,13 @@ export function createEditorInstance( id, post, editorSettings = DEFAULT_SETTING
settings: editorSettings,
} );

if ( window.getUserSetting( 'gutenberg_tracking' ) === '' ) {
store.dispatch( createInfoNotice(
<EnableTrackingPrompt />,
TRACKING_PROMPT_NOTICE_ID
) );
}

preparePostState( store, post );

render(
Expand Down
3 changes: 3 additions & 0 deletions editor/inserter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { createBlock } from '@wordpress/blocks';
import InserterMenu from './menu';
import { getBlockInsertionPoint, getEditorMode } from '../selectors';
import { insertBlock, hideInsertionPoint } from '../actions';
import { bumpStat } from '../utils/tracking';

class Inserter extends Component {
constructor() {
Expand Down Expand Up @@ -49,6 +50,8 @@ class Inserter extends Component {
name,
insertionPoint
);
bumpStat( 'add_block_inserter', name.replace( /\//g, '__' ) );
bumpStat( 'add_block_total', name.replace( /\//g, '__' ) );
}

this.close();
Expand Down
3 changes: 3 additions & 0 deletions editor/modes/visual-editor/block-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
getMultiSelectedBlockUids,
} from '../../selectors';
import { insertBlock, multiSelect } from '../../actions';
import { bumpStat } from '../../utils/tracking';

const INSERTION_POINT_PLACEHOLDER = '[[insertion-point]]';
const { ENTER } = keycodes;
Expand Down Expand Up @@ -201,6 +202,8 @@ class VisualEditorBlockList extends Component {
insertBlock( name ) {
const newBlock = createBlock( name );
this.props.onInsertBlock( newBlock );
bumpStat( 'add_block_quick', name.replace( /\//g, '__' ) );
bumpStat( 'add_block_total', name.replace( /\//g, '__' ) );
}

toggleContinueWritingControls( showContinueWritingControls ) {
Expand Down
83 changes: 83 additions & 0 deletions editor/utils/test/tracking.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/* eslint-disable no-console */

/**
* Internal dependencies
*/
import { bumpStat } from '../tracking';

describe( 'bumpStat', () => {
const originalConsoleError = console.error;
const originalGetUserSetting = window.getUserSetting;

beforeEach( () => {
console.error = jest.fn();
window.getUserSetting = () => 'off';
} );

afterEach( () => {
console.error = originalConsoleError;
window.getUserSetting = originalGetUserSetting;
} );

it( 'should reject non-string stat groups', () => {
expect( bumpStat( 42, 'valid-name' ) ).toBeUndefined();
expect( console.error ).toHaveBeenCalledWith(
'Stat group names and stat names must be strings.'
);
} );

it( 'should reject non-string stat names', () => {
expect( bumpStat( 'valid_group', 42 ) ).toBeUndefined();
expect( console.error ).toHaveBeenCalledWith(
'Stat group names and stat names must be strings.'
);
} );

it( 'should reject group names with invalid characters', () => {
expect( bumpStat( 'invalid-group', 'valid-name' ) ).toBeUndefined();
expect( console.error ).toHaveBeenCalledWith(
'Stat group names must consist of letters, numbers, and underscores.'
);
} );

it( 'should reject group names longer than 22 chars', () => {
expect( bumpStat( Array( 23 + 1 ).join( 'x' ), 'valid-name' ) ).toBeUndefined();
expect( console.error ).toHaveBeenCalledWith(
'Stat group names cannot be longer than 22 characters.'
);
} );

it( 'should reject stat names with invalid characters', () => {
expect( bumpStat( 'group', 'invalidName' ) ).toBeUndefined();
expect( console.error ).toHaveBeenCalledWith(
'Stat names must consist of letters, numbers, underscores, and dashes.'
);
} );

it( 'should reject stat names longer than 32 chars', () => {
expect( bumpStat( 'name', Array( 33 + 1 ).join( 'x' ) ) ).toBeUndefined();
expect( console.error ).toHaveBeenCalledWith(
'Stat names cannot be longer than 32 characters.'
);
} );

it( 'should do nothing if the user has not opted in', () => {
expect( bumpStat( 'valid_group', 'valid-name' ) ).toBeUndefined();
expect( console.error ).not.toHaveBeenCalled();
} );

it( 'should bump valid stats', () => {
window.getUserSetting = () => 'on';
const url = bumpStat( 'valid_group', 'valid-name' );
// There are a couple of pieces of the URL where we don't care about
// testing the specific value. Replace them with placeholders.
const urlMatch = url
.replace( /^[a-z]+:/, 'PROTOCOL:' )
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In actual usage this will either be http: or https:. During test suite runs it is about:. This is difficult to change (jsdom/jsdom#1700) and it doesn't seem worth testing for a specific value.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, we could run assertions against individual pieces from url.parse.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've incorporated this change into #2205.

.replace( /t=[0-9.]+$/, 't=NUMBER' );
expect( urlMatch ).toBe(
'PROTOCOL://pixel.wp.com/g.gif?v=wpcom-no-pv'
+ '&x_gutenberg_valid_group=valid-name'
+ '&t=NUMBER'
);
} );
} );
Loading