From 254a59f6c58df53057dfc631e14c95f67f06f93a Mon Sep 17 00:00:00 2001 From: Achal Talati Date: Wed, 13 Nov 2024 18:01:58 +0530 Subject: [PATCH 01/16] Initial sketch of telemetry some bug fixes and added debug logger Added flag to disable telemetry feature Updated LspServerTelemetryManager with slight refinements 1. The workspaceInfo values, especially the javaVersion field. 2. Also reverted the sendTelemetry() methods to the state similar to earlier definitions with synchronized blocks; with minor refactoring improvements. 3. Added the missed changes in NbCodeClientCapabilities for the addition of the wantsTelemetryEnabled field. Signed-off-by: Siddharth Srinivasan Updated telemetry l10n with only english 1. Added the same english key value for telemetry setting and message in all language bundles. This is to ensure that tests pass. 2. Removed the unused dependency "axios" pruned further dependencies of it. 3. Updated minimum runtime JDK version to 23 instead of 22 in l10n messages. Signed-off-by: Siddharth Srinivasan --- build.xml | 1 + patches/nb-telemetry.diff | 421 ++++++++++++++++++ vscode/.vscode/launch.json | 16 + vscode/l10n/bundle.l10n.en.json | 5 +- vscode/l10n/bundle.l10n.ja.json | 5 +- vscode/l10n/bundle.l10n.zh-cn.json | 5 +- vscode/package.json | 5 + vscode/package.nls.ja.json | 3 +- vscode/package.nls.json | 1 + vscode/package.nls.zh-cn.json | 3 +- vscode/src/configurations/configuration.ts | 3 +- vscode/src/configurations/handlers.ts | 4 + vscode/src/constants.ts | 2 +- vscode/src/extension.ts | 6 +- vscode/src/extensionContextInfo.ts | 3 + vscode/src/logger.ts | 10 + vscode/src/lsp/initializer.ts | 5 +- .../lsp/listeners/notifications/handlers.ts | 14 + vscode/src/lsp/nbLanguageClient.ts | 2 + vscode/src/telemetry/config.ts | 31 ++ vscode/src/telemetry/events/baseEvent.ts | 65 +++ vscode/src/telemetry/events/close.ts | 39 ++ vscode/src/telemetry/events/jdkDownload.ts | 33 ++ vscode/src/telemetry/events/jdkFeature.ts | 81 ++++ vscode/src/telemetry/events/start.ts | 80 ++++ .../src/telemetry/events/workspaceChange.ts | 40 ++ .../src/telemetry/impl/AnonymousIdManager.ts | 21 + vscode/src/telemetry/impl/cacheServiceImpl.ts | 43 ++ .../src/telemetry/impl/enviromentDetails.ts | 76 ++++ vscode/src/telemetry/impl/postTelemetry.ts | 86 ++++ .../src/telemetry/impl/telemetryEventQueue.ts | 38 ++ vscode/src/telemetry/impl/telemetryPrefs.ts | 64 +++ .../telemetry/impl/telemetryReporterImpl.ts | 108 +++++ vscode/src/telemetry/impl/telemetryRetry.ts | 133 ++++++ vscode/src/telemetry/telemetry.ts | 62 +++ vscode/src/telemetry/telemetryManager.ts | 74 +++ vscode/src/telemetry/types.ts | 51 +++ vscode/src/telemetry/utils.ts | 72 +++ .../src/test/unit/mocks/vscode/mockVscode.ts | 2 + .../test/unit/mocks/vscode/namespaces/env.ts | 32 ++ vscode/src/views/projects.ts | 2 + vscode/src/webviews/jdkDownloader/action.ts | 21 +- 42 files changed, 1754 insertions(+), 14 deletions(-) create mode 100644 patches/nb-telemetry.diff create mode 100644 vscode/src/telemetry/config.ts create mode 100644 vscode/src/telemetry/events/baseEvent.ts create mode 100644 vscode/src/telemetry/events/close.ts create mode 100644 vscode/src/telemetry/events/jdkDownload.ts create mode 100644 vscode/src/telemetry/events/jdkFeature.ts create mode 100644 vscode/src/telemetry/events/start.ts create mode 100644 vscode/src/telemetry/events/workspaceChange.ts create mode 100644 vscode/src/telemetry/impl/AnonymousIdManager.ts create mode 100644 vscode/src/telemetry/impl/cacheServiceImpl.ts create mode 100644 vscode/src/telemetry/impl/enviromentDetails.ts create mode 100644 vscode/src/telemetry/impl/postTelemetry.ts create mode 100644 vscode/src/telemetry/impl/telemetryEventQueue.ts create mode 100644 vscode/src/telemetry/impl/telemetryPrefs.ts create mode 100644 vscode/src/telemetry/impl/telemetryReporterImpl.ts create mode 100644 vscode/src/telemetry/impl/telemetryRetry.ts create mode 100644 vscode/src/telemetry/telemetry.ts create mode 100644 vscode/src/telemetry/telemetryManager.ts create mode 100644 vscode/src/telemetry/types.ts create mode 100644 vscode/src/telemetry/utils.ts create mode 100644 vscode/src/test/unit/mocks/vscode/namespaces/env.ts diff --git a/build.xml b/build.xml index 1363869..8acec65 100644 --- a/build.xml +++ b/build.xml @@ -70,6 +70,7 @@ patches/l10n-licence.diff patches/no-security-manager-allow.diff patches/dev-dependency-licenses.diff + patches/nb-telemetry.diff diff --git a/patches/nb-telemetry.diff b/patches/nb-telemetry.diff new file mode 100644 index 0000000..e7c17bb --- /dev/null +++ b/patches/nb-telemetry.diff @@ -0,0 +1,421 @@ +diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/LspServerTelemetryManager.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/LspServerTelemetryManager.java +index d82646afb1..3d507b5fe3 100644 +--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/LspServerTelemetryManager.java ++++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/LspServerTelemetryManager.java +@@ -21,6 +21,7 @@ package org.netbeans.modules.java.lsp.server.protocol; + import com.google.gson.JsonArray; + import com.google.gson.JsonObject; + import com.google.gson.JsonPrimitive; ++import java.lang.ref.WeakReference; + import java.math.BigInteger; + import java.nio.charset.StandardCharsets; + import java.security.MessageDigest; +@@ -28,25 +29,27 @@ import java.security.NoSuchAlgorithmException; + import java.util.ArrayList; + import java.util.Collection; + import java.util.Collections; +-import java.util.HashSet; ++import java.util.Iterator; + import java.util.List; + import java.util.Map; +-import java.util.Set; + import java.util.WeakHashMap; + import java.util.concurrent.Future; +-import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.function.Function; ++import java.util.logging.Level; ++import java.util.logging.Logger; + import java.util.stream.Collectors; + import org.eclipse.lsp4j.ConfigurationItem; + import org.eclipse.lsp4j.ConfigurationParams; + import org.eclipse.lsp4j.MessageType; + import org.eclipse.lsp4j.services.LanguageClient; ++import org.netbeans.api.java.platform.JavaPlatform; + import org.netbeans.api.java.queries.CompilerOptionsQuery; + import org.netbeans.api.java.queries.CompilerOptionsQuery.Result; + import org.netbeans.api.project.Project; + import org.netbeans.api.project.ProjectManager; + import org.netbeans.api.project.ui.ProjectProblems; ++import org.netbeans.modules.java.platform.implspi.JavaPlatformProvider; + import org.openide.filesystems.FileObject; +-import org.openide.util.Exceptions; + import org.openide.util.Lookup; + + /** +@@ -55,130 +58,164 @@ import org.openide.util.Lookup; + */ + public class LspServerTelemetryManager { + +- public final String SCAN_START_EVT = "SCAN_START_EVT"; +- public final String SCAN_END_EVT = "SCAN_END_EVT"; +- public final String WORKSPACE_INFO_EVT = "WORKSPACE_INFO_EVT"; ++ private static final Logger LOG = Logger.getLogger(LspServerTelemetryManager.class.getName()); ++ public static final String SCAN_START_EVT = "SCAN_START_EVT"; ++ public static final String SCAN_END_EVT = "SCAN_END_EVT"; ++ public static final String WORKSPACE_INFO_EVT = "workspaceChange"; + +- private final String ENABLE_PREVIEW = "--enable-preview"; +- private final String STANDALONE_PRJ = "Standalone"; +- private final WeakHashMap> clients = new WeakHashMap<>(); +- private long lspServerIntiailizationTime; ++ private static final String ENABLE_PREVIEW = "--enable-preview"; + +- public synchronized void connect(LanguageClient client, Future future) { +- clients.put(client, future); +- lspServerIntiailizationTime = System.currentTimeMillis(); ++ private static enum ProjectType { ++ standalone, ++ maven, ++ gradle; + } + +- public synchronized void sendTelemetry(TelemetryEvent event) { +- Set toRemove = new HashSet<>(); +- List toSendTelemetry = new ArrayList<>(); ++ private LspServerTelemetryManager() { ++ } ++ ++ public static LspServerTelemetryManager getInstance() { ++ return Singleton.instance; ++ } ++ ++ private static class Singleton { ++ ++ private static final LspServerTelemetryManager instance = new LspServerTelemetryManager(); ++ } + ++ private final WeakHashMap>> clients = new WeakHashMap<>(); ++ private volatile boolean telemetryEnabled = false; ++ private long lspServerIntializationTime; ++ ++ public boolean isTelemetryEnabled() { ++ return telemetryEnabled; ++ } ++ ++ public void connect(LanguageClient client, Future future) { + synchronized (clients) { +- for (Map.Entry> entry : clients.entrySet()) { +- if (entry.getValue().isDone()) { +- toRemove.add(entry.getKey()); +- } else { +- toSendTelemetry.add(entry.getKey()); +- } +- } +- clients.keySet().removeAll(toRemove); ++ clients.put(client, new WeakReference<>(future)); ++ telemetryEnabled = true; ++ lspServerIntializationTime = System.currentTimeMillis(); + } ++ } + +- for (LanguageClient client : toSendTelemetry) { +- client.telemetryEvent(event); ++ public void sendTelemetry(TelemetryEvent event) { ++ if (telemetryEnabled) { ++ ArrayList clientsCopy = new ArrayList<>(2); ++ synchronized (clients) { ++ Iterator>>> iterator = clients.entrySet().iterator(); ++ while (iterator.hasNext()) { ++ Map.Entry>> e = iterator.next(); ++ if (isInvalidClient(e.getValue())) { ++ iterator.remove(); ++ } else { ++ clientsCopy.add(e.getKey()); ++ } ++ } ++ if (clientsCopy.isEmpty()) { ++ telemetryEnabled = false; ++ } ++ } ++ clientsCopy.forEach(c -> sendTelemetryToValidClient(c, event)); + } + } +- +- public void sendTelemetry(LanguageClient client, TelemetryEvent event) { +- boolean shouldSendTelemetry = false; + +- synchronized (clients) { +- if(clients.containsKey(client)){ +- if (clients.get(client).isDone()) { +- clients.remove(client); +- } else { +- shouldSendTelemetry = true; ++ public void sendTelemetry(LanguageClient client, TelemetryEvent event) { ++ if (telemetryEnabled) { ++ WeakReference> closeListener = clients.get(client); ++ if (isInvalidClient(closeListener)) { ++ synchronized (clients) { ++ if (clients.remove(client, closeListener) && clients.isEmpty()) { ++ telemetryEnabled = false; ++ } + } ++ } else { ++ sendTelemetryToValidClient(client, event); + } + } ++ } + +- if (shouldSendTelemetry) { ++ private void sendTelemetryToValidClient(LanguageClient client, TelemetryEvent event) { ++ try { + client.telemetryEvent(event); ++ } catch (Exception e) { ++ LOG.log(Level.INFO, "telemetry send failed: {0}", e.getMessage()); + } + } + +- public void sendWorkspaceInfo(LanguageClient client, List workspaceClientFolders, Collection prjs, long timeToOpenPrjs) { ++ private boolean isInvalidClient(WeakReference> closeListener) { ++ Future close = closeListener == null ? null : closeListener.get(); ++ return close == null || close.isDone(); ++ } ++ ++ public void sendWorkspaceInfo(LanguageClient client, List workspaceClientFolders, Collection projects, long timeToOpenProjects) { + JsonObject properties = new JsonObject(); + JsonArray prjProps = new JsonArray(); + +- Map mp = prjs.stream() ++ Map mp = projects.stream() + .collect(Collectors.toMap(project -> project.getProjectDirectory().getPath(), project -> project)); + + for (FileObject workspaceFolder : workspaceClientFolders) { + try { + JsonObject obj = new JsonObject(); + String prjPath = workspaceFolder.getPath(); +- String prjId = this.getPrjId(prjPath); ++ String prjId = getPrjId(prjPath); + obj.addProperty("id", prjId); +- +- // In future if different JDK is used for different project then this can be updated +- obj.addProperty("javaVersion", System.getProperty("java.version")); +- +- if (mp.containsKey(prjPath)) { +- Project prj = mp.get(prjPath); ++ String javaVersion = getProjectJavaVersion(); ++ obj.addProperty("javaVersion", javaVersion); + +- ProjectManager.Result r = ProjectManager.getDefault().isProject2(prj.getProjectDirectory()); +- String projectType = r.getProjectType(); +- obj.addProperty("buildTool", (projectType.contains("maven") ? "MavenProject" : "GradleProject")); +- +- obj.addProperty("openedWithProblems", ProjectProblems.isBroken(prj)); +- +- boolean isPreviewFlagEnabled = this.isEnablePreivew(prj.getProjectDirectory(), projectType); +- obj.addProperty("enablePreview", isPreviewFlagEnabled); ++ Project prj = mp.get(prjPath); ++ FileObject projectDirectory; ++ ProjectType projectType; ++ if (prj == null) { ++ projectType = ProjectType.standalone; ++ projectDirectory = workspaceFolder; + } else { +- obj.addProperty("buildTool", this.STANDALONE_PRJ); +- obj.addProperty("javaVersion", System.getProperty("java.version")); +- obj.addProperty("openedWithProblems", false); +- +- boolean isPreviewFlagEnabled = this.isEnablePreivew(workspaceFolder, this.STANDALONE_PRJ); +- obj.addProperty("enablePreview", isPreviewFlagEnabled); ++ projectType = getProjectType(prj); ++ projectDirectory = prj.getProjectDirectory(); ++ obj.addProperty("isOpenedWithProblems", ProjectProblems.isBroken(prj)); + } ++ obj.addProperty("buildTool", projectType.name()); ++ boolean isPreviewFlagEnabled = isPreviewEnabled(projectDirectory, projectType); ++ obj.addProperty("isPreviewEnabled", isPreviewFlagEnabled); + + prjProps.add(obj); + +- } catch (NoSuchAlgorithmException ex) { +- Exceptions.printStackTrace(ex); ++ } catch (NoSuchAlgorithmException e) { ++ LOG.log(Level.INFO, "NoSuchAlgorithmException while creating workspaceInfo event: {0}", e.getMessage()); ++ } catch (Exception e) { ++ LOG.log(Level.INFO, "Exception while creating workspaceInfo event: {0}", e.getMessage()); + } + } + +- properties.add("prjsInfo", prjProps); ++ properties.add("projectInfo", prjProps); + +- properties.addProperty("timeToOpenPrjs", timeToOpenPrjs); +- properties.addProperty("numOfPrjsOpened", workspaceClientFolders.size()); +- properties.addProperty("lspServerInitializationTime", System.currentTimeMillis() - this.lspServerIntiailizationTime); ++ properties.addProperty("projInitTimeTaken", timeToOpenProjects); ++ properties.addProperty("numProjects", workspaceClientFolders.size()); ++ properties.addProperty("lspInitTimeTaken", System.currentTimeMillis() - this.lspServerIntializationTime); + +- this.sendTelemetry(client, new TelemetryEvent(MessageType.Info.toString(), this.WORKSPACE_INFO_EVT, properties)); ++ this.sendTelemetry(client, new TelemetryEvent(MessageType.Info.toString(), LspServerTelemetryManager.WORKSPACE_INFO_EVT, properties)); + } +- +- private boolean isEnablePreivew(FileObject source, String prjType) { +- if (prjType.equals(this.STANDALONE_PRJ)) { ++ ++ private boolean isPreviewEnabled(FileObject source, ProjectType prjType) { ++ if (prjType == ProjectType.standalone) { + NbCodeLanguageClient client = Lookup.getDefault().lookup(NbCodeLanguageClient.class); + if (client == null) { + return false; + } +- AtomicBoolean isEnablePreviewSet = new AtomicBoolean(false); ++ boolean[] isEnablePreviewSet = {false}; + ConfigurationItem conf = new ConfigurationItem(); +- conf.setSection(client.getNbCodeCapabilities().getAltConfigurationPrefix() + "runConfig.vmOptions"); +- client.configuration(new ConfigurationParams(Collections.singletonList(conf))).thenAccept(c -> { +- String config = ((JsonPrimitive) ((List) c).get(0)).getAsString(); +- isEnablePreviewSet.set(config.contains(this.ENABLE_PREVIEW)); +- }); +- +- return isEnablePreviewSet.get(); ++ conf.setSection(client.getNbCodeCapabilities().getConfigurationPrefix() + "runConfig.vmOptions"); ++ client.configuration(new ConfigurationParams(Collections.singletonList(conf))) ++ .thenAccept(c -> { ++ isEnablePreviewSet[0] = c != null && !c.isEmpty() ++ && ((JsonPrimitive) c.get(0)).getAsString().contains(ENABLE_PREVIEW); ++ }); ++ return isEnablePreviewSet[0]; + } +- ++ + Result result = CompilerOptionsQuery.getOptions(source); +- return result.getArguments().contains(this.ENABLE_PREVIEW); ++ return result.getArguments().contains(ENABLE_PREVIEW); + } + + private String getPrjId(String prjPath) throws NoSuchAlgorithmException { +@@ -187,15 +224,50 @@ public class LspServerTelemetryManager { + + BigInteger number = new BigInteger(1, hash); + +- // Convert message digest into hex value + StringBuilder hexString = new StringBuilder(number.toString(16)); + +- // Pad with leading zeros + while (hexString.length() < 64) { + hexString.insert(0, '0'); + } + + return hexString.toString(); + } +- ++ ++ private String getProjectJavaVersion() { ++ final JavaPlatformProvider javaPlatformProvider = Lookup.getDefault().lookup(JavaPlatformProvider.class); ++ final JavaPlatform defaultPlatform = javaPlatformProvider == null ? null : javaPlatformProvider.getDefaultPlatform(); ++ final Map props = defaultPlatform == null ? null : defaultPlatform.getSystemProperties(); ++ final Function propLookup = props == null ? System::getProperty : props::get; ++ ++ return getJavaRuntimeVersion(propLookup) + ';' + getJavaVmVersion(propLookup) + ';' + getJavaVmName(propLookup); ++ } ++ ++ public static String getJavaRuntimeVersion(Function propertyLookup) { ++ String version = propertyLookup.apply("java.runtime.version"); ++ if (version == null) { ++ version = propertyLookup.apply("java.version"); ++ } ++ return version; ++ } ++ ++ public static String getJavaVmVersion(Function propertyLookup) { ++ String version = propertyLookup.apply("java.vendor.version"); ++ if (version == null) { ++ version = propertyLookup.apply("java.vm.version"); ++ if (version == null) { ++ version = propertyLookup.apply("java.version"); ++ } ++ } ++ return version; ++ } ++ ++ public static String getJavaVmName(Function propertyLookup) { ++ return propertyLookup.apply("java.vm.name"); ++ } ++ ++ private ProjectType getProjectType(Project prj) { ++ ProjectManager.Result r = ProjectManager.getDefault().isProject2(prj.getProjectDirectory()); ++ String projectType = r == null ? null : r.getProjectType(); ++ return projectType != null && projectType.contains(ProjectType.maven.name()) ? ProjectType.maven : ProjectType.gradle; ++ } + } +diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeClientCapabilities.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeClientCapabilities.java +index 9134992f5f..f070fec320 100644 +--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeClientCapabilities.java ++++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeClientCapabilities.java +@@ -90,6 +90,11 @@ public final class NbCodeClientCapabilities { + * Secondary prefix for configuration. + */ + private String altConfigurationPrefix = "java+."; ++ ++ /** ++ * Whether telemetry needs to be enabled. ++ */ ++ private Boolean wantsTelemetryEnabled = Boolean.FALSE; + + public ClientCapabilities getClientCapabilities() { + return clientCaps; +@@ -179,6 +184,10 @@ public final class NbCodeClientCapabilities { + this.altConfigurationPrefix = altConfigurationPrefix; + } + ++ public boolean wantsTelemetryEnabled() { ++ return wantsTelemetryEnabled; ++ } ++ + private NbCodeClientCapabilities withCapabilities(ClientCapabilities caps) { + if (caps == null) { + caps = new ClientCapabilities(); +diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java +index 203cb9e7bc..b729ba0ef8 100644 +--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java ++++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java +@@ -160,7 +160,6 @@ import org.openide.util.lookup.ProxyLookup; + */ + public final class Server { + private static final Logger LOG = Logger.getLogger(Server.class.getName()); +- private static final LspServerTelemetryManager LSP_SERVER_TELEMETRY = new LspServerTelemetryManager(); + + private Server() { + } +@@ -183,7 +182,6 @@ public final class Server { + ((LanguageClientAware) server).connect(remote); + msgProcessor.attachClient(server.client); + Future runningServer = serverLauncher.startListening(); +- LSP_SERVER_TELEMETRY.connect(server.client, runningServer); + return new NbLspServer(server, runningServer); + } + +@@ -773,7 +771,7 @@ public final class Server { + } + f.complete(candidateMapping); + List workspaceClientFolders = workspaceService.getClientWorkspaceFolders(); +- LSP_SERVER_TELEMETRY.sendWorkspaceInfo(client, workspaceClientFolders, openedProjects, System.currentTimeMillis() - t); ++ LspServerTelemetryManager.getInstance().sendWorkspaceInfo(client, workspaceClientFolders, openedProjects, System.currentTimeMillis() - t); + LOG.log(Level.INFO, "{0} projects opened in {1}ms", new Object[] { prjsRequested.length, (System.currentTimeMillis() - t) }); + } else { + LOG.log(Level.FINER, "{0}: Collecting projects to prime from: {1}", new Object[]{id, Arrays.asList(additionalProjects)}); +@@ -930,6 +928,9 @@ public final class Server { + public CompletableFuture initialize(InitializeParams init) { + NbCodeClientCapabilities capa = NbCodeClientCapabilities.get(init); + client.setClientCaps(capa); ++ if (capa.wantsTelemetryEnabled()) { ++ LspServerTelemetryManager.getInstance().connect(client, lspSession.getLspServer().getRunningFuture()); ++ } + hackConfigureGroovySupport(capa); + hackNoReuseOfOutputsForAntProjects(); + List projectCandidates = new ArrayList<>(); +@@ -1405,13 +1406,13 @@ public final class Server { + + @Override + public synchronized boolean scanStarted(Context context) { +- LSP_SERVER_TELEMETRY.sendTelemetry(new TelemetryEvent(MessageType.Info.toString(), LSP_SERVER_TELEMETRY.SCAN_START_EVT, "nbls.scanStarted")); ++ LspServerTelemetryManager.getInstance().sendTelemetry(new TelemetryEvent(MessageType.Info.toString(), LspServerTelemetryManager.SCAN_START_EVT, "nbls.scanStarted")); + return true; + } + + @Override + public synchronized void scanFinished(Context context) { +- LSP_SERVER_TELEMETRY.sendTelemetry(new TelemetryEvent(MessageType.Info.toString(),LSP_SERVER_TELEMETRY.SCAN_END_EVT,"nbls.scanFinished")); ++ LspServerTelemetryManager.getInstance().sendTelemetry(new TelemetryEvent(MessageType.Info.toString(), LspServerTelemetryManager.SCAN_END_EVT,"nbls.scanFinished")); + } + + @Override diff --git a/vscode/.vscode/launch.json b/vscode/.vscode/launch.json index 1bcbd4e..14ef7a1 100644 --- a/vscode/.vscode/launch.json +++ b/vscode/.vscode/launch.json @@ -20,6 +20,22 @@ "env": { "nbcode_userdir": "global" } + },{ + "name": "Debug Telemetry", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "${defaultBuildTask}", + "env": { + "nbcode_userdir": "global", + "oracle.oracle-java.enable.debug-logs": "true" + } }, { "name": "Extension Tests", diff --git a/vscode/l10n/bundle.l10n.en.json b/vscode/l10n/bundle.l10n.en.json index e063545..627827d 100644 --- a/vscode/l10n/bundle.l10n.en.json +++ b/vscode/l10n/bundle.l10n.en.json @@ -93,5 +93,6 @@ "jdk.extension.debugger.error_msg.debugAdapterNotInitialized":"Oracle Java SE Debug Server Adapter not yet initialized. Please wait for a while and try again.", "jdk.workspace.new.prompt": "Input the directory path where the new file will be generated", "jdk.extension.utils.error_message.failedHttpsRequest": "Failed to get {url} ({statusCode})", - "jdk.extension.error_msg.notEnabled": "{SERVER_NAME} not enabled" -} \ No newline at end of file + "jdk.extension.error_msg.notEnabled": "{SERVER_NAME} not enabled", + "jdk.telemetry.consent": "Do you want to enable telemetry for {extensionName} extension? You may opt-out or in at any time from the Settings for jdk.telemetry.enabled." +} diff --git a/vscode/l10n/bundle.l10n.ja.json b/vscode/l10n/bundle.l10n.ja.json index 3085f37..361e1ed 100755 --- a/vscode/l10n/bundle.l10n.ja.json +++ b/vscode/l10n/bundle.l10n.ja.json @@ -93,5 +93,6 @@ "jdk.extension.debugger.error_msg.debugAdapterNotInitialized":"Oracle Java SEのデバッグ・サーバー・アダプタが、まだ初期化されていません。しばらく待ってから再試行してください。", "jdk.workspace.new.prompt": "新しいファイルを生成するディレクトリのパスを入力してください", "jdk.extension.utils.error_message.failedHttpsRequest": "{url}の取得に失敗しました({statusCode})", - "jdk.extension.error_msg.notEnabled": "{SERVER_NAME}が有効化されていません" -} \ No newline at end of file + "jdk.extension.error_msg.notEnabled": "{SERVER_NAME}が有効化されていません", + "jdk.telemetry.consent": "Do you want to enable telemetry for {extensionName} extension? You may opt-out or in at any time from the Settings for jdk.telemetry.enabled." +} diff --git a/vscode/l10n/bundle.l10n.zh-cn.json b/vscode/l10n/bundle.l10n.zh-cn.json index e35bd50..d095ec1 100755 --- a/vscode/l10n/bundle.l10n.zh-cn.json +++ b/vscode/l10n/bundle.l10n.zh-cn.json @@ -93,5 +93,6 @@ "jdk.extension.debugger.error_msg.debugAdapterNotInitialized":"Oracle Java SE 调试服务器适配器尚未初始化。请稍候,然后重试。", "jdk.workspace.new.prompt": "输入生成新文件的目录路径", "jdk.extension.utils.error_message.failedHttpsRequest": "无法获取 {url} ({statusCode})", - "jdk.extension.error_msg.notEnabled": "{SERVER_NAME} 未启用" -} \ No newline at end of file + "jdk.extension.error_msg.notEnabled": "{SERVER_NAME} 未启用", + "jdk.telemetry.consent": "Do you want to enable telemetry for {extensionName} extension? You may opt-out or in at any time from the Settings for jdk.telemetry.enabled." +} diff --git a/vscode/package.json b/vscode/package.json index 3c5dc28..22d1c15 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -236,6 +236,11 @@ "type": "boolean", "default": false, "description": "%jdk.configuration.disableProjectSearchLimit.description%" + }, + "jdk.telemetry.enabled": { + "type": "boolean", + "description": "%jdk.configuration.telemetry.enabled.description%", + "default": false } } }, diff --git a/vscode/package.nls.ja.json b/vscode/package.nls.ja.json index 1f07da0..fac493d 100755 --- a/vscode/package.nls.ja.json +++ b/vscode/package.nls.ja.json @@ -46,6 +46,7 @@ "jdk.configuration.runConfig.cwd.description": "作業ディレクトリ", "jdk.configuration.disableNbJavac.description": "拡張オプション: nb-javacライブラリを無効化すると、選択したJDKからのjavacが使用されます。選択したJDKは少なくともJDK 23である必要があります。", "jdk.configuration.disableProjectSearchLimit.description": "拡張オプション: プロジェクト情報が含まれているフォルダの検索に対する制限を無効化します。", + "jdk.configuration.telemetry.enabled.description": "Allow the Oracle Java Visual Studio Code extension to collect and send usage data to Oracle servers to help improve the Java platform support. No personal information nor source code is collected. You may refer to our general privacy policy at https://www.oracle.com/legal/privacy/services-privacy-policy/", "jdk.debugger.configuration.mainClass.description": "プログラムのメイン・クラスへの絶対パス。", "jdk.debugger.configuration.classPaths.description": "JVMの起動のためのクラスパス。", "jdk.debugger.configuration.console.description": "プログラムを起動する指定されたコンソール。", @@ -64,4 +65,4 @@ "jdk.configurationSnippets.name": "Javaアプリケーションの起動", "jdk.configurationSnippets.label": "Java+: Javaアプリケーションの起動", "jdk.configurationSnippets.description": "デバッグ・モードでのJavaアプリケーションの起動" -} \ No newline at end of file +} diff --git a/vscode/package.nls.json b/vscode/package.nls.json index 654199e..c4dab2d 100644 --- a/vscode/package.nls.json +++ b/vscode/package.nls.json @@ -46,6 +46,7 @@ "jdk.configuration.runConfig.cwd.description": "Working directory", "jdk.configuration.disableNbJavac.description": "Advanced option: disable nb-javac library, javac from the selected JDK will be used. The selected JDK must be at least JDK 23.", "jdk.configuration.disableProjectSearchLimit.description": "Advanced option: disable limits on searching in containing folders for project information.", + "jdk.configuration.telemetry.enabled.description": "Allow the Oracle Java Visual Studio Code extension to collect and send usage data to Oracle servers to help improve the Java platform support. No personal information nor source code is collected. You may refer to our general privacy policy at https://www.oracle.com/legal/privacy/services-privacy-policy/", "jdk.debugger.configuration.mainClass.description": "Absolute path to the program main class.", "jdk.debugger.configuration.classPaths.description": "The classpaths for launching the JVM.", "jdk.debugger.configuration.console.description": "The specified console to launch the program.", diff --git a/vscode/package.nls.zh-cn.json b/vscode/package.nls.zh-cn.json index 8bb29e6..af90e6b 100755 --- a/vscode/package.nls.zh-cn.json +++ b/vscode/package.nls.zh-cn.json @@ -46,6 +46,7 @@ "jdk.configuration.runConfig.cwd.description": "工作目录", "jdk.configuration.disableNbJavac.description": "高级选项:禁用 nb-javac 库,将使用来自所选 JDK 的 javac。所选 JDK 必须至少为 JDK 23。", "jdk.configuration.disableProjectSearchLimit.description": "高级选项:禁用在包含项目信息的文件夹中搜索的限制。", + "jdk.configuration.telemetry.enabled.description": "Allow the Oracle Java Visual Studio Code extension to collect and send usage data to Oracle servers to help improve the Java platform support. No personal information nor source code is collected. You may refer to our general privacy policy at https://www.oracle.com/legal/privacy/services-privacy-policy/", "jdk.debugger.configuration.mainClass.description": "程序主类的绝对路径。", "jdk.debugger.configuration.classPaths.description": "用于启动 JVM 的类路径。", "jdk.debugger.configuration.console.description": "用于启动程序的指定控制台。", @@ -64,4 +65,4 @@ "jdk.configurationSnippets.name": "启动 Java 应用程序", "jdk.configurationSnippets.label": "Java+:启动 Java 应用程序", "jdk.configurationSnippets.description": "以调试模式启动 Java 应用程序" -} \ No newline at end of file +} diff --git a/vscode/src/configurations/configuration.ts b/vscode/src/configurations/configuration.ts index eea3132..fadbe64 100644 --- a/vscode/src/configurations/configuration.ts +++ b/vscode/src/configurations/configuration.ts @@ -30,7 +30,8 @@ export const configKeys = { runConfigEnv: 'runConfig.env', verbose: 'verbose', userdir: 'userdir', - revealInActivteProj: "revealActiveInProjects" + revealInActivteProj: "revealActiveInProjects", + telemetryEnabled: 'telemetry.enabled', }; export const builtInConfigKeys = { diff --git a/vscode/src/configurations/handlers.ts b/vscode/src/configurations/handlers.ts index 2263bd8..33888e4 100644 --- a/vscode/src/configurations/handlers.ts +++ b/vscode/src/configurations/handlers.ts @@ -44,6 +44,10 @@ export const getBuiltinConfigurationValue = (key: string, defaultValue: T | u return defaultValue != undefined ? conf?.get(confKey, defaultValue) : conf?.get(confKey) as T; } +export const inspectConfiguration = (config: string) => { + return workspace.getConfiguration().inspect(config); +} + export const jdkHomeValueHandler = (): string | null => { return getConfigurationValue(configKeys.jdkHome) || process.env.JDK_HOME || diff --git a/vscode/src/constants.ts b/vscode/src/constants.ts index 7a6b493..cbc0324 100644 --- a/vscode/src/constants.ts +++ b/vscode/src/constants.ts @@ -34,7 +34,7 @@ export namespace jdkDownloaderConstants { export const OPEN_JDK_VERSION_DOWNLOAD_LINKS: { [key: string]: string } = { "23": "https://download.java.net/java/GA/jdk23.0.1/c28985cbf10d4e648e4004050f8781aa/11/GPL/openjdk-23.0.1" - }; + }; } export const NODE_WINDOWS_LABEL = "Windows_NT"; diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index 1dce05d..41ff804 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -33,11 +33,14 @@ import { registerFileProviders } from './lsp/listeners/textDocumentContentProvid import { ExtensionContextInfo } from './extensionContextInfo'; import { ClientPromise } from './lsp/clientPromise'; import { globalState } from './globalState'; +import { Telemetry } from './telemetry/telemetry'; export function activate(context: ExtensionContext): VSNetBeansAPI { - globalState.initialize(new ExtensionContextInfo(context), new ClientPromise()); + const contextInfo = new ExtensionContextInfo(context); + globalState.initialize(contextInfo, new ClientPromise()); globalState.getClientPromise().initialize(); + Telemetry.initializeTelemetry(contextInfo); registerConfigChangeListeners(context); clientInit(); @@ -59,6 +62,7 @@ export function activate(context: ExtensionContext): VSNetBeansAPI { export function deactivate(): Thenable { + Telemetry.enqueueCloseEvent(); const process = globalState.getNbProcessManager()?.getProcess(); if (process != null) { process?.kill(); diff --git a/vscode/src/extensionContextInfo.ts b/vscode/src/extensionContextInfo.ts index da0cdeb..fc893a4 100644 --- a/vscode/src/extensionContextInfo.ts +++ b/vscode/src/extensionContextInfo.ts @@ -23,4 +23,7 @@ export class ExtensionContextInfo { getExtensionStorageUri = () => this.context.extensionUri; getExtensionContext = () => this.context; pushSubscription = (listener: Disposable) => this.context.subscriptions.push(listener); + getExtensionId = () => this.context.extension.id; + getPackageJson = () => this.context.extension.packageJSON; + getVscGlobalState = () => this.context.globalState; } \ No newline at end of file diff --git a/vscode/src/logger.ts b/vscode/src/logger.ts index c0c4fa4..de0ca56 100644 --- a/vscode/src/logger.ts +++ b/vscode/src/logger.ts @@ -20,13 +20,16 @@ enum LogLevel { INFO = 'INFO', WARN = 'WARN', ERROR = 'ERROR', + DEBUG = 'DEBUG', } export class ExtensionLogger { private outChannel: OutputChannel; + private isDebugLogEnabled: boolean; constructor(channelName: string) { this.outChannel = window.createOutputChannel(channelName); + this.isDebugLogEnabled = process.env['oracle.oracle-java.enable.debug-logs'] === "true"; } public log(message: string): void { @@ -44,6 +47,13 @@ export class ExtensionLogger { this.printLog(formattedMessage); } + public debug(message: string): void { + if(this.isDebugLogEnabled){ + const formattedMessage = `[${LogLevel.DEBUG}]: ${message}`; + this.printLog(formattedMessage); + } + } + public logNoNL(message: string): void { this.outChannel.append(message); } diff --git a/vscode/src/lsp/initializer.ts b/vscode/src/lsp/initializer.ts index 793967b..dfe178b 100644 --- a/vscode/src/lsp/initializer.ts +++ b/vscode/src/lsp/initializer.ts @@ -28,6 +28,7 @@ import { registerNotificationListeners } from "./listeners/notifications/registe import { registerRequestListeners } from "./listeners/requests/register"; import { createViews } from "../views/initializer"; import { globalState } from "../globalState"; +import { Telemetry } from "../telemetry/telemetry"; const establishConnection = () => new Promise((resolve, reject) => { const nbProcessManager = globalState.getNbProcessManager(); @@ -108,8 +109,8 @@ export const clientInit = () => { LOGGER.log('Language Client: Starting'); client.start().then(() => { - - + Telemetry.enqueueStartEvent(); + registerListenersAfterClientInit(); registerNotificationListeners(client); registerRequestListeners(client); diff --git a/vscode/src/lsp/listeners/notifications/handlers.ts b/vscode/src/lsp/listeners/notifications/handlers.ts index 0db0c48..2387077 100644 --- a/vscode/src/lsp/listeners/notifications/handlers.ts +++ b/vscode/src/lsp/listeners/notifications/handlers.ts @@ -23,6 +23,8 @@ import { configKeys } from "../../../configurations/configuration"; import { builtInCommands } from "../../../commands/commands"; import { LOGGER } from '../../../logger'; import { globalState } from "../../../globalState"; +import { WorkspaceChangeData, WorkspaceChangeEvent } from "../../../telemetry/events/workspaceChange"; +import { Telemetry } from "../../../telemetry/telemetry"; const checkInstallNbJavac = (msg: string) => { const NO_JAVA_SUPPORT = "Cannot initialize Java support"; @@ -109,6 +111,18 @@ const textEditorDecorationDisposeHandler = (param: any) => { const telemetryEventHandler = (param: any) => { + if(WorkspaceChangeEvent.NAME === param?.name){ + const {projectInfo, numProjects, lspInitTimeTaken, projInitTimeTaken} = param?.properties; + const eventData: WorkspaceChangeData = { + projectInfo, + numProjects, + lspInitTimeTaken, + projInitTimeTaken + }; + const workspaceChangeEvent: WorkspaceChangeEvent = new WorkspaceChangeEvent(eventData); + Telemetry.sendTelemetry(workspaceChangeEvent); + return; + } const ls = globalState.getListener(param); if (ls) { for (const listener of ls) { diff --git a/vscode/src/lsp/nbLanguageClient.ts b/vscode/src/lsp/nbLanguageClient.ts index c2f1388..f472ab7 100644 --- a/vscode/src/lsp/nbLanguageClient.ts +++ b/vscode/src/lsp/nbLanguageClient.ts @@ -22,6 +22,7 @@ import { userConfigsListenedByServer } from '../configurations/configuration'; import { restartWithJDKLater } from './utils'; import { ExtensionLogger } from '../logger'; import { globalState } from '../globalState'; +import { Telemetry } from '../telemetry/telemetry'; export class NbLanguageClient extends LanguageClient { @@ -61,6 +62,7 @@ export class NbLanguageClient extends LanguageClient { 'showHtmlPageSupport': true, 'wantsJavaSupport': true, 'wantsGroovySupport': false, + 'wantsTelemetryEnabled': Telemetry.isTelemetryFeatureAvailable, 'commandPrefix': extConstants.COMMAND_PREFIX, 'configurationPrefix': `${extConstants.COMMAND_PREFIX}.`, 'altConfigurationPrefix': `${extConstants.COMMAND_PREFIX}.` diff --git a/vscode/src/telemetry/config.ts b/vscode/src/telemetry/config.ts new file mode 100644 index 0000000..1852099 --- /dev/null +++ b/vscode/src/telemetry/config.ts @@ -0,0 +1,31 @@ +/* + Copyright (c) 2024, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +import { RetryConfig, TelemetryApi } from "./types"; + +export const TELEMETRY_RETRY_CONFIG: RetryConfig = Object.freeze({ + maxRetries: 6, + baseCapacity: 256, + baseTimer: 5 * 1000, + maxDelayMs: 100 * 1000, + backoffFactor: 2, + jitterFactor: 0.25 +}); + +export const TELEMETRY_API: TelemetryApi = Object.freeze({ + baseUrl: null, + baseEndpoint: "/vscode/java/sendTelemetry", + version: "/v1" +}); \ No newline at end of file diff --git a/vscode/src/telemetry/events/baseEvent.ts b/vscode/src/telemetry/events/baseEvent.ts new file mode 100644 index 0000000..87a2087 --- /dev/null +++ b/vscode/src/telemetry/events/baseEvent.ts @@ -0,0 +1,65 @@ +/* + Copyright (c) 2024, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +import { LOGGER } from "../../logger"; +import { AnonymousIdManager } from "../impl/AnonymousIdManager"; +import { cacheService } from "../impl/cacheServiceImpl"; +import { getHashCode } from "../utils"; + +export interface BaseEventPayload { + vsCodeId: string; + vscSessionId: string; +} + +export abstract class BaseEvent { + protected _payload: T & BaseEventPayload; + protected _data: T + + constructor(public readonly NAME: string, + public readonly ENDPOINT: string, + data: T + ) { + this._data = data; + this._payload = { + vsCodeId: AnonymousIdManager.machineId, + vscSessionId: AnonymousIdManager.sessionId, + ...data + }; + } + + get getPayload(): T & BaseEventPayload { + return this._payload; + } + + get getData(): T { + return this._data; + } + + public onSuccessPostEventCallback = async (): Promise => { + LOGGER.debug(`${this.NAME} sent successfully`); + } + + public onFailPostEventCallback = async (): Promise => { + LOGGER.debug(`${this.NAME} send failed`); + } + + protected addEventToCache = (): void => { + const dataString = JSON.stringify(this.getData); + const calculatedHashVal = getHashCode(dataString); + const isAdded = cacheService.put(this.NAME, calculatedHashVal); + + LOGGER.debug(`${this.NAME} added in cache ${isAdded ? "Successfully" : "Unsucessfully"}`); + } +} \ No newline at end of file diff --git a/vscode/src/telemetry/events/close.ts b/vscode/src/telemetry/events/close.ts new file mode 100644 index 0000000..3e9d394 --- /dev/null +++ b/vscode/src/telemetry/events/close.ts @@ -0,0 +1,39 @@ +/* + Copyright (c) 2024, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +import { getCurrentUTCDateInSeconds } from "../utils"; +import { BaseEvent } from "./baseEvent"; + +export interface CloseEventData { + totalSessionTime: number; +} + +export class ExtensionCloseEvent extends BaseEvent { + public static readonly NAME = "close"; + public static readonly ENDPOINT = "/close"; + + constructor(payload: CloseEventData){ + super(ExtensionCloseEvent.NAME, ExtensionCloseEvent.ENDPOINT, payload); + } + + public static builder = (activationTime: number): ExtensionCloseEvent => { + const totalActiveSessionTimeInSeconds = getCurrentUTCDateInSeconds() - activationTime; + const closeEvent: ExtensionCloseEvent = new ExtensionCloseEvent({ + totalSessionTime: totalActiveSessionTimeInSeconds + }); + + return closeEvent; + } +} \ No newline at end of file diff --git a/vscode/src/telemetry/events/jdkDownload.ts b/vscode/src/telemetry/events/jdkDownload.ts new file mode 100644 index 0000000..f227b12 --- /dev/null +++ b/vscode/src/telemetry/events/jdkDownload.ts @@ -0,0 +1,33 @@ +/* + Copyright (c) 2024, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +import { BaseEvent } from "./baseEvent"; + +export interface JdkDownloadEventData { + vendor: string; + version: string; + os: string; + arch: string; + timeTaken: number; +} + +export class JdkDownloadEvent extends BaseEvent { + public static readonly NAME = "jdkDownload"; + public static readonly ENDPOINT = "/jdkDownload"; + + constructor(payload: JdkDownloadEventData) { + super(JdkDownloadEvent.NAME, JdkDownloadEvent.ENDPOINT, payload); + } +} \ No newline at end of file diff --git a/vscode/src/telemetry/events/jdkFeature.ts b/vscode/src/telemetry/events/jdkFeature.ts new file mode 100644 index 0000000..7c292e3 --- /dev/null +++ b/vscode/src/telemetry/events/jdkFeature.ts @@ -0,0 +1,81 @@ +/* + Copyright (c) 2024, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +import { BaseEvent } from "./baseEvent"; + +export interface JdkFeatureEventData { + jeps: number[]; + names: string[]; + javaVersion: string; + isPreviewEnabled: boolean; +} + +export class JdkFeatureEvent extends BaseEvent { + public static readonly NAME = "jdkFeature"; + public static readonly ENDPOINT = "/jdkFeature"; + + constructor(payload: JdkFeatureEventData) { + super(JdkFeatureEvent.NAME, JdkFeatureEvent.ENDPOINT, payload); + } + + public static concatEvents(events:JdkFeatureEvent[]): JdkFeatureEvent[] { + const jdkFeatureEvents = events.filter(event => event.NAME === this.NAME); + const { previewEnabledMap, previewDisabledMap } = this.groupEvents(jdkFeatureEvents); + + return [ + ...this.createEventsFromMap(previewEnabledMap, true), + ...this.createEventsFromMap(previewDisabledMap, false) + ]; + } + + private static createEventsFromMap( + map: Map, + isPreviewEnabled: boolean + ): JdkFeatureEvent[] { + return Array.from(map.entries()).map(([javaVersion, events]) => { + const jeps: number[] = []; + const names: string[] = []; + + events.forEach(event => { + jeps.push(...event.getPayload.jeps); + names.push(...event.getPayload.names); + }); + + return new JdkFeatureEvent({ + jeps, + names, + javaVersion, + isPreviewEnabled + }); + }); + } + + private static groupEvents(jdkFeatureEvents: JdkFeatureEvent[]) { + return jdkFeatureEvents.reduce((acc, event) => { + const { isPreviewEnabled, javaVersion } = event.getPayload; + const targetMap = isPreviewEnabled ? acc.previewEnabledMap : acc.previewDisabledMap; + + if (!targetMap.has(javaVersion)) { + targetMap.set(javaVersion, []); + } + targetMap.get(javaVersion)!.push(event); + + return acc; + }, { + previewEnabledMap: new Map(), + previewDisabledMap: new Map() + }); + } +} \ No newline at end of file diff --git a/vscode/src/telemetry/events/start.ts b/vscode/src/telemetry/events/start.ts new file mode 100644 index 0000000..8bb4f1c --- /dev/null +++ b/vscode/src/telemetry/events/start.ts @@ -0,0 +1,80 @@ +/* + Copyright (c) 2024, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +import { globalState } from "../../globalState"; +import { LOGGER } from "../../logger"; +import { cacheService } from "../impl/cacheServiceImpl"; +import { getEnvironmentInfo } from "../impl/enviromentDetails"; +import { getHashCode } from "../utils"; +import { BaseEvent } from "./baseEvent"; + +interface ExtensionInfo { + id: string; + name: string; + version: string; +} + +interface VscodeInfo { + version: string; + hostType: string; + locale: string; +} + +interface PlatformInfo { + os: string; + arch: string; + osVersion: string; +} + +interface LocationInfo { + timeZone: string; + locale: string; +} + +export interface StartEventData { + extension: ExtensionInfo; + vsCode: VscodeInfo; + platform: PlatformInfo; + location: LocationInfo; +} + +export class ExtensionStartEvent extends BaseEvent { + public static readonly NAME = "startup"; + public static readonly ENDPOINT = "/start"; + + constructor(payload: StartEventData) { + super(ExtensionStartEvent.NAME, ExtensionStartEvent.ENDPOINT, payload); + } + + onSuccessPostEventCallback = async (): Promise => { + this.addEventToCache(); + } + + public static builder = (): ExtensionStartEvent | null => { + const startEventData = getEnvironmentInfo(globalState.getExtensionContextInfo()); + const cachedValue: string | undefined = cacheService.get(this.NAME); + const envString = JSON.stringify(startEventData); + const newValue = getHashCode(envString); + + if (cachedValue != newValue) { + const startEvent: ExtensionStartEvent = new ExtensionStartEvent(startEventData); + return startEvent; + } + + LOGGER.debug(`No change in start event`); + + return null; + } +} diff --git a/vscode/src/telemetry/events/workspaceChange.ts b/vscode/src/telemetry/events/workspaceChange.ts new file mode 100644 index 0000000..2bf6214 --- /dev/null +++ b/vscode/src/telemetry/events/workspaceChange.ts @@ -0,0 +1,40 @@ +/* + Copyright (c) 2024, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +import { BaseEvent } from "./baseEvent"; + +interface ProjectInfo { + id: string; + buildTool: string; + javaVersion: string; + isOpenedWithProblems: boolean; + isPreviewEnabled: boolean; +} + +export interface WorkspaceChangeData { + projectInfo: ProjectInfo[]; + numProjects: number; + lspInitTimeTaken: number; + projInitTimeTaken: number; +} + +export class WorkspaceChangeEvent extends BaseEvent { + public static readonly NAME = "workspaceChange"; + public static readonly ENDPOINT = "/workspaceChange"; + + constructor(payload: WorkspaceChangeData) { + super(WorkspaceChangeEvent.NAME, WorkspaceChangeEvent.ENDPOINT, payload); + } +} \ No newline at end of file diff --git a/vscode/src/telemetry/impl/AnonymousIdManager.ts b/vscode/src/telemetry/impl/AnonymousIdManager.ts new file mode 100644 index 0000000..d1186d6 --- /dev/null +++ b/vscode/src/telemetry/impl/AnonymousIdManager.ts @@ -0,0 +1,21 @@ +/* + Copyright (c) 2024, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +import { env } from "vscode"; + +export class AnonymousIdManager { + public static readonly machineId: string = env.machineId; + public static readonly sessionId: string = env.sessionId; +} \ No newline at end of file diff --git a/vscode/src/telemetry/impl/cacheServiceImpl.ts b/vscode/src/telemetry/impl/cacheServiceImpl.ts new file mode 100644 index 0000000..e7aaf6a --- /dev/null +++ b/vscode/src/telemetry/impl/cacheServiceImpl.ts @@ -0,0 +1,43 @@ +/* + Copyright (c) 2024, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +import { CacheService } from "../types"; +import { LOGGER } from "../../logger"; +import { globalState } from "../../globalState"; + +class CacheServiceImpl implements CacheService { + public get = (key: string): string | undefined => { + try { + const vscGlobalState = globalState.getExtensionContextInfo().getVscGlobalState(); + return vscGlobalState.get(key); + } catch (err) { + LOGGER.error(`Error while retrieving ${key} from cache: ${(err as Error).message}`); + return undefined; + } + } + + public put = (key: string, value: string): boolean => { + try { + const vscGlobalState = globalState.getExtensionContextInfo().getVscGlobalState(); + vscGlobalState.update(key, value); + return true; + } catch (err) { + LOGGER.error(`Error while storing ${key} in cache: ${(err as Error).message}`); + return false; + } + } +} + +export const cacheService = new CacheServiceImpl(); \ No newline at end of file diff --git a/vscode/src/telemetry/impl/enviromentDetails.ts b/vscode/src/telemetry/impl/enviromentDetails.ts new file mode 100644 index 0000000..2b6d427 --- /dev/null +++ b/vscode/src/telemetry/impl/enviromentDetails.ts @@ -0,0 +1,76 @@ +/* + Copyright (c) 2024, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +import * as os from 'os'; +import { env as vscodeEnv, version } from 'vscode'; +import { ExtensionContextInfo } from '../../extensionContextInfo'; +import { StartEventData } from '../events/start'; + +const getPlatform = (): string => { + const platform: string = os.platform(); + if (platform.startsWith('darwin')) { + return 'Mac'; + } + if (platform.startsWith('win')) { + return 'Windows'; + } + return platform.charAt(0).toUpperCase() + platform.slice(1); +} + +const getArchType = (): string => { + return os.arch(); +} + +const getPlatformVersion = (): string => { + return os.release(); +} + +const getTimeZone = (): string => { + return Intl.DateTimeFormat().resolvedOptions().timeZone; +} + +const getLocale = (): string => { + return Intl.DateTimeFormat().resolvedOptions().locale; +} + +export const PLATFORM = getPlatform(); +export const ARCH_TYPE = getArchType(); +export const PLATFORM_VERSION = getPlatformVersion(); +export const TIMEZONE = getTimeZone(); +export const LOCALE = getLocale(); + +export const getEnvironmentInfo = (contextInfo: ExtensionContextInfo): StartEventData => { + return { + extension: { + id: contextInfo.getExtensionId(), + name: contextInfo.getPackageJson().name, + version: contextInfo.getPackageJson().version + }, + vsCode: { + version: version, + hostType: vscodeEnv.appHost, + locale: vscodeEnv.language, + }, + platform: { + os: PLATFORM, + arch: ARCH_TYPE, + osVersion: PLATFORM_VERSION, + }, + location: { + timeZone: TIMEZONE, + locale: LOCALE, + } + }; +} \ No newline at end of file diff --git a/vscode/src/telemetry/impl/postTelemetry.ts b/vscode/src/telemetry/impl/postTelemetry.ts new file mode 100644 index 0000000..32ce3f8 --- /dev/null +++ b/vscode/src/telemetry/impl/postTelemetry.ts @@ -0,0 +1,86 @@ +/* + Copyright (c) 2024, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +import { LOGGER } from "../../logger"; +import { TELEMETRY_API } from "../config"; +import { BaseEvent } from "../events/baseEvent"; + +interface TelemetryEventResponse { + statusCode: number; + event: BaseEvent; +}; + +export interface TelemetryPostResponse { + success: TelemetryEventResponse[]; + failures: TelemetryEventResponse[]; +}; + +export class PostTelemetry { + public post = async (events: BaseEvent[]): Promise => { + try { + if (TELEMETRY_API.baseUrl == null) { + return { + success: [], + failures: [] + } + } + LOGGER.debug("Posting telemetry..."); + const results = await Promise.allSettled(events.map(event => this.postEvent(event))); + + return this.parseTelemetryResponse(events, results); + } catch (err) { + LOGGER.debug(`Error occurred while posting telemetry : ${(err as Error)?.message}`); + throw err; + } + }; + + private addBaseEndpoint = (endpoint: string) => { + return `${TELEMETRY_API.baseUrl}${TELEMETRY_API.baseEndpoint}${TELEMETRY_API.version}${endpoint}`; + } + + private postEvent = (event: BaseEvent): Promise => { + const { ENDPOINT, getPayload: payload } = event; + + const serverEndpoint = this.addBaseEndpoint(ENDPOINT); + + return fetch(serverEndpoint, { + method: "POST", + body: JSON.stringify(payload) + }); + } + + private parseTelemetryResponse = (events: BaseEvent[], eventResponses: PromiseSettledResult[]): TelemetryPostResponse => { + let success: TelemetryEventResponse[] = [], failures: TelemetryEventResponse[] = []; + eventResponses.forEach((eventResponse, index) => { + const event = events[index]; + if (eventResponse.status === "rejected") { + failures.push({ + event, + statusCode: -1 + }); + } else { + success.push({ + statusCode: eventResponse.value.status, + event + }); + } + }); + + return { + success, + failures + }; + } +} \ No newline at end of file diff --git a/vscode/src/telemetry/impl/telemetryEventQueue.ts b/vscode/src/telemetry/impl/telemetryEventQueue.ts new file mode 100644 index 0000000..af5fd7d --- /dev/null +++ b/vscode/src/telemetry/impl/telemetryEventQueue.ts @@ -0,0 +1,38 @@ +/* + Copyright (c) 2024, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +import { BaseEvent } from "../events/baseEvent"; + +export class TelemetryEventQueue { + private events: BaseEvent[] = []; + + public enqueue = (e: BaseEvent): void => { + this.events.push(e); + } + + public dequeue = (): BaseEvent | undefined => this.events.shift(); + + public concatQueue = (queue: BaseEvent[], mergeAtStarting = false): void => { + this.events = mergeAtStarting ? [...queue, ...this.events] : [...this.events, ...queue]; + } + + public size = (): number => this.events.length; + + public flush = (): BaseEvent[] => { + const queue = [...this.events]; + this.events = []; + return queue; + } +} \ No newline at end of file diff --git a/vscode/src/telemetry/impl/telemetryPrefs.ts b/vscode/src/telemetry/impl/telemetryPrefs.ts new file mode 100644 index 0000000..c7213f0 --- /dev/null +++ b/vscode/src/telemetry/impl/telemetryPrefs.ts @@ -0,0 +1,64 @@ +/* + Copyright (c) 2024, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +import { ConfigurationChangeEvent, env, workspace } from "vscode"; +import { getConfigurationValue, inspectConfiguration, updateConfigurationValue } from "../../configurations/handlers"; +import { configKeys } from "../../configurations/configuration"; +import { appendPrefixToCommand } from "../../utils"; + +export class TelemetryPrefs { + public isExtTelemetryEnabled: boolean; + + constructor() { + this.isExtTelemetryEnabled = this.checkTelemetryStatus(); + } + + private checkTelemetryStatus = (): boolean => { + return getConfigurationValue(configKeys.telemetryEnabled, false); + } + + private configPref = (configCommand: string): boolean => { + const config = inspectConfiguration(configCommand); + return ( + config?.workspaceFolderValue !== undefined || + config?.workspaceFolderLanguageValue !== undefined || + config?.workspaceValue !== undefined || + config?.workspaceLanguageValue !== undefined || + config?.globalValue !== undefined || + config?.globalLanguageValue !== undefined + ); + } + + public isExtTelemetryConfigured = (): boolean => { + return this.configPref(appendPrefixToCommand(configKeys.telemetryEnabled)); + } + + public updateTelemetryEnabledConfig = (value: boolean): void => { + this.isExtTelemetryEnabled = value; + updateConfigurationValue(configKeys.telemetryEnabled, value, true); + } + + public didUserDisableVscodeTelemetry = (): boolean => { + return !env.isTelemetryEnabled; + } + + public onDidChangeTelemetryEnabled = () => workspace.onDidChangeConfiguration( + (e: ConfigurationChangeEvent) => { + if (e.affectsConfiguration(appendPrefixToCommand(configKeys.telemetryEnabled))) { + this.isExtTelemetryEnabled = this.checkTelemetryStatus(); + } + } + ); +} \ No newline at end of file diff --git a/vscode/src/telemetry/impl/telemetryReporterImpl.ts b/vscode/src/telemetry/impl/telemetryReporterImpl.ts new file mode 100644 index 0000000..ac78297 --- /dev/null +++ b/vscode/src/telemetry/impl/telemetryReporterImpl.ts @@ -0,0 +1,108 @@ +/* + Copyright (c) 2024, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +import { getCurrentUTCDateInSeconds } from "../utils"; +import { TelemetryEventQueue } from "./telemetryEventQueue"; +import { TelemetryReporter } from "../types"; +import { LOGGER } from "../../logger"; +import { isError } from "../../utils"; +import { BaseEvent } from "../events/baseEvent"; +import { ExtensionCloseEvent } from "../events/close"; +import { ExtensionStartEvent } from "../events/start"; +import { TelemetryRetry } from "./telemetryRetry"; +import { JdkFeatureEvent } from "../events/jdkFeature"; +import { PostTelemetry, TelemetryPostResponse } from "./postTelemetry"; + +export class TelemetryReporterImpl implements TelemetryReporter { + private activationTime: number = getCurrentUTCDateInSeconds(); + private disableReporter: boolean = false; + private postTelemetry: PostTelemetry = new PostTelemetry(); + + constructor( + private queue: TelemetryEventQueue, + private retryManager: TelemetryRetry, + ) { + this.retryManager.registerCallbackHandler(this.sendEvents); + } + + public startEvent = (): void => { + const extensionStartEvent = ExtensionStartEvent.builder(); + if(extensionStartEvent != null){ + this.addEventToQueue(extensionStartEvent); + LOGGER.debug(`Start event enqueued: ${extensionStartEvent.getPayload}`); + } + } + + public closeEvent = (): void => { + + const extensionCloseEvent = ExtensionCloseEvent.builder(this.activationTime); + this.addEventToQueue(extensionCloseEvent); + + LOGGER.debug(`Close event enqueued: ${extensionCloseEvent.getPayload}`); + this.sendEvents(); + } + + public addEventToQueue = (event: BaseEvent): void => { + if (!this.disableReporter) { + this.queue.enqueue(event); + if (this.retryManager.isQueueOverflow(this.queue.size())) { + LOGGER.debug(`Send triggered to queue size overflow`); + this.sendEvents(); + } + } + } + + private sendEvents = async (): Promise => { + try { + if(!this.queue.size()){ + LOGGER.debug(`Queue is empty nothing to send`); + return; + } + const eventsCollected = this.queue.flush(); + + LOGGER.debug(`Number of events to send: ${eventsCollected.length}`); + this.retryManager.clearTimer(); + + const transformedEvents = this.transformEvents(eventsCollected); + + const response = await this.postTelemetry.post(transformedEvents); + + LOGGER.debug(`Number of events successfully sent: ${response.success.length}`); + LOGGER.debug(`Number of events failed to send: ${response.failures.length}`); + this.handlePostTelemetryResponse(response); + + this.retryManager.startTimer(); + } catch (err: any) { + this.disableReporter = true; + LOGGER.debug(`Error while sending telemetry: ${isError(err) ? err.message : err}`); + } + } + + private transformEvents = (events: BaseEvent[]): BaseEvent[] => { + const jdkFeatureEvents = events.filter(event => event.NAME === JdkFeatureEvent.NAME); + const concatedEvents = JdkFeatureEvent.concatEvents(jdkFeatureEvents); + const removedJdkFeatureEvents = events.filter(event => event.NAME !== JdkFeatureEvent.NAME); + + return [...removedJdkFeatureEvents, ...concatedEvents]; + } + + private handlePostTelemetryResponse = (response: TelemetryPostResponse) => { + const eventsToBeEnqueued = this.retryManager.eventsToBeEnqueuedAgain(response); + + this.queue.concatQueue(eventsToBeEnqueued); + + LOGGER.debug(`Number of failed events enqueuing again: ${eventsToBeEnqueued.length}`); + } +} \ No newline at end of file diff --git a/vscode/src/telemetry/impl/telemetryRetry.ts b/vscode/src/telemetry/impl/telemetryRetry.ts new file mode 100644 index 0000000..12a70e4 --- /dev/null +++ b/vscode/src/telemetry/impl/telemetryRetry.ts @@ -0,0 +1,133 @@ +/* + Copyright (c) 2024, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { LOGGER } from "../../logger"; +import { TELEMETRY_RETRY_CONFIG } from "../config"; +import { BaseEvent } from "../events/baseEvent"; +import { TelemetryPostResponse } from "./postTelemetry"; + +export class TelemetryRetry { + private timePeriod: number = TELEMETRY_RETRY_CONFIG.baseTimer; + private timeout?: NodeJS.Timeout | null; + private numOfAttemptsWhenTimerHits: number = 1; + private queueCapacity: number = TELEMETRY_RETRY_CONFIG.baseCapacity; + private numOfAttemptsWhenQueueIsFull: number = 1; + private triggeredDueToQueueOverflow: boolean = false; + private callbackHandler?: () => {}; + + public registerCallbackHandler = (callbackHandler: () => {}): void => { + this.callbackHandler = callbackHandler; + } + + public startTimer = (): void => { + if (!this.callbackHandler) { + LOGGER.debug("Callback handler is not set for telemetry retry mechanism"); + return; + } + if (this.timeout) { + LOGGER.debug("Overriding current timeout"); + } + this.timeout = setInterval(this.callbackHandler, this.timePeriod); + } + + private resetTimerParameters = () => { + this.numOfAttemptsWhenTimerHits = 1; + this.timePeriod = TELEMETRY_RETRY_CONFIG.baseTimer; + this.clearTimer(); + } + + private increaseTimePeriod = (): void => { + if (this.numOfAttemptsWhenTimerHits <= TELEMETRY_RETRY_CONFIG.maxRetries) { + this.timePeriod = this.calculateDelay(); + this.numOfAttemptsWhenTimerHits++; + return; + } + throw new Error("Number of retries exceeded"); + } + + public clearTimer = (): void => { + if (this.timeout) { + clearInterval(this.timeout); + this.timeout = null; + } + } + + private calculateDelay = (): number => { + const baseDelay = TELEMETRY_RETRY_CONFIG.baseTimer * + Math.pow(TELEMETRY_RETRY_CONFIG.backoffFactor, this.numOfAttemptsWhenTimerHits); + + const cappedDelay = Math.min(baseDelay, TELEMETRY_RETRY_CONFIG.maxDelayMs); + + const jitterMultiplier = 1 + (Math.random() * 2 - 1) * TELEMETRY_RETRY_CONFIG.jitterFactor; + + return Math.floor(cappedDelay * jitterMultiplier); + }; + + private increaseQueueCapacity = (): void => { + if (this.numOfAttemptsWhenQueueIsFull < TELEMETRY_RETRY_CONFIG.maxRetries) { + this.queueCapacity = TELEMETRY_RETRY_CONFIG.baseCapacity * + Math.pow(TELEMETRY_RETRY_CONFIG.backoffFactor, this.numOfAttemptsWhenQueueIsFull); + } + throw new Error("Number of retries exceeded"); + } + + private resetQueueCapacity = (): void => { + this.queueCapacity = TELEMETRY_RETRY_CONFIG.baseCapacity; + this.numOfAttemptsWhenQueueIsFull = 1; + this.triggeredDueToQueueOverflow = false; + } + + public isQueueOverflow = (queueSize: number): boolean => { + if (queueSize >= this.queueCapacity) { + this.triggeredDueToQueueOverflow = true; + return true; + } + return false; + } + + public eventsToBeEnqueuedAgain = (eventResponses: TelemetryPostResponse): BaseEvent[] => { + eventResponses.success.forEach(res => { + res.event.onSuccessPostEventCallback(); + }); + + if (eventResponses.failures.length === 0) { + this.resetQueueCapacity(); + this.resetTimerParameters(); + } else { + const eventsToBeEnqueuedAgain: BaseEvent[] = []; + eventResponses.failures.forEach((eventRes) => { + if (eventRes.statusCode <= 0 || eventRes.statusCode > 500) + eventsToBeEnqueuedAgain.push(eventRes.event); + }); + + if (eventsToBeEnqueuedAgain.length) { + this.triggeredDueToQueueOverflow ? + this.increaseQueueCapacity() : + this.increaseTimePeriod(); + LOGGER.debug(`Queue max capacity size: ${this.queueCapacity}`); + LOGGER.debug(`Timer period: ${this.timePeriod}`); + } else { + eventResponses.failures.forEach(res => { + res.event.onFailPostEventCallback(); + }); + } + + return eventsToBeEnqueuedAgain; + } + + return []; + } +} \ No newline at end of file diff --git a/vscode/src/telemetry/telemetry.ts b/vscode/src/telemetry/telemetry.ts new file mode 100644 index 0000000..3f60cf4 --- /dev/null +++ b/vscode/src/telemetry/telemetry.ts @@ -0,0 +1,62 @@ +/* + Copyright (c) 2024, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +import { TelemetryManager } from "./telemetryManager"; +import { ExtensionContextInfo } from "../extensionContextInfo"; +import { LOGGER } from "../logger"; +import { BaseEvent } from "./events/baseEvent"; +import { TelemetryReporter } from "./types"; +import { TELEMETRY_API } from "./config"; + +export namespace Telemetry { + + let telemetryManager: TelemetryManager; + + export const isTelemetryFeatureAvailable = TELEMETRY_API.baseUrl != null && TELEMETRY_API.baseUrl.trim().length; + + export const initializeTelemetry = (contextInfo: ExtensionContextInfo): TelemetryManager => { + if (!!telemetryManager) { + LOGGER.warn("Telemetry is already initialized"); + return telemetryManager; + } + telemetryManager = new TelemetryManager(contextInfo); + if (isTelemetryFeatureAvailable) { + telemetryManager.initializeReporter(); + } + + return telemetryManager; + } + + const enqueueEvent = (cbFunction: (reporter: TelemetryReporter) => void) => { + if (telemetryManager.isExtTelemetryEnabled() && isTelemetryFeatureAvailable) { + const reporter = telemetryManager.getReporter(); + if (reporter) { + cbFunction(reporter); + } + } + } + + export const sendTelemetry = (event: BaseEvent): void => { + enqueueEvent((reporter) => reporter.addEventToQueue(event)); + } + + export const enqueueStartEvent = (): void => { + enqueueEvent((reporter) => reporter.startEvent()); + } + + export const enqueueCloseEvent = (): void => { + enqueueEvent((reporter) => reporter.closeEvent()); + } +} diff --git a/vscode/src/telemetry/telemetryManager.ts b/vscode/src/telemetry/telemetryManager.ts new file mode 100644 index 0000000..ce4e5ea --- /dev/null +++ b/vscode/src/telemetry/telemetryManager.ts @@ -0,0 +1,74 @@ +/* + Copyright (c) 2024, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +import { window } from "vscode"; +import { TelemetryPrefs } from "./impl/telemetryPrefs"; +import { TelemetryEventQueue } from "./impl/telemetryEventQueue"; +import { TelemetryReporterImpl } from "./impl/telemetryReporterImpl"; +import { TelemetryReporter } from "./types"; +import { LOGGER } from "../logger"; +import { ExtensionContextInfo } from "../extensionContextInfo"; +import { l10n } from "../localiser"; +import { TelemetryRetry } from "./impl/telemetryRetry"; + +export class TelemetryManager { + private extensionContextInfo: ExtensionContextInfo; + private settings: TelemetryPrefs = new TelemetryPrefs(); + private reporter?: TelemetryReporter; + private telemetryRetryManager: TelemetryRetry = new TelemetryRetry() + + constructor(extensionContextInfo: ExtensionContextInfo) { + this.extensionContextInfo = extensionContextInfo; + } + + public isExtTelemetryEnabled = (): boolean => { + return this.settings.isExtTelemetryEnabled; + } + + public initializeReporter = (): void => { + const queue = new TelemetryEventQueue(); + this.extensionContextInfo.pushSubscription(this.settings.onDidChangeTelemetryEnabled()); + this.reporter = new TelemetryReporterImpl(queue, this.telemetryRetryManager); + + this.openTelemetryDialog(); + } + + public getReporter = (): TelemetryReporter | null => { + return this.reporter || null; + } + + private openTelemetryDialog = async () => { + if (!this.settings.isExtTelemetryConfigured() && !this.settings.didUserDisableVscodeTelemetry()) { + LOGGER.log('Telemetry not enabled yet'); + + const yesLabel = l10n.value("jdk.downloader.message.confirmation.yes"); + const noLabel = l10n.value("jdk.downloader.message.confirmation.no"); + const telemetryLabel = l10n.value("jdk.telemetry.consent", { extensionName: this.extensionContextInfo.getPackageJson().name }); + + const enable = await window.showInformationMessage(telemetryLabel, yesLabel, noLabel); + if (enable == undefined) { + return; + } + + this.settings.updateTelemetryEnabledConfig(enable === yesLabel); + if (enable === yesLabel) { + LOGGER.log("Telemetry is now enabled"); + } + } + if (this.settings.isExtTelemetryEnabled) { + this.telemetryRetryManager.startTimer(); + } + } +}; \ No newline at end of file diff --git a/vscode/src/telemetry/types.ts b/vscode/src/telemetry/types.ts new file mode 100644 index 0000000..30a663f --- /dev/null +++ b/vscode/src/telemetry/types.ts @@ -0,0 +1,51 @@ +/* + Copyright (c) 2024, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +import { BaseEvent } from "./events/baseEvent"; + +export interface TelemetryReporter { + startEvent(): void; + + addEventToQueue(event: BaseEvent): void; + + closeEvent(): void; +} + +export interface CacheService { + get(key: string): string | undefined; + + put(key: string, value: string): boolean; +} + +export interface TelemetryEventQueue { + enqueue(e: BaseEvent): void; + + flush(): BaseEvent[]; +} + +export interface RetryConfig { + maxRetries: number; + baseTimer: number; + baseCapacity: number; + maxDelayMs: number; + backoffFactor: number; + jitterFactor: number; +} + +export interface TelemetryApi { + baseUrl: string | null; + baseEndpoint: string; + version: string; +} \ No newline at end of file diff --git a/vscode/src/telemetry/utils.ts b/vscode/src/telemetry/utils.ts new file mode 100644 index 0000000..7b69256 --- /dev/null +++ b/vscode/src/telemetry/utils.ts @@ -0,0 +1,72 @@ +/* + Copyright (c) 2024, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +import * as crypto from 'crypto'; +import { Uri, workspace } from 'vscode'; + +export const getCurrentUTCDateInSeconds = () => { + const date = Date.now(); + return Math.floor(date / 1000); +} + +export const getOriginalDateFromSeconds = (seconds: number) => { + return new Date(seconds * 1000); +} + +export const exists = async (pathOrUri: Uri | string): Promise => { + const uri = getUri(pathOrUri); + try { + await workspace.fs.stat(uri); + return true; + } catch (e) { + return false; + } +} + +export const writeFile = async (pathOrUri: Uri | string, content: string): Promise => { + const uri = getUri(pathOrUri); + const parent = Uri.joinPath(uri, ".."); + if (!(await exists(parent))) { + await mkdir(parent); + } + const res: Uint8Array = new TextEncoder().encode(content); + return workspace.fs.writeFile(uri, res); +} + +export const readFile = async (pathOrUri: Uri | string): Promise => { + const uri = getUri(pathOrUri); + if (!(await exists(uri))) { + return undefined; + } + const read = await workspace.fs.readFile(uri); + return new TextDecoder().decode(read); +} + +export const mkdir = async (pathOrUri: Uri | string): Promise => { + const uri = getUri(pathOrUri); + await workspace.fs.createDirectory(uri); +} + +export const getHashCode = (value: string, algorithm: string = 'sha256') => { + const hash: string = crypto.createHash(algorithm).update(value).digest('hex'); + return hash; +} + +const getUri = (pathOrUri: Uri | string): Uri => { + if (pathOrUri instanceof Uri) { + return pathOrUri; + } + return Uri.file(pathOrUri); +} diff --git a/vscode/src/test/unit/mocks/vscode/mockVscode.ts b/vscode/src/test/unit/mocks/vscode/mockVscode.ts index acb1305..0fe128a 100644 --- a/vscode/src/test/unit/mocks/vscode/mockVscode.ts +++ b/vscode/src/test/unit/mocks/vscode/mockVscode.ts @@ -17,6 +17,7 @@ import * as vscode from 'vscode'; import { URI } from './uri'; import { mockWindowNamespace } from './namespaces/window'; +import { mockEnvNamespace } from './namespaces/env'; import { mockedEnums } from './vscodeHostedTypes'; type VSCode = typeof vscode; @@ -29,6 +30,7 @@ const mockedVscodeClassesAndTypes = () => { const mockNamespaces = () => { mockWindowNamespace(mockedVSCode); + mockEnvNamespace(mockedVSCode); } export const initMockedVSCode = () => { diff --git a/vscode/src/test/unit/mocks/vscode/namespaces/env.ts b/vscode/src/test/unit/mocks/vscode/namespaces/env.ts new file mode 100644 index 0000000..3546b36 --- /dev/null +++ b/vscode/src/test/unit/mocks/vscode/namespaces/env.ts @@ -0,0 +1,32 @@ +/* + Copyright (c) 2024-2025, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import * as vscode from 'vscode'; +import { mock, when, anyString, anyOfClass, anything, instance } from "ts-mockito"; + +type VSCode = typeof vscode; + +let mockedEnv: typeof vscode.env; +export const mockEnvNamespace = (mockedVSCode: Partial) => { + mockedEnv = mock(); + mockedVSCode.env = instance(mockedEnv); + mockTelemetryFields(); +} + +const mockTelemetryFields = () => { + when(mockedEnv.machineId).thenReturn("00mocked-xVSx-Code-0000-machineIdxxx"); + when(mockedEnv.sessionId).thenReturn("00mocked-xVSx-Code-0000-sessionIdxxx"); +} \ No newline at end of file diff --git a/vscode/src/views/projects.ts b/vscode/src/views/projects.ts index 5f48453..c69e7cd 100644 --- a/vscode/src/views/projects.ts +++ b/vscode/src/views/projects.ts @@ -23,6 +23,7 @@ import { NbLanguageClient } from '../lsp/nbLanguageClient'; import { NodeChangedParams, NodeInfoNotification, NodeInfoRequest, GetResourceParams, NodeChangeType, NodeChangesParams } from '../lsp/protocol'; import { l10n } from '../localiser'; import { extCommands } from '../commands/commands'; +import { globalState } from '../globalState'; const doLog: boolean = false; const EmptyIcon = "EMPTY_ICON"; @@ -913,6 +914,7 @@ export function createTreeViewService(log: vscode.OutputChannel, c: NbLanguageCl })); } }); + globalState.getExtensionContextInfo().pushSubscription(d); const ts: TreeViewService = new TreeViewService(log, c, [d]); return ts; } diff --git a/vscode/src/webviews/jdkDownloader/action.ts b/vscode/src/webviews/jdkDownloader/action.ts index 75140da..f664a9f 100644 --- a/vscode/src/webviews/jdkDownloader/action.ts +++ b/vscode/src/webviews/jdkDownloader/action.ts @@ -14,7 +14,7 @@ limitations under the License. */ -import { commands, OpenDialogOptions, window, workspace } from "vscode"; +import { commands, OpenDialogOptions, window } from "vscode"; import { JdkDownloaderView } from "./view"; import { jdkDownloaderConstants } from "../../constants"; import * as path from 'path'; @@ -26,11 +26,15 @@ import { l10n } from "../../localiser"; import { LOGGER } from "../../logger"; import { updateConfigurationValue } from "../../configurations/handlers"; import { configKeys } from "../../configurations/configuration"; +import { Telemetry } from "../../telemetry/telemetry"; +import { JdkDownloadEvent, JdkDownloadEventData } from "../../telemetry/events/jdkDownload"; +import { getCurrentUTCDateInSeconds } from "../../telemetry/utils"; export class JdkDownloaderAction { public static readonly MANUAL_INSTALLATION_TYPE = "manual"; public static readonly AUTO_INSTALLATION_TYPE = "automatic"; private readonly DOWNLOAD_DIR = path.join(__dirname, 'jdk_downloads'); + private startTimer: number | null = null; private jdkType?: string; private jdkVersion?: string; @@ -98,11 +102,14 @@ export class JdkDownloaderAction { LOGGER.log(`manual JDK installation completed successfully`); return; } + this.startTimer = getCurrentUTCDateInSeconds(); await this.jdkInstallationManager(); } catch (err: any) { window.showErrorMessage(l10n.value("jdk.downloader.error_message.installingJDK", { error: err })); LOGGER.error(err?.message || "No Error message received"); + } finally { + this.startTimer = null; } } @@ -269,6 +276,18 @@ export class JdkDownloaderAction { } private installationCleanup = (tempDirPath: string, newDirPath: string) => { + const currentTime = getCurrentUTCDateInSeconds(); + const downloadTelemetryEvent: JdkDownloadEventData = { + vendor: this.jdkType!, + version: this.jdkVersion!, + os: this.osType!, + arch: this.machineArch!, + timeTaken: Math.min(currentTime - this.startTimer!) + }; + + const event: JdkDownloadEvent = new JdkDownloadEvent(downloadTelemetryEvent); + Telemetry.sendTelemetry(event); + fs.unlink(this.downloadFilePath!, async (err) => { if (err) { LOGGER.error(`Error while installation cleanup: ${err.message}`); From 61e389ec90920617a29a9b2406034621cc31330e Mon Sep 17 00:00:00 2001 From: Achal Talati Date: Fri, 27 Dec 2024 21:40:48 +0530 Subject: [PATCH 02/16] 1) Updated debug log env variable 2) Added build time config for telemetry 3) Fixed status code around bug 4) added important headers in the post request of telemetry events --- .gitignore | 1 + vscode/.vscode/launch.json | 2 +- vscode/esbuild.js | 112 ++++++++++++++------ vscode/package.json | 2 +- vscode/src/logger.ts | 2 +- vscode/src/lsp/nbLanguageClient.ts | 2 +- vscode/src/telemetry/config.ts | 70 +++++++++--- vscode/src/telemetry/impl/postTelemetry.ts | 36 ++++--- vscode/src/telemetry/impl/telemetryRetry.ts | 27 ++--- vscode/src/telemetry/telemetry.ts | 15 +-- 10 files changed, 187 insertions(+), 82 deletions(-) diff --git a/.gitignore b/.gitignore index 643416e..109efbe 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ build/ /nbcode/l10n/locale_zh_CN/release/ /nbcode/l10n/locale_ja/nbproject/private/ /nbcode/l10n/locale_zh_CN/nbproject/private/ +telemetryConfig.json \ No newline at end of file diff --git a/vscode/.vscode/launch.json b/vscode/.vscode/launch.json index 14ef7a1..45bf9d5 100644 --- a/vscode/.vscode/launch.json +++ b/vscode/.vscode/launch.json @@ -34,7 +34,7 @@ "preLaunchTask": "${defaultBuildTask}", "env": { "nbcode_userdir": "global", - "oracle.oracle-java.enable.debug-logs": "true" + "oracle_oracleJava_enable_debugLogs": "true" } }, { diff --git a/vscode/esbuild.js b/vscode/esbuild.js index dbcdf5d..80873aa 100644 --- a/vscode/esbuild.js +++ b/vscode/esbuild.js @@ -15,19 +15,19 @@ const scriptConfig = { }; const watchConfig = { - watch: { - onRebuild(error, result) { - console.log("[watch] build started"); - if (error) { - error.errors.forEach(error => - console.error(`> ${error.location.file}:${error.location.line}:${error.location.column}: error: ${error.text}`) - ); - } else { - console.log("[watch] build finished"); - } - }, + watch: { + onRebuild(error, result) { + console.log("[watch] build started"); + if (error) { + error.errors.forEach(error => + console.error(`> ${error.location.file}:${error.location.line}:${error.location.column}: error: ${error.text}`) + ); + } else { + console.log("[watch] build finished"); + } }, - }; + }, +}; const NON_NPM_ARTIFACTORY = new RegExp( String.raw`"resolved"\s*:\s*"http[s]*://(?!registry.npmjs.org)[^"]+"`, @@ -43,26 +43,74 @@ const checkAritfactoryUrl = () => { } } - (async () => { - const args = process.argv.slice(2); - try { - if (args.includes("--watch")) { - // Build and watch source code - console.log("[watch] build started"); - await build({ - ...scriptConfig, - ...watchConfig, +const createTelemetryConfig = () => { + const defaultConfig = { + telemetryRetryConfig: { + maxRetries: 6, + baseCapacity: 256, + baseTimer: 5000, + maxDelayMs: 100000, + backoffFactor: 2, + jitterFactor: 0.25 + }, + telemetryApi: { + baseUrl: null, + baseEndpoint: "/vscode/java/sendTelemetry", + version: "/v1" + } + } + + const envConfig = Object.freeze({ + telemetryRetryConfig: { + maxRetries: process.env.TELEMETRY_MAX_RETRIES, + baseCapacity: process.env.TELEMETRY_BASE_CAPACITY, + baseTimer: process.env.TELEMETRY_BASE_TIMER, + maxDelayMs: process.env.TELEMETRY_MAX_DELAY, + backoffFactor: process.env.TELEMETRY_BACKOFF_FACTOR, + jitterFactor: process.env.TELEMETRY_JITTER_FACTOR + }, + telemetryApi: { + baseUrl: process.env.TELEMETRY_API_BASE_URL, + baseEndpoint: process.env.TELEMETRY_API_ENDPOINT, + version: process.env.TELEMETRY_API_VERSION + } + }); + + Object.entries(defaultConfig).forEach(([parent, configs]) => { + if (parent in envConfig) { + Object.entries(configs).forEach(([key, _]) => { + if (envConfig[parent]?.[key]) { + defaultConfig[parent][key] = envConfig[parent][key]; + } }); - console.log("[watch] build finished"); - } else if(args.includes("--artifactory-check")){ - checkAritfactoryUrl(); - } else { - // Build source code - await build(scriptConfig); - console.log("build complete"); } - } catch (err) { - process.stderr.write(err.message); - process.exit(1); + }); + + fs.writeFileSync("telemetryConfig.json", JSON.stringify(defaultConfig, null, 4)); + console.log("Telemetry config generated successfully."); +} + +(async () => { + const args = process.argv.slice(2); + try { + if (args.includes("--watch")) { + // Build and watch source code + console.log("[watch] build started"); + await build({ + ...scriptConfig, + ...watchConfig, + }); + console.log("[watch] build finished"); + } else if (args.includes("--artifactory-check")) { + checkAritfactoryUrl(); + } else { + // Build source code + createTelemetryConfig(); + await build(scriptConfig); + console.log("build complete"); } - })(); \ No newline at end of file + } catch (err) { + process.stderr.write(err.message); + process.exit(1); + } +})(); \ No newline at end of file diff --git a/vscode/package.json b/vscode/package.json index 22d1c15..c6647ad 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -804,4 +804,4 @@ "jsonc-parser": "3.3.1", "vscode-languageclient": "^9.0.1" } -} +} \ No newline at end of file diff --git a/vscode/src/logger.ts b/vscode/src/logger.ts index de0ca56..660956a 100644 --- a/vscode/src/logger.ts +++ b/vscode/src/logger.ts @@ -29,7 +29,7 @@ export class ExtensionLogger { constructor(channelName: string) { this.outChannel = window.createOutputChannel(channelName); - this.isDebugLogEnabled = process.env['oracle.oracle-java.enable.debug-logs'] === "true"; + this.isDebugLogEnabled = process.env['oracle_oracleJava_enable_debugLogs'] === "true"; } public log(message: string): void { diff --git a/vscode/src/lsp/nbLanguageClient.ts b/vscode/src/lsp/nbLanguageClient.ts index f472ab7..05efe6e 100644 --- a/vscode/src/lsp/nbLanguageClient.ts +++ b/vscode/src/lsp/nbLanguageClient.ts @@ -62,7 +62,7 @@ export class NbLanguageClient extends LanguageClient { 'showHtmlPageSupport': true, 'wantsJavaSupport': true, 'wantsGroovySupport': false, - 'wantsTelemetryEnabled': Telemetry.isTelemetryFeatureAvailable, + 'wantsTelemetryEnabled': Telemetry.getIsTelemetryFeatureAvailable(), 'commandPrefix': extConstants.COMMAND_PREFIX, 'configurationPrefix': `${extConstants.COMMAND_PREFIX}.`, 'altConfigurationPrefix': `${extConstants.COMMAND_PREFIX}.` diff --git a/vscode/src/telemetry/config.ts b/vscode/src/telemetry/config.ts index 1852099..b087bae 100644 --- a/vscode/src/telemetry/config.ts +++ b/vscode/src/telemetry/config.ts @@ -5,7 +5,7 @@ you may not use this file except in compliance with the License. You may obtain a copy of the License at - https://www.apache.org/licenses/LICENSE-2.0 + https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -14,18 +14,58 @@ limitations under the License. */ import { RetryConfig, TelemetryApi } from "./types"; +import * as path from 'path'; +import * as fs from 'fs'; +import { LOGGER } from "../logger"; -export const TELEMETRY_RETRY_CONFIG: RetryConfig = Object.freeze({ - maxRetries: 6, - baseCapacity: 256, - baseTimer: 5 * 1000, - maxDelayMs: 100 * 1000, - backoffFactor: 2, - jitterFactor: 0.25 -}); - -export const TELEMETRY_API: TelemetryApi = Object.freeze({ - baseUrl: null, - baseEndpoint: "/vscode/java/sendTelemetry", - version: "/v1" -}); \ No newline at end of file +export class TelemetryConfiguration { + private static CONFIG_FILE_PATH = path.resolve(__dirname, "..", "..", "telemetryConfig.json"); + + private static instance: TelemetryConfiguration; + private retryConfig!: RetryConfig; + private apiConfig!: TelemetryApi; + + public constructor() { + this.initialize(); + } + + public static getInstance(): TelemetryConfiguration { + if (!TelemetryConfiguration.instance) { + TelemetryConfiguration.instance = new TelemetryConfiguration(); + } + return TelemetryConfiguration.instance; + } + + private initialize(): void { + try { + const config = JSON.parse(fs.readFileSync(TelemetryConfiguration.CONFIG_FILE_PATH).toString()); + + this.retryConfig = Object.freeze({ + maxRetries: config.telemetryRetryConfig.maxRetries, + baseCapacity: config.telemetryRetryConfig.baseCapacity, + baseTimer: config.telemetryRetryConfig.baseTimer, + maxDelayMs: config.telemetryRetryConfig.maxDelayMs, + backoffFactor: config.telemetryRetryConfig.backoffFactor, + jitterFactor: config.telemetryRetryConfig.jitterFactor + }); + + this.apiConfig = Object.freeze({ + baseUrl: config.telemetryApi.baseUrl, + baseEndpoint: config.telemetryApi.baseEndpoint, + version: config.telemetryApi.version + }); + } catch (error: any) { + LOGGER.error("Error occurred while setting up telemetry config"); + LOGGER.error(error.message); + } + } + + public getRetryConfig(): Readonly { + return this.retryConfig; + } + + public getApiConfig(): Readonly { + return this.apiConfig; + } + +} \ No newline at end of file diff --git a/vscode/src/telemetry/impl/postTelemetry.ts b/vscode/src/telemetry/impl/postTelemetry.ts index 32ce3f8..1385a91 100644 --- a/vscode/src/telemetry/impl/postTelemetry.ts +++ b/vscode/src/telemetry/impl/postTelemetry.ts @@ -13,8 +13,9 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { integer } from "vscode-languageclient"; import { LOGGER } from "../../logger"; -import { TELEMETRY_API } from "../config"; +import { TelemetryConfiguration } from "../config"; import { BaseEvent } from "../events/baseEvent"; interface TelemetryEventResponse { @@ -28,9 +29,11 @@ export interface TelemetryPostResponse { }; export class PostTelemetry { + private TELEMETRY_API = TelemetryConfiguration.getInstance().getApiConfig(); + public post = async (events: BaseEvent[]): Promise => { try { - if (TELEMETRY_API.baseUrl == null) { + if (this.TELEMETRY_API.baseUrl == null) { return { success: [], failures: [] @@ -47,7 +50,7 @@ export class PostTelemetry { }; private addBaseEndpoint = (endpoint: string) => { - return `${TELEMETRY_API.baseUrl}${TELEMETRY_API.baseEndpoint}${TELEMETRY_API.version}${endpoint}`; + return `${this.TELEMETRY_API.baseUrl}${this.TELEMETRY_API.baseEndpoint}${this.TELEMETRY_API.version}${endpoint}`; } private postEvent = (event: BaseEvent): Promise => { @@ -57,7 +60,12 @@ export class PostTelemetry { return fetch(serverEndpoint, { method: "POST", - body: JSON.stringify(payload) + body: JSON.stringify(payload), + redirect: "follow", + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } }); } @@ -65,17 +73,21 @@ export class PostTelemetry { let success: TelemetryEventResponse[] = [], failures: TelemetryEventResponse[] = []; eventResponses.forEach((eventResponse, index) => { const event = events[index]; + let list: TelemetryEventResponse[] = success; + let statusCode: integer = 0; if (eventResponse.status === "rejected") { - failures.push({ - event, - statusCode: -1 - }); + list = failures; + statusCode = -1; } else { - success.push({ - statusCode: eventResponse.value.status, - event - }); + statusCode = eventResponse.value.status; + if (statusCode <= 0 || statusCode >= 400) { + list = failures; + } } + list.push({ + event, + statusCode + }); }); return { diff --git a/vscode/src/telemetry/impl/telemetryRetry.ts b/vscode/src/telemetry/impl/telemetryRetry.ts index 12a70e4..c8a29db 100644 --- a/vscode/src/telemetry/impl/telemetryRetry.ts +++ b/vscode/src/telemetry/impl/telemetryRetry.ts @@ -15,15 +15,16 @@ */ import { LOGGER } from "../../logger"; -import { TELEMETRY_RETRY_CONFIG } from "../config"; +import { TelemetryConfiguration } from "../config"; import { BaseEvent } from "../events/baseEvent"; import { TelemetryPostResponse } from "./postTelemetry"; export class TelemetryRetry { - private timePeriod: number = TELEMETRY_RETRY_CONFIG.baseTimer; + private TELEMETRY_RETRY_CONFIG = TelemetryConfiguration.getInstance().getRetryConfig(); + private timePeriod: number = this.TELEMETRY_RETRY_CONFIG?.baseTimer; private timeout?: NodeJS.Timeout | null; private numOfAttemptsWhenTimerHits: number = 1; - private queueCapacity: number = TELEMETRY_RETRY_CONFIG.baseCapacity; + private queueCapacity: number = this.TELEMETRY_RETRY_CONFIG?.baseCapacity; private numOfAttemptsWhenQueueIsFull: number = 1; private triggeredDueToQueueOverflow: boolean = false; private callbackHandler?: () => {}; @@ -45,12 +46,12 @@ export class TelemetryRetry { private resetTimerParameters = () => { this.numOfAttemptsWhenTimerHits = 1; - this.timePeriod = TELEMETRY_RETRY_CONFIG.baseTimer; + this.timePeriod = this.TELEMETRY_RETRY_CONFIG.baseTimer; this.clearTimer(); } private increaseTimePeriod = (): void => { - if (this.numOfAttemptsWhenTimerHits <= TELEMETRY_RETRY_CONFIG.maxRetries) { + if (this.numOfAttemptsWhenTimerHits <= this.TELEMETRY_RETRY_CONFIG.maxRetries) { this.timePeriod = this.calculateDelay(); this.numOfAttemptsWhenTimerHits++; return; @@ -66,26 +67,26 @@ export class TelemetryRetry { } private calculateDelay = (): number => { - const baseDelay = TELEMETRY_RETRY_CONFIG.baseTimer * - Math.pow(TELEMETRY_RETRY_CONFIG.backoffFactor, this.numOfAttemptsWhenTimerHits); + const baseDelay = this.TELEMETRY_RETRY_CONFIG.baseTimer * + Math.pow(this.TELEMETRY_RETRY_CONFIG.backoffFactor, this.numOfAttemptsWhenTimerHits); - const cappedDelay = Math.min(baseDelay, TELEMETRY_RETRY_CONFIG.maxDelayMs); + const cappedDelay = Math.min(baseDelay, this.TELEMETRY_RETRY_CONFIG.maxDelayMs); - const jitterMultiplier = 1 + (Math.random() * 2 - 1) * TELEMETRY_RETRY_CONFIG.jitterFactor; + const jitterMultiplier = 1 + (Math.random() * 2 - 1) * this.TELEMETRY_RETRY_CONFIG.jitterFactor; return Math.floor(cappedDelay * jitterMultiplier); }; private increaseQueueCapacity = (): void => { - if (this.numOfAttemptsWhenQueueIsFull < TELEMETRY_RETRY_CONFIG.maxRetries) { - this.queueCapacity = TELEMETRY_RETRY_CONFIG.baseCapacity * - Math.pow(TELEMETRY_RETRY_CONFIG.backoffFactor, this.numOfAttemptsWhenQueueIsFull); + if (this.numOfAttemptsWhenQueueIsFull < this.TELEMETRY_RETRY_CONFIG.maxRetries) { + this.queueCapacity = this.TELEMETRY_RETRY_CONFIG.baseCapacity * + Math.pow(this.TELEMETRY_RETRY_CONFIG.backoffFactor, this.numOfAttemptsWhenQueueIsFull); } throw new Error("Number of retries exceeded"); } private resetQueueCapacity = (): void => { - this.queueCapacity = TELEMETRY_RETRY_CONFIG.baseCapacity; + this.queueCapacity = this.TELEMETRY_RETRY_CONFIG.baseCapacity; this.numOfAttemptsWhenQueueIsFull = 1; this.triggeredDueToQueueOverflow = false; } diff --git a/vscode/src/telemetry/telemetry.ts b/vscode/src/telemetry/telemetry.ts index 3f60cf4..a43623b 100644 --- a/vscode/src/telemetry/telemetry.ts +++ b/vscode/src/telemetry/telemetry.ts @@ -18,21 +18,24 @@ import { ExtensionContextInfo } from "../extensionContextInfo"; import { LOGGER } from "../logger"; import { BaseEvent } from "./events/baseEvent"; import { TelemetryReporter } from "./types"; -import { TELEMETRY_API } from "./config"; +import { TelemetryConfiguration } from "./config"; export namespace Telemetry { let telemetryManager: TelemetryManager; - - export const isTelemetryFeatureAvailable = TELEMETRY_API.baseUrl != null && TELEMETRY_API.baseUrl.trim().length; - + + export const getIsTelemetryFeatureAvailable = (): boolean => { + const TELEMETRY_API = TelemetryConfiguration.getInstance()?.getApiConfig(); + return TELEMETRY_API?.baseUrl != null && TELEMETRY_API?.baseUrl.trim().length > 0; + } + export const initializeTelemetry = (contextInfo: ExtensionContextInfo): TelemetryManager => { if (!!telemetryManager) { LOGGER.warn("Telemetry is already initialized"); return telemetryManager; } telemetryManager = new TelemetryManager(contextInfo); - if (isTelemetryFeatureAvailable) { + if (getIsTelemetryFeatureAvailable()) { telemetryManager.initializeReporter(); } @@ -40,7 +43,7 @@ export namespace Telemetry { } const enqueueEvent = (cbFunction: (reporter: TelemetryReporter) => void) => { - if (telemetryManager.isExtTelemetryEnabled() && isTelemetryFeatureAvailable) { + if (telemetryManager.isExtTelemetryEnabled() && getIsTelemetryFeatureAvailable()) { const reporter = telemetryManager.getReporter(); if (reporter) { cbFunction(reporter); From 1414fbc299725cb2ba2015910e72ed88bc48af61 Mon Sep 17 00:00:00 2001 From: Achal Talati Date: Thu, 9 Jan 2025 14:51:47 +0530 Subject: [PATCH 03/16] Updated start event trigger point and added workspaceChange event re-trigger ensure workspaceChangeEvent interval is triggered only once --- vscode/src/lsp/initializer.ts | 4 +--- vscode/src/telemetry/events/start.ts | 1 + vscode/src/telemetry/events/workspaceChange.ts | 13 +++++++++++++ vscode/src/telemetry/telemetry.ts | 4 ---- vscode/src/telemetry/telemetryManager.ts | 1 + 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/vscode/src/lsp/initializer.ts b/vscode/src/lsp/initializer.ts index dfe178b..36816b7 100644 --- a/vscode/src/lsp/initializer.ts +++ b/vscode/src/lsp/initializer.ts @@ -108,9 +108,7 @@ export const clientInit = () => { const client = NbLanguageClient.build(connection, LOGGER); LOGGER.log('Language Client: Starting'); - client.start().then(() => { - Telemetry.enqueueStartEvent(); - + client.start().then(() => { registerListenersAfterClientInit(); registerNotificationListeners(client); registerRequestListeners(client); diff --git a/vscode/src/telemetry/events/start.ts b/vscode/src/telemetry/events/start.ts index 8bb4f1c..4ae74ff 100644 --- a/vscode/src/telemetry/events/start.ts +++ b/vscode/src/telemetry/events/start.ts @@ -59,6 +59,7 @@ export class ExtensionStartEvent extends BaseEvent { } onSuccessPostEventCallback = async (): Promise => { + LOGGER.debug(`Start event sent successfully`); this.addEventToCache(); } diff --git a/vscode/src/telemetry/events/workspaceChange.ts b/vscode/src/telemetry/events/workspaceChange.ts index 2bf6214..7d57760 100644 --- a/vscode/src/telemetry/events/workspaceChange.ts +++ b/vscode/src/telemetry/events/workspaceChange.ts @@ -13,6 +13,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { LOGGER } from "../../logger"; +import { Telemetry } from "../telemetry"; import { BaseEvent } from "./baseEvent"; interface ProjectInfo { @@ -30,6 +32,8 @@ export interface WorkspaceChangeData { projInitTimeTaken: number; } +let workspaceChangeEventTimeout: NodeJS.Timeout | null = null; + export class WorkspaceChangeEvent extends BaseEvent { public static readonly NAME = "workspaceChange"; public static readonly ENDPOINT = "/workspaceChange"; @@ -37,4 +41,13 @@ export class WorkspaceChangeEvent extends BaseEvent { constructor(payload: WorkspaceChangeData) { super(WorkspaceChangeEvent.NAME, WorkspaceChangeEvent.ENDPOINT, payload); } + + public onSuccessPostEventCallback = async (): Promise => { + LOGGER.debug(`WorkspaceChange event sent successfully`); + if (workspaceChangeEventTimeout != null) { + clearTimeout(workspaceChangeEventTimeout); + workspaceChangeEventTimeout = null; + } + workspaceChangeEventTimeout = setTimeout(() => Telemetry.sendTelemetry(this), 60 * 60 * 24 * 1000); + }; } \ No newline at end of file diff --git a/vscode/src/telemetry/telemetry.ts b/vscode/src/telemetry/telemetry.ts index a43623b..4e61d65 100644 --- a/vscode/src/telemetry/telemetry.ts +++ b/vscode/src/telemetry/telemetry.ts @@ -55,10 +55,6 @@ export namespace Telemetry { enqueueEvent((reporter) => reporter.addEventToQueue(event)); } - export const enqueueStartEvent = (): void => { - enqueueEvent((reporter) => reporter.startEvent()); - } - export const enqueueCloseEvent = (): void => { enqueueEvent((reporter) => reporter.closeEvent()); } diff --git a/vscode/src/telemetry/telemetryManager.ts b/vscode/src/telemetry/telemetryManager.ts index ce4e5ea..eb1c40e 100644 --- a/vscode/src/telemetry/telemetryManager.ts +++ b/vscode/src/telemetry/telemetryManager.ts @@ -69,6 +69,7 @@ export class TelemetryManager { } if (this.settings.isExtTelemetryEnabled) { this.telemetryRetryManager.startTimer(); + this.reporter?.startEvent(); } } }; \ No newline at end of file From 2d51716771036f8f88cb8ed14348c6084dfd7cb2 Mon Sep 17 00:00:00 2001 From: Siddharth Srinivasan Date: Sat, 14 Dec 2024 14:31:07 +0530 Subject: [PATCH 04/16] Fixes and edits to telemetry code to support JdkFeatureEvent 1. Amended LspServerTelemetryManager to expose isPreviewEnabled() method and associated enums and methods to populate JdkFeatureEvent. 2. Amended NbCodeClientCapabilites.wantsTelemetryEnabled() to avoid an NPE in case of null Boolean -> boolean conversion. 3. Fixed handlers.ts to receive jdkFeature event from LSP and send it to the telemetry reporter. 4. Fixed jdkFeatures.ts to properly coalesce multiple events. Signed-off-by: Siddharth Srinivasan --- patches/nb-telemetry.diff | 45 +++++++++++-------- .../lsp/listeners/notifications/handlers.ts | 31 +++++++++---- vscode/src/telemetry/events/jdkFeature.ts | 4 +- 3 files changed, 51 insertions(+), 29 deletions(-) diff --git a/patches/nb-telemetry.diff b/patches/nb-telemetry.diff index e7c17bb..c7c381c 100644 --- a/patches/nb-telemetry.diff +++ b/patches/nb-telemetry.diff @@ -42,7 +42,7 @@ index d82646afb1..3d507b5fe3 100644 import org.openide.util.Lookup; /** -@@ -55,130 +58,164 @@ import org.openide.util.Lookup; +@@ -55,130 +58,171 @@ import org.openide.util.Lookup; */ public class LspServerTelemetryManager { @@ -63,7 +63,7 @@ index d82646afb1..3d507b5fe3 100644 - public synchronized void connect(LanguageClient client, Future future) { - clients.put(client, future); - lspServerIntiailizationTime = System.currentTimeMillis(); -+ private static enum ProjectType { ++ public static enum ProjectType { + standalone, + maven, + gradle; @@ -188,12 +188,12 @@ index d82646afb1..3d507b5fe3 100644 - - // In future if different JDK is used for different project then this can be updated - obj.addProperty("javaVersion", System.getProperty("java.version")); -- -- if (mp.containsKey(prjPath)) { -- Project prj = mp.get(prjPath); + String javaVersion = getProjectJavaVersion(); + obj.addProperty("javaVersion", javaVersion); - + +- if (mp.containsKey(prjPath)) { +- Project prj = mp.get(prjPath); +- - ProjectManager.Result r = ProjectManager.getDefault().isProject2(prj.getProjectDirectory()); - String projectType = r.getProjectType(); - obj.addProperty("buildTool", (projectType.contains("maven") ? "MavenProject" : "GradleProject")); @@ -220,7 +220,7 @@ index d82646afb1..3d507b5fe3 100644 + obj.addProperty("isOpenedWithProblems", ProjectProblems.isBroken(prj)); } + obj.addProperty("buildTool", projectType.name()); -+ boolean isPreviewFlagEnabled = isPreviewEnabled(projectDirectory, projectType); ++ boolean isPreviewFlagEnabled = isPreviewEnabled(projectDirectory, projectType, client); + obj.addProperty("isPreviewEnabled", isPreviewFlagEnabled); prjProps.add(obj); @@ -236,26 +236,35 @@ index d82646afb1..3d507b5fe3 100644 - properties.add("prjsInfo", prjProps); + properties.add("projectInfo", prjProps); - -- properties.addProperty("timeToOpenPrjs", timeToOpenPrjs); -- properties.addProperty("numOfPrjsOpened", workspaceClientFolders.size()); -- properties.addProperty("lspServerInitializationTime", System.currentTimeMillis() - this.lspServerIntiailizationTime); ++ + properties.addProperty("projInitTimeTaken", timeToOpenProjects); + properties.addProperty("numProjects", workspaceClientFolders.size()); + properties.addProperty("lspInitTimeTaken", System.currentTimeMillis() - this.lspServerIntializationTime); -- this.sendTelemetry(client, new TelemetryEvent(MessageType.Info.toString(), this.WORKSPACE_INFO_EVT, properties)); +- properties.addProperty("timeToOpenPrjs", timeToOpenPrjs); +- properties.addProperty("numOfPrjsOpened", workspaceClientFolders.size()); +- properties.addProperty("lspServerInitializationTime", System.currentTimeMillis() - this.lspServerIntiailizationTime); + this.sendTelemetry(client, new TelemetryEvent(MessageType.Info.toString(), LspServerTelemetryManager.WORKSPACE_INFO_EVT, properties)); ++ } + +- this.sendTelemetry(client, new TelemetryEvent(MessageType.Info.toString(), this.WORKSPACE_INFO_EVT, properties)); ++ public boolean isPreviewEnabled(FileObject source, ProjectType prjType) { ++ return isPreviewEnabled(source, prjType, null); } - - private boolean isEnablePreivew(FileObject source, String prjType) { - if (prjType.equals(this.STANDALONE_PRJ)) { +- NbCodeLanguageClient client = Lookup.getDefault().lookup(NbCodeLanguageClient.class); + -+ private boolean isPreviewEnabled(FileObject source, ProjectType prjType) { ++ public boolean isPreviewEnabled(FileObject source, ProjectType prjType, LanguageClient languageClient) { + if (prjType == ProjectType.standalone) { - NbCodeLanguageClient client = Lookup.getDefault().lookup(NbCodeLanguageClient.class); ++ NbCodeLanguageClient client = languageClient instanceof NbCodeLanguageClient ? (NbCodeLanguageClient) languageClient : null ; if (client == null) { - return false; +- return false; ++ client = Lookup.getDefault().lookup(NbCodeLanguageClient.class); ++ if (client == null) { ++ return false; ++ } } - AtomicBoolean isEnablePreviewSet = new AtomicBoolean(false); + boolean[] isEnablePreviewSet = {false}; @@ -283,7 +292,7 @@ index d82646afb1..3d507b5fe3 100644 } private String getPrjId(String prjPath) throws NoSuchAlgorithmException { -@@ -187,15 +224,50 @@ public class LspServerTelemetryManager { +@@ -187,15 +231,50 @@ public class LspServerTelemetryManager { BigInteger number = new BigInteger(1, hash); @@ -331,7 +340,7 @@ index d82646afb1..3d507b5fe3 100644 + return propertyLookup.apply("java.vm.name"); + } + -+ private ProjectType getProjectType(Project prj) { ++ public ProjectType getProjectType(Project prj) { + ProjectManager.Result r = ProjectManager.getDefault().isProject2(prj.getProjectDirectory()); + String projectType = r == null ? null : r.getProjectType(); + return projectType != null && projectType.contains(ProjectType.maven.name()) ? ProjectType.maven : ProjectType.gradle; @@ -358,7 +367,7 @@ index 9134992f5f..f070fec320 100644 } + public boolean wantsTelemetryEnabled() { -+ return wantsTelemetryEnabled; ++ return wantsTelemetryEnabled == Boolean.TRUE; + } + private NbCodeClientCapabilities withCapabilities(ClientCapabilities caps) { diff --git a/vscode/src/lsp/listeners/notifications/handlers.ts b/vscode/src/lsp/listeners/notifications/handlers.ts index 2387077..2ef5cf6 100644 --- a/vscode/src/lsp/listeners/notifications/handlers.ts +++ b/vscode/src/lsp/listeners/notifications/handlers.ts @@ -25,6 +25,7 @@ import { LOGGER } from '../../../logger'; import { globalState } from "../../../globalState"; import { WorkspaceChangeData, WorkspaceChangeEvent } from "../../../telemetry/events/workspaceChange"; import { Telemetry } from "../../../telemetry/telemetry"; +import { JdkFeatureEvent, JdkFeatureEventData } from "../../../telemetry/events/jdkFeature"; const checkInstallNbJavac = (msg: string) => { const NO_JAVA_SUPPORT = "Cannot initialize Java support"; @@ -44,8 +45,8 @@ const checkInstallNbJavac = (msg: string) => { } } -const showStatusBarMessageHandler = (params : ShowStatusMessageParams) => { - let decorated : string = params.message; +const showStatusBarMessageHandler = (params: ShowStatusMessageParams) => { + let decorated: string = params.message; let defTimeout; switch (params.type) { @@ -100,7 +101,7 @@ const textEditorDecorationDisposeHandler = (param: any) => { if (decorationType) { globalState.removeDecoration(param); decorationType.dispose(); - + globalState.getDecorationParamsByUri().forEach((value, key) => { if (value.key == param) { globalState.removeDecorationParams(key); @@ -111,8 +112,8 @@ const textEditorDecorationDisposeHandler = (param: any) => { const telemetryEventHandler = (param: any) => { - if(WorkspaceChangeEvent.NAME === param?.name){ - const {projectInfo, numProjects, lspInitTimeTaken, projInitTimeTaken} = param?.properties; + if (WorkspaceChangeEvent.NAME === param?.name) { + const { projectInfo, numProjects, lspInitTimeTaken, projInitTimeTaken } = param?.properties; const eventData: WorkspaceChangeData = { projectInfo, numProjects, @@ -123,6 +124,18 @@ const telemetryEventHandler = (param: any) => { Telemetry.sendTelemetry(workspaceChangeEvent); return; } + if (JdkFeatureEvent.NAME === param?.name) { + const { javaVersion, names, jeps, isPreviewEnabled } = param?.properties; + const eventData: JdkFeatureEventData = { + jeps, + names, + javaVersion, + isPreviewEnabled + }; + const jdkFeatureEvent: JdkFeatureEvent = new JdkFeatureEvent(eventData); + Telemetry.sendTelemetry(jdkFeatureEvent); + return; + } const ls = globalState.getListener(param); if (ls) { for (const listener of ls) { @@ -131,7 +144,7 @@ const telemetryEventHandler = (param: any) => { } } -export const notificationListeners : notificationOrRequestListenerType[] = [{ +export const notificationListeners: notificationOrRequestListenerType[] = [{ type: StatusMessageRequest.type, handler: showStatusBarMessageHandler }, { @@ -140,13 +153,13 @@ export const notificationListeners : notificationOrRequestListenerType[] = [{ }, { type: TestProgressNotification.type, handler: testProgressHandler -},{ +}, { type: TextEditorDecorationSetNotification.type, handler: textEditorSetDecorationHandler -},{ +}, { type: TextEditorDecorationDisposeNotification.type, handler: textEditorDecorationDisposeHandler -},{ +}, { type: TelemetryEventNotification.type, handler: telemetryEventHandler }]; \ No newline at end of file diff --git a/vscode/src/telemetry/events/jdkFeature.ts b/vscode/src/telemetry/events/jdkFeature.ts index 7c292e3..24c8e97 100644 --- a/vscode/src/telemetry/events/jdkFeature.ts +++ b/vscode/src/telemetry/events/jdkFeature.ts @@ -49,8 +49,8 @@ export class JdkFeatureEvent extends BaseEvent { const names: string[] = []; events.forEach(event => { - jeps.push(...event.getPayload.jeps); - names.push(...event.getPayload.names); + if (event.getPayload.jeps) jeps.push(...event.getPayload.jeps); + if (event.getPayload.names) names.push(...event.getPayload.names); }); return new JdkFeatureEvent({ From 2c6e8d66da0555c8d78f8d718a73a1e48cac7d2e Mon Sep 17 00:00:00 2001 From: Siddharth Srinivasan Date: Tue, 14 Jan 2025 03:29:52 +0530 Subject: [PATCH 05/16] Fixes to TelemetryServerManager to ensure multi-module projects under a workspace root are recognized 1. Amended LspServerTelemetryManager.sendWorkspaceInfo to use a NavigableMap to find sub-paths of the workspaceFolder that correspond to Projects, when it is not itself a Project. 2. Also, caught exceptions from ProjectProblems.isBroken() since it maybe a sign of a broken project. Signed-off-by: Siddharth Srinivasan --- patches/nb-telemetry.diff | 117 +++++++++++++++++++++++++------------- 1 file changed, 78 insertions(+), 39 deletions(-) diff --git a/patches/nb-telemetry.diff b/patches/nb-telemetry.diff index c7c381c..9310172 100644 --- a/patches/nb-telemetry.diff +++ b/patches/nb-telemetry.diff @@ -1,5 +1,5 @@ diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/LspServerTelemetryManager.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/LspServerTelemetryManager.java -index d82646afb1..3d507b5fe3 100644 +index d82646afb1..b008279cc4 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/LspServerTelemetryManager.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/LspServerTelemetryManager.java @@ -21,6 +21,7 @@ package org.netbeans.modules.java.lsp.server.protocol; @@ -10,7 +10,7 @@ index d82646afb1..3d507b5fe3 100644 import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; -@@ -28,25 +29,27 @@ import java.security.NoSuchAlgorithmException; +@@ -28,25 +29,29 @@ import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -19,6 +19,8 @@ index d82646afb1..3d507b5fe3 100644 import java.util.List; import java.util.Map; -import java.util.Set; ++import java.util.NavigableMap; ++import java.util.TreeMap; import java.util.WeakHashMap; import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicBoolean; @@ -42,7 +44,7 @@ index d82646afb1..3d507b5fe3 100644 import org.openide.util.Lookup; /** -@@ -55,130 +58,171 @@ import org.openide.util.Lookup; +@@ -55,130 +60,200 @@ import org.openide.util.Lookup; */ public class LspServerTelemetryManager { @@ -83,11 +85,11 @@ index d82646afb1..3d507b5fe3 100644 + + private static final LspServerTelemetryManager instance = new LspServerTelemetryManager(); + } - ++ + private final WeakHashMap>> clients = new WeakHashMap<>(); + private volatile boolean telemetryEnabled = false; + private long lspServerIntializationTime; -+ + + public boolean isTelemetryEnabled() { + return telemetryEnabled; + } @@ -175,22 +177,21 @@ index d82646afb1..3d507b5fe3 100644 JsonArray prjProps = new JsonArray(); - Map mp = prjs.stream() -+ Map mp = projects.stream() - .collect(Collectors.toMap(project -> project.getProjectDirectory().getPath(), project -> project)); +- .collect(Collectors.toMap(project -> project.getProjectDirectory().getPath(), project -> project)); ++ NavigableMap mp = projects.stream() ++ .collect(Collectors.toMap(project -> project.getProjectDirectory().getPath(), project -> project, (p1, p2) -> p1, TreeMap::new)); for (FileObject workspaceFolder : workspaceClientFolders) { try { - JsonObject obj = new JsonObject(); +- JsonObject obj = new JsonObject(); ++ boolean noProjectFound = true; String prjPath = workspaceFolder.getPath(); - String prjId = this.getPrjId(prjPath); -+ String prjId = getPrjId(prjPath); - obj.addProperty("id", prjId); +- obj.addProperty("id", prjId); - - // In future if different JDK is used for different project then this can be updated - obj.addProperty("javaVersion", System.getProperty("java.version")); -+ String javaVersion = getProjectJavaVersion(); -+ obj.addProperty("javaVersion", javaVersion); - +- - if (mp.containsKey(prjPath)) { - Project prj = mp.get(prjPath); - @@ -202,31 +203,40 @@ index d82646afb1..3d507b5fe3 100644 - - boolean isPreviewFlagEnabled = this.isEnablePreivew(prj.getProjectDirectory(), projectType); - obj.addProperty("enablePreview", isPreviewFlagEnabled); -+ Project prj = mp.get(prjPath); -+ FileObject projectDirectory; -+ ProjectType projectType; -+ if (prj == null) { -+ projectType = ProjectType.standalone; -+ projectDirectory = workspaceFolder; - } else { +- } else { - obj.addProperty("buildTool", this.STANDALONE_PRJ); - obj.addProperty("javaVersion", System.getProperty("java.version")); - obj.addProperty("openedWithProblems", false); - - boolean isPreviewFlagEnabled = this.isEnablePreivew(workspaceFolder, this.STANDALONE_PRJ); - obj.addProperty("enablePreview", isPreviewFlagEnabled); -+ projectType = getProjectType(prj); -+ projectDirectory = prj.getProjectDirectory(); -+ obj.addProperty("isOpenedWithProblems", ProjectProblems.isBroken(prj)); ++ String prjPathWithSlash = null; ++ for (Map.Entry p : mp.tailMap(prjPath, true).entrySet()) { ++ String projectPath = p.getKey(); ++ if (prjPathWithSlash == null) { ++ if (prjPath.equals(projectPath)) { ++ prjProps.add(createProjectInfo(prjPath, p.getValue(), workspaceFolder, client)); ++ noProjectFound = false; ++ break; ++ } ++ prjPathWithSlash = prjPath + '/'; ++ } ++ if (projectPath.startsWith(prjPathWithSlash)) { ++ prjProps.add(createProjectInfo(p.getKey(), p.getValue(), workspaceFolder, client)); ++ noProjectFound = false; ++ continue; ++ } ++ break; } -+ obj.addProperty("buildTool", projectType.name()); -+ boolean isPreviewFlagEnabled = isPreviewEnabled(projectDirectory, projectType, client); -+ obj.addProperty("isPreviewEnabled", isPreviewFlagEnabled); - - prjProps.add(obj); - +- +- prjProps.add(obj); +- - } catch (NoSuchAlgorithmException ex) { - Exceptions.printStackTrace(ex); ++ if (noProjectFound) { ++ // No project found ++ prjProps.add(createProjectInfo(prjPath, null, workspaceFolder, client)); ++ } + } catch (NoSuchAlgorithmException e) { + LOG.log(Level.INFO, "NoSuchAlgorithmException while creating workspaceInfo event: {0}", e.getMessage()); + } catch (Exception e) { @@ -236,26 +246,55 @@ index d82646afb1..3d507b5fe3 100644 - properties.add("prjsInfo", prjProps); + properties.add("projectInfo", prjProps); -+ -+ properties.addProperty("projInitTimeTaken", timeToOpenProjects); -+ properties.addProperty("numProjects", workspaceClientFolders.size()); -+ properties.addProperty("lspInitTimeTaken", System.currentTimeMillis() - this.lspServerIntializationTime); - properties.addProperty("timeToOpenPrjs", timeToOpenPrjs); - properties.addProperty("numOfPrjsOpened", workspaceClientFolders.size()); - properties.addProperty("lspServerInitializationTime", System.currentTimeMillis() - this.lspServerIntiailizationTime); -+ this.sendTelemetry(client, new TelemetryEvent(MessageType.Info.toString(), LspServerTelemetryManager.WORKSPACE_INFO_EVT, properties)); -+ } - ++ properties.addProperty("projInitTimeTaken", timeToOpenProjects); ++ properties.addProperty("numProjects", workspaceClientFolders.size()); ++ properties.addProperty("lspInitTimeTaken", System.currentTimeMillis() - this.lspServerIntializationTime); + - this.sendTelemetry(client, new TelemetryEvent(MessageType.Info.toString(), this.WORKSPACE_INFO_EVT, properties)); -+ public boolean isPreviewEnabled(FileObject source, ProjectType prjType) { -+ return isPreviewEnabled(source, prjType, null); ++ this.sendTelemetry(client, new TelemetryEvent(MessageType.Info.toString(), LspServerTelemetryManager.WORKSPACE_INFO_EVT, properties)); } - - private boolean isEnablePreivew(FileObject source, String prjType) { - if (prjType.equals(this.STANDALONE_PRJ)) { - NbCodeLanguageClient client = Lookup.getDefault().lookup(NbCodeLanguageClient.class); + ++ private JsonObject createProjectInfo(String prjPath, Project prj, FileObject workspaceFolder, LanguageClient client) throws NoSuchAlgorithmException { ++ JsonObject obj = new JsonObject(); ++ String prjId = getPrjId(prjPath); ++ obj.addProperty("id", prjId); ++ FileObject projectDirectory; ++ ProjectType projectType; ++ if (prj == null) { ++ projectType = ProjectType.standalone; ++ projectDirectory = workspaceFolder; ++ } else { ++ projectType = getProjectType(prj); ++ projectDirectory = prj.getProjectDirectory(); ++ boolean projectHasProblems; ++ try { ++ projectHasProblems = ProjectProblems.isBroken(prj); ++ } catch (RuntimeException e) { ++ LOG.log(Level.INFO, "Exception while checking project problems for workspaceInfo event: {0}", e.getMessage()); ++ projectHasProblems = true; ++ } ++ obj.addProperty("isOpenedWithProblems", projectHasProblems); ++ } ++ String javaVersion = getProjectJavaVersion(); ++ obj.addProperty("javaVersion", javaVersion); ++ obj.addProperty("buildTool", projectType.name()); ++ boolean isPreviewFlagEnabled = isPreviewEnabled(projectDirectory, projectType, client); ++ obj.addProperty("isPreviewEnabled", isPreviewFlagEnabled); ++ return obj; ++ } ++ ++ public boolean isPreviewEnabled(FileObject source, ProjectType prjType) { ++ return isPreviewEnabled(source, prjType, null); ++ } ++ + public boolean isPreviewEnabled(FileObject source, ProjectType prjType, LanguageClient languageClient) { + if (prjType == ProjectType.standalone) { + NbCodeLanguageClient client = languageClient instanceof NbCodeLanguageClient ? (NbCodeLanguageClient) languageClient : null ; @@ -292,7 +331,7 @@ index d82646afb1..3d507b5fe3 100644 } private String getPrjId(String prjPath) throws NoSuchAlgorithmException { -@@ -187,15 +231,50 @@ public class LspServerTelemetryManager { +@@ -187,15 +262,50 @@ public class LspServerTelemetryManager { BigInteger number = new BigInteger(1, hash); From 559570c353c4f88c36e2f741d6043f27ce306ba7 Mon Sep 17 00:00:00 2001 From: Siddharth Srinivasan Date: Sat, 14 Dec 2024 14:31:07 +0530 Subject: [PATCH 06/16] Addition of the java.lsp.server.telemetry module to obtain Java language features from the NetBeans LSP Server. 1. Added nbcode.java.lsp.server.telemetry module to provide the implementation. 2. Added jdk.compiler module access to NBLS in launchOptions.ts Co-authored-by: Jan Lahoda @lahodaj Co-authored-by: Siddharth Srinivasan @sid-srini Signed-off-by: Siddharth Srinivasan --- nbcode/nbproject/project.properties | 4 +- nbcode/telemetry/build.xml | 23 +++ nbcode/telemetry/manifest.mf | 6 + nbcode/telemetry/nbproject/build-impl.xml | 56 +++++++ .../telemetry/nbproject/genfiles.properties | 23 +++ nbcode/telemetry/nbproject/project.properties | 30 ++++ nbcode/telemetry/nbproject/project.xml | 136 +++++++++++++++++ nbcode/telemetry/nbproject/suite.properties | 16 ++ .../lsp/server/telemetry/Bundle.properties | 17 +++ .../server/telemetry/JavaLangFeatures.java | 91 +++++++++++ .../JavaLangFeaturesTelemetryProvider.java | 86 +++++++++++ .../JavaLanguageFeaturesEmitter.java | 105 +++++++++++++ .../lsp/server/telemetry/JdkFeatureEvent.java | 102 +++++++++++++ .../server/telemetry/SourceFeatureCache.java | 116 ++++++++++++++ .../java/lsp/server/telemetry/SourceInfo.java | 142 ++++++++++++++++++ .../JavaLanguageFeaturesEmitterTest.java | 71 +++++++++ vscode/src/lsp/launchOptions.ts | 1 + 17 files changed, 1024 insertions(+), 1 deletion(-) create mode 100644 nbcode/telemetry/build.xml create mode 100644 nbcode/telemetry/manifest.mf create mode 100644 nbcode/telemetry/nbproject/build-impl.xml create mode 100644 nbcode/telemetry/nbproject/genfiles.properties create mode 100644 nbcode/telemetry/nbproject/project.properties create mode 100644 nbcode/telemetry/nbproject/project.xml create mode 100644 nbcode/telemetry/nbproject/suite.properties create mode 100644 nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/Bundle.properties create mode 100644 nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/JavaLangFeatures.java create mode 100644 nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/JavaLangFeaturesTelemetryProvider.java create mode 100644 nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/JavaLanguageFeaturesEmitter.java create mode 100644 nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/JdkFeatureEvent.java create mode 100644 nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/SourceFeatureCache.java create mode 100644 nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/SourceInfo.java create mode 100644 nbcode/telemetry/test/unit/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/JavaLanguageFeaturesEmitterTest.java diff --git a/nbcode/nbproject/project.properties b/nbcode/nbproject/project.properties index 864aa45..f3e0906 100644 --- a/nbcode/nbproject/project.properties +++ b/nbcode/nbproject/project.properties @@ -27,5 +27,7 @@ auxiliary.org-netbeans-modules-apisupport-installer.os-solaris=false auxiliary.org-netbeans-modules-apisupport-installer.os-windows=false auxiliary.org-netbeans-spi-editor-hints-projects.perProjectHintSettingsFile=nbproject/cfg_hints.xml modules=\ - ${project.org.netbeans.modules.nbcode.integration} + ${project.org.netbeans.modules.nbcode.integration} :\ + ${project.org.netbeans.modules.nbcode.java.lsp.server.telemetry} project.org.netbeans.modules.nbcode.integration=integration +project.org.netbeans.modules.nbcode.java.lsp.server.telemetry=telemetry diff --git a/nbcode/telemetry/build.xml b/nbcode/telemetry/build.xml new file mode 100644 index 0000000..339fecb --- /dev/null +++ b/nbcode/telemetry/build.xml @@ -0,0 +1,23 @@ + + + + Builds, tests, and runs the project org.netbeans.modules.nbcode.java.lsp.server.telemetry. + + + + + diff --git a/nbcode/telemetry/manifest.mf b/nbcode/telemetry/manifest.mf new file mode 100644 index 0000000..61a299d --- /dev/null +++ b/nbcode/telemetry/manifest.mf @@ -0,0 +1,6 @@ +Manifest-Version: 1.0 +AutoUpdate-Show-In-Client: false +OpenIDE-Module: org.netbeans.modules.nbcode.java.lsp.server.telemetry +OpenIDE-Module-Implementation-Version: 1 +OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/nbcode/java/lsp/server/telemetry/Bundle.properties + diff --git a/nbcode/telemetry/nbproject/build-impl.xml b/nbcode/telemetry/nbproject/build-impl.xml new file mode 100644 index 0000000..208c413 --- /dev/null +++ b/nbcode/telemetry/nbproject/build-impl.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + You must set 'suite.dir' to point to your containing module suite + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nbcode/telemetry/nbproject/genfiles.properties b/nbcode/telemetry/nbproject/genfiles.properties new file mode 100644 index 0000000..4f8a333 --- /dev/null +++ b/nbcode/telemetry/nbproject/genfiles.properties @@ -0,0 +1,23 @@ +# +# Copyright (c) 2024-2025, Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +build.xml.data.CRC32=bcbc94fb +build.xml.script.CRC32=f4d83a2b +build.xml.stylesheet.CRC32=15ca8a54@2.97 +# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. +# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. +nbproject/build-impl.xml.data.CRC32=decee057 +nbproject/build-impl.xml.script.CRC32=547e2c6a +nbproject/build-impl.xml.stylesheet.CRC32=49aa68b0@2.97 diff --git a/nbcode/telemetry/nbproject/project.properties b/nbcode/telemetry/nbproject/project.properties new file mode 100644 index 0000000..787b525 --- /dev/null +++ b/nbcode/telemetry/nbproject/project.properties @@ -0,0 +1,30 @@ +# +# Copyright (c) 2024-2025, Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +javac.source=1.8 +requires.nb.javac=true +javac.compilerargs=-Xlint -Xlint:-serial +test.run.args=--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ + --add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED \ + --add-exports=jdk.compiler/com.sun.tools.javac.resources=ALL-UNNAMED \ + --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \ + +test.unit.lib.cp= +test.unit.run.cp.extra= +license.file=../../LICENSE.txt +nbm.homepage=https://github.com/oracle/javavscode/ +nbplatform.default.netbeans.dest.dir=${suite.dir}/../netbeans/nbbuild/netbeans +nbplatform.default.harness.dir=${nbplatform.default.netbeans.dest.dir}/harness +spec.version.base=1.0 diff --git a/nbcode/telemetry/nbproject/project.xml b/nbcode/telemetry/nbproject/project.xml new file mode 100644 index 0000000..7a2c069 --- /dev/null +++ b/nbcode/telemetry/nbproject/project.xml @@ -0,0 +1,136 @@ + + + + org.netbeans.modules.apisupport.project + + + org.netbeans.modules.nbcode.java.lsp.server.telemetry + + + + org.netbeans.api.lsp + + + + 1 + 1.28 + + + + org.netbeans.libs.javacapi + + + + + + + + org.netbeans.modules.editor.mimelookup + + + + 1 + 1.65 + + + + org.netbeans.modules.java.lsp.server + + + + 2 + + + + + org.netbeans.modules.java.platform + + + + 1 + 1.67 + + + + org.netbeans.modules.java.project + + + + 1 + 1.97 + + + + org.netbeans.modules.java.source.base + + + + 2.68.0.6.4.3.8.1 + + + + org.netbeans.modules.projectapi + + + + 1 + 1.96 + + + + org.openide.filesystems + + + + 9.38 + + + + org.openide.util + + + + 9.33 + + + + org.openide.util.lookup + + + + 8.59 + + + + + + unit + + org.netbeans.libs.junit4 + + + + org.netbeans.modules.nbjunit + + + + + + + + + diff --git a/nbcode/telemetry/nbproject/suite.properties b/nbcode/telemetry/nbproject/suite.properties new file mode 100644 index 0000000..0b44bb9 --- /dev/null +++ b/nbcode/telemetry/nbproject/suite.properties @@ -0,0 +1,16 @@ +# +# Copyright (c) 2024-2025, Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +suite.dir=${basedir}/.. diff --git a/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/Bundle.properties b/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/Bundle.properties new file mode 100644 index 0000000..7c1563f --- /dev/null +++ b/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/Bundle.properties @@ -0,0 +1,17 @@ +# +# Copyright (c) 2024-2025, Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +OpenIDE-Module-Display-Category=Java +OpenIDE-Module-Name=Java LSP Server - Telemetry diff --git a/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/JavaLangFeatures.java b/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/JavaLangFeatures.java new file mode 100644 index 0000000..7595b60 --- /dev/null +++ b/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/JavaLangFeatures.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2024-2025, Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.netbeans.modules.nbcode.java.lsp.server.telemetry; + +import com.sun.tools.javac.code.Source; +import com.sun.tools.javac.resources.CompilerProperties; +import com.sun.tools.javac.util.JCDiagnostic; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +class JavaLangFeatures { + + public static boolean isDiagnosticForUnsupportedFeatures(String diagnosticCode) { + return Singleton.javacParentDiagnosticKeys.contains(diagnosticCode); + } + + public static String getFeatureName(String featureCode) { + Source.Feature feature = Singleton.fragmentCodeToFeature.get(featureCode); + if (feature == null && featureCode.startsWith(Singleton.javacFragmentCodePrefix)) { + feature = Singleton.fragmentCodeToFeature.get(featureCode.substring(Singleton.javacFragmentCodePrefix.length())); + } + return feature == null ? featureCode : feature.name(); + } + + private static class Singleton { + + private static final Map fragmentCodeToFeature; + private static final Set javacParentDiagnosticKeys; + private static final String javacFragmentCodePrefix; + + static { + Map featureFragments = new HashMap<>(); + Set parentDiagnosticKeys = new HashSet<>(); + String prefix = "compiler.misc."; + try { + final JCDiagnostic.Fragment fragment = CompilerProperties.Fragments.FeatureNotSupportedInSource((JCDiagnostic) null, null, null); + final String fragmentKey = fragment.key(); + final String fragmentCode = fragment.getCode(); + if (fragmentKey.startsWith(fragmentCode)) { + prefix = fragmentKey.substring(fragmentCode.length()); + } + + parentDiagnosticKeys.add(fragmentKey); + parentDiagnosticKeys.add(CompilerProperties.Fragments.FeatureNotSupportedInSourcePlural((JCDiagnostic) null, null, null).key()); + parentDiagnosticKeys.add(CompilerProperties.Errors.FeatureNotSupportedInSource((JCDiagnostic) null, null, null).key()); + parentDiagnosticKeys.add(CompilerProperties.Errors.FeatureNotSupportedInSourcePlural((JCDiagnostic) null, null, null).key()); + + parentDiagnosticKeys.add(CompilerProperties.Errors.PreviewFeatureDisabled((JCDiagnostic) null).key()); + parentDiagnosticKeys.add(CompilerProperties.Errors.PreviewFeatureDisabledPlural((JCDiagnostic) null).key()); + parentDiagnosticKeys.add(CompilerProperties.Warnings.PreviewFeatureUse((JCDiagnostic) null).key()); + parentDiagnosticKeys.add(CompilerProperties.Warnings.PreviewFeatureUsePlural((JCDiagnostic) null).key()); + + parentDiagnosticKeys.add(CompilerProperties.Errors.IsPreview(null).key()); + parentDiagnosticKeys.add(CompilerProperties.Warnings.IsPreview(null).key()); + parentDiagnosticKeys.add(CompilerProperties.Warnings.IsPreviewReflective(null).key()); + + for (Source.Feature f : Source.Feature.values()) { + try { + featureFragments.put(f.nameFragment().getCode(), f); + } catch (AssertionError | NullPointerException e) { + // In case no error message code has been registered; for example: LOCAL_VARIABLE_TYPE_INFERENCE + featureFragments.put(f.name(), f); + } + } + } catch (VirtualMachineError e) { + throw e; + } catch (Throwable ignore) { + } + javacFragmentCodePrefix = prefix; + javacParentDiagnosticKeys = parentDiagnosticKeys.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(parentDiagnosticKeys); + fragmentCodeToFeature = featureFragments.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(featureFragments); + } + } + +} diff --git a/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/JavaLangFeaturesTelemetryProvider.java b/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/JavaLangFeaturesTelemetryProvider.java new file mode 100644 index 0000000..8a0043e --- /dev/null +++ b/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/JavaLangFeaturesTelemetryProvider.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2024-2025, Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.netbeans.modules.nbcode.java.lsp.server.telemetry; + +import java.lang.ref.WeakReference; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import org.netbeans.api.editor.mimelookup.MimeRegistration; +import org.netbeans.api.lsp.Diagnostic; +import org.netbeans.modules.java.lsp.server.protocol.LspServerTelemetryManager; +import org.netbeans.spi.lsp.ErrorProvider; +import org.openide.util.NbPreferences; +import org.openide.util.RequestProcessor; +import org.openide.util.RequestProcessor.Task; + +@MimeRegistration(mimeType="text/x-java", service=ErrorProvider.class) +public class JavaLangFeaturesTelemetryProvider implements ErrorProvider { + + static final String PREFERENCES_NODE = "jdk.telemetry"; + static final String PREFERENCES_KEY_DEBOUNCE_TIME = "java-lang-features-debounce"; + static final String PREFERENCES_KEY_CACHE_EXPIRY = "java-lang-features-cache-expiry"; + + static RequestProcessor getRequestProcessor() { + return RPSingleton.instance; + } + + private static int getRequestDebounceTime() { + return RPSingleton.DEBOUNCE_TIME; + } + + private static final class RPSingleton { + private static final RequestProcessor instance = new RequestProcessor(JavaLangFeaturesTelemetryProvider.class.getName(), 10, true, false); + private static final int DEBOUNCE_TIME = Math.max(0, NbPreferences.forModule(JavaLangFeaturesTelemetryProvider.class).node(PREFERENCES_NODE).getInt(PREFERENCES_KEY_DEBOUNCE_TIME, 1000)); // 1 sec + } + + private static final ConcurrentHashMap> sourceAnalysisTasks = new ConcurrentHashMap<>(); + + @Override + public List computeErrors(Context context) { + if (context.errorKind() == ErrorProvider.Kind.HINTS && LspServerTelemetryManager.getInstance().isTelemetryEnabled()) { + final SourceInfo sourceInfo = SourceInfo.getSourceObject(context); + if (sourceInfo.source != null) { + scheduleTask(sourceInfo, JavaLanguageFeaturesEmitter::new); + } + } + return Collections.emptyList(); + } + + private WeakReference scheduleTask(SourceInfo sourceInfo, Function runner) { + String sourceFileName = sourceInfo.getSourceName(); + return sourceAnalysisTasks.compute(sourceFileName, (file, existingTaskRef) -> { + Task existingTask = existingTaskRef == null ? null : existingTaskRef.get(); + final Task task; + final WeakReference taskRef; + if (existingTask == null) { + task = getRequestProcessor().create(runner.apply(sourceInfo)); + taskRef = new WeakReference<>(existingTask); + task.addTaskListener(t -> sourceAnalysisTasks.remove(sourceFileName, taskRef)); + } else { + task = existingTask; + taskRef = existingTaskRef; + } + task.schedule(getRequestDebounceTime()); + return taskRef; + }); + } + + static ConcurrentHashMap> getSourceAnalysisTasks() { + return sourceAnalysisTasks; + } +} diff --git a/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/JavaLanguageFeaturesEmitter.java b/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/JavaLanguageFeaturesEmitter.java new file mode 100644 index 0000000..dfc7022 --- /dev/null +++ b/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/JavaLanguageFeaturesEmitter.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2024-2025, Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.netbeans.modules.nbcode.java.lsp.server.telemetry; + +import com.sun.source.util.JavacTask; +import com.sun.tools.javac.api.ClientCodeWrapper.DiagnosticSourceUnwrapper; +import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.util.JCDiagnostic; +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.tools.DiagnosticListener; +import javax.tools.JavaFileObject; +import javax.tools.ToolProvider; +import org.eclipse.lsp4j.services.LanguageClient; +import org.netbeans.modules.java.lsp.server.protocol.LspServerTelemetryManager; + +class JavaLanguageFeaturesEmitter implements Runnable { + private static final Logger LOG = Logger.getLogger(JavaLanguageFeaturesEmitter.class.getName()); + + private final SourceInfo sourceInfo; + + JavaLanguageFeaturesEmitter(SourceInfo sourceInfo) { + this.sourceInfo = sourceInfo; + } + + @Override + public void run() { + Set featuresUsed = checkJavaFeatures(); + if (!featuresUsed.isEmpty() && SourceFeatureCache.add(sourceInfo.getProjectName(), featuresUsed)) { + final LanguageClient client = sourceInfo.getLanguageClient(); + final SourceFeatureCache.SourceFeatureCacheEntry cached = SourceFeatureCache.get(sourceInfo.getProjectName()); + final boolean previewEnabled = cached == null ? sourceInfo.getPreviewEnabled() : cached.isPreviewEnabled(sourceInfo); + final JdkFeatureEvent event = new JdkFeatureEvent.Builder() + .setJavaVersion(sourceInfo.getJavaVersion()) + .setIsPreviewEnabled(previewEnabled) + .setNames(featuresUsed) + .build(); + if (client == null) { + LspServerTelemetryManager.getInstance().sendTelemetry(event); + } else { + LspServerTelemetryManager.getInstance().sendTelemetry(client, event); + } + } + } + + Set checkJavaFeatures() { + Set featuresUsed = new HashSet<>(); + DiagnosticListener dl = d -> { + //this is not an API, requires access to internals: + addNewJavaFeaturesUsed(featuresUsed, + d instanceof DiagnosticSourceUnwrapper ? ((DiagnosticSourceUnwrapper) d).d + : d instanceof JCDiagnostic ? (JCDiagnostic) d + : null); + }; + JavacTask task; + try { + task = (JavacTask) ToolProvider.getSystemJavaCompiler().getTask(null, null, dl, List.of("--source", "8", "--source-path", sourceInfo.getSourcesPath()), null, List.of(sourceInfo.source)); + task.analyze(); + } catch (IOException e) { + LOG.log(Level.FINE, "IO error while scanning Java Language features: {0}", e); + } catch (IllegalArgumentException e) { + LOG.log(Level.CONFIG, "Invalid parsing parameters for scanning Java Language features: {0}", e); + } catch (RuntimeException ignored) { + } + return featuresUsed; + } + + void addNewJavaFeaturesUsed(Set featuresUsed, JCDiagnostic jcDiag) { + if (jcDiag == null) + return; + if (JavaLangFeatures.isDiagnosticForUnsupportedFeatures(jcDiag.getCode())) { + if (jcDiag.getArgs().length > 0) { + if (jcDiag.getArgs()[0] instanceof JCDiagnostic) { + featuresUsed.add(JavaLangFeatures.getFeatureName(((JCDiagnostic) jcDiag.getArgs()[0]).getCode())); + } else if (jcDiag.getArgs()[0] instanceof JCDiagnostic.DiagnosticInfo) { + featuresUsed.add(JavaLangFeatures.getFeatureName(((JCDiagnostic.DiagnosticInfo) jcDiag.getArgs()[0]).getCode())); + } else if (jcDiag.getArgs()[0] instanceof Symbol) { + featuresUsed.add(JavaLangFeatures.getFeatureName(((Symbol) jcDiag.getArgs()[0]).getSimpleName().toString())); + } + } + return; + } + for (Object arg : jcDiag.getArgs()) { + if (arg instanceof JCDiagnostic) + addNewJavaFeaturesUsed(featuresUsed, (JCDiagnostic) arg); + } + } +} diff --git a/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/JdkFeatureEvent.java b/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/JdkFeatureEvent.java new file mode 100644 index 0000000..a24ac31 --- /dev/null +++ b/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/JdkFeatureEvent.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2024-2025, Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.netbeans.modules.nbcode.java.lsp.server.telemetry; + +import java.util.Set; +import org.eclipse.lsp4j.MessageType; +import org.netbeans.modules.java.lsp.server.protocol.TelemetryEvent; + +public class JdkFeatureEvent extends TelemetryEvent { + + public static final String JDK_FEATURES_EVT = "jdkFeature"; + + public JdkFeatureEvent(String jsonString) { + super(MessageType.Info.toString(), JDK_FEATURES_EVT, jsonString); + } + + public JdkFeatureEvent(JdkFeatures features) { + super(MessageType.Info.toString(), JDK_FEATURES_EVT, features); + } + + public static class JdkFeatures { + private String javaVersion; + private Boolean isPreviewEnabled; + private Set names; + private Set jeps; + + public String getJavaVersion() { + return javaVersion; + } + + public void setJavaVersion(String javaVersion) { + this.javaVersion = javaVersion; + } + + public Boolean getIsPreviewEnabled() { + return isPreviewEnabled; + } + + public void setIsPreviewEnabled(Boolean isPreviewEnabled) { + this.isPreviewEnabled = isPreviewEnabled; + } + + public Set getNames() { + return names; + } + + public void setNames(Set names) { + this.names = names; + } + + public Set getJeps() { + return jeps; + } + + public void setJeps(Set jeps) { + this.jeps = jeps; + } + + } + + public static class Builder { + private final JdkFeatures properties = new JdkFeatures(); + + public Builder setJavaVersion(String javaVersion) { + properties.setJavaVersion(javaVersion); + return this; + } + + public Builder setNames(Set names) { + properties.setNames(names); + return this; + } + + public Builder setJeps(Set jeps) { + properties.setJeps(jeps); + return this; + } + + public Builder setIsPreviewEnabled(boolean previewEnabled) { + properties.setIsPreviewEnabled(previewEnabled); + return this; + } + + public JdkFeatureEvent build() { + return new JdkFeatureEvent(properties); + } + } + +} diff --git a/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/SourceFeatureCache.java b/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/SourceFeatureCache.java new file mode 100644 index 0000000..31e392e --- /dev/null +++ b/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/SourceFeatureCache.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2024-2025, Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.netbeans.modules.nbcode.java.lsp.server.telemetry; + +import java.util.Collections; +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; +import org.openide.util.NbPreferences; +import org.openide.util.RequestProcessor.Task; + +class SourceFeatureCache { + + static class SourceFeatureCacheEntry { + + private final long timestamp; + private final Set featuresUsed; + private final AtomicReference previewEnabled; + + public SourceFeatureCacheEntry(long timestamp, Set featuresUsed) { + this(timestamp, featuresUsed, null); + } + + protected SourceFeatureCacheEntry(long timestamp, Set featuresUsed, SourceFeatureCacheEntry copy) { + this.timestamp = timestamp; + this.featuresUsed = featuresUsed == null ? Collections.emptySet() : featuresUsed; + this.previewEnabled = new AtomicReference<>(copy == null ? null : copy.previewEnabled.get()); + } + + public long getTimestamp() { + return timestamp; + } + + public Set getFeaturesUsed() { + return featuresUsed; + } + + public boolean isPreviewEnabled(SourceInfo sourceInfo) { + Boolean value = previewEnabled.get(); + if (value == null) { + value = sourceInfo.getPreviewEnabled(); + if (!previewEnabled.compareAndSet(null, value)) + value = previewEnabled.get(); + } + return value; + } + } + + private static class Singleton { + + private static final int CACHE_EXPIRY = Math.max(0, NbPreferences.forModule(JavaLangFeaturesTelemetryProvider.class).node(JavaLangFeaturesTelemetryProvider.PREFERENCES_NODE).getInt(JavaLangFeaturesTelemetryProvider.PREFERENCES_KEY_CACHE_EXPIRY, 3_600_000)); // 1 hour + private static final ConcurrentHashMap cachedSourceFeatures = new ConcurrentHashMap<>(); + } + + static ConcurrentHashMap getCachedSourceFeatures() { + return Singleton.cachedSourceFeatures; + } + + public static SourceFeatureCacheEntry get(String sourceName) { + return Singleton.cachedSourceFeatures.get(sourceName); + } + + public static boolean add(String sourceName, Set features) { + final Set newFeatures = Collections.unmodifiableSet(features); + final SourceFeatureCacheEntry entry = getCachedSourceFeatures().compute(sourceName, + (name, cache) -> cache != null && cache.getFeaturesUsed().containsAll(newFeatures) ? cache + : new SourceFeatureCacheEntry(System.currentTimeMillis(), newFeatures, cache)); + + boolean added = newFeatures == entry.getFeaturesUsed(); + if (!added) { + SourceFeatureCacheCleaner.delay(); + } + return added; + } + + private static class SourceFeatureCacheCleaner implements Runnable { + + private static final int CLEANER_DELAY = Math.max(10_000, Singleton.CACHE_EXPIRY / 10); // 10 times/expiry period; min. 10secs. + private static final Task cacheCleaner = JavaLangFeaturesTelemetryProvider.getRequestProcessor().create(new SourceFeatureCacheCleaner(), true); + + static { + cacheCleaner.schedule(CLEANER_DELAY); + } + + static void delay() { + cacheCleaner.schedule(CLEANER_DELAY); + } + + @Override + public void run() { + final long cleanBeforeTime = System.currentTimeMillis() - Singleton.CACHE_EXPIRY; + final Iterator iterator = getCachedSourceFeatures().values().iterator(); + while (iterator.hasNext()) { + if (iterator.next().getTimestamp() < cleanBeforeTime) { + iterator.remove(); + } + } + cacheCleaner.schedule(CLEANER_DELAY); + } + + } +} diff --git a/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/SourceInfo.java b/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/SourceInfo.java new file mode 100644 index 0000000..9c6adac --- /dev/null +++ b/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/SourceInfo.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2024-2025, Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.netbeans.modules.nbcode.java.lsp.server.telemetry; + +import java.io.File; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.net.URI; +import java.util.Map; +import java.util.function.Function; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; +import org.eclipse.lsp4j.services.LanguageClient; +import org.netbeans.api.java.platform.JavaPlatform; +import org.netbeans.api.java.project.JavaProjectConstants; +import org.netbeans.api.project.FileOwnerQuery; +import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectUtils; +import org.netbeans.api.project.SourceGroup; +import org.netbeans.api.project.Sources; +import org.netbeans.modules.java.lsp.server.protocol.LspServerTelemetryManager; +import org.netbeans.spi.lsp.ErrorProvider; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.util.Lookup; + +class SourceInfo { + + private final FileObject file; + private final Project owner; + final JavaFileObject source; + /** + * A transient reference cache to the LanguageClient associated with this source. + * This does not need to be made concurrency-safe since the source is expected + * to be associated with a single client during its lifetime here. Further, + * it can be queried and rewritten again safely, especially across threads. + * So, there is no need to make this an atomic reference. + * Finally, it needs to be a weak-reference so that the client resources are + * released as soon as possible. + */ + private transient WeakReference client; + + public SourceInfo(FileObject file, Project owner, JavaFileObject source) { + this.file = file; + this.owner = owner; + this.source = source; + } + + public String getSourceName() { + return source == null ? "" : source.getName(); + } + + public String getProjectName() { + return owner == null ? "" : ProjectUtils.getInformation(ProjectUtils.rootOf(owner)).getName(); + } + + public String getSourcesPath() { + if (owner != null) { + final Sources sources = ProjectUtils.getSources(owner); + SourceGroup[] sourceGroups = sources.getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA); + if (sourceGroups.length == 0) { + sourceGroups = sources.getSourceGroups(Sources.TYPE_GENERIC); + } + StringBuilder sb = new StringBuilder(); + for (SourceGroup group : sourceGroups) { + final File root = FileUtil.toFile(group.getRootFolder()); + if (root != null) { + if (!sb.isEmpty()) { + sb.append(File.pathSeparatorChar); + } + sb.append(root.getAbsolutePath()); + } + } + return sb.toString(); + } else { + final File parent = FileUtil.toFile(file.getParent()); + return parent == null ? "." : parent.getAbsolutePath(); + } + } + + public LanguageClient getLanguageClient() { + LanguageClient client = this.client == null ? null : this.client.get(); + if (client == null) { + client = Lookup.getDefault().lookup(LanguageClient.class); + if (client != null) + this.client = new WeakReference<>(client); + } + return client; + } + + public String getJavaVersion() { + final JavaPlatform defaultPlatform = JavaPlatform.getDefault(); + final Map systemProperties = defaultPlatform.getSystemProperties(); + Function lookupFunction = systemProperties == null ? System::getProperty : systemProperties::get; + return LspServerTelemetryManager.getJavaRuntimeVersion(lookupFunction); + } + + public boolean getPreviewEnabled() { + return LspServerTelemetryManager.getInstance().isPreviewEnabled(file, + owner == null ? LspServerTelemetryManager.ProjectType.standalone : LspServerTelemetryManager.getInstance().getProjectType(owner), + getLanguageClient()); + } + + public static SourceInfo getSourceObject(ErrorProvider.Context context) { + final FileObject file = context.file(); + final Project owner = FileOwnerQuery.getOwner(file); + JavaFileObject source = null; + try { + source = new BasicJavaFileObject(file.toURI(), file.asText()); + } catch (IOException | IllegalArgumentException ignore) { + } + return new SourceInfo(file, owner, source); + } + + static class BasicJavaFileObject extends SimpleJavaFileObject { + + private final String content; + + public BasicJavaFileObject(URI uri, String content) { + super(uri, Kind.SOURCE); + this.content = content; + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { + return content; + } + } +} diff --git a/nbcode/telemetry/test/unit/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/JavaLanguageFeaturesEmitterTest.java b/nbcode/telemetry/test/unit/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/JavaLanguageFeaturesEmitterTest.java new file mode 100644 index 0000000..52eb2fd --- /dev/null +++ b/nbcode/telemetry/test/unit/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/JavaLanguageFeaturesEmitterTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2024-2025, Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.netbeans.modules.nbcode.java.lsp.server.telemetry; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.junit.Test; +import org.netbeans.junit.NbTestCase; +import org.openide.filesystems.FileUtil; + +public class JavaLanguageFeaturesEmitterTest extends NbTestCase { + + public JavaLanguageFeaturesEmitterTest(String name) { + super(name); + } + + @Test + public void testDiagToFeatureName() { + List diagCodes = Arrays.asList("feature.records", "feature.switch.rules", "feature.diamond.and.anon.class", "feature.pattern.switch", "feature.switch.expressions", "feature.xyz"); + List featNames = Arrays.asList("RECORDS", "SWITCH_RULE", "DIAMOND_WITH_ANONYMOUS_CLASS_CREATION", "PATTERN_SWITCH", "SWITCH_EXPRESSION", "feature.xyz"); + for (int i = 0; i < diagCodes.size() ; i++) { + String code = diagCodes.get(i); + String name = JavaLangFeatures.getFeatureName(code); + assertEquals(featNames.get(i), name); + } + } + + @Test + public void testCheckJavaFeatures() { + SourceInfo sourceInfo; + try { + sourceInfo = new SourceInfo(FileUtil.toFileObject(File.createTempFile("test", ".java")), null, new SourceInfo.BasicJavaFileObject(new URI("mem://test.java"), getTestCode1())); + } catch (IOException | URISyntaxException ex) { + fail(ex.toString()); + return; + } + JavaLanguageFeaturesEmitter instance = new JavaLanguageFeaturesEmitter(sourceInfo); + Set expResult = new HashSet<>(Arrays.asList("RECORDS", "DIAMOND_WITH_ANONYMOUS_CLASS_CREATION", "SWITCH_EXPRESSION", "SWITCH_RULE", "PATTERN_SWITCH")); + Set result = instance.checkJavaFeatures(); + assertEquals(expResult, result); + } + + private String getTestCode1() { + return " import java.util.*;\n" + + " public record R(int i) {\n" + + " private int t(Object o) {\n" + + " List l = new ArrayList<>() {};\n" + + " return switch (o) { default -> 0; }\n" + + " }\n" + + " }"; + } +} diff --git a/vscode/src/lsp/launchOptions.ts b/vscode/src/lsp/launchOptions.ts index 63e93b0..b909330 100644 --- a/vscode/src/lsp/launchOptions.ts +++ b/vscode/src/lsp/launchOptions.ts @@ -55,6 +55,7 @@ const extraLaunchOptions = [ "--start-java-language-server=listen-hash:0", "--start-java-debug-adapter-server=listen-hash:0", "-J-DTopSecurityManager.disable=true", + "-J--add-exports=jdk.compiler/com.sun.tools.javac.resources=ALL-UNNAMED", "-J--enable-native-access=ALL-UNNAMED" ]; From a73ccc0b8898ce4bf85c6557cc446f7bac8fdd4f Mon Sep 17 00:00:00 2001 From: Achal Talati Date: Tue, 25 Feb 2025 10:55:34 +0530 Subject: [PATCH 07/16] fixed label and id issue in JDK downloader --- vscode/l10n/bundle.l10n.en.json | 2 ++ vscode/l10n/bundle.l10n.ja.json | 2 ++ vscode/l10n/bundle.l10n.zh-cn.json | 2 ++ vscode/src/webviews/jdkDownloader/action.ts | 6 +++--- vscode/src/webviews/jdkDownloader/view.ts | 17 +++++++++++------ 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/vscode/l10n/bundle.l10n.en.json b/vscode/l10n/bundle.l10n.en.json index 627827d..2f913dc 100644 --- a/vscode/l10n/bundle.l10n.en.json +++ b/vscode/l10n/bundle.l10n.en.json @@ -3,6 +3,8 @@ "jdk.downloader.html.details":"

This tool enables you to download either the latest Oracle Java SE JDK with Oracle No-Fee Terms and Conditions or the Oracle OpenJDK builds under the GNU Public License with ClassPath Exception

It will then handle the installation and configuration on your behalf.

This enables you to take full advantage of all the features offered by this extension.

", "jdk.downloader.button.label.oracleJdk": "Download Oracle Java SE JDK", "jdk.downloader.label.or": "or", + "jdk.downloader.label.openJdk": "OpenJDK", + "jdk.downloader.label.oracleJdk": "Oracle JDK", "jdk.downloader.button.label.openJdk": "Download Oracle OpenJDK", "jdk.downloader.button.label.selectJdkFromSystem": "Select installed JDK from my system", "jdk.downloader.label.selectOracleJdkVersion": "Select Oracle Java SE Version", diff --git a/vscode/l10n/bundle.l10n.ja.json b/vscode/l10n/bundle.l10n.ja.json index 361e1ed..a966b88 100755 --- a/vscode/l10n/bundle.l10n.ja.json +++ b/vscode/l10n/bundle.l10n.ja.json @@ -3,6 +3,8 @@ "jdk.downloader.html.details":"

このツールは、Oracle No-Fee Terms and Conditionsの最新のOracle Java SE JDKまたは、クラスパス例外付きGNU Public Licenseに基づいたOracle OpenJDKビルドのいずれかをダウンロードできます

次に、インストールおよび構成をかわりに処理します。

これにより、この拡張によって提供されたすべての機能を最大限活用できます。

", "jdk.downloader.button.label.oracleJdk": "Oracle Java SE JDKのダウンロード", "jdk.downloader.label.or": "または", + "jdk.downloader.label.openJdk": "OpenJDK", + "jdk.downloader.label.oracleJdk": "Oracle JDK", "jdk.downloader.button.label.openJdk": "Oracle OpenJDKのダウンロード", "jdk.downloader.button.label.selectJdkFromSystem": "システムからインストール済JDKの選択", "jdk.downloader.label.selectOracleJdkVersion": "Oracle Java SEバージョンの選択", diff --git a/vscode/l10n/bundle.l10n.zh-cn.json b/vscode/l10n/bundle.l10n.zh-cn.json index d095ec1..9551f2a 100755 --- a/vscode/l10n/bundle.l10n.zh-cn.json +++ b/vscode/l10n/bundle.l10n.zh-cn.json @@ -3,6 +3,8 @@ "jdk.downloader.html.details":"

使用此工具,您可以遵循 Oracle 免费条款和条件下载最新的 Oracle Java SE JDK,或者依照 GNU 公共许可证(包含 ClassPath 例外条款)下载 Oracle OpenJDK 构建

之后,它将代表您处理安装和配置。

这样,您可以充分利用此扩展提供的所有功能。

", "jdk.downloader.button.label.oracleJdk": "下载 Oracle Java SE JDK", "jdk.downloader.label.or": "或", + "jdk.downloader.label.openJdk": "OpenJDK", + "jdk.downloader.label.oracleJdk": "Oracle JDK", "jdk.downloader.button.label.openJdk": "下载 Oracle OpenJDK", "jdk.downloader.button.label.selectJdkFromSystem": "从我的系统选择安装的 JDK", "jdk.downloader.label.selectOracleJdkVersion": "选择 Oracle Java SE 版本", diff --git a/vscode/src/webviews/jdkDownloader/action.ts b/vscode/src/webviews/jdkDownloader/action.ts index f664a9f..8ea1e65 100644 --- a/vscode/src/webviews/jdkDownloader/action.ts +++ b/vscode/src/webviews/jdkDownloader/action.ts @@ -49,7 +49,7 @@ export class JdkDownloaderAction { public attachListener = async (message: any) => { const { command, id, jdkVersion, jdkOS, jdkArch, installType } = message; - if (command === JdkDownloaderView.DOWNLOAD_CMD_LABEL) { + if (command === JdkDownloaderView.DOWNLOAD_CMD) { LOGGER.log(`Request received for downloading ${id} version ${jdkVersion}`); this.jdkType = id; @@ -160,10 +160,10 @@ export class JdkDownloaderAction { private generateDownloadUrl = (): string => { let baseDownloadUrl: string = ''; - if (this.jdkType === JdkDownloaderView.OPEN_JDK_LABEL) { + if (this.jdkType === JdkDownloaderView.JDK_TYPE.openJdk) { baseDownloadUrl = `${jdkDownloaderConstants.OPEN_JDK_VERSION_DOWNLOAD_LINKS[`${this.jdkVersion}`]}_${this.osType!.toLowerCase()}-${this.machineArch}_bin`; } - else if (this.jdkType === JdkDownloaderView.ORACLE_JDK_LABEL) { + else if (this.jdkType === JdkDownloaderView.JDK_TYPE.oracleJdk) { baseDownloadUrl = `${jdkDownloaderConstants.ORACLE_JDK_BASE_DOWNLOAD_URL}/${this.jdkVersion}/latest/jdk-${this.jdkVersion}_${this.osType!.toLowerCase()}-${this.machineArch}_bin`; } const downloadUrl = this.osType === 'windows' ? `${baseDownloadUrl}.zip` : `${baseDownloadUrl}.tar.gz`; diff --git a/vscode/src/webviews/jdkDownloader/view.ts b/vscode/src/webviews/jdkDownloader/view.ts index 48126ad..f569501 100644 --- a/vscode/src/webviews/jdkDownloader/view.ts +++ b/vscode/src/webviews/jdkDownloader/view.ts @@ -23,9 +23,14 @@ import { l10n } from '../../localiser'; import { LOGGER } from '../../logger'; export class JdkDownloaderView { - public static readonly OPEN_JDK_LABEL = "OpenJDK"; - public static readonly ORACLE_JDK_LABEL = "Oracle JDK"; - public static readonly DOWNLOAD_CMD_LABEL = 'downloadJDK'; + public static readonly DOWNLOAD_CMD = 'downloadJDK'; + public static readonly JDK_TYPE = { + oracleJdk: "oracleJdk", + openJdk: "openJdk", + } + + private static readonly OPEN_JDK_LABEL = l10n.value("jdk.downloader.label.openJdk"); + private static readonly ORACLE_JDK_LABEL = l10n.value("jdk.downloader.label.oracleJdk"); private readonly jdkDownloaderTitle = l10n.value("jdk.downloader.heading"); private jdkDownloaderWebView?: WebviewPanel; @@ -222,7 +227,7 @@ export class JdkDownloaderView { document.getElementById("addJDKPathManually")?.addEventListener('click', event => { vscode.postMessage({ - command: "${JdkDownloaderView.DOWNLOAD_CMD_LABEL}", + command: "${JdkDownloaderView.DOWNLOAD_CMD}", installType: "${JdkDownloaderAction.MANUAL_INSTALLATION_TYPE}", }); }); @@ -276,9 +281,9 @@ export class JdkDownloaderView { const triggerJDKDownload = (e) => { const { id } = e.target; - const jdkType = id === openJdkButtonId+'DownloadButton' ? "${JdkDownloaderView.OPEN_JDK_LABEL}" : "${JdkDownloaderView.ORACLE_JDK_LABEL}"; + const jdkType = id === openJdkButtonId+'DownloadButton' ? "${JdkDownloaderView.JDK_TYPE.openJdk}" : "${JdkDownloaderView.JDK_TYPE.oracleJdk}"; vscode.postMessage({ - command: "${JdkDownloaderView.DOWNLOAD_CMD_LABEL}", + command: "${JdkDownloaderView.DOWNLOAD_CMD}", id: jdkType, installType: "${JdkDownloaderAction.AUTO_INSTALLATION_TYPE}", jdkVersion: document.getElementById(activeButton.id+'VersionDropdown').value, From 83a76722ec2686da77e724fa0f917625dc8d8d31 Mon Sep 17 00:00:00 2001 From: Achal Talati Date: Tue, 18 Mar 2025 00:29:23 +0530 Subject: [PATCH 08/16] Fixed label issue in JDK Downloader --- vscode/src/webviews/jdkDownloader/action.ts | 18 +++++++++--------- vscode/src/webviews/jdkDownloader/view.ts | 10 +++++++--- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/vscode/src/webviews/jdkDownloader/action.ts b/vscode/src/webviews/jdkDownloader/action.ts index 8ea1e65..ecd9943 100644 --- a/vscode/src/webviews/jdkDownloader/action.ts +++ b/vscode/src/webviews/jdkDownloader/action.ts @@ -36,7 +36,7 @@ export class JdkDownloaderAction { private readonly DOWNLOAD_DIR = path.join(__dirname, 'jdk_downloads'); private startTimer: number | null = null; - private jdkType?: string; + private jdkType!: string; private jdkVersion?: string; private osType?: string; private machineArch?: string; @@ -132,7 +132,7 @@ export class JdkDownloaderAction { private jdkInstallationManager = async () => { const startingInstallationMessage = l10n.value("jdk.downloader.message.downloadingAndCompletingSetup", { - jdkType: this.jdkType, + jdkType: JdkDownloaderView.getJdkLabel(this.jdkType), jdkVersion: this.jdkVersion }); @@ -144,7 +144,7 @@ export class JdkDownloaderAction { } await this.downloadAndVerify(); const downloadSuccessLabel = l10n.value("jdk.downloader.message.downloadCompleted", { - jdkType: this.jdkType, + jdkType: JdkDownloaderView.getJdkLabel(this.jdkType), jdkVersion: this.jdkVersion, osType: this.osType }); @@ -187,7 +187,7 @@ export class JdkDownloaderAction { private downloadAndVerify = async (): Promise => { const message = l10n.value("jdk.downloader.message.downloadProgressBar", { - jdkType: this.jdkType, + jdkType: JdkDownloaderView.getJdkLabel(this.jdkType), jdkVersion: this.jdkVersion }); await downloadFileWithProgressBar(this.downloadUrl!, this.downloadFilePath!, message); @@ -196,7 +196,7 @@ export class JdkDownloaderAction { const doesMatch = await this.checksumMatch(); if (!doesMatch) { const checksumMatchFailedLabel = l10n.value("jdk.downloader.message.downloadFailed", { - jdkType: this.jdkType, + jdkType: JdkDownloaderView.getJdkLabel(this.jdkType), jdkVersion: this.jdkVersion, osType: this.osType }); @@ -230,7 +230,7 @@ export class JdkDownloaderAction { } catch (err) { LOGGER.error(`Error while extracting JDK: ${(err as Error).message}`); throw new Error(l10n.value("jdk.downloader.error_message.extractionError", { - jdkType: this.jdkType, + jdkType: JdkDownloaderView.getJdkLabel(this.jdkType), jdkVersion: this.jdkVersion })); } @@ -250,11 +250,11 @@ export class JdkDownloaderAction { const tempDirectoryPath = path.join(this.DOWNLOAD_DIR, matchingJdkDir[0]); // If directory with same name is present in the user selected download location then ask user if they want to delete it or not? - const newDirName = `${this.jdkType!.split(' ').join('_')}-${this.jdkVersion}`; + const newDirName = `${this.jdkType.split(' ').join('_')}-${this.jdkVersion}`; const newDirectoryPath = await this.handleJdkPaths(newDirName, this.installationPath!, this.osType!); if (newDirectoryPath === null) { throw new Error(l10n.value('jdk.downloader.error_message.jdkNewDirectoryIssueCannotInstall', { - jdkType: this.jdkType, + jdkType: JdkDownloaderView.getJdkLabel(this.jdkType), jdkVersion: this.jdkVersion, newDirName })); @@ -278,7 +278,7 @@ export class JdkDownloaderAction { private installationCleanup = (tempDirPath: string, newDirPath: string) => { const currentTime = getCurrentUTCDateInSeconds(); const downloadTelemetryEvent: JdkDownloadEventData = { - vendor: this.jdkType!, + vendor: this.jdkType, version: this.jdkVersion!, os: this.osType!, arch: this.machineArch!, diff --git a/vscode/src/webviews/jdkDownloader/view.ts b/vscode/src/webviews/jdkDownloader/view.ts index f569501..f410479 100644 --- a/vscode/src/webviews/jdkDownloader/view.ts +++ b/vscode/src/webviews/jdkDownloader/view.ts @@ -26,11 +26,15 @@ export class JdkDownloaderView { public static readonly DOWNLOAD_CMD = 'downloadJDK'; public static readonly JDK_TYPE = { oracleJdk: "oracleJdk", - openJdk: "openJdk", + openJdk: "openJdk" + } + + public static getJdkLabel = (id: string): string => { + const key = "jdk.downloader.label." + id; + const label = l10n.value(key); + return label !== key ? label : id; } - private static readonly OPEN_JDK_LABEL = l10n.value("jdk.downloader.label.openJdk"); - private static readonly ORACLE_JDK_LABEL = l10n.value("jdk.downloader.label.oracleJdk"); private readonly jdkDownloaderTitle = l10n.value("jdk.downloader.heading"); private jdkDownloaderWebView?: WebviewPanel; From da7958a0c42cb8c9cf509960883745845008dd1d Mon Sep 17 00:00:00 2001 From: Achal Talati Date: Tue, 8 Apr 2025 12:12:23 +0530 Subject: [PATCH 09/16] Added 429 error code retryable support --- vscode/src/telemetry/impl/telemetryRetry.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vscode/src/telemetry/impl/telemetryRetry.ts b/vscode/src/telemetry/impl/telemetryRetry.ts index c8a29db..0ee6302 100644 --- a/vscode/src/telemetry/impl/telemetryRetry.ts +++ b/vscode/src/telemetry/impl/telemetryRetry.ts @@ -99,6 +99,10 @@ export class TelemetryRetry { return false; } + private isEventRetryable = (statusCode: number): boolean => { + return statusCode <= 0 || statusCode > 500 || statusCode == 429; + } + public eventsToBeEnqueuedAgain = (eventResponses: TelemetryPostResponse): BaseEvent[] => { eventResponses.success.forEach(res => { res.event.onSuccessPostEventCallback(); @@ -110,7 +114,7 @@ export class TelemetryRetry { } else { const eventsToBeEnqueuedAgain: BaseEvent[] = []; eventResponses.failures.forEach((eventRes) => { - if (eventRes.statusCode <= 0 || eventRes.statusCode > 500) + if (this.isEventRetryable(eventRes.statusCode)) eventsToBeEnqueuedAgain.push(eventRes.event); }); From d30e0611f602a3f483ecf883a443d635a6fca1d5 Mon Sep 17 00:00:00 2001 From: Achal Talati Date: Tue, 15 Apr 2025 14:47:07 +0530 Subject: [PATCH 10/16] updated telemetry prefs and reporter --- vscode/esbuild.js | 6 + vscode/package.json | 5 +- vscode/src/telemetry/config.ts | 11 +- vscode/src/telemetry/constants.ts | 3 + vscode/src/telemetry/impl/cacheServiceImpl.ts | 1 + .../src/telemetry/impl/telemetryEventQueue.ts | 17 +- vscode/src/telemetry/impl/telemetryPrefs.ts | 172 +++++++++++++++--- .../telemetry/impl/telemetryReporterImpl.ts | 13 +- vscode/src/telemetry/impl/telemetryRetry.ts | 24 ++- vscode/src/telemetry/telemetry.ts | 2 +- vscode/src/telemetry/telemetryManager.ts | 38 ++-- vscode/src/telemetry/types.ts | 14 +- 12 files changed, 246 insertions(+), 60 deletions(-) create mode 100644 vscode/src/telemetry/constants.ts diff --git a/vscode/esbuild.js b/vscode/esbuild.js index 80873aa..61db24a 100644 --- a/vscode/esbuild.js +++ b/vscode/esbuild.js @@ -57,6 +57,9 @@ const createTelemetryConfig = () => { baseUrl: null, baseEndpoint: "/vscode/java/sendTelemetry", version: "/v1" + }, + metadata: { + consentSchemaVersion: "v1" } } @@ -73,6 +76,9 @@ const createTelemetryConfig = () => { baseUrl: process.env.TELEMETRY_API_BASE_URL, baseEndpoint: process.env.TELEMETRY_API_ENDPOINT, version: process.env.TELEMETRY_API_VERSION + }, + metadata: { + consentSchemaVersion: process.env.CONSENT_SCHEMA_VERSION } }); diff --git a/vscode/package.json b/vscode/package.json index 230b8d4..0a9a7e7 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -245,7 +245,10 @@ "jdk.telemetry.enabled": { "type": "boolean", "description": "%jdk.configuration.telemetry.enabled.description%", - "default": false + "default": false, + "tags": [ + "telemetry" + ] } } }, diff --git a/vscode/src/telemetry/config.ts b/vscode/src/telemetry/config.ts index b087bae..eda1b56 100644 --- a/vscode/src/telemetry/config.ts +++ b/vscode/src/telemetry/config.ts @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { RetryConfig, TelemetryApi } from "./types"; +import { RetryConfig, TelemetryApi, TelemetryConfigMetadata } from "./types"; import * as path from 'path'; import * as fs from 'fs'; import { LOGGER } from "../logger"; @@ -24,6 +24,7 @@ export class TelemetryConfiguration { private static instance: TelemetryConfiguration; private retryConfig!: RetryConfig; private apiConfig!: TelemetryApi; + private metadata!: TelemetryConfigMetadata; public constructor() { this.initialize(); @@ -54,6 +55,11 @@ export class TelemetryConfiguration { baseEndpoint: config.telemetryApi.baseEndpoint, version: config.telemetryApi.version }); + + this.metadata = Object.freeze({ + consentSchemaVersion: config.metadata.consentSchemaVersion + }); + } catch (error: any) { LOGGER.error("Error occurred while setting up telemetry config"); LOGGER.error(error.message); @@ -68,4 +74,7 @@ export class TelemetryConfiguration { return this.apiConfig; } + public getTelemetryConfigMetadata(): Readonly { + return this.metadata; + } } \ No newline at end of file diff --git a/vscode/src/telemetry/constants.ts b/vscode/src/telemetry/constants.ts new file mode 100644 index 0000000..e90d441 --- /dev/null +++ b/vscode/src/telemetry/constants.ts @@ -0,0 +1,3 @@ +export const TELEMETRY_CONSENT_VERSION_SCHEMA_KEY = "telemetryConsentSchemaVersion"; +export const TELEMETRY_CONSENT_POPUP_TIME_KEY = "telemetryConsentPopupTime"; +export const TELEMETRY_SETTING_VALUE_KEY = "telemetrySettingValue"; \ No newline at end of file diff --git a/vscode/src/telemetry/impl/cacheServiceImpl.ts b/vscode/src/telemetry/impl/cacheServiceImpl.ts index e7aaf6a..94c12d8 100644 --- a/vscode/src/telemetry/impl/cacheServiceImpl.ts +++ b/vscode/src/telemetry/impl/cacheServiceImpl.ts @@ -32,6 +32,7 @@ class CacheServiceImpl implements CacheService { try { const vscGlobalState = globalState.getExtensionContextInfo().getVscGlobalState(); vscGlobalState.update(key, value); + LOGGER.debug(`Updating key: ${key} to ${value}`); return true; } catch (err) { LOGGER.error(`Error while storing ${key} in cache: ${(err as Error).message}`); diff --git a/vscode/src/telemetry/impl/telemetryEventQueue.ts b/vscode/src/telemetry/impl/telemetryEventQueue.ts index af5fd7d..667e8f7 100644 --- a/vscode/src/telemetry/impl/telemetryEventQueue.ts +++ b/vscode/src/telemetry/impl/telemetryEventQueue.ts @@ -15,7 +15,7 @@ */ import { BaseEvent } from "../events/baseEvent"; -export class TelemetryEventQueue { +export class TelemetryEventQueue { private events: BaseEvent[] = []; public enqueue = (e: BaseEvent): void => { @@ -35,4 +35,19 @@ export class TelemetryEventQueue { this.events = []; return queue; } + + public decreaseSizeOnMaxOverflow = () => { + const seen = new Set(); + const newQueueStart = Math.floor(this.size() / 2); + + const secondHalf = this.events.slice(newQueueStart); + + const uniqueEvents = secondHalf.filter(event => { + if (seen.has(event.NAME)) return false; + seen.add(event.NAME); + return true; + }); + + this.events = [...uniqueEvents, ...secondHalf]; + } } \ No newline at end of file diff --git a/vscode/src/telemetry/impl/telemetryPrefs.ts b/vscode/src/telemetry/impl/telemetryPrefs.ts index c7213f0..eae6c5f 100644 --- a/vscode/src/telemetry/impl/telemetryPrefs.ts +++ b/vscode/src/telemetry/impl/telemetryPrefs.ts @@ -13,52 +13,166 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ConfigurationChangeEvent, env, workspace } from "vscode"; +import { ConfigurationChangeEvent, env, workspace, Disposable } from "vscode"; import { getConfigurationValue, inspectConfiguration, updateConfigurationValue } from "../../configurations/handlers"; import { configKeys } from "../../configurations/configuration"; import { appendPrefixToCommand } from "../../utils"; +import { ExtensionContextInfo } from "../../extensionContextInfo"; +import { TelemetryPreference } from "../types"; +import { cacheService } from "./cacheServiceImpl"; +import { TELEMETRY_CONSENT_POPUP_TIME_KEY, TELEMETRY_CONSENT_VERSION_SCHEMA_KEY, TELEMETRY_SETTING_VALUE_KEY } from "../constants"; +import { TelemetryConfiguration } from "../config"; +import { LOGGER } from "../../logger"; -export class TelemetryPrefs { - public isExtTelemetryEnabled: boolean; +export class TelemetrySettings { + private isTelemetryEnabled: boolean; + private extensionPrefs: ExtensionTelemetryPreference; + private vscodePrefs: VscodeTelemetryPreference; + + constructor( + extensionContext: ExtensionContextInfo, + private onTelemetryEnableCallback: () => void, + private onTelemetryDisableCallback: () => void, + private triggerPopup: () => void) { + + this.extensionPrefs = new ExtensionTelemetryPreference(); + this.vscodePrefs = new VscodeTelemetryPreference(); + + extensionContext.pushSubscription( + this.extensionPrefs.onChangeTelemetrySetting(this.onChangeTelemetrySettingCallback) + ); + extensionContext.pushSubscription( + this.vscodePrefs.onChangeTelemetrySetting(this.onChangeTelemetrySettingCallback) + ); + + this.isTelemetryEnabled = this.checkTelemetryStatus(); + this.updateGlobalState(); + this.checkConsentVersion(); + } + + private checkTelemetryStatus = (): boolean => this.extensionPrefs.getIsTelemetryEnabled() && this.vscodePrefs.getIsTelemetryEnabled(); + + private onChangeTelemetrySettingCallback = () => { + const newTelemetryStatus = this.checkTelemetryStatus(); + if (newTelemetryStatus !== this.isTelemetryEnabled) { + this.isTelemetryEnabled = newTelemetryStatus; + cacheService.put(TELEMETRY_SETTING_VALUE_KEY, newTelemetryStatus.toString()); + + if (newTelemetryStatus) { + this.onTelemetryEnableCallback(); + } else { + this.onTelemetryDisableCallback(); + } + } else if (this.vscodePrefs.getIsTelemetryEnabled() && !this.extensionPrefs.isTelemetrySettingSet()) { + this.triggerPopup(); + } + } + + public getIsTelemetryEnabled = (): boolean => this.isTelemetryEnabled; + + public isConsentPopupToBeTriggered = (): boolean => { + const isExtensionSettingSet = this.extensionPrefs.isTelemetrySettingSet(); + const isVscodeSettingEnabled = this.vscodePrefs.getIsTelemetryEnabled(); + + const showPopup = !isExtensionSettingSet && isVscodeSettingEnabled; + + if (showPopup) { + cacheService.put(TELEMETRY_CONSENT_POPUP_TIME_KEY, Date.now().toString()); + } + + return showPopup; + } + + public updateTelemetrySetting = (value: boolean | undefined): void => { + this.extensionPrefs.updateTelemetryConfig(value); + } + + private updateGlobalState(): void { + const cachedValue = cacheService.get(TELEMETRY_SETTING_VALUE_KEY); + + if (this.isTelemetryEnabled.toString() !== cachedValue) { + cacheService.put(TELEMETRY_SETTING_VALUE_KEY, this.isTelemetryEnabled.toString()); + } + } + + private checkConsentVersion(): void { + const cachedVersion = cacheService.get(TELEMETRY_CONSENT_VERSION_SCHEMA_KEY); + const currentVersion = TelemetryConfiguration.getInstance().getTelemetryConfigMetadata()?.consentSchemaVersion; + + if (cachedVersion !== currentVersion) { + cacheService.put(TELEMETRY_CONSENT_VERSION_SCHEMA_KEY, currentVersion); + LOGGER.debug("Removing telemetry config from user settings"); + if (this.extensionPrefs.isTelemetrySettingSet()) { + this.updateTelemetrySetting(undefined); + } + this.isTelemetryEnabled = false; + } + } +} + +class ExtensionTelemetryPreference implements TelemetryPreference { + private isTelemetryEnabled: boolean | undefined; + private readonly CONFIG = appendPrefixToCommand(configKeys.telemetryEnabled); constructor() { - this.isExtTelemetryEnabled = this.checkTelemetryStatus(); + this.isTelemetryEnabled = getConfigurationValue(configKeys.telemetryEnabled, false); } - private checkTelemetryStatus = (): boolean => { - return getConfigurationValue(configKeys.telemetryEnabled, false); + public getIsTelemetryEnabled = (): boolean => this.isTelemetryEnabled === undefined ? false : this.isTelemetryEnabled; + + public onChangeTelemetrySetting = (callback: () => void): Disposable => workspace.onDidChangeConfiguration((e: ConfigurationChangeEvent) => { + if (e.affectsConfiguration(this.CONFIG)) { + this.isTelemetryEnabled = getConfigurationValue(configKeys.telemetryEnabled, false); + callback(); + } + }); + + public updateTelemetryConfig = (value: boolean | undefined): void => { + this.isTelemetryEnabled = value; + updateConfigurationValue(configKeys.telemetryEnabled, value, true); } - private configPref = (configCommand: string): boolean => { - const config = inspectConfiguration(configCommand); + public isTelemetrySettingSet = (): boolean => { + if (this.isTelemetryEnabled === undefined) return false; + const config = inspectConfiguration(this.CONFIG); return ( - config?.workspaceFolderValue !== undefined || - config?.workspaceFolderLanguageValue !== undefined || - config?.workspaceValue !== undefined || - config?.workspaceLanguageValue !== undefined || config?.globalValue !== undefined || config?.globalLanguageValue !== undefined ); } +} - public isExtTelemetryConfigured = (): boolean => { - return this.configPref(appendPrefixToCommand(configKeys.telemetryEnabled)); - } +class VscodeTelemetryPreference implements TelemetryPreference { + private isTelemetryEnabled: boolean; - public updateTelemetryEnabledConfig = (value: boolean): void => { - this.isExtTelemetryEnabled = value; - updateConfigurationValue(configKeys.telemetryEnabled, value, true); + constructor() { + this.isTelemetryEnabled = env.isTelemetryEnabled; } - public didUserDisableVscodeTelemetry = (): boolean => { - return !env.isTelemetryEnabled; - } + public getIsTelemetryEnabled = (): boolean => this.isTelemetryEnabled; - public onDidChangeTelemetryEnabled = () => workspace.onDidChangeConfiguration( - (e: ConfigurationChangeEvent) => { - if (e.affectsConfiguration(appendPrefixToCommand(configKeys.telemetryEnabled))) { - this.isExtTelemetryEnabled = this.checkTelemetryStatus(); - } - } - ); -} \ No newline at end of file + public onChangeTelemetrySetting = (callback: () => void): Disposable => env.onDidChangeTelemetryEnabled((newSetting: boolean) => { + this.isTelemetryEnabled = newSetting; + callback(); + }); +} + +// Question: +// When consent version is changed, we have to show popup to all the users or only those who had accepted earlier? + +// Test cases: +// 1. User accepts consent and VSCode telemetry is set to 'all'. Output: enabled telemetry +// 2. User accepts consent and VSCode telemetry is not set to 'all'. Output: disabled telemetry +// 3. User rejects consent and VSCode telemetry is set to 'all'. Output: disabled telemetry +// 4. User rejects consent and VSCode telemetry is not set to 'all'. Output: disabled telemetry +// 5. User changes from accept to reject consent and VSCode telemetry is set to 'all'. Output: disabled telemetry +// 6. User changes from accept to reject consent and VSCode telemetry is not set to 'all'. Output: disabled telemetry +// 7. User changes from reject to accept consent and VSCode telemetry is set to 'all'. Output: enabled telemetry +// 8. User changes from reject to accept consent and VSCode telemetry is not set to 'all'. Output: disabled telemetry +// 9. User accepts consent and VSCode telemetry is changed from 'all' to 'error'. Output: disabled telemetry +// 10. User accepts consent and VSCode telemetry is changed from 'error' to 'all'. Output: enabled telemetry +// 11. When consent schema version updated, pop up should trigger again. +// 12. When consent schema version updated, pop up should trigger again, if closed without selecting any value and again reloading the screen, it should pop-up again.: Disabled telemetry in settings +// 13. When consent schema version updated, pop up should trigger again, if selected yes and again reloading the screen, it shouldn't pop-up again. Output: Enabled telemetry in settings +// 14. When consent schema version updated, pop up should trigger again, if selected no and again reloading the screen, it shouldn't pop-up again. Output: Disabled telemetry in settings +// 15. When VSCode setting is changed from reject to accept, our pop-up should come. diff --git a/vscode/src/telemetry/impl/telemetryReporterImpl.ts b/vscode/src/telemetry/impl/telemetryReporterImpl.ts index ac78297..20af65d 100644 --- a/vscode/src/telemetry/impl/telemetryReporterImpl.ts +++ b/vscode/src/telemetry/impl/telemetryReporterImpl.ts @@ -46,7 +46,6 @@ export class TelemetryReporterImpl implements TelemetryReporter { } public closeEvent = (): void => { - const extensionCloseEvent = ExtensionCloseEvent.builder(this.activationTime); this.addEventToQueue(extensionCloseEvent); @@ -59,6 +58,10 @@ export class TelemetryReporterImpl implements TelemetryReporter { this.queue.enqueue(event); if (this.retryManager.isQueueOverflow(this.queue.size())) { LOGGER.debug(`Send triggered to queue size overflow`); + if(this.retryManager.IsMaxRetryReached()){ + LOGGER.debug('Decreasing size of the queue'); + this.queue.decreaseSizeOnMaxOverflow(); + } this.sendEvents(); } } @@ -81,9 +84,9 @@ export class TelemetryReporterImpl implements TelemetryReporter { LOGGER.debug(`Number of events successfully sent: ${response.success.length}`); LOGGER.debug(`Number of events failed to send: ${response.failures.length}`); - this.handlePostTelemetryResponse(response); + const isResetRetryParams = this.handlePostTelemetryResponse(response); - this.retryManager.startTimer(); + this.retryManager.startTimer(isResetRetryParams); } catch (err: any) { this.disableReporter = true; LOGGER.debug(`Error while sending telemetry: ${isError(err) ? err.message : err}`); @@ -98,11 +101,13 @@ export class TelemetryReporterImpl implements TelemetryReporter { return [...removedJdkFeatureEvents, ...concatedEvents]; } - private handlePostTelemetryResponse = (response: TelemetryPostResponse) => { + private handlePostTelemetryResponse = (response: TelemetryPostResponse): boolean => { const eventsToBeEnqueued = this.retryManager.eventsToBeEnqueuedAgain(response); this.queue.concatQueue(eventsToBeEnqueued); LOGGER.debug(`Number of failed events enqueuing again: ${eventsToBeEnqueued.length}`); + + return eventsToBeEnqueued.length === 0; } } \ No newline at end of file diff --git a/vscode/src/telemetry/impl/telemetryRetry.ts b/vscode/src/telemetry/impl/telemetryRetry.ts index 0ee6302..d2f904c 100644 --- a/vscode/src/telemetry/impl/telemetryRetry.ts +++ b/vscode/src/telemetry/impl/telemetryRetry.ts @@ -33,7 +33,7 @@ export class TelemetryRetry { this.callbackHandler = callbackHandler; } - public startTimer = (): void => { + public startTimer = (resetParameters: boolean = true): void => { if (!this.callbackHandler) { LOGGER.debug("Callback handler is not set for telemetry retry mechanism"); return; @@ -41,10 +41,16 @@ export class TelemetryRetry { if (this.timeout) { LOGGER.debug("Overriding current timeout"); } + if(resetParameters){ + this.resetTimerParameters(); + this.resetQueueCapacity(); + } this.timeout = setInterval(this.callbackHandler, this.timePeriod); } private resetTimerParameters = () => { + LOGGER.debug("Resetting time period to default"); + this.numOfAttemptsWhenTimerHits = 1; this.timePeriod = this.TELEMETRY_RETRY_CONFIG.baseTimer; this.clearTimer(); @@ -56,7 +62,7 @@ export class TelemetryRetry { this.numOfAttemptsWhenTimerHits++; return; } - throw new Error("Number of retries exceeded"); + LOGGER.debug("Keeping timer same as max retries exceeded"); } public clearTimer = (): void => { @@ -82,10 +88,16 @@ export class TelemetryRetry { this.queueCapacity = this.TELEMETRY_RETRY_CONFIG.baseCapacity * Math.pow(this.TELEMETRY_RETRY_CONFIG.backoffFactor, this.numOfAttemptsWhenQueueIsFull); } - throw new Error("Number of retries exceeded"); + LOGGER.debug("Keeping queue capacity same as max retries exceeded"); } + public IsMaxRetryReached = (): boolean => + this.numOfAttemptsWhenQueueIsFull >= this.TELEMETRY_RETRY_CONFIG.maxRetries || + this.numOfAttemptsWhenTimerHits > this.TELEMETRY_RETRY_CONFIG.maxRetries + private resetQueueCapacity = (): void => { + LOGGER.debug("Resetting queue capacity to default"); + this.queueCapacity = this.TELEMETRY_RETRY_CONFIG.baseCapacity; this.numOfAttemptsWhenQueueIsFull = 1; this.triggeredDueToQueueOverflow = false; @@ -108,10 +120,7 @@ export class TelemetryRetry { res.event.onSuccessPostEventCallback(); }); - if (eventResponses.failures.length === 0) { - this.resetQueueCapacity(); - this.resetTimerParameters(); - } else { + if (eventResponses.failures.length) { const eventsToBeEnqueuedAgain: BaseEvent[] = []; eventResponses.failures.forEach((eventRes) => { if (this.isEventRetryable(eventRes.statusCode)) @@ -119,6 +128,7 @@ export class TelemetryRetry { }); if (eventsToBeEnqueuedAgain.length) { + this.triggeredDueToQueueOverflow ? this.increaseQueueCapacity() : this.increaseTimePeriod(); diff --git a/vscode/src/telemetry/telemetry.ts b/vscode/src/telemetry/telemetry.ts index 4e61d65..116cdda 100644 --- a/vscode/src/telemetry/telemetry.ts +++ b/vscode/src/telemetry/telemetry.ts @@ -43,7 +43,7 @@ export namespace Telemetry { } const enqueueEvent = (cbFunction: (reporter: TelemetryReporter) => void) => { - if (telemetryManager.isExtTelemetryEnabled() && getIsTelemetryFeatureAvailable()) { + if (telemetryManager.isTelemetryEnabled() && getIsTelemetryFeatureAvailable()) { const reporter = telemetryManager.getReporter(); if (reporter) { cbFunction(reporter); diff --git a/vscode/src/telemetry/telemetryManager.ts b/vscode/src/telemetry/telemetryManager.ts index eb1c40e..e18dff4 100644 --- a/vscode/src/telemetry/telemetryManager.ts +++ b/vscode/src/telemetry/telemetryManager.ts @@ -14,7 +14,7 @@ limitations under the License. */ import { window } from "vscode"; -import { TelemetryPrefs } from "./impl/telemetryPrefs"; +import { TelemetrySettings } from "./impl/telemetryPrefs"; import { TelemetryEventQueue } from "./impl/telemetryEventQueue"; import { TelemetryReporterImpl } from "./impl/telemetryReporterImpl"; import { TelemetryReporter } from "./types"; @@ -25,24 +25,27 @@ import { TelemetryRetry } from "./impl/telemetryRetry"; export class TelemetryManager { private extensionContextInfo: ExtensionContextInfo; - private settings: TelemetryPrefs = new TelemetryPrefs(); + private settings: TelemetrySettings; private reporter?: TelemetryReporter; private telemetryRetryManager: TelemetryRetry = new TelemetryRetry() constructor(extensionContextInfo: ExtensionContextInfo) { this.extensionContextInfo = extensionContextInfo; + this.settings = new TelemetrySettings(extensionContextInfo, + this.onTelemetryEnable, + this.onTelemetryDisable, + this.openTelemetryDialog); } - public isExtTelemetryEnabled = (): boolean => { - return this.settings.isExtTelemetryEnabled; + public isTelemetryEnabled = (): boolean => { + return this.settings.getIsTelemetryEnabled(); } public initializeReporter = (): void => { const queue = new TelemetryEventQueue(); - this.extensionContextInfo.pushSubscription(this.settings.onDidChangeTelemetryEnabled()); this.reporter = new TelemetryReporterImpl(queue, this.telemetryRetryManager); - this.openTelemetryDialog(); + this.isTelemetryEnabled() ? this.onTelemetryEnable() : this.openTelemetryDialog(); } public getReporter = (): TelemetryReporter | null => { @@ -50,7 +53,7 @@ export class TelemetryManager { } private openTelemetryDialog = async () => { - if (!this.settings.isExtTelemetryConfigured() && !this.settings.didUserDisableVscodeTelemetry()) { + if (this.settings?.isConsentPopupToBeTriggered()) { LOGGER.log('Telemetry not enabled yet'); const yesLabel = l10n.value("jdk.downloader.message.confirmation.yes"); @@ -62,14 +65,19 @@ export class TelemetryManager { return; } - this.settings.updateTelemetryEnabledConfig(enable === yesLabel); - if (enable === yesLabel) { - LOGGER.log("Telemetry is now enabled"); - } - } - if (this.settings.isExtTelemetryEnabled) { - this.telemetryRetryManager.startTimer(); - this.reporter?.startEvent(); + this.settings.updateTelemetrySetting(enable === yesLabel); } } + + private onTelemetryEnable = () => { + LOGGER.log("Telemetry is now enabled"); + this.telemetryRetryManager.startTimer(); + this.reporter?.startEvent(); + } + + private onTelemetryDisable = () => { + // Remaining: Check what needs to be done when disabled + LOGGER.log("Telemetry is now disabled"); + this.telemetryRetryManager.clearTimer(); + } }; \ No newline at end of file diff --git a/vscode/src/telemetry/types.ts b/vscode/src/telemetry/types.ts index 30a663f..18bc6e2 100644 --- a/vscode/src/telemetry/types.ts +++ b/vscode/src/telemetry/types.ts @@ -14,6 +14,7 @@ limitations under the License. */ import { BaseEvent } from "./events/baseEvent"; +import { Disposable } from "vscode"; export interface TelemetryReporter { startEvent(): void; @@ -48,4 +49,15 @@ export interface TelemetryApi { baseUrl: string | null; baseEndpoint: string; version: string; -} \ No newline at end of file +} + +export interface TelemetryConfigMetadata { + consentSchemaVersion: string; +} + +export interface TelemetryPreference { + getIsTelemetryEnabled(): boolean; + onChangeTelemetrySetting(cb: () => void): Disposable; + updateTelemetryConfig?(value: boolean): void; + isTelemetrySettingSet?: () => boolean; +} From efab04db21d6e282617c7e4ce167710ead5dd0f2 Mon Sep 17 00:00:00 2001 From: Achal Talati Date: Wed, 16 Apr 2025 15:35:45 +0530 Subject: [PATCH 11/16] added telemetry disable strategy --- .../telemetry/impl/telemetryReporterImpl.ts | 70 ++++++++++++++----- vscode/src/telemetry/impl/telemetryRetry.ts | 7 +- vscode/src/telemetry/telemetryManager.ts | 4 +- 3 files changed, 56 insertions(+), 25 deletions(-) diff --git a/vscode/src/telemetry/impl/telemetryReporterImpl.ts b/vscode/src/telemetry/impl/telemetryReporterImpl.ts index 20af65d..f302ff7 100644 --- a/vscode/src/telemetry/impl/telemetryReporterImpl.ts +++ b/vscode/src/telemetry/impl/telemetryReporterImpl.ts @@ -27,8 +27,9 @@ import { PostTelemetry, TelemetryPostResponse } from "./postTelemetry"; export class TelemetryReporterImpl implements TelemetryReporter { private activationTime: number = getCurrentUTCDateInSeconds(); - private disableReporter: boolean = false; private postTelemetry: PostTelemetry = new PostTelemetry(); + private onCloseEventState: { status: boolean, numOfRetries: number } = { status: false, numOfRetries: 0 }; + private readonly MAX_RETRY_ON_CLOSE = 5; constructor( private queue: TelemetryEventQueue, @@ -38,14 +39,22 @@ export class TelemetryReporterImpl implements TelemetryReporter { } public startEvent = (): void => { + this.resetOnCloseEventState(); + this.retryManager.startTimer(); + const extensionStartEvent = ExtensionStartEvent.builder(); - if(extensionStartEvent != null){ + if (extensionStartEvent != null) { this.addEventToQueue(extensionStartEvent); LOGGER.debug(`Start event enqueued: ${extensionStartEvent.getPayload}`); - } + } } public closeEvent = (): void => { + this.onCloseEventState = { + status: true, + numOfRetries: 0 + }; + const extensionCloseEvent = ExtensionCloseEvent.builder(this.activationTime); this.addEventToQueue(extensionCloseEvent); @@ -54,22 +63,46 @@ export class TelemetryReporterImpl implements TelemetryReporter { } public addEventToQueue = (event: BaseEvent): void => { - if (!this.disableReporter) { - this.queue.enqueue(event); - if (this.retryManager.isQueueOverflow(this.queue.size())) { - LOGGER.debug(`Send triggered to queue size overflow`); - if(this.retryManager.IsMaxRetryReached()){ - LOGGER.debug('Decreasing size of the queue'); - this.queue.decreaseSizeOnMaxOverflow(); - } - this.sendEvents(); + this.resetOnCloseEventState(); + + this.queue.enqueue(event); + if (this.retryManager.isQueueOverflow(this.queue.size())) { + LOGGER.debug(`Send triggered to queue size overflow`); + if (this.retryManager.IsQueueMaxCapacityReached()) { + LOGGER.debug('Decreasing size of the queue as max capacity reached'); + this.queue.decreaseSizeOnMaxOverflow(); + } + this.sendEvents(); + } + } + + private resetOnCloseEventState = () => { + this.onCloseEventState = { + status: false, + numOfRetries: 0 + }; + } + + private increaseRetryCountOrDisableRetry = () => { + if (this.onCloseEventState.status) { + if (this.onCloseEventState.numOfRetries < this.MAX_RETRY_ON_CLOSE && this.queue.size()) { + LOGGER.debug("Telemetry disabled state: Increasing retry count"); + this.onCloseEventState.numOfRetries++; + } else { + LOGGER.debug(`Telemetry disabled state: ${this.queue.size() ? 'Max retries reached': 'queue is empty'}, resetting timer`); + this.retryManager.clearTimer(); + this.queue.flush(); + this.onCloseEventState = { + status: false, + numOfRetries: 0 + }; } } } private sendEvents = async (): Promise => { try { - if(!this.queue.size()){ + if (!this.queue.size()) { LOGGER.debug(`Queue is empty nothing to send`); return; } @@ -84,20 +117,21 @@ export class TelemetryReporterImpl implements TelemetryReporter { LOGGER.debug(`Number of events successfully sent: ${response.success.length}`); LOGGER.debug(`Number of events failed to send: ${response.failures.length}`); - const isResetRetryParams = this.handlePostTelemetryResponse(response); + const isAllEventsSuccess = this.handlePostTelemetryResponse(response); - this.retryManager.startTimer(isResetRetryParams); + this.retryManager.startTimer(isAllEventsSuccess); + + this.increaseRetryCountOrDisableRetry(); } catch (err: any) { - this.disableReporter = true; LOGGER.debug(`Error while sending telemetry: ${isError(err) ? err.message : err}`); } } - + private transformEvents = (events: BaseEvent[]): BaseEvent[] => { const jdkFeatureEvents = events.filter(event => event.NAME === JdkFeatureEvent.NAME); const concatedEvents = JdkFeatureEvent.concatEvents(jdkFeatureEvents); const removedJdkFeatureEvents = events.filter(event => event.NAME !== JdkFeatureEvent.NAME); - + return [...removedJdkFeatureEvents, ...concatedEvents]; } diff --git a/vscode/src/telemetry/impl/telemetryRetry.ts b/vscode/src/telemetry/impl/telemetryRetry.ts index d2f904c..5147e85 100644 --- a/vscode/src/telemetry/impl/telemetryRetry.ts +++ b/vscode/src/telemetry/impl/telemetryRetry.ts @@ -57,7 +57,7 @@ export class TelemetryRetry { } private increaseTimePeriod = (): void => { - if (this.numOfAttemptsWhenTimerHits <= this.TELEMETRY_RETRY_CONFIG.maxRetries) { + if (this.numOfAttemptsWhenTimerHits < this.TELEMETRY_RETRY_CONFIG.maxRetries) { this.timePeriod = this.calculateDelay(); this.numOfAttemptsWhenTimerHits++; return; @@ -91,9 +91,8 @@ export class TelemetryRetry { LOGGER.debug("Keeping queue capacity same as max retries exceeded"); } - public IsMaxRetryReached = (): boolean => - this.numOfAttemptsWhenQueueIsFull >= this.TELEMETRY_RETRY_CONFIG.maxRetries || - this.numOfAttemptsWhenTimerHits > this.TELEMETRY_RETRY_CONFIG.maxRetries + public IsQueueMaxCapacityReached = (): boolean => + this.numOfAttemptsWhenQueueIsFull > this.TELEMETRY_RETRY_CONFIG.maxRetries; private resetQueueCapacity = (): void => { LOGGER.debug("Resetting queue capacity to default"); diff --git a/vscode/src/telemetry/telemetryManager.ts b/vscode/src/telemetry/telemetryManager.ts index e18dff4..7fdaceb 100644 --- a/vscode/src/telemetry/telemetryManager.ts +++ b/vscode/src/telemetry/telemetryManager.ts @@ -71,13 +71,11 @@ export class TelemetryManager { private onTelemetryEnable = () => { LOGGER.log("Telemetry is now enabled"); - this.telemetryRetryManager.startTimer(); this.reporter?.startEvent(); } private onTelemetryDisable = () => { - // Remaining: Check what needs to be done when disabled LOGGER.log("Telemetry is now disabled"); - this.telemetryRetryManager.clearTimer(); + this.reporter?.closeEvent(); } }; \ No newline at end of file From 2c20449e367eb093c89887d80d2d61bd6fd7e64c Mon Sep 17 00:00:00 2001 From: Achal Talati Date: Thu, 17 Apr 2025 14:31:49 +0530 Subject: [PATCH 12/16] Improved retry mechanism, closeEvent handling and fixed some bugs --- vscode/src/telemetry/constants.ts | 2 +- vscode/src/telemetry/events/baseEvent.ts | 8 +-- vscode/src/telemetry/impl/cacheServiceImpl.ts | 4 +- .../src/telemetry/impl/telemetryEventQueue.ts | 34 ++++++---- vscode/src/telemetry/impl/telemetryPrefs.ts | 63 +++++++++---------- .../telemetry/impl/telemetryReporterImpl.ts | 62 +++++++++--------- vscode/src/telemetry/impl/telemetryRetry.ts | 9 +-- vscode/src/telemetry/types.ts | 2 +- 8 files changed, 94 insertions(+), 90 deletions(-) diff --git a/vscode/src/telemetry/constants.ts b/vscode/src/telemetry/constants.ts index e90d441..a076338 100644 --- a/vscode/src/telemetry/constants.ts +++ b/vscode/src/telemetry/constants.ts @@ -1,3 +1,3 @@ export const TELEMETRY_CONSENT_VERSION_SCHEMA_KEY = "telemetryConsentSchemaVersion"; -export const TELEMETRY_CONSENT_POPUP_TIME_KEY = "telemetryConsentPopupTime"; +export const TELEMETRY_CONSENT_RESPONSE_TIME_KEY = "telemetryConsentResponseTime"; export const TELEMETRY_SETTING_VALUE_KEY = "telemetrySettingValue"; \ No newline at end of file diff --git a/vscode/src/telemetry/events/baseEvent.ts b/vscode/src/telemetry/events/baseEvent.ts index 87a2087..be32f71 100644 --- a/vscode/src/telemetry/events/baseEvent.ts +++ b/vscode/src/telemetry/events/baseEvent.ts @@ -42,7 +42,7 @@ export abstract class BaseEvent { get getPayload(): T & BaseEventPayload { return this._payload; } - + get getData(): T { return this._data; } @@ -58,8 +58,8 @@ export abstract class BaseEvent { protected addEventToCache = (): void => { const dataString = JSON.stringify(this.getData); const calculatedHashVal = getHashCode(dataString); - const isAdded = cacheService.put(this.NAME, calculatedHashVal); - - LOGGER.debug(`${this.NAME} added in cache ${isAdded ? "Successfully" : "Unsucessfully"}`); + cacheService.put(this.NAME, calculatedHashVal).then((isAdded: boolean) => { + LOGGER.debug(`${this.NAME} added in cache ${isAdded ? "Successfully" : "Unsucessfully"}`); + }); } } \ No newline at end of file diff --git a/vscode/src/telemetry/impl/cacheServiceImpl.ts b/vscode/src/telemetry/impl/cacheServiceImpl.ts index 94c12d8..0d7b758 100644 --- a/vscode/src/telemetry/impl/cacheServiceImpl.ts +++ b/vscode/src/telemetry/impl/cacheServiceImpl.ts @@ -28,10 +28,10 @@ class CacheServiceImpl implements CacheService { } } - public put = (key: string, value: string): boolean => { + public put = async (key: string, value: string): Promise => { try { const vscGlobalState = globalState.getExtensionContextInfo().getVscGlobalState(); - vscGlobalState.update(key, value); + await vscGlobalState.update(key, value); LOGGER.debug(`Updating key: ${key} to ${value}`); return true; } catch (err) { diff --git a/vscode/src/telemetry/impl/telemetryEventQueue.ts b/vscode/src/telemetry/impl/telemetryEventQueue.ts index 667e8f7..0bec18b 100644 --- a/vscode/src/telemetry/impl/telemetryEventQueue.ts +++ b/vscode/src/telemetry/impl/telemetryEventQueue.ts @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { LOGGER } from "../../logger"; import { BaseEvent } from "../events/baseEvent"; export class TelemetryEventQueue { @@ -36,18 +37,25 @@ export class TelemetryEventQueue { return queue; } - public decreaseSizeOnMaxOverflow = () => { - const seen = new Set(); - const newQueueStart = Math.floor(this.size() / 2); - - const secondHalf = this.events.slice(newQueueStart); - - const uniqueEvents = secondHalf.filter(event => { - if (seen.has(event.NAME)) return false; - seen.add(event.NAME); - return true; - }); - - this.events = [...uniqueEvents, ...secondHalf]; + public decreaseSizeOnMaxOverflow = (maxNumberOfEventsToBeKept: number) => { + const excess = this.size() - maxNumberOfEventsToBeKept; + + if (excess > 0) { + LOGGER.debug('Decreasing size of the queue as max capacity reached'); + + const seen = new Set(); + const deduplicated = []; + + for (let i = 0; i < excess; i++) { + const event = this.events[i]; + if (!seen.has(event.NAME)) { + deduplicated.push(event); + seen.add(event.NAME); + } + } + + this.events = [...deduplicated, ...this.events.slice(excess)]; + } } + } \ No newline at end of file diff --git a/vscode/src/telemetry/impl/telemetryPrefs.ts b/vscode/src/telemetry/impl/telemetryPrefs.ts index eae6c5f..2c6893d 100644 --- a/vscode/src/telemetry/impl/telemetryPrefs.ts +++ b/vscode/src/telemetry/impl/telemetryPrefs.ts @@ -20,7 +20,7 @@ import { appendPrefixToCommand } from "../../utils"; import { ExtensionContextInfo } from "../../extensionContextInfo"; import { TelemetryPreference } from "../types"; import { cacheService } from "./cacheServiceImpl"; -import { TELEMETRY_CONSENT_POPUP_TIME_KEY, TELEMETRY_CONSENT_VERSION_SCHEMA_KEY, TELEMETRY_SETTING_VALUE_KEY } from "../constants"; +import { TELEMETRY_CONSENT_RESPONSE_TIME_KEY, TELEMETRY_CONSENT_VERSION_SCHEMA_KEY, TELEMETRY_SETTING_VALUE_KEY } from "../constants"; import { TelemetryConfiguration } from "../config"; import { LOGGER } from "../../logger"; @@ -37,17 +37,11 @@ export class TelemetrySettings { this.extensionPrefs = new ExtensionTelemetryPreference(); this.vscodePrefs = new VscodeTelemetryPreference(); - - extensionContext.pushSubscription( - this.extensionPrefs.onChangeTelemetrySetting(this.onChangeTelemetrySettingCallback) - ); - extensionContext.pushSubscription( - this.vscodePrefs.onChangeTelemetrySetting(this.onChangeTelemetrySettingCallback) - ); - + extensionContext.pushSubscription(this.extensionPrefs.onChangeTelemetrySetting(this.onChangeTelemetrySettingCallback)); + extensionContext.pushSubscription(this.vscodePrefs.onChangeTelemetrySetting(this.onChangeTelemetrySettingCallback)); + this.isTelemetryEnabled = this.checkTelemetryStatus(); - this.updateGlobalState(); - this.checkConsentVersion(); + this.syncTelemetrySettingGlobalState(); } private checkTelemetryStatus = (): boolean => this.extensionPrefs.getIsTelemetryEnabled() && this.vscodePrefs.getIsTelemetryEnabled(); @@ -56,14 +50,16 @@ export class TelemetrySettings { const newTelemetryStatus = this.checkTelemetryStatus(); if (newTelemetryStatus !== this.isTelemetryEnabled) { this.isTelemetryEnabled = newTelemetryStatus; - cacheService.put(TELEMETRY_SETTING_VALUE_KEY, newTelemetryStatus.toString()); + this.updateGlobalStates(); if (newTelemetryStatus) { this.onTelemetryEnableCallback(); } else { this.onTelemetryDisableCallback(); } - } else if (this.vscodePrefs.getIsTelemetryEnabled() && !this.extensionPrefs.isTelemetrySettingSet()) { + } else if (this.vscodePrefs.getIsTelemetryEnabled() + && !this.extensionPrefs.isTelemetrySettingSet() + && !cacheService.get(TELEMETRY_CONSENT_RESPONSE_TIME_KEY)) { this.triggerPopup(); } } @@ -74,38 +70,38 @@ export class TelemetrySettings { const isExtensionSettingSet = this.extensionPrefs.isTelemetrySettingSet(); const isVscodeSettingEnabled = this.vscodePrefs.getIsTelemetryEnabled(); - const showPopup = !isExtensionSettingSet && isVscodeSettingEnabled; - - if (showPopup) { - cacheService.put(TELEMETRY_CONSENT_POPUP_TIME_KEY, Date.now().toString()); - } - - return showPopup; + return !isExtensionSettingSet && isVscodeSettingEnabled; } public updateTelemetrySetting = (value: boolean | undefined): void => { this.extensionPrefs.updateTelemetryConfig(value); } - private updateGlobalState(): void { - const cachedValue = cacheService.get(TELEMETRY_SETTING_VALUE_KEY); + private syncTelemetrySettingGlobalState (): void { + const cachedSettingValue = cacheService.get(TELEMETRY_SETTING_VALUE_KEY); + const cachedConsentSchemaVersion = cacheService.get(TELEMETRY_CONSENT_VERSION_SCHEMA_KEY); - if (this.isTelemetryEnabled.toString() !== cachedValue) { - cacheService.put(TELEMETRY_SETTING_VALUE_KEY, this.isTelemetryEnabled.toString()); + if (this.isTelemetryEnabled.toString() !== cachedSettingValue) { + this.updateGlobalStates(); } + this.checkConsentVersionSchemaGlobalState(cachedConsentSchemaVersion); } - private checkConsentVersion(): void { - const cachedVersion = cacheService.get(TELEMETRY_CONSENT_VERSION_SCHEMA_KEY); - const currentVersion = TelemetryConfiguration.getInstance().getTelemetryConfigMetadata()?.consentSchemaVersion; + private updateGlobalStates(): void { + cacheService.put(TELEMETRY_CONSENT_RESPONSE_TIME_KEY, Date.now().toString()); + cacheService.put(TELEMETRY_CONSENT_VERSION_SCHEMA_KEY, TelemetryConfiguration.getInstance().getTelemetryConfigMetadata()?.consentSchemaVersion); + cacheService.put(TELEMETRY_SETTING_VALUE_KEY, this.isTelemetryEnabled.toString()); + } - if (cachedVersion !== currentVersion) { - cacheService.put(TELEMETRY_CONSENT_VERSION_SCHEMA_KEY, currentVersion); - LOGGER.debug("Removing telemetry config from user settings"); - if (this.extensionPrefs.isTelemetrySettingSet()) { + private checkConsentVersionSchemaGlobalState(consentSchemaVersion: string | undefined): void { + if (this.extensionPrefs.isTelemetrySettingSet()) { + const currentExtConsentSchemaVersion = TelemetryConfiguration.getInstance().getTelemetryConfigMetadata()?.consentSchemaVersion; + + if (consentSchemaVersion !== currentExtConsentSchemaVersion) { + LOGGER.debug("Removing telemetry config from user settings due to consent schema version change"); + this.isTelemetryEnabled = false; this.updateTelemetrySetting(undefined); } - this.isTelemetryEnabled = false; } } } @@ -157,9 +153,6 @@ class VscodeTelemetryPreference implements TelemetryPreference { }); } -// Question: -// When consent version is changed, we have to show popup to all the users or only those who had accepted earlier? - // Test cases: // 1. User accepts consent and VSCode telemetry is set to 'all'. Output: enabled telemetry // 2. User accepts consent and VSCode telemetry is not set to 'all'. Output: disabled telemetry diff --git a/vscode/src/telemetry/impl/telemetryReporterImpl.ts b/vscode/src/telemetry/impl/telemetryReporterImpl.ts index f302ff7..6e8a5b3 100644 --- a/vscode/src/telemetry/impl/telemetryReporterImpl.ts +++ b/vscode/src/telemetry/impl/telemetryReporterImpl.ts @@ -39,7 +39,7 @@ export class TelemetryReporterImpl implements TelemetryReporter { } public startEvent = (): void => { - this.resetOnCloseEventState(); + this.setOnCloseEventState(); this.retryManager.startTimer(); const extensionStartEvent = ExtensionStartEvent.builder(); @@ -50,11 +50,6 @@ export class TelemetryReporterImpl implements TelemetryReporter { } public closeEvent = (): void => { - this.onCloseEventState = { - status: true, - numOfRetries: 0 - }; - const extensionCloseEvent = ExtensionCloseEvent.builder(this.activationTime); this.addEventToQueue(extensionCloseEvent); @@ -63,42 +58,49 @@ export class TelemetryReporterImpl implements TelemetryReporter { } public addEventToQueue = (event: BaseEvent): void => { - this.resetOnCloseEventState(); + this.setOnCloseEventState(event); this.queue.enqueue(event); if (this.retryManager.isQueueOverflow(this.queue.size())) { LOGGER.debug(`Send triggered to queue size overflow`); - if (this.retryManager.IsQueueMaxCapacityReached()) { - LOGGER.debug('Decreasing size of the queue as max capacity reached'); - this.queue.decreaseSizeOnMaxOverflow(); - } + const numOfeventsToBeDropped = this.retryManager.getNumberOfEventsToBeDropped(); this.sendEvents(); + if (numOfeventsToBeDropped) { + this.queue.decreaseSizeOnMaxOverflow(numOfeventsToBeDropped); + } } } - private resetOnCloseEventState = () => { - this.onCloseEventState = { - status: false, - numOfRetries: 0 - }; + private setOnCloseEventState = (event?: BaseEvent) => { + if (event?.NAME === ExtensionCloseEvent.NAME) { + this.onCloseEventState = { + status: true, + numOfRetries: 0 + }; + } else { + this.onCloseEventState = { + status: false, + numOfRetries: 0 + }; + } } private increaseRetryCountOrDisableRetry = () => { - if (this.onCloseEventState.status) { - if (this.onCloseEventState.numOfRetries < this.MAX_RETRY_ON_CLOSE && this.queue.size()) { - LOGGER.debug("Telemetry disabled state: Increasing retry count"); - this.onCloseEventState.numOfRetries++; - } else { - LOGGER.debug(`Telemetry disabled state: ${this.queue.size() ? 'Max retries reached': 'queue is empty'}, resetting timer`); - this.retryManager.clearTimer(); - this.queue.flush(); - this.onCloseEventState = { - status: false, - numOfRetries: 0 - }; - } + if (!this.onCloseEventState.status) return; + + const queueEmpty = this.queue.size() === 0; + const retriesExceeded = this.onCloseEventState.numOfRetries >= this.MAX_RETRY_ON_CLOSE; + + if (queueEmpty || retriesExceeded) { + LOGGER.debug(`Telemetry disabled state: ${queueEmpty ? 'Queue is empty' : 'Max retries reached'}, clearing timer`); + this.retryManager.clearTimer(); + this.queue.flush(); + this.setOnCloseEventState(); + } else { + LOGGER.debug("Telemetry disabled state: Increasing retry count"); + this.onCloseEventState.numOfRetries++; } - } + }; private sendEvents = async (): Promise => { try { diff --git a/vscode/src/telemetry/impl/telemetryRetry.ts b/vscode/src/telemetry/impl/telemetryRetry.ts index 5147e85..bb87153 100644 --- a/vscode/src/telemetry/impl/telemetryRetry.ts +++ b/vscode/src/telemetry/impl/telemetryRetry.ts @@ -62,7 +62,7 @@ export class TelemetryRetry { this.numOfAttemptsWhenTimerHits++; return; } - LOGGER.debug("Keeping timer same as max retries exceeded"); + LOGGER.debug("Keeping timer same as max capactiy reached"); } public clearTimer = (): void => { @@ -88,11 +88,12 @@ export class TelemetryRetry { this.queueCapacity = this.TELEMETRY_RETRY_CONFIG.baseCapacity * Math.pow(this.TELEMETRY_RETRY_CONFIG.backoffFactor, this.numOfAttemptsWhenQueueIsFull); } - LOGGER.debug("Keeping queue capacity same as max retries exceeded"); + LOGGER.debug("Keeping queue capacity same as max capacity reached"); } - public IsQueueMaxCapacityReached = (): boolean => - this.numOfAttemptsWhenQueueIsFull > this.TELEMETRY_RETRY_CONFIG.maxRetries; + public getNumberOfEventsToBeDropped = (): number => + this.numOfAttemptsWhenQueueIsFull >= this.TELEMETRY_RETRY_CONFIG.maxRetries ? + this.queueCapacity/2 : 0; private resetQueueCapacity = (): void => { LOGGER.debug("Resetting queue capacity to default"); diff --git a/vscode/src/telemetry/types.ts b/vscode/src/telemetry/types.ts index 18bc6e2..d04059f 100644 --- a/vscode/src/telemetry/types.ts +++ b/vscode/src/telemetry/types.ts @@ -27,7 +27,7 @@ export interface TelemetryReporter { export interface CacheService { get(key: string): string | undefined; - put(key: string, value: string): boolean; + put(key: string, value: string): Promise; } export interface TelemetryEventQueue { From 7aa08bba156d74e388d4acae0784dcf879f97caa Mon Sep 17 00:00:00 2001 From: Achal Talati Date: Thu, 17 Apr 2025 14:34:39 +0530 Subject: [PATCH 13/16] updated license headers --- vscode/src/telemetry/config.ts | 2 +- vscode/src/telemetry/constants.ts | 15 +++++++++++++++ vscode/src/telemetry/events/baseEvent.ts | 2 +- vscode/src/telemetry/events/close.ts | 2 +- vscode/src/telemetry/events/jdkDownload.ts | 2 +- vscode/src/telemetry/events/jdkFeature.ts | 2 +- vscode/src/telemetry/events/start.ts | 2 +- vscode/src/telemetry/events/workspaceChange.ts | 2 +- vscode/src/telemetry/impl/AnonymousIdManager.ts | 2 +- vscode/src/telemetry/impl/cacheServiceImpl.ts | 2 +- vscode/src/telemetry/impl/enviromentDetails.ts | 2 +- vscode/src/telemetry/impl/postTelemetry.ts | 2 +- vscode/src/telemetry/impl/telemetryEventQueue.ts | 2 +- vscode/src/telemetry/impl/telemetryPrefs.ts | 2 +- .../src/telemetry/impl/telemetryReporterImpl.ts | 2 +- vscode/src/telemetry/impl/telemetryRetry.ts | 2 +- vscode/src/telemetry/telemetry.ts | 2 +- vscode/src/telemetry/telemetryManager.ts | 2 +- vscode/src/telemetry/types.ts | 2 +- vscode/src/telemetry/utils.ts | 2 +- 20 files changed, 34 insertions(+), 19 deletions(-) diff --git a/vscode/src/telemetry/config.ts b/vscode/src/telemetry/config.ts index eda1b56..f18a171 100644 --- a/vscode/src/telemetry/config.ts +++ b/vscode/src/telemetry/config.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/constants.ts b/vscode/src/telemetry/constants.ts index a076338..f21dbf0 100644 --- a/vscode/src/telemetry/constants.ts +++ b/vscode/src/telemetry/constants.ts @@ -1,3 +1,18 @@ +/* + Copyright (c) 2024-2025, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ export const TELEMETRY_CONSENT_VERSION_SCHEMA_KEY = "telemetryConsentSchemaVersion"; export const TELEMETRY_CONSENT_RESPONSE_TIME_KEY = "telemetryConsentResponseTime"; export const TELEMETRY_SETTING_VALUE_KEY = "telemetrySettingValue"; \ No newline at end of file diff --git a/vscode/src/telemetry/events/baseEvent.ts b/vscode/src/telemetry/events/baseEvent.ts index be32f71..224dd2a 100644 --- a/vscode/src/telemetry/events/baseEvent.ts +++ b/vscode/src/telemetry/events/baseEvent.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/events/close.ts b/vscode/src/telemetry/events/close.ts index 3e9d394..0f5abc3 100644 --- a/vscode/src/telemetry/events/close.ts +++ b/vscode/src/telemetry/events/close.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/events/jdkDownload.ts b/vscode/src/telemetry/events/jdkDownload.ts index f227b12..b6eb158 100644 --- a/vscode/src/telemetry/events/jdkDownload.ts +++ b/vscode/src/telemetry/events/jdkDownload.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/events/jdkFeature.ts b/vscode/src/telemetry/events/jdkFeature.ts index 24c8e97..9d89226 100644 --- a/vscode/src/telemetry/events/jdkFeature.ts +++ b/vscode/src/telemetry/events/jdkFeature.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/events/start.ts b/vscode/src/telemetry/events/start.ts index 4ae74ff..9334064 100644 --- a/vscode/src/telemetry/events/start.ts +++ b/vscode/src/telemetry/events/start.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/events/workspaceChange.ts b/vscode/src/telemetry/events/workspaceChange.ts index 7d57760..23f3b43 100644 --- a/vscode/src/telemetry/events/workspaceChange.ts +++ b/vscode/src/telemetry/events/workspaceChange.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/impl/AnonymousIdManager.ts b/vscode/src/telemetry/impl/AnonymousIdManager.ts index d1186d6..092475c 100644 --- a/vscode/src/telemetry/impl/AnonymousIdManager.ts +++ b/vscode/src/telemetry/impl/AnonymousIdManager.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/impl/cacheServiceImpl.ts b/vscode/src/telemetry/impl/cacheServiceImpl.ts index 0d7b758..8374687 100644 --- a/vscode/src/telemetry/impl/cacheServiceImpl.ts +++ b/vscode/src/telemetry/impl/cacheServiceImpl.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/impl/enviromentDetails.ts b/vscode/src/telemetry/impl/enviromentDetails.ts index 2b6d427..c14d267 100644 --- a/vscode/src/telemetry/impl/enviromentDetails.ts +++ b/vscode/src/telemetry/impl/enviromentDetails.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/impl/postTelemetry.ts b/vscode/src/telemetry/impl/postTelemetry.ts index 1385a91..2fbe8b5 100644 --- a/vscode/src/telemetry/impl/postTelemetry.ts +++ b/vscode/src/telemetry/impl/postTelemetry.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/impl/telemetryEventQueue.ts b/vscode/src/telemetry/impl/telemetryEventQueue.ts index 0bec18b..fced412 100644 --- a/vscode/src/telemetry/impl/telemetryEventQueue.ts +++ b/vscode/src/telemetry/impl/telemetryEventQueue.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/impl/telemetryPrefs.ts b/vscode/src/telemetry/impl/telemetryPrefs.ts index 2c6893d..620a677 100644 --- a/vscode/src/telemetry/impl/telemetryPrefs.ts +++ b/vscode/src/telemetry/impl/telemetryPrefs.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/impl/telemetryReporterImpl.ts b/vscode/src/telemetry/impl/telemetryReporterImpl.ts index 6e8a5b3..1b696b6 100644 --- a/vscode/src/telemetry/impl/telemetryReporterImpl.ts +++ b/vscode/src/telemetry/impl/telemetryReporterImpl.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/impl/telemetryRetry.ts b/vscode/src/telemetry/impl/telemetryRetry.ts index bb87153..22faee0 100644 --- a/vscode/src/telemetry/impl/telemetryRetry.ts +++ b/vscode/src/telemetry/impl/telemetryRetry.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/telemetry.ts b/vscode/src/telemetry/telemetry.ts index 116cdda..fc55d4c 100644 --- a/vscode/src/telemetry/telemetry.ts +++ b/vscode/src/telemetry/telemetry.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/telemetryManager.ts b/vscode/src/telemetry/telemetryManager.ts index 7fdaceb..076801c 100644 --- a/vscode/src/telemetry/telemetryManager.ts +++ b/vscode/src/telemetry/telemetryManager.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/types.ts b/vscode/src/telemetry/types.ts index d04059f..bb273d2 100644 --- a/vscode/src/telemetry/types.ts +++ b/vscode/src/telemetry/types.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vscode/src/telemetry/utils.ts b/vscode/src/telemetry/utils.ts index 7b69256..fcf1a0f 100644 --- a/vscode/src/telemetry/utils.ts +++ b/vscode/src/telemetry/utils.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2024, Oracle and/or its affiliates. + Copyright (c) 2024-2025, Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 546665af0436adb6317c749da14210747578afb3 Mon Sep 17 00:00:00 2001 From: Siddharth Srinivasan Date: Thu, 24 Apr 2025 14:25:59 +0530 Subject: [PATCH 14/16] Updated telemetry jdk-features cache expiry time and maintenance as a union across the source root instead of only those in the active editor. Signed-off-by: Siddharth Srinivasan --- .gitignore | 3 ++- .../java/lsp/server/telemetry/SourceFeatureCache.java | 11 +++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 109efbe..671e79f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,10 @@ build/ .DS_STORE +.vscode/ .idea/ *.iml /nbcode/l10n/locale_ja/release/ /nbcode/l10n/locale_zh_CN/release/ /nbcode/l10n/locale_ja/nbproject/private/ /nbcode/l10n/locale_zh_CN/nbproject/private/ -telemetryConfig.json \ No newline at end of file +telemetryConfig.json diff --git a/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/SourceFeatureCache.java b/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/SourceFeatureCache.java index 31e392e..6b1c5b2 100644 --- a/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/SourceFeatureCache.java +++ b/nbcode/telemetry/src/org/netbeans/modules/nbcode/java/lsp/server/telemetry/SourceFeatureCache.java @@ -61,8 +61,7 @@ public boolean isPreviewEnabled(SourceInfo sourceInfo) { } private static class Singleton { - - private static final int CACHE_EXPIRY = Math.max(0, NbPreferences.forModule(JavaLangFeaturesTelemetryProvider.class).node(JavaLangFeaturesTelemetryProvider.PREFERENCES_NODE).getInt(JavaLangFeaturesTelemetryProvider.PREFERENCES_KEY_CACHE_EXPIRY, 3_600_000)); // 1 hour + private static final int CACHE_EXPIRY = Math.max(0, NbPreferences.forModule(JavaLangFeaturesTelemetryProvider.class).node(JavaLangFeaturesTelemetryProvider.PREFERENCES_NODE).getInt(JavaLangFeaturesTelemetryProvider.PREFERENCES_KEY_CACHE_EXPIRY, 12 * 3_600_000)); // 12 hours private static final ConcurrentHashMap cachedSourceFeatures = new ConcurrentHashMap<>(); } @@ -77,8 +76,12 @@ public static SourceFeatureCacheEntry get(String sourceName) { public static boolean add(String sourceName, Set features) { final Set newFeatures = Collections.unmodifiableSet(features); final SourceFeatureCacheEntry entry = getCachedSourceFeatures().compute(sourceName, - (name, cache) -> cache != null && cache.getFeaturesUsed().containsAll(newFeatures) ? cache - : new SourceFeatureCacheEntry(System.currentTimeMillis(), newFeatures, cache)); + (name, cache) -> { + if (cache != null && cache.getFeaturesUsed().containsAll(newFeatures)) return cache; + if (cache != null) features.addAll(cache.getFeaturesUsed()); + return new SourceFeatureCacheEntry(System.currentTimeMillis(), newFeatures, cache); + } + ); boolean added = newFeatures == entry.getFeaturesUsed(); if (!added) { From 13182aaf357e922bc263c3525b38dc6c7a8b480b Mon Sep 17 00:00:00 2001 From: Achal Talati Date: Fri, 18 Apr 2025 11:23:41 +0530 Subject: [PATCH 15/16] added some unit test --- .../src/telemetry/impl/telemetryEventQueue.ts | 4 +- .../telemetry/impl/telemetryReporterImpl.ts | 6 +- vscode/src/telemetry/impl/telemetryRetry.ts | 4 +- .../unit/telemetry/cacheService.unit.test.ts | 86 ++++++++ .../test/unit/telemetry/queue.unit.test.ts | 184 ++++++++++++++++++ 5 files changed, 277 insertions(+), 7 deletions(-) create mode 100644 vscode/src/test/unit/telemetry/cacheService.unit.test.ts create mode 100644 vscode/src/test/unit/telemetry/queue.unit.test.ts diff --git a/vscode/src/telemetry/impl/telemetryEventQueue.ts b/vscode/src/telemetry/impl/telemetryEventQueue.ts index fced412..5a6e44f 100644 --- a/vscode/src/telemetry/impl/telemetryEventQueue.ts +++ b/vscode/src/telemetry/impl/telemetryEventQueue.ts @@ -37,8 +37,8 @@ export class TelemetryEventQueue { return queue; } - public decreaseSizeOnMaxOverflow = (maxNumberOfEventsToBeKept: number) => { - const excess = this.size() - maxNumberOfEventsToBeKept; + public adjustQueueSize = (maxNumOfEventsToRetain: number) => { + const excess = this.size() - maxNumOfEventsToRetain; if (excess > 0) { LOGGER.debug('Decreasing size of the queue as max capacity reached'); diff --git a/vscode/src/telemetry/impl/telemetryReporterImpl.ts b/vscode/src/telemetry/impl/telemetryReporterImpl.ts index 1b696b6..7478acd 100644 --- a/vscode/src/telemetry/impl/telemetryReporterImpl.ts +++ b/vscode/src/telemetry/impl/telemetryReporterImpl.ts @@ -63,10 +63,10 @@ export class TelemetryReporterImpl implements TelemetryReporter { this.queue.enqueue(event); if (this.retryManager.isQueueOverflow(this.queue.size())) { LOGGER.debug(`Send triggered to queue size overflow`); - const numOfeventsToBeDropped = this.retryManager.getNumberOfEventsToBeDropped(); + const numOfEventsToBeRetained = this.retryManager.getNumberOfEventsToBeRetained(); this.sendEvents(); - if (numOfeventsToBeDropped) { - this.queue.decreaseSizeOnMaxOverflow(numOfeventsToBeDropped); + if (numOfEventsToBeRetained !== -1) { + this.queue.adjustQueueSize(numOfEventsToBeRetained); } } } diff --git a/vscode/src/telemetry/impl/telemetryRetry.ts b/vscode/src/telemetry/impl/telemetryRetry.ts index 22faee0..8580a79 100644 --- a/vscode/src/telemetry/impl/telemetryRetry.ts +++ b/vscode/src/telemetry/impl/telemetryRetry.ts @@ -91,9 +91,9 @@ export class TelemetryRetry { LOGGER.debug("Keeping queue capacity same as max capacity reached"); } - public getNumberOfEventsToBeDropped = (): number => + public getNumberOfEventsToBeRetained = (): number => this.numOfAttemptsWhenQueueIsFull >= this.TELEMETRY_RETRY_CONFIG.maxRetries ? - this.queueCapacity/2 : 0; + this.queueCapacity/2 : -1; private resetQueueCapacity = (): void => { LOGGER.debug("Resetting queue capacity to default"); diff --git a/vscode/src/test/unit/telemetry/cacheService.unit.test.ts b/vscode/src/test/unit/telemetry/cacheService.unit.test.ts new file mode 100644 index 0000000..8ab10d7 --- /dev/null +++ b/vscode/src/test/unit/telemetry/cacheService.unit.test.ts @@ -0,0 +1,86 @@ +/* + Copyright (c) 2025, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { expect } from "chai"; +import * as sinon from "sinon"; +import { globalState } from "../../../globalState"; +import { LOGGER } from "../../../logger"; +import { describe, it, beforeEach, afterEach } from "mocha"; +import { cacheService } from "../../../telemetry/impl/cacheServiceImpl"; + +describe("CacheServiceImpl", () => { + let getStub: sinon.SinonStub; + let updateStub: sinon.SinonStub; + let loggerErrorStub: sinon.SinonStub; + let loggerDebugStub: sinon.SinonStub; + + const fakeState = { + get: (key: string) => `value-${key}`, + update: async (key: string, value: string) => {}, + }; + + beforeEach(() => { + getStub = sinon.stub(fakeState, "get").callThrough(); + updateStub = sinon.stub(fakeState, "update").resolves(); + + sinon.stub(globalState, "getExtensionContextInfo").returns({ + getVscGlobalState: () => fakeState, + } as any); + + loggerErrorStub = sinon.stub(LOGGER, "error"); + loggerDebugStub = sinon.stub(LOGGER, "debug"); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe("get", () => { + it("should return the cached value for a key", () => { + const key = "example"; + const value = cacheService.get(key); + expect(value).to.equal(`value-${key}`); + expect(getStub.calledOnceWith(key)).to.be.true; + }); + + it("should log and return undefined on error", () => { + getStub.throws(new Error("key not found error")); + + const result = cacheService.get("notPresent"); + expect(result).to.be.undefined; + expect(loggerErrorStub.calledOnce).to.be.true; + }); + }); + + describe("put", () => { + it("should store the value and return true", async () => { + const key = "example"; + const value = "example-value" + const result = await cacheService.put(key, value); + expect(result).to.be.true; + expect(updateStub.calledOnceWith(key, value)).to.be.true; + expect(loggerDebugStub.calledOnce).to.be.true; + }); + + it("should log and return false on error", async () => { + updateStub.rejects(new Error("Error while storing key")); + + const result = await cacheService.put("badKey", "value"); + expect(result).to.be.false; + expect(loggerErrorStub.calledOnce).to.be.true; + }); + }); +}); diff --git a/vscode/src/test/unit/telemetry/queue.unit.test.ts b/vscode/src/test/unit/telemetry/queue.unit.test.ts new file mode 100644 index 0000000..8e195ae --- /dev/null +++ b/vscode/src/test/unit/telemetry/queue.unit.test.ts @@ -0,0 +1,184 @@ +/* + Copyright (c) 2025, Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { TelemetryEventQueue } from '../../../telemetry/impl/telemetryEventQueue'; +import { BaseEvent } from '../../../telemetry/events/baseEvent'; +import { LOGGER } from '../../../logger'; +import { describe, it, beforeEach, afterEach } from 'mocha'; + +describe('TelemetryEventQueue', () => { + let queue: TelemetryEventQueue; + let loggerStub: sinon.SinonStub; + + class MockEvent extends BaseEvent { + public static readonly NAME = "mock"; + public static readonly ENDPOINT = "/mock"; + + + constructor(name?: string, data?: any) { + super(name || MockEvent.NAME, MockEvent.ENDPOINT, data || {}); + } + } + + beforeEach(() => { + queue = new TelemetryEventQueue(); + + loggerStub = sinon.stub(LOGGER, 'debug'); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('enqueue', () => { + it('should add an event to the queue', () => { + const event = new MockEvent(); + queue.enqueue(event); + expect(queue.size()).to.equal(1); + }); + + it('should add multiple events in order', () => { + const event1 = new MockEvent('event1'); + const event2 = new MockEvent('event2'); + + queue.enqueue(event1); + queue.enqueue(event2); + + const firstEvent = queue.dequeue(); + expect(firstEvent).to.equal(event1); + expect(queue.size()).to.equal(1); + }); + }); + + describe('dequeue', () => { + it('should remove and return the first event from the queue', () => { + const event = new MockEvent(); + queue.enqueue(event); + + const dequeuedEvent = queue.dequeue(); + expect(dequeuedEvent).to.equal(event); + expect(queue.size()).to.equal(0); + }); + + it('should return undefined if queue is empty', () => { + const dequeuedEvent = queue.dequeue(); + expect(dequeuedEvent).to.be.undefined; + }); + }); + + describe('concatQueue', () => { + it('should append events to the end of the queue by default', () => { + const event1 = new MockEvent('event1'); + const event2 = new MockEvent('event2'); + const event3 = new MockEvent('event3'); + + queue.enqueue(event1); + queue.concatQueue([event2, event3]); + + expect(queue.size()).to.equal(3); + expect(queue.dequeue()).to.equal(event1); + expect(queue.dequeue()).to.equal(event2); + expect(queue.dequeue()).to.equal(event3); + }); + + it('should prepend events to the start of the queue when mergeAtStarting is true', () => { + const event1 = new MockEvent('event1'); + const event2 = new MockEvent('event2'); + const event3 = new MockEvent('event3'); + + queue.enqueue(event1); + queue.concatQueue([event2, event3], true); + + expect(queue.size()).to.equal(3); + expect(queue.dequeue()).to.equal(event2); + expect(queue.dequeue()).to.equal(event3); + expect(queue.dequeue()).to.equal(event1); + }); + }); + + describe('size', () => { + it('should return the number of events in the queue', () => { + expect(queue.size()).to.equal(0); + + queue.enqueue(new MockEvent('event1')); + expect(queue.size()).to.equal(1); + + queue.enqueue(new MockEvent('event2')); + expect(queue.size()).to.equal(2); + + queue.dequeue(); + expect(queue.size()).to.equal(1); + }); + }); + + describe('flush', () => { + it('should return all events and empty the queue', () => { + const event1 = new MockEvent('event1'); + const event2 = new MockEvent('event2'); + + queue.enqueue(event1); + queue.enqueue(event2); + + const flushedEvents = queue.flush(); + + expect(flushedEvents).to.deep.equal([event1, event2]); + expect(queue.size()).to.equal(0); + }); + }); + + describe('decreaseSizeOnMaxOverflow', () => { + it('should do nothing if queue size is below the max', () => { + const event1 = new MockEvent('event1'); + const event2 = new MockEvent('event2'); + + queue.enqueue(event1); + queue.enqueue(event2); + + queue.adjustQueueSize(5); + + expect(queue.size()).to.equal(2); + expect(loggerStub.called).to.be.false; + }); + + it('should log and deduplicate events when queue exceeds max size', () => { + const event1 = new MockEvent('event1'); + const event2 = new MockEvent('event2'); + const event3 = new MockEvent('event1'); + const event4 = new MockEvent('event3'); + const event5 = new MockEvent('event4'); + const event6 = new MockEvent('event5'); + + queue.enqueue(event1); + queue.enqueue(event2); + queue.enqueue(event3); + queue.enqueue(event4); + queue.enqueue(event5); + queue.enqueue(event6); + + queue.adjustQueueSize(3); + + expect(queue.size()).to.equal(5); + expect(loggerStub.calledOnce).to.be.true; + + const remainingEvents = queue.flush(); + const eventNames = remainingEvents.map(e => e.NAME); + expect(eventNames).to.deep.equal(['event1', 'event2', 'event3', 'event4', 'event5']); + }); + }); + +}); \ No newline at end of file From eb9d4f623d287a6da15e2a298152996938014844 Mon Sep 17 00:00:00 2001 From: Siddharth Srinivasan Date: Tue, 8 Apr 2025 18:02:55 +0530 Subject: [PATCH 16/16] Updating telemetry consent messages 1. Added a detailed data usage policy document as vscode/TELEMETRY.md 2. Updated telemetry consent settings and pop-up messages to refer to this document. Signed-off-by: Siddharth Srinivasan --- vscode/TELEMETRY.md | 69 ++++++++++++++++++++++++ vscode/l10n/bundle.l10n.en.json | 2 +- vscode/l10n/bundle.l10n.ja.json | 2 +- vscode/l10n/bundle.l10n.zh-cn.json | 2 +- vscode/package.json | 3 +- vscode/package.nls.ja.json | 3 +- vscode/package.nls.json | 3 +- vscode/package.nls.zh-cn.json | 3 +- vscode/src/telemetry/telemetryManager.ts | 2 +- 9 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 vscode/TELEMETRY.md diff --git a/vscode/TELEMETRY.md b/vscode/TELEMETRY.md new file mode 100644 index 0000000..b3c4e0d --- /dev/null +++ b/vscode/TELEMETRY.md @@ -0,0 +1,69 @@ +# About Oracle Java Platform Extension for Visual Studio Code use of Telemetry +## First Your Privacy and Anonymity + +During startup, close or usage of the Oracle Java Platform extension for Visual Studio Code (hereafter referred to as "JVSCE"), Oracle does not collect or track any personally identifiable information (such as names or email addresses) about you or your source code, or associate your usage of JVSCE with personally identifiable data. + +If you choose to enable telemetry collection in JVSCE, we will collect a limited set of information that is not personally identifiable. It is anonymous technical data commonly known as "telemetry". An element-by-element description of what is collected and how it helps us, [is listed below](#specifics-about-java-extension-usage-metrics). + +We do not transmit your source code nor the contents of the files you edit with Visual Studio Code. + +## We Collect this Data to Measure Our Own Performance, Diagnose Problems, and Improve Your Experience +The purpose of the telemetry transmitted to Oracle during the JVSCE startup, close, or usage, is to find any technical problems or anomalies that occur, so we can understand and correct them; as well as, to understand the usage of Java versions and language features, so we can help improve the platform. It's similar to a car mechanic's practice of plugging into your engine's diagnostic port to identify performance issues (not to identify the car's owner!). The issues we identify could be anything from slow performance or missing platforms for JDK builds to adoption of popular Java language capabilities. Your source code, privacy and anonymity are not compromised by our collecting this telemetry. The information thus collected helps to improve the extension experience and the Java platform. + +## If You Would Like to Enable or Disable the Collection of this Data +If you have not already made a choice for this setting, the extension might request that you enable Telemetry via a notification pop-up at the time of activation of the extension. + +If you wish to enable or disable the collection and transmission of the telemetry, you may do so from VS Code Settings → [jdk.telemetry.enabled](vscode://settings/jdk.telemetry.enabled). + +No information is sent to Oracle prior to you enabling Telemetry. + +# Specifics About Java Extension Usage Metrics +## WHO Sends Information to Oracle? +We, the authors of the JVSCE, collect and send information from the Java extension, when it is running in VS Code, only for users who consent to its collection. + +## HOW Is Information Sent to Oracle? +Small JSON messages are transported via a secure HTTPS connection over the Internet. + +## WHEN Is Information Sent to Oracle? +Information may be sent in one or more separate messages, depending on the events reported by JVSCE. + +The following types of messages may be sent if users have consented to telemetry from JVSCE: +1. Start + - When a JVSCE instance is launched in a window, a "Start" event is sent. + - This contains information about the version, language, architecture etc. of JVSCE, VSCode and the OS. +2. Close + - When a JVSCE instance is closed in a window, a "Close" event is sent. + - This contains timing information for the session. +3. Workspace Change + - When a workspace is loaded for which JVSCE is active, a "Workspace Change" event is sent. + - This contains information about the Java version used, build tool used, and performance times. +4. JDK Download + - When a JDK is downloaded and installed using JVSCE, a "JDK Download" event is sent. + - This contains information about the Java version downloaded. +5. JDK Feature + - A "JDK Feature" event is sent when certain Java language features, such as `records`, `switch-expressions`, `pattern-matching` etc., or, functionality introduced in [JEPs](https://openjdk.org/jeps/0), is used while editing a Java file. + - This contains information about the Java version in use and the feature names or JEP numbers used. + +## WHAT Information is Sent to Oracle? +Some or all of the following information may be sent to Oracle depending on the type of event triggered: + +| Event | Data Name | Value Description | +|-------|----------------|--------------------------------------------------------------| +| _ALL_ | VsCodeId | Anonymous machine identifier generated by VS Code.
It cannot be used to track the IP address or identity of the user.
Example: `9c5aa07a61ed54bbf61705d17bd214ef269bbed8d5a557ba9e50658b31ffae79` | +| _ALL_ | VscSessionId | Anonymous session identifier generated by VS Code.
It cannot be used to track the IP address or identity of the user.
Example: `15cf5498-7981-45af-aca3-34e0fa6e906c1716187959345` | +| Start | ExtensionInfo | JVSCE information like extension name, id and version.
Example: `Oracle.oracle-java`, `oracle-java`, `23.1.0` | +| Start | VsCodeInfo | VS Code related information like version, language and host-type.
Example: `1.97.2`, `en`, `desktop` | +| Start | PlatformInfo | Host platform/OS information like os name, architecture and version.
Example: `mac`, `arm64`, `14.6` | +| Start | LocationInfo | Platform location information like language-region and time-zone.
Example: `en-US`, `Asia/Jakarta` | +| Close | SessionTime | Indicates the time for which JVSCE was active in a session. | +| Workspace Change | Timing Info | Indicates performance information like the time taken to initialise the language server and the time taken to parse and load the project information | +| Workspace Change | ProjectInfo | Indicates project-related Java and JVSCE information like an anonymous project identifier, Java version, Java VM name, Java preview-feature enablement, build tool and whether project loading had problems.
The identifier cannot be used to track the IP address or identity of the user.
Example: `[{"id": "02a4eb67236abe93d137136be01f1372b8372e2230896932ef25267df32b4f52", "buildTool": "MavenProject", "isOpenedWithProblems": false, "javaVersion": "21.0.6+8-LTS-jvmci-23.1-b59;Oracle GraalVM 21.0.6+8.1;Java HotSpot(TM) 64-Bit Server VM", "isPreviewEnabled": false}]` | +| JDK Download | Vendor | Indicates the vendor of the JDK downloaded using JVSCE.
Example: `OracleJDK` | +| JDK Download | Version | Indicates the version of the JDK downloaded.
Example: `21` | +| JDK Download | Platform | Indicates the OS name and architecture of the downloaded JDK.
Example: `windows`, `x64` | +| JDK Download | Timing Info | Indicates the time taken to download the JDK. | +| JDK Feature | JEPs | Indicates the Java language features used as denoted by their [JEP numbers](https://openjdk.org/jeps/0).
Example: `[477, 395, 394]` | +| JDK Feature | Features | Indicates the Java language features used as denoted by their common names.
Example: `["RECORDS", "SWITCH_RULE", "PATTERN_MATCHING_IN_INSTANCEOF"]` | +| JDK Feature | Version | Indicates the Java version and enablement of Java preview-features.
Example: `23.0.2+7-58`, `"isPreviewEnabled": true` | + +See information on [Oracle Privacy Policies](https://www.oracle.com/legal/privacy/). diff --git a/vscode/l10n/bundle.l10n.en.json b/vscode/l10n/bundle.l10n.en.json index 2f913dc..d58a178 100644 --- a/vscode/l10n/bundle.l10n.en.json +++ b/vscode/l10n/bundle.l10n.en.json @@ -96,5 +96,5 @@ "jdk.workspace.new.prompt": "Input the directory path where the new file will be generated", "jdk.extension.utils.error_message.failedHttpsRequest": "Failed to get {url} ({statusCode})", "jdk.extension.error_msg.notEnabled": "{SERVER_NAME} not enabled", - "jdk.telemetry.consent": "Do you want to enable telemetry for {extensionName} extension? You may opt-out or in at any time from the Settings for jdk.telemetry.enabled." + "jdk.telemetry.consent": "Allow anonymous telemetry data to be reported to Oracle? You may opt-out or in at any time from the Settings for jdk.telemetry.enabled." } diff --git a/vscode/l10n/bundle.l10n.ja.json b/vscode/l10n/bundle.l10n.ja.json index a966b88..3f610e9 100755 --- a/vscode/l10n/bundle.l10n.ja.json +++ b/vscode/l10n/bundle.l10n.ja.json @@ -96,5 +96,5 @@ "jdk.workspace.new.prompt": "新しいファイルを生成するディレクトリのパスを入力してください", "jdk.extension.utils.error_message.failedHttpsRequest": "{url}の取得に失敗しました({statusCode})", "jdk.extension.error_msg.notEnabled": "{SERVER_NAME}が有効化されていません", - "jdk.telemetry.consent": "Do you want to enable telemetry for {extensionName} extension? You may opt-out or in at any time from the Settings for jdk.telemetry.enabled." + "jdk.telemetry.consent": "Allow anonymous telemetry data to be reported to Oracle? You may opt-out or in at any time from the Settings for jdk.telemetry.enabled." } diff --git a/vscode/l10n/bundle.l10n.zh-cn.json b/vscode/l10n/bundle.l10n.zh-cn.json index 9551f2a..1b0217b 100755 --- a/vscode/l10n/bundle.l10n.zh-cn.json +++ b/vscode/l10n/bundle.l10n.zh-cn.json @@ -96,5 +96,5 @@ "jdk.workspace.new.prompt": "输入生成新文件的目录路径", "jdk.extension.utils.error_message.failedHttpsRequest": "无法获取 {url} ({statusCode})", "jdk.extension.error_msg.notEnabled": "{SERVER_NAME} 未启用", - "jdk.telemetry.consent": "Do you want to enable telemetry for {extensionName} extension? You may opt-out or in at any time from the Settings for jdk.telemetry.enabled." + "jdk.telemetry.consent": "Allow anonymous telemetry data to be reported to Oracle? You may opt-out or in at any time from the Settings for jdk.telemetry.enabled." } diff --git a/vscode/package.json b/vscode/package.json index 0a9a7e7..d8328bf 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -245,9 +245,10 @@ "jdk.telemetry.enabled": { "type": "boolean", "description": "%jdk.configuration.telemetry.enabled.description%", + "markdownDescription": "%jdk.configuration.telemetry.enabled.markdownDescription%", "default": false, "tags": [ - "telemetry" + "telemetry" ] } } diff --git a/vscode/package.nls.ja.json b/vscode/package.nls.ja.json index 9958675..1531b5f 100755 --- a/vscode/package.nls.ja.json +++ b/vscode/package.nls.ja.json @@ -46,7 +46,8 @@ "jdk.configuration.runConfig.cwd.description": "作業ディレクトリ", "jdk.configuration.disableNbJavac.description": "拡張オプション: nb-javacライブラリを無効化すると、選択したJDKからのjavacが使用されます。選択したJDKは少なくともJDK 23である必要があります。", "jdk.configuration.disableProjectSearchLimit.description": "拡張オプション: プロジェクト情報が含まれているフォルダの検索に対する制限を無効化します。", - "jdk.configuration.telemetry.enabled.description": "Allow the Oracle Java Visual Studio Code extension to collect and send usage data to Oracle servers to help improve the Java platform support. No personal information nor source code is collected. You may refer to our general privacy policy at https://www.oracle.com/legal/privacy/services-privacy-policy/", + "jdk.configuration.telemetry.enabled.description": "Allow the Oracle Java extension for Visual Studio Code (\"JVSCE\") to collect and send anonymous technical data commonly known as \"telemetry data\" to Oracle to help improve the Java platform. No personal information nor source code is collected. You may refer to the data collection and privacy policy for JVSCE at https://github.com/oracle/javavscode/blob/main/vscode/TELEMETRY.md", + "jdk.configuration.telemetry.enabled.markdownDescription": "Allow the Oracle Java extension for Visual Studio Code (\"*JVSCE*\") to collect and send anonymous technical data commonly known as \"*telemetry data*\" to Oracle to help improve the Java platform. No personal information nor source code is collected. You may refer to the data collection and privacy policy for JVSCE at [TELEMETRY.md](https://github.com/oracle/javavscode/blob/main/vscode/TELEMETRY.md).", "jdk.debugger.configuration.mainClass.markdownDescription": "Main class specification. Supported formats:\n - an absolute path\n - a path relative to any of the workspace folders\n - a fully qualified name of a class.", "jdk.debugger.configuration.classPaths.description": "JVMの起動のためのクラスパス。", "jdk.debugger.configuration.console.description": "プログラムを起動する指定されたコンソール。", diff --git a/vscode/package.nls.json b/vscode/package.nls.json index 074203d..9ac9617 100644 --- a/vscode/package.nls.json +++ b/vscode/package.nls.json @@ -46,7 +46,8 @@ "jdk.configuration.runConfig.cwd.description": "Working directory", "jdk.configuration.disableNbJavac.description": "Advanced option: disable nb-javac library, javac from the selected JDK will be used. The selected JDK must be at least JDK 23.", "jdk.configuration.disableProjectSearchLimit.description": "Advanced option: disable limits on searching in containing folders for project information.", - "jdk.configuration.telemetry.enabled.description": "Allow the Oracle Java Visual Studio Code extension to collect and send usage data to Oracle servers to help improve the Java platform support. No personal information nor source code is collected. You may refer to our general privacy policy at https://www.oracle.com/legal/privacy/services-privacy-policy/", + "jdk.configuration.telemetry.enabled.description": "Allow the Oracle Java extension for Visual Studio Code (\"JVSCE\") to collect and send anonymous technical data commonly known as \"telemetry data\" to Oracle to help improve the Java platform. No personal information nor source code is collected. You may refer to the data collection and privacy policy for JVSCE at https://github.com/oracle/javavscode/blob/main/vscode/TELEMETRY.md", + "jdk.configuration.telemetry.enabled.markdownDescription": "Allow the Oracle Java extension for Visual Studio Code (\"*JVSCE*\") to collect and send anonymous technical data commonly known as \"*telemetry data*\" to Oracle to help improve the Java platform. No personal information nor source code is collected. You may refer to the data collection and privacy policy for JVSCE at [TELEMETRY.md](https://github.com/oracle/javavscode/blob/main/vscode/TELEMETRY.md).", "jdk.debugger.configuration.mainClass.markdownDescription": "Main class specification. Supported formats:\n - an absolute path\n - a path relative to any of the workspace folders\n - a fully qualified name of a class.", "jdk.debugger.configuration.classPaths.description": "The classpaths for launching the JVM.", "jdk.debugger.configuration.console.description": "The specified console to launch the program.", diff --git a/vscode/package.nls.zh-cn.json b/vscode/package.nls.zh-cn.json index b727850..6cc9f88 100755 --- a/vscode/package.nls.zh-cn.json +++ b/vscode/package.nls.zh-cn.json @@ -46,7 +46,8 @@ "jdk.configuration.runConfig.cwd.description": "工作目录", "jdk.configuration.disableNbJavac.description": "高级选项:禁用 nb-javac 库,将使用来自所选 JDK 的 javac。所选 JDK 必须至少为 JDK 23。", "jdk.configuration.disableProjectSearchLimit.description": "高级选项:禁用在包含项目信息的文件夹中搜索的限制。", - "jdk.configuration.telemetry.enabled.description": "Allow the Oracle Java Visual Studio Code extension to collect and send usage data to Oracle servers to help improve the Java platform support. No personal information nor source code is collected. You may refer to our general privacy policy at https://www.oracle.com/legal/privacy/services-privacy-policy/", + "jdk.configuration.telemetry.enabled.description": "Allow the Oracle Java extension for Visual Studio Code (\"JVSCE\") to collect and send anonymous technical data commonly known as \"telemetry data\" to Oracle to help improve the Java platform. No personal information nor source code is collected. You may refer to the data collection and privacy policy for JVSCE at https://github.com/oracle/javavscode/blob/main/vscode/TELEMETRY.md", + "jdk.configuration.telemetry.enabled.markdownDescription": "Allow the Oracle Java extension for Visual Studio Code (\"*JVSCE*\") to collect and send anonymous technical data commonly known as \"*telemetry data*\" to Oracle to help improve the Java platform. No personal information nor source code is collected. You may refer to the data collection and privacy policy for JVSCE at [TELEMETRY.md](https://github.com/oracle/javavscode/blob/main/vscode/TELEMETRY.md)", "jdk.debugger.configuration.mainClass.markdownDescription": "Main class specification. Supported formats:\n - an absolute path\n - a path relative to any of the workspace folders\n - a fully qualified name of a class.", "jdk.debugger.configuration.classPaths.description": "用于启动 JVM 的类路径。", "jdk.debugger.configuration.console.description": "用于启动程序的指定控制台。", diff --git a/vscode/src/telemetry/telemetryManager.ts b/vscode/src/telemetry/telemetryManager.ts index 076801c..28b1242 100644 --- a/vscode/src/telemetry/telemetryManager.ts +++ b/vscode/src/telemetry/telemetryManager.ts @@ -58,7 +58,7 @@ export class TelemetryManager { const yesLabel = l10n.value("jdk.downloader.message.confirmation.yes"); const noLabel = l10n.value("jdk.downloader.message.confirmation.no"); - const telemetryLabel = l10n.value("jdk.telemetry.consent", { extensionName: this.extensionContextInfo.getPackageJson().name }); + const telemetryLabel = l10n.value("jdk.telemetry.consent"); const enable = await window.showInformationMessage(telemetryLabel, yesLabel, noLabel); if (enable == undefined) {