Skip to content

Commit

Permalink
feat: Portable Debug Data images (#56)
Browse files Browse the repository at this point in the history
  • Loading branch information
miquelbeltran authored Nov 8, 2024
1 parent 78063c5 commit c74d809
Show file tree
Hide file tree
Showing 14 changed files with 965 additions and 10 deletions.
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:

```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

0 comments on commit c74d809

Please sign in to comment.