diff --git a/CHANGELOG.md b/CHANGELOG.md index de36b19f2..d95b8253a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Enable structured logs by default; logs are now opt-out via `sentry_options_set_enable_logs(options, false)`. ([#1673](https://github.com/getsentry/sentry-native/pull/1673)) - Crashpad: add macOS support for the `crashpad_wait_for_upload` flag. ([#1679](https://github.com/getsentry/sentry-native/pull/1679), [crashpad#152](https://github.com/getsentry/crashpad/pull/152)) +- Crashpad: add experimental support for large attachment uploads, opt-in via `sentry_options_set_enable_large_attachments`. ([#1674](https://github.com/getsentry/sentry-native/pull/1674), [crashpad#151](https://github.com/getsentry/crashpad/pull/151)) **Fixes**: diff --git a/examples/example.c b/examples/example.c index 34f002c77..9c68643bb 100644 --- a/examples/example.c +++ b/examples/example.c @@ -565,6 +565,25 @@ main(int argc, char **argv) sentry_options_add_attachment(options, "./CMakeCache.txt"); } + if (has_arg(argc, argv, "large-attachment")) { + sentry_options_set_enable_large_attachments(options, 1); + const char *large_file = ".sentry-large-attachment"; + FILE *f = fopen(large_file, "wb"); + if (f) { + char zeros[4096]; + memset(zeros, 0, sizeof(zeros)); + size_t remaining = 100 * 1024 * 1024; + while (remaining > 0) { + size_t chunk + = remaining < sizeof(zeros) ? remaining : sizeof(zeros); + fwrite(zeros, 1, chunk, f); + remaining -= chunk; + } + fclose(f); + sentry_options_add_attachment(options, large_file); + } + } + if (has_arg(argc, argv, "stdout")) { sentry_options_set_transport( options, sentry_transport_new(print_envelope)); diff --git a/external/crashpad b/external/crashpad index 17b7aca16..18c866648 160000 --- a/external/crashpad +++ b/external/crashpad @@ -1 +1 @@ -Subproject commit 17b7aca1634f1a91018f1bba13f7941a2892e864 +Subproject commit 18c866648945ade8705a99bf5f372e4b1183e624 diff --git a/include/sentry.h b/include/sentry.h index 3dde7bfea..28e1ac255 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2351,6 +2351,21 @@ SENTRY_EXPERIMENTAL_API void sentry_options_set_http_retry( SENTRY_EXPERIMENTAL_API int sentry_options_get_http_retry( const sentry_options_t *opts); +/** + * Enables or disables out-of-band upload of large attachments. + * + * When enabled, attachments above an internal size threshold are uploaded + * via a separate request before the envelope is sent, and referenced from + * the envelope by location instead of being embedded inline. When disabled, + * all attachments are embedded in the envelope regardless of size. + * + * Disabled by default. + */ +SENTRY_EXPERIMENTAL_API void sentry_options_set_enable_large_attachments( + sentry_options_t *opts, int enable_large_attachments); +SENTRY_EXPERIMENTAL_API int sentry_options_get_enable_large_attachments( + const sentry_options_t *opts); + /** * Enables or disables custom attributes parsing for structured logging. * diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index e63dd1fe6..83a4a8740 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -707,6 +707,9 @@ crashpad_backend_startup( } std::vector arguments { "--no-rate-limit" }; + if (options->enable_large_attachments) { + arguments.push_back("--enable-large-attachments"); + } // Map sentry's log level to mini_chromium's LogSeverity. They diverge at // FATAL (sentry=3, mini_chromium=4); otherwise 1:1. diff --git a/src/sentry_options.c b/src/sentry_options.c index 995e407ce..100015cfe 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -96,6 +96,7 @@ sentry_options_new(void) // both worlds opts->http_retry = false; opts->send_client_reports = true; + opts->enable_large_attachments = false; return opts; } @@ -873,6 +874,19 @@ sentry_options_get_enable_metrics(const sentry_options_t *opts) return opts->enable_metrics; } +void +sentry_options_set_enable_large_attachments( + sentry_options_t *opts, int enable_large_attachments) +{ + opts->enable_large_attachments = !!enable_large_attachments; +} + +int +sentry_options_get_enable_large_attachments(const sentry_options_t *opts) +{ + return opts->enable_large_attachments; +} + void sentry_options_set_before_send_metric(sentry_options_t *opts, sentry_before_send_metric_function_t func, void *user_data) diff --git a/src/sentry_options.h b/src/sentry_options.h index 86ee949c2..0a063a3aa 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -82,6 +82,7 @@ struct sentry_options_s { void *before_send_metric_data; bool http_retry; bool send_client_reports; + bool enable_large_attachments; /* everything from here on down are options which are stored here but not exposed through the options API */ diff --git a/tests/__init__.py b/tests/__init__.py index b98e38456..5b9603b3a 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -350,16 +350,21 @@ def deserialize_from( headers = json.loads(line) length = headers["length"] payload = f.read(length) - if headers.get("type") in [ - "event", - "feedback", - "session", - "transaction", - "user_report", - "log", - "trace_metric", - "client_report", - ]: + if ( + headers.get("type") + in [ + "event", + "feedback", + "session", + "transaction", + "user_report", + "log", + "trace_metric", + "client_report", + ] + or headers.get("content_type") + == "application/vnd.sentry.attachment-ref+json" + ): rv = cls(headers=headers, payload=PayloadRef(json=json.loads(payload))) else: rv = cls(headers=headers, payload=payload) diff --git a/tests/test_integration_tus.py b/tests/test_integration_tus.py new file mode 100644 index 000000000..642eb7ef0 --- /dev/null +++ b/tests/test_integration_tus.py @@ -0,0 +1,97 @@ +import os + +import pytest + +from . import ( + make_dsn, + run, + Envelope, + SENTRY_VERSION, +) +from .conditions import has_crashpad, has_http, is_qemu + +pytestmark = pytest.mark.skipif(not has_http, reason="tests need http") + +# fmt: off +auth_header = ( + f"Sentry sentry_key=uiaeosnrtdy, sentry_version=7, sentry_client=sentry.native/{SENTRY_VERSION}" +) +# fmt: on + + +@pytest.mark.skipif( + not has_crashpad or is_qemu, reason="crashpad backend not available" +) +def test_tus_crash_crashpad(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "crashpad"}) + + upload_uri = "/api/123456/upload/abc123def456789/" + upload_qs = "length=104857600&signature=xyz" + location = httpserver.url_for(upload_uri) + "?" + upload_qs + + httpserver.expect_oneshot_request( + "/api/123456/upload/", + headers={"tus-resumable": "1.0.0"}, + ).respond_with_data("OK", status=201, headers={"Location": location}) + + httpserver.expect_oneshot_request( + upload_uri, + method="PATCH", + headers={"tus-resumable": "1.0.0"}, + query_string=upload_qs, + ).respond_with_data("", status=204) + + httpserver.expect_oneshot_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + with httpserver.wait(timeout=15) as waiting: + run( + tmp_path, + "sentry_example", + ["log", "large-attachment", "crashpad-wait-for-upload", "crash"], + expect_failure=True, + env=env, + ) + assert waiting.result + + create_req = None + upload_req = None + envelope_req = None + for entry in httpserver.log: + req = entry[0] + if req.path == "/api/123456/upload/" and req.method == "POST": + create_req = req + elif upload_uri in req.path and req.method == "PATCH": + upload_req = req + elif "/envelope/" in req.path: + envelope_req = req + + assert create_req is not None + assert upload_req is not None + assert envelope_req is not None + assert int(create_req.headers.get("upload-length")) == 100 * 1024 * 1024 + assert upload_req.headers.get("content-type") == "application/offset+octet-stream" + assert upload_req.headers.get("upload-offset") == "0" + + envelope = Envelope.deserialize(envelope_req.get_data()) + attachment_ref = None + minidump_item = None + for item in envelope: + if item.headers.get("attachment_type") == "event.minidump": + minidump_item = item + if ( + item.headers.get("content_type") + == "application/vnd.sentry.attachment-ref+json" + and item.headers.get("filename") == ".sentry-large-attachment" + ): + if hasattr(item.payload, "json") and "location" in item.payload.json: + attachment_ref = item + + assert minidump_item is not None + assert attachment_ref is not None + assert attachment_ref.payload.json["location"] == location + assert attachment_ref.headers.get("attachment_length") == 100 * 1024 * 1024