Skip to content

Commit 52d7d00

Browse files
authored
Add cloudformation usage metrics (#308)
1 parent 90d9c8a commit 52d7d00

File tree

11 files changed

+161
-1
lines changed

11 files changed

+161
-1
lines changed

src/autocomplete/CompletionRouter.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CompletionParams } from 'vscode-languageserver';
1+
import { CompletionItem, CompletionParams } from 'vscode-languageserver';
22
import { Context } from '../context/Context';
33
import { ContextManager } from '../context/ContextManager';
44
import {
@@ -20,6 +20,7 @@ import { SettingsConfigurable, ISettingsSubscriber, SettingsSubscription } from
2020
import { CompletionSettings, DefaultSettings } from '../settings/Settings';
2121
import { LoggerFactory } from '../telemetry/LoggerFactory';
2222
import { Track } from '../telemetry/TelemetryDecorator';
23+
import { EventType, UsageTracker } from '../usageTracker/UsageTracker';
2324
import { Closeable } from '../utils/Closeable';
2425
import { CompletionFormatter } from './CompletionFormatter';
2526
import { CompletionProvider } from './CompletionProvider';
@@ -51,6 +52,7 @@ export class CompletionRouter implements SettingsConfigurable, Closeable {
5152
private readonly documentManager: DocumentManager,
5253
private readonly schemaRetriever: SchemaRetriever,
5354
private readonly entityFieldCompletionProviderMap = createEntityFieldProviders(),
55+
private readonly usageTracker: UsageTracker,
5456
) {}
5557

5658
@Track({ name: 'getCompletions', trackObjectKey: 'items' })
@@ -95,6 +97,7 @@ export class CompletionRouter implements SettingsConfigurable, Closeable {
9597

9698
if (completions instanceof Promise) {
9799
return await completions.then((result) => {
100+
trackCompletion(this.usageTracker, provider, result);
98101
return this.formatter.format(
99102
{
100103
isIncomplete: result.length > this.completionSettings.maxCompletions,
@@ -107,6 +110,7 @@ export class CompletionRouter implements SettingsConfigurable, Closeable {
107110
);
108111
});
109112
} else if (completions) {
113+
trackCompletion(this.usageTracker, provider, completions);
110114
const completionList = {
111115
isIncomplete: completions.length > this.completionSettings.maxCompletions,
112116
items: completions.slice(0, this.completionSettings.maxCompletions),
@@ -284,6 +288,7 @@ export class CompletionRouter implements SettingsConfigurable, Closeable {
284288
core.documentManager,
285289
external.schemaRetriever,
286290
createEntityFieldProviders(),
291+
core.usageTracker,
287292
);
288293
}
289294
}
@@ -325,3 +330,13 @@ export function createEntityFieldProviders() {
325330
entityFieldProviderMap.set(EntityType.Output, new EntityFieldCompletionProvider<Output>());
326331
return entityFieldProviderMap;
327332
}
333+
334+
function trackCompletion(
335+
tracker: UsageTracker,
336+
provider: CompletionProvider | undefined,
337+
completions: CompletionItem[],
338+
) {
339+
if (provider !== undefined && !(provider instanceof TopLevelSectionCompletionProvider) && completions.length > 0) {
340+
tracker.track(EventType.MeaningfulCompletion);
341+
}
342+
}

src/handlers/HoverHandler.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Hover, HoverParams, MarkupKind } from 'vscode-languageserver';
22
import { ServerRequestHandler } from 'vscode-languageserver/lib/common/server';
33
import { ServerComponents } from '../server/ServerComponents';
44
import { TelemetryService } from '../telemetry/TelemetryService';
5+
import { EventType } from '../usageTracker/UsageTracker';
56

67
export function hoverHandler(
78
components: ServerComponents,
@@ -15,6 +16,7 @@ export function hoverHandler(
1516
};
1617
}
1718

19+
components.usageTracker.track(EventType.MeaningfulHover);
1820
return {
1921
contents: {
2022
kind: MarkupKind.Markdown,

src/handlers/ResourceHandler.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { ServerComponents } from '../server/ServerComponents';
2222
import { GetStackTemplateParams, GetStackTemplateResult } from '../stacks/StackRequestType';
2323
import { LoggerFactory } from '../telemetry/LoggerFactory';
2424
import { TelemetryService } from '../telemetry/TelemetryService';
25+
import { EventType } from '../usageTracker/UsageTracker';
2526
import { parseWithPrettyError } from '../utils/ZodErrorWrapper';
2627

2728
const log = LoggerFactory.getLogger('ResourceHandler');
@@ -80,6 +81,7 @@ export function importResourceStateHandler(
8081
components: ServerComponents,
8182
): ServerRequestHandler<ResourceStateParams, ResourceStateResult, never, void> {
8283
return async (params: ResourceStateParams): Promise<ResourceStateResult> => {
84+
components.usageTracker.track(EventType.DidImportResources);
8385
return await components.resourceStateImporter.importResourceState(params);
8486
};
8587
}

src/handlers/StackHandler.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {
5050
DescribeChangeSetResult,
5151
} from '../stacks/StackRequestType';
5252
import { TelemetryService } from '../telemetry/TelemetryService';
53+
import { EventType } from '../usageTracker/UsageTracker';
5354
import { handleLspError } from '../utils/Errors';
5455
import { parseWithPrettyError } from '../utils/ZodErrorWrapper';
5556

@@ -111,6 +112,7 @@ export function createValidationHandler(
111112
): RequestHandler<CreateValidationParams, CreateStackActionResult, void> {
112113
return async (rawParams) => {
113114
return await TelemetryService.instance.get('StackHandler').measureAsync('createValidation', async () => {
115+
components.usageTracker.track(EventType.DidValidation);
114116
try {
115117
const params = parseWithPrettyError(parseCreateValidationParams, rawParams);
116118
return await components.validationWorkflowService.start(params);
@@ -126,6 +128,7 @@ export function createDeploymentHandler(
126128
): RequestHandler<CreateDeploymentParams, CreateStackActionResult, void> {
127129
return async (rawParams) => {
128130
return await TelemetryService.instance.get('StackHandler').measureAsync('createDeployment', async () => {
131+
components.usageTracker.track(EventType.DidDeployment);
129132
try {
130133
const params = parseWithPrettyError(parseCreateDeploymentParams, rawParams);
131134
return await components.deploymentWorkflowService.start(params);

src/server/CfnInfraCore.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { SettingsManager } from '../settings/SettingsManager';
1111
import { ValidationManager } from '../stacks/actions/ValidationManager';
1212
import { ClientMessage } from '../telemetry/ClientMessage';
1313
import { TelemetryService } from '../telemetry/TelemetryService';
14+
import { UsageTracker } from '../usageTracker/UsageTracker';
15+
import { UsageTrackerMetrics } from '../usageTracker/UsageTrackerMetrics';
1416
import { Closeable, closeSafely } from '../utils/Closeable';
1517
import { Configurable, Configurables } from '../utils/Configurable';
1618
import { ExtendedInitializeParams } from './InitParams';
@@ -34,6 +36,8 @@ export class CfnInfraCore implements Configurables, Closeable {
3436
readonly validationManager: ValidationManager;
3537
readonly diagnosticCoordinator: DiagnosticCoordinator;
3638
readonly cloudformationEndpoint?: string;
39+
readonly usageTracker: UsageTracker;
40+
readonly usageTrackerMetrics: UsageTrackerMetrics;
3741

3842
constructor(
3943
lspComponents: LspComponents,
@@ -68,6 +72,9 @@ export class CfnInfraCore implements Configurables, Closeable {
6872
this.diagnosticCoordinator =
6973
overrides.diagnosticCoordinator ??
7074
new DiagnosticCoordinator(lspComponents.diagnostics, this.syntaxTreeManager, this.validationManager);
75+
76+
this.usageTracker = overrides.usageTracker ?? new UsageTracker();
77+
this.usageTrackerMetrics = overrides.usageTrackerMetrics ?? new UsageTrackerMetrics(this.usageTracker);
7178
}
7279

7380
configurables(): Configurable[] {

src/usageTracker/UsageTracker.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export enum EventType {
2+
DidDeployment = 'DidDeployment',
3+
DidValidation = 'DidValidation',
4+
DidImportResources = 'DidImportResources',
5+
MeaningfulHover = 'MeaningfulHover',
6+
MeaningfulCompletion = 'MeaningfulCompletion',
7+
}
8+
9+
export class UsageTracker {
10+
private readonly events = new Set<EventType>();
11+
12+
track(event: EventType): void {
13+
this.events.add(event);
14+
}
15+
16+
allUsed(...events: EventType[]): boolean {
17+
return events.every((event) => {
18+
return this.events.has(event);
19+
});
20+
}
21+
22+
someUsed(...events: EventType[]): boolean {
23+
return events.some((event) => {
24+
return this.events.has(event);
25+
});
26+
}
27+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ScopedTelemetry } from '../telemetry/ScopedTelemetry';
2+
import { Telemetry } from '../telemetry/TelemetryDecorator';
3+
import { EventType, UsageTracker } from './UsageTracker';
4+
5+
export class UsageTrackerMetrics {
6+
@Telemetry() private readonly telemetry!: ScopedTelemetry;
7+
8+
constructor(private readonly usageTracker: UsageTracker) {
9+
this.registerMetrics();
10+
}
11+
12+
private registerMetrics(): void {
13+
this.telemetry.registerGaugeProvider('cloudformation.used', () =>
14+
this.usageTracker.someUsed(
15+
EventType.DidDeployment,
16+
EventType.DidValidation,
17+
EventType.DidImportResources,
18+
EventType.MeaningfulHover,
19+
EventType.MeaningfulCompletion,
20+
)
21+
? 1
22+
: 0,
23+
);
24+
}
25+
}

tst/unit/autocomplete/CompletionRouter.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { EntityType } from '../../../src/context/semantic/SemanticTypes';
1010
import { SyntaxTreeManager } from '../../../src/context/syntaxtree/SyntaxTreeManager';
1111
import { DocumentType } from '../../../src/document/Document';
1212
import { CombinedSchemas } from '../../../src/schema/CombinedSchemas';
13+
import { UsageTracker } from '../../../src/usageTracker/UsageTracker';
1314
import { ExtensionName } from '../../../src/utils/ExtensionConfig';
1415
import { createResourceContext, createTopLevelContext } from '../../utils/MockContext';
1516
import {
@@ -250,6 +251,7 @@ describe('CompletionRouter', () => {
250251
mockDocumentManager,
251252
mockComponents.schemaRetriever,
252253
entityFieldProviderMap,
254+
new UsageTracker(),
253255
);
254256

255257
function expectCompletionProvider(
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { beforeEach, describe, expect, test } from 'vitest';
2+
import { EventType, UsageTracker } from '../../../src/usageTracker/UsageTracker';
3+
4+
describe('UsageTracker', () => {
5+
let tracker: UsageTracker;
6+
7+
beforeEach(() => {
8+
tracker = new UsageTracker();
9+
});
10+
11+
describe('track', () => {
12+
test('tracks single event', () => {
13+
tracker.track(EventType.DidDeployment);
14+
expect(tracker.allUsed(EventType.DidDeployment)).toBe(true);
15+
});
16+
17+
test('tracks multiple events', () => {
18+
tracker.track(EventType.DidDeployment);
19+
tracker.track(EventType.DidValidation);
20+
expect(tracker.allUsed(EventType.DidDeployment, EventType.DidValidation)).toBe(true);
21+
});
22+
23+
test('does not duplicate events', () => {
24+
tracker.track(EventType.DidDeployment);
25+
tracker.track(EventType.DidDeployment);
26+
expect(tracker.allUsed(EventType.DidDeployment)).toBe(true);
27+
});
28+
});
29+
30+
describe('allUsed', () => {
31+
test('returns true when all events tracked', () => {
32+
tracker.track(EventType.DidDeployment);
33+
tracker.track(EventType.DidValidation);
34+
expect(tracker.allUsed(EventType.DidDeployment, EventType.DidValidation)).toBe(true);
35+
});
36+
37+
test('returns false when some events not tracked', () => {
38+
tracker.track(EventType.DidDeployment);
39+
expect(tracker.allUsed(EventType.DidDeployment, EventType.DidValidation)).toBe(false);
40+
});
41+
42+
test('returns false when no events tracked', () => {
43+
expect(tracker.allUsed(EventType.DidDeployment)).toBe(false);
44+
});
45+
46+
test('returns true for empty list', () => {
47+
expect(tracker.allUsed()).toBe(true);
48+
});
49+
});
50+
51+
describe('someUsed', () => {
52+
test('returns true when at least one event tracked', () => {
53+
tracker.track(EventType.DidDeployment);
54+
expect(tracker.someUsed(EventType.DidDeployment, EventType.DidValidation)).toBe(true);
55+
});
56+
57+
test('returns false when no events tracked', () => {
58+
expect(tracker.someUsed(EventType.DidDeployment, EventType.DidValidation)).toBe(false);
59+
});
60+
61+
test('returns true when all events tracked', () => {
62+
tracker.track(EventType.DidDeployment);
63+
tracker.track(EventType.DidValidation);
64+
expect(tracker.someUsed(EventType.DidDeployment, EventType.DidValidation)).toBe(true);
65+
});
66+
67+
test('returns false for empty list', () => {
68+
expect(tracker.someUsed()).toBe(false);
69+
});
70+
});
71+
});

tst/utils/MockServerComponents.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ import { ValidationWorkflow } from '../../src/stacks/actions/ValidationWorkflow'
6565
import { StackEventManager } from '../../src/stacks/StackEventManager';
6666
import { StackManager } from '../../src/stacks/StackManager';
6767
import { ClientMessage } from '../../src/telemetry/ClientMessage';
68+
import { UsageTracker } from '../../src/usageTracker/UsageTracker';
69+
import { UsageTrackerMetrics } from '../../src/usageTracker/UsageTrackerMetrics';
6870
import { Closeable } from '../../src/utils/Closeable';
6971
import { Configurables } from '../../src/utils/Configurable';
7072

@@ -368,6 +370,8 @@ export function createMockComponents(o: Partial<CfnLspServerComponentsType> = {}
368370
awsCredentials: overrides.awsCredentials ?? createMockAwsCredentials(),
369371
validationManager: overrides.validationManager ?? stubInterface<ValidationManager>(),
370372
diagnosticCoordinator: overrides.diagnosticCoordinator ?? createMockDiagnosticCoordinator(),
373+
usageTracker: stubInterface<UsageTracker>(),
374+
usageTrackerMetrics: stubInterface<UsageTrackerMetrics>(),
371375
close: () => Promise.resolve(),
372376
configurables: () => [],
373377
};

0 commit comments

Comments
 (0)