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

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arize-phoenix
3
- Version: 8.19.1
3
+ Version: 8.21.0
4
4
  Summary: AI Observability and Evaluation
5
5
  Project-URL: Documentation, https://docs.arize.com/phoenix/
6
6
  Project-URL: Issues, https://github.com/Arize-ai/phoenix/issues
@@ -71,7 +71,7 @@ Requires-Dist: opentelemetry-sdk; extra == 'container'
71
71
  Requires-Dist: opentelemetry-semantic-conventions; extra == 'container'
72
72
  Requires-Dist: prometheus-client; extra == 'container'
73
73
  Requires-Dist: py-grpc-prometheus; extra == 'container'
74
- Requires-Dist: strawberry-graphql[opentelemetry]==0.262.0; extra == 'container'
74
+ Requires-Dist: strawberry-graphql[opentelemetry]==0.262.5; extra == 'container'
75
75
  Requires-Dist: umap-learn; extra == 'container'
76
76
  Requires-Dist: uvloop; (platform_system != 'Windows') and extra == 'container'
77
77
  Provides-Extra: dev
@@ -102,7 +102,7 @@ Requires-Dist: pytest-postgresql; extra == 'dev'
102
102
  Requires-Dist: pytest-xdist; extra == 'dev'
103
103
  Requires-Dist: pytest==8.3.3; extra == 'dev'
104
104
  Requires-Dist: ruff==0.6.9; extra == 'dev'
105
- Requires-Dist: strawberry-graphql[debug-server,opentelemetry]==0.262.0; extra == 'dev'
105
+ Requires-Dist: strawberry-graphql[debug-server,opentelemetry]==0.262.5; extra == 'dev'
106
106
  Requires-Dist: tabulate; extra == 'dev'
107
107
  Requires-Dist: tox-uv==1.11.3; extra == 'dev'
108
108
  Requires-Dist: tox==4.18.1; extra == 'dev'
@@ -1,12 +1,12 @@
1
1
  phoenix/__init__.py,sha256=X3eUEwd2rG8KKWWYVNNDJoqo08ihfjgHhlP29dcdNJE,5481
2
2
  phoenix/auth.py,sha256=VVMHrWN31tln3Zo4z6ofecrV4daiqJjLd8r85mqlxek,10939
3
- phoenix/config.py,sha256=j5oTPHMcr73DnPpnhl3induhAGwxLsf2BX6DF0YXUOY,32408
3
+ phoenix/config.py,sha256=dSUZUhHVkfMP5yNs_fyvM96ZcZvY7KlyrDKkVflS9aM,35855
4
4
  phoenix/datetime_utils.py,sha256=iJzNG6YJ6V7_u8B2iA7P2Z26FyxYbOPtx0dhJ7kNDHA,3398
5
5
  phoenix/exceptions.py,sha256=n2L2KKuecrdflB9MsCdAYCiSEvGJptIsfRkXMoJle7A,169
6
6
  phoenix/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
7
7
  phoenix/services.py,sha256=kpW1WL0kiB8XJsO6XycvZVJ-lBkNoenhQ7atCvBoSe8,5365
8
8
  phoenix/settings.py,sha256=x87BX7hWGQQZbrW_vrYqFR_izCGfO9gFc--JXUG4Tdk,754
9
- phoenix/version.py,sha256=EdcJbWKzyoFhfVdECuY4DGAr_RdK6ZEbxDWTeRBVI3s,23
9
+ phoenix/version.py,sha256=IcFew5OfUbzsX2QRcX00Ngf0cInzeuF_IhyKYRXmTmI,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
@@ -267,7 +267,7 @@ phoenix/server/api/types/MimeType.py,sha256=Zpi6zCalkSFgsvhzvOs-O1gYA04usAi9H__Q
267
267
  phoenix/server/api/types/Model.py,sha256=8UIFqMe1q-2ufBNg-gxHusV8wM1h-KbfLUeJjyVcMvQ,8081
268
268
  phoenix/server/api/types/NumericRange.py,sha256=afEjgF97Go_OvmjMggbPBt-zGM8IONewAyEiKEHRds0,192
269
269
  phoenix/server/api/types/PerformanceMetric.py,sha256=KFkmJDqP43eDUtARQOUqR7NYcxvL6Vh2uisHWU6H3ko,387
270
- phoenix/server/api/types/Project.py,sha256=Rtggx5_X9SfVeepxjRt8Qfy0H2EY26U5kUQq_geuZNI,20573
270
+ phoenix/server/api/types/Project.py,sha256=d9bEG5nejolqFmgpNF-LtXJK8kFxVo_5qmHRH3YAf-8,20639
271
271
  phoenix/server/api/types/ProjectSession.py,sha256=fyfVtpUpFOTnBx8DFnH3dr7WXAssN0ooAgrQSSi7kEI,4677
272
272
  phoenix/server/api/types/Prompt.py,sha256=ccP4eq1e38xbF0afclGWLOuDpBVpNbJ3AOSRClF8yFQ,4955
273
273
  phoenix/server/api/types/PromptLabel.py,sha256=g3IDSPYRZwb0qpMAk93R6J96jgYULUYGOciTnpeh3sI,1321
@@ -314,16 +314,16 @@ phoenix/server/static/apple-touch-icon-76x76.png,sha256=CT_xT12I0u2i0WU8JzBZBuOQ
314
314
  phoenix/server/static/apple-touch-icon.png,sha256=fOfpjqGpWYbJ0eAurKsyoZP1EAs6ZVooBJ_SGk2ZkDs,3801
315
315
  phoenix/server/static/favicon.ico,sha256=bY0vvCKRftemZfPShwZtE93DiiQdaYaozkPGwNFr6H8,34494
316
316
  phoenix/server/static/modernizr.js,sha256=mvK-XtkNqjOral-QvzoqsyOMECXIMu5BQwSVN_wcU9c,2564
317
- phoenix/server/static/.vite/manifest.json,sha256=86sNJXO2BgT6lbAV6gur9ttIsslZFkrasYN8_exwTYI,2165
318
- phoenix/server/static/assets/components-C28SkolB.js,sha256=8vbczseUKgr-98J4CHl508XrdK4faQ_q56DNwOQgNrk,451442
319
- phoenix/server/static/assets/index-WrtAISug.js,sha256=fQf4Q1uBcWK7p8J6CSCSZpxBCYgKpy92KRrmJt1zx2E,60234
320
- phoenix/server/static/assets/pages-eLqonMjF.js,sha256=_bwL6q4bhIw8WNg2nb9Fw7NZgjF5l3W9zksMpfxad5s,861335
317
+ phoenix/server/static/.vite/manifest.json,sha256=aDi-Uk9t65RmDnEk-9GWEFulGc6ogxP_FMVJrkIPZvI,2165
318
+ phoenix/server/static/assets/components-BAc4OPED.js,sha256=VL_AageEdDLGbyR_LF0v8sfcbYtLllsbqrOflzE2OLo,455490
319
+ phoenix/server/static/assets/index-Du53xkjY.js,sha256=kqiASYQH1J0Zg5c2Jz5XL4UhDOW8pqhhAfJITONFot8,60397
320
+ phoenix/server/static/assets/pages-Dz-gbBPF.js,sha256=gBI9zPk6YJhSia8YDopK6JgtsxwqVeheWpLY76N43gw,862644
321
+ phoenix/server/static/assets/vendor-CEisxXSv.js,sha256=TS8_RMXjkexQB-ENo06BZS77xnf8TMf68k03NhEZyVE,2510162
321
322
  phoenix/server/static/assets/vendor-Cg6lcjUC.css,sha256=nZrkr0u6NNElFGvpWHk9GTHeGoibCXCli1bE7mXZGZg,1816
322
- phoenix/server/static/assets/vendor-CwC1K6mo.js,sha256=gbXr7JyB3INCl8tooZjOWgh17pPzJccRJerVxaq9wsw,2503909
323
- phoenix/server/static/assets/vendor-arizeai-B7-2AIAM.js,sha256=FprcglI19nrEQstQKTkSMRdM3asUu39eOAsSED6jh7k,193248
324
- phoenix/server/static/assets/vendor-codemirror-D3xhkqGE.js,sha256=dRCy3Yh4HSQ7ipu9hdWwHebeU6zK58LLUDiebC4U98A,781264
325
- phoenix/server/static/assets/vendor-recharts-B_VgO7bY.js,sha256=PyctbCEswAkidvKG9rrcHexOWhn8jW7U66np1L92ZcE,282109
326
- phoenix/server/static/assets/vendor-shiki-CK18CSqG.js,sha256=jEit22_c1RfyEMpL4lSaxSFdAo14sDGi6OluXw8dzsA,8980312
323
+ phoenix/server/static/assets/vendor-arizeai-BCTsSnvS.js,sha256=bOMRhNcnPkdTP12LEkBKJmvRBJwA5BKMxZvpkAX_irg,193248
324
+ phoenix/server/static/assets/vendor-codemirror-DIWnRs_7.js,sha256=JkqAk2m_o589elkpaXuwlOaq0lfDrLK09-6D9eBBcKQ,781264
325
+ phoenix/server/static/assets/vendor-recharts-Bame54mG.js,sha256=fhgG0ZqpDuiVwh71xq7Xb9vHxjnhxHOhp179cTnNwpM,282109
326
+ phoenix/server/static/assets/vendor-shiki-Cc73E4D-.js,sha256=YEuUQiR8772y7nz6Eu1rdhl5evg5XJkRGxuwKcxCNqk,8980312
327
327
  phoenix/server/static/assets/vendor-three-C5WAXd5r.js,sha256=ELkg06u70N7h8oFmvqdoHyPuUf9VgGEWeT4LKFx4VWo,620975
328
328
  phoenix/server/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
329
329
  phoenix/server/templates/index.html,sha256=e8_jdi7Eo19SK7DI_gglkTW094D17E0VAegoMmmmvIc,4330
@@ -331,7 +331,7 @@ phoenix/session/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
331
331
  phoenix/session/client.py,sha256=nXSn2Zmf9wTxgSe11Y9kKSLClkG8pNEAJgfEmy4mPm8,35234
332
332
  phoenix/session/data_extractor.py,sha256=Y0RzYFaNy9fQj8PEIeQ76TBZ90_E1FW7bXu3K5x0EZY,2782
333
333
  phoenix/session/evaluation.py,sha256=Q3fOMNELvqkk-b6a6PKc8pDJdsNQ0ZbTpseUSA2NKqs,5300
334
- phoenix/session/session.py,sha256=nD0v1VWQVOnKpviK2RjjFV3YLcNdrDkrclsK64Y3H6Y,27540
334
+ phoenix/session/session.py,sha256=acCqJlPeczUDQcowXGvb8vcR1GcWqOxAn7oRBK3IL7U,27560
335
335
  phoenix/trace/__init__.py,sha256=ujk_uYjM8gmm-YqnyXxF-kekfwid0bcaPMTtNNcaw6U,407
336
336
  phoenix/trace/attributes.py,sha256=hyEKYZWPCP4NRmW7VmiC2voa3TH7FYKUBR9DYiVfXlw,12627
337
337
  phoenix/trace/errors.py,sha256=wB1z8qdPckngdfU-TORToekvg3344oNFAA83_hC2yFY,180
@@ -364,9 +364,9 @@ phoenix/utilities/project.py,sha256=auVpARXkDb-JgeX5f2aStyFIkeKvGwN9l7qrFeJMVxI,
364
364
  phoenix/utilities/re.py,sha256=6YyUWIkv0zc2SigsxfOWIHzdpjKA_TZo2iqKq7zJKvw,2081
365
365
  phoenix/utilities/span_store.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
366
366
  phoenix/utilities/template_formatters.py,sha256=gh9PJD6WEGw7TEYXfSst1UR4pWWwmjxMLrDVQ_CkpkQ,2779
367
- arize_phoenix-8.19.1.dist-info/METADATA,sha256=uE6TpM6rcJFGmcava63Vnw2Q3lCjpJtpC2QU5dp45Bg,21378
368
- arize_phoenix-8.19.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
369
- arize_phoenix-8.19.1.dist-info/entry_points.txt,sha256=Pgpn8Upxx9P8z8joPXZWl2LlnAlGc3gcQoVchb06X1Q,94
370
- arize_phoenix-8.19.1.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
371
- arize_phoenix-8.19.1.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
372
- arize_phoenix-8.19.1.dist-info/RECORD,,
367
+ arize_phoenix-8.21.0.dist-info/METADATA,sha256=0DkKUK_8BHpdYpiMIfmCLhA8bRmpQeHmmh0PgqoMmUU,21378
368
+ arize_phoenix-8.21.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
369
+ arize_phoenix-8.21.0.dist-info/entry_points.txt,sha256=Pgpn8Upxx9P8z8joPXZWl2LlnAlGc3gcQoVchb06X1Q,94
370
+ arize_phoenix-8.21.0.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
371
+ arize_phoenix-8.21.0.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
372
+ arize_phoenix-8.21.0.dist-info/RECORD,,
phoenix/config.py CHANGED
@@ -7,9 +7,10 @@ from datetime import timedelta
7
7
  from enum import Enum
8
8
  from importlib.metadata import version
9
9
  from pathlib import Path
10
- from typing import Optional, cast, overload
10
+ from typing import Any, Optional, Union, cast, overload
11
11
  from urllib.parse import quote_plus, urlparse
12
12
 
13
+ import wrapt
13
14
  from email_validator import EmailNotValidError, validate_email
14
15
 
15
16
  from phoenix.utilities.logging import log_a_list
@@ -144,7 +145,7 @@ ENV_PHOENIX_DISABLE_RATE_LIMIT = "PHOENIX_DISABLE_RATE_LIMIT"
144
145
  ENV_PHOENIX_SECRET = "PHOENIX_SECRET"
145
146
  ENV_PHOENIX_DEFAULT_ADMIN_INITIAL_PASSWORD = "PHOENIX_DEFAULT_ADMIN_INITIAL_PASSWORD"
146
147
  """
147
- The initial password for the default admin account, which defaults to admin if not
148
+ The initial password for the default admin account, which defaults to 'admin' if not
148
149
  explicitly set. Note that changing this value will have no effect if the default admin
149
150
  record already exists in the database. In such cases, the default admin password must
150
151
  be updated manually in the application.
@@ -615,17 +616,138 @@ GENERATED_INFERENCES_NAME_PREFIX = "phoenix_inferences_"
615
616
  WORKING_DIR = get_working_dir()
616
617
  """The work directory for saving, loading, and exporting data."""
617
618
 
618
- ROOT_DIR = WORKING_DIR
619
- EXPORT_DIR = ROOT_DIR / "exports"
620
- INFERENCES_DIR = ROOT_DIR / "inferences"
621
- TRACE_DATASETS_DIR = ROOT_DIR / "trace_datasets"
622
619
 
620
+ class DirectoryError(Exception):
621
+ def __init__(self, message: Optional[str] = None) -> None:
622
+ if message is None:
623
+ message = (
624
+ "Local storage is not configured. Please set the "
625
+ "PHOENIX_WORKING_DIR environment variable to fix this."
626
+ )
627
+ super().__init__(message)
628
+
629
+
630
+ def get_env_postgres_connection_str() -> Optional[str]:
631
+ pg_user = os.getenv(ENV_PHOENIX_POSTGRES_USER)
632
+ pg_password = os.getenv(ENV_PHOENIX_POSTGRES_PASSWORD)
633
+ pg_host = os.getenv(ENV_PHOENIX_POSTGRES_HOST)
634
+ pg_port = os.getenv(ENV_PHOENIX_POSTGRES_PORT)
635
+ pg_db = os.getenv(ENV_PHOENIX_POSTGRES_DB)
636
+
637
+ if pg_host and ":" in pg_host:
638
+ pg_host, parsed_port = pg_host.split(":")
639
+ pg_port = pg_port or parsed_port # use the explicitly set port if provided
640
+
641
+ if pg_host and pg_user and pg_password:
642
+ encoded_password = quote_plus(pg_password)
643
+ connection_str = f"postgresql://{pg_user}:{encoded_password}@{pg_host}"
644
+ if pg_port:
645
+ connection_str = f"{connection_str}:{pg_port}"
646
+ if pg_db:
647
+ connection_str = f"{connection_str}/{pg_db}"
648
+
649
+ return connection_str
650
+ return None
651
+
652
+
653
+ def _no_local_storage() -> bool:
654
+ """
655
+ Check if we're using a postgres database by checking if postgres connection string is set
656
+ and a working directory was not explicitly set.
657
+ """
658
+ return get_env_postgres_connection_str() is not None and getenv(ENV_PHOENIX_WORKING_DIR) is None
659
+
660
+
661
+ class RestrictedPath(wrapt.ObjectProxy): # type: ignore[misc]
662
+ """
663
+ This wraps pathlib.Path and will raise a DirectoryError if no local storage is configured.
664
+
665
+ Users can forego configuring a working directory if they are using a postgres database. If this
666
+ condition is met, the working directory path wrapped by this object will raise an error when
667
+ accessed in any way.
668
+ """
669
+
670
+ def __init__(self, wrapped: Union[str, Path]) -> None:
671
+ super().__init__(Path(wrapped))
672
+ self.__wrapped__: Path
673
+
674
+ def _check_forbidden(self) -> None:
675
+ if _no_local_storage():
676
+ raise DirectoryError()
677
+ return
678
+
679
+ def __getattr__(self, name: str) -> Any:
680
+ attr = getattr(self.__wrapped__, name)
681
+
682
+ if callable(attr):
683
+
684
+ def wrapped_attr(*args: Any, **kwargs: Any) -> Any:
685
+ result = attr(*args, **kwargs)
686
+ if isinstance(result, Path):
687
+ self._check_forbidden()
688
+ return RestrictedPath(result)
689
+ elif hasattr(result, "__iter__") and not isinstance(result, (str, bytes)):
690
+ return (RestrictedPath(p) if isinstance(p, Path) else p for p in result)
691
+ return result
623
692
 
624
- def ensure_working_dir() -> None:
693
+ return wrapped_attr
694
+ else:
695
+ if isinstance(attr, Path):
696
+ self._check_forbidden()
697
+ return RestrictedPath(attr)
698
+ return attr
699
+
700
+ def __str__(self) -> str:
701
+ self._check_forbidden()
702
+ return str(self.__wrapped__)
703
+
704
+ def __repr__(self) -> str:
705
+ return f"<RestrictedPath({repr(self.__wrapped__)})>"
706
+
707
+ def __fspath__(self) -> str:
708
+ self._check_forbidden()
709
+ return str(self.__wrapped__)
710
+
711
+ def __truediv__(self, other: Union[str, Path]) -> Path:
712
+ self._check_forbidden()
713
+ return self.__wrapped__ / other
714
+
715
+ def __itruediv__(self, other: Union[str, Path]) -> Path:
716
+ self.__wrapped__ /= other
717
+ self._check_forbidden()
718
+ return self.__wrapped__
719
+
720
+ def __eq__(self, other: object) -> bool:
721
+ if isinstance(other, RestrictedPath):
722
+ return bool(self.__wrapped__ == other.__wrapped__)
723
+ return bool(self.__wrapped__ == other)
724
+
725
+ def __hash__(self) -> int:
726
+ return hash(self.__wrapped__)
727
+
728
+ def __len__(self) -> int:
729
+ return len(self.__wrapped__.parts)
730
+
731
+ def __contains__(self, item: str) -> bool:
732
+ return item in self.__wrapped__.parts
733
+
734
+
735
+ ROOT_DIR = RestrictedPath(WORKING_DIR)
736
+ EXPORT_DIR = RestrictedPath(WORKING_DIR / "exports")
737
+ INFERENCES_DIR = RestrictedPath(WORKING_DIR / "inferences")
738
+ TRACE_DATASETS_DIR = RestrictedPath(WORKING_DIR / "trace_datasets")
739
+
740
+
741
+ def ensure_working_dir_if_needed() -> None:
625
742
  """
626
743
  Ensure the working directory exists. This is needed because the working directory
627
744
  must exist before certain operations can be performed.
745
+
746
+ This is bypassed if a postgres database is configured and a working directory is not set.
628
747
  """
748
+ if _no_local_storage():
749
+ pass
750
+
629
751
  logger.info(f"📋 Ensuring phoenix working directory: {WORKING_DIR}")
630
752
  try:
631
753
  for path in (
@@ -644,8 +766,8 @@ def ensure_working_dir() -> None:
644
766
  raise
645
767
 
646
768
 
647
- # Invoke ensure_working_dir() to ensure the working directory exists
648
- ensure_working_dir()
769
+ # Invoke ensure_working_dir_if_needed() to ensure the working directory exists
770
+ ensure_working_dir_if_needed()
649
771
 
650
772
 
651
773
  def get_exported_files(directory: Path) -> list[Path]:
@@ -662,6 +784,8 @@ def get_exported_files(directory: Path) -> list[Path]:
662
784
  list: list[Path]
663
785
  List of paths of the exported files.
664
786
  """
787
+ if _no_local_storage():
788
+ return [] # Do not attempt to access local storage
665
789
  return list(directory.glob("*.parquet"))
666
790
 
667
791
 
@@ -734,29 +858,6 @@ def get_env_database_connection_str() -> str:
734
858
  return f"sqlite:///{working_dir}/phoenix.db"
735
859
 
736
860
 
737
- def get_env_postgres_connection_str() -> Optional[str]:
738
- pg_user = os.getenv(ENV_PHOENIX_POSTGRES_USER)
739
- pg_password = os.getenv(ENV_PHOENIX_POSTGRES_PASSWORD)
740
- pg_host = os.getenv(ENV_PHOENIX_POSTGRES_HOST)
741
- pg_port = os.getenv(ENV_PHOENIX_POSTGRES_PORT)
742
- pg_db = os.getenv(ENV_PHOENIX_POSTGRES_DB)
743
-
744
- if pg_host and ":" in pg_host:
745
- pg_host, parsed_port = pg_host.split(":")
746
- pg_port = pg_port or parsed_port # use the explicitly set port if provided
747
-
748
- if pg_host and pg_user and pg_password:
749
- encoded_password = quote_plus(pg_password)
750
- connection_str = f"postgresql://{pg_user}:{encoded_password}@{pg_host}"
751
- if pg_port:
752
- connection_str = f"{connection_str}:{pg_port}"
753
- if pg_db:
754
- connection_str = f"{connection_str}/{pg_db}"
755
-
756
- return connection_str
757
- return None
758
-
759
-
760
861
  def get_env_database_schema() -> Optional[str]:
761
862
  if get_env_database_connection_str().startswith("sqlite"):
762
863
  return None
@@ -236,21 +236,6 @@ class Project(Node):
236
236
  stmt = stmt.where(time_range.start <= models.Span.start_time)
237
237
  if time_range.end:
238
238
  stmt = stmt.where(models.Span.start_time < time_range.end)
239
- if root_spans_only:
240
- # A root span is either a span with no parent_id or an orphan span
241
- # (a span whose parent_id references a span that doesn't exist in the database)
242
- if orphan_span_as_root_span:
243
- # Include both types of root spans
244
- parent_spans = select(models.Span.span_id).alias("parent_spans")
245
- stmt = stmt.where(
246
- ~select(1).where(models.Span.parent_id == parent_spans.c.span_id).exists()
247
- # Note: We avoid using an OR clause with Span.parent_id.is_(None) here
248
- # because it significantly degraded PostgreSQL performance (>10x worse)
249
- # during testing.
250
- )
251
- else:
252
- # Only include explicit root spans (spans with parent_id = NULL)
253
- stmt = stmt.where(models.Span.parent_id.is_(None))
254
239
  if filter_condition:
255
240
  span_filter = SpanFilter(condition=filter_condition)
256
241
  stmt = span_filter(stmt)
@@ -274,11 +259,29 @@ class Project(Node):
274
259
  )
275
260
  else:
276
261
  stmt = stmt.where(models.Span.id > cursor.rowid)
262
+ stmt = stmt.order_by(cursor_rowid_column)
263
+ if root_spans_only:
264
+ # A root span is either a span with no parent_id or an orphan span
265
+ # (a span whose parent_id references a span that doesn't exist in the database)
266
+ if orphan_span_as_root_span:
267
+ # Include both types of root spans
268
+ parent_spans = select(models.Span.span_id).alias("parent_spans")
269
+ candidate_spans = stmt.add_columns(models.Span.parent_id).cte("candidate_spans")
270
+ stmt = select(candidate_spans).where(
271
+ or_(
272
+ candidate_spans.c.parent_id.is_(None),
273
+ ~select(1)
274
+ .where(candidate_spans.c.parent_id == parent_spans.c.span_id)
275
+ .exists(),
276
+ )
277
+ )
278
+ else:
279
+ # Only include explicit root spans (spans with parent_id = NULL)
280
+ stmt = stmt.where(models.Span.parent_id.is_(None))
277
281
  if first:
278
282
  stmt = stmt.limit(
279
283
  first + 1 # overfetch by one to determine whether there's a next page
280
284
  )
281
- stmt = stmt.order_by(cursor_rowid_column)
282
285
  cursors_and_nodes = []
283
286
  async with info.context.db() as session:
284
287
  span_records = await session.stream(stmt)
@@ -1,32 +1,28 @@
1
1
  {
2
- "_components-C28SkolB.js": {
3
- "file": "assets/components-C28SkolB.js",
2
+ "_components-BAc4OPED.js": {
3
+ "file": "assets/components-BAc4OPED.js",
4
4
  "name": "components",
5
5
  "imports": [
6
- "_vendor-CwC1K6mo.js",
7
- "_pages-eLqonMjF.js",
8
- "_vendor-arizeai-B7-2AIAM.js",
9
- "_vendor-codemirror-D3xhkqGE.js",
6
+ "_vendor-CEisxXSv.js",
7
+ "_pages-Dz-gbBPF.js",
8
+ "_vendor-arizeai-BCTsSnvS.js",
9
+ "_vendor-codemirror-DIWnRs_7.js",
10
10
  "_vendor-three-C5WAXd5r.js"
11
11
  ]
12
12
  },
13
- "_pages-eLqonMjF.js": {
14
- "file": "assets/pages-eLqonMjF.js",
13
+ "_pages-Dz-gbBPF.js": {
14
+ "file": "assets/pages-Dz-gbBPF.js",
15
15
  "name": "pages",
16
16
  "imports": [
17
- "_vendor-CwC1K6mo.js",
18
- "_vendor-arizeai-B7-2AIAM.js",
19
- "_components-C28SkolB.js",
20
- "_vendor-codemirror-D3xhkqGE.js",
21
- "_vendor-recharts-B_VgO7bY.js"
17
+ "_vendor-CEisxXSv.js",
18
+ "_vendor-arizeai-BCTsSnvS.js",
19
+ "_components-BAc4OPED.js",
20
+ "_vendor-codemirror-DIWnRs_7.js",
21
+ "_vendor-recharts-Bame54mG.js"
22
22
  ]
23
23
  },
24
- "_vendor-Cg6lcjUC.css": {
25
- "file": "assets/vendor-Cg6lcjUC.css",
26
- "src": "_vendor-Cg6lcjUC.css"
27
- },
28
- "_vendor-CwC1K6mo.js": {
29
- "file": "assets/vendor-CwC1K6mo.js",
24
+ "_vendor-CEisxXSv.js": {
25
+ "file": "assets/vendor-CEisxXSv.js",
30
26
  "name": "vendor",
31
27
  "imports": [
32
28
  "_vendor-three-C5WAXd5r.js"
@@ -35,33 +31,37 @@
35
31
  "assets/vendor-Cg6lcjUC.css"
36
32
  ]
37
33
  },
38
- "_vendor-arizeai-B7-2AIAM.js": {
39
- "file": "assets/vendor-arizeai-B7-2AIAM.js",
34
+ "_vendor-Cg6lcjUC.css": {
35
+ "file": "assets/vendor-Cg6lcjUC.css",
36
+ "src": "_vendor-Cg6lcjUC.css"
37
+ },
38
+ "_vendor-arizeai-BCTsSnvS.js": {
39
+ "file": "assets/vendor-arizeai-BCTsSnvS.js",
40
40
  "name": "vendor-arizeai",
41
41
  "imports": [
42
- "_vendor-CwC1K6mo.js"
42
+ "_vendor-CEisxXSv.js"
43
43
  ]
44
44
  },
45
- "_vendor-codemirror-D3xhkqGE.js": {
46
- "file": "assets/vendor-codemirror-D3xhkqGE.js",
45
+ "_vendor-codemirror-DIWnRs_7.js": {
46
+ "file": "assets/vendor-codemirror-DIWnRs_7.js",
47
47
  "name": "vendor-codemirror",
48
48
  "imports": [
49
- "_vendor-CwC1K6mo.js",
50
- "_vendor-shiki-CK18CSqG.js"
49
+ "_vendor-CEisxXSv.js",
50
+ "_vendor-shiki-Cc73E4D-.js"
51
51
  ]
52
52
  },
53
- "_vendor-recharts-B_VgO7bY.js": {
54
- "file": "assets/vendor-recharts-B_VgO7bY.js",
53
+ "_vendor-recharts-Bame54mG.js": {
54
+ "file": "assets/vendor-recharts-Bame54mG.js",
55
55
  "name": "vendor-recharts",
56
56
  "imports": [
57
- "_vendor-CwC1K6mo.js"
57
+ "_vendor-CEisxXSv.js"
58
58
  ]
59
59
  },
60
- "_vendor-shiki-CK18CSqG.js": {
61
- "file": "assets/vendor-shiki-CK18CSqG.js",
60
+ "_vendor-shiki-Cc73E4D-.js": {
61
+ "file": "assets/vendor-shiki-Cc73E4D-.js",
62
62
  "name": "vendor-shiki",
63
63
  "imports": [
64
- "_vendor-CwC1K6mo.js"
64
+ "_vendor-CEisxXSv.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-WrtAISug.js",
72
+ "file": "assets/index-Du53xkjY.js",
73
73
  "name": "index",
74
74
  "src": "index.tsx",
75
75
  "isEntry": true,
76
76
  "imports": [
77
- "_vendor-CwC1K6mo.js",
78
- "_vendor-arizeai-B7-2AIAM.js",
79
- "_pages-eLqonMjF.js",
80
- "_components-C28SkolB.js",
77
+ "_vendor-CEisxXSv.js",
78
+ "_vendor-arizeai-BCTsSnvS.js",
79
+ "_pages-Dz-gbBPF.js",
80
+ "_components-BAc4OPED.js",
81
81
  "_vendor-three-C5WAXd5r.js",
82
- "_vendor-codemirror-D3xhkqGE.js",
83
- "_vendor-shiki-CK18CSqG.js",
84
- "_vendor-recharts-B_VgO7bY.js"
82
+ "_vendor-codemirror-DIWnRs_7.js",
83
+ "_vendor-shiki-Cc73E4D-.js",
84
+ "_vendor-recharts-Bame54mG.js"
85
85
  ]
86
86
  }
87
87
  }