Skip to content
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

feat: Portable Debug Data images #56

Merged
merged 11 commits into from
Nov 8, 2024
89 changes: 89 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,95 @@ This method accepts the following arguments:
- `category`: A custom value used to arbitrarily group this Breadcrumb.
- `customData`: Any custom data you want to record about application state when the Breadcrumb was recorded.

### Stack traces and portable debug data

Raygun for Blazor attaches both stack traces and the necessary info for portable debug data automatically.

#### Blazor stack traces

Exceptions originating within the Blazor environment should contain a stack trace attached.
For details, check the "Raw Data" tab on the Raygun error report.
The stack trace is contained inside `error.stackTrace` property.

For example:

```json
"error": {
"className": "System.DivideByZeroException",
"message": "Attempted to divide by zero.",
"stackTrace": [
{
"className": "Raygun.Samples.Blazor.Server.Components.Pages.Sample",
"columnNumber": 75,
"fileName": "...\\src\\Raygun.Samples.Blazor.Server\\Components\\Pages\\Sample.razor",
"ilOffset": 5,
"lineNumber": 13,
"methodName": "",
"methodToken": 100663352
},
// ...
]
},
```

This also works for exceptions originating in Blazor WebAssembly applications.
For example, this exception captured on WebAssembly:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...this exception was captured...


```json
"error": {
"className": "System.DivideByZeroException",
"message": "Attempted to divide by zero.",
"stackTrace": [
{
"className": "Raygun.Samples.Blazor.WebAssembly.ViewModels.CounterViewModel",
"columnNumber": 17,
"fileName": "...\\src\\Raygun.Samples.Blazor.WebAssembly\\ViewModels\\CounterViewModel.cs",
"ilOffset": 34,
"lineNumber": 48,
"methodName": "IncrementCountAsync",
"methodToken": 100663343
},
// ...
]
},
```

#### JavaScript stack traces

Exceptions happening in the JavaScript side of a Blazor application should contain a stack trace referring to the JavaScript code that caused the error.

For example:

```
ReferenceError: undefinedfunction3 is not defined
at causeErrors (https://localhost:7254/myfunctions.js:10:9)
at window.onmessage (https://localhost:7254/:21:17)
```

#### Portable debug data (PDB)

Raygun for Blazor supports debugging reports using PDB files when running on Blazor Server and MAUI applications.
The necessary image information will be attached automatically to error reports.

The debug data can be found in the `error.images` property:

```json
"error": {
"className": "System.DivideByZeroException",
"images": [
{
"signature": "a93d65be-ba53-4743-a1a5-4743716b7a42",
"checksum": "SHA256:BE653DA953BA4337A1A54743716B7A42A678F57568949BE3C375DB0BABF8EC35",
"file": "...\\src\\Raygun.Samples.Blazor.Server\\obj\\Debug\\net8.0\\Raygun.Samples.Blazor.Server.pdb",
"timestamp": "A1E84548"
},
// ...
],
},
```

You can learn more about Portable PDB Support [on Raygun's .Net Framework documentation](https://raygun.com/documentation/language-guides/dotnet/crash-reporting/net-framework/#portable-pdb-support).

### Environment details

Raygun for Blazor captures environment details differently depending on the platform where the error originated.
Expand Down
4 changes: 3 additions & 1 deletion src/Raygun.Blazor.Maui/Control/RaygunErrorBoundary.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Microsoft.AspNetCore.Components;
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.Extensions.Options;
Expand Down
16 changes: 8 additions & 8 deletions src/Raygun.Blazor.Maui/Extensions/MauiExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
using KristofferStrube.Blazor.Window;
using Microsoft.Extensions.Options;
using Raygun.Blazor;
using Raygun.Blazor.Interfaces;
using Raygun.Blazor.Offline;
using Raygun.Blazor.Offline.SendStrategy;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.Hosting;
using Raygun.Blazor.Models;

namespace Raygun.Blazor.Maui
{
public static class MauiExtensions
{
public static MauiAppBuilder UseRaygunBlazorMaui(this MauiAppBuilder builder, string configSectionName = "Raygun")
{
#if ANDROID
// Replace default AssemblyReaderProvider with the Android Assembly reader from Raygun4Maui
ErrorDetails.AssemblyReaderProvider = AndroidUtilities.CreateAssemblyReader()!.TryGetReader;
#endif

builder.Services.Configure<RaygunSettings>(builder.Configuration.GetSection(configSectionName));
builder.Services.AddScoped<RaygunBrowserInterop>();
builder.Services.AddScoped<IWindowService, WindowService>();
Expand Down
84 changes: 84 additions & 0 deletions src/Raygun.Blazor.Maui/Platforms/Android/AndroidUtilities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System.IO.Compression;
using System.Runtime.InteropServices;
using System.Text;

namespace Raygun.Blazor.Maui;

internal static class AndroidUtilities
{
/// <summary>
/// Factory method to create the correct assembly reader for the current application
/// </summary>
/// <returns></returns>
public static IAssemblyReader? CreateAssemblyReader()
{
var apkPath = Android.App.Application.Context.ApplicationInfo?.SourceDir;
var supportedAbis = new List<string>();

if (Android.OS.Build.SupportedAbis != null)
{
supportedAbis.AddRange(Android.OS.Build.SupportedAbis);
}

if (!File.Exists(apkPath))
{
// No apk, so return nothing
return null;
}

if (!IsAndroidArchive(apkPath))
{
// Not a valid android archive so nothing to return
return null;
}

// Open the apk file, and see if it has a manifest, if it does,
// we are using the new assembly store method,
// else it's just a normal zip with assemblies as archive entries
using var zipArchive = ZipFile.Open(apkPath, ZipArchiveMode.Read);

if (zipArchive.GetEntry("assemblies/assemblies.manifest") != null)
{
return new AssemblyBlobStoreReader(zipArchive, supportedAbis);
}

return new AssemblyZipEntryReader(zipArchive, supportedAbis);
}

public static bool IsAndroidArchive(string filePath)
{
return filePath.EndsWith(".aab", StringComparison.OrdinalIgnoreCase) ||
filePath.EndsWith(".apk", StringComparison.OrdinalIgnoreCase) ||
filePath.EndsWith(".zip", StringComparison.OrdinalIgnoreCase);
}

public static ReadOnlyMemory<byte> AsReadOnlyMemory(this Stream stream)
{
ArgumentNullException.ThrowIfNull(stream);

using var memoryStream = new MemoryStream();
stream.CopyTo(memoryStream);
return new ReadOnlyMemory<byte>(memoryStream.ToArray());
}

public static byte[] ToArray(this ReadOnlyMemory<byte> memory)
{
if (!MemoryMarshal.TryGetArray(memory, out var segment))
{
throw new InvalidOperationException("Could not get array segment from ReadOnlyMemory.");
}

return segment.Array!;
}

public static BinaryReader GetBinaryReader(this ReadOnlyMemory<byte> memory, Encoding? encoding = null)
{
return new BinaryReader(memory.AsStream(), encoding ?? Encoding.UTF8, false);
}


public static MemoryStream AsStream(this ReadOnlyMemory<byte> memory)
{
return new MemoryStream(memory.ToArray(), writable: false);
}
}
Loading
Loading