diff --git a/toolkit/components/telemetry/TelemetrySession.jsm b/toolkit/components/telemetry/TelemetrySession.jsm index 933ec80beb36..41d0513937c9 100644 --- a/toolkit/components/telemetry/TelemetrySession.jsm +++ b/toolkit/components/telemetry/TelemetrySession.jsm @@ -22,6 +22,7 @@ Cu.import("resource://gre/modules/Timer.jsm"); Cu.import("resource://gre/modules/TelemetrySend.jsm", this); Cu.import("resource://gre/modules/TelemetryUtils.jsm", this); Cu.import("resource://gre/modules/AppConstants.jsm"); +Cu.import("resource://gre/modules/ClientID.jsm"); const Utils = TelemetryUtils; @@ -66,7 +67,7 @@ const PREF_PREVIOUS_BUILDID = PREF_BRANCH + "previousBuildID"; const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled"; const PREF_ASYNC_PLUGIN_INIT = "dom.ipc.plugins.asyncInit.enabled"; const PREF_UNIFIED = PREF_BRANCH + "unified"; - +const PREF_SERVER = PREF_BRANCH + "server"; const MESSAGE_TELEMETRY_PAYLOAD = "Telemetry:Payload"; const MESSAGE_TELEMETRY_THREAD_HANGS = "Telemetry:ChildThreadHangs"; @@ -203,12 +204,25 @@ function getPingType(aPayload) { /** * Annotate the current session ID with the crash reporter to map potential * crash pings with the related main ping. + * + * @param sessionId {String} The telemetry session ID + * @param clientId {String} The telemetry client ID + * @param telemetryServer {String} The URL of the telemetry server */ -function annotateCrashReport(sessionId) { +function annotateCrashReport(sessionId, clientId, telemetryServer) { try { const cr = Cc["@mozilla.org/toolkit/crash-reporter;1"]; if (cr) { - cr.getService(Ci.nsICrashReporter).setTelemetrySessionId(sessionId); + const crs = cr.getService(Ci.nsICrashReporter); + + crs.setTelemetrySessionId(sessionId); + + // We do not annotate the crash if telemetry is disabled to prevent the + // crashreporter client from sending the crash ping. + if (Utils.isTelemetryEnabled) { + crs.annotateCrashReport("TelemetryClientId", clientId); + crs.annotateCrashReport("TelemetryServerURL", telemetryServer); + } } } catch (e) { // Ignore errors when crash reporting is disabled @@ -1471,7 +1485,10 @@ var Impl = { // the very same value for |_sessionStartDate|. this._sessionStartDate = this._subsessionStartDate; - annotateCrashReport(this._sessionId); + // Annotate crash reports using the cached client ID which can be accessed + // synchronously. If it is not available yet, we'll update it later. + annotateCrashReport(this._sessionId, ClientID.getCachedClientID(), + Preferences.get(PREF_SERVER, undefined)); // Initialize some probes that are kept in their own modules this._thirdPartyCookies = new ThirdPartyCookieProbe(); @@ -1498,13 +1515,13 @@ var Impl = { ppml.addMessageListener(MESSAGE_TELEMETRY_PAYLOAD, this); ppml.addMessageListener(MESSAGE_TELEMETRY_THREAD_HANGS, this); ppml.addMessageListener(MESSAGE_TELEMETRY_USS, this); -}, + }, -/** - * Does the "heavy" Telemetry initialization later on, so we - * don't impact startup performance. - * @return {Promise} Resolved when the initialization completes. - */ + /** + * Does the "heavy" Telemetry initialization later on, so we + * don't impact startup performance. + * @return {Promise} Resolved when the initialization completes. + */ delayedInit() { this._log.trace("delayedInit"); @@ -1526,6 +1543,10 @@ var Impl = { Telemetry.asyncFetchTelemetryData(function() {}); + // Update the crash annotation with the proper client ID. + annotateCrashReport(this._sessionId, yield ClientID.getClientID(), + Preferences.get(PREF_SERVER, undefined)); + if (IS_UNIFIED_TELEMETRY) { // Check for a previously written aborted session ping. yield TelemetryController.checkAbortedSessionPing(); diff --git a/toolkit/components/telemetry/docs/data/crash-ping.rst b/toolkit/components/telemetry/docs/data/crash-ping.rst index 33d1f0695576..58ce1858af01 100644 --- a/toolkit/components/telemetry/docs/data/crash-ping.rst +++ b/toolkit/components/telemetry/docs/data/crash-ping.rst @@ -2,7 +2,18 @@ "crash" ping ============ -This ping is captured after the main Firefox process crashes, whether or not the crash report is submitted to crash-stats.mozilla.org. It includes non-identifying metadata about the crash. +This ping is captured after the main Firefox process crashes or after a content +process crashes, whether or not the crash report is submitted to +crash-stats.mozilla.org. It includes non-identifying metadata about the crash. + +This ping is sent either by the ```CrashManager``` or by the crash reporter +client. The ```CrashManager``` is responsible for sending crash pings for the +content process crashes, which are sent right after the crash is detected, +as well as for main process crashes, which are sent after Firefox restarts +successfully. The crash reporter client sends crash pings only for main process +crashes whether or not the user also reports the crash. The crash reporter +client will not send the crash ping if telemetry has been disabled in Firefox +though. The environment block that is sent with this ping varies: if Firefox was running long enough to record the environment block before the crash, then the environment at the time of the crash will be recorded and ``hasCrashEnvironment`` will be true. If Firefox crashed before the environment was recorded, ``hasCrashEnvironment`` will be false and the recorded environment will be the environment at time of submission. @@ -13,7 +24,6 @@ Structure: .. code-block:: js { - version: 1, type: "crash", ... common ping data clientId: , @@ -21,6 +31,7 @@ Structure: processType: , // Type of process that crashed, see below for a list of types payload: { crashDate: "YYYY-MM-DD", + version: 1, sessionId: , // may be missing for crashes that happen early // in startup. Added in Firefox 48 with the // intention of uplifting to Firefox 46 diff --git a/toolkit/crashreporter/client/crashreporter.cpp b/toolkit/crashreporter/client/crashreporter.cpp index 10aa65dd2406..026c1212227b 100644 --- a/toolkit/crashreporter/client/crashreporter.cpp +++ b/toolkit/crashreporter/client/crashreporter.cpp @@ -13,9 +13,10 @@ #include #include #include -#include -#include -#include +#include +#include +#include +#include using std::string; using std::istream; @@ -196,9 +197,9 @@ static string GetDumpLocalID() return localId.substr(0, dot); } -// This appends the StackTraces entry generated by the minidump analyzer to the -// main crash event so that it can be picked up by Firefox once it restarts -static void AppendStackTracesToEventFile(const string& aStackTraces) +// This appends the aKey/aValue entry to the main crash event so that it can +// be picked up by Firefox once it restarts +static void AppendToEventFile(const string& aKey, const string& aValue) { if (gEventsPath.empty()) { // If there is no path for finding the crash event, skip this step. @@ -210,7 +211,7 @@ static void AppendStackTracesToEventFile(const string& aStackTraces) ofstream* f = UIOpenWrite(path.c_str(), true); if (f->is_open()) { - *f << "StackTraces=" << aStackTraces; + *f << aKey << "=" << aValue << std::endl; f->close(); } @@ -265,8 +266,9 @@ static void OpenLogFile() static bool ReadConfig() { string iniPath; - if (!UIGetIniPath(iniPath)) + if (!UIGetIniPath(iniPath)) { return false; + } if (!ReadStringsFromFile(iniPath, gStrings, true)) return false; @@ -513,21 +515,17 @@ bool CheckEndOfLifed(string version) return UIFileExists(reportPath); } -#ifndef RELEASE_OR_BETA - static string -GetMinidumpAnalyzerPath() +GetProgramPath(const string& exename) { string path = gArgv[0]; size_t pos = path.rfind(UI_CRASH_REPORTER_FILENAME BIN_SUFFIX); path.erase(pos); - path.append(UI_MINIDUMP_ANALYZER_FILENAME BIN_SUFFIX); + path.append(exename + BIN_SUFFIX); return path; } -#endif - int main(int argc, char** argv) { gArgc = argc; @@ -552,7 +550,9 @@ int main(int argc, char** argv) #ifndef RELEASE_OR_BETA // start by running minidump analyzer, this is currently enabled only in // nightly and aurora - UIRunMinidumpAnalyzer(GetMinidumpAnalyzerPath(), gReporterDumpFile); + string empty; + UIRunProgram(GetProgramPath(UI_MINIDUMP_ANALYZER_FILENAME), + gReporterDumpFile, empty, /* wait */ true); #endif // go ahead with the crash reporter @@ -640,10 +640,17 @@ int main(int argc, char** argv) gEventsPath.clear(); } + // Assemble and send the crash ping + string pingUuid; + + if (SendCrashPing(queryParameters, pingUuid)) { + AppendToEventFile("CrashPingUUID", pingUuid); + } + // Update the crash event with stacks if they are present auto stackTracesItr = queryParameters.find("StackTraces"); if (stackTracesItr != queryParameters.end()) { - AppendStackTracesToEventFile(stackTracesItr->second); + AppendToEventFile(stackTracesItr->first, stackTracesItr->second); } if (!UIFileExists(gReporterDumpFile)) { diff --git a/toolkit/crashreporter/client/crashreporter.h b/toolkit/crashreporter/client/crashreporter.h index 8c5ca3613d71..e2363d0c0839 100644 --- a/toolkit/crashreporter/client/crashreporter.h +++ b/toolkit/crashreporter/client/crashreporter.h @@ -39,6 +39,11 @@ std::string WideToUTF8(const std::wstring& wide, bool* success = 0); #define UI_CRASH_REPORTER_FILENAME "crashreporter" #define UI_MINIDUMP_ANALYZER_FILENAME "minidump-analyzer" +#ifndef XP_MACOSX +#define UI_PING_SENDER_FILENAME "pingsender" +#else +#define UI_PING_SENDER_FILENAME "../../../pingsender" +#endif typedef std::map StringTable; @@ -79,7 +84,7 @@ typedef std::map StringTable; #define ST_ERROR_ENDOFLIFE "ErrorEndOfLife" //============================================================================= -// implemented in crashreporter.cpp +// implemented in crashreporter.cpp and ping.cpp //============================================================================= namespace CrashReporter { @@ -112,6 +117,9 @@ namespace CrashReporter { void DeleteDump(); bool ShouldEnableSending(); + // Telemetry ping + bool SendCrashPing(StringTable& strings, std::string& pingUuid); + static const unsigned int kSaveCount = 10; } @@ -148,8 +156,16 @@ std::ofstream* UIOpenWrite(const std::string& filename, bool append=false, bool binary=false); void UIPruneSavedDumps(const std::string& directory); -void UIRunMinidumpAnalyzer(const std::string& exename, - const std::string& filename); + +// Run the program specified by exename, passing it the parameter in arg and +// then sending it the contents of data (if any) via a pipe tied to its stdin. +// If wait is true, wait for the program to terminate execution before +// returning. Returns true if the program was launched correctly, false +// otherwise. +bool UIRunProgram(const std::string& exename, + const std::string& arg, + const std::string& data, + bool wait = false); #ifdef _MSC_VER # pragma warning( pop ) diff --git a/toolkit/crashreporter/client/crashreporter_unix_common.cpp b/toolkit/crashreporter/client/crashreporter_unix_common.cpp index f42a35616aac..c7adc903e4a4 100644 --- a/toolkit/crashreporter/client/crashreporter_unix_common.cpp +++ b/toolkit/crashreporter/client/crashreporter_unix_common.cpp @@ -70,16 +70,67 @@ void UIPruneSavedDumps(const std::string& directory) } } -void UIRunMinidumpAnalyzer(const string& exename, const string& filename) +bool UIRunProgram(const std::string& exename, const std::string& arg, + const std::string& data, bool wait) { - // Run the minidump analyzer and wait for it to finish + bool usePipes = !data.empty(); + int fd[2]; + + if (usePipes && (pipe(fd) != 0)) { + return false; + } + pid_t pid = fork(); if (pid == -1) { - return; // Nothing to do upon failure + return false; } else if (pid == 0) { - execl(exename.c_str(), exename.c_str(), filename.c_str(), nullptr); + // Child + if (usePipes) { + if (dup2(fd[0], STDIN_FILENO) == -1) { + exit(EXIT_FAILURE); + } + + close(fd[0]); + close(fd[1]); + } + + char* argv[] = { + const_cast(exename.c_str()), + const_cast(arg.c_str()), + nullptr + }; + + // Run the program + int rv = execv(exename.c_str(), argv); + + if (rv == -1) { + exit(EXIT_FAILURE); + } } else { - waitpid(pid, nullptr, 0); + // Parent + if (usePipes) { + ssize_t rv; + size_t offset = 0; + size_t size = data.size(); + + do { + rv = write(fd[1], data.c_str() + offset, size); + + if (rv > 0) { + size -= rv; + offset += rv; + } + } while (size && ((rv > 0) || ((rv == -1) && errno == EINTR))); + + close(fd[0]); + close(fd[1]); + } + + if (wait) { + waitpid(pid, nullptr, 0); + } } + + return true; } diff --git a/toolkit/crashreporter/client/crashreporter_win.cpp b/toolkit/crashreporter/client/crashreporter_win.cpp index 03ab3c4d3363..36fc0a0fa581 100644 --- a/toolkit/crashreporter/client/crashreporter_win.cpp +++ b/toolkit/crashreporter/client/crashreporter_win.cpp @@ -1545,24 +1545,81 @@ void UIPruneSavedDumps(const std::string& directory) } } -void UIRunMinidumpAnalyzer(const string& exename, const string& filename) +bool UIRunProgram(const string& exename, const string& arg, + const string& data, bool wait) { - wstring cmdLine; + bool usePipes = !data.empty(); + HANDLE childStdinPipeRead = nullptr; + HANDLE childStdinPipeWrite = nullptr; - cmdLine += L"\"" + UTF8ToWide(exename) + L"\" "; - cmdLine += L"\"" + UTF8ToWide(filename) + L"\" "; + if (usePipes) { + // Set up the pipes + SECURITY_ATTRIBUTES sa = {}; + sa.nLength = sizeof(SECURITY_ATTRIBUTES); + sa.lpSecurityDescriptor = nullptr; + sa.bInheritHandle = true; + + // Create a pipe for the child process's stdin and ensure its writable end is + // not inherited by the child process. + if (!CreatePipe(&childStdinPipeRead, &childStdinPipeWrite, &sa, 0)) { + return false; + } + + if (!SetHandleInformation(childStdinPipeWrite, HANDLE_FLAG_INHERIT, 0)) { + return false; + } + } + + wstring cmdLine = L"\"" + UTF8ToWide(exename) + L"\" " + + L"\"" + UTF8ToWide(arg) + L"\" "; STARTUPINFO si = {}; + si.cb = sizeof(si); + + if (usePipes) { + si.dwFlags |= STARTF_USESTDHANDLES; + si.hStdInput = childStdinPipeRead; + si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); + si.hStdError = GetStdHandle(STD_ERROR_HANDLE); + } + PROCESS_INFORMATION pi = {}; - si.cb = sizeof(si); - si.dwFlags = STARTF_USESHOWWINDOW; - si.wShowWindow = SW_SHOWNORMAL; - - if (CreateProcess(nullptr, (LPWSTR)cmdLine.c_str(), nullptr, nullptr, FALSE, - 0, nullptr, nullptr, &si, &pi)) { - WaitForSingleObject(pi.hProcess, INFINITE); - CloseHandle(pi.hProcess); - CloseHandle(pi.hThread); + if (!CreateProcess(/* lpApplicationName */ nullptr, + (LPWSTR)cmdLine.c_str(), + /* lpProcessAttributes */ nullptr, + /* lpThreadAttributes */ nullptr, + usePipes, + NORMAL_PRIORITY_CLASS | CREATE_NO_WINDOW, + /* lpEnvironment */ nullptr, + /* lpCurrentDirectory */ nullptr, + &si, &pi)) { + return false; } + + if (usePipes) { + size_t offset = 0; + size_t size = data.size(); + while (size > 0) { + DWORD written = 0; + bool success = WriteFile(childStdinPipeWrite, data.data() + offset, size, + &written, nullptr); + if (!success) { + break; + } else { + size -= written; + offset += written; + } + } + + CloseHandle(childStdinPipeWrite); + } + + if (wait) { + WaitForSingleObject(pi.hProcess, INFINITE); + } + + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + return true; } diff --git a/toolkit/crashreporter/client/moz.build b/toolkit/crashreporter/client/moz.build index 456c794af31d..9776d8d380eb 100644 --- a/toolkit/crashreporter/client/moz.build +++ b/toolkit/crashreporter/client/moz.build @@ -9,6 +9,15 @@ if CONFIG['OS_TARGET'] != 'Android': UNIFIED_SOURCES += [ 'crashreporter.cpp', + 'ping.cpp', + ] + + LOCAL_INCLUDES += [ + '/toolkit/crashreporter/jsoncpp/include', + ] + + USE_LIBS += [ + 'jsoncpp', ] if CONFIG['OS_ARCH'] == 'WINNT': @@ -22,6 +31,7 @@ if CONFIG['OS_ARCH'] == 'WINNT': ] OS_LIBS += [ 'comctl32', + 'ole32', 'shell32', 'wininet', 'shlwapi', diff --git a/toolkit/crashreporter/client/ping.cpp b/toolkit/crashreporter/client/ping.cpp new file mode 100644 index 000000000000..e402cb704de8 --- /dev/null +++ b/toolkit/crashreporter/client/ping.cpp @@ -0,0 +1,323 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "crashreporter.h" + +#include +#include + +#if defined(XP_LINUX) +#include +#include +#include +#elif defined(XP_MACOSX) +#include +#elif defined(XP_WIN) +#include +#endif + +#include "json/json.h" + +using std::string; + +namespace CrashReporter { + +struct UUID { + uint32_t m0; + uint16_t m1; + uint16_t m2; + uint8_t m3[8]; +}; + +// Generates an UUID; the code here is mostly copied from nsUUIDGenerator.cpp +static string +GenerateUUID() +{ + UUID id = {}; + +#if defined(XP_WIN) // Windows + HRESULT hr = CoCreateGuid((GUID*)&id); + if (FAILED(hr)) { + return ""; + } +#elif defined(XP_MACOSX) // MacOS X + CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault); + if (!uuid) { + return ""; + } + + CFUUIDBytes bytes = CFUUIDGetUUIDBytes(uuid); + memcpy(&id, &bytes, sizeof(UUID)); + + CFRelease(uuid); +#elif defined(HAVE_ARC4RANDOM_BUF) // Android, BSD, ... + arc4random_buf(id, sizeof(UUID)); +#else // Linux + int fd = open("/dev/urandom", O_RDONLY); + + if (fd == -1) { + return ""; + } + + if (read(fd, &id, sizeof(UUID)) != sizeof(UUID)) { + close(fd); + return ""; + } + + close(fd); +#endif + + /* Put in the version */ + id.m2 &= 0x0fff; + id.m2 |= 0x4000; + + /* Put in the variant */ + id.m3[0] &= 0x3f; + id.m3[0] |= 0x80; + + const char* kUUIDFormatString = + "%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x"; + const size_t kUUIDFormatStringLength = 36; + char str[kUUIDFormatStringLength + 1] = { '\0' }; + + int num = snprintf(str, kUUIDFormatStringLength + 1, kUUIDFormatString, + id.m0, id.m1, id.m2, id.m3[0], id.m3[1], id.m3[2], + id.m3[3], id.m3[4], id.m3[5], id.m3[6], id.m3[7]); + + if (num != kUUIDFormatStringLength) { + return ""; + } + + return str; +} + +const char kISO8601Date[] = "%F"; +const char kISO8601FullDate[] = "%FT%T.000Z"; + +// Return the current date as a string in the specified format, the following +// constants are provided: +// - kISO8601Date, the ISO 8601 date format, YYYY-MM-DD +// - kISO8601FullDate, the ISO 8601 full date format, YYYY-MM-DDTHH:MM:SS.000Z +static string +CurrentDate(string format) +{ + time_t now; + time(&now); + char buf[64]; // This should be plenty + strftime(buf, sizeof buf, format.c_str(), gmtime(&now)); + return buf; +} + +const char kTelemetryClientId[] = "TelemetryClientId"; +const char kTelemetryUrl[] = "TelemetryServerURL"; +const char kTelemetrySessionId[] = "TelemetrySessionId"; +const int kTelemetryVersion = 4; + +// Create the payload.metadata node of the crash ping using fields extracted +// from the .extra file +static Json::Value +CreateMetadataNode(StringTable& strings) +{ + // The following list should be kept in sync with the one in CrashManager.jsm + const char *entries[] = { + "AvailablePageFile", + "AvailablePhysicalMemory", + "AvailableVirtualMemory", + "BlockedDllList", + "BlocklistInitFailed", + "BuildID", + "ContainsMemoryReport", + "CrashTime", + "EventLoopNestingLevel", + "IsGarbageCollecting", + "MozCrashReason", + "OOMAllocationSize", + "ProductID", + "ProductName", + "ReleaseChannel", + "SecondsSinceLastCrash", + "StartupCrash", + "SystemMemoryUsePercentage", + "TelemetrySessionId", + "TextureUsage", + "TotalPageFile", + "TotalPhysicalMemory", + "TotalVirtualMemory", + "UptimeTS", + "User32BeforeBlocklist", + "Version", + }; + + Json::Value node; + + for (auto entry : entries) { + if ((strings.find(entry) != strings.end()) && !strings[entry].empty()) { + node[entry] = strings[entry]; + } + } + + return node; +} + +// Create the payload node of the crash ping +static Json::Value +CreatePayloadNode(StringTable& strings, const string& aSessionId) +{ + Json::Value payload; + + payload["sessionId"] = aSessionId; + payload["version"] = 1; + payload["crashDate"] = CurrentDate(kISO8601Date); + payload["hasCrashEnvironment"] = true; + payload["crashId"] = GetDumpLocalID(); + payload["processType"] = "main"; // This is always a main crash + + // Parse the stack traces + Json::Value stackTracesValue; + Json::Reader reader; + + if (reader.parse(strings["StackTraces"], stackTracesValue, + /* collectComments */ false)) { + payload["stackTraces"] = stackTracesValue; + } + + // Assemble the payload metadata + payload["metadata"] = CreateMetadataNode(strings); + + return payload; +} + +// Create the application node of the crash ping +static Json::Value +CreateApplicationNode(const string& aVendor, const string& aName, + const string& aVersion, const string& aChannel, + const string& aBuildId, const string& aArchitecture, + const string& aXpcomAbi) +{ + Json::Value application; + + application["vendor"] = aVendor; + application["name"] = aName; + application["buildId"] = aBuildId; + application["displayVersion"] = aVersion; + application["platformVersion"] = aVersion; + application["version"] = aVersion; + application["channel"] = aChannel; + if (!aArchitecture.empty()) { + application["architecture"] = aArchitecture; + } + if (!aXpcomAbi.empty()) { + application["xpcomAbi"] = aXpcomAbi; + } + + return application; +} + +// Create the root node of the crash ping +static Json::Value +CreateRootNode(StringTable& strings, const string& aUuid, + const string& aClientId, const string& aSessionId, + const string& aName, const string& aVersion, + const string& aChannel, const string& aBuildId) +{ + Json::Value root; + root["type"] = "crash"; // This is a crash ping + root["id"] = aUuid; + root["version"] = kTelemetryVersion; + root["creationDate"] = CurrentDate(kISO8601FullDate); + root["clientId"] = aClientId; + + // Parse the telemetry environment + Json::Value environment; + Json::Reader reader; + string architecture; + string xpcomAbi; + + if (reader.parse(strings["TelemetryEnvironment"], environment, + /* collectComments */ false)) { + if (environment.isMember("build") && environment["build"].isObject()) { + Json::Value build = environment["build"]; + if (build.isMember("architecture") && build["architecture"].isString()) { + architecture = build["architecture"].asString(); + } + if (build.isMember("xpcomAbi") && build["xpcomAbi"].isString()) { + xpcomAbi = build["xpcomAbi"].asString(); + } + } + + root["environment"] = environment; + } + + root["payload"] = CreatePayloadNode(strings, aSessionId); + root["application"] = CreateApplicationNode(strings["Vendor"], aName, + aVersion, aChannel, aBuildId, + architecture, xpcomAbi); + + return root; +} + +// Generates the URL used to submit the crash ping, see TelemetrySend.jsm +string +GenerateSubmissionUrl(const string& aUrl, const string& aId, + const string& aName, const string& aVersion, + const string& aChannel, const string& aBuildId) +{ + return aUrl + "/submit/telemetry/" + aId + "/crash/" + aName + "/" + + aVersion + "/" + aChannel + "/" + aBuildId + + "?v=" + std::to_string(kTelemetryVersion); +} + +// Assembles the crash ping using the strings extracted from the .extra file +// and sends it using the crash sender. All the telemetry specific data but the +// environment will be stripped from the annotations so that it won't be sent +// together with the crash report. +// +// Note that the crash ping sender is invoked in a fire-and-forget way so this +// won't block waiting for the ping to be delivered. +// +// Returns true if the ping was assembled and handed over to the pingsender +// correctly, false otherwise and populates the aUUID field with the ping UUID. +bool +SendCrashPing(StringTable& strings, string& pingUuid) +{ + string clientId = strings[kTelemetryClientId]; + string serverUrl = strings[kTelemetryUrl]; + string sessionId = strings[kTelemetrySessionId]; + + // Remove the telemetry-related data from the crash annotations + strings.erase(kTelemetryClientId); + strings.erase(kTelemetryUrl); + strings.erase(kTelemetrySessionId); + + string buildId = strings["BuildID"]; + string channel = strings["ReleaseChannel"]; + string name = strings["ProductName"]; + string version = strings["Version"]; + string uuid = GenerateUUID(); + string url = GenerateSubmissionUrl(serverUrl, uuid, name, version, + channel, buildId); + + if (serverUrl.empty() || uuid.empty()) { + return false; + } + + Json::Value root = CreateRootNode(strings, uuid, clientId, sessionId, + name, version, channel, buildId); + + // Write out the result + Json::FastWriter writer; + string ping = writer.write(root); + + // Hand over the ping to the sender + if (UIRunProgram(GetProgramPath(UI_PING_SENDER_FILENAME), url, ping)) { + pingUuid = uuid; + return true; + } else { + return false; + } +} + +} // namespace crashreporter + diff --git a/toolkit/crashreporter/docs/index.rst b/toolkit/crashreporter/docs/index.rst index 37933ca85de5..87794406b31e 100644 --- a/toolkit/crashreporter/docs/index.rst +++ b/toolkit/crashreporter/docs/index.rst @@ -38,6 +38,12 @@ Minidump Analyzer the dump files generated during a crash. It appends the stack traces to the .extra file associated with the crash dump. +Ping Sender + The ping sender is a standalone executable that is launched by the crash + reporter client to deliver a crash ping to our telemetry servers. The ping + sender is used to speed up delivery of the crash ping which would otherwise + have to wait for Firefox to be restarted in order to be sent. + How Main-Process Crash Handling Works ===================================== @@ -90,7 +96,13 @@ argument. The *crash reporter client* performs a number of roles. There's a lot going on, so you may want to look at ``main()`` in ``crashreporter.cpp``. First, stack traces are extracted from the dump via the *minidump analyzer* tool. -The resulting traces are appended to the .extra file of the crash. Then, the +The resulting traces are appended to the .extra file of the crash. Once this +is done a crash ping is assembled holding the same information as the one +generated by the ```CrashManager``` and it's sent to the telemetry servers via +the *ping sender* program. The UUID of the ping is then stored in the extra +file; the ```CrashManager``` will later pick it up and generate a new ping +with the same UUID so that the telemetry server can deduplicate both pings. +Then, the *crash reporter client* verifies that the dump data is sane. If it isn't (e.g. required metadata is missing), the dump data is ignored. If dump data looks sane, the dump data diff --git a/toolkit/crashreporter/nsExceptionHandler.cpp b/toolkit/crashreporter/nsExceptionHandler.cpp index b34d109f1f2d..f9f6b2ef2477 100644 --- a/toolkit/crashreporter/nsExceptionHandler.cpp +++ b/toolkit/crashreporter/nsExceptionHandler.cpp @@ -1097,9 +1097,6 @@ bool MinidumpCallback( WriteLiteral(eventFile, "\n"); WriteString(eventFile, id_ascii); WriteLiteral(eventFile, "\n"); - if (currentSessionId) { - WriteAnnotation(eventFile, "TelemetrySessionId", currentSessionId); - } if (crashEventAPIData) { eventFile.WriteBuffer(crashEventAPIData->get(), crashEventAPIData->Length()); } @@ -1114,6 +1111,12 @@ bool MinidumpCallback( #endif apiData.WriteBuffer(crashReporterAPIData->get(), crashReporterAPIData->Length()); } + + if (currentSessionId) { + WriteAnnotation(apiData, "TelemetrySessionId", crashTimeString); + WriteAnnotation(eventFile, "TelemetrySessionId", currentSessionId); + } + WriteAnnotation(apiData, "CrashTime", crashTimeString); WriteAnnotation(eventFile, "CrashTime", crashTimeString); diff --git a/toolkit/crashreporter/test/unit/test_crashreporter_crash.js b/toolkit/crashreporter/test/unit/test_crashreporter_crash.js index ed17af35b9fc..3cb063f2e974 100644 --- a/toolkit/crashreporter/test/unit/test_crashreporter_crash.js +++ b/toolkit/crashreporter/test/unit/test_crashreporter_crash.js @@ -45,6 +45,30 @@ function run_test() { do_check_eq(extra.TestKey, "TestValue"); do_check_eq(extra["\u2665"], "\u{1F4A9}"); do_check_eq(extra.Notes, "JunkMoreJunk"); - do_check_true(!("TelemetrySessionId" in extra)); + do_check_true("TelemetrySessionId" in extra); + Assert.ok( + "TelemetryServerURL" in extra, + "The TelemetryServerURL field is omitted when telemetry is off" + ); }); + + do_crash(function() { + // Enable telemetry + let prefs = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + + // Turn on telemetry and specify a telemetry server + prefs.setCharPref("toolkit.telemetry.server", "http://a.telemetry.server"); + prefs.setBoolPref("toolkit.telemetry.enabled", true); + + // TelemetrySession setup will trigger the session annotation + let scope = {}; + Components.utils.import("resource://gre/modules/TelemetryController.jsm", scope); + scope.TelemetryController.testSetup(); + }, function(mdump, extra) { + Assert.ok("TelemetryServerURL" in extra, + "The TelemetryServerURL field is present in the extra file"); + Assert.equal(extra.TelemetryServerURL, "http://a.telemetry.server", + "The TelemetryServerURL field is properly set"); + }); }