Repository: getsentry/sentry

Issue:
Webpack ChunkLoadErrors Filtered out
### Problem Statement

ChunkLoadErrors are not new, but seem to be impacting NextJS devs working with AppDir disproportionately. This leads to a buggy page or just a white page, it seems.

The problem is these errors are not very actionable all the time, and can happen often which simply leads to consuming a lot of Sentry quota. 

As a fix there is now an inbound filter on by default, good for quota, bad for visibilty. Is there way we can ensure users are still alerted to this problem, without killing their quota?

![image](https://github.com/getsentry/sentry-javascript/assets/47563310/5505041c-bd72-4cdf-8dd7-1e3504c23907)

As seen in examples like [this](https://dev.to/ianwalter/fixing-chunkloaderror-3791), Sentry plays an important role in telling developers that this problem exists

### Solution Brainstorm

1. some kind of auto-refund
2. performance issue

### Background

Was surfaced on [twitter](https://twitter.com/viktorsarstrom/status/1707105377328804296) with link to error on NextjS 

https://github.com/vercel/next.js/issues/38507


Discussion:
Assigning to @getsentry/support for [routing](https://open.sentry.io/triage/#2-route) ⏲️
Routing to @getsentry/product-owners-issues for [triage](https://develop.sentry.dev/processing-tickets/#3-triage) ⏲️
Besides what @smeubank proposed -

We could try to group all these errors together - explain to users that they should capture the error in their app and refresh the page.
While at the same time pointing them to the inbound filter where they can enable it then in case they don't care to do this.

But since it's a valid error, this approach might maintain trust in Sentry and our reporting.
It could be a new Issue Type - where instead of us showing a Stacktrace, we explain what the error is:

"This error happens when your the users bundle isn't refreshed when you do a new deploy. To fix this capture this error in your application and prompt the user to refresh the page."
We also need a grouping rule to ensure that all ChunkLoadErrors get grouped together. We want to add more help text to help people debug the error and link to the filter to turn it on if they don't want to see those types of errors.
Action items:
- Add a strict grouping rule for these errors
- Messaging with:
  - A description of the error
  - A link to the inbound filter setting for that project

Resolving patch:
diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py
--- a/src/sentry/event_manager.py
+++ b/src/sentry/event_manager.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import copy
+import hashlib
 import ipaddress
 import logging
 import mimetypes
@@ -2551,7 +2552,13 @@ def _calculate_event_grouping(
             # event. If that config has since been deleted (because it was an
             # experimental grouping config) we fall back to the default.
             try:
-                hashes = event.get_hashes(grouping_config)
+                hashes = None
+                # If the event platform is javascript, it could have a ChunkLoadError that we want to force group
+                if event.platform == "javascript":
+                    hashes = get_chunk_load_error_hash(event)
+
+                if not hashes:
+                    hashes = event.get_hashes(grouping_config)
             except GroupingConfigNotFound:
                 event.data["grouping_config"] = get_grouping_config_dict_for_project(project)
                 hashes = event.get_hashes()
@@ -2593,6 +2600,30 @@ def _detect_performance_problems(jobs: Sequence[Job], projects: ProjectsMapping)
         )
 
 
+def get_chunk_load_error_hash(event: Event) -> Optional[CalculatedHashes]:
+    """
+    Return the same hash if the event is a ChunkLoadError, otherwise return None
+    """
+    try:
+        exception_value = event.data["exception"]["values"][0]["value"]
+        exception_type = event.data["exception"]["values"][0]["type"]
+    except KeyError:
+        return None
+
+    hashes = None
+    # Only check for the flag after it is established if it's a ChunkLoadError to avoid
+    # unnecessary querying
+    if "ChunkLoadError" in exception_value or exception_type == "ChunkLoadError":
+        organization = Project.objects.get(id=event.project_id).organization
+        if features.has("organizations:group-chunk-load-errors", organization):
+            hashes = CalculatedHashes(
+                hashes=[hashlib.md5(b"chunkloaderror").hexdigest()],
+                hierarchical_hashes=[],
+                tree_labels=[],
+            )
+    return hashes
+
+
 class PerformanceJob(TypedDict, total=False):
     performance_problems: Sequence[PerformanceProblem]
     event: Event


Test patch:
diff --git a/tests/sentry/event_manager/test_event_manager_grouping.py b/tests/sentry/event_manager/test_event_manager_grouping.py
--- a/tests/sentry/event_manager/test_event_manager_grouping.py
+++ b/tests/sentry/event_manager/test_event_manager_grouping.py
@@ -9,6 +9,7 @@
 from sentry.models.grouphash import GroupHash
 from sentry.testutils.cases import TestCase
 from sentry.testutils.helpers.datetime import before_now, freeze_time, iso_format
+from sentry.testutils.helpers.features import with_feature
 from sentry.testutils.silo import region_silo_test
 from sentry.testutils.skips import requires_snuba
 from sentry.tsdb.base import TSDBModel
@@ -342,3 +343,138 @@ def test_ignores_fingerprint_on_transaction_event(self):
             )[event1.group.id]
             == 1
         )
+
+    @with_feature("organizations:group-chunk-load-errors")
+    def test_group_chunk_load_errors_flag_on(self):
+        """
+        Test that when `group-chunk-load-errors` flag is on, ChunkLoadErrors with different stack
+        traces and values are grouped together
+        """
+        manager = EventManager(
+            make_event(
+                platform="javascript",
+                exception={
+                    "values": [
+                        {
+                            "type": "Error",
+                            "stacktrace": {
+                                "frames": [
+                                    {
+                                        "function": "in_app_function",
+                                    }
+                                ]
+                            },
+                            "value": "ChunkLoadError: Loading chunk 123 failed",
+                            "in_app": True,
+                        }
+                    ]
+                },
+            )
+        )
+        with self.tasks():
+            manager.normalize()
+            event = manager.save(self.project.id)
+
+        manager = EventManager(
+            make_event(
+                platform="javascript",
+                exception={
+                    "values": [
+                        {
+                            "type": "ChunkLoadError",
+                            "stacktrace": {
+                                "frames": [
+                                    {
+                                        "function": "different_in_app_function",
+                                    }
+                                ]
+                            },
+                            "value": "Loading chunk 321 failed",
+                            "in_app": True,
+                        }
+                    ]
+                },
+            )
+        )
+
+        with self.tasks():
+            manager.normalize()
+            event2 = manager.save(self.project.id)
+
+        assert event.group_id == event2.group_id
+
+    @with_feature({"organizations:group-chunk-load-errors": False})
+    def test_do_not_group_chunk_load_errors_flag_off(self):
+        """
+        Test that when `group-chunk-load-errors` flag is off, ChunkLoadErrors with different stack
+        traces and values are not grouped together
+        """
+        manager = EventManager(
+            make_event(
+                platform="javascript",
+                exception={
+                    "values": [
+                        {
+                            "type": "Error",
+                            "stacktrace": {
+                                "frames": [
+                                    {
+                                        "function": "in_app_function",
+                                    }
+                                ]
+                            },
+                            "value": "ChunkLoadError: Loading chunk 123 failed",
+                            "in_app": True,
+                        }
+                    ]
+                },
+            )
+        )
+        with self.tasks():
+            manager.normalize()
+            event = manager.save(self.project.id)
+
+        manager = EventManager(
+            make_event(
+                platform="javascript",
+                exception={
+                    "values": [
+                        {
+                            "type": "ChunkLoadError",
+                            "stacktrace": {
+                                "frames": [
+                                    {
+                                        "function": "different_in_app_function",
+                                    }
+                                ]
+                            },
+                            "value": "Loading chunk 321 failed",
+                            "in_app": True,
+                        }
+                    ]
+                },
+            )
+        )
+
+        with self.tasks():
+            manager.normalize()
+            event2 = manager.save(self.project.id)
+
+        assert event.group_id != event2.group_id
+
+    def test_chunk_load_errors_exception(self):
+        """
+        Test that when an error does not have an exception, the `get_chunk_load_error_hash`
+        function catches the exception and does not stop execution
+        """
+        manager = EventManager(
+            make_event(
+                platform="javascript",
+                exception={},
+            )
+        )
+        with self.tasks():
+            manager.normalize()
+            event = manager.save(self.project.id)
+
+        assert event.group