Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,17 @@
logger = logging.getLogger(__name__)

TRACE_METRICS_GUIDANCE = """When generating widgets with `widget_type: "tracemetrics"`:
- Aggregates use a multi-argument form: `func(attribute, metric_name, metric_type)`.
- Aggregates use a required 4-argument form: `func(attribute, metric_name, metric_type, metric_unit)`.
- `attribute` must be `value` (the numeric value of the metric); no other attributes are supported at this time.
- `metric_name` is the metric's name as ingested (e.g. `my.app.latency`).
- `metric_type` is exactly one of `counter`, `gauge`, or `distribution`.
- Examples: `count(value, my.app.requests, counter)`, `avg(value, my.app.cpu, gauge)`, `p95(value, my.app.latency, distribution)`.
- Single-argument forms like `p50(my.metric)` are INVALID for tracemetrics.
- Before emitting a tracemetrics widget you MUST look up the metric's `metric_type` using available tools (e.g. by querying the tracemetrics dataset for distinct `metric.name`/`metric.type` values, or fetching trace-item attributes). Do NOT guess the type — if you cannot confirm it, pick a different dataset or omit the widget."""
- `metric_unit` is the metric's unit as ingested (e.g. `milliseconds`, `bytes`). Use `none` only when the metric has no unit.
- Each `metric_type` only accepts a specific set of aggregate functions. Using a function not listed for the metric's type will fail:
- `counter`: `sum`, `per_second`, `per_minute`.
- `gauge`: `avg`, `min`, `max`, `per_second`, `per_minute`.
- `distribution`: `p50`, `p75`, `p90`, `p95`, `p99`, `avg`, `min`, `max`, `sum`, `count`, `per_second`, `per_minute`.
- Examples: `sum(value, my.app.requests, counter, none)`, `avg(value, my.app.cpu, gauge, percent)`, `p95(value, my.app.latency, distribution, milliseconds)`.
- Before emitting a tracemetrics widget you MUST look up the metric's `metric_type` AND `metric_unit` using available tools (e.g. by querying the tracemetrics dataset for distinct `metric.name`/`metric.type`/`metric.unit` values, or fetching trace-item attributes). Do NOT guess the type or unit — if you cannot confirm both, pick a different dataset or omit the widget."""

CREATE_ON_PAGE_CONTEXT = (
"The user is on the dashboard generation page. This session must ONLY generate a dashboard "
Expand Down
33 changes: 19 additions & 14 deletions src/sentry/dashboards/models/generate_dashboard_artifact.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,25 @@ class GeneratedWidgetQuery(BaseModel):
"values; for table widgets they become data columns alongside columns[]. Valid "
"aggregate function values vary by dataset type. Do not make up functions or use "
"unsupported functions.\n\n"
"For the 'tracemetrics' widget_type, aggregates have a SPECIAL multi-argument form: "
"`func(attribute, metric_name, metric_type)` where attribute must be `value` "
"(the numeric value of the metric; no other attributes are supported at this time), "
"metric_name is the metric's name as ingested, and "
"metric_type is one of 'counter', 'gauge', or 'distribution'. Examples: "
"`count(value, my.app.requests, counter)`, "
"`avg(value, my.app.cpu, gauge)`, "
"`p95(value, my.app.latency, distribution)`. "
"Allowed functions for tracemetrics: count, count_unique, sum, avg, max, min, "
"p50, p75, p90, p95, p99. The single-argument form like `p50(my.metric)` is INVALID "
"for tracemetrics — the metric_name and metric_type MUST be passed as separate "
"positional arguments. You MUST NOT guess the metric_name or metric_type; look them "
"up first using the available tools (e.g. by querying the tracemetrics dataset for "
"distinct `metric.name` and `metric.type` values, or fetching trace-item attributes)."
"For the 'tracemetrics' widget_type, aggregates use a required 4-argument form: "
"`func(attribute, metric_name, metric_type, metric_unit)` where attribute must be "
"`value` (the numeric value of the metric; no other attributes are supported at this "
"time), metric_name is the metric's name as ingested, metric_type is one of "
"'counter', 'gauge', or 'distribution', and metric_unit is the metric's unit as "
"ingested (e.g. 'milliseconds', 'bytes'); use 'none' only when the metric has no "
"unit. Examples: `sum(value, my.app.requests, counter, none)`, "
"`avg(value, my.app.cpu, gauge, percent)`, "
"`p95(value, my.app.latency, distribution, milliseconds)`. "
"Each metric_type only accepts a specific set of aggregate functions, and using a "
"function outside that set will fail:\n"
"- counter: sum, per_second, per_minute.\n"
"- gauge: avg, min, max, per_second, per_minute.\n"
"- distribution: p50, p75, p90, p95, p99, avg, min, max, sum, count, per_second, "
"per_minute.\n"
"You MUST NOT guess metric_name, metric_type, or metric_unit; look them up first "
"using the available tools (e.g. by querying the tracemetrics dataset for distinct "
"`metric.name`, `metric.type`, and `metric.unit` values, or fetching trace-item "
"attributes)."
),
)
columns: list[str] = Field(
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/features/temporary.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,8 @@ def register_temporary_features(manager: FeatureManager) -> None:
manager.add("organizations:seer-explorer-context-engine-fe-override-ui-flag", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable code editing tools in Seer Agent chat
manager.add("organizations:seer-explorer-chat-coding", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable sentry source code search tool
manager.add("organizations:seer-agent-source-code-search", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable code mode tools (sentry_api_search/execute) in Seer Agent
manager.add("organizations:seer-explorer-code-mode-tools", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable code mode tools for Slack-initiated Explorer sessions
Expand Down
14 changes: 14 additions & 0 deletions src/sentry/seer/agent/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,13 @@ def start_run(
):
chat_body["is_context_engine_enabled"] = override_ce_enable

if features.has(
"organizations:seer-agent-source-code-search",
self.organization,
actor=self.user,
):
chat_body["enable_frontend_code_search"] = True

if features.has("organizations:seer-run-mirror-explorer", self.organization):
user_id = (
self.user.id
Expand Down Expand Up @@ -542,6 +549,13 @@ def continue_run(
if _has_context_engine(self.organization, self.user):
chat_body["is_context_engine_enabled"] = True

if features.has(
"organizations:seer-agent-source-code-search",
self.organization,
actor=self.user,
):
chat_body["enable_frontend_code_search"] = True

response = make_agent_chat_request(chat_body, viewer_context=self.viewer_context)

if response.status >= 400:
Expand Down
1 change: 1 addition & 0 deletions src/sentry/seer/agent/client_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class AgentChatRequest(TypedDict):
category_value: NotRequired[str]
metadata: NotRequired[dict[str, Any]]
is_context_engine_enabled: NotRequired[bool]
enable_frontend_code_search: NotRequired[bool]
max_iterations: NotRequired[int]
proxy_headers: NotRequired[dict[str, str] | None]
ui_tools: NotRequired[str | None]
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/seer/endpoints/organization_seer_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
get_attributes_and_values,
get_attributes_for_span,
get_github_enterprise_integration_config,
get_organization_features,
get_organization_project_ids,
get_organization_slug,
has_repo_code_mappings,
Expand All @@ -87,6 +88,7 @@
# Common to Seer features
"get_organization_project_ids": map_org_id_param(get_organization_project_ids),
"get_organization_slug": map_org_id_param(get_organization_slug),
"get_organization_features": map_org_id_param(get_organization_features),
"validate_repo": validate_repo,
"get_github_enterprise_integration_config": get_github_enterprise_integration_config,
#
Expand Down
45 changes: 45 additions & 0 deletions src/sentry/seer/endpoints/seer_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from sentry_protos.snuba.v1.trace_item_attribute_pb2 import AttributeKey, AttributeValue, StrArray
from sentry_protos.snuba.v1.trace_item_filter_pb2 import ComparisonFilter, TraceItemFilter

from sentry import features
from sentry.api.api_owners import ApiOwner
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.authentication import AuthenticationSiloLimit, StandardAuthentication
Expand All @@ -44,6 +45,7 @@
from sentry.api.utils import get_date_range_from_params
from sentry.constants import ObjectStatus
from sentry.exceptions import InvalidSearchQuery
from sentry.features.base import OrganizationFeature
from sentry.hybridcloud.rpc.service import RpcAuthenticationSetupException, RpcResolutionException
from sentry.hybridcloud.rpc.sig import SerializableFunctionValueException
from sentry.integrations.github_enterprise.integration import GitHubEnterpriseIntegration
Expand Down Expand Up @@ -124,6 +126,7 @@
from sentry.sentry_apps.tasks.sentry_apps import broadcast_webhooks_for_organization
from sentry.silo.base import SiloMode
from sentry.snuba.referrer import Referrer
from sentry.users.services.user.service import user_service
from sentry.utils import snuba_rpc
from sentry.utils.env import in_test_environment
from sentry.utils.snuba_rpc import SnubaRPCRateLimitExceeded
Expand Down Expand Up @@ -317,6 +320,47 @@ def get_organization_project_ids(*, org_id: int) -> dict:
return {"projects": projects}


_ORGANIZATION_SCOPE_PREFIX = "organizations:"


def get_organization_features(*, org_id: int, user_id: int | None = None) -> dict[str, list[str]]:
try:
organization = Organization.objects.get(id=org_id)
except Organization.DoesNotExist:
return {"features": []}

actor = user_service.get_user(user_id=user_id) if user_id is not None else None

features_to_check = {
feature
for feature in features.all(feature_type=OrganizationFeature, api_expose_only=True).keys()
if feature.startswith(_ORGANIZATION_SCOPE_PREFIX)
}

feature_set: set[str] = set()

with sentry_sdk.start_span(op="features.check", name="check batch features"):
batch = features.batch_has(
list(features_to_check),
actor=actor,
organization=organization,
skip_experiment_exposure=True,
)

if batch:
for name, active in batch.get(f"organization:{organization.id}", {}).items():
if active:
feature_set.add(name[len(_ORGANIZATION_SCOPE_PREFIX) :])
features_to_check.discard(name)

with sentry_sdk.start_span(op="features.check", name="check individual features"):
for name in features_to_check:
if features.has(name, organization, actor=actor, skip_entity=True):
feature_set.add(name[len(_ORGANIZATION_SCOPE_PREFIX) :])

return {"features": list(sorted(feature_set))}


class SentryOrganizaionIdsAndSlugs(TypedDict):
org_ids: list[int]
org_slugs: list[str]
Expand Down Expand Up @@ -917,6 +961,7 @@ def bulk_get_project_preferences(
# Common to Seer features
"get_github_enterprise_integration_config": get_github_enterprise_integration_config,
"get_organization_project_ids": get_organization_project_ids,
"get_organization_features": get_organization_features,
"check_repository_integrations_status": check_repository_integrations_status,
"validate_repo": validate_repo,
"get_repo_installation_id": get_repo_installation_id,
Expand Down
5 changes: 5 additions & 0 deletions static/app/components/charts/baseChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,11 @@ const getTooltipStyles = (p: {theme: Theme}) => css`
justify-content: flex-start;
align-items: baseline;
}
.tooltip-label-centered {
display: flex;
justify-content: center;
align-items: center;
}
.tooltip-code-no-margin {
padding-left: 0;
margin-left: 0;
Expand Down
15 changes: 0 additions & 15 deletions static/app/components/charts/components/tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,21 +243,6 @@ export function getFormatter({
serie
);

if (serie.seriesType === 'heatmap') {
const zAxisCountValue = (getSeriesValue(serie, 2) ?? 0).toString();
const yAxisValue = valueFormatter(
getSeriesValue(serie, 1),
serie.seriesName,
serie
);

acc.series.push(
`<div><span class="tooltip-label"><strong>${yAxisValue}</strong></span> ${zAxisCountValue}</div>`
);

return acc;
}

const value = valueFormatter(getSeriesValue(serie, 1), serie.seriesName, serie);

const marker = markerFormatter(serie.marker ?? '', serie.seriesName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,14 +333,12 @@ export function GlobalCommandPaletteActions() {
to={`${prefix}/issues/views/${starredView.id}/`}
/>
))}
{organization.features.includes('autofix-on-explorer') && (
<CMDKAction display={{label: t('Autofix')}}>
<CMDKAction
display={{label: t('Recently Run')}}
to={`${prefix}/issues/autofix/recent/`}
/>
</CMDKAction>
)}
<CMDKAction display={{label: t('Autofix')}}>
<CMDKAction
display={{label: t('Recently Run')}}
to={`${prefix}/issues/autofix/recent/`}
/>
</CMDKAction>
</CMDKAction>

<CMDKAction display={{label: t('Explore'), icon: <IconCompass />}} limit={4}>
Expand Down
Loading
Loading