Skip to content

Instantly share code, notes, and snippets.

@PaulGruffyddAmaze
Last active May 12, 2019 19:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save PaulGruffyddAmaze/69cbe6ed1020d85b39898e57cc9f6094 to your computer and use it in GitHub Desktop.
Save PaulGruffyddAmaze/69cbe6ed1020d85b39898e57cc9f6094 to your computer and use it in GitHub Desktop.
PoC for getting lists of popular content from Episerver Profile Store
[ScheduledPlugIn(DisplayName = "Index Recent Page Hits", DefaultEnabled = true, IntervalLength = 1, IntervalType = EPiServer.DataAbstraction.ScheduledIntervalType.Hours)]
public class IndexRecentHits : ScheduledJobBase
{
//Settings
private readonly string _apiRootUrl = ConfigurationManager.AppSettings["episerver:profiles.ProfileApiBaseUrl"];
private readonly string _appKey = ConfigurationManager.AppSettings["episerver:profiles.ProfileApiSubscriptionKey"];
private readonly string _eventUrl = "/api/v1.0/trackevents/";
private readonly string _timeWindow = ConfigurationManager.AppSettings["RecentHours"] ?? "24";
private readonly int _resultsPerPage = 1000;
private Dictionary<string, int> _recentHits = new Dictionary<string, int>();
private bool _stopSignaled;
private static DynamicDataStoreFactory _dataStoreFactory;
private static IContentLoader _contentLoader;
public IndexRecentHits(DynamicDataStoreFactory dataStoreFactory, IContentLoader contentLoader)
{
_dataStoreFactory = dataStoreFactory;
_contentLoader = contentLoader;
}
public IndexRecentHits()
{
IsStoppable = true;
}
/// <summary>
/// Called when a user clicks on Stop for a manually started job, or when ASP.NET shuts down.
/// </summary>
public override void Stop()
{
_stopSignaled = true;
}
/// <summary>
/// Called when a scheduled job executes
/// </summary>
/// <returns>A status message to be stored in the database log and visible from admin mode</returns>
public override string Execute()
{
//Call OnStatusChanged to periodically notify progress of job for manually started jobs
OnStatusChanged(String.Format("Beginning processing of recent hits"));
var totalProcessed = 0;
var errorCount = 0;
//Get the recent hit counts
if (!int.TryParse(_timeWindow, out int recentHours))
{
recentHours = 24;
}
var fromDate = DateTime.Now.AddHours(0 - recentHours).ToUniversalTime().ToString("o");
// Set up the request
var request = GetTrackingRequest($"EventType eq epiPageView and EventTime gt {fromDate}", _resultsPerPage);
ProcessEventResults(1, request);
if (_stopSignaled)
{
return "Execution was cancelled by user";
}
var store = _dataStoreFactory.CreateStore(typeof(RecentHit));
store.DeleteAll();
foreach (var hit in _recentHits)
{
if (_stopSignaled)
{
return "Execution was cancelled by user";
}
try
{
var keyParts = hit.Key.Split('_');
var page = _contentLoader.Get<SitePageData>(new Guid(keyParts.FirstOrDefault() ?? Guid.Empty.ToString()));
var recentHit = new RecentHit
{
PageId = page.ContentLink.ID,
PageTypeId = page.ContentTypeID,
Parents = _contentLoader.GetAncestors(page.ContentLink).Select(x => x.ContentLink.ID).ToArray(),
Language = keyParts.LastOrDefault() ?? "en",
Hits = hit.Value
};
store.Save(recentHit);
}
catch (Exception)
{
errorCount++;
}
totalProcessed++;
if (totalProcessed.ToString().EndsWith("0"))
{
OnStatusChanged($"Indexed {totalProcessed} of {_recentHits.Count} with {errorCount} errors");
}
}
return $"Reindexed {totalProcessed} pages with {errorCount} errors";
}
#region Private Methods
/// <summary>
/// Makes a request to ProfileStore and processes results
/// </summary>
private void ProcessEventResults(int page, RestRequest request)
{
OnStatusChanged($"Fetching hits page {page}");
if (_stopSignaled)
{
return;
}
//Handle pagination
request.AddOrUpdateParameter("$skip", (page - 1) * _resultsPerPage);
// Execute the request to get the events matching the filter
var eventResponseObject = GetTrackingResponse(request);
foreach (var result in eventResponseObject.Items)
{
//Add/update the hit count per event
var key = $"{result.Payload.Epi.ContentGuid}_{result.Payload.Epi.Language}";
if (_recentHits.ContainsKey(key))
{
_recentHits[key]++;
}
else
{
_recentHits.Add(key, 1);
}
}
//Repeat until all pages of results have been processed
if (eventResponseObject.Total > _resultsPerPage * page)
{
ProcessEventResults(page + 1, request);
}
}
/// <summary>
/// Builds the ProfileStore request
/// </summary>
private RestRequest GetTrackingRequest(string filter, int resultsPerPage)
{
var req = new RestRequest(_eventUrl, Method.GET);
req.AddHeader("Ocp-Apim-Subscription-Key", _appKey);
req.AddParameter("$top", resultsPerPage);
req.AddParameter("$filter", filter);
return req;
}
/// <summary>
/// Serialises the ProfileStore response into an object
/// </summary>
private TrackingObjectResponse GetTrackingResponse(RestRequest request)
{
var client = new RestClient(_apiRootUrl);
var getEventResponse = client.Execute(request);
return JsonConvert.DeserializeObject<TrackingObjectResponse>(getEventResponse.Content);
}
#endregion
}
public class RecentHit : IDynamicData
{
public Identity Id { get; set; }
public int PageId { get; set; }
public string Language { get; set; }
public int PageTypeId { get; set; }
public int[] Parents { get; set; }
public long Hits { get; set; }
}
public class RecentHitsUtils
{
//Stuff to be injected
private DynamicDataStoreFactory _dataStoreFactory;
private IContentTypeRepository _contentTypeRepository;
private IContentLoader _contentLoader;
#region Constructors
public RecentHitsUtils(ISynchronizedObjectInstanceCache cache, DynamicDataStoreFactory dataStoreFactory, IContentTypeRepository contentTypeRepository, IContentLoader contentLoader)
{
_dataStoreFactory = dataStoreFactory;
_contentTypeRepository = contentTypeRepository;
_contentLoader = contentLoader;
}
#endregion
#region Public Methods
public IEnumerable<T> GetPopularPages<T>(ContentReference ancestor, string language, int numberOfResults) where T : PageData
{
var store = _dataStoreFactory.CreateStore(typeof(RecentHit));
var contentTypeId = _contentTypeRepository.Load<T>().ID;
var hits = store.Items<RecentHit>().Where(x => x.Parents.Contains(ancestor.ID) && x.Language.Equals(language) && x.PageTypeId.Equals(contentTypeId)).OrderByDescending(x => x.Hits).Take(numberOfResults).ToList();
var contentRefs = hits.Select(x => new ContentReference(x.PageId));
return _contentLoader.GetItems(contentRefs, new LoaderOptions() { LanguageLoaderOption.Specific(CultureInfo.GetCultureInfo(language)) }).OfType<T>();
}
public IEnumerable<IContent> GetPopularPages(ContentReference ancestor, string language, int numberOfResults)
{
var store = _dataStoreFactory.CreateStore(typeof(RecentHit));
var hits = store.Items<RecentHit>().Where(x => x.Parents.Contains(ancestor.ID) && x.Language.Equals(language)).OrderByDescending(x => x.Hits).Take(numberOfResults);
var contentRefs = hits.Select(x => new ContentReference(x.PageId));
return _contentLoader.GetItems(contentRefs, new LoaderOptions() { LanguageLoaderOption.Specific(CultureInfo.GetCultureInfo(language)) });
}
#endregion
}
public class TrackingObjectResponse
{
public int Total { get; set; }
public int Count { get; set; }
public List<TrackingItem> Items { get; set; }
}
public class TrackingItem
{
public Payload Payload { get; set; }
}
public class Payload
{
public Epi Epi { get; set; }
}
public class Epi
{
public string ContentGuid { get; set; }
public string Language { get; set; }
}
@model TrendingArticlesViewModel
<h2>@Model.Heading</h2>
<hr />
<ol>
@foreach (var article in Model.TrendingArticles)
{
<li class="listResult">
<h3>
@Html.PageLink(article)
</h3>
<p class="date">@Html.DisplayFor(x => article.StartPublish)</p>
<hr />
</li>
}
</ol>
[ContentType(DisplayName = "Trending Articles Block", GUID = "30d6ef65-d46e-4478-b66c-e54b3385e68d", Description = "")]
public class TrendingArticlesBlock : BlockData
{
[CultureSpecific]
[Display(
Name = "Heading",
Description = "Heading for this block",
GroupName = SystemTabNames.Content,
Order = 10)]
public virtual string Heading { get; set; }
[Display(
Name = "No of items",
Description = "The number of items to show",
GroupName = SystemTabNames.Content,
Order = 20)]
public virtual int ItemCount { get; set; }
[Display(
Name = "Root page for items",
Description = "The ancestor of the content items returned",
GroupName = SystemTabNames.Content,
Order = 30)]
[Required]
public virtual PageReference Root { get; set; }
}
public class TrendingArticlesBlockController : BlockController<TrendingArticlesBlock>
{
private RecentHitsUtils _recentHitsUtils;
public TrendingArticlesBlockController(RecentHitsUtils recentHitsUtils)
{
_recentHitsUtils = recentHitsUtils;
}
public override ActionResult Index(TrendingArticlesBlock currentBlock)
{
var lang = (currentBlock as ILocalizable).Language;
var trendingArticles = _recentHitsUtils.GetPopularPages<ArticlePage>(ContentReference.StartPage, lang.Name, currentBlock.ItemCount);
var viewModel = new TrendingArticlesViewModel
{
Heading = currentBlock.Heading,
TrendingArticles = trendingArticles
};
return PartialView(viewModel);
}
}
public class TrendingArticlesViewModel
{
public string Heading { get; set; }
public IEnumerable<ArticlePage> TrendingArticles { get; set; }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment