Skip to content

Commit 01d166b

Browse files
authored
telemetry(auth): snooze techdebt and add telemetry for evaluation (aws#5516)
We are still getting hits in telemetry for users seeing the split session notification. Adding more fine-grained telemetry to determined if this users are still migrating. This telemetry will also help us track other auth issues. --- <!--- REMINDER: Ensure that your PR meets the guidelines in CONTRIBUTING.md --> License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 56507e2 commit 01d166b

File tree

7 files changed

+198
-23
lines changed

7 files changed

+198
-23
lines changed

packages/core/src/auth/auth.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,7 @@ export class Auth implements AuthService, ConnectionManager {
579579
: undefined
580580
span.record({
581581
action: 'updateConnectionState',
582+
id,
582583
connectionState,
583584
source: asStringifiedStack(telemetry.getFunctionStack()),
584585
credentialStartUrl: oldProfile.type === 'sso' ? oldProfile.startUrl : undefined,

packages/core/src/auth/connection.ts

Lines changed: 111 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import { getLogger } from '../shared/logger/logger'
1313
import { showMessageWithUrl } from '../shared/utilities/messages'
1414
import { onceChanged } from '../shared/utilities/functionUtils'
1515
import { AuthAddConnection, AwsLoginWithBrowser } from '../shared/telemetry/telemetry.gen'
16+
import { withTelemetryContext } from '../shared/telemetry/util'
17+
import { AuthModifyConnection, telemetry } from '../shared/telemetry/telemetry'
18+
import { asStringifiedStack } from '../shared/telemetry/spans'
19+
import { getTelemetryReason, getTelemetryReasonDesc } from '../shared/errors'
1620

1721
/** Shows a warning message unless it is the same as the last one shown. */
1822
const warnOnce = onceChanged((s: string, url: string) => {
@@ -197,17 +201,74 @@ export interface ProfileMetadata {
197201

198202
export type StoredProfile<T extends Profile = Profile> = T & { readonly metadata: ProfileMetadata }
199203

204+
function getTelemetryForProfile(profile: StoredProfile<Profile> | undefined) {
205+
if (!profile) {
206+
return {}
207+
}
208+
209+
let metadata: Partial<AuthModifyConnection> = {
210+
connectionState: profile?.metadata.connectionState ?? 'undefined',
211+
}
212+
213+
if (profile.type === 'sso') {
214+
metadata = {
215+
...metadata,
216+
authScopes: profile.scopes?.join(','),
217+
credentialStartUrl: profile.startUrl,
218+
awsRegion: profile.ssoRegion,
219+
}
220+
}
221+
222+
return metadata
223+
}
224+
225+
const profileStoreClassName = 'ProfileStore'
200226
export class ProfileStore {
227+
// To de-dupe telemetry
228+
private _prevGetProfile: { id: string; connectionState: ProfileMetadata['connectionState'] } | undefined
229+
201230
public constructor(private readonly memento: vscode.Memento) {}
202231

203232
public getProfile(id: string): StoredProfile | undefined {
204233
return this.getData()[id]
205234
}
206235

236+
@withTelemetryContext({ name: 'getProfileOrThrow', class: profileStoreClassName })
207237
public getProfileOrThrow(id: string): StoredProfile {
208-
const profile = this.getProfile(id)
209-
if (profile === undefined) {
210-
throw new Error(`Profile does not exist: ${id}`)
238+
const metadata: AuthModifyConnection = {
239+
action: 'getProfile',
240+
id,
241+
source: asStringifiedStack(telemetry.getFunctionStack()),
242+
}
243+
244+
let profile: StoredProfile<Profile> | undefined
245+
try {
246+
profile = this.getProfile(id)
247+
if (profile === undefined) {
248+
throw new Error(`Profile does not exist: ${id}`)
249+
}
250+
} catch (err) {
251+
// Always emit failures
252+
telemetry.auth_modifyConnection.emit({
253+
...metadata,
254+
result: 'Failed',
255+
reason: getTelemetryReason(err),
256+
reasonDesc: getTelemetryReasonDesc(err),
257+
})
258+
throw err
259+
}
260+
261+
// De-dupe metric on last id and connection state
262+
if (
263+
this._prevGetProfile?.id !== id ||
264+
this._prevGetProfile?.connectionState !== profile.metadata.connectionState
265+
) {
266+
telemetry.auth_modifyConnection.emit({
267+
...metadata,
268+
...getTelemetryForProfile(profile),
269+
result: 'Succeeded',
270+
})
271+
this._prevGetProfile = { id, connectionState: profile.metadata.connectionState }
211272
}
212273

213274
return profile
@@ -219,17 +280,40 @@ export class ProfileStore {
219280

220281
public async addProfile(id: string, profile: SsoProfile): Promise<StoredProfile<SsoProfile>>
221282
public async addProfile(id: string, profile: IamProfile): Promise<StoredProfile<IamProfile>>
283+
@withTelemetryContext({ name: 'addProfile', class: profileStoreClassName })
222284
public async addProfile(id: string, profile: Profile): Promise<StoredProfile> {
223-
return this.putProfile(id, this.initMetadata(profile))
285+
return telemetry.auth_modifyConnection.run(async (span) => {
286+
span.record({
287+
action: 'addProfile',
288+
id,
289+
source: asStringifiedStack(telemetry.getFunctionStack()),
290+
})
291+
292+
const newProfile = this.initMetadata(profile)
293+
span.record(getTelemetryForProfile(newProfile))
294+
295+
return await this.putProfile(id, newProfile)
296+
})
224297
}
225298

299+
@withTelemetryContext({ name: 'updateProfile', class: profileStoreClassName })
226300
public async updateProfile(id: string, profile: Profile): Promise<StoredProfile> {
227-
const oldProfile = this.getProfileOrThrow(id)
228-
if (oldProfile.type !== profile.type) {
229-
throw new Error(`Cannot change profile type from "${oldProfile.type}" to "${profile.type}"`)
230-
}
301+
return telemetry.auth_modifyConnection.run(async (span) => {
302+
span.record({
303+
action: 'updateProfile',
304+
id,
305+
source: asStringifiedStack(telemetry.getFunctionStack()),
306+
})
307+
308+
const oldProfile = this.getProfileOrThrow(id)
309+
if (oldProfile.type !== profile.type) {
310+
throw new Error(`Cannot change profile type from "${oldProfile.type}" to "${profile.type}"`)
311+
}
231312

232-
return this.putProfile(id, { ...oldProfile, ...profile })
313+
const newProfile = await this.putProfile(id, { ...oldProfile, ...profile })
314+
span.record(getTelemetryForProfile(newProfile))
315+
return newProfile
316+
})
233317
}
234318

235319
public async updateMetadata(id: string, metadata: ProfileMetadata): Promise<StoredProfile> {
@@ -238,11 +322,21 @@ export class ProfileStore {
238322
return this.putProfile(id, { ...profile, metadata: { ...profile.metadata, ...metadata } })
239323
}
240324

325+
@withTelemetryContext({ name: 'deleteProfile', class: profileStoreClassName })
241326
public async deleteProfile(id: string): Promise<void> {
242-
const data = this.getData()
243-
delete (data as Mutable<typeof data>)[id]
244-
245-
await this.updateData(data)
327+
return telemetry.auth_modifyConnection.run(async (span) => {
328+
span.record({
329+
action: 'deleteProfile',
330+
id,
331+
source: asStringifiedStack(telemetry.getFunctionStack()),
332+
})
333+
334+
const data = this.getData()
335+
span.record(getTelemetryForProfile(data[id]))
336+
delete (data as Mutable<typeof data>)[id]
337+
338+
await this.updateData(data)
339+
})
246340
}
247341

248342
public getCurrentProfileId(): string | undefined {
@@ -395,11 +489,13 @@ export interface AwsConnection {
395489
}
396490

397491
type Writeable<T> = { -readonly [U in keyof T]: T[U] }
398-
export type TelemetryMetadata = Partial<Writeable<AuthAddConnection | AwsLoginWithBrowser>>
492+
export type TelemetryMetadata = Partial<Writeable<AuthAddConnection & AwsLoginWithBrowser & AuthModifyConnection>>
399493

400494
export async function getTelemetryMetadataForConn(conn?: Connection): Promise<TelemetryMetadata> {
401495
if (conn === undefined) {
402-
return {}
496+
return {
497+
id: 'undefined',
498+
}
403499
}
404500

405501
if (isSsoConnection(conn)) {

packages/core/src/auth/secondaryAuth.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,17 +256,25 @@ export class SecondaryAuth<T extends Connection = Connection> {
256256
@withTelemetryContext({ name: 'restoreConnection', class: secondaryAuthClassName })
257257
private async _restoreConnection(): Promise<T | undefined> {
258258
try {
259-
return await telemetry.auth_modifyConnection.run(async () => {
260-
telemetry.record({
259+
return await telemetry.auth_modifyConnection.run(async (span) => {
260+
span.record({
261261
source: asStringifiedStack(telemetry.getFunctionStack()),
262262
action: 'restore',
263+
id: 'undefined',
263264
connectionState: 'undefined',
264265
})
265266
await this.auth.tryAutoConnect()
266267
this.#savedConnection = await this._loadSavedConnection()
267268
this.#onDidChangeActiveConnection.fire(this.activeConnection)
268269

269-
telemetry.record(await getTelemetryMetadataForConn(this.#savedConnection))
270+
const conn = this.#savedConnection
271+
if (conn) {
272+
span.record({
273+
connectionState: this.auth.getConnectionState(conn),
274+
...(await getTelemetryMetadataForConn(conn)),
275+
})
276+
}
277+
270278
return this.#savedConnection
271279
})
272280
} catch (err) {

packages/core/src/codecatalyst/activation.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ import { getLogger } from '../shared/logger/logger'
2525
import { DevEnvActivityStarter } from './devEnv'
2626
import { learnMoreCommand, onboardCommand, reauth } from './explorer'
2727
import { isInDevEnv } from '../shared/vscode/env'
28-
import { hasScopes, scopesCodeWhispererCore } from '../auth/connection'
28+
import { hasScopes, scopesCodeWhispererCore, getTelemetryMetadataForConn } from '../auth/connection'
2929
import { SessionSeparationPrompt } from '../auth/auth'
30+
import { telemetry } from '../shared/telemetry/telemetry'
31+
import { asStringifiedStack } from '../shared/telemetry/spans'
3032

3133
const localize = nls.loadMessageBundle()
3234

@@ -50,8 +52,23 @@ export async function activate(ctx: ExtContext): Promise<void> {
5052
// Note: credentials on disk in the dev env cannot have Q scopes, so it will never be forgotten.
5153
// TODO: Remove after some time?
5254
if (authProvider.isConnected() && hasScopes(authProvider.activeConnection!, scopesCodeWhispererCore)) {
53-
await authProvider.secondaryAuth.forgetConnection()
54-
await SessionSeparationPrompt.instance.showForCommand('aws.codecatalyst.manageConnections')
55+
await telemetry.function_call.run(
56+
async () => {
57+
await telemetry.auth_modifyConnection.run(async () => {
58+
const conn = authProvider.activeConnection
59+
telemetry.record({
60+
action: 'forget',
61+
source: asStringifiedStack(telemetry.getFunctionStack()),
62+
connectionState: conn ? authProvider.auth.getConnectionState(conn) : undefined,
63+
...(await getTelemetryMetadataForConn(conn)),
64+
})
65+
66+
await authProvider.secondaryAuth.forgetConnection()
67+
await SessionSeparationPrompt.instance.showForCommand('aws.codecatalyst.manageConnections')
68+
})
69+
},
70+
{ emit: false, functionId: { name: 'activate', class: 'CodeCatalyst' } }
71+
)
5572
}
5673

5774
ctx.extensionContext.subscriptions.push(

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1094,7 +1094,7 @@
10941094
},
10951095
{
10961096
"type": "connectionState",
1097-
"required": true
1097+
"required": false
10981098
},
10991099
{
11001100
"type": "credentialStartUrl",
@@ -1115,6 +1115,10 @@
11151115
{
11161116
"type": "ssoRegistrationExpiresAt",
11171117
"required": false
1118+
},
1119+
{
1120+
"type": "id",
1121+
"required": false
11181122
}
11191123
],
11201124
"passive": true

packages/core/src/test/credentials/auth.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,27 @@ describe('Auth', function () {
5555
await auth.deleteConnection({ id: conn.id })
5656
assert.strictEqual((await auth.listConnections()).length, 0)
5757
assertTelemetry('auth_modifyConnection', [
58+
{
59+
action: 'addProfile',
60+
connectionState: 'unauthenticated',
61+
source: 'Auth#createConnection:ProfileStore#addProfile',
62+
},
63+
{
64+
action: 'getProfile',
65+
connectionState: 'unauthenticated',
66+
source: 'Auth#createConnection,updateConnectionState:ProfileStore#getProfileOrThrow',
67+
},
5868
{
5969
action: 'updateConnectionState',
6070
connectionState: 'valid',
6171
source: 'Auth#createConnection,updateConnectionState',
6272
sessionDuration: undefined,
6373
},
74+
{
75+
action: 'getProfile',
76+
connectionState: 'valid',
77+
source: 'Auth#deleteConnection,invalidateConnection:ProfileStore#getProfileOrThrow',
78+
},
6479
{
6580
action: 'updateConnectionState',
6681
connectionState: 'invalid',
@@ -78,12 +93,27 @@ describe('Auth', function () {
7893
assert.strictEqual((await auth.listConnections()).length, 0)
7994
assert.strictEqual(auth.activeConnection, undefined)
8095
assertTelemetry('auth_modifyConnection', [
96+
{
97+
action: 'addProfile',
98+
connectionState: 'unauthenticated',
99+
source: 'Auth#createConnection:ProfileStore#addProfile',
100+
},
101+
{
102+
action: 'getProfile',
103+
connectionState: 'unauthenticated',
104+
source: 'Auth#createConnection,updateConnectionState:ProfileStore#getProfileOrThrow',
105+
},
81106
{
82107
action: 'updateConnectionState',
83108
connectionState: 'valid',
84109
source: 'Auth#createConnection,updateConnectionState',
85110
sessionDuration: undefined,
86111
},
112+
{
113+
action: 'getProfile',
114+
connectionState: 'valid',
115+
source: 'Auth#useConnection,refreshConnectionState,validateConnection,updateConnectionState:ProfileStore#getProfileOrThrow',
116+
},
87117
{
88118
action: 'updateConnectionState',
89119
connectionState: 'invalid',
@@ -113,12 +143,27 @@ describe('Auth', function () {
113143
assert.strictEqual(auth.activeConnection, undefined)
114144
assert.strictEqual(auth.activeConnectionEvents.last, undefined)
115145
assertTelemetry('auth_modifyConnection', [
146+
{
147+
action: 'addProfile',
148+
connectionState: 'unauthenticated',
149+
source: 'Auth#createConnection:ProfileStore#addProfile',
150+
},
151+
{
152+
action: 'getProfile',
153+
connectionState: 'unauthenticated',
154+
source: 'Auth#createConnection,updateConnectionState:ProfileStore#getProfileOrThrow',
155+
},
116156
{
117157
action: 'updateConnectionState',
118158
connectionState: 'valid',
119159
source: 'Auth#createConnection,updateConnectionState',
120160
sessionDuration: undefined,
121161
},
162+
{
163+
action: 'getProfile',
164+
connectionState: 'valid',
165+
source: 'Auth#useConnection,refreshConnectionState,validateConnection,updateConnectionState:ProfileStore#getProfileOrThrow',
166+
},
122167
{
123168
action: 'updateConnectionState',
124169
connectionState: 'invalid',

packages/core/src/test/techdebt.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,11 @@ describe('tech debt', function () {
4949
it('remove separate sessions login edge cases', async function () {
5050
// src/auth/auth.ts:SessionSeparationPrompt
5151
// forgetConnection() function and calls
52-
fixByDate('2024-08-30', 'Remove the edge case code from the commit that this test is a part of.')
52+
53+
// Monitor telemtry to determine removal or snooze
54+
// toolkit_showNotification.id = sessionSeparation
55+
// auth_modifyConnection.action = deleteProfile OR auth_modifyConnection.source contains CodeCatalyst
56+
fixByDate('2024-9-30', 'Remove the edge case code from the commit that this test is a part of.')
5357
})
5458

5559
it('remove deprecated code transform metrics', async function () {

0 commit comments

Comments
 (0)