├── strong-name-key.snk ├── cmd.txt ├── src └── Mixpanel │ ├── Mixpanel.Tests │ ├── MixpanelClient │ │ ├── People │ │ │ ├── DistinctIdType.cs │ │ │ ├── PeopleSuperPropsDetails.cs │ │ │ ├── PeopleSuperPropsAttribute.cs │ │ │ └── MixpanelClientPeopleTestsBase.cs │ │ ├── Track │ │ │ ├── TrackSuperPropsDetails.cs │ │ │ ├── TrackSuperPropsAttribute.cs │ │ │ ├── MixpanelClientTrackTestsBase.cs │ │ │ └── MixpanelClientAliasTests.cs │ │ ├── MixpanelClientSendJsonTests.cs │ │ ├── MixpanelClientTestsBase.cs │ │ ├── HttpMockMixpanelConfig.cs │ │ ├── MixpanelClientSendTests.cs │ │ └── MixpanelClientHttpTests.cs │ ├── Mixpanel.Tests.Unit.csproj.DotSettings │ ├── Parsers │ │ ├── DurationParserTests.cs │ │ ├── IpParserTests.cs │ │ ├── DistinctIdParserTests.cs │ │ ├── CollectionParserTests.cs │ │ └── TimeParserTests.cs │ ├── Mixpanel.Tests.Unit.csproj │ ├── MixpanelTestsBase.cs │ └── MessageBuilders │ │ ├── People │ │ ├── PeopleTestsBase.cs │ │ ├── PeopleTrackChargeMessageBuilderTests.cs │ │ ├── PeopleAppendMessageBuilderTests.cs │ │ ├── PeopleRemoveMessageBuilderTests.cs │ │ ├── PeopleAddMessageBuilderTests.cs │ │ ├── PeopleDeleteMessageBuilderTests.cs │ │ ├── PeopleUnionMessageBuilderTests.cs │ │ └── PeopleUnsetMessageBuilderTests.cs │ │ └── Track │ │ └── AliasMessageBuilderTests.cs │ ├── Mixpanel │ ├── MessageProperties │ │ ├── PropertyOrigin.cs │ │ ├── PropertyNameSource.cs │ │ ├── TrackSpecialProperty.cs │ │ ├── ObjectProperty.cs │ │ ├── PeopleSpecialProperty.cs │ │ ├── TrackSpecialPropertyMapper.cs │ │ ├── PeopleSpecialPropertyMapper.cs │ │ ├── PropertyNameFormatter.cs │ │ └── PropertiesDigger.cs │ ├── Exceptions │ │ ├── MixpanelConfigurationException.cs │ │ └── MixpanelMessageBuildException.cs │ ├── MessageBuilders │ │ ├── BatchMessageBuildResult.cs │ │ ├── People │ │ │ ├── PeopleAddMessageBuilder.cs │ │ │ ├── PeopleAppendMessageBuilder.cs │ │ │ ├── PeopleRemoveMessageBuilder.cs │ │ │ ├── PeopleUnionMessageBuilder.cs │ │ │ ├── PeopleDeleteMessageBuilder.cs │ │ │ ├── PeopleTrackChargeMessageBuilder.cs │ │ │ ├── PeopleUnsetMessageBuilder.cs │ │ │ └── PeopleSetMessageBuilder.cs │ │ ├── MessageBuildResult.cs │ │ ├── Track │ │ │ ├── TrackMessageBuilderBase.cs │ │ │ ├── AliasMessageBuilder.cs │ │ │ └── TrackMessageBuilder.cs │ │ └── MessageCandidate.cs │ ├── MixpanelDataResidencyHandling.cs │ ├── Parsers │ │ ├── PropertyNameComparer.cs │ │ ├── BoolParser.cs │ │ ├── StringParser.cs │ │ ├── ValueParseResult.cs │ │ ├── NumberParser.cs │ │ ├── DurationParser.cs │ │ ├── DistinctIdParser.cs │ │ ├── GenericPropertyParser.cs │ │ ├── IpParser.cs │ │ ├── TrackSpecialPropertyParser.cs │ │ ├── CollectionParser.cs │ │ ├── PeopleSpecialPropertyParser.cs │ │ └── TimeParser.cs │ ├── MixpanelIpAddressHandling.cs │ ├── MixpanelMessageEndpoint.cs │ ├── MixpanelMessage.cs │ ├── DefaultHttpClient.cs │ ├── SendResult.cs │ ├── MixpanelMessageTest.cs │ ├── MixpanelNameAttribute.cs │ ├── MixpanelBatchMessageTest.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── MixpanelPropertyNameFormat.cs │ ├── SendResultInternal.cs │ ├── BatchMessageWrapper.cs │ ├── MixpanelProperty.cs │ ├── MixpanelConfig.cs │ ├── ConfigHelper.cs │ ├── MessageKind.cs │ └── Mixpanel.csproj │ ├── Benchmarks │ ├── Program.cs │ ├── Benchmarks.csproj │ └── Json │ │ ├── MessageCreator.cs │ │ ├── DictionaryBenchmark.cs │ │ └── DictionaryArrayBenchmark.cs │ ├── Mixpanel.sln.DotSettings │ ├── Mixpanel.Tests.Integration │ ├── UriHelper.cs │ ├── Mixpanel.Tests.Integration.csproj │ ├── SecretsProvider.cs │ ├── DefaultHttpClient │ │ └── DefaultHttpClientTests.cs │ ├── MixpanelClient │ │ └── MixpanelClientTrackTests.cs │ └── MixpanelExportApi.cs │ └── Mixpanel.sln ├── .gitignore ├── LICENSE ├── nuget-readme.md └── README.md /strong-name-key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eealeivan/mixpanel-csharp/HEAD/strong-name-key.snk -------------------------------------------------------------------------------- /cmd.txt: -------------------------------------------------------------------------------- 1 | Execute in \mixpanel-csharp\src\Mixpanel\Mixpanel 2 | 3 | dotnet clean -c Release 4 | dotnet build -c Release 5 | dotnet pack -c Release --include-source --include-symbols -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/MixpanelClient/People/DistinctIdType.cs: -------------------------------------------------------------------------------- 1 | namespace Mixpanel.Tests.Unit.MixpanelClient.People 2 | { 3 | public enum DistinctIdType 4 | { 5 | Argument, 6 | SuperProps 7 | } 8 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MessageProperties/PropertyOrigin.cs: -------------------------------------------------------------------------------- 1 | namespace Mixpanel.MessageProperties 2 | { 3 | internal enum PropertyOrigin 4 | { 5 | Parameter, 6 | SuperProperty, 7 | RawProperty 8 | } 9 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MessageProperties/PropertyNameSource.cs: -------------------------------------------------------------------------------- 1 | namespace Mixpanel.MessageProperties 2 | { 3 | internal enum PropertyNameSource 4 | { 5 | Default, 6 | DataMember, 7 | MixpanelName 8 | } 9 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/MixpanelClient/Track/TrackSuperPropsDetails.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Mixpanel.Tests.Unit.MixpanelClient.Track 4 | { 5 | [Flags] 6 | public enum TrackSuperPropsDetails 7 | { 8 | DistinctId = 0x0, 9 | SpecialProperties = 0x1, 10 | UserProperties = 0x2, 11 | All = DistinctId | SpecialProperties | UserProperties 12 | } 13 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/MixpanelClient/People/PeopleSuperPropsDetails.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Mixpanel.Tests.Unit.MixpanelClient.People 4 | { 5 | [Flags] 6 | public enum PeopleSuperPropsDetails 7 | { 8 | DistinctId = 0x0, 9 | MessageSpecialProperties = 0x1, 10 | UserProperties = 0x2, 11 | All = DistinctId | MessageSpecialProperties | UserProperties 12 | } 13 | } -------------------------------------------------------------------------------- /src/Mixpanel/Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BenchmarkDotNet.Running; 3 | using Benchmarks.Json; 4 | 5 | namespace Benchmarks 6 | { 7 | class Program 8 | { 9 | static void Main(string[] args) 10 | { 11 | var summary1 = BenchmarkRunner.Run(); 12 | var summary2 = BenchmarkRunner.Run(); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/Exceptions/MixpanelConfigurationException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Mixpanel.Exceptions 4 | { 5 | /// 6 | /// Exception indicates that Mixpanel is not properly configured. 7 | /// 8 | public class MixpanelConfigurationException : Exception 9 | { 10 | internal MixpanelConfigurationException(string message) : base(message) 11 | { 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/Mixpanel.Tests.Unit.csproj.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | CSharp90 -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MessageBuilders/BatchMessageBuildResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace Mixpanel.MessageBuilders 5 | { 6 | internal sealed class BatchMessageBuildResult 7 | { 8 | public IDictionary[] Message { get; } 9 | 10 | public BatchMessageBuildResult(IEnumerable messages) 11 | { 12 | Message = messages.Select(message => message.Data).ToArray(); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MixpanelDataResidencyHandling.cs: -------------------------------------------------------------------------------- 1 | namespace Mixpanel 2 | { 3 | /// 4 | /// Controls which API host to route data to. 5 | /// 6 | public enum MixpanelDataResidencyHandling 7 | { 8 | /// 9 | /// Route data to Mixpanel's US servers. 10 | /// 11 | Default, 12 | 13 | /// 14 | /// Route data to Mixpanel's EU servers. 15 | /// 16 | EU, 17 | } 18 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/MixpanelClient/Track/TrackSuperPropsAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | 4 | namespace Mixpanel.Tests.Unit.MixpanelClient.Track 5 | { 6 | [AttributeUsage(AttributeTargets.Method)] 7 | public class TrackSuperPropsAttribute : PropertyAttribute 8 | { 9 | public const string Name = "TrackSuperProps"; 10 | 11 | public TrackSuperPropsAttribute(TrackSuperPropsDetails details) 12 | : base(details) 13 | { 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | True 3 | True -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/MixpanelClient/People/PeopleSuperPropsAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | 4 | namespace Mixpanel.Tests.Unit.MixpanelClient.People 5 | { 6 | [AttributeUsage(AttributeTargets.Method)] 7 | public class PeopleSuperPropsAttribute : PropertyAttribute 8 | { 9 | public const string Name = "PeopleSuperProps"; 10 | 11 | public PeopleSuperPropsAttribute(PeopleSuperPropsDetails details) 12 | : base(details) 13 | { 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/Parsers/PropertyNameComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Mixpanel.Parsers 5 | { 6 | internal class PropertyNameComparer : IEqualityComparer 7 | { 8 | public bool Equals(string x, string y) 9 | { 10 | return StringComparer.OrdinalIgnoreCase.Equals(x, y); 11 | } 12 | 13 | public int GetHashCode(string obj) 14 | { 15 | return StringComparer.OrdinalIgnoreCase.GetHashCode(obj); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build Folders (you can keep bin if you'd like, to store dlls and pdbs) 2 | [Bb]in/ 3 | [Oo]bj/ 4 | images/ 5 | package/ 6 | TestResult.xml 7 | 8 | # VisualStudio files 9 | *.suo 10 | *.user 11 | .vs/ 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Rr]elease/ 16 | x64/ 17 | *_i.c 18 | *_p.c 19 | *.ilk 20 | *.meta 21 | *.obj 22 | *.pch 23 | *.pdb 24 | *.pgc 25 | *.pgd 26 | *.rsp 27 | *.sbr 28 | *.tlb 29 | *.tli 30 | *.tlh 31 | *.tmp 32 | *.log 33 | *.vspscc 34 | *.vssscc 35 | .builds 36 | 37 | # Visual Studio profiler 38 | *.psess 39 | *.vsp 40 | *.vspx -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/Exceptions/MixpanelMessageBuildException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Mixpanel.Exceptions 4 | { 5 | /// 6 | /// Exception indicates that Mixpanel was not able to build a message from provided data. 7 | /// Typically it happens when some required properties are not provided or have a wrong format. 8 | /// 9 | public class MixpanelMessageBuildException : Exception 10 | { 11 | internal MixpanelMessageBuildException(string message) : base(message) 12 | { 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MessageProperties/TrackSpecialProperty.cs: -------------------------------------------------------------------------------- 1 | namespace Mixpanel.MessageProperties 2 | { 3 | internal static class TrackSpecialProperty 4 | { 5 | public const string Token = "token"; 6 | public const string DistinctId = "distinct_id"; 7 | public const string Time = "time"; 8 | public const string Ip = "ip"; 9 | public const string Duration = "$duration"; 10 | public const string Os = "$os"; 11 | public const string ScreenWidth = "$screen_width"; 12 | public const string ScreenHeight = "$screen_height"; 13 | } 14 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/Parsers/BoolParser.cs: -------------------------------------------------------------------------------- 1 | namespace Mixpanel.Parsers 2 | { 3 | internal static class BoolParser 4 | { 5 | public static ValueParseResult Parse(object rawValue) 6 | { 7 | switch (rawValue) 8 | { 9 | case null: 10 | return ValueParseResult.CreateFail("Can't be null."); 11 | 12 | case bool _: 13 | return ValueParseResult.CreateSuccess(rawValue); 14 | 15 | default: 16 | return ValueParseResult.CreateFail("Expected type is: bool."); 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/Parsers/StringParser.cs: -------------------------------------------------------------------------------- 1 | namespace Mixpanel.Parsers 2 | { 3 | internal static class StringParser 4 | { 5 | public static ValueParseResult Parse(object rawValue) 6 | { 7 | switch (rawValue) 8 | { 9 | case null: 10 | return ValueParseResult.CreateFail("Can't be null."); 11 | case string _: 12 | return ValueParseResult.CreateSuccess(rawValue); 13 | default: 14 | return ValueParseResult.CreateFail("Expected type is: string."); 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MixpanelIpAddressHandling.cs: -------------------------------------------------------------------------------- 1 | namespace Mixpanel 2 | { 3 | /// 4 | /// Controls request "ip" query string parameter. 5 | /// 6 | public enum MixpanelIpAddressHandling 7 | { 8 | /// 9 | /// "ip" query string parameter will not be added to requests. 10 | /// 11 | None, 12 | 13 | /// 14 | /// "ip=1" query string parameter will be added to each request. 15 | /// 16 | UseRequestIp, 17 | 18 | /// 19 | /// "ip=0" query string parameter will be added to each request. 20 | /// 21 | IgnoreRequestIp 22 | } 23 | } -------------------------------------------------------------------------------- /src/Mixpanel/Benchmarks/Benchmarks.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | True 7 | ..\..\..\strong-name-key.snk 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MixpanelMessageEndpoint.cs: -------------------------------------------------------------------------------- 1 | namespace Mixpanel 2 | { 3 | /// 4 | /// There are two different URLs where Mixpanel messages can be sent: 5 | /// https://api.mixpanel.com/track and https://api.mixpanel.com/engage 6 | /// 7 | public enum MixpanelMessageEndpoint 8 | { 9 | /// 10 | /// Used for building https://api.mixpanel.com/track URL. 11 | /// Two types of messages are sent to this URL: 'Track' and 'Alias'. 12 | /// 13 | Track, 14 | 15 | /// 16 | /// Used for building https://api.mixpanel.com/engage URL. 17 | /// This URL is used for all 'People' messages. 18 | /// 19 | Engage 20 | } 21 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests.Integration/UriHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Specialized; 3 | using System.Web; 4 | 5 | namespace Mixpanel.Tests.Integration 6 | { 7 | internal static class UriHelper 8 | { 9 | public static Uri CreateUri(string baseUri, params (string name, string value)[] query) 10 | { 11 | var uriBuilder = new UriBuilder(new Uri(baseUri)); 12 | 13 | NameValueCollection finalQuery = HttpUtility.ParseQueryString(string.Empty); 14 | foreach ((string name, string value) in query) 15 | { 16 | finalQuery.Add(name, value); 17 | } 18 | 19 | uriBuilder.Query = finalQuery.ToString(); 20 | 21 | return uriBuilder.Uri; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MixpanelMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Mixpanel 4 | { 5 | /// 6 | /// Used as a return type for Get*Message methods. property contains generated 7 | /// message that is ready to be serialized to JSON. This class can be passed as argument 8 | /// to Send(Async) method. 9 | /// 10 | public sealed class MixpanelMessage 11 | { 12 | /// 13 | /// Kind (type) of the message. 14 | /// 15 | public MessageKind Kind { get; set; } 16 | 17 | /// 18 | /// Generated message data with correct structure ready to be serialized to JSON. 19 | /// 20 | public IDictionary Data { get; set; } 21 | } 22 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/Parsers/ValueParseResult.cs: -------------------------------------------------------------------------------- 1 | namespace Mixpanel.Parsers 2 | { 3 | internal sealed class ValueParseResult 4 | { 5 | public bool Success { get; } 6 | public object Value { get; } 7 | public string ErrorDetails { get; } 8 | 9 | public ValueParseResult(bool success, object value, string errorDetails) 10 | { 11 | Success = success; 12 | Value = value; 13 | ErrorDetails = errorDetails; 14 | } 15 | 16 | public static ValueParseResult CreateSuccess(object value) 17 | { 18 | return new ValueParseResult(true, value, null); 19 | } 20 | 21 | public static ValueParseResult CreateFail(string message) 22 | { 23 | return new ValueParseResult(false, null, message); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/Parsers/NumberParser.cs: -------------------------------------------------------------------------------- 1 | namespace Mixpanel.Parsers 2 | { 3 | internal static class NumberParser 4 | { 5 | public static bool IsNumber(object value) 6 | { 7 | if (value is int || value is double || value is decimal || value is float || 8 | value is short || value is ushort || value is uint || value is long || 9 | value is ulong || value is byte || value is sbyte) 10 | { 11 | return true; 12 | } 13 | 14 | return false; 15 | } 16 | 17 | public static ValueParseResult Parse(object rawNumber) 18 | { 19 | return IsNumber(rawNumber) 20 | ? ValueParseResult.CreateSuccess(rawNumber) 21 | : ValueParseResult.CreateFail("Expected type is: number (int, double etc)"); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/Parsers/DurationParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Mixpanel.Parsers 4 | { 5 | internal static class DurationParser 6 | { 7 | public static ValueParseResult Parse(object rawDuration) 8 | { 9 | if (rawDuration == null) 10 | { 11 | return ValueParseResult.CreateFail("Can't be null."); 12 | } 13 | 14 | if (rawDuration is TimeSpan timeSpan) 15 | { 16 | return ValueParseResult.CreateSuccess(timeSpan.TotalSeconds); 17 | } 18 | 19 | if (NumberParser.IsNumber(rawDuration)) 20 | { 21 | return ValueParseResult.CreateSuccess(rawDuration); 22 | } 23 | 24 | return ValueParseResult.CreateFail( 25 | "Expected types are: TimeSpan or number (double, int etc)."); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/DefaultHttpClient.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Mixpanel 6 | { 7 | internal sealed class DefaultHttpClient 8 | { 9 | static readonly HttpClient HttpClient = new HttpClient(); 10 | 11 | public async Task PostAsync(string url, string formData, CancellationToken cancellationToken) 12 | { 13 | HttpResponseMessage responseMessage = 14 | await HttpClient.PostAsync(url, new StringContent(formData), cancellationToken).ConfigureAwait(false); 15 | if (!responseMessage.IsSuccessStatusCode) 16 | { 17 | return false; 18 | } 19 | 20 | string responseContent = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); 21 | return responseContent == "1"; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/SendResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | 3 | namespace Mixpanel 4 | { 5 | /// 6 | /// Contains information about 'Send(Async)' method work result. 7 | /// 8 | public sealed class SendResult 9 | { 10 | /// 11 | /// True if all message batches were successfully sent. 12 | /// False if at least one message batch failed. 13 | /// 14 | public bool Success { get; internal set; } 15 | 16 | /// 17 | /// A collection of successfully sent message batches. 18 | /// 19 | public ReadOnlyCollection> SentBatches { get; internal set; } 20 | 21 | /// 22 | /// A collection of failed message batches. 23 | /// 24 | public ReadOnlyCollection> FailedBatches { get; internal set; } 25 | } 26 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MixpanelMessageTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Mixpanel 5 | { 6 | /// 7 | /// Can be used to check all steps of creating a Mixpanel message. 8 | /// 9 | public class MixpanelMessageTest 10 | { 11 | /// 12 | /// Message data that was constructed from user input. 13 | /// 14 | public IDictionary Data { get; set; } 15 | 16 | /// 17 | /// serialized to JSON. 18 | /// 19 | public string Json { get; set; } 20 | 21 | /// 22 | /// converted to Base64 string. 23 | /// 24 | public string Base64 { get; set; } 25 | 26 | /// 27 | /// Contains the exception if some error occurs during the process of creating a message. 28 | /// 29 | public Exception Exception { get; set; } 30 | } 31 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MixpanelNameAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Mixpanel 4 | { 5 | /// 6 | /// If specified, then value of will be used as a property name, 7 | /// instead of target property name. 8 | /// 9 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] 10 | public sealed class MixpanelNameAttribute : Attribute 11 | { 12 | /// 13 | /// Name to use for a property. 14 | /// 15 | public readonly string Name; 16 | 17 | /// 18 | /// Creates an instance of class. 19 | /// 20 | /// 21 | /// Alternative name for property that will be sent to Mixpanel. 22 | /// For special Mixpanel properties use values from class. 23 | /// 24 | public MixpanelNameAttribute(string name) 25 | { 26 | Name = name; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MessageBuilders/People/PeopleAddMessageBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Mixpanel.MessageProperties; 3 | using Mixpanel.Parsers; 4 | 5 | namespace Mixpanel.MessageBuilders.People 6 | { 7 | // Message example: 8 | // { 9 | // "$token": "36ada5b10da39a1347559321baf13063", 10 | // "$distinct_id": "13793", 11 | // "$add": { "Coins Gathered": 12 } 12 | // } 13 | 14 | internal static class PeopleAddMessageBuilder 15 | { 16 | public static MessageBuildResult Build( 17 | string token, 18 | IEnumerable superProperties, 19 | object rawProperties, 20 | object distinctId, 21 | MixpanelConfig config) 22 | { 23 | return PeopleMessageBuilderBase.CreateMessage( 24 | token, 25 | superProperties, 26 | rawProperties, 27 | distinctId, 28 | config, 29 | "$add", 30 | NumberParser.Parse); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/Parsers/DistinctIdParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Mixpanel.Parsers 4 | { 5 | internal static class DistinctIdParser 6 | { 7 | public static ValueParseResult Parse(object rawDistinctId) 8 | { 9 | if (rawDistinctId == null) 10 | { 11 | return ValueParseResult.CreateFail("Can't be null."); 12 | } 13 | 14 | if (!(rawDistinctId is string) && 15 | !NumberParser.IsNumber(rawDistinctId) && 16 | !(rawDistinctId is Guid)) 17 | { 18 | return ValueParseResult.CreateFail( 19 | "Expected types are: string, number (int, long etc) or Guid."); 20 | } 21 | 22 | string distinctId = rawDistinctId.ToString(); 23 | if (string.IsNullOrWhiteSpace(distinctId)) 24 | { 25 | return ValueParseResult.CreateFail("Can't be empty string."); 26 | } 27 | 28 | 29 | return ValueParseResult.CreateSuccess(distinctId); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MessageBuilders/MessageBuildResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Mixpanel.MessageBuilders 4 | { 5 | internal sealed class MessageBuildResult 6 | { 7 | public bool Success { get; } 8 | public IDictionary Message { get; } 9 | public string Error { get; } 10 | 11 | public MessageBuildResult(bool success, IDictionary message, string error) 12 | { 13 | Success = success; 14 | Message = message; 15 | Error = error; 16 | } 17 | 18 | public static MessageBuildResult CreateSuccess(IDictionary message) 19 | { 20 | return new MessageBuildResult(true, message, null); 21 | } 22 | 23 | public static MessageBuildResult CreateFail(string error, string details = null) 24 | { 25 | return new MessageBuildResult( 26 | false, 27 | null, 28 | string.IsNullOrWhiteSpace(details) ? error : error + " " + details); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Aleksandr Ivanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MessageProperties/ObjectProperty.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Mixpanel.MessageProperties 4 | { 5 | [DebuggerDisplay("{PropertyName} - {Value}")] 6 | internal sealed class ObjectProperty 7 | { 8 | public string PropertyName { get; } 9 | public PropertyNameSource PropertyNameSource { get; } 10 | public PropertyOrigin Origin { get; } 11 | public object Value { get; } 12 | 13 | public ObjectProperty( 14 | string propertyName, 15 | PropertyNameSource propertyNameSource, 16 | PropertyOrigin origin, 17 | object value) 18 | { 19 | PropertyName = propertyName; 20 | PropertyNameSource = propertyNameSource; 21 | Origin = origin; 22 | Value = value; 23 | } 24 | 25 | public static ObjectProperty Default( 26 | string propertyName, 27 | PropertyOrigin origin, 28 | object value = null) 29 | { 30 | return new ObjectProperty(propertyName, PropertyNameSource.Default, origin, value); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MessageBuilders/People/PeopleAppendMessageBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Mixpanel.MessageProperties; 3 | using Mixpanel.Parsers; 4 | 5 | namespace Mixpanel.MessageBuilders.People 6 | { 7 | // Message example: 8 | // { 9 | // "$token": "36ada5b10da39a1347559321baf13063", 10 | // "$distinct_id": "13793", 11 | // "$append": { "Power Ups": "Bubble Lead" } 12 | // } 13 | 14 | internal static class PeopleAppendMessageBuilder 15 | { 16 | public static MessageBuildResult Build( 17 | string token, 18 | IEnumerable superProperties, 19 | object rawProperties, 20 | object distinctId, 21 | MixpanelConfig config) 22 | { 23 | return PeopleMessageBuilderBase.CreateMessage( 24 | token, 25 | superProperties, 26 | rawProperties, 27 | distinctId, 28 | config, 29 | "$append", 30 | rawValue => GenericPropertyParser.Parse(rawValue, allowCollections: false)); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MessageBuilders/People/PeopleRemoveMessageBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Mixpanel.MessageProperties; 3 | using Mixpanel.Parsers; 4 | 5 | namespace Mixpanel.MessageBuilders.People 6 | { 7 | // Message example: 8 | // { 9 | // "$token": "36ada5b10da39a1347559321baf13063", 10 | // "$distinct_id": "13793", 11 | // "$remove": { "Items purchased": "socks" } 12 | // } 13 | 14 | internal static class PeopleRemoveMessageBuilder 15 | { 16 | public static MessageBuildResult Build( 17 | string token, 18 | IEnumerable superProperties, 19 | object rawProperties, 20 | object distinctId, 21 | MixpanelConfig config) 22 | { 23 | return PeopleMessageBuilderBase.CreateMessage( 24 | token, 25 | superProperties, 26 | rawProperties, 27 | distinctId, 28 | config, 29 | "$remove", 30 | rawValue => GenericPropertyParser.Parse(rawValue, allowCollections: false)); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MixpanelBatchMessageTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Mixpanel 5 | { 6 | /// 7 | /// Can be used to check all steps of creating a batch mixpanel messages for 8 | /// MixpanelClient 'Send' and 'SendAsync' methods. 9 | /// 10 | public sealed class MixpanelBatchMessageTest 11 | { 12 | /// 13 | /// Message data that was constructed from user input. 14 | /// 15 | public IList> Data { get; internal set; } 16 | 17 | /// 18 | /// serialized to JSON. 19 | /// 20 | public string Json { get; internal set; } 21 | 22 | /// 23 | /// converted to Base64 string. 24 | /// 25 | public string Base64 { get; internal set; } 26 | 27 | /// 28 | /// Contains the exception if some error occurs during the process of creating a message. 29 | /// 30 | public Exception Exception { get; internal set; } 31 | } 32 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MessageBuilders/People/PeopleUnionMessageBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Mixpanel.MessageProperties; 3 | using Mixpanel.Parsers; 4 | 5 | namespace Mixpanel.MessageBuilders.People 6 | { 7 | // Message example: 8 | // { 9 | // "$token": "36ada5b10da39a1347559321baf13063", 10 | // "$distinct_id": "13793", 11 | // "$union": { "Items purchased": ["socks", "shirts"] } 12 | // } 13 | 14 | internal static class PeopleUnionMessageBuilder 15 | { 16 | public static MessageBuildResult Build( 17 | string token, 18 | IEnumerable superProperties, 19 | object rawProperties, 20 | object distinctId, 21 | MixpanelConfig config) 22 | { 23 | return PeopleMessageBuilderBase.CreateMessage( 24 | token, 25 | superProperties, 26 | rawProperties, 27 | distinctId, 28 | config, 29 | "$union", 30 | rawValue => CollectionParser.Parse(rawValue, _ => GenericPropertyParser.Parse(_, allowCollections: false))); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Mixpanel.Tests.Unit, PublicKey=00240000048000009400000006020000002400005253413100040000010001006d4dc39efaa7ce073be0e2fc77b8b038269e3375fb334bd6336feb53506450c29405ba91261b81b2b71c77bf68cbd267bb337279a2121e0aed653a38076149cd3f5f7d04e7ac4c736e5c2ba3287b0722f1597f6ec371d92d2c4e471ee2c0e3d88343e876e9dd82e5bac1130f76aad7d6ff02d4ce0f9e86ab55944dc1a67c35b3")] 4 | [assembly: InternalsVisibleTo("Mixpanel.Tests.Integration, PublicKey=00240000048000009400000006020000002400005253413100040000010001006d4dc39efaa7ce073be0e2fc77b8b038269e3375fb334bd6336feb53506450c29405ba91261b81b2b71c77bf68cbd267bb337279a2121e0aed653a38076149cd3f5f7d04e7ac4c736e5c2ba3287b0722f1597f6ec371d92d2c4e471ee2c0e3d88343e876e9dd82e5bac1130f76aad7d6ff02d4ce0f9e86ab55944dc1a67c35b3")] 5 | [assembly: InternalsVisibleTo("Benchmarks, PublicKey=00240000048000009400000006020000002400005253413100040000010001006d4dc39efaa7ce073be0e2fc77b8b038269e3375fb334bd6336feb53506450c29405ba91261b81b2b71c77bf68cbd267bb337279a2121e0aed653a38076149cd3f5f7d04e7ac4c736e5c2ba3287b0722f1597f6ec371d92d2c4e471ee2c0e3d88343e876e9dd82e5bac1130f76aad7d6ff02d4ce0f9e86ab55944dc1a67c35b3")] -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MessageProperties/PeopleSpecialProperty.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Mixpanel.MessageProperties 4 | { 5 | internal static class PeopleSpecialProperty 6 | { 7 | public const string Token = "$token"; 8 | 9 | public const string DistinctId = "$distinct_id"; 10 | public const string Ip = "$ip"; 11 | public const string Time = "$time"; 12 | public const string IgnoreTime = "$ignore_time"; 13 | public const string IgnoreAlias = "$ignore_alias"; 14 | 15 | public const string FirstName = "$first_name"; 16 | public const string LastName = "$last_name"; 17 | public const string Name = "$name"; 18 | public const string Email = "$email"; 19 | public const string Phone = "$phone"; 20 | public const string Created = "$created"; 21 | 22 | public static readonly HashSet MessageSpecialProperties = new HashSet(new[] 23 | { 24 | Token, 25 | DistinctId, 26 | Ip, 27 | Time, 28 | IgnoreTime, 29 | IgnoreAlias 30 | }); 31 | 32 | public static bool IsMessageSpecialProperty(string name) => 33 | MessageSpecialProperties.Contains(name); 34 | } 35 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/Parsers/GenericPropertyParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Mixpanel.Parsers 4 | { 5 | internal static class GenericPropertyParser 6 | { 7 | public static ValueParseResult Parse(object rawValue, bool allowCollections = false) 8 | { 9 | if (rawValue == null || 10 | rawValue is char || 11 | rawValue is string || 12 | rawValue is bool || 13 | NumberParser.IsNumber(rawValue) || 14 | rawValue is Guid || 15 | rawValue is TimeSpan) 16 | { 17 | return ValueParseResult.CreateSuccess(rawValue); 18 | } 19 | 20 | if (rawValue is DateTime || rawValue is DateTimeOffset) 21 | { 22 | return TimeParser.ParseMixpanelFormat(rawValue); 23 | } 24 | 25 | if (allowCollections && CollectionParser.IsCollection(rawValue)) 26 | { 27 | return CollectionParser.Parse(rawValue, _ => Parse(_, allowCollections: false)); 28 | } 29 | 30 | return ValueParseResult.CreateFail( 31 | "Expected types are: string, bool, char, number (int, double etc), Guid, DateTime, DateTimeOffset or TimeSpan."); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/Mixpanel/Benchmarks/Json/MessageCreator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using AutoFixture; 4 | 5 | namespace Benchmarks.Json 6 | { 7 | public static class MessageCreator 8 | { 9 | public static Dictionary Dictionary() 10 | { 11 | var fixture = new Fixture(); 12 | 13 | return new Dictionary 14 | { 15 | {"event", fixture.Create()}, 16 | { 17 | "properties", new Dictionary 18 | { 19 | {"token", fixture.Create()}, 20 | {"distinct_id", fixture.Create()}, 21 | {"IntProperty", fixture.Create()}, 22 | {"DecimalProperty", fixture.Create()}, 23 | { 24 | "ArrayProperty", 25 | new[] 26 | { 27 | fixture.Create(), 28 | fixture.Create(), 29 | fixture.Create() 30 | } 31 | } 32 | } 33 | } 34 | }; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests.Integration/Mixpanel.Tests.Integration.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net461;net6.0 5 | True 6 | ..\..\..\strong-name-key.snk 7 | 7c084124-97b4-4229-be3e-d474385480ac 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/Parsers/IpParser.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace Mixpanel.Parsers 5 | { 6 | internal static class IpParser 7 | { 8 | private const string RegexPattern = 9 | @"^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$"; 10 | 11 | public static ValueParseResult Parse(object rawIp) 12 | { 13 | switch (rawIp) 14 | { 15 | case null: 16 | return ValueParseResult.CreateFail("Can't be null."); 17 | case string ipString: 18 | return ParseString(ipString); 19 | case IPAddress ipAddress: 20 | return ValueParseResult.CreateSuccess(ipAddress.ToString()); 21 | default: 22 | return ValueParseResult.CreateFail( 23 | "Expected types are: string (example: 192.168.0.136) or IPAddress."); 24 | } 25 | } 26 | 27 | private static ValueParseResult ParseString(string rawIp) 28 | { 29 | if (Regex.IsMatch(rawIp, RegexPattern)) 30 | { 31 | return ValueParseResult.CreateSuccess(rawIp); 32 | } 33 | 34 | return ValueParseResult.CreateFail("Not a valid IP address."); 35 | 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MixpanelPropertyNameFormat.cs: -------------------------------------------------------------------------------- 1 | namespace Mixpanel 2 | { 3 | /// 4 | /// Type of name formatting that will be applied when generating property names. 5 | /// Formatting will be applied only for properties coming from classes that 6 | /// do not have an explicit . 7 | /// Formatting supports two-letter acronyms as described in 8 | /// https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/capitalization-conventions. 9 | /// 10 | public enum MixpanelPropertyNameFormat 11 | { 12 | /// 13 | /// No formatting will be applied. This is a default value. 14 | /// 15 | None, 16 | 17 | /// 18 | /// Property name will be parsed in sentence with only first word capitalized. 19 | /// Example: 'MySuperProperty' -> 'My super property'. 20 | /// 21 | SentenceCase, 22 | 23 | /// 24 | /// Property name will be parsed in sentence with all words capitalized. 25 | /// Example: 'MySuperProperty' -> 'My Super Property'. 26 | /// 27 | TitleCase, 28 | 29 | /// 30 | /// Property name will be parsed in sentence with no words capitalized. 31 | /// Example: 'MySuperProperty' -> 'my super property'. 32 | /// 33 | LowerCase 34 | } 35 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/Parsers/TrackSpecialPropertyParser.cs: -------------------------------------------------------------------------------- 1 | using Mixpanel.MessageProperties; 2 | 3 | namespace Mixpanel.Parsers 4 | { 5 | internal static class TrackSpecialPropertyParser 6 | { 7 | public static ValueParseResult Parse( 8 | string specialPropertyName, 9 | object rawValue) 10 | { 11 | switch (specialPropertyName) 12 | { 13 | case TrackSpecialProperty.Token: 14 | return StringParser.Parse(rawValue); 15 | 16 | case TrackSpecialProperty.DistinctId: 17 | return DistinctIdParser.Parse(rawValue); 18 | 19 | case TrackSpecialProperty.Time: 20 | return TimeParser.ParseUnix(rawValue); 21 | 22 | case TrackSpecialProperty.Ip: 23 | return IpParser.Parse(rawValue); 24 | 25 | case TrackSpecialProperty.Duration: 26 | return DurationParser.Parse(rawValue); 27 | 28 | case TrackSpecialProperty.Os: 29 | return StringParser.Parse(rawValue); 30 | 31 | case TrackSpecialProperty.ScreenWidth: 32 | case TrackSpecialProperty.ScreenHeight: 33 | return NumberParser.Parse(rawValue); 34 | 35 | default: 36 | return ValueParseResult.CreateFail($"No parser for '{nameof(specialPropertyName)}'."); 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/Mixpanel/Benchmarks/Json/DictionaryBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using BenchmarkDotNet.Attributes; 3 | using Newtonsoft.Json; 4 | 5 | namespace Benchmarks.Json 6 | { 7 | [HtmlExporter] 8 | [MemoryDiagnoser] 9 | public class DictionaryBenchmark 10 | { 11 | private const int N = 1000; 12 | private readonly Dictionary[] dictionaries; 13 | 14 | public DictionaryBenchmark() 15 | { 16 | dictionaries = new Dictionary[N]; 17 | for (int i = 0; i < N; i++) 18 | { 19 | dictionaries[i] = MessageCreator.Dictionary(); 20 | } 21 | } 22 | 23 | [Benchmark] 24 | public long Default() 25 | { 26 | long totalLength = 0L; 27 | foreach (var dictionary in dictionaries) 28 | { 29 | string json = Mixpanel.Json.MixpanelJsonSerializer.Serialize(dictionary); 30 | totalLength += json.Length; 31 | } 32 | 33 | return totalLength; 34 | } 35 | 36 | [Benchmark] 37 | public long JsonNet() 38 | { 39 | long totalLength = 0L; 40 | foreach (var dictionary in dictionaries) 41 | { 42 | string json = JsonConvert.SerializeObject(dictionary); 43 | totalLength += json.Length; 44 | } 45 | 46 | return totalLength; 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/Parsers/DurationParserTests.cs: -------------------------------------------------------------------------------- 1 | using Mixpanel.Parsers; 2 | using NUnit.Framework; 3 | 4 | namespace Mixpanel.Tests.Unit.Parsers 5 | { 6 | [TestFixture] 7 | public class DurationParserTests : MixpanelTestsBase 8 | { 9 | [Test] 10 | public void When_TimeSpan_Then_Success() 11 | { 12 | AssertSuccess(Duration, DurationSeconds); 13 | } 14 | 15 | [Test] 16 | public void When_Double_Then_Success() 17 | { 18 | AssertSuccess(DurationSeconds, DurationSeconds); 19 | } 20 | 21 | [Test] 22 | public void When_Null_Then_Fail() 23 | { 24 | string nullDuration = null; 25 | // ReSharper disable once ExpressionIsAlwaysNull 26 | AssertFail(nullDuration); 27 | } 28 | 29 | private void AssertSuccess(object durationToParse, object expectedDuration) 30 | { 31 | ValueParseResult parseResult = DurationParser.Parse(durationToParse); 32 | Assert.That(parseResult.Success, Is.True); 33 | Assert.That(parseResult.Value, Is.EqualTo(expectedDuration)); 34 | } 35 | 36 | private void AssertFail(object durationToParse) 37 | { 38 | ValueParseResult parseResult = DurationParser.Parse(durationToParse); 39 | Assert.That(parseResult.Success, Is.False); 40 | Assert.That(parseResult.Value, Is.Null); 41 | Assert.That(parseResult.ErrorDetails, Is.Not.Empty); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /nuget-readme.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | An open source Mixpanel .NET integration library that supports complete Mixpanel API. 3 | 4 | ## Sample usage for track message 5 | ```csharp 6 | var mc = new MixpanelClient("e3bc4100330c35722740fb8c6f5abddc"); 7 | await mc.TrackAsync("Level Complete", new { 8 | DistinctId = "12345", 9 | LevelNumber = 5, 10 | Duration = TimeSpan.FromMinutes(1) 11 | }); 12 | ``` 13 | This will send the following JSON to `https://api.mixpanel.com/track/`: 14 | ```json 15 | { 16 | "event": "Level Complete", 17 | "properties": { 18 | "token": "e3bc4100330c35722740fb8c6f5abddc", 19 | "distinct_id": "12345", 20 | "LevelNumber": 5, 21 | "$duration": 60 22 | } 23 | } 24 | ``` 25 | 26 | ## Sample usage for profile message 27 | ```csharp 28 | var mc = new MixpanelClient("e3bc4100330c35722740fb8c6f5abddc"); 29 | await mc.PeopleSetAsync(new { 30 | DistinctId = "12345", 31 | Name = "Darth Vader", 32 | Kills = 215 33 | }); 34 | ``` 35 | This will send the following JSON to `https://api.mixpanel.com/engage/`: 36 | ```json 37 | { 38 | "$token": "e3bc4100330c35722740fb8c6f5abddc", 39 | "$distinct_id": "12345", 40 | "$set": { 41 | "$name": "Darth Vader", 42 | "Kills": 215 43 | } 44 | } 45 | ``` 46 | 47 | ## Project URL 48 | https://github.com/eealeivan/mixpanel-csharp 49 | 50 | ## License 51 | ```mixpanel-csharp``` is licensed under [MIT](https://www.opensource.org/licenses/mit-license.php). Refer to [LICENSE](https://github.com/eealeivan/mixpanel-csharp/blob/master/LICENSE) for more information. 52 | -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/Parsers/IpParserTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Net; 3 | using Mixpanel.Parsers; 4 | using NUnit.Framework; 5 | 6 | namespace Mixpanel.Tests.Unit.Parsers 7 | { 8 | [TestFixture] 9 | public class IpParserTests : MixpanelTestsBase 10 | { 11 | [Test] 12 | public void Given_StringInput_When_ValidIp_Then_Success() 13 | { 14 | AssertSuccess(Ip, Ip); 15 | } 16 | 17 | [Test] 18 | public void Given_StringInput_When_InvalidIp_Then_Fail() 19 | { 20 | AssertFail("023.44.33.22"); 21 | } 22 | 23 | [Test] 24 | public void Given_IPAddressInput_When_ValidIp_Then_Success() 25 | { 26 | byte[] ipBytes = Ip.Split('.').Select(byte.Parse).ToArray(); 27 | var ipAddress = new IPAddress(ipBytes); 28 | 29 | AssertSuccess(ipAddress, Ip); 30 | } 31 | 32 | private void AssertSuccess(object ipToParse, string expectedIp) 33 | { 34 | ValueParseResult parseResult = IpParser.Parse(ipToParse); 35 | Assert.That(parseResult.Success, Is.True); 36 | Assert.That(parseResult.Value, Is.EqualTo(expectedIp)); 37 | } 38 | 39 | private void AssertFail(object ipToParse) 40 | { 41 | ValueParseResult parseResult = IpParser.Parse(ipToParse); 42 | Assert.That(parseResult.Success, Is.False); 43 | Assert.That(parseResult.Value, Is.Null); 44 | Assert.That(parseResult.ErrorDetails, Is.Not.Empty); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MessageBuilders/People/PeopleDeleteMessageBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Mixpanel.MessageProperties; 4 | using static System.String; 5 | 6 | namespace Mixpanel.MessageBuilders.People 7 | { 8 | // Message example: 9 | // { 10 | // "$token": "36ada5b10da39a1347559321baf13063", 11 | // "$distinct_id": "13793", 12 | // "$delete": "", 13 | // "$ignore_alias": true 14 | // } 15 | 16 | internal static class PeopleDeleteMessageBuilder 17 | { 18 | public static MessageBuildResult Build( 19 | string token, 20 | IEnumerable superProperties, 21 | object distinctId, 22 | bool ignoreAlias, 23 | MixpanelConfig config) 24 | { 25 | MessageBuildResult messageBuildResult = PeopleMessageBuilderBase.CreateMessage( 26 | token, 27 | superProperties, 28 | null, 29 | distinctId, 30 | config, 31 | "$delete", 32 | rawValue => throw new InvalidOperationException()); 33 | 34 | if (!messageBuildResult.Success) 35 | { 36 | return messageBuildResult; 37 | } 38 | 39 | messageBuildResult.Message["$delete"] = Empty; 40 | 41 | if (ignoreAlias) 42 | { 43 | messageBuildResult.Message[PeopleSpecialProperty.IgnoreAlias] = true; 44 | } 45 | 46 | return messageBuildResult; 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/Parsers/CollectionParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | 5 | namespace Mixpanel.Parsers 6 | { 7 | internal static class CollectionParser 8 | { 9 | public static bool IsCollection(object rawCollection) 10 | { 11 | return rawCollection is IEnumerable && !(rawCollection is string); 12 | } 13 | 14 | public static ValueParseResult Parse( 15 | object rawCollection, 16 | Func itemParseFn) 17 | { 18 | switch (rawCollection) 19 | { 20 | case null: 21 | return ValueParseResult.CreateFail("Can't be null."); 22 | 23 | case string _: 24 | return ValueParseResult.CreateFail("Can't be string."); 25 | 26 | case IEnumerable collection: 27 | var validItems = new List(); 28 | foreach (object item in collection) 29 | { 30 | ValueParseResult itemParseResult = itemParseFn(item); 31 | if (!itemParseResult.Success) 32 | { 33 | continue; 34 | } 35 | 36 | validItems.Add(itemParseResult.Value); 37 | } 38 | return ValueParseResult.CreateSuccess(validItems); 39 | 40 | default: 41 | return ValueParseResult.CreateFail("Expected type is: IEnumerable."); 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MessageBuilders/Track/TrackMessageBuilderBase.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Mixpanel.MessageProperties; 3 | using Mixpanel.Parsers; 4 | 5 | namespace Mixpanel.MessageBuilders.Track 6 | { 7 | internal static class TrackMessageBuilderBase 8 | { 9 | public static MessageCandidate CreateValidMessageCandidate( 10 | string token, 11 | IEnumerable superProperties, 12 | object rawProperties, 13 | object distinctId, 14 | MixpanelConfig config, 15 | out string errorMessage) 16 | { 17 | var messageCandidate = new MessageCandidate( 18 | token, 19 | superProperties, 20 | rawProperties, 21 | distinctId, 22 | config, 23 | TrackSpecialPropertyMapper.RawNameToSpecialProperty); 24 | 25 | ObjectProperty tokenProp = messageCandidate.GetSpecialProperty(TrackSpecialProperty.Token); 26 | if (tokenProp == null) 27 | { 28 | errorMessage = $"'{TrackSpecialProperty.Token}' is not set."; 29 | return null; 30 | } 31 | 32 | ValueParseResult tokenParseResult = 33 | TrackSpecialPropertyParser.Parse(TrackSpecialProperty.Token, tokenProp.Value); 34 | if (!tokenParseResult.Success) 35 | { 36 | errorMessage = $"Error parsing '{TrackSpecialProperty.Token}'. {tokenParseResult.ErrorDetails}"; 37 | return null; 38 | } 39 | 40 | errorMessage = null; 41 | return messageCandidate; 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/Parsers/PeopleSpecialPropertyParser.cs: -------------------------------------------------------------------------------- 1 | using Mixpanel.MessageProperties; 2 | 3 | namespace Mixpanel.Parsers 4 | { 5 | internal static class PeopleSpecialPropertyParser 6 | { 7 | public static ValueParseResult Parse( 8 | string specialPropertyName, 9 | object rawValue) 10 | { 11 | switch (specialPropertyName) 12 | { 13 | case PeopleSpecialProperty.Token: 14 | return StringParser.Parse(rawValue); 15 | 16 | case PeopleSpecialProperty.DistinctId: 17 | return DistinctIdParser.Parse(rawValue); 18 | 19 | case PeopleSpecialProperty.Ip: 20 | return IpParser.Parse(rawValue); 21 | 22 | case PeopleSpecialProperty.Time: 23 | return TimeParser.ParseUnix(rawValue); 24 | 25 | case PeopleSpecialProperty.IgnoreTime: 26 | case PeopleSpecialProperty.IgnoreAlias: 27 | return BoolParser.Parse(rawValue); 28 | 29 | case PeopleSpecialProperty.FirstName: 30 | case PeopleSpecialProperty.LastName: 31 | case PeopleSpecialProperty.Name: 32 | case PeopleSpecialProperty.Email: 33 | case PeopleSpecialProperty.Phone: 34 | return StringParser.Parse(rawValue); 35 | 36 | case PeopleSpecialProperty.Created: 37 | return TimeParser.ParseMixpanelFormat(rawValue); 38 | 39 | default: 40 | return ValueParseResult.CreateFail($"No parser for '{nameof(specialPropertyName)}'."); 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/Parsers/DistinctIdParserTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Mixpanel.Parsers; 3 | using NUnit.Framework; 4 | 5 | namespace Mixpanel.Tests.Unit.Parsers 6 | { 7 | [TestFixture] 8 | public class DistinctIdParserTests : MixpanelTestsBase 9 | { 10 | [Test] 11 | public void When_String_Then_Success() 12 | { 13 | AssertSuccess(DistinctId, DistinctId); 14 | } 15 | 16 | [Test] 17 | public void When_Int_Then_Success() 18 | { 19 | AssertSuccess(DistinctIdInt, DistinctIdInt.ToString()); 20 | } 21 | 22 | [Test] 23 | public void When_Guid_Then_Success() 24 | { 25 | var guidDistinctId = Guid.NewGuid(); 26 | AssertSuccess(guidDistinctId, guidDistinctId.ToString()); 27 | } 28 | 29 | [Test] 30 | public void When_Null_Then_Fail() 31 | { 32 | string nullDistinctId = null; 33 | // ReSharper disable once ExpressionIsAlwaysNull 34 | AssertFail(nullDistinctId); 35 | } 36 | 37 | private void AssertSuccess(object distinctIdToParse, string expectedDistinctId) 38 | { 39 | ValueParseResult parseResult = DistinctIdParser.Parse(distinctIdToParse); 40 | Assert.That(parseResult.Success, Is.True); 41 | Assert.That(parseResult.Value, Is.EqualTo(expectedDistinctId)); 42 | } 43 | 44 | private void AssertFail(object distinctIdToParse) 45 | { 46 | ValueParseResult parseResult = DistinctIdParser.Parse(distinctIdToParse); 47 | Assert.That(parseResult.Success, Is.False); 48 | Assert.That(parseResult.Value, Is.Null); 49 | Assert.That(parseResult.ErrorDetails, Is.Not.Empty); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/Mixpanel/Benchmarks/Json/DictionaryArrayBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using BenchmarkDotNet.Attributes; 3 | using Newtonsoft.Json; 4 | 5 | namespace Benchmarks.Json 6 | { 7 | [HtmlExporter] 8 | [MemoryDiagnoser] 9 | public class DictionaryArrayBenchmark 10 | { 11 | private const int N = 1000; 12 | private const int ArraySize = 10; 13 | private readonly List[]> dictionaryArrays; 14 | 15 | public DictionaryArrayBenchmark() 16 | { 17 | dictionaryArrays = new List[]>(); 18 | for (int i = 0; i < N; i++) 19 | { 20 | var dictionaryArray = new Dictionary[ArraySize]; 21 | for (int j = 0; j < ArraySize; j++) 22 | { 23 | dictionaryArray[j] = MessageCreator.Dictionary(); 24 | } 25 | 26 | dictionaryArrays.Add(dictionaryArray); 27 | } 28 | } 29 | 30 | [Benchmark] 31 | public long Default() 32 | { 33 | long totalLength = 0L; 34 | foreach (Dictionary[] dictionaryArray in dictionaryArrays) 35 | { 36 | string json = Mixpanel.Json.MixpanelJsonSerializer.Serialize(dictionaryArray); 37 | totalLength += json.Length; 38 | } 39 | 40 | return totalLength; 41 | } 42 | 43 | [Benchmark] 44 | public long JsonNet() 45 | { 46 | long totalLength = 0L; 47 | foreach (Dictionary[] dictionaryArray in dictionaryArrays) 48 | { 49 | string json = JsonConvert.SerializeObject(dictionaryArray); 50 | totalLength += json.Length; 51 | } 52 | 53 | return totalLength; 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests.Integration/SecretsProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Configuration; 3 | using Mixpanel.Tests.Integration.DefaultHttpClient; 4 | 5 | namespace Mixpanel.Tests.Integration 6 | { 7 | public static class SecretsProvider 8 | { 9 | public static string MixpanelProjectId { get; } 10 | public static string MixpanelProjectToken { get; } 11 | public static string MixpanelServiceAccountUsername { get; } 12 | public static string MixpanelServiceAccountSecret { get; } 13 | 14 | static SecretsProvider() 15 | { 16 | var builder = new ConfigurationBuilder() 17 | .AddUserSecrets(); 18 | 19 | IConfiguration configuration = builder.Build(); 20 | 21 | MixpanelProjectId = configuration["Mixpanel:Project:Id"]; 22 | if (string.IsNullOrWhiteSpace(MixpanelProjectId)) 23 | { 24 | throw new Exception("Mixpanel:ProjectId is empty."); 25 | } 26 | 27 | MixpanelProjectToken = configuration["Mixpanel:Project:Token"]; 28 | if (string.IsNullOrWhiteSpace(MixpanelProjectToken)) 29 | { 30 | throw new Exception("Mixpanel:ProjectToken is empty."); 31 | } 32 | 33 | MixpanelServiceAccountUsername = configuration["Mixpanel:ServiceAccount:Username"]; 34 | if (string.IsNullOrWhiteSpace(MixpanelServiceAccountUsername)) 35 | { 36 | throw new Exception("Mixpanel:ServiceAccount:Username is empty."); 37 | } 38 | 39 | MixpanelServiceAccountSecret = configuration["Mixpanel:ServiceAccount:Secret"]; 40 | if (string.IsNullOrWhiteSpace(MixpanelServiceAccountSecret)) 41 | { 42 | throw new Exception("Mixpanel:ServiceAccount:Secret is empty."); 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/SendResultInternal.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace Mixpanel 5 | { 6 | internal sealed class SendResultInternal 7 | { 8 | private bool success; 9 | private List> sentBatches; 10 | private List> failedBatches; 11 | 12 | public SendResultInternal() 13 | { 14 | success = true; 15 | } 16 | 17 | public void Update(bool batchSuccess, List mixpanelMessages) 18 | { 19 | success &= batchSuccess; 20 | 21 | if (batchSuccess) 22 | { 23 | if (sentBatches == null) 24 | { 25 | sentBatches = new List>(); 26 | } 27 | 28 | sentBatches.Add(mixpanelMessages); 29 | } 30 | else 31 | { 32 | if (failedBatches == null) 33 | { 34 | failedBatches = new List>(); 35 | } 36 | 37 | failedBatches.Add(mixpanelMessages); 38 | } 39 | } 40 | 41 | public SendResult ToRealSendResult() 42 | { 43 | var result = new SendResult { Success = success }; 44 | 45 | if (sentBatches != null) 46 | { 47 | result.SentBatches = sentBatches 48 | .Select(x => x.AsReadOnly()) 49 | .ToList() 50 | .AsReadOnly(); 51 | } 52 | 53 | if (failedBatches != null) 54 | { 55 | result.FailedBatches = failedBatches 56 | .Select(x => x.AsReadOnly()) 57 | .ToList() 58 | .AsReadOnly(); 59 | } 60 | 61 | return result; 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests.Integration/DefaultHttpClient/DefaultHttpClientTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using FluentAssertions; 5 | using NUnit.Framework; 6 | 7 | namespace Mixpanel.Tests.Integration.DefaultHttpClient 8 | { 9 | public class DefaultHttpClientTests 10 | { 11 | private const string BaseUrl = "https://eealeivan-mixpanel.free.beeceptor.com/"; 12 | 13 | [Test] 14 | public async Task PostAsync_SuccessRequest_TrueReturned() 15 | { 16 | // Arrange 17 | var httpClient = new Mixpanel.DefaultHttpClient(); 18 | 19 | // Act 20 | var result = await httpClient.PostAsync(BaseUrl + "success", "", CancellationToken.None); 21 | 22 | // Assert 23 | result.Should().BeTrue(); 24 | } 25 | 26 | [Test] 27 | public async Task PostAsync_FailRequest_FalseReturned() 28 | { 29 | // Arrange 30 | var httpClient = new Mixpanel.DefaultHttpClient(); 31 | 32 | // Act 33 | var result = await httpClient.PostAsync(BaseUrl + "fail", "", CancellationToken.None); 34 | 35 | // Assert 36 | result.Should().BeFalse(); 37 | } 38 | 39 | [Test] 40 | public async Task PostAsync_CancellationToken_CancelledForSlowRequest() 41 | { 42 | // Arrange 43 | var httpClient = new Mixpanel.DefaultHttpClient(); 44 | var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(500)); 45 | var cancellationToken = cancellationTokenSource.Token; 46 | 47 | // Act 48 | Func act = async () => { await httpClient.PostAsync(BaseUrl + "success-5000ms", "", cancellationToken); }; 49 | 50 | // Assert 51 | await act.Should().ThrowAsync(); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MessageBuilders/People/PeopleTrackChargeMessageBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Mixpanel.MessageProperties; 4 | using Mixpanel.Parsers; 5 | 6 | namespace Mixpanel.MessageBuilders.People 7 | { 8 | // Message example: 9 | // { 10 | // "$append": { 11 | // "$transactions": { 12 | // "$time": "2013-01-03T09:00:00", 13 | // "$amount": 25.34 14 | // } 15 | // }, 16 | // "$token": "36ada5b10da39a1347559321baf13063", 17 | // "$distinct_id": "13793" 18 | // } 19 | 20 | internal static class PeopleTrackChargeMessageBuilder 21 | { 22 | public static MessageBuildResult Build( 23 | string token, 24 | IEnumerable superProperties, 25 | decimal amount, 26 | DateTime time, 27 | object distinctId, 28 | MixpanelConfig config) 29 | { 30 | MessageBuildResult messageBuildResult = PeopleMessageBuilderBase.CreateMessage( 31 | token, 32 | superProperties, 33 | null, 34 | distinctId, 35 | config, 36 | "$append", 37 | rawValue => throw new InvalidOperationException()); 38 | 39 | if (!messageBuildResult.Success) 40 | { 41 | return messageBuildResult; 42 | } 43 | 44 | messageBuildResult.Message["$append"] = new Dictionary(1) 45 | { 46 | { 47 | "$transactions", new Dictionary(2) 48 | { 49 | {"$time", TimeParser.ParseMixpanelFormat(time).Value}, 50 | {"$amount", amount} 51 | } 52 | } 53 | }; 54 | 55 | return messageBuildResult; 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MessageBuilders/People/PeopleUnsetMessageBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Mixpanel.MessageProperties; 3 | 4 | namespace Mixpanel.MessageBuilders.People 5 | { 6 | internal static class PeopleUnsetMessageBuilder 7 | { 8 | // Message example: 9 | // { 10 | // "$token": "36ada5b10da39a1347559321baf13063", 11 | // "$distinct_id": "13793", 12 | // "$unset": [ "Days Overdue" ] 13 | // } 14 | 15 | public static MessageBuildResult Build( 16 | string token, 17 | IEnumerable superProperties, 18 | IEnumerable propertyNames, 19 | object distinctId, 20 | MixpanelConfig config) 21 | { 22 | MessageCandidate messageCandidate = PeopleMessageBuilderBase.CreateValidMessageCandidate( 23 | token, 24 | superProperties, 25 | null, 26 | distinctId, 27 | config, 28 | out string messageCandidateErrorMessage); 29 | 30 | if (messageCandidate == null) 31 | { 32 | return MessageBuildResult.CreateFail(messageCandidateErrorMessage); 33 | } 34 | 35 | var message = new Dictionary(); 36 | 37 | // Special properties 38 | PeopleMessageBuilderBase.RunForValidSpecialProperties( 39 | messageCandidate, 40 | (specialPropertyName, isMessageSpecialProperty, value) => 41 | { 42 | // Ignore non-message specific special properties as they are not valid in profile update messages 43 | if (isMessageSpecialProperty) 44 | { 45 | message[specialPropertyName] = value; 46 | } 47 | }); 48 | 49 | message["$unset"] = propertyNames ?? new string[0]; 50 | 51 | return MessageBuildResult.CreateSuccess(message); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MessageProperties/TrackSpecialPropertyMapper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Mixpanel.Parsers; 3 | 4 | namespace Mixpanel.MessageProperties 5 | { 6 | internal static class TrackSpecialPropertyMapper 7 | { 8 | private static readonly Dictionary RawNameToSpecialPropertyMap; 9 | 10 | static TrackSpecialPropertyMapper() 11 | { 12 | RawNameToSpecialPropertyMap = new Dictionary(new PropertyNameComparer()) 13 | { 14 | {"token", TrackSpecialProperty.Token }, 15 | {"$token", TrackSpecialProperty.Token }, 16 | 17 | { "distinctid", TrackSpecialProperty.DistinctId }, 18 | { "distinct_id", TrackSpecialProperty.DistinctId }, 19 | { "$distinctid", TrackSpecialProperty.DistinctId }, 20 | { "$distinct_id", TrackSpecialProperty.DistinctId }, 21 | 22 | { "time", TrackSpecialProperty.Time }, 23 | { "$time", TrackSpecialProperty.Time }, 24 | 25 | { "ip", TrackSpecialProperty.Ip }, 26 | { "$ip", TrackSpecialProperty.Ip }, 27 | 28 | { "duration", TrackSpecialProperty.Duration }, 29 | { "$duration", TrackSpecialProperty.Duration }, 30 | 31 | {"os", TrackSpecialProperty.Os }, 32 | {"$os", TrackSpecialProperty.Os }, 33 | 34 | {"screenwidth", TrackSpecialProperty.ScreenWidth }, 35 | {"screen_width", TrackSpecialProperty.ScreenWidth }, 36 | {"$screenwidth", TrackSpecialProperty.ScreenWidth }, 37 | {"$screen_width", TrackSpecialProperty.ScreenWidth }, 38 | 39 | {"screenheight", TrackSpecialProperty.ScreenHeight }, 40 | {"screen_height", TrackSpecialProperty.ScreenHeight }, 41 | {"$screenheight", TrackSpecialProperty.ScreenHeight }, 42 | {"$screen_height", TrackSpecialProperty.ScreenHeight } 43 | }; 44 | } 45 | 46 | public static string RawNameToSpecialProperty(string propertyName) 47 | { 48 | return RawNameToSpecialPropertyMap.TryGetValue(propertyName, out var specialProperty) 49 | ? specialProperty 50 | : null; 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests.Integration/MixpanelClient/MixpanelClientTrackTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Bogus; 4 | using FluentAssertions; 5 | using Newtonsoft.Json.Linq; 6 | using NUnit.Framework; 7 | 8 | // ReSharper disable AssignNullToNotNullAttribute 9 | 10 | namespace Mixpanel.Tests.Integration.MixpanelClient 11 | { 12 | public class MixpanelClientTrackTests 13 | { 14 | [Test] 15 | public async Task TrackAsync_SimpleMessage_TrueReturned() 16 | { 17 | // Arrange 18 | var (token, _, _, @event) = GenerateInputs(); 19 | var client = new Mixpanel.MixpanelClient(token); 20 | 21 | // Act 22 | var result = await client.TrackAsync(@event, new { }); 23 | 24 | // Assert 25 | result.Should().BeTrue(); 26 | } 27 | 28 | [Test] 29 | public async Task TrackAsync_SimpleMessage_CorrectDataSavedInMixpanel() 30 | { 31 | // Arrange 32 | var (token, distinctId, insertId, @event) = GenerateInputs(); 33 | var client = new Mixpanel.MixpanelClient(token); 34 | 35 | // Act 36 | await client.TrackAsync(@event, new Dictionary 37 | { 38 | { "$insert_id", insertId }, 39 | { MixpanelProperty.DistinctId, distinctId } 40 | }); 41 | 42 | // Assert 43 | JObject message = await MixpanelExportApi.GetRecentMessage(insertId); 44 | message.Should().ContainKey("event").WhoseValue.Value().Should().Be(@event); 45 | 46 | JObject messageProperties = (JObject)message["properties"]; 47 | messageProperties.Should().ContainKey("$insert_id").WhoseValue.Value().Should().Be(insertId); 48 | messageProperties.Should().ContainKey("distinct_id").WhoseValue.Value().Should().Be(distinctId); 49 | } 50 | 51 | private (string token, string distinctId, string insertId, string @event) GenerateInputs() 52 | { 53 | var randomizer = new Randomizer(); 54 | return 55 | ( 56 | SecretsProvider.MixpanelProjectToken, 57 | randomizer.AlphaNumeric(10), 58 | randomizer.AlphaNumeric(20), 59 | randomizer.Words() 60 | ); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/Mixpanel.Tests.Unit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net461;net6.0 5 | True 6 | ..\..\..\strong-name-key.snk 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | NET461 26 | 27 | 28 | NET461 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 4.5.0 39 | 40 | 41 | 42 | 43 | 44 | NET6 45 | 46 | 47 | NET6 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/MixpanelClient/MixpanelClientSendJsonTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Bogus; 5 | using FluentAssertions; 6 | using Newtonsoft.Json; 7 | using Newtonsoft.Json.Linq; 8 | using NUnit.Framework; 9 | 10 | namespace Mixpanel.Tests.Unit.MixpanelClient 11 | { 12 | [TestFixture] 13 | public class MixpanelClientSendJsonTests 14 | { 15 | [Test] 16 | public async Task SendAsync_TrackMessage_CorrectDataSent() 17 | { 18 | // Arrange 19 | var httpMockMixpanelConfig = new HttpMockMixpanelConfig(); 20 | var client = new Mixpanel.MixpanelClient(httpMockMixpanelConfig.Instance); 21 | var message = CreateJsonMessage(); 22 | 23 | // Act 24 | var result = await client.SendJsonAsync(MixpanelMessageEndpoint.Track, message); 25 | 26 | // Assert 27 | result.Should().BeTrue(); 28 | var (endpoint, sentMessage) = httpMockMixpanelConfig.Messages.Single(); 29 | endpoint.Should().Be("https://api.mixpanel.com/track"); 30 | sentMessage.ToString(Formatting.None).Should().Be(message); 31 | } 32 | 33 | [Test] 34 | public void SendAsync_CancellationRequested_RequestCancelled() 35 | { 36 | // Arrange 37 | var httpMockMixpanelConfig = new HttpMockMixpanelConfig(); 38 | var cancellationTokenSource = new CancellationTokenSource(); 39 | var client = new Mixpanel.MixpanelClient(httpMockMixpanelConfig.Instance); 40 | var jsonMessage = CreateJsonMessage(); 41 | 42 | // Act 43 | var task = Task.Factory.StartNew( 44 | async () => await client.SendJsonAsync( 45 | MixpanelMessageEndpoint.Track, jsonMessage, cancellationTokenSource.Token)); 46 | cancellationTokenSource.Cancel(); 47 | task.Wait(); 48 | 49 | // Assert 50 | httpMockMixpanelConfig.RequestCancelled.Should().BeTrue(); 51 | } 52 | 53 | private string CreateJsonMessage() 54 | { 55 | var randomizer = new Randomizer(); 56 | var message = new 57 | { 58 | @event = randomizer.Words(), 59 | properties = new 60 | { 61 | token = randomizer.AlphaNumeric(30), 62 | distinct_id = randomizer.AlphaNumeric(10) 63 | } 64 | }; 65 | 66 | return JsonConvert.SerializeObject(message); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests.Integration/MixpanelExportApi.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Net.Http.Headers; 7 | using System.Threading.Tasks; 8 | using Newtonsoft.Json; 9 | using Newtonsoft.Json.Linq; 10 | using Polly; 11 | using Polly.Retry; 12 | 13 | namespace Mixpanel.Tests.Integration 14 | { 15 | public static class MixpanelExportApi 16 | { 17 | private const string ExportBaseUri = "https://data.mixpanel.com/api/2.0/export"; 18 | 19 | private static readonly AsyncRetryPolicy EmptyResponsePolicy = Policy 20 | .HandleResult(response => response.Content.Headers.ContentLength == 0) 21 | .WaitAndRetryAsync(5, retryAttempt => TimeSpan.FromSeconds(retryAttempt * 2)); 22 | 23 | private static readonly HttpClient HttpClient = new HttpClient 24 | { 25 | DefaultRequestHeaders = 26 | { 27 | Authorization = new AuthenticationHeaderValue( 28 | "Basic", 29 | $"{SecretsProvider.MixpanelServiceAccountUsername}:{SecretsProvider.MixpanelServiceAccountSecret}") 30 | } 31 | }; 32 | 33 | public static async Task GetRecentMessage(string insertId) 34 | { 35 | var uri = UriHelper.CreateUri( 36 | ExportBaseUri, 37 | ("project_id", SecretsProvider.MixpanelProjectId), 38 | ("from_date", DateTime.UtcNow.ToString("yyyy-MM-dd")), 39 | ("to_date", DateTime.UtcNow.ToString("yyyy-MM-dd")), 40 | ("where", $"properties[\"$insert_id\"] == \"{insertId}\"")); 41 | 42 | var response = await EmptyResponsePolicy.ExecuteAsync(async _ => await HttpClient.GetAsync(uri), new Context()); 43 | response.EnsureSuccessStatusCode(); 44 | 45 | string jsonl = await response.Content.ReadAsStringAsync(); 46 | return ParseResponse(jsonl).Single(); 47 | } 48 | 49 | private static List ParseResponse(string jsonl) 50 | { 51 | List items = new List(); 52 | 53 | var jsonReader = new JsonTextReader(new StringReader(jsonl)) 54 | { 55 | SupportMultipleContent = true, 56 | DateParseHandling = DateParseHandling.None 57 | }; 58 | 59 | while (jsonReader.Read()) 60 | { 61 | items.Add(JObject.Load(jsonReader)); 62 | } 63 | 64 | return items; 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.2.32519.379 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mixpanel", "Mixpanel\Mixpanel.csproj", "{073DD847-DB73-4AFF-A37E-0ECD66CEE9EF}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mixpanel.Tests.Unit", "Mixpanel.Tests\Mixpanel.Tests.Unit.csproj", "{77B94569-E2E9-4B19-BF6A-6E9B245ED7BE}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "Benchmarks\Benchmarks.csproj", "{B61C6995-7CCF-4899-9C14-21DB347CBEDE}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mixpanel.Tests.Integration", "Mixpanel.Tests.Integration\Mixpanel.Tests.Integration.csproj", "{C70E67E8-B537-4E14-B2A6-EE12784AF0A8}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {073DD847-DB73-4AFF-A37E-0ECD66CEE9EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {073DD847-DB73-4AFF-A37E-0ECD66CEE9EF}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {073DD847-DB73-4AFF-A37E-0ECD66CEE9EF}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {073DD847-DB73-4AFF-A37E-0ECD66CEE9EF}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {77B94569-E2E9-4B19-BF6A-6E9B245ED7BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {77B94569-E2E9-4B19-BF6A-6E9B245ED7BE}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {77B94569-E2E9-4B19-BF6A-6E9B245ED7BE}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {77B94569-E2E9-4B19-BF6A-6E9B245ED7BE}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {B61C6995-7CCF-4899-9C14-21DB347CBEDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {B61C6995-7CCF-4899-9C14-21DB347CBEDE}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {B61C6995-7CCF-4899-9C14-21DB347CBEDE}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {B61C6995-7CCF-4899-9C14-21DB347CBEDE}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {C70E67E8-B537-4E14-B2A6-EE12784AF0A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {C70E67E8-B537-4E14-B2A6-EE12784AF0A8}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {C70E67E8-B537-4E14-B2A6-EE12784AF0A8}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {C70E67E8-B537-4E14-B2A6-EE12784AF0A8}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {564998EA-0119-4044-BA93-FA1864B63D0F} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/BatchMessageWrapper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | 5 | namespace Mixpanel 6 | { 7 | internal sealed class BatchMessageWrapper 8 | { 9 | public List> TrackMessages { get; private set; } 10 | public List> EngageMessages { get; private set; } 11 | 12 | internal const int MaxBatchSize = 50; 13 | 14 | public BatchMessageWrapper(IEnumerable messages) 15 | { 16 | if (messages == null) 17 | { 18 | return; 19 | } 20 | 21 | foreach (var message in messages) 22 | { 23 | if (message == null || message.Data == null || message.Kind == MessageKind.Batch) 24 | { 25 | continue; 26 | } 27 | 28 | bool isTrackMessage = 29 | message.Kind == MessageKind.Track || message.Kind == MessageKind.Alias; 30 | if (isTrackMessage) 31 | { 32 | AddTrackMessage(message); 33 | } 34 | else 35 | { 36 | AddEngageMessage(message); 37 | } 38 | } 39 | } 40 | 41 | private void AddTrackMessage(MixpanelMessage message) 42 | { 43 | Debug.Assert(message != null); 44 | 45 | if (TrackMessages == null) 46 | { 47 | TrackMessages = new List>(); 48 | } 49 | 50 | AddBatchMessage(TrackMessages, message); 51 | } 52 | 53 | private void AddEngageMessage(MixpanelMessage message) 54 | { 55 | Debug.Assert(message != null); 56 | 57 | if (EngageMessages == null) 58 | { 59 | EngageMessages = new List>(); 60 | } 61 | 62 | AddBatchMessage(EngageMessages, message); 63 | } 64 | 65 | private void AddBatchMessage(List> list, MixpanelMessage message) 66 | { 67 | Debug.Assert(list != null); 68 | Debug.Assert(message != null); 69 | 70 | var lastInnerList = list.LastOrDefault(); 71 | bool newInnerListNeeded = lastInnerList == null || lastInnerList.Count >= MaxBatchSize; 72 | if (newInnerListNeeded) 73 | { 74 | var newInnerList = new List { message }; 75 | list.Add(newInnerList); 76 | } 77 | else 78 | { 79 | lastInnerList.Add(message); 80 | } 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/Parsers/CollectionParserTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using Mixpanel.Parsers; 4 | using NUnit.Framework; 5 | 6 | namespace Mixpanel.Tests.Unit.Parsers 7 | { 8 | [TestFixture] 9 | public class CollectionParserTests : MixpanelTestsBase 10 | { 11 | [Test] 12 | public void Given_CollectionOfNumbers_When_OnlyNumbersAllowed_Then_AllNumbersParsed() 13 | { 14 | AssertSuccess( 15 | NumberParser.Parse, 16 | new object[] { 1, 5M, 2.3D }, 17 | new object[] { 1, 5M, 2.3D }); 18 | } 19 | 20 | [Test] 21 | public void Given_CollectionOfMixedTypes_When_OnlyNumbersAllowed_Then_OnlyNumbersParsed() 22 | { 23 | AssertSuccess( 24 | NumberParser.Parse, 25 | new object[] { 1, 5M, 2.3D, StringPropertyValue, Created, Duration }, 26 | new object[] { 1, 5M, 2.3D }); 27 | } 28 | 29 | [Test] 30 | public void Given_CollectionOfMixedTypes_When_MixedTypesAllowed_Then_AllElementsParsed() 31 | { 32 | AssertSuccess( 33 | rawValue => GenericPropertyParser.Parse(rawValue, allowCollections: false), 34 | new object[] { 1, 5M, 2.3D, StringPropertyValue, Created, Duration }, 35 | new object[] { 1, 5M, 2.3D, StringPropertyValue, CreatedFormat, Duration }); 36 | } 37 | 38 | [Test] 39 | public void Given_NonCollectionType_When_MixedTypesAllowed_Then_FailReturned() 40 | { 41 | AssertFail( 42 | rawValue => GenericPropertyParser.Parse(rawValue, allowCollections: false), 43 | Created); 44 | AssertFail( 45 | rawValue => GenericPropertyParser.Parse(rawValue, allowCollections: false), 46 | StringPropertyValue); 47 | } 48 | 49 | private void AssertSuccess( 50 | Func itemParseFn, 51 | object collectionToParse, 52 | IEnumerable expectedCollection) 53 | { 54 | ValueParseResult parseResult = CollectionParser.Parse(collectionToParse, itemParseFn); 55 | Assert.That(parseResult.Success, Is.True); 56 | Assert.That(parseResult.Value, Is.EquivalentTo(expectedCollection)); 57 | } 58 | 59 | private void AssertFail( 60 | Func itemParseFn, 61 | object collectionToParse) 62 | { 63 | ValueParseResult parseResult = CollectionParser.Parse(collectionToParse, itemParseFn); 64 | Assert.That(parseResult.Success, Is.False); 65 | Assert.That(parseResult.Value, Is.Null); 66 | Assert.That(parseResult.ErrorDetails, Is.Not.Empty); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MixpanelProperty.cs: -------------------------------------------------------------------------------- 1 | namespace Mixpanel 2 | { 3 | /// 4 | /// Contains predefined property names for Mixpanel that can be used for message building. 5 | /// 6 | public static class MixpanelProperty 7 | { 8 | /// 9 | /// Token for both track and engage messages. 10 | /// 11 | public const string Token = "token"; 12 | 13 | /// 14 | /// Distinct ID for both track and engage messages. 15 | /// 16 | public const string DistinctId = "distinct_id"; 17 | 18 | /// 19 | /// Time for both track and engage messages. 20 | /// 21 | public const string Time = "time"; 22 | 23 | /// 24 | /// Ip for both track and engage messages. 25 | /// 26 | public const string Ip = "ip"; 27 | 28 | /// 29 | /// Duration for track messages. 30 | /// 31 | public const string Duration = "duration"; 32 | 33 | /// 34 | /// Operating system for track messages. 35 | /// 36 | public const string Os = "os"; 37 | 38 | /// 39 | /// Screen width for track messages. 40 | /// 41 | public const string ScreenWidth = "screen_width"; 42 | 43 | /// 44 | /// Screen height for track messages. 45 | /// 46 | public const string ScreenHeight = "screen_height"; 47 | 48 | /// 49 | /// First name for engage messages. 50 | /// 51 | public const string FirstName = "first_name"; 52 | 53 | /// 54 | /// Last name for engage messages. 55 | /// 56 | public const string LastName = "last_name"; 57 | 58 | /// 59 | /// Name for engage messages. 60 | /// 61 | public const string Name = "name"; 62 | 63 | /// 64 | /// Created for engage messages. 65 | /// 66 | public const string Created = "created"; 67 | 68 | /// 69 | /// Email for engage messages. 70 | /// 71 | public const string Email = "email"; 72 | 73 | /// 74 | /// Phone for engage messages. 75 | /// 76 | public const string Phone = "phone"; 77 | 78 | /// 79 | /// Ignore time for engage messages. 80 | /// 81 | public const string IgnoreTime = "ignore_time"; 82 | 83 | /// 84 | /// Ignore alias for engage messages. 85 | /// 86 | public const string IgnoreAlias = "ignore_alias"; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MixpanelConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Mixpanel 6 | { 7 | /// 8 | /// Provides properties for configuring custom behaviour of the library. You can use config by setting 9 | /// values on MixpanelConfig.Global, or by passing an instance to 10 | /// constructor (in this case configuration property will be first checked from passed instance, 11 | /// and if it's not set, then from MixpanelConfig.Global). 12 | /// 13 | public class MixpanelConfig 14 | { 15 | /// 16 | /// Gets or sets user defined JSON serialization function. Takes an object as a parameter and returns 17 | /// serialized JSON string. 18 | /// 19 | public Func SerializeJsonFn { get; set; } 20 | 21 | /// 22 | /// Gets or sets user defined function that will make async HTTP POST requests to mixpanel endpoints. 23 | /// Takes 3 parameters: url, content and cancellation token. Returns true if call was successful, and false otherwise. 24 | /// 25 | public Func> AsyncHttpPostFn { get; set; } 26 | 27 | /// 28 | /// Gets ot sets user defined function for retrieving error logs. Takes 2 parameters: message and exception. 29 | /// 30 | public Action ErrorLogFn { get; set; } 31 | 32 | /// 33 | /// Gets or sets the format for mixpanel properties. 34 | /// 35 | public MixpanelPropertyNameFormat? MixpanelPropertyNameFormat { get; set; } 36 | 37 | /// 38 | /// Regulates "ip" query string parameter. 39 | /// 40 | public MixpanelIpAddressHandling? IpAddressHandling { get; set; } 41 | 42 | /// 43 | /// Regulates which API host to route data to. 44 | /// 45 | public MixpanelDataResidencyHandling? DataResidencyHandling { get; set; } 46 | 47 | /// 48 | /// A global instance of the config. 49 | /// 50 | public static MixpanelConfig Global { get; } 51 | 52 | static MixpanelConfig() 53 | { 54 | Global = new MixpanelConfig(); 55 | } 56 | 57 | /// 58 | /// Resets all properties for current instance to it's default values. 59 | /// 60 | public void Reset() 61 | { 62 | SerializeJsonFn = null; 63 | AsyncHttpPostFn = null; 64 | ErrorLogFn = null; 65 | MixpanelPropertyNameFormat = null; 66 | IpAddressHandling = null; 67 | DataResidencyHandling = null; 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/MixpanelClient/MixpanelClientTestsBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Newtonsoft.Json; 7 | using Newtonsoft.Json.Linq; 8 | using NUnit.Framework; 9 | 10 | namespace Mixpanel.Tests.Unit.MixpanelClient 11 | { 12 | [TestFixture] 13 | public abstract class MixpanelClientTestsBase : MixpanelTestsBase 14 | { 15 | protected Mixpanel.MixpanelClient Client; 16 | 17 | protected List<(string Endpoint, string Data)> HttpPostEntries; 18 | 19 | protected string TrackUrl { get; set; } 20 | 21 | protected string EngageUrl { get; set; } 22 | 23 | [SetUp] 24 | public void MixpanelClientSetUp() 25 | { 26 | MixpanelConfig.Global.Reset(); 27 | 28 | Client = new Mixpanel.MixpanelClient(Token, GetConfig()); 29 | 30 | HttpPostEntries = new List<(string Endpoint, string Data)>(); 31 | 32 | TrackUrl = string.Format(Client.GetUrlFormat(), Mixpanel.MixpanelClient.EndpointTrack); 33 | EngageUrl = string.Format(Client.GetUrlFormat(), Mixpanel.MixpanelClient.EndpointEngage); 34 | } 35 | 36 | protected static void IncludeDistinctIdIfNeeded(bool includeDistinctId, Dictionary dic) 37 | { 38 | if (includeDistinctId) 39 | { 40 | dic.Add(MixpanelProperty.DistinctId, DistinctId); 41 | } 42 | } 43 | 44 | protected MixpanelConfig GetConfig() 45 | { 46 | return new MixpanelConfig 47 | { 48 | AsyncHttpPostFn = (endpoint, data, cancellationToken) => 49 | { 50 | HttpPostEntries.Add((endpoint, data)); 51 | return Task.Run(() => true); 52 | } 53 | }; 54 | } 55 | 56 | protected JObject ParseMessageData(string data) 57 | { 58 | // Can't use JObject.Parse because it's not possible to disable DateTime parsing 59 | using (var stringReader = new StringReader(GetJsonFromData(data))) 60 | { 61 | using (JsonReader jsonReader = new JsonTextReader(stringReader)) 62 | { 63 | jsonReader.DateParseHandling = DateParseHandling.None; 64 | JObject msg = JObject.Load(jsonReader); 65 | return msg; 66 | } 67 | } 68 | } 69 | 70 | protected string GetJsonFromData(string data) 71 | { 72 | // Remove "data=" to get raw BASE64 73 | string base64 = data.Remove(0, 5); 74 | byte[] bytesString = Convert.FromBase64String(base64); 75 | 76 | var json = Encoding.UTF8.GetString(bytesString, 0, bytesString.Length); 77 | return json; 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MessageBuilders/Track/AliasMessageBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Mixpanel.MessageProperties; 3 | using Mixpanel.Parsers; 4 | 5 | namespace Mixpanel.MessageBuilders.Track 6 | { 7 | // Message example: 8 | // { 9 | // "event": "$create_alias", 10 | // "properties": { 11 | // "token": "e3bc4100330c35722740fb8c6f5abddc", 12 | // "distinct_id": "ORIGINAL_ID", 13 | // "alias": "NEW_ID" 14 | // } 15 | // } 16 | 17 | internal static class AliasMessageBuilder 18 | { 19 | public static MessageBuildResult Build( 20 | string token, 21 | IEnumerable superProperties, 22 | object distinctId, 23 | object alias) 24 | { 25 | MessageCandidate messageCandidate = TrackMessageBuilderBase.CreateValidMessageCandidate( 26 | token, 27 | superProperties, 28 | null, 29 | distinctId, 30 | null, 31 | out string messageCandidateErrorMessage); 32 | 33 | if (messageCandidate == null) 34 | { 35 | return MessageBuildResult.CreateFail(messageCandidateErrorMessage); 36 | } 37 | 38 | var message = new Dictionary(2); 39 | message["event"] = "$create_alias"; 40 | 41 | var properties = new Dictionary(3); 42 | message["properties"] = properties; 43 | 44 | // token 45 | properties["token"] = messageCandidate.GetSpecialProperty(TrackSpecialProperty.Token).Value; 46 | 47 | // distinct_id 48 | ObjectProperty rawDistinctId = messageCandidate.GetSpecialProperty(TrackSpecialProperty.DistinctId); 49 | if (rawDistinctId == null) 50 | { 51 | return MessageBuildResult.CreateFail($"'{TrackSpecialProperty.DistinctId}' is not set."); 52 | } 53 | 54 | ValueParseResult distinctIdParseResult = DistinctIdParser.Parse(rawDistinctId.Value); 55 | if (!distinctIdParseResult.Success) 56 | { 57 | return MessageBuildResult.CreateFail( 58 | $"Error parsing '{TrackSpecialProperty.DistinctId}'.", distinctIdParseResult.ErrorDetails); 59 | } 60 | 61 | properties["distinct_id"] = distinctIdParseResult.Value; 62 | 63 | // alias 64 | ValueParseResult aliasParseResult = DistinctIdParser.Parse(alias); 65 | if (!aliasParseResult.Success) 66 | { 67 | return MessageBuildResult.CreateFail("Error parsing 'alias'. " + aliasParseResult.ErrorDetails); 68 | } 69 | properties["alias"] = aliasParseResult.Value; 70 | 71 | 72 | return MessageBuildResult.CreateSuccess(message); 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/MixpanelClient/HttpMockMixpanelConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Newtonsoft.Json; 7 | using Newtonsoft.Json.Linq; 8 | 9 | namespace Mixpanel.Tests.Unit.MixpanelClient 10 | { 11 | public class HttpMockMixpanelConfig where TMessage : class 12 | { 13 | public MixpanelConfig Instance { get; } 14 | public List<(string Endpoint, TMessage Message)> Messages { get; } 15 | public bool RequestCancelled { get; private set; } 16 | 17 | public HttpMockMixpanelConfig(MixpanelConfig config = null) 18 | { 19 | Instance = config ?? new MixpanelConfig(); 20 | Messages = new List<(string Endpoint, TMessage Message)>(); 21 | 22 | if (Instance.AsyncHttpPostFn != null) 23 | { 24 | throw new Exception("AsyncHttpPostFn is expected to be null."); 25 | } 26 | 27 | Instance.AsyncHttpPostFn = async (endpoint, data, cancellationToken) => 28 | { 29 | if (cancellationToken.CanBeCanceled) 30 | { 31 | cancellationToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(1)); 32 | } 33 | 34 | RequestCancelled = cancellationToken.IsCancellationRequested; 35 | cancellationToken.ThrowIfCancellationRequested(); 36 | 37 | Messages.Add((endpoint, ParseMessageData(data))); 38 | return await Task.FromResult(true); 39 | }; 40 | } 41 | 42 | private TMessage ParseMessageData(string data) 43 | { 44 | // Can't use JObject.Parse because it's not possible to disable DateTime parsing 45 | using (var stringReader = new StringReader(GetJsonFromData(data))) 46 | { 47 | using (JsonReader jsonReader = new JsonTextReader(stringReader)) 48 | { 49 | jsonReader.DateParseHandling = DateParseHandling.None; 50 | 51 | if (typeof(TMessage) == typeof(JObject)) 52 | { 53 | return JObject.Load(jsonReader) as TMessage; 54 | } 55 | 56 | if (typeof(TMessage) == typeof(JArray)) 57 | { 58 | return JArray.Load(jsonReader) as TMessage; 59 | } 60 | 61 | throw new NotSupportedException($"{typeof(TMessage)} is not supported."); 62 | } 63 | } 64 | } 65 | 66 | private string GetJsonFromData(string data) 67 | { 68 | // Remove "data=" to get raw BASE64 69 | string base64 = data.Remove(0, 5); 70 | byte[] bytesString = Convert.FromBase64String(base64); 71 | 72 | var json = Encoding.UTF8.GetString(bytesString, 0, bytesString.Length); 73 | return json; 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/Parsers/TimeParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | 4 | namespace Mixpanel.Parsers 5 | { 6 | internal static class TimeParser 7 | { 8 | private const long UnixEpoch = 621355968000000000L; 9 | private const string MixpanelDateFormat = "yyyy-MM-ddTHH:mm:ss"; 10 | 11 | public static ValueParseResult ParseUnix(object rawDateTime) 12 | { 13 | switch (rawDateTime) 14 | { 15 | case null: 16 | return ValueParseResult.CreateFail("Can't be null."); 17 | 18 | case DateTime dateTime: 19 | return ValueParseResult.CreateSuccess( 20 | ToUnixTime(dateTime.ToUniversalTime().Ticks)); 21 | 22 | case DateTimeOffset dateTimeOffset: 23 | return ValueParseResult.CreateSuccess( 24 | ToUnixTime(dateTimeOffset.ToUniversalTime().Ticks)); 25 | 26 | case long unixTime: 27 | return ValueParseResult.CreateSuccess(unixTime); 28 | 29 | default: 30 | return ValueParseResult.CreateFail( 31 | "Expected types are: DateTime, DateTimeOffset or long (unix time)."); 32 | } 33 | 34 | long ToUnixTime(long ticks) 35 | { 36 | return (ticks - UnixEpoch) / TimeSpan.TicksPerSecond; 37 | } 38 | } 39 | 40 | public static ValueParseResult ParseMixpanelFormat(object rawDateTime) 41 | { 42 | switch (rawDateTime) 43 | { 44 | case null: 45 | return ValueParseResult.CreateFail("Can't be null."); 46 | 47 | case DateTime dateTime: 48 | return ValueParseResult.CreateSuccess( 49 | dateTime.ToUniversalTime().ToString(MixpanelDateFormat)); 50 | 51 | case DateTimeOffset dateTimeOffset: 52 | return ValueParseResult.CreateSuccess( 53 | dateTimeOffset.ToUniversalTime().ToString(MixpanelDateFormat)); 54 | 55 | case string str: 56 | bool isValidDateTimeString = DateTime.TryParseExact( 57 | str, 58 | MixpanelDateFormat, 59 | CultureInfo.InvariantCulture, 60 | DateTimeStyles.AssumeUniversal, 61 | out var _); 62 | return isValidDateTimeString 63 | ? ValueParseResult.CreateSuccess(str) 64 | : ValueParseResult.CreateFail($"Expected date time format is: '{MixpanelDateFormat}'."); 65 | 66 | default: 67 | return ValueParseResult.CreateFail( 68 | "Expected types are: DateTime, DateTimeOffset or correctly formatted Mixpanel date string (yyyy-MM-ddTHH:mm:ss)."); 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/ConfigHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Mixpanel.Json; 5 | 6 | namespace Mixpanel 7 | { 8 | internal static class ConfigHelper 9 | { 10 | public static Func GetSerializeJsonFn(MixpanelConfig config) 11 | { 12 | if (config != null && config.SerializeJsonFn != null) 13 | return config.SerializeJsonFn; 14 | 15 | if (MixpanelConfig.Global.SerializeJsonFn != null) 16 | return MixpanelConfig.Global.SerializeJsonFn; 17 | 18 | return MixpanelJsonSerializer.Serialize; 19 | } 20 | 21 | public static Func> GetHttpPostAsyncFn(MixpanelConfig config) 22 | { 23 | if (config != null && config.AsyncHttpPostFn != null) 24 | return config.AsyncHttpPostFn; 25 | 26 | if (MixpanelConfig.Global.AsyncHttpPostFn != null) 27 | return MixpanelConfig.Global.AsyncHttpPostFn; 28 | 29 | return new DefaultHttpClient().PostAsync; 30 | } 31 | 32 | public static Action GetErrorLogFn(MixpanelConfig config) 33 | { 34 | if (config != null && config.ErrorLogFn != null) 35 | return config.ErrorLogFn; 36 | 37 | if (MixpanelConfig.Global.ErrorLogFn != null) 38 | return MixpanelConfig.Global.ErrorLogFn; 39 | 40 | return null; 41 | } 42 | 43 | public static MixpanelPropertyNameFormat GetMixpanelPropertyNameFormat(MixpanelConfig config) 44 | { 45 | if (config != null && config.MixpanelPropertyNameFormat != null) 46 | { 47 | return config.MixpanelPropertyNameFormat.Value; 48 | } 49 | 50 | if (MixpanelConfig.Global.MixpanelPropertyNameFormat != null) 51 | { 52 | return MixpanelConfig.Global.MixpanelPropertyNameFormat.Value; 53 | } 54 | 55 | return MixpanelPropertyNameFormat.None; 56 | } 57 | 58 | public static MixpanelIpAddressHandling GetIpAddressHandling(MixpanelConfig config) 59 | { 60 | if (config != null && config.IpAddressHandling != null) 61 | { 62 | return config.IpAddressHandling.Value; 63 | } 64 | 65 | if (MixpanelConfig.Global.IpAddressHandling != null) 66 | { 67 | return MixpanelConfig.Global.IpAddressHandling.Value; 68 | } 69 | 70 | return MixpanelIpAddressHandling.None; 71 | } 72 | 73 | public static MixpanelDataResidencyHandling GetDataResidencyHandling(MixpanelConfig config) 74 | { 75 | if (config != null && config.DataResidencyHandling != null) 76 | { 77 | return config.DataResidencyHandling.Value; 78 | } 79 | 80 | if (MixpanelConfig.Global.DataResidencyHandling != null) 81 | { 82 | return MixpanelConfig.Global.DataResidencyHandling.Value; 83 | } 84 | 85 | return MixpanelDataResidencyHandling.Default; 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MessageBuilders/Track/TrackMessageBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Mixpanel.MessageProperties; 3 | using Mixpanel.Parsers; 4 | 5 | namespace Mixpanel.MessageBuilders.Track 6 | { 7 | // Message example: 8 | // { 9 | // "event": "Signed Up", 10 | // "properties": { 11 | // "token": "e3bc4100330c35722740fb8c6f5abddc", 12 | // "distinct_id": "13793", 13 | // "Referred By": "Friend" 14 | // } 15 | // } 16 | 17 | internal static class TrackMessageBuilder 18 | { 19 | public static MessageBuildResult Build( 20 | string token, 21 | string @event, 22 | IEnumerable superProperties, 23 | object rawProperties, 24 | object distinctId, 25 | MixpanelConfig config) 26 | { 27 | if (string.IsNullOrWhiteSpace(@event)) 28 | { 29 | return MessageBuildResult.CreateFail($"'{nameof(@event)}' is not set."); 30 | } 31 | 32 | MessageCandidate messageCandidate = TrackMessageBuilderBase.CreateValidMessageCandidate( 33 | token, 34 | superProperties, 35 | rawProperties, 36 | distinctId, 37 | config, 38 | out string messageCandidateErrorMessage); 39 | 40 | if (messageCandidate == null) 41 | { 42 | return MessageBuildResult.CreateFail(messageCandidateErrorMessage); 43 | } 44 | 45 | var message = new Dictionary(2); 46 | message["event"] = @event; 47 | 48 | var properties = new Dictionary(); 49 | message["properties"] = properties; 50 | 51 | // Special properties 52 | foreach (KeyValuePair pair in messageCandidate.SpecialProperties) 53 | { 54 | string specialPropertyName = pair.Key; 55 | ObjectProperty objectProperty = pair.Value; 56 | 57 | ValueParseResult result = 58 | TrackSpecialPropertyParser.Parse(specialPropertyName, objectProperty.Value); 59 | if (!result.Success) 60 | { 61 | // The only required special properties are 'event' and 'token' which are controlled separately 62 | continue; 63 | } 64 | 65 | properties[specialPropertyName] = result.Value; 66 | } 67 | 68 | // User properties 69 | foreach (KeyValuePair pair in messageCandidate.UserProperties) 70 | { 71 | string formattedPropertyName = pair.Key; 72 | ObjectProperty objectProperty = pair.Value; 73 | 74 | ValueParseResult result = GenericPropertyParser.Parse(objectProperty.Value, allowCollections: true); 75 | if (!result.Success) 76 | { 77 | continue; 78 | } 79 | 80 | properties[formattedPropertyName] = result.Value; 81 | } 82 | 83 | return MessageBuildResult.CreateSuccess(message); 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MessageProperties/PeopleSpecialPropertyMapper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Mixpanel.Parsers; 3 | 4 | namespace Mixpanel.MessageProperties 5 | { 6 | internal static class PeopleSpecialPropertyMapper 7 | { 8 | private static readonly Dictionary RawNameToSpecialPropertyMap; 9 | 10 | static PeopleSpecialPropertyMapper() 11 | { 12 | RawNameToSpecialPropertyMap = new Dictionary(new PropertyNameComparer()) 13 | { 14 | { "token", PeopleSpecialProperty.Token }, 15 | { "$token", PeopleSpecialProperty.Token }, 16 | 17 | { "distinctid", PeopleSpecialProperty.DistinctId }, 18 | { "distinct_id", PeopleSpecialProperty.DistinctId }, 19 | { "$distinctid", PeopleSpecialProperty.DistinctId }, 20 | { "$distinct_id", PeopleSpecialProperty.DistinctId }, 21 | 22 | { "ip", PeopleSpecialProperty.Ip }, 23 | { "$ip", PeopleSpecialProperty.Ip }, 24 | 25 | { "time", PeopleSpecialProperty.Time }, 26 | { "$time", PeopleSpecialProperty.Time }, 27 | 28 | { "ignoretime", PeopleSpecialProperty.IgnoreTime }, 29 | { "ignore_time", PeopleSpecialProperty.IgnoreTime }, 30 | { "$ignoretime", PeopleSpecialProperty.IgnoreTime }, 31 | { "$ignore_time", PeopleSpecialProperty.IgnoreTime }, 32 | 33 | { "ignorealias", PeopleSpecialProperty.IgnoreAlias }, 34 | { "ignore_alias", PeopleSpecialProperty.IgnoreAlias }, 35 | { "$ignorealias", PeopleSpecialProperty.IgnoreAlias }, 36 | { "$ignore_alias", PeopleSpecialProperty.IgnoreAlias }, 37 | 38 | {"firstname", PeopleSpecialProperty.FirstName }, 39 | {"first_name", PeopleSpecialProperty.FirstName }, 40 | {"$firstname", PeopleSpecialProperty.FirstName }, 41 | {"$first_name", PeopleSpecialProperty.FirstName }, 42 | 43 | {"lastname", PeopleSpecialProperty.LastName }, 44 | {"last_name", PeopleSpecialProperty.LastName }, 45 | {"$lastname", PeopleSpecialProperty.LastName }, 46 | {"$last_name", PeopleSpecialProperty.LastName }, 47 | 48 | {"name", PeopleSpecialProperty.Name }, 49 | {"$name", PeopleSpecialProperty.Name }, 50 | 51 | {"email", PeopleSpecialProperty.Email }, 52 | {"$email", PeopleSpecialProperty.Email }, 53 | {"e-mail", PeopleSpecialProperty.Email }, 54 | {"$e-mail", PeopleSpecialProperty.Email }, 55 | 56 | {"phone", PeopleSpecialProperty.Phone }, 57 | {"$phone", PeopleSpecialProperty.Phone }, 58 | 59 | {"created", PeopleSpecialProperty.Created }, 60 | {"$created", PeopleSpecialProperty.Created } 61 | }; 62 | } 63 | 64 | public static string RawNameToSpecialProperty(string propertyName) 65 | { 66 | return RawNameToSpecialPropertyMap.TryGetValue(propertyName, out var specialProperty) 67 | ? specialProperty 68 | : null; 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MessageKind.cs: -------------------------------------------------------------------------------- 1 | namespace Mixpanel 2 | { 3 | /// 4 | /// Kind (type) of . 5 | /// 6 | public enum MessageKind 7 | { 8 | /// 9 | /// Track message. If you manually create a , please check 10 | /// corresponding Test method for a correct message structure. 11 | /// 12 | Track, 13 | 14 | /// 15 | /// Alias message. If you manually create a , please check 16 | /// corresponding Test method for a correct message structure. 17 | /// 18 | Alias, 19 | 20 | /// 21 | /// PeopleSet message. If you manually create a , please check 22 | /// corresponding Test method for a correct message structure. 23 | /// 24 | PeopleSet, 25 | 26 | /// 27 | /// PeopleSetOnce message. If you manually create a , please check 28 | /// corresponding Test method for a correct message structure. 29 | /// 30 | PeopleSetOnce, 31 | 32 | /// 33 | /// PeopleAdd message. If you manually create a , please check 34 | /// corresponding Test method for a correct message structure. 35 | /// 36 | PeopleAdd, 37 | 38 | /// 39 | /// PeopleAppend message. If you manually create a , please check 40 | /// corresponding Test method for a correct message structure. 41 | /// 42 | PeopleAppend, 43 | 44 | /// 45 | /// PeopleUnion message. If you manually create a , please check 46 | /// corresponding Test method for a correct message structure. 47 | /// 48 | PeopleUnion, 49 | 50 | /// 51 | /// PeopleRemove message. If you manually create a , please check 52 | /// corresponding Test method for a correct message structure. 53 | /// 54 | PeopleRemove, 55 | 56 | /// 57 | /// PeopleUnset message. If you manually create a , please check 58 | /// corresponding Test method for a correct message structure. 59 | /// 60 | PeopleUnset, 61 | 62 | /// 63 | /// PeopleDelete message. If you manually create a , please check 64 | /// corresponding Test method for a correct message structure. 65 | /// 66 | PeopleDelete, 67 | 68 | /// 69 | /// PeopleTrackCharge message. If you manually create a , please check 70 | /// corresponding Test method for a correct message structure. 71 | /// 72 | PeopleTrackCharge, 73 | 74 | /// 75 | /// Batch message. Used internally. If you manually create a message of this kind and try to 76 | /// send it, it will be ignored. 77 | /// 78 | Batch 79 | } 80 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mixpanel-csharp [![NuGet](http://img.shields.io/nuget/v/mixpanel-csharp.svg)](https://www.nuget.org/packages/mixpanel-csharp/) [![Downloads](https://img.shields.io/nuget/dt/mixpanel-csharp.svg)](https://www.nuget.org/packages/mixpanel-csharp/) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.md) 2 | [Mixpanel](https://mixpanel.com/) is a great analitics platform, but unfortunetally there is no official integration library for .NET. So if you are writing code on .NET and want to use Mixpanel, then ```mixpanel-csharp``` can be an excellent choice. ```mixpanel-csharp``` main idea is to hide most api details (you don't need to remember what time formatting to use, or in which cases you should prefix properties with ```$```) and concentrate on data that you want to analyze. 3 | 4 | ## Features 5 | - Supports the following [Mixpanel Ingestion API's](https://developer.mixpanel.com/reference/ingestion-api): 6 | - [Track Events](https://developer.mixpanel.com/reference/track-event) 7 | - [User Profiles](https://developer.mixpanel.com/reference/user-profiles) 8 | - [Send messages simultaneously](https://github.com/eealeivan/mixpanel-csharp/wiki/Sending-messages) or just create a message instance and send it later 9 | - Pass [the message data](https://github.com/eealeivan/mixpanel-csharp/wiki/Message-data) in form that you prefer: predefined contract, `IDictionary`, anonymous type or dynamic 10 | - Add properties globally to all messages with super properties. Usable for properties such as `distinct_id` 11 | - Great [configurability](https://github.com/eealeivan/mixpanel-csharp/wiki/Configuration). For example you can provide your own JSON serializer or function that will make HTTP requests 12 | - No dependencies. Keeps your project clean 13 | - Runs on .NET 4.6.1 and [.NET Standard 2.0](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) 14 | - Detailed [documentation](https://github.com/eealeivan/mixpanel-csharp/wiki) 15 | 16 | ## Sample usage for track message 17 | ```csharp 18 | var mc = new MixpanelClient("e3bc4100330c35722740fb8c6f5abddc"); 19 | await mc.TrackAsync("Level Complete", new { 20 | DistinctId = "12345", 21 | LevelNumber = 5, 22 | Duration = TimeSpan.FromMinutes(1) 23 | }); 24 | ``` 25 | This will send the following JSON to `https://api.mixpanel.com/track/`: 26 | ```json 27 | { 28 | "event": "Level Complete", 29 | "properties": { 30 | "token": "e3bc4100330c35722740fb8c6f5abddc", 31 | "distinct_id": "12345", 32 | "LevelNumber": 5, 33 | "$duration": 60 34 | } 35 | } 36 | ``` 37 | 38 | ## Sample usage for profile message 39 | ```csharp 40 | var mc = new MixpanelClient("e3bc4100330c35722740fb8c6f5abddc"); 41 | await mc.PeopleSetAsync(new { 42 | DistinctId = "12345", 43 | Name = "Darth Vader", 44 | Kills = 215 45 | }); 46 | ``` 47 | This will send the following JSON to `https://api.mixpanel.com/engage/`: 48 | ```json 49 | { 50 | "$token": "e3bc4100330c35722740fb8c6f5abddc", 51 | "$distinct_id": "12345", 52 | "$set": { 53 | "$name": "Darth Vader", 54 | "Kills": 215 55 | } 56 | } 57 | ``` 58 | 59 | ## Copyright 60 | Copyright © 2022 Aleksandr Ivanov 61 | 62 | ## License 63 | ```mixpanel-csharp``` is licensed under [MIT](https://www.opensource.org/licenses/mit-license.php). Refer to [LICENSE](https://github.com/eealeivan/mixpanel-csharp/blob/master/LICENSE) for more information. 64 | -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/MixpanelClient/Track/MixpanelClientTrackTestsBase.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Newtonsoft.Json.Linq; 4 | using NUnit.Framework; 5 | 6 | namespace Mixpanel.Tests.Unit.MixpanelClient.Track 7 | { 8 | [TestFixture] 9 | public abstract class MixpanelClientTrackTestsBase : MixpanelClientTestsBase 10 | { 11 | protected TrackSuperPropsDetails? SuperPropsDetails { get; private set; } 12 | 13 | [SetUp] 14 | public void MixpanelClientPeopleSetUp() 15 | { 16 | var properties = TestContext.CurrentContext.Test.Properties; 17 | if (properties != null && properties.ContainsKey(TrackSuperPropsAttribute.Name)) 18 | { 19 | SuperPropsDetails = (TrackSuperPropsDetails)properties[TrackSuperPropsAttribute.Name].Single(); 20 | Client = new Mixpanel.MixpanelClient(Token, GetConfig(), GetSuperProperties()); 21 | } 22 | } 23 | 24 | protected void AssertJsonSpecialProperties( 25 | JObject msg, 26 | object expectedDistinctId) 27 | { 28 | Assert.That(msg.Count, Is.EqualTo(2)); 29 | Assert.That(msg["event"].Value(), Is.EqualTo(Event)); 30 | 31 | var props = (JObject)msg["properties"]; 32 | Assert.That(props["token"].Value(), Is.EqualTo(Token)); 33 | if (expectedDistinctId != null) 34 | { 35 | Assert.That(props["distinct_id"].Value(), Is.EqualTo(expectedDistinctId)); 36 | } 37 | Assert.That(props["ip"].Value(), Is.EqualTo(Ip)); 38 | Assert.That(props["time"].Value(), Is.EqualTo(TimeUnix)); 39 | } 40 | 41 | protected void AssertDictionarySpecialProperties( 42 | IDictionary dic, 43 | object expectedDistinctId) 44 | { 45 | Assert.That(dic.Count, Is.EqualTo(2)); 46 | Assert.That(dic["event"], Is.EqualTo(Event)); 47 | 48 | Assert.That(dic["properties"], Is.TypeOf>()); 49 | var props = (Dictionary)dic["properties"]; 50 | Assert.That(props["token"], Is.EqualTo(Token)); 51 | if (expectedDistinctId != null) 52 | { 53 | Assert.That(props["distinct_id"], Is.EqualTo(expectedDistinctId)); 54 | } 55 | Assert.That(props["ip"], Is.EqualTo(Ip)); 56 | Assert.That(props["time"], Is.EqualTo(TimeUnix)); 57 | } 58 | 59 | private IDictionary GetSuperProperties() 60 | { 61 | var dic = new Dictionary(); 62 | 63 | if (SuperPropsDetails?.HasFlag(TrackSuperPropsDetails.DistinctId) ?? false) 64 | { 65 | dic.Add(MixpanelProperty.DistinctId, SuperDistinctId); 66 | } 67 | 68 | if (SuperPropsDetails?.HasFlag(TrackSuperPropsDetails.SpecialProperties) ?? false) 69 | { 70 | } 71 | 72 | if (SuperPropsDetails?.HasFlag(TrackSuperPropsDetails.UserProperties) ?? false) 73 | { 74 | dic.Add(DecimalSuperPropertyName, DecimalSuperPropertyValue); 75 | dic.Add(StringSuperPropertyName, StringSuperPropertyValue); 76 | } 77 | 78 | return dic; 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MessageBuilders/People/PeopleSetMessageBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Mixpanel.MessageProperties; 3 | using Mixpanel.Parsers; 4 | 5 | namespace Mixpanel.MessageBuilders.People 6 | { 7 | // Message example: 8 | // { 9 | // "$token": "36ada5b10da39a1347559321baf13063", 10 | // "$distinct_id": "13793", 11 | // "$ip": "123.123.123.123", 12 | // "$set": { 13 | // "Address": "1313 Mockingbird Lane" 14 | // } 15 | // } 16 | 17 | internal static class PeopleSetMessageBuilder 18 | { 19 | public static MessageBuildResult BuildSet( 20 | string token, 21 | IEnumerable superProperties, 22 | object rawProperties, 23 | object distinctId, 24 | MixpanelConfig config) 25 | { 26 | return Build("$set", token, superProperties, rawProperties, distinctId, config); 27 | } 28 | 29 | public static MessageBuildResult BuildSetOnce( 30 | string token, 31 | IEnumerable superProperties, 32 | object rawProperties, 33 | object distinctId, 34 | MixpanelConfig config) 35 | { 36 | return Build("$set_once", token, superProperties, rawProperties, distinctId, config); 37 | } 38 | 39 | private static MessageBuildResult Build( 40 | string operation, 41 | string token, 42 | IEnumerable superProperties, 43 | object rawProperties, 44 | object distinctId, 45 | MixpanelConfig config) 46 | { 47 | MessageCandidate messageCandidate = PeopleMessageBuilderBase.CreateValidMessageCandidate( 48 | token, 49 | superProperties, 50 | rawProperties, 51 | distinctId, 52 | config, 53 | out string messageCandidateErrorMessage); 54 | 55 | if (messageCandidate == null) 56 | { 57 | return MessageBuildResult.CreateFail(messageCandidateErrorMessage); 58 | } 59 | 60 | var message = new Dictionary(); 61 | var set = new Dictionary(); 62 | message[operation] = set; 63 | 64 | // Special properties 65 | PeopleMessageBuilderBase.RunForValidSpecialProperties( 66 | messageCandidate, 67 | (specialPropertyName, isMessageSpecialProperty, value) => 68 | { 69 | if (isMessageSpecialProperty) 70 | { 71 | message[specialPropertyName] = value; 72 | } 73 | else 74 | { 75 | set[specialPropertyName] = value; 76 | } 77 | }); 78 | 79 | // User properties 80 | PeopleMessageBuilderBase.RunForValidUserProperties( 81 | messageCandidate, 82 | rawValue => GenericPropertyParser.Parse(rawValue, allowCollections: true), 83 | (formattedPropertyName, value) => 84 | { 85 | set[formattedPropertyName] = value; 86 | }); 87 | 88 | return MessageBuildResult.CreateSuccess(message); 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/MixpanelClient/People/MixpanelClientPeopleTestsBase.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Newtonsoft.Json.Linq; 4 | using NUnit.Framework; 5 | 6 | namespace Mixpanel.Tests.Unit.MixpanelClient.People 7 | { 8 | [TestFixture] 9 | public abstract class MixpanelClientPeopleTestsBase : MixpanelClientTestsBase 10 | { 11 | protected PeopleSuperPropsDetails? SuperPropsDetails { get; private set; } 12 | 13 | [SetUp] 14 | public void MixpanelClientPeopleSetUp() 15 | { 16 | var properties = TestContext.CurrentContext.Test.Properties; 17 | if (properties != null && properties.ContainsKey(PeopleSuperPropsAttribute.Name)) 18 | { 19 | SuperPropsDetails = (PeopleSuperPropsDetails)properties[PeopleSuperPropsAttribute.Name].Single(); 20 | Client = new Mixpanel.MixpanelClient(Token, GetConfig(), GetSuperProperties()); 21 | } 22 | } 23 | 24 | protected void AssertJsonMessageProperties( 25 | JObject msg, 26 | object expectedDistinctId) 27 | { 28 | Assert.That(msg.Count, Is.EqualTo(7)); // +1 for operation 29 | Assert.That(msg["$token"].Value(), Is.EqualTo(Token)); 30 | Assert.That(msg["$distinct_id"].Value(), Is.EqualTo(expectedDistinctId)); 31 | Assert.That(msg["$ip"].Value(), Is.EqualTo(Ip)); 32 | Assert.That(msg["$time"].Value(), Is.EqualTo(TimeUnix)); 33 | Assert.That(msg["$ignore_time"].Value(), Is.EqualTo(IgnoreTime)); 34 | Assert.That(msg["$ignore_alias"].Value(), Is.EqualTo(IgnoreAlias)); 35 | } 36 | 37 | protected void AssertDictionaryMessageProperties( 38 | IDictionary dic, 39 | object expectedDistinctId) 40 | { 41 | Assert.That(dic.Count, Is.EqualTo(7)); // +1 for operation 42 | Assert.That(dic["$token"], Is.EqualTo(Token)); 43 | Assert.That(dic["$distinct_id"], Is.EqualTo(expectedDistinctId)); 44 | Assert.That(dic["$ip"], Is.EqualTo(Ip)); 45 | Assert.That(dic["$time"], Is.EqualTo(TimeUnix)); 46 | Assert.That(dic["$ignore_time"], Is.EqualTo(IgnoreTime)); 47 | Assert.That(dic["$ignore_alias"], Is.EqualTo(IgnoreAlias)); 48 | } 49 | 50 | private IDictionary GetSuperProperties() 51 | { 52 | var dic = new Dictionary(); 53 | 54 | if (SuperPropsDetails?.HasFlag(PeopleSuperPropsDetails.DistinctId) ?? false) 55 | { 56 | dic.Add(MixpanelProperty.DistinctId, SuperDistinctId); 57 | } 58 | 59 | if (SuperPropsDetails?.HasFlag(PeopleSuperPropsDetails.MessageSpecialProperties) ?? false) 60 | { 61 | dic.Add(MixpanelProperty.Ip, Ip); 62 | dic.Add(MixpanelProperty.Time, Time); 63 | dic.Add(MixpanelProperty.IgnoreTime, IgnoreTime); 64 | dic.Add(MixpanelProperty.IgnoreAlias, IgnoreAlias); 65 | } 66 | 67 | if (SuperPropsDetails?.HasFlag(PeopleSuperPropsDetails.UserProperties) ?? false) 68 | { 69 | dic.Add(DecimalSuperPropertyName, DecimalSuperPropertyValue); 70 | dic.Add(StringSuperPropertyName, StringSuperPropertyValue); 71 | } 72 | 73 | return dic; 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/Mixpanel.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net461;netstandard2.0 5 | true 6 | false 7 | 6.0.0.0 8 | 6.0.0.0 9 | Mixpanel C# 10 | 6.0.0 11 | Aleksandr Ivanov 12 | mixpanel-csharp 13 | Copyright © Aleksandr Ivanov 2022 14 | MIT 15 | https://github.com/eealeivan/mixpanel-csharp 16 | An open source Mixpanel .NET integration library that supports complete Mixpanel API. The main idea of the library is to hide API details allowing you to concentrate on data that you want to analyze. Supported platforms: .NET 4.6.1 and .NET Standard 2.0. It's also well documented, configurable and testable. Check the example usage on the project site. 17 | mixpanel;analytics;data;tracking;.NET;netstandard 18 | nuget-readme.md 19 | 20 | - Sign Mixpanel assembly with a strong name 21 | - Update minimal supported versions to .NET Framework 4.6.1 (net461) and .NET Standard 2.0 (netstandard2.0) 22 | - Remove all synchronous methods from (I)MixpanelClient 23 | - Add IgnoreAlias parameter to PeopleDeleteAsync methods 24 | - Add CancellationToken parameter to all asynchronous methods in (I)MixpanelClient 25 | 26 | True 27 | ..\..\..\strong-name-key.snk 28 | 29 | 30 | 31 | 32 | Mixpanel C# .NET 4.6.1 33 | 34 | 35 | TRACE;DEBUG 36 | 37 | 38 | TRACE;RELEASE 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | Mixpanel C# .NET Standard 2.0 50 | 51 | 52 | TRACE;DEBUG 53 | 54 | 55 | TRACE;RELEASE 56 | 57 | 58 | 59 | 60 | 61 | 62 | True 63 | \ 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/Parsers/TimeParserTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Mixpanel.Parsers; 3 | using NUnit.Framework; 4 | 5 | namespace Mixpanel.Tests.Unit.Parsers 6 | { 7 | [TestFixture] 8 | public class TimeParserTests : MixpanelTestsBase 9 | { 10 | [Test] 11 | public void Given_ParsingUnixTime_When_Long_Then_Success() 12 | { 13 | AssertParseUnixSuccess(TimeUnix, TimeUnix); 14 | } 15 | 16 | [Test] 17 | public void Given_ParsingUnixTime_When_DateTime_Then_Success() 18 | { 19 | AssertParseUnixSuccess(Time, TimeUnix); 20 | } 21 | 22 | [Test] 23 | public void Given_ParsingUnixTime_When_DateTimeOffset_Then_Success() 24 | { 25 | AssertParseUnixSuccess(TimeOffset, TimeUnix); 26 | } 27 | 28 | [Test] 29 | public void Given_ParsingUnixTime_When_Null_Then_Fail() 30 | { 31 | DateTime? nullTime = null; 32 | // ReSharper disable once ExpressionIsAlwaysNull 33 | AssertParseUnixFail(nullTime); 34 | } 35 | 36 | [Test] 37 | public void Given_ParsingMixpanelFormatTime_When_DateTime_Then_Success() 38 | { 39 | AssertParseMixpanelFormatSuccess(Time, TimeFormat); 40 | } 41 | 42 | [Test] 43 | public void Given_ParsingMixpanelFormatTime_When_DateTimeOffset_Then_Success() 44 | { 45 | AssertParseMixpanelFormatSuccess(TimeOffset, TimeFormat); 46 | } 47 | 48 | [Test] 49 | public void Given_ParsingMixpanelFormatTime_When_CorrectFormatString_Then_Success() 50 | { 51 | AssertParseMixpanelFormatSuccess(TimeFormat, TimeFormat); 52 | } 53 | 54 | [Test] 55 | public void Given_ParsingMixpanelFormatTime_When_Null_Then_Fail() 56 | { 57 | DateTime? nullTime = null; 58 | // ReSharper disable once ExpressionIsAlwaysNull 59 | AssertParseMixpanelFormatFail(nullTime); 60 | } 61 | 62 | [Test] 63 | public void Given_ParsingMixpanelFormatTime_When_IncorrectFormatString_Then_Fail() 64 | { 65 | AssertParseMixpanelFormatFail("20-JUN-1990 08:03:00"); 66 | } 67 | 68 | private void AssertParseUnixSuccess(object timeToParse, long expectedTime) 69 | { 70 | ValueParseResult parseResult = TimeParser.ParseUnix(timeToParse); 71 | Assert.That(parseResult.Success, Is.True); 72 | Assert.That(parseResult.Value, Is.EqualTo(expectedTime)); 73 | } 74 | 75 | private void AssertParseUnixFail(object timeToParse) 76 | { 77 | ValueParseResult parseResult = TimeParser.ParseUnix(timeToParse); 78 | Assert.That(parseResult.Success, Is.False); 79 | Assert.That(parseResult.Value, Is.Null); 80 | Assert.That(parseResult.ErrorDetails, Is.Not.Empty); 81 | } 82 | 83 | private void AssertParseMixpanelFormatSuccess(object timeToParse, string expectedTime) 84 | { 85 | ValueParseResult parseResult = TimeParser.ParseMixpanelFormat(timeToParse); 86 | Assert.That(parseResult.Success, Is.True); 87 | Assert.That(parseResult.Value, Is.EqualTo(expectedTime)); 88 | } 89 | 90 | private void AssertParseMixpanelFormatFail(object timeToParse) 91 | { 92 | ValueParseResult parseResult = TimeParser.ParseMixpanelFormat(timeToParse); 93 | Assert.That(parseResult.Success, Is.False); 94 | Assert.That(parseResult.Value, Is.Null); 95 | Assert.That(parseResult.ErrorDetails, Is.Not.Empty); 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/MixpanelTestsBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Mixpanel.Tests.Unit 5 | { 6 | public abstract class MixpanelTestsBase 7 | { 8 | protected const string Event = "TestEvent"; 9 | protected const string Token = "1234"; 10 | protected const string DistinctIdPropertyName = "DistinctId"; 11 | protected const string DistinctId = "456"; 12 | protected const string SuperDistinctId = "789"; 13 | protected const int DistinctIdInt = 456; 14 | protected const string Ip = "192.168.0.136"; 15 | protected static readonly DateTime Time = new DateTime(2013, 11, 30, 0, 0, 0, DateTimeKind.Utc); 16 | protected static readonly DateTimeOffset TimeOffset = new DateTimeOffset(2013, 11, 30, 0, 0, 0, TimeSpan.Zero); 17 | protected const long TimeUnix = 1385769600L; 18 | protected const string TimeFormat = "2013-11-30T00:00:00"; 19 | protected const double DurationSeconds = 2.34D; 20 | protected static readonly TimeSpan Duration = TimeSpan.FromSeconds(DurationSeconds); 21 | protected const string Os = "Windows"; 22 | protected const int ScreenWidth = 1920; 23 | protected const int ScreenHeight = 1200; 24 | protected const bool IgnoreTime = true; 25 | protected const bool IgnoreAlias = true; 26 | 27 | protected const string FirstName = "Darth"; 28 | protected const string LastName = "Vader"; 29 | protected const string Name = "Darth Vader"; 30 | protected static readonly DateTime Created = new DateTime(2014, 10, 22, 0, 0, 0, DateTimeKind.Utc); 31 | protected const string CreatedFormat = "2014-10-22T00:00:00"; 32 | protected const string Email = "darth.vader@mail.com"; 33 | protected const string Phone = "589741"; 34 | protected const string Alias = "999"; 35 | 36 | protected const string StringPropertyName = "StringProperty"; 37 | protected const string StringPropertyValue = "Tatooine"; 38 | protected const string StringPropertyName2 = "StringProperty2"; 39 | protected const string StringPropertyValue2 = "Tatooine 2"; 40 | protected const string StringSuperPropertyName = "StringSuperProperty"; 41 | protected const string StringSuperPropertyValue = "Super Tatooine"; 42 | protected const string DecimalPropertyName = "DecimalProperty"; 43 | protected const decimal DecimalPropertyValue = 2.5m; 44 | protected const string DecimalPropertyName2 = "DecimalProperty2"; 45 | protected const decimal DecimalPropertyValue2 = 4.6m; 46 | protected const string DecimalSuperPropertyName = "DecimalSuperProperty"; 47 | protected const decimal DecimalSuperPropertyValue = 10.67m; 48 | protected const string IntPropertyName = "IntProperty"; 49 | protected const int IntPropertyValue = 3; 50 | protected const string DateTimePropertyName = "DateTimeProperty"; 51 | protected static readonly DateTime DateTimePropertyValue = new DateTime(2015, 07, 19, 22, 30, 50, DateTimeKind.Utc); 52 | protected const string DoublePropertyName = "DoubleProperty"; 53 | protected const double DoublePropertyValue = 4.5d; 54 | protected const string InvalidPropertyName = "InvalidProperty"; 55 | protected static readonly object InvalidPropertyValue = new object(); 56 | protected const string InvalidPropertyName2 = "InvalidProperty2"; 57 | protected static readonly object InvalidPropertyValue2 = new object(); 58 | protected static readonly string[] StringPropertyArray = {"Prop1", "Prop2"}; 59 | protected static readonly decimal[] DecimalPropertyArray = {5.5m, 6.6m}; 60 | protected static readonly Dictionary DictionaryWithStringProperty = 61 | new Dictionary 62 | { 63 | {StringPropertyName, StringPropertyValue} 64 | }; 65 | } 66 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MessageBuilders/MessageCandidate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Mixpanel.MessageProperties; 4 | 5 | namespace Mixpanel.MessageBuilders 6 | { 7 | internal sealed class MessageCandidate 8 | { 9 | private readonly MixpanelConfig config; 10 | private readonly Func mapRawNameToSpecialPropertyFn; 11 | 12 | public Dictionary SpecialProperties { get; } 13 | public Dictionary UserProperties { get; } 14 | 15 | public bool HasSpecialProperty(string name) => 16 | SpecialProperties.ContainsKey(name); 17 | 18 | public ObjectProperty GetSpecialProperty(string name) => 19 | SpecialProperties.TryGetValue(name, out var objectProperty) ? objectProperty : null; 20 | 21 | public MessageCandidate( 22 | string token, 23 | IEnumerable superProperties, 24 | object rawProperties, 25 | object distinctId, 26 | MixpanelConfig config, 27 | Func mapRawNameToSpecialPropertyFn) 28 | { 29 | this.config = config; 30 | this.mapRawNameToSpecialPropertyFn = mapRawNameToSpecialPropertyFn; 31 | 32 | SpecialProperties = new Dictionary(); 33 | UserProperties = new Dictionary(); 34 | 35 | ProcessSuperProperties(superProperties); 36 | ProcessRawProperties(rawProperties); 37 | ProcessToken(token); 38 | ProcessDistinctId(distinctId); 39 | } 40 | 41 | private void ProcessSuperProperties(IEnumerable superProperties) 42 | { 43 | if (superProperties == null) 44 | { 45 | return; 46 | } 47 | 48 | foreach (ObjectProperty superProperty in superProperties) 49 | { 50 | ProcessObjectProperty(superProperty); 51 | } 52 | } 53 | 54 | private void ProcessRawProperties(object rawProperties) 55 | { 56 | if (rawProperties == null) 57 | { 58 | return; 59 | } 60 | 61 | foreach (ObjectProperty objectProperty in PropertiesDigger.Get(rawProperties, PropertyOrigin.RawProperty)) 62 | { 63 | ProcessObjectProperty(objectProperty); 64 | } 65 | } 66 | 67 | private void ProcessToken(string token) 68 | { 69 | if (string.IsNullOrWhiteSpace(token)) 70 | { 71 | return; 72 | } 73 | 74 | SpecialProperties[mapRawNameToSpecialPropertyFn("Token")] = 75 | new ObjectProperty("Token", PropertyNameSource.Default, PropertyOrigin.Parameter, token); 76 | } 77 | 78 | private void ProcessDistinctId(object distinctId) 79 | { 80 | if (distinctId == null) 81 | { 82 | return; 83 | } 84 | 85 | SpecialProperties[mapRawNameToSpecialPropertyFn("DistinctId")] = 86 | new ObjectProperty("DistinctId", PropertyNameSource.Default, PropertyOrigin.Parameter, distinctId); 87 | } 88 | 89 | private void ProcessObjectProperty(ObjectProperty objectProperty) 90 | { 91 | string specialProperty = mapRawNameToSpecialPropertyFn(objectProperty.PropertyName); 92 | if (specialProperty != null) 93 | { 94 | SpecialProperties[specialProperty] = objectProperty; 95 | } 96 | else 97 | { 98 | string formattedPropertyName = PropertyNameFormatter.Format(objectProperty, config); 99 | UserProperties[formattedPropertyName] = objectProperty; 100 | } 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MessageProperties/PropertyNameFormatter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text; 4 | using static System.Char; 5 | using static System.String; 6 | 7 | namespace Mixpanel.MessageProperties 8 | { 9 | internal static class PropertyNameFormatter 10 | { 11 | public static string Format( 12 | ObjectProperty objectProperty, 13 | MixpanelConfig config = null) 14 | { 15 | return Format(objectProperty.PropertyName, objectProperty.PropertyNameSource, config); 16 | } 17 | 18 | public static string Format( 19 | string propertyName, 20 | PropertyNameSource propertyNameSource, 21 | MixpanelConfig config = null) 22 | { 23 | MixpanelPropertyNameFormat propertyNameFormat = 24 | ConfigHelper.GetMixpanelPropertyNameFormat(config); 25 | 26 | bool formatNeeded = 27 | propertyNameFormat != MixpanelPropertyNameFormat.None && 28 | propertyNameSource == PropertyNameSource.Default; 29 | if (!formatNeeded || IsNullOrWhiteSpace(propertyName)) 30 | { 31 | return propertyName; 32 | } 33 | 34 | var words = SplitInWords(propertyName); 35 | var newPropertyName = new StringBuilder(propertyName.Length + 5); 36 | 37 | switch (propertyNameFormat) 38 | { 39 | case MixpanelPropertyNameFormat.SentenceCase: 40 | ApplySentenceCase(words); 41 | break; 42 | case MixpanelPropertyNameFormat.TitleCase: 43 | ApplyTitleCase(words); 44 | break; 45 | case MixpanelPropertyNameFormat.LowerCase: 46 | ApplyLowerCase(words); 47 | break; 48 | } 49 | 50 | for (int i = 0; i < words.Length; i++) 51 | { 52 | newPropertyName.Append(words[i]); 53 | if (i != words.Length - 1) 54 | { 55 | newPropertyName.Append(' '); 56 | } 57 | } 58 | 59 | return newPropertyName.ToString(); 60 | } 61 | 62 | private static StringBuilder[] SplitInWords(string propertyName) 63 | { 64 | var word = new StringBuilder(); 65 | word.Append(propertyName[0]); 66 | 67 | var words = new List { word }; 68 | 69 | for (int i = 1; i < propertyName.Length; i++) 70 | { 71 | char c = propertyName[i]; 72 | 73 | bool twoLetterAcronym = 74 | IsUpper(c) && 75 | word.Length == 1 && 76 | IsUpper(word[0]); 77 | 78 | if (twoLetterAcronym) 79 | { 80 | word.Append(c); 81 | AddNewWord(); 82 | } 83 | else if (IsUpper(c)) 84 | { 85 | AddNewWord(); 86 | word.Append(c); 87 | } 88 | else 89 | { 90 | word.Append(c); 91 | } 92 | } 93 | 94 | return words.Where(w => w.Length > 0).ToArray(); 95 | 96 | void AddNewWord() 97 | { 98 | word = new StringBuilder(); 99 | words.Add(word); 100 | } 101 | } 102 | 103 | private static void ApplySentenceCase(StringBuilder[] words) 104 | { 105 | ApplyLowerCase(words.Skip(1)); 106 | ApplyTitleCase(words); 107 | } 108 | 109 | private static void ApplyTitleCase(StringBuilder[] words) 110 | { 111 | words[0][0] = ToUpper(words[0][0]); 112 | } 113 | 114 | private static void ApplyLowerCase(IEnumerable words) 115 | { 116 | foreach (var word in words) 117 | { 118 | for (int i = 0; i < word.Length; i++) 119 | { 120 | word[i] = ToLower(word[i]); 121 | } 122 | } 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/MessageBuilders/People/PeopleTestsBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using Mixpanel.MessageBuilders; 6 | using Mixpanel.MessageProperties; 7 | using Mixpanel.Parsers; 8 | using NUnit.Framework; 9 | 10 | namespace Mixpanel.Tests.Unit.MessageBuilders.People 11 | { 12 | public abstract class PeopleTestsBase : MixpanelTestsBase 13 | { 14 | protected abstract string OperationName { get; } 15 | 16 | internal List CreateSuperProperties(params ObjectProperty[] objectProperties) 17 | { 18 | return objectProperties.ToList(); 19 | } 20 | 21 | internal void AssertMessageSuccess( 22 | MessageBuildResult messageBuildResult, 23 | (string name, object value)[] messageProperties, 24 | (string name, object value)[] operationProperties) 25 | { 26 | AssertMessageSuccess(messageBuildResult); 27 | AssetMessageProperties(messageBuildResult, messageProperties); 28 | AssertOperationProperties(messageBuildResult, operationProperties); 29 | } 30 | 31 | internal void AssertMessageSuccess( 32 | MessageBuildResult messageBuildResult) 33 | { 34 | Assert.That(messageBuildResult.Success, Is.True); 35 | Assert.That(messageBuildResult.Error, Is.Null); 36 | } 37 | 38 | internal void AssetMessageProperties( 39 | MessageBuildResult messageBuildResult, 40 | (string name, object value)[] messageProperties) 41 | { 42 | 43 | IDictionary message = messageBuildResult.Message; 44 | Assert.That(message.Count, Is.EqualTo(messageProperties.Length + 1 /*OPERATION_NAME*/)); 45 | 46 | foreach ((string propertyName, object expectedValue) in messageProperties) 47 | { 48 | bool propertyExists = message.TryGetValue(propertyName, out var actualValue); 49 | 50 | Assert.That(propertyExists, "Missing property: " + propertyName); 51 | Assert.That(expectedValue, Is.EqualTo(actualValue)); 52 | } 53 | } 54 | 55 | internal void AssertOperationProperties( 56 | MessageBuildResult messageBuildResult, 57 | (string name, object value)[] operationProperties) 58 | { 59 | IDictionary message = messageBuildResult.Message; 60 | Assert.That(message.ContainsKey(OperationName)); 61 | var operation = (Dictionary)message[OperationName]; 62 | 63 | if (operationProperties == null) 64 | { 65 | Assert.That(operation.Count, Is.EqualTo(0)); 66 | return; 67 | } 68 | 69 | Assert.That(operation.Count, Is.EqualTo(operationProperties.Length)); 70 | 71 | foreach ((string propertyName, object expectedValue) in operationProperties) 72 | { 73 | bool propertyExists = operation.TryGetValue(propertyName, out var actualValue); 74 | 75 | Assert.That(propertyExists, "Missing property: " + propertyName); 76 | if (CollectionParser.IsCollection(actualValue)) 77 | { 78 | Assert.That(expectedValue, Is.EquivalentTo((IEnumerable)actualValue)); 79 | } 80 | else 81 | { 82 | Assert.That(expectedValue, Is.EqualTo(actualValue)); 83 | } 84 | } 85 | } 86 | 87 | internal void AssertOperation( 88 | MessageBuildResult messageBuildResult, 89 | Action operationAssertFn) 90 | { 91 | IDictionary message = messageBuildResult.Message; 92 | Assert.That(message.ContainsKey(OperationName)); 93 | 94 | operationAssertFn(message[OperationName]); 95 | } 96 | 97 | internal void AssertMessageFail(MessageBuildResult messageBuildResult) 98 | { 99 | Assert.That(messageBuildResult.Success, Is.False); 100 | Assert.That(messageBuildResult.Error, Is.Not.Null); 101 | Assert.That(messageBuildResult.Message, Is.Null); 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/MessageBuilders/People/PeopleTrackChargeMessageBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Mixpanel.MessageBuilders; 3 | using Mixpanel.MessageBuilders.People; 4 | using Mixpanel.MessageProperties; 5 | using NUnit.Framework; 6 | 7 | namespace Mixpanel.Tests.Unit.MessageBuilders.People 8 | { 9 | // Message example: 10 | // { 11 | // "$append": { 12 | // "$transactions": { 13 | // "$time": "2013-01-03T09:00:00", 14 | // "$amount": 25.34 15 | // } 16 | // }, 17 | // "$token": "36ada5b10da39a1347559321baf13063", 18 | // "$distinct_id": "13793" 19 | // } 20 | 21 | [TestFixture] 22 | public class PeopleTrackChargeMessageBuilderTests : PeopleTestsBase 23 | { 24 | protected override string OperationName => "$append"; 25 | 26 | [Test] 27 | public void When_DistinctIdParameter_Then_DistinctIdSetInMessage() 28 | { 29 | MessageBuildResult messageBuildResult = 30 | PeopleTrackChargeMessageBuilder.Build( 31 | Token, null, DecimalPropertyValue, Time, DistinctId, null); 32 | 33 | AssertTrackChargeMessageSuccess(messageBuildResult, DistinctId); 34 | } 35 | 36 | [Test] 37 | public void When_DistinctIdFromSuperProperties_Then_DistinctIdSetInMessage() 38 | { 39 | var superProperties = CreateSuperProperties( 40 | ObjectProperty.Default(DistinctIdPropertyName, PropertyOrigin.SuperProperty, SuperDistinctId)); 41 | 42 | MessageBuildResult messageBuildResult = 43 | PeopleTrackChargeMessageBuilder.Build( 44 | Token, superProperties, DecimalPropertyValue, Time, null, null); 45 | 46 | AssertTrackChargeMessageSuccess(messageBuildResult, SuperDistinctId); 47 | } 48 | 49 | [Test] 50 | public void When_NonSpecialSuperProperties_Then_Ignored() 51 | { 52 | var superProperties = CreateSuperProperties( 53 | ObjectProperty.Default(DistinctIdPropertyName, PropertyOrigin.SuperProperty, SuperDistinctId), 54 | // Should be ignored 55 | ObjectProperty.Default(DecimalSuperPropertyName, PropertyOrigin.SuperProperty, DecimalSuperPropertyValue)); 56 | 57 | 58 | MessageBuildResult messageBuildResult = 59 | PeopleTrackChargeMessageBuilder.Build( 60 | Token, superProperties, DecimalPropertyValue, Time, null, null); 61 | 62 | AssertTrackChargeMessageSuccess(messageBuildResult, SuperDistinctId); 63 | } 64 | 65 | [Test] 66 | public void When_NoToken_Then_MessageBuildFails() 67 | { 68 | MessageBuildResult messageBuildResult = PeopleTrackChargeMessageBuilder.Build( 69 | null, null, DecimalPropertyValue, Time, null, null); 70 | AssertMessageFail(messageBuildResult); 71 | } 72 | 73 | [Test] 74 | public void When_NoDistinctId_Then_MessageBuildFails() 75 | { 76 | MessageBuildResult messageBuildResult = PeopleTrackChargeMessageBuilder.Build( 77 | Token, null, DecimalPropertyValue, Time, null, null); 78 | AssertMessageFail(messageBuildResult); 79 | } 80 | 81 | private void AssertTrackChargeMessageSuccess(MessageBuildResult messageBuildResult, object expectedDistinctId) 82 | { 83 | AssertMessageSuccess(messageBuildResult); 84 | 85 | AssetMessageProperties( 86 | messageBuildResult, 87 | new (string name, object value)[] 88 | { 89 | (PeopleSpecialProperty.Token, Token), 90 | (PeopleSpecialProperty.DistinctId, expectedDistinctId) 91 | }); 92 | 93 | AssertOperation( 94 | messageBuildResult, 95 | operation => 96 | { 97 | var append = (IDictionary)operation; 98 | Assert.That(append.Count, Is.EqualTo(1)); 99 | 100 | var transactions = (IDictionary)append["$transactions"]; 101 | Assert.That(transactions.Count, Is.EqualTo(2)); 102 | Assert.That(transactions["$time"], Is.EqualTo(TimeFormat)); 103 | Assert.That(transactions["$amount"], Is.EqualTo(DecimalPropertyValue)); 104 | }); 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/MessageBuilders/People/PeopleAppendMessageBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Mixpanel.MessageBuilders; 3 | using Mixpanel.MessageBuilders.People; 4 | using Mixpanel.MessageProperties; 5 | using NUnit.Framework; 6 | 7 | namespace Mixpanel.Tests.Unit.MessageBuilders.People 8 | { 9 | // Message example: 10 | // { 11 | // "$token": "36ada5b10da39a1347559321baf13063", 12 | // "$distinct_id": "13793", 13 | // "$append": { "Power Ups": "Bubble Lead" } 14 | // } 15 | 16 | [TestFixture] 17 | public class PeopleAppendMessageBuilderTests : PeopleTestsBase 18 | { 19 | protected override string OperationName => "$append"; 20 | 21 | [Test] 22 | public void When_DistinctIdParameter_Then_DistinctIdSetInMessage() 23 | { 24 | MessageBuildResult messageBuildResult = 25 | PeopleAppendMessageBuilder.Build(Token, null, null, DistinctId, null); 26 | 27 | AssertMessageSuccess( 28 | messageBuildResult, 29 | new (string name, object value)[] 30 | { 31 | (PeopleSpecialProperty.Token, Token), 32 | (PeopleSpecialProperty.DistinctId, DistinctId) 33 | }, 34 | null); 35 | } 36 | 37 | [Test] 38 | public void When_DistinctIdFromSuperProperties_Then_DistinctIdSetInMessage() 39 | { 40 | var superProperties = CreateSuperProperties( 41 | ObjectProperty.Default(DistinctIdPropertyName, PropertyOrigin.SuperProperty, SuperDistinctId)); 42 | 43 | MessageBuildResult messageBuildResult = 44 | PeopleAppendMessageBuilder.Build(Token, superProperties, null, null, null); 45 | 46 | AssertMessageSuccess( 47 | messageBuildResult, 48 | new (string name, object value)[] 49 | { 50 | (PeopleSpecialProperty.Token, Token), 51 | (PeopleSpecialProperty.DistinctId, SuperDistinctId) 52 | }, 53 | null); 54 | } 55 | 56 | [Test] 57 | public void When_NonSpecialSuperProperties_Then_Ignored() 58 | { 59 | var superProperties = CreateSuperProperties( 60 | ObjectProperty.Default(DistinctIdPropertyName, PropertyOrigin.SuperProperty, SuperDistinctId), 61 | // Should be ignored 62 | ObjectProperty.Default(DecimalSuperPropertyName, PropertyOrigin.SuperProperty, DecimalSuperPropertyValue)); 63 | 64 | 65 | MessageBuildResult messageBuildResult = 66 | PeopleAppendMessageBuilder.Build(Token, superProperties, null, null, null); 67 | 68 | AssertMessageSuccess( 69 | messageBuildResult, 70 | new (string name, object value)[] 71 | { 72 | (PeopleSpecialProperty.Token, Token), 73 | (PeopleSpecialProperty.DistinctId, SuperDistinctId) 74 | }, 75 | null); 76 | } 77 | 78 | [Test] 79 | public void When_RawProperties_Then_AllPropertiesInMessage() 80 | { 81 | var rawProperties = new Dictionary 82 | { 83 | {DistinctIdPropertyName, DistinctId }, 84 | {DecimalPropertyName, DecimalPropertyValue}, 85 | {StringPropertyName, StringPropertyValue} 86 | }; 87 | 88 | MessageBuildResult messageBuildResult = 89 | PeopleAppendMessageBuilder.Build(Token, null, rawProperties, null, null); 90 | 91 | AssertMessageSuccess( 92 | messageBuildResult, 93 | new (string name, object value)[] 94 | { 95 | (PeopleSpecialProperty.Token, Token), 96 | (PeopleSpecialProperty.DistinctId, DistinctId) 97 | }, 98 | new (string name, object value)[] 99 | { 100 | (DecimalPropertyName, DecimalPropertyValue), 101 | (StringPropertyName, StringPropertyValue) 102 | }); 103 | } 104 | 105 | [Test] 106 | public void When_NoToken_Then_MessageBuildFails() 107 | { 108 | MessageBuildResult messageBuildResult = PeopleAppendMessageBuilder.Build(null, null, null, null, null); 109 | AssertMessageFail(messageBuildResult); 110 | } 111 | 112 | [Test] 113 | public void When_NoDistinctId_Then_MessageBuildFails() 114 | { 115 | MessageBuildResult messageBuildResult = PeopleAppendMessageBuilder.Build(Token, null, null, null, null); 116 | AssertMessageFail(messageBuildResult); 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/MessageBuilders/People/PeopleRemoveMessageBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Mixpanel.MessageBuilders; 3 | using Mixpanel.MessageBuilders.People; 4 | using Mixpanel.MessageProperties; 5 | using NUnit.Framework; 6 | 7 | namespace Mixpanel.Tests.Unit.MessageBuilders.People 8 | { 9 | // Message example: 10 | // { 11 | // "$token": "36ada5b10da39a1347559321baf13063", 12 | // "$distinct_id": "13793", 13 | // "$remove": { "Items purchased": "socks" } 14 | // } 15 | 16 | [TestFixture] 17 | public class PeopleRemoveMessageBuilderTests : PeopleTestsBase 18 | { 19 | protected override string OperationName => "$remove"; 20 | 21 | [Test] 22 | public void When_DistinctIdParameter_Then_DistinctIdSetInMessage() 23 | { 24 | MessageBuildResult messageBuildResult = 25 | PeopleRemoveMessageBuilder.Build(Token, null, null, DistinctId, null); 26 | 27 | AssertMessageSuccess( 28 | messageBuildResult, 29 | new (string name, object value)[] 30 | { 31 | (PeopleSpecialProperty.Token, Token), 32 | (PeopleSpecialProperty.DistinctId, DistinctId) 33 | }, 34 | null); 35 | } 36 | 37 | [Test] 38 | public void When_DistinctIdFromSuperProperties_Then_DistinctIdSetInMessage() 39 | { 40 | var superProperties = CreateSuperProperties( 41 | ObjectProperty.Default(DistinctIdPropertyName, PropertyOrigin.SuperProperty, SuperDistinctId)); 42 | 43 | MessageBuildResult messageBuildResult = 44 | PeopleRemoveMessageBuilder.Build(Token, superProperties, null, null, null); 45 | 46 | AssertMessageSuccess( 47 | messageBuildResult, 48 | new (string name, object value)[] 49 | { 50 | (PeopleSpecialProperty.Token, Token), 51 | (PeopleSpecialProperty.DistinctId, SuperDistinctId) 52 | }, 53 | null); 54 | } 55 | 56 | [Test] 57 | public void When_NonSpecialSuperProperties_Then_Ignored() 58 | { 59 | var superProperties = CreateSuperProperties( 60 | ObjectProperty.Default(DistinctIdPropertyName, PropertyOrigin.SuperProperty, SuperDistinctId), 61 | // Should be ignored 62 | ObjectProperty.Default(DecimalSuperPropertyName, PropertyOrigin.SuperProperty, DecimalSuperPropertyValue)); 63 | 64 | 65 | MessageBuildResult messageBuildResult = 66 | PeopleRemoveMessageBuilder.Build(Token, superProperties, null, null, null); 67 | 68 | AssertMessageSuccess( 69 | messageBuildResult, 70 | new (string name, object value)[] 71 | { 72 | (PeopleSpecialProperty.Token, Token), 73 | (PeopleSpecialProperty.DistinctId, SuperDistinctId) 74 | }, 75 | null); 76 | } 77 | 78 | [Test] 79 | public void When_RawProperties_Then_AllPropertiesInMessage() 80 | { 81 | var rawProperties = new Dictionary 82 | { 83 | {DistinctIdPropertyName, DistinctId }, 84 | {DecimalPropertyName, DecimalPropertyValue}, 85 | {StringPropertyName, StringPropertyValue} 86 | }; 87 | 88 | MessageBuildResult messageBuildResult = 89 | PeopleRemoveMessageBuilder.Build(Token, null, rawProperties, null, null); 90 | 91 | AssertMessageSuccess( 92 | messageBuildResult, 93 | new (string name, object value)[] 94 | { 95 | (PeopleSpecialProperty.Token, Token), 96 | (PeopleSpecialProperty.DistinctId, DistinctId) 97 | }, 98 | new (string name, object value)[] 99 | { 100 | (DecimalPropertyName, DecimalPropertyValue), 101 | (StringPropertyName, StringPropertyValue) 102 | }); 103 | } 104 | 105 | [Test] 106 | public void When_NoToken_Then_MessageBuildFails() 107 | { 108 | MessageBuildResult messageBuildResult = PeopleRemoveMessageBuilder.Build(null, null, null, null, null); 109 | AssertMessageFail(messageBuildResult); 110 | } 111 | 112 | [Test] 113 | public void When_NoDistinctId_Then_MessageBuildFails() 114 | { 115 | MessageBuildResult messageBuildResult = PeopleRemoveMessageBuilder.Build(Token, null, null, null, null); 116 | AssertMessageFail(messageBuildResult); 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/MessageBuilders/People/PeopleAddMessageBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Mixpanel.MessageBuilders; 3 | using Mixpanel.MessageBuilders.People; 4 | using Mixpanel.MessageProperties; 5 | using NUnit.Framework; 6 | 7 | namespace Mixpanel.Tests.Unit.MessageBuilders.People 8 | { 9 | // Message example: 10 | // { 11 | // "$token": "36ada5b10da39a1347559321baf13063", 12 | // "$distinct_id": "13793", 13 | // "$add": { "Coins Gathered": 12 } 14 | // } 15 | 16 | [TestFixture] 17 | public class PeopleAddMessageBuilderTests : PeopleTestsBase 18 | { 19 | protected override string OperationName => "$add"; 20 | 21 | [Test] 22 | public void When_DistinctIdParameter_Then_DistinctIdSetInMessage() 23 | { 24 | MessageBuildResult messageBuildResult = 25 | PeopleAddMessageBuilder.Build(Token, null, null, DistinctId, null); 26 | 27 | AssertMessageSuccess( 28 | messageBuildResult, 29 | new (string name, object value)[] 30 | { 31 | (PeopleSpecialProperty.Token, Token), 32 | (PeopleSpecialProperty.DistinctId, DistinctId) 33 | }, 34 | null); 35 | } 36 | 37 | [Test] 38 | public void When_DistinctIdFromSuperProperties_Then_DistinctIdSetInMessage() 39 | { 40 | var superProperties = CreateSuperProperties( 41 | ObjectProperty.Default(DistinctIdPropertyName, PropertyOrigin.SuperProperty, SuperDistinctId)); 42 | 43 | MessageBuildResult messageBuildResult = 44 | PeopleAddMessageBuilder.Build(Token, superProperties, null, null, null); 45 | 46 | AssertMessageSuccess( 47 | messageBuildResult, 48 | new (string name, object value)[] 49 | { 50 | (PeopleSpecialProperty.Token, Token), 51 | (PeopleSpecialProperty.DistinctId, SuperDistinctId) 52 | }, 53 | null); 54 | } 55 | 56 | [Test] 57 | public void When_NonSpecialSuperProperties_Then_Ignored() 58 | { 59 | var superProperties = CreateSuperProperties( 60 | ObjectProperty.Default(DistinctIdPropertyName, PropertyOrigin.SuperProperty, SuperDistinctId), 61 | // Should be ignored 62 | ObjectProperty.Default(DecimalSuperPropertyName, PropertyOrigin.SuperProperty, DecimalSuperPropertyValue)); 63 | 64 | 65 | MessageBuildResult messageBuildResult = 66 | PeopleAddMessageBuilder.Build(Token, superProperties, null, null, null); 67 | 68 | AssertMessageSuccess( 69 | messageBuildResult, 70 | new (string name, object value)[] 71 | { 72 | (PeopleSpecialProperty.Token, Token), 73 | (PeopleSpecialProperty.DistinctId, SuperDistinctId) 74 | }, 75 | null); 76 | } 77 | 78 | [Test] 79 | public void When_RawProperties_Then_OnlyNumericPropertiesInMessage() 80 | { 81 | var rawProperties = new Dictionary 82 | { 83 | {DistinctIdPropertyName, DistinctId }, 84 | {DecimalPropertyName, DecimalPropertyValue}, 85 | {IntPropertyName, IntPropertyValue }, 86 | // Must be ignored 87 | {StringPropertyName, StringPropertyValue} 88 | }; 89 | 90 | MessageBuildResult messageBuildResult = 91 | PeopleAddMessageBuilder.Build(Token, null, rawProperties, null, null); 92 | 93 | AssertMessageSuccess( 94 | messageBuildResult, 95 | new (string name, object value)[] 96 | { 97 | (PeopleSpecialProperty.Token, Token), 98 | (PeopleSpecialProperty.DistinctId, DistinctId) 99 | }, 100 | new (string name, object value)[] 101 | { 102 | (DecimalPropertyName, DecimalPropertyValue), 103 | (IntPropertyName, IntPropertyValue) 104 | }); 105 | } 106 | 107 | [Test] 108 | public void When_NoToken_Then_MessageBuildFails() 109 | { 110 | MessageBuildResult messageBuildResult = PeopleAddMessageBuilder.Build(null, null, null, null, null); 111 | AssertMessageFail(messageBuildResult); 112 | } 113 | 114 | [Test] 115 | public void When_NoDistinctId_Then_MessageBuildFails() 116 | { 117 | MessageBuildResult messageBuildResult = PeopleAddMessageBuilder.Build(Token, null, null, null, null); 118 | AssertMessageFail(messageBuildResult); 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/MessageBuilders/Track/AliasMessageBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Mixpanel.MessageBuilders; 4 | using Mixpanel.MessageBuilders.Track; 5 | using Mixpanel.MessageProperties; 6 | using NUnit.Framework; 7 | 8 | namespace Mixpanel.Tests.Unit.MessageBuilders.Track 9 | { 10 | [TestFixture] 11 | public class AliasMessageBuilderTests : MixpanelTestsBase 12 | { 13 | 14 | // Message example: 15 | // { 16 | // "event": "$create_alias", 17 | // "properties": { 18 | // "token": "e3bc4100330c35722740fb8c6f5abddc", 19 | // "distinct_id": "ORIGINAL_ID", 20 | // "alias": "NEW_ID" 21 | // } 22 | // } 23 | 24 | [Test] 25 | public void When_DistinctIdAndAliasFromParams_Then_AllPropertiesSet() 26 | { 27 | MessageBuildResult messageBuildResult = 28 | AliasMessageBuilder.Build(Token, null, DistinctId, Alias); 29 | 30 | AssertMessageSuccess(messageBuildResult, Token, DistinctId, Alias); 31 | } 32 | 33 | [Test] 34 | public void When_DistinctIdFromSuperPropsAndAliasFromParams_Then_AllPropertiesSet() 35 | { 36 | var superProperties = CreateSuperProperties( 37 | ObjectProperty.Default(DistinctIdPropertyName, PropertyOrigin.SuperProperty, SuperDistinctId)); 38 | 39 | MessageBuildResult messageBuildResult = AliasMessageBuilder.Build(Token, superProperties, null, Alias); 40 | 41 | AssertMessageSuccess(messageBuildResult, Token, SuperDistinctId, Alias); 42 | } 43 | 44 | [Test] 45 | public void When_DistinctIdFromParamsAndSuperProps_Then_DistinctIdFromParamsOverwritesSuperProps() 46 | { 47 | var superProperties = CreateSuperProperties( 48 | ObjectProperty.Default(DistinctIdPropertyName, PropertyOrigin.SuperProperty, SuperDistinctId)); 49 | 50 | MessageBuildResult messageBuildResult = AliasMessageBuilder.Build(Token, superProperties, DistinctId, Alias); 51 | AssertMessageSuccess(messageBuildResult, Token, DistinctId, Alias); 52 | } 53 | 54 | [Test] 55 | public void When_TokenFromSuperProps_Then_AllPropertiesSet() 56 | { 57 | var superProperties = CreateSuperProperties( 58 | ObjectProperty.Default("token", PropertyOrigin.SuperProperty, Token)); 59 | 60 | MessageBuildResult messageBuildResult = 61 | AliasMessageBuilder.Build(null, superProperties, DistinctId, Alias); 62 | 63 | AssertMessageSuccess(messageBuildResult, Token, DistinctId, Alias); 64 | } 65 | 66 | [Test] 67 | public void When_NoToken_Then_MessageBuildFails() 68 | { 69 | MessageBuildResult messageBuildResult = AliasMessageBuilder.Build(null, null, DistinctId, Alias); 70 | AssertMessageFail(messageBuildResult); 71 | } 72 | 73 | [Test] 74 | public void When_NoDistinctId_Then_MessageBuildFails() 75 | { 76 | MessageBuildResult messageBuildResult = AliasMessageBuilder.Build(Token, null, null, Alias); 77 | AssertMessageFail(messageBuildResult); 78 | } 79 | 80 | [Test] 81 | public void When_NoAlias_Then_MessageBuildFails() 82 | { 83 | MessageBuildResult messageBuildResult = AliasMessageBuilder.Build(Token, null, DistinctId, null); 84 | AssertMessageFail(messageBuildResult); 85 | } 86 | 87 | private List CreateSuperProperties(params ObjectProperty[] objectProperties) 88 | { 89 | return objectProperties.ToList(); 90 | } 91 | 92 | private void AssertMessageSuccess( 93 | MessageBuildResult messageBuildResult, string token, string distinctId, string alias) 94 | { 95 | Assert.That(messageBuildResult.Success, Is.True); 96 | Assert.That(messageBuildResult.Error, Is.Null); 97 | 98 | IDictionary message = messageBuildResult.Message; 99 | Assert.That(message.Count, Is.EqualTo(2)); 100 | 101 | Assert.That(message["event"], Is.EqualTo("$create_alias")); 102 | 103 | var properties = message["properties"] as IDictionary; 104 | Assert.That(properties, Is.Not.Null); 105 | Assert.That(properties.Count, Is.EqualTo(3)); 106 | Assert.That(properties["token"], Is.EqualTo(token)); 107 | Assert.That(properties["distinct_id"], Is.EqualTo(distinctId)); 108 | Assert.That(properties["alias"], Is.EqualTo(alias)); 109 | } 110 | 111 | private void AssertMessageFail(MessageBuildResult messageBuildResult) 112 | { 113 | Assert.That(messageBuildResult.Success, Is.False); 114 | Assert.That(messageBuildResult.Error, Is.Not.Null); 115 | Assert.That(messageBuildResult.Message, Is.Null); 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/MixpanelClient/MixpanelClientSendTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Bogus; 6 | using FluentAssertions; 7 | using Newtonsoft.Json.Linq; 8 | using NUnit.Framework; 9 | 10 | namespace Mixpanel.Tests.Unit.MixpanelClient 11 | { 12 | [TestFixture] 13 | public class MixpanelClientSendTests 14 | { 15 | [TestCase(5, 7, 1 + 1)] 16 | [TestCase(49, 51, 1 + 2)] 17 | [TestCase(50, 50, 1 + 1)] 18 | [TestCase(51, 49, 2 + 1)] 19 | [TestCase(0, 5, 0 + 1)] 20 | [TestCase(0, 75, 0 + 2)] 21 | [TestCase(5, 0, 1 + 0)] 22 | [TestCase(75, 0, 2 + 0)] 23 | [TestCase(125, 200, 3 + 4)] 24 | public async Task SendAsync_VariousNumberOfMessages_CorrectNumberOfBatchesSent( 25 | int trackMessagesCount, int engageMessagesCount, int expectedBatchesCount) 26 | { 27 | // Arrange 28 | var (token, httpMockMixpanelConfig) = GenerateInputs(); 29 | IMixpanelClient client = new Mixpanel.MixpanelClient(token, httpMockMixpanelConfig.Instance); 30 | var messages = GenerateMessages(client, trackMessagesCount, engageMessagesCount); 31 | 32 | // Act 33 | SendResult sendResult = await client.SendAsync(messages); 34 | 35 | // Assert 36 | sendResult.Success.Should().BeTrue(); 37 | sendResult.FailedBatches.Should().BeNullOrEmpty(); 38 | sendResult.SentBatches.Should().HaveCount(expectedBatchesCount); 39 | httpMockMixpanelConfig.Messages.Should().HaveCount(expectedBatchesCount); 40 | } 41 | 42 | [TestCase(5, 7, 1 + 1)] 43 | [TestCase(49, 51, 1 + 2)] 44 | [TestCase(50, 50, 1 + 1)] 45 | [TestCase(51, 49, 2 + 1)] 46 | [TestCase(0, 5, 0 + 1)] 47 | [TestCase(0, 75, 0 + 2)] 48 | [TestCase(5, 0, 1 + 0)] 49 | [TestCase(75, 0, 2 + 0)] 50 | [TestCase(125, 200, 3 + 4)] 51 | public void SendTest_VariousNumberOfMessages_CorrectNumberOfBatchesCreated( 52 | int trackMessagesCount, int engageMessagesCount, int expectedBatchesCount) 53 | { 54 | // Arrange 55 | var (token, _) = GenerateInputs(); 56 | IMixpanelClient client = new Mixpanel.MixpanelClient(token); 57 | var messages = GenerateMessages(client, trackMessagesCount, engageMessagesCount); 58 | 59 | // Act 60 | ReadOnlyCollection mixpanelBatchMessageTests = 61 | client.SendTest(messages); 62 | 63 | // Assert 64 | mixpanelBatchMessageTests.Should().HaveCount(expectedBatchesCount); 65 | } 66 | 67 | [Test] 68 | public void SendAsync_CancellationRequested_RequestCancelled() 69 | { 70 | // Arrange 71 | var (token, httpMockMixpanelConfig) = GenerateInputs(); 72 | var cancellationTokenSource = new CancellationTokenSource(); 73 | var client = new Mixpanel.MixpanelClient(token, httpMockMixpanelConfig.Instance); 74 | 75 | // Act 76 | var task = Task.Factory.StartNew( 77 | async () => await client.SendAsync(GenerateMessages(client, 1, 1), cancellationTokenSource.Token)); 78 | cancellationTokenSource.Cancel(); 79 | task.Wait(); 80 | 81 | // Assert 82 | httpMockMixpanelConfig.RequestCancelled.Should().BeTrue(); 83 | } 84 | 85 | private IList GenerateMessages( 86 | IMixpanelClient client, int trackMessagesCount = 0, int engageMessagesCount = 0) 87 | { 88 | var randomizer = new Randomizer(); 89 | 90 | var messages = new List(trackMessagesCount + engageMessagesCount); 91 | for (int i = 0; i < trackMessagesCount; i++) 92 | { 93 | var properties = new Dictionary 94 | { 95 | { randomizer.Words(), randomizer.Int() } 96 | }; 97 | 98 | messages.Add(client.GetTrackMessage(randomizer.Words(), properties)); 99 | } 100 | 101 | for (int i = 0; i < engageMessagesCount; i++) 102 | { 103 | var properties = new Dictionary 104 | { 105 | { MixpanelProperty.DistinctId, randomizer.Uuid() }, 106 | { randomizer.Words(), randomizer.Bool() } 107 | }; 108 | 109 | messages.Add(client.GetPeopleSetMessage(properties)); 110 | } 111 | 112 | return messages; 113 | } 114 | 115 | private (string token, HttpMockMixpanelConfig httpMockMixpanelConfig) GenerateInputs() 116 | { 117 | var randomizer = new Randomizer(); 118 | return 119 | ( 120 | randomizer.AlphaNumeric(32), 121 | new HttpMockMixpanelConfig() 122 | ); 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel/MessageProperties/PropertiesDigger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | using System.Runtime.Serialization; 6 | using System.Collections.Concurrent; 7 | 8 | namespace Mixpanel.MessageProperties 9 | { 10 | internal static class PropertiesDigger 11 | { 12 | public static IEnumerable Get(object obj, PropertyOrigin propertyOrigin) 13 | { 14 | switch (obj) 15 | { 16 | case null: 17 | yield break; 18 | 19 | case IDictionary dic: 20 | foreach (KeyValuePair entry in dic) 21 | { 22 | yield return new ObjectProperty( 23 | entry.Key, 24 | PropertyNameSource.Default, 25 | propertyOrigin, 26 | entry.Value); 27 | } 28 | yield break; 29 | 30 | case IDictionary dic: 31 | foreach (DictionaryEntry entry in dic) 32 | { 33 | if (!(entry.Key is string stringKey)) 34 | { 35 | continue; 36 | } 37 | 38 | yield return new ObjectProperty( 39 | stringKey, 40 | PropertyNameSource.Default, 41 | propertyOrigin, 42 | entry.Value); 43 | } 44 | yield break; 45 | 46 | default: 47 | foreach (var propertyInfo in GetObjectPropertyInfos(obj.GetType())) 48 | { 49 | yield return new ObjectProperty( 50 | propertyInfo.PropertyName, 51 | propertyInfo.PropertyNameSource, 52 | propertyOrigin, 53 | propertyInfo.PropertyInfo.GetValue(obj, null)); 54 | } 55 | yield break; 56 | 57 | } 58 | } 59 | 60 | private static readonly ConcurrentDictionary> 61 | PropertyInfosCache = new ConcurrentDictionary>(); 62 | 63 | private static List GetObjectPropertyInfos(Type type) 64 | { 65 | return PropertyInfosCache.GetOrAdd(type, t => 66 | { 67 | bool isDataContract = t.GetCustomAttribute() != null; 68 | var infos = t.GetProperties(BindingFlags.Public | BindingFlags.Instance); 69 | 70 | var res = new List(infos.Length); 71 | 72 | foreach (var info in infos) 73 | { 74 | if (info.GetCustomAttribute() != null) 75 | continue; 76 | 77 | DataMemberAttribute dataMemberAttr = null; 78 | if (isDataContract) 79 | { 80 | dataMemberAttr = info.GetCustomAttribute(); 81 | if (dataMemberAttr == null) 82 | continue; 83 | } 84 | 85 | var mixpanelNameAttr = info.GetCustomAttribute(); 86 | if (mixpanelNameAttr != null) 87 | { 88 | var isMixpanelNameEmpty = string.IsNullOrWhiteSpace(mixpanelNameAttr.Name); 89 | res.Add(new ObjectPropertyInfo( 90 | isMixpanelNameEmpty ? info.Name : mixpanelNameAttr.Name, 91 | isMixpanelNameEmpty ? PropertyNameSource.Default : PropertyNameSource.MixpanelName, 92 | info)); 93 | continue; 94 | } 95 | 96 | if (dataMemberAttr != null) 97 | { 98 | var isDataMemberNameEmpty = string.IsNullOrWhiteSpace(dataMemberAttr.Name); 99 | res.Add(new ObjectPropertyInfo( 100 | isDataMemberNameEmpty ? info.Name : dataMemberAttr.Name, 101 | isDataMemberNameEmpty ? PropertyNameSource.Default : PropertyNameSource.DataMember, 102 | info)); 103 | continue; 104 | } 105 | 106 | res.Add(new ObjectPropertyInfo(info.Name, PropertyNameSource.Default, info)); 107 | } 108 | return res; 109 | }); 110 | } 111 | 112 | sealed class ObjectPropertyInfo 113 | { 114 | public string PropertyName { get; } 115 | public PropertyNameSource PropertyNameSource { get; } 116 | public PropertyInfo PropertyInfo { get; } 117 | 118 | public ObjectPropertyInfo( 119 | string propertyName, 120 | PropertyNameSource propertyNameSource, 121 | PropertyInfo propertyInfo) 122 | { 123 | PropertyName = propertyName; 124 | PropertyNameSource = propertyNameSource; 125 | PropertyInfo = propertyInfo; 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/MessageBuilders/People/PeopleDeleteMessageBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using Bogus; 2 | using FluentAssertions; 3 | using Mixpanel.MessageBuilders; 4 | using Mixpanel.MessageBuilders.People; 5 | using Mixpanel.MessageProperties; 6 | using NUnit.Framework; 7 | 8 | // ReSharper disable ExpressionIsAlwaysNull 9 | 10 | namespace Mixpanel.Tests.Unit.MessageBuilders.People 11 | { 12 | // Message example: 13 | // { 14 | // "$token": "36ada5b10da39a1347559321baf13063", 15 | // "$distinct_id": "13793", 16 | // "$delete": "", 17 | // "$ignore_alias": true 18 | // } 19 | 20 | [TestFixture] 21 | public class PeopleDeleteMessageBuilderTests 22 | { 23 | [Test] 24 | public void Build_FullMessage_AllPropertiesSetInMessage() 25 | { 26 | // Arrange 27 | var (token, distinctId) = GenerateInputs(); 28 | const bool ignoreAlias = true; 29 | 30 | // Act 31 | MessageBuildResult messageBuildResult = 32 | PeopleDeleteMessageBuilder.Build(token, null, distinctId, ignoreAlias, null); 33 | 34 | // Assert 35 | messageBuildResult.Success.Should().BeTrue(); 36 | messageBuildResult.Error.Should().BeNull(); 37 | messageBuildResult.Message.Count.Should().Be(4); 38 | messageBuildResult.Message.Should().ContainKey("$delete").WhoseValue.Should().Be(""); 39 | messageBuildResult.Message.Should().ContainKey("$token").WhoseValue.Should().Be(token); 40 | messageBuildResult.Message.Should().ContainKey("$distinct_id").WhoseValue.Should().Be(distinctId); 41 | messageBuildResult.Message.Should().ContainKey("$ignore_alias").WhoseValue.Should().Be(ignoreAlias); 42 | } 43 | 44 | [Test] 45 | public void Build_DistinctIdProvidedAsArgument_DistinctIdSetInMessage() 46 | { 47 | // Arrange 48 | var (token, distinctId) = GenerateInputs(); 49 | 50 | // Act 51 | MessageBuildResult messageBuildResult = 52 | PeopleDeleteMessageBuilder.Build(token, null, distinctId, false, null); 53 | 54 | // Assert 55 | messageBuildResult.Message.Should().ContainKey("$distinct_id").WhoseValue.Should().Be(distinctId); 56 | } 57 | 58 | [Test] 59 | public void Build_DistinctIdProvidedAsSuperProperty_DistinctIdSetInMessage() 60 | { 61 | // Arrange 62 | var (token, distinctId) = GenerateInputs(); 63 | var superProperties = new[] 64 | { 65 | new ObjectProperty(MixpanelProperty.DistinctId, PropertyNameSource.Default, PropertyOrigin.SuperProperty, distinctId) 66 | }; 67 | 68 | // Act 69 | MessageBuildResult messageBuildResult = 70 | PeopleDeleteMessageBuilder.Build(token, superProperties, null, false, null); 71 | 72 | // Assert 73 | messageBuildResult.Message.Should().ContainKey("$distinct_id").WhoseValue.Should().Be(distinctId); 74 | } 75 | 76 | [Test] 77 | public void Build_DistinctIdProvidedAsArgumentAndSuperProperty_DistinctIdFromArgumentSetInMessage() 78 | { 79 | // Arrange 80 | var (token, _) = GenerateInputs(); 81 | string argDistinctId = new Randomizer().AlphaNumeric(10); 82 | string superPropsDistinctId = new Randomizer().AlphaNumeric(10); 83 | var superProperties = new[] 84 | { 85 | new ObjectProperty(MixpanelProperty.DistinctId, PropertyNameSource.Default, PropertyOrigin.SuperProperty, superPropsDistinctId) 86 | }; 87 | 88 | // Act 89 | MessageBuildResult messageBuildResult = 90 | PeopleDeleteMessageBuilder.Build(token, superProperties, argDistinctId, false, null); 91 | 92 | // Assert 93 | messageBuildResult.Message.Should().ContainKey("$distinct_id").WhoseValue.Should().Be(argDistinctId); 94 | } 95 | 96 | [Test] 97 | public void Build_NoTokenProvided_MessageBuildFails() 98 | { 99 | // Arrange 100 | string token = null; 101 | var (_, distinctId) = GenerateInputs(); 102 | 103 | // Act 104 | MessageBuildResult messageBuildResult = 105 | PeopleDeleteMessageBuilder.Build(token, null, distinctId, false, null); 106 | 107 | // Assert 108 | messageBuildResult.Success.Should().BeFalse(); 109 | messageBuildResult.Message.Should().BeNull(); 110 | messageBuildResult.Error.Should().NotBeNull(); 111 | } 112 | 113 | [Test] 114 | public void Build_NoDistinctIdProvided_MessageBuildFails() 115 | { 116 | // Arrange 117 | string distinctId = null; 118 | var (token, _) = GenerateInputs(); 119 | 120 | // Act 121 | MessageBuildResult messageBuildResult = 122 | PeopleDeleteMessageBuilder.Build(token, null, distinctId, false, null); 123 | 124 | // Assert 125 | messageBuildResult.Success.Should().BeFalse(); 126 | messageBuildResult.Message.Should().BeNull(); 127 | messageBuildResult.Error.Should().NotBeNull(); 128 | } 129 | 130 | private (string token, string distinctId) GenerateInputs() 131 | { 132 | return 133 | ( 134 | new Randomizer().AlphaNumeric(32), 135 | new Randomizer().AlphaNumeric(10) 136 | ); 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/MixpanelClient/MixpanelClientHttpTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using NUnit.Framework; 5 | 6 | namespace Mixpanel.Tests.Unit.MixpanelClient 7 | { 8 | [TestFixture] 9 | public class MixpanelClientHttpTests : MixpanelTestsBase 10 | { 11 | private const string IpParam = "ip"; 12 | private const string Ip1Param = "ip=1"; 13 | private const string Ip0Param = "ip=0"; 14 | 15 | private List urls; 16 | private Mixpanel.MixpanelClient client; 17 | private List> mixpanelMethods; 18 | 19 | [SetUp] 20 | public void SetUp() 21 | { 22 | MixpanelConfig.Global.Reset(); 23 | 24 | urls = new List(); 25 | client = new Mixpanel.MixpanelClient(Token, GetConfig()); 26 | 27 | mixpanelMethods = new List> 28 | { 29 | () => client.TrackAsync(Event, DistinctId, DictionaryWithStringProperty), 30 | () => client.AliasAsync(DistinctId, Alias), 31 | () => client.PeopleSetAsync(DistinctId, DictionaryWithStringProperty), 32 | () => client.PeopleSetOnceAsync(DistinctId, DictionaryWithStringProperty), 33 | () => client.PeopleAddAsync(DistinctId, DictionaryWithStringProperty), 34 | () => client.PeopleAppendAsync(DistinctId, DictionaryWithStringProperty), 35 | () => client.PeopleUnionAsync(DistinctId, DictionaryWithStringProperty), 36 | () => client.PeopleRemoveAsync(DistinctId, DictionaryWithStringProperty), 37 | () => client.PeopleUnsetAsync(DistinctId, StringPropertyArray), 38 | () => client.PeopleDeleteAsync(DistinctId), 39 | () => client.PeopleTrackChargeAsync(DistinctId, DecimalPropertyValue), 40 | () => client.SendAsync(new[] 41 | { 42 | new MixpanelMessage 43 | { 44 | Kind = MessageKind.PeopleSet, 45 | Data = DictionaryWithStringProperty 46 | } 47 | }) 48 | }; 49 | } 50 | 51 | private MixpanelConfig GetConfig() 52 | { 53 | return new MixpanelConfig 54 | { 55 | AsyncHttpPostFn = (endpoint, data, cancellationToken) => 56 | { 57 | urls.Add(endpoint); 58 | return Task.FromResult(true); 59 | } 60 | }; 61 | } 62 | 63 | [Test] 64 | public async Task Given_CallingAllMethods_When_DefaultConfig_Then_UrlsAreValid() 65 | { 66 | await CallAllMixpanelMethods(); 67 | AssertAllUrls(url => Assert.That(IsUrlValid(url))); 68 | } 69 | 70 | [Test] 71 | public async Task Given_CallingAllMethods_When_DefaultConfig_Then_NoIpParam() 72 | { 73 | await CallAllMixpanelMethods(); 74 | AssertAllUrls(url => Assert.That(url, Is.Not.Contains(IpParam))); 75 | } 76 | 77 | [Test] 78 | public async Task Given_CallingAllMethods_When_ConfigUseRequestIp_Then_IpParam() 79 | { 80 | MixpanelConfig.Global.IpAddressHandling = MixpanelIpAddressHandling.UseRequestIp; 81 | 82 | await CallAllMixpanelMethods(); 83 | AssertAllUrls(url => Assert.That(url, Does.Contain(Ip1Param))); 84 | } 85 | 86 | [Test] 87 | public async Task Given_CallingAllMethods_When_ConfigIgnoreRequestIp_Then_IpParam() 88 | { 89 | MixpanelConfig.Global.IpAddressHandling = MixpanelIpAddressHandling.IgnoreRequestIp; 90 | 91 | await CallAllMixpanelMethods(); 92 | AssertAllUrls(url => Assert.That(url, Does.Contain(Ip0Param))); 93 | } 94 | 95 | [Test] 96 | public async Task Given_CallingAllMethods_When_DefaultConfig_Then_UseDefaultHost() 97 | { 98 | await CallAllMixpanelMethods(); 99 | AssertAllUrls(url => Assert.That(url, Does.StartWith("https://api.mixpanel.com"))); 100 | } 101 | 102 | [Test] 103 | public async Task Given_CallingAllMethods_When_ConfigUseDefaultDataResidency_Then_UseDefaultHost() 104 | { 105 | MixpanelConfig.Global.DataResidencyHandling = MixpanelDataResidencyHandling.Default; 106 | 107 | await CallAllMixpanelMethods(); 108 | AssertAllUrls(url => Assert.That(url, Does.StartWith("https://api.mixpanel.com"))); 109 | } 110 | 111 | [Test] 112 | public async Task Given_CallingAllMethods_When_ConfigUseEUDataResidency_Then_UseEUHost() 113 | { 114 | MixpanelConfig.Global.DataResidencyHandling = MixpanelDataResidencyHandling.EU; 115 | 116 | await CallAllMixpanelMethods(); 117 | AssertAllUrls(url => Assert.That(url, Does.StartWith("https://api-eu.mixpanel.com"))); 118 | } 119 | 120 | private async Task CallAllMixpanelMethods() 121 | { 122 | foreach (Func mixpanelMethod in mixpanelMethods) 123 | { 124 | await mixpanelMethod(); 125 | } 126 | 127 | Assert.That(urls.Count, Is.EqualTo(mixpanelMethods.Count)); 128 | } 129 | 130 | private void AssertAllUrls(Action assertFn) 131 | { 132 | foreach (string url in urls) 133 | { 134 | assertFn(url); 135 | } 136 | } 137 | 138 | private bool IsUrlValid(string url) 139 | { 140 | return 141 | Uri.TryCreate(url, UriKind.Absolute, out var uriResult) 142 | && uriResult.Scheme == Uri.UriSchemeHttps; 143 | } 144 | } 145 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/MessageBuilders/People/PeopleUnionMessageBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Mixpanel.MessageBuilders; 3 | using Mixpanel.MessageBuilders.People; 4 | using Mixpanel.MessageProperties; 5 | using NUnit.Framework; 6 | 7 | namespace Mixpanel.Tests.Unit.MessageBuilders.People 8 | { 9 | // Message example: 10 | // { 11 | // "$token": "36ada5b10da39a1347559321baf13063", 12 | // "$distinct_id": "13793", 13 | // "$union": { "Items purchased": ["socks", "shirts"] } 14 | // } 15 | 16 | [TestFixture] 17 | public class PeopleUnionMessageBuilderTests : PeopleTestsBase 18 | { 19 | protected override string OperationName => "$union"; 20 | 21 | [Test] 22 | public void When_DistinctIdParameter_Then_DistinctIdSetInMessage() 23 | { 24 | MessageBuildResult messageBuildResult = 25 | PeopleUnionMessageBuilder.Build(Token, null, null, DistinctId, null); 26 | 27 | AssertMessageSuccess( 28 | messageBuildResult, 29 | new (string name, object value)[] 30 | { 31 | (PeopleSpecialProperty.Token, Token), 32 | (PeopleSpecialProperty.DistinctId, DistinctId) 33 | }, 34 | null); 35 | } 36 | 37 | [Test] 38 | public void When_DistinctIdFromSuperProperties_Then_DistinctIdSetInMessage() 39 | { 40 | var superProperties = CreateSuperProperties( 41 | ObjectProperty.Default(DistinctIdPropertyName, PropertyOrigin.SuperProperty, SuperDistinctId)); 42 | 43 | MessageBuildResult messageBuildResult = 44 | PeopleUnionMessageBuilder.Build(Token, superProperties, null, null, null); 45 | 46 | AssertMessageSuccess( 47 | messageBuildResult, 48 | new (string name, object value)[] 49 | { 50 | (PeopleSpecialProperty.Token, Token), 51 | (PeopleSpecialProperty.DistinctId, SuperDistinctId) 52 | }, 53 | null); 54 | } 55 | 56 | [Test] 57 | public void When_NonSpecialSuperProperties_Then_Ignored() 58 | { 59 | var superProperties = CreateSuperProperties( 60 | ObjectProperty.Default(DistinctIdPropertyName, PropertyOrigin.SuperProperty, SuperDistinctId), 61 | // Should be ignored 62 | ObjectProperty.Default(DecimalSuperPropertyName, PropertyOrigin.SuperProperty, DecimalSuperPropertyValue)); 63 | 64 | 65 | MessageBuildResult messageBuildResult = 66 | PeopleUnionMessageBuilder.Build(Token, superProperties, null, null, null); 67 | 68 | AssertMessageSuccess( 69 | messageBuildResult, 70 | new (string name, object value)[] 71 | { 72 | (PeopleSpecialProperty.Token, Token), 73 | (PeopleSpecialProperty.DistinctId, SuperDistinctId) 74 | }, 75 | null); 76 | } 77 | 78 | [Test] 79 | public void When_ValidRawProperties_Then_AllPropertiesInMessage() 80 | { 81 | var rawProperties = new Dictionary 82 | { 83 | {DistinctIdPropertyName, DistinctId }, 84 | {DecimalPropertyName, DecimalPropertyArray}, 85 | {StringPropertyName, StringPropertyArray} 86 | }; 87 | 88 | MessageBuildResult messageBuildResult = 89 | PeopleUnionMessageBuilder.Build(Token, null, rawProperties, null, null); 90 | 91 | AssertMessageSuccess( 92 | messageBuildResult, 93 | new (string name, object value)[] 94 | { 95 | (PeopleSpecialProperty.Token, Token), 96 | (PeopleSpecialProperty.DistinctId, DistinctId) 97 | }, 98 | new (string name, object value)[] 99 | { 100 | (DecimalPropertyName, DecimalPropertyArray), 101 | (StringPropertyName, StringPropertyArray) 102 | }); 103 | } 104 | 105 | [Test] 106 | public void When_InvalidRawProperties_Then_InvalidPropertiesIgnored() 107 | { 108 | var rawProperties = new Dictionary 109 | { 110 | {DistinctIdPropertyName, DistinctId }, 111 | {DecimalPropertyName, DecimalPropertyArray}, 112 | // Must be ignored 113 | {StringPropertyName, StringPropertyValue}, 114 | {IntPropertyName, IntPropertyValue } 115 | }; 116 | 117 | MessageBuildResult messageBuildResult = 118 | PeopleUnionMessageBuilder.Build(Token, null, rawProperties, null, null); 119 | 120 | AssertMessageSuccess( 121 | messageBuildResult, 122 | new (string name, object value)[] 123 | { 124 | (PeopleSpecialProperty.Token, Token), 125 | (PeopleSpecialProperty.DistinctId, DistinctId) 126 | }, 127 | new (string name, object value)[] 128 | { 129 | (DecimalPropertyName, DecimalPropertyArray) 130 | }); 131 | } 132 | 133 | [Test] 134 | public void When_NoToken_Then_MessageBuildFails() 135 | { 136 | MessageBuildResult messageBuildResult = PeopleUnionMessageBuilder.Build(null, null, null, null, null); 137 | AssertMessageFail(messageBuildResult); 138 | } 139 | 140 | [Test] 141 | public void When_NoDistinctId_Then_MessageBuildFails() 142 | { 143 | MessageBuildResult messageBuildResult = PeopleUnionMessageBuilder.Build(Token, null, null, null, null); 144 | AssertMessageFail(messageBuildResult); 145 | } 146 | } 147 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/MessageBuilders/People/PeopleUnsetMessageBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Mixpanel.MessageBuilders; 4 | using Mixpanel.MessageBuilders.People; 5 | using Mixpanel.MessageProperties; 6 | using NUnit.Framework; 7 | 8 | namespace Mixpanel.Tests.Unit.MessageBuilders.People 9 | { 10 | // Message example: 11 | // { 12 | // "$token": "36ada5b10da39a1347559321baf13063", 13 | // "$distinct_id": "13793", 14 | // "$unset": [ "Days Overdue" ] 15 | // } 16 | 17 | [TestFixture] 18 | public class PeopleUnsetMessageBuilderTests : MixpanelTestsBase 19 | { 20 | 21 | [Test] 22 | public void When_DistinctIdParameter_Then_DistinctIdSetInMessage() 23 | { 24 | MessageBuildResult messageBuildResult = 25 | PeopleUnsetMessageBuilder.Build(Token, null, null, DistinctId, null); 26 | 27 | AssertMessageSuccess( 28 | messageBuildResult, 29 | new (string name, object value)[] 30 | { 31 | (PeopleSpecialProperty.Token, Token), 32 | (PeopleSpecialProperty.DistinctId, DistinctId) 33 | }, 34 | null); 35 | } 36 | 37 | [Test] 38 | public void When_DistinctIdFromSuperProperties_Then_DistinctIdSetInMessage() 39 | { 40 | var superProperties = CreateSuperProperties( 41 | ObjectProperty.Default(DistinctIdPropertyName, PropertyOrigin.SuperProperty, SuperDistinctId)); 42 | 43 | MessageBuildResult messageBuildResult = 44 | PeopleUnsetMessageBuilder.Build(Token, superProperties, null, null, null); 45 | 46 | AssertMessageSuccess( 47 | messageBuildResult, 48 | new (string name, object value)[] 49 | { 50 | (PeopleSpecialProperty.Token, Token), 51 | (PeopleSpecialProperty.DistinctId, SuperDistinctId) 52 | }, 53 | null); 54 | } 55 | 56 | [Test] 57 | public void When_NonSpecialSuperProperties_Then_Ignored() 58 | { 59 | var superProperties = CreateSuperProperties( 60 | ObjectProperty.Default(DistinctIdPropertyName, PropertyOrigin.SuperProperty, SuperDistinctId), 61 | // Should be ignored 62 | ObjectProperty.Default(DecimalSuperPropertyName, PropertyOrigin.SuperProperty, DecimalSuperPropertyValue)); 63 | 64 | 65 | MessageBuildResult messageBuildResult = 66 | PeopleUnsetMessageBuilder.Build(Token, superProperties, null, null, null); 67 | 68 | AssertMessageSuccess( 69 | messageBuildResult, 70 | new (string name, object value)[] 71 | { 72 | (PeopleSpecialProperty.Token, Token), 73 | (PeopleSpecialProperty.DistinctId, SuperDistinctId) 74 | }, 75 | null); 76 | } 77 | 78 | [Test] 79 | public void When_PropertyNames_Then_AllPropertyNamesInMessage() 80 | { 81 | MessageBuildResult messageBuildResult = 82 | PeopleUnsetMessageBuilder.Build(Token, null, StringPropertyArray, DistinctId, null); 83 | 84 | AssertMessageSuccess( 85 | messageBuildResult, 86 | new (string name, object value)[] 87 | { 88 | (PeopleSpecialProperty.Token, Token), 89 | (PeopleSpecialProperty.DistinctId, DistinctId) 90 | }, 91 | StringPropertyArray); 92 | } 93 | 94 | [Test] 95 | public void When_NoToken_Then_MessageBuildFails() 96 | { 97 | MessageBuildResult messageBuildResult = PeopleUnsetMessageBuilder.Build(null, null, null, null, null); 98 | AssertMessageFail(messageBuildResult); 99 | } 100 | 101 | [Test] 102 | public void When_NoDistinctId_Then_MessageBuildFails() 103 | { 104 | MessageBuildResult messageBuildResult = PeopleUnsetMessageBuilder.Build(Token, null, null, null, null); 105 | AssertMessageFail(messageBuildResult); 106 | } 107 | 108 | private List CreateSuperProperties(params ObjectProperty[] objectProperties) 109 | { 110 | return objectProperties.ToList(); 111 | } 112 | 113 | private void AssertMessageSuccess( 114 | MessageBuildResult messageBuildResult, 115 | (string name, object value)[] messageProperties, 116 | IEnumerable expectedPropertyNames) 117 | { 118 | Assert.That(messageBuildResult.Success, Is.True); 119 | Assert.That(messageBuildResult.Error, Is.Null); 120 | 121 | IDictionary message = messageBuildResult.Message; 122 | Assert.That(message.Count, Is.EqualTo(messageProperties.Length + 1 /*OPERATION_NAME*/)); 123 | 124 | foreach ((string propertyName, object expectedValue) in messageProperties) 125 | { 126 | bool propertyExists = message.TryGetValue(propertyName, out var actualValue); 127 | 128 | Assert.That(propertyExists, "Missing property: " + propertyName); 129 | Assert.That(expectedValue, Is.EqualTo(actualValue)); 130 | } 131 | 132 | Assert.That(message.ContainsKey("$unset")); 133 | var unset = (IEnumerable)message["$unset"]; 134 | 135 | if (expectedPropertyNames == null) 136 | { 137 | Assert.That(unset.Count(), Is.EqualTo(0)); 138 | return; 139 | } 140 | 141 | Assert.That(unset, Is.EquivalentTo(expectedPropertyNames)); 142 | } 143 | 144 | private void AssertMessageFail(MessageBuildResult messageBuildResult) 145 | { 146 | Assert.That(messageBuildResult.Success, Is.False); 147 | Assert.That(messageBuildResult.Error, Is.Not.Null); 148 | Assert.That(messageBuildResult.Message, Is.Null); 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /src/Mixpanel/Mixpanel.Tests/MixpanelClient/Track/MixpanelClientAliasTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Bogus; 6 | using FluentAssertions; 7 | using Newtonsoft.Json.Linq; 8 | using NUnit.Framework; 9 | 10 | namespace Mixpanel.Tests.Unit.MixpanelClient.Track 11 | { 12 | [TestFixture] 13 | public class MixpanelClientAliasTests : MixpanelClientTrackTestsBase 14 | { 15 | [Test] 16 | [TrackSuperProps(TrackSuperPropsDetails.DistinctId)] 17 | public async Task Given_SendAsync_When_DistinctIdFromSuperProps_Then_CorrectDataSent() 18 | { 19 | await Client.AliasAsync(Alias); 20 | AssertSentData(SuperDistinctId); 21 | } 22 | 23 | [Test] 24 | public async Task Given_SendAsync_When_DistinctIdFromParams_Then_CorrectDataSent() 25 | { 26 | await Client.AliasAsync(DistinctId, Alias); 27 | AssertSentData(DistinctId); 28 | } 29 | 30 | [Test] 31 | public void AliasAsync_CancellationRequested_RequestCancelled() 32 | { 33 | // Arrange 34 | var (token, distinctId, alias, httpMockMixpanelConfig) = GenerateInputs(); 35 | var superProps = new { DistinctId = distinctId }; 36 | var cancellationTokenSource = new CancellationTokenSource(); 37 | var client = new Mixpanel.MixpanelClient(token, httpMockMixpanelConfig.Instance, superProps); 38 | 39 | // Act 40 | var task = Task.Factory.StartNew( 41 | async () => await client.AliasAsync(alias, cancellationTokenSource.Token)); 42 | cancellationTokenSource.Cancel(); 43 | task.Wait(); 44 | 45 | // Assert 46 | httpMockMixpanelConfig.RequestCancelled.Should().BeTrue(); 47 | } 48 | 49 | [Test] 50 | public void AliasAsyncWithDistinctId_CancellationRequested_RequestCancelled() 51 | { 52 | // Arrange 53 | var (token, distinctId, alias, httpMockMixpanelConfig) = GenerateInputs(); 54 | var cancellationTokenSource = new CancellationTokenSource(); 55 | var client = new Mixpanel.MixpanelClient(token, httpMockMixpanelConfig.Instance); 56 | 57 | // Act 58 | var task = Task.Factory.StartNew( 59 | async () => await client.AliasAsync(distinctId, alias, cancellationTokenSource.Token)); 60 | cancellationTokenSource.Cancel(); 61 | task.Wait(); 62 | 63 | // Assert 64 | httpMockMixpanelConfig.RequestCancelled.Should().BeTrue(); 65 | } 66 | 67 | [Test] 68 | [TrackSuperProps(TrackSuperPropsDetails.DistinctId)] 69 | public void Given_GetMessage_When_DistinctIdFromSuperProps_Then_CorrectMessageReturned() 70 | { 71 | MixpanelMessage message = Client.GetAliasMessage(Alias); 72 | AssertMessage(message, SuperDistinctId); 73 | } 74 | 75 | [Test] 76 | public void Given_GetMessage_When_DistinctIdFromParams_Then_CorrectMessageReturned() 77 | { 78 | MixpanelMessage message = Client.GetAliasMessage(DistinctId, Alias); 79 | AssertMessage(message, DistinctId); 80 | } 81 | 82 | [Test] 83 | [TrackSuperProps(TrackSuperPropsDetails.DistinctId)] 84 | public void Given_GetTestMessage_When_DistinctIdFromSuperProps_Then_CorrectMessageReturned() 85 | { 86 | MixpanelMessageTest message = Client.AliasTest(Alias); 87 | AssertDictionary(message.Data, SuperDistinctId); 88 | } 89 | 90 | [Test] 91 | public void Given_GetTestMessage_When_DistinctIdFromParams_Then_CorrectMessageReturned() 92 | { 93 | MixpanelMessageTest message = Client.AliasTest(DistinctId, Alias); 94 | AssertDictionary(message.Data, DistinctId); 95 | } 96 | 97 | private void AssertSentData(object expectedDistinctId) 98 | { 99 | var (endpoint, data) = HttpPostEntries.Single(); 100 | 101 | Assert.That(endpoint, Is.EqualTo(TrackUrl)); 102 | 103 | var msg = ParseMessageData(data); 104 | AssertJson(msg, expectedDistinctId); 105 | } 106 | 107 | private void AssertJson(JObject msg, object expectedDistinctId) 108 | { 109 | Assert.That(msg.Count, Is.EqualTo(2)); 110 | Assert.That(msg["event"].Value(), Is.EqualTo("$create_alias")); 111 | 112 | var props = (JObject)msg["properties"]; 113 | Assert.That(props.Count, Is.EqualTo(3)); 114 | Assert.That(props["token"].Value(), Is.EqualTo(Token)); 115 | Assert.That(props["distinct_id"].Value(), Is.EqualTo(expectedDistinctId)); 116 | Assert.That(props["alias"].Value(), Is.EqualTo(Alias)); 117 | } 118 | 119 | private void AssertMessage(MixpanelMessage msg, object expectedDistinctId) 120 | { 121 | Assert.That(msg.Kind, Is.EqualTo(MessageKind.Alias)); 122 | AssertDictionary(msg.Data, expectedDistinctId); 123 | } 124 | 125 | private void AssertDictionary(IDictionary dic, object expectedDistinctId) 126 | { 127 | Assert.That(dic.Count, Is.EqualTo(2)); 128 | Assert.That(dic["event"], Is.EqualTo("$create_alias")); 129 | Assert.That(dic["properties"], Is.TypeOf>()); 130 | var props = (Dictionary)dic["properties"]; 131 | Assert.That(props.Count, Is.EqualTo(3)); 132 | Assert.That(props["token"], Is.EqualTo(Token)); 133 | Assert.That(props["distinct_id"], Is.EqualTo(expectedDistinctId)); 134 | Assert.That(props["alias"], Is.EqualTo(Alias)); 135 | } 136 | 137 | private (string token, string distinctId, string alias, HttpMockMixpanelConfig httpMockMixpanelConfig) GenerateInputs() 138 | { 139 | var randomizer = new Randomizer(); 140 | return 141 | ( 142 | randomizer.AlphaNumeric(32), 143 | randomizer.AlphaNumeric(10), 144 | randomizer.AlphaNumeric(10), 145 | new HttpMockMixpanelConfig() 146 | ); 147 | } 148 | } 149 | } --------------------------------------------------------------------------------