anymous telemetry based on redhat (#212)

* initial telemetry

* fixed segment bugs

* Move telemetry impl to submodule, add more actions

* Replace privacy policy link, minor refactoring

---------

Co-authored-by: Carl-Robert Linnupuu <carlrobertoh@gmail.com>
This commit is contained in:
keith siilats 2023-09-27 11:44:01 -04:00 committed by GitHub
parent 05c7560ec9
commit 8f9980fbf1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 5348 additions and 32 deletions

View file

@ -0,0 +1,18 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core;
import ee.carlrobert.codegpt.telemetry.core.service.TelemetryEvent;
public interface IMessageBroker {
void send(TelemetryEvent event);
void dispose();
}

View file

@ -0,0 +1,17 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core;
import ee.carlrobert.codegpt.telemetry.core.service.TelemetryEvent;
public interface ITelemetryService {
void send(TelemetryEvent event);
}

View file

@ -0,0 +1,31 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.configuration;
import ee.carlrobert.codegpt.telemetry.core.util.Lazy;
import java.util.Properties;
public abstract class AbstractConfiguration implements IConfiguration {
protected final Lazy<Properties> properties = new Lazy<>(this::loadProperties);
@Override
public String get(String key) {
return properties.get().getProperty(key);
}
@Override
public void put(String key, String value) {
properties.get().put(key, value);
}
protected abstract Properties loadProperties();
}

View file

@ -0,0 +1,37 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.configuration;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.nio.file.Path;
public class ClasspathConfiguration extends FileConfiguration {
private final ClassLoader classloader;
public ClasspathConfiguration(Path file) {
this(file, ClasspathConfiguration.class.getClassLoader());
}
public ClasspathConfiguration(Path file, ClassLoader classLoader) {
super(file);
this.classloader = classLoader;
}
@Override
protected InputStream createInputStream(Path path) throws FileNotFoundException {
if (path == null) {
return null;
}
return classloader.getResourceAsStream(path.toString());
}
}

View file

@ -0,0 +1,34 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.configuration;
import java.util.List;
import java.util.Objects;
public abstract class CompositeConfiguration implements IConfiguration {
@Override
public String get(final String key) {
List<IConfiguration> configurations = getConfigurations();
if (configurations == null
|| configurations.isEmpty()) {
return null;
}
return configurations.stream()
.map(configuration -> configuration.get(key))
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}
protected abstract List<IConfiguration> getConfigurations();
}

View file

@ -0,0 +1,54 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.configuration;
import com.intellij.openapi.diagnostic.Logger;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.Properties;
public class FileConfiguration extends AbstractConfiguration {
private static final Logger LOGGER = Logger.getInstance(FileConfiguration.class);
protected final Path path;
public FileConfiguration(Path path) {
this.path = path;
}
@Override
protected Properties loadProperties() {
Properties properties = new Properties();
try (InputStream in = createInputStream(path)) {
if (in != null) {
properties.load(in);
}
} catch (IOException e) {
LOGGER.warn("Could not load properties file " + (path == null? "" : path.toAbsolutePath()));
}
return properties;
}
protected InputStream createInputStream(Path path) throws IOException {
if (path == null) {
return null;
}
File file = path.toFile();
if (!file.exists()) {
return null;
}
return new FileInputStream(file);
}
}

View file

@ -0,0 +1,16 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.configuration;
public interface IConfiguration {
String get(String key);
void put(String key, String value);
}

View file

@ -0,0 +1,39 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.configuration;
import com.intellij.openapi.diagnostic.Logger;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.nio.file.Path;
import java.time.LocalDate;
public class SaveableFileConfiguration extends FileConfiguration {
private static final Logger LOGGER = Logger.getInstance(SaveableFileConfiguration.class);
public SaveableFileConfiguration(Path file) {
super(file);
}
public void save() throws IOException {
if (path == null) {
return;
}
File file = path.toFile();
file.createNewFile(); // ensure exists
try (Writer writer = new FileWriter(file)) {
properties.get().store(writer, "updated " + LocalDate.now());
}
}
}

View file

@ -0,0 +1,21 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.configuration;
import java.util.Properties;
public class SystemProperties extends AbstractConfiguration {
@Override
protected Properties loadProperties() {
return System.getProperties();
}
}

View file

@ -0,0 +1,163 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.configuration;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.util.messages.Topic;
import ee.carlrobert.codegpt.telemetry.core.util.Directories;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
public class TelemetryConfiguration extends CompositeConfiguration {
public static final String KEY_MODE = "ee.carlrobert.telemetry.mode";
private static final SaveableFileConfiguration FILE = new SaveableFileConfiguration(
Directories.PATH.resolve("ee.carlrobert.intellij.telemetry"));
private static TelemetryConfiguration INSTANCE = new TelemetryConfiguration();
public static TelemetryConfiguration getInstance() {
return INSTANCE;
}
// for testing purposes
protected TelemetryConfiguration() {
}
public void setMode(Mode mode) {
put(KEY_MODE, mode.toString());
}
public Mode getMode() {
return Mode.safeValueOf(get(KEY_MODE));
}
public boolean isEnabled() {
return getMode().isEnabled();
}
public void setEnabled(boolean enabled) {
setMode(Mode.valueOf(enabled));
}
public boolean isDebug() {
return getMode() == Mode.DEBUG;
}
public boolean isConfigured() {
return getMode().isConfigured();
}
@Override
public void put(String key, String value) {
getSaveableFile().put(key, value);
getNotifier().configurationChanged(key, value);
}
protected ConfigurationChangedListener getNotifier() {
return ApplicationManager.getApplication().getMessageBus()
.syncPublisher(ConfigurationChangedListener.CONFIGURATION_CHANGED);
}
@Override
protected List<IConfiguration> getConfigurations() {
return Arrays.asList(
new SystemProperties(),
getSaveableFile());
}
public void save() throws IOException {
getSaveableFile().save();
}
protected SaveableFileConfiguration getSaveableFile() {
return FILE;
}
public enum Mode {
NORMAL {
@Override
public boolean isEnabled() {
return true;
}
@Override
public boolean isConfigured() {
return true;
}
}
, DEBUG {
@Override
public boolean isEnabled() {
return true;
}
@Override
public boolean isConfigured() {
return true;
}
}, DISABLED {
@Override
public boolean isEnabled() {
return false;
}
@Override
public boolean isConfigured() {
return true;
}
}, UNKNOWN {
@Override
public boolean isEnabled() {
return false;
}
@Override
public boolean isConfigured() {
return false;
}
};
public abstract boolean isEnabled();
public abstract boolean isConfigured();
public static Mode safeValueOf(String value) {
try {
if (value == null) {
return UNKNOWN;
}
return Mode.valueOf(value.toUpperCase());
} catch (IllegalArgumentException e) {
return UNKNOWN;
}
}
public static Mode valueOf(boolean enabled) {
if (enabled) {
return Mode.NORMAL;
} else {
return Mode.DISABLED;
}
}
}
@FunctionalInterface
public interface ConfigurationChangedListener {
Topic<ConfigurationChangedListener> CONFIGURATION_CHANGED =
Topic.create("Telemetry Configuration Changed", ConfigurationChangedListener.class);
void configurationChanged(String property, String value);
}
}

View file

@ -0,0 +1,64 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.service;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
public class Application {
private final String name;
private final String version;
private final Map<String, String> properties = new HashMap<>();
Application(String name, String version) {
this.name = name;
this.version = version;
}
public String getName() {
return name;
}
public String getVersion() {
return version;
}
public Application property(String key, String value) {
this.properties.put(key, value);
return this;
}
public Collection<AbstractMap.SimpleEntry<String, Object>> getProperties() {
return properties.entrySet().stream()
.map(entry -> new AbstractMap.SimpleEntry<String, Object>(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Application)) return false;
Application that = (Application) o;
return Objects.equals(name, that.name)
&& Objects.equals(version, that.version)
&& Objects.equals(properties, that.properties);
}
@Override
public int hashCode() {
return Objects.hash(name, version, properties);
}
}

View file

@ -0,0 +1,82 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.intellij.openapi.diagnostic.Logger;
import ee.carlrobert.codegpt.telemetry.core.util.Lazy;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;
/**
* A class that provides the country for a given Timezone.
* The mapping that this is based on relies on data provided the "countries-and-timezones" project
* at https://github.com/manuelmhtr/countries-and-timezones
*/
public class Country {
private static final Logger LOGGER = Logger.getInstance(Country.class);
private static final String TIMEZONES = "/timezones.json";
private static final String KEY_COUNTRY = "c";
private static final String KEY_ALTERNATIVE = "a";
private static final Country INSTANCE = new Country();
public static Country getInstance() {
return INSTANCE;
}
private final Lazy<Map<String, Map<String, String>>> timezones = new Lazy<>(() -> deserialize(TIMEZONES));
protected Country() {
// for testing purposes
}
public String get(TimeZone timeZone) {
if (timeZone == null) {
return null;
}
return get(timeZone.getID());
}
public String get(String timezoneId) {
Map<String, String> timezone = timezones.get().get(timezoneId);
if (timezone == null) {
return null;
}
String abbreviation = timezone.get(KEY_COUNTRY);
if (abbreviation != null) {
return abbreviation;
}
String alternative = timezone.get(KEY_ALTERNATIVE);
if (alternative == null) {
return null;
}
return get(alternative);
}
private <V> Map<String, V> deserialize(String file) {
try {
ObjectMapper mapper = new ObjectMapper();
InputStream input = getClass().getResourceAsStream(file);
TypeReference<Map<String, V>> typeRef = new TypeReference<Map<String, V>>() {};
return mapper.readValue(input, typeRef);
} catch (IOException e) {
LOGGER.warn("Could not load file " + file, e);
return new HashMap<>();
}
}
}

View file

@ -0,0 +1,190 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.service;
import java.util.Locale;
import java.util.Objects;
import java.util.TimeZone;
public class Environment {
public static final String UNKNOWN_COUNTRY = "ZZ";
private final Plugin plugin;
private final IDE ide;
private final Platform platform;
private final String timezone;
private final String locale;
private final String country;
private Environment(Plugin plugin, IDE ide, Platform platform, String timezone, String locale, String country) {
this.plugin = plugin;
this.ide = ide;
this.platform = platform;
this.timezone = timezone;
this.locale = locale;
this.country = country;
}
public static class Builder {
private IDE ide;
private Plugin plugin;
private Platform platform;
private String timezone;
private String locale;
private String country;
public Builder ide(IDE ide) {
this.ide = ide;
return this;
}
private void ensureIDE() {
if (ide == null) {
ide(new IDE.Factory().create());
}
}
public Buildable plugin(ClassLoader classLoader) {
return plugin(new Plugin.Factory().create(classLoader));
}
public Buildable plugin(Plugin plugin) {
this.plugin = plugin;
return new Buildable();
}
public Builder platform(Platform platform) {
this.platform = platform;
return this;
}
private void ensurePlatform() {
if (platform == null) {
platform(new Platform());
}
}
public Builder timezone(String timezone) {
this.timezone = timezone;
return this;
}
private void ensureTimezone() {
if (timezone == null) {
timezone(TimeZone.getDefault().getID());
}
}
public Builder locale(String locale) {
this.locale = locale;
return this;
}
private void ensureLocale() {
if (locale == null) {
locale(Locale.getDefault().toString().replace('_', '-'));
}
}
public Builder country(String country) {
this.country = country;
return this;
}
private void ensureCountry() {
if (this.country == null) {
/*
* We're not allowed to query 3rd party services to determine the country.
* Segment won't report countries for incoming requests.
* We thus currently dont have any better solution than use the country in the Locale.
*/
ensureTimezone();
String country = Country.getInstance().get(timezone);
if (country == null) {
country = UNKNOWN_COUNTRY;
}
country(country);
}
}
class Buildable {
public Environment build() {
ensureIDE();
ensurePlatform();
ensureCountry();
ensureLocale();
ensureTimezone();
return new Environment(plugin, ide, platform, timezone, locale, country);
}
}
}
/**
* Returns the plugin from which Telemetry events are sent.
*/
public Application getPlugin() {
return plugin;
}
/**
* Returns the application from which Telemetry events are sent .
*/
public IDE getIde() {
return ide;
}
/**
* Returns the platform (or OS) from from which Telemetry events are sent.
*/
public Platform getPlatform() {
return platform;
}
/**
* Returns the user timezone, eg. 'Europe/Paris'
*/
public String getTimezone() {
return timezone;
}
/**
* Returns the user locale, eg. 'en-US'
*/
public String getLocale() {
return locale;
}
public String getCountry() {
return country;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Environment)) return false;
Environment that = (Environment) o;
return Objects.equals(plugin, that.plugin)
&& Objects.equals(ide, that.ide)
&& Objects.equals(platform, that.platform)
&& Objects.equals(timezone, that.timezone)
&& Objects.equals(locale, that.locale)
&& Objects.equals(country, that.country);
}
@Override
public int hashCode() {
return Objects.hash(plugin, ide, platform, timezone, locale, country);
}
}

View file

@ -0,0 +1,47 @@
/*******************************************************************************
* Copyright (c) 2022 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.service;
import com.intellij.openapi.application.ApplicationInfo;
import com.intellij.openapi.application.ApplicationNamesInfo;
public class IDE extends Application {
public static final String PROP_JAVA_VERSION = "java_version";
public static final class Factory {
public IDE create() {
return new IDE(
ApplicationNamesInfo.getInstance().getFullProductNameWithEdition(),
ApplicationInfo.getInstance().getFullVersion());
}
}
IDE(String applicationName, String applicationVersion) {
super(applicationName, applicationVersion);
}
public IDE setJavaVersion() {
return setJavaVersion(System.getProperty("java.version"));
}
public IDE setJavaVersion(String version) {
property(PROP_JAVA_VERSION, version);
return this;
}
@Override
public IDE property(String key, String value) {
super.property(key, value);
return this;
}
}

View file

@ -0,0 +1,63 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.service;
import com.intellij.openapi.util.SystemInfo;
import java.util.Objects;
public class Platform {
Platform() {
/*
* distribution not determined yet.
* A possible impl exists in jbosstools-base/usage:
* https://github.com/jbosstools/jbosstools-base/blob/master/usage/plugins/org.jboss.tools.usage/src/org/jboss/tools/usage/internal/environment/eclipse/LinuxSystem.java
*/
this(SystemInfo.OS_NAME, null, SystemInfo.OS_VERSION);
}
Platform(String name, String distribution, String version) {
this.name = name;
this.distribution = distribution;
this.version = version;
}
private final String name;
private final String distribution;
private final String version;
public String getName() {
return name;
}
public String getDistribution() {
return distribution;
}
public String getVersion() {
return version;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Platform)) return false;
Platform platform = (Platform) o;
return Objects.equals(name, platform.name)
&& Objects.equals(distribution, platform.distribution)
&& Objects.equals(version, platform.version);
}
@Override
public int hashCode() {
return Objects.hash(name, distribution, version);
}
}

View file

@ -0,0 +1,50 @@
/*******************************************************************************
* Copyright (c) 2022 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.service;
import com.intellij.ide.plugins.IdeaPluginDescriptor;
import com.intellij.ide.plugins.PluginManagerCore;
import java.util.Arrays;
public class Plugin extends Application {
public static final class Factory {
public Plugin create(ClassLoader classLoader) {
IdeaPluginDescriptor descriptor = getPluginDescriptor(classLoader);
if (descriptor == null) {
return null;
}
return create(descriptor.getName(), descriptor.getVersion());
}
public Plugin create(String name, String version) {
return new Plugin(name, version);
}
private IdeaPluginDescriptor getPluginDescriptor(ClassLoader classLoader) {
return Arrays.stream(PluginManagerCore.getPlugins())
.filter(descriptor -> classLoader.equals(descriptor.getPluginClassLoader()))
.findFirst()
.orElse(null);
}
}
Plugin(String name, String version) {
super(name, version);
}
@Override
public Plugin property(String key, String value) {
super.property(key, value);
return this;
}
}

View file

@ -0,0 +1,47 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.service;
import java.util.HashMap;
import java.util.Map;
public class TelemetryEvent {
public enum Type {
USER, ACTION, STARTUP, SHUTDOWN
}
private final Type type;
private final String name;
private final Map<String, String> properties;
public TelemetryEvent(Type type, String name) {
this(type, name, new HashMap<>());
}
public TelemetryEvent(Type type, String name, Map<String, String> properties) {
this.type = type;
this.name = name;
this.properties = properties;
}
public Type getType() {
return type;
}
public String getName() {
return name;
}
public Map<String, String> getProperties() {
return properties;
}
}

View file

@ -0,0 +1,283 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.service;
import static ee.carlrobert.codegpt.telemetry.core.service.TelemetryEvent.Type.ACTION;
import static ee.carlrobert.codegpt.telemetry.core.service.TelemetryEvent.Type.SHUTDOWN;
import static ee.carlrobert.codegpt.telemetry.core.service.TelemetryEvent.Type.STARTUP;
import static ee.carlrobert.codegpt.telemetry.core.util.TimeUtils.toLocalTime;
import com.intellij.ide.AppLifecycleListener;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.util.messages.MessageBusConnection;
import ee.carlrobert.codegpt.telemetry.core.ITelemetryService;
import ee.carlrobert.codegpt.telemetry.core.service.TelemetryEvent.Type;
import ee.carlrobert.codegpt.telemetry.core.util.AnonymizeUtils;
import ee.carlrobert.codegpt.telemetry.core.util.TimeUtils;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
public class TelemetryMessageBuilder {
private static final Logger LOGGER = Logger.getInstance(TelemetryMessageBuilder.class);
private final ServiceFacade service;
public TelemetryMessageBuilder(ClassLoader classLoader) {
this(new ServiceFacade(classLoader));
}
TelemetryMessageBuilder(ServiceFacade serviceFacade) {
this.service = serviceFacade;
}
public ActionMessage action(String name) {
return new ActionMessage(name, service);
}
static class StartupMessage extends Message<StartupMessage> {
private StartupMessage(ServiceFacade service) {
super(STARTUP, "startup", service);
}
}
static class ShutdownMessage extends Message<ShutdownMessage> {
private static final String PROP_SESSION_DURATION = "session_duration";
private ShutdownMessage(ServiceFacade service) {
this(toLocalTime(ApplicationManager.getApplication().getStartTime()), service);
}
private ShutdownMessage(LocalDateTime startup, ServiceFacade service) {
this(startup, LocalDateTime.now(), service);
}
ShutdownMessage(LocalDateTime startup, LocalDateTime shutdown, ServiceFacade service) {
super(SHUTDOWN, "shutdown", service);
sessionDuration(startup, shutdown);
}
private ShutdownMessage sessionDuration(LocalDateTime startup, LocalDateTime shutdown) {
return sessionDuration(Duration.between(startup, shutdown));
}
private ShutdownMessage sessionDuration(Duration duration) {
return property(PROP_SESSION_DURATION, TimeUtils.toString(duration));
}
String getSessionDuration() {
return getProperty(PROP_SESSION_DURATION);
}
}
public static class ActionMessage extends Message<ActionMessage> {
static final String PROP_DURATION = "duration";
static final String PROP_ERROR = "error";
static final String PROP_RESULT = "result";
public static final String RESULT_SUCCESS = "success";
private LocalDateTime started;
private ActionMessage(String name, ServiceFacade service) {
super(ACTION, name, service);
started();
}
public ActionMessage started() {
return started(LocalDateTime.now());
}
public ActionMessage started(LocalDateTime started) {
this.started = started;
return this;
}
public ActionMessage finished() {
finished(LocalDateTime.now());
return this;
}
public ActionMessage finished(LocalDateTime finished) {
duration(Duration.between(started, finished));
return this;
}
public ActionMessage duration(Duration duration) {
return property(PROP_DURATION, TimeUtils.toString(duration));
}
String getDuration() {
return getProperty(PROP_DURATION);
}
public ActionMessage success() {
return result(RESULT_SUCCESS);
}
public ActionMessage result(String result) {
property(PROP_RESULT, result);
return clearError();
}
protected ActionMessage clearResult() {
properties().remove(PROP_RESULT);
return this;
}
String getResult() {
return getProperty(PROP_RESULT);
}
public ActionMessage error(Exception exception) {
if (exception == null) {
return this;
}
return error(exception.getMessage());
}
public ActionMessage error(String message) {
property(PROP_ERROR, AnonymizeUtils.anonymize(message));
return clearResult();
}
protected ActionMessage clearError() {
properties().remove(PROP_ERROR);
return this;
}
String getError() {
return getProperty(PROP_ERROR);
}
@Override
public TelemetryEvent send() {
ensureFinished();
ensureResultOrError();
return super.send();
}
private void ensureFinished() {
if (!hasProperty(PROP_DURATION)) {
finished();
}
}
private void ensureResultOrError() {
if (!hasProperty(PROP_ERROR)
&& !hasProperty(PROP_RESULT)) {
success();
}
}
}
private abstract static class Message<T extends Message<?>> {
private final Type type;
private final Map<String, String> properties = new HashMap<>();
private final String name;
private final ServiceFacade service;
private Message(Type type, String name, ServiceFacade service) {
this.name = name;
this.type = type;
this.service = service;
}
String getName() {
return name;
}
Type getType() {
return type;
}
public T property(String key, String value) {
if (key == null
|| value == null) {
LOGGER.warn("Ignored property with key: " + key + " value: " + value);
} else {
properties.put(key, value);
}
return (T) this;
}
String getProperty(String key) {
return properties.get(key);
}
Map<String, String> properties() {
return properties;
}
protected boolean hasProperty(String key) {
return properties.containsKey(key);
}
public TelemetryEvent send() {
TelemetryEvent event = new TelemetryEvent(type, name, new HashMap<>(properties));
service.send(event);
return event;
}
}
static class ServiceFacade {
private final ClassLoader classLoader;
private ITelemetryService service = null;
protected ServiceFacade(final ClassLoader classLoader) {
this.classLoader = classLoader;
}
public void send(final TelemetryEvent event) {
if (service == null) {
this.service = createService(classLoader);
sendStartup();
onShutdown();
}
service.send(event);
}
protected ITelemetryService createService(ClassLoader classLoader) {
TelemetryServiceFactory factory = ApplicationManager.getApplication().getService(TelemetryServiceFactory.class);
return factory.create(classLoader);
}
private void sendStartup() {
new StartupMessage(this).send();
}
private void onShutdown() {
MessageBusConnection connection = createMessageBusConnection();
connection.subscribe(AppLifecycleListener.TOPIC, new AppLifecycleListener() {
@Override
public void appWillBeClosed(boolean isRestart) {
sendShutdown();
}
});
}
protected void sendShutdown() {
new ShutdownMessage(ServiceFacade.this).send();
}
protected MessageBusConnection createMessageBusConnection() {
return ApplicationManager.getApplication().getMessageBus().connect();
}
}
}

View file

@ -0,0 +1,111 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.service;
import static ee.carlrobert.codegpt.telemetry.core.configuration.TelemetryConfiguration.KEY_MODE;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.util.messages.MessageBusConnection;
import ee.carlrobert.codegpt.telemetry.core.IMessageBroker;
import ee.carlrobert.codegpt.telemetry.core.ITelemetryService;
import ee.carlrobert.codegpt.telemetry.core.configuration.TelemetryConfiguration;
import ee.carlrobert.codegpt.telemetry.core.configuration.TelemetryConfiguration.ConfigurationChangedListener;
import ee.carlrobert.codegpt.telemetry.core.configuration.TelemetryConfiguration.Mode;
import ee.carlrobert.codegpt.telemetry.core.service.TelemetryEvent.Type;
import ee.carlrobert.codegpt.telemetry.core.util.CircularBuffer;
import ee.carlrobert.codegpt.telemetry.ui.TelemetryNotifications;
import java.util.concurrent.atomic.AtomicBoolean;
public class TelemetryService implements ITelemetryService {
private static final Logger LOGGER = Logger.getInstance(TelemetryService.class);
private static final int BUFFER_SIZE = 35;
private final TelemetryNotifications notifications;
private final TelemetryConfiguration configuration;
protected final IMessageBroker broker;
private final AtomicBoolean userQueried = new AtomicBoolean(false);
private final CircularBuffer<TelemetryEvent> onHold = new CircularBuffer<>(BUFFER_SIZE);
public TelemetryService(final TelemetryConfiguration configuration, final IMessageBroker broker) {
this(configuration, broker, ApplicationManager.getApplication().getMessageBus().connect(), new TelemetryNotifications());
}
TelemetryService(final TelemetryConfiguration configuration,
final IMessageBroker broker,
final MessageBusConnection connection,
final TelemetryNotifications notifications) {
this.configuration = configuration;
this.broker = broker;
this.notifications = notifications;
onConfigurationChanged(connection);
}
private void onConfigurationChanged(MessageBusConnection connection) {
connection.subscribe(ConfigurationChangedListener.CONFIGURATION_CHANGED, (String key, String value) -> {
if (KEY_MODE.equals(key)
&& Mode.safeValueOf(value).isEnabled()) {
flushOnHold();
}
});
}
@Override
public void send(TelemetryEvent event) {
sendUserInfo();
doSend(event);
queryUserConsent();
}
private void sendUserInfo() {
doSend(new TelemetryEvent(
Type.USER,
"Anonymous ID: " + UserId.INSTANCE.get()));
}
private void queryUserConsent() {
if (!isConfigured()
&& userQueried.compareAndSet(false, true)) {
notifications.queryUserConsent();
}
}
private void doSend(TelemetryEvent event) {
if (isEnabled()) {
flushOnHold();
broker.send(event);
} else if (!isConfigured()) {
onHold.offer(event);
}
}
private boolean isEnabled() {
return configuration != null
&& configuration.isEnabled();
}
private boolean isConfigured() {
return configuration != null
&& configuration.isConfigured();
}
private void flushOnHold() {
onHold.pollAll().forEach(this::send);
}
public void dispose() {
flushOnHold();
onHold.clear();
broker.dispose();
}
}

View file

@ -0,0 +1,44 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.service;
import com.intellij.openapi.project.DumbAware;
import ee.carlrobert.codegpt.telemetry.core.IMessageBroker;
import ee.carlrobert.codegpt.telemetry.core.configuration.TelemetryConfiguration;
import ee.carlrobert.codegpt.telemetry.core.service.segment.SegmentBroker;
import ee.carlrobert.codegpt.telemetry.core.service.segment.SegmentConfiguration;
public class TelemetryServiceFactory implements DumbAware {
private final IDE ide = new IDE.Factory()
.create()
.setJavaVersion();
public TelemetryService create(ClassLoader classLoader) {
Environment environment = new Environment.Builder()
.ide(ide)
.plugin(classLoader)
.build();
TelemetryConfiguration configuration = TelemetryConfiguration.getInstance();
IMessageBroker broker = createSegmentBroker(configuration.isDebug(), classLoader, environment);
return new TelemetryService(configuration, broker);
}
private IMessageBroker createSegmentBroker(boolean isDebug, ClassLoader classLoader, Environment environment) {
SegmentConfiguration brokerConfiguration = new SegmentConfiguration(classLoader);
return new SegmentBroker(
isDebug,
UserId.INSTANCE.get(),
environment,
brokerConfiguration);
}
}

View file

@ -0,0 +1,93 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.service;
import com.intellij.openapi.diagnostic.Logger;
import ee.carlrobert.codegpt.telemetry.core.util.Directories;
import ee.carlrobert.codegpt.telemetry.core.util.FileUtils;
import ee.carlrobert.codegpt.telemetry.core.util.Lazy;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Stream;
public class UserId {
private static final Logger LOGGER = Logger.getInstance(UserId.class);
public static final UserId INSTANCE = new UserId();
private static final Path UUID_FILE = Directories.PATH.resolve("anonymousId");
private static final Pattern UUID_REGEX =
Pattern.compile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$");
private final Lazy<String> uuid = new Lazy<>(() -> loadOrCreate(UUID_FILE));
/** for testing purposes */
protected UserId() {}
public String get() {
return uuid.get();
}
private String loadOrCreate(Path file) {
String uuid = null;
if (exists(file)) {
uuid = load(file);
if (isValid(uuid)) {
return uuid;
}
}
uuid = create();
write(uuid, file);
return uuid;
}
/** for testing purposes */
protected boolean exists(Path file) {
return Files.exists(file);
}
/** for testing purposes */
protected String load(Path uuidFile) {
String uuid = null;
try(Stream<String> lines = Files.lines(uuidFile)) {
uuid = lines
.findAny()
.map(String::trim)
.orElse(null);
} catch (IOException e) {
LOGGER.warn("Could not read anonymous UUID file at " + uuidFile.toAbsolutePath(), e);
}
return uuid;
}
private boolean isValid(String uuid) {
if (uuid == null) {
return false;
}
return UUID_REGEX.matcher(uuid).matches();
}
private String create() {
return UUID.randomUUID().toString();
}
/** for testing purposes */
protected void write(String uuid, Path uuidFile) {
try {
FileUtils.createFileAndParent(uuidFile);
FileUtils.write(uuid, uuidFile);
} catch (IOException e) {
LOGGER.warn("Could not write anonymous UUID to file at " + UUID_FILE.toAbsolutePath(), e);
}
}
}

View file

@ -0,0 +1,16 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.service.segment;
public interface ISegmentConfiguration {
String getNormalWriteKey();
String getDebugWriteKey();
}

View file

@ -0,0 +1,69 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.service.segment;
import java.util.Objects;
/**
* Traits that the segment broker sends with am identify event.
*/
public class IdentifyTraits {
private final String locale;
private final String timezone;
private final String osName;
private final String osVersion;
private final String osDistribution;
public IdentifyTraits(String locale, String timezone, String osName, String osVersion, String osDistribution) {
this.locale = locale;
this.timezone = timezone;
this.osName = osName;
this.osVersion = osVersion;
this.osDistribution = osDistribution;
}
public String getLocale() {
return locale;
}
public String getTimezone() {
return timezone;
}
public String getOsName() {
return osName;
}
public String getOsVersion() {
return osVersion;
}
public String getOsDistribution() {
return osDistribution;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof IdentifyTraits)) return false;
IdentifyTraits identifyTraits = (IdentifyTraits) o;
return Objects.equals(locale, identifyTraits.locale)
&& Objects.equals(timezone, identifyTraits.timezone)
&& Objects.equals(osName, identifyTraits.osName)
&& Objects.equals(osVersion, identifyTraits.osVersion)
&& Objects.equals(osDistribution, identifyTraits.osDistribution);
}
@Override
public int hashCode() {
return Objects.hash(locale, timezone, osName, osVersion, osDistribution);
}
}

View file

@ -0,0 +1,106 @@
/*******************************************************************************
* Copyright (c) 2022 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.service.segment;
import com.google.gson.Gson;
import com.intellij.openapi.diagnostic.Logger;
import ee.carlrobert.codegpt.telemetry.core.util.Directories;
import ee.carlrobert.codegpt.telemetry.core.util.FileUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import java.util.stream.Stream;
/**
* Persistency for {@link IdentifyTraits}.
*/
public class IdentifyTraitsPersistence {
public static final IdentifyTraitsPersistence INSTANCE = new IdentifyTraitsPersistence();
private static final Logger LOGGER = Logger.getInstance(IdentifyTraitsPersistence.class);
private static final Path FILE = Directories.PATH.resolve("segment-identify-traits.json");
private IdentifyTraits identifyTraits = null;
protected IdentifyTraitsPersistence() {}
public synchronized IdentifyTraits get() {
if (identifyTraits == null) {
this.identifyTraits = deserialize(load(FILE));
}
return identifyTraits;
}
public synchronized void set(IdentifyTraits identifyTraits) {
if (Objects.equals(identifyTraits, this.identifyTraits)) {
return;
}
this.identifyTraits = identifyTraits;
String string = null;
if (identifyTraits != null) {
string = serialize(identifyTraits);
}
save(string, FILE);
}
private String serialize(IdentifyTraits identifyTraits) {
if (identifyTraits == null) {
return null;
}
return new Gson().toJson(identifyTraits);
}
private IdentifyTraits deserialize(String identity) {
if (identity == null) {
return null;
}
return new Gson().fromJson(identity, IdentifyTraits.class);
}
private String load(Path file) {
String event = null;
try(Stream<String> lines = getLines(file)) {
event = lines
.findAny()
.map(String::trim)
.orElse(null);
} catch (IOException e) {
LOGGER.warn("Could not read identity file at " + file.toAbsolutePath(), e);
}
return event;
}
/* for testing purposes */
protected Stream<String> getLines(Path file) throws IOException {
return Files.lines(file);
}
private void save(String event, Path file) {
try {
createFileAndParent(file);
writeFile(event, file);
} catch (IOException e) {
LOGGER.warn("Could not write identity to file at " + FILE.toAbsolutePath(), e);
}
}
/* for testing purposes */
protected void createFileAndParent(Path file) throws IOException {
FileUtils.createFileAndParent(file);
}
/* for testing purposes */
protected void writeFile(String event, Path file) throws IOException {
FileUtils.write(event, file);
}
}

View file

@ -0,0 +1,252 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.service.segment;
import com.intellij.openapi.diagnostic.Logger;
import com.segment.analytics.Analytics;
import com.segment.analytics.messages.IdentifyMessage;
import com.segment.analytics.messages.MessageBuilder;
import com.segment.analytics.messages.PageMessage;
import com.segment.analytics.messages.TrackMessage;
import ee.carlrobert.codegpt.telemetry.core.IMessageBroker;
import ee.carlrobert.codegpt.telemetry.core.service.Application;
import ee.carlrobert.codegpt.telemetry.core.service.Environment;
import ee.carlrobert.codegpt.telemetry.core.service.TelemetryEvent;
import ee.carlrobert.codegpt.telemetry.core.util.Lazy;
import ee.carlrobert.codegpt.telemetry.core.util.MapBuilder;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
public class SegmentBroker implements IMessageBroker {
private static final Logger LOGGER = Logger.getInstance(SegmentBroker.class);
public static final String PROP_NAME = "name";
public static final String PROP_VERSION = "version";
public static final String PROP_APP = "app";
public static final String PROP_IP = "ip";
public static final String PROP_COUNTRY = "country";
public static final String PROP_LOCALE = "locale";
public static final String PROP_LOCATION = "location";
public static final String PROP_OS = "os";
public static final String PROP_OS_NAME = "os_name";
public static final String PROP_OS_DISTRIBUTION = "os_distribution";
public static final String PROP_OS_VERSION = "os_version";
public static final String PROP_TIMEZONE = "timezone";
public static final String VALUE_NULL_IP = "0.0.0.0"; // fixed, faked ip addr
public static final String PROP_EXTENSION_NAME = "extension_name";
public static final String PROP_EXTENSION_VERSION = "extension_version";
public static final String PROP_APP_NAME = "app_name";
public static final String PROP_APP_VERSION = "app_version";
enum SegmentType {
IDENTIFY {
public MessageBuilder toMessage(TelemetryEvent event, Map<String, Object> context, SegmentBroker broker) {
return broker.toMessage(IdentifyMessage.builder(), event, context);
}
},
TRACK {
public MessageBuilder toMessage(TelemetryEvent event, Map<String, Object> context, SegmentBroker broker) {
return broker.toMessage(TrackMessage.builder(event.getName()), event, context);
}
},
PAGE {
public MessageBuilder toMessage(TelemetryEvent event, Map<String, Object> context, SegmentBroker broker) {
return broker.toMessage(PageMessage.builder(event.getName()), event, context);
}
};
public abstract MessageBuilder toMessage(TelemetryEvent event, Map<String, Object> context, SegmentBroker broker);
public static SegmentType valueOf(TelemetryEvent.Type eventType) {
switch (eventType) {
case USER:
return IDENTIFY;
case ACTION:
case STARTUP:
case SHUTDOWN:
default:
return TRACK;
}
}
}
private final String userId;
private final IdentifyTraitsPersistence identifyTraitsPersistence;
private final Environment environment;
private Lazy<Analytics> analytics;
public SegmentBroker(boolean isDebug, String userId, Environment environment, ISegmentConfiguration configuration) {
this(isDebug, userId, IdentifyTraitsPersistence.INSTANCE, environment, configuration, new AnalyticsFactory());
}
public SegmentBroker(boolean isDebug, String userId, IdentifyTraitsPersistence identifyTraitsPersistence, Environment environment, ISegmentConfiguration configuration, Function<String, Analytics> analyticsFactory) {
this.userId = userId;
this.identifyTraitsPersistence = identifyTraitsPersistence;
this.environment = environment;
this.analytics = new Lazy<>(() -> analyticsFactory.apply(getWriteKey(isDebug, configuration)));
}
@Override
public void send(TelemetryEvent event) {
try {
if (analytics.get() == null) {
LOGGER.warn("Could not send " + event.getType() + " event '" + event.getName() + "': no analytics instance present.");
return;
}
Map<String, Object> context = createContext(environment);
SegmentType segmentType = SegmentType.valueOf(event.getType());
MessageBuilder builder = segmentType.toMessage(event, context, this);
if (builder == null) {
LOGGER.debug("No message to be sent.");
} else {
LOGGER.debug("Sending message " + builder.type() + " to segment.");
analytics.get().enqueue(builder);
}
} catch (IllegalArgumentException e) {
LOGGER.warn("Could not send " + event.getName() + " event: unknown type '" + event.getType() + "'.");
}
}
private MessageBuilder toMessage(IdentifyMessage.Builder builder, TelemetryEvent event, Map<String, Object> context) {
IdentifyTraits identifyTraits = new IdentifyTraits(
environment.getLocale(),
environment.getTimezone(),
environment.getPlatform().getName(),
environment.getPlatform().getVersion(),
environment.getPlatform().getDistribution());
if (!haveChanged(identifyTraits, identifyTraitsPersistence)) {
LOGGER.debug("Skipping identify message: already sent." + identifyTraits);
return null;
}
return builder
.userId(userId)
.traits(addIdentifyTraits(identifyTraits, event.getProperties()))
.context(context);
}
/**
* Saves the given identify traits to persistence if persistence exists.
*
* @param identifyTraits the traits to save
* @return true if saving occurred or no persistence was present.
*/
private synchronized boolean haveChanged(IdentifyTraits identifyTraits, IdentifyTraitsPersistence persistence) {
if (identifyTraitsPersistence != null) {
if (identifyTraits.equals(persistence.get())) {
return false;
} else {
persistence.set(identifyTraits);
}
}
return true;
}
private Map<String, ?> addIdentifyTraits(final IdentifyTraits identifyTraits, final Map<String, String> properties) {
putIfNotNull(PROP_LOCALE, identifyTraits.getLocale(), properties);
putIfNotNull(PROP_TIMEZONE, identifyTraits.getTimezone(), properties);
putIfNotNull(PROP_OS_NAME, identifyTraits.getOsName(), properties);
putIfNotNull(PROP_OS_DISTRIBUTION, identifyTraits.getOsDistribution(), properties);
putIfNotNull(PROP_OS_VERSION, identifyTraits.getOsVersion(), properties);
return properties;
}
private MessageBuilder toMessage(TrackMessage.Builder builder, TelemetryEvent event, Map<String, Object> context) {
return builder
.userId(userId)
.properties(addTrackProperties(event.getProperties()))
.context(context);
}
private Map<String, ?> addTrackProperties(final Map<String, String> properties) {
Application application = environment.getIde();
putIfNotNull(PROP_APP_NAME, application.getName(), properties);
putIfNotNull(PROP_APP_VERSION, application.getVersion(), properties);
application.getProperties().forEach(
appProperty -> putIfNotNull(appProperty.getKey(), String.valueOf(appProperty.getValue()), properties));
putIfNotNull(PROP_EXTENSION_NAME, environment.getPlugin().getName(), properties);
putIfNotNull(PROP_EXTENSION_VERSION, environment.getPlugin().getVersion(), properties);
return properties;
}
private MessageBuilder toMessage(PageMessage.Builder builder, TelemetryEvent event, Map<String, Object> context) {
return builder
.userId(userId)
.properties(event.getProperties())
.context(context);
}
private void putIfNotNull(String key, String value, Map<String, String> properties) {
if (key == null
|| value == null
|| properties == null) {
return;
}
properties.put(key, value);
}
private Map<String, Object> createContext(Environment environment) {
return new MapBuilder()
.mapPair(PROP_APP)
.pair(PROP_NAME, environment.getIde().getName())
.pair(PROP_VERSION, environment.getIde().getVersion())
.pairs(environment.getIde().getProperties())
.build()
.pair(PROP_IP, VALUE_NULL_IP)
.pair(PROP_LOCALE, environment.getLocale())
.mapPair(PROP_LOCATION)
.pair(PROP_COUNTRY, environment.getCountry())
.build()
.mapPair(PROP_OS)
.pair(PROP_NAME, environment.getPlatform().getName())
.pair(PROP_VERSION, environment.getPlatform().getVersion())
.build()
.pair(PROP_TIMEZONE, environment.getTimezone())
.build();
}
public void dispose() {
analytics.get().flush();
analytics.get().shutdown();
}
private String getWriteKey(boolean isDebug, ISegmentConfiguration configuration) {
if (isDebug) {
return configuration.getDebugWriteKey();
} else {
return configuration.getNormalWriteKey();
}
}
private static class AnalyticsFactory implements Function<String, Analytics> {
private static final int FLUSH_INTERVAL = 10000;
private static final int FLUSH_QUEUE_SIZE = 10;
@Override
public Analytics apply(String writeKey) {
if (writeKey == null) {
LOGGER.warn("Could not create Segment Analytics instance, missing writeKey.");
return null;
}
LOGGER.debug("Creating Segment Analytics instance using " + writeKey + " writeKey.");
return Analytics.builder(writeKey)
.flushQueueSize(FLUSH_QUEUE_SIZE)
.flushInterval(FLUSH_INTERVAL, TimeUnit.MILLISECONDS)
.build();
}
}
}

View file

@ -0,0 +1,63 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.service.segment;
import ee.carlrobert.codegpt.telemetry.core.configuration.ClasspathConfiguration;
import ee.carlrobert.codegpt.telemetry.core.configuration.CompositeConfiguration;
import ee.carlrobert.codegpt.telemetry.core.configuration.IConfiguration;
import ee.carlrobert.codegpt.telemetry.core.configuration.SystemProperties;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
public class SegmentConfiguration extends CompositeConfiguration implements ISegmentConfiguration {
public static final String KEY_SEGMENT_WRITE = "writeKey";
public static final String KEY_SEGMENT_DEBUG_WRITE = "debugWriteKey";
private static final String SEGMENT_PROPERTIES = "segment.properties";
private static final String SEGMENT_DEFAULTS_PROPERTIES = "segment-defaults.properties";
private final ClasspathConfiguration consumerClasspathConfiguration;
public SegmentConfiguration(ClassLoader classLoader) {
this(new ClasspathConfiguration(Paths.get(SEGMENT_PROPERTIES), classLoader));
}
protected SegmentConfiguration(ClasspathConfiguration consumerClasspathConfiguration) {
this.consumerClasspathConfiguration = consumerClasspathConfiguration;
}
@Override
public void put(String key, String value) {
consumerClasspathConfiguration.put(key, value);
}
@Override
public List<IConfiguration> getConfigurations() {
return Arrays.asList(
new SystemProperties(),
// segment.properties in consuming plugin
consumerClasspathConfiguration,
// segment-defaults.properties in this plugin
new ClasspathConfiguration(Paths.get(SEGMENT_DEFAULTS_PROPERTIES), getClass().getClassLoader()));
}
@Override
public String getNormalWriteKey() {
return get(KEY_SEGMENT_WRITE);
}
@Override
public String getDebugWriteKey() {
return get(KEY_SEGMENT_DEBUG_WRITE);
}
}

View file

@ -0,0 +1,102 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.util;
import java.util.regex.Pattern;
public class AnonymizeUtils {
public static final String USER_NAME = System.getProperty("user.name");
public static final String ANONYMOUS_USER_NAME = "<USER>";
public static final String HOME_DIR = System.getProperty("user.home");
public static final String ANONYMOUS_HOMEDIR = "<HOMEDIR>";
public static final String TMP_DIR = System.getProperty("java.io.tmpdir");
public static final String ANONYMOUS_TMPDIR = "<TMPDIR>";
private static final Pattern EMAIL_PATTERN = Pattern.compile(
"[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}",
Pattern.CASE_INSENSITIVE);
public static final String ANONYMOUS_EMAIL = "<EMAIL>";
private static final Pattern IP_PATTERN = Pattern.compile(
"(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])");
public static final String ANONYMOUS_IP = "<IP>";
public static final String ANONYMOUS_RESOURCENAME = "<RESOURCENAME>";
public static final String ANONYMOUS_NAMESPACE = "<NAMESPACE>";
private AnonymizeUtils() {
}
public static String anonymize(String string) {
return anonymizeEmail(
anonymizeUserName(
anonymizeIP(
anonymizeHomeDir(
anonymizeTmpDir(string)
)
)
)
);
}
public static String anonymizeResource(String name, String namespace, String string) {
if (string == null
|| string.isEmpty()) {
return string;
}
if (name != null) {
string = string.replace(name, ANONYMOUS_RESOURCENAME);
}
if (namespace != null) {
string = string.replace(namespace, ANONYMOUS_NAMESPACE);
}
return string;
}
public static String anonymizeUserName(String string) {
if (string == null
|| string.isEmpty()) {
return string;
}
return string.replace(USER_NAME, ANONYMOUS_USER_NAME);
}
public static String anonymizeEmail(String string) {
if (string == null
|| string.isEmpty()) {
return string;
}
return EMAIL_PATTERN.matcher(string).replaceAll(ANONYMOUS_EMAIL);
}
public static String anonymizeHomeDir(String string) {
if (string == null
|| string.isEmpty()) {
return string;
}
return string.replace(HOME_DIR, ANONYMOUS_HOMEDIR);
}
public static String anonymizeTmpDir(String string) {
if (string == null
|| string.isEmpty()) {
return string;
}
return string.replace(TMP_DIR, ANONYMOUS_TMPDIR);
}
public static String anonymizeIP(String string) {
if (string == null
|| string.isEmpty()) {
return string;
}
return IP_PATTERN.matcher(string).replaceAll(ANONYMOUS_IP);
}
}

View file

@ -0,0 +1,98 @@
/**
* MIT License
* <p>
* Copyright (c) 2017 Eugen Paraschiv
* <p>
* 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:
* <p>
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* <p>
* 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.
* <p>
* Based on work by Eugen Paraschiv for Baeldung.com at
* https://github.com/eugenp/tutorials/blob/master/data-structures/src/main/java/com/baeldung/circularbuffer/CircularBuffer.java
*/
package ee.carlrobert.codegpt.telemetry.core.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class CircularBuffer<E> {
private static final int DEFAULT_CAPACITY = 8;
private final int capacity;
private final E[] data;
private volatile int writeSequence = -1;
private volatile int readSequence = 0;
@SuppressWarnings("unchecked")
public CircularBuffer(int capacity) {
this.capacity = (capacity < 1) ? DEFAULT_CAPACITY : capacity;
this.data = (E[]) new Object[capacity];
}
public boolean offer(E element) {
if (!isFull()) {
int nextWriteSeq = writeSequence + 1;
data[nextWriteSeq % capacity] = element;
writeSequence++;
return true;
}
return false;
}
public E poll() {
if (!isEmpty()) {
E nextValue = data[readSequence % capacity];
readSequence++;
return nextValue;
}
return null;
}
public List<E> pollAll() {
List<E> values = new ArrayList<>();
for(E value = poll(); value != null; value = poll()) {
values.add(value);
}
return values;
}
public int capacity() {
return capacity;
}
public int size() {
return (writeSequence - readSequence) + 1;
}
public boolean isEmpty() {
return writeSequence < readSequence;
}
public boolean isFull() {
return size() >= capacity;
}
public void clear() {
readSequence = 0;
writeSequence = -1;
Arrays.fill(data, null);
}
}

View file

@ -0,0 +1,22 @@
/*******************************************************************************
* Copyright (c) 2022 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.util;
import java.nio.file.Path;
import java.nio.file.Paths;
public class Directories {
public static final Path PATH = Paths.get(
System.getProperty("user.home"),
".codegpt");
}

View file

@ -0,0 +1,42 @@
/*******************************************************************************
* Copyright (c) 2022 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.util;
import java.io.IOException;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
public class FileUtils {
/**
* Creates the file for the given path and the folder that contains it.
* Does nothing if it any of those already exist.
*
* @param file the file to create
*
* @throws IOException if the file operation fails
*/
public static void createFileAndParent(Path file) throws IOException {
if (!Files.exists(file.getParent())) {
Files.createDirectories(file.getParent());
}
if (!Files.exists(file)) {
Files.createFile(file);
}
}
public static void write(String content, Path file) throws IOException {
try (Writer writer = Files.newBufferedWriter(file)) {
writer.append(content);
}
}
}

View file

@ -0,0 +1,31 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.util;
import java.util.function.Supplier;
public class Lazy<T> implements Supplier<T> {
private final Supplier<T> factory;
private volatile T value;
public Lazy(Supplier<T> factory) {
this.factory = factory;
}
@Override
public T get() {
if (value == null) {
this.value = factory.get();
}
return value;
}
}

View file

@ -0,0 +1,74 @@
/*******************************************************************************
* Copyright (c) 2022 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.util;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class MapBuilder {
private final Map<String, Object> map = new HashMap<>();
public MapBuilder pair(String key, String value) {
map.put(key, value);
return this;
}
public MapBuilder pair(String key, Object value) {
map.put(key, value);
return this;
}
public MapBuilder pairs(Collection<AbstractMap.SimpleEntry<String, Object>> entries) {
if (entries == null) {
return this;
}
entries.stream().forEach(entry -> map.put(entry.getKey(), entry.getValue()));
return this;
}
public MapValueBuilder mapPair(String key) {
return new MapValueBuilder(key);
}
public Map<String, Object> build() {
return map;
}
public class MapValueBuilder {
private final Map<String, Object> map = new HashMap<>();
private final String key;
private MapValueBuilder(String key) {
this.key = key;
}
public MapValueBuilder pair(String key, Object value) {
map.put(key, value);
return this;
}
public MapValueBuilder pairs(Collection<AbstractMap.SimpleEntry<String, Object>> entries) {
if (entries == null) {
return this;
}
entries.stream().forEach(entry -> map.put(entry.getKey(), entry.getValue()));
return this;
}
public MapBuilder build() {
MapBuilder.this.pair(key, map);
return MapBuilder.this;
}
}
}

View file

@ -0,0 +1,78 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.core.util;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TimeUtils {
private static final Pattern HH_MM_SS_DURATION = Pattern.compile("([\\d]+):([\\d]{2}):([\\d]{2})");
private TimeUtils() {}
/**
* Returns a {@link LocalTime} for a given milliseconds.
*
* @param millis
* @return
*/
public static LocalDateTime toLocalTime(long millis) {
Instant instant = new Date(millis).toInstant();
ZonedDateTime time = instant.atZone(ZoneId.systemDefault());
return time.toLocalDateTime();
}
/**
* Returns a human readable string representation in the format "HH:MM:SS" for a given duration.
*
* @param duration to be transformed to its human readable form
*
* @return a string "HH:MM:SS" representing the duration
*/
public static String toString(Duration duration) {
return String.format("%02d:%02d:%02d",
duration.toHours(),
duration.toMinutes() % 60,
duration.getSeconds() % 60);
}
/**
* Returns the duration for a given string "HH:MM:SS".
* It's the reverse operation for #toString(Duration).
*
* @param hoursMinutesSeconds
* @return the duration
*
* @see #toString(Duration)
*/
public static Duration toDuration(String hoursMinutesSeconds) {
Matcher matcher = HH_MM_SS_DURATION.matcher(hoursMinutesSeconds);
if (!matcher.matches()) {
return null;
}
long hours = Integer.parseInt(matcher.group(1));
Duration duration = Duration.ofHours(hours);
long minutes = Integer.parseInt(matcher.group(2));
duration = duration.plusMinutes(minutes);
long seconds = Integer.parseInt(matcher.group(3));
duration = duration.plusSeconds(seconds);
return duration;
}
}

View file

@ -0,0 +1,73 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.ui;
import com.intellij.icons.AllIcons;
import com.intellij.notification.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.DumbAwareAction;
import ee.carlrobert.codegpt.telemetry.core.configuration.TelemetryConfiguration;
import ee.carlrobert.codegpt.telemetry.ui.utils.NotificationGroupFactory;
import java.io.IOException;
public class TelemetryNotifications {
private static final Logger LOGGER = Logger.getInstance(TelemetryNotifications.class);
private static final NotificationGroup QUERY_USER_CONSENT = NotificationGroupFactory.create(
"Enable Telemetry",
NotificationDisplayType.STICKY_BALLOON,
true);
private final NotificationGroup group;
public TelemetryNotifications() {
this(QUERY_USER_CONSENT);
}
TelemetryNotifications(NotificationGroup group) {
this.group = group;
}
public void queryUserConsent() {
Notification notification = group.createNotification(
"Help CodeGPT improve its extensions by allowing them to collect anonymous usage data. " +
"Read our <a href=\"https://codegpt.carlrobert.ee/privacy\">privacy statement</a> " +
"and learn how to <a href=\"\">opt out</a>.",
NotificationType.INFORMATION);
notification.setTitle("Enable Telemetry");
notification.setListener(new NotificationListener.UrlOpeningListener(false));
DumbAwareAction accept = DumbAwareAction.create("Accept",
e -> enableTelemetry(true, notification));
DumbAwareAction deny = DumbAwareAction.create("Deny",
e -> enableTelemetry(false, notification));
DumbAwareAction later = DumbAwareAction.create("Later",
e -> notification.expire());
notification
.addAction(accept)
.addAction(deny)
.addAction(later)
.setIcon(AllIcons.General.TodoQuestion)
.notify(null);
}
private static void enableTelemetry(boolean enabled, Notification notification) {
TelemetryConfiguration configuration = TelemetryConfiguration.getInstance();
configuration.setEnabled(enabled);
try {
configuration.save();
} catch (IOException e) {
LOGGER.warn("Could not save telemetry configuration.", e);
} finally {
notification.expire();
}
}
}

View file

@ -0,0 +1,64 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.ui.preferences;
import com.intellij.ui.components.JBCheckBox;
import com.intellij.util.ui.FormBuilder;
import com.intellij.util.ui.UI;
import javax.swing.JComponent;
import javax.swing.JPanel;
/**
* Provides a {@link JPanel} with widgets for the Telemetry settings.
*/
public class TelemetryComponent {
private static final String DESCRIPTION =
"Help CodeGPT improve its products by sending anonymous data about features and plugins used, "
+ "hardware and software configuration.<br/>"
+ "<br/>"
+ "Please note that this will not include personal data or any sensitive Information.<br/>"
+ "The data sent complies with the <a href=\"https://codegpt.com/legal/privacy\">Privacy Policy</a>.";
private final JPanel panel;
private final JBCheckBox enabled = new JBCheckBox("Send usage statistics");
public TelemetryComponent() {
this.panel = FormBuilder.createFormBuilder()
.addComponent(createCommentedPanel(enabled, DESCRIPTION), 1)
.addComponentFillVertically(new JPanel(), 0)
.getPanel();
}
private JPanel createCommentedPanel(JComponent component, String comment) {
// @See com.intellij.internal.ui.ComponentPanelTestAction for more details on how to create comment panels
return UI.PanelFactory.panel(component)
.withComment(comment)
.createPanel();
}
public JPanel getPanel() {
return panel;
}
public JComponent getPreferredFocusedComponent() {
return enabled;
}
public boolean isEnabled() {
return enabled.isSelected();
}
public void setEnabled(boolean enabled) {
this.enabled.setSelected(enabled);
}
}

View file

@ -0,0 +1,85 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.ui.preferences;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.options.SearchableConfigurable;
import ee.carlrobert.codegpt.telemetry.core.configuration.TelemetryConfiguration;
import java.io.IOException;
import javax.swing.JComponent;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Controller for telemetry settings.
*/
public class TelemetryConfigurable implements SearchableConfigurable {
/* plugin.xml > applicationConfigurable > id */
public static final String ID = "tools.preferences.carlrobert.telemetry";
private static final Logger LOGGER = Logger.getInstance(TelemetryConfigurable.class);
private TelemetryComponent component;
private final TelemetryConfiguration configuration = TelemetryConfiguration.getInstance();
@Nls(capitalization = Nls.Capitalization.Title)
@Override
public String getDisplayName() {
return "CodeGPT Telemetry";
}
@Override
public JComponent getPreferredFocusedComponent() {
return component.getPreferredFocusedComponent();
}
@Nullable
@Override
public JComponent createComponent() {
this.component = new TelemetryComponent();
return component.getPanel();
}
@Override
public boolean isModified() {
boolean modified = false;
modified |= (component.isEnabled() != configuration.isEnabled());
return modified;
}
@Override
public void apply() {
configuration.setEnabled(component.isEnabled());
try {
configuration.save();
} catch (IOException e) {
LOGGER.warn("Could not save telemetry configuration.", e);
}
}
@Override
public void reset() {
component.setEnabled(configuration.isEnabled());
}
@Override
public void disposeUIResources() {
component = null;
}
@NotNull
@Override
public String getId() {
return ID;
}
}

View file

@ -0,0 +1,85 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc.
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.ui.utils;
import com.intellij.openapi.application.ApplicationInfo;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.ui.ColorUtil;
import java.awt.Color;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import javax.swing.JLabel;
public class JBLabelUtils {
private static final Logger LOGGER = Logger.getInstance(JBLabelUtils.class);
private JBLabelUtils() {
}
/**
* Turns the given text to html that a JBLabel can display (with links).
*
* @param text the text to convert to Html
* @param maxLineLength the maximum number of characters in a line
* @param component the component to retrieve the font metrics from
*
* @returns the text
*/
public static String toStyledHtml(String text, int maxLineLength, JLabel component) {
String css = "<head><style type=\"text/css\">\n" +
"a, a:link {color:#" + toHex(getColor(
"com.intellij.util.ui.JBUI$CurrentTheme$Link", "linkColor",
"com.intellij.util.ui.JBUI$CurrentTheme$Link$Foreground", "ENABLED")) + ";}\n" +
"a:visited {color:#" + toHex(getColor(
"com.intellij.util.ui.JBUI$CurrentTheme$Link", "linkVisitedColor",
"com.intellij.util.ui.JBUI$CurrentTheme$Link$Foreground", "VISITED")) + ";}\n" +
"a:hover {color:#" + toHex(getColor(
"com.intellij.util.ui.JBUI$CurrentTheme$Link", "linkHoverColor",
"com.intellij.util.ui.JBUI$CurrentTheme$Link$Foreground", "HOVERED")) + ";}\n" +
"a:active {color:#" + toHex(getColor(
"com.intellij.util.ui.JBUI$CurrentTheme$Link", "linkPressedColor",
"com.intellij.util.ui.JBUI$CurrentTheme$Link$Foreground", "PRESSED")) + ";}\n" +
"</style>\n</head>";
int width = component.getFontMetrics(component.getFont()).stringWidth(text.substring(0, maxLineLength));
return "<html>" + css + "<body><div width=" + width + ">" + text + "</div></body></html>";
}
private static Color getColor(String methodClass, String method, String fieldClass, String field) {
try {
// < IC-2022.1
Class clazz = Class.forName(methodClass);
Method colorMethod = clazz.getMethod(method);
return (Color) colorMethod.invoke(null);
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException | ClassCastException e) {
// >= IC-2022.1
try {
Class clazz = Class.forName(fieldClass);
Field colorField = clazz.getDeclaredField(field);
return (Color) colorField.get(null);
} catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException | ClassCastException ex) {
LOGGER.warn("Could not retrieve link colors from "
+ methodClass + "/" + method + " or " + fieldClass + "/" + field
+ "on IC-" + ApplicationInfo.getInstance().getBuild().asStringWithoutProductCode(), ex);
}
return null;
}
}
private static String toHex(Color color) {
if (color == null) {
return "";
}
return ColorUtil.toHex(color);
}
}

View file

@ -0,0 +1,70 @@
/*******************************************************************************
* Copyright (c) 2021 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package ee.carlrobert.codegpt.telemetry.ui.utils;
import com.intellij.notification.NotificationDisplayType;
import com.intellij.notification.NotificationGroup;
import com.intellij.openapi.application.ApplicationInfo;
import com.intellij.openapi.diagnostic.Logger;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* A factory to create {@link NotificationGroup} on &gt;= IC-2019.3 and &gt;= IC-2021.3 where the API was broken.
*
* <ul>
* <li><b>IC-2019.3</b>
* <pre>{@code
* new NotificationGroup(String displayId, NotificationDisplayType defaultDisplayType, boolean logByDefault)
* }</pre>
* </li>
* <li><b></b>IC-2021.3</b>
* <pre>{@code
* NotificationGroupManager.getInstance().getNotificationGroup(displayId)
* }</pre>
* </li>
* </ul>
*/
public class NotificationGroupFactory {
public static NotificationGroup create(String displayId, NotificationDisplayType type, boolean logByDefault) {
try {
// < IC-2021.3
// new NotificationGroup(String displayId, NotificationDisplayType defaultDisplayType, boolean logByDefault)
Constructor<NotificationGroup> constructor = NotificationGroup.class.getConstructor(String.class, NotificationDisplayType.class, boolean.class);
return constructor.newInstance(displayId, type, logByDefault);
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
// >= IC-2021.3
// constructor removed >= 2021.3
try {
// NotificationGroupManager.getInstance().getNotificationGroup(displayId)
Class<?> managerClass = Class.forName("com.intellij.notification.NotificationGroupManager");
Method getInstance = managerClass.getMethod("getInstance");
Object manager = getInstance.invoke(null);
if (manager == null) {
return null;
}
Method getNotificationGroup = managerClass.getMethod("getNotificationGroup", String.class);
Object group = getNotificationGroup.invoke(manager, displayId);
if (!(group instanceof NotificationGroup)) {
return null;
}
return (NotificationGroup) group;
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) {
Logger.getInstance(NotificationGroupFactory.class).warn("Could not create NotificationGroup for IC-"
+ ApplicationInfo.getInstance().getBuild().asStringWithoutProductCode());
return null;
}
}
}
}

View file

@ -0,0 +1,2 @@
writeKey=JO2CLEDKOdQklP9TKKVuUyONel5H2RXk
debugWriteKey=JO2CLEDKOdQklP9TKKVuUyONel5H2RXk

View file

@ -0,0 +1,2 @@
writeKey=JO2CLEDKOdQklP9TKKVuUyONel5H2RXk
debugWriteKey=JO2CLEDKOdQklP9TKKVuUyONel5H2RXk

File diff suppressed because it is too large Load diff