1+ from collections .abc import Callable
12from datetime import datetime , timedelta
23from typing import Literal
34
2526from sentry .api .serializers import serialize
2627from sentry .api .utils import handle_query_errors
2728from sentry .models .organization import Organization
29+ from sentry .models .release import Release
30+ from sentry .models .releaseenvironment import ReleaseEnvironment
31+ from sentry .models .releaseprojectenvironment import ReleaseStages
32+ from sentry .models .releases .release_project import ReleaseProject
2833from sentry .search .eap import constants
2934from sentry .search .eap .columns import ColumnDefinitions
3035from sentry .search .eap .ourlogs .definitions import OURLOG_DEFINITIONS
3742 translate_internal_to_public_alias ,
3843 translate_to_sentry_conventions ,
3944)
45+ from sentry .search .events .constants import (
46+ RELEASE_STAGE_ALIAS ,
47+ SEMVER_ALIAS ,
48+ SEMVER_BUILD_ALIAS ,
49+ SEMVER_PACKAGE_ALIAS ,
50+ )
51+ from sentry .search .events .filter import _flip_field_sort
4052from sentry .search .events .types import SnubaParams
4153from sentry .snuba .referrer import Referrer
4254from sentry .tagstore .types import TagValue
@@ -346,6 +358,16 @@ def __init__(
346358 params = snuba_params , config = SearchResolverConfig (), definitions = definitions
347359 )
348360 self .search_type , self .attribute_key = self .resolve_attribute_key (key , snuba_params )
361+ self .autocomplete_function : dict [str , Callable [[], list [TagValue ]]] = (
362+ {key : self .project_id_autocomplete_function for key in self .PROJECT_ID_KEYS }
363+ | {key : self .project_slug_autocomplete_function for key in self .PROJECT_SLUG_KEYS }
364+ | {
365+ RELEASE_STAGE_ALIAS : self .release_stage_autocomplete_function ,
366+ SEMVER_ALIAS : self .semver_autocomplete_function ,
367+ SEMVER_BUILD_ALIAS : self .semver_build_autocomplete_function ,
368+ SEMVER_PACKAGE_ALIAS : self .semver_package_autocomplete_function ,
369+ }
370+ )
349371
350372 def resolve_attribute_key (
351373 self , key : str , snuba_params : SnubaParams
@@ -354,11 +376,10 @@ def resolve_attribute_key(
354376 return resolved_attr .search_type , resolved_attr .proto_definition
355377
356378 def execute (self ) -> list [TagValue ]:
357- if self .key in self .PROJECT_ID_KEYS :
358- return self .project_id_autocomplete_function ()
379+ func = self .autocomplete_function .get (self .key )
359380
360- if self . key in self . PROJECT_SLUG_KEYS :
361- return self . project_slug_autocomplete_function ()
381+ if func is not None :
382+ return func ()
362383
363384 if self .search_type == "boolean" :
364385 return self .boolean_autocomplete_function ()
@@ -368,6 +389,146 @@ def execute(self) -> list[TagValue]:
368389
369390 return []
370391
392+ def release_stage_autocomplete_function (self ):
393+ return [
394+ TagValue (
395+ key = self .key ,
396+ value = stage .value ,
397+ times_seen = None ,
398+ first_seen = None ,
399+ last_seen = None ,
400+ )
401+ for stage in ReleaseStages
402+ if not self .query or self .query in stage .value
403+ ]
404+
405+ def semver_autocomplete_function (self ):
406+ versions = Release .objects .filter (version__contains = "@" + self .query )
407+
408+ project_ids = self .snuba_params .project_ids
409+ if project_ids :
410+ release_projects = ReleaseProject .objects .filter (project_id__in = project_ids )
411+ versions = versions .filter (id__in = release_projects .values_list ("release_id" , flat = True ))
412+
413+ environment_ids = self .snuba_params .environment_ids
414+ if environment_ids :
415+ release_environments = ReleaseEnvironment .objects .filter (
416+ environment_id__in = environment_ids
417+ )
418+ versions = versions .filter (
419+ id__in = release_environments .values_list ("release_id" , flat = True )
420+ )
421+
422+ order_by = map (_flip_field_sort , Release .SEMVER_COLS + ["package" ])
423+ versions = versions .filter_to_semver () # type: ignore[attr-defined] # mypy doesn't know about ReleaseQuerySet
424+ versions = versions .annotate_prerelease_column () # type: ignore[attr-defined] # mypy doesn't know about ReleaseQuerySet
425+ versions = versions .order_by (* order_by )
426+
427+ seen = set ()
428+ formatted_versions = []
429+ # We want to format versions here in a way that makes sense for autocomplete. So we
430+ # - Only include package if we think the user entered a package
431+ # - Exclude build number, since it's not used as part of filtering
432+ # When we don't include package, this can result in duplicate version numbers, so we
433+ # also de-dupe here. This can result in less than 1000 versions returned, but we
434+ # typically use very few values so this works ok.
435+ for version in versions .values_list ("version" , flat = True )[:1000 ]:
436+ formatted_version = version .split ("@" , 1 )[1 ]
437+ formatted_version = formatted_version .split ("+" , 1 )[0 ]
438+ if formatted_version in seen :
439+ continue
440+
441+ seen .add (formatted_version )
442+ formatted_versions .append (
443+ TagValue (
444+ key = self .key ,
445+ value = formatted_version ,
446+ times_seen = None ,
447+ first_seen = None ,
448+ last_seen = None ,
449+ )
450+ )
451+
452+ return formatted_versions
453+
454+ def semver_build_autocomplete_function (self ):
455+ build = self .query if self .query else ""
456+ if not build .endswith ("*" ):
457+ build += "*"
458+
459+ organization_id = self .snuba_params .organization_id
460+ assert organization_id is not None
461+
462+ versions = Release .objects .filter_by_semver_build (
463+ organization_id ,
464+ "exact" ,
465+ build ,
466+ self .snuba_params .project_ids ,
467+ )
468+
469+ environment_ids = self .snuba_params .environment_ids
470+ if environment_ids :
471+ release_environments = ReleaseEnvironment .objects .filter (
472+ environment_id__in = environment_ids
473+ )
474+ versions = versions .filter (
475+ id__in = release_environments .values_list ("release_id" , flat = True )
476+ )
477+
478+ builds = (
479+ versions .values_list ("build_code" , flat = True ).distinct ().order_by ("build_code" )[:1000 ]
480+ )
481+
482+ return [
483+ TagValue (
484+ key = self .key ,
485+ value = build ,
486+ times_seen = None ,
487+ first_seen = None ,
488+ last_seen = None ,
489+ )
490+ for build in builds
491+ ]
492+
493+ def semver_package_autocomplete_function (self ):
494+ packages = (
495+ Release .objects .filter (
496+ organization_id = self .snuba_params .organization_id , package__startswith = self .query
497+ )
498+ .values_list ("package" )
499+ .distinct ()
500+ )
501+
502+ versions = Release .objects .filter (
503+ organization_id = self .snuba_params .organization_id ,
504+ package__in = packages ,
505+ id__in = ReleaseProject .objects .filter (
506+ project_id__in = self .snuba_params .project_ids
507+ ).values_list ("release_id" , flat = True ),
508+ ).annotate_prerelease_column () # type: ignore[attr-defined] # mypy doesn't know about ReleaseQuerySet
509+
510+ environment_ids = self .snuba_params .environment_ids
511+ if environment_ids :
512+ release_environments = ReleaseEnvironment .objects .filter (
513+ environment_id__in = environment_ids
514+ )
515+ versions = versions .filter (
516+ id__in = release_environments .values_list ("release_id" , flat = True )
517+ )
518+
519+ packages = versions .values_list ("package" , flat = True ).distinct ().order_by ("package" )[:1000 ]
520+
521+ return [
522+ TagValue (
523+ key = self .key ,
524+ value = package ,
525+ times_seen = None ,
526+ first_seen = None ,
527+ last_seen = None ,
528+ )
529+ for package in packages
530+ ]
531+
371532 def boolean_autocomplete_function (self ) -> list [TagValue ]:
372533 return [
373534 TagValue (
0 commit comments