Adds lib-sentry-report

This commit is contained in:
Dmitry Vedenko 2021-06-08 13:40:08 +03:00 committed by Dmitry Vedenko
parent 185d1d5ecc
commit cb1f8b6c34
11 changed files with 852 additions and 2 deletions

View File

@ -168,6 +168,14 @@ include( CMakePushCheckState )
include( GNUInstallDirs )
include( TestBigEndian )
cmake_dependent_option(
${_OPT}has_sentry_reporting
"Build support for sending errors to Sentry"
On
"${_OPT}has_networking;DEFINED SENTRY_DSN_KEY;DEFINED SENTRY_HOST;DEFINED SENTRY_PROJECT"
Off
)
# Determine 32-bit or 64-bit target
if( CMAKE_C_COMPILER_ID MATCHES "MSVC" AND CMAKE_VS_PLATFORM_NAME MATCHES "Win64|x64" )
set( IS_64BIT ON )

View File

@ -144,6 +144,12 @@ if( NOT CMAKE_SYSTEM_NAME MATCHES "Darwin|Windows")
)
endif()
add_conan_lib(
RapidJSON
rapidjson/1.1.0
REQUIRED
)
set_conan_vars_to_parent()
# Required libraries

View File

@ -3,16 +3,21 @@
# The list of modules is ordered so that each library occurs after any others
# that it depends on
set( LIBRARIES
"lib-string-utils"
lib-string-utils
lib-strings
lib-utility
lib-uuid
)
if ( ${_OPT}has_networking )
list( APPEND LIBRARIES "lib-network-manager")
list( APPEND LIBRARIES lib-network-manager)
endif()
# This library depends on lib-network-manager
# If Sentry reporting is disabled, an INTERFACE library
# will be defined
list( APPEND LIBRARIES lib-sentry-reporting)
foreach( LIBRARY ${LIBRARIES} )
add_subdirectory( "${LIBRARY}" )
endforeach()

View File

@ -0,0 +1,91 @@
/*!********************************************************************
Audacity: A Digital Audio Editor
@file AnonymizedMessage.cpp
@brief Define a class to store anonymized messages.
Dmitry Vedenko
**********************************************************************/
#include "AnonymizedMessage.h"
#include <regex>
#include "CodeConversions.h"
namespace audacity
{
namespace sentry
{
AnonymizedMessage::AnonymizedMessage(std::string message)
: mMessage(std::move(message))
{
CleanupPaths();
}
AnonymizedMessage::AnonymizedMessage(const std::wstring& message)
: AnonymizedMessage(ToUTF8(message))
{
}
AnonymizedMessage::AnonymizedMessage(const wxString& message)
: AnonymizedMessage(ToUTF8(message))
{
}
AnonymizedMessage::AnonymizedMessage(const char* message)
: AnonymizedMessage(std::string(message))
{
}
AnonymizedMessage::AnonymizedMessage(const wchar_t* message)
: AnonymizedMessage(ToUTF8(message))
{
}
bool AnonymizedMessage::Empty() const noexcept
{
return mMessage.empty();
}
size_t AnonymizedMessage::Length() const noexcept
{
return mMessage.size();
}
const std::string& AnonymizedMessage::GetString() const noexcept
{
return mMessage;
}
wxString AnonymizedMessage::ToWXString() const noexcept
{
return audacity::ToWXString(mMessage);
}
const char* AnonymizedMessage::c_str() const noexcept
{
return mMessage.c_str();
}
size_t AnonymizedMessage::length() const noexcept
{
return mMessage.length();
}
void AnonymizedMessage::CleanupPaths()
{
// Finding the path boundary in the arbitrary text is a hard task.
// We assume that spaces cannot be a part of the path.
// In the worst case - we will get <path> <path>
static const std::regex re(
R"(\b(?:(?:[a-zA-Z]:)?[\\/]?)?(?:[^<>:"/|\\/?\s*]+[\\/]+)*(?:[^<>:"/|\\/?*\s]+\.\w+)?)");
mMessage = std::regex_replace(
mMessage, re, "<path>", std::regex_constants::match_not_null);
}
} // namespace sentry
} // namespace audacity

View File

@ -0,0 +1,55 @@
/*!********************************************************************
Audacity: A Digital Audio Editor
@file AnonymizedMessage.h
@brief Declare a class to store anonymized messages.
Dmitry Vedenko
**********************************************************************/
#include <string>
#include <wx/string.h>
#pragma once
namespace audacity
{
namespace sentry
{
class SENTRY_REPORTING_API AnonymizedMessage final
{
public:
AnonymizedMessage() = default;
AnonymizedMessage(const AnonymizedMessage&) = default;
AnonymizedMessage(AnonymizedMessage&&) = default;
AnonymizedMessage& operator=(const AnonymizedMessage&) = default;
AnonymizedMessage& operator=(AnonymizedMessage&&) = default;
AnonymizedMessage(std::string message);
AnonymizedMessage(const std::wstring& message);
AnonymizedMessage(const wxString& message);
AnonymizedMessage(const char* message);
AnonymizedMessage(const wchar_t* message);
bool Empty() const noexcept;
size_t Length() const noexcept;
const std::string& GetString() const noexcept;
wxString ToWXString() const noexcept;
// Immitate std::string interface
const char* c_str() const noexcept;
size_t length() const noexcept;
private:
void CleanupPaths();
std::string mMessage;
};
} // namespace sentry
} // namespace audacity

View File

@ -0,0 +1,48 @@
#[[
A library, that allows sending error reports to a Sentry server
using Exception and Message interfaces.
]]#
set( TARGET lib-sentry-reporting )
set( TARGET_ROOT ${CMAKE_CURRENT_SOURCE_DIR} )
def_vars()
if(${_OPT}has_sentry_reporting)
set( SOURCES
SentryHelper.h
AnonymizedMessage.h
AnonymizedMessage.cpp
SentryReport.h
SentryReport.cpp
SentryRequestBuilder.h
SentryRequestBuilder.cpp
)
set ( LIBRARIES PRIVATE
lib-network-manager # Required for the networking
lib-string-utils # ToUtf8
lib-uuid # UUIDs are required as an event identifier.
RapidJSON::RapidJSON # Protocol is JSON based
wxwidgets::base # Required to retreive the OS information
)
set ( DEFINES
INTERFACE
HAS_SENTRY_REPORTING=1
PRIVATE
# The variables below will be used to construct Sentry URL:
# https://${SENTRY_DSN_KEY}@${SENTRY_HOST}/api/${SENTRY_PROJECT}/store
SENTRY_DSN_KEY="${SENTRY_DSN_KEY}"
SENTRY_HOST="${SENTRY_HOST}"
SENTRY_PROJECT="${SENTRY_PROJECT}"
)
audacity_library( ${TARGET} "${SOURCES}" "${LIBRARIES}" "${DEFINES}" "" )
else()
audacity_header_only_library( ${TARGET} "SentryHelper.h" "" "" "" )
endif()

View File

@ -0,0 +1,25 @@
/**********************************************************************
Audacity: A Digital Audio Editor
SentryHelper.h
Defines a macro ADD_EXCEPTION_CONTEXT, that is a no op if Sentry reporting is disabled.
Dmitry Vedenko
**********************************************************************/
#ifndef __AUDACITY_SENTRY__
#define __AUDACITY_SENTRY__
#ifdef HAS_SENTRY_REPORTING
# include "SentryReport.h"
# define ADD_EXCEPTION_CONTEXT(name, value) audacity::sentry::AddExceptionContext(name, value)
#else
# define ADD_EXCEPTION_CONTEXT(name, value)
#endif // HAS_SENTRY_REPORTING
#endif /* __AUDACITY_SENTRY__ */

View File

@ -0,0 +1,428 @@
/*!********************************************************************
Audacity: A Digital Audio Editor
@file SentryReport.cpp
@brief Define a class to report errors to Sentry.
Dmitry Vedenko
**********************************************************************/
#include "SentryReport.h"
#include <chrono>
#include <cstring>
#include <mutex>
#include <algorithm>
#include <cctype>
#include <regex>
#include <rapidjson/document.h>
#include <rapidjson/writer.h>
#include <rapidjson/prettywriter.h>
#include <wx/platinfo.h>
#include <wx/log.h>
#include "CodeConversions.h"
#include "Uuid.h"
#include "IResponse.h"
#include "NetworkManager.h"
#include "SentryRequestBuilder.h"
namespace audacity
{
namespace sentry
{
namespace
{
//! Helper class to store additional details about the exception
/*! This small class is a thread safe store for the information
we want to add to the exception before the exception occurs.
For example, we may log SQLite3 return codes here, as otherwise
they wont be available when everything fails
*/
class ExceptionContext final
{
public:
//! Adds a new item to the exception context
void Add(std::string parameterName, AnonymizedMessage parameterValue)
{
std::lock_guard<std::mutex> lock(mDataMutex);
mData.emplace_back(std::move(parameterName), std::move(parameterValue));
}
//! Return the current context and reset it
std::vector<ExceptionData> MoveParameters()
{
std::lock_guard<std::mutex> lock(mDataMutex);
std::vector<ExceptionData> emptyVector;
std::swap(mData, emptyVector);
return emptyVector;
}
//! Get an instance of the ExceptionContext
static ExceptionContext& Get()
{
static ExceptionContext instance;
return instance;
}
private:
ExceptionContext() = default;
std::mutex mDataMutex;
std::vector<ExceptionData> mData;
};
//! Append the data about the operating system to the JSON document
void AddOSContext(
rapidjson::Value& root, rapidjson::Document::AllocatorType& allocator)
{
rapidjson::Value osContext(rapidjson::kObjectType);
const wxPlatformInfo platformInfo = wxPlatformInfo::Get();
const std::string osName =
ToUTF8(platformInfo.GetOperatingSystemFamilyName());
osContext.AddMember("type", rapidjson::Value("os", allocator), allocator);
osContext.AddMember(
"name", rapidjson::Value(osName.c_str(), osName.length(), allocator),
allocator);
const std::string osVersion =
std::to_string(platformInfo.GetOSMajorVersion()) + "." +
std::to_string(platformInfo.GetOSMinorVersion()) + "." +
std::to_string(platformInfo.GetOSMicroVersion());
osContext.AddMember(
"version",
rapidjson::Value(osVersion.c_str(), osVersion.length(), allocator),
allocator);
root.AddMember("os", std::move(osContext), allocator);
}
//! Create the minimal required Sentry JSON document
rapidjson::Document CreateSentryDocument()
{
using namespace std::chrono;
rapidjson::Document document;
document.SetObject();
document.AddMember(
"timestamp",
rapidjson::Value(
duration_cast<seconds>(system_clock::now().time_since_epoch())
.count()),
document.GetAllocator());
std::string eventId = Uuid::Generate().ToHexString();
document.AddMember(
"event_id",
rapidjson::Value(
eventId.c_str(), eventId.length(), document.GetAllocator()),
document.GetAllocator());
constexpr char platform[] = "native";
document.AddMember(
"platform",
rapidjson::Value(platform, sizeof(platform) - 1, document.GetAllocator()),
document.GetAllocator());
document["platform"].SetString(
platform, sizeof(platform) - 1, document.GetAllocator());
const std::string release = std::string("audacity@") +
std::to_string(AUDACITY_VERSION) + "." +
std::to_string(AUDACITY_RELEASE) + "." +
std::to_string(AUDACITY_REVISION);
document.AddMember(
"release",
rapidjson::Value(
release.c_str(), release.length(), document.GetAllocator()),
document.GetAllocator());
rapidjson::Value contexts = rapidjson::Value(rapidjson::kObjectType);
AddOSContext(contexts, document.GetAllocator());
document.AddMember("contexts", contexts, document.GetAllocator());
return document;
}
//! Append the ExceptionData to the Exception JSON object
void AddExceptionDataToJson(
rapidjson::Value& value, rapidjson::Document::AllocatorType& allocator,
const ExceptionData& data)
{
value.AddMember(
rapidjson::Value(data.first.c_str(), data.first.length(), allocator),
rapidjson::Value(data.second.c_str(), data.second.length(), allocator),
allocator);
}
//! Serialize the Exception to JSON
void SerializeException(
const Exception& exception, rapidjson::Value& root,
rapidjson::Document::AllocatorType& allocator)
{
root.AddMember(
"type",
rapidjson::Value(
exception.Type.c_str(), exception.Type.length(), allocator),
allocator);
root.AddMember(
"value",
rapidjson::Value(
exception.Value.c_str(), exception.Value.length(), allocator),
allocator);
rapidjson::Value mechanismObject(rapidjson::kObjectType);
mechanismObject.AddMember(
"type", rapidjson::Value("runtime_error", allocator), allocator);
mechanismObject.AddMember(
"handled", false, allocator);
auto contextData = ExceptionContext::Get().MoveParameters();
if (!exception.Data.empty() || !contextData.empty())
{
rapidjson::Value dataObject(rapidjson::kObjectType);
for (const auto& data : contextData)
AddExceptionDataToJson(dataObject, allocator, data);
for (const auto& data : exception.Data)
AddExceptionDataToJson(dataObject, allocator, data);
mechanismObject.AddMember("data", std::move(dataObject), allocator);
}
root.AddMember("mechanism", std::move(mechanismObject), allocator);
}
} // namespace
Exception Exception::Create(std::string type, AnonymizedMessage value)
{
std::replace_if(type.begin(), type.end(), [](char c) {
return std::isspace(c) != 0;
}, '_');
return { std::move(type), std::move(value) };
}
Exception Exception::Create(AnonymizedMessage value)
{
return { "runtime_error", std::move(value) };
}
Exception& Exception::AddData(std::string key, AnonymizedMessage value)
{
Data.emplace_back(std::move(key), std::move(value));
return *this;
}
Message Message::Create(AnonymizedMessage message)
{
return { std::move(message) };
}
Message& Message::AddParam(AnonymizedMessage value)
{
Params.emplace_back(std::move(value));
return *this;
}
void AddExceptionContext(
std::string parameterName, AnonymizedMessage parameterValue)
{
ExceptionContext::Get().Add(std::move (parameterName), std::move (parameterValue));
}
class Report::ReportImpl
{
public:
explicit ReportImpl(const Exception& exception);
explicit ReportImpl(const Message& message);
void AddUserComment(const std::string& message);
std::string ToString(bool pretty) const;
void Send(CompletionHandler completionHandler) const;
private:
rapidjson::Document mDocument;
};
Report::ReportImpl::ReportImpl(const Exception& exception)
: mDocument(CreateSentryDocument())
{
rapidjson::Value exceptionObject(rapidjson::kObjectType);
rapidjson::Value valuesArray(rapidjson::kArrayType);
rapidjson::Value valueObject(rapidjson::kObjectType);
SerializeException(exception, valueObject, mDocument.GetAllocator());
valuesArray.PushBack(std::move(valueObject), mDocument.GetAllocator());
exceptionObject.AddMember(
"values", std::move(valuesArray), mDocument.GetAllocator());
mDocument.AddMember(
"exception", std::move(exceptionObject), mDocument.GetAllocator());
}
Report::ReportImpl::ReportImpl(const Message& message)
: mDocument(CreateSentryDocument())
{
rapidjson::Value messageObject(rapidjson::kObjectType);
messageObject.AddMember(
"message",
rapidjson::Value(
message.Value.c_str(), message.Value.length(),
mDocument.GetAllocator()),
mDocument.GetAllocator());
if (!message.Params.empty())
{
rapidjson::Value paramsArray(rapidjson::kArrayType);
for (const AnonymizedMessage& param : message.Params)
{
paramsArray.PushBack(
rapidjson::Value(
param.c_str(), param.length(), mDocument.GetAllocator()),
mDocument.GetAllocator());
}
messageObject.AddMember(
"params", std::move(paramsArray), mDocument.GetAllocator());
}
mDocument.AddMember(
"message", std::move(messageObject), mDocument.GetAllocator());
}
void Report::ReportImpl::AddUserComment(const std::string& message)
{
// We only allow adding comment to exceptions now
if (!mDocument.HasMember("exception") || message.empty())
return;
rapidjson::Value& topException = mDocument["exception"]["values"][0];
if (!topException.IsObject())
return;
rapidjson::Value& mechanism = topException["mechanism"];
// Create a data object if it still does not exist
if (!mechanism.HasMember("data"))
{
mechanism.AddMember(
"data", rapidjson::Value(rapidjson::kObjectType),
mDocument.GetAllocator());
}
// Add a comment itself
mechanism["data"].AddMember(
"user_comment",
rapidjson::Value(
message.data(), message.length(), mDocument.GetAllocator()),
mDocument.GetAllocator());
}
void Report::ReportImpl::Send(CompletionHandler completionHandler) const
{
const std::string serializedDocument = ToString(false);
network_manager::Request request =
SentryRequestBuilder::Get().CreateRequest();
auto response = network_manager::NetworkManager::GetInstance().doPost(
request, serializedDocument.data(), serializedDocument.size());
response->setRequestFinishedCallback(
[response, handler = std::move(completionHandler)](network_manager::IResponse*) {
const std::string responseData = response->readAll<std::string>();
wxLogDebug(responseData.c_str());
if (handler)
handler(response->getHTTPCode(), responseData);
});
}
std::string Report::ReportImpl::ToString(bool pretty) const
{
rapidjson::StringBuffer buffer;
if (pretty)
{
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
mDocument.Accept(writer);
}
else
{
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
mDocument.Accept(writer);
}
return std::string(buffer.GetString());
}
Report::~Report()
{
}
Report::Report(const Exception& exception)
: mImpl(std::make_unique<ReportImpl>(exception))
{
}
Report::Report(const Message& message)
: mImpl(std::make_unique<ReportImpl>(message))
{
}
void Report::AddUserComment(const std::string& comment)
{
mImpl->AddUserComment(comment);
}
std::string Report::GetReportPreview() const
{
return mImpl->ToString(true);
}
void Report::Send(CompletionHandler completionHandler) const
{
mImpl->Send(std::move (completionHandler));
}
} // namespace sentry
} // namespace audacity

View File

@ -0,0 +1,95 @@
/*!********************************************************************
Audacity: A Digital Audio Editor
@file SentryReport.h
@brief Declare a class to report errors to Sentry.
Dmitry Vedenko
**********************************************************************/
#pragma once
#include <string>
#include <utility>
#include <vector>
#include <memory>
#include <functional>
#include "AnonymizedMessage.h"
namespace audacity
{
namespace sentry
{
//! Additional payload to the exception
using ExceptionData = std::pair<std::string, AnonymizedMessage>;
//! A DTO for the Sentry Exception interface
struct SENTRY_REPORTING_API Exception final
{
//! Exception type. Should not have spaces.
std::string Type;
//! Message, associated with the Exception
AnonymizedMessage Value;
//! Arbitrary payload
std::vector<ExceptionData> Data;
//! Create a new exception
static Exception Create(std::string type, AnonymizedMessage value);
//! Create a new exception with type runtime_error
static Exception Create(AnonymizedMessage value);
//! Add a payload to the exception
Exception& AddData(std::string key, AnonymizedMessage value);
};
//! A DTO for the Sentry Message interface
struct SENTRY_REPORTING_API Message final
{
//! A string, possibly with %s placeholders, containing the message
AnonymizedMessage Value;
//! Values for the placeholders
std::vector<AnonymizedMessage> Params;
//! Create a new Message
static Message Create(AnonymizedMessage message);
//! Add a parameter to the Message
Message& AddParam(AnonymizedMessage value);
};
//! Saves a parameter, that will be appended to the next Exception report
SENTRY_REPORTING_API void AddExceptionContext(
std::string parameterName, AnonymizedMessage parameterValue);
//! A report to Sentry
class SENTRY_REPORTING_API Report final
{
public:
//! A callback, that will be called when Send completes
using CompletionHandler = std::function<void (int httpCode, std::string responseBody)>;
//! Create a report from the exception and previously added exception context
explicit Report(const Exception& exception);
//! Create a report with a single log message
explicit Report(const Message& message);
~Report();
//! Adds a user comment to the exception report
void AddUserComment(const std::string& comment);
//! Get a pretty printed report preview
std::string GetReportPreview() const;
//! Send the report to Sentry
void Send(CompletionHandler completionHandler) const;
private:
class ReportImpl;
std::unique_ptr<ReportImpl> mImpl;
};
} // namespace sentry
} // namespace audacity

View File

@ -0,0 +1,53 @@
/*!********************************************************************
Audacity: A Digital Audio Editor
@file SentryRequestBuilder.h
@brief Define a class to generate the requests to Sentry.
Dmitry Vedenko
**********************************************************************/
#include "SentryRequestBuilder.h"
#include <chrono>
namespace audacity
{
namespace sentry
{
const SentryRequestBuilder& audacity::sentry::SentryRequestBuilder::Get()
{
static SentryRequestBuilder builder;
return builder;
}
network_manager::Request SentryRequestBuilder::CreateRequest() const
{
using namespace std::chrono;
const std::string sentryAuth =
std::string("Sentry sentry_version=7,sentry_timestamp=") +
std::to_string(
duration_cast<seconds>(system_clock::now().time_since_epoch())
.count()) +
",sentry_client=sentry-audacity/1.0,sentry_key=" + SENTRY_DSN_KEY;
network_manager::Request request(mUrl);
request.setHeader("Content-Type", "application/json");
request.setHeader("X-Sentry-Auth", sentryAuth);
return request;
}
SentryRequestBuilder::SentryRequestBuilder()
{
mUrl = std::string("https://") + SENTRY_DSN_KEY + "@" + SENTRY_HOST +
"/api/" + SENTRY_PROJECT + "/store/";
}
} // namespace sentry
} // namespace audacity

View File

@ -0,0 +1,36 @@
/*!********************************************************************
Audacity: A Digital Audio Editor
@file SentryRequestBuilder.cpp
@brief Declare a class to generate the requests to Sentry.
Dmitry Vedenko
**********************************************************************/
#pragma once
#include <string>
#include "Request.h"
namespace audacity
{
namespace sentry
{
// This is a private class, so it is not exported
//! A helper, that creates a correct Request to Sentry
class SentryRequestBuilder final
{
public:
static const SentryRequestBuilder& Get();
network_manager::Request CreateRequest() const;
private:
SentryRequestBuilder();
std::string mUrl;
};
} // namespace sentry
} // namespace audacity