diff --git a/examples/example.c b/examples/example.c index 0ace0324e6..29e5fd1404 100644 --- a/examples/example.c +++ b/examples/example.c @@ -93,6 +93,14 @@ 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); + } + + if (has_arg(argc, argv, "child-spans")) { + sentry_options_set_max_spans(options, 5); + } + sentry_init(options); if (!has_arg(argc, argv, "no-setup")) { @@ -208,6 +216,33 @@ 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("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_transaction_start(tx_ctx); + + if (has_arg(argc, argv, "child-spans")) { + sentry_value_t child_ctx = sentry_span_start_child( + sentry_value_new_null(), "littler.teapot", NULL); + sentry_value_t grandchild_ctx + = sentry_span_start_child(child_ctx, "littlest.teapot", NULL); + + sentry_value_t unfinished_ctx + = sentry_span_start_child(child_ctx, "large.teapot", NULL); + + sentry_value_decref(unfinished_ctx); + sentry_span_finish(grandchild_ctx); + sentry_span_finish(child_ctx); + } + + sentry_transaction_finish(); + } + // make sure everything flushes sentry_close(); diff --git a/include/sentry.h b/include/sentry.h index 3675e5527e..a1fd9558f2 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -554,13 +554,21 @@ typedef struct sentry_envelope_s sentry_envelope_t; SENTRY_API void sentry_envelope_free(sentry_envelope_t *envelope); /** - * Given an envelope returns the embedded event if there is one. + * Given an Envelope, returns the embedded Event if there is one. * - * This returns a borrowed value to the event in the envelope. + * This returns a borrowed value to the Event in the Envelope. */ SENTRY_API sentry_value_t sentry_envelope_get_event( const sentry_envelope_t *envelope); +/** + * Given an Envelope, returns the embedded Transaction if there is one. + * + * This returns a borrowed value to the Transaction in the Envelope. + */ +SENTRY_EXPERIMENTAL_API sentry_value_t sentry_envelope_get_transaction( + const sentry_envelope_t *envelope); + /** * Serializes the envelope. * @@ -1230,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/ @@ -1243,39 +1251,108 @@ 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_remove_sampled( +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. + * + * `sentry_transaction_finish` should be called after this is invoked, otherwise + * the Transaction will not be sent to sentry. New spans cannot be created + * unless there exists an active Transaction. + */ +SENTRY_EXPERIMENTAL_API void sentry_transaction_start( + sentry_value_t transaction_context); + +/** + * Finishes and sends the current active Transaction to sentry. Any unfinished + * spans are removed from the Transaction before it is sent over. + * + * No new spans can be created after this is invoked unless a new Transaction is + * started via `sentry_transaction_start`. + */ +SENTRY_EXPERIMENTAL_API sentry_uuid_t sentry_transaction_finish(); + +/** + * Starts a new Span. + * + * If `parent_span` is `sentry_value_null`, then the current active Transaction + * is used as the parent for the new Span. An active Transaction must be created + * via `sentry_transaction_start` in order for the Span to be successfully + * created. + * + * If `parent_span` is another Span, it must belong to the current active + * Transaction in order for Span creation to succeed. This will take ownership + * of any `parent_span`s that do reference non-existent Spans in the current + * active Transaction. + * + * Both operation and description can be null, but it is recommended to supply + * the former. See https://develop.sentry.dev/sdk/performance/span-operations/ + * for conventions around operations. + * + * See https://develop.sentry.dev/sdk/event-payloads/span/ for a description of + * the created Span's properties and expectations for operation and description. + * + * Returns a value that should be passed into `sentry_span_finish`. Not + * finishing the Span means it will be discarded, and will not be sent to + * sentry. `sentry_value_null` will be returned, and `parent_span`'s ownership + * will be taken if the child Span could not be created. + */ +SENTRY_EXPERIMENTAL_API sentry_value_t sentry_span_start_child( + sentry_value_t parent_span, char *operation, char *description); + +/** + * Finishes a span. + * + * Returns a value that should be passed into `sentry_span_finish`. Not + * finishing the span means it will be discarded, and will not be sent to + * sentry. + * + * This takes ownership of `span`, as child spans must always occur within the + * total duration of a parent span and cannot take a longer amount of time to + * complete than the parent span they belong to. + */ +SENTRY_EXPERIMENTAL_API void sentry_span_finish(sentry_value_t span); + #ifdef __cplusplus } #endif diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index c15493dab7..2318e27746 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 6eb2e84188..4054fe0f7f 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); } @@ -389,21 +391,21 @@ sentry__capture_event(sentry_value_t event) SENTRY_WITH_OPTIONS_MUT (mut_options) { sentry__envelope_add_session( envelope, mut_options->session); - // we're assuming that if a session is added to an envelope - // it will be sent onwards. This means we now need to set - // the init flag to false because we're no longer the - // initial session update. + // we're assuming that if a session is added to an + // envelope it will be sent onwards. This means we now + // need to set the init flag to false because we're no + // longer the initial session update. mut_options->session->init = false; } } - 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 @@ -415,32 +417,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 * @@ -453,7 +443,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; } @@ -508,6 +499,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) { @@ -689,3 +710,207 @@ sentry_set_level(sentry_level_t level) scope->level = level; } } + +void +sentry_transaction_start(sentry_value_t tx_cxt) +{ + // TODO: it would be nice if we could just merge tx_cxt into tx. + // `sentry_value_new_transaction_event()` is also an option, but risks + // causing more confusion as there's already a + // `sentry_value_new_transaction`. The ending timestamp is stripped as well + // to avoid misleading ourselves later down the line. + sentry_value_t tx = sentry_value_new_event(); + sentry_value_remove_by_key(tx, "timestamp"); + + 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); + } + 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, "transaction")); + sentry_value_set_by_key( + tx, "status", sentry_value_get_by_key_owned(tx_cxt, "status")); + sentry_value_set_by_key(tx, "start_timestamp", + sentry__value_new_string_owned( + sentry__msec_time_to_iso8601(sentry__msec_time()))); + + sentry__scope_set_span(tx); + sentry_value_decref(tx_cxt); +} + +sentry_uuid_t +sentry_transaction_finish() +{ + sentry_value_t tx = sentry_value_new_null(); + SENTRY_WITH_SCOPE (scope) { + if (sentry_value_is_null(scope->span)) { + SENTRY_DEBUG("could not find a transaction on the scope to finish"); + return sentry_uuid_nil(); + } + + // 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(scope->span, "sampled"); + if (!sentry_value_is_true(sampled)) { + SENTRY_DEBUG("throwing away transaction due to sample rate or " + "user-provided sampling value in transaction context"); + sentry__scope_remove_span(); + return sentry_uuid_nil(); + } + tx = sentry__value_clone(scope->span); + } + sentry__scope_remove_span(); + if (sentry_value_is_null(tx)) { + SENTRY_DEBUG("could not find a transaction on the scope to finish"); + return sentry_uuid_nil(); + } + + sentry_value_remove_by_key(tx, "sampled"); + + 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()))); + // TODO: This might not actually be necessary. Revisit after talking to + // the relay team about this. + sentry_value_set_by_key(tx, "level", sentry_value_new_string("info")); + + sentry_value_t name = sentry_value_get_by_key(tx, "transaction"); + if (sentry_value_is_null(name) || sentry_value_get_length(name) == 0) { + sentry_value_set_by_key(tx, "transaction", + sentry_value_new_string("")); + } + + // TODO: add tracestate + 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"); + + sentry_value_t spans = sentry_value_get_by_key(tx, "spans"); + size_t span_count = sentry_value_get_length(spans); + // Go backwards to avoid accidentally skipping elements + for (size_t i = span_count; i > 0; i--) { + // TODO: Assume that tags and data from scope do not need to be merged + // into spans. This may be completely wrong. + bool should_remove = false; + { + sentry_value_t span = sentry_value_get_by_index(spans, i - 1); + should_remove = sentry_value_is_null( + sentry_value_get_by_key(span, "timestamp")); + } + if (should_remove) { + SENTRY_DEBUG("dropped an unfinished span from transaction"); + sentry_value_remove_by_index(spans, i - 1); + } + } + + // This decrefs for us, generates an event ID, merges scope + return sentry__capture_event(tx); +} + +sentry_value_t +sentry_span_start_child( + sentry_value_t parent_span_context, char *operation, char *description) +{ + size_t max_spans = SENTRY_SPANS_MAX; + SENTRY_WITH_OPTIONS (options) { + max_spans = options->max_spans; + } + + sentry_value_t child_span_context = sentry_value_new_null(); + size_t span_count = 0; + SENTRY_WITH_SCOPE_MUT (scope) { + // There isn't an active transaction. This span has nothing to attach + // to. + if (sentry_value_is_null(scope->span)) { + return sentry_value_new_null(); + } + // Aggressively discard spans if a transaction is unsampled to avoid + // wasting memory + sentry_value_t sampled + = sentry_value_get_by_key(scope->span, "sampled"); + if (!sentry_value_is_true(sampled)) { + return sentry_value_new_null(); + } + sentry_value_t spans = sentry_value_get_by_key(scope->span, "spans"); + span_count = sentry_value_get_length(spans); + if (span_count >= max_spans) { + return sentry_value_new_null(); + } + // TODO: if the parent span can't be found in the current active + // transaction, take ownership of the parent span context and return + // null. + + sentry_value_t parent; + if (sentry_value_is_null(parent_span_context)) { + parent = scope->span; + } else { + parent = parent_span_context; + } + + sentry_value_t child = sentry__value_new_span(parent, operation); + sentry_uuid_t span_id = sentry_uuid_new_v4(); + sentry_value_set_by_key( + child, "span_id", sentry__value_new_span_uuid(&span_id)); + sentry_value_set_by_key( + child, "description", sentry_value_new_string(description)); + sentry_value_set_by_key(child, "start_timestamp", + sentry__value_new_string_owned( + sentry__msec_time_to_iso8601(sentry__msec_time()))); + + if (sentry_value_is_null(spans)) { + spans = sentry_value_new_list(); + sentry_value_set_by_key(scope->span, "spans", spans); + } + child_span_context = sentry__span_get_span_context(child); + sentry_value_append(spans, child); + } + sentry_value_set_by_key( + child_span_context, "index", sentry_value_new_int32((int)span_count)); + + return child_span_context; +} + +void +sentry_span_finish(sentry_value_t span_context) +{ + sentry_value_t sv_index = sentry_value_get_by_key(span_context, "index"); + if (sentry_value_is_null(sv_index)) { + sentry_value_decref(span_context); + return; + } + int index = sentry_value_as_int32(sv_index); + + SENTRY_WITH_SCOPE_MUT (scope) { + sentry_value_t spans = sentry_value_get_by_key(scope->span, "spans"); + // TODO: maybe validate that to_update.span_id == span.span_id + sentry_value_t to_update = sentry_value_get_by_index(spans, index); + sentry_value_set_by_key(to_update, "timestamp", + sentry__value_new_string_owned( + sentry__msec_time_to_iso8601(sentry__msec_time()))); + } + + sentry_value_decref(span_context); +} diff --git a/src/sentry_core.h b/src/sentry_core.h index bec05ef5a0..39c83492d5 100644 --- a/src/sentry_core.h +++ b/src/sentry_core.h @@ -5,6 +5,7 @@ #include "sentry_logger.h" #define SENTRY_BREADCRUMBS_MAX 100 +#define SENTRY_SPANS_MAX 1000 #if defined(__GNUC__) && (__GNUC__ >= 4) # define MUST_USE __attribute__((warn_unused_result)) @@ -35,7 +36,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 +58,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. @@ -99,12 +119,10 @@ 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 +// these for now are only needed outside of core 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_envelope.c b/src/sentry_envelope.c index 71cef6ed69..7ea05ad985 100644 --- a/src/sentry_envelope.c +++ b/src/sentry_envelope.c @@ -41,6 +41,9 @@ envelope_add_item(sentry_envelope_t *envelope) if (envelope->contents.items.item_count >= SENTRY_MAX_ENVELOPE_ITEMS) { return NULL; } + // TODO: Envelopes may have at most one event item or one transaction item, + // and not one of both. Some checking should be done here or in + // `sentry__envelope_add_[transaction|event]` to ensure this can't happen. sentry_envelope_item_t *rv = &envelope->contents.items @@ -197,7 +200,25 @@ sentry_envelope_get_event(const sentry_envelope_t *envelope) return sentry_value_new_null(); } for (size_t i = 0; i < envelope->contents.items.item_count; i++) { - if (!sentry_value_is_null(envelope->contents.items.items[i].event)) { + if (!sentry_value_is_null(envelope->contents.items.items[i].event) + && !sentry__event_is_transaction( + envelope->contents.items.items[i].event)) { + return envelope->contents.items.items[i].event; + } + } + return sentry_value_new_null(); +} + +sentry_value_t +sentry_envelope_get_transaction(const sentry_envelope_t *envelope) +{ + if (envelope->is_raw) { + return sentry_value_new_null(); + } + for (size_t i = 0; i < envelope->contents.items.item_count; i++) { + if (!sentry_value_is_null(envelope->contents.items.items[i].event) + && sentry__event_is_transaction( + envelope->contents.items.items[i].event)) { return envelope->contents.items.items[i].event; } } @@ -234,6 +255,45 @@ sentry__envelope_add_event(sentry_envelope_t *envelope, sentry_value_t event) return item; } +sentry_envelope_item_t * +sentry__envelope_add_transaction( + sentry_envelope_t *envelope, sentry_value_t transaction) +{ + sentry_envelope_item_t *item = envelope_add_item(envelope); + if (!item) { + return NULL; + } + + sentry_jsonwriter_t *jw = sentry__jsonwriter_new(NULL); + if (!jw) { + return NULL; + } + + sentry_value_t event_id = sentry__ensure_event_id(transaction, NULL); + + item->event = transaction; + sentry__jsonwriter_write_value(jw, transaction); + item->payload = sentry__jsonwriter_into_string(jw, &item->payload_len); + + sentry__envelope_item_set_header( + item, "type", sentry_value_new_string("transaction")); + sentry_value_t length = sentry_value_new_int32((int32_t)item->payload_len); + sentry__envelope_item_set_header(item, "length", length); + + sentry_value_incref(event_id); + sentry__envelope_set_header(envelope, "event_id", event_id); + +#ifdef SENTRY_UNITTEST + sentry_value_t now = sentry_value_new_string("2021-12-16T05:53:59.343Z"); +#else + sentry_value_t now = sentry__value_new_string_owned( + sentry__msec_time_to_iso8601(sentry__msec_time())); +#endif + sentry__envelope_set_header(envelope, "sent_at", now); + + return item; +} + sentry_envelope_item_t * sentry__envelope_add_session( sentry_envelope_t *envelope, const sentry_session_t *session) diff --git a/src/sentry_envelope.h b/src/sentry_envelope.h index ac5c69136f..b5a8f1ab09 100644 --- a/src/sentry_envelope.h +++ b/src/sentry_envelope.h @@ -36,6 +36,12 @@ sentry_uuid_t sentry__envelope_get_event_id(const sentry_envelope_t *envelope); sentry_envelope_item_t *sentry__envelope_add_event( sentry_envelope_t *envelope, sentry_value_t event); +/** + * Add a transaction to this envelope. + */ +sentry_envelope_item_t *sentry__envelope_add_transaction( + sentry_envelope_t *envelope, sentry_value_t transaction); + /** * Add a session to this envelope. */ diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 5052f023fc..99012fa9bd 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -230,9 +230,30 @@ sentry__symbolize_stacktrace(sentry_value_t stacktrace) void sentry__scope_set_span(sentry_value_t span) { - // TODO: implement this function and get rid of this line. - (void)span; - return; + SENTRY_WITH_SCOPE_MUT (scope) { + sentry_value_decref(scope->span); + scope->span = span; + } +} + +#ifdef SENTRY_UNITTEST +sentry_value_t +sentry__scope_get_span() +{ + SENTRY_WITH_SCOPE (scope) { + return scope->span; + } + return sentry_value_new_null(); +} +#endif + +void +sentry__scope_remove_span() +{ + SENTRY_WITH_SCOPE_MUT (scope) { + sentry_value_decref(scope->span); + scope->span = sentry_value_new_null(); + } } void @@ -268,7 +289,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_scope.h b/src/sentry_scope.h index 393cf349d4..029c771f3d 100644 --- a/src/sentry_scope.h +++ b/src/sentry_scope.h @@ -77,10 +77,23 @@ void sentry__scope_apply_to_event(const sentry_scope_t *scope, /** * Sets the span (actually transaction) on the scope. An internal way to pass - * around contextual information needed from a transaction into other events. + * around contextual information needed from a transaction into other events. If + * the scope already contains an unfinished transaction, that transaction will + * be discarded and will not be sent to sentry. + * + * This takes ownership of the span. */ void sentry__scope_set_span(sentry_value_t span); +/** + * Removes the current span (actually transaction) on the scope. If the + * transaction has not yet finished, this does not finish the transaction + * nor does it send it to sentry; The transaction will be discarded. + * + * Invoke this at your own discretion. + */ +void sentry__scope_remove_span(); + /** * These are convenience macros to automatically lock/unlock a scope inside a * code block. @@ -96,3 +109,8 @@ void sentry__scope_set_span(sentry_value_t span); sentry__scope_unlock(), Scope = NULL) #endif + +// this is only used in unit tests +#ifdef SENTRY_UNITTEST +sentry_value_t sentry__scope_get_span(); +#endif diff --git a/src/sentry_tracing.c b/src/sentry_tracing.c index 73e23c0d88..42a3f3af74 100644 --- a/src/sentry_tracing.c +++ b/src/sentry_tracing.c @@ -1,4 +1,5 @@ #include "sentry_sync.h" +#include "sentry_value.h" sentry_value_t sentry__span_get_trace_context(sentry_value_t span) @@ -11,23 +12,53 @@ sentry__span_get_trace_context(sentry_value_t span) sentry_value_t trace_context = sentry_value_new_object(); -#define PLACE_VALUE(Key, Source) \ +#define PLACE_CLONED_VALUE(Key, Source) \ do { \ sentry_value_t src = sentry_value_get_by_key(Source, Key); \ if (!sentry_value_is_null(src)) { \ - sentry_value_incref(src); \ - sentry_value_set_by_key(trace_context, Key, src); \ + sentry_value_set_by_key( \ + trace_context, Key, sentry__value_clone(src)); \ } \ } while (0) - PLACE_VALUE("trace_id", span); - PLACE_VALUE("span_id", span); - PLACE_VALUE("parent_span_id", span); - PLACE_VALUE("op", span); - PLACE_VALUE("description", span); - PLACE_VALUE("status", span); + PLACE_CLONED_VALUE("trace_id", span); + PLACE_CLONED_VALUE("span_id", span); + PLACE_CLONED_VALUE("parent_span_id", span); + PLACE_CLONED_VALUE("op", span); + PLACE_CLONED_VALUE("description", span); + PLACE_CLONED_VALUE("status", span); + // TODO: freeze this return trace_context; -#undef PLACE_VALUE +#undef PLACE_CLONED_VALUE +} + +sentry_value_t +sentry__span_get_span_context(sentry_value_t span) +{ + if (sentry_value_is_null(span) + || sentry_value_is_null(sentry_value_get_by_key(span, "trace_id")) + || sentry_value_is_null(sentry_value_get_by_key(span, "span_id"))) { + return sentry_value_new_null(); + } + + sentry_value_t span_context = sentry_value_new_object(); + +#define PLACE_CLONED_VALUE(Key, Source) \ + do { \ + sentry_value_t src = sentry_value_get_by_key(Source, Key); \ + if (!sentry_value_is_null(src)) { \ + sentry_value_set_by_key( \ + span_context, Key, sentry__value_clone(src)); \ + } \ + } while (0) + + PLACE_CLONED_VALUE("trace_id", span); + PLACE_CLONED_VALUE("span_id", span); + PLACE_CLONED_VALUE("status", span); + + return span_context; + +#undef PLACE_CLONED_VALUE } diff --git a/src/sentry_tracing.h b/src/sentry_tracing.h index d99afe828f..aecb7ab084 100644 --- a/src/sentry_tracing.h +++ b/src/sentry_tracing.h @@ -11,4 +11,5 @@ */ sentry_value_t sentry__span_get_trace_context(sentry_value_t span); +sentry_value_t sentry__span_get_span_context(sentry_value_t span); #endif diff --git a/src/sentry_value.c b/src/sentry_value.c index c71c68ecf9..98416cc287 100644 --- a/src/sentry_value.c +++ b/src/sentry_value.c @@ -1125,48 +1125,75 @@ 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_span(sentry_value_t parent, const char *operation) { - sentry_value_t transaction = sentry_value_new_object(); + sentry_value_t span = sentry_value_new_object(); - sentry_transaction_set_name(transaction, name); - sentry_transaction_set_operation(transaction, operation); + sentry_transaction_context_set_operation(span, operation); + sentry_value_set_by_key(span, "status", sentry_value_new_string("ok")); - return transaction; + // Span creation is currently aggressively pruned prior to this function so + // once we're in here we definitely know that the span and its parent + // transaction are sampled. + if (!sentry_value_is_null(parent)) { + sentry_value_set_by_key(span, "trace_id", + sentry_value_get_by_key_owned(parent, "trace_id")); + sentry_value_set_by_key(span, "parent_span_id", + sentry_value_get_by_key_owned(parent, "span_id")); + } + + return span; +} + +sentry_value_t +sentry_value_new_transaction_context(const char *name, const char *operation) +{ + sentry_value_t transaction_context + = sentry__value_new_span(sentry_value_new_null(), operation); + sentry_transaction_context_set_name(transaction_context, name); + + 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)); + + 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 - // the transaction. - if (sentry_value_is_null(sv_name) || sentry__string_eq(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, "transaction", sentry_value_new_string(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/src/sentry_value.h b/src/sentry_value.h index 2eb5004f13..cba7853643 100644 --- a/src/sentry_value.h +++ b/src/sentry_value.h @@ -61,6 +61,13 @@ sentry_value_t sentry__value_new_list_with_size(size_t size); */ sentry_value_t sentry__value_new_object_with_size(size_t size); +/** + * Constructs a new Span. + * + */ +sentry_value_t sentry__value_new_span( + sentry_value_t parent, const char *operation); + /** * This will parse the Value into a UUID, or return a `nil` UUID on error. * See also `sentry_uuid_from_string`. diff --git a/tests/unit/test_envelopes.c b/tests/unit/test_envelopes.c index a579afa3e5..2554be1c73 100644 --- a/tests/unit/test_envelopes.c +++ b/tests/unit/test_envelopes.c @@ -32,6 +32,38 @@ SENTRY_TEST(basic_http_request_preparation_for_event) sentry__dsn_decref(dsn); } +SENTRY_TEST(basic_http_request_preparation_for_transaction) +{ + sentry_dsn_t *dsn = sentry__dsn_new("https://foo@sentry.invalid/42"); + + sentry_uuid_t event_id + = sentry_uuid_from_string("c993afb6-b4ac-48a6-b61b-2558e601d65d"); + sentry_envelope_t *envelope = sentry__envelope_new(); + sentry_value_t transaction = sentry_value_new_object(); + sentry_value_set_by_key( + transaction, "event_id", sentry__value_new_uuid(&event_id)); + sentry_value_set_by_key( + transaction, "type", sentry_value_new_string("transaction")); + sentry__envelope_add_transaction(envelope, transaction); + + sentry_prepared_http_request_t *req + = sentry__prepare_http_request(envelope, dsn, NULL); + TEST_CHECK_STRING_EQUAL(req->method, "POST"); + TEST_CHECK_STRING_EQUAL( + req->url, "https://sentry.invalid:443/api/42/envelope/"); + TEST_CHECK_STRING_EQUAL(req->body, + "{\"event_id\":\"c993afb6-b4ac-48a6-b61b-2558e601d65d\",\"sent_at\":" + "\"2021-12-16T05:53:59.343Z\"}\n" + "{\"type\":\"transaction\",\"length\":72}\n" + "{\"event_id\":\"c993afb6-b4ac-48a6-b61b-2558e601d65d\",\"type\":" + "\"transaction\"}"); + + sentry__prepared_http_request_free(req); + sentry_envelope_free(envelope); + + sentry__dsn_decref(dsn); +} + SENTRY_TEST(basic_http_request_preparation_for_event_with_attachment) { sentry_dsn_t *dsn = sentry__dsn_new("https://foo@sentry.invalid/42"); diff --git a/tests/unit/test_sampling.c b/tests/unit/test_sampling.c index 8ab8d6ab4b..bcedcbbbb4 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 cce9bbc7fd..f11e25cc0c 100644 --- a/tests/unit/test_tracing.c +++ b/tests/unit/test_tracing.c @@ -1,7 +1,14 @@ +#include "sentry_scope.h" #include "sentry_testsupport.h" #include "sentry_tracing.h" #include "sentry_uuid.h" +#define IS_NULL(Src, Field) \ + sentry_value_is_null(sentry_value_get_by_key(Src, Field)) +#define CHECK_STRING_PROPERTY(Src, Field, Expected) \ + TEST_CHECK_STRING_EQUAL( \ + sentry_value_as_string(sentry_value_get_by_key(Src, Field)), Expected) + SENTRY_TEST(basic_tracing_context) { sentry_value_t span = sentry_value_new_object(); @@ -36,40 +43,473 @@ 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")); - TEST_CHECK_STRING_EQUAL(tx_name, ""); - const char *tx_op - = sentry_value_as_string(sentry_value_get_by_key(tx_cxt, "op")); - TEST_CHECK_STRING_EQUAL(tx_op, ""); + CHECK_STRING_PROPERTY(tx_cxt, "transaction", ""); + CHECK_STRING_PROPERTY(tx_cxt, "op", ""); + TEST_CHECK(!IS_NULL(tx_cxt, "trace_id")); + TEST_CHECK(!IS_NULL(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, ""); + CHECK_STRING_PROPERTY(tx_cxt, "transaction", ""); + CHECK_STRING_PROPERTY(tx_cxt, "op", ""); + TEST_CHECK(!IS_NULL(tx_cxt, "trace_id")); + TEST_CHECK(!IS_NULL(tx_cxt, "span_id")); sentry_value_decref(tx_cxt); - tx_cxt = sentry_value_new_transaction("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"); + tx_cxt = sentry_value_new_transaction_context("honk.beep", "beepbeep"); + CHECK_STRING_PROPERTY(tx_cxt, "transaction", "honk.beep"); + CHECK_STRING_PROPERTY(tx_cxt, "op", "beepbeep"); + TEST_CHECK(!IS_NULL(tx_cxt, "trace_id")); + TEST_CHECK(!IS_NULL(tx_cxt, "span_id")); - sentry_transaction_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_context_set_name(tx_cxt, ""); + CHECK_STRING_PROPERTY(tx_cxt, "transaction", ""); - sentry_transaction_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_operation(tx_cxt, ""); + CHECK_STRING_PROPERTY(tx_cxt, "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 +check_backfilled_name(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)); + CHECK_STRING_PROPERTY( + transaction, "transaction", ""); + + sentry_envelope_free(envelope); +} + +SENTRY_TEST(transaction_name_backfill_on_finish) +{ + 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(check_backfilled_name); + sentry_transport_set_state(transport, &called); + sentry_options_set_transport(options, transport); + + sentry_options_set_traces_sample_rate(options, 1.0); + sentry_init(options); + + sentry_value_t transaction + = sentry_value_new_transaction_context(NULL, NULL); + sentry_transaction_start(transaction); + sentry_uuid_t event_id = sentry_transaction_finish(); + TEST_CHECK(!sentry_uuid_is_nil(&event_id)); + + transaction = sentry_value_new_transaction_context("", ""); + sentry_transaction_start(transaction); + event_id = sentry_transaction_finish(); + TEST_CHECK(!sentry_uuid_is_nil(&event_id)); + + sentry_close(); + TEST_CHECK_INT_EQUAL(called, 2); +} + +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."); + sentry_transaction_start(transaction); + sentry_uuid_t event_id = sentry_transaction_finish(); + // 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"); + sentry_transaction_start(transaction); + event_id = sentry_transaction_finish(); + 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."); + sentry_transaction_start(transaction); + event_id = sentry_transaction_finish(); + // 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"); + sentry_transaction_start(transaction); + sentry_uuid_t event_id = sentry_transaction_finish(); + if (!sentry_uuid_is_nil(&event_id)) { + sent_transactions += 1; + } + } + + sentry_close(); + + // exact value is nondeterministic because of rng + 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"); + sentry_transaction_start(transaction); + sentry_uuid_t event_id = sentry_transaction_finish(); + TEST_CHECK(!sentry_uuid_is_nil(&event_id)); + + sentry_close(); + + TEST_CHECK_INT_EQUAL(called_transport, 1); + TEST_CHECK_INT_EQUAL(called_beforesend, 0); +} + +static void +before_transport(sentry_envelope_t *envelope, void *data) +{ + uint64_t *called = data; + *called += 1; + + sentry_envelope_free(envelope); +} + +SENTRY_TEST(multiple_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(before_transport); + sentry_transport_set_state(transport, &called_transport); + sentry_options_set_transport(options, transport); + + sentry_options_set_traces_sample_rate(options, 1.0); + sentry_init(options); + + sentry_value_t tx_cxt = sentry_value_new_transaction_context("wow!", NULL); + sentry_transaction_start(tx_cxt); + + sentry_value_t scope_tx = sentry__scope_get_span(); + CHECK_STRING_PROPERTY(scope_tx, "transaction", "wow!"); + + sentry_uuid_t event_id = sentry_transaction_finish(); + scope_tx = sentry__scope_get_span(); + TEST_CHECK(sentry_value_is_null(scope_tx)); + TEST_CHECK(!sentry_uuid_is_nil(&event_id)); + + tx_cxt = sentry_value_new_transaction_context("whoa!", NULL); + sentry_transaction_start(tx_cxt); + tx_cxt = sentry_value_new_transaction_context("wowee!", NULL); + sentry_transaction_start(tx_cxt); + scope_tx = sentry__scope_get_span(); + CHECK_STRING_PROPERTY(scope_tx, "transaction", "wowee!"); + event_id = sentry_transaction_finish(); + TEST_CHECK(!sentry_uuid_is_nil(&event_id)); + + sentry_close(); + + TEST_CHECK_INT_EQUAL(called_transport, 2); +} + +SENTRY_TEST(basic_spans) +{ + sentry_options_t *options = sentry_options_new(); + sentry_options_set_traces_sample_rate(options, 1.0); + sentry_options_set_max_spans(options, 3); + sentry_init(options); + + // Starting a child with no active transaction should fail + sentry_value_t parentless_child + = sentry_span_start_child(sentry_value_new_null(), NULL, NULL); + TEST_CHECK(sentry_value_is_null(parentless_child)); + + sentry_value_t tx_cxt = sentry_value_new_transaction_context("wow!", NULL); + sentry_transaction_start(tx_cxt); + + sentry_value_t child + = sentry_span_start_child(sentry_value_new_null(), "honk", "goose"); + TEST_CHECK(!sentry_value_is_null(child)); + + // Peek into the transaction's span list and make sure everything is + // good + sentry_value_t scope_tx = sentry__scope_get_span(); + const char *trace_id + = sentry_value_as_string(sentry_value_get_by_key(scope_tx, "trace_id")); + const char *parent_span_id + = sentry_value_as_string(sentry_value_get_by_key(scope_tx, "span_id")); + TEST_CHECK(!IS_NULL(scope_tx, "spans")); + TEST_CHECK_INT_EQUAL( + sentry_value_get_length(sentry_value_get_by_key(scope_tx, "spans")), 1); + + // Make sure the span inherited everything correctly + sentry_value_t stored_child = sentry_value_get_by_index( + sentry_value_get_by_key(scope_tx, "spans"), 0); + CHECK_STRING_PROPERTY(stored_child, "trace_id", trace_id); + CHECK_STRING_PROPERTY(stored_child, "parent_span_id", parent_span_id); + CHECK_STRING_PROPERTY(stored_child, "op", "honk"); + CHECK_STRING_PROPERTY(stored_child, "description", "goose"); + // Not finished yet + TEST_CHECK(IS_NULL(stored_child, "timestamp")); + // Span contexts carry indices in this SDK to make it easier to find and + // update them, make sure they don't leak into the transaction + TEST_CHECK(IS_NULL(stored_child, "index")); + + sentry_span_finish(child); + stored_child = sentry_value_get_by_index( + sentry_value_get_by_key(scope_tx, "spans"), 0); + // Should be finished + TEST_CHECK(!IS_NULL(stored_child, "timestamp")); + + sentry_close(); +} + +SENTRY_TEST(child_spans) +{ + sentry_options_t *options = sentry_options_new(); + sentry_options_set_traces_sample_rate(options, 1.0); + sentry_options_set_max_spans(options, 3); + sentry_init(options); + + // Starting a child with no active transaction should fail + sentry_value_t parentless_child + = sentry_span_start_child(sentry_value_new_null(), NULL, NULL); + TEST_CHECK(sentry_value_is_null(parentless_child)); + + // Finishing a nonexistent span doesn't explode anything + sentry_value_t fake_span + = sentry__value_new_span(sentry_value_new_null(), NULL); + sentry_span_finish(fake_span); + + sentry_value_t tx_cxt = sentry_value_new_transaction_context("wow!", NULL); + sentry_transaction_start(tx_cxt); + + sentry_value_t child + = sentry_span_start_child(sentry_value_new_null(), "honk", "goose"); + TEST_CHECK(!sentry_value_is_null(child)); + + // Peek into the transaction's span list and make sure everything is + // good + sentry_value_t scope_tx = sentry__scope_get_span(); + const char *trace_id + = sentry_value_as_string(sentry_value_get_by_key(scope_tx, "trace_id")); + TEST_CHECK_INT_EQUAL( + sentry_value_get_length(sentry_value_get_by_key(scope_tx, "spans")), 1); + + const char *parent_span_id + = sentry_value_as_string(sentry_value_get_by_key(child, "span_id")); + + sentry_value_t grandchild = sentry_span_start_child(child, "beep", "car"); + sentry_span_finish(grandchild); + + // Make sure everything on the transaction looks good, check grandchild + TEST_CHECK_INT_EQUAL( + sentry_value_get_length(sentry_value_get_by_key(scope_tx, "spans")), 2); + + sentry_value_t stored_grandchild = sentry_value_get_by_index( + sentry_value_get_by_key(scope_tx, "spans"), 1); + CHECK_STRING_PROPERTY(stored_grandchild, "trace_id", trace_id); + CHECK_STRING_PROPERTY(stored_grandchild, "parent_span_id", parent_span_id); + CHECK_STRING_PROPERTY(stored_grandchild, "op", "beep"); + CHECK_STRING_PROPERTY(stored_grandchild, "description", "car"); + // No span context-exclusive values leaking into transaction's spans + TEST_CHECK(IS_NULL(stored_grandchild, "index")); + // Should be finished + TEST_CHECK(!IS_NULL(stored_grandchild, "timestamp")); + + sentry_span_finish(child); + + sentry_close(); +} + +SENTRY_TEST(overflow_spans) +{ + sentry_options_t *options = sentry_options_new(); + sentry_options_set_traces_sample_rate(options, 1.0); + sentry_options_set_max_spans(options, 1); + sentry_init(options); + + sentry_value_t tx_cxt = sentry_value_new_transaction_context("wow!", NULL); + sentry_transaction_start(tx_cxt); + + sentry_value_t child + = sentry_span_start_child(sentry_value_new_null(), "honk", "goose"); + const char *child_span_id + = sentry_value_as_string(sentry_value_get_by_key(child, "span_id")); + + sentry_value_t scope_tx = sentry__scope_get_span(); + TEST_CHECK_INT_EQUAL( + sentry_value_get_length(sentry_value_get_by_key(scope_tx, "spans")), 1); + + sentry_value_t overflow_child + = sentry_span_start_child(child, "beep", "car"); + TEST_CHECK(sentry_value_is_null(overflow_child)); + + TEST_CHECK_INT_EQUAL( + sentry_value_get_length(sentry_value_get_by_key(scope_tx, "spans")), 1); + + sentry_value_t stored_child = sentry_value_get_by_index( + sentry_value_get_by_key(scope_tx, "spans"), 0); + CHECK_STRING_PROPERTY(stored_child, "span_id", child_span_id); + + sentry_value_decref(child); + sentry_value_decref(overflow_child); + + sentry_close(); +} + +static void +check_spans(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)); + + size_t span_count = sentry_value_get_length( + sentry_value_get_by_key(transaction, "spans")); + TEST_CHECK_INT_EQUAL(span_count, 1); + + sentry_envelope_free(envelope); +} + +SENTRY_TEST(drop_unfinished_spans) +{ + 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(check_spans); + 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_max_spans(options, 2); + sentry_init(options); + + sentry_value_t tx_cxt = sentry_value_new_transaction_context("wow!", NULL); + sentry_transaction_start(tx_cxt); + + sentry_value_t child + = sentry_span_start_child(sentry_value_new_null(), "honk", "goose"); + TEST_CHECK(!sentry_value_is_null(child)); + + sentry_value_t grandchild = sentry_span_start_child(child, "beep", "car"); + TEST_CHECK(!sentry_value_is_null(grandchild)); + sentry_span_finish(grandchild); + + sentry_value_t scope_tx = sentry__scope_get_span(); + TEST_CHECK_INT_EQUAL( + sentry_value_get_length(sentry_value_get_by_key(scope_tx, "spans")), 2); + + sentry_uuid_t event_id = sentry_transaction_finish(); + TEST_CHECK(!sentry_uuid_is_nil(&event_id)); + + sentry_value_decref(child); + + sentry_close(); + + TEST_CHECK_INT_EQUAL(called_transport, 1); +} + +#undef IS_NULL +#undef CHECK_STRING_PROPERTY diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 781361644a..de9b83afa4 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -1,15 +1,20 @@ 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) +XX(basic_http_request_preparation_for_transaction) +XX(basic_spans) XX(basic_tracing_context) XX(basic_transaction) XX(buildid_fallback) +XX(child_spans) XX(concurrent_init) XX(count_sampled_events) XX(custom_logger) +XX(drop_unfinished_spans) XX(dsn_parsing_complete) XX(dsn_parsing_invalid) XX(dsn_store_url_with_path) @@ -27,7 +32,9 @@ XX(module_finder) XX(mpack_newlines) XX(mpack_removed_tags) XX(multiple_inits) +XX(multiple_transactions) XX(os) +XX(overflow_spans) XX(page_allocator) XX(path_basics) XX(path_current_exe) @@ -40,13 +47,15 @@ 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(transaction_name_backfill_on_finish) +XX(transactions_skip_before_send) +XX(transport_sampling_transactions) XX(uninitialized) XX(unwinder) XX(url_parsing_complete)