mirror of
https://github.com/Xevion/rdap.git
synced 2025-12-08 04:08:11 -06:00
feat: add PostHog telemetry with privacy-focused tracking
Integrate optional PostHog telemetry to track page views, RDAP queries, user interactions, and errors while maintaining user privacy. Key features: - Type-safe event tracking with discriminated unions - Automatic source map upload for production error tracking - Privacy protections (query targets excluded from successful lookups) - Do Not Track (DNT) header support - Optional telemetry (disabled by default without environment variables) - Error boundary with automatic error tracking - Context-based telemetry integration throughout UI components Environment variables required for telemetry: - NEXT_PUBLIC_POSTHOG_KEY: PostHog project API key (client-side) - NEXT_PUBLIC_POSTHOG_HOST: PostHog API endpoint (client-side) - POSTHOG_PERSONAL_API_KEY: Source map upload key (server-side)
This commit is contained in:
@@ -3,6 +3,7 @@ import { useState, useCallback, useEffect, useRef } from "react";
|
||||
import { CheckIcon, ClipboardIcon } from "@radix-ui/react-icons";
|
||||
import type { IconButtonProps } from "@radix-ui/themes";
|
||||
import { IconButton, Tooltip } from "@radix-ui/themes";
|
||||
import { useTelemetry } from "@/contexts/TelemetryContext";
|
||||
|
||||
/**
|
||||
* Duration in milliseconds for how long the "copied" state persists
|
||||
@@ -64,6 +65,7 @@ const CopyButton: FunctionComponent<CopyButtonProps> = ({
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [tooltipOpen, setTooltipOpen] = useState(false);
|
||||
const forceOpenRef = useRef(false);
|
||||
const { track } = useTelemetry();
|
||||
|
||||
// Consolidated timer effect: Reset copied state, tooltip, and force-open flag
|
||||
useEffect(() => {
|
||||
@@ -85,12 +87,22 @@ const CopyButton: FunctionComponent<CopyButtonProps> = ({
|
||||
navigator.clipboard.writeText(value).then(
|
||||
() => {
|
||||
setCopied(true);
|
||||
|
||||
// Track copy action
|
||||
track({
|
||||
name: "user_interaction",
|
||||
properties: {
|
||||
action: "copy_button",
|
||||
component: "CopyButton",
|
||||
value: value.length, // Track length instead of actual value for privacy
|
||||
},
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
console.error("Failed to copy to clipboard:", err);
|
||||
}
|
||||
);
|
||||
}, [value]);
|
||||
}, [value, track]);
|
||||
|
||||
const handleTooltipOpenChange = useCallback((open: boolean) => {
|
||||
// Don't allow the tooltip to close if we're in the forced-open period
|
||||
|
||||
92
src/components/ErrorBoundary.tsx
Normal file
92
src/components/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Error boundary component that catches React errors and tracks them via telemetry.
|
||||
*/
|
||||
|
||||
import type { ReactNode, ErrorInfo } from "react";
|
||||
import { Component } from "react";
|
||||
import { Box, Flex, Heading, Text, Button } from "@radix-ui/themes";
|
||||
import { telemetry } from "@/telemetry/client";
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
fallback?: (error: Error) => ReactNode;
|
||||
}
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
constructor(props: ErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
||||
// Track the error via telemetry
|
||||
telemetry.track({
|
||||
name: "error",
|
||||
properties: {
|
||||
errorType: "runtime_error",
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
context: {
|
||||
componentStack: errorInfo.componentStack,
|
||||
route: typeof window !== "undefined" ? window.location.pathname : "unknown",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Log to console for debugging
|
||||
console.error("ErrorBoundary caught an error:", error, errorInfo);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError && this.state.error) {
|
||||
// If custom fallback is provided, use it
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback(this.state.error);
|
||||
}
|
||||
|
||||
// Default fallback UI using Radix components
|
||||
return (
|
||||
<Flex direction="column" align="center" justify="center" p="9" gap="4">
|
||||
<Box>
|
||||
<Heading size="6" align="center" mb="2">
|
||||
Something went wrong
|
||||
</Heading>
|
||||
<Text align="center" color="gray">
|
||||
{this.state.error.message}
|
||||
</Text>
|
||||
</Box>
|
||||
<Flex gap="3">
|
||||
<Button
|
||||
variant="soft"
|
||||
onClick={() => {
|
||||
this.setState({ hasError: false, error: null });
|
||||
}}
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
Reload Page
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
@@ -4,6 +4,7 @@ import { useTheme } from "next-themes";
|
||||
import { IconButton } from "@radix-ui/themes";
|
||||
import { MoonIcon, SunIcon, DesktopIcon } from "@radix-ui/react-icons";
|
||||
import { useEffect, useState, type ReactElement } from "react";
|
||||
import { useTelemetry } from "@/contexts/TelemetryContext";
|
||||
|
||||
type Theme = "light" | "dark" | "system";
|
||||
|
||||
@@ -21,6 +22,7 @@ const isTheme = (value: string | undefined): value is Theme => {
|
||||
|
||||
export const ThemeToggle = () => {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { track } = useTelemetry();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
// Avoid hydration mismatch by only rendering after mount
|
||||
@@ -36,6 +38,16 @@ export const ThemeToggle = () => {
|
||||
const { icon, next } = THEME_CONFIG[currentTheme];
|
||||
|
||||
const toggleTheme = () => {
|
||||
// Track theme change
|
||||
track({
|
||||
name: "user_interaction",
|
||||
properties: {
|
||||
action: "theme_toggle",
|
||||
component: "ThemeToggle",
|
||||
value: next,
|
||||
},
|
||||
});
|
||||
|
||||
setTheme(next);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user