Skip to content

feat(posthog): keep social_accounts_count and posts_count fresh on account group#43

Merged
paulocastellano merged 4 commits into
mainfrom
feat/posthog-onboarding-properties
May 19, 2026
Merged

feat(posthog): keep social_accounts_count and posts_count fresh on account group#43
paulocastellano merged 4 commits into
mainfrom
feat/posthog-onboarding-properties

Conversation

@paulocastellano
Copy link
Copy Markdown
Contributor

Why

Onboarding/lifecycle workflows in PostHog (and downstream tools like SendKit) need to segment users by how many social accounts they've connected and how many posts they've created. The existing SyncUser job only re-emitted these counts on signup and billing changes, so cohorts like "users with social_accounts_count >= 1" went stale the moment a user did anything meaningful — and never updated again.

What changes

Two new paths that refresh the account group automatically:

  • SocialAccountObserver#[ObservedBy] on the SocialAccount model. Fires SyncAccountUsage on created/deleted. One hook captures all 14 OAuth callback paths automatically (Facebook, X, LinkedIn, TikTok, YouTube, Threads, Instagram, Bluesky, Mastodon, Pinterest, etc).
  • SyncUsageOnPostCreated / SyncUsageOnPostDeleted — auto-discovered listeners on the existing PostCreated/PostDeleted events dispatched by CreatePost and DeletePost actions.

Both paths dispatch SyncAccountUsage, a new dedicated job that does groupIdentify('account', ...) and (optionally) groupIdentify('workspace', ...). The property mapping previously duplicated between SyncUser and inline group syncs now lives in one place.

SyncUser was slimmed to just identify(user) and delegate the group sync to SyncAccountUsage. Zero behavioural change for existing signup/billing flows — same end state in PostHog, just split across two jobs.

Open-source safe

All three entry points (observer + both listeners) short-circuit via PostHogService::isEnabled() before dispatch. Self-hosted instances without PostHog configured see zero queued jobs, zero worker overhead, zero noise.

Cache correctness

account:{id}:posts_count is invalidated before every sync so the job reads fresh counts from the DB, then usage() repopulates the cache for downstream dashboard readers. No double-invalidation — the cache survives across non-syncing reads as designed.

Tests

33 tests (10 new files):

  • SyncAccountUsage: 7 (disabled no-op, missing account, account group, workspace group, skip workspace null, cache invalidation, queue name)
  • SyncUser (refactored): 5 (disabled no-op, missing user, identify, delegate dispatch, queue name)
  • SocialAccountObserver: 4 (create dispatches, delete dispatches, update does not, disabled does not)
  • SyncUsageOnPostCreated: 3 (handle dispatches, auto-discovery wired, disabled does not)
  • SyncUsageOnPostDeleted: 3 (handle dispatches, auto-discovery wired, disabled does not)

Full suite: 1558 passing, 0 failing, 2 skipped (pre-existing). Pint clean.

Test plan

  • php artisan test --compact --parallel — full suite passes
  • vendor/bin/pint --dirty --format agent — clean
  • php artisan event:list | grep -i postcreated — confirms auto-discovery wired
  • Manual: connect a social account on staging → check PostHog account group → social_accounts_count incremented
  • Manual: disconnect → count decremented
  • Manual: create + delete a post → posts_count follows
  • Manual self-hosted check: with POSTHOG_ENABLED=false, create a post and confirm Horizon shows zero SyncAccountUsage jobs queued

Out of scope

SyncUser is still dispatched (and runs as a no-op) from CreateUser and TrackBilling when PostHog is disabled. Those existing dispatch sites could also be gated for full open-source-friendly zero-queue behaviour — small follow-up if we want it.

paulocastellano and others added 4 commits May 15, 2026 18:40
…count group

Onboarding/lifecycle workflows in PostHog (and downstream tools like SendKit)
need to segment users by how many social accounts they've connected and how
many posts they've created. The existing SyncUser job only re-emitted these
counts on signup and billing changes, so the values went stale the moment a
user did anything meaningful.

This wires up two new paths that refresh the account group automatically:

- SocialAccountObserver (#[ObservedBy] on the model) fires SyncAccountUsage
  on created/deleted, covering all 14 OAuth callback paths in one hook.
- SyncUsageOnPostCreated / SyncUsageOnPostDeleted listeners (auto-discovered)
  fire SyncAccountUsage on the corresponding events dispatched by CreatePost
  and DeletePost.

SyncAccountUsage is the new dedicated job for group properties only
(groupIdentify account + workspace). SyncUser was slimmed to just identify
the user and delegate the group sync, removing the duplicated property
mapping between the two jobs.

All entry points (observer + both listeners) short-circuit when PostHog is
disabled, so self-hosted instances without PostHog configured see zero
queued jobs and zero overhead.

posts_count cache is invalidated before each sync so the job reads fresh
counts from the database instead of stale cached values.
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>
Refactor the user identification process by removing unnecessary type casting for user ID and account ID. This change enhances code readability and maintains functionality for syncing account usage.
@paulocastellano paulocastellano merged commit 5f40b7b into main May 19, 2026
2 checks passed
@paulocastellano paulocastellano deleted the feat/posthog-onboarding-properties branch May 19, 2026 22:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant