mirror of
https://github.com/Unleash/unleash.git
synced 2026-09-10 22:25:45 +00:00
feat(tracking): emit dialog dismissals as journey events
Update dialog tracking to new format so its easier to link queries.
This commit is contained in:
@@ -15,7 +15,7 @@ import {
|
||||
useDialogDismissTracking,
|
||||
} from 'hooks/useTrackDialogDismissed';
|
||||
import { DIALOGUE_CONFIRM_ID } from 'utils/testIds';
|
||||
import type { DialogTrackingId } from 'utils/trackingEvents';
|
||||
import type { DialogTracking } from 'utils/trackingEvents';
|
||||
|
||||
const StyledDialog = styled(Dialog)(({ theme, maxWidth }) => ({
|
||||
'& .MuiDialog-paper': {
|
||||
@@ -63,7 +63,7 @@ interface IDialogue {
|
||||
permissionButton?: React.JSX.Element;
|
||||
customButton?: React.JSX.Element;
|
||||
children?: React.ReactNode;
|
||||
trackingId?: DialogTrackingId;
|
||||
tracking?: DialogTracking;
|
||||
}
|
||||
|
||||
export const Dialogue: React.FC<IDialogue> = ({
|
||||
@@ -81,9 +81,9 @@ export const Dialogue: React.FC<IDialogue> = ({
|
||||
formId,
|
||||
permissionButton,
|
||||
customButton,
|
||||
trackingId,
|
||||
tracking,
|
||||
}) => {
|
||||
const emitDismissed = useDialogDismissTracking(open, trackingId);
|
||||
const emitDismissed = useDialogDismissTracking(open, tracking);
|
||||
|
||||
const handleClick = formId
|
||||
? (e: React.SyntheticEvent) => {
|
||||
|
||||
@@ -7,10 +7,7 @@ import {
|
||||
useDialogDismissTracking,
|
||||
} from 'hooks/useTrackDialogDismissed';
|
||||
import { SIDEBAR_MODAL_ID } from 'utils/testIds';
|
||||
import type {
|
||||
DialogDismissMethod,
|
||||
DialogTrackingId,
|
||||
} from 'utils/trackingEvents';
|
||||
import type { DialogDismissMethod, DialogTracking } from 'utils/trackingEvents';
|
||||
import type * as React from 'react';
|
||||
|
||||
interface ISidebarModalProps {
|
||||
@@ -19,7 +16,7 @@ interface ISidebarModalProps {
|
||||
label: string;
|
||||
onClick?: (e: React.SyntheticEvent) => void;
|
||||
children: React.ReactElement<any, any>;
|
||||
trackingId?: DialogTrackingId;
|
||||
tracking?: DialogTracking;
|
||||
}
|
||||
|
||||
interface IBaseModalProps {
|
||||
@@ -87,10 +84,7 @@ export const BaseModal: FC<IBaseModalProps> = ({
|
||||
};
|
||||
|
||||
export const SidebarModal: FC<ISidebarModalProps> = (props) => {
|
||||
const emitDismissed = useDialogDismissTracking(
|
||||
props.open,
|
||||
props.trackingId,
|
||||
);
|
||||
const emitDismissed = useDialogDismissTracking(props.open, props.tracking);
|
||||
|
||||
return (
|
||||
<BaseModal {...props} onDismiss={emitDismissed}>
|
||||
@@ -102,10 +96,7 @@ export const SidebarModal: FC<ISidebarModalProps> = (props) => {
|
||||
};
|
||||
|
||||
export const DynamicSidebarModal: FC<ISidebarModalProps> = (props) => {
|
||||
const emitDismissed = useDialogDismissTracking(
|
||||
props.open,
|
||||
props.trackingId,
|
||||
);
|
||||
const emitDismissed = useDialogDismissTracking(props.open, props.tracking);
|
||||
|
||||
return (
|
||||
<BaseModal {...props} onDismiss={emitDismissed}>
|
||||
|
||||
@@ -159,7 +159,9 @@ export const FeedbackComponent = ({
|
||||
const { trackEvent } = useEventTracker();
|
||||
const theme = useTheme();
|
||||
|
||||
const emitDismissed = useDialogDismissTracking(showFeedback, 'feedback');
|
||||
const emitDismissed = useDialogDismissTracking(showFeedback, {
|
||||
event: 'feedback',
|
||||
});
|
||||
|
||||
const { addFeedback } = useUserFeedbackApi();
|
||||
const { setHasSubmittedFeedback } = useUserSubmittedFeedback(
|
||||
@@ -202,6 +204,7 @@ export const FeedbackComponent = ({
|
||||
props: {
|
||||
eventType: `submitted - ${feedbackData.category}`,
|
||||
category: feedbackData.category,
|
||||
action: 'submitted',
|
||||
},
|
||||
});
|
||||
toastTitle = 'Feedback sent';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { styled } from '@mui/material';
|
||||
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
||||
import { useDialogDismissTracking } from 'hooks/useTrackDialogDismissed';
|
||||
import { useEventTracker } from 'hooks/useEventTracker';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { ImportTimeline } from './ImportTimeline.tsx';
|
||||
@@ -56,7 +57,18 @@ export const ImportModal = ({ open, setOpen, project }: IImportModalProps) => {
|
||||
const [importPayload, setImportPayload] = useState('');
|
||||
const [activeTab, setActiveTab] = useState<ImportMode>('file');
|
||||
|
||||
const emitDismissed = useDialogDismissTracking(open, 'import-flags');
|
||||
const emitDismissed = useDialogDismissTracking(open, {
|
||||
event: 'export_import',
|
||||
type: 'import completed',
|
||||
});
|
||||
const { trackEvent } = useEventTracker();
|
||||
|
||||
const startImport = () => {
|
||||
trackEvent('export_import', {
|
||||
props: { eventType: 'import completed', action: 'submitted' },
|
||||
});
|
||||
setImportStage('import');
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
setOpen(false);
|
||||
@@ -85,7 +97,7 @@ export const ImportModal = ({ open, setOpen, project }: IImportModalProps) => {
|
||||
open={open}
|
||||
onClose={close}
|
||||
label='Import flags'
|
||||
trackingId='import-flags'
|
||||
tracking={{ event: 'export_import', type: 'import completed' }}
|
||||
>
|
||||
<ModalContentContainer>
|
||||
<TimelineContainer>
|
||||
@@ -135,7 +147,7 @@ export const ImportModal = ({ open, setOpen, project }: IImportModalProps) => {
|
||||
environment={environment}
|
||||
payload={importPayload}
|
||||
onBack={() => setImportStage('configure')}
|
||||
onSubmit={() => setImportStage('import')}
|
||||
onSubmit={startImport}
|
||||
onClose={cancel}
|
||||
/>
|
||||
}
|
||||
|
||||
+15
-1
@@ -6,6 +6,7 @@ import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
||||
import { type ReactNode, useState, type FormEvent, useMemo } from 'react';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { useEventTracker } from 'hooks/useEventTracker';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { Dialog, IconButton, styled } from '@mui/material';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
@@ -150,7 +151,11 @@ const CreateFeatureDialogContent = ({
|
||||
onSuccess,
|
||||
}: ICreateFeatureDialogProps) => {
|
||||
const useNewDesign = useUiFlag('newModalDesign');
|
||||
const emitDismissed = useDialogDismissTracking(open, 'create-feature');
|
||||
const emitDismissed = useDialogDismissTracking(open, {
|
||||
event: 'flag-creation',
|
||||
type: 'created',
|
||||
});
|
||||
const { trackEvent } = useEventTracker();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { uiConfig, isOss } = useUiConfig();
|
||||
const navigate = useNavigate();
|
||||
@@ -221,8 +226,14 @@ const CreateFeatureDialogContent = ({
|
||||
|
||||
if (validToggleName) {
|
||||
const payload = getTogglePayload();
|
||||
trackEvent('flag-creation', {
|
||||
props: { eventType: 'created', action: 'submitted' },
|
||||
});
|
||||
try {
|
||||
await createFeatureToggle(project, payload);
|
||||
trackEvent('flag-creation', {
|
||||
props: { eventType: 'created', action: 'succeeded' },
|
||||
});
|
||||
navigate(`/projects/${project}/features/${name}`);
|
||||
setToastData({
|
||||
text: 'Flag created successfully',
|
||||
@@ -233,6 +244,9 @@ const CreateFeatureDialogContent = ({
|
||||
setStoredFlagConfig({});
|
||||
openFeatureCreatedFeedback();
|
||||
} catch (error: unknown) {
|
||||
trackEvent('flag-creation', {
|
||||
props: { eventType: 'created', action: 'failed' },
|
||||
});
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useEventTracker } from 'hooks/useEventTracker';
|
||||
import type {
|
||||
DialogDismissMethod,
|
||||
DialogTrackingId,
|
||||
} from 'utils/trackingEvents';
|
||||
import type { DialogDismissMethod, DialogTracking } from 'utils/trackingEvents';
|
||||
|
||||
export const useTrackDialogDismissed = () => {
|
||||
const { trackEvent } = useEventTracker();
|
||||
|
||||
return useCallback(
|
||||
(dialog: DialogTrackingId | undefined, method: DialogDismissMethod) => {
|
||||
if (!dialog && import.meta.env.DEV) {
|
||||
console.warn(
|
||||
'dialog-dismissed emitted without a trackingId; add one to the dialog component',
|
||||
);
|
||||
(tracking: DialogTracking | undefined, method: DialogDismissMethod) => {
|
||||
if (!tracking) {
|
||||
trackEvent('dialog-dismissed', {
|
||||
props: { action: 'dismissed', method },
|
||||
});
|
||||
return;
|
||||
}
|
||||
trackEvent('dialog-dismissed', {
|
||||
trackEvent(tracking.event, {
|
||||
props: {
|
||||
...(tracking.type ? { eventType: tracking.type } : {}),
|
||||
action: 'dismissed',
|
||||
dialog: dialog ?? 'untagged',
|
||||
method,
|
||||
},
|
||||
});
|
||||
@@ -34,7 +32,7 @@ export const dismissMethodFromCloseReason = (
|
||||
// One dismissal can fire two close paths: a manual Escape handler and MUI's onClose.
|
||||
export const useDialogDismissTracking = (
|
||||
open: boolean,
|
||||
dialog: DialogTrackingId | undefined,
|
||||
tracking: DialogTracking | undefined,
|
||||
) => {
|
||||
const trackDialogDismissed = useTrackDialogDismissed();
|
||||
const emittedRef = useRef(false);
|
||||
@@ -45,14 +43,15 @@ export const useDialogDismissTracking = (
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const { event, type } = tracking ?? {};
|
||||
return useCallback(
|
||||
(method: DialogDismissMethod) => {
|
||||
if (emittedRef.current) {
|
||||
return;
|
||||
}
|
||||
emittedRef.current = true;
|
||||
trackDialogDismissed(dialog, method);
|
||||
trackDialogDismissed(event ? { event, type } : undefined, method);
|
||||
},
|
||||
[trackDialogDismissed, dialog],
|
||||
[trackDialogDismissed, event, type],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -95,15 +95,16 @@ export type CustomEvents =
|
||||
| 'search-docs'
|
||||
| 'flag-actions'
|
||||
| 'flag-tags'
|
||||
| 'flag-creation'
|
||||
| 'dialog-dismissed';
|
||||
|
||||
export type DialogTrackingId =
|
||||
| 'toggle-prod-guard'
|
||||
| 'toggle-enable-strategies'
|
||||
| 'toggle-change-request'
|
||||
| 'create-feature'
|
||||
| 'import-flags'
|
||||
| 'feedback';
|
||||
// A dialog is identified by the journey it serves: the journey's event name plus,
|
||||
// where the surface has operation variants, its eventType. Dismissals emit as rows
|
||||
// of that journey with action 'dismissed', so funnels need no join.
|
||||
export type DialogTracking = {
|
||||
event: CustomEvents;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
export type DialogDismissMethod =
|
||||
| 'cancel-button'
|
||||
|
||||
Reference in New Issue
Block a user