feat: explicit POSTHOG_ENABLED gate for self-hosted safety
Self-hosted installs that inherited POSTHOG_API_KEY from an example or older deploy were still seeing SyncUser/SendEvent jobs run because the gate was based on the api key alone. Switches the gate to an explicit 'services.posthog.enabled' flag (env: POSTHOG_ENABLED, default false) and requires both enabled=true AND api_key for tracking to fire. Backend gating: - PostHogService::isEnabled() — single static helper used everywhere. - AppServiceProvider::configurePostHog — skips PostHog::init when off. - CreateUser::execute — does not enqueue SyncUser when off. - SyncUser::handle, TrackBilling::handle, SendEvent::handle — early return before any DB query so the queue worker does no work. Frontend gating: - New VITE_POSTHOG_ENABLED env var mirrored from POSTHOG_ENABLED. - initializePostHog, syncPostHogContext, capturePageview all gated. Tests updated to set both flags on the happy path; adds explicit 'CreateUser does not dispatch SyncUser when PostHog is disabled'. Deploy note: the trypost.it cloud .env must set POSTHOG_ENABLED=true before this branch is merged or analytics will go dark.
This commit is contained in:
parent
9421717207
commit
cd28ac4025
13 changed files with 80 additions and 32 deletions
|
|
@ -193,7 +193,8 @@ GIPHY_API_KEY=
|
|||
# Google Tag Manager (optional - analytics)
|
||||
GTM_ID=
|
||||
|
||||
# PostHog (optional - analytics)
|
||||
# PostHog (optional - analytics; off by default, self-hosted installs can ignore)
|
||||
POSTHOG_ENABLED=false
|
||||
POSTHOG_API_KEY=
|
||||
POSTHOG_HOST=https://us.i.posthog.com
|
||||
|
||||
|
|
@ -203,5 +204,6 @@ VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
|
|||
VITE_REVERB_HOST="${REVERB_HOST}"
|
||||
VITE_REVERB_PORT="${REVERB_PORT}"
|
||||
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
|
||||
VITE_POSTHOG_ENABLED="${POSTHOG_ENABLED}"
|
||||
VITE_POSTHOG_API_KEY="${POSTHOG_API_KEY}"
|
||||
VITE_POSTHOG_HOST="${POSTHOG_HOST}"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
use App\Jobs\PostHog\SyncUser;
|
||||
use App\Models\Account;
|
||||
use App\Models\User;
|
||||
use App\Services\PostHogService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CreateUser
|
||||
|
|
@ -41,7 +42,9 @@ public static function execute(array $data, array $utmParameters = []): User
|
|||
return $user;
|
||||
});
|
||||
|
||||
SyncUser::dispatch((string) $user->id);
|
||||
if (PostHogService::isEnabled()) {
|
||||
SyncUser::dispatch((string) $user->id);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
namespace App\Jobs\PostHog;
|
||||
|
||||
use App\Services\PostHogService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
|
@ -18,8 +19,9 @@
|
|||
* namespace (`SyncUser`, `TrackBilling`) build the payloads and queue this
|
||||
* job to do the network work.
|
||||
*
|
||||
* No-op when `POSTHOG_API_KEY` is unset so self-hosted installs are
|
||||
* unaffected.
|
||||
* No-op when `POSTHOG_ENABLED` is false (or `POSTHOG_API_KEY` is unset),
|
||||
* via `PostHogService::isEnabled()`, so self-hosted installs never reach
|
||||
* the SDK.
|
||||
*/
|
||||
class SendEvent implements ShouldQueue
|
||||
{
|
||||
|
|
@ -41,7 +43,7 @@ public function __construct(
|
|||
|
||||
public function handle(): void
|
||||
{
|
||||
if (! config('services.posthog.api_key')) {
|
||||
if (! PostHogService::isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@
|
|||
* - group `account` → Account (billing/plan, parent of workspaces)
|
||||
* - group `workspace` → Workspace (collaboration unit, child of account)
|
||||
*
|
||||
* No-op when POSTHOG_API_KEY is unset (PostHogService short-circuits), so
|
||||
* self-hosted installs are unaffected.
|
||||
* No-op when PostHog is disabled (`POSTHOG_ENABLED=false` or no API key)
|
||||
* via `PostHogService::isEnabled()`, so self-hosted installs are unaffected.
|
||||
*/
|
||||
class SyncUser implements ShouldQueue
|
||||
{
|
||||
|
|
@ -43,6 +43,10 @@ public function __construct(public string $userId)
|
|||
|
||||
public function handle(PostHogService $postHog): void
|
||||
{
|
||||
if (! PostHogService::isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = User::with([
|
||||
'account.plan',
|
||||
'currentWorkspace' => fn ($query) => $query->withCount('socialAccounts'),
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
* group properties (plan, has_active_subscription, is_on_trial) reflect
|
||||
* the new Stripe state. Dispatched by `StripeEventListener`.
|
||||
*
|
||||
* No-op when `POSTHOG_API_KEY` is unset.
|
||||
* No-op when PostHog is disabled (`POSTHOG_ENABLED=false` or no API key).
|
||||
*/
|
||||
class TrackBilling implements ShouldQueue
|
||||
{
|
||||
|
|
@ -43,6 +43,10 @@ public function __construct(
|
|||
|
||||
public function handle(PostHogService $postHog): void
|
||||
{
|
||||
if (! PostHogService::isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$account = Account::with('plan')->find($this->accountId);
|
||||
|
||||
if (! $account || ! $account->owner_id) {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
use App\Models\WorkspaceInvite;
|
||||
use App\Models\WorkspaceLabel;
|
||||
use App\Models\WorkspaceSignature;
|
||||
use App\Services\PostHogService;
|
||||
use App\Services\PostTemplate\Registry as PostTemplateRegistry;
|
||||
use App\Socialite\InstagramProvider;
|
||||
use App\Socialite\LinkedInPageExtendSocialite;
|
||||
|
|
@ -187,13 +188,13 @@ protected function configureMorphMap(): void
|
|||
|
||||
protected function configurePostHog(): void
|
||||
{
|
||||
$apiKey = config('services.posthog.api_key');
|
||||
|
||||
if ($apiKey) {
|
||||
PostHog::init($apiKey, [
|
||||
'host' => config('services.posthog.host'),
|
||||
]);
|
||||
if (! PostHogService::isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PostHog::init(config('services.posthog.api_key'), [
|
||||
'host' => config('services.posthog.host'),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function configureRateLimiting(): void
|
||||
|
|
|
|||
|
|
@ -10,6 +10,18 @@
|
|||
|
||||
class PostHogService
|
||||
{
|
||||
/**
|
||||
* Whether PostHog tracking should run at all. Controlled by the explicit
|
||||
* `POSTHOG_ENABLED` env var so self-hosted installs that inherited an
|
||||
* example `POSTHOG_API_KEY` from a config snippet do not silently send
|
||||
* events upstream. The SaaS deployment opts in via `POSTHOG_ENABLED=true`.
|
||||
*/
|
||||
public static function isEnabled(): bool
|
||||
{
|
||||
return (bool) config('services.posthog.enabled')
|
||||
&& (bool) config('services.posthog.api_key');
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture an event for the given distinct id. When `$account` is supplied,
|
||||
* the workspace/plan group is auto-attached so the event is filterable in
|
||||
|
|
@ -19,7 +31,7 @@ class PostHogService
|
|||
*/
|
||||
public function capture(string $distinctId, string $event, array $properties = [], ?Account $account = null): void
|
||||
{
|
||||
if (! $this->shouldSend()) {
|
||||
if (! self::isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -43,7 +55,7 @@ public function capture(string $distinctId, string $event, array $properties = [
|
|||
*/
|
||||
public function identify(string $distinctId, array $properties = []): void
|
||||
{
|
||||
if (! $this->shouldSend()) {
|
||||
if (! self::isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +70,7 @@ public function identify(string $distinctId, array $properties = []): void
|
|||
*/
|
||||
public function groupIdentify(string $groupType, string $groupKey, array $properties = []): void
|
||||
{
|
||||
if (! $this->shouldSend()) {
|
||||
if (! self::isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -69,11 +81,6 @@ public function groupIdentify(string $groupType, string $groupKey, array $proper
|
|||
]);
|
||||
}
|
||||
|
||||
private function shouldSend(): bool
|
||||
{
|
||||
return (bool) config('services.posthog.api_key');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@
|
|||
],
|
||||
|
||||
'posthog' => [
|
||||
'enabled' => (bool) env('POSTHOG_ENABLED', false),
|
||||
'api_key' => env('POSTHOG_API_KEY'),
|
||||
'host' => env('POSTHOG_HOST', 'https://us.i.posthog.com'),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -5,13 +5,16 @@ import type { Auth, Usage } from './types';
|
|||
|
||||
const apiKey = import.meta.env.VITE_POSTHOG_API_KEY as string | undefined;
|
||||
const host = import.meta.env.VITE_POSTHOG_HOST as string | undefined;
|
||||
// Vite stringifies env vars, so explicit equality with 'true' avoids the
|
||||
// gotcha where 'false' would be truthy. Mirrors backend `POSTHOG_ENABLED`.
|
||||
const enabled = import.meta.env.VITE_POSTHOG_ENABLED === 'true' && !!apiKey;
|
||||
|
||||
export const initializePostHog = (): void => {
|
||||
if (!apiKey) {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
posthog.init(apiKey, {
|
||||
posthog.init(apiKey as string, {
|
||||
api_host: host || 'https://us.i.posthog.com',
|
||||
ui_host: 'https://us.posthog.com',
|
||||
capture_pageview: false,
|
||||
|
|
@ -40,6 +43,10 @@ export const initializePostHog = (): void => {
|
|||
* group `workspace` → collaboration child (carries `account_id`).
|
||||
*/
|
||||
export const syncPostHogContext = (page: Page): void => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auth = page.props.auth as Auth | undefined;
|
||||
const usage = page.props.usage as Usage | null | undefined;
|
||||
|
||||
|
|
@ -77,6 +84,9 @@ export const syncPostHogContext = (page: Page): void => {
|
|||
* every navigation (and once at boot for the first page).
|
||||
*/
|
||||
export const capturePageview = (): void => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
posthog.capture('$pageview', { $current_url: window.location.href });
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@
|
|||
expect(Workspace::count())->toBe(0);
|
||||
});
|
||||
|
||||
test('CreateUser dispatches SyncUser with the new user id', function () {
|
||||
test('CreateUser dispatches SyncUser with the new user id when PostHog is enabled', function () {
|
||||
config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']);
|
||||
Bus::fake([SyncUser::class]);
|
||||
|
||||
$user = CreateUser::execute([
|
||||
|
|
@ -59,3 +60,16 @@
|
|||
fn ($job) => $job->userId === (string) $user->id,
|
||||
);
|
||||
});
|
||||
|
||||
test('CreateUser does not dispatch SyncUser when PostHog is disabled', function () {
|
||||
config(['services.posthog.enabled' => false]);
|
||||
Bus::fake([SyncUser::class]);
|
||||
|
||||
CreateUser::execute([
|
||||
'name' => 'Jane Doe',
|
||||
'email' => 'jane.posthog.disabled@example.com',
|
||||
'password' => 'secret123',
|
||||
]);
|
||||
|
||||
Bus::assertNotDispatched(SyncUser::class);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
use Illuminate\Support\Str;
|
||||
|
||||
beforeEach(function () {
|
||||
config(['services.posthog.api_key' => 'phc_test_key']);
|
||||
config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']);
|
||||
|
||||
$this->account = Account::factory()->create([
|
||||
'plan_id' => Plan::query()->where('slug', 'starter')->first()?->id,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
use Illuminate\Support\Facades\Queue;
|
||||
|
||||
beforeEach(function () {
|
||||
config(['services.posthog.api_key' => 'phc_test_key']);
|
||||
config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']);
|
||||
|
||||
$this->account = Account::factory()->create([
|
||||
'plan_id' => Plan::query()->where('slug', 'starter')->first()?->id,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
test('capture dispatches job when api key is configured', function () {
|
||||
Queue::fake();
|
||||
config(['services.posthog.api_key' => 'phc_test_key']);
|
||||
config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']);
|
||||
|
||||
$service = new PostHogService;
|
||||
$service->capture('user-123', 'test_event', ['foo' => 'bar']);
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
|
||||
test('identify dispatches job when api key is configured', function () {
|
||||
Queue::fake();
|
||||
config(['services.posthog.api_key' => 'phc_test_key']);
|
||||
config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']);
|
||||
|
||||
$service = new PostHogService;
|
||||
$service->identify('user-123', ['$email' => 'test@example.com']);
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
|
||||
test('group identify dispatches job when api key is configured', function () {
|
||||
Queue::fake();
|
||||
config(['services.posthog.api_key' => 'phc_test_key']);
|
||||
config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']);
|
||||
|
||||
$service = new PostHogService;
|
||||
$service->groupIdentify('workspace', 'ws-123', ['name' => 'Test Workspace']);
|
||||
|
|
@ -84,7 +84,7 @@
|
|||
|
||||
test('capture auto-attaches account groups when account is supplied', function () {
|
||||
Queue::fake();
|
||||
config(['services.posthog.api_key' => 'phc_test_key']);
|
||||
config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']);
|
||||
|
||||
$plan = Plan::query()->where('slug', 'starter')->first();
|
||||
$account = Account::factory()->create(['plan_id' => $plan?->id]);
|
||||
|
|
@ -105,7 +105,7 @@
|
|||
|
||||
test('capture without account does not attach group properties', function () {
|
||||
Queue::fake();
|
||||
config(['services.posthog.api_key' => 'phc_test_key']);
|
||||
config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']);
|
||||
|
||||
$service = new PostHogService;
|
||||
$service->capture('user-123', 'page.viewed', ['url' => '/dashboard']);
|
||||
|
|
|
|||
Loading…
Reference in a new issue