Merge "Get rid of logging for autoclose bug."
diff --git a/java/com/google/gerrit/server/change/RebaseUtil.java b/java/com/google/gerrit/server/change/RebaseUtil.java
index 8acc925..65fef05 100644
--- a/java/com/google/gerrit/server/change/RebaseUtil.java
+++ b/java/com/google/gerrit/server/change/RebaseUtil.java
@@ -18,29 +18,38 @@
import com.google.common.flogger.FluentLogger;
import com.google.common.primitives.Ints;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.api.changes.RebaseInput;
import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.git.ObjectIds;
import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.RefPermission;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.server.update.BatchUpdate;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.IOException;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
@@ -50,6 +59,11 @@
public class RebaseUtil {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private final Provider<PersonIdent> serverIdent;
+ private final IdentifiedUser.GenericFactory userFactory;
+ private final PermissionBackend permissionBackend;
+ private final ChangeResource.Factory changeResourceFactory;
+ private final GitRepositoryManager repoManager;
private final Provider<InternalChangeQuery> queryProvider;
private final ChangeNotes.Factory notesFactory;
private final PatchSetUtil psUtil;
@@ -57,10 +71,20 @@
@Inject
RebaseUtil(
+ @GerritPersonIdent Provider<PersonIdent> serverIdent,
+ IdentifiedUser.GenericFactory userFactory,
+ PermissionBackend permissionBackend,
+ ChangeResource.Factory changeResourceFactory,
+ GitRepositoryManager repoManager,
Provider<InternalChangeQuery> queryProvider,
ChangeNotes.Factory notesFactory,
PatchSetUtil psUtil,
RebaseChangeOp.Factory rebaseFactory) {
+ this.serverIdent = serverIdent;
+ this.userFactory = userFactory;
+ this.permissionBackend = permissionBackend;
+ this.changeResourceFactory = changeResourceFactory;
+ this.repoManager = repoManager;
this.queryProvider = queryProvider;
this.notesFactory = notesFactory;
this.psUtil = psUtil;
@@ -68,6 +92,143 @@
}
/**
+ * Checks that the uploader has permissions to create a new patch set and creates a new {@link
+ * RevisionResource} that contains the uploader (aka the impersonated user) as the current user
+ * which can be used for {@link BatchUpdate} to do the rebase on behalf of the uploader.
+ *
+ * <p>The following permissions are required for the uploader:
+ *
+ * <ul>
+ * <li>The {@code Read} permission that allows to see the change.
+ * <li>The {@code Push} permission that allows upload.
+ * <li>The {@code Add Patch Set} permission, required if the change is owned by another user
+ * (change owners implicitly have this permission).
+ * <li>The {@code Forge Author} permission if the patch set that is rebased has a forged author
+ * (author != uploader).
+ * <li>The {@code Forge Server} permission if the patch set that is rebased has the server
+ * identity as the author.
+ * </ul>
+ *
+ * <p>Usually the uploader should have all these permission since they were already required for
+ * the original upload, but there is the edge case that the uploader had the permission when doing
+ * the original upload and then the permission was revoked.
+ *
+ * <p>Note that patch sets with a forged committer (committer != uploader) can be rebased on
+ * behalf of the uploader, even if the uploader doesn't have the {@code Forge Committer}
+ * permission. This is because on rebase on behalf of the uploader the uploader will become the
+ * committer of the new rebased patch set, hence for the rebased patch set the committer is no
+ * longer forged (committer == uploader) and hence the {@code Forge Committer} permission is not
+ * required.
+ *
+ * <p>Note that the {@code Rebase} permission is not required for the uploader since the {@code
+ * Rebase} permission is specifically about allowing a user to do a rebase via the web UI by
+ * clicking on the {@code REBASE} button and the uploader is not clicking on this button.
+ *
+ * <p>The permissions of the uploader are checked explicitly here so that we can return a {@code
+ * 409 Conflict} response with a proper error message if they are missing (the error message says
+ * that the permission is missing for the uploader). The normal code path also checks these
+ * permission but the exception thrown there would result in a {@code 403 Forbidden} response and
+ * the error message would wrongly look like the caller (i.e. the rebaser) is missing the
+ * permission.
+ *
+ * <p>Note that this method doesn't check permissions for the rebaser (aka the impersonating user
+ * aka the calling user). Callers should check the permissions for the rebaser before calling this
+ * method.
+ *
+ * @param rsrc the revision resource that should be rebased
+ * @param rebaseInput the request input containing options for the rebase
+ * @return revision resource that contains the uploader (aka the impersonated user) as the current
+ * user which can be used for {@link BatchUpdate} to do the rebase on behalf of the uploader
+ */
+ public RevisionResource onBehalfOf(RevisionResource rsrc, RebaseInput rebaseInput)
+ throws IOException, PermissionBackendException, BadRequestException,
+ ResourceConflictException {
+ if (rsrc.getPatchSet().id().get() != rsrc.getChange().currentPatchSetId().get()) {
+ throw new BadRequestException(
+ "non-current patch set cannot be rebased on behalf of the uploader");
+ }
+ if (rebaseInput.allowConflicts) {
+ throw new BadRequestException(
+ "allow_conflicts and on_behalf_of_uploader are mutually exclusive");
+ }
+
+ CurrentUser caller = rsrc.getUser();
+ Account.Id uploaderId = rsrc.getPatchSet().uploader();
+ IdentifiedUser uploader = userFactory.runAs(/*remotePeer= */ null, uploaderId, caller);
+ logger.atFine().log(
+ "%s is rebasing patch set %s of project %s on behalf of uploader %s",
+ caller.getLoggableName(),
+ rsrc.getPatchSet().id(),
+ rsrc.getProject(),
+ uploader.getLoggableName());
+
+ checkPermissionForUploader(
+ uploader,
+ rsrc.getNotes(),
+ ChangePermission.READ,
+ String.format("uploader %s cannot read change", uploader.getLoggableName()));
+ checkPermissionForUploader(
+ uploader,
+ rsrc.getNotes(),
+ ChangePermission.ADD_PATCH_SET,
+ String.format("uploader %s cannot add patch set", uploader.getLoggableName()));
+
+ try (Repository repo = repoManager.openRepository(rsrc.getProject())) {
+ RevCommit commit = repo.parseCommit(rsrc.getPatchSet().commitId());
+
+ if (!uploader.hasEmailAddress(commit.getAuthorIdent().getEmailAddress())) {
+ checkPermissionForUploader(
+ uploader,
+ rsrc.getNotes(),
+ RefPermission.FORGE_AUTHOR,
+ String.format(
+ "author of patch set %d is forged and the uploader %s cannot forge author",
+ rsrc.getPatchSet().id().get(), uploader.getLoggableName()));
+
+ if (serverIdent.get().getEmailAddress().equals(commit.getAuthorIdent().getEmailAddress())) {
+ checkPermissionForUploader(
+ uploader,
+ rsrc.getNotes(),
+ RefPermission.FORGE_SERVER,
+ String.format(
+ "author of patch set %d is the server identity and the uploader %s cannot forge"
+ + " the server identity",
+ rsrc.getPatchSet().id().get(), uploader.getLoggableName()));
+ }
+ }
+ }
+
+ return new RevisionResource(
+ changeResourceFactory.create(rsrc.getNotes(), uploader), rsrc.getPatchSet());
+ }
+
+ private void checkPermissionForUploader(
+ IdentifiedUser uploader,
+ ChangeNotes changeNotes,
+ ChangePermission changePermission,
+ String errorMessage)
+ throws PermissionBackendException, ResourceConflictException {
+ try {
+ permissionBackend.user(uploader).change(changeNotes).check(changePermission);
+ } catch (AuthException e) {
+ throw new ResourceConflictException(errorMessage, e);
+ }
+ }
+
+ private void checkPermissionForUploader(
+ IdentifiedUser uploader,
+ ChangeNotes changeNotes,
+ RefPermission refPermission,
+ String errorMessage)
+ throws PermissionBackendException, ResourceConflictException {
+ try {
+ permissionBackend.user(uploader).ref(changeNotes.getChange().getDest()).check(refPermission);
+ } catch (AuthException e) {
+ throw new ResourceConflictException(errorMessage, e);
+ }
+ }
+
+ /**
* Checks whether the given change fulfills all preconditions to be rebased.
*
* <p>This method does not check whether the calling user is allowed to rebase the change.
diff --git a/java/com/google/gerrit/server/restapi/change/Rebase.java b/java/com/google/gerrit/server/restapi/change/Rebase.java
index 5368c75..3cb1870 100644
--- a/java/com/google/gerrit/server/restapi/change/Rebase.java
+++ b/java/com/google/gerrit/server/restapi/change/Rebase.java
@@ -19,23 +19,16 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.extensions.api.changes.RebaseInput;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeResource;
@@ -45,36 +38,28 @@
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.permissions.RefPermission;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.UpdateException;
import com.google.gerrit.server.update.context.RefUpdateContext;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.inject.Inject;
-import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
@Singleton
public class Rebase
implements RestModifyView<RevisionResource, RebaseInput>, UiAction<RevisionResource> {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
private static final ImmutableSet<ListChangesOption> OPTIONS =
Sets.immutableEnumSet(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT);
- private final Provider<PersonIdent> serverIdent;
private final BatchUpdate.Factory updateFactory;
private final GitRepositoryManager repoManager;
private final RebaseUtil rebaseUtil;
@@ -82,13 +67,10 @@
private final PermissionBackend permissionBackend;
private final ProjectCache projectCache;
private final PatchSetUtil patchSetUtil;
- private final IdentifiedUser.GenericFactory userFactory;
- private final ChangeResource.Factory changeResourceFactory;
private final RebaseMetrics rebaseMetrics;
@Inject
public Rebase(
- @GerritPersonIdent Provider<PersonIdent> serverIdent,
BatchUpdate.Factory updateFactory,
GitRepositoryManager repoManager,
RebaseUtil rebaseUtil,
@@ -96,10 +78,7 @@
PermissionBackend permissionBackend,
ProjectCache projectCache,
PatchSetUtil patchSetUtil,
- IdentifiedUser.GenericFactory userFactory,
- ChangeResource.Factory changeResourceFactory,
RebaseMetrics rebaseMetrics) {
- this.serverIdent = serverIdent;
this.updateFactory = updateFactory;
this.repoManager = repoManager;
this.rebaseUtil = rebaseUtil;
@@ -107,8 +86,6 @@
this.permissionBackend = permissionBackend;
this.projectCache = projectCache;
this.patchSetUtil = patchSetUtil;
- this.userFactory = userFactory;
- this.changeResourceFactory = changeResourceFactory;
this.rebaseMetrics = rebaseMetrics;
}
@@ -118,7 +95,7 @@
if (input.onBehalfOfUploader && !rsrc.getPatchSet().uploader().equals(rsrc.getAccountId())) {
rsrc.permissions().check(ChangePermission.REBASE_ON_BEHALF_OF_UPLOADER);
- rsrc = onBehalfOf(rsrc, input);
+ rsrc = rebaseUtil.onBehalfOf(rsrc, input);
} else {
rsrc.permissions().check(ChangePermission.REBASE);
}
@@ -160,143 +137,6 @@
}
}
- /**
- * Checks that the uploader has permissions to create a new patch set and creates a new {@link
- * RevisionResource} that contains the uploader (aka the impersonated user) as the current user
- * which can be used for {@link BatchUpdate} to do the rebase on behalf of the uploader.
- *
- * <p>The following permissions are required for the uploader:
- *
- * <ul>
- * <li>The {@code Read} permission that allows to see the change.
- * <li>The {@code Push} permission that allows upload.
- * <li>The {@code Add Patch Set} permission, required if the change is owned by another user
- * (change owners implicitly have this permission).
- * <li>The {@code Forge Author} permission if the patch set that is rebased has a forged author
- * (author != uploader).
- * <li>The {@code Forge Server} permission if the patch set that is rebased has the server
- * identity as the author.
- * </ul>
- *
- * <p>Usually the uploader should have all these permission since they were already required for
- * the original upload, but there is the edge case that the uploader had the permission when doing
- * the original upload and then the permission was revoked.
- *
- * <p>Note that patch sets with a forged committer (committer != uploader) can be rebased on
- * behalf of the uploader, even if the uploader doesn't have the {@code Forge Committer}
- * permission. This is because on rebase on behalf of the uploader the uploader will become the
- * committer of the new rebased patch set, hence for the rebased patch set the committer is no
- * longer forged (committer == uploader) and hence the {@code Forge Committer} permission is not
- * required.
- *
- * <p>Note that the {@code Rebase} permission is not required for the uploader since the {@code
- * Rebase} permission is specifically about allowing a user to do a rebase via the web UI by
- * clicking on the {@code REBASE} button and the uploader is not clicking on this button.
- *
- * <p>The permissions of the uploader are checked explicitly here so that we can return a {@code
- * 409 Conflict} response with a proper error message if they are missing (the error message says
- * that the permission is missing for the uploader). The normal code path also checks these
- * permission but the exception thrown there would result in a {@code 403 Forbidden} response and
- * the error message would wrongly look like the caller (i.e. the rebaser) is missing the
- * permission.
- *
- * <p>Note that this method doesn't check permissions for the rebaser (aka the impersonating user
- * aka the calling user). Callers should check the permissions for the rebaser before calling this
- * method.
- *
- * @param rsrc the revision resource that should be rebased
- * @param rebaseInput the request input containing options for the rebase
- * @return revision resource that contains the uploader (aka the impersonated user) as the current
- * user which can be used for {@link BatchUpdate} to do the rebase on behalf of the uploader
- */
- private RevisionResource onBehalfOf(RevisionResource rsrc, RebaseInput rebaseInput)
- throws IOException, PermissionBackendException, BadRequestException,
- ResourceConflictException {
- if (rsrc.getPatchSet().id().get() != rsrc.getChange().currentPatchSetId().get()) {
- throw new BadRequestException(
- "non-current patch set cannot be rebased on behalf of the uploader");
- }
- if (rebaseInput.allowConflicts) {
- throw new BadRequestException(
- "allow_conflicts and on_behalf_of_uploader are mutually exclusive");
- }
-
- CurrentUser caller = rsrc.getUser();
- Account.Id uploaderId = rsrc.getPatchSet().uploader();
- IdentifiedUser uploader = userFactory.runAs(/*remotePeer= */ null, uploaderId, caller);
- logger.atFine().log(
- "%s is rebasing patch set %s of project %s on behalf of uploader %s",
- caller.getLoggableName(),
- rsrc.getPatchSet().id(),
- rsrc.getProject(),
- uploader.getLoggableName());
-
- checkPermissionForUploader(
- uploader,
- rsrc.getNotes(),
- ChangePermission.READ,
- String.format("uploader %s cannot read change", uploader.getLoggableName()));
- checkPermissionForUploader(
- uploader,
- rsrc.getNotes(),
- ChangePermission.ADD_PATCH_SET,
- String.format("uploader %s cannot add patch set", uploader.getLoggableName()));
-
- try (Repository repo = repoManager.openRepository(rsrc.getProject())) {
- RevCommit commit = repo.parseCommit(rsrc.getPatchSet().commitId());
-
- if (!uploader.hasEmailAddress(commit.getAuthorIdent().getEmailAddress())) {
- checkPermissionForUploader(
- uploader,
- rsrc.getNotes(),
- RefPermission.FORGE_AUTHOR,
- String.format(
- "author of patch set %d is forged and the uploader %s cannot forge author",
- rsrc.getPatchSet().id().get(), uploader.getLoggableName()));
-
- if (serverIdent.get().getEmailAddress().equals(commit.getAuthorIdent().getEmailAddress())) {
- checkPermissionForUploader(
- uploader,
- rsrc.getNotes(),
- RefPermission.FORGE_SERVER,
- String.format(
- "author of patch set %d is the server identity and the uploader %s cannot forge"
- + " the server identity",
- rsrc.getPatchSet().id().get(), uploader.getLoggableName()));
- }
- }
- }
-
- return new RevisionResource(
- changeResourceFactory.create(rsrc.getNotes(), uploader), rsrc.getPatchSet());
- }
-
- private void checkPermissionForUploader(
- IdentifiedUser uploader,
- ChangeNotes changeNotes,
- ChangePermission changePermission,
- String errorMessage)
- throws PermissionBackendException, ResourceConflictException {
- try {
- permissionBackend.user(uploader).change(changeNotes).check(changePermission);
- } catch (AuthException e) {
- throw new ResourceConflictException(errorMessage, e);
- }
- }
-
- private void checkPermissionForUploader(
- IdentifiedUser uploader,
- ChangeNotes changeNotes,
- RefPermission refPermission,
- String errorMessage)
- throws PermissionBackendException, ResourceConflictException {
- try {
- permissionBackend.user(uploader).ref(changeNotes.getChange().getDest()).check(refPermission);
- } catch (AuthException e) {
- throw new ResourceConflictException(errorMessage, e);
- }
- }
-
@Override
public UiAction.Description getDescription(RevisionResource rsrc) throws IOException {
UiAction.Description description =
diff --git a/java/com/google/gerrit/server/update/BatchUpdate.java b/java/com/google/gerrit/server/update/BatchUpdate.java
index 2572271..745c918 100644
--- a/java/com/google/gerrit/server/update/BatchUpdate.java
+++ b/java/com/google/gerrit/server/update/BatchUpdate.java
@@ -23,6 +23,7 @@
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;
+import com.google.auto.value.AutoValue;
import com.google.common.base.Throwables;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
@@ -50,6 +51,7 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.extensions.events.AttentionSetObserver;
@@ -243,6 +245,12 @@
}
class ContextImpl implements Context {
+ private final CurrentUser contextUser;
+
+ ContextImpl(@Nullable CurrentUser contextUser) {
+ this.contextUser = contextUser != null ? contextUser : user;
+ }
+
@Override
public RepoView getRepoView() throws IOException {
return BatchUpdate.this.getRepoView();
@@ -270,7 +278,7 @@
@Override
public CurrentUser getUser() {
- return user;
+ return contextUser;
}
@Override
@@ -281,6 +289,10 @@
}
private class RepoContextImpl extends ContextImpl implements RepoContext {
+ RepoContextImpl(@Nullable CurrentUser contextUser) {
+ super(contextUser);
+ }
+
@Override
public ObjectInserter getInserter() throws IOException {
return getRepoView().getInserterWrapper();
@@ -310,7 +322,8 @@
private boolean deleted;
- ChangeContextImpl(ChangeNotes notes) {
+ ChangeContextImpl(@Nullable CurrentUser contextUser, ChangeNotes notes) {
+ super(contextUser);
this.notes = requireNonNull(notes);
defaultUpdates = new TreeMap<>(comparing(PatchSet.Id::get));
distinctUpdates = ArrayListMultimap.create();
@@ -334,7 +347,7 @@
}
private ChangeUpdate getNewChangeUpdate(PatchSet.Id psId) {
- ChangeUpdate u = changeUpdateFactory.create(notes, user, when);
+ ChangeUpdate u = changeUpdateFactory.create(notes, getUser(), getWhen());
if (newChanges.containsKey(notes.getChangeId())) {
u.setAllowWriteToNewRef(true);
}
@@ -356,7 +369,9 @@
private class PostUpdateContextImpl extends ContextImpl implements PostUpdateContext {
private final Map<Change.Id, ChangeData> changeDatas;
- PostUpdateContextImpl(Map<Change.Id, ChangeData> changeDatas) {
+ PostUpdateContextImpl(
+ @Nullable CurrentUser contextUser, Map<Change.Id, ChangeData> changeDatas) {
+ super(contextUser);
this.changeDatas = changeDatas;
}
@@ -385,6 +400,7 @@
}
private final GitRepositoryManager repoManager;
+ private final AccountCache accountCache;
private final ChangeData.Factory changeDataFactory;
private final ChangeNotes.Factory changeNotesFactory;
private final ChangeUpdate.Factory changeUpdateFactory;
@@ -397,10 +413,10 @@
private final Instant when;
private final ZoneId zoneId;
- private final ListMultimap<Change.Id, BatchUpdateOp> ops =
+ private final ListMultimap<Change.Id, OpData<BatchUpdateOp>> ops =
MultimapBuilder.linkedHashKeys().arrayListValues().build();
private final Map<Change.Id, Change> newChanges = new HashMap<>();
- private final List<RepoOnlyOp> repoOnlyOps = new ArrayList<>();
+ private final List<OpData<RepoOnlyOp>> repoOnlyOps = new ArrayList<>();
private final Map<Change.Id, NotifyHandling> perChangeNotifyHandling = new HashMap<>();
private RepoView repoView;
@@ -419,6 +435,7 @@
BatchUpdate(
GitRepositoryManager repoManager,
@GerritPersonIdent PersonIdent serverIdent,
+ AccountCache accountCache,
ChangeData.Factory changeDataFactory,
ChangeNotes.Factory changeNotesFactory,
ChangeUpdate.Factory changeUpdateFactory,
@@ -430,6 +447,7 @@
@Assisted CurrentUser user,
@Assisted Instant when) {
this.repoManager = repoManager;
+ this.accountCache = accountCache;
this.changeDataFactory = changeDataFactory;
this.changeNotesFactory = changeNotesFactory;
this.changeUpdateFactory = changeUpdateFactory;
@@ -549,48 +567,72 @@
toMap(entry -> BranchNameKey.create(project, entry.getKey()), Map.Entry::getValue));
}
+ /**
+ * Adds a {@link BatchUpdate} for a change.
+ *
+ * <p>The op is executed by the user for which the {@link BatchUpdate} has been created.
+ */
public BatchUpdate addOp(Change.Id id, BatchUpdateOp op) {
checkArgument(!(op instanceof InsertChangeOp), "use insertChange");
requireNonNull(op);
- ops.put(id, op);
+ ops.put(id, OpData.create(op, user));
return this;
}
+ /** Adds a {@link BatchUpdate} for a change that should be executed by the given context user. */
+ public BatchUpdate addOp(Change.Id id, CurrentUser contextUser, BatchUpdateOp op) {
+ checkArgument(!(op instanceof InsertChangeOp), "use insertChange");
+ requireNonNull(op);
+ ops.put(id, OpData.create(op, contextUser));
+ return this;
+ }
+
+ /**
+ * Adds a {@link RepoOnlyOp}.
+ *
+ * <p>The op is executed by the user for which the {@link BatchUpdate} has been created.
+ */
public BatchUpdate addRepoOnlyOp(RepoOnlyOp op) {
checkArgument(!(op instanceof BatchUpdateOp), "use addOp()");
- repoOnlyOps.add(op);
+ repoOnlyOps.add(OpData.create(op, user));
+ return this;
+ }
+
+ /** Adds a {@link RepoOnlyOp} that should be executed by the given context user. */
+ public BatchUpdate addRepoOnlyOp(CurrentUser contextUser, RepoOnlyOp op) {
+ checkArgument(!(op instanceof BatchUpdateOp), "use addOp()");
+ repoOnlyOps.add(OpData.create(op, contextUser));
return this;
}
public BatchUpdate insertChange(InsertChangeOp op) throws IOException {
- Context ctx = new ContextImpl();
+ Context ctx = new ContextImpl(user);
Change c = op.createChange(ctx);
checkArgument(
!newChanges.containsKey(c.getId()), "only one op allowed to create change %s", c.getId());
newChanges.put(c.getId(), c);
- ops.get(c.getId()).add(0, op);
+ ops.get(c.getId()).add(0, OpData.create(op, user));
return this;
}
private void executeUpdateRepo() throws UpdateException, RestApiException {
try {
logDebug("Executing updateRepo on %d ops", ops.size());
- RepoContextImpl ctx = new RepoContextImpl();
- for (Map.Entry<Change.Id, BatchUpdateOp> op : ops.entries()) {
+ for (Map.Entry<Change.Id, OpData<BatchUpdateOp>> e : ops.entries()) {
+ BatchUpdateOp op = e.getValue().op();
+ RepoContextImpl ctx = new RepoContextImpl(e.getValue().user());
try (TraceContext.TraceTimer ignored =
TraceContext.newTimer(
- op.getValue().getClass().getSimpleName() + "#updateRepo",
- Metadata.builder()
- .projectName(project.get())
- .changeId(op.getKey().get())
- .build())) {
- op.getValue().updateRepo(ctx);
+ op.getClass().getSimpleName() + "#updateRepo",
+ Metadata.builder().projectName(project.get()).changeId(e.getKey().get()).build())) {
+ op.updateRepo(ctx);
}
}
logDebug("Executing updateRepo on %d RepoOnlyOps", repoOnlyOps.size());
- for (RepoOnlyOp op : repoOnlyOps) {
- op.updateRepo(ctx);
+ for (OpData<RepoOnlyOp> opData : repoOnlyOps) {
+ RepoContextImpl ctx = new RepoContextImpl(opData.user());
+ opData.op().updateRepo(ctx);
}
if (onSubmitValidators != null && !getRefUpdates().isEmpty()) {
@@ -599,7 +641,7 @@
// first update's executeRefUpdates has finished, hence after first repo's refs have been
// updated, which is too late.
onSubmitValidators.validate(
- project, ctx.getRevWalk().getObjectReader(), repoView.getCommands());
+ project, getRepoView().getRevWalk().getObjectReader(), repoView.getCommands());
}
} catch (Exception e) {
Throwables.throwIfInstanceOf(e, RestApiException.class);
@@ -613,12 +655,14 @@
}
}
- private void fireAttentionSetUpdateEvents(PostUpdateContext ctx) {
+ private void fireAttentionSetUpdateEvents(Map<Change.Id, ChangeData> changeDatas) {
for (ProjectChangeKey key : attentionSetUpdates.keySet()) {
- ChangeData change = ctx.getChangeData(key.projectName(), key.changeId());
- AccountState account = ctx.getAccount();
+ ChangeData change =
+ changeDatas.computeIfAbsent(
+ key.changeId(), id -> changeDataFactory.create(key.projectName(), key.changeId()));
for (AttentionSetUpdate update : attentionSetUpdates.get(key)) {
- attentionSetObserver.fire(change, account, update, ctx.getWhen());
+ attentionSetObserver.fire(
+ change, accountCache.getEvenIfMissing(update.account()), update, when);
}
}
}
@@ -709,31 +753,45 @@
}
handle.manager.setRefLogMessage(refLogMessage);
handle.manager.setPushCertificate(pushCert);
- for (Map.Entry<Change.Id, Collection<BatchUpdateOp>> e : ops.asMap().entrySet()) {
+ for (Map.Entry<Change.Id, Collection<OpData<BatchUpdateOp>>> e : ops.asMap().entrySet()) {
Change.Id id = e.getKey();
- ChangeContextImpl ctx = newChangeContext(id);
boolean dirty = false;
+ boolean deleted = false;
+ List<ChangeUpdate> changeUpdates = new ArrayList<>();
+ ChangeContextImpl ctx = null;
logDebug(
"Applying %d ops for change %s: %s",
e.getValue().size(),
id,
lazy(() -> e.getValue().stream().map(op -> op.getClass().getName()).collect(toSet())));
- for (BatchUpdateOp op : e.getValue()) {
+ for (OpData<BatchUpdateOp> opData : e.getValue()) {
+ if (ctx == null) {
+ ctx = newChangeContext(opData.user(), id);
+ } else if (!ctx.getUser().equals(opData.user())) {
+ ctx.defaultUpdates.values().forEach(changeUpdates::add);
+ ctx.distinctUpdates.values().forEach(changeUpdates::add);
+ ctx = newChangeContext(opData.user(), id);
+ }
try (TraceContext.TraceTimer ignored =
TraceContext.newTimer(
- op.getClass().getSimpleName() + "#updateChange",
+ opData.getClass().getSimpleName() + "#updateChange",
Metadata.builder().projectName(project.get()).changeId(id.get()).build())) {
- dirty |= op.updateChange(ctx);
+ dirty |= opData.op().updateChange(ctx);
+ deleted |= ctx.deleted;
}
}
+ if (ctx != null) {
+ ctx.defaultUpdates.values().forEach(changeUpdates::add);
+ ctx.distinctUpdates.values().forEach(changeUpdates::add);
+ }
+
if (!dirty) {
logDebug("No ops reported dirty, short-circuiting");
handle.setResult(id, ChangeResult.SKIPPED);
continue;
}
- ctx.defaultUpdates.values().forEach(handle.manager::add);
- ctx.distinctUpdates.values().forEach(handle.manager::add);
- if (ctx.deleted) {
+ changeUpdates.forEach(handle.manager::add);
+ if (deleted) {
logDebug("Change %s was deleted", id);
handle.manager.deleteChange(id);
handle.setResult(id, ChangeResult.DELETED);
@@ -744,7 +802,7 @@
return handle;
}
- private ChangeContextImpl newChangeContext(Change.Id id) {
+ private ChangeContextImpl newChangeContext(@Nullable CurrentUser contextUser, Change.Id id) {
logDebug("Opening change %s for update", id);
Change c = newChanges.get(id);
boolean isNew = c != null;
@@ -757,27 +815,30 @@
logDebug("Change %s is new", id);
}
ChangeNotes notes = changeNotesFactory.createForBatchUpdate(c, !isNew);
- return new ChangeContextImpl(notes);
+ return new ChangeContextImpl(contextUser, notes);
}
private void executePostOps(Map<Change.Id, ChangeData> changeDatas) throws Exception {
- PostUpdateContextImpl ctx = new PostUpdateContextImpl(changeDatas);
- for (BatchUpdateOp op : ops.values()) {
+ for (OpData<BatchUpdateOp> opData : ops.values()) {
+ PostUpdateContextImpl ctx = new PostUpdateContextImpl(opData.user(), changeDatas);
try (TraceContext.TraceTimer ignored =
- TraceContext.newTimer(op.getClass().getSimpleName() + "#postUpdate", Metadata.empty())) {
- op.postUpdate(ctx);
+ TraceContext.newTimer(
+ opData.getClass().getSimpleName() + "#postUpdate", Metadata.empty())) {
+ opData.op().postUpdate(ctx);
}
}
- for (RepoOnlyOp op : repoOnlyOps) {
+ for (OpData<RepoOnlyOp> opData : repoOnlyOps) {
+ PostUpdateContextImpl ctx = new PostUpdateContextImpl(opData.user(), changeDatas);
try (TraceContext.TraceTimer ignored =
- TraceContext.newTimer(op.getClass().getSimpleName() + "#postUpdate", Metadata.empty())) {
- op.postUpdate(ctx);
+ TraceContext.newTimer(
+ opData.getClass().getSimpleName() + "#postUpdate", Metadata.empty())) {
+ opData.op().postUpdate(ctx);
}
}
try (TraceContext.TraceTimer ignored =
TraceContext.newTimer("fireAttentionSetUpdates#postUpdate", Metadata.empty())) {
- fireAttentionSetUpdateEvents(ctx);
+ fireAttentionSetUpdateEvents(changeDatas);
}
}
@@ -808,4 +869,18 @@
logger.atFine().log(msg, arg1, arg2, arg3);
}
}
+
+ /** Data needed to execute a {@link RepoOnlyOp} or a {@link BatchUpdateOp}. */
+ @AutoValue
+ abstract static class OpData<T extends RepoOnlyOp> {
+ /** Op that should be executed. */
+ abstract T op();
+
+ /** User that should be used to execute the {@link #op}. */
+ abstract CurrentUser user();
+
+ static <T extends RepoOnlyOp> OpData<T> create(T op, CurrentUser user) {
+ return new AutoValue_BatchUpdate_OpData<>(op, user);
+ }
+ }
}
diff --git a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
index 07159b7..3066d9e 100644
--- a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
+++ b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
@@ -38,6 +38,7 @@
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.change.AddReviewersOp;
@@ -46,6 +47,7 @@
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.notedb.Sequences;
import com.google.gerrit.server.patch.DiffSummary;
import com.google.gerrit.server.patch.DiffSummaryKey;
@@ -55,6 +57,8 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.name.Named;
+import java.util.ArrayList;
+import java.util.List;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
@@ -97,6 +101,7 @@
@Inject private DynamicSet<AttentionSetListener> attentionSetListeners;
@Inject private AccountManager accountManager;
@Inject private AuthRequest.Factory authRequestFactory;
+ @Inject private IdentifiedUser.GenericFactory userFactory;
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
@@ -374,6 +379,157 @@
assertThat(diffSummaryCache.asMap()).hasSize(cacheSizeBefore + 1);
}
+ @Test
+ public void executeOpsWithDifferentUsers() throws Exception {
+ Change.Id changeId = createChange();
+
+ ObjectId oldHead =
+ repo.getRepository().exactRef(RefNames.changeMetaRef(changeId)).getObjectId();
+
+ CurrentUser defaultUser = user.get();
+ IdentifiedUser user1 =
+ userFactory.create(
+ accountManager.authenticate(authRequestFactory.createForUser("user1")).getAccountId());
+ IdentifiedUser user2 =
+ userFactory.create(
+ accountManager.authenticate(authRequestFactory.createForUser("user2")).getAccountId());
+
+ TestOp testOp1 = new TestOp().addReviewer(defaultUser.getAccountId());
+ TestOp testOp2 = new TestOp().addReviewer(user1.getAccountId());
+ TestOp testOp3 = new TestOp().addReviewer(user2.getAccountId());
+
+ try (BatchUpdate bu = batchUpdateFactory.create(project, defaultUser, TimeUtil.now())) {
+ bu.addOp(changeId, user1, testOp1);
+ bu.addOp(changeId, user2, testOp2);
+ bu.addOp(changeId, testOp3);
+ bu.execute();
+ }
+
+ assertThat(testOp1.updateRepoUser).isEqualTo(user1);
+ assertThat(testOp1.updateChangeUser).isEqualTo(user1);
+ assertThat(testOp1.postUpdateUser).isEqualTo(user1);
+
+ assertThat(testOp2.updateRepoUser).isEqualTo(user2);
+ assertThat(testOp2.updateChangeUser).isEqualTo(user2);
+ assertThat(testOp2.postUpdateUser).isEqualTo(user2);
+
+ assertThat(testOp3.updateRepoUser).isEqualTo(defaultUser);
+ assertThat(testOp3.updateChangeUser).isEqualTo(defaultUser);
+ assertThat(testOp3.postUpdateUser).isEqualTo(defaultUser);
+
+ // Verify that we got one meta commit per op.
+ RevCommit metaCommitForTestOp3 =
+ repo.getRepository()
+ .parseCommit(
+ repo.getRepository().exactRef(RefNames.changeMetaRef(changeId)).getObjectId());
+ assertThat(metaCommitForTestOp3.getAuthorIdent().getEmailAddress())
+ .isEqualTo(String.format("%s@gerrit", defaultUser.getAccountId()));
+ assertThat(metaCommitForTestOp3.getFullMessage())
+ .startsWith(
+ "Update patch set 1\n"
+ + "\n"
+ + "Patch-set: 1\n"
+ + String.format(
+ "Reviewer: Gerrit User %s <%s@gerrit>\n",
+ user2.getAccountId(), user2.getAccountId())
+ + "Attention:");
+
+ RevCommit metaCommitForTestOp2 =
+ repo.getRepository().parseCommit(metaCommitForTestOp3.getParent(0));
+ assertThat(metaCommitForTestOp2.getAuthorIdent().getEmailAddress())
+ .isEqualTo(String.format("%s@gerrit", user2.getAccountId()));
+ assertThat(metaCommitForTestOp2.getFullMessage())
+ .startsWith(
+ "Update patch set 1\n"
+ + "\n"
+ + "Patch-set: 1\n"
+ + String.format(
+ "Reviewer: Gerrit User %s <%s@gerrit>\n",
+ user1.getAccountId(), user1.getAccountId())
+ + "Attention:");
+
+ RevCommit metaCommitForTestOp1 =
+ repo.getRepository().parseCommit(metaCommitForTestOp2.getParent(0));
+ assertThat(metaCommitForTestOp1.getAuthorIdent().getEmailAddress())
+ .isEqualTo(String.format("%s@gerrit", user1.getAccountId()));
+ assertThat(metaCommitForTestOp1.getFullMessage())
+ .isEqualTo(
+ "Update patch set 1\n"
+ + "\n"
+ + "Patch-set: 1\n"
+ + String.format(
+ "Reviewer: Gerrit User %s <%s@gerrit>\n",
+ defaultUser.getAccountId(), defaultUser.getAccountId()));
+
+ assertThat(metaCommitForTestOp1.getParent(0)).isEqualTo(oldHead);
+ }
+
+ @Test
+ public void executeOpsWithSameUser() throws Exception {
+ Change.Id changeId = createChange();
+
+ ObjectId oldHead =
+ repo.getRepository().exactRef(RefNames.changeMetaRef(changeId)).getObjectId();
+
+ CurrentUser defaultUser = user.get();
+ IdentifiedUser user1 =
+ userFactory.create(
+ accountManager.authenticate(authRequestFactory.createForUser("user1")).getAccountId());
+ IdentifiedUser user2 =
+ userFactory.create(
+ accountManager.authenticate(authRequestFactory.createForUser("user2")).getAccountId());
+
+ TestOp testOp1 = new TestOp().addReviewer(user1.getAccountId());
+ TestOp testOp2 = new TestOp().addReviewer(user2.getAccountId());
+
+ try (BatchUpdate bu = batchUpdateFactory.create(project, defaultUser, TimeUtil.now())) {
+ bu.addOp(changeId, defaultUser, testOp1);
+ bu.addOp(changeId, testOp2);
+ bu.execute();
+ }
+
+ assertThat(testOp1.updateRepoUser).isEqualTo(defaultUser);
+ assertThat(testOp1.updateChangeUser).isEqualTo(defaultUser);
+ assertThat(testOp1.postUpdateUser).isEqualTo(defaultUser);
+
+ assertThat(testOp2.updateRepoUser).isEqualTo(defaultUser);
+ assertThat(testOp2.updateChangeUser).isEqualTo(defaultUser);
+ assertThat(testOp2.postUpdateUser).isEqualTo(defaultUser);
+
+ // Verify that we got a single meta commit (updates of both ops squashed into one commit).
+ RevCommit metaCommit =
+ repo.getRepository()
+ .parseCommit(
+ repo.getRepository().exactRef(RefNames.changeMetaRef(changeId)).getObjectId());
+ assertThat(metaCommit.getAuthorIdent().getEmailAddress())
+ .isEqualTo(String.format("%s@gerrit", defaultUser.getAccountId()));
+ assertThat(metaCommit.getFullMessage())
+ .startsWith(
+ "Update patch set 1\n"
+ + "\n"
+ + "Patch-set: 1\n"
+ + String.format(
+ "Reviewer: Gerrit User %s <%s@gerrit>\n",
+ user1.getAccountId(), user1.getAccountId())
+ + String.format(
+ "Reviewer: Gerrit User %s <%s@gerrit>\n",
+ user2.getAccountId(), user2.getAccountId())
+ + "Attention:");
+
+ assertThat(metaCommit.getParent(0)).isEqualTo(oldHead);
+ }
+
+ private Change.Id createChange() throws Exception {
+ Change.Id id = Change.id(sequences.nextChangeId());
+ try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
+ bu.insertChange(
+ changeInserterFactory.create(
+ id, repo.commit().message("Change").insertChangeId().create(), "refs/heads/master"));
+ bu.execute();
+ }
+ return id;
+ }
+
private Change.Id createChangeWithUpdates(int totalUpdates) throws Exception {
checkArgument(totalUpdates > 0);
checkArgument(totalUpdates <= MAX_UPDATES);
@@ -468,4 +624,38 @@
return true;
}
}
+
+ private static class TestOp implements BatchUpdateOp {
+ CurrentUser updateRepoUser;
+ CurrentUser updateChangeUser;
+ CurrentUser postUpdateUser;
+
+ private List<Account.Id> reviewersToAdd = new ArrayList<>();
+
+ TestOp addReviewer(Account.Id accountId) {
+ reviewersToAdd.add(accountId);
+ return this;
+ }
+
+ @Override
+ public void updateRepo(RepoContext ctx) {
+ updateRepoUser = ctx.getUser();
+ }
+
+ @Override
+ public boolean updateChange(ChangeContext ctx) {
+ updateChangeUser = ctx.getUser();
+
+ reviewersToAdd.forEach(
+ accountId ->
+ ctx.getUpdate(ctx.getChange().currentPatchSetId())
+ .putReviewer(accountId, ReviewerStateInternal.REVIEWER));
+ return true;
+ }
+
+ @Override
+ public void postUpdate(PostUpdateContext ctx) {
+ postUpdateUser = ctx.getUser();
+ }
+ }
}
diff --git a/polygerrit-ui/app/constants/reporting.ts b/polygerrit-ui/app/constants/reporting.ts
index 92d6bdf..ad59edd 100644
--- a/polygerrit-ui/app/constants/reporting.ts
+++ b/polygerrit-ui/app/constants/reporting.ts
@@ -125,4 +125,6 @@
CHECKS_RUNS_SELECTED_TRIGGERED = 'checks-runs-selected-triggered',
CHECKS_STATS = 'checks-stats',
CHANGE_ACTION_FIRED = 'change-action-fired',
+ BUTTON_CLICK = 'button-click',
+ LINK_CLICK = 'link-click',
}
diff --git a/polygerrit-ui/app/elements/gr-app-global-var-init.ts b/polygerrit-ui/app/elements/gr-app-global-var-init.ts
index d66fec0..d6a14ed 100644
--- a/polygerrit-ui/app/elements/gr-app-global-var-init.ts
+++ b/polygerrit-ui/app/elements/gr-app-global-var-init.ts
@@ -20,6 +20,7 @@
initPerformanceReporter,
initErrorReporter,
initWebVitals,
+ initClickReporter,
} from '../services/gr-reporting/gr-reporting_impl';
import {Finalizable} from '../services/registry';
@@ -34,6 +35,7 @@
initPerformanceReporter(reportingService);
initWebVitals(reportingService);
initErrorReporter(reportingService);
+ initClickReporter(reportingService);
}
window.GrAnnotation = GrAnnotation;
window.GrPluginActionContext = GrPluginActionContext;
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
index 75974e5..b44a16b 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
@@ -11,6 +11,7 @@
import {addShortcut, getEventPath, Key} from '../../../utils/dom-util';
import {getAppContext} from '../../../services/app-context';
import {classMap} from 'lit/directives/class-map.js';
+import {Interaction} from '../../../constants/reporting';
declare global {
interface HTMLElementTagNameMap {
@@ -228,6 +229,8 @@
return;
}
- this.reporting.reportInteraction('button-click', {path: getEventPath(e)});
+ this.reporting.reportInteraction(Interaction.BUTTON_CLICK, {
+ path: getEventPath(e),
+ });
}
}
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
index 06e8204..5b6e852 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
@@ -17,6 +17,7 @@
Timing,
} from '../../constants/reporting';
import {onCLS, onFID, onLCP, Metric, onINP} from 'web-vitals';
+import {getEventPath, isElementTarget} from '../../utils/dom-util';
// Latency reporting constants.
@@ -186,6 +187,22 @@
});
}
+export function initClickReporter(reportingService: ReportingService) {
+ document.addEventListener('click', (e: MouseEvent) => {
+ const anchorEl = e
+ .composedPath()
+ .find(el => isElementTarget(el) && el.tagName.toUpperCase() === 'A') as
+ | HTMLAnchorElement
+ | undefined;
+ if (!anchorEl) return;
+ reportingService.reportInteraction(Interaction.LINK_CLICK, {
+ path: getEventPath(e),
+ link: anchorEl.href,
+ text: anchorEl.innerText,
+ });
+ });
+}
+
export function initWebVitals(reportingService: ReportingService) {
function reportWebVitalMetric(name: Timing, metric: Metric) {
let score = metric.value;