feat(ui/ingest): add analytics events (#14557)

This commit is contained in:
Aseem Bansal 2025-09-01 17:41:48 +05:30 committed by GitHub
parent 22e382d1be
commit 6044d8d298
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 216 additions and 20 deletions

View File

@ -194,4 +194,17 @@ yarn type-check
# Run tests # Run tests
yarn test yarn test
# Run specific test file
yarn test path/to/file.test.tsx --run
``` ```
## Writing Tests - Best Practices & Common Pitfalls
### Test Setup Essentials
**Always use the existing test infrastructure:**
- Use `TestPageContainer` from `@utils/test-utils/TestPageContainer` - it provides all necessary providers
- Use `MockedProvider` from `@apollo/client/testing` for GraphQL components
- Use Vitest with React Testing Library

View File

@ -75,6 +75,9 @@ export enum EventType {
CreateGlossaryEntityEvent, CreateGlossaryEntityEvent,
CreateDomainEvent, CreateDomainEvent,
MoveDomainEvent, MoveDomainEvent,
IngestionTestConnectionEvent,
IngestionExecutionResultViewedEvent,
IngestionSourceConfigurationImpressionEvent,
CreateIngestionSourceEvent, CreateIngestionSourceEvent,
UpdateIngestionSourceEvent, UpdateIngestionSourceEvent,
DeleteIngestionSourceEvent, DeleteIngestionSourceEvent,
@ -610,18 +613,43 @@ export interface MoveDomainEvent extends BaseEvent {
// Managed Ingestion Events // Managed Ingestion Events
export interface IngestionTestConnectionEvent extends BaseEvent {
type: EventType.IngestionTestConnectionEvent;
sourceType: string;
sourceUrn?: string;
outcome?: string;
}
export interface IngestionExecutionResultViewedEvent extends BaseEvent {
type: EventType.IngestionExecutionResultViewedEvent;
executionUrn: string;
executionStatus: string;
viewedSection: string;
}
export interface IngestionSourceConfigurationImpressionEvent extends BaseEvent {
type: EventType.IngestionSourceConfigurationImpressionEvent;
viewedSection: 'SELECT_TEMPLATE' | 'DEFINE_RECIPE' | 'CREATE_SCHEDULE' | 'NAME_SOURCE';
sourceType?: string;
sourceUrn?: string;
}
export interface CreateIngestionSourceEvent extends BaseEvent { export interface CreateIngestionSourceEvent extends BaseEvent {
type: EventType.CreateIngestionSourceEvent; type: EventType.CreateIngestionSourceEvent;
sourceType: string; sourceType: string;
sourceUrn?: string;
interval?: string; interval?: string;
numOwners?: number; numOwners?: number;
outcome?: string;
} }
export interface UpdateIngestionSourceEvent extends BaseEvent { export interface UpdateIngestionSourceEvent extends BaseEvent {
type: EventType.UpdateIngestionSourceEvent; type: EventType.UpdateIngestionSourceEvent;
sourceType: string; sourceType: string;
sourceUrn: string;
interval?: string; interval?: string;
numOwners?: number; numOwners?: number;
outcome?: string;
} }
export interface DeleteIngestionSourceEvent extends BaseEvent { export interface DeleteIngestionSourceEvent extends BaseEvent {
@ -630,6 +658,8 @@ export interface DeleteIngestionSourceEvent extends BaseEvent {
export interface ExecuteIngestionSourceEvent extends BaseEvent { export interface ExecuteIngestionSourceEvent extends BaseEvent {
type: EventType.ExecuteIngestionSourceEvent; type: EventType.ExecuteIngestionSourceEvent;
sourceType?: string;
sourceUrn?: string;
} }
// TODO: Find a way to use this event // TODO: Find a way to use this event
@ -1216,4 +1246,7 @@ export type Event =
| WelcomeToDataHubModalInteractEvent | WelcomeToDataHubModalInteractEvent
| WelcomeToDataHubModalExitEvent | WelcomeToDataHubModalExitEvent
| WelcomeToDataHubModalClickViewDocumentationEvent | WelcomeToDataHubModalClickViewDocumentationEvent
| ProductTourButtonClickEvent; | ProductTourButtonClickEvent
| IngestionTestConnectionEvent
| IngestionExecutionResultViewedEvent
| IngestionSourceConfigurationImpressionEvent;

View File

@ -256,6 +256,8 @@ export const IngestionSourceList = ({ showCreateModal, setShowCreateModal }: Pro
.then(() => { .then(() => {
analytics.event({ analytics.event({
type: EventType.ExecuteIngestionSourceEvent, type: EventType.ExecuteIngestionSourceEvent,
sourceType: focusSource?.type,
sourceUrn: focusSource?.urn,
}); });
message.success({ message.success({
content: `Successfully submitted ingestion execution request!`, content: `Successfully submitted ingestion execution request!`,
@ -308,6 +310,7 @@ export const IngestionSourceList = ({ showCreateModal, setShowCreateModal }: Pro
analytics.event({ analytics.event({
type: EventType.UpdateIngestionSourceEvent, type: EventType.UpdateIngestionSourceEvent,
sourceType: input.type, sourceType: input.type,
sourceUrn: focusSourceUrn,
interval: input.schedule?.interval, interval: input.schedule?.interval,
}); });
message.success({ message.success({
@ -359,6 +362,7 @@ export const IngestionSourceList = ({ showCreateModal, setShowCreateModal }: Pro
analytics.event({ analytics.event({
type: EventType.CreateIngestionSourceEvent, type: EventType.CreateIngestionSourceEvent,
sourceType: input.type, sourceType: input.type,
sourceUrn: newSource.urn,
interval: input.schedule?.interval, interval: input.schedule?.interval,
}); });
message.success({ message.success({

View File

@ -1,10 +1,11 @@
import { LoadingOutlined } from '@ant-design/icons'; import { LoadingOutlined } from '@ant-design/icons';
import { Icon, Modal, Pill } from '@components'; import { Icon, Modal, Pill } from '@components';
import { message } from 'antd'; import { message } from 'antd';
import React, { useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Tab, Tabs } from '@components/components/Tabs/Tabs'; import { Tab, Tabs } from '@components/components/Tabs/Tabs';
import analytics, { EventType } from '@app/analytics';
import { LogsTab } from '@app/ingestV2/executions/components/LogsTab'; import { LogsTab } from '@app/ingestV2/executions/components/LogsTab';
import { RecipeTab } from '@app/ingestV2/executions/components/RecipeTab'; import { RecipeTab } from '@app/ingestV2/executions/components/RecipeTab';
import { SummaryTab } from '@app/ingestV2/executions/components/SummaryTab'; import { SummaryTab } from '@app/ingestV2/executions/components/SummaryTab';
@ -38,6 +39,30 @@ export const ExecutionDetailsModal = ({ urn, open, onClose }: Props) => {
const status = getIngestionSourceStatus(result); const status = getIngestionSourceStatus(result);
const [selectedTab, setSelectedTab] = useState<TabType>(TabType.Summary); const [selectedTab, setSelectedTab] = useState<TabType>(TabType.Summary);
const sendAnalyticsTabViewedEvent = useCallback(
(tab: TabType) => {
if (!result) return;
analytics.event({
type: EventType.IngestionExecutionResultViewedEvent,
executionUrn: urn,
executionStatus: status || 'UNKNOWN',
viewedSection: tab,
});
},
[result, urn, status],
);
const selectTab = (tab: TabType) => {
setSelectedTab(tab);
sendAnalyticsTabViewedEvent(tab);
};
useEffect(() => {
if (open) {
sendAnalyticsTabViewedEvent(TabType.Summary);
}
}, [open, urn, status, sendAnalyticsTabViewedEvent]);
const ResultIcon = status && getExecutionRequestStatusIcon(status); const ResultIcon = status && getExecutionRequestStatusIcon(status);
const resultColor = status ? getExecutionRequestStatusDisplayColor(status) : 'gray'; const resultColor = status ? getExecutionRequestStatusDisplayColor(status) : 'gray';
const titlePill = status && ResultIcon && ( const titlePill = status && ResultIcon && (
@ -106,7 +131,7 @@ export const ExecutionDetailsModal = ({ urn, open, onClose }: Props) => {
<Tabs <Tabs
tabs={tabs} tabs={tabs}
selectedTab={selectedTab} selectedTab={selectedTab}
onChange={(tab) => setSelectedTab(tab as TabType)} onChange={(tab) => selectTab(tab as TabType)}
getCurrentUrl={() => window.location.pathname} getCurrentUrl={() => window.location.pathname}
scrollToTopOnChange scrollToTopOnChange
maxHeight="80vh" maxHeight="80vh"

View File

@ -272,6 +272,8 @@ export const IngestionSourceList = ({
const focusSource = finalSources.find((s) => s.urn === focusSourceUrn); const focusSource = finalSources.find((s) => s.urn === focusSourceUrn);
const isLastPage = totalSources <= pageSize * page; const isLastPage = totalSources <= pageSize * page;
// this is required when the ingestion source has not been created
const [selectedSourceType, setSelectedSourceType] = useState<string | undefined>(undefined);
useEffect(() => { useEffect(() => {
const sources = (data?.listIngestionSources?.ingestionSources || []) as IngestionSource[]; const sources = (data?.listIngestionSources?.ingestionSources || []) as IngestionSource[];
@ -392,8 +394,10 @@ export const IngestionSourceList = ({
analytics.event({ analytics.event({
type: EventType.UpdateIngestionSourceEvent, type: EventType.UpdateIngestionSourceEvent,
sourceType: input.type, sourceType: input.type,
sourceUrn: focusSourceUrn,
interval: input.schedule?.interval, interval: input.schedule?.interval,
numOwners: owners?.length, numOwners: owners?.length,
outcome: shouldRun ? 'save_and_run' : 'save',
}); });
message.success({ message.success({
content: `Successfully updated ingestion source!`, content: `Successfully updated ingestion source!`,
@ -460,8 +464,10 @@ export const IngestionSourceList = ({
analytics.event({ analytics.event({
type: EventType.CreateIngestionSourceEvent, type: EventType.CreateIngestionSourceEvent,
sourceType: input.type, sourceType: input.type,
sourceUrn: newSource.urn,
interval: input.schedule?.interval, interval: input.schedule?.interval,
numOwners: ownersToAdd?.length, numOwners: ownersToAdd?.length,
outcome: shouldRun ? 'save_and_run' : 'save',
}); });
message.success({ message.success({
content: `Successfully created ingestion source!`, content: `Successfully created ingestion source!`,
@ -696,6 +702,8 @@ export const IngestionSourceList = ({
return Promise.resolve(); return Promise.resolve();
}} }}
selectedSource={focusSource} selectedSource={focusSource}
selectedSourceType={selectedSourceType}
setSelectedSourceType={setSelectedSourceType}
loading={isModalWaiting} loading={isModalWaiting}
/> />
{isViewingRecipe && <RecipeViewerModal recipe={focusSource?.config?.recipe} onCancel={onCancel} />} {isViewingRecipe && <RecipeViewerModal recipe={focusSource?.config?.recipe} onCancel={onCancel} />}

View File

@ -39,7 +39,15 @@ const ControlsContainer = styled.div`
/** /**
* The step for defining a recipe * The step for defining a recipe
*/ */
export const DefineRecipeStep = ({ state, updateState, goTo, prev, ingestionSources }: StepProps) => { export const DefineRecipeStep = ({
state,
updateState,
goTo,
prev,
ingestionSources,
selectedSource,
setSelectedSourceType,
}: StepProps) => {
const existingRecipeJson = state.config?.recipe; const existingRecipeJson = state.config?.recipe;
const existingRecipeYaml = existingRecipeJson && jsonToYaml(existingRecipeJson); const existingRecipeYaml = existingRecipeJson && jsonToYaml(existingRecipeJson);
const { type } = state; const { type } = state;
@ -96,6 +104,7 @@ export const DefineRecipeStep = ({ state, updateState, goTo, prev, ingestionSour
updateState(newState); updateState(newState);
goTo(IngestionSourceBuilderStep.CREATE_SCHEDULE); goTo(IngestionSourceBuilderStep.CREATE_SCHEDULE);
setSelectedSourceType?.(newState.type);
}; };
if (type && CONNECTORS_WITH_FORM.has(type)) { if (type && CONNECTORS_WITH_FORM.has(type)) {
@ -109,6 +118,7 @@ export const DefineRecipeStep = ({ state, updateState, goTo, prev, ingestionSour
setStagedRecipe={setStagedRecipeYml} setStagedRecipe={setStagedRecipeYml}
onClickNext={onClickNext} onClickNext={onClickNext}
goToPrevious={prev} goToPrevious={prev}
selectedSource={selectedSource}
/> />
); );
} }

View File

@ -2,9 +2,10 @@ import { LoadingOutlined } from '@ant-design/icons';
import { Modal } from '@components'; import { Modal } from '@components';
import { Spin, Steps } from 'antd'; import { Spin, Steps } from 'antd';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import React, { useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import analytics, { EventType } from '@app/analytics';
import { CreateScheduleStep } from '@app/ingestV2/source/builder/CreateScheduleStep'; import { CreateScheduleStep } from '@app/ingestV2/source/builder/CreateScheduleStep';
import { DefineRecipeStep } from '@app/ingestV2/source/builder/DefineRecipeStep'; import { DefineRecipeStep } from '@app/ingestV2/source/builder/DefineRecipeStep';
import { NameSourceStep } from '@app/ingestV2/source/builder/NameSourceStep'; import { NameSourceStep } from '@app/ingestV2/source/builder/NameSourceStep';
@ -58,6 +59,8 @@ type Props = {
sourceRefetch?: () => Promise<any>; sourceRefetch?: () => Promise<any>;
selectedSource?: IngestionSource; selectedSource?: IngestionSource;
loading?: boolean; loading?: boolean;
selectedSourceType?: string;
setSelectedSourceType?: (sourceType: string) => void;
}; };
export const IngestionSourceBuilderModal = ({ export const IngestionSourceBuilderModal = ({
@ -68,6 +71,8 @@ export const IngestionSourceBuilderModal = ({
sourceRefetch, sourceRefetch,
selectedSource, selectedSource,
loading, loading,
selectedSourceType,
setSelectedSourceType,
}: Props) => { }: Props) => {
const isEditing = initialState !== undefined; const isEditing = initialState !== undefined;
const titleText = isEditing ? 'Edit Data Source' : 'Connect Data Source'; const titleText = isEditing ? 'Edit Data Source' : 'Connect Data Source';
@ -84,20 +89,46 @@ export const IngestionSourceBuilderModal = ({
const ingestionSources = JSON.parse(JSON.stringify(sourcesJson)); // TODO: replace with call to server once we have access to dynamic list of sources const ingestionSources = JSON.parse(JSON.stringify(sourcesJson)); // TODO: replace with call to server once we have access to dynamic list of sources
// Reset the ingestion builder modal state when the modal is re-opened. const sendAnalyticsStepViewedEvent = useCallback(
const prevInitialState = useRef(initialState); (step: IngestionSourceBuilderStep) => {
useEffect(() => { if (open) {
if (!isEqual(prevInitialState.current, initialState)) { analytics.event({
setIngestionBuilderState(initialState || {}); type: EventType.IngestionSourceConfigurationImpressionEvent,
viewedSection: step,
sourceType: selectedSource?.type || selectedSourceType,
sourceUrn: selectedSource?.urn,
});
} }
prevInitialState.current = initialState; },
}, [initialState]); [selectedSource?.type, selectedSource?.urn, selectedSourceType, open],
);
// Reset the step stack to the initial step when the modal is re-opened. // Reset the modal state when initialState changes or modal opens
useEffect(() => setStepStack([initialStep]), [initialStep]); const prevInitialState = useRef(initialState);
const prevOpen = useRef(open);
useEffect(() => {
const stateChanged = !isEqual(prevInitialState.current, initialState);
const modalOpened = !prevOpen.current && open;
if (stateChanged) {
setIngestionBuilderState(initialState || {});
setStepStack([initialStep]);
setSelectedSourceType?.('');
prevInitialState.current = initialState;
}
// Fire event when modal opens
if (modalOpened) {
setStepStack([initialStep]); // Ensure correct step when modal opens
sendAnalyticsStepViewedEvent(initialStep);
}
prevOpen.current = open;
}, [initialState, initialStep, open, sendAnalyticsStepViewedEvent, setSelectedSourceType]);
const goTo = (step: IngestionSourceBuilderStep) => { const goTo = (step: IngestionSourceBuilderStep) => {
setStepStack([...stepStack, step]); setStepStack([...stepStack, step]);
sendAnalyticsStepViewedEvent(step);
}; };
const prev = () => { const prev = () => {
@ -151,6 +182,8 @@ export const IngestionSourceBuilderModal = ({
ingestionSources={ingestionSources} ingestionSources={ingestionSources}
sourceRefetch={sourceRefetch} sourceRefetch={sourceRefetch}
selectedSource={selectedSource} selectedSource={selectedSource}
selectedSourceType={selectedSourceType}
setSelectedSourceType={setSelectedSourceType}
/> />
</Spin> </Spin>
</Modal> </Modal>

View File

@ -14,6 +14,8 @@ import { CSV, LOOKER, LOOK_ML } from '@app/ingestV2/source/builder/constants';
import { SourceBuilderState, SourceConfig } from '@app/ingestV2/source/builder/types'; import { SourceBuilderState, SourceConfig } from '@app/ingestV2/source/builder/types';
import { Button } from '@src/alchemy-components'; import { Button } from '@src/alchemy-components';
import { IngestionSource } from '@types';
export const ControlsContainer = styled.div` export const ControlsContainer = styled.div`
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -61,13 +63,23 @@ interface Props {
isEditing: boolean; isEditing: boolean;
displayRecipe: string; displayRecipe: string;
sourceConfigs?: SourceConfig; sourceConfigs?: SourceConfig;
selectedSource?: IngestionSource;
setStagedRecipe: (recipe: string) => void; setStagedRecipe: (recipe: string) => void;
onClickNext: () => void; onClickNext: () => void;
goToPrevious?: () => void; goToPrevious?: () => void;
} }
function RecipeBuilder(props: Props) { function RecipeBuilder(props: Props) {
const { state, isEditing, displayRecipe, sourceConfigs, setStagedRecipe, onClickNext, goToPrevious } = props; const {
state,
isEditing,
displayRecipe,
sourceConfigs,
setStagedRecipe,
onClickNext,
goToPrevious,
selectedSource,
} = props;
const { type } = state; const { type } = state;
const [isViewingForm, setIsViewingForm] = useState(true); const [isViewingForm, setIsViewingForm] = useState(true);
const [hideDocsHint, setHideDocsHint] = useState(false); const [hideDocsHint, setHideDocsHint] = useState(false);
@ -119,6 +131,7 @@ function RecipeBuilder(props: Props) {
{isViewingForm && ( {isViewingForm && (
<RecipeForm <RecipeForm
state={state} state={state}
selectedSource={selectedSource}
isEditing={isEditing} isEditing={isEditing}
displayRecipe={displayRecipe} displayRecipe={displayRecipe}
sourceConfigs={sourceConfigs} sourceConfigs={sourceConfigs}

View File

@ -16,6 +16,7 @@ import { jsonToYaml } from '@app/ingestV2/source/utils';
import { RequiredFieldForm } from '@app/shared/form/RequiredFieldForm'; import { RequiredFieldForm } from '@app/shared/form/RequiredFieldForm';
import { useListSecretsQuery } from '@graphql/ingestion.generated'; import { useListSecretsQuery } from '@graphql/ingestion.generated';
import { IngestionSource } from '@types';
export const ControlsContainer = styled.div` export const ControlsContainer = styled.div`
display: flex; display: flex;
@ -102,10 +103,20 @@ interface Props {
setStagedRecipe: (recipe: string) => void; setStagedRecipe: (recipe: string) => void;
onClickNext: () => void; onClickNext: () => void;
goToPrevious?: () => void; goToPrevious?: () => void;
selectedSource?: IngestionSource;
} }
function RecipeForm(props: Props) { function RecipeForm(props: Props) {
const { state, isEditing, displayRecipe, sourceConfigs, setStagedRecipe, onClickNext, goToPrevious } = props; const {
state,
isEditing,
displayRecipe,
sourceConfigs,
setStagedRecipe,
onClickNext,
goToPrevious,
selectedSource,
} = props;
const { type } = state; const { type } = state;
const version = state.config?.version; const version = state.config?.version;
const { fields, advancedFields, filterFields, filterSectionTooltip, advancedSectionTooltip, defaultOpenSections } = const { fields, advancedFields, filterFields, filterSectionTooltip, advancedSectionTooltip, defaultOpenSections } =
@ -171,6 +182,7 @@ function RecipeForm(props: Props) {
recipe={displayRecipe} recipe={displayRecipe}
sourceConfigs={sourceConfigs} sourceConfigs={sourceConfigs}
version={version} version={version}
selectedSource={selectedSource}
/> />
</TestConnectionWrapper> </TestConnectionWrapper>
)} )}

View File

@ -2,6 +2,7 @@ import { CheckCircleOutlined } from '@ant-design/icons';
import { message } from 'antd'; import { message } from 'antd';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import analytics, { EventType } from '@app/analytics';
import { EXECUTION_REQUEST_STATUS_FAILURE, EXECUTION_REQUEST_STATUS_RUNNING } from '@app/ingestV2/executions/constants'; import { EXECUTION_REQUEST_STATUS_FAILURE, EXECUTION_REQUEST_STATUS_RUNNING } from '@app/ingestV2/executions/constants';
import TestConnectionModal from '@app/ingestV2/source/builder/RecipeForm/TestConnection/TestConnectionModal'; import TestConnectionModal from '@app/ingestV2/source/builder/RecipeForm/TestConnection/TestConnectionModal';
import { TestConnectionResult } from '@app/ingestV2/source/builder/RecipeForm/TestConnection/types'; import { TestConnectionResult } from '@app/ingestV2/source/builder/RecipeForm/TestConnection/types';
@ -13,6 +14,7 @@ import {
useCreateTestConnectionRequestMutation, useCreateTestConnectionRequestMutation,
useGetIngestionExecutionRequestLazyQuery, useGetIngestionExecutionRequestLazyQuery,
} from '@graphql/ingestion.generated'; } from '@graphql/ingestion.generated';
import { ExecutionRequestResult, IngestionSource } from '@types';
export function getRecipeJson(recipeYaml: string) { export function getRecipeJson(recipeYaml: string) {
// Convert the recipe into it's json representation, and catch + report exceptions while we do it. // Convert the recipe into it's json representation, and catch + report exceptions while we do it.
@ -29,18 +31,33 @@ export function getRecipeJson(recipeYaml: string) {
return recipeJson; return recipeJson;
} }
export function getSourceTypeFromRecipeJson(recipeJson: string) {
const recipe = JSON.parse(recipeJson);
return recipe.source.type;
}
export function getBasicConnectivityFromResult(result: ExecutionRequestResult) {
if (!result?.structuredReport?.serializedValue) {
return false;
}
const resultJson = JSON.parse(result.structuredReport.serializedValue);
return resultJson?.basic_connectivity?.capable;
}
interface Props { interface Props {
recipe: string; recipe: string;
sourceConfigs?: SourceConfig; sourceConfigs?: SourceConfig;
version?: string | null; version?: string | null;
selectedSource?: IngestionSource;
} }
function TestConnectionButton(props: Props) { function TestConnectionButton(props: Props) {
const { recipe, sourceConfigs, version } = props; const { recipe, sourceConfigs, version, selectedSource } = props;
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isModalVisible, setIsModalVisible] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false);
const [pollingInterval, setPollingInterval] = useState<null | NodeJS.Timeout>(null); const [pollingInterval, setPollingInterval] = useState<null | NodeJS.Timeout>(null);
const [testConnectionResult, setTestConnectionResult] = useState<null | TestConnectionResult>(null); const [testConnectionResult, setTestConnectionResult] = useState<null | TestConnectionResult>(null);
const [hasEmittedAnalytics, setHasEmittedAnalytics] = useState(false);
const [createTestConnectionRequest, { data: requestData }] = useCreateTestConnectionRequestMutation(); const [createTestConnectionRequest, { data: requestData }] = useCreateTestConnectionRequestMutation();
const [getIngestionExecutionRequest, { data: resultData, loading }] = useGetIngestionExecutionRequestLazyQuery(); const [getIngestionExecutionRequest, { data: resultData, loading }] = useGetIngestionExecutionRequestLazyQuery();
@ -63,6 +80,18 @@ function TestConnectionButton(props: Props) {
if (!loading && resultData) { if (!loading && resultData) {
const result = resultData.executionRequest?.result; const result = resultData.executionRequest?.result;
if (result && result.status !== EXECUTION_REQUEST_STATUS_RUNNING) { if (result && result.status !== EXECUTION_REQUEST_STATUS_RUNNING) {
const recipeJson = getRecipeJson(recipe);
if (recipeJson && !hasEmittedAnalytics) {
const basicConnectivity = getBasicConnectivityFromResult(result);
analytics.event({
type: EventType.IngestionTestConnectionEvent,
sourceType: getSourceTypeFromRecipeJson(recipeJson),
sourceUrn: selectedSource?.urn,
outcome: basicConnectivity ? 'completed' : 'failed',
});
setHasEmittedAnalytics(true);
}
if (result.status === EXECUTION_REQUEST_STATUS_FAILURE) { if (result.status === EXECUTION_REQUEST_STATUS_FAILURE) {
message.error( message.error(
'Something went wrong with your connection test. Please check your recipe and try again.', 'Something went wrong with your connection test. Please check your recipe and try again.',
@ -77,7 +106,7 @@ function TestConnectionButton(props: Props) {
setIsLoading(false); setIsLoading(false);
} }
} }
}, [resultData, pollingInterval, loading]); }, [resultData, pollingInterval, loading, recipe, selectedSource?.urn, hasEmittedAnalytics]);
useEffect(() => { useEffect(() => {
if (!isModalVisible && pollingInterval) { if (!isModalVisible && pollingInterval) {
@ -88,6 +117,7 @@ function TestConnectionButton(props: Props) {
function testConnection() { function testConnection() {
const recipeJson = getRecipeJson(recipe); const recipeJson = getRecipeJson(recipe);
if (recipeJson) { if (recipeJson) {
setHasEmittedAnalytics(false);
createTestConnectionRequest({ variables: { input: { recipe: recipeJson, version } } }) createTestConnectionRequest({ variables: { input: { recipe: recipeJson, version } } })
.then((res) => .then((res) =>
getIngestionExecutionRequest({ getIngestionExecutionRequest({

View File

@ -82,7 +82,14 @@ function SourceOption({ source, onClick }: SourceOptionProps) {
/** /**
* Component responsible for selecting the mechanism for constructing a new Ingestion Source * Component responsible for selecting the mechanism for constructing a new Ingestion Source
*/ */
export const SelectTemplateStep = ({ state, updateState, goTo, cancel, ingestionSources }: StepProps) => { export const SelectTemplateStep = ({
state,
updateState,
goTo,
cancel,
ingestionSources,
setSelectedSourceType,
}: StepProps) => {
const [searchFilter, setSearchFilter] = useState(''); const [searchFilter, setSearchFilter] = useState('');
const onSelectTemplate = (type: string) => { const onSelectTemplate = (type: string) => {
@ -93,6 +100,7 @@ export const SelectTemplateStep = ({ state, updateState, goTo, cancel, ingestion
}; };
updateState(newState); updateState(newState);
goTo(IngestionSourceBuilderStep.DEFINE_RECIPE); goTo(IngestionSourceBuilderStep.DEFINE_RECIPE);
setSelectedSourceType?.(type);
}; };
const filteredSources = ingestionSources.filter( const filteredSources = ingestionSources.filter(

View File

@ -38,6 +38,10 @@ export type StepProps = {
isEditing: boolean; isEditing: boolean;
sourceRefetch?: () => Promise<any>; sourceRefetch?: () => Promise<any>;
selectedSource?: IngestionSource; selectedSource?: IngestionSource;
// This is not same as selectedSource
// This is required when the ingestion source has not been created
selectedSourceType?: string;
setSelectedSourceType?: (sourceType: string) => void;
}; };
export type StringMapEntryInput = { export type StringMapEntryInput = {

View File

@ -55,6 +55,9 @@ public enum DataHubUsageEventType {
CREATE_GLOSSARY_ENTITY_EVENT("CreateGlossaryEntityEvent"), CREATE_GLOSSARY_ENTITY_EVENT("CreateGlossaryEntityEvent"),
CREATE_DOMAIN_EVENT("CreateDomainEvent"), CREATE_DOMAIN_EVENT("CreateDomainEvent"),
MOVE_DOMAIN_EVENT("MoveDomainEvent"), MOVE_DOMAIN_EVENT("MoveDomainEvent"),
INGESTION_TEST_CONNECTION_EVENT("IngestionTestConnectionEvent"),
INGESTION_EXECUTION_RESULT_VIEWED_EVENT("IngestionExecutionResultViewedEvent"),
INGESTION_SOURCE_CONFIGURATION_IMPRESSION_EVENT("IngestionSourceConfigurationImpressionEvent"),
CREATE_INGESTION_SOURCE_EVENT("CreateIngestionSourceEvent"), CREATE_INGESTION_SOURCE_EVENT("CreateIngestionSourceEvent"),
UPDATE_INGESTION_SOURCE_EVENT("UpdateIngestionSourceEvent"), UPDATE_INGESTION_SOURCE_EVENT("UpdateIngestionSourceEvent"),
DELETE_INGESTION_SOURCE_EVENT("DeleteIngestionSourceEvent"), DELETE_INGESTION_SOURCE_EVENT("DeleteIngestionSourceEvent"),