From 105b85049a0acef24d0019a86c7bd35feef268a5 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:22:08 -0500 Subject: [PATCH 1/2] Add SyncMetrics --- src/Api/Platform/Sync/SyncMetrics.cs | 23 ++++++++++++ src/Api/Startup.cs | 4 +++ src/Api/Vault/Controllers/SyncController.cs | 17 +++++---- .../Vault/Controllers/SyncControllerTests.cs | 1 + .../Attributes/MeterCustomizeAttribute.cs | 35 +++++++++++++++++++ 5 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 src/Api/Platform/Sync/SyncMetrics.cs create mode 100644 test/Common/AutoFixture/Attributes/MeterCustomizeAttribute.cs diff --git a/src/Api/Platform/Sync/SyncMetrics.cs b/src/Api/Platform/Sync/SyncMetrics.cs new file mode 100644 index 000000000000..616c487cf994 --- /dev/null +++ b/src/Api/Platform/Sync/SyncMetrics.cs @@ -0,0 +1,23 @@ +using System.Diagnostics.Metrics; + +namespace Bit.Api.Platform.Sync; + +public sealed class SyncMetrics +{ + private readonly Histogram _syncVaultCount; + + public SyncMetrics(IMeterFactory meterFactory) + { + var meter = meterFactory.Create("Bitwarden.Sync"); + _syncVaultCount = meter.CreateHistogram( + "bitwarden.sync.vault_count", + unit: "{item}", + description: "The number of ciphers returned in the sync operation." + ); + } + + public void RecordSyncInfo(int cipherCount) + { + _syncVaultCount.Record(cipherCount); + } +} diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 7ac9c2813950..5f6c7edf4f26 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -34,6 +34,8 @@ using Bit.Core.Auth.IdentityServer; using Bit.Core.Auth.Identity; using Bit.Core.Enums; +using Bit.Api.Platform.Sync; + #if !OSS @@ -179,6 +181,8 @@ public void ConfigureServices(IServiceCollection services) .AddScoped, IEnumerable>, DeviceRotationValidator>(); + services.TryAddSingleton(); + // Services services.AddBaseServices(globalSettings); services.AddDefaultServices(globalSettings); diff --git a/src/Api/Vault/Controllers/SyncController.cs b/src/Api/Vault/Controllers/SyncController.cs index b186e4b60116..98be420492cf 100644 --- a/src/Api/Vault/Controllers/SyncController.cs +++ b/src/Api/Vault/Controllers/SyncController.cs @@ -1,6 +1,4 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - +using Bit.Api.Platform.Sync; using Bit.Api.Vault.Models.Response; using Bit.Core; using Bit.Core.AdminConsole.Entities; @@ -47,6 +45,7 @@ public class SyncController : Controller private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository; private readonly IUserAccountKeysQuery _userAccountKeysQuery; + private readonly SyncMetrics _syncMetrics; public SyncController( IUserService userService, @@ -64,7 +63,8 @@ public SyncController( IApplicationCacheService applicationCacheService, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IWebAuthnCredentialRepository webAuthnCredentialRepository, - IUserAccountKeysQuery userAccountKeysQuery) + IUserAccountKeysQuery userAccountKeysQuery, + SyncMetrics syncMetrics) { _userService = userService; _folderRepository = folderRepository; @@ -82,6 +82,7 @@ public SyncController( _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _webAuthnCredentialRepository = webAuthnCredentialRepository; _userAccountKeysQuery = userAccountKeysQuery; + _syncMetrics = syncMetrics; } [HttpGet("")] @@ -107,8 +108,8 @@ await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id, var ciphers = FilterSSHKeys(allCiphers); var sends = await _sendRepository.GetManyByUserIdAsync(user.Id); - IEnumerable collections = null; - IDictionary> collectionCiphersGroupDict = null; + IEnumerable? collections = null; + IDictionary>? collectionCiphersGroupDict = null; IEnumerable policies = await _policyRepository.GetManyByUserIdAsync(user.Id); if (hasEnabledOrgs) @@ -128,13 +129,15 @@ await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id, ? await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id) : []; - UserAccountKeysData userAccountKeys = null; + UserAccountKeysData? userAccountKeys = null; // JIT TDE users and some broken/old users may not have a private key. if (!string.IsNullOrWhiteSpace(user.PrivateKey)) { userAccountKeys = await _userAccountKeysQuery.Run(user); } + _syncMetrics.RecordSyncInfo(ciphers.Count); + var response = new SyncResponseModel(_globalSettings, user, userAccountKeys, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationAbilities, organizationIdsClaimingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails, folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends, webAuthnCredentials); diff --git a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs index e6d34592c7d1..db4a06a14de9 100644 --- a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs @@ -33,6 +33,7 @@ namespace Bit.Api.Test.Controllers; [ControllerCustomize(typeof(SyncController))] +[MeterCustomize] [SutProviderCustomize] public class SyncControllerTests { diff --git a/test/Common/AutoFixture/Attributes/MeterCustomizeAttribute.cs b/test/Common/AutoFixture/Attributes/MeterCustomizeAttribute.cs new file mode 100644 index 000000000000..d939c9d27f76 --- /dev/null +++ b/test/Common/AutoFixture/Attributes/MeterCustomizeAttribute.cs @@ -0,0 +1,35 @@ +using System.Diagnostics.Metrics; +using AutoFixture; +using NSubstitute; + +namespace Bit.Test.Common.AutoFixture.Attributes; + +/// +/// Customizes a to be able to actually create 's. +/// +public class MeterCustomizeAttribute : BitCustomizeAttribute +{ + private static readonly MeterCustomization _meterCustomization = new(); + public override ICustomization GetCustomization() => _meterCustomization; + + private class MeterCustomization : ICustomization + { + public void Customize(IFixture fixture) + { + fixture.Customize(factory => + { + return factory.FromFactory(() => + { + var fakeFactory = Substitute.For(); + fakeFactory.Create(Arg.Any()) + .Returns((call) => + { + return new Meter(call.Arg()); + }); + + return fakeFactory; + }); + }); + } + } +} From 31f5a3e8ceccdb5878b60f090186f71b63d305be Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:22:24 -0500 Subject: [PATCH 2/2] Add Aspire Dashboard to dev docker compose --- dev/docker-compose.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index b3d2b0bb5bf5..4a0d79425cea 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -147,6 +147,14 @@ services: - redis - cloud + telemetry-dashboard: + image: mcr.microsoft.com/dotnet/aspire-dashboard:latest + ports: + - "18888:18888" + - "4317:18889" + profiles: + - telemetry + volumes: mssql_dev_data: postgres_dev_data: