From e5d70cb63a13fc7320887bb138326b3e7b7609d0 Mon Sep 17 00:00:00 2001 From: Anthony Greer Date: Fri, 8 Dec 2017 12:50:21 -0500 Subject: [PATCH] Add Support for UA's UserID (#10) * initial add with tests * add tests to new factory methods * some renaming of 'Anonymous Client Id' to 'Client Id' * allow exception tracking via a returned object... nobody wants their app to crash because of tracking. * implemented Version 5 GUID logic for ClientId * added 1.4.4 and 1.4.4-rc nuget packages (they are the same code) --- ...icsMeasurementProtocolWrapper.1.2.0.nuspec | 47 ---- ...sMeasurementProtocolWrapper.1.4.4-rc.nupkg | Bin 0 -> 12372 bytes ...ticsMeasurementProtocolWrapper.1.4.4.nupkg | Bin 0 -> 12369 bytes ...nalyticsMeasurementProtocolWrapper.nuspec} | 3 +- README.md | 135 +++++++++--- .../EventTracker.cs | 57 +++-- .../GoogleDataSender.cs | 7 +- .../IEventTracker.cs | 16 +- .../IUniversalAnalyticsEvent.cs | 4 + .../IUniversalAnalyticsEventFactory.cs | 66 +++++- .../Objects/ClientId.cs | 114 ++++++++++ .../Objects/UserId.cs | 31 +++ .../PostDataBuilder.cs | 13 +- .../TrackingResult.cs | 29 +++ .../UniversalAnalyticsEvent.cs | 72 +++---- .../UniversalAnalyticsEventFactory.cs | 101 ++++++++- .../UniversalAnalyticsHttpWrapper.csproj | 3 + ...salAnalyticsHttpWrapper.csproj.DotSettings | 2 + .../EventTrackerTests.cs | 39 ++++ .../IntegrationTests.cs | 204 ++++++++++++------ .../Objects/ClientIdTests.cs | 57 +++++ .../Objects/UserIdTests.cs | 26 +++ .../PostDataBuilderTests.cs | 10 + .../TrackingResultTests.cs | 26 +++ .../UniversalAnalyticsEventFactoryTests.cs | 76 ++++++- .../UniversalAnalyticsEventTests.cs | 67 ++++-- ...UniversalAnalyticsHttpWrapper.Tests.csproj | 3 + 27 files changed, 954 insertions(+), 254 deletions(-) delete mode 100644 Nuget/UniversalAnalyticsMeasurementProtocolWrapper.1.2.0.nuspec create mode 100644 Nuget/UniversalAnalyticsMeasurementProtocolWrapper.1.4.4-rc.nupkg create mode 100644 Nuget/UniversalAnalyticsMeasurementProtocolWrapper.1.4.4.nupkg rename Nuget/{UniversalAnalyticsMeasurementProtocolWrapper.1.4.0.nuspec => UniversalAnalyticsMeasurementProtocolWrapper.nuspec} (96%) create mode 100644 Source/UniversalAnalyticsHttpWrapper/Objects/ClientId.cs create mode 100644 Source/UniversalAnalyticsHttpWrapper/Objects/UserId.cs create mode 100644 Source/UniversalAnalyticsHttpWrapper/TrackingResult.cs create mode 100644 Source/UniversalAnalyticsHttpWrapper/UniversalAnalyticsHttpWrapper.csproj.DotSettings create mode 100644 Tests/UniversalAnalyticsHttpWrapper.Tests/Objects/ClientIdTests.cs create mode 100644 Tests/UniversalAnalyticsHttpWrapper.Tests/Objects/UserIdTests.cs create mode 100644 Tests/UniversalAnalyticsHttpWrapper.Tests/TrackingResultTests.cs diff --git a/Nuget/UniversalAnalyticsMeasurementProtocolWrapper.1.2.0.nuspec b/Nuget/UniversalAnalyticsMeasurementProtocolWrapper.1.2.0.nuspec deleted file mode 100644 index 3c42d4b..0000000 --- a/Nuget/UniversalAnalyticsMeasurementProtocolWrapper.1.2.0.nuspec +++ /dev/null @@ -1,47 +0,0 @@ - - - - UniversalAnalyticsMeasurementProtocolWrapper - 1.2.0 - Universal Analytics Measurement Protocol .NET Wrapper - jakejgordon - jakejgordon - https://github.com/jakejgordon/Universal-Analytics-For-DotNet/blob/master/LICENSE - https://github.com/jakejgordon/Universal-Analytics-For-DotNet - https://raw.githubusercontent.com/jakejgordon/Universal-Analytics-For-DotNet/master/universal_analytics_measurent_protocol_wrapper_logo.png - false - Wrapper for pushing data to Universal Analytics properties via the Measurement Protocol (version 1) on the server-side. This version only supports pushing Event data to web properties. - Wrapper for pushing data to Universal Analytics properties via the Measurement Protocol on the server-side. This version only supports pushing Event data to web properties. - Major overhaul to the interfaces. Provided a factory for getting IUniversalAnalyticsEvent objects, and eliminated one of the required app settings. - jakejgordon 2014 - en-US - UniversalAnalytics Analytics MeasurementProtocol GoogleAnalytics - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Nuget/UniversalAnalyticsMeasurementProtocolWrapper.1.4.4-rc.nupkg b/Nuget/UniversalAnalyticsMeasurementProtocolWrapper.1.4.4-rc.nupkg new file mode 100644 index 0000000000000000000000000000000000000000..714f69de8dd12e463196ebbb2799fd4425dbfa0e GIT binary patch literal 12372 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 literal 0 HcmV?d00001 diff --git a/Nuget/UniversalAnalyticsMeasurementProtocolWrapper.1.4.0.nuspec b/Nuget/UniversalAnalyticsMeasurementProtocolWrapper.nuspec similarity index 96% rename from Nuget/UniversalAnalyticsMeasurementProtocolWrapper.1.4.0.nuspec rename to Nuget/UniversalAnalyticsMeasurementProtocolWrapper.nuspec index db749c2..358d795 100644 --- a/Nuget/UniversalAnalyticsMeasurementProtocolWrapper.1.4.0.nuspec +++ b/Nuget/UniversalAnalyticsMeasurementProtocolWrapper.nuspec @@ -2,7 +2,7 @@ UniversalAnalyticsMeasurementProtocolWrapper - 1.4.0 + 1.4.4 Universal Analytics Measurement Protocol .NET Wrapper jakejgordon jakejgordon @@ -13,6 +13,7 @@ Wrapper for pushing data to Universal Analytics properties via the Measurement Protocol (version 1) on the server-side. This version only supports pushing Event data to web properties. Wrapper for pushing data to Universal Analytics properties via the Measurement Protocol on the server-side. This version only supports pushing Event data to web properties. + * 1.4.4 Added ability to track via userId; Compress errors into object to avoid crashes. * 1.4.0 Added async methods and switched the back-end to use the HTTPS Google endpoint instead of the HTTP one * 1.3.0 Major overhaul to the interfaces. Provided a factory for getting IUniversalAnalyticsEvent objects, and eliminated one of the required app settings. jakejgordon 2017 diff --git a/README.md b/README.md index d31a2e3..582e806 100644 --- a/README.md +++ b/README.md @@ -4,48 +4,125 @@ Universal-Analytics-For-DotNet ============================== -A .NET wrapper over top of Google's Universal Analytics Measurement Protocol HTTP API. For now, this wrapper allows you to push Events from server-side code. This offers the advantages of 1) not relying on client-side javascript and 2) allowing you to push more interesting events that may not be available on the client side. For example, if you have a website for collecting donations you could push an event to indicate that a donation occurred and push the "value" of that donation. See https://developers.google.com/analytics/devguides/collection/protocol/v1/devguide#event for more details. +A .NET wrapper over top of Google's Universal Analytics Measurement Protocol HTTP API. This wrapper allows you to push UA events from server-side code. -Pushing an event is as simple as: +## Event Tracking +First, add your tracking id to your App.config/Web.config: +```xml + + + + + ... + ``` + +Then, create an event tracker and factory. +```c# IEventTracker eventTracker = new EventTracker(); -IUniversalAnalyticsEvent analyticsEvent = new UniversalAnalyticsEvent( - //Required. Anonymous client id. - //See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#cid for details. - "developer", - //Required. The event category for the event. - // See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ec for details. - "test category", - //Required. The event action for the event. - //See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ea for details. - "test action", - //The event label for the event. - // See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#el for details. - "test label", - //Optional. The event value for the event. - // See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ev for details. - "10"); -eventTracker.TrackEvent(analyticsEvent); +// The factory pulls your tracking ID from the .config so you don't have to. +IUniversalAnalyticsEventFactory eventFactory = new UniversalAnalyticsEventFactory(); ``` -Make sure you define your UA tracking ID in your project's App.config/Web.config! +Next, create an event to push to Google Analytics. Note that Google has defined that an event must have either a `ClientId (cid)`, [see here](https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#cid), or `UserId (uid)`, [see here](https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#uid). The factory has overloads for each of these cases. + +To create an event with a `ClientId`: + +```c# +// Create a clientId with a random Guid... +ClientId clientId = new ClientId(); +// OR from a supplied Guid... +ClientId clientId = new ClientId(new Guid("...")); +// OR from a supplied string (uses a hash of the string). +ClientId clientId = new ClientId("..."); +var analyticsEvent = eventFactory.MakeUniversalAnalyticsEvent( + // Required. The client id associated with this event. + // See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#cid for details. + clientId, + // Required. The event category for the event. + // See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ec for details. + "test category", + // Required. The event action for the event. + //See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ea for details. + "test action", + // Optional. The event label for the event. + // See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#el for details. + "test label", + // Optional. The event value for the event. + // See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ev for details. + "10"); ``` - - - - - ... - + +To create an event with a `UserId`: + +```c# +// Create a user id from a string +UserId userId = new UserId("user-id"); + +// Create an event with a user id: +var analyticsEvent = eventFactory.MakeUniversalAnalyticsEvent( + // Required. The user id associated with this event. + // See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#uid for details. + userId, + // Required. The event category for the event. + // See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ec for details. + "test category", + // Required. The event action for the event. + //See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ea for details. + "test action", + // Optional. The event label for the event. + // See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#el for details. + "test label", + // Optional. The event value for the event. + // See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ev for details. + "10"); +``` + +To create an event without using `ClientId` or `UserId` objects: + +```c# +var analyticsEvent = eventFactory.MakeUniversalAnalyticsEvent( + // Required (if not using userId). The client id for this event. + // See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#cid for details. + "35009a79-1a05-49d7-b876-2b884d0f825b", + // Required. The event category for the event. + // See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ec for details. + "test category", + // Required. The event action for the event. + //See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ea for details. + "test action", + // Optional. The event label for the event. + // See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#el for details. + "test label", + // Optional. The event value for the event. + // See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ev for details. + "10", + // Required (if not using clientId). The user id associated with this event. + // See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#uid for details. + "user-id"); ``` + +Finally, push the event to Google Analytics using the EventTracker: + +```c# +var trackingResult = eventTracker.TrackEvent(analyticsEvent); + +// Note that exceptions are contained in the result object and not thrown for stability. +if (trackingResult.Failed) +{ + // Log to the appropriate error handler. + Console.Error.WriteLine(trackingResult.Exception); +} +``` + +## Notes The code is almost entirely unit/integration tested so it should be stable and easily updatable. I'm using it on my own site right now so you can find more specific examples at: https://github.com/jakejgordon/NemeStats For your own application you will probably want to create an additional wrapper over top of this so you can confine the EventCategory and EventAction values to something that makes sense for your own app (without having to hard-code magic strings for the parameters). My website has examples of this as well. ![Alt text](https://raw.githubusercontent.com/jakejgordon/Universal-Analytics-For-DotNet/master/universal_analytics_realtime_events_screenshot.jpg?raw=true "Screenshot of Real-Time Events After Pushing Data") -# NuGet +## NuGet [Check out the NuGet page for this package](https://www.nuget.org/packages/UniversalAnalyticsMeasurementProtocolWrapper/) - - diff --git a/Source/UniversalAnalyticsHttpWrapper/EventTracker.cs b/Source/UniversalAnalyticsHttpWrapper/EventTracker.cs index 821881c..75e5da9 100644 --- a/Source/UniversalAnalyticsHttpWrapper/EventTracker.cs +++ b/Source/UniversalAnalyticsHttpWrapper/EventTracker.cs @@ -8,8 +8,9 @@ namespace UniversalAnalyticsHttpWrapper /// public class EventTracker : IEventTracker { - private readonly IPostDataBuilder postDataBuilder; - private readonly IGoogleDataSender googleDataSender; + private readonly IPostDataBuilder _postDataBuilder; + private readonly IGoogleDataSender _googleDataSender; + /// /// This is the current Google collection URI for version 1 of the measurement protocol /// @@ -24,8 +25,8 @@ public class EventTracker : IEventTracker /// public EventTracker() { - postDataBuilder = new PostDataBuilder(); - googleDataSender = new GoogleDataSender(); + this._postDataBuilder = new PostDataBuilder(); + this._googleDataSender = new GoogleDataSender(); } /// @@ -35,30 +36,52 @@ public EventTracker() /// internal EventTracker(IPostDataBuilder postDataBuilder, IGoogleDataSender googleDataSender) { - this.postDataBuilder = postDataBuilder; - this.googleDataSender = googleDataSender; + this._postDataBuilder = postDataBuilder; + this._googleDataSender = googleDataSender; } /// - /// Pushes an event up to the Universal Analytics web property specified in the .config file. + /// Tracks the event and puts the result in a container object. /// - /// The event to be logged. - public void TrackEvent(IUniversalAnalyticsEvent analyticsEvent) - { - string postData = postDataBuilder.BuildPostDataString(MEASUREMENT_PROTOCOL_VERSION, analyticsEvent); + /// + /// The result of the tracking operation + public TrackingResult TrackEvent(IUniversalAnalyticsEvent analyticsEvent) + { + var result = new TrackingResult(); + + try + { + string postData = this._postDataBuilder.BuildPostDataString(MEASUREMENT_PROTOCOL_VERSION, analyticsEvent); + this._googleDataSender.SendData(GOOGLE_COLLECTION_URI, postData); + } + catch (Exception e) + { + result.Exception = e; + } - googleDataSender.SendData(GOOGLE_COLLECTION_URI, postData); + return result; } /// - /// Pushes an event up to the Universal Analytics web property specified in the .config file. + /// Tracks the event and puts the result in a container object. /// - /// The event to be logged. - public async Task TrackEventAsync(IUniversalAnalyticsEvent analyticsEvent) + /// + /// The result of the tracking operation + public async Task TrackEventAsync(IUniversalAnalyticsEvent analyticsEvent) { - var postData = postDataBuilder.BuildPostDataCollection(MEASUREMENT_PROTOCOL_VERSION, analyticsEvent); + var result = new TrackingResult(); + + try + { + var postData = this._postDataBuilder.BuildPostDataCollection(MEASUREMENT_PROTOCOL_VERSION, analyticsEvent); + await this._googleDataSender.SendDataAsync(GOOGLE_COLLECTION_URI, postData); + } + catch (Exception e) + { + result.Exception = e; + } - await googleDataSender.SendDataAsync(GOOGLE_COLLECTION_URI, postData); + return result; } } } diff --git a/Source/UniversalAnalyticsHttpWrapper/GoogleDataSender.cs b/Source/UniversalAnalyticsHttpWrapper/GoogleDataSender.cs index 575b1b6..944edb0 100644 --- a/Source/UniversalAnalyticsHttpWrapper/GoogleDataSender.cs +++ b/Source/UniversalAnalyticsHttpWrapper/GoogleDataSender.cs @@ -21,13 +21,14 @@ public GoogleDataSender() public void SendData(Uri googleCollectionUri, string postData) { - if (String.IsNullOrEmpty(postData)) + if (string.IsNullOrEmpty(postData)) throw new ArgumentNullException("postData", "Request body cannot be empty."); - HttpWebRequest httpRequest = WebRequest.CreateHttp(googleCollectionUri); + var httpRequest = WebRequest.CreateHttp(googleCollectionUri); httpRequest.ContentLength = Encoding.UTF8.GetByteCount(postData); httpRequest.Method = "POST"; - using(Stream requestStream = httpRequest.GetRequestStream()) + + using(var requestStream = httpRequest.GetRequestStream()) { using (var writer = new StreamWriter(requestStream)) { diff --git a/Source/UniversalAnalyticsHttpWrapper/IEventTracker.cs b/Source/UniversalAnalyticsHttpWrapper/IEventTracker.cs index 5a87fb3..e47f16d 100644 --- a/Source/UniversalAnalyticsHttpWrapper/IEventTracker.cs +++ b/Source/UniversalAnalyticsHttpWrapper/IEventTracker.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Linq; using System.Threading.Tasks; namespace UniversalAnalyticsHttpWrapper @@ -12,14 +9,17 @@ namespace UniversalAnalyticsHttpWrapper public interface IEventTracker { /// - /// + /// Tracks the event and puts the result in a container object. /// /// - void TrackEvent(IUniversalAnalyticsEvent analyticsEvent); + /// The result of the tracking operation + TrackingResult TrackEvent(IUniversalAnalyticsEvent analyticsEvent); + /// - /// + /// Tracks the event and puts the result in a container object. /// /// - Task TrackEventAsync(IUniversalAnalyticsEvent analyticsEvent); + /// The result of the tracking operation + Task TrackEventAsync(IUniversalAnalyticsEvent analyticsEvent); } } diff --git a/Source/UniversalAnalyticsHttpWrapper/IUniversalAnalyticsEvent.cs b/Source/UniversalAnalyticsHttpWrapper/IUniversalAnalyticsEvent.cs index 616df7f..5a4b2a8 100644 --- a/Source/UniversalAnalyticsHttpWrapper/IUniversalAnalyticsEvent.cs +++ b/Source/UniversalAnalyticsHttpWrapper/IUniversalAnalyticsEvent.cs @@ -31,5 +31,9 @@ public interface IUniversalAnalyticsEvent /// Gets the event value for this event. /// string EventValue { get; } + /// + /// Gets the optional user Id for this event. + /// + string UserId { get; } } } diff --git a/Source/UniversalAnalyticsHttpWrapper/IUniversalAnalyticsEventFactory.cs b/Source/UniversalAnalyticsHttpWrapper/IUniversalAnalyticsEventFactory.cs index f9a3f52..785d50a 100644 --- a/Source/UniversalAnalyticsHttpWrapper/IUniversalAnalyticsEventFactory.cs +++ b/Source/UniversalAnalyticsHttpWrapper/IUniversalAnalyticsEventFactory.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using UniversalAnalyticsHttpWrapper.Objects; namespace UniversalAnalyticsHttpWrapper { @@ -15,7 +11,7 @@ public interface IUniversalAnalyticsEventFactory /// This constructor expects an App Setting for 'UniversalAnalytics.TrackingId' /// in the config. UniversalAnalytics.TrackingId must be a Universal Analytics Web Property. /// - /// Required. Anonymous client id for the event. + /// Required. Anonymous client id for the event. /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#cid for details. /// Required. The event category for the event. /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ec for details. @@ -25,13 +21,69 @@ public interface IUniversalAnalyticsEventFactory /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#el for details. /// Optional. The event value for the event. /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ev for details. + /// Optional. The userId value for the event. This will override anonymousClientId. + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#uid for details. /// Thrown when /// one of the required config attributes are missing. /// Thrown when one of the required fields are null or whitespace. /// Thrown when the HttpRequest that's posted to Google returns something /// other than a 200 OK response. + /// IUniversalAnalyticsEvent MakeUniversalAnalyticsEvent( - string anonymousClientId, + string clientId, + string eventCategory, + string eventAction, + string eventLabel, + string eventValue = null, + string userId = null); + + /// + /// This constructor expects an App Setting for 'UniversalAnalytics.TrackingId' + /// in the config. UniversalAnalytics.TrackingId must be a Universal Analytics Web Property. + /// + /// Required. Anonymous client id for the event. + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#cid for details. + /// Required. The event category for the event. + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ec for details. + /// Required. The event action for the event. + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ea for details. + /// The event label for the event. + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#el for details. + /// Optional. The event value for the event. + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ev for details. + /// Thrown when + /// one of the required config attributes are missing. + /// Thrown when one of the required fields are null or whitespace. + /// Thrown when the HttpRequest that's posted to Google returns something + /// other than a 200 OK response. + IUniversalAnalyticsEvent MakeUniversalAnalyticsEvent( + ClientId clientId, + string eventCategory, + string eventAction, + string eventLabel, + string eventValue = null); + + /// + /// This constructor expects an App Setting for 'UniversalAnalytics.TrackingId' + /// in the config. UniversalAnalytics.TrackingId must be a Universal Analytics Web Property. + /// + /// Required. The user id for the event. Will create an event with the uid defined. + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#uid for details. + /// Required. The event category for the event. + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ec for details. + /// Required. The event action for the event. + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ea for details. + /// The event label for the event. + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#el for details. + /// Optional. The event value for the event. + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ev for details. + /// Thrown when + /// one of the required config attributes are missing. + /// Thrown when one of the required fields are null or whitespace. + /// Thrown when the HttpRequest that's posted to Google returns something + /// other than a 200 OK response. + IUniversalAnalyticsEvent MakeUniversalAnalyticsEvent( + UserId userId, string eventCategory, string eventAction, string eventLabel, diff --git a/Source/UniversalAnalyticsHttpWrapper/Objects/ClientId.cs b/Source/UniversalAnalyticsHttpWrapper/Objects/ClientId.cs new file mode 100644 index 0000000..d4490fa --- /dev/null +++ b/Source/UniversalAnalyticsHttpWrapper/Objects/ClientId.cs @@ -0,0 +1,114 @@ +using System; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace UniversalAnalyticsHttpWrapper.Objects +{ + /// + /// Data object holding a GUID representing a client id (or cid). + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#cid + /// + public class ClientId + { + // Randomly generated UUID chosen as the 'namespace' for generating deterministic GUIDs from strings for use as + // client ids. + private static readonly Guid CidNamespace = new Guid("174bb9db-f7df-4503-9302-56c6d58e53d2"); + + /// + /// Creates a new client id with a random Guid as the id. + /// + public ClientId() + { + this.Id = Guid.NewGuid(); + } + + /// + /// Creates a new client id with a guid generated from a seed string. + /// + public ClientId(string value) + { + // Use a version 5 UUID generated from the SHA1 hash of 'value' + // to create the cid. + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + byte[] namespaceBytes = CidNamespace.ToByteArray(); + + // GUID struct stores time_low, time_mid, and time_hi_and_version (see RFC 4122) + // as DWORD and WORD(s), respectively. Network byte order + // requires big endian for these groups but they are stored in little + // endian on x86 machines. Swap those byte groups: + SwapByteOrder(namespaceBytes); + + // Concatenate the namespace with the 'name' (valueBytes) for hashing. + byte[] concatBytes = namespaceBytes.Concat(valueBytes).ToArray(); + + // Hash the concatenated byte array. + byte[] hash; + using (SHA1 sha1 = new SHA1CryptoServiceProvider()) + { + hash = sha1.ComputeHash(concatBytes); + } + + byte[] cidBytes = new byte[16]; // 128-bytes for a new GUID. + Buffer.BlockCopy(hash, 0, cidBytes, 0, 16); // Copy the first 128-bytes of the hash (160 bytes) to the new GUID. + + // The current GUID is in network byte order; this is big-endian. So, the MSB of + // time_hi_and_version is index 6 not 7. + cidBytes[6] &= 0x5F; + + // Set msb of clock_seq_hi_and_reserved to zero and one, respectively. + cidBytes[8] &= 0x7F; + + // Convert back to local byte order (little-endian): + SwapByteOrder(cidBytes); + + Id = new Guid(cidBytes); + } + + private static void SwapByteOrder(byte[] guid) + { + // time_low, DWORD + Swap(0, 3, guid); + Swap(1, 2, guid); + + // time_mid, WORD + Swap(5, 4, guid); + + // time_hi_and_version, WORD + Swap(7, 6, guid); + } + + private static void Swap(int a, int b, byte[] arr) + { + byte temp = arr[a]; + arr[a] = arr[b]; + arr[b] = temp; + } + + /// + /// Creates a new client id with the supplied Guid 'id' as the client id. + /// + /// The Guid representing the client id. + /// Thrown if 'id' is null. + public ClientId(Guid id) + { + if (id == null) + { + throw new ArgumentNullException(nameof(id)); + } + + this.Id = id; + } + + /// + /// Value holder for the client id. + /// + public Guid Id { get; } + } +} \ No newline at end of file diff --git a/Source/UniversalAnalyticsHttpWrapper/Objects/UserId.cs b/Source/UniversalAnalyticsHttpWrapper/Objects/UserId.cs new file mode 100644 index 0000000..15e27da --- /dev/null +++ b/Source/UniversalAnalyticsHttpWrapper/Objects/UserId.cs @@ -0,0 +1,31 @@ +using System; + +namespace UniversalAnalyticsHttpWrapper.Objects +{ + /// + /// Data object holding a string representing a user id (or uid). + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#uid + /// + public class UserId + { + /// + /// Creates a user id with an id of parameter 'id'. + /// + /// string representing the user id. + /// Thrown if id is null. + public UserId(string id) + { + if (id == null) + { + throw new ArgumentNullException(nameof(id)); + } + + this.Id = id; + } + + /// + /// The UA user Id. + /// + public string Id { get; } + } +} diff --git a/Source/UniversalAnalyticsHttpWrapper/PostDataBuilder.cs b/Source/UniversalAnalyticsHttpWrapper/PostDataBuilder.cs index d165756..0e21596 100644 --- a/Source/UniversalAnalyticsHttpWrapper/PostDataBuilder.cs +++ b/Source/UniversalAnalyticsHttpWrapper/PostDataBuilder.cs @@ -10,6 +10,7 @@ internal class PostDataBuilder : IPostDataBuilder internal const string PARAMETER_KEY_VERSION = "v"; internal const string PARAMETER_KEY_TRACKING_ID = "tid"; internal const string PARAMETER_KEY_ANONYMOUS_CLIENT_ID = "cid"; + internal const string PARAMETER_KEY_USER_ID = "uid"; internal const string PARAMETER_KEY_HIT_TYPE = "t"; internal const string PARAMETER_KEY_EVENT_CATEGORY = "ec"; internal const string PARAMETER_KEY_EVENT_ACTION = "ea"; @@ -39,7 +40,17 @@ internal NameValueCollection BuildPostData(string measurementProtocolVersion, IU NameValueCollection nameValueCollection = HttpUtility.ParseQueryString(string.Empty); nameValueCollection[PARAMETER_KEY_VERSION] = measurementProtocolVersion; nameValueCollection[PARAMETER_KEY_TRACKING_ID] = analyticsEvent.TrackingId; - nameValueCollection[PARAMETER_KEY_ANONYMOUS_CLIENT_ID] = analyticsEvent.AnonymousClientId; + + if (!string.IsNullOrWhiteSpace(analyticsEvent.UserId)) + { + nameValueCollection[PARAMETER_KEY_USER_ID] = analyticsEvent.UserId; + } + + if (!string.IsNullOrWhiteSpace(analyticsEvent.AnonymousClientId)) + { + nameValueCollection[PARAMETER_KEY_ANONYMOUS_CLIENT_ID] = analyticsEvent.AnonymousClientId; + } + nameValueCollection[PARAMETER_KEY_HIT_TYPE] = HitTypeEnum.@event.ToString(); nameValueCollection[PARAMETER_KEY_EVENT_ACTION] = analyticsEvent.EventAction; nameValueCollection[PARAMETER_KEY_EVENT_CATEGORY] = analyticsEvent.EventCategory; diff --git a/Source/UniversalAnalyticsHttpWrapper/TrackingResult.cs b/Source/UniversalAnalyticsHttpWrapper/TrackingResult.cs new file mode 100644 index 0000000..535b857 --- /dev/null +++ b/Source/UniversalAnalyticsHttpWrapper/TrackingResult.cs @@ -0,0 +1,29 @@ +using System; +using System.Linq; + +namespace UniversalAnalyticsHttpWrapper +{ + /// + /// Result of a tracking request. + /// + public class TrackingResult + { + /// + /// Creates a tracking result based on the HTTP status code as well as an exception, if any. + /// + /// + public TrackingResult(Exception exception = null) + { + this.Exception = exception; + } + + /// + /// Checks if the tracking attempt was successful. + /// + public bool Failed => Exception != null; + /// + /// An exception caught during the tracking process. Not thrown for stability. + /// + public Exception Exception { get; internal set; } + } +} diff --git a/Source/UniversalAnalyticsHttpWrapper/UniversalAnalyticsEvent.cs b/Source/UniversalAnalyticsHttpWrapper/UniversalAnalyticsEvent.cs index ec335fb..5828ec5 100644 --- a/Source/UniversalAnalyticsHttpWrapper/UniversalAnalyticsEvent.cs +++ b/Source/UniversalAnalyticsHttpWrapper/UniversalAnalyticsEvent.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; namespace UniversalAnalyticsHttpWrapper { @@ -15,13 +14,6 @@ public class UniversalAnalyticsEvent : IUniversalAnalyticsEvent { internal const string EXCEPTION_MESSAGE_PARAMETER_CANNOT_BE_NULL_OR_WHITESPACE = "{0} cannot be null or whitespace"; - private readonly string trackingId; - private readonly string anonymousClientId; - private readonly string eventCategory; - private readonly string eventAction; - private readonly string eventLabel; - private readonly string eventValue; - /// Required. The universal analytics tracking id for the property /// that events will be logged to. If you don't want to pass this every time, set the UniversalAnalytics.TrackingId /// app setting and use the UniversalAnalyticsEventFactory to get instances of this class. @@ -36,6 +28,8 @@ public class UniversalAnalyticsEvent : IUniversalAnalyticsEvent /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#el for details. /// Optional. The event value for the event. /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ev for details. + /// Optional. The userId value for the event. This will override anonymousClientId. + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#uid for details. /// Thrown when /// one of the required config attributes are missing. /// Thrown when one of the required fields are null or whitespace. @@ -47,14 +41,16 @@ public UniversalAnalyticsEvent( string eventCategory, string eventAction, string eventLabel, - string eventValue = null) + string eventValue = null, + string userId = null) { - this.trackingId = trackingId; - this.anonymousClientId = anonymousClientId; - this.eventCategory = eventCategory; - this.eventAction = eventAction; - this.eventLabel = eventLabel; - this.eventValue = eventValue; + this.TrackingId = trackingId; + this.AnonymousClientId = anonymousClientId; + this.EventCategory = eventCategory; + this.EventAction = eventAction; + this.EventLabel = eventLabel; + this.EventValue = eventValue; + this.UserId = userId; ValidateRequiredFields(); } @@ -62,75 +58,63 @@ public UniversalAnalyticsEvent( /// /// Gets the tracking id for the Universal Analytics property /// - public string TrackingId - { - get { return trackingId; } - } + public string TrackingId { get; } /// /// Gets the anonymousClientId that was passed in when the object was constructed. /// - public string AnonymousClientId - { - get { return anonymousClientId; } - } + public string AnonymousClientId { get; } /// /// Gets the eventCategory that was passed in when the object was constructed. /// - public string EventCategory - { - get { return eventCategory; } - } + public string EventCategory { get; } /// /// Gets the eventAction that was passed in when the object was constructed. /// - public string EventAction - { - get { return eventAction; } - } + public string EventAction { get; } /// /// Gets the eventLabel that was passed in when the object was constructed. /// - public string EventLabel - { - get { return eventLabel; } - } + public string EventLabel { get; } /// /// Gets the eventValue that was passed in when the object was constructed. /// - public string EventValue - { - get { return eventValue; } - } + public string EventValue { get; } + + /// + /// Gets the userId that was passed in when the object was constructed. + /// + public string UserId { get; } + private void ValidateRequiredFields() { - if (string.IsNullOrWhiteSpace(trackingId)) + if (string.IsNullOrWhiteSpace(TrackingId)) { throw new ArgumentException( string.Format(EXCEPTION_MESSAGE_PARAMETER_CANNOT_BE_NULL_OR_WHITESPACE, "analyticsEvent.TrackingId")); } - if (string.IsNullOrWhiteSpace(anonymousClientId)) + if (string.IsNullOrWhiteSpace(AnonymousClientId) && string.IsNullOrWhiteSpace(UserId)) { throw new ArgumentException( string.Format(EXCEPTION_MESSAGE_PARAMETER_CANNOT_BE_NULL_OR_WHITESPACE, - "analyticsEvent.AnonymousClientId")); + "analyticsEvent.AnonymousClientId || analyticsEvent.UserId")); } - if (string.IsNullOrWhiteSpace(eventCategory)) + if (string.IsNullOrWhiteSpace(EventCategory)) { throw new ArgumentException( string.Format(EXCEPTION_MESSAGE_PARAMETER_CANNOT_BE_NULL_OR_WHITESPACE, "analyticsEvent.EventCategory")); } - if (string.IsNullOrWhiteSpace(eventAction)) + if (string.IsNullOrWhiteSpace(EventAction)) { throw new ArgumentException( string.Format(EXCEPTION_MESSAGE_PARAMETER_CANNOT_BE_NULL_OR_WHITESPACE, diff --git a/Source/UniversalAnalyticsHttpWrapper/UniversalAnalyticsEventFactory.cs b/Source/UniversalAnalyticsHttpWrapper/UniversalAnalyticsEventFactory.cs index 91c0fde..d9d0638 100644 --- a/Source/UniversalAnalyticsHttpWrapper/UniversalAnalyticsEventFactory.cs +++ b/Source/UniversalAnalyticsHttpWrapper/UniversalAnalyticsEventFactory.cs @@ -1,4 +1,7 @@ -namespace UniversalAnalyticsHttpWrapper +using System; +using UniversalAnalyticsHttpWrapper.Objects; + +namespace UniversalAnalyticsHttpWrapper { /// /// Class for making instances of IUniversalAnalyticsEvent objects @@ -30,7 +33,7 @@ internal UniversalAnalyticsEventFactory(IConfigurationManager configurationManag /// This constructor expects an App Setting for 'UniversalAnalytics.TrackingId' /// in the config. UniversalAnalytics.TrackingId must be a Universal Analytics Web Property. /// - /// Required. Anonymous client id for the event. + /// Required if userId is not set. /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#cid for details. /// Required. The event category for the event. /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ec for details. @@ -40,6 +43,8 @@ internal UniversalAnalyticsEventFactory(IConfigurationManager configurationManag /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#el for details. /// Optional. The event value for the event. /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ev for details. + /// /// Required if anonymousClientId is not set. + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#uid for details. /// Thrown when /// one of the required config attributes are missing. /// Thrown when one of the required fields are null or whitespace. @@ -50,17 +55,101 @@ public IUniversalAnalyticsEvent MakeUniversalAnalyticsEvent( string eventCategory, string eventAction, string eventLabel, - string eventValue = null) + string eventValue = null, + string userId = null) { - string trackingId = _configurationManager.GetAppSetting(APP_KEY_UNIVERSAL_ANALYTICS_TRACKING_ID); - return new UniversalAnalyticsEvent( - trackingId, + GetAnalyticsTrackingIdFromConfig(), anonymousClientId, eventCategory, eventAction, eventLabel, + eventValue, + userId); + } + + /// + /// This constructor expects an App Setting for 'UniversalAnalytics.TrackingId' + /// in the config. UniversalAnalytics.TrackingId must be a Universal Analytics Web Property. + /// + /// Required. Anonymous client id for the event. + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#cid for details. + /// Required. The event category for the event. + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ec for details. + /// Required. The event action for the event. + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ea for details. + /// The event label for the event. + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#el for details. + /// Optional. The event value for the event. + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ev for details. + /// Thrown when + /// one of the required config attributes are missing. + /// Thrown when one of the required fields are null or whitespace. + /// Thrown when the HttpRequest that's posted to Google returns something + /// other than a 200 OK response. + public IUniversalAnalyticsEvent MakeUniversalAnalyticsEvent( + ClientId clientId, + string eventCategory, + string eventAction, + string eventLabel, + string eventValue = null) + { + if (clientId == null) + { + throw new ArgumentNullException(nameof(clientId)); + } + + return MakeUniversalAnalyticsEvent( + clientId.Id.ToString(), + eventCategory, + eventAction, + eventLabel, eventValue); } + + /// + /// This constructor expects an App Setting for 'UniversalAnalytics.TrackingId' + /// in the config. UniversalAnalytics.TrackingId must be a Universal Analytics Web Property. + /// + /// Required. The user id for the event. Will create an event with the uid defined. + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#uid for details. + /// Required. The event category for the event. + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ec for details. + /// Required. The event action for the event. + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ea for details. + /// The event label for the event. + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#el for details. + /// Optional. The event value for the event. + /// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ev for details. + /// Thrown when + /// one of the required config attributes are missing. + /// Thrown when one of the required fields are null or whitespace. + /// Thrown when the HttpRequest that's posted to Google returns something + /// other than a 200 OK response. + public IUniversalAnalyticsEvent MakeUniversalAnalyticsEvent( + UserId userId, + string eventCategory, + string eventAction, + string eventLabel, + string eventValue = null) + { + if (userId == null) + { + throw new ArgumentNullException(nameof(userId)); + } + + return MakeUniversalAnalyticsEvent( + null, + eventCategory, + eventAction, + eventLabel, + eventValue, + userId.Id); + } + + private string GetAnalyticsTrackingIdFromConfig() + { + return _configurationManager.GetAppSetting(APP_KEY_UNIVERSAL_ANALYTICS_TRACKING_ID); + } } } diff --git a/Source/UniversalAnalyticsHttpWrapper/UniversalAnalyticsHttpWrapper.csproj b/Source/UniversalAnalyticsHttpWrapper/UniversalAnalyticsHttpWrapper.csproj index c80903f..af26311 100644 --- a/Source/UniversalAnalyticsHttpWrapper/UniversalAnalyticsHttpWrapper.csproj +++ b/Source/UniversalAnalyticsHttpWrapper/UniversalAnalyticsHttpWrapper.csproj @@ -47,6 +47,7 @@ + @@ -59,8 +60,10 @@ + +