From fe45ffa500fc78a6429dd1191f7524099d0e9b7b Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Fri, 7 Aug 2026 02:09:10 -0700 Subject: [PATCH 1/2] fix(appinsights): propagate trace into try iframe telemetry --- src/Microsoft.TryDotNet/ContentGenerator.cs | 3 + src/microsoft-trydotnet-editor/src/factory.ts | 1 + src/microsoft-trydotnet-editor/src/index.ts | 167 +++++++++++++++++- 3 files changed, 170 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.TryDotNet/ContentGenerator.cs b/src/Microsoft.TryDotNet/ContentGenerator.cs index 45f36c4cf..3e5873005 100644 --- a/src/Microsoft.TryDotNet/ContentGenerator.cs +++ b/src/Microsoft.TryDotNet/ContentGenerator.cs @@ -36,12 +36,15 @@ public static Task GenerateEditorPageAsync(HttpRequest request) correlationContext = correlationContextQueryValue.FirstOrDefault(); } + string? applicationInsightsConnectionString = Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING"); + var configuration = new { wasmRunnerUrl = wasmRunnerUri.AbsoluteUri, commandsUrl = commandsUri.AbsoluteUri, refererUrl = !string.IsNullOrWhiteSpace(referer) ? new Uri(referer, UriKind.Absolute) : null, correlationContext, + applicationInsightsConnectionString, enableLogging }; diff --git a/src/microsoft-trydotnet-editor/src/factory.ts b/src/microsoft-trydotnet-editor/src/factory.ts index 620f0b139..06c8ef582 100644 --- a/src/microsoft-trydotnet-editor/src/factory.ts +++ b/src/microsoft-trydotnet-editor/src/factory.ts @@ -161,5 +161,6 @@ export interface IConfiguration { refererUrl: string, commandsUrl: string, correlationContext?: string, + applicationInsightsConnectionString?: string, enableLogging: boolean } diff --git a/src/microsoft-trydotnet-editor/src/index.ts b/src/microsoft-trydotnet-editor/src/index.ts index f55021e44..9d9c5f84d 100644 --- a/src/microsoft-trydotnet-editor/src/index.ts +++ b/src/microsoft-trydotnet-editor/src/index.ts @@ -11,8 +11,9 @@ import * as apiService from './apiService'; import * as polyglotNotebooks from '@microsoft/polyglot-notebooks'; import { configureLogging } from './log'; -if (window) { +const sdkUrl = "https://js.monitor.azure.com/scripts/b/ai.3.gbl.min.js"; +if (window) { const settings: TryDotNetEditorSettings = { editorId: "-0-" }; @@ -22,6 +23,7 @@ if (window) { console.log(`[trydotnet-editor] configuration: ${JSON.stringify(configuration)}`); configureLogging({ enableLogging: configuration.enableLogging }); + void initializeAppInsights(configuration); const frame = window?.frameElement as HTMLIFrameElement; if (frame) { @@ -85,6 +87,169 @@ if (window) { }); } +async function initializeAppInsights(configuration: factory.IConfiguration): Promise { + const connectionString = configuration.applicationInsightsConnectionString?.trim(); + const traceId = normalizeTraceId(configuration.correlationContext); + + if (!connectionString || !traceId) { + return; + } + + await ensureAppInsightsSdkLoaded(); + + const ApplicationInsights = getApplicationInsightsConstructor(); + if (!ApplicationInsights) { + return; + } + + const appInsights = new ApplicationInsights({ + config: { + connectionString, + disableAjaxTracking: false, + disableFetchTracking: false, + distributedTracingMode: 2, + enableCorsCorrelation: true + } + }); + + appInsights.addTelemetryInitializer((item) => { + item.tags = item.tags || []; + + if (item.ext?.trace) { + item.ext.trace.traceID = traceId; + item.ext.trace.parentID = item.ext.trace.parentID || createSpanId(); + } else { + item.ext = item.ext || {}; + item.ext.trace = { + traceID: traceId, + parentID: createSpanId() + }; + } + }); + + appInsights.loadAppInsights(); + appInsights.trackPageView({ name: "TryDotNet Editor" }); + instrumentRunRequests(appInsights, traceId); +} + +function normalizeTraceId(value?: string): string | null { + if (!value) { + return null; + } + + const traceParentMatch = value.match(/^00-([a-fA-F0-9]{32})-[a-fA-F0-9]{16}-[a-fA-F0-9]{2}$/); + if (traceParentMatch) { + return traceParentMatch[1].toLowerCase(); + } + + if (/^[a-fA-F0-9]{32}$/.test(value)) { + return value.toLowerCase(); + } + + return null; +} + +function createSpanId(): string { + const randomValues = new Uint8Array(8); + crypto.getRandomValues(randomValues); + return Array.from(randomValues, b => b.toString(16).padStart(2, '0')).join(''); +} + +function ensureAppInsightsSdkLoaded(): Promise { + if (getApplicationInsightsConstructor()) { + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + const existing = document.querySelector(`script[src="${sdkUrl}"]`) as HTMLScriptElement | null; + if (existing) { + existing.addEventListener('load', () => resolve(), { once: true }); + existing.addEventListener('error', () => reject(new Error('Failed to load App Insights SDK.')), { once: true }); + return; + } + + const script = document.createElement('script'); + script.src = sdkUrl; + script.async = true; + script.defer = true; + script.addEventListener('load', () => resolve(), { once: true }); + script.addEventListener('error', () => reject(new Error('Failed to load App Insights SDK.')), { once: true }); + document.head.appendChild(script); + }); +} + +function getApplicationInsightsConstructor(): + | (new (options: { config: Record }) => { + addTelemetryInitializer: (initializer: (item: any) => void) => void; + loadAppInsights: () => void; + trackPageView: (pageView: { name: string }) => void; + trackDependencyData: (dependency: { + id: string; + absoluteUrl: string; + method: string; + responseCode: number; + success: boolean; + duration: number; + }) => void; + }) + | null { + const candidate = (window as typeof window & { + Microsoft?: { + ApplicationInsights?: { + ApplicationInsights?: new (options: { config: Record }) => any; + }; + }; + }).Microsoft?.ApplicationInsights?.ApplicationInsights; + + return candidate ?? null; +} + +function instrumentRunRequests(appInsights: any, traceId: string): void { + const originalFetch = window.fetch.bind(window); + + window.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { + const url = typeof input === 'string' + ? input + : input instanceof URL + ? input.toString() + : input.url; + const method = (init?.method || (typeof input !== 'string' && !(input instanceof URL) ? input.method : 'GET') || 'GET').toUpperCase(); + + if (!url.includes('/commands')) { + return originalFetch(input as any, init); + } + + const dependencyId = `|${traceId}.${createSpanId()}.`; + const startTime = performance.now(); + + try { + const response = await originalFetch(input as any, init); + + appInsights.trackDependencyData({ + id: dependencyId, + absoluteUrl: url, + method, + responseCode: response.status, + success: response.ok, + duration: performance.now() - startTime + }); + + return response; + } catch (error) { + appInsights.trackDependencyData({ + id: dependencyId, + absoluteUrl: url, + method, + responseCode: 0, + success: false, + duration: performance.now() - startTime + }); + + throw error; + } + }; +} + interface TryDotNetEditorSettings { editorId: string; }; From b4d7025f47e9b04df8d6016f339b4db4e90a50f6 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Sat, 8 Aug 2026 23:33:28 -0700 Subject: [PATCH 2/2] fix(appinsights): remove connection string from browser config Connection strings contain ingest credentials and must never be exposed to untrusted clients. Removed connection string from iframe bootstrap config to prevent data pollution and cost overruns. Correlation still works via W3C traceparent headers: browser sends the traceparent header in API requests, backend receives it and instruments its own traces under the same operation ID. No credentials needed in browser. --- src/Microsoft.TryDotNet/ContentGenerator.cs | 3 - src/microsoft-trydotnet-editor/src/factory.ts | 1 - src/microsoft-trydotnet-editor/src/index.ts | 166 ------------------ 3 files changed, 170 deletions(-) diff --git a/src/Microsoft.TryDotNet/ContentGenerator.cs b/src/Microsoft.TryDotNet/ContentGenerator.cs index 3e5873005..45f36c4cf 100644 --- a/src/Microsoft.TryDotNet/ContentGenerator.cs +++ b/src/Microsoft.TryDotNet/ContentGenerator.cs @@ -36,15 +36,12 @@ public static Task GenerateEditorPageAsync(HttpRequest request) correlationContext = correlationContextQueryValue.FirstOrDefault(); } - string? applicationInsightsConnectionString = Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING"); - var configuration = new { wasmRunnerUrl = wasmRunnerUri.AbsoluteUri, commandsUrl = commandsUri.AbsoluteUri, refererUrl = !string.IsNullOrWhiteSpace(referer) ? new Uri(referer, UriKind.Absolute) : null, correlationContext, - applicationInsightsConnectionString, enableLogging }; diff --git a/src/microsoft-trydotnet-editor/src/factory.ts b/src/microsoft-trydotnet-editor/src/factory.ts index 06c8ef582..620f0b139 100644 --- a/src/microsoft-trydotnet-editor/src/factory.ts +++ b/src/microsoft-trydotnet-editor/src/factory.ts @@ -161,6 +161,5 @@ export interface IConfiguration { refererUrl: string, commandsUrl: string, correlationContext?: string, - applicationInsightsConnectionString?: string, enableLogging: boolean } diff --git a/src/microsoft-trydotnet-editor/src/index.ts b/src/microsoft-trydotnet-editor/src/index.ts index 9d9c5f84d..56eb4d3b8 100644 --- a/src/microsoft-trydotnet-editor/src/index.ts +++ b/src/microsoft-trydotnet-editor/src/index.ts @@ -11,8 +11,6 @@ import * as apiService from './apiService'; import * as polyglotNotebooks from '@microsoft/polyglot-notebooks'; import { configureLogging } from './log'; -const sdkUrl = "https://js.monitor.azure.com/scripts/b/ai.3.gbl.min.js"; - if (window) { const settings: TryDotNetEditorSettings = { editorId: "-0-" @@ -23,7 +21,6 @@ if (window) { console.log(`[trydotnet-editor] configuration: ${JSON.stringify(configuration)}`); configureLogging({ enableLogging: configuration.enableLogging }); - void initializeAppInsights(configuration); const frame = window?.frameElement as HTMLIFrameElement; if (frame) { @@ -87,169 +84,6 @@ if (window) { }); } -async function initializeAppInsights(configuration: factory.IConfiguration): Promise { - const connectionString = configuration.applicationInsightsConnectionString?.trim(); - const traceId = normalizeTraceId(configuration.correlationContext); - - if (!connectionString || !traceId) { - return; - } - - await ensureAppInsightsSdkLoaded(); - - const ApplicationInsights = getApplicationInsightsConstructor(); - if (!ApplicationInsights) { - return; - } - - const appInsights = new ApplicationInsights({ - config: { - connectionString, - disableAjaxTracking: false, - disableFetchTracking: false, - distributedTracingMode: 2, - enableCorsCorrelation: true - } - }); - - appInsights.addTelemetryInitializer((item) => { - item.tags = item.tags || []; - - if (item.ext?.trace) { - item.ext.trace.traceID = traceId; - item.ext.trace.parentID = item.ext.trace.parentID || createSpanId(); - } else { - item.ext = item.ext || {}; - item.ext.trace = { - traceID: traceId, - parentID: createSpanId() - }; - } - }); - - appInsights.loadAppInsights(); - appInsights.trackPageView({ name: "TryDotNet Editor" }); - instrumentRunRequests(appInsights, traceId); -} - -function normalizeTraceId(value?: string): string | null { - if (!value) { - return null; - } - - const traceParentMatch = value.match(/^00-([a-fA-F0-9]{32})-[a-fA-F0-9]{16}-[a-fA-F0-9]{2}$/); - if (traceParentMatch) { - return traceParentMatch[1].toLowerCase(); - } - - if (/^[a-fA-F0-9]{32}$/.test(value)) { - return value.toLowerCase(); - } - - return null; -} - -function createSpanId(): string { - const randomValues = new Uint8Array(8); - crypto.getRandomValues(randomValues); - return Array.from(randomValues, b => b.toString(16).padStart(2, '0')).join(''); -} - -function ensureAppInsightsSdkLoaded(): Promise { - if (getApplicationInsightsConstructor()) { - return Promise.resolve(); - } - - return new Promise((resolve, reject) => { - const existing = document.querySelector(`script[src="${sdkUrl}"]`) as HTMLScriptElement | null; - if (existing) { - existing.addEventListener('load', () => resolve(), { once: true }); - existing.addEventListener('error', () => reject(new Error('Failed to load App Insights SDK.')), { once: true }); - return; - } - - const script = document.createElement('script'); - script.src = sdkUrl; - script.async = true; - script.defer = true; - script.addEventListener('load', () => resolve(), { once: true }); - script.addEventListener('error', () => reject(new Error('Failed to load App Insights SDK.')), { once: true }); - document.head.appendChild(script); - }); -} - -function getApplicationInsightsConstructor(): - | (new (options: { config: Record }) => { - addTelemetryInitializer: (initializer: (item: any) => void) => void; - loadAppInsights: () => void; - trackPageView: (pageView: { name: string }) => void; - trackDependencyData: (dependency: { - id: string; - absoluteUrl: string; - method: string; - responseCode: number; - success: boolean; - duration: number; - }) => void; - }) - | null { - const candidate = (window as typeof window & { - Microsoft?: { - ApplicationInsights?: { - ApplicationInsights?: new (options: { config: Record }) => any; - }; - }; - }).Microsoft?.ApplicationInsights?.ApplicationInsights; - - return candidate ?? null; -} - -function instrumentRunRequests(appInsights: any, traceId: string): void { - const originalFetch = window.fetch.bind(window); - - window.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { - const url = typeof input === 'string' - ? input - : input instanceof URL - ? input.toString() - : input.url; - const method = (init?.method || (typeof input !== 'string' && !(input instanceof URL) ? input.method : 'GET') || 'GET').toUpperCase(); - - if (!url.includes('/commands')) { - return originalFetch(input as any, init); - } - - const dependencyId = `|${traceId}.${createSpanId()}.`; - const startTime = performance.now(); - - try { - const response = await originalFetch(input as any, init); - - appInsights.trackDependencyData({ - id: dependencyId, - absoluteUrl: url, - method, - responseCode: response.status, - success: response.ok, - duration: performance.now() - startTime - }); - - return response; - } catch (error) { - appInsights.trackDependencyData({ - id: dependencyId, - absoluteUrl: url, - method, - responseCode: 0, - success: false, - duration: performance.now() - startTime - }); - - throw error; - } - }; -} - interface TryDotNetEditorSettings { editorId: string; };