├── clientside ├── src │ ├── __init__.py │ ├── captureflow │ │ ├── __init__.py │ │ └── tracer.py │ ├── test.py │ └── ot_tracer.py ├── tests │ ├── __init__.py │ └── test_fastapi_tracer.py ├── examples │ └── fastapi │ │ ├── requirements.txt │ │ ├── install_package.sh │ │ ├── README │ │ ├── server.py │ │ └── utilz.py ├── requirements.txt ├── requirements-dev.txt ├── pyproject.toml └── README.md ├── serverside ├── src │ ├── __init__.py │ ├── utils │ │ ├── __init__.py │ │ ├── integrations │ │ │ ├── __init__.py │ │ │ ├── redis_integration.py │ │ │ └── openai_integration.py │ │ ├── resources │ │ │ └── pytest_template.py │ │ ├── docker_executor.py │ │ └── call_graph.py │ ├── config.py │ └── server.py ├── tests │ ├── __init__.py │ ├── utils │ │ ├── __init__.py │ │ ├── test_call_graph.py │ │ ├── test_creator.py │ │ └── test_exception_patcher.py │ ├── integration │ │ ├── __init__.py │ │ └── basic_flow.py │ ├── conftest.py │ └── test_server.py ├── requirements.txt ├── requirements-dev.txt ├── Dockerfile ├── docker-compose.yml └── README.md ├── clientside_v2 ├── tests │ ├── __init__.py │ ├── instrumentation │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── test_fastapi_instrumentation.py │ │ ├── test_requests_instrumentation.py │ │ ├── test_flask_instrumentation.py │ │ ├── test_httpx_instrumentation.py │ │ ├── test_redis_instrumentation.py │ │ └── test_sqlalchemy_instrumentation.py │ ├── conftest.py │ └── test_span_processor.py ├── captureflow │ ├── __init__.py │ ├── resource.py │ ├── config.py │ ├── tracer_provider.py │ ├── distro.py │ └── span_processor.py ├── examples │ ├── test.db │ ├── sqlalchemy_init.py │ └── server.py ├── README.md ├── docker-compose.yml └── pyproject.toml ├── examples ├── fastapi-carshop-erp-example │ ├── fastapi-bigger-application-master │ │ ├── app │ │ │ ├── __init__.py │ │ │ ├── src │ │ │ │ ├── __init__.py │ │ │ │ ├── routers │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── converter │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── car_converter.py │ │ │ │ │ │ ├── seller_converter.py │ │ │ │ │ │ ├── sale_converter.py │ │ │ │ │ │ └── buyer_converter.py │ │ │ │ │ ├── handlers │ │ │ │ │ │ └── http_error.py │ │ │ │ │ ├── items.py │ │ │ │ │ ├── api.py │ │ │ │ │ ├── cars.py │ │ │ │ │ ├── auth.py │ │ │ │ │ ├── users.py │ │ │ │ │ ├── buyers.py │ │ │ │ │ ├── sellers.py │ │ │ │ │ ├── stocks.py │ │ │ │ │ └── sales.py │ │ │ │ ├── domain │ │ │ │ │ ├── buyer │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── models.py │ │ │ │ │ │ ├── schemas.py │ │ │ │ │ │ └── service.py │ │ │ │ │ ├── car │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── schemas.py │ │ │ │ │ │ ├── models.py │ │ │ │ │ │ ├── repository.py │ │ │ │ │ │ └── service.py │ │ │ │ │ ├── sale │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── schemas.py │ │ │ │ │ │ ├── service.py │ │ │ │ │ │ └── models.py │ │ │ │ │ ├── stock │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── schemas.py │ │ │ │ │ │ ├── models.py │ │ │ │ │ │ └── service.py │ │ │ │ │ ├── user │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── schemas.py │ │ │ │ │ │ ├── models.py │ │ │ │ │ │ └── service.py │ │ │ │ │ └── seller │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── schemas.py │ │ │ │ │ │ ├── models.py │ │ │ │ │ │ └── service.py │ │ │ │ ├── internal │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── admin.py │ │ │ │ ├── config.py │ │ │ │ ├── database.py │ │ │ │ └── dependencies.py │ │ │ ├── test │ │ │ │ ├── __init__.py │ │ │ │ ├── config │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── database_test_config.py │ │ │ │ ├── templates │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── car_tempĺates.py │ │ │ │ │ ├── seller_tempĺates.py │ │ │ │ │ ├── buyer_tempĺates.py │ │ │ │ │ ├── stock_tempĺates.py │ │ │ │ │ └── sale_tempĺates.py │ │ │ │ ├── test_api │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── test_cars.py │ │ │ │ │ ├── test_buyers.py │ │ │ │ │ ├── test_sellers.py │ │ │ │ │ ├── test_stocks.py │ │ │ │ │ └── test_sales.py │ │ │ │ ├── database_tables.py │ │ │ │ ├── database_test.py │ │ │ │ ├── test_jwt.py │ │ │ │ └── base_insertion.py │ │ │ ├── resources │ │ │ │ ├── __init__.py │ │ │ │ └── strings.py │ │ │ └── main.py │ │ ├── runtime.txt │ │ ├── .coveragerc │ │ ├── Procfile │ │ ├── .gitignore │ │ ├── docker-compose.yml │ │ ├── requirements.txt │ │ ├── requirements-local.txt │ │ └── README.md │ └── .DS_Store ├── .DS_Store ├── README.md └── scenario.py ├── Dockerfile.cf ├── .github └── workflows │ └── ci.yml ├── assets └── creating-github-app.md ├── .gitignore └── README.md /clientside/src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /clientside/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /serverside/src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /serverside/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /clientside_v2/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /serverside/src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /clientside/src/captureflow/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /clientside_v2/captureflow/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /serverside/tests/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /serverside/tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /clientside_v2/tests/instrumentation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /serverside/src/utils/integrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaptureFlow/captureflow-py/HEAD/examples/.DS_Store -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/resources/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/routers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/test/config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.8.6 -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/buyer/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/car/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/sale/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/stock/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/user/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/internal/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/test/templates/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/test/test_api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/seller/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/routers/converter/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = *app/test* -------------------------------------------------------------------------------- /clientside_v2/examples/test.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaptureFlow/captureflow-py/HEAD/clientside_v2/examples/test.db -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/Procfile: -------------------------------------------------------------------------------- 1 | web: uvicorn app.main:app --host=0.0.0.0 --port=${PORT:-5000} -------------------------------------------------------------------------------- /clientside/examples/fastapi/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi=0.110.0 2 | uvicorn=0.29.0 3 | # captureflow # (use `install_package.sh` to install from source) -------------------------------------------------------------------------------- /clientside/requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2024.2.2 2 | charset-normalizer==3.3.2 3 | idna==3.6 4 | httpx==0.27.0 5 | requests==2.31.0 6 | urllib3==2.2.1 7 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaptureFlow/captureflow-py/HEAD/examples/fastapi-carshop-erp-example/.DS_Store -------------------------------------------------------------------------------- /serverside/src/utils/integrations/redis_integration.py: -------------------------------------------------------------------------------- 1 | import redis 2 | from src.config import REDIS_URL 3 | 4 | 5 | def get_redis_connection(): 6 | return redis.Redis.from_url(REDIS_URL) 7 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/internal/admin.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | router = APIRouter() 4 | 5 | 6 | @router.post("/") 7 | async def update_admin(): 8 | ''' Example route ''' 9 | return {"message": "Admin getting schwifty"} 10 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/test/database_tables.py: -------------------------------------------------------------------------------- 1 | ### 2 | # Tables for clear on test 3 | ### 4 | 5 | tables = ( 6 | "sales", 7 | "buyers", 8 | "sellers", 9 | "stocks", 10 | "cars", 11 | "users", 12 | "items", 13 | ) 14 | 15 | -------------------------------------------------------------------------------- /clientside_v2/captureflow/resource.py: -------------------------------------------------------------------------------- 1 | import platform 2 | 3 | from opentelemetry.sdk.resources import Resource 4 | 5 | from captureflow.config import CF_SERVICE_NAME 6 | 7 | 8 | def get_resource(): 9 | return Resource.create({"service.name": CF_SERVICE_NAME, "python_version": platform.python_version()}) 10 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/test/templates/car_tempĺates.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def car_json(): 6 | return {"id": 1, "name": "Galardo", "year": 1999, "brand": "lamborghini"} 7 | 8 | 9 | @pytest.fixture 10 | def car_not_found_error(): 11 | return { 'errors': ['car does not exist'] } 12 | -------------------------------------------------------------------------------- /clientside_v2/captureflow/config.py: -------------------------------------------------------------------------------- 1 | # captureflow/config.py 2 | import os 3 | 4 | from dotenv import load_dotenv 5 | 6 | load_dotenv(override=True) 7 | 8 | CF_SERVICE_NAME = os.getenv("CF_SERVICE_NAME", "default_service_name") 9 | CF_DEBUG = os.getenv("CF_DEBUG", False) 10 | CF_OTLP_ENDPOINT = os.getenv("CF_OTLP_ENDPOINT", "http://localhost:4317") # gRPC OTLP by default 11 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .ipynb_checkpoints 3 | .mypy_cache 4 | .vscode 5 | __pycache__ 6 | .pytest_cache 7 | htmlcov 8 | dist 9 | site 10 | .coverage 11 | coverage.xml 12 | .netlify 13 | test.db 14 | log.txt 15 | Pipfile.lock 16 | env3.* 17 | env 18 | docs_build 19 | venv 20 | docs.zip 21 | archive.zip 22 | 23 | # vim temporary files 24 | *~ 25 | .*.sw? -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Use tinnova/tinnova123 user/password credentials 2 | version: '3.1' 3 | 4 | services: 5 | 6 | db: 7 | image: postgres:alpine3.14 8 | ports: 9 | - 5432:5432 10 | environment: 11 | POSTGRES_USER: tinnova 12 | POSTGRES_PASSWORD: tinnova123 13 | 14 | adminer: 15 | image: adminer 16 | ports: 17 | - 9000:8080 18 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/test/templates/seller_tempĺates.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def seller_json(): 6 | return { 7 | "id": 1, 8 | "name": "João da Silva", 9 | "cpf": "69285717640", 10 | "phone": "1299871234" 11 | } 12 | 13 | 14 | @pytest.fixture 15 | def seller_not_found_error(): 16 | return { "errors": ["seller does not exist"] } 17 | 18 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/stock/schemas.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | from ..car.schemas import Car 4 | 5 | class StockBase(BaseModel): 6 | id: int 7 | 8 | 9 | class StockCreate(BaseModel): 10 | car_id: int 11 | quantity: int 12 | 13 | 14 | class Stock(StockBase): 15 | car: Car 16 | quantity: int 17 | 18 | class Config: 19 | orm_mode = True 20 | 21 | -------------------------------------------------------------------------------- /serverside/src/config.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import os 3 | 4 | from dotenv import load_dotenv 5 | 6 | load_dotenv(override=True) 7 | 8 | GITHUB_APP_ID = os.getenv("GITHUB_APP_ID", "EMPTY_GITHUB_APP_ID") 9 | GITHUB_APP_PRIVATE_KEY_BASE64 = os.getenv( 10 | "GITHUB_APP_PRIVATE_KEY_BASE64", base64.b64encode(b"EMPTY_GITHUB_APP_PRIVATE_KEY_BASE64") 11 | ) 12 | OPENAI_KEY = os.getenv("OPENAI_KEY", "EMPTY_OPENAI_KEY") 13 | REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0") 14 | -------------------------------------------------------------------------------- /clientside/src/test.py: -------------------------------------------------------------------------------- 1 | # main.py 2 | from .ot_tracer import trace_function 3 | 4 | @trace_function 5 | def example_function(a, b): 6 | return a + b 7 | 8 | @trace_function 9 | def another_example_function(x): 10 | return x * x 11 | 12 | if __name__ == "__main__": 13 | result = example_function(1, 2) 14 | print(f"Result of example_function: {result}") 15 | 16 | result = another_example_function(3) 17 | print(f"Result of another_example_function: {result}") 18 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/car/schemas.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class CarBase(BaseModel): 7 | id: int 8 | 9 | 10 | class CarCreate(BaseModel): 11 | name: str 12 | year: int 13 | brand: str 14 | 15 | 16 | class Car(CarBase): 17 | name: str 18 | year: int 19 | brand: str 20 | 21 | class Config: 22 | orm_mode = True 23 | 24 | -------------------------------------------------------------------------------- /clientside/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | annotated-types==0.6.0 2 | anyio==4.3.0 3 | black==24.3.0 4 | certifi==2024.2.2 5 | charset-normalizer==3.3.2 6 | fastapi==0.110.0 7 | h11==0.14.0 8 | httpcore==1.0.4 9 | httpx==0.27.0 10 | idna==3.6 11 | iniconfig==2.0.0 12 | isort==5.13.2 13 | packaging==24.0 14 | pluggy==1.4.0 15 | pydantic==2.6.4 16 | pydantic_core==2.16.3 17 | pytest==8.1.1 18 | pytest-asyncio==0.23.6 19 | requests==2.31.0 20 | sniffio==1.3.1 21 | starlette==0.36.3 22 | typing_extensions==4.10.0 23 | urllib3==2.2.1 24 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/seller/schemas.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class SellerBase(BaseModel): 7 | id: int 8 | 9 | 10 | class SellerCreate(BaseModel): 11 | name: str 12 | cpf: str 13 | phone: str 14 | 15 | 16 | class Seller(SellerBase): 17 | name: str 18 | cpf: str 19 | phone: str 20 | 21 | class Config: 22 | orm_mode = True 23 | 24 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/test/database_test.py: -------------------------------------------------------------------------------- 1 | from .config import database_test_config 2 | 3 | from .database_tables import tables 4 | 5 | 6 | ### 7 | # Suport test database dependencies 8 | ### 9 | 10 | def configure_test_database(app): 11 | ''' Configure test database ''' 12 | database_test_config.configure_test_database(app) 13 | 14 | 15 | def clear_database(): 16 | ''' Clear test database ''' 17 | database_test_config.truncate_tables(tables) 18 | 19 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | This is an example app for testing CaptureFlow functionality. 2 | It's copied from https://github.com/skatesham/fastapi-bigger-application 3 | 4 | 5 | The idea is that after: 6 | 1. Instrumenting `fastapi-carshop` with captureflow-agent. 7 | 2. Adding minimal CaptureFlow-related configuration (`Dockerfile.cf` explaining how to run existing test suite) 8 | 3. Simulating some traffic data going through it. 9 | 10 | CaptureFlow is going to be able to produce test suite characterizing existing endpoint behaviour. 11 | -------------------------------------------------------------------------------- /clientside_v2/README.md: -------------------------------------------------------------------------------- 1 | # What 2 | 3 | OpenTelemetry-based tracer with custom instrumentations that are crucial for CaptureFlow. 4 | 5 | # Development 6 | 7 | - Uses Poetry, as it's easy to publish this way. 8 | 9 | # Running 10 | 11 | Run Jaeger-UI and trace collector via `docker-compose up`. 12 | Run your app via `opentelemetry-instrument uvicorn server:app` 13 | 14 | Check your `http://localhost:16686/search` for application monitoring. 15 | 16 | # Publishing 17 | 18 | `poetry config pypi-token.pypi ` 19 | `poetry publish` -------------------------------------------------------------------------------- /clientside/examples/fastapi/install_package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # It assumes it has FastAPI example server running on localhost:8000 4 | 5 | # Navigate up two parent folders 6 | cd ../../ 7 | 8 | # Run python build command 9 | python -m build 10 | 11 | # Assuming the build produces a single .whl file in the dist/ directory. 12 | # Adjust the glob pattern as needed if your package produces different files. 13 | PACKAGE_FILE=$(ls dist/*.whl | head -n 1) 14 | 15 | # Install the package using pip 16 | pip install --force-reinstall "$PACKAGE_FILE" 17 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/seller/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String 2 | 3 | from sqlalchemy.orm import relationship 4 | 5 | from ...database import Base 6 | 7 | 8 | class Seller(Base): 9 | __tablename__ = "sellers" 10 | 11 | id = Column(Integer, primary_key=True, index=True) 12 | name = Column(String) 13 | cpf = Column(String, index=True) 14 | phone = Column(String) 15 | 16 | sale = relationship("Sale", back_populates="seller") 17 | 18 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/routers/handlers/http_error.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException 2 | from starlette.requests import Request 3 | from starlette.responses import JSONResponse 4 | 5 | 6 | ### 7 | # Exception Handlers for filter exception error and personalize messages 8 | ### 9 | 10 | async def http_error_handler(_: Request, exc: HTTPException) -> JSONResponse: 11 | ''' Personalize response when HTTPException ''' 12 | return JSONResponse({"errors": [exc.detail]}, status_code=exc.status_code) 13 | 14 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/config.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from starlette.config import Config 4 | 5 | from starlette.datastructures import CommaSeparatedStrings, Secret 6 | 7 | 8 | ### 9 | # Properties configurations 10 | ### 11 | 12 | API_PREFIX = "/api" 13 | 14 | JWT_TOKEN_PREFIX = "Authorization" 15 | 16 | config = Config(".env") 17 | 18 | ROUTE_PREFIX_V1 = "/v1" 19 | 20 | ALLOWED_HOSTS: List[str] = config( 21 | "ALLOWED_HOSTS", 22 | cast=CommaSeparatedStrings, 23 | default="", 24 | ) 25 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/database.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import sessionmaker 4 | 5 | import os 6 | 7 | ### 8 | # Database Configuration 9 | ### 10 | 11 | SQLALCHEMY_DATABASE_URL = "postgresql://tinnova:tinnova123@localhost/tinnova" 12 | 13 | engine = create_engine( 14 | os.getenv("DB_URL", SQLALCHEMY_DATABASE_URL) 15 | ) 16 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 17 | 18 | Base = declarative_base() -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/routers/converter/car_converter.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from ...domain.car import schemas, models 4 | 5 | 6 | def convert(db_car: models.Car): 7 | ''' Customized convertion to response template ''' 8 | return schemas.Car( 9 | id=db_car.id, 10 | name=db_car.name, 11 | year=db_car.year, 12 | brand=db_car.brand 13 | ) 14 | 15 | 16 | def convert_many(db_cars: List): 17 | ''' Convert list customized ''' 18 | return [convert(db_car) for db_car in db_cars] -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/routers/converter/seller_converter.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from ...domain.seller import schemas, models 4 | 5 | 6 | def convert(db_seller: models.Seller): 7 | ''' Customized convertion to response template ''' 8 | return schemas.Seller( 9 | id=db_seller.id, 10 | name=db_seller.name, 11 | cpf=db_seller.cpf, 12 | phone=db_seller.phone) 13 | 14 | 15 | def convert_many(db_sellers: List): 16 | ''' Convert list customized ''' 17 | return [convert(db_seller) for db_seller in db_sellers] -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/test/templates/buyer_tempĺates.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def buyer_json(): 6 | return { 7 | "id": 1, 8 | "name": "Bruce Lee", 9 | "address": { 10 | "cep": "73770-000", 11 | "public_place": "Banbusal", 12 | "city": "Alto Paraiso de Goias", 13 | "district": "Cidade Baixa", 14 | "state": "Goias" 15 | }, 16 | "phone": "12996651234" 17 | } 18 | 19 | 20 | @pytest.fixture 21 | def buyer_not_found_error(): 22 | return { 'errors': ['buyer does not exist'] } 23 | 24 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/resources/strings.py: -------------------------------------------------------------------------------- 1 | ### 2 | # Centralize response messages 3 | ### 4 | 5 | # Errors does not exist 6 | CAR_DOES_NOT_EXIST_ERROR = "car does not exist" 7 | STOCK_DOES_NOT_EXIST_ERROR = "stock does not exist" 8 | BUYER_DOES_NOT_EXIST_ERROR = "buyer does not exist" 9 | SELLER_DOES_NOT_EXIST_ERROR = "seller does not exist" 10 | SALES_DOES_NOT_EXIST_ERROR = "sale does not exist" 11 | USER_DOES_NOT_EXIST_ERROR = "user does not exist" 12 | 13 | # Errors already exists 14 | STOCK_ALREADY_EXISTS_ERROR = "stock already exist" 15 | 16 | # Errors Out of Stock 17 | STOCK_OUT_OF_STOCK_ERROR = "out of stock" 18 | 19 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/buyer/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String 2 | 3 | from sqlalchemy.orm import relationship 4 | 5 | from ...database import Base 6 | 7 | 8 | class Buyer(Base): 9 | __tablename__ = "buyers" 10 | 11 | id = Column(Integer, primary_key=True, index=True) 12 | name = Column(String) 13 | phone = Column(String) 14 | address_cep = Column(String) 15 | address_public_place = Column(String) 16 | address_city = Column(String) 17 | address_district = Column(String) 18 | address_state = Column(String) 19 | 20 | sale = relationship("Sale", back_populates="buyer") 21 | 22 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/car/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, UniqueConstraint 2 | 3 | from sqlalchemy.orm import relationship 4 | 5 | from ...database import Base 6 | 7 | 8 | class Car(Base): 9 | __tablename__ = "cars" 10 | 11 | id = Column(Integer, primary_key=True, index=True) 12 | name = Column(String, index=True) 13 | year = Column(Integer, index=True) 14 | brand = Column(String, index=True) 15 | 16 | stock = relationship("Stock", back_populates="car") 17 | sale = relationship("Sale", back_populates="car") 18 | UniqueConstraint('year', 'name', 'brand', name='cars_year_name_brand_uk_idx1') 19 | 20 | -------------------------------------------------------------------------------- /serverside/requirements.txt: -------------------------------------------------------------------------------- 1 | annotated-types==0.6.0 2 | anyio==4.3.0 3 | certifi==2024.2.2 4 | cffi==1.16.0 5 | charset-normalizer==3.3.2 6 | click==8.1.7 7 | cryptography==42.0.5 8 | Deprecated==1.2.14 9 | distro==1.9.0 10 | fastapi==0.110.0 11 | h11==0.14.0 12 | httpcore==1.0.4 13 | httpx==0.27.0 14 | idna==3.6 15 | networkx==3.2.1 16 | openai==1.14.2 17 | pycparser==2.21 18 | pydantic==2.6.4 19 | pydantic_core==2.16.3 20 | PyGithub==2.2.0 21 | PyJWT==2.8.0 22 | PyNaCl==1.5.0 23 | python-dotenv==1.0.1 24 | redis==5.0.3 25 | requests==2.31.0 26 | sniffio==1.3.1 27 | starlette==0.36.3 28 | tqdm==4.66.2 29 | typing_extensions==4.10.0 30 | urllib3==2.2.1 31 | uvicorn==0.29.0 32 | wrapt==1.16.0 33 | captureflow-agent==0.0.11 34 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/sale/schemas.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from pydantic import BaseModel 3 | 4 | from ..car.schemas import Car 5 | from ..buyer.schemas import Buyer 6 | from ..seller.schemas import Seller 7 | 8 | 9 | class SaleBase(BaseModel): 10 | id: int 11 | 12 | 13 | class SaleCreate(BaseModel): 14 | car_id: int 15 | seller_id: int 16 | buyer_id: int 17 | 18 | class SaleCreateResponse(SaleCreate): 19 | id: int 20 | 21 | class Sale(SaleBase): 22 | car: Car 23 | buyer: Buyer 24 | seller: Seller 25 | created_at: datetime 26 | 27 | class Config: 28 | orm_mode = True 29 | 30 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/stock/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, ForeignKey, Integer 2 | 3 | from sqlalchemy.orm import relationship 4 | 5 | from ...database import Base 6 | 7 | 8 | class Stock(Base): 9 | __tablename__ = "stocks" 10 | 11 | id = Column(Integer, primary_key=True, index=True) 12 | quantity = Column(Integer) 13 | car_id = Column(Integer, ForeignKey("cars.id"), unique=True) 14 | 15 | car = relationship("Car", back_populates="stock") 16 | 17 | 18 | def hasStock(self, quantity): 19 | return self.quantity >= quantity 20 | 21 | 22 | def reduce_quantity(self, quantity): 23 | self.quantity -= quantity 24 | 25 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/car/repository.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | from . import models, schemas 4 | 5 | 6 | def create_car(db: Session, car: schemas.CarCreate): 7 | db_car = models.Car(**car.dict()) 8 | db.add(db_car) 9 | db.commit() 10 | db.refresh(db_car) 11 | return db_car 12 | 13 | def get_car(db: Session, car_id: int): 14 | return db.query(models.Car).filter(models.Car.id == car_id).first() 15 | 16 | def get_cars(db: Session, skip: int = 0, limit: int = 100): 17 | return db.query(models.Car).offset(skip).limit(limit).all() 18 | 19 | def remove_car(db: Session, db_car: models.Car): 20 | db.delete(db_car) 21 | db.commit() 22 | return True 23 | 24 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/routers/items.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fastapi import APIRouter, Depends, HTTPException 4 | 5 | from sqlalchemy.orm import Session 6 | 7 | from ..dependencies import get_token_header, get_db 8 | 9 | from ..domain.user import service, schemas 10 | 11 | 12 | router = APIRouter( 13 | prefix="/items", 14 | tags=["items"], 15 | dependencies=[Depends(get_token_header)], 16 | responses={404: {"description": "Not found"}}, 17 | ) 18 | 19 | @router.get("/items/", response_model=List[schemas.Item]) 20 | def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): 21 | items = service.get_items(db, skip=skip, limit=limit) 22 | return items 23 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/sale/service.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | from . import models, schemas 4 | 5 | def create_sale(db: Session, sale: schemas.SaleCreate): 6 | db_sale = models.Sale(**sale.dict()) 7 | db.add(db_sale) 8 | db.commit() 9 | db.refresh(db_sale) 10 | return db_sale 11 | 12 | def get_sale(db: Session, sale_id: int): 13 | return db.query(models.Sale).filter(models.Sale.id == sale_id).first() 14 | 15 | def get_sales(db: Session, skip: int = 0, limit: int = 100): 16 | return db.query(models.Sale).offset(skip).limit(limit).all() 17 | 18 | def remove_sale(db: Session, db_sale: models.Sale): 19 | db.delete(db_sale) 20 | db.commit() 21 | return True 22 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/user/schemas.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class ItemBase(BaseModel): 7 | title: str 8 | description: Optional[str] = None 9 | 10 | 11 | class ItemCreate(ItemBase): 12 | pass 13 | 14 | 15 | class Item(ItemBase): 16 | id: int 17 | owner_id: int 18 | 19 | class Config: 20 | orm_mode = True 21 | 22 | 23 | class UserBase(BaseModel): 24 | email: str 25 | 26 | 27 | class UserCreate(UserBase): 28 | password: str 29 | 30 | 31 | class User(UserBase): 32 | id: int 33 | is_active: bool 34 | items: List[Item] = [] 35 | 36 | class Config: 37 | orm_mode = True 38 | 39 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/routers/converter/sale_converter.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from . import buyer_converter 4 | 5 | from . import seller_converter 6 | 7 | from . import car_converter 8 | 9 | from ...domain.sale import schemas, models 10 | 11 | 12 | def convert(db_sale: models.Sale): 13 | ''' Customized convertion to response template ''' 14 | return schemas.Sale( 15 | id=db_sale.id, 16 | car=car_converter.convert(db_sale.car), 17 | seller=seller_converter.convert(db_sale.seller), 18 | buyer=buyer_converter.convert(db_sale.buyer), 19 | created_at=db_sale.created_at 20 | ) 21 | 22 | 23 | def convert_many(db_sales: List): 24 | ''' Convert list customized ''' 25 | return [convert(db_sale) for db_sale in db_sales] -------------------------------------------------------------------------------- /clientside_v2/examples/sqlalchemy_init.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, Sequence, String, create_engine 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import sessionmaker 4 | 5 | DATABASE_URL = "sqlite:///./test.db" 6 | Base = declarative_base() 7 | engine = create_engine(DATABASE_URL) 8 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 9 | 10 | 11 | class Item(Base): 12 | __tablename__ = "items" 13 | id = Column(Integer, Sequence("item_id_seq"), primary_key=True, index=True) 14 | name = Column(String, index=True) 15 | description = Column(String, index=True) 16 | 17 | 18 | Base.metadata.create_all(bind=engine) 19 | 20 | 21 | def get_db(): 22 | db = SessionLocal() 23 | try: 24 | yield db 25 | finally: 26 | db.close() 27 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/sale/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from sqlalchemy import Column, ForeignKey, Integer, DateTime 3 | from sqlalchemy.orm import relationship 4 | 5 | from ...database import Base 6 | 7 | 8 | class Sale(Base): 9 | __tablename__ = "sales" 10 | 11 | id = Column(Integer, primary_key=True, index=True) 12 | car_id = Column(Integer, ForeignKey("cars.id")) 13 | buyer_id = Column(Integer, ForeignKey("buyers.id")) 14 | seller_id = Column(Integer, ForeignKey("sellers.id")) 15 | created_at = Column(DateTime, default=datetime.utcnow) 16 | 17 | car = relationship("Car", back_populates="sale") 18 | buyer = relationship("Buyer", back_populates="sale") 19 | seller = relationship("Seller", back_populates="sale") 20 | 21 | -------------------------------------------------------------------------------- /Dockerfile.cf: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a parent image 2 | FROM python:3.11-slim 3 | 4 | ENV RUN_TEST_COVERAGE "cd serverside && pytest --cov=. --cov-report json >pytest_output; cat coverage.json; echo '====SPLIT===='; cat pytest_output;" 5 | 6 | # Set the working directory in the container 7 | WORKDIR /usr/src/app 8 | 9 | # Install any needed packages specified in requirements.txt 10 | COPY serverside/requirements-dev.txt ./requirements-dev.txt 11 | RUN python -m pip install --upgrade pip 12 | RUN python -m pip install --no-cache-dir -r ./requirements-dev.txt 13 | RUN apt-get update -y && apt-get install git -y 14 | 15 | # Copy the server directory contents into the container at /usr/src/app 16 | COPY . . 17 | 18 | # Make port 8000 available to the world outside this container 19 | EXPOSE 8000 20 | 21 | CMD ["tail", "-f", "/dev/null"] 22 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/routers/converter/buyer_converter.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from ...domain.buyer import schemas, models 4 | 5 | 6 | def convert(db_buyer: models.Buyer): 7 | ''' Customized convertion to response template ''' 8 | return schemas.Buyer( 9 | id=db_buyer.id, 10 | name=db_buyer.name, 11 | phone=db_buyer.phone, 12 | address=schemas.Address( 13 | cep=db_buyer.address_cep, 14 | public_place=db_buyer.address_public_place, 15 | city=db_buyer.address_city, 16 | district=db_buyer.address_district, 17 | state=db_buyer.address_state, 18 | ) 19 | ) 20 | 21 | 22 | def convert_many(db_buyers: List): 23 | ''' Convert list customized ''' 24 | return [convert(db_buyer) for db_buyer in db_buyers] -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/requirements.txt: -------------------------------------------------------------------------------- 1 | aiocontextvars==0.2.2 2 | asgiref==3.4.1 3 | async-exit-stack==1.0.1 4 | async-generator==1.10 5 | attrs==21.2.0 6 | autopep8==1.5.7 7 | captureflow-agent==0.0.11 8 | certifi==2021.5.30 9 | charset-normalizer==2.0.3 10 | click==8.0.1 11 | contextvars==2.4 12 | coverage==5.5 13 | dataclasses 14 | fastapi==0.66.0 15 | greenlet==3.0.3 16 | h11==0.12.0 17 | idna==3.2 18 | immutables==0.15 19 | importlib-metadata==4.6.1 20 | iniconfig==1.1.1 21 | packaging==21.0 22 | pluggy==0.13.1 23 | psycopg2-binary==2.9.6 24 | py==1.10.0 25 | pycodestyle==2.7.0 26 | pydantic==1.8.2 27 | PyJWT==2.7.0 28 | pyparsing==2.4.7 29 | pytest==6.2.4 30 | pytest-cov==2.12.1 31 | requests==2.26.0 32 | SQLAlchemy==1.4.21 33 | starlette==0.14.2 34 | toml==0.10.2 35 | typing-extensions==3.10.0.0 36 | urllib3==1.26.6 37 | uvicorn==0.14.0 38 | zipp==3.5.0 39 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/requirements-local.txt: -------------------------------------------------------------------------------- 1 | aiocontextvars==0.2.2 2 | asgiref==3.4.1 3 | async-exit-stack==1.0.1 4 | async-generator==1.10 5 | attrs==21.2.0 6 | autopep8==1.5.7 7 | captureflow-agent==0.0.11 8 | certifi==2021.5.30 9 | charset-normalizer==2.0.3 10 | click==8.0.1 11 | contextvars==2.4 12 | coverage==5.5 13 | dataclasses==0.6 14 | fastapi==0.111.0 15 | greenlet==2.0.0 16 | h11==0.12.0 17 | idna==3.2 18 | immutables==0.15 19 | importlib-metadata==6.0.0 20 | iniconfig==1.1.1 21 | packaging==21.0 22 | pluggy==0.13.1 23 | psycopg2-binary==2.9.6 24 | py==1.10.0 25 | pycodestyle==2.7.0 26 | pydantic==1.8.2 27 | PyJWT==2.7.0 28 | pyparsing==2.4.7 29 | pytest==6.2.4 30 | pytest-cov==2.12.1 31 | requests==2.26.0 32 | SQLAlchemy==2.0.30 33 | starlette==0.37.2 34 | toml==0.10.2 35 | typing-extensions==4.8.0.0 36 | urllib3==1.26.6 37 | uvicorn==0.14.0 38 | zipp==3.5.0 39 | -------------------------------------------------------------------------------- /clientside_v2/tests/instrumentation/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from opentelemetry.sdk.trace.export import SimpleSpanProcessor 3 | from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter 4 | from opentelemetry.trace import get_tracer_provider 5 | 6 | from captureflow.distro import CaptureFlowDistro 7 | 8 | 9 | @pytest.fixture(scope="session") 10 | def span_exporter(): 11 | distro = CaptureFlowDistro() 12 | distro._configure() 13 | 14 | # Retrieve the global tracer provider 15 | tracer_provider = get_tracer_provider() 16 | 17 | # Set up in-memory span exporter 18 | span_exporter = InMemorySpanExporter() 19 | span_processor = SimpleSpanProcessor(span_exporter) 20 | tracer_provider.add_span_processor(span_processor) 21 | 22 | return span_exporter 23 | 24 | 25 | @pytest.fixture(autouse=True) 26 | def clear_exporter(span_exporter): 27 | span_exporter.clear() 28 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/routers/api.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from ..config import ROUTE_PREFIX_V1 4 | 5 | from . import items, users, cars, stocks, sellers, buyers, sales, auth 6 | 7 | router = APIRouter() 8 | 9 | def include_api_routes(): 10 | ''' Include to router all api rest routes with version prefix ''' 11 | router.include_router(auth.router) 12 | router.include_router(users.router, prefix=ROUTE_PREFIX_V1) 13 | router.include_router(items.router, prefix=ROUTE_PREFIX_V1) 14 | router.include_router(cars.router, prefix=ROUTE_PREFIX_V1) 15 | router.include_router(stocks.router, prefix=ROUTE_PREFIX_V1) 16 | router.include_router(sellers.router, prefix=ROUTE_PREFIX_V1) 17 | router.include_router(buyers.router, prefix=ROUTE_PREFIX_V1) 18 | router.include_router(sales.router, prefix=ROUTE_PREFIX_V1) 19 | 20 | include_api_routes() -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/user/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Boolean, Column, ForeignKey, Integer, String 2 | from sqlalchemy.orm import relationship 3 | 4 | from ...database import Base 5 | 6 | 7 | class User(Base): 8 | __tablename__ = "users" 9 | 10 | id = Column(Integer, primary_key=True, index=True) 11 | email = Column(String, unique=True, index=True) 12 | hashed_password = Column(String) 13 | is_active = Column(Boolean, default=True) 14 | 15 | items = relationship("Item", back_populates="owner") 16 | 17 | 18 | class Item(Base): 19 | __tablename__ = "items" 20 | 21 | id = Column(Integer, primary_key=True, index=True) 22 | title = Column(String, index=True) 23 | description = Column(String, index=True) 24 | owner_id = Column(Integer, ForeignKey("users.id")) 25 | 26 | owner = relationship("User", back_populates="items") 27 | 28 | -------------------------------------------------------------------------------- /clientside/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools] 6 | packages = { find = { where = ["src"], exclude = ["tests", "examples"] } } 7 | 8 | [project] 9 | name = "captureflow-agent" 10 | version = "0.0.9" 11 | description = "Data collector to unleash LLM power" 12 | authors = [ 13 | {name = "Nikita Kuts", email = "me@nikitakuts.com"}, 14 | ] 15 | readme = "README.md" 16 | license = {file = "LICENSE"} 17 | classifiers = [ 18 | "Programming Language :: Python :: 3.11", 19 | ] 20 | dependencies = [ 21 | "certifi>=2024.2.2", 22 | "charset-normalizer>=3.3.2", 23 | "idna>=3.6", 24 | "requests>=2.31.0", 25 | "urllib3>=2.2.1", 26 | "httpx>=0.27.0", 27 | ] 28 | 29 | [project.urls] 30 | homepage = "https://captureflow.dev/" 31 | repository = "https://github.com/CaptureFlow/captureflow-py" 32 | 33 | [tool.isort] 34 | profile = "black" 35 | -------------------------------------------------------------------------------- /clientside_v2/captureflow/tracer_provider.py: -------------------------------------------------------------------------------- 1 | # captureflow/tracer_provider.py 2 | from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter 3 | from opentelemetry.sdk.resources import Resource 4 | from opentelemetry.sdk.trace import TracerProvider 5 | from opentelemetry.sdk.trace.export import ( 6 | BatchSpanProcessor, 7 | ConsoleSpanExporter, 8 | SimpleSpanProcessor, 9 | ) 10 | 11 | from captureflow.config import CF_DEBUG, CF_OTLP_ENDPOINT 12 | 13 | 14 | def get_tracer_provider(resource: Resource) -> TracerProvider: 15 | trace_provider = TracerProvider(resource=resource) 16 | 17 | # Replace with Jaeger 18 | otlp_exporter = OTLPSpanExporter(endpoint=CF_OTLP_ENDPOINT, insecure=True) 19 | 20 | trace_provider.add_span_processor(BatchSpanProcessor(otlp_exporter)) 21 | 22 | if CF_DEBUG: 23 | trace_provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) 24 | 25 | return trace_provider 26 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/seller/service.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | from . import models, schemas 4 | 5 | def create_seller(db: Session, seller: schemas.SellerCreate): 6 | db_seller = models.Seller(**seller.dict()) 7 | db.add(db_seller) 8 | db.commit() 9 | db.refresh(db_seller) 10 | return db_seller 11 | 12 | def get_seller(db: Session, seller_id: int): 13 | return db.query(models.Seller).filter(models.Seller.id == seller_id).first() 14 | 15 | def get_sellers(db: Session, skip: int = 0, limit: int = 100): 16 | return db.query(models.Seller).offset(skip).limit(limit).all() 17 | 18 | def remove_seller(db: Session, db_seller: models.Seller): 19 | db.delete(db_seller) 20 | db.commit() 21 | return True 22 | 23 | def get_by_cpf(db: Session, seller_cpf: str): 24 | return db.query(models.Seller).filter(models.Seller.cpf == seller_cpf).first() 25 | 26 | -------------------------------------------------------------------------------- /serverside/tests/conftest.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import MagicMock, patch 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture 7 | def mock_openai_helper(): 8 | from src.utils.integrations.openai_integration import OpenAIHelper 9 | 10 | with patch.object(OpenAIHelper, "_create_openai_client", return_value=MagicMock()): 11 | mock_helper = OpenAIHelper() 12 | mock_helper.call_chatgpt = MagicMock(return_value="Mocked GPT response") 13 | yield mock_helper 14 | 15 | 16 | @pytest.fixture 17 | def mock_repo_helper(): 18 | from src.utils.integrations.github_integration import RepoHelper 19 | 20 | with patch.object(RepoHelper, "_get_integration", return_value=MagicMock()): 21 | mock_helper = RepoHelper("mock://repo_url") 22 | mock_helper.enrich_callgraph_with_github_context = MagicMock(return_value=None) 23 | mock_helper.create_pull_request_with_new_function = MagicMock(return_value=None) 24 | yield mock_helper 25 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/buyer/schemas.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from app.src.domain.sale.models import Sale 3 | from pydantic import BaseModel 4 | 5 | class Address(BaseModel): 6 | cep: str 7 | public_place: str 8 | city: str 9 | district: str 10 | state: str 11 | 12 | 13 | class BuyerBase(BaseModel): 14 | id: int 15 | 16 | 17 | class BuyerCreate(BaseModel): 18 | name: str 19 | address: Address 20 | phone: str 21 | 22 | 23 | class BuyerSimpleResponse(BaseModel): 24 | id: int 25 | name: str 26 | phone: str 27 | address_cep: str 28 | address_public_place: str 29 | address_city: str 30 | address_district: str 31 | address_state: str 32 | 33 | 34 | 35 | class Buyer(BuyerBase): 36 | name: str 37 | phone: str 38 | address: Address 39 | 40 | class Config: 41 | orm_mode = True 42 | 43 | -------------------------------------------------------------------------------- /serverside/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | annotated-types==0.6.0 2 | anyio==4.3.0 3 | black==24.3.0 4 | captureflow-agent==0.0.9 5 | certifi==2024.2.2 6 | cffi==1.16.0 7 | charset-normalizer==3.3.2 8 | click==8.1.7 9 | cryptography==42.0.5 10 | Deprecated==1.2.14 11 | distro==1.9.0 12 | fastapi==0.110.0 13 | graphviz==0.20.3 14 | h11==0.14.0 15 | httpcore==1.0.4 16 | httpx==0.27.0 17 | idna==3.6 18 | iniconfig==2.0.0 19 | isort==5.13.2 20 | mypy-extensions==1.0.0 21 | networkx==3.2.1 22 | openai==1.14.2 23 | packaging==24.0 24 | pathspec==0.12.1 25 | platformdirs==4.2.0 26 | pluggy==1.4.0 27 | pycparser==2.21 28 | pydantic==2.6.4 29 | pydantic_core==2.16.3 30 | PyGithub==2.2.0 31 | PyJWT==2.8.0 32 | PyNaCl==1.5.0 33 | pytest==8.1.1 34 | pytest-mock==3.14.0 35 | python-dotenv==1.0.1 36 | redis==5.0.3 37 | requests==2.31.0 38 | sniffio==1.3.1 39 | starlette==0.36.3 40 | tqdm==4.66.2 41 | typing_extensions==4.10.0 42 | urllib3==2.2.1 43 | uvicorn==0.29.0 44 | wrapt==1.16.0 45 | pytest-cov==5.0.0 46 | -------------------------------------------------------------------------------- /serverside/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a parent image 2 | FROM python:3.11-slim 3 | 4 | # Set the working directory in the container 5 | WORKDIR /usr/src/app 6 | 7 | # Install any needed packages specified in requirements.txt 8 | COPY ./requirements.txt ./requirements.txt 9 | RUN python -m pip install --upgrade pip 10 | RUN python -m pip install --no-cache-dir -r requirements.txt 11 | 12 | # Install docker to start client's docker images. 13 | RUN apt-get update -y && apt-get install git curl -y 14 | RUN curl -fsSL https://get.docker.com -o get-docker.sh 15 | RUN sh get-docker.sh 16 | 17 | # Copy the current directory contents into the container at /usr/src/app 18 | COPY . . 19 | 20 | ENV CAPTUREFLOW_DEV_SERVER=true 21 | 22 | # Make port 8000 available to the world outside this container 23 | EXPOSE 8000 24 | 25 | # Run server.py when the container launches 26 | CMD ["opentelemetry-instrument", "uvicorn", "src.server:app", "--host", "0.0.0.0", "--port", "8000"] 27 | # CMD ["tail", "-f", "/dev/null"] 28 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/car/service.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | from fastapi import HTTPException 4 | 5 | from . import repository, schemas 6 | 7 | from ....resources.strings import CAR_DOES_NOT_EXIST_ERROR 8 | 9 | 10 | def create_car(db: Session, car: schemas.CarCreate): 11 | return repository.create_car(db, car); 12 | 13 | def get_car(db: Session, car_id: int): 14 | db_car = repository.get_car(db, car_id=car_id) 15 | if db_car is None: 16 | raise HTTPException(status_code=404, detail=CAR_DOES_NOT_EXIST_ERROR) 17 | return repository.get_car(db, car_id); 18 | 19 | def get_cars(db: Session, skip: int = 0, limit: int = 100): 20 | return repository.get_cars(db, skip, limit); 21 | 22 | def remove_car(db: Session, car_id: int): 23 | db_car = get_car(db, car_id=car_id) 24 | if db_car is None: 25 | raise HTTPException(status_code=404, detail=CAR_DOES_NOT_EXIST_ERROR) 26 | return repository.remove_car(db, db_car) 27 | 28 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/test/templates/stock_tempĺates.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def stock_request_json(): 6 | return { 7 | "id": 1, 8 | "car_id": 1, 9 | "quantity": 10 10 | } 11 | 12 | @pytest.fixture 13 | def stock_request_json_out_of_stock(): 14 | return { 15 | "id": 1, 16 | "car_id": 1, 17 | "quantity": 0 18 | } 19 | 20 | 21 | @pytest.fixture 22 | def stock_response_json(): 23 | return { 24 | "id": 1, 25 | "car": {"id": 1, "name": "Galardo", "year": 1999, "brand": "lamborghini"}, 26 | "quantity": 10 27 | } 28 | 29 | 30 | @pytest.fixture 31 | def stock_not_found_error(): 32 | return { "errors": ["stock does not exist"] } 33 | 34 | 35 | @pytest.fixture 36 | def stock_already_exist(): 37 | return { "errors": ["stock already exist"] } 38 | 39 | 40 | @pytest.fixture 41 | def stock_out_of_stock(): 42 | return { "errors": ["out of stock"] } 43 | 44 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/test/test_jwt.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from typing import Optional 3 | 4 | import jwt 5 | 6 | SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" 7 | 8 | ALGORITHM = "HS256" 9 | 10 | def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): 11 | to_encode = data.copy() 12 | if expires_delta: 13 | expire = datetime.utcnow() + expires_delta 14 | else: 15 | expire = datetime.utcnow() + timedelta(minutes=15) 16 | to_encode.update({"exp": expire}) 17 | encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) 18 | return encoded_jwt 19 | 20 | 21 | def decode(token): 22 | payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) 23 | username: str = payload.get("sub") 24 | return username 25 | 26 | 27 | def test_create_token(): 28 | user = {"sub":"sham"} 29 | token = create_access_token(user) 30 | assert decode(token) == "sham" 31 | 32 | -------------------------------------------------------------------------------- /clientside_v2/tests/instrumentation/test_fastapi_instrumentation.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | import pytest 3 | from fastapi import FastAPI 4 | from fastapi.testclient import TestClient 5 | 6 | app = FastAPI() 7 | 8 | 9 | @app.get("/external") 10 | async def call_external(): 11 | async with httpx.AsyncClient() as client: 12 | response = await client.get("https://jsonplaceholder.typicode.com/posts/1") 13 | return {"status_code": response.status_code, "body": response.json()} 14 | 15 | 16 | @pytest.fixture(scope="module") 17 | def client(): 18 | with TestClient(app) as client: 19 | yield client 20 | 21 | 22 | def test_fastapi_instrumentation(span_exporter, client): 23 | # TODO, implement 24 | # The challenge: fastapi.testclient is not a real ASGI app that gets instrumented by OTel-Contrib module, so we need to spawn real FastAPI app, yet we still need to access processed spans 25 | # It's still testable by examples/server.py, but that's suboptimal DX 26 | pass 27 | 28 | 29 | if __name__ == "__main__": 30 | pytest.main([__file__]) 31 | -------------------------------------------------------------------------------- /serverside/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | app: 4 | build: . 5 | volumes: 6 | - .:/usr/src/app 7 | - /var/run/docker.sock:/var/run/docker.sock 8 | ports: 9 | - "8000:8000" 10 | environment: 11 | - REDIS_URL=redis://redis:6379/0 12 | - CF_OTLP_ENDPOINT=http://jaeger:4317 13 | depends_on: 14 | - redis 15 | - jaeger 16 | env_file: 17 | - .env 18 | 19 | jaeger: 20 | image: jaegertracing/all-in-one:1.57 21 | environment: 22 | - COLLECTOR_ZIPKIN_HOST_PORT=:9411 23 | ports: 24 | - "6831:6831/udp" # Thrift compact 25 | - "6832:6832/udp" # Thrift binary 26 | - "5778:5778" # Agent HTTP 27 | - "16686:16686" # Jaeger UI 28 | - "4317:4317" # OTLP gRPC 29 | - "4318:4318" # OTLP HTTP 30 | - "14250:14250" # OTLP HTTP (alternative) 31 | - "14268:14268" # Jaeger HTTP thrift 32 | - "14269:14269" # Jaeger gRPC 33 | - "9411:9411" # Zipkin 34 | 35 | redis: 36 | image: "redis:alpine" 37 | -------------------------------------------------------------------------------- /clientside/examples/fastapi/README: -------------------------------------------------------------------------------- 1 | ## Why? 2 | 3 | Sometimes, there's a need to extend an HTTP server to observe how the captureflow tracer would track events, like what JSONs would it send to serverside. You can check `traces/sample_trace.json`. 4 | 5 | Note that for pinpointing specific functionalities, corresponding unit tests (e.g., `tests/test_fastapi_tracer.py``) might offer a more straightforward approach. This method is more valuable for dynamic, real-world testing scenarios. 6 | 7 | ## Running locally 8 | 9 | For the time being, I've opted not to encapsulate such applications within virtual environments—doing so would introduce additional complexity, including the need to initialize server-side components and retrieve data from Redis. 10 | 11 | Instead, you can: 12 | 13 | 1. Install captureflow directly from the local source code using ./install_package.sh. 14 | 2. Install a minimal set of Python dependencies as listed in requirements.txt. 15 | 3. Execute a sample cURL request to the endpoints specified in server.py, for example, curl http://localhost:8000/fetch_similar/. 16 | 4. This process will populate the traces/ folder with trace logs. 17 | 18 | -------------------------------------------------------------------------------- /clientside_v2/captureflow/distro.py: -------------------------------------------------------------------------------- 1 | from logging import getLogger 2 | 3 | from opentelemetry.instrumentation.distro import BaseDistro 4 | from opentelemetry.trace import set_tracer_provider 5 | 6 | from captureflow.instrumentation import apply_instrumentation 7 | from captureflow.resource import get_resource 8 | from captureflow.span_processor import FrameInfoSpanProcessor 9 | from captureflow.tracer_provider import get_tracer_provider 10 | 11 | logger = getLogger(__name__) 12 | 13 | 14 | class CaptureFlowDistro(BaseDistro): 15 | def _configure(self, **kwargs): 16 | logger.error("CaptureFlow instrumentation initialized") 17 | 18 | # Setup basic tracer 19 | resource = get_resource() 20 | tracer_provider = get_tracer_provider(resource) 21 | 22 | # Add custom span processor to include frame information 23 | frame_info_span_processor = FrameInfoSpanProcessor() 24 | tracer_provider.add_span_processor(frame_info_span_processor) 25 | 26 | set_tracer_provider(tracer_provider) 27 | 28 | # Make sure libraries of interest are instrumented 29 | apply_instrumentation(tracer_provider) 30 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/buyer/service.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | from . import models, schemas 4 | 5 | def create_buyer(db: Session, buyer: schemas.BuyerCreate): 6 | buyer_dict = buyer.dict() 7 | address_dict = buyer_dict["address"] 8 | db_buyer = models.Buyer( 9 | name=buyer_dict["name"], 10 | phone=buyer_dict["phone"], 11 | address_cep=address_dict["cep"], 12 | address_public_place=address_dict["public_place"], 13 | address_city=address_dict["city"], 14 | address_district=address_dict["district"], 15 | address_state=address_dict["state"], 16 | ) 17 | 18 | db.add(db_buyer) 19 | db.commit() 20 | db.refresh(db_buyer) 21 | return db_buyer 22 | 23 | def get_buyer(db: Session, buyer_id: int): 24 | return db.query(models.Buyer).filter(models.Buyer.id == buyer_id).first() 25 | 26 | def get_buyers(db: Session, skip: int = 0, limit: int = 100): 27 | return db.query(models.Buyer).offset(skip).limit(limit).all() 28 | 29 | def remove_buyer(db: Session, db_buyer: models.Buyer): 30 | db.delete(db_buyer) 31 | db.commit() 32 | return True 33 | 34 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/user/service.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | from . import models, schemas 4 | 5 | 6 | def get_user(db: Session, user_id: int): 7 | return db.query(models.User).filter(models.User.id == user_id).first() 8 | 9 | def get_user_by_email(db: Session, email: str): 10 | return db.query(models.User).filter(models.User.email == email).first() 11 | 12 | def get_users(db: Session, skip: int = 0, limit: int = 100): 13 | return db.query(models.User).offset(skip).limit(limit).all() 14 | 15 | def create_user(db: Session, user: schemas.UserCreate): 16 | fake_hashed_password = user.password + "notreallyhashed" 17 | db_user = models.User(email=user.email, hashed_password=fake_hashed_password) 18 | db.add(db_user) 19 | db.commit() 20 | db.refresh(db_user) 21 | return db_user 22 | 23 | def get_items(db: Session, skip: int = 0, limit: int = 100): 24 | return db.query(models.Item).offset(skip).limit(limit).all() 25 | 26 | def create_user_item(db: Session, item: schemas.ItemCreate, user_id: int): 27 | db_item = models.Item(**item.dict(), owner_id=user_id) 28 | db.add(db_item) 29 | db.commit() 30 | db.refresh(db_item) 31 | return db_item 32 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/dependencies.py: -------------------------------------------------------------------------------- 1 | from fastapi import Header, HTTPException 2 | 3 | from .database import SessionLocal 4 | 5 | import jwt 6 | 7 | SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" 8 | 9 | ALGORITHM = "HS256" 10 | 11 | def decode(token): 12 | striped_token = token.replace("Bearer ", "") 13 | return jwt.decode(token, "secret", algorithm="HS256") 14 | 15 | def encode(): 16 | return jwt.encode({"some": "payload"}, "secret", algorithm="HS256") 17 | 18 | def get_db(): 19 | ''' Method for configure database ''' 20 | db = SessionLocal() 21 | try: 22 | yield db 23 | finally: 24 | db.close() 25 | 26 | 27 | async def get_token_header(x_token: str = Header(...)): 28 | ''' Exemplo of header validation dependency ''' 29 | payload = decode(x_token) 30 | username: str = payload.get("email") 31 | if username == None: 32 | raise HTTPException(status_code=403, detail="Unauthorized") 33 | 34 | 35 | async def get_query_token(token: str): 36 | ''' Exemplo of header validation dependency ''' 37 | if token != "jessica": 38 | raise HTTPException(status_code=400, detail="No Jessica token provided") 39 | 40 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CaptureFlow Python CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | client_build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | python-version: [ 3.11 ] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | 27 | - name: Install Poetry 28 | run: | 29 | curl -sSL https://install.python-poetry.org | python3 - 30 | echo 'export PATH="$HOME/.local/bin:$PATH"' >> $GITHUB_ENV 31 | 32 | - name: Install dependencies with Poetry 33 | run: | 34 | cd clientside_v2/ 35 | poetry install 36 | 37 | - name: Run client pytest 38 | run: | 39 | cd clientside_v2/ 40 | poetry run pytest 41 | 42 | - name: Check client code with black 43 | run: | 44 | cd clientside_v2/ 45 | poetry run black --check . --line-length 120 46 | 47 | - name: Check client code with isort 48 | run: | 49 | cd clientside_v2/ 50 | poetry run isort --check-only . 51 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/routers/cars.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fastapi import APIRouter, Depends, HTTPException 4 | 5 | from sqlalchemy.orm import Session 6 | 7 | from ..dependencies import get_token_header, get_db 8 | 9 | from ..domain.car import service, schemas 10 | 11 | 12 | router = APIRouter( 13 | prefix="/cars", 14 | tags=["cars"], 15 | dependencies=[], 16 | responses={404: {"description": "Not found"}}, 17 | ) 18 | 19 | 20 | @router.post("/", response_model=schemas.Car, status_code=201) 21 | def create_car(car: schemas.CarCreate, db: Session = Depends(get_db)): 22 | return service.create_car(db=db, car=car) 23 | 24 | @router.get("/{car_id}", response_model=schemas.Car) 25 | def read_car(car_id: int, db: Session = Depends(get_db)): 26 | return service.get_car(db, car_id=car_id) 27 | 28 | @router.get("/", response_model=List[schemas.Car]) 29 | def read_cars(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): 30 | cars = service.get_cars(db, skip=skip, limit=limit) 31 | return cars 32 | 33 | @router.delete("/{car_id}", response_model=bool) 34 | def delete_car(car_id: int, db: Session = Depends(get_db)): 35 | return service.remove_car(db, car_id=car_id) 36 | 37 | -------------------------------------------------------------------------------- /serverside/tests/integration/basic_flow.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | import requests 5 | 6 | 7 | def load_sample_trace(): 8 | trace_path = Path(__file__).parent.parent / "assets" / "sample_trace.json" 9 | with open(trace_path) as f: 10 | return json.load(f) 11 | 12 | 13 | BASE_URL = "http://localhost:8000/api/v1" 14 | REPO_URL = "https://github.com/NickKuts/capture_flow" 15 | 16 | 17 | def test_store_traces_and_generate_mr(): 18 | sample_trace_1 = load_sample_trace() 19 | sample_trace_2 = load_sample_trace() 20 | 21 | # Store 22 | response_1 = requests.post(f"{BASE_URL}/traces", params={"repository-url": REPO_URL}, json=sample_trace_1) 23 | assert response_1.status_code == 200 24 | assert response_1.json()["message"] == "Trace log saved successfully" 25 | 26 | # Store 27 | response_2 = requests.post(f"{BASE_URL}/traces", params={"repository-url": REPO_URL}, json=sample_trace_2) 28 | assert response_2.status_code == 200 29 | assert response_2.json()["message"] == "Trace log saved successfully" 30 | 31 | # Generate 32 | response_mr = requests.post(f"{BASE_URL}/merge-requests", params={"repository-url": REPO_URL}) 33 | assert response_mr.status_code == 200 34 | assert response_mr.json()["message"] == "MR generation process started successfully" 35 | 36 | 37 | if __name__ == "__main__": 38 | test_store_traces_and_generate_mr() 39 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/routers/auth.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from typing import Optional 3 | 4 | from fastapi import APIRouter, Depends, HTTPException 5 | 6 | from sqlalchemy.orm import Session 7 | 8 | from ..dependencies import get_db 9 | 10 | from ..domain.user import service, schemas 11 | 12 | SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" 13 | 14 | ALGORITHM = "HS256" 15 | 16 | def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): 17 | to_encode = data.copy() 18 | data["hashed_password"] = "" 19 | if expires_delta: 20 | expire = datetime.utcnow() + expires_delta 21 | else: 22 | expire = datetime.utcnow() + timedelta(minutes=15) 23 | to_encode.update({"exp": expire}) 24 | 25 | import jwt 26 | 27 | encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) 28 | return encoded_jwt 29 | 30 | 31 | router = APIRouter(tags=["auth"]) 32 | 33 | @router.post("/login/", response_model=dict) 34 | def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): 35 | db_user = service.get_user_by_email(db, email=user.email) 36 | if db_user and user.password == db_user.hashed_password: 37 | raise HTTPException(status_code=400, detail="Email already registered") 38 | return {"Authorization": "Bearer " + create_access_token(user.dict())} 39 | 40 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/domain/stock/service.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | # TODO: Remove this dependency 4 | from fastapi import HTTPException 5 | 6 | from . import models, schemas 7 | 8 | from ....resources.strings import STOCK_OUT_OF_STOCK_ERROR 9 | 10 | 11 | def create_stock(db: Session, stock: schemas.StockCreate): 12 | db_stock = models.Stock(**stock.dict()) 13 | db.add(db_stock) 14 | db.commit() 15 | db.refresh(db_stock) 16 | return db_stock 17 | 18 | def get_stock(db: Session, stock_id: int): 19 | return db.query(models.Stock).filter(models.Stock.id == stock_id).first() 20 | 21 | 22 | def get_stock_by_car(db: Session, car_id: int): 23 | return db.query(models.Stock).filter(models.Stock.car_id == car_id).first() 24 | 25 | 26 | def buy_car_from_stock(db: Session, car_id: int, quantity: int ): 27 | db_stock = get_stock_by_car(db, car_id=car_id) 28 | if not db_stock.hasStock(quantity): 29 | raise HTTPException(status_code=422, detail=STOCK_OUT_OF_STOCK_ERROR) 30 | db_stock.reduce_quantity(quantity) 31 | db.commit() 32 | db.refresh(db_stock) 33 | return db_stock 34 | 35 | 36 | def get_stocks(db: Session, skip: int = 0, limit: int = 100): 37 | return db.query(models.Stock).offset(skip).limit(limit).all() 38 | 39 | 40 | def remove_stock(db: Session, db_stock: models.Stock): 41 | db.delete(db_stock) 42 | db.commit() 43 | return True 44 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/test/templates/sale_tempĺates.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from datetime import datetime 4 | 5 | 6 | @pytest.fixture 7 | def sale_request_json(): 8 | return { 9 | "id": 1, 10 | "car_id": 1, 11 | "seller_id": 1, 12 | "buyer_id": 1, 13 | "created_at": str(datetime.utcnow()) 14 | } 15 | 16 | 17 | @pytest.fixture 18 | def sale_response_json(): 19 | return { 20 | "id": 1, 21 | "car": { 22 | "id": 1, 23 | "name": "Galardo", 24 | "year": 1999, 25 | "brand": "lamborghini" 26 | }, 27 | "buyer": { 28 | "id": 1, 29 | "name": "Bruce Lee", 30 | "address": { 31 | "cep": "73770-000", 32 | "public_place": "Banbusal", 33 | "city": "Alto Paraiso de Goias", 34 | "district": "Cidade Baixa", 35 | "state": "Goias" 36 | }, 37 | "phone": "12996651234" 38 | }, 39 | "seller": { 40 | "id": 1, 41 | "name": "João da Silva", 42 | "cpf": "69285717640", 43 | "phone": "1299871234" 44 | }, 45 | "created_at": None 46 | } 47 | 48 | 49 | @pytest.fixture 50 | def sale_not_found_error(): 51 | return { "errors": ["sale does not exist"] } 52 | 53 | 54 | @pytest.fixture 55 | def sale_all_not_found_error(): 56 | return {'errors': ['car does not exist, buyer does not exist, seller does not exist, stock does not exist']} 57 | 58 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/routers/users.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fastapi import APIRouter, Depends, HTTPException 4 | 5 | from sqlalchemy.orm import Session 6 | 7 | from ..dependencies import get_db 8 | 9 | from ..domain.user import service, schemas 10 | 11 | 12 | router = APIRouter(tags=["users"]) 13 | 14 | @router.post("/users/", response_model=schemas.User) 15 | def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): 16 | db_user = service.get_user_by_email(db, email=user.email) 17 | if db_user: 18 | raise HTTPException(status_code=400, detail="Email already registered") 19 | return service.create_user(db=db, user=user) 20 | 21 | @router.get("/users/", response_model=List[schemas.User]) 22 | def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): 23 | users = service.get_users(db, skip=skip, limit=limit) 24 | return users 25 | 26 | @router.get("/users/{user_id}", response_model=schemas.User) 27 | def read_user(user_id: int, db: Session = Depends(get_db)): 28 | db_user = service.get_user(db, user_id=user_id) 29 | if db_user is None: 30 | raise HTTPException(status_code=404, detail="User not found") 31 | return db_user 32 | 33 | @router.post("/users/{user_id}/items/", response_model=schemas.Item) 34 | def create_item_for_user( 35 | user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db) 36 | ): 37 | return service.create_user_item(db=db, item=item, user_id=user_id) 38 | -------------------------------------------------------------------------------- /clientside_v2/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | # This docker-compose will help you spawn all dependencies for examples/server.py 4 | 5 | services: 6 | # Jaeger is needed to ensure you have somewhere to store all emitted metrics + provides nice UI 7 | jaeger: 8 | image: jaegertracing/all-in-one:1.57 9 | environment: 10 | - COLLECTOR_ZIPKIN_HOST_PORT=:9411 11 | ports: 12 | - "6831:6831/udp" # Thrift compact 13 | - "6832:6832/udp" # Thrift binary 14 | - "5778:5778" # Agent HTTP 15 | - "16686:16686" # Jaeger UI 16 | - "4317:4317" # OTLP gRPC 17 | - "4318:4318" # OTLP HTTP 18 | - "14250:14250" # OTLP HTTP (alternative) 19 | - "14268:14268" # Jaeger HTTP thrift 20 | - "14269:14269" # Jaeger gRPC 21 | - "9411:9411" # Zipkin 22 | 23 | # Redis is one of the instrumented libraries that is tested inside examples/server.py 24 | redis: 25 | image: redis:latest 26 | ports: 27 | - "6379:6379" 28 | volumes: 29 | - redis-data:/data 30 | command: ["redis-server", "--appendonly", "yes", "--appendfsync", "always"] 31 | restart: always 32 | 33 | postgres: 34 | image: postgres:13 35 | environment: 36 | POSTGRES_USER: user 37 | POSTGRES_PASSWORD: password 38 | POSTGRES_DB: testdb 39 | ports: 40 | - "5432:5432" 41 | volumes: 42 | - postgres-data:/var/lib/postgresql/data 43 | restart: always 44 | 45 | volumes: 46 | redis-data: 47 | driver: local 48 | postgres-data: 49 | driver: local 50 | -------------------------------------------------------------------------------- /serverside/README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | The server-side component of CaptureFlow provides an HTTP API for integrating with the client-side library. It supports: 4 | 5 | - `/api/v1/traces`: For storing traces generated by the client-side library. 6 | - `/api/v1/merge-requests/bugfix`: Initiates server actions to process traces with exception events, analyzes the CallGraph, enriches it with GitHub implementation context, and leverages AI to generate a fix in the form of a merge request (MR). 7 | 8 | ## Running Locally 9 | 10 | ### Dev Setup 11 | 12 | For the development setup, ensure you have Python 3.8+, `venv`, and have installed the dependencies listed in `requirements-dev.txt`. Then, run `pytest` to check if the setup was successful. 13 | 14 | ### Prerequisites 15 | 16 | To run the server-side component locally, you'll need: 17 | 18 | 1. **Docker Compose**: Essential for hosting the server-side application logic. 19 | 2. **GitHub App Instance**: Necessary for generating improvement MRs. Follow the detailed guide [here](https://github.com/CaptureFlow/captureflow-py/blob/main/assets/creating-github-app.md) to set up your GitHub app instance. You will need the `APP_ID` and `PRIVATE_KEY`. 20 | 3. **OpenAI Key**: Essential for code generation. 21 | 22 | ### Configuration 23 | 24 | After securing your prerequisites, add your secrets into the [config.py](https://github.com/CaptureFlow/captureflow-py/blob/main/serverside/src/config.py). 25 | 26 | Alternatively, you can place them in a `.env` file inside the `serverside/` folder for running locally. 27 | 28 | ### Running 29 | 30 | Launch the server with: `docker compose up --build` -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/test/config/database_test_config.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm import sessionmaker 3 | 4 | from ...src.database import Base 5 | 6 | from ...src.dependencies import get_db 7 | 8 | ## Configure SQLite embedded for file "test.db" 9 | SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" 10 | 11 | engine = create_engine( 12 | SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} 13 | ) 14 | 15 | TestingSessionLocal = sessionmaker( 16 | autocommit=False, autoflush=False, bind=engine) 17 | 18 | 19 | def override_get_db(): 20 | ''' Method for override database default configuration ''' 21 | try: 22 | db = TestingSessionLocal() 23 | yield db 24 | finally: 25 | db.close() 26 | 27 | 28 | def configure_test_database(app): 29 | ''' Override default database for test embedded database ''' 30 | 31 | Base.metadata.create_all(bind=engine) 32 | 33 | app.dependency_overrides[get_db] = override_get_db 34 | 35 | 36 | def truncate_tables(tables): 37 | ''' Truncate rows of all input tables ''' 38 | 39 | with engine.connect() as con: 40 | 41 | IGNORE_CONSTRAINTS = """PRAGMA ignore_check_constraints = 0""" 42 | DISABLE_IGNORE_CONSTRAINTS = """PRAGMA ignore_check_constraints = 1""" 43 | 44 | statement = """DELETE FROM {table:s}""" 45 | 46 | con.execute(IGNORE_CONSTRAINTS) 47 | for line in tables: 48 | con.execute(statement.format(table = line)) 49 | con.execute(DISABLE_IGNORE_CONSTRAINTS) 50 | 51 | -------------------------------------------------------------------------------- /clientside/examples/fastapi/server.py: -------------------------------------------------------------------------------- 1 | import utilz 2 | from fastapi import FastAPI, HTTPException 3 | from pydantic import BaseModel 4 | 5 | from captureflow.tracer import Tracer 6 | 7 | tracer = Tracer( 8 | repo_url="https://github.com/CaptureFlow/captureflow-py", 9 | server_base_url="http://127.0.0.1:8000", 10 | ) 11 | 12 | app = FastAPI() 13 | 14 | 15 | class Transaction(BaseModel): 16 | user_id: str 17 | company_id: str 18 | amount: float 19 | 20 | 21 | @app.on_event("startup") 22 | async def startup_event(): 23 | """Initialize the database on startup.""" 24 | utilz.init_db() 25 | 26 | 27 | @app.post("/score_transaction/") 28 | @tracer.trace_endpoint 29 | async def score_transaction(transaction: Transaction): 30 | """ 31 | Scores a given transaction for fraud potential based on amount similarity to the last 5 transactions for the same company_id. 32 | 33 | ## cURL examples: 34 | ``` 35 | curl -X 'POST' 'http://127.0.0.1:1337/score_transaction/' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"user_id": "user123", "company_id": "company456", "amount": 100.0}' 36 | ``` 37 | """ 38 | score = utilz.calculate_score(transaction.user_id, transaction.company_id, transaction.amount) 39 | try: 40 | utilz.add_transaction(transaction.user_id, transaction.company_id, transaction.amount, score) 41 | except Exception as e: 42 | raise HTTPException(status_code=500, detail=str(e)) 43 | return { 44 | "user_id": transaction.user_id, 45 | "company_id": transaction.company_id, 46 | "amount": transaction.amount, 47 | "score": score, 48 | } 49 | -------------------------------------------------------------------------------- /clientside_v2/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import time 3 | 4 | import pytest 5 | import requests 6 | 7 | 8 | @pytest.fixture(scope="session", autouse=True) 9 | def start_docker_compose(): 10 | """ 11 | All the custom instrumentations require docker contains running (e.g. postgres, redis, etc). 12 | Also, make sure Docker itself is running. 13 | """ 14 | 15 | def is_jaeger_running(): 16 | """ 17 | Checking if docker compose is already running. 18 | Currently, check is simplified to just checking "jaeger" container, that runs on :16686 port 19 | """ 20 | try: 21 | response = requests.get("http://localhost:16686") 22 | return response.status_code == 200 23 | except requests.ConnectionError: 24 | return False 25 | 26 | if is_jaeger_running(): 27 | print("Jaeger is already running, skipping docker compose start.") 28 | else: 29 | subprocess.run(["docker", "compose", "up", "-d"], check=True) 30 | 31 | # Wait for Jaeger to be available 32 | for _ in range(10): 33 | if is_jaeger_running(): 34 | print("Docker compose started successfully.") 35 | break 36 | time.sleep(2) 37 | else: 38 | print("Docker compose did not start in time") 39 | 40 | yield 41 | 42 | # OpenTelemetry exports some additional traces after test suite completes 43 | # To allow for that, adding a delay to make sure test container can consume it 44 | print("Waiting for traces to be sent...") 45 | time.sleep(7) 46 | subprocess.run(["docker", "compose", "down"], check=True) 47 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/routers/buyers.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fastapi import APIRouter, Depends, HTTPException 4 | 5 | from sqlalchemy.orm import Session 6 | 7 | from ..dependencies import get_db 8 | 9 | from ..domain.buyer import service, schemas 10 | 11 | from .converter.buyer_converter import convert, convert_many 12 | 13 | from ...resources.strings import BUYER_DOES_NOT_EXIST_ERROR 14 | 15 | 16 | router = APIRouter( 17 | prefix="/buyers", 18 | tags=["buyers"], 19 | dependencies=[], 20 | responses={404: {"description": "Not found"}}, 21 | ) 22 | 23 | 24 | @router.post("/", response_model=schemas.Buyer, status_code=201) 25 | def create_buyer(buyer: schemas.BuyerCreate, db: Session = Depends(get_db)): 26 | return convert(service.create_buyer(db=db, buyer=buyer)) 27 | 28 | 29 | @router.get("/{buyer_id}", response_model=schemas.Buyer) 30 | def read_buyer(buyer_id: int, db: Session = Depends(get_db)): 31 | db_buyer = service.get_buyer(db, buyer_id=buyer_id) 32 | if db_buyer is None: 33 | raise HTTPException(status_code=404, detail=BUYER_DOES_NOT_EXIST_ERROR) 34 | return convert(db_buyer) 35 | 36 | 37 | @router.get("/", response_model=List[schemas.Buyer]) 38 | def read_buyers(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): 39 | buyers = service.get_buyers(db, skip=skip, limit=limit) 40 | return convert_many(buyers) 41 | 42 | 43 | @router.delete("/{buyer_id}", response_model=bool) 44 | def delete_buyer(buyer_id: int, db: Session = Depends(get_db)): 45 | db_buyer = service.get_buyer(db, buyer_id=buyer_id) 46 | if db_buyer is None: 47 | raise HTTPException(status_code=404, detail=BUYER_DOES_NOT_EXIST_ERROR) 48 | return service.remove_buyer(db, db_buyer=db_buyer) 49 | 50 | -------------------------------------------------------------------------------- /clientside_v2/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "captureflow-agent" 3 | version = "0.0.16" 4 | description = "The CaptureFlow Tracer is a Python package crafted for in-depth tracing of function calls within Python applications. Its primary function is to capture and relay execution data to the CaptureFlow server-side system for decision making." 5 | authors = ["Nick Kutz "] 6 | readme = "README.md" 7 | packages = [{include = "captureflow"}] 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.9" 11 | opentelemetry-api = "^1.25.0" 12 | opentelemetry-instrumentation = "^0.46b0" 13 | opentelemetry-sdk = "^1.25.0" 14 | python-dotenv = "1.0.1" 15 | opentelemetry-exporter-otlp = "^1.25.0" 16 | opentelemetry-instrumentation-fastapi = "^0.46b0" 17 | opentelemetry-instrumentation-requests = "^0.46b0" 18 | opentelemetry-instrumentation-httpx = "^0.46b0" 19 | opentelemetry-instrumentation-flask = "^0.46b0" 20 | opentelemetry-instrumentation-sqlalchemy = "^0.46b0" 21 | opentelemetry-instrumentation-sqlite3 = "^0.46b0" 22 | SQLAlchemy = "^2.0.30" 23 | opentelemetry-instrumentation-dbapi = "^0.46b0" 24 | wrapt = "^1.16.0" 25 | opentelemetry-instrumentation-redis = "^0.46b0" 26 | sqlparse = "^0.5.1" 27 | 28 | [tool.poetry.dev-dependencies] 29 | fastapi = "^0.111.0" 30 | httpx = "^0.27.0" 31 | pytest = "^8.2.2" 32 | redis = "^5.0.6" 33 | black = "^24.4.2" 34 | isort = "^5.13.2" 35 | Flask = "^3.0.3" 36 | psycopg2-binary = "2.9.3" 37 | sqlparse = "^0.5.1" 38 | 39 | [build-system] 40 | requires = ["poetry-core"] 41 | build-backend = "poetry.core.masonry.api" 42 | 43 | [tool.poetry.plugins."opentelemetry_distro"] 44 | distro = "captureflow.distro:CaptureFlowDistro" 45 | 46 | [tool.black] 47 | line-length = 120 48 | target-version = ['py39'] 49 | include = '\.pyi?$' 50 | exclude = ''' 51 | /( 52 | \.git 53 | | \.hg 54 | | \.mypy_cache 55 | | \.tox 56 | | \.venv 57 | | _build 58 | | build 59 | | dist 60 | )/ 61 | ''' 62 | 63 | [tool.isort] 64 | profile = "black" 65 | -------------------------------------------------------------------------------- /serverside/tests/utils/test_call_graph.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | import pytest 5 | from src.utils.call_graph import CallGraph 6 | 7 | 8 | @pytest.fixture 9 | def sample_trace(): 10 | trace_path = Path(__file__).parent.parent / "assets" / "sample_trace.json" 11 | with open(trace_path) as f: 12 | return json.load(f) 13 | 14 | 15 | def test_call_graph_build(sample_trace): 16 | call_graph = CallGraph(json.dumps(sample_trace)) 17 | 18 | assert call_graph.graph.number_of_nodes() > 0 19 | assert call_graph.graph.number_of_edges() > 0 20 | 21 | # Assert specific function presence and properties 22 | function_nodes = call_graph.find_node_by_fname("calculate_avg") 23 | assert function_nodes 24 | 25 | for node_id in function_nodes: 26 | node = call_graph.graph.nodes[node_id] 27 | assert node["function"] == "calculate_avg" 28 | assert "arguments" in node 29 | assert "return_value" in node 30 | 31 | 32 | def test_call_graph_build_and_tags(sample_trace): 33 | call_graph = CallGraph(json.dumps(sample_trace)) 34 | 35 | assert call_graph.graph.number_of_nodes() > 0 36 | assert call_graph.graph.number_of_edges() > 0 37 | 38 | internal_nodes = call_graph.find_node_by_fname("calculate_avg") 39 | stdlib_nodes = call_graph.find_node_by_fname("iscoroutinefunction") 40 | 41 | # Ensure we found the nodes 42 | assert internal_nodes 43 | assert stdlib_nodes 44 | 45 | # Check we are able to differentiate between INTERNAL (interesting modules) and LIB modules (not-so-interesting) 46 | for node_id in internal_nodes: 47 | node = call_graph.graph.nodes[node_id] 48 | assert node["tag"] == "INTERNAL", f"Node {node_id} expected to be INTERNAL, got {node['tag']}" 49 | 50 | for node_id in stdlib_nodes: 51 | node = call_graph.graph.nodes[node_id] 52 | assert node["tag"] == "STDLIB", f"Node {node_id} expected to be STDLIB, got {node['tag']}" 53 | -------------------------------------------------------------------------------- /serverside/src/utils/resources/pytest_template.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import MagicMock, patch 2 | 3 | import pytest 4 | from fastapi.testclient import TestClient 5 | from your_application import app 6 | 7 | 8 | # Fixture for the TestClient to use with your FastAPI app 9 | @pytest.fixture 10 | def client(): 11 | """Provides a test client for the FastAPI application.""" 12 | with TestClient(app) as client: 13 | yield client 14 | 15 | 16 | # Fixture for dynamic test payloads 17 | @pytest.fixture 18 | def test_payload(): 19 | """Dynamically generates test payload data.""" 20 | return {"user_id": "example_user", "company_id": "example_company", "amount": 150.0} 21 | 22 | 23 | # Fixture to handle database setup and mocking 24 | @pytest.fixture 25 | def mock_db(): 26 | with patch("sqlite3.connect") as mock_connect: 27 | # Create a cursor object from a connection object 28 | mock_cursor = MagicMock() 29 | mock_connect.return_value.__enter__.return_value.cursor.return_value = mock_cursor 30 | yield mock_cursor 31 | 32 | 33 | # Test function for endpoints that involve database interactions 34 | def test_endpoint_with_db_interaction(client, test_payload, mock_db): 35 | mock_db.fetchall.return_value = [(50,), (60,), (70,), (80,), (90,)] 36 | 37 | response = client.post("/test_endpoint_path/", json=test_payload) 38 | 39 | assert response.status_code == 200 40 | assert response.json() == { 41 | "user_id": test_payload["user_id"], 42 | "company_id": test_payload["company_id"], 43 | "amount": test_payload["amount"], 44 | "score": test_payload["amount"] / sum([50, 60, 70, 80, 90]), # Example calculation 45 | } 46 | 47 | # That's how you would mock DB interactions 48 | mock_db.execute.assert_called_once_with( 49 | """ 50 | SELECT amount FROM transactions 51 | WHERE company_id = ? 52 | ORDER BY timestamp DESC 53 | LIMIT 5 54 | """, 55 | (test_payload["company_id"],), 56 | ) 57 | -------------------------------------------------------------------------------- /clientside_v2/tests/instrumentation/test_requests_instrumentation.py: -------------------------------------------------------------------------------- 1 | """ 2 | This test verifies that HTTP request spans include request and response details: 3 | 'http.request.method' in span.attributes 4 | 'http.request.url' in span.attributes 5 | 'http.request.body' in span.attributes 6 | 'http.response.status_code' in span.attributes 7 | 'http.response.body' in span.attributes 8 | """ 9 | 10 | import pytest 11 | import requests 12 | from fastapi import FastAPI 13 | from fastapi.testclient import TestClient 14 | 15 | app = FastAPI() 16 | 17 | 18 | @app.get("/external") 19 | async def call_external(): 20 | response = requests.get("https://jsonplaceholder.typicode.com/posts/1") 21 | return {"status_code": response.status_code, "body": response.json()} 22 | 23 | 24 | def test_requests_instrumentation(span_exporter): 25 | client = TestClient(app) 26 | 27 | # Make a request to the FastAPI server that calls an external HTTP service 28 | response = client.get("/external") 29 | assert response.status_code == 200 30 | 31 | # Retrieve the spans 32 | spans = span_exporter.get_finished_spans() 33 | http_spans = [span for span in spans if span.attributes.get("http.method") is not None] 34 | 35 | assert len(http_spans) >= 1, "Expected at least one HTTP span" 36 | 37 | external_call_span = None 38 | 39 | for span in http_spans: 40 | if span.attributes.get("http.url") == "https://jsonplaceholder.typicode.com/posts/1": 41 | external_call_span = span 42 | break 43 | 44 | assert external_call_span is not None, "External call span not found" 45 | 46 | # Validate external call span 47 | assert external_call_span.attributes["http.method"] == "GET" 48 | assert external_call_span.attributes["http.url"] == "https://jsonplaceholder.typicode.com/posts/1" 49 | assert external_call_span.attributes["http.status_code"] == 200 50 | assert "http.response.body" in external_call_span.attributes 51 | 52 | 53 | if __name__ == "__main__": 54 | pytest.main([__file__]) 55 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/routers/sellers.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fastapi import APIRouter, Depends, HTTPException 4 | 5 | from sqlalchemy.orm import Session 6 | 7 | from ..dependencies import get_db 8 | 9 | from ..domain.seller import service, schemas 10 | 11 | from ...resources.strings import SELLER_DOES_NOT_EXIST_ERROR 12 | 13 | 14 | router = APIRouter( 15 | prefix="/sellers", 16 | tags=["sellers"], 17 | dependencies=[], 18 | responses={404: {"description": "Not found"}}, 19 | ) 20 | 21 | @router.post("/", response_model=schemas.Seller, status_code=201) 22 | def create_seller(seller: schemas.SellerCreate, db: Session = Depends(get_db)): 23 | return service.create_seller(db=db, seller=seller) 24 | 25 | @router.get("/{seller_id}", response_model=schemas.Seller) 26 | def read_seller(seller_id: int, db: Session = Depends(get_db)): 27 | db_seller = service.get_seller(db, seller_id=seller_id) 28 | if db_seller is None: 29 | raise HTTPException(status_code=404, detail=SELLER_DOES_NOT_EXIST_ERROR) 30 | return db_seller 31 | 32 | @router.get("/cpf/{seller_cpf}", response_model=schemas.Seller) 33 | def read_seller_by_cpf(seller_cpf: str, db: Session = Depends(get_db)): 34 | db_seller = service.get_by_cpf(db, seller_cpf=seller_cpf) 35 | if db_seller is None: 36 | raise HTTPException(status_code=404, detail=SELLER_DOES_NOT_EXIST_ERROR) 37 | return db_seller 38 | 39 | @router.get("/", response_model=List[schemas.Seller]) 40 | def read_sellers(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): 41 | sellers = service.get_sellers(db, skip=skip, limit=limit) 42 | return sellers 43 | 44 | @router.delete("/{seller_id}", response_model=bool) 45 | def delete_seller(seller_id: int, db: Session = Depends(get_db)): 46 | db_seller = service.get_seller(db, seller_id=seller_id) 47 | if db_seller is None: 48 | raise HTTPException(status_code=404, detail=SELLER_DOES_NOT_EXIST_ERROR) 49 | return service.remove_seller(db, db_seller=db_seller) 50 | -------------------------------------------------------------------------------- /assets/creating-github-app.md: -------------------------------------------------------------------------------- 1 | # Creating a GitHub App for CaptureFlow 2 | 3 | This guide provides step-by-step instructions for creating a GitHub App necessary for CaptureFlow, enabling the automation of improvement merge requests (MRs). You'll learn how to retrieve your `APP_ID` and `PRIVATE_KEY`, which are essential for configuring the CaptureFlow server-side component. 4 | 5 | ## Step 1: Create a New GitHub App 6 | 7 | 1. Navigate to your GitHub account settings. 8 | 2. In the left sidebar, click on **Developer settings**. 9 | 3. Select **GitHub Apps** and then click on the **New GitHub App** button. 10 | 4. Fill in the necessary details: 11 | - **GitHub App name**: Provide a unique name for your app. 12 | - **Homepage URL**: You can use the URL of your project or repository. 13 | - **Webhook**: Disable it, as it's not required for CaptureFlow. 14 | - **Repository permissions**: Set the permissions required by CaptureFlow, typically including: 15 | - **Contents**: `Read & write` for accessing and modifying code. 16 | - **Pull requests**: `Read & write` for creating and managing pull requests. 17 | 5. Scroll down and click on **Create GitHub App**. 18 | 19 | ## Step 2: Generate a Private Key 20 | 21 | After creating your GitHub App, you'll be redirected to the app's settings page. 22 | 23 | 1. Scroll down to the **Private keys** section. 24 | 2. Click on **Generate a private key**. 25 | 3. Once the key is generated, it will be automatically downloaded to your computer. This file contains your `PRIVATE_KEY`. 26 | 27 | ## Step 3: Retrieve Your App ID 28 | 29 | 1. On the same GitHub App settings page, find the **App ID** section at the top. 30 | 2. Note down the `App ID` displayed here. This is your `APP_ID`. 31 | 32 | ## Configuring CaptureFlow 33 | 34 | With your `APP_ID` and `PRIVATE_KEY` obtained, you're ready to configure the CaptureFlow server-side component. You need to set corresponding environment variables defined in [config.py](https://github.com/CaptureFlow/captureflow-py/blob/main/serverside/src/config.py). 35 | 36 | Yes, also, please base64 encode your private key :) 37 | -------------------------------------------------------------------------------- /clientside/examples/fastapi/utilz.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | DATABASE_URL = "db.sqlite3" 4 | 5 | 6 | def init_db(): 7 | with sqlite3.connect(DATABASE_URL) as conn: 8 | cursor = conn.cursor() 9 | cursor.execute( 10 | """ 11 | CREATE TABLE IF NOT EXISTS transactions ( 12 | id INTEGER PRIMARY KEY AUTOINCREMENT, 13 | user_id TEXT NOT NULL, 14 | company_id TEXT NOT NULL, 15 | amount REAL NOT NULL, 16 | timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, 17 | score REAL DEFAULT 0.0 18 | ); 19 | """ 20 | ) 21 | # Pre-populate the database with a transaction 22 | cursor.execute( 23 | "INSERT INTO transactions (user_id, company_id, amount, score) VALUES ('user123', 'company456', 100.0, 0.5)" 24 | ) 25 | conn.commit() 26 | 27 | 28 | def calculate_score(user_id: str, company_id: str, amount: float) -> float: 29 | # Intentional error trigger for a specific company_id 30 | if company_id == "errorTrigger": 31 | raise ValueError("Intentional Error Triggered") 32 | 33 | with sqlite3.connect(DATABASE_URL) as conn: 34 | cursor = conn.cursor() 35 | cursor.execute( 36 | """ 37 | SELECT amount FROM transactions 38 | WHERE company_id = ? 39 | ORDER BY timestamp DESC 40 | LIMIT 5 41 | """, 42 | (company_id,), 43 | ) 44 | past_amounts = cursor.fetchall() 45 | score = amount / sum([amt[0] for amt in past_amounts]) 46 | return score 47 | 48 | 49 | def add_transaction(user_id: str, company_id: str, amount: float, score: float): 50 | with sqlite3.connect(DATABASE_URL) as conn: 51 | cursor = conn.cursor() 52 | cursor.execute( 53 | """ 54 | INSERT INTO transactions (user_id, company_id, amount, score) 55 | VALUES (?, ?, ?, ?) 56 | """, 57 | (user_id, company_id, amount, score), 58 | ) 59 | conn.commit() 60 | -------------------------------------------------------------------------------- /clientside/src/ot_tracer.py: -------------------------------------------------------------------------------- 1 | # tracer.py 2 | 3 | from opentelemetry import trace 4 | from opentelemetry.sdk.trace import TracerProvider 5 | from opentelemetry.sdk.trace.export import SimpleSpanProcessor 6 | from opentelemetry.exporter.jaeger.thrift import JaegerExporter 7 | from opentelemetry.instrumentation.requests import RequestsInstrumentor 8 | from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor 9 | from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor 10 | from opentelemetry.instrumentation.pymongo import PymongoInstrumentor 11 | from functools import wraps 12 | import json 13 | 14 | # Configure OpenTelemetry 15 | def configure_tracer(): 16 | provider = TracerProvider() 17 | trace.set_tracer_provider(provider) 18 | 19 | # Configure Jaeger Exporter 20 | jaeger_exporter = JaegerExporter( 21 | agent_host_name='localhost', # Change to your Jaeger agent host 22 | agent_port=6831 # Change to your Jaeger agent port 23 | ) 24 | provider.add_span_processor(SimpleSpanProcessor(jaeger_exporter)) 25 | 26 | # Instrument libraries 27 | RequestsInstrumentor().instrument() 28 | HTTPXClientInstrumentor().instrument() 29 | Psycopg2Instrumentor().instrument() 30 | PymongoInstrumentor().instrument() 31 | 32 | # Decorator to trace function calls 33 | def trace_function(func): 34 | @wraps(func) 35 | def wrapper(*args, **kwargs): 36 | tracer = trace.get_tracer(__name__) 37 | with tracer.start_as_current_span(func.__name__) as span: 38 | span.set_attribute("args", json.dumps(args, default=str)) 39 | span.set_attribute("kwargs", json.dumps(kwargs, default=str)) 40 | try: 41 | result = func(*args, **kwargs) 42 | span.set_attribute("result", json.dumps(result, default=str)) 43 | return result 44 | except Exception as e: 45 | span.record_exception(e) 46 | span.set_attribute("error", True) 47 | raise 48 | return wrapper 49 | 50 | # Configure the tracer when the module is imported 51 | configure_tracer() 52 | -------------------------------------------------------------------------------- /clientside_v2/tests/instrumentation/test_flask_instrumentation.py: -------------------------------------------------------------------------------- 1 | """ 2 | This test verifies that Flask request spans include request and response details: 3 | 'http.request.method' in span.attributes 4 | 'http.request.url' in span.attributes 5 | 'http.request.body' in span.attributes 6 | 'http.response.status_code' in span.attributes 7 | 'http.response.body' in span.attributes 8 | """ 9 | 10 | import pytest 11 | import requests 12 | from flask import Flask, jsonify 13 | 14 | app = Flask(__name__) 15 | 16 | 17 | @app.route("/external", methods=["GET"]) 18 | def call_external(): 19 | response = requests.get("https://jsonplaceholder.typicode.com/posts/1") 20 | return jsonify({"status_code": response.status_code, "body": response.json()}) 21 | 22 | 23 | @pytest.fixture(scope="module") 24 | def client(): 25 | app.config["TESTING"] = True 26 | with app.test_client() as client: 27 | yield client 28 | 29 | 30 | def test_flask_instrumentation(span_exporter, client): 31 | # Make a request to the Flask server that calls an external HTTP service 32 | response = client.get("/external") 33 | assert response.status_code == 200 34 | 35 | # Retrieve the spans 36 | spans = span_exporter.get_finished_spans() 37 | flask_spans = [span for span in spans if span.name.startswith("HTTP GET /external")] 38 | 39 | # Debug: Print all spans 40 | print("All spans:") 41 | for span in spans: 42 | print(f"Span name: {span.name}, Kind: {span.kind}, Attributes: {span.attributes}") 43 | 44 | assert len(flask_spans) == 1, "Expected at least one Flask span" 45 | 46 | flask_span = flask_spans[0] 47 | 48 | # Validate Flask span 49 | assert flask_span.attributes["http.method"] == "GET" 50 | assert flask_span.attributes["http.url"].endswith("/external") 51 | assert "http.request.headers" in flask_span.attributes 52 | assert flask_span.attributes["http.status_code"] == 200 53 | assert "http.response.headers" in flask_span.attributes 54 | assert "http.response.body" in flask_span.attributes 55 | 56 | response_body = flask_span.attributes["http.response.body"] 57 | assert "status_code" in response_body 58 | assert "body" in response_body 59 | 60 | 61 | if __name__ == "__main__": 62 | pytest.main([__file__]) 63 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends, FastAPI, Request, Response 2 | 3 | from fastapi.middleware.cors import CORSMiddleware 4 | 5 | from starlette.exceptions import HTTPException 6 | 7 | from .src.dependencies import get_query_token, get_token_header 8 | 9 | from .src.internal import admin 10 | 11 | from .src.routers.api import router as router_api 12 | 13 | from .src.database import engine, SessionLocal, Base 14 | 15 | from .src.config import API_PREFIX, ALLOWED_HOSTS 16 | 17 | from .src.routers.handlers.http_error import http_error_handler 18 | 19 | 20 | ### 21 | # Main application file 22 | ### 23 | 24 | def get_application() -> FastAPI: 25 | ''' Configure, start and return the application ''' 26 | 27 | ## Start FastApi App 28 | application = FastAPI() 29 | 30 | ## Generate database tables 31 | Base.metadata.create_all(bind=engine) 32 | 33 | ## Mapping api routes 34 | application.include_router(router_api, prefix=API_PREFIX) 35 | 36 | ## Add exception handlers 37 | application.add_exception_handler(HTTPException, http_error_handler) 38 | 39 | ## Allow cors 40 | application.add_middleware( 41 | CORSMiddleware, 42 | allow_origins=ALLOWED_HOSTS or ["*"], 43 | allow_credentials=True, 44 | allow_methods=["*"], 45 | allow_headers=["*"], 46 | ) 47 | 48 | ## Example of admin route 49 | application.include_router( 50 | admin.router, 51 | prefix="/admin", 52 | tags=["admin"], 53 | dependencies=[Depends(get_token_header)], 54 | responses={418: {"description": "I'm a teapot"}}, 55 | ) 56 | 57 | return application 58 | 59 | 60 | app = get_application() 61 | 62 | 63 | @app.middleware("http") 64 | async def db_session_middleware(request: Request, call_next): 65 | ''' 66 | The middleware we'll add (just a function) will create 67 | a new SQLAlchemy SessionLocal for each request, add it to 68 | the request and then close it once the request is finished. 69 | ''' 70 | response = Response("Internal server error", status_code=500) 71 | try: 72 | request.state.db = SessionLocal() 73 | response = await call_next(request) 74 | finally: 75 | request.state.db.close() 76 | return response 77 | 78 | -------------------------------------------------------------------------------- /clientside_v2/examples/server.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | 3 | import httpx 4 | import requests 5 | from fastapi import FastAPI 6 | from sqlalchemy_init import Item, get_db 7 | 8 | app = FastAPI() 9 | 10 | 11 | def external_call(): 12 | response = requests.get("https://jsonplaceholder.typicode.com/posts/1") 13 | return response.json() 14 | 15 | 16 | async def external_call_httpx(): 17 | async with httpx.AsyncClient() as client: 18 | response = await client.get("https://jsonplaceholder.typicode.com/posts/2") 19 | return response.json() 20 | 21 | 22 | async def external_post_call_httpx(): 23 | async with httpx.AsyncClient() as client: 24 | response = await client.post( 25 | "https://jsonplaceholder.typicode.com/posts", json={"title": "foo", "body": "bar", "userId": 1} 26 | ) 27 | return response.json() 28 | 29 | 30 | @contextmanager 31 | def get_db_session(): 32 | db = next(get_db()) 33 | try: 34 | yield db 35 | finally: 36 | db.close() 37 | 38 | 39 | def perform_database_operations(): 40 | with get_db_session() as db: 41 | new_item = Item(name="Test Item", description="This is a test item") 42 | db.add(new_item) 43 | db.commit() 44 | db.refresh(new_item) 45 | items = db.query(Item).all() 46 | return items 47 | 48 | 49 | def perform_redis_operations(): 50 | import redis 51 | 52 | redis_client = redis.Redis(host="localhost", port=6379, db=0) 53 | redis_client.set("test_key", "test_value") 54 | value = redis_client.get("test_key") 55 | return value.decode("utf-8") if value else None 56 | 57 | 58 | @app.get("/") 59 | async def read_root(): 60 | # [HTTP API] Requests invocation 61 | data = external_call() 62 | 63 | # [HTTP API] Httpx invocation 64 | data_post_httpx = await external_post_call_httpx() 65 | data_get_httpx = await external_call_httpx() 66 | 67 | # [DB] SQLite invocation 68 | data_sqlite = perform_database_operations() 69 | 70 | # [DB] Redis invocation 71 | data_redis = perform_redis_operations() 72 | 73 | return { 74 | "message": "Hello World", 75 | "data": data, 76 | "items": [{"id": item.id, "name": item.name, "description": item.description} for item in data_sqlite], 77 | } 78 | 79 | 80 | @app.get("/items/{item_id}") 81 | async def read_item(item_id: int, q: str = None): 82 | return {"item_id": item_id, "q": q} 83 | -------------------------------------------------------------------------------- /clientside_v2/captureflow/span_processor.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from logging import getLogger 3 | from pathlib import Path 4 | from types import CodeType, FrameType 5 | 6 | import opentelemetry 7 | from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor 8 | 9 | import captureflow 10 | 11 | logger = getLogger(__name__) 12 | 13 | # Set up constants for determining user code 14 | CWD = Path(".").resolve() 15 | PREFIXES = ( 16 | str(Path(opentelemetry.sdk.trace.__file__).parent.parent.parent.parent), 17 | str(Path(inspect.__file__).parent), 18 | str(Path(captureflow.__file__).parent), 19 | ) 20 | 21 | 22 | def get_stack_info_from_frame(frame: FrameType): 23 | """ 24 | Extract file path, function name, and line number from a frame. 25 | """ 26 | code = frame.f_code 27 | info = {"code.filepath": get_relative_filepath(code.co_filename)} 28 | if code.co_name != "": 29 | info["code.function"] = code.co_name 30 | info["code.lineno"] = frame.f_lineno 31 | return info 32 | 33 | 34 | def get_relative_filepath(file: str) -> str: 35 | """ 36 | Convert absolute file path to relative path from CWD if possible. 37 | """ 38 | path = Path(file) 39 | try: 40 | return str(path.relative_to(CWD)) 41 | except ValueError: 42 | return str(path) 43 | 44 | 45 | def is_user_code(code: CodeType) -> bool: 46 | """ 47 | Determine if a code object is from user code. 48 | """ 49 | return not any(str(Path(code.co_filename).absolute()).startswith(prefix) for prefix in PREFIXES) 50 | 51 | 52 | def get_user_stack_info(): 53 | """ 54 | Get the stack info for the first calling frame in user code. 55 | """ 56 | frame = inspect.currentframe() 57 | while frame: 58 | if is_user_code(frame.f_code): 59 | return get_stack_info_from_frame(frame) 60 | frame = frame.f_back 61 | return {} 62 | 63 | 64 | class FrameInfoSpanProcessor(SpanProcessor): 65 | def on_start(self, span: Span, parent_context): 66 | """ 67 | Add user stack info attributes to the span when it starts. 68 | """ 69 | stack_info = get_user_stack_info() 70 | for key, value in stack_info.items(): 71 | span.set_attribute(key, value) 72 | 73 | def on_end(self, span: ReadableSpan): 74 | pass 75 | 76 | def shutdown(self): 77 | pass 78 | 79 | def force_flush(self, timeout_millis: int = 30000): 80 | pass 81 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/routers/stocks.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fastapi import APIRouter, Depends, HTTPException 4 | 5 | from sqlalchemy.orm import Session 6 | 7 | from ..dependencies import get_db, get_token_header 8 | 9 | from ..domain.stock import service, schemas 10 | 11 | from ..domain.car import service as car_service 12 | 13 | from ...resources.strings import STOCK_DOES_NOT_EXIST_ERROR 14 | from ...resources.strings import STOCK_ALREADY_EXISTS_ERROR 15 | from ...resources.strings import CAR_DOES_NOT_EXIST_ERROR 16 | 17 | 18 | router = APIRouter( 19 | prefix="/stocks", 20 | tags=["stocks"], 21 | dependencies=[], 22 | responses={404: {"description": "Not found"}}, 23 | ) 24 | 25 | @router.post("/", response_model=schemas.Stock, status_code=201) 26 | def create_stock(stock: schemas.StockCreate, db: Session = Depends(get_db)): 27 | if car_service.get_car(db, car_id=stock.car_id) is None: 28 | raise HTTPException(status_code=404, detail=CAR_DOES_NOT_EXIST_ERROR) 29 | if service.get_stock_by_car(db, car_id=stock.car_id): 30 | raise HTTPException(status_code=422, detail=STOCK_ALREADY_EXISTS_ERROR) 31 | return service.create_stock(db=db, stock=stock) 32 | 33 | 34 | @router.get("/{stock_id}", response_model=schemas.Stock) 35 | def read_stock(stock_id: int, db: Session = Depends(get_db)): 36 | db_stock = service.get_stock(db, stock_id=stock_id) 37 | if db_stock is None: 38 | raise HTTPException(status_code=404, detail=STOCK_DOES_NOT_EXIST_ERROR) 39 | return service.get_stock(db, stock_id=stock_id) 40 | 41 | 42 | @router.get("/cars/{car_id}", response_model=schemas.Stock) 43 | def read_stock_by_car(car_id: int, db: Session = Depends(get_db)): 44 | db_stock = service.get_stock_by_car(db, car_id=car_id) 45 | if db_stock is None: 46 | raise HTTPException(status_code=404, detail=STOCK_DOES_NOT_EXIST_ERROR) 47 | return service.get_stock_by_car(db, car_id=car_id) 48 | 49 | 50 | @router.get("/", response_model=List[schemas.Stock]) 51 | def read_stocks(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): 52 | return service.get_stocks(db, skip=skip, limit=limit) 53 | 54 | 55 | @router.delete("/{stock_id}", response_model=bool) 56 | def delete_stock(stock_id: int, db: Session = Depends(get_db)): 57 | db_stock = service.get_stock(db, stock_id=stock_id) 58 | if db_stock is None: 59 | raise HTTPException(status_code=404, detail=STOCK_DOES_NOT_EXIST_ERROR) 60 | return service.remove_stock(db, db_stock=db_stock) 61 | 62 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/test/test_api/test_cars.py: -------------------------------------------------------------------------------- 1 | from fastapi.testclient import TestClient 2 | 3 | from ..database_test import configure_test_database, clear_database 4 | 5 | from ..base_insertion import insert_into_cars 6 | 7 | from ..templates.car_tempĺates import car_json, car_not_found_error 8 | 9 | from ...main import app 10 | 11 | 12 | CAR_ROUTE = "/api/v1/cars" 13 | 14 | 15 | client = TestClient(app) 16 | 17 | 18 | def setup_module(module): 19 | configure_test_database(app) 20 | 21 | 22 | def setup_function(module): 23 | clear_database() 24 | 25 | 26 | def test_create_car(car_json): 27 | ''' Create a car with success ''' 28 | response = client.post(CAR_ROUTE + "/", json=car_json) 29 | assert response.status_code == 201 30 | assert response.json() == car_json 31 | 32 | 33 | def test_read_car(car_json): 34 | ''' Read a car with success ''' 35 | insert_into_cars(car_json) 36 | 37 | request_url = CAR_ROUTE + "/1" 38 | response = client.get(request_url) 39 | assert response.status_code == 200 40 | assert response.json() == car_json 41 | 42 | 43 | def test_read_cars(car_json): 44 | ''' Read all cars paginated with success ''' 45 | insert_into_cars(car_json) 46 | 47 | request_url = CAR_ROUTE + "?skip=0&limit=100" 48 | response = client.get(request_url) 49 | assert response.status_code == 200 50 | assert response.json() == [ car_json ] 51 | 52 | 53 | def test_delete_car(car_json): 54 | ''' Delete a car with success ''' 55 | insert_into_cars(car_json) 56 | 57 | request_url = CAR_ROUTE + "/1" 58 | response = client.delete(request_url) 59 | assert response.status_code == 200 60 | assert response.json() == True 61 | 62 | 63 | def test_read_car_not_found(car_not_found_error): 64 | ''' Read a car when not found ''' 65 | request_url = CAR_ROUTE + "/1" 66 | response = client.get(request_url) 67 | assert response.status_code == 404 68 | assert response.json() == car_not_found_error 69 | 70 | 71 | def test_read_cars_not_found(): 72 | ''' Read all cars paginated when not found ''' 73 | request_url = CAR_ROUTE + "?skip=0&limit=100" 74 | response = client.get(request_url) 75 | assert response.status_code == 200 76 | assert response.json() == [] 77 | 78 | 79 | def test_delete_car_not_found(car_not_found_error): 80 | ''' Delete a car when not exists ''' 81 | request_url = CAR_ROUTE + "/1" 82 | response = client.delete(request_url) 83 | assert response.status_code == 404 84 | assert response.json() == car_not_found_error 85 | 86 | -------------------------------------------------------------------------------- /clientside/README.md: -------------------------------------------------------------------------------- 1 | # CaptureFlow Tracer 2 | 3 | ## Overview 4 | 5 | The CaptureFlow Tracer is a Python package crafted for in-depth tracing of function calls within Python applications. Its primary function is to capture and relay execution data to the CaptureFlow server-side system for decision making. 6 | 7 | ## Performance Considerations 8 | 9 | The current implementation of the Tracer module utilizes the `sys.settrace` module ([Python Doc](https://docs.python.org/3/library/sys.html#sys.settrace)), leading to two outcomes: 10 | 11 | - **Detailed Logging**: Captures verbose logs with debugger-level insight into method calls, variable states, and more. 12 | - **Performance Impact**: The logging method significantly affects application performance. 13 | 14 | ## Building and Testing Locally 15 | 16 | To begin, ensure you have Python 3.8+, `venv`, and the `requirements-dev.txt` installed. 17 | 18 | Explore the `clientside/examples` directory for sample applications suitable for testing. For local package building with source code, each folder contains an `./install_package.sh` script. After building, run a local server and cURL some endpoints to simulate a real-life workflow. 19 | 20 | If the environment variable `CAPTUREFLOW_DEV_SERVER` is set to `"true"`, trace JSON objects will be dumped locally for further inspection. Additionally, running the `serverside` Docker container allows you to observe trace data ingestion in real-time. 21 | 22 | For an **example of the data structure** captured by the tracer, you can check [sample_trace_with_exception.json](https://github.com/CaptureFlow/captureflow-py/blob/main/serverside/tests/assets/sample_trace_with_exception.json). 23 | 24 | ## Roadmap for Optimization 25 | 26 | We need to balancing trace detail with performance, planning enhancements for the Tracer's efficiency: 27 | 28 | #### Short-Term Objectives 29 | 30 | - **Selective Tracing**: Implement a "verbosity" parameter for targeted tracing, focusing on relevant trace events. For example, we need to "sample" a representative subset of requests instead of capturing all of them. 31 | - **Framework Integration and Middleware Enhancements**: 32 | - Research if it's feasible to develop middleware/patching for popular frameworks to ship data of similar detalization. 33 | - Explore integration with tracing standards like OpenTelemetry for better application monitoring. 34 | 35 | #### Long-Term Objectives 36 | 37 | - **Performance vs. Data Utility for CodeGen**: Focus on collecting essential data for high-quality server-side improvements. We're pinpointing the specific data subset required to minimize performance disruption while maximizing optimization quality. -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/test/test_api/test_buyers.py: -------------------------------------------------------------------------------- 1 | from fastapi.testclient import TestClient 2 | 3 | from ..database_test import configure_test_database, clear_database 4 | 5 | from ..base_insertion import insert_into_buyers 6 | 7 | from ..templates.buyer_tempĺates import buyer_json, buyer_not_found_error 8 | 9 | from ...main import app 10 | 11 | 12 | client = TestClient(app) 13 | 14 | buyers_route = "/api/v1/buyers" 15 | 16 | 17 | def setup_module(module): 18 | configure_test_database(app) 19 | 20 | 21 | def setup_function(module): 22 | clear_database() 23 | 24 | 25 | def test_create_buyer(buyer_json): 26 | ''' Create a buyer with success ''' 27 | response = client.post(buyers_route + "/", json=buyer_json) 28 | assert response.status_code == 201 29 | assert response.json() == buyer_json 30 | 31 | 32 | def test_read_buyer(buyer_json): 33 | ''' Read a buyer with success ''' 34 | insert_into_buyers(buyer_json) 35 | request_url = buyers_route + "/1" 36 | response = client.get(request_url) 37 | assert response.status_code == 200 38 | assert response.json() == buyer_json 39 | 40 | 41 | def test_read_buyers(buyer_json): 42 | ''' Read all buyers paginated with success ''' 43 | insert_into_buyers(buyer_json) 44 | request_url = buyers_route + "?skip=0&limit=100" 45 | response = client.get(request_url) 46 | assert response.status_code == 200 47 | assert response.json() == [ buyer_json ] 48 | 49 | 50 | def test_delete_buyer(buyer_json): 51 | ''' Delete a buyer with success ''' 52 | insert_into_buyers(buyer_json) 53 | request_url = buyers_route + "/1" 54 | response = client.delete(request_url) 55 | assert response.status_code == 200 56 | assert response.json() == True 57 | 58 | 59 | def test_read_buyer_not_found(buyer_not_found_error): 60 | ''' Read a buyer when not found ''' 61 | request_url = buyers_route + "/1" 62 | response = client.get(request_url) 63 | assert response.status_code == 404 64 | assert response.json() == buyer_not_found_error 65 | 66 | 67 | def test_read_buyers_not_found(): 68 | ''' Read all buyers paginated when not found ''' 69 | request_url = buyers_route + "?skip=0&limit=100" 70 | response = client.get(request_url) 71 | assert response.status_code == 200 72 | assert response.json() == [] 73 | 74 | 75 | def test_delete_buyer_not_found(buyer_not_found_error): 76 | ''' Delete a buyer when not exists ''' 77 | request_url = buyers_route + "/1" 78 | response = client.delete(request_url) 79 | assert response.status_code == 404 80 | assert response.json() == buyer_not_found_error 81 | -------------------------------------------------------------------------------- /clientside_v2/tests/instrumentation/test_httpx_instrumentation.py: -------------------------------------------------------------------------------- 1 | """ 2 | This test verifies that HTTPX request spans include request and response details: 3 | 'http.request.method' in span.attributes 4 | 'http.request.url' in span.attributes 5 | 'http.request.body' in span.attributes 6 | 'http.response.status_code' in span.attributes 7 | 'http.response.body' in span.attributes 8 | """ 9 | 10 | import httpx 11 | import pytest 12 | from fastapi import FastAPI 13 | from fastapi.testclient import TestClient 14 | 15 | app = FastAPI() 16 | 17 | 18 | @app.get("/external") 19 | async def call_external(): 20 | async with httpx.AsyncClient() as client: 21 | response = await client.get("https://jsonplaceholder.typicode.com/posts/1") 22 | return {"status_code": response.status_code, "body": response.json()} 23 | 24 | 25 | def test_httpx_instrumentation(span_exporter): 26 | client = TestClient(app) 27 | 28 | # Make a request to the FastAPI server that calls an external HTTP service 29 | response = client.get("/external") 30 | assert response.status_code == 200 31 | 32 | # Retrieve the spans 33 | spans = span_exporter.get_finished_spans() 34 | print(f"Total spans: {len(spans)}") 35 | for span in spans: 36 | print(f"[HTTPX] Span name: {span.name}, Kind: {span.kind}, Attributes: {span.attributes}") 37 | 38 | http_spans = [span for span in spans if span.name.startswith("HTTP")] 39 | assert len(http_spans) == 1, "Expected at least one HTTP span" 40 | 41 | external_call_span = None 42 | 43 | for span in http_spans: 44 | if span.attributes.get("http.request.url") == "https://jsonplaceholder.typicode.com/posts/1": 45 | external_call_span = span 46 | break 47 | 48 | assert external_call_span is not None, "External call span not found" 49 | 50 | # Validate external call span 51 | assert external_call_span.attributes["http.request.method"] == "GET" 52 | assert external_call_span.attributes["http.request.url"] == "https://jsonplaceholder.typicode.com/posts/1" 53 | assert "http.request.headers" in external_call_span.attributes 54 | assert external_call_span.attributes["http.response.status_code"] == 200 55 | assert "http.response.headers" in external_call_span.attributes 56 | assert "http.response.body" in external_call_span.attributes 57 | 58 | # Optionally, you can add more specific checks for the content of the body 59 | response_body = external_call_span.attributes["http.response.body"] 60 | assert "userId" in response_body 61 | assert "id" in response_body 62 | assert "title" in response_body 63 | assert "body" in response_body 64 | 65 | 66 | if __name__ == "__main__": 67 | pytest.main([__file__]) 68 | 69 | 70 | if __name__ == "__main__": 71 | pytest.main([__file__]) 72 | -------------------------------------------------------------------------------- /clientside_v2/tests/instrumentation/test_redis_instrumentation.py: -------------------------------------------------------------------------------- 1 | """ 2 | This test verifies that Redis spans include command and arguments: 3 | 'redis.command' in span.attributes 4 | 'redis.command.args' in span.attributes 5 | """ 6 | 7 | import pytest 8 | import redis 9 | from fastapi import FastAPI 10 | from fastapi.testclient import TestClient 11 | 12 | app = FastAPI() 13 | redis_client = redis.Redis(host="localhost", port=6379, db=0) 14 | 15 | 16 | def perform_redis_operations(): 17 | # Two operations [SET, GET] for test 18 | redis_client.set("test_key", "test_value") 19 | value = redis_client.get("test_key") 20 | return value.decode("utf-8") if value else None 21 | 22 | 23 | @app.get("/") 24 | async def read_root(): 25 | # Redis operations 26 | redis_value = perform_redis_operations() 27 | return {"message": "Hello World", "redis_value": redis_value} 28 | 29 | 30 | def test_redis_instrumentation(span_exporter): 31 | client = TestClient(app) 32 | 33 | # Make a request to the FastAPI server 34 | response = client.get("/") 35 | assert response.status_code == 200 36 | assert response.json()["redis_value"] == "test_value" 37 | 38 | # Retrieve the spans 39 | spans = span_exporter.get_finished_spans() 40 | redis_spans = [span for span in spans if span.attributes.get("db.system") == "redis"] 41 | 42 | assert len(redis_spans) >= 2, "Expected at least two Redis spans" 43 | 44 | set_span = None 45 | get_span = None 46 | 47 | for span in redis_spans: 48 | if span.name == "SET": 49 | set_span = span 50 | elif span.name == "GET": 51 | get_span = span 52 | 53 | assert set_span is not None, "SET span not found" 54 | assert get_span is not None, "GET span not found" 55 | 56 | # Validate SET span 57 | assert set_span.name == "SET" 58 | assert set_span.attributes["redis.command"] == "SET" 59 | assert set_span.attributes["redis.command.args"] == "('test_key', 'test_value')" 60 | assert set_span.attributes["db.statement"] == "SET ? ?" 61 | assert set_span.attributes["db.system"] == "redis" 62 | assert set_span.attributes["net.peer.name"] == "localhost" 63 | assert set_span.attributes["net.peer.port"] == 6379 64 | assert set_span.attributes["redis.response"] == "True" 65 | 66 | # Validate GET span 67 | assert get_span.name == "GET" 68 | assert get_span.attributes["redis.command"] == "GET" 69 | assert get_span.attributes["redis.command.args"] == "('test_key',)" 70 | assert get_span.attributes["db.statement"] == "GET ?" 71 | assert get_span.attributes["db.system"] == "redis" 72 | assert get_span.attributes["net.peer.name"] == "localhost" 73 | assert get_span.attributes["net.peer.port"] == 6379 74 | assert get_span.attributes["redis.response"] == "b'test_value'" 75 | 76 | 77 | if __name__ == "__main__": 78 | pytest.main([__file__]) 79 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/src/routers/sales.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fastapi import APIRouter, Depends, HTTPException 4 | 5 | from sqlalchemy.orm import Session 6 | 7 | from ..dependencies import get_db 8 | 9 | from ..domain.sale import service, schemas 10 | 11 | from ..domain.car import repository as car_repository 12 | 13 | from ..domain.buyer import service as buyer_service 14 | 15 | from ..domain.seller import service as seller_service 16 | 17 | from ..domain.stock import service as stock_service 18 | 19 | from .converter import sale_converter 20 | 21 | from ...resources.strings import CAR_DOES_NOT_EXIST_ERROR 22 | from ...resources.strings import STOCK_DOES_NOT_EXIST_ERROR 23 | from ...resources.strings import BUYER_DOES_NOT_EXIST_ERROR 24 | from ...resources.strings import SELLER_DOES_NOT_EXIST_ERROR 25 | from ...resources.strings import SALES_DOES_NOT_EXIST_ERROR 26 | 27 | router = APIRouter( 28 | prefix="/sales", 29 | tags=["sales"], 30 | dependencies=[], 31 | responses={404: {"description": "Not found"}}, 32 | ) 33 | 34 | @router.post("/", response_model=schemas.Sale, status_code=201) 35 | def create_sale(sale: schemas.SaleCreate, db: Session = Depends(get_db)): 36 | errors = [] 37 | 38 | if car_repository.get_car(db, car_id=sale.car_id) is None: 39 | errors.append(CAR_DOES_NOT_EXIST_ERROR) 40 | if buyer_service.get_buyer(db, buyer_id=sale.buyer_id) is None: 41 | errors.append(BUYER_DOES_NOT_EXIST_ERROR) 42 | if seller_service.get_seller(db, seller_id=sale.seller_id) is None: 43 | errors.append(SELLER_DOES_NOT_EXIST_ERROR) 44 | if stock_service.get_stock_by_car(db, car_id=sale.car_id) is None: 45 | errors.append(STOCK_DOES_NOT_EXIST_ERROR) 46 | if len(errors) > 0: 47 | raise HTTPException(status_code=404, detail=", ".join(errors)) 48 | 49 | stock_service.buy_car_from_stock(db, car_id=sale.car_id, quantity=1) 50 | db_sale = service.create_sale(db=db, sale=sale) 51 | return sale_converter.convert(db_sale) 52 | 53 | @router.get("/{sale_id}", response_model=schemas.Sale) 54 | def read_sale(sale_id: int, db: Session = Depends(get_db)): 55 | db_sale = service.get_sale(db, sale_id=sale_id) 56 | if db_sale is None: 57 | raise HTTPException(status_code=404, detail=SALES_DOES_NOT_EXIST_ERROR) 58 | return sale_converter.convert(db_sale) 59 | 60 | @router.get("/", response_model=List[schemas.Sale]) 61 | def read_sales(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): 62 | sales = service.get_sales(db, skip=skip, limit=limit) 63 | return sale_converter.convert_many(sales) 64 | 65 | @router.delete("/{sale_id}", response_model=bool) 66 | def delete_sale(sale_id: int, db: Session = Depends(get_db)): 67 | db_sale = service.get_sale(db, sale_id=sale_id) 68 | if db_sale is None: 69 | raise HTTPException(status_code=404, detail=SALES_DOES_NOT_EXIST_ERROR) 70 | return service.remove_sale(db, db_sale=db_sale) 71 | 72 | -------------------------------------------------------------------------------- /examples/scenario.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import random 3 | 4 | BASE_URL = "http://localhost:9999" 5 | 6 | def create_user(email, password): 7 | response = requests.post(f"{BASE_URL}/api/login/", json={"email": email, "password": password}) 8 | print("RESPONSE = ", response.text) 9 | return response.json() 10 | 11 | def create_car(name, year, brand): 12 | response = requests.post(f"{BASE_URL}/api/v1/cars/", json={"name": name, "year": year, "brand": brand}) 13 | return response.json() 14 | 15 | def create_stock(car_id, quantity): 16 | response = requests.post(f"{BASE_URL}/api/v1/stocks/", json={"car_id": car_id, "quantity": quantity}) 17 | return response.json() 18 | 19 | def create_seller(name, cpf, phone): 20 | response = requests.post(f"{BASE_URL}/api/v1/sellers/", json={"name": name, "cpf": cpf, "phone": phone}) 21 | return response.json() 22 | 23 | def create_buyer(name, phone, address): 24 | response = requests.post(f"{BASE_URL}/api/v1/buyers/", json={"name": name, "phone": phone, "address": address}) 25 | return response.json() 26 | 27 | def create_sale(car_id, seller_id, buyer_id): 28 | response = requests.post(f"{BASE_URL}/api/v1/sales/", json={"car_id": car_id, "seller_id": seller_id, "buyer_id": buyer_id}) 29 | return response.json() 30 | 31 | def main(): 32 | # Create users 33 | users = [ 34 | create_user(f"user{i}@example.com", f"password{i}") for i in range(1, 6) 35 | ] 36 | print("Created users:", users) 37 | 38 | # Create cars 39 | cars = [ 40 | create_car("Sedan", 2022, "Toyota"), 41 | create_car("SUV", 2023, "Honda"), 42 | create_car("Hatchback", 2021, "Ford"), 43 | create_car("Truck", 2022, "Chevrolet"), 44 | create_car("Coupe", 2023, "BMW") 45 | ] 46 | print("Created cars:", cars) 47 | 48 | # Create stock for cars 49 | stocks = [create_stock(car['id'], random.randint(1, 10)) for car in cars] 50 | print("Created stocks:", stocks) 51 | 52 | # Create sellers 53 | sellers = [ 54 | create_seller(f"Seller {i}", f"CPF{i}", f"555-0000{i}") for i in range(1, 4) 55 | ] 56 | print("Created sellers:", sellers) 57 | 58 | # Create buyers with addresses 59 | addresses = [ 60 | { 61 | "cep": f"1234{i}", 62 | "public_place": f"Street {i}", 63 | "city": "New York", 64 | "district": f"District {i}", 65 | "state": "NY" 66 | } for i in range(1, 6) 67 | ] 68 | buyers = [ 69 | create_buyer(f"Buyer {i}", f"555-1111{i}", address) for i, address in enumerate(addresses, 1) 70 | ] 71 | print("Created buyers:", buyers) 72 | 73 | # Create sales 74 | sales = [] 75 | for _ in range(10): 76 | car = random.choice(cars) 77 | seller = random.choice(sellers) 78 | buyer = random.choice(buyers) 79 | sale = create_sale(car['id'], seller['id'], buyer['id']) 80 | sales.append(sale) 81 | print("Created sales:", sales) 82 | 83 | # Read some data to verify 84 | response = requests.get(f"{BASE_URL}/api/v1/cars/") 85 | print("All cars:", response.json()) 86 | 87 | response = requests.get(f"{BASE_URL}/api/v1/sales/") 88 | print("All sales:", response.json()) 89 | 90 | if __name__ == "__main__": 91 | main() -------------------------------------------------------------------------------- /clientside/tests/test_fastapi_tracer.py: -------------------------------------------------------------------------------- 1 | import json 2 | from unittest.mock import patch 3 | 4 | import pytest 5 | from fastapi import FastAPI 6 | from fastapi.testclient import TestClient 7 | 8 | from src.captureflow.tracer import Tracer 9 | 10 | app = FastAPI() 11 | 12 | tracer = Tracer( 13 | repo_url="https://github.com/DummyUser/DummyRepo", 14 | server_base_url="http://127.0.0.1:8000", 15 | ) 16 | 17 | 18 | @app.get("/add/{x}/{y}") 19 | @tracer.trace_endpoint 20 | async def add(x: int, y: int): 21 | return {"result": x + y} 22 | 23 | 24 | @pytest.mark.asyncio 25 | async def test_trace_endpoint_fastapi(): 26 | with patch("src.captureflow.tracer.Tracer._send_trace_log") as mock_log: 27 | with TestClient(app) as client: 28 | response = client.get("/add/2/3") 29 | assert response.status_code == 200 30 | assert response.json() == {"result": 5} 31 | 32 | mock_log.assert_called_once() 33 | log_data = mock_log.call_args[0][0] # Get the context data passed to _send_trace_log 34 | 35 | assert log_data["endpoint"] == "add" 36 | 37 | assert "x" in log_data["input"]["kwargs"] and "y" in log_data["input"]["kwargs"] 38 | assert log_data["input"]["kwargs"]["x"]["json_serialized"] == json.dumps(2) # Use json.dumps for consistency 39 | assert log_data["input"]["kwargs"]["y"]["json_serialized"] == json.dumps(3) 40 | 41 | assert len(log_data["execution_trace"]) > 0 42 | assert log_data["execution_trace"][0]["event"] == "call" 43 | assert log_data["output"]["result"]["json_serialized"] == json.dumps({"result": 5}) 44 | 45 | # Additional checks for type information 46 | assert log_data["input"]["kwargs"]["x"]["python_type"] == "" 47 | assert log_data["input"]["kwargs"]["y"]["python_type"] == "" 48 | assert log_data["output"]["result"]["python_type"] == "" 49 | 50 | 51 | @app.get("/divide/{x}/{y}") 52 | @tracer.trace_endpoint 53 | async def divide(x: int, y: int): 54 | return {"result": x / y} 55 | 56 | 57 | @pytest.mark.asyncio 58 | async def test_trace_endpoint_with_exception(): 59 | with patch("src.captureflow.tracer.Tracer._send_trace_log") as mock_log: 60 | with TestClient(app, raise_server_exceptions=False) as client: 61 | response = client.get("/divide/10/0") 62 | assert response.status_code == 500 63 | 64 | mock_log.assert_called_once() 65 | log_data = mock_log.call_args[0][0] 66 | 67 | assert log_data["endpoint"] == "divide" 68 | assert "x" in log_data["input"]["kwargs"] and "y" in log_data["input"]["kwargs"] 69 | assert log_data["input"]["kwargs"]["x"]["json_serialized"] == json.dumps(10) 70 | assert log_data["input"]["kwargs"]["y"]["json_serialized"] == json.dumps(0) 71 | 72 | exception_events = [e for e in log_data["execution_trace"] if e["event"] == "exception"] 73 | assert len(exception_events) > 0, "Exception event not found in the execution trace" 74 | exception_event = exception_events[0] 75 | assert "ZeroDivisionError" in exception_event["exception_info"]["type"], "Expected ZeroDivisionError" 76 | assert "division by zero" in exception_event["exception_info"]["value"], "Expected 'division by zero' message" 77 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/test/base_insertion.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.sql import text 2 | 3 | from .config import database_test_config 4 | 5 | 6 | ### 7 | # Suport test for database insertions 8 | ### 9 | 10 | engine = database_test_config.engine 11 | 12 | 13 | def insert_into_cars(input): 14 | ''' Insert into table cars ''' 15 | with engine.connect() as con: 16 | 17 | data = (input, ) 18 | 19 | statement = text( 20 | """INSERT INTO cars(id, name, year, brand) VALUES(:id, :name, :year, :brand)""") 21 | 22 | for line in data: 23 | con.execute(statement, **line) 24 | 25 | 26 | def insert_into_sellers(input): 27 | ''' Insert into table sellers ''' 28 | with engine.connect() as con: 29 | 30 | data = (input, ) 31 | 32 | statement = text( 33 | """INSERT INTO sellers(id, name, cpf, phone) VALUES(:id, :name, :cpf, :phone)""") 34 | 35 | for line in data: 36 | con.execute(statement, **line) 37 | 38 | 39 | def insert_into_buyers(input): 40 | ''' Insert into table buyers ''' 41 | with engine.connect() as con: 42 | 43 | data = ( 44 | { 45 | "id": input["id"], 46 | "name":input["name"], 47 | "phone": input["phone"], 48 | "address_cep": input["address"]["cep"], 49 | "address_public_place": input["address"]["public_place"], 50 | "address_district": input["address"]["district"], 51 | "address_city": input["address"]["city"], 52 | "address_state": input["address"]["state"] 53 | }, 54 | ) 55 | 56 | 57 | statement = text("""INSERT INTO buyers( 58 | id, name, phone, address_cep, address_public_place, 59 | address_district, address_city, address_state) 60 | VALUES(:id, :name, :phone, :address_cep, :address_public_place, 61 | :address_district, :address_city, :address_state)""") 62 | 63 | for line in data: 64 | con.execute(statement, **line) 65 | 66 | 67 | def insert_into_stocks(input): 68 | ''' Insert into table stocks ''' 69 | with engine.connect() as con: 70 | 71 | data = (input, ) 72 | 73 | statement = text( 74 | """INSERT INTO stocks(id, car_id, quantity) VALUES(:id, :car_id, :quantity)""") 75 | 76 | for line in data: 77 | con.execute(statement, **line) 78 | 79 | 80 | def insert_into_sales(input): 81 | ''' Insert into table sales ''' 82 | with engine.connect() as con: 83 | 84 | data = (input, ) 85 | 86 | statement = text( 87 | """INSERT INTO sales(id, car_id, buyer_id, seller_id, created_at) VALUES(:id, :car_id, :buyer_id, :seller_id, :created_at)""") 88 | 89 | for line in data: 90 | con.execute(statement, **line) 91 | 92 | 93 | def read_stock_by_id(id): 94 | with engine.connect() as con: 95 | statement = "SELECT * FROM stocks WHERE id = " + str(id) 96 | keys = ("id", "quantity", "car_id") 97 | for row in con.execute(statement): 98 | return dict(zip(keys, row)) 99 | return None 100 | 101 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/test/test_api/test_sellers.py: -------------------------------------------------------------------------------- 1 | from fastapi.testclient import TestClient 2 | 3 | from ..database_test import configure_test_database, clear_database 4 | 5 | from ..templates.seller_tempĺates import seller_json, seller_not_found_error 6 | 7 | from ..base_insertion import insert_into_sellers 8 | 9 | from ...main import app 10 | 11 | 12 | client = TestClient(app) 13 | 14 | sellers_route = "/api/v1/sellers" 15 | 16 | def setup_module(module): 17 | configure_test_database(app) 18 | 19 | 20 | def setup_function(module): 21 | clear_database() 22 | 23 | 24 | def test_create_seller(seller_json): 25 | ''' Create a seller with success ''' 26 | response = client.post(sellers_route + "/", json=seller_json) 27 | assert response.status_code == 201 28 | assert response.json() == seller_json 29 | 30 | 31 | def test_read_seller(seller_json): 32 | ''' Read a seller with success ''' 33 | insert_into_sellers(seller_json) 34 | request_url = sellers_route + "/1" 35 | response = client.get(request_url) 36 | assert response.status_code == 200 37 | assert response.json() == seller_json 38 | 39 | 40 | def test_read_seller_by_cpf(seller_json): 41 | ''' Read a seller by cpf with success ''' 42 | insert_into_sellers(seller_json) 43 | request_url = sellers_route + "/cpf/69285717640" 44 | response = client.get(request_url) 45 | assert response.status_code == 200 46 | assert response.json() == seller_json 47 | 48 | 49 | def test_read_sellers(seller_json): 50 | ''' Read all sellers paginated with success ''' 51 | insert_into_sellers(seller_json) 52 | request_url = sellers_route + "?skip=0&limit=100" 53 | response = client.get(request_url) 54 | assert response.status_code == 200 55 | assert response.json() == [ seller_json ] 56 | 57 | 58 | def test_delete_seller(seller_json): 59 | ''' Delete a seller with success ''' 60 | insert_into_sellers(seller_json) 61 | request_url = sellers_route + "/1" 62 | response = client.delete(request_url) 63 | assert response.status_code == 200 64 | assert response.json() == True 65 | 66 | 67 | def test_read_seller_not_found(seller_not_found_error): 68 | ''' Read a seller when not found ''' 69 | request_url = sellers_route + "/1" 70 | response = client.get(request_url) 71 | assert response.status_code == 404 72 | assert response.json() == seller_not_found_error 73 | 74 | 75 | def test_read_seller_by_cpf(seller_not_found_error): 76 | ''' Read a seller by cpf when not found ''' 77 | request_url = sellers_route + "/cpf/69285717640" 78 | response = client.get(request_url) 79 | assert response.status_code == 404 80 | assert response.json() == seller_not_found_error 81 | 82 | 83 | def test_read_sellers_not_found(): 84 | ''' Read all sellers paginated when not found ''' 85 | request_url = sellers_route + "?skip=0&limit=100" 86 | response = client.get(request_url) 87 | assert response.status_code == 200 88 | assert response.json() == [] 89 | 90 | 91 | def test_delete_seller_not_found(seller_not_found_error): 92 | ''' Delete a seller when not exists ''' 93 | request_url = sellers_route + "/1" 94 | response = client.delete(request_url) 95 | assert response.status_code == 404 96 | assert response.json() == seller_not_found_error 97 | 98 | -------------------------------------------------------------------------------- /clientside_v2/tests/test_span_processor.py: -------------------------------------------------------------------------------- 1 | """ 2 | This test verifies that custom SpanProcessor implemented in CaptureFlow client library 3 | is capable of enriching all spans with python execution context, namely: 4 | 'code.filepath' in span.attributes 5 | 'code.lineno' in span.attributes 6 | 'code.function' in span.attributes 7 | """ 8 | 9 | import os 10 | 11 | import pytest 12 | from opentelemetry.sdk.trace.export import SimpleSpanProcessor 13 | from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter 14 | from opentelemetry.trace import get_tracer, get_tracer_provider, set_tracer_provider 15 | 16 | from captureflow.distro import CaptureFlowDistro 17 | 18 | 19 | @pytest.fixture(scope="module") 20 | def setup_tracer_and_exporter(): 21 | # Initialize CaptureFlowDistro 22 | distro = CaptureFlowDistro() 23 | distro._configure() 24 | 25 | # Retrieve the global tracer provider 26 | tracer_provider = get_tracer_provider() 27 | 28 | # Set up in-memory span exporter 29 | span_exporter = InMemorySpanExporter() 30 | span_processor = SimpleSpanProcessor(span_exporter) 31 | tracer_provider.add_span_processor(span_processor) 32 | 33 | yield tracer_provider, span_exporter 34 | 35 | # Reset the global tracer provider to avoid conflicts with other tests 36 | set_tracer_provider(None) 37 | 38 | 39 | @pytest.fixture(autouse=True) 40 | def clear_spans(setup_tracer_and_exporter): 41 | _, span_exporter = setup_tracer_and_exporter 42 | span_exporter.clear() 43 | yield 44 | span_exporter.clear() 45 | 46 | 47 | def relative_path(filepath): 48 | return os.path.relpath(filepath, start=os.getcwd()) 49 | 50 | 51 | def test_span_processor_adds_frame_info(setup_tracer_and_exporter): 52 | tracer_provider, span_exporter = setup_tracer_and_exporter 53 | tracer = get_tracer(__name__) 54 | 55 | with tracer.start_as_current_span("test_span") as span: 56 | pass 57 | 58 | spans = span_exporter.get_finished_spans() 59 | 60 | assert len(spans) == 1 61 | span = spans[0] 62 | 63 | assert "code.filepath" in span.attributes 64 | assert "code.lineno" in span.attributes 65 | assert "code.function" in span.attributes 66 | 67 | expected_filepath = relative_path(__file__) 68 | 69 | assert span.attributes["code.filepath"] == expected_filepath 70 | assert span.attributes["code.function"] == "test_span_processor_adds_frame_info" 71 | assert isinstance(span.attributes["code.lineno"], int) 72 | 73 | 74 | def test_span_processor_in_different_function(setup_tracer_and_exporter): 75 | tracer_provider, span_exporter = setup_tracer_and_exporter 76 | tracer = get_tracer(__name__) 77 | 78 | def inner_function(): 79 | with tracer.start_as_current_span("inner_span") as span: 80 | return span 81 | 82 | span = inner_function() 83 | 84 | spans = span_exporter.get_finished_spans() 85 | 86 | assert len(spans) == 1 87 | span = spans[0] 88 | 89 | assert "code.filepath" in span.attributes 90 | assert "code.lineno" in span.attributes 91 | assert "code.function" in span.attributes 92 | 93 | expected_filepath = relative_path(__file__) 94 | 95 | assert span.attributes["code.filepath"] == expected_filepath 96 | assert span.attributes["code.function"] == "inner_function" 97 | assert isinstance(span.attributes["code.lineno"], int) 98 | 99 | 100 | if __name__ == "__main__": 101 | pytest.main([__file__]) 102 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Traces 10 | trace_*.json 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | *.whl 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | # Pipfile.lock 100 | 101 | # poetry 102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 106 | #poetry.lock 107 | 108 | # pdm 109 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 110 | #pdm.lock 111 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 112 | # in version control. 113 | # https://pdm.fming.dev/#use-with-ide 114 | .pdm.toml 115 | 116 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 117 | __pypackages__/ 118 | 119 | # Celery stuff 120 | celerybeat-schedule 121 | celerybeat.pid 122 | 123 | # SageMath parsed files 124 | *.sage.py 125 | 126 | # Environments 127 | .env 128 | .venv 129 | env/ 130 | venv/ 131 | ENV/ 132 | env.bak/ 133 | venv.bak/ 134 | 135 | # Spyder project settings 136 | .spyderproject 137 | .spyproject 138 | 139 | # Rope project settings 140 | .ropeproject 141 | 142 | # mkdocs documentation 143 | /site 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | # pytype static type analyzer 154 | .pytype/ 155 | 156 | # Cython debug symbols 157 | cython_debug/ 158 | 159 | # certs 160 | *.pem 161 | *.key -------------------------------------------------------------------------------- /serverside/tests/test_server.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | from typing import Any, Dict 4 | from unittest.mock import MagicMock 5 | 6 | import pytest 7 | from fastapi.testclient import TestClient 8 | 9 | 10 | @pytest.fixture 11 | def client(): 12 | from src.server import app 13 | 14 | with TestClient(app) as client: 15 | yield client 16 | 17 | 18 | @pytest.fixture 19 | def mock_redis(mocker): 20 | mock_redis = MagicMock() 21 | mocker.patch("src.server.redis", new=mock_redis) 22 | return mock_redis 23 | 24 | 25 | @pytest.fixture 26 | def sample_trace(): 27 | trace_path = Path(__file__).parent / "assets" / "sample_trace.json" 28 | with open(trace_path) as f: 29 | return json.load(f) 30 | 31 | 32 | @pytest.fixture 33 | def sample_trace_with_exception(): 34 | trace_path = Path(__file__).parent / "assets" / "sample_trace_with_exception.json" 35 | with open(trace_path) as f: 36 | return json.load(f) 37 | 38 | 39 | def normalize_trace_data(data: Dict[str, Any]) -> Dict[str, Any]: 40 | """ 41 | Normalizes trace data by ensuring optional fields are present and correctly formatted. 42 | """ 43 | 44 | if "output" not in data: 45 | data["output"] = None 46 | 47 | return data 48 | 49 | 50 | def test_store_trace_log(client, mock_redis, sample_trace): 51 | repo_url = "https://github.com/NickKuts/capture_flow" 52 | response = client.post("/api/v1/traces", params={"repository-url": repo_url}, json=sample_trace) 53 | 54 | assert response.status_code == 200 55 | assert response.json() == {"message": "Trace log saved successfully"} 56 | 57 | # Verify Redis 'set' was called 58 | assert mock_redis.set.called, "Redis 'set' method was not called" 59 | called_args, _ = mock_redis.set.call_args 60 | key_passed_to_redis, json_data_passed_to_redis = called_args 61 | 62 | expected_key = f"{repo_url}:{sample_trace['invocation_id']}" 63 | assert key_passed_to_redis == expected_key, "Key passed to Redis does not match expected format" 64 | 65 | # Deserialize & normalize 66 | actual_data = json.loads(json_data_passed_to_redis) 67 | actual_data = normalize_trace_data(actual_data) 68 | expected_data = normalize_trace_data(sample_trace) 69 | 70 | # Compare normalized data 71 | assert actual_data == expected_data, "Normalized data passed to Redis does not match expected data" 72 | 73 | 74 | def test_store_trace_log_with_exception(client, mock_redis, sample_trace_with_exception): 75 | repo_url = "https://github.com/NickKuts/capture_flow" 76 | response = client.post("/api/v1/traces", params={"repository-url": repo_url}, json=sample_trace_with_exception) 77 | 78 | assert response.status_code == 200 79 | assert response.json() == {"message": "Trace log saved successfully"} 80 | 81 | # Verify Redis 'set' was called 82 | assert mock_redis.set.called, "Redis 'set' method was not called" 83 | called_args, _ = mock_redis.set.call_args 84 | key_passed_to_redis, json_data_passed_to_redis = called_args 85 | 86 | expected_key = f"{repo_url}:{sample_trace_with_exception['invocation_id']}" 87 | assert key_passed_to_redis == expected_key, "Key passed to Redis does not match expected format" 88 | 89 | # Deserialize & normalize 90 | actual_data = json.loads(json_data_passed_to_redis) 91 | actual_data = normalize_trace_data(actual_data) 92 | expected_data = normalize_trace_data(sample_trace_with_exception) 93 | 94 | # Compare normalized data 95 | assert actual_data == expected_data, "Normalized data passed to Redis does not match expected data" 96 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/README.md: -------------------------------------------------------------------------------- 1 | # FastAPI for Car Shop ERP 2 | 3 | ![coverage](https://img.shields.io/badge/coverage-93%25-darkgreen) 4 | 5 | This rest api is a kind of ERP of car shop. 6 | App is available on cloud by https://car-shop-fastapi.herokuapp.com/docs. 7 | 8 | ## Requirements 9 | You'll must have installed: 10 | - [Python 3.6+](https://www.python.org/downloads/) 11 | - [Virtual Environments with Python3.6+](https://docs.python.org/3/tutorial/venv.html) 12 | - [Docker](https://docs.docker.com/engine/install/) 13 | - [Docker-compose](https://docs.docker.com/compose/install/) 14 | ___ 15 | ## Setup Project 16 | 17 | Create virtual environment 18 | ```bash 19 | python3 -m venv env 20 | ``` 21 | 22 | Activating created virtual environment 23 | ```bash 24 | source env/bin/activate 25 | ``` 26 | Install app dependencies 27 | ```bash 28 | pip install -r requirements-local.txt 29 | ``` 30 | ___ 31 | ## Running Application 32 | 33 | Starting database (postgres:alpine3.14) 34 | ```bash 35 | docker-compose up 36 | ``` 37 | 38 | Starting application, run: 39 | ```bash 40 | uvicorn app.main:app --reload 41 | ``` 42 | 43 | ##### Obs: It's possible to configure the database by environment variable as: 44 | ##### `export DB_URL="postgresql://user-name:password@host-name/database-name"` 45 | 46 | 47 | ## Acessing on local 48 | The application will get started in http://127.0.0.1:8000 49 | 50 | Swagger Documentation: http://127.0.0.1:8000/docs 51 | 52 | Redoc Documentation: http://127.0.0.1:8000/redoc 53 | 54 | Database Adminer: http://127.0.0.1:9000 55 | - credentials tinnova/tinnova123(user/password). 56 | 57 | If required authentication on routes add headers: 58 | - token = my-jwt-token 59 | - x_token = fake-super-secret-token 60 | ___ 61 | ## Testing 62 | 63 | __For run tests__ 64 | ```bash 65 | pytest 66 | ``` 67 | 68 | __For run tests with coverage report__ 69 | ```bash 70 | pytest --cov=app app/test/ 71 | ``` 72 | ___ 73 | ## Development 74 | 75 | For update dependencies on `requirements.txt`, run: 76 | Obs: For production must have extra changes. 77 | 1. Remove version of `dataclasses` 78 | 79 | ```bash 80 | pip freeze > requirements.txt 81 | ``` 82 | ___ 83 | ## Deploy On Heroku 84 | 85 | __Requirements__ 86 | 87 | - [Heroku Cli](https://devcenter.heroku.com/articles/heroku-cli) 88 | 89 | __Install Heroku Cli__ 90 | ``` 91 | sudo snap install --classic heroku 92 | ``` 93 | 94 | __Deploy__ 95 | 96 | **Case is activated automatic deploy for `master` branch, just commit on `master` branch, 97 | instead make manual deploy from Heroku Cli, like below** 98 | ``` 99 | heroku login 100 | heroku git:remote -a car-shop-fastapi 101 | git add . 102 | git commit -m "Deploy on heroku" 103 | git push origin master 104 | git push heroku master 105 | ``` 106 | ___ 107 | 108 | ### Source Documentation 109 | - [FastAPI](https://fastapi.tiangolo.com/) 110 | 111 | - [Bigger Application](https://fastapi.tiangolo.com/tutorial/bigger-applications/) 112 | 113 | - [SQL](https://fastapi.tiangolo.com/tutorial/sql-databases/) 114 | 115 | - [Testing](https://fastapi.tiangolo.com/tutorial/testing/) 116 | 117 | - [Pydantic](https://pydantic-docs.helpmanual.io/) 118 | 119 | - [SQL Relational Database SQLAlchemy by FastAPI](https://fastapi.tiangolo.com/tutorial/sql-databases/?h=databa#sql-relational-databases) 120 | 121 | - [SQLAlchemy 1.4](https://docs.sqlalchemy.org/en/14/tutorial/engine.html) 122 | 123 | - [FastAPI "Real world example app"](https://github.com/nsidnev/fastapi-realworld-example-app) 124 | 125 | -------------------------------------------------------------------------------- /serverside/src/server.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any, Dict, List, Optional 3 | 4 | from fastapi import FastAPI, Query 5 | from pydantic import BaseModel, Field, parse_obj_as, validator 6 | from src.utils.exception_patcher import ExceptionPatcher 7 | from src.utils.integrations.redis_integration import get_redis_connection 8 | from src.utils.test_creator import TestCoverageCreator 9 | 10 | app = FastAPI() 11 | redis = get_redis_connection() 12 | 13 | 14 | class SerializedObject(BaseModel): 15 | python_type: str 16 | json_serialized: str 17 | 18 | 19 | class Arguments(BaseModel): 20 | args: List[Any] = Field(default_factory=list) 21 | kwargs: Dict[str, SerializedObject] = Field(default_factory=dict) 22 | 23 | 24 | class ExceptionInfo(BaseModel): 25 | type: str 26 | value: str 27 | traceback: List[str] 28 | 29 | 30 | class BaseExecutionTraceItem(BaseModel): 31 | id: str 32 | timestamp: str 33 | event: str 34 | function: str 35 | caller_id: Optional[str] = None 36 | file: str 37 | line: int 38 | source_line: str 39 | tag: str 40 | 41 | 42 | class CallExecutionTraceItem(BaseExecutionTraceItem): 43 | arguments: Arguments 44 | return_value: SerializedObject 45 | 46 | 47 | class LineExecutionTraceItem(BaseExecutionTraceItem): 48 | pass 49 | 50 | 51 | class ExceptionExecutionTraceItem(BaseExecutionTraceItem): 52 | exception_info: ExceptionInfo 53 | 54 | 55 | class ReturnExecutionTraceItem(BaseExecutionTraceItem): 56 | return_value: SerializedObject 57 | 58 | 59 | class TraceData(BaseModel): 60 | invocation_id: str 61 | timestamp: str 62 | endpoint: str 63 | execution_trace: List[Any] 64 | output: Optional[Dict[str, Any]] = None 65 | call_stack: List[Dict[str, Any]] = [] 66 | log_filename: Optional[str] = None 67 | input: Dict[str, Any] 68 | 69 | @validator("execution_trace", pre=True) 70 | def parse_execution_trace(cls, v): 71 | items = [] 72 | mapping = { 73 | "call": CallExecutionTraceItem, 74 | "line": LineExecutionTraceItem, 75 | "exception": ExceptionExecutionTraceItem, 76 | "return": ReturnExecutionTraceItem, 77 | } 78 | for item in v: 79 | item_type = mapping.get(item.get("event"), BaseExecutionTraceItem) 80 | items.append(parse_obj_as(item_type, item)) 81 | return items 82 | 83 | 84 | # Store new trace 85 | @app.post("/api/v1/traces") 86 | async def store_trace_log(trace_data: TraceData, repo_url: str = Query(..., alias="repository-url")): 87 | trace_data_json = trace_data.json() 88 | trace_log_key = f"{repo_url}:{trace_data.invocation_id}" 89 | 90 | redis.set(trace_log_key, trace_data_json) 91 | return {"message": "Trace log saved successfully"} 92 | 93 | 94 | # Process accumulated traces and create bugfix MR if needed 95 | @app.post("/api/v1/merge-requests/bugfix") 96 | async def generate_bugfix_mr(repo_url: str = Query(..., alias="repository-url")): 97 | orchestrator = ExceptionPatcher(redis_client=redis, repo_url=repo_url) 98 | orchestrator.run() 99 | return {"message": "MR generation process started successfully"} 100 | 101 | 102 | @app.post("/api/v1/test-coverage/create") 103 | async def generate_test_coverage(repo_url: str = Query(..., alias="repository-url")): 104 | """ 105 | Endpoint to trigger test coverage creation using the TestCoverageCreator. 106 | The process will look at non-standard library functions in the trace and attempt to generate tests. 107 | """ 108 | test_creator = TestCoverageCreator(redis_client=redis, repo_url=repo_url) 109 | test_creator.run() 110 | return {"message": "Test coverage creation process initiated successfully"} 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # captureflow-py 2 | 3 | CaptureFlow combines Application Monitoring with power of LLMs, to ship you Pull Requests that are guaranteed to work in production. 4 | 5 | Deployed applications are already rich with embedded context. By utilizing production traces, CaptureFlow can start saving you time: 6 | 7 | 1. Is your app's test coverage lacking or behaviour is unclear? Improve it with CaptureFlow tests 8 | - -> automated integration / unit test generation: [MR](https://github.com/CaptureFlow/captureflow-py/pull/62) 9 | 10 | 2. Save debugging time by starting with a Pull Request that fixes issues and doesn't introduce new regressions, all verified by the aforementioned unit tests 11 | - -> automated bug fixes in response to exceptions: [MR](https://github.com/CaptureFlow/captureflow-py/pull/21) 12 | 13 | **NOTE:** This is not yet ready for production use and it will degrade your application's performance. It presents an end-to-end pipeline that can be optimized by balancing tracing verbosity with the impact it can provide. For more details, check the [clientside/README](https://github.com/CaptureFlow/captureflow-py/blob/main/clientside/README.md). 14 | 15 | ## Main components 16 | 17 | ![Alt text](./assets/main-chart.svg) 18 | 19 | CaptureFlow generates unit tests based on observations from your production app and uses them as acceptance criteria. This ensures LLMs can reliably solve end-to-end maintenance tasks, allowing you to merge changes safely without the need to verify each line extensively. 20 | 21 | **Support is currently limited to Python, OpenAI API, and GitHub.** 22 | 23 | ## Roadmap / Current Status 24 | 25 | - [x] **Pipeline Setup**: Implement an end-to-end pipeline, including a tracer and server. The tracer outputs JSONs for Python execution frames, while the server stores and enriches traces with GitHub metadata. 26 | - [x] **MR Generation Heuristic**: Focuses on methods as the unit of optimization for the initial heuristic approach to generating Merge Requests. 27 | - [x] **Automated Code Fixes**: Utilizes exception traces and the execution context to propose targeted fixes for exceptions. 28 | - [x] **Test Case Extension**: Extend existing test cases using accumulated trace data to generate more realistic mock data and scenarios. 29 | - [ ] Client-side: Introduce trace sampling that respects infrequently used functions. 30 | - [ ] Client-side: Enable re-creation of non-serializable objects via pickling. 31 | - [ ] Server-side: Transition from FastAPI to a generic WSGI/ASGI approach. 32 | - [ ] Server-side: Facilitate on-demand creation of bottom-up unit tests and explore potential real-time IDE synergies. 33 | - [x] **Tests as Acceptance Criteria for RAG**: 34 | - [x] Auto-bugfix 35 | - [ ] Code refactoring / Library migrations / Safe deletion of unused code 36 | - [ ] Validate arbitrary code changes through observation-based unit tests. 37 | - [ ] **Add Support for Open LLMs**. 38 | 39 | 40 | ## Setup 41 | 42 | #### Clientside 43 | 44 | ```sh 45 | pip install captureflow-agent 46 | ``` 47 | 48 | ```python 49 | from captureflow.tracer import Tracer 50 | tracer = Tracer( 51 | repo_url="https://github.com/CaptureFlow/captureflow-py", 52 | server_base_url="http://127.0.0.1:8000", 53 | ) 54 | 55 | @app.get("/") 56 | @tracer.trace_endpoint 57 | def process_data(request): 58 | response = do_stuff(request) 59 | ... 60 | ``` 61 | 62 | And... you're almost ready to go: you need to deploy `serverside` by yourself (for now). 63 | 64 | Please check [clientside/README](https://github.com/CaptureFlow/captureflow-py/blob/main/clientside/README.md). 65 | 66 | #### Serverside 67 | 68 | You will need to deploy `fastapi` app together with `redis` instance. 69 | 70 | ```sh 71 | docker compose up --build 72 | ``` 73 | 74 | Please check [serverside/README](https://github.com/CaptureFlow/captureflow-py/blob/main/serverside/README.md). 75 | 76 | ## Contributing 77 | 78 | No structure yet, feel free to join [discord](https://discord.gg/9VVqZBFt). 79 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/test/test_api/test_stocks.py: -------------------------------------------------------------------------------- 1 | from fastapi.testclient import TestClient 2 | 3 | from ..database_test import configure_test_database, clear_database 4 | 5 | from ..templates.stock_tempĺates import stock_request_json, stock_response_json, stock_not_found_error, stock_already_exist 6 | 7 | from ..templates.car_tempĺates import car_json, car_not_found_error 8 | 9 | from ..base_insertion import insert_into_stocks, insert_into_cars 10 | 11 | from ...main import app 12 | 13 | 14 | client = TestClient(app) 15 | 16 | stocks_route = "/api/v1/stocks" 17 | 18 | def setup_module(module): 19 | configure_test_database(app) 20 | 21 | 22 | def setup_function(module): 23 | clear_database() 24 | 25 | 26 | def test_create_stock(car_json, stock_request_json, stock_response_json): 27 | ''' Create a stock with success ''' 28 | insert_into_cars(car_json) 29 | response = client.post(stocks_route + "/", json=stock_request_json) 30 | assert response.status_code == 201 31 | assert response.json() == stock_response_json 32 | 33 | 34 | def test_read_stock(car_json, stock_request_json, stock_response_json): 35 | ''' Read a stock with success ''' 36 | insert_into_cars(car_json) 37 | insert_into_stocks(stock_request_json) 38 | request_url = stocks_route + "/1" 39 | response = client.get(request_url) 40 | assert response.status_code == 200 41 | assert response.json() == stock_response_json 42 | 43 | 44 | def test_read_stock_by_car(car_json, stock_request_json, stock_response_json): 45 | ''' Read a stock with success ''' 46 | insert_into_cars(car_json) 47 | insert_into_stocks(stock_request_json) 48 | request_url = stocks_route + "/cars/1" 49 | response = client.get(request_url) 50 | assert response.status_code == 200 51 | assert response.json() == stock_response_json 52 | 53 | 54 | def test_read_stocks(car_json, stock_request_json, stock_response_json): 55 | ''' Read all stocks paginated with success ''' 56 | insert_into_cars(car_json) 57 | insert_into_stocks(stock_request_json) 58 | request_url = stocks_route + "?skip=0&limit=100" 59 | response = client.get(request_url) 60 | assert response.status_code == 200 61 | assert response.json() == [stock_response_json] 62 | 63 | 64 | def test_delete_stock(car_json, stock_request_json): 65 | ''' Delete a stock with success ''' 66 | insert_into_cars(car_json) 67 | insert_into_stocks(stock_request_json) 68 | request_url = stocks_route + "/1" 69 | response = client.delete(request_url) 70 | assert response.status_code == 200 71 | assert response.json() == True 72 | 73 | def test_create_stock_car_not_found(stock_request_json, stock_response_json, car_not_found_error): 74 | ''' Create a stock with success ''' 75 | response = client.post(stocks_route + "/", json=stock_request_json) 76 | assert response.status_code == 404 77 | assert response.json() == car_not_found_error 78 | 79 | 80 | def test_create_stock_unique_car_uk_error(car_json, stock_request_json, stock_already_exist): 81 | ''' Create a stock with success ''' 82 | insert_into_cars(car_json) 83 | insert_into_stocks(stock_request_json) 84 | response = client.post(stocks_route + "/", json=stock_request_json) 85 | assert response.status_code == 422 86 | assert response.json() == stock_already_exist 87 | 88 | 89 | def test_read_stock_not_found(stock_not_found_error): 90 | ''' Read a stock when not found ''' 91 | request_url = stocks_route + "/1" 92 | response = client.get(request_url) 93 | assert response.status_code == 404 94 | assert response.json() == stock_not_found_error 95 | 96 | 97 | def test_read_stock_by_car(car_json, stock_request_json, stock_response_json, stock_not_found_error): 98 | ''' Read a stock with success ''' 99 | request_url = stocks_route + "/cars/1" 100 | response = client.get(request_url) 101 | assert response.status_code == 404 102 | assert response.json() == stock_not_found_error 103 | 104 | 105 | def test_read_stocks_not_found(): 106 | ''' Read all stocks paginated when not found ''' 107 | request_url = stocks_route + "?skip=0&limit=100" 108 | response = client.get(request_url) 109 | assert response.status_code == 200 110 | assert response.json() == [] 111 | 112 | 113 | def test_delete_stock_not_found(stock_not_found_error): 114 | ''' Delete a stock when not exists ''' 115 | request_url = stocks_route + "/1" 116 | response = client.delete(request_url) 117 | assert response.status_code == 404 118 | assert response.json() == stock_not_found_error 119 | -------------------------------------------------------------------------------- /clientside_v2/tests/instrumentation/test_sqlalchemy_instrumentation.py: -------------------------------------------------------------------------------- 1 | """ 2 | This test verifies that SQLAlchemy query spans include execution and result details: 3 | 'db.system' in span.attributes 4 | 'db.statement' in span.attributes 5 | 'db.parameters' in span.attributes 6 | 'db.row_count' in span.attributes 7 | 'db.result_columns' in span.attributes (for SELECT queries) 8 | 'db.result_data' in span.attributes (for SELECT queries) 9 | """ 10 | 11 | import json 12 | import os 13 | 14 | import pytest 15 | from fastapi import FastAPI 16 | from fastapi.testclient import TestClient 17 | from opentelemetry.sdk.trace.export import SimpleSpanProcessor 18 | from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter 19 | from opentelemetry.trace import get_tracer_provider 20 | from sqlalchemy import Column, Integer, String, create_engine, text 21 | from sqlalchemy.orm import declarative_base, sessionmaker 22 | 23 | from captureflow.distro import CaptureFlowDistro 24 | 25 | # SQLAlchemy setup 26 | Base = declarative_base() 27 | 28 | 29 | class User(Base): 30 | __tablename__ = "users" 31 | id = Column(Integer, primary_key=True) 32 | name = Column(String(50)) 33 | 34 | 35 | def setup_database(engine): 36 | Base.metadata.drop_all(engine) 37 | Base.metadata.create_all(engine) 38 | 39 | 40 | @pytest.fixture(scope="function") 41 | def engine(): 42 | DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://user:password@localhost:5432/testdb") 43 | engine = create_engine(DATABASE_URL) 44 | setup_database(engine) 45 | yield engine 46 | Base.metadata.drop_all(engine) 47 | 48 | 49 | @pytest.fixture(scope="function") 50 | def app(engine): 51 | app = FastAPI() 52 | Session = sessionmaker(bind=engine) 53 | 54 | @app.get("/") 55 | async def read_root(): 56 | with Session() as session: 57 | new_user = User(name="Test User") 58 | session.add(new_user) 59 | session.commit() 60 | user = session.query(User).filter_by(name="Test User").first() 61 | return {"message": "Hello World", "user_id": user.id if user else None} 62 | 63 | return app 64 | 65 | 66 | @pytest.fixture(scope="function") 67 | def client(app): 68 | return TestClient(app) 69 | 70 | 71 | def test_sqlalchemy_instrumentation(engine, client, span_exporter): 72 | # Make a request to the FastAPI server 73 | response = client.get("/") 74 | assert response.status_code == 200 75 | assert response.json()["user_id"] is not None 76 | 77 | # Retrieve the spans 78 | spans = span_exporter.get_finished_spans() 79 | sql_spans = [span for span in spans if span.attributes.get("db.system") == "sqlalchemy"] 80 | 81 | # Filter out bootstrap-related spans 82 | relevant_sql_spans = [ 83 | span for span in sql_spans if not span.attributes.get("db.statement", "").startswith("SELECT pg_catalog") 84 | ] 85 | 86 | assert len(relevant_sql_spans) >= 2, "Expected at least two relevant SQLAlchemy spans" 87 | 88 | insert_span = next((span for span in relevant_sql_spans if span.name.startswith("SQLAlchemy: INSERT")), None) 89 | select_span = next((span for span in relevant_sql_spans if span.name.startswith("SQLAlchemy: SELECT")), None) 90 | 91 | assert insert_span is not None, "INSERT span not found" 92 | assert select_span is not None, "SELECT span not found" 93 | 94 | # Validate INSERT span 95 | assert insert_span.attributes["db.system"] == "sqlalchemy" 96 | assert "INSERT INTO users" in insert_span.attributes["db.statement"] 97 | assert "db.parameters" in insert_span.attributes 98 | assert "db.row_count" in insert_span.attributes 99 | 100 | # Validate SELECT span 101 | assert select_span.attributes["db.system"] == "sqlalchemy" 102 | assert "SELECT" in select_span.attributes["db.statement"] 103 | assert "FROM users" in select_span.attributes["db.statement"] 104 | assert "db.parameters" in select_span.attributes 105 | assert "db.result_columns" in select_span.attributes 106 | assert "db.row_count" in select_span.attributes 107 | 108 | # Validate SELECT has the db.result_data attribute 109 | assert "db.result_data" in select_span.attributes 110 | result_data = eval(select_span.attributes["db.result_data"]) # Convert string representation to list of dicts 111 | assert isinstance(result_data, list) 112 | assert len(result_data) > 0 113 | assert isinstance(result_data[0], dict) 114 | assert "users_id" in result_data[0] # For some reason SQLAlchemy does "SELECT users.id AS users_id" 115 | assert "users_name" in result_data[0] # # For some reason SQLAlchemy does "SELECT users.name AS users_name" 116 | assert result_data[0]["users_name"] == "Test User" 117 | -------------------------------------------------------------------------------- /serverside/tests/utils/test_creator.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | from unittest.mock import Mock, patch 4 | 5 | import pytest 6 | from src.utils.call_graph import CallGraph 7 | 8 | 9 | @pytest.fixture(autouse=True) 10 | def disable_network_access(): 11 | with patch("socket.socket") as mock_socket, patch("socket.create_connection") as mock_create_conn: 12 | mock_socket.side_effect = Exception("Network access not allowed during tests!") 13 | mock_create_conn.side_effect = Exception("Network access not allowed during tests!") 14 | yield 15 | 16 | 17 | @pytest.fixture 18 | def mock_docker_executor(): 19 | from pathlib import Path 20 | 21 | from src.utils.docker_executor import PytestOutput, TestCoverageItem 22 | 23 | with patch("src.utils.docker_executor.DockerExecutor") as MockDocker: 24 | mock_executor = MockDocker.return_value 25 | mock_executor.execute_with_new_files.return_value = PytestOutput( 26 | test_coverage={Path("/path/to/function.py"): TestCoverageItem(coverage=80, missing_lines=[1, 2, 3, 4])}, 27 | pytest_raw_output="test_output", 28 | ) 29 | yield mock_executor 30 | 31 | 32 | @pytest.fixture 33 | def sample_trace_json(): 34 | trace_path = Path(__file__).parent.parent / "assets" / "sample_trace.json" 35 | with open(trace_path) as f: 36 | return json.load(f) 37 | 38 | 39 | @pytest.fixture 40 | def mock_redis_client(sample_trace_json): 41 | with patch("redis.Redis") as MockRedis: 42 | mock_redis = MockRedis() 43 | mock_redis.scan_iter.return_value = ["key:1"] 44 | mock_redis.get.return_value = json.dumps(sample_trace_json).encode("utf-8") 45 | yield mock_redis 46 | 47 | 48 | @pytest.fixture 49 | def mock_openai_helper(): 50 | with patch("src.utils.integrations.openai_integration.OpenAIHelper") as MockOpenAIHelper: 51 | mock_helper = MockOpenAIHelper() 52 | mock_helper.call_chatgpt.side_effect = [ 53 | json.dumps( 54 | { 55 | "interactions": [ 56 | {"type": "DB_INTERACTION", "details": "Mock DB query", "mock_idea": "mock_db_query()"} 57 | ] 58 | } 59 | ), # Second call for INTERNAL function 60 | "```python\ndef test_calculate_average(): assert True```", # Second call for generating full pytest code 61 | ] 62 | mock_helper.extract_first_code_block.return_value = "def test_calculate_average(): assert True" 63 | yield mock_helper 64 | 65 | 66 | @pytest.fixture 67 | def github_data_mapping(): 68 | return { 69 | "calculate_average": { 70 | "github_file_path": "/path/to/function.py", 71 | "github_function_implementation": { 72 | "start_line": 10, 73 | "end_line": 20, 74 | "content": "def calculate_average(): pass", 75 | }, 76 | "github_file_content": "import numpy\ndef calculcate_average(): pass", 77 | } 78 | } 79 | 80 | 81 | @pytest.fixture 82 | def mock_repo_helper(github_data_mapping): 83 | def mock_enrich_callgraph_with_github_context(callgraph: CallGraph) -> None: 84 | for node_id in callgraph.graph.nodes: 85 | node = callgraph.graph.nodes[node_id] 86 | if "function" in node: 87 | enriched_node = github_data_mapping.get(node["function"]) 88 | if enriched_node: 89 | callgraph.graph.nodes[node_id].update(enriched_node) 90 | 91 | mock_instance = Mock() 92 | mock_instance._get_repo_by_url.return_value = Mock(html_url="http://sample.repo.url") 93 | mock_instance.enrich_callgraph_with_github_context.side_effect = mock_enrich_callgraph_with_github_context 94 | mock_instance.get_fastapi_endpoints.return_value = [ 95 | {"file_path": "/path/to/function.py", "function": "calculate_average", "line_start": 10, "line_end": 20} 96 | ] 97 | 98 | return mock_instance 99 | 100 | 101 | def test_test_coverage_creator_run(mock_redis_client, mock_openai_helper, mock_repo_helper, mock_docker_executor): 102 | from src.utils.test_creator import TestCoverageCreator 103 | 104 | with patch("src.utils.test_creator.RepoHelper", return_value=mock_repo_helper), patch( 105 | "src.utils.test_creator.OpenAIHelper", return_value=mock_openai_helper 106 | ), patch("src.utils.docker_executor.DockerExecutor", return_value=mock_docker_executor): 107 | 108 | test_creator = TestCoverageCreator(redis_client=mock_redis_client, repo_url="http://sample.repo.url") 109 | test_creator.run() 110 | 111 | # Check interactions for each ChatGPT call 112 | interaction_call = mock_openai_helper.call_chatgpt.call_args_list[0] 113 | test_generation_call = mock_openai_helper.call_chatgpt.call_args_list[1] 114 | 115 | assert ( 116 | "Please analyze the provided Python code snippet to identify any external interactions" 117 | in interaction_call[0][0] 118 | ) 119 | 120 | # Ensure the correct handling of responses 121 | assert mock_openai_helper.extract_first_code_block.called 122 | pytest_code = mock_openai_helper.extract_first_code_block.return_value 123 | assert "def test_calculate_average(): assert True" == pytest_code.strip() 124 | 125 | # Check if enriched GitHub data & mocking hints were considered in the prompt 126 | assert ( 127 | "Create a pytest file to test the endpoint 'calculate_average' at '/path/to/function.py' using the FastAPI app" 128 | in test_generation_call[0][0] 129 | ) 130 | 131 | assert "Mocking instructions (refer to the JSON files specified for details)" in test_generation_call[0][0] 132 | -------------------------------------------------------------------------------- /serverside/src/utils/docker_executor.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import logging 4 | import subprocess 5 | import tempfile 6 | import time 7 | from dataclasses import dataclass 8 | from pathlib import Path 9 | from uuid import uuid4 10 | 11 | import jwt 12 | import requests 13 | from src.config import GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY_BASE64 14 | from src.utils.integrations.github_integration import RepoHelper 15 | 16 | 17 | @dataclass 18 | class TestCoverageItem: 19 | coverage: float 20 | missing_lines: list[int] 21 | 22 | 23 | logging.basicConfig(level=logging.INFO) 24 | 25 | 26 | class PytestOutput: 27 | def __init__(self, test_coverage: dict[Path, TestCoverageItem], pytest_raw_output: str): 28 | self.test_coverage = test_coverage 29 | self.pytest_raw_output = pytest_raw_output 30 | 31 | 32 | class DockerExecutor: 33 | SPLIT_TOKEN = "====SPLIT=====" 34 | 35 | def __init__(self, repo_url): 36 | """ 37 | User repo will have .captureflow['run-tests'] 38 | """ 39 | self.repo_url = repo_url 40 | self.repo_helper = RepoHelper(repo_url=self.repo_url) 41 | 42 | def _generate_jwt(self, app_id, private_key): 43 | payload = { 44 | "iat": int(time.time()) - 60, # Issued at time 45 | "exp": int(time.time()) + 600, # JWT expiration time 46 | "iss": app_id, 47 | } 48 | token = jwt.encode(payload, private_key, algorithm="RS256") 49 | return token 50 | 51 | def _get_installation_access_token(self, installation_id, jwt): 52 | headers = {"Authorization": f"Bearer {jwt}", "Accept": "application/vnd.github.v3+json"} 53 | url = f"https://api.github.com/app/installations/{installation_id}/access_tokens" 54 | response = requests.post(url, headers=headers) 55 | return response.json()["token"] 56 | 57 | def _clone_repository(self, repo_url: str, access_token: str, output_path: Path): 58 | # Modify the repo URL to include the access token 59 | auth_repo_url = repo_url.replace("https://", f"https://x-access-token:{access_token}@") 60 | cmd = f"git clone {auth_repo_url} {output_path}" 61 | logging.info(f"Running command: {cmd}") 62 | subprocess.run(cmd.split(" ")) 63 | 64 | def _build_container(self, tag: str, repo_path: Path): 65 | cmd = f'docker build -f {repo_path / "Dockerfile.cf"} -t {tag} {repo_path}' 66 | logging.info(f"Running cmd: {cmd}") 67 | subprocess.run(cmd.split(" ")) 68 | 69 | def _run_tests_and_get_coverage(self, tag: str) -> PytestOutput: 70 | # TODO: It's temporary fix, will think about it later. 71 | cmd = f'docker run -t {tag} /bin/bash -c "cd serverside && pytest --cov=. --cov-report json >pytest_output; cat coverage.json; echo "{self.SPLIT_TOKEN}"; cat pytest_output;"' 72 | logging.info(f"Running cmd: {cmd}") 73 | proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True) 74 | 75 | cmd_output = proc.stdout.read().decode("utf-8") 76 | coverage_output, pytest_raw_output = cmd_output.split(self.SPLIT_TOKEN) 77 | coverage_output = json.loads(coverage_output) 78 | 79 | # TODO: This is temporary solution for testing. 80 | test_coverage = { 81 | f"serverside/{key}": TestCoverageItem( 82 | coverage=float(info_dict["summary"]["percent_covered"]), missing_lines=list(info_dict["missing_lines"]) 83 | ) 84 | for key, info_dict in coverage_output["files"].items() 85 | } 86 | 87 | return PytestOutput(test_coverage=test_coverage, pytest_raw_output=pytest_raw_output) 88 | 89 | def _create_files(self, repo_dir: Path, new_files: dict[str, str]): 90 | for file_path, contents in new_files.items(): 91 | # TODO: This is done temporarily, will change it later to properly copy it to docker. 92 | with open(repo_dir / file_path, "w") as f: 93 | logging.info(f"Creating new file at: {repo_dir / file_path}") 94 | f.write(contents) 95 | 96 | def execute_with_new_files(self, new_files: dict[str, str]) -> PytestOutput: 97 | """ 98 | new_files: 99 | { 100 | '/path/to/new/test_1': 'def test_blah():\n return True', 101 | '/path/to/new/test_2': 'def test_blah2():\n return True', 102 | } 103 | 104 | gh_repo.clone_repo() 105 | run tests from command with coverage 106 | return PytestOutput 107 | """ 108 | APP_ID = GITHUB_APP_ID 109 | PRIVATE_KEY = base64.b64decode(GITHUB_APP_PRIVATE_KEY_BASE64).decode("utf-8") 110 | installation = self.repo_helper.get_installation_by_url(self.repo_url) 111 | 112 | jwt_key = self._generate_jwt(APP_ID, PRIVATE_KEY) 113 | access_token = self._get_installation_access_token(installation.id, jwt_key) 114 | 115 | with tempfile.TemporaryDirectory(dir=Path.cwd()) as repo_dir: 116 | logging.info(f"Created temporary directory to clone repo: {repo_dir}") 117 | self._clone_repository(self.repo_url, access_token, output_path=repo_dir) 118 | if len(new_files) > 0: 119 | self._create_files(Path(repo_dir), new_files) 120 | tag = str(uuid4()).split("-")[0] 121 | self._build_container(tag=tag, repo_path=Path(repo_dir)) 122 | 123 | pytest_output = self._run_tests_and_get_coverage(tag=tag) 124 | return pytest_output 125 | 126 | 127 | def main(): 128 | docker_executor = DockerExecutor("https://github.com/CaptureFlow/captureflow-py") 129 | # Valid input 130 | pytest_output = docker_executor.execute_with_new_files({"serverside/tests/test_a.py": "print(1)"}) 131 | print(pytest_output.test_coverage) 132 | 133 | # Invalid input 134 | pytest_output = docker_executor.execute_with_new_files({"serverside/tests/test_a.py": "ppprint(1)"}) 135 | print(pytest_output.test_coverage) 136 | 137 | 138 | if __name__ == "__main__": 139 | main() 140 | -------------------------------------------------------------------------------- /serverside/tests/utils/test_exception_patcher.py: -------------------------------------------------------------------------------- 1 | import json 2 | import socket 3 | from pathlib import Path 4 | from unittest.mock import Mock, patch 5 | 6 | import pytest 7 | from src.utils.call_graph import CallGraph 8 | from src.utils.exception_patcher import ExceptionPatcher 9 | 10 | 11 | @pytest.fixture(autouse=True) 12 | def disable_network_access(): 13 | def guard(*args, **kwargs): 14 | raise Exception("Network access not allowed during tests!") 15 | 16 | orig_socket = socket.socket 17 | orig_create_connection = socket.create_connection 18 | socket.socket = guard 19 | socket.create_connection = guard 20 | 21 | yield 22 | 23 | socket.socket = orig_socket 24 | socket.create_connection = orig_create_connection 25 | 26 | 27 | @pytest.fixture 28 | def sample_trace_json(): 29 | trace_path = Path(__file__).parent.parent / "assets" / "sample_trace_with_exception.json" 30 | with open(trace_path) as f: 31 | return json.load(f) 32 | 33 | 34 | @pytest.fixture 35 | def mock_redis_client(sample_trace_json): 36 | with patch("redis.Redis") as MockRedis: 37 | mock_redis_client = MockRedis() 38 | mock_redis_client.scan_iter.return_value = [f"key:{i}" for i in range(3)] 39 | mock_redis_client.get.side_effect = lambda k: json.dumps(sample_trace_json).encode("utf-8") 40 | yield mock_redis_client 41 | 42 | 43 | @pytest.fixture 44 | def mock_openai_helper(): 45 | with patch("src.utils.integrations.openai_integration.OpenAIHelper") as MockOpenAIHelper: 46 | mock_helper = MockOpenAIHelper() 47 | # Mocking expected ChatGPT response structure 48 | dummy_function_code = "def dummy_function(): pass" 49 | mock_response_json_str = json.dumps( 50 | { 51 | "confidence": 5, 52 | "function_name": "calculate_avg", 53 | "change_reasoning": "Just a mock response.", 54 | } 55 | ) 56 | mock_response_code_str = f"```python\n{dummy_function_code}```" 57 | mock_response = f"{mock_response_json_str}\n{mock_response_code_str}" 58 | mock_helper.call_chatgpt.return_value = mock_response 59 | mock_helper.extract_first_code_block.return_value = dummy_function_code 60 | 61 | yield mock_helper 62 | 63 | 64 | @pytest.fixture 65 | def github_data_mapping(): 66 | return { 67 | "calculate_average": { 68 | "github_file_path": "/path/to/calculate_average.py", 69 | "github_function_implementation": { 70 | "start_line": 1, 71 | "end_line": 5, 72 | "content": "def calculate_average(): pass", 73 | }, 74 | "github_file_content": "import numpy\ndef calculate_average(): pass", 75 | }, 76 | "calculate_sum": { 77 | "github_file_path": "/path/to/calculate_sum.py", 78 | "github_function_implementation": { 79 | "start_line": 1, 80 | "end_line": 5, 81 | "content": "def calculate_sum(): pass", 82 | }, 83 | "github_file_content": "import numpy\ndef calculate_sum(): pass", 84 | }, 85 | } 86 | 87 | 88 | @pytest.fixture 89 | def mock_repo_helper(github_data_mapping): 90 | def mock_enrich_callgraph_with_github_context(callgraph: CallGraph) -> None: 91 | for node_id in callgraph.graph.nodes: 92 | node = callgraph.graph.nodes[node_id] 93 | if "function" in node: 94 | enriched_node = github_data_mapping.get(node["function"]) 95 | if enriched_node: 96 | callgraph.graph.nodes[node_id].update(enriched_node) 97 | 98 | mock_instance = Mock() 99 | mock_instance._get_repo_by_url.return_value = Mock(html_url="http://sample.repo.url") 100 | mock_instance.enrich_callgraph_with_github_context.side_effect = mock_enrich_callgraph_with_github_context 101 | 102 | return mock_instance 103 | 104 | 105 | def test_bug_orchestrator_run(mock_redis_client, mock_openai_helper, mock_repo_helper): 106 | with patch("src.utils.exception_patcher.RepoHelper", return_value=mock_repo_helper), patch( 107 | "src.utils.exception_patcher.OpenAIHelper", return_value=mock_openai_helper 108 | ): 109 | orchestrator = ExceptionPatcher(redis_client=mock_redis_client, repo_url="http://sample.repo.url") 110 | orchestrator.run() 111 | 112 | # Call arguments for mock_openai_helper.call_chatgpt 113 | actual_prompt = mock_openai_helper.call_chatgpt.call_args[0][0] 114 | 115 | assert "Exception Chain Analysis:" in actual_prompt 116 | assert "Function: calculate_average" in actual_prompt 117 | assert "Function: calculate_avg" in actual_prompt 118 | assert "ZeroDivisionError - division by zero" in actual_prompt 119 | expected_function_implementation_snippets = ["def calculate_average(): pass", "def calculate_sum(): pass"] 120 | for snippet in expected_function_implementation_snippets: 121 | assert snippet in actual_prompt 122 | 123 | # Validate the call to create_pull_request_with_new_function 124 | mock_repo_helper.create_pull_request_with_new_function.assert_called() 125 | called_args = mock_repo_helper.create_pull_request_with_new_function.call_args[0] 126 | node_arg = called_args[0] 127 | 128 | # Validate key fields of the node argument 129 | assert node_arg.get("function") == "calculate_avg" 130 | assert node_arg.get("did_raise") is True 131 | assert node_arg.get("unhandled_exception").get("type") == "ZeroDivisionError" 132 | assert node_arg.get("unhandled_exception").get("value") == "division by zero" 133 | 134 | # There are two exception nodes in sample trace json 135 | exception_context = called_args[1] 136 | assert len(exception_context["exception_nodes"]) == 2 137 | 138 | # New function code is exactly what mock ChatGPT returned 139 | new_function_code = called_args[2] 140 | expected_new_function_code = "def dummy_function(): pass" 141 | assert new_function_code.strip() == expected_new_function_code.strip() 142 | -------------------------------------------------------------------------------- /serverside/src/utils/integrations/openai_integration.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import re 4 | from typing import Optional 5 | 6 | from openai import OpenAI 7 | from src.config import OPENAI_KEY 8 | 9 | logger = logging.getLogger(__name__) 10 | logger.setLevel(logging.INFO) 11 | 12 | 13 | class OpenAIHelper: 14 | def __init__(self): 15 | self.client = self._create_openai_client() 16 | 17 | def _create_openai_client(self) -> OpenAI: 18 | return OpenAI(api_key=OPENAI_KEY) 19 | 20 | def generate_initial_scoring_query(self, node) -> str: 21 | cur_fun_name = node["function"] 22 | cur_fun_impl = node["github_function_implementation"] 23 | cur_file_impl = node["github_file_content"] 24 | 25 | query = f""" 26 | Imagine that you're the most competent programmer in San Francisco. 27 | You are tasked with making very safe update to a FUNCTION (not anything else), 28 | but you can improve readability/logic if you're 100% function will do exactly the same thing. 29 | 30 | target_function: {cur_fun_name} 31 | function_code: {cur_fun_impl} 32 | 33 | whole file code for context: ```{cur_file_impl}``` 34 | 35 | Please output JSON structuring your view on the question. It needs to have two fields: "quality_score" how much would you rate quality of this function from 1 to 10 and "easy_to_optimize" to label that takes values "EASY_TO_OPTIMIZE", "MAYBE_OPTIMIZE", "HARD_OPTIMIZE" representing if there is a safe refactoring available. 36 | """ 37 | 38 | return query 39 | 40 | def generate_improvement_query(self, call_graph, node) -> str: 41 | # Extract the required details from the log data 42 | cur_fun_name = node["function"] 43 | cur_fun_path = node["github_file_path"] 44 | cur_fun_impl = node["github_function_implementation"] 45 | cur_fun_input = node["arguments"] 46 | cur_fun_output = node["return_value"] 47 | cur_file_impl = node["github_file_content"] 48 | 49 | # parent_nodes = list(call_graph.graph.predecessors(node_id)) 50 | # children = list(call_graph.graph.successors(node_id)) 51 | 52 | query = f""" 53 | Imagine that you're the most competent programmer in San Francisco. 54 | You are tasked with making very safe update to a FUNCTION (not anything else), 55 | but you can improve readability/logic if you're 100% function will do exactly the same thing. 56 | 57 | How function is actually implemented: 58 | 59 | path: {cur_fun_path}, target_function: {cur_fun_name} 60 | function_code: {cur_fun_impl} 61 | example input: {cur_fun_input} 62 | example output: {cur_fun_output} 63 | 64 | whole file code for context: ```{cur_file_impl}``` 65 | 66 | Please output only single thing, the proposed code of the same function. You can also leave comment in it, asking for for follow-ups. 67 | """ 68 | 69 | return query 70 | 71 | def generate_simulation_query(self, call_graph, node) -> str: 72 | # Extract the required details from the log data 73 | cur_fun_name = node["function"] 74 | cur_fun_path = node["github_file_path"] 75 | cur_fun_impl = ( 76 | node["github_function_implementation"]["content"] 77 | if "github_function_implementation" in node 78 | else "Function implementation not found." 79 | ) 80 | cur_fun_input = json.dumps(node.get("input_value", {}), indent=2) 81 | cur_fun_output = json.dumps(node.get("return_value", {}), indent=2) 82 | cur_file_impl = node["github_file_content"] 83 | 84 | query = f""" 85 | As a highly skilled software engineer, you're reviewing a Python function to ensure its correctness and readability. Here's the task: 86 | 87 | - File path: {cur_fun_path} 88 | - Target function: {cur_fun_name} 89 | 90 | The current implementation of the function is as follows: 91 | ```python 92 | {cur_fun_impl} 93 | ``` 94 | 95 | Given an example input: 96 | ``` 97 | {cur_fun_input} 98 | ``` 99 | 100 | The function is expected to produce the following output: 101 | ``` 102 | {cur_fun_output} 103 | ``` 104 | 105 | The context of the whole file where the function is located is provided for better understanding: 106 | ```python 107 | {cur_file_impl} 108 | ``` 109 | 110 | Simulate the environment: Run the improved function with the given example input and compare the output to the expected output. 111 | Finally, provide a confidence level (from 0 to 100%) on whether the improved function will consistently produce the correct output across various inputs, similar to the example provided. 112 | I only need one of three enums in your response "MUST_WORK", "MAYBE_WORK", "DOESNT_WORK". It will show how condident you are new function will function in exactly the same way. 113 | 114 | Also note that inputs and outputs are serialized but probably they're python objects you can deduct from this seralization code 115 | ```python 116 | def _serialize_variable(self, value: Any) -> Dict[str, Any]: 117 | try: 118 | json_value = json.dumps(value, default=str) 119 | except TypeError: 120 | json_value = str(value) 121 | return {{ 122 | "python_type": str(type(value)), 123 | "json_serialized": json_value 124 | }} 125 | ``` 126 | """ 127 | 128 | return query.strip() 129 | 130 | def generate_after_insert_style_query(self, new_file_code, function_name) -> str: 131 | query = f""" 132 | I have programatically changed source code of my file attempting to rewrite function called {function_name}. 133 | Important: I will give you source code of a file that contains this function, please make sure it aligns well with files (style, tabs, etc). 134 | Do nothing more and give me whole new script (even if nothing needs to be changed)! 135 | 136 | Here's script text: {new_file_code} 137 | """ 138 | 139 | return query.strip() 140 | 141 | def call_chatgpt(self, query: str) -> str: 142 | chat_completion = self.client.chat.completions.create( 143 | messages=[ 144 | { 145 | "role": "user", 146 | "content": query, 147 | }, 148 | {"role": "system", "content": "You are a helpful assistant."}, 149 | ], 150 | model="gpt-4", 151 | ) 152 | 153 | assert len(chat_completion.choices) == 1 154 | 155 | return chat_completion.choices[0].message.content 156 | 157 | @staticmethod 158 | def extract_first_code_block(text: str) -> Optional[str]: 159 | pattern = r"```(.*?)```" 160 | match = re.search(pattern, text, re.DOTALL) 161 | code = match.group(1) 162 | 163 | if match: 164 | code = code.lstrip("python") 165 | code = code.lstrip("\n") 166 | return code 167 | 168 | return None 169 | -------------------------------------------------------------------------------- /examples/fastapi-carshop-erp-example/fastapi-bigger-application-master/app/test/test_api/test_sales.py: -------------------------------------------------------------------------------- 1 | from fastapi.testclient import TestClient 2 | 3 | from ..database_test import configure_test_database, clear_database 4 | 5 | from ..templates.sale_tempĺates import sale_request_json, sale_response_json, sale_not_found_error, sale_all_not_found_error 6 | 7 | from ..templates.car_tempĺates import car_json, car_not_found_error 8 | 9 | from ..templates.stock_tempĺates import stock_request_json, stock_not_found_error, stock_out_of_stock, stock_request_json_out_of_stock 10 | 11 | from ..templates.seller_tempĺates import seller_json, seller_not_found_error 12 | 13 | from ..templates.buyer_tempĺates import buyer_json, buyer_not_found_error 14 | 15 | from ..base_insertion import insert_into_sales, insert_into_cars, insert_into_stocks, insert_into_sellers, insert_into_buyers, read_stock_by_id 16 | 17 | from ...main import app 18 | 19 | 20 | client = TestClient(app) 21 | 22 | sales_route = "/api/v1/sales" 23 | 24 | 25 | def setup_module(module): 26 | configure_test_database(app) 27 | 28 | 29 | def setup_function(module): 30 | clear_database() 31 | 32 | 33 | def test_create_sale(car_json, stock_request_json, seller_json, buyer_json, sale_request_json, sale_response_json): 34 | ''' Create a sale with success ''' 35 | insert_into_cars(car_json) 36 | insert_into_stocks(stock_request_json) 37 | insert_into_buyers(buyer_json) 38 | insert_into_sellers(seller_json) 39 | 40 | response = client.post(sales_route + "/", json=sale_request_json) 41 | assert response.status_code == 201 42 | sale_response_json["created_at"] = response.json()["created_at"] 43 | assert response.json() == sale_response_json 44 | db_stock = read_stock_by_id(stock_request_json["id"]) 45 | assert (stock_request_json["quantity"] - 1) == db_stock["quantity"] 46 | 47 | 48 | def test_read_sale(car_json, stock_request_json, seller_json, buyer_json, sale_request_json, sale_response_json): 49 | ''' Read a sale with success ''' 50 | insert_into_cars(car_json) 51 | insert_into_stocks(stock_request_json) 52 | insert_into_buyers(buyer_json) 53 | insert_into_sellers(seller_json) 54 | insert_into_sales(sale_request_json) 55 | request_url = sales_route + "/1" 56 | response = client.get(request_url) 57 | assert response.status_code == 200 58 | sale_response_json["created_at"] = response.json()["created_at"] 59 | assert response.json() == sale_response_json 60 | 61 | 62 | def test_read_sales(car_json, stock_request_json, seller_json, buyer_json, sale_request_json, sale_response_json): 63 | ''' Read all sales paginated with success ''' 64 | insert_into_cars(car_json) 65 | insert_into_stocks(stock_request_json) 66 | insert_into_buyers(buyer_json) 67 | insert_into_sellers(seller_json) 68 | insert_into_sales(sale_request_json) 69 | request_url = sales_route + "?skip=0&limit=100" 70 | response = client.get(request_url) 71 | assert response.status_code == 200 72 | sale_response_json["created_at"] = response.json()[0]["created_at"] 73 | assert response.json() == [ sale_response_json ] 74 | 75 | 76 | def test_delete_sale(car_json, stock_request_json, seller_json, buyer_json, sale_request_json): 77 | ''' Delete a sale with success ''' 78 | insert_into_cars(car_json) 79 | insert_into_stocks(stock_request_json) 80 | insert_into_buyers(buyer_json) 81 | insert_into_sellers(seller_json) 82 | insert_into_sales(sale_request_json) 83 | request_url = sales_route + "/1" 84 | response = client.delete(request_url) 85 | assert response.status_code == 200 86 | assert response.json() == True 87 | 88 | 89 | def test_read_sale_not_found(sale_not_found_error): 90 | ''' Read a sale when not found ''' 91 | request_url = sales_route + "/1" 92 | response = client.get(request_url) 93 | assert response.status_code == 404 94 | assert response.json() == sale_not_found_error 95 | 96 | 97 | def test_read_sales_not_found(): 98 | ''' Read all sales paginated when not found ''' 99 | request_url = sales_route + "?skip=0&limit=100" 100 | response = client.get(request_url) 101 | assert response.status_code == 200 102 | assert response.json() == [] 103 | 104 | 105 | def test_delete_sale_not_found(sale_not_found_error): 106 | ''' Delete a sale when not exists ''' 107 | request_url = sales_route + "/1" 108 | response = client.delete(request_url) 109 | assert response.status_code == 404 110 | assert response.json() == sale_not_found_error 111 | 112 | 113 | def test_create_sale_car_not_found(stock_request_json, seller_json, buyer_json, sale_request_json, car_not_found_error): 114 | ''' Create a sale when car not found ''' 115 | insert_into_stocks(stock_request_json) 116 | insert_into_buyers(buyer_json) 117 | insert_into_sellers(seller_json) 118 | 119 | response = client.post(sales_route + "/", json=sale_request_json) 120 | assert response.status_code == 404 121 | assert response.json() == car_not_found_error 122 | 123 | 124 | def test_create_sale_buyer_not_found(car_json, stock_request_json, seller_json, sale_request_json, buyer_not_found_error): 125 | ''' Create a sale when buyer not found ''' 126 | insert_into_cars(car_json) 127 | insert_into_stocks(stock_request_json) 128 | insert_into_sellers(seller_json) 129 | 130 | response = client.post(sales_route + "/", json=sale_request_json) 131 | assert response.status_code == 404 132 | assert response.json() == buyer_not_found_error 133 | 134 | 135 | def test_create_sale_seller_not_found(car_json, stock_request_json, buyer_json, sale_request_json, seller_not_found_error): 136 | ''' Create a sale when seller not found ''' 137 | insert_into_cars(car_json) 138 | insert_into_stocks(stock_request_json) 139 | insert_into_buyers(buyer_json) 140 | 141 | response = client.post(sales_route + "/", json=sale_request_json) 142 | assert response.status_code == 404 143 | assert response.json() == seller_not_found_error 144 | 145 | 146 | def test_create_sale_stock_not_found(car_json, seller_json, buyer_json, sale_request_json, stock_not_found_error): 147 | ''' Create a sale when stock not found ''' 148 | insert_into_cars(car_json) 149 | insert_into_buyers(buyer_json) 150 | insert_into_sellers(seller_json) 151 | 152 | response = client.post(sales_route + "/", json=sale_request_json) 153 | assert response.status_code == 404 154 | assert response.json() == stock_not_found_error 155 | 156 | 157 | def test_create_sale_out_of_stock(car_json, seller_json, buyer_json, stock_request_json_out_of_stock, sale_request_json, stock_out_of_stock): 158 | ''' Create a sale when stock has no quantity available ''' 159 | insert_into_cars(car_json) 160 | insert_into_buyers(buyer_json) 161 | insert_into_sellers(seller_json) 162 | insert_into_stocks(stock_request_json_out_of_stock) 163 | 164 | response = client.post(sales_route + "/", json=sale_request_json) 165 | assert response.status_code == 422 166 | assert response.json() == stock_out_of_stock 167 | 168 | 169 | def test_create_sale_all_not_found(sale_request_json, sale_all_not_found_error): 170 | ''' Create a sale when stock has no quantity available ''' 171 | response = client.post(sales_route + "/", json=sale_request_json) 172 | assert response.status_code == 404 173 | assert response.json() == sale_all_not_found_error 174 | 175 | -------------------------------------------------------------------------------- /clientside/src/captureflow/tracer.py: -------------------------------------------------------------------------------- 1 | """Module for tracing function calls in Python applications with remote logging capability.""" 2 | 3 | import asyncio 4 | import inspect 5 | import json 6 | import linecache 7 | import logging 8 | import os 9 | import sys 10 | import traceback 11 | import uuid 12 | from datetime import datetime 13 | from functools import wraps 14 | from typing import Any, Callable, Dict 15 | 16 | import httpx 17 | import requests 18 | 19 | STDLIB_PATH = "/lib/python" 20 | LIBRARY_PATH = "/site-packages/" 21 | TEMP_FOLDER = "temp/" 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | class Tracer: 27 | def __init__(self, repo_url: str, server_base_url: str = "http://127.0.0.1:8000"): 28 | """Initialize the tracer with the repository URL and optionally the remote logging URL.""" 29 | self.repo_url = repo_url 30 | self.trace_endpoint_url = f"{server_base_url.rstrip('/')}/api/v1/traces" 31 | 32 | def trace_endpoint(self, func: Callable) -> Callable: 33 | """Decorator to trace endpoint function calls.""" 34 | 35 | # TODO: Give option to specify log "verbosity" 36 | # Max verbosity => need "heavy" sys.settrace() 37 | # Subgoal: investigate sampling of requests (e.g. log every N-th request) to make it less "heavy" 38 | # Min verbosity (e.g. only exceptions) => we could patch Flask/FastAPI methods and that would be better performance wise 39 | @wraps(func) 40 | async def wrapper(*args, **kwargs) -> Any: 41 | try: 42 | invocation_id = str(uuid.uuid4()) 43 | context = { 44 | "invocation_id": invocation_id, 45 | "timestamp": datetime.now().isoformat(), 46 | "endpoint": func.__qualname__, 47 | "input": { 48 | "args": [self._serialize_variable(arg) for arg in args], 49 | "kwargs": {k: self._serialize_variable(v) for k, v in kwargs.items()}, 50 | }, 51 | "execution_trace": [], 52 | "log_filename": f"{TEMP_FOLDER}{func.__name__}_trace_{invocation_id}.json", 53 | } 54 | 55 | sys.settrace(self._setup_trace(context)) 56 | result = await func(*args, **kwargs) if asyncio.iscoroutinefunction(func) else func(*args, **kwargs) 57 | context["output"] = {"result": self._serialize_variable(result)} 58 | finally: 59 | sys.settrace(None) 60 | await self._send_trace_log(context) 61 | 62 | return result 63 | 64 | return wrapper 65 | 66 | async def _send_trace_log(self, context: Dict[str, Any]) -> None: 67 | """Asynchronously send trace log to the specified endpoint.""" 68 | # If in development, optionally save the trace log locally 69 | if os.getenv("CAPTUREFLOW_DEV_SERVER") == "true": 70 | log_filename = f"trace_{context['invocation_id']}.json" 71 | with open(log_filename, "w") as f: 72 | json.dump(context, f, indent=4) 73 | 74 | try: 75 | # Perform the POST request asynchronously 76 | async with httpx.AsyncClient() as client: 77 | response = await client.post( 78 | self.trace_endpoint_url, 79 | params={"repository-url": self.repo_url}, 80 | json=context, 81 | headers={"Content-Type": "application/json"}, 82 | ) 83 | if response.status_code != 200: 84 | logger.error(f"CaptureFlow server responded with {response.status_code}: {response.text}") 85 | except Exception as e: 86 | logger.error(f"Exception during logging: {e}") 87 | 88 | def _serialize_variable(self, value: Any) -> Dict[str, Any]: 89 | try: 90 | json_value = json.dumps(value) 91 | except Exception as e: 92 | try: 93 | json_value = str(value) # If the value cannot be serialized to JSON, use str() / repr() 94 | except Exception as e: 95 | json_value = "" # Very rare case, but can happen with e.g. MagicMocks 96 | logger.info(f"Failed to convert variable to string. Type: {type(value)}, Error: {e}") 97 | 98 | return {"python_type": str(type(value)), "json_serialized": json_value} 99 | 100 | def _get_file_tag(self, file_path: str) -> str: 101 | """Determine the file tag based on the file path.""" 102 | if STDLIB_PATH in file_path: 103 | return "STDLIB" 104 | elif LIBRARY_PATH in file_path: 105 | return "LIBRARY" 106 | return "INTERNAL" 107 | 108 | def _setup_trace(self, context: Dict[str, Any]) -> Callable: 109 | """Setup the trace function.""" 110 | context["call_stack"] = [] 111 | return lambda frame, event, arg: self._trace_function_calls(frame, event, arg, context) 112 | 113 | def _capture_arguments(self, frame) -> Dict[str, Any]: 114 | """ 115 | Capture arguments passed to the function and serialize them. 116 | This simplified version does not distinguish between args and kwargs based on the function's signature. 117 | """ 118 | args, _, _, values = inspect.getargvalues(frame) 119 | 120 | serialized_args = [] 121 | serialized_kwargs = {} 122 | for arg in args: 123 | serialized_value = self._serialize_variable(values[arg]) 124 | if arg == "args" or arg.startswith("arg"): 125 | serialized_args.append(serialized_value) 126 | else: 127 | serialized_kwargs[arg] = serialized_value 128 | 129 | return {"args": serialized_args, "kwargs": serialized_kwargs} 130 | 131 | def _trace_function_calls(self, frame, event, arg, context: Dict[str, Any]) -> Callable: 132 | """Trace function calls and capture relevant data.""" 133 | code = frame.f_code 134 | func_name, file_name, line_no = code.co_name, code.co_filename, frame.f_lineno 135 | 136 | tag = self._get_file_tag(file_name) 137 | 138 | # Skip STDLIB, LIBRARY, and everything that does not start with '/' (like /usr/app/src etc) 139 | if tag == "STDLIB" or tag == "LIBRARY" or not file_name.startswith("/"): 140 | return lambda frame, event, arg: self._trace_function_calls(frame, event, arg, context) 141 | 142 | # Skip lines for now 143 | if event == "line": 144 | return lambda frame, event, arg: self._trace_function_calls(frame, event, arg, context) 145 | 146 | caller_id = context["call_stack"][-1]["id"] if context["call_stack"] else None 147 | 148 | call_id = str(uuid.uuid4()) 149 | trace_event = { 150 | "id": call_id, 151 | "timestamp": datetime.now().isoformat(), 152 | "event": event, 153 | "function": func_name, 154 | "caller_id": caller_id, 155 | "file": file_name, 156 | "line": line_no, 157 | "source_line": linecache.getline(file_name, line_no).strip(), 158 | "tag": tag, 159 | } 160 | 161 | if event == "call": 162 | trace_event["arguments"] = self._capture_arguments(frame) 163 | context["call_stack"].append(trace_event) 164 | elif event == "return": 165 | trace_event["return_value"] = self._serialize_variable(arg) 166 | # Also update "call" frame, because it's quick 167 | if context["call_stack"]: 168 | context["call_stack"][-1]["return_value"] = self._serialize_variable(arg) 169 | context["call_stack"].pop() 170 | elif event == "exception": 171 | exc_type, exc_value, exc_traceback = arg 172 | trace_event["exception_info"] = { 173 | "type": str(exc_type.__name__), 174 | "value": str(exc_value), 175 | "traceback": traceback.format_tb(exc_traceback), 176 | } 177 | 178 | context["execution_trace"].append(trace_event) 179 | 180 | return lambda frame, event, arg: self._trace_function_calls(frame, event, arg, context) 181 | -------------------------------------------------------------------------------- /serverside/src/utils/call_graph.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | 4 | import networkx as nx 5 | 6 | logger = logging.getLogger(__name__) 7 | logger.setLevel(logging.INFO) 8 | 9 | 10 | class CallGraph: 11 | def __init__(self, log_data: str): 12 | self.graph = nx.DiGraph() 13 | self._build_graph(log_data) 14 | 15 | def _build_graph(self, log_data: str) -> None: 16 | data = json.loads(log_data) if isinstance(log_data, str) else log_data 17 | # Track nodes that threw exceptions 18 | exception_nodes = {} 19 | 20 | for event in data["execution_trace"]: 21 | if event["event"] in ["call", "exception"]: 22 | if event["event"] == "call": 23 | node_attrs = { 24 | "function": event["function"], 25 | "file_line": f"{event['file']}:{event['line']}", 26 | "tag": event.get("tag", "INTERNAL"), 27 | "arguments": event.get("arguments", {}), 28 | "return_value": event.get("return_value", {}), 29 | "exception": False, # Initialize nodes with no exception 30 | } 31 | elif event["event"] == "exception": 32 | # Update the caller node with exception info 33 | caller_node = self.graph.nodes[event["caller_id"]] 34 | caller_node["did_raise"] = True 35 | caller_node["unhandled_exception"] = { 36 | "type": event["exception_info"]["type"], 37 | "value": event["exception_info"]["value"], 38 | "traceback": event["exception_info"]["traceback"], 39 | } 40 | 41 | continue 42 | 43 | self.graph.add_node(event["id"], **node_attrs) 44 | 45 | # Add an edge from the caller to the current function call 46 | caller_id = event.get("caller_id") 47 | if caller_id and caller_id in self.graph: 48 | self.graph.add_edge(caller_id, event["id"]) 49 | 50 | # If the caller had an exception, link this event as part of the exception chain 51 | if caller_id in exception_nodes: 52 | self.graph.add_edge(exception_nodes[caller_id], event["id"]) 53 | 54 | # After building the graph, calculate descendants for all nodes 55 | for node in list(self.graph.nodes): 56 | self._calculate_descendants(node) 57 | 58 | def iterate_graph(self) -> None: 59 | """Iterates through the graph, printing details of each node and its successors.""" 60 | for node, attrs in self.graph.nodes(data=True): 61 | function_name = attrs["function"] 62 | file_line = attrs["file_line"] 63 | tag = attrs["tag"] 64 | successors = ", ".join(self.graph.nodes[succ]["function"] for succ in self.graph.successors(node)) 65 | logging.info(f"Function: {function_name} ({file_line}, {tag}) -> {successors or 'No outgoing calls'}") 66 | 67 | def export_for_graphviz(self) -> None: 68 | """Exports the graph in a format compatible with Graphviz.""" 69 | nodes = [(node, self.graph.nodes[node]) for node in self.graph.nodes()] 70 | edges = list(self.graph.edges()) 71 | return nodes, edges 72 | 73 | def find_node_by_fname(self, function_name: str) -> list[any]: 74 | """Finds nodes that correspond to a given function name.""" 75 | matching_nodes = [] 76 | for node, attrs in self.graph.nodes(data=True): 77 | if attrs["function"] == function_name: 78 | matching_nodes.append(node) 79 | return matching_nodes 80 | 81 | def compress_graph(self): 82 | changes_made = True 83 | while changes_made: 84 | changes_made = False 85 | node_depths = self._calculate_depths() # Recalculate depths on each iteration 86 | nodes_by_depth = sorted(self.graph.nodes(), key=lambda n: node_depths.get(n, 0), reverse=True) 87 | 88 | for node in nodes_by_depth: 89 | if self.graph.has_node(node) and self.graph.nodes[node].get("total_children_count", 0) > 50: 90 | # Remove this node's children 91 | children = list(self.graph.successors(node)) 92 | for child in children: 93 | self._remove_descendants(child) 94 | 95 | self.graph.nodes[node]["total_children_count"] = 0 96 | self.graph.nodes[node]["is_node_compressed"] = True # Mark this node as compressed 97 | changes_made = True # Indicate changes for another pass 98 | 99 | # changes_made = False 100 | 101 | # Recalculate descendants at the end of each full pass 102 | for node in self.graph.nodes(): 103 | self._calculate_descendants(node) 104 | 105 | def _remove_descendants(self, node): 106 | """Recursively remove a node and all its descendants from the graph.""" 107 | # List all children (successors) of the node 108 | children = list(self.graph.successors(node)) 109 | for child in children: 110 | self._remove_descendants(child) # Recursively remove each child 111 | 112 | # After all children are removed, remove the node itself 113 | self.graph.remove_node(node) 114 | 115 | def _calculate_depths(self): 116 | """Calculate depth for each node based on the longest path to any leaf.""" 117 | node_depths = {} 118 | # Ensure calculation respects topological order 119 | try: 120 | for node in nx.topological_sort(self.graph): # Ensures we calculate from leaves to root 121 | if not list(self.graph.predecessors(node)): # If no children, depth is 0 122 | node_depths[node] = 0 123 | self.graph.nodes[node]["depth"] = 0 124 | else: 125 | # Only consider children that are still in the graph 126 | # node_depths[node] = max((node_depths[child] + 1 for child in self.graph.successors(node) if child in node_depths), default=0) 127 | node_depths[node] = max( 128 | (node_depths[child] + 1 for child in self.graph.predecessors(node) if child in node_depths), 129 | default=0, 130 | ) 131 | self.graph.nodes[node]["depth"] = node_depths[node] 132 | except nx.NetworkXError as e: 133 | logger.error(f"Failed to calculate depths, possibly due to cyclic dependency: {e}") 134 | return {} 135 | return node_depths 136 | 137 | def _calculate_descendants(self, node): 138 | """Recalculate the total number of descendants for each node.""" 139 | if not list(self.graph.successors(node)): # If no children 140 | self.graph.nodes[node]["total_children_count"] = 0 141 | return 0 142 | total_count = 0 143 | for successor in self.graph.successors(node): 144 | child_count = self._calculate_descendants(successor) 145 | total_count += child_count + 1 146 | self.graph.nodes[node]["total_children_count"] = total_count 147 | return total_count 148 | 149 | def draw(self, output_filename="func_call_graph", compressed=False): 150 | from graphviz import Digraph 151 | 152 | dot = Digraph(comment="Function Call Graph") 153 | color_mapping = {"STDLIB": "gray", "LIBRARY": "blue", "INTERNAL": "white"} 154 | compressed_color = "lightgrey" # Color for compressed nodes 155 | 156 | if compressed: 157 | self.compress_graph() # Compress the graph before drawing 158 | 159 | nodes = [(node, self.graph.nodes[node]) for node in self.graph.nodes()] 160 | edges = list(self.graph.edges()) 161 | 162 | for node, attrs in nodes: 163 | tag = attrs["tag"] 164 | node_color = color_mapping.get("EXCEPTION" if attrs["exception"] else tag, "white") 165 | 166 | if attrs.get("is_node_compressed", False): 167 | node_color = compressed_color # Use special color for compressed nodes 168 | else: 169 | node_color = color_mapping.get(tag, "white") # Use default colors for tags 170 | 171 | label_parts = [ 172 | f"Function: {attrs['function']}", 173 | f"Tag: {tag}", 174 | f"Total Children Count: {attrs['total_children_count']}", 175 | f"Depth = {attrs['depth']}", 176 | f"Compression Status: {'Compressed' if attrs.get('is_node_compressed', False) else 'Not Compressed'}", 177 | # f"Arguments: {json.dumps(attrs.get('arguments', {}), indent=2)}", 178 | # f"Returns: {json.dumps(attrs.get('return_value', {}), indent=2)}", 179 | f"Did Raise: {attrs.get('did_raise', {})}", 180 | f"Traceback: {attrs.get('unhandled_exception', {})}", 181 | ] 182 | 183 | dot.node(node, label="\n".join(label_parts), style="filled", fillcolor=node_color) 184 | 185 | for u, v in edges: 186 | dot.edge(u, v) 187 | 188 | dot.render(output_filename, view=True) 189 | 190 | 191 | if __name__ == "__main__": 192 | log_file_path = ( 193 | "/Users/nikitakutc/projects/captureflow-py/serverside/trace_33c73b42-bf7c-4196-bdd8-048049edff00.json" 194 | ) 195 | # log_file_path = "/Users/nikitakutc/projects/captureflow-py/serverside/tests/assets/sample_trace.json" 196 | with open(log_file_path, "r") as file: 197 | log_data = file.read() 198 | 199 | call_graph = CallGraph(log_data) 200 | call_graph.draw(compressed=True) 201 | print(f"Generated call graph has been saved.") 202 | --------------------------------------------------------------------------------