-
Notifications
You must be signed in to change notification settings - Fork 133
feat: add anonymous usage telemetry for Espressif-IDE #1494
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| /******************************************************************************* | ||
| * Copyright 2026 Espressif Systems (Shanghai) PTE LTD. All rights reserved. | ||
| * Use is subject to license terms. | ||
| *******************************************************************************/ | ||
| package com.espressif.idf.core.telemetry; | ||
|
|
||
| import java.net.URI; | ||
| import java.util.Locale; | ||
| import java.util.Optional; | ||
|
|
||
| /** | ||
| * Azure Application Insights connection string, holding the instrumentation key and the regional ingestion endpoint. | ||
| * | ||
| * @author Kondal Kolipaka <kondal.kolipaka@espressif.com> | ||
| * | ||
| */ | ||
| public final class TelemetryConnection | ||
| { | ||
| private static final String INSTRUMENTATION_KEY = "instrumentationkey"; //$NON-NLS-1$ | ||
| private static final String INGESTION_ENDPOINT = "ingestionendpoint"; //$NON-NLS-1$ | ||
| private static final String DEFAULT_INGESTION_ENDPOINT = "https://dc.services.visualstudio.com/"; //$NON-NLS-1$ | ||
| private static final String TRACK_PATH = "v2/track"; //$NON-NLS-1$ | ||
|
|
||
| private final String instrumentationKey; | ||
| private final String ingestionEndpoint; | ||
|
|
||
| private TelemetryConnection(String instrumentationKey, String ingestionEndpoint) | ||
| { | ||
| this.instrumentationKey = instrumentationKey; | ||
| this.ingestionEndpoint = ingestionEndpoint; | ||
| } | ||
|
|
||
| /** | ||
| * Parses an Application Insights connection string of the form | ||
| * <code>InstrumentationKey=<key>;IngestionEndpoint=<url></code>. Unknown fields are ignored and the | ||
| * ingestion endpoint falls back to the global one when absent. | ||
| * | ||
| * @param connectionString connection string to parse, may be <code>null</code> | ||
| * @return the parsed connection or an empty optional when no instrumentation key is present | ||
| */ | ||
| public static Optional<TelemetryConnection> parse(String connectionString) | ||
| { | ||
| if (connectionString == null || connectionString.isBlank()) | ||
| { | ||
| return Optional.empty(); | ||
| } | ||
|
|
||
| String key = null; | ||
| String endpoint = DEFAULT_INGESTION_ENDPOINT; | ||
| for (String field : connectionString.split(";")) //$NON-NLS-1$ | ||
| { | ||
| int separator = field.indexOf('='); | ||
| if (separator <= 0) | ||
| { | ||
| continue; | ||
| } | ||
| String name = field.substring(0, separator).trim().toLowerCase(Locale.ENGLISH); | ||
| String value = field.substring(separator + 1).trim(); | ||
| if (value.isEmpty()) | ||
| { | ||
| continue; | ||
| } | ||
| if (INSTRUMENTATION_KEY.equals(name)) | ||
| { | ||
| key = value; | ||
| } | ||
| else if (INGESTION_ENDPOINT.equals(name)) | ||
| { | ||
| endpoint = value; | ||
| } | ||
| } | ||
|
|
||
| if (key == null) | ||
| { | ||
| return Optional.empty(); | ||
| } | ||
| if (!endpoint.endsWith("/")) //$NON-NLS-1$ | ||
| { | ||
| endpoint = endpoint + "/"; //$NON-NLS-1$ | ||
| } | ||
| return Optional.of(new TelemetryConnection(key, endpoint)); | ||
| } | ||
|
|
||
| public String getInstrumentationKey() | ||
| { | ||
| return instrumentationKey; | ||
| } | ||
|
|
||
| public String getIngestionEndpoint() | ||
| { | ||
| return ingestionEndpoint; | ||
| } | ||
|
|
||
| /** | ||
| * @return endpoint accepting the telemetry envelopes | ||
| */ | ||
| public URI getTrackUri() | ||
| { | ||
| return URI.create(ingestionEndpoint + TRACK_PATH); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| /******************************************************************************* | ||
| * Copyright 2026 Espressif Systems (Shanghai) PTE LTD. All rights reserved. | ||
| * Use is subject to license terms. | ||
| *******************************************************************************/ | ||
| package com.espressif.idf.core.telemetry; | ||
|
|
||
| import java.time.Instant; | ||
| import java.time.ZoneOffset; | ||
| import java.time.format.DateTimeFormatter; | ||
| import java.util.Map; | ||
|
|
||
| import com.google.gson.Gson; | ||
| import com.google.gson.JsonObject; | ||
|
|
||
| /** | ||
| * Builds the JSON payload accepted by the Application Insights ingestion endpoint. | ||
| * | ||
| * @author Kondal Kolipaka <kondal.kolipaka@espressif.com> | ||
| * | ||
| */ | ||
| public final class TelemetryEnvelope | ||
| { | ||
| private static final String ENVELOPE_NAME = "Microsoft.ApplicationInsights.Event"; //$NON-NLS-1$ | ||
| private static final String EVENT_BASE_TYPE = "EventData"; //$NON-NLS-1$ | ||
| private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter | ||
| .ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").withZone(ZoneOffset.UTC); //$NON-NLS-1$ | ||
|
|
||
| private static final Gson GSON = new Gson(); | ||
|
|
||
| private TelemetryEnvelope() | ||
| { | ||
| } | ||
|
|
||
| /** | ||
| * @param instrumentationKey Application Insights instrumentation key | ||
| * @param eventName name of the custom event, for instance <code>espressif-ide/session</code> | ||
| * @param tags Application Insights context tags, such as <code>ai.user.id</code> | ||
| * @param properties custom string properties reported along with the event | ||
| * @param timestamp time the event occurred | ||
| * @return the serialized envelope | ||
| */ | ||
| public static String build(String instrumentationKey, String eventName, Map<String, String> tags, | ||
| Map<String, String> properties, Instant timestamp) | ||
| { | ||
| JsonObject baseData = new JsonObject(); | ||
| baseData.addProperty("ver", 2); //$NON-NLS-1$ | ||
| baseData.addProperty("name", eventName); //$NON-NLS-1$ | ||
| baseData.add("properties", toJsonObject(properties)); //$NON-NLS-1$ | ||
|
|
||
| JsonObject data = new JsonObject(); | ||
| data.addProperty("baseType", EVENT_BASE_TYPE); //$NON-NLS-1$ | ||
| data.add("baseData", baseData); //$NON-NLS-1$ | ||
|
|
||
| JsonObject envelope = new JsonObject(); | ||
| envelope.addProperty("name", ENVELOPE_NAME); //$NON-NLS-1$ | ||
| envelope.addProperty("time", TIMESTAMP_FORMAT.format(timestamp)); //$NON-NLS-1$ | ||
| envelope.addProperty("iKey", instrumentationKey); //$NON-NLS-1$ | ||
| envelope.add("tags", toJsonObject(tags)); //$NON-NLS-1$ | ||
| envelope.add("data", data); //$NON-NLS-1$ | ||
|
|
||
| return GSON.toJson(envelope); | ||
| } | ||
|
|
||
| private static JsonObject toJsonObject(Map<String, String> values) | ||
| { | ||
| JsonObject object = new JsonObject(); | ||
| if (values != null) | ||
| { | ||
| values.forEach((key, value) -> { | ||
| if (key != null && value != null) | ||
| { | ||
| object.addProperty(key, value); | ||
| } | ||
| }); | ||
| } | ||
| return object; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| /******************************************************************************* | ||
| * Copyright 2026 Espressif Systems (Shanghai) PTE LTD. All rights reserved. | ||
| * Use is subject to license terms. | ||
| *******************************************************************************/ | ||
| package com.espressif.idf.core.telemetry; | ||
|
|
||
| import java.util.UUID; | ||
|
|
||
| import org.eclipse.core.runtime.preferences.ConfigurationScope; | ||
| import org.eclipse.core.runtime.preferences.DefaultScope; | ||
| import org.eclipse.core.runtime.preferences.IEclipsePreferences; | ||
| import org.eclipse.core.runtime.preferences.InstanceScope; | ||
| import org.osgi.service.prefs.BackingStoreException; | ||
|
|
||
| import com.espressif.idf.core.IDFCorePlugin; | ||
| import com.espressif.idf.core.IDFCorePreferenceConstants; | ||
| import com.espressif.idf.core.logging.Logger; | ||
|
|
||
| /** | ||
| * Stores the opt-out flag and the anonymous state needed to count installations and updates. | ||
| * <p> | ||
| * The anonymous identifier and the report timestamps live in the configuration scope so that all workspaces of one | ||
| * installation are counted as a single user. | ||
| * | ||
| * @author Kondal Kolipaka <kondal.kolipaka@espressif.com> | ||
| * | ||
| */ | ||
| public final class TelemetryPreferences | ||
| { | ||
| /** System property to disable telemetry, for instance <code>-Didf.telemetry=false</code>. */ | ||
| public static final String TELEMETRY_SYSTEM_PROPERTY = "idf.telemetry"; //$NON-NLS-1$ | ||
|
|
||
| /** Environment variable to disable telemetry, for instance <code>IDF_TELEMETRY=0</code>. */ | ||
| public static final String TELEMETRY_ENV_VARIABLE = "IDF_TELEMETRY"; //$NON-NLS-1$ | ||
|
|
||
| private static final String INSTALL_ID = "telemetryInstallId"; //$NON-NLS-1$ | ||
| private static final String LAST_SESSION_REPORT = "telemetryLastSessionReport"; //$NON-NLS-1$ | ||
| private static final String LAST_REPORTED_VERSION = "telemetryLastReportedVersion"; //$NON-NLS-1$ | ||
| private static final String NOTICE_SHOWN = "telemetryNoticeShown"; //$NON-NLS-1$ | ||
|
|
||
| private TelemetryPreferences() | ||
| { | ||
| } | ||
|
|
||
| /** | ||
| * @return <code>false</code> when the user opted out through the preference page, the system property or the | ||
| * environment variable | ||
| */ | ||
| public static boolean isEnabled() | ||
| { | ||
| return !isDisabledByOverride() && isEnabledByPreference(); | ||
| } | ||
|
|
||
| /** | ||
| * Tells whether reporting is switched off outside of the preference page, which is how shared and automated | ||
| * installations opt out. The override can only disable reporting, so that it can never overrule a user who opted | ||
| * out through the preference page. | ||
| * | ||
| * @return <code>true</code> when the system property or the environment variable disables reporting | ||
| */ | ||
| public static boolean isDisabledByOverride() | ||
| { | ||
| String property = System.getProperty(TELEMETRY_SYSTEM_PROPERTY); | ||
| if (property == null || property.isBlank()) | ||
| { | ||
| property = System.getenv(TELEMETRY_ENV_VARIABLE); | ||
| } | ||
| return property != null && isDisabledValue(property.trim()); | ||
| } | ||
|
|
||
| /** | ||
| * @return the stored opt-out, which is off as soon as any scope switched reporting off | ||
| */ | ||
| public static boolean isEnabledByPreference() | ||
| { | ||
| boolean defaultValue = DefaultScope.INSTANCE.getNode(IDFCorePlugin.PLUGIN_ID).getBoolean( | ||
| IDFCorePreferenceConstants.TELEMETRY_ENABLED, | ||
| IDFCorePreferenceConstants.TELEMETRY_ENABLED_DEFAULT); | ||
| return ConfigurationScope.INSTANCE.getNode(IDFCorePlugin.PLUGIN_ID) | ||
| .getBoolean(IDFCorePreferenceConstants.TELEMETRY_ENABLED, defaultValue) | ||
| && InstanceScope.INSTANCE.getNode(IDFCorePlugin.PLUGIN_ID) | ||
| .getBoolean(IDFCorePreferenceConstants.TELEMETRY_ENABLED, defaultValue); | ||
| } | ||
|
|
||
| /** | ||
| * Stores the opt-out for the whole installation, so that opting out in one workspace also applies to the others. | ||
| * | ||
| * @param enabled <code>true</code> to report usage statistics | ||
| */ | ||
| public static void setEnabled(boolean enabled) | ||
| { | ||
| for (IEclipsePreferences node : new IEclipsePreferences[] { | ||
| ConfigurationScope.INSTANCE.getNode(IDFCorePlugin.PLUGIN_ID), | ||
| InstanceScope.INSTANCE.getNode(IDFCorePlugin.PLUGIN_ID) }) | ||
| { | ||
| node.putBoolean(IDFCorePreferenceConstants.TELEMETRY_ENABLED, enabled); | ||
| flush(node); | ||
| } | ||
| } | ||
|
|
||
| private static boolean isDisabledValue(String value) | ||
| { | ||
| return "false".equalsIgnoreCase(value) || "0".equals(value) || "no".equalsIgnoreCase(value) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ | ||
| || "off".equalsIgnoreCase(value) || "disabled".equalsIgnoreCase(value); //$NON-NLS-1$ //$NON-NLS-2$ | ||
| } | ||
|
|
||
| /** | ||
| * @return <code>true</code> once the installation told the user that usage statistics are reported | ||
| */ | ||
| public static boolean isNoticeShown() | ||
| { | ||
| return getStateNode().getBoolean(NOTICE_SHOWN, false); | ||
| } | ||
|
|
||
| public static void setNoticeShown() | ||
| { | ||
| IEclipsePreferences node = getStateNode(); | ||
| node.putBoolean(NOTICE_SHOWN, true); | ||
| flush(node); | ||
| } | ||
|
|
||
| /** | ||
| * @return a random identifier created on first use, which is not derived from any machine or user attribute | ||
| */ | ||
| public static String getInstallId() | ||
| { | ||
| IEclipsePreferences node = getStateNode(); | ||
| String installId = node.get(INSTALL_ID, null); | ||
| if (installId == null || installId.isBlank()) | ||
| { | ||
| installId = UUID.randomUUID().toString(); | ||
| node.put(INSTALL_ID, installId); | ||
| flush(node); | ||
| } | ||
| return installId; | ||
| } | ||
|
|
||
| public static long getLastSessionReport() | ||
| { | ||
| return getStateNode().getLong(LAST_SESSION_REPORT, 0L); | ||
| } | ||
|
|
||
| public static void setLastSessionReport(long timestamp) | ||
| { | ||
| IEclipsePreferences node = getStateNode(); | ||
| node.putLong(LAST_SESSION_REPORT, timestamp); | ||
| flush(node); | ||
| } | ||
|
|
||
| public static String getLastReportedVersion() | ||
| { | ||
| return getStateNode().get(LAST_REPORTED_VERSION, ""); //$NON-NLS-1$ | ||
| } | ||
|
|
||
| public static void setLastReportedVersion(String version) | ||
| { | ||
| IEclipsePreferences node = getStateNode(); | ||
| node.put(LAST_REPORTED_VERSION, version); | ||
| flush(node); | ||
| } | ||
|
|
||
| private static IEclipsePreferences getStateNode() | ||
| { | ||
| IEclipsePreferences node = ConfigurationScope.INSTANCE.getNode(IDFCorePlugin.PLUGIN_ID); | ||
| if (node == null) | ||
| { | ||
| node = InstanceScope.INSTANCE.getNode(IDFCorePlugin.PLUGIN_ID); | ||
| } | ||
| return node; | ||
| } | ||
|
|
||
| private static void flush(IEclipsePreferences node) | ||
| { | ||
| try | ||
| { | ||
| node.flush(); | ||
| } | ||
| catch (BackingStoreException e) | ||
| { | ||
| // A read-only configuration area only means the state is recomputed on the next start | ||
| Logger.log(e, true); | ||
|
Comment on lines
+162
to
+181
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Locate file and relevant symbols"
if [ -f bundles/com.espressif.idf.core/src/com/espressif/idf/core/telemetry/TelemetryPreferences.java ]; then
wc -l bundles/com.espressif.idf.core/src/com/espressif/idf/core/telemetry/TelemetryPreferences.java
sed -n '1,260p' bundles/com.espressif.idf.core/src/com/espressif/idf/core/telemetry/TelemetryPreferences.java
else
echo "File not found"
fd -i 'TelemetryPreferences.java' .
fi
echo
echo "Search telemetry persistence usages"
rg -n "TelemetryPreferences|getStateNode|flush\\(|ConfigurationScope|InstanceScope|noticeFlag|installationId|reportedVersion|sessionTimestamp" bundles/com.espressif.idf.core/src/com/espressif/idf/core/telemetry -SRepository: espressif/idf-eclipse-plugin Length of output: 11313 Persist installation telemetry in InstanceScope when configuration scope is read-only.
🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: espressif/idf-eclipse-plugin
Length of output: 233
🏁 Script executed:
Repository: espressif/idf-eclipse-plugin
Length of output: 29806
Sensitive Data Exposure (CWE-359)
Reachability path
Evaluate both opt-out sources.
A nonblank enabling system property currently prevents checking
IDF_TELEMETRY=0, soisDisabledByOverride()returnsfalseand telemetry can run despite the environment variable opt-out. When the system property is not disabling, continue and apply the environment-variable opt-out.🤖 Prompt for AI Agents