Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ AWS_ACCESS_KEY_ID="AWS_KEY_ID"
AWS_SECRET_ACCESS_KEY="AWS_SECRET_KEY"
AWS_S3_ENDPOINT="http://localhost:8081" # for dev
# AWS_S3_ENDPOINT="https://bucket-name.s3.bucket-region.amazonaws.com" # when using AWS

# Umami Analytics
UMAMI_HOST="http://localhost:8082" # for dev
UMAMI_WEBSITE_ID="<your-website-uuid>"
22 changes: 21 additions & 1 deletion .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@
"group": "frontend"
}
},
{
"label": "Run Analytics",
"type": "shell",
"command": "pnpm analytics:start",
"hide": true,
"runOptions": {
"instanceLimit": 1,
"instancePolicy": "terminateOldest"
},
"presentation": {
"revealProblems": "onProblem",
"group": "localenv"
}
},
{
"label": "Dev",
"icon": {
Expand All @@ -78,7 +92,13 @@
"reveal": "always",
"showReuseMessage": true
},
"dependsOn": ["Run Local S3", "Run CDN", "Run API", "Run Editor UI"]
"dependsOn": [
"Run Local S3",
"Run CDN",
"Run API",
"Run Editor UI",
"Run Analytics"
]
}
]
}
18 changes: 18 additions & 0 deletions docs/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Then, edit the `.dev.vars` file to set the required values as described below.
| `AWS_ACCESS_KEY_ID` | AWS access key for S3. Not used in local dev. | `ABCDEFGHIJKLMN12OPQR` |
| `AWS_SECRET_ACCESS_KEY` | AWS secret key for S3. Not used in local dev. | `ab1cD/2e/fGhIJ11kL13mN0pQrS45tu6V7w8X9yZ` |
| `AWS_S3_ENDPOINT` | The endpoint for the S3-compatible storage. | `http://localhost:8081` |
| `UMAMI_HOST` | URL of your Umami instance. | `http://localhost:8082` |
| `UMAMI_WEBSITE_ID` | Website ID from the Umami dashboard. | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` |

## Detailed Configuration

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

</details>

### Umami Analytics

Optional. If unset, the analytics script is silently skipped.
For local dev, start the Umami instance first:

```sh
cd localenv/analytics && docker compose up -d
```

Then log in at `http://localhost:8082` (default credentials: `admin` / `umami`), go to Settings → Websites → Add website, and copy the **Website ID**.

```
UMAMI_HOST="http://localhost:8082"
UMAMI_WEBSITE_ID="<your-website-id>"
```

## Development vs Production

### Development Setup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '@shared/utils'
import { SVGRefresh, SVGSpinner } from '~/assets/svg'
import { useConnectWallet } from '~/hooks/useConnectWallet'
import { useTrackEvent } from '~/lib/analytics'
import type { ElementErrors } from '~/lib/types'
import { useUIActions } from '~/stores/uiStore'
import type { WalletActions, WalletStore } from '~/stores/wallet-store'
Expand All @@ -26,6 +27,7 @@ export const ToolsWalletAddress = ({
}: Props) => {
const { connect, disconnect } = useConnectWallet(snap, walletActions)
const uiActions = useUIActions()
const trackEvent = useTrackEvent()
const [error, setError] = useState<ElementErrors>()
const [isLoading, setIsLoading] = useState(false)
const inputRef = useRef<HTMLInputElement>(null)
Expand Down Expand Up @@ -63,6 +65,7 @@ export const ToolsWalletAddress = ({
const walletAddressInfo = await getWalletAddress(walletAddressUrl)
walletActions.setWalletAddressId(walletAddressInfo.id)
await connect()
trackEvent('wallet_connected')
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: could be a later add, but thinking keeping the events in a separate file with a const map of event names as more events get added

} catch (error) {
setError({
fieldErrors: { walletAddress: [(error as Error).message] },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useId } from 'react'
import { Link } from 'react-router'
import { PillTag } from '@/components'
import arrowOutwardIcon from '~/assets/images/landing/arrow-outward.svg'
import { useTrackEvent } from '~/lib/analytics'

export type ToolCardProps = {
children?: React.ReactNode
Expand All @@ -22,12 +23,17 @@ export const ToolCard = ({
target,
className = '',
}: ToolCardProps) => {
const trackEvent = useTrackEvent()
const id = useId()
const linkContent = title
const linkClasses =
'font-bold text-xl leading-normal text-text-primary after:absolute after:inset-0 after:z-10'
const isExternalLink = target !== undefined

const handleClick = () => {
trackEvent('click_card_tool', { link: to })
}

return (
<article
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}`}
Expand All @@ -44,11 +50,12 @@ export const ToolCard = ({
target={target}
rel={target === '_blank' ? 'noreferrer' : undefined}
className={linkClasses}
onClick={handleClick}
>
{linkContent}
</a>
) : (
<Link to={to} className={linkClasses}>
<Link to={to} className={linkClasses} onClick={handleClick}>
{linkContent}
</Link>
)}
Expand Down
28 changes: 28 additions & 0 deletions frontend/app/lib/analytics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createContext, useCallback, useContext } from 'react'
import type { ReactNode } from 'react'
import { useLocation } from 'react-router'
import { TOOLS } from '@shared/types'

type TrackFn = (
eventName: string,
eventData?: Record<string, string | number | boolean | null>,
) => void

const TrackContext = createContext<TrackFn>(() => {})

export function TelemetryProvider({ children }: { children: ReactNode }) {
const { pathname } = useLocation()
const tool = TOOLS.find((t) => pathname.startsWith(`/${t}`))

const track = useCallback<TrackFn>(
(eventName, eventData) => {
window.umami?.track(eventName, tool ? { tool, ...eventData } : eventData)
},
[tool],
)
return <TrackContext.Provider value={track}>{children}</TrackContext.Provider>
}

export function useTrackEvent(): TrackFn {
return useContext(TrackContext)
}
6 changes: 6 additions & 0 deletions frontend/app/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ export type ElementErrors = {
}

declare global {
interface Window {
umami?: {
track(eventName: string, eventData?: Record<string, unknown>): void
}
}

interface Env {
OP_KEY_ID: string
OP_PRIVATE_KEY: string
Expand Down
20 changes: 19 additions & 1 deletion frontend/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,44 @@ import {
type MetaFunction,
} from 'react-router'
import { Header, Footer } from '@/components'
import { APP_URL, UMAMI_HOST, UMAMI_WEBSITE_ID } from '@shared/defines'
import faviconSvg from '~/assets/images/favicon.svg?url'
import { TelemetryProvider } from '~/lib/analytics'
import { UIProvider } from '~/stores/uiStore'
import stylesheet from '~/tailwind.css?url'
import { XCircle } from './components/icons.js'
import { Button } from './components/index.js'

export default function App() {
const domain = new URL(
process.env.NODE_ENV === 'development'
? APP_URL.development
: APP_URL.production,
).hostname

return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
{UMAMI_HOST && UMAMI_WEBSITE_ID && (
<script
defer
src={`${UMAMI_HOST}/script.js`}
data-website-id={UMAMI_WEBSITE_ID}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we add data-domains attribute too, so in production, we only track when on right domain (and not on PR previews/staging)?

Copy link
Copy Markdown
Member Author

@DarianM DarianM Apr 6, 2026

Choose a reason for hiding this comment

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

I noticed. web mon sets data-domains
Any env without the vars set - script tag isn't rendered at all, zero analytics (i.e. no data pollution in staging).
I think we're safe in our case. set vars in local dev and prod only

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

When we set up these vars in GitHub (CI), they'll end up setting for both staging/previews and production.

Copy link
Copy Markdown
Member Author

@DarianM DarianM Apr 6, 2026

Choose a reason for hiding this comment

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

right, then we need to specify data-domain to localhost for dev and webmonetization.org prod. likely through another variable

data-domains={domain}
/>
)}
Comment on lines +36 to +43
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: with this script injection here, do you think we should do any checks to make sure the URLs aren't malformed?

</head>
<body className="h-screen bg-interface-bg-main flex flex-col">
<UIProvider>
<Header />
<main className="flex-grow flex flex-col">
<Outlet />
<TelemetryProvider>
<Outlet />
</TelemetryProvider>
</main>
<Footer />
</UIProvider>
Expand Down
6 changes: 3 additions & 3 deletions frontend/app/routes/banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,15 +233,15 @@ export default function Banner() {
<div
id="builder-actions"
className="xl:flex xl:items-center xl:justify-end xl:gap-sm xl:mt-lg xl:static xl:bg-transparent xl:p-0 xl:border-0 xl:backdrop-blur-none xl:flex-row
fixed bottom-0 left-0 right-0 flex flex-col gap-xs px-md sm:px-lg md:px-xl py-md bg-interface-bg-stickymenu/95 backdrop-blur-[20px] border-t border-field-border z-40"
fixed bottom-0 left-0 right-0 flex flex-col gap-xs px-md sm:px-lg md:px-xl py-md bg-interface-bg-stickymenu/95 backdrop-blur-[20px] border-t border-field-border z-40"
>
<div
id="builder-actions-inner"
className="xl:contents flex flex-col gap-xs mx-auto w-full xl:w-auto xl:p-0 xl:mx-0 xl:flex-row xl:gap-sm"
>
<ToolsSecondaryButton
className="xl:w-[150px] xl:rounded-lg
w-full min-w-0 border-0 xl:border order-last xl:order-first"
w-full min-w-0 border-0 xl:border order-last xl:order-first"
disabled={isLoading}
onClick={() => handleSave('save-success')}
>
Expand All @@ -256,7 +256,7 @@ export default function Banner() {
icon="script"
iconPosition={isLoadingScript ? 'none' : 'left'}
className="xl:w-[250px] xl:rounded-lg
w-full min-w-0 order-first xl:order-last"
w-full min-w-0 order-first xl:order-last"
disabled={isLoadingScript}
onClick={() => handleSave('script')}
>
Expand Down
8 changes: 8 additions & 0 deletions frontend/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@ import { cloudflare } from '@cloudflare/vite-plugin'
import { reactRouter } from '@react-router/dev/vite'
import { APP_BASEPATH } from './app/lib/constants.js'

try {
process.loadEnvFile('.dev.vars')
} catch {
// do nothing.
}

export default defineConfig(({ mode, isSsrBuild }) => ({
define: {
BUILD_CDN_URL: JSON.stringify(process.env.BUILD_CDN_URL),
BUILD_API_URL: JSON.stringify(process.env.BUILD_API_URL),
BUILD_UMAMI_HOST: JSON.stringify(process.env.UMAMI_HOST),
BUILD_UMAMI_WEBSITE_ID: JSON.stringify(process.env.UMAMI_WEBSITE_ID),
},
plugins: [
cloudflare({
Expand Down
28 changes: 28 additions & 0 deletions localenv/analytics/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
services:
umami-db:
image: postgres:15-alpine
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: umami_local_password
volumes:
- umami-db-data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U umami']
interval: 5s
timeout: 5s
retries: 5

umami:
image: ghcr.io/umami-software/umami:postgresql-latest
ports:
- '8082:3000'
environment:
DATABASE_URL: postgresql://umami:umami_local_password@umami-db:5432/umami
APP_SECRET: local_dev_secret
depends_on:
umami-db:
condition: service_healthy

volumes:
umami-db-data:
9 changes: 9 additions & 0 deletions localenv/analytics/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "localenv-analytics",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "docker compose up -d",
"down": "docker compose down"
}
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"lint": "eslint . --fix --cache --cache-location 'node_modules/.cache/eslintcache'",
"lint:check": "eslint . --cache --cache-location 'node_modules/.cache/eslintcache'",
"typecheck": "pnpm -r typecheck",
"test": "pnpm -r test"
"test": "pnpm -r test",
"analytics:dev": "pnpm -C localenv/analytics dev",
"analytics:down": "pnpm -C localenv/analytics down"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
Expand Down
12 changes: 12 additions & 0 deletions shared/defines/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
declare const BUILD_API_URL: string
declare const BUILD_CDN_URL: string
declare const BUILD_AWS_PREFIX: string
declare const BUILD_UMAMI_HOST: string
declare const BUILD_UMAMI_WEBSITE_ID: string

const DEV_API_URL = 'http://localhost:8787'
const DEV_CDN_URL = 'http://localhost:5173'
Expand Down Expand Up @@ -31,3 +33,13 @@ export const AWS_PREFIX =
typeof BUILD_AWS_PREFIX === 'string' && BUILD_AWS_PREFIX
? BUILD_AWS_PREFIX
: DEV_AWS_PREFIX

export const UMAMI_HOST =
typeof BUILD_UMAMI_HOST === 'string' && BUILD_UMAMI_HOST
? BUILD_UMAMI_HOST
: undefined

export const UMAMI_WEBSITE_ID =
typeof BUILD_UMAMI_WEBSITE_ID === 'string' && BUILD_UMAMI_WEBSITE_ID
? BUILD_UMAMI_WEBSITE_ID
: undefined
Loading