arize-phoenix 10.5.0__py3-none-any.whl → 10.6.1__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.

Potentially problematic release.


This version of arize-phoenix might be problematic. Click here for more details.

Files changed (20) hide show
  1. {arize_phoenix-10.5.0.dist-info → arize_phoenix-10.6.1.dist-info}/METADATA +3 -3
  2. {arize_phoenix-10.5.0.dist-info → arize_phoenix-10.6.1.dist-info}/RECORD +18 -18
  3. phoenix/server/api/routers/v1/annotations.py +26 -4
  4. phoenix/server/api/routers/v1/spans.py +180 -35
  5. phoenix/server/static/.vite/manifest.json +36 -36
  6. phoenix/server/static/assets/{components-i0MTMvxr.js → components-J06J_j9O.js} +303 -247
  7. phoenix/server/static/assets/{index-BBU9ybeN.js → index-DfT39tc3.js} +22 -8
  8. phoenix/server/static/assets/{pages-Dmq64JIh.js → pages-nxs-tDxQ.js} +389 -406
  9. phoenix/server/static/assets/vendor-B52WHALA.js +911 -0
  10. phoenix/server/static/assets/vendor-arizeai-DGHetzZW.js +642 -0
  11. phoenix/server/static/assets/{vendor-codemirror-c6BZvTq5.js → vendor-codemirror-QIdVJrP_.js} +1 -1
  12. phoenix/server/static/assets/{vendor-recharts-ButLmGII.js → vendor-recharts-GmWamXB4.js} +1 -1
  13. phoenix/server/static/assets/{vendor-shiki-BDqcZGjN.js → vendor-shiki-C8cTrXI5.js} +1 -1
  14. phoenix/version.py +1 -1
  15. phoenix/server/static/assets/vendor-Cm32tkMA.js +0 -905
  16. phoenix/server/static/assets/vendor-arizeai-CB-pngCZ.js +0 -649
  17. {arize_phoenix-10.5.0.dist-info → arize_phoenix-10.6.1.dist-info}/WHEEL +0 -0
  18. {arize_phoenix-10.5.0.dist-info → arize_phoenix-10.6.1.dist-info}/entry_points.txt +0 -0
  19. {arize_phoenix-10.5.0.dist-info → arize_phoenix-10.6.1.dist-info}/licenses/IP_NOTICE +0 -0
  20. {arize_phoenix-10.5.0.dist-info → arize_phoenix-10.6.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arize-phoenix
3
- Version: 10.5.0
3
+ Version: 10.6.1
4
4
  Summary: AI Observability and Evaluation
5
5
  Project-URL: Documentation, https://arize.com/docs/phoenix/
6
6
  Project-URL: Issues, https://github.com/Arize-ai/phoenix/issues
@@ -32,8 +32,8 @@ Requires-Dist: grpcio
32
32
  Requires-Dist: httpx
33
33
  Requires-Dist: jinja2
34
34
  Requires-Dist: numpy!=2.0.0
35
- Requires-Dist: openinference-instrumentation>=0.1.12
36
- Requires-Dist: openinference-semantic-conventions>=0.1.12
35
+ Requires-Dist: openinference-instrumentation>=0.1.32
36
+ Requires-Dist: openinference-semantic-conventions>=0.1.17
37
37
  Requires-Dist: opentelemetry-exporter-otlp
38
38
  Requires-Dist: opentelemetry-proto>=1.12.0
39
39
  Requires-Dist: opentelemetry-sdk
@@ -6,7 +6,7 @@ phoenix/exceptions.py,sha256=n2L2KKuecrdflB9MsCdAYCiSEvGJptIsfRkXMoJle7A,169
6
6
  phoenix/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
7
7
  phoenix/services.py,sha256=ngkyKGVatX3cO2WJdo2hKdaVKP-xJCMvqthvga6kJss,5196
8
8
  phoenix/settings.py,sha256=x87BX7hWGQQZbrW_vrYqFR_izCGfO9gFc--JXUG4Tdk,754
9
- phoenix/version.py,sha256=3-q0XruBmNJiX-F197OpDoO5BK7-xeVvZDRy9Ue9_KU,23
9
+ phoenix/version.py,sha256=hmMNQcyOVrQB5x2nAaJLi4IxHK6TvSgxZcMlUMDDDi8,23
10
10
  phoenix/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  phoenix/core/embedding_dimension.py,sha256=zKGbcvwOXgLf-yrJBpQyKtd-LEOPRKHnUToyAU8Owis,87
12
12
  phoenix/core/model.py,sha256=qBFraOtmwCCnWJltKNP18DDG0mULXigytlFsa6YOz6k,4837
@@ -231,7 +231,7 @@ phoenix/server/api/routers/oauth2.py,sha256=EUoBeh4Ix-Uwt_q_RD75xbMcdVdd0xJLDYjE
231
231
  phoenix/server/api/routers/utils.py,sha256=M41BoH-fl37izhRuN2aX7lWm7jOC20A_3uClv9TVUUY,583
232
232
  phoenix/server/api/routers/v1/__init__.py,sha256=ngLMPjC7lgZxgKy_Is33KxTRnMzSqy25qTTChCVx_Mo,2696
233
233
  phoenix/server/api/routers/v1/annotation_configs.py,sha256=rZ3yJm7m75BVegSjSHqsdqf7n26roGg7vYYiiKfWA3A,15898
234
- phoenix/server/api/routers/v1/annotations.py,sha256=oeafR2tCLu-uIwM9J72gN3MX5WDhrOMU3Jqd1uIiFqg,5921
234
+ phoenix/server/api/routers/v1/annotations.py,sha256=fVl2qeh_ZbWXGvFBTZgeL7aGkkINIScdjuyxnOoSzNM,6817
235
235
  phoenix/server/api/routers/v1/datasets.py,sha256=Xh-M8bnCmjflmPcgv8dAG8Cek88sApuqQlvNBuSnrYc,37534
236
236
  phoenix/server/api/routers/v1/evaluations.py,sha256=GFTo42aIEX0Htn0EjjoE1JZDYlvryeZ_CK9kowhwzGw,12830
237
237
  phoenix/server/api/routers/v1/experiment_evaluations.py,sha256=xSs004jNYsOl3eg-6Zjo2tt9TefTd7WR3twWYrsNQNk,4828
@@ -240,7 +240,7 @@ phoenix/server/api/routers/v1/experiments.py,sha256=V9_sxqLTE1MKGFu9H3FEdGKr70lY
240
240
  phoenix/server/api/routers/v1/models.py,sha256=p3gJN-9SWiUYTUTft4bZMsZVCBNTb4nN1Foy68eRZzQ,1997
241
241
  phoenix/server/api/routers/v1/projects.py,sha256=LFWxHYPRZy-1EvroNylL635vU1UuDbcuRo1oD26yBnw,12551
242
242
  phoenix/server/api/routers/v1/prompts.py,sha256=aBOUBwLDzZDIzJQkxJcR8ZKnakNJOLMwzsLKINSs1mA,26545
243
- phoenix/server/api/routers/v1/spans.py,sha256=qJVN0pVgZM5cMXQoNrCwmFjKDm_7-JHKdt_KU9IDFsA,32121
243
+ phoenix/server/api/routers/v1/spans.py,sha256=MxZ9RvZqc8JGgcDAgH_S9rTc9YfXOEF_j9kqBf3z4qY,38379
244
244
  phoenix/server/api/routers/v1/traces.py,sha256=DfzeszQvtlrVxvurJLaWJJAhkCZ4BodLwpFuBYPwN5Q,8206
245
245
  phoenix/server/api/routers/v1/users.py,sha256=ZcW3if0L8-lUusTzET7fhlhvnmiCICDrGimzB7i3irc,11947
246
246
  phoenix/server/api/routers/v1/utils.py,sha256=oXIOGPzPTkE0ZWUTRCoRIQQ7wTzoSwtWFaUSjlGBqts,4960
@@ -348,16 +348,16 @@ phoenix/server/static/apple-touch-icon-76x76.png,sha256=CT_xT12I0u2i0WU8JzBZBuOQ
348
348
  phoenix/server/static/apple-touch-icon.png,sha256=fOfpjqGpWYbJ0eAurKsyoZP1EAs6ZVooBJ_SGk2ZkDs,3801
349
349
  phoenix/server/static/favicon.ico,sha256=bY0vvCKRftemZfPShwZtE93DiiQdaYaozkPGwNFr6H8,34494
350
350
  phoenix/server/static/modernizr.js,sha256=mvK-XtkNqjOral-QvzoqsyOMECXIMu5BQwSVN_wcU9c,2564
351
- phoenix/server/static/.vite/manifest.json,sha256=x62yRLMKjgJyXFG9yR9TMrcRSfeySlIxNrDjfnO7crM,2165
352
- phoenix/server/static/assets/components-i0MTMvxr.js,sha256=_msrnkJIJukGXFYOv4LnttNGlzsTTy4AyQmRrb3wsqE,548395
353
- phoenix/server/static/assets/index-BBU9ybeN.js,sha256=FoLYEc1jFrb7O-ogLyfSb0OZbAIRpWNf6voOgr_z4bo,60427
354
- phoenix/server/static/assets/pages-Dmq64JIh.js,sha256=ZwWYh_e9U0ieGWJm345TcB15kSGNdXTS47hRepGALVU,1034399
355
- phoenix/server/static/assets/vendor-Cm32tkMA.js,sha256=4iw8vSPqX1wOqwsSuSRzzcKU-e-HEFzg8runFhtH6oA,2743927
351
+ phoenix/server/static/.vite/manifest.json,sha256=E-0Lfjo3ifvH_nTkOfxSUD71-14G-h4J_SPkp45W6i4,2165
352
+ phoenix/server/static/assets/components-J06J_j9O.js,sha256=Fc_oqiCnUDql98GSn9T4Ye1t5NCQHLx1C5lfVMJgH3o,560166
353
+ phoenix/server/static/assets/index-DfT39tc3.js,sha256=ZZMrxzzP1lR_4_LbrTY6AzlU8jmXPaeLx1XU3NpNDMM,61125
354
+ phoenix/server/static/assets/pages-nxs-tDxQ.js,sha256=3eEO9a8WoKg9IsVzOtxigo5dEwjkSoa0_Sb87JGf6HU,1032945
355
+ phoenix/server/static/assets/vendor-B52WHALA.js,sha256=qNJdtbj4rc-YYJc4JF9crIVk9G3iSnPB5NbDhXzS4VM,2731862
356
356
  phoenix/server/static/assets/vendor-WIZid84E.css,sha256=spZD2r7XL5GfLO13ln-IuXfnjAref8l6g_n_AvxxOlI,5517
357
- phoenix/server/static/assets/vendor-arizeai-CB-pngCZ.js,sha256=YkfFgyz52akbta4dpw2Iaj1yXWEnHPrcP-UUQzUYlPA,193248
358
- phoenix/server/static/assets/vendor-codemirror-c6BZvTq5.js,sha256=Ua8ZGPlB0SRj-0MWpGgpfzupTD5qskBqbvjy7SBTVSg,781264
359
- phoenix/server/static/assets/vendor-recharts-ButLmGII.js,sha256=RBCkqUBqEVvSLiW2p18Ea5OqFLXwAmCpHcwz18LGZg8,282150
360
- phoenix/server/static/assets/vendor-shiki-BDqcZGjN.js,sha256=GmGwTFBmAaMrDUqcMjsd4qQlZR7GHpKv0gUXOxHTfbw,8980312
357
+ phoenix/server/static/assets/vendor-arizeai-DGHetzZW.js,sha256=0GqwRIhagIyYxB33qbBQaHZaxLVExlvEXiOTCy7Ybc4,181777
358
+ phoenix/server/static/assets/vendor-codemirror-QIdVJrP_.js,sha256=kMwrQyDFj51M4Wz8PxZUtYKu9jfcxZQaSFVH18zQcKE,781264
359
+ phoenix/server/static/assets/vendor-recharts-GmWamXB4.js,sha256=4Qkqq7rtva-XKz0cYfvNWuArDKJYr41ohfwmcct6D44,282150
360
+ phoenix/server/static/assets/vendor-shiki-C8cTrXI5.js,sha256=Op8ln0V7Vo23uJvqEzcXw9irmK8k2_mQiKLFaXLk8HU,8980312
361
361
  phoenix/server/static/assets/vendor-three-C5WAXd5r.js,sha256=ELkg06u70N7h8oFmvqdoHyPuUf9VgGEWeT4LKFx4VWo,620975
362
362
  phoenix/server/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
363
363
  phoenix/server/templates/index.html,sha256=NpJ83DULqcStXFbShNamX4_NPDtnnucuBxppvUYjJa8,4409
@@ -398,9 +398,9 @@ phoenix/utilities/project.py,sha256=auVpARXkDb-JgeX5f2aStyFIkeKvGwN9l7qrFeJMVxI,
398
398
  phoenix/utilities/re.py,sha256=6YyUWIkv0zc2SigsxfOWIHzdpjKA_TZo2iqKq7zJKvw,2081
399
399
  phoenix/utilities/span_store.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
400
400
  phoenix/utilities/template_formatters.py,sha256=gh9PJD6WEGw7TEYXfSst1UR4pWWwmjxMLrDVQ_CkpkQ,2779
401
- arize_phoenix-10.5.0.dist-info/METADATA,sha256=2kF8SzBBAbVWbNOCdSgvhbe_MKks6ddm3r8eh6ULdsY,27004
402
- arize_phoenix-10.5.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
403
- arize_phoenix-10.5.0.dist-info/entry_points.txt,sha256=Pgpn8Upxx9P8z8joPXZWl2LlnAlGc3gcQoVchb06X1Q,94
404
- arize_phoenix-10.5.0.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
405
- arize_phoenix-10.5.0.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
406
- arize_phoenix-10.5.0.dist-info/RECORD,,
401
+ arize_phoenix-10.6.1.dist-info/METADATA,sha256=bKdR8tOlMRt59PTNBZ6WoNYnNd4oSzSaLW9lsdPkGqg,27004
402
+ arize_phoenix-10.6.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
403
+ arize_phoenix-10.6.1.dist-info/entry_points.txt,sha256=Pgpn8Upxx9P8z8joPXZWl2LlnAlGc3gcQoVchb06X1Q,94
404
+ arize_phoenix-10.6.1.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
405
+ arize_phoenix-10.6.1.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
406
+ arize_phoenix-10.6.1.dist-info/RECORD,,
@@ -62,6 +62,17 @@ async def list_span_annotations(
62
62
  span_ids: list[str] = Query(
63
63
  ..., min_length=1, description="One or more span id to fetch annotations for"
64
64
  ),
65
+ include_annotation_names: Optional[list[str]] = Query(
66
+ default=None,
67
+ description=(
68
+ "Optional list of annotation names to include. If provided, only annotations with "
69
+ "these names will be returned. 'note' annotations are excluded by default unless "
70
+ "explicitly included in this list."
71
+ ),
72
+ ),
73
+ exclude_annotation_names: Optional[list[str]] = Query(
74
+ default=None, description="Optional list of annotation names to exclude from results."
75
+ ),
65
76
  cursor: Optional[str] = Query(default=None, description="A cursor for pagination"),
66
77
  limit: int = Query(
67
78
  default=10,
@@ -84,15 +95,26 @@ async def list_span_annotations(
84
95
  status_code=HTTP_404_NOT_FOUND,
85
96
  detail=f"Project with identifier {project_identifier} not found",
86
97
  )
98
+
99
+ # Build the base query
100
+ where_conditions = [
101
+ models.Project.id == project.id,
102
+ models.Span.span_id.in_(span_ids),
103
+ ]
104
+
105
+ # Add annotation name filtering
106
+ if include_annotation_names:
107
+ where_conditions.append(models.SpanAnnotation.name.in_(include_annotation_names))
108
+
109
+ if exclude_annotation_names:
110
+ where_conditions.append(models.SpanAnnotation.name.not_in(exclude_annotation_names))
111
+
87
112
  stmt = (
88
113
  select(models.Span.span_id, models.SpanAnnotation)
89
114
  .join(models.Trace, models.Span.trace_rowid == models.Trace.id)
90
115
  .join(models.Project, models.Trace.project_rowid == models.Project.id)
91
116
  .join(models.SpanAnnotation, models.SpanAnnotation.span_rowid == models.Span.id)
92
- .where(
93
- models.Project.id == project.id,
94
- models.Span.span_id.in_(span_ids),
95
- )
117
+ .where(*where_conditions)
96
118
  .order_by(models.SpanAnnotation.id.desc())
97
119
  .limit(limit + 1)
98
120
  )
@@ -370,12 +370,48 @@ class OtlpSpan(BaseModel):
370
370
  )
371
371
 
372
372
 
373
- class SpanSearchResponseBody(PaginatedResponseBody[OtlpSpan]):
373
+ class OtlpSpansResponseBody(PaginatedResponseBody[OtlpSpan]):
374
374
  """Paginated response where each span follows OTLP JSON structure."""
375
375
 
376
376
  pass
377
377
 
378
378
 
379
+ ################################################################################
380
+ # Phoenix Span Models
381
+ ################################################################################
382
+
383
+
384
+ class SpanContext(V1RoutesBaseModel):
385
+ trace_id: str = Field(description="OpenTelemetry trace ID")
386
+ span_id: str = Field(description="OpenTelemetry span ID")
387
+
388
+
389
+ class SpanEvent(V1RoutesBaseModel):
390
+ name: str = Field(description="Name of the event")
391
+ timestamp: datetime = Field(description="When the event occurred")
392
+ attributes: dict[str, Any] = Field(default_factory=dict, description="Event attributes")
393
+
394
+
395
+ class Span(V1RoutesBaseModel):
396
+ id: str = Field(description="Span Global ID, distinct from the OpenTelemetry span ID")
397
+ name: str = Field(description="Name of the span operation")
398
+ context: SpanContext = Field(description="Span context containing trace_id and span_id")
399
+ span_kind: str = Field(description="Type of work that the span encapsulates")
400
+ parent_id: Optional[str] = Field(
401
+ default=None, description="OpenTelemetry span ID of the parent span"
402
+ )
403
+ start_time: datetime = Field(description="Start time of the span")
404
+ end_time: datetime = Field(description="End time of the span")
405
+ status_code: str = Field(description="Status code of the span")
406
+ status_message: str = Field(default="", description="Status message")
407
+ attributes: dict[str, Any] = Field(default_factory=dict, description="Span attributes")
408
+ events: list[SpanEvent] = Field(default_factory=list, description="Span events")
409
+
410
+
411
+ class SpansResponseBody(PaginatedResponseBody[Span]):
412
+ pass
413
+
414
+
379
415
  # TODO: Add property details to SpanQuery schema
380
416
  @router.post(
381
417
  "/spans",
@@ -513,11 +549,11 @@ def _to_any_value(value: Any) -> OtlpAnyValue:
513
549
  "/projects/{project_identifier}/spans/otlpv1",
514
550
  operation_id="spanSearch",
515
551
  summary="Search spans with simple filters (no DSL)",
516
- description="Return spans within a project filtered by time range, annotation names, "
517
- "and ordered by start_time. Supports cursor-based pagination.",
552
+ description="Return spans within a project filtered by time range. "
553
+ "Supports cursor-based pagination.",
518
554
  responses=add_errors_to_responses([HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_ENTITY]),
519
555
  )
520
- async def span_search(
556
+ async def span_search_otlpv1(
521
557
  request: Request,
522
558
  project_identifier: str = Path(
523
559
  description=(
@@ -525,30 +561,18 @@ async def span_search(
525
561
  "it cannot contain slash (/), question mark (?), or pound sign (#) characters."
526
562
  )
527
563
  ),
528
- cursor: Optional[str] = Query(default=None, description="Pagination cursor (GlobalID of Span)"),
564
+ cursor: Optional[str] = Query(default=None, description="Pagination cursor (Span Global ID)"),
529
565
  limit: int = Query(default=100, gt=0, le=1000, description="Maximum number of spans to return"),
530
- sort_direction: Literal["asc", "desc"] = Query(
531
- default="desc",
532
- description="Sort direction for the sort field",
533
- ),
534
566
  start_time: Optional[datetime] = Query(default=None, description="Inclusive lower bound time"),
535
567
  end_time: Optional[datetime] = Query(default=None, description="Exclusive upper bound time"),
536
- annotation_names: Optional[list[str]] = Query(
537
- default=None,
538
- description=(
539
- "If provided, only include spans that have at least one annotation with one "
540
- "of these names."
541
- ),
542
- alias="annotationNames",
543
- ),
544
- ) -> SpanSearchResponseBody:
568
+ ) -> OtlpSpansResponseBody:
545
569
  """Search spans with minimal filters instead of the old SpanQuery DSL."""
546
570
 
547
571
  async with request.app.state.db() as session:
548
572
  project = await _get_project_by_identifier(session, project_identifier)
549
573
 
550
574
  project_id: int = project.id
551
- order_by = [models.Span.id.asc() if sort_direction == "asc" else models.Span.id.desc()]
575
+ order_by = [models.Span.id.desc()]
552
576
 
553
577
  stmt = (
554
578
  select(
@@ -565,23 +589,10 @@ async def span_search(
565
589
  if end_time:
566
590
  stmt = stmt.where(models.Span.start_time < normalize_datetime(end_time, timezone.utc))
567
591
 
568
- if annotation_names:
569
- stmt = (
570
- stmt.join(
571
- models.SpanAnnotation,
572
- onclause=models.SpanAnnotation.span_rowid == models.Span.id,
573
- )
574
- .where(models.SpanAnnotation.name.in_(annotation_names))
575
- .group_by(models.Span.id, models.Trace.trace_id)
576
- )
577
-
578
592
  if cursor:
579
593
  try:
580
594
  cursor_rowid = int(GlobalID.from_id(cursor).node_id)
581
- if sort_direction == "asc":
582
- stmt = stmt.where(models.Span.id >= cursor_rowid)
583
- else:
584
- stmt = stmt.where(models.Span.id <= cursor_rowid)
595
+ stmt = stmt.where(models.Span.id <= cursor_rowid)
585
596
  except Exception:
586
597
  raise HTTPException(status_code=HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid cursor")
587
598
 
@@ -591,7 +602,7 @@ async def span_search(
591
602
  rows: list[tuple[models.Span, str]] = [r async for r in await session.stream(stmt)]
592
603
 
593
604
  if not rows:
594
- return SpanSearchResponseBody(next_cursor=None, data=[])
605
+ return OtlpSpansResponseBody(next_cursor=None, data=[])
595
606
 
596
607
  next_cursor: Optional[str] = None
597
608
  if len(rows) == limit + 1:
@@ -668,7 +679,141 @@ async def span_search(
668
679
  )
669
680
  )
670
681
 
671
- return SpanSearchResponseBody(next_cursor=next_cursor, data=result_spans)
682
+ return OtlpSpansResponseBody(next_cursor=next_cursor, data=result_spans)
683
+
684
+
685
+ @router.get(
686
+ "/projects/{project_identifier}/spans",
687
+ operation_id="getSpans",
688
+ summary="List spans with simple filters (no DSL)",
689
+ description="Return spans within a project filtered by time range. "
690
+ "Supports cursor-based pagination.",
691
+ responses=add_errors_to_responses([HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_ENTITY]),
692
+ )
693
+ async def span_search(
694
+ request: Request,
695
+ project_identifier: str = Path(
696
+ description=(
697
+ "The project identifier: either project ID or project name. If using a project name, "
698
+ "it cannot contain slash (/), question mark (?), or pound sign (#) characters."
699
+ )
700
+ ),
701
+ cursor: Optional[str] = Query(default=None, description="Pagination cursor (Span Global ID)"),
702
+ limit: int = Query(default=100, gt=0, le=1000, description="Maximum number of spans to return"),
703
+ start_time: Optional[datetime] = Query(default=None, description="Inclusive lower bound time"),
704
+ end_time: Optional[datetime] = Query(default=None, description="Exclusive upper bound time"),
705
+ ) -> SpansResponseBody:
706
+ async with request.app.state.db() as session:
707
+ project = await _get_project_by_identifier(session, project_identifier)
708
+
709
+ project_id: int = project.id
710
+ order_by = [models.Span.id.desc()]
711
+
712
+ stmt = (
713
+ select(
714
+ models.Span,
715
+ models.Trace.trace_id,
716
+ )
717
+ .join(models.Trace, onclause=models.Trace.id == models.Span.trace_rowid)
718
+ .join(models.Project, onclause=models.Project.id == project_id)
719
+ .order_by(*order_by)
720
+ )
721
+
722
+ if start_time:
723
+ stmt = stmt.where(models.Span.start_time >= normalize_datetime(start_time, timezone.utc))
724
+ if end_time:
725
+ stmt = stmt.where(models.Span.start_time < normalize_datetime(end_time, timezone.utc))
726
+
727
+ if cursor:
728
+ try:
729
+ cursor_rowid = int(GlobalID.from_id(cursor).node_id)
730
+ except Exception:
731
+ raise HTTPException(status_code=HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid cursor")
732
+ stmt = stmt.where(models.Span.id <= cursor_rowid)
733
+
734
+ stmt = stmt.limit(limit + 1)
735
+
736
+ async with request.app.state.db() as session:
737
+ rows: list[tuple[models.Span, str]] = [r async for r in await session.stream(stmt)]
738
+
739
+ if not rows:
740
+ return SpansResponseBody(next_cursor=None, data=[])
741
+
742
+ next_cursor: Optional[str] = None
743
+ if len(rows) == limit + 1:
744
+ *rows, extra = rows # extra is first item of next page
745
+ span_extra, _ = extra
746
+ next_cursor = str(GlobalID("Span", str(span_extra.id)))
747
+
748
+ # Convert ORM rows -> Phoenix spans
749
+ result_spans: list[Span] = []
750
+ for span_orm, trace_id in rows:
751
+ # Convert events to Phoenix Event list
752
+ events: list[SpanEvent] = []
753
+ for event in span_orm.events:
754
+ event_time = event.get("timestamp")
755
+ parsed_time = None
756
+
757
+ if event_time:
758
+ if isinstance(event_time, datetime):
759
+ parsed_time = normalize_datetime(event_time, timezone.utc)
760
+ elif isinstance(event_time, str):
761
+ try:
762
+ naive_time = datetime.fromisoformat(event_time)
763
+ parsed_time = normalize_datetime(naive_time, timezone.utc)
764
+ except ValueError:
765
+ # If ISO format fails, try to parse as timestamp
766
+ try:
767
+ parsed_time = datetime.fromtimestamp(float(event_time), tz=timezone.utc)
768
+ except (ValueError, TypeError):
769
+ parsed_time = datetime.now(timezone.utc) # fallback
770
+ elif isinstance(event_time, (int, float)):
771
+ try:
772
+ # Assume nanoseconds if very large, otherwise seconds
773
+ if event_time > 1e12: # nanoseconds
774
+ parsed_time = datetime.fromtimestamp(
775
+ event_time / 1_000_000_000, tz=timezone.utc
776
+ )
777
+ else: # seconds
778
+ parsed_time = datetime.fromtimestamp(event_time, tz=timezone.utc)
779
+ except (ValueError, OSError):
780
+ parsed_time = datetime.now(timezone.utc) # fallback
781
+ else:
782
+ parsed_time = datetime.now(timezone.utc) # fallback
783
+
784
+ events.append(
785
+ SpanEvent(
786
+ name=event.get("name", ""),
787
+ timestamp=parsed_time,
788
+ attributes=event.get("attributes", {}),
789
+ )
790
+ )
791
+
792
+ attributes = {
793
+ k: v for k, v in flatten(span_orm.attributes or dict(), recurse_on_sequence=True)
794
+ }
795
+ openinference_span_kind = attributes.pop("openinference.span.kind", "UNKNOWN")
796
+
797
+ result_spans.append(
798
+ Span(
799
+ id=str(GlobalID("Span", str(span_orm.id))),
800
+ name=span_orm.name or "",
801
+ context=SpanContext(
802
+ trace_id=trace_id,
803
+ span_id=span_orm.span_id or "",
804
+ ),
805
+ span_kind=openinference_span_kind,
806
+ parent_id=span_orm.parent_id,
807
+ start_time=span_orm.start_time,
808
+ end_time=span_orm.end_time,
809
+ status_code=span_orm.status_code,
810
+ status_message=span_orm.status_message or "",
811
+ attributes=attributes,
812
+ events=events,
813
+ )
814
+ )
815
+
816
+ return SpansResponseBody(next_cursor=next_cursor, data=result_spans)
672
817
 
673
818
 
674
819
  @router.get("/spans", include_in_schema=False, deprecated=True)
@@ -1,28 +1,28 @@
1
1
  {
2
- "_components-i0MTMvxr.js": {
3
- "file": "assets/components-i0MTMvxr.js",
2
+ "_components-J06J_j9O.js": {
3
+ "file": "assets/components-J06J_j9O.js",
4
4
  "name": "components",
5
5
  "imports": [
6
- "_vendor-Cm32tkMA.js",
7
- "_pages-Dmq64JIh.js",
8
- "_vendor-arizeai-CB-pngCZ.js",
9
- "_vendor-codemirror-c6BZvTq5.js",
6
+ "_vendor-B52WHALA.js",
7
+ "_pages-nxs-tDxQ.js",
8
+ "_vendor-arizeai-DGHetzZW.js",
9
+ "_vendor-codemirror-QIdVJrP_.js",
10
10
  "_vendor-three-C5WAXd5r.js"
11
11
  ]
12
12
  },
13
- "_pages-Dmq64JIh.js": {
14
- "file": "assets/pages-Dmq64JIh.js",
13
+ "_pages-nxs-tDxQ.js": {
14
+ "file": "assets/pages-nxs-tDxQ.js",
15
15
  "name": "pages",
16
16
  "imports": [
17
- "_vendor-Cm32tkMA.js",
18
- "_vendor-arizeai-CB-pngCZ.js",
19
- "_components-i0MTMvxr.js",
20
- "_vendor-codemirror-c6BZvTq5.js",
21
- "_vendor-recharts-ButLmGII.js"
17
+ "_vendor-B52WHALA.js",
18
+ "_vendor-arizeai-DGHetzZW.js",
19
+ "_components-J06J_j9O.js",
20
+ "_vendor-codemirror-QIdVJrP_.js",
21
+ "_vendor-recharts-GmWamXB4.js"
22
22
  ]
23
23
  },
24
- "_vendor-Cm32tkMA.js": {
25
- "file": "assets/vendor-Cm32tkMA.js",
24
+ "_vendor-B52WHALA.js": {
25
+ "file": "assets/vendor-B52WHALA.js",
26
26
  "name": "vendor",
27
27
  "imports": [
28
28
  "_vendor-three-C5WAXd5r.js"
@@ -35,33 +35,33 @@
35
35
  "file": "assets/vendor-WIZid84E.css",
36
36
  "src": "_vendor-WIZid84E.css"
37
37
  },
38
- "_vendor-arizeai-CB-pngCZ.js": {
39
- "file": "assets/vendor-arizeai-CB-pngCZ.js",
38
+ "_vendor-arizeai-DGHetzZW.js": {
39
+ "file": "assets/vendor-arizeai-DGHetzZW.js",
40
40
  "name": "vendor-arizeai",
41
41
  "imports": [
42
- "_vendor-Cm32tkMA.js"
42
+ "_vendor-B52WHALA.js"
43
43
  ]
44
44
  },
45
- "_vendor-codemirror-c6BZvTq5.js": {
46
- "file": "assets/vendor-codemirror-c6BZvTq5.js",
45
+ "_vendor-codemirror-QIdVJrP_.js": {
46
+ "file": "assets/vendor-codemirror-QIdVJrP_.js",
47
47
  "name": "vendor-codemirror",
48
48
  "imports": [
49
- "_vendor-Cm32tkMA.js",
50
- "_vendor-shiki-BDqcZGjN.js"
49
+ "_vendor-B52WHALA.js",
50
+ "_vendor-shiki-C8cTrXI5.js"
51
51
  ]
52
52
  },
53
- "_vendor-recharts-ButLmGII.js": {
54
- "file": "assets/vendor-recharts-ButLmGII.js",
53
+ "_vendor-recharts-GmWamXB4.js": {
54
+ "file": "assets/vendor-recharts-GmWamXB4.js",
55
55
  "name": "vendor-recharts",
56
56
  "imports": [
57
- "_vendor-Cm32tkMA.js"
57
+ "_vendor-B52WHALA.js"
58
58
  ]
59
59
  },
60
- "_vendor-shiki-BDqcZGjN.js": {
61
- "file": "assets/vendor-shiki-BDqcZGjN.js",
60
+ "_vendor-shiki-C8cTrXI5.js": {
61
+ "file": "assets/vendor-shiki-C8cTrXI5.js",
62
62
  "name": "vendor-shiki",
63
63
  "imports": [
64
- "_vendor-Cm32tkMA.js"
64
+ "_vendor-B52WHALA.js"
65
65
  ]
66
66
  },
67
67
  "_vendor-three-C5WAXd5r.js": {
@@ -69,19 +69,19 @@
69
69
  "name": "vendor-three"
70
70
  },
71
71
  "index.tsx": {
72
- "file": "assets/index-BBU9ybeN.js",
72
+ "file": "assets/index-DfT39tc3.js",
73
73
  "name": "index",
74
74
  "src": "index.tsx",
75
75
  "isEntry": true,
76
76
  "imports": [
77
- "_vendor-Cm32tkMA.js",
78
- "_vendor-arizeai-CB-pngCZ.js",
79
- "_pages-Dmq64JIh.js",
80
- "_components-i0MTMvxr.js",
77
+ "_vendor-B52WHALA.js",
78
+ "_vendor-arizeai-DGHetzZW.js",
79
+ "_pages-nxs-tDxQ.js",
80
+ "_components-J06J_j9O.js",
81
81
  "_vendor-three-C5WAXd5r.js",
82
- "_vendor-codemirror-c6BZvTq5.js",
83
- "_vendor-shiki-BDqcZGjN.js",
84
- "_vendor-recharts-ButLmGII.js"
82
+ "_vendor-codemirror-QIdVJrP_.js",
83
+ "_vendor-shiki-C8cTrXI5.js",
84
+ "_vendor-recharts-GmWamXB4.js"
85
85
  ]
86
86
  }
87
87
  }