diff --git a/src/browser/modules/Stream/PlayFrame.tsx b/src/browser/modules/Stream/PlayFrame.tsx index 52ce627ce84..247210fa0fe 100644 --- a/src/browser/modules/Stream/PlayFrame.tsx +++ b/src/browser/modules/Stream/PlayFrame.tsx @@ -47,7 +47,7 @@ import { isConnectedAuraHost } from 'shared/modules/connections/connectionsDuck' import { getEdition, isEnterprise } from 'shared/modules/dbMeta/dbMetaDuck' import { DARK_THEME } from 'shared/modules/settings/settingsDuck' import { LAST_GUIDE_SLIDE } from 'shared/modules/udc/udcDuck' -import { PreviewFrame } from './StartPreviewFrame' +import PreviewFrame from './StartPreviewFrame' const AuraPromotion = () => { const theme = useContext(ThemeContext) diff --git a/src/browser/modules/Stream/StartPreviewFrame.tsx b/src/browser/modules/Stream/StartPreviewFrame.tsx index e43569948a8..7935d8b7606 100644 --- a/src/browser/modules/Stream/StartPreviewFrame.tsx +++ b/src/browser/modules/Stream/StartPreviewFrame.tsx @@ -17,7 +17,11 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -import React from 'react' +import React, { Dispatch } from 'react' +import { Action } from 'redux' +import { trackNavigateToPreview } from 'shared/modules/preview/previewDuck' +import { connect } from 'react-redux' +import { withBus } from 'react-suber' export const navigateToPreview = (): void => { const path = window.location.pathname @@ -26,7 +30,15 @@ export const navigateToPreview = (): void => { } } -export const PreviewFrame = () => { +type PreviewFrameProps = { + executeTrackNavigateToPreview: () => void +} +const PreviewFrame = ({ executeTrackNavigateToPreview }: PreviewFrameProps) => { + function trackAndNavigateToPreview() { + executeTrackNavigateToPreview() + navigateToPreview() + } + return ( <>
@@ -36,7 +48,10 @@ export const PreviewFrame = () => {

Switch to the preview experience to access all the latest features.

-
@@ -86,3 +101,11 @@ export const PreviewFrame = () => { ) } + +const mapDispatchToProps = (dispatch: Dispatch) => { + return { + executeTrackNavigateToPreview: () => dispatch(trackNavigateToPreview()) + } +} + +export default withBus(connect(null, mapDispatchToProps)(PreviewFrame)) diff --git a/src/shared/modules/dbMeta/dbMetaEpics.ts b/src/shared/modules/dbMeta/dbMetaEpics.ts index 3bfeaf7472f..c3ee5ca6750 100644 --- a/src/shared/modules/dbMeta/dbMetaEpics.ts +++ b/src/shared/modules/dbMeta/dbMetaEpics.ts @@ -97,6 +97,7 @@ import { getCurrentDatabase } from 'shared/utils/selectors' import { isBoltConnectionErrorCode } from 'services/bolt/boltConnectionErrors' +import { trackPageLoad } from '../preview/previewDuck' function handleConnectionError(store: any, e: any) { if (!e.code || isBoltConnectionErrorCode(e.code)) { @@ -546,6 +547,12 @@ export const serverConfigEpic = (some$: any, store: any) => store.dispatch(triggerCredentialsTimeout()) } + setTimeout(() => { + // Track page load after server config is done + // setTimeout ensures telemetry settings have been propagated to the App + store.dispatch(trackPageLoad()) + }) + return Rx.Observable.of(null) }) }) diff --git a/src/shared/modules/preview/previewDuck.test.ts b/src/shared/modules/preview/previewDuck.test.ts new file mode 100644 index 00000000000..c86c21c145a --- /dev/null +++ b/src/shared/modules/preview/previewDuck.test.ts @@ -0,0 +1,154 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +import { createBus, createReduxMiddleware } from 'suber' +import configureMockStore, { MockStoreEnhanced } from 'redux-mock-store' +import { + PREVIEW_EVENT, + trackNavigateToPreview, + trackPageLoad +} from './previewDuck' + +describe('previewDuck tests', () => { + let store: MockStoreEnhanced + const bus = createBus() + const mockStore = configureMockStore([createReduxMiddleware(bus)]) + + beforeAll(() => { + store = mockStore() + }) + + afterEach(() => { + bus.reset() + store.clearActions() + localStorage.clear() + }) + + test('trackNavigateToPreview sends a PREVIEW_EVENT', done => { + const action = trackNavigateToPreview() + + bus.take(PREVIEW_EVENT, () => { + // Then + const [action] = store.getActions() + expect(action).toEqual({ + type: PREVIEW_EVENT, + label: 'PREVIEW_UI_SWITCH', + data: { + switchedTo: 'preview', + timeSinceLastSwitch: null + } + }) + done() + }) + + // When + store.dispatch(action) + }) + + test('trackNavigateToPreview sets hasTriedPreviewUI', done => { + localStorage.setItem('hasTriedPreviewUI', 'false') + const action = trackNavigateToPreview() + + bus.take(PREVIEW_EVENT, () => { + // Then + const hasTriedPreviewUI = localStorage.getItem('hasTriedPreviewUI') + expect(hasTriedPreviewUI).toBe('true') + done() + }) + + // When + store.dispatch(action) + }) + + test('trackNavigateToPreview sends correct timeSinceLastSwitch when timeSinceLastSwitchMs is unset', done => { + const action = trackNavigateToPreview() + + bus.take(PREVIEW_EVENT, () => { + // Then + const [action] = store.getActions() + expect(action.data.timeSinceLastSwitch).toBeNull() + done() + }) + + // When + store.dispatch(action) + }) + + test('trackNavigateToPreview sends correct timeSinceLastSwitch when timeSinceLastSwitchMs has been set', done => { + localStorage.setItem('timeSinceLastSwitchMs', Date.now().toString()) + const action = trackNavigateToPreview() + + bus.take(PREVIEW_EVENT, () => { + // Then + const [action] = store.getActions() + expect(action.data.timeSinceLastSwitch).not.toBeNull() + done() + }) + + // When + store.dispatch(action) + }) + + test('trackPageLoad sends a PREVIEW_EVENT', done => { + const action = trackPageLoad() + + bus.take(PREVIEW_EVENT, () => { + // Then + const [action] = store.getActions() + expect(action).toEqual({ + type: PREVIEW_EVENT, + label: 'PREVIEW_PAGE_LOAD', + data: { previewUI: false, hasTriedPreviewUI: false } + }) + done() + }) + + // When + store.dispatch(action) + }) + + test('trackPageLoad sends correct hasTriedPreviewUI value when flag is unset', done => { + const action = trackPageLoad() + + bus.take(PREVIEW_EVENT, () => { + // Then + const [action] = store.getActions() + expect(action.data.hasTriedPreviewUI).toBeFalsy() + done() + }) + + // When + store.dispatch(action) + }) + + test('trackPageLoad sends correct hasTriedPreviewUI value when flag is set', done => { + localStorage.setItem('hasTriedPreviewUI', 'true') + const action = trackPageLoad() + + bus.take(PREVIEW_EVENT, () => { + // Then + const [action] = store.getActions() + expect(action.data.hasTriedPreviewUI).toBeTruthy() + done() + }) + + // When + store.dispatch(action) + }) +}) diff --git a/src/shared/modules/preview/previewDuck.ts b/src/shared/modules/preview/previewDuck.ts new file mode 100644 index 00000000000..ce828d88010 --- /dev/null +++ b/src/shared/modules/preview/previewDuck.ts @@ -0,0 +1,66 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +export const PREVIEW_EVENT = 'preview/PREVIEW_EVENT' + +interface PreviewEventAction { + type: typeof PREVIEW_EVENT + label: string + data: + | { + switchedTo: 'preview' | 'classic' + timeSinceLastSwitch: number | null + } + | { + previewUI: boolean + hasTriedPreviewUI: boolean + } +} + +export const trackNavigateToPreview = (): PreviewEventAction => { + const now = Date.now() + localStorage.setItem('hasTriedPreviewUI', 'true') + + const timeSinceLastSwitchMs = localStorage.getItem('timeSinceLastSwitchMs') + localStorage.setItem('timeSinceLastSwitchMs', now.toString()) + + let timeSinceLastSwitch = null + if (timeSinceLastSwitchMs !== null) { + timeSinceLastSwitch = now - parseInt(timeSinceLastSwitchMs) + } + + return { + type: PREVIEW_EVENT, + label: 'PREVIEW_UI_SWITCH', + data: { + switchedTo: 'preview', + timeSinceLastSwitch: timeSinceLastSwitch + } + } +} + +export const trackPageLoad = (): PreviewEventAction => { + const hasTriedPreviewUI = localStorage.getItem('hasTriedPreviewUI') === 'true' + + return { + type: PREVIEW_EVENT, + label: 'PREVIEW_PAGE_LOAD', + data: { previewUI: false, hasTriedPreviewUI } + } +} diff --git a/src/shared/modules/udc/udcDuck.ts b/src/shared/modules/udc/udcDuck.ts index a0827ac90a1..ac84f6eaf63 100644 --- a/src/shared/modules/udc/udcDuck.ts +++ b/src/shared/modules/udc/udcDuck.ts @@ -57,6 +57,7 @@ import { TRACK_CANNY_FEATURE_REQUEST } from 'shared/modules/sidebar/sidebarDuck' import cmdHelper from 'shared/services/commandInterpreterHelper' +import { PREVIEW_EVENT } from '../preview/previewDuck' // Action types export const NAME = 'udc' @@ -324,3 +325,13 @@ export const trackErrorFramesEpic: Epic = ( } }) .ignoreElements() + +export const trackPreviewEpic: Epic = action$ => { + return action$.ofType(PREVIEW_EVENT).map((action: any) => { + return metricsEvent({ + category: 'preview', + label: action.label, + data: action.data + }) + }) +} diff --git a/src/shared/rootEpic.ts b/src/shared/rootEpic.ts index e7e4f9a9d49..2b375b6e55f 100644 --- a/src/shared/rootEpic.ts +++ b/src/shared/rootEpic.ts @@ -89,6 +89,7 @@ import { import { trackCommandUsageEpic, trackErrorFramesEpic, + trackPreviewEpic, trackReduxActionsEpic, udcStartupEpic } from './modules/udc/udcDuck' @@ -148,5 +149,6 @@ export default combineEpics( trackReduxActionsEpic, initializeCypherEditorEpic, updateEditorSupportSchemaEpic, - fetchRemoteGuideEpic + fetchRemoteGuideEpic, + trackPreviewEpic )