Code review comment: I am not fully convinced this is the proper error handling logic to extract the http or grpc status code here.  DatastoreException has an int getCode() this should return the http code which serviceCallMetri.call() will convert to a grpc status code.  So lets's just change the logic to   } catch (DatastoreException exception) {           serviceCallMetric.call(exception.getCode());            ....             and we can remove getErrorInfo

Code under review:
private void flushBatch() throws DatastoreException, IOException, InterruptedException {
      LOG.debug("Writing batch of {} mutations", mutations.size());
      Sleeper sleeper = Sleeper.DEFAULT;
      BackOff backoff = BUNDLE_WRITE_BACKOFF.backoff();

      while (true) {
        
        CommitRequest.Builder commitRequest = CommitRequest.newBuilder();
        commitRequest.addAllMutations(mutations);
        commitRequest.setMode(CommitRequest.Mode.NON_TRANSACTIONAL);
        long startTime = System.currentTimeMillis(), endTime;

        if (adaptiveThrottler.throttleRequest(startTime)) {
          LOG.info("Delaying request due to previous failures");
          throttlingMsecs.inc(WriteBatcherImpl.DATASTORE_BATCH_TARGET_LATENCY_MS);
          sleeper.sleep(WriteBatcherImpl.DATASTORE_BATCH_TARGET_LATENCY_MS);
          continue;
        }

        HashMap<String, String> baseLabels = new HashMap<>();
        baseLabels.put(MonitoringInfoConstants.Labels.PTRANSFORM, "");
        baseLabels.put(MonitoringInfoConstants.Labels.SERVICE, "Datastore");
        baseLabels.put(MonitoringInfoConstants.Labels.METHOD, "flushBatch");
        baseLabels.put(MonitoringInfoConstants.Labels.RESOURCE, "");
        baseLabels.put(MonitoringInfoConstants.Labels.DATASTORE_PROJECT, projectId.get());
        baseLabels.put(
            MonitoringInfoConstants.Labels.DATASTORE_NAMESPACE, getNameSpace(projectId.get(), ""));
        ServiceCallMetric serviceCallMetric =
            new ServiceCallMetric(MonitoringInfoConstants.Urns.API_REQUEST_COUNT, baseLabels);
        try {
          datastore.commit(commitRequest.build());
          endTime = System.currentTimeMillis();
          serviceCallMetric.call("ok");

          writeBatcher.addRequestLatency(endTime, endTime - startTime, mutations.size());
          adaptiveThrottler.successfulRequest(startTime);
          latencyMsPerMutation.update((endTime - startTime) / mutations.size());
          rpcSuccesses.inc();
          entitiesMutated.inc(mutations.size());

          
          break;
        } catch (DatastoreException exception) {
          GoogleJsonError.ErrorInfo errorInfo = getErrorInfo(exception);
          if (errorInfo == null) {
            serviceCallMetric.call(ServiceCallMetric.CANONICAL_STATUS_UNKNOWN);
          } else {
            serviceCallMetric.call(errorInfo.getReason());
          }
          if (exception.getCode() == Code.DEADLINE_EXCEEDED) {
            /* Most errors are not related to request size, and should not change our expectation of
             * the latency of successful requests. DEADLINE_EXCEEDED can be taken into
             * consideration, though. */
            endTime = System.currentTimeMillis();
            writeBatcher.addRequestLatency(endTime, endTime - startTime, mutations.size());
            latencyMsPerMutation.update((endTime - startTime) / mutations.size());
          }
          
          
          LOG.error(
              "Error writing batch of {} mutations to Datastore ({}): {}",
              mutations.size(),
              exception.getCode(),
              exception.getMessage());
          rpcErrors.inc();

          if (NON_RETRYABLE_ERRORS.contains(exception.getCode())) {
            throw exception;
          }
          if (!BackOffUtils.next(sleeper, backoff)) {
            LOG.error("Aborting after {} retries.", MAX_RETRIES);
            throw exception;
          }
        }
      }
      LOG.debug("Successfully wrote {} mutations", mutations.size());
      mutations.clear();
      mutationsSize = 0;
    }

Line commented on: serviceCallMetric.call(errorInfo.getReason());

Revised code:
private void flushBatch() throws DatastoreException, IOException, InterruptedException {
      LOG.debug("Writing batch of {} mutations", mutations.size());
      Sleeper sleeper = Sleeper.DEFAULT;
      BackOff backoff = BUNDLE_WRITE_BACKOFF.backoff();

      while (true) {
        
        CommitRequest.Builder commitRequest = CommitRequest.newBuilder();
        commitRequest.addAllMutations(mutations);
        commitRequest.setMode(CommitRequest.Mode.NON_TRANSACTIONAL);
        long startTime = System.currentTimeMillis(), endTime;

        if (adaptiveThrottler.throttleRequest(startTime)) {
          LOG.info("Delaying request due to previous failures");
          throttlingMsecs.inc(WriteBatcherImpl.DATASTORE_BATCH_TARGET_LATENCY_MS);
          sleeper.sleep(WriteBatcherImpl.DATASTORE_BATCH_TARGET_LATENCY_MS);
          continue;
        }

        HashMap<String, String> baseLabels = new HashMap<>();
        baseLabels.put(MonitoringInfoConstants.Labels.PTRANSFORM, "");
        baseLabels.put(MonitoringInfoConstants.Labels.SERVICE, "Datastore");
        baseLabels.put(MonitoringInfoConstants.Labels.METHOD, "BatchDatastoreWrite");
        baseLabels.put(
            MonitoringInfoConstants.Labels.RESOURCE,
            GcpResourceIdentifiers.datastoreResource(projectId.get(), ""));
        baseLabels.put(MonitoringInfoConstants.Labels.DATASTORE_PROJECT, projectId.get());
        baseLabels.put(MonitoringInfoConstants.Labels.DATASTORE_NAMESPACE, "");
        ServiceCallMetric serviceCallMetric =
            new ServiceCallMetric(MonitoringInfoConstants.Urns.API_REQUEST_COUNT, baseLabels);
        try {
          datastore.commit(commitRequest.build());
          endTime = System.currentTimeMillis();
          serviceCallMetric.call("ok");

          writeBatcher.addRequestLatency(endTime, endTime - startTime, mutations.size());
          adaptiveThrottler.successfulRequest(startTime);
          latencyMsPerMutation.update((endTime - startTime) / mutations.size());
          rpcSuccesses.inc();
          entitiesMutated.inc(mutations.size());

          
          break;
        } catch (DatastoreException exception) {
          serviceCallMetric.call(exception.getCode().getNumber());
          if (exception.getCode() == Code.DEADLINE_EXCEEDED) {
            /* Most errors are not related to request size, and should not change our expectation of
             * the latency of successful requests. DEADLINE_EXCEEDED can be taken into
             * consideration, though. */
            endTime = System.currentTimeMillis();
            writeBatcher.addRequestLatency(endTime, endTime - startTime, mutations.size());
            latencyMsPerMutation.update((endTime - startTime) / mutations.size());
          }
          
          
          LOG.error(
              "Error writing batch of {} mutations to Datastore ({}): {}",
              mutations.size(),
              exception.getCode(),
              exception.getMessage());
          rpcErrors.inc();

          if (NON_RETRYABLE_ERRORS.contains(exception.getCode())) {
            throw exception;
          }
          if (!BackOffUtils.next(sleeper, backoff)) {
            LOG.error("Aborting after {} retries.", MAX_RETRIES);
            throw exception;
          }
        }
      }
      LOG.debug("Successfully wrote {} mutations", mutations.size());
      mutations.clear();
      mutationsSize = 0;
    }