sdk.js

import utils from './utils';
/* eslint-disable no-underscore-dangle */
/**
 * Pinpoll Sdk
 * Currently used for
 * - adding audiences to a given visitor
 * - removing audiences from a given visitor
 * - setting custom attributes to a given visitor
 * - querying audiences of given visitor
 */

/**
 * Definition of Audience Object
 * @typedef {Object} Audience - Audience Object
 * @property {number} id - Id of the audience.
 * @property {boolean} isPinpoll - Indicator if this audience is a pinpoll audience or not.
 * @property {number} parentId - Parent Id of the audience.
 * @property {number} realDepth - Depth of the audience (needed for audience tree).
 * @property {Object} text - Translations of the audience as object.
 * @property {string} text.en - English translation of the audience.
 * @property {string} text.de - German translation of the audience.
 * @property {number} typeId - Type of the audience (1 = interest, 2 = property).
 */

/**
 * Private methods defined as symbols
 * @private
 */
const _fetch = Symbol('submit');
const _parseOptions = Symbol('parseOptions');

class Sdk {
  /**
   * Constructor of Pinpoll Sdk
   */
  constructor() {
    this.attributes = {};
    this.audiences = {
      addedAudiences: [],
      removedAudiences: [],
    };
    this.visitorAudiences = {};
  }

  /**
   * Pings the sdk which logs 'pong' in the console if reachable
   * @static
   * @returns {void}
   */
  static ping() {
    console.info('PinpollSdk: pong!');
  }

  /**
   * Set attributes object
   * @param {Object<string,string>} attributes - Attributes object in the form key:value
   * @returns {void}
   */
  setAttributes(attributes) {
    this.attributes = attributes;
  }

  /**
   * Add audience to addedAudiences array
   * @param {object} audience - Audience Object
   * @param {number} audience.id - Id of the audience (must be a valid id!)
   * @param {number} [audience.affinityScore=1] - Optional Affinity Score (Range 1-10, where 10 is the highest)
   * @returns {void}
   */
  addAudience({ id, affinityScore = 1 }) {
    this.audiences.addedAudiences.push({ id, affinityScore });
  }

  /**
   * Add audiences to addedAudiences array
   * @param {Array<Object>} audiences - Audiences array with audience objects with the properties id and an optional affinityScore
   * @returns {void}
   */
  addAudiences(audiences) {
    this.audiences.addedAudiences = [...this.audiences.addedAudiences, ...audiences];
  }

  /**
   * Add audience too removedAudiences array
   * @param {object} audience - Audience Object
   * @param {number} audience.id - Id of the audience (must be a valid id!)
   * @param {number} [audience.affinityScore=null] - Optional Affinity Score (This will only remove the audience with exact match of affinity score)
   * @returns {void}
   */
  removeAudience({ id, affinityScore = null }) {
    this.audiences.removedAudiences.push({ id, affinityScore });
  }

  /**
   * Add audiences to removedAudiences array
   * @param {Array<Object>} audiences - Audiences array with audience objects with the properties id and an optional affinityScore
   * @returns {void}
   */
  removeAudiences(audiences) {
    this.audiences.removedAudiences = [...this.audiences.removedAudiences, ...audiences];
  }

  /**
   * Send data to endpoint
   * @returns {void}
   */
  async sendData() {
    if (this.attributes) {
      const data = {
        attributes: this.attributes,
        audiences: this.audiences,
      };
      await Sdk[_fetch]({ endpoint: '/sdk/visitors', data });
    }
  }

  /**
   * Get audiences of current visitor
   * @param {Object} options - Options how to display audiences
   * @param {boolean} options.tree=true - Display audiences as tree structure
   * @param {string} options.removeRootLevel='none' - Remove root level audiences ['interests', 'properties', 'all', 'none']
   * @param {boolean|string} options.onlyLabels=false - Show only labels of one specific language ['en', 'de', false]
   * @returns {Object<string, Audience[]>} Audiences of the current visitor divided in interests and properties
   */
  async getAudiences(options = { tree: true, removeRootLevel: 'none', onlyLabels: false }) {
    try {
      const jsonResponse = await Sdk[_fetch]({ method: 'get', endpoint: '/sdk/getAudiences' }).then((response) => response.json());
      if (jsonResponse.status === 'success') {
        const audiences = jsonResponse.data;
        // parse options
        this.visitorAudiences = Sdk[_parseOptions](audiences, options);
      } else {
        console.warn(jsonResponse.data);
      }
    } catch (err) {
      console.error('Failed to fetch getAudiences!');
      console.error(err);
    }
    return this.visitorAudiences;
  }

  /**
   * Track a new event with a given event name
   * @static
   * @param {string} eventKey - Name of the event to track
   * @param {number} [eventValue] - Value of the event to track
   * @param {string} [eventLabel] - Label of the event to track
   * @returns {void}
   */
  static async trackEvent(eventKey, eventValue, eventLabel) {
    try {
      if (!eventKey) {
        console.warn('Please provide an event name in order to make an event tracking request!');
      } else {
        const postParams = {
          eventKey,
          location: document.location.href,
          title: document.title,
        };
        if (eventValue) postParams.eventValue = eventValue;
        if (eventLabel) postParams.eventLabel = eventLabel;
        await Sdk[_fetch]({ method: 'post', endpoint: '/sdk/event', data: postParams });
      }
    } catch (err) {
      console.error('Failed to fetch trackEvent!');
      console.error(err);
    }
  }

  /**
   * Parse options for transforming audiences
   * @static
   * @private
   * @param {Object} audiences - Given audiences Object
   * @param {Object} options - Options how to display audiences
   * @param {boolean} options.tree - Display audiences as tree structure
   * @param {string} options.removeRootLevel='properties' - Remove root level ['interests', 'properties', 'all', 'none']
   * @param {boolean|string} options.onlyLabels=false - Show only labels of one specific language ['en', 'de', false]
   * @returns {Object} transformedAudiences - transformed audiences with applied options
   */
  static [_parseOptions](audiences, { tree, removeRootLevel, onlyLabels }) {
    const transformedAudiences = audiences;
    // remove root level
    if (removeRootLevel !== 'none') {
      if (removeRootLevel === 'interests' || removeRootLevel === 'all') {
        transformedAudiences.interests = utils.removeRootLevel(audiences.interests);
      }
      if (removeRootLevel === 'properties' || removeRootLevel === 'all') {
        transformedAudiences.properties = utils.removeRootLevel(audiences.properties);
      }
    }
    // create tree
    if (tree) {
      transformedAudiences.interests = utils.transformToTree(transformedAudiences.interests, removeRootLevel === 'interests');
      transformedAudiences.properties = utils.transformToTree(transformedAudiences.properties, removeRootLevel === 'properties');
    }
    // show only labels
    if (onlyLabels) {
      transformedAudiences.interests = utils.onlyLabels(transformedAudiences.interests, onlyLabels, tree);
      transformedAudiences.properties = utils.onlyLabels(transformedAudiences.properties, onlyLabels, tree);
    }
    return transformedAudiences;
  }

  /**
   * Fetch data from analytics endpoint
   * @static
   * @private
   * @param {Object} params parameters
   * @param {String} [params.method='post'] - method of call
   * @param {String} [params.endpoint=''] - endpoint to send to
   * @param {Object} params.data - data to send
   * @returns {Promise}
   */
  static async [_fetch]({ method = 'post', endpoint = '', data }) {
    // send request
    const dmpEndpoint = process.env.MIX_DMP_ENDPOINT || 'https://api.dmp.pinpoll.com';
    try {
      return fetch(dmpEndpoint + endpoint, {
        method,
        headers: {
          Accept: 'application/json',
          'Content-Type': 'application/json',
          'PP-Visitor': Sdk.getVisitor(), // TODO 11084 use storage module from global.js instead
        },
        referrerPolicy: 'no-referrer-when-downgrade',
        body: JSON.stringify(data),
      });
    } catch (err) {
      console.error(err);
    }
    return null;
  }

  /**
   * Code duplicate of global js storage module getter
   * @returns {string} encrypted visitor
   */
  static getVisitor() {
    try {
      if (!localStorage.PP_visitor) {
        return null;
      }
      const visitor = JSON.parse(localStorage.PP_visitor);
      if (visitor.expires && visitor.expires < Date.now()) {
        delete localStorage.PP_visitor;
        return null;
      }
      // refresh expiry
      visitor.expires = new Date().setDate(new Date().getDate() + 30); // 30 days from now
      localStorage.PP_visitor = JSON.stringify(visitor);
      return visitor.value;
    } catch (e) {
      // eslint-disable-next-line no-console
      console.warn(e);
      return null;
    }
  }
}

// set class to 'PinpollSdk'
window.PinpollSdk = Sdk;

export default Sdk;