Skip to content

Commit ed79556

Browse files
committed
Merge branch 'master' into tkdodo/ref/knip-zero
2 parents d8447d8 + 24d0105 commit ed79556

File tree

22 files changed

+298
-38
lines changed

22 files changed

+298
-38
lines changed

migrations_lockfile.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ nodestore: 0001_squashed_0002_nodestore_no_dictfield
2121

2222
replays: 0001_squashed_0005_drop_replay_index
2323

24-
sentry: 0909_django_array_field_not_release
24+
sentry: 0911_increase_email_model_email_field_length
2525

2626
social_auth: 0001_squashed_0002_default_auto_field
2727

src/sentry/incidents/subscription_processor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ def process_update(self, subscription_update: QuerySubscriptionUpdate) -> None:
498498
"organizations:workflow-engine-metric-alert-dual-processing-logs",
499499
self.subscription.project.organization,
500500
):
501-
metrics.incr("dual_processing.alert_rules.fire")
501+
metrics.incr("dual_processing.alert_rules.resolve")
502502
incident_trigger = self.trigger_resolve_threshold(
503503
trigger, aggregation_value
504504
)

src/sentry/integrations/tasks/sync_status_outbound.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from sentry import analytics, features
22
from sentry.constants import ObjectStatus
3+
from sentry.integrations.base import IntegrationInstallation
34
from sentry.integrations.models.external_issue import ExternalIssue
45
from sentry.integrations.models.integration import Integration
56
from sentry.integrations.project_management.metrics import (
@@ -8,7 +9,7 @@
89
)
910
from sentry.integrations.services.integration import integration_service
1011
from sentry.models.group import Group, GroupStatus
11-
from sentry.shared_integrations.exceptions import IntegrationFormError
12+
from sentry.shared_integrations.exceptions import ApiUnauthorized, IntegrationFormError
1213
from sentry.silo.base import SiloMode
1314
from sentry.tasks.base import instrumented_task, retry, track_group_async_operation
1415
from sentry.taskworker.config import TaskworkerConfig
@@ -55,7 +56,9 @@ def sync_status_outbound(group_id: int, external_issue_id: int) -> bool | None:
5556
)
5657
if not integration:
5758
return None
58-
installation = integration.get_installation(organization_id=external_issue.organization_id)
59+
installation: IntegrationInstallation = integration.get_installation(
60+
organization_id=external_issue.organization_id
61+
)
5962
if not (hasattr(installation, "should_sync") and hasattr(installation, "sync_status_outbound")):
6063
return None
6164

@@ -76,7 +79,7 @@ def sync_status_outbound(group_id: int, external_issue_id: int) -> bool | None:
7679
installation.sync_status_outbound(
7780
external_issue, group.status == GroupStatus.RESOLVED, group.project_id
7881
)
79-
except IntegrationFormError as e:
82+
except (IntegrationFormError, ApiUnauthorized) as e:
8083
lifecycle.record_halt(halt_reason=e)
8184
return None
8285
analytics.record(

src/sentry/integrations/utils/metrics.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ def __init__(self, payload: EventLifecycleMetric, assume_success: bool = True) -
108108
self._state: EventLifecycleOutcome | None = None
109109
self._extra = dict(self.payload.get_extras())
110110

111+
def get_state(self) -> EventLifecycleOutcome | None:
112+
return self._state
113+
111114
def add_extra(self, name: str, value: Any) -> None:
112115
"""Add a value to logged "extra" data.
113116

src/sentry/integrations/vsts/issues.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from abc import ABC
44
from collections.abc import Mapping, MutableMapping, Sequence
5-
from typing import TYPE_CHECKING, Any
5+
from typing import TYPE_CHECKING, Any, NoReturn
66

77
from django.urls import reverse
88
from django.utils.translation import gettext as _
@@ -17,6 +17,7 @@
1717
from sentry.models.activity import Activity
1818
from sentry.shared_integrations.exceptions import ApiError, ApiUnauthorized, IntegrationError
1919
from sentry.silo.base import all_silo_function
20+
from sentry.users.models.identity import Identity
2021
from sentry.users.models.user import User
2122
from sentry.users.services.user import RpcUser
2223
from sentry.users.services.user.service import user_service
@@ -25,6 +26,12 @@
2526
from sentry.integrations.models.external_issue import ExternalIssue
2627
from sentry.models.group import Group
2728

29+
# Specific substring to identify Azure Entra ID "identity deleted" errors
30+
# Example: According to Microsoft Entra, your Identity xxx is currently Deleted within the following Microsoft Entra tenant: xxx Please contact your Microsoft Entra administrator to resolve this.
31+
VSTS_IDENTITY_DELETED_ERROR_SUBSTRING = [
32+
"is currently Deleted within the following Microsoft Entra tenant"
33+
]
34+
2835

2936
class VstsIssuesSpec(IssueSyncIntegration, SourceCodeIssueIntegration, ABC):
3037
description = "Integrate Azure DevOps work items by linking a project."
@@ -46,6 +53,14 @@ def create_default_repo_choice(self, default_repo: str) -> tuple[str, str]:
4653
project = self.get_client().get_project(default_repo)
4754
return (project["id"], project["name"])
4855

56+
def raise_error(self, exc: Exception, identity: Identity | None = None) -> NoReturn:
57+
# Reraise Azure Specific Errors correctly
58+
if isinstance(exc, ApiError) and any(
59+
substring in str(exc) for substring in VSTS_IDENTITY_DELETED_ERROR_SUBSTRING
60+
):
61+
raise ApiUnauthorized(text=str(exc))
62+
raise super().raise_error(exc, identity)
63+
4964
def get_project_choices(
5065
self, group: Group | None = None, **kwargs: Any
5166
) -> tuple[str | None, Sequence[tuple[str, str]]]:
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Generated by Django 5.2.1 on 2025-05-21 20:55
2+
3+
from django.db import migrations, models
4+
5+
from sentry.new_migrations.migrations import CheckedMigration
6+
7+
8+
class Migration(CheckedMigration):
9+
# This flag is used to mark that a migration shouldn't be automatically run in production.
10+
# This should only be used for operations where it's safe to run the migration after your
11+
# code has deployed. So this should not be used for most operations that alter the schema
12+
# of a table.
13+
# Here are some things that make sense to mark as post deployment:
14+
# - Large data migrations. Typically we want these to be run manually so that they can be
15+
# monitored and not block the deploy for a long period of time while they run.
16+
# - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to
17+
# run this outside deployments so that we don't block them. Note that while adding an index
18+
# is a schema change, it's completely safe to run the operation after the code has deployed.
19+
# Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment
20+
21+
is_post_deployment = False
22+
23+
dependencies = [
24+
("sentry", "0909_django_array_field_not_release"),
25+
]
26+
27+
operations = [
28+
migrations.AlterField(
29+
model_name="organizationmemberteam",
30+
name="is_active",
31+
field=models.BooleanField(db_default=True),
32+
),
33+
]
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Generated by Django 5.2.1 on 2025-05-21 23:05
2+
3+
from django.db import migrations, models
4+
5+
import sentry.db.models.fields.citext
6+
from sentry.new_migrations.migrations import CheckedMigration
7+
8+
9+
class Migration(CheckedMigration):
10+
# This flag is used to mark that a migration shouldn't be automatically run in production.
11+
# This should only be used for operations where it's safe to run the migration after your
12+
# code has deployed. So this should not be used for most operations that alter the schema
13+
# of a table.
14+
# Here are some things that make sense to mark as post deployment:
15+
# - Large data migrations. Typically we want these to be run manually so that they can be
16+
# monitored and not block the deploy for a long period of time while they run.
17+
# - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to
18+
# run this outside deployments so that we don't block them. Note that while adding an index
19+
# is a schema change, it's completely safe to run the operation after the code has deployed.
20+
# Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment
21+
22+
is_post_deployment = False
23+
24+
dependencies = [
25+
("sentry", "0910_make_organizationmemberteam_is_active_default"),
26+
]
27+
28+
operations = [
29+
migrations.AlterField(
30+
model_name="email",
31+
name="email",
32+
field=sentry.db.models.fields.citext.CIEmailField(max_length=200, unique=True),
33+
),
34+
migrations.AlterField(
35+
model_name="useremail",
36+
name="email",
37+
field=models.EmailField(max_length=200),
38+
),
39+
]

src/sentry/models/organizationmemberteam.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class OrganizationMemberTeam(ReplicatedRegionModel):
3131
organizationmember = FlexibleForeignKey("sentry.OrganizationMember")
3232
# an inactive membership simply removes the team from the default list
3333
# but still allows them to re-join without request
34-
is_active = models.BooleanField(default=True)
34+
is_active = models.BooleanField(db_default=True)
3535
role = models.CharField(max_length=32, null=True, blank=True)
3636

3737
class Meta:

src/sentry/users/models/email.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class Email(Model):
2222
__relocation_dependencies__ = {"sentry.User"}
2323
__relocation_custom_ordinal__ = ["email"]
2424

25-
email = CIEmailField(_("email address"), unique=True, max_length=75)
25+
email = CIEmailField(_("email address"), unique=True, max_length=200)
2626
date_added = models.DateTimeField(default=timezone.now)
2727

2828
class Meta:

src/sentry/users/models/useremail.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class UserEmail(ControlOutboxProducingModel):
5959
__relocation_custom_ordinal__ = ["user", "email"]
6060

6161
user = FlexibleForeignKey(settings.AUTH_USER_MODEL, related_name="emails")
62-
email = models.EmailField(_("email address"), max_length=75)
62+
email = models.EmailField(_("email address"), max_length=200)
6363
validation_hash = models.CharField(max_length=32, default=get_secure_token)
6464
date_hash_added = models.DateTimeField(default=timezone.now)
6565
is_verified = models.BooleanField(

src/sentry/workflow_engine/processors/workflow.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -313,9 +313,10 @@ def process_workflows(event_data: WorkflowEventData) -> set[Workflow]:
313313
},
314314
)
315315
# in order to check if workflow engine is firing 1:1 with the old system, we must only count once rather than each action
316-
metrics.incr(
317-
"workflow_engine.process_workflows.fired_actions",
318-
tags={"detector_type": detector.type},
319-
)
316+
if len(actions) > 0:
317+
metrics.incr(
318+
"workflow_engine.process_workflows.fired_actions",
319+
tags={"detector_type": detector.type},
320+
)
320321

321322
return triggered_workflows

static/app/components/searchQueryBuilder/context.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ interface SearchQueryBuilderContextData {
3636
filterKeys: TagCollection;
3737
focusOverride: FocusOverride | null;
3838
getFieldDefinition: (key: string, kind?: FieldKind) => FieldDefinition | null;
39+
getSuggestedFilterKey: (key: string) => string | null;
3940
getTagValues: (tag: Tag, query: string) => Promise<string[]>;
4041
handleSearch: (query: string) => void;
4142
parseQuery: (query: string) => ParseResult | null;
@@ -78,6 +79,7 @@ export function SearchQueryBuilderProvider({
7879
filterKeys,
7980
filterKeyMenuWidth = 360,
8081
filterKeySections,
82+
getSuggestedFilterKey,
8183
getTagValues,
8284
onSearch,
8385
placeholder,
@@ -139,6 +141,7 @@ export function SearchQueryBuilderProvider({
139141
filterKeySections: filterKeySections ?? [],
140142
filterKeyMenuWidth,
141143
filterKeys,
144+
getSuggestedFilterKey: getSuggestedFilterKey ?? ((key: string) => key),
142145
getTagValues,
143146
getFieldDefinition: fieldDefinitionGetter,
144147
dispatch,
@@ -160,6 +163,7 @@ export function SearchQueryBuilderProvider({
160163
filterKeySections,
161164
filterKeyMenuWidth,
162165
filterKeys,
166+
getSuggestedFilterKey,
163167
getTagValues,
164168
fieldDefinitionGetter,
165169
dispatch,

static/app/components/searchQueryBuilder/index.spec.tsx

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ const FILTER_KEYS: TagCollection = {
9090
key: 'tags[foo,string]',
9191
name: 'foo',
9292
},
93-
'tags[bar,string]': {
94-
key: 'tags[bar,string]',
93+
'tags[bar,number]': {
94+
key: 'tags[bar,number]',
9595
name: 'bar',
9696
},
9797
};
@@ -3458,4 +3458,90 @@ describe('SearchQueryBuilder', function () {
34583458
expect(option).toHaveTextContent('bar');
34593459
});
34603460
});
3461+
3462+
describe('autocomplete using suggestions', function () {
3463+
function getSuggestedFilterKey(key: string) {
3464+
if (key === 'foo') {
3465+
return 'tags[foo,string]';
3466+
}
3467+
3468+
if (key === 'bar') {
3469+
return 'tags[bar,number]';
3470+
}
3471+
3472+
return key;
3473+
}
3474+
3475+
it('replace string key with suggestion when autocompleting', async function () {
3476+
render(
3477+
<SearchQueryBuilder
3478+
{...defaultProps}
3479+
getSuggestedFilterKey={getSuggestedFilterKey}
3480+
/>
3481+
);
3482+
3483+
await userEvent.click(getLastInput());
3484+
await userEvent.keyboard('foo:');
3485+
await userEvent.keyboard('{Escape}');
3486+
3487+
expect(
3488+
screen.getByRole('button', {name: 'Edit key for filter: tags[foo,string]'})
3489+
).toHaveTextContent('foo');
3490+
});
3491+
3492+
it('replace number key with suggestion when autocompleting', async function () {
3493+
render(
3494+
<SearchQueryBuilder
3495+
{...defaultProps}
3496+
getSuggestedFilterKey={getSuggestedFilterKey}
3497+
/>
3498+
);
3499+
3500+
await userEvent.click(getLastInput());
3501+
await userEvent.keyboard('bar:');
3502+
await userEvent.keyboard('{Escape}');
3503+
3504+
expect(
3505+
screen.getByRole('button', {name: 'Edit key for filter: tags[bar,number]'})
3506+
).toHaveTextContent('bar');
3507+
});
3508+
3509+
it('replaces string key with suggestion on enter', async function () {
3510+
render(
3511+
<SearchQueryBuilder
3512+
{...defaultProps}
3513+
initialQuery="browser.name:firefox"
3514+
getSuggestedFilterKey={getSuggestedFilterKey}
3515+
/>
3516+
);
3517+
3518+
await userEvent.click(
3519+
screen.getByRole('button', {name: 'Edit key for filter: browser.name'})
3520+
);
3521+
await userEvent.keyboard('foo{Enter}{Escape}');
3522+
3523+
expect(
3524+
screen.getByRole('button', {name: 'Edit key for filter: tags[foo,string]'})
3525+
).toHaveTextContent('foo');
3526+
});
3527+
3528+
it('replaces number key with suggestion on enter', async function () {
3529+
render(
3530+
<SearchQueryBuilder
3531+
{...defaultProps}
3532+
initialQuery="browser.name:firefox"
3533+
getSuggestedFilterKey={getSuggestedFilterKey}
3534+
/>
3535+
);
3536+
3537+
await userEvent.click(
3538+
screen.getByRole('button', {name: 'Edit key for filter: browser.name'})
3539+
);
3540+
await userEvent.keyboard('bar{Enter}{Escape}');
3541+
3542+
expect(
3543+
screen.getByRole('button', {name: 'Edit key for filter: tags[bar,number]'})
3544+
).toHaveTextContent('bar');
3545+
});
3546+
});
34613547
});

static/app/components/searchQueryBuilder/index.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ export interface SearchQueryBuilderProps {
8181
* will only render a warning if the value is truthy
8282
*/
8383
getFilterTokenWarning?: (key: string) => React.ReactNode;
84+
/**
85+
* This is used when a user types in a search key and submits the token.
86+
* The submission happens when the user types a colon or presses enter.
87+
* When this happens, this function is used to map the user input to a
88+
* known column.
89+
*/
90+
getSuggestedFilterKey?: (key: string) => string | null;
8491
/**
8592
* Allows for customization of the invalid token messages.
8693
*/

0 commit comments

Comments
 (0)