Skip to content

Commit fb00610

Browse files
authored
collect and publish anonymous telemetry payload (#126)
1 parent 48e96c6 commit fb00610

17 files changed

Lines changed: 2495 additions & 421 deletions

.vscode/settings.json

Lines changed: 0 additions & 3 deletions
This file was deleted.

PublishTestPlanResultsV1/TaskParameters.ts

Lines changed: 140 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,26 @@ import FeatureFlags, { FeatureFlag } from './services/FeatureFlags';
1313

1414
class TaskParameters {
1515

16+
// dependencies
1617
tph: TaskParameterHelper
18+
19+
// cached parameters
20+
accessToken?: string;
21+
collectionUri?: string;
22+
projectName?: string;
23+
24+
buildId?: string;
25+
releaseUri?: string;
26+
releaseEnvironmentUri?: string;
27+
28+
testFiles: string[] = [];
29+
1730
constructor(tph: TaskParameterHelper) {
1831
this.tph = tph;
32+
const {buildId, releaseUri, releaseEnvironmentUri } = this.#getPipelineEnvironment();
33+
this.buildId = buildId;
34+
this.releaseUri = releaseUri;
35+
this.releaseEnvironmentUri = releaseEnvironmentUri;
1936
}
2037

2138
static getInstance() : TaskParameters {
@@ -27,154 +44,194 @@ class TaskParameters {
2744
/* Fetch the parameters used to obtain the working details for the ADO Test Plan */
2845
getTestContextParameters(): TestResultContextParameters {
2946
tl.debug("reading TestContextParameters from task inputs.");
47+
this.tph.recordStage("getTestContextParameters");
3048

31-
const accessToken = tl.getInput("accessToken", false) ?? tl.getVariable("SYSTEM_ACCESSTOKEN");
32-
const collectionUri = tl.getInput("collectionUri", false) ?? tl.getVariable("SYSTEM_COLLECTIONURI");
33-
const projectName = tl.getInput("projectName", false) ?? tl.getVariable("SYSTEM_TEAMPROJECT");
34-
35-
var parameters = new TestResultContextParameters(
36-
(collectionUri as string),
37-
(projectName as string),
38-
(accessToken as string));
49+
this.#ensureCredentialsAreSet();
50+
var parameters = new TestResultContextParameters(this.collectionUri!, this.projectName!, this.accessToken!);
3951

40-
parameters.testPlan = tl.getInput("testPlan", false);
41-
parameters.testConfigFilter = tl.getInput("testConfigFilter", false);
42-
tl.getDelimitedInput("testConfigAliases", ",", false).forEach((alias: string) => {
52+
parameters.testPlan = this.tph.getInput("testPlan", false, { recordNonDefault: true })!;
53+
parameters.testConfigFilter = this.tph.getInput("testConfigFilter", false, { recordNonDefault: true });
54+
this.tph.getDelimitedInput("testConfigAliases", { recordNonDefault: true }).forEach((alias: string) => {
4355
let parts = alias.split("=");
4456
if (parts.length > 1) {
4557
parameters.testConfigAliases.push(new configAlias(parts[0], parts[1]));
4658
}
4759
});
4860

61+
this.tph.recordStage("createContext");
4962
return parameters;
5063
}
5164

5265
/* Fetch the parameters used to parse through automated test results */
5366
getFrameworkParameters(): TestFrameworkParameters {
5467
tl.debug("reading TestFrameworkParameters from task inputs.");
68+
this.tph.recordStage("getFrameworkParameters");
5569

56-
let testResultFormat = tl.getInput("testResultFormat", true);
57-
let failTaskOnMissingResultsFile = getBoolInput("failTaskOnMissingResultsFile", /*default*/ true);
58-
let failTaskOnMissingTests = getBoolInput("failTaskOnMissingTests", /*default*/ false);
59-
let testResultFiles = getTestFiles(failTaskOnMissingResultsFile);
60-
61-
return new TestFrameworkParameters(testResultFiles, testResultFormat!.toLowerCase(), failTaskOnMissingResultsFile, failTaskOnMissingTests);
62-
}
70+
let testResultFormat = this.tph.getInput("testResultFormat", true, { recordValue: true});
71+
let failTaskOnMissingResultsFile = this.tph.getBoolInput("failTaskOnMissingResultsFile", /*default*/ true, { recordValue: true, recordNonDefault: true});
72+
let failTaskOnMissingTests = this.tph.getBoolInput("failTaskOnMissingTests", /*default*/ false, { recordValue: true, recordNonDefault: true});
73+
this.testFiles = this.#getTestFiles(failTaskOnMissingResultsFile);
74+
75+
const parameters = new TestFrameworkParameters(this.testFiles, testResultFormat!.toLowerCase(), failTaskOnMissingResultsFile, failTaskOnMissingTests);
76+
this.tph.recordStage("readFrameworkResults");
77+
return parameters;
78+
}
6379

6480
/* Fetch the parameters used to process test results and match them to test cases */
6581
getProcessorParameters() : TestResultProcessorParameters {
6682
tl.debug("reading TestResultProcessorParameters from task inputs.");
83+
this.tph.recordStage("getProcessorParameters");
6784

68-
let matchingStrategy = tl.getInput("testCaseMatchStrategy", false) ?? "Auto";
85+
let matchingStrategy = this.tph.getInputOrFallback("testCaseMatchStrategy", () => "Auto", { recordValue: true, recordNonDefault: true, dontRecordDefault: true })!;
6986
var parameters = new TestResultProcessorParameters(matchingStrategy);
7087

7188
// optional parameters
72-
parameters.testConfigFilter = tl.getInput("testConfigFilter", false);
73-
parameters.testCaseProperty = tl.getInput("testCaseProperty", false) ?? "TestCase";
74-
parameters.testCaseRegEx = tl.getInput("testCaseRegex", false) ?? "(\\d+)";
75-
parameters.testConfigProperty = tl.getInput("testConfigProperty", false) ?? "Config";
89+
parameters.testConfigFilter = this.tph.getInput("testConfigFilter", false, { recordNonDefault: true });
90+
parameters.testCaseProperty = this.tph.getInputOrFallback("testCaseProperty", () => "TestCase", { recordValue: true, recordNonDefault: true, dontRecordDefault: true });
91+
parameters.testCaseRegEx = this.tph.getInputOrFallback("testCaseRegex", () => "(\\d+)", { recordValue: true, recordNonDefault: true, dontRecordDefault: true });
92+
parameters.testConfigProperty = this.tph.getInputOrFallback("testConfigProperty", () => "Config", { recordValue: true, recordNonDefault: true, dontRecordDefault: true });
7693

94+
this.tph.recordStage("processFrameworkResults");
7795
return parameters;
7896
}
7997

8098
/* Fetch the parameters used to publish test results to ADO Test Plan */
8199
getPublisherParameters() : TestRunPublisherParameters {
82100
tl.debug("reading TestRunPublisherParameters from task inputs.");
83-
84-
const accessToken = tl.getInput("accessToken", false) ?? tl.getVariable("SYSTEM_ACCESSTOKEN");
85-
const buildId = tl.getVariable("BUILD_BUILDID")!; // available in build and release pipelines
86-
const releaseUri = tl.getVariable("RELEASE_RELEASEURI"); // only in release pipelines
87-
const releaseEnvironmentUri = tl.getVariable("RELEASE_ENVIRONMENTURI"); // only in release pipelines
88-
const collectionUri = tl.getInput("collectionUri", false) ?? tl.getVariable("SYSTEM_COLLECTIONURI")!;
89-
const dryRun = tl.getBoolInput("dryRun", false);
90-
const testRunTitle = tl.getInput("testRunTitle", false) ?? "PublishTestPlanResult";
91-
let verifyFiles = getBoolInput("failTaskOnMissingResultsFile", /*default*/ true);
92-
let failTaskOnUnmatchedTestCases = getBoolInput("failTaskOnUnmatchedTestCases", /*default*/ true);
93-
const testFiles = getTestFiles(verifyFiles).filter(file => file.indexOf('**') == -1);
101+
this.tph.recordStage("getPublisherParameters");
102+
103+
this.#ensureCredentialsAreSet();
104+
const dryRun = this.tph.getBoolInput("dryRun", false, { recordValue: true, dontRecordDefault: true });
105+
const testRunTitle = this.tph.getInputOrFallback("testRunTitle", () => "PublishTestPlanResult", { recordNonDefault: true });
106+
let failTaskOnUnmatchedTestCases = this.tph.getBoolInput("failTaskOnUnmatchedTestCases", /*default*/ true, { recordValue: true, dontRecordDefault: true });
107+
const testFiles = this.testFiles.filter(file => file.indexOf('**') == -1);
94108
let result = new TestRunPublisherParameters(
95-
collectionUri,
96-
accessToken as string,
109+
this.collectionUri!,
110+
this.accessToken!,
97111
dryRun,
98112
testRunTitle,
99-
buildId,
113+
this.buildId!,
100114
testFiles,
101115
failTaskOnUnmatchedTestCases
102116
);
103-
result.releaseUri = releaseUri;
104-
result.releaseEnvironmentUri = releaseEnvironmentUri;
117+
result.releaseUri = this.releaseUri;
118+
result.releaseEnvironmentUri = this.releaseEnvironmentUri;
119+
120+
this.tph.recordStage("publishTestRunResults");
105121
return result;
106122
}
107123

108124
/* Fetch the parameters used to filter test results and finalize task outcome */
109125
getStatusFilterParameters() : StatusFilterParameters {
110126
tl.debug("reading StatusFilterParameters from task inputs.");
127+
this.tph.recordStage("getStatusFilterParameters");
111128

112129
var parameters = new StatusFilterParameters();
113-
parameters.failTaskOnFailedTests = getBoolInput("failTaskOnFailedTests", /*default*/ false);
114-
parameters.failTaskOnSkippedTests = getBoolInput("failTaskOnSkippedTests", /*default*/ false);
130+
parameters.failTaskOnFailedTests = this.tph.getBoolInput("failTaskOnFailedTests", /*default*/ false, { recordValue: true, dontRecordDefault: true });
131+
parameters.failTaskOnSkippedTests = this.tph.getBoolInput("failTaskOnSkippedTests", /*default*/ false, { recordValue: true, dontRecordDefault: true });
115132

133+
this.tph.recordStage("finalizeResults");
116134
return parameters;
117135
}
118136

119137
/* Fetch the telemetry payload for this task execution */
120138
getTelemetryParameters(err?: any) : TelemetryPublisherParameters {
121139
const hasError = err !== undefined && err !== null;
122-
const withErrorOrWithoutError = hasError ? "with error" : "without error";
140+
const withErrorOrWithoutError = hasError ? "(error condition)" : "";
123141
tl.debug(`reading TelemetryPublisherParameters from task inputs ${withErrorOrWithoutError}.`);
142+
// don't record stage so that we can publish which stage we last completed
124143

125144
const result = new TelemetryPublisherParameters();
126145
result.displayTelemetryPayload = FeatureFlags.isFeatureEnabled(FeatureFlag.DisplayTelemetry);
127146
result.displayTelemetryErrors = FeatureFlags.isFeatureEnabled(FeatureFlag.DisplayTelemetryErrors);
147+
result.publishTelemetry = FeatureFlags.isFeatureEnabled(FeatureFlag.PublishTelemetry);
128148

129149
result.payload = this.tph.getPayload(err); // todo: specify privacy level
130150
result.payload["flags"] = FeatureFlags.getFlags();
131151
return result;
132152
}
133-
}
134153

135-
export default TaskParameters.getInstance();
136-
export { TaskParameters };
137-
138-
function getTestFiles(verifyFiles: boolean) : string[] {
139-
140-
let testResultFolder = tl.getInput("testResultDirectory", false);
141-
if (testResultFolder == undefined) {
142-
// System.DefaultWorkingDirectory:
143-
// - build pipelines: "C:\agent\work\1\s" equivalent to "$(Build.SourcesDirectory)"
144-
// - release pipelines: "C:\agent\work\r1\a" equivalent to "$(System.ArtifactsDirectory)"
145-
testResultFolder = tl.getVariable("SYSTEM_DEFAULTWORKINGDIRECTORY") as string;
146-
147-
tl.debug(`testResultDirectory was not specified. Using default working directory: ${testResultFolder}`);
154+
#ensureCredentialsAreSet() {
155+
if (!this.accessToken) {
156+
this.accessToken = this.tph.getInputOrFallback("accessToken", () => tl.getVariable("SYSTEM_ACCESSTOKEN"), { recordNonDefault: true });
157+
this.projectName = this.tph.getInputOrFallback("projectName", () => tl.getVariable("SYSTEM_TEAMPROJECT"), { recordNonDefault: true, anonymize: true });
158+
this.collectionUri = this.tph.getInputOrFallback("collectionUri", () => tl.getVariable("SYSTEM_COLLECTIONURI"), { recordNonDefault: true, anonymize: true });
159+
160+
let serverType = (this.collectionUri && (this.collectionUri.startsWith("https://dev.azure.com/") || this.collectionUri.includes(".visualstudio.com"))) ?
161+
"Hosted" : "OnPremises";
162+
this.tph.payloadBuilder.add("serverType", serverType);
163+
}
148164
}
149165

150-
let testResultFiles = tl.getDelimitedInput("testResultFiles", ",", true)
151-
.map(file => {
152-
// merge relative paths with the testresult folder
153-
if (!path.isAbsolute(file)) {
154-
tl.debug(`joining relative path '${file}' with testResultDirectory '${testResultFolder}'`);
155-
return path.join(testResultFolder, file);
156-
}
157-
return file;
158-
})
159-
.filter(file => {
160-
// if it's not a wildcard, verify that the file exists
161-
if (file.indexOf('**') == -1) {
162-
// either filter out missing files, or fail the task based on user-preference
163-
if (verifyFiles) {
164-
// fail if the file does not exist
165-
tl.checkPath(file, "testResultFile(s)");
166-
} else {
167-
// task supports missing files, so filter out missing files
168-
return tl.exist(file);
166+
#getTestFiles(verifyFiles: boolean) : string[] {
167+
var wildCardUsed : boolean | undefined = undefined;
168+
169+
// Resolve the user specified testResultDirectory.
170+
// If not specified, default to SYSTEM_DEFAULTWORKINGDIRECTORY.
171+
// - For Build Pipelines: "C:\agent\work\1\s" equivalent to "$(Build.SourcesDirectory)"
172+
// - For Release Pipelines: "C:\agent\work\r1\a" equivalent to "$(System.ArtifactsDirectory)"
173+
let testResultFolder = this.tph.getInputOrFallback("testResultDirectory", () => tl.getVariable("SYSTEM_DEFAULTWORKINGDIRECTORY")!, { recordNonDefault: true });
174+
175+
let testResultFiles = tl.getDelimitedInput("testResultFiles", ",", true)
176+
.map(file => {
177+
// merge relative paths with the testresult folder
178+
if (!path.isAbsolute(file)) {
179+
tl.debug(`joining relative path '${file}' with testResultDirectory '${testResultFolder}'`);
180+
return path.join(testResultFolder, file);
169181
}
170-
}
171-
return true;
172-
});
182+
return file;
183+
})
184+
.filter(file => {
185+
// if it's not a wildcard, verify that the file exists
186+
if (file.indexOf('**') == -1) {
187+
// either filter out missing files, or fail the task based on user-preference
188+
if (verifyFiles) {
189+
// fail if the file does not exist
190+
tl.checkPath(file, "testResultFile(s)");
191+
} else {
192+
// task supports missing files, so filter out missing files
193+
return tl.exist(file);
194+
}
195+
}
196+
else {
197+
wildCardUsed = true;
198+
}
199+
return true;
200+
});
201+
202+
// update telemetry
203+
if (wildCardUsed) {
204+
this.tph.payloadBuilder.add("testResultFilesWildcard", true);
205+
}
206+
if (testResultFiles.length > 0) {
207+
this.tph.payloadBuilder.add("numTestFiles", testResultFiles.length);
208+
}
209+
210+
return testResultFiles;
211+
}
173212

174-
return testResultFiles;
213+
#getPipelineEnvironment() : { buildId: string, releaseUri?: string, releaseEnvironmentUri?: string } {
214+
215+
// determine whether we're running in a build or release pipeline
216+
const buildId = tl.getVariable("BUILD_BUILDID")!; // available in build and release pipelines
217+
const releaseUri = tl.getVariable("RELEASE_RELEASEURI"); // only in release pipelines
218+
const releaseEnvironmentUri = tl.getVariable("RELEASE_ENVIRONMENTURI"); // only in release pipelines
219+
220+
// detect running in a build or a release
221+
let hostType = (releaseUri && releaseEnvironmentUri) ? "release" : "build";
222+
this.tph.payloadBuilder.add("hostType", hostType);
223+
224+
// detect if we're using a hosted or self-hosted agent
225+
let agentType = tl.getVariable("Agent.CloudId") ? "Hosted" : "OnPremises";
226+
this.tph.payloadBuilder.add("agentType", agentType);
227+
228+
// collect the agent version
229+
let agentVersion = tl.getVariable("Agent.Version") || '';
230+
this.tph.payloadBuilder.add("agentVersion", agentVersion);
231+
232+
return { buildId, releaseUri, releaseEnvironmentUri };
233+
}
175234
}
176235

177-
function getBoolInput(name: string, defaultValue: boolean) : boolean {
178-
let input = tl.getInput(name, false);
179-
return input ? tl.getBoolInput(name, false) : defaultValue;
180-
}
236+
export default TaskParameters.getInstance();
237+
export { TaskParameters };

PublishTestPlanResultsV1/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ async function run() {
6565
tl.setResult(tl.TaskResult.Failed, 'An unhandled error occurred.');
6666
}
6767
}
68+
finally {
69+
TelemetryPublisher.dispose();
70+
}
6871
}
6972

7073
run();

0 commit comments

Comments
 (0)