Skip to content

Commit d8bdd82

Browse files
committed
Adding new survey question and improving logic for capturing answers
1 parent 5e59bc4 commit d8bdd82

File tree

12 files changed

+1296
-1267
lines changed

12 files changed

+1296
-1267
lines changed

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66

77
As more companies adopt GitHub Copilot, it becomes increasingly important to measure the benefits it brings to the organization. This survey is an effort to combine both quantitative and qualitative data. To improve validity of the quantitative responses, Developers are asked to document their rationale for the time-savings percentage they choose.
88

9-
Quantitative feedback from the Developer at the time of creating a PR provides valuable insights on the time savings experienced by the Developer. Time savings is needed first before other downstream impacts (like velocity increases, or other improvements can happen. The level of granularity provides multiple feedback opportunities for Developers and can capture a variety of PRs so we can understand adoption challenges and improvement opportunities. If helpful, the Survey results may also be combined with Key Performance Indicators (KPIs) that the product provides to further contextualize the survey responses.
9+
Quantitative feedback from the Developer at the time of creating a PR provides valuable insights on the time savings experienced by the Developer. Time savings is needed first before other downstream impacts like velocity increases, or other improvements can happen. The level of granularity provides multiple feedback opportunities for Developers and can capture a variety of PRs so we can understand adoption challenges and improvement opportunities. If helpful, the Survey results may also be combined with Key Performance Indicators (KPIs) that the product provides to further contextualize the survey responses.
1010

11-
The survey responses are stored in your private Azure SQL database to provide insights into how developers are using the tool, the value they report, and the challenges they encounter.
11+
The survey responses are stored in a file called results.csv in a new branch "copilot-survey-engine-results" to provide insights into how developers are using the tool, the value they report, and the challenges they encounter.
1212

1313
We hope that this project provides value to your organization, and we encourage you to contribute and build upon it. Your contributions can help further enhance the survey capabilities and provide even greater insights into the developer experience with Copilot.
1414

@@ -55,6 +55,13 @@ Note: *If the env file does not contain a Language API Key or Endpoint, the anal
5555

5656
- [ replace this line with your answer. ]
5757

58+
6. ***Where did you invest your Copilot Time Savings?***
59+
- [ ] Resolve vulnerabilites
60+
- [ ] Experiment, Learn and Wellness
61+
- [ ] Technical debt and refactorization
62+
- [ ] Work on other items in the backlog
63+
- [ ] Other. Please explain in the comment
64+
5865
### Where does the app store surveys?
5966

6067
As we receive edits on the issue, the App will validate the responses received (options selected) and once all questions have been answered, the issue will be closed automatically and the responses will be saved into a results.csv file in the same repo in which the issue was created.

index.js

Lines changed: 104 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,6 @@ module.exports = (app) => {
6565

6666
async function GetSurveyData(context) {
6767
let issue_body = context.payload.issue.body;
68-
let pctSelected = false;
69-
let pctValue = new Array();
70-
let freqSelected = false;
71-
let freqValue = new Array();
7268

7369
// save comment body if present
7470
let comment = null;
@@ -79,56 +75,49 @@ module.exports = (app) => {
7975
// find regex [0-9]\+ in issue_body and get first result
8076
let pr_number = issue_body.match(/[0-9]+/)[0];
8177

82-
// find regex \[x\] in issue_body and get complete line in an array
83-
let checkboxes = issue_body.match(/\[x\].*/g);
84-
85-
// find if checkboxes array contains Sim o Si or Yes
86-
let isCopilotUsed = checkboxes.some((checkbox) => {
78+
// Get answers to first question and find if they contain affirmative answer
79+
let firstQuestionResponse = await getQuestionResponse(1, 2, issue_body);
80+
let isCopilotUsed = firstQuestionResponse.some((response) => {
8781
return (
88-
checkbox.includes("Sim") ||
89-
checkbox.includes("Si") ||
90-
checkbox.includes("Yes") ||
91-
checkbox.includes("Oui")
82+
response.includes("Sim") ||
83+
response.includes("Si") ||
84+
response.includes("Yes") ||
85+
response.includes("Oui")
9286
);
9387
});
9488

95-
if (isCopilotUsed) {
96-
// loop through checkboxes and find the one that contains %
97-
98-
for (const checkbox of checkboxes) {
99-
if (checkbox.includes("%")) {
100-
pctSelected = true;
101-
copilotPercentage = checkbox;
102-
copilotPercentage = copilotPercentage.replace(/\[x\] /g, "");
103-
pctValue.push(copilotPercentage);
104-
app.log.info(copilotPercentage);
105-
}
106-
}
89+
// Get answers to second question and store in pctValue
90+
let pctValue = await getQuestionResponse(2, 3, issue_body);
91+
let freqValue = await getQuestionResponse(4, 5, issue_body);
92+
let savingsInvestedValue = await getQuestionResponse(6, '', issue_body);
10793

108-
// loop through checkboxes and find the ones that do not contain % and are not Yes or No
109-
for (const checkbox of checkboxes) {
110-
if (
111-
!checkbox.includes("%") &&
112-
!checkbox.includes("Sim") &&
113-
!checkbox.includes("Si") &&
114-
!checkbox.includes("Yes") &&
115-
!checkbox.includes("Oui") &&
116-
!checkbox.includes("Não") &&
117-
!checkbox.includes("No") &&
118-
!checkbox.includes("Non") ||
119-
checkbox.includes("Not very much")
120-
) {
121-
freqSelected = true;
122-
frequencyValue = checkbox;
123-
frequencyValue = frequencyValue.replace(/\[x\] /g, "");
124-
freqValue.push(frequencyValue);
125-
app.log.info(frequencyValue);
126-
}
94+
if( isCopilotUsed && pctValue && freqValue && savingsInvestedValue){
95+
// All questions have been answered and we can close the issue
96+
app.log.info("Closing the issue");
97+
try {
98+
await context.octokit.issues.update({
99+
owner: context.payload.repository.owner.login,
100+
repo: context.payload.repository.name,
101+
issue_number: context.payload.issue.number,
102+
state: "closed",
103+
});
104+
} catch (err) {
105+
app.log.error(err);
127106
}
128-
129-
if( pctSelected && freqSelected ){
130-
// close the issue
107+
}
108+
109+
if (
110+
firstQuestionResponse.some((response) => {
111+
return (
112+
response.includes("Não") ||
113+
response.includes("No") ||
114+
response.includes("Non")
115+
);
116+
})
117+
){
118+
if (comment) {
131119
try {
120+
// close the issue
132121
await context.octokit.issues.update({
133122
owner: context.payload.repository.owner.login,
134123
repo: context.payload.repository.name,
@@ -139,33 +128,8 @@ module.exports = (app) => {
139128
app.log.error(err);
140129
}
141130
}
142-
} else {
143-
if (
144-
checkboxes.some((checkbox) => {
145-
return (
146-
checkbox.includes("Não") ||
147-
checkbox.includes("No") ||
148-
checkbox.includes("Non")
149-
);
150-
})
151-
) {
152-
153-
if (comment) {
154-
try {
155-
// close the issue
156-
await context.octokit.issues.update({
157-
owner: context.payload.repository.owner.login,
158-
repo: context.payload.repository.name,
159-
issue_number: context.payload.issue.number,
160-
state: "closed",
161-
});
162-
} catch (err) {
163-
app.log.error(err);
164-
}
165-
}
166-
}
167131
}
168-
132+
169133
let data = {
170134
enterprise_name: context.payload.enterprise ? context.payload.enterprise.name : '',
171135
organization_name: context.payload.organization ? context.payload.organization.login : '',
@@ -175,8 +139,9 @@ module.exports = (app) => {
175139
PR_number: pr_number || '',
176140
assignee_name: context.payload.issue.assignee ? context.payload.issue.assignee.login : '',
177141
is_copilot_used: isCopilotUsed ? 1 : 0,
178-
saving_percentage: pctValue || '',
179-
usage_frequency: freqValue || '',
142+
saving_percentage: pctValue ? pctValue.join(" || ") : '',
143+
frequency: freqValue ? freqValue.join(" || ") : '',
144+
savings_invested: savingsInvestedValue ? savingsInvestedValue.join(" || ") : '',
180145
comment: comment || '',
181146
created_at: context.payload.issue ? context.payload.issue.created_at : '',
182147
completed_at: context.payload.issue ? context.payload.issue.updated_at : ''
@@ -187,13 +152,13 @@ module.exports = (app) => {
187152
}
188153

189154
async function insertIntoFile(context) {
190-
let fileContent = "";
191-
let results = [];
155+
let newContent = "";
192156
let resultString = "";
157+
let results = [];
193158

194159
try {
195160

196-
fileContent = await GetSurveyData(context);
161+
newContent = await GetSurveyData(context);
197162

198163
// Try to get the file
199164
let file = await context.octokit.repos.getContent({
@@ -207,6 +172,7 @@ module.exports = (app) => {
207172
let fileContents = Buffer.from(file.data.content, "base64").toString();
208173
// If the file contents are not empty, parse the CSV
209174
if (fileContents.length > 0) {
175+
app.log.info("Starting to parse the CSV file...");
210176
// create a readable stream
211177
let readableStream = new stream.Readable();
212178
readableStream.push(fileContents);
@@ -232,14 +198,14 @@ module.exports = (app) => {
232198
if(issue_id_index != -1){
233199
// save previous comments
234200
if (results[issue_id_index].comment) {
235-
fileContent.comment = results[issue_id_index].comment + ' || ' + fileContent.comment;
201+
newContent.comment = results[issue_id_index].comment + ' || ' + newContent.comment;
236202
}
237203

238204
// if the issue_id exists, update the row in the array results
239-
results[issue_id_index] = fileContent;
205+
results[issue_id_index] = newContent;
240206
}else{
241207
// if the issue_id does not exist, push the row into the array
242-
results.push(fileContent);
208+
results.push(newContent);
243209
}
244210

245211
resultString = Object.keys(results[0]).join(',') + '\n';
@@ -267,8 +233,8 @@ module.exports = (app) => {
267233
} catch (error) {
268234
// If the file does not exist, create it
269235
if (error.status === 404) {
270-
let completeData = 'enterprise_name,organization_name,repository_name,issue_id,issue_number,PR_number,assignee_name,is_copilot_used,saving_percentage,usage_frequency,comment,created_at,completed_at\n'
271-
+ Object.values(fileContent).join(',');
236+
let completeData = 'enterprise_name,organization_name,repository_name,issue_id,issue_number,PR_number,assignee_name,is_copilot_used,saving_percentage,usage_frequency,savings_invested,comment,created_at,completed_at\n'
237+
+ Object.values(newContent).join(',');
272238
await createBranch(context);
273239
await context.octokit.repos.createOrUpdateFileContents({
274240
owner: context.payload.repository.owner.login,
@@ -282,49 +248,64 @@ module.exports = (app) => {
282248
app.log.error(error);
283249
}
284250
}
285-
286-
async function createBranch(context) {
287-
// Step 1: Get reference to the default branch
288-
let ref;
289-
try {
290-
// Try to get the 'main' branch
291-
const { data: mainRef } = await context.octokit.git.getRef({
292-
owner: context.payload.repository.owner.login,
293-
repo: context.payload.repository.name,
294-
ref: 'heads/main',
295-
});
296-
ref = mainRef;
297-
} catch (error) {
298-
// If 'main' branch does not exist, try to get the 'master' branch
299-
if (error.status === 404) {
300-
const { data: masterRef } = await context.octokit.git.getRef({
301-
owner: context.payload.repository.owner.login,
302-
repo: context.payload.repository.name,
303-
ref: 'heads/master',
304-
});
305-
ref = masterRef;
306-
} else {
307-
app.log.error(error);
308-
}
309-
}
310251

311-
// Step 2: Create a new branch from the default branch
312-
try {
313-
await context.octokit.git.createRef({
314-
owner: context.payload.repository.owner.login,
315-
repo: context.payload.repository.name,
316-
ref: `refs/heads/${BranchName}`,
317-
sha: ref.object.sha,
318-
});
319-
} catch (error) {
320-
if (error.status === 422) {
321-
app.log.info(`Branch ${BranchName} already exists`);
322-
} else {
323-
app.log.error(error);
324-
}
325-
}
252+
}
253+
254+
async function createBranch(context) {
255+
// Step 1: Get reference to the default branch
256+
let RefBranch = null;
257+
try {
258+
// Try to get the 'main' branch
259+
RefBranch = await context.octokit.git.getRef({
260+
owner: context.payload.repository.owner.login,
261+
repo: context.payload.repository.name,
262+
ref: 'heads/main',
263+
});
264+
} catch (error) {
265+
// If 'main' branch does not exist, try to get the 'master' branch
266+
if (error.status === 404) {
267+
RefBranch = await context.octokit.git.getRef({
268+
owner: context.payload.repository.owner.login,
269+
repo: context.payload.repository.name,
270+
ref: 'heads/master',
271+
});
272+
} else {
273+
app.log.error(error);
274+
}
275+
}
276+
277+
// Step 2: Create a new branch from the default branch
278+
try {
279+
await context.octokit.git.createRef({
280+
owner: context.payload.repository.owner.login,
281+
repo: context.payload.repository.name,
282+
ref: `refs/heads/${BranchName}`,
283+
sha: RefBranch.data.object.sha,
284+
});
285+
} catch (error) {
286+
if (error.status === 422) {
287+
app.log.info(`Branch ${BranchName} already exists`);
288+
} else {
289+
app.log.error(error);
326290
}
291+
}
292+
}
327293

294+
async function getQuestionResponse(start, end, issue_body) {
295+
app.log.info("Getting answers for question " + start + " to " + end);
296+
let AnswerSelected = false;
297+
let Answers = new Array();
298+
let Expression = end ? new RegExp(start + "\\. (.*" + end + "\\." + ")?", "s") : new RegExp(start + "\\. (.*" + ")?", "s");
299+
let QuestionOptions = issue_body.match(Expression)[0].match(/\[x\].*/g);
300+
if(QuestionOptions){
301+
AnswerSelected = true;
302+
QuestionOptions.forEach((option) => {
303+
let cleanAnswer = option;
304+
cleanAnswer = cleanAnswer.replace(/\[x\] /g, "");
305+
Answers.push(cleanAnswer);
306+
});
307+
}
308+
return AnswerSelected ? Answers : null;
328309
}
329310

330311
// For more information on building apps:

issue_template/copilot-usage-en.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,11 @@ For Pull Request XXX:
2828

2929
5. ***What other information can you share about Copilot's ability to save you time coding?***
3030

31-
- (Please tell us in a comment)
31+
- (Please tell us in a comment)
32+
33+
6. ***Where did you invest your Copilot Time Savings?***
34+
- [ ] Resolve vulnerabilites
35+
- [ ] Experiment + Learn and Wellness
36+
- [ ] Technical debt and refactorization
37+
- [ ] Work on other items in the backlog
38+
- [ ] Other. Please explain in the comment

issue_template/copilot-usage-es.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,11 @@ Para el Pull Request XXX:
2828

2929
5. ***¿Qué otra información puedes compartir sobre la capacidad de Copilot para ahorrarte tiempo codificando?***
3030

31-
- (Por favor díganos en un comentario)
31+
- (Por favor díganos en un comentario)
32+
33+
6. ***¿En qué has invertido el tiempo ganado con Copilot?***
34+
- [ ] Resolver vulnerabilidades
35+
- [ ] Experimentar + Aprender y Bienestar
36+
- [ ] Reducir deuda técnica y/o refactorización
37+
- [ ] Trabajar en otras tareas en el backlog
38+
- [ ] Otros. Por favor explica en el comentario

issue_template/copilot-usage-fr.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,11 @@ Pour le Pull Request XXX :
2828

2929
5. ***Quelles autres informations pouvez-vous partager sur la capacité de Copilot à vous faire gagner du temps en matière de codage ?***
3030

31-
- (Merci de nous le dire dans un commentaire)
31+
- (Merci de nous le dire dans un commentaire)
32+
33+
6. ***Qu'avez-vous investi dans le temps gagné avec Copilot ?***
34+
- [ ] Résoudre les vulnérabilités
35+
- [ ] Expérience + Apprendre et/ou Bien-être
36+
- [ ] Réduire la dette technique et/ou refactoring
37+
- [ ] Travailler sur d'autres tâches dans le backlog
38+
- [ ] Autres. Veuillez expliquer dans le commentaire

0 commit comments

Comments
 (0)