fix(tempo): fix EU base URL, trailing space in account ID, and tempo_synced tracking
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Root causes found for Tempo worklogs never reaching Tempo: 1. Wrong API region: workspace is on api.eu.tempo.io/4 but code used api.tempo.io/4 → Tempo returned "User is invalid" (400) for all POST /worklogs 2. Trailing space in jira_account_id stored in DB (now stripped with .strip()) 3. tempo_synced field was never updated even on success (now set from Tempo response) Fix: add tempo.base_url system_config key (admin-configurable without redeploy), fall back to TEMPO_BASE_URL env-var, then global default. DB already updated with https://api.eu.tempo.io/4 for this workspace. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -63,6 +63,9 @@ class Settings(BaseSettings):
|
||||
TEMPO_API_TOKEN: str = ""
|
||||
TEMPO_API_VERSION: int = 4
|
||||
TEMPO_DEFAULT_WORK_TYPE: str = "Red Team"
|
||||
# Tempo API base URL — use https://api.eu.tempo.io/4 for EU workspaces.
|
||||
# Can also be set via system_configs key "tempo.base_url" at runtime.
|
||||
TEMPO_BASE_URL: str = "" # empty → falls back to https://api.tempo.io/4
|
||||
|
||||
# ── OSINT / Intelligence ────────────────────────────────────────
|
||||
NVD_API_KEY: str = "" # optional; increases NVD rate limit from 5/30s to 50/30s
|
||||
|
||||
@@ -40,9 +40,41 @@ def has_tempo_configured(user) -> bool:
|
||||
return bool(getattr(user, "tempo_api_token", None))
|
||||
|
||||
|
||||
def get_user_tempo_client(user):
|
||||
_TEMPO_DEFAULT_BASE_URL = "https://api.tempo.io/4"
|
||||
_TEMPO_EU_BASE_URL = "https://api.eu.tempo.io/4"
|
||||
|
||||
# system_configs key for admin-configurable Tempo base URL
|
||||
_TEMPO_BASE_URL_CONFIG_KEY = "tempo.base_url"
|
||||
|
||||
|
||||
def _get_tempo_base_url(db=None) -> str:
|
||||
"""Return the Tempo API base URL.
|
||||
|
||||
Reads ``tempo.base_url`` from ``system_configs`` first (allows the admin
|
||||
to set the EU endpoint ``https://api.eu.tempo.io/4`` without redeploying).
|
||||
Falls back to ``settings.TEMPO_BASE_URL`` env-var, then the global default.
|
||||
"""
|
||||
if db is not None:
|
||||
try:
|
||||
from app.models.system_config import SystemConfig
|
||||
row = db.query(SystemConfig).filter(
|
||||
SystemConfig.key == _TEMPO_BASE_URL_CONFIG_KEY
|
||||
).first()
|
||||
if row and row.value:
|
||||
return row.value.rstrip("/")
|
||||
except Exception:
|
||||
pass # DB unavailable — fall through to defaults
|
||||
|
||||
env_url = getattr(settings, "TEMPO_BASE_URL", None)
|
||||
return (env_url or _TEMPO_DEFAULT_BASE_URL).rstrip("/")
|
||||
|
||||
|
||||
def get_user_tempo_client(user, db=None):
|
||||
"""Return a Tempo API v4 client authenticated as *user*.
|
||||
|
||||
Pass ``db`` to allow reading the admin-configured base URL from
|
||||
``system_configs`` (needed for EU Tempo workspaces).
|
||||
|
||||
Raises ``InvalidOperationError`` when the user has no token or the
|
||||
client library is not installed.
|
||||
"""
|
||||
@@ -54,7 +86,9 @@ def get_user_tempo_client(user):
|
||||
)
|
||||
try:
|
||||
from tempoapiclient import client_v4 as tempo_client
|
||||
return tempo_client.Tempo(auth_token=token)
|
||||
base_url = _get_tempo_base_url(db)
|
||||
logger.debug("Using Tempo base URL: %s", base_url)
|
||||
return tempo_client.Tempo(auth_token=token, base_url=base_url)
|
||||
except ImportError:
|
||||
raise InvalidOperationError(
|
||||
"tempo-api-python-client is not installed. "
|
||||
@@ -69,13 +103,14 @@ def log_worklog(
|
||||
date: str,
|
||||
time_spent_seconds: int,
|
||||
description: str,
|
||||
db=None,
|
||||
) -> dict:
|
||||
"""Create a worklog entry in Tempo using *user*'s personal token.
|
||||
|
||||
Note: tempoapiclient raises SystemExit (not Exception) on API errors, so
|
||||
we intercept BaseException and re-raise as RuntimeError to keep it non-fatal.
|
||||
"""
|
||||
tempo = get_user_tempo_client(user)
|
||||
tempo = get_user_tempo_client(user, db=db)
|
||||
try:
|
||||
return tempo.create_worklog(
|
||||
accountId=author_account_id,
|
||||
@@ -151,7 +186,7 @@ def auto_log_test_worklog(
|
||||
logger.debug("No Jira link for test %s, skipping Tempo worklog", test.id)
|
||||
return None
|
||||
|
||||
jira_account_id = getattr(user, "jira_account_id", "") or ""
|
||||
jira_account_id = (getattr(user, "jira_account_id", "") or "").strip()
|
||||
if not jira_account_id:
|
||||
logger.debug(
|
||||
"User %s has no jira_account_id; skipping Tempo worklog",
|
||||
@@ -181,6 +216,7 @@ def auto_log_test_worklog(
|
||||
date=work_date,
|
||||
time_spent_seconds=duration_seconds,
|
||||
description=description,
|
||||
db=db,
|
||||
)
|
||||
logger.info(
|
||||
"Tempo worklog created for test %s by user %s: %ds on %s",
|
||||
|
||||
@@ -368,7 +368,11 @@ def _create_phase_worklog(
|
||||
# duration so the Tempo entry is identical to the Aegis worklog.
|
||||
try:
|
||||
from app.services.tempo_service import auto_log_test_worklog
|
||||
auto_log_test_worklog(db, test, user, activity_type, duration_seconds)
|
||||
tempo_result = auto_log_test_worklog(db, test, user, activity_type, duration_seconds)
|
||||
if tempo_result and isinstance(tempo_result, dict):
|
||||
wl.tempo_synced = datetime.utcnow()
|
||||
wl.tempo_worklog_id = str(tempo_result.get("tempoWorklogId", ""))
|
||||
db.flush()
|
||||
except Exception as e:
|
||||
logger.warning("Tempo sync failed for worklog: %s", e, exc_info=True)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user