Analytics: Refactor dashboard sharing analytics (#78612)

This commit is contained in:
Ezequiel Victorero 2023-11-24 15:10:48 -03:00 committed by GitHub
parent 29853f624e
commit 0f25f18739
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 6458 additions and 6622 deletions

View File

@ -3,12 +3,11 @@ import React from 'react';
import { useAsync } from 'react-use';
import AutoSizer from 'react-virtualized-auto-sizer';
import { reportInteraction } from '@grafana/runtime';
import { SceneComponentProps, SceneObjectBase, SceneObjectRef } from '@grafana/scenes';
import { Button, ClipboardButton, CodeEditor, Field, Modal, Switch, VerticalGroup } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { DashboardExporter } from 'app/features/dashboard/components/DashExportModal';
import { trackDashboardSharingActionPerType } from 'app/features/dashboard/components/ShareModal/analytics';
import { shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils';
import { DashboardModel } from 'app/features/dashboard/state';
import { DashboardScene } from '../scene/DashboardScene';
@ -72,6 +71,7 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> {
public async onSaveAsFile() {
const dashboardJson = await this.getExportableDashboardJson();
const dashboardJsonPretty = JSON.stringify(dashboardJson, null, 2);
const { isSharingExternally } = this.state;
const blob = new Blob([dashboardJsonPretty], {
type: 'application/json;charset=utf-8',
@ -83,7 +83,7 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> {
title = dashboardJson.title;
}
saveAs(blob, `${title}-${time}.json`);
trackDashboardSharingActionPerType('save_export', shareDashboardType.export);
reportInteraction('dashboards_sharing_export_download_json_clicked', { externally: isSharingExternally });
}
}

View File

@ -2,15 +2,13 @@ import React from 'react';
import { dateTime, UrlQueryMap } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { config, locationService } from '@grafana/runtime';
import { config, locationService, reportInteraction } from '@grafana/runtime';
import { SceneComponentProps, SceneObjectBase, SceneObjectRef, VizPanel, sceneGraph } from '@grafana/scenes';
import { TimeZone } from '@grafana/schema';
import { Alert, ClipboardButton, Field, FieldSet, Icon, Input, Switch } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { createShortLink } from 'app/core/utils/shortLinks';
import { ThemePicker } from 'app/features/dashboard/components/ShareModal/ThemePicker';
import { trackDashboardSharingActionPerType } from 'app/features/dashboard/components/ShareModal/analytics';
import { shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils';
import { DashboardScene } from '../scene/DashboardScene';
import { getDashboardUrl } from '../utils/urlBuilders';
@ -121,7 +119,11 @@ export class ShareLinkTab extends SceneObjectBase<ShareLinkTabState> {
};
onCopy() {
trackDashboardSharingActionPerType('copy_link', shareDashboardType.link);
reportInteraction('dashboards_sharing_link_copy_clicked', {
currentTimeRange: this.state.useLockedTime,
theme: this.state.selectedTheme,
shortenURL: this.state.useShortUrl,
});
}
}

View File

@ -2,12 +2,10 @@ import React from 'react';
import useAsyncFn from 'react-use/lib/useAsyncFn';
import { SelectableValue } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { getBackendSrv, reportInteraction } from '@grafana/runtime';
import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectRef, VizPanel } from '@grafana/scenes';
import { Button, ClipboardButton, Field, Input, Modal, RadioButtonGroup } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { trackDashboardSharingActionPerType } from 'app/features/dashboard/components/ShareModal/analytics';
import { shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils';
import { getDashboardSnapshotSrv, SnapshotSharingOptions } from 'app/features/dashboard/services/SnapshotSrv';
import { DashboardScene } from '../scene/DashboardScene';
@ -124,7 +122,15 @@ export class ShareSnapshotTab extends SceneObjectBase<ShareSnapshotTabState> {
const results: { deleteUrl: string; url: string } = await getBackendSrv().post(SNAPSHOTS_API_ENDPOINT, cmdData);
return results;
} finally {
trackDashboardSharingActionPerType(external ? 'publish_snapshot' : 'local_snapshot', shareDashboardType.snapshot);
if (external) {
reportInteraction('dashboards_sharing_snapshot_publish_clicked', {
expires: cmdData.expires,
});
} else {
reportInteraction('dashboards_sharing_snapshot_local_clicked', {
expires: cmdData.expires,
});
}
}
};
}

View File

@ -76,7 +76,17 @@ export function ShareEmbed({ panel, dashboard, range, buildIframe = buildIframeH
/>
</Field>
<Modal.ButtonRow>
<ClipboardButton icon="copy" variant="primary" getText={() => iframeHtml}>
<ClipboardButton
icon="copy"
variant="primary"
getText={() => iframeHtml}
onClipboardCopy={() => {
reportInteraction('dashboards_sharing_embed_copy_clicked', {
currentTimeRange: useCurrentTimeRange,
theme: selectedTheme,
});
}}
>
<Trans i18nKey="share-modal.embed.copy">Copy to clipboard</Trans>
</ClipboardButton>
</Modal.ButtonRow>

View File

@ -1,6 +1,7 @@
import { saveAs } from 'file-saver';
import React, { PureComponent } from 'react';
import { reportInteraction } from '@grafana/runtime';
import { Button, Field, Modal, Switch } from '@grafana/ui';
import { appEvents } from 'app/core/core';
import { t, Trans } from 'app/core/internationalization';
@ -8,9 +9,7 @@ import { DashboardExporter } from 'app/features/dashboard/components/DashExportM
import { ShowModalReactEvent } from 'app/types/events';
import { ViewJsonModal } from './ViewJsonModal';
import { trackDashboardSharingActionPerType } from './analytics';
import { ShareModalTabProps } from './types';
import { shareDashboardType } from './utils';
interface Props extends ShareModalTabProps {}
@ -40,6 +39,8 @@ export class ShareExport extends PureComponent<Props, State> {
const { dashboard } = this.props;
const { shareExternally } = this.state;
reportInteraction('dashboards_sharing_export_save_json_clicked', { externally: shareExternally });
if (shareExternally) {
this.exporter.makeExportable(dashboard).then((dashboardJson) => {
this.openSaveAsDialog(dashboardJson);
@ -53,6 +54,8 @@ export class ShareExport extends PureComponent<Props, State> {
const { dashboard } = this.props;
const { shareExternally } = this.state;
reportInteraction('dashboards_sharing_export_view_json_clicked', { externally: shareExternally });
if (shareExternally) {
this.exporter.makeExportable(dashboard).then((dashboardJson) => {
this.openJsonModal(dashboardJson);
@ -69,7 +72,6 @@ export class ShareExport extends PureComponent<Props, State> {
});
const time = new Date().getTime();
saveAs(blob, `${dash.title}-${time}.json`);
trackDashboardSharingActionPerType('save_export', shareDashboardType.export);
};
openJsonModal = (clone: object) => {

View File

@ -1,14 +1,14 @@
import React, { PureComponent } from 'react';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { reportInteraction } from '@grafana/runtime';
import { Alert, ClipboardButton, Field, FieldSet, Input, Switch, TextLink } from '@grafana/ui';
import config from 'app/core/config';
import { t, Trans } from 'app/core/internationalization';
import { ThemePicker } from './ThemePicker';
import { trackDashboardSharingActionPerType } from './analytics';
import { ShareModalTabProps } from './types';
import { buildImageUrl, buildShareUrl, shareDashboardType } from './utils';
import { buildImageUrl, buildShareUrl } from './utils';
export interface Props extends ShareModalTabProps {}
@ -75,7 +75,7 @@ export class ShareLink extends PureComponent<Props, State> {
};
onCopy() {
trackDashboardSharingActionPerType('copy_link', shareDashboardType.link, {
reportInteraction('dashboards_sharing_link_copy_clicked', {
currentTimeRange: this.state.useCurrentTimeRange,
theme: this.state.selectedTheme,
shortenURL: this.state.useShortUrl,

View File

@ -4,6 +4,7 @@ import { useForm } from 'react-hook-form';
import { GrafanaTheme2, TimeRange } from '@grafana/data/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { reportInteraction } from '@grafana/runtime';
import { config, featureEnabled } from '@grafana/runtime/src';
import {
Button,
@ -30,7 +31,6 @@ import { contextSrv } from '../../../../../../core/services/context_srv';
import { AccessControlAction, useSelector } from '../../../../../../types';
import { useIsDesktop } from '../../../../utils/screen';
import { ShareModal } from '../../ShareModal';
import { trackDashboardSharingActionPerType } from '../../analytics';
import { shareDashboardType } from '../../utils';
import { NoUpsertPermissionsAlert } from '../ModalAlerts/NoUpsertPermissionsAlert';
import { SaveDashboardChangesAlert } from '../ModalAlerts/SaveDashboardChangesAlert';
@ -111,7 +111,7 @@ export function ConfigPublicDashboardBase({
};
function onCopyURL() {
trackDashboardSharingActionPerType('copy_public_url', shareDashboardType.publicDashboard);
reportInteraction('dashboards_sharing_public_copy_url_clicked');
}
return (
@ -151,10 +151,9 @@ export function ConfigPublicDashboardBase({
{...register('isPaused')}
disabled={disableInputs}
onChange={(e) => {
trackDashboardSharingActionPerType(
e.currentTarget.checked ? 'disable_sharing' : 'enable_sharing',
shareDashboardType.publicDashboard
);
reportInteraction('dashboards_sharing_public_pause_clicked', {
paused: e.currentTarget.checked,
});
onChange('isPaused', e.currentTarget.checked);
}}
data-testid={selectors.PauseSwitch}
@ -243,6 +242,7 @@ export function ConfigPublicDashboard({ publicDashboard, unsupportedDatasources
showSaveChangesAlert={hasWritePermissions && dashboard.hasUnsavedChanges()}
hasTemplateVariables={hasTemplateVariables}
onRevoke={() => {
reportInteraction('dashboards_sharing_public_revoke_clicked');
showModal(DeletePublicDashboardModal, {
dashboardTitle: dashboard.title,
onConfirm: () => onDeletePublicDashboardClick(hideModal),

View File

@ -3,12 +3,10 @@ import { UseFormRegister } from 'react-hook-form';
import { TimeRange } from '@grafana/data/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { reportInteraction } from '@grafana/runtime';
import { FieldSet, Label, Switch, TimeRangeInput, VerticalGroup } from '@grafana/ui/src';
import { Layout } from '@grafana/ui/src/components/Layout/Layout';
import { trackDashboardSharingActionPerType } from '../../analytics';
import { shareDashboardType } from '../../utils';
import { ConfigPublicDashboardForm } from './ConfigPublicDashboard';
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
@ -39,11 +37,10 @@ export const Configuration = ({
{...register('isTimeSelectionEnabled')}
data-testid={selectors.EnableTimeRangeSwitch}
onChange={(e) => {
trackDashboardSharingActionPerType(
e.currentTarget.checked ? 'enable_time' : 'disable_time',
shareDashboardType.publicDashboard
);
onChange('isTimeSelectionEnabled', e.currentTarget.checked);
reportInteraction('dashboards_sharing_public_time_picker_clicked', {
enabled: e.currentTarget.checked,
});
}}
/>
<Label description="Allow viewers to change time range">Time range picker enabled</Label>
@ -52,11 +49,10 @@ export const Configuration = ({
<Switch
{...register('isAnnotationsEnabled')}
onChange={(e) => {
trackDashboardSharingActionPerType(
e.currentTarget.checked ? 'enable_annotations' : 'disable_annotations',
shareDashboardType.publicDashboard
);
onChange('isAnnotationsEnabled', e.currentTarget.checked);
reportInteraction('dashboards_sharing_public_annotations_clicked', {
enabled: e.currentTarget.checked,
});
}}
data-testid={selectors.EnableAnnotationsSwitch}
/>

View File

@ -5,6 +5,7 @@ import { useWindowSize } from 'react-use';
import { GrafanaTheme2, SelectableValue } from '@grafana/data/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { reportInteraction } from '@grafana/runtime';
import { FieldSet } from '@grafana/ui';
import {
Button,
@ -26,8 +27,6 @@ import {
} from 'app/features/dashboard/api/publicDashboardApi';
import { AccessControlAction, useSelector } from 'app/types';
import { trackDashboardSharingActionPerType } from '../../analytics';
import { shareDashboardType } from '../../utils';
import { PublicDashboard, PublicDashboardShareType, validEmailRegex } from '../SharePublicDashboardUtils';
interface EmailSharingConfigurationForm {
@ -58,12 +57,12 @@ const EmailList = ({
const isLoading = isDeleteLoading || isReshareLoading;
const onDeleteEmail = (recipientUid: string) => {
trackDashboardSharingActionPerType('delete_email', shareDashboardType.publicDashboard);
reportInteraction('dashboards_sharing_public_email_revoke_clicked');
deleteEmail({ recipientUid, dashboardUid: dashboardUid, uid: publicDashboardUid });
};
const onReshare = (recipientUid: string) => {
trackDashboardSharingActionPerType('reshare_email', shareDashboardType.publicDashboard);
reportInteraction('dashboards_sharing_public_email_resend_clicked');
reshareAccess({ recipientUid, uid: publicDashboardUid });
};
@ -151,8 +150,7 @@ export const EmailSharingConfiguration = () => {
};
const onSubmit = async (data: EmailSharingConfigurationForm) => {
//TODO: add if it's domain or not when developed.
trackDashboardSharingActionPerType('invite_email', shareDashboardType.publicDashboard);
reportInteraction('dashboards_sharing_public_email_invite_clicked');
await addEmail({ recipient: data.email, uid: publicDashboard!.uid, dashboardUid: dashboard.uid }).unwrap();
reset({ email: '', shareType: PublicDashboardShareType.EMAIL });
};
@ -172,10 +170,9 @@ export const EmailSharingConfiguration = () => {
size={width < 480 ? 'sm' : 'md'}
options={options}
onChange={(shareType: PublicDashboardShareType) => {
trackDashboardSharingActionPerType(
`share_type_${shareType === PublicDashboardShareType.EMAIL ? 'email' : 'public'}`,
shareDashboardType.publicDashboard
);
reportInteraction('dashboards_sharing_public_can_view_clicked', {
shareType: shareType === PublicDashboardShareType.EMAIL ? 'email' : 'public',
});
setValue('shareType', shareType);
onUpdateShareType(shareType);
}}

View File

@ -4,6 +4,7 @@ import { FormState, UseFormRegister } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { reportInteraction } from '@grafana/runtime';
import { Button, Form, Spinner, useStyles2 } from '@grafana/ui/src';
import { useCreatePublicDashboardMutation } from 'app/features/dashboard/api/publicDashboardApi';
import { DashboardModel } from 'app/features/dashboard/state';
@ -11,8 +12,6 @@ import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScen
import { contextSrv } from '../../../../../../core/services/context_srv';
import { AccessControlAction, useSelector } from '../../../../../../types';
import { trackDashboardSharingActionPerType } from '../../analytics';
import { shareDashboardType } from '../../utils';
import { NoUpsertPermissionsAlert } from '../ModalAlerts/NoUpsertPermissionsAlert';
import { UnsupportedDataSourcesAlert } from '../ModalAlerts/UnsupportedDataSourcesAlert';
import { UnsupportedTemplateVariablesAlert } from '../ModalAlerts/UnsupportedTemplateVariablesAlert';
@ -47,7 +46,7 @@ export const CreatePublicDashboardBase = ({
const [createPublicDashboard, { isLoading, isError }] = useCreatePublicDashboardMutation();
const onCreate = () => {
createPublicDashboard({ dashboard, payload: { isEnabled: true } });
trackDashboardSharingActionPerType('generate_public_url', shareDashboardType.publicDashboard);
reportInteraction('dashboards_sharing_public_generate_url_clicked', {});
};
const disableInputs = !hasWritePermissions || isLoading || isError || hasError;

View File

@ -6,7 +6,7 @@ import { setupServer } from 'msw/node';
import 'whatwg-fetch';
import { BootData, DataQuery } from '@grafana/data/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { setEchoSrv } from '@grafana/runtime';
import { reportInteraction, setEchoSrv } from '@grafana/runtime';
import { Panel } from '@grafana/schema';
import config from 'app/core/config';
import { backendSrv } from 'app/core/services/backend_srv';
@ -14,7 +14,7 @@ import { contextSrv } from 'app/core/services/context_srv';
import { Echo } from 'app/core/services/echo/Echo';
import { createDashboardModelFixture } from 'app/features/dashboard/state/__fixtures__/dashboardFixtures';
import { trackDashboardSharingTypeOpen, trackDashboardSharingActionPerType } from '../analytics';
import { trackDashboardSharingTypeOpen } from '../analytics';
import { shareDashboardType } from '../utils';
import * as sharePublicDashboardUtils from './SharePublicDashboardUtils';
@ -30,12 +30,12 @@ const server = setupServer();
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => backendSrv,
reportInteraction: jest.fn(),
}));
jest.mock('../analytics', () => ({
...jest.requireActual('../analytics'),
trackDashboardSharingTypeOpen: jest.fn(),
trackDashboardSharingActionPerType: jest.fn(),
}));
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
@ -360,12 +360,10 @@ describe('SharePublic - Report interactions', () => {
await userEvent.click(screen.getByTestId(selectors.EnableTimeRangeSwitch));
await waitFor(() => {
expect(trackDashboardSharingActionPerType).toHaveBeenCalledTimes(1);
// if time range was enabled, then the item is now disable_time
expect(trackDashboardSharingActionPerType).toHaveBeenLastCalledWith(
pubdashResponse.timeSelectionEnabled ? 'disable_time' : 'enable_time',
shareDashboardType.publicDashboard
);
expect(reportInteraction).toHaveBeenCalledTimes(1);
expect(reportInteraction).toHaveBeenLastCalledWith('dashboards_sharing_public_time_picker_clicked', {
enabled: !pubdashResponse.timeSelectionEnabled,
});
});
});
@ -379,12 +377,10 @@ describe('SharePublic - Report interactions', () => {
await userEvent.click(screen.getByTestId(selectors.EnableAnnotationsSwitch));
await waitFor(() => {
expect(trackDashboardSharingActionPerType).toHaveBeenCalledTimes(1);
// if annotations was enabled, then the item is now disable_annotations
expect(trackDashboardSharingActionPerType).toHaveBeenCalledWith(
pubdashResponse.annotationsEnabled ? 'disable_annotations' : 'enable_annotations',
shareDashboardType.publicDashboard
);
expect(reportInteraction).toHaveBeenCalledTimes(1);
expect(reportInteraction).toHaveBeenLastCalledWith('dashboards_sharing_public_annotations_clicked', {
enabled: !pubdashResponse.annotationsEnabled,
});
});
});
it('reports interaction when pause is clicked', async () => {
@ -395,12 +391,10 @@ describe('SharePublic - Report interactions', () => {
await userEvent.click(screen.getByTestId(selectors.PauseSwitch));
await waitFor(() => {
expect(trackDashboardSharingActionPerType).toHaveBeenCalledTimes(1);
// if sharing was enabled, then the item is now disable_sharing
expect(trackDashboardSharingActionPerType).toHaveBeenLastCalledWith(
pubdashResponse.isEnabled ? 'disable_sharing' : 'enable_sharing',
shareDashboardType.publicDashboard
);
expect(reportInteraction).toHaveBeenCalledTimes(1);
expect(reportInteraction).toHaveBeenLastCalledWith('dashboards_sharing_public_pause_clicked', {
paused: pubdashResponse.isEnabled,
});
});
});
});

View File

@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import { isEmptyObject, SelectableValue } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { getBackendSrv, reportInteraction } from '@grafana/runtime';
import { Button, ClipboardButton, Field, Input, LinkButton, Modal, Select, Spinner } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
@ -10,9 +10,7 @@ import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { VariableRefresh } from '../../../variables/types';
import { getDashboardSnapshotSrv } from '../../services/SnapshotSrv';
import { trackDashboardSharingActionPerType } from './analytics';
import { ShareModalTabProps } from './types';
import { shareDashboardType } from './utils';
const snapshotApiUrl = '/api/snapshots';
@ -97,7 +95,7 @@ export class ShareSnapshot extends PureComponent<Props, State> {
};
saveSnapshot = async (dashboard: DashboardModel, external?: boolean) => {
const { snapshotExpires } = this.state;
const { snapshotExpires, timeoutSeconds } = this.state;
const dash = this.dashboard.getSaveModelCloneOld();
this.scrubDashboard(dash);
@ -117,7 +115,17 @@ export class ShareSnapshot extends PureComponent<Props, State> {
step: 2,
});
} finally {
trackDashboardSharingActionPerType(external ? 'publish_snapshot' : 'local_snapshot', shareDashboardType.snapshot);
if (external) {
reportInteraction('dashboards_sharing_snapshot_publish_clicked', {
expires: snapshotExpires,
timeout: timeoutSeconds,
});
} else {
reportInteraction('dashboards_sharing_snapshot_local_clicked', {
expires: snapshotExpires,
timeout: timeoutSeconds,
});
}
this.setState({ isLoading: false });
}
};

View File

@ -1,6 +1,7 @@
import React, { useCallback } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { reportInteraction } from '@grafana/runtime';
import { ClipboardButton, CodeEditor, Modal } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
@ -17,7 +18,13 @@ export function ViewJsonModal({ json, onDismiss }: ViewJsonModalProps): JSX.Elem
{({ width }) => <CodeEditor value={json} language="json" showMiniMap={false} height="500px" width={width} />}
</AutoSizer>
<Modal.ButtonRow>
<ClipboardButton icon="copy" getText={getClipboardText}>
<ClipboardButton
icon="copy"
getText={getClipboardText}
onClipboardCopy={() => {
reportInteraction('dashboards_sharing_export_copy_json_clicked');
}}
>
<Trans i18nKey="share-modal.view-json.copy-button">Copy to Clipboard</Trans>
</ClipboardButton>
</Modal.ButtonRow>

View File

@ -4,21 +4,8 @@ export const shareAnalyticsEventNames: {
[key: string]: string;
} = {
sharingCategoryClicked: 'dashboards_sharing_category_clicked',
sharingActionClicked: 'dashboards_sharing_actions_clicked',
};
export function trackDashboardSharingTypeOpen(sharingType: string) {
reportInteraction(shareAnalyticsEventNames.sharingCategoryClicked, { item: sharingType });
}
export function trackDashboardSharingActionPerType(
action: string,
sharingType: string,
options?: Record<string, unknown>
) {
reportInteraction(shareAnalyticsEventNames.sharingActionClicked, {
item: action,
sharing_category: sharingType,
...options,
});
}