Repository: getsentry/sentry-native
Pull request #1236: feat: Add `before_send_transaction`
Author: login: JoshuaMoelans
Created: 2025-05-13T13:50:23Z | Merged: 2025-05-15T10:26:45Z

Fixes https://github.com/getsentry/sentry-native/issues/1214 (and related to https://github.com/getsentry/sentry-native/issues/864#issuecomment-1649461251)

ToDo
- [x] add option
- [x] call inside transaction event preparation
- [x] add to example
- [x] answer todo questions in-code
- [x] add tests
- [x] changelog
- [x] docs https://github.com/getsentry/sentry-docs/pull/13715

## questions (resolved)
- Why do we have invoke_before_send passed into prepare_event ([see code](https://github.com/getsentry/sentry-native/blob/e511a8ed0be28e4fd9826854032df370ac0acd77/src/sentry_core.c#L554-L555)), and why is it true for sentry_core occurrences ([1](https://github.com/getsentry/sentry-native/blob/e511a8ed0be28e4fd9826854032df370ac0acd77/src/sentry_core.c#L466), [2](https://github.com/getsentry/sentry-native/blob/e511a8ed0be28e4fd9826854032df370ac0acd77/src/sentry_core.cL1407-L1408)), but passed as !options->on_crash_func for [breakpad](https://github.com/getsentry/sentry-native/blob/e511a8ed0be28e4fd9826854032df370ac0acd77/src/backends/sentry_backend_breakpad.cpp#L145-L146)?

  > For crashes, we only `before_send` if there is no `on_crash` hook.
- Where do we callback? Before or after applying scope?
  
  > As late as possible, to give the user maximal context.
- (void* hint) parameter (which is unused for the other callbacks, but exists afaict), [see here](https://github.com/getsentry/sentry-native/pull/1236/files#diff-9c77242571e813d699b88663fdc7d7ad7bb5b481a3c1af8d34e1a9e242cbf076R1765-R1766)
  
  > Not relevant for `before_send_transaction`.
- Do we want to add `before_transaction_data` to modify transactions before sending? [see here](https://github.com/getsentry/sentry-native/pull/1236/files#diff-9c77242571e813d699b88663fdc7d7ad7bb5b481a3c1af8d34e1a9e242cbf076R1767)
  
  > Yes.



Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d2993dd4c0..6ba7a65e6a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Changelog
 
+## Unreleased
+
+**Features**:
+
+- Provide `before_send_transaction` callback. ([#1236](https://github.com/getsentry/sentry-native/pull/1236))
+
 ## 0.8.5
 
 **Breaking changes**:
@@ -9,7 +15,7 @@
 **Features**:
 
 - Add `sentry_value_new_user(id, username, email, ip_address)` function to avoid ambiguous user-context-keys. ([#1228](https://github.com/getsentry/sentry-native/pull/1228))
-
+ 
 **Fixes**:
 
 - Remove compile-time check for the `libcurl` feature `AsynchDNS`. ([#1206](https://github.com/getsentry/sentry-native/pull/1206))
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 3402c22bd2..e7abe52348 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -157,6 +157,8 @@ The example currently supports the following commands:
 - `capture-transaction`: Captures a transaction.
   - `update-tx-from-header`: Updates the transaction with trace header `"2674eb52d5874b13b560236d6c79ce8a-a0f9fdf04f1a63df"` (`trace_id`-`parent_span_id`).
   - `scope-transaction-event`: Scopes the created transaction and captures an additional event.
+- `before-transaction`: Installs a `before_transaction()` callback that updates the transaction title.
+- `discarding-before-transaction`: Installs a `before_transaction()` callback that discards the transaction.
 - `traces-sampler`: Installs a traces sampler callback function when used alongside `capture-transaction`.
 - `attach-view-hierarchy`: Adds a `view-hierarchy.json` attachment file, giving it the proper `attachment_type` and `content_type`. 
  This file can be found in `./tests/fixtures/view-hierachy.json`.
diff --git a/examples/example.c b/examples/example.c
index b4dad2834c..8f27165b9d 100644
--- a/examples/example.c
+++ b/examples/example.c
@@ -57,10 +57,10 @@ traces_sampler_callback(const sentry_transaction_context_t *transaction_ctx,
 }
 
 static sentry_value_t
-before_send_callback(sentry_value_t event, void *hint, void *closure)
+before_send_callback(sentry_value_t event, void *hint, void *user_data)
 {
     (void)hint;
-    (void)closure;
+    (void)user_data;
 
     // make our mark on the event
     sentry_value_set_by_key(
@@ -71,10 +71,11 @@ before_send_callback(sentry_value_t event, void *hint, void *closure)
 }
 
 static sentry_value_t
-discarding_before_send_callback(sentry_value_t event, void *hint, void *closure)
+discarding_before_send_callback(
+    sentry_value_t event, void *hint, void *user_data)
 {
     (void)hint;
-    (void)closure;
+    (void)user_data;
 
     // discard event and signal backend to stop further processing
     sentry_value_decref(event);
@@ -83,10 +84,10 @@ discarding_before_send_callback(sentry_value_t event, void *hint, void *closure)
 
 static sentry_value_t
 discarding_on_crash_callback(
-    const sentry_ucontext_t *uctx, sentry_value_t event, void *closure)
+    const sentry_ucontext_t *uctx, sentry_value_t event, void *user_data)
 {
     (void)uctx;
-    (void)closure;
+    (void)user_data;
 
     // discard event and signal backend to stop further processing
     sentry_value_decref(event);
@@ -95,15 +96,37 @@ discarding_on_crash_callback(
 
 static sentry_value_t
 on_crash_callback(
-    const sentry_ucontext_t *uctx, sentry_value_t event, void *closure)
+    const sentry_ucontext_t *uctx, sentry_value_t event, void *user_data)
 {
     (void)uctx;
-    (void)closure;
+    (void)user_data;
 
     // tell the backend to retain the event
     return event;
 }
 
+static sentry_value_t
+before_transaction_callback(sentry_value_t tx, void *user_data)
+{
+    (void)user_data;
+
+    sentry_value_set_by_key(
+        tx, "transaction", sentry_value_new_string("little.coffeepot"));
+    return tx;
+}
+
+static sentry_value_t
+discarding_before_transaction_callback(sentry_value_t tx, void *user_data)
+{
+    (void)user_data;
+    // throw out any transaction while a tag is active
+    if (!sentry_value_is_null(sentry_value_get_by_key(tx, "tags"))) {
+        sentry_value_decref(tx);
+        return sentry_value_new_null();
+    }
+    return tx;
+}
+
 static void
 print_envelope(sentry_envelope_t *envelope, void *unused_state)
 {
@@ -260,6 +283,16 @@ main(int argc, char **argv)
             options, discarding_on_crash_callback, NULL);
     }
 
+    if (has_arg(argc, argv, "before-transaction")) {
+        sentry_options_set_before_transaction(
+            options, before_transaction_callback, NULL);
+    }
+
+    if (has_arg(argc, argv, "discarding-before-transaction")) {
+        sentry_options_set_before_transaction(
+            options, discarding_before_transaction_callback, NULL);
+    }
+
     if (has_arg(argc, argv, "traces-sampler")) {
         sentry_options_set_traces_sampler(options, traces_sampler_callback);
     }
diff --git a/include/sentry.h b/include/sentry.h
index 425204b3bb..bf7a25fb25 100644
--- a/include/sentry.h
+++ b/include/sentry.h
@@ -862,7 +862,7 @@ SENTRY_API void sentry_options_set_send_default_pii(
  * sent.
  */
 typedef sentry_value_t (*sentry_event_function_t)(
-    sentry_value_t event, void *hint, void *closure);
+    sentry_value_t event, void *hint, void *user_data);
 
 /**
  * Sets the `before_send` callback.
@@ -870,7 +870,7 @@ typedef sentry_value_t (*sentry_event_function_t)(
  * See the `sentry_event_function_t` typedef above for more information.
  */
 SENTRY_API void sentry_options_set_before_send(
-    sentry_options_t *opts, sentry_event_function_t func, void *data);
+    sentry_options_t *opts, sentry_event_function_t func, void *user_data);
 
 /**
  * Type of the `on_crash` callback.
@@ -923,7 +923,7 @@ SENTRY_API void sentry_options_set_before_send(
  * sent.
  */
 typedef sentry_value_t (*sentry_crash_function_t)(
-    const sentry_ucontext_t *uctx, sentry_value_t event, void *closure);
+    const sentry_ucontext_t *uctx, sentry_value_t event, void *user_data);
 
 /**
  * Sets the `on_crash` callback.
@@ -1754,6 +1754,23 @@ SENTRY_EXPERIMENTAL_API void sentry_end_session_with_status(
 struct sentry_transaction_s;
 typedef struct sentry_transaction_s sentry_transaction_t;
 
+/**
+ * Type of the `before_transaction` callback.
+ *
+ * The callback takes ownership of the `transaction`, and should usually return
+ * that same transaction. In case the transaction should be discarded, the
+ * callback needs to call `sentry_value_decref` on the provided transaction, and
+ * return a `sentry_value_new_null()` instead.
+ */
+typedef sentry_value_t (*sentry_transaction_function_t)(
+    sentry_value_t transaction, void *user_data);
+
+/**
+ * Sets the `before_transaction` callback.
+ */
+SENTRY_EXPERIMENTAL_API void sentry_options_set_before_transaction(
+    sentry_options_t *opts, sentry_transaction_function_t func, void *data);
+
 /**
  * A sentry Span.
  *
diff --git a/src/sentry_core.c b/src/sentry_core.c
index 910c51533d..d003057829 100644
--- a/src/sentry_core.c
+++ b/src/sentry_core.c
@@ -633,6 +633,17 @@ sentry__prepare_transaction(const sentry_options_t *options,
         sentry__scope_apply_to_event(scope, options, transaction, mode);
     }
 
+    if (options->before_transaction_func) {
+        SENTRY_DEBUG("invoking `before_transaction` hook");
+        transaction = options->before_transaction_func(
+            transaction, options->before_transaction_data);
+        if (sentry_value_is_null(transaction)) {
+            SENTRY_DEBUG(
+                "transaction was discarded by the `before_transaction` hook");
+            return NULL;
+        }
+    }
+
     sentry__ensure_event_id(transaction, event_id);
     envelope = sentry__envelope_new();
     if (!envelope || !sentry__envelope_add_transaction(envelope, transaction)) {
diff --git a/src/sentry_options.c b/src/sentry_options.c
index cbb181bdbe..0bfb70b10d 100644
--- a/src/sentry_options.c
+++ b/src/sentry_options.c
@@ -143,18 +143,26 @@ sentry_options_set_send_default_pii(sentry_options_t *opts, int value)
 
 void
 sentry_options_set_before_send(
-    sentry_options_t *opts, sentry_event_function_t func, void *data)
+    sentry_options_t *opts, sentry_event_function_t func, void *user_data)
 {
     opts->before_send_func = func;
-    opts->before_send_data = data;
+    opts->before_send_data = user_data;
 }
 
 void
 sentry_options_set_on_crash(
-    sentry_options_t *opts, sentry_crash_function_t func, void *data)
+    sentry_options_t *opts, sentry_crash_function_t func, void *user_data)
 {
     opts->on_crash_func = func;
-    opts->on_crash_data = data;
+    opts->on_crash_data = user_data;
+}
+
+void
+sentry_options_set_before_transaction(
+    sentry_options_t *opts, sentry_transaction_function_t func, void *user_data)
+{
+    opts->before_transaction_func = func;
+    opts->before_transaction_data = user_data;
 }
 
 void
diff --git a/src/sentry_options.h b/src/sentry_options.h
index f53317f8d8..0185bc67c9 100644
--- a/src/sentry_options.h
+++ b/src/sentry_options.h
@@ -68,6 +68,8 @@ struct sentry_options_s {
     void *before_send_data;
     sentry_crash_function_t on_crash_func;
     void *on_crash_data;
+    sentry_transaction_function_t before_transaction_func;
+    void *before_transaction_data;
 
     /* Experimentally exposed */
     double traces_sample_rate;
diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py
index a580b587fc..49a3613b5f 100644
--- a/tests/test_integration_http.py
+++ b/tests/test_integration_http.py
@@ -590,6 +590,87 @@ def test_transaction_only(cmake, httpserver, build_args):
     assert trace_context["data"] == {"url": "https://example.com"}
 
 
+def test_before_transaction_callback(cmake, httpserver):
+    tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"})
+
+    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), SENTRY_RELEASE="🤮🚀")
+
+    run(
+        tmp_path,
+        "sentry_example",
+        ["log", "capture-transaction", "before-transaction"],
+        check=True,
+        env=env,
+    )
+
+    assert len(httpserver.log) == 1
+    req = httpserver.log[0][0]
+    body = req.get_data()
+
+    envelope = Envelope.deserialize(body)
+
+    # Show what the envelope looks like if the test fails.
+    envelope.print_verbose()
+
+    # callback has changed from default teapot to coffeepot
+    assert_meta(
+        envelope,
+        transaction="little.coffeepot",
+    )
+
+    # Extract the one-and-only-item
+    (event,) = envelope.items
+
+    assert event.headers["type"] == "transaction"
+    payload = event.payload.json
+
+    # See https://develop.sentry.dev/sdk/data-model/event-payloads/contexts/#trace-context
+    trace_context = payload["contexts"]["trace"]
+
+    assert (
+        trace_context["op"] == "Short and stout here is my handle and here is my spout"
+    )
+
+    assert trace_context["trace_id"]
+    trace_id = uuid.UUID(hex=trace_context["trace_id"])
+    assert trace_id
+
+    assert trace_context["span_id"]
+    assert trace_context["status"] == "ok"
+
+    start_timestamp = time.strptime(payload["start_timestamp"], RFC3339_FORMAT)
+    assert start_timestamp
+    timestamp = time.strptime(payload["timestamp"], RFC3339_FORMAT)
+    assert timestamp >= start_timestamp
+
+    assert trace_context["data"] == {"url": "https://example.com"}
+
+
+def test_before_transaction_discard(cmake, httpserver):
+    tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"})
+
+    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), SENTRY_RELEASE="🤮🚀")
+
+    run(
+        tmp_path,
+        "sentry_example",
+        ["log", "capture-transaction", "discarding-before-transaction"],
+        check=True,
+        env=env,
+    )
+
+    # transaction should have been discarded
+    assert len(httpserver.log) == 0
+
+
 def test_transaction_event(cmake, httpserver):
     tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"})
 


Reviews:
user: login: JoshuaMoelans
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T14:04:21Z
user: login: supervacuus
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T14:45:51Z
user: login: JoshuaMoelans
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T15:01:08Z
user: login: supervacuus
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T15:07:50Z
user: login: supervacuus
body: Looks great. Just wonder about the parameter naming and whether we should use the PR to make its use consistent.
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-15T07:59:13Z
user: login: JoshuaMoelans
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T08:33:16Z
user: login: supervacuus
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-15T08:57:56Z

Review comments:
diff_hunk: @@ -633,6 +633,18 @@ sentry__prepare_transaction(const sentry_options_t *options,
         sentry__scope_apply_to_event(scope, options, transaction, mode);
     }
 
+    if (options->before_transaction_func) {
+        // TODO why no invoke_before_send in func arguments (vs prepare_event)
+        //      (also why is it always `true` for prepare_event)
+        SENTRY_DEBUG("invoking `before_transaction` hook");
+        transaction = options->before_transaction_func(transaction);
path: src/sentry_core.c
user: login: JoshuaMoelans
body: I'm not 💯 on calling the `before_transaction_func` here in `sentry__prepare_transaction`, because at this point the transaction is already +- an event (and has way more data than a `sentry_transaction_t` has, since the scope was applied to the event). 

If we move the callback to before we apply scope, we would have the following data

<details>

```json
{
  "event_id" : "ca50d0de-615b-4bd2-f3c3-61d894f5ca5b",
  "platform" : "native",
  "transaction" : "little.teapot",
  "start_timestamp" : "2025-05-13T14:07:09.048432Z",
  "spans" : [ {
    "op" : "littlest.teapot",
    "span_id" : "91abb32a0790493e",
    "status" : "ok",
    "trace_id" : "344225a2f88849d60fd029945b78cdd2",
    "parent_span_id" : "d9bce753d65e4454",
    "description" : null,
    "start_timestamp" : "2025-05-13T14:07:09.048441Z",
    "timestamp" : "2025-05-13T14:07:09.048443Z"
  }, {
    "op" : "littler.teapot",
    "span_id" : "d9bce753d65e4454",
    "status" : "ok",
    "trace_id" : "344225a2f88849d60fd029945b78cdd2",
    "parent_span_id" : "3fc2208733e5487a",
    "description" : null,
    "start_timestamp" : "2025-05-13T14:07:09.048439Z",
    "data" : {
      "span_data_says" : "hi!"
    },
    "timestamp" : "2025-05-13T14:07:09.048446Z"
  } ],
  "type" : "transaction",
  "timestamp" : "2025-05-13T14:07:09.048450Z",
  "level" : "info",
  "contexts" : {
    "trace" : {
      "trace_id" : "344225a2f88849d60fd029945b78cdd2",
      "span_id" : "3fc2208733e5487a",
      "op" : "Short and stout here is my handle and here is my spout",
      "status" : "ok",
      "data" : {
        "url" : "https://example.com"
      }
    }
  }
}
```

</details>

versus a full event json like below:
<details>

```json
{
  "event_id" : "3236d6f4-75bd-44d7-4b77-4f2bbc700137",
  "platform" : "native",
  "transaction" : "little.teapot",
  "start_timestamp" : "2025-05-13T13:58:26.352429Z",
  "spans" : [ {
    "op" : "littlest.teapot",
    "span_id" : "98ed455fca274524",
    "status" : "ok",
    "trace_id" : "ce3e2f93331243724a8cbc02555d2cae",
    "parent_span_id" : "4ebf9dbce31d408d",
    "description" : null,
    "start_timestamp" : "2025-05-13T13:58:26.352437Z",
    "timestamp" : "2025-05-13T13:58:26.352439Z"
  }, {
    "op" : "littler.teapot",
    "span_id" : "4ebf9dbce31d408d",
    "status" : "ok",
    "trace_id" : "ce3e2f93331243724a8cbc02555d2cae",
    "parent_span_id" : "1dafa81af1e34160",
    "description" : null,
    "start_timestamp" : "2025-05-13T13:58:26.352434Z",
    "data" : {
      "span_data_says" : "hi!"
    },
    "timestamp" : "2025-05-13T13:58:26.352442Z"
  } ],
  "type" : "transaction",
  "timestamp" : "2025-05-13T13:58:26.352444Z",
  "level" : "info",
  "contexts" : {
    "trace" : {
      "trace_id" : "ce3e2f93331243724a8cbc02555d2cae",
      "span_id" : "1dafa81af1e34160",
      "op" : "Short and stout here is my handle and here is my spout",
      "status" : "ok",
      "data" : {
        "url" : "https://example.com"
      }
    },
    "os" : {
      "name" : "macOS",
      "version" : "15.4.1",
      "build" : "24E263",
      "kernel_version" : "24.4.0"
    },
    "runtime" : {
      "type" : "runtime",
      "name" : "testing-runtime"
    }
  },
  "release" : "test-example-release",
  "environment" : "development",
  "user" : {
    "id" : "42",
    "username" : "some_name"
  },
  "sdk" : {
    "name" : "sentry.native",
    "version" : "0.8.4",
    "packages" : [ {
      "name" : "github:getsentry/sentry-native",
      "version" : "0.8.4"
    } ]
  },
  "tags" : {
    "expected-tag" : "some value"
  },
  "extra" : {
    "extra stuff" : "some value",
    "…unicode key…" : "őá…–🤮🚀¿ 한글 테스트"
  },
  "breadcrumbs" : [ {
    "timestamp" : "2025-05-13T13:58:26.350755Z",
    "message" : "default level is info"
  }, {
    "timestamp" : "2025-05-13T13:58:26.352420Z",
    "type" : "http",
    "message" : "debug crumb",
    "category" : "example!",
    "level" : "debug",
    "data" : {
      "url" : "https://example.com/api/1.0/users",
      "method" : "GET",
      "status_code" : 200,
      "reason" : "OK"
    }
  }, {
    "timestamp" : "2025-05-13T13:58:26.352423Z",
    "message" : "lf\ncrlf\r\nlf\n...",
    "category" : "something else"
  } ]
}
```

</details>

---


(below is an alternative but makes a bit less sense)
Maybe it makes more sense to apply the callback in the `sentry_transaction_finish` (e.g. [here](https://github.com/getsentry/sentry-native/blob/e511a8ed0be28e4fd9826854032df370ac0acd77/src/sentry_core.c#L1040-L1118)) where users only have access to the following keys: `"trace_id", "span_id", "parent_span_id", "op", "description", "status", "data"` 

(we don't have access to the `options` at that point, so think it's not as good as just moving around the callback inside the `prepare_transaction`.
created_at: 2025-05-13T14:04:21Z
author_association: MEMBER
diff_hunk: @@ -633,6 +633,18 @@ sentry__prepare_transaction(const sentry_options_t *options,
         sentry__scope_apply_to_event(scope, options, transaction, mode);
     }
 
+    if (options->before_transaction_func) {
+        // TODO why no invoke_before_send in func arguments (vs prepare_event)
+        //      (also why is it always `true` for prepare_event)
+        SENTRY_DEBUG("invoking `before_transaction` hook");
+        transaction = options->before_transaction_func(transaction);
path: src/sentry_core.c
user: login: supervacuus
body: > because at this point the transaction is already +- an event (and has way more data than a sentry_transaction_t has, since the scope was applied to the event).

Why is this a bad thing? The more context a user gets, the closer they are to sending, the better. No?
created_at: 2025-05-13T14:45:51Z
author_association: COLLABORATOR
diff_hunk: @@ -633,6 +633,18 @@ sentry__prepare_transaction(const sentry_options_t *options,
         sentry__scope_apply_to_event(scope, options, transaction, mode);
     }
 
+    if (options->before_transaction_func) {
+        // TODO why no invoke_before_send in func arguments (vs prepare_event)
+        //      (also why is it always `true` for prepare_event)
+        SENTRY_DEBUG("invoking `before_transaction` hook");
+        transaction = options->before_transaction_func(transaction);
path: src/sentry_core.c
user: login: JoshuaMoelans
body: It's not bad, just wanted to make sure this is the intended use & that we have no reason to restrict access to this kind of event data. Since the `before_send_func` callback only gets called for non-transaction events, I agree with you that it does make sense to give the user as much context for transactions as possible.
created_at: 2025-05-13T15:01:08Z
author_association: MEMBER
diff_hunk: @@ -633,6 +633,18 @@ sentry__prepare_transaction(const sentry_options_t *options,
         sentry__scope_apply_to_event(scope, options, transaction, mode);
     }
 
+    if (options->before_transaction_func) {
+        // TODO why no invoke_before_send in func arguments (vs prepare_event)
+        //      (also why is it always `true` for prepare_event)
+        SENTRY_DEBUG("invoking `before_transaction` hook");
+        transaction = options->before_transaction_func(transaction);
path: src/sentry_core.c
user: login: supervacuus
body: I wouldn't restrict filter options without a "designed" reason. This is the only place where users can make changes and should have as much access as possible. However, other SDKs should be cross-checked.
created_at: 2025-05-13T15:07:50Z
author_association: COLLABORATOR
diff_hunk: @@ -870,7 +870,7 @@ typedef sentry_value_t (*sentry_event_function_t)(
  * See the `sentry_event_function_t` typedef above for more information.
  */
 SENTRY_API void sentry_options_set_before_send(
-    sentry_options_t *opts, sentry_event_function_t func, void *data);
+    sentry_options_t *opts, sentry_event_function_t func, void *closure);
path: include/sentry.h
user: login: supervacuus
body: I don't follow: why the change to `closure` from `data`, but below you use `data`? Similarly, in the example callback definition, you name the parameter `closure`.

I'm happy with any name, but we should use the same name for the same thing. So, if we prefer to use `data` for a parameter users can pass in at callback registration to later access it from within the callback, then let's use that name everywhere. When I say "everywhere", I mean across interfaces that provide a parameter for a similar purpose, as well as across internal names and callback definitions inside example or test code.

I don't think `closure` is a particularly good name, even though closures are used in other languages for that purpose. We don't _close_ over any lexical scope. I would probably call it `user_data`, but think `data` is fine in that context.

By the way, changing parameter _names_ is thankfully not a breaking change in C (neither for ABI nor API).
created_at: 2025-05-15T07:51:24Z
author_association: COLLABORATOR
diff_hunk: @@ -870,7 +870,7 @@ typedef sentry_value_t (*sentry_event_function_t)(
  * See the `sentry_event_function_t` typedef above for more information.
  */
 SENTRY_API void sentry_options_set_before_send(
-    sentry_options_t *opts, sentry_event_function_t func, void *data);
+    sentry_options_t *opts, sentry_event_function_t func, void *closure);
path: include/sentry.h
user: login: JoshuaMoelans
body: Thanks for bringing this up. I was also confused where the term 'closure' came from, but since it had been in use forever, I just changed my `data` parameter to `closure` (but forgot an occurrence, which luckily started this conversation)

I'll rename them all to `user_data`, since in the options we store them as `before_send_data`/`on_crash_data`/`before_transaction_data` anyway.
created_at: 2025-05-15T08:33:16Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-13T13:50:48Z
body: 
<!--
  0 failure: 
  0 warning: 
  1 messages
  
  DangerID: danger-id-Danger;
-->



<table>
  <thead>
    <tr>
      <th width="50"></th>
      <th width="100%" data-danger-table="true">Messages</th>
    </tr>
  </thead>
  <tbody><tr>
      <td>:book:</td>
      <td>Do not forget to update <a href="https://github.com/getsentry/sentry-docs">Sentry-docs</a> with your feature once the pull request gets approved.</td>
    </tr>
  </tbody>
</table>


<p align="right">
  Generated by :no_entry_sign: <a href="https://danger.systems/js">dangerJS</a> against bdc89fe320a3e8fd7c6d163b576babb7a0098fee
</p>

author_association: NONE
user: login: supervacuus
created_at: 2025-05-13T15:02:01Z
body: > Why do we have invoke_before_send passed into prepare_event ([see code](https://github.com/getsentry/sentry-native/blob/e511a8ed0be28e4fd9826854032df370ac0acd77/src/sentry_core.c#L554-L555)), and why is it true for sentry_core occurrences ([1](https://github.com/getsentry/sentry-native/blob/e511a8ed0be28e4fd9826854032df370ac0acd77/src/sentry_core.c#L466), [2](https://github.com/getsentry/sentry-native/blob/e511a8ed0be28e4fd9826854032df370ac0acd77/src/sentry_core.cL1407-L1408)), but passed as !options->on_crash_func for [breakpad](https://github.com/getsentry/sentry-native/blob/e511a8ed0be28e4fd9826854032df370ac0acd77/src/backends/sentry_backend_breakpad.cpp#L145-L146)?

Because this function is used for regular event capture, and during the capture of crashes from the handler. We want to invoke the `before_send` hook with regular events, because that is what we use it for. For crashes, we only run the `before_send` hook if no `on_crash` hook has been defined. This is documented in the respective hook docs.

> Where do we callback? Before or after applying scope?

As late as possible: after applying the scope. What would your argument be against that?

> (void* hint) parameter (which is unused for the other callbacks, but exists afaict), [see here](https://github.com/getsentry/sentry-native/pull/1236/files#diff-9c77242571e813d699b88663fdc7d7ad7bb5b481a3c1af8d34e1a9e242cbf076R1765-R1766)

The `hint` parameter isn't really applicable to the Native SDK, but we could pass in the ucontext or exception_ptrs if it is used in the crash context as we do for the `on_crash` hook (but in an untyped fashion). I don't think it is relevant to the `before_send_transaction`.

> Do we want to add before_transaction_data to modify transactions before sending? [see here](https://github.com/getsentry/sentry-native/pull/1236/files#diff-9c77242571e813d699b88663fdc7d7ad7bb5b481a3c1af8d34e1a9e242cbf076R1767)

I think so, but similar to the concrete place of calling the callback I would compare with other SDKs.
author_association: COLLABORATOR