From f416011abc906cdc977535edf3979d4dcf017b93 Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Thu, 7 May 2026 14:29:22 -0700 Subject: [PATCH 1/4] add product context fallback --- .../src/utils/TurnContextUtils.ts | 29 ++++++++++++- .../hosting/baggage-middleware.test.ts | 43 +++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts b/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts index 7781095b..2d183fee 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 + if (!subChannel && turnContext.activity?.channelData) { + try { + const channelData = turnContext.activity.channelData; + let channelDataObj: any; + + // Convert channelData to object if it's a string + if (typeof channelData === 'string') { + channelDataObj = JSON.parse(channelData); + } else if (typeof channelData === 'object') { + channelDataObj = channelData; + } + + // 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..dca9e206 100644 --- a/tests/observability/extension/hosting/baggage-middleware.test.ts +++ b/tests/observability/extension/hosting/baggage-middleware.test.ts @@ -140,4 +140,47 @@ 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'); + }); }); From 65204f964a77e1bf19ebedf3d95990d9b8cd1a7d Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Thu, 7 May 2026 16:06:11 -0700 Subject: [PATCH 2/4] add tests --- .../hosting/baggage-middleware.test.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/observability/extension/hosting/baggage-middleware.test.ts b/tests/observability/extension/hosting/baggage-middleware.test.ts index dca9e206..89c1f5f8 100644 --- a/tests/observability/extension/hosting/baggage-middleware.test.ts +++ b/tests/observability/extension/hosting/baggage-middleware.test.ts @@ -183,4 +183,47 @@ describe('BaggageMiddleware', () => { // 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 handle invalid JSON channelData gracefully without setting baggage', 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; + } + }); + + // Should not set ChannelLink, should fail gracefully + expect(capturedChannelLink).toBeUndefined(); + }); }); From f467870d5ad6cbc81e304ded4acf6a398125e579 Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Thu, 7 May 2026 17:02:43 -0700 Subject: [PATCH 3/4] fix: replace 'any' type with 'Record' to satisfy ESLint --- .../src/utils/TurnContextUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts b/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts index 2d183fee..ddc56d44 100644 --- a/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts +++ b/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts @@ -107,13 +107,13 @@ export function getChannelBaggagePairs(turnContext: TurnContext): Array<[string, if (!subChannel && turnContext.activity?.channelData) { try { const channelData = turnContext.activity.channelData; - let channelDataObj: any; + 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; + channelDataObj = channelData as Record; } // Extract productContext if available From 6c1a80e0f0fb81a94f54ed685e71cd2509fc0b2d Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Thu, 7 May 2026 17:16:12 -0700 Subject: [PATCH 4/4] fix: address PR review comments - Handle empty/whitespace channelIdSubChannel by checking trim() - Remove trailing whitespace from test code - Rename test to be more specific about what is not set --- .../src/utils/TurnContextUtils.ts | 4 ++-- .../hosting/baggage-middleware.test.ts | 20 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts b/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts index ddc56d44..88dcc36b 100644 --- a/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts +++ b/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts @@ -103,8 +103,8 @@ export function getChannelBaggagePairs(turnContext: TurnContext): Array<[string, let subChannel = turnContext.activity?.channelIdSubChannel as string | undefined; - // Try to get subChannel from productContext in channelData if subChannel is not set - if (!subChannel && turnContext.activity?.channelData) { + // 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; diff --git a/tests/observability/extension/hosting/baggage-middleware.test.ts b/tests/observability/extension/hosting/baggage-middleware.test.ts index 89c1f5f8..f1428e79 100644 --- a/tests/observability/extension/hosting/baggage-middleware.test.ts +++ b/tests/observability/extension/hosting/baggage-middleware.test.ts @@ -144,11 +144,11 @@ describe('BaggageMiddleware', () => { 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 () => { @@ -165,11 +165,11 @@ describe('BaggageMiddleware', () => { 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 () => { @@ -187,11 +187,11 @@ describe('BaggageMiddleware', () => { 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 () => { @@ -205,14 +205,14 @@ describe('BaggageMiddleware', () => { expect(capturedChannelLink).toBe('COPILOT'); }); - it('should handle invalid JSON channelData gracefully without setting baggage', async () => { + 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 () => { @@ -223,7 +223,7 @@ describe('BaggageMiddleware', () => { } }); - // Should not set ChannelLink, should fail gracefully + // Channel link should not be set when JSON parsing fails expect(capturedChannelLink).toBeUndefined(); }); });