SaaS CMS has officially launched! Learn more now.

How to extend optimizely's builtin audience criteria?

Vote:
 

Hello everyone,

the customer has the requirement to persist audience(visitorgroup) memberships of users in a cookie. I found out that it's better to just save in a cookie that an audience criterion matches, so not the whole audience. You can easily support that in your custom criteria's ICriterion.IsMatch-logic. But the challenge here is to extend the builtin criteria to (optionally) support persisting the match in a cookie. 

Some of the builtin criteria already support that out of the box like the "Url criteria"-group, so basically all inheriting from UriSessionStartCriterionBase. Otherwise it would not be possible to for example evaluate the "Landing url" on the user's journey(saved in an "EPiStartUrlKey"-cookie). So my question is:

Is it possible to extend the ICriterionModel of the builtin audience criteria so that i can add a checkbox “Persist match“(or a "Cookie name" Textbox) and if checked(or given) the IsMatch result is saved in a cookie(or other IStateStorage) and evaluated first, so even if the criterion would not match anymore it returns true because it's in the cookie? The whole cookie logic is clear, the challenge here is just how to modify the cms behavior of the builtin criteria. 

A workaround i would like to avoid is to replace all builtin- with custom criteria doing the same but can be persisted in a cookie. 

I was playing around with using custom IVisitorGroupCriterionRepository and IVisitorGroupRepository but to no avail yet:

public static IServiceCollection AddCookieBasedVisitorGroups(this IServiceCollection services)
{
    services.Intercept<IVisitorGroupCriterionRepository>((a, b) => new CookieBasedVisitorGroupCriteriaRepository(a, b));
    services.Intercept<IVisitorGroupRepository>((a, b) => new CookieBasedVisitorGroupRepository(a, b));
    return services;
}

I cannot replace the existing criterion model with my new one and i doubt that this is the right approach anyway.

Edit:

I have now an idea how to solve this. I will configure if an audience(not the criterion) is persisted in a cookie in a file which contains these:

  • VisitorGroup-Guid
  • Lifetime of cookie

I will replace these optimizely services with my own, a lot of boilerplate:

public static IServiceCollection AddCookieBasedVisitorGroups(this IServiceCollection services)
{
	services.Intercept<IVisitorGroupRoleRepository>((a, b)
		=>
	{
		IVirtualRoleReplication virtualRoleReplication = a.GetRequiredService<IVirtualRoleReplication>();
		ICriterionEvents criterionEvents = a.GetRequiredService<ICriterionEvents>();
		ISynchronizedObjectInstanceCache cache = a.GetRequiredService<ISynchronizedObjectInstanceCache>();
		IVisitorGroupRoleFactory visitorGroupRoleFactory = a.GetRequiredService<IVisitorGroupRoleFactory>();
		IVisitorGroupStatisticsLogger visitorGroupStatisticsLogger = a.GetRequiredService<IVisitorGroupStatisticsLogger>();
		IAggregatedPersonalizationEvaluator personalizationEvaluator = a.GetRequiredService<IAggregatedPersonalizationEvaluator>();
		ICriterionFactory criterionFactory = a.GetRequiredService<ICriterionFactory>();
		IHttpContextAccessor httpContextAccessor = a.GetRequiredService<IHttpContextAccessor>();
		IVisitorGroupRepository visitorGroupRepository = a.GetRequiredService<IVisitorGroupRepository>();
		return new CookieBasedVisitorGroupRoleRepository(virtualRoleReplication, criterionEvents, cache, visitorGroupRoleFactory, visitorGroupStatisticsLogger, personalizationEvaluator, criterionFactory, httpContextAccessor, visitorGroupRepository);
	});

	services.Intercept<IVisitorGroupRoleFactory>((a, b)
		=>
	{
	  IVisitorGroupRepository visitorGroupRepository = a.GetRequiredService<IVisitorGroupRepository>();
	  IVisitorGroupStatisticsLogger  visitorGroupStatisticsLogger = a.GetRequiredService<IVisitorGroupStatisticsLogger>();
	  ICriterionFactory criterionFactory = a.GetRequiredService<ICriterionFactory>();
	  IAggregatedPersonalizationEvaluator personalizationEvaluator = a.GetRequiredService<IAggregatedPersonalizationEvaluator>();
	  IHttpContextAccessor httpContextAccessor = a.GetRequiredService<IHttpContextAccessor>();
	  return new CookieBasedVisitorGroupRoleFactory(visitorGroupRepository, visitorGroupStatisticsLogger, criterionFactory, personalizationEvaluator, httpContextAccessor);
	});
	return services;
}

CookieBasedVisitorGroupRoleRepository overrides just TryGetRole and inherits from the default VisitorGroupRoleRepository:

public class CookieBasedVisitorGroupRoleRepository(
	IVirtualRoleReplication virtualRoleReplication,
	ICriterionEvents criterionEvents,
	ISynchronizedObjectInstanceCache cache,
	IVisitorGroupRoleFactory roleFactory,
	IVisitorGroupStatisticsLogger visitorGroupStatisticsLogger,
	IAggregatedPersonalizationEvaluator personalizationEvaluator,
	ICriterionFactory criterionFactory,
	IHttpContextAccessor httpContextAccessor,
	IVisitorGroupRepository visitorGroupRepository)
	: VisitorGroupRoleRepository(virtualRoleReplication, criterionEvents, cache, roleFactory)
{
	public override bool TryGetRole(string name, out VisitorGroupRole visitorGroupRole)
	{
		if (base.TryGetRole(name, out visitorGroupRole) && visitorGroupRole is not null)
		{
			visitorGroupRole = GetCookieBasedVisitorGroupRole(visitorGroupRole);
			return true;
		}

		return false;
	}

	private CookieBasedVisitorGroupRole GetCookieBasedVisitorGroupRole(VisitorGroupRole visitorGroupRole)
	{
		VisitorGroup? vg = visitorGroupRepository.Load(visitorGroupRole.ID);
		return new CookieBasedVisitorGroupRole(vg, visitorGroupRepository, visitorGroupStatisticsLogger, criterionFactory, personalizationEvaluator, httpContextAccessor);
	}
}

The CookieBasedVisitorGroupRoleFactory is also necessary to return the same CookieBasedVisitorGroupRole in Create:

public class CookieBasedVisitorGroupRoleFactory(
	IVisitorGroupRepository visitorGroupRepository,
	IVisitorGroupStatisticsLogger visitorGroupStatisticsLogger,
	ICriterionFactory criterionFactory,
	IAggregatedPersonalizationEvaluator personalizationEvaluator,
	IHttpContextAccessor httpContextAccessor)
	: IVisitorGroupRoleFactory
{
	public VisitorGroupRole Create(VisitorGroup visitorGroup)
	{
		return new CookieBasedVisitorGroupRole(visitorGroup, visitorGroupRepository, visitorGroupStatisticsLogger, criterionFactory, personalizationEvaluator, httpContextAccessor);
	}
}

The VisitorGroupRole finally is where all the visitor-group magic of optimizely happens, so i need to replace it with CookieBasedVisitorGroupRole and override IsInVirtualRole:

public class CookieBasedVisitorGroupRole(
	VisitorGroup visitorGroup,
	IVisitorGroupRepository visitorGroupRepository,
	IVisitorGroupStatisticsLogger visitorGroupStatisticsLogger,
	ICriterionFactory criterionFactory,
	IAggregatedPersonalizationEvaluator personalizationEvaluator,
	IHttpContextAccessor httpContextAccessor)
	: VisitorGroupRole(visitorGroup, visitorGroupRepository, visitorGroupStatisticsLogger, criterionFactory, personalizationEvaluator, httpContextAccessor)
{
	public override bool IsInVirtualRole(IPrincipal principal, object context)
	{
		if (IsVisitorGroupContainedInCookie())
		{
			return true;
		}

		bool visitorGroupMatches = base.IsInVirtualRole(principal, context);
		if (visitorGroupMatches)
		{
			PersistInCookie();
		}

		return visitorGroupMatches;
	}

	private bool IsVisitorGroupContainedInCookie()
	{
		VisitorGroup currentVisitorGroup = visitorGroup; // TODO
		return false;
	}

	private void PersistInCookie()
	{
		VisitorGroup currentVisitorGroup = visitorGroup; // TODO
	}
}
#322823
Edited, May 29, 2024 10:43
Vote:
 

May I ask why you want to do this? Criterierias not already using IStateStorage, would not benefit from storing their state.

#322875
May 30, 2024 10:58
Tim Schmelter - May 30, 2024 16:50
I need to support that every visitor group membership of a user can be persisted across the current request. So for example that the URL Parameter matches the criterion and later the user still matches, even if he's on different pages without the URL Parameter.
Vote:
 

I managed to solve this now in a slightly different way: 

So the requirement was to persist an audience match in a cookie. The configuration of the audience and the match-lifetime(how long the cookie persists the match) is configureable in the CMS. So in theory every existing audience is persistable. If it is a custom VisitorGroupRole checks in IsInVirtualRole if it's contained in the cookie and returns true then, otherwise the default implementation is used(so the optimizely logic to evaluate visitor-group matches). If the auzdience matches, the match is persisted in the cookie. For this i needed to implement a custom CookieBasedVisitorGroupRole class which will replace optimizely`s VisitorGroupRole(some boilerplate code above in my question).

Note: the implementation of IsInVirtualRole must be fully thread-safe since only one instance of the class is created and any role checks are made against the same instance.

#324020
Edited, Jun 24, 2024 8:20
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.