diff --git a/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts b/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts index 7781095b..88dcc36b 100644 --- a/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts +++ b/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts @@ -99,10 +99,35 @@ export function getTenantIdPair(turnContext: TurnContext): Array<[string, string export function getChannelBaggagePairs(turnContext: TurnContext): Array<[string, string]> { if (!turnContext) { return []; - } + } + + let subChannel = turnContext.activity?.channelIdSubChannel as string | undefined; + + // Try to get subChannel from productContext in channelData if subChannel is not set or empty + if ((!subChannel || subChannel.trim() === '') && turnContext.activity?.channelData) { + try { + const channelData = turnContext.activity.channelData; + let channelDataObj: Record | undefined; + + // Convert channelData to object if it's a string + if (typeof channelData === 'string') { + channelDataObj = JSON.parse(channelData); + } else if (typeof channelData === 'object') { + channelDataObj = channelData as Record; + } + + // Extract productContext if available + if (channelDataObj && typeof channelDataObj.productContext === 'string') { + subChannel = channelDataObj.productContext; + } + } catch { + // Silently ignore any parsing errors + } + } + const pairs: Array<[string, string | undefined]> = [ [OpenTelemetryConstants.CHANNEL_NAME_KEY, turnContext.activity?.channelId], - [OpenTelemetryConstants.CHANNEL_LINK_KEY, turnContext.activity?.channelIdSubChannel as string | undefined] + [OpenTelemetryConstants.CHANNEL_LINK_KEY, subChannel] ]; return normalizePairs(pairs); } diff --git a/tests/observability/extension/hosting/baggage-middleware.test.ts b/tests/observability/extension/hosting/baggage-middleware.test.ts index 4a1fd00c..f1428e79 100644 --- a/tests/observability/extension/hosting/baggage-middleware.test.ts +++ b/tests/observability/extension/hosting/baggage-middleware.test.ts @@ -140,4 +140,90 @@ describe('BaggageMiddleware', () => { expect(nextCalled).toBe(true); }); + + it('should extract productContext from channelData when channelIdSubChannel is not set', async () => { + const middleware = new BaggageMiddleware(); + const ctx: any = makeMockTurnContext({ channelId: 'msteams' }); + + // Add channelData with productContext, no channelIdSubChannel + ctx.activity.channelData = { productContext: 'COPILOT' }; + ctx.activity.channelIdSubChannel = undefined; + + let capturedChannelLink: string | undefined; + + await middleware.onTurn(ctx, async () => { + const bag = propagation.getBaggage(otelContext.active()); + if (bag) { + const entry = bag.getEntry(OpenTelemetryConstants.CHANNEL_LINK_KEY); + capturedChannelLink = entry?.value; + } + }); + + expect(capturedChannelLink).toBe('COPILOT'); + }); + + it('should use channelIdSubChannel when both channelIdSubChannel and productContext are present', async () => { + const middleware = new BaggageMiddleware(); + const ctx: any = makeMockTurnContext({ channelId: 'msteams' }); + + // Set BOTH channelIdSubChannel and productContext in channelData + ctx.activity.channelIdSubChannel = 'teams-subchannel'; + ctx.activity.channelData = { productContext: 'COPILOT' }; + + let capturedChannelLink: string | undefined; + + await middleware.onTurn(ctx, async () => { + const bag = propagation.getBaggage(otelContext.active()); + if (bag) { + const entry = bag.getEntry(OpenTelemetryConstants.CHANNEL_LINK_KEY); + capturedChannelLink = entry?.value; + } + }); + + // channelIdSubChannel should take precedence, productContext should be ignored + expect(capturedChannelLink).toBe('teams-subchannel'); + }); + + it('should extract productContext from channelData when it is a JSON string', async () => { + const middleware = new BaggageMiddleware(); + const ctx: any = makeMockTurnContext({ channelId: 'msteams' }); + + // Set channelData as a JSON string (simulating wire format) + ctx.activity.channelIdSubChannel = undefined; + ctx.activity.channelData = JSON.stringify({ productContext: 'COPILOT' }); + + let capturedChannelLink: string | undefined; + + await middleware.onTurn(ctx, async () => { + const bag = propagation.getBaggage(otelContext.active()); + if (bag) { + const entry = bag.getEntry(OpenTelemetryConstants.CHANNEL_LINK_KEY); + capturedChannelLink = entry?.value; + } + }); + + expect(capturedChannelLink).toBe('COPILOT'); + }); + + it('should not set channel link when channelData is invalid JSON', async () => { + const middleware = new BaggageMiddleware(); + const ctx: any = makeMockTurnContext({ channelId: 'msteams' }); + + // Set channelData as an invalid JSON string + ctx.activity.channelIdSubChannel = undefined; + ctx.activity.channelData = 'not valid json'; + + let capturedChannelLink: string | undefined; + + await middleware.onTurn(ctx, async () => { + const bag = propagation.getBaggage(otelContext.active()); + if (bag) { + const entry = bag.getEntry(OpenTelemetryConstants.CHANNEL_LINK_KEY); + capturedChannelLink = entry?.value; + } + }); + + // Channel link should not be set when JSON parsing fails + expect(capturedChannelLink).toBeUndefined(); + }); });