diff --git a/skate-plugin/README.md b/skate-plugin/README.md new file mode 100644 index 000000000..38684acca --- /dev/null +++ b/skate-plugin/README.md @@ -0,0 +1,35 @@ +Skate Plugin +========================= + +An Intellij plugin that helps to improve local developer productivity by surfacing useful information in the IDE. + +We use this at Slack for s several use cases: +* Announce updates, latest changes and improvements through "What's New" panel +* Annotate Feature Flag to help easily access its setup +* Generate API model translators when migrating legacy API +* Create initial new subproject setup from `File` dropdown + +## Installation + +#### Artifactory +1. Install `artifactory-authenticator` plugin from disk and authenticate with Artifactory +2. Add custom plugin repository link from "Manage Plugin Repositories" +3. Search "Skate" in the plugins marketplace and install it + +#### Local testing +1. Build local version of the plugin with `./gradlew buildPlugin` +2. Open IDE settings, then "Install Plugin from Disk..." + +## Implementation +All registered plugin actions can be found in `skate.xml` config file + +## Tracing +We're sending analytics for almost all Skate features to track user usage. To set this up, +1. Register new feature event in `SkateTracingEvent` +2. Use `SkateSpanBuilder` to create the span for event you want to track +3. Make call to `SkateTraceReporter` to send up the traces + +## Publishing +Run `publish-skate` Github Action to publish to the configured repository. + +Behind the scene the action's running`./gradlew :skate-plugin:uploadPluginToArtifactory` diff --git a/skate-plugin/build.gradle.kts b/skate-plugin/build.gradle.kts index 0fa308153..e39235253 100644 --- a/skate-plugin/build.gradle.kts +++ b/skate-plugin/build.gradle.kts @@ -1,8 +1,8 @@ import com.jetbrains.plugin.structure.base.utils.exists +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.nio.file.Paths import java.util.Locale import kotlin.io.path.readText -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { java @@ -90,6 +90,10 @@ tasks dependencies { implementation(libs.bugsnag) { exclude(group = "org.slf4j") } + implementation(libs.okhttp) + implementation(libs.okhttp.loggingInterceptor) + implementation(projects.tracing) + testImplementation(libs.junit) testImplementation(libs.truth) } diff --git a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/SkatePluginSettings.kt b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/SkatePluginSettings.kt index 7b09de6bf..91d8dc829 100644 --- a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/SkatePluginSettings.kt +++ b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/SkatePluginSettings.kt @@ -98,6 +98,18 @@ class SkatePluginSettings : SimplePersistentStateComponent() @@ -50,17 +49,14 @@ class SkateProjectServiceImpl(private val project: Project) : SkateProjectServic // Changelog is parsed val parsedChangelog = ChangelogParser.readFile(changeLogString, changelogJournal.lastReadDate) if (parsedChangelog.changeLogString.isNullOrBlank()) return - // Creating the tool window val toolWindowManager = ToolWindowManager.getInstance(project) - toolWindowManager.invokeLater { val toolWindow = - toolWindowManager.registerToolWindow("skate-whats-new") { + toolWindowManager.registerToolWindow(WHATS_NEW_PANEL_ID) { stripeTitle = Supplier { "What's New in Slack!" } anchor = ToolWindowAnchor.RIGHT } - // The Disposable is necessary to prevent a substantial memory leak while working with // MarkdownJCEFHtmlPanel val parentDisposable = Disposer.newDisposable() @@ -71,4 +67,8 @@ class SkateProjectServiceImpl(private val project: Project) : SkateProjectServic toolWindow.show() } } + + companion object { + const val WHATS_NEW_PANEL_ID = "skate-whats-new" + } } diff --git a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/WhatsNewToolWindowListener.kt b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/WhatsNewToolWindowListener.kt new file mode 100644 index 000000000..41e5b80ed --- /dev/null +++ b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/WhatsNewToolWindowListener.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2023 Slack Technologies, LLC + * + * 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 com.slack.sgp.intellij + +import com.intellij.openapi.project.Project +import com.intellij.openapi.wm.ToolWindowManager +import com.intellij.openapi.wm.ex.ToolWindowManagerListener +import com.slack.sgp.intellij.SkateProjectServiceImpl.Companion.WHATS_NEW_PANEL_ID +import com.slack.sgp.intellij.tracing.SkateSpanBuilder +import com.slack.sgp.intellij.tracing.SkateTraceReporter +import com.slack.sgp.intellij.tracing.SkateTracingEvent +import com.slack.sgp.intellij.tracing.SkateTracingEvent.EventType.SKATE_WHATS_NEW_PANEL_CLOSED +import com.slack.sgp.intellij.tracing.SkateTracingEvent.EventType.SKATE_WHATS_NEW_PANEL_OPENED +import com.slack.sgp.intellij.util.isTracingEnabled +import java.time.Instant + +/** Custom listener for WhatsNew Tool Window. */ +class WhatsNewToolWindowListener(private val project: Project) : ToolWindowManagerListener { + // Initial state of screen should be hidden + private var wasVisible = false + private val startTimestamp = Instant.now() + + override fun stateChanged(toolWindowManager: ToolWindowManager) { + super.stateChanged(toolWindowManager) + if (!project.isTracingEnabled()) return + + val skateSpanBuilder = SkateSpanBuilder() + val toolWindow = toolWindowManager.getToolWindow(WHATS_NEW_PANEL_ID) ?: return + val isVisible = toolWindow.isVisible + val visibilityChanged = visibilityChanged(isVisible) + + if (visibilityChanged) { + if (isVisible) { + skateSpanBuilder.addSpanTag("event", SkateTracingEvent(SKATE_WHATS_NEW_PANEL_OPENED)) + } else { + skateSpanBuilder.addSpanTag("event", SkateTracingEvent(SKATE_WHATS_NEW_PANEL_CLOSED)) + } + SkateTraceReporter(project) + .createPluginUsageTraceAndSendTrace( + WHATS_NEW_PANEL_ID.replace('-', '_'), + startTimestamp, + skateSpanBuilder.getKeyValueList() + ) + } + } + + fun visibilityChanged(isVisible: Boolean): Boolean { + val visibilityChanged = isVisible != wasVisible + wasVisible = isVisible + return visibilityChanged + } +} diff --git a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/featureflags/FeatureFlagAnnotator.kt b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/featureflags/FeatureFlagAnnotator.kt index 972305dd6..da8378e74 100644 --- a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/featureflags/FeatureFlagAnnotator.kt +++ b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/featureflags/FeatureFlagAnnotator.kt @@ -23,13 +23,18 @@ import com.intellij.lang.annotation.HighlightSeverity import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.PsiFile +import com.slack.sgp.intellij.tracing.SkateSpanBuilder +import com.slack.sgp.intellij.tracing.SkateTraceReporter +import com.slack.sgp.intellij.tracing.SkateTracingEvent +import com.slack.sgp.intellij.tracing.SkateTracingEvent.EventType.HOUSTON_FEATURE_FLAG_URL_CLICKED import com.slack.sgp.intellij.util.featureFlagFilePattern import com.slack.sgp.intellij.util.isLinkifiedFeatureFlagsEnabled +import com.slack.sgp.intellij.util.isTracingEnabled import java.net.URI +import java.time.Instant import org.jetbrains.kotlin.psi.KtFile class FeatureFlagAnnotator : ExternalAnnotator, List>() { - override fun collectInformation(file: PsiFile): List { val isEligibleForLinkifiedFeatureProcessing = file.project.isLinkifiedFeatureFlagsEnabled() && file is KtFile && isKotlinFeatureFile(file) @@ -69,6 +74,10 @@ class UrlIntentionAction( private val message: String, private val url: String, ) : IntentionAction { + + private val startTimestamp = Instant.now() + private val skateSpanBuilder = SkateSpanBuilder() + override fun getText(): String = message override fun getFamilyName(): String = text @@ -77,9 +86,21 @@ class UrlIntentionAction( override fun invoke(project: Project, editor: Editor?, file: PsiFile?) { BrowserUtil.browse(URI(url)) + skateSpanBuilder.addSpanTag("event", SkateTracingEvent(HOUSTON_FEATURE_FLAG_URL_CLICKED)) + sendUsageTrace(project, project.isTracingEnabled()) } override fun startInWriteAction(): Boolean { return false } + + fun sendUsageTrace(project: Project, isTracingEnabled: Boolean) { + if (!isTracingEnabled) return + SkateTraceReporter(project) + .createPluginUsageTraceAndSendTrace( + "feature_flag_annotator", + startTimestamp, + skateSpanBuilder.getKeyValueList() + ) + } } diff --git a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/ProjectGenMenuAction.kt b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/ProjectGenMenuAction.kt index e8b521b1b..b81e26c73 100644 --- a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/ProjectGenMenuAction.kt +++ b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/ProjectGenMenuAction.kt @@ -17,28 +17,51 @@ package com.slack.sgp.intellij.projectgen import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.components.service import com.intellij.openapi.project.Project -import com.slack.sgp.intellij.SkatePluginSettings +import com.slack.sgp.intellij.tracing.SkateSpanBuilder +import com.slack.sgp.intellij.tracing.SkateTraceReporter +import com.slack.sgp.intellij.tracing.SkateTracingEvent +import com.slack.sgp.intellij.tracing.SkateTracingEvent.EventType.PROJECT_GEN_OPENED +import com.slack.sgp.intellij.util.isProjectGenMenuActionEnabled +import com.slack.sgp.intellij.util.isTracingEnabled +import com.slack.sgp.intellij.util.projectGenRunCommand +import java.time.Instant class ProjectGenMenuAction @JvmOverloads constructor( - private val terminalViewWrapper: (Project) -> TerminalViewWrapper = ::RealTerminalViewWrapper + private val terminalViewWrapper: (Project) -> TerminalViewWrapper = ::RealTerminalViewWrapper, + private val offline: Boolean = false ) : AnAction() { + private val skateSpanBuilder = SkateSpanBuilder() + private val startTimestamp = Instant.now() + override fun actionPerformed(e: AnActionEvent) { val currentProject: Project = e.project ?: return - val settings = currentProject.service() - val isProjectGenMenuActionEnabled = settings.isProjectGenMenuActionEnabled - val projectGenRunCommand = settings.projectGenRunCommand - if (!isProjectGenMenuActionEnabled) return + val projectGenRunCommand = currentProject.projectGenRunCommand() + if (!currentProject.isProjectGenMenuActionEnabled()) return + executeProjectGenCommand(projectGenRunCommand, currentProject) + + if (currentProject.isTracingEnabled()) { + sendUsageTrace(currentProject) + } } fun executeProjectGenCommand(command: String, project: Project) { val terminalCommand = TerminalCommand(command, project.basePath, PROJECT_GEN_TAB_NAME) terminalViewWrapper(project).executeCommand(terminalCommand) + skateSpanBuilder.addSpanTag("event", SkateTracingEvent(PROJECT_GEN_OPENED)) + } + + fun sendUsageTrace(project: Project) { + SkateTraceReporter(project, offline) + .createPluginUsageTraceAndSendTrace( + "project_generator", + startTimestamp, + skateSpanBuilder.getKeyValueList() + ) } companion object { diff --git a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/tracing/SkateSpanBuilder.kt b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/tracing/SkateSpanBuilder.kt new file mode 100644 index 000000000..e95a9dab0 --- /dev/null +++ b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/tracing/SkateSpanBuilder.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 Slack Technologies, LLC + * + * 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 com.slack.sgp.intellij.tracing + +import com.slack.sgp.tracing.KeyValue +import com.slack.sgp.tracing.model.TagBuilder +import com.slack.sgp.tracing.model.newTagBuilder + +class SkateSpanBuilder { + private val keyValueList: TagBuilder = newTagBuilder() + + fun addSpanTag(key: String, value: String) { + keyValueList.apply { key tagTo value } + } + + fun addSpanTag(key: String, value: SkateTracingEvent) { + keyValueList.apply { key tagTo value.type.name } + } + + fun getKeyValueList(): List { + return keyValueList.toList() + } +} + +class SkateTracingEvent(val type: EventType) { + enum class EventType { + PROJECT_GEN_OPENED, + HOUSTON_FEATURE_FLAG_URL_CLICKED, + SKATE_WHATS_NEW_PANEL_OPENED, + SKATE_WHATS_NEW_PANEL_CLOSED + } +} diff --git a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/tracing/SkateTraceReporter.kt b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/tracing/SkateTraceReporter.kt new file mode 100644 index 000000000..96f88d7b9 --- /dev/null +++ b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/tracing/SkateTraceReporter.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2023 Slack Technologies, LLC + * + * 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 com.slack.sgp.intellij.tracing + +import com.intellij.ide.plugins.PluginManagerCore +import com.intellij.openapi.application.ApplicationInfo +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.extensions.PluginId +import com.intellij.openapi.project.Project +import com.slack.sgp.intellij.util.tracingEndpoint +import com.slack.sgp.tracing.KeyValue +import com.slack.sgp.tracing.ListOfSpans +import com.slack.sgp.tracing.model.buildSpan +import com.slack.sgp.tracing.model.newTagBuilder +import com.slack.sgp.tracing.reporter.SimpleTraceReporter +import com.slack.sgp.tracing.reporter.TraceReporter +import com.slack.sgp.tracing.reporter.TraceReporter.NoOpTraceReporter +import java.time.Duration +import java.time.Instant +import kotlin.time.DurationUnit +import kotlin.time.toDuration +import kotlinx.coroutines.runBlocking +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import org.apache.http.HttpException + +class SkateTraceReporter(private val project: Project, private val offline: Boolean = false) : + TraceReporter { + + private val delegate by lazy { + val okHttpClient = lazy { OkHttpClient.Builder().build() } + val loggingInterceptor = + HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY } + + val loggingClient = lazy { + okHttpClient.value.newBuilder().addInterceptor(loggingInterceptor).build() + } + if (offline) { + NoOpTraceReporter + } else { + val endPoint = + project.tracingEndpoint() + ?: throw RuntimeException("Required tracing endpoint to upload analytics") + SimpleTraceReporter(endPoint, loggingClient) + } + } + + override suspend fun sendTrace(spans: ListOfSpans) { + try { + delegate.sendTrace(spans) + LOG.info(spans.toString()) + } catch (httpException: HttpException) { + LOG.error("Uploading Skate plugin trace failed.", httpException) + } + } + + fun createPluginUsageTraceAndSendTrace( + spanName: String, + startTimestamp: Instant, + spanDataMap: List, + ideVersion: String = ApplicationInfo.getInstance().fullVersion, + skatePluginVersion: String? = + PluginManagerCore.getPlugin(PluginId.getId("com.slack.intellij.skate"))?.version + ): ListOfSpans? { + if (spanDataMap.isEmpty() || skatePluginVersion.isNullOrBlank()) { + return null + } + val traceTags = + newTagBuilder().apply { + "service_name" tagTo SERVICE_NAME + "database" tagTo DATABASE_NAME + } + val span = + buildSpan( + name = spanName, + startTimestampMicros = + startTimestamp.toEpochMilli().toDuration(DurationUnit.MILLISECONDS).inWholeMicroseconds, + durationMicros = + Duration.between(startTimestamp, Instant.now()) + .toMillis() + .toDuration(DurationUnit.MILLISECONDS) + .inWholeMicroseconds + ) { + "skate_version" tagTo skatePluginVersion + "ide_version" tagTo ideVersion + System.getenv("USER").takeUnless { it.isBlank() }?.let { "user" tagTo it } + "project_name" tagTo project.name + this.addAll(spanDataMap) + } + val spans = ListOfSpans(spans = listOf(span), tags = traceTags) + runBlocking { sendTrace(spans) } + return spans + } + + companion object { + const val SERVICE_NAME: String = "skate_plugin" + const val DATABASE_NAME: String = "itools" + private val LOG: Logger = Logger.getInstance(SkateTraceReporter::class.java) + } +} diff --git a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/ui/SkateConfigUI.kt b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/ui/SkateConfigUI.kt index c7653f1c4..becdfc6a0 100644 --- a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/ui/SkateConfigUI.kt +++ b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/ui/SkateConfigUI.kt @@ -40,6 +40,7 @@ internal class SkateConfigUI( choosePathRow() enableProjectGenMenuAction() featureFlagSettings() + tracingSettings() } private fun Panel.checkBoxRow() { @@ -125,6 +126,26 @@ internal class SkateConfigUI( ) } + private fun Panel.tracingSettings() { + lateinit var tracingEnabledButton: Cell + + row(SkateBundle.message("skate.configuration.enableTracing.title")) { + tracingEnabledButton = + checkBox(SkateBundle.message("skate.configuration.enableTracing.description")) + .bindSelected( + getter = { settings.isTracingEnabled }, + setter = { settings.isTracingEnabled = it } + ) + } + bindAndValidateTextFieldRow( + titleMessageKey = "skate.configuration.tracingEndpoint.title", + getter = { settings.tracingEndpoint }, + setter = { settings.tracingEndpoint = it }, + errorMessageKey = "skate.configuration.tracingEndpoint.description", + enabledCondition = tracingEnabledButton.selected + ) + } + private fun Panel.bindAndValidateTextFieldRow( titleMessageKey: String, getter: () -> String?, diff --git a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/util/ProjectUtils.kt b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/util/ProjectUtils.kt index 4baf14045..2ed41173c 100644 --- a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/util/ProjectUtils.kt +++ b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/util/ProjectUtils.kt @@ -24,3 +24,11 @@ fun Project.settings(): SkatePluginSettings = service() fun Project.isLinkifiedFeatureFlagsEnabled(): Boolean = settings().isLinkifiedFeatureFlagsEnabled fun Project.featureFlagFilePattern(): String? = settings().featureFlagFilePattern + +fun Project.isTracingEnabled(): Boolean = settings().isTracingEnabled + +fun Project.isProjectGenMenuActionEnabled(): Boolean = settings().isProjectGenMenuActionEnabled + +fun Project.projectGenRunCommand(): String = settings().projectGenRunCommand + +fun Project.tracingEndpoint(): String? = settings().tracingEndpoint diff --git a/skate-plugin/src/main/resources/META-INF/plugin.xml b/skate-plugin/src/main/resources/META-INF/plugin.xml index 5ea61bb65..abc65e65b 100644 --- a/skate-plugin/src/main/resources/META-INF/plugin.xml +++ b/skate-plugin/src/main/resources/META-INF/plugin.xml @@ -40,6 +40,11 @@ + + + ().isProjectGenMenuActionEnabled = false // Perform action diff --git a/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/SkateTraceReporterTest.kt b/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/SkateTraceReporterTest.kt new file mode 100644 index 000000000..6aaed0925 --- /dev/null +++ b/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/SkateTraceReporterTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2023 Slack Technologies, LLC + * + * 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 com.slack.sgp.intellij + +import com.google.common.truth.Truth.assertThat +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.slack.sgp.intellij.tracing.SkateTraceReporter +import com.slack.sgp.intellij.tracing.SkateTraceReporter.Companion.DATABASE_NAME +import com.slack.sgp.intellij.tracing.SkateTraceReporter.Companion.SERVICE_NAME +import com.slack.sgp.tracing.KeyValue +import com.slack.sgp.tracing.ValueType +import com.slack.sgp.tracing.model.newTagBuilder +import java.time.Instant + +class SkateTraceReporterTest : BasePlatformTestCase() { + fun testSpanCreatedWithCorrectTags() { + val traceTags = + newTagBuilder().apply { + "test_string" tagTo "test_value" + "test_boolean" tagTo true + "test_long" tagTo 10000000L + "test_double" tagTo "1.25" + } + + val listOfSpans = + SkateTraceReporter(project, offline = true) + .createPluginUsageTraceAndSendTrace( + "fake_span_name", + Instant.now(), + traceTags, + "Studio Giraffe", + "0.2.0" + ) + val expectedTags = + mutableListOf( + KeyValue("service_name", ValueType.STRING, SERVICE_NAME), + KeyValue("database", ValueType.STRING, DATABASE_NAME) + ) + + val expectedSpanTags = + newTagBuilder().apply { + "skate_version" tagTo "0.2.0" + "ide_version" tagTo "Studio Giraffe" + "user" tagTo System.getenv("USER") + "project_name" tagTo project.name + addAll(traceTags) + } + + assertThat(listOfSpans).isNotNull() + if (listOfSpans != null) { + assertThat(listOfSpans.tags).isEqualTo(expectedTags) + assertThat(listOfSpans.spans.first().tags).isEqualTo(expectedSpanTags.toList()) + } + } + + fun testSpanNotCreatedWhenSpanDataIsEmpty() { + val listOfSpans = + SkateTraceReporter(project, offline = true) + .createPluginUsageTraceAndSendTrace( + "fake_span_name", + Instant.now(), + newTagBuilder(), + "Studio Giraffe", + "0.2.0" + ) + assertThat(listOfSpans).isNull() + } + + fun testSpanNotCreatedWhenIdeVersionEmpty() { + val listOfSpans = + SkateTraceReporter(project, offline = true) + .createPluginUsageTraceAndSendTrace( + "fake_span_name", + Instant.now(), + newTagBuilder(), + "Studio Giraffe", + "" + ) + assertThat(listOfSpans).isNull() + } +} diff --git a/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/WhatsNewToolWindowListenerTest.kt b/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/WhatsNewToolWindowListenerTest.kt new file mode 100644 index 000000000..e6b441ac9 --- /dev/null +++ b/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/WhatsNewToolWindowListenerTest.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2023 Slack Technologies, LLC + * + * 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 com.slack.sgp.intellij + +import com.intellij.openapi.wm.* +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import junit.framework.TestCase + +class WhatsNewToolWindowListenerTest : BasePlatformTestCase() { + + fun testVisibilityChangedFromInitialState() { + val whatsNewToolWindowListener = WhatsNewToolWindowListener(project) + val visibilityChanged = whatsNewToolWindowListener.visibilityChanged(true) + TestCase.assertEquals(visibilityChanged, true) + } + + fun testVisibilityChangedFalseWhenStateRemains() { + val whatsNewToolWindowListener = WhatsNewToolWindowListener(project) + val visibilityChanged = whatsNewToolWindowListener.visibilityChanged(false) + TestCase.assertEquals(visibilityChanged, false) + } +} diff --git a/tracing/src/main/kotlin/com/slack/sgp/tracing/model/Spans.kt b/tracing/src/main/kotlin/com/slack/sgp/tracing/model/Spans.kt index 28d946df1..e1fbf12ef 100644 --- a/tracing/src/main/kotlin/com/slack/sgp/tracing/model/Spans.kt +++ b/tracing/src/main/kotlin/com/slack/sgp/tracing/model/Spans.kt @@ -35,6 +35,6 @@ public fun buildSpan( trace_id = traceId, start_timestamp_micros = startTimestampMicros, duration_micros = durationMicros, - tags = tagBuilderImpl().apply(addTags) + tags = newTagBuilder().apply(addTags) ) } diff --git a/tracing/src/main/kotlin/com/slack/sgp/tracing/model/TagBuilder.kt b/tracing/src/main/kotlin/com/slack/sgp/tracing/model/TagBuilder.kt index ffe244fb6..3f548dcbe 100644 --- a/tracing/src/main/kotlin/com/slack/sgp/tracing/model/TagBuilder.kt +++ b/tracing/src/main/kotlin/com/slack/sgp/tracing/model/TagBuilder.kt @@ -37,5 +37,5 @@ public interface TagBuilder : MutableList { KeyValue(key = this, v_type = ValueType.BINARY, v_binary = value).also(::add) } -internal fun tagBuilderImpl(): TagBuilder = +public fun newTagBuilder(): TagBuilder = object : TagBuilder, MutableList by mutableListOf() {}