bitfab-py 0.76.0__tar.gz → 0.76.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/PKG-INFO +1 -1
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/client.py +92 -29
- bitfab_py-0.76.2/bitfab/error_chain.py +101 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/replay.py +10 -12
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/subtree.py +9 -2
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/thread_propagation.py +15 -5
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/pyproject.toml +1 -1
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/README.md +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/__init__.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/agent_middleware.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/approval.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/assertion_categories.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/baml.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/baml_auto_trace.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/categories.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/claude_agent_sdk.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/client_patching.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/cloudReplay.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/cloud_replay_cli.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/commit_ref.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/compress.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/constants.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/datasets.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/db_snapshot.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/experiment_id.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/experiments.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/finalizers.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/git_command.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/git_state.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/graders.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/http.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/integration_fixture.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/jev.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/labels.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/langgraph.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/langgraph_integration.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/langgraph_runs.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/mock_matching.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/mock_override.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/openai_agent_sdk.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/organization_members.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/otel.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/payload_budget.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/processor_payload.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/provider_calls.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/replay_branch.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/replay_cli.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/replay_heartbeat.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/replay_interrupt.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/replay_invocation.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/replay_memory.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/replay_processes.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/replay_registry.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/seed_cli.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/selective_replay.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/serialize.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/simulation_plan.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/span_meta.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/span_origin.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/spans.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/timestamp.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/trace_completion.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/trace_metadata.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/trace_payload.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/traces.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/tracing.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/transport.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/transport_types.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/verdicts.py +0 -0
- {bitfab_py-0.76.0 → bitfab_py-0.76.2}/bitfab/warn_once.py +0 -0
|
@@ -45,6 +45,7 @@ from bitfab.constants import (
|
|
|
45
45
|
)
|
|
46
46
|
from bitfab.datasets import DatasetsClient
|
|
47
47
|
from bitfab.db_snapshot import DbSnapshotRef, build_snapshot_ref
|
|
48
|
+
from bitfab.error_chain import describe_error
|
|
48
49
|
from bitfab.experiment_id import resolve_experiment_id
|
|
49
50
|
from bitfab.experiments import ExperimentsClient
|
|
50
51
|
from bitfab.graders import GradersClient
|
|
@@ -477,6 +478,7 @@ class SpanContext(TypedDict, total=False):
|
|
|
477
478
|
enclosingTrace: TraceLink
|
|
478
479
|
variantChain: VariantChain
|
|
479
480
|
spanName: str
|
|
481
|
+
traceFunctionKey: str
|
|
480
482
|
rootTraceFunctionKey: str
|
|
481
483
|
instrumentation: SpanInstrumentation
|
|
482
484
|
declaredNode: bool
|
|
@@ -547,6 +549,10 @@ _span_stack: ContextVar[list[SpanContext]] = ContextVar(
|
|
|
547
549
|
"bitfab_span_stack", default=None
|
|
548
550
|
)
|
|
549
551
|
|
|
552
|
+
_enclosing_trace_function_keys: ContextVar[tuple[str, ...]] = ContextVar(
|
|
553
|
+
"bitfab_enclosing_trace_function_keys", default=()
|
|
554
|
+
)
|
|
555
|
+
|
|
550
556
|
# Global dict to track active trace states (trace_id -> TraceState)
|
|
551
557
|
_active_trace_states: dict[str, TraceState] = {}
|
|
552
558
|
|
|
@@ -680,11 +686,28 @@ def _trace_root_span_context() -> SpanContext | None:
|
|
|
680
686
|
)
|
|
681
687
|
|
|
682
688
|
|
|
689
|
+
def _running_trace_function_keys(stack: list[SpanContext]) -> list[str]:
|
|
690
|
+
keys: list[str] = []
|
|
691
|
+
candidates = list(_enclosing_trace_function_keys.get())
|
|
692
|
+
candidates.extend(context.get("traceFunctionKey", "") for context in stack)
|
|
693
|
+
for key in candidates:
|
|
694
|
+
if key and key not in keys:
|
|
695
|
+
keys.append(key)
|
|
696
|
+
return keys
|
|
697
|
+
|
|
698
|
+
|
|
683
699
|
class CurrentSpan:
|
|
684
700
|
"""Handle to the current active span, allowing context to be added."""
|
|
685
701
|
|
|
686
|
-
def __init__(
|
|
702
|
+
def __init__(
|
|
703
|
+
self, context: SpanContext, trace_function_keys: list[str] | None = None
|
|
704
|
+
) -> None:
|
|
687
705
|
self._context = context
|
|
706
|
+
self._trace_function_keys = (
|
|
707
|
+
trace_function_keys
|
|
708
|
+
if trace_function_keys is not None
|
|
709
|
+
else _running_trace_function_keys([context])
|
|
710
|
+
)
|
|
688
711
|
|
|
689
712
|
@property
|
|
690
713
|
def id(self) -> str:
|
|
@@ -696,6 +719,15 @@ class CurrentSpan:
|
|
|
696
719
|
"""The trace ID for the current span."""
|
|
697
720
|
return self._context["traceId"]
|
|
698
721
|
|
|
722
|
+
@property
|
|
723
|
+
def trace_function_keys(self) -> list[str]:
|
|
724
|
+
"""The keys of every trace function running at this point, outermost
|
|
725
|
+
first, each listed once. A trace function called inside another one
|
|
726
|
+
comes after it, so a cost recorded here can be counted toward each of
|
|
727
|
+
them. Empty outside a trace.
|
|
728
|
+
"""
|
|
729
|
+
return list(self._trace_function_keys)
|
|
730
|
+
|
|
699
731
|
def add_context(self, context: dict[str, Any]) -> None:
|
|
700
732
|
"""Add a context entry to this span.
|
|
701
733
|
|
|
@@ -744,8 +776,26 @@ def _on_event_loop_thread() -> bool:
|
|
|
744
776
|
class CurrentTrace:
|
|
745
777
|
"""Handle to the current active trace, allowing trace-level context to be set."""
|
|
746
778
|
|
|
747
|
-
def __init__(self,
|
|
748
|
-
self._trace_id =
|
|
779
|
+
def __init__(self, context: SpanContext) -> None:
|
|
780
|
+
self._trace_id = context["traceId"]
|
|
781
|
+
self._root_trace_function_key = context.get("rootTraceFunctionKey")
|
|
782
|
+
|
|
783
|
+
@property
|
|
784
|
+
def trace_function_key(self) -> str:
|
|
785
|
+
"""The key of the trace function the current trace is stored under,
|
|
786
|
+
which is the one that started it.
|
|
787
|
+
|
|
788
|
+
A ``@span`` or ``get_function`` call inside another trace function
|
|
789
|
+
joins that trace, so it reports the outer key. A ``trace()`` root
|
|
790
|
+
inside another root starts its own trace, so it reports its own key.
|
|
791
|
+
Empty string outside a trace.
|
|
792
|
+
"""
|
|
793
|
+
return (
|
|
794
|
+
_resolve_root_trace_function_key(
|
|
795
|
+
self._root_trace_function_key, self._trace_id, None
|
|
796
|
+
)
|
|
797
|
+
or ""
|
|
798
|
+
)
|
|
749
799
|
|
|
750
800
|
def _get_or_create_trace_state(self) -> TraceState:
|
|
751
801
|
"""Get existing trace state or create a new one."""
|
|
@@ -961,6 +1011,11 @@ class _NoOpCurrentSpan:
|
|
|
961
1011
|
"""Return empty string when outside a span context."""
|
|
962
1012
|
return ""
|
|
963
1013
|
|
|
1014
|
+
@property
|
|
1015
|
+
def trace_function_keys(self) -> list[str]:
|
|
1016
|
+
"""Return an empty list when outside a span context."""
|
|
1017
|
+
return []
|
|
1018
|
+
|
|
964
1019
|
def add_context(self, context: dict[str, Any]) -> None:
|
|
965
1020
|
pass
|
|
966
1021
|
|
|
@@ -971,6 +1026,11 @@ class _NoOpCurrentSpan:
|
|
|
971
1026
|
class _NoOpCurrentTrace:
|
|
972
1027
|
"""No-op trace handle returned when outside a span context."""
|
|
973
1028
|
|
|
1029
|
+
@property
|
|
1030
|
+
def trace_function_key(self) -> str:
|
|
1031
|
+
"""Return empty string when outside a span context."""
|
|
1032
|
+
return ""
|
|
1033
|
+
|
|
974
1034
|
def set_session_id(self, session_id: str) -> None:
|
|
975
1035
|
pass
|
|
976
1036
|
|
|
@@ -1001,7 +1061,7 @@ def get_current_span() -> CurrentSpan | _NoOpCurrentSpan:
|
|
|
1001
1061
|
"""
|
|
1002
1062
|
stack = _get_span_stack()
|
|
1003
1063
|
if stack:
|
|
1004
|
-
return CurrentSpan(stack[-1])
|
|
1064
|
+
return CurrentSpan(stack[-1], _running_trace_function_keys(stack))
|
|
1005
1065
|
return _NO_OP_SPAN
|
|
1006
1066
|
|
|
1007
1067
|
|
|
@@ -1045,10 +1105,22 @@ def get_current_trace() -> CurrentTrace | _NoOpCurrentTrace:
|
|
|
1045
1105
|
"""
|
|
1046
1106
|
stack = _get_span_stack()
|
|
1047
1107
|
if stack:
|
|
1048
|
-
return CurrentTrace(stack[-1]
|
|
1108
|
+
return CurrentTrace(stack[-1])
|
|
1049
1109
|
return _NO_OP_TRACE
|
|
1050
1110
|
|
|
1051
1111
|
|
|
1112
|
+
@contextlib.contextmanager
|
|
1113
|
+
def _trace_root_scope(scope: tuple[list[SpanContext], tuple[str, ...]]) -> Any:
|
|
1114
|
+
stack, enclosing_keys = scope
|
|
1115
|
+
stack_token = _span_stack.set(stack)
|
|
1116
|
+
keys_token = _enclosing_trace_function_keys.set(enclosing_keys)
|
|
1117
|
+
try:
|
|
1118
|
+
yield
|
|
1119
|
+
finally:
|
|
1120
|
+
_enclosing_trace_function_keys.reset(keys_token)
|
|
1121
|
+
_span_stack.reset(stack_token)
|
|
1122
|
+
|
|
1123
|
+
|
|
1052
1124
|
def _run_with_span_stack(stack: list[SpanContext], fn: Callable[[], T]) -> T:
|
|
1053
1125
|
"""Run a function with a new span stack context."""
|
|
1054
1126
|
token = _span_stack.set(stack)
|
|
@@ -2660,6 +2732,7 @@ class Bitfab:
|
|
|
2660
2732
|
"spanId": span_id,
|
|
2661
2733
|
"threadId": executing_thread.ident or 0,
|
|
2662
2734
|
"spanName": span_name,
|
|
2735
|
+
"traceFunctionKey": trace_function_key,
|
|
2663
2736
|
"rootTraceFunctionKey": (
|
|
2664
2737
|
root_state_at_open.get("traceFunctionKey", trace_function_key)
|
|
2665
2738
|
if root_state_at_open
|
|
@@ -3258,7 +3331,7 @@ class Bitfab:
|
|
|
3258
3331
|
yielded.append(value)
|
|
3259
3332
|
yield value
|
|
3260
3333
|
except Exception as e:
|
|
3261
|
-
error_msg =
|
|
3334
|
+
error_msg = describe_error(e)
|
|
3262
3335
|
raise
|
|
3263
3336
|
finally:
|
|
3264
3337
|
token = _span_stack.set(new_stack)
|
|
@@ -3271,7 +3344,7 @@ class Bitfab:
|
|
|
3271
3344
|
):
|
|
3272
3345
|
iterator.close()
|
|
3273
3346
|
except Exception as e:
|
|
3274
|
-
error_msg =
|
|
3347
|
+
error_msg = describe_error(e)
|
|
3275
3348
|
raise
|
|
3276
3349
|
finally:
|
|
3277
3350
|
if finalize is not None and error_msg is None:
|
|
@@ -3369,7 +3442,7 @@ class Bitfab:
|
|
|
3369
3442
|
error_msg = str(e) or e.__class__.__name__
|
|
3370
3443
|
raise
|
|
3371
3444
|
except Exception as e:
|
|
3372
|
-
error_msg =
|
|
3445
|
+
error_msg = describe_error(e)
|
|
3373
3446
|
raise
|
|
3374
3447
|
finally:
|
|
3375
3448
|
token = _span_stack.set(new_stack)
|
|
@@ -3385,7 +3458,7 @@ class Bitfab:
|
|
|
3385
3458
|
error_msg = str(e) or e.__class__.__name__
|
|
3386
3459
|
raise
|
|
3387
3460
|
except Exception as e:
|
|
3388
|
-
error_msg =
|
|
3461
|
+
error_msg = describe_error(e)
|
|
3389
3462
|
raise
|
|
3390
3463
|
finally:
|
|
3391
3464
|
if not sent:
|
|
@@ -3563,7 +3636,7 @@ class Bitfab:
|
|
|
3563
3636
|
span_ctx,
|
|
3564
3637
|
**base_params,
|
|
3565
3638
|
result=None,
|
|
3566
|
-
error=
|
|
3639
|
+
error=describe_error(e),
|
|
3567
3640
|
ended_at=_get_ended_at(),
|
|
3568
3641
|
)
|
|
3569
3642
|
raise
|
|
@@ -3649,7 +3722,7 @@ class Bitfab:
|
|
|
3649
3722
|
span_ctx,
|
|
3650
3723
|
**base_params,
|
|
3651
3724
|
result=None,
|
|
3652
|
-
error=
|
|
3725
|
+
error=describe_error(e),
|
|
3653
3726
|
ended_at=_get_ended_at(),
|
|
3654
3727
|
)
|
|
3655
3728
|
raise
|
|
@@ -4431,16 +4504,16 @@ class Bitfab:
|
|
|
4431
4504
|
surface="opt-out",
|
|
4432
4505
|
)(inner)
|
|
4433
4506
|
|
|
4434
|
-
def root_span_stack() -> list[SpanContext] | None:
|
|
4507
|
+
def root_span_stack() -> tuple[list[SpanContext], tuple[str, ...]] | None:
|
|
4435
4508
|
stack = _get_span_stack()
|
|
4436
4509
|
enclosing = stack[-1].get("surface") if stack else None
|
|
4437
4510
|
if enclosing == "opt-in":
|
|
4438
4511
|
raise _mixed_tracing_error("trace", "span")
|
|
4439
4512
|
if enclosing != "opt-out":
|
|
4440
|
-
return stack
|
|
4513
|
+
return stack, _enclosing_trace_function_keys.get()
|
|
4441
4514
|
if _in_replay_item() or _in_seed_scope():
|
|
4442
4515
|
return None
|
|
4443
|
-
return []
|
|
4516
|
+
return [], tuple(_running_trace_function_keys(stack))
|
|
4444
4517
|
|
|
4445
4518
|
if inspect.isasyncgenfunction(fn):
|
|
4446
4519
|
|
|
@@ -4458,21 +4531,15 @@ class Bitfab:
|
|
|
4458
4531
|
iterator = spanned(*args, **kwargs)
|
|
4459
4532
|
try:
|
|
4460
4533
|
while True:
|
|
4461
|
-
|
|
4462
|
-
try:
|
|
4534
|
+
with _trace_root_scope(root_stack):
|
|
4463
4535
|
try:
|
|
4464
4536
|
item = await iterator.__anext__()
|
|
4465
4537
|
except StopAsyncIteration:
|
|
4466
4538
|
return
|
|
4467
|
-
finally:
|
|
4468
|
-
_span_stack.reset(token)
|
|
4469
4539
|
yield item
|
|
4470
4540
|
finally:
|
|
4471
|
-
|
|
4472
|
-
try:
|
|
4541
|
+
with _trace_root_scope(root_stack):
|
|
4473
4542
|
await iterator.aclose()
|
|
4474
|
-
finally:
|
|
4475
|
-
_span_stack.reset(token)
|
|
4476
4543
|
|
|
4477
4544
|
elif inspect.iscoroutinefunction(fn):
|
|
4478
4545
|
|
|
@@ -4481,11 +4548,8 @@ class Bitfab:
|
|
|
4481
4548
|
root_stack = root_span_stack()
|
|
4482
4549
|
if root_stack is None:
|
|
4483
4550
|
return await body(*args, **kwargs)
|
|
4484
|
-
|
|
4485
|
-
try:
|
|
4551
|
+
with _trace_root_scope(root_stack):
|
|
4486
4552
|
return await spanned(*args, **kwargs)
|
|
4487
|
-
finally:
|
|
4488
|
-
_span_stack.reset(token)
|
|
4489
4553
|
|
|
4490
4554
|
else:
|
|
4491
4555
|
|
|
@@ -4494,9 +4558,8 @@ class Bitfab:
|
|
|
4494
4558
|
root_stack = root_span_stack()
|
|
4495
4559
|
if root_stack is None:
|
|
4496
4560
|
return body(*args, **kwargs)
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
)
|
|
4561
|
+
with _trace_root_scope(root_stack):
|
|
4562
|
+
return spanned(*args, **kwargs)
|
|
4500
4563
|
|
|
4501
4564
|
_trace_root_wrappers.add(detached)
|
|
4502
4565
|
return detached # type: ignore[return-value]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Read the chain of causes below an exception.
|
|
2
|
+
|
|
3
|
+
A database driver error usually reaches a traced function wrapped one or two
|
|
4
|
+
levels down (``raise QueryError(...) from timeout``). Recording only the top
|
|
5
|
+
message makes a connection timeout read as a broken query, so spans and replay
|
|
6
|
+
results carry the deepest cause too.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Callable
|
|
12
|
+
|
|
13
|
+
MAX_ERROR_CAUSE_DEPTH = 10
|
|
14
|
+
"""How many causes below an exception are followed."""
|
|
15
|
+
|
|
16
|
+
_CODE_ATTRIBUTES = ("code", "sqlstate", "pgcode", "errno")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _cause_of(error: BaseException) -> BaseException | None:
|
|
20
|
+
# The same rule Python's traceback uses: an explicit ``from`` cause wins,
|
|
21
|
+
# and an implicit context counts unless ``from None`` suppressed it. Bitfab's
|
|
22
|
+
# own replay errors also keep their cause on a ``cause`` attribute.
|
|
23
|
+
if error.__cause__ is not None:
|
|
24
|
+
return error.__cause__
|
|
25
|
+
attribute = getattr(error, "cause", None)
|
|
26
|
+
if isinstance(attribute, BaseException):
|
|
27
|
+
return attribute
|
|
28
|
+
if error.__suppress_context__:
|
|
29
|
+
return None
|
|
30
|
+
return error.__context__
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _error_code(error: BaseException) -> str | int | None:
|
|
34
|
+
for attribute in _CODE_ATTRIBUTES:
|
|
35
|
+
value = getattr(error, attribute, None)
|
|
36
|
+
if isinstance(value, (str, int)) and not isinstance(value, bool):
|
|
37
|
+
return value
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def error_causes(error: BaseException) -> list[BaseException]:
|
|
42
|
+
"""The causes below ``error``, nearest first, stopping at the depth limit
|
|
43
|
+
or at a cause already seen."""
|
|
44
|
+
causes: list[BaseException] = []
|
|
45
|
+
seen = {id(error)}
|
|
46
|
+
current = error
|
|
47
|
+
while len(causes) < MAX_ERROR_CAUSE_DEPTH:
|
|
48
|
+
cause = _cause_of(current)
|
|
49
|
+
if cause is None or id(cause) in seen:
|
|
50
|
+
break
|
|
51
|
+
causes.append(cause)
|
|
52
|
+
seen.add(id(cause))
|
|
53
|
+
current = cause
|
|
54
|
+
return causes
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def describe_error(error: BaseException) -> str:
|
|
58
|
+
"""The exception's message, followed by the deepest cause's message when
|
|
59
|
+
it wraps another exception, e.g. ``Failed query: ... (caused by:
|
|
60
|
+
connection timeout)``. The cause is left off when the message already
|
|
61
|
+
contains it."""
|
|
62
|
+
message = str(error)
|
|
63
|
+
causes = error_causes(error)
|
|
64
|
+
if not causes:
|
|
65
|
+
return message
|
|
66
|
+
deepest = str(causes[-1]).strip()
|
|
67
|
+
if not deepest or deepest in message:
|
|
68
|
+
return message
|
|
69
|
+
return f"{message} (caused by: {deepest})"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def serialize_error(
|
|
73
|
+
error: BaseException,
|
|
74
|
+
extra: Callable[[BaseException], dict[str, Any]] | None = None,
|
|
75
|
+
) -> dict[str, Any]:
|
|
76
|
+
"""An exception as a JSON record: type, message, the driver's code when it
|
|
77
|
+
has one, and its causes nested under ``cause`` down to the depth limit.
|
|
78
|
+
``extra`` adds fields a specific exception type carries."""
|
|
79
|
+
seen: set[int] = set()
|
|
80
|
+
|
|
81
|
+
def serialize(value: BaseException, depth: int) -> dict[str, Any]:
|
|
82
|
+
seen.add(id(value))
|
|
83
|
+
serialized: dict[str, Any] = {
|
|
84
|
+
"type": type(value).__name__,
|
|
85
|
+
"message": str(value),
|
|
86
|
+
}
|
|
87
|
+
code = _error_code(value)
|
|
88
|
+
if code is not None:
|
|
89
|
+
serialized["code"] = code
|
|
90
|
+
if extra is not None:
|
|
91
|
+
serialized.update(extra(value))
|
|
92
|
+
cause = _cause_of(value)
|
|
93
|
+
if (
|
|
94
|
+
cause is not None
|
|
95
|
+
and id(cause) not in seen
|
|
96
|
+
and depth < MAX_ERROR_CAUSE_DEPTH
|
|
97
|
+
):
|
|
98
|
+
serialized["cause"] = serialize(cause, depth + 1)
|
|
99
|
+
return serialized
|
|
100
|
+
|
|
101
|
+
return serialize(error, 1)
|
|
@@ -24,6 +24,7 @@ from bitfab.constants import (
|
|
|
24
24
|
_replay_context,
|
|
25
25
|
_seed_context,
|
|
26
26
|
)
|
|
27
|
+
from bitfab.error_chain import describe_error, serialize_error
|
|
27
28
|
from bitfab.git_command import run_git
|
|
28
29
|
from bitfab.integration_fixture import extract_integration_fixtures
|
|
29
30
|
from bitfab.mock_matching import MATCHER
|
|
@@ -580,19 +581,16 @@ def report_replay_progress(
|
|
|
580
581
|
|
|
581
582
|
def _replay_json_default(value: Any) -> Any:
|
|
582
583
|
if isinstance(value, BaseException):
|
|
583
|
-
|
|
584
|
-
"type": type(value).__name__,
|
|
585
|
-
"message": str(value),
|
|
586
|
-
}
|
|
587
|
-
if isinstance(value, DbBranchReplayError):
|
|
588
|
-
serialized["code"] = value.code
|
|
589
|
-
serialized["original_trace_id"] = value.original_trace_id
|
|
590
|
-
if value.cause is not None:
|
|
591
|
-
serialized["cause"] = value.cause
|
|
592
|
-
return serialized
|
|
584
|
+
return serialize_error(value, _replay_error_fields)
|
|
593
585
|
return str(value)
|
|
594
586
|
|
|
595
587
|
|
|
588
|
+
def _replay_error_fields(error: BaseException) -> dict[str, Any]:
|
|
589
|
+
if isinstance(error, DbBranchReplayError):
|
|
590
|
+
return {"original_trace_id": error.original_trace_id}
|
|
591
|
+
return {}
|
|
592
|
+
|
|
593
|
+
|
|
596
594
|
def serialize_replay_result(result: ReplayResult) -> str:
|
|
597
595
|
"""Serialize a replay result while retaining structured exception fields."""
|
|
598
596
|
return json.dumps(result, indent=2, default=_replay_json_default)
|
|
@@ -653,7 +651,7 @@ def _replay_item_error_message(error: BaseException) -> str:
|
|
|
653
651
|
"replaying it against the live database would produce a result that "
|
|
654
652
|
"looks valid but did not use the historical data you asked for."
|
|
655
653
|
)
|
|
656
|
-
return
|
|
654
|
+
return describe_error(error)
|
|
657
655
|
|
|
658
656
|
|
|
659
657
|
class AdaptContext(TypedDict):
|
|
@@ -3041,7 +3039,7 @@ async def _execute_item_async(
|
|
|
3041
3039
|
input=reported_input,
|
|
3042
3040
|
result=fn_result,
|
|
3043
3041
|
original_output=item.get("output"),
|
|
3044
|
-
error=
|
|
3042
|
+
error=describe_error(item_error) if item_error is not None else None,
|
|
3045
3043
|
trace_error=fn_error,
|
|
3046
3044
|
replay_error=replay_error,
|
|
3047
3045
|
duration_ms=replay_duration_ms,
|
|
@@ -23,6 +23,7 @@ from dataclasses import dataclass, field
|
|
|
23
23
|
from typing import Any, Callable
|
|
24
24
|
|
|
25
25
|
from bitfab.constants import _replay_context
|
|
26
|
+
from bitfab.error_chain import describe_error
|
|
26
27
|
from bitfab.trace_metadata import record_caller_trace_metadata
|
|
27
28
|
from bitfab.warn_once import warn_once
|
|
28
29
|
|
|
@@ -795,7 +796,9 @@ def _on_unwind(code: Any, instruction_offset: int, exception: BaseException) ->
|
|
|
795
796
|
# the first time an exception passed through library code. Filter instead.
|
|
796
797
|
if is_library_code(code.co_filename):
|
|
797
798
|
return None
|
|
798
|
-
return _close(
|
|
799
|
+
return _close(
|
|
800
|
+
code, None, f"{type(exception).__name__}: {describe_error(exception)}"
|
|
801
|
+
)
|
|
799
802
|
|
|
800
803
|
|
|
801
804
|
def _is_free_threaded() -> bool:
|
|
@@ -1033,7 +1036,11 @@ class BorrowedSpan:
|
|
|
1033
1036
|
updated[session] = _without(stack, index)
|
|
1034
1037
|
_open.set(updated)
|
|
1035
1038
|
if exc_type is not None:
|
|
1036
|
-
self.emit(
|
|
1039
|
+
self.emit(
|
|
1040
|
+
None,
|
|
1041
|
+
f"{exc_type.__name__}: "
|
|
1042
|
+
f"{describe_error(exc) if isinstance(exc, BaseException) else exc}",
|
|
1043
|
+
)
|
|
1037
1044
|
|
|
1038
1045
|
|
|
1039
1046
|
def borrowed_span(
|
|
@@ -17,8 +17,8 @@ the SDK's own span-transport threads are excluded so a long-lived dispatch
|
|
|
17
17
|
loop can never pin the context of whichever trace happened to spawn it.
|
|
18
18
|
Callables that already carry a context (``asyncio.to_thread`` submits
|
|
19
19
|
``Context.run``) pass through untouched. Only Bitfab's own variables are
|
|
20
|
-
carried (the span stack, the
|
|
21
|
-
origin: the identity of the thread that called submit()/start(), recorded onto
|
|
20
|
+
carried (the span stack, the keys of the trace functions around a nested
|
|
21
|
+
trace root, the replay context, the seed context, and the submit origin: the identity of the thread that called submit()/start(), recorded onto
|
|
22
22
|
worker spans as ``runtime.submit_thread_id``), so no other library's
|
|
23
23
|
contextvars change behavior.
|
|
24
24
|
|
|
@@ -54,7 +54,7 @@ _installed = False
|
|
|
54
54
|
_original_submit: Callable[..., Any] | None = None
|
|
55
55
|
_original_thread_start: Callable[..., Any] | None = None
|
|
56
56
|
|
|
57
|
-
_CapturedState = tuple[Any, Any, Any, dict[str, Any]]
|
|
57
|
+
_CapturedState = tuple[Any, tuple[str, ...], Any, Any, dict[str, Any]]
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
def _span_stack_var() -> contextvars.ContextVar[Any]:
|
|
@@ -63,9 +63,16 @@ def _span_stack_var() -> contextvars.ContextVar[Any]:
|
|
|
63
63
|
return _span_stack
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
def _enclosing_keys_var() -> contextvars.ContextVar[tuple[str, ...]]:
|
|
67
|
+
from bitfab.client import _enclosing_trace_function_keys
|
|
68
|
+
|
|
69
|
+
return _enclosing_trace_function_keys
|
|
70
|
+
|
|
71
|
+
|
|
66
72
|
def _capture() -> _CapturedState | None:
|
|
67
73
|
try:
|
|
68
74
|
stack = _span_stack_var().get()
|
|
75
|
+
enclosing_keys = _enclosing_keys_var().get()
|
|
69
76
|
replay_ctx = _replay_context.get()
|
|
70
77
|
seed_ctx = _seed_context.get()
|
|
71
78
|
except Exception:
|
|
@@ -74,14 +81,16 @@ def _capture() -> _CapturedState | None:
|
|
|
74
81
|
return None
|
|
75
82
|
submitter = threading.current_thread()
|
|
76
83
|
origin = {"thread_id": submitter.ident, "thread_name": submitter.name}
|
|
77
|
-
return (stack, replay_ctx, seed_ctx, origin)
|
|
84
|
+
return (stack, enclosing_keys, replay_ctx, seed_ctx, origin)
|
|
78
85
|
|
|
79
86
|
|
|
80
87
|
@contextlib.contextmanager
|
|
81
88
|
def _entered(state: _CapturedState) -> Any:
|
|
82
|
-
stack, replay_ctx, seed_ctx, origin = state
|
|
89
|
+
stack, enclosing_keys, replay_ctx, seed_ctx, origin = state
|
|
83
90
|
span_var = _span_stack_var()
|
|
91
|
+
keys_var = _enclosing_keys_var()
|
|
84
92
|
span_token = span_var.set(stack)
|
|
93
|
+
keys_token = keys_var.set(enclosing_keys)
|
|
85
94
|
replay_token = _replay_context.set(replay_ctx)
|
|
86
95
|
seed_token = _seed_context.set(seed_ctx)
|
|
87
96
|
origin_token = _submit_origin.set(origin)
|
|
@@ -89,6 +98,7 @@ def _entered(state: _CapturedState) -> Any:
|
|
|
89
98
|
yield
|
|
90
99
|
finally:
|
|
91
100
|
span_var.reset(span_token)
|
|
101
|
+
keys_var.reset(keys_token)
|
|
92
102
|
_replay_context.reset(replay_token)
|
|
93
103
|
_seed_context.reset(seed_token)
|
|
94
104
|
_submit_origin.reset(origin_token)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|