From 619966f63864d6489a17b169e3f5b7c9dae420a3 Mon Sep 17 00:00:00 2001 From: rushtong Date: Tue, 12 Nov 2024 12:09:37 -0500 Subject: [PATCH 1/9] feat: add appcues support --- public/index.html | 10 ++++- src/components/SignInButton.tsx | 18 ++++----- src/libs/ajax/{Metrics.js => Metrics.ts} | 50 +++++++++++++++++++----- src/libs/auth/auth.ts | 23 +++++++++++ src/libs/events.js | 14 ------- src/libs/events.ts | 26 ++++++++++++ tsconfig.json | 3 +- types/index.d.ts | 4 ++ 8 files changed, 113 insertions(+), 35 deletions(-) rename src/libs/ajax/{Metrics.js => Metrics.ts} (54%) delete mode 100644 src/libs/events.js create mode 100644 src/libs/events.ts create mode 100644 types/index.d.ts diff --git a/public/index.html b/public/index.html index b5fcc0215..0de336d71 100644 --- a/public/index.html +++ b/public/index.html @@ -22,7 +22,6 @@ - + + + + + + Broad Data Use Oversight System diff --git a/src/components/SignInButton.tsx b/src/components/SignInButton.tsx index 8e762a8a8..bede9f788 100644 --- a/src/components/SignInButton.tsx +++ b/src/components/SignInButton.tsx @@ -9,7 +9,7 @@ import {Storage} from '../libs/storage'; import {Navigation, setUserRoleStatuses} from '../libs/utils'; import loadingIndicator from '../images/loading-indicator.svg'; import ReactTooltip from 'react-tooltip'; -import eventList from '../libs/events'; +import eventList, {MetricsEventName} from '../libs/events'; import {StackdriverReporter} from '../libs/stackdriverReporter'; import {History} from 'history'; import {OidcUser} from '../libs/auth/oidcBroker'; @@ -107,9 +107,9 @@ export const SignInButton = (props: SignInButtonProps) => { history.push(`/tos_acceptance${shouldRedirect ? `?redirectTo=${redirectTo}` : ''}`); }; - const syncSignInOrRegistrationEvent = async (event: String) => { + const syncSignInOrRegistrationEvent = async (event: MetricsEventName) => { Storage.setAnonymousId(); - await Metrics.identify(Storage.getAnonymousId()); + await Metrics.identify(`${Storage.getAnonymousId()}`); await Metrics.syncProfile(); await Metrics.captureEvent(event); }; @@ -195,10 +195,10 @@ export const SignInButton = (props: SignInButtonProps) => { className='navbar-duos-icon-help' style={{color: 'white', height: 16, width: 16, marginLeft: 5}} href='https://support.terra.bio/hc/en-us/articles/28504837523995-How-to-Register-for-DUOS' - data-for="tip_google-help" - data-tip="Need account help? Click here!" + data-for='tip_google-help' + data-tip='Need account help? Click here!' /> - + ); }; @@ -209,10 +209,10 @@ export const SignInButton = (props: SignInButtonProps) => { ?
{signInElement()}
- :
+ :
setErrorDisplay({})} diff --git a/src/libs/ajax/Metrics.js b/src/libs/ajax/Metrics.ts similarity index 54% rename from src/libs/ajax/Metrics.js rename to src/libs/ajax/Metrics.ts index 305781817..24955847d 100644 --- a/src/libs/ajax/Metrics.js +++ b/src/libs/ajax/Metrics.ts @@ -1,15 +1,24 @@ -import axios from 'axios'; +import axios, {AxiosRequestConfig} from 'axios'; import {getDefaultProperties} from '@databiosphere/bard-client'; import {Storage} from '../storage'; import {getBardApiUrl} from '../ajax'; import {Token} from '../config'; +import {MetricsEventName} from 'src/libs/events'; + +// Set default timeout for all metrics calls to 30 seconds +const defaultSignal: AbortSignal = AbortSignal.timeout(30000); export const Metrics = { - captureEvent: (event, details, signal) => captureEventFn(event, details, signal).catch(() => { + captureEvent: ( + event: MetricsEventName, + details: Record = {}, + signal: AbortSignal = defaultSignal, + refreshAppcues: boolean = true + ) => captureEventFn(event, details, signal, refreshAppcues).catch(() => { }), - syncProfile: (signal) => syncProfile(signal), - identify: (anonId, signal) => identify(anonId, signal), + syncProfile: (signal: AbortSignal = defaultSignal) => syncProfile(signal), + identify: (anonId: String, signal: AbortSignal = defaultSignal) => identify(anonId, signal), }; /** @@ -18,12 +27,19 @@ export const Metrics = { * @param {string} event - The event name. * @param {Object} [details={}] - The event details. * @param {AbortSignal} [signal] - The abort signal. + * @param refreshAppcues - The refresh Appcues flag. * @returns {Promise} - A Promise that resolves when the event is captured. */ -const captureEventFn = async (event, details = {}, signal) => { +const captureEventFn = async (event: MetricsEventName, details: object = {}, signal: AbortSignal, refreshAppcues: boolean): Promise => { const isSignedIn = Storage.userIsLogged(); const isRegistered = isSignedIn && Storage.getCurrentUser(); + // Send event to Appcues and refresh Appcues state + window.Appcues?.track(event); + if (refreshAppcues) { + window.Appcues?.page(); + } + if (!isRegistered && !Storage.getAnonymousId()) { Storage.setAnonymousId(); } @@ -40,7 +56,7 @@ const captureEventFn = async (event, details = {}, signal) => { }, }; - const config = { + const config: AxiosRequestConfig = { method: 'POST', url: `${await getBardApiUrl()}/api/event`, data: body, @@ -57,8 +73,8 @@ const captureEventFn = async (event, details = {}, signal) => { * @param {AbortSignal} [signal] - The abort signal. * @returns {Promise} - A Promise that resolves when the profile is synced. */ -const syncProfile = async (signal) => { - const config = { +const syncProfile = async (signal: AbortSignal): Promise => { + const config: AxiosRequestConfig = { method: 'POST', url: `${await getBardApiUrl()}/api/syncProfile`, headers: {Authorization: `Bearer ${Token.getToken()}`}, @@ -76,10 +92,24 @@ const syncProfile = async (signal) => { * @param {AbortSignal} [signal] - The abort signal. * @returns {Promise} - A Promise that resolves when the user is identified. */ -const identify = async (anonId, signal) => { +const identify = async (anonId: String, signal: AbortSignal): Promise => { const body = {anonId}; - const config = { + if (window.Appcues) { + const user = Storage.getCurrentUser(); + const createDate = user.createDate ? user.createDate : new Date().getTime(); + const appcuesProps = { + dateJoined: createDate, + app: 'DUOS' + }; + if (user.userStatusInfo?.userSubjectId) { + window.Appcues.identify(user.userStatusInfo.userSubjectId, appcuesProps); + } else { + window.Appcues.identify(`${user.userId}`, appcuesProps); + } + } + + const config: AxiosRequestConfig = { method: 'POST', url: `${await getBardApiUrl()}/api/identify`, data: body, diff --git a/src/libs/auth/auth.ts b/src/libs/auth/auth.ts index 2b3ff8272..bc1b9e61c 100644 --- a/src/libs/auth/auth.ts +++ b/src/libs/auth/auth.ts @@ -5,6 +5,7 @@ import {OidcBroker, OidcUser} from './oidcBroker'; import {Storage} from './../storage'; import {UserManager} from 'oidc-client-ts'; +import {MetricsEventName} from '../events'; export const Auth = { signInError: () => { @@ -42,3 +43,25 @@ export const Auth = { await OidcBroker.signOut(); }, }; + +// extending Window interface to access Appcues +declare global { + interface Window { + Appcues?: { + /** Identifies the current user with an ID and an optional set of properties. */ + identify: (userId: string, properties?: any) => void; + /** Notifies the SDK that the state of the application has changed. */ + page: () => void; + /** Forces specific Appcues content to appear for the current user by passing in the ID. */ + show: (contentId: string) => void; + /** Fire the callback function when the given event is triggered by the SDK */ + on: ((eventName: Exclude, callbackFn: (event: any) => void | Promise) => void) & + ((eventName: 'all', callbackFn: (eventName: string, event: any) => void | Promise) => void); + /** Clears all known information about the current user in this session */ + reset: () => void; + /** Tracks a custom event (by name) taken by the current user. */ + track: (eventName: MetricsEventName) => void; + }; + forceSignIn: any; + } +} diff --git a/src/libs/events.js b/src/libs/events.js deleted file mode 100644 index cce8efa84..000000000 --- a/src/libs/events.js +++ /dev/null @@ -1,14 +0,0 @@ -/* - * NOTE: See the Mixpanel guide in the terra-ui GitHub Wiki for more details: - * https://github.com/DataBiosphere/terra-ui/wiki/Mixpanel - */ -const eventList = { - userRegister: 'user:register', - userSignIn: 'user:signin', - - pageView: 'page:view', - dataLibrary: 'page:view:data-library', - dar: 'page:view:dar' -}; - -export default eventList; diff --git a/src/libs/events.ts b/src/libs/events.ts new file mode 100644 index 000000000..6929c1722 --- /dev/null +++ b/src/libs/events.ts @@ -0,0 +1,26 @@ +/* + * NOTE: See the Mixpanel guide in the terra-ui GitHub Wiki for more details: + * https://github.com/DataBiosphere/terra-ui/wiki/Mixpanel + */ +const eventList = { + userRegister: 'user:register', + userSignIn: 'user:signin', + + pageView: 'page:view', + dataLibrary: 'page:view:data-library', + dar: 'page:view:dar' +}; + +export default eventList; + +// Helper type to create BaseMetricsEventName. +type MetricsEventsMap = { [key: string]: EventName | MetricsEventsMap }; +// Union type of all event names configured in eventsList. +type BaseMetricsEventName = typeof eventList extends MetricsEventsMap ? EventName : never; +// Each route has its own page view event, where the event name includes the name of the route. +type PageViewMetricsEventName = `${typeof eventList.pageView}:${string}`; + +/** + * Union type of all metrics event names. + */ +export type MetricsEventName = BaseMetricsEventName | PageViewMetricsEventName; diff --git a/tsconfig.json b/tsconfig.json index 99002350f..f3d6e5ce6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,8 @@ } }, "include": [ - "src" + "src", + "types/index.d.ts" ], "plugins": ["@typescript-eslint", "import"] } diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 000000000..77bd64b77 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,4 @@ + +declare module "@databiosphere/bard-client" { + export function getDefaultProperties(): any +} From 058918ebc0c140ad55fc04ec2fe567fee462e71e Mon Sep 17 00:00:00 2001 From: rushtong Date: Tue, 12 Nov 2024 12:16:56 -0500 Subject: [PATCH 2/9] feat: add types to docker build --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 41ce0894b..4f3282e5b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,7 @@ ENV PATH /usr/src/app/node_modules/.bin:$PATH # install and cache app dependencies COPY src /usr/src/app/src +COPY types /usr/src/app/types COPY public /usr/src/app/public COPY package.json /usr/src/app/package.json COPY package-lock.json /usr/src/app/package-lock.json From c020243c10d6a20663b1b6164a8459b05c417025 Mon Sep 17 00:00:00 2001 From: rushtong Date: Wed, 13 Nov 2024 08:57:36 -0500 Subject: [PATCH 3/9] feat: basic tests for metrics class --- cypress/component/utils/metrics.spec.ts | 42 +++++++++++++++++++++++++ cypress/support/component-index.html | 5 ++- 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 cypress/component/utils/metrics.spec.ts diff --git a/cypress/component/utils/metrics.spec.ts b/cypress/component/utils/metrics.spec.ts new file mode 100644 index 000000000..939d2410f --- /dev/null +++ b/cypress/component/utils/metrics.spec.ts @@ -0,0 +1,42 @@ +/* eslint-disable no-undef */ +import {Metrics} from '../../../src/libs/ajax/Metrics'; +import eventList from '../../../src/libs/events'; + +describe('Metrics Tests', function () { + + // Intercept configuration calls + beforeEach(() => { + cy.intercept({ + method: 'GET', + url: '/config.json', + hostname: 'localhost', + }, {'env': 'ci'}); + }); + + Cypress._.each(Object.keys(eventList), (eventType) => { + it(`Captures ${eventType} Event`, function () { + cy.intercept('**/event').as('event'); + Metrics.captureEvent(eventType); + cy.wait('@event').then(interception => { + expect(interception).to.exist; + }); + }); + }); + + it(`Sync Profile`, function () { + cy.intercept('**/syncProfile').as('sync'); + Metrics.syncProfile(); + cy.wait('@sync').then(interception => { + expect(interception).to.exist; + }); + }); + + it(`Identify`, function () { + cy.intercept('**/identify').as('identify'); + Metrics.identify('anonymousId'); + cy.wait('@identify').then(interception => { + expect(interception).to.exist; + }); + }); + +}); diff --git a/cypress/support/component-index.html b/cypress/support/component-index.html index 23b2efe9d..f00558ec8 100644 --- a/cypress/support/component-index.html +++ b/cypress/support/component-index.html @@ -5,8 +5,11 @@ - Components App + + From 052e03b426f3389fcb404bc1e1ac6752ea336a42 Mon Sep 17 00:00:00 2001 From: rushtong Date: Wed, 13 Nov 2024 09:58:24 -0500 Subject: [PATCH 4/9] formatting --- types/index.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index 77bd64b77..5587585ee 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,4 +1,3 @@ - declare module "@databiosphere/bard-client" { export function getDefaultProperties(): any } From 554e21fed1bef3a191daa285fba21f28f7a46d80 Mon Sep 17 00:00:00 2001 From: rushtong Date: Wed, 13 Nov 2024 09:59:26 -0500 Subject: [PATCH 5/9] doc update --- DEVNOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DEVNOTES.md b/DEVNOTES.md index 4173d00bb..de23c8622 100644 --- a/DEVNOTES.md +++ b/DEVNOTES.md @@ -69,6 +69,8 @@ docker build . -t duos docker compose up -d ``` +Visit https://local.dsde-dev.broadinstitute.org/ to see the instance running under docker. + # Testing ## Cypress Tests From 6a5750e538dc17b5646c57456de9d630ededc10a Mon Sep 17 00:00:00 2001 From: rushtong Date: Wed, 13 Nov 2024 11:46:33 -0500 Subject: [PATCH 6/9] pr feedback --- src/libs/ajax/Metrics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ajax/Metrics.ts b/src/libs/ajax/Metrics.ts index 24955847d..8e4bb1609 100644 --- a/src/libs/ajax/Metrics.ts +++ b/src/libs/ajax/Metrics.ts @@ -30,7 +30,7 @@ export const Metrics = { * @param refreshAppcues - The refresh Appcues flag. * @returns {Promise} - A Promise that resolves when the event is captured. */ -const captureEventFn = async (event: MetricsEventName, details: object = {}, signal: AbortSignal, refreshAppcues: boolean): Promise => { +const captureEventFn = async (event: MetricsEventName, details: {} = {}, signal: AbortSignal, refreshAppcues: boolean): Promise => { const isSignedIn = Storage.userIsLogged(); const isRegistered = isSignedIn && Storage.getCurrentUser(); From 5552e3d06ca09e9a1d6b4e09d46acaff7b8a8664 Mon Sep 17 00:00:00 2001 From: rushtong Date: Wed, 13 Nov 2024 11:48:56 -0500 Subject: [PATCH 7/9] formatting --- cypress/component/utils/metrics.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cypress/component/utils/metrics.spec.ts b/cypress/component/utils/metrics.spec.ts index 939d2410f..b927a4cb6 100644 --- a/cypress/component/utils/metrics.spec.ts +++ b/cypress/component/utils/metrics.spec.ts @@ -23,7 +23,7 @@ describe('Metrics Tests', function () { }); }); - it(`Sync Profile`, function () { + it('Sync Profile', function () { cy.intercept('**/syncProfile').as('sync'); Metrics.syncProfile(); cy.wait('@sync').then(interception => { @@ -31,7 +31,7 @@ describe('Metrics Tests', function () { }); }); - it(`Identify`, function () { + it('Identify', function () { cy.intercept('**/identify').as('identify'); Metrics.identify('anonymousId'); cy.wait('@identify').then(interception => { From cda0778d916a09159f779bd256a0bfa503f2a2be Mon Sep 17 00:00:00 2001 From: rushtong Date: Fri, 15 Nov 2024 09:46:59 -0500 Subject: [PATCH 8/9] feat: use the same id we send to bard --- src/libs/ajax/Metrics.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/libs/ajax/Metrics.ts b/src/libs/ajax/Metrics.ts index 8e4bb1609..8e482df9b 100644 --- a/src/libs/ajax/Metrics.ts +++ b/src/libs/ajax/Metrics.ts @@ -102,11 +102,7 @@ const identify = async (anonId: String, signal: AbortSignal): Promise => { dateJoined: createDate, app: 'DUOS' }; - if (user.userStatusInfo?.userSubjectId) { - window.Appcues.identify(user.userStatusInfo.userSubjectId, appcuesProps); - } else { - window.Appcues.identify(`${user.userId}`, appcuesProps); - } + window.Appcues.identify(`${Storage.getAnonymousId()}`, appcuesProps); } const config: AxiosRequestConfig = { From 8f3f267a60192123509ed063e4f83cffc6921d81 Mon Sep 17 00:00:00 2001 From: rushtong Date: Fri, 15 Nov 2024 11:24:14 -0500 Subject: [PATCH 9/9] feat: use the oidc profile sub value which is what Terra does --- src/libs/ajax/Metrics.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/ajax/Metrics.ts b/src/libs/ajax/Metrics.ts index 8e482df9b..a82b38e8b 100644 --- a/src/libs/ajax/Metrics.ts +++ b/src/libs/ajax/Metrics.ts @@ -97,12 +97,13 @@ const identify = async (anonId: String, signal: AbortSignal): Promise => { if (window.Appcues) { const user = Storage.getCurrentUser(); + const oidcSub = Storage.getOidcUser()?.profile?.sub || Storage.getAnonymousId(); const createDate = user.createDate ? user.createDate : new Date().getTime(); const appcuesProps = { dateJoined: createDate, app: 'DUOS' }; - window.Appcues.identify(`${Storage.getAnonymousId()}`, appcuesProps); + window.Appcues.identify(oidcSub, appcuesProps); } const config: AxiosRequestConfig = {