Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tracing): Support basic sampling of transactions #620

Merged
merged 15 commits into from
Dec 15, 2021
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions include/sentry.h
Original file line number Diff line number Diff line change
Expand Up @@ -1226,6 +1226,57 @@ SENTRY_EXPERIMENTAL_API void sentry_options_set_traces_sample_rate(
SENTRY_EXPERIMENTAL_API double sentry_options_get_traces_sample_rate(
sentry_options_t *opts);

/* -- Performance Monitoring/Tracing APIs -- */

/**
* Constructs a new Transaction context to be passed
* into `sentry_start_transaction`.
*
* See
* https://docs.sentry.io/platforms/native/enriching-events/transaction-name/
* for an explanation of a Transaction's `name`, and
* https://develop.sentry.dev/sdk/performance/span-operations/ for conventions
* around an `operation`'s value.
*
* Also see https://develop.sentry.dev/sdk/event-payloads/transaction/#anatomy
* 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_context(
const char *name, const char *operation);

/**
* Sets the `name` of a Transaction on a `transaction_context`.
*/
SENTRY_EXPERIMENTAL_API void sentry_transaction_context_set_name(
sentry_value_t transaction_context, const char *name);

/**
* Sets the `operation` of a Transaction on a `transaction_context`.
*
* See https://develop.sentry.dev/sdk/performance/span-operations/ for
* conventions on `operation`s.
*/
SENTRY_EXPERIMENTAL_API void sentry_transaction_context_set_operation(
sentry_value_t transaction_context, const char *operation);

/**
* Sets the `sampled` field on a `transaction_context`. When turned on, the
* Transaction constructed from the `transaction_context` will bypass all
* sampling options and always be sent to sentry. If this is explicitly turned
* off in the `transaction_context`, the Transaction will never be sent to
* sentry.
*/
SENTRY_EXPERIMENTAL_API void sentry_transaction_context_set_sampled(
sentry_value_t transaction_context, int sampled);

/**
* Removes the sampled field on a `transaction_context`. The Transaction
* contructed from it will use the sampling rate as defined in `sentry_options`.
*/
SENTRY_EXPERIMENTAL_API void sentry_transaction_context_remove_sampled(
sentry_value_t transaction_context);

#ifdef __cplusplus
}
#endif
Expand Down
43 changes: 40 additions & 3 deletions src/sentry_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,45 @@ sentry_capture_event(sentry_value_t event)
return was_captured ? event_id : sentry_uuid_nil();
}

bool
sentry__is_unsampled(double sample_rate)
{
uint64_t rnd;
return sample_rate < 1.0 && !sentry__getrandom(&rnd, sizeof(rnd))
relaxolotl marked this conversation as resolved.
Show resolved Hide resolved
&& ((double)rnd / (double)UINT64_MAX) > sample_rate;
}

bool
sentry__should_skip_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);
}

bool skip = true;
SENTRY_WITH_OPTIONS (options) {
skip = sentry__is_unsampled(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)
{
sentry_value_t event_type = sentry_value_get_by_key(event, "type");
// Not a transaction
if (sentry_value_is_null(event_type)) {
return sentry__is_unsampled(options->sample_rate);
} else {
// 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"));
}
}

sentry_envelope_t *
sentry__prepare_event(const sentry_options_t *options, sentry_value_t event,
sentry_uuid_t *event_id)
Expand All @@ -395,9 +434,7 @@ sentry__prepare_event(const sentry_options_t *options, sentry_value_t event,
sentry__record_errors_on_current_session(1);
}

uint64_t rnd;
if (options->sample_rate < 1.0 && !sentry__getrandom(&rnd, sizeof(rnd))
&& ((double)rnd / (double)UINT64_MAX) > options->sample_rate) {
if (sentry__should_skip_event(options, event)) {
SENTRY_DEBUG("throwing away event due to sample rate");
goto fail;
}
Expand Down
8 changes: 8 additions & 0 deletions src/sentry_core.h
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,12 @@ void sentry__options_unlock(void);
for (sentry_options_t *Options = sentry__options_lock(); Options; \
sentry__options_unlock(), Options = NULL)

// these for now are only needed for tests
#ifdef SENTRY_UNITTEST
bool sentry__is_unsampled(double sample_rate);
bool sentry__should_skip_transaction(sentry_value_t tx_cxt);
bool sentry__should_skip_event(
const sentry_options_t *options, sentry_value_t event);
#endif

#endif
47 changes: 47 additions & 0 deletions src/sentry_value.c
Original file line number Diff line number Diff line change
Expand Up @@ -1124,6 +1124,53 @@ sentry_value_new_stacktrace(void **ips, size_t len)
return stacktrace;
}

sentry_value_t
sentry_value_new_transaction_context(const char *name, const char *operation)
{
sentry_value_t transaction_context = sentry_value_new_object();

sentry_transaction_context_set_name(transaction_context, name);
sentry_transaction_context_set_operation(transaction_context, operation);

return transaction_context;
}

void
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
// the transaction.
if (sentry_value_is_null(sv_name) || sentry__string_eq(name, "")) {
sentry_value_decref(sv_name);
sv_name = sentry_value_new_string("<unlabeled transaction>");
}
sentry_value_set_by_key(transaction_context, "name", sv_name);
}

void
sentry_transaction_context_set_operation(
sentry_value_t transaction_context, const char *operation)
{
sentry_value_set_by_key(
transaction_context, "op", sentry_value_new_string(operation));
}

void
sentry_transaction_context_set_sampled(
sentry_value_t transaction_context, int sampled)
{
sentry_value_set_by_key(
transaction_context, "sampled", sentry_value_new_bool(sampled));
}

void
sentry_transaction_context_remove_sampled(sentry_value_t transaction_context)
{
sentry_value_remove_by_key(transaction_context, "sampled");
}

static sentry_value_t
sentry__get_or_insert_values_list(sentry_value_t parent, const char *key)
{
Expand Down
1 change: 1 addition & 0 deletions tests/unit/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ add_executable(sentry_test_unit
test_mpack.c
test_path.c
test_ratelimiter.c
test_sampling.c
test_session.c
test_slice.c
test_symbolizer.c
Expand Down
67 changes: 67 additions & 0 deletions tests/unit/test_sampling.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#include "sentry_core.h"
#include "sentry_testsupport.h"

SENTRY_TEST(sampling_decision)
{
TEST_CHECK(sentry__is_unsampled(0.0));
TEST_CHECK(sentry__is_unsampled(1.0) == false);
TEST_CHECK(sentry__is_unsampled(2.0) == false);
}

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_context -> transaction_context_set_sampled ->
// start_transaction
// using transaction context in place of a full transaction for now.
sentry_value_t tx_cxt = sentry_value_new_transaction_context("honk");

sentry_transaction_context_set_sampled(tx_cxt, 0);
TEST_CHECK(sentry__should_skip_transaction(tx_cxt));

sentry_transaction_context_set_sampled(tx_cxt, 1);
TEST_CHECK(sentry__should_skip_transaction(tx_cxt) == false);

// fall back to default in sentry options (0.0) if sampled isn't there
sentry_transaction_context_remove_sampled(tx_cxt);
TEST_CHECK(sentry__should_skip_transaction(tx_cxt));

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);

sentry_value_decref(tx_cxt);
}

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);
}
40 changes: 40 additions & 0 deletions tests/unit/test_tracing.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,43 @@ SENTRY_TEST(basic_tracing_context)
sentry_value_decref(trace_context);
sentry_value_decref(span);
}

SENTRY_TEST(basic_transaction_context)
{
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"));
TEST_CHECK_STRING_EQUAL(tx_name, "<unlabeled transaction>");
const char *tx_op
= sentry_value_as_string(sentry_value_get_by_key(tx_cxt, "op"));
TEST_CHECK_STRING_EQUAL(tx_op, "");

sentry_value_decref(tx_cxt);
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, "<unlabeled transaction>");
TEST_CHECK_STRING_EQUAL(tx_op, "");

sentry_value_decref(tx_cxt);
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");

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, "<unlabeled transaction>");

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_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);
}
4 changes: 4 additions & 0 deletions tests/unit/tests.inc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ XX(basic_http_request_preparation_for_event)
XX(basic_http_request_preparation_for_event_with_attachment)
XX(basic_http_request_preparation_for_minidump)
XX(basic_tracing_context)
XX(basic_transaction_context)
XX(buildid_fallback)
XX(concurrent_init)
XX(count_sampled_events)
Expand Down Expand Up @@ -38,6 +39,9 @@ XX(procmaps_parser)
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)
Expand Down