mirror of
https://github.com/prowler-cloud/prowler.git
synced 2025-12-19 05:17:47 +00:00
Compare commits
11 Commits
d15dd53708
...
add-postho
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c89489ee3 | ||
|
|
34bf308195 | ||
|
|
7e176a620e | ||
|
|
9ccb9430d2 | ||
|
|
f7fe55d95a | ||
|
|
c7e4c3d839 | ||
|
|
d0d0ae8716 | ||
|
|
bc34c9df4a | ||
|
|
255df2dbc1 | ||
|
|
998f32f451 | ||
|
|
8a65efa441 |
4
.env
4
.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=""
|
||||
|
||||
@@ -6,6 +6,7 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- PostHog analytics integration [(#8357)](https://github.com/prowler-cloud/prowler/pull/8357)
|
||||
- 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)
|
||||
|
||||
@@ -5,8 +5,12 @@ 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 * as React from "react";
|
||||
|
||||
import { initializePostHog } from "@/lib/analytics";
|
||||
|
||||
export interface ProvidersProps {
|
||||
children: React.ReactNode;
|
||||
themeProps?: ThemeProviderProps;
|
||||
@@ -15,10 +19,17 @@ export interface ProvidersProps {
|
||||
export function Providers({ children, themeProps }: ProvidersProps) {
|
||||
const router = useRouter();
|
||||
|
||||
// Initialize PostHog
|
||||
React.useMemo(() => {
|
||||
initializePostHog();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SessionProvider>
|
||||
<NextUIProvider navigate={router.push}>
|
||||
<NextThemesProvider {...themeProps}>{children}</NextThemesProvider>
|
||||
<NextThemesProvider {...themeProps}>
|
||||
<PHProvider client={posthog}>{children}</PHProvider>
|
||||
</NextThemesProvider>
|
||||
</NextUIProvider>
|
||||
</SessionProvider>
|
||||
);
|
||||
|
||||
@@ -21,6 +21,11 @@ import {
|
||||
FormField,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import {
|
||||
initializeSession,
|
||||
trackUserLogin,
|
||||
trackUserRegistration,
|
||||
} from "@/lib/analytics";
|
||||
import { ApiError, authFormSchema } from "@/types";
|
||||
|
||||
export const AuthForm = ({
|
||||
@@ -80,6 +85,8 @@ export const AuthForm = ({
|
||||
const isSamlMode = form.watch("isSamlMode");
|
||||
|
||||
const onSubmit = async (data: z.infer<typeof formSchema>) => {
|
||||
//getting an new posthog init
|
||||
initializeSession();
|
||||
if (type === "sign-in") {
|
||||
if (data.isSamlMode) {
|
||||
const email = data.email.toLowerCase();
|
||||
@@ -107,6 +114,7 @@ export const AuthForm = ({
|
||||
password: data.password,
|
||||
});
|
||||
if (result?.message === "Success") {
|
||||
trackUserLogin({ email: data.email });
|
||||
router.push("/");
|
||||
} else if (result?.errors && "credentials" in result.errors) {
|
||||
form.setError("email", {
|
||||
@@ -128,6 +136,11 @@ export const AuthForm = ({
|
||||
const newUser = await createNewUser(data);
|
||||
|
||||
if (!newUser.errors) {
|
||||
trackUserRegistration({
|
||||
email: data.email,
|
||||
fullName: data.name,
|
||||
company: data.company,
|
||||
});
|
||||
toast({
|
||||
title: "Success!",
|
||||
description: "The user was registered successfully.",
|
||||
|
||||
@@ -19,6 +19,7 @@ import { CheckIcon, RocketIcon } from "@/components/icons";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomButton } from "@/components/ui/custom";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { trackCloudConnectionSuccess } from "@/lib/analytics";
|
||||
import { checkTaskStatus } from "@/lib/helper";
|
||||
import { ProviderType } from "@/types";
|
||||
import { ApiError, testConnectionFormSchema } from "@/types";
|
||||
@@ -145,6 +146,12 @@ export const TestConnectionForm = ({
|
||||
});
|
||||
} else {
|
||||
setIsRedirecting(true);
|
||||
// Track cloud connection success event
|
||||
trackCloudConnectionSuccess({
|
||||
providerType: providerType,
|
||||
providerAlias: providerData.data.attributes.alias,
|
||||
scanType: form.watch("runOnce") ? "single" : "scheduled",
|
||||
});
|
||||
router.push("/scans");
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
177
ui/lib/analytics.ts
Normal file
177
ui/lib/analytics.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import posthog from "posthog-js";
|
||||
|
||||
// Initialize PostHog
|
||||
export const initializePostHog = (): void => {
|
||||
if (typeof window === "undefined") return; // Don't initialize on server side
|
||||
|
||||
try {
|
||||
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,
|
||||
session_recording: {
|
||||
maskAllInputs: true,
|
||||
maskTextSelector: "*",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize PostHog:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Type definitions for tracking payloads
|
||||
export interface UserLoginPayload {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface UserRegistrationPayload {
|
||||
email: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
company?: string;
|
||||
fullName?: string;
|
||||
}
|
||||
|
||||
export interface CloudConnectionPayload {
|
||||
providerType: string;
|
||||
providerAlias: string;
|
||||
scanType: "single" | "scheduled";
|
||||
}
|
||||
|
||||
// Initialize a new PostHog session
|
||||
export const initializeSession = (): void => {
|
||||
try {
|
||||
posthog.reset(true);
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize PostHog session:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Identify a user in PostHog
|
||||
export const identifyUser = (email: string): void => {
|
||||
try {
|
||||
posthog.identify(email.toLowerCase());
|
||||
} catch (error) {
|
||||
console.error("Failed to identify user in PostHog:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Track user login event
|
||||
export const trackUserLogin = ({ email }: UserLoginPayload): void => {
|
||||
try {
|
||||
const normalizedEmail = email.toLowerCase();
|
||||
identifyUser(normalizedEmail);
|
||||
posthog.capture("userLogin", {
|
||||
email: normalizedEmail,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to track user login:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Track user registration event
|
||||
export const trackUserRegistration = ({
|
||||
email,
|
||||
firstName = "",
|
||||
lastName = "",
|
||||
company = "",
|
||||
fullName = "",
|
||||
}: UserRegistrationPayload): void => {
|
||||
try {
|
||||
// Parse name if fullName is provided
|
||||
let first = firstName;
|
||||
let last = lastName;
|
||||
|
||||
if (fullName && (!firstName || !lastName)) {
|
||||
const nameParts = fullName.trim().split(" ");
|
||||
first = nameParts[0] || "";
|
||||
last = nameParts.slice(1).join(" ") || "";
|
||||
}
|
||||
|
||||
posthog.capture("userRegistered", {
|
||||
email: email.toLowerCase(),
|
||||
firstName: first,
|
||||
lastName: last,
|
||||
company: company,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to track user registration:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Track cloud connection success
|
||||
export const trackCloudConnectionSuccess = ({
|
||||
providerType,
|
||||
providerAlias,
|
||||
scanType,
|
||||
}: CloudConnectionPayload): void => {
|
||||
try {
|
||||
posthog.capture("cloudConnectionSuccess", {
|
||||
providerType: providerType,
|
||||
providerAlias: providerAlias,
|
||||
scanType: scanType,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to track cloud connection success:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Generic event tracking function for custom events
|
||||
export const trackEvent = (
|
||||
eventName: string,
|
||||
properties?: Record<string, any>,
|
||||
): void => {
|
||||
try {
|
||||
posthog.capture(eventName, {
|
||||
...properties,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Failed to track event "${eventName}":`, error);
|
||||
}
|
||||
};
|
||||
|
||||
// Track page view
|
||||
export const trackPageView = (
|
||||
pageName: string,
|
||||
properties?: Record<string, any>,
|
||||
): void => {
|
||||
try {
|
||||
posthog.capture("$pageview", {
|
||||
$current_url: window.location.href,
|
||||
pageName,
|
||||
...properties,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to track page view:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Set user properties
|
||||
export const setUserProperties = (properties: Record<string, any>): void => {
|
||||
try {
|
||||
posthog.people.set(properties);
|
||||
} catch (error) {
|
||||
console.error("Failed to set user properties:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Check if PostHog is initialized and ready
|
||||
export const isAnalyticsReady = (): boolean => {
|
||||
try {
|
||||
return (
|
||||
typeof window !== "undefined" &&
|
||||
typeof posthog !== "undefined" &&
|
||||
posthog.__loaded === true
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
13
ui/lib/posthog.ts
Normal file
13
ui/lib/posthog.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
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.
|
||||
const PostHogClient = () => {
|
||||
const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
|
||||
host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
flushAt: 1,
|
||||
flushInterval: 0,
|
||||
});
|
||||
return posthogClient;
|
||||
};
|
||||
|
||||
export default PostHogClient;
|
||||
@@ -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';
|
||||
`;
|
||||
|
||||
|
||||
58
ui/package-lock.json
generated
58
ui/package-lock.json
generated
@@ -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",
|
||||
@@ -8322,6 +8324,17 @@
|
||||
"simple-wcswidth": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.44.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.44.0.tgz",
|
||||
"integrity": "sha512-aFCtd4l6GvAXwVEh3XbbVqJGHDJt0OZRa+5ePGx3LLwi12WfexqQxcsohb2wgsa/92xtl19Hd66G/L+TaAxDMw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/cosmiconfig": {
|
||||
"version": "8.3.6",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
|
||||
@@ -9969,6 +9982,12 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.4.8",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
|
||||
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||
@@ -13913,6 +13932,39 @@
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.258.2",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.258.2.tgz",
|
||||
"integrity": "sha512-XBSeiN4HjiYsy3tW5zss8WOJF2JXTQXAYw2wZ+zjqQuzzi7kkLEXjIgsVrBnt5Opwhqn0krZVsb0ZBw34dIiyQ==",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"core-js": "^3.38.1",
|
||||
"fflate": "^0.4.8",
|
||||
"preact": "^10.19.3",
|
||||
"web-vitals": "^4.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rrweb/types": "2.0.0-alpha.17",
|
||||
"rrweb-snapshot": "2.0.0-alpha.17"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@rrweb/types": {
|
||||
"optional": true
|
||||
},
|
||||
"rrweb-snapshot": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/posthog-node": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.6.0.tgz",
|
||||
"integrity": "sha512-MVXxKmqAYp2cPBrN1YMhnhYsJYIu6yc6wumbHz1dbo67wZBf2WtMm67Uh+4VCrp07049qierWlxQqz1W5zGDeg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.24.3",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
|
||||
@@ -16547,6 +16599,12 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/web-vitals": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
|
||||
"integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user