Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions src/sentry/api/endpoints/organization_onboarding_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
from rest_framework.request import Request
from rest_framework.response import Response

from sentry import onboarding_tasks
from sentry.api.base import region_silo_endpoint
from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission
from sentry.models import OnboardingTaskStatus, OrganizationOnboardingTask
from sentry.receivers import try_mark_onboarding_complete
from sentry.models import OnboardingTaskStatus


class OnboardingTaskPermission(OrganizationPermission):
Expand All @@ -17,9 +17,9 @@ class OrganizationOnboardingTaskEndpoint(OrganizationEndpoint):
permission_classes = (OnboardingTaskPermission,)

def post(self, request: Request, organization) -> Response:
try:
task_id = OrganizationOnboardingTask.TASK_LOOKUP_BY_KEY[request.data["task"]]
except KeyError:

task_id = onboarding_tasks.get_task_lookup_by_key(request.data["task"])
if task_id is None:
return Response({"detail": "Invalid task key"}, status=422)

status_value = request.data.get("status")
Expand All @@ -28,15 +28,15 @@ def post(self, request: Request, organization) -> Response:
if status_value is None and completion_seen is None:
return Response({"detail": "completionSeen or status must be provided"}, status=422)

status = OrganizationOnboardingTask.STATUS_LOOKUP_BY_KEY.get(status_value)
status = onboarding_tasks.get_status_lookup_by_key(status_value)

if status_value and status is None:
return Response({"detail": "Invalid status key"}, status=422)

# Cannot skip unskippable tasks
if (
status == OnboardingTaskStatus.SKIPPED
and task_id not in OrganizationOnboardingTask.SKIPPABLE_TASKS
and task_id not in onboarding_tasks.get_skippable_tasks()
):
return Response(status=422)

Expand All @@ -48,14 +48,14 @@ def post(self, request: Request, organization) -> Response:
if completion_seen:
values["completion_seen"] = timezone.now()

rows_affected, created = OrganizationOnboardingTask.objects.create_or_update(
rows_affected, created = onboarding_tasks.create_or_update_onboarding_task(
organization=organization,
task=task_id,
user=request.user,
values=values,
defaults={"user_id": request.user.id},
)

if rows_affected or created:
try_mark_onboarding_complete(organization.id)
onboarding_tasks.try_mark_onboarding_complete(organization.id)

return Response(status=204)
24 changes: 17 additions & 7 deletions src/sentry/api/serializers/models/organization.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
from __future__ import annotations

from collections.abc import Mapping, MutableMapping, Sequence
from datetime import datetime
from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, Union, cast
from typing import (
TYPE_CHECKING,
Any,
Callable,
List,
Mapping,
MutableMapping,
Optional,
Sequence,
Tuple,
Union,
cast,
)

from rest_framework import serializers
from sentry_relay.auth import PublicKey
from sentry_relay.exceptions import RelayError
from typing_extensions import TypedDict

from sentry import features, quotas, roles
from sentry import features, onboarding_tasks, quotas, roles
from sentry.api.serializers import Serializer, register, serialize
from sentry.api.serializers.models.project import ProjectSerializerResponse
from sentry.api.serializers.models.role import (
Expand Down Expand Up @@ -339,6 +350,7 @@ class OnboardingTasksSerializerResponse(TypedDict):
data: Any # JSON object


@register(OrganizationOnboardingTask)
class OnboardingTasksSerializer(Serializer): # type: ignore
def get_attrs(
self, item_list: OrganizationOnboardingTask, user: User, **kwargs: Any
Expand Down Expand Up @@ -415,9 +427,7 @@ def serialize( # type: ignore

from sentry import experiments

onboarding_tasks = list(
OrganizationOnboardingTask.objects.filter(organization=obj).select_related("user")
)
tasks_to_serialize = list(onboarding_tasks.fetch_onboarding_tasks(obj, user))

experiment_assignments = experiments.all(org=obj, actor=user)

Expand Down Expand Up @@ -517,7 +527,7 @@ def serialize( # type: ignore
context["pendingAccessRequests"] = OrganizationAccessRequest.objects.filter(
team__organization=obj
).count()
context["onboardingTasks"] = serialize(onboarding_tasks, user, OnboardingTasksSerializer())
context["onboardingTasks"] = serialize(tasks_to_serialize, user)
return context


Expand Down
1 change: 1 addition & 0 deletions src/sentry/assistant/guides.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

# demo mode has different guides
if settings.DEMO_MODE:
# TODO: remove old guides
GUIDES = {
"sidebar": 20,
"issue_stream_v2": 21,
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/conf/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -3112,6 +3112,8 @@ def build_cdc_postgres_init_db_volume(settings):
# Set the URL for signup page that we redirect to for the setup wizard if signup=1 is in the query params
SENTRY_SIGNUP_URL = None

SENTRY_ORGANIZATION_ONBOARDING_TASK = "sentry.onboarding_tasks.backends.organization_onboarding_task.OrganizationOnboardingTaskBackend"

# Temporary allowlist for specially configured organizations to use the direct-storage
# driver.
SENTRY_REPLAYS_STORAGE_ALLOWLIST = []
6 changes: 6 additions & 0 deletions src/sentry/models/organizationonboardingtask.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from django.conf import settings
from django.core.cache import cache
from django.db import IntegrityError, models, transaction
Expand Down Expand Up @@ -99,6 +101,10 @@ class AbstractOnboardingTask(Model):
project = FlexibleForeignKey("sentry.Project", db_constraint=False, null=True)
data = JSONField() # INVITE_MEMBER { invited_member: user.id }

# fields for typing
TASK_LOOKUP_BY_KEY: dict[str, int]
SKIPPABLE_TASKS: frozenset[int]

class Meta:
abstract = True

Expand Down
20 changes: 20 additions & 0 deletions src/sentry/onboarding_tasks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from typing import TYPE_CHECKING

from django.conf import settings

from sentry.onboarding_tasks.base import OnboardingTaskBackend
from sentry.utils.services import LazyServiceWrapper

backend = LazyServiceWrapper(
OnboardingTaskBackend, settings.SENTRY_ORGANIZATION_ONBOARDING_TASK, {}
)
backend.expose(locals())

if TYPE_CHECKING:
__onboarding_task_backend = OnboardingTaskBackend()
get_task_lookup_by_key = __onboarding_task_backend.get_task_lookup_by_key
get_status_lookup_by_key = __onboarding_task_backend.get_status_lookup_by_key
get_skippable_tasks = __onboarding_task_backend.get_skippable_tasks
fetch_onboarding_tasks = __onboarding_task_backend.fetch_onboarding_tasks
create_or_update_onboarding_task = __onboarding_task_backend.create_or_update_onboarding_task
try_mark_onboarding_complete = __onboarding_task_backend.try_mark_onboarding_complete
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from django.db import IntegrityError, transaction
from django.db.models import Q
from django.utils import timezone

from sentry.models import OnboardingTaskStatus, OrganizationOnboardingTask, OrganizationOption
from sentry.onboarding_tasks.base import OnboardingTaskBackend
from sentry.utils import json


class OrganizationOnboardingTaskBackend(OnboardingTaskBackend):
Model = OrganizationOnboardingTask

def fetch_onboarding_tasks(self, organization, user):
return self.Model.objects.filter(organization=organization).select_related("user")

def create_or_update_onboarding_task(self, organization, user, task, values):
return self.Model.objects.create_or_update(
organization=organization,
task=task,
values=values,
defaults={"user_id": user.id},
)

def try_mark_onboarding_complete(self, organization_id):
if OrganizationOption.objects.filter(
organization_id=organization_id, key="onboarding:complete"
).exists():
return

completed = set(
OrganizationOnboardingTask.objects.filter(
Q(organization_id=organization_id)
& (Q(status=OnboardingTaskStatus.COMPLETE) | Q(status=OnboardingTaskStatus.SKIPPED))
).values_list("task", flat=True)
)
if completed >= OrganizationOnboardingTask.REQUIRED_ONBOARDING_TASKS:
try:
with transaction.atomic():
OrganizationOption.objects.create(
organization_id=organization_id,
key="onboarding:complete",
value={"updated": json.datetime_to_str(timezone.now())},
)
except IntegrityError:
pass
32 changes: 32 additions & 0 deletions src/sentry/onboarding_tasks/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from sentry.models.organizationonboardingtask import AbstractOnboardingTask
from sentry.utils.services import Service


class OnboardingTaskBackend(Service):
__all__ = (
"get_task_lookup_by_key",
"get_status_lookup_by_key",
"get_skippable_tasks",
"fetch_onboarding_tasks",
"create_or_update_onboarding_task",
"try_mark_onboarding_complete",
)
Model: AbstractOnboardingTask = AbstractOnboardingTask

def get_task_lookup_by_key(self, key):
return self.Model.TASK_LOOKUP_BY_KEY.get(key)

def get_status_lookup_by_key(self, key):
return self.Model.STATUS_LOOKUP_BY_KEY.get(key)

def get_skippable_tasks(self):
return self.Model.SKIPPABLE_TASKS

def fetch_onboarding_tasks(self, organization, user):
raise NotImplementedError

def create_or_update_onboarding_task(self, organization, user, task, values):
raise NotImplementedError

def try_mark_onboarding_complete(self, organization_id):
raise NotImplementedError
30 changes: 2 additions & 28 deletions src/sentry/receivers/onboarding.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
from datetime import datetime

import pytz
from django.db import IntegrityError, transaction
from django.db.models import F, Q
from django.db.models import F
from django.utils import timezone

from sentry import analytics
Expand All @@ -12,9 +11,9 @@
OnboardingTaskStatus,
Organization,
OrganizationOnboardingTask,
OrganizationOption,
Project,
)
from sentry.onboarding_tasks import try_mark_onboarding_complete
from sentry.plugins.bases import IssueTrackingPlugin, IssueTrackingPlugin2
from sentry.services.hybrid_cloud.user import RpcUser
from sentry.signals import (
Expand All @@ -36,7 +35,6 @@
project_created,
transaction_processed,
)
from sentry.utils import json
from sentry.utils.event import has_event_minified_stack_trace
from sentry.utils.javascript import has_sourcemap

Expand All @@ -49,30 +47,6 @@
)


def try_mark_onboarding_complete(organization_id):
if OrganizationOption.objects.filter(
organization_id=organization_id, key="onboarding:complete"
).exists():
return

completed = set(
OrganizationOnboardingTask.objects.filter(
Q(organization_id=organization_id)
& (Q(status=OnboardingTaskStatus.COMPLETE) | Q(status=OnboardingTaskStatus.SKIPPED))
).values_list("task", flat=True)
)
if completed >= OrganizationOnboardingTask.REQUIRED_ONBOARDING_TASKS:
try:
with transaction.atomic():
OrganizationOption.objects.create(
organization_id=organization_id,
key="onboarding:complete",
value={"updated": json.datetime_to_str(timezone.now())},
)
except IntegrityError:
pass


@project_created.connect(weak=False)
def record_new_project(project, user, **kwargs):
if user.is_authenticated:
Expand Down