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: track video and text activities #1736

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions .changeset/eleven-dingos-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@commercetools-website/self-learning-smoke-test': minor
'@commercetools-docs/gatsby-theme-api-docs': minor
'@commercetools-website/api-docs-smoke-test': minor
'@commercetools-docs/gatsby-theme-docs': minor
'@commercetools-website/docs-smoke-test': minor
'@commercetools-website/documentation': minor
'@commercetools-website/site-template': minor
'@commercetools-docs/ui-kit': minor
---

Add the ability of tracking course content from video and text pages
11 changes: 11 additions & 0 deletions packages/gatsby-theme-docs/gatsby-config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ const validateThemeOptions = (options) => {
};
const productionHostname = 'docs.commercetools.com';

const getTagListOption = (themeOptions) =>
!themeOptions.transformerMdx ? [] : themeOptions.transformerMdx.tagList;

const config = (themeOptions = {}) => {
const pluginOptions = { ...defaultOptions, ...themeOptions };
const additionalTags = getTagListOption(themeOptions);
// backwards compat to single value GA configuration
if (
pluginOptions.gaTrackingId &&
Expand Down Expand Up @@ -318,6 +322,13 @@ const config = (themeOptions = {}) => {
],
},
},
{
resolve: '@commercetools-docs/gatsby-transformer-mdx-introspection',
options: {
...themeOptions.transformerMdx,
tagList: [...additionalTags],
},
},
].filter(Boolean),
};
};
Expand Down
11 changes: 11 additions & 0 deletions packages/gatsby-theme-docs/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {
VideoProgressReachedEvent,
ContentPageViewedEvent,
} from './src/modules/self-learning/hooks/use-learning-tracking';

declare global {
interface GlobalEventHandlersEventMap {
'selflearning:video:progressReached': VideoProgressReachedEvent;
'selflearning:pageContent:viewed': ContentPageViewedEvent;
}
}
1 change: 1 addition & 0 deletions packages/gatsby-theme-docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
},
"dependencies": {
"@auth0/auth0-react": "2.1.0",
"@commercetools-docs/gatsby-transformer-mdx-introspection": "16.0.0",
"@commercetools-docs/ui-kit": "22.2.0",
"@commercetools-uikit/card": "16.3.0",
"@commercetools-uikit/checkbox-input": "16.3.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import PropTypes from 'prop-types';
import { useLazyLoad } from '@commercetools-docs/ui-kit';
import LoadingSpinner from '@commercetools-uikit/loading-spinner';
import VideoPlaceholder from './video-placeholder';
import { EVENT_VIDEO_PROGRESS } from '../modules/self-learning/hooks/use-learning-tracking';

const videoJsVersion = '8.2.1';
const videoJsVersion = '8.3.0';

/**
* Preset value. Evaluate overtime if any of these needs to be a prop
Expand Down Expand Up @@ -52,6 +53,27 @@ const VideoPlayer = (props) => {
);
}
const player = playerRef.current;
let eventTriggered = false; // Flag to track event triggering

// for tracking purposes, the video player emits a selflearning:video:progressReached
// event when the play time reached a certain threshold (defaults to 80%)
player.on('timeupdate', () => {
const completeThreshold = props.completeAtPercent
? parseInt(props.completeAtPercent) / 100
: 0.8; // defaults to 80% (0.8)
const currentTime = player.currentTime();
const duration = player.duration();
const progress = currentTime / duration;
if (!eventTriggered && progress >= completeThreshold) {
// Trigger custom event
const customEvent = new CustomEvent(EVENT_VIDEO_PROGRESS, {
detail: { progress, url: props.url },
});
const el = document.getElementById('application');
el.dispatchEvent(customEvent);
eventTriggered = true; // Set the flag to prevent further triggering
}
});
return () => {
if (player) {
player.dispose();
Expand All @@ -78,6 +100,7 @@ const VideoPlayer = (props) => {
VideoPlayer.propTypes = {
url: PropTypes.string.isRequired,
poster: PropTypes.string,
completeAtPercent: PropTypes.string,
};

export default VideoPlayer;
26 changes: 26 additions & 0 deletions packages/gatsby-theme-docs/src/hooks/use-page-visibility.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useState, useEffect } from 'react';

const usePageVisibility = (isEnabled) => {
const [isVisible, setIsVisible] = useState(false);

useEffect(() => {
function handleVisibilityChange() {
setIsVisible(document.visibilityState === 'visible');
}

if (isEnabled) {
setIsVisible(document.visibilityState === 'visible');
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener(
'visibilitychange',
handleVisibilityChange
);
};
}
}, [isEnabled]);

return isVisible;
};

export default usePageVisibility;
29 changes: 27 additions & 2 deletions packages/gatsby-theme-docs/src/layouts/content.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useRef } from 'react';
import PropTypes from 'prop-types';
import { Markdown } from '@commercetools-docs/ui-kit';
import SpacingsStack from '@commercetools-uikit/spacings-stack';
Expand Down Expand Up @@ -26,11 +26,36 @@ import {
CourseCompleteModal,
useCourseInfoByPageSlugs,
} from '../modules/self-learning';
import {
useFetchCourseDetails,
getMatchingTopic,
} from '../modules/self-learning/hooks/use-course-details';
import {
useLearningTrackingHandler,
useContentPageTrackingDispatcher,
} from '../modules/self-learning/hooks/use-learning-tracking';

const LayoutContent = (props) => {
const courseInfo = useCourseInfoByPageSlugs([props.pageContext.slug]);
const courseId = courseInfo[props.pageContext.slug]?.courseId;
const { data } = useFetchCourseDetails(courseId);
const isSelfLearningPage = !!courseId;
let selfLearningTopic;
if (isSelfLearningPage) {
selfLearningTopic = getMatchingTopic(
data?.result?.topics,
courseInfo[props.pageContext.slug]?.topicName
);
}
useLearningTrackingHandler(
courseId,
selfLearningTopic,
props.pageContext.slug
);
useContentPageTrackingDispatcher(selfLearningTopic);

const { ref, inView, entry } = useInView();
const contentRef = useRef();
const isSearchBoxInView = !Boolean(entry) || inView;
const layoutState = useLayoutState();
const siteData = useSiteData();
Expand Down Expand Up @@ -95,7 +120,7 @@ const LayoutContent = (props) => {
<PlaceholderPageHeaderSideBannerArea />
</SpacingsStack>
</LayoutPageHeaderSide>
<LayoutPageContent>
<LayoutPageContent ref={courseId ? contentRef : null}>
<PageContentInset id="body-content" showRightBorder>
{props.children}
<ContentPagination slug={props.pageContext.slug} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export const fetcherWithToken = async (
url: string,
getAuthToken: () => Promise<string>,
learnApiBaseUrl: string,
method: 'GET' | 'POST'
method: 'GET' | 'POST',
body?: object
): Promise<ApiCallResult<EnrolledCourses | CourseWithDetails>> => {
const responseHandler = async (response: Response) => {
if (!response.ok) {
Expand Down Expand Up @@ -60,17 +61,23 @@ export const fetcherWithToken = async (
const accessToken = await getAuthToken();

// ...then performs fetch
let bodyHeaders = {};
if (body) {
bodyHeaders = { 'Content-Type': 'application/json' };
}
const response = await fetch(`${learnApiBaseUrl}${url}`, {
method,
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
...bodyHeaders,
},
// Allowing to include authorization and cookie headers because the API is hosted on a different
// subdomain even in production and as opposed to cookie domains, CORS same-origin
// policy does not consider a subdomain of the document domain same-origin.
// ("learning-api.docs.commercetools.com" is not same-origin with "docs.commercetools.com")
credentials: 'include',
body: body ? JSON.stringify(body) : undefined,
});
return responseHandler(response);
} catch (e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,24 @@ export const useFetchCourseDetails = (
};
};

export const getTopicStatusByPageTitle = (
export const getMatchingTopic = (
topics: CourseTopic[] | undefined,
pageTitle: string
) => {
if (!topics || !pageTitle) {
return 'notAvailable';
return;
}
const matchingTopic = topics.find(
return topics.find(
(topic) =>
topic.name.trim().toLowerCase() === pageTitle.trim().toLowerCase()
);
};

export const getTopicStatusByPageTitle = (
topics: CourseTopic[] | undefined,
pageTitle: string
) => {
const matchingTopic = getMatchingTopic(topics, pageTitle);
if (matchingTopic) {
return matchingTopic.completed ? 'completed' : 'inProgress';
}
Expand Down
Loading