platform-helpers/trace-summary.js

'use strict';

const rootRequire = require('rpcm-root-require');
const Constants = rootRequire('/platform-helpers/constants');
const ConnectorAction = rootRequire('/platform-helpers/connector-action');

/**
 * A summary of an entire Trace record, up to the present.
 * @hideconstructor
 */
class TraceSummary {
  /**
   * @param {array} traceEventArray the output of TraceHelper.generateTraceEventArray
   * @param {object} cdhProfileData the CDH profile itself, to pull action info
   */
  constructor (traceEventArray, cdhProfileData) {
    /**
     * An object, with numeric keys, starting at 1 (events are numbered in the order delivered by Trace).  Each value is an instance of [TraceEvent]{@link module:TraceEvent}
     *
     * @type {array}
     */
    this.events = {};

    const summaryStringParts = [];

    /**
     * All the connector actions (including EventStream and AudienceStream, triggered, non-triggered, executed, delayed, queued).  Each element is an instance of [ConnectorAction]{@link module:ConnectorAction}
     *
     * @type {array}
     */
    this.allConnectorActions = [];

    // handle connector actions
    traceEventArray.forEach((entry, index) => {
      const eventNumber = index + 1;
      this.events[eventNumber] = entry;
      Constants.EVENT_ACTION_TYPES.forEach((type) => {
        if (Array.isArray(entry[type])) {
          const actionsArray = this.generateConnectorActionEntry(type, entry[type], cdhProfileData, eventNumber);
          actionsArray.forEach((action, index) => {
            this.allConnectorActions.push(action);
          });
          const actionSummaryString = this.pushActionArrayToList(actionsArray, []);
          summaryStringParts.push(`  ${type}: \n    ${JSON.stringify(actionSummaryString, null, 2)}`);
        }
      });
    });

    /**
     * The initial AudienceStream Visitor Profile, upon Trace start (before the first event), an instance of [VisitorProfile]{@link module:traceHelper~VisitorProfile}
     *
     * For this to exist, the Trace needs to have actually received at least one event, the CDH profile needs have AudienceStream active, and there can't have been a Visitor ID collision within the AS Region.
     *
     * @type {object}
     */
    this.firstProfile = traceEventArray && traceEventArray[0] && traceEventArray[0].visitor_profile_before;

    /**
     * The final AudienceStream Visitor Profile found in the trace (after the last Trace event), an instance of [VisitorProfile]{@link module:traceHelper~VisitorProfile}
     *
     * For this to exist, the Trace needs to have actually received at least one event, the CDH profile needs have AudienceStream active, and there can't have been a Visitor ID collision within the AS Region.
     *
     * @type {object}
     */
    this.lastProfile = (traceEventArray && traceEventArray[traceEventArray.length - 1] && traceEventArray[traceEventArray.length - 1].visitor_profile_after) || {};

    /**
     * All the EventStream connector actions (including triggered, non-triggered, executed, etc.). Each element is an instance of [ConnectorAction]{@link module:ConnectorAction}
     *
     * @type {array}
     */
    this.eventStreamActions = this.allConnectorActions.filter((action) => {
      return action.connectorType === 'eventStream';
    });

    /**
     * All the AudienceStream connector actions (including triggered, non-triggered, executed, etc.). Each element is an instance of [ConnectorAction]{@link module:ConnectorAction}
     *
     * @type {array}
     */
    this.audienceStreamActions = this.allConnectorActions.filter((action) => {
      return action.connectorType === 'audienceStream';
    });

    const keys = Object.keys(this.events);

    /**
     * A long string designed to give an overview of what happened in the Trace, event-by-event.
     *
     * Intended to be output as Markdown as part of the report to provide an overview and sanity check.
     *
     * @type {string}
     * @example
     * // this looks better rendered as Markdown (which is how it's expected to be viewed)
     * ...
     * ----
     *
     * ## Event 4 - button\_click\_increment
     *
     * Event VID      : 017329504780001c1f002e96b46303079005507101274
     *
     * Event has spec : no
     *
     * URL            : https://solutions.tealium.net/hosted/webdriver-testing/standard-integration-test.html
     * Querystring    : -
     *
     * Profile VID    : 017329504780001c1f002e96b46303079005507101274
     *
     * Visit Count    : 1
     * Event Count    : 5
     *
     *
     * ### Badges
     *
     * After
     *  - Live On Site
     *
     *
     * ### Connector Activity
     *
     * Executed
     *  - EventStream Success: 07b670a4-dc55-41cb-ee8c-1e1ec619e6aa - Webhooks - Postman Echo Send Event (webhook/send\_events) *
     *
     *
     * ----
     * ...
     */
    this.summaryString = keys.map((key, index) => {
      const event = this.events[key];
      return this.getPrettyEventSummaryString(event, index);
    }).join('\n');
  }

  /**
 * Deduplicates and adds additional info to arrays of actions from the Trace
 *
 * @private
 * @param {string} actionType the type of actions contained in the traceActionArray (executed_actions, triggered_actions, etc.)
 * @param {array} traceActionArray an array of actions from a trace summary event
 * @param {object} cdhProfileData the CDH profile JSON itself
 * @param {number} eventNumber the current event number
 *
 * @returns {array} a deduplicated and enhanced array of actions, based on the traceActionArray
 */
  generateConnectorActionEntry (actionType, traceActionArray, cdhProfileData, eventNumber) {
    if (Array.isArray(traceActionArray)) {
      const actions = [];
      if (!traceActionArray) return actions;
      traceActionArray.forEach((action, index) => {
        action = new ConnectorAction(action, actionType, cdhProfileData, eventNumber, index);
        actions.push(action);
      });
      return actions;
    }
    return [];
  }

  pushListToSummary (listName, listArray, summaryArray) {
    const joiner = '\n - ';
    if (Array.isArray(listArray) && listArray.length > 0) {
      const escapedArray = listArray.map((entry) => {
        return this.escapeForMarkdown(entry);
      });
      summaryArray.push(`${listName}${joiner}${escapedArray.sort().join(joiner)}\n`);
    }
  }

  pushActionsToSummary (actionGroupTitle, actionArray, summaryArray) {
    // const joiner = '    '
    if (Array.isArray(actionArray) && actionArray.length > 0) {
      summaryArray.push(`${actionGroupTitle}`);
      actionArray.forEach((action) => {
        let successString = '';
        if (typeof action.is_success === 'boolean') successString = action.is_success ? 'Success' : 'Failure';
        let connectorType = 'UNKNOWN!';
        if (action.connectorType === 'audienceStream') connectorType = 'AudienceStream';
        if (action.connectorType === 'eventStream') connectorType = 'EventStream';
        summaryArray.push(` - ${connectorType} ${successString}: ${action.action} - ${this.escapeForMarkdown(action.connectorInfoFromProfile.name)} ${this.escapeForMarkdown(action.actionInfoFromProfile.name)} (${this.escapeForMarkdown(action.connectorInfoFromProfile.type)}/${this.escapeForMarkdown(action.actionInfoFromProfile.type)})`);
      });
      summaryArray.push('\n');
    }
  }

  escapeForMarkdown (str) {
    if (!str || typeof str.replace !== 'function') return str;
    return str.replace(/_/g, '\\_').replace(/\[/g, '\\[').replace(/\]/g, '\\]');
  }

  getPrettyEventSummaryString (entry, index) {
    const summaryStringParts = [];

    const hasSpec = !!entry.event_spec_exists;
    const validSpec = entry.event_spec_exists && entry.event_spec_is_valid;
    summaryStringParts.push(`----\n\n## Event ${index + 1} - ${this.escapeForMarkdown(entry.trace_event_type)}\n`);
    // summaryStringParts.push(`New Visitor: ${entry.is_new_visitor}\n`)
    summaryStringParts.push(`Event VID      : ${this.escapeForMarkdown(entry.visitor_id) || '-'}\n`);
    // summaryStringParts.push(`tealium_event  : ${escapeForMarkdown(entry.tealium_event) || '-'}\n`)
    summaryStringParts.push(`Event has spec : ${hasSpec ? 'yes' : 'no'}`);
    if (hasSpec) { summaryStringParts.push(`Event is valid : ${validSpec ? 'yes' : 'no'}`); }
    if (hasSpec && !validSpec) { summaryStringParts.push(`Missing vars  : ${entry.event_spec_missing_attributes.join(' , ')}`); }

    summaryStringParts.push(`\nURL            : ${this.escapeForMarkdown(entry.last_event_url) || '-'}`);
    summaryStringParts.push(`Querystring    : ${this.escapeForMarkdown(entry.last_event_querystring) || '-'}`);

    summaryStringParts.push(`\nProfile VID    : ${this.escapeForMarkdown(entry.visitor_profile_after._id) || '-'}\n`);

    summaryStringParts.push(`Visit Count    : ${entry.lifetime_visit_count || '-'}`);
    summaryStringParts.push(`Event Count    : ${entry.lifetime_event_count || '-'}\n`);

    if (entry.badges_before || entry.badges_removed || entry.badges_added || entry.badges_after) {
      summaryStringParts.push('\n### Badges\n');
      // this.pushListToSummary('Before', entry.badges_before_pretty, summaryStringParts)
      this.pushListToSummary('Removed', entry.badges_removed_pretty, summaryStringParts);
      this.pushListToSummary('Added', entry.badges_added_pretty, summaryStringParts);
      this.pushListToSummary('After', entry.badges_after_pretty, summaryStringParts);
    }

    if (entry.audiences_before || entry.audiences_left || entry.audiences_joined || entry.audiences_after) {
      summaryStringParts.push('\n### Audiences\n');
      // this.pushListToSummary('Before', entry.audiences_before_pretty, summaryStringParts)
      this.pushListToSummary('Left', entry.audiences_left_pretty, summaryStringParts);
      this.pushListToSummary('Joined', entry.audiences_joined_pretty, summaryStringParts);
      this.pushListToSummary('After', entry.audiences_after_pretty, summaryStringParts);
    }

    if (entry.triggered_actions || entry.queued_delayed_actions || entry.queued_actions || entry.executed_actions || entry.not_triggered_actions) {
      summaryStringParts.push('\n### Connector Activity\n');
      this.pushActionsToSummary('Suppressed Actions', entry.not_triggered_actions, summaryStringParts);
      this.pushActionsToSummary('Triggered', entry.triggered_actions, summaryStringParts);
      this.pushActionsToSummary('Queued (Batched)', entry.queued_actions, summaryStringParts);
      this.pushActionsToSummary('Delayed', entry.queued_delayed_actions, summaryStringParts);
      this.pushActionsToSummary('Executed', entry.executed_actions, summaryStringParts);
    }

    return summaryStringParts.join('\n');
  }

  pushActionArrayToList (actionArray, summaryArray) {
    summaryArray = summaryArray || [];
    const summaryList = [];
    actionArray.forEach((action) => {
      summaryList.push(`  ${this.escapeForMarkdown(action.action)} - ${this.escapeForMarkdown(action.connectorInfoFromProfile.name)} ${this.escapeForMarkdown(action.actionInfoFromProfile.name)} - ${this.escapeForMarkdown(action.actionInfoFromProfile.name)}`);
    });
    summaryArray.push(`${summaryList.join('\n')}`);
    return summaryArray.join('');
  }
}

module.exports = TraceSummary;