Skip to content

Commit 28bb680

Browse files
committed
Add a 'More Information' option in telemetry popup
This commit moves the URL in the text of the telemetry pop up to a separate button. Clicking on the button will automatically open the link in the browser. Also, adds unit tests for the open dialog helper functions.
1 parent 4e94f70 commit 28bb680

5 files changed

Lines changed: 126 additions & 10 deletions

File tree

extensions/ql-vscode/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## [UNRELEASED]
44

5+
- Add a _More Information_ button in the telemetry popup. [#742](https://github.com/github/vscode-codeql/pull/742)
6+
57
## 1.4.1 - 29 January 2021
68

79
- Reword the telemetry modal dialog box. [#738](https://github.com/github/vscode-codeql/pull/738)

extensions/ql-vscode/src/helpers.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import * as yaml from 'js-yaml';
44
import * as path from 'path';
55
import {
66
ExtensionContext,
7+
Uri,
78
window as Window,
8-
workspace
9+
workspace,
10+
env
911
} from 'vscode';
1012
import { CodeQLCliServer } from './cli';
1113
import { logger } from './logging';
@@ -100,6 +102,38 @@ export async function showBinaryChoiceDialog(message: string, modal = true): Pro
100102
return chosenItem?.title === yesItem.title;
101103
}
102104

105+
/**
106+
* Opens a modal dialog for the user to make a yes/no choice.
107+
*
108+
* @param message The message to show.
109+
* @param modal If true (the default), show a modal dialog box, otherwise dialog is non-modal and can
110+
* be closed even if the user does not make a choice.
111+
*
112+
* @return
113+
* `true` if the user clicks 'Yes',
114+
* `false` if the user clicks 'No' or cancels the dialog,
115+
* `undefined` if the dialog is closed without the user making a choice.
116+
*/
117+
export async function showBinaryChoiceWithUrlDialog(message: string, url: string, modal = true): Promise<boolean | undefined> {
118+
const urlItem = { title: 'More Information', isCloseAffordance: false };
119+
const yesItem = { title: 'Yes', isCloseAffordance: false };
120+
const noItem = { title: 'No', isCloseAffordance: true };
121+
let chosenItem;
122+
123+
// Keep the dialog open as long as the user is clicking the 'more information' option.
124+
do {
125+
chosenItem = await Window.showInformationMessage(message, { modal }, urlItem, yesItem, noItem);
126+
if (chosenItem === urlItem) {
127+
await env.openExternal(Uri.parse(url, true));
128+
}
129+
} while (chosenItem === urlItem);
130+
131+
if (!chosenItem) {
132+
return undefined;
133+
}
134+
return chosenItem?.title === yesItem.title;
135+
}
136+
103137
/**
104138
* Show an information message with a customisable action.
105139
* @param message The message to show.

extensions/ql-vscode/src/telemetry.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ConfigListener, CANARY_FEATURES, ENABLE_TELEMETRY, GLOBAL_ENABLE_TELEME
44
import * as appInsights from 'applicationinsights';
55
import { logger } from './logging';
66
import { UserCancellationException } from './commandRunner';
7-
import { showBinaryChoiceDialog } from './helpers';
7+
import { showBinaryChoiceWithUrlDialog } from './helpers';
88

99
// Key is injected at build time through the APP_INSIGHTS_KEY environment variable.
1010
const key = 'REPLACE-APP-INSIGHTS-KEY';
@@ -164,10 +164,11 @@ export class TelemetryListener extends ConfigListener {
164164
let result = undefined;
165165
if (GLOBAL_ENABLE_TELEMETRY.getValue()) {
166166
// Extension won't start until this completes.
167-
result = await showBinaryChoiceDialog(
168-
'Does the CodeQL Extension by GitHub have your permission to collect usage data and metrics to help us improve CodeQL for VSCode?\n\nFor details of what we collect and how we use it, see https://github.com/github/vscode-codeql/blob/main/extensions/ql-vscode/TELEMETRY.md.',
167+
result = await showBinaryChoiceWithUrlDialog(
168+
'Does the CodeQL Extension by GitHub have your permission to collect usage data and metrics to help us improve CodeQL for VSCode?',
169+
'https://github.com/github/vscode-codeql/blob/main/extensions/ql-vscode/TELEMETRY.md',
169170
// We make this dialog modal for now.
170-
// Note that non-modal dialogs allow for markdown in their text, but modal dialogs do not.
171+
// Note that non-modal dialogs allow for markdown in their text, but modal dialogs do not.
171172
// If we do decide to keep this dialog as modal, then this implementation can change and
172173
// we no longer need to call Promise.race. Before committing this PR, we need to make
173174
// this decision.

extensions/ql-vscode/src/vscode-tests/no-workspace/helpers.test.ts

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { expect } from 'chai';
22
import 'mocha';
3-
import { ExtensionContext, Memento } from 'vscode';
3+
import { ExtensionContext, Memento, window } from 'vscode';
44
import * as yaml from 'js-yaml';
55
import * as tmp from 'tmp';
66
import * as path from 'path';
77
import * as fs from 'fs-extra';
88
import * as sinon from 'sinon';
99

10-
import { getInitialQueryContents, InvocationRateLimiter, isLikelyDbLanguageFolder } from '../../helpers';
10+
import { getInitialQueryContents, InvocationRateLimiter, isLikelyDbLanguageFolder, showBinaryChoiceDialog, showBinaryChoiceWithUrlDialog, showInformationMessageWithAction } from '../../helpers';
1111
import { reportStreamProgress } from '../../commandRunner';
12+
import Sinon = require('sinon');
13+
import { fail } from 'assert';
1214

1315
describe('helpers', () => {
1416
let sandbox: sinon.SinonSandbox;
@@ -225,4 +227,81 @@ describe('helpers', () => {
225227
message: 'My prefix (Size unknown)',
226228
});
227229
});
230+
231+
describe('open dialog', () => {
232+
let showInformationMessageSpy: Sinon.SinonStub;
233+
beforeEach(() => {
234+
showInformationMessageSpy = sandbox.stub(window, 'showInformationMessage');
235+
});
236+
237+
it('should show a binary choice dialog and return `yes`', (done) => {
238+
// pretend user clicks on the url twice and then 'yes'
239+
showInformationMessageSpy.onCall(0).resolvesArg(2);
240+
const res = showBinaryChoiceDialog('xxx');
241+
res.then((val) => {
242+
expect(val).to.eq(true);
243+
done();
244+
});
245+
res.catch(e => fail(e));
246+
});
247+
248+
it('should show a binary choice dialog and return `no`', (done) => {
249+
// pretend user clicks on the url twice and then 'yes'
250+
showInformationMessageSpy.onCall(0).resolvesArg(3);
251+
const res = showBinaryChoiceDialog('xxx');
252+
res.then((val) => {
253+
expect(val).to.eq(false);
254+
done();
255+
});
256+
res.catch(e => fail(e));
257+
});
258+
259+
it('should show an info dialog and confirm the action', (done) => {
260+
// pretend user clicks on the url twice and then 'yes'
261+
showInformationMessageSpy.onCall(0).resolvesArg(1);
262+
const res = showInformationMessageWithAction('xxx', 'yyy');
263+
res.then((val) => {
264+
expect(val).to.eq(true);
265+
done();
266+
});
267+
res.catch(e => fail(e));
268+
});
269+
270+
it('should show an info dialog and not confirm the', (done) => {
271+
// pretend user clicks on the url twice and then 'yes'
272+
showInformationMessageSpy.onCall(0).resolves(undefined);
273+
const res = showInformationMessageWithAction('xxx', 'yyy');
274+
res.then((val) => {
275+
expect(val).to.eq(false);
276+
done();
277+
});
278+
res.catch(e => fail(e));
279+
});
280+
281+
it('should show a binary choice dialog with a url and return `yes`', (done) => {
282+
// pretend user clicks on the url twice and then 'yes'
283+
showInformationMessageSpy.onCall(0).resolvesArg(2);
284+
showInformationMessageSpy.onCall(1).resolvesArg(2);
285+
showInformationMessageSpy.onCall(0).resolvesArg(3);
286+
const res = showBinaryChoiceWithUrlDialog('xxx', 'invalid:url');
287+
res.then((val) => {
288+
expect(val).to.eq(true);
289+
done();
290+
});
291+
res.catch(e => fail(e));
292+
});
293+
294+
it('should show a binary choice dialog with a url and return `no`', (done) => {
295+
// pretend user clicks on the url twice and then 'yes'
296+
showInformationMessageSpy.onCall(0).resolvesArg(2);
297+
showInformationMessageSpy.onCall(1).resolvesArg(2);
298+
showInformationMessageSpy.onCall(0).resolvesArg(4);
299+
const res = showBinaryChoiceWithUrlDialog('xxx', 'invalid:url');
300+
res.then((val) => {
301+
expect(val).to.eq(false);
302+
done();
303+
});
304+
res.catch(e => fail(e));
305+
});
306+
});
228307
});

extensions/ql-vscode/src/vscode-tests/no-workspace/telemetry.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const expect = chai.expect;
1515

1616
const sandbox = sinon.createSandbox();
1717

18-
describe('telemetry reporting', function() {
18+
describe.only('telemetry reporting', function() {
1919
// setting preferences can trigger lots of background activity
2020
// so need to bump up the timeout of this test.
2121
this.timeout(10000);
@@ -249,7 +249,7 @@ describe('telemetry reporting', function() {
249249
});
250250

251251
it('should request permission if popup has never been seen before', async () => {
252-
sandbox.stub(window, 'showInformationMessage').resolvesArg(2 /* "yes" item */);
252+
sandbox.stub(window, 'showInformationMessage').resolvesArg(3 /* "yes" item */);
253253
await ctx.globalState.update('telemetry-request-viewed', false);
254254
await enableTelemetry('codeQL.telemetry', false);
255255

@@ -262,7 +262,7 @@ describe('telemetry reporting', function() {
262262
});
263263

264264
it('should prevent telemetry if permission is denied', async () => {
265-
sandbox.stub(window, 'showInformationMessage').resolvesArg(3 /* "no" item */);
265+
sandbox.stub(window, 'showInformationMessage').resolvesArg(4 /* "no" item */);
266266
await ctx.globalState.update('telemetry-request-viewed', false);
267267
await enableTelemetry('codeQL.telemetry', true);
268268

0 commit comments

Comments
 (0)