From d91e646069524609fc0691be257858331c5bce8f Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Fri, 7 Aug 2026 00:37:23 -0700 Subject: [PATCH 1/2] fix(tracing): correlate browser and server app insights --- .../wwwroot/js/appinsights-manager.js | 30 ++++++++++++++++++- EssentialCSharp.Web/wwwroot/js/chat-module.js | 12 ++++++++ .../wwwroot/js/trydotnet-module.js | 14 ++++++++- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/EssentialCSharp.Web/wwwroot/js/appinsights-manager.js b/EssentialCSharp.Web/wwwroot/js/appinsights-manager.js index 6b9846b8..f92501a2 100644 --- a/EssentialCSharp.Web/wwwroot/js/appinsights-manager.js +++ b/EssentialCSharp.Web/wwwroot/js/appinsights-manager.js @@ -5,6 +5,11 @@ (function () { const SDK_URL = "https://js.monitor.azure.com/scripts/b/ai.3.gbl.min.js"; const CONSENT_EVENT = "ecs:consent-changed"; + const DistributedTracingModes = { + AI: 0, + AI_AND_W3C: 1, + W3C: 2 + }; let appInsights = null; let sdkLoadPromise = null; @@ -15,6 +20,25 @@ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; } + function getTryDotNetOrigin() { + const value = window.TRYDOTNET_ORIGIN; + return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; + } + + function getCorrelationHeaderDomains() { + const domains = [window.location.hostname]; + const tryDotNetOrigin = getTryDotNetOrigin(); + if (tryDotNetOrigin) { + try { + domains.push(new URL(tryDotNetOrigin).hostname); + } catch (error) { + console.warn("Ignoring invalid TryDotNet origin for App Insights correlation:", error); + } + } + + return Array.from(new Set(domains.filter(Boolean))); + } + function hasAnalyticsConsent() { if (window.consentManager && typeof window.consentManager.hasAnalyticsConsent === "function") { return window.consentManager.hasAnalyticsConsent(); @@ -113,7 +137,11 @@ const instance = new window.Microsoft.ApplicationInsights.ApplicationInsights({ config: { connectionString, - disableAjaxTracking: true, // avoid duplicate/debatable dependency telemetry from browser fetch/XHR + disableAjaxTracking: false, + disableFetchTracking: false, + distributedTracingMode: DistributedTracingModes.W3C, + correlationHeaderDomains: getCorrelationHeaderDomains(), + enableCorsCorrelation: true, disableTelemetry: false } }); diff --git a/EssentialCSharp.Web/wwwroot/js/chat-module.js b/EssentialCSharp.Web/wwwroot/js/chat-module.js index fae834df..a5d05086 100644 --- a/EssentialCSharp.Web/wwwroot/js/chat-module.js +++ b/EssentialCSharp.Web/wwwroot/js/chat-module.js @@ -12,6 +12,17 @@ const errorIconClassByType = { 'connection-error': 'fas fa-plug' }; +function getTraceHeaders() { + const headers = {}; + if (typeof window.ecsGetCorrelationContext === 'function') { + const traceparent = window.ecsGetCorrelationContext(); + if (typeof traceparent === 'string' && traceparent.length > 0) { + headers.traceparent = traceparent; + } + } + return headers; +} + // hCaptcha integration — invisible widget renders once and is reused across messages. // When HCAPTCHA_SITE_KEY is null (dev / captcha not configured), all captcha calls are no-ops. const captchaSiteKey = window.HCAPTCHA_SITE_KEY || null; @@ -338,6 +349,7 @@ export function useChatWidget() { method: 'POST', headers: { 'Content-Type': 'application/json', + ...getTraceHeaders() }, body: JSON.stringify(requestBody) }); diff --git a/EssentialCSharp.Web/wwwroot/js/trydotnet-module.js b/EssentialCSharp.Web/wwwroot/js/trydotnet-module.js index ddff070e..336e6a1e 100644 --- a/EssentialCSharp.Web/wwwroot/js/trydotnet-module.js +++ b/EssentialCSharp.Web/wwwroot/js/trydotnet-module.js @@ -31,6 +31,15 @@ function getCorrelationContext() { return null; } +function getTraceHeaders() { + const headers = {}; + const correlationContext = getCorrelationContext(); + if (typeof correlationContext === 'string' && correlationContext.length > 0) { + headers.traceparent = correlationContext; + } + return headers; +} + function trackTryEvent(name, properties = {}, measurements = {}) { const appInsights = getAppInsights(); if (!appInsights || typeof appInsights.trackEvent !== 'function') { @@ -244,6 +253,7 @@ export function useTryDotNet() { const res = await fetch(`${origin}/api/trydotnet.min.js`, { method: 'HEAD', mode: 'no-cors', + headers: getTraceHeaders(), signal: controller.signal, }); // mode: 'no-cors' gives an opaque response (status 0), which is fine @@ -489,7 +499,9 @@ export function useTryDotNet() { * @returns {Promise} The listing source code (extracted snippet) */ async function fetchListingCode(chapter, listing) { - const response = await fetch(`/api/ListingSourceCode/chapter/${chapter}/listing/${listing}`); + const response = await fetch(`/api/ListingSourceCode/chapter/${chapter}/listing/${listing}`, { + headers: getTraceHeaders() + }); if (!response.ok) { throw new Error(ERROR_MESSAGES.fetchFailed); } From a3d3b5f3782ad9275885c6ef6e26da6d021e2f98 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Fri, 7 Aug 2026 00:55:49 -0700 Subject: [PATCH 2/2] fix(tracing): remove no-cors health check trace header Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- EssentialCSharp.Web/wwwroot/js/trydotnet-module.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/EssentialCSharp.Web/wwwroot/js/trydotnet-module.js b/EssentialCSharp.Web/wwwroot/js/trydotnet-module.js index 336e6a1e..d99a4b4e 100644 --- a/EssentialCSharp.Web/wwwroot/js/trydotnet-module.js +++ b/EssentialCSharp.Web/wwwroot/js/trydotnet-module.js @@ -250,10 +250,9 @@ export function useTryDotNet() { try { // Check the actual script endpoint rather than the bare origin, // which may not have a handler and would return 404. - const res = await fetch(`${origin}/api/trydotnet.min.js`, { + await fetch(`${origin}/api/trydotnet.min.js`, { method: 'HEAD', mode: 'no-cors', - headers: getTraceHeaders(), signal: controller.signal, }); // mode: 'no-cors' gives an opaque response (status 0), which is fine