-
-
Notifications
You must be signed in to change notification settings - Fork 507
Add JavaScript source map symbolication #2373
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
Open
ejsmith
wants to merge
41
commits into
main
Choose a base branch
from
feature/source-map-symbolication
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
41 commits
Select commit
Hold shift + click to select a range
6550a8d
Add JavaScript source map symbolication
ejsmith 5e7dd25
Harden source map symbolication lifecycle
ejsmith 3a42b1f
Add project-scoped source map upload tokens
ejsmith 5bc2414
Add source map token creation to the UI
ejsmith 609846c
Harden source map parser limits
ejsmith 8bed4e0
Clean up source maps for deleted projects
ejsmith 911eb5d
Throttle automatic source map discovery
ejsmith a687fb5
Fix source map upload page render
ejsmith a9ab52a
Merge remote-tracking branch 'origin/main' into feature/source-map-sy…
ejsmith 618e4ce
Merge origin/main into feature/source-map-symbolication
niemyjski 758ddaf
Harden source map processing
niemyjski 1064caf
Fix OpenAPI snapshot terminator
niemyjski e1e4ab2
Honor source map response headers before size checks
niemyjski 7cb15bf
Validate source map symbolication inputs
niemyjski 9645042
Stabilize source map content validation tests
niemyjski 3a381b2
Bound source map segments before allocation
niemyjski 945a328
Stabilize source map validation and fallback
niemyjski efad47d
Merge remote-tracking branch 'origin/main' into feature/source-map-sy…
ejsmith 54e7a7a
Fix source map CDN fallback and preview rollout
ejsmith 4d819eb
Wait for current preview pods
ejsmith e20efcb
Capture preview pod readiness diagnostics
ejsmith a8562bb
Fix API and job container ports
ejsmith 410a30b
Harden source map storage and cache consistency
ejsmith 65a76c9
fix source map discovery and storage cleanup
ejsmith 0ef1ef8
fix source root and artifact deletion edge cases
ejsmith b84b1d8
validate downloaded source maps before storage
ejsmith 49b6f9b
fix source map cache lifetimes
ejsmith 62afc2d
persist source map cache generations
ejsmith 9efbdb2
refine source map settings navigation
ejsmith b484b40
align source map settings styling
ejsmith 15fc27e
move source map back action
ejsmith d300ebf
move source map action below description
ejsmith 5d08fd6
rename source map settings back action
ejsmith cc0f331
address source map review feedback
ejsmith 82fe65e
harden source map fallback and storage rollback
ejsmith 91a3d43
disable decompression for source map range probes
ejsmith e751a8f
limit and clean up source map storage
ejsmith a945ef3
address source map review feedback
ejsmith ad4b36e
expose source map last used time
ejsmith 7451d4c
fix source map API regressions
ejsmith 5b220ba
coordinate source map cleanup
ejsmith 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| --- | ||
| title: "JavaScript Source Maps" | ||
| --- | ||
|
|
||
| # JavaScript Source Maps | ||
|
|
||
| Exceptionless uses source maps to turn minified JavaScript stack frames into the original file names, line and column numbers, and function names. Symbolication happens before an event is assigned to a stack, so readable function names also improve stack grouping. | ||
|
|
||
| ## Automatic discovery | ||
|
|
||
| No domain allowlist or project setup is required for public source maps. When an error contains an absolute HTTPS JavaScript URL, Exceptionless checks the generated file's `SourceMap` or `X-SourceMap` response header and its `sourceMappingURL` comment. If neither is present, it also checks the conventional `<generated-file>.map` URL. | ||
|
|
||
| Downloaded maps are validated and cached in project-scoped file storage. Automatically downloaded maps are revalidated after one hour so a stable generated-file URL cannot retain a map from an older deployment indefinitely. If refresh fails, Exceptionless leaves the generated frame unchanged instead of risking a misleading stack trace from the stale map. | ||
|
|
||
| Exceptionless only makes anonymous HTTPS requests to public network addresses. Redirects are revalidated, and downloads have time, redirect, size, concurrency, and per-project rate limits. Parsed maps use a bounded in-memory cache. Self-hosted installations can tune these safeguards under the `SourceMaps` configuration section, including `AutoDownloadRefreshIntervalMinutes`, `ParsedSourceMapCacheLifetimeMinutes`, and `MaximumParsedSourceMapCacheSize`. | ||
|
|
||
| ## Uploading a source map | ||
|
|
||
| Upload a map when it is private or is not deployed next to the generated JavaScript: | ||
|
|
||
| 1. Open the project and select **Source Maps** under **Project Settings**. | ||
| 2. Enter the exact absolute URL that appears in the generated stack frame, including any path or query string used to identify the build. | ||
| 3. Select the corresponding source map and upload it. | ||
|
|
||
| Uploading another map for the same generated file URL replaces the previous map. Uploaded and automatically discovered maps appear together on the Source Maps page and can be deleted there. | ||
|
|
||
| Source maps must use the version 3 flat-map format. Indexed source maps with a `sections` property and authenticated automatic downloads are planned follow-up capabilities; private maps can be uploaded in the meantime. | ||
|
|
||
| ## Deployment guidance | ||
|
|
||
| Generate source maps as part of the same build that produces the minified JavaScript. Content-hashed generated file names are preferred because the generated URL then identifies a specific build. You can publish the `.map` file for zero-configuration discovery or keep it private and upload it to Exceptionless during deployment. |
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
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,49 @@ | ||
| using Microsoft.Extensions.Configuration; | ||
|
|
||
| namespace Exceptionless.Core.Configuration; | ||
|
|
||
| public sealed class SourceMapOptions | ||
| { | ||
| public bool EnableAutoDownload { get; internal set; } | ||
| public int RequestTimeoutMilliseconds { get; internal set; } | ||
| public int MaximumGeneratedFileSize { get; internal set; } | ||
| public int MaximumSourceMapSize { get; internal set; } | ||
| public int MaximumMappingSegments { get; internal set; } | ||
| public int MaximumRedirects { get; internal set; } | ||
| public int MaximumConcurrentDownloads { get; internal set; } | ||
| public int MaximumAutoDownloadsPerProjectPerHour { get; internal set; } | ||
| public int MaximumFramesPerError { get; internal set; } | ||
| public int MaximumProcessingTimeMilliseconds { get; internal set; } | ||
| public int AutoDownloadRefreshIntervalMinutes { get; internal set; } | ||
| public int ParsedSourceMapCacheLifetimeMinutes { get; internal set; } | ||
| public long MaximumParsedSourceMapCacheSize { get; internal set; } | ||
|
|
||
| public TimeSpan RequestTimeout => TimeSpan.FromMilliseconds(RequestTimeoutMilliseconds); | ||
| public TimeSpan MaximumProcessingTime => TimeSpan.FromMilliseconds(MaximumProcessingTimeMilliseconds); | ||
| public TimeSpan AutoDownloadRefreshInterval => TimeSpan.FromMinutes(AutoDownloadRefreshIntervalMinutes); | ||
| public TimeSpan ParsedSourceMapCacheLifetime => TimeSpan.FromMinutes(ParsedSourceMapCacheLifetimeMinutes); | ||
|
|
||
| public static SourceMapOptions ReadFromConfiguration(IConfiguration configuration) | ||
| { | ||
| var section = configuration.GetSection("SourceMaps"); | ||
| return new SourceMapOptions | ||
| { | ||
| EnableAutoDownload = section.GetValue(nameof(EnableAutoDownload), true), | ||
| RequestTimeoutMilliseconds = ReadPositive(section, nameof(RequestTimeoutMilliseconds), 3000), | ||
| MaximumGeneratedFileSize = ReadPositive(section, nameof(MaximumGeneratedFileSize), 5 * 1024 * 1024), | ||
| MaximumSourceMapSize = ReadPositive(section, nameof(MaximumSourceMapSize), 20 * 1024 * 1024), | ||
| MaximumMappingSegments = ReadPositive(section, nameof(MaximumMappingSegments), 1_000_000), | ||
| MaximumRedirects = Math.Max(0, section.GetValue(nameof(MaximumRedirects), 3)), | ||
| MaximumConcurrentDownloads = ReadPositive(section, nameof(MaximumConcurrentDownloads), 4), | ||
| MaximumAutoDownloadsPerProjectPerHour = Math.Max(0, section.GetValue(nameof(MaximumAutoDownloadsPerProjectPerHour), 100)), | ||
| MaximumFramesPerError = ReadPositive(section, nameof(MaximumFramesPerError), 100), | ||
| MaximumProcessingTimeMilliseconds = ReadPositive(section, nameof(MaximumProcessingTimeMilliseconds), 5000), | ||
| AutoDownloadRefreshIntervalMinutes = ReadPositive(section, nameof(AutoDownloadRefreshIntervalMinutes), 60), | ||
| ParsedSourceMapCacheLifetimeMinutes = ReadPositive(section, nameof(ParsedSourceMapCacheLifetimeMinutes), 5), | ||
| MaximumParsedSourceMapCacheSize = Math.Max(1, section.GetValue(nameof(MaximumParsedSourceMapCacheSize), 100L * 1024 * 1024)) | ||
| }; | ||
| } | ||
|
|
||
| private static int ReadPositive(IConfiguration section, string name, int defaultValue) | ||
| => Math.Max(1, section.GetValue(name, defaultValue)); | ||
| } |
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
36 changes: 36 additions & 0 deletions
36
src/Exceptionless.Core/Plugins/EventProcessor/Default/15_SourceMapPlugin.cs
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,36 @@ | ||
| using Exceptionless.Core.Extensions; | ||
| using Exceptionless.Core.Models; | ||
| using Exceptionless.Core.Pipeline; | ||
| using Exceptionless.Core.Services.SourceMaps; | ||
| using Foundatio.Serializer; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| namespace Exceptionless.Core.Plugins.EventProcessor.Default; | ||
|
|
||
| [Priority(15)] | ||
| public sealed class SourceMapPlugin : EventProcessorPluginBase | ||
| { | ||
| private readonly SourceMapService _sourceMapService; | ||
| private readonly ITextSerializer _serializer; | ||
|
|
||
| public SourceMapPlugin(SourceMapService sourceMapService, ITextSerializer serializer, AppOptions options, ILoggerFactory loggerFactory) | ||
| : base(options, loggerFactory) | ||
| { | ||
| _sourceMapService = sourceMapService; | ||
| _serializer = serializer; | ||
| ContinueOnError = true; | ||
| } | ||
|
|
||
| public override async Task EventProcessingAsync(EventContext context) | ||
| { | ||
| if (!context.Event.IsError()) | ||
| return; | ||
|
|
||
| var error = context.Event.GetError(_serializer, _logger); | ||
| if (error is null) | ||
| return; | ||
|
|
||
| if (await _sourceMapService.SymbolicateAsync(context.Project.Id, error)) | ||
| context.Event.SetError(error); | ||
| } | ||
| } |
12 changes: 12 additions & 0 deletions
12
src/Exceptionless.Core/Services/SourceMaps/SourceMapArtifact.cs
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,12 @@ | ||
| namespace Exceptionless.Core.Services.SourceMaps; | ||
|
|
||
| public sealed record SourceMapArtifact | ||
| { | ||
| public required string Id { get; init; } | ||
| public required string GeneratedFileUrl { get; init; } | ||
| public string? SourceMapUrl { get; init; } | ||
| public string? FileName { get; init; } | ||
| public required long Size { get; init; } | ||
| public required bool IsAutoDownloaded { get; init; } | ||
| public required DateTime CreatedUtc { get; init; } | ||
| } |
20 changes: 20 additions & 0 deletions
20
src/Exceptionless.Core/Services/SourceMaps/SourceMapContent.cs
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,20 @@ | ||
| namespace Exceptionless.Core.Services.SourceMaps; | ||
|
|
||
| internal static class SourceMapContent | ||
| { | ||
| public static async Task<byte[]> ReadLimitedAsync(Stream stream, int maximumBytes, CancellationToken cancellationToken) | ||
| { | ||
| using var memoryStream = new MemoryStream(Math.Min(maximumBytes, 64 * 1024)); | ||
| byte[] buffer = new byte[81920]; | ||
| int read; | ||
| while ((read = await stream.ReadAsync(buffer, cancellationToken)) > 0) | ||
| { | ||
| if (memoryStream.Length + read > maximumBytes) | ||
| throw new InvalidOperationException("The file exceeded the configured maximum size."); | ||
|
|
||
| await memoryStream.WriteAsync(buffer.AsMemory(0, read), cancellationToken); | ||
| } | ||
|
|
||
| return memoryStream.ToArray(); | ||
| } | ||
| } |
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.