Custom Instrumentation
Learn how to capture performance data on any action in your app.
To capture transactions and spans customized to your organization's needs, you must first set up tracing.
To instrument certain regions of your code, you can create transactions to capture them.
The following example creates a transaction that contains an expensive operation (for example, processOrderBatch), and sends the result to Sentry:
import io.sentry.ITransaction;
import io.sentry.Sentry;
import io.sentry.SpanStatus;
// A good name for the transaction is key, to help identify what this is about
ITransaction transaction = Sentry.startTransaction("processOrderBatch()", "task");
try {
processOrderBatch();
} catch (Exception e) {
transaction.setThrowable(e);
transaction.setStatus(SpanStatus.INTERNAL_ERROR);
throw e;
} finally {
transaction.finish();
}
import io.sentry.ITransaction;
import io.sentry.Sentry;
import io.sentry.SpanStatus;
// A good name for the transaction is key, to help identify what this is about
ITransaction transaction = Sentry.startTransaction("processOrderBatch()", "task");
try {
processOrderBatch();
} catch (Exception e) {
transaction.setThrowable(e);
transaction.setStatus(SpanStatus.INTERNAL_ERROR);
throw e;
} finally {
transaction.finish();
}
import io.sentry.Sentry
import io.sentry.SpanStatus
// A good name for the transaction is key, to help identify what this is about
val transaction = Sentry.startTransaction("processOrderBatch()", "task")
try {
processOrderBatch()
} catch (e: Exception) {
transaction.throwable = e
transaction.status = SpanStatus.INTERNAL_ERROR
throw e
} finally {
transaction.finish();
}
By default the transaction is not bound to the scope. Our auto instrumentation, for example those instrumenting database requests or HTTP calls, rely on a transaction to be present in order to attach new child spans to it. Please take a look at the Create Transaction Bound to The Current Scope section further down.
By default, transactions are not bound to the scope. Transaction has to be passed manually as a method parameter to enable attaching nested spans. When creating nested span, you can choose the value of operation and description.
import java.io.FileNotFoundException;
import io.sentry.ISpan;
import io.sentry.ITransaction;
import io.sentry.Sentry;
import io.sentry.SpanStatus;
// A good name for the transaction is key, to help identify what this is about
ITransaction transaction = Sentry.startTransaction("processOrderBatch()", "task");
try {
processOrderBatch(transaction);
} catch (Exception e) {
transaction.setThrowable(e);
transaction.setStatus(SpanStatus.INTERNAL_ERROR);
throw e;
} finally {
transaction.finish();
}
void processOrderBatch(ISpan parentSpan) {
// span operation: task, span description: operation
ISpan innerSpan = parentSpan.startChild("task", "operation");
try {
// omitted code
} catch (FileNotFoundException e) {
innerSpan.setThrowable(e);
innerSpan.setStatus(SpanStatus.NOT_FOUND);
throw e;
} finally {
innerSpan.finish();
}
}
import java.io.FileNotFoundException;
import io.sentry.ISpan;
import io.sentry.ITransaction;
import io.sentry.Sentry;
import io.sentry.SpanStatus;
// A good name for the transaction is key, to help identify what this is about
ITransaction transaction = Sentry.startTransaction("processOrderBatch()", "task");
try {
processOrderBatch(transaction);
} catch (Exception e) {
transaction.setThrowable(e);
transaction.setStatus(SpanStatus.INTERNAL_ERROR);
throw e;
} finally {
transaction.finish();
}
void processOrderBatch(ISpan parentSpan) {
// span operation: task, span description: operation
ISpan innerSpan = parentSpan.startChild("task", "operation");
try {
// omitted code
} catch (FileNotFoundException e) {
innerSpan.setThrowable(e);
innerSpan.setStatus(SpanStatus.NOT_FOUND);
throw e;
} finally {
innerSpan.finish();
}
}
import java.io.FileNotFoundException
import io.sentry.Sentry
import io.sentry.SpanStatus
val transaction = Sentry.startTransaction("processOrderBatch()", "task")
try {
processOrderBatch(transaction)
} catch (e: Exception) {
transaction.throwable = e
transaction.status = SpanStatus.INTERNAL_ERROR
throw e
} finally {
transaction.finish()
}
fun processOrderBatch(parentSpan: ISpan) {
// span operation: task, span description: operation
val innerSpan = parentSpan.startChild("task", "operation")
try {
// omitted code
} catch (e: FileNotFoundException) {
innerSpan.throwable = e
innerSpan.status = SpanStatus.NOT_FOUND
throw e
} finally {
innerSpan.finish()
}
}
Keep in mind that each individual span also needs to be manually finished; otherwise, spans will not show up in the transaction.
Spans are sent together with their parent transaction when the transaction is finished. Make sure to call finish() on transaction once all the child spans have finished.
Our SDK can bind a transaction to the scope making it accessible to every method running within this scope by calling Sentry#startTransaction method with bindToScope parameter to true.
Our auto instrumentation, for example those instrumenting database requests or HTTP calls, require a transaction to be bound to the current scope in order to attach a new child span.
bindToScope additionally ensures that your new transaction replaces any one that may be already started. This is useful if you want custom instrumentation to co-exist with auto-instrumented transactions.
In cases where you want to attach Spans to an already ongoing Transaction you can use Sentry#getSpan. This method will return a SentryTransaction in case there is a running Transaction or a Span in case there is already a running Span, otherwise it returns null.
import java.io.FileNotFoundException;
import io.sentry.ISpan;
import io.sentry.ITransaction;
import io.sentry.Sentry;
import io.sentry.SpanStatus;
import io.sentry.TransactionOptions;
// A good name for the transaction is key, to help identify what this is about
TransactionOptions txOptions = new TransactionOptions();
txOptions.setBindToScope(true);
ITransaction transaction = Sentry.startTransaction("processOrderBatch()", "task", txOptions);
try {
processOrderBatch();
} catch (Exception e) {
transaction.setThrowable(e);
transaction.setStatus(SpanStatus.INTERNAL_ERROR);
throw e;
} finally {
transaction.finish();
}
void processOrderBatch() {
ISpan parentSpan = Sentry.getSpan(); // returns the "processOrderBatch()" transaction
ISpan innerSpan = null;
if (parentSpan != null) {
innerSpan = parentSpan.startChild("task", "operation");
}
try {
// omitted code
} catch (FileNotFoundException e) {
if (innerSpan != null) {
innerSpan.setThrowable(e);
innerSpan.setStatus(SpanStatus.NOT_FOUND);
throw e;
} finally {
if (innerSpan != null) {
innerSpan.finish();
}
}
}
import java.io.FileNotFoundException;
import io.sentry.ISpan;
import io.sentry.ITransaction;
import io.sentry.Sentry;
import io.sentry.SpanStatus;
import io.sentry.TransactionOptions;
// A good name for the transaction is key, to help identify what this is about
TransactionOptions txOptions = new TransactionOptions();
txOptions.setBindToScope(true);
ITransaction transaction = Sentry.startTransaction("processOrderBatch()", "task", txOptions);
try {
processOrderBatch();
} catch (Exception e) {
transaction.setThrowable(e);
transaction.setStatus(SpanStatus.INTERNAL_ERROR);
throw e;
} finally {
transaction.finish();
}
void processOrderBatch() {
ISpan parentSpan = Sentry.getSpan(); // returns the "processOrderBatch()" transaction
ISpan innerSpan = null;
if (parentSpan != null) {
innerSpan = parentSpan.startChild("task", "operation");
}
try {
// omitted code
} catch (FileNotFoundException e) {
if (innerSpan != null) {
innerSpan.setThrowable(e);
innerSpan.setStatus(SpanStatus.NOT_FOUND);
throw e;
} finally {
if (innerSpan != null) {
innerSpan.finish();
}
}
}
import java.io.FileNotFoundException
import io.sentry.Sentry
import io.sentry.SpanStatus
import io.sentry.TransactionOptions
val transaction = Sentry.startTransaction("processOrderBatch()", "task", TransactionOptions().apply { isBindToScope = true })
try {
processOrderBatch()
} catch (e: Exception) {
transaction.throwable = e
transaction.status = SpanStatus.INTERNAL_ERROR
throw e
} finally {
transaction.finish()
}
fun processOrderBatch() {
val parentSpan = Sentry.getSpan() // returns the "processOrderBatch()" transaction
val innerSpan = parentSpan?.startChild("task", "operation")
try {
// omitted code
} catch (e: FileNotFoundException) {
innerSpan?.throwable = e
innerSpan?.status = SpanStatus.NOT_FOUND
throw e
} finally {
innerSpan?.finish()
}
}
Sentry errors can be linked with transactions and spans.
Errors reported to Sentry while transaction or span bound to the scope is running are linked automatically:
import io.sentry.Sentry;
import io.sentry.ISpan;
import io.sentry.ITransaction;
import io.sentry.TransactionOptions;
TransactionOptions txOptions = new TransactionOptions();
txOptions.setBindToScope(true);
ITransaction span = Sentry.startTransaction(item.getTransactionName(), "task", txOptions);
try {
processItem();
} catch (Exception e) {
Sentry.captureException(e);
} finally {
span.finish();
}
import io.sentry.Sentry;
import io.sentry.ISpan;
import io.sentry.ITransaction;
import io.sentry.TransactionOptions;
TransactionOptions txOptions = new TransactionOptions();
txOptions.setBindToScope(true);
ITransaction span = Sentry.startTransaction(item.getTransactionName(), "task", txOptions);
try {
processItem();
} catch (Exception e) {
Sentry.captureException(e);
} finally {
span.finish();
}
import io.sentry.Sentry
import io.sentry.TransactionOptions;
val span = Sentry.startTransaction(item.getTransactionName(), "task", TransactionOptions().apply { isBindToScope = true })
try {
processItem()
} catch (e: Exception) {
Sentry.captureException(e)
} finally {
span.finish()
}
Exceptions may be thrown within spans that can finish before exception gets reported to Sentry. To attach span information to this exception, you must link it by calling setThrowable method:
import io.sentry.Sentry;
import io.sentry.ISpan;
ISpan span = Sentry.getSpan();
if (span == null) {
span = Sentry.startTransaction(item.getTransactionName(), "task");
}
try {
processItem();
} catch (Exception e) {
span.setThrowable(e);
throw e;
} finally {
span.finish();
}
import io.sentry.Sentry;
import io.sentry.ISpan;
ISpan span = Sentry.getSpan();
if (span == null) {
span = Sentry.startTransaction(item.getTransactionName(), "task");
}
try {
processItem();
} catch (Exception e) {
span.setThrowable(e);
throw e;
} finally {
span.finish();
}
import io.sentry.Sentry
val span = Sentry.getSpan()?: Sentry.startTransaction(item.getTransactionName(), "task")
try {
processItem()
} catch (e: Exception) {
span.throwable = e
throw e
} finally {
span.finish()
}
You can add data attributes to your transactions. This data is visible in the trace explorer in Sentry. Data attributes can be of type String, Number or Boolean, as well as (non-mixed) arrays of these types:
ITransaction transaction = Sentry.startTransaction("processOrderBatch()", "task");
transaction.setData("my-data-attribute-1", "value1");
transaction.setData("my-data-attribute-2", 42);
transaction.setData("my-data-attribute-3", true);
transaction.setData("my-data-attribute-4", Arrays.asList("value1", "value2", "value3"));
transaction.setData("my-data-attribute-5", Arrays.asList(42, 43, 44));
transaction.setData("my-data-attribute-6", Arrays.asList(true, false, true));
ITransaction transaction = Sentry.startTransaction("processOrderBatch()", "task");
transaction.setData("my-data-attribute-1", "value1");
transaction.setData("my-data-attribute-2", 42);
transaction.setData("my-data-attribute-3", true);
transaction.setData("my-data-attribute-4", Arrays.asList("value1", "value2", "value3"));
transaction.setData("my-data-attribute-5", Arrays.asList(42, 43, 44));
transaction.setData("my-data-attribute-6", Arrays.asList(true, false, true));
ITransaction transaction = Sentry.startTransaction("processOrderBatch()", "task");
transaction.setData("my-data-attribute-1", "value1")
transaction.setData("my-data-attribute-2", 42)
transaction.setData("my-data-attribute-3", true)
transaction.setData("my-data-attribute-4", listOf("value1", "value2", "value3"))
transaction.setData("my-data-attribute-5", listOf(42, 43, 44))
transaction.setData("my-data-attribute-6", listOf(true, false, true))
You can add data attributes to your spans. This data is visible in the trace explorer in Sentry. Data attributes can be of type String, Number or Boolean, as well as (non-mixed) arrays of these types:
ISpan span = parent.startChild("task", "operation");
span.setData("my-data-attribute-1", "value1");
span.setData("my-data-attribute-2", 42);
span.setData("my-data-attribute-3", true);
span.setData("my-data-attribute-4", Arrays.asList("value1", "value2", "value3"));
span.setData("my-data-attribute-5", Arrays.asList(42, 43, 44));
span.setData("my-data-attribute-6", Arrays.asList(true, false, true));
ISpan span = parent.startChild("task", "operation");
span.setData("my-data-attribute-1", "value1");
span.setData("my-data-attribute-2", 42);
span.setData("my-data-attribute-3", true);
span.setData("my-data-attribute-4", Arrays.asList("value1", "value2", "value3"));
span.setData("my-data-attribute-5", Arrays.asList(42, 43, 44));
span.setData("my-data-attribute-6", Arrays.asList(true, false, true));
ISpan span = parent.startChild("task", "operation");
span.setData("my-data-attribute-1", "value1")
span.setData("my-data-attribute-2", 42)
span.setData("my-data-attribute-3", true)
span.setData("my-data-attribute-4", listOf("value1", "value2", "value3"))
span.setData("my-data-attribute-5", listOf(42, 43, 44))
span.setData("my-data-attribute-6", listOf(true, false, true))
To add an attribute to all spans, use the beforeSendTransaction callback:
Sentry.init(options -> {
options.setBeforeSendTransaction((transaction, hint) -> {
// set the attribute on the root span
if (transaction.getContexts().getTrace() == null) {
SpanContext spanContext = new SpanContext("op");
transaction.getContexts().setTrace(spanContext);
}
transaction.getContexts().getTrace().setData("myAttribute", "myValue");
// and on all child spans
transaction.getSpans().forEach(span -> {
if (span.getData() == null) {
span.setData(new HashMap<>());
}
span.getData().put("myAttribute", "myValue");
});
return transaction;
});
});
Sentry.init(options -> {
options.setBeforeSendTransaction((transaction, hint) -> {
// set the attribute on the root span
if (transaction.getContexts().getTrace() == null) {
SpanContext spanContext = new SpanContext("op");
transaction.getContexts().setTrace(spanContext);
}
transaction.getContexts().getTrace().setData("myAttribute", "myValue");
// and on all child spans
transaction.getSpans().forEach(span -> {
if (span.getData() == null) {
span.setData(new HashMap<>());
}
span.getData().put("myAttribute", "myValue");
});
return transaction;
});
});
Sentry.init { options ->
options.setBeforeSendTransaction { transaction, hint ->
// set the attribute on the root span
if (transaction.contexts.trace == null) {
transaction.contexts.setTrace(SpanContext("op"))
}
transaction.contexts.trace?.setData("myAttribute", "myValue")
// and on all child spans
transaction.spans.forEach { span ->
if (span.data == null) {
span.data = HashMap()
}
span.data?.set("myAttribute", "myValue")
}
transaction
}
}
Our documentation is open source and available on GitHub. Your contributions are welcome, whether fixing a typo (drat!) or suggesting an update ("yeah, this would be better").