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}`);