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
2 changes: 2 additions & 0 deletions src/sentry/dynamic_sampling/per_org/tasks/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def get_configuration(organization_id: int) -> BaseDynamicSamplingConfiguration:

class BaseDynamicSamplingConfiguration(ABC):
measure: SamplingMeasure
should_balance_projects: bool = True
projects: list[Project]

def __init__(self, organization: Organization) -> None:
Expand Down Expand Up @@ -123,6 +124,7 @@ def is_enabled(self) -> bool:

class CustomDynamicSamplingProjectConfiguration(BaseDynamicSamplingConfiguration):
project_target_sample_rates: ProjectTargetSampleRates
should_balance_projects: bool = False

def __init__(self, organization: Organization) -> None:
super().__init__(organization)
Expand Down
81 changes: 77 additions & 4 deletions src/sentry/dynamic_sampling/per_org/tasks/queries.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from __future__ import annotations

from collections.abc import Iterator, Mapping
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from enum import StrEnum
from typing import Any

from sentry_protos.snuba.v1.trace_item_attribute_pb2 import ExtrapolationMode

from sentry.dynamic_sampling.per_org.tasks.configuration import BaseDynamicSamplingConfiguration
from sentry.dynamic_sampling.rules.utils import ProjectId
from sentry.dynamic_sampling.tasks.common import (
ACTIVE_ORGS_VOLUMES_DEFAULT_TIME_INTERVAL,
OrganizationDataVolume,
Expand All @@ -18,6 +21,24 @@
from sentry.snuba.spans_rpc import Spans


class DynamicSamplingQueryFilters(StrEnum):
IS_SEGMENT = "sentry.is_segment:true"


class DynamicSamplingQueryFields(StrEnum):
ROOT_PROJECT = "sentry.dsc.root_project"
COUNT = "count()"
COUNT_SAMPLE = "count_sample()"


@dataclass(order=True)
class ProjectVolume:
project_id: ProjectId
total: int
keep: int
drop: int


def _get_aggregate_int(row: Mapping[str, Any], column: str) -> int:
return int(row.get(column, 0))

Expand Down Expand Up @@ -58,8 +79,11 @@ def get_eap_organization_volume(
projects=config.projects,
organization=config.organization,
),
query_string="is_transaction:true",
selected_columns=["count()", "count_sample()"],
query_string=DynamicSamplingQueryFilters.IS_SEGMENT,
selected_columns=[
DynamicSamplingQueryFields.COUNT,
DynamicSamplingQueryFields.COUNT_SAMPLE,
],
orderby=None,
offset=0,
limit=1,
Expand All @@ -76,9 +100,58 @@ def get_eap_organization_volume(
return None

row = data[0]
total = _get_aggregate_int(row, "count()")
total = _get_aggregate_int(row, DynamicSamplingQueryFields.COUNT)
if total <= 0:
return None
indexed = _get_aggregate_int(row, "count_sample()")
indexed = _get_aggregate_int(row, DynamicSamplingQueryFields.COUNT_SAMPLE)

return OrganizationDataVolume(org_id=config.organization.id, total=total, indexed=indexed)


def get_eap_project_volumes(
config: BaseDynamicSamplingConfiguration,
time_interval: timedelta = timedelta(hours=1),
) -> list[ProjectVolume]:
end_time = datetime.now(UTC)
start_time = end_time - time_interval
project_volumes: list[ProjectVolume] = []

for row in run_eap_spans_table_query_in_chunks(
{
"params": SnubaParams(
start=start_time,
end=end_time,
projects=config.projects,
organization=config.organization,
),
"query_string": DynamicSamplingQueryFilters.IS_SEGMENT,
"selected_columns": [
DynamicSamplingQueryFields.ROOT_PROJECT,
DynamicSamplingQueryFields.COUNT,
DynamicSamplingQueryFields.COUNT_SAMPLE,
],
"orderby": [DynamicSamplingQueryFields.ROOT_PROJECT],
"referrer": Referrer.DYNAMIC_SAMPLING_PER_ORG_GET_EAP_PROJECT_VOLUMES.value,
"config": SearchResolverConfig(
auto_fields=True,
extrapolation_mode=ExtrapolationMode.EXTRAPOLATION_MODE_SERVER_ONLY,
),
"sampling_mode": SAMPLING_MODE_HIGHEST_ACCURACY,
}
):
total = _get_aggregate_int(row, DynamicSamplingQueryFields.COUNT)
keep = _get_aggregate_int(row, DynamicSamplingQueryFields.COUNT_SAMPLE)
root_project = row.get(DynamicSamplingQueryFields.ROOT_PROJECT)
if root_project is None:
continue

project_volumes.append(
ProjectVolume(
project_id=ProjectId(int(root_project)),
total=total,
keep=keep,
drop=max(total - keep, 0),
)
)

return project_volumes
12 changes: 10 additions & 2 deletions src/sentry/dynamic_sampling/per_org/tasks/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@

from sentry.dynamic_sampling.per_org.tasks.configuration import get_configuration
from sentry.dynamic_sampling.per_org.tasks.gate import is_org_in_rollout
from sentry.dynamic_sampling.per_org.tasks.queries import get_eap_organization_volume
from sentry.dynamic_sampling.per_org.tasks.queries import (
get_eap_organization_volume,
get_eap_project_volumes,
)
from sentry.dynamic_sampling.per_org.tasks.telemetry import (
SCHEDULER_BUCKET_ORG_STATUS_METRIC,
DynamicSamplingStatus,
Expand Down Expand Up @@ -106,6 +109,11 @@ def run_calculations_per_org_task(org_id: OrganizationId) -> DynamicSamplingStat

org_volume = get_eap_organization_volume(config)
if org_volume is None:
return DynamicSamplingStatus.NO_VOLUME
return DynamicSamplingStatus.NO_ORG_VOLUME

if config.should_balance_projects:
project_volumes = get_eap_project_volumes(config)
if not project_volumes:
return DynamicSamplingStatus.NO_PROJECT_VOLUMES

return None
3 changes: 2 additions & 1 deletion src/sentry/dynamic_sampling/per_org/tasks/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ class DynamicSamplingStatus(StrEnum):
FAILED = "failed"
KILLSWITCHED = "killswitched"
NO_SUBSCRIPTION = "no_subscription"
NO_VOLUME = "no_volume"
NO_ORG_VOLUME = "no_org_volume"
NO_PROJECT_VOLUMES = "no_project_volumes"
NOT_IN_ROLLOUT = "not_in_rollout"
ORG_HAS_NO_DYNAMIC_SAMPLING = "org_has_no_dynamic_sampling"
ORG_HAS_NO_PROJECTS = "org_has_no_projects"
Expand Down
3 changes: 3 additions & 0 deletions src/sentry/snuba/referrer.py
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,9 @@ class Referrer(StrEnum):
"dynamic_sampling.distribution.fetch_projects_with_count_per_root_total_volumes"
)
DYNAMIC_SAMPLING_PER_ORG_GET_EAP_ORG_VOLUME = "dynamic_sampling.per_org.get_eap_org_volume"
DYNAMIC_SAMPLING_PER_ORG_GET_EAP_PROJECT_VOLUMES = (
"dynamic_sampling.per_org.get_eap_project_volumes"
)
DYNAMIC_SAMPLING_COUNTERS_FETCH_PROJECTS_WITH_COUNT_PER_TRANSACTION = (
"dynamic_sampling.counters.fetch_projects_with_count_per_transaction_volumes"
)
Expand Down
15 changes: 2 additions & 13 deletions src/sentry/utils/sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import asyncio
import copy
import logging
import re
import sys
import typing
from collections.abc import Generator, Mapping, Sequence, Sized
Expand Down Expand Up @@ -93,12 +92,6 @@
"/api/0/auth/validate/": 0.0,
}

# List of (compiled_pattern, sample_rate) tried in order when no exact route matches.
SAMPLED_ROUTE_PATTERNS: list[tuple[re.Pattern[str], float]] = [
# Temporary: monitoring AI Conversations rollout
(re.compile(r"^/api/0/organizations/[^/]+/ai-conversations/"), 1.0),
]

if settings.ADDITIONAL_SAMPLED_TASKS:
SAMPLED_TASKS.update(settings.ADDITIONAL_SAMPLED_TASKS)

Expand Down Expand Up @@ -189,12 +182,8 @@ def get_project_key():

def traces_sampler(sampling_context):
wsgi_path = sampling_context.get("wsgi_environ", {}).get("PATH_INFO")
if wsgi_path:
if wsgi_path in SAMPLED_ROUTES:
return SAMPLED_ROUTES[wsgi_path]
for pattern, rate in SAMPLED_ROUTE_PATTERNS:
if pattern.search(wsgi_path):
return rate
if wsgi_path and wsgi_path in SAMPLED_ROUTES:
return SAMPLED_ROUTES[wsgi_path]

# Apply sample_rate from custom_sampling_context
custom_sample_rate = sampling_context.get("sample_rate")
Expand Down
4 changes: 4 additions & 0 deletions static/app/components/tables/simpleTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ const ColumnHeaderCell = styled('div')<{isSorted?: boolean}>`
gap: ${p => p.theme.space.md};
height: 100%;

&:focus-visible {
box-shadow: inset 0 0 0 2px ${p => p.theme.tokens.focus.default};
}

&:first-child {
${HeaderDivider} {
display: none;
Expand Down
7 changes: 7 additions & 0 deletions static/app/components/webAuthn/handlers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ function isAssertion(r: AuthenticatorResponse): r is AuthenticatorAssertionRespo
* Register a new credential using WebAuthn (FIDO2) and return its attestation data.
*/
export async function handleEnroll(challengeData: ChallengeData) {
if (!challengeData.webAuthnRegisterData) {
return null;
}

const binaryChallenge = base64urlToUint8(challengeData.webAuthnRegisterData);
const {publicKey}: CredentialCreationOptions = decode(binaryChallenge);
const credential = await navigator.credentials.create({publicKey});
Expand Down Expand Up @@ -69,6 +73,9 @@ export async function handleEnroll(challengeData: ChallengeData) {
* Perform a WebAuthn assertion (login) using an existing credential.
*/
export async function handleSign(challengeData: ChallengeData) {
if (!challengeData.webAuthnAuthenticationData) {
return null;
}
const binaryChallenge = base64urlToUint8(challengeData.webAuthnAuthenticationData);
const options = decode<PublicKeyCredentialRequestOptions>(binaryChallenge);
const credential = await navigator.credentials.get({publicKey: options});
Expand Down
4 changes: 2 additions & 2 deletions static/app/types/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,9 @@ export type ChallengeData = {
authenticateRequests: SignRequest;
registerRequests: RegisterRequest;
registeredKeys: RegisteredKey[];
webAuthnAuthenticationData: string;
webAuthnAuthenticationData: string | undefined;
// for WebAuthn register
webAuthnRegisterData: string;
webAuthnRegisterData: string | undefined;
};

type EnrolledAuthenticator = {
Expand Down
2 changes: 2 additions & 0 deletions static/app/utils/analytics/conversationsAnalyticsEvents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ type ConversationOpenSource = 'table_conversation_id' | 'table_input' | 'table_o
export type ConversationsEventParameters = {
'conversations.detail.click-errors-link': Record<string, unknown>;
'conversations.detail.click-trace-link': Record<string, unknown>;
'conversations.detail.copy-conversation': Record<string, unknown>;
'conversations.detail.copy-conversation-id': Record<string, unknown>;
'conversations.detail.page-view': Record<string, unknown>;
'conversations.detail.select-span': Record<string, unknown>;
Expand All @@ -28,6 +29,7 @@ export const conversationsEventMap: Record<keyof ConversationsEventParameters, s
'conversations.detail.page-view': 'Conversations: Detail Page View',
'conversations.detail.tab-switch': 'Conversations: Detail Tab Switch',
'conversations.detail.select-span': 'Conversations: Detail Select Span',
'conversations.detail.copy-conversation': 'Conversations: Detail Copy Conversation',
'conversations.detail.copy-conversation-id':
'Conversations: Detail Copy Conversation ID',
'conversations.detail.click-trace-link': 'Conversations: Detail Click Trace Link',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {type PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConf
import {DETAILS_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/webVitals/settings';
import {ISSUE_TYPES} from 'sentry/views/dashboards/utils/prebuiltConfigs/webVitals/webVitals';
import {DEFAULT_QUERY_FILTER} from 'sentry/views/insights/browser/webVitals/settings';
import {ModuleName} from 'sentry/views/insights/types';
import {ModuleName, SpanFields} from 'sentry/views/insights/types';

export const WEB_VITALS_DETAILS_PREBUILT_CONFIG: PrebuiltDashboard = {
dateCreated: '',
Expand Down Expand Up @@ -117,8 +117,8 @@ export const WEB_VITALS_DETAILS_PREBUILT_CONFIG: PrebuiltDashboard = {
{
name: '',
conditions: DEFAULT_QUERY_FILTER,
fields: ['p75(measurements.lcp)'],
aggregates: ['p75(measurements.lcp)'],
fields: [`p75(${SpanFields.BROWSER_WEB_VITAL_LCP_VALUE})`],
aggregates: [`p75(${SpanFields.BROWSER_WEB_VITAL_LCP_VALUE})`],
columns: [],
orderby: '',
slideOutId: SlideoutId.LCP_SUMMARY,
Expand Down Expand Up @@ -152,8 +152,8 @@ export const WEB_VITALS_DETAILS_PREBUILT_CONFIG: PrebuiltDashboard = {
{
name: '',
conditions: DEFAULT_QUERY_FILTER,
fields: ['p75(measurements.inp)'],
aggregates: ['p75(measurements.inp)'],
fields: [`p75(${SpanFields.BROWSER_WEB_VITAL_INP_VALUE})`],
aggregates: [`p75(${SpanFields.BROWSER_WEB_VITAL_INP_VALUE})`],
columns: [],
orderby: '',
slideOutId: SlideoutId.INP_SUMMARY,
Expand Down Expand Up @@ -187,8 +187,8 @@ export const WEB_VITALS_DETAILS_PREBUILT_CONFIG: PrebuiltDashboard = {
{
name: '',
conditions: DEFAULT_QUERY_FILTER,
fields: ['p75(measurements.cls)'],
aggregates: ['p75(measurements.cls)'],
fields: [`p75(${SpanFields.BROWSER_WEB_VITAL_CLS_VALUE})`],
aggregates: [`p75(${SpanFields.BROWSER_WEB_VITAL_CLS_VALUE})`],
columns: [],
orderby: '',
slideOutId: SlideoutId.CLS_SUMMARY,
Expand Down Expand Up @@ -222,8 +222,8 @@ export const WEB_VITALS_DETAILS_PREBUILT_CONFIG: PrebuiltDashboard = {
{
name: '',
conditions: DEFAULT_QUERY_FILTER,
fields: ['p75(measurements.ttfb)'],
aggregates: ['p75(measurements.ttfb)'],
fields: [`p75(${SpanFields.BROWSER_WEB_VITAL_TTFB_VALUE})`],
aggregates: [`p75(${SpanFields.BROWSER_WEB_VITAL_TTFB_VALUE})`],
columns: [],
orderby: '',
slideOutId: SlideoutId.TTFB_SUMMARY,
Expand Down Expand Up @@ -279,12 +279,12 @@ export const WEB_VITALS_DETAILS_PREBUILT_CONFIG: PrebuiltDashboard = {
queries: [
{
name: '',
conditions: 'has:measurements.lcp',
conditions: `has:${SpanFields.BROWSER_WEB_VITAL_LCP_VALUE}`,
fields: [
'project',
'trace',
'lcp.element',
'measurements.lcp',
SpanFields.BROWSER_WEB_VITAL_LCP_ELEMENT,
SpanFields.BROWSER_WEB_VITAL_LCP_VALUE,
'profile.id',
'replay.id',
'measurements.score.ratio.lcp',
Expand All @@ -294,8 +294,8 @@ export const WEB_VITALS_DETAILS_PREBUILT_CONFIG: PrebuiltDashboard = {
columns: [
'project',
'trace',
'lcp.element',
'measurements.lcp',
SpanFields.BROWSER_WEB_VITAL_LCP_ELEMENT,
SpanFields.BROWSER_WEB_VITAL_LCP_VALUE,
'profile.id',
'replay.id',
'measurements.score.ratio.lcp',
Expand All @@ -321,11 +321,11 @@ export const WEB_VITALS_DETAILS_PREBUILT_CONFIG: PrebuiltDashboard = {
queries: [
{
name: '',
conditions: 'has:measurements.inp',
conditions: `has:${SpanFields.BROWSER_WEB_VITAL_INP_VALUE}`,
fields: [
'project',
'trace',
'measurements.inp',
SpanFields.BROWSER_WEB_VITAL_INP_VALUE,
'profile.id',
'replay.id',
'measurements.score.ratio.inp',
Expand All @@ -335,7 +335,7 @@ export const WEB_VITALS_DETAILS_PREBUILT_CONFIG: PrebuiltDashboard = {
columns: [
'project',
'trace',
'measurements.inp',
SpanFields.BROWSER_WEB_VITAL_INP_VALUE,
'profile.id',
'replay.id',
'measurements.score.ratio.inp',
Expand All @@ -361,11 +361,11 @@ export const WEB_VITALS_DETAILS_PREBUILT_CONFIG: PrebuiltDashboard = {
queries: [
{
name: '',
conditions: 'has:measurements.cls',
conditions: `has:${SpanFields.BROWSER_WEB_VITAL_CLS_VALUE}`,
fields: [
'project',
'trace',
'measurements.cls',
SpanFields.BROWSER_WEB_VITAL_CLS_VALUE,
'profile.id',
'replay.id',
'measurements.score.ratio.cls',
Expand All @@ -375,7 +375,7 @@ export const WEB_VITALS_DETAILS_PREBUILT_CONFIG: PrebuiltDashboard = {
columns: [
'project',
'trace',
'measurements.cls',
SpanFields.BROWSER_WEB_VITAL_CLS_VALUE,
'profile.id',
'replay.id',
'measurements.score.ratio.cls',
Expand Down
Loading
Loading