[aider assisted] MM-61888: Add ClientSideUserIds field to MetricsSettings (#30127)

We add a new config setting to allow the admin to set a fixed
list of userIDs to track for all client side webapp metrics.

This gives the admin to get a deeper look at how the application
is behaving for a single user.

A new section in the system console is also added for the user
to edit this setting from the UI.

https://mattermost.atlassian.net/browse/MM-61888

```release-note
A new config setting MetricsSettings.ClientSideUserIds is added
where you can set the user ids you want to track for client side webapp
metrics.
```

* fix lint errors

```release-note
NONE
```

* fixing tests

```release-note
NONE
```
Этот коммит содержится в:
Agniva De Sarker
2025-02-13 21:10:34 +05:30
коммит произвёл GitHub
родитель 632a60b332
Коммит 1a58f923e0
13 изменённых файлов: 861 добавлений и 88 удалений

Просмотреть файл

@@ -0,0 +1,470 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/AdminConsole/ClientSideUserIdsSetting initial state with multiple items 1`] = `
<ClientSideUserIdsSetting
disabled={false}
id="MySetting"
onChange={[MockFunction]}
setByEnv={false}
value={
Array [
"userid1",
"userid2",
"id3",
]
}
>
<Memo(Settings)
helpText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Set the user ids you want to track for client side metrics. Separate values with a comma."
id="admin.customization.clientSideUserIdsDesc"
/>
}
inputId="MySetting"
label={
<Memo(MemoizedFormattedMessage)
defaultMessage="Client side user ids:"
id="admin.customization.clientSideUserIds"
/>
}
setByEnv={false}
>
<div
className="form-group"
data-testid="MySetting"
>
<label
className="control-label col-sm-4"
htmlFor="MySetting"
>
<FormattedMessage
defaultMessage="Client side user ids:"
id="admin.customization.clientSideUserIds"
>
<span>
Client side user ids:
</span>
</FormattedMessage>
</label>
<div
className="col-sm-8"
>
<LocalizedPlaceholderInput
className="form-control"
disabled={false}
id="MySetting"
onChange={[Function]}
placeholder={
Object {
"defaultMessage": "E.g.: \\"userid1,userid2\\"",
"id": "admin.customization.clientSideUserIdsPlaceholder",
}
}
type="text"
value="userid1,userid2,id3"
>
<input
className="form-control"
disabled={false}
id="MySetting"
onChange={[Function]}
placeholder="E.g.: \\"userid1,userid2\\""
type="text"
value="userid1,userid2,id3"
/>
</LocalizedPlaceholderInput>
<div
className="help-text"
data-testid="MySettinghelp-text"
>
<FormattedMessage
defaultMessage="Set the user ids you want to track for client side metrics. Separate values with a comma."
id="admin.customization.clientSideUserIdsDesc"
>
<span>
Set the user ids you want to track for client side metrics. Separate values with a comma.
</span>
</FormattedMessage>
</div>
</div>
</div>
</Memo(Settings)>
</ClientSideUserIdsSetting>
`;
exports[`components/AdminConsole/ClientSideUserIdsSetting initial state with no items 1`] = `
<ClientSideUserIdsSetting
disabled={false}
id="MySetting"
onChange={[MockFunction]}
setByEnv={false}
value={Array []}
>
<Memo(Settings)
helpText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Set the user ids you want to track for client side metrics. Separate values with a comma."
id="admin.customization.clientSideUserIdsDesc"
/>
}
inputId="MySetting"
label={
<Memo(MemoizedFormattedMessage)
defaultMessage="Client side user ids:"
id="admin.customization.clientSideUserIds"
/>
}
setByEnv={false}
>
<div
className="form-group"
data-testid="MySetting"
>
<label
className="control-label col-sm-4"
htmlFor="MySetting"
>
<FormattedMessage
defaultMessage="Client side user ids:"
id="admin.customization.clientSideUserIds"
>
<span>
Client side user ids:
</span>
</FormattedMessage>
</label>
<div
className="col-sm-8"
>
<LocalizedPlaceholderInput
className="form-control"
disabled={false}
id="MySetting"
onChange={[Function]}
placeholder={
Object {
"defaultMessage": "E.g.: \\"userid1,userid2\\"",
"id": "admin.customization.clientSideUserIdsPlaceholder",
}
}
type="text"
value=""
>
<input
className="form-control"
disabled={false}
id="MySetting"
onChange={[Function]}
placeholder="E.g.: \\"userid1,userid2\\""
type="text"
value=""
/>
</LocalizedPlaceholderInput>
<div
className="help-text"
data-testid="MySettinghelp-text"
>
<FormattedMessage
defaultMessage="Set the user ids you want to track for client side metrics. Separate values with a comma."
id="admin.customization.clientSideUserIdsDesc"
>
<span>
Set the user ids you want to track for client side metrics. Separate values with a comma.
</span>
</FormattedMessage>
</div>
</div>
</div>
</Memo(Settings)>
</ClientSideUserIdsSetting>
`;
exports[`components/AdminConsole/ClientSideUserIdsSetting initial state with one item 1`] = `
<ClientSideUserIdsSetting
disabled={false}
id="MySetting"
onChange={[MockFunction]}
setByEnv={false}
value={
Array [
"userid1",
]
}
>
<Memo(Settings)
helpText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Set the user ids you want to track for client side metrics. Separate values with a comma."
id="admin.customization.clientSideUserIdsDesc"
/>
}
inputId="MySetting"
label={
<Memo(MemoizedFormattedMessage)
defaultMessage="Client side user ids:"
id="admin.customization.clientSideUserIds"
/>
}
setByEnv={false}
>
<div
className="form-group"
data-testid="MySetting"
>
<label
className="control-label col-sm-4"
htmlFor="MySetting"
>
<FormattedMessage
defaultMessage="Client side user ids:"
id="admin.customization.clientSideUserIds"
>
<span>
Client side user ids:
</span>
</FormattedMessage>
</label>
<div
className="col-sm-8"
>
<LocalizedPlaceholderInput
className="form-control"
disabled={false}
id="MySetting"
onChange={[Function]}
placeholder={
Object {
"defaultMessage": "E.g.: \\"userid1,userid2\\"",
"id": "admin.customization.clientSideUserIdsPlaceholder",
}
}
type="text"
value="userid1"
>
<input
className="form-control"
disabled={false}
id="MySetting"
onChange={[Function]}
placeholder="E.g.: \\"userid1,userid2\\""
type="text"
value="userid1"
/>
</LocalizedPlaceholderInput>
<div
className="help-text"
data-testid="MySettinghelp-text"
>
<FormattedMessage
defaultMessage="Set the user ids you want to track for client side metrics. Separate values with a comma."
id="admin.customization.clientSideUserIdsDesc"
>
<span>
Set the user ids you want to track for client side metrics. Separate values with a comma.
</span>
</FormattedMessage>
</div>
</div>
</div>
</Memo(Settings)>
</ClientSideUserIdsSetting>
`;
exports[`components/AdminConsole/ClientSideUserIdsSetting renders properly when disabled 1`] = `
<ClientSideUserIdsSetting
disabled={true}
id="MySetting"
onChange={[MockFunction]}
setByEnv={false}
value={
Array [
"userid1",
"userid2",
]
}
>
<Memo(Settings)
helpText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Set the user ids you want to track for client side metrics. Separate values with a comma."
id="admin.customization.clientSideUserIdsDesc"
/>
}
inputId="MySetting"
label={
<Memo(MemoizedFormattedMessage)
defaultMessage="Client side user ids:"
id="admin.customization.clientSideUserIds"
/>
}
setByEnv={false}
>
<div
className="form-group"
data-testid="MySetting"
>
<label
className="control-label col-sm-4"
htmlFor="MySetting"
>
<FormattedMessage
defaultMessage="Client side user ids:"
id="admin.customization.clientSideUserIds"
>
<span>
Client side user ids:
</span>
</FormattedMessage>
</label>
<div
className="col-sm-8"
>
<LocalizedPlaceholderInput
className="form-control"
disabled={true}
id="MySetting"
onChange={[Function]}
placeholder={
Object {
"defaultMessage": "E.g.: \\"userid1,userid2\\"",
"id": "admin.customization.clientSideUserIdsPlaceholder",
}
}
type="text"
value="userid1,userid2"
>
<input
className="form-control"
disabled={true}
id="MySetting"
onChange={[Function]}
placeholder="E.g.: \\"userid1,userid2\\""
type="text"
value="userid1,userid2"
/>
</LocalizedPlaceholderInput>
<div
className="help-text"
data-testid="MySettinghelp-text"
>
<FormattedMessage
defaultMessage="Set the user ids you want to track for client side metrics. Separate values with a comma."
id="admin.customization.clientSideUserIdsDesc"
>
<span>
Set the user ids you want to track for client side metrics. Separate values with a comma.
</span>
</FormattedMessage>
</div>
</div>
</div>
</Memo(Settings)>
</ClientSideUserIdsSetting>
`;
exports[`components/AdminConsole/ClientSideUserIdsSetting renders properly when set by environment variable 1`] = `
<ClientSideUserIdsSetting
disabled={false}
id="MySetting"
onChange={[MockFunction]}
setByEnv={true}
value={
Array [
"userid1",
"userid2",
]
}
>
<Memo(Settings)
helpText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Set the user ids you want to track for client side metrics. Separate values with a comma."
id="admin.customization.clientSideUserIdsDesc"
/>
}
inputId="MySetting"
label={
<Memo(MemoizedFormattedMessage)
defaultMessage="Client side user ids:"
id="admin.customization.clientSideUserIds"
/>
}
setByEnv={true}
>
<div
className="form-group"
data-testid="MySetting"
>
<label
className="control-label col-sm-4"
htmlFor="MySetting"
>
<FormattedMessage
defaultMessage="Client side user ids:"
id="admin.customization.clientSideUserIds"
>
<span>
Client side user ids:
</span>
</FormattedMessage>
</label>
<div
className="col-sm-8"
>
<LocalizedPlaceholderInput
className="form-control"
disabled={true}
id="MySetting"
onChange={[Function]}
placeholder={
Object {
"defaultMessage": "E.g.: \\"userid1,userid2\\"",
"id": "admin.customization.clientSideUserIdsPlaceholder",
}
}
type="text"
value="userid1,userid2"
>
<input
className="form-control"
disabled={true}
id="MySetting"
onChange={[Function]}
placeholder="E.g.: \\"userid1,userid2\\""
type="text"
value="userid1,userid2"
/>
</LocalizedPlaceholderInput>
<div
className="help-text"
data-testid="MySettinghelp-text"
>
<FormattedMessage
defaultMessage="Set the user ids you want to track for client side metrics. Separate values with a comma."
id="admin.customization.clientSideUserIdsDesc"
>
<span>
Set the user ids you want to track for client side metrics. Separate values with a comma.
</span>
</FormattedMessage>
</div>
<SetByEnv>
<div
className="alert alert-warning"
>
<FormattedMessage
defaultMessage="This setting has been set through an environment variable. It cannot be changed through the System Console."
id="admin.set_by_env"
>
<span>
This setting has been set through an environment variable. It cannot be changed through the System Console.
</span>
</FormattedMessage>
</div>
</SetByEnv>
</div>
</div>
</Memo(Settings)>
</ClientSideUserIdsSetting>
`;

Просмотреть файл

@@ -49,6 +49,7 @@ import CompanyInfo, {searchableStrings as billingCompanyInfoSearchableStrings} f
import CompanyInfoEdit from './billing/company_info_edit';
import BleveSettings, {searchableStrings as bleveSearchableStrings} from './bleve_settings';
import BrandImageSetting from './brand_image_setting/brand_image_setting';
import ClientSideUserIdsSetting from './client_side_userids_setting';
import ClusterSettings, {searchableStrings as clusterSearchableStrings} from './cluster_settings';
import CustomEnableDisableGuestAccountsSetting from './custom_enable_disable_guest_accounts_setting';
import CustomTermsOfServiceSettings from './custom_terms_of_service_settings';
@@ -1963,6 +1964,15 @@ const AdminDefinition: AdminDefinitionType = {
it.configIsFalse('MetricsSettings', 'Enable'),
),
},
{
type: 'custom',
key: 'MetricsSettings.ClientSideUserIds',
component: ClientSideUserIdsSetting,
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.PERFORMANCE_MONITORING)),
it.configIsFalse('MetricsSettings', 'EnableClientMetrics'),
),
},
{
type: 'text',
key: 'MetricsSettings.ListenAddress',

Просмотреть файл

@@ -0,0 +1,133 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import ClientSideUserIdsSetting from './client_side_userids_setting';
describe('components/AdminConsole/ClientSideUserIdsSetting', () => {
const baseProps = {
id: 'MySetting',
value: ['userid1', 'userid2'],
onChange: jest.fn(),
disabled: false,
setByEnv: false,
};
describe('initial state', () => {
test('with no items', () => {
const props = {
...baseProps,
value: [],
};
const wrapper = mountWithIntl(
<ClientSideUserIdsSetting {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.state('value')).toEqual('');
});
test('with one item', () => {
const props = {
...baseProps,
value: ['userid1'],
};
const wrapper = mountWithIntl(
<ClientSideUserIdsSetting {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.state('value')).toEqual('userid1');
});
test('with multiple items', () => {
const props = {
...baseProps,
value: ['userid1', 'userid2', 'id3'],
};
const wrapper = mountWithIntl(
<ClientSideUserIdsSetting {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.state('value')).toEqual('userid1,userid2,id3');
});
});
describe('onChange', () => {
test('called on change to empty', () => {
const props = {
...baseProps,
onChange: jest.fn(),
};
const wrapper = mountWithIntl(
<ClientSideUserIdsSetting {...props}/>,
);
wrapper.find('input').simulate('change', {target: {value: ''}});
expect(props.onChange).toBeCalledWith(baseProps.id, []);
});
test('called on change to one item', () => {
const props = {
...baseProps,
onChange: jest.fn(),
};
const wrapper = mountWithIntl(
<ClientSideUserIdsSetting {...props}/>,
);
wrapper.find('input').simulate('change', {target: {value: ' id2 '}});
expect(props.onChange).toBeCalledWith(baseProps.id, ['id2']);
});
test('called on change to two items', () => {
const props = {
...baseProps,
onChange: jest.fn(),
};
const wrapper = mountWithIntl(
<ClientSideUserIdsSetting {...props}/>,
);
wrapper.find('input').simulate('change', {target: {value: 'id1, id99'}});
expect(props.onChange).toBeCalledWith(baseProps.id, ['id1', 'id99']);
});
});
test('renders properly when disabled', () => {
const props = {
...baseProps,
disabled: true,
};
const wrapper = mountWithIntl(
<ClientSideUserIdsSetting {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('renders properly when set by environment variable', () => {
const props = {
...baseProps,
setByEnv: true,
};
const wrapper = mountWithIntl(
<ClientSideUserIdsSetting {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
});

Просмотреть файл

@@ -0,0 +1,81 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import type {ChangeEvent} from 'react';
import {defineMessage, FormattedMessage} from 'react-intl';
import LocalizedPlaceholderInput from 'components/localized_placeholder_input';
import Setting from './setting';
type Props = {
id: string;
value: string[];
onChange: (id: string, valueAsArray: string[]) => void;
disabled: boolean;
setByEnv: boolean;
}
type State = {
value: string;
}
export default class ClientSideUserIdsSetting extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
value: this.arrayToString(props.value),
};
}
stringToArray = (str: string): string[] => {
return str.split(',').map((s) => s.trim()).filter(Boolean);
};
arrayToString = (arr: string[]): string => {
return arr.join(',');
};
handleChange = (e: ChangeEvent<HTMLInputElement>): void => {
const valueAsArray = this.stringToArray(e.target.value);
this.props.onChange(this.props.id, valueAsArray);
this.setState({
value: e.target.value,
});
};
render() {
return (
<Setting
label={
<FormattedMessage
id='admin.customization.clientSideUserIds'
defaultMessage='Client side user ids:'
/>
}
helpText={
<FormattedMessage
id='admin.customization.clientSideUserIdsDesc'
defaultMessage='Set the user ids you want to track for client side metrics. Separate values with a comma.'
/>
}
inputId={this.props.id}
setByEnv={this.props.setByEnv}
>
<LocalizedPlaceholderInput
id={this.props.id}
className='form-control'
type='text'
placeholder={defineMessage({id: 'admin.customization.clientSideUserIdsPlaceholder', defaultMessage: 'E.g.: "userid1,userid2"'})}
value={this.state.value}
onChange={this.handleChange}
disabled={this.props.disabled || this.props.setByEnv}
/>
</Setting>
);
}
}

Просмотреть файл

@@ -656,6 +656,9 @@
"admin.customization.announcement.enableBannerTitle": "Enable System-wide Notifications:",
"admin.customization.appDownloadLinkDesc": "Add a link to a download page for the Mattermost apps. When a link is present, an option to \"Download Mattermost Apps\" will be added in the Product Menu so users can find the download page. Leave this field blank to hide the option from the Product Menu.",
"admin.customization.appDownloadLinkTitle": "Mattermost Apps Download Page Link:",
"admin.customization.clientSideUserIds": "Client side user ids:",
"admin.customization.clientSideUserIdsDesc": "Set the user ids you want to track for client side metrics. Separate values with a comma.",
"admin.customization.clientSideUserIdsPlaceholder": "E.g.: \"userid1,userid2\"",
"admin.customization.customUrlSchemes": "Custom URL Schemes:",
"admin.customization.customUrlSchemesDesc": "Allows message text to link if it begins with any of the comma-separated URL schemes listed. By default, the following schemes will create links: \"http\", \"https\", \"ftp\", \"tel\", and \"mailto\".",
"admin.customization.customUrlSchemesPlaceholder": "E.g.: \"git,smtp\"",