fix: initial optimizely and segment events (#170)

This commit is contained in:
Jody Bailey
2023-06-30 12:40:59 +02:00
committed by GitHub
parent 2c7e10ffc2
commit e7d9255fe5
23 changed files with 542 additions and 47 deletions

1
.env
View File

@@ -40,3 +40,4 @@ ACCOUNT_SETTINGS_URL=''
ACCOUNT_PROFILE_URL=''
ENABLE_NOTICES=''
CAREER_LINK_URL=''
OPTIMIZELY_FULL_STACK_SDK_KEY=''

View File

@@ -47,3 +47,4 @@ ACCOUNT_SETTINGS_URL='http://localhost:1997'
ACCOUNT_PROFILE_URL='http://localhost:1995'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
OPTIMIZELY_FULL_STACK_SDK_KEY=''

View File

@@ -46,3 +46,4 @@ ACCOUNT_SETTINGS_URL='http://account-settings-url.test'
ACCOUNT_PROFILE_URL='http://account-profile-url.test'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
OPTIMIZELY_FULL_STACK_SDK_KEY='SDK Key'

151
package-lock.json generated
View File

@@ -19,6 +19,7 @@
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.15",
"@optimizely/react-sdk": "^2.9.2",
"@redux-beacon/segment": "^1.1.0",
"@reduxjs/toolkit": "^1.6.1",
"@testing-library/user-event": "^13.5.0",
@@ -4370,6 +4371,135 @@
"@octokit/openapi-types": "^17.2.0"
}
},
"node_modules/@optimizely/js-sdk-logging": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@optimizely/js-sdk-logging/-/js-sdk-logging-0.3.1.tgz",
"integrity": "sha512-K71Jf283FP0E4oXehcXTTM3gvgHZHr7FUrIsw//0mdJlotHJT4Nss4hE0CWPbBxO7LJAtwNnO+VIA/YOcO4vHg==",
"dependencies": {
"@optimizely/js-sdk-utils": "^0.4.0"
}
},
"node_modules/@optimizely/js-sdk-utils": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@optimizely/js-sdk-utils/-/js-sdk-utils-0.4.0.tgz",
"integrity": "sha512-QG2oytnITW+VKTJK+l0RxjaS5VrA6W+AZMzpeg4LCB4Rn4BEKtF+EcW/5S1fBDLAviGq/0TLpkjM3DlFkJ9/Gw==",
"dependencies": {
"uuid": "^3.3.2"
}
},
"node_modules/@optimizely/js-sdk-utils/node_modules/uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"bin": {
"uuid": "bin/uuid"
}
},
"node_modules/@optimizely/optimizely-sdk": {
"version": "4.9.4",
"resolved": "https://registry.npmjs.org/@optimizely/optimizely-sdk/-/optimizely-sdk-4.9.4.tgz",
"integrity": "sha512-aYxndR6RahnLdX7SQR1YO2dklfNjbCGUUvRaYJZ50LIsDqhkAu426vxHwYO+V+QJxqipypPG5SVdG1m32AgDvw==",
"dependencies": {
"@optimizely/js-sdk-datafile-manager": "^0.9.5",
"@optimizely/js-sdk-event-processor": "^0.9.2",
"@optimizely/js-sdk-logging": "^0.3.1",
"json-schema": "^0.4.0",
"murmurhash": "0.0.2",
"uuid": "^3.3.2"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@optimizely/optimizely-sdk/node_modules/@optimizely/js-sdk-datafile-manager": {
"version": "0.9.5",
"resolved": "https://registry.npmjs.org/@optimizely/js-sdk-datafile-manager/-/js-sdk-datafile-manager-0.9.5.tgz",
"integrity": "sha512-O4ujr1nBBAQBtx8YoKNpzzaEZgsE+aU4dxubT17ePqv/YVUWE+JOY21tSRrqZy/BlbbyzL+ElT8hrGB5ZzVoIQ==",
"dependencies": {
"@optimizely/js-sdk-logging": "^0.3.1",
"@optimizely/js-sdk-utils": "^0.4.0",
"decompress-response": "^4.2.1"
},
"engines": {
"node": ">=8.0.0"
},
"peerDependencies": {
"@react-native-async-storage/async-storage": "^1.2.0"
},
"peerDependenciesMeta": {
"@react-native-async-storage/async-storage": {
"optional": true
}
}
},
"node_modules/@optimizely/optimizely-sdk/node_modules/@optimizely/js-sdk-event-processor": {
"version": "0.9.5",
"resolved": "https://registry.npmjs.org/@optimizely/js-sdk-event-processor/-/js-sdk-event-processor-0.9.5.tgz",
"integrity": "sha512-g5zqAjJuexxgbNvn7dacFkQXQxH3+OtjELfmSswvhxP9EHkyNR0ZdQF/kBxFxr335F2/RRPvAJ9tQBPkwaBg8g==",
"dependencies": {
"@optimizely/js-sdk-logging": "^0.3.1",
"@optimizely/js-sdk-utils": "^0.4.0"
},
"peerDependencies": {
"@react-native-async-storage/async-storage": "^1.2.0",
"@react-native-community/netinfo": "5.9.4"
},
"peerDependenciesMeta": {
"@react-native-async-storage/async-storage": {
"optional": true
},
"@react-native-community/netinfo": {
"optional": true
}
}
},
"node_modules/@optimizely/optimizely-sdk/node_modules/decompress-response": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
"integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==",
"dependencies": {
"mimic-response": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@optimizely/optimizely-sdk/node_modules/mimic-response": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
"integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@optimizely/optimizely-sdk/node_modules/uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"bin": {
"uuid": "bin/uuid"
}
},
"node_modules/@optimizely/react-sdk": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/@optimizely/react-sdk/-/react-sdk-2.9.2.tgz",
"integrity": "sha512-//OozC59dr5Lsss2H9Jnyb35FMTF8Z+CMFi89kVs1U1Fy1sKOXK7Web1hw18DBZctwKfbb8Sl+Yw7Pgmo3P2fA==",
"dependencies": {
"@optimizely/js-sdk-logging": "^0.3.1",
"@optimizely/optimizely-sdk": "^4.9.1",
"hoist-non-react-statics": "^3.3.0",
"prop-types": "^15.6.2",
"utility-types": "^2.1.0 || ^3.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz",
@@ -16520,6 +16650,11 @@
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
"node_modules/json-schema": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -17573,6 +17708,14 @@
"multicast-dns": "cli.js"
}
},
"node_modules/murmurhash": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/murmurhash/-/murmurhash-0.0.2.tgz",
"integrity": "sha512-LKlwdZKWzvCQpMszb2HO5leJ7P9T4m5XuDKku8bM0uElrzqK9cn0+iozwQS8jO4SNjrp4w7olalgd8WgsIjhWA==",
"engines": {
"node": "*"
}
},
"node_modules/nanoid": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
@@ -27075,6 +27218,14 @@
"integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==",
"dev": true
},
"node_modules/utility-types": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz",
"integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==",
"engines": {
"node": ">= 4"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",

View File

@@ -36,6 +36,7 @@
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.15",
"@optimizely/react-sdk": "^2.9.2",
"@redux-beacon/segment": "^1.1.0",
"@reduxjs/toolkit": "^1.6.1",
"@testing-library/user-event": "^13.5.0",

View File

@@ -5,10 +5,12 @@ exports[`ProductRecommendations matches snapshot 1`] = `
crossProductCourses={
Array [
Object {
"courseRunKey": "course-v1:Test+Course+2022T2",
"courseType": "executive-education-2u",
"image": Object {
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
},
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
"owners": Array [
Object {
"key": "HarvardX",
@@ -16,14 +18,15 @@ exports[`ProductRecommendations matches snapshot 1`] = `
"name": "Harvard University",
},
],
"prospectusPath": "course/introduction-to-computer-sceince",
"title": "Introduction to Computer Science",
},
Object {
"courseRunKey": "course-v1:Test+Course+2022T2",
"courseType": "bootcamp-2u",
"image": Object {
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
},
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
"owners": Array [
Object {
"key": "HarvardX",
@@ -31,7 +34,6 @@ exports[`ProductRecommendations matches snapshot 1`] = `
"name": "Harvard University",
},
],
"prospectusPath": "course/introduction-to-computer-sceince",
"title": "Introduction to Computer Science",
},
]
@@ -39,10 +41,12 @@ exports[`ProductRecommendations matches snapshot 1`] = `
openCourses={
Array [
Object {
"courseRunKey": "course-v1:Test+Course+2022T2",
"courseType": "verified-audit",
"image": Object {
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
},
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
"owners": Array [
Object {
"key": "HarvardX",
@@ -50,14 +54,15 @@ exports[`ProductRecommendations matches snapshot 1`] = `
"name": "Harvard University",
},
],
"prospectusPath": "course/introduction-to-computer-sceince",
"title": "Introduction to Computer Science",
},
Object {
"courseRunKey": "course-v1:Test+Course+2022T2",
"courseType": "audit",
"image": Object {
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
},
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
"owners": Array [
Object {
"key": "HarvardX",
@@ -65,14 +70,15 @@ exports[`ProductRecommendations matches snapshot 1`] = `
"name": "Harvard University",
},
],
"prospectusPath": "course/introduction-to-computer-sceince",
"title": "Introduction to Computer Science",
},
Object {
"courseRunKey": "course-v1:Test+Course+2022T2",
"courseType": "verified",
"image": Object {
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
},
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
"owners": Array [
Object {
"key": "HarvardX",
@@ -80,14 +86,15 @@ exports[`ProductRecommendations matches snapshot 1`] = `
"name": "Harvard University",
},
],
"prospectusPath": "course/introduction-to-computer-sceince",
"title": "Introduction to Computer Science",
},
Object {
"courseRunKey": "course-v1:Test+Course+2022T2",
"courseType": "course",
"image": Object {
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
},
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
"owners": Array [
Object {
"key": "HarvardX",
@@ -95,7 +102,6 @@ exports[`ProductRecommendations matches snapshot 1`] = `
"name": "Harvard University",
},
],
"prospectusPath": "course/introduction-to-computer-sceince",
"title": "Introduction to Computer Science",
},
]

View File

@@ -20,7 +20,7 @@ describe('ProductRecommendations ProductCard', () => {
headerImage,
schoolLogo,
courseType: courseTypeToProductTypeMap[course.courseType],
url: `https://www.edx.org/${course.prospectusPath}`,
url: `${course.marketingUrl}&linked_from=recommender`,
};
it('matches snapshot', () => {

View File

@@ -22,7 +22,7 @@ const ProductCardContainer = ({ finalProductList, courseTypes }) => (
.map((item) => (
<ProductCard
key={item.title}
url={`https://www.edx.org/${item.prospectusPath}`}
url={`${item.marketingUrl}&linked_from=recommender`}
title={item.title}
subtitle={item.owners[0].name}
headerImage={item.image.src}

View File

@@ -19,10 +19,12 @@ exports[`ProductRecommendations LoadedView matches snapshot 1`] = `
finalProductList={
Array [
Object {
"courseRunKey": "course-v1:Test+Course+2022T2",
"courseType": "executive-education-2u",
"image": Object {
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
},
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
"owners": Array [
Object {
"key": "HarvardX",
@@ -30,14 +32,15 @@ exports[`ProductRecommendations LoadedView matches snapshot 1`] = `
"name": "Harvard University",
},
],
"prospectusPath": "course/introduction-to-computer-sceince",
"title": "Introduction to Computer Science",
},
Object {
"courseRunKey": "course-v1:Test+Course+2022T2",
"courseType": "bootcamp-2u",
"image": Object {
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
},
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
"owners": Array [
Object {
"key": "HarvardX",
@@ -45,14 +48,15 @@ exports[`ProductRecommendations LoadedView matches snapshot 1`] = `
"name": "Harvard University",
},
],
"prospectusPath": "course/introduction-to-computer-sceince",
"title": "Introduction to Computer Science",
},
Object {
"courseRunKey": "course-v1:Test+Course+2022T2",
"courseType": "verified-audit",
"image": Object {
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
},
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
"owners": Array [
Object {
"key": "HarvardX",
@@ -60,14 +64,15 @@ exports[`ProductRecommendations LoadedView matches snapshot 1`] = `
"name": "Harvard University",
},
],
"prospectusPath": "course/introduction-to-computer-sceince",
"title": "Introduction to Computer Science",
},
Object {
"courseRunKey": "course-v1:Test+Course+2022T2",
"courseType": "audit",
"image": Object {
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
},
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
"owners": Array [
Object {
"key": "HarvardX",
@@ -75,7 +80,6 @@ exports[`ProductRecommendations LoadedView matches snapshot 1`] = `
"name": "Harvard University",
},
],
"prospectusPath": "course/introduction-to-computer-sceince",
"title": "Introduction to Computer Science",
},
]

View File

@@ -4,7 +4,7 @@ exports[`ProductRecommendations ProductCard matches snapshot 1`] = `
<Card
as="Hyperlink"
className="base-card d-flex text-decoration-none"
destination="https://www.edx.org/course/introduction-to-computer-sceince"
destination="https://www.edx.org/course/some-course?utm_source=source&linked_from=recommender"
isClickable={true}
>
<Card.ImageCap

View File

@@ -20,7 +20,7 @@ exports[`ProductRecommendations ProductCardContainer matches snapshot 1`] = `
schoolLogo="http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png"
subtitle="Harvard University"
title="Introduction to Computer Science"
url="https://www.edx.org/course/introduction-to-computer-sceince"
url="https://www.edx.org/course/some-course?utm_source=source&linked_from=recommender"
/>
</div>
</div>
@@ -40,7 +40,7 @@ exports[`ProductRecommendations ProductCardContainer matches snapshot 1`] = `
schoolLogo="http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png"
subtitle="Harvard University"
title="Introduction to Computer Science"
url="https://www.edx.org/course/introduction-to-computer-sceince"
url="https://www.edx.org/course/some-course?utm_source=source&linked_from=recommender"
/>
</div>
</div>
@@ -60,7 +60,7 @@ exports[`ProductRecommendations ProductCardContainer matches snapshot 1`] = `
schoolLogo="http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png"
subtitle="Harvard University"
title="Introduction to Computer Science"
url="https://www.edx.org/course/introduction-to-computer-sceince"
url="https://www.edx.org/course/some-course?utm_source=source&linked_from=recommender"
/>
<ProductCard
courseType="Course"
@@ -69,7 +69,7 @@ exports[`ProductRecommendations ProductCardContainer matches snapshot 1`] = `
schoolLogo="http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png"
subtitle="Harvard University"
title="Introduction to Computer Science"
url="https://www.edx.org/course/introduction-to-computer-sceince"
url="https://www.edx.org/course/some-course?utm_source=source&linked_from=recommender"
/>
<ProductCard
courseType="Course"
@@ -78,7 +78,7 @@ exports[`ProductRecommendations ProductCardContainer matches snapshot 1`] = `
schoolLogo="http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png"
subtitle="Harvard University"
title="Introduction to Computer Science"
url="https://www.edx.org/course/introduction-to-computer-sceince"
url="https://www.edx.org/course/some-course?utm_source=source&linked_from=recommender"
/>
<ProductCard
courseType="Course"
@@ -87,7 +87,7 @@ exports[`ProductRecommendations ProductCardContainer matches snapshot 1`] = `
schoolLogo="http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png"
subtitle="Harvard University"
title="Introduction to Computer Science"
url="https://www.edx.org/course/introduction-to-computer-sceince"
url="https://www.edx.org/course/some-course?utm_source=source&linked_from=recommender"
/>
</div>
</div>

View File

@@ -1,9 +1,9 @@
import { useState, useEffect } from 'react';
import { RequestStates, RequestKeys } from 'data/constants/requests';
import { StrictDict } from 'utils';
import { reduxHooks } from 'hooks';
import { SortKeys } from 'data/constants/app';
import { useWindowSize, breakpoints } from '@edx/paragon';
import api from './api';
import * as module from './hooks';
@@ -12,11 +12,15 @@ export const state = StrictDict({
data: (val) => useState(val), // eslint-disable-line
});
export const useIsMobile = () => {
const { width } = useWindowSize();
return width < breakpoints.small.minWidth;
};
export const useShowRecommendationsFooter = () => {
const hasAvailableDashboards = reduxHooks.useHasAvailableDashboards();
const hasRequestCompleted = reduxHooks.useRequestIsCompleted(RequestKeys.initialize);
// Hardcoded to not show until experiment related code is implemented
return {
shouldShowFooter: false,
shouldLoadFooter: hasRequestCompleted && !hasAvailableDashboards,
@@ -83,4 +87,4 @@ export const useProductRecommendationsData = () => {
};
};
export default { useProductRecommendationsData, useShowRecommendationsFooter };
export default { useProductRecommendationsData, useShowRecommendationsFooter, useIsMobile };

View File

@@ -4,6 +4,7 @@ import { waitFor } from '@testing-library/react';
import { MockUseState } from 'testUtils';
import { RequestStates } from 'data/constants/requests';
import { reduxHooks } from 'hooks';
import { useWindowSize } from '@edx/paragon';
import { wait } from './utils';
import api from './api';
@@ -67,6 +68,23 @@ describe('ProductRecommendations hooks', () => {
});
});
describe('useIsMobile', () => {
it('returns false if the width of the window is greater than or equal to 576px', () => {
useWindowSize
.mockReturnValueOnce({ width: 576, height: 943 })
.mockReturnValueOnce({ width: 1400, height: 943 });
expect(hooks.useIsMobile()).toBeFalsy();
expect(hooks.useIsMobile()).toBeFalsy();
});
it('returns true if the width of the window is less than 576px', () => {
useWindowSize.mockReturnValueOnce({ width: 575, height: 943 });
expect(hooks.useIsMobile()).toBeTruthy();
});
});
describe('useShowRecommendationsFooter', () => {
// TODO: Update when hardcoded value is removed
it('returns whether the footer widget should show and should load', () => {

View File

@@ -1,11 +1,10 @@
import React from 'react';
import { reduxHooks } from 'hooks';
import './index.scss';
import { useWindowSize, breakpoints } from '@edx/paragon';
import { reduxHooks } from 'hooks';
import NoCoursesView from 'containers/CourseList/NoCoursesView';
import LoadingView from './components/LoadingView';
import LoadedView from './components/LoadedView';
import { useProductRecommendationsData } from './hooks';
import hooks from './hooks';
const ProductRecommendations = () => {
const checkEmptyResponse = (obj) => {
@@ -14,9 +13,8 @@ const ProductRecommendations = () => {
return result.length === values.length;
};
const { productRecommendations, isLoading, isLoaded } = useProductRecommendationsData();
const { width } = useWindowSize();
const isMobile = width < breakpoints.small.minWidth;
const { productRecommendations, isLoading, isLoaded } = hooks.useProductRecommendationsData();
const isMobile = hooks.useIsMobile();
const hasCourses = reduxHooks.useHasCourses();
const shouldShowPlaceholder = checkEmptyResponse(productRecommendations);

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { shallow } from 'enzyme';
import { useWindowSize } from '@edx/paragon';
import { reduxHooks } from 'hooks';
import hooks from './hooks';
import ProductRecommendations from './index';
import LoadingView from './components/LoadingView';
@@ -10,7 +10,13 @@ import { mockCrossProductResponse, mockAmplitudeResponse } from './testData';
jest.mock('./hooks', () => ({
useProductRecommendationsData: jest.fn(),
useHasCourses: jest.fn(),
useIsMobile: jest.fn(),
}));
jest.mock('hooks', () => ({
reduxHooks: {
useHasCourses: jest.fn(),
},
}));
jest.mock('./components/LoadingView', () => 'LoadingView');
@@ -31,13 +37,10 @@ describe('ProductRecommendations', () => {
productRecommendations: mockCrossProductResponse,
};
const desktopWindowSize = {
width: 1400,
height: 943,
};
beforeEach(() => reduxHooks.useHasCourses.mockReturnValueOnce(true));
it('matches snapshot', () => {
useWindowSize.mockReturnValueOnce(desktopWindowSize);
hooks.useIsMobile.mockReturnValueOnce(false);
hooks.useProductRecommendationsData.mockReturnValueOnce({
...successfullLoadValues,
});
@@ -46,7 +49,7 @@ describe('ProductRecommendations', () => {
});
it('renders the LoadingView if the request is pending', () => {
useWindowSize.mockReturnValueOnce(desktopWindowSize);
hooks.useIsMobile.mockReturnValueOnce(false);
hooks.useProductRecommendationsData.mockReturnValueOnce({
...defaultValues,
isLoading: true,
@@ -57,7 +60,7 @@ describe('ProductRecommendations', () => {
);
});
it('renders nothing if the request has failed', () => {
useWindowSize.mockReturnValueOnce(desktopWindowSize);
hooks.useIsMobile.mockReturnValueOnce(false);
hooks.useProductRecommendationsData.mockReturnValueOnce({
...defaultValues,
hasFailed: true,
@@ -67,8 +70,8 @@ describe('ProductRecommendations', () => {
expect(wrapper.type()).toBeNull();
});
it('renders nothing if the width of the screen size is less than 576px (mobile view)', () => {
useWindowSize.mockReturnValueOnce({ width: 575, height: 976 });
it('renders nothing if the user is on the mobile view', () => {
hooks.useIsMobile.mockReturnValueOnce(true);
hooks.useProductRecommendationsData.mockReturnValueOnce({
...successfullLoadValues,
});
@@ -79,7 +82,7 @@ describe('ProductRecommendations', () => {
});
it('renders NoCoursesView if the request is loaded, user has courses, and the response is empty', () => {
useWindowSize.mockReturnValueOnce(desktopWindowSize);
hooks.useIsMobile.mockReturnValueOnce(false);
hooks.useProductRecommendationsData.mockReturnValueOnce({
...successfullLoadValues,
productRecommendations: {
@@ -87,7 +90,6 @@ describe('ProductRecommendations', () => {
crossProductCourses: [],
},
});
hooks.useHasCourses.mockReturnValueOnce(true);
expect(shallow(<ProductRecommendations />)).toMatchObject(
shallow(<NoCoursesView />),
@@ -96,11 +98,10 @@ describe('ProductRecommendations', () => {
describe('LoadedView', () => {
it('renders with cross product data if the request completed and the user has courses', () => {
useWindowSize.mockReturnValueOnce(desktopWindowSize);
hooks.useIsMobile.mockReturnValueOnce(false);
hooks.useProductRecommendationsData.mockReturnValueOnce({
...successfullLoadValues,
});
hooks.useHasCourses.mockReturnValueOnce(true);
expect(shallow(<ProductRecommendations />)).toMatchObject(
shallow(
@@ -113,12 +114,11 @@ describe('ProductRecommendations', () => {
});
it('renders the LoadedView with Amplitude course data if the request completed', () => {
useWindowSize.mockReturnValueOnce(desktopWindowSize);
hooks.useIsMobile.mockReturnValueOnce(false);
hooks.useProductRecommendationsData.mockReturnValueOnce({
...successfullLoadValues,
productRecommendations: mockAmplitudeResponse,
});
hooks.useHasCourses.mockReturnValueOnce(true);
expect(shallow(<ProductRecommendations />)).toMatchObject(
shallow(

View File

@@ -0,0 +1,9 @@
import { createInstance } from '@optimizely/react-sdk';
const OPTIMIZELY_SDK_KEY = process.env.OPTIMIZELY_FULL_STACK_SDK_KEY;
const optimizelyClient = createInstance({
sdkKey: OPTIMIZELY_SDK_KEY,
});
export default optimizelyClient;

View File

@@ -0,0 +1,13 @@
import { createInstance } from '@optimizely/react-sdk';
import optimizelyClient from './optimizely';
jest.mock('@optimizely/react-sdk', () => ({
createInstance: jest.fn(() => 'mockedClient'),
}));
describe('optimizelyClient', () => {
it('should create an Optimizely client instance with the correct SDK key', () => {
expect(optimizelyClient).toBeDefined();
expect(createInstance).toHaveBeenCalledWith({ sdkKey: 'SDK Key' });
});
});

View File

@@ -0,0 +1,38 @@
import { StrictDict } from 'utils';
import optimizelyClient from './optimizely';
export const PRODUCT_RECOMMENDATIONS_EXP_KEY = 'learner_dashboard_product_recommendations_exp';
export const PRODUCT_RECOMMENDATIONS_EXP_VARIATION = 'learner_dashboard_product_recommendations_enabled';
export const eventNames = StrictDict({
productRecommendationsViewed: 'product_recommendations_viewed',
productHeaderClicked: 'product_header_clicked',
productCardClicked: 'product_card_clicked',
courseCardClicked: 'course_card_clicked',
});
export const activateProductRecommendationsExperiment = (userId, userAttributes) => {
const variation = optimizelyClient?.activate(
PRODUCT_RECOMMENDATIONS_EXP_KEY,
userId,
userAttributes,
);
return variation === PRODUCT_RECOMMENDATIONS_EXP_VARIATION;
};
export const trackProductRecommendationsViewed = (userId, userAttributes = {}) => {
optimizelyClient.track(eventNames.productRecommendationsViewed, userId, userAttributes);
};
export const trackProductHeaderClicked = (userId, userAttributes = {}) => {
optimizelyClient.track(eventNames.productHeaderClicked, userId, userAttributes);
};
export const trackProductCardClicked = (userId, userAttributes = {}) => {
optimizelyClient.track(eventNames.productCardClicked, userId, userAttributes);
};
export const trackCourseCardClicked = (userId, userAttributes = {}) => {
optimizelyClient.track(eventNames.courseCardClicked, userId, userAttributes);
};

View File

@@ -0,0 +1,79 @@
import optimizelyClient from './optimizely';
import {
eventNames,
PRODUCT_RECOMMENDATIONS_EXP_KEY,
PRODUCT_RECOMMENDATIONS_EXP_VARIATION,
activateProductRecommendationsExperiment,
trackProductRecommendationsViewed,
trackProductHeaderClicked,
trackProductCardClicked,
trackCourseCardClicked,
} from './optimizelyExperiment';
jest.mock('./optimizely', () => ({
activate: jest.fn(),
track: jest.fn(),
}));
const userId = '1';
const userAttributes = {
is_enterprise_user: false,
location: 'us',
is_mobile_user: false,
};
describe('Optimizely events', () => {
describe('activateProductRecommendationsExperiment', () => {
it('activates the experiment and returns in recommendations experiment variant', () => {
optimizelyClient.activate.mockReturnValueOnce(PRODUCT_RECOMMENDATIONS_EXP_VARIATION);
const inRecommendationsVariant = activateProductRecommendationsExperiment(userId, userAttributes);
expect(inRecommendationsVariant).toBeTruthy();
expect(optimizelyClient.activate).toHaveBeenCalledWith(
PRODUCT_RECOMMENDATIONS_EXP_KEY,
userId,
userAttributes,
);
});
});
describe('trackProductRecommendationsViewed', () => {
it('sends the productRecommendationsViewed event', () => {
trackProductRecommendationsViewed(userId);
expect(optimizelyClient.track).toHaveBeenCalledWith(
eventNames.productRecommendationsViewed,
userId,
{},
);
});
});
describe('trackProductHeaderClicked', () => {
it('sends the productHeaderClicked event', () => {
trackProductHeaderClicked(userId);
expect(optimizelyClient.track).toHaveBeenCalledWith(
eventNames.productHeaderClicked,
userId,
{},
);
});
});
describe('trackProductCardClicked', () => {
it('sends the productCardClicked event', () => {
trackProductCardClicked(userId);
expect(optimizelyClient.track).toHaveBeenCalledWith(
eventNames.productCardClicked,
userId,
{},
);
});
});
describe('trackCourseCardClicked', () => {
it('sends the courseCardClicked event', () => {
trackCourseCardClicked(userId);
expect(optimizelyClient.track).toHaveBeenCalledWith(
eventNames.courseCardClicked,
userId,
{},
);
});
});
});

View File

@@ -4,10 +4,12 @@ export const getCoursesWithType = (courseTypes) => {
courseTypes.forEach((type) => {
courses.push({
title: 'Introduction to Computer Science',
courseRunKey: 'course-v1:Test+Course+2022T2',
marketingUrl: 'https://www.edx.org/course/some-course?utm_source=source',
courseType: type,
image: {
src: 'https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg',
},
prospectusPath: 'course/introduction-to-computer-sceince',
owners: [
{
key: 'HarvardX',
@@ -15,7 +17,6 @@ export const getCoursesWithType = (courseTypes) => {
logoImageUrl: 'http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png',
},
],
courseType: type,
});
});

View File

@@ -0,0 +1,55 @@
import { StrictDict } from 'utils';
import { createLinkTracker, createEventTracker } from 'data/services/segment/utils';
import { courseTypeToProductLineMap, convertCourseRunKeyToCourseKey } from './utils';
export const eventNames = StrictDict({
productCardClicked: 'edx.bi.2u-product-card.clicked',
discoveryCardClicked: 'edx.bi.user.discovery.card.click',
recommendationsHeaderClicked: 'edx.bi.link.recommendations.header.clicked',
recommendationsViewed: 'edx.bi.user.recommendations.viewed',
});
export const productCardClicked = (courseRunKey, courseTitle, courseType, href) => {
createLinkTracker(
createEventTracker(eventNames.productCardClicked, {
category: 'recommender',
label: courseTitle,
courserun_key: courseRunKey,
page: 'dashboard',
product_line: courseTypeToProductLineMap[courseType],
}),
href,
);
};
export const discoveryCardClicked = (courseRunKey, courseTitle, href) => {
createLinkTracker(
createEventTracker(eventNames.discoveryCardClicked, {
category: 'recommender',
label: courseTitle,
courserun_key: courseRunKey,
page: 'dashboard',
product_line: 'open-course',
}),
href,
);
};
export const recommendationsHeaderClicked = (courseType, href) => {
createLinkTracker(
createEventTracker(eventNames.recommendationsHeaderClicked, {
category: 'recommender',
page: 'dashboard',
product_line: courseTypeToProductLineMap[courseType],
}),
href,
);
};
export const recommendationsViewed = (isControl, courseRunKey) => {
createEventTracker(eventNames.recommendationsViewed, {
is_control: isControl,
page: 'dashboard',
course_key: convertCourseRunKeyToCourseKey(courseRunKey),
});
};

View File

@@ -0,0 +1,98 @@
import { createLinkTracker, createEventTracker } from 'data/services/segment/utils';
import {
eventNames,
productCardClicked,
discoveryCardClicked,
recommendationsHeaderClicked,
recommendationsViewed,
} from './track';
jest.mock('data/services/segment/utils', () => ({
createEventTracker: jest.fn((args) => ({ createEventTracker: args })),
createLinkTracker: jest.fn((args) => ({ createLinkTracker: args })),
}));
const courseKey = 'MITx+5.0.01';
const courseRunKeyNew = `course-v1:${courseKey}+2022T2`;
const courseRunKeyOld = 'MITx/5.0.01/2022T2/';
const label = 'Web Design';
const headerLink = 'https://www.edx.org/search?tab=course?linked_from=recommender';
const courseUrl = 'https://www.edx.org/course/some-course';
const category = 'recommender';
describe('product recommendations trackers', () => {
describe('recommendationsViewed', () => {
describe('with old course run key format', () => {
it('creates an event tracker for when cross product recommendations are present', () => {
recommendationsViewed(false, courseRunKeyOld);
expect(createEventTracker).toHaveBeenCalledWith(
eventNames.recommendationsViewed,
{
is_control: false,
page: 'dashboard',
course_key: courseKey,
},
);
});
});
describe('with new course run key format', () => {
it('creates an event tracker for when cross product recommendations are present', () => {
recommendationsViewed(false, courseRunKeyNew);
expect(createEventTracker).toHaveBeenCalledWith(
eventNames.recommendationsViewed,
{
is_control: false,
page: 'dashboard',
course_key: courseKey,
},
);
});
});
});
describe('recommendationsHeaderClicked', () => {
it('creates a link tracker for when a recommendations header is clicked', () => {
const attributes = {
category,
product_line: 'open-courses',
page: 'dashboard',
};
const args = [eventNames.recommendationsHeaderClicked, attributes];
recommendationsHeaderClicked('Course', headerLink);
expect(createEventTracker).toHaveBeenCalledWith(...args);
expect(createLinkTracker).toHaveBeenCalledWith(createEventTracker(...args), headerLink);
});
});
describe('discoveryCardClicked', () => {
it('creates a link tracker for when a open course card is clicked', () => {
const attributes = {
category,
label,
courserun_key: courseRunKeyNew,
page: 'dashboard',
product_line: 'open-course',
};
const args = [eventNames.discoveryCardClicked, attributes];
discoveryCardClicked(courseRunKeyNew, label, courseUrl);
expect(createEventTracker).toHaveBeenCalledWith(...args);
expect(createLinkTracker).toHaveBeenCalledWith(createEventTracker(...args), courseUrl);
});
});
describe('productCardClicked', () => {
it('creates a link tracker for when a cross product course card is clicked', () => {
const attributes = {
category,
label,
courserun_key: courseRunKeyNew,
page: 'dashboard',
product_line: 'boot-camps',
};
const args = [eventNames.productCardClicked, attributes];
productCardClicked(courseRunKeyNew, label, 'Boot Camp', courseUrl);
expect(createEventTracker).toHaveBeenCalledWith(...args);
expect(createLinkTracker).toHaveBeenCalledWith(createEventTracker(...args), courseUrl);
});
});
});

View File

@@ -36,6 +36,23 @@ export const courseTypeToProductTypeMap = {
'masters-verified-audit': "Master's",
};
export const courseTypeToProductLineMap = {
'Executive Education': 'executive-education',
'Boot Camp': 'boot-camps',
Course: 'open-courses',
};
export const convertCourseRunKeyToCourseKey = (courseRunKey) => {
const newKeyFormat = courseRunKey.includes('+');
if (newKeyFormat) {
const splitCourseRunKey = courseRunKey.split(':').slice(-1)[0];
const splitCourseKey = splitCourseRunKey.split('+').slice(0, 2);
return `${splitCourseKey[0]}+${splitCourseKey[1]}`;
}
const splitCourseKey = courseRunKey.split('/').slice(0, 2);
return `${splitCourseKey[0]}+${splitCourseKey[1]}`;
};
export const wait = (time) => new Promise((resolve) => {
setTimeout(resolve, time);
});