Skip to content

Commit 74a0e08

Browse files
authored
feat(frontend): set up Umami for events data collection (#651)
1 parent fe20451 commit 74a0e08

File tree

16 files changed

+180
-7
lines changed

16 files changed

+180
-7
lines changed

.env.sample

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ AWS_ACCESS_KEY_ID="AWS_KEY_ID"
1111
AWS_SECRET_ACCESS_KEY="AWS_SECRET_KEY"
1212
AWS_S3_ENDPOINT="http://localhost:8081" # for dev
1313
# AWS_S3_ENDPOINT="https://bucket-name.s3.bucket-region.amazonaws.com" # when using AWS
14+
15+
# Umami Analytics
16+
UMAMI_HOST="http://localhost:8082" # for dev
17+
UMAMI_WEBSITE_ID="<your-website-uuid>"

.github/workflows/deploy-worker/action.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ inputs:
4444
BUILD_AWS_PREFIX:
4545
description: 'AWS_PREFIX used for config storage'
4646
required: true
47+
BUILD_UMAMI_HOST:
48+
description: 'Umami analytics host URL'
49+
required: false
50+
BUILD_UMAMI_WEBSITE_ID:
51+
description: 'Umami analytics website ID'
52+
required: false
4753

4854
outputs:
4955
url:
@@ -68,6 +74,8 @@ runs:
6874
BUILD_API_URL: ${{ inputs.BUILD_API_URL }}
6975
BUILD_CDN_URL: ${{ inputs.BUILD_CDN_URL }}
7076
BUILD_AWS_PREFIX: ${{ inputs.BUILD_AWS_PREFIX }}
77+
BUILD_UMAMI_HOST: ${{ inputs.BUILD_UMAMI_HOST }}
78+
BUILD_UMAMI_WEBSITE_ID: ${{ inputs.BUILD_UMAMI_WEBSITE_ID }}
7179

7280
- name: Get worker deploy command
7381
if: ${{ inputs.skip-deploy == 'false' && (inputs.changed == 'true' || inputs.mode == 'production') }}

.github/workflows/deploy.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ jobs:
170170
BUILD_API_URL: ${{ steps.api.outputs.url }}
171171
BUILD_CDN_URL: ${{ steps.cdn.outputs.url }}
172172
BUILD_AWS_PREFIX: ${{ vars.AWS_PREFIX }}
173+
BUILD_UMAMI_HOST: ${{ vars.UMAMI_HOST }}
174+
BUILD_UMAMI_WEBSITE_ID: ${{ vars.UMAMI_WEBSITE_ID }}
173175
stagingUrl: ${{ vars.APP_STAGING_URL }}
174176
productionUrl: ${{ vars.APP_PRODUCTION_URL }}
175177
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}

.vscode/tasks.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,20 @@
5757
"group": "frontend"
5858
}
5959
},
60+
{
61+
"label": "Run Analytics",
62+
"type": "shell",
63+
"command": "pnpm analytics:start",
64+
"hide": true,
65+
"runOptions": {
66+
"instanceLimit": 1,
67+
"instancePolicy": "terminateOldest"
68+
},
69+
"presentation": {
70+
"revealProblems": "onProblem",
71+
"group": "localenv"
72+
}
73+
},
6074
{
6175
"label": "Dev",
6276
"icon": {
@@ -78,7 +92,13 @@
7892
"reveal": "always",
7993
"showReuseMessage": true
8094
},
81-
"dependsOn": ["Run Local S3", "Run CDN", "Run API", "Run Editor UI"]
95+
"dependsOn": [
96+
"Run Local S3",
97+
"Run CDN",
98+
"Run API",
99+
"Run Editor UI",
100+
"Run Analytics"
101+
]
82102
}
83103
]
84104
}

docs/env-vars.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ Then, edit the `.dev.vars` file to set the required values as described below.
2020
| `AWS_ACCESS_KEY_ID` | AWS access key for S3. Not used in local dev. | `ABCDEFGHIJKLMN12OPQR` |
2121
| `AWS_SECRET_ACCESS_KEY` | AWS secret key for S3. Not used in local dev. | `ab1cD/2e/fGhIJ11kL13mN0pQrS45tu6V7w8X9yZ` |
2222
| `AWS_S3_ENDPOINT` | The endpoint for the S3-compatible storage. | `http://localhost:8081` |
23+
| `UMAMI_HOST` | URL of your Umami instance. | `http://localhost:8082` |
24+
| `UMAMI_WEBSITE_ID` | Website ID from the Umami dashboard. | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` |
2325

2426
## Detailed Configuration
2527

@@ -123,6 +125,22 @@ AWS_S3_ENDPOINT="https://your-bucket-name.s3.your-region.amazonaws.com"
123125

124126
</details>
125127

128+
### Umami Analytics
129+
130+
Optional. If unset, the analytics script is silently skipped.
131+
For local dev, start the Umami instance first:
132+
133+
```sh
134+
cd localenv/analytics && docker compose up -d
135+
```
136+
137+
Then log in at `http://localhost:8082` (default credentials: `admin` / `umami`), go to Settings → Websites → Add website, and copy the **Website ID**.
138+
139+
```
140+
UMAMI_HOST="http://localhost:8082"
141+
UMAMI_WEBSITE_ID="<your-website-id>"
142+
```
143+
126144
## Development vs Production
127145

128146
### Development Setup

frontend/app/components/redesign/components/ToolsWalletAddress.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from '@shared/utils'
1010
import { SVGRefresh, SVGSpinner } from '~/assets/svg'
1111
import { useConnectWallet } from '~/hooks/useConnectWallet'
12+
import { useTrackEvent } from '~/lib/analytics'
1213
import type { ElementErrors } from '~/lib/types'
1314
import { useUIActions } from '~/stores/uiStore'
1415
import type { WalletActions, WalletStore } from '~/stores/wallet-store'
@@ -26,6 +27,7 @@ export const ToolsWalletAddress = ({
2627
}: Props) => {
2728
const { connect, disconnect } = useConnectWallet(snap, walletActions)
2829
const uiActions = useUIActions()
30+
const trackEvent = useTrackEvent()
2931
const [error, setError] = useState<ElementErrors>()
3032
const [isLoading, setIsLoading] = useState(false)
3133
const inputRef = useRef<HTMLInputElement>(null)
@@ -63,6 +65,7 @@ export const ToolsWalletAddress = ({
6365
const walletAddressInfo = await getWalletAddress(walletAddressUrl)
6466
walletActions.setWalletAddressId(walletAddressInfo.id)
6567
await connect()
68+
trackEvent('wallet_connected')
6669
} catch (error) {
6770
setError({
6871
fieldErrors: { walletAddress: [(error as Error).message] },

frontend/app/components/redesign/components/landing/ToolCard.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useId } from 'react'
22
import { Link } from 'react-router'
33
import { PillTag } from '@/components'
44
import arrowOutwardIcon from '~/assets/images/landing/arrow-outward.svg'
5+
import { useTrackEvent } from '~/lib/analytics'
56

67
export type ToolCardProps = {
78
children?: React.ReactNode
@@ -22,12 +23,17 @@ export const ToolCard = ({
2223
target,
2324
className = '',
2425
}: ToolCardProps) => {
26+
const trackEvent = useTrackEvent()
2527
const id = useId()
2628
const linkContent = title
2729
const linkClasses =
2830
'font-bold text-xl leading-normal text-text-primary after:absolute after:inset-0 after:z-10'
2931
const isExternalLink = target !== undefined
3032

33+
const handleClick = () => {
34+
trackEvent('click_card_tool', { link: to })
35+
}
36+
3137
return (
3238
<article
3339
className={`bg-interface-bg-main rounded-2xl w-full max-w-[330px] p-md pb-xl flex flex-col gap-md relative group hover:bg-white hover:cursor-pointer focus-within:bg-white ${className}`}
@@ -44,11 +50,12 @@ export const ToolCard = ({
4450
target={target}
4551
rel={target === '_blank' ? 'noreferrer' : undefined}
4652
className={linkClasses}
53+
onClick={handleClick}
4754
>
4855
{linkContent}
4956
</a>
5057
) : (
51-
<Link to={to} className={linkClasses}>
58+
<Link to={to} className={linkClasses} onClick={handleClick}>
5259
{linkContent}
5360
</Link>
5461
)}

frontend/app/lib/analytics.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { createContext, useCallback, useContext } from 'react'
2+
import type { ReactNode } from 'react'
3+
import { useLocation } from 'react-router'
4+
import { TOOLS } from '@shared/types'
5+
6+
type TrackFn = (
7+
eventName: string,
8+
eventData?: Record<string, string | number | boolean | null>,
9+
) => void
10+
11+
const TrackContext = createContext<TrackFn>(() => {})
12+
13+
export function TelemetryProvider({ children }: { children: ReactNode }) {
14+
const { pathname } = useLocation()
15+
const tool = TOOLS.find((t) => pathname.startsWith(`/${t}`))
16+
17+
const track = useCallback<TrackFn>(
18+
(eventName, eventData) => {
19+
window.umami?.track(eventName, tool ? { tool, ...eventData } : eventData)
20+
},
21+
[tool],
22+
)
23+
return <TrackContext.Provider value={track}>{children}</TrackContext.Provider>
24+
}
25+
26+
export function useTrackEvent(): TrackFn {
27+
return useContext(TrackContext)
28+
}

frontend/app/lib/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ export type ElementErrors = {
6363
}
6464

6565
declare global {
66+
interface Window {
67+
umami?: {
68+
track(eventName: string, eventData?: Record<string, unknown>): void
69+
}
70+
}
71+
6672
interface Env {
6773
OP_KEY_ID: string
6874
OP_PRIVATE_KEY: string

frontend/app/root.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,44 @@ import {
1111
type MetaFunction,
1212
} from 'react-router'
1313
import { Header, Footer } from '@/components'
14+
import { APP_URL, UMAMI_HOST, UMAMI_WEBSITE_ID } from '@shared/defines'
1415
import faviconSvg from '~/assets/images/favicon.svg?url'
16+
import { TelemetryProvider } from '~/lib/analytics'
1517
import { UIProvider } from '~/stores/uiStore'
1618
import stylesheet from '~/tailwind.css?url'
1719
import { XCircle } from './components/icons.js'
1820
import { Button } from './components/index.js'
1921

2022
export default function App() {
23+
const domain = new URL(
24+
process.env.NODE_ENV === 'development'
25+
? APP_URL.development
26+
: APP_URL.production,
27+
).hostname
28+
2129
return (
2230
<html lang="en">
2331
<head>
2432
<meta charSet="utf-8" />
2533
<meta name="viewport" content="width=device-width, initial-scale=1" />
2634
<Meta />
2735
<Links />
36+
{UMAMI_HOST && UMAMI_WEBSITE_ID && (
37+
<script
38+
defer
39+
src={`${UMAMI_HOST}/script.js`}
40+
data-website-id={UMAMI_WEBSITE_ID}
41+
data-domains={domain}
42+
/>
43+
)}
2844
</head>
2945
<body className="h-screen bg-interface-bg-main flex flex-col">
3046
<UIProvider>
3147
<Header />
3248
<main className="flex-grow flex flex-col">
33-
<Outlet />
49+
<TelemetryProvider>
50+
<Outlet />
51+
</TelemetryProvider>
3452
</main>
3553
<Footer />
3654
</UIProvider>

0 commit comments

Comments
 (0)