---
title: "Instrumentation"
description: "Learn how to add spans that measure operations in your Godot Engine game."
url: https://docs.sentry.io/platforms/godot/tracing/instrumentation/
---

# Instrumentation for Godot Engine

Before adding spans, [enable tracing](https://docs.sentry.io/platforms/godot/tracing.md) by setting a trace sample rate.

The SDK automatically instruments HTTP and HTTPS requests made with `SentryHTTPRequest`. Add manual instrumentation around other operations whose timing helps you understand the player experience, such as loading a level, generating a world, or writing a save file.

## [Trace HTTP Requests Automatically](https://docs.sentry.io/platforms/godot/tracing/instrumentation.md#trace-http-requests-automatically)

Use `SentryHTTPRequest` in place of Godot's `HTTPRequest` to measure an outgoing request, propagate its trace context, and record an HTTP breadcrumb without managing the span yourself.

The node wraps an internal `HTTPRequest` and exposes the same request methods, configuration properties, and `request_completed` signal. It inherits `Node`, not `HTTPRequest`, so replace the node rather than casting or extending an existing `HTTPRequest`.

This example fetches leaderboard data and waits for the instrumented request to finish:

```GDScript
extends Node

func fetch_scores() -> void:
	var request := SentryHTTPRequest.new()
	request.timeout = 10.0
	add_child(request)

	var error: Error = request.request("https://api.example.com/scores")
	if error != OK:
		request.queue_free()
		push_error("Could not start the scores request: %s" % error_string(error))
		return

	var response: Array = await request.request_completed
	request.queue_free()

	var result: int = response[0]
	var status_code: int = response[1]
	var body: PackedByteArray = response[3]
	if result != HTTPRequest.RESULT_SUCCESS or status_code != 200:
		push_error("Could not fetch scores")
		return

	var scores: Variant = JSON.parse_string(body.get_string_from_utf8())
	print(scores)
```

Each request starts an `http.client` span and uses the active span as its parent when one exists. The span ends when the request completes, is canceled, or the node leaves the scene tree.

The SDK adds trace headers according to your [trace propagation targets](https://docs.sentry.io/platforms/godot/tracing/distributed-tracing/limiting-trace-propagation.md) without replacing headers supplied by your code. For Web exports, the destination must also [allow the tracing headers through CORS](https://docs.sentry.io/platforms/godot/tracing/distributed-tracing/dealing-with-cors-issues.md).

The span records the request method, host, redacted URL, request and response body sizes, and HTTP response status code. For transport failures, it also records an error type when available.

HTTP responses with status codes of `400` or higher mark the span as an error. Recorded URLs omit credentials, query strings, and fragments. The SDK doesn't record body contents or custom header values.

Each `SentryHTTPRequest` handles one request at a time. Use a separate node for each concurrent request, and call its methods from the main thread even when `use_threads` is enabled.

##### Check Redirect Destinations

Godot forwards custom headers, including tracing headers, when it follows redirects. `SentryHTTPRequest` checks propagation targets against only the initial URL. If a request can redirect to an untrusted destination, set `max_redirects` to `0`, validate the `Location` response header, and start a separate request.

### [Instrument HTTP Requests in C#](https://docs.sentry.io/platforms/godot/tracing/instrumentation.md#instrument-http-requests-in-c)

For C# requests made with `HttpClient`, use the .NET SDK's `SentryHttpMessageHandler`:

```csharp
using System.Net.Http;
using Sentry;

var httpClient = new HttpClient(new SentryHttpMessageHandler());
```

The handler creates spans for outgoing requests and propagates trace headers. See [Automatic Instrumentation for .NET](https://docs.sentry.io/platforms/dotnet/tracing/instrumentation/automatic-instrumentation.md) for details.

## [Wrap a Synchronous Operation](https://docs.sentry.io/platforms/godot/tracing/instrumentation.md#wrap-a-synchronous-operation)

Use `SentrySDK.with_span()` when the operation fits inside one synchronous callable. It makes the span active while the callable runs, ends it automatically, and returns the callable's result:

```GDScript
func generate_chunk(chunk: Vector2i) -> bool:
	# Keep telemetry from chunk generation associated with this span.
	var generated: bool = SentrySDK.with_span(
		"generate_chunk",
		func(span: SentrySpan) -> bool:
			# Identify the chunk when inspecting the trace.
			span.set_attributes({
				"chunk.x": chunk.x,
				"chunk.y": chunk.y,
			})

			var succeeded := _generate_chunk(chunk)
			# Record whether the measured operation succeeded.
			if succeeded:
				span.set_status(SentrySpan.SPAN_STATUS_OK)
			else:
				span.set_status(SentrySpan.SPAN_STATUS_ERROR)
			return succeeded
	)
	return generated
```

Telemetry captured on the same thread inside the callable is connected to the span. Nested `with_span()` calls create child spans and restore the enclosing span when the inner callable returns.

##### Synchronous Callables Only

`with_span()` ends the span when the callable returns. If the callable uses `await`, the span ends at the first `await` instead of when the asynchronous work finishes. Use `start_span()` for work that crosses an asynchronous boundary.

## [Control the Span Lifetime](https://docs.sentry.io/platforms/godot/tracing/instrumentation.md#control-the-span-lifetime)

Use `SentrySDK.start_span()` when an operation crosses functions or an `await`, needs `sentry.op` set when the span starts, or needs explicit control over its parent. End every span yourself when the measured work finishes:

```GDScript
func load_level(level_path: String) -> void:
	# Start the span before the operation begins.
	var span := SentrySDK.start_span("load_level", {
		"sentry.op": "asset.load",
		"level.path": level_path,
	})

	var packed_scene: PackedScene = await _load_level_async(level_path)
	# Record the final outcome on the span.
	if packed_scene == null:
		span.set_status(SentrySpan.SPAN_STATUS_ERROR)
	else:
		span.set_status(SentrySpan.SPAN_STATUS_OK)
		_change_level(packed_scene)

	# Spans created with start_span() must be ended explicitly.
	span.end()
```

Pass `sentry.op` when starting the span to categorize the work in Sentry. Some platforms fix the operation when the span starts, so setting this attribute later isn't supported.

By default, starting an active span forks the current scope and makes the fork current. Changes made to that scope do not outlive the span. See [Scopes and Active Spans](https://docs.sentry.io/platforms/godot/enriching-events/scopes.md#scopes-and-active-spans) for details.

##### End Every Span

With the default static trace lifecycle, an unended root span isn't sent. End child spans before their parent; platforms can handle unfinished children differently if the parent ends first.

## [Build a Span Hierarchy](https://docs.sentry.io/platforms/godot/tracing/instrumentation.md#build-a-span-hierarchy)

By default, each new span becomes a child of the active span. Once the child span ends, its parent becomes active again:

```GDScript
# Start the parent span. It becomes the active span.
var level_span := SentrySDK.start_span("prepare_level", {
	"sentry.op": "level.prepare",
})

# The active level_span becomes this span's parent.
var navmesh_span := SentrySDK.start_span("bake_navmesh")
_bake_navmesh()
# Ending the child makes level_span active again.
navmesh_span.end()

# The active level_span also becomes this span's parent.
var lighting_span := SentrySDK.start_span("bake_lighting")
_bake_lighting()
lighting_span.end()

# End the parent after all its children have ended.
level_span.end()
```

Use the `parent_span` argument when the child should belong to a specific span instead of the currently active one. The parent must still be open, so insert this fragment before `level_span.end()` in the preceding example:

```GDScript
# Group this work under the level-load span.
var child_span := SentrySDK.start_span("stream_region", {}, level_span)
_stream_region()
child_span.end()
```

Pass `null` as `parent_span` to force a new root span. Call `SentrySDK.get_active_span()` when lower-level code needs to read the span attached to the current scope.

Starting a span doesn't start a new trace. To start a separate trace—for example, for a new match—call `SentrySDK.start_new_trace()`. See [Start a New Trace](https://docs.sentry.io/platforms/godot/tracing/distributed-tracing/custom-trace-propagation.md#start-a-new-trace).

## [Start an Inactive Span](https://docs.sentry.io/platforms/godot/tracing/instrumentation.md#start-an-inactive-span)

Use an inactive span when work should be grouped under a parent but remain independent of the currently active span. Because it doesn't become active, it doesn't affect `SentrySDK.get_active_span()`, make new spans its children automatically, or add its trace context to telemetry captured alongside it.

For example, measure optional asset preloading under a broader level-load span without making the preload span active:

```GDScript
func preload_optional_assets(parent_span: SentrySpan) -> void:
	# Keep this work separate from the currently active span.
	var span := SentrySDK.start_span(
		"preload_optional_assets", {}, parent_span, false
	)
	_preload_optional_assets()
	span.end()
```

## [Group Work Across Multiple Batches](https://docs.sentry.io/platforms/godot/tracing/instrumentation.md#group-work-across-multiple-batches)

An inactive parent can also group a long-running operation whose work happens in short batches across multiple frames or method calls. Start an active child for each batch by passing the parent explicitly.

Errors and other telemetry captured while a batch runs carry the active child's trace context. Between batches, the inactive parent leaves the current active span unchanged.

```GDScript
var preload_span: SentrySpan

func begin_asset_preload() -> void:
	# Keep the parent open between batches without making it active.
	preload_span = SentrySDK.start_span(
		"preload_assets",
		{"sentry.op": "asset.preload"},
		null,
		false,
	)

func preload_batch(asset_paths: Array[String]) -> void:
	# Start an active span so errors from this batch are linked to it.
	var batch_span := SentrySDK.start_span(
		"preload_batch",
		{"asset.count": asset_paths.size()},
		preload_span,
	)
	for asset_path in asset_paths:
		_preload_asset(asset_path)
	batch_span.end()

func finish_asset_preload() -> void:
	# End the parent after every batch has finished.
	preload_span.end()
```

## [Add Span Data](https://docs.sentry.io/platforms/godot/tracing/instrumentation.md#add-span-data)

Use `set_attribute()` or `set_attributes()` before ending a span to add searchable details about the operation. Attribute keys must not be empty, and values can be a `bool`, `int`, `float`, or `String`; the SDK converts other types to strings.

Use `set_status()` to record whether the operation succeeded or failed:

* `SentrySpan.SPAN_STATUS_OK` means the operation completed successfully.
* `SentrySpan.SPAN_STATUS_ERROR` means the operation failed.

A span without an explicit status is treated as successful.

## [Spans and Threads](https://docs.sentry.io/platforms/godot/tracing/instrumentation.md#spans-and-threads)

Spans belong to the thread that created them. Call their methods only from that thread. If work moves to another thread, start and end its spans inside that threaded function instead of passing a span across threads.

## [Distributed Tracing](https://docs.sentry.io/platforms/godot/tracing/instrumentation.md#distributed-tracing)

To connect a span with work in a backend service, [attach its trace headers to the outgoing request](https://docs.sentry.io/platforms/godot/tracing/distributed-tracing/custom-trace-propagation.md).
