Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
6579ea8
feat(analytics): add tools event tracking and shared event constants
kjmitchelljr Apr 15, 2026
118e943
Merge branch 'main' into feat/663-tools-analytics-events
kjmitchelljr Apr 15, 2026
3dd8b6a
Resolve pnpm lockfile
kjmitchelljr Apr 15, 2026
95649ef
Merge branch 'main' into feat/663-tools-analytics-events
kjmitchelljr Apr 16, 2026
f5b43de
removed shared analytics and made TrackFn constrained to ToolsEventMap
kjmitchelljr Apr 21, 2026
613f985
resolve pnpm lockfile
kjmitchelljr Apr 21, 2026
5b8bc1c
use id instead of authServer for wallet provider capture
kjmitchelljr Apr 21, 2026
51274ac
Add simple tracking for fields changed
kjmitchelljr Apr 21, 2026
db4fd66
Merge branch 'main' into feat/663-tools-analytics-events
kjmitchelljr Apr 21, 2026
100df55
Revert "Add simple tracking for fields changed"
kjmitchelljr Apr 21, 2026
28ae2bc
Remove tools prefix
kjmitchelljr Apr 21, 2026
4613aa0
Merge branch 'main' into feat/663-tools-analytics-events
kjmitchelljr Apr 23, 2026
598f8e9
Remove tool settings and update link tag tracking
kjmitchelljr Apr 23, 2026
5dcc993
Tool name added to event
kjmitchelljr Apr 23, 2026
5890bda
Merge branch 'main' into feat/663-tools-analytics-events
kjmitchelljr Apr 23, 2026
9621e4a
Add analytic tracking for extension link clicks on banner and offerwall
kjmitchelljr Apr 23, 2026
8e74ad4
prettier fix
kjmitchelljr Apr 23, 2026
6a750ba
lint fix
kjmitchelljr Apr 23, 2026
6827127
Clean up code - don't need to match frontend as much
kjmitchelljr Apr 24, 2026
81753d0
Merge branch 'main' into feat/663-tools-analytics-events
kjmitchelljr Apr 27, 2026
5f1532b
Simplify logic for analytic events being tracked in umami
kjmitchelljr Apr 28, 2026
6cbdcec
Merge branch 'main' into feat/663-tools-analytics-events
kjmitchelljr Apr 28, 2026
c473b26
Merge branch 'feat/663-tools-analytics-events' into feat/682-cdn-anal…
kjmitchelljr Apr 28, 2026
1d9ee2d
Merge branch 'main' into feat/682-cdn-analytics-events
kjmitchelljr Apr 28, 2026
e0c0f6a
feat(analytics): proxy CDN extension-link clicks to Umami via API
kjmitchelljr Apr 30, 2026
132294d
Merge branch 'main' into feat/682-cdn-analytics-events
kjmitchelljr May 6, 2026
6858838
feat(analytics): proxy Umami payload andinclude url and extension lin…
kjmitchelljr May 7, 2026
9db44cd
Resolve prettier issue
kjmitchelljr May 7, 2026
8e215c8
set prettier path for pnpm
kjmitchelljr May 7, 2026
918c997
Merge branch 'main' into feat/682-cdn-analytics-events
kjmitchelljr May 11, 2026
f671e58
Resolve issue where zValidator wasn't accepting text/plain and fix nit
kjmitchelljr May 12, 2026
a1b024a
Use build time variables for API and hostname from CDN
kjmitchelljr May 12, 2026
903bfe1
Added event detail in controller
kjmitchelljr May 12, 2026
1d22a5a
introduce trackEventFactory and rename events
kjmitchelljr May 12, 2026
19ba347
Merge branch 'main' into feat/682-cdn-analytics-events
kjmitchelljr May 12, 2026
2ce05a5
Remove bubbles and composed flags on click-extension-link
kjmitchelljr May 12, 2026
b8b5162
Fix prettier error
kjmitchelljr May 12, 2026
0aad2d9
Forward Cloudflare geo headers to Umami
kjmitchelljr May 12, 2026
9fc349e
Add a few mock tests if anything to catch any future changes
kjmitchelljr May 12, 2026
87b0a5c
Remove API_HOST variable
kjmitchelljr May 13, 2026
a6063d1
add comments for schema object and afterEach for each test
kjmitchelljr May 13, 2026
2876510
Export of EventsData and more ergonomic function handling
kjmitchelljr May 13, 2026
2354170
Nit - clean up event url
kjmitchelljr May 13, 2026
af0ff16
Merge branch 'main' into feat/682-cdn-analytics-events
kjmitchelljr May 13, 2026
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
2 changes: 2 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ jobs:
build: 'pnpm -C cdn run build'
BUILD_API_URL: ${{ steps.api.outputs.url }}
BUILD_AWS_PREFIX: ${{ vars.AWS_PREFIX }}
BUILD_UMAMI_HOST: ${{ vars.UMAMI_HOST }}
BUILD_UMAMI_WEBSITE_ID: ${{ vars.UMAMI_WEBSITE_ID }}
origin: ${{ vars.CDN_ORIGIN }}
accountId: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
Expand Down
6 changes: 6 additions & 0 deletions cdn/@types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,10 @@ declare global {
'wm-banner': Banner
'wm-payment-widget': PaymentWidget
}

interface Window {
umami?: {
track(eventName: string, eventData?: Record<string, unknown>): void
}
}
}
6 changes: 6 additions & 0 deletions cdn/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ await build({
treeShaking: true,
define: {
BUILD_API_URL: JSON.stringify(process.env.BUILD_API_URL ?? ''),
BUILD_UMAMI_HOST: JSON.stringify(
process.env.BUILD_UMAMI_HOST || process.env.UMAMI_HOST || '',
),
BUILD_UMAMI_WEBSITE_ID: JSON.stringify(
process.env.BUILD_UMAMI_WEBSITE_ID || process.env.UMAMI_WEBSITE_ID || '',
),
},
assetNames: 'assets/[ext]/[name]-[hash]',
loader: {
Expand Down
6 changes: 6 additions & 0 deletions cdn/src/banner.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { API_URL } from '@shared/defines'
import type { BannerProfile } from '@shared/types'
import { Banner } from '@tools/components/banner'
import { injectUmami, trackEvent } from './lib/analytics'
import { appendPaymentPointer, fetchProfile, getScriptParams } from './utils'

customElements.define('wm-banner', Banner)
injectUmami()

const params = getScriptParams('banner')

Expand Down Expand Up @@ -39,6 +41,10 @@ function drawBanner(profile: BannerProfile) {
cdnUrl: params.cdnUrl,
}

bannerElement.addEventListener('click-extension-link', () => {
trackEvent('click_link_banner')
})

const position = profile.position ? profile.position.toLowerCase() : 'bottom'

bannerElement.style.position = 'fixed'
Expand Down
39 changes: 39 additions & 0 deletions cdn/src/lib/analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { UMAMI_HOST, UMAMI_WEBSITE_ID } from '@shared/defines'

type ExtensionLinkSource = 'banner' | 'offerwall'
type ClickLinkEvent = `click_link_${ExtensionLinkSource}`

export type CdnEventMap = {
[K in ClickLinkEvent]: undefined
Comment thread
kjmitchelljr marked this conversation as resolved.
Outdated
}

type TrackArgs<E extends keyof CdnEventMap> = CdnEventMap[E] extends undefined
? [eventName: E]
: [eventName: E, data: CdnEventMap[E]]

let umamiInjected = false

/**
* Appended the Umami tracker script so it's loaded before any
* trackEvent() call fires (avoids a race on the first user interaction).
*/
export function injectUmami() {
if (!UMAMI_HOST || !UMAMI_WEBSITE_ID) return
if (typeof window === 'undefined' || typeof document === 'undefined') return
if (window.umami || umamiInjected) return

umamiInjected = true
const script = document.createElement('script')
script.defer = true
script.src = `${UMAMI_HOST}/script.js`
script.setAttribute('data-website-id', UMAMI_WEBSITE_ID)
Comment thread
kjmitchelljr marked this conversation as resolved.
Outdated
document.head.appendChild(script)
}

export function trackEvent<E extends keyof CdnEventMap>(
...args: TrackArgs<E>
): void {
const [eventName, data] = args as [E, CdnEventMap[E] | undefined]
injectUmami()
window.umami?.track(eventName, data)
}
2 changes: 2 additions & 0 deletions cdn/src/offerwall.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { API_URL } from '@shared/defines'
import { OfferwallModal } from '@tools/components'
import { injectUmami } from './lib/analytics'
import { appendPaymentPointer, fetchProfile, getScriptParams } from './utils'
import {
WebMonetizationCustomOfferwallChoice,
Expand All @@ -9,6 +10,7 @@ import {

const NAME = 'wm-offerwall'
customElements.define(NAME, OfferwallModal)
injectUmami()

const params = getScriptParams('offerwall')
const linkElem = appendPaymentPointer(params.walletAddress)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
OfferwallCustomChoice,
StoredEvent,
} from './types'
import { trackEvent } from '../../lib/analytics'

export class WebMonetizationCustomOfferwallChoice implements OfferwallCustomChoice {
#browserSupportKey = getBrowserSupportForExtension(
Expand Down Expand Up @@ -99,7 +100,7 @@ export class WebMonetizationCustomOfferwallChoice implements OfferwallCustomChoi
const owElem = document.createElement(elementName) as OfferwallModal
const actions = owElem.setController({
onExtensionLinkClick() {
// can start tracking
trackEvent('click_link_offerwall')
},
onModalClose() {
abortController.abort('modal closed by user')
Expand Down
7 changes: 6 additions & 1 deletion components/src/banner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,12 @@ export class Banner extends LitElement {
}

private handleLinkClick() {
// TODO: do anything other than open the link in a new tab, like analytics, showing some thank you message etc.
this.dispatchEvent(
new CustomEvent('click-extension-link', {
bubbles: true,
composed: true,
Comment thread
kjmitchelljr marked this conversation as resolved.
Outdated
}),
)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
validateAndConfirmPointer,
WalletAddressFormatError,
} from '@shared/utils/index'
import { useTrackEvent } from '~/lib/analytics'

const htmlEncodePointer = (pointer: string): string => {
return pointer
Expand All @@ -24,6 +25,7 @@ export const LinkTagGenerator = () => {
const [error, setError] = useState('')
const [showCodeBox, setShowCodeBox] = useState(false)
const [isCopied, setIsCopied] = useState(false)
const trackEvent = useTrackEvent()

const handleSubmit = useCallback(
async (e: React.FormEvent) => {
Expand All @@ -36,6 +38,9 @@ export const LinkTagGenerator = () => {
const validatedPointer = await validateAndConfirmPointer(pointerInput)
setParsedLinkTag(htmlEncodePointer(validatedPointer))
setShowCodeBox(true)
trackEvent('link_tag_generated', {
wallet_provider: new URL(validatedPointer).hostname,
})
} catch (err) {
const message =
err instanceof WalletAddressFormatError
Expand All @@ -47,7 +52,7 @@ export const LinkTagGenerator = () => {
setIsLoading(false)
}
},
[pointerInput],
[pointerInput, trackEvent],
)

const handleOnChange = useCallback(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,21 @@ export const ToolsWalletAddress = ({
const walletAddressInfo = await getWalletAddress(walletAddressUrl)
walletActions.setWalletAddressId(walletAddressInfo.id)
await connect()
trackEvent('wallet_connected')
trackEvent('wallet_connected', {
wallet_provider: new URL(walletAddressInfo.id).hostname,
})
} catch (error) {
setError({ walletAddress: [(error as Error).message] })
} finally {
setIsLoading(false)
}
}

const handleDisconnect = () => {
trackEvent('wallet_disconnected')
disconnect()
}

const handleWalletAddressChange = (
e: React.ChangeEvent<HTMLInputElement>,
) => {
Expand Down Expand Up @@ -160,7 +167,7 @@ export const ToolsWalletAddress = ({
</div>
{snap.isWalletConnected && (
<button
onClick={disconnect}
onClick={handleDisconnect}
className="flex items-center justify-center w-12 h-12 p-2 rounded-lg shrink-0 hover:bg-gray-50 active:bg-gray-100 transition-colors"
aria-label="Disconnect wallet"
>
Expand Down
7 changes: 6 additions & 1 deletion frontend/app/hooks/useSaveProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '@/components'
import { TOOL_BANNER, TOOL_OFFERWALL, TOOL_WIDGET } from '@shared/types'
import { useDialog } from '~/hooks/useDialog'
import { useTrackEvent } from '~/lib/analytics'
import { ApiError } from '~/lib/helpers'
import { actions as bannerActions } from '~/stores/banner-store'
import { actions as offerwallActions } from '~/stores/offerwall-store'
Expand All @@ -28,6 +29,7 @@ function getToolActions() {

export const useSaveProfile = (wallet: WalletStore) => {
const [openDialog, closeDialog] = useDialog()
const trackEvent = useTrackEvent()

const save = useCallback(
async (action: 'save-success' | 'script'): Promise<void> => {
Expand All @@ -48,10 +50,13 @@ export const useSaveProfile = (wallet: WalletStore) => {

if (result.success) {
actions.commitProfile()
const tool = toolState.currentToolType

if (action === 'script') {
trackEvent(`${tool}_script_generated`)
openDialog(<ScriptDialog wallet={wallet} />)
} else {
trackEvent(`${tool}_profile_saved`)
openDialog(<StatusDialog onDone={closeDialog} />)
}
}
Expand All @@ -69,7 +74,7 @@ export const useSaveProfile = (wallet: WalletStore) => {
)
}
},
[openDialog, closeDialog],
[openDialog, closeDialog, trackEvent],
)

const saveLastAction = useCallback(async (): Promise<void> => {
Expand Down
13 changes: 13 additions & 0 deletions frontend/app/lib/analytics-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Tool } from '@shared/types'

type ProfileSavedEvent = `${Tool}_profile_saved`
type ScriptGeneratedEvent = `${Tool}_script_generated`

export type ToolsEventMap = {
click_card_tool: { link: string }
wallet_connected: { wallet_provider: string }
wallet_disconnected: undefined
link_tag_generated: { wallet_provider: string }
} & {
[K in ProfileSavedEvent | ScriptGeneratedEvent]: undefined
}
17 changes: 10 additions & 7 deletions frontend/app/lib/analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { createContext, useCallback, useContext } from 'react'
import type { ReactNode } from 'react'
import { useLocation } from 'react-router'
import { TOOLS } from '@shared/types'
import type { ToolsEventMap } from '~/lib/analytics-events'

type TrackFn = (
eventName: string,
eventData?: Record<string, string | number | boolean | null>,
export type TrackFn = <E extends keyof ToolsEventMap>(
...args: ToolsEventMap[E] extends undefined
? [eventName: E]
: [eventName: E, data: ToolsEventMap[E]]
) => void

const TrackContext = createContext<TrackFn>(() => {})
Expand All @@ -14,12 +16,13 @@ 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)
},
const track = useCallback(
((eventName, eventData) => {
window.umami?.track(eventName, { ...(tool && { tool }), ...eventData })
}) as TrackFn,
[tool],
)

return <TrackContext.Provider value={track}>{children}</TrackContext.Provider>
}

Expand Down
Loading