Skip to content

Commit 712b283

Browse files
committed
Replace Plausible with OpenPanel analytics
1 parent e7a0a0e commit 712b283

22 files changed

Lines changed: 516 additions & 103 deletions

apps/web/__mocks__/next-plausible.ts

Lines changed: 0 additions & 10 deletions
This file was deleted.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { createRouteHandler } from '@openpanel/nextjs/server'
2+
3+
export const { GET, POST } = createRouteHandler()

apps/web/app/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Providers } from '@/components/layout/providers'
66
import { OrganizationSchema } from '@/components/seo/json-ld'
77
import { VercelToolbarWrapper } from '@/components/vercel-toolbar'
88
import { rootMetadata } from '@/lib/seo'
9+
import { AnalyticsHead } from '@thedaviddias/analytics/head'
910
import { GeistMono } from 'geist/font/mono'
1011
import { GeistSans } from 'geist/font/sans'
1112
import './globals.css'
@@ -31,6 +32,7 @@ export default async function RootLayout({
3132
suppressHydrationWarning
3233
>
3334
<head>
35+
<AnalyticsHead openPanelClientId={process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID} />
3436
<OrganizationSchema />
3537
</head>
3638
<body className="font-sans antialiased bg-bg text-text">

apps/web/components/layout/providers.tsx

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
'use client'
22

33
import { KeyboardShortcutsProvider } from '@/components/shortcuts/keyboard-shortcuts-provider'
4+
import { OpenPanelIdentify } from '@/components/user/openpanel-identify'
45
import { UserBootstrap } from '@/components/user/user-bootstrap'
56
import { ConvexAuthProvider } from '@convex-dev/auth/react'
67
import { ConvexReactClient } from 'convex/react'
7-
import PlausibleProvider from 'next-plausible'
88
import { ThemeProvider as NextThemesProvider } from 'next-themes'
99
import { NuqsAdapter } from 'nuqs/adapters/next/app'
1010
import type { ReactNode } from 'react'
@@ -24,44 +24,40 @@ interface ProvidersProps {
2424
* Includes:
2525
* - Convex (database & auth)
2626
* - Theme management (next-themes)
27-
* - Analytics (Plausible)
27+
* - Analytics identity bridge (OpenPanel)
2828
* - Toast notifications (sonner)
2929
* - Keyboard shortcuts (power users)
3030
*/
3131
export function Providers({ children }: ProvidersProps) {
32-
const domain = process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN || 'souls.directory'
33-
const enabled = process.env.NODE_ENV === 'production'
34-
3532
const content = (
3633
<NuqsAdapter>
37-
<PlausibleProvider domain={domain} enabled={enabled} trackOutboundLinks>
38-
<NextThemesProvider
39-
attribute="class"
40-
defaultTheme="dark"
41-
enableSystem={false}
42-
disableTransitionOnChange
43-
>
44-
<KeyboardShortcutsProvider>
45-
{children}
46-
<Toaster
47-
position="bottom-right"
48-
toastOptions={{
49-
style: {
50-
background: 'var(--color-surface)',
51-
border: '1px solid var(--color-border)',
52-
color: 'var(--color-text)',
53-
},
54-
}}
55-
/>
56-
</KeyboardShortcutsProvider>
57-
</NextThemesProvider>
58-
</PlausibleProvider>
34+
<NextThemesProvider
35+
attribute="class"
36+
defaultTheme="dark"
37+
enableSystem={false}
38+
disableTransitionOnChange
39+
>
40+
<KeyboardShortcutsProvider>
41+
{children}
42+
<Toaster
43+
position="bottom-right"
44+
toastOptions={{
45+
style: {
46+
background: 'var(--color-surface)',
47+
border: '1px solid var(--color-border)',
48+
color: 'var(--color-text)',
49+
},
50+
}}
51+
/>
52+
</KeyboardShortcutsProvider>
53+
</NextThemesProvider>
5954
</NuqsAdapter>
6055
)
6156

6257
return (
6358
<ConvexAuthProvider client={convex}>
6459
<UserBootstrap />
60+
<OpenPanelIdentify />
6561
{content}
6662
</ConvexAuthProvider>
6763
)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
'use client'
2+
3+
import { api } from '@/lib/convex-api'
4+
import { useConvexAuth, useQuery } from 'convex/react'
5+
import { useEffect } from 'react'
6+
7+
function splitName(name?: string | null) {
8+
const trimmed = name?.trim()
9+
if (!trimmed) {
10+
return {}
11+
}
12+
13+
const [firstName, ...rest] = trimmed.split(/\s+/)
14+
return {
15+
firstName,
16+
lastName: rest.length > 0 ? rest.join(' ') : undefined,
17+
}
18+
}
19+
20+
export function OpenPanelIdentify() {
21+
const { isAuthenticated, isLoading } = useConvexAuth()
22+
const me = useQuery(api.users.me)
23+
24+
useEffect(() => {
25+
if (typeof window === 'undefined' || !window.op || process.env.NODE_ENV !== 'production') {
26+
return
27+
}
28+
29+
if (isLoading || me === undefined) {
30+
return
31+
}
32+
33+
if (!isAuthenticated || !me) {
34+
window.op.clear()
35+
return
36+
}
37+
38+
const identity = splitName(me.displayName ?? me.name)
39+
40+
window.op.identify({
41+
profileId: me._id,
42+
firstName: identity.firstName,
43+
lastName: identity.lastName,
44+
email: me.email ?? undefined,
45+
properties: {
46+
handle: me.handle ?? undefined,
47+
githubHandle: me.githubHandle ?? undefined,
48+
role: me.role ?? undefined,
49+
},
50+
})
51+
}, [isAuthenticated, isLoading, me])
52+
53+
return null
54+
}

apps/web/env.d.ts

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -112,28 +112,22 @@ declare module '@convex-dev/auth/react' {
112112
export function AuthLoading(props: { children: ReactNode }): JSX.Element | null
113113
}
114114

115-
// Next Plausible analytics
116-
declare module 'next-plausible' {
117-
import type { ReactNode } from 'react'
118-
119-
export interface PlausibleProviderProps {
120-
domain: string
121-
children: ReactNode
122-
trackOutboundLinks?: boolean
123-
trackFileDownloads?: boolean
124-
taggedEvents?: boolean
125-
hash?: boolean
126-
exclude?: string
127-
selfHosted?: boolean
128-
enabled?: boolean
129-
integrity?: string
130-
scriptProps?: Record<string, string>
115+
declare global {
116+
interface Window {
117+
op?: {
118+
(command: string, ...args: unknown[]): void
119+
track: (eventName: string, properties?: Record<string, unknown>) => void
120+
identify: (profile: {
121+
profileId: string
122+
firstName?: string
123+
lastName?: string
124+
email?: string
125+
properties?: Record<string, unknown>
126+
}) => void
127+
clear: () => void
128+
setGlobalProperties: (properties: Record<string, unknown>) => void
129+
}
131130
}
132-
133-
export default function PlausibleProvider(props: PlausibleProviderProps): JSX.Element
134-
135-
export function usePlausible(): (
136-
eventName: string,
137-
options?: { props?: Record<string, string | number | boolean> }
138-
) => void
139131
}
132+
133+
export {}

apps/web/hooks/use-analytics.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
'use client'
22

33
import type { AnalyticsEvents } from '@/lib/analytics-events'
4-
import { usePlausible } from 'next-plausible'
54

65
export function useAnalytics() {
7-
const plausible = usePlausible()
8-
96
return {
107
track: <E extends keyof AnalyticsEvents>(
118
event: E,
129
...args: AnalyticsEvents[E] extends never ? [] : [props: AnalyticsEvents[E]]
1310
) => {
14-
plausible(event, args[0] ? { props: args[0] } : undefined)
11+
if (process.env.NODE_ENV !== 'production' || typeof window === 'undefined' || !window.op) {
12+
return
13+
}
14+
15+
window.op.track(event, args[0] ?? {})
1516
},
1617
}
1718
}

apps/web/lib/analytics-events.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
/**
22
* Centralized analytics event definitions.
33
* Add new events here — this is the single source of truth.
4-
*
5-
* Plausible custom properties docs:
6-
* https://plausible.io/docs/custom-props/introduction
74
*/
85
export type AnalyticsEvents = {
96
// Conversions
@@ -20,7 +17,7 @@ export type AnalyticsEvents = {
2017
collection_create: never
2118
github_import: { url: string }
2219

23-
// Navigation (outbound_click is auto-tracked by next-plausible; listed for documentation)
20+
// Navigation (outbound clicks are auto-tracked by OpenPanel; listed for documentation)
2421
outbound_click: { url: string }
2522
}
2623

apps/web/lib/faq-data.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export const faqItems: FAQItem[] = [
8484
category: 'Privacy',
8585
question: 'What data do you collect?',
8686
answer:
87-
"Without an account: Page views via Plausible Analytics (no tracking, no cookies, no personal data)\n\nWith an account: GitHub username, email, avatar (from GitHub OAuth), souls you create, souls you star\n\nWe don't sell or share data.",
87+
"Without an account: privacy-friendly analytics for page views and product usage.\n\nWith an account: GitHub username, email, avatar (from GitHub OAuth), souls you create, souls you star.\n\nWe don't sell or share data.",
8888
},
8989
{
9090
category: 'Privacy',

apps/web/lib/sentry.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,6 @@ export const DENY_URLS: (string | RegExp)[] = [
6666
/googletagmanager\.com/,
6767
/google-analytics\.com/,
6868

69-
// Other analytics
70-
/plausible\.io/,
71-
/analytics\./,
72-
7369
// Browser internals
7470
/^webkit-masked-url:\/\//,
7571
]
@@ -173,7 +169,7 @@ export const sharedSentryOptions = {
173169
if (breadcrumb.category === 'fetch') {
174170
const url = breadcrumb.data?.url || ''
175171
if (
176-
url.includes('plausible.io') ||
172+
url.includes('/api/op') ||
177173
url.includes('google-analytics.com') ||
178174
url.includes('googletagmanager.com')
179175
) {

0 commit comments

Comments
 (0)