Monday, September 26, 2022

Copy Sitecore User Roles from One User to Another User using Sitecore PowerShell

I recently had received a request to add several new Content Author accounts to the CMS and apply for the exact user roles from a specific user to the new users.  I ended up scripting this with Sitecore PowerShell Extensions using hardcoded variables (usernames and roles), but because I expect similar requests in the future, I wanted a quick way to copy roles from one user account to another to use in the future.  Since I couldn't find a script readily available to do this, I developed my own. 

The script utilizes the 'user' editor type (which doesn't happen to be listed on https://doc.sitecorepowershell.com/interfaces/interactive-dialogs - yet still works) within an interactive 'Show-ModalDialog.' It allows you to select a "source user" and a "target user" from the list of all available user accounts.  After choosing both, the script iterates through each source user's roles and applies the target user to each user role. 

The script loads the modal and requires the user to select a Source User and Target User.

Clicking the ellipses button opens a separate dialog that lists all users. 



After selections are made, click OK to initiate the copying of roles. 

The script can be run in Sitecore PowerShell ISE or configured as a module that can be run from the Start Menu.

Happy scripting! 🦸‍♂️

Monday, August 1, 2022

Sitecore Symposium 2022 in Chicago - Tips from an Illinois Native

I'm so excited for Sitecore Symposium 2022 to be hosted in Chicago, the city I grew up and work in! I can't wait to connect with the Sitecore community (in-person 😀), and share some of the city's many offerings with those visiting from out of town. Breaking from my typical technical-centric posts, I wanted this one to contain some of my own tips and recommendations to help prepare and enhance your trip to Chicago this autumn.

Credit: Chait Goli

🚖 Getting Around

Which airport you land in may shape your experience and the cost of transportation to make it into the city. Chicago has two major international airports:

  • O'Hare International Airport
  • Midway International Airport

O'Hare, the larger (and arguably the 'nicer' airport) of the two (~20.5 miles from McCormick Place), is between 26 - 45 minutes (and up to 90 minutes during peak rush hour) to and from Loop, depending on the time of day.  



Midway is closer to the Loop (~10 miles from McCormick Place), coming in at about a 14 - 28 minute drive to/from McCormick Place. 



While you could reach your hotel and McCormick Place through a series of trains and busses, I highly recommend taking an Uber/Lyft or taxi for convenience and speed. Taxis and rideshares are also the way to go if you decide to venture away from the McCormick Place grounds.

🏢 McCormick Place

Located in the "South Loop", McCormick Place is the largest convention center in North America. 

It's absolutely massive and regularly used for annual conventions like the Chicago Auto Show, International Manufacturing Technology Show, Chicago Comic & Entertainment Expo (C2E2), and International Home & Housewares Show. 

From their website:
With over 2.6 million square feet of exhibit space, McCormick Place is the largest and most flexible use convention center in North America. Located on Chicago’s lakefront, just minutes from downtown and facing one of the most iconic skylines in the world, the McCormick Place campus provides world-class facilities that can accommodate just about any event.
You can take a virtual tour ahead of Symposium to help familiarize yourself here: McCormick Place 360° Virtual Tour | Choose Chicago

🍂 Chicago October Weather

Sitecore Symposium 2022 will run in Chicago during mid/late October. While Symposium itself will take place in a temperature-controlled McCormick Place where comfort will be a lesser concern, if and when you venture out into the city you don't want to get caught off-guard by the weather. It could be cloudy and chilly, sunny and warm, windy (particularly closer to the lake), possibly rainy, or a slim chance of snow flurries - all in the same week!

I'm personally hoping for unseasonably high temperatures, but freezing temperatures are not uncommon by mid/late October, so don't forget to pack appropriately! 😇

Here's what you can expect in terms of weather on average during the third week of October: 
  • Sunrise: ~7 AM 🌅
  • Sunset: ~6 PM 🌆
  • High Temp: 62° F (17° C) 🌞
  • Low Temp: 45° F (7° C) 🌚
  • Average Temp: 53° F (12° C) 👍
  • Daily wind average: 12.4 mph (19.9 km/h) 💨 

🌆 Essential Chicago Loop Spots to Visit 

Chicago is a vibrant city with much to offer, and (contrary to the above paragraph) October is a great time to visit.  Cooler temparetures and shorter days mean you can explore without getting too hot or exhausted. At the same time, it's still usually warm enough to enjoy outdoor activities and see the sights if you want to explore beyond McCormick Place. Here are my top picks for first-time visitors.

Travel times are approximate from McCormick Place *

Millennium Park 

Credit: Bhargava Marripati

Millennium Park is located on Michigan Avenue between Washington Street and Columbus Drive. 



This park is home to some of Chicago's most iconic art installations, including Cloud Gate (a.k.a. the "Bean") and Crown Fountain, an interactive work of art and video sculpture. 


  • 💵 Price: Free 
  • 🚕 Uber/Lyft/Taxi: ~ 8 to 10 min ($10 - $25)
  • 🚶‍♀️ Walk: 45 min 

Buckingham Fountain

Credit: Petr Kratochvil
Buckingham Fountain (or The Clarence Buckingham Memorial Fountain as it is formally known) is located in the center of Grant Park between Queen's Landing and Ida B. Wells Drive, just an 8-10 minute walk southeast of Millenium Park.



Built in 1927, Buckingham Fountain is one of the largest fountains in the world and remains one of the most popular spots in Chicago for tourists and locals alike due to its stunning beauty and incredible views of Lake Michigan and the city skyline.

The fountain is typically operated from early May through mid-October, depending on the weather, so there is a chance that the water may not be flowing. That's OK! The views of the city and lake from the fountain grounds are spectacular and worth the visit. 

  • 💵 Price: Free 
  • 🚕 Uber/Lyft/Taxi: ~8 to 10 min ($10 - $25)
  • 🚶‍♀️ Walk: 45 min 

The Skydeck at Willis Tower 


Credit: Jared VanderMeer
Credit: Amit Thakral

Once holding the title of the largest building in the world, the Willis Tower (formally Sears Tower - which natives still often refer to it as) is one of the most iconic sights in Chicago. It's located on the western edge of Chicago's Loop.




The SkyDeck / The Ledge , after a quick 90-second elevator ride (that's 16 feet per second!), presents a breath-taking 360-degree view of The Windy City from 1,353 feet up in the air.

Given that this is one of Chicago's most popular tourist attractions, it's highly recommended that you purchase tickets in advance. They offer a couple of pricing options that range from $35 (Skydeck General Admission) to $64 (Skydeck Expedited Entry).

  • 💵 Price: $35 - $65 
  • 🚕 Uber/Lyft/Taxi: ~8 to 10 min  ($11 - $25)
  • 🚶‍♀️ Walk: 1 hour

Navy Pier 

Credit: Pixabay

A bit farther from McCormick (but well worth the 20-minute rideshare or taxi ride), Navy Pier is one of Chicago's top attractions, for good reasons. From its many restaurants and bars (including the iconic Giordanos and their deep-dish pizza) to the Centennial Wheel in Pier Park that offers a year-round bird's eye view of Lake Michigan and the skyline, Navy Pier has a little something for everyone.



  • 💵 Price: Free
  • 🚕 Uber/Lyft/Taxi: 15 to 20 min ($15 - $30)
  • 🚶‍♀️ Walk: 🚷

Guided Tours 

If you plan to spend a few extra days here before or after Symposium and have some proper downtime to explore the city, consider a guided tour to soak in the sights. You can opt for walking, biking, Segway, double-decker bus, or boat tours. Whether you're into ghosts (it is 'Spooky Season,' after all), food, art, architecture, or sports, there's sure to be a tour for you. You can find more information about tour options here: 

Walking Tours

Segway Tours

Bus Tours

Boat


Final Thoughts

There is certainly no shortage of famous sights and hidden gems to explore throughout Chicago, so plan ahead to ensure you don't miss out on all this beautiful city has to offer. From the great (freeeee!) public spaces to the many incredible museums, restaurants, art installations, and historical sites, you're bound to find something that fits your taste, budget, and schedule. 

For more advice while in town, feel free to reach out. 

See you there!

Thursday, July 7, 2022

Sitecore Icon Search 2022 Web App and Extensions Updates + Microsoft Edge Addon v1.0.0 Release

It's been 671 days since the last update to the Sitecore Icon Search web app and 1,278 days since my last update to the Chrome and Firefox extensions. I've made updates to squash new bugs across the Sitecore Icon Search ecosystem that have surfaced since the previous release while mitigating the extensions' delisting at the beginning of 2023. 

WebApp: Sitecore.com dependency migration. 


I recently visited sitecoreicons.com only to find that all icon images failed to load. 😢

For context, by default, the app has always used www.sitecore.com to source the icon images themselves since sitecore.com happens to be a Sitecore site. Given how lazy loading is implemented in the data table, I always expected the strain on www.sitecore.com to have remained minimal.  

It turns out Sitecore has recently implemented DDOS security protection via Cloudflare. You might notice that if you go to www.sitecore.com, you'll land on a screen that looks like this before you are redirected to the site:



Due to this change, it left sitecoreicons.com imageless.   The latest updates to the Web app resolves this by hosting the icon images within the application - cutting out the dependency on www.sitecore.com completely. The browser extensions have also been updated to source icon images from sitecoreicons.com


Chrome Extension: Critical Manifest Updates to avoid looming January 2023 Deadline

The Sitecore Icon Search Chrome extension is actively used by ~500 active users. When I originally built the extension, the latest 'manifest' version (a JSON file containing information that defines the extension) was v2.  

Google has provided two critical dates as manifest v2 is deprecated and replaced entirely with v3.  

January 17, 2022: New Manifest V2 extensions will no longer be accepted by the Chrome Web Store. Developers may still push updates to existing Manifest V2 extensions, but no new Manifest V2 items may be submitted.

January 2023: The Chrome browser will no longer run Manifest V2 extensions. Developers may no longer push updates to existing Manifest V2 extensions.

This means that the Sitecore Icon Search Chome extension will no longer function after January 2023 and, in its current state, cannot be updated until upgraded. The latest release brings the extension's manifest version up to version 3 and provides code fixes for several deprecated APIs that surfaced with the upgrade. 


By default, Chrome should update to the latest version automatically. 


Firefox Add-on: Revival

Back in December 2021, I received an email from the Mozilla team regarding a policy update that I, unfortunately, hadn't found time to respond to.   This results in version 1.0.0 of the Firefox add-on being removed 😢


Version 2.0.0 of the Firefox addon contains all the latest updates made for the WebApp and the Chrome extension and resolves the policy issues. I'm actively awaiting the add-on approval process to conclude and will update this portion of this blog post once the extension is available.   


UI and UX enhancements 

While I was making some updates, I at least decided to have a little fun. I've updated some fonts and scattered emojis across the app. I've also included some phrases catered to Gen-Z Sitecore developers. 😁






Microsoft Edge Addon v1.0.0 Release

While Chrome Extensions can already be added to Microsoft Edge by toggling on a feature, however, to avoid requiring Edge users to jump through hoops, I've decided to port and release the Microsoft Edge version of the browser extension. Version 1.0.0 can be downloaded on the Microsoft Add-ons library.



Monday, May 9, 2022

Latest Azure PaaS Sitecore Logs using a single line of PowerShell

If you’re anything like me, you probably don’t have a passion for manually digging through the series of hundreds of randomly dated folders that look like this in search of the latest Sitecore logs:


Hello darkness, my old friend

Although several tools and approaches are available (including this nifty tool credited to fellow Sitecore MVP Kiran Patil - as well as some of my own previous posts from 2018 and 2019 covering this topic), I've recently adopted a different strategy that's proved to be successful across several Sitecore PaaS clients for quickly obtaining the latest physical Sitecore log for a given server. 

The post-worthy kicker?  It's one line of PowerShell:

$kuduHost = "https://yourazuresitename-xp2-cd.scm.azurewebsites.net"; Write-Output "`n[ LATEST SITECORE LOGS ]`n"; $array = @(); Get-ChildItem "C:\home\site\wwwroot\app_data\logs" -File -Recurse | Where-Object { $_.FullName -match "azure.*.txt" -and $_.LastWriteTime -gt (Get-Date).AddHours(-12) } | ForEach-Object { $path = $_.FullName.replace("C:\home\site\wwwroot\app_data\logs\", "$kuduHost/api/vfs/site/wwwroot/App_Data/logs/"); $array += "`n[$($_.LastWriteTime)]`n$path`n"}; $array | Sort-Object $_.LastWriteTime | Select-Object -Last 3

😬

Okay, it's...kind of a long one-liner...but one line nevertheless 

The above example outputs direct links to the latest three physical Sitecore log files, which match the pattern 'azure.*.

In practice, the desired file can be highlighted from the console and at which point you can copy the URL or open it in a new tab.



Let's break it down

The first line defines a variable for the KUDU host you're using:

$kuduHost = "https://yourazuresitename-xp2-cd.scm.azurewebsites.net"

The second line outputs a (wholly arbitrary and unnecessary) title:

Write-Output "`n`[ LATEST SITECORE LOGS ]`n"

The third line represents an array variable aptly named `$array` (because I'm clever):

$array = @();

This is where it gets exciting. This Get-ChildItem cmdlet gets all files recursively under the site's `\App_Data\logs` location:

Get-ChildItem "C:\home\site\wwwroot\app_data\logs" -File -Recurse
Neat!

We can pipe in a Where-Object cmdlet to filter only file names that match 'azure.*.txt' (or if you want all log types - Publishing, Crawling, Dianoga, SPE, etc. - *.txt) and provide a 12-hour threshold against the `LastWriteTime` property:

Get-ChildItem "C:\home\site\wwwroot\app_data\logs" -File -Recurse |
Where-Object {$_.FullName -match "azure.*.txt" -and $_.LastWriteTime -gt (Get-Date).AddHours(-12)}

We can then pipe in a ForEach-Object cmdlet to iterate through each file:

Get-ChildItem "C:\home\site\wwwroot\app_data\logs" -File -Recurse | Where-Object { $_.FullName -match "azure.*.txt" -and $_.LastWriteTime -gt (Get-Date).AddHours(-12) } | ForEach-Object { $path = $_.FullName.replace("C:\home\site\wwwroot\app_data\logs\", "$kuduHost/api/vfs/site/wwwroot/App_Data/logs/"); $array += "[$($_.LastWriteTime)]`n$path`n"}

Notice that in the ForEach-Object cmdlet, we create a variable called `$path` and set it to a string that takes the file's FullName and replaces the 'system path' portion, and replace it with our `$kuduHost` variable concatenated to `/api/vfs/site/wwwroot/App_Data/logs/`.

$path = $_.FullName.replace("C:\home\site\wwwroot\app_data\logs\", "$kuduHost/api/vfs/site/wwwroot/App_Data/logs/")

Without this string replacement, we'd only get the system path for the files in the dataset, which would still require navigating to the file manually:

Also, within the ForEach-Object cmdlet, a formatted string containing the LastWriteTime and the `$path` variable is added to the `$array` variable:

$array += "[$($_.LastWriteTime)]`n$path`n"

The `n used above allows for line breaks.

After the files have been processed, the `$array` variable is called and sorted by LastWriteTime.

A Select-Object cmdlet is piped in to limit the number of results to 3:

$array | Sort-Object $_.LastWriteTime | Select-Object -Last 3


By combining these together, eliminating spaces, and adding semi-colons to separate commands, we've got our one-liner! 🕺

Bonus: IIS HTTP Request Logs

Using the same approach with a few modifications, the application's raw IIS HTTP Request Logs can also be obtained (differences bolded below):

$kuduHost = "https://yourazuresitename-xp2-cm.scm.azurewebsites.net"; Write-Output "`n[ LATEST IIS LOGS ]`n"; $array = @(); Get-ChildItem "C:\home\LogFiles\http\RawLogs" -Recurse | Where-Object { $_.FullName -match ".log" -and $_.LastWriteTime -gt (Get-Date).AddHours(-12) } | Sort-Object $_.LastWriteTime | ForEach-Object { $path = $_.FullName.replace("C:\home\LogFiles\http\RawLogs\", "$kuduHost/api/vfs/LogFiles/http/RawLogs/"); $array += "[$($_.LastWriteTime)]`n$path`n"}; $array | Sort-Object $_.LastWriteTime | Select-Object -Last 3


Final Thoughts

You can generate variations of this one-liner by changing the various variables, which can be shared with the rest of your development/troubleshooting team and readily ready to copy from an internal Wiki:

Feel free to use and modify the script as you see fit. 🚀

Friday, April 1, 2022

'Tracker.Current.Session.Interaction should not be null' CountryCondition

Uh oh. 

The following errors are heavily present on my client's content delivery servers:

ERROR Evaluation of condition failed. Rule item ID:
Unknown, condition item ID: {9A4BEB4B-4B0F-4392-A798-124CEC8AADA4}

Exception: Sitecore.Framework.Conditions.PostconditionException

Message: Postcondition 'Tracker.Current.Session.Interaction should not be null' failed.

Source: Sitecore.Framework.Conditions
at Sitecore.Framework.Conditions.EnsuresValidator`1.ThrowExceptionCore(String condition, String additionalMessage, ConstraintViolationType type)
at Sitecore.Framework.Conditions.Throw.ValueShouldNotBeNull[T](ConditionValidator`1 validator, String conditionDescription)
at Sitecore.Framework.Conditions.ValidatorExtensions.IsNotNull[T](ConditionValidator`1 validator)
at Sitecore.ContentTesting.Rules.Conditions.CountryCondition`1.Execute(T ruleContext)
at Sitecore.Rules.Conditions.WhenCondition`1.Evaluate(T ruleContext, RuleStack stack)
at Sitecore.Rules.Conditions.OrCondition`1.Evaluate(T ruleContext, RuleStack stack)
at Sitecore.Rules.Conditions.WhenRule`1.Execute(T ruleContext)
at Sitecore.Rules.Conditions.WhenCondition`1.Evaluate(T ruleContext, RuleStack stack)
at Sitecore.Rules.RuleList`1.Run(T ruleContext, Boolean stopOnFirstMatching, Int32& executedRulesCount)

We're talking thousands upon thousands of these errors. 

The error references a GUID we can use to determine the context of this:
{9A4BEB4B-4B0F-4392-A798-124CEC8AADA4}
Moreover, the error's message hints are what ultimately causes the error to trigger: 
Tracker.Current.Session.Interaction should not be null
For context, the GUID refers to the following out-of-the-box Sitecore item:
/sitecore/system/Settings/Rules/Definitions/Elements/Predefined Rules/Predefined Rule
Nothing to see here...


After digging in, I found that the client had configured personalization on a Controller Rendering for their site's homepage component rendering so that depending on the user’s country, unique content is presented. They've implemented the following Predefined rules on the rendering:

- visitor is located in the Asia Pacific region
- visitor is located in the UK
    - or where visitor is equal to the United Kingdom
- where visitor is located in Continental Europe

These are custom rules they've added that categorize a list of countries into a region they want to target.

For example, the `visitor is located in the Asia Pacific region` looks like this:
where the country is equal to Australia
or where the country is equal to Brunei Darussalam
or where the country is equal to China
or where the country is equal to Hong Kong
or where the country is equal to Singapore
or where the country is equal to Taiwan
or where the country is equal to Thailand
or where the country is equal to New Zealand
or where the country is equal to Qatar
or where the country is equal to Turkey
or where the country is equal to Malaysia
or where the country is equal to Russian Federation
or where the country is equal to India
or where the country is equal to Indonesia
or where the country is equal to United Arab Emirates
or where the country is equal to Philippines
or where the country is equal to Israel
or where the country is equal to Pakistan
or where the country is equal to Saudi Arabia
or where the country is equal to Vietnam
or where the country is equal to Japan

Each country has been defined using the out-of-the-box `GeoIP` > `where the country compares to specific countries` condition template.

This line in the error denotes that the `CountryCondition` is the culprit in regards to the recurring error:
at Sitecore.ContentTesting.Rules.Conditions.CountryCondition`1.Execute(T ruleContext)
According to a reflection of the `Sitecore.ContentTesting.Rules.Conditions` namespace in Sitecore.ContentTesting.dll for v10.0.0, this series of checks in the first few lines of the `CountryCondition<T> : VisitCondition<T> where T : RuleContext` class's Execute() method:

[GIST_START]
[GIST_END]

My hunch was that the last check was causing the failure, and instead of failing silently, it throws an error - specifically often in cases where bot-like traffic is involved. 
Condition.Ensures<CurrentInteraction>(Tracker.Current.Session.Interaction, "Tracker.Current.Session.Interaction").IsNotNull<CurrentInteraction>();

To quell the flood of errors initially, I applied a new stand-alone rule preceding each existing visitor is rule located in {REGION_NAME} region called `visitor is human` (also out-of-the-box) to attempt to deflect bots and spam where a country code or identifiable contact/interaction is unlikely to yield a country (which would result in an error):

After applying these changes and publishing them, we see those errors disappear entirely from the error logs.  However, the introduction of the `visitor is human` rule proceeding the rest had an unintended effect: first-time visitors always failed this first condition, causing them to only see the personalized rendering after reloading the page. Removing the rule enabled users to receive the personalized rendering on the first load but reintroduced the flood of errors.

It seemed that instances where `Tracker.Current.Session.Interaction` is null may be caused by bot traffic according to: https://support.sitecore.com/kb?id=kb_article_view&sysparm_article=KB0960279

Given that this is version 10.0.0, this particular bug should have been resolved in all versions above 8.1.

Upon decompiling and comparing the GetTracker : CreateTrackerProcessor > Process() override in Sitecore.Support.424667.81.dll for v8.1+ to the 10.0.0 Sitecore.Analytics.dll's GetTracker : CreateTrackerProcessor > Process method, there's one notable difference:

v8.1+


v10.0.0

It was unclear if we need a similar patch to implement `args.set_Tracker(new DefaultTracker());` in the Process() method.

I raised the question with Sitecore Support to explore one of two resolutions:

1) A patch for suppressing errors in Sitecore.ContentTesting.Rules.Conditions.CountryCondition's Execute() method, when the `Condition.Ensures<CurrentInteraction>(Tracker.Current.Session.Interaction, "Tracker.Current.Session.Interaction").IsNotNull<CurrentInteraction>();` is not satisfied (perhaps by returning false).

or

2) An override patch for the tracker creation similar to the https://support.sitecore.com/kb?id=kb_article_view&sysparm_article=KB0960279 hotfix.

Sitecore Support was able to reproduce the reported behavior in a clean local instance of Sitecore 10.0.0 (Initial Release) without involving any of our specific customizations. They registered the issue as a bug in their bug tracking system (future reference number 456739).

Sitecore's official statement regarding the bug: 
The reported behavior occurs because Sitecore.Rules.Conditions.WhenRule class inherits CanEvaluate() method from Sitecore.Rules.Conditions.RuleCondition class which always returns true. That is why the rule is not skipped so the evaluation continues and leads to the exception. 
In order to workaround the issue, please try to avoid the usage of Conditional renderings rules.
Please assign conditions to your renderings directly without wrapping them into conditional renderings rule.
 
In this case, Sitecore will trigger CanEvaluate method related to the specific condition, so the condition will be skipped.

I ended up pursuing the following approach myself:

  1. Decompiled the `Sitecore.ContentTesting.dll` and copied the Sitecore.ContentTesting.Rules.Conditions.CountryCondition class into our solution called `CountryConditionOverride`.  I used Telerik JustDecompile for this. 

  2. Commented the offending line (48) and replaced it with a simple null check that returns false.

  3. Enabled the custom override class by updating the `Type` field on the following default Sitecore item and published it:
/sitecore/system/Settings/Rules/Definitions/Elements/GeoIP/Country
{52E42C59-7210-43E5-94A6-3EA6B98835B8}
Field: Script (section) > Type 
Old value:  `Sitecore.ContentTesting.Rules.Conditions.CountryCondition,Sitecore.ContentTesting`
New value: `Foundation.Overrides.Rules.Conditions.CountryConditionOverride, Foundation.Overrides`

The final override class:


This resolves the issue with erroneous error log entries while retaining the expected Country condition functionality when the `Tracker.Current.Session.Interaction` happens to not be null.

I've shared my resolution with Sitecore Support and will keep an eye on an official hotfix (reference 456739), which I will share once it's available. 

Hopefully, in the meantime, this helps someone out there in a similar scenario!