Add searchable/filterable attributes to BugSplat

New metadata file method. 
Like the previous method that only aplied to windows this is so far only available on PC and XBox BugSplat APIS. Unmlike the deprecated version, it is expected to be added to MacOS and crashpad "soon".
master
Beq 2024-12-11 01:58:54 +00:00
parent d75439985f
commit 2545ffd25a
7 changed files with 337 additions and 14 deletions

View File

@ -1691,6 +1691,15 @@ set(viewer_HEADER_FILES
list(APPEND viewer_SOURCE_FILES llperfstats.cpp)
list(APPEND viewer_HEADER_FILES llperfstats.h)
if (USE_BUGSPLAT)
list(APPEND viewer_SOURCE_FILES
bugsplatattributes.cpp
)
list(APPEND viewer_HEADER_FILES
bugsplatattributes.h
)
endif (USE_BUGSPLAT)
# <FS:Ansariel> Flickr / Discord keys and fsversionvalues headers are generated in here
include_directories( ${CMAKE_CURRENT_BINARY_DIR} )

View File

@ -0,0 +1,132 @@
#include "bugsplatattributes.h"
#include <fstream>
#include <filesystem>
std::wstring BugSplatAttributes::mCrashContextFileName;
BugSplatAttributes& BugSplatAttributes::instance()
{
static BugSplatAttributes sInstance;
return sInstance;
}
#include <string>
#include <cctype>
std::wstring BugSplatAttributes::to_xml_token(const std::wstring& input)
{
// Example usage:
// std::wstring token = to_xml_token(L"Bandwidth (kbit/s)");
// The result should be: L"Bandwidth_kbit_per_s"
std::wstring result;
result.reserve(input.size() * 2); // Reserve some space, since we might insert more chars for "/"
for (wchar_t ch : input)
{
if (ch == L'/')
{
// Replace '/' with "_per_"
result.append(L"_per_");
}
else if (std::iswalnum(ch) || ch == L'_' || ch == L'-')
{
// Letters, digits, underscore, and hyphen are okay
result.push_back(ch);
}
else if (ch == L' ' || ch == L'(' || ch == L')')
{
// Replace spaces and parentheses with underscore
result.push_back(L'_');
}
else
{
// Other characters map to underscore
result.push_back(L'_');
}
}
// Ensure the first character is a letter or underscore:
// If not, prepend underscore.
if (result.empty() || !(std::iswalpha(result.front()) || result.front() == L'_'))
{
result.insert(result.begin(), L'_');
}
// trim trailing underscores
while (!result.empty() && result.back() == L'_')
{
result.pop_back();
}
return result;
}
bool BugSplatAttributes::writeToFile(const std::wstring& file_path)
{
std::lock_guard<std::mutex> lock(mMutex);
// Write to a temporary file first
std::wstring tmp_file = file_path + L".tmp";
{
std::wofstream ofs(tmp_file, std::ios::out | std::ios::trunc);
if (!ofs.good())
{
return false;
}
ofs << L"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
ofs << L"<XmlCrashContext>\n";
// First, write out attributes that have an empty category (top-level)
auto empty_category_it = mAttributes.find(L"");
if (empty_category_it != mAttributes.end())
{
for (const auto& kv : empty_category_it->second)
{
const std::wstring& key = kv.first;
const std::wstring& val = kv.second;
ofs << L" <" << key << L">" << val << L"</" << key << L">\n";
}
}
// Write out all other categories
for (const auto& cat_pair : mAttributes)
{
const std::wstring& category = cat_pair.first;
// Skip empty category since we already handled it above
if (category.empty())
{
continue;
}
ofs << L" <" << category << L">\n";
for (const auto& kv : cat_pair.second)
{
const std::wstring& key = kv.first;
const std::wstring& val = kv.second;
ofs << L" <" << key << L">" << val << L"</" << key << L">\n";
}
ofs << L" </" << category << L">\n";
}
ofs << L"</XmlCrashContext>\n";
// Close the file before renaming
ofs.close();
}
// Rename the temp file to the final file. If rename fails, leave the old file intact.
std::error_code ec;
std::filesystem::rename(tmp_file, file_path, ec);
if (ec)
{
// Rename failed, remove the temp file and return false
std::filesystem::remove(tmp_file, ec);
return false;
}
return true;
}

View File

@ -0,0 +1,71 @@
#ifndef BUGSPLATATTRIBUTES_INCLUDED
#define BUGSPLATATTRIBUTES_INCLUDED
#ifdef LL_BUGSPLAT
// Currently on Windows BugSplat supports the new attributes file, but it is expected to be added to Mac and Linux soon.
#include <string>
#include <unordered_map>
#include <mutex>
class BugSplatAttributes
{
public:
// Obtain the singleton instance
static BugSplatAttributes& instance();
template<typename T>
void setAttribute(const std::wstring& key, const T& value, const std::wstring& category = L"FS")
{
std::lock_guard<std::mutex> lock(mMutex);
const auto& xml_key = to_xml_token(key);
if constexpr (std::is_same_v<T, std::wstring>)
{
// Already wide string
mAttributes[category][xml_key] = value;
}
else if constexpr (std::is_same_v<T, const wchar_t*> || std::is_array_v<T>)
{
// Handle wide string literals and arrays (which includes string literals)
mAttributes[category][xml_key] = std::wstring(value);
}
else if constexpr (std::is_same_v<T, std::string>)
{
// Convert narrow to wide
std::wstring wide_value(value.begin(), value.end());
mAttributes[category][xml_key] = wide_value;
}
else if constexpr (std::is_same_v<T, bool>)
{
// Convert boolean to "true"/"false"
mAttributes[category][xml_key] = value ? L"true" : L"false";
}
else if constexpr (std::is_arithmetic_v<T>)
{
// Convert arithmetic types (int, float, double, etc.) to wstring
mAttributes[category][xml_key] = std::to_wstring(value);
}
else
{
static_assert(!sizeof(T*), "No known conversion for this type to std::wstring.");
}
}
// Returns true on success, false on failure.
bool writeToFile(const std::wstring& file_path);
const static std::wstring& getCrashContextFileName() { return mCrashContextFileName; }
static void setCrashContextFileName(const std::wstring& file_name) { mCrashContextFileName = file_name; }
private:
BugSplatAttributes() = default;
~BugSplatAttributes() = default;
BugSplatAttributes(const BugSplatAttributes&) = delete;
BugSplatAttributes& operator=(const BugSplatAttributes&) = delete;
std::wstring to_xml_token(const std::wstring& input);
// Internal structure to hold attributes by category
// For example:
// attributes_["RuntimeProperties"]["CrashGUID"] = L"649F57B7EE6E92A0_0000"
std::unordered_map<std::wstring, std::unordered_map<std::wstring, std::wstring>> mAttributes;
std::mutex mMutex;
static std::wstring mCrashContextFileName;
};
#endif // LL_BUGSPLAT
#endif // BUGSPLATATTRIBUTES_INCLUDED

View File

@ -289,7 +289,7 @@ using namespace LL;
#include "fsradar.h"
#include "fsassetblacklist.h"
#include "bugsplatattributes.h"
// #include "fstelemetry.h" // <FS:Beq> Tracy profiler support
#if LL_LINUX && LL_GTK
@ -767,6 +767,10 @@ LLAppViewer::LLAppViewer()
// MAINT-8917: don't create a dump directory just for the
// static_debug_info.log file
std::string logdir = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, "");
// <FS:Beq> Improve Bugsplat tracking by using attribu
std::wstring wlogdir(logdir.begin(), logdir.end());
BugSplatAttributes::setCrashContextFileName(wlogdir + L"crash-context.xml");
// </FS:Beq>
# else // ! LL_BUGSPLAT
// write Google Breakpad minidump files to a per-run dump directory to avoid multiple viewer issues.
std::string logdir = gDirUtilp->getExpandedFilename(LL_PATH_DUMP, "");
@ -3762,6 +3766,10 @@ bool LLAppViewer::waitForUpdater()
void LLAppViewer::writeDebugInfo(bool isStatic)
{
#if LL_WINDOWS && LL_BUGSPLAT
// <FS:Beq> Improve Bugsplat tracking by using attributes for certain static data items.
const LLSD& info = getViewerInfo();
bugsplatAddStaticAttributes(info);
// </FS:Beq>
// bugsplat does not create dump folder and debug logs are written directly
// to logs folder, so it conflicts with main instance
if (mSecondInstance)
@ -3970,10 +3978,17 @@ LLSD LLAppViewer::getViewerInfo() const
info["OPENGL_VERSION"] = ll_safe_string((const char*)(glGetString(GL_VERSION)));
info["LIBCURL_VERSION"] = LLCore::LLHttp::getCURLVersion();
// Settings
LLRect window_rect = gViewerWindow->getWindowRectRaw();
info["WINDOW_WIDTH"] = window_rect.getWidth();
info["WINDOW_HEIGHT"] = window_rect.getHeight();
// <FS:Beq> gViewerWindow can be null on shutdown. Crashes if bugsplatt uses the info
// LLRect window_rect = gViewerWindow->getWindowRectRaw();
// info["WINDOW_WIDTH"] = window_rect.getWidth();
// info["WINDOW_HEIGHT"] = window_rect.getHeight();
if(gViewerWindow)
{
LLRect window_rect = gViewerWindow->getWindowRectRaw();
info["WINDOW_WIDTH"] = window_rect.getWidth();
info["WINDOW_HEIGHT"] = window_rect.getHeight();
}
// </FS:Beq>
// <FS> Custom sysinfo
//info["FONT_SIZE_ADJUSTMENT"] = gSavedSettings.getF32("FontScreenDPI");
@ -3994,7 +4009,7 @@ LLSD LLAppViewer::getViewerInfo() const
info["J2C_VERSION"] = LLImageJ2C::getEngineInfo();
bool want_fullname = true;
info["AUDIO_DRIVER_VERSION"] = gAudiop ? LLSD(gAudiop->getDriverName(want_fullname)) : "Undefined";
if(LLVoiceClient::getInstance()->voiceEnabled())
if(LLStartUp::getStartupState() == STATE_STARTED && LLVoiceClient::getInstance()->voiceEnabled()) // <FS:Beq/> Calling this too earli leads to a nasty crash loop
{
LLVoiceVersionInfo version = LLVoiceClient::getInstance()->getVersion();
const std::string build_version = version.mBuildVersion;
@ -4086,7 +4101,13 @@ LLSD LLAppViewer::getViewerInfo() const
}
// populate field for new local disk cache with some details
info["DISK_CACHE_INFO"] = LLDiskCache::getInstance()->getCacheInfo();
// <FS:Beq> only populate if the cache is available
// info["DISK_CACHE_INFO"] = LLDiskCache::getInstance()->getCacheInfo();
if (auto cache = LLDiskCache::getInstance(); cache)
{
info["DISK_CACHE_INFO"] = cache->getCacheInfo();
}
// </FS:Beq>
// <FS:PP> FIRE-4785: Current render quality setting in sysinfo / about floater
switch (gSavedSettings.getU32("RenderQualityPerformance"))

View File

@ -253,6 +253,7 @@ protected:
virtual void overrideDetectedHardware(); // <FS:Beq/> Override VRAM (and others in future?) consistently.
virtual bool initSLURLHandler();
virtual bool sendURLToOtherInstance(const std::string& url);
virtual void bugsplatAddStaticAttributes(const LLSD& info) {/*empty*/}; // <FS:Beq/> create a NOOP base impl
virtual bool initParseCommandLine(LLCommandLineParser& clp)
{ return true; } // Allow platforms to specify the command line args.

View File

@ -75,6 +75,7 @@
// Bugsplat (http://bugsplat.com) crash reporting tool
#ifdef LL_BUGSPLAT
#include "bugsplatattributes.h"
#include "BugSplat.h"
#include "boost/json.hpp" // Boost.Json
#include "llagent.h" // for agent location
@ -164,6 +165,7 @@ namespace
flavor = "oss";
#endif
sBugSplatSender->setDefaultUserEmail( WCSTR(STRINGIZE(LLOSInfo::instance().getOSStringSimple() << " (" << ADDRESS_SIZE << "-bit, flavor " << flavor <<")")));
// BugSplatAttributes::instance().setAttribute(L"Flavor", flavor);
// </FS:ND>
//<FS:ND/> Clear out username first, as we get some crashes that has the OS set as username, let's see if this fixes it. Use Crash.Linden as a usr can never have a "Linden"
@ -195,7 +197,6 @@ namespace
// LL_ERRS message, when there is one
sBugSplatSender->setDefaultUserDescription(WCSTR(LLError::getFatalMessage()));
sBugSplatSender->setAttribute(WCSTR(L"OS"), WCSTR(LLOSInfo::instance().getOSStringSimple())); // In case we ever stop using email for this
sBugSplatSender->setAttribute(WCSTR(L"AppState"), WCSTR(LLStartUp::getStartupStateString()));
sBugSplatSender->setAttribute(WCSTR(L"GL Vendor"), WCSTR(gGLManager.mGLVendor));
@ -203,18 +204,41 @@ namespace
sBugSplatSender->setAttribute(WCSTR(L"GPU Version"), WCSTR(gGLManager.mDriverVersionVendorString));
sBugSplatSender->setAttribute(WCSTR(L"GL Renderer"), WCSTR(gGLManager.mGLRenderer));
sBugSplatSender->setAttribute(WCSTR(L"VRAM"), WCSTR(STRINGIZE(gGLManager.mVRAM)));
sBugSplatSender->setAttribute(WCSTR(L"RAM"), WCSTR(STRINGIZE(gSysMemory.getPhysicalMemoryKB().value())));
// sBugSplatSender->setAttribute(WCSTR(L"RAM"), WCSTR(STRINGIZE(gSysMemory.getPhysicalMemoryKB().value())));
// <FS:Beq> Improve bugsplpat reporting with attributes
// sBugSplatSender->setDefaultUserDescription(WCSTR(LLError::getFatalMessage()));
// sBugSplatSender->setAttribute(WCSTR(L"AppState"), WCSTR(LLStartUp::getStartupStateString()));
auto fatal_message = LLError::getFatalMessage();
sBugSplatSender->setDefaultUserDescription(WCSTR(fatal_message));
BugSplatAttributes::instance().setAttribute(L"FatalMessage", fatal_message); // <FS:Beq/> Store this additionally as an attribute in case user overwrites.
// App state
BugSplatAttributes::instance().setAttribute(L"AppState", LLStartUp::getStartupStateString());
// Location
// </FS:Beq>
if (gAgent.getRegion())
{
// region location, when we have it
LLVector3 loc = gAgent.getPositionAgent();
sBugSplatSender->resetAppIdentifier(
WCSTR(STRINGIZE(gAgent.getRegion()->getName()
// <FS:Beq> Improve bugsplat reporting with attributes
// LLVector3 loc = gAgent.getPositionAgent();
// sBugSplatSender->resetAppIdentifier(
// WCSTR(STRINGIZE(gAgent.getRegion()->getName()
// << '/' << loc.mV[0]
// << '/' << loc.mV[1]
// << '/' << loc.mV[2])));
const LLVector3 loc = gAgent.getPositionAgent();
const auto & fullLocation = STRINGIZE(gAgent.getRegion()->getName()
<< '/' << loc.mV[0]
<< '/' << loc.mV[1]
<< '/' << loc.mV[2])));
<< '/' << loc.mV[2]);
sBugSplatSender->resetAppIdentifier(WCSTR(fullLocation));
BugSplatAttributes::instance().setAttribute(L"Location", std::string(fullLocation));
// </FS:Beq>
}
// <FS:Beq> Improve bugsplat reporting with attributes
LLAppViewer::instance()->writeDebugInfo();
sBugSplatSender->sendAdditionalFile(WCSTR(BugSplatAttributes::getCrashContextFileName())); // <FS:Beq/> Add the new attributes file
// </FS:Beq>
} // MDSCB_EXCEPTIONCODE
return false;
@ -619,6 +643,71 @@ int APIENTRY wWinMain(HINSTANCE hInstance,
}
}
#endif
// <FS:Beq> Use the Attributes API on Windows to enhance crash metadata
void LLAppViewerWin32::bugsplatAddStaticAttributes(const LLSD& info)
{
#ifdef LL_BUGSPLAT
static bool write_statics = true;
auto& bugSplatMap = BugSplatAttributes::instance();
if (write_statics)
{
write_statics = false;
auto multipleInstances = gDebugInfo["FoundOtherInstanceAtStartup"].asBoolean();
bugSplatMap.setAttribute(L"MultipleInstance", multipleInstances);
bugSplatMap.setAttribute(L"GPU", info["GRAPHICS_CARD"].asString());
bugSplatMap.setAttribute(L"GPU VRAM (MB)", info["GRAPHICS_CARD_MEMORY"].asInteger());
bugSplatMap.setAttribute(L"GPU VRAM Detected (MB)", info["GRAPHICS_CARD_MEMORY_DETECTED"].asInteger());
bugSplatMap.setAttribute(L"GPU VRAM (Budget)", info["VRAM_BUDGET_ENGLISH"].asInteger());
bugSplatMap.setAttribute(L"CPU", info["CPU"].asString());
bugSplatMap.setAttribute(L"Graphics Driver", info["GRAPHICS_DRIVER_VERSION"].asString());
bugSplatMap.setAttribute(L"CPU MHz", (S32)gSysCPU.getMHz()); //
#ifdef USE_AVX2_OPTIMIZATION
bugSplatMap.setAttribute(L"SIMD", L"AVX2");
#elif USE_AVX_OPTIMIZATION
bugSplatMap.setAttribute(L"SIMD", L"AVX");
#else
bugSplatMap.setAttribute(L"SIMD", L"SSE2");
#endif
// set physical ram integer as a string attribute
bugSplatMap.setAttribute(L"Physical RAM (KB)", LLMemory::getMaxMemKB().value());
bugSplatMap.setAttribute(L"OpenGL Version", info["OPENGL_VERSION"].asString());
bugSplatMap.setAttribute(L"libcurl Version", info["LIBCURL_VERSION"].asString());
bugSplatMap.setAttribute(L"J2C Decoder Version", info["J2C_VERSION"].asString());
bugSplatMap.setAttribute(L"Audio Driver Version", info["AUDIO_DRIVER_VERSION"].asString());
// bugSplatMap.setAttribute(L"CEF Info", info["LIBCEF_VERSION"].asString());
bugSplatMap.setAttribute(L"LibVLC Version", info["LIBVLC_VERSION"].asString());
bugSplatMap.setAttribute(L"Vivox Version", info["VOICE_VERSION"].asString());
bugSplatMap.setAttribute(L"RLVa", info["RLV_VERSION"].asString());
bugSplatMap.setAttribute(L"Mode", info["MODE"].asString());
bugSplatMap.setAttribute(L"Skin", llformat("%s (%s)", info["SKIN"].asString().c_str(), info["THEME"].asString().c_str()));
#if LL_DARWIN
bugSplatMap.setAttribute(L"HiDPI", info["HIDPI"].asBoolean() ? L"Enabled" : L"Disabled");
#endif
}
// These attributes are potentially dynamic
bugSplatMap.setAttribute(L"Packets Lost", llformat("%.0f/%.0f (%.1f%%)", info["PACKETS_LOST"].asReal(), info["PACKETS_IN"].asReal(), info["PACKETS_PCT"].asReal()));
bugSplatMap.setAttribute(L"Window Size", llformat("%sx%s px", info["WINDOW_WIDTH"].asString().c_str(), info["WINDOW_HEIGHT"].asString().c_str()));
bugSplatMap.setAttribute(L"Draw Distance (m)", info["DRAW_DISTANCE"].asInteger());
bugSplatMap.setAttribute(L"Bandwidth (kbit/s)", info["BANDWIDTH"].asInteger());
bugSplatMap.setAttribute(L"LOD Factor", info["LOD"].asReal());
bugSplatMap.setAttribute(L"Render quality", info["RENDERQUALITY_FSDATA_ENGLISH"].asString());
bugSplatMap.setAttribute(L"Disk Cache", info["DISK_CACHE_INFO"].asString());
bugSplatMap.setAttribute(L"GridName", gDebugInfo["GridName"].asString());
bugSplatMap.setAttribute(L"Available RAM (KB)", LLMemory::getAvailableMemKB().value());
bugSplatMap.setAttribute(L"Allocated RAM (KB)", LLMemory::getAllocatedMemKB().value());
if (bugSplatMap.writeToFile(BugSplatAttributes::getCrashContextFileName()))
{
LL_INFOS() << "Crash context saved to " << WCSTR(BugSplatAttributes::getCrashContextFileName()) << LL_ENDL;
}
#endif
}
// </FS:Beq>
void LLAppViewerWin32::disableWinErrorReporting()
{

View File

@ -62,7 +62,7 @@ protected:
private:
void disableWinErrorReporting();
void bugsplatAddStaticAttributes(const LLSD& info) override; // <FS:Beq> override for windows attributes
std::string mCmdLine;
bool mIsConsoleAllocated;