com.dynatrace.openkit.protocol.Beacon Maven / Gradle / Ivy
Show all versions of openkit-java Show documentation
/**
* Copyright 2018-2019 Dynatrace LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.dynatrace.openkit.protocol;
import com.dynatrace.openkit.CrashReportingLevel;
import com.dynatrace.openkit.DataCollectionLevel;
import com.dynatrace.openkit.api.Logger;
import com.dynatrace.openkit.core.ActionImpl;
import com.dynatrace.openkit.core.SessionImpl;
import com.dynatrace.openkit.core.WebRequestTracerBaseImpl;
import com.dynatrace.openkit.core.caching.BeaconCacheImpl;
import com.dynatrace.openkit.core.configuration.BeaconConfiguration;
import com.dynatrace.openkit.core.configuration.Configuration;
import com.dynatrace.openkit.core.configuration.HTTPClientConfiguration;
import com.dynatrace.openkit.core.util.InetAddressValidator;
import com.dynatrace.openkit.core.util.PercentEncoder;
import com.dynatrace.openkit.providers.HTTPClientProvider;
import com.dynatrace.openkit.providers.ThreadIDProvider;
import com.dynatrace.openkit.providers.TimingProvider;
import java.io.UnsupportedEncodingException;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/**
* The Beacon class holds all the beacon data and the beacon protocol implementation.
*/
public class Beacon {
// basic data constants
private static final String BEACON_KEY_PROTOCOL_VERSION = "vv";
private static final String BEACON_KEY_OPENKIT_VERSION = "va";
private static final String BEACON_KEY_APPLICATION_ID = "ap";
private static final String BEACON_KEY_APPLICATION_NAME = "an";
private static final String BEACON_KEY_APPLICATION_VERSION = "vn";
private static final String BEACON_KEY_PLATFORM_TYPE = "pt";
private static final String BEACON_KEY_AGENT_TECHNOLOGY_TYPE = "tt";
private static final String BEACON_KEY_VISITOR_ID = "vi";
private static final String BEACON_KEY_SESSION_NUMBER = "sn";
private static final String BEACON_KEY_CLIENT_IP_ADDRESS = "ip";
private static final String BEACON_KEY_MULTIPLICITY = "mp";
private static final String BEACON_KEY_DATA_COLLECTION_LEVEL = "dl";
private static final String BEACON_KEY_CRASH_REPORTING_LEVEL = "cl";
// device data constants
private static final String BEACON_KEY_DEVICE_OS = "os";
private static final String BEACON_KEY_DEVICE_MANUFACTURER = "mf";
private static final String BEACON_KEY_DEVICE_MODEL = "md";
// timestamp constants
private static final String BEACON_KEY_SESSION_START_TIME = "tv";
private static final String BEACON_KEY_TIMESYNC_TIME = "ts";
private static final String BEACON_KEY_TRANSMISSION_TIME = "tx";
// Action related constants
private static final String BEACON_KEY_EVENT_TYPE = "et";
private static final String BEACON_KEY_NAME = "na";
private static final String BEACON_KEY_THREAD_ID = "it";
private static final String BEACON_KEY_ACTION_ID = "ca";
private static final String BEACON_KEY_PARENT_ACTION_ID = "pa";
private static final String BEACON_KEY_START_SEQUENCE_NUMBER = "s0";
private static final String BEACON_KEY_TIME_0 = "t0";
private static final String BEACON_KEY_END_SEQUENCE_NUMBER = "s1";
private static final String BEACON_KEY_TIME_1 = "t1";
// data, error & crash capture constants
private static final String BEACON_KEY_VALUE = "vl";
private static final String BEACON_KEY_ERROR_CODE = "ev";
private static final String BEACON_KEY_ERROR_REASON = "rs";
private static final String BEACON_KEY_ERROR_STACKTRACE = "st";
private static final String BEACON_KEY_ERROR_TECHNOLOGY_TYPE = "tt";
// web request constants
private static final String BEACON_KEY_WEBREQUEST_RESPONSECODE = "rc";
private static final String BEACON_KEY_WEBREQUEST_BYTES_SENT = "bs";
private static final String BEACON_KEY_WEBREQUEST_BYTES_RECEIVED = "br";
// in Java 6 there is no constant for "UTF-8" in the JDK yet, so we define it ourselves
static final String CHARSET = "UTF-8";
// max name length
private static final int MAX_NAME_LEN = 250;
// web request tag prefix constant
private static final String TAG_PREFIX = "MT";
// web request tag reserved characters
private static final char[] RESERVED_CHARACTERS = {'_'};
private static final char BEACON_DATA_DELIMITER = '&';
// next ID and sequence number
private final AtomicInteger nextID = new AtomicInteger(0);
private final AtomicInteger nextSequenceNumber = new AtomicInteger(0);
// session number & start time
private final int sessionNumber;
private final TimingProvider timingProvider;
private final ThreadIDProvider threadIDProvider;
private final long sessionStartTime;
// unique device identifier
private final long deviceID;
// client IP address
private final String clientIPAddress;
// basic beacon data which does not change over time
private final String immutableBasicBeaconData;
// AbstractConfiguration reference
private final Configuration configuration;
// HTTPClientConfiguration reference
private final HTTPClientConfiguration httpConfiguration;
private final Logger logger;
private final BeaconCacheImpl beaconCache;
private final AtomicReference beaconConfiguration;
private final Random random;
// *** constructors ***
/**
* Constructor.
*
* @param logger Logger for logging messages.
* @param beaconCache Cache storing beacon related data.
* @param configuration OpenKit related configuration.
* @param clientIPAddress The client's IP address.
* @param threadIDProvider Provider for retrieving thread id.
* @param timingProvider Provider for time related methods.
*/
public Beacon(Logger logger, BeaconCacheImpl beaconCache, Configuration configuration, String clientIPAddress, ThreadIDProvider threadIDProvider, TimingProvider timingProvider) {
this(logger, beaconCache, configuration, clientIPAddress, threadIDProvider, timingProvider, new Random());
}
/**
* Constructor additionally taking a Random object, used for testing
*
* @param logger Logger for logging messages.
* @param beaconCache Cache storing beacon related data.
* @param configuration OpenKit related configuration.
* @param clientIPAddress The client's IP address.
* @param threadIDProvider Provider for retrieving thread id.
* @param timingProvider Provider for time related methods.
* @param random Random that can be mocked for tests
*/
Beacon(Logger logger, BeaconCacheImpl beaconCache, Configuration configuration, String clientIPAddress, ThreadIDProvider threadIDProvider, TimingProvider timingProvider, Random random) {
this.logger = logger;
this.beaconCache = beaconCache;
this.sessionNumber = configuration.createSessionNumber();
this.timingProvider = timingProvider;
this.configuration = configuration;
this.threadIDProvider = threadIDProvider;
this.sessionStartTime = timingProvider.provideTimestampInMilliseconds();
this.deviceID = createDeviceID(random, configuration);
this.random = random;
if (clientIPAddress == null) {
// A client IP address, which is a null, is valid.
// The real IP address is determined on the server side.
this.clientIPAddress = "";
} else if (InetAddressValidator.isValidIP(clientIPAddress)) {
this.clientIPAddress = clientIPAddress;
} else {
if (logger.isWarnEnabled()) {
logger.warning(getClass().getSimpleName() + " Client IP address validation failed: " + clientIPAddress);
}
this.clientIPAddress = "";
}
// store the current configuration
this.httpConfiguration = configuration.getHttpClientConfig();
beaconConfiguration = new AtomicReference(configuration.getBeaconConfiguration());
immutableBasicBeaconData = createImmutableBasicBeaconData();
}
/**
* Create a device ID.
*
* @param random Pseudo random number generator.
* @param configuration Configuration.
* @return A device ID, which might either be the one set when building OpenKit or a randomly generated one.
*/
private static long createDeviceID(Random random, Configuration configuration) {
if (configuration == null) {
return nextRandomPositiveLong(random);
}
BeaconConfiguration beaconConfig = configuration.getBeaconConfiguration();
if (beaconConfig != null && beaconConfig.getDataCollectionLevel() == DataCollectionLevel.USER_BEHAVIOR) {
// configuration is valid and user allows data tracking
return configuration.getDeviceID();
}
// no user tracking allowed
return nextRandomPositiveLong(random);
}
/**
* Get a random long, which is in positive range (including {@code 0}).
*
* @param random Pseudo random number generator.
* @return Randomly generated long, which is greater than or equal to {@code 0}.
*/
private static long nextRandomPositiveLong(Random random) {
return random.nextLong() & 0x7fffffffffffffffL;
}
/**
* Create a unique identifier.
*
*
* The identifier returned is only unique per Beacon.
* Calling this method on two different Beacon instances, might give the same result.
*
*
* @return A unique identifier.
*/
public int createID() {
return nextID.incrementAndGet();
}
/**
* Get the current timestamp in milliseconds by delegating to TimingProvider
*
* @return Current timestamp in milliseconds.
*/
public long getCurrentTimestamp() {
return timingProvider.provideTimestampInMilliseconds();
}
/**
* Create a unique sequence number.
*
*
* The sequence number returned is only unique per Beacon.
* Calling this method on two different Beacon instances, might give the same result.
*
*
* @return A unique sequence number.
*/
public int createSequenceNumber() {
return nextSequenceNumber.incrementAndGet();
}
/**
* Create a web request tag.
*
*
* Web request tags can be attached as HTTP header for web request tracing.
* If data collection level is 0 ({@link DataCollectionLevel#OFF}) an empty tag is returned.
*
*
* @param parentActionID The ID of the {@link com.dynatrace.openkit.api.Action} for which to create a web request tag
* or {@code 0} if no parent action exists.
* @param sequenceNo Sequence number of the {@link com.dynatrace.openkit.api.WebRequestTracer}.
* @return A web request tracer tag.
*/
public String createTag(int parentActionID, int sequenceNo) {
if (getBeaconConfiguration().getDataCollectionLevel() == DataCollectionLevel.OFF) {
return "";
}
return TAG_PREFIX
+ "_" + ProtocolConstants.PROTOCOL_VERSION
+ "_" + httpConfiguration.getServerID()
+ "_" + getDeviceID()
+ "_" + getSessionNumber()
+ "_" + configuration.getApplicationIDPercentEncoded()
+ "_" + parentActionID
+ "_" + threadIDProvider.getThreadID()
+ "_" + sequenceNo;
}
/**
* Add {@link ActionImpl} to Beacon.
*
*
* The serialized data is added to {@link com.dynatrace.openkit.core.caching.BeaconCache}.
*
*
* @param action The action to add.
*/
public void addAction(ActionImpl action) {
if (isCapturingDisabled()) {
return;
}
if (getBeaconConfiguration().getDataCollectionLevel() == DataCollectionLevel.OFF) {
return;
}
StringBuilder actionBuilder = new StringBuilder();
buildBasicEventData(actionBuilder, EventType.ACTION, action.getName());
addKeyValuePair(actionBuilder, BEACON_KEY_ACTION_ID, action.getID());
addKeyValuePair(actionBuilder, BEACON_KEY_PARENT_ACTION_ID, action.getParentID());
addKeyValuePair(actionBuilder, BEACON_KEY_START_SEQUENCE_NUMBER, action.getStartSequenceNo());
addKeyValuePair(actionBuilder, BEACON_KEY_TIME_0, getTimeSinceSessionStartTime(action.getStartTime()));
addKeyValuePair(actionBuilder, BEACON_KEY_END_SEQUENCE_NUMBER, action.getEndSequenceNo());
addKeyValuePair(actionBuilder, BEACON_KEY_TIME_1, action.getEndTime() - action.getStartTime());
addActionData(action.getStartTime(), actionBuilder);
}
/**
* Add start session event to Beacon.
*
*
* The serialized data is added to {@link com.dynatrace.openkit.core.caching.BeaconCache}.
*
*/
public void startSession() {
if (isCapturingDisabled()) {
return;
}
StringBuilder eventBuilder = new StringBuilder();
buildBasicEventData(eventBuilder, EventType.SESSION_START, null);
addKeyValuePair(eventBuilder, BEACON_KEY_PARENT_ACTION_ID, 0);
addKeyValuePair(eventBuilder, BEACON_KEY_START_SEQUENCE_NUMBER, createSequenceNumber());
addKeyValuePair(eventBuilder, BEACON_KEY_TIME_0, 0L);
addEventData(sessionStartTime, eventBuilder);
}
/**
* Add {@link SessionImpl} to Beacon when session is ended.
*
*
* The serialized data is added to {@link com.dynatrace.openkit.core.caching.BeaconCache}.
*
*
* @param session The session to add.
*/
public void endSession(SessionImpl session) {
if (isCapturingDisabled()) {
return;
}
if (getBeaconConfiguration().getDataCollectionLevel() == DataCollectionLevel.OFF) {
return;
}
StringBuilder eventBuilder = new StringBuilder();
buildBasicEventData(eventBuilder, EventType.SESSION_END, null);
addKeyValuePair(eventBuilder, BEACON_KEY_PARENT_ACTION_ID, 0);
addKeyValuePair(eventBuilder, BEACON_KEY_START_SEQUENCE_NUMBER, createSequenceNumber());
addKeyValuePair(eventBuilder, BEACON_KEY_TIME_0, getTimeSinceSessionStartTime(session.getEndTime()));
addEventData(session.getEndTime(), eventBuilder);
}
/**
* Add key-value-pair to Beacon.
*
*
* The serialized data is added to {@link com.dynatrace.openkit.core.caching.BeaconCache}.
*
*
* @param parentAction The {@link com.dynatrace.openkit.api.Action} on which this value was reported.
* @param valueName Value's name.
* @param value Actual value to report.
*/
public void reportValue(ActionImpl parentAction, String valueName, int value) {
if (isCapturingDisabled()) {
return;
}
if (getBeaconConfiguration().getDataCollectionLevel() != DataCollectionLevel.USER_BEHAVIOR) {
return;
}
StringBuilder eventBuilder = new StringBuilder();
long eventTimestamp = buildEvent(eventBuilder, EventType.VALUE_INT, valueName, parentAction);
addKeyValuePair(eventBuilder, BEACON_KEY_VALUE, value);
addEventData(eventTimestamp, eventBuilder);
}
/**
* Add key-value-pair to Beacon.
*
*
* The serialized data is added to {@link com.dynatrace.openkit.core.caching.BeaconCache}.
*
*
* @param parentAction The {@link com.dynatrace.openkit.api.Action} on which this value was reported.
* @param valueName Value's name.
* @param value Actual value to report.
*/
public void reportValue(ActionImpl parentAction, String valueName, double value) {
if (isCapturingDisabled()) {
return;
}
if (getBeaconConfiguration().getDataCollectionLevel() != DataCollectionLevel.USER_BEHAVIOR) {
return;
}
StringBuilder eventBuilder = new StringBuilder();
long eventTimestamp = buildEvent(eventBuilder, EventType.VALUE_DOUBLE, valueName, parentAction);
addKeyValuePair(eventBuilder, BEACON_KEY_VALUE, value);
addEventData(eventTimestamp, eventBuilder);
}
/**
* Add key-value-pair to Beacon.
*
*
* The serialized data is added to {@link com.dynatrace.openkit.core.caching.BeaconCache}.
*
*
* @param parentAction The {@link com.dynatrace.openkit.api.Action} on which this value was reported.
* @param valueName Value's name.
* @param value Actual value to report.
*/
public void reportValue(ActionImpl parentAction, String valueName, String value) {
if (isCapturingDisabled()) {
return;
}
if (getBeaconConfiguration().getDataCollectionLevel() != DataCollectionLevel.USER_BEHAVIOR) {
return;
}
StringBuilder eventBuilder = new StringBuilder();
long eventTimestamp = buildEvent(eventBuilder, EventType.VALUE_STRING, valueName, parentAction);
if (value != null) {
addKeyValuePair(eventBuilder, BEACON_KEY_VALUE, truncate(value));
}
addEventData(eventTimestamp, eventBuilder);
}
/**
* Add event (aka. named event) to Beacon.
*
*
* The serialized data is added to {@link com.dynatrace.openkit.core.caching.BeaconCache}.
*
*
* @param parentAction The {@link com.dynatrace.openkit.api.Action} on which this event was reported.
* @param eventName Event's name.
*/
public void reportEvent(ActionImpl parentAction, String eventName) {
if (isCapturingDisabled()) {
return;
}
if (getBeaconConfiguration().getDataCollectionLevel() != DataCollectionLevel.USER_BEHAVIOR) {
return;
}
StringBuilder eventBuilder = new StringBuilder();
long eventTimestamp = buildEvent(eventBuilder, EventType.NAMED_EVENT, eventName, parentAction);
addEventData(eventTimestamp, eventBuilder);
}
/**
* Add error to Beacon.
*
*
* The serialized data is added to {@link com.dynatrace.openkit.core.caching.BeaconCache}.
*
*
* @param parentAction The {@link com.dynatrace.openkit.api.Action} on which this error was reported.
* @param errorName Error's name.
* @param errorCode Some error code.
* @param reason Reason for that error.
*/
public void reportError(ActionImpl parentAction, String errorName, int errorCode, String reason) {
// if capture errors is off -> do nothing
if (isCapturingDisabled() || !configuration.isCaptureErrors()) {
return;
}
if (getBeaconConfiguration().getDataCollectionLevel() == DataCollectionLevel.OFF) {
return;
}
StringBuilder eventBuilder = new StringBuilder();
buildBasicEventData(eventBuilder, EventType.ERROR, errorName);
long timestamp = timingProvider.provideTimestampInMilliseconds();
addKeyValuePair(eventBuilder, BEACON_KEY_PARENT_ACTION_ID, parentAction.getID());
addKeyValuePair(eventBuilder, BEACON_KEY_START_SEQUENCE_NUMBER, createSequenceNumber());
addKeyValuePair(eventBuilder, BEACON_KEY_TIME_0, getTimeSinceSessionStartTime(timestamp));
addKeyValuePair(eventBuilder, BEACON_KEY_ERROR_CODE, errorCode);
addKeyValuePairIfNotNull(eventBuilder, BEACON_KEY_ERROR_REASON, reason);
addKeyValuePair(eventBuilder, BEACON_KEY_ERROR_TECHNOLOGY_TYPE, ProtocolConstants.ERROR_TECHNOLOGY_TYPE);
addEventData(timestamp, eventBuilder);
}
/**
* Add crash to Beacon.
*
*
* The serialized data is added to {@link com.dynatrace.openkit.core.caching.BeaconCache}.
*
*
* @param errorName Error's name.
* @param reason Reason for that error.
* @param stacktrace Crash stacktrace.
*/
public void reportCrash(String errorName, String reason, String stacktrace) {
// if capture crashes is off -> do nothing
if (isCapturingDisabled() || !configuration.isCaptureCrashes()) {
return;
}
if (getBeaconConfiguration().getCrashReportingLevel() != CrashReportingLevel.OPT_IN_CRASHES) {
return;
}
StringBuilder eventBuilder = new StringBuilder();
buildBasicEventData(eventBuilder, EventType.CRASH, errorName);
long timestamp = timingProvider.provideTimestampInMilliseconds();
addKeyValuePair(eventBuilder, BEACON_KEY_PARENT_ACTION_ID, 0); // no parent action
addKeyValuePair(eventBuilder, BEACON_KEY_START_SEQUENCE_NUMBER, createSequenceNumber());
addKeyValuePair(eventBuilder, BEACON_KEY_TIME_0, getTimeSinceSessionStartTime(timestamp));
addKeyValuePairIfNotNull(eventBuilder, BEACON_KEY_ERROR_REASON, reason);
addKeyValuePairIfNotNull(eventBuilder, BEACON_KEY_ERROR_STACKTRACE, stacktrace);
addKeyValuePair(eventBuilder, BEACON_KEY_ERROR_TECHNOLOGY_TYPE, ProtocolConstants.ERROR_TECHNOLOGY_TYPE);
addEventData(timestamp, eventBuilder);
}
/**
* Add web request to Beacon.
*
*
* The serialized data is added to {@link com.dynatrace.openkit.core.caching.BeaconCache}.
*
*
* @param parentActionID The id of the parent {@link com.dynatrace.openkit.api.Action} on which this web request was reported.
* @param webRequestTracer Web request tracer to serialize.
*/
public void addWebRequest(int parentActionID, WebRequestTracerBaseImpl webRequestTracer) {
if (isCapturingDisabled()) {
return;
}
if (getBeaconConfiguration().getDataCollectionLevel() == DataCollectionLevel.OFF) {
return;
}
StringBuilder eventBuilder = new StringBuilder();
buildBasicEventData(eventBuilder, EventType.WEBREQUEST, webRequestTracer.getURL());
addKeyValuePair(eventBuilder, BEACON_KEY_PARENT_ACTION_ID, parentActionID);
addKeyValuePair(eventBuilder, BEACON_KEY_START_SEQUENCE_NUMBER, webRequestTracer.getStartSequenceNo());
addKeyValuePair(eventBuilder, BEACON_KEY_TIME_0, getTimeSinceSessionStartTime(webRequestTracer.getStartTime()));
addKeyValuePair(eventBuilder, BEACON_KEY_END_SEQUENCE_NUMBER, webRequestTracer.getEndSequenceNo());
addKeyValuePair(eventBuilder, BEACON_KEY_TIME_1, webRequestTracer.getEndTime() - webRequestTracer.getStartTime());
addKeyValuePairIfNotNegative(eventBuilder, BEACON_KEY_WEBREQUEST_BYTES_SENT, webRequestTracer.getBytesSent());
addKeyValuePairIfNotNegative(eventBuilder, BEACON_KEY_WEBREQUEST_BYTES_RECEIVED, webRequestTracer.getBytesReceived());
addKeyValuePairIfNotNegative(eventBuilder, BEACON_KEY_WEBREQUEST_RESPONSECODE, webRequestTracer.getResponseCode());
addEventData(webRequestTracer.getStartTime(), eventBuilder);
}
/**
* Add user identification to Beacon.
*
*
* The serialized data is added to {@link com.dynatrace.openkit.core.caching.BeaconCache}.
*
*
* @param userTag User tag containing data to serialize.
*/
public void identifyUser(String userTag) {
if (isCapturingDisabled()) {
return;
}
if (getBeaconConfiguration().getDataCollectionLevel() != DataCollectionLevel.USER_BEHAVIOR) {
return;
}
StringBuilder eventBuilder = new StringBuilder();
buildBasicEventData(eventBuilder, EventType.IDENTIFY_USER, userTag);
long timestamp = timingProvider.provideTimestampInMilliseconds();
addKeyValuePair(eventBuilder, BEACON_KEY_PARENT_ACTION_ID, 0);
addKeyValuePair(eventBuilder, BEACON_KEY_START_SEQUENCE_NUMBER, createSequenceNumber());
addKeyValuePair(eventBuilder, BEACON_KEY_TIME_0, getTimeSinceSessionStartTime(timestamp));
addEventData(timestamp, eventBuilder);
}
/**
* Send current state of Beacon.
*
*
* This method tries to send all so far collected and serialized data.
*
*
* @param provider Provider for getting an {@link HTTPClient} required to send the data.
*
* @return Returns the last status response retrieved from the server side, or {@code null} if an error occurred.
*/
public StatusResponse send(HTTPClientProvider provider) {
HTTPClient httpClient = provider.createClient(httpConfiguration);
StatusResponse response = null;
while (true) {
// prefix for this chunk - must be built up newly, due to changing timestamps
String prefix = appendMutableBeaconData(immutableBasicBeaconData);
// subtract 1024 to ensure that the chunk does not exceed the send size configured on server side?
// i guess that was the original intention, but i'm not sure about this
// TODO stefan.eberl - This is a quite uncool algorithm and should be improved, avoid subtracting some "magic" number
String chunk = beaconCache.getNextBeaconChunk(sessionNumber, prefix, configuration.getMaxBeaconSize() - 1024, BEACON_DATA_DELIMITER);
if (chunk == null || chunk.isEmpty()) {
// no data added so far or no data to send
return response;
}
byte[] encodedBeacon;
try {
encodedBeacon = chunk.getBytes(CHARSET);
} catch (UnsupportedEncodingException e) {
// must not happen, as UTF-8 should *really* be supported
logger.error(getClass().getSimpleName() + "Required charset \"" + CHARSET + "\" is not supported.", e);
beaconCache.resetChunkedData(sessionNumber);
return response;
}
// send the request
response = httpClient.sendBeaconRequest(clientIPAddress, encodedBeacon);
if (response == null || response.isErroneousResponse()) {
// error happened - but don't know what exactly
// reset the previously retrieved chunk (restore it in internal cache) & retry another time
beaconCache.resetChunkedData(sessionNumber);
break;
} else {
// worked -> remove previously retrieved chunk from cache
beaconCache.removeChunkedData(sessionNumber);
}
}
return response;
}
private String appendMutableBeaconData(String immutableBasicBeaconData) {
StringBuilder mutableBeaconDataBuilder;
if (immutableBasicBeaconData != null && !immutableBasicBeaconData.isEmpty()) {
mutableBeaconDataBuilder = new StringBuilder(immutableBasicBeaconData);
mutableBeaconDataBuilder.append(BEACON_DATA_DELIMITER);
} else {
mutableBeaconDataBuilder = new StringBuilder();
}
// append timestamp data
mutableBeaconDataBuilder.append(createTimestampData());
// append multiplicity
mutableBeaconDataBuilder.append(BEACON_DATA_DELIMITER).append(createMultiplicityData());
return mutableBeaconDataBuilder.toString();
}
/**
* Gets all events.
*
*
* This returns a shallow copy of events entries and is intended only
* for testing purposes.
*
*/
String[] getEvents() {
return beaconCache.getEvents(sessionNumber);
}
/**
* Gets all actions.
*
*
* This returns a shallow copy of all actions and is intended only
* for testing purposes.
*
*/
String[] getActions() {
return beaconCache.getActions(sessionNumber);
}
/**
* Add previously serialized action data to the beacon cache.
*
* @param timestamp The timestamp when the action data occurred.
* @param actionBuilder Contains the serialized action data.
*/
private void addActionData(long timestamp, StringBuilder actionBuilder) {
if (configuration.isCapture()) {
beaconCache.addActionData(sessionNumber, timestamp, actionBuilder.toString());
}
}
/**
* Add previously serialized event data to the beacon cache.
*
* @param timestamp The timestamp when the event data occurred.
* @param eventBuilder Contains the serialized event data.
*/
private void addEventData(long timestamp, StringBuilder eventBuilder) {
if (configuration.isCapture()) {
beaconCache.addEventData(sessionNumber, timestamp, eventBuilder.toString());
}
}
/**
* Clears all previously collected data for this Beacon.
*
*
* This only affects the so far serialized data, which gets removed from the cache.
*
*/
public void clearData() {
// remove all cached data for this Beacon from the cache
beaconCache.deleteCacheEntry(sessionNumber);
}
/**
* Serialization helper for event data.
*
* @param builder String builder storing the serialzed data.
* @param eventType The event's type.
* @param name Event name
* @param parentAction The action on which this event was reported.
* @return The timestamp associated with the event (timestamp since session start time).
*/
private long buildEvent(StringBuilder builder, EventType eventType, String name, ActionImpl parentAction) {
buildBasicEventData(builder, eventType, name);
long eventTimestamp = timingProvider.provideTimestampInMilliseconds();
addKeyValuePair(builder, BEACON_KEY_PARENT_ACTION_ID, parentAction.getID());
addKeyValuePair(builder, BEACON_KEY_START_SEQUENCE_NUMBER, createSequenceNumber());
addKeyValuePair(builder, BEACON_KEY_TIME_0, getTimeSinceSessionStartTime(eventTimestamp));
return eventTimestamp;
}
/**
* Serialization for building basic event data.
*
* @param builder String builder storing serialized data.
* @param eventType The event's type.
* @param name Event's name.
*/
private void buildBasicEventData(StringBuilder builder, EventType eventType, String name) {
addKeyValuePair(builder, BEACON_KEY_EVENT_TYPE, eventType.protocolValue());
if (name != null) {
addKeyValuePair(builder, BEACON_KEY_NAME, truncate(name));
}
addKeyValuePair(builder, BEACON_KEY_THREAD_ID, threadIDProvider.getThreadID());
}
/**
* Serialization helper method for creating basic beacon protocol data.
*
* @return Serialized data.
*/
private String createImmutableBasicBeaconData() {
StringBuilder basicBeaconBuilder = new StringBuilder();
// version and application information
addKeyValuePair(basicBeaconBuilder, BEACON_KEY_PROTOCOL_VERSION, ProtocolConstants.PROTOCOL_VERSION);
addKeyValuePair(basicBeaconBuilder, BEACON_KEY_OPENKIT_VERSION, ProtocolConstants.OPENKIT_VERSION);
addKeyValuePair(basicBeaconBuilder, BEACON_KEY_APPLICATION_ID, configuration.getApplicationID());
addKeyValuePair(basicBeaconBuilder, BEACON_KEY_APPLICATION_NAME, configuration.getApplicationName());
addKeyValuePairIfNotNull(basicBeaconBuilder, BEACON_KEY_APPLICATION_VERSION, configuration.getApplicationVersion());
addKeyValuePair(basicBeaconBuilder, BEACON_KEY_PLATFORM_TYPE, ProtocolConstants.PLATFORM_TYPE_OPENKIT);
addKeyValuePair(basicBeaconBuilder, BEACON_KEY_AGENT_TECHNOLOGY_TYPE, ProtocolConstants.AGENT_TECHNOLOGY_TYPE);
// device/visitor ID, session number and IP address
addKeyValuePair(basicBeaconBuilder, BEACON_KEY_VISITOR_ID, getDeviceID());
addKeyValuePair(basicBeaconBuilder, BEACON_KEY_SESSION_NUMBER, getSessionNumber());
addKeyValuePair(basicBeaconBuilder, BEACON_KEY_CLIENT_IP_ADDRESS, clientIPAddress);
// platform information
addKeyValuePairIfNotNull(basicBeaconBuilder, BEACON_KEY_DEVICE_OS, configuration.getDevice()
.getOperatingSystem());
addKeyValuePairIfNotNull(basicBeaconBuilder, BEACON_KEY_DEVICE_MANUFACTURER, configuration.getDevice()
.getManufacturer());
addKeyValuePairIfNotNull(basicBeaconBuilder, BEACON_KEY_DEVICE_MODEL, configuration.getDevice().getModelID());
BeaconConfiguration beaconConfig = beaconConfiguration.get();
addKeyValuePair(basicBeaconBuilder, BEACON_KEY_DATA_COLLECTION_LEVEL, beaconConfig.getDataCollectionLevel().getIntValue());
addKeyValuePair(basicBeaconBuilder, BEACON_KEY_CRASH_REPORTING_LEVEL, beaconConfig.getCrashReportingLevel().getIntValue());
return basicBeaconBuilder.toString();
}
/**
* Get a visitor ID for the current data collection level
*
* in case of level 2 (USER_BEHAVIOR) the value from the configuration is used
* in case of level 1 (PERFORMANCE) or 0 (OFF) a random number in the positive Long range is used
*
* @return The device identifier
*/
public long getDeviceID() {
return deviceID;
}
/**
* Get a session ID for the current data collection level
*
* in case of level 2 ({@link DataCollectionLevel#USER_BEHAVIOR}) the value from the session id provider is used
* in case of level 1 ({@link DataCollectionLevel#PERFORMANCE}) or 0 ({@link DataCollectionLevel#OFF}) a random positive int value is used
*
* @return Pre calculated session number or {@code 1} if data collection level is not equal to {@link DataCollectionLevel#USER_BEHAVIOR}.
*/
public int getSessionNumber() {
DataCollectionLevel dataCollectionLevel = getBeaconConfiguration().getDataCollectionLevel();
if (dataCollectionLevel == DataCollectionLevel.USER_BEHAVIOR) {
return sessionNumber;
}
return 1;//the visitor/device id is already random, it is fine to use 1 here
}
/**
* Serialization helper method for creating basic timestamp data.
*
* @return Serialized data.
*/
private String createTimestampData() {
StringBuilder timestampBuilder = new StringBuilder();
// timestamp information
addKeyValuePair(timestampBuilder, BEACON_KEY_SESSION_START_TIME, timingProvider.convertToClusterTime(sessionStartTime));
addKeyValuePair(timestampBuilder, BEACON_KEY_TIMESYNC_TIME, timingProvider.convertToClusterTime(sessionStartTime));
if (!timingProvider.isTimeSyncSupported()) {
addKeyValuePair(timestampBuilder, BEACON_KEY_TRANSMISSION_TIME, timingProvider.provideTimestampInMilliseconds());
}
return timestampBuilder.toString();
}
/**
* Serialization helper method for creating multiplicity data.
*
* @return Serialized data.
*/
private String createMultiplicityData() {
StringBuilder multiplicityBuilder = new StringBuilder();
// timestamp information
addKeyValuePair(multiplicityBuilder, BEACON_KEY_MULTIPLICITY, getMultiplicity());
return multiplicityBuilder.toString();
}
/**
* Serialization helper method for adding key/value pairs with string values
*
* @param builder The string builder storing serialized data.
* @param key The key to add.
* @param stringValue The value to add.
*/
private void addKeyValuePair(StringBuilder builder, String key, String stringValue) {
String encodedValue = PercentEncoder.encode(stringValue, CHARSET, RESERVED_CHARACTERS);
if (encodedValue == null) {
// if encoding fails, skip this key/value pair
logger.error(getClass().getSimpleName() + "Skipped encoding of Key/Value: " + key + "/" + stringValue);
return;
}
appendKey(builder, key);
builder.append(encodedValue);
}
/**
* Serialization helper method for adding key/value pairs with string values
*
* if the string value turns out to be null the key value pair is not added
* to the string builder
*
* @param builder The string builder storing serialized data.
* @param key The key to add.
* @param stringValue The value to add.
*/
private void addKeyValuePairIfNotNull(StringBuilder builder, String key, String stringValue) {
if (stringValue != null) {
addKeyValuePair(builder, key, stringValue);
}
}
/**
* Serialization helper method for adding key/value pairs with long values
*
* @param builder The string builder storing serialized data.
* @param key The key to add.
* @param longValue The value to add.
*/
private void addKeyValuePair(StringBuilder builder, String key, long longValue) {
appendKey(builder, key);
builder.append(longValue);
}
/**
* Serialization helper method for adding key/value pairs with int values
*
* @param builder The string builder storing serialized data.
* @param key The key to add.
* @param intValue The value to add.
*/
private void addKeyValuePair(StringBuilder builder, String key, int intValue) {
appendKey(builder, key);
builder.append(intValue);
}
/**
* Serialization helper method for adding key/value pairs with int values
*
* the key value pair is only added to the string builder when the int is not negative
*
* @param builder The string builder storing serialized data.
* @param key The key to add.
* @param intValue The value to add.
*/
private void addKeyValuePairIfNotNegative(StringBuilder builder, String key, int intValue) {
if (intValue >= 0) {
addKeyValuePair(builder, key, intValue);
}
}
/**
* Serialization helper method for adding key/value pairs with double values
*
* @param builder The string builder storing serialized data.
* @param key The key to add.
* @param doubleValue The value to add.
*/
private void addKeyValuePair(StringBuilder builder, String key, double doubleValue) {
appendKey(builder, key);
builder.append(doubleValue);
}
/**
* Serialization helper method for appending a key.
*
* @param builder The string builder storing serialized data.
* @param key The key to add.
*/
private void appendKey(StringBuilder builder, String key) {
if (!builder.toString().isEmpty()) {
builder.append('&');
}
builder.append(key);
builder.append('=');
}
/**
* helper method for truncating name at max name size
*/
private static String truncate(String name) {
name = name.trim();
if (name.length() > MAX_NAME_LEN) {
name = name.substring(0, MAX_NAME_LEN);
}
return name;
}
/**
* Get a timestamp relative to the time this session (aka. beacon) was created.
*
* @param timestamp The absolute timestamp for which to get a relative one.
* @return Relative timestamp.
*/
private long getTimeSinceSessionStartTime(long timestamp) {
return timestamp - sessionStartTime;
}
/**
* Tests if the Beacon is empty.
*
*
* A beacon is considered to be empty, if it does not contain any action or event data.
*
*
* @return {@code true} if the beacon is empty, {@code false} otherwise.
*/
public boolean isEmpty() {
return beaconCache.isEmpty(sessionNumber);
}
/**
* Sets the Beacon configuration.
*
* @param beaconConfiguration The new beacon configuration to set.
*/
public void setBeaconConfiguration(BeaconConfiguration beaconConfiguration) {
if (beaconConfiguration != null) {
this.beaconConfiguration.set(beaconConfiguration);
}
}
/**
* Get the Beacon configuration.
* @return Currently set beacon configuration.
*/
public BeaconConfiguration getBeaconConfiguration() {
return beaconConfiguration.get();
}
/**
* Tests if capturing is allowed.
*
*
* Note: Due to multithreading this behaviour could already change,
* when evaluating the result of this call.
*
*
* @return {@code true} if capturing is allowed, {@code false} otherwise.
*/
boolean isCapturingDisabled() {
return !getBeaconConfiguration().isCapturingAllowed();
}
/**
* Get multiplicity from {@link BeaconConfiguration}.
*
* @return Multiplicity received from the server.
*/
int getMultiplicity() {
return getBeaconConfiguration().getMultiplicity();
}
}