1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-02-24 05:26:23 +00:00

Resolve "Send information to baserow.io if the user agrees during the onboarding"

This commit is contained in:
Bram Wiepjes 2024-05-27 14:24:54 +00:00
parent 1b29f60191
commit 8288f32f8f
18 changed files with 311 additions and 32 deletions
backend
changelog/entries/unreleased/feature
premium/backend/src/baserow_premium/license
web-frontend/modules
core
database/components/onboarding

View file

@ -406,3 +406,22 @@ class DashboardSerializer(serializers.Serializer):
many=True, source="workspace_invitations"
)
workspace_invitations = UserWorkspaceInvitationSerializer(many=True)
class ShareOnboardingDetailsWithBaserowSerializer(serializers.Serializer):
team = serializers.CharField(
help_text="The team that the user has chosen during the onboarding.",
required=True,
)
role = serializers.CharField(
help_text="The role that the user has chosen during the onboarding",
required=True,
)
size = serializers.CharField(
help_text="The company size that the user has chosen during the onboarding.",
required=True,
)
country = serializers.CharField(
help_text="The country that the user has chosen during the onboarding.",
required=True,
)

View file

@ -12,6 +12,7 @@ from .views import (
ScheduleAccountDeletionView,
SendResetPasswordEmailView,
SendVerifyEmailView,
ShareOnboardingDetailsWithBaserowView,
UndoView,
UserView,
VerifyEmailAddressView,
@ -49,5 +50,10 @@ urlpatterns = [
re_path(r"^dashboard/$", DashboardView.as_view(), name="dashboard"),
re_path(r"^undo/$", UndoView.as_view(), name="undo"),
re_path(r"^redo/$", RedoView.as_view(), name="redo"),
re_path(
r"^share-onboarding-details-with-baserow/$",
ShareOnboardingDetailsWithBaserowView.as_view(),
name="share_onboarding_details_with_baserow",
),
re_path(r"^$", UserView.as_view(), name="index"),
]

View file

@ -113,6 +113,7 @@ from .serializers import (
ResetPasswordBodyValidationSerializer,
SendResetPasswordEmailBodyValidationSerializer,
SendVerifyEmailAddressSerializer,
ShareOnboardingDetailsWithBaserowSerializer,
TokenBlacklistSerializer,
TokenObtainPairWithUserSerializer,
TokenRefreshWithUserSerializer,
@ -752,3 +753,17 @@ class RedoView(APIView):
redone_actions = ActionHandler.redo(request.user, data, session_id)
serializer = UndoRedoResponseSerializer({"actions": redone_actions})
return Response(serializer.data, status=200)
class ShareOnboardingDetailsWithBaserowView(APIView):
permission_classes = (IsAuthenticated,)
@extend_schema(exclude=True)
@transaction.atomic
@validate_body(ShareOnboardingDetailsWithBaserowSerializer)
def post(self, request, data):
UserHandler().start_share_onboarding_details_with_baserow(
request.user, data["team"], data["role"], data["size"], data["country"]
)
return Response(status=204)

View file

@ -1149,6 +1149,7 @@ BASEROW_PERSONAL_VIEW_LOWEST_ROLE_ALLOWED = (
)
LICENSE_AUTHORITY_CHECK_TIMEOUT_SECONDS = 10
ADDITIONAL_INFORMATION_TIMEOUT_SECONDS = 10
MAX_NUMBER_CALENDAR_DAYS = 45

View file

@ -13,7 +13,34 @@ from baserow.core.models import Workspace
from baserow.core.utils import exception_capturer
def capture_event(
def capture_event(distinct_id: str, event: str, properties: dict):
"""
Capture a single Posthog event.
:param distinct_id: The distinct ID of the event.
:param event: The event type name.
:param properties: A dictionary containing all properties that must be added to
the event.
"""
if not settings.POSTHOG_ENABLED:
return
try:
posthog.capture(
distinct_id=distinct_id,
event=event,
properties=properties,
)
except Exception as e:
logger.warning(
"Failed to log to Posthog because of {e}.",
e=str(e),
)
exception_capturer(e)
def capture_user_event(
user: AbstractUser,
event: str,
properties: dict,
@ -21,7 +48,7 @@ def capture_event(
workspace: Optional[Workspace] = None,
):
"""
Captures a Posthog event in a consistent property format.
Captures a Posthog event of a user in a consistent property format.
:param user: The user that performed the event.
:param event: Unique name identifying the event.
@ -33,9 +60,6 @@ def capture_event(
:param workspace: Optionally the workspace related to the event.
"""
if not settings.POSTHOG_ENABLED:
return
properties["user_email"] = user.email
if session is not None:
@ -44,18 +68,7 @@ def capture_event(
if workspace is not None:
properties["workspace_id"] = workspace.id
try:
posthog.capture(
distinct_id=user.id,
event=event,
properties=properties,
)
except Exception as e:
logger.error(
"Failed to log to Posthog because of {e}.",
e=str(e),
)
exception_capturer(e)
capture_event(user.id, event, properties)
@receiver(action_done)
@ -78,6 +91,6 @@ def capture_event_action_done(
key: action_params_copy.get(key, None)
for key in action_type.analytics_params
}
capture_event(
capture_user_event(
user, action_type.type, properties, workspace=workspace, session=session
)

View file

@ -11,7 +11,10 @@ from .trash.tasks import (
setup_period_trash_tasks,
)
from .usage.tasks import run_calculate_storage
from .user.tasks import check_pending_account_deletion
from .user.tasks import (
check_pending_account_deletion,
share_onboarding_details_with_baserow,
)
@app.task(
@ -36,4 +39,5 @@ __all__ = [
"check_pending_account_deletion",
"delete_expired_snapshots",
"initialize_otel",
"share_onboarding_details_with_baserow",
]

View file

@ -14,8 +14,11 @@ from django.db.utils import IntegrityError
from django.utils import timezone, translation
from django.utils.translation import gettext as _
import requests
from itsdangerous import URLSafeSerializer, URLSafeTimedSerializer
from loguru import logger
from opentelemetry import trace
from requests.exceptions import RequestException
from baserow.core.auth_provider.handler import PasswordProviderHandler
from baserow.core.auth_provider.models import AuthProviderModel
@ -43,7 +46,7 @@ from baserow.core.signals import (
user_updated,
)
from baserow.core.trash.handler import TrashHandler
from baserow.core.utils import generate_hash
from baserow.core.utils import generate_hash, get_baserow_saas_base_url
from ..telemetry.utils import baserow_trace_methods
from .emails import (
@ -66,6 +69,7 @@ from .exceptions import (
UserNotFound,
)
from .signals import user_password_changed
from .tasks import share_onboarding_details_with_baserow
from .utils import normalize_email_address
User = get_user_model()
@ -776,3 +780,61 @@ class UserHandler(metaclass=baserow_trace_methods(tracer)):
confirm_url=confirm_url,
)
email.send(fail_silently=False)
def start_share_onboarding_details_with_baserow(
self, user, team: str, role: str, size: str, country: str
):
"""
Starts a celery task that shares some user information with baserow.io. Note
that this is only triggered if the user given permission during the onboarding
in the web-frontend.
:param user: The user on whose behalf the information is shared.
:param team: The team type that the user shared.
:param role: The role that the user shared.
:param size: The company size that the user shared.
:param country: The country name that the user shared.
"""
email = user.email
share_onboarding_details_with_baserow.delay(
email=email, team=team, role=role, size=size, country=country
)
def share_onboarding_details_with_baserow(self, email, team, role, size, country):
"""
Makes an API request to baserow.io that shares the additional information. Note
that this is only triggered if the user given permission during the onboarding
in the web-frontend.
:param team: The team type that the user shared.
:param role: The role that the user shared.
:param size: The company size that the user shared.
:param country: The country name that the user shared.
"""
settings_object = CoreHandler().get_settings()
base_url, headers = get_baserow_saas_base_url()
authority_url = f"{base_url}/api/saas/onboarding/additional-details/"
try:
response = requests.post(
authority_url,
json={
"team": team,
"role": role,
"size": size,
"country": country,
"email": email,
"instance_id": settings_object.instance_id,
},
timeout=settings.ADDITIONAL_INFORMATION_TIMEOUT_SECONDS,
headers=headers,
)
response.raise_for_status()
except RequestException:
logger.warning(
"The onboarding details could not be shared with the Baserow team "
"because the SaaS environment could not be reached."
)

View file

@ -47,6 +47,17 @@ def clean_up_user_log_entry(self):
UserHandler().delete_user_log_entries_older_than(cutoff_datetime)
@app.task(bind=True, queue="export")
def share_onboarding_details_with_baserow(self, **kwargs):
"""
Task wrapper that calls the `share_onboarding_details_with_baserow` method.
"""
from baserow.core.user.handler import UserHandler
UserHandler().share_onboarding_details_with_baserow(**kwargs)
# noinspection PyUnusedLocal
@app.on_after_finalize.connect
def setup_periodic_tasks(sender, **kwargs):

View file

@ -15,6 +15,7 @@ from itertools import islice
from numbers import Number
from typing import Any, Dict, Iterable, List, Optional, Tuple, Type, Union
from django.conf import settings
from django.db import transaction
from django.db.models import ForeignKey, ManyToManyField, Model
from django.db.models.fields import NOT_PROVIDED
@ -1017,3 +1018,22 @@ def escape_csv_cell(payload):
payload = payload.replace("|", "\\|")
payload = "'" + payload
return payload
def get_baserow_saas_base_url() -> [str, dict]:
"""
Returns the base url of the Baserow SaaS host. In production we always want to
connect to api.baserow.io, but in development to the saas dev env for testing
purposes.
:return: The base url and the headers object that must be added to the request.
"""
base_url = "https://api.baserow.io"
headers = {}
if settings.DEBUG:
base_url = "http://baserow-saas-backend:8000"
headers["Host"] = "localhost"
return base_url, headers

View file

@ -1167,3 +1167,32 @@ def test_send_verify_email_address_inactive_user(client, data_fixture, mailoutbo
assert response.status_code == HTTP_204_NO_CONTENT
assert len(mailoutbox) == 0
@pytest.mark.django_db
@patch("baserow.core.user.handler.share_onboarding_details_with_baserow")
def test_share_onboarding_details_with_baserow(mock_task, client, data_fixture):
data_fixture.update_settings(instance_id="1")
user, token = data_fixture.create_user_and_token()
response = client.post(
reverse("api:user:share_onboarding_details_with_baserow"),
{
"team": "Marketing",
"role": "CEO",
"size": "11 - 50",
"country": "The Netherlands",
},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_204_NO_CONTENT
mock_task.delay.assert_called_with(
email=user.email,
team="Marketing",
role="CEO",
size="11 - 50",
country="The Netherlands",
)

View file

@ -3,6 +3,7 @@ from io import BytesIO
from unittest.mock import MagicMock, patch
from django.db import OperationalError
from django.test.utils import override_settings
import pytest
@ -20,6 +21,7 @@ from baserow.core.utils import (
extract_allowed,
find_intermediate_order,
find_unused_name,
get_baserow_saas_base_url,
get_value_at_path,
grouper,
random_string,
@ -615,3 +617,16 @@ def test_safe_sample_payloads(input):
@pytest.mark.parametrize("input", [1, 2, True])
def test_safe_nonstr_sample_payloads(input):
assert escape_csv_cell(input) == input
@override_settings(DEBUG=False)
def test_get_baserow_saas_base_url_without_debug():
assert get_baserow_saas_base_url() == ("https://api.baserow.io", {})
@override_settings(DEBUG=True)
def test_get_baserow_saas_base_url_with_debug():
assert get_baserow_saas_base_url() == (
"http://baserow-saas-backend:8000",
{"Host": "localhost"},
)

View file

@ -6,7 +6,7 @@ import pytest
from baserow.core.action.registries import ActionType
from baserow.core.action.signals import ActionCommandType
from baserow.core.posthog import capture_event, capture_event_action_done
from baserow.core.posthog import capture_event_action_done, capture_user_event
class TestActionType(ActionType):
@ -29,7 +29,7 @@ class TestActionType(ActionType):
@patch("baserow.core.posthog.posthog")
def test_not_capture_event_if_not_enabled(mock_posthog, data_fixture):
user = data_fixture.create_user()
capture_event(user, "test", {})
capture_user_event(user, "test", {})
mock_posthog.capture.assert_not_called()
@ -39,7 +39,7 @@ def test_not_capture_event_if_not_enabled(mock_posthog, data_fixture):
def test_capture_event_if_enabled(mock_posthog, data_fixture):
user = data_fixture.create_user()
workspace = data_fixture.create_workspace()
capture_event(user, "test", {}, session="session", workspace=workspace)
capture_user_event(user, "test", {}, session="session", workspace=workspace)
mock_posthog.capture.assert_called_once_with(
distinct_id=user.id,
event="test",
@ -52,7 +52,7 @@ def test_capture_event_if_enabled(mock_posthog, data_fixture):
@pytest.mark.django_db
@patch("baserow.core.posthog.capture_event")
@patch("baserow.core.posthog.capture_user_event")
def test_capture_event_action_done(mock_capture_event, data_fixture):
user = data_fixture.create_user()

View file

@ -9,8 +9,10 @@ from django.db import connections, transaction
from django.test import override_settings
import pytest
import responses
from freezegun import freeze_time
from itsdangerous.exc import BadSignature, SignatureExpired
from responses import json_params_matcher
from baserow.contrib.database.fields.models import SelectOption
from baserow.contrib.database.models import Database, Table
@ -783,3 +785,56 @@ def test_send_email_pending_verification_already_verified(data_fixture):
with pytest.raises(EmailAlreadyVerified):
UserHandler().send_email_pending_verification(user)
@pytest.mark.django_db
@patch("baserow.core.user.handler.share_onboarding_details_with_baserow")
def test_start_share_onboarding_details_with_baserow(mock_task, data_fixture):
user = data_fixture.create_user()
UserHandler().start_share_onboarding_details_with_baserow(
user, "Marketing", "CEO", "11 - 50", "The Netherlands"
)
mock_task.delay.assert_called_with(
email=user.email,
team="Marketing",
role="CEO",
size="11 - 50",
country="The Netherlands",
)
@pytest.mark.django_db
@override_settings(DEBUG=True)
@responses.activate
def test_share_onboarding_details_with_baserow_valid_response(data_fixture):
data_fixture.update_settings(instance_id="1")
response1 = responses.add(
responses.POST,
"http://baserow-saas-backend:8000/api/saas/onboarding/additional-details/",
status=204,
match=[
json_params_matcher(
{
"email": "test@test.nl",
"team": "Marketing",
"role": "CEO",
"size": "11 - 50",
"country": "The Netherlands",
"instance_id": "1",
}
)
],
)
UserHandler().share_onboarding_details_with_baserow(
email="test@test.nl",
team="Marketing",
role="CEO",
size="11 - 50",
country="The Netherlands",
)
assert response1.call_count == 1

View file

@ -0,0 +1,7 @@
{
"type": "feature",
"message": "Send information to baserow.io if user agrees during onboarding.",
"issue_number": 2412,
"bullet_points": [],
"created_at": "2024-04-26"
}

View file

@ -34,6 +34,7 @@ from baserow.core.exceptions import IsNotAdminError
from baserow.core.handler import CoreHandler
from baserow.core.models import Workspace
from baserow.core.registries import plugin_registry
from baserow.core.utils import get_baserow_saas_base_url
from baserow.ws.signals import broadcast_to_users
from .constants import (
@ -292,13 +293,7 @@ class LicenseHandler:
settings_object = CoreHandler().get_settings()
try:
base_url = "https://api.baserow.io"
headers = {}
if settings.DEBUG:
base_url = "http://baserow-saas-backend:8000"
headers["Host"] = "localhost"
base_url, headers = get_baserow_saas_base_url()
authority_url = f"{base_url}/api/saas/licenses/check/"
response = requests.post(

View file

@ -7,6 +7,7 @@ import TeamStep from '@baserow/modules/core/components/onboarding/TeamStep'
import WorkspaceStep from '@baserow/modules/core/components/onboarding/WorkspaceStep'
import AppLayoutPreview from '@baserow/modules/core/components/onboarding/AppLayoutPreview'
import WorkspaceService from '@baserow/modules/core/services/workspace'
import AuthService from '@baserow/modules/core/services/auth'
import { MemberRoleType } from '@baserow/modules/database/roleTypes'
export class OnboardingType extends Registerable {
@ -132,6 +133,22 @@ export class MoreOnboardingType extends OnboardingType {
canSkip() {
return true
}
async complete(data, responses) {
const teamData = data[TeamOnboardingType.getType()]
const moreData = data[this.getType()]
const share = moreData?.share
if (share) {
const team = teamData?.team || 'undefined'
await AuthService(this.app.$client).shareOnboardingDetailsWithBaserow(
team,
moreData.role,
moreData.companySize,
moreData.country
)
}
}
}
export class WorkspaceOnboardingType extends OnboardingType {

View file

@ -79,5 +79,13 @@ export default (client) => {
deleteAccount() {
return client.post('/user/schedule-account-deletion/')
},
shareOnboardingDetailsWithBaserow(team, role, size, country) {
return client.post('/user/share-onboarding-details-with-baserow/', {
team,
role,
size,
country,
})
},
}
}

View file

@ -107,7 +107,9 @@ export default {
this.what !== value &&
Object.prototype.hasOwnProperty.call(this.whatItems, value)
) {
this.rows = this.whatItems[value]
this.row0 = this.whatItems[value][0]
this.row1 = this.whatItems[value][1]
this.row2 = this.whatItems[value][2]
}
this.what = value
this.updateValue()