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
1 change: 1 addition & 0 deletions src/Exceptionless.Core/Models/Data/Error.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public class Error : InnerError
public static class KnownDataKeys
{
public const string ExtraProperties = "@ext";
public const string SourceMap = "@source_map";
public const string TargetInfo = "@target";
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ public override async Task EventProcessingAsync(EventContext context)
context.Project.Id,
context.EventPostInfo?.ClientKeyHash,
String.Equals(context.Organization.PlanId, _billingPlans.FreePlan.Id, StringComparison.OrdinalIgnoreCase));
if (await _sourceMapService.SymbolicateAsync(request, error))
var result = await _sourceMapService.ProcessAsync(request, error);
if (result.Modified)
context.Event.SetError(error);
}
}
298 changes: 232 additions & 66 deletions src/Exceptionless.Core/Services/SourceMaps/SourceMapService.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<script lang="ts">
import type { ErrorInfo, SourceMapFailureInfo } from '$features/events/models/event-data';

import { resolve } from '$app/paths';
import { Button } from '$comp/ui/button';
import * as Popover from '$comp/ui/popover';
import TriangleAlert from '@lucide/svelte/icons/triangle-alert';

interface Props {
error: ErrorInfo;
projectId: string;
}

let { error, projectId }: Props = $props();

const sourceMapStatus = $derived(error.data?.['@source_map']);
const failures = $derived(sourceMapStatus?.failures ?? []);
const processingTruncated = $derived(sourceMapStatus?.processing_truncated === true);
const title = $derived(sourceMapStatus?.status === 'partial' ? 'Stack trace partially symbolicated' : 'Source map unavailable');

function getFailureDescription(failure: SourceMapFailureInfo): string {
switch (failure.reason) {
case 'invalid':
return 'The downloaded source map is invalid or unsupported.';
case 'no_matching_mapping':
return 'The source map does not contain this generated location.';
case 'not_found':
return 'No usable source map could be downloaded.';
case 'timeout':
return 'The source map download timed out.';
default:
return 'The source map could not be downloaded.';
}
}
</script>

{#if failures.length > 0 || processingTruncated}
<Popover.Root>
<Popover.Trigger>
{#snippet child({ props })}
<Button {...props} size="sm" title="View source map details" variant="outline">
<TriangleAlert class="size-3.5 text-amber-500" />
{title}
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content align="end" class="w-96 max-w-[calc(100vw-2rem)]">
<Popover.Header>
<Popover.Title>{title}</Popover.Title>
<Popover.Description class="text-xs">
{#if failures.length > 0}
<p>
Exceptionless couldn't map {failures.length === 1 ? 'a JavaScript file' : `${failures.length} JavaScript files`} to original source. The
stack trace may be minified. Uploading a source map will improve new events.
</p>
<ul class="mt-1.5 space-y-1">
{#each failures as failure (failure.generated_file_name)}
<li>
<span class="font-mono break-all">{failure.generated_file_name}</span>
<span> — {getFailureDescription(failure)}</span>
</li>
{/each}
{#if sourceMapStatus?.truncated}<li>Additional generated files were omitted.</li>{/if}
</ul>
{/if}
{#if processingTruncated}
<p class:mt-1.5={failures.length > 0}>
Exceptionless stopped checking source maps after reaching the stack-frame processing limit. Some frames may remain minified.
</p>
{/if}
</Popover.Description>
</Popover.Header>
<div>
<Button
href={resolve('/(app)/project/[projectId]/source-maps', {
projectId
})}
size="sm"
variant="outline">Manage source maps</Button
>
</div>
</Popover.Content>
</Popover.Root>
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { ErrorInfo } from '$features/events/models/event-data';

import { fireEvent, render, screen } from '@testing-library/svelte';
import { describe, expect, it } from 'vitest';

import SourceMapStatus from './source-map-status.svelte';

const projectId = '507f1f77bcf86cd799439011';

describe('SourceMapStatus', () => {
it('shows source map failure details on demand', async () => {
const error: ErrorInfo = {
data: {
'@source_map': {
failures: [
{
generated_file_name: 'https://cdn.example.com/assets/app.min.js',
reason: 'invalid'
}
],
status: 'failed'
}
}
};

render(SourceMapStatus, { error, projectId });

const trigger = screen.getByRole('button', { name: 'Source map unavailable' });
expect(trigger).toBeTruthy();
expect(screen.queryByText('https://cdn.example.com/assets/app.min.js')).toBeNull();

await fireEvent.click(trigger);

expect(screen.getByText('https://cdn.example.com/assets/app.min.js')).toBeTruthy();
expect(screen.getByText(/downloaded source map is invalid or unsupported/i)).toBeTruthy();
expect(screen.getByRole('link', { name: 'Manage source maps' }).getAttribute('href')).toBe(`/next/project/${projectId}/source-maps`);
});

it('does not render without failure metadata', () => {
render(SourceMapStatus, { error: {}, projectId });

expect(screen.queryByRole('button', { name: /source map/i })).toBeNull();
});

it('shows processing limit details on demand', async () => {
const error: ErrorInfo = {
data: {
'@source_map': {
failures: [],
processing_truncated: true,
status: 'failed'
}
}
};

render(SourceMapStatus, { error, projectId });

await fireEvent.click(screen.getByRole('button', { name: 'Source map unavailable' }));

expect(screen.getByText(/stack-frame processing limit/i)).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import ExtendedDataItem from '../extended-data-item.svelte';
import SimpleStackTrace from '../simple-stack-trace/simple-stack-trace.svelte';
import SourceMapStatus from '../stack-trace/source-map-status.svelte';
import StackTrace from '../stack-trace/stack-trace.svelte';

interface Props {
Expand Down Expand Up @@ -63,7 +64,10 @@

<div class="mt-4 mb-2 flex justify-between">
<H3>Stack Trace</H3>
<div class="flex justify-end">
<div class="flex items-center justify-end gap-1.5">
{#if event.data?.['@error']}
<SourceMapStatus error={event.data['@error']} projectId={event.project_id} />
{/if}
<CopyToClipboardButton size="icon-sm" title="Copy Stack Trace to Clipboard" value={stackTrace} variant="outline"></CopyToClipboardButton>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import LogLevel from '../log-level.svelte';
import SessionEventDuration from '../session-event-duration.svelte';
import SimpleStackTrace from '../simple-stack-trace/simple-stack-trace.svelte';
import SourceMapStatus from '../stack-trace/source-map-status.svelte';
import StackTrace from '../stack-trace/stack-trace.svelte';

interface Props {
Expand Down Expand Up @@ -237,7 +238,10 @@
{#if hasError}
<div class="mt-4 flex justify-between">
<H3>Stack Trace</H3>
<div class="flex justify-end">
<div class="flex items-center justify-end gap-1.5">
{#if event.data?.['@error']}
<SourceMapStatus error={event.data['@error']} projectId={event.project_id} />
{/if}
<CopyToClipboardButton size="icon-sm" title="Copy Stack Trace to Clipboard" value={stackTrace} variant="outline"></CopyToClipboardButton>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface ErrorInfo extends InnerErrorInfo {

export interface IErrorData extends Record<string, unknown> {
'@ext'?: Record<string, unknown> | string;
'@source_map'?: SourceMapStatusInfo;
'@target'?: ITargetErrorData;
}

Expand Down Expand Up @@ -116,6 +117,18 @@ export interface SimpleErrorInfo {
type?: string;
}

export interface SourceMapFailureInfo {
generated_file_name: string;
reason: string;
}

export interface SourceMapStatusInfo {
failures: SourceMapFailureInfo[];
processing_truncated?: boolean;
status: 'failed' | 'partial' | string;
truncated?: boolean;
}

export interface StackFrameInfo extends MethodInfo {
column?: number;
file_name?: string;
Expand Down
Loading
Loading