fix(posthog): harden usage sync after deletes and workspace removal
Use Account::postsCountCacheKey everywhere, avoid findOrFail when syncing post deletes, dispatch SyncAccountUsage after DeleteWorkspace when enabled, and add coverage for the new paths. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a659a9eadf
commit
ccd78e3c8d
9 changed files with 100 additions and 7 deletions
|
|
@ -4,8 +4,10 @@
|
|||
|
||||
namespace App\Actions\Workspace;
|
||||
|
||||
use App\Jobs\PostHog\SyncAccountUsage;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Services\PostHogService;
|
||||
|
||||
class DeleteWorkspace
|
||||
{
|
||||
|
|
@ -13,6 +15,12 @@ public static function execute(User $user, Workspace $workspace): void
|
|||
{
|
||||
User::where('current_workspace_id', $workspace->id)->update(['current_workspace_id' => null]);
|
||||
|
||||
$accountId = (string) $workspace->account_id;
|
||||
|
||||
$workspace->delete();
|
||||
|
||||
if (PostHogService::isEnabled()) {
|
||||
SyncAccountUsage::dispatch($accountId, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ public function handle(PostHogService $postHog): void
|
|||
return;
|
||||
}
|
||||
|
||||
Cache::forget("account:{$this->accountId}:posts_count");
|
||||
Cache::forget(Account::postsCountCacheKey($this->accountId));
|
||||
|
||||
$account = Account::with('plan')->find($this->accountId);
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,11 @@ public function handle(PostDeleted $event): void
|
|||
return;
|
||||
}
|
||||
|
||||
$workspace = Workspace::findOrFail($event->workspaceId);
|
||||
$workspace = Workspace::find($event->workspaceId);
|
||||
|
||||
if (! $workspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
SyncAccountUsage::dispatch((string) $workspace->account_id, (string) $workspace->id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,15 @@ class Account extends Model
|
|||
|
||||
public const SUBSCRIPTION_NAME = 'default';
|
||||
|
||||
/**
|
||||
* Redis/cache key for aggregated post counts across the account's workspaces.
|
||||
* Invalidated by the PostHog usage sync job before re-reading aggregates for analytics.
|
||||
*/
|
||||
public static function postsCountCacheKey(string $accountId): string
|
||||
{
|
||||
return "account:{$accountId}:posts_count";
|
||||
}
|
||||
|
||||
protected $fillable = [
|
||||
'owner_id',
|
||||
'name',
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
use App\Features\MonthlyCreditsLimit;
|
||||
use App\Features\SocialAccountLimit;
|
||||
use App\Features\WorkspaceLimit;
|
||||
use App\Models\Account;
|
||||
use App\Models\AiUsageLog;
|
||||
use App\Models\Invite;
|
||||
use App\Models\Post;
|
||||
|
|
@ -78,7 +79,7 @@ private function cachedPostCount(array $workspaceIds): int
|
|||
}
|
||||
|
||||
return (int) Cache::remember(
|
||||
"account:{$this->id}:posts_count",
|
||||
Account::postsCountCacheKey((string) $this->id),
|
||||
self::POST_COUNT_CACHE_TTL,
|
||||
fn () => Post::whereIn('workspace_id', $workspaceIds)->count(),
|
||||
);
|
||||
|
|
|
|||
60
tests/Feature/Actions/Workspace/DeleteWorkspaceTest.php
Normal file
60
tests/Feature/Actions/Workspace/DeleteWorkspaceTest.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Actions\Workspace\DeleteWorkspace;
|
||||
use App\Enums\UserWorkspace\Role;
|
||||
use App\Jobs\PostHog\SyncAccountUsage;
|
||||
use App\Models\Account;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
|
||||
test('delete workspace dispatches SyncAccountUsage when PostHog is enabled', function () {
|
||||
config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']);
|
||||
|
||||
$account = Account::factory()->create();
|
||||
$user = User::factory()->create(['account_id' => $account->id]);
|
||||
$account->update(['owner_id' => $user->id]);
|
||||
|
||||
$workspace = Workspace::factory()->create([
|
||||
'account_id' => $account->id,
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
$workspace->members()->attach($user->id, ['role' => Role::Member->value]);
|
||||
$user->update(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
Bus::fake();
|
||||
|
||||
DeleteWorkspace::execute($user, $workspace);
|
||||
|
||||
expect(Workspace::find($workspace->id))->toBeNull();
|
||||
|
||||
Bus::assertDispatched(SyncAccountUsage::class, function ($job) use ($account) {
|
||||
return $job->accountId === (string) $account->id
|
||||
&& $job->workspaceId === null;
|
||||
});
|
||||
});
|
||||
|
||||
test('delete workspace does not dispatch SyncAccountUsage when PostHog is disabled', function () {
|
||||
config(['services.posthog.enabled' => false, 'services.posthog.api_key' => null]);
|
||||
|
||||
$account = Account::factory()->create();
|
||||
$user = User::factory()->create(['account_id' => $account->id]);
|
||||
$account->update(['owner_id' => $user->id]);
|
||||
|
||||
$workspace = Workspace::factory()->create([
|
||||
'account_id' => $account->id,
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
$workspace->members()->attach($user->id, ['role' => Role::Member->value]);
|
||||
$user->update(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
Bus::fake();
|
||||
|
||||
DeleteWorkspace::execute($user, $workspace);
|
||||
|
||||
expect(Workspace::find($workspace->id))->toBeNull();
|
||||
|
||||
Bus::assertNotDispatched(SyncAccountUsage::class);
|
||||
});
|
||||
|
|
@ -107,7 +107,7 @@
|
|||
'user_id' => $this->user->id,
|
||||
]);
|
||||
|
||||
Cache::put("account:{$this->account->id}:posts_count", 999, 300);
|
||||
Cache::put(Account::postsCountCacheKey((string) $this->account->id), 999, 300);
|
||||
|
||||
Post::factory()->count(2)->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,17 @@
|
|||
Bus::assertDispatched(SyncAccountUsage::class);
|
||||
});
|
||||
|
||||
test('listener does not dispatch when workspace no longer exists', function () {
|
||||
Bus::fake();
|
||||
|
||||
(new SyncUsageOnPostDeleted)->handle(new PostDeleted(
|
||||
postId: (string) Str::uuid(),
|
||||
workspaceId: (string) Str::uuid(),
|
||||
));
|
||||
|
||||
Bus::assertNotDispatched(SyncAccountUsage::class);
|
||||
});
|
||||
|
||||
test('listener does not dispatch when PostHog is disabled', function () {
|
||||
config(['services.posthog.enabled' => false]);
|
||||
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@
|
|||
expect($this->account->usage()['postCount'])->toBe(2);
|
||||
|
||||
// Forgetting the cache key returns the fresh count.
|
||||
Cache::forget("account:{$this->account->id}:posts_count");
|
||||
Cache::forget(Account::postsCountCacheKey((string) $this->account->id));
|
||||
expect($this->account->usage()['postCount'])->toBe(3);
|
||||
});
|
||||
|
||||
|
|
@ -102,7 +102,7 @@
|
|||
expect($this->account->usage()['postCount'])->toBe(0);
|
||||
|
||||
// No cache entry should be written for the empty case.
|
||||
expect(Cache::has("account:{$this->account->id}:posts_count"))->toBeFalse();
|
||||
expect(Cache::has(Account::postsCountCacheKey((string) $this->account->id)))->toBeFalse();
|
||||
});
|
||||
|
||||
test('postCount survives a string-typed cache value (Redis serializer quirk)', function () {
|
||||
|
|
@ -116,7 +116,7 @@
|
|||
'user_id' => $this->owner->id,
|
||||
]);
|
||||
|
||||
Cache::put("account:{$this->account->id}:posts_count", '42', 300);
|
||||
Cache::put(Account::postsCountCacheKey((string) $this->account->id), '42', 300);
|
||||
|
||||
$usage = $this->account->usage();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue