src/app/lib/analyticsUtils/index.js

Summary

Maintainability
C
1 day
Test Coverage
A
98%
import Cookie from 'js-cookie';
import { v4 as uuid } from 'uuid';
import pathOr from 'ramda/src/pathOr';
import path from 'ramda/src/path';
import Url from 'url-parse';
import onClient from '../utilities/onClient';
import isOperaProxy from '../utilities/isOperaProxy';
import {
  MEDIUM_CAMPAIGN_IDENTIFIER,
  XTOR_CAMPAIGN_IDENTIFIER,
  SUPPORTED_MEDIUM_CAMPAIGN_TYPES,
} from './analytics.const';
import getAmpDestination from './getAmpDestination';

export const getDestination = (platform, statsDestination) => {
  const destinationIDs = {
    DEFAULT: 596068,
    DEFAULT_TEST: 596068,
    NEWS_PS: 598285,
    NEWS_LANGUAGES_PS: 598291,
    NEWS_GNL: 598287,
    NEWS_LANGUAGES_GNL: 598289,
    NEWS_PS_TEST: 598286,
    NEWS_LANGUAGES_PS_TEST: 598292,
    NEWS_LANGUAGES_GNL_TEST: 598290,
    NEWS_GNL_TEST: 598288,
    WS_NEWS_LANGUAGES: 598342,
    WS_NEWS_LANGUAGES_TEST: 598343,
    PS_HOMEPAGE: 598273,
    PS_HOMEPAGE_TEST: 598274,
    BBC_ARCHIVE_PS: 605565,
    BBC_ARCHIVE_PS_TEST: 605566,
    NEWSROUND: 598293,
    NEWSROUND_TEST: 598294,
    SPORT_GNL: 598308,
    SPORT_GNL_TEST: 598309,
    SPORT_PS: 598310,
    SPORT_PS_TEST: 598311,
  };

  const geoVariants = {
    NEWS_PS: {
      PS: destinationIDs.NEWS_PS,
      GNL: destinationIDs.NEWS_GNL,
    },
    NEWS_PS_TEST: {
      PS: destinationIDs.NEWS_PS_TEST,
      GNL: destinationIDs.NEWS_GNL_TEST,
    },
    SPORT_PS: {
      PS: destinationIDs.SPORT_PS,
      GNL: destinationIDs.SPORT_GNL,
    },
    SPORT_PS_TEST: {
      PS: destinationIDs.SPORT_PS_TEST,
      GNL: destinationIDs.SPORT_GNL_TEST,
    },
    NEWS_LANGUAGES_PS: {
      PS: destinationIDs.NEWS_LANGUAGES_PS,
      GNL: destinationIDs.NEWS_LANGUAGES_GNL,
    },
    NEWS_LANGUAGES_PS_TEST: {
      PS: destinationIDs.NEWS_LANGUAGES_PS_TEST,
      GNL: destinationIDs.NEWS_LANGUAGES_GNL_TEST,
    },
  };

  if (platform === 'amp' && geoVariants[statsDestination]) {
    return getAmpDestination(geoVariants[statsDestination]);
  }

  return destinationIDs[statsDestination] || destinationIDs.NEWS_PS;
};

export const getAppType = platform => {
  switch (platform) {
    case 'amp':
      return 'amp';
    case 'app':
      return 'mobile-app';
    case 'canonical':
      return 'responsive';
    default:
      return 'responsive';
  }
};

export const isLocServeCookieSet = platform => {
  if (platform === 'amp') {
    return false;
  }

  if (onClient()) {
    return !!Cookie.get('loc_serve');
  }

  return null;
};

export const getScreenInfo = platform => {
  if (platform === 'amp') {
    return `\${screenWidth}x\${screenHeight}x\${screenColorDepth}`;
  }

  if (onClient()) {
    const { width, height, colorDepth, pixelDepth } = window.screen;
    const orderArray = [
      width || 0,
      height || 0,
      colorDepth || 0,
      pixelDepth || 0,
    ];

    return orderArray.join('x');
  }

  return null;
};

export const getBrowserViewPort = platform => {
  if (platform === 'amp') {
    return `\${availableScreenWidth}x\${availableScreenHeight}`;
  }

  if (onClient()) {
    const { innerWidth, innerHeight } = window;

    return [innerWidth || 0, innerHeight || 0].join('x');
  }

  return null;
};

export const getCurrentTime = platform => {
  if (platform === 'amp') {
    return `\${timestamp}`;
  }

  if (onClient()) {
    const now = new Date();
    const hours = now.getHours();
    const mins = now.getMinutes();
    const secs = now.getSeconds();

    return [hours, mins, secs].join('x');
  }

  return null;
};

export const getDeviceLanguage = platform => {
  if (platform === 'amp') {
    // Using browserlanguage since AMP doesn't have access to device language
    return `\${browserLanguage}`;
  }

  if (onClient() && navigator.language) {
    return navigator.language;
  }

  return null;
};

export const getHref = platform => {
  if (platform === 'amp') {
    return `\${sourceUrl}`;
  }

  if (onClient() && window.location.href) {
    const { href } = window.location;
    return href;
  }

  return null;
};

export const getReferrer = (platform, origin, previousPath) => {
  if (platform === 'amp') {
    /* On AMP, `\${documentReferrer}` is an amp analytics variable that resolves
       to a `document.referrer` equivalent as the window document is undefined on amp pages.
       https://github.com/ampproject/amphtml/blob/master/spec/amp-var-substitutions.md#document-referrer
    */
    return `\${documentReferrer}`;
  }

  if (onClient() && (document.referrer || previousPath)) {
    const referrer = previousPath
      ? `${origin}${previousPath}`
      : document.referrer;
    return referrer;
  }

  return null;
};

export const getAtUserId = () => {
  if (!onClient()) return null;

  // Users accessing the site on opera "extreme data saving mode" have the pages rendered by an intermediate service
  // Attempting to track these users is just tracking that proxy, causing all opera mini visitors to have the same id
  if (isOperaProxy()) return null;

  const cookieName = 'atuserid';
  let cookie = Cookie.get(cookieName);
  const expires = 397; // expires in 13 months

  if (cookie) {
    try {
      cookie = JSON.parse(cookie);
    } catch (error) {
      // eslint-disable-next-line no-console
      console.log(error);
      cookie = null;
    }
  }

  const val = path(['val'], cookie) || uuid();

  Cookie.set(cookieName, JSON.stringify({ val }), {
    expires,
    path: '/',
    secure: true,
  });

  return val;
};

export const sanitise = initialString =>
  initialString ? initialString.trim().replace(/\s/g, '%20') : null;

const isValidDateTime = dateTime => !isNaN(dateTime); // eslint-disable-line no-restricted-globals

const getISODate = unixTimestamp => {
  const date = new Date(unixTimestamp);
  return date.toISOString();
};

export const getPublishedDatetime = (attribute, data) => {
  const publishedDatetime = pathOr(null, ['metadata', attribute], data);

  return publishedDatetime && isValidDateTime(publishedDatetime)
    ? getISODate(publishedDatetime)
    : null;
};

export const getContentId = pathOr(null, [
  'metadata',
  'analyticsLabels',
  'contentId',
]);

export const getAtiUrl = (data = []) => {
  const cleanedValues = data
    .filter(({ value }) => value)
    .map(item => {
      const { value, disableEncoding } = item;
      const finalValue = disableEncoding ? value : encodeURIComponent(value);
      return { ...item, value: finalValue };
    });

  const parsedAtiValues = cleanedValues.map(({ key, value, wrap }) =>
    wrap ? `${key}=[${value}]` : `${key}=${value}`,
  );

  return parsedAtiValues.join('&');
};

export const getEventInfo = ({
  pageIdentifier = '',
  componentName = '',
  campaignID = '',
  variant = '', // not a service variant - used for A/B testing
  format = '',
  detailedPlacement = '',
  advertiserID = '',
  url = '',
} = {}) => {
  const generalPlacement = pageIdentifier;
  const creation = componentName;

  return `PUB-[${campaignID}]-[${creation}]-[${variant}]-[${format}]-[${generalPlacement}]-[${detailedPlacement}]-[${advertiserID}]-[${url}]`;
};

export const getThingAttributes = (attribute, articleData) => {
  const things = pathOr(null, ['metadata', 'tags', 'about'], articleData);

  if (things) {
    const attributes = [];

    things.forEach(thing => {
      if (thing[attribute]) {
        attributes.push(thing[attribute].trim().replace(/\s/g, '%20'));
      }
    });

    return attributes.join('~') || null;
  }

  return null;
};

export const getCampaignType = () => {
  if (!onClient()) return null;

  // Gets the query string parameters from the current url parsing them as an object
  const { query, hash } = new Url(window.location.href, true);

  // Check for the presence of the `?at_medium` QS
  const isMediumCampaign = Object.prototype.hasOwnProperty.call(
    query,
    MEDIUM_CAMPAIGN_IDENTIFIER,
  );

  // Checks for the presence of the `?xtor` WS or anchor e.g. `#xtor`
  const isXtorCampaign =
    Object.prototype.hasOwnProperty.call(query, XTOR_CAMPAIGN_IDENTIFIER) ||
    hash.includes(XTOR_CAMPAIGN_IDENTIFIER);

  if (isMediumCampaign) {
    const isSupportedMediumCampaignType = SUPPORTED_MEDIUM_CAMPAIGN_TYPES.some(
      type => query[MEDIUM_CAMPAIGN_IDENTIFIER].includes(type),
    );

    return isSupportedMediumCampaignType
      ? query[MEDIUM_CAMPAIGN_IDENTIFIER]
      : null;
  }

  if (isXtorCampaign) return 'XTOR';

  return null;
};

/* This transforms urls with a hash param to query params
   ie. from bbc.com/mundo#some_param_1=24&some_param_2=48 to bbc.com/mundo?some_param_1=24&some_param_2=48
*/
const parameteriseHash = hash => new Url(hash.replace('#', '?'), true).query;

const getMarketingUrlParam = (href, field) => {
  const { query, hash } = new Url(href, true);

  const queryWithParams = hash ? parameteriseHash(hash) : query;

  return Object.prototype.hasOwnProperty.call(queryWithParams, field)
    ? queryWithParams[field]
    : '';
};

const buildMarketingString = marketingValues =>
  marketingValues
    .map(({ value, wrap }) => (wrap && value ? `[${value}]` : value))
    .join('-');

/*
 * RSS marketing string uses v2 full-custom campaigns which uses specifies that parameters are in the format "src_myproperty: myvalue" for each property in the campaign.
 * more information at https://developers.atinternet-solutions.com/as2-tagging-en/javascript-en/campaigns-javascript-en/marketing-campaigns-v2/?kw=at_custom#full-custom-campaigns_13
 */
const buildRSSMarketingString = href => {
  const { query, hash } = new Url(href, true);

  const queryWithParams = hash ? parameteriseHash(hash) : query;

  return Object.keys(queryWithParams).reduce((accum, currVal) => {
    if (currVal.includes('at_')) {
      const type = currVal.replace('at_', '');

      if (type === 'medium') {
        return [
          {
            key: 'src_medium',
            description: 'rss campaign prefix',
            value: 'RSS',
            wrap: false,
          },
        ];
      }

      return [
        ...accum,
        {
          key: `src_${type}`,
          description: `src_${type} field`,
          value: getMarketingUrlParam(href, currVal),
          wrap: false,
        },
      ];
    }
    return accum;
  }, []);
};

export const getRSSMarketingString = (href, campaignType) =>
  campaignType === 'RSS' ? buildRSSMarketingString(href) : [];

export const getAffiliateMarketingString = href =>
  buildMarketingString([
    {
      description: 'affiliate campaign prefix',
      value: 'al',
      wrap: false,
    },
    {
      description: 'at_campaign field',
      value: getMarketingUrlParam(href, 'at_campaign'),
      wrap: false,
    },
    {
      description: 'at_type field',
      value: getMarketingUrlParam(href, 'at_type'),
      wrap: true,
    },
    {
      description: 'at_identifier field',
      value: getMarketingUrlParam(href, 'at_identifier'),
      wrap: true,
    },
    {
      description: 'at_format field',
      value: getMarketingUrlParam(href, 'at_format'),
      wrap: true,
    },
    {
      description: 'at_creation field',
      value: getMarketingUrlParam(href, 'at_creation'),
      wrap: true,
    },
    {
      description: 'at_variant field',
      value: getMarketingUrlParam(href, 'at_variant'),
      wrap: true,
    },
  ]);

export const getSLMarketingString = href =>
  buildMarketingString([
    {
      description: 'sponsored links campaign prefix',
      value: 'SEC',
      wrap: false,
    },
    {
      description: 'at_campaign field',
      value: getMarketingUrlParam(href, 'at_campaign'),
      wrap: false,
    },
    {
      description: 'at_platform field',
      value: getMarketingUrlParam(href, 'at_platform'),
      wrap: true,
    },
    {
      description: 'at_creation field',
      value: getMarketingUrlParam(href, 'at_creation'),
      wrap: true,
    },
    {
      description: 'at_variant field',
      value: getMarketingUrlParam(href, 'at_variant'),
      wrap: true,
    },
    {
      description: 'at_network field',
      value:
        {
          search: 'F=S',
          content: 'F=C',
        }[getMarketingUrlParam(href, 'at_network')] || '',
      wrap: false,
    },
    {
      description: 'at_term field',
      value: getMarketingUrlParam(href, 'at_term'),
      wrap: true,
    },
  ]);

export const getEmailMarketingString = href =>
  buildMarketingString([
    {
      description:
        'email campaign prefix depending on value of at_emailtype param',
      value:
        {
          acquisition: 'EREC',
          retention: 'EPR',
          promotion: 'ES',
        }[getMarketingUrlParam(href, 'at_emailtype')] || '',
      wrap: false,
    },
    {
      description: 'at_campaign field',
      value: getMarketingUrlParam(href, 'at_campaign'),
      wrap: false,
    },
    {
      description: 'at_creation field',
      value: getMarketingUrlParam(href, 'at_creation'),
      wrap: true,
    },
    {
      description: 'at_send_date field',
      value: getMarketingUrlParam(href, 'at_send_date'),
      wrap: false,
    },
    {
      description: 'at_link field',
      value: getMarketingUrlParam(href, 'at_link'),
      wrap: true,
    },
    {
      description: 'at_recipient_id + @ + at_recipient_list field',
      value: `${getMarketingUrlParam(
        href,
        'at_recipient_id',
      )}@${getMarketingUrlParam(href, 'at_recipient_list')}`,
      wrap: false,
    },
  ]);

export const getDisplayMarketingString = href =>
  buildMarketingString([
    {
      description: 'display campaign prefix',
      value: 'AD',
      wrap: false,
    },
    {
      description: 'at_campaign field',
      value: getMarketingUrlParam(href, 'at_campaign'),
      wrap: false,
    },
    {
      description: 'at_creation field',
      value: getMarketingUrlParam(href, 'at_creation'),
      wrap: true,
    },
    {
      description: 'at_variant field',
      value: getMarketingUrlParam(href, 'at_variant'),
      wrap: true,
    },
    {
      description: 'at_format field',
      value: getMarketingUrlParam(href, 'at_format'),
      wrap: true,
    },
    {
      description: 'blank value (-)',
      value: '',
      wrap: false,
    },
    {
      description: 'at_general_placement field',
      value: getMarketingUrlParam(href, 'at_general_placement'),
      wrap: true,
    },
    {
      description: 'at_detail_placement field',
      value: getMarketingUrlParam(href, 'at_detail_placement'),
      wrap: true,
    },
  ]);

export const getCustomMarketingString = href =>
  buildMarketingString([
    {
      description: 'custom campaign prefix',
      value: `CS${getMarketingUrlParam(href, 'at_medium').replace(
        'custom',
        '',
      )}`,
      wrap: false,
    },
    {
      description: 'at_campaign field',
      value: getMarketingUrlParam(href, 'at_campaign'),
      wrap: false,
    },
    {
      description: 'at_custom1 field',
      value: getMarketingUrlParam(href, 'at_custom1'),
      wrap: true,
    },
    {
      description: 'at_custom2 field',
      value: getMarketingUrlParam(href, 'at_custom2'),
      wrap: true,
    },
    {
      description: 'at_custom_3 field',
      value: getMarketingUrlParam(href, 'at_custom3'),
      wrap: true,
    },
    {
      description: 'at_custom_4 field',
      value: getMarketingUrlParam(href, 'at_custom4'),
      wrap: true,
    },
  ]);

export const getXtorMarketingString = href => {
  const field = 'xtor';

  const { query, hash } = new Url(href, true);

  const hashObject = hash ? parameteriseHash(hash) : '';

  const queryWithParams = { ...query, ...hashObject };

  return queryWithParams[field] || null;
};

export const getATIMarketingString = (href, campaignType) => {
  if (!campaignType) return null;

  const supportedCampaignMappings = {
    affiliate: () => getAffiliateMarketingString(href),
    sl: () => getSLMarketingString(href),
    email: () => getEmailMarketingString(href),
    display: () => getDisplayMarketingString(href),
    custom: () => getCustomMarketingString(href),
    XTOR: () => getXtorMarketingString(href),
  };

  const isSupportedCampaign = campaignMapping =>
    campaignType.startsWith(campaignMapping);

  const selectedCampaignType = Object.keys(supportedCampaignMappings).find(
    campaignMapping => isSupportedCampaign(campaignMapping),
  );

  return supportedCampaignMappings[selectedCampaignType]
    ? supportedCampaignMappings[selectedCampaignType]()
    : null;
};

export const LIBRARY_VERSION = 'simorgh';

export const onOnionTld = () =>
  onClient() ? window.location.host.endsWith('.onion') : false;