@@ -14,6 +14,7 @@ import { WorkspaceService } from '../../../client/common/application/workspace';
1414import { ExperimentService } from '../../../client/common/experiments/service' ;
1515import { Experiments } from '../../../client/common/utils/localize' ;
1616import * as Telemetry from '../../../client/telemetry' ;
17+ import { EventName } from '../../../client/telemetry/constants' ;
1718import { PVSC_EXTENSION_ID_FOR_TESTS } from '../../constants' ;
1819import { MockOutputChannel } from '../../mockClasses' ;
1920import { MockMemento } from '../../mocks/mementos' ;
@@ -56,10 +57,10 @@ suite('Experimentation service', () => {
5657 } as any ) ;
5758 }
5859
59- function configureApplicationEnvironment ( channel : Channel , version : string ) {
60+ function configureApplicationEnvironment ( channel : Channel , version : string , contributes ?: Record < string , unknown > ) {
6061 when ( appEnvironment . extensionChannel ) . thenReturn ( channel ) ;
6162 when ( appEnvironment . extensionName ) . thenReturn ( PVSC_EXTENSION_ID_FOR_TESTS ) ;
62- when ( appEnvironment . packageJson ) . thenReturn ( { version } ) ;
63+ when ( appEnvironment . packageJson ) . thenReturn ( { version, contributes } ) ;
6364 }
6465
6566 suite ( 'Initialization' , ( ) => {
@@ -149,7 +150,7 @@ suite('Experimentation service', () => {
149150 ) ;
150151 const output = `${ Experiments . inGroup ( ) . format ( 'pythonExperiment' ) } \n` ;
151152
152- assert . equal ( outputChannel . output , output ) ;
153+ assert . strictEqual ( outputChannel . output , output ) ;
153154 } ) ;
154155 } ) ;
155156
@@ -223,7 +224,7 @@ suite('Experimentation service', () => {
223224 const result = await experimentService . inExperiment ( experiment ) ;
224225
225226 assert . isTrue ( result ) ;
226- assert . equal ( telemetryEvents . length , 0 ) ;
227+ assert . strictEqual ( telemetryEvents . length , 0 ) ;
227228 } ) ;
228229
229230 test ( 'If the opt-in setting contains `All`, inExperiment should check the value cached by the experiment service' , async ( ) => {
@@ -270,7 +271,7 @@ suite('Experimentation service', () => {
270271 const result = await experimentService . inExperiment ( experiment ) ;
271272
272273 assert . isTrue ( result ) ;
273- assert . equal ( telemetryEvents . length , 0 ) ;
274+ assert . strictEqual ( telemetryEvents . length , 0 ) ;
274275 sinon . assert . calledOnce ( isCachedFlightEnabledStub ) ;
275276 } ) ;
276277
@@ -318,7 +319,7 @@ suite('Experimentation service', () => {
318319 const result = await experimentService . inExperiment ( experiment ) ;
319320
320321 assert . isFalse ( result ) ;
321- assert . equal ( telemetryEvents . length , 0 ) ;
322+ assert . strictEqual ( telemetryEvents . length , 0 ) ;
322323 sinon . assert . notCalled ( isCachedFlightEnabledStub ) ;
323324 } ) ;
324325 } ) ;
@@ -347,7 +348,7 @@ suite('Experimentation service', () => {
347348 ) ;
348349 const result = await experimentService . getExperimentValue ( experiment ) ;
349350
350- assert . equal ( result , 'value' ) ;
351+ assert . strictEqual ( result , 'value' ) ;
351352 sinon . assert . calledOnce ( getTreatmentVariableAsyncStub ) ;
352353 } ) ;
353354
@@ -396,4 +397,196 @@ suite('Experimentation service', () => {
396397 sinon . assert . notCalled ( getTreatmentVariableAsyncStub ) ;
397398 } ) ;
398399 } ) ;
400+
401+ suite ( 'Opt-in/out telemetry' , ( ) => {
402+ let telemetryEvents : { eventName : string ; properties : Record < string , unknown > } [ ] = [ ] ;
403+ let sendTelemetryEventStub : sinon . SinonStub ;
404+
405+ setup ( ( ) => {
406+ sendTelemetryEventStub = sinon
407+ . stub ( Telemetry , 'sendTelemetryEvent' )
408+ . callsFake ( ( eventName : string , _ , properties : Record < string , unknown > ) => {
409+ const telemetry = { eventName, properties } ;
410+ telemetryEvents . push ( telemetry ) ;
411+ } ) ;
412+
413+ configureApplicationEnvironment ( 'stable' , extensionVersion ) ;
414+ } ) ;
415+
416+ teardown ( ( ) => {
417+ telemetryEvents = [ ] ;
418+ } ) ;
419+
420+ test ( 'Telemetry should be sent when activating the ExperimentService instance' , async ( ) => {
421+ configureSettings ( true , [ ] , [ ] ) ;
422+ configureApplicationEnvironment ( 'stable' , extensionVersion , { configuration : { properties : { } } } ) ;
423+
424+ const experimentService = new ExperimentService (
425+ instance ( workspaceService ) ,
426+ instance ( appEnvironment ) ,
427+ globalMemento ,
428+ outputChannel ,
429+ ) ;
430+
431+ await experimentService . activate ( ) ;
432+
433+ assert . strictEqual ( telemetryEvents . length , 1 ) ;
434+ assert . strictEqual ( telemetryEvents [ 0 ] . eventName , EventName . PYTHON_EXPERIMENTS_OPT_IN_OPT_OUT_SETTINGS ) ;
435+ sinon . assert . calledOnce ( sendTelemetryEventStub ) ;
436+ } ) ;
437+
438+ test ( 'The telemetry event properties should only be populated with valid experiment values' , async ( ) => {
439+ const contributes = {
440+ configuration : {
441+ properties : {
442+ 'python.experiments.optInto' : {
443+ items : {
444+ enum : [ 'foo' , 'bar' ] ,
445+ } ,
446+ } ,
447+ 'python.experiments.optOutFrom' : {
448+ items : {
449+ enum : [ 'foo' , 'bar' ] ,
450+ } ,
451+ } ,
452+ } ,
453+ } ,
454+ } ;
455+ configureSettings ( true , [ 'foo' , 'baz' ] , [ 'bar' , 'invalid' ] ) ;
456+ configureApplicationEnvironment ( 'stable' , extensionVersion , contributes ) ;
457+
458+ const experimentService = new ExperimentService (
459+ instance ( workspaceService ) ,
460+ instance ( appEnvironment ) ,
461+ globalMemento ,
462+ outputChannel ,
463+ ) ;
464+
465+ await experimentService . activate ( ) ;
466+
467+ const { properties } = telemetryEvents [ 0 ] ;
468+ assert . deepStrictEqual ( properties , { optedInto : [ 'foo' ] , optedOutFrom : [ 'bar' ] } ) ;
469+ } ) ;
470+
471+ test ( 'Set telemetry properties to empty arrays if no experiments have been opted into or out from' , async ( ) => {
472+ const contributes = {
473+ configuration : {
474+ properties : {
475+ 'python.experiments.optInto' : {
476+ items : {
477+ enum : [ 'foo' , 'bar' ] ,
478+ } ,
479+ } ,
480+ 'python.experiments.optOutFrom' : {
481+ items : {
482+ enum : [ 'foo' , 'bar' ] ,
483+ } ,
484+ } ,
485+ } ,
486+ } ,
487+ } ;
488+ configureSettings ( true , [ ] , [ ] ) ;
489+ configureApplicationEnvironment ( 'stable' , extensionVersion , contributes ) ;
490+
491+ const experimentService = new ExperimentService (
492+ instance ( workspaceService ) ,
493+ instance ( appEnvironment ) ,
494+ globalMemento ,
495+ outputChannel ,
496+ ) ;
497+
498+ await experimentService . activate ( ) ;
499+
500+ const { properties } = telemetryEvents [ 0 ] ;
501+ assert . deepStrictEqual ( properties , { optedInto : [ ] , optedOutFrom : [ ] } ) ;
502+ } ) ;
503+
504+ test ( 'If the entered value for a setting contains "All", do not expand it to be a list of all experiments, and pass it as-is' , async ( ) => {
505+ const contributes = {
506+ configuration : {
507+ properties : {
508+ 'python.experiments.optInto' : {
509+ items : {
510+ enum : [ 'foo' , 'bar' , 'All' ] ,
511+ } ,
512+ } ,
513+ 'python.experiments.optOutFrom' : {
514+ items : {
515+ enum : [ 'foo' , 'bar' , 'All' ] ,
516+ } ,
517+ } ,
518+ } ,
519+ } ,
520+ } ;
521+ configureSettings ( true , [ 'All' ] , [ 'All' ] ) ;
522+ configureApplicationEnvironment ( 'stable' , extensionVersion , contributes ) ;
523+
524+ const experimentService = new ExperimentService (
525+ instance ( workspaceService ) ,
526+ instance ( appEnvironment ) ,
527+ globalMemento ,
528+ outputChannel ,
529+ ) ;
530+
531+ await experimentService . activate ( ) ;
532+
533+ const { properties } = telemetryEvents [ 0 ] ;
534+ assert . deepStrictEqual ( properties , { optedInto : [ 'All' ] , optedOutFrom : [ 'All' ] } ) ;
535+ } ) ;
536+
537+ // This is an unlikely scenario.
538+ test ( 'If a setting is not in package.json, set the corresponding telemetry property to an empty array' , async ( ) => {
539+ const contributes = {
540+ configuration : {
541+ properties : { } ,
542+ } ,
543+ } ;
544+ configureSettings ( true , [ 'something' ] , [ 'another' ] ) ;
545+ configureApplicationEnvironment ( 'stable' , extensionVersion , contributes ) ;
546+
547+ const experimentService = new ExperimentService (
548+ instance ( workspaceService ) ,
549+ instance ( appEnvironment ) ,
550+ globalMemento ,
551+ outputChannel ,
552+ ) ;
553+
554+ await experimentService . activate ( ) ;
555+
556+ const { properties } = telemetryEvents [ 0 ] ;
557+ assert . deepStrictEqual ( properties , { optedInto : [ ] , optedOutFrom : [ ] } ) ;
558+ } ) ;
559+
560+ // This is also an unlikely scenario.
561+ test ( 'If a setting does not have an enum of valid values, set the corresponding telemetry property to an empty array' , async ( ) => {
562+ const contributes = {
563+ configuration : {
564+ properties : {
565+ 'python.experiments.optInto' : {
566+ items : { } ,
567+ } ,
568+ 'python.experiments.optOutFrom' : {
569+ items : {
570+ enum : [ 'foo' , 'bar' , 'All' ] ,
571+ } ,
572+ } ,
573+ } ,
574+ } ,
575+ } ;
576+ configureSettings ( true , [ 'something' ] , [ ] ) ;
577+ configureApplicationEnvironment ( 'stable' , extensionVersion , contributes ) ;
578+
579+ const experimentService = new ExperimentService (
580+ instance ( workspaceService ) ,
581+ instance ( appEnvironment ) ,
582+ globalMemento ,
583+ outputChannel ,
584+ ) ;
585+
586+ await experimentService . activate ( ) ;
587+
588+ const { properties } = telemetryEvents [ 0 ] ;
589+ assert . deepStrictEqual ( properties , { optedInto : [ ] , optedOutFrom : [ ] } ) ;
590+ } ) ;
591+ } ) ;
399592} ) ;
0 commit comments