Skip to content

Commit

Permalink
Add opt-in usage tracking for blocks added to post content (#2140)
Browse files Browse the repository at this point in the history
  • Loading branch information
nylen authored Aug 3, 2017
1 parent b9deeb8 commit 51466b2
Show file tree
Hide file tree
Showing 12 changed files with 356 additions and 5 deletions.
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
} = 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';

/**
* 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' )
.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:' )
.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

0 comments on commit 51466b2

Please sign in to comment.