teamdigitale/italia-app

View on GitHub
ts/store/middlewares/analytics.ts

Summary

Maintainability
D
1 day
Test Coverage
/* eslint-disable no-fallthrough */
// disabled in order to allows comments between the switch
import * as O from "fp-ts/lib/Option";
import { getType } from "typesafe-actions";

import trackCdc from "../../features/bonus/cdc/analytics/index";
import trackCgnAction from "../../features/bonus/cgn/analytics/index";
import { loadAvailableBonuses } from "../../features/bonus/common/store/actions/availableBonusesTypes";
import trackEuCovidCertificateActions from "../../features/euCovidCert/analytics/index";
import trackFciAction from "../../features/fci/analytics";
import { fciEnvironmentSelector } from "../../features/fci/store/reducers/fciEnvironment";
import { trackBPayAction } from "../../features/wallet/onboarding/bancomatPay/analytics";
import { trackCoBadgeAction } from "../../features/wallet/onboarding/cobadge/analytics";
import trackPaypalOnboarding from "../../features/wallet/onboarding/paypal/analytics/index";
import trackZendesk from "../../features/zendesk/analytics/index";
import { mixpanel } from "../../mixpanel";
import { getNetworkErrorMessage } from "../../utils/errors";
import {
  analyticsAuthenticationCompleted,
  analyticsAuthenticationStarted
} from "../actions/analytics";
import { applicationChangeState } from "../actions/application";
import {
  idpLoginUrlChanged,
  idpSelected,
  loginFailure,
  loginSuccess,
  logoutFailure,
  logoutSuccess,
  sessionExpired,
  sessionInformationLoadFailure,
  sessionInformationLoadSuccess,
  sessionInvalid
} from "../actions/authentication";
import { cieAuthenticationError } from "../actions/cie";
import { contentMunicipalityLoad } from "../actions/content";
import {
  identificationCancel,
  identificationFailure,
  identificationForceLogout,
  identificationPinReset,
  identificationRequest,
  identificationStart,
  identificationSuccess
} from "../actions/identification";
import {
  profileFirstLogin,
  profileLoadFailure,
  profileLoadRequest,
  profileUpsert,
  removeAccountMotivation
} from "../actions/profile";
import { profileEmailValidationChanged } from "../actions/profileEmailValidationChange";
import { searchMessagesEnabled } from "../actions/search";
import { Action, Dispatch, MiddlewareAPI } from "../actions/types";
import {
  deleteUserDataProcessing,
  upsertUserDataProcessing
} from "../actions/userDataProcessing";
import { deleteAllPaymentMethodsByFunction } from "../actions/wallet/delete";
import {
  addCreditCardOutcomeCode,
  paymentOutcomeCode
} from "../actions/wallet/outcomeCode";
import {
  abortRunningPayment,
  paymentAttiva,
  paymentCheck,
  paymentCompletedSuccess,
  paymentDeletePayment,
  paymentExecuteStart,
  paymentIdPolling,
  paymentInitializeState,
  paymentUpdateWalletPsp,
  paymentVerifica,
  paymentWebViewEnd
} from "../actions/wallet/payment";
import {
  fetchTransactionsFailure,
  fetchTransactionsRequest,
  fetchTransactionsSuccess
} from "../actions/wallet/transactions";
import {
  addCreditCardWebViewEnd,
  addWalletCreditCardFailure,
  addWalletCreditCardInit,
  addWalletCreditCardRequest,
  addWalletNewCreditCardFailure,
  addWalletNewCreditCardSuccess,
  deleteWalletFailure,
  deleteWalletRequest,
  deleteWalletSuccess,
  refreshPMTokenWhileAddCreditCard,
  setFavouriteWalletFailure,
  setFavouriteWalletRequest,
  setFavouriteWalletSuccess,
  updatePaymentStatus
} from "../actions/wallet/wallets";
import { buildEventProperties } from "../../utils/analytics";
import { trackServicesAction } from "../../features/services/common/analytics";
import { trackMessagesActionsPostDispatch } from "../../features/messages/analytics";
import { trackContentAction } from "./contentAnalytics";

const trackAction =
  (mp: NonNullable<typeof mixpanel>) =>
  // eslint-disable-next-line complexity
  (action: Action): void | ReadonlyArray<null> => {
    switch (action.type) {
      //
      // Application state actions
      //
      case getType(applicationChangeState):
        return mp.track("APP_STATE_CHANGE", {
          APPLICATION_STATE_NAME: action.payload
        });
      //
      // Authentication actions (with properties)
      //
      case getType(idpSelected):
        return mp.track(action.type, {
          SPID_IDP_ID: action.payload.id,
          SPID_IDP_NAME: action.payload.name
        });

      case getType(idpLoginUrlChanged):
        return mp.track(action.type, {
          SPID_URL: action.payload.url
        });

      // dispatch to mixpanel when the email is validated
      case getType(profileEmailValidationChanged):
        return mp.track(action.type, { isEmailValidated: action.payload });

      case getType(fetchTransactionsSuccess):
        return mp.track(action.type, {
          count: action.payload.data.length,
          total: O.getOrElse(() => -1)(action.payload.total)
        });
      // end pay webview Payment (payment + onboarding credit card) actions (with properties)
      case getType(addCreditCardWebViewEnd):
        return mp.track(action.type, {
          exitType: action.payload
        });
      case getType(paymentOutcomeCode):
        return mp.track(action.type, {
          outCome: O.getOrElse(() => "")(action.payload.outcome),
          paymentMethodType: action.payload.paymentMethodType
        });
      case getType(addCreditCardOutcomeCode):
        return mp.track(action.type, {
          outCome: O.getOrElse(() => "")(action.payload)
        });
      case getType(paymentWebViewEnd):
        return mp.track(action.type, {
          exitType: action.payload.reason,
          paymentMethodType: action.payload.paymentMethodType
        });
      //
      // Payment actions (with properties)
      //
      case getType(paymentAttiva.request):
      case getType(paymentVerifica.request):
        return mp.track(action.type, {
          organizationFiscalCode: action.payload.rptId.organizationFiscalCode,
          paymentNoticeNumber: action.payload.rptId.paymentNoticeNumber
        });
      case getType(paymentVerifica.success):
        return mp.track(action.type, {
          amount: action.payload.importoSingoloVersamento
        });
      case getType(paymentCompletedSuccess):
        // PaymentCompletedSuccess may be generated by a completed payment or
        // by a verifica operation that return a duplicated payment error.
        // Only in the former case we have a transaction and an amount.
        if (action.payload.kind === "COMPLETED") {
          const amount = action.payload.transaction?.amount.amount;
          mp.track(action.type, {
            amount,
            kind: action.payload.kind
          });
          return mp.getPeople().trackCharge(amount ?? -1, {});
        } else {
          return mp.track(action.type, {
            kind: action.payload.kind
          });
        }
      //
      // Wallet / payment failure actions (reason in the payload)
      //
      case getType(addWalletCreditCardFailure):
        return mp.track(action.type, {
          reason: action.payload.kind,
          // only GENERIC_ERROR could have details of the error
          error:
            action.payload.kind === "GENERIC_ERROR"
              ? action.payload.reason
              : "n/a"
        });
      case getType(addWalletNewCreditCardFailure):
        return mp.track(action.type);

      case getType(paymentAttiva.failure):
      case getType(paymentVerifica.failure):
      case getType(paymentIdPolling.failure):
      case getType(paymentCheck.failure):
        return mp.track(action.type, {
          reason: action.payload
        });
      case getType(updatePaymentStatus.failure):
        return mp.track(action.type, {
          reason: getNetworkErrorMessage(action.payload)
        });

      // logout / load message / delete wallets / failure
      case getType(deleteAllPaymentMethodsByFunction.failure):
      case getType(upsertUserDataProcessing.failure):
      case getType(logoutFailure):
        return mp.track(action.type, {
          reason: action.payload.error.message
        });
      // Failures with reason as Error and optional description
      case getType(cieAuthenticationError):
        return mp.track(action.type, action.payload);
      // Failures with reason as Error
      case getType(sessionInformationLoadFailure):
      case getType(profileLoadFailure):
      case getType(profileUpsert.failure):
      case getType(refreshPMTokenWhileAddCreditCard.failure):
      case getType(deleteWalletFailure):
      case getType(setFavouriteWalletFailure):
      case getType(fetchTransactionsFailure):
      case getType(paymentDeletePayment.failure):
      case getType(paymentUpdateWalletPsp.failure):
      case getType(paymentExecuteStart.failure):
      //  Bonus vacanze
      case getType(loadAvailableBonuses.failure):
        return mp.track(action.type, {
          reason: action.payload.message
        });
      // track when a missing municipality is detected
      case getType(contentMunicipalityLoad.failure):
        return mp.track(action.type, {
          reason: action.payload.error.message,
          codice_catastale: action.payload.codiceCatastale
        });
      // download / delete profile
      case getType(upsertUserDataProcessing.success):
        return mp.track(action.type, action.payload);
      // wallet
      case getType(updatePaymentStatus.success):
        return mp.track(action.type, {
          pagoPA: action.payload.paymentMethod?.pagoPA
        });
      //
      // Actions (without properties)
      //
      // authentication
      case getType(loginFailure):
        return mp.track(action.type, {
          idp: action.payload.idp,
          reason: action.payload.error.message
        });
      case getType(loginSuccess):
        return mp.track(action.type, {
          idp: action.payload.idp
        });
      case getType(analyticsAuthenticationStarted):
      case getType(analyticsAuthenticationCompleted):
      case getType(sessionInformationLoadSuccess):
      case getType(sessionExpired):
      case getType(sessionInvalid):
      case getType(logoutSuccess):
      // identification
      // identificationSuccess is handled separately
      // because it has a payload.
      case getType(identificationRequest):
      case getType(identificationStart):
      case getType(identificationCancel):
      case getType(identificationFailure):
      case getType(identificationPinReset):
      case getType(identificationForceLogout):
      // profile
      case getType(profileUpsert.success):
      case getType(profileLoadRequest):
      // messages
      case getType(searchMessagesEnabled):
      // wallet
      case getType(addWalletCreditCardInit):
      case getType(addWalletCreditCardRequest):
      case getType(addWalletNewCreditCardSuccess):
      case getType(deleteAllPaymentMethodsByFunction.request):
      case getType(deleteAllPaymentMethodsByFunction.success):
      case getType(deleteWalletRequest):
      case getType(deleteWalletSuccess):
      case getType(setFavouriteWalletRequest):
      case getType(setFavouriteWalletSuccess):
      case getType(fetchTransactionsRequest):
      case getType(refreshPMTokenWhileAddCreditCard.request):
      case getType(refreshPMTokenWhileAddCreditCard.success):
      case getType(updatePaymentStatus.request):
      // payment
      case getType(abortRunningPayment):
      case getType(paymentInitializeState):
      case getType(paymentAttiva.success):
      case getType(paymentIdPolling.request):
      case getType(paymentIdPolling.success):
      case getType(paymentCheck.request):
      case getType(paymentCheck.success):
      case getType(paymentExecuteStart.request):
      case getType(paymentExecuteStart.success):
      case getType(paymentUpdateWalletPsp.request):
      case getType(paymentUpdateWalletPsp.success):
      case getType(paymentDeletePayment.request):
      case getType(paymentDeletePayment.success):
      //  profile First time Login
      case getType(profileFirstLogin):
      // other
      case getType(loadAvailableBonuses.success):
      case getType(loadAvailableBonuses.request):
        return mp.track(action.type);

      case getType(deleteUserDataProcessing.request):
        return mp.track(action.type, { choice: action.payload });
      case getType(removeAccountMotivation):
      case getType(deleteUserDataProcessing.success):
        return mp.track(action.type, action.payload);
      case getType(deleteUserDataProcessing.failure):
        return mp.track(action.type, {
          choice: action.payload.choice,
          reason: action.payload.error.message
        });
      // identification: identificationSuccess
      case getType(identificationSuccess):
        return mp.track(
          action.type,
          buildEventProperties("UX", "confirm", {
            identification_method: action.payload.isBiometric ? "bio" : "pin"
          })
        );
    }
  };

/*
 * The middleware acts as a general hook in order to track any meaningful action
 */
export const actionTracking =
  (middleware: MiddlewareAPI) =>
  (next: Dispatch) =>
  (action: Action): Action => {
    if (mixpanel !== undefined) {
      // Call mixpanel tracking only after we have
      // initialized mixpanel with the API token

      // Be aware that, at this point, tracking is called before
      // the action has been dispatched to the redux store
      void trackAction(mixpanel)(action);
      void trackBPayAction(mixpanel)(action);
      void trackCoBadgeAction(mixpanel)(action);
      void trackCgnAction(mixpanel)(action);
      void trackContentAction(mixpanel)(action);
      void trackServicesAction(mixpanel)(action);
      void trackEuCovidCertificateActions(mixpanel)(action);
      void trackPaypalOnboarding(mixpanel)(action);
      void trackZendesk(mixpanel)(action);
      void trackCdc(mixpanel)(action);

      const fciEnvironment = fciEnvironmentSelector(middleware.getState());
      void trackFciAction(mixpanel, fciEnvironment)(action);
    }
    // This dispatches the action towards the redux store
    const result = next(action);
    if (mixpanel !== undefined) {
      // Call mixpanel tracking only after we have
      // initialized mixpanel with the API token

      // Be aware that, at this point, tracking is called after
      // the action has been dispatched to the redux store
      trackMessagesActionsPostDispatch(action, middleware.getState());
    }
    return result;
  };