Skip to content

Commit

Permalink
feat: Add Red Ventures event tracking to payment page (#25)
Browse files Browse the repository at this point in the history
EDX-932 RV
  • Loading branch information
julianajlk authored Nov 6, 2024
1 parent 8e605c4 commit 4493f8d
Show file tree
Hide file tree
Showing 30 changed files with 927 additions and 232 deletions.
3 changes: 2 additions & 1 deletion audit-ci.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"GHSA-qw6h-vgh9-j6wx",
"GHSA-9wv6-86v2-598j",
"GHSA-m6fv-jmcg-4jfg",
"GHSA-cm22-4g7w-348p"
"GHSA-cm22-4g7w-348p",
"GHSA-c7qv-q95q-8v27"
],
"moderate": true
}
38 changes: 38 additions & 0 deletions cohesion.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const cohesionConfig = {
name: 'edx',
slug: 'edx',
domain: 'edx.org',
domainLabel: 'edx',
domainExtension: '.org',
domainLabelWithExtension: 'edx.org',
postTypeGql: '',
homepageGql: '',
siteUrl: process.env.MARKETING_SITE_BASE_URL,
cmsUrl: process.env.NEXT_PUBLIC_WORDPRESS_URL || '',
cmsUser: process.env.WP_USER || '',
cmsPwd: process.env.WP_PWD || '',
logoUrl: '',
studyMatchUrl: '',
voyagerUrl: '/discover',
identityToken: '',
gaCid: '',
gaSid: '',
gaMid: '',
defaultDegree: '',
defaultCategory: '',
defaultSubject: '',
tagularApiKey: '',
tagularSourceKey: 'src_2euJfAVNt6Z9kQz4e9t1SQBtm8x',
tagularWriteKey: 'wk_2euJfDkJVTtEVzsC8BPOb0g9dVj',
tagularCookieDomain: 'edx.org',
tagularDomainWhitelist: JSON.stringify([
'edx.org',
]),
monarchSourceId: '',
monarchToken: '',
newRelicAppID: '',
newRelicVoyagerAppID: '',
cookieLawId: '',
};

module.exports = cohesionConfig;
23 changes: 18 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@
"redux-thunk": "^2.4.1",
"regenerator-runtime": "^0.13.9",
"reselect": "^4.1.6",
"universal-cookie": "^4.0.4"
"universal-cookie": "^4.0.4",
"uuid": "^11.0.2"
},
"devDependencies": {
"@edx/browserslist-config": "^1.2.0",
Expand Down
27 changes: 27 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,33 @@
<% if (htmlWebpackPlugin.options.OPTIMIZELY_PROJECT_ID) { %>
<script src="https://www.edx.org/optimizelyjs/<%= htmlWebpackPlugin.options.OPTIMIZELY_PROJECT_ID %>.js"></script>
<% } %>

<% /* NOTE: Adding Red Ventures related cohesion/tagular code for the launch of the new marketing website. */ %>
<% if (htmlWebpackPlugin.options.cohesionConfig) { %>
<script>
{`!function(co,h,e,s,i,o,n){var d='documentElement';var a='className';h[d][a]+=' preampjs';
n.k=e;co._Cohesion=n;co._Preamp={k:s,start:new Date};co._Fuse={k:i};co._Tagular={k:o};
[e,s,i,o].map(function(x){co[x]=co[x]||function(){(co[x].q=co[x].q||[]).push([].slice.call(arguments))}});
var b=function(){var u=h[d][a];h[d][a]=u.replace(/ ?preampjs/g,'')};
h.addEventListener('DOMContentLoaded',function(){co.setTimeout(b,3e3);
co._Preamp.docReady=co._Fuse.docReady=!0});var z=h.createElement('script');
z.async=1;z.src='https://beam.edx.org/cohesion/cohesion-latest.min.js';
z.onerror=function(){var ce='error',f='function';for(var o of co[e].q||[])o[0]===ce&&typeof o[1]==f&&o[1]();co[e]=function(n,cb){n===ce&&typeof cb==f&&cb()};b()};
h.head.appendChild(z);}
(window,document,'cohesion','preamp','fuse','tagular',{
tagular: {
apiHost: 'https://beam.edx.org/v2/t',
writeKey: '<%= htmlWebpackPlugin.options.cohesionConfig.tagularWriteKey %>',
sourceKey: '<%= htmlWebpackPlugin.options.cohesionConfig.tagularSourceKey %>',
cookieDomain: '<%= htmlWebpackPlugin.options.cohesionConfig.tagularCookieDomain %>',
domainWhitelist: <%= htmlWebpackPlugin.options.cohesionConfig.tagularDomainWhitelist %>,
apiVersion: 'v2/t',
multiparty: true,
taggy: { enabled: true },
}
})`}
</script>
<% } %>
</head>
<body>
<div id="root">
Expand Down
28 changes: 28 additions & 0 deletions src/cohesion/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export const ElementType = {
Link: 'LINK',
Entry: 'ENTRY',
Button: 'BUTTON',
};

export const PaymentTitle = 'Payment | edX';

export const EventMap = {
ProductClicked: 'redventures.ecommerce.v1.ProductClicked',
ProductLoaded: 'redventures.ecommerce.v1.ProductLoaded',
ProductViewed: 'redventures.ecommerce.v1.ProductViewed',
ElementClicked: 'redventures.usertracking.v3.ElementClicked',
ElementViewed: 'redventures.usertracking.v3.ElementViewed',
FieldSelected: 'redventures.usertracking.v3.FieldSelected',
FormSubmitted: 'redventures.usertracking.v3.FormSubmitted',
FormViewed: 'redventures.usertracking.v3.FormViewed',
ConversionTracked: 'core.conversions.ConversionTracked.v2',
};

export const IS_FULLY_SHOWN_THRESHOLD_OR_MARGIN = 1.0;
export const IS_SINGLE_PX_SHOWN_THRESHOLD_OR_MARGIN = 0.0;
export const DOCUMENT_ROOT_NODE = null;

export const defaultOptions = {
threshold: IS_FULLY_SHOWN_THRESHOLD_OR_MARGIN,
root: DOCUMENT_ROOT_NODE,
};
65 changes: 65 additions & 0 deletions src/cohesion/dataTranslationMatrices.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const DEFAULT_LOOKUP_VALUE = '*';

// enums cause noo-shadow errors in prospectus
export const BaseTagularVariant = {
Courses: 'courses',
};

const TagularVariant = {
// Include base/x-ref things
...BaseTagularVariant,
// Supplied from Data Team
XSeries: 'certificates-xseries',
ProfessionalCertificate: 'certificates-prof-cert',
ExecEd: 'certificates-exec-ed',
MicroBachelors: 'certificates-micro-bachelors',
MicroMasters: 'certificates-micro-masters',
Bachelors: 'degrees-bachelors',
Masters: 'degrees-masters',
Doctorate: 'degrees-doctorate',
Bootcamps: 'bootcamps',
// Not Final
Certificates: 'degrees-certificates',
Licenses: 'degrees-licenses',
// Special Values
All: 'all-products/mixed',
Unknown: BaseTagularVariant.Courses,
};

const typeToVariant = {
[DEFAULT_LOOKUP_VALUE]: TagularVariant.Unknown, // missing value
// type_attr Slugs
bachelors: TagularVariant.Bachelors,
masters: TagularVariant.Masters,
microbachelors: TagularVariant.MicroBachelors,
micromasters: TagularVariant.MicroMasters,
'professional-certificate': TagularVariant.ProfessionalCertificate,
// 'professional-program-wl': TagularVariant.Unknown, Whitelabel Programs are no more.
xseries: TagularVariant.XSeries,
doctorate: TagularVariant.Doctorate,
license: TagularVariant.Licenses,
certificate: TagularVariant.Certificates,
// type_attr Display Names
Bachelors: TagularVariant.Bachelors,
Masters: TagularVariant.Masters,
MicroBachelors: TagularVariant.MicroBachelors,
MicroMasters: TagularVariant.MicroMasters,
'Professional Certificate': TagularVariant.ProfessionalCertificate,
// 'Professional Program': TagularVariant.Unknown, Whitelabel Programs are no more.
XSeries: TagularVariant.XSeries,
Doctorate: TagularVariant.Doctorate,
License: TagularVariant.Licenses,
Certificate: TagularVariant.Certificates,
// course_type Slugs
'executive-education-2u': TagularVariant.ExecEd,
'bootcamp-2u': TagularVariant.Bootcamps,
// Skipped as it was a note in the doc: 'Anything else': TagularVariant.Courses,
// course_type Display Name
'Executive Education': TagularVariant.ExecEd,
'Boot Camp': TagularVariant.Bootcamps,
Course: TagularVariant.Courses,
};

export default function translateVariant(x) {
return typeToVariant[x] || typeToVariant[DEFAULT_LOOKUP_VALUE];
}
68 changes: 68 additions & 0 deletions src/cohesion/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import Cookies from 'universal-cookie';
import { v4 as uuidv4 } from 'uuid';
import { EventMap } from './constants';

/**
* Fetch or Create a Tagular CorrelationID. This also refreshes the cookie's expiry.
*/
export const getCorrelationID = () => {
const COOKIE_NAME = 'tglr_correlation_id';
const PARAM_NAME = 'correlationId';

function getQueryParameter(name) {
const params = new URLSearchParams(window.location.search);

return params.get(name);
}

let paramId = getQueryParameter(PARAM_NAME) || new Cookies().get(COOKIE_NAME);

if (!paramId) {
paramId = uuidv4();
}

const expirationDate = new Date();
expirationDate.setMinutes(expirationDate.getMinutes() + 30); // 30 mins expiration from now
new Cookies().set(COOKIE_NAME, paramId, { expires: expirationDate });

return paramId;
};

/**
* Submit ('beam') an event via Tagular to Make.
* @param eventName Schema Name of the Event
* @param eventData The data required by the schema
*/
export const tagularEvent = (eventName, eventData) => {
// if tagular is available, try sending given event with event data
if (typeof window !== 'undefined' && window.tagular) {
try {
window.tagular('beam', eventName, {
'@type': EventMap[eventName],
...eventData,
});
} catch (error) {
// eslint-disable-next-line no-console
console.warn(`Tagular event ${eventName} not sent.`, error);
}
} else {
// eslint-disable-next-line no-console
console.warn('Tagular not available on page.');
}
};

/**
* Make Near Slugs from Plain Strings for ease of eventing.
* @example
* "Computer Science" => "computer-science"
* "Humanities & Arts" => "humanities-&-arts"
* "Someone added a space " => "someone-added-a-space"
*
* @param x Input String
*/
export function hyphenateForTagular(x) {
return x
.trim()
.toLowerCase()
.replace(/[^\w&]/g, '-');
}
2 changes: 2 additions & 0 deletions src/feedback/AlertList.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import createRootReducer from '../data/reducers';
import { addMessage } from './data/actions';
import { MESSAGE_TYPES } from './data/constants';

import '../mockIntersectionObserver';

jest.mock('@edx/frontend-platform/logging', () => ({
logError: jest.fn(),
}));
Expand Down
54 changes: 50 additions & 4 deletions src/feedback/AlertMessage.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import React, { useCallback } from 'react';
import React, {
useCallback, useEffect, useRef,
} from 'react';
import { useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import { Alert } from '@openedx/paragon';
import { ALERT_TYPES, MESSAGE_TYPES } from './data/constants';
import { trackElementIntersection } from '../payment/data/actions';
import { ElementType, PaymentTitle, IS_FULLY_SHOWN_THRESHOLD_OR_MARGIN } from '../cohesion/constants';

// Put in a message type, get an alert type.
const severityMap = {
Expand All @@ -17,6 +22,45 @@ const AlertMessage = (props) => {
id, messageType, userMessage, closeHandler, data,
} = props;

const alertRef = useRef(null);
const dispatch = useDispatch();

// RV promo banner tracking for successful coupon application
useEffect(() => {
const observerCallback = (entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && messageType === 'success' && userMessage.includes('added to basket')) {
const tagularElement = {
title: PaymentTitle,
url: entry.target?.baseURI,
pageType: 'checkout',
elementType: ElementType.Button,
name: 'promotional-code',
text: 'Apply',
};
dispatch(trackElementIntersection(tagularElement));
}
});
};

const observer = new IntersectionObserver(observerCallback, {
threshold: IS_FULLY_SHOWN_THRESHOLD_OR_MARGIN,
});

const currentElement = alertRef.current;

if (currentElement) {
observer.observe(currentElement);
}

return () => {
if (currentElement) {
observer.unobserve(currentElement);
}
observer.disconnect();
};
}, [messageType, userMessage, dispatch]);

const statusAlertProps = {
variant: ALERT_TYPES.WARNING,
onClose: useCallback(() => { closeHandler(id); }, [closeHandler, id]),
Expand All @@ -43,9 +87,11 @@ const AlertMessage = (props) => {
}

return (
<Alert {...statusAlertProps} dismissible>
{statusAlertProps.dialog}
</Alert>
<div ref={alertRef} id={userMessage}>
<Alert {...statusAlertProps} dismissible>
{statusAlertProps.dialog}
</Alert>
</div>
);
};

Expand Down
Loading

0 comments on commit 4493f8d

Please sign in to comment.