Merge branch 'main' into remove-print-messages
This commit is contained in:
commit
9ea1afbdbf
50 changed files with 971 additions and 404 deletions
|
|
@ -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 = {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 [];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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 \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 \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 \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 \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 \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 \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 \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 \n\n',
|
||||
)
|
||||
// buildBenchmarkInfo should NOT be called when benchmark_details is null
|
||||
expect(mockBuilder.buildBenchmarkInfo).not.toHaveBeenCalled()
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ appExpress.listen(Number(port), () => {
|
|||
})
|
||||
})
|
||||
|
||||
posthog.shutdown()
|
||||
posthog?.shutdown()
|
||||
|
||||
// Handle unhandled promise rejections and uncaught exceptions
|
||||
process.on("unhandledRejection", GlobalErrorHandler.handleUnhandledRejection)
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)) || {
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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) */}
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue