-
Notifications
You must be signed in to change notification settings - Fork 882
Add tracked browser logs and network capture #16310
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
eab2c02
Add tracked browser logs and network capture
davidfowl ee8df52
Harden tracked browser log runtime
davidfowl 5235c7c
Fix XML doc indentation and use injected TimeProvider consistently
mitchdenny cb9c284
Split tracked browser log runtime
davidfowl 3fefe17
Address browser logs review feedback
davidfowl ac01379
Add browser logs configuration and session metadata
davidfowl 7b0f98a
Prefer installed browser fallback
davidfowl e079f40
Fix browser logs review findings
davidfowl 57f2789
Address browser logs PR feedback
davidfowl bea945d
Fix shared console logging helper timeout
davidfowl 4853bf2
Address latest browser logs feedback
davidfowl e28edf9
Fix browser logs websocket cleanup
davidfowl File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
39 changes: 38 additions & 1 deletion
39
playground/BrowserTelemetry/BrowserTelemetry.Web/Pages/Index.cshtml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,41 @@ | ||
| @page | ||
| <div class="container"> | ||
| <p>Hello world</p> | ||
| <div class="row g-4"> | ||
| <div class="col-lg-8"> | ||
| <h1 class="display-6">Browser logs demo</h1> | ||
| <p class="lead"> | ||
| Use the <strong>web-browser-logs</strong> resource in the dashboard to open a tracked browser session, then | ||
| use the buttons below to emit browser-side console logs, network requests, and unhandled failures. | ||
| </p> | ||
| </div> | ||
| <div class="col-lg-4"> | ||
| <div class="alert alert-secondary mb-0" id="browser-log-status"> | ||
| Waiting for browser interaction. | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div class="row row-cols-1 row-cols-md-2 g-3 mt-1"> | ||
| <div class="col"> | ||
| <button class="btn btn-primary w-100" id="emit-console-log" type="button">Emit console.log</button> | ||
| </div> | ||
| <div class="col"> | ||
| <button class="btn btn-warning w-100" id="emit-console-warn" type="button">Emit console.warn</button> | ||
| </div> | ||
| <div class="col"> | ||
| <button class="btn btn-danger w-100" id="emit-console-error" type="button">Emit console.error</button> | ||
| </div> | ||
| <div class="col"> | ||
| <button class="btn btn-dark w-100" id="emit-unhandled-exception" type="button">Throw unhandled exception</button> | ||
| </div> | ||
| <div class="col"> | ||
| <button class="btn btn-outline-danger w-100" id="emit-unhandled-rejection" type="button">Reject unhandled promise</button> | ||
| </div> | ||
| <div class="col"> | ||
| <button class="btn btn-outline-primary w-100" id="emit-successful-fetch" type="button">Fetch successful request</button> | ||
| </div> | ||
| <div class="col"> | ||
| <button class="btn btn-outline-secondary w-100" id="emit-failing-fetch" type="button">Fetch failing request</button> | ||
| </div> | ||
| </div> | ||
| </div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using Aspire.Hosting.ApplicationModel; | ||
| using Aspire.Hosting.Resources; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.DependencyInjection.Extensions; | ||
|
|
||
| namespace Aspire.Hosting; | ||
|
|
||
| /// <summary> | ||
| /// Extension methods for adding tracked browser log resources to browser-based application resources. | ||
| /// </summary> | ||
| public static class BrowserLogsBuilderExtensions | ||
| { | ||
| internal const string BrowserResourceType = "BrowserLogs"; | ||
| internal const string BrowserPropertyName = "Browser"; | ||
| internal const string BrowserExecutablePropertyName = "Browser executable"; | ||
| internal const string TargetUrlPropertyName = "Target URL"; | ||
| internal const string ActiveSessionsPropertyName = "Active sessions"; | ||
| internal const string ActiveSessionCountPropertyName = "Active session count"; | ||
| internal const string TotalSessionsLaunchedPropertyName = "Total sessions launched"; | ||
| internal const string LastSessionPropertyName = "Last session"; | ||
| internal const string OpenTrackedBrowserCommandName = "open-tracked-browser"; | ||
|
|
||
| /// <summary> | ||
| /// Adds a child resource that can open the application's primary browser endpoint in a tracked browser session and | ||
| /// surface browser console output in the dashboard console logs. | ||
| /// </summary> | ||
| /// <typeparam name="T">The type of resource being configured.</typeparam> | ||
| /// <param name="builder">The resource builder.</param> | ||
| /// <param name="browser"> | ||
| /// The browser to launch. Defaults to <c>"msedge"</c>. Supported values include logical browser names such as | ||
| /// <c>"msedge"</c> and <c>"chrome"</c>, or an explicit browser executable path. | ||
| /// </param> | ||
| /// <returns>A reference to the original <see cref="IResourceBuilder{T}"/> for further chaining.</returns> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// This method adds a child browser logs resource beneath the parent resource represented by <paramref name="builder"/>. | ||
| /// The child resource exposes a dashboard command that launches a Chromium-based browser in a tracked mode, attaches to | ||
| /// the browser's debugging protocol, and forwards browser console, error, and exception output to the child resource's | ||
| /// console log stream. | ||
| /// </para> | ||
| /// <para> | ||
| /// The tracked browser session uses the <a href="https://chromedevtools.github.io/devtools-protocol/">Chrome DevTools | ||
| /// Protocol (CDP)</a> to subscribe to browser runtime, log, page, and network events. | ||
| /// </para> | ||
| /// <para> | ||
| /// The parent resource must expose at least one HTTP or HTTPS endpoint. HTTPS endpoints are preferred over HTTP | ||
| /// endpoints when selecting the browser target URL. | ||
| /// </para> | ||
| /// </remarks> | ||
| /// <example> | ||
| /// Add tracked browser logs for a web front end: | ||
| /// <code> | ||
| /// var builder = DistributedApplication.CreateBuilder(args); | ||
| /// | ||
| /// builder.AddProject<Projects.WebFrontend>("web") | ||
| /// .WithExternalHttpEndpoints() | ||
| /// .WithBrowserLogs(); | ||
| /// </code> | ||
| /// </example> | ||
| [AspireExport(Description = "Adds a child browser logs resource that opens tracked browser sessions and captures browser logs.")] | ||
| public static IResourceBuilder<T> WithBrowserLogs<T>(this IResourceBuilder<T> builder, string browser = "msedge") | ||
| where T : IResourceWithEndpoints | ||
| { | ||
| ArgumentNullException.ThrowIfNull(builder); | ||
| ArgumentException.ThrowIfNullOrWhiteSpace(browser); | ||
|
|
||
| builder.ApplicationBuilder.Services.TryAddSingleton<IBrowserLogsSessionManager, BrowserLogsSessionManager>(); | ||
|
|
||
| var parentResource = builder.Resource; | ||
| var browserLogsResource = new BrowserLogsResource($"{parentResource.Name}-browser-logs", parentResource, browser); | ||
|
|
||
| builder.ApplicationBuilder.AddResource(browserLogsResource) | ||
| .WithParentRelationship(parentResource) | ||
| .ExcludeFromManifest() | ||
| .WithIconName("GlobeDesktop") | ||
| .WithInitialState(new CustomResourceSnapshot | ||
| { | ||
| ResourceType = BrowserResourceType, | ||
| CreationTimeStamp = DateTime.UtcNow, | ||
| State = KnownResourceStates.NotStarted, | ||
| Properties = | ||
| [ | ||
| new ResourcePropertySnapshot(CustomResourceKnownProperties.Source, parentResource.Name), | ||
| new ResourcePropertySnapshot(BrowserPropertyName, browser), | ||
| new ResourcePropertySnapshot(ActiveSessionCountPropertyName, 0), | ||
| new ResourcePropertySnapshot(ActiveSessionsPropertyName, "None"), | ||
| new ResourcePropertySnapshot(TotalSessionsLaunchedPropertyName, 0) | ||
| ] | ||
| }) | ||
| .WithCommand( | ||
| OpenTrackedBrowserCommandName, | ||
| CommandStrings.OpenTrackedBrowserName, | ||
| async context => | ||
| { | ||
| try | ||
| { | ||
| var url = ResolveBrowserUrl(parentResource); | ||
| var sessionManager = context.ServiceProvider.GetRequiredService<IBrowserLogsSessionManager>(); | ||
| await sessionManager.StartSessionAsync(browserLogsResource, context.ResourceName, url, context.CancellationToken).ConfigureAwait(false); | ||
| return CommandResults.Success(); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| return CommandResults.Failure(ex.Message); | ||
| } | ||
| }, | ||
| new CommandOptions | ||
| { | ||
| Description = CommandStrings.OpenTrackedBrowserDescription, | ||
| IconName = "Open", | ||
| IconVariant = IconVariant.Regular, | ||
| IsHighlighted = true, | ||
| UpdateState = context => | ||
| { | ||
| var childState = context.ResourceSnapshot.State?.Text; | ||
| if (childState == KnownResourceStates.Starting) | ||
| { | ||
| return ResourceCommandState.Disabled; | ||
| } | ||
|
|
||
| var resourceNotifications = context.ServiceProvider.GetRequiredService<ResourceNotificationService>(); | ||
| foreach (var resourceName in parentResource.GetResolvedResourceNames()) | ||
| { | ||
| if (resourceNotifications.TryGetCurrentState(resourceName, out var resourceEvent)) | ||
| { | ||
| var parentState = resourceEvent.Snapshot.State?.Text; | ||
| if (parentState == KnownResourceStates.Running || parentState == KnownResourceStates.RuntimeUnhealthy) | ||
| { | ||
| return ResourceCommandState.Enabled; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return ResourceCommandState.Disabled; | ||
| } | ||
| }); | ||
|
|
||
| builder.OnBeforeResourceStarted((_, @event, _) => RefreshBrowserLogsResourceAsync(@event.Services.GetRequiredService<ResourceNotificationService>())) | ||
| .OnResourceReady((_, @event, _) => RefreshBrowserLogsResourceAsync(@event.Services.GetRequiredService<ResourceNotificationService>())) | ||
| .OnResourceStopped((_, @event, _) => RefreshBrowserLogsResourceAsync(@event.Services.GetRequiredService<ResourceNotificationService>())); | ||
|
|
||
| return builder; | ||
|
|
||
| Task RefreshBrowserLogsResourceAsync(ResourceNotificationService notifications) => | ||
| notifications.PublishUpdateAsync(browserLogsResource, snapshot => snapshot); | ||
|
|
||
| static Uri ResolveBrowserUrl(T resource) | ||
| { | ||
| EndpointAnnotation? endpointAnnotation = null; | ||
| if (resource.TryGetAnnotationsOfType<EndpointAnnotation>(out var endpoints)) | ||
| { | ||
| endpointAnnotation = endpoints.FirstOrDefault(e => e.UriScheme == "https") | ||
| ?? endpoints.FirstOrDefault(e => e.UriScheme == "http"); | ||
| } | ||
|
|
||
| if (endpointAnnotation is null) | ||
| { | ||
| throw new InvalidOperationException($"Resource '{resource.Name}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to."); | ||
| } | ||
|
|
||
| var endpointReference = resource.GetEndpoint(endpointAnnotation.Name); | ||
| if (!endpointReference.IsAllocated) | ||
| { | ||
| throw new InvalidOperationException($"Endpoint '{endpointAnnotation.Name}' for resource '{resource.Name}' has not been allocated yet."); | ||
| } | ||
|
|
||
| return new Uri(endpointReference.Url, UriKind.Absolute); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.