(req: NextRequest)
| 697 | } |
| 698 | |
| 699 | export async function handleUnifiedChatPost(req: NextRequest) { |
| 700 | let actualChatId: string | undefined |
| 701 | let userMessageId = '' |
| 702 | let chatStreamLockAcquired = false |
| 703 | // Started once we've parsed the body (need userMessageId to stamp as |
| 704 | // streamId). Every subsequent span (persistUserMessage, |
| 705 | // createRunSegment, the whole SSE stream, etc.) nests under this |
| 706 | // root via AsyncLocalStorage / explicit propagation, and the stream's |
| 707 | // terminal code path calls finish() when the request actually ends. |
| 708 | // Errors thrown from the handler before the stream starts are |
| 709 | // finished here in the catch below. |
| 710 | let otelRoot: ReturnType<typeof startCopilotOtelRoot> | undefined |
| 711 | // Canonical logical ID; assigned from otelRoot.requestId (the OTel |
| 712 | // trace ID) as soon as startCopilotOtelRoot runs. Empty only in the |
| 713 | // narrow pre-otelRoot window where errors don't correlate anyway. |
| 714 | let requestId = '' |
| 715 | const executionId = generateId() |
| 716 | const runId = generateId() |
| 717 | |
| 718 | try { |
| 719 | const session = await getSession() |
| 720 | if (!session?.user?.id) { |
| 721 | return createUnauthorizedResponse() |
| 722 | } |
| 723 | const authenticatedUserId = session.user.id |
| 724 | const authenticatedUserEmail = session.user.email |
| 725 | const authenticatedUserName = |
| 726 | typeof session.user.name === 'string' ? session.user.name : undefined |
| 727 | |
| 728 | const body = ChatMessageSchema.parse(await req.json()) |
| 729 | const userMetadata = { |
| 730 | ...(authenticatedUserName ? { name: authenticatedUserName } : {}), |
| 731 | ...(authenticatedUserEmail ? { email: authenticatedUserEmail } : {}), |
| 732 | ...(body.userTimezone ? { timezone: body.userTimezone } : {}), |
| 733 | } |
| 734 | const normalizedContexts = normalizeContexts(body.contexts) ?? [] |
| 735 | userMessageId = body.userMessageId || generateId() |
| 736 | |
| 737 | otelRoot = startCopilotOtelRoot({ |
| 738 | streamId: userMessageId, |
| 739 | executionId, |
| 740 | runId, |
| 741 | transport: CopilotTransport.Stream, |
| 742 | userMessagePreview: body.message, |
| 743 | }) |
| 744 | if (otelRoot.requestId) { |
| 745 | requestId = otelRoot.requestId |
| 746 | } |
| 747 | // Identity stamp — Go already stamps `user.id` on spans from the |
| 748 | // validated API-key path, but Sim is the only side of the wire |
| 749 | // that knows the human-facing email. Stamping both on the Sim |
| 750 | // root (so they show up on `rootAttrs` in Tempo search) saves |
| 751 | // the "turn user.id into a real person" round-trip to the DB |
| 752 | // for every ad-hoc investigation. |
| 753 | otelRoot.span.setAttribute(TraceAttr.UserId, authenticatedUserId) |
| 754 | if (authenticatedUserEmail) { |
| 755 | otelRoot.span.setAttribute(TraceAttr.UserEmail, authenticatedUserEmail) |
| 756 | } |
no test coverage detected