Skip to content

Commit 965969c

Browse files
telemetry(webview): Emit toolkit_X module telemetry on auth webview
Problem: With our telemetry, we do not know when the frontend webview UI has actually loaded Solution: Emit certain metrics during the webview loading process to get a better idea of if the webview UI successfully completed its initial load. - toolkit_willOpenModule, indicates intent to render a webview - toolkit_didLoadModule, indicates the final result of loading the webview - On Success it it just a success result. We know a success when the frontend send a successful message to the backend. It knows this by ensuring there were no errors and that a certain HTML element can be found, then once the page finishes its initial load it will send a success message to the backend. - On Failure, what happens is a timer times out after 10 seconds. Signed-off-by: nkomonen-amazon <[email protected]>
1 parent 2c0b4a5 commit 965969c

File tree

9 files changed

+243
-37
lines changed

9 files changed

+243
-37
lines changed

packages/core/src/amazonq/webview/messages/messageDispatcher.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import globals from '../../../shared/extensionGlobals'
1616
import { openUrl } from '../../../shared/utilities/vsCodeUtils'
1717
import { DefaultAmazonQAppInitContext } from '../../apps/initContext'
1818

19+
const qChatModuleName = 'amazonqChat'
20+
1921
export function dispatchWebViewMessagesToApps(
2022
webview: Webview,
2123
webViewToAppsMessagePublishers: Map<TabType, MessagePublisher<any>>
@@ -29,8 +31,8 @@ export function dispatchWebViewMessagesToApps(
2931
* This would be equivalent of the duration between "user clicked open q" and "ui has become available"
3032
* NOTE: Amazon Q UI is only loaded ONCE. The state is saved between each hide/show of the webview.
3133
*/
32-
telemetry.webview_load.emit({
33-
webviewName: 'amazonq',
34+
telemetry.toolkit_didLoadModule.emit({
35+
module: qChatModuleName,
3436
duration: performance.measure(amazonqMark.uiReady, amazonqMark.open).duration,
3537
result: 'Succeeded',
3638
})
@@ -86,12 +88,19 @@ export function dispatchWebViewMessagesToApps(
8688
}
8789

8890
if (msg.type === 'error') {
89-
const event = msg.event === 'webview_load' ? telemetry.webview_load : telemetry.webview_error
90-
event.emit({
91-
webviewName: 'amazonqChat',
92-
result: 'Failed',
93-
reasonDesc: msg.errorMessage,
94-
})
91+
if (msg.event === 'toolkit_didLoadModule') {
92+
telemetry.toolkit_didLoadModule.emit({
93+
module: qChatModuleName,
94+
result: 'Failed',
95+
reasonDesc: msg.errorMessage,
96+
})
97+
} else {
98+
telemetry.webview_error.emit({
99+
webviewName: qChatModuleName,
100+
result: 'Failed',
101+
reasonDesc: msg.errorMessage,
102+
})
103+
}
95104
return
96105
}
97106

packages/core/src/amazonq/webview/ui/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export const createMynahUI = (
6363
const { error, message } = e
6464
ideApi.postMessage({
6565
type: 'error',
66-
event: connector.isUIReady ? 'webview_error' : 'webview_load',
66+
event: connector.isUIReady ? 'webview_error' : 'toolkit_didLoadModule',
6767
errorMessage: error ? error.toString() : message,
6868
})
6969
})

packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const className = 'AmazonQLoginWebview'
2727
export class AmazonQLoginWebview extends CommonAuthWebview {
2828
public override id: string = 'aws.amazonq.AmazonCommonAuth'
2929
public static sourcePath: string = 'vue/src/login/webview/vue/amazonq/index.js'
30+
public override supportsLoadTelemetry: boolean = true
3031

3132
override onActiveConnectionModified = new vscode.EventEmitter<void>()
3233

packages/core/src/login/webview/vue/backend.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,25 @@ export abstract class CommonAuthWebview extends VueWebview {
6262
}
6363

6464
private didCall: { login: boolean; reauth: boolean } = { login: false, reauth: false }
65-
public setUiReady(state: 'login' | 'reauth') {
66-
// Prevent telemetry spam, since showing/hiding chat triggers this each time.
67-
// So only emit once.
65+
/**
66+
* Called when the UI load process is completed, regardless of success or failure
67+
*
68+
* @param errorMessage IF an error is caught on the frontend, this is the message. It will result in a failure metric.
69+
* Otherwise we assume success.
70+
*/
71+
public setUiReady(state: 'login' | 'reauth', errorMessage?: string) {
72+
// Only emit once to prevent telemetry spam, since showing/hiding chat triggers this each time.
73+
// TODO: Research how to not trigger this on every show/hide
6874
if (this.didCall[state]) {
6975
return
7076
}
7177

72-
telemetry.webview_load.emit({
73-
passive: true,
74-
webviewName: state,
75-
result: 'Succeeded',
76-
})
78+
if (errorMessage) {
79+
this.setLoadFailure(state, errorMessage)
80+
} else {
81+
this.setDidLoad(state)
82+
}
83+
7784
this.didCall[state] = true
7885
}
7986

packages/core/src/login/webview/vue/login.vue

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
></SelectableItem>
146146
<button
147147
class="continue-button"
148+
id="connection-selection-continue-button"
148149
:disabled="selectedLoginOption === 0"
149150
v-on:click="handleContinueClick()"
150151
>
@@ -368,8 +369,6 @@ export default defineComponent({
368369
// Pre-select the first available login option
369370
await this.preselectLoginOption()
370371
await this.handleUrlInput() // validate the default startUrl
371-
372-
await client.setUiReady('login')
373372
},
374373
methods: {
375374
toggleItemSelection(itemId: number) {
@@ -590,6 +589,18 @@ export default defineComponent({
590589
},
591590
},
592591
})
592+
593+
/**
594+
* The ID of the element we will use to determine that the UI has completed its initial load.
595+
*
596+
* This makes assumptions that we will be in a certain state of the UI (eg showing a form vs. a loading bar).
597+
* So if the UI flow changes, this may need to be updated.
598+
*/
599+
export function getReadyElementId() {
600+
// On every initial load, we ASSUME that the user will always be in the connection selection state,
601+
// which is why we specifically look for this button.
602+
return 'connection-selection-continue-button'
603+
}
593604
</script>
594605

595606
<style>

packages/core/src/login/webview/vue/reauthenticate.vue

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ and the final results are retrieved by the frontend. For this Component to updat
6161
</div>
6262

6363
<div>
64-
<button id="reauthenticate" v-on:click="reauthenticate">Re-authenticate</button>
64+
<button id="reauthenticate-button" v-on:click="reauthenticate">Re-authenticate</button>
6565
<div v-if="errorMessage" id="error-message" style="color: red">{{ errorMessage }}</div>
6666
</div>
6767

@@ -127,9 +127,6 @@ export default defineComponent({
127127
128128
this.doShow = true
129129
},
130-
async mounted() {
131-
await client.setUiReady('reauth')
132-
},
133130
methods: {
134131
async reauthenticate() {
135132
client.emitUiClick('auth_reauthenticate')
@@ -148,6 +145,16 @@ export default defineComponent({
148145
},
149146
},
150147
})
148+
149+
/**
150+
* The ID of the element we will use to determine that the UI has completed its initial load.
151+
*
152+
* This makes assumptions that we will be in a certain state of the UI (eg showing a form vs. a loading bar).
153+
* So if the UI flow changes, this may need to be updated.
154+
*/
155+
export function getReadyElementId() {
156+
return 'reauthenticate-button'
157+
}
151158
</script>
152159
<style>
153160
@import './base.css';
@@ -199,7 +206,7 @@ export default defineComponent({
199206
flex-direction: column;
200207
}
201208
202-
button#reauthenticate {
209+
button#reauthenticate-button {
203210
cursor: pointer;
204211
background-color: var(--vscode-button-background);
205212
color: white;
@@ -231,7 +238,7 @@ button#cancel {
231238
cursor: pointer;
232239
}
233240
234-
body.vscode-high-contrast:not(body.vscode-high-contrast-light) button#reauthenticate {
241+
body.vscode-high-contrast:not(body.vscode-high-contrast-light) button#reauthenticate-button {
235242
background-color: white;
236243
color: black;
237244
}

packages/core/src/login/webview/vue/root.vue

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ configure app to AMAZONQ if for Amazon Q login
1616
</template>
1717
<script lang="ts">
1818
import { PropType, defineComponent } from 'vue'
19-
import Login from './login.vue'
20-
import Reauthenticate from './reauthenticate.vue'
19+
import Login, { getReadyElementId as getLoginReadyElementId } from './login.vue'
20+
import Reauthenticate, { getReadyElementId as getReauthReadyElementId } from './reauthenticate.vue'
2121
import { AuthFlowState, FeatureId } from './types'
2222
import { WebviewClientFactory } from '../../../webviews/client'
2323
import { CommonAuthWebview } from './backend'
@@ -51,15 +51,83 @@ export default defineComponent({
5151
})
5252
5353
await this.refreshAuthState()
54+
55+
// We were recieving the 'load' event before refreshAuthState() resolved (I'm assuming behavior w/ Vue + browser loading not blocking),
56+
// so post refreshAuhState() if we detect we already loaded, then execute immediately since the event already happened.
57+
if (didLoad) {
58+
handleLoaded()
59+
} else {
60+
window.addEventListener('load', () => {
61+
handleLoaded()
62+
})
63+
}
5464
},
5565
methods: {
5666
async refreshAuthState() {
5767
await client.refreshAuthState()
5868
this.authFlowState = await client.getAuthState()
69+
70+
// Used for telemetry purposes
71+
if (this.authFlowState === 'LOGIN') {
72+
;(window as any).uiState = 'login'
73+
;(window as any).uiReadyElementId = getLoginReadyElementId()
74+
} else if (this.authFlowState && this.authFlowState !== undefined) {
75+
;(window as any).uiState = 'reauth'
76+
;(window as any).uiReadyElementId = getReauthReadyElementId()
77+
}
78+
5979
this.refreshKey += 1
6080
},
6181
},
6282
})
83+
84+
// ---- START ---- The following handles the process of indicating the UI has loaded successfully.
85+
// TODO: Move this in to a reusable class for other webviews, it feels a bit messy here
86+
let didSetReady = false
87+
88+
// Setup error handlers to report
89+
window.onerror = function (message) {
90+
if (didSetReady) {
91+
return
92+
}
93+
94+
setUiReady((window as any).uiState, message.toString())
95+
}
96+
document.addEventListener(
97+
'error',
98+
(e) => {
99+
if (didSetReady) {
100+
return
101+
}
102+
103+
setUiReady((window as any).uiState, e.message)
104+
},
105+
true
106+
)
107+
108+
let didLoad = false
109+
window.addEventListener('load', () => {
110+
didLoad = true
111+
})
112+
const handleLoaded = () => {
113+
// in case some unexpected behavior triggers this flow again, skip since we already emitted for this instance
114+
if (didSetReady) {
115+
return
116+
}
117+
118+
const foundElement = !!document.getElementById((window as any).uiReadyElementId)
119+
if (!foundElement) {
120+
setUiReady((window as any).uiState, `Could not find element: ${(window as any).uiReadyElementId}`)
121+
} else {
122+
// Successful load!
123+
setUiReady((window as any).uiState)
124+
}
125+
}
126+
const setUiReady = (state: 'login' | 'reauth', errorMessage?: string) => {
127+
client.setUiReady(state, errorMessage)
128+
didSetReady = true
129+
}
130+
// ---- END ----
63131
</script>
64132
<style>
65133
body {

packages/core/src/shared/telemetry/vscodeTelemetry.json

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -985,16 +985,6 @@
985985
],
986986
"passive": true
987987
},
988-
{
989-
"name": "webview_load",
990-
"description": "Represents a webview load event",
991-
"metadata": [
992-
{
993-
"type": "webviewName",
994-
"required": true
995-
}
996-
]
997-
},
998988
{
999989
"name": "webview_error",
1000990
"description": "Represents an error that occurs in a webview",

0 commit comments

Comments
 (0)