Skip to content

Commit

Permalink
Google Analytics events for opening and closing the app, and opening …
Browse files Browse the repository at this point in the history
…and closing projects (#9779)

- Closes #9778
- Add `open_app`, `close_app`, `open_workflow`, and `close_workflow` events
- Miscellaneous fixes for Google Analytics:
- Fix `open_chat` and `close_chat` events firing even when chat is not visible
- Add Google Analytics script to GUI2 entrypoint (i.e. the entrypoint used by the desktop app)

Unrelated changes:
- Add Nix development shell to allow Nix users to build GUI2 and the build script
- Java dependencies have *not* been added in this PR to keep things simple

# Important Notes
None
  • Loading branch information
somebody1234 authored Apr 25, 2024
1 parent 0d495ff commit 3a53d47
Show file tree
Hide file tree
Showing 19 changed files with 352 additions and 60 deletions.
7 changes: 7 additions & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
strict_env

if ! has nix_direnv_version || ! nix_direnv_version 3.0.4; then
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.4/direnvrc" "sha256-DzlYZ33mWF/Gs8DDeyjr8mnVmQGx7ASYqA5WlxwvBG4="
fi

use flake
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,9 @@ test-results
*.ir
*.meta
.enso/

##################
## direnv cache ##
##################

.direnv
3 changes: 2 additions & 1 deletion app/ide-desktop/lib/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"./src/appConfig": "./src/appConfig.js",
"./src/buildUtils": "./src/buildUtils.js",
"./src/detect": "./src/detect.ts",
"./src/gtag": "./src/gtag.ts"
"./src/gtag": "./src/gtag.ts",
"./src/load": "./src/load.ts"
}
}
10 changes: 10 additions & 0 deletions app/ide-desktop/lib/common/src/appConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import * as url from 'node:url'

// =================
// === Constants ===
// =================

const ENSO_CLOUD_GOOGLE_ANALYTICS_TAG = 'G-CLTBJ37MDM'

// ===============================
// === readEnvironmentFromFile ===
// ===============================
Expand Down Expand Up @@ -35,6 +41,10 @@ export async function readEnvironmentFromFile() {
}
const variables = Object.fromEntries(entries)
Object.assign(process.env, variables)
if (!('' in process.env)) {
// @ts-expect-error This is the only place where this environment variable is set.
process.env.ENSO_CLOUD_GOOGLE_ANALYTICS_TAG = ENSO_CLOUD_GOOGLE_ANALYTICS_TAG
}
} catch (error) {
if (isProduction) {
console.warn('Could not load `.env` file; disabling cloud backend.')
Expand Down
111 changes: 104 additions & 7 deletions app/ide-desktop/lib/common/src/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,38 +17,65 @@ export enum Platform {
windows = 'Windows',
macOS = 'macOS',
linux = 'Linux',
windowsPhone = 'Windows Phone',
iPhoneOS = 'iPhone OS',
android = 'Android',
}

/** Return the platform the app is currently running on.
/** The platform the app is currently running on.
* This is used to determine whether `metaKey` or `ctrlKey` is used in shortcuts. */
export function platform(): Platform {
if (isOnWindows()) {
export function platform() {
if (isOnWindowsPhone()) {
// MUST be before Android and Windows.
return Platform.windowsPhone
} else if (isOnWindows()) {
return Platform.windows
} else if (isOnIPhoneOS()) {
// MUST be before macOS.
return Platform.iPhoneOS
} else if (isOnMacOS()) {
return Platform.macOS
} else if (isOnAndroid()) {
// MUST be before Linux.
return Platform.android
} else if (isOnLinux()) {
return Platform.linux
} else {
return Platform.unknown
}
}

/** Return whether the device is running Windows. */
/** Whether the device is running Windows. */
export function isOnWindows() {
return /windows/i.test(navigator.userAgent)
}

/** Return whether the device is running macOS. */
/** Whether the device is running macOS. */
export function isOnMacOS() {
return /mac os/i.test(navigator.userAgent)
}

/** Return whether the device is running Linux. */
/** Whether the device is running Linux. */
export function isOnLinux() {
return /linux/i.test(navigator.userAgent)
}

/** Return whether the device is running an unknown OS. */
/** Whether the device is running Windows Phone. */
export function isOnWindowsPhone() {
return /windows phone/i.test(navigator.userAgent)
}

/** Whether the device is running iPhone OS. */
export function isOnIPhoneOS() {
return /iPhone/i.test(navigator.userAgent)
}

/** Whether the device is running Android. */
export function isOnAndroid() {
return /android/i.test(navigator.userAgent)
}

/** Whether the device is running an unknown OS. */
export function isOnUnknownOS() {
return platform() === Platform.unknown
}
Expand Down Expand Up @@ -126,3 +153,73 @@ export function isOnSafari() {
export function isOnUnknownBrowser() {
return browser() === Browser.unknown
}

// ====================
// === Architecture ===
// ====================

let detectedArchitecture: string | null = null
// Only implemented by Chromium.
// @ts-expect-error This API exists, but no typings exist for it yet.
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
navigator.userAgentData
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
?.getHighEntropyValues(['architecture'])
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
.then((values: unknown) => {
if (
typeof values === 'object' &&
values != null &&
'architecture' in values &&
typeof values.architecture === 'string'
) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
detectedArchitecture = String(values.architecture)
}
})

/** Possible processor architectures. */
export enum Architecture {
intel64 = 'x86_64',
arm64 = 'arm64',
}

/** The processor architecture of the current system. */
export function architecture() {
if (detectedArchitecture != null) {
switch (detectedArchitecture) {
case 'arm': {
return Architecture.arm64
}
default: {
return Architecture.intel64
}
}
}
switch (platform()) {
case Platform.windows:
case Platform.linux:
case Platform.unknown: {
return Architecture.intel64
}
case Platform.macOS:
case Platform.iPhoneOS:
case Platform.android:
case Platform.windowsPhone: {
// Assume the macOS device is on a M-series CPU.
// This is highly unreliable, but operates under the assumption that all
// new macOS devices will be ARM64.
return Architecture.arm64
}
}
}

/** Whether the device has an Intel 64-bit CPU. */
export function isIntel64() {
return architecture() === Architecture.intel64
}

/** Whether the device has an ARM 64-bit CPU. */
export function isArm64() {
return architecture() === Architecture.arm64
}
7 changes: 7 additions & 0 deletions app/ide-desktop/lib/common/src/gtag.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
/** @file Google Analytics tag. */
import * as load from './load'

if (process.env.ENSO_CLOUD_GOOGLE_ANALYTICS_TAG != null) {
void load.loadScript(
`https://www.googletagmanager.com/gtag/js?id=${process.env.ENSO_CLOUD_GOOGLE_ANALYTICS_TAG}`
)
}

// @ts-expect-error This is explicitly not given types as it is a mistake to acess this
// anywhere else.
Expand Down
32 changes: 32 additions & 0 deletions app/ide-desktop/lib/common/src/load.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/** @file Utilities for loading resources. */

/** Add a script to the DOM. */
export function loadScript(url: string) {
const script = document.createElement('script')
script.crossOrigin = 'anonymous'
script.src = url
document.head.appendChild(script)
return new Promise<HTMLScriptElement>((resolve, reject) => {
script.onload = () => {
resolve(script)
}
script.onerror = reject
})
}

/** Add a CSS stylesheet to the DOM. */
export function loadStyle(url: string) {
const style = document.createElement('link')
style.crossOrigin = 'anonymous'
style.href = url
style.rel = 'stylesheet'
style.media = 'screen'
style.type = 'text/css'
document.head.appendChild(style)
return new Promise<HTMLLinkElement>((resolve, reject) => {
style.onload = () => {
resolve(style)
}
style.onerror = reject
})
}
5 changes: 0 additions & 5 deletions app/ide-desktop/lib/dashboard/404.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,5 @@
<noscript>
This page requires JavaScript to run. Please enable it in your browser.
</noscript>
<!-- Google tag (gtag.js) -->
<script
async
src="https://www.googletagmanager.com/gtag/js?id=G-CLTBJ37MDM"
></script>
</body>
</html>
5 changes: 0 additions & 5 deletions app/ide-desktop/lib/dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,5 @@
<noscript>
This page requires JavaScript to run. Please enable it in your browser.
</noscript>
<!-- Google tag (gtag.js) -->
<script
async
src="https://www.googletagmanager.com/gtag/js?id=G-CLTBJ37MDM"
></script>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ const MODIFIER_JSX: Readonly<
</aria.Text>
),
},
[detect.Platform.iPhoneOS]: {},
[detect.Platform.android]: {},
[detect.Platform.windowsPhone]: {},
/* eslint-enable @typescript-eslint/naming-convention */
}

Expand Down
31 changes: 28 additions & 3 deletions app/ide-desktop/lib/dashboard/src/hooks/gtagHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import * as gtag from 'enso-common/src/gtag'

import * as authProvider from '#/providers/AuthProvider'

// ===================
// === useGtag ===
// ===================
// ====================
// === useGtagEvent ===
// ====================

/** A hook that returns a no-op if the user is offline, otherwise it returns
* a transparent wrapper around `gtag.event`. */
Expand All @@ -22,3 +22,28 @@ export function useGtagEvent() {
[sessionType]
)
}

// =============================
// === gtagOpenCloseCallback ===
// =============================

/** Send an event indicating that something has been opened, and return a cleanup function
* sending an event indicating that it has been closed.
*
* Also sends the close event when the window is unloaded. */
export function gtagOpenCloseCallback(
gtagEventRef: React.MutableRefObject<ReturnType<typeof useGtagEvent>>,
openEvent: string,
closeEvent: string
) {
const gtagEventCurrent = gtagEventRef.current
gtagEventCurrent(openEvent)
const onBeforeUnload = () => {
gtagEventCurrent(closeEvent)
}
window.addEventListener('beforeunload', onBeforeUnload)
return () => {
window.removeEventListener('beforeunload', onBeforeUnload)
gtagEventCurrent(closeEvent)
}
}
16 changes: 12 additions & 4 deletions app/ide-desktop/lib/dashboard/src/layouts/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,6 @@ interface InternalChatHeaderProps {
function ChatHeader(props: InternalChatHeaderProps) {
const { threads, setThreads, threadId, threadTitle, setThreadTitle } = props
const { switchThread, sendMessage, doClose } = props
const gtagEvent = gtagHooks.useGtagEvent()
const [isThreadListVisible, setIsThreadListVisible] = React.useState(false)
// These will never be `null` as their values are set immediately.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
Expand All @@ -258,12 +257,10 @@ function ChatHeader(props: InternalChatHeaderProps) {
setIsThreadListVisible(false)
}
document.addEventListener('click', onClick)
gtagEvent('cloud_open_chat')
return () => {
document.removeEventListener('click', onClick)
gtagEvent('cloud_close_chat')
}
}, [gtagEvent])
}, [])

return (
<>
Expand Down Expand Up @@ -394,6 +391,17 @@ export default function Chat(props: ChatProps) {
}
},
})
const gtagEvent = gtagHooks.useGtagEvent()
const gtagEventRef = React.useRef(gtagEvent)
gtagEventRef.current = gtagEvent

React.useEffect(() => {
if (!isOpen) {
return
} else {
return gtagHooks.gtagOpenCloseCallback(gtagEventRef, 'cloud_open_chat', 'cloud_close_chat')
}
}, [isOpen])

/** This is SAFE, because this component is only rendered when `accessToken` is present.
* See `dashboard.tsx` for its sole usage. */
Expand Down
16 changes: 14 additions & 2 deletions app/ide-desktop/lib/dashboard/src/layouts/Editor.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
/** @file The container that launches the IDE. */
import * as React from 'react'

import * as load from 'enso-common/src/load'

import * as appUtils from '#/appUtils'

import * as gtagHooks from '#/hooks/gtagHooks'
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'

import * as backendModule from '#/services/Backend'

import * as load from '#/utilities/load'

// =================
// === Constants ===
// =================
Expand Down Expand Up @@ -39,6 +40,9 @@ export interface EditorProps {
export default function Editor(props: EditorProps) {
const { hidden, supportsLocalBackend, projectStartupInfo, appRunner } = props
const toastAndLog = toastAndLogHooks.useToastAndLog()
const gtagEvent = gtagHooks.useGtagEvent()
const gtagEventRef = React.useRef(gtagEvent)
gtagEventRef.current = gtagEvent
const [initialized, setInitialized] = React.useState(supportsLocalBackend)

React.useEffect(() => {
Expand All @@ -48,6 +52,14 @@ export default function Editor(props: EditorProps) {
}
}, [hidden])

React.useEffect(() => {
if (hidden) {
return
} else {
return gtagHooks.gtagOpenCloseCallback(gtagEventRef, 'open_workflow', 'close_workflow')
}
}, [projectStartupInfo, hidden])

let hasEffectRun = false

React.useEffect(() => {
Expand Down
Loading

0 comments on commit 3a53d47

Please sign in to comment.