Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/amazonq/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is
// Give time for the extension to finish initializing.
globals.clock.setTimeout(async () => {
CommonAuthWebview.authSource = ExtStartUpSources.firstStartUp
void focusAmazonQPanel.execute(placeholder, 'firstStartUp')
void focusAmazonQPanel.execute(placeholder, ExtStartUpSources.firstStartUp)
}, 1000)
}
}
Expand Down
44 changes: 35 additions & 9 deletions packages/core/src/auth/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ import { EcsCredentialsProvider } from './providers/ecsCredentialsProvider'
import { EnvVarsCredentialsProvider } from './providers/envVarsCredentialsProvider'
import { showMessageWithUrl } from '../shared/utilities/messages'
import { credentialHelpUrl } from '../shared/constants'
import { ExtStartUpSources, ExtStartUpSource } from '../shared/telemetry/util'
import { ExtStartUpSources, ExtStartUpSource, hadClientIdOnStartup } from '../shared/telemetry/util'

// iam-only excludes Builder ID and IAM Identity Center from the list of valid connections
// TODO: Understand if "iam" should include these from the list at all
Expand Down Expand Up @@ -688,17 +688,43 @@ export class ExtensionUse {
return this.isFirstUseCurrentSession
}

this.isFirstUseCurrentSession = globals.globalState.get('isExtensionFirstUse')
if (this.isFirstUseCurrentSession === undefined) {
// This is for sure not their first use
const isFirstUse = globals.globalState.tryGet('isExtensionFirstUse', Boolean)
if (isFirstUse === false) {
this.isFirstUseCurrentSession = isFirstUse
return this.isFirstUseCurrentSession
}

/**
* SANITY CHECK: If the clientId already existed on startup, then isFirstUse MUST be false. So
* there is a bug in the state.
*/
if (hadClientIdOnStartup(globals.globalState)) {
telemetry.function_call.emit({
result: 'Failed',
functionName: 'isFirstUse',
reason: 'ClientIdAlreadyExisted',
})
}

if (isAmazonQ()) {
this.isFirstUseCurrentSession = true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qq: why is this always true for amazonQ?

Copy link
Contributor Author

@nkomonen-amazon nkomonen-amazon Feb 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was previous logic where if we detected an auth connection in Toolkit that we would not consider it a first time user. But this doesn't make sense for Q.

Also if they get this far in the function it is guaranteed they are a first time user, since they would have otherwise returned earlier

if (hasExistingConnections()) {
telemetry.function_call.emit({
result: 'Failed',
functionName: 'isFirstUse',
reason: 'UnexpectedConnections',
})
}
} else {
// The variable in the store is not defined yet, fallback to checking if they have existing connections.
this.isFirstUseCurrentSession = !hasExistingConnections()

getLogger().debug(
`isFirstUse: State not found, marking user as '${
this.isFirstUseCurrentSession ? '' : 'NOT '
}first use' since they 'did ${this.isFirstUseCurrentSession ? 'NOT ' : ''}have existing connections'.`
)
}
getLogger().debug(
`isFirstUse: State not found, marking user as '${
this.isFirstUseCurrentSession ? '' : 'NOT '
}first use' since they 'did ${this.isFirstUseCurrentSession ? 'NOT ' : ''}have existing connections'.`
)

// Update state, so next time it is not first use
this.updateMemento('isExtensionFirstUse', false)
Expand Down
13 changes: 11 additions & 2 deletions packages/core/src/login/webview/commonAuthViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { AuthSources } from './util'
import { AuthFlowStates } from './vue/types'
import { getTelemetryMetadataForConn } from '../../auth/connection'
import { AuthUtil } from '../../codewhisperer/util/authUtil'
import { ExtensionUse } from '../../auth/utils'

export class CommonAuthViewProvider implements WebviewViewProvider {
public readonly viewType: string
Expand Down Expand Up @@ -83,14 +84,22 @@ export class CommonAuthViewProvider implements WebviewViewProvider {
) {
// Our callback won't fire on the first view.
if (webviewView.visible) {
telemetry.auth_signInPageOpened.emit({ result: 'Succeeded', passive: true })
telemetry.auth_signInPageOpened.emit({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we modify this metric and make it more general in the future?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, we need to better structure out telemetry since it is messy with what "opened" means. We can also collapse all webviews in to a single metric probably

result: 'Succeeded',
passive: true,
source: ExtensionUse.instance.sourceForTelemetry(),
})
}

// This will fire whenever the user opens or closes the login page from 'somewhere else'
// i.e. NOT when switching from/to the chat window, which uses the same view area.
webviewView.onDidChangeVisibility(async () => {
if (webviewView.visible) {
telemetry.auth_signInPageOpened.emit({ result: 'Succeeded', passive: true })
telemetry.auth_signInPageOpened.emit({
result: 'Succeeded',
passive: true,
source: ExtensionUse.instance.sourceForTelemetry(),
})
} else {
telemetry.auth_signInPageClosed.emit({ result: 'Succeeded', passive: true })

Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/login/webview/vue/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,22 @@ export abstract class CommonAuthWebview extends VueWebview {
return globals.regionProvider.getRegions().reverse()
}

private didCall: { login: boolean; reauth: boolean } = { login: false, reauth: false }
public setUiReady(state: 'login' | 'reauth') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this something we could just use the once() util for? That way you don't have to keep track of the extra state?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to emit once for login and reauth separately which didn't allow me to use once(). But I think in a future PR we could do some memoize + once to address this

// Prevent telemetry spam, since showing/hiding chat triggers this each time.
// So only emit once.
if (this.didCall[state]) {
return
}

telemetry.webview_load.emit({
passive: true,
webviewName: state,
result: 'Succeeded',
})
this.didCall[state] = true
}

/**
* This wraps the execution of the given setupFunc() and handles common errors from the SSO setup process.
*
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/login/webview/vue/login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,8 @@ export default defineComponent({
// Pre-select the first available login option
await this.preselectLoginOption()
await this.handleUrlInput() // validate the default startUrl

await client.setUiReady('login')
},
methods: {
toggleItemSelection(itemId: number) {
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/login/webview/vue/reauthenticate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ export default defineComponent({

this.doShow = true
},
async mounted() {
await client.setUiReady('reauth')
},
methods: {
async reauthenticate() {
client.emitUiClick('auth_reauthenticate')
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/shared/telemetry/telemetryService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { telemetry, MetricBase } from './telemetry'
import fs from '../fs/fs'
import fsNode from 'fs/promises'
import * as collectionUtil from '../utilities/collectionUtils'
import { ExtensionUse } from '../../auth/utils'

export type TelemetryService = ClassToInterfaceType<DefaultTelemetryService>

Expand Down Expand Up @@ -98,7 +99,9 @@ export class DefaultTelemetryService {
// TODO: `readEventsFromCache` should be async
this._eventQueue.push(...(await DefaultTelemetryService.readEventsFromCache(this.persistFilePath)))
this._endOfCache = this._eventQueue[this._eventQueue.length - 1]
telemetry.session_start.emit()
telemetry.session_start.emit({
source: ExtensionUse.instance.sourceForTelemetry(),
})
this.startFlushInterval()
}

Expand Down
19 changes: 19 additions & 0 deletions packages/core/src/shared/telemetry/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { asStringifiedStack, FunctionEntry } from './spans'
import { telemetry } from './telemetry'
import { v5 as uuidV5 } from 'uuid'
import { ToolkitError } from '../errors'
import { GlobalState } from '../globalState'

const legacySettingsTelemetryValueDisable = 'Disable'
const legacySettingsTelemetryValueEnable = 'Enable'
Expand Down Expand Up @@ -177,6 +178,8 @@ export const getClientId = memoize(
const localClientId = globalState.tryGet('telemetryClientId', String) // local to extension, despite accessing "global" state
let clientId: string

_hadClientIdOnStartup = !!globalClientId || !!localClientId

if (isWeb()) {
const machineId = vscode.env.machineId
clientId = localClientId ?? machineId
Expand Down Expand Up @@ -210,6 +213,22 @@ export const getClientId = memoize(
}
)

let _hadClientIdOnStartup = false
/**
* Returns true if the ClientID existed before this session started
*/
export const hadClientIdOnStartup = (
globalState: GlobalState,
update = (globalState: GlobalState) => {
getClientId(globalState)
}
) => {
// triggers the flow that will update the state, if not done already
update(globalState)

return _hadClientIdOnStartup
}

export const platformPair = () => `${env.appName.replace(/\s/g, '-')}/${version}`

/**
Expand Down
22 changes: 22 additions & 0 deletions packages/core/src/shared/telemetry/vscodeTelemetry.json
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,17 @@
],
"passive": true
},
{
"name": "auth_signInPageOpened",
"description": "When the Amazon Q sign in page is opened and focused.",
"metadata": [
{
"type": "source",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm being rushed to get the change out for this release, but I will port this over after

"required": true
}
],
"passive": true
},
{
"name": "function_call",
"description": "Represents a function call. In most cases this should wrap code with a run(), then you can add context.",
Expand Down Expand Up @@ -1207,6 +1218,17 @@
}
]
},
{
"name": "session_start",
Copy link
Contributor

@justinmk3 justinmk3 Feb 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed? There is already a metric for starting the plugin. Adding similar, semantically-redundant metrics will make it harder to reason about the lifecycle and telemetry.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the metric?

Copy link
Contributor

@justinmk3 justinmk3 Feb 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, this metric already exists. So different question is can the existing metric be updated instead , in the common repo

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I plan to do this. It was just due to time constraints. I'll follow up after this PR

"description": "Called when starting the plugin",
"metadata": [
{
"type": "source",
"required": false
}
],
"passive": true
},
{
"name": "session_end",
"description": "Called when stopping the IDE on a best effort basis",
Expand Down
10 changes: 5 additions & 5 deletions packages/core/src/test/credentials/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,23 @@ import globals from '../../shared/extensionGlobals'

describe('ExtensionUse.isFirstUse()', function () {
let instance: ExtensionUse
const notHasExistingConnections = () => false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be hasNoExistingConnections ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, I'll follow up with the name change after. Don't want to have to run CI again unless theres something critical.


beforeEach(async function () {
instance = new ExtensionUse()
await globals.globalState.update(ExtensionUse.instance.isExtensionFirstUseKey, true)
await makeStateValueNotExist()
})

it('is true only on first startup', function () {
assert.strictEqual(instance.isFirstUse(), true, 'Failed on first call.')
assert.strictEqual(instance.isFirstUse(), true, 'Failed on second call.')
assert.strictEqual(instance.isFirstUse(notHasExistingConnections), true, 'Failed on first call.')
assert.strictEqual(instance.isFirstUse(notHasExistingConnections), true, 'Failed on second call.')

const nextStartup = nextExtensionStartup()
assert.strictEqual(nextStartup.isFirstUse(), false, 'Failed on new startup.')
assert.strictEqual(nextStartup.isFirstUse(notHasExistingConnections), false, 'Failed on new startup.')
})

it('true when: (state value not exists + NOT has existing connections)', async function () {
await makeStateValueNotExist()
const notHasExistingConnections = () => false
assert.strictEqual(
instance.isFirstUse(notHasExistingConnections),
true,
Expand Down
37 changes: 33 additions & 4 deletions packages/core/src/test/shared/telemetry/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
convertLegacy,
getClientId,
getUserAgent,
hadClientIdOnStartup,
platformPair,
SessionId,
telemetryClientIdEnvKey,
Expand Down Expand Up @@ -128,20 +129,29 @@ describe('getSessionId', function () {

describe('getClientId', function () {
before(function () {
delete process.env[telemetryClientIdEnvKey]
setClientIdEnvVar(undefined)
})

afterEach(function () {
delete process.env[telemetryClientIdEnvKey]
setClientIdEnvVar(undefined)
})

function testGetClientId(globalState: GlobalState) {
return getClientId(globalState, true, false, randomUUID())
}

function setClientIdEnvVar(val: string | undefined) {
if (val === undefined) {
delete process.env[telemetryClientIdEnvKey]
return
}

process.env[telemetryClientIdEnvKey] = val
}

it('generates a unique id if no other id is available', function () {
const c1 = testGetClientId(new GlobalState(new FakeMemento()))
delete process.env[telemetryClientIdEnvKey]
setClientIdEnvVar(undefined)
const c2 = testGetClientId(new GlobalState(new FakeMemento()))
assert.notStrictEqual(c1, c2)
})
Expand All @@ -160,7 +170,7 @@ describe('getClientId', function () {

const e = new GlobalState(new FakeMemento())
await e.update('telemetryClientId', randomUUID())
process.env[telemetryClientIdEnvKey] = expectedClientId
setClientIdEnvVar(expectedClientId)

assert.strictEqual(testGetClientId(new GlobalState(new FakeMemento())), expectedClientId)
})
Expand Down Expand Up @@ -218,6 +228,25 @@ describe('getClientId', function () {
const clientId = getClientId(new GlobalState(new FakeMemento()), false, false)
assert.strictEqual(clientId, '11111111-1111-1111-1111-111111111111')
})

describe('hadClientIdOnStartup', async function () {
it('returns false when no existing clientId', async function () {
const globalState = new GlobalState(new FakeMemento())
assert.strictEqual(hadClientIdOnStartup(globalState, testGetClientId), false)
})

it('returns true when existing env var clientId', async function () {
const globalState = new GlobalState(new FakeMemento())
setClientIdEnvVar('aaa-111')
assert.strictEqual(hadClientIdOnStartup(globalState, testGetClientId), true)
})

it('returns true when existing state clientId', async function () {
const globalState = new GlobalState(new FakeMemento())
await globalState.update('telemetryClientId', 'bbb-222')
assert.strictEqual(hadClientIdOnStartup(globalState, testGetClientId), true)
})
})
})

describe('getUserAgent', function () {
Expand Down
Loading