fix(backend): resolve missing email and display name for user identity tracking (#12719)

This commit is contained in:
sp.wack
2026-02-05 20:50:33 +04:00
committed by GitHub
parent 634c2439b4
commit a9ede73391
13 changed files with 1099 additions and 10 deletions

View File

@@ -4,6 +4,7 @@ from typing import Any
import httpx
from pydantic import SecretStr
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.protocols.http_client import HTTPClient
from openhands.integrations.service_types import (
BaseGitService,
@@ -24,6 +25,18 @@ class BitBucketMixinBase(BaseGitService, HTTPClient):
BASE_URL = 'https://api.bitbucket.org/2.0'
@staticmethod
def _resolve_primary_email(emails: list[dict]) -> str | None:
"""Find the primary confirmed email from a list of Bitbucket email objects.
Bitbucket's /user/emails endpoint returns objects with
'email', 'is_primary', and 'is_confirmed' keys.
"""
for entry in emails:
if entry.get('is_primary') and entry.get('is_confirmed'):
return entry.get('email')
return None
def _extract_owner_and_repo(self, repository: str) -> tuple[str, str]:
"""Extract owner and repo from repository string.
@@ -137,6 +150,17 @@ class BitBucketMixinBase(BaseGitService, HTTPClient):
return all_items[:max_items]
async def get_user_emails(self) -> list[dict]:
"""Fetch the authenticated user's email addresses from Bitbucket.
Calls GET /user/emails which returns a paginated response with a
'values' list of email objects containing 'email', 'is_primary',
and 'is_confirmed' fields.
"""
url = f'{self.BASE_URL}/user/emails'
response, _ = await self._make_request(url)
return response.get('values', [])
async def get_user(self) -> User:
"""Get the authenticated user's information."""
url = f'{self.BASE_URL}/user'
@@ -144,12 +168,22 @@ class BitBucketMixinBase(BaseGitService, HTTPClient):
account_id = data.get('account_id', '')
email = None
try:
emails = await self.get_user_emails()
email = self._resolve_primary_email(emails)
except Exception:
logger.warning(
'bitbucket:get_user:email_fallback_failed',
exc_info=True,
)
return User(
id=account_id,
login=data.get('username', ''),
avatar_url=data.get('links', {}).get('avatar', {}).get('href', ''),
name=data.get('display_name'),
email=None, # Bitbucket API doesn't return email in this endpoint
email=email,
)
def _parse_repository(

View File

@@ -4,6 +4,7 @@ from typing import Any, cast
import httpx
from pydantic import SecretStr
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.protocols.http_client import HTTPClient
from openhands.integrations.service_types import (
BaseGitService,
@@ -22,6 +23,19 @@ class GitHubMixinBase(BaseGitService, HTTPClient):
BASE_URL: str
GRAPHQL_URL: str
@staticmethod
def _resolve_primary_email(emails: list[dict]) -> str | None:
"""Find the primary verified email from a list of GitHub email objects.
GitHub's /user/emails endpoint returns a list of dicts, each with
'email', 'primary', and 'verified' keys. This selects the one marked
as both primary and verified — the email the user considers canonical.
"""
for entry in emails:
if entry.get('primary') and entry.get('verified'):
return entry.get('email')
return None
async def _get_headers(self) -> dict:
"""Retrieve the GH Token from settings store to construct the headers."""
if not self.token:
@@ -107,6 +121,17 @@ class GitHubMixinBase(BaseGitService, HTTPClient):
except httpx.HTTPError as e:
raise self.handle_http_error(e)
async def get_user_emails(self) -> list[dict]:
"""Fetch the authenticated user's email addresses from GitHub.
Calls GET /user/emails which returns a list of email objects, each
containing 'email', 'primary', 'verified', and 'visibility' fields.
Requires the user:email OAuth scope.
"""
url = f'{self.BASE_URL}/user/emails'
response, _ = await self._make_request(url)
return response
async def verify_access(self) -> bool:
url = f'{self.BASE_URL}'
await self._make_request(url)
@@ -116,11 +141,22 @@ class GitHubMixinBase(BaseGitService, HTTPClient):
url = f'{self.BASE_URL}/user'
response, _ = await self._make_request(url)
email = response.get('email')
if email is None:
try:
emails = await self.get_user_emails()
email = self._resolve_primary_email(emails)
except Exception:
logger.warning(
'github:get_user:email_fallback_failed',
exc_info=True,
)
return User(
id=str(response.get('id', '')),
login=cast(str, response.get('login') or ''),
avatar_url=cast(str, response.get('avatar_url') or ''),
company=response.get('company'),
name=response.get('name'),
email=response.get('email'),
email=email,
)