Skip to content

Commit e15c217

Browse files
jpnurmiclaude
andauthored
feat: add before_screenshot hook (#1641)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ea6b39d commit e15c217

File tree

10 files changed

+178
-16
lines changed

10 files changed

+178
-16
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
**Features**:
6+
7+
- Add `before_screenshot` hook. ([#1641](https://github.com/getsentry/sentry-native/pull/1641))
8+
59
**Fixes**:
610

711
- Reset client report counters during initialization ([#1632](https://github.com/getsentry/sentry-native/pull/1632))

include/sentry.h

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1580,13 +1580,47 @@ SENTRY_API void sentry_options_add_view_hierarchy_n(
15801580
* Enables or disables attaching screenshots to fatal error events. Disabled by
15811581
* default.
15821582
*
1583-
* This feature is currently supported by all backends on Windows. Only the
1584-
* `crashpad` backend can capture screenshots of fast-fail crashes that bypass
1585-
* SEH (structured exception handling).
1583+
* This feature is currently supported by all backends on Windows. The
1584+
* `crashpad` and `native` backends capture screenshots from an out-of-process
1585+
* handler. Only the `crashpad` backend can capture screenshots of fast-fail
1586+
* crashes that bypass SEH (structured exception handling).
1587+
*
1588+
* To decide per-event whether a screenshot should be captured, set a
1589+
* `before_screenshot` callback via `sentry_options_set_before_screenshot`.
15861590
*/
15871591
SENTRY_EXPERIMENTAL_API void sentry_options_set_attach_screenshot(
15881592
sentry_options_t *opts, int val);
15891593

1594+
/**
1595+
* Type of the `before_screenshot` callback.
1596+
*
1597+
* The callback is invoked before a screenshot is captured for an event. It
1598+
* receives the event (without ownership) and should return non-zero to capture
1599+
* the screenshot, or zero to skip it. The `attach_screenshot` option must be
1600+
* enabled for screenshots to be considered at all.
1601+
*
1602+
* Capturing screenshots is only supported on Windows, so the callback is
1603+
* only invoked there. The callback is not supported by the `crashpad`
1604+
* backend, which captures screenshots from its out-of-process handler.
1605+
*
1606+
* The callback may be called from inside an `UnhandledExceptionFilter`, see
1607+
* the documentation on SEH (structured exception handling) for more
1608+
* information
1609+
* https://docs.microsoft.com/en-us/windows/win32/debug/structured-exception-handling
1610+
*/
1611+
typedef int (*sentry_before_screenshot_function_t)(
1612+
sentry_value_t event, void *user_data);
1613+
1614+
/**
1615+
* Sets the `before_screenshot` callback.
1616+
*
1617+
* See the `sentry_before_screenshot_function_t` typedef above for more
1618+
* information.
1619+
*/
1620+
SENTRY_EXPERIMENTAL_API void sentry_options_set_before_screenshot(
1621+
sentry_options_t *opts, sentry_before_screenshot_function_t func,
1622+
void *user_data);
1623+
15901624
/**
15911625
* Sets the path to the crashpad handler if the crashpad backend is used.
15921626
*

src/backends/native/sentry_crash_daemon.c

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2392,7 +2392,7 @@ write_envelope_with_native_stacktrace(const sentry_options_t *options,
23922392
}
23932393

23942394
// Add screenshot attachment if captured by the daemon
2395-
if (options && options->attach_screenshot && run_folder) {
2395+
if (ctx->attach_screenshot && run_folder) {
23962396
sentry_path_t *screenshot_path
23972397
= sentry__path_join_str(run_folder, "screenshot.png");
23982398
if (screenshot_path) {
@@ -2417,8 +2417,9 @@ write_envelope_with_native_stacktrace(const sentry_options_t *options,
24172417
*/
24182418
static bool
24192419
write_envelope_with_minidump(const sentry_options_t *options,
2420-
const char *envelope_path, const char *event_msgpack_path,
2421-
const char *minidump_path, sentry_path_t *run_folder)
2420+
const sentry_crash_context_t *ctx, const char *envelope_path,
2421+
const char *event_msgpack_path, const char *minidump_path,
2422+
sentry_path_t *run_folder)
24222423
{
24232424
// Read event JSON data
24242425
size_t event_size = 0;
@@ -2626,7 +2627,7 @@ write_envelope_with_minidump(const sentry_options_t *options,
26262627
}
26272628

26282629
// Add screenshot attachment if captured by the daemon
2629-
if (options && options->attach_screenshot && run_folder) {
2630+
if (ctx->attach_screenshot && run_folder) {
26302631
sentry_path_t *screenshot_path
26312632
= sentry__path_join_str(run_folder, "screenshot.png");
26322633
if (screenshot_path) {
@@ -2802,7 +2803,7 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc)
28022803
// This is done in the daemon process (out-of-process) because
28032804
// screenshot capture is NOT signal-safe (uses LoadLibrary, GDI+, etc.)
28042805
#if defined(SENTRY_PLATFORM_WINDOWS)
2805-
if (options && options->attach_screenshot && run_folder) {
2806+
if (ctx->attach_screenshot && run_folder) {
28062807
SENTRY_DEBUG("Capturing screenshot");
28072808
sentry_path_t *screenshot_path
28082809
= sentry__path_join_str(run_folder, "screenshot.png");
@@ -2867,7 +2868,7 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc)
28672868
// Mode 0 (MINIDUMP only)
28682869
SENTRY_DEBUG("Writing envelope with minidump");
28692870
envelope_written = write_envelope_with_minidump(
2870-
options, envelope_path, event_path, minidump_path, run_folder);
2871+
options, ctx, envelope_path, event_path, minidump_path, run_folder);
28712872
}
28722873

28732874
if (!envelope_written) {

src/backends/sentry_backend_breakpad.cpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,16 @@ breakpad_backend_callback(const google_breakpad::MinidumpDescriptor &descriptor,
162162
}
163163

164164
if (should_handle) {
165+
bool capture_screenshot = options->attach_screenshot;
166+
#ifdef SENTRY_PLATFORM_WINDOWS
167+
if (capture_screenshot && options->before_screenshot_func) {
168+
SENTRY_DEBUG("invoking `before_screenshot` hook");
169+
capture_screenshot = options->before_screenshot_func(
170+
event, options->before_screenshot_data)
171+
!= 0;
172+
}
173+
#endif
174+
165175
sentry_envelope_t *envelope = sentry__prepare_event(
166176
options, event, nullptr, !options->on_crash_func, nullptr);
167177
sentry_session_t *session = sentry__end_current_session_with_status(
@@ -180,7 +190,7 @@ breakpad_backend_callback(const google_breakpad::MinidumpDescriptor &descriptor,
180190
sentry_value_new_string(sentry__path_filename(dump_path)));
181191
}
182192

183-
if (options->attach_screenshot) {
193+
if (capture_screenshot) {
184194
sentry_attachment_t *screenshot = sentry__attachment_from_path(
185195
sentry__screenshot_get_path(options));
186196
if (screenshot

src/backends/sentry_backend_inproc.c

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1198,6 +1198,16 @@ process_ucontext_deferred(const sentry_ucontext_t *uctx,
11981198
}
11991199
TEST_CRASH_POINT("before_capture");
12001200
if (should_handle) {
1201+
bool capture_screenshot = options->attach_screenshot;
1202+
#ifdef SENTRY_PLATFORM_WINDOWS
1203+
if (capture_screenshot && options->before_screenshot_func) {
1204+
SENTRY_DEBUG("invoking `before_screenshot` hook");
1205+
capture_screenshot = options->before_screenshot_func(
1206+
event, options->before_screenshot_data)
1207+
!= 0;
1208+
}
1209+
#endif
1210+
12011211
sentry_envelope_t *envelope = sentry__prepare_event(options, event,
12021212
NULL, !options->on_crash_func && !skip_hooks, NULL);
12031213
// TODO(tracing): Revisit when investigating transaction flushing
@@ -1207,7 +1217,7 @@ process_ucontext_deferred(const sentry_ucontext_t *uctx,
12071217
SENTRY_SESSION_STATUS_CRASHED);
12081218
sentry__envelope_add_session(envelope, session);
12091219

1210-
if (options->attach_screenshot) {
1220+
if (capture_screenshot) {
12111221
sentry_attachment_t *screenshot = sentry__attachment_from_path(
12121222
sentry__screenshot_get_path(options));
12131223
if (screenshot

src/backends/sentry_backend_native.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,24 @@ native_backend_except(sentry_backend_t *backend, const sentry_ucontext_t *uctx)
856856
device_context, "arch", sentry_value_new_string("arm64"));
857857
# endif
858858
sentry_value_set_by_key(contexts, "device", device_context);
859+
860+
// The screenshot is captured by the daemon out-of-process, so
861+
// we invoke the hook here (in the crashing process, where
862+
// user callbacks can run) and communicate the decision to the
863+
// daemon by flipping attach_screenshot in the shared crash
864+
// context. Screenshots are only captured on Windows.
865+
if (options->attach_screenshot
866+
&& options->before_screenshot_func && state && state->ipc
867+
&& state->ipc->shmem) {
868+
SENTRY_DEBUG("invoking `before_screenshot` hook");
869+
if (options->before_screenshot_func(
870+
event, options->before_screenshot_data)
871+
== 0) {
872+
SENTRY_DEBUG("screenshot skipped by "
873+
"`before_screenshot` hook");
874+
state->ipc->shmem->attach_screenshot = false;
875+
}
876+
}
859877
#endif
860878

861879
// Write event as JSON file

src/sentry_options.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,14 @@ sentry_options_set_attach_screenshot(sentry_options_t *opts, int val)
630630
opts->attach_screenshot = !!val;
631631
}
632632

633+
void
634+
sentry_options_set_before_screenshot(sentry_options_t *opts,
635+
sentry_before_screenshot_function_t func, void *user_data)
636+
{
637+
opts->before_screenshot_func = func;
638+
opts->before_screenshot_data = user_data;
639+
}
640+
633641
void
634642
sentry_options_set_handler_path(sentry_options_t *opts, const char *path)
635643
{

src/sentry_options.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ struct sentry_options_s {
4141
bool symbolize_stacktraces;
4242
bool system_crash_reporter_enabled;
4343
bool attach_screenshot;
44+
sentry_before_screenshot_function_t before_screenshot_func;
45+
void *before_screenshot_data;
4446
bool crashpad_wait_for_upload;
4547
bool enable_logging_when_crashed;
4648
bool propagate_traceparent;

tests/fixtures/screenshot/screenshot_win32.c

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ trigger_fastfail_crash()
4646
__fastfail(77);
4747
}
4848

49+
static int
50+
before_screenshot_skip(sentry_value_t event, void *user_data)
51+
{
52+
(void)event;
53+
(void)user_data;
54+
return 0;
55+
}
56+
4957
static LRESULT CALLBACK
5058
wnd_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
5159
{
@@ -74,16 +82,20 @@ int WINAPI
7482
wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine,
7583
int nCmdShow)
7684
{
85+
int argc = 0;
86+
LPWSTR *argv = CommandLineToArgvW(lpCmdLine, &argc);
87+
7788
sentry_options_t *options = sentry_options_new();
7889
sentry_options_set_release(options, "sentry-screenshot");
7990
sentry_options_set_auto_session_tracking(options, false);
8091
sentry_options_set_attach_screenshot(options, true);
8192
sentry_options_set_debug(options, true);
93+
if (has_arg(argc, argv, L"before-screenshot")) {
94+
sentry_options_set_before_screenshot(
95+
options, before_screenshot_skip, NULL);
96+
}
8297
sentry_init(options);
8398

84-
int argc = 0;
85-
LPWSTR *argv = CommandLineToArgvW(lpCmdLine, &argc);
86-
8799
#if (WINVER >= 0x0605)
88100
if (has_arg(argc, argv, L"dpi-unaware")) {
89101
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE);

tests/test_integration_screenshot.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ def assert_screenshot_file(database_path):
1919
assert screenshot_path.stat().st_size > 0, "Screenshot file is empty"
2020

2121

22+
def assert_no_screenshot_file(database_path):
23+
run_dirs = [d for d in database_path.glob("*.run") if d.is_dir()]
24+
assert (
25+
len(run_dirs) == 1
26+
), f"Expected exactly one .run directory, found {len(run_dirs)}"
27+
screenshot_path = run_dirs[0] / "screenshot.png"
28+
assert not screenshot_path.exists(), "Unexpected screenshot file was captured"
29+
30+
2231
def assert_screenshot_envelope(envelope):
2332
found_screenshot = False
2433
for item in envelope.items:
@@ -30,7 +39,7 @@ def assert_screenshot_envelope(envelope):
3039
assert item.headers.get("attachment_type") == "event.attachment"
3140
assert len(item.payload.bytes) > 0
3241
found_screenshot = True
33-
assert found_screenshot, "No screenshot item found in envelope"
42+
return found_screenshot
3443

3544

3645
def assert_screenshot_upload(req):
@@ -79,7 +88,9 @@ def test_capture_screenshot_native(cmake, httpserver):
7988

8089
assert len(httpserver.log) == 1
8190
envelope = Envelope.deserialize(httpserver.log[0][0].get_data())
82-
assert_screenshot_envelope(envelope)
91+
assert (
92+
assert_screenshot_envelope(envelope) == True
93+
), "No screenshot item found in envelope"
8394

8495

8596
@pytest.mark.skipif(
@@ -124,3 +135,55 @@ def test_capture_screenshot_crashpad(cmake, httpserver, run_args):
124135
)
125136
def test_capture_screenshot_crashpad_wer(cmake, httpserver, run_args):
126137
test_capture_screenshot_crashpad(cmake, httpserver, run_args)
138+
139+
140+
@pytest.mark.skipif(
141+
sys.platform != "win32",
142+
reason="Screenshots are only supported on Windows",
143+
)
144+
@pytest.mark.parametrize("backend", ["inproc", "breakpad"])
145+
def test_before_screenshot(cmake, httpserver, backend):
146+
tmp_path = cmake(
147+
["sentry_screenshot"], {"SENTRY_BACKEND": backend, "SENTRY_TRANSPORT": "none"}
148+
)
149+
150+
env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver))
151+
152+
run(
153+
tmp_path,
154+
"sentry_screenshot",
155+
["crash", "before-screenshot"],
156+
expect_failure=True,
157+
env=env,
158+
)
159+
160+
assert_no_screenshot_file(tmp_path / ".sentry-native")
161+
162+
163+
@pytest.mark.skipif(
164+
sys.platform != "win32",
165+
reason="Screenshots are only supported on Windows",
166+
)
167+
def test_before_screenshot_native(cmake, httpserver):
168+
tmp_path = cmake(["sentry_screenshot"], {"SENTRY_BACKEND": "native"})
169+
170+
env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver))
171+
172+
httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK")
173+
174+
with httpserver.wait(timeout=10) as waiting:
175+
run(
176+
tmp_path,
177+
"sentry_screenshot",
178+
["crash", "before-screenshot"],
179+
expect_failure=True,
180+
env=env,
181+
)
182+
183+
assert waiting.result
184+
185+
assert len(httpserver.log) == 1
186+
envelope = Envelope.deserialize(httpserver.log[0][0].get_data())
187+
assert (
188+
assert_screenshot_envelope(envelope) == False
189+
), "Screenshot item was unexpectedly found in envelope"

0 commit comments

Comments
 (0)