Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 4 additions & 2 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const CONNECT_SRC_HOSTS = [
'https://pass.celopg.eco',
'https://*.rainbow.me',
'https://us-central1-staked-celo-bot.cloudfunctions.net',
'https://us.i.posthog.com',
'https://us-assets.i.posthog.com',
];
const FRAME_SRC_HOSTS = [
'https://*.walletconnect.com',
Expand All @@ -38,12 +40,12 @@ const IMG_SRC_HOSTS = [
'https://app.safe.global',
'https://pass.celopg.eco',
];
const SCRIPTS_SRC_HOSTS = ['https://*.safe.global'];
const SCRIPTS_SRC_HOSTS = ['https://*.safe.global', 'https://us-assets.i.posthog.com'];

const cspHeader = `
default-src 'self';
script-src 'self' ${isDev ? "'unsafe-eval'" : SCRIPTS_SRC_HOSTS.join(' ')};
script-src-elem 'self' 'unsafe-inline';
script-src-elem 'self' 'unsafe-inline' https://us-assets.i.posthog.com;
style-src 'self' 'unsafe-inline';
connect-src 'self' ${CONNECT_SRC_HOSTS.join(' ')};
img-src 'self' blob: data: ${IMG_SRC_HOSTS.join(' ')};
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@metamask/jazzicon": "^2.0.0",
"@metamask/post-message-stream": "6.1.2",
"@metamask/providers": "10.2.1",
"@posthog/react": "^1.8.2",
"@rainbow-me/rainbowkit": "2.2.9",
"@slack/socket-mode": "^2.0.5",
"@slack/web-api": "^7.13.0",
Expand All @@ -40,6 +41,7 @@
"next": "15.5.9",
"octokit": "^5.0.3",
"postgres": "^3.4.7",
"posthog-js": "^1.363.5",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-minimal-pie-chart": "^8.4.1",
Expand Down
2 changes: 1 addition & 1 deletion src/app/account/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export default function Page() {
<Amount valueWei={totalBalance} className="-mt-1 text-3xl md:text-4xl" />
</div>
{isVoteSigner ? (
<div className="align-right flex flex-col items-end">
<div className="ph-no-capture align-right flex flex-col items-end">
<h2 className="font-medium">Vote Signer For</h2>
<span className="hidden font-mono text-sm md:flex">{signingFor}</span>
<span className="font-mono text-sm md:hidden">{shortenAddress(signingFor!)}</span>
Expand Down
59 changes: 48 additions & 11 deletions src/app/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,19 +89,56 @@ export interface BridgeClickCount {
count: number;
}

async function getBridgeClickedCountsFromDb(): Promise<BridgeClickCount[]> {
const bridgeClickCounts = await database
.select({
bridgeId: sql<string>`properties->>'bridgeId'`,
count: sql<number>`count(distinct "sessionId")::int`,
})
.from(analyticsEventsTable)
.where(eq(analyticsEventsTable.eventName, 'bridge_clicked'))
.groupBy(sql`properties->>'bridgeId'`)
.orderBy(sql`count(distinct "sessionId") desc`);

return bridgeClickCounts;
}

async function getBridgeClickedCountsFromPostHog(): Promise<BridgeClickCount[]> {
const res = await fetch(
`https://us.posthog.com/api/projects/${process.env.POSTHOG_PROJECT_ID}/query`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTHOG_API_KEY}`,
'Content-Type': 'application/json',
Comment thread
shazarre marked this conversation as resolved.
Outdated
},
body: JSON.stringify({
query: {
kind: 'HogQLQuery',
query: `
SELECT properties.bridgeId, count() AS cnt
Comment thread
shazarre marked this conversation as resolved.
Outdated
FROM events
WHERE event = 'bridge_clicked'
GROUP BY properties.bridgeId
ORDER BY cnt DESC
`,
},
}),
// Cache the response for an hour
next: { revalidate: 3600 },
},
);

const data = await res.json();
return data.results.map(([bridgeId, count]: [string, number]) => ({ bridgeId, count }));
}

export async function getBridgeClickedCounts(): Promise<BridgeClickCount[]> {
const source = process.env.BRIDGE_COUNTS_SOURCE ?? 'database';
try {
const bridgeClickCounts = await database
.select({
bridgeId: sql<string>`properties->>'bridgeId'`,
count: sql<number>`count(distinct "sessionId")::int`,
})
.from(analyticsEventsTable)
.where(eq(analyticsEventsTable.eventName, 'bridge_clicked'))
.groupBy(sql`properties->>'bridgeId'`)
.orderBy(sql`count(distinct "sessionId") desc`);

return bridgeClickCounts;
return await (source === 'posthog'
? getBridgeClickedCountsFromPostHog()
: getBridgeClickedCountsFromDb());
Comment on lines +64 to +66
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fall back to DB counts when PostHog is unavailable

When BRIDGE_COUNTS_SOURCE is set to posthog, any transient PostHog failure (rate limit, bad key, network issue) currently drops into the shared catch path and returns an empty list, which makes bridge rankings collapse to all-zero even though the app is still writing analytics events to the local database. This introduces an avoidable production reliability regression for the bridge page; in the PostHog path, failures should fall back to getBridgeClickedCountsFromDb() instead of returning [].

Useful? React with 👍 / 👎.

} catch (error) {
// eslint-disable-next-line no-console
console.error('Error fetching bridge click counts:', error);
Expand Down
70 changes: 54 additions & 16 deletions src/app/app.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use client';
import { PostHogProvider } from '@posthog/react';
import { Analytics } from '@vercel/analytics/react';
import { PropsWithChildren } from 'react';
import posthog from 'posthog-js';
import { PropsWithChildren, useEffect } from 'react';
import { ToastContainer, Zoom } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { ErrorBoundary } from 'src/components/errors/ErrorBoundary';
Expand All @@ -17,25 +19,61 @@ import StakingModeProvider from 'src/utils/useStakingMode';
import 'src/vendor/inpage-metamask.js';
import 'src/vendor/polyfill';

function PHProvider({ children }: PropsWithChildren) {
useEffect(() => {
if (posthog.__loaded) return;
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN as string, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
person_profiles: 'never',
autocapture: true,
Comment thread
shazarre marked this conversation as resolved.
capture_pageview: true,
capture_pageleave: true,
persistence: 'sessionStorage',
defaults: '2026-01-30',
loaded: (posthog) => {
if (process.env.NODE_ENV === 'development') {
posthog.debug();
}
},
});
}, []);

useEffect(() => {
const markNoCapture = () => {
document.querySelectorAll('[data-rk] [role="dialog"]').forEach((el) => {
el.classList.add('ph-no-capture');
});
};

const observer = new MutationObserver(markNoCapture);
observer.observe(document.body, { childList: true, subtree: true });
Comment thread
shazarre marked this conversation as resolved.
return () => observer.disconnect();
}, []);

return <PostHogProvider client={posthog}>{children}</PostHogProvider>;
}

export function App({ children }: PropsWithChildren<any>) {
return (
<ErrorBoundary>
<SafeHydrate>
<WagmiContext>
<HistoryProvider>
<StakingModeProvider>
<ENSProvider>
<LegalRestrict>
<BodyLayout>{children}</BodyLayout>
</LegalRestrict>
<TransactionModal />
<ErrorBoundaryInline>
<ToastContainer transition={Zoom} position="bottom-right" limit={12} />
</ErrorBoundaryInline>
</ENSProvider>
</StakingModeProvider>
</HistoryProvider>
</WagmiContext>
<PHProvider>
<WagmiContext>
<HistoryProvider>
<StakingModeProvider>
<ENSProvider>
<LegalRestrict>
<BodyLayout>{children}</BodyLayout>
</LegalRestrict>
<TransactionModal />
<ErrorBoundaryInline>
<ToastContainer transition={Zoom} position="bottom-right" limit={12} />
</ErrorBoundaryInline>
</ENSProvider>
</StakingModeProvider>
</HistoryProvider>
</WagmiContext>
</PHProvider>
</SafeHydrate>
<Analytics />
</ErrorBoundary>
Expand Down
8 changes: 5 additions & 3 deletions src/features/wallet/WalletDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export function WalletDropdown() {
{address && isConnected ? (
<DropdownModal
button={() => (
<div className="flex items-center justify-center space-x-1">
<div className="ph-no-capture flex items-center justify-center space-x-1">
<Identicon address={address} size={26} />
<AddressLabel address={address} />
</div>
Expand Down Expand Up @@ -105,7 +105,7 @@ function DropdownContent({

return (
<div className="flex min-w-[18rem] flex-col items-center space-y-3">
<div className="flex flex-col items-center">
<div className="ph-no-capture flex flex-col items-center">
<Identicon address={address} size={34} />
<button title="Click to copy" onClick={onClickCopy} className="flex flex-col text-sm">
<AddressLabel address={address} hiddenIfNoLabel shortener={() => shortAddress} />
Expand Down Expand Up @@ -179,7 +179,9 @@ function ValueRow({
<div className="flex flex-row justify-between">
<label className="text-sm">{label} </label>
{address && (
<span className="font-mono text-xs text-taupe-600">&hellip;{address?.slice(-4)}</span>
<span className="ph-no-capture font-mono text-xs text-taupe-600">
&hellip;{address?.slice(-4)}
</span>
)}
</div>
<Amount value={value} valueWei={valueWei} className="text-xl" />
Expand Down
6 changes: 4 additions & 2 deletions src/utils/useTrackEvent.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { usePostHog } from '@posthog/react';
import { useCallback, useEffect, useState } from 'react';
import { AnalyticsEventMap, AnalyticsEventName } from 'src/types/analytics';
import { v4 as uuidv4, validate as validateUUID } from 'uuid';
import { trackEvent } from './analytics';

const SESSION_STORAGE_KEY = 'analytics_session_id';

// React hook for analytics tracking with session management
export function useTrackEvent() {
const posthog = usePostHog();
const [sessionId, setSessionId] = useState<string>('');

useEffect(() => {
Expand Down Expand Up @@ -40,8 +41,9 @@ export function useTrackEvent() {
const track = useCallback(
<T extends AnalyticsEventName>(eventName: T, properties: AnalyticsEventMap[T]) => {
trackEvent(eventName, properties, sessionId);
posthog?.capture(eventName, properties);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Redact address-like analytics fields before PostHog capture

useTrackEvent now forwards every analytics payload to PostHog, but many event payloads include raw on-chain addresses (for example groupAddress/delegateeAddress in src/types/analytics.ts). The only sanitizer added in this change (scrubEventUrlProperties) runs in before_send and only rewrites URL-related keys, so these address fields are sent unchanged to a third-party endpoint whenever those events fire. This bypasses the privacy hardening added for autocapture and should be fixed by hashing/removing address-like custom properties before calling capture.

Useful? React with 👍 / 👎.

},
[sessionId],
[sessionId, posthog],
);

return track;
Expand Down
Loading
Loading