Skip to content

Commit

Permalink
add usage tracking (with segment) (airbytehq#174)
Browse files Browse the repository at this point in the history
  • Loading branch information
cgardens authored Sep 9, 2020
1 parent 9f71989 commit 38e84f1
Show file tree
Hide file tree
Showing 22 changed files with 774 additions and 21 deletions.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ DATABASE_URL=jdbc:postgresql://db:5432/dataline
CONFIG_ROOT=/data
WORKSPACE_ROOT=/tmp/workspace
WORKSPACE_DOCKER_MOUNT=workspace
TRACKING_STRATEGY=segment
7 changes: 7 additions & 0 deletions dataline-analytics/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
dependencies {
implementation 'com.segment.analytics.java:analytics:2.1.1'


implementation project(':dataline-config:models')
implementation project(':dataline-config:persistence')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* MIT License
*
* Copyright (c) 2020 Dataline
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package io.dataline.analytics;

import java.util.Collections;
import java.util.Map;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LoggingTrackingClient implements TrackingClient {

private static final Logger LOGGER = LoggerFactory.getLogger(LoggingTrackingClient.class);

private final Supplier<TrackingIdentity> identitySupplier;

public LoggingTrackingClient(Supplier<TrackingIdentity> identitySupplier) {
this.identitySupplier = identitySupplier;
}

@Override
public void identify() {
LOGGER.info("identify. userId: {}", identitySupplier.get().getCustomerId());
}

@Override
public void track(String action) {
track(action, Collections.emptyMap());
}

@Override
public void track(String action, Map<String, Object> metadata) {
LOGGER.info("track. userId: {} action: {}, metadata: {}", identitySupplier.get().getCustomerId(), action, metadata);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* MIT License
*
* Copyright (c) 2020 Dataline
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package io.dataline.analytics;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.segment.analytics.Analytics;
import com.segment.analytics.messages.IdentifyMessage;
import com.segment.analytics.messages.TrackMessage;
import java.util.Collections;
import java.util.Map;
import java.util.function.Supplier;

public class SegmentTrackingClient implements TrackingClient {

private static final String SEGMENT_WRITE_KEY = "7UDdp5K55CyiGgsauOr2pNNujGvmhaeu";

// Analytics is threadsafe.
private final Analytics analytics;
private final Supplier<TrackingIdentity> identitySupplier;

@VisibleForTesting
SegmentTrackingClient(Supplier<TrackingIdentity> identitySupplier, Analytics analytics) {
this.identitySupplier = identitySupplier;
this.analytics = analytics;
}

public SegmentTrackingClient(Supplier<TrackingIdentity> identitySupplier) {
this.analytics = Analytics.builder(SEGMENT_WRITE_KEY).build();
this.identitySupplier = identitySupplier;
}

@Override
public void identify() {
final TrackingIdentity trackingIdentity = identitySupplier.get();
final ImmutableMap.Builder<String, Object> identityMetadataBuilder = ImmutableMap.<String, Object>builder()
.put("anonymized", trackingIdentity.isAnonymousDataCollection())
.put("subscribed_newsletter", trackingIdentity.isNews())
.put("subscribed_security", trackingIdentity.isSecurityUpdates());
trackingIdentity.getEmail().ifPresent(email -> identityMetadataBuilder.put("email", email));

analytics.enqueue(IdentifyMessage.builder()
.userId(trackingIdentity.getCustomerId().toString())
.traits(identityMetadataBuilder.build()));
}

@Override
public void track(String action) {
track(action, Collections.emptyMap());
}

@Override
public void track(String action, Map<String, Object> metadata) {
analytics.enqueue(TrackMessage.builder(action)
.userId(identitySupplier.get().getCustomerId().toString())
.properties(metadata));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* MIT License
*
* Copyright (c) 2020 Dataline
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package io.dataline.analytics;

import java.util.Map;

public interface TrackingClient {

void identify();

void track(String action);

void track(String action, Map<String, Object> metadata);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* MIT License
*
* Copyright (c) 2020 Dataline
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package io.dataline.analytics;

import com.google.common.annotations.VisibleForTesting;
import io.dataline.config.Configs;
import io.dataline.config.StandardWorkspace;
import io.dataline.config.persistence.ConfigNotFoundException;
import io.dataline.config.persistence.ConfigRepository;
import io.dataline.config.persistence.JsonValidationException;
import io.dataline.config.persistence.PersistenceConstants;
import java.io.IOException;
import java.util.function.Supplier;

public class TrackingClientSingleton {

private static final Object lock = new Object();
private static TrackingClient trackingClient;

public static TrackingClient get() {
synchronized (lock) {
if (trackingClient == null) {
initialize();
}
return trackingClient;
}
}

@VisibleForTesting
static void initialize(TrackingClient trackingClient) {
synchronized (lock) {
TrackingClientSingleton.trackingClient = trackingClient;
}
}

public static void initialize(Configs.TrackingStrategy trackingStrategy, ConfigRepository configRepository) {
initialize(createTrackingClient(trackingStrategy, () -> getTrackingIdentity(configRepository)));
}

// fallback on a logging client with an empty identity.
private static void initialize() {
initialize(new LoggingTrackingClient(TrackingIdentity::empty));
}

@VisibleForTesting
static TrackingIdentity getTrackingIdentity(ConfigRepository configRepository) {
try {
final StandardWorkspace workspace = configRepository.getStandardWorkspace(PersistenceConstants.DEFAULT_WORKSPACE_ID);
String email = null;
if (workspace.getEmail() != null && workspace.getAnonymousDataCollection() != null && !workspace.getAnonymousDataCollection()) {
email = workspace.getEmail();
}
return new TrackingIdentity(
workspace.getCustomerId(),
email,
workspace.getAnonymousDataCollection(),
workspace.getNews(),
workspace.getSecurityUpdates());
} catch (ConfigNotFoundException e) {
throw new RuntimeException("could not find workspace with id: " + PersistenceConstants.DEFAULT_WORKSPACE_ID, e);
} catch (JsonValidationException | IOException e) {
throw new RuntimeException(e);
}
}

/**
* Creates a tracking client that uses the appropriate strategy from an identity supplier.
*
* @param trackingStrategy - what type of tracker we want to use.
* @param trackingIdentitySupplier - how we get the identity of the user. we have a supplier,
* because we if the identity updates over time (which happens during initial setup), we
* always want the most recent info.
* @return tracking client
*/
@VisibleForTesting
static TrackingClient createTrackingClient(Configs.TrackingStrategy trackingStrategy, Supplier<TrackingIdentity> trackingIdentitySupplier) {

switch (trackingStrategy) {
case SEGMENT:
return new SegmentTrackingClient(trackingIdentitySupplier);
case LOGGING:
return new LoggingTrackingClient(trackingIdentitySupplier);
default:
throw new RuntimeException("unrecognized tracking strategy");
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* MIT License
*
* Copyright (c) 2020 Dataline
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package io.dataline.analytics;

import java.util.Objects;
import java.util.Optional;
import java.util.UUID;

public class TrackingIdentity {

private final UUID customerId;
private final String email;
private final Boolean anonymousDataCollection;
private final Boolean news;
private final Boolean securityUpdates;

public static TrackingIdentity empty() {
return new TrackingIdentity(null, null, null, null, null);
}

public TrackingIdentity(UUID customerId, String email, Boolean anonymousDataCollection, Boolean news, Boolean securityUpdates) {
this.customerId = customerId;
this.email = email;
this.anonymousDataCollection = anonymousDataCollection;
this.news = news;
this.securityUpdates = securityUpdates;
}

public UUID getCustomerId() {
return customerId;
}

public Optional<String> getEmail() {
return Optional.ofNullable(email);
}

public boolean isAnonymousDataCollection() {
return anonymousDataCollection != null && anonymousDataCollection;
}

public boolean isNews() {
return news != null && news;
}

public boolean isSecurityUpdates() {
return securityUpdates != null && securityUpdates;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
TrackingIdentity that = (TrackingIdentity) o;
return anonymousDataCollection == that.anonymousDataCollection &&
news == that.news &&
securityUpdates == that.securityUpdates &&
Objects.equals(customerId, that.customerId) &&
Objects.equals(email, that.email);
}

@Override
public int hashCode() {
return Objects.hash(customerId, email, anonymousDataCollection, news, securityUpdates);
}

}
Loading

0 comments on commit 38e84f1

Please sign in to comment.