Skip to content

Commit

Permalink
feat: new ui usage information tracking (#627)
Browse files Browse the repository at this point in the history
* chore: collect analytics data on new ui features

* feat: add enabled param for analytics and ability to turn it of with NO_ANALYTICS env

* test: implement e2e tests on analytics

* docs: update docs on usage info tracking

* fix: sync package-lock with package.json

* fix: update e2e tests and metrika init settings
  • Loading branch information
shadowusr authored Jan 14, 2025
1 parent 3d04404 commit c774f69
Show file tree
Hide file tree
Showing 31 changed files with 449 additions and 102 deletions.
48 changes: 17 additions & 31 deletions docs/en/html-reporter-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,41 +311,27 @@ customScripts: [

### yandexMetrika

This parameter allows you to add [Yandex.Metrika][yandex-metrika] to the report. The parameter is set as an object with the key `counterNumber`. As the key value, you must specify the Yandex.Metrica counter number (see "[How to create a counter][how-to-create-counter]"). The number should be set as a Number, not a String.
By default, anonymous html-reporter interface usage information is collected for us to analyze usage patterns and improve UX. We collect such info as html-reporter loading speed, how often certain UI features are used (e.g. sorting tests) or clicks on UI elements. NO information about your project or tests is ever tracked.

Also, in the Yandex.Metrika interface, go to the _"Counter"_ tab in the settings section, click _"Copy"_ and paste the counter code into the [customScripts](#customscripts) field.
If you want to opt out, choose any of the options below:

With the help of metrics, you can find out how developers interact with your report and what kind of problems they face.

The report supports the following [goals of metrics][yandex-metrika-goals]:

* **ACCEPT_SCREENSHOT**—there was a click on the _Accept_ button to accept a screenshot;
* **ACCEPT_OPENED_SCREENSHOTS**—there was a click on the _Accept opened_ button to accept screenshots from open tests.

Example of setting up Yandex.Metrika in one of the projects:

```javascript
module.exports = {
plugins: {
'html-reporter/hermione': {
customScripts: [
function(){(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)}; m[i].l=1*new Date();k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)}) (window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym"); ym(56782912, "init", { clickmap:true, trackLinks:true, accurateTrackBounce:true, webvisor:true })},

// other scripts...
],
yandexMetrika: {
counterNumber: 1234567
- Edit your config:
```javascript
module.exports = {
plugins: {
'html-reporter/testplane': {
yandexMetrika: {
enabled: false
},
// other html-reporter settings...
},

// other plugin settings...
// other Testplane plugins...
},

// other hermione plugins...
},

// other hermione settings...
};
```
// other Testplane settings...
};
```
- Using environment variables: `html_reporter_yandex_metrika_enabled=false` or simply `NO_ANALYTICS=true`
- Using CLI arguments: `--html-reporter-yandex_metrika_enabled=false`

### Passing parameters via the CLI

Expand Down
48 changes: 17 additions & 31 deletions docs/ru/html-reporter-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,41 +312,27 @@ customScripts: [

### yandexMetrika

Данный параметр позволяет добавить в отчет [Яндекс.Метрику][yandex-metrika]. Параметр задается в виде объекта с ключом `counterNumber` _(номер счетчика)_. В качестве значения ключа необходимо указать номер счетчика Яндекс.Метрики (см. «[Как создать счетчик][how-to-create-counter]»). Номер должен задаваться как число _(Number)_, а не строка.
По умолчанию выполняется сбор анонимных сведений об использовании интерфейса отчета в целях анализа и улучшения UX. Собираются такие сведения, как скорость загрузки отчета, частота использования некоторых функций (например, сортировка тестов) и клики по элементам управления. Сведения о вашем проекте или содержимом тестов НЕ собираются ни при каких обстоятельствах.

Также в интерфейсе Яндекс.Метрики необходимо перейти в разделе настроек на вкладку _«Счетчик»_, нажать кнопку _«Скопировать»_ и вставить код счетчика в поле [customScripts](#customscripts).
Если вы не хотите делиться аналитикой с нами, вы можете отключить это любым из способов:

С помощью метрики вы сможете узнать как разработчики взаимодействуют с вашим отчетом и с какого рода проблемами они сталкиваются.

Отчет поддерживает следующие [цели для метрики][yandex-metrika-goals]:

* **ACCEPT_SCREENSHOT** — было нажатие на кнопку _Accept_ для принятия скриншота;
* **ACCEPT_OPENED_SCREENSHOTS** — было нажатие на кнопку _Accept opened_ для принятия скриншотов из открытых тестов.

Пример настройки _Яндекс.Метрики_ в одном из проектов:

```javascript
module.exports = {
plugins: {
'html-reporter/hermione': {
customScripts: [
function(){(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)}; m[i].l=1*new Date();k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)}) (window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym"); ym(56782912, "init", { clickmap:true, trackLinks:true, accurateTrackBounce:true, webvisor:true })},

// другие скрипты...
],
yandexMetrika: {
counterNumber: 1234567
- В конфиге
```javascript
module.exports = {
plugins: {
'html-reporter/testplane': {
yandexMetrika: {
enabled: false
},
// другие настройки html-reporter...
},

// другие настройки плагина...
// другие плагины Testplane...
},

// другие плагины гермионы...
},

// другие настройки гермионы...
};
```
// другие настройки Testplane...
};
```
- С помощью переменных окружения: `html_reporter_yandex_metrika_enabled=false` или просто `NO_ANALYTICS=true`
- С помощью аргументов CLI: `--html-reporter-yandex_metrika_enabled=false`

### Передача параметров через CLI

Expand Down
20 changes: 18 additions & 2 deletions lib/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const assertType = <T>(name: string, validationFn: (value: unknown) => value is
};
const assertString = (name: string): AssertionFn<string> => assertType(name, _.isString, 'string');
const assertBoolean = (name: string): AssertionFn<boolean> => assertType(name, _.isBoolean, 'boolean');
const assertNumber = (name: string): AssertionFn<number> => assertType(name, _.isNumber, 'number');
export const assertNumber = (name: string): AssertionFn<number> => assertType(name, _.isNumber, 'number');
const assertPlainObject = (name: string): AssertionFn<Record<string, unknown>> => assertType(name, isPlainObject, 'plain object');

const assertSaveFormat = (saveFormat: unknown): asserts saveFormat is SaveFormat => {
Expand Down Expand Up @@ -206,11 +206,27 @@ const getParser = (): ReturnType<typeof root<ReporterConfig>> => {
validate: assertArrayOf('functions', 'customScripts', _.isFunction)
}),
yandexMetrika: section({
enabled: option({
defaultValue: () => {
return !(process.env.NO_ANALYTICS && JSON.parse(process.env.NO_ANALYTICS));
},
parseEnv: JSON.parse,
parseCli: JSON.parse,
validate: assertBoolean('yandexMetrika.enabled'),
map: (value: boolean) => {
if (process.env.NO_ANALYTICS && JSON.parse(process.env.NO_ANALYTICS)) {
return false;
}

return value;
}
}),
counterNumber: option({
isDeprecated: true,
defaultValue: configDefaults.yandexMetrika.counterNumber,
parseEnv: Number,
parseCli: Number,
validate: (value) => _.isNull(value) || assertNumber('yandexMetrika.counterNumber')(value)
map: () => configDefaults.yandexMetrika.counterNumber
})
}),
pluginsEnabled: option({
Expand Down
2 changes: 1 addition & 1 deletion lib/constants/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const configDefaults: StoreReporterConfig = {
saveErrorDetails: false,
saveFormat: SaveFormat.SQLITE,
yandexMetrika: {
counterNumber: null
counterNumber: 99267510
},
staticImageAccepter: {
enabled: false,
Expand Down
3 changes: 3 additions & 0 deletions lib/static/components/controls/report-info.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@ import {isEmpty} from 'lodash';
import {version} from '../../../../package.json';
import useLocalStorage from '@/static/hooks/useLocalStorage';
import {LocalStorageKey, UiMode} from '@/constants/local-storage';
import {useAnalytics} from '@/static/new-ui/hooks/useAnalytics';

function ReportInfo(props) {
const analytics = useAnalytics();
const {gui, timestamp} = props;
const lang = isEmpty(navigator.languages) ? navigator.language : navigator.languages[0];
const date = new Date(timestamp).toLocaleString(lang);

const [, setUiMode] = useLocalStorage(LocalStorageKey.UIMode, UiMode.New);

const onNewUiButtonClick = () => {
analytics?.trackFeatureUsage({featureName: 'Switch to new UI'});
setUiMode(UiMode.New);

const targetUrl = new URL(window.location.href);
Expand Down
2 changes: 2 additions & 0 deletions lib/static/components/gui.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {ClientEvents} from '../../gui/constants/client-events';
import FaviconChanger from './favicon-changer';
import ExtensionPoint from './extension-point';
import BottomProgressBar from './bottom-progress-bar';
import {MetrikaScript} from '@/static/new-ui/components/MetrikaScript';

class Gui extends Component {
static propTypes = {
Expand Down Expand Up @@ -81,6 +82,7 @@ class Gui extends Component {
<Fragment>
<ExtensionPoint name={ROOT}>
<CustomScripts scripts={customScripts}/>
<MetrikaScript />
{notificationElem}
<FaviconChanger />
<StickyHeader />
Expand Down
2 changes: 2 additions & 0 deletions lib/static/components/report.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {CustomScripts} from '../new-ui/components/CustomScripts';
import FaviconChanger from './favicon-changer';
import ExtensionPoint from './extension-point';
import BottomProgressBar from './bottom-progress-bar';
import {MetrikaScript} from '@/static/new-ui/components/MetrikaScript';

class Report extends Component {
static propTypes = {
Expand Down Expand Up @@ -50,6 +51,7 @@ class Report extends Component {
<Fragment>
<ExtensionPoint name={ROOT}>
<CustomScripts scripts={this.props.customScripts}/>
<MetrikaScript />
{notificationElem}
<FaviconChanger />
<StickyHeader />
Expand Down
29 changes: 28 additions & 1 deletion lib/static/modules/middlewares/metrika.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export function getMetrikaMiddleware(analytics: YandexMetrika): Middleware<{}, S
analytics.setVisitParams({
[action.type]: Date.now() - startLoadTime,
initView: state.view,
testsCount
testsCount,
isNewUi: Boolean(state?.app?.isNewUi)
});

return result;
Expand Down Expand Up @@ -82,6 +83,32 @@ export function getMetrikaMiddleware(analytics: YandexMetrika): Middleware<{}, S
return next(action);
}

case actionNames.GROUP_TESTS_SET_CURRENT_EXPRESSION: {
analytics.trackFeatureUsage({featureName: action.type, groupByKey: action.payload.expressionIds[0]});

return next(action);
}

case actionNames.SORT_TESTS_SET_CURRENT_EXPRESSION: {
analytics.trackFeatureUsage({
featureName: action.type,
sortByKey: action.payload.expressionIds[0],
sortDirection: store.getState().app.sortTestsData.currentDirection
});

return next(action);
}

case actionNames.SORT_TESTS_SET_DIRECTION: {
analytics.trackFeatureUsage({
featureName: action.type,
sortByKey: store.getState().app.sortTestsData.currentExpressionIds[0],
sortDirection: action.payload.direction
});

return next(action);
}

case actionNames.OPEN_MODAL:
case actionNames.CLOSE_MODAL: {
const modalId = get(action, 'payload.id', action.type);
Expand Down
4 changes: 2 additions & 2 deletions lib/static/modules/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ if (process.env.NODE_ENV !== 'production') {
}

const metrikaConfig = (window.data || {}).config?.yandexMetrika;
const areAnalyticsEnabled = metrikaConfig?.enabled && metrikaConfig?.counterId;
const areAnalyticsEnabled = metrikaConfig?.enabled && metrikaConfig?.counterNumber;
const isYaMetrikaAvailable = window.ym && typeof window.ym === 'function';

if (areAnalyticsEnabled && isYaMetrikaAvailable) {
const metrika = new YandexMetrika();
const metrika = new YandexMetrika(areAnalyticsEnabled && isYaMetrikaAvailable);
const metrikaMiddleware = getMetrikaMiddleware(metrika);

middlewares.push(metrikaMiddleware);
Expand Down
23 changes: 22 additions & 1 deletion lib/static/modules/yandex-metrika.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {SortDirection} from '@/static/new-ui/types/store';

enum YandexMetrikaMethod {
ReachGoal = 'reachGoal',
Params = 'params',
Expand All @@ -24,10 +26,29 @@ interface ScreenshotAcceptData {
acceptedImagesCount: number;
}

interface FeatureUsageData {
interface BasicFeatureUsageData {
featureName: string;
}

interface GroupByFeatureUsageData extends BasicFeatureUsageData {
groupByKey: string;
}

interface SortByFeatureUsageData extends BasicFeatureUsageData {
sortByKey: string;
sortDirection: SortDirection;
}

interface ChangeTreeViewModeFeatureUsageData extends BasicFeatureUsageData {
treeViewMode: string;
}

type FeatureUsageData =
| BasicFeatureUsageData
| GroupByFeatureUsageData
| SortByFeatureUsageData
| ChangeTreeViewModeFeatureUsageData;

export class YandexMetrika {
protected readonly _isEnabled: boolean;
protected readonly _counterNumber: number;
Expand Down
2 changes: 2 additions & 0 deletions lib/static/new-ui/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import store from '../../modules/store';
import {CustomScripts} from '@/static/new-ui/components/CustomScripts';
import {State} from '@/static/new-ui/types/store';
import {AnalyticsProvider} from '@/static/new-ui/providers/analytics';
import {MetrikaScript} from '@/static/new-ui/components/MetrikaScript';

export function App(): ReactNode {
const pages = [
Expand All @@ -36,6 +37,7 @@ export function App(): ReactNode {
<ThemeProvider theme='light'>
<ToasterProvider>
<Provider store={store}>
<MetrikaScript/>
<AnalyticsProvider>
<HashRouter>
<MainLayout menuItems={pages}>
Expand Down
3 changes: 3 additions & 0 deletions lib/static/new-ui/components/AttemptPicker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {Button, Icon, Spin} from '@gravity-ui/uikit';
import {RunTestsFeature} from '@/constants';
import {thunkRunTest} from '@/static/modules/actions';
import {getCurrentBrowser, getCurrentResultId} from '@/static/new-ui/features/suites/selectors';
import {useAnalytics} from '@/static/new-ui/hooks/useAnalytics';

interface AttemptPickerProps {
onChange?: (browserId: string, resultId: string, attemptIndex: number) => unknown;
Expand All @@ -23,6 +24,7 @@ interface AttemptPickerInternalProps extends AttemptPickerProps {
function AttemptPickerInternal(props: AttemptPickerInternalProps): ReactNode {
const {resultIds, currentResultId} = props;

const analytics = useAnalytics();
const dispatch = useDispatch();
const currentBrowser = useSelector(getCurrentBrowser);
const isRunTestsAvailable = useSelector(state => state.app.availableFeatures)
Expand All @@ -39,6 +41,7 @@ function AttemptPickerInternal(props: AttemptPickerInternalProps): ReactNode {

const onRetryTestHandler = (): void => {
if (currentBrowser) {
analytics?.trackFeatureUsage({featureName: 'Attempt picker: retry test button click'});
dispatch(thunkRunTest({test: {testName: currentBrowser.parentId, browserName: currentBrowser.name}}));
}
};
Expand Down
1 change: 1 addition & 0 deletions lib/static/new-ui/components/MainLayout/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export function Footer(props: FooterProps): ReactNode {
title: 'Info',
onItemClick: props.onFooterItemClick,
current: isInfoCurrent,
qa: 'footer-item-info',
itemWrapper: (params, makeItem) => makeItem({
...params,
icon: <Icon className={classNames({
Expand Down
14 changes: 12 additions & 2 deletions lib/static/new-ui/components/MainLayout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import styles from './index.module.css';
import {Footer} from './Footer';
import {EmptyReportCard} from '@/static/new-ui/components/Card/EmptyReportCard';
import {InfoPanel} from '@/static/new-ui/components/InfoPanel';
import {useAnalytics} from '@/static/new-ui/hooks/useAnalytics';

export enum PanelId {
Settings = 'settings',
Expand All @@ -31,13 +32,17 @@ export interface MainLayoutProps {
export function MainLayout(props: MainLayoutProps): ReactNode {
const navigate = useNavigate();
const location = useLocation();
const analytics = useAnalytics();

const gravityMenuItems: GravityMenuItem[] = props.menuItems.map(item => ({
id: item.url,
title: item.title,
icon: item.icon,
current: Boolean(matchPath(`${item.url.replace(/\/$/, '')}/*`, location.pathname)),
onItemClick: () => navigate(item.url)
onItemClick: (): void => {
analytics?.trackFeatureUsage({featureName: `Go to ${item.url} page`});
navigate(item.url);
}
}));

const isInitialized = useSelector(getIsInitialized);
Expand All @@ -47,7 +52,12 @@ export function MainLayout(props: MainLayoutProps): ReactNode {

const [visiblePanel, setVisiblePanel] = useState<PanelId | null>(null);
const onFooterItemClick = (item: GravityMenuItem): void => {
visiblePanel === item.id ? setVisiblePanel(null) : setVisiblePanel(item.id as PanelId);
if (visiblePanel === item.id) {
setVisiblePanel(null);
} else {
setVisiblePanel(item.id as PanelId);
analytics?.trackFeatureUsage({featureName: `Open ${item.id} panel`});
}
};

return <AsideHeader
Expand Down
Loading

0 comments on commit c774f69

Please sign in to comment.