Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 92 Next »

🛠️ Potential Solutions

Note: this section should be short and sweet, documenting notes about different solutions that were considered during ideation. Can just be a link to a whiteboard for example. Expected outcome: one solution is chosen and documented in more detail under Proposed Solution.

✅ Proposed Solution

Context

Currently, the way issues on our platform are presented in Amplitude occurs through error modal events. While this approach is functional, it has significant limitations as it focuses solely on the appearance of the modal, without providing a clear view of the true source of the problems. Error modals can represent both front-end and back-end failures, resulting from a variety of factors, such as configuration errors, RBI implementation, partner integrations, and user-induced errors. At present, there is no clear path to understand the fluctuations in error modals, which prevents the team from comprehending the friction points that users face on our platform.

To address these issues, a list of error codes has been proposed that should be documented and shared with the FZ team, enabling clear and standardized communication regarding the types of errors that may arise. Moreover, a collaborative effort between the front-end and back-end teams will be necessary to ensure that error codes are sent correctly and that the documentation is accessible and understandable. We will implement a protocol to send errors with an initial layer of granularity to mParticle, so they can be utilized in Amplitude, providing immediate insights into the causes of variations and enabling deeper analysis.

In other words:

  • Allow greater versatility to display errors;

  • Allow user-friendly messages;

  • Organized and centralized errors;

  • Provide a list of accessible errors;

  • Allow greater versatility in sending events to mParticle, showing or not the error modal;

More details:

Proposal

Description

The focus of the frontend (Whitelabel) is to receive the new error code and be able to generate the translation according to the error, displaying it in the modals of the Commit Order flow.

Additionally, it falls within the scope of the frontend to create a new event sending method for mParticle/Amplitude that enables sending these events either when the modal appears or independently, without the need to display the modal.

At this moment the focus will be just on PAYCOMET errors:

Erros description

Paycomet Error Code

Operation not allowed for the credit card type

102

Card issuer could not validate card owner

115

CVC2 block incorrect

111

Operation not allowed for the credit card type

130

Country (customer IP address) not allowed

1192

Number of failed attempts (last 30 minutes) from the same ip address exceeded

1202

When the same card has several payments in "flight" at the time that one is finalized, the rest are denied with this code. This restriction is for security

1367

Authorization from different country than card issuer, not allowed

1196

  • The backend will return to the frontend from CommitOrder the response with:

    • code: The new error code with Domains that will be translated to show the error description in the modal;

    • rbiErrorCode: The new error code without Domains;

      • e.g.: 2.001, 2.102.003, 4.004

    • rbiErrorDomain: Domain related to the flow

      • e.g.: PAYMENT, CHECKOUT, CART

    • statusCode: Status code

      • e.g.: 500, 303, 404

    • statusCategory: Status category

      • e.g.: 5XX, 3XX, 4XX

 Response Details
[
  {
    "errors": [
      {
        "locations": [
          {
            "line": 2,
            "column": 3
          }
        ],
        "message": "General external validation error",
        "path": [
          "commitOrder"
        ],
        "extensions": {
          "code": "PAYMENT.2.001",
          "rbiErrorCode": "2.001",
          "rbiErrorDomain": "PAYMENT",
          "statusCode": 500,
          "statusCategory": "5XX"
        }
      }
    ],
    "data": null
  }
]

Currently, there are two error handling methods: mapErrorsWithCodes and parseGraphQLErrorCodes, which return the IGQLParsedErrorCode object used for all GraphQL errors.

We will need to implement new methods to handle the new error response without impacting the existing error flow. These new methods will be the main ones for handling GraphQL errors.

For example, the payment flow will use the new methods solely for the errors mentioned above, while all other methods will continue to use mapErrorsWithCodes and parseGraphQLErrorCodes.

After consolidating and validating the hypotheses regarding this feature, we will begin the migration to the new error model.

Workflow

 Workflow High-level

Exported Diagram Image

image-20240828-101328.png

Diagram ZenUML

 Workflow detailed

Exported Diagram Image

image-20240828-101501.png

Diagram ZenUML

Error Messages

Paycomet

Key

Description

Modal Title

Modal Message

Modal CTA

Screen

102

error.PAYMENT.2.001

Operation not allowed for the credit card type

Payment Declined

Your card does not support this payment type at the moment. Please, contact your bank or try another payment method.

"Change Payment Method”

image-20241001-045844.png

130

error.PAYMENT.2.100

Represents an error related to credit/debit card transactions.

Payment Declined"

Your card does not support this payment type at the moment. Please, contact your bank or try another payment method."

"Change Payment Method”

image-20241001-050256.png

111

error.PAYMENT.2.100.001

The payment transaction was rejected because the user informed an invalid card CVV.

Payment Declined"

The payment transaction was declined. Please check the card name, card number, CVV, expire date and try again.

"Review Card Details

Screenshot 2024-10-02 at 08.26.26.png

115

error.PAYMENT.2.101.001

The payment transaction was rejected because card issuer could not validate card owner's identity.

Payment Declined"

“Sorry, we were unable to verify the cardholder's identity. Please authorize payment and try again.

"Go Back”

Screenshot 2024-10-02 at 09.56.53.png

1367

error.PAYMENT.2.101.002

Represents an error when In-Flight offline payments are rejected.

An example of occurrence is when the same card has several payments in "flight" at the time and one is finalized, the rest might be denied.

Payment Failed

Sorry, we are unable to process this payment. Please use another payment method.

"Go Back And Try Again”

image-20241001-050417.png

1192

error.PAYMENT.2.102.002

Represents an error when the user's IP address is not from an allowed country.

Ex.: Iberia payments only allowed from Spain and Portugal.

Payment Declined"

“It seems you are ordering from a different location than our restaurants. Please, update your delivery address or choose the nearest restaurant and try again.”

"Change Your Location or Restaurant”

image-20241001-050459.png

1202

error.PAYMENT.2.102.003

Represents an error when the number of failed attempts from the same user has been exceeded.

Order Attempts Exceeded"

“You have exceeded the number of attempts to place an order. Please try again later.”

"Go Back and Try Again Later”

Screenshot 2024-10-02 at 08.29.26.png

Old Methods

Until that migration starts, we will deprecate the methods mapErrorsWithCodes and parseGraphQLErrorCodes to prevent the creation of new error flows using the "old" approach, encouraging the adoption of the new model.

In summary, the current error flows will function independently of the new error model.

For this, we will add a doc comment in the top methods:

mapErrorsWithCodes
  • path: intl-whitelabel-app/workspaces/frontend/src/utils/errors/index.ts

 mapErrorsWithCodes details
/**
 * @deprecated This function will be removed in future versions.
 * Use `<NEW_METHOD_NAME>` instead.
 */
 const mapErrorsWithCodes = filterMap<GQLError, NonNullableObject<GQLError>, IGQLParsedErrorCode>(
  err => !!err.extensions?.code,
  err => ({ errorCode: err.extensions.code, message: err.message }) as IGQLParsedErrorCode
);
parseGraphQLErrorCodes
  • path: intl-whitelabel-app/workspaces/frontend/src/utils/errors/index.ts

 parseGraphQLErrorCodes details
/**
 * @deprecated This function will be removed in future versions.
 * Use `NEW_METHOD_NAME` instead.
 */
 export function parseGraphQLErrorCodes(error: GraphQLError | ApolloError): IGQLParsedErrorCode[] {
  if (isApolloError(error)) {
    return mapErrorsWithCodes(error.graphQLErrors);
  }
  return mapErrorsWithCodes(error.originalError || []);
}

New Methods

Creating new methods to validate the new errors is good because it will allow the validation of specific attributes (for example: err.extensions?.rbiErrorDomain) without needing to use the feature flag or the optional creation.

How?

The new interface IRbiError and new methods will be parseGraphQLRBIError and parseGraphQLRBIError:

IRbiError
 IRbiError interface details
export interface IRbiError {
  errorCode: string;
  rbiErrorCode: string;
  rbiErrorDomain: string;
  statusCode: string;
  message: string;
}
mapRBIErrors
  • path: intl-whitelabel-app/workspaces/frontend/src/utils/errors/index.ts

 mapRBIErrors details
/**
 * Maps GraphQL errors to a specific IRbiError format.
 *
 * This function filters and transforms GraphQL errors based on the presence
 * of the 'rbiErrorDomain' field in the error's extensions. If the 'rbiErrorDomain'
 * is present, the error is transformed into the IRbiError format
 *
 * @param err - The GraphQL error object to be transformed.
 * @returns A transformed error object in IRbiError format if applicable, otherwise undefined.
 */
const mapRBIErrors = filterMap<GQLError, NonNullableObject<GQLError>, IRbiError>(
  err => !!err.extensions?.rbiErrorDomain,
  err => {
    const { code, rbiErrorCode, rbiErrorDomain, statusCode } = err.extensions || {};
    return {
      errorCode: code,
      rbiErrorCode,
      rbiErrorDomain,
      statusCode,
      message: err?.message,
    } as IRbiError;
  }
);
parseGraphQLRBIError
  • path: intl-whitelabel-app/workspaces/frontend/src/utils/errors/index.ts

 parseGraphQLRBIError details
/**
 * Parses a GraphQL error, returning an array of IRbiError objects.
 * If the error is an ApolloError, it uses the graphQLErrors; otherwise, it falls back on originalError.
 *
 * @param error - The GraphQL error to parse.
 * @returns An array of IRbiError objects.
 */
export function parseGraphQLRBIError(error: GraphQLError | ApolloError): IRbiError[] {
  if (isApolloError(error)) {
    return mapRBIErrors(error.graphQLErrors);
  }
  return mapRBIErrors(error.originalError || []);
}

Looking especially for the mapRBIErrors method:

  • The new error will have a unique attribute, which no error object within Whitelabel has had until now: rbiErrorDomain

  • This attribute will be just in the new error model, that is, any error returned by the backend with this existing or populated field will mean that it must follow the new error flow and will have the specific interface of the new model.

But, for this feature there won't be a feature flag?

Not exactly, the backend has a feature flag to the feature.

  • Therefore, when the backend returns the response with this attribute, the frontend will know what flow follows.

(tick) Positive Points:

  • No need to create more than one feature flag to the same feature;

  • Use the effect of enabling the Feature Flag from the backend to the frontend;

  • Avoids errors of enabling a feature flag, for example, on the frontend and not enabling it on the backend, as has already happened in the case of the skip pre-auth feature flags.

  • Deprecating the old methods will prevent the creation of new error streams using the "old" approach.

  • Creating new methods will guarantee the segregation of the flows between old errors and new errors.

(error) Negative points:

  • Method validation cannot change (!!err.extensions?.rbiErrorDomain) to ensure that the flow works;

  • To use the new error model, we will need to create an error code on the backend first;

  • For some time, the coexistence of 2 error streams will be necessary for old errors and new errors;

OBS.: If necessary there is a feature flag on the frontend, we can add the validation on the method mapRBIErrors.

  • path: intl-whitelabel-app/workspaces/frontend/src/utils/errors/index.ts

 mapRBIErrors details
const mapRBIErrors = filterMap<GQLError, NonNullableObject<GQLError>, IRbiError>(
  err => _FEATURE_FLAG_VALUE_ && !!err.extensions?.rbiErrorDomain,
  err => {
    const { code, rbiErrorCode, rbiErrorDomain, statusCode } = err.extensions || {};
    return {
      errorCode: code,
      rbiErrorCode,
      rbiErrorDomain,
      statusCode,
      message: err?.message,
    } as IRbiError;
  }
);

In addition of creating the new methods mapRBIErrors and parseGraphQLRBIError, it will need to create the other methods:

isRBIError
  • path: intl-whitelabel-app/workspaces/frontend/src/utils/errors/index.ts

  • This method will be created to validate if the error is an RBI error.

 isRBIError details
/**
 * Checks if the provided error code is an RBI error.
 *
 * @param {IRbiError} error - The error code to be evaluated. Can be of any type.
 * @returns {boolean} - Returns true if the error code is considered an RBI error,
 *                      otherwise returns false.
 */
export const isRBIError = (error: IRbiError): boolean => !!error?.rbiErrorCode && !!error?.rbiErrorDomain;
extractRBIErrorCodes
  • path: intl-whitelabel-app/workspaces/frontend/src/utils/errors/index.ts

  • As the error is DOMAIN.SEVERITY.CODE.ERROR, for the translation, it will be necessary to create a method to split the error and get the translation.

  • For example:

    • If the error is: PAYMENT.2.102.001, the split will return the array with:

      • PAYMENT.2.102.001

      • PAYMENT.2.102

      • PAYMENT.2

      • PAYMENT

    • This array will be processed for the method getRBIErrorTranslation, which will be discussed in the below topic.

      • OBS.: This method needs to add in the return a function .reverse() to translation correct because normally the .split() will return an array: PAYMENT, PAYMENT.2, PAYMENT.2.102, PAYMENT.2.102.001, so the reverse is necessary to get the translation of the most detailed (PAYMENT.2.102.001) error to the least detailed (PAYMENT).

 extractRBIErrorCodes details
/**
 * Extracts error codes from a given error code string, returning an array of hierarchical error codes.
 *
 * @param errorCode - The error code string to be split.
 * @returns An array of error codes representing the hierarchy.
 */
export const extractRBIErrorCodes = (errorCode: string): string[] => {
  const errorCodes = errorCode.split('.');
  return errorCodes.map((_, index) => errorCodes.slice(0, index + 1).join('.')).reverse();
};
getRBIErrorTranslation
  • path: intl-whitelabel-app/workspaces/frontend/src/utils/errors/index.ts

  • This method calls the extractRBIErrorCodes and will get the Lokalise the correct translation.

    • For example:

      • Example 1:

        • If the translation file (Lokalise) is:

          "error.PAYMENT.title": "Translation for the error error.PAYMENT.title",
          "error.PAYMENT.default": "Translation for the error.PAYMENT.default",
          "error.PAYMENT.2.001": "Translation for the error 2.001",
          "error.PAYMENT.2.002": "Translation for the error 2.002",
          "error.PAYMENT.2.003": "Translation for the error 2.003",
          "error.PAYMENT.2.004": "Translation for the error 2.004",
          "error.PAYMENT.2.100": "Translation for the error 2.100",
        • And the extractRBIErrorCodes method returns the array:

          ['PAYMENT.2.102.001', 'PAYMENT.2.102', 'PAYMENT.2', 'PAYMENT']
          • The getRBIErrorTranslation will search first PAYMENT.2.102.001, as this item does not exist, the method will look for the next valid PAYMENT.2.102, In this example there is the translation "error.PAYMENT.2.102": "Translation for the error 2.102", as the translation was found, so the getRBIErrorTranslation method will return the Translation for the error 2.102.

            • OBS.: These translation labels are an example;

            • The label “error” at the beginning of the Lokalise file and the parameters, will explained below topic:

      • Example 2:

        • Using the same Lokalise translation described above:

          • If the extractRBIErrorCodes method returns the array:

            ['PAYMENT.4.008', 'PAYMENT.4', 'PAYMENT']
          • The getRBIErrorTranslation will search first PAYMENT.4.008, as this item does not exist, the method will look for the next valid PAYMENT.4, but there isn’t too, so the method will search for the next PAYMENT and there isn’t.

          • In this example, will return the default translation, "error.PAYMENT.default": "Translation for the error.PAYMENT.default".

 getRBIErrorTranslation details
/**
 * Retrieves the translation for an error based on the provided error code and translation mappings.
 * It attempts to find a specific translation for the given error code; if none exists, it falls back to a default translation.
 *
 * @param errorCode - Optional error code to translate.
 * @param errorTranslation - Optional specific translation key.
 * @param defaultTranslationId - Default translation identifier if no specific translation is found.
 * @param formatMessage - Function to format messages.
 * @returns The translated error message.
 */
export const getRBIErrorTranslation = ({
  errorCode,
  errorTranslation,
  defaultTranslationId,
  formatMessage,
}: {
  errorCode: string;
  errorTranslation?: ErrorTranslation;
  defaultTranslationId: string;
  formatMessage: IntlShape['formatMessage'];
}) => {
  const errorCodes = errorCode ? extractRBIErrorCodes(errorCode) : [];

  for (const code of errorCodes) {
    const key = errorTranslation ? `error.${code}.${errorTranslation}` : `error.${code}`;

    const translation = formatMessage({ id: key as TLocalizationKey });

    if (translation !== key) {
      return translation;
    }
  }
  return formatMessage({ id: defaultTranslationId as TLocalizationKey });
};
fetchRBIErrorDetails

The last new method will be: fetchRBIErrorDetails;

This method is responsible for orchestrating which label will need to be translated. If it will translate the modal title, description modal, etc.

  • path: intl-whitelabel-app/workspaces/frontend/src/utils/errors/index.ts

  • For example, if the translation needs to be the title, this method will call the getRBIErrorTranslation like this:

    const title = getRBIErrorTranslation({
      errorCode: rbiError.errorCode,
      errorTranslation: ErrorTranslation.TITLE,
      defaultTranslationId: `error.${rbiError.rbiErrorDomain}.${ErrorTranslation.TITLE}`,
      formatMessage,
    });
  • Parameters:

    • errorCode: The new error code with Domains that will be translated to show the error description in the modal;

      • e.g.: PAYMENT.2.001

    • errorTranslation: If this translation is a title or description error, if it will be the description, it won’t add the errorTranslation, because the own error PAYMENT.2.001 key will be the description;

    • defaultTranslationId: What default key will need to translate if the error code won’t be found:

      • `error.PAYMENT.title`for titles;

      • `error.PAYMENT.default`for descriptions;

    • formatMessage: Method used for Whitelabel to get the translation from Lokalise;

 fetchRBIErrorDetails details
/**
 * Fetches details for a given RBI error, including its translated title and body.
 *
 * @param {Object} params - The parameters object.
 * @param {IRbiError} params.rbiError - The RBI error object to fetch details for.
 * @param {IntlShape['formatMessage']} params.formatMessage - The function used to format messages based on localization.
 *
 * @returns {Object} An object containing the title, body, message, and the original error.
 */
export const fetchRBIErrorDetails = ({
  rbiError,
  formatMessage,
}: {
  rbiError: IRbiErrorIRbiError;
  formatMessage: IntlShape['formatMessage'];
}) => {
  try {
    const body = getRBIErrorTranslation({
      errorCode: rbiError.errorCode,
      defaultTranslationId: `error.${rbiError.rbiErrorDomain}.${ErrorTranslation.DEFAULT}`,
      formatMessage,
    });

    const title = getRBIErrorTranslation({
      errorCode: rbiError.errorCode,
      errorTranslation: ErrorTranslation.TITLE,
      defaultTranslationId: `error.${rbiError.rbiErrorDomain}.${ErrorTranslation.TITLE}`,
      formatMessage,
    });

    return {
      title,
      body,
      message: rbiError.message,
      error: rbiError,
    };
  } catch (error) {
    return {
      title: formatMessage({
        id: `error.${rbiError.rbiErrorDomain}.${ErrorTranslation.TITLE}` as TLocalizationKey,
      }),
      body: formatMessage({
        id: `error.${rbiError.rbiErrorDomain}.${ErrorTranslation.DEFAULT}` as TLocalizationKey,
      }),
      message: rbiError.message,
      error: rbiError,
    };
  }
};

Other Changes

ErrorTranslation
  • Create a new enum to be used on fetchRBIErrorDetails

  • path: intl-whitelabel-app/workspaces/frontend/src/utils/errors/types.ts

 ErrorTranslation details
export enum ErrorTranslation {
  TITLE = 'title',
  DEFAULT = 'default',
}
IDialogProps
  • Change the attribute modalAppearanceEventMessage to optional;

  • path: intl-whitelabel-app/workspaces/frontend/src/components/dialog/index.tsx

export interface IDialogProps extends IBaseProps {
  ...
  modalAppearanceEventMessage?: string;
  ...
}

useErrorModal

The existing hook is the hook that processes the modal error component;

  • Path: intl-whitelabel-app/workspaces/frontend/src/hooks/use-error-modal.tsx

For this feature, we will need to change this hook to accommodate the requirements to add to this doc;

  • Change the openErrorDialog method to receive the new error model;

  • Create a new method recordRbiError to send the events to mParticle/Amplitude without open the modal;

Change types
  • Path: intl-whitelabel-app/workspaces/frontend/src/hooks/use-error-modal.tsx

    • Create a new type:

      export type RecordRBIErrorType = ({ rbiErrors }: { rbiErrors: IRbiError[] }) => void;
    • Change the type UseErrorDialogHook to create a new attribute RecordRBIErrorType:

      export type UseErrorDialogHook<P = any> = [
        React.FC<Props<P>>,
        ModalCb,
        VoidFunction,
        recordRBIErrorType,
      ];
    • Change the interface IUseErrorModalArgs to optional:

      interface IUseErrorModalArgs<T> {
        ...
        modalAppearanceEventMessage?: string;
      }
    • Change the interface IErrorModal to optional and create a new attribute rbiError:

      export interface IErrorModal {
        ...
        message?: string | ReactNode;
        ...
        rbiError?: IRbiError[];
      }
recordRBIErrorEvents

This method will send the new error events to mParticle/Amplitude;

The events will be:

event

Name

Value

name

ERROR (CustomEventNames.ERROR)

type

Other = 8 (EventTypes.Other)

attributes

Name

Value

'Error Code'

PAYMENT.2.001

Name

CVC2 block incorrect

Path

/cart/payment

'Response Title' (modal title)

title: Uh oh!

'Response Description' (modal description - User-friendly Messages)

description: The CVC number is incorrect. Please look carefully at your card details. This number is 3 figures and is usually on the back of your card. If the charge has been pre-authorized on your card, we will reimburse you within a maximum period of seven days.

If called to recordRBIErrorEvents (Without Modal)

  • Won’t be sent the 'Response Title' and 'Response Description'

 recordRBIErrorEvents details
/**
   * Records RBI error events by tracking each error and logging the overall process.
   *
   * @param {Object} params - The parameters for the function.
   * @param {IRbiError[]} params.rbiErrors - An array of RBI error objects to be recorded.
   * @param {Object} [params.attrs] - Optional attributes to include with each event.
   * @returns {void}
   */
  const recordRBIErrorEvents = useCallback(
    ({
      rbiErrors,
      attrs,
    }: {
      rbiErrors: IRbiError[];
      attrs?: { [key: string]: string | number | boolean };
    }) => {
      try {
        rbiErrors.forEach(error => {
          trackEvent({
            name: CustomEventNames.ERROR,
            type: EventTypes.Other,
            attributes: {
              'Error Code': error?.errorCode,
              Message: error.message,
              Path: pathname,
              ...attrs,
            },
          });
        });

        logger.error({
          error: rbiErrors,
          message: 'Record RBI Error Modal',
        });
      } catch (error) {
        logger.error({
          message: 'Error while recording error events',
          error,
        });
      }
    },
    [logger, pathname, trackEvent]
  );
openErrorDialog
  • This method is responsible for opening the modal error;

  • Path: intl-whitelabel-app/workspaces/frontend/src/hooks/use-error-modal.tsx

For the code to get more legible, we will need to refactor it:

  • We will create a new errorModal method to receive the existing if code block inside the openErrorDialog

 openErrorDialog details
/**
 * Opens an error dialog with the provided error data.
 *
 * @param {IErrorModal} data - The error data containing details for the modal.
 * @returns {void} - This method does not return a value.
 */
const openErrorDialog = useCallback(
(data: IErrorModal) => {
  onOpen(data);

  const { rbiError, ...rest } = data;

  if (Array.isArray(rbiError) && rbiError.length) {
    const rbiErrorDetails = fetchRBIErrorDetails({ rbiError: rbiError?.[0], formatMessage });

    recordRBIErrorEvents({
      rbiErrors: rbiError,
      attrs: {
        'Response Title': rbiErrorDetails.title,
        'Response Description': rbiErrorDetails.body,
      },
    });

    setPending(rbiErrorDetails as T);
  } else {
    parseErrorModal(rest);
    setPending(rest as T);
  }

  setOpen(true);
},
[errorModal, formatMessage, onOpen, recordRBIErrorEvents]
);
parseErrorModal
  • We will create a new parseErrorModal method to receive the existing if code block inside the openErrorDialog

 errorModal details
/**
   * Displays an error modal based on the provided error data.
   *
   * @param {IErrorModal} data - The error data containing error details and message.
   * @param {Error | IGraphqlErrorMap | undefined} data.error - The error object to be logged.
   * @param {string | null} data.message - The message to be displayed in the modal.
   * @param {string} [data.modalAppearanceEventMessage] - Optional override message for modal appearance.
   *
   * @returns {void}
   */
  const parseErrorModal = useCallback(
    (data: IErrorModal) => {
      if (data) {
        const { error, modalAppearanceEventMessage: eventMessageOverride } = data;
        const message: string | null = typeof data.message === 'string' ? data.message : null;
        let parsedError: Error | IGraphqlErrorMap | undefined = error;

        if (eventMessageOverride) {
          modalAppearanceEventMessageOverride.current = eventMessageOverride;
        }

        if (parsedError && isApolloError(parsedError)) {
          const errors = parseGraphQLErrorCodes(parsedError);

          if (errors.length) {
            const [{ errorCode, message: errorMessage }] = errors;
            errorEventMessageOverride.current = [errorCode, errorMessage].join(' ');
            // we cannot filter datadog results based on array elements
            // so we create a map of the list of errors supplied by apollo
            parsedError = createGqlErrorMap([...parsedError.graphQLErrors]);
          }
        }

        logger.error({
          error: parsedError,
          message: eventMessageOverride || modalAppearanceEventMessage,
          modalHeader: eventMessageOverride || modalAppearanceEventMessage,
          modalMessage: message,
        });
      }
    },
    [logger, modalAppearanceEventMessage]
  );
ErrorDialogComponent
  • Path: intl-whitelabel-app/workspaces/frontend/src/hooks/use-error-modal.tsx

To accommodate the changes, will need to change the component adding the get to recover the error:

const pendingRbiError = delve(pendingData as T, 'error', null);

This component calls the other component Dialog and this sends its own event to mParticle/Amplitude . We will need to change this calling to not send the event from Dialog and send the event independently.

Then, we will need to validate the error type:

  • If the error is the new error modal (RBIError), the modalAppearanceEvent and errorEventMessage will be undefined:

const modalAppearanceEvent = isRBIError(pendingRbiError)
  ? undefined
  : modalAppearanceEventMessageOverride.current || modalAppearanceEventMessage;

const errorEventMessage = isRBIError(pendingRbiError)
  ? undefined
  : errorEventMessageOverride.current;
  • And change the Dialog calls:

    <Dialog
      heading={title || heading}
      body={dialogBody}
      image={image}
      onDismiss={dismissDialog}
      actions={actions}
      modalAppearanceEventMessage={modalAppearanceEvent}
      errorEventMessage={errorEventMessage}
      aria-label="error"
      {...rest}
    />
 ErrorDialogComponent details
const ErrorDialogComponent: UseErrorDialogHook<P>[0] = useCallback(
    ({
      buttonLabel = formatMessage({ id: 'okay' }),
      heading = formatMessage({ id: 'somethingWrong' }),
      image,
      ...rest
    }) => {
      ...
      const pendingRbiError = delve(pendingData as T, 'error', null);

      ...

      const modalAppearanceEvent = isRBIError(pendingRbiError)
        ? undefined
        : modalAppearanceEventMessageOverride.current || modalAppearanceEventMessage;

      const errorEventMessage = isRBIError(pendingRbiError)
        ? undefined
        : errorEventMessageOverride.current;

      return (
        <Dialog
          heading={title || heading}
          body={dialogBody}
          image={image}
          onDismiss={dismissDialog}
          actions={actions}
          modalAppearanceEventMessage={modalAppearanceEvent}
          errorEventMessage={errorEventMessage}
          aria-label="error"
          {...rest}
        />
      );
    },
    ...
  );

How to use the new error model?

After all these changes, when it’s needed to use the new error model:

import the useErrorModal:

const [ErrorDialog, openErrorDialog, recordRBIErrorEvents] = useErrorModal();
Open the modal
const errorCodeRBI = parseGraphQLRBIError(error);
openErrorDialog({ rbiError: errorCodeRBI });
Send the event without opening the modal
const errorCodeRBI = parseGraphQLRBIError(error);
recordRBIErrorEvents({ rbiErrors: errorCodeRBI });

POC

More details on Github: Generic Errors, Paycomet Error Map, and PSP Error

  • Lokalise file:

    • Path: intl-whitelabel-app/workspaces/frontend/src/state/translations/en.json

 en.json details
{
  ...
  "error.PAYMENT.title": "Uh oh!",
  "error.PAYMENT.default": "Something went wrong, please try again.",
  "error.PAYMENT.button": "Go back",
  "error.PAYMENT.2.001.title": "Card type invalid",
  "error.PAYMENT.2.001.button": "Pay another way",
  "error.PAYMENT.2.001": "Your card is not enabled to allow this payment at the moment. Please, contact your bank or try another payment method.",
  "error.PAYMENT.2.002.title": "Something went wrong",
  "error.PAYMENT.2.002.button": "Pay another way",
  "error.PAYMENT.2.002": "We had a problem processing this payment. Please try another payment method.",
  "error.PAYMENT.2.003.title": "Something went wrong",
  "error.PAYMENT.2.003.button": "Pay another way",
  "error.PAYMENT.2.003": "We had a problem processing this payment with this payment method. Please contact your bank support or try another payment method.",
  "error.PAYMENT.2.004.title": "Validation error",
  "error.PAYMENT.2.004.button": "Check the information",
  "error.PAYMENT.2.004": "The payment transaction was declined. Please check the Card information, number, CVV, expire date and try again.",
  "error.PAYMENT.2.100.title": "Card type invalid",
  "error.PAYMENT.2.100.button": "Go back",
  "error.PAYMENT.2.100": "Your card is not enabled to allow this payment at the moment. Please, contact your bank or try another payment method.",
  "error.PAYMENT.2.100.001.title": "Validation error",
  "error.PAYMENT.2.100.001.button": "Check the information",
  "error.PAYMENT.2.100.001": "The payment transaction was declined. Please check the Card information, number, CVV, expire date and try again.",
  "error.PAYMENT.2.101.title": "Something went wrong",
  "error.PAYMENT.2.101.button": "Go back",
  "error.PAYMENT.2.101": "We were unable to process this payment due to internal errors. Please try again later.",
  "error.PAYMENT.2.101.001.title": "Identity error",
  "error.PAYMENT.2.101.001.button": "Go back",
  "error.PAYMENT.2.101.001": "Sorry, we were unable to verify the cardholder's identity. Please use another payment method.",
  "error.PAYMENT.2.101.002.title": "Something went wrong",
  "error.PAYMENT.2.101.002.button": "Go back",
  "error.PAYMENT.2.101.002": "Sorry, we are unable to process this payment. Please use another payment method.",
  "error.PAYMENT.2.102.title": "Something went wrong",
  "error.PAYMENT.2.102.button": "Go back",
  "error.PAYMENT.2.102": "Sorry, we are unable to process this payment. Please use another payment method.",
  "error.PAYMENT.2.102.001.title": "Card type invalid",
  "error.PAYMENT.2.102.001.button": "Go back",
  "error.PAYMENT.2.102.001": "Your card is not enabled to allow this payment at the moment. Please, contact your bank or try another payment method.",
  "error.PAYMENT.2.102.002.title": "Location Invalid",
  "error.PAYMENT.2.102.002.button": "Go back",
  "error.PAYMENT.2.102.002": "You cannot place orders from this country at this store. Please choose another store and try again.",
  "error.PAYMENT.2.102.003.title": "Order limit exceeded",
  "error.PAYMENT.2.102.003.button": "Go back",
  "error.PAYMENT.2.102.003": "You have exceeded the number of attempts to place an order. Please try again later.",
  "error.PAYMENT.4.000.title": "Failed Payment",
  "error.PAYMENT.4.000.button": "Go back",
  "error.PAYMENT.4.000": "Something went wrong with this payment. Please try again later.",
  "error.PAYMENT.4.001.title": "Failed Payment",
  "error.PAYMENT.4.001.button": "Go back",
  "error.PAYMENT.4.001": "Something went wrong with this payment. Please try again later.",
  "error.PAYMENT.4.002.title": "Failed Payment",
  "error.PAYMENT.4.002.button": "Go back",
  "error.PAYMENT.4.002": "Something went wrong with this payment. Please try again later.",
  "error.PAYMENT.4.003.title": "Failed Payment",
  "error.PAYMENT.4.003.button": "Go back",
  "error.PAYMENT.4.003": "Something went wrong with this payment. Please try again later.",
  "error.PAYMENT.4.004.title": "Failed Payment",
  "error.PAYMENT.4.004.button": "Go back",
  "error.PAYMENT.4.004": "Something went wrong with this payment. Please try again later.",
  "error.PAYMENT.4.005.title": "Failed Payment",
  "error.PAYMENT.4.005.button": "Go back",
  "error.PAYMENT.4.005": "Something went wrong with this payment. Please try again later.",
  "error.PAYMENT.4.006.title": "Failed Payment",
  "error.PAYMENT.4.006.button": "Go back",
  "error.PAYMENT.4.006": "Something went wrong with this payment. Please try again later.",
  "error.PAYMENT.4.007.title": "Failed Payment",
  "error.PAYMENT.4.007.button": "Go back",
  "error.PAYMENT.4.007": "Something went wrong with this payment. Please try again later.",
  "error.PAYMENT.4.008.title": "Failed Payment",
  "error.PAYMENT.4.008.button": "Go back",
  "error.PAYMENT.4.008": "Something went wrong with this payment. Please try again later.",
  ...
}

(warning) Potential Challenges

A potential challenge is:

  • Keeping updated sync of all translations (English, Spanish, Portuguese, etc);

  • Migrating the old error model to the new error model that will be made in the future, can be difficult due to the number of error flows that exist in Whitelabel.

💰 Cost

Describe any additional cost this solution may imply.

🎛️ Configuration

Include here any configuration required (Sanity, LaunchDarkly flags, etc.) required to enable the solution.

📈 Metrics

After the feature, will it be possible to look at the event attributes, will be able to verify an increase of x% of detailed errors compared to previous deployment events;

🗓️ Delivery Plan

Link to the task or list of all the tasks required to deliver the solution, along with its progress.

🧑‍🚒 QA Plan

You can verify the test plan on the page:

⚠️ Call-outs

Tip: Document here any improvement or discussion regarding the feature

  • No labels