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.
// Transaction can be started by providing, at minimum, the name and the operation
var transaction = SentrySdk.StartTransaction(
"test-transaction-name",
"test-transaction-operation"
);
// Transactions can have child spans (and those spans can have child spans as well)
var span = transaction.StartChild("test-child-operation");
// ...
// (Perform the operation represented by the span/transaction)
// ...
span.Finish(); // Mark the span as finished
transaction.Finish(); // Mark the transaction as finished and send it to Sentry
// Transaction can be started by providing, at minimum, the name and the operation
var transaction = SentrySdk.StartTransaction(
"test-transaction-name",
"test-transaction-operation"
);
// Transactions can have child spans (and those spans can have child spans as well)
var span = transaction.StartChild("test-child-operation");
// ...
// (Perform the operation represented by the span/transaction)
// ...
span.Finish(); // Mark the span as finished
transaction.Finish(); // Mark the transaction as finished and send it to Sentry
For example, if you want to create a transaction for a user interaction in your application:
// Let's say this method is invoked when a user clicks on the checkout button of your shop
public async Task PerformCheckoutAsync()
{
// This will create a new Transaction for you
var transaction = SentrySdk.StartTransaction(
"checkout", // name
"perform-checkout" // operation
);
// Set transaction on scope to associate with errors and get included span instrumentation
// If there's currently an unfinished transaction, it may be dropped
SentrySdk.ConfigureScope(scope => scope.Transaction = transaction);
// Validate the cart
var validationSpan = transaction.StartChild(
"validation", // operation
"validating shopping cart" // description
);
await ValidateShoppingCartAsync();
validationSpan.Finish();
// Process the order
var processSpan = transaction.StartChild(
"process", // operation
"processing shopping cart" // description
)
await ProcessShoppingCartAsync();
processSpan.Finish();
transaction.Finish();
}
// Let's say this method is invoked when a user clicks on the checkout button of your shop
public async Task PerformCheckoutAsync()
{
// This will create a new Transaction for you
var transaction = SentrySdk.StartTransaction(
"checkout", // name
"perform-checkout" // operation
);
// Set transaction on scope to associate with errors and get included span instrumentation
// If there's currently an unfinished transaction, it may be dropped
SentrySdk.ConfigureScope(scope => scope.Transaction = transaction);
// Validate the cart
var validationSpan = transaction.StartChild(
"validation", // operation
"validating shopping cart" // description
);
await ValidateShoppingCartAsync();
validationSpan.Finish();
// Process the order
var processSpan = transaction.StartChild(
"process", // operation
"processing shopping cart" // description
)
await ProcessShoppingCartAsync();
processSpan.Finish();
transaction.Finish();
}
This example will send a transaction checkout to Sentry. The transaction will contain a validation span that measures how long ValidateShoppingCartAsync took and a process span that measures ProcessShoppingCartAsync. Finally, the call to transaction.Finish() will finish the transaction and send it to Sentry.
In cases where you want to attach Spans to an already ongoing Transaction you can use SentrySdk.GetSpan(). If there is a running Transaction or Span currently on the scope, this method will return a SentryTransaction or Span; otherwise, it returns null.
var span = SentrySdk.GetSpan();
if (span == null)
{
span = SentrySdk.StartTransaction("task", "op");
}
else
{
span = span.StartChild("subtask");
}
var span = SentrySdk.GetSpan();
if (span == null)
{
span = SentrySdk.StartTransaction("task", "op");
}
else
{
span = span.StartChild("subtask");
}
Requires Sentry SDK version 6.8.0 or higher.
StartTransaction measures work as it happens, using a live stopwatch. Sometimes, though, the work you want to report has already finished somewhere else — for example, spans measured on another machine or in another process and relayed to your application through a proxy. For those cases, use SentrySdk.RecordTransaction to record a transaction whose timing you supply explicitly:
SentrySdk.RecordTransaction(
"checkout", // name
"http.server", // operation
originalStartTimestamp, // when the work started (DateTimeOffset)
originalDuration, // how long it ran (TimeSpan, must not be negative)
traceId: originTraceId, // optional: preserve the trace id from the originating system
spanId: originRootSpanId, // optional: preserve the root span id
parentSpanId: originParentId, // optional: continue a trace from another service
configure: transaction =>
{
transaction.Release = "1.2.3";
transaction.SetTag("origin", "proxy");
// Child spans also take their timing up front and can be nested arbitrarily
transaction.RecordSpan("db.query", queryStart, queryDuration, configure: span =>
{
span.Description = "SELECT * FROM orders";
span.RecordSpan("db.connection", connectStart, connectDuration);
});
});
SentrySdk.RecordTransaction(
"checkout", // name
"http.server", // operation
originalStartTimestamp, // when the work started (DateTimeOffset)
originalDuration, // how long it ran (TimeSpan, must not be negative)
traceId: originTraceId, // optional: preserve the trace id from the originating system
spanId: originRootSpanId, // optional: preserve the root span id
parentSpanId: originParentId, // optional: continue a trace from another service
configure: transaction =>
{
transaction.Release = "1.2.3";
transaction.SetTag("origin", "proxy");
// Child spans also take their timing up front and can be nested arbitrarily
transaction.RecordSpan("db.query", queryStart, queryDuration, configure: span =>
{
span.Description = "SELECT * FROM orders";
span.RecordSpan("db.connection", connectStart, connectDuration);
});
});
The transaction is captured and sent to Sentry once the configure callback returns — there is no Finish() to call.
Because a recorded transaction represents work that happened elsewhere, it behaves differently from one started with StartTransaction:
- Timing is explicit. Each transaction and span takes a start timestamp and a duration up front, rather than being measured live.
- Sampling is skipped. Recorded transactions are always sent, regardless of
TracesSampleRateorTracesSampler. YourBeforeSendTransactioncallback still runs, so you can filter or scrub there. - The current scope doesn't apply. The transaction is captured against a clean scope, so breadcrumbs, user data, and tags from the current process won't leak onto work that happened elsewhere. To attach scope data that belongs with the original trace, use
transaction.ConfigureScopeinside the callback, or theReleaseandEnvironmentsetters.
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").