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

feat: new ui usage information tracking #627

Merged
merged 6 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
shadowusr marked this conversation as resolved.
Show resolved Hide resolved
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
Loading