All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vwo.VWO Maven / Gradle / Ivy

There is a newer version: 1.66.0
Show newest version
package com.vwo;

import com.vwo.bucketing.Bucketer;
import com.vwo.bucketing.BucketingService;
import com.vwo.config.ConfigParseException;
import com.vwo.config.FileSettingUtils;
import com.vwo.config.ProjectConfig;
import com.vwo.config.VWOConfig;
import com.vwo.enums.GoalEnum;
import com.vwo.enums.LoggerMessagesEnum;
import com.vwo.enums.VWOEnums;
import com.vwo.event.DispatchEvent;
import com.vwo.event.EventDispatcher;
import com.vwo.event.EventFactory;
import com.vwo.event.EventHandler;
import com.vwo.logger.LoggerManager;
import com.vwo.logger.VWOLogger;
import com.vwo.models.Campaign;
import com.vwo.models.Goal;
import com.vwo.models.Variation;
import com.vwo.userprofile.UserProfileService;

import java.io.Closeable;
import javafx.util.Pair;

/**
 * The VWO client class needs to be instantiated as an instance that exposes various API methods like activate, getVariation and track.
 */
public class VWO implements AutoCloseable {

  final ProjectConfig projectConfig;
  final UserProfileService userProfileService;
  final EventHandler eventHandler;
  final BucketingService bucketingService;
  final VWOLogger customLogger;
  private boolean developmentMode;

  public static final class Enums extends VWOEnums {}

  private static final LoggerManager LOGGER = LoggerManager.getLogger(VWO.class);

  private VWO(ProjectConfig projectConfig,
              UserProfileService userProfileService,
              EventHandler eventHandler,
              BucketingService bucketingService,
              VWOLogger customLogger,
              boolean developmentMode) {

    this.projectConfig = projectConfig;
    this.userProfileService = userProfileService;
    this.eventHandler = eventHandler;
    this.bucketingService = bucketingService;
    this.developmentMode = developmentMode;
    this.customLogger = customLogger;
  }

  /**
   * Fetch the account settings.
   *
   * @param accountID VWO application account-id.
   * @param sdkKey    Unique sdk-key provided to you inside VWO Application under the Apps section of server-side A/B Testing
   * @return JSON representation String representing the current state of campaign settings
   */
  public static String getSettingsFile(String accountID, String sdkKey) {
    return FileSettingUtils.getSettingsFile(accountID, sdkKey);
  }

  public ProjectConfig getProjectConfig() {
    return this.projectConfig;
  }

  public UserProfileService getUserProfileService() {
    return this.userProfileService;
  }

  public EventHandler getEventHandler() {
    return this.eventHandler;
  }

  public BucketingService getBucketingService() {
    return this.bucketingService;
  }

  public VWOLogger getCustomLogger() {
    return this.customLogger;
  }

  public boolean isDevelopmentMode() {
    return this.developmentMode;
  }

  public void setDevelopmentMode(boolean developmentMode) {
    this.developmentMode = developmentMode;
  }

  /**
   * Validates the parameters passed.
   * checks whether the user qualifies to become a part of the campaign
   * assigns a deterministic variation to the qualified user
   * sends an impression event to the VWO server for generating reports
   *
   * @param campaignTestKey - Campaign name
   * @param userId          - User ID
   * @return String name of the variation in which the user is bucketed, or null if the user doesn't qualify to become a part of the campaign.
   */
  public String activate(String campaignTestKey, String userId) {
    LOGGER.info(LoggerMessagesEnum.INFO_MESSAGES.INITIATING_ACTIVATE.value(new Pair<>("userId", userId), new Pair<>("campaignTestKey", campaignTestKey)));

    try {
      if (campaignTestKey == null || campaignTestKey.isEmpty()) {
        LOGGER.error(LoggerMessagesEnum.ERROR_MESSAGES.MISSING_CAMPAIGN_KEY.value());
        return null;
      }
      if (userId == null || userId.isEmpty()) {
        LOGGER.error(LoggerMessagesEnum.ERROR_MESSAGES.MISSING_USER_ID.value());
        return null;
      }
      if (this.projectConfig == null) {
        LOGGER.error(LoggerMessagesEnum.ERROR_MESSAGES.MISSING_PROJECT_CONFIG.value(
                new Pair<>("campaignTestKey", campaignTestKey),
                new Pair<>("userId", userId)
        ));
        return null;
      }

      Campaign campaign = this.projectConfig.getCampaignTestKey(campaignTestKey);

      if (campaign == null) {
        LOGGER.error(LoggerMessagesEnum.ERROR_MESSAGES.CAMPAIGN_NOT_FOUND.value());
        return null;
      }

      return this.activateCampaign(campaign, userId);
    } catch (Exception e) {
      LOGGER.error(LoggerMessagesEnum.ERROR_MESSAGES.GENERIC_ERROR.value(), e);
      return null;
    }
  }

  private String activateCampaign(Campaign campaign, String userId) {
    String variation = this.getCampaignVariation(campaign, userId);

    if (variation != null) {
      LOGGER.debug(LoggerMessagesEnum.DEBUG_MESSAGES.ACTIVATING_CAMPAIGN.value(new Pair<>("userId", userId), new Pair<>("variation", variation)));

      // Send Impression Call for Stats
      this.sendImpressionCall(this.projectConfig, campaign, userId, CampaignUtils.getVariationObjectFromCampaign(campaign, variation));
    } else {
      LOGGER.info(LoggerMessagesEnum.INFO_MESSAGES.NO_VARIATION_ALLOCATED.value(new Pair<>("userId", userId), new Pair<>("campaignTestKey", campaign.getKey())));
    }
    return variation;
  }

  /**
   * getVariation API activates a server-side A/B test for the specified user for a particular running campaign.
   * validates the parameters passed
   * checks whether the user qualifies to become a part of the campaign
   * assigns a deterministic variation to the qualified user
   * does not send an impression event to the VWO server
   *
   * @param campaignTestKey key provided at the time of server-side campaign creation
   * @param userId          unique id associated with the user for identification
   * @return                Variation name
   */
  public String getVariation(String campaignTestKey, String userId) {
    LOGGER.info(LoggerMessagesEnum.INFO_MESSAGES.INITIATING_GET_VARIATION.value(new Pair<>("userId", userId), new Pair<>("campaignTestKey", campaignTestKey)));

    try {
      if (campaignTestKey == null || campaignTestKey.isEmpty()) {
        LOGGER.error(LoggerMessagesEnum.ERROR_MESSAGES.MISSING_CAMPAIGN_KEY.value());
        return null;
      }
      if (userId == null || userId.isEmpty()) {
        LOGGER.error(LoggerMessagesEnum.ERROR_MESSAGES.MISSING_USER_ID.value());
        return null;
      }
      if (this.projectConfig == null) {
        LOGGER.error(LoggerMessagesEnum.ERROR_MESSAGES.MISSING_PROJECT_CONFIG.value(
                new Pair<>("campaignTestKey", campaignTestKey),
                new Pair<>("userId", userId)
        ));
        return null;
      }
      Campaign campaign = this.projectConfig.getCampaignTestKey(campaignTestKey);

      if (campaign == null) {
        LOGGER.error(LoggerMessagesEnum.ERROR_MESSAGES.CAMPAIGN_NOT_FOUND.value());
        return null;
      }

      return this.getCampaignVariation(campaign, userId);
    } catch (Exception e) {
      LOGGER.error(LoggerMessagesEnum.ERROR_MESSAGES.GENERIC_ERROR.value(), e);
      return null;
    }
  }

  private String getCampaignVariation(Campaign campaign, String userId) {
    Variation variation = this.bucketingService.getVariation(campaign, userId);
    return variation != null ? variation.getName() : null;
  }

  /**
   * Tracks a conversion event for a particular user for a running server-side campaign.
   * validates the parameters passed
   * assigns the same variation to the same qualified user
   * sends an impression event to the VWO server for generating reports
   *
   * @param campaignTestKey key provided at the time of server-side campaign creation.
   * @param userId          unique id associated with the user for identification
   * @param goalIdentifier  key provided at the time of creating the goal in the server-side
   * @param revenueValue    revenue generated on triggering the goal
   * @return                Boolean value whether user is tracked or not.
   */
  public boolean track(String campaignTestKey, String userId, String goalIdentifier, Object revenueValue) {
    return this.trackGoal(campaignTestKey, userId, goalIdentifier, revenueValue);
  }

  public boolean track(String campaignTestKey, String userId, String goalIdentifier) {
    return this.trackGoal(campaignTestKey, userId, goalIdentifier, null);
  }

  private boolean trackGoal(String campaignTestKey, String userId, String goalIdentifier, Object revenueValue) {
    try {
      if (!this.isTrackParamsValid(campaignTestKey, userId, goalIdentifier)) {
        return false;
      }

      Campaign campaign = this.projectConfig.getCampaignTestKey(campaignTestKey);

      if (campaign == null) {
        LOGGER.error(LoggerMessagesEnum.ERROR_MESSAGES.CAMPAIGN_NOT_FOUND.value());
        return false;
      }

      String variation = this.getCampaignVariation(campaign, userId);

      if (variation != null) {
        Goal goal = this.getGoalId(campaign, goalIdentifier);

        if (goal == null) {
          LOGGER.error(LoggerMessagesEnum.ERROR_MESSAGES.TRACK_API_GOAL_NOT_FOUND.value(
                  new Pair<>("goalIdentifier", goalIdentifier),
                  new Pair<>("userId", userId),
                  new Pair<>("campaignTestKey", campaign.getKey())
          ));
          return false;
        } else if (goal.getType().equals(GoalEnum.GOAL_TYPES.REVENUE.value()) && revenueValue == null) {
          LOGGER.error(LoggerMessagesEnum.ERROR_MESSAGES.MISSING_GOAL_REVENUE.value(
                  new Pair<>("goalIdentifier", goalIdentifier),
                  new Pair<>("campaignTestKey", campaign.getKey()),
                  new Pair<>("userId", userId)
          ));
          return false;
        }

        this.sendConversionCall(this.projectConfig, campaign, userId, goal, CampaignUtils.getVariationObjectFromCampaign(campaign, variation), revenueValue);

        return true;
      } else {
        LOGGER.info(LoggerMessagesEnum.INFO_MESSAGES.TRACK_API_VARIATION_NOT_FOUND.value(new Pair<>("userId", userId), new Pair<>("campaignTestKey", campaign.getKey())));
      }

      return false;
    } catch (Exception e) {
      LOGGER.error(LoggerMessagesEnum.ERROR_MESSAGES.GENERIC_ERROR.value(), e);
      return false;
    }
  }

  private boolean isTrackParamsValid(String campaignTestKey, String userId, String goalIdentifier) {
    if (campaignTestKey == null || campaignTestKey.isEmpty()) {
      LOGGER.error(LoggerMessagesEnum.ERROR_MESSAGES.MISSING_CAMPAIGN_KEY.value());
      return false;
    }
    if (userId == null || userId.isEmpty()) {
      LOGGER.error(LoggerMessagesEnum.ERROR_MESSAGES.MISSING_USER_ID.value());
      return false;
    }
    if (goalIdentifier == null || goalIdentifier.isEmpty()) {
      LOGGER.error(LoggerMessagesEnum.ERROR_MESSAGES.MISSING_GOAL_IDENTIFIER.value());
      return false;
    }
    if (this.projectConfig == null) {
      LOGGER.error(LoggerMessagesEnum.ERROR_MESSAGES.MISSING_PROJECT_CONFIG.value(
              new Pair<>("campaignTestKey", campaignTestKey),
              new Pair<>("userId", userId)
      ));
      return false;
    }

    return true;
  }

  private Goal getGoalId(Campaign campaign, String goalIdentifier) {
    for (Goal singleGoal : campaign.getGoals()) {
      if (goalIdentifier.equalsIgnoreCase(singleGoal.getIdentifier())) {
        return singleGoal;
      }
    }
    return null;
  }

  private void sendImpressionCall(ProjectConfig projectConfig, Campaign campaign, String userId, Variation variation) {
    DispatchEvent dispatchEvent = EventFactory.createImpressionLogEvent(projectConfig, campaign, userId, variation);
    try {
      if (!this.isDevelopmentMode()) {
        eventHandler.dispatchEvent(dispatchEvent);
      }
    } catch (Exception e) {
      LOGGER.error("Unexpected exception in event dispatcher");
    }
  }

  private void sendConversionCall(ProjectConfig projectConfig, Campaign campaign, String userId, Goal goal, Variation variation, Object revenueValue) {
    DispatchEvent dispatchEvent = EventFactory.createGoalLogEvent(projectConfig, campaign, userId, goal, variation, revenueValue);
    try {
      if (!this.isDevelopmentMode()) {
        eventHandler.dispatchEvent(dispatchEvent);
      }
    } catch (Exception e) {
      LOGGER.error(LoggerMessagesEnum.ERROR_MESSAGES.UNABLE_TO_DISPATCH_EVENT.value());
    }
  }

  /**
   * Creates builder instance.
   *
   * @param settingFile - Setting string
   * @return - Builder instance
   */
  public static Builder createInstance(String settingFile) {
    try {
      return new Builder().withSettingFile(settingFile);
    } catch (Exception e) {
      LOGGER.error(LoggerMessagesEnum.ERROR_MESSAGES.GENERIC_ERROR.value(), e.getStackTrace());
    }
    return null;
  }

  public static class Builder {

    private ProjectConfig projectConfig;
    private UserProfileService userProfileService;
    private EventHandler eventHandler;
    private BucketingService bucketingService;
    private String settingFile;
    private Bucketer bucketer;
    private VWOLogger customLogger;
    private boolean developmentMode;


    private Builder withSettingFile(String settingFile) {
      this.settingFile = settingFile;
      return this;
    }

    public Builder withUserProfileService(UserProfileService userProfileService) {
      this.userProfileService = userProfileService;
      return this;
    }

    public Builder withEventHandler(EventHandler eventHandler) {
      this.eventHandler = eventHandler;
      return this;
    }

    public Builder withDevelopmentMode(boolean developmentMode) {
      this.developmentMode = developmentMode;
      return this;
    }

    public Builder withCustomLogger(VWOLogger customLogger) {
      this.customLogger = customLogger;
      return this;
    }

    public VWO build() {
      // Init logger at start.
      LoggerManager.init(this.customLogger);

      if (this.projectConfig == null && this.settingFile != null && !this.settingFile.isEmpty()) {
        try {
          this.projectConfig = VWOConfig.Builder.getInstance(this.settingFile).build();
          this.projectConfig.processSettingsFile();
          LOGGER.debug(LoggerMessagesEnum.DEBUG_MESSAGES.SETTINGS_FILE_PROCESSED.value());
        } catch (ConfigParseException e) {
          LOGGER.error(LoggerMessagesEnum.ERROR_MESSAGES.GENERIC_ERROR.value(), e);
        }
      }

      if (this.eventHandler == null) {
        this.eventHandler = EventDispatcher.builder().build();
      }

      this.bucketer = new Bucketer();
      this.bucketingService = new BucketingService(bucketer, userProfileService);
      this.developmentMode = this.developmentMode || false;

      // process SettingsFile
      VWO vwoInstance = new VWO(this.projectConfig, this.userProfileService, this.eventHandler, this.bucketingService, this.customLogger, this.developmentMode);
      if (vwoInstance != null) {
        LOGGER.debug(LoggerMessagesEnum.DEBUG_MESSAGES.SDK_INITIALIZED.value());
      }
      return vwoInstance;
    }
  }

  /**
   * Closes this resource, relinquishing any underlying resources.
   * This method is invoked automatically on objects managed by the
   * {@code try}-with-resources statement.
   *
   * 

While this interface method is declared to throw {@code * Exception}, implementers are strongly encouraged to * declare concrete implementations of the {@code close} method to * throw more specific exceptions, or to throw no exception at all * if the close operation cannot fail. * *

Cases where the close operation may fail require careful * attention by implementers. It is strongly advised to relinquish * the underlying resources and to internally mark the * resource as closed, prior to throwing the exception. The {@code * close} method is unlikely to be invoked more than once and so * this ensures that the resources are released in a timely manner. * Furthermore it reduces problems that could arise when the resource * wraps, or is wrapped, by another resource. * *

Implementers of this interface are also strongly advised * to not have the {@code close} method throw {@link * InterruptedException}. * *

This exception interacts with a thread's interrupted status, * and runtime misbehavior is likely to occur if an {@code * InterruptedException} is {@linkplain Throwable#addSuppressed * suppressed}. * *

More generally, if it would cause problems for an * exception to be suppressed, the {@code AutoCloseable.close} * method should not throw it. * *

Note that unlike the {@link Closeable#close close} * method of {@link Closeable}, this {@code close} method * is not required to be idempotent. In other words, * calling this {@code close} method more than once may have some * visible side effect, unlike {@code Closeable.close} which is * required to have no effect if called more than once. * *

However, implementers of this interface are strongly encouraged * to make their {@code close} methods idempotent. * * @throws Exception if this resource cannot be closed */ @Override public void close() throws Exception { tryClose(this.eventHandler); tryClose(this.projectConfig); } public void tryClose(Object obj) { if (!(obj instanceof AutoCloseable)) { return; } try { ((AutoCloseable) obj).close(); } catch (Exception e) { LOGGER.warn(LoggerMessagesEnum.WARNING_MESSAGES.CLOSE_GENERIC_CONNECTION.value(), obj); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy