From b12f184d182ecfc299c04efdb562d192fce6ba6f Mon Sep 17 00:00:00 2001 From: Carla Duarte Date: Fri, 19 Mar 2021 12:36:36 -0400 Subject: [PATCH] AA-712: upsell link click tracking (#393) * AA-712: course_home_audit_access_expires and in_course_audit_access_expires * AA-712: course_home_welcome and in_course_welcome * AA-712: course_home_dates * AA-712: course_home_course_tools * AA-712: course_home_upgrade_shift_dates and dates_upgrade * AA-712: fixing up PR comments --- .../AccessExpirationAlert.jsx | 19 ++ src/alerts/access-expiration-alert/hooks.js | 5 +- src/alerts/offer-alert/OfferAlert.jsx | 19 ++ src/alerts/offer-alert/hooks.js | 5 +- .../__factories__/outlineTabData.factory.js | 21 +- .../data/__snapshots__/redux.test.js.snap | 7 +- .../dates-banner/DatesBannerContainer.jsx | 13 +- src/course-home/dates-tab/DatesTab.jsx | 17 ++ src/course-home/dates-tab/DatesTab.test.jsx | 54 ++++ src/course-home/outline-tab/DateSummary.jsx | 49 +++- src/course-home/outline-tab/OutlineTab.jsx | 25 +- .../outline-tab/OutlineTab.test.jsx | 257 +++++++++++++++++- .../outline-tab/widgets/CourseTools.jsx | 20 +- src/courseware/course/Course.jsx | 4 +- src/courseware/course/Course.test.jsx | 63 +++++ 15 files changed, 533 insertions(+), 45 deletions(-) diff --git a/src/alerts/access-expiration-alert/AccessExpirationAlert.jsx b/src/alerts/access-expiration-alert/AccessExpirationAlert.jsx index b51b3a95..450604f9 100644 --- a/src/alerts/access-expiration-alert/AccessExpirationAlert.jsx +++ b/src/alerts/access-expiration-alert/AccessExpirationAlert.jsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; +import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { FormattedMessage, FormattedDate, injectIntl, intlShape, } from '@edx/frontend-platform/i18n'; @@ -21,7 +22,10 @@ function AccessExpirationAlert({ intl, payload }) { const { accessExpiration, + courseId, + org, userTimezone, + analyticsPageName, } = payload; const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {}; @@ -66,6 +70,17 @@ function AccessExpirationAlert({ intl, payload }) { ); } + const logClick = () => { + sendTrackEvent('edx.bi.ecommerce.upsell_links_clicked', { + org_key: org, + courserun_key: courseId, + linkCategory: 'FBE_banner', + linkName: `${analyticsPageName}_audit_access_expires`, + linkType: 'link', + pageName: analyticsPageName, + }); + }; + let deadlineMessage = null; if (upgradeDeadline && upgradeUrl) { deadlineMessage = ( @@ -92,6 +107,7 @@ function AccessExpirationAlert({ intl, payload }) { className="font-weight-bold" style={{ textDecoration: 'underline' }} destination={upgradeUrl} + onClick={logClick} > {intl.formatMessage(messages.upgradeNow)} @@ -150,7 +166,10 @@ AccessExpirationAlert.propTypes = { upgradeDeadline: PropTypes.string, upgradeUrl: PropTypes.string, }).isRequired, + courseId: PropTypes.string.isRequired, + org: PropTypes.string.isRequired, userTimezone: PropTypes.string.isRequired, + analyticsPageName: PropTypes.string.isRequired, }).isRequired, }; diff --git a/src/alerts/access-expiration-alert/hooks.js b/src/alerts/access-expiration-alert/hooks.js index 7a994007..4744ef3e 100644 --- a/src/alerts/access-expiration-alert/hooks.js +++ b/src/alerts/access-expiration-alert/hooks.js @@ -3,11 +3,14 @@ import { useAlert } from '../../generic/user-messages'; const AccessExpirationAlert = React.lazy(() => import('./AccessExpirationAlert')); -function useAccessExpirationAlert(accessExpiration, userTimezone, topic) { +function useAccessExpirationAlert(accessExpiration, courseId, org, userTimezone, topic, analyticsPageName) { const isVisible = !!accessExpiration; // If it exists, show it. const payload = { accessExpiration, + courseId, + org, userTimezone, + analyticsPageName, }; useAlert(isVisible, { diff --git a/src/alerts/offer-alert/OfferAlert.jsx b/src/alerts/offer-alert/OfferAlert.jsx index a21e3df8..dce4b52c 100644 --- a/src/alerts/offer-alert/OfferAlert.jsx +++ b/src/alerts/offer-alert/OfferAlert.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { FormattedMessage, FormattedDate, injectIntl, intlShape, } from '@edx/frontend-platform/i18n'; @@ -11,7 +12,10 @@ import messages from './messages'; function OfferAlert({ intl, payload }) { const { + analyticsPageName, + courseId, offer, + org, userTimezone, } = payload; @@ -27,6 +31,17 @@ function OfferAlert({ intl, payload }) { } = offer; const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {}; + const logClick = () => { + sendTrackEvent('edx.bi.ecommerce.upsell_links_clicked', { + org_key: org, + courserun_key: courseId, + linkCategory: 'welcome', + linkName: `${analyticsPageName}_welcome`, + linkType: 'link', + pageName: analyticsPageName, + }); + }; + return ( @@ -61,6 +76,7 @@ function OfferAlert({ intl, payload }) { className="font-weight-bold" style={{ textDecoration: 'underline' }} destination={upgradeUrl} + onClick={logClick} > {intl.formatMessage(messages.upgradeNow)} @@ -71,6 +87,7 @@ function OfferAlert({ intl, payload }) { OfferAlert.propTypes = { intl: intlShape.isRequired, payload: PropTypes.shape({ + courseId: PropTypes.string.isRequired, offer: PropTypes.shape({ code: PropTypes.string.isRequired, discountedPrice: PropTypes.string.isRequired, @@ -79,7 +96,9 @@ OfferAlert.propTypes = { percentage: PropTypes.number.isRequired, upgradeUrl: PropTypes.string.isRequired, }).isRequired, + org: PropTypes.string.isRequired, userTimezone: PropTypes.string.isRequired, + analyticsPageName: PropTypes.string.isRequired, }).isRequired, }; diff --git a/src/alerts/offer-alert/hooks.js b/src/alerts/offer-alert/hooks.js index 318cf646..be86db3e 100644 --- a/src/alerts/offer-alert/hooks.js +++ b/src/alerts/offer-alert/hooks.js @@ -3,10 +3,13 @@ import { useAlert } from '../../generic/user-messages'; const OfferAlert = React.lazy(() => import('./OfferAlert')); -export function useOfferAlert(offer, userTimezone, topic) { +export function useOfferAlert(courseId, offer, org, userTimezone, topic, analyticsPageName) { const isVisible = !!offer; // if it exists, show it. const payload = { + analyticsPageName, + courseId, offer, + org, userTimezone, }; diff --git a/src/course-home/data/__factories__/outlineTabData.factory.js b/src/course-home/data/__factories__/outlineTabData.factory.js index d7a8b977..615033c9 100644 --- a/src/course-home/data/__factories__/outlineTabData.factory.js +++ b/src/course-home/data/__factories__/outlineTabData.factory.js @@ -5,19 +5,14 @@ import buildSimpleCourseBlocks from './courseBlocks.factory'; Factory.define('outlineTabData') .option('courseId', 'course-v1:edX+DemoX+Demo_Course') .option('host', 'http://localhost:18000') - .option('dateBlocks', []) - .attr('course_tools', ['host', 'courseId'], (host, courseId) => ([{ - analytics_id: 'edx.bookmarks', - title: 'Bookmarks', - url: `${host}/courses/${courseId}/bookmarks/`, - }])) + .option('date_blocks', []) .attr('course_blocks', ['courseId'], courseId => { const { courseBlocks } = buildSimpleCourseBlocks(courseId); return { blocks: courseBlocks.blocks, }; }) - .attr('dates_widget', ['dateBlocks'], (dateBlocks) => ({ + .attr('dates_widget', ['date_blocks'], (dateBlocks) => ({ course_date_blocks: dateBlocks, user_timezone: 'UTC', })) @@ -40,6 +35,18 @@ Factory.define('outlineTabData') goal_options: [], selected_goal: null, }, + course_tools: [ + { + analytics_id: 'edx.bookmarks', + title: 'Bookmarks', + url: 'https://example.com/bookmarks', + }, + { + analytics_id: 'edx.tool.verified_upgrade', + title: 'Upgrade to Verified', + url: 'https://example.com/upgrade', + }, + ], dates_banner_info: { content_type_gating_enabled: false, missed_gated_content: false, diff --git a/src/course-home/data/__snapshots__/redux.test.js.snap b/src/course-home/data/__snapshots__/redux.test.js.snap index 6e66374c..3aca4caa 100644 --- a/src/course-home/data/__snapshots__/redux.test.js.snap +++ b/src/course-home/data/__snapshots__/redux.test.js.snap @@ -405,7 +405,12 @@ Object { Object { "analyticsId": "edx.bookmarks", "title": "Bookmarks", - "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/bookmarks/", + "url": "https://example.com/bookmarks", + }, + Object { + "analyticsId": "edx.tool.verified_upgrade", + "title": "Upgrade to Verified", + "url": "https://example.com/upgrade", }, ], "datesBannerInfo": Object { diff --git a/src/course-home/dates-banner/DatesBannerContainer.jsx b/src/course-home/dates-banner/DatesBannerContainer.jsx index c5ac48cf..d9b0ea84 100644 --- a/src/course-home/dates-banner/DatesBannerContainer.jsx +++ b/src/course-home/dates-banner/DatesBannerContainer.jsx @@ -11,6 +11,7 @@ function DatesBannerContainer({ courseDateBlocks, datesBannerInfo, hasEnded, + logUpgradeLinkClick, model, tabFetch, }) { @@ -43,13 +44,19 @@ function DatesBannerContainer({ name: 'upgradeToCompleteGradedBanner', // verifiedUpgradeLink can be null if we've passed the upgrade deadline shouldDisplay: upgradeToCompleteGraded && verifiedUpgradeLink, - clickHandler: () => global.location.replace(verifiedUpgradeLink), + clickHandler: () => { + logUpgradeLinkClick(); + global.location.replace(verifiedUpgradeLink); + }, }, { name: 'upgradeToResetBanner', // verifiedUpgradeLink can be null if we've passed the upgrade deadline shouldDisplay: upgradeToReset && verifiedUpgradeLink, - clickHandler: () => global.location.replace(verifiedUpgradeLink), + clickHandler: () => { + logUpgradeLinkClick(); + global.location.replace(verifiedUpgradeLink); + }, }, { name: 'resetDatesBanner', @@ -80,12 +87,14 @@ DatesBannerContainer.propTypes = { verifiedUpgradeLink: PropTypes.string, }).isRequired, hasEnded: PropTypes.bool, + logUpgradeLinkClick: PropTypes.func, model: PropTypes.string.isRequired, tabFetch: PropTypes.func.isRequired, }; DatesBannerContainer.defaultProps = { hasEnded: false, + logUpgradeLinkClick: () => {}, }; export default DatesBannerContainer; diff --git a/src/course-home/dates-tab/DatesTab.jsx b/src/course-home/dates-tab/DatesTab.jsx index 7abdd015..bb52de8c 100644 --- a/src/course-home/dates-tab/DatesTab.jsx +++ b/src/course-home/dates-tab/DatesTab.jsx @@ -1,5 +1,6 @@ import React from 'react'; import { useSelector } from 'react-redux'; +import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import messages from './messages'; @@ -17,6 +18,10 @@ function DatesTab({ intl }) { courseId, } = useSelector(state => state.courseHome); + const { + org, + } = useModel('courseHomeMeta', courseId); + const { courseDateBlocks, datesBannerInfo, @@ -26,6 +31,17 @@ function DatesTab({ intl }) { /** [MM-P2P] Experiment */ const mmp2p = initDatesMMP2P(courseId); + const logUpgradeLinkClick = () => { + sendTrackEvent('edx.bi.ecommerce.upsell_links_clicked', { + org_key: org, + courserun_key: courseId, + linkCategory: 'personalized_learner_schedules', + linkName: 'dates_upgrade', + linkType: 'button', + pageName: 'dates_tab', + }); + }; + return ( <>
@@ -37,6 +53,7 @@ function DatesTab({ intl }) { courseDateBlocks={courseDateBlocks} datesBannerInfo={datesBannerInfo} hasEnded={hasEnded} + logUpgradeLinkClick={logUpgradeLinkClick} model="dates" tabFetch={fetchDatesTab} /> diff --git a/src/course-home/dates-tab/DatesTab.test.jsx b/src/course-home/dates-tab/DatesTab.test.jsx index 0904aab2..b89e4198 100644 --- a/src/course-home/dates-tab/DatesTab.test.jsx +++ b/src/course-home/dates-tab/DatesTab.test.jsx @@ -3,6 +3,7 @@ import { Route } from 'react-router'; import MockAdapter from 'axios-mock-adapter'; import { Factory } from 'rosie'; import { getConfig, history } from '@edx/frontend-platform'; +import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider } from '@edx/frontend-platform/react'; import { waitForElementToBeRemoved } from '@testing-library/dom'; @@ -18,6 +19,7 @@ import { appendBrowserTimezoneToUrl } from '../../utils'; import { UserMessagesProvider } from '../../generic/user-messages'; initializeMockApp(); +jest.mock('@edx/frontend-platform/analytics'); describe('DatesTab', () => { let axiosMock; @@ -241,5 +243,57 @@ describe('DatesTab', () => { // confirm "Shift due dates" button has not rendered expect(screen.queryByRole('button', { name: 'Shift due dates' })).not.toBeInTheDocument(); }); + + it('sends analytics event onClick of upgrade button in upgradeToCompleteGradedBanner', async () => { + sendTrackEvent.mockClear(); + datesTabData.datesBannerInfo = { + contentTypeGatingEnabled: true, + missedDeadlines: false, + missedGatedContent: false, + verifiedUpgradeLink: 'http://localhost:18130/basket/add/?sku=8CF08E5', + }; + + axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/course_home/v1/dates/${courseId}`).reply(200, datesTabData); + render(component); + + const upgradeButton = await waitFor(() => screen.getByRole('button', { name: 'Upgrade now' })); + fireEvent.click(upgradeButton); + + expect(sendTrackEvent).toHaveBeenCalledTimes(1); + expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.ecommerce.upsell_links_clicked', { + org_key: 'edX', + courserun_key: courseId, + linkCategory: 'personalized_learner_schedules', + linkName: 'dates_upgrade', + linkType: 'button', + pageName: 'dates_tab', + }); + }); + + it('sends analytics event onClick of upgrade button in upgradeToResetBanner', async () => { + sendTrackEvent.mockClear(); + datesTabData.datesBannerInfo = { + contentTypeGatingEnabled: true, + missedDeadlines: true, + missedGatedContent: true, + verifiedUpgradeLink: 'http://localhost:18130/basket/add/?sku=8CF08E5', + }; + + axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/course_home/v1/dates/${courseId}`).reply(200, datesTabData); + render(component); + + const upgradeButton = await waitFor(() => screen.getByRole('button', { name: 'Upgrade to shift due dates' })); + fireEvent.click(upgradeButton); + + expect(sendTrackEvent).toHaveBeenCalledTimes(1); + expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.ecommerce.upsell_links_clicked', { + org_key: 'edX', + courserun_key: courseId, + linkCategory: 'personalized_learner_schedules', + linkName: 'dates_upgrade', + linkType: 'button', + pageName: 'dates_tab', + }); + }); }); }); diff --git a/src/course-home/outline-tab/DateSummary.jsx b/src/course-home/outline-tab/DateSummary.jsx index 6e818169..05eae4b2 100644 --- a/src/course-home/outline-tab/DateSummary.jsx +++ b/src/course-home/outline-tab/DateSummary.jsx @@ -1,8 +1,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons'; +import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { FormattedDate } from '@edx/frontend-platform/i18n'; import React from 'react'; +import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; +import { useModel } from '../../generic/model-store'; import { isLearnerAssignment } from '../dates-tab/utils'; import './DateSummary.scss'; @@ -12,12 +15,30 @@ export default function DateSummary({ /** [MM-P2P] Experiment */ mmp2p, }) { + const { + courseId, + } = useSelector(state => state.courseHome); + const { + org, + } = useModel('courseHomeMeta', courseId); + const linkedTitle = dateBlock.link && isLearnerAssignment(dateBlock); const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {}; /** [MM-P2P] Experiment */ const showMMP2P = mmp2p.state.isEnabled && (dateBlock.dateType === 'verified-upgrade-deadline'); + const logVerifiedUpgradeClick = () => { + sendTrackEvent('edx.bi.ecommerce.upsell_links_clicked', { + org_key: org, + courserun_key: courseId, + linkCategory: '(none)', + linkName: 'course_home_dates', + linkType: 'link', + pageName: 'course_home', + }); + }; + return (
  • @@ -50,15 +71,27 @@ export default function DateSummary({ ) : (
    - {linkedTitle - && } - {!linkedTitle - &&
    {dateBlock.title}
    } + {linkedTitle && ( + + )} + {!linkedTitle && ( +
    {dateBlock.title}
    + )}
    - {dateBlock.description - &&
    {dateBlock.description}
    } - {!linkedTitle && dateBlock.link - && {dateBlock.linkText}} + {dateBlock.description && ( +
    {dateBlock.description}
    + )} + {!linkedTitle && dateBlock.link && ( + {}} + className="description-link" + > + {dateBlock.linkText} + + )}
    )}
  • diff --git a/src/course-home/outline-tab/OutlineTab.jsx b/src/course-home/outline-tab/OutlineTab.jsx index 086a3723..1b82f854 100644 --- a/src/course-home/outline-tab/OutlineTab.jsx +++ b/src/course-home/outline-tab/OutlineTab.jsx @@ -1,6 +1,6 @@ import React, { useRef, useState } from 'react'; import { useSelector } from 'react-redux'; -import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics'; +import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Button, Toast } from '@edx/paragon'; @@ -70,18 +70,22 @@ function OutlineTab({ intl }) { const [goalToastHeader, setGoalToastHeader] = useState(''); const [expandAll, setExpandAll] = useState(false); + const eventProperties = { + org_key: org, + courserun_key: courseId, + }; + const logResumeCourseClick = () => { sendTrackingLogEvent('edx.course.home.resume_course.clicked', { - courserun_key: courseId, + ...eventProperties, event_type: hasVisitedCourse ? 'resume' : 'start', - org_key: org, url: resumeCourseUrl, }); }; // Below the course title alerts (appearing in the order listed here) - const offerAlert = useOfferAlert(offer, userTimezone, 'outline-course-alerts'); - const accessExpirationAlert = useAccessExpirationAlert(accessExpiration, userTimezone, 'outline-course-alerts'); + const offerAlert = useOfferAlert(courseId, offer, org, userTimezone, 'outline-course-alerts', 'course_home'); + const accessExpirationAlert = useAccessExpirationAlert(accessExpiration, courseId, org, userTimezone, 'outline-course-alerts', 'course_home'); const courseStartAlert = useCourseStartAlert(courseId); const courseEndAlert = useCourseEndAlert(courseId); const certificateAvailableAlert = useCertificateAvailableAlert(courseId); @@ -91,6 +95,16 @@ function OutlineTab({ intl }) { const courseSock = useRef(null); + const logUpgradeLinkClick = () => { + sendTrackEvent('edx.bi.ecommerce.upsell_links_clicked', { + ...eventProperties, + linkCategory: 'personalized_learner_schedules', + linkName: 'course_home_upgrade_shift_dates', + linkType: 'button', + pageName: 'course_home', + }); + }; + /** [[MM-P2P] Experiment */ const MMP2P = initHomeMMP2P(courseId); @@ -146,6 +160,7 @@ function OutlineTab({ intl }) { courseDateBlocks={courseDateBlocks} datesBannerInfo={datesBannerInfo} hasEnded={hasEnded} + logUpgradeLinkClick={logUpgradeLinkClick} model="outline" tabFetch={fetchOutlineTab} /** [MM-P2P] Experiment */ diff --git a/src/course-home/outline-tab/OutlineTab.test.jsx b/src/course-home/outline-tab/OutlineTab.test.jsx index ea46922d..f1c59485 100644 --- a/src/course-home/outline-tab/OutlineTab.test.jsx +++ b/src/course-home/outline-tab/OutlineTab.test.jsx @@ -158,6 +158,58 @@ describe('Outline Tab', () => { }); }); + describe('Dates Banner', () => { + beforeEach(() => { + setMetadata({ is_enrolled: true }); + setTabData({ + dates_banner_info: { + content_type_gating_enabled: true, + missed_deadlines: true, + missed_gated_content: true, + verified_upgrade_link: 'http://localhost:18130/basket/add/?sku=8CF08E5', + }, + }, { + date_blocks: [ + { + assignment_type: 'Homework', + date: '2010-08-20T05:59:40.942669Z', + date_type: 'assignment-due-date', + description: '', + learner_has_access: true, + title: 'Missed assignment', + extra_info: null, + }, + ], + }); + }); + + it('renders upgradeToReset', async () => { + await fetchAndRender(); + + expect(screen.getByText('You are auditing this course,')).toBeInTheDocument(); + expect(screen.getByText('which means that you are unable to participate in graded assignments. It looks like you missed some important deadlines based on our suggested schedule. To complete graded assignments as part of this course and shift the past due assignments into the future, you can upgrade today.')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Upgrade to shift due dates' })).toBeInTheDocument(); + }); + + it('sends analytics event onClick of upgrade button in banner', async () => { + await fetchAndRender(); + sendTrackEvent.mockClear(); + + const upgradeButton = screen.getByRole('button', { name: 'Upgrade to shift due dates' }); + fireEvent.click(upgradeButton); + + expect(sendTrackEvent).toHaveBeenCalledTimes(1); + expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.ecommerce.upsell_links_clicked', { + org_key: 'edX', + courserun_key: courseId, + linkCategory: 'personalized_learner_schedules', + linkName: 'course_home_upgrade_shift_dates', + linkType: 'button', + pageName: 'course_home', + }); + }); + }); + describe('Welcome Message', () => { beforeEach(() => { setMetadata({ is_enrolled: true }); @@ -214,6 +266,63 @@ describe('Outline Tab', () => { }); }); + describe('Course Dates', () => { + it('renders when course date blocks are populated', async () => { + const startDate = new Date(); + startDate.setHours(startDate.getHours() + 1); + setMetadata({ is_enrolled: true }); + setTabData({}, { + date_blocks: [ + { + date_type: 'course-start-date', + date: startDate.toISOString(), + title: 'Start', + }, + ], + }); + await fetchAndRender(); + expect(screen.getByRole('heading', { name: 'Upcoming Dates' })).toBeInTheDocument(); + }); + + it('does not render when course date blocks are not populated', async () => { + setMetadata({ is_enrolled: true }); + await fetchAndRender(); + expect(screen.queryByRole('heading', { name: 'Upcoming Dates' })).not.toBeInTheDocument(); + }); + + it('sends analytics event onClick of upgrade link', async () => { + const now = new Date(); + const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1); + setMetadata({ is_enrolled: true }); + setTabData({}, { + date_blocks: [ + { + date_type: 'verified-upgrade-deadline', + date: tomorrow.toISOString(), + link: 'https://example.com/upgrade', + link_text: 'Upgrade to Verified Certificate', + title: 'Verification Upgrade Deadline', + }, + ], + }); + await fetchAndRender(); + sendTrackEvent.mockClear(); + + const upgradeLink = screen.getByRole('link', { name: 'Upgrade to Verified Certificate' }); + fireEvent.click(upgradeLink); + + expect(sendTrackEvent).toHaveBeenCalledTimes(1); + expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.ecommerce.upsell_links_clicked', { + org_key: 'edX', + courserun_key: courseId, + linkCategory: '(none)', + linkName: 'course_home_dates', + linkType: 'link', + pageName: 'course_home', + }); + }); + }); + describe('Course Goals', () => { const goalOptions = [ ['certify', 'Earn a certificate'], @@ -312,6 +421,51 @@ describe('Outline Tab', () => { }); }); + describe('Course Tools', () => { + it('renders title when tools are available', async () => { + await fetchAndRender(); + expect(screen.getByRole('heading', { name: 'Course Tools' })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'Bookmarks' })).toBeInTheDocument(); + }); + + it('does not render title when tools are not available', async () => { + setTabData({ + course_tools: [], + }); + await fetchAndRender(); + expect(screen.queryByRole('heading', { name: 'Course Tools' })).not.toBeInTheDocument(); + }); + + it('analytics sent when upgrade link clicked', async () => { + await fetchAndRender(); + expect(screen.getByRole('heading', { name: 'Course Tools' })).toBeInTheDocument(); + sendTrackEvent.mockClear(); + sendTrackingLogEvent.mockClear(); + + const upgradeLink = screen.getByRole('link', { name: 'Upgrade to Verified' }); + fireEvent.click(upgradeLink); + + expect(sendTrackEvent).toHaveBeenCalledTimes(1); + expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.ecommerce.upsell_links_clicked', { + org_key: 'edX', + courserun_key: courseId, + linkCategory: '(none)', + linkName: 'course_home_course_tools', + linkType: 'link', + pageName: 'course_home', + }); + + expect(sendTrackingLogEvent).toHaveBeenCalledTimes(1); + expect(sendTrackingLogEvent).toHaveBeenCalledWith('edx.course.tool.accessed', { + org_key: 'edX', + courserun_key: courseId, + course_id: courseId, + is_staff: false, + tool_name: 'edx.tool.verified_upgrade', + }); + }); + }); + describe('Alert List', () => { describe('Private Course Alert', () => { it('does not display alert for enrolled user', async () => { @@ -403,6 +557,33 @@ describe('Outline Tab', () => { await fetchAndRender(); await screen.findByText('to get unlimited access to the course as long as it exists on the site.', { exact: false }); }); + + it('sends analytics event onClick of upgrade link', async () => { + setTabData({ + access_expiration: { + expiration_date: '2080-01-01T12:00:00Z', + masquerading_expired_course: false, + upgrade_deadline: '2070-01-01T12:00:00Z', + upgrade_url: 'https://example.com/upgrade', + }, + }); + await fetchAndRender(); + + // Clearing after render to remove any events sent on view (ex. 'Promotion Viewed') + sendTrackEvent.mockClear(); + const upgradeLink = screen.getByRole('link', { name: 'Upgrade now' }); + fireEvent.click(upgradeLink); + + expect(sendTrackEvent).toHaveBeenCalledTimes(1); + expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.ecommerce.upsell_links_clicked', { + org_key: 'edX', + courserun_key: courseId, + linkCategory: 'FBE_banner', + linkName: 'course_home_audit_access_expires', + linkType: 'link', + pageName: 'course_home', + }); + }); }); describe('Course Start Alert', () => { @@ -412,7 +593,7 @@ describe('Outline Tab', () => { startDate.setDate(startDate.getDate() + 100); setMetadata({ is_enrolled: true }); setTabData({}, { - dateBlocks: [ + date_blocks: [ { date_type: 'course-start-date', date: startDate.toISOString(), @@ -430,7 +611,7 @@ describe('Outline Tab', () => { startDate.setHours(startDate.getHours() + 1); setMetadata({ is_enrolled: true }); setTabData({}, { - dateBlocks: [ + date_blocks: [ { date_type: 'course-start-date', date: startDate.toISOString(), @@ -451,7 +632,7 @@ describe('Outline Tab', () => { endDate.setDate(endDate.getDate() + 13); setMetadata({ is_enrolled: true }); setTabData({}, { - dateBlocks: [ + date_blocks: [ { date_type: 'course-end-date', date: endDate.toISOString(), @@ -469,7 +650,7 @@ describe('Outline Tab', () => { endDate.setHours(endDate.getHours() + 1); setMetadata({ is_enrolled: true }); setTabData({}, { - dateBlocks: [ + date_blocks: [ { date_type: 'course-end-date', date: endDate.toISOString(), @@ -491,7 +672,7 @@ describe('Outline Tab', () => { const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1); setMetadata({ is_enrolled: true }); setTabData({}, { - dateBlocks: [ + date_blocks: [ { date_type: 'course-end-date', date: yesterday.toISOString(), @@ -508,6 +689,53 @@ describe('Outline Tab', () => { await screen.findByText('We are working on generating course certificates.'); }); }); + + describe('Offer Alert', () => { + it('sends analytics event onClick of upgrade link', async () => { + setTabData({ + offer: { + code: 'EDXWELCOME', + expiration_date: '2070-01-01T12:00:00Z', + original_price: '$100', + discounted_price: '$85', + percentage: 15, + upgrade_url: 'https://example.com/upgrade', + }, + }); + await fetchAndRender(); + + expect(screen.getByRole('link', { name: 'Upgrade now' })).toBeInTheDocument(); + }); + + it('sends analytics event onClick of upgrade link', async () => { + setTabData({ + offer: { + code: 'EDXWELCOME', + expiration_date: '2070-01-01T12:00:00Z', + original_price: '$100', + discounted_price: '$85', + percentage: 15, + upgrade_url: 'https://example.com/upgrade', + }, + }); + await fetchAndRender(); + + // Clearing after render to remove any events sent on view (ex. 'Promotion Viewed') + sendTrackEvent.mockClear(); + const upgradeLink = screen.getByRole('link', { name: 'Upgrade now' }); + fireEvent.click(upgradeLink); + + expect(sendTrackEvent).toHaveBeenCalledTimes(1); + expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.ecommerce.upsell_links_clicked', { + org_key: 'edX', + courserun_key: courseId, + linkCategory: 'welcome', + linkName: 'course_home_welcome', + linkType: 'link', + pageName: 'course_home', + }); + }); + }); }); describe('Proctoring Info Panel', () => { @@ -697,17 +925,17 @@ describe('Outline Tab', () => { }); it('clicking upgrade link sends analytics', async () => { + await fetchAndRender(); + + // Clearing after render to remove any events sent on view (ex. 'Promotion Viewed') sendTrackEvent.mockClear(); sendTrackingLogEvent.mockClear(); - - await fetchAndRender(); const upgradeButton = screen.getByRole('link', { name: 'Upgrade ($149)' }); fireEvent.click(upgradeButton); - // 3 sendTrackEvent calls are expected because 1 happens on render, and 2 happen onClick - expect(sendTrackEvent).toHaveBeenCalledTimes(3); - expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'Promotion Clicked', { + expect(sendTrackEvent).toHaveBeenCalledTimes(2); + expect(sendTrackEvent).toHaveBeenNthCalledWith(1, 'Promotion Clicked', { org_key: 'edX', courserun_key: courseId, creative: 'sidebarupsell', @@ -715,7 +943,7 @@ describe('Outline Tab', () => { position: 'sidebar-message', promotion_id: 'courseware_verified_certificate_upsell', }); - expect(sendTrackEvent).toHaveBeenNthCalledWith(3, 'edx.bi.ecommerce.upsell_links_clicked', { + expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'edx.bi.ecommerce.upsell_links_clicked', { org_key: 'edX', courserun_key: courseId, linkCategory: 'green_upgrade', @@ -724,13 +952,12 @@ describe('Outline Tab', () => { pageName: 'course_home', }); - // 3 sendTrackingLogEvent calls are expected because 1 happens on render, and 2 happen onClick - expect(sendTrackingLogEvent).toHaveBeenCalledTimes(3); - expect(sendTrackingLogEvent).toHaveBeenNthCalledWith(2, 'edx.bi.course.upgrade.sidebarupsell.clicked', { + expect(sendTrackingLogEvent).toHaveBeenCalledTimes(2); + expect(sendTrackingLogEvent).toHaveBeenNthCalledWith(1, 'edx.bi.course.upgrade.sidebarupsell.clicked', { org_key: 'edX', courserun_key: courseId, }); - expect(sendTrackingLogEvent).toHaveBeenNthCalledWith(3, 'edx.course.enrollment.upgrade.clicked', { + expect(sendTrackingLogEvent).toHaveBeenNthCalledWith(2, 'edx.course.enrollment.upgrade.clicked', { org_key: 'edX', courserun_key: courseId, location: 'sidebar-message', diff --git a/src/course-home/outline-tab/widgets/CourseTools.jsx b/src/course-home/outline-tab/widgets/CourseTools.jsx index 36485706..85c87367 100644 --- a/src/course-home/outline-tab/widgets/CourseTools.jsx +++ b/src/course-home/outline-tab/widgets/CourseTools.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics'; +import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -23,15 +23,29 @@ function CourseTools({ courseId, intl }) { return null; } + const eventProperties = { + org_key: org, + courserun_key: courseId, + }; + const logClick = (analyticsId) => { const { administrator } = getAuthenticatedUser(); sendTrackingLogEvent('edx.course.tool.accessed', { - org_key: org, - courserun_key: courseId, + ...eventProperties, course_id: courseId, // should only be courserun_key, but left as-is for historical reasons is_staff: administrator, tool_name: analyticsId, }); + + if (analyticsId === 'edx.tool.verified_upgrade') { + sendTrackEvent('edx.bi.ecommerce.upsell_links_clicked', { + ...eventProperties, + linkCategory: '(none)', + linkName: 'course_home_course_tools', + linkType: 'link', + pageName: 'course_home', + }); + } }; const renderIcon = (iconClasses) => { diff --git a/src/courseware/course/Course.jsx b/src/courseware/course/Course.jsx index bfd8f671..2220d63e 100644 --- a/src/courseware/course/Course.jsx +++ b/src/courseware/course/Course.jsx @@ -48,8 +48,8 @@ function Course({ } = course; // Below the tabs, above the breadcrumbs alerts (appearing in the order listed here) - const offerAlert = useOfferAlert(offer, userTimezone, 'course'); - const accessExpirationAlert = useAccessExpirationAlert(accessExpiration, userTimezone, 'course'); + const offerAlert = useOfferAlert(courseId, offer, org, userTimezone, 'course', 'in_course'); + const accessExpirationAlert = useAccessExpirationAlert(accessExpiration, courseId, org, userTimezone, 'course', 'in_course'); const dispatch = useDispatch(); const celebrateFirstSection = celebrations && celebrations.firstSection; diff --git a/src/courseware/course/Course.test.jsx b/src/courseware/course/Course.test.jsx index fee1d3e8..389ce8dd 100644 --- a/src/courseware/course/Course.test.jsx +++ b/src/courseware/course/Course.test.jsx @@ -1,5 +1,6 @@ import React from 'react'; import { Factory } from 'rosie'; +import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { loadUnit, render, screen, waitFor, getByRole, initializeTestStore, fireEvent, } from '../../setupTest'; @@ -108,6 +109,68 @@ describe('Course', () => { await screen.findByText('Audit Access Expires'); }); + it('sends analytics event onClick of access expiration upgrade link', async () => { + sendTrackEvent.mockClear(); + + const courseMetadata = Factory.build('courseMetadata', { + access_expiration: { + expiration_date: '2080-01-01T12:00:00Z', + masquerading_expired_course: false, + upgrade_deadline: '2070-01-01T12:00:00Z', + upgrade_url: 'https://example.com/upgrade', + }, + user_timezone: 'UTC', + }); + const testStore = await initializeTestStore({ courseMetadata, excludeFetchSequence: true }, false); + render(, { store: testStore }); + await screen.findByText('Audit Access Expires'); + + const upgradeLink = screen.getByRole('link', { name: 'Upgrade now' }); + fireEvent.click(upgradeLink); + + expect(sendTrackEvent).toHaveBeenCalledTimes(1); + expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.ecommerce.upsell_links_clicked', { + org_key: 'edX', + courserun_key: courseMetadata.id, + linkCategory: 'FBE_banner', + linkName: 'in_course_audit_access_expires', + linkType: 'link', + pageName: 'in_course', + }); + }); + + it('sends analytics event onClick of offer alert link', async () => { + sendTrackEvent.mockClear(); + + const courseMetadata = Factory.build('courseMetadata', { + offer: { + code: 'EDXWELCOME', + expiration_date: '2070-01-01T12:00:00Z', + original_price: '$100', + discounted_price: '$85', + percentage: 15, + upgrade_url: 'https://example.com/upgrade', + }, + user_timezone: 'UTC', + }); + const testStore = await initializeTestStore({ courseMetadata, excludeFetchSequence: true }, false); + render(, { store: testStore }); + await screen.findByText('EDXWELCOME'); + + const upgradeLink = screen.getByRole('link', { name: 'Upgrade now' }); + fireEvent.click(upgradeLink); + + expect(sendTrackEvent).toHaveBeenCalledTimes(1); + expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.ecommerce.upsell_links_clicked', { + org_key: 'edX', + courserun_key: courseMetadata.id, + linkCategory: 'welcome', + linkName: 'in_course_welcome', + linkType: 'link', + pageName: 'in_course', + }); + }); + it('passes handlers to the sequence', async () => { const nextSequenceHandler = jest.fn(); const previousSequenceHandler = jest.fn();