Repository: getsentry/sentry

Issue:
Internal error when exporting date
### Environment

SaaS (https://sentry.io/)

### Steps to Reproduce

Some users are receiving the message below after trying to export data from Discover. At the moment I an unable to reproduce on two of my organisations.

[Internal issue link](https://sentry.sentry.io/issues/4742527173)

### Expected Result

Data is exported correctly. 

### Actual Result

![Captura de tela de 2023-12-27 08-38-55](https://github.com/getsentry/sentry/assets/64906376/4a2abdcc-ea93-4003-bd6a-ff82365b6c00)


### Product Area

Issues

### Link

_No response_

### DSN

_No response_

### Version

_No response_


Discussion:
Assigning to @getsentry/support for [routing](https://open.sentry.io/triage/#2-route) ⏲️
Routing to @getsentry/product-owners-discover for [triage](https://develop.sentry.dev/processing-tickets/#3-triage) ⏲️

Resolving patch:
diff --git a/src/sentry/data_export/tasks.py b/src/sentry/data_export/tasks.py
--- a/src/sentry/data_export/tasks.py
+++ b/src/sentry/data_export/tasks.py
@@ -102,7 +102,9 @@ def assemble_download(
                 # file handle to a stream writer that will encode to utf8.
                 tfw = codecs.getwriter("utf-8")(tf)
 
-                writer = csv.DictWriter(tfw, processor.header_fields, extrasaction="ignore")
+                writer = csv.DictWriter(
+                    tfw, processor.header_fields, escapechar="\\", extrasaction="ignore"
+                )
                 if first_page:
                     writer.writeheader()
 


Test patch:
diff --git a/tests/sentry/data_export/test_tasks.py b/tests/sentry/data_export/test_tasks.py
--- a/tests/sentry/data_export/test_tasks.py
+++ b/tests/sentry/data_export/test_tasks.py
@@ -618,17 +618,7 @@ def setUp(self):
         self.user = self.create_user()
         self.org = self.create_organization()
         self.project = self.create_project()
-        data = load_data("transaction")
-        for i in range(50):
-            event = data.copy()
-            event.update(
-                {
-                    "transaction": f"/event/{i:03d}/",
-                    "timestamp": iso_format(before_now(minutes=1, seconds=i)),
-                    "start_timestamp": iso_format(before_now(minutes=1, seconds=i + 1)),
-                }
-            )
-            self.store_event(event, project_id=self.project.id)
+        self.data = load_data("transaction")
 
     @patch("sentry.data_export.tasks.MAX_BATCH_SIZE", 200)
     @patch("sentry.data_export.models.ExportedData.email_success")
@@ -640,6 +630,16 @@ def test_discover_large_batch(self, emailer):
         it stops the current batch and starts another. This runs for 2 batches and
         during the 3rd batch, it will finish exporting all 50 rows.
         """
+        for i in range(50):
+            event = self.data.copy()
+            event.update(
+                {
+                    "transaction": f"/event/{i:03d}/",
+                    "timestamp": iso_format(before_now(minutes=1, seconds=i)),
+                    "start_timestamp": iso_format(before_now(minutes=1, seconds=i + 1)),
+                }
+            )
+            self.store_event(event, project_id=self.project.id)
         de = ExportedData.objects.create(
             user_id=self.user.id,
             organization=self.org,
@@ -656,6 +656,37 @@ def test_discover_large_batch(self, emailer):
 
         assert emailer.called
 
+    @patch("sentry.data_export.models.ExportedData.email_success")
+    def test_character_escape(self, emailer):
+        strings = [
+            "SyntaxError: Unexpected token '\u0003', \"\u0003WM�\u0000\u0000\u0000\u0000��\"... is not valid JSON"
+        ]
+        for string in strings:
+            event = self.data.copy()
+            event.update(
+                {
+                    "transaction": string,
+                    "timestamp": iso_format(before_now(minutes=1, seconds=0)),
+                    "start_timestamp": iso_format(before_now(minutes=1, seconds=1)),
+                }
+            )
+            self.store_event(event, project_id=self.project.id)
+        de = ExportedData.objects.create(
+            user_id=self.user.id,
+            organization=self.org,
+            query_type=ExportQueryType.DISCOVER,
+            query_info={"project": [self.project.id], "field": ["transaction"], "query": ""},
+        )
+        with self.tasks():
+            assemble_download(de.id, batch_size=3)
+        de = ExportedData.objects.get(id=de.id)
+        assert de.date_finished is not None
+        assert de.date_expired is not None
+        assert de.file_id is not None
+        assert isinstance(de._get_file(), File)
+
+        assert emailer.called
+
 
 class MergeExportBlobsTest(TestCase, SnubaTestCase):
     def test_task_persistent_name(self):