Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Copilot / Agent Instructions

## Copyright headers

Existing source files carry the `.NET Foundation` copyright header:
```
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
```

This repo is a fork that is no longer maintained under the .NET Foundation.
**Do not add this header to new files.** Leave it on files that already have it.
9 changes: 8 additions & 1 deletion src/Microsoft.TryDotNet/ContentGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,18 @@ public static Task<string> GenerateEditorPageAsync(HttpRequest request)
enableLogging = enableLoggingString.FirstOrDefault()?.ToLowerInvariant() == "true";
}

string? correlationContext = null;
if (request.Query.TryGetValue("correlationContext", out var correlationContextQueryValue))
{
correlationContext = correlationContextQueryValue.FirstOrDefault();
}

var configuration = new
{
wasmRunnerUrl = wasmRunnerUri.AbsoluteUri,
commandsUrl = commandsUri.AbsoluteUri,
refererUrl = !string.IsNullOrWhiteSpace(referer) ? new Uri(referer, UriKind.Absolute) : null,
correlationContext,
enableLogging
};

Expand All @@ -61,4 +68,4 @@ public static Task<string> GenerateEditorPageAsync(HttpRequest request)

return Task.FromResult(value);
}
}
}
34 changes: 33 additions & 1 deletion src/microsoft-trydotnet-editor/src/apiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface IServiceError {
export interface IApiServiceConfiguration {
referer?: URL;
commandsUrl: URL;
correlationContext?: string;
onServiceError: (error: IServiceError) => void;
}

Expand All @@ -25,6 +26,7 @@ export interface IApiService {


function createApiServiceWithConfiguration(configuration: IApiServiceConfiguration): IApiService {
const traceId = normalizeTraceId(configuration.correlationContext);
let service: IApiService = async (commands) => {
let bodyContent = JSON.stringify({
commands: commands.map(command => command.toJson())
Expand All @@ -35,6 +37,9 @@ function createApiServiceWithConfiguration(configuration: IApiServiceConfigurati
if (configuration.referer) {
headers['Referer'] = configuration.referer.toString();
}
if (traceId) {
headers['traceparent'] = createTraceParent(traceId);
}

let response = await fetch(configuration.commandsUrl.toString(), {
method: 'POST',
Expand All @@ -61,4 +66,31 @@ function createApiServiceWithConfiguration(configuration: IApiServiceConfigurati
};

return service;
}
}

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 createTraceParent(traceId: string): string {
return `00-${traceId}-${createSpanId()}-01`;
}

function createSpanId(): string {
const randomValues = new Uint8Array(8);
crypto.getRandomValues(randomValues);
return Array.from(randomValues, b => b.toString(16).padStart(2, '0')).join('');
}
4 changes: 3 additions & 1 deletion src/microsoft-trydotnet-editor/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function createWasmProjectKernel(onServiceError: (serviceError: IServiceE
const apiService = createApiService({
commandsUrl: new URL(configuration.commandsUrl),
referer: configuration.refererUrl ? new URL(configuration.refererUrl) : null,
correlationContext: configuration.correlationContext,
onServiceError: onServiceError
});

Expand Down Expand Up @@ -159,5 +160,6 @@ export interface IConfiguration {
wasmRunnerUrl: string,
refererUrl: string,
commandsUrl: string,
correlationContext?: string,
enableLogging: boolean
}
}
59 changes: 59 additions & 0 deletions src/microsoft-trydotnet-editor/tests/apiService.tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { expect } from "chai";
import { describe } from "mocha";
import { createApiService } from "../src/apiService";

describe("apiService", () => {
it("adds traceparent header when correlationContext is a trace id", async () => {
let capturedHeaders: any = null;
const originalFetch = globalThis.fetch;

(globalThis as any).fetch = async (_url: string, options: any) => {
capturedHeaders = options.headers;
return {
ok: true,
json: async () => ({ events: [] })
};
};

try {
const service = createApiService({
commandsUrl: new URL("https://example.org/commands"),
correlationContext: "0123456789abcdef0123456789abcdef",
onServiceError: () => { /* no-op */ }
});

await service([{ toJson: () => ({ commandType: "SubmitCode", command: {} }) } as any]);

expect(capturedHeaders).to.have.property("traceparent");
expect(capturedHeaders.traceparent).to.match(/^00-0123456789abcdef0123456789abcdef-[a-f0-9]{16}-01$/);
} finally {
(globalThis as any).fetch = originalFetch;
}
});

it("does not add traceparent header when correlationContext is missing", async () => {
let capturedHeaders: any = null;
const originalFetch = globalThis.fetch;

(globalThis as any).fetch = async (_url: string, options: any) => {
capturedHeaders = options.headers;
return {
ok: true,
json: async () => ({ events: [] })
};
};

try {
const service = createApiService({
commandsUrl: new URL("https://example.org/commands"),
onServiceError: () => { /* no-op */ }
});

await service([{ toJson: () => ({ commandType: "SubmitCode", command: {} }) } as any]);

expect(capturedHeaders).to.not.have.property("traceparent");
} finally {
(globalThis as any).fetch = originalFetch;
}
});
});
1 change: 1 addition & 0 deletions src/microsoft-trydotnet/src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { MonacoEditorConfiguration } from "./editor";
export type Configuration = {
hostOrigin?: string,
trydotnetOrigin?: string,
correlationContext?: string,
editorConfiguration?: MonacoEditorConfiguration,
enableLogging?: boolean,
}
8 changes: 6 additions & 2 deletions src/microsoft-trydotnet/src/internals/urlHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@ export function generateEditorUrl(configuration: Configuration, packageName?: st
url.searchParams.append("enableLogging", "true");
}

buildQueryString(url, packageName);
buildQueryString(url, packageName, configuration.correlationContext);
return url.href;
}

function buildQueryString(url: URL, packageName?: string) {
function buildQueryString(url: URL, packageName?: string, correlationContext?: string) {
if (packageName) {
url.searchParams.append("workspaceType", packageName);
}

if (correlationContext) {
url.searchParams.append("correlationContext", correlationContext);
}
}

export function extractTargetOriginFromIFrame(iframe: HTMLIFrameElement): string {
Expand Down
44 changes: 44 additions & 0 deletions src/microsoft-trydotnet/test/session.creationapi.specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,50 @@ describe("a user", () => {
configuration = { hostOrigin: "https://learn.microsoft.com" };
});
describe("with single iframe", () => {
it("includes correlationContext in editor iframe url when configured", async () => {
configuration = {
hostOrigin: "https://learn.microsoft.com",
correlationContext: "0123456789abcdef0123456789abcdef"
};

const dom = buildSimpleIFrameDom(configuration);
const editorIFrame = getEditorIFrame(dom);
const project = {
package: "console",
files: [{ name: "program.cs", content: "" }]
};

const awaitableSession = createSessionWithProjectAndOpenDocument(
configuration,
[editorIFrame],
dom.window as unknown as Window,
project,
"program.cs");

const iframeUrl = new URL(editorIFrame.getAttribute("src")!);
iframeUrl.searchParams.get("correlationContext").should.equal("0123456789abcdef0123456789abcdef");

registerForOpenProject(configuration, editorIFrame, dom.window, (files) => {
return files.map(f => {
const item: polyglotNotebooks.ProjectItem = {
relativeFilePath: f.relativeFilePath,
regionNames: [],
regionsContent: {}
};
return item;
});
});

registerForOpeDocument(configuration, editorIFrame, dom.window, (documentId) => {
const content = project.files.find(f => areSameFile(f.name, documentId.relativeFilePath))?.content ?? "";
return content;
});

notifyEditorReady(configuration, dom.window);
const session = await awaitableSession;
session.should.not.be.null;
});

it("can create a session with initial project", async () => {
let dom = buildSimpleIFrameDom(configuration);
let editorIFrame = getEditorIFrame(dom);
Expand Down
Loading