From 911589111e1774a66c66c11013c41b78b523445b Mon Sep 17 00:00:00 2001
From: David Capello <davidcapello@gmail.com>
Date: Tue, 21 Sep 2021 11:29:48 -0300
Subject: [PATCH 01/10] Add Sentry as an alternative to handling minidumps
 manually (#2857)

---
 CMakeLists.txt         |  6 ++++++
 src/app/CMakeLists.txt | 14 +++++++++++++-
 src/app/app.cpp        |  2 ++
 src/app/send_crash.h   |  6 +++++-
 src/main/main.cpp      | 36 +++++++++++++++++++++++++++++++++++-
 5 files changed, 61 insertions(+), 3 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 749f8359c..3af0bd074 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -79,8 +79,14 @@ option(ENABLE_UI           "Compile UI (turn off to compile CLI-only version)" o
 option(FULLSCREEN_PLATFORM "Enable fullscreen by default" off)
 option(ENABLE_CLANG_TIDY   "Enable static analysis" off)
 option(ENABLE_CCACHE       "Use CCache to improve recompilation speed (optional)" on)
+option(ENABLE_SENTRY       "Use Sentry SDK to report crashes" off)
 set(CUSTOM_WEBSITE_URL "" CACHE STRING "Enable custom local webserver to check updates")
 
+if(ENABLE_SENTRY)
+  set(SENTRY_DIR "" CACHE STRING "Sentry native location")
+  set(SENTRY_DNS "" CACHE STRING "Sentry DNS URL")
+endif()
+
 if(ENABLE_NEWS OR ENABLE_UPDATER)
   set(REQUIRE_CURL ON)
 else()
diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt
index 826072348..24af3321d 100644
--- a/src/app/CMakeLists.txt
+++ b/src/app/CMakeLists.txt
@@ -402,6 +402,11 @@ if(ENABLE_UI)
   endif()
 endif()
 
+set(send_crash_files)
+if(NOT ENABLE_SENTRY)
+  set(send_crash_files send_crash.cpp)
+endif()
+
 add_library(app-lib
   active_site_handler.cpp
   app.cpp
@@ -575,7 +580,6 @@ add_library(app-lib
   res/resources_loader.cpp
   resource_finder.cpp
   restore_visible_layers.cpp
-  send_crash.cpp
   shade.cpp
   site.cpp
   snap_to_grid.cpp
@@ -617,6 +621,7 @@ add_library(app-lib
   util/wrap_point.cpp
   xml_document.cpp
   xml_exception.cpp
+  ${send_crash_files}
   ${ui_app_files}
   ${app_platform_files}
   ${data_recovery_files}
@@ -677,3 +682,10 @@ if(ENABLE_STEAM)
   add_definitions(-DENABLE_STEAM)
   target_link_libraries(app-lib steam-lib)
 endif()
+
+if(ENABLE_SENTRY)
+  target_compile_definitions(app-lib PUBLIC -DENABLE_SENTRY -DSENTRY_BUILD_STATIC=1)
+  target_compile_definitions(app-lib PUBLIC -DSENTRY_DNS="${SENTRY_DNS}" -DSENTRY_BUILD_STATIC=1)
+  add_subdirectory(${SENTRY_DIR} sentry)
+  target_link_libraries(app-lib sentry)
+endif()
diff --git a/src/app/app.cpp b/src/app/app.cpp
index 814c99ab0..905a2202a 100644
--- a/src/app/app.cpp
+++ b/src/app/app.cpp
@@ -418,8 +418,10 @@ void App::run()
     checkUpdate.launch();
 #endif
 
+#if !ENABLE_SENTRY
     app::SendCrash sendCrash;
     sendCrash.search();
+#endif
 
     // Keep the console alive the whole program execute (just in case
     // we've to print errors).
diff --git a/src/app/send_crash.h b/src/app/send_crash.h
index 01c29d5f0..c8b9ce3fc 100644
--- a/src/app/send_crash.h
+++ b/src/app/send_crash.h
@@ -1,5 +1,5 @@
 // Aseprite
-// Copyright (C) 2020  Igara Studio S.A.
+// Copyright (C) 2020-2021  Igara Studio S.A.
 // Copyright (C) 2001-2018  David Capello
 //
 // This program is distributed under the terms of
@@ -16,6 +16,8 @@
 
 namespace app {
 
+#if !ENABLE_SENTRY
+
   class SendCrash
 #ifdef ENABLE_UI
     : public INotificationDelegate
@@ -43,6 +45,8 @@ namespace app {
     std::string m_dumpFilename;
   };
 
+#endif // !ENABLE_SENTRY
+
 } // namespace app
 
 #endif // APP_SEND_CRASH_H_INCLUDED
diff --git a/src/main/main.cpp b/src/main/main.cpp
index 1c18a3df7..c8af5c2a1 100644
--- a/src/main/main.cpp
+++ b/src/main/main.cpp
@@ -16,10 +16,16 @@
 #include "app/send_crash.h"
 #include "base/exception.h"
 #include "base/memory.h"
-#include "base/memory_dump.h"
 #include "base/system_console.h"
 #include "os/error.h"
 #include "os/system.h"
+#include "ver/info.h"
+
+#if ENABLE_SENTRY
+  #include "sentry.h"
+#else
+  #include "base/memory_dump.h"
+#endif
 
 #include <clocale>
 #include <cstdlib>
@@ -59,6 +65,28 @@ namespace {
   };
 #endif
 
+#if ENABLE_SENTRY
+  class Sentry {
+  public:
+    Sentry() {
+      sentry_options_t* options = sentry_options_new();
+      sentry_options_set_dsn(options, SENTRY_DNS);
+
+      std::string release = "aseprite@";
+      release += get_app_version();
+      sentry_options_set_release(options, release.c_str());
+
+#if _DEBUG
+      sentry_options_set_debug(options, 1);
+#endif
+      sentry_init(options);
+    }
+    ~Sentry() {
+      sentry_close();
+    }
+  };
+#endif
+
 }
 
 // Aseprite entry point. (Called from "os" library.)
@@ -78,13 +106,18 @@ int app_main(int argc, char* argv[])
 #endif
 
   try {
+#if ENABLE_SENTRY
+    Sentry sentry;
+#else
     base::MemoryDump memoryDump;
+#endif
     MemLeak memleak;
     base::SystemConsole systemConsole;
     app::AppOptions options(argc, const_cast<const char**>(argv));
     os::SystemRef system(os::make_system());
     app::App app;
 
+#if !ENABLE_SENTRY
     // Change the memory dump filename to save on disk (.dmp
     // file). Note: Only useful on Windows.
     {
@@ -92,6 +125,7 @@ int app_main(int argc, char* argv[])
       if (!fn.empty())
         memoryDump.setFileName(fn);
     }
+#endif
 
     const int code = app.initialize(options);
 

From 8b215235c775eb8c2ca8f653dfffbbe47ef68755 Mon Sep 17 00:00:00 2001
From: David Capello <davidcapello@gmail.com>
Date: Wed, 22 Sep 2021 18:34:23 -0300
Subject: [PATCH 02/10] Minor fix for app-lib flags in CMakeLists.txt file

---
 src/app/CMakeLists.txt | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt
index 24af3321d..2040a86e7 100644
--- a/src/app/CMakeLists.txt
+++ b/src/app/CMakeLists.txt
@@ -684,8 +684,10 @@ if(ENABLE_STEAM)
 endif()
 
 if(ENABLE_SENTRY)
-  target_compile_definitions(app-lib PUBLIC -DENABLE_SENTRY -DSENTRY_BUILD_STATIC=1)
-  target_compile_definitions(app-lib PUBLIC -DSENTRY_DNS="${SENTRY_DNS}" -DSENTRY_BUILD_STATIC=1)
+  target_compile_definitions(app-lib PUBLIC
+    -DENABLE_SENTRY
+    -DSENTRY_BUILD_STATIC=1
+    -DSENTRY_DNS="${SENTRY_DNS}")
   add_subdirectory(${SENTRY_DIR} sentry)
   target_link_libraries(app-lib sentry)
 endif()

From 024cd73f624486aba389d2b676f67d0169e11d06 Mon Sep 17 00:00:00 2001
From: David Capello <davidcapello@gmail.com>
Date: Wed, 22 Sep 2021 19:48:44 -0300
Subject: [PATCH 03/10] Change location for crash data (e.g.
 %AppData%/Aseprite/crashdb)

---
 src/app/sentry_wrapper.h | 67 ++++++++++++++++++++++++++++++++++++++++
 src/main/main.cpp        | 30 +++---------------
 2 files changed, 72 insertions(+), 25 deletions(-)
 create mode 100644 src/app/sentry_wrapper.h

diff --git a/src/app/sentry_wrapper.h b/src/app/sentry_wrapper.h
new file mode 100644
index 000000000..3eb9805f0
--- /dev/null
+++ b/src/app/sentry_wrapper.h
@@ -0,0 +1,67 @@
+// Aseprite
+// Copyright (C) 2021  Igara Studio S.A.
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifndef APP_SENTRY_WRAPPER_H
+#define APP_SENTRY_WRAPPER_H
+
+#if !ENABLE_SENTRY
+  #error ENABLE_SENTRY must be defined
+#endif
+
+#include "app/resource_finder.h"
+#include "base/string.h"
+
+#include "sentry.h"
+
+namespace app {
+
+class Sentry {
+public:
+  void init()
+  {
+    sentry_options_t* options = sentry_options_new();
+    sentry_options_set_dsn(options, SENTRY_DNS);
+
+    std::string release = "aseprite@";
+    release += get_app_version();
+    sentry_options_set_release(options, release.c_str());
+
+#if _DEBUG
+    sentry_options_set_debug(options, 1);
+#endif
+
+    setupDirs(options);
+
+    if (sentry_init(options) == 0)
+      m_init = true;
+  }
+
+  ~Sentry()
+  {
+    if (m_init)
+      sentry_close();
+  }
+
+private:
+  void setupDirs(sentry_options_t* options)
+  {
+    ResourceFinder rf;
+    rf.includeUserDir("crashdb");
+    std::string dir = rf.getFirstOrCreateDefault();
+
+#if SENTRY_PLATFORM_WINDOWS
+    sentry_options_set_database_pathw(options, base::from_utf8(dir).c_str());
+#else
+    sentry_options_set_database_path(options, dir.c_str());
+#endif
+  }
+
+  bool m_init = false;
+};
+
+} // namespace app
+
+#endif  // APP_SENTRY_WRAPPER_H
diff --git a/src/main/main.cpp b/src/main/main.cpp
index c8af5c2a1..e7940e7c2 100644
--- a/src/main/main.cpp
+++ b/src/main/main.cpp
@@ -22,7 +22,7 @@
 #include "ver/info.h"
 
 #if ENABLE_SENTRY
-  #include "sentry.h"
+  #include "app/sentry_wrapper.h"
 #else
   #include "base/memory_dump.h"
 #endif
@@ -65,28 +65,6 @@ namespace {
   };
 #endif
 
-#if ENABLE_SENTRY
-  class Sentry {
-  public:
-    Sentry() {
-      sentry_options_t* options = sentry_options_new();
-      sentry_options_set_dsn(options, SENTRY_DNS);
-
-      std::string release = "aseprite@";
-      release += get_app_version();
-      sentry_options_set_release(options, release.c_str());
-
-#if _DEBUG
-      sentry_options_set_debug(options, 1);
-#endif
-      sentry_init(options);
-    }
-    ~Sentry() {
-      sentry_close();
-    }
-  };
-#endif
-
 }
 
 // Aseprite entry point. (Called from "os" library.)
@@ -107,7 +85,7 @@ int app_main(int argc, char* argv[])
 
   try {
 #if ENABLE_SENTRY
-    Sentry sentry;
+    app::Sentry sentry;
 #else
     base::MemoryDump memoryDump;
 #endif
@@ -117,7 +95,9 @@ int app_main(int argc, char* argv[])
     os::SystemRef system(os::make_system());
     app::App app;
 
-#if !ENABLE_SENTRY
+#if ENABLE_SENTRY
+    sentry.init();
+#else
     // Change the memory dump filename to save on disk (.dmp
     // file). Note: Only useful on Windows.
     {

From 6cafec8d0647d28d369235090a99b534593f6d6c Mon Sep 17 00:00:00 2001
From: David Capello <davidcapello@gmail.com>
Date: Thu, 23 Sep 2021 12:55:30 -0300
Subject: [PATCH 04/10] Move Sentry class impl to a .cpp file

---
 src/app/CMakeLists.txt     |  4 ++-
 src/app/sentry_wrapper.cpp | 59 ++++++++++++++++++++++++++++++++++++++
 src/app/sentry_wrapper.h   | 42 ++-------------------------
 3 files changed, 65 insertions(+), 40 deletions(-)
 create mode 100644 src/app/sentry_wrapper.cpp

diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt
index 2040a86e7..1de4c0d21 100644
--- a/src/app/CMakeLists.txt
+++ b/src/app/CMakeLists.txt
@@ -403,7 +403,9 @@ if(ENABLE_UI)
 endif()
 
 set(send_crash_files)
-if(NOT ENABLE_SENTRY)
+if(ENABLE_SENTRY)
+  set(send_crash_files sentry_wrapper.cpp)
+else()
   set(send_crash_files send_crash.cpp)
 endif()
 
diff --git a/src/app/sentry_wrapper.cpp b/src/app/sentry_wrapper.cpp
new file mode 100644
index 000000000..06411b10b
--- /dev/null
+++ b/src/app/sentry_wrapper.cpp
@@ -0,0 +1,59 @@
+// Aseprite
+// Copyright (C) 2021  Igara Studio S.A.
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "app/sentry_wrapper.h"
+
+#include "app/resource_finder.h"
+#include "base/string.h"
+#include "ver/info.h"
+
+#include "sentry.h"
+
+namespace app {
+
+void Sentry::init()
+{
+  sentry_options_t* options = sentry_options_new();
+  sentry_options_set_dsn(options, SENTRY_DNS);
+
+  std::string release = "aseprite@";
+  release += get_app_version();
+  sentry_options_set_release(options, release.c_str());
+
+#if _DEBUG
+  sentry_options_set_debug(options, 1);
+#endif
+
+  setupDirs(options);
+
+  if (sentry_init(options) == 0)
+    m_init = true;
+}
+
+Sentry::~Sentry()
+{
+  if (m_init)
+    sentry_close();
+}
+
+void Sentry::setupDirs(sentry_options_t* options)
+{
+  ResourceFinder rf;
+  rf.includeUserDir("crashdb");
+  std::string dir = rf.getFirstOrCreateDefault();
+
+#if SENTRY_PLATFORM_WINDOWS
+  sentry_options_set_database_pathw(options, base::from_utf8(dir).c_str());
+#else
+  sentry_options_set_database_path(options, dir.c_str());
+#endif
+}
+
+} // namespace app
diff --git a/src/app/sentry_wrapper.h b/src/app/sentry_wrapper.h
index 3eb9805f0..eac861848 100644
--- a/src/app/sentry_wrapper.h
+++ b/src/app/sentry_wrapper.h
@@ -11,53 +11,17 @@
   #error ENABLE_SENTRY must be defined
 #endif
 
-#include "app/resource_finder.h"
-#include "base/string.h"
-
 #include "sentry.h"
 
 namespace app {
 
 class Sentry {
 public:
-  void init()
-  {
-    sentry_options_t* options = sentry_options_new();
-    sentry_options_set_dsn(options, SENTRY_DNS);
-
-    std::string release = "aseprite@";
-    release += get_app_version();
-    sentry_options_set_release(options, release.c_str());
-
-#if _DEBUG
-    sentry_options_set_debug(options, 1);
-#endif
-
-    setupDirs(options);
-
-    if (sentry_init(options) == 0)
-      m_init = true;
-  }
-
-  ~Sentry()
-  {
-    if (m_init)
-      sentry_close();
-  }
+  void init();
+  ~Sentry();
 
 private:
-  void setupDirs(sentry_options_t* options)
-  {
-    ResourceFinder rf;
-    rf.includeUserDir("crashdb");
-    std::string dir = rf.getFirstOrCreateDefault();
-
-#if SENTRY_PLATFORM_WINDOWS
-    sentry_options_set_database_pathw(options, base::from_utf8(dir).c_str());
-#else
-    sentry_options_set_database_path(options, dir.c_str());
-#endif
-  }
+  void setupDirs(sentry_options_t* options);
 
   bool m_init = false;
 };

From 0c604ca4baef0658bc5f48fb44be0b081c372aaf Mon Sep 17 00:00:00 2001
From: David Capello <davidcapello@gmail.com>
Date: Thu, 23 Sep 2021 15:14:03 -0300
Subject: [PATCH 05/10] Ask for consent to share crash data

---
 data/extensions/aseprite-theme/theme.xml |  3 ++
 data/strings/en.ini                      |  5 ++++
 data/widgets/home_view.xml               |  4 ++-
 data/widgets/options.xml                 |  3 ++
 src/app/commands/cmd_options.cpp         | 21 ++++++++++++++
 src/app/sentry_wrapper.cpp               | 27 +++++++++++++++++
 src/app/sentry_wrapper.h                 |  5 ++++
 src/app/ui/home_view.cpp                 | 37 +++++++++++++++++++++++-
 src/app/ui/home_view.h                   |  6 +++-
 src/app/ui/main_window.cpp               |  7 +++++
 src/app/ui/main_window.h                 |  3 ++
 11 files changed, 118 insertions(+), 3 deletions(-)

diff --git a/data/extensions/aseprite-theme/theme.xml b/data/extensions/aseprite-theme/theme.xml
index e7067f6e9..8e5064c6e 100644
--- a/data/extensions/aseprite-theme/theme.xml
+++ b/data/extensions/aseprite-theme/theme.xml
@@ -723,6 +723,9 @@
         <style id="workspace_tabs">
             <background color="workspace" />
         </style>
+        <style id="workspace_check_box" extends="check_box" padding="4">
+          <text color="workspace_text" align="left middle" x="14" />
+        </style>
         <style id="tab">
             <background part="tab_normal" align="middle" />
             <background part="tab_active" align="middle" state="focus" />
diff --git a/data/strings/en.ini b/data/strings/en.ini
index b8050c61f..839d3dbc8 100644
--- a/data/strings/en.ini
+++ b/data/strings/en.ini
@@ -731,6 +731,11 @@ Recover files from crashed sessions or
 closed sprite that were not saved in
 previous sessions.
 END
+share_crashdb = Share crash data with Aseprite developers
+share_crashdb_tooltip = <<<END
+Give consent to share crash data with Aseprite developers
+automatically to fix bugs for all users.
+END
 recent_files = Recent files:
 recent_folders = Recent folders:
 news = News:
diff --git a/data/widgets/home_view.xml b/data/widgets/home_view.xml
index 2074d588c..9389c0ebb 100644
--- a/data/widgets/home_view.xml
+++ b/data/widgets/home_view.xml
@@ -1,5 +1,5 @@
 <!-- Aseprite -->
-<!-- Copyright (C) 2019  Igara Studio S.A. -->
+<!-- Copyright (C) 2019-2021  Igara Studio S.A. -->
 <!-- Copyright (C) 2001-2017  David Capello -->
 <gui>
   <vbox noborders="true" id="home_view" border="4" childspacing="2" expansive="true">
@@ -14,6 +14,8 @@
       </vbox>
       <boxfiller />
       <vbox border="4">
+        <check id="share_crashdb" text="@.share_crashdb"
+               tooltip="@.share_crashdb_tooltip" style="workspace_check_box" />
         <link id="check_update" text="" style="workspace_link" />
       </vbox>
     </hbox>
diff --git a/data/widgets/options.xml b/data/widgets/options.xml
index 6a2dc27be..43854fd45 100644
--- a/data/widgets/options.xml
+++ b/data/widgets/options.xml
@@ -68,6 +68,9 @@
                  text="@.color_bar_entries_separator"
                  tooltip="@.color_bar_entries_separator"
                  pref="color_bar.entries_separator" />
+          <check id="share_crashdb"
+                 text="@home_view.share_crashdb"
+                 tooltip="@home_view.share_crashdb_tooltip" />
 
           <separator horizontal="true" />
           <link id="locate_file" text="@.locate_file" />
diff --git a/src/app/commands/cmd_options.cpp b/src/app/commands/cmd_options.cpp
index 0be02c97b..055707554 100644
--- a/src/app/commands/cmd_options.cpp
+++ b/src/app/commands/cmd_options.cpp
@@ -27,6 +27,7 @@
 #include "app/resource_finder.h"
 #include "app/tx.h"
 #include "app/ui/color_button.h"
+#include "app/ui/main_window.h"
 #include "app/ui/pref_widget.h"
 #include "app/ui/separator_in_view.h"
 #include "app/ui/skin/skin_theme.h"
@@ -42,6 +43,10 @@
 #include "render/render.h"
 #include "ui/ui.h"
 
+#if ENABLE_SENTRY
+#include "app/sentry_wrapper.h"
+#endif
+
 #include "options.xml.h"
 
 namespace app {
@@ -490,6 +495,13 @@ public:
     else
       locateCrashFolder()->setVisible(false);
 
+    // Share crashdb
+#if ENABLE_SENTRY
+    shareCrashdb()->setSelected(Sentry::consentGiven());
+#else
+    shareCrashdb()->setVisible(false);
+#endif
+
     // Undo preferences
     limitUndo()->Click.connect([this]{ onLimitUndoCheck(); });
     limitUndo()->setSelected(m_pref.undo.sizeLimit() != 0);
@@ -541,6 +553,15 @@ public:
       sendMessage(msg);
     }
 
+    // Share crashdb
+#if ENABLE_SENTRY
+    if (shareCrashdb()->isSelected())
+      Sentry::giveConsent();
+    else
+      Sentry::revokeConsent();
+    App::instance()->mainWindow()->updateConsentCheckbox();
+#endif
+
     // Update language
     Strings::instance()->setCurrentLanguage(
       language()->getItemText(language()->getSelectedItemIndex()));
diff --git a/src/app/sentry_wrapper.cpp b/src/app/sentry_wrapper.cpp
index 06411b10b..3154faa6c 100644
--- a/src/app/sentry_wrapper.cpp
+++ b/src/app/sentry_wrapper.cpp
@@ -33,6 +33,9 @@ void Sentry::init()
 
   setupDirs(options);
 
+  // We require the user consent to upload files.
+  sentry_options_set_require_user_consent(options, 1);
+
   if (sentry_init(options) == 0)
     m_init = true;
 }
@@ -43,6 +46,30 @@ Sentry::~Sentry()
     sentry_close();
 }
 
+// static
+bool Sentry::requireConsent()
+{
+  return (sentry_user_consent_get() != SENTRY_USER_CONSENT_GIVEN);
+}
+
+// static
+bool Sentry::consentGiven()
+{
+  return (sentry_user_consent_get() == SENTRY_USER_CONSENT_GIVEN);
+}
+
+// static
+void Sentry::giveConsent()
+{
+  sentry_user_consent_give();
+}
+
+// static
+void Sentry::revokeConsent()
+{
+  sentry_user_consent_revoke();
+}
+
 void Sentry::setupDirs(sentry_options_t* options)
 {
   ResourceFinder rf;
diff --git a/src/app/sentry_wrapper.h b/src/app/sentry_wrapper.h
index eac861848..6a78df3e2 100644
--- a/src/app/sentry_wrapper.h
+++ b/src/app/sentry_wrapper.h
@@ -20,6 +20,11 @@ public:
   void init();
   ~Sentry();
 
+  static bool requireConsent();
+  static bool consentGiven();
+  static void giveConsent();
+  static void revokeConsent();
+
 private:
   void setupDirs(sentry_options_t* options);
 
diff --git a/src/app/ui/home_view.cpp b/src/app/ui/home_view.cpp
index f8b8e82cc..69e4fdcf3 100644
--- a/src/app/ui/home_view.cpp
+++ b/src/app/ui/home_view.cpp
@@ -1,5 +1,5 @@
 // Aseprite
-// Copyright (C) 2019-2020  Igara Studio S.A.
+// Copyright (C) 2019-2021  Igara Studio S.A.
 // Copyright (C) 2001-2018  David Capello
 //
 // This program is distributed under the terms of
@@ -38,6 +38,10 @@
 #include "app/ui/news_listbox.h"
 #endif
 
+#if ENABLE_SENTRY
+#include "app/sentry_wrapper.h"
+#endif
+
 namespace app {
 
 using namespace ui;
@@ -66,6 +70,22 @@ HomeView::HomeView()
 #endif
 
   checkUpdate()->setVisible(false);
+  shareCrashdb()->setVisible(false);
+
+#if ENABLE_SENTRY
+  // Show this option in home tab only when we require consent for the
+  // first time and there is crash data available to report
+  if (Sentry::requireConsent()) {
+    shareCrashdb()->setVisible(true);
+    shareCrashdb()->Click.connect(
+      [this]{
+        if (shareCrashdb()->isSelected())
+          Sentry::giveConsent();
+        else
+          Sentry::revokeConsent();
+      });
+  }
+#endif
 
   InitTheme.connect(
     [this]{
@@ -101,6 +121,21 @@ void HomeView::dataRecoverySessionsAreReady()
 #endif
 }
 
+#if ENABLE_SENTRY
+void HomeView::updateConsentCheckbox()
+{
+  if (Sentry::requireConsent()) {
+    shareCrashdb()->setVisible(true);
+    shareCrashdb()->setSelected(false);
+  }
+  else if (Sentry::consentGiven()) {
+    shareCrashdb()->setVisible(false);
+    shareCrashdb()->setSelected(true);
+  }
+  layout();
+}
+#endif
+
 std::string HomeView::getTabText()
 {
   return Strings::home_view_title();
diff --git a/src/app/ui/home_view.h b/src/app/ui/home_view.h
index a6f071a86..e07842b2d 100644
--- a/src/app/ui/home_view.h
+++ b/src/app/ui/home_view.h
@@ -1,5 +1,5 @@
 // Aseprite
-// Copyright (C) 2019  Igara Studio S.A.
+// Copyright (C) 2019-2021  Igara Studio S.A.
 // Copyright (C) 2001-2016  David Capello
 //
 // This program is distributed under the terms of
@@ -46,6 +46,10 @@ namespace app {
     // function is called.
     void dataRecoverySessionsAreReady();
 
+#if ENABLE_SENTRY
+    void updateConsentCheckbox();
+#endif
+
     // TabView implementation
     std::string getTabText() override;
     TabIcon getTabIcon() override;
diff --git a/src/app/ui/main_window.cpp b/src/app/ui/main_window.cpp
index 61ba7ba3d..080d161ab 100644
--- a/src/app/ui/main_window.cpp
+++ b/src/app/ui/main_window.cpp
@@ -223,6 +223,13 @@ CheckUpdateDelegate* MainWindow::getCheckUpdateDelegate()
 }
 #endif
 
+#if ENABLE_SENTRY
+void MainWindow::updateConsentCheckbox()
+{
+  getHomeView()->updateConsentCheckbox();
+}
+#endif
+
 void MainWindow::showNotification(INotificationDelegate* del)
 {
   m_notifications->addLink(del);
diff --git a/src/app/ui/main_window.h b/src/app/ui/main_window.h
index f6213bf7b..9db1452a0 100644
--- a/src/app/ui/main_window.h
+++ b/src/app/ui/main_window.h
@@ -66,6 +66,9 @@ namespace app {
 #ifdef ENABLE_UPDATER
     CheckUpdateDelegate* getCheckUpdateDelegate();
 #endif
+#if ENABLE_SENTRY
+    void updateConsentCheckbox();
+#endif
 
     void start();
     void showNotification(INotificationDelegate* del);

From f6322a137331036dc434d37b8327ba7b2755b02d Mon Sep 17 00:00:00 2001
From: David Capello <davidcapello@gmail.com>
Date: Thu, 23 Sep 2021 15:27:09 -0300
Subject: [PATCH 06/10] Don't show "Aseprite is up to date" when there is no
 new version

---
 data/strings/en.ini      | 1 -
 src/app/ui/home_view.cpp | 4 +---
 2 files changed, 1 insertion(+), 4 deletions(-)

diff --git a/data/strings/en.ini b/data/strings/en.ini
index 839d3dbc8..352f1c7c3 100644
--- a/data/strings/en.ini
+++ b/data/strings/en.ini
@@ -740,7 +740,6 @@ recent_files = Recent files:
 recent_folders = Recent folders:
 news = News:
 checking_updates = Checking Updates...
-is_up_to_date = {0} is up to date
 new_version_available = New {0} v{1} available!
 
 [import_sprite_sheet]
diff --git a/src/app/ui/home_view.cpp b/src/app/ui/home_view.cpp
index 69e4fdcf3..7a95f0720 100644
--- a/src/app/ui/home_view.cpp
+++ b/src/app/ui/home_view.cpp
@@ -211,9 +211,7 @@ void HomeView::onCheckingUpdates()
 
 void HomeView::onUpToDate()
 {
-  checkUpdate()->setText(
-    fmt::format(Strings::home_view_is_up_to_date(), get_app_name()));
-  checkUpdate()->setVisible(true);
+  checkUpdate()->setVisible(false);
 
   layout();
 }

From 18bebeaba8716103a00e46d933c4c790a116c5fe Mon Sep 17 00:00:00 2001
From: David Capello <davidcapello@gmail.com>
Date: Thu, 23 Sep 2021 19:41:40 -0300
Subject: [PATCH 07/10] Minor fix in share crash data tooltip

---
 data/strings/en.ini | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/data/strings/en.ini b/data/strings/en.ini
index 352f1c7c3..fbfa87cda 100644
--- a/data/strings/en.ini
+++ b/data/strings/en.ini
@@ -733,8 +733,9 @@ previous sessions.
 END
 share_crashdb = Share crash data with Aseprite developers
 share_crashdb_tooltip = <<<END
-Give consent to share crash data with Aseprite developers
-automatically to fix bugs for all users.
+Check to share crash data with Aseprite developers automatically.
+This will help to find new bugs and improve the general stability
+of Aseprite for all users.
 END
 recent_files = Recent files:
 recent_folders = Recent folders:

From 8512ea6f984b73322b5ac5570105f772ea5fd392 Mon Sep 17 00:00:00 2001
From: David Capello <davidcapello@gmail.com>
Date: Thu, 23 Sep 2021 19:47:21 -0300
Subject: [PATCH 08/10] Add Sentry license

---
 docs/LICENSES.md | 25 +++++++++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/docs/LICENSES.md b/docs/LICENSES.md
index 46794370f..6776e8242 100644
--- a/docs/LICENSES.md
+++ b/docs/LICENSES.md
@@ -951,6 +951,31 @@ possible. They may also add themselves to the list below.
  */
 ```
 
+# [Sentry](https://sentry.io)
+
+```
+Copyright (c) 2019 Sentry (https://sentry.io) and individual contributors.
+All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
 # [skia](https://skia.org)
 
 ```

From c6c1393402bccd2b96b3a6401b7c9cd33e93d9a0 Mon Sep 17 00:00:00 2001
From: David Capello <davidcapello@gmail.com>
Date: Thu, 23 Sep 2021 20:40:02 -0300
Subject: [PATCH 09/10] Show consent to share crash data only when there are
 something to share

---
 src/app/sentry_wrapper.cpp | 21 ++++++++++++++++++++-
 src/app/sentry_wrapper.h   |  7 +++++++
 src/app/ui/home_view.cpp   |  3 ++-
 3 files changed, 29 insertions(+), 2 deletions(-)

diff --git a/src/app/sentry_wrapper.cpp b/src/app/sentry_wrapper.cpp
index 3154faa6c..0dd6a8cf0 100644
--- a/src/app/sentry_wrapper.cpp
+++ b/src/app/sentry_wrapper.cpp
@@ -11,6 +11,7 @@
 #include "app/sentry_wrapper.h"
 
 #include "app/resource_finder.h"
+#include "base/fs.h"
 #include "base/string.h"
 #include "ver/info.h"
 
@@ -18,6 +19,9 @@
 
 namespace app {
 
+// Directory where Sentry database is saved.
+std::string Sentry::m_dbdir;
+
 void Sentry::init()
 {
   sentry_options_t* options = sentry_options_new();
@@ -70,17 +74,32 @@ void Sentry::revokeConsent()
   sentry_user_consent_revoke();
 }
 
+// static
+bool Sentry::areThereCrashesToReport()
+{
+  if (m_dbdir.empty())
+    return false;
+
+  for (auto f : base::list_files(base::join_path(m_dbdir, "completed"))) {
+    if (base::get_file_extension(f) == "dmp")
+      return true;              // At least one .dmp file in the completed/ directory
+  }
+  return false;
+}
+
 void Sentry::setupDirs(sentry_options_t* options)
 {
   ResourceFinder rf;
   rf.includeUserDir("crashdb");
-  std::string dir = rf.getFirstOrCreateDefault();
+  const std::string dir = rf.getFirstOrCreateDefault();
 
 #if SENTRY_PLATFORM_WINDOWS
   sentry_options_set_database_pathw(options, base::from_utf8(dir).c_str());
 #else
   sentry_options_set_database_path(options, dir.c_str());
 #endif
+
+  m_dbdir = dir;
 }
 
 } // namespace app
diff --git a/src/app/sentry_wrapper.h b/src/app/sentry_wrapper.h
index 6a78df3e2..97873016c 100644
--- a/src/app/sentry_wrapper.h
+++ b/src/app/sentry_wrapper.h
@@ -13,6 +13,8 @@
 
 #include "sentry.h"
 
+#include <string>
+
 namespace app {
 
 class Sentry {
@@ -25,10 +27,15 @@ public:
   static void giveConsent();
   static void revokeConsent();
 
+  // Returns true if there are some crash to report. Used to display
+  // the "give consent" check box for first time.
+  static bool areThereCrashesToReport();
+
 private:
   void setupDirs(sentry_options_t* options);
 
   bool m_init = false;
+  static std::string m_dbdir;
 };
 
 } // namespace app
diff --git a/src/app/ui/home_view.cpp b/src/app/ui/home_view.cpp
index 7a95f0720..482773b8c 100644
--- a/src/app/ui/home_view.cpp
+++ b/src/app/ui/home_view.cpp
@@ -75,7 +75,8 @@ HomeView::HomeView()
 #if ENABLE_SENTRY
   // Show this option in home tab only when we require consent for the
   // first time and there is crash data available to report
-  if (Sentry::requireConsent()) {
+  if (Sentry::requireConsent() &&
+      Sentry::areThereCrashesToReport()) {
     shareCrashdb()->setVisible(true);
     shareCrashdb()->Click.connect(
       [this]{

From f07dc53d8311740c229d99eef09821c05df04ba9 Mon Sep 17 00:00:00 2001
From: David Capello <davidcapello@gmail.com>
Date: Thu, 23 Sep 2021 21:06:19 -0300
Subject: [PATCH 10/10] Use our anonymous UUID to identify Sentry users

---
 src/app/check_update.cpp   | 22 ++++++++++++++++++----
 src/app/sentry_wrapper.cpp |  8 ++++++++
 src/app/sentry_wrapper.h   |  2 ++
 3 files changed, 28 insertions(+), 4 deletions(-)

diff --git a/src/app/check_update.cpp b/src/app/check_update.cpp
index 2fe2d7714..6490cacf0 100644
--- a/src/app/check_update.cpp
+++ b/src/app/check_update.cpp
@@ -1,5 +1,5 @@
 // Aseprite
-// Copyright (C) 2020  Igara Studio S.A.
+// Copyright (C) 2020-2021  Igara Studio S.A.
 // Copyright (C) 2001-2017  David Capello
 //
 // This program is distributed under the terms of
@@ -21,6 +21,10 @@
 #include "base/version.h"
 #include "ver/info.h"
 
+#if ENABLE_SENTRY
+  #include "app/sentry_wrapper.h"
+#endif
+
 #include <ctime>
 #include <sstream>
 
@@ -113,6 +117,14 @@ CheckUpdateThreadLauncher::~CheckUpdateThreadLauncher()
 
 void CheckUpdateThreadLauncher::launch()
 {
+  if (m_uuid.empty())
+    m_uuid = m_preferences.updater.uuid();
+
+#if ENABLE_SENTRY
+  if (!m_uuid.empty())
+    Sentry::setUserID(m_uuid);
+#endif
+
   // In this case we are in the "wait days" period, so we don't check
   // for updates.
   if (!m_doCheck) {
@@ -120,9 +132,6 @@ void CheckUpdateThreadLauncher::launch()
     return;
   }
 
-  if (m_uuid.empty())
-    m_uuid = m_preferences.updater.uuid();
-
   m_delegate->onCheckingUpdates();
 
   m_bgJob.reset(new CheckUpdateBackgroundJob);
@@ -168,6 +177,11 @@ void CheckUpdateThreadLauncher::onMonitoringTick()
   if (!m_response.getUuid().empty()) {
     m_uuid = m_response.getUuid();
     m_preferences.updater.uuid(m_uuid);
+
+#if ENABLE_SENTRY
+    if (!m_uuid.empty())
+      Sentry::setUserID(m_uuid);
+#endif
   }
 
   // Set the date of the last "check for updates" and the "WaitDays" parameter.
diff --git a/src/app/sentry_wrapper.cpp b/src/app/sentry_wrapper.cpp
index 0dd6a8cf0..40e3427d6 100644
--- a/src/app/sentry_wrapper.cpp
+++ b/src/app/sentry_wrapper.cpp
@@ -50,6 +50,14 @@ Sentry::~Sentry()
     sentry_close();
 }
 
+// static
+void Sentry::setUserID(const std::string& uuid)
+{
+  sentry_value_t user = sentry_value_new_object();
+  sentry_value_set_by_key(user, "id", sentry_value_new_string(uuid.c_str()));
+  sentry_set_user(user);
+}
+
 // static
 bool Sentry::requireConsent()
 {
diff --git a/src/app/sentry_wrapper.h b/src/app/sentry_wrapper.h
index 97873016c..c833f9f9d 100644
--- a/src/app/sentry_wrapper.h
+++ b/src/app/sentry_wrapper.h
@@ -22,6 +22,8 @@ public:
   void init();
   ~Sentry();
 
+  static void setUserID(const std::string& uuid);
+
   static bool requireConsent();
   static bool consentGiven();
   static void giveConsent();