diff --git a/coding/serdes_json.hpp b/coding/serdes_json.hpp index c570a84a51..cedcd56acf 100644 --- a/coding/serdes_json.hpp +++ b/coding/serdes_json.hpp @@ -1,5 +1,7 @@ #pragma once +#include "geometry/latlon.hpp" + #include "base/exception.hpp" #include "base/scope_guard.hpp" @@ -130,6 +132,14 @@ public: (*this)(static_cast>(t), name); } + void operator()(ms::LatLon const & ll, char const * name = nullptr) + { + NewScopeWith(base::NewJSONObject(), name, [this, &ll] { + (*this)(ll.lat, "lat"); + (*this)(ll.lon, "lon"); + }); + } + protected: template void NewScopeWith(base::JSONPtr json_object, char const * name, Fn && fn) @@ -218,8 +228,8 @@ public: RestoreContext(outerContext); } - template - void operator()(std::unordered_set & dest, char const * name = nullptr) + template + void operator()(std::unordered_set & dest, char const * name = nullptr) { json_t * outerContext = SaveContext(name); @@ -241,6 +251,12 @@ public: RestoreContext(outerContext); } + template + void operator()(std::unordered_set & dest, char const * name = nullptr) + { + (*this)::key_type, std::unordered_set::hasher>(dest, name); + } + template void operator()(std::array & dst, char const * name = nullptr) { @@ -314,6 +330,14 @@ public: t = static_cast(res); } + void operator()(ms::LatLon & ll, char const * name = nullptr) + { + json_t * outerContext = SaveContext(name); + (*this)(ll.lat, "lat"); + (*this)(ll.lon, "lon"); + RestoreContext(outerContext); + } + protected: json_t * SaveContext(char const * name = nullptr) { diff --git a/metrics/eye_info.hpp b/metrics/eye_info.hpp index d9baf0143d..06bc31dedd 100644 --- a/metrics/eye_info.hpp +++ b/metrics/eye_info.hpp @@ -2,6 +2,7 @@ #include "storage/index.hpp" +#include "geometry/latlon.hpp" #include "geometry/point2d.hpp" #include "base/visitor.hpp" @@ -11,6 +12,7 @@ #include #include #include +#include #include namespace eye @@ -158,6 +160,49 @@ struct Tip using Tips = std::vector; +struct MapObject +{ + struct Event + { + enum Type + { + Open, + AddToBookmark, + UgcEditorOpened, + UgcSaved, + RouteToCreated + }; + + DECLARE_VISITOR(visitor(m_type, "type"), visitor(m_userPos, "user_pos"), + visitor(m_eventTime, "event_time")); + + Type m_type; + ms::LatLon m_userPos; + Time m_eventTime; + }; + + struct Hash + { + size_t operator()(MapObject const & p) const + { + return base::Hash( + base::Hash(p.m_pos.lat, p.m_pos.lon), + base::Hash(p.m_bestType, p.m_bestType)); + } + }; + + bool operator==(MapObject const & rhs) const + { + return m_pos.EqualDxDy(rhs.m_pos, 1e-6) && + m_bestType == rhs.m_bestType; + } + + std::string m_bestType; + ms::LatLon m_pos; +}; + +using MapObjects = std::unordered_map, MapObject::Hash>; + struct InfoV0 { static Version GetVersion() { return Version::V0; } @@ -170,6 +215,7 @@ struct InfoV0 Discovery m_discovery; Layers m_layers; Tips m_tips; + MapObjects m_mapObjects; }; using Info = InfoV0; diff --git a/metrics/eye_serdes.cpp b/metrics/eye_serdes.cpp index c3a1b2f1e0..11f6243a3b 100644 --- a/metrics/eye_serdes.cpp +++ b/metrics/eye_serdes.cpp @@ -1,4 +1,5 @@ #include "metrics/eye_serdes.hpp" +#include "metrics/eye_info.hpp" #include "coding/reader.hpp" #include "coding/serdes_json.hpp" @@ -7,13 +8,27 @@ #include "base/logging.hpp" +#include #include #include +namespace +{ +struct MapObjectEvent +{ + DECLARE_VISITOR(visitor(m_bestPoiType, "best_type"), visitor(m_poiPos, "pos"), + visitor(m_event, "event")); + + std::string m_bestPoiType; + ms::LatLon m_poiPos; + eye::MapObject::Event m_event; +}; +} // namespace + namespace eye { // static -void Serdes::Serialize(Info const & info, std::vector & result) +void Serdes::SerializeInfo(Info const & info, std::vector & result) { result.clear(); using Sink = MemWriter>; @@ -26,7 +41,7 @@ void Serdes::Serialize(Info const & info, std::vector & result) } // static -void Serdes::Deserialize(std::vector const & bytes, Info & result) +void Serdes::DeserializeInfo(std::vector const & bytes, Info & result) { MemReader reader(bytes.data(), bytes.size()); NonOwningReaderSource source(reader); @@ -45,11 +60,102 @@ void Serdes::Deserialize(std::vector const & bytes, Info & result) catch (base::Json::Exception & ex) { LOG(LERROR, ("Cannot deserialize eye file. Exception:", ex.Msg(), "Version:", version, - "File content:", bytes)); + "File content:", std::string(bytes.begin(), bytes.end()))); } return; } MYTHROW(UnknownVersion, ("Unknown data version:", static_cast(version))); } + +// static +void Serdes::SerializeMapObjects(MapObjects const & mapObjects, std::vector & result) +{ + result.clear(); + using Sink = MemWriter>; + + Sink writer(result); + std::string const nextLine = "\n"; + MapObjectEvent event; + + for (auto const & poi : mapObjects) + { + for (auto const & poiEvent : poi.second) + { + { + coding::SerializerJson ser(writer); + event.m_bestPoiType = poi.first.m_bestType; + event.m_poiPos = poi.first.m_pos; + event.m_event = poiEvent; + ser(event); + } + writer.Write(nextLine.data(), nextLine.size()); + } + } +} + +// static +void Serdes::DeserializeMapObjects(std::vector const & bytes, MapObjects & result) +{ + MemReader reader(bytes.data(), bytes.size()); + NonOwningReaderSource source(reader); + + std::string tmp(bytes.begin(), bytes.end()); + std::istringstream is(tmp); + + std::string eventString; + MapObjectEvent event; + MapObject poi; + + try + { + while (getline(is, eventString)) + { + if (eventString.empty()) + return; + + coding::DeserializerJson des(eventString); + des(event); + poi.m_bestType = event.m_bestPoiType; + poi.m_pos = event.m_poiPos; + + auto it = result.find(poi); + if (it == result.end()) + { + std::vector events = {event.m_event}; + result.emplace(poi, std::move(events)); + } + else + { + it->second.emplace_back(event.m_event); + } + } + } + catch (base::Json::Exception & ex) + { + LOG(LERROR, ("Cannot deserialize map objects. Exception:", ex.Msg(), + ". Event string:", eventString, + ". Content:", std::string(bytes.begin(), bytes.end()))); + } +} + +// static +void Serdes::SerializeMapObjectEvent(MapObject const & poi, MapObject::Event const & poiEvent, + std::vector & result) +{ + result.clear(); + + using Sink = MemWriter>; + + Sink writer(result); + coding::SerializerJson ser(writer); + std::string const nextLine = "\n"; + + MapObjectEvent event; + event.m_bestPoiType = poi.m_bestType; + event.m_poiPos = poi.m_pos; + event.m_event = poiEvent; + ser(event); + writer.Write(nextLine.data(), nextLine.size()); +} } // namespace eye diff --git a/metrics/eye_serdes.hpp b/metrics/eye_serdes.hpp index 46e72d04f8..6e7e9cb1f5 100644 --- a/metrics/eye_serdes.hpp +++ b/metrics/eye_serdes.hpp @@ -14,7 +14,11 @@ class Serdes public: DECLARE_EXCEPTION(UnknownVersion, RootException); - static void Serialize(Info const & info, std::vector & result); - static void Deserialize(std::vector const & bytes, Info & result); + static void SerializeInfo(Info const & info, std::vector & result); + static void DeserializeInfo(std::vector const & bytes, Info & result); + static void SerializeMapObjects(MapObjects const & mapObjects, std::vector & result); + static void DeserializeMapObjects(std::vector const & bytes, MapObjects & result); + static void SerializeMapObjectEvent(MapObject const & poi, MapObject::Event const & poiEvent, + std::vector & result); }; } // namespace eye diff --git a/metrics/eye_storage.cpp b/metrics/eye_storage.cpp index 9aaf7eb282..499b608dde 100644 --- a/metrics/eye_storage.cpp +++ b/metrics/eye_storage.cpp @@ -11,18 +11,11 @@ #include -namespace eye +namespace { -// static -std::string Storage::GetEyeFilePath() +bool Save(std::string const & filename, std::vector const & src) { - return GetPlatform().WritablePathForFile("metrics"); -} - -// static -bool Storage::Save(std::string const & filePath, std::vector const & src) -{ - return base::WriteToTempAndRenameToFile(filePath, [&src](string const & fileName) + return base::WriteToTempAndRenameToFile(filename, [&src](string const & fileName) { try { @@ -38,19 +31,18 @@ bool Storage::Save(std::string const & filePath, std::vector const & src }); } -// static -bool Storage::Load(std::string const & filePath, std::vector & dst) +bool Load(std::string const & filename, std::vector & dst) { try { - FileReader reader(filePath); + FileReader reader(filename); dst.clear(); dst.resize(reader.Size()); reader.Read(0, dst.data(), dst.size()); } - catch (FileReader::Exception const &) + catch (FileReader::Exception const & ex) { dst.clear(); return false; @@ -58,4 +50,93 @@ bool Storage::Load(std::string const & filePath, std::vector & dst) return true; } + +bool Append(std::string const & filename, std::vector const & src) +{ + try + { + FileWriter writer(filename, FileWriter::Op::OP_APPEND); + writer.Write(src.data(), src.size()); + } + catch (FileWriter::Exception const &) + { + return false; + } + + return true; +} +} // namespace + +namespace eye +{ +// static +std::string Storage::GetEyeDir() +{ + return base::JoinPath(GetPlatform().SettingsDir(), "metric"); +} + +// static +std::string Storage::GetInfoFilePath() +{ + return base::JoinPath(GetEyeDir(), "info"); +} + +// static +std::string Storage::GetPoiEventsFilePath() +{ + return base::JoinPath(GetEyeDir(), "pevents"); +} + + +// static +bool Storage::SaveInfo(std::vector const & src) +{ + return Save(GetInfoFilePath(), src); +} + +// static +bool Storage::LoadInfo(std::vector & dst) +{ + return Load(GetInfoFilePath(), dst); +} + +// static +bool Storage::SaveMapObjects(std::vector const & src) +{ + return Save(GetPoiEventsFilePath(), src); +} + +// static +bool Storage::LoadMapObjects(std::vector & dst) +{ + return Load(GetPoiEventsFilePath(), dst); +} + +// static +bool Storage::AppendMapObjectEvent(std::vector const & src) +{ + return Append(GetPoiEventsFilePath(), src); +} + +// static +void Storage::Migrate() +{ + auto const oldPath = GetPlatform().WritablePathForFile("metrics"); + if (!GetPlatform().IsFileExistsByFullPath(oldPath)) + return; + + if (GetPlatform().IsFileExistsByFullPath(GetInfoFilePath())) + { + base::DeleteFileX(oldPath); + return; + } + + if (!GetPlatform().MkDirChecked(GetEyeDir())) + return; + + if (!base::CopyFileX(oldPath, GetInfoFilePath())) + return; + + base::DeleteFileX(oldPath); +} } // namespace eye diff --git a/metrics/eye_storage.hpp b/metrics/eye_storage.hpp index 3e2bcfce0d..737429bf8f 100644 --- a/metrics/eye_storage.hpp +++ b/metrics/eye_storage.hpp @@ -9,8 +9,14 @@ namespace eye class Storage { public: - static std::string GetEyeFilePath(); - static bool Save(std::string const & filePath, std::vector const & src); - static bool Load(std::string const & filePath, std::vector & dst); + static std::string GetEyeDir(); + static std::string GetInfoFilePath(); + static std::string GetPoiEventsFilePath(); + static bool SaveInfo(std::vector const & src); + static bool LoadInfo(std::vector & dst); + static bool SaveMapObjects(std::vector const & src); + static bool LoadMapObjects(std::vector & dst); + static bool AppendMapObjectEvent(std::vector const & src); + static void Migrate(); }; } // namespace eye diff --git a/metrics/metrics_tests/CMakeLists.txt b/metrics/metrics_tests/CMakeLists.txt index 95ddec4488..25e3d5c3a7 100644 --- a/metrics/metrics_tests/CMakeLists.txt +++ b/metrics/metrics_tests/CMakeLists.txt @@ -12,6 +12,7 @@ omim_link_libraries( metrics platform coding + geometry base stats_client jansson diff --git a/metrics/metrics_tests/eye_tests.cpp b/metrics/metrics_tests/eye_tests.cpp index 2c927c2b2b..51f2f6b001 100644 --- a/metrics/metrics_tests/eye_tests.cpp +++ b/metrics/metrics_tests/eye_tests.cpp @@ -18,12 +18,43 @@ namespace Info MakeDefaultInfoForTesting() { Info info; + Tip tip; tip.m_type = Tip::Type::DiscoverButton; tip.m_eventCounters.Increment(Tip::Event::GotitClicked); tip.m_lastShownTime = Time(std::chrono::hours(101010)); info.m_tips.emplace_back(std::move(tip)); + info.m_booking.m_lastFilterUsedTime = Time(std::chrono::minutes(100100)); + + info.m_bookmarks.m_lastOpenedTime = Time(std::chrono::minutes(10000)); + + Layer layer; + layer.m_useCount = 3; + layer.m_lastTimeUsed = Time(std::chrono::hours(20000)); + layer.m_type = Layer::Type::PublicTransport; + info.m_layers.emplace_back(std::move(layer)); + + + info.m_discovery.m_lastOpenedTime = Time(std::chrono::hours(30000)); + info.m_discovery.m_eventCounters.Increment(Discovery::Event::MoreAttractionsClicked); + info.m_discovery.m_lastClickedTime = Time(std::chrono::hours(30005)); + + MapObject poi; + poi.m_bestType = "shop"; + poi.m_pos = {53.652007, 108.143443}; + MapObject::Event eventInfo; + std::vector events; + eventInfo.m_eventTime = Time(std::chrono::hours(90000)); + eventInfo.m_userPos = {72.045507, 81.408095}; + eventInfo.m_type = MapObject::Event::Type::AddToBookmark; + events.emplace_back(eventInfo); + eventInfo.m_eventTime = Time(std::chrono::hours(80000)); + eventInfo.m_userPos = {53.016347, 158.683327}; + eventInfo.m_type = MapObject::Event::Type::Open; + events.emplace_back(eventInfo); + info.m_mapObjects.emplace(poi, events); + return info; } @@ -37,6 +68,29 @@ void CompareWithDefaultInfo(Info const & lhs) TEST_EQUAL(lhs.m_tips[0].m_lastShownTime, rhs.m_tips[0].m_lastShownTime, ()); TEST_EQUAL(lhs.m_tips[0].m_eventCounters.Get(Tip::Event::GotitClicked), rhs.m_tips[0].m_eventCounters.Get(Tip::Event::GotitClicked), ()); + TEST_EQUAL(lhs.m_booking.m_lastFilterUsedTime, rhs.m_booking.m_lastFilterUsedTime, ()); + TEST_EQUAL(lhs.m_bookmarks.m_lastOpenedTime, rhs.m_bookmarks.m_lastOpenedTime, ()); + TEST_EQUAL(lhs.m_layers.size(), rhs.m_layers.size(), ()); + TEST_EQUAL(lhs.m_layers.back().m_type, rhs.m_layers.back().m_type, ()); + TEST_EQUAL(lhs.m_layers.back().m_lastTimeUsed, rhs.m_layers.back().m_lastTimeUsed, ()); + TEST_EQUAL(lhs.m_layers.back().m_useCount, rhs.m_layers.back().m_useCount, ()); + TEST_EQUAL(lhs.m_discovery.m_lastOpenedTime, rhs.m_discovery.m_lastOpenedTime, ()); + TEST_EQUAL(lhs.m_discovery.m_lastClickedTime, rhs.m_discovery.m_lastClickedTime, ()); + TEST_EQUAL(lhs.m_discovery.m_eventCounters.Get(Discovery::Event::MoreAttractionsClicked), + rhs.m_discovery.m_eventCounters.Get(Discovery::Event::MoreAttractionsClicked), ()); + TEST_EQUAL(lhs.m_mapObjects.size(), rhs.m_mapObjects.size(), ()); + + auto const & lPoi = *(lhs.m_mapObjects.begin()); + auto const & rPoi = *(rhs.m_mapObjects.begin()); + TEST(lPoi.first.m_pos.EqualDxDy(rPoi.first.m_pos, 1e-6), ()); + TEST_EQUAL(lPoi.first.m_bestType, rPoi.first.m_bestType,()); + TEST_EQUAL(lPoi.second.size(), rPoi.second.size(),()); + TEST(lPoi.second[0].m_userPos.EqualDxDy(rPoi.second[0].m_userPos, 1e-6), ()); + TEST_EQUAL(lPoi.second[0].m_eventTime, rPoi.second[0].m_eventTime, ()); + TEST_EQUAL(lPoi.second[0].m_type, rPoi.second[0].m_type, ()); + TEST(lPoi.second[1].m_userPos.EqualDxDy(rPoi.second[1].m_userPos, 1e-6), ()); + TEST_EQUAL(lPoi.second[1].m_eventTime, rPoi.second[1].m_eventTime, ()); + TEST_EQUAL(lPoi.second[1].m_type, rPoi.second[1].m_type, ()); } Time GetLastShownTipTime(Tips const & tips) @@ -68,27 +122,36 @@ UNIT_TEST(Eye_SerdesTest) { auto const info = MakeDefaultInfoForTesting(); - std::vector s; - eye::Serdes::Serialize(info, s); - Info d; - eye::Serdes::Deserialize(s, d); + std::vector infoData; + std::vector mapObjectsData; + eye::Serdes::SerializeInfo(info, infoData); + eye::Serdes::SerializeMapObjects(info.m_mapObjects, mapObjectsData); + Info result; + eye::Serdes::DeserializeInfo(infoData, result); + eye::Serdes::DeserializeMapObjects(mapObjectsData, result.m_mapObjects); - CompareWithDefaultInfo(d); + CompareWithDefaultInfo(result); } UNIT_CLASS_TEST(ScopedEyeForTesting, SaveLoadTest) { auto const info = MakeDefaultInfoForTesting(); - std::vector s; - eye::Serdes::Serialize(info, s); - TEST(eye::Storage::Save(eye::Storage::GetEyeFilePath(), s), ()); - s.clear(); - TEST(eye::Storage::Load(eye::Storage::GetEyeFilePath(), s), ()); - Info d; - eye::Serdes::Deserialize(s, d); + std::vector infoData; + std::vector mapObjectsData; + eye::Serdes::SerializeInfo(info, infoData); + eye::Serdes::SerializeMapObjects(info.m_mapObjects, mapObjectsData); + TEST(eye::Storage::SaveInfo(infoData), ()); + TEST(eye::Storage::SaveMapObjects(mapObjectsData), ()); + infoData.clear(); + mapObjectsData.clear(); + TEST(eye::Storage::LoadInfo(infoData), ()); + TEST(eye::Storage::LoadMapObjects(mapObjectsData), ()); + Info result; + eye::Serdes::DeserializeInfo(infoData, result); + eye::Serdes::DeserializeMapObjects(mapObjectsData, result.m_mapObjects); - CompareWithDefaultInfo(d); + CompareWithDefaultInfo(result); } UNIT_CLASS_TEST(ScopedEyeForTesting, AppendTipTest) diff --git a/metrics/metrics_tests_support/eye_for_testing.cpp b/metrics/metrics_tests_support/eye_for_testing.cpp index 782b15e142..b9bb5b5de1 100644 --- a/metrics/metrics_tests_support/eye_for_testing.cpp +++ b/metrics/metrics_tests_support/eye_for_testing.cpp @@ -3,8 +3,12 @@ #include "metrics/eye.hpp" #include "metrics/eye_storage.hpp" +#include "platform/platform.hpp" + #include "coding/internal/file_data.hpp" +#include "base/macros.hpp" + #include namespace eye @@ -12,12 +16,18 @@ namespace eye // static void EyeForTesting::ResetEye() { + UNUSED_VALUE(GetPlatform().MkDirChecked(Storage::GetEyeDir())); + SetInfo({}); - auto const path = Storage::GetEyeFilePath(); + auto path = Storage::GetInfoFilePath(); uint64_t unused; if (base::GetFileSize(path, unused)) base::DeleteFileX(path); + + path = Storage::GetPoiEventsFilePath(); + if (base::GetFileSize(path, unused)) + base::DeleteFileX(path); } // static @@ -29,7 +39,7 @@ void EyeForTesting::SetInfo(Info const & info) // static void EyeForTesting::AppendTip(Tip::Type type, Tip::Event event) { - Eye::Instance().AppendTip(type, event); + Eye::Instance().RegisterTipClick(type, event); } // static @@ -59,6 +69,6 @@ void EyeForTesting::IncrementDiscoveryItem(Discovery::Event event) // static void EyeForTesting::AppendLayer(Layer::Type type) { - Eye::Instance().AppendLayer(type); + Eye::Instance().RegisterLayerShown(type); } } // namespace eye