From 3a4af64a64be6f3506d5c12927c4412f95ea38b4 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Tue, 15 Sep 2026 16:08:14 -0300 Subject: [PATCH] fix(observability): preserve backend release identity (#114) --- .../SentryObservabilityTests.cs | 70 ++++++++++++++++++- .../Observability/SentryObservability.cs | 43 +++++++++--- docs/adr/0002-sentry-observability.md | 7 +- scripts/package-elastic-beanstalk.sh | 1 + 4 files changed, 108 insertions(+), 13 deletions(-) diff --git a/Api.SeaHavenIndustries.Tests/SentryObservabilityTests.cs b/Api.SeaHavenIndustries.Tests/SentryObservabilityTests.cs index cb760e8..4b83b8d 100644 --- a/Api.SeaHavenIndustries.Tests/SentryObservabilityTests.cs +++ b/Api.SeaHavenIndustries.Tests/SentryObservabilityTests.cs @@ -1,3 +1,4 @@ +using System.Reflection; using System.Security.Claims; using Api.SeaHavenIndustries.Observability; using FluentAssertions; @@ -14,6 +15,7 @@ namespace Api.SeaHavenIndustries.Tests; public class SentryObservabilityTests { private static readonly string ValidSha = new string('a', 40); + private static readonly string UpperSha = "ABCDEF0123456789ABCDEF0123456789ABCDEF01"; [Fact] public void ResolveRelease_Accepts_ServicePrefixed40HexCommit() @@ -23,15 +25,57 @@ public class SentryObservabilityTests } [Theory] + [InlineData(null)] [InlineData("1.0.0")] [InlineData("shoc-backend@abc")] [InlineData("shoc-backend@not-a-sha-at-all")] - [InlineData("other-service@" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] + [InlineData("other-service@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] + [InlineData("SHOC-BACKEND@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] [InlineData("")] - public void ResolveRelease_FallsBackToLocalDevelopment_ForNonReleaseVersion(string version) + [InlineData("shoc-backend@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] + [InlineData("shoc-backend@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] + [InlineData("shoc-backend@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")] + [InlineData("shoc-backend@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+dirty")] + [InlineData("shoc-backend@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+")] + [InlineData("shoc-backend@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa++")] + [InlineData("shoc-backend@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+extra")] + public void ResolveRelease_FallsBackToLocalDevelopment_ForNonReleaseVersion(string? version) { SentryObservability.ResolveRelease(version) .Should().Be(SentryObservability.LocalDevelopmentRelease); + SentryObservability.ResolveCommitSha(version) + .Should().BeNull(); + } + + [Fact] + public void ResolveRelease_Accepts_SdkAppendedMatchingSourceRevisionSuffix() + { + var informationalVersion = "shoc-backend@" + ValidSha + "+" + ValidSha; + + SentryObservability.ResolveRelease(informationalVersion) + .Should().Be("shoc-backend@" + ValidSha); + SentryObservability.ResolveCommitSha(informationalVersion) + .Should().Be(ValidSha); + } + + [Fact] + public void ResolveRelease_Accepts_SuffixDifferingOnlyInCase_FromTheCommitSha() + { + var canonicalSha = UpperSha.ToLowerInvariant(); + + SentryObservability.ResolveRelease("shoc-backend@" + UpperSha + "+" + canonicalSha) + .Should().Be("shoc-backend@" + canonicalSha); + } + + [Fact] + public void ResolveRelease_NormalizesUppercaseCommitShaToLowercase() + { + var canonicalSha = UpperSha.ToLowerInvariant(); + + SentryObservability.ResolveRelease("shoc-backend@" + UpperSha) + .Should().Be("shoc-backend@" + canonicalSha); + SentryObservability.ResolveCommitSha("shoc-backend@" + UpperSha) + .Should().Be(canonicalSha); } [Fact] @@ -39,6 +83,28 @@ public class SentryObservabilityTests { var release = SentryObservability.ResolveRelease("shoc-backend@" + ValidSha); release[(SentryObservability.ServiceName.Length + 1)..].Should().Be(ValidSha); + SentryObservability.ResolveCommitSha("shoc-backend@" + ValidSha) + .Should().Be(ValidSha); + } + + [Fact] + public void ResolveRelease_AndCommitSha_FromNonReleaseAssembly_FallBackSafely() + { + var assembly = typeof(SentryObservabilityTests).Assembly; + + SentryObservability.ResolveRelease(assembly) + .Should().Be(SentryObservability.LocalDevelopmentRelease); + SentryObservability.ResolveCommitSha(assembly) + .Should().BeNull(); + } + + [Fact] + public void ResolveRelease_AndCommitSha_FromNullAssembly_FallBackSafely() + { + SentryObservability.ResolveRelease((Assembly?)null) + .Should().Be(SentryObservability.LocalDevelopmentRelease); + SentryObservability.ResolveCommitSha((Assembly?)null) + .Should().BeNull(); } [Fact] diff --git a/Api.SeaHavenIndustries/Observability/SentryObservability.cs b/Api.SeaHavenIndustries/Observability/SentryObservability.cs index 0a1ec56..f58f8f7 100644 --- a/Api.SeaHavenIndustries/Observability/SentryObservability.cs +++ b/Api.SeaHavenIndustries/Observability/SentryObservability.cs @@ -29,21 +29,46 @@ public static class SentryObservability .GetCustomAttribute()? .InformationalVersion; - return IsReleaseInformationalVersion(informationalVersion) - ? informationalVersion![ReleasePrefix.Length..] - : null; + return ResolveCommitSha(informationalVersion); } public static string ResolveRelease(string? informationalVersion) => - IsReleaseInformationalVersion(informationalVersion) - ? informationalVersion! + TryResolveCommitSha(informationalVersion, out var commitSha) + ? ReleasePrefix + commitSha : LocalDevelopmentRelease; + public static string? ResolveCommitSha(string? informationalVersion) => + TryResolveCommitSha(informationalVersion, out var commitSha) + ? commitSha + : null; + public static bool IsReleaseInformationalVersion(string? value) => - value is not null - && value.StartsWith(ReleasePrefix, StringComparison.Ordinal) - && value.Length == ReleasePrefix.Length + CommitShaLength - && value[ReleasePrefix.Length..].All(c => c is >= '0' and <= '9' or >= 'a' and <= 'f' or >= 'A' and <= 'F'); + TryResolveCommitSha(value, out _); + + private static bool TryResolveCommitSha(string? value, out string? commitSha) + { + commitSha = null; + + if (value is null || !value.StartsWith(ReleasePrefix, StringComparison.Ordinal)) + return false; + + var body = value[ReleasePrefix.Length..]; + var suffixSeparatorIndex = body.IndexOf('+'); + var sha = suffixSeparatorIndex < 0 ? body : body[..suffixSeparatorIndex]; + + if (sha.Length != CommitShaLength || !sha.All(IsCommitShaHexDigit)) + return false; + + if (suffixSeparatorIndex >= 0 + && !string.Equals(body[(suffixSeparatorIndex + 1)..], sha, StringComparison.OrdinalIgnoreCase)) + return false; + + commitSha = sha.ToLowerInvariant(); + return true; + } + + private static bool IsCommitShaHexDigit(char c) => + c is >= '0' and <= '9' or >= 'a' and <= 'f' or >= 'A' and <= 'F'; public static void ConfigureRequest(IHub hub, HttpContext context) { diff --git a/docs/adr/0002-sentry-observability.md b/docs/adr/0002-sentry-observability.md index 6bf7c52..37a2a78 100644 --- a/docs/adr/0002-sentry-observability.md +++ b/docs/adr/0002-sentry-observability.md @@ -14,11 +14,14 @@ route values, query strings, or identity claims. ## Decision 1. **Release identity.** `SentryObservability.ResolveRelease(Assembly)` accepts an - informational version shaped `shoc-backend@<40 hex commit sha>` and otherwise - resolves to `local-development`. `scripts/package-elastic-beanstalk.sh` resolves + informational version shaped `shoc-backend@<40 hex commit sha>`, tolerates the + .NET SDK's matching `+` suffix for compatibility, canonicalizes the + result to lowercase, and otherwise resolves to `local-development`. + `scripts/package-elastic-beanstalk.sh` resolves the commit from `APP_COMMIT_SHA` → `GITHUB_SHA` → `git rev-parse HEAD`, fails when `CI=true` cannot produce a valid 40-hex sha, and publishes with `-p:SourceRevisionId`, `-p:InformationalVersion=shoc-backend@`, + `-p:IncludeSourceRevisionInInformationalVersion=false`, `-p:DebugType=portable`, `-p:DebugSymbols=true`. 2. **Default tags.** `service=shoc-backend` and `app.commit=` on `SentryOptions.DefaultTags`. All events use the same commit-addressed diff --git a/scripts/package-elastic-beanstalk.sh b/scripts/package-elastic-beanstalk.sh index 27e1288..bef20cf 100755 --- a/scripts/package-elastic-beanstalk.sh +++ b/scripts/package-elastic-beanstalk.sh @@ -112,6 +112,7 @@ log "publish $API_PROJECT (Release, self-contained, $RUNTIME)" -p:UseAppHost=true \ -p:SourceRevisionId="$COMMIT_SHA" \ -p:InformationalVersion="shoc-backend@$COMMIT_SHA" \ + -p:IncludeSourceRevisionInInformationalVersion=false \ -p:DebugType=portable \ -p:DebugSymbols=true