Skip to content

Commit 8df9b06

Browse files
authored
chore(core): add parseAnalytics function for debugging (#36125)
### Reason for this change It always bothered me that we don't have an easy way of converting a template analytics string back for debugging. This PR adds a debugging utility to decode analytics strings back into readable ConstructInfo objects. ### Description of changes - Added `parseAnalytics()` function that reverses the `formatAnalytics()` encoding - Added `parsePrefixEncodedList()` helper to decode the trie structure - Added `trieToConstructInfos()` to convert trie back to ConstructInfo array - Added comprehensive tests verifying parseAnalytics is the inverse of formatAnalytics The implementation handles the v2:deflate64 format by base64 decoding, gunzipping, and parsing the prefix-encoded trie structure. ### Describe any new or updated permissions being added None ### Description of how you validated changes Added unit tests covering: - Single construct parsing - Multiple constructs with same version - Nested module constructs - Different versions All tests verify that `parseAnalytics(formatAnalytics(x)) === x` ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 5891172 commit 8df9b06

File tree

2 files changed

+123
-1
lines changed

2 files changed

+123
-1
lines changed

packages/aws-cdk-lib/core/lib/private/metadata-resource.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,43 @@ export function formatAnalytics(infos: ConstructInfo[], enableAdditionalTelemtry
111111
return analyticsString;
112112
}
113113

114+
/**
115+
* Takes an analytics string and converts it back into a readable format.
116+
* Useful for debugging.
117+
*
118+
* @internal
119+
*/
120+
export function parseAnalytics(analyticsString: string): ConstructInfo[] {
121+
const analyticsData = analyticsString.split(':');
122+
if (analyticsData.length >= 3 && analyticsData[0] === 'v2' && analyticsData[1] === 'deflate64') {
123+
const buffer = Buffer.from(analyticsData[2], 'base64');
124+
const decompressedBuffer = zlib.gunzipSync(buffer);
125+
const prefixEncodedList = decompressedBuffer.toString('utf8');
126+
const trie = parsePrefixEncodedList(prefixEncodedList);
127+
return trieToConstructInfos(trie);
128+
} else {
129+
throw new AssumptionError(`Invalid analytics string: ${analyticsString}`);
130+
}
131+
}
132+
133+
/**
134+
* Converts a Trie back to a list of ConstructInfo objects.
135+
*/
136+
function trieToConstructInfos(trie: Trie): ConstructInfo[] {
137+
const infos: ConstructInfo[] = [];
138+
function traverse(node: Trie, path: string) {
139+
if (node.size === 0) {
140+
const [version, fqn] = path.split('!');
141+
infos.push({ version, fqn });
142+
}
143+
for (const [key, value] of node.entries()) {
144+
traverse(value, path + key);
145+
}
146+
}
147+
traverse(trie, '');
148+
return infos;
149+
}
150+
114151
/**
115152
* Splits after non-alphanumeric characters (e.g., '.', '/') in the FQN
116153
* and insert each piece of the FQN in nested map (i.e., simple trie).
@@ -163,6 +200,54 @@ function prefixEncodeTrie(trie: Trie) {
163200
return prefixEncoded;
164201
}
165202

203+
/**
204+
* Parses a prefix-encoded "trie-ish" structure.
205+
* This is the inverse of `prefixEncodeTrie`.
206+
*
207+
* Example input:
208+
* A{B{C,D},EF}
209+
*
210+
* Becomes:
211+
* ABC,ABD,AEF
212+
*
213+
* Example trie:
214+
* A --> B --> C
215+
* | \--> D
216+
* \--> E --> F
217+
*/
218+
function parsePrefixEncodedList(data: string): Trie {
219+
const trie = new Trie();
220+
let i = 0;
221+
222+
function parse(currentTrie: Trie, prefix: string) {
223+
let token = '';
224+
while (i < data.length) {
225+
const char = data[i];
226+
if (char === '{') {
227+
i++;
228+
parse(currentTrie, prefix + token);
229+
token = '';
230+
} else if (char === '}' || char === ',') {
231+
if (token) {
232+
insertFqnInTrie(prefix + token, trie);
233+
}
234+
i++;
235+
if (char === '}') return;
236+
token = '';
237+
} else {
238+
token += char;
239+
i++;
240+
}
241+
}
242+
if (token) {
243+
insertFqnInTrie(prefix + token, trie);
244+
}
245+
}
246+
247+
parse(trie, '');
248+
return trie;
249+
}
250+
166251
/**
167252
* Sets the OS flag to "unknown" in order to ensure we get consistent results across operating systems.
168253
*

packages/aws-cdk-lib/core/test/metadata-resource.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as zlib from 'zlib';
22
import { Construct } from 'constructs';
33
import { ENABLE_ADDITIONAL_METADATA_COLLECTION } from '../../cx-api';
44
import { App, Stack, IPolicyValidationPluginBeta1, IPolicyValidationContextBeta1, Stage, PolicyValidationPluginReportBeta1, FeatureFlags, Duration } from '../lib';
5-
import { formatAnalytics } from '../lib/private/metadata-resource';
5+
import { formatAnalytics, parseAnalytics } from '../lib/private/metadata-resource';
66
import { ConstructInfo } from '../lib/private/runtime-info';
77

88
describe('MetadataResource', () => {
@@ -222,6 +222,43 @@ describe('formatAnalytics', () => {
222222
}
223223
});
224224

225+
describe('parseAnalytics', () => {
226+
test('parseAnalytics is the inverse of formatAnalytics for single construct', () => {
227+
const constructInfo = [{ fqn: 'aws-cdk-lib.Construct', version: '1.2.3' }];
228+
const analytics = formatAnalytics(constructInfo);
229+
expect(parseAnalytics(analytics)).toEqual(constructInfo);
230+
});
231+
232+
test('parseAnalytics is the inverse of formatAnalytics for multiple constructs', () => {
233+
const constructInfo = [
234+
{ fqn: 'aws-cdk-lib.Construct', version: '1.2.3' },
235+
{ fqn: 'aws-cdk-lib.CfnResource', version: '1.2.3' },
236+
{ fqn: 'aws-cdk-lib.Stack', version: '1.2.3' },
237+
];
238+
const analytics = formatAnalytics(constructInfo);
239+
expect(parseAnalytics(analytics)).toEqual(constructInfo);
240+
});
241+
242+
test('parseAnalytics is the inverse of formatAnalytics for nested modules', () => {
243+
const constructInfo = [
244+
{ fqn: 'aws-cdk-lib.Construct', version: '1.2.3' },
245+
{ fqn: 'aws-cdk-lib.aws_servicefoo.CoolResource', version: '1.2.3' },
246+
{ fqn: 'aws-cdk-lib.aws_servicefoo.OtherResource', version: '1.2.3' },
247+
];
248+
const analytics = formatAnalytics(constructInfo);
249+
expect(parseAnalytics(analytics)).toEqual(constructInfo);
250+
});
251+
252+
test('parseAnalytics is the inverse of formatAnalytics for different versions', () => {
253+
const constructInfo = [
254+
{ fqn: 'aws-cdk-lib.Construct', version: '1.2.3' },
255+
{ fqn: 'aws-cdk-lib.CoolResource', version: '0.1.2' },
256+
];
257+
const analytics = formatAnalytics(constructInfo);
258+
expect(parseAnalytics(analytics)).toEqual(constructInfo);
259+
});
260+
});
261+
225262
function plaintextConstructsFromAnalytics(analytics: string) {
226263
return zlib.gunzipSync(Buffer.from(analytics.split(':')[2], 'base64')).toString('utf-8');
227264
}

0 commit comments

Comments
 (0)