Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,34 @@ hide:

Full release notes for the Python SDK are available on [GitHub](https://github.com/taskbadger/taskbadger-python/releases).

## v2.5.2

**2026-08-07**

**Python SDK**

* **FIX** [`taskbadger_track=False`](python-celery.md#opting-out) now opts a single Celery execution out of tracking, as an `apply_async` argument or in the message headers. Previously it could only ever enable tracking: it was ignored when the `CelerySystemIntegration` was auto-tracking, and `apply_async` overwrote it on tasks using `base=Task`.

## v2.5.1

**2026-08-07**

**Python SDK**

* **CHANGED** [`list_tasks`](python.md#listing-tasks) returns a `TaskList` of `taskbadger.Task` objects, which can be iterated over directly. It previously returned the generated `PaginatedTaskList`, whose `results` were internal models without the SDK's update methods. Note that an empty `TaskList` is falsy, where `PaginatedTaskList` was always truthy.
* **FIX** Eager Celery tasks honour an explicit [`taskbadger_parent`](python-celery.md#subtasks), including `taskbadger_parent=None` to opt out of nesting.
* **FIX** Copying or pickling a `Task` no longer recurses until the stack overflows.

## v2.5.0

**2026-08-06**

**Python SDK**

* **NEW** [Parent and child tasks](python.md#parent-and-child-tasks). Tasks can be nested one level deep via the [`parent`](data_model.md#parent) field on `create_task` / `update_task`, and `list_tasks` can filter by parent. The [`@track` decorator](python-decorator.md#nested-tasks), [Celery](python-celery.md#subtasks) and [Procrastinate](python-procrastinate.md#subtasks) integrations set it automatically for tasks enqueued from within a tracked task.
* **NEW** [Context providers](python.md#error-context-providers), a pluggable way to attach extra data to a task when it errors, along with a [Sentry provider](python.md#sentry) that links a failed task to its Sentry issue. Install with `pip install 'taskbadger[sentry]'`.
* **NEW** `Task.error` accepts an `exception` argument, which is passed to the configured context providers.

## v2.4.0

**2026-07-30**
Expand Down
16 changes: 15 additions & 1 deletion docs/data_model.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ The main attributes or a task are:
: This is a computed percentage which is equivalent to `100 * value / value_max`. This will be `null`
if **value** is null.

<a name="data"></a>
`data`

: This can be used to store arbitrary JSON data that may be useful to store along with the task such
Expand Down Expand Up @@ -79,6 +80,18 @@ The main attributes or a task are:
with its logs. Set it when creating or updating a task, then filter tasks by exact match on this
value in the web UI.

<a name="parent"></a>
`parent`

: The ID of the task this task is part of. Tasks can only be nested one level deep, so the parent
must be a task that is not itself a child. A task's parent can not be changed once it has been
set. Both rules are enforced by the API, so breaking either one causes the request to fail.

It can be set explicitly when creating or updating a task. The Celery and Procrastinate
integrations also set it automatically for tasks enqueued from within a tracked task, and the
`track` decorator sets it on a decorated function called from within one. Each integration
excludes some cases — see [Python SDK](python.md#parent-and-child-tasks) for details.

### Example Task

```json
Expand Down Expand Up @@ -106,7 +119,8 @@ The main attributes or a task are:
{"environment": "production"}
],
"queue": "default",
"external_id": "celery-abc-123"
"external_id": "celery-abc-123",
"parent": null
}
```

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ task.update(status=StatusEnum.SUCCESS, value=100)
Or monitor Celery tasks automatically with the [Celery integration](python-celery.md):

```python
from taskbadger.systems import CelerySystemIntegration
from taskbadger.systems.celery import CelerySystemIntegration

taskbadger.init(
token="YOUR_API_KEY",
Expand Down
168 changes: 163 additions & 5 deletions docs/python-celery.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ task that is executed by the Celery workers (except the internal Celery tasks),

```python
import taskbadger
from taskbadger.systems import CelerySystemIntegration
from taskbadger.systems.celery import CelerySystemIntegration

taskbadger.init(
token="YOUR_API_KEY",
Expand Down Expand Up @@ -107,9 +107,22 @@ The Task Badger task will also be updated when the task completes.
### Task Customization

You can pass additional parameters to the Task Badger `Task` class which will be used when creating the task.
This can be done by passing keyword arguments prefixed with `taskbadger_` to the `.appy_async()` function or
This can be done by passing keyword arguments prefixed with `taskbadger_` to the `.apply_async()` function or
to the task decorator.

!!! warning "`taskbadger_` arguments to `apply_async` require the task base class"

It is `taskbadger.celery.Task` that intercepts `taskbadger_`-prefixed arguments to `apply_async`, so
those only work on tasks declared with `base=Task`. On a plain Celery task they are silently ignored:
the task still publishes, and is still tracked if the
[system integration](#celery-system-integration) tracks it, but the options have no effect.

`taskbadger_` arguments on the **task decorator** are not affected. They are read off the task class
when the task is published, so they apply whether or not the task uses `base=Task`.

To set options per call on a task that is tracked by the system integration alone, pass them in the
message headers instead — see [Customization without the task base class](#customization-without-the-task-base-class).

```python
# using the task decorator

Expand All @@ -120,14 +133,14 @@ def my_task(arg1, arg2):

# using individual keyword arguments
my_task.apply_async(
arg1, arg2,
args=[arg1, arg2],
taskbadger_name="my task",
taskbadger_value_max=1000,
taskbadger_data={"custom": "data"},
)

# using a dictionary
my_task.apply_async(arg1, arg2, taskbadger_kwargs={
my_task.apply_async(args=[arg1, arg2], taskbadger_kwargs={
"name": "my task",
"value_max": 1000,
"data": {"custom": "data"}
Expand All @@ -151,6 +164,58 @@ my_task.apply_async(arg1, arg2, taskbadger_kwargs={
==Since v1.4.0==


### Customization without the task base class

Task Badger options can also be passed in the Celery message headers, which works for any task,
including plain Celery tasks tracked by the `CelerySystemIntegration`:

```python
my_task.apply_async(
args=[arg1, arg2],
headers={"taskbadger_kwargs": {
"name": "my task",
"value_max": 1000,
"data": {"custom": "data"},
}},
)
```

Unlike the `taskbadger_`-prefixed arguments, the header is read when the task is published rather than
by the task class, so no `base=Task` is needed. It takes the same options as `taskbadger_kwargs`, minus
the prefix, and takes precedence over values set on the task decorator.

!!! warning "Eager tasks read fewer options from the header"

That applies to tasks which are actually published. When Celery runs a task [eagerly][always_eager]
nothing is published, so the Task Badger task is created as the task starts instead, reading the
header directly. Only `parent`, `heartbeat_interval` and `stale_timeout` are honoured there —
`name`, `value_max` and `data` are ignored.

`taskbadger_track` is also *required* for an eager task that doesn't use `base=Task`, even if the
system integration would otherwise track it.

If the task would not otherwise be tracked — it isn't using the base class and doesn't match the system
integration's tracking rules — add `taskbadger_track` to the headers to track it anyway:

```python
my_task.apply_async(
args=[arg1, arg2],
headers={
"taskbadger_track": True,
"taskbadger_kwargs": {"name": "my task"},
},
)
```

!!! note

`record_task_args` is a header of its own rather than an entry in `taskbadger_kwargs`:
`headers={"taskbadger_record_task_args": True}`.

The `taskbadger_task_id` attribute and `get_taskbadger_task()` method of the
[result object](#basic-usage) are added by `taskbadger.celery.Task`, so they are not available on
the result when using headers alone.

### Accessing the Task Object

The `taskbadger.celery.Task` class provides access to the Task Badger task object via the `taskbadger_task` property
Expand Down Expand Up @@ -269,6 +334,65 @@ Task Badger will create task records for each inner invocation with metadata sim
}
```

## Subtasks

==Since v2.5.0==

A Celery task published from inside a tracked task is automatically nested under it via the
[`parent`](data_model.md#parent) field, so you can see the work a task spawned.

Tasks nest a single level deep. A task published by a task that is itself a child becomes a sibling
of that child rather than a grandchild.

What gets nested:

- Tasks published from the body of a tracked task via `.delay()` or `.apply_async()`, including when
Celery runs them eagerly.
- Retries, which are nested under the first attempt.

What does not get nested:

- The next link of a `chain`, and `link` callbacks. These are successors of the task rather than work
it chose to enqueue.
- Tasks produced by [canvas primitives](#canvas-primitives-map-starmap-chunks) (`map`, `starmap`,
`chunks`) when they run on a worker. Their Task Badger tasks are created in the worker rather than
at publish time, so the enclosing task isn't visible there. They do nest when Celery runs eagerly.

!!! note

Known edge case: if a chain's next link is *also* called directly from the task body, that direct
call is not nested.

To override the automatic nesting, pass `taskbadger_parent` for the call. An explicit `None` makes the
task a root task even though it was published from inside a tracked task:

```python
# nest under a different task
my_task.apply_async(args=[arg1, arg2], taskbadger_parent=parent_task.id)

# opt out of nesting
my_task.apply_async(args=[arg1, arg2], taskbadger_parent=None)
```

`taskbadger_parent` takes a **Task Badger** task ID, not a Celery task or result ID.

As with the other `taskbadger_` arguments to `apply_async` this requires the task to use `base=Task`;
without it, pass `headers={"taskbadger_kwargs": {"parent": None}}` instead. See
[Customization without the task base class](#customization-without-the-task-base-class).

==Since v2.5.1== eager tasks honour `taskbadger_parent`; before that they always nested under the
enclosing task.

!!! warning "Canvas primitives ignore `taskbadger_parent`"

[Canvas primitives](#canvas-primitives-map-starmap-chunks) (`map`, `starmap`, `chunks`) are
published under Celery's own `celery.map` / `celery.starmap` task rather than your own, so
`taskbadger_parent` is never intercepted and the `taskbadger_kwargs` header doesn't reach the
worker. On a worker their parent can't be set per call, and they are not nested at all.

Run eagerly they do nest under the enclosing task, and there the parent can be overridden with
`headers={"taskbadger_kwargs": {"parent": ...}}`.

## External ID

The Celery task ID is automatically recorded on the Task Badger task's
Expand All @@ -277,8 +401,42 @@ originating Celery task.

## Opting out

If you want to prevent TaskBadger from tracking a particular execution, set the `taskbadger_track` header (False) when publishing:
To stop Task Badger tracking a single execution, set `taskbadger_track` to `False`, either as an
argument to `apply_async` or in the message headers:

```python
# as an argument — needs base=Task
my_task.apply_async(args=[arg1, arg2], taskbadger_track=False)

# in the headers — works for any task
my_task.apply_async(args=[arg1, arg2], headers={"taskbadger_track": False})

# canvas primitives take the header form only
add.map([(1, 2), (2, 3)]).apply_async(headers={"taskbadger_track": False})
```

This takes precedence over the `CelerySystemIntegration`, so it opts out even when `auto_track_tasks`
is on, and over the `base=Task` class.

The value must be exactly `False`. Omitting it leaves tracking to the normal rules.

!!! warning "The argument form needs the task base class"

As with the other [`taskbadger_` arguments to `apply_async`](#task-customization),
`taskbadger_track=False` is intercepted by `taskbadger.celery.Task`, so it only works on tasks
declared with `base=Task`. On a plain Celery task, or on a
[canvas primitive](#canvas-primitives-map-starmap-chunks), it is silently ignored and the task
stays tracked.

The header form works in all three cases, so prefer it unless you know the task uses `base=Task`.

To exclude a task from tracking on every call rather than per execution, use the `excludes` argument to
[`CelerySystemIntegration`](#celery-system-integration), which matches on task name.

!!! warning "Only works from v2.5.2"

==Since v2.5.2==

Before that this header could only ever *enable* tracking, never suppress it. `False` was
indistinguishable from omitting the header, and `apply_async` on a `base=Task` task overwrote it
with `True`. On earlier versions use `excludes` instead.
23 changes: 22 additions & 1 deletion docs/python-decorator.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,27 @@ status to `success` when the function completes or `error` if an exception is ra
The decorator also applies the `taskbadger.Session` context manager to the function.
See [connection management](python.md#connection-management).

When the function raises, the exception is recorded on the task data along with any context from the
configured [context providers](python.md#error-context-providers).

## Nested tasks

==Since v2.5.0==

Tasks tracked by another integration while a decorated function is running are nested under its task
via the [`parent`](data_model.md#parent) field. That means other `@track` decorated functions called
from the body, as well as [Celery](python-celery.md#subtasks) and
[Procrastinate](python-procrastinate.md#subtasks) tasks enqueued from it. A bare `Task.create` in the
function body is not nested unless you pass `parent` yourself.

Each integration carves out some exceptions — Celery, for instance, doesn't nest chain successors,
`link` callbacks, or canvas primitives running on a worker. The [Celery](python-celery.md#subtasks) and
[Procrastinate](python-procrastinate.md#subtasks) pages list what does and doesn't get nested.

Tasks nest a single level deep, so anything created by a decorated function that is itself a child
becomes a sibling of that child rather than a grandchild. Passing `parent` to the decorator explicitly
overrides the automatic nesting.

## API Docs

::: taskbadger.track
::: taskbadger.track
13 changes: 13 additions & 0 deletions docs/python-procrastinate.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,19 @@ Task Badger task, so you can monitor the history and health of your scheduled jo
The name of the Procrastinate queue a task is deferred to is automatically recorded on the Task Badger
task's [`queue`](data_model.md#queue) field.

## Subtasks

==Since v2.5.0==

A job deferred from inside a tracked task is automatically nested under it via the
[`parent`](data_model.md#parent) field, so you can see the work a job spawned.

Tasks nest a single level deep, so a job deferred by a task that is itself a child becomes a sibling
of that child rather than a grandchild.

Nesting relies on the same wrapping as the rest of the integration, so the cases listed under
[Known Limitations](#known-limitations) are neither tracked nor nested.

## External ID

The Procrastinate job ID is automatically recorded on the Task Badger task's
Expand Down
Loading