diff --git a/Analytics/Analytics.conf.in b/Analytics/Analytics.conf.in new file mode 100644 index 0000000000..a37c47a59f --- /dev/null +++ b/Analytics/Analytics.conf.in @@ -0,0 +1,23 @@ +precondition = ["Platform"] +callsign = "org.rdk.Analytics" +autostart = "@PLUGIN_ANALYTICS_AUTOSTART@" +startuporder = "@PLUGIN_ANALYTICS_STARTUPORDER@" + +configuration = JSON() + +configuration.add("deviceosname", "@PLUGIN_ANALYTICS_DEVICE_OS_NAME@") + +if boolean("@PLUGIN_ANALYTICS_SIFT_BACKEND_ENABLED@"): + sift = JSON() + sift.add("commonschema", "@PLUGIN_ANALYTICS_SIFT_COMMON_SCHEMA@") + sift.add("env", "@PLUGIN_ANALYTICS_SIFT_ENV@") + sift.add("productname", "@PLUGIN_ANALYTICS_SIFT_PRODUCT_NAME@") + sift.add("loggername", "@PLUGIN_ANALYTICS_SIFT_LOGGER_NAME@") + sift.add("loggerversion", "@PLUGIN_ANALYTICS_SIFT_LOGGER_VERSION@") + sift.add("maxrandomisationwindowtime", "@PLUGIN_ANALYTICS_SIFT_MAX_RANDOMISATION_WINDOW_TIME@") + sift.add("maxeventsinpost", "@PLUGIN_ANALYTICS_SIFT_MAX_EVENTS_IN_POST@") + sift.add("maxretries", "@PLUGIN_ANALYTICS_SIFT_MAX_RETRIES@") + sift.add("minretryperiod", "@PLUGIN_ANALYTICS_SIFT_MIN_RETRY_PERIOD@") + sift.add("maxretryperiod", "@PLUGIN_ANALYTICS_SIFT_MAX_RETRY_PERIOD@") + sift.add("exponentialperiodicfactor", "@PLUGIN_ANALYTICS_SIFT_EXPONENTIAL_PERIODIC_FACTOR@") + configuration.add("sift", sift) \ No newline at end of file diff --git a/Analytics/Analytics.config b/Analytics/Analytics.config new file mode 100644 index 0000000000..e0c3e45aa7 --- /dev/null +++ b/Analytics/Analytics.config @@ -0,0 +1,31 @@ +set (autostart ${PLUGIN_ANALYTICS_AUTOSTART}) +set (preconditions Platform) +set (callsign "org.rdk.Analytics") + +if(PLUGIN_ANALYTICS_STARTUPORDER) +set (startuporder ${PLUGIN_ANALYTICS_STARTUPORDER}) +endif() + +map() + kv(deviceosversion ${PLUGIN_ANALYTICS_DEVICE_OS_VERSION}) +end() +ans(configuration) + + +if(PLUGIN_ANALYTICS_SIFT_BACKEND_ENABLED) + map() + kv(commonschema ${PLUGIN_ANALYTICS_SIFT_COMMON_SCHEMA}) + kv(env ${PLUGIN_ANALYTICS_SIFT_ENV}) + kv(productname ${PLUGIN_ANALYTICS_SIFT_PRODUCT_NAME}) + kv(loggername ${PLUGIN_ANALYTICS_SIFT_LOGGER_NAME}) + kv(loggerversion ${PLUGIN_ANALYTICS_SIFT_LOGGER_VERSION}) + kv(maxrandomisationwindowtime, ${PLUGIN_ANALYTICS_SIFT_MAX_RANDOMISATION_WINDOW_TIME}) + kv(maxeventsinpost, ${PLUGIN_ANALYTICS_SIFT_MAX_EVENTS_IN_POST}) + kv(maxretries, ${PLUGIN_ANALYTICS_SIFT_MAX_RETRIES}) + kv(minretryperiod, ${PLUGIN_ANALYTICS_SIFT_MIN_RETRY_PERIOD}) + kv(maxretryperiod, ${PLUGIN_ANALYTICS_SIFT_MAX_RETRY_PERIOD}) + kv(exponentialperiodicfactor, ${PLUGIN_ANALYTICS_SIFT_EXPONENTIAL_PERIODIC_FACTOR}) + end() + ans(siftobject) + map_append(${configuration} sift ${siftobject}) +endif() \ No newline at end of file diff --git a/Analytics/Analytics.cpp b/Analytics/Analytics.cpp new file mode 100644 index 0000000000..007241fa63 --- /dev/null +++ b/Analytics/Analytics.cpp @@ -0,0 +1,112 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2020 RDK Management + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Analytics.h" +#include + +#define API_VERSION_NUMBER_MAJOR ANALYTICS_MAJOR_VERSION +#define API_VERSION_NUMBER_MINOR ANALYTICS_MINOR_VERSION +#define API_VERSION_NUMBER_PATCH ANALYTICS_PATCH_VERSION + +namespace WPEFramework { + +namespace { + static Plugin::Metadata metadata( + // Version (Major, Minor, Patch) + API_VERSION_NUMBER_MAJOR, API_VERSION_NUMBER_MINOR, API_VERSION_NUMBER_PATCH, + // Preconditions + {}, + // Terminations + {}, + // Controls + {} + ); +} + +namespace Plugin { + SERVICE_REGISTRATION(Analytics, API_VERSION_NUMBER_MAJOR, API_VERSION_NUMBER_MINOR, API_VERSION_NUMBER_PATCH); + + /* virtual */ const string Analytics::Initialize(PluginHost::IShell* service) + { + ASSERT(service != nullptr); + mService = service; + + ASSERT(mAnalytics == nullptr); + + mAnalytics = service->Root(mConnectionId, 2000, _T("AnalyticsImplementation")); + ASSERT(mAnalytics != nullptr); + + if (mAnalytics != nullptr) { + auto configConnection = mAnalytics->QueryInterface(); + if (configConnection != nullptr) { + configConnection->Configure(service); + configConnection->Release(); + } + RegisterAll(); + } + // On success return empty, to indicate there is no error text. + return ((mAnalytics != nullptr)) + ? EMPTY_STRING + : _T("Could not retrieve the Analytics interface."); + } + + /* virtual */ void Analytics::Deinitialize(PluginHost::IShell* service) + { + TRACE(Trace::Information, (_T("Analytics::Deinitialize"))); + ASSERT(service != nullptr); + + if (mAnalytics != nullptr) { + UnregisterAll(); + + RPC::IRemoteConnection *connection(service->RemoteConnection(mConnectionId)); + + VARIABLE_IS_NOT_USED uint32_t result = mAnalytics->Release(); + mAnalytics = nullptr; + + // It should have been the last reference we are releasing, + // so it should end up in a DESCRUCTION_SUCCEEDED, if not we + // are leaking... + ASSERT(result == Core::ERROR_DESTRUCTION_SUCCEEDED); + + // If this was running in a (container) process... + if (connection != nullptr) + { + // Lets trigger a cleanup sequence for + // out-of-process code. Which will guard + // that unwilling processes, get shot if + // not stopped friendly :~) + connection->Terminate(); + connection->Release(); + } + } + } + + void Analytics::Deactivated(RPC::IRemoteConnection* connection) + { + if (connection->Id() == mConnectionId) { + TRACE(Trace::Information, (_T("Analytics::Deactivated"))); + + ASSERT(mService != nullptr); + + Core::IWorkerPool::Instance().Submit(PluginHost::IShell::Job::Create(mService, PluginHost::IShell::DEACTIVATED, PluginHost::IShell::FAILURE)); + } + } + +} // namespace Plugin +} // namespace WPEFramework \ No newline at end of file diff --git a/Analytics/Analytics.h b/Analytics/Analytics.h new file mode 100644 index 0000000000..4bba57c2d0 --- /dev/null +++ b/Analytics/Analytics.h @@ -0,0 +1,85 @@ +/** +* If not stated otherwise in this file or this component's LICENSE +* file the following copyright and licenses apply: +* +* Copyright 2020 RDK Management +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +**/ + +#pragma once + +#include "Module.h" + +#include + +namespace WPEFramework { + + namespace Plugin { + + + // This is a server for a JSONRPC communication channel. + // For a plugin to be capable to handle JSONRPC, inherit from PluginHost::JSONRPC. + // By inheriting from this class, the plugin realizes the interface PluginHost::IDispatcher. + // This realization of this interface implements, by default, the following methods on this plugin + // - exists + // - register + // - unregister + // Any other methood to be handled by this plugin can be added can be added by using the + // templated methods Register on the PluginHost::JSONRPC class. + // As the registration/unregistration of notifications is realized by the class PluginHost::JSONRPC, + // this class exposes a public method called, Notify(), using this methods, all subscribed clients + // will receive a JSONRPC message as a notification, in case this method is called. + class Analytics : public PluginHost::IPlugin, public PluginHost::JSONRPC { + private: + // We do not allow this plugin to be copied !! + Analytics(const Analytics&) = delete; + Analytics& operator=(const Analytics&) = delete; + + public: + Analytics(): + mConnectionId(0), + mAnalytics(nullptr) + { + RegisterAll(); + } + virtual ~Analytics() + { + UnregisterAll(); + } + virtual const string Initialize(PluginHost::IShell* shell) override; + virtual void Deinitialize(PluginHost::IShell* service) override; + virtual string Information() const override { return {}; } + + BEGIN_INTERFACE_MAP(Analytics) + INTERFACE_ENTRY(PluginHost::IPlugin) + INTERFACE_ENTRY(PluginHost::IDispatcher) + INTERFACE_AGGREGATE(Exchange::IAnalytics, mAnalytics) + END_INTERFACE_MAP + + static const string ANALYTICS_METHOD_SEND_EVENT; + private: + void Deactivated(RPC::IRemoteConnection* connection); + // JSONRPC methods + void RegisterAll(); + void UnregisterAll(); + + uint32_t SendEventWrapper(const JsonObject& parameters, JsonObject& response); + + private: + PluginHost::IShell* mService; + uint32_t mConnectionId; + Exchange::IAnalytics* mAnalytics; + }; + } // namespace Plugin +} // namespace WPEFramework diff --git a/Analytics/Analytics.json b/Analytics/Analytics.json new file mode 100644 index 0000000000..e070ef4666 --- /dev/null +++ b/Analytics/Analytics.json @@ -0,0 +1,104 @@ +{ + "$schema": "https://raw.githubusercontent.com/rdkcentral/rdkservices/main/Tools/json_generator/schemas/interface.schema.json", + "jsonrpc": "2.0", + "info": { + "title": "Analytics API", + "class": "Analytics", + "description": "The `Analytics` plugin allows sending analytics events to dedicated backends." + }, + "common": { + "$ref": "../common/common.json" + }, + "definitions": { + "eventName":{ + "summary": "Name of the event", + "type": "string", + "example": "app_summary" + }, + "eventVersion":{ + "summary": "Version number of event schema", + "type": "string", + "example": "1.0.0" + }, + "eventSource": { + "summary": "Name of the component that originates the event (Durable App ID if an App)", + "type": "string", + "example": "epg" + }, + "eventSourceVersion": { + "summary": "Version number for the component that originates the event", + "type": "string", + "example": "1.0.0" + }, + "cetList": { + "summary": "An array of Capability Exclusion Tags to be included on the report. Each CET will exclude the event from being processed for the specified process, any may result in the event being dropped. May be an array of length zero", + "type": "array", + "items": { + "type": "string", + "example": "cet1" + } + }, + "epochTimestamp":{ + "summary": "Timestamp for the START of this event, epoch time, in ms UTC", + "type": "integer", + "example": 1721906631000 + }, + "uptimeTimestamp":{ + "summary": "Timestamp for the START of this event, uptime of the device, in ms. ONLY to be used when Time quality is not good.", + "type": "integer", + "example": 35000 + }, + "eventPayload":{ + "summary": "The payload of the event", + "type": "object", + "example": { + "key1": "value1", + "key2": "value2" + } + } + }, + "methods": { + "sendEvent":{ + "summary": "Send event", + "params": { + "type":"object", + "properties": { + "eventName":{ + "$ref": "#/definitions/eventName" + }, + "eventVersion":{ + "$ref": "#/definitions/eventVersion" + }, + "eventSource":{ + "$ref": "#/definitions/eventSource" + }, + "eventSourceVersion":{ + "$ref": "#/definitions/eventSourceVersion" + }, + "cetList":{ + "$ref": "#/definitions/cetList" + }, + "epochTimestamp":{ + "$ref": "#/definitions/epochTimestamp" + }, + "uptimeTimestamp":{ + "$ref": "#/definitions/uptimeTimestamp" + }, + "eventPayload":{ + "$ref": "#/definitions/eventPayload" + } + }, + "required": [ + "eventName", + "eventSource", + "eventSourceVersion", + "cetList", + "eventPayload" + ] + }, + "result": { + "$ref": "#/common/result" + } + } + } +} diff --git a/Analytics/AnalyticsJsonRpc.cpp b/Analytics/AnalyticsJsonRpc.cpp new file mode 100644 index 0000000000..2a0a3f5ade --- /dev/null +++ b/Analytics/AnalyticsJsonRpc.cpp @@ -0,0 +1,88 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2020 RDK Management + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Analytics.h" +#include "UtilsJsonRpc.h" + +const string WPEFramework::Plugin::Analytics::ANALYTICS_METHOD_SEND_EVENT = "sendEvent"; + +namespace WPEFramework { + +namespace Plugin { + // Registration + // + + void Analytics::RegisterAll() + { + Register(_T(ANALYTICS_METHOD_SEND_EVENT), &Analytics::SendEventWrapper, this); + } + + void Analytics::UnregisterAll() + { + Unregister(_T(ANALYTICS_METHOD_SEND_EVENT)); + } + + // API implementation + // + + // Method: sendEvent - Send an event to the analytics server + // Return codes: + // - ERROR_NONE: Success + // - ERROR_GENERAL: Failed to send the event + uint32_t Analytics::SendEventWrapper(const JsonObject& parameters, JsonObject& response) + { + LOGINFOMETHOD(); + + uint32_t result = Core::ERROR_NONE; + + returnIfStringParamNotFound(parameters, "eventName"); + returnIfStringParamNotFound(parameters, "eventSource"); + returnIfStringParamNotFound(parameters, "eventSourceVersion"); + returnIfParamNotFound(parameters, "cetList"); + returnIfParamNotFound(parameters, "eventPayload"); + + string eventName = parameters["eventName"].String(); + string eventVersion = (parameters.HasLabel("eventVersion") ? parameters["eventVersion"].String() : ""); + string eventSource = parameters["eventSource"].String(); + string eventSourceVersion = parameters["eventSourceVersion"].String(); + JsonArray cetListJson = parameters["cetList"].Array(); + std::list cetList; + for (int i=0; i::Create(cetList); + uint64_t epochTimestamp = (parameters.HasLabel("epochTimestamp"))? parameters["epochTimestamp"].Number() : 0; + uint64_t uptimeTimestamp = (parameters.HasLabel("uptimeTimestamp"))? parameters["uptimeTimestamp"].Number() : 0; + string eventPayload = parameters["eventPayload"].String(); + + result = mAnalytics->SendEvent(eventName, + eventVersion, + eventSource, + eventSourceVersion, + cetListIterator, + epochTimestamp, + uptimeTimestamp, + eventPayload); + cetListIterator->Release(); + returnResponse(result); + } + +} + +} \ No newline at end of file diff --git a/Analytics/AnalyticsPlugin.json b/Analytics/AnalyticsPlugin.json new file mode 100644 index 0000000000..5e36a8ec32 --- /dev/null +++ b/Analytics/AnalyticsPlugin.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://raw.githubusercontent.com/rdkcentral/rdkservices/main/Tools/json_generator/schemas/plugin.schema.json", + "info": { + "title": "Analytics Plugin", + "callsign": "org.rdk.Analytics", + "locator": "libWPEFrameworkAnalytics.so", + "status": "development", + "description": "The `Analytics` plugin allows to send analytics events to dedicated backends." + }, + "interface": { + "$ref": "Analytics.json#" + } +} diff --git a/Analytics/CHANGELOG.md b/Analytics/CHANGELOG.md new file mode 100644 index 0000000000..e23241b2ce --- /dev/null +++ b/Analytics/CHANGELOG.md @@ -0,0 +1,20 @@ +All notable changes to this RDK Service will be documented in this file. + + Each RDK Service has a CHANGELOG file that contains all changes done so far. When version is updated, add a entry in the CHANGELOG.md at the top with user friendly information on what was changed with the new version. Please don't mention JIRA tickets in CHANGELOG. + + Please Add entry in the CHANGELOG for each version change and indicate the type of change with these labels: + Added for new features. + Changed for changes in existing functionality. + Deprecated for soon-to-be removed features. + Removed for now removed features. + Fixed for any bug fixes. + Security in case of vulnerabilities. + + Changes in CHANGELOG should be updated when commits are added to the main or release branches. There should be one CHANGELOG entry per JIRA Ticket. This is not enforced on sprint branches since there could be multiple changes for the same JIRA ticket during development. + + For more details, refer to versioning section under Main README. + +## [1.0.0] - 2024-07-25 +### Added +- New RDK Service Analytics to handle analytics events and send them to dedicated backends + diff --git a/Analytics/CMakeLists.txt b/Analytics/CMakeLists.txt new file mode 100644 index 0000000000..cc710c98d0 --- /dev/null +++ b/Analytics/CMakeLists.txt @@ -0,0 +1,89 @@ +# If not stated otherwise in this file or this component's license file the +# following copyright and licenses apply: +# +# Copyright 2020 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set(PLUGIN_NAME Analytics) +set(MODULE_NAME ${NAMESPACE}${PLUGIN_NAME}) + +set(VERSION_MAJOR 1) +set(VERSION_MINOR 0) +set(VERSION_PATCH 0) + +add_compile_definitions(ANALYTICS_MAJOR_VERSION=${VERSION_MAJOR}) +add_compile_definitions(ANALYTICS_MINOR_VERSION=${VERSION_MINOR}) +add_compile_definitions(ANALYTICS_PATCH_VERSION=${VERSION_PATCH}) + +set(MODULE_VERSION ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}) + +option(PLUGIN_ANALYTICS_SIFT_BACKEND "Enable Sift backend" OFF) + + +set(PLUGIN_ANALYTICS_STARTUPORDER "" CACHE STRING "To configure startup order of Analytics plugin") +set(PLUGIN_ANALYTICS_AUTOSTART "false" CACHE STRING "Automatically start Analytics plugin") +set(PLUGIN_ANALYTICS_DEVICE_OS_NAME "rdk" CACHE STRING "Device OS name") + +set(PLUGIN_ANALYTICS_SIFT_BACKEND_ENABLED ${PLUGIN_ANALYTICS_SIFT_BACKEND} CACHE BOOL "Enable Sift backend configuration") +set(PLUGIN_ANALYTICS_SIFT_COMMON_SCHEMA "entos/common/v1" CACHE STRING "Sift common schema") +set(PLUGIN_ANALYTICS_SIFT_ENV "prod" CACHE STRING "Sift environment") +set(PLUGIN_ANALYTICS_SIFT_PRODUCT_NAME "entos-immerse" CACHE STRING "Sift product name") +set(PLUGIN_ANALYTICS_SIFT_LOGGER_NAME "Analytics" CACHE STRING "Sift logger name") +set(PLUGIN_ANALYTICS_SIFT_LOGGER_VERSION "${MODULE_VERSION}" CACHE STRING "Sift logger version") +set(PLUGIN_ANALYTICS_SIFT_MAX_RANDOMISATION_WINDOW_TIME 300 CACHE STRING "Sift max randomisation window time of posting queued events") +set(PLUGIN_ANALYTICS_SIFT_MAX_EVENTS_IN_POST 10 CACHE STRING "Sift max events in post") +set(PLUGIN_ANALYTICS_SIFT_MAX_RETRIES 10 CACHE STRING "Sift max retries posting events") +set(PLUGIN_ANALYTICS_SIFT_MIN_RETRY_PERIOD 1 CACHE STRING "Sift min retry period seconds") +set(PLUGIN_ANALYTICS_SIFT_MAX_RETRY_PERIOD 30 CACHE STRING "Sift max retry period seconds") +set(PLUGIN_ANALYTICS_SIFT_EXPONENTIAL_PERIODIC_FACTOR 2 CACHE STRING "Sift exponential periodic factor") + + +message("Setup ${MODULE_NAME} v${MODULE_VERSION}") + +find_package(${NAMESPACE}Plugins REQUIRED) + +add_library(${MODULE_NAME} SHARED + Analytics.cpp + AnalyticsJsonRpc.cpp + Implementation/AnalyticsImplementation.cpp + Module.cpp) + +target_include_directories(${MODULE_NAME} PRIVATE Implementation) +target_include_directories(${MODULE_NAME} PRIVATE ../helpers) + +add_subdirectory(Implementation/Backend) + +set_target_properties(${MODULE_NAME} PROPERTIES + CXX_STANDARD 11 + CXX_STANDARD_REQUIRED YES) + +target_compile_definitions(${MODULE_NAME} PRIVATE MODULE_NAME=Plugin_${PLUGIN_NAME}) + +find_package(DS) +if (DS_FOUND) + find_package(IARMBus) + add_definitions(-DDS_FOUND) + target_include_directories(${MODULE_NAME} PRIVATE ${IARMBUS_INCLUDE_DIRS}) + target_include_directories(${MODULE_NAME} PRIVATE ${DS_INCLUDE_DIRS}) + target_include_directories(${MODULE_NAME} PRIVATE ../helpers) + target_link_libraries(${MODULE_NAME} PRIVATE ${NAMESPACE}Plugins::${NAMESPACE}Plugins ${IARMBUS_LIBRARIES} ${OEMHAL_LIBRARIES} + -lpthread -lglib-2.0 -ldbus-1 ${IARMBUS_LIBRARIES} ${MODULE_NAME}Backends) +else (DS_FOUND) + target_link_libraries(${MODULE_NAME} PRIVATE ${NAMESPACE}Plugins::${NAMESPACE}Plugins) +endif(DS_FOUND) + +install(TARGETS ${MODULE_NAME} + DESTINATION lib/${STORAGE_DIRECTORY}/plugins) + +write_config(${PLUGIN_NAME}) diff --git a/Analytics/Implementation/AnalyticsImplementation.cpp b/Analytics/Implementation/AnalyticsImplementation.cpp new file mode 100644 index 0000000000..ac137c5bd1 --- /dev/null +++ b/Analytics/Implementation/AnalyticsImplementation.cpp @@ -0,0 +1,248 @@ +#include "AnalyticsImplementation.h" +#include "Backend/AnalyticsBackend.h" +#include "UtilsLogging.h" + +#include +#include +#include + +namespace WPEFramework { +namespace Plugin { + + const uint32_t POPULATE_DEVICE_INFO_RETRY_MS = 3000; + + SERVICE_REGISTRATION(AnalyticsImplementation, 1, 0); + + AnalyticsImplementation::AnalyticsImplementation(): + mQueueMutex(), + mQueueCondition(), + mBackends(IAnalyticsBackendAdministrator::Instances()) + { + mThread = std::thread(&AnalyticsImplementation::ActionLoop, this); + mThread.detach(); + } + + AnalyticsImplementation::~AnalyticsImplementation() + { + mQueueMutex.lock(); + mActionQueue.push({ACTION_TYPE_SHUTDOWN, nullptr}); + mQueueMutex.unlock(); + mQueueCondition.notify_one(); + mThread.join(); + } + + /* virtual */ uint32_t AnalyticsImplementation::SendEvent(const string& eventName, + const string& eventVersion, + const string& eventSource, + const string& eventSourceVersion, + RPC::IStringIterator* const& cetList, + const uint64_t& epochTimestamp, + const uint64_t& uptimeTimestamp, + const string& eventPayload) + { + std::shared_ptr event = std::make_shared(); + event->eventName = eventName; + event->eventVersion = eventVersion; + event->eventSource = eventSource; + event->eventSourceVersion = eventSourceVersion; + + LOGINFO("Event Name: %s", eventName.c_str()); + LOGINFO("Event Version: %s", eventVersion.c_str()); + LOGINFO("Event Source: %s", eventSource.c_str()); + LOGINFO("Event Source Version: %s", eventSourceVersion.c_str()); + LOGINFO("cetList[]: "); + std::string entry; + while (cetList->Next(entry) == true) { + event->cetList.push_back(entry); + LOGINFO(" %s ", entry.c_str()); + } + event->epochTimestamp = epochTimestamp; + event->uptimeTimestamp = uptimeTimestamp; + event->eventPayload = eventPayload; + + LOGINFO("Epoch Timestamp: %" PRIu64, epochTimestamp); + LOGINFO("Uptime Timestamp: %" PRIu64, uptimeTimestamp); + LOGINFO("Event Payload: %s", eventPayload.c_str()); + + // Fill the uptime if no time provided + if (event->epochTimestamp == 0 && event->uptimeTimestamp == 0) + { + event->uptimeTimestamp = GetCurrentUptimeInMs(); + } + + std::unique_lock lock(mQueueMutex); + mActionQueue.push({ACTION_TYPE_SEND_EVENT, event}); + lock.unlock(); + mQueueCondition.notify_one(); + return Core::ERROR_NONE; + } + + uint32_t AnalyticsImplementation::Configure(PluginHost::IShell* shell) + { + LOGINFO("Configuring Analytics"); + uint32_t result = Core::ERROR_NONE; + ASSERT(shell != nullptr); + mShell = shell; + + for (auto &backend : mBackends) + { + LOGINFO("Configuring backend: %s", backend.first.c_str()); + backend.second.Configure(shell); + } + + return result; + } + + void AnalyticsImplementation::ActionLoop() + { + std::unique_lock lock(mQueueMutex); + + while (true) { + + std::chrono::milliseconds queueTimeout(std::chrono::milliseconds::max()); + + if (!mSysTimeValid) + { + queueTimeout = std::chrono::milliseconds(POPULATE_DEVICE_INFO_RETRY_MS); + } + + if (queueTimeout == std::chrono::milliseconds::max()) + { + mQueueCondition.wait(lock, [this] { return !mActionQueue.empty(); }); + } + else + { + mQueueCondition.wait_for(lock, queueTimeout, [this] { return !mActionQueue.empty(); }); + } + + Action action = {ACTION_TYPE_UNDEF, nullptr}; + + if (mActionQueue.empty() && !mSysTimeValid) + { + action = {ACTION_POPULATE_DEVICE_INFO, nullptr}; + } + else + { + action = mActionQueue.front(); + mActionQueue.pop(); + } + + lock.unlock(); + + switch (action.type) { + case ACTION_POPULATE_DEVICE_INFO: + + mSysTimeValid = IsSysTimeValid(); + + if ( mSysTimeValid ) + { + // Send the events from the queue, if there are any. + while ( !mEventQueue.empty() ) + { + SendEventToBackend( mEventQueue.front() ); + mEventQueue.pop(); + } + } + break; + case ACTION_TYPE_SEND_EVENT: + + if (mSysTimeValid) + { + // Add epoch timestamp if needed + // It should have at least uptime already + if (action.payload->epochTimestamp == 0) + { + action.payload->epochTimestamp = ConvertUptimeToTimestampInMs(action.payload->uptimeTimestamp); + } + + SendEventToBackend(*action.payload); + } + else + { + // pass to backend if epoch available + if (action.payload->epochTimestamp != 0) + { + SendEventToBackend(*action.payload); + } + else + { + // Store the event in the queue + mEventQueue.push(*action.payload); + } + } + break; + case ACTION_TYPE_SHUTDOWN: + return; + default: + break; + } + + lock.lock(); + } + } + + bool AnalyticsImplementation::IsSysTimeValid() + { + bool ret = false; + //TODO: Check here if time is OK + // for now check if /tmp/as_timezone_ready exists + std::ifstream timezoneFile("/tmp/as_timezone_ready"); + if (timezoneFile.is_open()) + { + timezoneFile.close(); + ret = true; + } + + return ret; + } + + void AnalyticsImplementation::SendEventToBackend(const AnalyticsImplementation::Event& event) + { + //TODO: Add mapping of event source/name to backend + IAnalyticsBackend::Event backendEvent; + backendEvent.eventName = event.eventName; + backendEvent.eventVersion = event.eventVersion; + backendEvent.eventSource = event.eventSource; + backendEvent.eventSourceVersion = event.eventSourceVersion; + backendEvent.epochTimestamp = event.epochTimestamp; + backendEvent.eventPayload = event.eventPayload; + backendEvent.cetList = event.cetList; + + if (mBackends.empty()) + { + LOGINFO("No backends available!"); + } + else if (mBackends.find(IAnalyticsBackend::SIFT) != mBackends.end()) + { + LOGINFO("Sending event to Sift backend"); + mBackends.at(IAnalyticsBackend::SIFT).SendEvent(backendEvent); + } + } + + uint64_t AnalyticsImplementation::GetCurrentTimestampInMs() + { + return std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch() ).count(); + } + + uint64_t AnalyticsImplementation::GetCurrentUptimeInMs() + { + struct sysinfo info = {0}; + sysinfo(&info); + return info.uptime * 1000; + } + + uint64_t AnalyticsImplementation::ConvertUptimeToTimestampInMs(uint64_t uptimeMs) + { + uint64_t uptimeDiff = 0; + uint64_t currentUptime = GetCurrentUptimeInMs(); + uint64_t currentTimestamp = GetCurrentTimestampInMs(); + if (currentUptime > uptimeMs) + { + uptimeDiff = currentUptime - uptimeMs; + } + + return currentTimestamp - uptimeDiff; + } + +} +} \ No newline at end of file diff --git a/Analytics/Implementation/AnalyticsImplementation.h b/Analytics/Implementation/AnalyticsImplementation.h new file mode 100644 index 0000000000..affb4d417f --- /dev/null +++ b/Analytics/Implementation/AnalyticsImplementation.h @@ -0,0 +1,89 @@ +#pragma once + +#include "../Module.h" +#include +#include +#include "Backend/AnalyticsBackend.h" + +#include +#include +#include +#include + +namespace WPEFramework { +namespace Plugin { + class AnalyticsImplementation : public Exchange::IAnalytics, public Exchange::IConfiguration { + private: + AnalyticsImplementation(const AnalyticsImplementation&) = delete; + AnalyticsImplementation& operator=(const AnalyticsImplementation&) = delete; + + public: + AnalyticsImplementation(); + ~AnalyticsImplementation(); + + BEGIN_INTERFACE_MAP(AnalyticsImplementation) + INTERFACE_ENTRY(Exchange::IAnalytics) + INTERFACE_ENTRY(Exchange::IConfiguration) + END_INTERFACE_MAP + + private: + + enum ActionType + { + ACTION_TYPE_UNDEF, + ACTION_POPULATE_DEVICE_INFO, + ACTION_TYPE_SEND_EVENT, + ACTION_TYPE_SHUTDOWN + }; + + struct Event + { + std::string eventName; + std::string eventVersion; + std::string eventSource; + std::string eventSourceVersion; + std::list cetList; + uint64_t epochTimestamp; + uint64_t uptimeTimestamp; + std::string eventPayload; + }; + + struct Action + { + ActionType type; + std::shared_ptr payload; + }; + + + // IAnalyticsImplementation interface + uint32_t SendEvent(const string& eventName, + const string& eventVersion, + const string& eventSource, + const string& eventSourceVersion, + RPC::IStringIterator* const& cetList, + const uint64_t& epochTimestamp, + const uint64_t& uptimeTimestamp, + const string& eventPayload) override; + + // IConfiguration interface + uint32_t Configure(PluginHost::IShell* shell); + + void ActionLoop(); + bool IsSysTimeValid(); + void SendEventToBackend(const Event& event); + + static uint64_t GetCurrentTimestampInMs(); + static uint64_t GetCurrentUptimeInMs(); + static uint64_t ConvertUptimeToTimestampInMs(uint64_t uptimeMs); + + std::mutex mQueueMutex; + std::condition_variable mQueueCondition; + std::thread mThread; + std::queue mActionQueue; + std::queue mEventQueue; + IAnalyticsBackends mBackends; + bool mSysTimeValid; + PluginHost::IShell* mShell; + }; +} +} \ No newline at end of file diff --git a/Analytics/Implementation/Backend/AnalyticsBackend.cpp b/Analytics/Implementation/Backend/AnalyticsBackend.cpp new file mode 100644 index 0000000000..75d6dc2da4 --- /dev/null +++ b/Analytics/Implementation/Backend/AnalyticsBackend.cpp @@ -0,0 +1,25 @@ +#include "Module.h" +#include "AnalyticsBackend.h" + +#ifdef ANALYTICS_SIFT_BACKEND +#include "SiftBackend.h" +#endif + +namespace WPEFramework { +namespace Plugin { + +const std::string IAnalyticsBackend::SIFT = "Sift"; + +IAnalyticsBackends& IAnalyticsBackendAdministrator::Instances() +{ + static SiftBackend siftBackend; + static IAnalyticsBackends backendInstances = { +#ifdef ANALYTICS_SIFT_BACKEND + {IAnalyticsBackend::SIFT, siftBackend}, +#endif + }; + return (backendInstances); +} + +} +} \ No newline at end of file diff --git a/Analytics/Implementation/Backend/AnalyticsBackend.h b/Analytics/Implementation/Backend/AnalyticsBackend.h new file mode 100644 index 0000000000..2dd2953f91 --- /dev/null +++ b/Analytics/Implementation/Backend/AnalyticsBackend.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include "../../Module.h" + +// Interface for Analytics Backedn +namespace WPEFramework { +namespace Plugin { + + struct IAnalyticsBackend { + virtual ~IAnalyticsBackend() = default; + + struct Event + { + std::string eventName; + std::string eventVersion; + std::string eventSource; + std::string eventSourceVersion; + std::list cetList; + uint64_t epochTimestamp; + std::string eventPayload; + }; + + const static std::string SIFT; + + virtual uint32_t Configure(PluginHost::IShell* shell) = 0; + virtual uint32_t SendEvent(const Event& event) = 0; + }; + + typedef std::map IAnalyticsBackends; + + struct IAnalyticsBackendAdministrator { + static IAnalyticsBackends& Instances(); + + virtual ~IAnalyticsBackendAdministrator() = default; + }; + +} +} diff --git a/Analytics/Implementation/Backend/CMakeLists.txt b/Analytics/Implementation/Backend/CMakeLists.txt new file mode 100644 index 0000000000..a2af1afa82 --- /dev/null +++ b/Analytics/Implementation/Backend/CMakeLists.txt @@ -0,0 +1,17 @@ + +set(TARGET_LIB ${NAMESPACE}${PLUGIN_NAME}Backends) + +add_library(${TARGET_LIB} STATIC) + +if (PLUGIN_ANALYTICS_SIFT_BACKEND) + add_subdirectory(Sift) + target_link_libraries(${TARGET_LIB} PRIVATE ${NAMESPACE}${PLUGIN_NAME}SiftBackend) + target_compile_definitions(${TARGET_LIB} PRIVATE ANALYTICS_SIFT_BACKEND=1) +endif() + +target_sources(${TARGET_LIB} PRIVATE AnalyticsBackend.cpp) +target_include_directories(${TARGET_LIB} PUBLIC "${CMAKE_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}") +set_property(TARGET ${TARGET_LIB} PROPERTY POSITION_INDEPENDENT_CODE ON) +set_target_properties(${TARGET_LIB} PROPERTIES CXX_STANDARD 11 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF) +target_link_libraries(${TARGET_LIB} PRIVATE ${NAMESPACE}Plugins::${NAMESPACE}Plugins) + diff --git a/Analytics/Implementation/Backend/Sift/CMakeLists.txt b/Analytics/Implementation/Backend/Sift/CMakeLists.txt new file mode 100644 index 0000000000..7a1e39b6e5 --- /dev/null +++ b/Analytics/Implementation/Backend/Sift/CMakeLists.txt @@ -0,0 +1,19 @@ +set(TARGET_LIB ${NAMESPACE}${PLUGIN_NAME}SiftBackend) + +add_library(${TARGET_LIB} STATIC) + +target_sources(${TARGET_LIB} PRIVATE SiftBackend.cpp SiftConfig.cpp) + +find_package(CURL) +if (CURL_FOUND) + include_directories(${CURL_INCLUDE_DIRS}) + target_link_libraries(${TARGET_LIB} PRIVATE ${CURL_LIBRARIES}) +else (CURL_FOUND) + message ("Curl/libcurl required.") +endif (CURL_FOUND) + +target_include_directories(${TARGET_LIB} PUBLIC "${CMAKE_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}") +target_include_directories(${TARGET_LIB} PRIVATE ../../../../helpers) +set_property(TARGET ${TARGET_LIB} PROPERTY POSITION_INDEPENDENT_CODE ON) +set_target_properties(${TARGET_LIB} PROPERTIES CXX_STANDARD 11 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF) +target_link_libraries(${TARGET_LIB} PRIVATE ${NAMESPACE}Plugins::${NAMESPACE}Plugins) \ No newline at end of file diff --git a/Analytics/Implementation/Backend/Sift/SiftBackend.cpp b/Analytics/Implementation/Backend/Sift/SiftBackend.cpp new file mode 100644 index 0000000000..e63a4b29d7 --- /dev/null +++ b/Analytics/Implementation/Backend/Sift/SiftBackend.cpp @@ -0,0 +1,361 @@ +#include "SiftBackend.h" +#include "UtilsLogging.h" + +#include +#include +#include +#include +#include + +#include + +namespace WPEFramework +{ + namespace Plugin + { + + const uint32_t POPULATE_CONFIG_TIMEOUT_MS = 3000; + + SiftBackend::SiftBackend() + : mQueueMutex() + , mQueueCondition() + , mActionQueue() + , mShell(nullptr) + , mConfigPtr(nullptr) + + { + mThread = std::thread(&SiftBackend::ActionLoop, this); + mThread.detach(); + } + + SiftBackend::~SiftBackend() + { + Action action = {ACTION_TYPE_SHUTDOWN, nullptr}; + { + std::lock_guard lock(mQueueMutex); + mActionQueue.push(action); + } + mQueueCondition.notify_one(); + } + + /* virtual */ uint32_t SiftBackend::Configure(PluginHost::IShell *shell) + { + ASSERT(shell != nullptr); + std::unique_lock lock(mQueueMutex); + mShell = shell; + mConfigPtr = std::unique_ptr(new SiftConfig(shell)); + return Core::ERROR_NONE; + } + + /* virtual */ uint32_t SiftBackend::SendEvent(const Event &event) + { + Action action; + action.type = ACTION_TYPE_SEND_EVENT; + action.payload = std::make_shared(event); + std::unique_lock lock(mQueueMutex); + mActionQueue.push(action); + lock.unlock(); + mQueueCondition.notify_one(); + return Core::ERROR_NONE; + } + + void SiftBackend::ActionLoop() + { + std::unique_lock lock(mQueueMutex); + bool configValid = false; + while (true) + { + std::chrono::milliseconds queueTimeout(std::chrono::milliseconds::max()); + SiftConfig::Config config; + + if (!configValid) + { + queueTimeout = std::chrono::milliseconds(POPULATE_CONFIG_TIMEOUT_MS); + } + + if (queueTimeout == std::chrono::milliseconds::max()) + { + mQueueCondition.wait(lock, [this] { return !mActionQueue.empty(); }); + } + else + { + mQueueCondition.wait_for(lock, queueTimeout, [this] { return !mActionQueue.empty(); }); + } + + Action action = {ACTION_TYPE_UNDEF, nullptr}; + + if (mActionQueue.empty()) + { + action = {ACTION_TYPE_POPULATE_CONFIG, nullptr}; + } + else + { + action = mActionQueue.front(); + mActionQueue.pop(); + } + + //Always get the most recent config + if (mConfigPtr != nullptr && mConfigPtr->Get(config)) + { + configValid = true; + } + else + { + configValid = false; + } + lock.unlock(); + + LOGINFO("Action %d", action.type); + + switch (action.type) + { + case ACTION_TYPE_POPULATE_CONFIG: + if (configValid) + { + // Try to send the events from the queue + while (!mEventQueue.empty() && SendEventInternal(mEventQueue.front(), config)) + { + mEventQueue.pop(); + } + + if (!mEventQueue.empty()) + { + LOGERR("Failed to send all events from queue"); + configValid = false; + } + } + break; + case ACTION_TYPE_SEND_EVENT: + if (configValid) + { + SendEventInternal(*action.payload, config); + } + else + { + mEventQueue.push(*action.payload); + } + break; + case ACTION_TYPE_SHUTDOWN: + return; + default: + break; + } + + lock.lock(); + } + } + + bool SiftBackend::SendEventInternal(const Event &event, const SiftConfig::Config &config) + { + // Sift 2.0 schema + JsonObject eventJson = JsonObject(); + //TODO: Sift does not accept: eventJson["common_schema"] = config.commonSchema; + if (!config.env.empty()) + { + eventJson["env"] = config.env; + } + eventJson["product_name"] = config.productName; + eventJson["product_version"] = config.productVersion; + eventJson["event_schema"] = config.productName + "/" + event.eventName + "/" + event.eventVersion; + eventJson["event_name"] = event.eventName; + eventJson["timestamp"] = event.epochTimestamp; + eventJson["event_id"] = GenerateRandomUUID(); + eventJson["event_source"] = event.eventSource; + eventJson["event_source_version"] = event.eventSourceVersion; + if (!event.cetList.empty()) + { + JsonArray cetList = JsonArray(); + for (const std::string &cet : event.cetList) + { + cetList.Add(cet); + } + eventJson["cet_list"] = cetList; + } + eventJson["logger_name"] = config.loggerName; + eventJson["logger_version"] = config.loggerVersion; + eventJson["partner_id"] = config.partnerId; + if (config.activated) + { + eventJson["xbo_account_id"] = config.xboAccountId; + eventJson["xbo_device_id"] = config.xboDeviceId; + eventJson["activated"] = config.activated; + } + eventJson["device_model"] = config.deviceModel; + eventJson["device_type"] = config.deviceType; + eventJson["device_timezone"] = std::stoi(config.deviceTimeZone); + eventJson["device_os_name"] = config.deviceOsName; + eventJson["device_os_version"] = config.deviceOsVersion; + eventJson["platform"] = config.platform; + eventJson["device_manufacturer"] = config.deviceManufacturer; + eventJson["authenticated"] = config.authenticated; + eventJson["session_id"] = config.sessionId; + eventJson["proposition"] = config.proposition; + if (!config.retailer.empty()) + { + eventJson["retailer"] = config.retailer; + } + if (!config.jvAgent.empty()) + { + eventJson["jv_agent"] = config.jvAgent; + } + if (!config.coam.empty()) + { + eventJson["coam"] = JsonValue(config.coam == "true"); + } + eventJson["device_serial_number"] = config.deviceSerialNumber; + if (!config.deviceFriendlyName.empty()) + { + eventJson["device_friendly_name"] = config.deviceFriendlyName; + } + if (!config.deviceMacAddress.empty()) + { + eventJson["device_mac_address"] = config.deviceMacAddress; + } + if (!config.country.empty()) + { + eventJson["country"] = config.country; + } + if (!config.region.empty()) + { + eventJson["region"] = config.region; + } + if (!config.accountType.empty()) + { + eventJson["account_type"] = config.accountType; + } + if (!config.accountOperator.empty()) + { + eventJson["operator"] = config.accountOperator; + } + if (!config.accountDetailType.empty()) + { + eventJson["account_detail_type"] = config.accountDetailType; + } + + eventJson["event_payload"] = JsonObject(event.eventPayload); + + // TODO: push to persistent queue instead of sending array + JsonArray eventArray = JsonArray(); + eventArray.Add(eventJson); + + std::string json; + eventArray.ToString(json); + LOGINFO("Sending event: %s", json.c_str()); + + // Upload the event to Sift + uint32_t httpCode = PostJson(config.url, config.apiKey, json); + if (httpCode == 400) + { + LOGINFO("Backend refused data, skipping: %s, HTTP Code: %d", event.eventName.c_str(), httpCode); + return true; + } + if (httpCode == 200) + { + LOGINFO("Backend accepted data: %s, HTTP Code: %d", event.eventName.c_str(), httpCode); + return true; + } + LOGERR("Backend failed to accept data: %s, HTTP COde: %d", event.eventName.c_str(), httpCode); + return false; + } + + uint8_t SiftBackend::GenerateRandomCharacter() + { + static std::random_device randomDevice; + static std::mt19937 randomNumberGenerator(randomDevice()); + static std::uniform_int_distribution<> distribution(0, 255); + + return static_cast(distribution(randomNumberGenerator)); + } + + std::string SiftBackend::GenerateRandomOctetString(uint32_t numOctets) + { + std::stringstream ss; + for (uint32_t index = 0; index < numOctets; index++) + { + ss << std::hex << std::setfill('0') << std::setw(2) << static_cast(GenerateRandomCharacter()); + } + + return ss.str(); + } + + std::string SiftBackend::GenerateRandomUUID() + { + std::stringstream randomUUIDStream; + + // The UUID format is xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx (8-4-4-4-12) where each x is a hexadecimal digit. + // M indicates the version of the UUID (4 in this case) and the 2 most significant bits of N should be 10 (binary) indicating + // that it is variant-1 (RFC 4122). + + randomUUIDStream << GenerateRandomOctetString(4); + randomUUIDStream << ("-" + GenerateRandomOctetString(2)); + randomUUIDStream << std::hex << "-" << static_cast(0x40 + (GenerateRandomCharacter() & 0x0Fu)) << GenerateRandomOctetString(1); + randomUUIDStream << std::hex << "-" << static_cast(0x80 + (GenerateRandomCharacter() & 0x3Fu)) << GenerateRandomOctetString(1); + randomUUIDStream << ("-" + GenerateRandomOctetString(6)); + + return randomUUIDStream.str(); + } + + static size_t CurlWriteCallback(void* contents, size_t size, size_t nmemb, void* userp) { + ((std::string*)userp)->append((char*)contents, size * nmemb); + return size * nmemb; + } + + uint32_t SiftBackend::PostJson(const std::string &url, const std::string &apiKey, const std::string &json) + { + CURL *curl; + CURLcode res; + uint32_t retHttpCode = 0; + std::string response; + + if (url.empty() || apiKey.empty() || json.empty()) + { + LOGERR("Invalid parameters for postJson"); + return retHttpCode; + } + + curl = curl_easy_init(); + if (!curl) + { + LOGERR("Failed to initialize curl"); + return retHttpCode; + } + + curl_easy_setopt(curl, CURLOPT_POST, 1L); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json.data()); + + // Create a linked list of custom headers + struct curl_slist *headers = NULL; + std::string keyHeader("X-Api-Key: " + apiKey); + headers = curl_slist_append(headers, "Content-Type: application/json"); + headers = curl_slist_append(headers, keyHeader.data()); + + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + + // Perform the request, res will get the return code + res = curl_easy_perform(curl); + + // Check for errors + if (res != CURLE_OK) + { + LOGERR("curl_easy_perform() failed: %s", curl_easy_strerror(res)); + } + else + { + LOGINFO("Response: %s", response.c_str()); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &retHttpCode); + } + + // Clean up the header list + curl_slist_free_all(headers); + + // Clean up curl session + curl_easy_cleanup(curl); + + return retHttpCode; + } + } +} \ No newline at end of file diff --git a/Analytics/Implementation/Backend/Sift/SiftBackend.h b/Analytics/Implementation/Backend/Sift/SiftBackend.h new file mode 100644 index 0000000000..41a1bf4519 --- /dev/null +++ b/Analytics/Implementation/Backend/Sift/SiftBackend.h @@ -0,0 +1,68 @@ +#pragma once + +#include "../AnalyticsBackend.h" +#include "SiftConfig.h" + +#include +#include +#include +#include + +namespace WPEFramework { +namespace Plugin { + + class SiftBackend: public IAnalyticsBackend { + private: + SiftBackend(const SiftBackend&) = delete; + SiftBackend& operator=(const SiftBackend&) = delete; + + public: + SiftBackend(); + ~SiftBackend(); + uint32_t SendEvent(const Event& event) override; + uint32_t Configure(PluginHost::IShell* shell) override; + + private: + + struct Config + { + std::string url; + std::string apiKey; + }; + + enum ActionType + { + ACTION_TYPE_UNDEF, + ACTION_TYPE_DEVICE_INFO_SET, + ACTION_TYPE_POPULATE_CONFIG, + ACTION_TYPE_SEND_EVENT, + ACTION_TYPE_SHUTDOWN + }; + + struct Action + { + ActionType type; + std::shared_ptr payload; + }; + + void ActionLoop(); + bool SendEventInternal(const Event& event, const SiftConfig::Config &config); + + static uint8_t GenerateRandomCharacter(); + static std::string GenerateRandomOctetString( uint32_t numOctets ); + static std::string GenerateRandomUUID(); + static uint32_t PostJson(const std::string& url, const std::string& apiKey, const std::string& json); + + std::mutex mQueueMutex; + std::condition_variable mQueueCondition; + std::thread mThread; + std::queue mActionQueue; + std::queue mEventQueue; + + PluginHost::IShell* mShell; + SiftConfigPtr mConfigPtr; + }; + +} +} + \ No newline at end of file diff --git a/Analytics/Implementation/Backend/Sift/SiftConfig.cpp b/Analytics/Implementation/Backend/Sift/SiftConfig.cpp new file mode 100644 index 0000000000..8538fcf351 --- /dev/null +++ b/Analytics/Implementation/Backend/Sift/SiftConfig.cpp @@ -0,0 +1,712 @@ +#include "SiftConfig.h" +#include "UtilsLogging.h" + +#include +#include + +#define AUTHSERVICE_CALLSIGN "org.rdk.AuthService" +#define SYSTEM_CALLSIGN "org.rdk.System" +#define PERSISTENT_STORE_CALLSIGN "org.rdk.PersistentStore" +#define PERSISTENT_STORE_ANALYTICS_NAMESPACE "Analytics" +#define PERSISTENT_STORE_ACCOUNT_PROFILE_NAMESPACE "accountProfile" +#define JSONRPC_THUNDER_TIMEOUT 20000 + +#include + +namespace WPEFramework +{ + namespace Plugin + { + static std::string sThunderSecurityToken; + + class AnalyticsConfig : public Core::JSON::Container { + private: + AnalyticsConfig(const AnalyticsConfig&) = delete; + AnalyticsConfig& operator=(const AnalyticsConfig&) = delete; + + public: + class SiftConfig : public Core::JSON::Container { + public: + SiftConfig(const SiftConfig&) = delete; + SiftConfig& operator=(const SiftConfig&) = delete; + + SiftConfig() + : Core::JSON::Container() + , CommonSchema() + , Env() + , ProductName() + , LoggerName() + , LoggerVersion() + { + Add(_T("commonschema"), &CommonSchema); + Add(_T("env"), &Env); + Add(_T("productname"), &ProductName); + Add(_T("loggername"), &LoggerName); + Add(_T("loggerversion"), &LoggerVersion); + } + ~SiftConfig() = default; + + public: + Core::JSON::String CommonSchema; + Core::JSON::String Env; + Core::JSON::String ProductName; + Core::JSON::String LoggerName; + Core::JSON::String LoggerVersion; + }; + + + public: + AnalyticsConfig() + : Core::JSON::Container() + , DeviceOsName() + , Sift() + { + Add(_T("deviceosname"), &DeviceOsName); + Add(_T("sift"), &Sift); + } + ~AnalyticsConfig() + { + } + + public: + Core::JSON::String DeviceOsName; + SiftConfig Sift; + }; + + struct JSONRPCDirectLink + { + private: + uint32_t mId{0}; + std::string mCallSign{}; +#if ((THUNDER_VERSION >= 4) && (THUNDER_VERSION_MINOR == 4)) + PluginHost::ILocalDispatcher *mDispatcher{nullptr}; +#else + PluginHost::IDispatcher *mDispatcher{nullptr}; +#endif + + Core::ProxyType Message() const + { + return (Core::ProxyType(PluginHost::IFactories::Instance().JSONRPC())); + } + + template + bool ToMessage(PARAMETERS ¶meters, Core::ProxyType &message) const + { + return ToMessage((Core::JSON::IElement *)(¶meters), message); + } + bool ToMessage(Core::JSON::IElement *parameters, Core::ProxyType &message) const + { + if (!parameters->IsSet()) + return true; + string values; + if (!parameters->ToString(values)) + { + LOGERR("Failed to convert params to string"); + return false; + } + if (values.empty() != true) + { + message->Parameters = values; + } + return true; + } + template + bool FromMessage(RESPONSE &response, const Core::ProxyType &message, bool isResponseString = false) const + { + return FromMessage((Core::JSON::IElement *)(&response), message, isResponseString); + } + bool FromMessage(Core::JSON::IElement *response, const Core::ProxyType &message, bool isResponseString = false) const + { + Core::OptionalType error; + if (!isResponseString && !response->FromString(message->Result.Value(), error)) + { + LOGERR("Failed to parse response!!! Error: %s", error.Value().Message().c_str()); + return false; + } + return true; + } + + public: + JSONRPCDirectLink(PluginHost::IShell *service, std::string callsign) + : mCallSign(callsign) + { + if (service) +#if ((THUNDER_VERSION >= 4) && (THUNDER_VERSION_MINOR == 4)) + mDispatcher = service->QueryInterfaceByCallsign(mCallSign); +#else + mDispatcher = service->QueryInterfaceByCallsign(mCallSign); +#endif + } + + JSONRPCDirectLink(PluginHost::IShell *service) + : JSONRPCDirectLink(service, "Controller") + { + } + ~JSONRPCDirectLink() + { + if (mDispatcher) + mDispatcher->Release(); + } + + template + uint32_t Get(const uint32_t waitTime, const string &method, PARAMETERS &respObject) + { + JsonObject empty; + return Invoke(waitTime, method, empty, respObject); + } + + template + uint32_t Set(const uint32_t waitTime, const string &method, const PARAMETERS &sendObject) + { + JsonObject empty; + return Invoke(waitTime, method, sendObject, empty); + } + + template + uint32_t Invoke(const uint32_t waitTime, const string &method, const PARAMETERS ¶meters, RESPONSE &response, bool isResponseString = false) + { + if (mDispatcher == nullptr) + { + LOGERR("No JSON RPC dispatcher for %s", mCallSign.c_str()); + return Core::ERROR_GENERAL; + } + + auto message = Message(); + + message->JSONRPC = Core::JSONRPC::Message::DefaultVersion; + message->Id = Core::JSON::DecUInt32(++mId); + message->Designator = Core::JSON::String(mCallSign + ".1." + method); + + ToMessage(parameters, message); + + const uint32_t channelId = ~0; +#if ((THUNDER_VERSION >= 4) && (THUNDER_VERSION_MINOR == 4)) + string output = ""; + uint32_t result = Core::ERROR_BAD_REQUEST; + + if (mDispatcher != nullptr) + { + PluginHost::ILocalDispatcher *localDispatcher = mDispatcher->Local(); + + ASSERT(localDispatcher != nullptr); + + if (localDispatcher != nullptr) + { + result = mDispatcher->Invoke(channelId, message->Id.Value(), sThunderSecurityToken, message->Designator.Value(), message->Parameters.Value(), output); + } + } + + if (message.IsValid() == true) + { + if (result == static_cast(~0)) + { + message.Release(); + } + else if (result == Core::ERROR_NONE) + { + if (output.empty() == true) + { + message->Result.Null(true); + } + else + { + message->Result = output; + } + } + else + { + message->Error.SetError(result); + if (output.empty() == false) + { + message->Error.Text = output; + } + LOGERR("Call failed: %s error: %s", message->Designator.Value().c_str(), message->Error.Text.Value().c_str()); + } + } +#elif (THUNDER_VERSION == 2) + auto resp = mDispatcher->Invoke(sThunderSecurityToken, channelId, *message); +#else + Core::JSONRPC::Context context(channelId, message->Id.Value(), sThunderSecurityToken); + auto resp = mDispatcher->Invoke(context, *message); +#endif + +#if ((THUNDER_VERSION == 2) || (THUNDER_VERSION >= 4) && (THUNDER_VERSION_MINOR == 2)) + + if (resp->Error.IsSet()) + { + LOGERR("Call failed: %s error: %s", message->Designator.Value().c_str(), resp->Error.Text.Value().c_str()); + return resp->Error.Code; + } + + if (!FromMessage(response, resp, isResponseString)) + { + return Core::ERROR_GENERAL; + } + + return Core::ERROR_NONE; +#else + if (!FromMessage(response, message, isResponseString)) + { + return Core::ERROR_GENERAL; + } + + return result; +#endif + } + }; + + SiftConfig::SiftConfig(PluginHost::IShell *shell) : mInitializationThread(), + mMonitorKeys(), + mMutex(), + mConfig(), + mShell(shell) + { + ASSERT(shell != nullptr); + InitializeKeysMap(); + ParsePluginConfig(); + TriggerInitialization(); + } + + SiftConfig::~SiftConfig() + { + mInitializationThread.join(); + // Unregister for notifications + auto interface = mShell->QueryInterfaceByCallsign(PERSISTENT_STORE_CALLSIGN); + if (interface == nullptr) + { + LOGERR("No IStore"); + } + else + { + uint32_t result = interface->Unregister(&mMonitorKeys); + LOGINFO("IStore status %d", result); + interface->Release(); + } + } + + bool SiftConfig::Get(SiftConfig::Config &config) + { + //Read /tmp/sift_session evrytime to get the latest session id + std::ifstream sessionFile("/tmp/sift_session"); + if (sessionFile.is_open()) + { + std::getline(sessionFile, mConfig.sessionId); + sessionFile.close(); + } + + // Get latest values from AuthService + GetAuthServiceValues(); + + mMutex.lock(); + + bool activatedValid = mConfig.activated? + (!mConfig.xboDeviceId.empty() && !mConfig.xboAccountId.empty()) : true; + + //Sift 2.0 required fields + bool valid = ( !mConfig.commonSchema.empty() + && !mConfig.productName.empty() + && !mConfig.productVersion.empty() + && !mConfig.loggerName.empty() + && !mConfig.loggerVersion.empty() + && !mConfig.partnerId.empty() + && activatedValid + && !mConfig.deviceModel.empty() + && !mConfig.deviceType.empty() + && !mConfig.deviceTimeZone.empty() + && !mConfig.deviceOsName.empty() + && !mConfig.deviceOsVersion.empty() + && !mConfig.platform.empty() + && !mConfig.deviceManufacturer.empty() + && !mConfig.sessionId.empty() + && !mConfig.proposition.empty() + && !mConfig.deviceSerialNumber.empty() + && !mConfig.deviceMacAddress.empty() ); + + LOGINFO( " commonSchema: %s," + " productName: %s," + " productVersion: %s," + " loggerName: %s," + " loggerVersion: %s," + " partnerId: %s," + " activatedValid %d," + " deviceModel: %s," + " deviceType: %s," + " deviceTimeZone: %s," + " deviceOsName: %s," + " deviceOsVersion: %s," + " platform: %s," + " deviceManufacturer: %s," + " sessionId: %s," + " proposition: %s," + " deviceSerialNumber: %s," + " deviceMacAddress: %s,", + mConfig.commonSchema.c_str(), + mConfig.productName.c_str(), + mConfig.productVersion.c_str(), + mConfig.loggerName.c_str(), + mConfig.loggerVersion.c_str(), + mConfig.partnerId.c_str(), + activatedValid, + mConfig.deviceModel.c_str(), + mConfig.deviceType.c_str(), + mConfig.deviceTimeZone.c_str(), + mConfig.deviceOsName.c_str(), + mConfig.deviceOsVersion.c_str(), + mConfig.platform.c_str(), + mConfig.deviceManufacturer.c_str(), + mConfig.sessionId.c_str(), + mConfig.proposition.c_str(), + mConfig.deviceSerialNumber.c_str(), + mConfig.deviceMacAddress.c_str()); + + if (valid) + { + if (mConfig.deviceType == "TV") + { + mConfig.deviceType = "IPTV"; + } + else if (mConfig.deviceType == "IPSETTOPBOX") + { + mConfig.deviceType = "IPSTB"; + } + config = mConfig; + } + mMutex.unlock(); + return valid; + } + + void SiftConfig::TriggerInitialization() + { + mInitializationThread = std::thread(&SiftConfig::Initialize, this); + mInitializationThread.detach(); + } + + void SiftConfig::InitializeKeysMap() + { + //Based on SIFT 2.0 properties + //Device info based on SIFT 2.0 properties + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["deviceHardwareModel"] = &mConfig.deviceModel; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["deviceType"] = &mConfig.deviceType; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["devicePlatform"] = &mConfig.platform;//TODO: in ripple equal to 'proposition' + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["modelNumber"] = &mConfig.deviceOsVersion; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["manufacturer"] = &mConfig.deviceManufacturer; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["serialNumber"] = &mConfig.deviceSerialNumber; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["macAddress"] = &mConfig.deviceMacAddress; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["entertainmentOSVersion"] = &mConfig.productVersion; + mKeysMap[PERSISTENT_STORE_ACCOUNT_PROFILE_NAMESPACE]["proposition"] = &mConfig.proposition; + mKeysMap[PERSISTENT_STORE_ACCOUNT_PROFILE_NAMESPACE]["retailer"] = &mConfig.retailer; + mKeysMap[PERSISTENT_STORE_ACCOUNT_PROFILE_NAMESPACE]["jvagent"] = &mConfig.jvAgent; + mKeysMap[PERSISTENT_STORE_ACCOUNT_PROFILE_NAMESPACE]["coam"] = &mConfig.coam; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["country"] = &mConfig.country;//TODO + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["region"] = &mConfig.region;//TODO + mKeysMap[PERSISTENT_STORE_ACCOUNT_PROFILE_NAMESPACE]["accountType"] = &mConfig.accountType; + mKeysMap[PERSISTENT_STORE_ACCOUNT_PROFILE_NAMESPACE]["operator"] = &mConfig.accountOperator; + mKeysMap[PERSISTENT_STORE_ACCOUNT_PROFILE_NAMESPACE]["detailType"] = &mConfig.accountDetailType; + + //TODO: Values provided by AS but should be provided by RDK + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["deviceTimeZone"] = &mConfig.deviceTimeZone; + + + + //TODO: Sift cloud configuration - move to plugin config? (at least url) + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["sift_url"] = &mConfig.url; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["sift_xapikey"] = &mConfig.apiKey; + } + + void SiftConfig::ParsePluginConfig() + { + ASSERT(mShell != nullptr); + std::string configLine = mShell->ConfigLine(); + Core::OptionalType error; + AnalyticsConfig config; + + if (config.FromString(configLine, error) == false) + { + SYSLOG(Logging::ParsingError, + (_T("Failed to parse config line, error: '%s', config line: '%s'."), + (error.IsSet() ? error.Value().Message().c_str() : "Unknown"), + configLine.c_str())); + } + else + { + mConfig.commonSchema = config.Sift.CommonSchema.Value(); + mConfig.env = config.Sift.Env.Value(); + mConfig.productName = "entos-immerse";//config.Sift.ProductName.Value(); + mConfig.loggerName = config.Sift.LoggerName.Value(); + mConfig.loggerVersion = config.Sift.LoggerVersion.Value(); + mConfig.deviceOsName = "rdk";//config.DeviceOsName.Value(); + SYSLOG(Logging::Startup, (_T("Parsed config: '%s', '%s', '%s', '%s', '%s', '%s'."), + mConfig.commonSchema.c_str(), + mConfig.env.c_str(), + mConfig.productName.c_str(), + mConfig.loggerName.c_str(), + mConfig.loggerVersion.c_str(), + mConfig.deviceOsName.c_str() + )); + } + } + + void SiftConfig::Initialize() + { + // Generate jsonrpc token + string token; + // TODO: use interfaces and remove token + auto security = mShell->QueryInterfaceByCallsign("SecurityAgent"); + if (security != nullptr) + { + string payload = "http://localhost"; + if (security->CreateToken( + static_cast(payload.length()), + reinterpret_cast(payload.c_str()), + token) == Core::ERROR_NONE) + { + LOGINFO("Got security token\n"); + sThunderSecurityToken = token; + } + else + { + LOGINFO("Failed to get security token\n"); + } + security->Release(); + } + else + { + LOGINFO("No security agent\n"); + } + + // Set to true if the event is to be SAT authenticated + mMutex.lock(); + mConfig.authenticated = false; + mMutex.unlock(); + + //Activate AuthService plugin if needed + if (IsPluginActivated(mShell, AUTHSERVICE_CALLSIGN) == false) + { + ActivatePlugin(mShell, AUTHSERVICE_CALLSIGN); + } + + GetAuthServiceValues(); + + //Activate System plugin if needed + if (IsPluginActivated(mShell, SYSTEM_CALLSIGN) == false) + { + ActivatePlugin(mShell, SYSTEM_CALLSIGN); + } + + // One time readout attributes + auto systemLink = std::unique_ptr(new JSONRPCDirectLink(mShell, SYSTEM_CALLSIGN)); + if (systemLink != nullptr) + { + JsonObject params; + JsonObject response; + + // Get env from System.1.getDeviceInfo[build_type] + uint32_t result = systemLink->Invoke(JSONRPC_THUNDER_TIMEOUT, "getDeviceInfo", params, response); + if (result == Core::ERROR_NONE && response.HasLabel("build_type")) + { + mMutex.lock(); + mConfig.env = response["build_type"].String(); + std::transform(mConfig.env.begin(), mConfig.env.end(), mConfig.env.begin(), + [](unsigned char c){ return std::tolower(c); }); + mMutex.unlock(); + } + } + + //Activate persistent store plugin if needed + if (IsPluginActivated(mShell, PERSISTENT_STORE_CALLSIGN) == false) + { + ActivatePlugin(mShell, PERSISTENT_STORE_CALLSIGN); + } + + //Prepare callbacks for persistent store keys + for(auto& ns : mKeysMap) + { + for(auto& key : ns.second) + { + mMonitorKeys.RegisterCallback(ns.first, key.first, [this, ns, key](const std::string& value) { + mMutex.lock(); + *(mKeysMap[ns.first][key.first]) = value; + mMutex.unlock(); + }); + } + } + + // Register for notifications from persistent store + auto interface = mShell->QueryInterfaceByCallsign(PERSISTENT_STORE_CALLSIGN); + if (interface == nullptr) + { + LOGERR("No IStore"); + } + else + { + uint32_t result = interface->Register(&mMonitorKeys); + LOGINFO("IStore status %d", result); + interface->Release(); + } + + //Get current values from persistent store + for(auto& ns : mKeysMap) + { + for(auto& key : ns.second) + { + std::string value; + uint32_t result = GetValueFromPersistent(ns.first, key.first, value); + if (result == Core::ERROR_NONE) + { + mMutex.lock(); + *(mKeysMap[ns.first][key.first]) = value; + mMutex.unlock(); + } + } + } + } + + uint32_t SiftConfig::GetValueFromPersistent(const string& ns, const string& key, string& value) + { + uint32_t result; + auto interface = mShell->QueryInterfaceByCallsign(PERSISTENT_STORE_CALLSIGN); + if (interface == nullptr) + { + result = Core::ERROR_UNAVAILABLE; + LOGERR("No IStore"); + } + else + { + result = interface->GetValue(ns, key, value); + LOGINFO("IStore status %d for get %s", result, key.c_str()); + interface->Release(); + } + return result; + } + + void SiftConfig::GetAuthServiceValues() + { + auto authServiceLink = std::unique_ptr(new JSONRPCDirectLink(mShell, AUTHSERVICE_CALLSIGN)); + if (authServiceLink == nullptr) + { + LOGERR("Failed to create JSONRPCDirectLink"); + return; + } + + JsonObject params; + JsonObject response; + + uint32_t result = authServiceLink->Invoke(JSONRPC_THUNDER_TIMEOUT, "getDeviceId", params, response); + if (result == Core::ERROR_NONE && response.HasLabel("partnerId")) + { + mMutex.lock(); + mConfig.partnerId = response["partnerId"].String(); + LOGINFO("Got partnerId %s", mConfig.partnerId.c_str()); + mMutex.unlock(); + } + + // get activation status from AuthService.getActivationStatus + result = authServiceLink->Invoke(JSONRPC_THUNDER_TIMEOUT, "getActivationStatus", params, response); + if (result == Core::ERROR_NONE && response.HasLabel("status") && response["status"].String() == "activated") + { + LOGINFO("Device is activated"); + // get xboAccountId from AuthService.getServiceAccountId + result = authServiceLink->Invoke(JSONRPC_THUNDER_TIMEOUT, "getServiceAccountId", params, response); + if (result == Core::ERROR_NONE && response.HasLabel("serviceAccountId")) + { + mMutex.lock(); + mConfig.xboAccountId = response["serviceAccountId"].String(); + mMutex.unlock(); + LOGINFO("Got xboAccountId %s", mConfig.xboAccountId.c_str()); + } + + // get xboDeviceId from AuthService.getXDeviceId + result = authServiceLink->Invoke(JSONRPC_THUNDER_TIMEOUT, "getXDeviceId", params, response); + if (result == Core::ERROR_NONE && response.HasLabel("xDeviceId")) + { + mMutex.lock(); + mConfig.xboDeviceId = response["xDeviceId"].String(); + mMutex.unlock(); + LOGINFO("Got xboDeviceId %s", mConfig.xboDeviceId.c_str()); + } + + mMutex.lock(); + mConfig.activated = true; + mMutex.unlock(); + } + else + { + mMutex.lock(); + mConfig.activated = false; + mMutex.unlock(); + } + } + + void SiftConfig::ActivatePlugin(PluginHost::IShell *shell, const char *callSign) + { + JsonObject joParams; + joParams.Set("callsign", callSign); + JsonObject joResult; + + if (!IsPluginActivated(shell, callSign)) + { + LOGINFO("Activating %s", callSign); + auto thunderController = std::unique_ptr(new JSONRPCDirectLink(shell)); + if (thunderController != nullptr) + { + uint32_t status = thunderController->Invoke(2000, "activate", joParams, joResult); + string strParams; + string strResult; + joParams.ToString(strParams); + joResult.ToString(strResult); + LOGINFO("Called method %s, with params %s, status: %d, result: %s", "activate", strParams.c_str(), status, strResult.c_str()); + if (status == Core::ERROR_NONE) + { + LOGINFO("%s Plugin activation status ret: %d ", callSign, status); + } + } + else + { + LOGERR("Failed to create JSONRPCDirectLink"); + } + } + } + + bool SiftConfig::IsPluginActivated(PluginHost::IShell *shell, const char *callSign) + { + PluginHost::IShell::state state = PluginHost::IShell::DEACTIVATED; + std::string callsign = PERSISTENT_STORE_CALLSIGN; + auto interface = shell->QueryInterfaceByCallsign(callsign); + if (interface == nullptr) + { + LOGERR("No IShell for %s", callsign.c_str()); + } + else + { + state = interface->State(); + LOGINFO("IShell state %d for callsing %s", state, callsign.c_str()); + interface->Release(); + } + + return state == PluginHost::IShell::ACTIVATED; + } + + void SiftConfig::MonitorKeys::ValueChanged(const string& ns, const string& key, const string& value) + { + auto it = mCallbacks.find(ns); + if (it != mCallbacks.end()) + { + auto it2 = it->second.find(key); + if (it2 != it->second.end()) + { + LOGINFO("ValueChanged %s, %s, %s",ns.c_str(), key.c_str(), value.c_str()); + it2->second(value); + } + } + } + + void SiftConfig::MonitorKeys::StorageExceeded() + { + } + + void SiftConfig::MonitorKeys::RegisterCallback(const string& ns, const string& key, Callback callback) + { + mCallbacks[ns][key] = callback; + } + } +} diff --git a/Analytics/Implementation/Backend/Sift/SiftConfig.h b/Analytics/Implementation/Backend/Sift/SiftConfig.h new file mode 100644 index 0000000000..8029a8ebc7 --- /dev/null +++ b/Analytics/Implementation/Backend/Sift/SiftConfig.h @@ -0,0 +1,109 @@ +#pragma once + +#include "../../../Module.h" +#include +#include +#include + +namespace WPEFramework +{ + namespace Plugin + { + + class SiftConfig + { + public: + struct Config + { + // Sift 2.0 decoration + std::string commonSchema; + std::string env; + std::string productName; + std::string productVersion; //TODO: in persistent or /system/information/entertainmentOSVersion + std::string loggerName; + std::string loggerVersion; + std::string partnerId; // from authservice + std::string xboAccountId; // from authservice ServiceAccountId + std::string xboDeviceId; // from authservice XDeviceId + bool activated; // from authservice IF (authservice getActivationStatus returns activation-state = activated) then TRUE + std::string deviceModel; + std::string deviceType; + std::string deviceTimeZone; + std::string deviceOsName; + std::string deviceOsVersion; + std::string platform; + std::string deviceManufacturer; + bool authenticated; // set to true if the event is to be SAT authenticated + std::string sessionId; // TODO: should be auto generated, but for now we will use sessionId from file + std::string proposition; + std::string retailer; + std::string jvAgent; + std::string coam; + std::string deviceSerialNumber; // /system/information serialNumber + std::string deviceFriendlyName; + std::string deviceMacAddress; // /system/information MACAddress + std::string country; + std::string region; + std::string accountType; + std::string accountOperator; + std::string accountDetailType; + + // TODO: read in SiftUploader + std::string url; + std::string apiKey; + }; + + SiftConfig(const SiftConfig &) = delete; + SiftConfig &operator=(const SiftConfig &) = delete; + + SiftConfig(PluginHost::IShell *shell); + ~SiftConfig(); + + bool Get(Config& config); + + private: + class MonitorKeys : public Exchange::IStore::INotification { + private: + MonitorKeys(const MonitorKeys&) = delete; + MonitorKeys& operator=(const MonitorKeys&) = delete; + + public: + MonitorKeys() : mCallbacks() {} + ~MonitorKeys() = default; + + typedef std::function Callback; + + void ValueChanged(const string &ns, const string &key, const string &value) override; + void StorageExceeded() override; + + void RegisterCallback(const string &ns, const string &key, Callback callback); + + BEGIN_INTERFACE_MAP(MonitorKeys) + INTERFACE_ENTRY(Exchange::IStore::INotification) + END_INTERFACE_MAP + private: + std::map> mCallbacks; + }; + + private: + void TriggerInitialization(); + void InitializeKeysMap(); + void Initialize(); + void ParsePluginConfig(); + + uint32_t GetValueFromPersistent(const string &ns, const string &key, string &value); + void GetAuthServiceValues(); + static void ActivatePlugin(PluginHost::IShell *shell, const char *callSign); + static bool IsPluginActivated(PluginHost::IShell *shell, const char *callSign); + + std::thread mInitializationThread; + Core::Sink mMonitorKeys; + std::mutex mMutex; + Config mConfig; + PluginHost::IShell *mShell; + std::map> mKeysMap; + }; + + typedef std::unique_ptr SiftConfigPtr; + } +} \ No newline at end of file diff --git a/Analytics/Module.cpp b/Analytics/Module.cpp new file mode 100644 index 0000000000..69ecca0532 --- /dev/null +++ b/Analytics/Module.cpp @@ -0,0 +1,22 @@ +/** +* If not stated otherwise in this file or this component's LICENSE +* file the following copyright and licenses apply: +* +* Copyright 2020 RDK Management +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +**/ + +#include "Module.h" + +MODULE_NAME_DECLARATION(BUILD_REFERENCE) diff --git a/Analytics/Module.h b/Analytics/Module.h new file mode 100644 index 0000000000..d414ea8724 --- /dev/null +++ b/Analytics/Module.h @@ -0,0 +1,29 @@ +/** +* If not stated otherwise in this file or this component's LICENSE +* file the following copyright and licenses apply: +* +* Copyright 2020 RDK Management +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +**/ + +#pragma once +#ifndef MODULE_NAME +#define MODULE_NAME Plugin_Analytics +#endif + +#include +#include + +#undef EXTERNAL +#define EXTERNAL diff --git a/CMakeLists.txt b/CMakeLists.txt index 31a5f514fe..f942fdc331 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -401,6 +401,10 @@ if(PLUGIN_SHAREDSTORAGE) add_subdirectory(SharedStorage) endif() +if (PLUGIN_ANALYTICS) + add_subdirectory(Analytics) +endif() + if(WPEFRAMEWORK_CREATE_IPKG_TARGETS) set(CPACK_GENERATOR "DEB") set(CPACK_DEB_COMPONENT_INSTALL ON)