mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-08 20:30:59 +00:00
feat: report independent instance and workspace identities (#2394)
* feat: report independent instance and workspace identities * test: include workspace in OAuth callback fixture * ci: pin production cloud adapter to Space release * fix: preserve authenticated Workspace telemetry attribution * ci: pin production cloud adapter to final Space release --------- Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>
This commit is contained in:
@@ -120,6 +120,7 @@ async def build_heartbeat_payload(
|
||||
ap: core_app.Application,
|
||||
*,
|
||||
workspace_uuid: str,
|
||||
workspace_create_ts: int = 0,
|
||||
workspace_resource: WorkspaceResourceSnapshot | None = None,
|
||||
) -> dict:
|
||||
"""Collect one anonymous Workspace profile snapshot."""
|
||||
@@ -212,7 +213,9 @@ async def build_heartbeat_payload(
|
||||
'event_type': 'instance_heartbeat',
|
||||
'query_id': '',
|
||||
'version': constants.semantic_version,
|
||||
'instance_id': constants.instance_id,
|
||||
'workspace_uuid': workspace_uuid,
|
||||
'workspace_create_ts': workspace_create_ts,
|
||||
'instance_create_ts': constants.instance_create_ts,
|
||||
'edition': constants.edition,
|
||||
'features': features,
|
||||
@@ -220,10 +223,24 @@ async def build_heartbeat_payload(
|
||||
}
|
||||
|
||||
|
||||
def _workspace_created_timestamp(created_at: datetime | None) -> int:
|
||||
if created_at is None:
|
||||
return 0
|
||||
if created_at.tzinfo is None:
|
||||
# SQLAlchemy may return persisted UTC values without tzinfo. Never
|
||||
# reinterpret them in the host's local timezone.
|
||||
created_at = created_at.replace(tzinfo=timezone.utc)
|
||||
return int(created_at.timestamp())
|
||||
|
||||
|
||||
async def build_heartbeat_payloads(ap: core_app.Application) -> list[dict]:
|
||||
"""Build one heartbeat per active Workspace."""
|
||||
bindings = await ap.workspace_service.list_active_execution_bindings()
|
||||
workspace_uuids = sorted({binding.workspace_uuid for binding in bindings})
|
||||
workspace_create_ts = {
|
||||
binding.workspace_uuid: _workspace_created_timestamp(getattr(binding, 'workspace_created_at', None))
|
||||
for binding in bindings
|
||||
}
|
||||
resources = {
|
||||
resource['workspace_uuid']: resource for resource in await _cloud_workspace_resource_counts(ap, bindings)
|
||||
}
|
||||
@@ -231,6 +248,7 @@ async def build_heartbeat_payloads(ap: core_app.Application) -> list[dict]:
|
||||
await build_heartbeat_payload(
|
||||
ap,
|
||||
workspace_uuid=workspace_uuid,
|
||||
workspace_create_ts=workspace_create_ts.get(workspace_uuid, 0),
|
||||
workspace_resource=resources.get(workspace_uuid),
|
||||
)
|
||||
for workspace_uuid in workspace_uuids
|
||||
|
||||
@@ -4,13 +4,19 @@ import typing
|
||||
|
||||
|
||||
class WorkspaceExecutionContext(typing.Protocol):
|
||||
@property
|
||||
def instance_uuid(self) -> str: ...
|
||||
|
||||
@property
|
||||
def workspace_uuid(self) -> str: ...
|
||||
|
||||
|
||||
def workspace_identity(execution_context: WorkspaceExecutionContext) -> dict[str, str]:
|
||||
"""Build the canonical telemetry identity for one Workspace execution."""
|
||||
"""Build both first-class telemetry identities for one execution."""
|
||||
instance_id = execution_context.instance_uuid.strip()
|
||||
workspace_uuid = execution_context.workspace_uuid.strip()
|
||||
if not instance_id:
|
||||
raise ValueError('Telemetry execution instance ID is empty')
|
||||
if not workspace_uuid:
|
||||
raise ValueError('Telemetry execution Workspace UUID is empty')
|
||||
return {'workspace_uuid': workspace_uuid}
|
||||
return {'instance_id': instance_id, 'workspace_uuid': workspace_uuid}
|
||||
|
||||
@@ -136,12 +136,31 @@ class TelemetryManager:
|
||||
try:
|
||||
# Use asyncio.wait_for to ensure we always bound the total time
|
||||
telemetry_token = os.getenv('LANGBOT_TELEMETRY_INGEST_TOKEN', '').strip()
|
||||
headers: dict[str, str] = {}
|
||||
if telemetry_token:
|
||||
request = client.post(
|
||||
url,
|
||||
json=sanitized,
|
||||
headers={'X-LangBot-Telemetry-Token': telemetry_token},
|
||||
)
|
||||
headers['X-LangBot-Telemetry-Token'] = telemetry_token
|
||||
else:
|
||||
workspace_uuid = str(sanitized.get('workspace_uuid', '')).strip()
|
||||
user_service = getattr(self.ap, 'user_service', None)
|
||||
if workspace_uuid and user_service is not None:
|
||||
try:
|
||||
owner = await user_service.get_workspace_owner(workspace_uuid)
|
||||
owner_email = str(getattr(owner, 'user', '') or '').strip()
|
||||
space_service = getattr(self.ap, 'space_service', None)
|
||||
access_token = (
|
||||
await space_service.get_valid_access_token(owner_email)
|
||||
if owner_email and space_service is not None
|
||||
else None
|
||||
)
|
||||
access_token = str(access_token or '').strip()
|
||||
if access_token:
|
||||
headers['Authorization'] = f'Bearer {access_token}'
|
||||
except Exception:
|
||||
self.ap.logger.debug(
|
||||
'Could not resolve authenticated telemetry reporter', exc_info=True
|
||||
)
|
||||
if headers:
|
||||
request = client.post(url, json=sanitized, headers=headers)
|
||||
else:
|
||||
request = client.post(url, json=sanitized)
|
||||
resp = await asyncio.wait_for(request, timeout=10 + 1)
|
||||
|
||||
Reference in New Issue
Block a user