[vcpkg-contact-survey] Add monthly survey prompt
MODIFY
include/pch.h
include/pch.h
#include <map>
#include <memory>
#include <mutex>
+#include <random>
#include <regex>
#include <set>
#include <stdexcept>

MODIFY
include/tests.pch.h
include/tests.pch.h
#include <CppUnitTest.h>
+#include <vcpkg/base/chrono.h>
#include <vcpkg/base/sortedvector.h>
#include <vcpkg/base/strings.h>
#include <vcpkg/base/util.h>

MODIFY
include/vcpkg/base/chrono.h
include/vcpkg/base/chrono.h
#include <chrono>
#include <string>
+#include <time.h>
+#include <vcpkg/base/optional.h>
namespace vcpkg::Chrono
{
@@ -44,4 +46,21 @@ namespace vcpkg::Chrono
private:
std::chrono::high_resolution_clock::time_point m_start_tick;
};
+
+ class CTime
+ {
+ public:
+ static Optional<CTime> get_current_date_time();
+ static Optional<CTime> parse(CStringView str);
+
+ constexpr CTime() : m_tm{0} {}
+ explicit constexpr CTime(tm t) : m_tm{t} {}
+
+ std::string to_string() const;
+
+ std::chrono::system_clock::time_point to_time_point() const;
+
+ private:
+ mutable tm m_tm;
+ };
}

MODIFY
include/vcpkg/globalstate.h
include/vcpkg/globalstate.h
@@ -10,6 +10,8 @@ namespace vcpkg
struct GlobalState
{
static Util::LockGuarded<Chrono::ElapsedTimer> timer;
+ static Util::LockGuarded<std::string> g_surveydate;
+
static std::atomic<bool> debugging;
static std::atomic<bool> feature_packages;

ADD
include/vcpkg/userconfig.h
+#pragma once
+
+#include <string>
+#include <vcpkg/base/files.h>
+
+namespace vcpkg
+{
+ struct UserConfig
+ {
+ std::string user_id;
+ std::string user_time;
+ std::string user_mac;
+
+ std::string last_completed_survey;
+
+ static UserConfig try_read_data(const Files::Filesystem& fs);
+
+ void try_write_data(Files::Filesystem& fs) const;
+ };
+}

ADD
src/tests.chrono.cpp
+#include "tests.pch.h"
+
+using namespace Microsoft::VisualStudio::CppUnitTestFramework;
+
+namespace Chrono = vcpkg::Chrono;
+
+namespace UnitTest1
+{
+ class ChronoTests : public TestClass<ChronoTests>
+ {
+ TEST_METHOD(parse_time)
+ {
+ auto timestring = "1990-02-03T04:05:06.0Z";
+ auto maybe_time = Chrono::CTime::parse(timestring);
+
+ Assert::IsTrue(maybe_time.has_value());
+
+ Assert::AreEqual(timestring, maybe_time.get()->to_string().c_str());
+ }
+
+ TEST_METHOD(parse_time_blank)
+ {
+ auto maybe_time = Chrono::CTime::parse("");
+
+ Assert::IsFalse(maybe_time.has_value());
+ }
+
+ TEST_METHOD(time_difference)
+ {
+ auto maybe_time1 = Chrono::CTime::parse("1990-02-03T04:05:06.0Z");
+ auto maybe_time2 = Chrono::CTime::parse("1990-02-10T04:05:06.0Z");
+
+ Assert::IsTrue(maybe_time1.has_value());
+ Assert::IsTrue(maybe_time2.has_value());
+
+ auto delta = maybe_time2.get()->to_time_point() - maybe_time1.get()->to_time_point();
+
+ Assert::AreEqual(24 * 7, std::chrono::duration_cast<std::chrono::hours>(delta).count());
+ }
+ };
+}

MODIFY
src/vcpkg.cpp
src/vcpkg.cpp
#include <vcpkg/input.h>
#include <vcpkg/metrics.h>
#include <vcpkg/paragraphs.h>
+#include <vcpkg/userconfig.h>
#include <vcpkg/vcpkglib.h>
#include <cassert>
#include <fstream>
#include <memory>
+#include <random>
#pragma comment(lib, "ole32")
#pragma comment(lib, "shell32")
@@ -110,6 +112,28 @@ static void inner(const VcpkgCmdArguments& args)
if (args.command != "autocomplete")
{
Commands::Version::warn_if_vcpkg_version_mismatch(paths);
+ std::string surveydate = *GlobalState::g_surveydate.lock();
+ auto maybe_surveydate = Chrono::CTime::parse(surveydate);
+ if (auto p_surveydate = maybe_surveydate.get())
+ {
+ auto delta = std::chrono::system_clock::now() - p_surveydate->to_time_point();
+ // 24 hours/day * 30 days/month
+ if (std::chrono::duration_cast<std::chrono::hours>(delta).count() > 24 * 30)
+ {
+ std::default_random_engine generator(
+ static_cast<unsigned int>(std::chrono::system_clock::now().time_since_epoch().count()));
+ std::uniform_int_distribution<int> distribution(1, 4);
+
+ if (distribution(generator) == 1)
+ {
+ Metrics::g_metrics.lock()->track_property("surveyprompt", "true");
+ System::println(
+ System::Color::success,
+ "Your feedback is important to improve Vcpkg! Please take 3 minutes to complete our survey "
+ "by running: vcpkg contact --survey");
+ }
+ }
+ }
}
if (const auto command_function = find_command(Commands::get_available_commands_type_b()))
@@ -148,80 +172,41 @@ static void inner(const VcpkgCmdArguments& args)
static void load_config()
{
#if defined(_WIN32)
- fs::path localappdata;
- {
- // Config path in AppDataLocal
- wchar_t* localappdatapath = nullptr;
- if (S_OK != SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &localappdatapath)) __fastfail(1);
- localappdata = localappdatapath;
- CoTaskMemFree(localappdatapath);
- }
-
- std::string user_id, user_time, user_mac;
- try
- {
- auto maybe_pghs = Paragraphs::get_paragraphs(Files::get_real_filesystem(), localappdata / "vcpkg" / "config");
- if (const auto p_pghs = maybe_pghs.get())
- {
- const auto& pghs = *p_pghs;
-
- std::unordered_map<std::string, std::string> keys;
- if (pghs.size() > 0) keys = pghs[0];
-
- for (size_t x = 1; x < pghs.size(); ++x)
- {
- for (auto&& p : pghs[x])
- keys.insert(p);
- }
+ auto& fs = Files::get_real_filesystem();
- user_id = keys["User-Id"];
- user_time = keys["User-Since"];
- user_mac = keys["Mac-Hash"];
- }
- }
- catch (...)
- {
- }
+ auto config = UserConfig::try_read_data(fs);
bool write_config = false;
// config file not found, could not be read, or invalid
- if (user_id.empty() || user_time.empty())
+ if (config.user_id.empty() || config.user_time.empty())
{
- ::vcpkg::Metrics::Metrics::init_user_information(user_id, user_time);
+ ::vcpkg::Metrics::Metrics::init_user_information(config.user_id, config.user_time);
write_config = true;
}
- if (user_mac.empty())
+ if (config.user_mac.empty())
{
- user_mac = Metrics::get_MAC_user();
+ config.user_mac = Metrics::get_MAC_user();
write_config = true;
}
{
auto locked_metrics = Metrics::g_metrics.lock();
- locked_metrics->set_user_information(user_id, user_time);
- locked_metrics->track_property("user_mac", user_mac);
+ locked_metrics->set_user_information(config.user_id, config.user_time);
+ locked_metrics->track_property("user_mac", config.user_mac);
}
- if (write_config)
+ if (config.last_completed_survey.empty())
{
- try
- {
- std::error_code ec;
- auto& fs = Files::get_real_filesystem();
- fs.create_directory(localappdata / "vcpkg", ec);
- fs.write_contents(localappdata / "vcpkg" / "config",
- Strings::format("User-Id: %s\n"
- "User-Since: %s\n"
- "Mac-Hash: %s\n",
- user_id,
- user_time,
- user_mac));
+ config.last_completed_survey = config.user_time;
}
- catch (...)
+
+ GlobalState::g_surveydate.lock()->assign(config.last_completed_survey);
+
+ if (write_config)
{
- }
+ config.try_write_data(fs);
}
#endif
}

MODIFY
src/vcpkg/base/chrono.cpp
src/vcpkg/base/chrono.cpp
@@ -60,4 +60,66 @@ namespace vcpkg::Chrono
std::string ElapsedTime::to_string() const { return format_time_userfriendly(as<std::chrono::nanoseconds>()); }
std::string ElapsedTimer::to_string() const { return elapsed().to_string(); }
+
+ Optional<CTime> CTime::get_current_date_time()
+ {
+ CTime ret;
+
+#if defined(_WIN32)
+ struct _timeb timebuffer;
+
+ _ftime_s(&timebuffer);
+
+ const errno_t err = gmtime_s(&ret.m_tm, &timebuffer.time);
+
+ if (err)
+ {
+ return nullopt;
+ }
+#else
+ time_t now = {0};
+ time(&now);
+ auto null_if_failed = gmtime_r(&now, &ret.m_tm);
+ if (null_if_failed == nullptr)
+ {
+ return nullopt;
+ }
+#endif
+
+ return ret;
+ }
+
+ Optional<CTime> CTime::parse(CStringView str)
+ {
+ CTime ret;
+ auto assigned = sscanf_s(str.c_str(),
+ "%d-%d-%dT%d:%d:%d.",
+ &ret.m_tm.tm_year,
+ &ret.m_tm.tm_mon,
+ &ret.m_tm.tm_mday,
+ &ret.m_tm.tm_hour,
+ &ret.m_tm.tm_min,
+ &ret.m_tm.tm_sec);
+ if (assigned != 6) return nullopt;
+ if (ret.m_tm.tm_year < 1900) return nullopt;
+ ret.m_tm.tm_year -= 1900;
+ if (ret.m_tm.tm_mon < 1) return nullopt;
+ ret.m_tm.tm_mon -= 1;
+ mktime(&ret.m_tm);
+ return ret;
+ }
+
+ std::string CTime::to_string() const
+ {
+ std::array<char, 80> date;
+ date.fill(0);
+
+ strftime(&date[0], date.size(), "%Y-%m-%dT%H:%M:%S.0Z", &m_tm);
+ return &date[0];
+ }
+ std::chrono::system_clock::time_point CTime::to_time_point() const
+ {
+ auto t = mktime(&m_tm);
+ return std::chrono::system_clock::from_time_t(t);
+ }
}

MODIFY
src/vcpkg/commands.contact.cpp
src/vcpkg/commands.contact.cpp
#include "pch.h"
+#include <vcpkg/base/chrono.h>
#include <vcpkg/base/system.h>
#include <vcpkg/commands.h>
#include <vcpkg/help.h>
+#include <vcpkg/userconfig.h>
namespace vcpkg::Commands::Contact
{
@@ -28,6 +30,17 @@ namespace vcpkg::Commands::Contact
if (Util::Sets::contains(parsed_args.switches, switches[0].name))
{
+#if defined(_WIN32)
+ auto maybe_now = Chrono::CTime::get_current_date_time();
+ if (auto p_now = maybe_now.get())
+ {
+ auto& fs = Files::get_real_filesystem();
+ auto config = UserConfig::try_read_data(fs);
+ config.last_completed_survey = p_now->to_string();
+ config.try_write_data(fs);
+ }
+#endif
+
System::cmd_execute("start https://aka.ms/NPS_vcpkg");
System::println("Default browser launched to https://aka.ms/NPS_vcpkg, thank you for your feedback!");
}

MODIFY
src/vcpkg/globalstate.cpp
src/vcpkg/globalstate.cpp
namespace vcpkg
{
Util::LockGuarded<Chrono::ElapsedTimer> GlobalState::timer;
+ Util::LockGuarded<std::string> GlobalState::g_surveydate;
+
std::atomic<bool> GlobalState::debugging(false);
std::atomic<bool> GlobalState::feature_packages(false);

MODIFY
src/vcpkg/metrics.cpp
src/vcpkg/metrics.cpp
#include <vcpkg/metrics.h>
+#include <vcpkg/base/chrono.h>
#include <vcpkg/base/files.h>
#include <vcpkg/base/strings.h>
#include <vcpkg/base/system.h>
@@ -15,32 +16,13 @@ namespace vcpkg::Metrics
static std::string get_current_date_time()
{
- struct tm newtime;
- std::array<char, 80> date;
- date.fill(0);
-
-#if defined(_WIN32)
- struct _timeb timebuffer;
-
- _ftime_s(&timebuffer);
- time_t now = timebuffer.time;
- const int milli = timebuffer.millitm;
-
- const errno_t err = gmtime_s(&newtime, &now);
-
- if (err)
+ auto maybe_time = Chrono::CTime::get_current_date_time();
+ if (auto ptime = maybe_time.get())
{
- return "";
+ return ptime->to_string();
}
-#else
- time_t now;
- time(&now);
- gmtime_r(&now, &newtime);
- const int milli = 0;
-#endif
- strftime(&date[0], date.size(), "%Y-%m-%dT%H:%M:%S", &newtime);
- return std::string(&date[0]) + "." + std::to_string(milli) + "Z";
+ return "";
}
static std::string generate_random_UUID()

ADD
src/vcpkg/userconfig.cpp
+#include "pch.h"
+
+#include <vcpkg/base/files.h>
+#include <vcpkg/base/lazy.h>
+#include <vcpkg/paragraphs.h>
+#include <vcpkg/userconfig.h>
+
+namespace
+{
+ static vcpkg::Lazy<fs::path> s_localappdata;
+
+ static const fs::path& get_localappdata()
+ {
+ return s_localappdata.get_lazy([]() {
+ fs::path localappdata;
+ {
+ // Config path in AppDataLocal
+ wchar_t* localappdatapath = nullptr;
+ if (S_OK != SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &localappdatapath)) __fastfail(1);
+ localappdata = localappdatapath;
+ CoTaskMemFree(localappdatapath);
+ }
+ return localappdata;
+ });
+ }
+}
+
+namespace vcpkg
+{
+ UserConfig UserConfig::try_read_data(const Files::Filesystem& fs)
+ {
+ UserConfig ret;
+
+ try
+ {
+ auto maybe_pghs = Paragraphs::get_paragraphs(fs, get_localappdata() / "vcpkg" / "config");
+ if (const auto p_pghs = maybe_pghs.get())
+ {
+ const auto& pghs = *p_pghs;
+
+ std::unordered_map<std::string, std::string> keys;
+ if (pghs.size() > 0) keys = pghs[0];
+
+ for (size_t x = 1; x < pghs.size(); ++x)
+ {
+ for (auto&& p : pghs[x])
+ keys.insert(p);
+ }
+
+ ret.user_id = keys["User-Id"];
+ ret.user_time = keys["User-Since"];
+ ret.user_mac = keys["Mac-Hash"];
+ ret.last_completed_survey = keys["Survey-Completed"];
+ }
+ }
+ catch (...)
+ {
+ }
+
+ return ret;
+ }
+
+ void UserConfig::try_write_data(Files::Filesystem& fs) const
+ {
+ try
+ {
+ std::error_code ec;
+ fs.create_directory(get_localappdata() / "vcpkg", ec);
+ fs.write_contents(get_localappdata() / "vcpkg" / "config",
+ Strings::format("User-Id: %s\n"
+ "User-Since: %s\n"
+ "Mac-Hash: %s\n"
+ "Survey-Completed: %s\n",
+ user_id,
+ user_time,
+ user_mac,
+ last_completed_survey));
+ }
+ catch (...)
+ {
+ }
+ }
+}

MODIFY
vcpkglib/vcpkglib.vcxproj
vcpkglib/vcpkglib.vcxproj
<ClInclude Include="..\include\vcpkg\statusparagraphs.h" />
<ClInclude Include="..\include\vcpkg\triplet.h" />
<ClInclude Include="..\include\vcpkg\update.h" />
+ <ClInclude Include="..\include\vcpkg\userconfig.h" />
<ClInclude Include="..\include\vcpkg\vcpkgcmdarguments.h" />
<ClInclude Include="..\include\vcpkg\vcpkglib.h" />
<ClInclude Include="..\include\vcpkg\vcpkgpaths.h" />
<ClCompile Include="..\src\vcpkg\statusparagraphs.cpp" />
<ClCompile Include="..\src\vcpkg\triplet.cpp" />
<ClCompile Include="..\src\vcpkg\update.cpp" />
+ <ClCompile Include="..\src\vcpkg\userconfig.cpp" />
<ClCompile Include="..\src\vcpkg\vcpkgcmdarguments.cpp" />
<ClCompile Include="..\src\vcpkg\vcpkglib.cpp" />
<ClCompile Include="..\src\vcpkg\vcpkgpaths.cpp" />

MODIFY
vcpkglib/vcpkglib.vcxproj.filters
vcpkglib/vcpkglib.vcxproj.filters
<ClCompile Include="..\src\vcpkg\base\system.cpp">
<Filter>Source Files\vcpkg\base</Filter>
</ClCompile>
+ <ClCompile Include="..\src\vcpkg\userconfig.cpp">
+ <Filter>Source Files\vcpkg</Filter>
+ </ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\include\pch.h">
<ClInclude Include="..\include\vcpkg\export.ifw.h">
<Filter>Header Files\vcpkg</Filter>
</ClInclude>
+ <ClInclude Include="..\include\vcpkg\userconfig.h">
+ <Filter>Header Files\vcpkg</Filter>
+ </ClInclude>
</ItemGroup>
</Project>
\ No newline at end of file

MODIFY
vcpkgtest/vcpkgtest.vcxproj
vcpkgtest/vcpkgtest.vcxproj
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\src\tests.arguments.cpp" />
+ <ClCompile Include="..\src\tests.chrono.cpp" />
<ClCompile Include="..\src\tests.dependencies.cpp" />
<ClCompile Include="..\src\tests.packagespec.cpp" />
<ClCompile Include="..\src\tests.paragraph.cpp" />

MODIFY
vcpkgtest/vcpkgtest.vcxproj.filters
vcpkgtest/vcpkgtest.vcxproj.filters
<ClCompile Include="..\src\tests.utils.cpp">
<Filter>Source Files</Filter>
</ClCompile>
+ <ClCompile Include="..\src\tests.chrono.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\include\tests.pch.h">

[vcpkg-contact-survey] Add monthly survey prompt