Skip to content

Commit

Permalink
Add tracing to Skate plugin (#617)
Browse files Browse the repository at this point in the history
Add the first set of metrics to be tracked for Skate plugin, including

* Whats New Panel:
      * When the What's new panel opened / closed
* Feature Flag Annotator:
      * When user clicked on Houston link from feature flag annotation
 * Project Generator:
* When the terminal opened and execute command to spin up ProjectGen UI

Noted that for the WhatsNewPanel, I added a custom
`WhatsNewToolWindowListener` to subscribe to the event for the window
opened/closed. I followed the set up described
[here](https://plugins.jetbrains.com/docs/intellij/plugin-listeners.html#defining-project-level-listeners)
to set up the listener


Testing:
Example traces sent:
https://analytics.tinyspeck.com/v3/explore/1000000001135498


  

<!--
  ⬆ Put your description above this! ⬆

  Please be descriptive and detailed.
  
Please read our [Contributing
Guidelines](https://github.com/tinyspeck/slack-gradle-plugin/blob/main/.github/CONTRIBUTING.md)
and [Code of Conduct](https://slackhq.github.io/code-of-conduct).

Don't worry about deleting this, it's not visible in the PR!
-->
  • Loading branch information
linhpha authored Oct 26, 2023
1 parent e60f4f6 commit b2709dd
Show file tree
Hide file tree
Showing 18 changed files with 507 additions and 18 deletions.
35 changes: 35 additions & 0 deletions skate-plugin/README.md
Original file line number Diff line number Diff line change
@@ -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`
6 changes: 5 additions & 1 deletion skate-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ class SkatePluginSettings : SimplePersistentStateComponent<SkatePluginSettings.S
state.featureFlagFilePattern = value
}

var isTracingEnabled: Boolean
get() = state.isTracingEnabled
set(value) {
state.isTracingEnabled = value
}

var tracingEndpoint: String?
get() = state.tracingEndpoint
set(value) {
state.tracingEndpoint = value
}

class State : BaseState() {
var whatsNewFilePath by string()
var isWhatsNewEnabled by property(true)
Expand All @@ -109,5 +121,7 @@ class SkatePluginSettings : SimplePersistentStateComponent<SkatePluginSettings.S
var featureFlagBaseUrl by string()
var featureFlagAnnotation by string()
var featureFlagFilePattern by string()
var isTracingEnabled by property(true)
var tracingEndpoint by string()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ interface SkateProjectService {
* New UI of the Skate Plugin
*/
class SkateProjectServiceImpl(private val project: Project) : SkateProjectService {

override fun showWhatsNewWindow() {

val settings = project.service<SkatePluginSettings>()
Expand All @@ -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()
Expand All @@ -71,4 +67,8 @@ class SkateProjectServiceImpl(private val project: Project) : SkateProjectServic
toolWindow.show()
}
}

companion object {
const val WHATS_NEW_PANEL_ID = "skate-whats-new"
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<FeatureFlagSymbol>, List<FeatureFlagSymbol>>() {

override fun collectInformation(file: PsiFile): List<FeatureFlagSymbol> {
val isEligibleForLinkifiedFeatureProcessing =
file.project.isLinkifiedFeatureFlagsEnabled() && file is KtFile && isKotlinFeatureFile(file)
Expand Down Expand Up @@ -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
Expand All @@ -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()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<SkatePluginSettings>()
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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<KeyValue> {
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
}
}
Loading

0 comments on commit b2709dd

Please sign in to comment.