feat: added onboarding setup for Producer for Messaging queues (#6236)

* fix: added onboarding setup for producer/consumer for Messaging queues

* fix: polled onboarding status api

* feat: added onboarding status api with useQueury functions and updated endpoints

* feat: added onboarding status api util for attribute data

* feat: refactoring and url query changes

* feat: changed start and end time to nanosecond for api payload

* feat: added comment description
This commit is contained in:
SagarRajput-7 2024-11-05 19:40:23 +05:30 committed by GitHub
parent 12377be809
commit 975307a8b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 272 additions and 33 deletions

View File

@ -0,0 +1,37 @@
import { ApiBaseInstance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { ErrorResponse, SuccessResponse } from 'types/api';
export interface OnboardingStatusResponse {
status: string;
data: {
attribute?: string;
error_message?: string;
status?: string;
}[];
}
const getOnboardingStatus = async (props: {
start: number;
end: number;
}): Promise<SuccessResponse<OnboardingStatusResponse> | ErrorResponse> => {
try {
const response = await ApiBaseInstance.post(
'/messaging-queues/kafka/onboarding/consumers',
props,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG);
}
};
export default getOnboardingStatus;

View File

@ -38,4 +38,6 @@ export enum QueryParams {
selectedTimelineQuery = 'selectedTimelineQuery', selectedTimelineQuery = 'selectedTimelineQuery',
ruleType = 'ruleType', ruleType = 'ruleType',
configDetail = 'configDetail', configDetail = 'configDetail',
getStartedSource = 'getStartedSource',
getStartedSourceService = 'getStartedSourceService',
} }

View File

@ -0,0 +1,29 @@
&nbsp;
Once you are done intrumenting your Java application, you can run it using the below commands
**Note:**
- Ensure you have Java and Maven installed. Compile your Java producer applications: Ensure your producer and consumer apps are compiled and ready to run.
**Run Producer App with Java Agent:**
```bash
java -javaagent:/path/to/opentelemetry-javaagent.jar \
-Dotel.service.name=producer-svc \
-Dotel.traces.exporter=otlp \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-jar /path/to/your/producer.jar
```
<path> - update it to the path where you downloaded the Java JAR agent in previous step
<my-app> - Jar file of your application
&nbsp;
**Note:**
- In case you're dockerising your application, make sure to dockerise it along with OpenTelemetry instrumentation done in previous step.
&nbsp;
If you encounter any difficulties, please consult the [troubleshooting section](https://signoz.io/docs/instrumentation/springboot/#troubleshooting-your-installation) for assistance.

View File

@ -6,11 +6,15 @@ import {
LoadingOutlined, LoadingOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import Header from 'container/OnboardingContainer/common/Header/Header'; import Header from 'container/OnboardingContainer/common/Header/Header';
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext'; import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
import { useOnboardingStatus } from 'hooks/messagingQueue / onboarding/useOnboardingStatus';
import { useQueryService } from 'hooks/useQueryService'; import { useQueryService } from 'hooks/useQueryService';
import useResourceAttribute from 'hooks/useResourceAttribute'; import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils'; import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import useUrlQuery from 'hooks/useUrlQuery';
import { getAttributeDataFromOnboardingStatus } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
@ -27,6 +31,9 @@ export default function ConnectionStatus(): JSX.Element {
GlobalReducer GlobalReducer
>((state) => state.globalTime); >((state) => state.globalTime);
const urlQuery = useUrlQuery();
const getStartedSource = urlQuery.get(QueryParams.getStartedSource);
const { const {
serviceName, serviceName,
selectedDataSource, selectedDataSource,
@ -57,8 +64,65 @@ export default function ConnectionStatus(): JSX.Element {
maxTime, maxTime,
selectedTime, selectedTime,
selectedTags, selectedTags,
options: {
enabled: getStartedSource !== 'kafka',
},
}); });
const [pollInterval, setPollInterval] = useState<number | false>(10000);
const {
data: onbData,
error: onbErr,
isFetching: onbFetching,
} = useOnboardingStatus({
enabled: getStartedSource === 'kafka',
refetchInterval: pollInterval,
});
const [
shouldRetryOnboardingCall,
setShouldRetryOnboardingCall,
] = useState<boolean>(false);
useEffect(() => {
// runs only when the caller is coming from 'kafka' i.e. coming from Messaging Queues - setup helper
if (getStartedSource === 'kafka') {
if (onbData?.statusCode !== 200) {
setShouldRetryOnboardingCall(true);
} else if (onbData?.payload?.status === 'success') {
const attributeData = getAttributeDataFromOnboardingStatus(
onbData?.payload,
);
if (attributeData.overallStatus === 'success') {
setLoading(false);
setIsReceivingData(true);
setPollInterval(false);
} else {
setShouldRetryOnboardingCall(true);
}
}
}
}, [
shouldRetryOnboardingCall,
onbData,
onbErr,
onbFetching,
getStartedSource,
]);
useEffect(() => {
if (retryCount < 0 && getStartedSource === 'kafka') {
setPollInterval(false);
setLoading(false);
}
}, [retryCount, getStartedSource]);
useEffect(() => {
if (getStartedSource === 'kafka' && !onbFetching) {
setRetryCount((prevCount) => prevCount - 1);
}
}, [getStartedSource, onbData, onbFetching]);
const renderDocsReference = (): JSX.Element => { const renderDocsReference = (): JSX.Element => {
switch (selectedDataSource?.name) { switch (selectedDataSource?.name) {
case 'java': case 'java':
@ -192,6 +256,7 @@ export default function ConnectionStatus(): JSX.Element {
useEffect(() => { useEffect(() => {
let pollingTimer: string | number | NodeJS.Timer | undefined; let pollingTimer: string | number | NodeJS.Timer | undefined;
if (getStartedSource !== 'kafka') {
if (loading) { if (loading) {
pollingTimer = setInterval(() => { pollingTimer = setInterval(() => {
// Trigger a refetch with the updated parameters // Trigger a refetch with the updated parameters
@ -212,6 +277,7 @@ export default function ConnectionStatus(): JSX.Element {
} else if (!loading && pollingTimer) { } else if (!loading && pollingTimer) {
clearInterval(pollingTimer); clearInterval(pollingTimer);
} }
}
// Clean up the interval when the component unmounts // Clean up the interval when the component unmounts
return (): void => { return (): void => {
@ -221,15 +287,24 @@ export default function ConnectionStatus(): JSX.Element {
}, [refetch, selectedTags, selectedTime, loading]); }, [refetch, selectedTags, selectedTime, loading]);
useEffect(() => { useEffect(() => {
if (getStartedSource !== 'kafka') {
verifyApplicationData(data); verifyApplicationData(data);
}
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isServiceLoading, data, error, isError]); }, [isServiceLoading, data, error, isError]);
useEffect(() => { useEffect(() => {
if (getStartedSource !== 'kafka') {
refetch(); refetch();
}
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const isQueryServiceLoading = useMemo(
() => isServiceLoading || loading || onbFetching,
[isServiceLoading, loading, onbFetching],
);
return ( return (
<div className="connection-status-container"> <div className="connection-status-container">
<div className="full-docs-link">{renderDocsReference()}</div> <div className="full-docs-link">{renderDocsReference()}</div>
@ -250,14 +325,14 @@ export default function ConnectionStatus(): JSX.Element {
<div className="label"> Status </div> <div className="label"> Status </div>
<div className="status"> <div className="status">
{(loading || isServiceLoading) && <LoadingOutlined />} {isQueryServiceLoading && <LoadingOutlined />}
{!(loading || isServiceLoading) && isReceivingData && ( {!isQueryServiceLoading && isReceivingData && (
<> <>
<CheckCircleTwoTone twoToneColor="#52c41a" /> <CheckCircleTwoTone twoToneColor="#52c41a" />
<span> Success </span> <span> Success </span>
</> </>
)} )}
{!(loading || isServiceLoading) && !isReceivingData && ( {!isQueryServiceLoading && !isReceivingData && (
<> <>
<CloseCircleTwoTone twoToneColor="#e84749" /> <CloseCircleTwoTone twoToneColor="#e84749" />
<span> Failed </span> <span> Failed </span>
@ -269,11 +344,11 @@ export default function ConnectionStatus(): JSX.Element {
<div className="label"> Details </div> <div className="label"> Details </div>
<div className="details"> <div className="details">
{(loading || isServiceLoading) && <div> Waiting for Update </div>} {isQueryServiceLoading && <div> Waiting for Update </div>}
{!(loading || isServiceLoading) && isReceivingData && ( {!isQueryServiceLoading && isReceivingData && (
<div> Received data from the application successfully. </div> <div> Received data from the application successfully. </div>
)} )}
{!(loading || isServiceLoading) && !isReceivingData && ( {!isQueryServiceLoading && !isReceivingData && (
<div> Could not detect the install </div> <div> Could not detect the install </div>
)} )}
</div> </div>

View File

@ -75,3 +75,10 @@ div[class*='-setup-instructions-container'] {
color: var(--bg-slate-500); color: var(--bg-slate-500);
} }
} }
.supported-languages-container {
.disabled {
cursor: not-allowed;
opacity: 0.5;
}
}

View File

@ -6,6 +6,7 @@ import { LoadingOutlined } from '@ant-design/icons';
import { Button, Card, Form, Input, Select, Space, Typography } from 'antd'; import { Button, Card, Form, Input, Select, Space, Typography } from 'antd';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import cx from 'classnames'; import cx from 'classnames';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext'; import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
import { useCases } from 'container/OnboardingContainer/OnboardingContainer'; import { useCases } from 'container/OnboardingContainer/OnboardingContainer';
@ -13,8 +14,10 @@ import {
getDataSources, getDataSources,
getSupportedFrameworks, getSupportedFrameworks,
hasFrameworks, hasFrameworks,
messagingQueueKakfaSupportedDataSources,
} from 'container/OnboardingContainer/utils/dataSourceUtils'; } from 'container/OnboardingContainer/utils/dataSourceUtils';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import { Blocks, Check } from 'lucide-react'; import { Blocks, Check } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -33,6 +36,8 @@ export default function DataSource(): JSX.Element {
const { t } = useTranslation(['common']); const { t } = useTranslation(['common']);
const history = useHistory(); const history = useHistory();
const getStartedSource = useUrlQuery().get(QueryParams.getStartedSource);
const { const {
serviceName, serviceName,
selectedModule, selectedModule,
@ -150,13 +155,19 @@ export default function DataSource(): JSX.Element {
className={cx( className={cx(
'supported-language', 'supported-language',
selectedDataSource?.name === dataSource.name ? 'selected' : '', selectedDataSource?.name === dataSource.name ? 'selected' : '',
getStartedSource === 'kafka' &&
!messagingQueueKakfaSupportedDataSources.includes(dataSource?.id || '')
? 'disabled'
: '',
)} )}
key={dataSource.name} key={dataSource.name}
onClick={(): void => { onClick={(): void => {
if (getStartedSource !== 'kafka') {
updateSelectedFramework(null); updateSelectedFramework(null);
updateSelectedEnvironment(null); updateSelectedEnvironment(null);
updateSelectedDataSource(dataSource); updateSelectedDataSource(dataSource);
form.setFieldsValue({ selectFramework: null }); form.setFieldsValue({ selectFramework: null });
}
}} }}
> >
<div> <div>

View File

@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/ban-ts-comment */
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer'; import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
import { QueryParams } from 'constants/query';
import { ApmDocFilePaths } from 'container/OnboardingContainer/constants/apmDocFilePaths'; import { ApmDocFilePaths } from 'container/OnboardingContainer/constants/apmDocFilePaths';
import { AwsMonitoringDocFilePaths } from 'container/OnboardingContainer/constants/awsMonitoringDocFilePaths'; import { AwsMonitoringDocFilePaths } from 'container/OnboardingContainer/constants/awsMonitoringDocFilePaths';
import { AzureMonitoringDocFilePaths } from 'container/OnboardingContainer/constants/azureMonitoringDocFilePaths'; import { AzureMonitoringDocFilePaths } from 'container/OnboardingContainer/constants/azureMonitoringDocFilePaths';
@ -10,6 +11,7 @@ import {
useOnboardingContext, useOnboardingContext,
} from 'container/OnboardingContainer/context/OnboardingContext'; } from 'container/OnboardingContainer/context/OnboardingContext';
import { ModulesMap } from 'container/OnboardingContainer/OnboardingContainer'; import { ModulesMap } from 'container/OnboardingContainer/OnboardingContainer';
import useUrlQuery from 'hooks/useUrlQuery';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
export interface IngestionInfoProps { export interface IngestionInfoProps {
@ -31,6 +33,12 @@ export default function MarkdownStep(): JSX.Element {
const [markdownContent, setMarkdownContent] = useState(''); const [markdownContent, setMarkdownContent] = useState('');
const urlQuery = useUrlQuery();
const getStartedSource = urlQuery.get(QueryParams.getStartedSource);
const getStartedSourceService = urlQuery.get(
QueryParams.getStartedSourceService,
);
const { step } = activeStep; const { step } = activeStep;
const getFilePath = (): any => { const getFilePath = (): any => {
@ -54,6 +62,12 @@ export default function MarkdownStep(): JSX.Element {
path += `_${step?.id}`; path += `_${step?.id}`;
if (
getStartedSource === 'kafka' &&
path === 'APM_java_springBoot_kubernetes_recommendedSteps_runApplication' // todo: Sagar - Make this generic logic in followup PRs
) {
path += `_${getStartedSourceService}`;
}
return path; return path;
}; };

View File

@ -252,6 +252,7 @@ import APM_java_springBoot_docker_recommendedSteps_runApplication from '../Modul
import APM_java_springBoot_kubernetes_recommendedSteps_setupOtelCollector from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-installOtelCollector.md'; import APM_java_springBoot_kubernetes_recommendedSteps_setupOtelCollector from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-installOtelCollector.md';
import APM_java_springBoot_kubernetes_recommendedSteps_instrumentApplication from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-instrumentApplication.md'; import APM_java_springBoot_kubernetes_recommendedSteps_instrumentApplication from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-instrumentApplication.md';
import APM_java_springBoot_kubernetes_recommendedSteps_runApplication from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-runApplication.md'; import APM_java_springBoot_kubernetes_recommendedSteps_runApplication from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-runApplication.md';
import APM_java_springBoot_kubernetes_recommendedSteps_runApplication_producer from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-runApplication-producer.md';
// SpringBoot-LinuxAMD64-quickstart // SpringBoot-LinuxAMD64-quickstart
import APM_java_springBoot_linuxAMD64_quickStart_instrumentApplication from '../Modules/APM/Java/md-docs/SpringBoot/LinuxAMD64/QuickStart/springBoot-linuxamd64-quickStart-instrumentApplication.md'; import APM_java_springBoot_linuxAMD64_quickStart_instrumentApplication from '../Modules/APM/Java/md-docs/SpringBoot/LinuxAMD64/QuickStart/springBoot-linuxamd64-quickStart-instrumentApplication.md';
import APM_java_springBoot_linuxAMD64_quickStart_runApplication from '../Modules/APM/Java/md-docs/SpringBoot/LinuxAMD64/QuickStart/springBoot-linuxamd64-quickStart-runApplication.md'; import APM_java_springBoot_linuxAMD64_quickStart_runApplication from '../Modules/APM/Java/md-docs/SpringBoot/LinuxAMD64/QuickStart/springBoot-linuxamd64-quickStart-runApplication.md';
@ -1053,6 +1054,7 @@ export const ApmDocFilePaths = {
APM_java_springBoot_kubernetes_recommendedSteps_setupOtelCollector, APM_java_springBoot_kubernetes_recommendedSteps_setupOtelCollector,
APM_java_springBoot_kubernetes_recommendedSteps_instrumentApplication, APM_java_springBoot_kubernetes_recommendedSteps_instrumentApplication,
APM_java_springBoot_kubernetes_recommendedSteps_runApplication, APM_java_springBoot_kubernetes_recommendedSteps_runApplication,
APM_java_springBoot_kubernetes_recommendedSteps_runApplication_producer,
// SpringBoot-LinuxAMD64-recommended // SpringBoot-LinuxAMD64-recommended
APM_java_springBoot_linuxAMD64_recommendedSteps_setupOtelCollector, APM_java_springBoot_linuxAMD64_recommendedSteps_setupOtelCollector,

View File

@ -399,3 +399,5 @@ export const moduleRouteMap = {
[ModulesMap.AwsMonitoring]: ROUTES.GET_STARTED_AWS_MONITORING, [ModulesMap.AwsMonitoring]: ROUTES.GET_STARTED_AWS_MONITORING,
[ModulesMap.AzureMonitoring]: ROUTES.GET_STARTED_AZURE_MONITORING, [ModulesMap.AzureMonitoring]: ROUTES.GET_STARTED_AZURE_MONITORING,
}; };
export const messagingQueueKakfaSupportedDataSources = ['java'];

View File

@ -0,0 +1,22 @@
import getOnboardingStatus, {
OnboardingStatusResponse,
} from 'api/messagingQueues/onboarding/getOnboardingStatus';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
type UseOnboardingStatus = (
options?: UseQueryOptions<
SuccessResponse<OnboardingStatusResponse> | ErrorResponse
>,
) => UseQueryResult<SuccessResponse<OnboardingStatusResponse> | ErrorResponse>;
export const useOnboardingStatus: UseOnboardingStatus = (options) =>
useQuery<SuccessResponse<OnboardingStatusResponse> | ErrorResponse>({
queryKey: ['onboardingStatus'],
queryFn: () =>
getOnboardingStatus({
start: (Date.now() - 15 * 60 * 1000) * 1_000_000,
end: Date.now() * 1_000_000,
}),
...options,
});

View File

@ -4,6 +4,7 @@ import { ExclamationCircleFilled } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens'; import { Color } from '@signozhq/design-tokens';
import { Button, Modal } from 'antd'; import { Button, Modal } from 'antd';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2'; import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { Calendar, ListMinus } from 'lucide-react'; import { Calendar, ListMinus } from 'lucide-react';
@ -86,7 +87,7 @@ function MessagingQueues(): JSX.Element {
type="default" type="default"
onClick={(): void => onClick={(): void =>
getStartedRedirect( getStartedRedirect(
ROUTES.GET_STARTED_APPLICATION_MONITORING, `${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=consumer`,
'Configure Consumer', 'Configure Consumer',
) )
} }
@ -105,7 +106,7 @@ function MessagingQueues(): JSX.Element {
type="default" type="default"
onClick={(): void => onClick={(): void =>
getStartedRedirect( getStartedRedirect(
ROUTES.GET_STARTED_APPLICATION_MONITORING, `${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=producer`,
'Configure Producer', 'Configure Producer',
) )
} }
@ -124,7 +125,7 @@ function MessagingQueues(): JSX.Element {
type="default" type="default"
onClick={(): void => onClick={(): void =>
getStartedRedirect( getStartedRedirect(
ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING, `${ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=kafka`,
'Monitor kafka', 'Monitor kafka',
) )
} }

View File

@ -1,3 +1,4 @@
import { OnboardingStatusResponse } from 'api/messagingQueues/onboarding/getOnboardingStatus';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetWidgetQueryBuilderProps } from 'container/MetricsApplication/types'; import { GetWidgetQueryBuilderProps } from 'container/MetricsApplication/types';
@ -344,3 +345,39 @@ export const getMetaDataAndAPIPerView = (
}, },
}; };
}; };
interface OnboardingStatusAttributeData {
overallStatus: string;
allAvailableAttributes: string[];
attributeDataWithError: { attributeName: string; errorMsg: string }[];
}
export const getAttributeDataFromOnboardingStatus = (
onboardingStatus?: OnboardingStatusResponse | null,
): OnboardingStatusAttributeData => {
const allAvailableAttributes: string[] = [];
const attributeDataWithError: {
attributeName: string;
errorMsg: string;
}[] = [];
if (onboardingStatus?.data && !isEmpty(onboardingStatus?.data)) {
onboardingStatus.data.forEach((status) => {
if (status.attribute) {
allAvailableAttributes.push(status.attribute);
if (status.status === '0') {
attributeDataWithError.push({
attributeName: status.attribute,
errorMsg: status.error_message || '',
});
}
}
});
}
return {
overallStatus: attributeDataWithError.length ? 'error' : 'success',
allAvailableAttributes,
attributeDataWithError,
};
};