Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Red Ventures event tracking to payment page #25

Merged
merged 25 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1a22a2b
feat: Add cohesion config to htmlWebpackPlugin options
julianajlk Oct 16, 2024
69c4447
feat: Add cohesion snippet to index.html
julianajlk Oct 16, 2024
920a38f
feat: Add trackPaymentButtonClick on PayPal button
julianajlk Oct 17, 2024
3e1c233
feat: Add cohesion directory with helper functions
julianajlk Oct 18, 2024
187958f
feat: Add tracking data for elementClicked and elementViewed to Check…
julianajlk Oct 22, 2024
0d629dd
feat: Add tracking to AlertMessage for coupon code banner
julianajlk Oct 22, 2024
9baf8fd
refactor: Modify contants and actions/reducers to fit changes done in…
julianajlk Oct 22, 2024
d7d74ac
refactor: Add RV tracking to handleSubmitPayment saga on success/fail…
julianajlk Oct 31, 2024
406b2d1
fix: Add hyphenateForTagular helper
julianajlk Nov 1, 2024
4638b7e
feat: Add dataTranslationMatrices file
julianajlk Nov 1, 2024
952a594
fix: Delete hooks.js file not needed
julianajlk Oct 23, 2024
630c382
refactor: Remove uniqueness from events in AlertMessage
julianajlk Nov 1, 2024
7317203
refactor: Remove uniqueness from events in Checkout
julianajlk Nov 1, 2024
43af071
feat: Att getCorrelationID helper
julianajlk Nov 4, 2024
4ef7f83
feat: Add correlationID to action creators for elementViewed and clicked
julianajlk Nov 4, 2024
c605238
chore: Install uuid dependency
julianajlk Nov 4, 2024
a537505
fix: Remove IntersectionObserver and add PayPalButton back in parent …
julianajlk Nov 5, 2024
cf6e794
feat: Add IntersectionObserver to PayPalButton and PlaceOrderButton
julianajlk Nov 5, 2024
b76579f
test: Update existing tests
julianajlk Oct 31, 2024
de33c82
test: Update snapshots
julianajlk Oct 31, 2024
d3ce906
chore: Update allowlist advisories
julianajlk Nov 4, 2024
c2be3c1
fix: Add siteUrl cohesionConfig from settings
julianajlk Nov 5, 2024
25a7891
fix: Feedback edits
julianajlk Nov 5, 2024
7356019
fix: Click events are not the same, updating event structure
julianajlk Nov 6, 2024
c1cd3aa
fix: Add cohesionConfig to htmlWebpackPlugin userOptions as well
julianajlk Nov 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

@julianajlk julianajlk Nov 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example structure of a ConversionTracked event (this is just a console.log, not actually a Tagular Debug log):

Screenshot 2024-11-05 at 5 09 58 PM

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the name is blank for the product, should also be run through hyphenateForTagular but looks great otherwise

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name in this case is the course title so it should not be hyphenated. It's null in local but should be populated in stage/prod

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm... ok, ive been told otherwise, but cool

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';

julianajlk marked this conversation as resolved.
Show resolved Hide resolved
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
Loading