Skip to content

Commit 04a844f

Browse files
committed
Add initial infrastructure for search side-by-side page (#7175)
Progress on #7152
1 parent ebda79a commit 04a844f

14 files changed

Lines changed: 429 additions & 6 deletions

File tree

src/NuGetGallery/App_Data/Files/Content/flags.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
"SiteAdmins": true,
1616
"Accounts": [],
1717
"Domains": []
18+
},
19+
"NuGetGallery.SearchSideBySide": {
20+
"All": true,
21+
"SiteAdmins": false,
22+
"Accounts": [],
23+
"Domains": []
1824
}
1925
}
2026
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Net.Mail;
6+
using System.Text;
7+
using System.Text.RegularExpressions;
8+
using System.Web;
9+
using NuGet.Services.Messaging.Email;
10+
11+
namespace NuGetGallery.Infrastructure.Mail.Messages
12+
{
13+
public class SearchSideBySideMessage : MarkdownEmailBuilder
14+
{
15+
private readonly IMessageServiceConfiguration _configuration;
16+
private readonly SearchSideBySideViewModel _model;
17+
private readonly string _searchUrl;
18+
19+
public SearchSideBySideMessage(
20+
IMessageServiceConfiguration configuration,
21+
SearchSideBySideViewModel model,
22+
string searchUrl)
23+
{
24+
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
25+
_model = model ?? throw new ArgumentNullException(nameof(model));
26+
_searchUrl = searchUrl ?? throw new ArgumentNullException(nameof(searchUrl));
27+
}
28+
29+
public override MailAddress Sender => _configuration.GalleryNoReplyAddress;
30+
31+
public override IEmailRecipients GetRecipients()
32+
{
33+
MailAddress[] replyTo = null;
34+
if (!string.IsNullOrWhiteSpace(_model.EmailAddress)
35+
&& Regex.IsMatch(
36+
_model.EmailAddress.Trim(),
37+
GalleryConstants.EmailValidationRegex,
38+
RegexOptions.None,
39+
GalleryConstants.EmailValidationRegexTimeout))
40+
{
41+
replyTo = new[] { new MailAddress(_model.EmailAddress.Trim()) };
42+
}
43+
44+
return new EmailRecipients(
45+
new[] { _configuration.GalleryOwner },
46+
cc: null,
47+
bcc: null,
48+
replyTo: replyTo);
49+
}
50+
51+
public override string GetSubject() => $"[{_configuration.GalleryOwner.DisplayName}] Search Feedback";
52+
53+
protected override string GetMarkdownBody()
54+
{
55+
var sb = new StringBuilder();
56+
57+
sb.AppendLine("The following feedback has come from the search side-by-side page.");
58+
sb.AppendLine();
59+
60+
var encodedSearchTerm = HttpUtility.HtmlEncode(_model.SearchTerm.Trim());
61+
sb.AppendFormat("**Search Query:** [{0}]({1})", encodedSearchTerm, _searchUrl);
62+
sb.AppendLine();
63+
sb.AppendLine();
64+
65+
Append(sb, "Old Hits:", _model.OldHits);
66+
Append(sb, "New Hits:", _model.NewHits);
67+
68+
Append(sb, SearchSideBySideViewModel.BetterSideLabel, _model.BetterSide);
69+
Append(sb, SearchSideBySideViewModel.MostRelevantPackageLabel, _model.MostRelevantPackage);
70+
Append(sb, SearchSideBySideViewModel.ExpectedPackagesLabel, _model.ExpectedPackages);
71+
Append(sb, SearchSideBySideViewModel.CommentsLabel, _model.Comments, extraLine: true);
72+
Append(sb, "Email:", _model.EmailAddress);
73+
74+
return sb.ToString();
75+
}
76+
77+
private void Append(
78+
StringBuilder sb,
79+
string label,
80+
object value,
81+
bool extraLine = false)
82+
{
83+
Append(sb, label, value?.ToString(), extraLine);
84+
}
85+
86+
private void Append(
87+
StringBuilder sb,
88+
string label,
89+
string value,
90+
bool extraLine = false)
91+
{
92+
if (!string.IsNullOrWhiteSpace(value))
93+
{
94+
sb.AppendFormat("**{0}**", label);
95+
96+
if (extraLine)
97+
{
98+
sb.AppendLine();
99+
}
100+
else
101+
{
102+
sb.Append(" ");
103+
}
104+
105+
sb.AppendLine(HttpUtility.HtmlEncode(value.Trim()));
106+
sb.AppendLine();
107+
}
108+
}
109+
}
110+
}

src/NuGetGallery/NuGetGallery.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@
236236
<Compile Include="Infrastructure\Lucene\Models\SearchResults.cs" />
237237
<Compile Include="Infrastructure\Lucene\Models\SortOrder.cs" />
238238
<Compile Include="Infrastructure\Lucene\ServiceResponse.cs" />
239+
<Compile Include="Infrastructure\Mail\Messages\SearchSideBySideMessage.cs" />
239240
<Compile Include="Migrations\201903020136235_CvesCanBeEmpty.cs" />
240241
<Compile Include="Migrations\201903020136235_CvesCanBeEmpty.Designer.cs">
241242
<DependentUpon>201903020136235_CvesCanBeEmpty.cs</DependentUpon>
@@ -767,6 +768,7 @@
767768
<Compile Include="ViewModels\HandleOrganizationMembershipRequestModel.cs" />
768769
<Compile Include="ViewModels\ListPackageItemRequiredSignerViewModel.cs" />
769770
<Compile Include="ViewModels\ManagePackageViewModel.cs" />
771+
<Compile Include="ViewModels\SearchSideBySideViewModel.cs" />
770772
<Compile Include="ViewModels\SignerViewModel.cs" />
771773
<Compile Include="WebRole.cs" />
772774
<Compile Include="Areas\Admin\AdminAreaRegistration.cs" />

src/NuGetGallery/Services/FeatureFlagService.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public class FeatureFlagService : IFeatureFlagService
1616
private const string TyposquattingFeatureName = GalleryPrefix + "Typosquatting";
1717
private const string TyposquattingFlightName = GalleryPrefix + "TyposquattingFlight";
1818
private const string EmbeddedIconFlightName = GalleryPrefix + "EmbeddedIcons";
19+
private const string SearchSideBySideFlightName = GalleryPrefix + "SearchSideBySide";
1920

2021
private const string PackagesAtomFeedFeatureName = GalleryPrefix + "PackagesAtomFeed";
2122

@@ -63,5 +64,10 @@ public bool IsSearchCircuitBreakerEnabled()
6364
{
6465
return _client.IsEnabled(SearchCircuitBreakerFeatureName, defaultValue: false);
6566
}
67+
68+
public bool IsSearchSideBySideEnabled(User user)
69+
{
70+
return _client.IsEnabled(SearchSideBySideFlightName, user, defaultValue: false);
71+
}
6672
}
6773
}

src/NuGetGallery/Services/IFeatureFlagService.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,10 @@ public interface IFeatureFlagService
4747
/// Whether the user is allowed to publish packages with an embedded icon.
4848
/// </summary>
4949
bool AreEmbeddedIconsEnabled(User user);
50+
51+
/// <summary>
52+
/// Whether the user is able to access the search side-by-side experiment.
53+
/// </summary>
54+
bool IsSearchSideBySideEnabled(User user);
5055
}
5156
}

src/NuGetGallery/Services/ITelemetryService.cs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Net;
76
using System.Net.Http;
87
using System.Security.Principal;
98
using NuGet.Services.Entities;
@@ -293,5 +292,41 @@ void TrackMetricForTyposquattingCheckResultAndTotalTime(
293292
/// <param name="uri">The request uri.</param>
294293
/// <param name="circuitBreakerStatus">The CircuitBreakerStatus at the time of the Retry action.</param>
295294
void TrackMetricForSearchOnRetry(string searchName, Exception exception, string correlationId, string uri, string circuitBreakerStatus);
295+
296+
/// <summary>
297+
/// Tracks that some feedback was left on the search side-by-side page.
298+
/// </summary>
299+
/// <param name="searchTerm">The search term used.</param>
300+
/// <param name="oldHits">The total count of old results.</param>
301+
/// <param name="newHits">The total count of new results.</param>
302+
/// <param name="betterSide">Which side was better.</param>
303+
/// <param name="mostRelevantPackage">The most relevant package.</param>
304+
/// <param name="expectedPackages">The expected packages.</param>
305+
/// <param name="hasComments">Whether or not comments were provided.</param>
306+
/// <param name="hasEmailAddress">Whether or not an email address was provided.</param>
307+
void TrackSearchSideBySideFeedback(
308+
string searchTerm,
309+
int oldHits,
310+
int newHits,
311+
string betterSide,
312+
string mostRelevantPackage,
313+
string expectedPackages,
314+
bool hasComments,
315+
bool hasEmailAddress);
316+
317+
/// <summary>
318+
/// Track a search request made on the search side-by-side page.
319+
/// </summary>
320+
/// <param name="searchTerm">The search term used.</param>
321+
/// <param name="oldSuccess">Whether or not the old query succeeded.</param>
322+
/// <param name="oldHits">The total count of old results.</param>
323+
/// <param name="oldSuccess">Whether or not the old query succeeded.</param>
324+
/// <param name="newHits">The total count of new results.</param>
325+
void TrackSearchSideBySide(
326+
string searchTerm,
327+
bool oldSuccess,
328+
int oldHits,
329+
bool newSuccess,
330+
int newHits);
296331
}
297332
}

src/NuGetGallery/Services/SearchResults.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public SearchResults(int hits, DateTime? indexTimestampUtc, IQueryable<Package>
4040

4141
public static bool IsSuccessful(SearchResults searchResults)
4242
{
43-
return searchResults.ResponseMessage?.IsSuccessStatusCode ?? true ;
43+
return searchResults.ResponseMessage?.IsSuccessStatusCode ?? true;
4444
}
4545
}
4646
}

src/NuGetGallery/Services/TelemetryService.cs

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7-
using System.Net;
87
using System.Net.Http;
98
using System.Security.Principal;
109
using System.Web;
@@ -75,6 +74,8 @@ internal class Events
7574
public const string SearchCircuitBreakerOnBreak = "SearchCircuitBreakerOnBreak";
7675
public const string SearchCircuitBreakerOnReset = "SearchCircuitBreakerOnReset";
7776
public const string SearchOnRetry = "SearchOnRetry";
77+
public const string SearchSideBySideFeedback = "SearchSideBySideFeedback";
78+
public const string SearchSideBySide = "SearchSideBySide";
7879
}
7980

8081
private IDiagnosticsSource _diagnosticsSource;
@@ -176,6 +177,18 @@ internal class Events
176177
public const string SearchPollyCorrelationId = "SearchPollyCorrelationId";
177178
public const string SearchCircuitBreakerStatus = "SearchCircuitBreakerStatus";
178179

180+
// Search side-by-side properties
181+
public const string SearchTerm = "SearchTerm";
182+
public const string OldHits = "OldHits";
183+
public const string OldSuccess = "OldSuccess";
184+
public const string NewHits = "NewHits";
185+
public const string NewSuccess = "NewSuccess";
186+
public const string BetterSide = "BetterSide";
187+
public const string MostRelevantPackage = "MostRelevantPackage";
188+
public const string ExpectedPackages = "ExpectedPackages";
189+
public const string HasComments = "HasComments";
190+
public const string HasEmailAddress = "HasEmailAddress";
191+
179192
public TelemetryService(IDiagnosticsService diagnosticsService, ITelemetryClient telemetryClient = null)
180193
{
181194
if (diagnosticsService == null)
@@ -835,6 +848,45 @@ public void TrackMetricForSearchOnRetry(string searchName, Exception exception,
835848
properties.Add(SearchCircuitBreakerStatus, circuitBreakerStatus);
836849
});
837850
}
851+
852+
public void TrackSearchSideBySideFeedback(
853+
string searchTerm,
854+
int oldHits,
855+
int newHits,
856+
string betterSide,
857+
string mostRelevantPackage,
858+
string expectedPackages,
859+
bool hasComments,
860+
bool hasEmailAddress)
861+
{
862+
TrackMetric(Events.SearchSideBySideFeedback, 1, properties => {
863+
properties.Add(SearchTerm, searchTerm);
864+
properties.Add(OldHits, oldHits.ToString());
865+
properties.Add(NewHits, newHits.ToString());
866+
properties.Add(BetterSide, betterSide);
867+
properties.Add(MostRelevantPackage, mostRelevantPackage);
868+
properties.Add(ExpectedPackages, expectedPackages);
869+
properties.Add(HasComments, hasComments.ToString());
870+
properties.Add(HasEmailAddress, hasEmailAddress.ToString());
871+
});
872+
}
873+
874+
public void TrackSearchSideBySide(
875+
string searchTerm,
876+
bool oldSuccess,
877+
int oldHits,
878+
bool newSuccess,
879+
int newHits)
880+
{
881+
TrackMetric(Events.SearchSideBySide, 1, properties => {
882+
properties.Add(SearchTerm, searchTerm);
883+
properties.Add(OldSuccess, oldSuccess.ToString());
884+
properties.Add(OldHits, oldHits.ToString());
885+
properties.Add(NewSuccess, newSuccess.ToString());
886+
properties.Add(NewHits, newHits.ToString());
887+
});
888+
}
889+
838890
/// <summary>
839891
/// We use <see cref="ITelemetryClient.TrackMetric(string, double, IDictionary{string, string})"/> instead of
840892
/// <see cref="ITelemetryClient.TrackEvent(string, IDictionary{string, string}, IDictionary{string, double})"/>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
using System.ComponentModel.DataAnnotations;
6+
7+
namespace NuGetGallery
8+
{
9+
public class SearchSideBySideViewModel
10+
{
11+
public const string BetterSideLabel = "Which results are better?";
12+
public const string MostRelevantPackageLabel = "What was the most relevant package?";
13+
public const string ExpectedPackagesLabel = "Name at least one package you were expecting to see.";
14+
public const string CommentsLabel = "Comments:";
15+
public const string EmailLabel = "Email (optional, provide only if you want us to be able to follow up):";
16+
17+
public string SearchTerm { get; set; } = string.Empty;
18+
19+
public bool OldSuccess { get; set; }
20+
public int OldHits { get; set; }
21+
public IReadOnlyList<ListPackageItemViewModel> OldItems { get; set; } = new List<ListPackageItemViewModel>();
22+
23+
public bool NewSuccess { get; set; }
24+
public int NewHits { get; set; }
25+
public IReadOnlyList<ListPackageItemViewModel> NewItems { get; set; } = new List<ListPackageItemViewModel>();
26+
27+
[Display(Name = BetterSideLabel)]
28+
public string BetterSide { get; set; }
29+
30+
[Display(Name = MostRelevantPackageLabel)]
31+
public string MostRelevantPackage { get; set; }
32+
33+
[Display(Name = ExpectedPackagesLabel)]
34+
public string ExpectedPackages { get; set; }
35+
36+
[Display(Name = CommentsLabel)]
37+
public string Comments { get; set; }
38+
39+
[Display(Name = EmailLabel)]
40+
public string EmailAddress { get; set; }
41+
}
42+
}

src/NuGetGallery/Web.config

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@
3333
<add key="Gallery.AzureStorage.Packages.ConnectionString" value=""/>
3434
<add key="Gallery.AzureStorage.Statistics.ConnectionString" value=""/>
3535
<add key="Gallery.AzureStorage.Uploads.ConnectionString" value=""/>
36-
<add key="Gallery.AzureStorage.Revalidation.ConnectionString" value="" />
36+
<add key="Gallery.AzureStorage.Revalidation.ConnectionString" value=""/>
3737
<!-- The various Azure Storage connection strings to use. If the Gallery.StorageType is AzureStorage, all must be defined. -->
3838
<add key="Gallery.AzureStorageReadAccessGeoRedundant" value="false"/>
3939
<!-- If the storage account has to fall-back to a secondary (when Read Access Geo Redundant is enabled in Azure storage), change Gallery.AzureStorageReadAccessGeoRedundant to true -->
40-
<add key="Gallery.FeatureFlagsRefreshInterval" value="00:01:00" />
40+
<add key="Gallery.FeatureFlagsRefreshInterval" value="00:01:00"/>
4141
<add key="Gallery.AdminPanelDatabaseAccessEnabled" value="false"/>
4242
<add key="Gallery.AsynchronousPackageValidationEnabled" value="false"/>
4343
<add key="Gallery.BlockingAsynchronousPackageValidationEnabled" value="false"/>
@@ -130,6 +130,8 @@
130130
<!-- Set this values to use a remote search service. This overrides ALL other Lucene-related settings and disables indexing jobs. -->
131131
<add key="Gallery.SearchServiceUriPrimary" value=""/>
132132
<add key="Gallery.SearchServiceUriSecondary" value=""/>
133+
<add key="Gallery.PreviewSearchServiceUriPrimary" value=""/>
134+
<add key="Gallery.PreviewSearchServiceUriSecondary" value=""/>
133135
<add key="Gallery.SearchCircuitBreakerDelayInSeconds" value="600"/>
134136
<add key="Gallery.SearchCircuitBreakerWaitAndRetryIntervalInMilliseconds" value="500"/>
135137
<add key="Gallery.SearchCircuitBreakerWaitAndRetryCount" value="3"/>

0 commit comments

Comments
 (0)