/*!******************************************************************** Audacity: A Digital Audio Editor @file SentryReport.cpp @brief Define a class to report errors to Sentry. Dmitry Vedenko **********************************************************************/ #include "SentryReport.h" #include #include #include #include #include #include #include #include #include #include #include #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 lock(mDataMutex); mData.emplace_back(std::move(parameterName), std::move(parameterValue)); } //! Return the current context and reset it std::vector MoveParameters() { std::lock_guard lock(mDataMutex); std::vector 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 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(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(); 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 writer(buffer); mDocument.Accept(writer); } else { rapidjson::Writer writer(buffer); mDocument.Accept(writer); } return std::string(buffer.GetString()); } Report::~Report() { } Report::Report(const Exception& exception) : mImpl(std::make_unique(exception)) { } Report::Report(const Message& message) : mImpl(std::make_unique(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