Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions Compendium.sln
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Architecture", "Architectur
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compendium.ArchitectureTests", "tests\Architecture\Compendium.ArchitectureTests\Compendium.ArchitectureTests.csproj", "{D685E8D8-B3DC-4A65-9ED3-675776610190}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compendium.Adapters.Shared", "src\Adapters\Compendium.Adapters.Shared\Compendium.Adapters.Shared.csproj", "{DE527E82-7509-4E3F-B002-8D53CFAA97DA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -232,6 +234,10 @@ Global
{D685E8D8-B3DC-4A65-9ED3-675776610190}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D685E8D8-B3DC-4A65-9ED3-675776610190}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D685E8D8-B3DC-4A65-9ED3-675776610190}.Release|Any CPU.Build.0 = Release|Any CPU
{DE527E82-7509-4E3F-B002-8D53CFAA97DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DE527E82-7509-4E3F-B002-8D53CFAA97DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DE527E82-7509-4E3F-B002-8D53CFAA97DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DE527E82-7509-4E3F-B002-8D53CFAA97DA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{FE421F00-7FFD-4666-A961-F1FF325ECD34} = {E35C8F52-5000-4427-9589-AEB5987C1AC6}
Expand Down Expand Up @@ -278,5 +284,6 @@ Global
{C4495E66-500F-4582-B56D-C30ED0F41FF3} = {A0005488-4247-4872-9CD5-95696E47BEA5}
{72B1C880-12A5-4568-85E0-3800536158DC} = {B23A5693-C266-43DC-8D3E-CBB108131762}
{D685E8D8-B3DC-4A65-9ED3-675776610190} = {72B1C880-12A5-4568-85E0-3800536158DC}
{DE527E82-7509-4E3F-B002-8D53CFAA97DA} = {73261E87-8FCA-40B6-940B-E25CBDBE33FB}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// </copyright>
// -----------------------------------------------------------------------

using System.Diagnostics;
using Compendium.Adapters.LemonSqueezy.Configuration;
using Compendium.Adapters.LemonSqueezy.Http;
using Compendium.Adapters.LemonSqueezy.Http.Models;
Expand Down Expand Up @@ -130,7 +131,7 @@ public async Task<Result<BillingCustomer>> GetCustomerByEmailAsync(
{
ArgumentNullException.ThrowIfNull(email);

_logger.LogDebug("Getting customer by email {Email}", email);
_logger.LogDebug("Getting customer by email (activity {ActivityId})", Activity.Current?.Id);

var result = await _httpClient.ListCustomersAsync(email, cancellationToken);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// </copyright>
// -----------------------------------------------------------------------

using System.Diagnostics;
using Compendium.Adapters.Listmonk.Configuration;
using Compendium.Adapters.Listmonk.Http;
using Compendium.Adapters.Listmonk.Http.Models;
Expand Down Expand Up @@ -40,7 +41,7 @@ public async Task<Result<Subscriber>> SubscribeAsync(
{
ArgumentNullException.ThrowIfNull(request);

_logger.LogInformation("Subscribing {Email} to newsletter", request.Email);
_logger.LogInformation("Subscribing to newsletter (activity {ActivityId})", Activity.Current?.Id);

var listIds = new List<int>();

Expand Down Expand Up @@ -74,12 +75,12 @@ public async Task<Result<Subscriber>> SubscribeAsync(

if (result.IsFailure)
{
_logger.LogWarning("Failed to subscribe {Email}: {Error}", request.Email, result.Error.Message);
_logger.LogWarning("Failed to subscribe (activity {ActivityId}): {Error}", Activity.Current?.Id, result.Error.Message);
return result.Error;
}

var subscriber = MapToSubscriber(result.Value);
_logger.LogInformation("Subscribed {Email} with ID {SubscriberId}", request.Email, subscriber.Id);
_logger.LogInformation("Subscribed with ID {SubscriberId}", subscriber.Id);

return subscriber;
}
Expand All @@ -92,7 +93,7 @@ public async Task<Result> UnsubscribeAsync(
{
ArgumentNullException.ThrowIfNull(email);

_logger.LogInformation("Unsubscribing {Email} from list {ListId}", email, listId ?? "all");
_logger.LogInformation("Unsubscribing from list {ListId} (activity {ActivityId})", listId ?? "all", Activity.Current?.Id);

// First, find the subscriber by email
var subscriberResult = await _httpClient.GetSubscriberByEmailAsync(email, cancellationToken);
Expand All @@ -111,12 +112,12 @@ public async Task<Result> UnsubscribeAsync(

if (result.IsFailure)
{
_logger.LogWarning("Failed to unsubscribe {Email} from list {ListId}: {Error}",
email, listId, result.Error.Message);
_logger.LogWarning("Failed to unsubscribe {SubscriberId} from list {ListId}: {Error}",
subscriberId, listId, result.Error.Message);
}
else
{
_logger.LogInformation("Unsubscribed {Email} from list {ListId}", email, listId);
_logger.LogInformation("Unsubscribed {SubscriberId} from list {ListId}", subscriberId, listId);
}

return result;
Expand All @@ -133,11 +134,11 @@ public async Task<Result> UnsubscribeAsync(

if (result.IsFailure)
{
_logger.LogWarning("Failed to unsubscribe {Email}: {Error}", email, result.Error.Message);
_logger.LogWarning("Failed to unsubscribe {SubscriberId}: {Error}", subscriberId, result.Error.Message);
}
else
{
_logger.LogInformation("Unsubscribed {Email} from all lists", email);
_logger.LogInformation("Unsubscribed {SubscriberId} from all lists", subscriberId);
}

return result.IsSuccess ? Result.Success() : result.Error;
Expand All @@ -151,7 +152,7 @@ public async Task<Result<Subscriber>> GetSubscriberAsync(
{
ArgumentNullException.ThrowIfNull(email);

_logger.LogDebug("Getting subscriber by email {Email}", email);
_logger.LogDebug("Getting subscriber by email (activity {ActivityId})", Activity.Current?.Id);

var result = await _httpClient.GetSubscriberByEmailAsync(email, cancellationToken);

Expand All @@ -172,7 +173,7 @@ public async Task<Result> UpdateSubscriberAttributesAsync(
ArgumentNullException.ThrowIfNull(email);
ArgumentNullException.ThrowIfNull(attributes);

_logger.LogInformation("Updating attributes for subscriber {Email}", email);
_logger.LogInformation("Updating subscriber attributes (activity {ActivityId})", Activity.Current?.Id);

// First, find the subscriber by email
var subscriberResult = await _httpClient.GetSubscriberByEmailAsync(email, cancellationToken);
Expand All @@ -191,12 +192,12 @@ public async Task<Result> UpdateSubscriberAttributesAsync(

if (result.IsFailure)
{
_logger.LogWarning("Failed to update attributes for {Email}: {Error}",
email, result.Error.Message);
_logger.LogWarning("Failed to update attributes for {SubscriberId}: {Error}",
subscriberResult.Value.Id, result.Error.Message);
}
else
{
_logger.LogInformation("Updated attributes for subscriber {Email}", email);
_logger.LogInformation("Updated attributes for subscriber {SubscriberId}", subscriberResult.Value.Id);
}

return result.IsSuccess ? Result.Success() : result.Error;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<AssemblyName>Compendium.Adapters.Shared</AssemblyName>
<RootNamespace>Compendium.Adapters.Shared</RootNamespace>
<PackageId>Compendium.Adapters.Shared</PackageId>
<Description>Shared utilities for Compendium adapters: PII masking helpers, logging conventions.</Description>
</PropertyGroup>

</Project>
30 changes: 30 additions & 0 deletions src/Adapters/Compendium.Adapters.Shared/Logging/PiiMasking.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// -----------------------------------------------------------------------
// <copyright file="PiiMasking.cs" company="Sassy Solutions">
// Copyright (c) 2026 Sassy Solutions. Licensed under the MIT License.
// See LICENSE in the project root for license information.
// </copyright>
// -----------------------------------------------------------------------

namespace Compendium.Adapters.Shared.Logging;

/// <summary>
/// PII masking helpers for log statements. Use sparingly — prefer non-PII identifiers
/// (subscriber_id, customer_id, activity_id) over masked PII per GDPR data-minimization.
/// </summary>
public static class PiiMasking
{
/// <summary>
/// Masks an email for logging: "john.doe@acme.com" → "j***@acme.com".
/// Returns "&lt;empty&gt;" or "&lt;null&gt;" for non-values.
/// Use only when subscriber_id/customer_id is unavailable AND email correlation is required for debugging.
/// </summary>
/// <param name="email">The email to mask.</param>
/// <returns>Masked email, or a placeholder for empty/invalid input.</returns>
public static string MaskEmail(string? email)
{
if (string.IsNullOrWhiteSpace(email)) return "<empty>";
var atIndex = email.IndexOf('@');
if (atIndex <= 0) return "***";
return $"{email[0]}***{email[atIndex..]}";
Comment on lines +17 to +28

Copilot AI Apr 25, 2026

Copy link

Choose a reason for hiding this comment

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

The XML docs say this returns "" or "" for non-values, but the implementation returns "" for both null and whitespace, and returns "***" for invalid formats (no '@'). Please align the docs with the actual behavior, or update the implementation to distinguish null vs empty (and document the invalid-email case).

Copilot uses AI. Check for mistakes.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// </copyright>
// -----------------------------------------------------------------------

using System.Diagnostics;
using Compendium.Adapters.Stripe.Configuration;
using Stripe.Checkout;

Expand Down Expand Up @@ -131,7 +132,7 @@ public async Task<Result<BillingCustomer>> GetCustomerByEmailAsync(
}
catch (StripeException ex)
{
_logger.LogError(ex, "Stripe customer lookup by email failed for {Email}", email);
_logger.LogError(ex, "Stripe customer lookup by email failed (activity {ActivityId})", Activity.Current?.Id);
return Result.Failure<BillingCustomer>(
Error.Failure("Billing.Stripe.GetCustomerByEmailFailed", ex.Message));
}
Expand Down Expand Up @@ -184,7 +185,7 @@ public async Task<Result<BillingCustomer>> UpsertCustomerAsync(
}
catch (StripeException ex)
{
_logger.LogError(ex, "Stripe customer upsert failed for {Email}", request.Email);
_logger.LogError(ex, "Stripe customer upsert failed (activity {ActivityId})", Activity.Current?.Id);
return Result.Failure<BillingCustomer>(
Error.Failure("Billing.Stripe.UpsertCustomerFailed", ex.Message));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ public async Task<Result<IdentityUser>> GetUserByEmailAsync(

// POM-170: debug logs also flow into centralised log stores, so use the
// short hash here as well.
_logger.LogDebug("Getting user by email (hash {EmailHashPrefix})", HashPrefix(email));
_logger.LogDebug("Getting user by email (hash {HashPrefix})", HashPrefix(email));

Copilot AI Apr 25, 2026

Copy link

Choose a reason for hiding this comment

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

This placeholder was renamed to {HashPrefix} here, but the same file still contains log templates using {EmailHashPrefix} (e.g., in CreateUserAsync). That leaves {Email placeholders in adapter logs and contradicts the PR description/verification grep. Please update the remaining templates to use {HashPrefix} as well for consistency and to keep the grep clean.

Copilot uses AI. Check for mistakes.

var searchRequest = new ZitadelUserSearchRequest
{
Expand Down
Loading