diff --git a/apps/basic-integration/laravel/laravel12-saas/.claude/skills/integration-laravel/.posthog-wizard b/apps/basic-integration/laravel/laravel12-saas/.claude/skills/integration-laravel/.posthog-wizard new file mode 100644 index 000000000..e69de29bb diff --git a/apps/basic-integration/laravel/laravel12-saas/.claude/skills/integration-laravel/references/identify-users.md b/apps/basic-integration/laravel/laravel12-saas/.claude/skills/integration-laravel/references/identify-users.md new file mode 100644 index 000000000..8647dcb37 --- /dev/null +++ b/apps/basic-integration/laravel/laravel12-saas/.claude/skills/integration-laravel/references/identify-users.md @@ -0,0 +1,307 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Identify users - Docs + +Copy page + +# Identify users - Docs + +Linking events to specific users enables you to build a full picture of how they're using your product across different sessions, devices, and platforms. + +This is straightforward to do when [capturing backend events](/docs/product-analytics/capture-events?tab=Node.js.md), as you associate events to a specific user using a `distinct_id`, which is a required argument. + +However, in the frontend of a [web](/docs/libraries/js/usage.md#capturing-events) or [mobile app](/docs/libraries/ios.md#capturing-events), a `distinct_id` is not a required argument — PostHog's SDKs will generate an anonymous `distinct_id` for you automatically and you can capture events anonymously, provided you use the appropriate [configuration](/docs/libraries/js/usage.md#capturing-anonymous-events). + +To link events to specific users, call `identify`: + +PostHog AI + +### Web + +```javascript +posthog.identify( + 'distinct_id', // Replace 'distinct_id' with your user's unique identifier + { email: 'max@hedgehogmail.com', name: 'Max Hedgehog' } // optional: set additional person properties +); +``` + +### Android + +```kotlin +PostHog.identify( + distinctId = distinctID, // Replace 'distinctID' with your user's unique identifier + // optional: set additional person properties + userProperties = mapOf( + "name" to "Max Hedgehog", + "email" to "max@hedgehogmail.com" + ) +) +``` + +### iOS + +```swift +PostHogSDK.shared.identify("distinct_id", // Replace "distinct_id" with your user's unique identifier + userProperties: ["name": "Max Hedgehog", "email": "max@hedgehogmail.com"]) // optional: set additional person properties +``` + +### React Native + +```jsx +posthog.identify('distinct_id', { // Replace "distinct_id" with your user's unique identifier + email: 'max@hedgehogmail.com', // optional: set additional person properties + name: 'Max Hedgehog' +}) +``` + +### Dart + +```dart +await Posthog().identify( + userId: 'distinct_id', // Replace "distinct_id" with your user's unique identifier + userProperties: { + 'email': 'max@hedgehogmail.com', // optional: set additional person properties + 'name': 'Max Hedgehog', + }, +); +``` + +Events captured after calling `identify` are identified events and this creates a person profile if one doesn't exist already. + +Due to the cost of processing them, anonymous events can be up to 4x cheaper than identified events, so it's recommended you only capture identified events when needed. + +## How identify works + +When a user starts browsing your website or app, PostHog automatically assigns them an **anonymous ID**, which is stored locally. + +Provided you've [configured persistence](/docs/libraries/js/persistence.md) to use cookies or `localStorage`, this enables us to track anonymous users – even across different sessions. + +By calling `identify` with a `distinct_id` of your choice (usually the user's ID in your database, or their email), you link the anonymous ID and distinct ID together. + +Thus, all past and future events made with that anonymous ID are now associated with the distinct ID. + +This enables you to do things like associate events with a user from before they log in for the first time, or associate their events across different devices or platforms. + +Using identify in the backend + +Although you can call `identify` using our backend SDKs, it is used most in frontends. This is because there is no concept of anonymous sessions in the backend SDKs, so calling `identify` only updates person profiles. + +## Best practices when using `identify` + +### 1\. Call `identify` as soon as you're able to + +In your frontend, you should call `identify` as soon as you're able to. + +Typically, this is every time your **app loads** for the first time, and directly after your **users log in**. + +This ensures that events sent during your users' sessions are correctly associated with them. + +You only need to call `identify` once per session, and you should avoid calling it multiple times unnecessarily. + +If you call `identify` multiple times with the same data without reloading the page in between, PostHog will ignore the subsequent calls. + +#### Identify users when the web SDK loads + +If your app already knows the signed-in user when you initialize the JavaScript web SDK, the [`loaded` callback](/docs/libraries/js/config.md) is a convenient place to call `identify`. This identifies the user as soon as the SDK has loaded: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + loaded: (posthog) => { + if (currentUser?.id) { + posthog.identify(currentUser.id, { + email: currentUser.email, + name: currentUser.name, + }) + } + }, +}) +``` + +In this example, `currentUser` represents user data already available from your authentication system. If your app loads the user asynchronously, call `posthog.identify()` as soon as that data becomes available instead. + +### 2\. Use unique strings for distinct IDs + +If two users have the same distinct ID, their data is merged and they are considered one user in PostHog. Two common ways this can happen are: + +- Your logic for generating IDs does not generate sufficiently strong IDs and you can end up with a clash where 2 users have the same ID. +- There's a bug, typo, or mistake in your code leading to most or all users being identified with generic IDs like `null`, `true`, or `distinctId`. + +PostHog also has built-in protections to stop the most common distinct ID mistakes. + +### 3\. Reset after logout + +If a user logs out on your frontend, you should call `reset()` to unlink any future events made on that device with that user. + +This is important if your users are sharing a computer, as otherwise all of those users are grouped together into a single user due to shared cookies between sessions. + +**We strongly recommend you call `reset` on logout even if you don't expect users to share a computer.** + +You can do that like so: + +PostHog AI + +### Web + +```javascript +posthog.reset() +``` + +### iOS + +```swift +PostHogSDK.shared.reset() +``` + +### Android + +```kotlin +PostHog.reset() +``` + +### React Native + +```jsx +posthog.reset() +``` + +### Dart + +```dart +await Posthog().reset(); +``` + +If you *also* want to reset the `device_id` so that the device will be considered a new device in future events, you can pass `true` as an argument: + +Web + +PostHog AI + +```javascript +posthog.reset(true) +``` + +### 4\. Person profiles and properties + +You'll notice that one of the parameters in the `identify` method is a `properties` object. + +This enables you to set [person properties](/docs/product-analytics/person-properties.md). + +Whenever possible, we recommend passing in all person properties you have available each time you call identify, as this ensures their person profile on PostHog is up to date. + +Person properties can also be set being adding a `$set` property to a event `capture` call. + +**\`$set\` and \`$set\_once\` aren't stored on events** + +These properties only tell PostHog how to update person data during ingestion — they aren't kept on the stored event, so you can't filter, break down, or query events by them. To query the values you set, use [person properties](/docs/product-analytics/person-properties.md) instead. + +See our [person properties docs](/docs/product-analytics/person-properties.md) for more details on how to work with them and best practices. + +### 5\. Use deep links between platforms + +We recommend you call `identify` [as soon as you're able](#1-call-identify-as-soon-as-youre-able), typically when a user signs up or logs in. + +This doesn't work if one or both platforms are unauthenticated. Some examples of such cases are: + +- Onboarding and signup flows before authentication. +- Unauthenticated web pages redirecting to authenticated mobile apps. +- Authenticated web apps prompting an app download. + +In these cases, you can use a [deep link](https://developer.android.com/training/app-links/deep-linking) on Android and [universal links](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app) on iOS to identify users. + +1. Use `posthog.get_distinct_id()` to get the current distinct ID. Even if you cannot call identify because the user is unauthenticated, this will return an anonymous distinct ID generated by PostHog. +2. Add the distinct ID to the deep link as query parameters, along with other properties like UTM parameters. +3. When the user is redirected to the app, parse the deep link and handle the following cases: + +- The mobile app is already authenticated. In this case, call [`posthog.alias()`](/docs/libraries/js/usage.md#alias) with the distinct ID from the web. This associates the two distinct IDs as a single person. +- The mobile app is unauthenticated. In this case, call [`posthog.identify()`](/docs/libraries/js/usage.md#identifying-users) with the distinct ID from the web so pre-login mobile events stay connected to the web session. When the user later logs in on mobile, call `identify()` again with your canonical user ID. + +As long as you associate the distinct IDs with `posthog.identify()` or `posthog.alias()`, you can track events generated across platforms. + +Here's an example implementation for handling deep links from web to mobile: + +PostHog AI + +### iOS + +```swift +import PostHog +class DeepLinkIdentityManager { + static let shared = DeepLinkIdentityManager() + // MARK: - Deep Link Received + func handleDeepLink(_ url: URL, isAuthenticatedOnMobile: Bool) { + guard let webDistinctId = URLComponents(url: url, resolvingAgainstBaseURL: true)? + .queryItems?.first(where: { $0.name == "ph_distinct_id" })?.value else { + return + } + if isAuthenticatedOnMobile { + // The mobile app already knows the current user. + // Alias the incoming web distinct ID to that user. + PostHogSDK.shared.alias(webDistinctId) + } else { + // Reuse the web distinct ID until login on mobile. + PostHogSDK.shared.identify(webDistinctId) + } + } + // MARK: - Login/Signup + func handleLogin(canonicalUserId: String) { + // Switch from the web distinct ID (or a mobile anon ID) + // to your canonical user ID. + PostHogSDK.shared.identify(canonicalUserId) + // Set user properties, track signup event, etc. + } + func handleLogout() { + PostHogSDK.shared.reset() + } +} +``` + +### Android + +```kotlin +import android.net.Uri +import com.posthog.PostHog +object DeepLinkIdentityManager { + // Deep Link Received + fun handleDeepLink(uri: Uri, isAuthenticatedOnMobile: Boolean) { + val webDistinctId = uri.getQueryParameter("ph_distinct_id") ?: return + if (isAuthenticatedOnMobile) { + // The mobile app already knows the current user. + // Alias the incoming web distinct ID to that user. + PostHog.alias(webDistinctId) + } else { + // Reuse the web distinct ID until login on mobile. + PostHog.identify(webDistinctId) + } + } + // Login/Signup + fun handleLogin(canonicalUserId: String) { + // Switch from the web distinct ID (or a mobile anon ID) + // to your canonical user ID. + PostHog.identify(canonicalUserId) + // Set user properties, track signup event, etc. + } + fun handleLogout() { + PostHog.reset() + } +} +``` + +## Further reading + +- [Identifying users docs](/docs/product-analytics/identify.md) +- [How person processing works](/docs/how-posthog-works/ingestion-pipeline.md#2-person-processing) +- [An introductory guide to identifying users in PostHog](/tutorials/identifying-users-guide.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/apps/basic-integration/laravel/laravel12-saas/.claude/skills/integration-laravel/references/laravel.md b/apps/basic-integration/laravel/laravel12-saas/.claude/skills/integration-laravel/references/laravel.md new file mode 100644 index 000000000..830063b9a --- /dev/null +++ b/apps/basic-integration/laravel/laravel12-saas/.claude/skills/integration-laravel/references/laravel.md @@ -0,0 +1,176 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Laravel - Docs + +Copy page + +# Laravel - Docs + +PostHog integrates with Laravel through the [PostHog PHP SDK](/docs/libraries/php.md). This page covers Laravel-specific setup. For SDK features such as event capture, identifying users, feature flags, group analytics, and configuration options, see the [PHP SDK docs](/docs/libraries/php.md). + +## Installation + +Install the PHP SDK as described in the [PHP installation guide](/docs/libraries/php.md#installation), then add your project token and host to `.env`: + +.env + +PostHog AI + +```bash +POSTHOG_API_KEY= +POSTHOG_HOST=https://us.i.posthog.com +``` + +Add PostHog to Laravel's services config: + +config/services.php + +PostHog AI + +```php +'posthog' => [ + 'api_key' => env('POSTHOG_API_KEY'), + 'host' => env('POSTHOG_HOST', 'https://us.i.posthog.com'), +], +``` + +Initialize PostHog in the `boot` method of `app/Providers/AppServiceProvider.php`: + +app/Providers/AppServiceProvider.php + +PostHog AI + +```php + config('services.posthog.host'), + ] + ); + } +} +``` + +## Request context middleware + +Client SDKs such as [PostHog JS](/docs/libraries/js.md) can send tracing headers to your Laravel backend. Configure [`tracing_headers`](/docs/libraries/js/config.md#tracing-headers) for your Laravel backend hostname so browser requests include the session and distinct ID headers. + +The PHP SDK can read `X-PostHog-Distinct-Id` and `X-PostHog-Session-Id` headers and apply them to events captured during the request. Tracing headers are client-controlled analytics context, not authentication or authorization. For security-sensitive server-side events or decisions, pass an authenticated `distinctId` explicitly, such as `auth()->id()`. For the lower-level context APIs, see the [PHP request context docs](/docs/libraries/php.md#request-context). + +Add middleware like this: + +app/Http/Middleware/PostHogRequestContext.php + +PostHog AI + +```php +headers->all()); + $context['properties'] = array_merge( + $context['properties'] ?? [], + array_filter([ + '$current_url' => $request->fullUrl(), + '$request_method' => $request->method(), + '$request_path' => $request->getPathInfo(), + '$user_agent' => $request->userAgent(), + '$ip' => $request->ip(), + ], static fn ($value): bool => $value !== null && $value !== '') + ); + return PostHog::withContext( + $context, + static fn (): Response => $next($request), + ['fresh' => true] + ); + } +} +``` + +Register this middleware using your Laravel version's normal middleware registration. + +## Error tracking in Laravel + +The PHP SDK supports [error tracking](/docs/libraries/php.md#error-tracking), but Laravel handles most request exceptions before they become uncaught PHP exceptions. Capture Laravel-reported exceptions explicitly. + +In Laravel 11 and later, add a report callback in `bootstrap/app.php`: + +bootstrap/app.php + +PostHog AI + +```php +use Illuminate\Foundation\Configuration\Exceptions; +use PostHog\PostHog; +use Throwable; +->withExceptions(function (Exceptions $exceptions): void { + $exceptions->report(function (Throwable $e): void { + if (! config('services.posthog.api_key')) { + return; + } + PostHog::captureException( + $e, + auth()->id() !== null ? (string) auth()->id() : null, + [ + '$current_url' => request()->fullUrl(), + '$request_method' => request()->method(), + ] + ); + }); +}) +``` + +For older Laravel versions, call `PostHog::captureException()` from your exception handler's `report` method. + +## Long-running processes + +In normal PHP request lifecycles, queued events flush when the client is destroyed. In long-running Laravel processes such as queue workers, Horizon, or Octane, call `PostHog::flush()` after capturing important events or at the end of a job/request. + +If you prefer immediate delivery in queue workers, configure the PHP SDK with `batch_size` set to `1` for those workers: + +PHP + +PostHog AI + +```php +PostHog::init( + '', + [ + 'host' => config('services.posthog.host'), + 'batch_size' => 1, + ] +); +``` + +## Next steps + +See the [PHP SDK docs](/docs/libraries/php.md) for usage examples and the full API reference. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/apps/basic-integration/laravel/laravel12-saas/.env.example b/apps/basic-integration/laravel/laravel12-saas/.env.example index d6998f6cb..cd8c96708 100644 --- a/apps/basic-integration/laravel/laravel12-saas/.env.example +++ b/apps/basic-integration/laravel/laravel12-saas/.env.example @@ -22,6 +22,10 @@ BRAND_LINKEDIN_URL= STRIPE_KEY= STRIPE_SECRET= +POSTHOG_PROJECT_TOKEN=your_posthog_project_token +POSTHOG_HOST=your_posthog_host +POSTHOG_DISABLED=false + APP_LOCALE=en APP_FALLBACK_LOCALE=en APP_FAKER_LOCALE=en_US diff --git a/apps/basic-integration/laravel/laravel12-saas/app/Http/Controllers/Auth/SocialiteController.php b/apps/basic-integration/laravel/laravel12-saas/app/Http/Controllers/Auth/SocialiteController.php index 1345f8f11..9770d4d69 100644 --- a/apps/basic-integration/laravel/laravel12-saas/app/Http/Controllers/Auth/SocialiteController.php +++ b/apps/basic-integration/laravel/laravel12-saas/app/Http/Controllers/Auth/SocialiteController.php @@ -4,6 +4,7 @@ use App\Http\Controllers\Controller; use App\Models\User; +use App\Services\PostHogService; use Exception; use Illuminate\Support\Facades\Auth; use Laravel\Socialite\Facades\Socialite; @@ -40,6 +41,16 @@ public function callback($provider) Auth::login($user); + $posthog = app(PostHogService::class); + $posthog->identify((string) $user->getAuthIdentifier(), [ + 'email' => $user->email, + 'name' => $user->name, + ]); + $posthog->capture((string) $user->getAuthIdentifier(), 'user_logged_in', [ + 'login_method' => 'oauth', + 'provider' => $provider, + ]); + return redirect('/dashboard'); } } diff --git a/apps/basic-integration/laravel/laravel12-saas/app/Http/Controllers/Auth/VerifyEmailController.php b/apps/basic-integration/laravel/laravel12-saas/app/Http/Controllers/Auth/VerifyEmailController.php index 784765e3a..763407a39 100644 --- a/apps/basic-integration/laravel/laravel12-saas/app/Http/Controllers/Auth/VerifyEmailController.php +++ b/apps/basic-integration/laravel/laravel12-saas/app/Http/Controllers/Auth/VerifyEmailController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; +use App\Services\PostHogService; use Illuminate\Auth\Events\Verified; use Illuminate\Foundation\Auth\EmailVerificationRequest; use Illuminate\Http\RedirectResponse; @@ -20,6 +21,8 @@ public function __invoke(EmailVerificationRequest $request): RedirectResponse if ($request->user()->markEmailAsVerified()) { event(new Verified($request->user())); + + app(PostHogService::class)->capture((string) $request->user()->getAuthIdentifier(), 'email_verified'); } return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); diff --git a/apps/basic-integration/laravel/laravel12-saas/app/Http/Controllers/SubscriptionController.php b/apps/basic-integration/laravel/laravel12-saas/app/Http/Controllers/SubscriptionController.php index 373b681d9..86915e420 100644 --- a/apps/basic-integration/laravel/laravel12-saas/app/Http/Controllers/SubscriptionController.php +++ b/apps/basic-integration/laravel/laravel12-saas/app/Http/Controllers/SubscriptionController.php @@ -7,6 +7,7 @@ use App\Actions\Billing\RedirectToBillingPortal; use App\Actions\Billing\SwapPlan; use App\Domains\Billing\PlanCatalog; +use App\Services\PostHogService; use Exception; use Illuminate\Http\Request; @@ -35,6 +36,12 @@ public function checkout(Request $request, PlanCatalog $catalog, CheckoutPlan $c $checkoutSession = $checkoutPlan($user, $plan); + app(PostHogService::class)->capture((string) $user->getAuthIdentifier(), 'subscription_checkout_started', [ + 'plan_id' => $plan->getKey(), + 'plan_slug' => $plan->slug, + 'billing_mode' => 'stripe', + ]); + return redirect($checkoutSession->url); } @@ -58,6 +65,12 @@ protected function createStubSubscription($user, $plan) 'amount' => $plan->price ?? 0, ]); + app(PostHogService::class)->capture((string) $user->getAuthIdentifier(), 'subscription_started', [ + 'plan_id' => $plan->getKey(), + 'plan_slug' => $plan->slug, + 'billing_mode' => 'demo', + ]); + return redirect()->route('dashboard')->with('success', 'Demo subscription created for ' . $plan->name . '. (Stripe not configured)'); } @@ -70,6 +83,11 @@ public function swap(Request $request, PlanCatalog $catalog, SwapPlan $swapPlan) try { $swapPlan($user, $plan); + app(PostHogService::class)->capture((string) $user->getAuthIdentifier(), 'subscription_plan_changed', [ + 'plan_id' => $plan->getKey(), + 'plan_slug' => $plan->slug, + ]); + return redirect()->route('subscribe')->with('success', 'Your subscription has been updated to '.$plan->name.'.'); } catch (Exception $e) { return redirect()->route('subscribe')->with('error', 'There was an error updating your subscription: '.$e->getMessage()); @@ -81,6 +99,14 @@ public function swap(Request $request, PlanCatalog $catalog, SwapPlan $swapPlan) public function redirectToBillingPortal(Request $request, RedirectToBillingPortal $billingPortal) { - return $billingPortal($request->user()); + $user = $request->user(); + + $response = $billingPortal($user); + + if (CheckoutPlan::isStripeConfigured()) { + app(PostHogService::class)->capture((string) $user->getAuthIdentifier(), 'billing_portal_opened'); + } + + return $response; } } diff --git a/apps/basic-integration/laravel/laravel12-saas/app/Livewire/Actions/Logout.php b/apps/basic-integration/laravel/laravel12-saas/app/Livewire/Actions/Logout.php index 3ef481d1c..e52436f89 100644 --- a/apps/basic-integration/laravel/laravel12-saas/app/Livewire/Actions/Logout.php +++ b/apps/basic-integration/laravel/laravel12-saas/app/Livewire/Actions/Logout.php @@ -2,6 +2,7 @@ namespace App\Livewire\Actions; +use App\Services\PostHogService; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Session; @@ -12,6 +13,12 @@ class Logout */ public function __invoke(): void { + $user = Auth::user(); + + if ($user) { + app(PostHogService::class)->capture((string) $user->getAuthIdentifier(), 'user_logged_out'); + } + Auth::guard('web')->logout(); Session::invalidate(); diff --git a/apps/basic-integration/laravel/laravel12-saas/app/Providers/AppServiceProvider.php b/apps/basic-integration/laravel/laravel12-saas/app/Providers/AppServiceProvider.php index 2da343881..fb4c792aa 100644 --- a/apps/basic-integration/laravel/laravel12-saas/app/Providers/AppServiceProvider.php +++ b/apps/basic-integration/laravel/laravel12-saas/app/Providers/AppServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use App\Models\Subscription; +use App\Services\PostHogService; use App\Support\Branding; use Illuminate\Support\Facades\View; use Illuminate\Support\ServiceProvider; @@ -15,7 +16,7 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - // + $this->app->singleton(PostHogService::class); } /** diff --git a/apps/basic-integration/laravel/laravel12-saas/app/Services/PostHogService.php b/apps/basic-integration/laravel/laravel12-saas/app/Services/PostHogService.php new file mode 100644 index 000000000..62c07b353 --- /dev/null +++ b/apps/basic-integration/laravel/laravel12-saas/app/Services/PostHogService.php @@ -0,0 +1,83 @@ + $host, + ]); + + self::$initialized = true; + } + } + + /** + * Update the person profile associated with a stable application user ID. + * + * @param array $properties + */ + public function identify(string $distinctId, array $properties = []): void + { + if (config('posthog.disabled') || ! self::$initialized) { + return; + } + + PostHog::identify([ + 'distinctId' => $distinctId, + 'properties' => $properties, + ]); + } + + /** + * Capture a product event for a stable application user ID. + * + * @param array $properties + */ + public function capture(string $distinctId, string $event, array $properties = []): void + { + if (config('posthog.disabled') || ! self::$initialized) { + return; + } + + PostHog::capture([ + 'distinctId' => $distinctId, + 'event' => $event, + 'properties' => $properties, + ]); + } + + public function captureException(\Throwable $exception, ?string $distinctId = null, array $properties = []): void + { + if (config('posthog.disabled') || ! self::$initialized) { + return; + } + + PostHog::captureException($exception, $distinctId, $properties); + } +} diff --git a/apps/basic-integration/laravel/laravel12-saas/bootstrap/app.php b/apps/basic-integration/laravel/laravel12-saas/bootstrap/app.php index 7b162dac3..3c310b547 100644 --- a/apps/basic-integration/laravel/laravel12-saas/bootstrap/app.php +++ b/apps/basic-integration/laravel/laravel12-saas/bootstrap/app.php @@ -1,8 +1,10 @@ withRouting( @@ -14,5 +16,14 @@ // }) ->withExceptions(function (Exceptions $exceptions) { - // + $exceptions->report(function (Throwable $exception): void { + app(PostHogService::class)->captureException( + $exception, + auth()->id() !== null ? (string) auth()->id() : null, + [ + '$current_url' => request()->fullUrl(), + '$request_method' => request()->method(), + ], + ); + }); })->create(); diff --git a/apps/basic-integration/laravel/laravel12-saas/composer.json b/apps/basic-integration/laravel/laravel12-saas/composer.json index 114f06b1a..f736a22a0 100644 --- a/apps/basic-integration/laravel/laravel12-saas/composer.json +++ b/apps/basic-integration/laravel/laravel12-saas/composer.json @@ -21,6 +21,7 @@ "propaganistas/laravel-disposable-email": "^2.4", "pxlrbt/filament-activity-log": "^2.0", "pxlrbt/filament-environment-indicator": "^3.0", + "posthog/posthog-php": "^4.12.1", "spatie/laravel-activitylog": "^4.8", "spatie/laravel-cookie-consent": "^3.3", "spatie/laravel-sitemap": "^7.2", diff --git a/apps/basic-integration/laravel/laravel12-saas/config/posthog.php b/apps/basic-integration/laravel/laravel12-saas/config/posthog.php new file mode 100644 index 000000000..f127b994f --- /dev/null +++ b/apps/basic-integration/laravel/laravel12-saas/config/posthog.php @@ -0,0 +1,7 @@ + env('POSTHOG_PROJECT_TOKEN'), + 'host' => env('POSTHOG_HOST'), + 'disabled' => env('POSTHOG_DISABLED', false), +]; diff --git a/apps/basic-integration/laravel/laravel12-saas/package-lock.json b/apps/basic-integration/laravel/laravel12-saas/package-lock.json index cda57ee18..df28db18a 100644 --- a/apps/basic-integration/laravel/laravel12-saas/package-lock.json +++ b/apps/basic-integration/laravel/laravel12-saas/package-lock.json @@ -1,5 +1,5 @@ { - "name": "mvpable", + "name": "laravel12-saas", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/apps/basic-integration/laravel/laravel12-saas/resources/views/livewire/pages/auth/login.blade.php b/apps/basic-integration/laravel/laravel12-saas/resources/views/livewire/pages/auth/login.blade.php index b4ee555d3..e162c4ec2 100644 --- a/apps/basic-integration/laravel/laravel12-saas/resources/views/livewire/pages/auth/login.blade.php +++ b/apps/basic-integration/laravel/laravel12-saas/resources/views/livewire/pages/auth/login.blade.php @@ -1,6 +1,8 @@ form->authenticate(); + $user = Auth::user(); + + $posthog = app(PostHogService::class); + $posthog->identify((string) $user->getAuthIdentifier(), [ + 'email' => $user->email, + 'name' => $user->name, + ]); + $posthog->capture((string) $user->getAuthIdentifier(), 'user_logged_in', [ + 'login_method' => 'password', + ]); + Session::regenerate(); $this->redirectIntended(default: route('dashboard', absolute: false), navigate: false); diff --git a/apps/basic-integration/laravel/laravel12-saas/resources/views/livewire/pages/auth/register.blade.php b/apps/basic-integration/laravel/laravel12-saas/resources/views/livewire/pages/auth/register.blade.php index 30ed2b0e5..a86b5022c 100644 --- a/apps/basic-integration/laravel/laravel12-saas/resources/views/livewire/pages/auth/register.blade.php +++ b/apps/basic-integration/laravel/laravel12-saas/resources/views/livewire/pages/auth/register.blade.php @@ -1,6 +1,7 @@ identify((string) $user->getAuthIdentifier(), [ + 'email' => $user->email, + 'name' => $user->name, + ]); + $posthog->capture((string) $user->getAuthIdentifier(), 'user_registered', [ + 'registration_method' => 'password', + ]); + $this->redirect(route('dashboard', absolute: false), navigate: false); } }; ?> diff --git a/apps/basic-integration/laravel/laravel12-saas/resources/views/livewire/profile/update-password-form.blade.php b/apps/basic-integration/laravel/laravel12-saas/resources/views/livewire/profile/update-password-form.blade.php index 95df55c92..2c2059a86 100644 --- a/apps/basic-integration/laravel/laravel12-saas/resources/views/livewire/profile/update-password-form.blade.php +++ b/apps/basic-integration/laravel/laravel12-saas/resources/views/livewire/profile/update-password-form.blade.php @@ -1,5 +1,6 @@ update([ + $user = Auth::user(); + $user->update([ 'password' => Hash::make($validated['password']), ]); + app(PostHogService::class)->capture((string) $user->getAuthIdentifier(), 'password_updated'); + $this->reset('current_password', 'password', 'password_confirmation'); $this->dispatch('password-updated'); diff --git a/apps/basic-integration/laravel/laravel12-saas/resources/views/livewire/profile/update-profile-information-form.blade.php b/apps/basic-integration/laravel/laravel12-saas/resources/views/livewire/profile/update-profile-information-form.blade.php index 6d1bdcdab..d75de964e 100644 --- a/apps/basic-integration/laravel/laravel12-saas/resources/views/livewire/profile/update-profile-information-form.blade.php +++ b/apps/basic-integration/laravel/laravel12-saas/resources/views/livewire/profile/update-profile-information-form.blade.php @@ -1,6 +1,7 @@ save(); + app(PostHogService::class)->capture((string) $user->getAuthIdentifier(), 'profile_updated', [ + 'email_changed' => $user->wasChanged('email'), + 'name_changed' => $user->wasChanged('name'), + ]); + $this->dispatch('profile-updated', name: $user->name); } diff --git a/apps/basic-integration/laravel/laravel12-saas/routes/auth.php b/apps/basic-integration/laravel/laravel12-saas/routes/auth.php index 79b8c5f1e..0588a641d 100644 --- a/apps/basic-integration/laravel/laravel12-saas/routes/auth.php +++ b/apps/basic-integration/laravel/laravel12-saas/routes/auth.php @@ -1,6 +1,7 @@ name('password.confirm'); Route::get('logout', function () { + $user = Auth::user(); + + if ($user) { + app(PostHogService::class)->capture((string) $user->getAuthIdentifier(), 'user_logged_out'); + } + Auth::guard('web')->logout(); request()->session()->invalidate(); request()->session()->regenerateToken();