Skip to content

Commit 30ac508

Browse files
dhasani23David Hasani
and
David Hasani
authored
feat(amazonq): show users estimated cost of /transform (aws#4767)
* feat(amazonq): show users estimated cost of /transform * fix Lint * remove unused imports * fix Lint * localize string * add unit test * update LOC threshold to 100K * escape apostrophe to pass test * escape apostrophe in text * escape apostrophe correctly * update text * fix typo * update unit test * address comment * move constant * put constants together --------- Co-authored-by: David Hasani <davhasan@amazon.com>
1 parent 4f5c106 commit 30ac508

File tree

8 files changed

+84
-27
lines changed

8 files changed

+84
-27
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type" : "feature",
3+
"description" : "feat(Amazon Q Code Transformation): show pro tier users estimated cost of /transform on projects over 100K lines"
4+
}

plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeTransformTelemetryManager.kt

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,13 @@ import com.intellij.openapi.components.service
88
import com.intellij.openapi.project.Project
99
import org.apache.commons.codec.digest.DigestUtils
1010
import software.amazon.awssdk.services.codewhispererruntime.model.TransformationStatus
11-
import software.aws.toolkits.jetbrains.core.gettingstarted.editor.ActiveConnection
12-
import software.aws.toolkits.jetbrains.core.gettingstarted.editor.ActiveConnectionType
13-
import software.aws.toolkits.jetbrains.core.gettingstarted.editor.BearerTokenFeatureSet
14-
import software.aws.toolkits.jetbrains.core.gettingstarted.editor.checkBearerConnectionValidity
1511
import software.aws.toolkits.jetbrains.services.codemodernizer.model.CustomerSelection
1612
import software.aws.toolkits.jetbrains.services.codemodernizer.model.JobId
1713
import software.aws.toolkits.jetbrains.services.codemodernizer.model.ValidationResult
1814
import software.aws.toolkits.jetbrains.services.codemodernizer.state.CodeModernizerSessionState
1915
import software.aws.toolkits.jetbrains.services.codemodernizer.state.CodeTransformTelemetryState
2016
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.calculateTotalLatency
17+
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.getAuthType
2118
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.getJavaVersionFromProjectSetting
2219
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.getMavenVersion
2320
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.tryGetJdk
@@ -32,7 +29,6 @@ import software.aws.toolkits.telemetry.CodeTransformPatchViewerCancelSrcComponen
3229
import software.aws.toolkits.telemetry.CodeTransformPreValidationError
3330
import software.aws.toolkits.telemetry.CodeTransformVCSViewerSrcComponents
3431
import software.aws.toolkits.telemetry.CodetransformTelemetry
35-
import software.aws.toolkits.telemetry.CredentialSourceId
3632
import software.aws.toolkits.telemetry.Result
3733
import java.time.Instant
3834
import java.util.Base64
@@ -157,31 +153,17 @@ class CodeTransformTelemetryManager(private val project: Project) {
157153
fun dependenciesCopied() = CodetransformTelemetry.dependenciesCopied(codeTransformSessionId = sessionId)
158154

159155
fun jobIsStartedFromChatPrompt() {
160-
val connection = checkBearerConnectionValidity(project, BearerTokenFeatureSet.Q)
161-
var authType: CredentialSourceId? = null
162-
if (connection.connectionType == ActiveConnectionType.IAM_IDC && connection is ActiveConnection.ValidBearer) {
163-
authType = CredentialSourceId.IamIdentityCenter
164-
} else if (connection.connectionType == ActiveConnectionType.BUILDER_ID && connection is ActiveConnection.ValidBearer) {
165-
authType = CredentialSourceId.AwsId
166-
}
167-
CodetransformTelemetry.jobIsStartedFromChatPrompt(codeTransformSessionId = sessionId, credentialSourceId = authType)
156+
CodetransformTelemetry.jobIsStartedFromChatPrompt(codeTransformSessionId = sessionId, credentialSourceId = getAuthType(project))
168157
}
169158

170159
/**
171160
* END - DEPRECATED METRICS (below are new metrics to keep)
172161
*/
173162

174163
fun initiateTransform(telemetryErrorMessage: String? = null) {
175-
val connection = checkBearerConnectionValidity(project, BearerTokenFeatureSet.Q)
176-
var authType: CredentialSourceId? = null
177-
if (connection.connectionType == ActiveConnectionType.IAM_IDC && connection is ActiveConnection.ValidBearer) {
178-
authType = CredentialSourceId.IamIdentityCenter
179-
} else if (connection.connectionType == ActiveConnectionType.BUILDER_ID && connection is ActiveConnection.ValidBearer) {
180-
authType = CredentialSourceId.AwsId
181-
}
182164
CodetransformTelemetry.initiateTransform(
183165
codeTransformSessionId = sessionId,
184-
credentialSourceId = authType,
166+
credentialSourceId = getAuthType(project),
185167
result = if (telemetryErrorMessage.isNullOrEmpty()) Result.Succeeded else Result.Failed,
186168
reason = telemetryErrorMessage,
187169
)

plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/constants/CodeModernizerUIConstants.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ import javax.swing.BorderFactory
1515

1616
const val FEATURE_NAME = "Amazon Q Transform"
1717

18+
const val APPENDIX_TABLE_KEY = "-1"
19+
const val JOB_STATISTICS_TABLE_KEY = "0"
20+
const val LOC_THRESHOLD = 100000
21+
const val BILLING_RATE = 0.003
22+
1823
class CodeModernizerUIConstants {
1924

2025
object HEADER {

plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/plan/CodeModernizerPlanEditor.kt

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,18 @@ import com.intellij.ui.components.JBScrollPane
1818
import icons.AwsIcons
1919
import software.amazon.awssdk.services.codewhispererruntime.model.TransformationPlan
2020
import software.amazon.awssdk.services.codewhispererruntime.model.TransformationStep
21+
import software.aws.toolkits.jetbrains.services.codemodernizer.constants.APPENDIX_TABLE_KEY
2122
import software.aws.toolkits.jetbrains.services.codemodernizer.constants.CodeModernizerUIConstants
23+
import software.aws.toolkits.jetbrains.services.codemodernizer.constants.JOB_STATISTICS_TABLE_KEY
24+
import software.aws.toolkits.jetbrains.services.codemodernizer.constants.LOC_THRESHOLD
2225
import software.aws.toolkits.jetbrains.services.codemodernizer.model.PlanTable
2326
import software.aws.toolkits.jetbrains.services.codemodernizer.plan.CodeModernizerPlanEditorProvider.Companion.MIGRATION_PLAN_KEY
27+
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.getAuthType
28+
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.getBillingText
2429
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.getTableMapping
2530
import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.addHorizontalGlue
2631
import software.aws.toolkits.resources.message
32+
import software.aws.toolkits.telemetry.CredentialSourceId
2733
import java.awt.BorderLayout
2834
import java.awt.Color
2935
import java.awt.Component
@@ -51,9 +57,9 @@ import javax.swing.event.HyperlinkEvent
5157
import javax.swing.table.DefaultTableCellRenderer
5258
import javax.swing.table.DefaultTableModel
5359

54-
class CodeModernizerPlanEditor(val project: Project, val virtualFile: VirtualFile) : UserDataHolderBase(), FileEditor {
60+
class CodeModernizerPlanEditor(val project: Project, private val virtualFile: VirtualFile) : UserDataHolderBase(), FileEditor {
5561
val plan = virtualFile.getUserData(MIGRATION_PLAN_KEY) ?: throw RuntimeException("Migration plan not found")
56-
val tableMapping =
62+
private val tableMapping =
5763
if (!plan.transformationSteps()[0].progressUpdates().isNullOrEmpty()) {
5864
getTableMapping(plan.transformationSteps()[0].progressUpdates())
5965
} else {
@@ -72,17 +78,39 @@ class CodeModernizerPlanEditor(val project: Project, val virtualFile: VirtualFil
7278
)
7379
// key "0" reserved for job statistics table
7480
// comes from "name" field of each progressUpdate in step zero of plan
75-
if ("0" in tableMapping) {
81+
if (JOB_STATISTICS_TABLE_KEY in tableMapping) {
82+
val planTable = mapper.readValue(tableMapping[JOB_STATISTICS_TABLE_KEY], PlanTable::class.java)
83+
val linesOfCode = planTable.rows.find { it.name == "linesOfCode" }?.value?.toInt()
84+
if (linesOfCode != null && linesOfCode > LOC_THRESHOLD && getAuthType(project) == CredentialSourceId.IamIdentityCenter) {
85+
val billingText = getBillingText(linesOfCode)
86+
val billingTextComponent =
87+
JEditorPane("text/html", billingText).apply {
88+
addHyperlinkListener { he ->
89+
if (he.eventType == HyperlinkEvent.EventType.ACTIVATED) {
90+
BrowserUtil.browse(he.url)
91+
}
92+
}
93+
isEditable = false
94+
isOpaque = false
95+
alignmentX = Component.LEFT_ALIGNMENT
96+
font =
97+
font.deriveFont(
98+
CodeModernizerUIConstants.FONT_CONSTRAINTS.BOLD,
99+
CodeModernizerUIConstants.PLAN_CONSTRAINTS.SUBTITLE_FONT_SIZE,
100+
)
101+
}
102+
add(billingTextComponent, CodeModernizerUIConstants.transformationPlanPlaneConstraint)
103+
}
76104
add(
77-
transformationPlanInfo(mapper.readValue(tableMapping["0"], PlanTable::class.java)),
105+
transformationPlanInfo(planTable),
78106
CodeModernizerUIConstants.transformationPlanPlaneConstraint,
79107
)
80108
}
81109
add(transformationPlanPanel(plan), CodeModernizerUIConstants.transformationPlanPlaneConstraint)
82110
// key "-1" reserved for appendix table
83-
if ("-1" in tableMapping) {
111+
if (APPENDIX_TABLE_KEY in tableMapping) {
84112
add(
85-
transformationPlanAppendix(mapper.readValue(tableMapping["-1"], PlanTable::class.java)),
113+
transformationPlanAppendix(mapper.readValue(tableMapping[APPENDIX_TABLE_KEY], PlanTable::class.java)),
86114
CodeModernizerUIConstants.transformationPlanPlaneConstraint,
87115
)
88116
}

plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/utils/CodeTransformApiUtils.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@ import software.aws.toolkits.core.utils.WaiterUnrecoverableException
2121
import software.aws.toolkits.core.utils.Waiters.waitUntil
2222
import software.aws.toolkits.jetbrains.services.codemodernizer.CodeTransformTelemetryManager
2323
import software.aws.toolkits.jetbrains.services.codemodernizer.client.GumbyClient
24+
import software.aws.toolkits.jetbrains.services.codemodernizer.constants.BILLING_RATE
2425
import software.aws.toolkits.jetbrains.services.codemodernizer.model.JobId
26+
import software.aws.toolkits.resources.message
2527
import java.lang.Thread.sleep
2628
import java.time.Duration
29+
import java.util.Locale
2730
import java.util.concurrent.atomic.AtomicBoolean
2831

2932
data class PollingResult(
@@ -128,3 +131,8 @@ suspend fun JobId.pollTransformationStatusAndPlan(
128131
fun getTableMapping(stepZeroProgressUpdates: List<TransformationProgressUpdate>) = stepZeroProgressUpdates.associate {
129132
it.name() to it.description()
130133
}
134+
135+
fun getBillingText(linesOfCode: Int): String {
136+
val estimatedCost = String.format(Locale.US, "%.2f", linesOfCode.times(BILLING_RATE))
137+
return message("codemodernizer.migration_plan.header.billing_text", linesOfCode, BILLING_RATE, estimatedCost)
138+
}

plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/utils/CodeTransformUtils.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,13 @@ import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
1313
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
1414
import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
1515
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProvider
16+
import software.aws.toolkits.jetbrains.core.gettingstarted.editor.ActiveConnection
17+
import software.aws.toolkits.jetbrains.core.gettingstarted.editor.ActiveConnectionType
18+
import software.aws.toolkits.jetbrains.core.gettingstarted.editor.BearerTokenFeatureSet
19+
import software.aws.toolkits.jetbrains.core.gettingstarted.editor.checkBearerConnectionValidity
1620
import software.aws.toolkits.jetbrains.utils.actions.OpenBrowserAction
1721
import software.aws.toolkits.resources.message
22+
import software.aws.toolkits.telemetry.CredentialSourceId
1823

1924
val STATES_WHERE_PLAN_EXIST = setOf(
2025
TransformationStatus.PLANNED,
@@ -44,6 +49,17 @@ fun refreshToken(project: Project) {
4449
provider.refresh()
4550
}
4651

52+
fun getAuthType(project: Project): CredentialSourceId? {
53+
val connection = checkBearerConnectionValidity(project, BearerTokenFeatureSet.Q)
54+
var authType: CredentialSourceId? = null
55+
if (connection.connectionType == ActiveConnectionType.IAM_IDC && connection is ActiveConnection.ValidBearer) {
56+
authType = CredentialSourceId.IamIdentityCenter
57+
} else if (connection.connectionType == ActiveConnectionType.BUILDER_ID && connection is ActiveConnection.ValidBearer) {
58+
authType = CredentialSourceId.AwsId
59+
}
60+
return authType
61+
}
62+
4763
fun getQTokenProvider(project: Project) = (
4864
ToolkitConnectionManager
4965
.getInstance(project)

plugins/amazonq/codetransform/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codemodernizer/CodeWhispererCodeModernizerUtilsTest.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import software.amazon.awssdk.services.codewhispererruntime.model.AccessDeniedEx
1919
import software.amazon.awssdk.services.codewhispererruntime.model.TransformationProgressUpdate
2020
import software.amazon.awssdk.services.codewhispererruntime.model.TransformationStatus
2121
import software.amazon.awssdk.services.ssooidc.model.InvalidGrantException
22+
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.getBillingText
2223
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.getTableMapping
2324
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.pollTransformationStatusAndPlan
2425
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.refreshToken
@@ -204,4 +205,16 @@ class CodeWhispererCodeModernizerUtilsTest : CodeWhispererCodeModernizerTestBase
204205
val expected = mapOf("0" to jobStats, "1" to depChanges, "2" to apiChanges, "-1" to fileChanges)
205206
assertThat(expected).isEqualTo(actual)
206207
}
208+
209+
@Test
210+
fun `getBillingText on small project returns correct String`() {
211+
val expected = "<html><body style=\"line-height:2; font-family: Arial, sans-serif; font-size: 14;\"><br>" +
212+
"376 lines of code were submitted for transformation. If you reach the quota for lines of code included " +
213+
"in your subscription, you will be charged $0.003 for each additional line of code. You might be charged up " +
214+
"to $1.13 for this transformation. To avoid being charged, stop the transformation job before it completes. " +
215+
"For more information on pricing and quotas, see <a href=\"https://aws.amazon.com/q/developer/pricing/\">" +
216+
"Amazon Q Developer pricing</a>.</p>"
217+
val actual = getBillingText(376)
218+
assertThat(expected).isEqualTo(actual)
219+
}
207220
}

plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,7 @@ codemodernizer.migration_plan.body.steps_intro_title=Planned transformation chan
668668
codemodernizer.migration_plan.body.steps_name=<html><body>{0}</body></html>
669669
codemodernizer.migration_plan.body.steps_scroll_top=<html><body style="font-family: Arial, sans-serif; font-size: 14;"><a href="#top">Scroll to top</a></body></html>
670670
codemodernizer.migration_plan.header.awsq=<html><body style="line-height:2; font-family: Arial, sans-serif; font-size: 14;">Amazon Q reviewed your code and generated a transformation plan. Amazon Q will suggest code changes according to the plan, and you can review the updated code before accepting changes to your files.</body></html>
671+
codemodernizer.migration_plan.header.billing_text=<html><body style="line-height:2; font-family: Arial, sans-serif; font-size: 14;"><br>{0} lines of code were submitted for transformation. If you reach the quota for lines of code included in your subscription, you will be charged ${1} for each additional line of code. You might be charged up to ${2} for this transformation. To avoid being charged, stop the transformation job before it completes. For more information on pricing and quotas, see <a href="https://aws.amazon.com/q/developer/pricing/">Amazon Q Developer pricing</a>.</p>
671672
codemodernizer.migration_plan.header.description=Plan to Transform your project
672673
codemodernizer.migration_plan.header.title=Code Transformation plan by Amazon Q
673674
codemodernizer.migration_plan.substeps.description_failed=Build failed

0 commit comments

Comments
 (0)