Skip to content

Commit 39baa70

Browse files
committed
Merge branch 'master' into shruthi/feat/add-eap-items-results-consumers
2 parents dd2c91b + 82d3edc commit 39baa70

File tree

33 files changed

+586
-196
lines changed

33 files changed

+586
-196
lines changed

bin/load-integration-data

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#!/usr/bin/env python
2+
from sentry.runner import configure
3+
4+
configure()
5+
6+
import argparse
7+
8+
import click
9+
from django.core import serializers
10+
from django.db import IntegrityError, router
11+
12+
from sentry.integrations.models.integration import Integration
13+
from sentry.integrations.models.organization_integration import OrganizationIntegration
14+
from sentry.users.models.identity import Identity, IdentityProvider
15+
from sentry.utils.db import atomic_transaction
16+
17+
# The order in which models should be loaded to respect foreign key dependencies.
18+
MODEL_LOAD_ORDER = [
19+
"sentry.identityprovider",
20+
"sentry.integration",
21+
"sentry.identity", # Depends on sentry.identityprovider
22+
"sentry.organizationintegration", # Depends on sentry.integration (and sentry.organization)
23+
]
24+
25+
26+
def load_data(input_file, org_id):
27+
"""
28+
Loads data from a JSON file and saves it to the database.
29+
Assumes that PKs from the file should be preserved.
30+
"""
31+
click.echo(f"Reading serialized data from {input_file}...")
32+
with open(input_file) as f:
33+
serialized_data = f.read()
34+
35+
if not serialized_data.strip() or serialized_data.strip() == "[]":
36+
click.echo("Input file is empty or contains no data. Nothing to load.")
37+
return
38+
39+
click.echo("Deserializing objects...")
40+
try:
41+
deserialized_objects = list(serializers.deserialize("json", serialized_data))
42+
except Exception as e:
43+
click.echo(f"Error during deserialization: {e}")
44+
click.echo(
45+
"Please ensure the input file is a valid JSON dump created by the save_integration_data.py script."
46+
)
47+
return
48+
49+
if not deserialized_objects:
50+
click.echo("No objects were deserialized from the file.")
51+
return
52+
53+
click.echo(f"Deserialized {len(deserialized_objects)} objects.")
54+
55+
# Sort deserialized objects based on MODEL_LOAD_ORDER to handle dependencies.
56+
# Objects not in MODEL_LOAD_ORDER will be placed at the end.
57+
def get_sort_key(d_obj):
58+
model_key = f"{d_obj.object._meta.app_label}.{d_obj.object._meta.model_name}"
59+
try:
60+
return MODEL_LOAD_ORDER.index(model_key)
61+
except ValueError:
62+
return len(MODEL_LOAD_ORDER) # Put unknown models at the end
63+
64+
sorted_deserialized_objects = sorted(deserialized_objects, key=get_sort_key)
65+
66+
saved_count = 0
67+
skipped_count = 0
68+
error_count = 0
69+
70+
parsed_org_id = None
71+
if org_id:
72+
try:
73+
parsed_org_id = int(org_id)
74+
click.echo(
75+
f"Will update OrganizationIntegration objects to organization_id: {parsed_org_id}"
76+
)
77+
except ValueError:
78+
click.echo(
79+
f"Warning: Invalid org_id '{org_id}'. It will be ignored. Please provide a valid integer."
80+
)
81+
parsed_org_id = None
82+
83+
click.echo("Attempting to save objects to the database...")
84+
with atomic_transaction(
85+
using=(
86+
router.db_for_write(Integration),
87+
router.db_for_write(OrganizationIntegration),
88+
router.db_for_write(Identity),
89+
router.db_for_write(IdentityProvider),
90+
)
91+
):
92+
for deserialized_object in sorted_deserialized_objects:
93+
model_name = deserialized_object.object._meta.object_name
94+
pk = deserialized_object.object.pk
95+
96+
# If org_id is provided, update OrganizationIntegration's organization_id
97+
if parsed_org_id is not None and isinstance(
98+
deserialized_object.object, OrganizationIntegration
99+
):
100+
click.echo(
101+
f" Updating organization_id for {model_name} (PK: {pk}) to {parsed_org_id}"
102+
)
103+
deserialized_object.object.organization_id = parsed_org_id
104+
105+
try:
106+
# The deserialized_object.save() method handles saving the object
107+
# and its many-to-many data (if any). It attempts to use the PK
108+
# from the serialized data.
109+
deserialized_object.save()
110+
saved_count += 1
111+
click.echo(f" Saved: {model_name} (PK: {pk})")
112+
except IntegrityError as e:
113+
# This can occur due to PK conflict, unique constraint violation,
114+
# or a non-existent foreign key (e.g., if a referenced User or Organization
115+
# doesn't exist in the target DB).
116+
skipped_count += 1
117+
click.echo(f" Skipped: {model_name} (PK: {pk}) due to IntegrityError: {e}")
118+
except Exception as e:
119+
# Catch other potential errors during save.
120+
error_count += 1
121+
click.echo(f" Error saving: {model_name} (PK: {pk}): {e}")
122+
# Depending on severity, you might want to re-raise to stop the transaction.
123+
# For now, we'll log and continue.
124+
125+
click.echo("\nLoad process completed.")
126+
click.echo(f" Successfully saved: {saved_count} objects.")
127+
click.echo(f" Skipped (IntegrityError): {skipped_count} objects.")
128+
click.echo(f" Errors (Other): {error_count} objects.")
129+
if skipped_count > 0 or error_count > 0:
130+
click.echo(
131+
"Please check skipped/error messages. This might indicate that the target database was not clean,"
132+
)
133+
click.echo("or that required related objects (like Organizations or Users) were missing.")
134+
135+
136+
if __name__ == "__main__":
137+
parser = argparse.ArgumentParser(
138+
description="Load Sentry integration-related models from a JSON file into the database."
139+
)
140+
parser.add_argument(
141+
"--input-file",
142+
required=True,
143+
help="Path to the input JSON file containing the data to load.",
144+
)
145+
parser.add_argument(
146+
"--org-id",
147+
required=False,
148+
help="The organization ID to save integration data for.",
149+
)
150+
args = parser.parse_args()
151+
152+
load_data(args.input_file, args.org_id)

bin/save-integration-data

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/usr/bin/env python
2+
from sentry.runner import configure
3+
4+
configure()
5+
6+
import argparse
7+
8+
import click
9+
from django.core import serializers
10+
11+
from sentry.integrations.models import Integration, OrganizationIntegration
12+
from sentry.users.models.identity import Identity, IdentityProvider
13+
14+
MODELS_TO_SERIALIZE = [
15+
IdentityProvider, # Has no FKs to other serialized models
16+
Integration, # Has no FKs to other serialized models
17+
Identity, # Depends on IdentityProvider
18+
OrganizationIntegration, # Depends on Integration
19+
]
20+
21+
22+
def save_data(output_file):
23+
"""
24+
Collects data from specified models and serializes it to a JSON file.
25+
"""
26+
all_objects_to_serialize = []
27+
click.echo("Collecting data from models...")
28+
for model_cls in MODELS_TO_SERIALIZE:
29+
model_name = f"{model_cls._meta.app_label}.{model_cls._meta.model_name}"
30+
click.echo(f" Fetching from {model_name}...")
31+
# Order by PK for consistent output, though serializer might reorder.
32+
# Convert queryset to list to avoid issues with extending during iteration if any.
33+
objects = list(model_cls.objects.order_by("pk").all())
34+
all_objects_to_serialize.extend(objects)
35+
click.echo(f" Found {len(objects)} objects.")
36+
37+
if not all_objects_to_serialize:
38+
click.echo("No objects found to serialize.")
39+
serialized_data = "[]"
40+
else:
41+
click.echo(f"\nSerializing {len(all_objects_to_serialize)} objects in total...")
42+
serialized_data = serializers.serialize("json", all_objects_to_serialize, indent=2)
43+
44+
click.echo(f"Writing serialized data to {output_file}...")
45+
with open(output_file, "w") as f:
46+
f.write(serialized_data)
47+
click.echo(f"Successfully saved data to {output_file}")
48+
49+
50+
if __name__ == "__main__":
51+
parser = argparse.ArgumentParser(
52+
description="Save Sentry integration-related models to a JSON file."
53+
)
54+
parser.add_argument(
55+
"--output-file",
56+
required=True,
57+
help="Path to the output JSON file where data will be saved.",
58+
)
59+
args = parser.parse_args()
60+
61+
save_data(args.output_file)

rspack.config.ts

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -293,10 +293,32 @@ const appConfig: Configuration = {
293293
},
294294
plugins: [
295295
/**
296-
* Adds build time measurement instrumentation, which will be reported back
297-
* to sentry
296+
* Without this, webpack will chunk the locales but attempt to load them all
297+
* eagerly.
298298
*/
299-
// new SentryInstrumentation(),
299+
new rspack.IgnorePlugin({
300+
contextRegExp: /moment$/,
301+
resourceRegExp: /^\.\/locale$/,
302+
}),
303+
304+
/**
305+
* Restrict translation files that are pulled in through app/translations.jsx
306+
* and through moment/locale/* to only those which we create bundles for via
307+
* locale/catalogs.json.
308+
*
309+
* Without this, webpack will still output all of the unused locale files despite
310+
* the application never loading any of them.
311+
*/
312+
new rspack.ContextReplacementPlugin(
313+
/sentry-locale$/,
314+
path.join(__dirname, 'src', 'sentry', 'locale', path.sep),
315+
true,
316+
new RegExp(`(${supportedLocales.join('|')})/.*\\.po$`)
317+
),
318+
new rspack.ContextReplacementPlugin(
319+
/moment\/locale/,
320+
new RegExp(`(${supportedLanguages.join('|')})\\.js$`)
321+
),
300322

301323
/**
302324
* TODO(epurkhiser): Figure out if we still need these
@@ -332,25 +354,6 @@ const appConfig: Configuration = {
332354

333355
...(SHOULD_ADD_RSDOCTOR ? [new RsdoctorWebpackPlugin({})] : []),
334356

335-
/**
336-
* Restrict translation files that are pulled in through app/translations.jsx
337-
* and through moment/locale/* to only those which we create bundles for via
338-
* locale/catalogs.json.
339-
*
340-
* Without this, webpack will still output all of the unused locale files despite
341-
* the application never loading any of them.
342-
*/
343-
new rspack.ContextReplacementPlugin(
344-
/sentry-locale$/,
345-
path.join(__dirname, 'src', 'sentry', 'locale', path.sep),
346-
true,
347-
new RegExp(`(${supportedLocales.join('|')})/.*\\.po$`)
348-
),
349-
new rspack.ContextReplacementPlugin(
350-
/moment\/locale/,
351-
new RegExp(`(${supportedLanguages.join('|')})\\.js$`)
352-
),
353-
354357
/**
355358
* Copies file logo-sentry.svg to the dist/entrypoints directory so that it can be accessed by
356359
* the backend

src/sentry/grouping/enhancer/__init__.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@
4444
]
4545
LATEST_VERSION = VERSIONS[-1]
4646

47+
# A delimiter to insert between rulesets in the base64 represenation of enhancements (by spec,
48+
# base64 strings never contain '#')
49+
BASE64_ENHANCEMENTS_DELIMITER = b"#"
50+
4751
VALID_PROFILING_MATCHER_PREFIXES = (
4852
"stack.abs_path",
4953
"path", # stack.abs_path alias
@@ -807,7 +811,19 @@ def _get_base64_bytes_from_rules(self, rules: list[EnhancementRule]) -> bytes:
807811
@cached_property
808812
def base64_string(self) -> str:
809813
"""A base64 string representation of the enhancements object"""
810-
base64_bytes = self._get_base64_bytes_from_rules(self.rules)
814+
rulesets = [self.rules]
815+
816+
if self.run_split_enhancements:
817+
rulesets.extend([self.classifier_rules, self.contributes_rules])
818+
819+
# Create a base64 bytestring for each set of rules, and join them with a character we know
820+
# can never appear in base64. We do it this way rather than combining all three sets of
821+
# rules into a single bytestring because the rust enhancer only knows how to deal with
822+
# bytestrings encoding data of the form `[version, bases, rules]` (not
823+
# `[version, bases, rules, rules, rules]`).
824+
base64_bytes = BASE64_ENHANCEMENTS_DELIMITER.join(
825+
self._get_base64_bytes_from_rules(ruleset) for ruleset in rulesets
826+
)
811827
base64_str = base64_bytes.decode("ascii")
812828
return base64_str
813829

@@ -845,13 +861,25 @@ def from_base64_string(
845861
with metrics.timer("grouping.enhancements.creation") as metrics_timer_tags:
846862
metrics_timer_tags.update({"source": "base64_string", "referrer": referrer})
847863

848-
bytes_str = (
864+
raw_bytes_str = (
849865
base64_string.encode("ascii", "ignore")
850866
if isinstance(base64_string, str)
851867
else base64_string
852868
)
853869

854-
unsplit_config = cls._get_config_from_base64_bytes(bytes_str)
870+
# Split the string to get encoded data for each set of rules: unsplit rules (i.e., rules
871+
# the way they're stored in project config), classifier rules, and contributes rules.
872+
# Older base64 strings - such as those stored in events created before rule-splitting was
873+
# introduced - will only have one part and thus will end up unchanged. (The delimiter is
874+
# chosen specifically to be a character which can't appear in base64.)
875+
bytes_strs = raw_bytes_str.split(BASE64_ENHANCEMENTS_DELIMITER)
876+
configs = [cls._get_config_from_base64_bytes(bytes_str) for bytes_str in bytes_strs]
877+
878+
unsplit_config = configs[0]
879+
split_configs = None
880+
881+
if len(configs) == 3:
882+
split_configs = (configs[1], configs[2])
855883

856884
version = unsplit_config.version
857885
bases = unsplit_config.bases
@@ -861,6 +889,7 @@ def from_base64_string(
861889
return cls(
862890
rules=unsplit_config.rules,
863891
rust_enhancements=unsplit_config.rust_enhancements,
892+
split_enhancement_configs=split_configs,
864893
version=version,
865894
bases=bases,
866895
)

src/sentry/options/defaults.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3113,6 +3113,13 @@
31133113
flags=FLAG_AUTOMATOR_MODIFIABLE,
31143114
)
31153115

3116+
register(
3117+
"taskworker.try_compress.profile_metrics.rollout",
3118+
default=0.0,
3119+
type=Float,
3120+
flags=FLAG_AUTOMATOR_MODIFIABLE,
3121+
)
3122+
31163123
# Taskbroker flags
31173124
register(
31183125
"taskworker.try_compress.profile_metrics.level",

0 commit comments

Comments
 (0)