Merge branch 'main' into remove-print-messages

This commit is contained in:
Kevin Turcios 2026-01-06 18:24:22 -05:00 committed by GitHub
commit 9ea1afbdbf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 971 additions and 404 deletions

View file

@ -3,9 +3,10 @@ from typing import Any
from posthog import Posthog
_posthog: Posthog = Posthog(
project_api_key="phc_aUO790jHd7z1SXwsYCz8dRApxueplZlZWeDSpKc5hol", host="https://us.posthog.com"
)
_posthog: Posthog | None = None
if os.environ.get("ENVIRONMENT", default="") == "PRODUCTION":
_posthog = Posthog(project_api_key="phc_aUO790jHd7z1SXwsYCz8dRApxueplZlZWeDSpKc5hol", host="https://us.posthog.com")
ENVIRONMENT_TYPE = os.environ.get("ENVIRONMENT", default="None")
OPENAI_API_TYPE = os.environ.get("OPENAI_API_TYPE", default="azure")
@ -18,6 +19,9 @@ def ph(user_id: str, event: str, properties: dict[str, Any] | None = None) -> No
:param event: The name of the event.
:param properties: A dictionary of properties to attach to the event.
"""
if _posthog is None:
return
if properties is None:
properties = {}

View file

@ -32,6 +32,7 @@ You are a professional computer programmer who specializes in writing high-perfo
**Code Style & Structure**
- Do NOT replace walrus operators (`:=`) for optimization purposes.
- Keep `assert` statements as-is - do NOT convert them to `if/raise AssertionError` patterns, it doesn't improve the performance.
- **DO NOT convert `isinstance()` checks to `type()` checks**. `isinstance()` correctly handles inheritance and subclasses, while `type()` checks are incorrect for subclass instances and represent a micro-optimization that should be avoided.
- You may write new async helper functions that do not already exist in the codebase.
- Avoid purely stylistic changes unless they result in noticeable performance improvements
- Ensure all new async code follows proper async patterns and conventions

View file

@ -4,14 +4,14 @@ Each distribution is a list of (model_name, num_calls) tuples.
Available models: "gpt-4.1", "claude-sonnet-4-5"
"""
# Standard optimization endpoint
MODEL_DISTRIBUTION: list[tuple[str, int]] = [("gpt-4.1", 3), ("claude-sonnet-4-5", 2)]
MAX_OPTIMIZER_CALLS = 6
MAX_OPTIMIZER_LP_CALLS = 7
# Standard optimization - LSP mode (fewer candidates for faster response)
MODEL_DISTRIBUTION_LSP: list[tuple[str, int]] = [("gpt-4.1", 2), ("claude-sonnet-4-5", 1)]
# Line profiler optimization endpoint
MODEL_DISTRIBUTION_LP: list[tuple[str, int]] = [("gpt-4.1", 4), ("claude-sonnet-4-5", 2)]
def get_model_distribution(total_calls: int, max_calls: int) -> list[tuple[str, int]]:
final_total_calls = min(total_calls, max_calls)
# claude_calls should always be less than gpt_calls as it's more expensive
claude_calls = (final_total_calls - 1) // 2
gpt_calls = final_total_calls - claude_calls
# Line profiler - LSP mode (fewer candidates for faster response)
MODEL_DISTRIBUTION_LP_LSP: list[tuple[str, int]] = [("gpt-4.1", 2), ("claude-sonnet-4-5", 1)]
return [("gpt-4.1", gpt_calls), ("claude-sonnet-4-5", claude_calls)]

View file

@ -70,9 +70,22 @@ class OptimizeSchema(Schema):
repo_owner: str | None = None
repo_name: str | None = None
is_async: bool | None = False
model: str | None = None # Deprecated: multi-model is now handled by MODEL_DISTRIBUTION
model: str | None = None # Deprecated: multi-model is now handled by get_model_distribution
call_sequence: int | None = None # Deprecated: call_sequence is now auto-assigned
lsp_mode: bool = False # Use fewer candidates for faster LSP response
n_candidates: int = 5 # default value for backward compatibility
class OptimizeSchemaLP(Schema):
source_code: str
dependency_code: str | None
line_profiler_results: str | None
trace_id: str
python_version: str
experiment_metadata: dict[str, str] | None = None
codeflash_version: str | None = None
n_candidates: int = 6 # default value for backward compatibility
model: str | None = None
call_sequence: int | None = None
def get_model_from_name(model_name: str | None) -> LLM:

View file

@ -21,7 +21,7 @@ from authapp.auth import AuthenticatedRequest
from authapp.user import get_user_by_id
from log_features.log_event import get_or_create_optimization_event
from log_features.log_features import log_features
from optimizer.config import MODEL_DISTRIBUTION, MODEL_DISTRIBUTION_LSP
from optimizer.config import MAX_OPTIMIZER_CALLS, get_model_distribution
from optimizer.context_utils.context_helpers import group_code
from optimizer.context_utils.optimizer_context import (
BaseOptimizerContext,
@ -189,7 +189,7 @@ async def optimize_python_code(
original_source_code: str,
dependency_code: str | None = None,
python_version: tuple[int, int, int] = (3, 12, 9),
model_distribution: list[tuple[str, int]] | None = None,
n_candidates: int = 0,
) -> tuple[list[OptimizeResponseItemSchema], float, dict[str, dict[str, str]], dict[str, str]]:
"""Run parallel optimizations with multiple models based on the distribution config.
@ -201,17 +201,17 @@ async def optimize_python_code(
- dict mapping optimization_id to model name
"""
if model_distribution is None:
model_distribution = MODEL_DISTRIBUTION
# Create tasks for each model call
tasks: list[
tuple[asyncio.Task[tuple[OptimizeResponseItemSchema | None, float | None, str]], BaseOptimizerContext]
] = []
call_sequence = 1
if n_candidates == 0:
return [], 0.0, {}, {}
async with asyncio.TaskGroup() as tg:
for model_name, num_calls in model_distribution:
for model_name, num_calls in get_model_distribution(n_candidates, MAX_OPTIMIZER_CALLS):
model = get_model_from_name(model_name)
for _ in range(num_calls):
# Each call needs its own context instance to avoid shared state issues
@ -325,8 +325,6 @@ async def optimize(
for item in response.optimizations:
item.optimization_event_id = str(event.id) if event else None
return response_code, response
# Determine model distribution based on LSP mode
model_distribution = MODEL_DISTRIBUTION_LSP if data.lsp_mode else MODEL_DISTRIBUTION
try:
async with asyncio.TaskGroup() as tg:
@ -338,7 +336,7 @@ async def optimize(
original_source_code=data.source_code,
dependency_code=data.dependency_code,
python_version=python_version,
model_distribution=model_distribution,
n_candidates=data.n_candidates,
)
)
user_task = None

View file

@ -6,7 +6,7 @@ from pathlib import Path
from typing import TYPE_CHECKING
import sentry_sdk
from ninja import NinjaAPI, Schema
from ninja import NinjaAPI
from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam
from aiservice.analytics.posthog import ph
@ -15,14 +15,14 @@ from aiservice.env_specific import debug_log_sensitive_data, debug_log_sensitive
from aiservice.llm import OPTIMIZE_MODEL, calculate_llm_cost, call_llm
from log_features.log_event import update_optimization_cost
from log_features.log_features import log_features
from optimizer.config import MODEL_DISTRIBUTION_LP, MODEL_DISTRIBUTION_LP_LSP
from optimizer.config import MAX_OPTIMIZER_LP_CALLS, get_model_distribution
from optimizer.context_utils.optimizer_context import (
BaseOptimizerContext,
OptimizeErrorResponseSchema,
OptimizeResponseSchema,
)
from optimizer.diff_patches_utils.diff import DiffMethod
from optimizer.models import OptimizedCandidateSource, get_model_from_name
from optimizer.models import OptimizedCandidateSource, OptimizeSchemaLP, get_model_from_name
if TYPE_CHECKING:
from openai.types.chat import ChatCompletionMessageParam
@ -47,7 +47,6 @@ async def optimize_python_code_line_profiler_single(
ctx: BaseOptimizerContext,
dependency_code: str | None = None,
optimize_model: LLM = OPTIMIZE_MODEL,
lsp_mode: bool = False, # noqa: FBT001, FBT002
python_version: tuple[int, int, int] = (3, 12, 9),
call_sequence: int | None = None,
) -> tuple[OptimizeResponseItemSchema | None, float | None, str]:
@ -70,7 +69,7 @@ async def optimize_python_code_line_profiler_single(
debug_log_sensitive_data(f"This was the user prompt\n {user_prompt}\n")
# TODO: Verify if the context window length is within the model capability
obs_context: dict = {"lsp_mode": lsp_mode}
obs_context: dict = {}
if call_sequence is not None:
obs_context["call_sequence"] = call_sequence
@ -116,7 +115,7 @@ async def optimize_python_code_line_profiler(
ctx: BaseOptimizerContext,
original_source_code: str,
dependency_code: str | None = None,
lsp_mode: bool = False,
n_candidates: int = 0,
python_version: tuple[int, int, int] = (3, 12, 9),
) -> tuple[list[OptimizeResponseItemSchema], float, dict[str, dict], dict[str, str]]:
"""Run parallel line profiler optimizations with multiple models.
@ -129,7 +128,9 @@ async def optimize_python_code_line_profiler(
- dict mapping optimization_id to model name
"""
model_distribution = MODEL_DISTRIBUTION_LP_LSP if lsp_mode else MODEL_DISTRIBUTION_LP
if n_candidates == 0:
return [], 0.0, {}, {}
model_distribution = get_model_distribution(n_candidates, MAX_OPTIMIZER_LP_CALLS)
# Create tasks for each model call
tasks: list[
@ -154,7 +155,6 @@ async def optimize_python_code_line_profiler(
ctx=task_ctx,
dependency_code=dependency_code,
optimize_model=model,
lsp_mode=lsp_mode,
python_version=python_version,
call_sequence=call_sequence,
)
@ -182,19 +182,6 @@ async def optimize_python_code_line_profiler(
return optimization_results, total_cost, code_and_explanations, optimization_models
class OptimizeSchemaLP(Schema):
source_code: str
dependency_code: str | None
line_profiler_results: str | None
trace_id: str
python_version: str
experiment_metadata: dict[str, str] | None = None
codeflash_version: str | None = None
lsp_mode: bool = False
model: str | None = None
call_sequence: int | None = None
@optimize_line_profiler_api.post(
"/", response={200: OptimizeResponseSchema, 400: OptimizeErrorResponseSchema, 500: OptimizeErrorResponseSchema}
)
@ -232,7 +219,7 @@ async def optimize(request, data: OptimizeSchemaLP) -> tuple[int, OptimizeRespon
ctx=ctx,
original_source_code=data.source_code,
dependency_code=data.dependency_code,
lsp_mode=data.lsp_mode,
n_candidates=data.n_candidates,
python_version=python_version,
)

View file

@ -59,6 +59,7 @@ Rules to follow while refining the quality of the optimized code -
- Analyze the original code and the optimized code and look at the line profiler info and the explanation to understand how the optimization works
- Introduction of the `global` and `nonlocal` keywords in optimized_source_code is **HIGHLY DISCOURAGED** as it reduces code clarity and maintainability, introduces hidden dependencies, can cause subtle bugs and breaks modularity. Revert any such changes.
- Revert any changes which are micro-optimizations like inlining a function call, or localizing variables or methods (attribute lookup optimizations). The performance improvements are minimal and come at a substantial cost to readability.
- **Revert any conversion of `isinstance()` checks to `type()` checks**. `isinstance()` correctly handles inheritance and subclasses, while `type()` checks are incorrect for subclass instances and represent a micro-optimization that should be avoided. Preserve the original `isinstance()` usage.
- Figure out the code difference between the original_source_code and the optimized_source_code to see what part of the optimized_source_code is not contributing to the optimization. In such a case, we want to revert that part of the optimized_source_code to the original_source_code. It is okay to revert parts of the changes that aren't faster by at least 1%.
- If there are any changes in the optimized code that make that code section slower than the original then we want to revert such a change to the original.
- Revert the new comments in the optimized_source_code that are different from the original_source_code unless the new code is complex and requires additional context.

View file

@ -15,6 +15,7 @@ You are a professional computer programmer who specializes in writing high-perfo
- Do NOT replace walrus operators (`:=`) for optimization purposes.
- DO NOT introduce attribute lookup optimizations. The performance improvements are minimal and come at a substantial cost to readability.
- Keep `assert` statements as-is - do NOT convert them to `if/raise AssertionError` patterns, it doesn't improve the performance.
- **DO NOT convert `isinstance()` checks to `type()` checks**. `isinstance()` correctly handles inheritance and subclasses, while `type()` checks are incorrect for subclass instances and represent a micro-optimization that should be avoided.
- You may write new helper functions that do not already exist in the codebase.
- Avoid purely stylistic changes unless they result in noticeable performance improvements

View file

@ -1,4 +1,4 @@
**Role**: You are Codeflash, a world-class Python developer with an eagle eye for unintended bugs and edge cases. You write careful, accurate unit tests for **asynchronous functions**. When asked to reply only with code, you write all of your code in a single block.
**Role**: You are Codeflash, a world-class Python developer with an eagle eye for unintended bugs and edge cases. You write careful, accurate unit tests for **asynchronous functions**. When asked to reply only with code, you write all of your code in a single markdown code block.
**Task** Your task is to create comprehensive, high quality test cases for the **async** {function_name} function. These test cases should encompass Basic, Edge, and Large Scale scenarios to ensure the code's robustness, reliability, and scalability. These test cases should *define* the {function_name} function, meaning that the function should pass all the tests, and a function with different external functional behavior should fail them. In other words, the test suite should fail under mutation testing of the source code.
@ -54,3 +54,4 @@
- **CRITICAL**: All throughput test functions MUST include `_throughput_` in their name to clearly identify them as throughput tests.
- Always use proper async/await syntax and test decorators.
- **IMPORTANT**: Generate tests that complete quickly and deterministically. Avoid any test patterns that could cause timeouts, hanging, or excessive delays. Keep async operations fast and bounded.
- **CRITICAL: IMPORT CLASSES FROM THEIR REAL MODULES** - When the context shows class definitions with file paths (e.g., ```python:path/to/module.py), you MUST import those classes from their actual modules instead of redefining them. For example, if you see `class Foo` in `mypackage/utils.py`, import it as `from mypackage.utils import Foo`. This is essential because the function under test uses `isinstance()` checks against the real classes, not mock versions. Never redefine classes that exist in the codebase - always import them.

View file

@ -1,4 +1,4 @@
**Role**: You are Codeflash, a world-class Python developer with an eagle eye for unintended bugs and edge cases. You write careful, accurate unit tests. When asked to reply only with code, you write all of your code in a single block.
**Role**: You are Codeflash, a world-class Python developer with an eagle eye for unintended bugs and edge cases. You write careful, accurate unit tests. When asked to reply only with code, you write all of your code in a single markdown code block.
**Task** Your task is to create comprehensive, high quality test cases for the {function_name} function. These test cases should encompass Basic, Edge, and Large Scale scenarios to ensure the code's robustness, reliability, and scalability. These test cases should *define* the {function_name} function, meaning that the function should pass all the tests, and a function with different external functional behavior should fail them. In other words, the test suite should fail under mutation testing of the source code.
@ -15,3 +15,4 @@
- Pay special attention to edge cases as they often reveal hidden bugs.
- For large-scale tests, focus on the function's efficiency and performance under heavy loads. Avoid loops exceeding 1000 steps, and keep data structures under 1000 elements.
- **CRITICAL: DO NOT MOCK THE FUNCTION UNDER TEST** - Never mock, stub, or patch the {function_name} function itself or any internal functions/methods it calls. You may mock external dependencies (APIs, databases, network calls, file I/O, etc.) if necessary, but the function being tested must execute with its real implementation.
- **CRITICAL: IMPORT CLASSES FROM THEIR REAL MODULES** - When the context shows class definitions with file paths (e.g., ```python:path/to/module.py), you MUST import those classes from their actual modules instead of redefining them. For example, if you see `class Foo` in `mypackage/utils.py`, import it as `from mypackage.utils import Foo`. This is essential because the function under test uses `isinstance()` checks against the real classes, not mock versions. Never redefine classes that exist in the codebase - always import them.

View file

@ -173,6 +173,18 @@ class DependencyCollector(cst.CSTVisitor):
self.current_class = class_name
self.current_top_level_name = class_name
# Track base classes as dependencies
for base in node.bases:
if isinstance(base.value, cst.Name):
base_name = base.value.value
if base_name in self.definitions and class_name in self.definitions:
self.definitions[class_name].dependencies.add(base_name)
elif isinstance(base.value, cst.Attribute):
# Handle cases like module.ClassName
attr_name = base.value.attr.value
if attr_name in self.definitions and class_name in self.definitions:
self.definitions[class_name].dependencies.add(attr_name)
self.class_depth += 1
def leave_ClassDef(self, original_node: cst.ClassDef) -> None:

View file

@ -1,23 +1,82 @@
from __future__ import annotations
from libcst import (
Arg,
Attribute,
BaseCompoundStatement,
Call,
ClassDef,
CSTTransformer,
CSTVisitor,
EmptyLine,
FunctionDef,
Module,
Name,
RemovalSentinel,
RemoveFromParent,
SimpleStatementLine,
)
class UsedNameCollector(CSTVisitor):
"""Collects all names that are actually used/referenced in the module.
This includes:
- Names used as base classes
- Names that are called/instantiated (e.g., MyClass())
- Names referenced in expressions
"""
def __init__(self) -> None:
super().__init__()
self.used_names: set[str] = set()
# Track names being defined so we don't count definitions as usage
self.currently_defining: str | None = None
def visit_ClassDef(self, node: ClassDef) -> bool:
# Track that we're defining this class (don't count it as "used")
self.currently_defining = node.name.value
# Collect base class names as used
for base in node.bases:
if isinstance(base, Arg) and isinstance(base.value, Name):
self.used_names.add(base.value.value)
elif isinstance(base, Arg) and isinstance(base.value, Attribute):
self.used_names.add(base.value.attr.value)
return True
def leave_ClassDef(self, node: ClassDef) -> None:
self.currently_defining = None
def visit_FunctionDef(self, node: FunctionDef) -> bool:
# Track that we're defining this function
self.currently_defining = node.name.value
return True
def leave_FunctionDef(self, node: FunctionDef) -> None:
self.currently_defining = None
def visit_Call(self, node: Call) -> bool:
# Collect names that are called (instantiation or function calls)
if isinstance(node.func, Name):
self.used_names.add(node.func.value)
elif isinstance(node.func, Attribute) and isinstance(node.func.value, Name):
# For chained calls like module.func(), collect the module name
self.used_names.add(node.func.value.value)
return True
def visit_Name(self, node: Name) -> bool:
# Collect any name reference (but not the name being defined)
if node.value != self.currently_defining:
self.used_names.add(node.value)
return True
class TopDefTerminator(CSTTransformer):
def __init__(self, hit_list: list[str]) -> None:
def __init__(self, hit_list: list[str], protected_names: set[str] | None = None) -> None:
super().__init__()
self.hit_list_parents_names: list[list[str]] = [s.split(".")[-2:] for s in hit_list]
self.hit_list_names: set[str] = {pair[-1] for pair in self.hit_list_parents_names if pair}
self.protected_names: set[str] = protected_names or set()
self.level: int = 0
self.is_killable_class: bool = False
self.potential_methods: set[str] = set()
@ -26,7 +85,8 @@ class TopDefTerminator(CSTTransformer):
self.level += 1
if self.level > 1:
return False
if node.name.value in self.hit_list_names:
# Don't mark as killable if this class is actually used somewhere in the code
if node.name.value in self.hit_list_names and node.name.value not in self.protected_names:
self.is_killable_class = True
return False
if potential_methods := {
@ -49,7 +109,8 @@ class TopDefTerminator(CSTTransformer):
def leave_FunctionDef(self, original_node: FunctionDef, updated_node: FunctionDef) -> FunctionDef | RemovalSentinel:
if self.level == 0:
if original_node.name.value in self.hit_list_names:
# Don't remove if the function is actually used somewhere in the code
if original_node.name.value in self.hit_list_names and original_node.name.value not in self.protected_names:
return RemovalSentinel.REMOVE
return updated_node
if original_node.name.value in self.potential_methods:
@ -77,6 +138,11 @@ class LeadingWhitespaceRemover(CSTTransformer):
def delete_top_def_nodes(module: Module, deletable_list: list[str]) -> Module:
# First apply the TopDefTerminator
module = module.visit(TopDefTerminator(deletable_list))
# First collect all names that are actually used in the module
# This protects classes/functions that are referenced in test code from being deleted
used_name_collector = UsedNameCollector()
module.visit(used_name_collector)
# Apply the TopDefTerminator, protecting names that are actually used
module = module.visit(TopDefTerminator(deletable_list, used_name_collector.used_names))
return module.visit(LeadingWhitespaceRemover())

View file

@ -33,3 +33,177 @@ class ClassTypeNonHelper:
"""
result = delete_top_def_nodes(parse_module_to_cst(module_str), deletable_list).code
assert result == expected
def test_delete_top_def_nodes_preserves_base_classes() -> None:
"""Test that classes used as base classes are not deleted even if in deletable_list.
This tests the fix for the LayoutDumper bug where base classes were being deleted
but their child classes were preserved, causing NameError at runtime.
"""
module_str = """class LayoutDumper:
layout_source: str = "unknown"
def dump(self) -> dict:
raise NotImplementedError()
class ObjectDetectionLayoutDumper(LayoutDumper):
def __init__(self, layout):
self._layout = layout
def dump(self) -> dict:
return self._layout
class UnusedHelper:
pass
def test_object_detection():
dumper = ObjectDetectionLayoutDumper({})
assert dumper.dump() == {}
"""
# LayoutDumper is in the deletable list, but it should NOT be deleted
# because ObjectDetectionLayoutDumper inherits from it
deletable_list = ["module.LayoutDumper", "module.UnusedHelper"]
expected = """class LayoutDumper:
layout_source: str = "unknown"
def dump(self) -> dict:
raise NotImplementedError()
class ObjectDetectionLayoutDumper(LayoutDumper):
def __init__(self, layout):
self._layout = layout
def dump(self) -> dict:
return self._layout
def test_object_detection():
dumper = ObjectDetectionLayoutDumper({})
assert dumper.dump() == {}
"""
result = delete_top_def_nodes(parse_module_to_cst(module_str), deletable_list).code
assert result == expected
def test_delete_top_def_nodes_deletes_unused_base_class() -> None:
"""Test that base classes CAN be deleted if no class inherits from them."""
module_str = """class LayoutDumper:
pass
class UnrelatedClass:
pass
def test_something():
pass
"""
# LayoutDumper is in deletable list and nothing inherits from it, so it should be deleted
deletable_list = ["module.LayoutDumper"]
expected = """class UnrelatedClass:
pass
def test_something():
pass
"""
result = delete_top_def_nodes(parse_module_to_cst(module_str), deletable_list).code
assert result == expected
def test_delete_top_def_nodes_preserves_used_classes() -> None:
"""Test that classes directly instantiated in tests are preserved.
This tests the fix where helper classes that are actually used in tests
should NOT be deleted, even if they're in the deletable list.
"""
module_str = """class LayoutDumper:
layout_source: str = "unknown"
def dump(self) -> dict:
return {}
class ObjectDetectionLayoutDumper(LayoutDumper):
def __init__(self, layout):
self._layout = layout
def dump(self) -> dict:
return self._layout
class ExtractedLayoutDumper(LayoutDumper):
def __init__(self, layout):
self._layout = layout
def dump(self) -> dict:
return self._layout
class UnusedHelper:
pass
def test_object_detection():
dumper = ObjectDetectionLayoutDumper({})
assert dumper.dump() == {}
def test_extracted():
dumper = ExtractedLayoutDumper({"text": "hello"})
assert dumper.dump() == {"text": "hello"}
"""
# All these are in deletable list, but LayoutDumper, ObjectDetectionLayoutDumper,
# and ExtractedLayoutDumper should be preserved because they're actually used
deletable_list = [
"module.LayoutDumper",
"module.ObjectDetectionLayoutDumper",
"module.ExtractedLayoutDumper",
"module.UnusedHelper",
]
expected = """class LayoutDumper:
layout_source: str = "unknown"
def dump(self) -> dict:
return {}
class ObjectDetectionLayoutDumper(LayoutDumper):
def __init__(self, layout):
self._layout = layout
def dump(self) -> dict:
return self._layout
class ExtractedLayoutDumper(LayoutDumper):
def __init__(self, layout):
self._layout = layout
def dump(self) -> dict:
return self._layout
def test_object_detection():
dumper = ObjectDetectionLayoutDumper({})
assert dumper.dump() == {}
def test_extracted():
dumper = ExtractedLayoutDumper({"text": "hello"})
assert dumper.dump() == {"text": "hello"}
"""
result = delete_top_def_nodes(parse_module_to_cst(module_str), deletable_list).code
assert result == expected
def test_delete_top_def_nodes_preserves_used_functions() -> None:
"""Test that helper functions actually called in tests are preserved."""
module_str = """def helper_function(x):
return x * 2
def unused_helper():
return 42
def test_helper():
result = helper_function(5)
assert result == 10
"""
deletable_list = ["module.helper_function", "module.unused_helper"]
# helper_function should be preserved because it's called in test_helper
# unused_helper should be deleted because nothing uses it
expected = """def helper_function(x):
return x * 2
def test_helper():
result = helper_function(5)
assert result == 10
"""
result = delete_top_def_nodes(parse_module_to_cst(module_str), deletable_list).code
assert result == expected

View file

@ -1250,3 +1250,71 @@ class TestSorter(unittest.TestCase):
result = remove_unused_definitions_from_pytest_file(cst.parse_module(code)).code
assert result.strip() == expected.strip()
def test_abstract_base_class_inheritance() -> None:
"""Test that abstract base classes used only for inheritance are preserved.
This mimics the real-world case where LLM generates mock classes that inherit
from a base class, and the base class should not be removed even though it's
only referenced in the inheritance declaration.
"""
code = """
class LayoutDumper:
layout_source: str = "unknown"
def dump(self) -> dict:
raise NotImplementedError()
class ObjectDetectionLayoutDumper(LayoutDumper):
def __init__(self, layout):
self._layout = layout
def dump(self) -> dict:
return self._layout
class ExtractedLayoutDumper(LayoutDumper):
def __init__(self, layout):
self._layout = layout
def dump(self) -> dict:
return self._layout
class UnusedClass:
pass
def test_object_detection():
dumper = ObjectDetectionLayoutDumper({})
assert dumper.dump() == {}
def test_extracted():
dumper = ExtractedLayoutDumper({"text": "hello"})
assert dumper.dump() == {"text": "hello"}
"""
expected = """
class LayoutDumper:
layout_source: str = "unknown"
def dump(self) -> dict:
raise NotImplementedError()
class ObjectDetectionLayoutDumper(LayoutDumper):
def __init__(self, layout):
self._layout = layout
def dump(self) -> dict:
return self._layout
class ExtractedLayoutDumper(LayoutDumper):
def __init__(self, layout):
self._layout = layout
def dump(self) -> dict:
return self._layout
def test_object_detection():
dumper = ObjectDetectionLayoutDumper({})
assert dumper.dump() == {}
def test_extracted():
dumper = ExtractedLayoutDumper({"text": "hello"})
assert dumper.dump() == {"text": "hello"}
"""
result = remove_unused_definitions_from_pytest_file(cst.parse_module(code)).code
assert result.strip() == expected.strip()

View file

@ -1,4 +1,6 @@
import * as vscode from "vscode";
import * as path from "path";
import * as os from "os";
import type { LanguageClient } from "vscode-languageclient/node";
import { State as LanguageClientState } from "vscode-languageclient/node";
import { Logger } from "../utils";
@ -93,7 +95,30 @@ export class CodeflashCodeLensProvider
// check if it's inside the module root or not
const moduleRootAbs = this.globalState.get(GlobalStateKey.ModuleRoot, "")!;
if (!document.uri.fsPath.startsWith(moduleRootAbs)) {
if (!moduleRootAbs) {
return [];
}
// Normalize paths for cross-platform compatibility
// Use path.normalize to handle different separators and resolve relative paths
let documentPath = path.normalize(document.uri.fsPath);
let moduleRootPath = path.normalize(moduleRootAbs);
// On Windows, file system is case-insensitive, so use case-insensitive comparison
// On Linux/Mac, file system is case-sensitive, so use case-sensitive comparison
const isWindows = os.platform() === "win32";
if (isWindows) {
documentPath = documentPath.toLowerCase();
moduleRootPath = moduleRootPath.toLowerCase();
}
// Ensure module root path ends with a separator for proper comparison
// This prevents false matches (e.g., /foo/bar matching /foo/barbaz)
const normalizedModuleRoot = moduleRootPath.endsWith(path.sep)
? moduleRootPath
: moduleRootPath + path.sep;
if (!documentPath.startsWith(normalizedModuleRoot)) {
return [];
}

View file

@ -1,5 +1,9 @@
import { PostHog } from "posthog-node" // new
export const posthog = new PostHog("phc_aUO790jHd7z1SXwsYCz8dRApxueplZlZWeDSpKc5hol", {
host: "https://app.posthog.com",
})
const isProduction = process.env.NODE_ENV === "production"
export const posthog = isProduction
? new PostHog("phc_aUO790jHd7z1SXwsYCz8dRApxueplZlZWeDSpKc5hol", {
host: "https://app.posthog.com",
})
: undefined

View file

@ -113,7 +113,7 @@ export async function is_code_being_optimized_again(req: Request, res: Response)
const alreadyOptimizedTuples = code_contexts
.filter(({ code_hash }) => existingHashes.has(code_hash))
.map(({ file_path, function_name }) => [file_path, function_name])
posthog.capture({
posthog?.capture({
distinctId: userId,
event: `cfapi-github-pr-optimization`,
properties: {

View file

@ -66,6 +66,7 @@ export function createStandalonePRTitleAndBody(
replayTests: string = "",
concolicTests: string = "",
optimizationReview: string = "",
trace_id: string,
): { title: string; body: string } {
const prCommentHeader = builder.buildResultHeader(prCommentFields)
@ -90,7 +91,7 @@ export function createStandalonePRTitleAndBody(
prCommentFields.speedup_x,
)
const metadata = buildOptimizationMetadata(prCommentFields)
const metadata = buildOptimizationMetadata(prCommentFields, trace_id)
let optReviewBadge = generateOptimizationReviewTemplate(optimizationReview)
if (optReviewBadge) {
optReviewBadge = ` ${optReviewBadge}\n`
@ -300,9 +301,16 @@ export async function createPr(req: Request, res: Response) {
// TODO: Remove this background upsert logic after ensuring all old repositories have been saved.
dependencies
.registerRepositoryAndMember(owner, repo, nickname, userId, installationOctokit)
.then(() => logger.info(`Background repo and member upsert completed for ${owner}/${repo}`, req))
.then(() =>
logger.info(`Background repo and member upsert completed for ${owner}/${repo}`, req),
)
.catch(err => {
logger.errorWithSentry(`Error in background upsertRepoAndCreateMember:`, req, {}, err as Error)
logger.errorWithSentry(
`Error in background upsertRepoAndCreateMember:`,
req,
{},
err as Error,
)
Sentry.captureException(err)
})
@ -440,16 +448,21 @@ export async function createPr(req: Request, res: Response) {
return res.status(500).send("Error creating pull request")
}
} catch (error) {
logger.errorWithSentry(`Error in /cfapi/create-pr: ${error}`, req, {
errorMessage: error instanceof Error ? error.message : String(error),
}, error as Error)
logger.errorWithSentry(
`Error in /cfapi/create-pr: ${error}`,
req,
{
errorMessage: error instanceof Error ? error.message : String(error),
},
error as Error,
)
if (error instanceof Error) {
//add exception to sentry
Sentry.captureException("Create-PR: " + error.message, {
extra: { reqBody: req.body, userId: (req as any).userId },
})
// Capture error in PostHog
dependencies.posthog.capture({
dependencies.posthog?.capture({
distinctId: (req as any).userId,
event: `cfapi-create-pr-failed-error-creating-standalone-pr`,
properties: {
@ -480,18 +493,22 @@ export async function triggerCreatePr(
traceId = "",
optimizationReview = "",
): Promise<number> {
logger.info(`Starting PR creation for ${owner}/${repo}`, {
userId,
endpoint: "/cfapi/create-pr",
operation: "trigger_create_pr",
owner,
repo,
}, {
traceId,
nickname,
baseBranch,
functionName: prCommentFields?.function_name,
})
logger.info(
`Starting PR creation for ${owner}/${repo}`,
{
userId,
endpoint: "/cfapi/create-pr",
operation: "trigger_create_pr",
owner,
repo,
},
{
traceId,
nickname,
baseBranch,
functionName: prCommentFields?.function_name,
},
)
try {
const diffContentsMap: Map<string, FileDiffContent> =
@ -549,6 +566,7 @@ export async function triggerCreatePr(
replayTests,
concolicTests,
optimizationReview,
traceId,
)
logger.info(`Creating PR with title: ${title}`, {
@ -585,13 +603,18 @@ export async function triggerCreatePr(
},
})
} catch (eventError) {
logger.error("Failed to update optimization event:", {
userId,
endpoint: "/cfapi/create-pr",
operation: "update_optimization_event",
owner,
repo,
}, {}, eventError as Error)
logger.error(
"Failed to update optimization event:",
{
userId,
endpoint: "/cfapi/create-pr",
operation: "update_optimization_event",
owner,
repo,
},
{},
eventError as Error,
)
}
await triggerCreatePrDeps.assignReviewer(
@ -626,7 +649,7 @@ export async function triggerCreatePr(
owner,
repo,
})
triggerCreatePrDeps.posthog.capture({
triggerCreatePrDeps.posthog?.capture({
distinctId: userId,
event: `cfapi-create-pr-success-standalone-pr-created`,
properties: {
@ -668,17 +691,22 @@ export async function triggerCreatePr(
return newPrData.data.number
} catch (error) {
logger.errorWithSentry(`Error creating PR for ${owner}/${repo}:`, {
userId,
endpoint: "/cfapi/create-pr",
operation: "trigger_create_pr",
owner,
repo,
}, {
traceId,
nickname,
errorMessage: error instanceof Error ? error.message : String(error),
}, error as Error)
logger.errorWithSentry(
`Error creating PR for ${owner}/${repo}:`,
{
userId,
endpoint: "/cfapi/create-pr",
operation: "trigger_create_pr",
owner,
repo,
},
{
traceId,
nickname,
errorMessage: error instanceof Error ? error.message : String(error),
},
error as Error,
)
Sentry.captureException("triggerCreatePr: " + error, {
extra: {
traceId,
@ -749,7 +777,12 @@ export async function addRepositoryManually(req: AuthorizedUserReq, res: Respons
})
logger.info(`Repository upserted: ${repo.full_name}`, req)
} catch (error) {
logger.errorWithSentry(`Failed to add/reactivate repository ${repo.full_name}:`, req, {}, error as Error)
logger.errorWithSentry(
`Failed to add/reactivate repository ${repo.full_name}:`,
req,
{},
error as Error,
)
Sentry.captureException(error)
}
}

View file

@ -412,7 +412,7 @@ export async function setupGithubActions(req: Request, res: Response): Promise<v
}
// Track success
dependencies.posthog.capture({
dependencies.posthog?.capture({
distinctId: `github|${userId}`,
event: "cfapi-setup-github-actions-success",
properties: {
@ -437,7 +437,7 @@ export async function setupGithubActions(req: Request, res: Response): Promise<v
Sentry.captureException(error)
const userId = (req as any).userId
dependencies.posthog.capture({
dependencies.posthog?.capture({
distinctId: `github|${userId}`,
event: "cfapi-setup-github-actions-error",
properties: {

View file

@ -108,7 +108,7 @@ export async function handleSlackEvents(req: Request, res: Response) {
const processed = await dependencies.processReaction(event)
if (processed) {
dependencies.posthog.capture({
dependencies.posthog?.capture({
distinctId: "system",
event: "slack-approval-reaction-processed",
properties: {
@ -128,7 +128,7 @@ export async function handleSlackEvents(req: Request, res: Response) {
}
dependencies.Sentry.captureException(error)
// Log to monitoring
dependencies.posthog.capture({
dependencies.posthog?.capture({
distinctId: "system",
event: "slack-optimization-approval-error",
properties: {

View file

@ -335,7 +335,7 @@ export async function suggestPrChanges(
logger.errorWithSentry(`Error in /cfapi/suggest-pr-changes: ${error}`, req, {
errorMessage: error.message,
}, error as Error)
dependencies.posthog.capture({
dependencies.posthog?.capture({
distinctId: req.userId,
event: `cfapi-suggest-pr-changes-failed-error`,
properties: {
@ -515,7 +515,7 @@ export async function triggerSuggestPrChanges(
)
}
dependencies.posthog.capture({
dependencies.posthog?.capture({
distinctId: userId,
event: `cfapi-suggest-pr-changes-success-dependent-pr-created`,
properties: {
@ -580,6 +580,7 @@ export async function triggerSuggestPrChanges(
newBranchName,
replayTests,
concolicTests,
traceId,
{ isUnifiedReview: true, includeHeader: false, isCollapsed: true },
)
let optReviewBadge = generateOptimizationReviewTemplate(optimizationReview)
@ -667,7 +668,7 @@ export async function triggerSuggestPrChanges(
)
}
dependencies.posthog.capture({
dependencies.posthog?.capture({
distinctId: userId,
event: `cfapi-suggest-pr-changes-success-suggestions-made`,
properties: {

View file

@ -638,6 +638,7 @@ describe("triggerCreatePr", () => {
"replay tests",
"concolic tests",
"medium",
"trace123",
)
expect(mockDeps.createStandalonePullRequest).toHaveBeenCalledWith(
mockInstallationOctokit,
@ -1160,11 +1161,12 @@ describe("createStandalonePRTitleAndBody", () => {
"replay tests",
"concolic tests",
"medium",
"f47ac10b-58cc-4372-a567-0e02b2c3d478",
)
expect(result.title).toBe("Test PR Title")
expect(result.body).toBe(
'<!-- CODEFLASH_OPTIMIZATION: {"function":"testFunc","speedup_pct":50,"speedup_x":2,"optimization_type":"general","timestamp":"2025-01-01T00:00:00.000Z","version":"1.0"} -->\n## Header\nDetails\nTest report\nFooter ![Static Badge](https://img.shields.io/badge/🎯_Optimization_Quality-medium-blue)\n\n',
'<!-- CODEFLASH_OPTIMIZATION: {"trace_id":"f47ac10b-58cc-4372-a567-0e02b2c3d478","function":"testFunc","speedup_pct":50,"speedup_x":2,"optimization_type":"general","timestamp":"2025-01-01T00:00:00.000Z","version":"1.0"} -->\n## Header\nDetails\nTest report\nFooter ![Static Badge](https://img.shields.io/badge/🎯_Optimization_Quality-medium-blue)\n\n',
)
expect(mockBuilder.buildPrTitle).toHaveBeenCalledWith("testFunc", 50, 2)
expect(mockBuilder.buildResultHeader).toHaveBeenCalledWith(prCommentFields)
@ -1211,11 +1213,12 @@ describe("createStandalonePRTitleAndBody", () => {
"replay tests",
"concolic tests",
"medium",
"f47ac10b-58cc-4372-a567-0e02b2c3d478",
)
expect(result.title).toBe("Test PR Title")
expect(result.body).toBe(
'<!-- CODEFLASH_OPTIMIZATION: {"function":"testFunc","speedup_pct":50,"speedup_x":2,"optimization_type":"general","timestamp":"2025-01-01T00:00:00.000Z","version":"1.0"} -->\n## Header\nBenchmark data\nDetails\nTest report\nFooter ![Static Badge](https://img.shields.io/badge/🎯_Optimization_Quality-medium-blue)\n\n',
'<!-- CODEFLASH_OPTIMIZATION: {"trace_id":"f47ac10b-58cc-4372-a567-0e02b2c3d478","function":"testFunc","speedup_pct":50,"speedup_x":2,"optimization_type":"general","timestamp":"2025-01-01T00:00:00.000Z","version":"1.0"} -->\n## Header\nBenchmark data\nDetails\nTest report\nFooter ![Static Badge](https://img.shields.io/badge/🎯_Optimization_Quality-medium-blue)\n\n',
)
expect(mockBuilder.buildBenchmarkInfo).toHaveBeenCalledWith(prCommentFields)
})
@ -1239,10 +1242,11 @@ describe("createStandalonePRTitleAndBody", () => {
"replay tests",
"concolic tests",
"medium",
"f47ac10b-58cc-4372-a567-0e02b2c3d478",
)
expect(result.body).toBe(
'<!-- CODEFLASH_OPTIMIZATION: {"function":"testFunc","speedup_pct":50,"speedup_x":2,"optimization_type":"general","timestamp":"2025-01-01T00:00:00.000Z","version":"1.0"} -->\n## Header\nDetails\nTest report\nFooter ![Static Badge](https://img.shields.io/badge/🎯_Optimization_Quality-medium-blue)\n\n',
'<!-- CODEFLASH_OPTIMIZATION: {"trace_id":"f47ac10b-58cc-4372-a567-0e02b2c3d478","function":"testFunc","speedup_pct":50,"speedup_x":2,"optimization_type":"general","timestamp":"2025-01-01T00:00:00.000Z","version":"1.0"} -->\n## Header\nDetails\nTest report\nFooter ![Static Badge](https://img.shields.io/badge/🎯_Optimization_Quality-medium-blue)\n\n',
)
// buildBenchmarkInfo should NOT be called when benchmark_details is empty array
expect(mockBuilder.buildBenchmarkInfo).not.toHaveBeenCalled()
@ -1267,10 +1271,11 @@ describe("createStandalonePRTitleAndBody", () => {
"replay tests",
"concolic tests",
"medium",
"f47ac10b-58cc-4372-a567-0e02b2c3d478",
)
expect(result.body).toBe(
'<!-- CODEFLASH_OPTIMIZATION: {"function":"testFunc","speedup_pct":50,"speedup_x":2,"optimization_type":"general","timestamp":"2025-01-01T00:00:00.000Z","version":"1.0"} -->\n## Header\nDetails\nTest report\nFooter ![Static Badge](https://img.shields.io/badge/🎯_Optimization_Quality-medium-blue)\n\n',
'<!-- CODEFLASH_OPTIMIZATION: {"trace_id":"f47ac10b-58cc-4372-a567-0e02b2c3d478","function":"testFunc","speedup_pct":50,"speedup_x":2,"optimization_type":"general","timestamp":"2025-01-01T00:00:00.000Z","version":"1.0"} -->\n## Header\nDetails\nTest report\nFooter ![Static Badge](https://img.shields.io/badge/🎯_Optimization_Quality-medium-blue)\n\n',
)
// buildBenchmarkInfo should NOT be called when benchmark_details is null
expect(mockBuilder.buildBenchmarkInfo).not.toHaveBeenCalled()

View file

@ -285,7 +285,7 @@ export async function verifyExistingOptimizations(req: Request, res: Response) {
response_dict[key] = Array.from(optimizations_dict[key])
}
dependencies.posthog.capture({
dependencies.posthog?.capture({
distinctId: userId,
event: `cfapi-github-pr-optimization`,
properties: {

View file

@ -101,7 +101,7 @@ export const githubApp = await (async () => {
app.webhooks.onAny(async ({ id, name, payload }) => {
// Only log event type and ID, not full payload (too verbose)
console.log(`Github App: Received webhook event ${name} (${id})`)
posthog.capture({
posthog?.capture({
distinctId: `github|${payload.sender?.id}`,
event: `cfapi-github-webhook-received`,
properties: {
@ -188,7 +188,7 @@ export const githubApp = await (async () => {
- #${payload.pull_request.number}`,
})
posthog.capture({
posthog?.capture({
distinctId: `github|${payload.sender.id}`, // this is the user who merged the PR
event: `cfapi-github-dependent-pr-merged`,
properties: {
@ -203,7 +203,7 @@ export const githubApp = await (async () => {
`Commented on original PR #${originalPrNumber} and logged the event to Posthog.`,
)
} else if (standalonePrMatch != null) {
posthog.capture({
posthog?.capture({
distinctId: `github|${payload.sender.id}`,
event: `cfapi-github-standalone-pr-merged`,
properties: {
@ -310,7 +310,7 @@ export const githubApp = await (async () => {
app.webhooks.on("marketplace_purchase", async ({ id, name, payload }) => {
console.log(`Received a marketplace purchase event: ${name} (${id})`)
posthog.capture({
posthog?.capture({
distinctId: `github|${payload.sender.id}`,
event: `cfapi-github-marketplace-purchase`,
properties: {
@ -341,7 +341,7 @@ export const githubApp = await (async () => {
)
) {
// Log the event to Posthog
posthog.capture({
posthog?.capture({
distinctId: `github|${payload.sender.id}`,
event: `cfapi-github-commit-coauthored-by-codeflash`,
properties: {

View file

@ -62,6 +62,7 @@ describe("buildPrCommentBody (Integration)", () => {
NEW_BRANCH_NAME,
REPLAY_TESTS,
CONCOLIC_TESTS,
"f47ac10b-58cc-4372-a567-0e02b2c3d478",
)
// Test that all key sections are present
@ -89,6 +90,7 @@ describe("buildPrCommentBody (Integration)", () => {
NEW_BRANCH_NAME,
REPLAY_TESTS,
CONCOLIC_TESTS,
"f47ac10b-58cc-4372-a567-0e02b2c3d478",
{ isCollapsed: true },
)
@ -147,6 +149,7 @@ describe("parseAndCreateOptimizationsDict", () => {
NEW_BRANCH_NAME,
REPLAY_TESTS,
CONCOLIC_TESTS,
"f47ac10b-58cc-4372-a567-0e02b2c3d478",
)
const result = parseAndCreateOptimizationsDict(PRBODY, [])
expect(result).toEqual({
@ -204,7 +207,7 @@ describe("GitHub Utils", () => {
// Unit tests for individual functions
describe("buildOptimizationMetadata", () => {
it("should build metadata JSON comment", () => {
const result = buildOptimizationMetadata(PR_COMMENT_FIELDS)
const result = buildOptimizationMetadata(PR_COMMENT_FIELDS, "")
expect(result).toContain("<!-- CODEFLASH_OPTIMIZATION:")
expect(result).toContain('"function":"find_validation_error"')
expect(result).toContain('"file":"src/backend/base/langflow/api/v1/mcp.py"')
@ -320,6 +323,7 @@ describe("buildPrCommentBody - Extended Coverage", () => {
NEW_BRANCH_NAME,
REPLAY_TESTS,
CONCOLIC_TESTS,
"f47ac10b-58cc-4372-a567-0e02b2c3d478",
{ includeHeader: false },
)
expect(result).not.toContain("Codeflash found optimizations for this PR")
@ -334,6 +338,7 @@ describe("buildPrCommentBody - Extended Coverage", () => {
NEW_BRANCH_NAME,
REPLAY_TESTS,
CONCOLIC_TESTS,
"f47ac10b-58cc-4372-a567-0e02b2c3d478",
{ isUnifiedReview: true },
)
// Should still contain the core content
@ -361,6 +366,7 @@ describe("buildPrCommentBody - Extended Coverage", () => {
NEW_BRANCH_NAME,
REPLAY_TESTS,
CONCOLIC_TESTS,
"f47ac10b-58cc-4372-a567-0e02b2c3d478",
)
expect(result).toContain("This change will improve the performance")
})

View file

@ -40,8 +40,9 @@ export function generateOptimizationReviewTemplate(optimizationReview: string):
}
// Helper function to create invisible HTML metadata
export function buildOptimizationMetadata(fields: PrCommentFields): string {
export function buildOptimizationMetadata(fields: PrCommentFields, trace_id: string): string {
const metadata: {
trace_id: string
function: string
file: string
speedup_pct: string
@ -54,6 +55,7 @@ export function buildOptimizationMetadata(fields: PrCommentFields): string {
original_async_throughput?: string
best_async_throughput?: string
} = {
trace_id,
function: fields.function_name,
file: fields.file_path,
speedup_pct: fields.speedup_pct,
@ -173,6 +175,7 @@ export function buildPrCommentBody(
newBranchName: string,
replayTests: any,
concolicTests: any,
trace_id: string,
options: {
isUnifiedReview?: boolean
includeHeader?: boolean
@ -186,12 +189,19 @@ export function buildPrCommentBody(
? buildBenchmarkInfo(prCommentFields)
: ""
return (
`${buildOptimizationMetadata(prCommentFields)}\n` +
`${buildOptimizationMetadata(prCommentFields, trace_id)}\n` +
(includeHeader ? `#### ⚡️ Codeflash found optimizations for this PR\n` : "") +
`${buildResultHeader(prCommentFields, isUnifiedReview)}\n` +
(benchmarkInfo ? `${benchmarkInfo}\n` : "") +
`${buildResultDetails(prCommentFields, isCollapsed)}\n` +
`${buildResultTestReport(prCommentFields, existingTests, generatedTests, coverage_message, replayTests, concolicTests)}\n` +
`${buildResultTestReport(
prCommentFields,
existingTests,
generatedTests,
coverage_message,
replayTests,
concolicTests,
)}\n` +
`${buildMergeBranchMsg(newBranchName)}\n`
)
}

View file

@ -261,7 +261,7 @@ appExpress.listen(Number(port), () => {
})
})
posthog.shutdown()
posthog?.shutdown()
// Handle unhandled promise rejections and uncaught exceptions
process.on("unhandledRejection", GlobalErrorHandler.handleUnhandledRejection)

View file

@ -24,7 +24,7 @@ export async function checkForValidAPIKey(
method: req.method,
})
posthog.capture({
posthog?.capture({
distinctId: "null-user-with-missing-authorization-header",
event: `cfapi-endpoint-called-with-missing-authorization-header`,
properties: {
@ -53,7 +53,7 @@ export async function checkForValidAPIKey(
apiKeyLength: apiKey.length,
})
posthog.capture({
posthog?.capture({
distinctId: "null-user-with-invalid-api-key",
event: `cfapi-endpoint-called-with-invalid-api-key`,
properties: {

View file

@ -19,7 +19,7 @@ export function trackEndpointCalls(req, res, next) {
...(req.ip && { ip: req.ip }),
})
posthog.capture({
posthog?.capture({
distinctId: req.userId,
event: `cfapi-endpoint-called-${req.path}`,
})

View file

@ -584,7 +584,7 @@ export class Logger {
entry.context?.operation === "optimization_event" ||
entry.context?.operation === "github_webhook"
) {
posthog.capture({
posthog?.capture({
distinctId: entry.context.userId || "anonymous",
event: `cfapi-${entry.context.operation}`,
properties: {

View file

@ -4,8 +4,12 @@
import * as Sentry from "@sentry/nextjs"
const isProduction = process.env.NODE_ENV === "production"
Sentry.init({
dsn: "https://0fa0f40b2d709e4f1eb9aac76ff9e6be@o4506833230561280.ingest.us.sentry.io/4506833279582208",
dsn: isProduction
? "https://0fa0f40b2d709e4f1eb9aac76ff9e6be@o4506833230561280.ingest.us.sentry.io/4506833279582208"
: undefined,
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,

View file

@ -19,6 +19,8 @@ export async function SubmitFirstOnboardingPage(
const user_id = session?.user?.sub
const nickname = session?.user?.nickname
const posthog = PostHogClient()
if (!posthog) return
posthog.identify({
distinctId: user_id,
properties: {
@ -37,7 +39,7 @@ export async function SubmitFirstOnboardingPage(
custom_pain_point: customOptionInput,
},
})
await posthog.shutdown()
await posthog?.shutdown()
await submitOnboardingQuestions(user_id, email)
// Check for saved redirect URL after onboarding completion
@ -62,6 +64,8 @@ export async function SubmitSkipOnboardingPage(): Promise<void> {
const user_id = session?.user?.sub
const nickname = session?.user?.nickname
const posthog = PostHogClient()
if (!posthog) return
posthog.identify({
distinctId: user_id,
properties: {
@ -76,7 +80,7 @@ export async function SubmitSkipOnboardingPage(): Promise<void> {
username: nickname,
},
})
await posthog.shutdown()
await posthog?.shutdown()
await markUserCompletedOnboarding(user_id)
// Checking for saved redirect URL after onboarding completion

View file

@ -18,6 +18,7 @@ export async function SubmitSecondOnboardingPage(
const user_id = session?.user?.sub
const nickname = session?.user?.nickname
const posthog = PostHogClient()
if (!posthog) return
posthog.capture({
distinctId: user_id,
@ -30,5 +31,5 @@ export async function SubmitSecondOnboardingPage(
...(colleagueInviteEmail && { colleague_invite_email: colleagueInviteEmail }),
},
})
await posthog.shutdown()
await posthog?.shutdown()
}

View file

@ -16,6 +16,7 @@ export async function upsertReferralSource(
if (session != null) {
setUserReferralData(session.user.sub, referralSource, additionalComments)
const posthog = PostHogClient()
if (!posthog) return
// Log the referral event to PostHog
posthog.capture({

View file

@ -22,13 +22,13 @@ export default async function APIKeyGenerator(): Promise<JSX.Element> {
})
const posthog = PostHogClient()
posthog.capture({
posthog?.capture({
distinctId: userId,
properties: { username: session.nickname },
event: "webapp-loaded-api-keys",
})
await posthog.shutdown()
await posthog?.shutdown()
return (
<div>

View file

@ -11,12 +11,12 @@ export default async function BillingPage() {
try {
// Track page view
const posthog = PostHogClient()
posthog.capture({
posthog?.capture({
distinctId: userId,
properties: { username: session.user.nickname },
event: "webapp-loaded-billing-page",
})
await posthog.shutdown()
await posthog?.shutdown()
// Get subscription info from database with lazy reset
const subscription = (await checkAndResetSubscriptionPeriod(userId)) || {

View file

@ -8,13 +8,13 @@ export default async function GettingStarted() {
const userId = session.user.sub
const posthog = PostHogClient()
posthog.capture({
posthog?.capture({
distinctId: userId,
properties: { username: session.nickname },
event: "webapp-loaded-getting-started",
})
await posthog.shutdown()
await posthog?.shutdown()
return <GettingStartedClient />
}

View file

@ -16,7 +16,6 @@ import {
import { useViewMode } from "@/app/app/ViewModeContext"
import { MembersList } from "@/components/members/members-list"
import { UserSearchModal } from "@/components/members/user-search-modal"
import { RoleSelector } from "@/components/members/role-selector"
function OrganizationMembers() {
const { currentOrg } = useViewMode()
@ -32,7 +31,6 @@ function OrganizationMembers() {
const [searchQuery, setSearchQuery] = useState("")
const [filterRole, setFilterRole] = useState<"all" | "owner" | "admin" | "member">("all")
const [isRefreshing, setIsRefreshing] = useState(false)
const [selectedRole, setSelectedRole] = useState<"admin" | "member">("member")
const [confirmDialog, setConfirmDialog] = useState<{
open: boolean
memberId: string
@ -105,12 +103,15 @@ function OrganizationMembers() {
setSuccess("Member added successfully!")
}
const handleUserAdd = async (user: GitHubUserSearchResult) => {
const handleUserAdd = async (
user: GitHubUserSearchResult,
role: "admin" | "member",
) => {
if (!currentOrg?.id) {
return { success: false, error: "No organization selected" }
}
const result = await addOrganizationMember(currentUserId, user, selectedRole, currentOrg?.id)
const result = await addOrganizationMember(currentUserId, user, role, currentOrg?.id)
if (result.success) {
handleMemberAdded()
}
@ -231,22 +232,15 @@ function OrganizationMembers() {
</p>
</div>
{isAdmin && (
<div className="flex items-center gap-3">
<RoleSelector
selectedRole={selectedRole}
onChange={setSelectedRole}
disabled={showAddModal}
/>
<button
onClick={() => setShowAddModal(true)}
disabled={showAddModal}
className="flex items-center gap-2 px-4 py-2.5 bg-primary text-primary-foreground rounded-xl hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 text-sm font-medium whitespace-nowrap flex-shrink-0 shadow-sm hover:shadow-md"
>
<UserPlus size={16} />
<span className="hidden sm:inline">Add Member</span>
<span className="sm:hidden">Add</span>
</button>
</div>
<button
onClick={() => setShowAddModal(true)}
disabled={showAddModal}
className="flex items-center gap-2 px-4 py-2.5 bg-primary text-primary-foreground rounded-xl hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 text-sm font-medium whitespace-nowrap flex-shrink-0 shadow-sm hover:shadow-md"
>
<UserPlus size={16} />
<span className="hidden sm:inline">Add Member</span>
<span className="sm:hidden">Add</span>
</button>
)}
</div>
</div>
@ -277,9 +271,9 @@ function OrganizationMembers() {
isOpen={showAddModal}
onClose={() => setShowAddModal(false)}
onUserAdd={handleUserAdd}
title={`Add Organization Member as ${selectedRole === "admin" ? "Admin" : "Member"}`}
title="Add Organization Member"
description="Search for GitHub users and add them to this organization"
addButtonText={`Add as ${selectedRole === "admin" ? "Admin" : "Member"}`}
showRoleSelector={true}
/>
<ConfirmDialog

View file

@ -709,7 +709,7 @@ function RepositoriesPage() {
Install GitHub App
</a>
<a
href="https://docs.codeflash.ai/getting-started/github-app"
href="https://docs.codeflash.ai/getting-started/local-installation"
target="_blank"
rel="noopener noreferrer"
className="w-full sm:w-auto inline-flex items-center justify-center rounded-lg border border-border px-4 py-2 text-xs sm:text-sm font-medium text-foreground hover:bg-muted/40 transition-colors"

View file

@ -2,6 +2,35 @@
import { getRepositoriesForAccountCached } from "@/lib/services/repository-utils"
import { AccountPayload, buildOptimizationOrCondition, prisma } from "@codeflash-ai/common"
export async function getRepositoriesWithStagingEvents(
payload: AccountPayload,
): Promise<Array<{ id: string; full_name: string }>> {
const { repoIds, repos: allRepos } = await getRepositoriesForAccountCached(payload)
if (repoIds.length === 0) {
return []
}
// Get distinct repository IDs that have staging events using groupBy (more efficient than findMany with distinct)
const repoIdsWithStagingEvents = await prisma.optimization_events.groupBy({
by: ["repository_id"],
where: {
is_staging: true,
...buildOptimizationOrCondition(payload, repoIds),
repository_id: { not: null },
},
})
// Filter and map repos that have staging events
return allRepos
.filter(repo => repoIdsWithStagingEvents.some(group => group.repository_id === repo.id))
.map(repo => ({
id: repo.id,
full_name: repo.full_name,
}))
.sort((a, b) => a.full_name.localeCompare(b.full_name))
}
export async function getAllOptimizationEvents({
payload,
search,
@ -40,6 +69,14 @@ export async function getAllOptimizationEvents({
mode: "insensitive",
},
},
{
repository: {
full_name: {
contains: search,
mode: "insensitive",
},
},
},
],
})
}
@ -78,7 +115,7 @@ export async function getAllOptimizationEvents({
// Add search conditions
if (search) {
whereConditions.push(
`(oe.function_name ILIKE $${paramIndex} OR oe.file_path ILIKE $${paramIndex})`,
`(oe.function_name ILIKE $${paramIndex} OR oe.file_path ILIKE $${paramIndex} OR r.full_name ILIKE $${paramIndex})`,
)
params.push(`%${search}%`)
paramIndex += 1
@ -139,6 +176,7 @@ export async function getAllOptimizationEvents({
of.review_explanation
FROM optimization_events oe
LEFT JOIN optimization_features of ON oe.trace_id = of.trace_id
LEFT JOIN repositories r ON oe.repository_id = r.id
WHERE ${whereClause}
ORDER BY ${orderByClause}
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
@ -153,6 +191,7 @@ export async function getAllOptimizationEvents({
SELECT COUNT(*) as count
FROM optimization_events oe
LEFT JOIN optimization_features of ON oe.trace_id = of.trace_id
LEFT JOIN repositories r ON oe.repository_id = r.id
WHERE ${whereClause}
`,
...params,

View file

@ -1,5 +1,5 @@
"use client"
import { useState, useEffect, useCallback, useRef } from "react"
import { useState, useEffect, useLayoutEffect, useCallback, useRef } from "react"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import {
@ -33,7 +33,7 @@ import { formatDistanceToNow } from "date-fns"
import { useRouter } from "next/navigation"
import { getUserId, getUserIdAndUsername } from "@/app/utils/auth"
import { Button } from "@/components/ui/button"
import { getAllOptimizationEvents } from "./action"
import { getAllOptimizationEvents, getRepositoriesWithStagingEvents } from "./action"
import Image from "next/image"
import { useViewMode } from "@/app/app/ViewModeContext"
import { ReviewQualityBadge } from "@/components/ui/quality_badge"
@ -71,7 +71,7 @@ interface OptimizationEvent {
interface FilterState {
search: string
hasRepo: string
repositoryId: string | null
status: string
eventType: string
reviewQuality: string
@ -226,12 +226,16 @@ export default function StagingPage() {
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const { currentOrg } = useViewMode()
const { currentOrg, loading: isLoadingViewMode } = useViewMode()
const [availableRepositories, setAvailableRepositories] = useState<
Array<{ id: string; full_name: string }>
>([])
// Combined filter state
const [filters, setFilters] = useState<FilterState>({
search: "",
hasRepo: "all",
repositoryId: null,
status: "all",
eventType: "all",
reviewQuality: "all",
@ -261,6 +265,59 @@ export default function StagingPage() {
loadUserId()
}, [])
// Track previous org to detect context switches
const previousOrgId = useRef<string | null | undefined>(undefined)
const isContextSwitching = useRef(false)
// Reset repository filter immediately when switching contexts (synchronously, before other effects)
useLayoutEffect(() => {
const currentOrgId = currentOrg?.id ?? null
// Skip on initial mount
if (previousOrgId.current === undefined) {
previousOrgId.current = currentOrgId
return
}
// Check if context actually changed
if (previousOrgId.current !== currentOrgId) {
// Mark that we're switching contexts
isContextSwitching.current = true
// Reset filter immediately (synchronously) if a specific repository is selected
setFilters(prev => {
if (prev.repositoryId && prev.repositoryId !== "none") {
return { ...prev, repositoryId: null }
}
return prev
})
previousOrgId.current = currentOrgId
}
}, [currentOrg])
// Load repositories with staging events
useEffect(() => {
const loadRepositories = async () => {
if (!userId) return
try {
const userSession = await getUserIdAndUsername()
const repos = await getRepositoriesWithStagingEvents(
currentOrg
? { orgId: currentOrg.id }
: { userId: userSession.userId, username: userSession.username },
)
setAvailableRepositories(repos)
} catch (err) {
console.error("Failed to load repositories:", err)
}
}
// Wait for both user and view mode to be ready before loading repositories
if (!isLoadingUser && !isLoadingViewMode && userId) {
loadRepositories()
}
}, [userId, isLoadingUser, isLoadingViewMode, currentOrg])
// Memoized load events function
const loadEvents = useCallback(async () => {
if (!userId) return
@ -271,11 +328,12 @@ export default function StagingPage() {
try {
const filter: Record<string, string | null | { not: null }> = {}
if (filters.hasRepo === "yes") {
filter.repository_id = { not: null }
} else if (filters.hasRepo === "no") {
if (filters.repositoryId === "none") {
filter.repository_id = null
} else if (filters.repositoryId) {
filter.repository_id = filters.repositoryId
}
// If null (all), don't add filter
if (filters.status !== "all") {
filter.status = filters.status
@ -344,7 +402,8 @@ export default function StagingPage() {
// Load events when filters change - with debounce for search
useEffect(() => {
if (!isLoadingUser && userId) {
// Wait for both user and view mode to be ready before loading
if (!isLoadingUser && !isLoadingViewMode && userId) {
// Skip initial mount
if (isInitialMount.current) {
isInitialMount.current = false
@ -352,6 +411,20 @@ export default function StagingPage() {
return
}
// Skip loading if we're in the middle of a context switch and filter hasn't been reset yet
// This prevents loading with an invalid filter before it's reset
if (isContextSwitching.current && filters.repositoryId && filters.repositoryId !== "none") {
return
}
// Clear context switching flag once filter is reset (repositoryId is null or "none")
if (
isContextSwitching.current &&
(!filters.repositoryId || filters.repositoryId === "none")
) {
isContextSwitching.current = false
}
// Clear existing timer
if (debounceTimer.current) {
clearTimeout(debounceTimer.current)
@ -373,7 +446,7 @@ export default function StagingPage() {
clearTimeout(debounceTimer.current)
}
}
}, [userId, isLoadingUser, filters, loadEvents])
}, [userId, isLoadingUser, isLoadingViewMode, filters, loadEvents])
const handleRowClick = useCallback(
(traceId: string) => {
@ -383,7 +456,7 @@ export default function StagingPage() {
)
// Update filter functions
const updateFilter = useCallback((key: keyof FilterState, value: string | number) => {
const updateFilter = useCallback((key: keyof FilterState, value: string | number | null) => {
setFilters(prev => ({
...prev,
[key]: value,
@ -395,7 +468,7 @@ export default function StagingPage() {
const clearFilters = useCallback(() => {
setFilters({
search: "",
hasRepo: "all",
repositoryId: null,
status: "all",
eventType: "all",
reviewQuality: "all",
@ -406,7 +479,7 @@ export default function StagingPage() {
const hasActiveFilters =
filters.search ||
filters.hasRepo !== "all" ||
filters.repositoryId !== null ||
filters.status !== "all" ||
filters.eventType !== "all" ||
filters.reviewQuality !== "all" ||
@ -456,12 +529,13 @@ export default function StagingPage() {
const x = clamp(speedup, 1, 300)
const t = (x - 1) / 299
const hue = 137
const lightness = 92 - t * (92 - 28)
const saturation = 65 + t * (95 - 65)
const textColor = lightness < 55 ? "#fff" : "#14532d"
const borderLightness = lightness > 60 ? lightness - 12 : lightness + 12
const borderSaturation = saturation > 80 ? saturation - 10 : saturation + 10
// Use emerald color scheme that matches the additions badge
const hue = 158 // Emerald hue
const lightness = 95 - t * (95 - 35)
const saturation = 45 + t * (75 - 45)
const textColor = lightness < 60 ? "#fff" : "#047857" // emerald-700
const borderLightness = lightness > 60 ? lightness - 8 : lightness + 8
const borderSaturation = saturation > 70 ? saturation - 10 : saturation + 10
const bgColor = `hsl(${hue}, ${saturation}%, ${lightness}%)`
const borderColor = `hsl(${hue}, ${borderSaturation}%, ${borderLightness}%)`
@ -469,7 +543,7 @@ export default function StagingPage() {
return (
<Badge
variant="default"
className="font-mono text-[11px] px-2 py-0.5 whitespace-nowrap"
className="font-mono text-[11px] px-2 py-0.5 whitespace-nowrap font-medium"
style={{
backgroundColor: bgColor,
color: textColor,
@ -547,7 +621,7 @@ export default function StagingPage() {
)
}, [])
if (isLoadingUser) {
if (isLoadingUser || isLoadingViewMode) {
return (
<div className="flex flex-col items-center justify-center h-[70vh]">
<div className="animate-spin rounded-full h-10 w-10 sm:h-12 sm:w-12 border-t-2 border-b-2 border-primary mb-4"></div>
@ -578,7 +652,7 @@ export default function StagingPage() {
<div className="relative flex-1 min-w-[200px] max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
<Input
placeholder="Search by function name or file path..."
placeholder="Search by function name, file path, or repository name..."
value={filters.search}
onChange={e => updateFilter("search", e.target.value)}
className="pl-10 w-full"
@ -592,19 +666,43 @@ export default function StagingPage() {
</div>
{/* Filter Controls */}
<Select value={filters.hasRepo} onValueChange={value => updateFilter("hasRepo", value)}>
<SelectTrigger className="w-[140px] sm:w-[180px]">
<SelectValue placeholder="Repository" />
<Select
value={filters.repositoryId || "all"}
onValueChange={value =>
updateFilter(
"repositoryId",
value === "all" ? null : value === "none" ? "none" : value,
)
}
>
<SelectTrigger
className={`w-[180px] sm:w-[220px] ${
filters.repositoryId === null
? "text-muted-foreground [&>span]:text-muted-foreground"
: ""
}`}
>
<SelectValue placeholder="All Repositories" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Repositories</SelectItem>
<SelectItem value="yes">With Repository</SelectItem>
<SelectItem value="no">Without Repository</SelectItem>
<SelectItem value="all">All Repositories</SelectItem>
<SelectItem value="none">Without Repository</SelectItem>
{availableRepositories.map(repo => (
<SelectItem key={repo.id} value={repo.id}>
{repo.full_name}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={filters.status} onValueChange={value => updateFilter("status", value)}>
<SelectTrigger className="w-[120px] sm:w-[150px]">
<SelectTrigger
className={`w-[120px] sm:w-[150px] ${
filters.status === "all"
? "text-muted-foreground [&>span]:text-muted-foreground"
: ""
}`}
>
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
@ -618,7 +716,13 @@ export default function StagingPage() {
value={filters.eventType}
onValueChange={value => updateFilter("eventType", value)}
>
<SelectTrigger className="w-[120px] sm:w-[150px]">
<SelectTrigger
className={`w-[120px] sm:w-[150px] ${
filters.eventType === "all"
? "text-muted-foreground [&>span]:text-muted-foreground"
: ""
}`}
>
<SelectValue placeholder="Event Type" />
</SelectTrigger>
<SelectContent>
@ -634,7 +738,13 @@ export default function StagingPage() {
value={filters.reviewQuality}
onValueChange={value => updateFilter("reviewQuality", value)}
>
<SelectTrigger className="w-[120px] sm:w-[150px]">
<SelectTrigger
className={`w-[120px] sm:w-[150px] ${
filters.reviewQuality === "all"
? "text-muted-foreground [&>span]:text-muted-foreground"
: ""
}`}
>
<SelectValue placeholder="Quality" />
</SelectTrigger>
<SelectContent>
@ -646,7 +756,13 @@ export default function StagingPage() {
</Select>
<Select value={filters.sortBy} onValueChange={value => updateFilter("sortBy", value)}>
<SelectTrigger className="w-[140px] sm:w-[200px]">
<SelectTrigger
className={`w-[140px] sm:w-[200px] ${
filters.sortBy === "created_at_desc"
? "text-muted-foreground [&>span]:text-muted-foreground"
: ""
}`}
>
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
@ -721,7 +837,7 @@ export default function StagingPage() {
onClick={() => toggleSort("created_at")}
>
<div className="flex items-center justify-end gap-1">
<span>CREATED AT</span>
<span>CREATED</span>
{getSortIcon("created_at")}
</div>
</TableHead>
@ -875,7 +991,7 @@ export default function StagingPage() {
{diffStats.totalAdditions > 0 && (
<Badge
variant="secondary"
className="bg-green-100 text-green-800 border-green-300 dark:bg-green-900 dark:text-green-100 dark:border-green-700 whitespace-nowrap"
className="bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-950/50 dark:text-emerald-400 dark:border-emerald-800 whitespace-nowrap font-medium"
>
+{diffStats.totalAdditions}
</Badge>
@ -883,13 +999,13 @@ export default function StagingPage() {
{diffStats.totalDeletions > 0 && (
<Badge
variant="secondary"
className="bg-red-100 text-red-800 border-red-300 dark:bg-red-900 dark:text-red-100 dark:border-red-700 whitespace-nowrap"
className="bg-rose-50 text-rose-700 border-rose-200 dark:bg-rose-950/50 dark:text-rose-400 dark:border-rose-800 whitespace-nowrap font-medium"
>
-{diffStats.totalDeletions}
</Badge>
)}
{diffStats.totalAdditions === 0 && diffStats.totalDeletions === 0 && (
<span className="text-muted-foreground">-</span>
<span className="text-muted-foreground text-xs"></span>
)}
</div>
</TableCell>

View file

@ -279,7 +279,7 @@ function Dashboard() {
Install GitHub App
</a>
<a
href="https://docs.codeflash.ai/getting-started/github-app"
href="https://docs.codeflash.ai/getting-started/local-installation"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center rounded-lg border border-border px-4 py-2 text-xs sm:text-sm font-medium text-foreground hover:bg-muted/40 transition-colors"

View file

@ -2,7 +2,7 @@
import posthog from "posthog-js"
import { PostHogProvider } from "posthog-js/react"
if (typeof window !== "undefined") {
if (typeof window !== "undefined" && process.env.NODE_ENV === "production") {
posthog.init("phc_aUO790jHd7z1SXwsYCz8dRApxueplZlZWeDSpKc5hol", {
api_host: "https://app.posthog.com",
capture_pageview: false, // Disable automatic pageview capture, as we capture manually

View file

@ -23,6 +23,7 @@ import {
UserCircle,
Menu,
Zap,
BarChart3,
} from "lucide-react"
import { UserProfile } from "@auth0/nextjs-auth0/client"
import { SignOut } from "../ui/SignOut"
@ -32,6 +33,7 @@ import { SIDEBAR_ANNOUNCEMENT } from "@/config/announcements"
import { getCurrentUserSubscriptionData } from "@/app/dashboard/action"
import { Progress } from "@/components/ui/progress"
import { formatCredits, calculateCreditsPercentage, getProgressBarClassName } from "@/lib/utils"
import { isTeamMemberCheck } from "@/lib/team-members"
interface SidebarProps {
className: string
@ -272,6 +274,29 @@ export function Sidebar({ className, user, isLoading, error }: SidebarProps): JS
</Link>
)}
{/* Observability Links - Only for Team Members */}
{user && isTeamMemberCheck({ email: user.email || undefined, nickname: user.nickname || undefined }) && (
<>
{/* Divider */}
<div className="my-3">
<div className="h-px bg-border" />
</div>
{/* Observability Group */}
<Link href="/observability/traces" target="_blank" rel="noopener noreferrer" className="block mb-1">
<Button
variant={
currentRoute?.startsWith("/observability") ? "secondary" : "ghost"
}
className="flex items-center justify-start w-full"
>
<BarChart3 size={16} className="mr-2 h-4 w-4 shrink-0" />
Observability
</Button>
</Link>
</>
)}
{/* Divider */}
<div className="my-3">
<div className="h-px bg-border" />

View file

@ -1,6 +1,6 @@
"use client"
import React, { useState, useEffect } from "react"
import { Search, X, AlertCircle, Check, UserPlus, Users, Shield, ChevronDown } from "lucide-react"
import { Search, X, AlertCircle, Check, UserPlus, Users, Shield } from "lucide-react"
import Image from "next/image"
import { GitHubUserSearchResult } from "@/lib/types"
import { githubService } from "@/lib/services/github-service"
@ -18,8 +18,8 @@ interface UserSearchModalProps {
showRoleSelector?: boolean
}
// Role Selector Component (internal to modal)
const RoleSelector = ({
// Compact Role Toggle Component (segmented control style)
const RoleToggle = ({
selectedRole,
onChange,
disabled = false,
@ -28,86 +28,40 @@ const RoleSelector = ({
onChange: (role: "admin" | "member") => void
disabled?: boolean
}) => {
const [isOpen, setIsOpen] = useState(false)
const roles = [
{
value: "member" as const,
label: "Member",
icon: <Users size={16} />,
description: "Can view repository data",
color: "text-gray-700 dark:text-gray-300",
bgColor: "bg-gray-50 dark:bg-gray-800/50",
borderColor: "border-gray-300 dark:border-gray-700",
hoverBg: "hover:bg-gray-100 dark:hover:bg-gray-800",
},
{
value: "admin" as const,
label: "Admin",
icon: <Shield size={16} />,
description: "Can manage members and settings",
color: "text-blue-700 dark:text-blue-300",
bgColor: "bg-blue-50 dark:bg-blue-900/20",
borderColor: "border-blue-300 dark:border-blue-700",
hoverBg: "hover:bg-blue-100 dark:hover:bg-blue-900/30",
},
]
const selectedRoleData = roles.find(r => r.value === selectedRole)
return (
<div className="relative">
<div
className={`inline-flex items-center rounded-lg border border-border bg-muted/50 p-0.5 ${
disabled ? "opacity-50 cursor-not-allowed" : ""
}`}
>
<button
type="button"
onClick={() => !disabled && setIsOpen(!isOpen)}
onClick={() => !disabled && onChange("member")}
disabled={disabled}
className={`w-full sm:w-auto min-w-[160px] flex items-center justify-between gap-3 px-4 py-3 rounded-xl border-2 transition-all duration-200 ${
selectedRoleData?.bgColor
} ${selectedRoleData?.borderColor} ${selectedRoleData?.color} ${
disabled
? "opacity-50 cursor-not-allowed"
: "hover:shadow-md cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
}`}
title="Member: Can view repository data"
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium transition-all duration-200 ${
selectedRole === "member"
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
} ${disabled ? "cursor-not-allowed" : "cursor-pointer"}`}
>
<div className="flex items-center gap-2">
{selectedRoleData?.icon}
<span className="font-medium text-sm">{selectedRoleData?.label}</span>
</div>
<ChevronDown
size={16}
className={`transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`}
/>
<Users size={13} />
<span>Member</span>
</button>
<button
type="button"
onClick={() => !disabled && onChange("admin")}
disabled={disabled}
title="Admin: Can manage members and settings"
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium transition-all duration-200 ${
selectedRole === "admin"
? "bg-blue-600 text-white shadow-sm"
: "text-muted-foreground hover:text-foreground"
} ${disabled ? "cursor-not-allowed" : "cursor-pointer"}`}
>
<Shield size={13} />
<span>Admin</span>
</button>
{isOpen && !disabled && (
<>
<div className="fixed inset-0 z-10" onClick={() => setIsOpen(false)} />
<div className="absolute top-full left-0 right-0 mt-2 z-20 bg-card border border-border rounded-xl shadow-lg overflow-hidden animate-in slide-in-from-top-2">
{roles.map(role => (
<button
key={role.value}
type="button"
onClick={() => {
onChange(role.value)
setIsOpen(false)
}}
className={`w-full flex items-start gap-3 px-4 py-3 transition-colors ${
selectedRole === role.value ? `${role.bgColor} ${role.color}` : role.hoverBg
} ${selectedRole === role.value ? "cursor-default" : "cursor-pointer"}`}
>
<div className="flex-shrink-0 mt-0.5">{role.icon}</div>
<div className="flex-1 text-left">
<div className="font-medium text-sm flex items-center gap-2">
{role.label}
{selectedRole === role.value && <Check size={14} className={role.color} />}
</div>
<div className="text-xs text-muted-foreground mt-0.5">{role.description}</div>
</div>
</button>
))}
</div>
</>
)}
</div>
)
}
@ -127,7 +81,8 @@ export const UserSearchModal: React.FC<UserSearchModalProps> = ({
const [addingUser, setAddingUser] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)
const [selectedRole, setSelectedRole] = useState<"admin" | "member">("member")
// Per-user role selections for inline role toggle
const [userRoles, setUserRoles] = useState<Record<string, "admin" | "member">>({})
const handleSearch = async () => {
if (!searchQuery.trim()) {
@ -165,15 +120,27 @@ export const UserSearchModal: React.FC<UserSearchModalProps> = ({
setError(null)
setSuccess(null)
const result = await onUserAdd(user, selectedRole)
// Get the role for this specific user (default to member)
const roleToUse = showRoleSelector ? userRoles[user.username] || "member" : "member"
const result = await onUserAdd(user, roleToUse)
if (result.success) {
setSuccess(
`${user.username} has been added as ${selectedRole === "admin" ? "Admin" : "Member"}!`,
`${user.username} has been added as ${roleToUse === "admin" ? "Admin" : "Member"}!`,
)
// Clear search results and query to allow adding more users
setSearchQuery("")
setSearchResults([])
// Remove the added user from results
setSearchResults(prev => prev.filter(u => u.username !== user.username))
// Clean up their role selection
setUserRoles(prev => {
const newRoles = { ...prev }
delete newRoles[user.username]
return newRoles
})
// If no more results, clear search query
if (searchResults.length === 1) {
setSearchQuery("")
}
// Auto-dismiss success message after 3 seconds
setTimeout(() => {
@ -194,7 +161,7 @@ export const UserSearchModal: React.FC<UserSearchModalProps> = ({
setError(null)
setSuccess(null)
setAddingUser(null)
setSelectedRole("member")
setUserRoles({})
}
}, [isOpen])
@ -223,71 +190,46 @@ export const UserSearchModal: React.FC<UserSearchModalProps> = ({
</button>
</div>
{/* Search Input and Role Selection */}
{/* Search Input */}
<div className="p-6 border-b border-border bg-accent/30">
<div className="flex flex-col gap-3">
{/* Role Selection - Only show if enabled */}
{showRoleSelector && (
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Role <span className="text-destructive">*</span>
</label>
<RoleSelector
selectedRole={selectedRole}
onChange={setSelectedRole}
disabled={!!addingUser || isSearching}
/>
</div>
)}
{/* Search Input with Button */}
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Search GitHub User <span className="text-destructive">*</span>
</label>
<div className="flex gap-2">
<div className="relative flex-1">
<Search
size={18}
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground"
/>
<input
type="text"
placeholder="Enter username to search..."
value={searchQuery}
onChange={e => {
setSearchQuery(e.target.value)
setError(null)
setSuccess(null)
}}
onKeyPress={handleKeyPress}
className="w-full pl-10 pr-4 py-3 bg-background border border-border rounded-xl focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm placeholder:text-muted-foreground"
autoFocus
disabled={!!addingUser || isSearching}
/>
</div>
<button
onClick={handleSearch}
disabled={!!addingUser || isSearching || !searchQuery.trim()}
className="px-6 py-3 bg-primary text-primary-foreground rounded-xl hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 text-sm font-medium whitespace-nowrap shadow-sm hover:shadow-md flex items-center gap-2"
>
{isSearching ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary-foreground"></div>
<span className="hidden sm:inline">Searching...</span>
</>
) : (
<>
<Search size={16} />
<span className="hidden sm:inline">Search</span>
</>
)}
</button>
</div>
<p className="text-xs text-muted-foreground mt-2">
Press Enter or click Search to find GitHub users
</p>
<div className="flex gap-2">
<div className="relative flex-1">
<Search
size={18}
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground"
/>
<input
type="text"
placeholder="Search GitHub username..."
value={searchQuery}
onChange={e => {
setSearchQuery(e.target.value)
setError(null)
setSuccess(null)
}}
onKeyPress={handleKeyPress}
className="w-full pl-10 pr-4 py-2.5 bg-background border border-border rounded-xl focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm placeholder:text-muted-foreground"
autoFocus
disabled={!!addingUser || isSearching}
/>
</div>
<button
onClick={handleSearch}
disabled={!!addingUser || isSearching || !searchQuery.trim()}
className="px-5 py-2.5 bg-primary text-primary-foreground rounded-xl hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 text-sm font-medium whitespace-nowrap shadow-sm hover:shadow-md flex items-center gap-2"
>
{isSearching ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary-foreground"></div>
<span className="hidden sm:inline">Searching...</span>
</>
) : (
<>
<Search size={16} />
<span className="hidden sm:inline">Search</span>
</>
)}
</button>
</div>
{/* Error Message */}
@ -311,51 +253,63 @@ export const UserSearchModal: React.FC<UserSearchModalProps> = ({
<div className="flex-1 overflow-y-auto p-6">
{searchResults.length > 0 ? (
<div className="space-y-2">
{searchResults.map(user => (
<div
key={user.username}
className="group flex items-center justify-between p-4 border border-border rounded-xl hover:border-primary/50 hover:bg-accent/50 transition-all duration-200"
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="relative flex-shrink-0">
{searchResults.map(user => {
const userRole = userRoles[user.username] || "member"
return (
<div
key={user.username}
className="group flex items-center gap-3 p-3 border border-border rounded-xl hover:border-primary/50 hover:bg-accent/30 transition-all duration-200"
>
{/* User Avatar */}
<div className="flex-shrink-0">
<Image
src={user.avatarUrl}
alt={user.username}
width={48}
height={48}
width={40}
height={40}
className="rounded-full ring-2 ring-border group-hover:ring-primary/50 transition-all"
/>
</div>
{/* Username */}
<div className="flex-1 min-w-0">
<div className="font-medium text-sm sm:text-base truncate text-foreground">
<div className="font-medium text-sm truncate text-foreground">
{user.username}
</div>
</div>
{/* Role Toggle + Add Button */}
<div className="flex items-center gap-2 flex-shrink-0">
{showRoleSelector && (
<RoleToggle
selectedRole={userRole}
onChange={role =>
setUserRoles(prev => ({ ...prev, [user.username]: role }))
}
disabled={!!addingUser}
/>
)}
<button
onClick={() => handleAddUser(user)}
disabled={!!addingUser}
className="px-3 py-1.5 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 text-xs font-medium whitespace-nowrap shadow-sm hover:shadow-md flex items-center gap-1.5"
>
{addingUser === user.username ? (
<>
<div className="animate-spin rounded-full h-3 w-3 border-b-2 border-primary-foreground"></div>
<span>Adding...</span>
</>
) : (
<>
<UserPlus size={14} />
<span>{showRoleSelector ? "Add" : addButtonText}</span>
</>
)}
</button>
</div>
</div>
<button
onClick={() => handleAddUser(user)}
disabled={!!addingUser}
className="ml-3 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 text-sm font-medium whitespace-nowrap flex-shrink-0 shadow-sm hover:shadow-md flex items-center gap-2"
>
{addingUser === user.username ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary-foreground"></div>
<span>Adding...</span>
</>
) : (
<>
<UserPlus size={16} />
<span className="hidden sm:inline">
{showRoleSelector
? `Add as ${selectedRole === "admin" ? "Admin" : "Member"}`
: addButtonText}
</span>
<span className="sm:hidden">Add</span>
</>
)}
</button>
</div>
))}
)
})}
</div>
) : (
<div className="text-center py-12">
@ -364,9 +318,7 @@ export const UserSearchModal: React.FC<UserSearchModalProps> = ({
</div>
<p className="text-foreground font-medium">Ready to add a member</p>
<p className="text-sm text-muted-foreground mt-1">
{showRoleSelector
? "Select a role and search for a GitHub user to add"
: "Search for a GitHub user to add"}
Search for a GitHub user to add to your organization
</p>
</div>
)}

View file

@ -18,6 +18,7 @@ import {
Lock,
Monitor,
Smartphone,
BarChart3,
} from "lucide-react"
// Ensure you have lucide-react installed as per your package.json
import { Loader2, FileText, AlertTriangle } from "lucide-react"
@ -300,24 +301,38 @@ const MonacoDiffViewer: React.FC<MonacoDiffViewerProps> = ({
{/* Header Section - Mobile Optimized */}
<div className="px-3 sm:px-4 md:px-6 py-2 sm:py-3 md:py-4 border-b border-slate-700/50 overflow-y-auto max-h-[40vh] md:max-h-none">
<div className="flex flex-col gap-3 sm:gap-4 md:gap-6">
{/* Top Row - Title and PR Link */}
{/* Top Row - Title with PR Link and Observability Link */}
<div className="flex items-center justify-between gap-2">
<h1 className="text-lg sm:text-xl md:text-2xl lg:text-3xl font-bold bg-gradient-to-r from-cyan-400 via-blue-500 to-purple-600 text-transparent bg-clip-text truncate flex-1">
CodeFlash Optimization
</h1>
{metadata.pullNumber && (
<a
href={`https://github.com/${metadata.owner || repoFullName.split("/")[0]}/${metadata.repo || repoFullName.split("/")[1]}/pull/${metadata.pullNumber}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-cyan-400 hover:text-cyan-300 transition-colors bg-slate-800/50 px-2 py-1 rounded-md text-xs sm:text-sm flex-shrink-0"
>
<GitPullRequest className="h-3 w-3 sm:h-4 sm:w-4" />
<span className="hidden sm:inline">PR #{metadata.pullNumber}</span>
<span className="sm:hidden">#{metadata.pullNumber}</span>
<ExternalLink className="h-2.5 w-2.5 sm:h-3 sm:w-3" />
</a>
)}
<div className="flex items-center gap-2 sm:gap-3 min-w-0 flex-1">
<h1 className="text-lg sm:text-xl md:text-2xl lg:text-3xl font-bold bg-gradient-to-r from-cyan-400 via-blue-500 to-purple-600 text-transparent bg-clip-text truncate">
CodeFlash Optimization
</h1>
{metadata.pullNumber && (
<a
href={`https://github.com/${metadata.owner || repoFullName.split("/")[0]}/${metadata.repo || repoFullName.split("/")[1]}/pull/${metadata.pullNumber}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-cyan-400 hover:text-cyan-300 transition-colors bg-slate-800/50 px-2 py-1 rounded-md text-xs sm:text-sm flex-shrink-0"
>
<GitPullRequest className="h-3 w-3 sm:h-4 sm:w-4" />
<span className="hidden sm:inline">PR #{metadata.pullNumber}</span>
<span className="sm:hidden">#{metadata.pullNumber}</span>
<ExternalLink className="h-2.5 w-2.5 sm:h-3 sm:w-3" />
</a>
)}
</div>
{/* Observability Link */}
<a
href={`/observability/trace/${traceId}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-cyan-400 hover:text-cyan-300 transition-colors bg-slate-800/50 px-2 py-1 rounded-md text-xs sm:text-sm flex-shrink-0"
>
<BarChart3 className="h-3 w-3 sm:h-4 sm:w-4" />
<span className="hidden sm:inline">Observability</span>
<span className="sm:hidden">Obs</span>
<ExternalLink className="h-2.5 w-2.5 sm:h-3 sm:w-3" />
</a>
</div>
{/* Info Row - Repository and Function (Compact on Mobile) */}

View file

@ -34,13 +34,10 @@ export function ReviewQualityBadge({
}: ReviewQualityBadgeProps) {
if (!quality) return null
const variant = REVIEW_QUALITY_VARIANTS[
quality.toLowerCase() as keyof typeof REVIEW_QUALITY_VARIANTS
] || {
className:
"bg-gradient-to-br from-gray-50 to-gray-100 text-gray-700 border-gray-300 dark:from-gray-900/50 dark:to-gray-800/50 dark:text-gray-300 dark:border-gray-600 shadow-sm",
label: quality,
}
const normalizedQuality = quality.toLowerCase() as keyof typeof REVIEW_QUALITY_VARIANTS
const variant = REVIEW_QUALITY_VARIANTS[normalizedQuality]
if (!variant) return null
const content = (
<>

View file

@ -7,8 +7,12 @@ import * as Sentry from "@sentry/nextjs"
// Required for Next.js router instrumentation
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart
const isProduction = process.env.NODE_ENV === "production"
Sentry.init({
dsn: "https://0fa0f40b2d709e4f1eb9aac76ff9e6be@o4506833230561280.ingest.us.sentry.io/4506833279582208",
dsn: isProduction
? "https://0fa0f40b2d709e4f1eb9aac76ff9e6be@o4506833230561280.ingest.us.sentry.io/4506833279582208"
: undefined,
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,

View file

@ -13,6 +13,7 @@ export async function trackUserLogin(userData: {
}) {
try {
const posthog = getPostHogClient()
if (!posthog) return
// Identify the user
posthog.identify({
@ -36,7 +37,7 @@ export async function trackUserLogin(userData: {
})
// Ensure events are sent
await posthog.shutdown()
await posthog?.shutdown()
console.log(`[Analytics] Tracked login for user ${userData.userId}`)
} catch (error) {

View file

@ -1,6 +1,9 @@
import { PostHog } from "posthog-node"
export default function PostHogClient(): PostHog {
export default function PostHogClient(): PostHog | undefined {
if (process.env.NODE_ENV !== "production") {
return undefined
}
return new PostHog("phc_aUO790jHd7z1SXwsYCz8dRApxueplZlZWeDSpKc5hol", {
host: "https://app.posthog.com",
flushAt: 1,