All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.launchdarkly.client.EventOutputFormatter Maven / Gradle / Ivy

package com.launchdarkly.client;

import com.google.gson.Gson;
import com.google.gson.stream.JsonWriter;
import com.launchdarkly.client.EventSummarizer.CounterKey;
import com.launchdarkly.client.EventSummarizer.CounterValue;
import com.launchdarkly.client.value.LDValue;

import java.io.IOException;
import java.io.Writer;

/**
 * Transforms analytics events and summary data into the JSON format that we send to LaunchDarkly.
 * Rather than creating intermediate objects to represent this schema, we use the Gson streaming
 * output API to construct JSON directly.
 */
final class EventOutputFormatter {
  private final EventsConfiguration config;
  private final Gson gson;
  
  EventOutputFormatter(EventsConfiguration config) {
    this.config = config;
    this.gson = JsonHelpers.gsonInstanceForEventsSerialization(config);
  }
  
  int writeOutputEvents(Event[] events, EventSummarizer.EventSummary summary, Writer writer) throws IOException {
    int count = 0;    
    try (JsonWriter jsonWriter = new JsonWriter(writer)) {
      jsonWriter.beginArray();
      for (Event event: events) {
        if (writeOutputEvent(event, jsonWriter)) {
          count++;
        }
      }
      if (!summary.isEmpty()) {
        writeSummaryEvent(summary, jsonWriter);
        count++;
      }
      jsonWriter.endArray();
    }
    return count;
  }
  
  private boolean writeOutputEvent(Event event, JsonWriter jw) throws IOException {
    if (event instanceof Event.FeatureRequest) {
      Event.FeatureRequest fe = (Event.FeatureRequest)event;
      startEvent(fe, fe.debug ? "debug" : "feature", fe.key, jw);
      writeUserOrKey(fe, fe.debug, jw);
      if (fe.version != null) {
        jw.name("version");
        jw.value(fe.version);
      }
      if (fe.variation != null) {
        jw.name("variation");
        jw.value(fe.variation);
      }
      writeLDValue("value", fe.value, jw);
      writeLDValue("default", fe.defaultVal, jw);
      if (fe.prereqOf != null) {
        jw.name("prereqOf");
        jw.value(fe.prereqOf);
      }
      writeEvaluationReason("reason", fe.reason, jw);
      jw.endObject();
    } else if (event instanceof Event.Identify) {
      startEvent(event, "identify", event.user == null ? null : event.user.getKeyAsString(), jw);
      writeUser(event.user, jw);
      jw.endObject();
    } else if (event instanceof Event.Custom) {
      Event.Custom ce = (Event.Custom)event;
      startEvent(event, "custom", ce.key, jw);
      writeUserOrKey(ce, false, jw);
      writeLDValue("data", ce.data, jw);
      if (ce.metricValue != null) {
        jw.name("metricValue");
        jw.value(ce.metricValue);
      }
      jw.endObject();
    } else if (event instanceof Event.Index) {
      startEvent(event, "index", null, jw);
      writeUser(event.user, jw);
      jw.endObject();
    } else {
      return false;
    }
    return true;
  }
  
  private void writeSummaryEvent(EventSummarizer.EventSummary summary, JsonWriter jw) throws IOException {
    jw.beginObject();
    
    jw.name("kind");
    jw.value("summary");
    
    jw.name("startDate");
    jw.value(summary.startDate);
    jw.name("endDate");
    jw.value(summary.endDate);
    
    jw.name("features");
    jw.beginObject();
    
    CounterKey[] unprocessedKeys = summary.counters.keySet().toArray(new CounterKey[summary.counters.size()]);
    for (int i = 0; i < unprocessedKeys.length; i++) {
      if (unprocessedKeys[i] == null) {
        continue;
      }
      CounterKey key = unprocessedKeys[i];
      String flagKey = key.key;
      CounterValue firstValue = summary.counters.get(key);
      
      jw.name(flagKey);
      jw.beginObject();
      
      writeLDValue("default", firstValue.defaultVal, jw);
      
      jw.name("counters");
      jw.beginArray();
      
      for (int j = i; j < unprocessedKeys.length; j++) {
        CounterKey keyForThisFlag = unprocessedKeys[j];
        if (j != i && (keyForThisFlag == null || !keyForThisFlag.key.equals(flagKey))) {
          continue;
        }
        CounterValue value = keyForThisFlag == key ? firstValue : summary.counters.get(keyForThisFlag);
        unprocessedKeys[j] = null;
           
        jw.beginObject();
        
        if (keyForThisFlag.variation != null) {
          jw.name("variation");
          jw.value(keyForThisFlag.variation);
        }
        if (keyForThisFlag.version != null) {
          jw.name("version");
          jw.value(keyForThisFlag.version);
        } else {
          jw.name("unknown");
          jw.value(true);
        }
        writeLDValue("value", value.flagValue, jw);
        jw.name("count");
        jw.value(value.count);
        
        jw.endObject(); // end of this counter
      }
      
      jw.endArray(); // end of "counters" array
      
      jw.endObject(); // end of this flag
    }
    
    jw.endObject(); // end of "features"
    
    jw.endObject();
  }
  
  private void startEvent(Event event, String kind, String key, JsonWriter jw) throws IOException {
    jw.beginObject();
    jw.name("kind");
    jw.value(kind);
    jw.name("creationDate");
    jw.value(event.creationDate);
    if (key != null) {
      jw.name("key");
      jw.value(key);
    }
  }
  
  private void writeUserOrKey(Event event, boolean forceInline, JsonWriter jw) throws IOException {
    LDUser user = event.user;
    if (user != null) {
      if (config.inlineUsersInEvents || forceInline) {
        writeUser(user, jw);
      } else {
        jw.name("userKey");
        jw.value(user.getKeyAsString());
      }
    }
  }
  
  private void writeUser(LDUser user, JsonWriter jw) throws IOException {
    jw.name("user");
    // config.gson is already set up to use our custom serializer, which knows about private attributes
    // and already uses the streaming approach
    gson.toJson(user, LDUser.class, jw);
  }
  
  private void writeLDValue(String key, LDValue value, JsonWriter jw) throws IOException {
    if (value == null || value.isNull()) {
      return;
    }
    jw.name(key);
    gson.toJson(value, LDValue.class, jw); // LDValue defines its own custom serializer
  }
  
  // This logic is so that we don't have to define multiple custom serializers for the various reason subclasses.
  private void writeEvaluationReason(String key, EvaluationReason er, JsonWriter jw) throws IOException {
    if (er == null) {
      return;
    }
    jw.name(key);
    
    jw.beginObject();
    
    jw.name("kind");
    jw.value(er.getKind().name());
    
    switch (er.getKind()) {
    case ERROR:
      jw.name("errorKind");
      jw.value(er.getErrorKind().name());
      break;
    case PREREQUISITE_FAILED:
      jw.name("prerequisiteKey");
      jw.value(er.getPrerequisiteKey());
      break;
    case RULE_MATCH:
      jw.name("ruleIndex");
      jw.value(er.getRuleIndex());
      if (er.getRuleId() != null) {
        jw.name("ruleId");
        jw.value(er.getRuleId());
      }
      break;
    default:
      break;
    }   
    
    jw.endObject();
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy