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:
Gergo Kekesi
2026-08-17 15:17:36 +02:00
committed by GitHub
parent 1a02d8b239
commit f63abc9cc1
7 changed files with 63 additions and 43 deletions
@@ -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}
/>
}
@@ -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));
}
}
+13 -14
View File
@@ -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],
);
};
+8 -7
View File
@@ -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'