Skip to content

Commit a79b0a5

Browse files
authored
[Cookies] Expire the AB test cookie in a year (#8275)
* Expire the cookie in a year * Upgrade AB test cookie to V3 * Fix build * Combine V2 and V3
1 parent 82de1df commit a79b0a5

9 files changed

Lines changed: 139 additions & 47 deletions

File tree

src/GitHubVulnerabilities2Db/Gallery/ThrowingTelemetryService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public void TrackABTestEnrollmentInitialized(int schemaVersion, int previewSearc
2626
throw new NotImplementedException();
2727
}
2828

29-
public void TrackABTestEnrollmentUpgraded(int schemaVersion, int previewSearchBucket, int packageDepentsBucket)
29+
public void TrackABTestEnrollmentUpgraded(int oldSchemaVersion, int newSchemaVersion, int previewSearchBucket, int packageDepentsBucket)
3030
{
3131
throw new NotImplementedException();
3232
}

src/NuGetGallery.Services/Telemetry/ITelemetryService.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,11 +378,13 @@ void TrackABTestEnrollmentInitialized(
378378
/// <summary>
379379
/// Track when an A/B test enrollment is upgraded
380380
/// </summary>
381-
/// <param name="schemaVersion">The schema version.</param>
381+
/// <param name="oldSchemaVersion">The old schema version.</param>
382+
/// <param name="newSchemaVersion">The new schema version.</param>
382383
/// <param name="previewSearchBucket">The bucket for the preview search test.</param>
383384
/// <param name="packageDependentBucket">The bucket for the package dependents test</param>
384385
void TrackABTestEnrollmentUpgraded(
385-
int schemaVersion,
386+
int oldSchemaVersion,
387+
int newSchemaVersion,
386388
int previewSearchBucket,
387389
int packageDependentBucket);
388390

src/NuGetGallery.Services/Telemetry/TelemetryService.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,12 +1055,14 @@ public void TrackABTestEnrollmentInitialized(
10551055
}
10561056

10571057
public void TrackABTestEnrollmentUpgraded(
1058+
int oldSchemaVersion,
10581059
int newSchemaVersion,
10591060
int previewSearchBucket,
10601061
int packageDependentBucket)
10611062
{
10621063
TrackMetric(Events.ABTestEnrollmentUpgraded, 1, properties =>
10631064
{
1065+
properties.Add(OldSchemaVersion, oldSchemaVersion.ToString());
10641066
properties.Add(NewSchemaVersion, newSchemaVersion.ToString());
10651067
properties.Add(PreviewSearchBucket, previewSearchBucket.ToString());
10661068
properties.Add(PackageDependentBucket, packageDependentBucket.ToString());

src/NuGetGallery/Infrastructure/ABTestEnrollmentFactory.cs

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public class ABTestEnrollmentFactory : IABTestEnrollmentFactory
1717
{
1818
private const int SchemaVersion1 = 1; // PreviewSearch: {"v":1,"ps":100}
1919
private const int SchemaVersion2 = 2; // PreviewSearch + PackageDependent: {"v":2,"ps":100,"pd":100}
20+
private const int SchemaVersion3 = 3; // PreviewSearch + PackageDependent: {"v":2,"ps":100,"pd":100}, and expired in a year.
2021

2122
// Note that a new schema version could theoretically reuse any currently unused cookie properties. However
2223
// this does have questionable statistical correctness due treatment assignment of one A/B test being reused
@@ -36,7 +37,7 @@ public ABTestEnrollment Initialize()
3637
{
3738
var enrollment = new ABTestEnrollment(
3839
ABTestEnrollmentState.FirstHit,
39-
SchemaVersion2,
40+
SchemaVersion3,
4041
previewSearchBucket: GetRandomWholePercentage(),
4142
packageDependentBucket: GetRandomWholePercentage());
4243

@@ -66,19 +67,19 @@ private static int GetRandomWholePercentage()
6667

6768
public string Serialize(ABTestEnrollment enrollment)
6869
{
69-
if (enrollment.SchemaVersion != SchemaVersion2)
70+
if (enrollment.SchemaVersion != SchemaVersion3)
7071
{
7172
throw new NotImplementedException($"Serializing schema version {enrollment.SchemaVersion} is not implemented.");
7273
}
7374

74-
var deserialized2 = new StateVersion2
75+
var deserialized3 = new StateVersion2OrVersion3
7576
{
76-
SchemaVersion = SchemaVersion2,
77+
SchemaVersion = SchemaVersion3,
7778
PreviewSearchBucket = enrollment.PreviewSearchBucket,
7879
PackageDependentBucket = enrollment.PackageDependentBucket,
7980
};
8081

81-
return JsonConvert.SerializeObject(deserialized2);
82+
return JsonConvert.SerializeObject(deserialized3);
8283
}
8384

8485
public bool TryDeserialize(string serialized, out ABTestEnrollment enrollment)
@@ -89,7 +90,8 @@ public bool TryDeserialize(string serialized, out ABTestEnrollment enrollment)
8990
return false;
9091
}
9192

92-
return TryDeserializeStateVer2(serialized, out enrollment)
93+
return TryDeserializeStateVer3(serialized, out enrollment)
94+
|| TryDeserializeStateVer2(serialized, out enrollment)
9395
|| TryDeserializeStateVer1(serialized, out enrollment);
9496
}
9597

@@ -108,14 +110,15 @@ private bool TryDeserializeStateVer1(string serialized, out ABTestEnrollment enr
108110

109111
enrollment = new ABTestEnrollment(
110112
ABTestEnrollmentState.Upgraded,
111-
SchemaVersion2,
113+
SchemaVersion3,
112114
v1.PreviewSearchBucket,
113115
packageDependentBucket: GetRandomWholePercentage());
114116

115117
_telemetryService.TrackABTestEnrollmentUpgraded(
116-
enrollment.SchemaVersion,
117-
enrollment.PreviewSearchBucket,
118-
enrollment.PackageDependentBucket);
118+
SchemaVersion1,
119+
enrollment.SchemaVersion,
120+
enrollment.PreviewSearchBucket,
121+
enrollment.PackageDependentBucket);
119122

120123
return true;
121124
}
@@ -130,7 +133,7 @@ private bool TryDeserializeStateVer2(string serialized, out ABTestEnrollment enr
130133
enrollment = null;
131134
try
132135
{
133-
var v2 = JsonConvert.DeserializeObject<StateVersion2>(serialized);
136+
var v2 = JsonConvert.DeserializeObject<StateVersion2OrVersion3>(serialized);
134137
if (v2 == null
135138
|| v2.SchemaVersion != SchemaVersion2
136139
|| IsNotPercentage(v2.PreviewSearchBucket)
@@ -140,11 +143,45 @@ private bool TryDeserializeStateVer2(string serialized, out ABTestEnrollment enr
140143
}
141144

142145
enrollment = new ABTestEnrollment(
143-
ABTestEnrollmentState.Active,
144-
v2.SchemaVersion,
146+
ABTestEnrollmentState.Upgraded,
147+
SchemaVersion3,
145148
v2.PreviewSearchBucket,
146149
v2.PackageDependentBucket);
147150

151+
_telemetryService.TrackABTestEnrollmentUpgraded(
152+
SchemaVersion2,
153+
enrollment.SchemaVersion,
154+
enrollment.PreviewSearchBucket,
155+
enrollment.PackageDependentBucket);
156+
157+
return true;
158+
}
159+
catch (JsonException)
160+
{
161+
return false;
162+
}
163+
}
164+
165+
private bool TryDeserializeStateVer3(string serialized, out ABTestEnrollment enrollment)
166+
{
167+
enrollment = null;
168+
try
169+
{
170+
var v3 = JsonConvert.DeserializeObject<StateVersion2OrVersion3>(serialized);
171+
if (v3 == null
172+
|| v3.SchemaVersion != SchemaVersion3
173+
|| IsNotPercentage(v3.PreviewSearchBucket)
174+
|| IsNotPercentage(v3.PackageDependentBucket))
175+
{
176+
return false;
177+
}
178+
179+
enrollment = new ABTestEnrollment(
180+
ABTestEnrollmentState.Active,
181+
v3.SchemaVersion,
182+
v3.PreviewSearchBucket,
183+
v3.PackageDependentBucket);
184+
148185
return true;
149186
}
150187
catch (JsonException)
@@ -167,7 +204,7 @@ private class StateVersion1
167204
public int PreviewSearchBucket { get; set; }
168205
}
169206

170-
private class StateVersion2
207+
private class StateVersion2OrVersion3
171208
{
172209
[JsonProperty("v", Required = Required.Always)]
173210
public int SchemaVersion { get; set; }

src/NuGetGallery/Infrastructure/CookieBasedABTestService.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,16 @@ public class CookieBasedABTestService : IABTestService
2020
private readonly ITelemetryService _telemetryService;
2121
private readonly ILogger<CookieBasedABTestService> _logger;
2222
private readonly Lazy<ABTestEnrollment> _lazyEnrollment;
23+
private readonly IDateTimeProvider _dateTimeProvider;
2324

2425
public CookieBasedABTestService(
2526
HttpContextBase httpContext,
2627
IFeatureFlagService featureFlagService,
2728
IABTestEnrollmentFactory enrollmentFactory,
2829
IContentObjectService contentObjectService,
2930
ITelemetryService telemetryService,
30-
ILogger<CookieBasedABTestService> logger)
31+
ILogger<CookieBasedABTestService> logger,
32+
IDateTimeProvider dateTimeProvider)
3133
{
3234
_httpContext = httpContext ?? throw new ArgumentNullException(nameof(httpContext));
3335
_featureFlagService = featureFlagService ?? throw new ArgumentNullException(nameof(featureFlagService));
@@ -36,6 +38,7 @@ public CookieBasedABTestService(
3638
_telemetryService = telemetryService ?? throw new ArgumentNullException(nameof(telemetryService));
3739
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
3840
_lazyEnrollment = new Lazy<ABTestEnrollment>(DetermineEnrollment);
41+
_dateTimeProvider = dateTimeProvider ?? throw new ArgumentNullException(nameof(dateTimeProvider));
3942
}
4043

4144
public bool IsPreviewSearchEnabled(User user)
@@ -74,7 +77,7 @@ private ABTestEnrollment DetermineEnrollment()
7477
HttpOnly = true,
7578
Secure = true,
7679
Value = _enrollmentFactory.Serialize(enrollment),
77-
Expires = DateTime.MaxValue,
80+
Expires = _dateTimeProvider.UtcNow.AddYears(1),
7881
};
7982
_httpContext.Response.Cookies.Add(responseCookie);
8083
}

src/VerifyMicrosoftPackage/Fakes/FakeTelemetryService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public void TrackABTestEnrollmentInitialized(int schemaVersion, int previewSearc
2323
throw new NotImplementedException();
2424
}
2525

26-
public void TrackABTestEnrollmentUpgraded(int schemaVersion, int previewSearchBucket, int packageDepentsBucket)
26+
public void TrackABTestEnrollmentUpgraded(int oldSchemaVersion, int newSchemaVersion, int previewSearchBucket, int packageDepentsBucket)
2727
{
2828
throw new NotImplementedException();
2929
}

tests/NuGetGallery.Facts/Infrastructure/ABTestEnrollmentFactoryFacts.cs

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
45
using Microsoft.Extensions.Logging;
56
using Moq;
67
using Xunit;
@@ -18,40 +19,45 @@ public void CreatesANewEnrollmentInstance()
1819

1920
Assert.NotNull(enrollment);
2021
Assert.Equal(ABTestEnrollmentState.FirstHit, enrollment.State);
21-
Assert.Equal(2, enrollment.SchemaVersion);
22+
Assert.Equal(3, enrollment.SchemaVersion);
2223
Assert.InRange(enrollment.PreviewSearchBucket, 1, 100);
2324
Assert.InRange(enrollment.PackageDependentBucket, 1, 100);
2425
}
2526
}
2627

2728
public class Serialize : Facts
2829
{
29-
[Fact]
30-
public void ProducesExpectedString()
30+
[Theory]
31+
[InlineData(1)]
32+
[InlineData(2)]
33+
[InlineData(4)]
34+
public void SerializeWithNotImplementedSchemaVersion_ThrowNotImplementedException(int schemaVersion)
3135
{
3236
var enrollment = new ABTestEnrollment(
33-
ABTestEnrollmentState.Active,
34-
schemaVersion: 2,
35-
previewSearchBucket: 42,
36-
packageDependentBucket: 82);
37+
state: It.IsAny<ABTestEnrollmentState>(),
38+
schemaVersion: schemaVersion,
39+
previewSearchBucket: It.IsAny<int>(),
40+
packageDependentBucket: It.IsAny<int>());
3741

38-
var serialized = Target.Serialize(enrollment);
39-
40-
Assert.Equal(@"{""v"":2,""ps"":42,""pd"":82}", serialized);
42+
var exception = Assert.Throws<NotImplementedException>(() => Target.Serialize(enrollment));
43+
Assert.Equal($"Serializing schema version {schemaVersion} is not implemented.", exception.Message);
4144
}
4245

43-
[Fact]
44-
public void ProducesExpectedStringVersionTwo()
46+
[Theory]
47+
[InlineData(3, ABTestEnrollmentState.Active)]
48+
[InlineData(3, ABTestEnrollmentState.FirstHit)]
49+
[InlineData(3, ABTestEnrollmentState.Upgraded)]
50+
public void ProducesExpectedString(int schemaVersion, ABTestEnrollmentState state)
4551
{
4652
var enrollment = new ABTestEnrollment(
47-
ABTestEnrollmentState.Active,
48-
schemaVersion: 2,
53+
state: state,
54+
schemaVersion: schemaVersion,
4955
previewSearchBucket: 42,
50-
packageDependentBucket: 74);
56+
packageDependentBucket: 82);
5157

5258
var serialized = Target.Serialize(enrollment);
5359

54-
Assert.Equal(@"{""v"":2,""ps"":42,""pd"":74}", serialized);
60+
Assert.Equal(@"{""v"":3,""ps"":42,""pd"":82}", serialized);
5561
}
5662
}
5763

@@ -66,22 +72,33 @@ public class Deserialize : Facts
6672
[InlineData("[]")]
6773
[InlineData("null")]
6874
[InlineData(@"{""ps"":42}")]
69-
[InlineData(@"{""v"":2,""ps"":42}")]
75+
[InlineData(@"{""pd"":42}")]
76+
[InlineData(@"{""ps"":20,""pd"":82}")]
7077
[InlineData(@"{""v"":1}")]
7178
[InlineData(@"{""v"":1,""ps"":-1}")]
7279
[InlineData(@"{""v"":1,""ps"":0}")]
7380
[InlineData(@"{""v"":1,""ps"":101}")]
74-
[InlineData(@"{""v"":2,""ps"":10,""pd"":101}")]
7581
[InlineData(@"{""v"":2}")]
76-
[InlineData(@"{""pd"":42}")]
82+
[InlineData(@"{""v"":2,""ps"":42}")]
83+
[InlineData(@"{""v"":2,""pd"":42}")]
84+
[InlineData(@"{""v"":2,""ps"":10,""pd"":101}")]
7785
[InlineData(@"{""v"":2,""ps"":1,""pd"":-1}")]
7886
[InlineData(@"{""v"":2,""ps"":10,""pd"":0}")]
79-
[InlineData(@"{""v"":3,""ps"":42,""pd"":53}")]
8087
[InlineData(@"{""v"":2,""ps"":-1,""pd"":24}")]
8188
[InlineData(@"{""v"":2,""ps"":0,""pd"":35}")]
8289
[InlineData(@"{""v"":2,""ps"":101,""pd"":56}")]
8390
[InlineData(@"{""v"":2,""ps"":200,""pd"":82}")]
84-
[InlineData(@"{""ps"":20,""pd"":82}")]
91+
[InlineData(@"{""v"":3}")]
92+
[InlineData(@"{""v"":3,""ps"":42}")]
93+
[InlineData(@"{""v"":3,""pd"":42}")]
94+
[InlineData(@"{""v"":3,""ps"":10,""pd"":101}")]
95+
[InlineData(@"{""v"":3,""ps"":1,""pd"":-1}")]
96+
[InlineData(@"{""v"":3,""ps"":10,""pd"":0}")]
97+
[InlineData(@"{""v"":3,""ps"":-1,""pd"":24}")]
98+
[InlineData(@"{""v"":3,""ps"":0,""pd"":35}")]
99+
[InlineData(@"{""v"":3,""ps"":101,""pd"":56}")]
100+
[InlineData(@"{""v"":3,""ps"":200,""pd"":82}")]
101+
[InlineData(@"{""v"":4,""ps"":42,""pd"":53}")]
85102
public void RejectsInvalid(string input)
86103
{
87104
var success = Target.TryDeserialize(input, out var enrollment);
@@ -95,14 +112,16 @@ public void RejectsInvalid(string input)
95112
[InlineData(@"{""v"":1,""ps"":42,""zzz"":false}", 42)]
96113
[InlineData(@"{""v"":1,""ps"":1}", 1)]
97114
[InlineData(@"{""v"":1,""ps"":100}", 100)]
98-
public void UpgradesValidVersionV1(string input, int previewSearchBucket)
115+
public void UpgradesValidVersion(string input, int previewSearchBucket)
99116
{
117+
TelemetryService.Setup(t => t.TrackABTestEnrollmentUpgraded(1, 3, It.IsAny<int>(), It.IsAny<int>()));
100118
var success = Target.TryDeserialize(input, out var enrollment);
101119

120+
TelemetryService.Verify(t => t.TrackABTestEnrollmentUpgraded(1, 3, It.IsAny<int>(), It.IsAny<int>()), Times.Once);
102121
Assert.True(success, "The derialization should have succeeded.");
103122
Assert.NotNull(enrollment);
104123
Assert.Equal(ABTestEnrollmentState.Upgraded, enrollment.State);
105-
Assert.Equal(2, enrollment.SchemaVersion);
124+
Assert.Equal(3, enrollment.SchemaVersion);
106125
Assert.Equal(previewSearchBucket, enrollment.PreviewSearchBucket);
107126
Assert.InRange(enrollment.PackageDependentBucket, 1, 100);
108127
}
@@ -116,13 +135,35 @@ public void UpgradesValidVersionV1(string input, int previewSearchBucket)
116135
[InlineData(@"{""v"":2,""ps"":52,""pd"":1}", 52, 1)]
117136
[InlineData(@"{""v"":2,""ps"":68,""pd"":100}", 68, 100)]
118137
public void ParsesValidVersion2(string input, int previewSearchBucket, int packageDependentBucket)
138+
{
139+
TelemetryService.Setup(t => t.TrackABTestEnrollmentUpgraded(2, 3, It.IsAny<int>(), It.IsAny<int>()));
140+
var success = Target.TryDeserialize(input, out var enrollment);
141+
142+
TelemetryService.Verify(t => t.TrackABTestEnrollmentUpgraded(2, 3, It.IsAny<int>(), It.IsAny<int>()), Times.Once);
143+
Assert.True(success, "The derialization should have succeeded.");
144+
Assert.NotNull(enrollment);
145+
Assert.Equal(ABTestEnrollmentState.Upgraded, enrollment.State);
146+
Assert.Equal(3, enrollment.SchemaVersion);
147+
Assert.Equal(previewSearchBucket, enrollment.PreviewSearchBucket);
148+
Assert.Equal(packageDependentBucket, enrollment.PackageDependentBucket);
149+
}
150+
151+
[Theory]
152+
[InlineData(@"{""v"":3,""ps"":42,""pd"":57}", 42, 57)]
153+
[InlineData(@"{""v"":3,""ps"":1,""pd"":1}", 1, 1)]
154+
[InlineData(@"{""v"":3,""ps"":100,""pd"":100}", 100, 100)]
155+
[InlineData(@"{""v"":3,""ps"":1,""pd"":32}", 1, 32)]
156+
[InlineData(@"{""v"":3,""ps"":100,""pd"":57}", 100, 57)]
157+
[InlineData(@"{""v"":3,""ps"":52,""pd"":1}", 52, 1)]
158+
[InlineData(@"{""v"":3,""ps"":68,""pd"":100}", 68, 100)]
159+
public void ParsesValidVersion3(string input, int previewSearchBucket, int packageDependentBucket)
119160
{
120161
var success = Target.TryDeserialize(input, out var enrollment);
121162

122163
Assert.True(success, "The derialization should have succeeded.");
123164
Assert.NotNull(enrollment);
124165
Assert.Equal(ABTestEnrollmentState.Active, enrollment.State);
125-
Assert.Equal(2, enrollment.SchemaVersion);
166+
Assert.Equal(3, enrollment.SchemaVersion);
126167
Assert.Equal(previewSearchBucket, enrollment.PreviewSearchBucket);
127168
Assert.Equal(packageDependentBucket, enrollment.PackageDependentBucket);
128169
}

0 commit comments

Comments
 (0)