From 83eb87fb55853a503acc2a3bd78c71633b35c508 Mon Sep 17 00:00:00 2001 From: David Russell Date: Wed, 30 Oct 2024 16:57:39 +0000 Subject: [PATCH] refactor preview events to use redux --- src/browser/modules/App/App.tsx | 77 +++++---- src/browser/modules/Stream/PlayFrame.tsx | 24 +-- .../modules/Stream/StartPreviewFrame.tsx | 69 +++----- .../modules/preview/previewDuck.test.ts | 156 ++++++++++++++++++ src/shared/modules/preview/previewDuck.ts | 74 +++++++++ 5 files changed, 304 insertions(+), 96 deletions(-) create mode 100644 src/shared/modules/preview/previewDuck.test.ts create mode 100644 src/shared/modules/preview/previewDuck.ts diff --git a/src/browser/modules/App/App.tsx b/src/browser/modules/App/App.tsx index 611726acb4..1d07c5e67a 100644 --- a/src/browser/modules/App/App.tsx +++ b/src/browser/modules/App/App.tsx @@ -18,7 +18,7 @@ * along with this program. If not, see . */ import { setEditorTheme } from 'neo4j-arc/cypher-language-support' -import React, { useEffect, useRef } from 'react' +import React, { useCallback, useEffect, useRef } from 'react' import { connect } from 'react-redux' import { withBus } from 'react-suber' import { ThemeProvider } from 'styled-components' @@ -96,6 +96,10 @@ import { updateUdcData } from 'shared/modules/udc/udcDuck' import { getTelemetrySettings } from 'shared/utils/selectors' +import { + PREVIEW_EVENT, + trackPageLoad +} from 'shared/modules/preview/previewDuck' export const MAIN_WRAPPER_DOM_ID = 'MAIN_WRAPPER_DOM_ID' @@ -119,30 +123,45 @@ export function App(props: any) { const eventMetricsCallback = useRef((_: MetricsData) => _) const segmentTrackCallback = useRef((_: MetricsData) => _) + const invokeTrackingCallbacks = useCallback( + ({ category, label, data }: MetricsData) => { + if (!isRunningE2ETest() && props.telemetrySettings.allowUserStats) { + const extendedData = { + browserVersion: version, + neo4jEdition: props.edition, + connectedTo: props.connectedTo, + ...data + } + + eventMetricsCallback && + eventMetricsCallback.current && + eventMetricsCallback.current({ category, label, data: extendedData }) + + segmentTrackCallback && + segmentTrackCallback.current && + segmentTrackCallback.current({ category, label, data: extendedData }) + } + }, + [props.telemetrySettings.allowUserStats] + ) + useEffect(() => { const unsub = props.bus && - props.bus.take( - METRICS_EVENT, - ({ category, label, data: originalData }: MetricsData) => { - if (!isRunningE2ETest() && props.telemetrySettings.allowUserStats) { - const data = { - browserVersion: version, - neo4jEdition: props.edition, - connectedTo: props.connectedTo, - ...originalData - } - eventMetricsCallback && - eventMetricsCallback.current && - eventMetricsCallback.current({ category, label, data }) - segmentTrackCallback && - segmentTrackCallback.current && - segmentTrackCallback.current({ category, label, data }) - } - } - ) + props.bus.take(METRICS_EVENT, (metricsData: MetricsData) => { + invokeTrackingCallbacks(metricsData) + }) return () => unsub && unsub() - }, [props.telemetrySettings.allowUserStats, props.bus]) + }, [props.bus, invokeTrackingCallbacks]) + + useEffect(() => { + const unsub = + props.bus && + props.bus.take(PREVIEW_EVENT, (metricsData: MetricsData) => { + invokeTrackingCallbacks(metricsData) + }) + return () => unsub && unsub() + }, [props.bus, invokeTrackingCallbacks]) useEffect(() => { const initAction = udcInit() @@ -150,18 +169,9 @@ export function App(props: any) { }, [props.bus]) useEffect(() => { - if (!isRunningE2ETest() && props.telemetrySettings.allowUserStats) { - const hasTriedPreviewUI = - localStorage.getItem('hasTriedPreviewUI') === 'true' - segmentTrackCallback && - segmentTrackCallback.current && - segmentTrackCallback.current({ - category: 'preview', - label: 'PREVIEW_PAGE_LOAD', - data: { previewUI: false, hasTriedPreviewUI } - }) - } - }, [props.telemetrySettings.allowUserStats]) + const pageLoadAction = trackPageLoad() + props.bus && props.bus.send(pageLoadAction.type, pageLoadAction) + }, [props.bus, props.telemetrySettings.allowUserStats]) const { browserSyncAuthStatus, @@ -195,6 +205,7 @@ export function App(props: any) { }, [titleString]) const wrapperClassNames = codeFontLigatures ? '' : 'disable-font-ligatures' + return ( { const theme = useContext(ThemeContext) @@ -90,15 +89,13 @@ type PlayFrameProps = { showPromotion: boolean isFullscreen: boolean isCollapsed: boolean - telemetrySettings: TelemetrySettings } export function PlayFrame({ stack, bus, showPromotion, isFullscreen, - isCollapsed, - telemetrySettings + isCollapsed }: PlayFrameProps): JSX.Element { const [stackIndex, setStackIndex] = useState(0) const [atSlideStart, setAtSlideStart] = useState(null) @@ -127,8 +124,7 @@ export function PlayFrame({ bus, onSlide, initialPlay, - showPromotion, - telemetrySettings + showPromotion ) if (stillMounted) { setInitialPlay(false) @@ -211,8 +207,7 @@ function generateContent( bus: Bus, onSlide: any, shouldUseSlidePointer: boolean, - showPromotion = false, - telemetrySettings: TelemetrySettings + showPromotion = false ): Content | Promise { // Not found if (stackFrame.response && stackFrame.response.status === 404) { @@ -301,15 +296,11 @@ function generateContent( const updatedContent = isPlayStart && showPromotion ? ( <> - {isPreviewAvailable ? ( - - ) : ( - content - )} + {isPreviewAvailable ? : content} ) : isPreviewAvailable ? ( - + ) : ( content ) @@ -392,8 +383,7 @@ const mapStateToProps = (state: GlobalState) => ({ (getEdition(state) !== null && !isEnterprise(state) && !isConnectedAuraHost(state)) || - inDesktop(state), - telemetrySettings: getTelemetrySettings(state) + inDesktop(state) }) export default connect(mapStateToProps)(withBus(PlayFrame)) diff --git a/src/browser/modules/Stream/StartPreviewFrame.tsx b/src/browser/modules/Stream/StartPreviewFrame.tsx index ae3c36d01a..904d8ff270 100644 --- a/src/browser/modules/Stream/StartPreviewFrame.tsx +++ b/src/browser/modules/Stream/StartPreviewFrame.tsx @@ -17,10 +17,11 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -import React, { useRef } from 'react' -import { isRunningE2ETest } from 'services/utils' -import { TelemetrySettings } from 'shared/utils/selectors' -import { MetricsData } from '../Segment' +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 @@ -29,50 +30,14 @@ export const navigateToPreview = (): void => { } } -const useTrackAndNavigateToPreview = ( - telemetrySettings: TelemetrySettings -): (() => void) => { - const segmentTrackCallback = useRef((_: MetricsData) => _) - const path = window.location.pathname - - return () => { - if (!path.endsWith('/preview/')) { - if (!isRunningE2ETest() && telemetrySettings.allowUserStats) { - const now = Date.now() - localStorage.setItem('hasTriedPreviewUI', 'true') - - const timeSinceLastSwitchMs = - localStorage.getItem('timeSinceLastSwitchMs') ?? null - localStorage.setItem('timeSinceLastSwitchMs', now.toString()) - - let timeSinceLastSwitch = null - if (timeSinceLastSwitchMs !== null) { - timeSinceLastSwitch = now - parseInt(timeSinceLastSwitchMs) - } - - segmentTrackCallback && - segmentTrackCallback.current && - segmentTrackCallback.current({ - category: 'preview', - label: 'PREVIEW_UI_SWITCH', - data: { - switchedTo: 'preview', - timeSinceLastSwitch: timeSinceLastSwitch ?? 0 - } - }) - } - - navigateToPreview() - } - } -} - type PreviewFrameProps = { - telemetrySettings: TelemetrySettings + executeTrackNavigateToPreview: () => void } -export const PreviewFrame = ({ telemetrySettings }: PreviewFrameProps) => { - const trackAndNavigateToPreview = - useTrackAndNavigateToPreview(telemetrySettings) +const PreviewFrame = ({ executeTrackNavigateToPreview }: PreviewFrameProps) => { + function trackAndNavigateToPreview() { + executeTrackNavigateToPreview() + navigateToPreview() + } return ( <> @@ -136,3 +101,15 @@ export const PreviewFrame = ({ telemetrySettings }: PreviewFrameProps) => { ) } + +const mapDispatchToProps = (dispatch: Dispatch) => { + return { + executeTrackNavigateToPreview: () => dispatch(trackNavigateToPreview()) + } +} + +export default withBus( + connect(() => { + return {} + }, mapDispatchToProps)(PreviewFrame) +) diff --git a/src/shared/modules/preview/previewDuck.test.ts b/src/shared/modules/preview/previewDuck.test.ts new file mode 100644 index 0000000000..a70f01f82d --- /dev/null +++ b/src/shared/modules/preview/previewDuck.test.ts @@ -0,0 +1,156 @@ +/* + * 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, + category: 'preview', + label: '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, + category: 'preview', + label: '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 0000000000..d420a46ba3 --- /dev/null +++ b/src/shared/modules/preview/previewDuck.ts @@ -0,0 +1,74 @@ +/* + * 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 PreviewUiSwitchAction { + type: typeof PREVIEW_EVENT + category: 'preview' + label: string + data: { + switchedTo: 'preview' | 'classic' + timeSinceLastSwitch: number | null + } +} + +interface PreviewPageLoadAction { + type: typeof PREVIEW_EVENT + category: 'preview' + label: string + data: { + previewUI: boolean + hasTriedPreviewUI: boolean + } +} + +export const trackNavigateToPreview = (): PreviewUiSwitchAction => { + 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, + category: 'preview', + label: 'ui-switch', + data: { + switchedTo: 'preview', + timeSinceLastSwitch: timeSinceLastSwitch + } + } +} + +export const trackPageLoad = (): PreviewPageLoadAction => { + const hasTriedPreviewUI = localStorage.getItem('hasTriedPreviewUI') === 'true' + + return { + type: PREVIEW_EVENT, + category: 'preview', + label: 'page-load', + data: { previewUI: false, hasTriedPreviewUI } + } +}