Skip to content

Commit 097860c

Browse files
jpnurmiclaude
andcommitted
WIP: native backend TUS follow-up
Stage large attachments at crash time in the native backend's daemon process and emit attachment-ref items into the envelope (instead of inlining), so the transport's existing resolve_ref_items uploads via TUS and POSTs the envelope — all from the daemon, no restart. - attachment_is_external() / stage_external_attachment() in the daemon - add_external_attachment_refs() called after sentry__envelope_from_path reuses sentry__envelope_add_attachment_ref so the daemon doesn't duplicate the attachment-ref byte format - new test_tus_crash_native integration test Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e92091c commit 097860c

2 files changed

Lines changed: 193 additions & 3 deletions

File tree

src/backends/native/sentry_crash_daemon.c

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include "minidump/sentry_minidump_writer.h"
44
#include "sentry_alloc.h"
5+
#include "sentry_attachment.h"
56
#include "sentry_core.h"
67
#include "sentry_crash_ipc.h"
78
#include "sentry_database.h"
@@ -195,6 +196,123 @@ write_attachment_to_envelope(int fd, const char *file_path,
195196
return true;
196197
}
197198

199+
// Returns true if the attachment at `file_path` should be staged to the cache
200+
// and emitted as an attachment-ref item rather than inlined into the envelope.
201+
static bool
202+
attachment_is_external(const char *file_path)
203+
{
204+
sentry_path_t *src = sentry__path_from_str(file_path);
205+
size_t file_size = src ? sentry__path_get_size(src) : 0;
206+
sentry__path_free(src);
207+
return file_size >= SENTRY_LARGE_ATTACHMENT_SIZE;
208+
}
209+
210+
// Stage `src_path` as a sibling of the cached envelope at
211+
// <db>/cache/<event_id>-<filename> (with `-N` collision suffix). Returns the
212+
// chosen basename (caller frees) or NULL on failure.
213+
static char *
214+
stage_external_attachment(const char *src_path, const char *filename,
215+
const char *event_id, const char *db_dir)
216+
{
217+
char cache_dir_buf[SENTRY_CRASH_MAX_PATH];
218+
int n = snprintf(cache_dir_buf, sizeof(cache_dir_buf), "%s/cache", db_dir);
219+
if (n <= 0 || (size_t)n >= sizeof(cache_dir_buf)) {
220+
return NULL;
221+
}
222+
sentry_path_t *cache_path = sentry__path_from_str(cache_dir_buf);
223+
if (!cache_path || sentry__path_create_dir_all(cache_path) != 0) {
224+
sentry__path_free(cache_path);
225+
return NULL;
226+
}
227+
char buf[256];
228+
snprintf(buf, sizeof(buf), "%s-%s", event_id, filename);
229+
char *basename = sentry__path_unique_name(cache_path, buf);
230+
if (!basename) {
231+
sentry__path_free(cache_path);
232+
return NULL;
233+
}
234+
sentry_path_t *dst = sentry__path_join_str(cache_path, basename);
235+
sentry__path_free(cache_path);
236+
sentry_path_t *src = sentry__path_from_str(src_path);
237+
int rv = (src && dst) ? sentry__path_copy(src, dst) : -1;
238+
sentry__path_free(src);
239+
sentry__path_free(dst);
240+
if (rv != 0) {
241+
sentry_free(basename);
242+
return NULL;
243+
}
244+
return basename;
245+
}
246+
247+
// For each large attachment listed in `<run_folder>/__sentry-attachments`,
248+
// stage it to <db>/cache and append an `attachment-ref` item to `envelope`.
249+
// The transport then uploads via TUS at send time. Small attachments were
250+
// already inlined during envelope writing.
251+
static void
252+
add_external_attachment_refs(sentry_envelope_t *envelope,
253+
const sentry_path_t *run_folder, const char *db_dir)
254+
{
255+
if (!envelope || !run_folder || !db_dir) {
256+
return;
257+
}
258+
sentry_path_t *attach_list_path
259+
= sentry__path_join_str(run_folder, "__sentry-attachments");
260+
if (!attach_list_path) {
261+
return;
262+
}
263+
size_t attach_json_len = 0;
264+
char *attach_json
265+
= sentry__path_read_to_buffer(attach_list_path, &attach_json_len);
266+
sentry__path_free(attach_list_path);
267+
if (!attach_json) {
268+
return;
269+
}
270+
sentry_value_t list = attach_json_len > 0
271+
? sentry__value_from_json(attach_json, attach_json_len)
272+
: sentry_value_new_null();
273+
sentry_free(attach_json);
274+
if (sentry_value_is_null(list)) {
275+
return;
276+
}
277+
278+
sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope);
279+
if (sentry_uuid_is_nil(&event_id)) {
280+
sentry_value_decref(list);
281+
return;
282+
}
283+
char event_id_str[37];
284+
sentry_uuid_as_string(&event_id, event_id_str);
285+
286+
size_t len = sentry_value_get_length(list);
287+
for (size_t i = 0; i < len; i++) {
288+
sentry_value_t info = sentry_value_get_by_index(list, i);
289+
const char *path
290+
= sentry_value_as_string(sentry_value_get_by_key(info, "path"));
291+
const char *filename
292+
= sentry_value_as_string(sentry_value_get_by_key(info, "filename"));
293+
const char *content_type = sentry_value_as_string(
294+
sentry_value_get_by_key(info, "content_type"));
295+
if (!path || !*path || !filename || !*filename
296+
|| !attachment_is_external(path)) {
297+
continue;
298+
}
299+
sentry_path_t *src = sentry__path_from_str(path);
300+
size_t file_size = src ? sentry__path_get_size(src) : 0;
301+
sentry__path_free(src);
302+
char *basename
303+
= stage_external_attachment(path, filename, event_id_str, db_dir);
304+
if (!basename) {
305+
SENTRY_WARNF("Failed to stage large attachment: %s", path);
306+
continue;
307+
}
308+
sentry__envelope_add_attachment_ref(envelope, basename, NULL, filename,
309+
(content_type && *content_type) ? content_type : NULL, ATTACHMENT,
310+
sentry_value_new_uint64((uint64_t)file_size));
311+
sentry_free(basename);
312+
}
313+
sentry_value_decref(list);
314+
}
315+
198316
#if defined(SENTRY_PLATFORM_UNIX)
199317
/**
200318
* Get signal name from signal number (Unix platforms only)
@@ -2382,7 +2500,7 @@ write_envelope_with_native_stacktrace(const sentry_options_t *options,
23822500
const char *content_type
23832501
= sentry_value_as_string(content_type_val);
23842502

2385-
if (path && filename) {
2503+
if (path && filename && !attachment_is_external(path)) {
23862504
write_attachment_to_envelope(
23872505
fd, path, filename, content_type);
23882506
}
@@ -2617,7 +2735,7 @@ write_envelope_with_minidump(const sentry_options_t *options,
26172735
const char *content_type
26182736
= sentry_value_as_string(content_type_val);
26192737

2620-
if (path && filename) {
2738+
if (path && filename && !attachment_is_external(path)) {
26212739
write_attachment_to_envelope(
26222740
fd, path, filename, content_type);
26232741
}
@@ -2923,6 +3041,8 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc)
29233041
goto cleanup;
29243042
}
29253043

3044+
add_external_attachment_refs(envelope, run_folder, db_dir);
3045+
29263046
SENTRY_DEBUG("Envelope loaded, sending via transport");
29273047

29283048
// Send directly via transport, or to external crash reporter

tests/test_integration_tus.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
Envelope,
1010
SENTRY_VERSION,
1111
)
12-
from .conditions import has_breakpad, has_http, is_qemu
12+
from .conditions import has_breakpad, has_http, has_native, is_qemu
1313

1414
pytestmark = pytest.mark.skipif(not has_http, reason="tests need http")
1515

@@ -295,6 +295,76 @@ def test_tus_crash_restart(cmake, httpserver, backend):
295295
assert leftover_siblings == []
296296

297297

298+
@pytest.mark.skipif(not has_native or is_qemu, reason="native backend not available")
299+
def test_tus_crash_native(cmake, httpserver):
300+
# The native backend's daemon process handles the crash out-of-process: it
301+
# stages the large attachment, writes the envelope with attachment-ref
302+
# items, and uploads via TUS itself. No restart needed — unlike the inproc
303+
# / breakpad backends, which rely on the SDK on the next startup.
304+
tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"})
305+
306+
upload_uri = "/api/123456/upload/abc123def456789/"
307+
upload_qs = "length=104857600&signature=xyz"
308+
location = httpserver.url_for(upload_uri) + "?" + upload_qs
309+
310+
httpserver.expect_oneshot_request(
311+
"/api/123456/upload/",
312+
headers={"tus-resumable": "1.0.0"},
313+
).respond_with_data("OK", status=201, headers={"Location": location})
314+
315+
httpserver.expect_oneshot_request(
316+
upload_uri,
317+
method="PATCH",
318+
headers={"tus-resumable": "1.0.0"},
319+
query_string=upload_qs,
320+
).respond_with_data("", status=204)
321+
322+
httpserver.expect_oneshot_request(
323+
"/api/123456/envelope/",
324+
headers={"x-sentry-auth": auth_header},
325+
).respond_with_data("OK")
326+
327+
env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver))
328+
329+
with httpserver.wait(timeout=15) as waiting:
330+
run(
331+
tmp_path,
332+
"sentry_example",
333+
["log", "large-attachment", "crash"],
334+
expect_failure=True,
335+
env=env,
336+
)
337+
assert waiting.result
338+
339+
create_req = upload_req = envelope_req = None
340+
for entry in httpserver.log:
341+
req = entry[0]
342+
if req.path == "/api/123456/upload/" and req.method == "POST":
343+
create_req = req
344+
elif upload_uri in req.path and req.method == "PATCH":
345+
upload_req = req
346+
elif "/envelope/" in req.path:
347+
envelope_req = req
348+
assert create_req is not None
349+
assert upload_req is not None
350+
assert envelope_req is not None
351+
assert int(create_req.headers.get("upload-length")) == 100 * 1024 * 1024
352+
353+
body = envelope_req.get_data()
354+
envelope = Envelope.deserialize(body)
355+
attachment_ref = None
356+
for item in envelope:
357+
if (
358+
item.headers.get("content_type")
359+
== "application/vnd.sentry.attachment-ref+json"
360+
):
361+
if hasattr(item.payload, "json") and "location" in item.payload.json:
362+
attachment_ref = item
363+
break
364+
assert attachment_ref is not None
365+
assert attachment_ref.payload.json["location"] == location
366+
367+
298368
def test_small_attachment_no_tus(cmake, httpserver):
299369
tmp_path = cmake(
300370
["sentry_example"],

0 commit comments

Comments
 (0)