diff --git a/package-lock.json b/package-lock.json index b2e60dba..ed9de586 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@fortawesome/free-regular-svg-icons": "5.15.4", "@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/react-fontawesome": "0.2.0", + "@optimizely/react-sdk": "^2.9.2", "core-js": "3.31.1", "prop-types": "15.8.1", "react-markdown": "^8.0.5" @@ -43,14 +44,14 @@ "semantic-release": "^21.0.7" }, "peerDependencies": { - "@edx/frontend-platform": "5.0.0", + "@edx/frontend-platform": "^4.3.0 || ^5.0.0", "@edx/paragon": "20.46.0", "@reduxjs/toolkit": "1.8.1", "react": "16.14.0 || ^17.0.0", "react-dom": "16.14.0 || ^17.0.0", "react-redux": "7.2.9", - "react-router": "6.15.0", - "react-router-dom": "6.15.0", + "react-router": "5.2.1 || ^6.0.0", + "react-router-dom": "5.3.0 || ^6.0.0", "redux": "4.1.2", "regenerator-runtime": "0.13.11", "uuid": "9.0.0" @@ -5062,6 +5063,135 @@ "@octokit/openapi-types": "^18.0.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", @@ -11781,7 +11911,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dev": true, "dependencies": { "react-is": "^16.7.0" } @@ -11789,8 +11918,7 @@ "node_modules/hoist-non-react-statics/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/hook-std": { "version": "3.0.0", @@ -15926,6 +16054,11 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "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", @@ -17374,6 +17507,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", @@ -26424,6 +26565,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", diff --git a/package.json b/package.json index c127c441..22afed95 100644 --- a/package.json +++ b/package.json @@ -45,9 +45,11 @@ "@fortawesome/free-regular-svg-icons": "5.15.4", "@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/react-fontawesome": "0.2.0", + "@optimizely/react-sdk": "^2.9.2", "core-js": "3.31.1", "prop-types": "15.8.1", - "react-markdown": "^8.0.5" + "react-markdown": "^8.0.5", + "uuid": "9.0.0" }, "peerDependencies": { "@edx/frontend-platform": "^4.3.0 || ^5.0.0", @@ -59,8 +61,7 @@ "react-router": "5.2.1 || ^6.0.0", "react-router-dom": "5.3.0 || ^6.0.0", "redux": "4.1.2", - "regenerator-runtime": "0.13.11", - "uuid": "9.0.0" + "regenerator-runtime": "0.13.11" }, "devDependencies": { "@edx/browserslist-config": "^1.1.1", diff --git a/src/components/ToggleXpertButton/index.jsx b/src/components/ToggleXpertButton/index.jsx index d6ae3519..ba3cd51f 100644 --- a/src/components/ToggleXpertButton/index.jsx +++ b/src/components/ToggleXpertButton/index.jsx @@ -1,11 +1,18 @@ import PropTypes from 'prop-types'; -import { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; -import { Button, Icon, IconButton } from '@edx/paragon'; +import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; +import { + Button, + Icon, + IconButton, + ProductTour, +} from '@edx/paragon'; import { Close } from '@edx/paragon/icons'; import { ReactComponent as XpertLogo } from '../../assets/xpert-logo.svg'; +import { activateProductTourExperiment, trackChatBotLaunchOptimizely } from '../../utils/optimizelyExperiment'; import './index.scss'; const ToggleXpert = ({ @@ -15,15 +22,26 @@ const ToggleXpert = ({ contentToolsEnabled, }) => { const [hasDismissed, setHasDismissed] = useState(false); + const [showProductTourExp, setShowProductTourExp] = useState(false); + const { userId } = getAuthenticatedUser(); + + useEffect(() => { + const showProductTour = activateProductTourExperiment(userId.toString()); + setShowProductTourExp(showProductTour); + }, [userId, setShowProductTourExp]); + const handleClick = (event) => { // log event if the tool is opened if (!isOpen) { sendTrackEvent( - `edx.ui.lms.learning_assistant.launch${event.target.id === 'toggle-button' ? '' : '.cta-triggered'}`, + 'edx.ui.lms.learning_assistant.launch', { course_id: courseId, + user_id: userId, + source: event.target.id === 'toggle-button' ? 'toggle' : 'cta', }, ); + trackChatBotLaunchOptimizely(userId.toString()); } setIsOpen(!isOpen); }; @@ -38,51 +56,84 @@ const ToggleXpert = ({ }); }; + const handleProductTourEnd = () => { + setIsOpen(true); + localStorage.setItem('completedLearningAssistantTour', 'true'); + sendTrackEvent( + 'edx.ui.lms.learning_assistant.launch', + { + course_id: courseId, + user_id: userId, + source: 'product-tour', + }, + ); + trackChatBotLaunchOptimizely(userId.toString()); + }; + + const learningAssistantTour = { + tourId: 'learningAssistantTour', + endButtonText: 'Check it out', + onEnd: () => { handleProductTourEnd(); }, + enabled: !localStorage.getItem('completedLearningAssistantTour') && showProductTourExp, + checkpoints: [ + { + placement: 'left', + target: '#cta-button', + body: 'Xpert is a new part of your learning experience. ' + + 'You can ask questions and get tutoring help during your course.', + }, + ], + }; + return ( (!isOpen && ( -
- {!hasDismissed && ( -
- - -
- )} - -
+ {!hasDismissed && ( +
+ + +
+ )} + + + )) ); }; diff --git a/src/data/optimizely.js b/src/data/optimizely.js new file mode 100644 index 00000000..e9e6e7ba --- /dev/null +++ b/src/data/optimizely.js @@ -0,0 +1,11 @@ +import { + createInstance, +} from '@optimizely/react-sdk'; + +const OPTIMIZELY_SDK_KEY = process.env.OPTIMIZELY_FULL_STACK_SDK_KEY; + +const optimizely = createInstance({ + sdkKey: OPTIMIZELY_SDK_KEY, +}); + +export default optimizely; diff --git a/src/data/thunks.js b/src/data/thunks.js index 16bbab17..0fae4793 100644 --- a/src/data/thunks.js +++ b/src/data/thunks.js @@ -12,6 +12,7 @@ import { setDisclosureAcknowledged, setSidebarIsOpen, } from './slice'; +import { trackChatBotMessageOptimizely } from '../utils/optimizelyExperiment'; export function addChatMessage(role, content, courseId) { return (dispatch, getState) => { @@ -40,6 +41,10 @@ export function addChatMessage(role, content, courseId) { role: message.role, content: message.content, }); + + if (message.role === 'user') { + trackChatBotMessageOptimizely(userId.toString()); + } }; } diff --git a/src/utils/optimizelyExperiment.js b/src/utils/optimizelyExperiment.js new file mode 100644 index 00000000..1d192303 --- /dev/null +++ b/src/utils/optimizelyExperiment.js @@ -0,0 +1,26 @@ +import optimizelyInstance from '../data/optimizely'; + +const PRODUCT_TOUR_EXP_KEY = 'la_product_tour'; +const PRODUCT_TOUR_EXP_VARIATION = 'learning_assistant_product_tour'; + +const activateProductTourExperiment = (userId) => { + const variant = optimizelyInstance.activate( + PRODUCT_TOUR_EXP_KEY, + userId, + ); + return variant === PRODUCT_TOUR_EXP_VARIATION; +}; + +const trackChatBotLaunchOptimizely = (userId, userAttributes = {}) => { + optimizelyInstance.track('learning_assistant_chat_click', userId, userAttributes); +}; + +const trackChatBotMessageOptimizely = (userId, userAttributes = {}) => { + optimizelyInstance.track('learning_assistant_chat_message', userId, userAttributes); +}; + +export { + activateProductTourExperiment, + trackChatBotLaunchOptimizely, + trackChatBotMessageOptimizely, +};