Skip to content

feat(users): add UserPersonalization side table with avatar_url - #72086

Closed
adboio wants to merge 2 commits into
masterfrom
posthog-code/user-avatar-url
Closed

feat(users): add UserPersonalization side table with avatar_url#72086
adboio wants to merge 2 commits into
masterfrom
posthog-code/user-avatar-url

Conversation

@adboio

@adboio adboio commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Problem

PostHog apps have no real profile photos for users — every avatar surface falls back to Gravatar (which most accounts don't have) or initials. PostHog Code is adding a generic UserAvatar component that reads a profile photo off the API, but there's nowhere to store one. An earlier revision of this PR added a column to posthog_user; that's a hot table, so the field now lives on a side table instead and the core user model is untouched.

Changes

  • New UserPersonalization model: one row per user (OneToOneField), currently holding avatar_url, with room for future per-user appearance/profile preferences. Follows the existing user-keyed side-table pattern (UserProductList, UserScenePersonalisation, UserHomeSettings).
  • avatar_url readable and writable (https-only) on /api/users/@me/ — writes upsert the side row, posthog_user is never written.
  • Exposed on the org members endpoint (OrganizationMemberSerializer.avatar_url) via a select_related on the shared members queryset, so clients resolve teammates' avatars from the members list. UserBasicSerializer is deliberately untouched — no payload changes ripple through other endpoints, and no SQL snapshots move.
  • Migration is a plain CREATE TABLE; no hot-table acknowledgment or coordinated deploy needed.

Stacked follow-up: #72092 backfills staff avatars from posthog.com/people. PostHog Code consumes this in PostHog/code#3549.

How did you test this code?

Automated tests added (not run locally — this environment has no dev stack, so CI is the arbiter): posthog/api/test/test_user_avatar_url.py — set/remove via PATCH @me (asserting the side row), default-null read without a row, https-only rejection, and exposure on the org members list. Catches the upsert path breaking, the field becoming read-only, or the members exposure regressing.

Not run locally: hogli build:openapi and mypy — expecting the CI bots/checks to flag anything needed.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Docs update

N/A — API reference flows from serializer help_text.

🤖 Agent context

Human-driven (agent-assisted) — requested by Adam Bowker in the project-bluebird channel (PostHog Code); reworked from a posthog_user column to a side table at his direction.


Created with PostHog Code

adboio added a commit to PostHog/code that referenced this pull request Jul 17, 2026
New "Profile picture" row in General settings (flag-gated): preview via
UserAvatar, set/change with an https image URL, and remove. Saves to
the PostHog account through PATCH /api/users/@me/ {avatar_url} (new
updateCurrentUserAvatar client method; backend field lands in
PostHog/posthog#72086). UserAvatar/useUserAvatar props widen to a
structural AvatarUser so the full user object works too.

Generated-By: PostHog Code
Task-Id: 66aecb98-c55b-4516-a7aa-42c53cfa8bd2
@adboio adboio added the run-ci-backend Force ci-backend's full test matrices to run even on a draft PR label Jul 17, 2026 — with PostHog
@adboio
adboio force-pushed the posthog-code/user-avatar-url branch from ec9706d to fffc818 Compare July 17, 2026 21:17
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

Bundle size — no change

Uncompressed size of every built .js bundle, compared against the base branch.

Total: 64.22 MiB · no change

No file changed by more than 1000 B.

Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report

Eager graph — within budget

How much code each root ships on the eager path — downloaded and parsed before the surface is interactive. Measured from the esbuild output chunks (post-tree-shake, static imports only); lazy import() / React.lazy chunks are not counted.

Root Eager (shipped) Δ vs base Budget
entry (logged-out pages, app bootstrap)
src/index.tsx
1.23 MiB · 22 files no change ███░░░░░░░ 28.6% of 4.29 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
8.16 MiB · 2,988 files no change █████████░ 88.2% of 9.25 MiB

🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx

Largest files eagerly shipped from src/index.tsx
Size File
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
24.6 KiB ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js
6.3 KiB ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
4.5 KiB ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js
3.9 KiB ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js
1.4 KiB ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js
1.3 KiB src/RootErrorBoundary.tsx
912 B ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js
789 B src/scenes/ChunkLoadErrorBoundary.tsx
762 B src/index.tsx
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
281.3 KiB ../node_modules/.pnpm/posthog-js@1.404.0/node_modules/posthog-js/dist/rrweb.js
267.7 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
235.5 KiB src/taxonomy/core-filter-definitions-by-group.json
223.2 KiB ../node_modules/.pnpm/posthog-js@1.404.0/node_modules/posthog-js/dist/module.js
164.0 KiB src/queries/validators.js
154.3 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
105.8 KiB src/lib/api.ts
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
93.2 KiB ../packages/quill/packages/quill/dist/index.js

Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479

Dist folder size — 🔺 +1.1 KiB (+0.0%)

Total size of the built frontend/dist folder (all assets), compared against the base branch.

Total: 1341.50 MiB · 🔺 +1.1 KiB (+0.0%)

⚠️ Backend snapshots — 1 updated (1 modified, 0 added, 0 deleted)

Query snapshots: Backend query snapshots updated

Changes: 1 snapshots (1 modified, 0 added, 0 deleted)

What this means:

  • Query snapshots have been automatically updated to match current output
  • These changes reflect modifications to database queries or schema

Next steps:

  • Review the query changes to ensure they're intentional
  • If unexpected, investigate what caused the query to change

Review snapshot changes →

⚠️ MCP snapshots — 2 updated (2 modified, 0 added, 0 deleted)

Snapshots: MCP unit test snapshots updated

Changes: 2 snapshots (2 modified, 0 added, 0 deleted)

What this means:

  • Snapshots have been automatically updated to match current output

Next steps:

  • Review the changes to ensure they're intentional
  • If unexpected, investigate what caused the output to change

Review snapshot changes →

⚠️ Django migration SQL — 2 new migrations to review

We've detected new migrations on this PR. Review the SQL output for each migration:

posthog/migrations/1259_userpersonalization.py

BEGIN;
--
-- Create model UserPersonalization
--
CREATE TABLE "posthog_userpersonalization" ("id" uuid NOT NULL PRIMARY KEY, "avatar_url" varchar(800) NULL, "created_at" timestamp with time zone NOT NULL, "updated_at" timestamp with time zone NOT NULL, "user_id" integer NOT NULL UNIQUE);
--
-- Add FOREIGN KEY constraint posthog_userpersonalization_user_id_fk on userpersonalization (NOT VALID)
--
ALTER TABLE "posthog_userpersonalization" ADD CONSTRAINT "posthog_userpersonalization_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "posthog_user" ("id") DEFERRABLE INITIALLY DEFERRED NOT VALID;
COMMIT;

posthog/migrations/1260_validate_userpersonalization_fk.py

BEGIN;
--
-- Validate constraint posthog_userpersonalization_user_id_fk on userpersonalization
--
SET lock_timeout = 0;
SET statement_timeout = 0;
ALTER TABLE "posthog_userpersonalization" VALIDATE CONSTRAINT "posthog_userpersonalization_user_id_fk";
COMMIT;

Last updated: 2026-07-17 23:54 UTC (1f8bcfa)

Django migration risk — migration analysis complete

We've analyzed your migrations for potential risks.

Summary: 0 Safe | 2 Needs Review | 0 Blocked

⚠️ Needs Review

May have performance impact

posthog.1259_userpersonalization
  └─ #1 ✅ CreateModel
     Creating new table is safe
     model: UserPersonalization
  └─ #2 ⚠️ AddForeignKeyNotValid: Unknown operation type: AddForeignKeyNotValid
  │
  └──> ℹ️  INFO:
       ℹ️  Skipped operations on newly created tables (empty tables
       don't cause lock contention).
posthog.1260_validate_userpersonalization_fk
  └─ #1 ⚠️ ValidateForeignKey: Unknown operation type: ValidateForeignKey

Last updated: 2026-07-17 23:54 UTC (1f8bcfa)

@adboio adboio changed the title feat(users): add avatar_url with staff backfill command feat(users): add user.avatar_url Jul 17, 2026
@adboio
adboio force-pushed the posthog-code/user-avatar-url branch from f592fe7 to 39fe807 Compare July 17, 2026 22:49
@adboio adboio changed the title feat(users): add user.avatar_url feat(users): add UserPersonalization side table with avatar_url Jul 17, 2026
@adboio
adboio force-pushed the posthog-code/user-avatar-url branch 4 times, most recently from 8ff71ae to 8768442 Compare July 17, 2026 23:13
Store the profile picture on a new UserPersonalization side table (one
row per user) instead of a column on the hot posthog_user table.
Readable and writable (https-only) via /api/users/@me/, and exposed on
the org members endpoint through a select_related so teammates'
avatars ride the members list without touching UserBasic. The user FK
is declared db_constraint=False, added NOT VALID in 1259, and
validated in 1260, so no migration blocks posthog_user traffic.
Registered in the user-scoped IDOR semgrep rules.
@adboio
adboio force-pushed the posthog-code/user-avatar-url branch from 8768442 to bf55fff Compare July 17, 2026 23:29
@adboio adboio closed this Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-ci-backend Force ci-backend's full test matrices to run even on a draft PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant