Share: Add analytics to invite user flow (#99116)

This commit is contained in:
Ezequiel Victorero 2025-01-21 11:47:57 -03:00 committed by GitHub
parent c7edbffd82
commit 865e911e10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 113 additions and 18 deletions

View File

@ -170,6 +170,8 @@ export interface GrafanaConfig {
externalUserMngLinkUrl: string;
externalUserMngLinkName: string;
externalUserMngInfo: string;
externalUserMngAnalytics: boolean;
externalUserMngAnalyticsParams: string;
allowOrgCreate: boolean;
disableLoginForm: boolean;
defaultDatasource: string;

View File

@ -73,6 +73,8 @@ export class GrafanaBootConfig implements GrafanaConfig {
externalUserMngLinkUrl = '';
externalUserMngLinkName = '';
externalUserMngInfo = '';
externalUserMngAnalytics = false;
externalUserMngAnalyticsParams = '';
allowOrgCreate = false;
feedbackLinksEnabled = true;
disableLoginForm = false;

View File

@ -200,6 +200,8 @@ type FrontendSettingsDTO struct {
ExternalUserMngInfo string `json:"externalUserMngInfo"`
ExternalUserMngLinkUrl string `json:"externalUserMngLinkUrl"`
ExternalUserMngLinkName string `json:"externalUserMngLinkName"`
ExternalUserMngAnalytics bool `json:"externalUserMngAnalytics"`
ExternalUserMngAnalyticsParams string `json:"externalUserMngAnalyticsParams"`
ViewersCanEdit bool `json:"viewersCanEdit"`
AngularSupportEnabled bool `json:"angularSupportEnabled"`
EditorsCanAdmin bool `json:"editorsCanAdmin"`

View File

@ -225,6 +225,8 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
ExternalUserMngInfo: hs.Cfg.ExternalUserMngInfo,
ExternalUserMngLinkUrl: hs.Cfg.ExternalUserMngLinkUrl,
ExternalUserMngLinkName: hs.Cfg.ExternalUserMngLinkName,
ExternalUserMngAnalytics: hs.Cfg.ExternalUserMngAnalytics,
ExternalUserMngAnalyticsParams: hs.Cfg.ExternalUserMngAnalyticsParams,
ViewersCanEdit: hs.Cfg.ViewersCanEdit,
AngularSupportEnabled: hs.Cfg.AngularSupportEnabled,
EditorsCanAdmin: hs.Cfg.EditorsCanAdmin,

View File

@ -406,6 +406,8 @@ type Cfg struct {
ExternalUserMngLinkUrl string
ExternalUserMngLinkName string
ExternalUserMngInfo string
ExternalUserMngAnalytics bool
ExternalUserMngAnalyticsParams string
AutoAssignOrg bool
AutoAssignOrgId int
AutoAssignOrgRole string
@ -1713,6 +1715,8 @@ func readUserSettings(iniFile *ini.File, cfg *Cfg) error {
cfg.ExternalUserMngLinkUrl = valueAsString(users, "external_manage_link_url", "")
cfg.ExternalUserMngLinkName = valueAsString(users, "external_manage_link_name", "")
cfg.ExternalUserMngInfo = valueAsString(users, "external_manage_info", "")
cfg.ExternalUserMngAnalytics = users.Key("external_manage_analytics").MustBool(false)
cfg.ExternalUserMngAnalyticsParams = valueAsString(users, "external_manage_analytics_params", "")
cfg.ViewersCanEdit = users.Key("viewers_can_edit").MustBool(false)
cfg.EditorsCanAdmin = users.Key("editors_can_admin").MustBool(false)

View File

@ -59,6 +59,34 @@ describe('ShareMenu', () => {
expect(await screen.queryByTestId(selector.inviteUser)).not.toBeInTheDocument();
});
it('should render invite user with analytics when config is provided', async () => {
Object.defineProperty(contextSrv, 'isSignedIn', {
value: true,
});
grantUserPermissions([AccessControlAction.OrgUsersAdd]);
config.externalUserMngLinkUrl = 'http://localhost:3000/users';
config.externalUserMngAnalytics = true;
config.externalUserMngAnalyticsParams = 'src=grafananet&other=value1';
setup({ meta: { canEdit: true } });
const inviteUser = await screen.findByTestId(selector.inviteUser);
// Mock window.open
const windowOpenMock = jest.spyOn(window, 'open').mockImplementation(() => null);
// Simulate click event
inviteUser.click();
// Assert window.open was called with the correct URL
expect(windowOpenMock).toHaveBeenCalledWith(
'http://localhost:3000/users?src=grafananet&other=value1&cnt=share-invite',
'_blank'
);
// Restore the original implementation
windowOpenMock.mockRestore();
});
it('should not render invite user when externalUserMngLinkUrl is not provided', async () => {
Object.defineProperty(contextSrv, 'isSignedIn', {
value: true,

View File

@ -13,6 +13,7 @@ import { AccessControlAction } from 'app/types';
import { isPublicDashboardsEnabled } from '../../../dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
import { getTrackingSource, shareDashboardType } from '../../../dashboard/components/ShareModal/utils';
import { getExternalUserMngLinkUrl } from '../../../users/utils';
import { DashboardScene } from '../../scene/DashboardScene';
import { DashboardInteractions } from '../../utils/interactions';
@ -93,7 +94,9 @@ export default function ShareMenu({ dashboard, panel }: { dashboard: DashboardSc
label: t('share-dashboard.menu.invite-user-title', 'Invite new member'),
renderCondition: !!config.externalUserMngLinkUrl && contextSrv.hasPermission(AccessControlAction.OrgUsersAdd),
onClick: () => {
window.open(config.externalUserMngLinkUrl, '_blank');
const url = getExternalUserMngLinkUrl('share-invite');
window.open(url.toString(), '_blank');
},
renderDividerAbove: true,
component: () => <Icon name="external-link-alt" className={styles.inviteUserItemIcon} />,

View File

@ -25,6 +25,9 @@ const setup = (propOverrides?: object) => {
Object.assign(props, propOverrides);
config.externalUserMngLinkUrl = props.externalUserMngLinkUrl;
config.externalUserMngLinkName = props.externalUserMngLinkName;
const { rerender } = render(<UsersActionBarUnconnected {...props} />);
return { rerender, props };
@ -53,11 +56,38 @@ describe('Render', () => {
it('should show external user management button', () => {
setup({
externalUserMngLinkUrl: 'some/url',
externalUserMngLinkUrl: 'http://some/url',
externalUserMngLinkName: 'someUrl',
});
expect(screen.getByRole('link', { name: 'someUrl' })).toHaveAttribute('href', 'some/url');
expect(screen.getByRole('link', { name: 'someUrl' })).toHaveAttribute('href', 'http://some/url');
});
it('should show external user management button with analytics values when configured', () => {
config.externalUserMngAnalytics = true;
config.externalUserMngAnalyticsParams = 'src=grafananet&other=value1';
setup({
externalUserMngLinkUrl: 'http://some/url',
externalUserMngLinkName: 'someUrl',
});
expect(screen.getByRole('link', { name: 'someUrl' })).toHaveAttribute(
'href',
'http://some/url?src=grafananet&other=value1&cnt=manage-users'
);
});
it('should show external user management button without analytics values when disabled', () => {
config.externalUserMngAnalytics = false;
config.externalUserMngAnalyticsParams = 'src=grafananet&other=value1';
setup({
externalUserMngLinkUrl: 'http://some/url',
externalUserMngLinkName: 'someUrl',
});
expect(screen.getByRole('link', { name: 'someUrl' })).toHaveAttribute('href', 'http://some/url');
});
it('should not show invite button when externalUserMngInfo is set and disableLoginForm is true', () => {

View File

@ -9,6 +9,7 @@ import { selectTotal } from '../invites/state/selectors';
import { changeSearchQuery } from './state/actions';
import { getUsersSearchQuery } from './state/selectors';
import { getExternalUserMngLinkUrl } from './utils';
export interface OwnProps {
showInvites: boolean;
@ -67,7 +68,7 @@ export const UsersActionBarUnconnected = ({
)}
{showInviteButton && <LinkButton href="org/users/invite">Invite</LinkButton>}
{externalUserMngLinkUrl && (
<LinkButton href={externalUserMngLinkUrl} target="_blank" rel="noopener">
<LinkButton href={getExternalUserMngLinkUrl('manage-users')} target="_blank" rel="noopener">
{externalUserMngLinkName}
</LinkButton>
)}

View File

@ -0,0 +1,21 @@
import { config } from '@grafana/runtime';
export function getExternalUserMngLinkUrl(cnt: string) {
const url = new URL(config.externalUserMngLinkUrl);
if (config.externalUserMngAnalytics) {
// Add query parameters in config.externalUserMngAnalyticsParams to track conversion
if (!!config.externalUserMngAnalyticsParams) {
const params = config.externalUserMngAnalyticsParams.split('&');
params.forEach((param) => {
const [key, value] = param.split('=');
url.searchParams.append(key, value);
});
}
// Add specific CTA cnt to track conversion
url.searchParams.append('cnt', cnt);
}
return url.toString();
}