Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(reactotron-app): Add basic analytics to Reactotron app with react-ga4 #1406

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
Draft
2 changes: 2 additions & 0 deletions apps/reactotron-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@
"lodash.debounce": "^4.0.8",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-fade-in": "^2.0.1",
"react-ga4": "^2.1.0",
"react-hotkeys": "^2.0.0",
"react-icons": "^4.11.0",
"react-modal": "3.16.1",
Expand Down
4 changes: 1 addition & 3 deletions apps/reactotron-app/src/main/menu.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { Menu, app, shell } from "electron"
import Store from "electron-store"

const configStore = new Store()
import configStore from "../renderer/config"

const isDarwin = process.platform === "darwin"

Expand Down
35 changes: 33 additions & 2 deletions apps/reactotron-app/src/renderer/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React from "react"
import React, { useEffect } from "react"
import { HashRouter as Router, Route, Routes } from "react-router-dom"
import styled from "styled-components"

import SideBar from "./components/SideBar"
import Footer from "./components/Footer"
import AnalyticsOptOut from "./components/AnalyticsOptOut"
import RootContextProvider from "./contexts"
import RootModals from "./RootModals"

Expand All @@ -15,8 +16,9 @@ import Overlay from "./pages/reactNative/Overlay"
import Storybook from "./pages/reactNative/Storybook"
import CustomCommands from "./pages/customCommands"
import Help from "./pages/help"
import { useAnalytics, usePageTracking } from "./util/analyticsHelpers"

const AppContainer = styled.div`
const AppContainerComponent = styled.div`
position: absolute;
top: 0;
bottom: 0;
Expand All @@ -28,6 +30,35 @@ const AppContainer = styled.div`
background-color: ${(props) => props.theme.background};
`

// This wrapper container is used to track page views within the app automatically using react-router-dom
// as well as to initialize analytics on app load.
const AppContainer: React.FC<{ children: React.ReactNode }> = ({ children }) => {
usePageTracking()
const { initializeAnalytics } = useAnalytics()

const [showsAnalyticsInterface, setShowsAnalyticsInterface] = React.useState(false)

useEffect(() => {
const timer = setTimeout(() => {
const status = initializeAnalytics()
if (status === "unknown" && showsAnalyticsInterface === false) {
// Show the user the interface to enable/disable analytics
setShowsAnalyticsInterface(true)
}
}, 250)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are we waiting for here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While developing, I found that there was the possibility of this component rendering a couple times. This delay with the clearTimeout is meant to only initialize the analytics once the electron renderer process has completely opened the app and react has stopped reacting. Not sure if there's a better way to handle this other than some memoizing

return () => clearTimeout(timer)
}, [showsAnalyticsInterface])

return (
<AppContainerComponent>
{children}
{showsAnalyticsInterface && (
<AnalyticsOptOut onClose={() => setShowsAnalyticsInterface(false)} />
)}
</AppContainerComponent>
)
}

const TopSection = styled.div`
overflow: hidden;
display: flex;
Expand Down
11 changes: 11 additions & 0 deletions apps/reactotron-app/src/renderer/KeybindHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useContext } from "react"
import { GlobalHotKeys, KeyEventName } from "react-hotkeys"
import { ReactotronContext, StateContext, TimelineContext } from "reactotron-core-ui"
import LayoutContext from "./contexts/Layout"
import { useAnalytics } from "./util/analyticsHelpers"

const keyMap = {
// Application wide
Expand Down Expand Up @@ -94,26 +95,33 @@ function KeybindHandler({ children }) {
const { openDispatchModal, openSubscriptionModal, clearCommands } = useContext(ReactotronContext)
const { openSearch, toggleSearch } = useContext(TimelineContext)
const { createSnapshot } = useContext(StateContext)
const { sendKeyboardShortcutAnalyticsEvent } = useAnalytics()

const handlers = {
// Tab Navigation
OpenHomeTab: () => {
window.location.hash = "/"
sendKeyboardShortcutAnalyticsEvent("OpenHomeTab")
},
OpenTimelineTab: () => {
window.location.hash = "/timeline"
sendKeyboardShortcutAnalyticsEvent("OpenTimelineTab")
},
OpenStateTab: () => {
window.location.hash = "/state/subscriptions"
sendKeyboardShortcutAnalyticsEvent("OpenStateTab")
},
OpenReactNativeTab: () => {
window.location.hash = "/native/overlay"
sendKeyboardShortcutAnalyticsEvent("OpenReactNativeTab")
},
OpenCustomCommandsTab: () => {
window.location.hash = "/customCommands"
sendKeyboardShortcutAnalyticsEvent("OpenCustomCommandsTab")
},
OpenHelpTab: () => {
window.location.hash = "/help"
sendKeyboardShortcutAnalyticsEvent("OpenHelpTab")
},

// Modals
Expand All @@ -128,11 +136,13 @@ function KeybindHandler({ children }) {
},
TakeSnapshot: () => {
createSnapshot()
sendKeyboardShortcutAnalyticsEvent("TakeSnapshot")
},

// Miscellaneous
ToggleSidebar: () => {
toggleSideBar()
sendKeyboardShortcutAnalyticsEvent("ToggleSidebar")
},
ToggleSearch: () => {
// If we're on the timeline page, toggle the search, otherwise switch to the timeline tab and open search
Expand All @@ -145,6 +155,7 @@ function KeybindHandler({ children }) {
},
ClearTimeline: () => {
clearCommands()
sendKeyboardShortcutAnalyticsEvent("ClearTimeline")
},
}

Expand Down
26 changes: 21 additions & 5 deletions apps/reactotron-app/src/renderer/RootModals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import {
ReactotronContext,
StateContext,
} from "reactotron-core-ui"
import { useAnalytics } from "./util/analyticsHelpers"

function RootModals() {
const { sendAnalyticsEvent } = useAnalytics()
const {
sendCommand,

Expand All @@ -20,30 +22,44 @@ function RootModals() {
} = useContext(ReactotronContext)
const { addSubscription } = useContext(StateContext)

const dispatchAction = (action: any) => {
sendCommand("state.action.dispatch", { action })
}

return (
<>
<DispatchActionModal
isOpen={isDispatchModalOpen}
initialValue={dispatchModalInitialAction}
onClose={() => {
closeDispatchModal()
sendAnalyticsEvent({
category: "dispatch",
action: "dispatchAbort",
})
}}
onDispatchAction={(action: any) => {
sendCommand("state.action.dispatch", { action })
sendAnalyticsEvent({
category: "dispatch",
action: "dispatchConfirm",
})
}}
onDispatchAction={dispatchAction}
isDarwin={window.process.platform === "darwin"}
/>
<SubscriptionAddModal
isOpen={isSubscriptionModalOpen}
onClose={() => {
closeSubscriptionModal()
sendAnalyticsEvent({
category: "subscription",
action: "addAbort",
})
}}
onAddSubscription={(path: string) => {
// TODO: Get this out of here.
closeSubscriptionModal()
addSubscription(path)
sendAnalyticsEvent({
category: "subscription",
action: "addConfirm",
})
}}
/>
</>
Expand Down
118 changes: 118 additions & 0 deletions apps/reactotron-app/src/renderer/components/AnalyticsOptOut/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import React from "react"
import styled, { useTheme } from "styled-components"
import FadeIn from "react-fade-in"
import { reactotronAnalytics } from "../../images"
import configStore from "../../config"
import { useAnalytics } from "../../util/analyticsHelpers"

const Overlay = styled.div`
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
background-color: ${(props) => props.theme.backgroundLighter};
justify-content: center;
align-items: center;
`

const AlertContainer = styled.div`
padding: 30px;
text-align: left;
border-radius: 10px;
background: ${(props) => props.theme.backgroundDarker};
color: ${(props) => props.theme.foregroundLight};

box-shadow: 0 20px 75px ${(props) => props.theme.backgroundSubtleLight};
width: 80%;
max-width: 500px;
`

const AlertHeader = styled.div`
display: flex;
justify-content: center;
margin-bottom: 20px;
`

const AlertHeaderImage = styled.img`
height: 128px;
`

const ButtonGroup = styled.div`
display: flex;
flex-direction: column;
justify-content: flex-start;
margin-top: 20px;
`

const Button = styled.button`
outline: none;
background: ${(props) => props.theme.backgroundLighter};
border: none;
display: inline-block;
padding: 6px 18px;
color: ${(props) => props.theme.background};
margin-right: 10px;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
`

// This is a custom alert that we use to ask the user if they want to opt-in to analytics
// We use this instead of the default alert because we want to style it to match our app.
const AnalyticsOptOut = ({ onClose }) => {
const theme = useTheme()
const { sendOptOutAnalyticsEvent } = useAnalytics()

return (
<FadeIn wrapperTag={Overlay}>
<AlertContainer>
<AlertHeader>
<AlertHeaderImage src={reactotronAnalytics} />
</AlertHeader>
<h1>Opt in to Reactotron analytics?</h1>
<p>Help us improve Reactotron!</p>
<p>
We&apos;d like to collect anonymous usage data to enhance Reactotron&apos;s performance
and features. This data includes general usage patterns and interactions. No personal
information will be collected.
</p>
<p>
You can change this setting at any time and by opting in, you can contribute to making
Reactotron better for everyone!
</p>
<p>Would you like to participate?</p>
<ButtonGroup>
<Button
onClick={() => {
configStore.set("analyticsOptOut", true)
sendOptOutAnalyticsEvent()
onClose()
}}
style={{
backgroundColor: theme.tag,
}}
>
No, don&apos;t collect any data
</Button>
<Button
onClick={() => {
configStore.set("analyticsOptOut", false)
onClose()
}}
style={{
marginTop: 20,
backgroundColor: theme.string,
fontWeight: "bold",
}}
>
Yes, I understand no personal information will be collected
</Button>
</ButtonGroup>
</AlertContainer>
</FadeIn>
)
}

export default AnalyticsOptOut
41 changes: 36 additions & 5 deletions apps/reactotron-app/src/renderer/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import Store from "electron-store"
import Store, { type Schema } from "electron-store"

const schema = {
type StoreType = {
serverPort: number,
commandHistory: number,
analyticsOptOut: string | boolean,
}

const schema:Schema<StoreType> = {
serverPort: {
type: "number",
default: 9090,
Expand All @@ -9,16 +15,41 @@ const schema = {
type: "number",
default: 500,
},
analyticsOptOut: {
type: ["string", "boolean"],
default: "unknown",
},
}

const configStore = new Store({ schema } as any)
const configStore = new Store<StoreType>({
schema,
defaults: {
serverPort: schema.serverPort.default,
commandHistory: schema.commandHistory.default,
analyticsOptOut: schema.analyticsOptOut.default,
},
beforeEachMigration: (_store, context) => {
console.log(`[main-config] migrate from ${context.fromVersion} → ${context.toVersion}`)
},
migrations: {
"1.0.0": (store) => {
// Added analyticsOptOut in order to track if the user has opted out of analytics
// and present the user with a blocking dialog box.
// When the user makes a choice, it will be set to "true" or "false"
store.set("analyticsOptOut", "unknown")
},
},
})

// Setup defaults
if (!configStore.has("serverPort")) {
configStore.set("serverPort", 9090)
configStore.set("serverPort", schema.serverPort.default)
}
if (!configStore.has("commandHistory")) {
configStore.set("commandHistory", 500)
configStore.set("commandHistory", schema.commandHistory.default)
}
if (!configStore.has("analyticsOptOut")) {
configStore.set("analyticsOptOut", schema.analyticsOptOut.default)
}

export default configStore
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/reactotron-app/src/renderer/images/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const reactotronLogo: string = require("./Reactotron-128.png").default
export const reactotronAnalytics: string = require("./Reactotron-analytics.png").default
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's throw this over to Justin and see what he'd like graphic wise.

export const storybookActiveImg: string = require("./storybook-logo-color.png").default
export const storybookInactiveImg: string = require("./storybook-logo.png").default
Loading