feat(posthog): correlate task events with copilot logs via request_id (#4453)

* feat(posthog): correlate task events with Go logs via request_id

Auto-injects server request_id into all PostHog server events from
AsyncLocalStorage context. Adds task_request_started client event fired
when SSE traceparent response header arrives, carrying the trace ID used
by Go for log correlation. task_generation_aborted now includes the
in-flight request_id when available.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* improvement(posthog): tighten trace ID parse and verbose comments

* fix(posthog): use traceparent header as canonical request_id source

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Waleed
2026-05-05 14:11:21 -07:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 6bc34c5a68
commit a9e9ecf767
5 changed files with 58 additions and 4 deletions
@@ -143,12 +143,21 @@ export function Home({ chatId }: HomeProps = {}) {
editQueuedMessage,
previewSession,
genericResourceData,
getCurrentRequestId,
} = useChat(
workspaceId,
chatId,
getMothershipUseChatOptions({
onResourceEvent: handleResourceEvent,
initialActiveResourceId: initialResourceId,
onRequestStarted: ({ requestId, userMessageId }) => {
captureEvent(posthogRef.current, 'task_request_started', {
workspace_id: workspaceId,
view: 'mothership',
request_id: requestId,
user_message_id: userMessageId,
})
},
})
)
@@ -198,6 +207,7 @@ export function Home({ chatId }: HomeProps = {}) {
captureEvent(posthogRef.current, 'task_generation_aborted', {
workspace_id: workspaceId,
view: 'mothership',
request_id: getCurrentRequestId(),
})
void stopGeneration().catch(() => {})
}
@@ -167,6 +167,7 @@ export interface UseChatReturn {
editQueuedMessage: (id: string) => QueuedMessage | undefined
previewSession: FilePreviewSession | null
genericResourceData: GenericResourceData | null
getCurrentRequestId: () => string | undefined
}
const DEPLOY_TOOL_NAMES: Set<string> = new Set([
@@ -1278,6 +1279,8 @@ export interface UseChatOptions {
onTitleUpdate?: () => void
onStreamEnd?: (chatId: string, messages: ChatMessage[]) => void
initialActiveResourceId?: string | null
/** Fired when the server's `traceparent` response header arrives, before any stream content. */
onRequestStarted?: (info: { requestId: string; userMessageId: string }) => void
}
interface ActiveStreamRecovery {
@@ -1293,7 +1296,10 @@ interface StopGenerationOptions {
}
export function getMothershipUseChatOptions(
options: Pick<UseChatOptions, 'onResourceEvent' | 'onStreamEnd' | 'initialActiveResourceId'> = {}
options: Pick<
UseChatOptions,
'onResourceEvent' | 'onStreamEnd' | 'initialActiveResourceId' | 'onRequestStarted'
> = {}
): UseChatOptions {
return {
apiPath: MOTHERSHIP_CHAT_API_PATH,
@@ -1305,7 +1311,7 @@ export function getMothershipUseChatOptions(
export function getWorkflowCopilotUseChatOptions(
options: Pick<
UseChatOptions,
'workflowId' | 'onToolResult' | 'onTitleUpdate' | 'onStreamEnd'
'workflowId' | 'onToolResult' | 'onTitleUpdate' | 'onStreamEnd' | 'onRequestStarted'
> = {}
): UseChatOptions {
return {
@@ -1351,6 +1357,13 @@ export function useChat(
onTitleUpdateRef.current = options?.onTitleUpdate
const onStreamEndRef = useRef(options?.onStreamEnd)
onStreamEndRef.current = options?.onStreamEnd
const onRequestStartedRef = useRef(options?.onRequestStarted)
onRequestStartedRef.current = options?.onRequestStarted
const getCurrentRequestId = useCallback(() => {
const traceId = streamTraceparentRef.current?.split('-')[1] ?? ''
return /^[0-9a-f]{32}$/.test(traceId) ? traceId : undefined
}, [])
const clearQueueDispatchState = useCallback(() => {
queueDispatchEpochRef.current++
@@ -4209,6 +4222,14 @@ export function useChat(
if (traceparent) {
streamTraceparentRef.current = traceparent
setCurrentChatTraceparent(traceparent)
const traceId = traceparent.split('-')[1] ?? ''
if (/^[0-9a-f]{32}$/.test(traceId)) {
try {
onRequestStartedRef.current?.({ requestId: traceId, userMessageId })
} catch (callbackError) {
logger.warn('onRequestStarted callback threw', { error: callbackError })
}
}
}
if (!response.ok) {
@@ -5103,5 +5124,6 @@ export function useChat(
editQueuedMessage,
previewSession,
genericResourceData,
getCurrentRequestId,
}
}
@@ -346,6 +346,7 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
removeFromQueue: copilotRemoveFromQueue,
sendNow: copilotSendNow,
editQueuedMessage: copilotEditQueuedMessage,
getCurrentRequestId: getCopilotCurrentRequestId,
} = useChat(
workspaceId,
copilotChatId,
@@ -353,6 +354,14 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
workflowId: activeWorkflowId || undefined,
onTitleUpdate: loadCopilotChats,
onToolResult: handleCopilotToolResult,
onRequestStarted: ({ requestId, userMessageId }) => {
captureEvent(posthogRef.current, 'task_request_started', {
workspace_id: workspaceId,
view: 'copilot',
request_id: requestId,
user_message_id: userMessageId,
})
},
})
)
@@ -413,9 +422,10 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
captureEvent(posthogRef.current, 'task_generation_aborted', {
workspace_id: workspaceId,
view: 'copilot',
request_id: getCopilotCurrentRequestId(),
})
copilotStopGeneration()
}, [copilotStopGeneration, workspaceId])
}, [copilotStopGeneration, getCopilotCurrentRequestId, workspaceId])
const handleCopilotSubmit = useCallback(
(text: string, fileAttachments?: FileAttachmentForApi[], contexts?: ChatContext[]) => {
+9
View File
@@ -418,6 +418,7 @@ export interface PostHogEventMap {
task_generation_aborted: {
workspace_id: string
view: 'mothership' | 'copilot'
request_id?: string
}
task_message_sent: {
@@ -427,6 +428,14 @@ export interface PostHogEventMap {
is_new_task: boolean
}
/** Pairs with `task_message_sent` via `request_id` for correlation with server-side logs. */
task_request_started: {
workspace_id: string
view: 'mothership' | 'copilot'
request_id: string
user_message_id: string
}
tour_started: {
tour_type: 'nav' | 'workflow'
}
+4 -1
View File
@@ -1,4 +1,4 @@
import { createLogger } from '@sim/logger'
import { createLogger, getRequestContext } from '@sim/logger'
import type { PostHog } from 'posthog-node'
import type { PostHogEventMap, PostHogEventName } from '@/lib/posthog/events'
@@ -71,11 +71,14 @@ export function captureServerEvent<E extends PostHogEventName>(
const client = getClient()
if (!client) return
const contextRequestId = getRequestContext()?.requestId
const props = properties as Record<string, unknown>
client.capture({
distinctId,
event,
properties: {
...properties,
...(contextRequestId && !('request_id' in props) ? { request_id: contextRequestId } : {}),
...(options?.groups ? { $groups: options.groups } : {}),
...(options?.set ? { $set: options.set } : {}),
...(options?.setOnce ? { $set_once: options.setOnce } : {}),