Showing posts with label Personalization. Show all posts
Showing posts with label Personalization. Show all posts

December 29, 2020
Estimated Post Reading Time ~

Target Integration with AEM 6.5 - Personalization

I have integrated AEM 6.5 and Target successfully through IMS and legacy Target cloud service. I created an activity on AEM and pushed it to Target. I am able to see the activity in AEM. When I open the page either in publish or in author with wcmmode disabled, I got the following error

Rendering mbox failed target-global-mbox : {status: "error", error: "unknown client code : <client-code>

For this, I changed the property "clientcode" from tenant id to client code in the target configuration. After this I am getting the following error:

"The default offer path was not found in the internal map for mbox target-global-mbox"

And the targeted content is hidden on the page. Even the default content is not shown.

Solution:
This was happening because, for some reason, another offer (which was configured to some other page) was being resolved on this page. Hence nothing was displayed. I deactivated the old offer and then the default content was displayed. I still have a problem with the offer resolution.


By aem4beginner

May 15, 2020
Estimated Post Reading Time ~

Email Personalization & Targeting with Adobe Campaign

You can't be effective in marketing anymore without knowing your customers. With Adobe Campaign you'll get a 360-degree view of their habits and behaviours.

Email personalisation to create ROI
We are now in the Big Data era and companies all over the world are able to gather an incredible amount of data and insights not only about their 'offline' customers, but also about their online visitors, social followers, etc. For most companies, this information is stored in a CRM and similar other databases but it is not always gathered in a comprehensive way. With the Adobe Marketing Cloud, the possibility to link and synchronise all this data is available and comes into action with Adobe Campaign in particular.

Adobe Campaign provides campaign planning as well as offer- and personalisation-management capabilities for marketing automation and the execution of marketing programs across all channels - online (emails, apps...) and offline (SMS...). As such, it is is the best-in-class tool to deliver personalised 1:1 marketing on an enterprise level.

As highlighted in a recent study by Adobe and Econsultancy (open the pdf on adobe.com), most companies are already personalising digital experiences and messaging with personal data such as name, gender and location (65 to 70 per cent of them), less than half are using user preferences, purchase history and online behavioural information. Yet, the latter are described as the top three type of data with high ROI when used to personalise offers. As such, it is time for companies to jump to the next level of personalisation.

Personalisation & targeting in Adobe Campaign

To kick-off email personalisation efforts in the right way, it is important that digital marketers check the quality and completeness of all data which is available and accessible for them. As said above, most of the time customer and prospect data is scattered inside different tools and it is often hard for companies to have a comprehensive view of this data. Marketers need a solution like Adobe Campaign to get a 360-degree view of their customer's personal information and behaviours across all channels. This is only achievable by leveraging data automatically from online and offline channels through connectors and automated imports.

Personalisation Fields
Adobe Campaign allows marketers and web developers to easily incorporate customers and prospects personal information and interests into an email or a offer easily. Once all your customer and visitor data has been synched or imported, it is easy to select the right fields of information in the dropdown menu.

It is also possible to add pre-defined personalised blocks like offers or promotions as it can be seen above. These pre-defined blocks can include conditional offers or just standardised HTML blocks (text, links, images...). Additionally, users are able to add conditional content directly and therefore adapt the text and content of emails based on the actual profile of the recipients.

Targeting and Segmentation
Regarding targeting, Adobe Campaign enables the creation of simple to complex and advanced workflows that can help marketers define multiple segments and offers as well as automating their entire marketing campaigns from A to Z. Segments can be pre-defined using filters or by using queries in workflows. Once workflows are ready to be launched, a preview of the estimated target by segment branch will be shown. Workflows provide the possibility to target offers based on personal data but also behavioural and purchasing data from external sources in a comprehensive and visual way.



Multi-staged workflows will show the campaign proceeding over time. Based on direct customer interaction within a campaign or even taking into account what happened offline during a campaign life-cycle, following campaign deliveries can be automated. Data import tasks can be managed easily by adding respective workflow elements.

Connecting AEM & Adobe Campaign for personalisation
As part of the Adobe Marketing Cloud, Adobe Campaign (AC) also allows to manage delivery contents, landing pages or forms directly in Adobe Experience Manager (AEM). This separation enables marketers to start creating their campaigns and targeting in AC while content creators can work on the design of the offers in AEM.

Like in Adobe Campaign, it will be possible to add personalised fields from a dropdown menu but also target offers!


Jumping to the next level of email personalisation

Right now, there are plenty of companies seeing and leveraging targeted and personalized digital marketing content to engage more users and customers. Yet, if you are one of the stakeholders in a marketing move of this type, you should be aware that this kind of innovation requires the reevaluation of not only the technical solutions you are using but also the auditing of your current organizational setup and available resources. Digital transformation is one of the current key topics and becomes very obvious in executing digital marketing campaigns. Digital marketing channels can no longer be seen and treated as silos, system integration is as well a crucial factor as keeping an eye on change management. Integrated solutions on an enterprise-level are the only way that will effectively help you to be always one step ahead of your competition while being able to follow your customer's expectations. As such, inform yourself about the latest email marketing automation tools and do not hesitate to ask advice from experts.

Source: https://www.netcentric.biz/insights/2015/09/email-personalisation-adobe-campaign.html


By aem4beginner

April 26, 2020
Estimated Post Reading Time ~

Personalization in AEM

Personalization: Personalization is a technique to serve personalized content to each user. It can be based on user logged in by city, country
Here are the steps involved in implementing personalization.

Step1: Create the Segments. (Segments hold the rule to be checked while displaying a teaser on any page)

Go to Tootls > Segmentation



Double click on created segment and configure the test condition to be evaluated. For eg: We are going to display the teaser for ‘Male’ users.

For this, drag the ‘User properties’ from sidekick.



Edit the property to check the condition ‘gender=male’



Now the segment will look as below.



Step2: Create Brand, Campaign Container (Brand holds campaigns, campaign holds teasers)

Once segment is created, Go to > Campaigns (http://localhost:4505/mcmadmin#/content/campaigns)

Create Brand selecting brand template,



Now create Campaigns by selecting campaign template



Step3: Create Teasers
Once campaign is created, go to lists view and create offer page(from aem 6.0). Earlier it was Teaser Page.



Add some text and image to teaser by editing it. Sample is shown below.



Configure the touchpoint properties of teaser by selecting the segment in ‘Advanced section’as shown below.
This is the linking point between segment and teaser.



Now the campaign page looks as below.



Step4: Add Teasers to any page
Now go to the page where you want to display the teaser, drag and drop teaser



Enter the configuration selecting campaign to be run for the page as shown below.



Step5: Test the teaser by setting client context. (Client Context is used to check/modify any user attributes during testing the personalization)



Now go to client context (CTRl+Alt+C) and select a ‘male’ user. You can see the teaser is displayed as shown below.



By aem4beginner

April 25, 2020
Estimated Post Reading Time ~

Personalization in AEM



A couple of months ago, while working for a big client, we got a request to show different data on a page depending on a set of specific user characteristics. Instead of doing a full custom solution, we decided to use AEM’s built-in feature ‘AEM Personalization’ to accomplish this task.

AEM Personalization is an AEM OOTB feature that lets you display different experiences according to the content of your interest. For more information about this (and most of the concepts on this article), visit this link: https://helpx.adobe.com/in/experience-manager/6-2/sites/authoring/using/personalization.html

This article will show a simple tutorial on how to use this feature, finishing with some final thoughts and a conclusion. Enjoy!
ContextHub Configuration

ContextHub is a framework for storing and retrieving client-side data. It provides a JavaScript API that enables the manipulation of the ContextHub. A ContextHub store is a place where you can persist and access data. For this article, I decided to add a new store to the default ContextHub configuration.

Go to http://localhost:4502/etc/cloudsettings/default/contexthub.html

Create a ContextHub Store Configuration



The store type is important because you will need to use the same value when registering the store. In this case, in CRX/DE you can see that it creates the node at the following location: /etc/cloudsettings/default/contexthub/conexio
Custom Store Logic

Another important piece of the puzzle consists in creating all the needed store logic. It uses the ContextHub JavaScript API mentioned before. You will need to create a clientlib in your project to accomplish this.



Use “contexthub.store.X”, where X is the store type you previously configured.



Here you can see a JS example of how to implement your custom store logic. In this case, it calls an internal API to get the user data and then it stores the area in which the user works. Some important notes:
There are some OOTB stores defined in AEM. You can inherit from the one you need so the code can be much simpler and cleaner.
You can see that the store is being registered with the same store type that you configured before.

Audience configuration
The next step is to create the necessary audiences you’d like. A user will belong to either one audience or another depending on the way you defined them.

To accomplish this, first you need to go to http://localhost:4502/libs/cq/personalization/touch-ui/content/audiences.html and select the option “Create a ContextHub Segment”. You need to add a title for the audience and then select “Create”.



For this example, I have created two new audiences: Management and Marketing. Those audiences will represent people that work in this areas respectively. You can see the created nodes in CRX/DE at the following paths:
/etc/segmentation/contexthub/management
/etc/segmentation/contexthub/marketing

After doing this, you’ll need to configure the audiences. I’ll explain how I did it just with Management because for Marketing it’s analogous. First, go to http://localhost:4502/editor.html/etc/segmentation/contexthub/managament.html. Insert the component “Comparison: Property – Value” and configure it as shown below:



It’s important to notice that the property name used is the same as previously used in the JS. Also, it’s important to use the correct store; in this case, it’s called “conexio”.

In the case of the Marketing audience the process is quite similar. The only difference is that you need to select “Marketing” value instead of “Management”.
Brand and Activity Configuration

To start using the above configuration, you’ll need to create your own brand and activity. This will be later used to configure the required components to show personalized data. First, you need to go to http://localhost:4502/libs/cq/personalization/touch-ui/content/activities.html and select the option “Create Brand”. For this example, I have named the brand “Conexio”.

After that, you need to go to http://localhost:4502/libs/cq/personalization/touch-ui/content/activities.html/content/campaigns/conexio/master and select the option “Create Activity”.



For this example, I have named the Activity “Test” and selected the option “ContextHub” for the targeting engine.



Finally, I have mapped the audiences “Management” and “Marketing” to new experiences. So basically, the idea is that, for an activity, all the audiences belonging to it will be mapped to a different experience.
Start Targeting

All the above configuration has been done to finally get to this point. Now, we have all elements to start showing personalized data for the components you want. The first thing you need to do is, for the parent page of your project, configure the ContextHub Path and the Segments Path. It’s important to notice that if you use the default configuration, this step won’t be necessary. Otherwise, it’ll be necessary to do it if you are working with a custom configuration.



In edit mode, you need to select the option “Target” and then the brand and activity that we configured above.



For each component you want to show personalized data for, you’ll need to first select the option Target.



After that, you can configure the component for each audience belonging to the brand and activity that you have chosen above. In addition, you can configure a default case for each user that doesn’t belong to any of the other audiences.

For this example, I’m using an Image component to show different images for each audience.



In the above image, you can see that I have configured the component for the default case.



In the above image, you can see that I have configured the component for the Management audience.

After doing this, you can configure again, if needed, the audiences and the experience names.

Finally, you can configure the duration of the targeting. By default, it will start when activated and end when deactivated. But you also have an option for setting a period.
Conclusion

As you can see, this feature is very powerful and useful but requires a lot of configuration. Hopefully, most of the configuration can be done and versioned by Development team so that, from an author’s perspective, they will only need to do the targeting section of this article. We tried this approach in a past projects and the clients found this very intuitive to use and were very satisfied with the solution.

Another important aspect to mention is the ability of this feature to be used in conjunction with other Adobe products – such as Campaign and Target. For example, you can use Adobe Target as the targeting engine for the activities (instead of ContextHub).


By aem4beginner

April 14, 2020
Estimated Post Reading Time ~

Creating Personalized Adobe Experience Manager Content

You can create a personalized experience for visitors to your web site by using Adobe Experience Manager (AEM). A personalized experience presents the visitor with a tailor-made environment displaying dynamic content that is selected according to their specific needs; be this on the basis of predefined profiles, user selection, or interactive user behavior.
That is, personalized content is what web site visitors want to see. It can be categorized and therefore made available to users according to predefined rules and it must be dynamic; in other words, the content must, in some way, be dependent upon the user. If every user would see the same content, then personalization would be redundant. For example, assume someone interested in Apple products is visiting your web site. You can display images of Mac products as opposed to Windows products.

This article walks you through how to set up a personalized experience using AEM.

To read this article for AEM 5.x, click https://helpx.adobe.com/experience-manager/using/personal.html.

To read this article for AEM 6.1, click https://helpx.adobe.com/experience-manager/using/personal61.html.


By aem4beginner

April 13, 2020
Estimated Post Reading Time ~

Ask the AEM Community Experts for Jan 2017 - Integrating Test and Target with Adobe Experience Manager for Personalization use cases

Join Varun Mitra, Tech Training Instructor, and Developer, Adobe Worldwide Field Enablement as he provides information about using Adobe Experience Manager and Test and Targets together for digital marketing solutions.

In this session, Varun will cover best practices on using AEM and Test and Target to set up a personalization use case.

Date: Thurs January 26, 2017
Time: 11 AM EST

To watch this session, click https://communities.adobeconnect.com/pnrm61xlhed9/?launcher=false&fcsContent=true&pbMode=normal


By aem4beginner

April 7, 2020
Estimated Post Reading Time ~

How to create personalized content in AEM 5.6.1, without coding

You can create a personalized experience for visitors to your web site by using Adobe Experience Manager (AEM). A personalized experience presents the visitor with a tailor-made environment displaying dynamic content that is selected according to their specific needs; be this on the basis of predefined profiles, user selection, or interactive user behavior.

adobe community: https://helpx.adobe.com/experience-manager/using/personal.html


By aem4beginner

April 1, 2020
Estimated Post Reading Time ~

How to use impression Service In CQ/AEM

Use Case:
You often have case where you want to use Impression service provided by CQ to do custom operation for example finding top 10 most viewed page or sorting all page based on there popularity.
It might possible that your impression data (Page Views) is in external system and then you want to import those data as impression in CQ to have more application context.
You want to aggregate all data across all publish instances.
Solutions:

Approach 1:
Creating your Own Impression service

You can create your own impression service by extending com.day.crx.statistics.The entry here is an example

Supporting class

import com.day.crx.statistics.Entry;
import com.day.crx.statistics.PathBuilder;
/**
* Custom Impression Path Builder
* @author Yogesh Upadhyay
*
*/
public class ImpressionsPathBuilder extends PathBuilder {
/** The name of the node that contains the statistical data about a page */
public static final String STATS_NAME = ".stats";

/** The path of the page. */
private final String path;

/** Default constructor */
public ImpressionsPathBuilder(String path) {
super("yyyy/MM/dd");
this.path = path;
}

/**
* Formats the path for a {@link ImpressionsEntry} instance.
*
* @param entry
* a {@link ImpressionsEntry} instance
* @param buffer
* where to write the path to
*/
public void formatPath(Entry entry, StringBuffer buffer) {
MicrositesImpressionEntry pv = (MicrositesImpressionEntry) entry;
buffer.append(pv.getPathPrefix());
buffer.append(path);
buffer.append("/").append(STATS_NAME).append("/");

// add date nodes as specified in constructor pattern
super.formatPath(pv, buffer);
}
}


import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;

import javax.jcr.Item;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.ValueFormatException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.day.crx.statistics.Entry;
import com.day.crx.statistics.PathBuilder;
/**
* Custom Impression Entry
* @author Yogesh Upadhyay
*
*/
public class CustomImpressionEntry extends Entry {
/** default log */
private final Logger log = LoggerFactory.getLogger(getClass());

/** Name of the property that contains the view count */
public static final String VIEWS = "views";

/** Name of the property that contains the rolling week count */
public static final String ROLLING_WEEK_COUNT = "rollingWeekViews";

/** Name of the property that contains the rolling month count */
public static final String ROLLING_MONTH_COUNT = "rollingMonthViews";

/** The page */
private final String pagePath;

private final long count;

public MicrositesImpressionEntry(String pathPrefix, String pagePath,
String date, long count) {
super(pathPrefix);
this.pagePath = pagePath;
DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
try {
this.setTimestamp(format.parse(date).getTime());
} catch (ParseException e) {
log.error("error while parsing date for impressionsentry", e);
}
this.count = count;
}

@Override
protected PathBuilder getPathBuilder() {
return new ImpressionsPathBuilder(pagePath);
}

@Override
public void write(Node node) throws RepositoryException {
log.info("writing impressions node " + node.getPath());

// If Node alredy have count property and it is increment by 1 then
// increment view by 1
if (this.count == 1) {
if (node.hasProperty(VIEWS)) {
long currentCount = node.getProperty(VIEWS).getLong();
node.setProperty(VIEWS, currentCount + 1);
}
} else {
node.setProperty(VIEWS, count);
}

// set month value
Node month = node.getParent();
NodeIterator dayIter = month.getNodes();
long monthCount = 0;
while (dayIter.hasNext()) {
Node tmp = dayIter.nextNode();
if (tmp.hasProperty(VIEWS)) {
monthCount += tmp.getProperty(VIEWS).getLong();

}
}
month.setProperty(VIEWS, monthCount);

// set year value
Node year = month.getParent();
NodeIterator monthIter = year.getNodes();
long yearCount = 0;
while (monthIter.hasNext()) {
Node tmp = monthIter.nextNode();
if (tmp.hasProperty(VIEWS)) {
yearCount += tmp.getProperty(VIEWS).getLong();

}
}
year.setProperty(VIEWS, yearCount);

// set cumulative values for week and month
node.setProperty(ROLLING_WEEK_COUNT, getCumulativeCount(node, 7, VIEWS));
node.setProperty(ROLLING_MONTH_COUNT, getCumulativeCount(node, 30, VIEWS));
}

/**
* Calculates the cumulative view count on the <code>node</code>.
*
* @param node
* the node where to update the cumulative view count
* @param numDays
* the number of days back in time that are cumulated
* @param propertyName
* the name of the count property
* @throws RepositoryException
* if an error occurs while reading or updating.
*/
private long getCumulativeCount(Node node, int numDays, String propName)
throws RepositoryException, ValueFormatException {
long viewCount = 0;
Session session = node.getSession();
PathBuilder builder = getPathBuilder();
Calendar date = Calendar.getInstance();
date.setTimeInMillis(getTimestamp());
CustomImpressionEntry entry = new CustomImpressionEntry(
getPathPrefix(), pagePath, "1970-01-01", 0);
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < numDays; i++) {
// re-use buffer
buffer.setLength(0);

entry.setTimestamp(date.getTimeInMillis());
builder.formatPath(entry, buffer);
String path = buffer.toString();
try {
Item item = session.getItem(path);
if (item.isNode()) {
Node n = (Node) item;
if (n.hasProperty(propName)) {
viewCount += n.getProperty(propName).getLong();
}
}
} catch (PathNotFoundException e) {
// no statistics found for that day
}

// go back one day
date.add(Calendar.DAY_OF_MONTH, -1);
}
return viewCount;
}
}



You need to embed the following dependency for this
<dependency>
    <groupId>com.day.cq</groupId>
    <artifactId>cq-statistics</artifactId>    <scope>provided</scope>
</dependency>


<!--All Other Dependencies -->
Here is an example of how you can use this service

import java.util.Date;

import org.apache.sling.api.resource.Resource;
/**
* Custom Page Impression Service
* @author Yogesh Upadhyay
*
*/
public interface CustomImpressionService {
/**
* Record Impression for a path given a date
* If this method is called multiple time for sam date then value will get overridden
* Date should always be in form of yyyy-MM-DD
* @param resourcePath
* @param date (In form of yyyy-MM-DD)
* @param count
*/
public void recordImpression(String resourcePath, String date, long count);
/**
* Record Impression for a path given a date
* If this method is called multiple time for sam date then value will get overridden
* Date should always be in form of yyyy-MM-DD
* @param resource
* @param date (In form of yyyy-MM-DD)
* @param count
*/
public void recordImpression(Resource resource, String date, long count);
/**
* Record Impression for a path given a date
* If this method is called multiple time for sam date then value will get overridden
* @param resource
* @param date
* @param count
*/
public void recordImpression(Resource resource, Date date, long count);
/**
* Record Impression for a path given a date
* Calling this method for same day will increase count of impression by 1
* @param resource
* @param date
*/
public void recordImpression(Resource resource, Date date);
/**
* Record Impression for a path given a date
* Calling this method for same day will increase count of impression by 1
* Date should be in form of yyyy-MM-DD
* @param resourcePath
* @param date (in form of yyyy-MM-DD)
*/
public void recordImpression(String resourcePath, String date);
/**
* Method that will return formated date for impression
* @param date
* @return formatted date in form of yyyy-MM-DD
*/
public String getFormattedDateForImpression(Date date);
}



import java.text.SimpleDateFormat;
import java.util.Date;

import javax.jcr.RepositoryException;

import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.NonExistingResource;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.day.cq.statistics.StatisticsService;
/**
*
* @author Yogesh Upadhyay
*
*/
@Component
@Service
public class CustomPageImpressionImpl implements CustomImpressionService {

private final Logger log = LoggerFactory.getLogger(getClass());

private static final String STATISTICS_PATH = "/pages";

@Reference
private StatisticsService statisticsService;

@Reference
private ResourceResolverFactory resourceResolverFactory;

private ResourceResolver resourceResolver;

private String statisticsPath;

/**
* Record Impression Method
* It essentially create Impression Entry and add through OOTB service
*/
@Override
public void recordImpression(String resourcePath, String date, long count) {
Resource resource;
ResourceResolver resourceResolver = null;
try {
resourceResolver = getAdminResourceResolver();
resource = resourceResolver.resolve(resourcePath);
if(!(resource instanceof NonExistingResource)){
CustomImpressionEntry customImpressionEntry = new CustomImpressionEntry(statisticsPath, resource.getPath(), date, count);
statisticsService.addEntry(customImpressionEntry);

}
} catch (LoginException e) {
log.error(e.getMessage());
e.printStackTrace();
} catch (RepositoryException e) {
log.error(e.getMessage());
e.printStackTrace();
} finally{
closeResourceResolver(resourceResolver);
}


}

@Override
public void recordImpression(Resource resource, String date, long count) {
if(null!=resource){
recordImpression(resource.getPath(), date,count);
}else{
log.error("Resource Provided is Null ");
}

}

@Override
public void recordImpression(Resource resource, Date date, long count) {
recordImpression(resource, getFormattedDateForImpression(date),count);

}

@Override
public void recordImpression(Resource resource, Date date) {
recordImpression(resource, getFormattedDateForImpression(date),1);

}

@Override
public void recordImpression(String resourcePath, String date) {
recordImpression(resourcePath, date,1);

}

private synchronized ResourceResolver getAdminResourceResolver() throws LoginException{
return resourceResolverFactory.getAdministrativeResourceResolver(null);
}

private synchronized void closeResourceResolver(ResourceResolver resourceResolver){
if(null!=resourceResolver && resourceResolver.isLive()){
resourceResolver.close();
}
}

public String getFormattedDateForImpression(Date date){
if(date!=null){
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
return simpleDateFormat.format(date);
}
return null;
}

@Activate
protected void activate(ComponentContext ctx) {
statisticsPath = statisticsService.getPath() + STATISTICS_PATH;
}

@Deactivate
protected void deactivate(ComponentContext ctx) {
if (resourceResolver != null && resourceResolver.isLive()) {
resourceResolver.close();
}
}

}


Now you can import data from an external system (GA, Site Catalyst, Kafka) and then populate it using this service to your instance.

Once you are ready with all data you can use the following service to use data,

import java.util.Iterator;
import java.util.Set;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
/**
* Custom Impression Provider Service
* @author Yogesh Upadhyay
*
*/
public interface CustomImpressionProvider {
/**
* Get Iterator of all popular resource
* @param root_path
* @param isDeep
* @param num_days
* @return {@link Iterator<Resource>}
*/
public Iterator<Resource> getPopularResource(String root_path, boolean isDeep, int num_days);
/**
* Get Page impression count based on page path
* @param page_path
* @param num_days
* @return
*/
public int getPageImpressionCount(String page_path,int num_days);
/**
* Get most popular Resource based on root path.
* @param root_path
* @param num_days
* @return {@link Resource}
*/
public Resource getMostPopularResource(String root_path,int num_days);
/**
* return set of all popular resources sorted by there impression
* @param root_path
* @param isDeep
* @param num_days
* @param total_count
* @return
*/
public Set<Resource> getPopularResource(String root_path,boolean isDeep,int num_days, int total_count);
/**
* Get Json Output of all popular resource under a path
* Json Output give page path and impression count for all resource under root path sorted by impression count
* @param httpServletRequest
* @param root_path
* @param num_days
* @return
*/
public String getJsonForPopularString(SlingHttpServletRequest httpServletRequest, String root_path,int num_days);
}



Actual Implementation
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;

import javax.jcr.RepositoryException;

import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.NonExistingResource;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.commons.json.JSONException;
import org.apache.sling.commons.json.io.JSONStringer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.day.cq.statistics.StatisticsService;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageFilter;
import com.day.cq.wcm.api.PageManager;
import com.day.cq.wcm.api.PageManagerFactory;
import com.day.cq.wcm.api.WCMMode;
import com.day.cq.wcm.core.stats.PageViewReport;

/**
* Custom Impression Provider implemetation
* @author Yogesh Upadhyay
*
*/
@Component
@Service
public class CustomImpressionProviderImpl implements CustomImpressionProvider {

private final Logger log = LoggerFactory.getLogger(getClass());

protected static final int MAX_RESULT_COUNT = 3000;

@Reference
protected StatisticsService statisticsService;

@Reference
protected ResourceResolverFactory resourceResolverFactory;

protected ResourceResolver resourceResolver;

protected String stat_path;

/**
* Get all popular resources.
* We use admin session for now to get popular resources
* TODO: may be not use admin session to get impression
* TODO: cache result some where so that we don't have to run this all the time.
*/
@Override
public Iterator<Resource> getPopularResource(final String root_path,
final boolean isDeep, final int num_days) {
Map<String, Integer> all_page_impression = null;
Iterator<Resource> popular_resource_iterator = null;
try {
resourceResolver = resourceResolverFactory.getAdministrativeResourceResolver(null);
Map<String, Integer> sorted_map = getpoularResourceMap(resourceResolver, root_path, isDeep, num_days);
Set<Resource> all_popular_resource_set = new HashSet<Resource>();
for (String each_popular_resource : sorted_map.keySet()) {
Resource each_popular_resource_object = resourceResolver.resolve(each_popular_resource);
if (!(each_popular_resource_object instanceof NonExistingResource)) {
all_popular_resource_set.add(each_popular_resource_object);
}
}
popular_resource_iterator = all_popular_resource_set.iterator();

} catch (LoginException e) {
log.error(e.getMessage());
e.printStackTrace();
}finally {
if (null != resourceResolver && resourceResolver.isLive()) {
resourceResolver.close();
}
}

return popular_resource_iterator;
}

/**
* Method to get page impression count
* We use admin session here as well to get count
*/

@Override
public int getPageImpressionCount(final String page_path, final int num_days) {
int total_count = 0;
try {
resourceResolver = resourceResolverFactory.getAdministrativeResourceResolver(null);
total_count = getPageImpressionCount(resourceResolver, page_path, num_days);
} catch (LoginException e) {
log.error(e.getMessage());
e.printStackTrace();
}catch (RepositoryException e) {
log.error(e.getMessage());
}finally{
if(this.resourceResolver!=null && resourceResolver.isLive()){
resourceResolver.close();
}
}
return total_count;
}


/**
* Method to get most popular resource
*/

@Override
public Resource getMostPopularResource(final String root_path,final int num_days) {
Iterator<Resource> most_popular_resource = getPopularResource(root_path, true, num_days);
if(most_popular_resource!=null){
while(most_popular_resource.hasNext()){
return most_popular_resource.next();
}
}
return null;
}

/**
* get popular resource based on total count
*/
@Override
public Set<Resource> getPopularResource(final String root_path,final boolean isDeep, final int num_days, final int total_count) {
Iterator<Resource> popular_resources = getPopularResource(root_path, isDeep, num_days);
Set<Resource> popular_resource_set=null;
if(popular_resources!=null){
int temp_count=0;
popular_resource_set = new HashSet<Resource>();
while(popular_resources.hasNext()){
//If result is more than total count then break
if(temp_count>total_count) break;
popular_resource_set.add(popular_resources.next());
temp_count++;
}
}
return popular_resource_set;
}

/**
* Utility method to get page impression using resource resolver
* @param resourceResolver
* @param page_path
* @param num_days
* @return
* @throws RepositoryException
*/
protected int getPageImpressionCount(ResourceResolver resourceResolver,String page_path, int num_days) throws RepositoryException{
if(null==resourceResolver || StringUtils.isBlank(page_path)){
return 0;
}
Page page = resourceResolver.resolve(page_path).adaptTo(Page.class);
stat_path = statisticsService.getPath() + "/pages";
//use Page view class
PageViewReport pageViewReport = new PageViewReport(stat_path, page,WCMMode.DISABLED);
pageViewReport.setPeriod(30);
//this is were report is ran
Iterator stats = statisticsService.runReport(pageViewReport);
int totalPageViews = 0;
while (stats.hasNext()) {
Object[] res = (Object[]) stats.next();
totalPageViews = totalPageViews + Integer.parseInt(res[1].toString());
}
log.debug("Total page view for path "+page_path+" is "+totalPageViews);
return totalPageViews;
}

/**
* Get Json string using JsonStringer
*/
@Override
public String getJsonForPopularString(SlingHttpServletRequest httpServletRequest, String root_path, int num_days) {
JSONStringer jsonStringer = new JSONStringer();
log.debug("Root path is "+root_path);
jsonStringer.setTidy(true);
try {
jsonStringer.array();
jsonStringer.object().key("rootpath").value(root_path);
jsonStringer.key("num_days").value(num_days).endObject();
Map<String, Integer> all_popular_resource = getpoularResourceMap(httpServletRequest.getResourceResolver(), root_path, true, num_days);
log.debug(all_popular_resource.toString());
for(Entry<String, Integer> each_resource_entry:all_popular_resource.entrySet()){
jsonStringer.object();
jsonStringer.key("path").value(each_resource_entry.getKey());
jsonStringer.key("impression_count").value(each_resource_entry.getValue());
jsonStringer.endObject();
}
jsonStringer.endArray();
} catch (JSONException e) {
log.error(e.getMessage());
e.printStackTrace();
}

return jsonStringer.toString();
}

/**
* Helper method to get sorted map for popular resources
* @param resourceResolver
* @param root_path
* @param isDeep
* @param num_days
* @return
*/
protected Map<String, Integer> getpoularResourceMap(final ResourceResolver resourceResolver, final String root_path,
final boolean isDeep, final int num_days){
Map<String, Integer> sorted_map = null;
Map<String, Integer> all_page_impression = null;
try {
PageManager pageManager = resourceResolver.adaptTo(PageManager.class);
Page page = resourceResolver.resolve(root_path).adaptTo(Page.class);
if(page==null){
log.error("Root path is not present "+root_path);
return null;
}
//get all children including subchildren
Iterator<Page> all_child_pages = page.listChildren(new PageFilter(), isDeep);
all_page_impression = new HashMap<String, Integer>();
int all_result_count = 0;
while (all_child_pages.hasNext()) {
//If result is more than MAX allowed then break. This is to make sure that performance is not imppacted.
if (all_result_count >= MAX_RESULT_COUNT) {
break;
}
Page each_page = all_child_pages.next();
if (null != each_page) {
int totalPageViews = getPageImpressionCount(resourceResolver, each_page.getPath(), num_days);
//Only if page view count is more than 0 consider adding them
if (totalPageViews > 0) {
log.debug("Adding "+each_page.getPath()+" to Map with value "+totalPageViews);
all_page_impression.put(each_page.getPath(), totalPageViews);
}
}
}

// Now create resource
log.debug("Unsorted Popular Map size is "+all_page_impression.size());
//Once we have whole map need to sort them based on impression
ValueComparator valueComparator = new ValueComparator(all_page_impression);
sorted_map = new TreeMap<String, Integer>(valueComparator);
sorted_map.putAll(all_page_impression);
log.debug("Soted Popular Map size is "+sorted_map.size());
}catch (RepositoryException e) {
log.error(e.getMessage());
}
return sorted_map;
}

}

/**
* Helper class to sort map based on impression count
* @author Yogesh Upadhyay
*
*/

class ValueComparator implements Comparator<String> {

Map<String, Integer> base;

public ValueComparator(Map<String, Integer> base) {
this.base = base;
}

// Note: this comparator imposes orderings that are inconsistent with equals.
public int compare(String a, String b) {
if (base.get(a) >= base.get(b)) {
return -1;
} else {
return 1;
} // returning 0 would merge keys
}
}



Approach 2:
You don't want to write your own service as mentioned in Approach 1 and use OOTB service available to you. Only problem with this is, You have multiple publish instance and somehow you want to combine all data in to one so that you get an accurate picture. It kind of tricky to get all data from all publish instance (through reverse replication) and then combine them on author and then push them over again. However you can use one instance to collect all stat data (king of single source of truth and then replicate it back to all instance every day)

Make sure that you enable page view tracking by adding following line

<cq:include script="/libs/foundation/components/page/stats.jsp" />

Then configure all publish instance to point to one DNS using following config (You can always override this under /apps)
/apps/wcm/core/config.publish/com.day.cq.wcm.core.stats.PageViewStatistics
/apps/wcm/core/config.publish/com.day.cq.wcm.core.stats.PageViewStatisticsImpl

make sure that pageviewstatistics.trackingurl is pointing to single domain (You need to create a domain, something like impression.mydomain.com that will be stand alone CQ instance to take all impression request)
Now you have consolidated page impression on one machine
You can easily write a schedular which will run every night and reverse replicate all data to author instance.
Once it is on author instance you can use replicator service to replicate to all other publish instance
Then you can use code mention in approach 1 to get popular resources.

Note: You can always use GA or something to track data. This is more useful if you want to do something internally and not want to share data with GA.


By aem4beginner

March 29, 2020
Estimated Post Reading Time ~

Personalization using ContextHub in AEM 6.4 Part-1

With every upgrade of AEM version, Adobe is trying to improve the capability of AEM. In this blog, we will talk about personalization using contexthub. In AEM 6.2, it was the first time when Adobe introduced contexthub in place of clientcontext.

We will learn personalization with contexthub step by step.
I am going to explain everything on AEM 6.4 here.
In the personalization section of AEM, we can see three options:
  1. Activities
  2. Offers
  3. Audiences
Here we will discuss audiences first but before that we will talk about contexthub stores. So how AEM personalize content using the information of users visiting the websites.AEM store information of user in the browser somewhere on the basis of which personalization can happen.

Where are these contexthub stores exist in browser?
AEM stores the user information in the ContextHubPersistance Object in the local storage.All the OOTB stores like geolocation,recently viewed etc can be seen under ContextHubPersistance object.


ContextHub Store Available in Browser
So now we have place where the user information is getting stored. Now the next step is we need to create audiences for personalization.

Audiences in Personalization
Audiences means some conditions on the basis of which you want to personalize the content. Right now in audiences, there are only two folders global,we-retail.Now I want to add a new project to create new audience(segments).

Go to: Adobe Experience Manager->General->Configuration Browser or go here

Follow the steps to create a demo-project.

Create a project using Configuration Browser

Note: Please create a new project using lower-case with no space(using hyphen) and go to the hierarchy and change the title.,because if you give space it will create the name of the project with some extra characters .

Use Case: Let’s go through the use case first. I want to personalize content on the basis of browsers like Google Chrome, Mozilla etc. So to detect which browser it is, we have a OOTB store called "surferinfo". If you see the browser Family of Google Chrome, it is “Chrome” and for Mozilla Firefox it is Firefox. So let’s create the audiences basis of these two conditions.

Steps to create the audience:
  • Go to personalization console and open audiences, you will be able to see a new project named Demo Project or directly click here. Let's make two segments regarding Chrome and Mozilla Firefox browsers. 
Note: Below image is showing the browser family for both browsers on basis of which we are creating audiences.


Values of SurferInfo Store in Chrome and Firefox Browsers
  • Click on Create
  • Create ContextHub Segment.
  • Add a Title “Any Title(e.g.Chrome Users)“ and Boost value.
  • Now click on segment.Open the newly created segment.
  • There are so many components in the group ContextHub Segmentation.We will discuss about all the components a bit later. First add a component named Comparison:Property-Value.
  • Add the values as shown in figure below

Condition in Contexthub Segment

Property Name: It is the hierarchy where the property exist in ContextHub Persistence Store.

Operator: It is the condition which you wanna put. It has many options like equal, greater-than,greater-than-or-equal,less-than etc.

Data Type: Here you can select data type of your value on which you are comparing. If you don't want to put data type just put as auto-detect.

Value: This is the value of the property on which you want to put some condition.
  • Click OK. If the condition is true the background of component will turn green

Note: It is very important to note here that sometimes background doesn’t get green even if the condition is true. I have gone through this issue. So What to do now.
  • First, you can refresh the page and then check if it turned into green
  • Second, if still the background is not green there is a technical way to check it. If in the segmentation store, you are able to see any segments that means that segment is getting resolved. In other words, only those segments will be visible in segmentation stores which are resolving for that page. 

In the same way create another segment for Mozilla FireFox Browser.
This is how, our audience is ready, Now the next step is creating activities.

Activities: Activities are collecting a same kind of audience(segments) in a brand.
  1. Go to activities and create a brand
  • Add a Title for Brand.
  • Click on Create.

2. 
Under Brand Create an Activity.
  • Add a Title for Activity.
  • Click on Create.


A configuration Activity screen will be visible and need to click on Next.

  • In the Configure Activity Wizard Screen shown below, “Add Experiences” and “Choose Audience” (segments) related to the activity and give a name to fragments and click on next.

  • In the next screen, you can specify the priority of the activity and date-time of start of personalize content which will start showing on a website and end date-time also.



All the prerequisites for personalization is done now it’s time to personalize the page.

To enable personalization on a website, We need to add this line in page component.

<sly data-sly-resource = "${'contexthub' @ resourceType='granite/contexthub/components/contexthub'}"/>

To enable particular segments on a page, you need to go to page properties and add segments path.You can also enable a particular brand from the Brand Configuration.

Page Properties Dialog of the page

Personalize content on a page
Now Go to a page, and click on Targeting mode and select the Brand and Activity been configured in the previous steps and click on Start Targeting.

Choosing Brand and Activity on a page

Now you can see all the audiences configured for that activity. If you want to add more audiences in the particular activity, click on “Add Experience Targeting”.

Different Segments associated with selected activity

Select a component which you want to target and click on target icon It will pop up a message that component is now targeted. Now click on different audience from the right rail,choose the audience and edit the content for that particular audience,after you are done click on next and save.Now you can see the personalize content on the view as published on author instance or publish instance..

Target Icon in a component

Trade Off of Personalization in AEM using ContextHub
The trade off of personalization using contexthub is that there is a lag in loading personalized content on a page.It first load default content on a page and then after some ms it loads personalized content.You can refer demo video to understand this issue more clearly.

When the page gets load it runs contexthub library, resolves the segments and then show personalize,it takes time and that’s the reason this lag is justified. But from the end user perspective, it spoils the user experience also.

Myths of Personalization in AEM using ContextHub
  • Using Personalization only content of the component can be changed not the components itself. For instance, you can’t show “Image Component” for audience 1 and “Text Component” for audience 2
  • You can’t delete one component for audience 2 which was present for audience 1.


By aem4beginner

Steps of Personalization using ContextHub in AEM 6.4: Part - 2

Boost Factor: There is a boost value associated with each contexthub segment.If there are more than one segments gets resolved at the same time then which one should take higher priority.So boost factor decides which segment should resolved and show content. A higher number indicates that the segment will be selected in preference to a segment with 
a lower number.
  • Minimum Value:0
  • Maximum Value:1000000
Segment Engine: Segment Engine allows to build conditions that gets resolved into a Boolean values.List of Components in the ContextHub Segmentation Group:
  • Comparison:Property-Value: Compares a property of a store to a defined value.
  • Comparison: Property-Script Reference: Sometimes to create conditions using Comparison: Property value is not possible.
Example: Let’s suppose you are storing the date when user has visited a particular page, and you want to personalize the content only till 4 days of his/her visit so you can not create such condition using OOTB components.You need to write one script to do this and if script returns true the segment gets resolve.

Defining a Script to Reference:
  • Add file to contexthub.segment-engine.scripts clientlib.
  • Implement a function that returns a value. 
For example: 

(function() {
'use strict';
var testScript = function(val1, val2) {
/* let the SegmentEngine know when script should be re-run */
this.dependOn(ContextHub.SegmentEngine.Property('profile/age'));
this.dependOn(ContextHub.SegmentEngine.Property('profile/givenName'));

var name = ContextHub.get('profile/givenName');
var age = ContextHub.get('profile/age');

/* return result */
return name === 'Joe' && age === 123 && val1 === 11 && val2 === 22;
};

/* register function */
ContextHub.SegmentEngine.ScriptManager.register('test-script', testScript);
})();

Register the script with ContextHub.SegmentEngine.ScriptManager.register.
If the script depends on additional properties, the script should call this.dependOn(). For example if the script depends on profile/age.

Referencing a Script:
  • Create ContextHub segment.
  • Add Script Reference component in the desired place of the segment.
  • Open the edit dialog of the Script Reference component. If properly configured, the script should be available in the Script name drop-down.


Fig 1: Script Reference Component

You can find all the scripts loaded here:
/etc/cloudsettings.kernel.js/libs/settings/cloudsettings/legacy/contexthub

Comparison:Segment Reference : In this component you can make conditions based on referring the segments. Let's take an example, you have already created two segments:
  • The Gender is Female.
  • The age is above 30. 
Now you can make a new segment that checks both conditions gender is female and age is above 30, no need to create it using Comparison:property-Value, you can make it using Segment Reference.


Fig 2: Segment Reference Component

Container AND and Container OR:Using the AND and OR container components, you can construct complex segments in AEM. When doing this, it helps to be aware of a few basic points:

The top level of the definition is always the AND container that is initially created. This cannot be changed, but does not have an effect on the rest of your segment definition.
Ensure that the nesting of your container makes sense. The containers can be viewed as the brackets of your Boolean expression. The following example is used to select visitors who are considered in our prime age group:

Male and between the ages of 30 and 59
OR
Female and between the ages of 30 and 59

You start by placing an OR container component within the default AND container. Within the OR container, you add two AND containers and within both of these you can add the property or reference components.
  • Comparison:Property-Property:In this component, you can compare the value of two properties of different stores or may be same stores.
  • Comparison:Property-Segment Reference:Comparison:Compares a property of a store to another referenced segment.
  • Comparison:Property-Script Reference: Compares a property of a store to the results of a script.
Note: I really don’t know the business use case of three components (Comparison: Property-Property,Comparison:Property-Script Reference,Comparison:Property-Segment Reference).So i just mentioned the definition of components but use case can’t explain.

How to create Offers in AEM using Personalization
In the previous blog, we talk about limitations of Personalization in Contexthub in AEM using component targeting that we can’t change the components for different audiences but using offers this can be achieved also.
  • Go to Offers console from Personalization.
  • Go to Project and click on Create Folder and Offers.
  • Choose Offer Page and Click on Next.
  • Now add the Offer Title and click on Create.

Fig 3: Console for Offer Creation
  • Now open the offer page,you will see an open parsys.You can drop n number of components in the parsys and create an offer page.
  • Now go to the page,select targeting mode then choose brand and activities,select the component and target it.
  • When you choose a particular segment,there is one icon saying “remove offer from Activity” click on it then you will see a placeholder like "Add Offers".

Fig 4: "Remove offer from Activity" option in the component

Then click on icon “choose from offer library” and select the particular offer for that particular segment.

Fig 5: "Add offer" option in the component

This is how you can select offers based on segments and personalized the content.

What are the changes in AEM 6.4 in contextHub
  • AEM 6.4 is using /conf//settings/wcm/segments to store segments
  • AEM 6.4 is using /conf//settings/cloudsettings//contexthub for configurations.
  • sample config moved from /etc/settings/cloudsettings/default/contexthub to /libs/settings/cloudsettings/legacy/contexthub
  • sample segments moved from /etc/segmentation/contexthub to /conf/we-retail/settings/wcm/segments.
  • segment generation and resolving performance improvements (/path/to/segments.seg.js)
Testing the Application of a segment in contextHub
  • Preview a Page.
  • Click the ContextHub icon to reveal the ContextHub toolbar.
  • Select a persona that matches the segment you created.
  • The ContextHub will resolve the applicable segments for the selected persona.

Fig 6: Steps for Testing the Segments in ContextHub

Once the segment has been defined, potential results can be tested with the help of the
ContextHub.

So this is all about OOTB functionalities of Personalization in AEM.In the upcoming blogs, we will talk about more about custom stores and modules.So stay tuned with more upcoming blogs of personalization.


By aem4beginner