From 418588eeb2b8126fc0d0647eed7a0a052d4b31c9 Mon Sep 17 00:00:00 2001 From: relaxolotl <5597345+relaxolotl@users.noreply.github.com> Date: Thu, 23 Dec 2021 12:10:09 -0500 Subject: [PATCH] feat(tracing): Allow manual creation and sending of spanless Transactions (#631) --- examples/example.c | 17 +++ include/sentry.h | 56 ++++++--- src/backends/sentry_backend_inproc.c | 2 + src/sentry_core.c | 144 +++++++++++++++++++---- src/sentry_core.h | 25 +++- src/sentry_scope.c | 3 +- src/sentry_value.c | 38 ++++--- tests/unit/test_sampling.c | 46 ++------ tests/unit/test_tracing.c | 164 ++++++++++++++++++++++++++- tests/unit/tests.inc | 4 +- 10 files changed, 398 insertions(+), 101 deletions(-) diff --git a/examples/example.c b/examples/example.c index 0ace0324e..8edca36fc 100644 --- a/examples/example.c +++ b/examples/example.c @@ -93,6 +93,10 @@ main(int argc, char **argv) options, sentry_transport_new(print_envelope)); } + if (has_arg(argc, argv, "capture-transaction")) { + sentry_options_set_traces_sample_rate(options, 1.0); + } + sentry_init(options); if (!has_arg(argc, argv, "no-setup")) { @@ -208,6 +212,19 @@ main(int argc, char **argv) sentry_capture_event(event); } + if (has_arg(argc, argv, "capture-transaction")) { + sentry_value_t tx_ctx + = sentry_value_new_transaction_context("I'm a little teapot", + "Short and stout here is my handle and here is my spout"); + + if (has_arg(argc, argv, "unsample-tx")) { + sentry_transaction_context_set_sampled(tx_ctx, 0); + } + + sentry_value_t tx = sentry_transaction_start(tx_ctx); + sentry_transaction_finish(tx); + } + // make sure everything flushes sentry_close(); diff --git a/include/sentry.h b/include/sentry.h index d0bcc485f..4ee29a958 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1238,8 +1238,8 @@ SENTRY_EXPERIMENTAL_API double sentry_options_get_traces_sample_rate( /* -- Performance Monitoring/Tracing APIs -- */ /** - * Constructs a new inert Transaction. The returned value needs to be passed - * into `sentry_start_transaction` in order to be recorded and sent to sentry. + * Constructs a new Transaction Context. The returned value needs to be passed + * into `sentry_transaction_start` in order to be recorded and sent to sentry. * * See * https://docs.sentry.io/platforms/native/enriching-events/transaction-name/ @@ -1251,37 +1251,65 @@ SENTRY_EXPERIMENTAL_API double sentry_options_get_traces_sample_rate( * for an explanation of `operation`, in addition to other properties and * actions that can be performed on a Transaction. */ -SENTRY_EXPERIMENTAL_API sentry_value_t sentry_value_new_transaction( +SENTRY_EXPERIMENTAL_API sentry_value_t sentry_value_new_transaction_context( const char *name, const char *operation); /** - * Sets the `name` of a Transaction. + * Sets the `name` on a Transaction Context, which will be used in the + * Transaction constructed off of the context. */ -SENTRY_EXPERIMENTAL_API void sentry_transaction_set_name( +SENTRY_EXPERIMENTAL_API void sentry_transaction_context_set_name( sentry_value_t transaction, const char *name); /** - * Sets the `operation` of a Transaction. + * Sets the `operation` on a Transaction Context, which will be used in the + * Transaction constructed off of the context * * See https://develop.sentry.dev/sdk/performance/span-operations/ for * conventions on `operation`s. */ -SENTRY_EXPERIMENTAL_API void sentry_transaction_set_operation( +SENTRY_EXPERIMENTAL_API void sentry_transaction_context_set_operation( sentry_value_t transaction, const char *operation); /** - * Sets the `sampled` field on a Transaction. When turned on, the Transaction - * will bypass all sampling options and always be sent to sentry. If this is - * explicitly turned off in the Transaction, it will never be sent to sentry. + * Sets the `sampled` field on a Transaction Context, which will be used in the + * Transaction constructed off of the context. + * + * When passed any value above 0, the Transaction will bypass all sampling + * options and always be sent to sentry. If passed 0, this Transaction and its + * child spans will never be sent to sentry. */ -SENTRY_EXPERIMENTAL_API void sentry_transaction_set_sampled( +SENTRY_EXPERIMENTAL_API void sentry_transaction_context_set_sampled( sentry_value_t transaction, int sampled); /** - * Removes the sampled field on a Transaction. The Transaction will use the - * sampling rate as defined in `sentry_options`. + * Removes the sampled field on a Transaction Context, which will be used in the + * Transaction constructed off of the context. + * + * The Transaction will use the sampling rate as defined in `sentry_options`. + */ +SENTRY_EXPERIMENTAL_API void sentry_transaction_context_remove_sampled( + sentry_value_t transaction); + +/** + * Starts a new Transaction based on the provided context, restored from an + * external integration (i.e. a span from a different SDK) or manually + * constructed by a user. + * + * Takes ownership of `transaction_context`. + */ +SENTRY_EXPERIMENTAL_API sentry_value_t sentry_transaction_start( + sentry_value_t transaction_context); + +/** + * Finishes and sends a transaction to sentry. The event ID of the transaction + * will be returned if this was successful; A nil UUID will be returned + * otherwise. + * + * Always takes ownership of `transaction`, regardless of whether the operation + * was successful or not. */ -SENTRY_EXPERIMENTAL_API void sentry_transaction_remove_sampled( +SENTRY_EXPERIMENTAL_API sentry_uuid_t sentry_transaction_finish( sentry_value_t transaction); #ifdef __cplusplus diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index c15493dab..2318e2774 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -260,6 +260,8 @@ handle_ucontext(const sentry_ucontext_t *uctx) sentry_envelope_t *envelope = sentry__prepare_event(options, event, NULL); + // TODO(tracing): Revisit when investigating transaction flushing during + // hard crashes. sentry_session_t *session = sentry__end_current_session_with_status( SENTRY_SESSION_STATUS_CRASHED); diff --git a/src/sentry_core.c b/src/sentry_core.c index ad7ea8882..1d9380ce9 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -16,6 +16,7 @@ #include "sentry_session.h" #include "sentry_string.h" #include "sentry_sync.h" +#include "sentry_tracing.h" #include "sentry_transport.h" #include "sentry_value.h" @@ -377,10 +378,11 @@ sentry__capture_event(sentry_value_t event) sentry_envelope_t *envelope = NULL; bool was_captured = false; + bool was_sent = false; SENTRY_WITH_OPTIONS (options) { was_captured = true; if (sentry__event_is_transaction(event)) { - return sentry_uuid_nil(); + envelope = sentry__prepare_transaction(options, event, &event_id); } else { envelope = sentry__prepare_event(options, event, &event_id); } @@ -395,14 +397,14 @@ sentry__capture_event(sentry_value_t event) mut_options->session->init = false; sentry__options_unlock(); } - sentry__capture_envelope(options->transport, envelope); + was_sent = true; } } if (!was_captured) { sentry_value_decref(event); } - return was_captured ? event_id : sentry_uuid_nil(); + return was_sent ? event_id : sentry_uuid_nil(); } bool @@ -414,32 +416,20 @@ sentry__roll_dice(double probability) } bool -sentry__should_skip_transaction(sentry_value_t tx_cxt) +sentry__should_send_transaction(sentry_value_t tx_cxt) { sentry_value_t context_setting = sentry_value_get_by_key(tx_cxt, "sampled"); if (!sentry_value_is_null(context_setting)) { - return !sentry_value_is_true(context_setting); + return sentry_value_is_true(context_setting); } - bool skip = true; + bool send = false; SENTRY_WITH_OPTIONS (options) { - skip = !sentry__roll_dice(options->traces_sample_rate); - // TODO: run through traces sampler function if rate is unavailable - } - return skip; -} - -bool -sentry__should_skip_event(const sentry_options_t *options, sentry_value_t event) -{ - if (sentry__event_is_transaction(event)) { - // The sampling decision should already be made for transactions - // during their construction. No need to recalculate here. - // See `sentry__should_skip_transaction`. - return !sentry_value_is_true(sentry_value_get_by_key(event, "sampled")); - } else { - return !sentry__roll_dice(options->sample_rate); + send = sentry__roll_dice(options->traces_sample_rate); + // TODO(tracing): Run through traces sampler function if rate is + // unavailable. } + return send; } sentry_envelope_t * @@ -452,7 +442,8 @@ sentry__prepare_event(const sentry_options_t *options, sentry_value_t event, sentry__record_errors_on_current_session(1); } - if (sentry__should_skip_event(options, event)) { + bool should_skip = !sentry__roll_dice(options->sample_rate); + if (should_skip) { SENTRY_DEBUG("throwing away event due to sample rate"); goto fail; } @@ -507,6 +498,36 @@ sentry__prepare_event(const sentry_options_t *options, sentry_value_t event, return NULL; } +sentry_envelope_t * +sentry__prepare_transaction(const sentry_options_t *options, + sentry_value_t transaction, sentry_uuid_t *event_id) +{ + sentry_envelope_t *envelope = NULL; + + SENTRY_WITH_SCOPE (scope) { + SENTRY_TRACE("merging scope into event"); + // Don't include debugging info + sentry_scope_mode_t mode = SENTRY_SCOPE_ALL & ~SENTRY_SCOPE_MODULES + & ~SENTRY_SCOPE_STACKTRACES; + sentry__scope_apply_to_event(scope, options, transaction, mode); + } + + sentry__ensure_event_id(transaction, event_id); + envelope = sentry__envelope_new(); + if (!envelope || !sentry__envelope_add_transaction(envelope, transaction)) { + goto fail; + } + + // TODO(tracing): Revisit when adding attachment support for transactions. + + return envelope; + +fail: + sentry_envelope_free(envelope); + sentry_value_decref(transaction); + return NULL; +} + void sentry_handle_exception(const sentry_ucontext_t *uctx) { @@ -688,3 +709,80 @@ sentry_set_level(sentry_level_t level) scope->level = level; } } + +sentry_value_t +sentry_transaction_start(sentry_value_t tx_cxt) +{ + sentry_value_t tx = sentry_value_new_event(); + + // TODO(tracing): stuff transaction into the scope + bool should_sample = sentry__should_send_transaction(tx_cxt); + sentry_value_set_by_key( + tx, "sampled", sentry_value_new_bool(should_sample)); + + // Avoid having this show up in the payload at all if it doesn't have a + // valid value + sentry_value_t parent_span + = sentry_value_get_by_key_owned(tx_cxt, "parent_span_id"); + if (sentry_value_get_length(parent_span) > 0) { + sentry_value_set_by_key(tx, "parent_span_id", parent_span); + } else { + sentry_value_decref(parent_span); + } + sentry_value_set_by_key( + tx, "trace_id", sentry_value_get_by_key_owned(tx_cxt, "trace_id")); + sentry_value_set_by_key( + tx, "span_id", sentry_value_get_by_key_owned(tx_cxt, "trace_id")); + sentry_value_set_by_key( + tx, "transaction", sentry_value_get_by_key_owned(tx_cxt, "name")); + sentry_value_set_by_key(tx, "start_timestamp", + sentry__value_new_string_owned( + sentry__msec_time_to_iso8601(sentry__msec_time()))); + + sentry_value_decref(tx_cxt); + + return tx; +} + +sentry_uuid_t +sentry_transaction_finish(sentry_value_t tx) +{ + // The sampling decision should already be made for transactions during + // their construction. No need to recalculate here. See + // `sentry__should_skip_transaction`. + sentry_value_t sampled = sentry_value_get_by_key(tx, "sampled"); + if (!sentry_value_is_null(sampled) && !sentry_value_is_true(sampled)) { + SENTRY_DEBUG("throwing away transaction due to sample rate or " + "user-provided sampling value in transaction context"); + sentry_value_decref(tx); + // TODO(tracing): remove from scope + return sentry_uuid_nil(); + } + + sentry_value_set_by_key(tx, "type", sentry_value_new_string("transaction")); + sentry_value_set_by_key(tx, "timestamp", + sentry__value_new_string_owned( + sentry__msec_time_to_iso8601(sentry__msec_time()))); + sentry_value_set_by_key(tx, "level", sentry_value_new_string("info")); + + // TODO(tracing): add tracestate + // set up trace context so it mirrors the final json value + sentry_value_set_by_key(tx, "status", sentry_value_new_string("ok")); + + sentry_value_t trace_context = sentry__span_get_trace_context(tx); + sentry_value_t contexts = sentry_value_new_object(); + sentry_value_set_by_key(contexts, "trace", trace_context); + sentry_value_set_by_key(tx, "contexts", contexts); + + // clean up trace context fields + sentry_value_remove_by_key(tx, "trace_id"); + sentry_value_remove_by_key(tx, "span_id"); + sentry_value_remove_by_key(tx, "parent_span_id"); + sentry_value_remove_by_key(tx, "op"); + sentry_value_remove_by_key(tx, "description"); + sentry_value_remove_by_key(tx, "status"); + + // This takes ownership of the transaction, generates an event ID, merges + // scope + return sentry__capture_event(tx); +} diff --git a/src/sentry_core.h b/src/sentry_core.h index d7fc9e4e2..542691921 100644 --- a/src/sentry_core.h +++ b/src/sentry_core.h @@ -35,7 +35,8 @@ bool sentry__should_skip_upload(void); bool sentry__event_is_transaction(sentry_value_t event); /** - * Convert the given event into an envelope. + * Convert the given event into an envelope. This assumes that the event + * being passed in is not a transaction. * * More specifically, it will do the following things: * - sample the event, possibly discarding it, @@ -56,6 +57,24 @@ sentry_envelope_t *sentry__prepare_event(const sentry_options_t *options, */ sentry_uuid_t sentry__capture_event(sentry_value_t event); +/** + * Convert the given transaction into an envelope. This assumes that the + * event being passed in is a transaction. + * + * It will do the following things: + * - discard the transaction if it is unsampled + * - apply the scope to the transaction + * - add the transaction to a new envelope + * - add any attachments to the envelope + * + * The function will ensure the transaction has a UUID and write it into the + * `event_id` out-parameter. This takes ownership of the transaction, which + * means that the caller no longer needs to call `sentry_value_decref` on the + * transaction. + */ +sentry_envelope_t *sentry__prepare_transaction(const sentry_options_t *options, + sentry_value_t transaction, sentry_uuid_t *event_id); + /** * This function will submit the `envelope` to the given `transport`, first * checking for consent. @@ -98,9 +117,7 @@ void sentry__options_unlock(void); // these for now are only needed for tests #ifdef SENTRY_UNITTEST bool sentry__roll_dice(double probability); -bool sentry__should_skip_transaction(sentry_value_t tx_cxt); -bool sentry__should_skip_event( - const sentry_options_t *options, sentry_value_t event); +bool sentry__should_send_transaction(sentry_value_t tx_cxt); #endif #endif diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 5052f023f..3edf49888 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -268,7 +268,8 @@ sentry__scope_apply_to_event(const sentry_scope_t *scope, PLACE_STRING("dist", options->dist); PLACE_STRING("environment", options->environment); - if (IS_NULL("level")) { + // is not transaction and has no level + if (IS_NULL("type") && IS_NULL("level")) { SET("level", sentry__value_new_level(scope->level)); } diff --git a/src/sentry_value.c b/src/sentry_value.c index c71c68ecf..9217267d8 100644 --- a/src/sentry_value.c +++ b/src/sentry_value.c @@ -1125,18 +1125,27 @@ sentry_value_new_stacktrace(void **ips, size_t len) } sentry_value_t -sentry_value_new_transaction(const char *name, const char *operation) +sentry_value_new_transaction_context(const char *name, const char *operation) { - sentry_value_t transaction = sentry_value_new_object(); + sentry_value_t transaction_context = sentry_value_new_object(); - sentry_transaction_set_name(transaction, name); - sentry_transaction_set_operation(transaction, operation); + sentry_uuid_t trace_id = sentry_uuid_new_v4(); + sentry_value_set_by_key(transaction_context, "trace_id", + sentry__value_new_internal_uuid(&trace_id)); - return transaction; + sentry_uuid_t span_id = sentry_uuid_new_v4(); + sentry_value_set_by_key( + transaction_context, "span_id", sentry__value_new_span_uuid(&span_id)); + + sentry_transaction_context_set_name(transaction_context, name); + sentry_transaction_context_set_operation(transaction_context, operation); + + return transaction_context; } void -sentry_transaction_set_name(sentry_value_t transaction, const char *name) +sentry_transaction_context_set_name( + sentry_value_t transaction_context, const char *name) { sentry_value_t sv_name = sentry_value_new_string(name); // TODO: Consider doing this checking right before sending or flushing @@ -1145,28 +1154,29 @@ sentry_transaction_set_name(sentry_value_t transaction, const char *name) sentry_value_decref(sv_name); sv_name = sentry_value_new_string(""); } - sentry_value_set_by_key(transaction, "name", sv_name); + sentry_value_set_by_key(transaction_context, "name", sv_name); } void -sentry_transaction_set_operation( - sentry_value_t transaction, const char *operation) +sentry_transaction_context_set_operation( + sentry_value_t transaction_context, const char *operation) { sentry_value_set_by_key( - transaction, "op", sentry_value_new_string(operation)); + transaction_context, "op", sentry_value_new_string(operation)); } void -sentry_transaction_set_sampled(sentry_value_t transaction, int sampled) +sentry_transaction_context_set_sampled( + sentry_value_t transaction_context, int sampled) { sentry_value_set_by_key( - transaction, "sampled", sentry_value_new_bool(sampled)); + transaction_context, "sampled", sentry_value_new_bool(sampled)); } void -sentry_transaction_remove_sampled(sentry_value_t transaction) +sentry_transaction_context_remove_sampled(sentry_value_t transaction_context) { - sentry_value_remove_by_key(transaction, "sampled"); + sentry_value_remove_by_key(transaction_context, "sampled"); } static sentry_value_t diff --git a/tests/unit/test_sampling.c b/tests/unit/test_sampling.c index 8ab8d6ab4..bcedcbbbb 100644 --- a/tests/unit/test_sampling.c +++ b/tests/unit/test_sampling.c @@ -13,54 +13,24 @@ SENTRY_TEST(sampling_transaction) sentry_options_t *options = sentry_options_new(); TEST_CHECK(sentry_init(options) == 0); - // TODO: replace with proper construction of a transaction, e.g. - // new_transaction -> transaction_set_sampled -> start_transaction - sentry_value_t tx_cxt = sentry_value_new_transaction("honk", NULL); + sentry_value_t tx_cxt = sentry_value_new_transaction_context("honk", NULL); - sentry_transaction_set_sampled(tx_cxt, 0); - TEST_CHECK(sentry__should_skip_transaction(tx_cxt)); + sentry_transaction_context_set_sampled(tx_cxt, 0); + TEST_CHECK(sentry__should_send_transaction(tx_cxt) == false); - sentry_transaction_set_sampled(tx_cxt, 1); - TEST_CHECK(sentry__should_skip_transaction(tx_cxt) == false); + sentry_transaction_context_set_sampled(tx_cxt, 1); + TEST_CHECK(sentry__should_send_transaction(tx_cxt)); // fall back to default in sentry options (0.0) if sampled isn't there - sentry_transaction_remove_sampled(tx_cxt); - TEST_CHECK(sentry__should_skip_transaction(tx_cxt)); + sentry_transaction_context_remove_sampled(tx_cxt); + TEST_CHECK(sentry__should_send_transaction(tx_cxt) == false); options = sentry_options_new(); sentry_options_set_traces_sample_rate(options, 1.0); TEST_CHECK(sentry_init(options) == 0); - TEST_CHECK(sentry__should_skip_transaction(tx_cxt) == false); + TEST_CHECK(sentry__should_send_transaction(tx_cxt)); sentry_value_decref(tx_cxt); sentry_close(); } - -SENTRY_TEST(sampling_event) -{ - // default is to sample all (error) events, and to not sample any - // transactions - sentry_options_t *options = sentry_options_new(); - - sentry_value_t event = sentry_value_new_object(); - sentry_value_set_by_key(event, "sampled", sentry_value_new_bool(0)); - - // events ignore sampled field if they're not transactions - TEST_CHECK(sentry__should_skip_event(options, event) == false); - - // respect sampled field if it is a transaction - sentry_value_set_by_key( - event, "type", sentry_value_new_string("transaction")); - TEST_CHECK(sentry__should_skip_event(options, event)); - - // if the sampled field isn't set on a transaction, don't ever send - // transactions even if the option says to do so - sentry_value_remove_by_key(event, "sampled"); - TEST_CHECK(sentry__should_skip_event(options, event)); - sentry_options_set_traces_sample_rate(options, 1.0); - TEST_CHECK(sentry__should_skip_event(options, event)); - - sentry_value_decref(event); - sentry_options_free(options); -} diff --git a/tests/unit/test_tracing.c b/tests/unit/test_tracing.c index cce9bbc7f..782b43d6c 100644 --- a/tests/unit/test_tracing.c +++ b/tests/unit/test_tracing.c @@ -36,7 +36,7 @@ SENTRY_TEST(basic_tracing_context) SENTRY_TEST(basic_transaction) { - sentry_value_t tx_cxt = sentry_value_new_transaction(NULL, NULL); + sentry_value_t tx_cxt = sentry_value_new_transaction_context(NULL, NULL); TEST_CHECK(!sentry_value_is_null(tx_cxt)); const char *tx_name = sentry_value_as_string(sentry_value_get_by_key(tx_cxt, "name")); @@ -44,32 +44,184 @@ SENTRY_TEST(basic_transaction) const char *tx_op = sentry_value_as_string(sentry_value_get_by_key(tx_cxt, "op")); TEST_CHECK_STRING_EQUAL(tx_op, ""); + TEST_CHECK( + !sentry_value_is_null(sentry_value_get_by_key(tx_cxt, "trace_id"))); + TEST_CHECK( + !sentry_value_is_null(sentry_value_get_by_key(tx_cxt, "span_id"))); sentry_value_decref(tx_cxt); - tx_cxt = sentry_value_new_transaction("", ""); + tx_cxt = sentry_value_new_transaction_context("", ""); TEST_CHECK(!sentry_value_is_null(tx_cxt)); tx_name = sentry_value_as_string(sentry_value_get_by_key(tx_cxt, "name")); TEST_CHECK_STRING_EQUAL(tx_name, ""); TEST_CHECK_STRING_EQUAL(tx_op, ""); + TEST_CHECK( + !sentry_value_is_null(sentry_value_get_by_key(tx_cxt, "trace_id"))); + TEST_CHECK( + !sentry_value_is_null(sentry_value_get_by_key(tx_cxt, "span_id"))); sentry_value_decref(tx_cxt); - tx_cxt = sentry_value_new_transaction("honk.beep", "beepbeep"); + tx_cxt = sentry_value_new_transaction_context("honk.beep", "beepbeep"); tx_name = sentry_value_as_string(sentry_value_get_by_key(tx_cxt, "name")); TEST_CHECK_STRING_EQUAL(tx_name, "honk.beep"); tx_op = sentry_value_as_string(sentry_value_get_by_key(tx_cxt, "op")); TEST_CHECK_STRING_EQUAL(tx_op, "beepbeep"); + TEST_CHECK( + !sentry_value_is_null(sentry_value_get_by_key(tx_cxt, "trace_id"))); + TEST_CHECK( + !sentry_value_is_null(sentry_value_get_by_key(tx_cxt, "span_id"))); - sentry_transaction_set_name(tx_cxt, ""); + sentry_transaction_context_set_name(tx_cxt, ""); tx_name = sentry_value_as_string(sentry_value_get_by_key(tx_cxt, "name")); TEST_CHECK_STRING_EQUAL(tx_name, ""); - sentry_transaction_set_operation(tx_cxt, ""); + sentry_transaction_context_set_operation(tx_cxt, ""); tx_op = sentry_value_as_string(sentry_value_get_by_key(tx_cxt, "op")); TEST_CHECK_STRING_EQUAL(tx_op, ""); - sentry_transaction_set_sampled(tx_cxt, 1); + sentry_transaction_context_set_sampled(tx_cxt, 1); TEST_CHECK( sentry_value_is_true(sentry_value_get_by_key(tx_cxt, "sampled")) == 1); sentry_value_decref(tx_cxt); } + +static void +send_transaction_envelope_test_basic(sentry_envelope_t *envelope, void *data) +{ + uint64_t *called = data; + *called += 1; + + sentry_value_t transaction = sentry_envelope_get_transaction(envelope); + TEST_CHECK(!sentry_value_is_null(transaction)); + const char *event_id = sentry_value_as_string( + sentry_value_get_by_key(transaction, "event_id")); + TEST_CHECK_STRING_EQUAL(event_id, "4c035723-8638-4c3a-923f-2ab9d08b4018"); + + if (*called == 1) { + const char *type = sentry_value_as_string( + sentry_value_get_by_key(transaction, "type")); + TEST_CHECK_STRING_EQUAL(type, "transaction"); + const char *name = sentry_value_as_string( + sentry_value_get_by_key(transaction, "transaction")); + TEST_CHECK_STRING_EQUAL(name, "honk"); + } + + sentry_envelope_free(envelope); +} + +SENTRY_TEST(basic_function_transport_transaction) +{ + uint64_t called = 0; + + sentry_options_t *options = sentry_options_new(); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + + sentry_transport_t *transport + = sentry_transport_new(send_transaction_envelope_test_basic); + sentry_transport_set_state(transport, &called); + sentry_options_set_transport(options, transport); + + sentry_options_set_traces_sample_rate(options, 1.0); + sentry_options_set_require_user_consent(options, true); + sentry_init(options); + + sentry_value_t transaction = sentry_value_new_transaction_context( + "How could you", "Don't capture this."); + transaction = sentry_transaction_start(transaction); + sentry_uuid_t event_id = sentry_transaction_finish(transaction); + // TODO: `sentry_capture_event` acts as if the event was sent if user + // consent was not given + TEST_CHECK(!sentry_uuid_is_nil(&event_id)); + sentry_user_consent_give(); + + transaction = sentry_value_new_transaction_context("honk", "beep"); + transaction = sentry_transaction_start(transaction); + event_id = sentry_transaction_finish(transaction); + TEST_CHECK(!sentry_uuid_is_nil(&event_id)); + + sentry_user_consent_revoke(); + transaction = sentry_value_new_transaction_context( + "How could you again", "Don't capture this either."); + transaction = sentry_transaction_start(transaction); + event_id = sentry_transaction_finish(transaction); + // TODO: `sentry_capture_event` acts as if the event was sent if user + // consent was not given + TEST_CHECK(!sentry_uuid_is_nil(&event_id)); + + sentry_close(); + + TEST_CHECK_INT_EQUAL(called, 1); +} + +SENTRY_TEST(transport_sampling_transactions) +{ + uint64_t called_transport = 0; + + sentry_options_t *options = sentry_options_new(); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + + sentry_transport_t *transport + = sentry_transport_new(send_transaction_envelope_test_basic); + sentry_transport_set_state(transport, &called_transport); + sentry_options_set_transport(options, transport); + + sentry_options_set_traces_sample_rate(options, 0.75); + sentry_init(options); + + uint64_t sent_transactions = 0; + for (int i = 0; i < 100; i++) { + sentry_value_t transaction + = sentry_value_new_transaction_context("honk", "beep"); + transaction = sentry_transaction_start(transaction); + sentry_uuid_t event_id = sentry_transaction_finish(transaction); + if (!sentry_uuid_is_nil(&event_id)) { + sent_transactions += 1; + } + } + + sentry_close(); + + // well, its random after all + TEST_CHECK(called_transport > 50 && called_transport < 100); + TEST_CHECK(called_transport == sent_transactions); +} + +static sentry_value_t +before_send(sentry_value_t event, void *UNUSED(hint), void *data) +{ + uint64_t *called = data; + *called += 1; + + sentry_value_decref(event); + return sentry_value_new_null(); +} + +SENTRY_TEST(transactions_skip_before_send) +{ + uint64_t called_beforesend = 0; + uint64_t called_transport = 0; + + sentry_options_t *options = sentry_options_new(); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + + sentry_transport_t *transport + = sentry_transport_new(send_transaction_envelope_test_basic); + sentry_transport_set_state(transport, &called_transport); + sentry_options_set_transport(options, transport); + + sentry_options_set_traces_sample_rate(options, 1.0); + sentry_options_set_before_send(options, before_send, &called_beforesend); + sentry_init(options); + + sentry_value_t transaction + = sentry_value_new_transaction_context("honk", "beep"); + transaction = sentry_transaction_start(transaction); + sentry_uuid_t event_id = sentry_transaction_finish(transaction); + TEST_CHECK(!sentry_uuid_is_nil(&event_id)); + + sentry_close(); + + TEST_CHECK_INT_EQUAL(called_transport, 1); + TEST_CHECK_INT_EQUAL(called_beforesend, 0); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index cc0f5bf43..3c904fa2a 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -1,6 +1,7 @@ XX(background_worker) XX(basic_consent_tracking) XX(basic_function_transport) +XX(basic_function_transport_transaction) XX(basic_http_request_preparation_for_event) XX(basic_http_request_preparation_for_event_with_attachment) XX(basic_http_request_preparation_for_minidump) @@ -42,13 +43,14 @@ XX(rate_limit_parsing) XX(recursive_paths) XX(sampling_before_send) XX(sampling_decision) -XX(sampling_event) XX(sampling_transaction) XX(serialize_envelope) XX(session_basics) XX(slice) XX(symbolizer) XX(task_queue) +XX(transactions_skip_before_send) +XX(transport_sampling_transactions) XX(uninitialized) XX(unwinder) XX(url_parsing_complete)