diff --git a/src/CodingWithCalvin.CouchbaseExplorer/CodingWithCalvin.CouchbaseExplorer.csproj b/src/CodingWithCalvin.CouchbaseExplorer/CodingWithCalvin.CouchbaseExplorer.csproj
index 1fe2e0d..950da09 100644
--- a/src/CodingWithCalvin.CouchbaseExplorer/CodingWithCalvin.CouchbaseExplorer.csproj
+++ b/src/CodingWithCalvin.CouchbaseExplorer/CodingWithCalvin.CouchbaseExplorer.csproj
@@ -9,11 +9,8 @@
true
-
- True
-
-
+
diff --git a/src/CodingWithCalvin.CouchbaseExplorer/CouchbaseExplorerPackage.cs b/src/CodingWithCalvin.CouchbaseExplorer/CouchbaseExplorerPackage.cs
index 82fc684..026629b 100644
--- a/src/CodingWithCalvin.CouchbaseExplorer/CouchbaseExplorerPackage.cs
+++ b/src/CodingWithCalvin.CouchbaseExplorer/CouchbaseExplorerPackage.cs
@@ -1,7 +1,8 @@
-using System;
+using System;
using System.Runtime.InteropServices;
using System.Threading;
using CodingWithCalvin.CouchbaseExplorer.Editors;
+using CodingWithCalvin.Otel4Vsix;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
@@ -32,6 +33,20 @@ IProgress progress
{
await JoinableTaskFactory.SwitchToMainThreadAsync();
+ var builder = VsixTelemetry.Configure()
+ .WithServiceName(VsixInfo.DisplayName)
+ .WithServiceVersion(VsixInfo.Version)
+ .WithVisualStudioAttributes(this)
+ .WithEnvironmentAttributes();
+
+#if !DEBUG
+ builder
+ .WithOtlpHttp("https://api.honeycomb.io")
+ .WithHeader("x-honeycomb-team", HoneycombConfig.ApiKey);
+#endif
+
+ builder.Initialize();
+
// Register the editor factory
_editorFactory = new DocumentEditorFactory();
RegisterEditorFactory(_editorFactory);
@@ -43,6 +58,7 @@ protected override void Dispose(bool disposing)
{
if (disposing)
{
+ VsixTelemetry.Shutdown();
_editorFactory?.Dispose();
}
base.Dispose(disposing);
diff --git a/src/CodingWithCalvin.CouchbaseExplorer/CouchbaseExplorerWindowCommand.cs b/src/CodingWithCalvin.CouchbaseExplorer/CouchbaseExplorerWindowCommand.cs
index d7a2cdc..c0052a2 100644
--- a/src/CodingWithCalvin.CouchbaseExplorer/CouchbaseExplorerWindowCommand.cs
+++ b/src/CodingWithCalvin.CouchbaseExplorer/CouchbaseExplorerWindowCommand.cs
@@ -1,5 +1,7 @@
-using System;
+using System;
+using System.Collections.Generic;
using System.ComponentModel.Design;
+using CodingWithCalvin.Otel4Vsix;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
@@ -44,14 +46,30 @@ public static void Initialize(Package package)
private void ShowToolWindow(object sender, EventArgs e)
{
- var window = this._package.FindToolWindow(typeof(CouchbaseExplorerWindow), 0, true);
- if (window?.Frame == null)
+ using var activity = VsixTelemetry.StartCommandActivity("CouchbaseExplorer.ShowToolWindow");
+
+ try
{
- throw new NotSupportedException("Cannot create tool window");
- }
+ var window = this._package.FindToolWindow(typeof(CouchbaseExplorerWindow), 0, true);
+ if (window?.Frame == null)
+ {
+ throw new NotSupportedException("Cannot create tool window");
+ }
+
+ var windowFrame = (IVsWindowFrame)window.Frame;
+ Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(windowFrame.Show());
- var windowFrame = (IVsWindowFrame)window.Frame;
- Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(windowFrame.Show());
+ VsixTelemetry.LogInformation("Couchbase Explorer tool window shown");
+ }
+ catch (Exception ex)
+ {
+ activity?.RecordError(ex);
+ VsixTelemetry.TrackException(ex, new Dictionary
+ {
+ { "operation.name", "ShowToolWindow" }
+ });
+ throw;
+ }
}
}
}
diff --git a/src/CodingWithCalvin.CouchbaseExplorer/HoneycombConfig.cs b/src/CodingWithCalvin.CouchbaseExplorer/HoneycombConfig.cs
new file mode 100644
index 0000000..710eb77
--- /dev/null
+++ b/src/CodingWithCalvin.CouchbaseExplorer/HoneycombConfig.cs
@@ -0,0 +1,7 @@
+namespace CodingWithCalvin.CouchbaseExplorer
+{
+ internal static class HoneycombConfig
+ {
+ public const string ApiKey = "PLACEHOLDER";
+ }
+}
diff --git a/src/CodingWithCalvin.CouchbaseExplorer/Services/CouchbaseService.cs b/src/CodingWithCalvin.CouchbaseExplorer/Services/CouchbaseService.cs
index b2a3767..7621184 100644
--- a/src/CodingWithCalvin.CouchbaseExplorer/Services/CouchbaseService.cs
+++ b/src/CodingWithCalvin.CouchbaseExplorer/Services/CouchbaseService.cs
@@ -3,6 +3,7 @@
using System.Linq;
using System.Net;
using System.Threading.Tasks;
+using CodingWithCalvin.Otel4Vsix;
using Couchbase;
using Couchbase.Management.Buckets;
@@ -52,64 +53,90 @@ public static class CouchbaseService
public static async Task ConnectAsync(string connectionId, string connectionString, string username, string password, bool useSsl)
{
- // Enable TLS 1.2/1.3 explicitly for .NET Framework
- ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls13;
+ using var activity = VsixTelemetry.StartCommandActivity("CouchbaseService.ConnectAsync");
- if (_connections.TryGetValue(connectionId, out var existing))
+ try
{
- return existing;
- }
+ activity?.SetTag("connection.isCapella", connectionString.Contains(".cloud.couchbase.com"));
- var options = new ClusterOptions
- {
- UserName = username,
- Password = password,
- KvTimeout = TimeSpan.FromSeconds(10),
- ManagementTimeout = TimeSpan.FromSeconds(10),
- QueryTimeout = TimeSpan.FromSeconds(10)
- };
+ // Enable TLS 1.2/1.3 explicitly for .NET Framework
+ ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls13;
- // Check if this is a Capella connection
- var isCapella = connectionString.Contains(".cloud.couchbase.com");
+ if (_connections.TryGetValue(connectionId, out var existing))
+ {
+ VsixTelemetry.LogInformation("Reusing existing connection");
+ return existing;
+ }
- if (useSsl || isCapella)
- {
- options.EnableTls = true;
- }
+ var options = new ClusterOptions
+ {
+ UserName = username,
+ Password = password,
+ KvTimeout = TimeSpan.FromSeconds(10),
+ ManagementTimeout = TimeSpan.FromSeconds(10),
+ QueryTimeout = TimeSpan.FromSeconds(10)
+ };
- // Capella-specific configuration
- if (isCapella)
- {
- options.EnableDnsSrvResolution = true;
- options.KvIgnoreRemoteCertificateNameMismatch = true;
- options.HttpIgnoreRemoteCertificateMismatch = true;
- options.ForceIPv4 = true;
- }
+ // Check if this is a Capella connection
+ var isCapella = connectionString.Contains(".cloud.couchbase.com");
- // Build connection string with protocol
- var fullConnectionString = connectionString;
- if (!connectionString.StartsWith("couchbase://") && !connectionString.StartsWith("couchbases://"))
- {
- fullConnectionString = useSsl ? $"couchbases://{connectionString}" : $"couchbase://{connectionString}";
- }
+ if (useSsl || isCapella)
+ {
+ options.EnableTls = true;
+ }
- // Connect on background thread to avoid UI blocking
- var cluster = await Task.Run(async () =>
- {
- return await Cluster.ConnectAsync(fullConnectionString, options);
- }).ConfigureAwait(true);
+ // Capella-specific configuration
+ if (isCapella)
+ {
+ options.EnableDnsSrvResolution = true;
+ options.KvIgnoreRemoteCertificateNameMismatch = true;
+ options.HttpIgnoreRemoteCertificateMismatch = true;
+ options.ForceIPv4 = true;
+ }
+
+ // Build connection string with protocol
+ var fullConnectionString = connectionString;
+ if (!connectionString.StartsWith("couchbase://") && !connectionString.StartsWith("couchbases://"))
+ {
+ fullConnectionString = useSsl ? $"couchbases://{connectionString}" : $"couchbase://{connectionString}";
+ }
- var connection = new ClusterConnection(cluster);
- _connections[connectionId] = connection;
- return connection;
+ VsixTelemetry.LogInformation("Connecting to Couchbase cluster");
+
+ // Connect on background thread to avoid UI blocking
+ var cluster = await Task.Run(async () =>
+ {
+ return await Cluster.ConnectAsync(fullConnectionString, options);
+ }).ConfigureAwait(true);
+
+ var connection = new ClusterConnection(cluster);
+ _connections[connectionId] = connection;
+
+ VsixTelemetry.LogInformation("Successfully connected to Couchbase cluster");
+
+ return connection;
+ }
+ catch (Exception ex)
+ {
+ activity?.RecordError(ex);
+ VsixTelemetry.TrackException(ex, new Dictionary
+ {
+ { "operation.name", "ConnectAsync" },
+ { "connection.id", connectionId }
+ });
+ throw;
+ }
}
public static async Task DisconnectAsync(string connectionId)
{
- if (_connections.TryGetValue(connectionId, out var connection))
+ using var activity = VsixTelemetry.StartCommandActivity("CouchbaseService.DisconnectAsync");
+
+ if (_connections.TryGetValue(connectionId, out var connection))
{
_connections.Remove(connectionId);
connection.Dispose();
+ VsixTelemetry.LogInformation("Disconnected from Couchbase cluster");
}
}
@@ -121,43 +148,82 @@ public static ClusterConnection GetConnection(string connectionId)
public static async Task> GetBucketsAsync(string connectionId)
{
- var connection = GetConnection(connectionId);
- if (connection == null)
+ using var activity = VsixTelemetry.StartCommandActivity("CouchbaseService.GetBucketsAsync");
+
+ try
{
- throw new InvalidOperationException("Not connected to cluster");
- }
+ var connection = GetConnection(connectionId);
+ if (connection == null)
+ {
+ throw new InvalidOperationException("Not connected to cluster");
+ }
- var buckets = await connection.Cluster.Buckets.GetAllBucketsAsync();
+ var buckets = await connection.Cluster.Buckets.GetAllBucketsAsync();
- return buckets.Values.Select(b => new BucketInfo
+ var result = buckets.Values.Select(b => new BucketInfo
+ {
+ Name = b.Name,
+ BucketType = b.BucketType,
+ RamQuotaMB = (long)(b.RamQuotaMB),
+ NumReplicas = b.NumReplicas
+ }).OrderBy(b => b.Name).ToList();
+
+ activity?.SetTag("buckets.count", result.Count);
+
+ return result;
+ }
+ catch (Exception ex)
{
- Name = b.Name,
- BucketType = b.BucketType,
- RamQuotaMB = (long)(b.RamQuotaMB),
- NumReplicas = b.NumReplicas
- }).OrderBy(b => b.Name).ToList();
+ activity?.RecordError(ex);
+ VsixTelemetry.TrackException(ex, new Dictionary
+ {
+ { "operation.name", "GetBucketsAsync" },
+ { "connection.id", connectionId }
+ });
+ throw;
+ }
}
public static async Task> GetScopesAsync(string connectionId, string bucketName)
{
- var connection = GetConnection(connectionId);
- if (connection == null)
+ using var activity = VsixTelemetry.StartCommandActivity("CouchbaseService.GetScopesAsync");
+
+ try
{
- throw new InvalidOperationException("Not connected to cluster");
- }
+ var connection = GetConnection(connectionId);
+ if (connection == null)
+ {
+ throw new InvalidOperationException("Not connected to cluster");
+ }
+
+ var bucket = await connection.Cluster.BucketAsync(bucketName);
+ var scopes = await bucket.Collections.GetAllScopesAsync();
+
+ var result = scopes.Select(s => new ScopeInfo
+ {
+ Name = s.Name,
+ Collections = s.Collections.Select(c => new CollectionInfo
+ {
+ Name = c.Name,
+ ScopeName = s.Name
+ }).OrderBy(c => c.Name).ToList()
+ }).OrderBy(s => s.Name).ToList();
- var bucket = await connection.Cluster.BucketAsync(bucketName);
- var scopes = await bucket.Collections.GetAllScopesAsync();
+ activity?.SetTag("scopes.count", result.Count);
- return scopes.Select(s => new ScopeInfo
+ return result;
+ }
+ catch (Exception ex)
{
- Name = s.Name,
- Collections = s.Collections.Select(c => new CollectionInfo
+ activity?.RecordError(ex);
+ VsixTelemetry.TrackException(ex, new Dictionary
{
- Name = c.Name,
- ScopeName = s.Name
- }).OrderBy(c => c.Name).ToList()
- }).OrderBy(s => s.Name).ToList();
+ { "operation.name", "GetScopesAsync" },
+ { "connection.id", connectionId },
+ { "bucket.name", bucketName }
+ });
+ throw;
+ }
}
public static async Task> GetCollectionsAsync(string connectionId, string bucketName, string scopeName)
@@ -169,56 +235,95 @@ public static async Task> GetCollectionsAsync(string connec
public static async Task GetDocumentIdsAsync(string connectionId, string bucketName, string scopeName, string collectionName, int limit = 50, int offset = 0)
{
- var connection = GetConnection(connectionId);
- if (connection == null)
+ using var activity = VsixTelemetry.StartCommandActivity("CouchbaseService.GetDocumentIdsAsync");
+
+ try
{
- throw new InvalidOperationException("Not connected to cluster");
- }
+ var connection = GetConnection(connectionId);
+ if (connection == null)
+ {
+ throw new InvalidOperationException("Not connected to cluster");
+ }
- var query = $"SELECT META().id FROM `{bucketName}`.`{scopeName}`.`{collectionName}` ORDER BY META().id LIMIT {limit + 1} OFFSET {offset}";
+ var query = $"SELECT META().id FROM `{bucketName}`.`{scopeName}`.`{collectionName}` ORDER BY META().id LIMIT {limit + 1} OFFSET {offset}";
- var result = await connection.Cluster.QueryAsync(query);
- var documentIds = new List();
+ var result = await connection.Cluster.QueryAsync(query);
+ var documentIds = new List();
- await foreach (var row in result.Rows)
- {
- documentIds.Add(row.Id);
- }
+ await foreach (var row in result.Rows)
+ {
+ documentIds.Add(row.Id);
+ }
- // Check if there are more documents (we fetched limit+1 to check)
- var hasMore = documentIds.Count > limit;
- if (hasMore)
- {
- documentIds.RemoveAt(documentIds.Count - 1);
- }
+ // Check if there are more documents (we fetched limit+1 to check)
+ var hasMore = documentIds.Count > limit;
+ if (hasMore)
+ {
+ documentIds.RemoveAt(documentIds.Count - 1);
+ }
+
+ activity?.SetTag("documents.count", documentIds.Count);
+ activity?.SetTag("documents.hasMore", hasMore);
- return new DocumentQueryResult
+ return new DocumentQueryResult
+ {
+ DocumentIds = documentIds,
+ HasMore = hasMore
+ };
+ }
+ catch (Exception ex)
{
- DocumentIds = documentIds,
- HasMore = hasMore
- };
+ activity?.RecordError(ex);
+ VsixTelemetry.TrackException(ex, new Dictionary
+ {
+ { "operation.name", "GetDocumentIdsAsync" },
+ { "connection.id", connectionId },
+ { "bucket.name", bucketName },
+ { "scope.name", scopeName },
+ { "collection.name", collectionName }
+ });
+ throw;
+ }
}
public static async Task GetDocumentAsync(string connectionId, string bucketName, string scopeName, string collectionName, string documentId)
{
- var connection = GetConnection(connectionId);
- if (connection == null)
+ using var activity = VsixTelemetry.StartCommandActivity("CouchbaseService.GetDocumentAsync");
+
+ try
{
- throw new InvalidOperationException("Not connected to cluster");
- }
+ var connection = GetConnection(connectionId);
+ if (connection == null)
+ {
+ throw new InvalidOperationException("Not connected to cluster");
+ }
+
+ var bucket = await connection.Cluster.BucketAsync(bucketName);
+ var scope = bucket.Scope(scopeName);
+ var collection = scope.Collection(collectionName);
- var bucket = await connection.Cluster.BucketAsync(bucketName);
- var scope = bucket.Scope(scopeName);
- var collection = scope.Collection(collectionName);
+ var result = await collection.GetAsync(documentId);
- var result = await collection.GetAsync(documentId);
+ VsixTelemetry.LogInformation("Retrieved document");
- return new DocumentContent
+ return new DocumentContent
+ {
+ Id = documentId,
+ Content = result.ContentAs