Browse Source

feat(demo): improve stability and add transactions (#25010)

Stephen Cefali 3 years ago
parent
commit
c8fadb9c0a

+ 3 - 2
src/sentry/demo/apps.py

@@ -5,6 +5,7 @@ class Config(AppConfig):
     name = "sentry.demo"
 
     def ready(self):
-        from .tasks import build_up_org_buffer
+        from .tasks import delete_initializing_orgs
 
-        build_up_org_buffer.apply_async()
+        # also rebuilds the org buffer
+        delete_initializing_orgs.apply_async()

+ 35 - 11
src/sentry/demo/data_population.py

@@ -5,6 +5,7 @@ import os
 import random
 import requests
 import pytz
+import sentry_sdk
 import time
 
 from collections import defaultdict
@@ -12,6 +13,7 @@ from datetime import timedelta
 from django.conf import settings
 from django.utils import timezone
 from hashlib import sha1
+from functools import wraps
 from uuid import uuid4
 from typing import List
 
@@ -389,6 +391,23 @@ def generate_issue_alert(project):
     project_rules.Creator.run(**data)
 
 
+def catch_and_log_errors(func):
+    """
+    Catches any errors, log them, and wait before continuing
+    """
+
+    @wraps(func)
+    def wrapped(*args, **kwargs):
+        try:
+            return func(*args, **kwargs)
+        except Exception as e:
+            logger.error(f"{func.__name__}.error", extra={"error": str(e)}, exc_info=True)
+            time.sleep(settings.DEMO_DATA_GEN_PARAMS["ERROR_BACKOFF_TIME"])
+
+    return wrapped
+
+
+@catch_and_log_errors
 def send_session(sid, user_id, dsn, time, release, **kwargs):
     """
     Creates an envelope payload for a session and posts it to Relay
@@ -410,11 +429,9 @@ def send_session(sid, user_id, dsn, time, release, **kwargs):
     core = json.dumps(data)
 
     body = f"{envelope_headers}\n{item_headers}\n{core}"
-
     endpoint = dsn.get_endpoint()
     url = f"{endpoint}/api/{dsn.project_id}/envelope/?sentry_key={dsn.public_key}&sentry_version=7"
     resp = requests.post(url=url, data=body)
-    logger.info("send_session.send")
     resp.raise_for_status()
 
 
@@ -1004,6 +1021,7 @@ def populate_connected_event_scenario_3(python_project: Project, quick=False):
 
 
 def populate_sessions(project, error_file, quick=False):
+    logger.info("populate_sessions.start")
     dsn = ProjectKey.objects.get(project=project)
 
     react_error = get_event_from_file(error_file)
@@ -1047,18 +1065,24 @@ def populate_sessions(project, error_file, quick=False):
             }
 
         send_session(sid, transaction_user["id"], dsn, timestamp, version, **data)
+    logger.info("populate_sessions.end")
 
 
 def handle_react_python_scenario(react_project: Project, python_project: Project, quick=False):
     """
     Handles all data population for the React + Python scenario
     """
-    generate_releases([react_project, python_project], quick=quick)
-    generate_alerts(python_project)
-    generate_saved_query(react_project, "/productstore", "Product Store")
-    populate_sessions(react_project, "sessions/react_unhandled_exception.json", quick=quick)
-    populate_sessions(python_project, "sessions/python_unhandled_exception.json", quick=quick)
-    populate_connected_event_scenario_1(react_project, python_project, quick=quick)
-    populate_connected_event_scenario_1b(react_project, python_project, quick=quick)
-    populate_connected_event_scenario_2(react_project, python_project, quick=quick)
-    populate_connected_event_scenario_3(python_project, quick=quick)
+    with sentry_sdk.start_span(op="handle_react_python_scenario", description="pre_event_setup"):
+        generate_releases([react_project, python_project], quick=quick)
+        generate_alerts(python_project)
+        generate_saved_query(react_project, "/productstore", "Product Store")
+    with sentry_sdk.start_span(op="handle_react_python_scenario", description="populate_sessions"):
+        populate_sessions(react_project, "sessions/react_unhandled_exception.json", quick=quick)
+        populate_sessions(python_project, "sessions/python_unhandled_exception.json", quick=quick)
+    with sentry_sdk.start_span(
+        op="handle_react_python_scenario", description="populate_connected_events"
+    ):
+        populate_connected_event_scenario_1(react_project, python_project, quick=quick)
+        populate_connected_event_scenario_1b(react_project, python_project, quick=quick)
+        populate_connected_event_scenario_2(react_project, python_project, quick=quick)
+        populate_connected_event_scenario_3(python_project, quick=quick)

+ 109 - 79
src/sentry/demo/demo_org_manager.py

@@ -1,4 +1,5 @@
 import logging
+import sentry_sdk
 
 from django.conf import settings
 from django.db import transaction
@@ -30,98 +31,127 @@ logger = logging.getLogger(__name__)
 
 
 def create_demo_org(quick=False) -> Organization:
-    # wrap the main org setup in transaction
-    with transaction.atomic():
-        name = generate_random_name()
-
-        slug = slugify(name)
-
-        demo_org = DemoOrganization.create_org(name=name, slug=slug)
-        org = demo_org.organization
-
-        logger.info("create_demo_org.created_org", {"organization_slug": slug})
-
-        owner = User.objects.get(email=settings.DEMO_ORG_OWNER_EMAIL)
-        OrganizationMember.objects.create(organization=org, user=owner, role=roles.get_top_dog().id)
-
-        team = org.team_set.create(name=org.name)
-        python_project = Project.objects.create(name="Python", organization=org, platform="python")
-        python_project.add_team(team)
-
-        react_project = Project.objects.create(
-            name="React", organization=org, platform="javascript-react"
+    with sentry_sdk.start_transaction(op="create_demo_org", name="create_demo_org", sampled=True):
+        sentry_sdk.set_tag("quick", quick)
+        # wrap the main org setup in transaction
+        with transaction.atomic():
+            name = generate_random_name()
+
+            slug = slugify(name)
+
+            demo_org = DemoOrganization.create_org(name=name, slug=slug)
+            org = demo_org.organization
+
+            logger.info("create_demo_org.created_org", {"organization_slug": slug})
+
+            owner = User.objects.get(email=settings.DEMO_ORG_OWNER_EMAIL)
+            OrganizationMember.objects.create(
+                organization=org, user=owner, role=roles.get_top_dog().id
+            )
+
+            team = org.team_set.create(name=org.name)
+            python_project = Project.objects.create(
+                name="Python", organization=org, platform="python"
+            )
+            python_project.add_team(team)
+
+            react_project = Project.objects.create(
+                name="React", organization=org, platform="javascript-react"
+            )
+            react_project.add_team(team)
+
+            # we'll be adding transactions later
+            Project.objects.filter(organization=org).update(
+                flags=F("flags").bitor(Project.flags.has_transactions)
+            )
+
+        logger.info(
+            "create_demo_org.post-transaction",
+            extra={"organization_slug": org.slug, "quick": quick},
         )
-        react_project.add_team(team)
 
-        # we'll be adding transactions later
-        Project.objects.filter(organization=org).update(
-            flags=F("flags").bitor(Project.flags.has_transactions)
+        with sentry_sdk.start_span(op="handle_react_python_scenario"):
+            try:
+                handle_react_python_scenario(react_project, python_project, quick=quick)
+            except Exception as e:
+                logger.error(
+                    "create_demo_org.population_error",
+                    extra={"organization_slug": org.slug, "quick": quick, "error": str(e)},
+                )
+                # delete the organization if data population fails
+                org.status = OrganizationStatus.PENDING_DELETION
+                org.save()
+                delete_organization.apply_async(kwargs={"object_id": org.id})
+                raise
+
+        # update the org status now that it's populated
+        demo_org.status = DemoOrgStatus.PENDING
+        demo_org.save()
+
+        logger.info(
+            "create_demo_org.complete",
+            extra={"organization_slug": org.slug, "quick": quick},
         )
 
-    logger.info(
-        "create_demo_org.post-transaction",
-        extra={"organization_slug": org.slug, "quick": quick},
-    )
-    try:
-        handle_react_python_scenario(react_project, python_project, quick=quick)
-    except Exception as e:
-        logger.error(
-            "create_demo_org.population_error",
-            extra={"organization_slug": org.slug, "quick": quick, "error": str(e)},
-        )
-        # delete the organization if data population fails
-        org.status = OrganizationStatus.PENDING_DELETION
-        org.save()
-        delete_organization.apply_async(kwargs={"object_id": org.id})
-        raise
-
-    # update the org status now that it's populated
-    demo_org.status = DemoOrgStatus.PENDING
-    demo_org.save()
-
-    return org
+        return org
 
 
 def assign_demo_org() -> Tuple[Organization, User]:
-    from .tasks import build_up_org_buffer
-
-    demo_org = None
-    # option to skip the buffer when testing things out locally
-    if settings.DEMO_NO_ORG_BUFFER:
-        org = create_demo_org()
-    else:
-        demo_org = DemoOrganization.objects.filter(status=DemoOrgStatus.PENDING).first()
-        # if no org in buffer, make a quick one with fewer events
-        if not demo_org:
-            org = create_demo_org(quick=True)
+    with sentry_sdk.configure_scope() as scope:
+        try:
+            parent_span_id = scope.span.span_id
+            trace_id = scope.span.trace_id
+        except AttributeError:
+            parent_span_id = None
+            trace_id = None
+    with sentry_sdk.start_transaction(
+        op="assign_demo_org",
+        name="assign_demo_org",
+        parent_span_id=parent_span_id,
+        trace_id=trace_id,
+        sampled=True,
+    ):
+        from .tasks import build_up_org_buffer
+
+        demo_org = None
+        # option to skip the buffer when testing things out locally
+        if settings.DEMO_NO_ORG_BUFFER:
+            org = create_demo_org()
+        else:
+            demo_org = DemoOrganization.objects.filter(status=DemoOrgStatus.PENDING).first()
+            # if no org in buffer, make a quick one with fewer events
+            if not demo_org:
+                org = create_demo_org(quick=True)
 
-    if not demo_org:
-        demo_org = DemoOrganization.objects.get(organization=org)
+        if not demo_org:
+            demo_org = DemoOrganization.objects.get(organization=org)
 
-    org = demo_org.organization
+        org = demo_org.organization
 
-    # wrap the assignment of the demo org in a transaction
-    with transaction.atomic():
-        email = create_fake_email(org.slug, "demo")
-        user = DemoUser.create_user(
-            email=email,
-            username=email,
-            is_managed=True,
-        )
+        # wrap the assignment of the demo org in a transaction
+        with transaction.atomic():
+            email = create_fake_email(org.slug, "demo")
+            user = DemoUser.create_user(
+                email=email,
+                username=email,
+                is_managed=True,
+            )
 
-        # TODO: May need logic in case team no longer exists
-        team = Team.objects.get(organization=org)
+            # TODO: May need logic in case team no longer exists
+            team = Team.objects.get(organization=org)
 
-        member = OrganizationMember.objects.create(organization=org, user=user, role="member")
-        OrganizationMemberTeam.objects.create(team=team, organizationmember=member, is_active=True)
+            member = OrganizationMember.objects.create(organization=org, user=user, role="member")
+            OrganizationMemberTeam.objects.create(
+                team=team, organizationmember=member, is_active=True
+            )
 
-        # delete all DSNs for the org so people don't send events
-        ProjectKey.objects.filter(project__organization=org).delete()
+            # delete all DSNs for the org so people don't send events
+            ProjectKey.objects.filter(project__organization=org).delete()
 
-        # update the date added to now so we reset the timer on deletion
-        demo_org.mark_assigned()
+            # update the date added to now so we reset the timer on deletion
+            demo_org.mark_assigned()
 
-    # build up the buffer
-    build_up_org_buffer.apply_async()
+        # build up the buffer
+        build_up_org_buffer.apply_async()
 
-    return (org, user)
+        return (org, user)

+ 4 - 0
src/sentry/demo/demo_start.py

@@ -1,10 +1,12 @@
 import logging
+import sentry_sdk
 
 from django.http import Http404
 from django.conf import settings
 
 from sentry.models import OrganizationMember, OrganizationStatus
 from sentry.utils import auth
+from sentry.web.decorators import transaction_start
 from sentry.web.frontend.base import BaseView
 
 
@@ -18,6 +20,7 @@ class DemoStartView(BaseView):
     csrf_protect = False
     auth_required = False
 
+    @transaction_start("DemoStartView")
     def post(self, request):
         # double check DEMO_MODE is disabled
         if not settings.DEMO_MODE:
@@ -27,6 +30,7 @@ class DemoStartView(BaseView):
         # see if the user already was assigned a member
         member_id = request.get_signed_cookie(MEMBER_ID_COOKIE, default="")
         logger.info("post.start", extra={"cookie_member_id": member_id})
+        sentry_sdk.set_tag("member_id", member_id)
 
         if member_id:
             try:

+ 3 - 0
src/sentry/demo/tasks.py

@@ -84,6 +84,9 @@ def delete_initializing_orgs(**kwargs):
         logger.info("delete_initializing_orgs.delete", extra={"organization_slug": org.slug})
         delete_organization.apply_async(kwargs={"object_id": org.id})
 
+    # build up the org buffer at the end to replace the orgs being removed
+    build_up_org_buffer()
+
 
 @instrumented_task(
     name="sentry.demo.tasks.build_up_org_buffer",

+ 4 - 1
tests/sentry/demo/test_tasks.py

@@ -136,7 +136,8 @@ class BuildUpOrgBufferTest(DemoTaskBaseClass):
 
 
 class DeleteInitializingOrgTest(DemoTaskBaseClass):
-    def test_basic(self):
+    @mock.patch("sentry.demo.tasks.build_up_org_buffer")
+    def test_basic(self, mock_build_up_org_buffer):
         before_cutoff = before_now(minutes=MAX_INITIALIZATION_TIME + 5)
         after_cutoff = before_now(minutes=MAX_INITIALIZATION_TIME - 5)
 
@@ -150,3 +151,5 @@ class DeleteInitializingOrgTest(DemoTaskBaseClass):
         assert not Organization.objects.filter(id=org1.id).exists()
         assert Organization.objects.filter(id=org2.id).exists()
         assert Organization.objects.filter(id=org3.id).exists()
+
+        mock_build_up_org_buffer.assert_called_once_with()