Skip to content

Commit

Permalink
User invitations v2: Send segment event when an invitation is created…
Browse files Browse the repository at this point in the history
… (#11893)
  • Loading branch information
pmossman committed Apr 1, 2024
1 parent d14be9e commit 62c881f
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

package io.airbyte.server.handlers;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import io.airbyte.analytics.TrackingClient;
import io.airbyte.api.model.generated.InviteCodeRequestBody;
import io.airbyte.api.model.generated.PermissionCreate;
import io.airbyte.api.model.generated.PermissionType;
import io.airbyte.api.model.generated.UserInvitationCreateRequestBody;
import io.airbyte.api.model.generated.UserInvitationCreateResponse;
import io.airbyte.api.model.generated.UserInvitationListRequestBody;
Expand Down Expand Up @@ -48,8 +51,8 @@
public class UserInvitationHandler {

static final String ACCEPT_INVITE_PATH = "/accept-invite?inviteCode=";

static final int INVITE_EXPIRATION_DAYS = 7;
static final String USER_INVITED = "User Invited";

final UserInvitationService service;
final UserInvitationMapper mapper;
Expand All @@ -60,6 +63,7 @@ public class UserInvitationHandler {
final UserPersistence userPersistence;
final PermissionPersistence permissionPersistence;
final PermissionHandler permissionHandler;
final TrackingClient trackingClient;

public UserInvitationHandler(final UserInvitationService service,
final UserInvitationMapper mapper,
Expand All @@ -69,7 +73,8 @@ public UserInvitationHandler(final UserInvitationService service,
final OrganizationService organizationService,
final UserPersistence userPersistence,
final PermissionPersistence permissionPersistence,
final PermissionHandler permissionHandler) {
final PermissionHandler permissionHandler,
final TrackingClient trackingClient) {
this.service = service;
this.mapper = mapper;
this.webUrlHelper = webUrlHelper;
Expand All @@ -79,6 +84,7 @@ public UserInvitationHandler(final UserInvitationService service,
this.userPersistence = userPersistence;
this.permissionPersistence = permissionPersistence;
this.permissionHandler = permissionHandler;
this.trackingClient = trackingClient;
}

public UserInvitationRead getByInviteCode(final String inviteCode, final User currentUser) {
Expand Down Expand Up @@ -108,20 +114,62 @@ public List<UserInvitationRead> getPendingInvitations(final UserInvitationListRe
public UserInvitationCreateResponse createInvitationOrPermission(final UserInvitationCreateRequestBody req, final User currentUser)
throws IOException, JsonValidationException, ConfigNotFoundException {

final UserInvitationCreateResponse response;
final boolean wasDirectAdd = attemptDirectAddEmailToOrg(req, currentUser);

if (wasDirectAdd) {
return new UserInvitationCreateResponse().directlyAdded(true);
} else {
try {
final UserInvitation invitation = createUserInvitationForNewOrgEmail(req, currentUser);
response = new UserInvitationCreateResponse().directlyAdded(false).inviteCode(invitation.getInviteCode());
trackUserInvited(req, currentUser);
return response;
} catch (final InvitationDuplicateException e) {
throw new ConflictException(e.getMessage());
}
}
}

private void trackUserInvited(final UserInvitationCreateRequestBody requestBody, final User currentUser) {
try {
final UserInvitation invitation = createUserInvitationForNewOrgEmail(req, currentUser);
return new UserInvitationCreateResponse().directlyAdded(false).inviteCode(invitation.getInviteCode());
} catch (final InvitationDuplicateException e) {
throw new ConflictException(e.getMessage());
switch (requestBody.getScopeType()) {
case ORGANIZATION -> {
// Implement once we support org-level invitations
}
case WORKSPACE -> trackUserInvitedToWorkspace(requestBody.getScopeId(),
requestBody.getInvitedEmail(),
currentUser.getEmail(),
currentUser.getUserId(),
getInvitedResourceName(requestBody),
requestBody.getPermissionType());
default -> throw new IllegalArgumentException("Unexpected scope type: " + requestBody.getScopeType());
}
} catch (final Exception e) {
// log the error, but don't throw an exception to prevent a user-facing error
log.error("Failed to track user invited", e);
}
}

private void trackUserInvitedToWorkspace(final UUID workspaceId,
final String email,
final String inviterUserEmail,
final UUID inviterUserId,
final String workspaceName,
final PermissionType permissionType) {
trackingClient.track(workspaceId,
USER_INVITED,
ImmutableMap.<String, Object>builder()
.put("email", email)
.put("inviter_user_email", inviterUserEmail)
.put("inviter_user_id", inviterUserId)
.put("role", permissionType)
.put("workspace_id", workspaceId)
.put("workspace_name", workspaceName)
.put("invited_from", "unspecified") // Note: currently we don't have a way to specify this, carryover from old cloud-only invite system
.build());
}

/**
* Attempts to add the invited email address to the requested workspace/organization directly.
* Searches for existing users with the invited email address, who are also currently members of the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,25 @@

import static io.airbyte.config.Permission.PermissionType.WORKSPACE_ADMIN;
import static io.airbyte.server.handlers.UserInvitationHandler.ACCEPT_INVITE_PATH;
import static io.airbyte.server.handlers.UserInvitationHandler.USER_INVITED;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import io.airbyte.analytics.TrackingClient;
import io.airbyte.api.model.generated.InviteCodeRequestBody;
import io.airbyte.api.model.generated.PermissionCreate;
import io.airbyte.api.model.generated.PermissionType;
Expand Down Expand Up @@ -82,13 +86,15 @@ public class UserInvitationHandlerTest {
PermissionPersistence permissionPersistence;
@Mock
PermissionHandler permissionHandler;
@Mock
TrackingClient trackingClient;

UserInvitationHandler handler;

@BeforeEach
void setup() {
handler = new UserInvitationHandler(service, mapper, customerIoEmailNotificationSender, webUrlHelper, workspaceService, organizationService,
userPersistence, permissionPersistence, permissionHandler);
userPersistence, permissionPersistence, permissionHandler, trackingClient);
}

@Nested
Expand Down Expand Up @@ -118,6 +124,7 @@ class CreateAndSendInvitation {
private void setupSendInvitationMocks() throws Exception {
when(webUrlHelper.getBaseUrl()).thenReturn(WEBAPP_BASE_URL);
when(service.createUserInvitation(USER_INVITATION)).thenReturn(USER_INVITATION);
when(workspaceService.getStandardWorkspaceNoSecrets(WORKSPACE_ID, false)).thenReturn(new StandardWorkspace().withName(WORKSPACE_NAME));
}

@BeforeEach
Expand Down Expand Up @@ -224,6 +231,12 @@ private void verifyInvitationCreatedAndEmailSentResult(final UserInvitationCreat
// make sure the final result is correct
assertFalse(result.getDirectlyAdded());
assertEquals(capturedUserInvitation.getInviteCode(), result.getInviteCode());

// verify we sent an invitation tracking event
verify(trackingClient, times(1)).track(
eq(WORKSPACE_ID),
eq(USER_INVITED),
anyMap());
}

}
Expand Down Expand Up @@ -295,6 +308,9 @@ private void verifyPermissionAddedResult(final Set<UUID> expectedUserIds, final
// make sure the final result is correct
assertTrue(result.getDirectlyAdded());
assertNull(result.getInviteCode());

// we don't send a "user invited" event when a user is directly added to a workspace.
verify(trackingClient, never()).track(any(), any(), any());
}

}
Expand Down

0 comments on commit 62c881f

Please sign in to comment.