diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts
index 50b9461ba19..b1f6ca4b28a 100644
--- a/packages/grafana-data/src/types/config.ts
+++ b/packages/grafana-data/src/types/config.ts
@@ -170,6 +170,8 @@ export interface GrafanaConfig {
externalUserMngLinkUrl: string;
externalUserMngLinkName: string;
externalUserMngInfo: string;
+ externalUserMngAnalytics: boolean;
+ externalUserMngAnalyticsParams: string;
allowOrgCreate: boolean;
disableLoginForm: boolean;
defaultDatasource: string;
diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts
index 0ebfab4e4fd..f8b087b1bc6 100644
--- a/packages/grafana-runtime/src/config.ts
+++ b/packages/grafana-runtime/src/config.ts
@@ -73,6 +73,8 @@ export class GrafanaBootConfig implements GrafanaConfig {
externalUserMngLinkUrl = '';
externalUserMngLinkName = '';
externalUserMngInfo = '';
+ externalUserMngAnalytics = false;
+ externalUserMngAnalyticsParams = '';
allowOrgCreate = false;
feedbackLinksEnabled = true;
disableLoginForm = false;
diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go
index e33490bd41b..d7094de9d90 100644
--- a/pkg/api/dtos/frontend_settings.go
+++ b/pkg/api/dtos/frontend_settings.go
@@ -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"`
diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go
index 8f17ad1e85e..f11114b299a 100644
--- a/pkg/api/frontendsettings.go
+++ b/pkg/api/frontendsettings.go
@@ -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,
diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go
index 7eca72b15f4..934fa3152a1 100644
--- a/pkg/setting/setting.go
+++ b/pkg/setting/setting.go
@@ -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)
diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx
index 5d865ce2b4c..403e76d2c7f 100644
--- a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx
+++ b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx
@@ -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,
diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx
index c701e2a41ec..fb97d7cd8e8 100644
--- a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx
+++ b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx
@@ -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: () => ,
diff --git a/public/app/features/users/UsersActionBar.test.tsx b/public/app/features/users/UsersActionBar.test.tsx
index 61516544a16..9e5e46f1494 100644
--- a/public/app/features/users/UsersActionBar.test.tsx
+++ b/public/app/features/users/UsersActionBar.test.tsx
@@ -25,6 +25,9 @@ const setup = (propOverrides?: object) => {
Object.assign(props, propOverrides);
+ config.externalUserMngLinkUrl = props.externalUserMngLinkUrl;
+ config.externalUserMngLinkName = props.externalUserMngLinkName;
+
const { rerender } = render();
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', () => {
diff --git a/public/app/features/users/UsersActionBar.tsx b/public/app/features/users/UsersActionBar.tsx
index ed09f86ed88..48ef3a81e83 100644
--- a/public/app/features/users/UsersActionBar.tsx
+++ b/public/app/features/users/UsersActionBar.tsx
@@ -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 && Invite}
{externalUserMngLinkUrl && (
-
+
{externalUserMngLinkName}
)}
diff --git a/public/app/features/users/utils.ts b/public/app/features/users/utils.ts
new file mode 100644
index 00000000000..def3777d476
--- /dev/null
+++ b/public/app/features/users/utils.ts
@@ -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();
+}