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

pull/99253/head
Ezequiel Victorero 4 months ago committed by GitHub
parent c7edbffd82
commit 865e911e10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      packages/grafana-data/src/types/config.ts
  2. 2
      packages/grafana-runtime/src/config.ts
  3. 2
      pkg/api/dtos/frontend_settings.go
  4. 2
      pkg/api/frontendsettings.go
  5. 32
      pkg/setting/setting.go
  6. 28
      public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx
  7. 5
      public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx
  8. 34
      public/app/features/users/UsersActionBar.test.tsx
  9. 3
      public/app/features/users/UsersActionBar.tsx
  10. 21
      public/app/features/users/utils.ts

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

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

@ -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"`

@ -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,

@ -397,20 +397,22 @@ type Cfg struct {
Quota QuotaSettings
// User settings
AllowUserSignUp bool
AllowUserOrgCreate bool
VerifyEmailEnabled bool
LoginHint string
PasswordHint string
DisableSignoutMenu bool
ExternalUserMngLinkUrl string
ExternalUserMngLinkName string
ExternalUserMngInfo string
AutoAssignOrg bool
AutoAssignOrgId int
AutoAssignOrgRole string
LoginDefaultOrgId int64
OAuthSkipOrgRoleUpdateSync bool
AllowUserSignUp bool
AllowUserOrgCreate bool
VerifyEmailEnabled bool
LoginHint string
PasswordHint string
DisableSignoutMenu bool
ExternalUserMngLinkUrl string
ExternalUserMngLinkName string
ExternalUserMngInfo string
ExternalUserMngAnalytics bool
ExternalUserMngAnalyticsParams string
AutoAssignOrg bool
AutoAssignOrgId int
AutoAssignOrgRole string
LoginDefaultOrgId int64
OAuthSkipOrgRoleUpdateSync bool
// ExpressionsEnabled specifies whether expressions are enabled.
ExpressionsEnabled bool
@ -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)

@ -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,

@ -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} />,

@ -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', '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', 'some/url');
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', () => {

@ -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>
)}

@ -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();
}
Loading…
Cancel
Save