PostHog event pass (#246)

This commit is contained in:
Brendan Kellam 2025-03-26 16:01:29 -07:00 committed by GitHub
parent 78b8916797
commit 8e3235a56d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 81 additions and 38 deletions

View file

@ -34,7 +34,7 @@ AUTH_URL="http://localhost:3000"
# Sentry # Sentry
# SENTRY_BACKEND_DSN="" # SENTRY_BACKEND_DSN=""
# NEXT_PUBLIC_SENTRY_WEBAPP_DSN="" # NEXT_PUBLIC_SENTRY_WEBAPP_DSN=""
SENTRY_ENVIRONMENT="dev" # SENTRY_ENVIRONMENT="dev"
# NEXT_PUBLIC_SENTRY_ENVIRONMENT="dev" # NEXT_PUBLIC_SENTRY_ENVIRONMENT="dev"
# SENTRY_AUTH_TOKEN= # SENTRY_AUTH_TOKEN=
@ -76,4 +76,6 @@ SOURCEBOT_TELEMETRY_DISABLED=true # Disables telemetry collection
# CONFIG_MAX_REPOS_NO_TOKEN= # CONFIG_MAX_REPOS_NO_TOKEN=
# NODE_ENV= # NODE_ENV=
# SOURCEBOT_TENANCY_MODE=single # SOURCEBOT_TENANCY_MODE=single
# NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT=

View file

@ -9,6 +9,7 @@ ARG SOURCEBOT_VERSION
# @see: https://posthog.com/tutorials/api-capture-events#authenticating-with-the-project-api-key # @see: https://posthog.com/tutorials/api-capture-events#authenticating-with-the-project-api-key
ARG POSTHOG_PAPIK ARG POSTHOG_PAPIK
ARG SENTRY_ENVIRONMENT ARG SENTRY_ENVIRONMENT
ARG SOURCEBOT_CLOUD_ENVIRONMENT
FROM node:20-alpine3.19 AS node-alpine FROM node:20-alpine3.19 AS node-alpine
FROM golang:1.23.4-alpine3.19 AS go-alpine FROM golang:1.23.4-alpine3.19 AS go-alpine
@ -52,9 +53,11 @@ ARG POSTHOG_PAPIK
ENV NEXT_PUBLIC_POSTHOG_PAPIK=$POSTHOG_PAPIK ENV NEXT_PUBLIC_POSTHOG_PAPIK=$POSTHOG_PAPIK
ARG SENTRY_ENVIRONMENT ARG SENTRY_ENVIRONMENT
ENV NEXT_PUBLIC_SENTRY_ENVIRONMENT=$SENTRY_ENVIRONMENT ENV NEXT_PUBLIC_SENTRY_ENVIRONMENT=$SENTRY_ENVIRONMENT
ARG SOURCEBOT_CLOUD_ENVIRONMENT
ENV NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT=$SOURCEBOT_CLOUD_ENVIRONMENT
# Local args # Local args
ARG SENTRY_WEBAPP_DSN ARG NEXT_PUBLIC_SENTRY_WEBAPP_DSN
ENV NEXT_PUBLIC_SENTRY_WEBAPP_DSN=$SENTRY_WEBAPP_DSN ENV NEXT_PUBLIC_SENTRY_WEBAPP_DSN=$NEXT_PUBLIC_SENTRY_WEBAPP_DSN
# ----------- # -----------
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
@ -104,6 +107,8 @@ ARG SOURCEBOT_VERSION
ENV SOURCEBOT_VERSION=$SOURCEBOT_VERSION ENV SOURCEBOT_VERSION=$SOURCEBOT_VERSION
ARG POSTHOG_PAPIK ARG POSTHOG_PAPIK
ENV POSTHOG_PAPIK=$POSTHOG_PAPIK ENV POSTHOG_PAPIK=$POSTHOG_PAPIK
ARG SENTRY_ENVIRONMENT
ENV SENTRY_ENVIRONMENT=$SENTRY_ENVIRONMENT
# Local args # Local args
# ----------- # -----------

View file

@ -32,6 +32,7 @@ import { useSession } from "next-auth/react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { signOut } from "next-auth/react" import { signOut } from "next-auth/react"
import { env } from "@/env.mjs"; import { env } from "@/env.mjs";
import posthog from "posthog-js";
interface SettingsDropdownProps { interface SettingsDropdownProps {
menuButtonClassName?: string; menuButtonClassName?: string;
@ -94,7 +95,9 @@ export const SettingsDropdown = ({
onClick={() => { onClick={() => {
signOut({ signOut({
redirectTo: "/login", redirectTo: "/login",
}); }).then(() => {
posthog.reset();
})
}} }}
> >
<LogOut className="mr-2 h-4 w-4" /> <LogOut className="mr-2 h-4 w-4" />

View file

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useEffect, useState } from "react"; import { useState } from "react";
import { CodeHostType } from "@/lib/utils"; import { CodeHostType } from "@/lib/utils";
import { getCodeHostIcon } from "@/lib/utils"; import { getCodeHostIcon } from "@/lib/utils";
import { import {
@ -15,8 +15,6 @@ import { OnboardingSteps } from "@/lib/constants";
import { BackButton } from "./onboardBackButton"; import { BackButton } from "./onboardBackButton";
import { CodeHostIconButton } from "../../components/codeHostIconButton"; import { CodeHostIconButton } from "../../components/codeHostIconButton";
import useCaptureEvent from "@/hooks/useCaptureEvent"; import useCaptureEvent from "@/hooks/useCaptureEvent";
import { useSession } from "next-auth/react";
import posthog from "posthog-js";
import SecurityCard from "@/app/components/securityCard"; import SecurityCard from "@/app/components/securityCard";
interface ConnectCodeHostProps { interface ConnectCodeHostProps {
@ -27,18 +25,6 @@ interface ConnectCodeHostProps {
export const ConnectCodeHost = ({ nextStep, securityCardEnabled }: ConnectCodeHostProps) => { export const ConnectCodeHost = ({ nextStep, securityCardEnabled }: ConnectCodeHostProps) => {
const [selectedCodeHost, setSelectedCodeHost] = useState<CodeHostType | null>(null); const [selectedCodeHost, setSelectedCodeHost] = useState<CodeHostType | null>(null);
const router = useRouter(); const router = useRouter();
const { data: session } = useSession();
// Note: this is currently the first client side page that gets loaded after a user registers. If this changes, we need to update this.
// @nocheckin
useEffect(() => {
if (session?.user) {
posthog.identify(session.user.id, {
email: session.user.email,
name: session.user.name,
});
}
}, [session?.user]);
const onCreated = useCallback(() => { const onCreated = useCallback(() => {
router.push(`?step=${nextStep}`); router.push(`?step=${nextStep}`);

View file

@ -1,5 +1,7 @@
import { LogOutIcon } from "lucide-react"; import { LogOutIcon } from "lucide-react";
import { signOut } from "@/auth"; import { signOut } from "@/auth";
import posthog from "posthog-js";
interface LogoutEscapeHatchProps { interface LogoutEscapeHatchProps {
className?: string; className?: string;
} }
@ -14,6 +16,8 @@ export const LogoutEscapeHatch = ({
"use server"; "use server";
await signOut({ await signOut({
redirectTo: "/login", redirectTo: "/login",
}).then(() => {
posthog.reset();
}); });
}} }}
> >

View file

@ -13,6 +13,12 @@ import { SourcebotLogo } from "@/app/components/sourcebotLogo";
import { TextSeparator } from "@/app/components/textSeparator"; import { TextSeparator } from "@/app/components/textSeparator";
import useCaptureEvent from "@/hooks/useCaptureEvent"; import useCaptureEvent from "@/hooks/useCaptureEvent";
import DemoCard from "@/app/[domain]/onboard/components/demoCard"; import DemoCard from "@/app/[domain]/onboard/components/demoCard";
import Link from "next/link";
import { env } from "@/env.mjs";
const TERMS_OF_SERVICE_URL = "https://sourcebot.dev/terms";
const PRIVACY_POLICY_URL = "https://sourcebot.dev/privacy";
interface LoginFormProps { interface LoginFormProps {
callbackUrl?: string; callbackUrl?: string;
error?: string; error?: string;
@ -98,6 +104,9 @@ export const LoginForm = ({ callbackUrl, error, enabledMethods }: LoginFormProps
]} ]}
/> />
</Card> </Card>
{env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT !== undefined && (
<p className="text-xs text-muted-foreground mt-8">By signing in, you agree to the <Link className="underline" href={TERMS_OF_SERVICE_URL} target="_blank">Terms of Service</Link> and <Link className="underline" href={PRIVACY_POLICY_URL} target="_blank">Privacy Policy</Link>.</p>
)}
</div> </div>
) )
} }

View file

@ -5,13 +5,15 @@ import { PostHogProvider as PHProvider } from 'posthog-js/react'
import { usePathname, useSearchParams } from "next/navigation" import { usePathname, useSearchParams } from "next/navigation"
import { Suspense, useEffect } from "react" import { Suspense, useEffect } from "react"
import { env } from '@/env.mjs' import { env } from '@/env.mjs'
import { useSession } from 'next-auth/react'
import { captureEvent } from '@/hooks/useCaptureEvent'
// @see: https://posthog.com/docs/libraries/next-js#capturing-pageviews
function PostHogPageView() { function PostHogPageView() {
const pathname = usePathname() const pathname = usePathname()
const searchParams = useSearchParams() const searchParams = useSearchParams()
const posthog = usePostHog() const posthog = usePostHog()
// Track pageviews
useEffect(() => { useEffect(() => {
if (pathname && posthog) { if (pathname && posthog) {
let url = window.origin + pathname let url = window.origin + pathname
@ -19,7 +21,9 @@ function PostHogPageView() {
url = url + `?${searchParams.toString()}` url = url + `?${searchParams.toString()}`
} }
posthog.capture('$pageview', { '$current_url': url }) captureEvent('$pageview', {
$current_url: url,
});
} }
}, [pathname, searchParams, posthog]) }, [pathname, searchParams, posthog])
@ -32,34 +36,55 @@ interface PostHogProviderProps {
} }
export function PostHogProvider({ children, disabled }: PostHogProviderProps) { export function PostHogProvider({ children, disabled }: PostHogProviderProps) {
const { data: session } = useSession();
useEffect(() => { useEffect(() => {
if (!disabled && env.NEXT_PUBLIC_POSTHOG_PAPIK) { if (!disabled && env.NEXT_PUBLIC_POSTHOG_PAPIK) {
console.debug(`PostHog telemetry enabled. Cloud environment: ${env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT}`);
posthog.init(env.NEXT_PUBLIC_POSTHOG_PAPIK, { posthog.init(env.NEXT_PUBLIC_POSTHOG_PAPIK, {
// @see next.config.mjs for path rewrites to the "/ingest" route. // @see next.config.mjs for path rewrites to the "/ingest" route.
api_host: "/ingest", api_host: "/ingest",
person_profiles: 'identified_only', person_profiles: 'identified_only',
capture_pageview: false, capture_pageview: false,
autocapture: false, autocapture: false,
// eslint-disable-next-line @typescript-eslint/no-explicit-any // In self-hosted mode, we don't want to capture the following
sanitize_properties: (properties: Record<string, any>, _event: string) => { // default properties.
// https://posthog.com/docs/libraries/js#config // @see: https://posthog.com/docs/data/events#default-properties
if (properties['$current_url']) { property_denylist: env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT === undefined ? [
properties['$current_url'] = null; '$current_url',
} '$pathname',
if (properties['$ip']) { '$session_entry_url',
properties['$ip'] = null; '$session_entry_host',
} '$session_entry_pathname',
'$session_entry_referrer',
return properties; '$session_entry_referring_domain',
} '$referrer',
'$referring_domain',
'$ip',
] : []
}); });
} else { } else {
console.log("PostHog telemetry disabled"); console.debug("PostHog telemetry disabled");
} }
}, [disabled]) }, [disabled]);
useEffect(() => {
if (!session) {
return;
}
// Only identify the user if we are running in a cloud environment.
if (env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT !== undefined) {
posthog.identify(session.user.id, {
email: session.user.email,
name: session.user.name,
});
}
}, [session]);
return ( return (
<PHProvider client={posthog}> <PHProvider client={posthog}>
{/* @see: https://github.com/vercel/next.js/issues/51581 */}
<Suspense fallback={null}> <Suspense fallback={null}>
<PostHogPageView /> <PostHogPageView />
</Suspense> </Suspense>

View file

@ -50,8 +50,10 @@ export const env = createEnv({
// Misc UI flags // Misc UI flags
SECURITY_CARD_ENABLED: booleanSchema.default('false'), SECURITY_CARD_ENABLED: booleanSchema.default('false'),
}, },
// @NOTE: Make sure you destructure all client variables in the // @NOTE: Please make sure of the following:
// `experimental__runtimeEnv` block below. // - Make sure you destructure all client variables in
// the `experimental__runtimeEnv` block below.
// - Update the Dockerfile to pass these variables as build-args.
client: { client: {
// PostHog // PostHog
NEXT_PUBLIC_POSTHOG_PAPIK: z.string().optional(), NEXT_PUBLIC_POSTHOG_PAPIK: z.string().optional(),
@ -59,12 +61,15 @@ export const env = createEnv({
// Misc // Misc
NEXT_PUBLIC_SOURCEBOT_VERSION: z.string().default('unknown'), NEXT_PUBLIC_SOURCEBOT_VERSION: z.string().default('unknown'),
NEXT_PUBLIC_POLLING_INTERVAL_MS: numberSchema.default(5000), NEXT_PUBLIC_POLLING_INTERVAL_MS: numberSchema.default(5000),
NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT: z.enum(["dev", "staging", "prod"]).optional(),
}, },
// For Next.js >= 13.4.4, you only need to destructure client variables: // For Next.js >= 13.4.4, you only need to destructure client variables:
experimental__runtimeEnv: { experimental__runtimeEnv: {
NEXT_PUBLIC_POSTHOG_PAPIK: process.env.NEXT_PUBLIC_POSTHOG_PAPIK, NEXT_PUBLIC_POSTHOG_PAPIK: process.env.NEXT_PUBLIC_POSTHOG_PAPIK,
NEXT_PUBLIC_SOURCEBOT_VERSION: process.env.NEXT_PUBLIC_SOURCEBOT_VERSION, NEXT_PUBLIC_SOURCEBOT_VERSION: process.env.NEXT_PUBLIC_SOURCEBOT_VERSION,
NEXT_PUBLIC_POLLING_INTERVAL_MS: process.env.NEXT_PUBLIC_POLLING_INTERVAL_MS, NEXT_PUBLIC_POLLING_INTERVAL_MS: process.env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT: process.env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT,
}, },
skipValidation: process.env.SKIP_ENV_VALIDATION === "1", skipValidation: process.env.SKIP_ENV_VALIDATION === "1",
emptyStringAsUndefined: true, emptyStringAsUndefined: true,

View file

@ -244,6 +244,10 @@ export type PosthogEventMap = {
wa_demo_card_click: {}, wa_demo_card_click: {},
wa_demo_try_card_pressed: {}, wa_demo_try_card_pressed: {},
wa_share_link_created: {}, wa_share_link_created: {},
//////////////////////////////////////////////////////////////////
$pageview: {
$current_url: string,
},
} }
export type PosthogEvent = keyof PosthogEventMap; export type PosthogEvent = keyof PosthogEventMap;