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
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
diff --git a/cypress/component/utils/metrics.spec.ts b/cypress/component/utils/metrics.spec.ts
new file mode 100644
index 000000000..b927a4cb6
--- /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
+
+
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 6de3cd37f..46c78ce38 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,10 +107,10 @@ export const SignInButton = (props: SignInButtonProps) => {
history.push(`/tos_acceptance${shouldRedirect ? `?redirectTo=${redirectTo}` : ''}`);
};
- const syncSignInOrRegistrationEvent = async (event: String) => {
+ const syncSignInOrRegistrationEvent = async (event: MetricsEventName) => {
Storage.setAnonymousId();
// noinspection ES6MissingAwait
- Metrics.identify(Storage.getAnonymousId());
+ Metrics.identify(`${Storage.getAnonymousId()}`);
// noinspection ES6MissingAwait
Metrics.syncProfile();
// noinspection ES6MissingAwait
@@ -198,10 +198,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!'
/>
-
+
);
};
@@ -212,10 +212,10 @@ export const SignInButton = (props: SignInButtonProps) => {
?
{signInElement()}
- :
+ :
setErrorDisplay({})}
diff --git a/src/libs/ajax/Metrics.js b/src/libs/ajax/Metrics.ts
similarity index 55%
rename from src/libs/ajax/Metrics.js
rename to src/libs/ajax/Metrics.ts
index 305781817..a82b38e8b 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: {} = {}, 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,21 @@ 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 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(oidcSub, 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..5587585ee
--- /dev/null
+++ b/types/index.d.ts
@@ -0,0 +1,3 @@
+declare module "@databiosphere/bard-client" {
+ export function getDefaultProperties(): any
+}