diff --git a/Source/Immutable/Private/Immutable/ImmutableAnalytics.cpp b/Source/Immutable/Private/Immutable/ImmutableAnalytics.cpp new file mode 100644 index 0000000..1ca47ce --- /dev/null +++ b/Source/Immutable/Private/Immutable/ImmutableAnalytics.cpp @@ -0,0 +1,71 @@ +#include "ImmutableAnalytics.h" + +#include "JsonDomBuilder.h" + +#define EVENT_DATA_SUCCESS "succeeded" + + +void UImmutableAnalytics::Track(EEventName EventName) +{ + check(JSConnector.IsValid()); + + FString FullData; + + FJsonObjectConverter::UStructToJsonObjectString({ "unrealSdk", GetEventName(EventName), "" }, FullData); + + JSConnector->CallJS(ImmutablePassportAction::TRACK, FullData, FImtblJSResponseDelegate::CreateUObject(this, &UImmutableAnalytics::OnTrackResponse)); +} + +void UImmutableAnalytics::Track(EEventName EventName, bool Success) +{ + TMap EventData = {{EVENT_DATA_SUCCESS, Success ? TEXT("true") : TEXT("false") } }; + Track(EventName, EventData); +} + +void UImmutableAnalytics::Track(EEventName EventName, TMap& EventData) +{ + check(JSConnector.IsValid()); + + FJsonDomBuilder::FObject Object; + + for (auto data : EventData) + { + Object.Set(data.Key, data.Value); + } + + FString FullData; + + FJsonObjectConverter::UStructToJsonObjectString({ "unrealSdk", GetEventName(EventName), Object.ToString() }, FullData); + + JSConnector->CallJS(ImmutablePassportAction::TRACK, FullData, FImtblJSResponseDelegate::CreateUObject(this, &UImmutableAnalytics::OnTrackResponse)); +} + +void UImmutableAnalytics::Setup(TWeakObjectPtr Connector) +{ + IMTBL_LOG_FUNCSIG + + if (!Connector.IsValid()) + { + IMTBL_ERR("Invalid JSConnector passed to UImmutableAnalytics::Setup.") + return; + } + + JSConnector = Connector.Get(); +} + +void UImmutableAnalytics::OnTrackResponse(FImtblJSResponse Response) +{ + // Currently, we ignore tracking errors. Edit if it is required +} + +FString UImmutableAnalytics::GetEventName(EEventName EventName) +{ + switch(EventName) + { + #define CONVERT(EventName, EventNameString) case EEventName::EventName: return EventNameString; + EVENT_NAME_LIST + #undef CONVERT + default: + return ""; + } +} diff --git a/Source/Immutable/Private/Immutable/ImmutableAnalytics.h b/Source/Immutable/Private/Immutable/ImmutableAnalytics.h new file mode 100644 index 0000000..8a45814 --- /dev/null +++ b/Source/Immutable/Private/Immutable/ImmutableAnalytics.h @@ -0,0 +1,78 @@ +#pragma once + +#include "Immutable/ImmutablePassport.h" + +#include "ImmutableAnalytics.generated.h" + +USTRUCT(BlueprintType) +struct FEventData +{ + GENERATED_BODY() + + UPROPERTY() + FString moduleName; + + UPROPERTY() + FString eventName; + + UPROPERTY() + FString properties; +}; + +/** + * Immutable bridge sdk analytics utility + */ +UCLASS() +class IMMUTABLE_API UImmutableAnalytics : public UObject +{ + GENERATED_BODY() + +public: + +/** + * Event names to track + */ +#define EVENT_NAME_LIST \ +CONVERT(INIT_PASSPORT, "initialisedPassport") \ +CONVERT(START_LOGIN, "startedLogin") \ +CONVERT(COMPLETE_LOGIN, "performedLogin") \ +CONVERT(START_LOGIN_PKCE, "startedLoginPkce") \ +CONVERT(COMPLETE_LOGIN_PKCE, "performedLoginPkce") \ +CONVERT(COMPLETE_RELOGIN, "performedRelogin") \ +CONVERT(START_CONNECT_IMX, "startedConnectImx") \ +CONVERT(COMPLETE_CONNECT_IMX, "performedConnectImx") \ +CONVERT(START_CONNECT_IMX_PKCE, "startedConnectImxPkce") \ +CONVERT(COMPLETE_CONNECT_IMX_PKCE, "performedConnectImxPkce") \ +CONVERT(COMPLETE_RECONNECT, "performedReconnect") \ +CONVERT(COMPLETE_LOGOUT, "performedLogout") \ +CONVERT(COMPLETE_LOGOUT_PKCE, "performedLogoutPkce") + + enum class EEventName: uint8 + { + #define CONVERT(name, nameString) name, + EVENT_NAME_LIST + #undef CONVERT + }; + +public: + void Setup(TWeakObjectPtr Connector); + /** + * Performs the call to game bridge track method + * @param EventName Name that will be tracked + * @param Success Single event data record that track "succeeded" field + * @param EventData Map with customed data, converted to json object + */ + void Track(EEventName EventName); + void Track(EEventName EventName, bool Success); + void Track(EEventName EventName, TMap& EventData); + +private: + // Convert enum to string + FString GetEventName(EEventName EventName); + // Callback method for Track from bridge + void OnTrackResponse(FImtblJSResponse Response); + +private: + TWeakObjectPtr JSConnector; + +}; \ No newline at end of file diff --git a/Source/Immutable/Private/Immutable/ImmutablePassport.cpp b/Source/Immutable/Private/Immutable/ImmutablePassport.cpp index e9ffd37..53d3f8e 100644 --- a/Source/Immutable/Private/Immutable/ImmutablePassport.cpp +++ b/Source/Immutable/Private/Immutable/ImmutablePassport.cpp @@ -2,6 +2,7 @@ #include "Immutable/ImmutablePassport.h" +#include "ImmutableAnalytics.h" #include "Immutable/Misc/ImtblLogging.h" #include "Immutable/ImmutableResponses.h" #include "Immutable/ImtblJSConnector.h" @@ -57,6 +58,7 @@ void UImmutablePassport::Connect(bool IsConnectImx, bool TryToRelogin, const FIm } else { + Analytics->Track(IsConnectImx ? UImmutableAnalytics::EEventName::START_CONNECT_IMX : UImmutableAnalytics::EEventName::START_LOGIN); CallJS(ImmutablePassportAction::INIT_DEVICE_FLOW, TEXT(""), ResponseDelegate, FImtblJSResponseDelegate::CreateUObject(this, &UImmutablePassport::OnInitDeviceFlowResponse)); } } @@ -70,6 +72,7 @@ void UImmutablePassport::ConnectPKCE(bool IsConnectImx, const FImtblPassportResp SetStateFlags(IPS_IMX); } PKCEResponseDelegate = ResponseDelegate; + Analytics->Track(IsConnectImx ? UImmutableAnalytics::EEventName::START_CONNECT_IMX_PKCE : UImmutableAnalytics::EEventName::START_LOGIN_PKCE); CallJS(ImmutablePassportAction::GetPKCEAuthUrl, TEXT(""), PKCEResponseDelegate, FImtblJSResponseDelegate::CreateUObject(this, &UImmutablePassport::OnGetPKCEAuthUrlResponse)); } #endif @@ -210,11 +213,15 @@ void UImmutablePassport::Setup(const TWeakObjectPtr Connector if (!Connector.IsValid()) { - IMTBL_ERR("Invalid JSConnector passed to UImmutablePassport::Initialize.") + IMTBL_ERR("Invalid JSConnector passed to UImmutablePassport::Setup.") return; } JSConnector = Connector.Get(); + + // Analytics + Analytics = NewObject(this); + Analytics->Setup(Connector); } void UImmutablePassport::ReinstateConnection(FImtblJSResponse Response) @@ -224,15 +231,19 @@ void UImmutablePassport::ReinstateConnection(FImtblJSResponse Response) if (auto ResponseDelegate = GetResponseDelegate(Response)) { // currently, this response has to be called only for RELOGIN AND RECONNECT bridge routines - const FString CallbackName = (Response.responseFor.Compare(ImmutablePassportAction::RELOGIN, ESearchCase::IgnoreCase) == 0) ? "Relogin" : "Reconnect"; + bool IsRelogin = Response.responseFor.Compare(ImmutablePassportAction::RELOGIN, ESearchCase::IgnoreCase) == 0; + const FString CallbackName = IsRelogin ? "Relogin" : "Reconnect"; + UImmutableAnalytics::EEventName EventName = IsRelogin ? UImmutableAnalytics::EEventName::COMPLETE_RELOGIN : UImmutableAnalytics::EEventName::COMPLETE_RECONNECT; if (Response.JsonObject->GetBoolField(TEXT("result"))) { SetStateFlags(IPS_CONNECTED); ResponseDelegate->ExecuteIfBound(FImmutablePassportResult{true, "", Response}); + Analytics->Track(EventName, true); } else { + Analytics->Track(EventName, false); #if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC if (IsStateFlagsSet(IPS_PKCE)) { @@ -296,7 +307,7 @@ void UImmutablePassport::OnInitializeResponse(FImtblJSResponse Response) IMTBL_ERR("Passport initialization failed.") Response.Error.IsSet() ? Msg = Response.Error->ToString() : Msg = Response.JsonObject->GetStringField(TEXT("error")); } - + Analytics->Track(UImmutableAnalytics::EEventName::INIT_PASSPORT, Response.success); ResponseDelegate->ExecuteIfBound(FImmutablePassportResult{Response.success, Msg, Response}); } } @@ -395,6 +406,7 @@ void UImmutablePassport::OnLogoutResponse(FImtblJSResponse Response) return; } + Analytics->Track(UImmutableAnalytics::EEventName::COMPLETE_LOGOUT); Message = "Logged out"; IMTBL_LOG("%s", *Message) ResponseDelegate->ExecuteIfBound(FImmutablePassportResult{ Response.success, Message }); @@ -498,6 +510,7 @@ void UImmutablePassport::OnConnectPKCEResponse(FImtblJSResponse Response) ResetStateFlags(IPS_PKCE); Response.Error.IsSet() ? Msg = Response.Error->ToString() : Msg = Response.JsonObject->GetStringField(TEXT("error")); } + Analytics->Track(IsStateFlagsSet(IPS_IMX) ? UImmutableAnalytics::EEventName::COMPLETE_CONNECT_IMX_PKCE : UImmutableAnalytics::EEventName::COMPLETE_LOGIN_PKCE, Response.success); PKCEResponseDelegate.ExecuteIfBound(FImmutablePassportResult{Response.success, Msg}); PKCEResponseDelegate = nullptr; @@ -675,6 +688,7 @@ void UImmutablePassport::OnConfirmCodeResponse(FImtblJSResponse Response) { FString Msg; FString TypeOfConnection = IsStateFlagsSet(IPS_IMX) ? TEXT("connect") : TEXT("login"); + UImmutableAnalytics::EEventName EventName = IsStateFlagsSet(IPS_IMX) ? UImmutableAnalytics::EEventName::COMPLETE_CONNECT_IMX : UImmutableAnalytics::EEventName::COMPLETE_LOGIN; ResetStateFlags(IPS_CONNECTING); if (Response.success) @@ -687,6 +701,7 @@ void UImmutablePassport::OnConfirmCodeResponse(FImtblJSResponse Response) IMTBL_LOG("%s code not confirmed.", *TypeOfConnection) Response.Error.IsSet() ? Msg = Response.Error->ToString() : Msg = Response.JsonObject->GetStringField(TEXT("error")); } + Analytics->Track(EventName, Response.success); ResponseDelegate->ExecuteIfBound(FImmutablePassportResult{Response.success, Msg, Response}); } } @@ -850,6 +865,7 @@ void UImmutablePassport::OnDeepLinkActivated(FString DeepLink) { FGraphEventRef GameThreadTask = FFunctionGraphTask::CreateAndDispatchWhenReady([this]() { + Analytics->Track(UImmutableAnalytics::EEventName::COMPLETE_LOGOUT_PKCE); PKCELogoutResponseDelegate.ExecuteIfBound(FImmutablePassportResult{true, "Logged out"}); PKCELogoutResponseDelegate = nullptr; ResetStateFlags(IPS_CONNECTED | IPS_PKCE | IPS_IMX); diff --git a/Source/Immutable/Public/Immutable/ImmutableNames.h b/Source/Immutable/Public/Immutable/ImmutableNames.h index 0edb262..2840d98 100644 --- a/Source/Immutable/Public/Immutable/ImmutableNames.h +++ b/Source/Immutable/Public/Immutable/ImmutableNames.h @@ -32,4 +32,6 @@ namespace ImmutablePassportAction const FString EnvProduction = TEXT("production"); const FString ImxIsRegisteredOffchain = TEXT("isRegisteredOffchain"); const FString ImxRegisterOffchain = TEXT("registerOffchain"); + + const FString TRACK = TEXT("track"); } // namespace ImmutablePassportAction diff --git a/Source/Immutable/Public/Immutable/ImmutablePassport.h b/Source/Immutable/Public/Immutable/ImmutablePassport.h index 6c4c208..0dc4d00 100644 --- a/Source/Immutable/Public/Immutable/ImmutablePassport.h +++ b/Source/Immutable/Public/Immutable/ImmutablePassport.h @@ -254,4 +254,8 @@ class IMMUTABLE_API UImmutablePassport : public UObject }; uint8 StateFlags = IPS_NONE; + + UPROPERTY() + class UImmutableAnalytics* Analytics = nullptr; + }; diff --git a/Source/Immutable/Public/Immutable/ImtblJSConnector.h b/Source/Immutable/Public/Immutable/ImtblJSConnector.h index c941522..bd0a282 100644 --- a/Source/Immutable/Public/Immutable/ImtblJSConnector.h +++ b/Source/Immutable/Public/Immutable/ImtblJSConnector.h @@ -27,6 +27,7 @@ class IMMUTABLE_API UImtblJSConnector : public UObject GENERATED_BODY() friend class UImmutablePassport; + friend class UImmutableAnalytics; public: DECLARE_MULTICAST_DELEGATE(FOnBridgeReadyDelegate);