Skip to content

Commit d39c3ec

Browse files
liuzulinkzr-at-amazonsjc25-test
authored
feat(sagemakerunifiedstudio): Add telemetry for login, signout, access project, start/stop space and data explorer (aws#2207)
## Problem Telemetry is missing from the feature. ## Solution - update packages/core/src/shared/telemetry/vscodeTelemetry.json with new types, metrics, and metadata - run npm run generateTelemetry in core package. this will create required obj that are accessible via telemetry.{metric name} - then emit metric in the code --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: Keyvan Zare Rami <[email protected]> Co-authored-by: Zulin Liu <[email protected]>
1 parent e8a8c59 commit d39c3ec

File tree

13 files changed

+1000
-563
lines changed

13 files changed

+1000
-563
lines changed

packages/core/src/awsService/sagemaker/commands.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,13 @@ export async function stopSpace(
158158
if (error.name === 'AccessDeniedException') {
159159
throw new ToolkitError('You do not have permission to stop spaces. Please contact your administrator', {
160160
cause: error,
161+
code: error.name,
161162
})
162163
} else {
163-
throw err
164+
throw new ToolkitError(`Failed to stop space: ${spaceName}`, {
165+
cause: error,
166+
code: error.name,
167+
})
164168
}
165169
}
166170
await tryRefreshNode(node)
@@ -185,14 +189,19 @@ export async function openRemoteConnect(
185189
await tryRefreshNode(node)
186190
const appType = node.spaceApp.SpaceSettingsSummary?.AppType
187191
if (!appType) {
188-
throw new ToolkitError('AppType is undefined for the selected space. Cannot start remote connection.')
192+
throw new ToolkitError('AppType is undefined for the selected space. Cannot start remote connection.', {
193+
code: 'undefinedAppType',
194+
})
189195
}
190196
await client.waitForAppInService(node.spaceApp.DomainId!, node.spaceApp.SpaceName!, appType)
191197
await tryRemoteConnection(node, ctx)
192198
} catch (err: any) {
193199
// Ignore InstanceTypeError since it means the user decided not to use an instanceType with more memory
194200
if (err.code !== InstanceTypeError) {
195-
throw err
201+
throw new ToolkitError('Remote connection failed.', {
202+
cause: err as Error,
203+
code: err.code,
204+
})
196205
}
197206
}
198207
} else if (node.getStatus() === 'Running') {

packages/core/src/sagemakerunifiedstudio/auth/providers/smusAuthenticationProvider.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { SsoConnection } from '../../../auth/connection'
1212
import { showReauthenticateMessage } from '../../../shared/utilities/messages'
1313
import * as localizedText from '../../../shared/localizedText'
1414
import { ToolkitPromptSettings } from '../../../shared/settings'
15-
import { setContext } from '../../../shared/vscode/setContext'
15+
import { setContext, getContext } from '../../../shared/vscode/setContext'
1616
import { SmusUtils, SmusErrorCodes } from '../../shared/smusUtils'
1717
import { createSmusProfile, isValidSmusConnection, SmusConnection } from '../model'
1818
import { getLogger } from '../../../shared/logger/logger'
@@ -22,6 +22,7 @@ import { ConnectionCredentialsProvider } from './connectionCredentialsProvider'
2222
import { ConnectionClientStore } from '../../shared/client/connectionClientStore'
2323
import { getResourceMetadata } from '../../shared/utils/resourceMetadataUtils'
2424
import { fromIni } from '@aws-sdk/credential-providers'
25+
import { randomUUID } from '../../../shared/crypto'
2526

2627
/**
2728
* Sets the context variable for SageMaker Unified Studio connection state
@@ -86,16 +87,17 @@ export class SmusAuthenticationProvider {
8687
* Gets the active connection
8788
*/
8889
public get activeConnection() {
89-
if (SmusUtils.isInSmusSpaceEnvironment()) {
90+
if (getContext('aws.smus.inSmusSpaceEnvironment')) {
9091
const resourceMetadata = getResourceMetadata()!
9192
if (resourceMetadata.AdditionalMetadata!.DataZoneDomainRegion) {
9293
return {
9394
domainId: resourceMetadata.AdditionalMetadata!.DataZoneDomainId!,
9495
ssoRegion: resourceMetadata.AdditionalMetadata!.DataZoneDomainRegion!,
9596
// The following fields won't be needed in SMUS space environment
96-
// Set them to be empty string for type checks only
97-
domainUrl: '',
98-
id: '',
97+
// Craft the domain url with known information
98+
// Use randome id as placeholder
99+
domainUrl: `https://${resourceMetadata.AdditionalMetadata!.DataZoneDomainId!}.sagemaker.${resourceMetadata.AdditionalMetadata!.DataZoneDomainRegion!}.on.aws/`,
100+
id: randomUUID(),
99101
}
100102
} else {
101103
throw new ToolkitError('Domain region not found in metadata file.')
@@ -115,7 +117,9 @@ export class SmusAuthenticationProvider {
115117
* Checks if the connection is valid
116118
*/
117119
public isConnectionValid(): boolean {
118-
if (SmusUtils.isInSmusSpaceEnvironment()) {
120+
// When in SMUS space, the extension is already running in projet context and sign in is not needed
121+
// Set isConnectionValid to always true
122+
if (getContext('aws.smus.inSmusSpaceEnvironment')) {
119123
return true
120124
}
121125
return this.activeConnection !== undefined && !this.secondaryAuth.isConnectionExpired
@@ -125,7 +129,9 @@ export class SmusAuthenticationProvider {
125129
* Checks if connected to SMUS
126130
*/
127131
public isConnected(): boolean {
128-
if (SmusUtils.isInSmusSpaceEnvironment()) {
132+
// When in SMUS space, the extension is already running in projet context and sign in is not needed
133+
// Set isConnected to always true
134+
if (getContext('aws.smus.inSmusSpaceEnvironment')) {
129135
return true
130136
}
131137
return this.activeConnection !== undefined
@@ -365,7 +371,7 @@ export class SmusAuthenticationProvider {
365371
* @returns Domain ID
366372
*/
367373
public getDomainId(): string {
368-
if (SmusUtils.isInSmusSpaceEnvironment()) {
374+
if (getContext('aws.smus.inSmusSpaceEnvironment')) {
369375
return getResourceMetadata()!.AdditionalMetadata!.DataZoneDomainId!
370376
}
371377

@@ -387,7 +393,7 @@ export class SmusAuthenticationProvider {
387393
}
388394

389395
public getDomainRegion(): string {
390-
if (SmusUtils.isInSmusSpaceEnvironment()) {
396+
if (getContext('aws.smus.inSmusSpaceEnvironment')) {
391397
const resourceMetadata = getResourceMetadata()!
392398
if (resourceMetadata.AdditionalMetadata!.DataZoneDomainRegion) {
393399
return resourceMetadata.AdditionalMetadata!.DataZoneDomainRegion
@@ -409,7 +415,9 @@ export class SmusAuthenticationProvider {
409415
public async getDerCredentialsProvider(): Promise<any> {
410416
const logger = getLogger()
411417

412-
if (SmusUtils.isInSmusSpaceEnvironment()) {
418+
if (getContext('aws.smus.inSmusSpaceEnvironment')) {
419+
// When in SMUS space, DomainExecutionRoleCreds can be found in config file
420+
// Read the credentials from credential profile DomainExecutionRoleCreds
413421
const credentials = fromIni({ profile: 'DomainExecutionRoleCreds' })
414422
return {
415423
getCredentials: async () => await credentials(),

packages/core/src/sagemakerunifiedstudio/explorer/activation.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { SageMakerUnifiedStudioProjectNode } from './nodes/sageMakerUnifiedStudi
1919
import { getLogger } from '../../shared/logger/logger'
2020
import { setSmusConnectedContext, SmusAuthenticationProvider } from '../auth/providers/smusAuthenticationProvider'
2121
import { setupUserActivityMonitoring } from '../../awsService/sagemaker/sagemakerSpace'
22+
import { telemetry } from '../../shared/telemetry/telemetry'
23+
import { SageMakerUnifiedStudioSpacesParentNode } from './nodes/sageMakerUnifiedStudioSpacesParentNode'
2224

2325
export async function activate(extensionContext: vscode.ExtensionContext): Promise<void> {
2426
// Initialize the SMUS authentication provider
@@ -71,7 +73,19 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi
7173
if (!validateNode(node)) {
7274
return
7375
}
74-
await stopSpace(node.resource, extensionContext, node.resource.sageMakerClient)
76+
await telemetry.smus_stopSpace.run(async (span) => {
77+
span.record({
78+
smusSpaceKey: node.resource.DomainSpaceKey,
79+
smusDomainRegion: node.resource.regionCode,
80+
smusDomainId: (
81+
node.resource.getParent() as SageMakerUnifiedStudioSpacesParentNode
82+
)?.getAuthProvider()?.activeConnection?.domainId,
83+
smusProjectId: (
84+
node.resource.getParent() as SageMakerUnifiedStudioSpacesParentNode
85+
)?.getProjectId(),
86+
})
87+
await stopSpace(node.resource, extensionContext, node.resource.sageMakerClient)
88+
})
7589
}),
7690

7791
vscode.commands.registerCommand(
@@ -80,7 +94,19 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi
8094
if (!validateNode(node)) {
8195
return
8296
}
83-
await openRemoteConnect(node.resource, extensionContext, node.resource.sageMakerClient)
97+
await telemetry.smus_startSpace.run(async (span) => {
98+
span.record({
99+
smusSpaceKey: node.resource.DomainSpaceKey,
100+
smusDomainRegion: node.resource.regionCode,
101+
smusDomainId: (
102+
node.resource.getParent() as SageMakerUnifiedStudioSpacesParentNode
103+
)?.getAuthProvider()?.activeConnection?.domainId,
104+
smusProjectId: (
105+
node.resource.getParent() as SageMakerUnifiedStudioSpacesParentNode
106+
)?.getProjectId(),
107+
})
108+
await openRemoteConnect(node.resource, extensionContext, node.resource.sageMakerClient)
109+
})
84110
}
85111
),
86112

@@ -112,7 +138,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi
112138
/**
113139
* Checks if a node is undefined and shows a warning message if so.
114140
*/
115-
function validateNode(node: unknown): boolean {
141+
function validateNode(node: SagemakerUnifiedStudioSpaceNode): boolean {
116142
if (!node) {
117143
void vscode.window.showWarningMessage('Space information is being refreshed. Please try again shortly.')
118144
return false

packages/core/src/sagemakerunifiedstudio/explorer/nodes/lakehouseStrategy.ts

Lines changed: 71 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import {
3333
import { createPlaceholderItem } from '../../../shared/treeview/utils'
3434
import { Column, Database, Table } from '@aws-sdk/client-glue'
3535
import { ConnectionCredentialsProvider } from '../../auth/providers/connectionCredentialsProvider'
36+
import { telemetry } from '../../../shared/telemetry/telemetry'
37+
import { getContext } from '../../../shared/vscode/setContext'
3638

3739
/**
3840
* Lakehouse data node for SageMaker Unified Studio
@@ -149,49 +151,63 @@ export function createLakehouseConnectionNode(
149151
},
150152
},
151153
async (node) => {
152-
try {
153-
logger.info(`Loading Lakehouse catalogs for connection ${connection.name}`)
154-
155-
// Check if this is a default connection
156-
const isDefaultConnection =
157-
DATA_DEFAULT_IAM_CONNECTION_NAME_REGEXP.test(connection.name) ||
158-
DATA_DEFAULT_LAKEHOUSE_CONNECTION_NAME_REGEXP.test(connection.name) ||
159-
DATA_DEFAULT_ATHENA_CONNECTION_NAME_REGEXP.test(connection.name)
160-
161-
// Follow the reference pattern with Promise.allSettled
162-
const [awsDataCatalogResult, catalogsResult] = await Promise.allSettled([
163-
// AWS Data Catalog node (only for default connections)
164-
isDefaultConnection
165-
? Promise.resolve([createAwsDataCatalogNode(node, glueClient)])
166-
: Promise.resolve([]),
167-
// Get catalogs by calling Glue API
168-
getCatalogs(glueCatalogClient, glueClient, node),
169-
])
170-
171-
const awsDataCatalog = awsDataCatalogResult.status === 'fulfilled' ? awsDataCatalogResult.value : []
172-
const apiCatalogs = catalogsResult.status === 'fulfilled' ? catalogsResult.value : []
173-
const errors: LakehouseNode[] = []
174-
175-
if (awsDataCatalogResult.status === 'rejected') {
176-
const errorMessage = (awsDataCatalogResult.reason as Error).message
177-
void vscode.window.showErrorMessage(errorMessage)
178-
errors.push(createErrorItem(errorMessage, 'aws-data-catalog', node.id) as LakehouseNode)
179-
}
154+
return telemetry.smus_renderLakehouseNode.run(async (span) => {
155+
const isInSmusSpace = getContext('aws.smus.inSmusSpaceEnvironment')
156+
157+
span.record({
158+
smusToolkitEnv: isInSmusSpace ? 'smus_space' : 'local',
159+
smusDomainId: connection.domainId,
160+
smusProjectId: connection.projectId,
161+
smusConnectionId: connection.connectionId,
162+
smusConnectionType: connection.type,
163+
smusProjectRegion: connection.location?.awsRegion,
164+
})
165+
try {
166+
logger.info(`Loading Lakehouse catalogs for connection ${connection.name}`)
167+
168+
// Check if this is a default connection
169+
const isDefaultConnection =
170+
DATA_DEFAULT_IAM_CONNECTION_NAME_REGEXP.test(connection.name) ||
171+
DATA_DEFAULT_LAKEHOUSE_CONNECTION_NAME_REGEXP.test(connection.name) ||
172+
DATA_DEFAULT_ATHENA_CONNECTION_NAME_REGEXP.test(connection.name)
173+
174+
// Follow the reference pattern with Promise.allSettled
175+
const [awsDataCatalogResult, catalogsResult] = await Promise.allSettled([
176+
// AWS Data Catalog node (only for default connections)
177+
isDefaultConnection
178+
? Promise.resolve([createAwsDataCatalogNode(node, glueClient)])
179+
: Promise.resolve([]),
180+
// Get catalogs by calling Glue API
181+
getCatalogs(glueCatalogClient, glueClient, node),
182+
])
183+
184+
const awsDataCatalog = awsDataCatalogResult.status === 'fulfilled' ? awsDataCatalogResult.value : []
185+
const apiCatalogs = catalogsResult.status === 'fulfilled' ? catalogsResult.value : []
186+
const errors: LakehouseNode[] = []
187+
188+
if (awsDataCatalogResult.status === 'rejected') {
189+
const errorMessage = (awsDataCatalogResult.reason as Error).message
190+
void vscode.window.showErrorMessage(errorMessage)
191+
errors.push(createErrorItem(errorMessage, 'aws-data-catalog', node.id) as LakehouseNode)
192+
}
193+
194+
if (catalogsResult.status === 'rejected') {
195+
const errorMessage = (catalogsResult.reason as Error).message
196+
void vscode.window.showErrorMessage(errorMessage)
197+
errors.push(createErrorItem(errorMessage, 'catalogs', node.id) as LakehouseNode)
198+
}
180199

181-
if (catalogsResult.status === 'rejected') {
182-
const errorMessage = (catalogsResult.reason as Error).message
200+
const allNodes = [...awsDataCatalog, ...apiCatalogs, ...errors]
201+
return allNodes.length > 0
202+
? allNodes
203+
: [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as LakehouseNode]
204+
} catch (err) {
205+
logger.error(`Failed to get Lakehouse catalogs: ${(err as Error).message}`)
206+
const errorMessage = (err as Error).message
183207
void vscode.window.showErrorMessage(errorMessage)
184-
errors.push(createErrorItem(errorMessage, 'catalogs', node.id) as LakehouseNode)
208+
return [createErrorItem(errorMessage, 'lakehouse-catalogs', node.id) as LakehouseNode]
185209
}
186-
187-
const allNodes = [...awsDataCatalog, ...apiCatalogs, ...errors]
188-
return allNodes.length > 0 ? allNodes : [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as LakehouseNode]
189-
} catch (err) {
190-
logger.error(`Failed to get Lakehouse catalogs: ${(err as Error).message}`)
191-
const errorMessage = (err as Error).message
192-
void vscode.window.showErrorMessage(errorMessage)
193-
return [createErrorItem(errorMessage, 'lakehouse-catalogs', node.id) as LakehouseNode]
194-
}
210+
})
195211
}
196212
)
197213
}
@@ -229,7 +245,9 @@ function createAwsDataCatalogNode(parent: LakehouseNode, glueClient: GlueClient)
229245
nextToken = token
230246
} while (nextToken)
231247

232-
return allDatabases.map((database) => createDatabaseNode(database.Name || '', database, glueClient, node))
248+
return allDatabases.length > 0
249+
? allDatabases.map((database) => createDatabaseNode(database.Name || '', database, glueClient, node))
250+
: [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as LakehouseNode]
233251
}
234252
)
235253
}
@@ -392,9 +410,11 @@ function createCatalogNode(
392410
nextToken = token
393411
} while (nextToken)
394412

395-
return allDatabases.map((database) =>
396-
createDatabaseNode(database.Name || '', database, glueClient, node)
397-
)
413+
return allDatabases.length > 0
414+
? allDatabases.map((database) =>
415+
createDatabaseNode(database.Name || '', database, glueClient, node)
416+
)
417+
: [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as LakehouseNode]
398418
} catch (err) {
399419
logger.error(`Failed to get databases for catalog ${catalogId}: ${(err as Error).message}`)
400420
const errorMessage = (err as Error).message
@@ -513,7 +533,10 @@ function createTableNode(
513533
const columns = tableDetails?.StorageDescriptor?.Columns || []
514534
const partitions = tableDetails?.PartitionKeys || []
515535

516-
return [...columns, ...partitions].map((column) => createColumnNode(column.Name || '', column, node))
536+
const allColumns = [...columns, ...partitions]
537+
return allColumns.length > 0
538+
? allColumns.map((column) => createColumnNode(column.Name || '', column, node))
539+
: [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as LakehouseNode]
517540
} catch (err) {
518541
logger.error(`Failed to get columns for table ${tableName}: ${(err as Error).message}`)
519542
return []
@@ -565,7 +588,9 @@ function createContainerNode(
565588
},
566589
async (node) => {
567590
// Map items to nodes
568-
return items.map((item) => createTableNode(item.Name || '', item, glueClient, node))
591+
return items.length > 0
592+
? items.map((item) => createTableNode(item.Name || '', item, glueClient, node))
593+
: [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as LakehouseNode]
569594
}
570595
)
571596
}

0 commit comments

Comments
 (0)