diff --git a/.env b/.env
index 4a0e1ef2d4..4bf45159bb 100644
--- a/.env
+++ b/.env
@@ -150,3 +150,7 @@ LANGSMITH_TRACING=false
LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
LANGSMITH_API_KEY=""
LANGCHAIN_PROJECT=""
+
+# posthog integration
+NEXT_PUBLIC_POSTHOG_KEY=""
+NEXT_PUBLIC_POSTHOG_HOST=https://eu.i.posthog.com
diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md
index 0915f7aea4..9fa8853737 100644
--- a/ui/CHANGELOG.md
+++ b/ui/CHANGELOG.md
@@ -6,6 +6,8 @@ All notable changes to the **Prowler UI** are documented in this file.
### 🚀 Added
+- PostHog analytics integration
+[(#PR_NUMBER)](https://github.com/prowler-cloud/prowler/pull/PR_NUMBER)
- Mutelist configuration form [(#8190)](https://github.com/prowler-cloud/prowler/pull/8190)
- SAML login integration [(#8203)](https://github.com/prowler-cloud/prowler/pull/8203)
- Resource view [(#7760)](https://github.com/prowler-cloud/prowler/pull/7760)
diff --git a/ui/app/providers.tsx b/ui/app/providers.tsx
index 942bce43dc..d5ff088ad6 100644
--- a/ui/app/providers.tsx
+++ b/ui/app/providers.tsx
@@ -5,6 +5,9 @@ import { useRouter } from "next/navigation";
import { SessionProvider } from "next-auth/react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { ThemeProviderProps } from "next-themes/dist/types";
+import posthog from "posthog-js";
+import { PostHogProvider as PHProvider } from "posthog-js/react";
+import { useEffect } from "react";
import * as React from "react";
export interface ProvidersProps {
@@ -15,10 +18,24 @@ export interface ProvidersProps {
export function Providers({ children, themeProps }: ProvidersProps) {
const router = useRouter();
+ useEffect(() => {
+ posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
+ api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
+ ui_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
+ autocapture: false,
+ defaults: "2025-05-24",
+ capture_exceptions: true,
+ capture_pageview: false,
+ capture_pageleave: false,
+ });
+ }, []);
+
return (
- {children}
+
+ {children}
+
);
diff --git a/ui/components/auth/oss/auth-form.tsx b/ui/components/auth/oss/auth-form.tsx
index a965204dbc..7c68363af8 100644
--- a/ui/components/auth/oss/auth-form.tsx
+++ b/ui/components/auth/oss/auth-form.tsx
@@ -4,6 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { Icon } from "@iconify/react";
import { Button, Checkbox, Divider, Link, Tooltip } from "@nextui-org/react";
import { useRouter, useSearchParams } from "next/navigation";
+import posthog from "posthog-js";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
@@ -80,6 +81,8 @@ export const AuthForm = ({
const isSamlMode = form.watch("isSamlMode");
const onSubmit = async (data: z.infer) => {
+ //getting an new posthog init
+ posthog.reset(true);
if (type === "sign-in") {
if (data.isSamlMode) {
const email = data.email.toLowerCase();
@@ -107,6 +110,11 @@ export const AuthForm = ({
password: data.password,
});
if (result?.message === "Success") {
+ posthog.identify(data.email.toLowerCase());
+ posthog.capture("user_login", {
+ email: data.email.toLocaleLowerCase(),
+ timestamp: Date.now(),
+ });
router.push("/");
} else if (result?.errors && "credentials" in result.errors) {
form.setError("email", {
@@ -128,6 +136,20 @@ export const AuthForm = ({
const newUser = await createNewUser(data);
if (!newUser.errors) {
+ let firstName = "";
+ let lastName = "";
+ if (data.name) {
+ const nameParts = data.name.trim().split(" ");
+ firstName = nameParts[0] || "";
+ lastName = nameParts.slice(1).join(" ") || "";
+ }
+ posthog.capture("user_registered", {
+ email: data.email.toLocaleLowerCase(),
+ firstname: firstName,
+ lastname: lastName,
+ company: data.company || "",
+ timestamp: Date.now(),
+ });
toast({
title: "Success!",
description: "The user was registered successfully.",
diff --git a/ui/components/providers/workflow/forms/test-connection-form.tsx b/ui/components/providers/workflow/forms/test-connection-form.tsx
index 6fdc6e2215..9cd0bb7718 100644
--- a/ui/components/providers/workflow/forms/test-connection-form.tsx
+++ b/ui/components/providers/workflow/forms/test-connection-form.tsx
@@ -5,6 +5,7 @@ import { Icon } from "@iconify/react";
import { Checkbox } from "@nextui-org/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
+import posthog from "posthog-js";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
@@ -145,6 +146,13 @@ export const TestConnectionForm = ({
});
} else {
setIsRedirecting(true);
+ // Capture PostHog event for successful cloud connection
+ posthog.capture("cloud_connection_success", {
+ provider_type: providerType,
+ provider_alias: providerData.data.attributes.alias,
+ scan_type: form.watch("runOnce") ? "single" : "scheduled",
+ timestamp: Date.now(),
+ });
router.push("/scans");
}
} catch (error) {
diff --git a/ui/lib/posthog.ts b/ui/lib/posthog.ts
new file mode 100644
index 0000000000..057cdcb5a3
--- /dev/null
+++ b/ui/lib/posthog.ts
@@ -0,0 +1,11 @@
+import { PostHog } from "posthog-node";
+
+// NOTE: This is a Node.js client, so you can use it for sending events from the server side to PostHog.
+export default function PostHogClient() {
+ const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
+ host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
+ flushAt: 1,
+ flushInterval: 0,
+ });
+ return posthogClient;
+}
diff --git a/ui/next.config.js b/ui/next.config.js
index af64900d4c..41e031f484 100644
--- a/ui/next.config.js
+++ b/ui/next.config.js
@@ -4,12 +4,12 @@
// 'unsafe-eval' is configured under `script-src` because it is required by NextJS for development mode
const cspHeader = `
default-src 'self';
- script-src 'self' 'unsafe-inline' 'unsafe-eval' https://js.stripe.com https://www.googletagmanager.com;
- connect-src 'self' https://api.iconify.design https://api.simplesvg.com https://api.unisvg.com https://js.stripe.com https://www.googletagmanager.com;
- img-src 'self' https://www.google-analytics.com https://www.googletagmanager.com;
+ script-src 'self' 'unsafe-inline' 'unsafe-eval' https://js.stripe.com https://www.googletagmanager.com https://*.posthog.com;
+ connect-src 'self' https://api.iconify.design https://api.simplesvg.com https://api.unisvg.com https://js.stripe.com https://www.googletagmanager.com https://*.posthog.com;
+ img-src 'self' https://www.google-analytics.com https://www.googletagmanager.com https://*.posthog.com;
font-src 'self';
style-src 'self' 'unsafe-inline';
- frame-src 'self' https://js.stripe.com https://www.googletagmanager.com;
+ frame-src 'self' https://js.stripe.com https://www.googletagmanager.com https://*.posthog.com;
frame-ancestors 'none';
`;
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 326bf8717a..5c22690d0d 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -47,6 +47,8 @@
"next": "^14.2.30",
"next-auth": "^5.0.0-beta.25",
"next-themes": "^0.2.1",
+ "posthog-js": "^1.256.1",
+ "posthog-node": "^5.1.1",
"radix-ui": "^1.1.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
diff --git a/ui/package.json b/ui/package.json
index da794f3b33..d4309c971a 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -39,6 +39,8 @@
"next": "^14.2.30",
"next-auth": "^5.0.0-beta.25",
"next-themes": "^0.2.1",
+ "posthog-js": "^1.256.1",
+ "posthog-node": "^5.1.1",
"radix-ui": "^1.1.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",