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:
parent
1b29f60191
commit
8288f32f8f
18 changed files with 311 additions and 32 deletions
backend
src/baserow
api/user
config/settings
core
tests/baserow
changelog/entries/unreleased/feature
premium/backend/src/baserow_premium/license
web-frontend/modules
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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"),
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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."
|
||||
)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
|
|
|
@ -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"},
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue