FlightGear next
sentryIntegration.cxx
Go to the documentation of this file.
1// sentryIntegration.cxx - Interface with Sentry.io crash reporting
2//
3// Copyright (C) 2020 James Turner james@flightgear.org
4//
5// This program is free software; you can redistribute it and/or
6// modify it under the terms of the GNU General Public License as
7// published by the Free Software Foundation; either version 2 of the
8// License, or (at your option) any later version.
9//
10// This program is distributed in the hope that it will be useful, but
11// WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13// General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with this program; if not, write to the Free Software
17// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
19#include "config.h"
20
21#include "sentryIntegration.hxx"
22
23#include <cstring> // for strcmp
24
25#include <simgear/debug/LogCallback.hxx>
26#include <simgear/debug/logstream.hxx>
27#include <simgear/debug/ErrorReportingCallback.hxx>
28#include <simgear/debug/Reporting.hxx>
29
30#include <simgear/misc/sg_path.hxx>
31#include <simgear/props/props.hxx>
32#include <simgear/structure/commands.hxx>
33#include <simgear/structure/exception.hxx>
34#include <simgear/io/iostreams/sgstream.hxx>
35
36#include <Main/fg_init.hxx>
37#include <Main/fg_props.hxx>
38#include <Main/globals.hxx>
39
40#include <flightgearBuildId.h>
41
42bool doesStringMatchPrefixes(const std::string& s, const std::initializer_list<const char*>& prefixes)
43{
44 if (s.empty())
45 return false;
46
47
48 for (auto c : prefixes) {
49 if (s.find(c) == 0)
50 return true;
51 }
52
53 return false;
54}
55
57 "PNG lib warning : iCCP: known incorrect sRGB profile",
58 "PNG lib warning : iCCP: profile 'ICC Profile': 1000000h: invalid rendering intent",
59 "osgDB ac3d reader: detected surface with less than 3",
60 "osgDB ac3d reader: detected line with less than 2",
61 "Detected particle system using segment(s) with less than 2 vertices"
62};
63
65 "position is invalid, NaNs",
66 "bad AI flight plan",
67 "couldn't find shader",
68
71 "metar data bogus",
72 "metar data incomplete",
73 "metar temperature data",
74 "metar pressure data"};
75
76// we don't want sentry enabled for the test suite
77#if defined(HAVE_SENTRY) && !defined(BUILDING_TESTSUITE)
78
79static bool static_sentryEnabled = false;
80static std::string static_sentryUUID;
81
82#include <sentry.h>
83
84namespace {
85
86// this callback is invoked whenever an instance of sg_throwable is created.
87// therefore we can log the stack trace at that point
88void sentryTraceSimgearThrow(const std::string& msg, const std::string& origin, const sg_location& loc)
89{
90 if (!static_sentryEnabled)
91 return;
92
94 return;
95 }
96
97 sentry_value_t exc = sentry_value_new_object();
98 sentry_value_set_by_key(exc, "type", sentry_value_new_string("Exception"));
99
100 std::string message = msg;
101 sentry_value_t info = sentry_value_new_object();
102 if (!origin.empty()) {
103 sentry_value_set_by_key(info, "origin", sentry_value_new_string(origin.c_str()));
104 }
105
106 if (loc.isValid()) {
107 const auto ls = loc.asString();
108 sentry_value_set_by_key(info, "location", sentry_value_new_string(ls.c_str()));
109 }
110
111 sentry_set_context("what", info);
112 sentry_value_set_by_key(exc, "value", sentry_value_new_string(message.c_str()));
113
114 sentry_value_t event = sentry_value_new_event();
115 sentry_value_set_by_key(event, "exception", exc);
116
117 sentry_event_value_add_stacktrace(event, nullptr, 0);
118 sentry_capture_event(event);
119}
120
121class SentryLogCallback : public simgear::LogCallback
122{
123public:
124 SentryLogCallback() : simgear::LogCallback(SG_ALL, SG_WARN)
125 {
126 }
127
128 bool doProcessEntry(const simgear::LogEntry& e) override
129 {
130 // we need original priority here, so we don't record MANDATORY_INFO
131 // or DEV_ messages, which would get noisy.
132 const auto op = e.originalPriority;
133 if ((op != SG_WARN) && (op != SG_ALERT)) {
134 return true;
135 }
136
137 if ((e.debugClass == SG_OSG) && doesStringMatchPrefixes(e.message, OSG_messageWhitelist)) {
138 return true;
139 }
140
141 if (e.message == _lastLoggedMessage) {
142 _lastLoggedCount++;
143 return true;
144 }
145
146 if (_lastLoggedCount > 0) {
147 flightgear::addSentryBreadcrumb("(repeats " + std::to_string(_lastLoggedCount) + " times)", "info");
148 _lastLoggedCount = 0;
149 }
150
151 _lastLoggedMessage = e.message;
152 flightgear::addSentryBreadcrumb(e.message, (op == SG_WARN) ? "warning" : "error");
153 return true;
154 }
155
156private:
157 std::string _lastLoggedMessage;
158 int _lastLoggedCount = 0;
159};
160
161const auto missingShaderPrefix = std::string{"Missing shader"};
162
163string_list anon_missingShaderList;
164
165bool isNewMissingShader(const std::string& path)
166{
167 auto it = std::find(anon_missingShaderList.begin(), anon_missingShaderList.end(), path);
168 if (it != anon_missingShaderList.end()) {
169 return false;
170 }
171
172 anon_missingShaderList.push_back(path);
173 return true;
174}
175
176void sentrySimgearReportCallback(const std::string& msg, const std::string& more, bool isFatal)
177{
178 // don't duplicate reports for missing shaders, once per sessions
179 // is sufficient
180 using simgear::strutils::starts_with;
181 if (starts_with(msg, missingShaderPrefix)) {
182 if (!isNewMissingShader(more)) {
183 return;
184 }
185 }
186
187 sentry_value_t exc = sentry_value_new_object();
188 if (isFatal) {
189 sentry_value_set_by_key(exc, "type", sentry_value_new_string("Fatal Error"));
190 } else {
191 sentry_value_set_by_key(exc, "type", sentry_value_new_string("Exception"));
192 }
193
194 sentry_value_set_by_key(exc, "value", sentry_value_new_string(msg.c_str()));
195
196 sentry_value_t event = sentry_value_new_event();
197 sentry_value_set_by_key(event, "exception", exc);
198
199 sentry_event_value_add_stacktrace(event, nullptr, 0);
200 sentry_capture_event(event);
201}
202
203void sentryReportBadAlloc()
204{
205 if (simgear::ReportBadAllocGuard::isSet()) {
206 sentry_value_t sentryMessage = sentry_value_new_object();
207 sentry_value_set_by_key(sentryMessage, "type", sentry_value_new_string("Fatal Error"));
208 sentry_value_set_by_key(sentryMessage, "formatted", sentry_value_new_string("bad allocation"));
209
210 sentry_value_t event = sentry_value_new_event();
211 sentry_value_set_by_key(event, "message", sentryMessage);
212
213 sentry_event_value_add_stacktrace(event, nullptr, 0);
214 sentry_capture_event(event);
215 }
216
217 throw std::bad_alloc(); // allow normal processing
218}
219
220} // namespace
221
222namespace flightgear
223{
224
225bool sentryReportCommand(const SGPropertyNode* args, SGPropertyNode* root)
226{
227 if (!static_sentryEnabled) {
228 SG_LOG(SG_GENERAL, SG_WARN, "Sentry.io not enabled at startup");
229 return false;
230 }
231
232 sentry_value_t exc = sentry_value_new_object();
233 sentry_value_set_by_key(exc, "type", sentry_value_new_string("Report"));
234
235 const auto message = args->getStringValue("message");
236 sentry_value_set_by_key(exc, "value", sentry_value_new_string(message.c_str()));
237
238 sentry_value_t event = sentry_value_new_event();
239 sentry_value_set_by_key(event, "exception", exc);
240 // capture the C++ stack-trace. Probably not that useful but can't hurt
241 sentry_event_value_add_stacktrace(event, nullptr, 0);
242
243 sentry_capture_event(event);
244
245 return true;
246}
247
248bool sentrySendError(const SGPropertyNode* args, SGPropertyNode* root)
249{
250 if (!static_sentryEnabled) {
251 SG_LOG(SG_GENERAL, SG_WARN, "Sentry.io not enabled at startup");
252 return false;
253 }
254
255 try {
256 throw sg_io_exception("Invalid flurlbe", sg_location("/Some/dummy/path/bar.txt", 100, 200));
257 } catch (sg_exception& e) {
258 SG_LOG(SG_GENERAL, SG_WARN, "caught dummy exception");
259 }
260
261 return true;
262}
263
264std::string sentryUserId()
265{
266 if (!static_sentryUUID.empty()) {
267 return static_sentryUUID;
268 }
269
270 const auto uuidPath = fgHomePath() / "sentry_uuid.txt";
271 if (!uuidPath.exists()) {
272 return {};
273 }
274
275 sg_ifstream f(uuidPath);
276 std::getline(f, static_sentryUUID);
277 return static_sentryUUID;
278}
279
280void initSentry()
281{
282 sentry_options_t *options = sentry_options_new();
283 // API key is defined in config.h, set in an environment variable prior
284 // to running CMake, so it can be customised. Env var at build time is:
285 // FLIGHTGEAR_SENTRY_API_KEY
286 sentry_options_set_dsn(options, SENTRY_API_KEY);
287
288 if (strcmp(FG_BUILD_TYPE, "Dev") == 0) {
289 sentry_options_set_release(options, "flightgear-dev@" REVISION);
290 } else if (strcmp(FG_BUILD_TYPE, "Nightly") == 0) {
291 sentry_options_set_release(options, "flightgear-nightly@" BUILD_DATE);
292 } else {
293 sentry_options_set_release(options, "flightgear@" FLIGHTGEAR_VERSION);
294 }
295
296 sentry_options_set_dist(options, REVISION);
297
298 // for dev / nightly builds, put Sentry in debug mode
299 if (strcmp(FG_BUILD_TYPE, "Release")) {
300 sentry_options_set_debug(options, 1);
301 }
302
303 SGPath dataPath = fgHomePath() / "sentry_db";
304#if defined(SG_WINDOWS)
305 const auto homePathString = dataPath.wstr();
306 sentry_options_set_database_pathw(options, homePathString.c_str());
307
308 const auto logPath = (fgHomePath() / "fgfs.log").wstr();
309 sentry_options_add_attachmentw(options, logPath.c_str());
310#else
311 const auto homePathString = dataPath.utf8Str();
312 sentry_options_set_database_path(options, homePathString.c_str());
313
314 const auto logPath = (fgHomePath() / "fgfs.log").utf8Str();
315 sentry_options_add_attachment(options, logPath.c_str());
316#endif
317
318 const auto uuidPath = fgHomePath() / "sentry_uuid.txt";
319 bool generateUuid = true;
320 std::string uuid;
321 if (uuidPath.exists()) {
322 sentryUserId(); // will cache into static_sentryUUID as a side-effect
323
324 // if we read enough bytes, that this is a valid UUID, then accept it
325 if ( static_sentryUUID.length() >= 36) {
326 generateUuid = false;
327 }
328 }
329
330 // we need to generate a new UUID
331 if (generateUuid) {
332 // use the Sentry APi to generate one
333 sentry_uuid_t su = sentry_uuid_new_v4();
334 char bytes[38];
335 sentry_uuid_as_string(&su, bytes);
336 bytes[37] = 0;
337
338 static_sentryUUID = std::string{bytes};
339 // write it back to disk for next time
340 sg_ofstream f(uuidPath);
341 f << static_sentryUUID << std::endl;
342 }
343
344 if (sentry_init(options) == 0) {
345 static_sentryEnabled = true;
346 sentry_value_t user = sentry_value_new_object();
347 sentry_value_t userUuidV = sentry_value_new_string(static_sentryUUID.c_str());
348 sentry_value_set_by_key(user, "id", userUuidV);
349 sentry_set_user(user);
350
351 sglog().addCallback(new SentryLogCallback);
352 setThrowCallback(sentryTraceSimgearThrow);
353 simgear::setErrorReportCallback(sentrySimgearReportCallback);
354
355 std::set_new_handler(sentryReportBadAlloc);
356 } else {
357 SG_LOG(SG_GENERAL, SG_WARN, "Failed to init Sentry reporting");
358 static_sentryEnabled = false;
359 }
360}
361
363{
364 if (!static_sentryEnabled)
365 return;
366
367 // allow the user to opt-out of sentry.io features
368 if (!fgGetBool("/sim/startup/sentry-crash-reporting-enabled", true)) {
369 SG_LOG(SG_GENERAL, SG_INFO, "Disabling Sentry.io reporting");
370 sentry_shutdown();
371 static_sentryEnabled = false;
372 return;
373 }
374
375 globals->get_commands()->addCommand("sentry-report", &sentryReportCommand);
376 globals->get_commands()->addCommand("sentry-exception", &sentrySendError);
377
378 // expose the anonymous user UUID to the property tree, so users
379 // can share it if they wish
380 fgSetString("/sim/crashreport/sentry-user-id", static_sentryUUID);
381}
382
383void shutdownSentry()
384{
385 if (static_sentryEnabled) {
386 sentry_shutdown();
387 static_sentryEnabled = false;
388 }
389}
390
391bool isSentryEnabled()
392{
393 return static_sentryEnabled;
394}
395
396void addSentryBreadcrumb(const std::string& msg, const std::string& level)
397{
398 if (!static_sentryEnabled)
399 return;
400
401 sentry_value_t crumb = sentry_value_new_breadcrumb("default", msg.c_str());
402 sentry_value_set_by_key(crumb, "level", sentry_value_new_string(level.c_str()));
403 sentry_add_breadcrumb(crumb);
404}
405
406void addSentryTag(const char* tag, const char* value)
407{
408 if (!static_sentryEnabled)
409 return;
410
411 if (!tag || !value)
412 return;
413
414 sentry_set_tag(tag, value);
415}
416
417void updateSentryTag(const std::string& tag, const std::string& value)
418{
419 if (tag.empty() || value.empty())
420 return;
421
422 if (!static_sentryEnabled)
423 return;
424
425 sentry_remove_tag(tag.c_str());
426 sentry_set_tag(tag.c_str(), value.c_str());
427}
428
429void sentryReportNasalError(const std::string& msg, const string_list& stack)
430{
431 if (!static_sentryEnabled)
432 return;
433#if 0
434 sentry_value_t exc = sentry_value_new_object();
435 sentry_value_set_by_key(exc, "type", sentry_value_new_string("Exception"));
436 sentry_value_set_by_key(exc, "value", sentry_value_new_string(msg.c_str()));
437
438 sentry_value_t stackData = sentry_value_new_list();
439 for (const auto& nasalFrame : stack) {
440 sentry_value_append(stackData, sentry_value_new_string(nasalFrame.c_str()));
441 }
442 sentry_value_set_by_key(exc, "stack", stackData);
443
444
445 sentry_value_t event = sentry_value_new_event();
446 sentry_value_set_by_key(event, "exception", exc);
447
448 // add the Nasal stack trace data
449
450 // capture the C++ stack-trace. Probably not that useful but can't hurt
451 sentry_event_value_add_stacktrace(event, nullptr, 0);
452
453 sentry_capture_event(event);
454
455#endif
456}
457
458void sentryReportException(const std::string& msg, const std::string& location)
459{
460 if (!static_sentryEnabled)
461 return;
462
463 sentry_value_t exc = sentry_value_new_object();
464 sentry_value_set_by_key(exc, "type", sentry_value_new_string("Exception"));
465
466
467 sentry_value_t info = sentry_value_new_object();
468 if (!location.empty()) {
469 sentry_value_set_by_key(info, "location", sentry_value_new_string(location.c_str()));
470 }
471 sentry_set_context("what", info);
472
473 sentry_value_set_by_key(exc, "value", sentry_value_new_string(msg.c_str()));
474
475 sentry_value_t event = sentry_value_new_event();
476 sentry_value_set_by_key(event, "exception", exc);
477
478 // capture the C++ stack-trace. Probably not that useful but can't hurt
479 sentry_event_value_add_stacktrace(event, nullptr, 0);
480 sentry_capture_event(event);
481}
482
483void sentryReportFatalError(const std::string& msg, const std::string& more)
484{
485 if (!static_sentryEnabled)
486 return;
487
488 sentry_value_t sentryMessage = sentry_value_new_object();
489 sentry_value_set_by_key(sentryMessage, "type", sentry_value_new_string("Fatal Error"));
490
491 sentry_value_t info = sentry_value_new_object();
492 if (!more.empty()) {
493 sentry_value_set_by_key(info, "more", sentry_value_new_string(more.c_str()));
494 }
495
496 sentry_set_context("what", info);
497 sentry_value_set_by_key(sentryMessage, "formatted", sentry_value_new_string(msg.c_str()));
498
499 sentry_value_t event = sentry_value_new_event();
500 sentry_value_set_by_key(event, "message", sentryMessage);
501
502 sentry_event_value_add_stacktrace(event, nullptr, 0);
503 sentry_capture_event(event);
504}
505
506void sentryReportUserError(const std::string& aggregate, const std::string& parameter, const std::string& details)
507{
508 if (!static_sentryEnabled)
509 return;
510
511 sentry_value_t sentryMessage = sentry_value_new_object();
512 sentry_value_set_by_key(sentryMessage, "type", sentry_value_new_string("Error"));
513
514 sentry_value_t info = sentry_value_new_object();
515 sentry_value_set_by_key(info, "details", sentry_value_new_string(details.c_str()));
516
517 sentry_set_context("what", info);
518
519 auto m = aggregate;
520 if (!parameter.empty()) {
521 m += ":" + parameter;
522 }
523
524 sentry_value_t event = sentry_value_new_event();
525 sentry_value_set_by_key(event, "message", sentry_value_new_string(m.c_str()));
526
527 sentry_capture_event(event);
528}
529
530} // of namespace
531
532#else
533
534// stubs for non-sentry case
535
536namespace flightgear
537{
538
540{
541}
542
544{
545}
546
548{
549}
550
551std::string sentryUserId()
552{
553 return {};
554}
555
557{
558 return false;
559}
560
561void addSentryBreadcrumb(const std::string&, const std::string&)
562{
563}
564
565void addSentryTag(const char*, const char*)
566{
567}
568
569void updateSentryTag(const std::string&, const std::string&)
570{
571}
572
573
574void sentryReportNasalError(const std::string&, const string_list&)
575{
576}
577
578void sentryReportException(const std::string&, const std::string&)
579{
580}
581
582void sentryReportFatalError(const std::string&, const std::string&)
583{
584}
585
586void sentryReportUserError(const std::string&, const std::string&, const std::string&)
587{
588}
589
590} // of namespace
591
592#endif
593
594// common helpers
595
596namespace flightgear
597{
598
599void addSentryTag(const std::string& tag, const std::string& value)
600{
601 if (tag.empty() || value.empty())
602 return;
603
604 addSentryTag(tag.c_str(), value.c_str());
605}
606
607} // of namespace flightgear
bool options(int, char **)
Definition JSBSim.cpp:568
SGCommandMgr * get_commands()
Definition globals.hxx:330
SGPath fgHomePath()
Definition fg_init.cxx:524
FGGlobals * globals
Definition globals.cxx:142
std::vector< std::string > string_list
Definition globals.hxx:36
FlightPlan.hxx - defines a full flight-plan object, including departure, cruise, arrival information ...
Definition Addon.cxx:53
void sentryReportUserError(const std::string &, const std::string &, const std::string &)
void updateSentryTag(const std::string &, const std::string &)
void addSentryBreadcrumb(const std::string &, const std::string &)
std::string sentryUserId()
retrive the anonymous user ID (a UUID) for this installation.
void addSentryTag(const char *, const char *)
void sentryReportException(const std::string &, const std::string &)
void sentryReportNasalError(const std::string &, const string_list &)
void sentryReportFatalError(const std::string &, const std::string &)
bool fgGetBool(char const *name, bool def)
Get a bool value for a property.
Definition proptest.cpp:25
bool fgSetString(char const *name, char const *str)
Set a string value for a property.
Definition proptest.cpp:26
auto OSG_messageWhitelist
bool doesStringMatchPrefixes(const std::string &s, const std::initializer_list< const char * > &prefixes)
auto exception_messageWhitelist