Skip to content

Commit

Permalink
Share: Add analytics to invite user flow (grafana#99116)
Browse files Browse the repository at this point in the history
  • Loading branch information
evictorero authored Jan 21, 2025
1 parent c7edbff commit 865e911
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 18 deletions.
2 changes: 2 additions & 0 deletions packages/grafana-data/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ export interface GrafanaConfig {
externalUserMngLinkUrl: string;
externalUserMngLinkName: string;
externalUserMngInfo: string;
externalUserMngAnalytics: boolean;
externalUserMngAnalyticsParams: string;
allowOrgCreate: boolean;
disableLoginForm: boolean;
defaultDatasource: string;
Expand Down
2 changes: 2 additions & 0 deletions packages/grafana-runtime/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ export class GrafanaBootConfig implements GrafanaConfig {
externalUserMngLinkUrl = '';
externalUserMngLinkName = '';
externalUserMngInfo = '';
externalUserMngAnalytics = false;
externalUserMngAnalyticsParams = '';
allowOrgCreate = false;
feedbackLinksEnabled = true;
disableLoginForm = false;
Expand Down
2 changes: 2 additions & 0 deletions pkg/api/dtos/frontend_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
2 changes: 2 additions & 0 deletions pkg/api/frontendsettings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
32 changes: 18 additions & 14 deletions pkg/setting/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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} />,
Expand Down
34 changes: 32 additions & 2 deletions public/app/features/users/UsersActionBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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', () => {
Expand Down
3 changes: 2 additions & 1 deletion public/app/features/users/UsersActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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>
)}
Expand Down
21 changes: 21 additions & 0 deletions public/app/features/users/utils.ts
Original file line number Diff line number Diff line change
@@ -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();
}

0 comments on commit 865e911

Please sign in to comment.