Skip to content

Commit

Permalink
feat: Added observability events for video detail page (#1159)
Browse files Browse the repository at this point in the history
  • Loading branch information
irfanuddinahmad authored Sep 2, 2024
1 parent 29aeaa8 commit 272f6dd
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 8 deletions.
53 changes: 49 additions & 4 deletions src/components/microlearning/VideoDetailPage.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* eslint-disable max-len */
import { useEffect } from 'react';
import React, { useContext, useEffect } from 'react';
import { AppContext } from '@edx/frontend-platform/react';
import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils';
import {
Container, Row, Badge, Skeleton,
Icon,
Expand Down Expand Up @@ -34,11 +36,13 @@ const VideoPlayer = loadable(() => import(/* webpackChunkName: "videojs" */ '../
});

const VideoDetailPage = () => {
const { authenticatedUser: { userId } } = useContext(AppContext);
const { data: enterpriseCustomer } = useEnterpriseCustomer();
const { data: videoData } = useVideoDetails();
const { data: courseMetadata } = useVideoCourseMetadata(videoData?.courseKey);
const [pacingType, pacingTypeContent] = useCoursePacingType(courseMetadata?.activeCourseRun);
const { data: { subscriptionLicense } } = useSubscriptions();
const playerRef = React.useRef(null);
const intl = useIntl();

const customOptions = {
Expand All @@ -52,10 +56,47 @@ const VideoDetailPage = () => {
);

useEffect(() => {
if (videoData?.videoURL) {
if (videoData?.videoUrl) {
VideoPlayer.preload();
}
}, [videoData?.videoURL]);
sendEnterpriseTrackEvent(
enterpriseCustomer.uuid,
'edx.ui.enterprise.learner_portal.video.detail_page_viewed',
{
userId,
video: videoData?.videoUrl,
courseKey: videoData?.courseKey,
title: videoData?.courseTitle,
},
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [videoData?.videoUrl]);

const publishEvent = (eventName) => {
sendEnterpriseTrackEvent(
enterpriseCustomer.uuid,
eventName,
{
userId,
video: videoData?.videoUrl,
courseKey: videoData?.courseKey,
title: videoData?.courseTitle,
},
);
};

const handlePlayerReady = (player) => {
playerRef.current = player;
player.on('waiting', () => {
publishEvent('edx.ui.enterprise.learner_portal.video.player.stopped');
});
player.on('play', () => {
publishEvent('edx.ui.enterprise.learner_portal.video.player.playing');
});
player.on('pause', () => {
publishEvent('edx.ui.enterprise.learner_portal.video.player.paused');
});
};

if (!enableVideos) {
return <NotFoundPage />;
Expand Down Expand Up @@ -123,7 +164,7 @@ const VideoDetailPage = () => {
</div>
{ videoData?.videoUrl && (
<div className="video-player-wrapper position-relative mw-100 overflow-hidden my-4 mt-2">
<VideoPlayer videoURL={videoData?.videoUrl} customOptions={customOptions} />
<VideoPlayer videoURL={videoData?.videoUrl} onReady={handlePlayerReady} customOptions={customOptions} />
</div>
)}
</article>
Expand Down Expand Up @@ -233,6 +274,8 @@ const VideoDetailPage = () => {
a: (chunks) => (
<Link
to={`/${enterpriseCustomer.slug}/course/${courseMetadata?.key}`}
onClick={() => publishEvent('edx.ui.enterprise.learner_portal.video.view_course_link.clicked')}
onMouseLeave={() => publishEvent('edx.ui.enterprise.learner_portal.video.view_course_link.hovered')}
>
{chunks}
</Link>
Expand All @@ -245,6 +288,8 @@ const VideoDetailPage = () => {
variant="primary"
as={Link}
to={`/${enterpriseCustomer.slug}/course/${courseMetadata?.key}`}
onClick={() => publishEvent('edx.ui.enterprise.learner_portal.video.view_course_button.clicked')}
onMouseLeave={() => publishEvent('edx.ui.enterprise.learner_portal.video.view_course_button.hovered')}
className="mt-4.5 w-100"
>
<FormattedMessage
Expand Down
30 changes: 30 additions & 0 deletions src/components/microlearning/__mocks__/@loadable/component.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';

// mock the loadable function to load the module eagarly and expose a preload() function
function loadable(load) {
let Component;
// Capture the component from the module load function
// eslint-disable-next-line no-return-assign
const loadPromise = load().then((val) => (Component = val.default));
// Create a react component which renders the loaded component
const payload = {
on: jest.fn().mockImplementation((event, callback) => { callback(); }),
};
const Loadable = (props) => {
if (!Component) {
throw new Error(
`Bundle split module not loaded yet, import statement: ${load.toString()}`,
);
}
// eslint-disable-next-line react/prop-types
if (props.onReady) {
// eslint-disable-next-line no-return-assign, react/prop-types
return <Component {...props} onLoad={props.onReady(payload)} />;
}
return <Component {...props} />;
};
Loadable.preload = () => loadPromise;
return Loadable;
}

export default loadable;
107 changes: 103 additions & 4 deletions src/components/microlearning/tests/VideoDetailPage.test.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import React from 'react';
import axios from 'axios';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppContext } from '@edx/frontend-platform/react';
import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils';
import '@testing-library/jest-dom/extend-expect';
import { screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { enterpriseCustomerFactory } from '../../app/data/services/data/__factories__';
import userEvent from '@testing-library/user-event';
import { authenticatedUserFactory, enterpriseCustomerFactory } from '../../app/data/services/data/__factories__';
import { renderWithRouter } from '../../../utils/tests';
import VideoDetailPage from '../VideoDetailPage';
import {
Expand Down Expand Up @@ -35,8 +38,8 @@ const VIDEO_MOCK_DATA = {
institutionLogo: 'test-institution-logo.png',
courseKey: 'test-course-key',
};

const mockEnterpriseCustomer = enterpriseCustomerFactory();
const mockAuthenticatedUser = authenticatedUserFactory();

jest.mock('@edx/frontend-platform/auth');
getAuthenticatedHttpClient.mockReturnValue(axios);
Expand All @@ -60,6 +63,17 @@ jest.mock('@edx/frontend-platform/config', () => ({
getConfig: jest.fn(() => APP_CONFIG),
}));

jest.mock('@edx/frontend-enterprise-utils', () => ({
...jest.requireActual('@edx/frontend-enterprise-utils'),
sendEnterpriseTrackEvent: jest.fn(),
}));

jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: () => 'en',
getMessages: () => ({}),
}));

const mockCourseRun = {
isEnrollable: true,
key: 'test-course-run-key',
Expand Down Expand Up @@ -89,9 +103,17 @@ const mockCourseReviews = {
totalEnrollments: 4444,
};

const VideoDetailPageWrapper = () => (
const defaultAppState = {
authenticatedUser: mockAuthenticatedUser,
};

const VideoDetailPageWrapper = ({
initialAppState = defaultAppState,
}) => (
<IntlProvider locale="en">
<VideoDetailPage />
<AppContext.Provider value={initialAppState}>
<VideoDetailPage />
</AppContext.Provider>
</IntlProvider>
);

Expand Down Expand Up @@ -127,6 +149,17 @@ describe('VideoDetailPage Tests', () => {
// expect(screen.getByText('Skill 1')).toBeInTheDocument();
// expect(screen.getByText('Skill 2')).toBeInTheDocument();
expect(container.querySelector('.video-player-wrapper')).toBeTruthy();
expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(4);
expect(sendEnterpriseTrackEvent).toHaveBeenCalledWith(
mockEnterpriseCustomer.uuid,
'edx.ui.enterprise.learner_portal.video.detail_page_viewed',
{
userId: mockAuthenticatedUser.userId,
video: VIDEO_MOCK_DATA.videoUrl,
courseKey: VIDEO_MOCK_DATA.courseKey,
title: VIDEO_MOCK_DATA.courseTitle,
},
);
});
it('renders a video detail page when course level type is Intermediate', () => {
useVideoCourseMetadata.mockReturnValue({ data: { ...mockCourseMetadata, activeCourseRun: { ...mockCourseRun, levelType: 'Intermediate' } } });
Expand Down Expand Up @@ -166,4 +199,70 @@ describe('VideoDetailPage Tests', () => {
renderWithRouter(<VideoDetailPageWrapper />);
expect(screen.getByTestId('not-found-page')).toBeInTheDocument();
});
it('Sends observability events for view course details click', () => {
useVideoCourseMetadata.mockReturnValue({ data: mockCourseMetadata });
renderWithRouter(<VideoDetailPageWrapper />);
userEvent.click(screen.getByText('View course details'));
expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(5);
expect(sendEnterpriseTrackEvent).toHaveBeenCalledWith(
mockEnterpriseCustomer.uuid,
'edx.ui.enterprise.learner_portal.video.view_course_button.clicked',
{
userId: mockAuthenticatedUser.userId,
video: VIDEO_MOCK_DATA.videoUrl,
courseKey: VIDEO_MOCK_DATA.courseKey,
title: VIDEO_MOCK_DATA.courseTitle,
},
);
});
it('Sends observability events for view more on course page click', () => {
useVideoCourseMetadata.mockReturnValue({ data: mockCourseMetadata });
renderWithRouter(<VideoDetailPageWrapper />);
userEvent.click(screen.getByText('View more on course page'));
expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(5);
expect(sendEnterpriseTrackEvent).toHaveBeenCalledWith(
mockEnterpriseCustomer.uuid,
'edx.ui.enterprise.learner_portal.video.view_course_link.clicked',
{
userId: mockAuthenticatedUser.userId,
video: VIDEO_MOCK_DATA.videoUrl,
courseKey: VIDEO_MOCK_DATA.courseKey,
title: VIDEO_MOCK_DATA.courseTitle,
},
);
});
it('Sends observability events for view more on course page hover', async () => {
useVideoCourseMetadata.mockReturnValue({ data: mockCourseMetadata });
renderWithRouter(<VideoDetailPageWrapper />);
userEvent.hover(screen.getByText('View more on course page'));
userEvent.unhover(screen.getByText('View more on course page'));
expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(5);
expect(sendEnterpriseTrackEvent).toHaveBeenCalledWith(
mockEnterpriseCustomer.uuid,
'edx.ui.enterprise.learner_portal.video.view_course_link.hovered',
{
userId: mockAuthenticatedUser.userId,
video: VIDEO_MOCK_DATA.videoUrl,
courseKey: VIDEO_MOCK_DATA.courseKey,
title: VIDEO_MOCK_DATA.courseTitle,
},
);
});
it('Sends observability events for view course details hover', async () => {
useVideoCourseMetadata.mockReturnValue({ data: mockCourseMetadata });
renderWithRouter(<VideoDetailPageWrapper />);
userEvent.hover(screen.getByText('View course details'));
userEvent.unhover(screen.getByText('View course details'));
expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(5);
expect(sendEnterpriseTrackEvent).toHaveBeenCalledWith(
mockEnterpriseCustomer.uuid,
'edx.ui.enterprise.learner_portal.video.view_course_button.hovered',
{
userId: mockAuthenticatedUser.userId,
video: VIDEO_MOCK_DATA.videoUrl,
courseKey: VIDEO_MOCK_DATA.courseKey,
title: VIDEO_MOCK_DATA.courseTitle,
},
);
});
});
4 changes: 4 additions & 0 deletions src/setupTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ global.structuredClone = val => JSON.parse(JSON.stringify(val));
jest.mock('@edx/frontend-platform/logging');
jest.mock('@edx/frontend-platform/analytics');

if (typeof global.window.URL.createObjectURL === 'undefined') {
global.window.URL.createObjectURL = jest.fn();
}

// Upgrading to Node16 shows unhandledPromiseRejection warnings as errors so adding a handler
process.on('unhandledRejection', (reason, p) => {
// eslint-disable-next-line no-console
Expand Down

0 comments on commit 272f6dd

Please sign in to comment.