From 2bbb12c94678ef06f0b42afe15fde1b2a758ca5a Mon Sep 17 00:00:00 2001 From: Christian Ricardo Buongarzoni <31810096+christianbuon@users.noreply.github.com> Date: Thu, 3 Oct 2024 13:37:32 -0300 Subject: [PATCH] [feat][sdk-105] Add telemetry (#313) * feat(telemetry): add log events * feat(telemetry): add navigation events * feat(telemetry): add Network events * feat(telemetry): add Manual events * feat(telemetry): add maximum telemetry data * feat(telemetry): describe usage for static factories * fix(telemetry): add maximumTelemetryData to Reactive Streams ConfigBuilder * refactor(telemetry): replace static factories for TelemetryEventTracker * fix(telemetry): remove reference to TelemetryEventTracker in Body * fix(telemetry): set source only as "client" or "server" for Telemetry events * test(telemetry): add tests for TelemetryEvent * refactor(telemetry): modify equals and hashcode, and create a new hashmap in constructor * revert(notifier): previous reference * refactor: set source as an enum * refactor: docs * fix: test * fix: checkstyle * fix: checkstyle * fix: checkstyle * fix: checkstyle * fix: add revapi acceptedBreaks * fix: checkstyles * fix(ci): set expected release configuration * fix(ci): remove jdk7 condition * fix(ci): set release.sh new folder * fix(ci): suggested change * refactor(telemetry): move coerce in logic to RollbarTelemetryEventTracker * refactor(telemetry): rename class * fix(telemetry): checkstyle * refactor(telemetry): modify Telemetry queue capacity * refactor(telemetry): modify Telemetry capacity default value * fix(telemetry): checkstyle * docs(telemetry): fix ConfigBuilder.telemetryEventTracker javadoc * docs(telemetry): fix ConfigBuilder.maximumTelemetryData javadoc --- .github/workflows/ci.yml | 2 +- .palantir/revapi.yml | 8 + .../java/com/rollbar/android/Rollbar.java | 44 ++++ .../com/rollbar/api/payload/data/Source.java | 30 +++ .../api/payload/data/TelemetryEvent.java | 101 +++++++++ .../api/payload/data/TelemetryType.java | 24 ++ .../rollbar/api/payload/data/body/Body.java | 34 ++- .../api/payload/data/TelemetryEventTest.java | 211 ++++++++++++++++++ .../com/rollbar/notifier/RollbarBase.java | 76 ++++++- .../rollbar/notifier/config/CommonConfig.java | 5 + .../notifier/config/ConfigBuilder.java | 57 +++++ .../RollbarTelemetryEventTracker.java | 123 ++++++++++ .../telemetry/TelemetryEventTracker.java | 64 ++++++ .../rollbar/notifier/util/BodyFactory.java | 31 ++- .../notifier/RollbarRecordTelemetryTest.java | 136 +++++++++++ .../notifier/config/ConfigBuilderTest.java | 6 + .../RollbarTelemetryEventTrackerTest.java | 203 +++++++++++++++++ .../notifier/config/ConfigBuilder.java | 54 +++++ 18 files changed, 1197 insertions(+), 12 deletions(-) create mode 100644 rollbar-api/src/main/java/com/rollbar/api/payload/data/Source.java create mode 100644 rollbar-api/src/main/java/com/rollbar/api/payload/data/TelemetryEvent.java create mode 100644 rollbar-api/src/main/java/com/rollbar/api/payload/data/TelemetryType.java create mode 100644 rollbar-api/src/test/java/com/rollbar/api/payload/data/TelemetryEventTest.java create mode 100644 rollbar-java/src/main/java/com/rollbar/notifier/telemetry/RollbarTelemetryEventTracker.java create mode 100644 rollbar-java/src/main/java/com/rollbar/notifier/telemetry/TelemetryEventTracker.java create mode 100644 rollbar-java/src/test/java/com/rollbar/notifier/RollbarRecordTelemetryTest.java create mode 100644 rollbar-java/src/test/java/com/rollbar/notifier/telemetry/RollbarTelemetryEventTrackerTest.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 582e95a3..85459fbf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,4 +105,4 @@ jobs: NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }} NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} run: | - ./gradlew -Dorg.gradle.internal.http.socketTimeout=300000 -Dorg.gradle.internal.http.connectionTimeout=300000 publishToSonatype + ./.github/scripts/release.sh diff --git a/.palantir/revapi.yml b/.palantir/revapi.yml index d16694e0..1c1ab03c 100644 --- a/.palantir/revapi.yml +++ b/.palantir/revapi.yml @@ -25,3 +25,11 @@ acceptedBreaks: justification: "This is a binary compatible change, which could only break custom\ \ implementations of our config interfaces, but those interfaces are not meant\ \ to be implemented by users" + "2.0.0": + com.rollbar:rollbar-java: + - code: "java.method.addedToInterface" + new: "method com.rollbar.notifier.telemetry.TelemetryEventTracker com.rollbar.notifier.config.CommonConfig::telemetryEventTracker()" + justification: "This is going to be added in a major version" + - code: "java.method.addedToInterface" + new: "method int com.rollbar.notifier.config.CommonConfig::maximumTelemetryData()" + justification: "This is going to be added in a major version" diff --git a/rollbar-android/src/main/java/com/rollbar/android/Rollbar.java b/rollbar-android/src/main/java/com/rollbar/android/Rollbar.java index c6c9cf51..5fe245c4 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/Rollbar.java +++ b/rollbar-android/src/main/java/com/rollbar/android/Rollbar.java @@ -12,6 +12,7 @@ import android.util.Log; import com.rollbar.android.notifier.sender.ConnectionAwareSenderFailureStrategy; import com.rollbar.android.provider.ClientProvider; +import com.rollbar.api.payload.data.TelemetryType; import com.rollbar.notifier.config.ConfigProvider; import com.rollbar.notifier.uncaughtexception.RollbarUncaughtExceptionHandler; import com.rollbar.android.provider.NotifierProvider; @@ -926,6 +927,49 @@ public void log(final Throwable error, final Map custom, final S rollbar.log(error, custom, description, level); } + /** + * Record log telemetry event. ({@link TelemetryType#LOG}). + * + * @param level the TelemetryEvent severity (e.g. {@link Level#DEBUG}). + * @param message the message sent for this event (e.g. "hello world"). + */ + public void recordLogEventFor(Level level, final String message) { + rollbar.recordLogEventFor(level, message); + } + + /** + * Record manual telemetry event. ({@link TelemetryType#MANUAL}) + * + * @param level the TelemetryEvent severity (e.g. {@link Level#DEBUG}). + * @param message the message sent for this event (e.g. "hello world"). + */ + public void recordManualEventFor(Level level, final String message) { + rollbar.recordManualEventFor(level, message); + } + + /** + * Record navigation telemetry event with from (origin) and to (destination).({@link TelemetryType#NAVIGATION}) + * + * @param level the TelemetryEvent severity (e.g. {@link Level#DEBUG}). + * @param from the starting point (e.g. "SettingView"). + * @param to the destination point (e.g. "HomeView"). + */ + public void recordNavigationEventFor(Level level, final String from, final String to) { + rollbar.recordNavigationEventFor(level, from, to); + } + + /** + * Record network telemetry event with method, url, and status code.({@link TelemetryType#NETWORK}) + * + * @param level the TelemetryEvent severity (e.g. {@link Level#DEBUG}). + * @param method the verb used (e.g. "POST"). + * @param url the api url (e.g. "http://rollbar.com/test/api"). + * @param statusCode the response status code (e.g. "404"). + */ + public void recordNetworkEventFor(Level level, final String method, final String url, final String statusCode) { + rollbar.recordNetworkEventFor(level, method, url, statusCode); + } + /** * Send payload to Rollbar. * diff --git a/rollbar-api/src/main/java/com/rollbar/api/payload/data/Source.java b/rollbar-api/src/main/java/com/rollbar/api/payload/data/Source.java new file mode 100644 index 00000000..58048168 --- /dev/null +++ b/rollbar-api/src/main/java/com/rollbar/api/payload/data/Source.java @@ -0,0 +1,30 @@ +package com.rollbar.api.payload.data; + +import com.rollbar.api.json.JsonSerializable; + +/** + * The Source of a payload. + */ +public enum Source implements JsonSerializable { + + /** + * A Client source (e.g. Android) + */ + CLIENT("client"), + + /** + * A Server source (e.g. Spring) + */ + SERVER("server"); + + private final String jsonName; + + Source(String jsonName) { + this.jsonName = jsonName; + } + + @Override + public Object asJson() { + return jsonName; + } +} diff --git a/rollbar-api/src/main/java/com/rollbar/api/payload/data/TelemetryEvent.java b/rollbar-api/src/main/java/com/rollbar/api/payload/data/TelemetryEvent.java new file mode 100644 index 00000000..37209f64 --- /dev/null +++ b/rollbar-api/src/main/java/com/rollbar/api/payload/data/TelemetryEvent.java @@ -0,0 +1,101 @@ +package com.rollbar.api.payload.data; + +import com.rollbar.api.json.JsonSerializable; +import com.rollbar.api.truncation.StringTruncatable; +import com.rollbar.api.truncation.TruncationHelper; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Represents an event that allows you to leave a 'breadcrumb' leading up to an exception. + */ +public class TelemetryEvent implements JsonSerializable, StringTruncatable { + + private final TelemetryType type; + private final Level level; + private final Long timestamp; + private final Map body; + private final Source source; + private static final long serialVersionUID = 2843361810242481727L; + + /** + * Construct a TelemetryEvent. + * + * @param telemetryType {@link TelemetryType} + * @param level {@link Level} + * @param timestamp the timestamp for this TelemetryEvent + * @param source {@link Source} + * @param body a map containing all the data required by the {@link TelemetryType} + */ + public TelemetryEvent( + TelemetryType telemetryType, + Level level, + Long timestamp, + Source source, + Map body + ) { + type = telemetryType; + this.timestamp = timestamp; + this.level = level; + this.source = source; + this.body = new HashMap<>(body); + } + + @Override + public Map asJson() { + Map values = new HashMap<>(); + values.put("type", type.asJson()); + values.put("level", level.asJson()); + values.put("source", source.asJson()); + values.put("timestamp_ms", timestamp); + values.put("body", body); + return values; + } + + @Override + public TelemetryEvent truncateStrings(int maxLength) { + Map truncatedMap = new HashMap<>(); + for (Map.Entry entry : body.entrySet()) { + String truncatedValue = TruncationHelper.truncateString(entry.getValue(), maxLength); + truncatedMap.put(entry.getKey(), truncatedValue); + } + return new TelemetryEvent( + this.type, + this.level, + this.timestamp, + this.source, + truncatedMap + ); + } + + @Override + public String toString() { + return "TelemetryEvent{" + + "type='" + type.asJson() + '\'' + + ", level='" + level.asJson() + '\'' + + ", source='" + source + '\'' + + ", timestamp_ms=" + timestamp + + ", body=" + body + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TelemetryEvent that = (TelemetryEvent) o; + return type == that.type && level == that.level && Objects.equals(timestamp, that.timestamp) + && Objects.equals(body, that.body) && Objects.equals(source, that.source); + } + + @Override + public int hashCode() { + return Objects.hash(type, level, timestamp, body, source); + } +} diff --git a/rollbar-api/src/main/java/com/rollbar/api/payload/data/TelemetryType.java b/rollbar-api/src/main/java/com/rollbar/api/payload/data/TelemetryType.java new file mode 100644 index 00000000..eda1759c --- /dev/null +++ b/rollbar-api/src/main/java/com/rollbar/api/payload/data/TelemetryType.java @@ -0,0 +1,24 @@ +package com.rollbar.api.payload.data; + +import com.rollbar.api.json.JsonSerializable; + +/** + * Represents the different types of {@link TelemetryEvent} available. + */ +public enum TelemetryType implements JsonSerializable { + LOG("log"), + MANUAL("manual"), + NAVIGATION("navigation"), + NETWORK("network"); + + private final String jsonName; + + TelemetryType(String jsonName) { + this.jsonName = jsonName; + } + + @Override + public Object asJson() { + return jsonName; + } +} diff --git a/rollbar-api/src/main/java/com/rollbar/api/payload/data/body/Body.java b/rollbar-api/src/main/java/com/rollbar/api/payload/data/body/Body.java index 51f8999c..2ba983de 100755 --- a/rollbar-api/src/main/java/com/rollbar/api/payload/data/body/Body.java +++ b/rollbar-api/src/main/java/com/rollbar/api/payload/data/body/Body.java @@ -1,9 +1,12 @@ package com.rollbar.api.payload.data.body; import com.rollbar.api.json.JsonSerializable; +import com.rollbar.api.payload.data.TelemetryEvent; import com.rollbar.api.truncation.StringTruncatable; import java.util.HashMap; +import java.util.List; +import java.util.Objects; /** * A container for the actual error(s), message, or crash report that caused this error. @@ -14,8 +17,11 @@ public class Body implements JsonSerializable, StringTruncatable { private final BodyContent bodyContent; + private final List telemetryEvents; + private Body(Builder builder) { this.bodyContent = builder.bodyContent; + this.telemetryEvents = builder.telemetryEvents; } /** @@ -34,6 +40,10 @@ public Object asJson() { values.put(bodyContent.getKeyName(), bodyContent); } + if (telemetryEvents != null) { + values.put("telemetry", telemetryEvents); + } + return values; } @@ -53,24 +63,26 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { + + if (!(o instanceof Body)) { return false; } Body body = (Body) o; - - return bodyContent != null ? bodyContent.equals(body.bodyContent) : body.bodyContent == null; + return Objects.equals(bodyContent, body.bodyContent) + && Objects.equals(telemetryEvents, body.telemetryEvents); } @Override public int hashCode() { - return bodyContent != null ? bodyContent.hashCode() : 0; + return Objects.hash(bodyContent, telemetryEvents); } @Override public String toString() { return "Body{" + "bodyContent=" + bodyContent + + ", telemetry=" + telemetryEvents + '}'; } @@ -81,6 +93,8 @@ public static final class Builder { private BodyContent bodyContent; + private List telemetryEvents; + /** * Constructor. */ @@ -95,6 +109,7 @@ public Builder() { */ public Builder(Body body) { this.bodyContent = body.bodyContent; + this.telemetryEvents = body.telemetryEvents; } /** @@ -109,6 +124,17 @@ public Builder bodyContent(BodyContent bodyContent) { return this; } + /** + * The Telemetry events of this body. + * + * @param telemetryEvents the events captured until this payload; + * @return the builder instance. + */ + public Builder telemetryEvents(List telemetryEvents) { + this.telemetryEvents = telemetryEvents; + return this; + } + /** * Builds the {@link Body body}. * diff --git a/rollbar-api/src/test/java/com/rollbar/api/payload/data/TelemetryEventTest.java b/rollbar-api/src/test/java/com/rollbar/api/payload/data/TelemetryEventTest.java new file mode 100644 index 00000000..525ea574 --- /dev/null +++ b/rollbar-api/src/test/java/com/rollbar/api/payload/data/TelemetryEventTest.java @@ -0,0 +1,211 @@ +package com.rollbar.api.payload.data; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +public class TelemetryEventTest { + private HashMap defaultBody; + private static final String TEN_CHARACTERS = "0123456789"; + private static final String BODY_KEY = "someKey"; + TelemetryEventBuilder builder; + + @Before + public void setup() { + builder = new TelemetryEventBuilder(); + defaultBody = makeBody(); + } + + @Test + public void anInstanceShouldBeEqualToIt() { + TelemetryEvent telemetryEvent = builder.build(); + + assertThat(telemetryEvent, is(telemetryEvent)); + } + + @Test + public void shouldNotBeEqualToNull() { + TelemetryEvent telemetryEvent = builder.build(); + + assertNotEquals(telemetryEvent, null); + } + + @Test + public void shouldNotBeEqualToADifferentObject() { + Object anotherObject = "anotherObject"; + TelemetryEvent telemetryEvent = builder.build(); + + assertNotEquals(telemetryEvent, anotherObject); + } + + @Test + public void shouldNotBeEqualToADifferentTypeOfTelemetryEvent() { + TelemetryEvent manualEvent = builder.setTelemetryType(TelemetryType.MANUAL).build(); + TelemetryEvent logEvent = builder.setTelemetryType(TelemetryType.LOG).build(); + + boolean equality = manualEvent.equals(logEvent); + + assertFalse(equality); + } + + @Test + public void shouldNotBeEqualIfTheyHaveDifferentLevel() { + TelemetryEvent debugEvent = builder.setLevel(Level.DEBUG).build(); + TelemetryEvent criticalEvent = builder.setLevel(Level.CRITICAL).build(); + + boolean equality = debugEvent.equals(criticalEvent); + + assertFalse(equality); + } + + @Test + public void shouldNotBeEqualIfTheyHaveDifferentTimestamp() { + TelemetryEvent firstEvent = builder.setTimestamp(10L).build(); + TelemetryEvent secondEvent = builder.setTimestamp(11L).build(); + + boolean equality = firstEvent.equals(secondEvent); + + assertFalse(equality); + } + + @Test + public void shouldNotBeEqualIfTheyHaveDifferentSource() { + TelemetryEvent clientEvent = builder.setSource(Source.CLIENT).build(); + TelemetryEvent serverEvent = builder.setSource(Source.SERVER).build(); + + boolean equality = clientEvent.equals(serverEvent); + + assertFalse(equality); + } + + @Test + public void shouldNotBeEqualIfTheyHaveDifferentBody() { + TelemetryEvent telemetryEvent1 = builder.setBody(defaultBody).build(); + defaultBody.put(BODY_KEY, TEN_CHARACTERS + TEN_CHARACTERS); + TelemetryEvent telemetryEvent2 = builder.setBody(defaultBody).build(); + + boolean equality = telemetryEvent1.equals(telemetryEvent2); + + assertFalse(equality); + } + + + @Test + public void twoTelemetryEventsShouldBeEqualIfTheyHaveTheSameValues() { + TelemetryEvent telemetryEvent1 = builder.setBody(defaultBody).build(); + TelemetryEvent telemetryEvent2 = builder.setBody(defaultBody).build(); + + boolean equality = telemetryEvent1.equals(telemetryEvent2); + + assertTrue(equality); + } + + @Test + public void twoTelemetryEventsShouldHaveTheSameHashCodeIfTheyHaveTheSameValues() { + TelemetryEvent telemetryEvent1 = builder.setBody(defaultBody).build(); + TelemetryEvent telemetryEvent2 = builder.setBody(defaultBody).build(); + + int hashCode1 = telemetryEvent1.hashCode(); + int hashCode2 = telemetryEvent2.hashCode(); + + assertThat(hashCode1, is(hashCode2)); + } + + @Test + public void shouldReturnAJsonRepresentation() { + defaultBody.put("someKey", "someValue"); + HashMap expected = new HashMap<>(); + expected.put("type", "log"); + expected.put("level", "debug"); + expected.put("source", TelemetryEventBuilder.DEFAULT_SOURCE.asJson()); + expected.put("timestamp_ms", TelemetryEventBuilder.DEFAULT_TIMESTAMP); + expected.put("body", defaultBody); + TelemetryEvent telemetryEvent1 = builder.setBody(defaultBody).build(); + + Map json = telemetryEvent1.asJson(); + + assertThat(json, is(expected)); + } + + + @Test + public void shouldReturnAStringRepresentation() { + String expected = "TelemetryEvent{type='log', level='debug', source='" + TelemetryEventBuilder.DEFAULT_SOURCE + '\'' + + ", timestamp_ms=" + TelemetryEventBuilder.DEFAULT_TIMESTAMP + + ", body=" + defaultBody + + '}'; + TelemetryEvent telemetryEvent1 = builder.setBody(defaultBody).build(); + + String stringRepresentation = telemetryEvent1.toString(); + + assertThat(stringRepresentation, is(expected)); + } + + @Test + public void shouldTruncateBody() { + HashMap body = new HashMap<>(); + body.put("short", TEN_CHARACTERS); + body.put("long", TEN_CHARACTERS + TEN_CHARACTERS); + TelemetryEvent expected = builder.setBody(body).build(); + body.put("long", TEN_CHARACTERS + TEN_CHARACTERS + TEN_CHARACTERS); + TelemetryEvent originalTelemetryEvent = builder.setBody(body).build(); + + TelemetryEvent telemetryEventTruncated = originalTelemetryEvent.truncateStrings(20); + + assertThat(telemetryEventTruncated, is(expected)); + } + + private HashMap makeBody() { + HashMap body = new HashMap<>(); + body.put(BODY_KEY, TEN_CHARACTERS); + return body; + } + +} + +class TelemetryEventBuilder { + public static long DEFAULT_TIMESTAMP = 10L; + public static Source DEFAULT_SOURCE = Source.CLIENT; + private long timestamp = DEFAULT_TIMESTAMP; + private TelemetryType telemetryType = TelemetryType.LOG; + private Level level = Level.DEBUG; + private Source source = DEFAULT_SOURCE; + HashMap body = new HashMap<>(); + + TelemetryEventBuilder setTelemetryType(TelemetryType telemetryType) { + this.telemetryType = telemetryType; + return this; + } + + TelemetryEventBuilder setLevel(Level level) { + this.level = level; + return this; + } + + TelemetryEventBuilder setTimestamp(long timestamp) { + this.timestamp = timestamp; + return this; + } + + TelemetryEventBuilder setSource(Source source) { + this.source = source; + return this; + } + + TelemetryEventBuilder setBody(HashMap body) { + this.body = body; + return this; + } + + TelemetryEvent build() { + return new TelemetryEvent(telemetryType, level, timestamp, source, body); + } +} diff --git a/rollbar-java/src/main/java/com/rollbar/notifier/RollbarBase.java b/rollbar-java/src/main/java/com/rollbar/notifier/RollbarBase.java index e1786518..15f458be 100644 --- a/rollbar-java/src/main/java/com/rollbar/notifier/RollbarBase.java +++ b/rollbar-java/src/main/java/com/rollbar/notifier/RollbarBase.java @@ -4,15 +4,21 @@ import com.rollbar.api.payload.Payload; import com.rollbar.api.payload.data.Data; import com.rollbar.api.payload.data.Level; +import com.rollbar.api.payload.data.Source; +import com.rollbar.api.payload.data.TelemetryEvent; +import com.rollbar.api.payload.data.TelemetryType; +import com.rollbar.api.payload.data.body.Body; import com.rollbar.jvmti.ThrowableCache; import com.rollbar.notifier.config.CommonConfig; -import com.rollbar.notifier.sender.json.JsonSerializer; +import com.rollbar.notifier.telemetry.TelemetryEventTracker; import com.rollbar.notifier.truncation.PayloadTruncator; import com.rollbar.notifier.util.BodyFactory; import com.rollbar.notifier.util.ObjectsUtils; import com.rollbar.notifier.wrapper.RollbarThrowableWrapper; import com.rollbar.notifier.wrapper.ThrowableWrapper; + import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; @@ -41,12 +47,60 @@ public abstract class RollbarBase { protected final Lock configReadLock = configReadWriteLock.readLock(); protected final Lock configWriteLock = configReadWriteLock.writeLock(); private final RESULT emptyResult; + private final TelemetryEventTracker telemetryEventTracker; protected RollbarBase(C config, BodyFactory bodyFactory, RESULT emptyResult) { this.config = config; configureTruncation(config); this.bodyFactory = bodyFactory; this.emptyResult = emptyResult; + this.telemetryEventTracker = config.telemetryEventTracker(); + } + + /** + * Record log telemetry event. ({@link TelemetryType#LOG}). + * + * @param level the TelemetryEvent severity (e.g. {@link Level#DEBUG}). + * @param message the message sent for this event (e.g. "hello world"). + */ + public void recordLogEventFor(Level level, String message) { + telemetryEventTracker.recordLogEventFor(level, getSource(), message); + } + + /** + * Record manual telemetry event. ({@link TelemetryType#MANUAL}) + * + * @param level the TelemetryEvent severity (e.g. {@link Level#DEBUG}). + * @param message the message sent for this event (e.g. "hello world"). + */ + public void recordManualEventFor(Level level, String message) { + telemetryEventTracker.recordManualEventFor(level, getSource(), message); + } + + /** + * Record navigation telemetry event with from (origin) and to (destination). + * ({@link TelemetryType#NAVIGATION}). + * + * @param level the TelemetryEvent severity (e.g. {@link Level#DEBUG}). + * @param from the starting point (e.g. "SettingView"). + * @param to the destination point (e.g. "HomeView"). + */ + public void recordNavigationEventFor(Level level, String from, String to) { + telemetryEventTracker.recordNavigationEventFor(level, getSource(), from, to); + } + + /** + * Record network telemetry event with method, url, and status code. + * ({@link TelemetryType#NETWORK}). + * + * @param level the TelemetryEvent severity (e.g. {@link Level#DEBUG}). + * @param method the verb used (e.g. "POST"). + * @param url the api url (e.g. " + * http://rollbar.com/test/api"). + * @param statusCode the response status code (e.g. "404"). + */ + public void recordNetworkEventFor(Level level, String method, String url, String statusCode) { + telemetryEventTracker.recordNetworkEventFor(level, getSource(), method, url, statusCode); } /** @@ -120,9 +174,8 @@ protected Data buildData(CommonConfig config, ThrowableWrapper error, Map telemetryEvents = telemetryEventTracker.dump(); + if (telemetryEvents.isEmpty()) { + return bodyFactory.from(error, description); + } + return bodyFactory.from(error, description, telemetryEvents); + } + + private Source getSource() { + String platform = config.platform(); + if ("android".equals(platform)) { + return Source.CLIENT; + } else { + return Source.SERVER; + } + } } diff --git a/rollbar-java/src/main/java/com/rollbar/notifier/config/CommonConfig.java b/rollbar-java/src/main/java/com/rollbar/notifier/config/CommonConfig.java index d903ecbf..f2fac03c 100644 --- a/rollbar-java/src/main/java/com/rollbar/notifier/config/CommonConfig.java +++ b/rollbar-java/src/main/java/com/rollbar/notifier/config/CommonConfig.java @@ -10,6 +10,7 @@ import com.rollbar.notifier.fingerprint.FingerprintGenerator; import com.rollbar.notifier.provider.Provider; import com.rollbar.notifier.sender.json.JsonSerializer; +import com.rollbar.notifier.telemetry.TelemetryEventTracker; import com.rollbar.notifier.transformer.Transformer; import com.rollbar.notifier.uuid.UuidGenerator; import java.util.List; @@ -210,4 +211,8 @@ public interface CommonConfig { * @return true to truncate payloads otherwise false. */ boolean truncateLargePayloads(); + + int maximumTelemetryData(); + + TelemetryEventTracker telemetryEventTracker(); } diff --git a/rollbar-java/src/main/java/com/rollbar/notifier/config/ConfigBuilder.java b/rollbar-java/src/main/java/com/rollbar/notifier/config/ConfigBuilder.java index cad0ae27..88f56b53 100644 --- a/rollbar-java/src/main/java/com/rollbar/notifier/config/ConfigBuilder.java +++ b/rollbar-java/src/main/java/com/rollbar/notifier/config/ConfigBuilder.java @@ -17,6 +17,8 @@ import com.rollbar.notifier.sender.SyncSender; import com.rollbar.notifier.sender.json.JsonSerializer; import com.rollbar.notifier.sender.json.JsonSerializerImpl; +import com.rollbar.notifier.telemetry.RollbarTelemetryEventTracker; +import com.rollbar.notifier.telemetry.TelemetryEventTracker; import com.rollbar.notifier.transformer.Transformer; import com.rollbar.notifier.uuid.UuidGenerator; import java.lang.Thread.UncaughtExceptionHandler; @@ -85,6 +87,11 @@ public class ConfigBuilder { protected boolean truncateLargePayloads; + private int maximumTelemetryData = + RollbarTelemetryEventTracker.MAXIMUM_CAPACITY_FOR_TELEMETRY_EVENTS; + + private TelemetryEventTracker telemetryEventTracker; + /** * Constructor with an access token. */ @@ -129,6 +136,8 @@ private ConfigBuilder(Config config) { this.appPackages = config.appPackages(); this.defaultLevels = new DefaultLevels(config); this.truncateLargePayloads = config.truncateLargePayloads(); + this.maximumTelemetryData = config.maximumTelemetryData(); + this.telemetryEventTracker = config.telemetryEventTracker(); } /** @@ -471,6 +480,33 @@ public ConfigBuilder truncateLargePayloads(boolean truncate) { return this; } + /** + *

+ * Maximum Telemetry events sent in a payload, only for the default TelemetryEventTracker, if + * a custom implementation is used this value will be ignored. Default is + * {@value RollbarTelemetryEventTracker#MAXIMUM_CAPACITY_FOR_TELEMETRY_EVENTS}. + *

+ * @param maximumTelemetryData max quantity of telemetry events sent. + * @return the builder instance. + */ + public ConfigBuilder maximumTelemetryData(int maximumTelemetryData) { + this.maximumTelemetryData = maximumTelemetryData; + return this; + } + + /** + *

+ * Set a {@link TelemetryEventTracker} implementation. + * Default: {@link RollbarTelemetryEventTracker} with a {@link TimestampProvider}. + *

+ * @param telemetryEventTracker the TelemetryEventTracker implementation. + * @return the builder instance. + */ + public ConfigBuilder telemetryEventTracker(TelemetryEventTracker telemetryEventTracker) { + this.telemetryEventTracker = telemetryEventTracker; + return this; + } + /** * Builds the {@link Config config}. * @@ -501,6 +537,11 @@ public Config build() { this.timestamp = new TimestampProvider(); } + if (telemetryEventTracker == null) { + telemetryEventTracker = + new RollbarTelemetryEventTracker(new TimestampProvider(), maximumTelemetryData); + } + return new ConfigImpl(this); } @@ -560,6 +601,10 @@ private static class ConfigImpl implements Config { private final boolean truncateLargePayloads; + private final int maximumTelemetryData; + + private final TelemetryEventTracker telemetryEventTracker; + ConfigImpl(ConfigBuilder builder) { this.accessToken = builder.accessToken; this.endpoint = builder.endpoint; @@ -592,6 +637,8 @@ private static class ConfigImpl implements Config { this.enabled = builder.enabled; this.defaultLevels = builder.defaultLevels; this.truncateLargePayloads = builder.truncateLargePayloads; + this.maximumTelemetryData = builder.maximumTelemetryData; + this.telemetryEventTracker = builder.telemetryEventTracker; } @Override @@ -738,5 +785,15 @@ public Level defaultThrowableLevel() { public boolean truncateLargePayloads() { return this.truncateLargePayloads; } + + @Override + public int maximumTelemetryData() { + return this.maximumTelemetryData; + } + + @Override + public TelemetryEventTracker telemetryEventTracker() { + return this.telemetryEventTracker; + } } } diff --git a/rollbar-java/src/main/java/com/rollbar/notifier/telemetry/RollbarTelemetryEventTracker.java b/rollbar-java/src/main/java/com/rollbar/notifier/telemetry/RollbarTelemetryEventTracker.java new file mode 100644 index 00000000..265f11db --- /dev/null +++ b/rollbar-java/src/main/java/com/rollbar/notifier/telemetry/RollbarTelemetryEventTracker.java @@ -0,0 +1,123 @@ +package com.rollbar.notifier.telemetry; + +import com.rollbar.api.payload.data.Level; +import com.rollbar.api.payload.data.Source; +import com.rollbar.api.payload.data.TelemetryEvent; +import com.rollbar.api.payload.data.TelemetryType; +import com.rollbar.notifier.provider.Provider; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Default {@link TelemetryEventTracker}. + */ +public class RollbarTelemetryEventTracker implements TelemetryEventTracker { + public static final int MAXIMUM_CAPACITY_FOR_TELEMETRY_EVENTS = 100; + private final int maximumTelemetryData; + private final Queue telemetryEvents = new ConcurrentLinkedQueue<>(); + private final Provider timestampProvider; + private static final String LOG_KEY_MESSAGE = "message"; + private static final String NAVIGATION_KEY_FROM = "from"; + private static final String NAVIGATION_KEY_TO = "to"; + private static final String NETWORK_KEY_METHOD = "method"; + private static final String NETWORK_KEY_URL = "url"; + private static final String NETWORK_KEY_STATUS_CODE = "status_code"; + private static final int NO_CAPACITY = 0; + + /** + * Construct a {@link RollbarTelemetryEventTracker}. + * + * @param timestampProvider A Provider of timestamps for the events + * @param maximumTelemetryData Maximum number of accumulated events (This value can be between 0 + * and 100, exceed any of these thresholds and the closest will be + * taken) + */ + public RollbarTelemetryEventTracker( + Provider timestampProvider, + int maximumTelemetryData + ) { + if (maximumTelemetryData < NO_CAPACITY) { + this.maximumTelemetryData = NO_CAPACITY; + } else { + this.maximumTelemetryData = + Math.min(maximumTelemetryData, MAXIMUM_CAPACITY_FOR_TELEMETRY_EVENTS); + } + this.timestampProvider = timestampProvider; + } + + @Override + public List dump() { + List events = new ArrayList<>(telemetryEvents); + telemetryEvents.clear(); + return events; + } + + @Override + public void recordLogEventFor(Level level, Source source, String message) { + Map body = new HashMap<>(); + body.put(LOG_KEY_MESSAGE, message); + addEvent(new TelemetryEvent(TelemetryType.LOG, level, getTimestamp(), source, body)); + } + + @Override + public void recordManualEventFor(Level level, Source source, String message) { + Map body = new HashMap<>(); + body.put(LOG_KEY_MESSAGE, message); + addEvent(new TelemetryEvent(TelemetryType.MANUAL, level, getTimestamp(), source, body)); + } + + @Override + public void recordNavigationEventFor(Level level, Source source, String from, String to) { + Map body = new HashMap<>(); + body.put(NAVIGATION_KEY_FROM, from); + body.put(NAVIGATION_KEY_TO, to); + addEvent(new TelemetryEvent(TelemetryType.NAVIGATION, level, getTimestamp(), source, body)); + } + + @Override + public void recordNetworkEventFor( + Level level, + Source source, + String method, + String url, + String statusCode + ) { + Map body = new HashMap<>(); + body.put(NETWORK_KEY_METHOD, method); + body.put(NETWORK_KEY_URL, url); + body.put(NETWORK_KEY_STATUS_CODE, statusCode); + addEvent(new TelemetryEvent(TelemetryType.NETWORK, level, getTimestamp(), source, body)); + } + + private void addEvent(TelemetryEvent telemetryEvent) { + if (doNotRecordEvents()) { + return; + } + + if (maxCapacityReached()) { + removeOldestEvent(); + } + telemetryEvents.add(telemetryEvent); + } + + private boolean doNotRecordEvents() { + return maximumTelemetryData == NO_CAPACITY; + } + + private boolean maxCapacityReached() { + return telemetryEvents.size() >= maximumTelemetryData; + } + + private void removeOldestEvent() { + telemetryEvents.poll(); + } + + private long getTimestamp() { + return timestampProvider.provide(); + } +} diff --git a/rollbar-java/src/main/java/com/rollbar/notifier/telemetry/TelemetryEventTracker.java b/rollbar-java/src/main/java/com/rollbar/notifier/telemetry/TelemetryEventTracker.java new file mode 100644 index 00000000..a5bbc16d --- /dev/null +++ b/rollbar-java/src/main/java/com/rollbar/notifier/telemetry/TelemetryEventTracker.java @@ -0,0 +1,64 @@ +package com.rollbar.notifier.telemetry; + +import com.rollbar.api.payload.data.Level; +import com.rollbar.api.payload.data.Source; +import com.rollbar.api.payload.data.TelemetryEvent; +import com.rollbar.api.payload.data.TelemetryType; + +import java.util.List; + +public interface TelemetryEventTracker { + + /** + * Dump all the events recorded. + */ + List dump(); + + /** + * Record log telemetry event. ({@link TelemetryType#LOG}). + * + * @param level the TelemetryEvent severity (e.g. {@link Level#DEBUG}). + * @param source the {@link Source} this event is recorded from (e.g. {@link Source#CLIENT}). + * @param message the message sent for this event (e.g. "hello world"). + */ + void recordLogEventFor(Level level, Source source, String message); + + /** + * Record manual telemetry event. ({@link TelemetryType#MANUAL}) . + * + * @param level the TelemetryEvent severity (e.g. {@link Level#DEBUG}). + * @param source the {@link Source} this event is recorded from (e.g. {@link Source#CLIENT}). + * @param message the message sent for this event (e.g. "hello world"). + */ + void recordManualEventFor(Level level, Source source, String message); + + /** + * Record navigation telemetry event with from (origin) and to (destination). + * ({@link TelemetryType#NAVIGATION}). + * + * @param level the TelemetryEvent severity (e.g. {@link Level#DEBUG}). + * @param source the {@link Source} this event is recorded from (e.g. {@link Source#CLIENT}). + * @param from the starting point (e.g. "SettingView"). + * @param to the destination point (e.g. "HomeView"). + */ + void recordNavigationEventFor(Level level, Source source, String from, String to); + + /** + * Record network telemetry event with method, url, and status code. + * ({@link TelemetryType#NETWORK}). + * + * @param level the TelemetryEvent severity (e.g. {@link Level#DEBUG}). + * @param source the {@link Source} this event is recorded from (e.g. {@link Source#CLIENT}). + * @param method the verb used (e.g. "POST"). + * @param url the api url (e.g. " + * http://rollbar.com/test/api"). + * @param statusCode the response status code (e.g. "404"). + */ + void recordNetworkEventFor( + Level level, + Source source, + String method, + String url, + String statusCode + ); +} diff --git a/rollbar-java/src/main/java/com/rollbar/notifier/util/BodyFactory.java b/rollbar-java/src/main/java/com/rollbar/notifier/util/BodyFactory.java index 7df49631..50a847c0 100755 --- a/rollbar-java/src/main/java/com/rollbar/notifier/util/BodyFactory.java +++ b/rollbar-java/src/main/java/com/rollbar/notifier/util/BodyFactory.java @@ -1,5 +1,6 @@ package com.rollbar.notifier.util; +import com.rollbar.api.payload.data.TelemetryEvent; import com.rollbar.api.payload.data.body.Body; import com.rollbar.api.payload.data.body.ExceptionInfo; import com.rollbar.api.payload.data.body.Frame; @@ -10,6 +11,7 @@ import com.rollbar.jvmti.ThrowableCache; import com.rollbar.notifier.wrapper.RollbarThrowableWrapper; import com.rollbar.notifier.wrapper.ThrowableWrapper; + import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -22,11 +24,10 @@ public class BodyFactory { /** * Builds the body for the throwable and description supplied. * - * @param throwable the throwable. + * @param throwable the throwable. * @param description the description. * @return the body. - * - * @deprecated Replaced by {@link #from(ThrowableWrapper, String)}. + * @deprecated Replaced by {@link #from(ThrowableWrapper, String)}. */ @Deprecated public Body from(Throwable throwable, String description) { @@ -41,11 +42,33 @@ public Body from(Throwable throwable, String description) { * supplied. * * @param throwableWrapper the throwable proxy. - * @param description the description. + * @param description the description. * @return the body. */ public Body from(ThrowableWrapper throwableWrapper, String description) { Body.Builder builder = new Body.Builder(); + return from(throwableWrapper, description, builder); + } + + /** + * Builds the body from the {@link ThrowableWrapper throwableWrapper}, the description + * supplied and telemetry events. + * + * @param throwableWrapper the throwable proxy. + * @param description the description. + * @param telemetryEvents the telemetry events. + * @return the body. + */ + public Body from( + ThrowableWrapper throwableWrapper, + String description, + List telemetryEvents + ) { + Body.Builder builder = new Body.Builder().telemetryEvents(telemetryEvents); + return from(throwableWrapper, description, builder); + } + + private Body from(ThrowableWrapper throwableWrapper, String description, Body.Builder builder) { if (throwableWrapper == null) { return builder.bodyContent(message(description)).build(); } diff --git a/rollbar-java/src/test/java/com/rollbar/notifier/RollbarRecordTelemetryTest.java b/rollbar-java/src/test/java/com/rollbar/notifier/RollbarRecordTelemetryTest.java new file mode 100644 index 00000000..e645fae3 --- /dev/null +++ b/rollbar-java/src/test/java/com/rollbar/notifier/RollbarRecordTelemetryTest.java @@ -0,0 +1,136 @@ +package com.rollbar.notifier; + +import static com.rollbar.notifier.config.ConfigBuilder.withAccessToken; + +import static org.mockito.Mockito.verify; + +import com.rollbar.api.payload.Payload; +import com.rollbar.api.payload.data.Level; +import com.rollbar.api.payload.data.Source; +import com.rollbar.notifier.config.Config; +import com.rollbar.notifier.telemetry.TelemetryEventTracker; +import com.rollbar.notifier.util.BodyFactory; + +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class RollbarRecordTelemetryTest { + private final Level level = Level.DEBUG; + + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + @Mock + private TelemetryEventTracker telemetryEventTracker; + + @Mock + private BodyFactory dummyFactory; + + @Test + public void shouldRecordALogEventWithServerSourceWhenThePlatformIsNotAndroid() { + String message = "message"; + RollbarBase sut = new RollbarBaseImpl(getConfigWith("spring"), dummyFactory, null); + + sut.recordLogEventFor(level, message); + + verify(telemetryEventTracker).recordLogEventFor(level, Source.SERVER, message); + } + + @Test + public void shouldRecordALogEventWithClientSourceWhenThePlatformIsAndroid() { + String message = "message"; + RollbarBase sut = new RollbarBaseImpl(getConfigWith("android"), dummyFactory, null); + + sut.recordLogEventFor(level, message); + + verify(telemetryEventTracker).recordLogEventFor(level, Source.CLIENT, message); + } + + @Test + public void shouldRecordAManualEventWithServerSourceWhenThePlatformIsNotAndroid() { + String message = "message"; + RollbarBase sut = new RollbarBaseImpl(getConfigWith(null), dummyFactory, null); + + sut.recordManualEventFor(level, message); + + verify(telemetryEventTracker).recordManualEventFor(level, Source.SERVER, message); + } + + @Test + public void shouldRecordAManualEventWithClientSourceWhenThePlatformIsAndroid() { + String message = "message"; + RollbarBase sut = new RollbarBaseImpl(getConfigWith("android"), dummyFactory, null); + + sut.recordManualEventFor(level, message); + + verify(telemetryEventTracker).recordManualEventFor(level, Source.CLIENT, message); + } + + @Test + public void shouldRecordANetworkEventWithServerSourceWhenThePlatformIsNotAndroid() { + String method = "method"; + String url = "url"; + String statusCode = "status code"; + RollbarBase sut = new RollbarBaseImpl(getConfigWith("any"), dummyFactory, null); + + sut.recordNetworkEventFor(level, method, url, statusCode); + + verify(telemetryEventTracker).recordNetworkEventFor(level, Source.SERVER, method, url, statusCode); + } + + @Test + public void shouldRecordANetworkEventWithClientSourceWhenThePlatformIsAndroid() { + String method = "method"; + String url = "url"; + String statusCode = "status code"; + RollbarBase sut = new RollbarBaseImpl(getConfigWith("android"), dummyFactory, null); + + sut.recordNetworkEventFor(level, method, url, statusCode); + + verify(telemetryEventTracker).recordNetworkEventFor(level, Source.CLIENT, method, url, statusCode); + } + + @Test + public void shouldRecordANavigationEventWithServerSourceWhenThePlatformIsNotAndroid() { + String from = "from"; + String to = "to"; + RollbarBase sut = new RollbarBaseImpl(getConfigWith("any"), dummyFactory, null); + + sut.recordNavigationEventFor(level, from, to); + + verify(telemetryEventTracker).recordNavigationEventFor(level, Source.SERVER, from, to); + } + + @Test + public void shouldRecordANavigationEventWithClientSourceWhenThePlatformIsAndroid() { + String from = "from"; + String to = "to"; + RollbarBase sut = new RollbarBaseImpl(getConfigWith("android"), dummyFactory, null); + + sut.recordNavigationEventFor(level, from, to); + + verify(telemetryEventTracker).recordNavigationEventFor(level, Source.CLIENT, from, to); + } + + private Config getConfigWith(String platform) { + return withAccessToken("dummy token") + .telemetryEventTracker(telemetryEventTracker) + .platform(platform) + .build(); + } + + static class RollbarBaseImpl extends RollbarBase { + + protected RollbarBaseImpl(Config config, BodyFactory bodyFactory, Void emptyResult) { + super(config, bodyFactory, emptyResult); + } + + @Override + protected Void sendPayload(Config config, Payload payload) { + return null; + } + } +} diff --git a/rollbar-java/src/test/java/com/rollbar/notifier/config/ConfigBuilderTest.java b/rollbar-java/src/test/java/com/rollbar/notifier/config/ConfigBuilderTest.java index 4c05dc14..b8199ee7 100644 --- a/rollbar-java/src/test/java/com/rollbar/notifier/config/ConfigBuilderTest.java +++ b/rollbar-java/src/test/java/com/rollbar/notifier/config/ConfigBuilderTest.java @@ -43,6 +43,8 @@ public class ConfigBuilderTest { static final String FRAMEWORK = "framework"; + private static final int DEFAULT_CAPACITY_FOR_TELEMETRY_EVENTS = 100; + @Rule public MockitoRule rule = MockitoJUnit.rule(); @@ -147,10 +149,12 @@ public void shouldBuildTheConfiguration() { assertThat(config.proxy(), is(proxy)); assertThat(config.handleUncaughtErrors(), is(false)); assertThat(config.isEnabled(), is(false)); + assertThat(config.maximumTelemetryData(), is(DEFAULT_CAPACITY_FOR_TELEMETRY_EVENTS)); } @Test public void shouldBuildWithConfig() { + int maximumTelemetryData = 3; Config config = ConfigBuilder.withAccessToken(ACCESS_TOKEN) .environment(ENVIRONMENT) .codeVersion(CODE_VERSION) @@ -172,6 +176,7 @@ public void shouldBuildWithConfig() { .proxy(proxy) .handleUncaughtErrors(false) .enabled(false) + .maximumTelemetryData(maximumTelemetryData) .build(); Config copy = withConfig(config).build(); @@ -197,5 +202,6 @@ public void shouldBuildWithConfig() { assertThat(config.proxy(), is(copy.proxy())); assertThat(config.handleUncaughtErrors(), is(copy.handleUncaughtErrors())); assertThat(config.isEnabled(), is(copy.isEnabled())); + assertThat(config.maximumTelemetryData(), is(maximumTelemetryData)); } } \ No newline at end of file diff --git a/rollbar-java/src/test/java/com/rollbar/notifier/telemetry/RollbarTelemetryEventTrackerTest.java b/rollbar-java/src/test/java/com/rollbar/notifier/telemetry/RollbarTelemetryEventTrackerTest.java new file mode 100644 index 00000000..a0d4475f --- /dev/null +++ b/rollbar-java/src/test/java/com/rollbar/notifier/telemetry/RollbarTelemetryEventTrackerTest.java @@ -0,0 +1,203 @@ +package com.rollbar.notifier.telemetry; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import com.rollbar.api.payload.data.Level; +import com.rollbar.api.payload.data.Source; +import com.rollbar.api.payload.data.TelemetryEvent; +import com.rollbar.api.payload.data.TelemetryType; +import com.rollbar.notifier.provider.timestamp.TimestampProvider; + +import org.junit.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class RollbarTelemetryEventTrackerTest { + + private static final Source SOURCE = Source.SERVER; + private static final String MESSAGE = "Any message"; + private static final String FROM = "Any origin"; + private static final String TO = "Any destination"; + private static final String METHOD = "Any method"; + private static final String URL = "Any url"; + private static final String STATUS_CODE = "Any status code"; + private static final Level LEVEL = Level.DEBUG; + private static final int MAXIMUM_TELEMETRY_DATA = 2; + private static final long TIMESTAMP = 10L; + private final TimestampProvider fakeTimestampProvider = new TimestampProviderFake(); + private final TelemetryEventTracker telemetryEventTracker = newEventTracker(MAXIMUM_TELEMETRY_DATA); + private static final int MINIMUM_CAPACITY_FOR_TELEMETRY_EVENTS = 0; + private static final int MAXIMUM_CAPACITY_FOR_TELEMETRY_EVENTS = 100; + + @Test + public void shouldDiscardOldestEventsWhenMaxCapacityIsReached() { + telemetryEventTracker.recordManualEventFor(LEVEL, SOURCE, MESSAGE); + telemetryEventTracker.recordLogEventFor(LEVEL, SOURCE, MESSAGE); + telemetryEventTracker.recordLogEventFor(LEVEL, SOURCE, MESSAGE); + + List telemetryEvents = telemetryEventTracker.dump(); + + assertThat(telemetryEvents.size(), is(MAXIMUM_TELEMETRY_DATA)); + verifyContainsOnlyLogEvents(telemetryEvents); + } + + @Test + public void shouldTrackALogEvent() { + Map expectedJson = getExpectedJsonForALogTelemetryEvent(); + + telemetryEventTracker.recordLogEventFor(LEVEL, SOURCE, MESSAGE); + + assertThat(getTrackedEventAsJson(), is(expectedJson)); + } + + @Test + public void shouldTrackAManualEvent() { + Map expectedJson = getExpectedJsonForAManualTelemetryEvent(); + + telemetryEventTracker.recordManualEventFor(LEVEL, SOURCE, MESSAGE); + + assertThat(getTrackedEventAsJson(), is(expectedJson)); + } + + @Test + public void shouldTrackANavigationEvent() { + Map expectedJson = getExpectedJsonForANavigationTelemetryEvent(); + + telemetryEventTracker.recordNavigationEventFor(LEVEL, SOURCE, FROM, TO); + + assertThat(getTrackedEventAsJson(), is(expectedJson)); + } + + @Test + public void shouldTrackANetworkEvent() { + Map expectedJson = getExpectedJsonForANetworkTelemetryEvent(); + + telemetryEventTracker.recordNetworkEventFor(LEVEL, SOURCE, METHOD, URL, STATUS_CODE); + + assertThat(getTrackedEventAsJson(), is(expectedJson)); + } + + @Test + public void shouldSetTheMaximumTelemetryDataLimitedToItsLowerLimit() { + TelemetryEventTracker telemetryEventTracker = newEventTracker(MINIMUM_CAPACITY_FOR_TELEMETRY_EVENTS - 1); + + List telemetryEvents = record70EventsAndDump(telemetryEventTracker); + + assertThat(telemetryEvents.size(), is(MINIMUM_CAPACITY_FOR_TELEMETRY_EVENTS)); + } + + @Test + public void shouldSetTheMaximumTelemetryDataLimitedToItsUpperLimit() { + TelemetryEventTracker telemetryEventTracker = newEventTracker(MAXIMUM_CAPACITY_FOR_TELEMETRY_EVENTS + 1); + + List telemetryEvents = record70EventsAndDump(telemetryEventTracker); + + assertThat(telemetryEvents.size(), is(MAXIMUM_CAPACITY_FOR_TELEMETRY_EVENTS)); + } + + @Test + public void shouldSetTheMaximumTelemetryDataLimitedToAValueBetweenBounds() { + int maximumTelemetryEvents = 20; + TelemetryEventTracker telemetryEventTracker = newEventTracker(maximumTelemetryEvents); + + List telemetryEvents = record70EventsAndDump(telemetryEventTracker); + + assertThat(telemetryEvents.size(), is(maximumTelemetryEvents)); + } + + private TelemetryEventTracker newEventTracker(int maximumTelemetryData) { + return new RollbarTelemetryEventTracker( + fakeTimestampProvider, + maximumTelemetryData + ); + } + + private List record70EventsAndDump(TelemetryEventTracker telemetryEventTracker) { + for (int i = 0; i < 120; i++) { + telemetryEventTracker.recordManualEventFor(LEVEL, SOURCE, MESSAGE); + } + return telemetryEventTracker.dump(); + } + + private Map getTrackedEventAsJson() { + return getFirstEvent().asJson(); + } + + private TelemetryEvent getFirstEvent() { + return telemetryEventTracker.dump().get(0); + } + + private Map getExpectedJsonForALogTelemetryEvent() { + Map map = commonFields(); + map.put("type", TelemetryType.LOG.asJson()); + + Map body = new HashMap<>(); + body.put("message", MESSAGE); + + map.put("body", body); + return map; + } + + private Map getExpectedJsonForAManualTelemetryEvent() { + Map map = commonFields(); + map.put("type", TelemetryType.MANUAL.asJson()); + + Map body = new HashMap<>(); + body.put("message", MESSAGE); + + map.put("body", body); + return map; + } + + private Map getExpectedJsonForANavigationTelemetryEvent() { + Map map = commonFields(); + map.put("type", TelemetryType.NAVIGATION.asJson()); + + Map body = new HashMap<>(); + body.put("from", FROM); + body.put("to", TO); + + map.put("body", body); + return map; + } + + private Map getExpectedJsonForANetworkTelemetryEvent() { + Map map = commonFields(); + map.put("type", TelemetryType.NETWORK.asJson()); + + Map body = new HashMap<>(); + body.put("method", METHOD); + body.put("url", URL); + body.put("status_code", STATUS_CODE); + + map.put("body", body); + return map; + } + + private Map commonFields() { + Map map = new HashMap<>(); + map.put("level", LEVEL.asJson()); + map.put("source", SOURCE.asJson()); + map.put("timestamp_ms", TIMESTAMP); + return map; + } + + private void verifyContainsOnlyLogEvents(List telemetryEvents) { + Map expectedJson = getExpectedJsonForALogTelemetryEvent(); + + for (int index = 0; index < telemetryEvents.size(); index++) { + TelemetryEvent telemetryEvent = telemetryEvents.get(index); + assertThat(telemetryEvent.asJson(), is(expectedJson)); + } + } + + private static class TimestampProviderFake extends TimestampProvider { + @Override + public Long provide() { + return TIMESTAMP; + } + } +} diff --git a/rollbar-reactive-streams/src/main/java/com/rollbar/reactivestreams/notifier/config/ConfigBuilder.java b/rollbar-reactive-streams/src/main/java/com/rollbar/reactivestreams/notifier/config/ConfigBuilder.java index 16746f87..48536116 100644 --- a/rollbar-reactive-streams/src/main/java/com/rollbar/reactivestreams/notifier/config/ConfigBuilder.java +++ b/rollbar-reactive-streams/src/main/java/com/rollbar/reactivestreams/notifier/config/ConfigBuilder.java @@ -15,6 +15,8 @@ import com.rollbar.notifier.provider.timestamp.TimestampProvider; import com.rollbar.notifier.sender.SyncSender; import com.rollbar.notifier.sender.json.JsonSerializer; +import com.rollbar.notifier.telemetry.RollbarTelemetryEventTracker; +import com.rollbar.notifier.telemetry.TelemetryEventTracker; import com.rollbar.notifier.transformer.Transformer; import com.rollbar.notifier.uuid.UuidGenerator; import com.rollbar.reactivestreams.notifier.sender.AsyncSender; @@ -59,6 +61,10 @@ public final class ConfigBuilder { private boolean enabled; private DefaultLevels defaultLevels; private boolean truncateLargePayloads; + private int maximumTelemetryData = + RollbarTelemetryEventTracker.MAXIMUM_CAPACITY_FOR_TELEMETRY_EVENTS; + private TelemetryEventTracker telemetryEventTracker; + /** * Constructor with an access token. @@ -100,6 +106,8 @@ private ConfigBuilder(Config config) { this.appPackages = config.appPackages(); this.defaultLevels = new DefaultLevels(config); this.truncateLargePayloads = config.truncateLargePayloads(); + this.maximumTelemetryData = config.maximumTelemetryData(); + this.telemetryEventTracker = config.telemetryEventTracker(); } private ConfigBuilder(Sender sender) { @@ -461,6 +469,33 @@ public ConfigBuilder truncateLargePayloads(boolean truncate) { return this; } + /** + *

+ * Maximum Telemetry events sent in a payload, only for the default TelemetryEventTracker, if + * a custom implementation is used this value will be ignored. Default is + * {@value RollbarTelemetryEventTracker#MAXIMUM_CAPACITY_FOR_TELEMETRY_EVENTS}. + *

+ * @param maximumTelemetryData max quantity of telemetry events sent. + * @return the builder instance. + */ + public ConfigBuilder maximumTelemetryData(int maximumTelemetryData) { + this.maximumTelemetryData = maximumTelemetryData; + return this; + } + + /** + *

+ * Set a {@link TelemetryEventTracker} implementation. + * Default: {@link RollbarTelemetryEventTracker} with a {@link TimestampProvider}. + *

+ * @param telemetryEventTracker the TelemetryEventTracker implementation. + * @return the builder instance. + */ + public ConfigBuilder telemetryEventTracker(TelemetryEventTracker telemetryEventTracker) { + this.telemetryEventTracker = telemetryEventTracker; + return this; + } + /** * Builds the {@link Config config}. * @@ -494,6 +529,11 @@ public Config build() { this.timestamp = new TimestampProvider(); } + if (telemetryEventTracker == null) { + telemetryEventTracker = + new RollbarTelemetryEventTracker(new TimestampProvider(), maximumTelemetryData); + } + return new ConfigImpl(this); } @@ -525,6 +565,8 @@ private static class ConfigImpl implements Config { private final DefaultLevels defaultLevels; private final JsonSerializer jsonSerializer; private final boolean truncateLargePayloads; + private final int maximumTelemetryData; + private final TelemetryEventTracker telemetryEventTracker; ConfigImpl(ConfigBuilder builder) { this.accessToken = builder.accessToken; @@ -557,6 +599,8 @@ private static class ConfigImpl implements Config { this.defaultLevels = builder.defaultLevels; this.jsonSerializer = builder.jsonSerializer; this.truncateLargePayloads = builder.truncateLargePayloads; + this.maximumTelemetryData = builder.maximumTelemetryData; + this.telemetryEventTracker = builder.telemetryEventTracker; } @Override @@ -698,5 +742,15 @@ public Level defaultThrowableLevel() { public boolean truncateLargePayloads() { return truncateLargePayloads; } + + @Override + public int maximumTelemetryData() { + return this.maximumTelemetryData; + } + + @Override + public TelemetryEventTracker telemetryEventTracker() { + return this.telemetryEventTracker; + } } }