Skip to content

Commit

Permalink
Merge branch 'master' into jb/core/imports-alertlink
Browse files Browse the repository at this point in the history
  • Loading branch information
JonasBa committed Feb 25, 2025
2 parents 51963c3 + ab65e3e commit 1631a44
Show file tree
Hide file tree
Showing 117 changed files with 2,360 additions and 788 deletions.
2 changes: 1 addition & 1 deletion migrations_lockfile.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ sentry: 0835_add_schema_version_to_grouphash_metadata

social_auth: 0002_default_auto_field

tempest: 0001_create_tempest_credentials_model
tempest: 0002_make_message_type_nullable

uptime: 0026_region_mode_col

Expand Down
7 changes: 1 addition & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,9 @@ module = [
"sentry.db.mixin",
"sentry.db.postgres.base",
"sentry.eventstore.models",
"sentry.identity.bitbucket.provider",
"sentry.identity.gitlab.provider",
"sentry.identity.oauth2",
"sentry.identity.pipeline",
"sentry.identity.providers.dummy",
"sentry.incidents.endpoints.bases",
"sentry.incidents.endpoints.organization_alert_rule_details",
"sentry.incidents.endpoints.organization_alert_rule_index",
Expand All @@ -158,7 +156,6 @@ module = [
"sentry.integrations.github.issues",
"sentry.integrations.github_enterprise.integration",
"sentry.integrations.gitlab.client",
"sentry.integrations.gitlab.integration",
"sentry.integrations.gitlab.issues",
"sentry.integrations.jira.client",
"sentry.integrations.jira.integration",
Expand Down Expand Up @@ -192,9 +189,6 @@ module = [
"sentry.net.http",
"sentry.net.socket",
"sentry.notifications.notifications.activity.base",
"sentry.pipeline.base",
"sentry.pipeline.views.base",
"sentry.pipeline.views.nested",
"sentry.plugins.config",
"sentry.release_health.metrics_sessions_v2",
"sentry.rules.actions.integrations.create_ticket.utils",
Expand Down Expand Up @@ -402,6 +396,7 @@ module = [
"sentry.options.rollout",
"sentry.organizations.*",
"sentry.ownership.*",
"sentry.pipeline.*",
"sentry.plugins.base.response",
"sentry.plugins.base.view",
"sentry.plugins.validators.*",
Expand Down
102 changes: 44 additions & 58 deletions src/sentry/api/endpoints/organization_spans_trace.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import TypedDict
from datetime import datetime
from typing import Any, TypedDict

import sentry_sdk
from django.http import HttpResponse
Expand All @@ -12,20 +13,24 @@
from sentry.api.paginator import GenericOffsetPaginator
from sentry.api.utils import handle_query_errors, update_snuba_params_with_timestamp
from sentry.models.organization import Organization
from sentry.search.events.builder.spans_indexed import SpansIndexedQueryBuilder
from sentry.search.eap.types import SearchResolverConfig
from sentry.search.events.types import SnubaParams
from sentry.snuba.dataset import Dataset
from sentry.snuba.referrer import Referrer
from sentry.utils.validators import INVALID_ID_DETAILS, is_event_id
from sentry.snuba.spans_rpc import run_trace_query


class SnubaTrace(TypedDict):
span_op: str
span_description: str
id: str
class SerializedEvent(TypedDict):
children: list["SerializedEvent"]
event_id: str
parent_span_id: str | None
project_id: int
project_slug: str
start_timestamp: datetime | None
end_timestamp: datetime | None
transaction: str
description: str
duration: float
is_transaction: bool
start_ts: float
finish_ts: float


@region_silo_endpoint
Expand All @@ -34,46 +39,35 @@ class OrganizationSpansTraceEndpoint(OrganizationEventsV2EndpointBase):
"GET": ApiPublishStatus.PRIVATE,
}

@sentry_sdk.tracing.trace
def query_trace_data(self, snuba_params: SnubaParams, trace_id: str) -> list[SnubaTrace]:
builder = SpansIndexedQueryBuilder(
Dataset.SpansIndexed,
params={},
snuba_params=snuba_params,
query=f"trace:{trace_id}",
selected_columns=[
"span.op",
"span.description",
"id",
"is_transaction",
"precise.start_ts",
"precise.finish_ts",
],
orderby="precise.start_ts",
limit=10_000,
def serialize_rpc_span(self, span: dict[str, Any]) -> SerializedEvent:
return SerializedEvent(
children=[self.serialize_rpc_span(child) for child in span["children"]],
event_id=span["id"],
project_id=span["project.id"],
project_slug=span["project.slug"],
parent_span_id=None if span["parent_span"] == "0" * 16 else span["parent_span"],
start_timestamp=span["precise.start_ts"],
end_timestamp=span["precise.finish_ts"],
duration=span["span.duration"],
transaction=span["transaction"],
is_transaction=span["is_transaction"],
description=span["description"],
)
query_result = builder.run_query(referrer=Referrer.API_SPANS_TRACE_VIEW.value)
result: list[SnubaTrace] = []
measurement_transaction_count = 0
for row in query_result["data"]:
result.append(
{
"span_op": row["span.op"],
"span_description": row["span.description"],
"id": row["id"],
"is_transaction": row["is_transaction"] == 1,
"start_ts": row["precise.start_ts"],
"finish_ts": row["precise.finish_ts"],
}
)
if row["is_transaction"] == 1:
measurement_transaction_count += 1
sentry_sdk.set_measurement("spans_trace.count.transactions", measurement_transaction_count)
sentry_sdk.set_measurement("spans_trace.count.spans", len(result))
sentry_sdk.set_measurement(
"spans_trace.count.non_transactions", len(result) - measurement_transaction_count

@sentry_sdk.tracing.trace
def query_trace_data(self, snuba_params: SnubaParams, trace_id: str) -> list[SerializedEvent]:
trace_data = run_trace_query(
trace_id, snuba_params, Referrer.API_TRACE_VIEW_GET_EVENTS.value, SearchResolverConfig()
)
return result
result = []
id_to_event = {event["id"]: event for event in trace_data}
for span in trace_data:
if span["parent_span"] in id_to_event:
parent = id_to_event[span["parent_span"]]
parent["children"].append(span)
else:
result.append(span)
return [self.serialize_rpc_span(root) for root in result]

def has_feature(self, organization: Organization, request: Request) -> bool:
return bool(
Expand All @@ -92,16 +86,8 @@ def get(self, request: Request, organization: Organization, trace_id: str) -> Ht

update_snuba_params_with_timestamp(request, snuba_params)

# Bias the results to include any given event_id - note because this loads spans without taking trace topology
# into account, the descendents of this event might not be in the response
event_id = request.GET.get("event_id") or request.GET.get("eventId")

# Only need to validate event_id as trace_id is validated in the URL
if event_id and not is_event_id(event_id):
return Response({"detail": INVALID_ID_DETAILS.format("Event ID")}, status=400)

def data_fn(offset: int, limit: int) -> list[SnubaTrace]:
"""API requires pagination even though it doesn't really work yet... ignoring limit and offset for now"""
def data_fn(offset: int, limit: int) -> list[SerializedEvent]:
"""offset and limit don't mean anything on this endpoint currently"""
with handle_query_errors():
spans = self.query_trace_data(snuba_params, trace_id)
return spans
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/api/event_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -916,7 +916,7 @@ def visit_aggregate_duration_filter(self, node, children):
try:
# Even if the search value matches duration format, only act as
# duration for certain columns
result_type = self.builder.get_function_result_type(search_key.name)
result_type = self.get_function_result_type(search_key.name)

if result_type == "duration" or result_type in DURATION_UNITS:
aggregate_value = parse_duration(*search_value)
Expand Down Expand Up @@ -957,7 +957,7 @@ def visit_aggregate_percentage_filter(self, node, children):
try:
# Even if the search value matches percentage format, only act as
# percentage for certain columns
result_type = self.builder.get_function_result_type(search_key.name)
result_type = self.get_function_result_type(search_key.name)
if result_type == "percentage":
aggregate_value = parse_percentage(search_value)
except ValueError:
Expand Down
6 changes: 4 additions & 2 deletions src/sentry/auth/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -729,11 +729,13 @@ def __init__(
self.organization: RpcOrganization = self.organization
self.provider: Provider = self.provider

def get_provider(self, provider_key: str | None, **kwargs) -> PipelineProvider:
def get_provider(
self, provider_key: str | None, *, organization: RpcOrganization | None
) -> PipelineProvider:
if self.provider_model:
return cast(PipelineProvider, self.provider_model.get_provider())
elif provider_key:
return super().get_provider(provider_key)
return super().get_provider(provider_key, organization=organization)
else:
raise NotImplementedError

Expand Down
4 changes: 4 additions & 0 deletions src/sentry/features/temporary.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,10 @@ def register_temporary_features(manager: FeatureManager):
manager.add("organizations:trace-view-quota-exceeded-banner", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable feature to use span only trace endpoint.
manager.add("organizations:trace-spans-format", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
# Enable feature to load new traces onboarding guide.
manager.add("organizations:traces-onboarding-guide", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable feature to load traces schema hints.
manager.add("organizations:traces-schema-hints", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Extraction metrics for transactions during ingestion.
manager.add("organizations:transaction-metrics-extraction", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, api_expose=False)
# Mark URL transactions scrubbed by regex patterns as "sanitized".
Expand Down
10 changes: 4 additions & 6 deletions src/sentry/identity/bitbucket/provider.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from django.http import HttpResponse, HttpResponseRedirect
from django.http.request import HttpRequest
from django.http.response import HttpResponseBase, HttpResponseRedirect

from sentry.identity.base import Provider
from sentry.pipeline import PipelineView
from sentry.pipeline import Pipeline, PipelineView
from sentry.utils.http import absolute_uri


Expand All @@ -13,11 +14,8 @@ def get_pipeline_views(self) -> list[PipelineView]:
return [BitbucketLoginView()]


from rest_framework.request import Request


class BitbucketLoginView(PipelineView):
def dispatch(self, request: Request, pipeline) -> HttpResponse:
def dispatch(self, request: HttpRequest, pipeline: Pipeline) -> HttpResponseBase:
from sentry.integrations.base import IntegrationDomain
from sentry.integrations.utils.metrics import (
IntegrationPipelineViewEvent,
Expand Down
13 changes: 6 additions & 7 deletions src/sentry/identity/oauth2.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
from urllib.parse import parse_qsl, urlencode

import orjson
from django.http import HttpResponse, HttpResponseRedirect
from django.http import HttpResponseRedirect
from django.http.request import HttpRequest
from django.http.response import HttpResponseBase
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
Expand Down Expand Up @@ -225,9 +227,6 @@ def refresh_identity(self, identity: Identity, **kwargs: Any) -> None:
identity.update(data=identity.data)


from rest_framework.request import Request


def record_event(event: IntegrationPipelineViewType, provider: str):
from sentry.identity import default_manager as identity_manager

Expand Down Expand Up @@ -271,7 +270,7 @@ def get_authorize_params(self, state, redirect_uri):
}

@method_decorator(csrf_exempt)
def dispatch(self, request: Request, pipeline) -> HttpResponse:
def dispatch(self, request: HttpRequest, pipeline: Pipeline) -> HttpResponseBase:
with record_event(IntegrationPipelineViewType.OAUTH_LOGIN, pipeline.provider.key).capture():
for param in ("code", "error", "state"):
if param in request.GET:
Expand Down Expand Up @@ -314,7 +313,7 @@ def get_token_params(self, code, redirect_uri):
"client_secret": self.client_secret,
}

def exchange_token(self, request: Request, pipeline, code):
def exchange_token(self, request: HttpRequest, pipeline: Pipeline, code: str) -> dict[str, str]:
with record_event(
IntegrationPipelineViewType.TOKEN_EXCHANGE, pipeline.provider.key
).capture() as lifecycle:
Expand Down Expand Up @@ -358,7 +357,7 @@ def exchange_token(self, request: Request, pipeline, code):
"error_description": "We were not able to parse a JSON response, please try again.",
}

def dispatch(self, request: Request, pipeline) -> HttpResponse:
def dispatch(self, request: HttpRequest, pipeline: Pipeline) -> HttpResponseBase:
with record_event(
IntegrationPipelineViewType.OAUTH_CALLBACK, pipeline.provider.key
).capture() as lifecycle:
Expand Down
9 changes: 4 additions & 5 deletions src/sentry/identity/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
IntegrationPipelineViewEvent,
IntegrationPipelineViewType,
)
from sentry.models.organization import Organization
from sentry.organizations.services.organization.model import RpcOrganization
from sentry.pipeline import Pipeline, PipelineProvider
from sentry.users.models.identity import Identity, IdentityProvider
Expand All @@ -26,9 +25,9 @@ class IdentityProviderPipeline(Pipeline):
provider_model_cls = IdentityProvider

# TODO(iamrajjoshi): Delete this after Azure DevOps migration is complete
def get_provider(self, provider_key: str, **kwargs) -> PipelineProvider:
if kwargs.get("organization"):
organization: Organization | RpcOrganization = kwargs["organization"]
def get_provider(
self, provider_key: str, *, organization: RpcOrganization | None
) -> PipelineProvider:
if provider_key == "vsts" and features.has(
"organizations:migrate-azure-devops-integration", organization
):
Expand All @@ -37,7 +36,7 @@ def get_provider(self, provider_key: str, **kwargs) -> PipelineProvider:
if provider_key == "vsts_login" and options.get("vsts.social-auth-migration"):
provider_key = "vsts_login_new"

return super().get_provider(provider_key)
return super().get_provider(provider_key, organization=organization)

def finish_pipeline(self):
with IntegrationPipelineViewEvent(
Expand Down
7 changes: 4 additions & 3 deletions src/sentry/identity/providers/dummy.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
from typing import Any

from django.http import HttpResponse
from rest_framework.request import Request
from django.http.request import HttpRequest
from django.http.response import HttpResponseBase

from sentry.identity.base import Provider
from sentry.pipeline import PipelineView
from sentry.pipeline import Pipeline, PipelineView
from sentry.users.models.identity import Identity

__all__ = ("DummyProvider",)


class AskEmail(PipelineView):
def dispatch(self, request: Request, pipeline) -> HttpResponse:
def dispatch(self, request: HttpRequest, pipeline: Pipeline) -> HttpResponseBase:
if "email" in request.POST:
pipeline.bind_state("email", request.POST.get("email"))
return pipeline.next_step()
Expand Down
7 changes: 4 additions & 3 deletions src/sentry/identity/vsts/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@

import orjson
from django.core.exceptions import PermissionDenied
from rest_framework.request import Request
from django.http.request import HttpRequest

from sentry import http, options
from sentry.identity.oauth2 import OAuth2CallbackView, OAuth2LoginView, OAuth2Provider, record_event
from sentry.integrations.utils.metrics import IntegrationPipelineViewType
from sentry.pipeline.base import Pipeline
from sentry.pipeline.views.base import PipelineView
from sentry.users.models.identity import Identity
from sentry.utils.http import absolute_uri
Expand Down Expand Up @@ -123,7 +124,7 @@ def build_identity(self, data):


class VSTSOAuth2CallbackView(OAuth2CallbackView):
def exchange_token(self, request: Request, pipeline, code):
def exchange_token(self, request: HttpRequest, pipeline: Pipeline, code: str) -> dict[str, str]:
from sentry.http import safe_urlopen, safe_urlread

with record_event(
Expand Down Expand Up @@ -240,7 +241,7 @@ def get_authorize_params(self, state, redirect_uri):


class VSTSNewOAuth2CallbackView(OAuth2CallbackView):
def exchange_token(self, request: Request, pipeline, code):
def exchange_token(self, request: HttpRequest, pipeline: Pipeline, code: str) -> dict[str, str]:
from urllib.parse import parse_qsl

from sentry.http import safe_urlopen, safe_urlread
Expand Down
Loading

0 comments on commit 1631a44

Please sign in to comment.