Skip to content

Commit 1eecf3e

Browse files
committed
fix: error handling & Sentry reporting in services
1 parent 19b8783 commit 1eecf3e

7 files changed

Lines changed: 126 additions & 91 deletions

File tree

src/Eurofurence.App.Server.Services/Abstractions/Dealers/IDealerApiClient.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ namespace Eurofurence.App.Server.Services.Abstractions.Dealers
44
{
55
public interface IDealerApiClient
66
{
7-
public Task<bool> DownloadDealersExportAsync(string path);
7+
public Task DownloadDealersExportAsync(string path);
88
}
99
}

src/Eurofurence.App.Server.Services/Dealers/DealerApiClient.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,14 @@ public DealerApiClient(IOptions<DealerOptions> dealerOptions)
1515
_dealerOptions = dealerOptions.Value;
1616
}
1717

18-
public async Task<bool> DownloadDealersExportAsync(string path)
18+
public async Task DownloadDealersExportAsync(string path)
1919
{
2020
using var handler = new HttpClientHandler();
2121
handler.Credentials = new NetworkCredential(_dealerOptions.User, _dealerOptions.Password);
2222
using var httpClient = new HttpClient(handler);
2323
var fileStream = await httpClient.GetStreamAsync(_dealerOptions.Url);
2424
await using var outputFileStream = new FileStream(path, FileMode.Create);
2525
await fileStream.CopyToAsync(outputFileStream);
26-
return true;
2726
}
2827
}
2928
}

src/Eurofurence.App.Server.Services/Dealers/DealerService.cs

Lines changed: 90 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
using Microsoft.EntityFrameworkCore;
2424
using Microsoft.Extensions.Logging;
2525
using Microsoft.Extensions.Options;
26+
using Sentry;
2627
using File = System.IO.File;
2728

2829
namespace Eurofurence.App.Server.Services.Dealers
@@ -198,94 +199,107 @@ public async Task RunImportAsync(CancellationToken cancellationToken = default)
198199
}
199200

200201
var dealerPackagePath = Path.Combine(_globalOptions.WorkingDirectory, "dealers.zip");
201-
var newDealersExportDownloaded = await _dealerApiClient.DownloadDealersExportAsync(dealerPackagePath);
202202

203-
if (!newDealersExportDownloaded)
203+
try
204204
{
205-
_logger.LogError(LogEvents.Import, $"Error downloading the dealers export csv.");
205+
await _dealerApiClient.DownloadDealersExportAsync(dealerPackagePath);
206+
}
207+
catch (Exception ex)
208+
{
209+
SentrySdk.CaptureException(ex);
210+
_logger.LogError(LogEvents.Import, "Failed to download dealer export data: {exception}", ex.Message);
206211
return;
207212
}
208213

209214
var importRecords = new List<DealerRecord>();
210215

211-
await using (var fileStream = File.OpenRead(dealerPackagePath))
212-
using (var archive = new ZipArchive(fileStream))
216+
try
213217
{
214-
var csvEntry =
215-
archive.Entries.Single(a => a.Name.EndsWith(".csv", StringComparison.InvariantCultureIgnoreCase));
216-
217-
TextReader reader = new StreamReader(csvEntry.Open(), true);
218-
219-
var badData = new List<string>();
220-
221-
var csvConfiguration = new CsvConfiguration(CultureInfo.InvariantCulture)
218+
await using (var fileStream = File.OpenRead(dealerPackagePath))
219+
using (var archive = new ZipArchive(fileStream))
222220
{
223-
Delimiter = ";",
224-
HasHeaderRecord = true,
225-
TrimOptions = TrimOptions.Trim,
226-
NewLine = "\n",
227-
BadDataFound = arg => badData.Add(arg.Context.Parser.RawRecord)
228-
};
221+
var csvEntry =
222+
archive.Entries.Single(a => a.Name.EndsWith(".csv", StringComparison.InvariantCultureIgnoreCase));
229223

230-
var csvReader = new CsvReader(reader, csvConfiguration);
231-
csvReader.Context.RegisterClassMap<DealerImportRowClassMap>();
232-
var csvRecords = csvReader.GetRecords<DealerImportRow>().ToList();
224+
TextReader reader = new StreamReader(csvEntry.Open(), true);
233225

234-
_logger.LogDebug(LogEvents.Import, $"Parsed {csvRecords.Count} records from CSV");
226+
var badData = new List<string>();
235227

236-
for (var i = 0; i < csvRecords.Count; i++)
237-
{
238-
var dealerRecord = new DealerRecord
228+
var csvConfiguration = new CsvConfiguration(CultureInfo.InvariantCulture)
239229
{
240-
Id = csvRecords[i].Id,
241-
AboutTheArtistText = csvRecords[i].AboutTheArtist.Trim(),
242-
AboutTheArtText = csvRecords[i].AboutTheArt.Trim(),
243-
ArtPreviewCaption = csvRecords[i].ArtPreviewCaption.Trim(),
244-
DisplayName = csvRecords[i].DisplayName.Trim(),
245-
ShortDescription = csvRecords[i].ShortDescription.Trim(),
246-
Merchandise = csvRecords[i].Merchandise.Trim(),
247-
AttendsOnThursday = !string.IsNullOrWhiteSpace(csvRecords[i].AttendsThu),
248-
AttendsOnFriday = !string.IsNullOrWhiteSpace(csvRecords[i].AttendsFri),
249-
AttendsOnSaturday = !string.IsNullOrWhiteSpace(csvRecords[i].AttendsSat),
250-
TelegramHandle = csvRecords[i].Telegram.Trim(),
251-
TwitterHandle = csvRecords[i].Twitter.Trim(),
252-
DiscordHandle = csvRecords[i].Discord.Trim(),
253-
MastodonHandle = csvRecords[i].Mastodon.Trim(),
254-
BlueskyHandle = csvRecords[i].Bluesky.Trim(),
255-
IsAfterDark = !string.IsNullOrWhiteSpace(csvRecords[i].AfterDark),
256-
Keywords = csvRecords[i].GetKeywords(),
257-
Categories = csvRecords[i].GetCategories()
230+
Delimiter = ";",
231+
HasHeaderRecord = true,
232+
TrimOptions = TrimOptions.Trim,
233+
NewLine = "\n",
234+
BadDataFound = arg => badData.Add(arg.Context.Parser.RawRecord)
258235
};
259236

260-
dealerRecord.ArtistImageId = await GetImageIdAsync(
261-
archive,
262-
$"artist_{csvRecords[i].Id}.",
263-
$"dealer:artist:{csvRecords[i].Id}",
264-
cancellationToken
265-
);
266-
dealerRecord.ArtistThumbnailImageId = await GetImageIdAsync(
267-
archive,
268-
$"thumbnail_{csvRecords[i].Id}.",
269-
$"dealer:thumbnail:{csvRecords[i].Id}",
270-
cancellationToken
271-
);
272-
dealerRecord.ArtPreviewImageId = await GetImageIdAsync(archive,
273-
$"art_{csvRecords[i].Id}.",
274-
$"dealer:art:{csvRecords[i].Id}",
275-
cancellationToken
276-
);
277-
278-
ImportLinks(dealerRecord, csvRecords[i].Website);
279-
SanitizeFields(dealerRecord);
280-
281-
importRecords.Add(dealerRecord);
282-
}
237+
var csvReader = new CsvReader(reader, csvConfiguration);
238+
csvReader.Context.RegisterClassMap<DealerImportRowClassMap>();
239+
var csvRecords = csvReader.GetRecords<DealerImportRow>().ToList();
283240

284-
if (badData.Count > 0)
285-
{
286-
_logger.LogInformation($"Found {badData.Count} bad rows:\n{string.Join("\n", badData)}");
241+
_logger.LogDebug(LogEvents.Import, $"Parsed {csvRecords.Count} records from CSV");
242+
243+
for (var i = 0; i < csvRecords.Count; i++)
244+
{
245+
var dealerRecord = new DealerRecord
246+
{
247+
Id = csvRecords[i].Id,
248+
AboutTheArtistText = csvRecords[i].AboutTheArtist.Trim(),
249+
AboutTheArtText = csvRecords[i].AboutTheArt.Trim(),
250+
ArtPreviewCaption = csvRecords[i].ArtPreviewCaption.Trim(),
251+
DisplayName = csvRecords[i].DisplayName.Trim(),
252+
ShortDescription = csvRecords[i].ShortDescription.Trim(),
253+
Merchandise = csvRecords[i].Merchandise.Trim(),
254+
AttendsOnThursday = !string.IsNullOrWhiteSpace(csvRecords[i].AttendsThu),
255+
AttendsOnFriday = !string.IsNullOrWhiteSpace(csvRecords[i].AttendsFri),
256+
AttendsOnSaturday = !string.IsNullOrWhiteSpace(csvRecords[i].AttendsSat),
257+
TelegramHandle = csvRecords[i].Telegram.Trim(),
258+
TwitterHandle = csvRecords[i].Twitter.Trim(),
259+
DiscordHandle = csvRecords[i].Discord.Trim(),
260+
MastodonHandle = csvRecords[i].Mastodon.Trim(),
261+
BlueskyHandle = csvRecords[i].Bluesky.Trim(),
262+
IsAfterDark = !string.IsNullOrWhiteSpace(csvRecords[i].AfterDark),
263+
Keywords = csvRecords[i].GetKeywords(),
264+
Categories = csvRecords[i].GetCategories(),
265+
ArtistImageId = await GetImageIdAsync(
266+
archive,
267+
$"artist_{csvRecords[i].Id}.",
268+
$"dealer:artist:{csvRecords[i].Id}",
269+
cancellationToken
270+
),
271+
ArtistThumbnailImageId = await GetImageIdAsync(
272+
archive,
273+
$"thumbnail_{csvRecords[i].Id}.",
274+
$"dealer:thumbnail:{csvRecords[i].Id}",
275+
cancellationToken
276+
),
277+
ArtPreviewImageId = await GetImageIdAsync(
278+
archive,
279+
$"art_{csvRecords[i].Id}.",
280+
$"dealer:art:{csvRecords[i].Id}",
281+
cancellationToken
282+
)
283+
};
284+
285+
ImportLinks(dealerRecord, csvRecords[i].Website);
286+
SanitizeFields(dealerRecord);
287+
288+
importRecords.Add(dealerRecord);
289+
}
290+
291+
if (badData.Count > 0)
292+
{
293+
_logger.LogInformation($"Found {badData.Count} bad rows:\n{string.Join("\n", badData)}");
294+
}
287295
}
288296
}
297+
catch (Exception ex)
298+
{
299+
SentrySdk.CaptureException(ex);
300+
_logger.LogError(LogEvents.Import, "Failed to process dealer export: {exception}", ex.Message);
301+
return;
302+
}
289303

290304
var existingRecords = FindAll();
291305

@@ -322,7 +336,7 @@ public async Task RunImportAsync(CancellationToken cancellationToken = default)
322336

323337
File.Delete(dealerPackagePath);
324338
_logger.LogInformation(LogEvents.Import,
325-
$"Dealers import with {diff.Count(p => p.Action == ActionEnum.Add)} addition(s), {diff.Count(p => p.Action == ActionEnum.Update)} update(s) and {diff.Count(p => p.Action == ActionEnum.Delete)} deletion(s) finished successfully with {diff.Count(a => a.Action == ActionEnum.NotModified)} unmodified.");
339+
$"Dealers import with {diff.Count(p => p.Action == ActionEnum.Add)} addition(s), {diff.Count(p => p.Action == ActionEnum.Update)} update(s) and {diff.Count(p => p.Action == ActionEnum.Delete)} deletion(s) finished successfully with {diff.Count(a => a.Action == ActionEnum.NotModified)} unmodified.");
326340
}
327341
finally
328342
{
@@ -382,10 +396,11 @@ private void ImportLinks(DealerRecord dealerRecord, string websiteUrls)
382396

383397
var sanitizedParts = websiteUrls
384398
.Replace(" / ", ";")
385-
.Split(new[]
386-
{
387-
' ', ',', ';'
388-
}, StringSplitOptions.RemoveEmptyEntries);
399+
.Split(
400+
[
401+
' ', ',', ';'
402+
],
403+
StringSplitOptions.RemoveEmptyEntries);
389404

390405
foreach (var part in sanitizedParts)
391406
{

src/Eurofurence.App.Server.Services/Eurofurence.App.Server.Services.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.10" />
3535
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.22.0" />
3636
<PackageReference Include="Minio" Version="7.0.0" />
37+
<PackageReference Include="Sentry.AspNetCore" Version="6.8.0" />
3738
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
3839
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.7" />
3940
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.22.0" />

src/Eurofurence.App.Server.Services/Identity/IdentityService.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
using Microsoft.Extensions.Caching.Distributed;
2323
using Microsoft.Extensions.Logging;
2424
using Microsoft.Extensions.Options;
25+
using Sentry;
2526

2627
namespace Eurofurence.App.Server.Services.Identity
2728
{
@@ -70,6 +71,8 @@ public async Task ReadUserInfo(ClaimsIdentity identity)
7071
return;
7172
}
7273

74+
var identityId = identity.FindFirst("sub")?.Value;
75+
7376
using var client = _httpClientFactory.CreateClient(OAuth2IntrospectionDefaults.BackChannelHttpClientName);
7477

7578
var response = await client.GetUserInfoAsync(new UserInfoRequest
@@ -88,7 +91,8 @@ public async Task ReadUserInfo(ClaimsIdentity identity)
8891
);
8992
if (hasMissingNameBug)
9093
{
91-
_logger.LogInformation("Response to userinfo request missing 'name' claim will not be cached.");
94+
SentrySdk.CaptureMessage($"IDP response to userinfo request missing 'name' claim for identity {identityId}.", SentryLevel.Warning);
95+
_logger.LogWarning("Response to userinfo request for identity {identityId} missing 'name' claim will not be cached.", identityId);
9296
}
9397

9498
var exp = identity.FindFirst(x => x.Type == "exp");
@@ -103,7 +107,7 @@ await _cache.SetStringAsync(
103107
}
104108
);
105109

106-
if (identity.FindFirst("sub")?.Value is { Length: > 0 } identityId &&
110+
if (!string.IsNullOrEmpty(identityId) &&
107111
GetUserGroups(identity).ToArray() is { Length: > 0 } groups)
108112
{
109113
var identityAnnouncementGroups = await _appDbContext.IdentityAnnouncementGroups.AsTracking().FirstOrDefaultAsync(iag => iag.IdentityId == identityId);
@@ -281,6 +285,7 @@ private async Task<RegistrationData> GetRegistrationStatus(string token, string?
281285

282286
if (!statusResponse.IsSuccessStatusCode)
283287
{
288+
SentrySdk.CaptureMessage($"Failed to get registration information from regsys for reg ID {id}: Status {statusResponse.StatusCode}", SentryLevel.Warning);
284289
_logger.LogWarning("Failed to get registration information from regsys for reg ID {id}: Status {httpStatus}", id, statusResponse.StatusCode);
285290
return new RegistrationData(id, UserRegistrationStatus.Unknown);
286291
}

src/Eurofurence.App.Server.Services/PushNotifications/PushNotificationChannelManager.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using Microsoft.EntityFrameworkCore;
2020
using Microsoft.Extensions.Logging;
2121
using Microsoft.Extensions.Options;
22+
using Sentry;
2223

2324
namespace Eurofurence.App.Server.Services.PushNotifications
2425
{
@@ -107,6 +108,7 @@ public async Task PushAnnouncementNotificationToGroupsAsync(
107108
}
108109
catch (Exception ex)
109110
{
111+
SentrySdk.CaptureException(ex);
110112
_logger.LogError($"Error when trying to get members of IDP group {groupId}: {ex.Message}");
111113
identityIds = [];
112114
}
@@ -493,6 +495,7 @@ private async Task<ApnsResult> PushApnsAsync(DeviceIdentityRecord deviceIdentity
493495
}
494496
catch (Exception ex)
495497
{
498+
SentrySdk.CaptureException(ex);
496499
_logger.LogError(ex, "Failed to send APNs push to {deviceIdentity} for type {pushEventType} with related ID {relatedId}", deviceIdentity, eventType, relatedId);
497500
}
498501

@@ -542,6 +545,7 @@ private Message CreateAndroidFcmMessage(DeviceIdentityRecord deviceIdentity, Pus
542545
};
543546

544547
if (deviceIdentity != null)
548+
//TODO: Migrate from Firebase registration tokens to installation IDs (https://firebase.google.com/docs/cloud-messaging/manage-tokens)
545549
fcmMessage.Token = deviceIdentity.DeviceToken;
546550
else
547551
fcmMessage.Topic = $"{_globalOptions.ConventionIdentifier}-android";

src/Eurofurence.App.Server.Web/Jobs/UpdateAnnouncementsJob.cs

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,21 +53,32 @@ public async Task Execute(IJobExecutionContext context)
5353
try
5454
{
5555
string response;
56-
using (var client = new HttpClient())
56+
57+
try
5758
{
58-
var url = _announcementOptions.Url;
59-
if (string.IsNullOrWhiteSpace(url))
59+
using (var client = new HttpClient())
6060
{
61-
_logger.LogError(LogEvents.Import, "Empty source url; cancelling job");
62-
return;
61+
var url = _announcementOptions.Url;
62+
if (string.IsNullOrWhiteSpace(url))
63+
{
64+
_logger.LogError(LogEvents.Import, "Empty source url; cancelling job");
65+
return;
66+
}
67+
68+
_logger.LogDebug(LogEvents.Import, "Fetching data from {url}", url);
69+
response = await client.GetStringAsync(url);
6370
}
64-
65-
_logger.LogDebug(LogEvents.Import, "Fetching data from {url}", url);
66-
response = await client.GetStringAsync(url);
71+
}
72+
catch (Exception ex)
73+
{
74+
SentrySdk.CaptureException(ex);
75+
_logger.LogError(LogEvents.Import, "Failed to retrieve data from announcements API: {message}", ex.Message);
76+
return;
6777
}
6878

6979
if (response == "null")
7080
{
81+
SentrySdk.CaptureMessage("Received 'null' response from announcements API.", SentryLevel.Error);
7182
_logger.LogDebug(LogEvents.Import, "Received null response");
7283
return;
7384
}
@@ -147,10 +158,10 @@ public async Task Execute(IJobExecutionContext context)
147158

148159
_logger.LogInformation(LogEvents.Import, "Announcements import finished successfully.");
149160
}
150-
catch (Exception e)
161+
catch (Exception ex)
151162
{
152-
SentrySdk.CaptureException(e);
153-
_logger.LogError(LogEvents.Import, e, "Job {Name} failed with exception", context.JobDetail.Key.Name);
163+
SentrySdk.CaptureException(ex);
164+
_logger.LogError(LogEvents.Import, ex, "Job {Name} failed with exception", context.JobDetail.Key.Name);
154165
}
155166
}
156167

0 commit comments

Comments
 (0)