Feat(gh): add tracking for metrics (#833)
* add MixPanel manager like v2 * add mixpanel to send and receive * fix tests * Delete old events * Don't track receive and send operation They are already tracked by UI - we shouldn't track them on low level, they always need to be tracked with UI clicks etc * Pass account from outside * Add email if available * Add mixpanel to GH * Add ui dui3 prop as default * Remove mixpanel object from tests * renames categories * TODO notes for NodeRun later * Add note for account id nullability * Grasshopper specific send and receive info for workspace ids * Auto property * isMultiplayer prop for mixpanel * fix mismatch in account id and user id * Helper function for convertion source app name to slug --------- Co-authored-by: Adam Hathcock <adam@hathcock.uk> Co-authored-by: Claire Kuang <kuang.claire@gmail.com>
This commit is contained in:
@@ -4,11 +4,11 @@ namespace Speckle.Connectors.GrasshopperShared.Components;
|
||||
public static class ComponentCategories
|
||||
{
|
||||
public const string PRIMARY_RIBBON = "Speckle";
|
||||
public const string OPERATIONS = "1-Ops";
|
||||
public const string OBJECTS = "2-Objects";
|
||||
public const string COLLECTIONS = "3-Collections";
|
||||
public const string PARAMETERS = "4-Parameters";
|
||||
public const string DEVELOPER = "5-Dev";
|
||||
public const string OPERATIONS = " Ops";
|
||||
public const string OBJECTS = " Objects";
|
||||
public const string COLLECTIONS = " Collections";
|
||||
public const string PARAMETERS = " Params";
|
||||
public const string DEVELOPER = "Dev";
|
||||
}
|
||||
|
||||
public enum ComponentState
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
using Speckle.Connectors.Common.Operations;
|
||||
|
||||
namespace Speckle.Connectors.GrasshopperShared.Components.Operations.Receive;
|
||||
|
||||
public record GrasshopperReceiveInfo(
|
||||
string AccountId,
|
||||
Uri ServerUrl,
|
||||
string? WorkspaceId,
|
||||
string ProjectId,
|
||||
string ProjectName,
|
||||
string ModelId,
|
||||
string ModelName,
|
||||
string SelectedVersionId,
|
||||
string SourceApplication,
|
||||
string? SelectedVersionUserId
|
||||
) : ReceiveInfo(AccountId, ServerUrl, ProjectId, ProjectName, ModelId, ModelName, SelectedVersionId, SourceApplication);
|
||||
+41
-4
@@ -6,6 +6,8 @@ using Grasshopper.Kernel.Attributes;
|
||||
using GrasshopperAsyncComponent;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Rhino;
|
||||
using Speckle.Connectors.Common;
|
||||
using Speckle.Connectors.Common.Analytics;
|
||||
using Speckle.Connectors.Common.Instances;
|
||||
using Speckle.Connectors.Common.Operations;
|
||||
using Speckle.Connectors.Common.Operations.Receive;
|
||||
@@ -48,10 +50,12 @@ public class ReceiveAsyncComponent : GH_AsyncComponent
|
||||
|
||||
// DI props
|
||||
public IClient ApiClient { get; private set; }
|
||||
public MixPanelManager MixPanelManager { get; private set; }
|
||||
public GrasshopperReceiveOperation ReceiveOperation { get; private set; }
|
||||
public RootObjectUnpacker RootObjectUnpacker { get; private set; }
|
||||
public static IServiceScope? Scope { get; private set; }
|
||||
public AccountService AccountManager { get; private set; }
|
||||
public AccountService AccountService { get; private set; }
|
||||
public AccountManager AccountManager { get; private set; }
|
||||
public IClientFactory ClientFactory { get; private set; }
|
||||
|
||||
protected override void RegisterInputParams(GH_InputParamManager pManager)
|
||||
@@ -77,8 +81,11 @@ public class ReceiveAsyncComponent : GH_AsyncComponent
|
||||
// Dependency Injection
|
||||
Scope = PriorityLoader.Container.CreateScope();
|
||||
ReceiveOperation = Scope.ServiceProvider.GetRequiredService<GrasshopperReceiveOperation>();
|
||||
|
||||
MixPanelManager = Scope.ServiceProvider.GetRequiredService<MixPanelManager>();
|
||||
RootObjectUnpacker = Scope.ServiceProvider.GetService<RootObjectUnpacker>();
|
||||
AccountManager = Scope.ServiceProvider.GetRequiredService<AccountService>();
|
||||
AccountService = Scope.ServiceProvider.GetRequiredService<AccountService>();
|
||||
AccountManager = Scope.ServiceProvider.GetRequiredService<AccountManager>();
|
||||
ClientFactory = Scope.ServiceProvider.GetRequiredService<IClientFactory>();
|
||||
|
||||
// We need to call this always in here to be able to react and set events :/
|
||||
@@ -280,8 +287,11 @@ public class ReceiveAsyncComponent : GH_AsyncComponent
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO: Get any account for this server, as we don't have a mechanism yet to pass accountIds through
|
||||
Account account = AccountManager.GetAccountWithServerUrlFallback("", new Uri(urlResource.Server));
|
||||
Account? account =
|
||||
urlResource.AccountId != null
|
||||
? AccountManager.GetAccount(urlResource.AccountId)
|
||||
: AccountService.GetAccountWithServerUrlFallback("", new Uri(urlResource.Server)); // fallback the account that matches with URL if any
|
||||
|
||||
if (account is null)
|
||||
{
|
||||
throw new SpeckleAccountManagerException($"No default account was found");
|
||||
@@ -345,7 +355,9 @@ public class ReceiveComponentWorker : WorkerInstance
|
||||
da.SetData(0, Result);
|
||||
}
|
||||
|
||||
#pragma warning disable CA1506
|
||||
public override void DoWork(Action<string, double> reportProgress, Action done)
|
||||
#pragma warning restore CA1506
|
||||
{
|
||||
var receiveComponent = (ReceiveAsyncComponent)Parent;
|
||||
|
||||
@@ -438,6 +450,31 @@ public class ReceiveComponentWorker : WorkerInstance
|
||||
|
||||
Result = new SpeckleCollectionWrapperGoo(collectionRebuilder.RootCollectionWrapper);
|
||||
|
||||
// TODO: If we have NodeRun events later, better to have `ComponentTracker` to use across components
|
||||
var customProperties = new Dictionary<string, object>()
|
||||
{
|
||||
{ "isAsync", true },
|
||||
{ "sourceHostApp", HostApplications.GetSlugFromHostAppNameAndVersion(receiveInfo.SourceApplication) },
|
||||
{ "auto", receiveComponent.AutoReceive }
|
||||
};
|
||||
if (receiveInfo.WorkspaceId != null)
|
||||
{
|
||||
customProperties.Add("workspace_id", receiveInfo.WorkspaceId);
|
||||
}
|
||||
|
||||
if (receiveInfo.SelectedVersionUserId != null)
|
||||
{
|
||||
customProperties.Add(
|
||||
"isMultiplayer",
|
||||
receiveInfo.SelectedVersionUserId != receiveComponent.ApiClient.Account.userInfo.id
|
||||
);
|
||||
}
|
||||
await receiveComponent.MixPanelManager.TrackEvent(
|
||||
MixPanelEvents.Receive,
|
||||
receiveComponent.ApiClient.Account,
|
||||
customProperties
|
||||
);
|
||||
|
||||
// DONE
|
||||
done();
|
||||
});
|
||||
|
||||
+31
-5
@@ -1,5 +1,7 @@
|
||||
using Grasshopper.Kernel;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Speckle.Connectors.Common;
|
||||
using Speckle.Connectors.Common.Analytics;
|
||||
using Speckle.Connectors.Common.Instances;
|
||||
using Speckle.Connectors.Common.Operations;
|
||||
using Speckle.Connectors.Common.Operations.Receive;
|
||||
@@ -8,6 +10,7 @@ using Speckle.Connectors.GrasshopperShared.HostApp;
|
||||
using Speckle.Connectors.GrasshopperShared.Operations.Receive;
|
||||
using Speckle.Connectors.GrasshopperShared.Parameters;
|
||||
using Speckle.Connectors.GrasshopperShared.Properties;
|
||||
using Speckle.Connectors.GrasshopperShared.Registration;
|
||||
using Speckle.Sdk;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Credentials;
|
||||
@@ -34,6 +37,8 @@ public class ReceiveComponentOutput
|
||||
|
||||
public class ReceiveComponent : SpeckleScopedTaskCapableComponent<ReceiveComponentInput, ReceiveComponentOutput>
|
||||
{
|
||||
private readonly MixPanelManager _mixpanel;
|
||||
|
||||
public ReceiveComponent()
|
||||
: base(
|
||||
"(Sync) Load",
|
||||
@@ -41,7 +46,10 @@ public class ReceiveComponent : SpeckleScopedTaskCapableComponent<ReceiveCompone
|
||||
"Load a model from Speckle, synchronously",
|
||||
ComponentCategories.PRIMARY_RIBBON,
|
||||
ComponentCategories.DEVELOPER
|
||||
) { }
|
||||
)
|
||||
{
|
||||
_mixpanel = PriorityLoader.Container.GetRequiredService<MixPanelManager>();
|
||||
}
|
||||
|
||||
public override Guid ComponentGuid => new("74954F59-B1B7-41FD-97DE-4C6B005F2801");
|
||||
protected override Bitmap Icon => Resources.speckle_operations_syncload;
|
||||
@@ -102,15 +110,17 @@ public class ReceiveComponent : SpeckleScopedTaskCapableComponent<ReceiveCompone
|
||||
return new();
|
||||
}
|
||||
|
||||
// TODO: Resolving dependencies here may be overkill in most cases. Must re-evaluate.
|
||||
var accountManager = scope.ServiceProvider.GetRequiredService<AccountService>();
|
||||
var accountService = scope.ServiceProvider.GetRequiredService<AccountService>();
|
||||
var accountManager = scope.ServiceProvider.GetRequiredService<AccountManager>();
|
||||
var clientFactory = scope.ServiceProvider.GetRequiredService<IClientFactory>();
|
||||
var receiveOperation = scope.ServiceProvider.GetRequiredService<GrasshopperReceiveOperation>();
|
||||
|
||||
// Do the thing 👇🏼
|
||||
|
||||
// TODO: Get any account for this server, as we don't have a mechanism yet to pass accountIds through
|
||||
var account = accountManager.GetAccountWithServerUrlFallback("", new Uri(input.Resource.Server));
|
||||
Account? account =
|
||||
input.Resource.AccountId != null
|
||||
? accountManager.GetAccount(input.Resource.AccountId)
|
||||
: accountService.GetAccountWithServerUrlFallback("", new Uri(input.Resource.Server)); // fallback the account that matches with URL if any
|
||||
|
||||
if (account is null)
|
||||
{
|
||||
@@ -130,6 +140,22 @@ public class ReceiveComponent : SpeckleScopedTaskCapableComponent<ReceiveCompone
|
||||
.ReceiveCommitObject(receiveInfo, progress, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// TODO: If we have NodeRun events later, better to have `ComponentTracker` to use across components
|
||||
var customProperties = new Dictionary<string, object>()
|
||||
{
|
||||
{ "isAsync", false },
|
||||
{ "sourceHostApp", HostApplications.GetSlugFromHostAppNameAndVersion(receiveInfo.SourceApplication) }
|
||||
};
|
||||
if (receiveInfo.WorkspaceId != null)
|
||||
{
|
||||
customProperties.Add("workspace_id", receiveInfo.WorkspaceId);
|
||||
}
|
||||
if (receiveInfo.SelectedVersionUserId != null)
|
||||
{
|
||||
customProperties.Add("isMultiplayer", receiveInfo.SelectedVersionUserId != client.Account.userInfo.id);
|
||||
}
|
||||
await _mixpanel.TrackEvent(MixPanelEvents.Receive, account, customProperties);
|
||||
|
||||
// We need to rethink these lovely unpackers, there's a bit too many of 'em
|
||||
var rootObjectUnpacker = scope.ServiceProvider.GetService<RootObjectUnpacker>();
|
||||
var localToGlobalUnpacker = new LocalToGlobalUnpacker();
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
using Speckle.Connectors.Common.Operations;
|
||||
|
||||
namespace Speckle.Connectors.GrasshopperShared.Components.Operations.Send;
|
||||
|
||||
public record GrasshopperSendInfo(
|
||||
string AccountId,
|
||||
Uri ServerUrl,
|
||||
string? WorkspaceId,
|
||||
string ProjectId,
|
||||
string ModelId,
|
||||
string SourceApplication
|
||||
) : SendInfo(AccountId, ServerUrl, ProjectId, ModelId, SourceApplication);
|
||||
+40
-6
@@ -8,6 +8,7 @@ using Grasshopper.Kernel.Attributes;
|
||||
using GrasshopperAsyncComponent;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Rhino;
|
||||
using Speckle.Connectors.Common.Analytics;
|
||||
using Speckle.Connectors.Common.Operations;
|
||||
using Speckle.Connectors.GrasshopperShared.HostApp;
|
||||
using Speckle.Connectors.GrasshopperShared.Parameters;
|
||||
@@ -56,6 +57,7 @@ public class SendAsyncComponent : GH_AsyncComponent
|
||||
public double OverallProgress { get; set; }
|
||||
public string? Url { get; set; }
|
||||
public IClient ApiClient { get; set; }
|
||||
public MixPanelManager MixPanelManager { get; set; }
|
||||
public HostApp.SpeckleUrlModelResource? UrlModelResource { get; set; }
|
||||
public SpeckleCollectionWrapperGoo? RootCollectionWrapper { get; set; }
|
||||
|
||||
@@ -141,11 +143,13 @@ public class SendAsyncComponent : GH_AsyncComponent
|
||||
Scope = PriorityLoader.Container.CreateScope();
|
||||
SendOperation = Scope.ServiceProvider.GetRequiredService<SendOperation<SpeckleCollectionWrapperGoo>>();
|
||||
|
||||
var accountManager = Scope.ServiceProvider.GetRequiredService<AccountService>();
|
||||
MixPanelManager = Scope.ServiceProvider.GetRequiredService<MixPanelManager>();
|
||||
var accountService = Scope.ServiceProvider.GetRequiredService<AccountService>();
|
||||
var accountManager = Scope.ServiceProvider.GetRequiredService<AccountManager>();
|
||||
var clientFactory = Scope.ServiceProvider.GetRequiredService<IClientFactory>();
|
||||
|
||||
// We need to call this always in here to be able to react and set events :/
|
||||
ParseInput(da, accountManager, clientFactory);
|
||||
ParseInput(da, accountService, accountManager, clientFactory);
|
||||
|
||||
if (
|
||||
(AutoSend || CurrentComponentState == ComponentState.Ready || CurrentComponentState == ComponentState.Sending)
|
||||
@@ -225,7 +229,12 @@ public class SendAsyncComponent : GH_AsyncComponent
|
||||
base.DocumentContextChanged(document, context);
|
||||
}
|
||||
|
||||
private void ParseInput(IGH_DataAccess da, AccountService accountManager, IClientFactory clientFactory)
|
||||
private void ParseInput(
|
||||
IGH_DataAccess da,
|
||||
AccountService accountService,
|
||||
AccountManager accountManager,
|
||||
IClientFactory clientFactory
|
||||
)
|
||||
{
|
||||
HostApp.SpeckleUrlModelResource? dataInput = null;
|
||||
da.GetData(0, ref dataInput);
|
||||
@@ -239,8 +248,10 @@ public class SendAsyncComponent : GH_AsyncComponent
|
||||
UrlModelResource = dataInput;
|
||||
try
|
||||
{
|
||||
// TODO: Get any account for this server, as we don't have a mechanism yet to pass accountIds through
|
||||
Account account = accountManager.GetAccountWithServerUrlFallback("", new Uri(dataInput.Server));
|
||||
Account? account =
|
||||
dataInput.AccountId != null
|
||||
? accountManager.GetAccount(dataInput.AccountId)
|
||||
: accountService.GetAccountWithServerUrlFallback("", new Uri(dataInput.Server)); // fallback the account that matches with URL if any
|
||||
if (account is null)
|
||||
{
|
||||
throw new SpeckleAccountManagerException($"No default account was found");
|
||||
@@ -392,8 +403,31 @@ public class SendComponentWorker : WorkerInstance
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// TODO: If we have NodeRun events later, better to have `ComponentTracker` to use across components
|
||||
var customProperties = new Dictionary<string, object>()
|
||||
{
|
||||
{ "isAsync", true },
|
||||
{ "auto", sendComponent.AutoSend }
|
||||
};
|
||||
if (sendInfo.WorkspaceId != null)
|
||||
{
|
||||
customProperties.Add("workspace_id", sendInfo.WorkspaceId);
|
||||
}
|
||||
await sendComponent.MixPanelManager.TrackEvent(
|
||||
MixPanelEvents.Send,
|
||||
sendComponent.ApiClient.Account,
|
||||
customProperties
|
||||
);
|
||||
|
||||
SpeckleUrlModelVersionResource? createdVersion =
|
||||
new(sendInfo.ServerUrl.ToString(), sendInfo.ProjectId, sendInfo.ModelId, result.VersionId);
|
||||
new(
|
||||
sendInfo.AccountId,
|
||||
sendInfo.ServerUrl.ToString(),
|
||||
sendInfo.WorkspaceId,
|
||||
sendInfo.ProjectId,
|
||||
sendInfo.ModelId,
|
||||
result.VersionId
|
||||
);
|
||||
OutputParam = createdVersion;
|
||||
sendComponent.Url =
|
||||
$"{createdVersion.Server}projects/{sendInfo.ProjectId}/models/{sendInfo.ModelId}@{result.VersionId}";
|
||||
|
||||
+29
-5
@@ -1,11 +1,13 @@
|
||||
using System.Diagnostics;
|
||||
using Grasshopper.Kernel;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Speckle.Connectors.Common.Analytics;
|
||||
using Speckle.Connectors.Common.Operations;
|
||||
using Speckle.Connectors.GrasshopperShared.Components.BaseComponents;
|
||||
using Speckle.Connectors.GrasshopperShared.HostApp;
|
||||
using Speckle.Connectors.GrasshopperShared.Parameters;
|
||||
using Speckle.Connectors.GrasshopperShared.Properties;
|
||||
using Speckle.Connectors.GrasshopperShared.Registration;
|
||||
using Speckle.Sdk;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Common;
|
||||
@@ -34,6 +36,8 @@ public class SendComponentOutput(SpeckleUrlModelResource? resource)
|
||||
|
||||
public class SendComponent : SpeckleScopedTaskCapableComponent<SendComponentInput, SendComponentOutput>
|
||||
{
|
||||
private readonly MixPanelManager _mixpanel;
|
||||
|
||||
public SendComponent()
|
||||
: base(
|
||||
"(Sync) Publish",
|
||||
@@ -41,7 +45,10 @@ public class SendComponent : SpeckleScopedTaskCapableComponent<SendComponentInpu
|
||||
"Publish a collection to Speckle, synchronously",
|
||||
ComponentCategories.PRIMARY_RIBBON,
|
||||
ComponentCategories.DEVELOPER
|
||||
) { }
|
||||
)
|
||||
{
|
||||
_mixpanel = PriorityLoader.Container.GetRequiredService<MixPanelManager>();
|
||||
}
|
||||
|
||||
public override Guid ComponentGuid => new("0CF0D173-BDF0-4AC2-9157-02822B90E9FB");
|
||||
|
||||
@@ -133,12 +140,15 @@ public class SendComponent : SpeckleScopedTaskCapableComponent<SendComponentInpu
|
||||
return new(null);
|
||||
}
|
||||
|
||||
var accountManager = scope.ServiceProvider.GetRequiredService<AccountService>();
|
||||
var accountService = scope.ServiceProvider.GetRequiredService<AccountService>();
|
||||
var accountManager = scope.ServiceProvider.GetRequiredService<AccountManager>();
|
||||
var clientFactory = scope.ServiceProvider.GetRequiredService<IClientFactory>();
|
||||
var sendOperation = scope.ServiceProvider.GetRequiredService<SendOperation<SpeckleCollectionWrapperGoo>>();
|
||||
|
||||
// TODO: Get any account for this server, as we don't have a mechanism yet to pass accountIds through
|
||||
var account = accountManager.GetAccountWithServerUrlFallback("", new Uri(input.Resource.Server));
|
||||
Account? account =
|
||||
input.Resource.AccountId != null
|
||||
? accountManager.GetAccount(input.Resource.AccountId)
|
||||
: accountService.GetAccountWithServerUrlFallback("", new Uri(input.Resource.Server)); // fallback the account that matches with URL if any
|
||||
|
||||
if (account is null)
|
||||
{
|
||||
@@ -157,8 +167,22 @@ public class SendComponent : SpeckleScopedTaskCapableComponent<SendComponentInpu
|
||||
.Execute(new List<SpeckleCollectionWrapperGoo>() { input.Input }, sendInfo, progress, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// TODO: If we have NodeRun events later, better to have `ComponentTracker` to use across components
|
||||
var customProperties = new Dictionary<string, object>() { { "isAsync", false } };
|
||||
if (sendInfo.WorkspaceId != null)
|
||||
{
|
||||
customProperties.Add("workspace_id", sendInfo.WorkspaceId);
|
||||
}
|
||||
await _mixpanel.TrackEvent(MixPanelEvents.Send, account, customProperties);
|
||||
|
||||
SpeckleUrlLatestModelVersionResource createdVersionResource =
|
||||
new(sendInfo.ServerUrl.ToString(), sendInfo.ProjectId, sendInfo.ModelId);
|
||||
new(
|
||||
sendInfo.AccountId,
|
||||
sendInfo.ServerUrl.ToString(),
|
||||
sendInfo.WorkspaceId,
|
||||
sendInfo.ProjectId,
|
||||
sendInfo.ModelId
|
||||
);
|
||||
Url = $"{createdVersionResource.Server}projects/{sendInfo.ProjectId}/models/{sendInfo.ModelId}"; // TODO: missing "@VersionId"
|
||||
|
||||
return new SendComponentOutput(createdVersionResource);
|
||||
|
||||
+4
@@ -250,7 +250,9 @@ public class SpeckleSelectModelComponent : GH_Component
|
||||
da.SetData(
|
||||
0,
|
||||
new SpeckleUrlLatestModelVersionResource(
|
||||
SpeckleOperationWizard.SelectedAccount.id,
|
||||
SpeckleOperationWizard.SelectedAccount.serverInfo.url,
|
||||
SpeckleOperationWizard.SelectedWorkspace?.id,
|
||||
SpeckleOperationWizard.SelectedProject.id,
|
||||
SpeckleOperationWizard.SelectedModel.id
|
||||
)
|
||||
@@ -262,7 +264,9 @@ public class SpeckleSelectModelComponent : GH_Component
|
||||
da.SetData(
|
||||
0,
|
||||
new SpeckleUrlModelVersionResource(
|
||||
SpeckleOperationWizard.SelectedAccount.id,
|
||||
SpeckleOperationWizard.SelectedAccount.serverInfo.url,
|
||||
SpeckleOperationWizard.SelectedWorkspace?.id,
|
||||
SpeckleOperationWizard.SelectedProject.id,
|
||||
SpeckleOperationWizard.SelectedModel.id,
|
||||
SpeckleOperationWizard.SelectedVersion.id
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Speckle.Connectors.Common.Operations;
|
||||
using Speckle.Connectors.GrasshopperShared.Components.Operations.Receive;
|
||||
using Speckle.Connectors.GrasshopperShared.Components.Operations.Send;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Common;
|
||||
@@ -6,17 +7,30 @@ using Version = Speckle.Sdk.Api.GraphQL.Models.Version;
|
||||
|
||||
namespace Speckle.Connectors.GrasshopperShared.HostApp;
|
||||
|
||||
public abstract record SpeckleUrlModelResource(string Server, string ProjectId)
|
||||
// noting that if the user inputs a model url string, this will not contain account info
|
||||
// (and that's why the accountID is nullable in the record resource)
|
||||
public abstract record SpeckleUrlModelResource(string? AccountId, string Server, string? WorkspaceId, string ProjectId)
|
||||
{
|
||||
public abstract Task<ReceiveInfo> GetReceiveInfo(IClient client, CancellationToken cancellationToken = default);
|
||||
public abstract Task<GrasshopperReceiveInfo> GetReceiveInfo(
|
||||
IClient client,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
|
||||
public abstract Task<SendInfo> GetSendInfo(IClient client, CancellationToken cancellationToken = default);
|
||||
public abstract Task<GrasshopperSendInfo> GetSendInfo(IClient client, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public record SpeckleUrlLatestModelVersionResource(string Server, string ProjectId, string ModelId)
|
||||
: SpeckleUrlModelResource(Server, ProjectId)
|
||||
public record SpeckleUrlLatestModelVersionResource(
|
||||
string? AccountId,
|
||||
string Server,
|
||||
string? WorkspaceId,
|
||||
string ProjectId,
|
||||
string ModelId
|
||||
) : SpeckleUrlModelResource(AccountId, Server, WorkspaceId, ProjectId)
|
||||
{
|
||||
public override async Task<ReceiveInfo> GetReceiveInfo(IClient client, CancellationToken cancellationToken = default)
|
||||
public override async Task<GrasshopperReceiveInfo> GetReceiveInfo(
|
||||
IClient client,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
Project project = await client.Project.Get(ProjectId, cancellationToken).ConfigureAwait(false);
|
||||
ModelWithVersions model = await client
|
||||
@@ -24,29 +38,35 @@ public record SpeckleUrlLatestModelVersionResource(string Server, string Project
|
||||
.ConfigureAwait(false);
|
||||
Version version = model.versions.items[0];
|
||||
|
||||
var info = new ReceiveInfo(
|
||||
var info = new GrasshopperReceiveInfo(
|
||||
client.Account.id,
|
||||
new Uri(Server),
|
||||
project.workspaceId,
|
||||
ProjectId,
|
||||
project.name,
|
||||
ModelId,
|
||||
model.name,
|
||||
version.id,
|
||||
version.sourceApplication.NotNull()
|
||||
version.sourceApplication.NotNull(),
|
||||
version.authorUser?.id
|
||||
);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
public override async Task<SendInfo> GetSendInfo(IClient client, CancellationToken cancellationToken = default)
|
||||
public override async Task<GrasshopperSendInfo> GetSendInfo(
|
||||
IClient client,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
// We don't care about the return info, we just want to be sure we have access and everything exists.
|
||||
await client.Project.Get(ProjectId, cancellationToken).ConfigureAwait(false);
|
||||
await client.Model.Get(ModelId, ProjectId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new SendInfo(
|
||||
return new GrasshopperSendInfo(
|
||||
client.Account.id,
|
||||
new Uri(Server),
|
||||
WorkspaceId,
|
||||
ProjectId,
|
||||
ModelId,
|
||||
"Grasshopper8" // TODO: Grab from the right place!
|
||||
@@ -54,38 +74,53 @@ public record SpeckleUrlLatestModelVersionResource(string Server, string Project
|
||||
}
|
||||
}
|
||||
|
||||
public record SpeckleUrlModelVersionResource(string Server, string ProjectId, string ModelId, string VersionId)
|
||||
: SpeckleUrlModelResource(Server, ProjectId)
|
||||
public record SpeckleUrlModelVersionResource(
|
||||
string? AccountId,
|
||||
string Server,
|
||||
string? WorkspaceId,
|
||||
string ProjectId,
|
||||
string ModelId,
|
||||
string VersionId
|
||||
) : SpeckleUrlModelResource(AccountId, Server, WorkspaceId, ProjectId)
|
||||
{
|
||||
public override async Task<ReceiveInfo> GetReceiveInfo(IClient client, CancellationToken cancellationToken = default)
|
||||
public override async Task<GrasshopperReceiveInfo> GetReceiveInfo(
|
||||
IClient client,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
Project project = await client.Project.Get(ProjectId, cancellationToken).ConfigureAwait(false);
|
||||
Model model = await client.Model.Get(ModelId, ProjectId, cancellationToken).ConfigureAwait(false);
|
||||
Version version = await client.Version.Get(VersionId, ProjectId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var info = new ReceiveInfo(
|
||||
var info = new GrasshopperReceiveInfo(
|
||||
client.Account.id,
|
||||
new Uri(Server),
|
||||
project.workspaceId,
|
||||
ProjectId,
|
||||
project.name,
|
||||
ModelId,
|
||||
model.name,
|
||||
VersionId,
|
||||
version.sourceApplication.NotNull()
|
||||
version.sourceApplication.NotNull(),
|
||||
version.authorUser?.id
|
||||
);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
public override async Task<SendInfo> GetSendInfo(IClient client, CancellationToken cancellationToken = default)
|
||||
public override async Task<GrasshopperSendInfo> GetSendInfo(
|
||||
IClient client,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
// We don't care about the return info, we just want to be sure we have access and everything exists.
|
||||
await client.Project.Get(ProjectId, cancellationToken).ConfigureAwait(false);
|
||||
await client.Model.Get(ModelId, ProjectId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new SendInfo(
|
||||
return new GrasshopperSendInfo(
|
||||
client.Account.id,
|
||||
new Uri(Server),
|
||||
WorkspaceId,
|
||||
ProjectId,
|
||||
ModelId,
|
||||
"Grasshopper8" // TODO: Grab from the right place!
|
||||
@@ -93,12 +128,21 @@ public record SpeckleUrlModelVersionResource(string Server, string ProjectId, st
|
||||
}
|
||||
}
|
||||
|
||||
public record SpeckleUrlModelObjectResource(string Server, string ProjectId, string ObjectId)
|
||||
: SpeckleUrlModelResource(Server, ProjectId)
|
||||
public record SpeckleUrlModelObjectResource(
|
||||
string? AccountId,
|
||||
string Server,
|
||||
string? WorkspaceId,
|
||||
string ProjectId,
|
||||
string ObjectId
|
||||
) : SpeckleUrlModelResource(AccountId, Server, WorkspaceId, ProjectId)
|
||||
{
|
||||
public override Task<ReceiveInfo> GetReceiveInfo(IClient client, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException("Object Resources are not supported yet");
|
||||
public override Task<GrasshopperReceiveInfo> GetReceiveInfo(
|
||||
IClient client,
|
||||
CancellationToken cancellationToken = default
|
||||
) => throw new NotImplementedException("Object Resources are not supported yet");
|
||||
|
||||
public override Task<SendInfo> GetSendInfo(IClient client, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException("Object Resources are not supported yet");
|
||||
public override Task<GrasshopperSendInfo> GetSendInfo(
|
||||
IClient client,
|
||||
CancellationToken cancellationToken = default
|
||||
) => throw new NotImplementedException("Object Resources are not supported yet");
|
||||
}
|
||||
|
||||
+12
-6
@@ -48,7 +48,7 @@ public record SpeckleResourceBuilder
|
||||
throw new NotSupportedException("Federation model urls are not supported");
|
||||
}
|
||||
|
||||
var modelRes = GetUrlModelResource(serverUrl, projectId.Value, model.Value);
|
||||
var modelRes = GetUrlModelResource(null, serverUrl, null, projectId.Value, model.Value);
|
||||
|
||||
var result = new List<SpeckleUrlModelResource> { modelRes };
|
||||
|
||||
@@ -56,7 +56,7 @@ public record SpeckleResourceBuilder
|
||||
{
|
||||
foreach (Capture additionalModelsCapture in additionalModels.Captures)
|
||||
{
|
||||
var extraModel = GetUrlModelResource(serverUrl, projectId.Value, additionalModelsCapture.Value);
|
||||
var extraModel = GetUrlModelResource(null, serverUrl, null, projectId.Value, additionalModelsCapture.Value);
|
||||
result.Add(extraModel);
|
||||
}
|
||||
}
|
||||
@@ -64,19 +64,25 @@ public record SpeckleResourceBuilder
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private static SpeckleUrlModelResource GetUrlModelResource(string serverUrl, string projectId, string modelValue)
|
||||
private static SpeckleUrlModelResource GetUrlModelResource(
|
||||
string? accountId,
|
||||
string serverUrl,
|
||||
string? workspaceId,
|
||||
string projectId,
|
||||
string modelValue
|
||||
)
|
||||
{
|
||||
if (modelValue.Length == 32)
|
||||
{
|
||||
return new SpeckleUrlModelObjectResource(serverUrl, projectId, modelValue); // Model value is an ObjectID
|
||||
return new SpeckleUrlModelObjectResource(accountId, serverUrl, workspaceId, projectId, modelValue); // Model value is an ObjectID
|
||||
}
|
||||
|
||||
if (!modelValue.Contains('@'))
|
||||
{
|
||||
return new SpeckleUrlLatestModelVersionResource(serverUrl, projectId, modelValue); // Model has no version attached
|
||||
return new SpeckleUrlLatestModelVersionResource(accountId, serverUrl, workspaceId, projectId, modelValue); // Model has no version attached
|
||||
}
|
||||
|
||||
var res = modelValue.Split('@');
|
||||
return new SpeckleUrlModelVersionResource(serverUrl, projectId, res[0], res[1]);
|
||||
return new SpeckleUrlModelVersionResource(accountId, serverUrl, workspaceId, projectId, res[0], res[1]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Grasshopper.Kernel;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Speckle.Connectors.Common;
|
||||
using Speckle.Connectors.Common.Analytics;
|
||||
using Speckle.Connectors.Common.Builders;
|
||||
using Speckle.Connectors.Common.Operations;
|
||||
using Speckle.Connectors.Common.Operations.Receive;
|
||||
@@ -32,6 +33,7 @@ public class PriorityLoader : GH_AssemblyPriority
|
||||
// receive
|
||||
services.AddTransient<GrasshopperReceiveOperation>();
|
||||
services.AddTransient<AccountService>();
|
||||
services.AddSingleton<MixPanelManager>();
|
||||
services.AddSingleton(DefaultTraversal.CreateTraversalFunc());
|
||||
services.AddScoped<RootObjectUnpacker>();
|
||||
services.AddTransient<TraversalContextUnpacker>();
|
||||
|
||||
+2
@@ -24,6 +24,8 @@
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Components\Dev\DeconstructSpeckleParam.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Components\Objects\CreateSpeckleObject.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Components\Objects\CreateSpeckleProperties.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Components\Operations\Receive\GrasshopperReceiveInfo.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Components\Operations\Send\GrasshopperSendInfo.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Components\Operations\Wizard\ModelMenuHandler.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Components\Operations\Wizard\ProjectMenuHandler.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Components\Operations\Wizard\SearchToolStripMenuItem.cs" />
|
||||
|
||||
@@ -3,6 +3,7 @@ using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Speckle.Connectors.Common.Analytics;
|
||||
using Speckle.Connectors.Common.Builders;
|
||||
using Speckle.Connectors.Common.Operations;
|
||||
using Speckle.Connectors.Common.Threading;
|
||||
@@ -89,6 +90,7 @@ public class ReceiveOperationTests : MoqTest
|
||||
var receiveVersionRetriever = Create<IReceiveVersionRetriever>();
|
||||
var activityFactory = Create<ISdkActivityFactory>(MockBehavior.Loose);
|
||||
var threadContext = Create<IThreadContext>();
|
||||
var mixPanelManager = Create<IMixPanelManager>();
|
||||
|
||||
var @base = new TestBase();
|
||||
var token = "token";
|
||||
|
||||
@@ -3,6 +3,7 @@ using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Speckle.Connectors.Common.Analytics;
|
||||
using Speckle.Connectors.Common.Builders;
|
||||
using Speckle.Connectors.Common.Caching;
|
||||
using Speckle.Connectors.Common.Conversion;
|
||||
@@ -40,6 +41,7 @@ public class SendOperationTests : MoqTest
|
||||
var sendOperationVersionRecorder = Create<ISendOperationVersionRecorder>();
|
||||
var activityFactory = Create<ISdkActivityFactory>();
|
||||
var threadContext = Create<IThreadContext>();
|
||||
var mixPanelManager = Create<IMixPanelManager>();
|
||||
|
||||
var ct = new CancellationToken();
|
||||
var objects = new List<object>();
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Speckle.Connectors.Common.Analytics;
|
||||
|
||||
/// <summary>
|
||||
/// Default Mixpanel events
|
||||
/// </summary>
|
||||
public enum MixPanelEvents
|
||||
{
|
||||
/// <summary>
|
||||
/// Event triggered when data is sent to a Speckle Server
|
||||
/// </summary>
|
||||
Send,
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when data is received from a Speckle Server
|
||||
/// </summary>
|
||||
Receive
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Sdk;
|
||||
using Speckle.Sdk.Credentials;
|
||||
using Speckle.Sdk.Helpers;
|
||||
#if NETFRAMEWORK
|
||||
using System.Net.Http;
|
||||
#endif
|
||||
|
||||
namespace Speckle.Connectors.Common.Analytics;
|
||||
|
||||
/// <summary>
|
||||
/// Anonymous telemetry to help us understand how to make a better Speckle.
|
||||
/// This really helps us to deliver a better open source project and product!
|
||||
/// </summary>
|
||||
[GenerateAutoInterface]
|
||||
public class MixPanelManager(ISpeckleApplication application, ISpeckleHttp speckleHttp, ILogger<MixPanelManager> logger)
|
||||
: IMixPanelManager
|
||||
{
|
||||
private const string MIXPANEL_TOKEN = "acd87c5a50b56df91a795e999812a3a4";
|
||||
private static readonly Uri s_mixpanelServer = new("https://analytics.speckle.systems");
|
||||
|
||||
/// <summary>
|
||||
/// Cached email
|
||||
/// </summary>
|
||||
private string? LastEmail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cached server URL
|
||||
/// </summary>
|
||||
private string? LastServer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <see langword="false"/> when the DEBUG pre-processor directive is <see langword="true"/>, <see langword="false"/> otherwise
|
||||
/// </summary>
|
||||
/// <remarks>This must be kept as a computed property, not a compile time const</remarks>
|
||||
private static bool IsReleaseMode =>
|
||||
#if DEBUG
|
||||
false;
|
||||
#else
|
||||
true;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Tracks an event without specifying the email and server.
|
||||
/// It's not always possible to know which account the user has selected, especially in visual programming.
|
||||
/// Therefore we are caching the email and server values so that they can be used also when nodes such as "Serialize" are used.
|
||||
/// If no account info is cached, we use the default account data.
|
||||
/// </summary>
|
||||
/// <param name="eventName">Name of the even</param>
|
||||
/// <param name="customProperties">Additional parameters to pass in to event</param>
|
||||
/// <param name="isAction">True if it's an action performed by a logged user</param>
|
||||
public async Task TrackEvent(
|
||||
MixPanelEvents eventName,
|
||||
Account? account,
|
||||
Dictionary<string, object>? customProperties = null,
|
||||
bool isAction = true
|
||||
)
|
||||
{
|
||||
string? email = account?.userInfo.email;
|
||||
string? hashedEmail;
|
||||
string? server;
|
||||
|
||||
if (LastEmail != null && LastServer != null && LastServer != "no-account-server")
|
||||
{
|
||||
hashedEmail = LastEmail;
|
||||
server = LastServer;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (account == null)
|
||||
{
|
||||
var macAddr = NetworkInterface
|
||||
.GetAllNetworkInterfaces()
|
||||
.Where(nic =>
|
||||
nic.OperationalStatus == OperationalStatus.Up && nic.NetworkInterfaceType != NetworkInterfaceType.Loopback
|
||||
)
|
||||
.Select(nic => nic.GetPhysicalAddress().ToString())
|
||||
.FirstOrDefault();
|
||||
|
||||
hashedEmail = macAddr;
|
||||
server = "no-account-server";
|
||||
isAction = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
hashedEmail = account.GetHashedEmail();
|
||||
server = account.GetHashedServer();
|
||||
}
|
||||
}
|
||||
|
||||
await TrackEvent(hashedEmail, server, eventName, email, customProperties, isAction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tracks an event from a specified email and server, anonymizes personal information
|
||||
/// </summary>
|
||||
/// <param name="hashedEmail">Email of the user anonymized</param>
|
||||
/// <param name="hashedServer">Server URL anonymized</param>
|
||||
/// <param name="eventName">Name of the event</param>
|
||||
/// <param name="customProperties">Additional parameters to pass to the event</param>
|
||||
/// <param name="isAction">True if it's an action performed by a logged user</param>
|
||||
private async Task TrackEvent(
|
||||
string? hashedEmail,
|
||||
string hashedServer,
|
||||
MixPanelEvents eventName,
|
||||
string? email,
|
||||
Dictionary<string, object>? customProperties = null,
|
||||
bool isAction = true
|
||||
)
|
||||
{
|
||||
LastEmail = hashedEmail;
|
||||
LastServer = hashedServer;
|
||||
|
||||
if (!IsReleaseMode)
|
||||
{
|
||||
//only track in prod
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var properties = new Dictionary<string, object>
|
||||
{
|
||||
{ "distinct_id", hashedEmail ?? string.Empty },
|
||||
{ "server_id", hashedServer },
|
||||
{ "token", MIXPANEL_TOKEN },
|
||||
{ "hostApp", application.Slug },
|
||||
{ "ui", "dui3" }, // this is the convention we use with next gen
|
||||
{ "hostAppVersion", application.HostApplicationVersion },
|
||||
{ "core_version", application.SpeckleVersion },
|
||||
{ "$os", GetOs() }
|
||||
};
|
||||
|
||||
if (email != null)
|
||||
{
|
||||
properties.Add("email", email);
|
||||
}
|
||||
|
||||
if (isAction)
|
||||
{
|
||||
properties.Add("type", "action");
|
||||
}
|
||||
|
||||
if (customProperties != null)
|
||||
{
|
||||
foreach (KeyValuePair<string, object> customProp in customProperties)
|
||||
{
|
||||
properties[customProp.Key] = customProp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
string json = JsonConvert.SerializeObject(new { @event = eventName.ToString(), properties });
|
||||
await SendAnalytics("/track?ip=1", json).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
{
|
||||
logger.LogWarning(
|
||||
ex,
|
||||
"Analytics event {event} {isAction} failed {exceptionMessage}",
|
||||
eventName.ToString(),
|
||||
isAction,
|
||||
ex.Message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AddConnectorToProfile(string hashedEmail, string connector)
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
{ "$token", MIXPANEL_TOKEN },
|
||||
{ "$distinct_id", hashedEmail },
|
||||
{
|
||||
"$union",
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{
|
||||
"Connectors",
|
||||
new List<string> { connector }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
string json = JsonConvert.SerializeObject(data);
|
||||
await SendAnalytics("/engage#profile-union", json).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
{
|
||||
logger.LogWarning(ex, "Failed add connector {connector} to profile", connector);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task IdentifyProfile(string hashedEmail, string connector)
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
{ "$token", MIXPANEL_TOKEN },
|
||||
{ "$distinct_id", hashedEmail },
|
||||
{
|
||||
"$set",
|
||||
new Dictionary<string, object> { { "Identified", true } }
|
||||
}
|
||||
};
|
||||
string json = JsonConvert.SerializeObject(data);
|
||||
|
||||
await SendAnalytics("/engage#profile-set", json).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
{
|
||||
logger.LogWarning(ex, "Failed identify profile: connector {connector}", connector);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendAnalytics(string relativeUri, string json)
|
||||
{
|
||||
var query = new StreamContent(new MemoryStream(Encoding.UTF8.GetBytes("data=" + HttpUtility.UrlEncode(json))));
|
||||
using HttpClient client = speckleHttp.CreateHttpClient();
|
||||
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain"));
|
||||
query.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||
var res = await client.PostAsync(new Uri(s_mixpanelServer, relativeUri), query).ConfigureAwait(false);
|
||||
res.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
private static string GetOs()
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return "Windows";
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
return "Mac OS X";
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
return "Linux";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.Connectors.Common.Analytics;
|
||||
using Speckle.Connectors.Common.Cancellation;
|
||||
using Speckle.Connectors.Common.Operations;
|
||||
using Speckle.Connectors.Common.Operations.Receive;
|
||||
@@ -19,6 +20,7 @@ public static class ContainerRegistration
|
||||
serviceCollection.AddScoped<RootObjectUnpacker>();
|
||||
serviceCollection.AddScoped<ReceiveOperation>();
|
||||
serviceCollection.AddSingleton<IAccountService, AccountService>();
|
||||
serviceCollection.AddSingleton<IMixPanelManager, MixPanelManager>();
|
||||
|
||||
serviceCollection.AddTransient(typeof(ILogger<>), typeof(Logger<>));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Speckle.Sdk;
|
||||
using System.Text.RegularExpressions;
|
||||
using Speckle.Sdk;
|
||||
|
||||
namespace Speckle.Connectors.Common;
|
||||
|
||||
@@ -39,4 +40,65 @@ public static class HostApplications
|
||||
Navisworks = new("Navisworks", "navisworks"),
|
||||
AdvanceSteel = new("Advance Steel", "advancesteel"),
|
||||
Other = new("Other", "other");
|
||||
|
||||
/// <summary>
|
||||
/// Gets a slug from a host application name and version.
|
||||
/// </summary>
|
||||
/// <param name="appName">Application name with its version, e.g., "Rhino 7", "Revit 2024".</param>
|
||||
/// <returns>Slug string.</returns>
|
||||
public static string GetSlugFromHostAppNameAndVersion(string appName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(appName))
|
||||
{
|
||||
return "other";
|
||||
}
|
||||
|
||||
// Remove whitespace and convert to lowercase
|
||||
appName = Regex.Replace(appName.ToLowerInvariant(), @"\s+", "");
|
||||
|
||||
var keywords = new List<string>
|
||||
{
|
||||
"dynamo",
|
||||
"revit",
|
||||
"autocad",
|
||||
"civil",
|
||||
"rhino",
|
||||
"grasshopper",
|
||||
"unity",
|
||||
"gsa",
|
||||
"microstation",
|
||||
"openroads",
|
||||
"openrail",
|
||||
"openbuildings",
|
||||
"etabs",
|
||||
"sap",
|
||||
"csibridge",
|
||||
"safe",
|
||||
"teklastructures",
|
||||
"dxf",
|
||||
"excel",
|
||||
"unreal",
|
||||
"powerbi",
|
||||
"blender",
|
||||
"qgis",
|
||||
"arcgis",
|
||||
"sketchup",
|
||||
"archicad",
|
||||
"topsolid",
|
||||
"python",
|
||||
"net",
|
||||
"navisworks",
|
||||
"advancesteel"
|
||||
};
|
||||
|
||||
foreach (var keyword in keywords)
|
||||
{
|
||||
if (appName.Contains(keyword))
|
||||
{
|
||||
return keyword;
|
||||
}
|
||||
}
|
||||
|
||||
return appName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ public sealed class ReceiveOperation(
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await receiveVersionRetriever.VersionReceived(account, version, receiveInfo, cancellationToken);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,6 @@ public sealed class SendOperation<T>(
|
||||
|
||||
// 8 - Create the version (commit)
|
||||
var versionId = await sendOperationVersionRecorder.RecordVersion(sendResult.RootId, sendInfo, account, ct);
|
||||
|
||||
return (sendResult, versionId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<ProjectReference Include="..\Speckle.Connectors.Logging\Speckle.Connectors.Logging.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Web" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(Configuration)' == 'Local'">
|
||||
<ProjectReference Include="..\..\..\speckle-sharp-sdk\src\Speckle.Sdk\Speckle.Sdk.csproj" />
|
||||
<ProjectReference Include="..\..\..\speckle-sharp-sdk\src\Speckle.Objects\Speckle.Objects.csproj" />
|
||||
|
||||
Reference in New Issue
Block a user