From 4627cf2db0213e0e5de75b0083e87c61d3949337 Mon Sep 17 00:00:00 2001 From: Adam Bowker Date: Fri, 17 Jul 2026 19:29:22 -0400 Subject: [PATCH 1/2] feat(users): add staff avatar backfill command One-shot management command that backfills UserPersonalization rows for active @posthog.com accounts from the public posthog.com/people roster, matched by normalized full name. Ambiguous names are skipped so nobody gets a namesake's photo; supports --dry-run and --overwrite and is idempotent otherwise. Stacked on the personalization-table PR. --- .../commands/backfill_staff_avatar_urls.py | 147 ++++++++++++++++++ .../test/test_backfill_staff_avatar_urls.py | 66 ++++++++ 2 files changed, 213 insertions(+) create mode 100644 posthog/management/commands/backfill_staff_avatar_urls.py create mode 100644 posthog/management/commands/test/test_backfill_staff_avatar_urls.py diff --git a/posthog/management/commands/backfill_staff_avatar_urls.py b/posthog/management/commands/backfill_staff_avatar_urls.py new file mode 100644 index 000000000000..f5b581920ce9 --- /dev/null +++ b/posthog/management/commands/backfill_staff_avatar_urls.py @@ -0,0 +1,147 @@ +import logging +import unicodedata +from typing import Any, Optional + +from django.core.management.base import BaseCommand, CommandParser + +import requests + +from posthog.models import User, UserPersonalization + +logger = logging.getLogger(__name__) + +# posthog.com is a Gatsby site with no team API: the roster ships as a +# build-time static-query artifact. The hash is a content hash of the site's +# GraphQL query, so when it drifts we rescan the hashes the /people page +# manifest declares. +PAGE_DATA_BASE = "https://posthog.com/page-data" +KNOWN_TEAM_QUERY_HASH = "2290419275" +STAFF_EMAIL_DOMAIN = "posthog.com" +REQUEST_TIMEOUT_SECONDS = 15 + + +def _fetch_json(url: str) -> Optional[Any]: + try: + response = requests.get(url, timeout=REQUEST_TIMEOUT_SECONDS) + if response.status_code != 200: + return None + return response.json() + except (requests.RequestException, ValueError): + return None + + +def _parse_profiles(payload: Any) -> list[dict[str, Any]]: + members = ((payload or {}).get("data") or {}).get("team", {}).get("teamMembers") + if not isinstance(members, list): + return [] + profiles = [] + for member in members: + if not isinstance(member, dict) or not isinstance(member.get("firstName"), str): + continue + avatar = member.get("avatar") or {} + url = avatar.get("url") if isinstance(avatar, dict) else None + profiles.append( + { + "first_name": member["firstName"], + "last_name": member.get("lastName") or "", + "avatar_url": url if isinstance(url, str) else None, + } + ) + return profiles + + +def fetch_team_profiles() -> list[dict[str, Any]]: + profiles = _parse_profiles(_fetch_json(f"{PAGE_DATA_BASE}/sq/d/{KNOWN_TEAM_QUERY_HASH}.json")) + if profiles: + return profiles + manifest = _fetch_json(f"{PAGE_DATA_BASE}/people/page-data.json") or {} + for query_hash in manifest.get("staticQueryHashes") or []: + if not isinstance(query_hash, str) or query_hash == KNOWN_TEAM_QUERY_HASH: + continue + profiles = _parse_profiles(_fetch_json(f"{PAGE_DATA_BASE}/sq/d/{query_hash}.json")) + if profiles: + return profiles + return [] + + +def normalize_name(value: str) -> str: + decomposed = unicodedata.normalize("NFKD", value) + stripped = "".join(char for char in decomposed if not unicodedata.combining(char)) + return " ".join(stripped.lower().split()) + + +def build_avatar_index(profiles: list[dict[str, Any]]) -> dict[str, Optional[str]]: + # Full-name key -> avatar URL; two different people sharing a name makes + # the key ambiguous (None) so nobody gets a namesake's photo. + index: dict[str, Optional[str]] = {} + for profile in profiles: + if not profile["avatar_url"]: + continue + key = normalize_name(f"{profile['first_name']} {profile['last_name']}") + if key in index and index[key] != profile["avatar_url"]: + index[key] = None + else: + index[key] = profile["avatar_url"] + return index + + +class Command(BaseCommand): + help = ( + "Backfill UserPersonalization.avatar_url for PostHog staff accounts from the public " + "posthog.com/people roster, matched by full name. Ambiguous names and accounts without a " + "roster match are skipped. Idempotent; skips users who already have an avatar unless " + "--overwrite." + ) + + def add_arguments(self, parser: CommandParser) -> None: + parser.add_argument("--dry-run", action="store_true", help="Report matches without writing.") + parser.add_argument( + "--overwrite", + action="store_true", + help="Also update users who already have an avatar set.", + ) + + def handle(self, *args: Any, **options: Any) -> None: + dry_run: bool = options["dry_run"] + overwrite: bool = options["overwrite"] + + profiles = fetch_team_profiles() + if not profiles: + self.stderr.write(self.style.ERROR("Could not fetch the posthog.com team roster; aborting.")) + return + index = build_avatar_index(profiles) + self.stdout.write(f"Fetched {len(profiles)} roster profiles ({len(index)} name keys).") + + users = User.objects.filter(email__iendswith=f"@{STAFF_EMAIL_DOMAIN}", is_active=True).select_related( + "personalization" + ) + + updated = 0 + ambiguous = 0 + unmatched = 0 + for user in users.iterator(): + existing = getattr(user, "personalization", None) + if existing and existing.avatar_url and not overwrite: + continue + # Staff accounts sometimes hold the full name in first_name with + # last_name empty, so match on the combined display name. + key = normalize_name(f"{user.first_name} {user.last_name}") + if key not in index: + unmatched += 1 + continue + avatar_url = index[key] + if avatar_url is None: + ambiguous += 1 + self.stdout.write(f" ambiguous roster name for {user.email}; skipping") + continue + if existing and existing.avatar_url == avatar_url: + continue + self.stdout.write(f" {user.email} -> {avatar_url}") + if not dry_run: + UserPersonalization.objects.update_or_create(user=user, defaults={"avatar_url": avatar_url}) + updated += 1 + + prefix = "[DRY RUN] Would update" if dry_run else "Updated" + self.stdout.write( + self.style.SUCCESS(f"{prefix} {updated} users ({ambiguous} ambiguous, {unmatched} without a roster match).") + ) diff --git a/posthog/management/commands/test/test_backfill_staff_avatar_urls.py b/posthog/management/commands/test/test_backfill_staff_avatar_urls.py new file mode 100644 index 000000000000..c0a87dcc8b56 --- /dev/null +++ b/posthog/management/commands/test/test_backfill_staff_avatar_urls.py @@ -0,0 +1,66 @@ +from typing import Any + +from posthog.test.base import BaseTest +from unittest.mock import patch + +from django.core.management import call_command + +from posthog.models import User, UserPersonalization + +ROSTER_PAYLOAD = { + "data": { + "team": { + "teamMembers": [ + {"firstName": "Raquel", "lastName": "Smith", "avatar": {"url": "https://cdn/raquel.png"}}, + {"firstName": "James", "lastName": "Hawkins", "avatar": {"url": "https://cdn/james-h.png"}}, + {"firstName": "James", "lastName": "Hawkins", "avatar": {"url": "https://cdn/other-james-h.png"}}, + {"firstName": "NoPhoto", "lastName": "Person", "avatar": None}, + ] + } + } +} + + +def avatar_of(user: User) -> Any: + personalization = UserPersonalization.objects.filter(user=user).first() + return personalization.avatar_url if personalization else None + + +@patch( + "posthog.management.commands.backfill_staff_avatar_urls._fetch_json", + return_value=ROSTER_PAYLOAD, +) +class TestBackfillStaffAvatarUrls(BaseTest): + def _create_staff_user(self, email: str, first_name: str, last_name: str = "", **kwargs: Any) -> User: + user = User.objects.create_user(email=email, password=None, first_name=first_name, **kwargs) + user.last_name = last_name + user.save() + return user + + def test_matches_staff_by_name_and_skips_ambiguous_and_non_staff(self, _mock_fetch: Any) -> None: + matched = self._create_staff_user("raquel@posthog.com", "Raquel", "Smith") + full_name_in_first = self._create_staff_user("raquel2@posthog.com", "Raquel Smith") + ambiguous = self._create_staff_user("james.h@posthog.com", "James", "Hawkins") + non_staff = self._create_staff_user("raquel@example.com", "Raquel", "Smith") + + call_command("backfill_staff_avatar_urls") + + self.assertEqual(avatar_of(matched), "https://cdn/raquel.png") + self.assertEqual(avatar_of(full_name_in_first), "https://cdn/raquel.png") + self.assertIsNone(avatar_of(ambiguous)) + self.assertIsNone(avatar_of(non_staff)) + + def test_dry_run_writes_nothing(self, _mock_fetch: Any) -> None: + user = self._create_staff_user("raquel@posthog.com", "Raquel", "Smith") + call_command("backfill_staff_avatar_urls", "--dry-run") + self.assertIsNone(avatar_of(user)) + + def test_existing_avatar_kept_unless_overwrite(self, _mock_fetch: Any) -> None: + user = self._create_staff_user("raquel@posthog.com", "Raquel", "Smith") + UserPersonalization.objects.create(user=user, avatar_url="https://example.com/custom.png") + + call_command("backfill_staff_avatar_urls") + self.assertEqual(avatar_of(user), "https://example.com/custom.png") + + call_command("backfill_staff_avatar_urls", "--overwrite") + self.assertEqual(avatar_of(user), "https://cdn/raquel.png") From 273d0891f2f1eaacab8cf5955ce7783b1227562c Mon Sep 17 00:00:00 2001 From: "tests-posthog[bot]" <250237707+tests-posthog[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 23:33:42 +0000 Subject: [PATCH 2/2] chore: update OpenAPI generated types --- frontend/src/generated/core/api.schemas.ts | 12 +++ frontend/src/generated/core/api.zod.ts | 77 +++++++++++++++++++ .../frontend/generated/api.schemas.ts | 10 +++ services/mcp/src/api/generated.ts | 22 ++++++ services/mcp/src/generated/core/api.ts | 7 ++ services/mcp/src/tools/generated/core.ts | 3 + 6 files changed, 131 insertions(+) diff --git a/frontend/src/generated/core/api.schemas.ts b/frontend/src/generated/core/api.schemas.ts index 4a6ebe54c419..7a8dae32f98f 100644 --- a/frontend/src/generated/core/api.schemas.ts +++ b/frontend/src/generated/core/api.schemas.ts @@ -3512,6 +3512,12 @@ export interface UserApi { readonly scene_personalisation: readonly ScenePersonalisationBasicApi[] theme_mode?: ThemeModeEnumApi | BlankEnumApi | null hedgehog_config?: unknown + /** + * Profile picture URL, shown across PostHog apps in place of the Gravatar/initials fallback. + * @maxLength 800 + * @nullable + */ + avatar_url?: string | null /** @nullable */ allow_sidebar_suggestions?: boolean | null shortcut_position?: ShortcutPositionEnumApi | BlankEnumApi | null @@ -3619,6 +3625,12 @@ export interface PatchedUserApi { readonly scene_personalisation?: readonly ScenePersonalisationBasicApi[] theme_mode?: ThemeModeEnumApi | BlankEnumApi | null hedgehog_config?: unknown + /** + * Profile picture URL, shown across PostHog apps in place of the Gravatar/initials fallback. + * @maxLength 800 + * @nullable + */ + avatar_url?: string | null /** @nullable */ allow_sidebar_suggestions?: boolean | null shortcut_position?: ShortcutPositionEnumApi | BlankEnumApi | null diff --git a/frontend/src/generated/core/api.zod.ts b/frontend/src/generated/core/api.zod.ts index 0c78dd3e7876..3893f0c8f8cb 100644 --- a/frontend/src/generated/core/api.zod.ts +++ b/frontend/src/generated/core/api.zod.ts @@ -9688,6 +9688,8 @@ export const usersUpdateBodyEmailMax = 254 export const usersUpdateBodyPasswordMax = 128 +export const usersUpdateBodyAvatarUrlMax = 800 + export const UsersUpdateBody = /* @__PURE__ */ zod.object({ first_name: zod.string().max(usersUpdateBodyFirstNameMax).optional(), last_name: zod.string().max(usersUpdateBodyLastNameMax).optional(), @@ -9732,6 +9734,11 @@ export const UsersUpdateBody = /* @__PURE__ */ zod.object({ ]) .optional(), hedgehog_config: zod.unknown().optional(), + avatar_url: zod + .url() + .max(usersUpdateBodyAvatarUrlMax) + .nullish() + .describe('Profile picture URL, shown across PostHog apps in place of the Gravatar\/initials fallback.'), allow_sidebar_suggestions: zod.boolean().nullish(), shortcut_position: zod .union([ @@ -9773,6 +9780,8 @@ export const usersPartialUpdateBodyEmailMax = 254 export const usersPartialUpdateBodyPasswordMax = 128 +export const usersPartialUpdateBodyAvatarUrlMax = 800 + export const UsersPartialUpdateBody = /* @__PURE__ */ zod.object({ first_name: zod.string().max(usersPartialUpdateBodyFirstNameMax).optional(), last_name: zod.string().max(usersPartialUpdateBodyLastNameMax).optional(), @@ -9817,6 +9826,11 @@ export const UsersPartialUpdateBody = /* @__PURE__ */ zod.object({ ]) .optional(), hedgehog_config: zod.unknown().optional(), + avatar_url: zod + .url() + .max(usersPartialUpdateBodyAvatarUrlMax) + .nullish() + .describe('Profile picture URL, shown across PostHog apps in place of the Gravatar\/initials fallback.'), allow_sidebar_suggestions: zod.boolean().nullish(), shortcut_position: zod .union([ @@ -9855,6 +9869,8 @@ export const usersHedgehogConfigPartialUpdateBodyEmailMax = 254 export const usersHedgehogConfigPartialUpdateBodyPasswordMax = 128 +export const usersHedgehogConfigPartialUpdateBodyAvatarUrlMax = 800 + export const UsersHedgehogConfigPartialUpdateBody = /* @__PURE__ */ zod.object({ first_name: zod.string().max(usersHedgehogConfigPartialUpdateBodyFirstNameMax).optional(), last_name: zod.string().max(usersHedgehogConfigPartialUpdateBodyLastNameMax).optional(), @@ -9899,6 +9915,11 @@ export const UsersHedgehogConfigPartialUpdateBody = /* @__PURE__ */ zod.object({ ]) .optional(), hedgehog_config: zod.unknown().optional(), + avatar_url: zod + .url() + .max(usersHedgehogConfigPartialUpdateBodyAvatarUrlMax) + .nullish() + .describe('Profile picture URL, shown across PostHog apps in place of the Gravatar\/initials fallback.'), allow_sidebar_suggestions: zod.boolean().nullish(), shortcut_position: zod .union([ @@ -10071,6 +10092,8 @@ export const usersScenePersonalisationCreateBodyEmailMax = 254 export const usersScenePersonalisationCreateBodyPasswordMax = 128 +export const usersScenePersonalisationCreateBodyAvatarUrlMax = 800 + export const UsersScenePersonalisationCreateBody = /* @__PURE__ */ zod.object({ first_name: zod.string().max(usersScenePersonalisationCreateBodyFirstNameMax).optional(), last_name: zod.string().max(usersScenePersonalisationCreateBodyLastNameMax).optional(), @@ -10115,6 +10138,11 @@ export const UsersScenePersonalisationCreateBody = /* @__PURE__ */ zod.object({ ]) .optional(), hedgehog_config: zod.unknown().optional(), + avatar_url: zod + .url() + .max(usersScenePersonalisationCreateBodyAvatarUrlMax) + .nullish() + .describe('Profile picture URL, shown across PostHog apps in place of the Gravatar\/initials fallback.'), allow_sidebar_suggestions: zod.boolean().nullish(), shortcut_position: zod .union([ @@ -10156,6 +10184,8 @@ export const usersTwoFactorBackupCodesCreateBodyEmailMax = 254 export const usersTwoFactorBackupCodesCreateBodyPasswordMax = 128 +export const usersTwoFactorBackupCodesCreateBodyAvatarUrlMax = 800 + export const UsersTwoFactorBackupCodesCreateBody = /* @__PURE__ */ zod.object({ first_name: zod.string().max(usersTwoFactorBackupCodesCreateBodyFirstNameMax).optional(), last_name: zod.string().max(usersTwoFactorBackupCodesCreateBodyLastNameMax).optional(), @@ -10200,6 +10230,11 @@ export const UsersTwoFactorBackupCodesCreateBody = /* @__PURE__ */ zod.object({ ]) .optional(), hedgehog_config: zod.unknown().optional(), + avatar_url: zod + .url() + .max(usersTwoFactorBackupCodesCreateBodyAvatarUrlMax) + .nullish() + .describe('Profile picture URL, shown across PostHog apps in place of the Gravatar\/initials fallback.'), allow_sidebar_suggestions: zod.boolean().nullish(), shortcut_position: zod .union([ @@ -10241,6 +10276,8 @@ export const usersTwoFactorDisableCreateBodyEmailMax = 254 export const usersTwoFactorDisableCreateBodyPasswordMax = 128 +export const usersTwoFactorDisableCreateBodyAvatarUrlMax = 800 + export const UsersTwoFactorDisableCreateBody = /* @__PURE__ */ zod.object({ first_name: zod.string().max(usersTwoFactorDisableCreateBodyFirstNameMax).optional(), last_name: zod.string().max(usersTwoFactorDisableCreateBodyLastNameMax).optional(), @@ -10285,6 +10322,11 @@ export const UsersTwoFactorDisableCreateBody = /* @__PURE__ */ zod.object({ ]) .optional(), hedgehog_config: zod.unknown().optional(), + avatar_url: zod + .url() + .max(usersTwoFactorDisableCreateBodyAvatarUrlMax) + .nullish() + .describe('Profile picture URL, shown across PostHog apps in place of the Gravatar\/initials fallback.'), allow_sidebar_suggestions: zod.boolean().nullish(), shortcut_position: zod .union([ @@ -10323,6 +10365,8 @@ export const usersTwoFactorValidateCreateBodyEmailMax = 254 export const usersTwoFactorValidateCreateBodyPasswordMax = 128 +export const usersTwoFactorValidateCreateBodyAvatarUrlMax = 800 + export const UsersTwoFactorValidateCreateBody = /* @__PURE__ */ zod.object({ first_name: zod.string().max(usersTwoFactorValidateCreateBodyFirstNameMax).optional(), last_name: zod.string().max(usersTwoFactorValidateCreateBodyLastNameMax).optional(), @@ -10367,6 +10411,11 @@ export const UsersTwoFactorValidateCreateBody = /* @__PURE__ */ zod.object({ ]) .optional(), hedgehog_config: zod.unknown().optional(), + avatar_url: zod + .url() + .max(usersTwoFactorValidateCreateBodyAvatarUrlMax) + .nullish() + .describe('Profile picture URL, shown across PostHog apps in place of the Gravatar\/initials fallback.'), allow_sidebar_suggestions: zod.boolean().nullish(), shortcut_position: zod .union([ @@ -10405,6 +10454,8 @@ export const usersValidate2faCreateBodyEmailMax = 254 export const usersValidate2faCreateBodyPasswordMax = 128 +export const usersValidate2faCreateBodyAvatarUrlMax = 800 + export const UsersValidate2faCreateBody = /* @__PURE__ */ zod.object({ first_name: zod.string().max(usersValidate2faCreateBodyFirstNameMax).optional(), last_name: zod.string().max(usersValidate2faCreateBodyLastNameMax).optional(), @@ -10449,6 +10500,11 @@ export const UsersValidate2faCreateBody = /* @__PURE__ */ zod.object({ ]) .optional(), hedgehog_config: zod.unknown().optional(), + avatar_url: zod + .url() + .max(usersValidate2faCreateBodyAvatarUrlMax) + .nullish() + .describe('Profile picture URL, shown across PostHog apps in place of the Gravatar\/initials fallback.'), allow_sidebar_suggestions: zod.boolean().nullish(), shortcut_position: zod .union([ @@ -10487,6 +10543,8 @@ export const usersCancelEmailChangeRequestPartialUpdateBodyEmailMax = 254 export const usersCancelEmailChangeRequestPartialUpdateBodyPasswordMax = 128 +export const usersCancelEmailChangeRequestPartialUpdateBodyAvatarUrlMax = 800 + export const UsersCancelEmailChangeRequestPartialUpdateBody = /* @__PURE__ */ zod.object({ first_name: zod.string().max(usersCancelEmailChangeRequestPartialUpdateBodyFirstNameMax).optional(), last_name: zod.string().max(usersCancelEmailChangeRequestPartialUpdateBodyLastNameMax).optional(), @@ -10531,6 +10589,11 @@ export const UsersCancelEmailChangeRequestPartialUpdateBody = /* @__PURE__ */ zo ]) .optional(), hedgehog_config: zod.unknown().optional(), + avatar_url: zod + .url() + .max(usersCancelEmailChangeRequestPartialUpdateBodyAvatarUrlMax) + .nullish() + .describe('Profile picture URL, shown across PostHog apps in place of the Gravatar\/initials fallback.'), allow_sidebar_suggestions: zod.boolean().nullish(), shortcut_position: zod .union([ @@ -10569,6 +10632,8 @@ export const usersRequestEmailVerificationCreateBodyEmailMax = 254 export const usersRequestEmailVerificationCreateBodyPasswordMax = 128 +export const usersRequestEmailVerificationCreateBodyAvatarUrlMax = 800 + export const UsersRequestEmailVerificationCreateBody = /* @__PURE__ */ zod.object({ first_name: zod.string().max(usersRequestEmailVerificationCreateBodyFirstNameMax).optional(), last_name: zod.string().max(usersRequestEmailVerificationCreateBodyLastNameMax).optional(), @@ -10613,6 +10678,11 @@ export const UsersRequestEmailVerificationCreateBody = /* @__PURE__ */ zod.objec ]) .optional(), hedgehog_config: zod.unknown().optional(), + avatar_url: zod + .url() + .max(usersRequestEmailVerificationCreateBodyAvatarUrlMax) + .nullish() + .describe('Profile picture URL, shown across PostHog apps in place of the Gravatar\/initials fallback.'), allow_sidebar_suggestions: zod.boolean().nullish(), shortcut_position: zod .union([ @@ -10651,6 +10721,8 @@ export const usersVerifyEmailCreateBodyEmailMax = 254 export const usersVerifyEmailCreateBodyPasswordMax = 128 +export const usersVerifyEmailCreateBodyAvatarUrlMax = 800 + export const UsersVerifyEmailCreateBody = /* @__PURE__ */ zod.object({ first_name: zod.string().max(usersVerifyEmailCreateBodyFirstNameMax).optional(), last_name: zod.string().max(usersVerifyEmailCreateBodyLastNameMax).optional(), @@ -10695,6 +10767,11 @@ export const UsersVerifyEmailCreateBody = /* @__PURE__ */ zod.object({ ]) .optional(), hedgehog_config: zod.unknown().optional(), + avatar_url: zod + .url() + .max(usersVerifyEmailCreateBodyAvatarUrlMax) + .nullish() + .describe('Profile picture URL, shown across PostHog apps in place of the Gravatar\/initials fallback.'), allow_sidebar_suggestions: zod.boolean().nullish(), shortcut_position: zod .union([ diff --git a/products/platform_features/frontend/generated/api.schemas.ts b/products/platform_features/frontend/generated/api.schemas.ts index bf5c535ca959..0d7a4795854b 100644 --- a/products/platform_features/frontend/generated/api.schemas.ts +++ b/products/platform_features/frontend/generated/api.schemas.ts @@ -316,6 +316,11 @@ export interface OrganizationMemberApi { readonly is_2fa_enabled: boolean readonly has_social_auth: boolean readonly last_login: string + /** + * The member's profile picture URL, when they have set one. + * @nullable + */ + readonly avatar_url: string | null /** How this row matched the `search` query parameter: `exact` (the term is a case-insensitive substring of a searched field) or `similar` (a fuzzy trigram match, returned only when no exact match exists). Null when the list is not filtered by `search`. */ readonly search_match_type: SearchMatchTypeEnumApi | null } @@ -338,6 +343,11 @@ export interface PatchedOrganizationMemberApi { readonly is_2fa_enabled?: boolean readonly has_social_auth?: boolean readonly last_login?: string + /** + * The member's profile picture URL, when they have set one. + * @nullable + */ + readonly avatar_url?: string | null /** How this row matched the `search` query parameter: `exact` (the term is a case-insensitive substring of a searched field) or `similar` (a fuzzy trigram match, returned only when no exact match exists). Null when the list is not filtered by `search`. */ readonly search_match_type?: SearchMatchTypeEnumApi | null } diff --git a/services/mcp/src/api/generated.ts b/services/mcp/src/api/generated.ts index d04e759cbd2c..9f792fad0e74 100644 --- a/services/mcp/src/api/generated.ts +++ b/services/mcp/src/api/generated.ts @@ -36714,6 +36714,11 @@ export namespace Schemas { readonly is_2fa_enabled: boolean; readonly has_social_auth: boolean; readonly last_login: string; + /** + * The member's profile picture URL, when they have set one. + * @nullable + */ + readonly avatar_url: string | null; /** How this row matched the `search` query parameter: `exact` (the term is a case-insensitive substring of a searched field) or `similar` (a fuzzy trigram match, returned only when no exact match exists). Null when the list is not filtered by `search`. */ readonly search_match_type: SearchMatchTypeEnum | null; } @@ -41883,6 +41888,12 @@ export namespace Schemas { readonly scene_personalisation: readonly ScenePersonalisationBasic[]; theme_mode?: ThemeModeEnum | BlankEnum | null; hedgehog_config?: unknown; + /** + * Profile picture URL, shown across PostHog apps in place of the Gravatar/initials fallback. + * @maxLength 800 + * @nullable + */ + avatar_url?: string | null; /** @nullable */ allow_sidebar_suggestions?: boolean | null; shortcut_position?: ShortcutPositionEnum | BlankEnum | null; @@ -46046,6 +46057,11 @@ export namespace Schemas { readonly is_2fa_enabled?: boolean; readonly has_social_auth?: boolean; readonly last_login?: string; + /** + * The member's profile picture URL, when they have set one. + * @nullable + */ + readonly avatar_url?: string | null; /** How this row matched the `search` query parameter: `exact` (the term is a case-insensitive substring of a searched field) or `similar` (a fuzzy trigram match, returned only when no exact match exists). Null when the list is not filtered by `search`. */ readonly search_match_type?: SearchMatchTypeEnum | null; } @@ -49417,6 +49433,12 @@ export namespace Schemas { readonly scene_personalisation?: readonly ScenePersonalisationBasic[]; theme_mode?: ThemeModeEnum | BlankEnum | null; hedgehog_config?: unknown; + /** + * Profile picture URL, shown across PostHog apps in place of the Gravatar/initials fallback. + * @maxLength 800 + * @nullable + */ + avatar_url?: string | null; /** @nullable */ allow_sidebar_suggestions?: boolean | null; shortcut_position?: ShortcutPositionEnum | BlankEnum | null; diff --git a/services/mcp/src/generated/core/api.ts b/services/mcp/src/generated/core/api.ts index 405ecd88d55d..e3bddfda3242 100644 --- a/services/mcp/src/generated/core/api.ts +++ b/services/mcp/src/generated/core/api.ts @@ -789,6 +789,8 @@ export const usersPartialUpdateBodyEmailMax = 254 export const usersPartialUpdateBodyPasswordMax = 128 +export const usersPartialUpdateBodyAvatarUrlMax = 800 + export const UsersPartialUpdateBody = /* @__PURE__ */ zod.object({ first_name: zod.string().max(usersPartialUpdateBodyFirstNameMax).optional(), last_name: zod.string().max(usersPartialUpdateBodyLastNameMax).optional(), @@ -830,6 +832,11 @@ export const UsersPartialUpdateBody = /* @__PURE__ */ zod.object({ ]) .optional(), hedgehog_config: zod.unknown().optional(), + avatar_url: zod + .url() + .max(usersPartialUpdateBodyAvatarUrlMax) + .nullish() + .describe('Profile picture URL, shown across PostHog apps in place of the Gravatar/initials fallback.'), allow_sidebar_suggestions: zod.boolean().nullish(), shortcut_position: zod .union([ diff --git a/services/mcp/src/tools/generated/core.ts b/services/mcp/src/tools/generated/core.ts index 3db2c1ae1fb0..82d8b06fd7b4 100644 --- a/services/mcp/src/tools/generated/core.ts +++ b/services/mcp/src/tools/generated/core.ts @@ -568,6 +568,9 @@ const userSettingsUpdate = (): ToolBase