mseep-agentops 0.4.18__py3-none-any.whl

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.
Files changed (94) hide show
  1. agentops/__init__.py +488 -0
  2. agentops/client/__init__.py +5 -0
  3. agentops/client/api/__init__.py +71 -0
  4. agentops/client/api/base.py +162 -0
  5. agentops/client/api/types.py +21 -0
  6. agentops/client/api/versions/__init__.py +10 -0
  7. agentops/client/api/versions/v3.py +65 -0
  8. agentops/client/api/versions/v4.py +104 -0
  9. agentops/client/client.py +211 -0
  10. agentops/client/http/__init__.py +0 -0
  11. agentops/client/http/http_adapter.py +116 -0
  12. agentops/client/http/http_client.py +215 -0
  13. agentops/config.py +268 -0
  14. agentops/enums.py +36 -0
  15. agentops/exceptions.py +38 -0
  16. agentops/helpers/__init__.py +44 -0
  17. agentops/helpers/dashboard.py +54 -0
  18. agentops/helpers/deprecation.py +50 -0
  19. agentops/helpers/env.py +52 -0
  20. agentops/helpers/serialization.py +137 -0
  21. agentops/helpers/system.py +178 -0
  22. agentops/helpers/time.py +11 -0
  23. agentops/helpers/version.py +36 -0
  24. agentops/instrumentation/__init__.py +598 -0
  25. agentops/instrumentation/common/__init__.py +82 -0
  26. agentops/instrumentation/common/attributes.py +278 -0
  27. agentops/instrumentation/common/instrumentor.py +147 -0
  28. agentops/instrumentation/common/metrics.py +100 -0
  29. agentops/instrumentation/common/objects.py +26 -0
  30. agentops/instrumentation/common/span_management.py +176 -0
  31. agentops/instrumentation/common/streaming.py +218 -0
  32. agentops/instrumentation/common/token_counting.py +177 -0
  33. agentops/instrumentation/common/version.py +71 -0
  34. agentops/instrumentation/common/wrappers.py +235 -0
  35. agentops/legacy/__init__.py +277 -0
  36. agentops/legacy/event.py +156 -0
  37. agentops/logging/__init__.py +4 -0
  38. agentops/logging/config.py +86 -0
  39. agentops/logging/formatters.py +34 -0
  40. agentops/logging/instrument_logging.py +91 -0
  41. agentops/sdk/__init__.py +27 -0
  42. agentops/sdk/attributes.py +151 -0
  43. agentops/sdk/core.py +607 -0
  44. agentops/sdk/decorators/__init__.py +51 -0
  45. agentops/sdk/decorators/factory.py +486 -0
  46. agentops/sdk/decorators/utility.py +216 -0
  47. agentops/sdk/exporters.py +87 -0
  48. agentops/sdk/processors.py +71 -0
  49. agentops/sdk/types.py +21 -0
  50. agentops/semconv/__init__.py +36 -0
  51. agentops/semconv/agent.py +29 -0
  52. agentops/semconv/core.py +19 -0
  53. agentops/semconv/enum.py +11 -0
  54. agentops/semconv/instrumentation.py +13 -0
  55. agentops/semconv/langchain.py +63 -0
  56. agentops/semconv/message.py +61 -0
  57. agentops/semconv/meters.py +24 -0
  58. agentops/semconv/resource.py +52 -0
  59. agentops/semconv/span_attributes.py +118 -0
  60. agentops/semconv/span_kinds.py +50 -0
  61. agentops/semconv/status.py +11 -0
  62. agentops/semconv/tool.py +15 -0
  63. agentops/semconv/workflow.py +69 -0
  64. agentops/validation.py +357 -0
  65. mseep_agentops-0.4.18.dist-info/METADATA +49 -0
  66. mseep_agentops-0.4.18.dist-info/RECORD +94 -0
  67. mseep_agentops-0.4.18.dist-info/WHEEL +5 -0
  68. mseep_agentops-0.4.18.dist-info/licenses/LICENSE +21 -0
  69. mseep_agentops-0.4.18.dist-info/top_level.txt +2 -0
  70. tests/__init__.py +0 -0
  71. tests/conftest.py +10 -0
  72. tests/unit/__init__.py +0 -0
  73. tests/unit/client/__init__.py +1 -0
  74. tests/unit/client/test_http_adapter.py +221 -0
  75. tests/unit/client/test_http_client.py +206 -0
  76. tests/unit/conftest.py +54 -0
  77. tests/unit/sdk/__init__.py +1 -0
  78. tests/unit/sdk/instrumentation_tester.py +207 -0
  79. tests/unit/sdk/test_attributes.py +392 -0
  80. tests/unit/sdk/test_concurrent_instrumentation.py +468 -0
  81. tests/unit/sdk/test_decorators.py +763 -0
  82. tests/unit/sdk/test_exporters.py +241 -0
  83. tests/unit/sdk/test_factory.py +1188 -0
  84. tests/unit/sdk/test_internal_span_processor.py +397 -0
  85. tests/unit/sdk/test_resource_attributes.py +35 -0
  86. tests/unit/test_config.py +82 -0
  87. tests/unit/test_context_manager.py +777 -0
  88. tests/unit/test_events.py +27 -0
  89. tests/unit/test_host_env.py +54 -0
  90. tests/unit/test_init_py.py +501 -0
  91. tests/unit/test_serialization.py +433 -0
  92. tests/unit/test_session.py +676 -0
  93. tests/unit/test_user_agent.py +34 -0
  94. tests/unit/test_validation.py +405 -0
@@ -0,0 +1,777 @@
1
+ import unittest
2
+ from unittest.mock import Mock, patch
3
+ import threading
4
+ import time
5
+ import asyncio
6
+
7
+ from agentops import start_trace
8
+ from agentops.sdk.core import TraceContext
9
+ from opentelemetry.trace import StatusCode
10
+
11
+
12
+ class TestContextManager(unittest.TestCase):
13
+ """Test the context manager functionality of TraceContext"""
14
+
15
+ def test_trace_context_has_context_manager_methods(self):
16
+ """Test that TraceContext has __enter__ and __exit__ methods"""
17
+ # TraceContext should have context manager protocol methods
18
+ assert hasattr(TraceContext, "__enter__")
19
+ assert hasattr(TraceContext, "__exit__")
20
+
21
+ @patch("agentops.sdk.core.tracer")
22
+ def test_trace_context_enter_returns_self(self, mock_tracer):
23
+ """Test that __enter__ returns the TraceContext instance"""
24
+ mock_span = Mock()
25
+ mock_token = Mock()
26
+ trace_context = TraceContext(span=mock_span, token=mock_token)
27
+
28
+ # __enter__ should return self
29
+ result = trace_context.__enter__()
30
+ assert result is trace_context
31
+
32
+ @patch("agentops.sdk.core.tracer")
33
+ def test_trace_context_exit_calls_end_trace(self, mock_tracer):
34
+ """Test that __exit__ calls end_trace with appropriate state"""
35
+ mock_span = Mock()
36
+ mock_token = Mock()
37
+ trace_context = TraceContext(span=mock_span, token=mock_token)
38
+
39
+ # Test normal exit (no exception)
40
+ trace_context.__exit__(None, None, None)
41
+ mock_tracer.end_trace.assert_called_once_with(trace_context, StatusCode.OK)
42
+
43
+ @patch("agentops.sdk.core.tracer")
44
+ def test_trace_context_exit_with_exception_sets_error_state(self, mock_tracer):
45
+ """Test that __exit__ sets ERROR state when exception occurs"""
46
+ mock_span = Mock()
47
+ mock_token = Mock()
48
+ trace_context = TraceContext(span=mock_span, token=mock_token)
49
+
50
+ # Test exit with exception
51
+ mock_tracer.reset_mock()
52
+ exc_type = ValueError
53
+ exc_val = ValueError("test error")
54
+ exc_tb = None
55
+
56
+ trace_context.__exit__(exc_type, exc_val, exc_tb)
57
+ mock_tracer.end_trace.assert_called_once_with(trace_context, StatusCode.ERROR)
58
+
59
+ @patch("agentops.sdk.core.tracer")
60
+ @patch("agentops.tracer")
61
+ def test_context_manager_usage_pattern(self, mock_agentops_tracer, mock_core_tracer):
62
+ """Test using start_trace as a context manager"""
63
+ # Create a mock TraceContext
64
+ mock_span = Mock()
65
+ mock_token = Mock()
66
+ mock_trace_context = TraceContext(span=mock_span, token=mock_token)
67
+
68
+ # Mock the tracer's start_trace method to return our TraceContext
69
+ mock_agentops_tracer.initialized = True
70
+ mock_agentops_tracer.start_trace.return_value = mock_trace_context
71
+
72
+ # Use as context manager
73
+ with start_trace("test_trace") as trace:
74
+ assert trace is mock_trace_context
75
+ assert trace.span is mock_span
76
+ assert trace.token is mock_token
77
+
78
+ # Verify start_trace was called
79
+ mock_agentops_tracer.start_trace.assert_called_once_with(trace_name="test_trace", tags=None)
80
+ # Verify end_trace was called
81
+ mock_core_tracer.end_trace.assert_called_once_with(mock_trace_context, StatusCode.OK)
82
+
83
+ @patch("agentops.sdk.core.tracer")
84
+ @patch("agentops.tracer")
85
+ def test_context_manager_with_exception(self, mock_agentops_tracer, mock_core_tracer):
86
+ """Test context manager handles exceptions properly"""
87
+ # Create a mock TraceContext
88
+ mock_span = Mock()
89
+ mock_token = Mock()
90
+ mock_trace_context = TraceContext(span=mock_span, token=mock_token)
91
+
92
+ # Mock the tracer's start_trace method
93
+ mock_agentops_tracer.initialized = True
94
+ mock_agentops_tracer.start_trace.return_value = mock_trace_context
95
+
96
+ # Test exception handling
97
+ with self.assertRaises(ValueError):
98
+ with start_trace("test_trace"):
99
+ raise ValueError("Test exception")
100
+
101
+ # Verify end_trace was called with ERROR state
102
+ mock_core_tracer.end_trace.assert_called_once_with(mock_trace_context, StatusCode.ERROR)
103
+
104
+ @patch("agentops.sdk.core.tracer")
105
+ @patch("agentops.tracer")
106
+ @patch("agentops.init")
107
+ def test_start_trace_auto_initializes_if_needed(self, mock_init, mock_agentops_tracer, mock_core_tracer):
108
+ """Test that start_trace attempts to initialize SDK if not initialized"""
109
+ # First call: SDK not initialized
110
+ mock_agentops_tracer.initialized = False
111
+
112
+ # After init() is called, set initialized to True
113
+ def set_initialized():
114
+ mock_agentops_tracer.initialized = True
115
+
116
+ mock_init.side_effect = set_initialized
117
+
118
+ # Create a mock TraceContext for when start_trace is called after init
119
+ mock_span = Mock()
120
+ mock_token = Mock()
121
+ mock_trace_context = TraceContext(span=mock_span, token=mock_token)
122
+ mock_agentops_tracer.start_trace.return_value = mock_trace_context
123
+
124
+ # Call start_trace
125
+ result = start_trace("test_trace")
126
+
127
+ # Verify init was called
128
+ mock_init.assert_called_once()
129
+ # Verify start_trace was called on tracer
130
+ mock_agentops_tracer.start_trace.assert_called_once_with(trace_name="test_trace", tags=None)
131
+ assert result is mock_trace_context
132
+
133
+ def test_no_wrapper_classes_needed(self):
134
+ """Test that we don't need wrapper classes - TraceContext is the context manager"""
135
+ # TraceContext itself implements the context manager protocol
136
+ # No need for TraceContextManager wrapper
137
+ mock_span = Mock()
138
+ mock_token = Mock()
139
+ trace_context = TraceContext(span=mock_span, token=mock_token)
140
+
141
+ # Can use directly as context manager
142
+ assert hasattr(trace_context, "__enter__")
143
+ assert hasattr(trace_context, "__exit__")
144
+ assert callable(trace_context.__enter__)
145
+ assert callable(trace_context.__exit__)
146
+
147
+ @patch("agentops.sdk.core.tracer")
148
+ @patch("agentops.tracer")
149
+ def test_parallel_traces_independence(self, mock_agentops_tracer, mock_core_tracer):
150
+ """Test that multiple traces can run in parallel independently"""
151
+ # Create mock TraceContexts
152
+ mock_trace1 = TraceContext(span=Mock(), token=Mock())
153
+ mock_trace2 = TraceContext(span=Mock(), token=Mock())
154
+
155
+ # Mock the tracer to return different traces
156
+ mock_agentops_tracer.initialized = True
157
+ mock_agentops_tracer.start_trace.side_effect = [mock_trace1, mock_trace2]
158
+
159
+ # Start two traces
160
+ trace1 = start_trace("trace1")
161
+ trace2 = start_trace("trace2")
162
+
163
+ # They should be different instances
164
+ assert trace1 is not trace2
165
+ assert trace1 is mock_trace1
166
+ assert trace2 is mock_trace2
167
+
168
+ # End them independently using context manager protocol
169
+ trace1.__exit__(None, None, None)
170
+ trace2.__exit__(None, None, None)
171
+
172
+ # Verify both were ended
173
+ assert mock_core_tracer.end_trace.call_count == 2
174
+
175
+ @patch("agentops.sdk.core.tracer")
176
+ @patch("agentops.tracer")
177
+ def test_nested_context_managers_create_parallel_traces(self, mock_agentops_tracer, mock_core_tracer):
178
+ """Test that nested context managers create parallel traces, not parent-child"""
179
+ # Create mock TraceContexts
180
+ mock_outer = TraceContext(span=Mock(), token=Mock())
181
+ mock_inner = TraceContext(span=Mock(), token=Mock())
182
+
183
+ # Mock the tracer
184
+ mock_agentops_tracer.initialized = True
185
+ mock_agentops_tracer.start_trace.side_effect = [mock_outer, mock_inner]
186
+
187
+ # Use nested context managers
188
+ with start_trace("outer_trace") as outer:
189
+ assert outer is mock_outer
190
+ with start_trace("inner_trace") as inner:
191
+ assert inner is mock_inner
192
+ assert inner is not outer
193
+ # Both traces are active
194
+ assert mock_agentops_tracer.start_trace.call_count == 2
195
+
196
+ # Verify both were ended
197
+ assert mock_core_tracer.end_trace.call_count == 2
198
+ # Inner trace ended first, then outer
199
+ calls = mock_core_tracer.end_trace.call_args_list
200
+ assert calls[0][0][0] is mock_inner
201
+ assert calls[1][0][0] is mock_outer
202
+
203
+ @patch("agentops.sdk.core.tracer")
204
+ @patch("agentops.tracer")
205
+ def test_exception_in_nested_traces(self, mock_agentops_tracer, mock_core_tracer):
206
+ """Test exception handling in nested traces"""
207
+ # Create mock TraceContexts
208
+ mock_outer = TraceContext(span=Mock(), token=Mock())
209
+ mock_inner = TraceContext(span=Mock(), token=Mock())
210
+
211
+ # Mock the tracer
212
+ mock_agentops_tracer.initialized = True
213
+ mock_agentops_tracer.start_trace.side_effect = [mock_outer, mock_inner]
214
+
215
+ # Test exception in inner trace
216
+ with self.assertRaises(ValueError):
217
+ with start_trace("outer_trace"):
218
+ with start_trace("inner_trace"):
219
+ raise ValueError("Inner exception")
220
+
221
+ # Both traces should be ended with ERROR state
222
+ assert mock_core_tracer.end_trace.call_count == 2
223
+ calls = mock_core_tracer.end_trace.call_args_list
224
+ # Inner trace ended with ERROR
225
+ assert calls[0][0][0] is mock_inner
226
+ assert calls[0][0][1] == StatusCode.ERROR
227
+ # Outer trace also ended with ERROR (exception propagated)
228
+ assert calls[1][0][0] is mock_outer
229
+ assert calls[1][0][1] == StatusCode.ERROR
230
+
231
+ @patch("agentops.sdk.core.tracer")
232
+ def test_trace_context_attributes_access(self, mock_tracer):
233
+ """Test accessing span and token attributes of TraceContext"""
234
+ mock_span = Mock()
235
+ mock_token = Mock()
236
+ trace_context = TraceContext(span=mock_span, token=mock_token)
237
+
238
+ # Direct attribute access
239
+ assert trace_context.span is mock_span
240
+ assert trace_context.token is mock_token
241
+
242
+ @patch("agentops.sdk.core.tracer")
243
+ @patch("agentops.tracer")
244
+ def test_multiple_exceptions_in_sequence(self, mock_agentops_tracer, mock_core_tracer):
245
+ """Test handling multiple exceptions in sequence"""
246
+ # Mock the tracer
247
+ mock_agentops_tracer.initialized = True
248
+
249
+ # Create different mock traces for each attempt
250
+ mock_traces = [TraceContext(span=Mock(), token=Mock()) for _ in range(3)]
251
+ mock_agentops_tracer.start_trace.side_effect = mock_traces
252
+
253
+ # Multiple traces with exceptions
254
+ for i in range(3):
255
+ with self.assertRaises(RuntimeError):
256
+ with start_trace(f"trace_{i}"):
257
+ raise RuntimeError(f"Error {i}")
258
+
259
+ # All should be ended with ERROR state
260
+ assert mock_core_tracer.end_trace.call_count == 3
261
+ for i, call in enumerate(mock_core_tracer.end_trace.call_args_list):
262
+ assert call[0][0] is mock_traces[i]
263
+ assert call[0][1] == StatusCode.ERROR
264
+
265
+ @patch("agentops.sdk.core.tracer")
266
+ @patch("agentops.tracer")
267
+ def test_trace_with_tags_dict(self, mock_agentops_tracer, mock_core_tracer):
268
+ """Test starting trace with tags as dictionary"""
269
+ # Create a mock TraceContext
270
+ mock_trace = TraceContext(span=Mock(), token=Mock())
271
+ mock_agentops_tracer.initialized = True
272
+ mock_agentops_tracer.start_trace.return_value = mock_trace
273
+
274
+ tags = {"environment": "test", "version": "1.0"}
275
+ with start_trace("tagged_trace", tags=tags) as trace:
276
+ assert trace is mock_trace
277
+
278
+ # Verify tags were passed
279
+ mock_agentops_tracer.start_trace.assert_called_once_with(trace_name="tagged_trace", tags=tags)
280
+
281
+ @patch("agentops.sdk.core.tracer")
282
+ @patch("agentops.tracer")
283
+ def test_trace_with_tags_list(self, mock_agentops_tracer, mock_core_tracer):
284
+ """Test starting trace with tags as list"""
285
+ # Create a mock TraceContext
286
+ mock_trace = TraceContext(span=Mock(), token=Mock())
287
+ mock_agentops_tracer.initialized = True
288
+ mock_agentops_tracer.start_trace.return_value = mock_trace
289
+
290
+ tags = ["test", "v1.0", "experimental"]
291
+ with start_trace("tagged_trace", tags=tags) as trace:
292
+ assert trace is mock_trace
293
+
294
+ # Verify tags were passed
295
+ mock_agentops_tracer.start_trace.assert_called_once_with(trace_name="tagged_trace", tags=tags)
296
+
297
+ @patch("agentops.sdk.core.tracer")
298
+ @patch("agentops.tracer")
299
+ def test_trace_context_manager_thread_safety(self, mock_agentops_tracer, mock_core_tracer):
300
+ """Test that context managers work correctly in multi-threaded environment"""
301
+ # Mock the tracer
302
+ mock_agentops_tracer.initialized = True
303
+
304
+ # Create unique traces for each thread
305
+ thread_traces = {}
306
+ trace_lock = threading.Lock()
307
+
308
+ def create_trace(trace_name=None, tags=None, **kwargs):
309
+ trace = TraceContext(span=Mock(), token=Mock())
310
+ with trace_lock:
311
+ thread_traces[threading.current_thread().ident] = trace
312
+ return trace
313
+
314
+ mock_agentops_tracer.start_trace.side_effect = create_trace
315
+
316
+ results = []
317
+ errors = []
318
+
319
+ def worker(thread_id):
320
+ try:
321
+ with start_trace(f"thread_{thread_id}_trace") as trace:
322
+ # Each thread should get its own trace
323
+ results.append((thread_id, trace))
324
+ time.sleep(0.01) # Simulate some work
325
+ except Exception as e:
326
+ errors.append((thread_id, str(e)))
327
+
328
+ # Start multiple threads
329
+ threads = []
330
+ for i in range(5):
331
+ t = threading.Thread(target=worker, args=(i,))
332
+ threads.append(t)
333
+ t.start()
334
+
335
+ # Wait for all threads
336
+ for t in threads:
337
+ t.join()
338
+
339
+ # Check results
340
+ assert len(errors) == 0, f"Errors in threads: {errors}"
341
+ assert len(results) == 5
342
+
343
+ # Each thread should have gotten a unique trace
344
+ traces = [r[1] for r in results]
345
+ assert len(set(id(t) for t in traces)) == 5 # All unique
346
+
347
+ @patch("agentops.sdk.core.tracer")
348
+ @patch("agentops.tracer")
349
+ def test_context_manager_with_early_return(self, mock_agentops_tracer, mock_core_tracer):
350
+ """Test that context manager properly cleans up with early return"""
351
+ # Create a mock TraceContext
352
+ mock_trace = TraceContext(span=Mock(), token=Mock())
353
+ mock_agentops_tracer.initialized = True
354
+ mock_agentops_tracer.start_trace.return_value = mock_trace
355
+
356
+ def function_with_early_return():
357
+ with start_trace("early_return_trace"):
358
+ if True: # Early return condition
359
+ return "early"
360
+ return "normal"
361
+
362
+ result = function_with_early_return()
363
+ assert result == "early"
364
+
365
+ # Verify trace was still ended properly
366
+ mock_core_tracer.end_trace.assert_called_once_with(mock_trace, StatusCode.OK)
367
+
368
+ @patch("agentops.sdk.core.tracer")
369
+ @patch("agentops.tracer")
370
+ def test_context_manager_with_finally_block(self, mock_agentops_tracer, mock_core_tracer):
371
+ """Test context manager with try-finally block"""
372
+ # Create a mock TraceContext
373
+ mock_trace = TraceContext(span=Mock(), token=Mock())
374
+ mock_agentops_tracer.initialized = True
375
+ mock_agentops_tracer.start_trace.return_value = mock_trace
376
+
377
+ finally_executed = False
378
+
379
+ try:
380
+ with start_trace("finally_trace"):
381
+ try:
382
+ raise ValueError("Test")
383
+ finally:
384
+ finally_executed = True
385
+ except ValueError:
386
+ pass
387
+
388
+ assert finally_executed
389
+ # Trace should be ended with ERROR due to exception
390
+ mock_core_tracer.end_trace.assert_called_once_with(mock_trace, StatusCode.ERROR)
391
+
392
+ @patch("agentops.sdk.core.tracer")
393
+ @patch("agentops.tracer")
394
+ def test_backwards_compatibility_existing_patterns(self, mock_agentops_tracer, mock_core_tracer):
395
+ """Test that existing usage patterns continue to work"""
396
+ # Create mock traces
397
+ mock_traces = [TraceContext(span=Mock(), token=Mock()) for _ in range(3)]
398
+ mock_agentops_tracer.initialized = True
399
+ mock_agentops_tracer.start_trace.side_effect = mock_traces
400
+
401
+ # Pattern 1: Basic context manager
402
+ with start_trace("basic") as trace:
403
+ assert trace is mock_traces[0]
404
+
405
+ # Pattern 2: Manual start/end using context manager protocol
406
+ trace = start_trace("manual")
407
+ assert trace is mock_traces[1]
408
+ trace.__exit__(None, None, None) # Use context manager exit instead of end_trace
409
+
410
+ # Pattern 3: With tags
411
+ with start_trace("tagged", tags=["production", "v2"]) as trace:
412
+ assert trace is mock_traces[2]
413
+
414
+ # All patterns should work
415
+ assert mock_agentops_tracer.start_trace.call_count == 3
416
+ assert mock_core_tracer.end_trace.call_count == 3
417
+
418
+ @patch("agentops.sdk.core.tracer")
419
+ @patch("agentops.tracer")
420
+ def test_edge_case_none_trace_context(self, mock_agentops_tracer, mock_core_tracer):
421
+ """Test handling when start_trace returns None"""
422
+ # Mock SDK not initialized and init fails
423
+ mock_agentops_tracer.initialized = False
424
+
425
+ # When start_trace is called on uninitialized tracer, it returns None
426
+ with patch("agentops.init") as mock_init:
427
+ mock_init.side_effect = Exception("Init failed")
428
+
429
+ result = start_trace("test_trace")
430
+ assert result is None
431
+
432
+ # Verify start_trace was not called on tracer (since init failed)
433
+ mock_agentops_tracer.start_trace.assert_not_called()
434
+
435
+ @patch("agentops.sdk.core.tracer")
436
+ @patch("agentops.tracer")
437
+ def test_edge_case_tracing_core_not_initialized(self, mock_agentops_tracer, mock_core_tracer):
438
+ """Test behavior when global tracer is not initialized"""
439
+ mock_agentops_tracer.initialized = False
440
+
441
+ # Mock init to succeed but tracer still not initialized
442
+ with patch("agentops.init") as mock_init:
443
+ mock_init.return_value = None # init succeeds but doesn't set initialized
444
+
445
+ result = start_trace("test")
446
+ assert result is None
447
+
448
+ @patch("agentops.sdk.core.tracer")
449
+ @patch("agentops.tracer")
450
+ def test_edge_case_exception_in_exit_method(self, mock_agentops_tracer, mock_core_tracer):
451
+ """Test handling when exception occurs in __exit__ method"""
452
+ # Create a mock TraceContext
453
+ mock_trace = TraceContext(span=Mock(), token=Mock())
454
+ mock_agentops_tracer.initialized = True
455
+ mock_agentops_tracer.start_trace.return_value = mock_trace
456
+
457
+ # Make end_trace raise an exception
458
+ mock_core_tracer.end_trace.side_effect = RuntimeError("End trace failed")
459
+
460
+ # The exception in __exit__ should be suppressed
461
+ with start_trace("exception_in_exit"):
462
+ pass # Should not raise
463
+
464
+ # Verify end_trace was attempted
465
+ mock_core_tracer.end_trace.assert_called_once()
466
+
467
+ @patch("agentops.sdk.core.tracer")
468
+ @patch("agentops.tracer")
469
+ def test_performance_many_sequential_traces(self, mock_agentops_tracer, mock_core_tracer):
470
+ """Test performance with many sequential traces"""
471
+ # Mock the tracer
472
+ mock_agentops_tracer.initialized = True
473
+
474
+ # Create traces on demand
475
+ def create_trace(trace_name=None, tags=None, **kwargs):
476
+ return TraceContext(span=Mock(), token=Mock())
477
+
478
+ mock_agentops_tracer.start_trace.side_effect = create_trace
479
+
480
+ # Create many traces sequentially
481
+ start_time = time.time()
482
+ for i in range(100):
483
+ with start_trace(f"trace_{i}") as trace:
484
+ assert trace is not None
485
+ assert trace.span is not None
486
+
487
+ elapsed = time.time() - start_time
488
+
489
+ # Should complete reasonably quickly (< 1 second for 100 traces)
490
+ assert elapsed < 1.0, f"Too slow: {elapsed:.2f}s for 100 traces"
491
+
492
+ # Verify all traces were started and ended
493
+ assert mock_agentops_tracer.start_trace.call_count == 100
494
+ assert mock_core_tracer.end_trace.call_count == 100
495
+
496
+ @patch("agentops.sdk.core.tracer")
497
+ def test_trace_context_state_management(self, mock_tracer):
498
+ """Test that TraceContext properly manages its internal state"""
499
+ mock_span = Mock()
500
+ mock_token = Mock()
501
+ trace_context = TraceContext(span=mock_span, token=mock_token)
502
+
503
+ # Initial state
504
+ assert trace_context.span is mock_span
505
+ assert trace_context.token is mock_token
506
+
507
+ # Enter context
508
+ result = trace_context.__enter__()
509
+ assert result is trace_context
510
+
511
+ # Exit context normally
512
+ trace_context.__exit__(None, None, None)
513
+ mock_tracer.end_trace.assert_called_once_with(trace_context, StatusCode.OK)
514
+
515
+ # State should remain accessible after exit
516
+ assert trace_context.span is mock_span
517
+ assert trace_context.token is mock_token
518
+
519
+ @patch("agentops.sdk.core.tracer")
520
+ @patch("agentops.tracer")
521
+ def test_context_manager_with_async_context(self, mock_agentops_tracer, mock_core_tracer):
522
+ """Test context manager works in async context"""
523
+ # Create a mock TraceContext
524
+ mock_trace = TraceContext(span=Mock(), token=Mock())
525
+ mock_agentops_tracer.initialized = True
526
+ mock_agentops_tracer.start_trace.return_value = mock_trace
527
+
528
+ async def async_function():
529
+ with start_trace("async_context") as trace:
530
+ assert trace is mock_trace
531
+ await asyncio.sleep(0.01)
532
+ return "done"
533
+
534
+ # Run async function
535
+ result = asyncio.run(async_function())
536
+ assert result == "done"
537
+
538
+ # Verify trace was properly managed
539
+ mock_agentops_tracer.start_trace.assert_called_once_with(trace_name="async_context", tags=None)
540
+ mock_core_tracer.end_trace.assert_called_once_with(mock_trace, StatusCode.OK)
541
+
542
+
543
+ class TestContextManagerBackwardCompatibility(unittest.TestCase):
544
+ """Test backward compatibility for context manager usage"""
545
+
546
+ @patch("agentops.sdk.core.tracer")
547
+ @patch("agentops.tracer")
548
+ def test_existing_code_patterns_still_work(self, mock_agentops_tracer, mock_core_tracer):
549
+ """Test that code using the old patterns still works"""
550
+ # Create mock traces - need more than 3 for this test
551
+ mock_traces = [TraceContext(span=Mock(), token=Mock()) for _ in range(5)]
552
+ mock_agentops_tracer.initialized = True
553
+ mock_agentops_tracer.start_trace.side_effect = mock_traces
554
+
555
+ # Old pattern 1: Simple context manager
556
+ with start_trace("basic") as trace:
557
+ # Should work without changes
558
+ assert trace.span is not None
559
+
560
+ # Old pattern 2: Context manager with exception handling
561
+ try:
562
+ with start_trace("with_error") as trace:
563
+ raise ValueError("test")
564
+ except ValueError:
565
+ pass
566
+
567
+ # Old pattern 3: Nested traces
568
+ with start_trace("outer") as outer:
569
+ with start_trace("inner") as inner:
570
+ assert outer is not inner
571
+
572
+ # All should work - 4 calls total (basic, with_error, outer, inner)
573
+ assert mock_agentops_tracer.start_trace.call_count == 4
574
+ assert mock_core_tracer.end_trace.call_count == 4
575
+
576
+ @patch("agentops.sdk.core.tracer")
577
+ @patch("agentops.tracer")
578
+ def test_api_compatibility(self, mock_agentops_tracer, mock_core_tracer):
579
+ """Test that the API remains compatible"""
580
+ # Create mock TraceContexts for each call
581
+ mock_traces = [TraceContext(span=Mock(), token=Mock()) for _ in range(3)]
582
+ mock_agentops_tracer.initialized = True
583
+ mock_agentops_tracer.start_trace.side_effect = mock_traces
584
+
585
+ # Test function signatures
586
+ # start_trace(trace_name, tags=None)
587
+ trace1 = start_trace("test1")
588
+ assert trace1 is mock_traces[0]
589
+
590
+ trace2 = start_trace("test2", tags=["tag1", "tag2"])
591
+ assert trace2 is mock_traces[1]
592
+
593
+ trace3 = start_trace("test3", tags={"key": "value"})
594
+ assert trace3 is mock_traces[2]
595
+
596
+ # Use context manager protocol to end traces
597
+ trace1.__exit__(None, None, None)
598
+ trace2.__exit__(ValueError, ValueError("test"), None)
599
+ trace3.__exit__(None, None, None)
600
+
601
+ # All calls should work
602
+ assert mock_agentops_tracer.start_trace.call_count == 3
603
+ assert mock_core_tracer.end_trace.call_count == 3
604
+
605
+ @patch("agentops.sdk.core.tracer")
606
+ @patch("agentops.tracer")
607
+ def test_return_type_compatibility(self, mock_agentops_tracer, mock_core_tracer):
608
+ """Test that return types are compatible with existing code"""
609
+ mock_span = Mock()
610
+ mock_token = Mock()
611
+ mock_trace = TraceContext(span=mock_span, token=mock_token)
612
+ mock_agentops_tracer.initialized = True
613
+ mock_agentops_tracer.start_trace.return_value = mock_trace
614
+
615
+ # start_trace returns TraceContext (or None)
616
+ trace = start_trace("test")
617
+ assert isinstance(trace, TraceContext)
618
+ assert hasattr(trace, "span")
619
+ assert hasattr(trace, "token")
620
+ assert hasattr(trace, "__enter__")
621
+ assert hasattr(trace, "__exit__")
622
+
623
+ # Can be used as context manager
624
+ with trace:
625
+ pass
626
+
627
+ @patch("agentops.sdk.core.tracer")
628
+ @patch("agentops.tracer")
629
+ def test_context_manager_with_keyboard_interrupt(self, mock_agentops_tracer, mock_core_tracer):
630
+ """Test context manager handles KeyboardInterrupt properly"""
631
+ # Create a mock TraceContext
632
+ mock_trace = TraceContext(span=Mock(), token=Mock())
633
+ mock_agentops_tracer.initialized = True
634
+ mock_agentops_tracer.start_trace.return_value = mock_trace
635
+
636
+ # Test KeyboardInterrupt handling
637
+ with self.assertRaises(KeyboardInterrupt):
638
+ with start_trace("keyboard_interrupt_trace"):
639
+ raise KeyboardInterrupt()
640
+
641
+ # Verify end_trace was called with ERROR state
642
+ mock_core_tracer.end_trace.assert_called_once_with(mock_trace, StatusCode.ERROR)
643
+
644
+ @patch("agentops.sdk.core.tracer")
645
+ @patch("agentops.tracer")
646
+ def test_context_manager_with_system_exit(self, mock_agentops_tracer, mock_core_tracer):
647
+ """Test context manager handles SystemExit properly"""
648
+ # Create a mock TraceContext
649
+ mock_trace = TraceContext(span=Mock(), token=Mock())
650
+ mock_agentops_tracer.initialized = True
651
+ mock_agentops_tracer.start_trace.return_value = mock_trace
652
+
653
+ # Test SystemExit handling
654
+ with self.assertRaises(SystemExit):
655
+ with start_trace("system_exit_trace"):
656
+ raise SystemExit(1)
657
+
658
+ # Verify end_trace was called with ERROR state
659
+ mock_core_tracer.end_trace.assert_called_once_with(mock_trace, StatusCode.ERROR)
660
+
661
+ @patch("agentops.sdk.core.tracer")
662
+ @patch("agentops.tracer")
663
+ def test_context_manager_in_generator_function(self, mock_agentops_tracer, mock_core_tracer):
664
+ """Test context manager works correctly in generator functions"""
665
+ # Create mock traces
666
+ mock_traces = [TraceContext(span=Mock(), token=Mock()) for _ in range(3)]
667
+ mock_agentops_tracer.initialized = True
668
+ mock_agentops_tracer.start_trace.side_effect = mock_traces
669
+
670
+ def trace_generator():
671
+ with start_trace("generator_trace"):
672
+ yield 1
673
+ yield 2
674
+ yield 3
675
+
676
+ # Consume the generator
677
+ results = list(trace_generator())
678
+ assert results == [1, 2, 3]
679
+
680
+ # Verify trace was properly managed
681
+ mock_agentops_tracer.start_trace.assert_called_once()
682
+ mock_core_tracer.end_trace.assert_called_once()
683
+
684
+ @patch("agentops.sdk.core.tracer")
685
+ def test_context_manager_exit_return_value(self, mock_tracer):
686
+ """Test that __exit__ returns None (doesn't suppress exceptions)"""
687
+ mock_span = Mock()
688
+ mock_token = Mock()
689
+ trace_context = TraceContext(span=mock_span, token=mock_token)
690
+
691
+ # __exit__ should return None (or falsy) to not suppress exceptions
692
+ result = trace_context.__exit__(None, None, None)
693
+ assert result is None or not result
694
+
695
+ # Also with exception
696
+ result = trace_context.__exit__(ValueError, ValueError("test"), None)
697
+ assert result is None or not result
698
+
699
+ @patch("agentops.sdk.core.tracer")
700
+ @patch("agentops.tracer")
701
+ def test_context_manager_with_very_large_data(self, mock_agentops_tracer, mock_core_tracer):
702
+ """Test context manager with very large trace names and tags"""
703
+ # Create a mock TraceContext
704
+ mock_trace = TraceContext(span=Mock(), token=Mock())
705
+ mock_agentops_tracer.initialized = True
706
+ mock_agentops_tracer.start_trace.return_value = mock_trace
707
+
708
+ # Very large trace name and tags
709
+ large_trace_name = "x" * 10000
710
+ large_tags = {f"key_{i}": f"value_{i}" * 100 for i in range(100)}
711
+
712
+ with start_trace(large_trace_name, tags=large_tags) as trace:
713
+ assert trace is mock_trace
714
+
715
+ # Should handle large data without issues
716
+ mock_agentops_tracer.start_trace.assert_called_once()
717
+ args, kwargs = mock_agentops_tracer.start_trace.call_args
718
+ assert kwargs["trace_name"] == large_trace_name
719
+ assert kwargs["tags"] == large_tags
720
+
721
+ @patch("agentops.sdk.core.tracer")
722
+ @patch("agentops.tracer")
723
+ def test_context_manager_with_asyncio_tasks(self, mock_agentops_tracer, mock_core_tracer):
724
+ """Test context manager with multiple asyncio tasks"""
725
+ # Mock the tracer
726
+ mock_agentops_tracer.initialized = True
727
+
728
+ # Create traces for each task
729
+ trace_count = 0
730
+
731
+ def create_trace(trace_name=None, tags=None, **kwargs):
732
+ nonlocal trace_count
733
+ trace_count += 1
734
+ return TraceContext(span=Mock(name=f"span_{trace_count}"), token=Mock())
735
+
736
+ mock_agentops_tracer.start_trace.side_effect = create_trace
737
+
738
+ async def task_with_trace(task_id):
739
+ with start_trace(f"async_task_{task_id}"):
740
+ await asyncio.sleep(0.01)
741
+ return task_id
742
+
743
+ async def run_concurrent_tasks():
744
+ tasks = [task_with_trace(i) for i in range(5)]
745
+ results = await asyncio.gather(*tasks)
746
+ return results
747
+
748
+ # Run async tasks
749
+ results = asyncio.run(run_concurrent_tasks())
750
+ assert results == [0, 1, 2, 3, 4]
751
+
752
+ # All traces should be started and ended
753
+ assert mock_agentops_tracer.start_trace.call_count == 5
754
+ assert mock_core_tracer.end_trace.call_count == 5
755
+
756
+ @patch("agentops.sdk.core.tracer")
757
+ @patch("agentops.tracer")
758
+ def test_context_manager_resource_cleanup_on_exit_failure(self, mock_agentops_tracer, mock_core_tracer):
759
+ """Test that resources are cleaned up even if __exit__ fails"""
760
+ # Create a mock TraceContext
761
+ mock_trace = TraceContext(span=Mock(), token=Mock())
762
+ mock_agentops_tracer.initialized = True
763
+ mock_agentops_tracer.start_trace.return_value = mock_trace
764
+
765
+ # Make end_trace fail
766
+ mock_core_tracer.end_trace.side_effect = Exception("Cleanup failed")
767
+
768
+ # Should not raise exception from __exit__
769
+ with start_trace("cleanup_test") as trace:
770
+ assert trace is mock_trace
771
+
772
+ # end_trace was attempted despite failure
773
+ mock_core_tracer.end_trace.assert_called_once()
774
+
775
+
776
+ if __name__ == "__main__":
777
+ unittest.main()